feat(web): rolling 'Last N days' presets in backtest picker (Stage E)
Adds a "Recent" preset row to HistoricalPresetPicker with three rolling
windows resolved against today at click time:
- Last 30 days — fast feedback for recently saved plans
- Last 90 days — captures most recent regime shifts
- Last 12 months — captures multiple regime cycles
These complement the fixed-date "Historical events" row (COVID etc.)
already shipped in Stage A. Together they answer two distinct user
questions: "how would this plan have played my last 90 days?" (Recent)
vs. "how would this plan have done in the COVID crash?" (Historical).
Internals:
- Refactored HistoricalEventPreset to use resolveRange() instead of
static fromDate/toDate strings — lets rolling windows stay accurate
across days without rebuilding the preset list.
- HistoricalPresetPicker now accepts both `recentPresets` and
`historicalPresets` props. Either can be empty/omitted to hide that
row.
No backend changes — uses existing /api/backtest/run with kraken or
upload sources. Stage E of docs/backtest/ENGINE_READINESS.md §4.
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
1a6c0352e3
commit
81b71dc96e
@ -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']
|
||||
) => (
|
||||
<div className="historical-preset-picker-row">
|
||||
{presets.map((preset) => {
|
||||
const active = selectedId === preset.id;
|
||||
const range = preset.resolveRange();
|
||||
return (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(range.fromDate, range.toDate, preset.id)}
|
||||
className={`historical-preset-chip ${active ? 'is-active' : ''}`}
|
||||
title={`${preset.description} (${range.fromDate} → ${range.toDate})`}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const HistoricalPresetPicker: React.FC<HistoricalPresetPickerProps> = ({
|
||||
selectedId,
|
||||
onSelect,
|
||||
presets = DEFAULT_HISTORICAL_PRESETS,
|
||||
recentPresets = RECENT_PRESETS,
|
||||
historicalPresets = DEFAULT_HISTORICAL_PRESETS,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`historical-preset-picker ${className || ''}`}>
|
||||
<div className="historical-preset-picker-label">Historical events</div>
|
||||
<div className="historical-preset-picker-row">
|
||||
{presets.map((preset) => {
|
||||
const active = selectedId === preset.id;
|
||||
return (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(preset.fromDate, preset.toDate, preset.id)}
|
||||
className={`historical-preset-chip ${active ? 'is-active' : ''}`}
|
||||
title={`${preset.description} (${preset.fromDate} → ${preset.toDate})`}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{recentPresets.length > 0 ? (
|
||||
<>
|
||||
<div className="historical-preset-picker-label">Recent</div>
|
||||
{renderRow(recentPresets, selectedId, onSelect)}
|
||||
</>
|
||||
) : null}
|
||||
{historicalPresets.length > 0 ? (
|
||||
<>
|
||||
<div className="historical-preset-picker-label">Historical events</div>
|
||||
{renderRow(historicalPresets, selectedId, onSelect)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user