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>
115 lines
4.1 KiB
TypeScript
115 lines
4.1 KiB
TypeScript
/**
|
|
* Authenticated fetch helpers for the new market-data proxy endpoints.
|
|
* Follows the same pattern as profileApi.ts.
|
|
*/
|
|
import { getPlatformAccessToken } from './authSession';
|
|
import { tradingRuntime } from './runtime';
|
|
import { createRequestId } from '../../../shared/request-id.js';
|
|
|
|
async function apiGet<T>(path: string): Promise<T> {
|
|
const token = await getPlatformAccessToken();
|
|
const res = await fetch(`${tradingRuntime.tradingApiUrl}${path}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'x-request-id': createRequestId('web-market'),
|
|
},
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({})) as any;
|
|
throw new Error(body?.error ?? `Request failed (${res.status})`);
|
|
}
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
// ── Chart bars ────────────────────────────────────────────────────────────────
|
|
|
|
export interface OHLCVBar {
|
|
ts: number;
|
|
open: number;
|
|
high: number;
|
|
low: number;
|
|
close: number;
|
|
volume: number;
|
|
}
|
|
|
|
export async function fetchChartBars(
|
|
symbol: string,
|
|
period: string,
|
|
): Promise<OHLCVBar[]> {
|
|
const data = await apiGet<{ bars: OHLCVBar[] }>(
|
|
`/api/chart/bars?symbol=${encodeURIComponent(symbol)}&period=${encodeURIComponent(period)}`,
|
|
);
|
|
return data.bars ?? [];
|
|
}
|
|
|
|
// ── Market indices ────────────────────────────────────────────────────────────
|
|
|
|
export interface IndexSnapshot {
|
|
label: string; // 'S&P 500' | 'Dow Jones' | 'Nasdaq'
|
|
symbol: string; // SPY | DIA | QQQ
|
|
price: number;
|
|
change: number;
|
|
changePct:number;
|
|
positive: boolean;
|
|
}
|
|
|
|
const INDEX_META: Record<string, { label: string; symbol: string }> = {
|
|
SPY: { label: 'S&P 500', symbol: 'SPY' },
|
|
DIA: { label: 'Dow Jones', symbol: 'DIA' },
|
|
QQQ: { label: 'Nasdaq', symbol: 'QQQ' },
|
|
};
|
|
|
|
export async function fetchMarketIndices(): Promise<IndexSnapshot[]> {
|
|
// Backend returns Alpaca snapshot shape: { SPY: { latestTrade, dailyBar, prevDailyBar, ... }, ... }
|
|
const data = await apiGet<Record<string, any>>('/api/market/indices');
|
|
return Object.entries(INDEX_META).map(([ticker, meta]) => {
|
|
const snap = data[ticker];
|
|
const price = snap?.latestTrade?.p ?? snap?.latestQuote?.ap ?? 0;
|
|
const prevClose = snap?.prevDailyBar?.c ?? 0;
|
|
const change = prevClose > 0 ? price - prevClose : 0;
|
|
const changePct = prevClose > 0 ? (change / prevClose) * 100 : 0;
|
|
return {
|
|
...meta,
|
|
price,
|
|
change,
|
|
changePct,
|
|
positive: changePct >= 0,
|
|
};
|
|
});
|
|
}
|
|
|
|
// ── News ──────────────────────────────────────────────────────────────────────
|
|
|
|
export interface NewsArticle {
|
|
id?: string;
|
|
url: string;
|
|
headline: string;
|
|
source: string;
|
|
created_at: string;
|
|
images?: Array<{ url: string; size?: string }>;
|
|
}
|
|
|
|
export async function fetchNews(symbol: string, limit = 8): Promise<NewsArticle[]> {
|
|
const data = await apiGet<{ news: NewsArticle[] }>(
|
|
`/api/news?symbols=${encodeURIComponent(symbol)}&limit=${limit}`,
|
|
);
|
|
return data.news ?? [];
|
|
}
|
|
|
|
// ── Research ──────────────────────────────────────────────────────────────────
|
|
|
|
export async function fetchResearchProfile(symbol: string): Promise<any> {
|
|
return apiGet(`/api/research/profile?symbol=${encodeURIComponent(symbol)}`);
|
|
}
|
|
|
|
export async function fetchResearchMetrics(symbol: string): Promise<any> {
|
|
return apiGet(`/api/research/metrics?symbol=${encodeURIComponent(symbol)}`);
|
|
}
|
|
|
|
export async function fetchResearchEarnings(symbol: string): Promise<any[]> {
|
|
const data = await apiGet<{ earnings: any[] }>(
|
|
`/api/research/earnings?symbol=${encodeURIComponent(symbol)}`,
|
|
);
|
|
return data.earnings ?? [];
|
|
}
|