diff --git a/frontend/src/api/market.ts b/frontend/src/api/market.ts index 4ca12c5..695fea5 100644 --- a/frontend/src/api/market.ts +++ b/frontend/src/api/market.ts @@ -1,6 +1,10 @@ import apiClient from './client'; -import type { MarketRegime } from '../lib/types'; +import type { BacktestReport, MarketRegime } from '../lib/types'; export function getMarketRegime() { return apiClient.get('market/regime').then((r) => r.data); } + +export function getBacktestReport() { + return apiClient.get('backtest/report').then((r) => r.data); +} diff --git a/frontend/src/components/signals/BacktestPanel.tsx b/frontend/src/components/signals/BacktestPanel.tsx new file mode 100644 index 0000000..f192ef2 --- /dev/null +++ b/frontend/src/components/signals/BacktestPanel.tsx @@ -0,0 +1,208 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useBacktestReport } from '../../hooks/useMarketRegime'; +import { triggerJob } from '../../api/admin'; +import { Button } from '../ui/Button'; +import { Callout } from '../ui/Callout'; +import { Disclosure } from '../ui/Disclosure'; +import { Section } from '../ui/Section'; +import { useToast } from '../ui/Toast'; +import type { BacktestBucket } from '../../lib/types'; + +function fmtR(v: number | null): string { + if (v === null) return '—'; + return `${v > 0 ? '+' : ''}${v.toFixed(2)}R`; +} +function fmtPct(v: number | null): string { + return v === null ? '—' : `${v.toFixed(1)}%`; +} +function rColor(v: number | null): string { + if (v === null) return 'text-gray-400'; + if (v > 0) return 'text-emerald-400'; + if (v < 0) return 'text-red-400'; + return 'text-gray-300'; +} + +function timeAgo(iso: string): string { + const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60_000); + if (mins < 1) return 'just now'; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + return `${Math.floor(hrs / 24)}d ago`; +} + +function Stat({ label, value, valueClass = 'text-gray-100', sub }: { + label: string; value: string; valueClass?: string; sub?: string; +}) { + return ( +
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+ ); +} + +function BucketRow({ label, b }: { label: string; b: BacktestBucket }) { + return ( + + {label} + {b.total} + {b.wins} + {b.losses} + {b.expired} + {fmtPct(b.hit_rate)} + {fmtR(b.avg_r)} + + ); +} + +export function BacktestPanel() { + const { data: report, isLoading } = useBacktestReport(); + const queryClient = useQueryClient(); + const toast = useToast(); + + const run = useMutation({ + mutationFn: () => triggerJob('backtest'), + onSuccess: (res) => { + if (res.status === 'triggered') { + toast.addToast('success', 'Backtest started — results appear when it finishes (a minute or two).'); + setTimeout(() => queryClient.invalidateQueries({ queryKey: ['backtest-report'] }), 8000); + } else { + toast.addToast('info', res.message || 'Could not start backtest'); + } + }, + onError: () => toast.addToast('error', 'Failed to start backtest'), + }); + + return ( +
+
+
+ +

+ At each weekly point in history, the setup is rebuilt using only data up to that day + (no lookahead), then the actual following ~30 trading days decide its outcome. This + shows how the current settings would have performed. Sentiment and + fundamentals are held neutral (no point-in-time history), so this calibrates the + price / support-resistance / probability machinery. ~6 months of data is roughly one + market regime — read it as directional, not a guarantee. +

+
+ +
+ + {isLoading && Loading…} + + {!isLoading && !report && ( + + No backtest yet. Click “Run backtest” (or trigger it in Admin → Jobs) — it replays every + ticker over history and takes a minute or two. + + )} + + {report && ( + <> +

+ Ran {timeAgo(report.generated_at)} · {report.tickers} tickers · {report.candidates} setups + ({report.qualified} qualified) · weekly cadence, {report.params.horizon_days}-day horizon +

+ +
+ + + + +
+ +
+ + + + + + + + + + + + + + + + {report.by_direction.long && } + {report.by_direction.short && } + +
SetSetupsWinsLossesExpiredHit RateAvg R
+
+ +
+

+ Probability calibration +

+

+ Do targets we call “X% likely” actually hit that often? Realized below predicted = + the model is over-confident. +

+ {report.calibration.length === 0 ? ( + Not enough resolved setups to calibrate. + ) : ( +
+ + + + + + + + + + + {report.calibration.map((row) => { + const over = row.realized_hit_rate < row.predicted_avg; + return ( + + + + + + + ); + })} + +
Predicted BucketSetupsAvg PredictedRealized Hit Rate
{row.bucket}{row.n}{row.predicted_avg.toFixed(0)}% + {row.realized_hit_rate.toFixed(0)}% +
+
+ )} +
+ +

{report.note}

+ + )} +
+
+ ); +} diff --git a/frontend/src/components/signals/TrackRecordPanel.tsx b/frontend/src/components/signals/TrackRecordPanel.tsx index cff4ec5..50cdffe 100644 --- a/frontend/src/components/signals/TrackRecordPanel.tsx +++ b/frontend/src/components/signals/TrackRecordPanel.tsx @@ -11,6 +11,7 @@ import { Section } from '../ui/Section'; import { SkeletonCard } from '../ui/Skeleton'; import { useToast } from '../ui/Toast'; import { RECOMMENDATION_ACTION_LABELS } from '../../lib/recommendation'; +import { BacktestPanel } from './BacktestPanel'; import type { OutcomeBucketStats } from '../../lib/types'; function fmtR(value: number | null): string { @@ -239,6 +240,9 @@ export function TrackRecordPanel() { )} + +
+
); } diff --git a/frontend/src/hooks/useMarketRegime.ts b/frontend/src/hooks/useMarketRegime.ts index 1c00c30..590489b 100644 --- a/frontend/src/hooks/useMarketRegime.ts +++ b/frontend/src/hooks/useMarketRegime.ts @@ -8,3 +8,11 @@ export function useMarketRegime() { staleTime: 60_000, }); } + +export function useBacktestReport() { + return useQuery({ + queryKey: ['backtest-report'], + queryFn: () => marketApi.getBacktestReport(), + staleTime: 60_000, + }); +} diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index cfbd866..431a405 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -179,6 +179,36 @@ export interface SentimentProviderConfig { custom_base_url_providers: string[]; } +export interface BacktestBucket { + total: number; + wins: number; + losses: number; + expired: number; + hit_rate: number | null; + avg_r: number | null; + total_r: number | null; +} + +export interface BacktestCalibrationRow { + bucket: string; + n: number; + predicted_avg: number; + realized_hit_rate: number; +} + +export interface BacktestReport { + generated_at: string; + tickers: number; + candidates: number; + qualified: number; + params: { step_days: number; horizon_days: number; min_lookback: number }; + overall_qualified: BacktestBucket; + overall_all: BacktestBucket; + by_direction: Record; + calibration: BacktestCalibrationRow[]; + note: string; +} + export interface MarketRegime { label: 'bullish' | 'bearish' | 'neutral' | 'unknown'; benchmark?: string; diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 4de8f62..dbd1c45 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/activation.ts","./src/api/admin.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/fundamentals.ts","./src/api/health.ts","./src/api/indicators.ts","./src/api/ingestion.ts","./src/api/jobs.ts","./src/api/market.ts","./src/api/ohlcv.ts","./src/api/performance.ts","./src/api/scores.ts","./src/api/sentiment.ts","./src/api/sr-levels.ts","./src/api/tickers.ts","./src/api/trades.ts","./src/api/watchlist.ts","./src/components/admin/activationsettings.tsx","./src/components/admin/alertsettings.tsx","./src/components/admin/datacleanup.tsx","./src/components/admin/jobcontrols.tsx","./src/components/admin/pipelinereadinesspanel.tsx","./src/components/admin/recommendationsettings.tsx","./src/components/admin/sentimentprovidersettings.tsx","./src/components/admin/settingsform.tsx","./src/components/admin/tickermanagement.tsx","./src/components/admin/tickeruniversebootstrap.tsx","./src/components/admin/usertable.tsx","./src/components/auth/protectedroute.tsx","./src/components/charts/candlestickchart.tsx","./src/components/layout/appshell.tsx","./src/components/layout/mobilenav.tsx","./src/components/layout/sidebar.tsx","./src/components/rankings/rankingstable.tsx","./src/components/rankings/weightsform.tsx","./src/components/scanner/tradetable.tsx","./src/components/signals/setupspanel.tsx","./src/components/signals/trackrecordpanel.tsx","./src/components/ticker/dimensionbreakdownpanel.tsx","./src/components/ticker/fundamentalspanel.tsx","./src/components/ticker/indicatorselector.tsx","./src/components/ticker/recommendationpanel.tsx","./src/components/ticker/sroverlay.tsx","./src/components/ticker/sentimentpanel.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/callout.tsx","./src/components/ui/confirmdialog.tsx","./src/components/ui/disclosure.tsx","./src/components/ui/dropdown.tsx","./src/components/ui/field.tsx","./src/components/ui/pageheader.tsx","./src/components/ui/scorecard.tsx","./src/components/ui/section.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/watchlist/addtickerform.tsx","./src/components/watchlist/watchlisttable.tsx","./src/hooks/useactivation.ts","./src/hooks/useadmin.ts","./src/hooks/useauth.ts","./src/hooks/usefetchsymboldata.ts","./src/hooks/usemarketregime.ts","./src/hooks/useperformance.ts","./src/hooks/userisksettings.ts","./src/hooks/usescores.ts","./src/hooks/usetickerdetail.ts","./src/hooks/usetickers.ts","./src/hooks/usetrades.ts","./src/hooks/usewatchlist.ts","./src/lib/format.ts","./src/lib/ingestionstatus.ts","./src/lib/position.ts","./src/lib/qualification.ts","./src/lib/recommendation.ts","./src/lib/regime.ts","./src/lib/types.ts","./src/pages/adminpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/loginpage.tsx","./src/pages/marketpage.tsx","./src/pages/registerpage.tsx","./src/pages/signalspage.tsx","./src/pages/tickerdetailpage.tsx","./src/stores/authstore.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/activation.ts","./src/api/admin.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/fundamentals.ts","./src/api/health.ts","./src/api/indicators.ts","./src/api/ingestion.ts","./src/api/jobs.ts","./src/api/market.ts","./src/api/ohlcv.ts","./src/api/performance.ts","./src/api/scores.ts","./src/api/sentiment.ts","./src/api/sr-levels.ts","./src/api/tickers.ts","./src/api/trades.ts","./src/api/watchlist.ts","./src/components/admin/activationsettings.tsx","./src/components/admin/alertsettings.tsx","./src/components/admin/datacleanup.tsx","./src/components/admin/jobcontrols.tsx","./src/components/admin/pipelinereadinesspanel.tsx","./src/components/admin/recommendationsettings.tsx","./src/components/admin/sentimentprovidersettings.tsx","./src/components/admin/settingsform.tsx","./src/components/admin/tickermanagement.tsx","./src/components/admin/tickeruniversebootstrap.tsx","./src/components/admin/usertable.tsx","./src/components/auth/protectedroute.tsx","./src/components/charts/candlestickchart.tsx","./src/components/layout/appshell.tsx","./src/components/layout/mobilenav.tsx","./src/components/layout/sidebar.tsx","./src/components/rankings/rankingstable.tsx","./src/components/rankings/weightsform.tsx","./src/components/scanner/tradetable.tsx","./src/components/signals/backtestpanel.tsx","./src/components/signals/setupspanel.tsx","./src/components/signals/trackrecordpanel.tsx","./src/components/ticker/dimensionbreakdownpanel.tsx","./src/components/ticker/fundamentalspanel.tsx","./src/components/ticker/indicatorselector.tsx","./src/components/ticker/recommendationpanel.tsx","./src/components/ticker/sroverlay.tsx","./src/components/ticker/sentimentpanel.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/callout.tsx","./src/components/ui/confirmdialog.tsx","./src/components/ui/disclosure.tsx","./src/components/ui/dropdown.tsx","./src/components/ui/field.tsx","./src/components/ui/pageheader.tsx","./src/components/ui/scorecard.tsx","./src/components/ui/section.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/watchlist/addtickerform.tsx","./src/components/watchlist/watchlisttable.tsx","./src/hooks/useactivation.ts","./src/hooks/useadmin.ts","./src/hooks/useauth.ts","./src/hooks/usefetchsymboldata.ts","./src/hooks/usemarketregime.ts","./src/hooks/useperformance.ts","./src/hooks/userisksettings.ts","./src/hooks/usescores.ts","./src/hooks/usetickerdetail.ts","./src/hooks/usetickers.ts","./src/hooks/usetrades.ts","./src/hooks/usewatchlist.ts","./src/lib/format.ts","./src/lib/ingestionstatus.ts","./src/lib/position.ts","./src/lib/qualification.ts","./src/lib/recommendation.ts","./src/lib/regime.ts","./src/lib/types.ts","./src/pages/adminpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/loginpage.tsx","./src/pages/marketpage.tsx","./src/pages/registerpage.tsx","./src/pages/signalspage.tsx","./src/pages/tickerdetailpage.tsx","./src/stores/authstore.ts"],"version":"5.6.3"} \ No newline at end of file