diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index a98d37b..96bb67f 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -13,7 +13,6 @@ 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 ( @@ -42,7 +41,6 @@ export default function App() { } /> } /> } /> - } /> {/* Catch-all redirect */} diff --git a/dashboard/src/api/meetKevinStrategy.ts b/dashboard/src/api/meetKevinStrategy.ts deleted file mode 100644 index 043c641..0000000 --- a/dashboard/src/api/meetKevinStrategy.ts +++ /dev/null @@ -1,60 +0,0 @@ -import client from './client'; -import type { - BacktestRun, - BacktestRunDetail, - StrategyEquityCurve, - StrategyPerformance, - StrategyTicker, -} from '../types/meetKevin'; - -export async function runBacktest(params: { - holding_days?: number; - slippage_pct?: number; - dedupe_policy?: 'roll' | 'ignore'; - initial_capital?: number; -}): Promise<{ run_uuid: string; status: string }> { - const { data } = await client.post('/meet-kevin/backtest/run', params); - return data; -} - -export async function listBacktestRuns(limit = 20): Promise { - const { data } = await client.get('/meet-kevin/backtest/runs', { - params: { limit }, - }); - return data; -} - -export async function getBacktestRun(runUuid: string): Promise { - const { data } = await client.get(`/meet-kevin/backtest/runs/${runUuid}`); - return data; -} - -export async function getLatestBacktest(): Promise { - const { data } = await client.get('/meet-kevin/backtest/latest'); - return data; -} - -export async function getStrategyTickers(): Promise { - const { data } = await client.get('/meet-kevin/strategy/tickers'); - return data; -} - -export async function getStrategyEquityCurve(params: { - from?: string; - to?: string; - include_benchmark?: 'spy'; -}): Promise { - const { data } = await client.get('/meet-kevin/strategy/equity-curve', { - params, - }); - return data; -} - -export async function getStrategyPerformance(): Promise { - const { data } = await client.get('/meet-kevin/strategy/performance'); - return data; -} - -export async function closeKevinPosition(symbol: string): Promise { - await client.post(`/meet-kevin/positions/${symbol}/close`); -} diff --git a/dashboard/src/components/Layout.tsx b/dashboard/src/components/Layout.tsx index a49bc45..f4fe45c 100644 --- a/dashboard/src/components/Layout.tsx +++ b/dashboard/src/components/Layout.tsx @@ -9,7 +9,6 @@ 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/components/meetKevin/BacktestRunHistory.tsx b/dashboard/src/components/meetKevin/BacktestRunHistory.tsx deleted file mode 100644 index c8a3169..0000000 --- a/dashboard/src/components/meetKevin/BacktestRunHistory.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import type { BacktestRun, BacktestRunStatus } from '../../types/meetKevin'; - -const STATUS_BADGE: Record = { - running: { label: 'Running', cls: 'bg-blue-600/30 text-blue-300 border-blue-500/40' }, - completed: { label: 'Completed', cls: 'bg-green-600/30 text-green-300 border-green-500/40' }, - failed: { label: 'Failed', cls: 'bg-red-600/30 text-red-300 border-red-500/40' }, -}; - -function fmt(v: number | null | undefined, suffix = '%', decimals = 2): string { - if (v == null) return '—'; - return `${v >= 0 ? '+' : ''}${v.toFixed(decimals)}${suffix}`; -} - -interface Props { - runs: BacktestRun[]; - isLoading: boolean; - selectedRunUuid: string | null; - onSelect: (runUuid: string) => void; -} - -export function BacktestRunHistory({ runs, isLoading, selectedRunUuid, onSelect }: Props) { - if (isLoading) { - return ( -
- -
- ); - } - - if (runs.length === 0) { - return ( -
- No backtest runs yet — click “Run Backtest” to start one. -
- ); - } - - return ( -
- - - - - - - - - - - - - - - {runs.map((run) => { - const badge = STATUS_BADGE[run.status]; - const m = run.metrics_json; - const isSelected = run.run_uuid === selectedRunUuid; - return ( - onSelect(run.run_uuid)} - className={`cursor-pointer transition-colors ${ - isSelected - ? 'bg-blue-600/10' - : 'hover:bg-slate-700/30' - }`} - > - - - - - - - - - - ); - })} - -
- Started - - Status - - Return - - Sharpe - - Max DD - - Alpha - - Trades - - Source -
- {new Date(run.started_at).toLocaleString()} - - - {badge.label} - - {run.error_message && ( - - {run.error_message} - - )} - = 0 ? 'text-green-400' : 'text-red-400'}`}> - {m ? fmt(m.total_return_pct) : '—'} - - {m?.sharpe_ratio != null ? m.sharpe_ratio.toFixed(2) : '—'} - - {m?.max_drawdown_pct != null ? `${m.max_drawdown_pct.toFixed(2)}%` : '—'} - = 0 ? 'text-green-400' : 'text-red-400'}`}> - {m ? fmt(m.alpha_vs_spy_pct) : '—'} - - {m?.trade_count ?? '—'} - - {run.trigger_source} -
-
- ); -} diff --git a/dashboard/src/components/meetKevin/StrategyVsBenchmarkCurve.tsx b/dashboard/src/components/meetKevin/StrategyVsBenchmarkCurve.tsx deleted file mode 100644 index cdbf0f0..0000000 --- a/dashboard/src/components/meetKevin/StrategyVsBenchmarkCurve.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { createChart, type IChartApi, LineSeries } from 'lightweight-charts'; -import type { EquityCurvePoint } from '../../types/meetKevin'; - -interface Props { - strategy: EquityCurvePoint[]; - benchmark: EquityCurvePoint[] | null; - height?: number; -} - -function toChartData(points: EquityCurvePoint[]) { - const byDay = new Map(); - for (const p of points) { - const day = p.timestamp.split('T')[0]; - byDay.set(day, p.value); - } - return Array.from(byDay.entries()) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([time, value]) => ({ time, value })); -} - -export function StrategyVsBenchmarkCurve({ strategy, benchmark, height = 350 }: Props) { - const containerRef = useRef(null); - const chartRef = useRef(null); - - useEffect(() => { - if (!containerRef.current) return; - - const chart = createChart(containerRef.current, { - height, - layout: { - background: { color: '#1e293b' }, - textColor: '#94a3b8', - }, - grid: { - vertLines: { color: '#334155' }, - horzLines: { color: '#334155' }, - }, - crosshair: { mode: 0 }, - rightPriceScale: { borderColor: '#475569' }, - timeScale: { borderColor: '#475569', timeVisible: true }, - }); - - const strategySeries = chart.addSeries(LineSeries, { - color: '#3b82f6', - lineWidth: 2, - title: 'Strategy', - priceFormat: { - type: 'custom', - formatter: (price: number) => - '$' + price.toLocaleString('en-US', { minimumFractionDigits: 2 }), - }, - }); - - const benchmarkSeries = chart.addSeries(LineSeries, { - color: '#94a3b8', - lineWidth: 1, - title: 'SPY', - lineStyle: 1, // dashed - priceFormat: { - type: 'custom', - formatter: (price: number) => - '$' + price.toLocaleString('en-US', { minimumFractionDigits: 2 }), - }, - }); - - chartRef.current = chart; - - if (strategy.length > 0) { - strategySeries.setData(toChartData(strategy) as { time: string; value: number }[]); - } - if (benchmark && benchmark.length > 0) { - benchmarkSeries.setData(toChartData(benchmark) as { time: string; value: number }[]); - } - - chart.timeScale().fitContent(); - - const handleResize = () => { - if (containerRef.current) { - chart.applyOptions({ width: containerRef.current.clientWidth }); - } - }; - window.addEventListener('resize', handleResize); - handleResize(); - - return () => { - window.removeEventListener('resize', handleResize); - chart.remove(); - chartRef.current = null; - }; - }, [height, strategy, benchmark]); - - return ( -
-
- - - Strategy - - - - SPY benchmark - -
-
-
- ); -} diff --git a/dashboard/src/components/meetKevin/TickerScorecardTable.tsx b/dashboard/src/components/meetKevin/TickerScorecardTable.tsx deleted file mode 100644 index 1943fdd..0000000 --- a/dashboard/src/components/meetKevin/TickerScorecardTable.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import type { BridgeStatus, StrategyTicker } from '../../types/meetKevin'; -import { ActionChip } from './ActionChip'; -import { ConvictionBar } from './ConvictionBar'; - -const BRIDGE_BADGE: Record = { - emitted: { label: 'WOULD-TRADE', cls: 'bg-blue-600/30 text-blue-300 border-blue-500/40' }, - dry_run: { label: 'WOULD-TRADE', cls: 'bg-blue-600/30 text-blue-300 border-blue-500/40' }, - skipped_non_tradable: { label: 'NOT TRADABLE', cls: 'bg-slate-600/30 text-slate-400 border-slate-500/40' }, - skipped_blocklist: { label: 'BLOCKLISTED', cls: 'bg-rose-600/30 text-rose-300 border-rose-500/40' }, - skipped_caps: { label: 'CAP HIT', cls: 'bg-yellow-600/30 text-yellow-300 border-yellow-500/40' }, - deferred: { label: 'DEFERRED', cls: 'bg-slate-600/30 text-slate-400 border-slate-500/40' }, - broker_rejected: { label: 'REJECTED', cls: 'bg-red-600/30 text-red-300 border-red-500/40' }, -}; - -interface Props { - tickers: StrategyTicker[]; - isLoading: boolean; - onClose: (symbol: string) => void; -} - -export function TickerScorecardTable({ tickers, isLoading, onClose }: Props) { - if (isLoading) { - return ( -
- -
- ); - } - - if (tickers.length === 0) { - return ( -
- No ticker signals yet -
- ); - } - - return ( -
- - - - - - - - - - - - - - - {tickers.map((t) => { - const badge = t.bridge_status ? BRIDGE_BADGE[t.bridge_status] : null; - const pnl = t.unrealized_pnl_pct; - return ( - - - - - - - - - - - - ); - })} - -
- Symbol - - Signal - - Conviction - - Price - - P&L - - Status - - Mentions - - Last seen - -
- ${t.symbol} - {t.is_held && ( - - HOLDING - - )} - - - -
-
- -
- - {(t.latest_conviction * 100).toFixed(0)}% - -
-
- {t.current_price != null - ? `$${t.current_price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` - : '—'} - - {pnl != null ? ( - = 0 ? 'text-green-400' : 'text-red-400'}> - {pnl >= 0 ? '+' : ''}{pnl.toFixed(2)}% - - ) : ( - - )} - - {badge ? ( - - {badge.label} - - ) : ( - - )} - {t.mention_count} - {new Date(t.last_mention_at).toLocaleDateString()} - - {t.is_held && ( - - )} -
-
- ); -} diff --git a/dashboard/src/pages/meetKevin/Strategy.tsx b/dashboard/src/pages/meetKevin/Strategy.tsx deleted file mode 100644 index 6ee8197..0000000 --- a/dashboard/src/pages/meetKevin/Strategy.tsx +++ /dev/null @@ -1,213 +0,0 @@ -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'} -

-
- )} -
- ); -} diff --git a/dashboard/src/types/meetKevin.ts b/dashboard/src/types/meetKevin.ts index 8a67325..fd656ca 100644 --- a/dashboard/src/types/meetKevin.ts +++ b/dashboard/src/types/meetKevin.ts @@ -112,94 +112,3 @@ export interface DashboardData { count: number; }[]; } - -// --- Strategy + backtest types (v2) --- - -export type BacktestRunStatus = 'running' | 'completed' | 'failed'; - -export interface BacktestTrade { - symbol: string; - entry_at: string; - entry_price: number; - exit_at: string | null; - exit_price: number | null; - qty: number; - pnl_usd: number | null; - pnl_pct: number | null; - holding_days_actual: number | null; -} - -export interface BacktestMetrics { - total_return_pct: number; - annualized_return_pct: number | null; - sharpe_ratio: number | null; - max_drawdown_pct: number | null; - win_rate: number | null; - trade_count: number; - alpha_vs_spy_pct: number | null; - beta_vs_spy: number | null; - winners: number | null; - losers: number | null; - best_trade_pct: number | null; - worst_trade_pct: number | null; -} - -export interface BacktestRun { - run_uuid: string; - status: BacktestRunStatus; - started_at: string; - finished_at: string | null; - trigger_source: 'manual' | 'scheduled'; - params_json: Record; - metrics_json: BacktestMetrics | null; - error_message: string | null; -} - -export interface BacktestRunDetail extends BacktestRun { - trades: BacktestTrade[]; - equity_curve_json: Array<{ timestamp: string; value: number }> | null; - benchmark_curve_json: Array<{ timestamp: string; value: number }> | null; -} - -export type BridgeStatus = - | 'emitted' - | 'skipped_non_tradable' - | 'skipped_blocklist' - | 'skipped_caps' - | 'deferred' - | 'broker_rejected' - | 'dry_run'; - -export interface StrategyTicker { - symbol: string; - latest_action: TickerAction; - latest_conviction: number; - mention_count: number; - last_mention_at: string; - bridge_status: BridgeStatus | null; - is_held: boolean; - current_price: number | null; - unrealized_pnl_pct: number | null; -} - -export interface EquityCurvePoint { - timestamp: string; - value: number; -} - -export interface StrategyEquityCurve { - strategy: EquityCurvePoint[]; - benchmark: EquityCurvePoint[] | null; -} - -export interface StrategyPerformance { - total_return_pct: number; - annualized_return_pct: number | null; - sharpe_ratio: number | null; - max_drawdown_pct: number | null; - win_rate: number | null; - trade_count: number; - alpha_vs_spy_pct: number | null; - open_positions: number; - last_backtest_at: string | null; -}