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 (
+
+ );
+}
+
+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'}
+
+
+ )}
+
+ );
+}