// TypeScript mirrors of the Pydantic models in app/models.py. // Keep these in sync if the backend contract changes. import { useCallback, useEffect, useState } from 'react'; export type Side = 'buy' | 'sell' | 'hold'; export interface MarketObservation { ticker: string; date: string; price: number; price_history: number[]; fundamentals: Record; cash: number; position: number; portfolio_value: number; task_id: string; step_number: number; total_steps: number; chart_path: string; headlines: Array<{ date: string; headline: string; source: string; sentiment_label: string }>; forum_excerpts: Array<{ date: string; subreddit: string; score: number; post_text: string }>; indicators: Record; peers: { peers: Array<{ peer_ticker: string; peer_close: number | null }>; commodity?: string | null; commodity_price?: number | null; }; macro: Array<{ date: string; country: string; headline: string; policy_signal: string }>; } export interface EnvironmentState { task_id: string; current_step: number; total_steps: number; done: boolean; cash: number; position: number; portfolio_value: number; action_history: Array<{ side: Side; quantity: number }>; reward_history: number[]; } export interface SpecialistVote { name: string; signal: number; confidence: number; rationale: string; } export interface CouncilDecision { votes: SpecialistVote[]; action: { side: Side; quantity: number }; rationale: string; } export interface OhlcvBar { time: string; open: number; high: number; low: number; close: number; volume: number; in_episode: boolean; } export interface OhlcvResponse { task_id: string; ticker: string; bars: OhlcvBar[]; } export interface TrainingMetrics { status: 'completed' | 'no_runs'; run_name?: string; summary: Array<{ task_id: string; total_reward: number; final_portfolio: number; buy_and_hold: number; alpha_pct: number; }>; mean_alpha_pct: number; reward_curve_png?: string | null; portfolio_curve_png?: string | null; } // ---------- fetch helpers -------------------------------------------------- async function jget(url: string): Promise { const r = await fetch(url); if (!r.ok) throw new Error(`${url}: ${r.status}`); return r.json() as Promise; } async function jpost(url: string, body: unknown): Promise { const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!r.ok) throw new Error(`${url}: ${r.status}`); return r.json() as Promise; } export const api = { meta: () => jget<{ tasks: string[]; name: string; version: string }>('/meta'), reset: (task_id: string) => jpost<{ observation: MarketObservation; info: Record }>( '/reset', { task_id }, ), step: (side: Side, quantity: number) => jpost<{ observation: MarketObservation; reward: number; done: boolean; info: Record }>( '/step', { side, quantity }, ), state: () => jget('/state'), ohlcv: (task_id: string) => jget(`/ohlcv?task_id=${encodeURIComponent(task_id)}`), council: () => jget('/council'), trainingMetrics: () => jget('/training/metrics'), }; // ---------- shared state hook --------------------------------------------- export interface StockerEnv { tasks: string[]; taskId: string | null; observation: MarketObservation | null; envState: EnvironmentState | null; council: CouncilDecision | null; ohlcv: OhlcvResponse | null; /** Starting cash for the active task, captured from /reset info. */ startingCash: number; loading: boolean; error: string | null; selectTask: (taskId: string) => Promise; submitTrade: (side: Side, quantity: number) => Promise; } export function useStockerEnv(): StockerEnv { const [tasks, setTasks] = useState([]); const [taskId, setTaskId] = useState(null); const [observation, setObservation] = useState(null); const [envState, setEnvState] = useState(null); const [council, setCouncil] = useState(null); const [ohlcv, setOhlcv] = useState(null); const [startingCash, setStartingCash] = useState(10000); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const selectTask = useCallback(async (next: string) => { setLoading(true); setError(null); try { const reset = await api.reset(next); setTaskId(next); setObservation(reset.observation); const sc = Number(reset.info?.starting_cash); if (Number.isFinite(sc) && sc > 0) setStartingCash(sc); const [bars, st] = await Promise.all([api.ohlcv(next), api.state()]); setOhlcv(bars); setEnvState(st); try { setCouncil(await api.council()); } catch (e) { // council can fail silently — UI shows empty state setCouncil(null); console.warn('council fetch failed', e); } } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); setError(msg); } finally { setLoading(false); } }, []); const submitTrade = useCallback(async (side: Side, quantity: number) => { setLoading(true); setError(null); try { const stepRes = await api.step(side, quantity); setObservation(stepRes.observation); const st = await api.state(); setEnvState(st); if (!stepRes.done) { try { setCouncil(await api.council()); } catch (e) { setCouncil(null); console.warn('council fetch failed', e); } } } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); setError(msg); } finally { setLoading(false); } }, []); // bootstrap: load tasks, then auto-select first useEffect(() => { let cancelled = false; api .meta() .then((m) => { if (cancelled) return; setTasks(m.tasks); if (m.tasks.length > 0) { void selectTask(m.tasks[0]); } }) .catch((e: unknown) => { const msg = e instanceof Error ? e.message : String(e); setError(msg); }); return () => { cancelled = true; }; }, [selectTask]); return { tasks, taskId, observation, envState, council, ohlcv, startingCash, loading, error, selectTask, submitTrade, }; } // Display-name + role keyword mapping for the seven specialists. // Backend `name` values come from app/council/specialists.py. export const SPECIALIST_DISPLAY: Record = { chart_pattern: 'Chart Pattern', seasonal_trend: 'Seasonal', indicator: 'Indicator', news: 'News', forum_sentiment: 'Forum', peer_commodity: 'Peer', geopolitics: 'Geo', }; export function statusFromSignal(signal: number): 'green' | 'red' | 'gray' { if (signal > 0.1) return 'green'; if (signal < -0.1) return 'red'; return 'gray'; }