diff --git a/web/src/backtest/components/HistoricalPresetPicker.tsx b/web/src/backtest/components/HistoricalPresetPicker.tsx index 15ed0f9..c168ea1 100644 --- a/web/src/backtest/components/HistoricalPresetPicker.tsx +++ b/web/src/backtest/components/HistoricalPresetPicker.tsx @@ -1,55 +1,84 @@ import React from 'react'; /** - * Preset historical event windows for the backtest configurator. + * Preset windows for the backtest configurator. * - * These are pre-validated against typical Kraken/Alpaca data depth (5-7 - * years), so customers don't waste a backtest on a window that will return - * "no data" for newer symbols. See docs/backtest/ENGINE_READINESS.md §3.4. + * Two categories: + * - "Recent" — rolling windows relative to today. Most useful for + * "how would this plan have played my last 90 days?" (Stage E of + * docs/backtest/ENGINE_READINESS.md §4). + * - "Historical events" — fixed-date windows around significant market + * regimes. Pre-validated against typical Kraken/Alpaca data depth + * (5-7 years) so customers don't waste a backtest on a window with + * no data for newer symbols. */ export interface HistoricalEventPreset { id: string; label: string; description: string; - fromDate: string; // YYYY-MM-DD - toDate: string; // YYYY-MM-DD + /** YYYY-MM-DD; resolved at click time so "Last 30 days" stays accurate. */ + resolveRange: () => { fromDate: string; toDate: string }; } +const isoDate = (d: Date): string => d.toISOString().slice(0, 10); +const fixedRange = (from: string, to: string) => () => ({ fromDate: from, toDate: to }); +const trailingDays = (n: number) => () => { + const to = new Date(); + const from = new Date(to.getTime() - n * 24 * 60 * 60 * 1000); + return { fromDate: isoDate(from), toDate: isoDate(to) }; +}; + +export const RECENT_PRESETS: HistoricalEventPreset[] = [ + { + id: 'recent-30d', + label: 'Last 30 days', + description: 'Rolling 30-day window — fast feedback for recently saved plans.', + resolveRange: trailingDays(30), + }, + { + id: 'recent-90d', + label: 'Last 90 days', + description: 'Rolling quarter — captures most recent regime shifts.', + resolveRange: trailingDays(90), + }, + { + id: 'recent-365d', + label: 'Last 12 months', + description: 'Rolling year — captures multiple regime cycles.', + resolveRange: trailingDays(365), + }, +]; + export const DEFAULT_HISTORICAL_PRESETS: HistoricalEventPreset[] = [ { id: 'covid-crash', label: 'COVID crash', description: 'Feb-Apr 2020 — ~30% drawdown across most assets in 5 weeks.', - fromDate: '2020-02-15', - toDate: '2020-04-30', + resolveRange: fixedRange('2020-02-15', '2020-04-30'), }, { id: 'covid-recovery', label: 'COVID recovery', description: 'Apr-Dec 2020 — fastest bull market in modern history.', - fromDate: '2020-04-01', - toDate: '2020-12-31', + resolveRange: fixedRange('2020-04-01', '2020-12-31'), }, { id: 'russia-ukraine', label: 'Russia/Ukraine 2022', description: 'Feb-Jun 2022 — energy + commodity shocks, risk-off pivot.', - fromDate: '2022-02-01', - toDate: '2022-06-30', + resolveRange: fixedRange('2022-02-01', '2022-06-30'), }, { id: 'bear-2022', label: '2022 bear market', description: 'Full year — sustained drawdown across crypto + tech equities.', - fromDate: '2022-01-01', - toDate: '2022-12-31', + resolveRange: fixedRange('2022-01-01', '2022-12-31'), }, { id: 'svb-banking', label: 'SVB banking shock', description: 'Mar 2023 — bank failures + emergency Fed response.', - fromDate: '2023-03-01', - toDate: '2023-04-15', + resolveRange: fixedRange('2023-03-01', '2023-04-15'), }, ]; @@ -59,35 +88,56 @@ export interface HistoricalPresetPickerProps { /** Called with `(fromDate, toDate, presetId)` when a preset is clicked. */ onSelect: (fromDate: string, toDate: string, presetId: string) => void; /** Override the default preset list. */ - presets?: HistoricalEventPreset[]; + recentPresets?: HistoricalEventPreset[]; + historicalPresets?: HistoricalEventPreset[]; className?: string; } +const renderRow = ( + presets: HistoricalEventPreset[], + selectedId: string | null, + onSelect: HistoricalPresetPickerProps['onSelect'] +) => ( +
+ {presets.map((preset) => { + const active = selectedId === preset.id; + const range = preset.resolveRange(); + return ( + + ); + })} +
+); + export const HistoricalPresetPicker: React.FC = ({ selectedId, onSelect, - presets = DEFAULT_HISTORICAL_PRESETS, + recentPresets = RECENT_PRESETS, + historicalPresets = DEFAULT_HISTORICAL_PRESETS, className, }) => { return (
-
Historical events
-
- {presets.map((preset) => { - const active = selectedId === preset.id; - return ( - - ); - })} -
+ {recentPresets.length > 0 ? ( + <> +
Recent
+ {renderRow(recentPresets, selectedId, onSelect)} + + ) : null} + {historicalPresets.length > 0 ? ( + <> +
Historical events
+ {renderRow(historicalPresets, selectedId, onSelect)} + + ) : null}
); };