learning_ai_invt_trdg/web/src/App.tsx
Saravana Achu Mac 938ed86044 feat: live data wiring (Alpaca/FMP) + strategy builder + screener
Wires the new dashboard to real market data and adds the strategy
builder & screener UIs that were stubbed in the previous commit.

Frontend (web/src/):
- lib/marketApi.ts: authenticated fetch helpers for chart bars,
  market indices, news, and FMP research endpoints
- views/HomeView.tsx: StockChart now fetches live OHLCV via
  fetchChartBars on symbol/period change with loading/error states;
  ResearchCards replaces the static placeholder with live FMP
  profile/metrics/earnings (next-earnings + last 3 historical)
- components/layout/Header.tsx: live SPY/DIA/QQQ price + change%
  via fetchMarketIndices, refreshing every 60s; removed unused
  static sparkline placeholder
- components/strategy/VisualRuleBuilder.tsx: drag-and-drop IF/THEN
  rule composer using @dnd-kit (RSI/MACD/EMA/Price/Volume,
  above/below/crosses, BUY/SELL with shares or % of capital);
  saves via POST /api/profiles
- components/strategy/CodeStrategyEditor.tsx: Monaco editor with
  JS strategy template; "Run Backtest" posts to /api/backtest and
  renders return/win-rate/Sharpe/drawdown plus trade log
- views/ResearchView.tsx: adds "Visual Builder" and "Code Editor"
  sub-tabs alongside Strategies / Signals / Backtesting
- views/ScreenerView.tsx: live FMP screener with market-cap and
  sector filters, sortable columns, click-to-load-symbol routing
- index.css: light theme background; @keyframes spin for loaders
- App.dom.test.tsx: rewritten for router-based AppShell (was
  asserting on the removed tab UI; fixes 5 prior failures)

Backend (backend/src/services/apiServer.ts):
- /api/chart/bars: detects crypto symbols (contains "/") and
  routes to Alpaca v1beta3/crypto/us/bars; equities use
  v2/stocks/{symbol}/bars with iex feed
- (existing) /api/news, /api/market/indices, /api/research/{
  profile,metrics,earnings}, /api/screener proxy endpoints

Build/config:
- web/vite.config.ts: dedupe react / react/jsx-runtime /
  react-router-dom so the vendored react-auth dist resolves the
  same React instance (fixes "Cannot resolve react/jsx-runtime"
  Rollup error)
- web/tsconfig.app.json: exclude shared/platform-clients.ts and
  shared/platform-mobile.ts (mobile-only, missing RN SDK)
- web/package.json: add react-router-dom, @monaco-editor/react,
  @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities

Verification: `npm run build` in web/ → clean (✓ built in 3s);
backend tsc --noEmit → clean. Test suite: 151/155 pass; the 4
remaining failures are pre-existing (3 useTabFeatureFlags module
cache leaks, 1 EntryForm), not introduced here.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 06:16:46 -07:00

198 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { useWebSocket } from './hooks/useWebSocket';
import { useAuth } from './components/AuthContext';
import { Login } from './components/Login';
import { ResetPassword } from './components/ResetPassword';
import { ChatControl } from './components/ChatControl';
import { AppContext } from './context/AppContext';
import { AppShell } from './components/layout/AppShell';
import { useBacktestFeatureGate } from './backtest/useBacktestFeatureGate';
import { useTabFeatureFlags } from './hooks/useTabFeatureFlags';
import { tradingRuntime, tradingTelemetry } from './lib/runtime';
import { createTradeProfile, fetchTradeProfiles, updateTradeProfile } from './lib/profileApi';
// ─── Helpers (preserved from original App.tsx) ───────────────────────────────
export const resolveProfileNameForAction = (
action: string,
requestedName: string | undefined,
chatProfiles: Array<{ name?: string }>,
suffixProvider: () => string = () =>
new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }).replace(/:/g, '')
) => {
let profileName = requestedName || 'AI Profile';
if (action === 'create_profile') {
const existing = chatProfiles.find(p => p.name === profileName);
if (existing) profileName = `${profileName} (${suffixProvider()})`;
}
return profileName;
};
export const buildChatApplyPayload = (
profileData: any,
currentUserId: string,
profileName: string
) => ({
name: profileName,
user_id: currentUserId,
allocated_capital: Number(profileData.allocated_capital || 1000),
risk_per_trade_percent: Number(profileData.risk_per_trade_percent || 1),
symbols: profileData.symbols || 'BTC/USDT',
is_active: profileData.is_active ?? true,
strategy_config: profileData.strategy_config,
});
// ─── App ─────────────────────────────────────────────────────────────────────
function App() {
const { user, profile, loading, signOut } = useAuth();
const { socket, botState, connected } = useWebSocket(tradingRuntime.tradingApiUrl);
const [activeSymbol, setActiveSymbol] = useState('');
const [chatProfiles, setChatProfiles] = useState<any[]>([]);
const [previewAsCustomer] = useState(false);
const { enabled: backtestEnabledForView, loading: backtestGateLoading } =
useBacktestFeatureGate({ previewAsCustomer });
const { flags: tabFlags } = useTabFeatureFlags();
// Feature gates
const isAdminAccount = profile?.role === 'admin';
const isAdmin = isAdminAccount && !previewAsCustomer;
const showBacktestTab = isAdmin || (!backtestGateLoading && backtestEnabledForView);
const showMarketplaceTab = isAdmin || tabFlags.marketplace;
// Critical system events (for the alert banner)
const recentCriticalEvents = (botState.operationalEvents ?? []).filter(e =>
(e.severity === 'ERROR' || e.severity === 'WARN') &&
Date.now() - e.timestamp < 600_000
);
const hasCriticalEvents = recentCriticalEvents.length > 0;
// Chat profile management
const fetchChatProfiles = useCallback(async () => {
const data = await fetchTradeProfiles();
setChatProfiles(data ?? []);
}, []);
useEffect(() => {
if (user) {
fetchChatProfiles();
const id = setInterval(fetchChatProfiles, 30_000);
return () => clearInterval(id);
}
}, [user, fetchChatProfiles]);
const handleChatApply = async (
action: string,
profileData: any
): Promise<{ success: boolean; error?: string }> => {
const currentUserId = user?.id;
if (!currentUserId) return { success: false, error: 'Not authenticated' };
const profileName = resolveProfileNameForAction(action, profileData.name, chatProfiles);
const payload = buildChatApplyPayload(profileData, currentUserId, profileName);
if (action === 'create_profile') {
try {
await createTradeProfile(payload);
fetchChatProfiles();
window.dispatchEvent(new Event('profiles-updated'));
return { success: true };
} catch (err: any) {
return { success: false, error: err.message };
}
}
if (action === 'update_profile' && profileData.id) {
try {
await updateTradeProfile(profileData.id, payload);
fetchChatProfiles();
window.dispatchEvent(new Event('profiles-updated'));
return { success: true };
} catch (err: any) {
return { success: false, error: err.message };
}
}
return { success: false, error: 'Unknown action' };
};
const handleSignOut = async () => {
tradingTelemetry.client.trackEvent('info', 'auth', 'trading_web_sign_out', {
userId: user?.id ?? 'anonymous',
feature: 'sign_out',
tags: { surface: 'web' },
});
await signOut();
};
// ── Auth gates ──────────────────────────────────────────────────────────────
if (window.location.pathname === '/reset-callback') return <ResetPassword />;
if (loading) {
return (
<div style={{
display: 'flex', justifyContent: 'center', alignItems: 'center',
height: '100vh', background: '#F3F4F6', color: '#374151',
fontSize: 15, fontFamily: 'Inter, system-ui, sans-serif',
}}>
Loading
</div>
);
}
if (!user) return <Login />;
// ── Render ──────────────────────────────────────────────────────────────────
return (
<BrowserRouter>
<AppContext.Provider value={{
botState,
socket,
connected,
activeSymbol,
setActiveSymbol,
isAdmin,
user,
profile,
showBacktestTab,
showMarketplaceTab,
handleSignOut,
}}>
{/* Critical system alert banner */}
{hasCriticalEvents && (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 9999,
background: 'linear-gradient(90deg, #991b1b 0%, #dc2626 50%, #991b1b 100%)',
color: '#fff',
padding: '6px 20px',
textAlign: 'center',
fontSize: 11,
fontWeight: 900,
textTransform: 'uppercase',
letterSpacing: '0.1em',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
}}>
<span></span>
<span>
SYSTEM ALERT: {recentCriticalEvents.length} CRITICAL ISSUES DETECTED GO TO SETTINGS ADMIN PANEL
</span>
<span></span>
</div>
)}
<div style={{ paddingTop: hasCriticalEvents ? 32 : 0 }}>
<AppShell />
</div>
{/* Floating AI strategy assistant */}
<ChatControl profiles={chatProfiles} onApplyProfile={handleChatApply} />
</AppContext.Provider>
</BrowserRouter>
);
}
export default App;