From 6636054742ca19fe47b784614e17e09b40e22e95 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sun, 24 May 2026 00:48:27 +0000 Subject: [PATCH] feat(dashboard): /meet-kevin/strategy page wired Strategy.tsx composes 6-up metrics header, StrategyVsBenchmarkCurve equity chart, TickerScorecardTable and BacktestRunHistory. Selecting a backtest run replaces the chart with that run's equity curve. Run Backtest button fires POST and polls for completion. App.tsx: +route /meet-kevin/strategy. Layout.tsx: +sidebar entry 'MK Strategy' under Meet Kevin group. --- dashboard/src/App.tsx | 2 + dashboard/src/components/Layout.tsx | 1 + dashboard/src/pages/meetKevin/Strategy.tsx | 213 +++++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 dashboard/src/pages/meetKevin/Strategy.tsx diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 96bb67f..a98d37b 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -13,6 +13,7 @@ import MeetKevinVideos from './pages/meetKevin/Videos'; import MeetKevinVideoDetail from './pages/meetKevin/VideoDetail'; import MeetKevinStocks from './pages/meetKevin/Stocks'; import MeetKevinStockDetail from './pages/meetKevin/StockDetail'; +import MeetKevinStrategy from './pages/meetKevin/Strategy'; export default function App() { return ( @@ -41,6 +42,7 @@ export default function App() { } /> } /> } /> + } /> {/* Catch-all redirect */} diff --git a/dashboard/src/components/Layout.tsx b/dashboard/src/components/Layout.tsx index f4fe45c..a49bc45 100644 --- a/dashboard/src/components/Layout.tsx +++ b/dashboard/src/components/Layout.tsx @@ -9,6 +9,7 @@ const navItems = [ { to: '/news', label: 'News', icon: 'M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2' }, { to: '/backtest', label: 'Backtest', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' }, { to: '/meet-kevin', label: 'Meet Kevin', icon: 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z' }, + { to: '/meet-kevin/strategy', label: 'MK Strategy', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' }, ]; export function Layout() { diff --git a/dashboard/src/pages/meetKevin/Strategy.tsx b/dashboard/src/pages/meetKevin/Strategy.tsx new file mode 100644 index 0000000..6ee8197 --- /dev/null +++ b/dashboard/src/pages/meetKevin/Strategy.tsx @@ -0,0 +1,213 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { TickerScorecardTable } from '../../components/meetKevin/TickerScorecardTable'; +import { BacktestRunHistory } from '../../components/meetKevin/BacktestRunHistory'; +import { StrategyVsBenchmarkCurve } from '../../components/meetKevin/StrategyVsBenchmarkCurve'; +import { + runBacktest, + listBacktestRuns, + getBacktestRun, + getStrategyTickers, + getStrategyEquityCurve, + getStrategyPerformance, + closeKevinPosition, +} from '../../api/meetKevinStrategy'; +import type { BacktestRunStatus } from '../../types/meetKevin'; + +function MetricCard({ label, value, color = 'text-white' }: { label: string; value: string; color?: string }) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +function fmt(v: number | null | undefined, suffix = '%', decimals = 2): string { + if (v == null) return '—'; + return `${v >= 0 ? '+' : ''}${v.toFixed(decimals)}${suffix}`; +} + +const POLL_STATUSES: BacktestRunStatus[] = ['running']; + +export default function MeetKevinStrategy() { + const queryClient = useQueryClient(); + const [selectedRunUuid, setSelectedRunUuid] = useState(null); + + const { data: performance } = useQuery({ + queryKey: ['meet-kevin', 'strategy', 'performance'], + queryFn: getStrategyPerformance, + refetchInterval: 60_000, + }); + + const { data: tickers, isLoading: tickersLoading } = useQuery({ + queryKey: ['meet-kevin', 'strategy', 'tickers'], + queryFn: getStrategyTickers, + refetchInterval: 60_000, + }); + + const { data: equityCurve } = useQuery({ + queryKey: ['meet-kevin', 'strategy', 'equity-curve'], + queryFn: () => getStrategyEquityCurve({ include_benchmark: 'spy' }), + refetchInterval: 120_000, + }); + + const { data: runs, isLoading: runsLoading } = useQuery({ + queryKey: ['meet-kevin', 'backtest', 'runs'], + queryFn: () => listBacktestRuns(20), + refetchInterval: 30_000, + }); + + const { data: selectedRun } = useQuery({ + queryKey: ['meet-kevin', 'backtest', 'run', selectedRunUuid], + queryFn: () => getBacktestRun(selectedRunUuid!), + enabled: selectedRunUuid != null, + refetchInterval: (query) => { + const data = query.state.data; + if (data && POLL_STATUSES.includes(data.status)) return 3_000; + return false; + }, + }); + + const backtestMutation = useMutation({ + mutationFn: runBacktest, + onSuccess: (data) => { + setSelectedRunUuid(data.run_uuid); + void queryClient.invalidateQueries({ queryKey: ['meet-kevin', 'backtest', 'runs'] }); + }, + }); + + const closeMutation = useMutation({ + mutationFn: closeKevinPosition, + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['meet-kevin', 'strategy', 'tickers'] }); + }, + }); + + const equityData = selectedRun?.equity_curve_json ?? equityCurve?.strategy ?? []; + const benchmarkData = selectedRun?.benchmark_curve_json ?? equityCurve?.benchmark ?? null; + + return ( +
+ {/* Page header */} +
+
+

Meet Kevin — Strategy

+

+ Backtest history, ticker scorecard, and equity vs SPY +

+
+ +
+ + {/* Headline metrics */} + {performance && ( +
+ = 0 ? 'text-green-400' : 'text-red-400'} + /> + = 0 ? 'text-green-400' : 'text-red-400'} + /> + = 1 ? 'text-green-400' : 'text-yellow-400'} + /> + + = 0 ? 'text-green-400' : 'text-red-400'} + /> + +
+ )} + + {/* Equity curve */} + {equityData.length > 0 && ( +
+

+ {selectedRunUuid ? 'Backtest Equity Curve' : 'Strategy vs SPY'} +

+ +
+ )} + + {/* Ticker scorecard */} +
+

Ticker Scorecard

+ closeMutation.mutate(symbol)} + /> +
+ + {/* Backtest run history */} +
+

Backtest History

+ +
+ + {/* Selected run detail */} + {selectedRun?.status === 'running' && ( +
+ +

Backtest running…

+

Results will appear automatically

+
+ )} + + {selectedRun?.status === 'failed' && ( +
+

Backtest Failed

+

+ {selectedRun.error_message ?? 'An unknown error occurred'} +

+
+ )} +
+ ); +}