feat(web): admin-only "Test against history" on /plans cards (Stage A)

Adds a "Test history" action button on each saved-setup card on /plans,
visible only when the authenticated profile has role=admin. Clicking
opens a portaled modal containing the existing BacktestRunnerPanel
pre-loaded with:
  - the plan's TradeProfile.strategy_config (the actual rules + risk
    limits the customer configured)
  - the saved-setup's symbol (overrides profile.symbols when scoped)
  - the profile's allocated_capital as initialCapitalUsd

Also adds a "Historical events" preset row to BacktestConfigurator with
5 pre-validated date ranges (COVID crash, COVID recovery, Russia/Ukraine
2022, 2022 bear market, SVB banking shock). Selecting a preset fills in
the from/to date inputs; manually editing dates clears the active preset
highlight.

This is a customer-experience-neutral change:
  - No production feature flag is flipped (customer non-admins see no
    new UI; existing useBacktestFeatureGate still gates the actual
    backtest API call)
  - No backend changes — reuses existing /api/backtest/run
  - No new strategy code paths

Files:
  + web/src/backtest/components/HistoricalPresetPicker.tsx (new)
  + web/src/backtest/components/BacktestPlanModal.tsx (new, portaled)
  ~ web/src/backtest/components/BacktestConfigurator.tsx (preset wiring)
  ~ web/src/views/SimpleView.tsx (admin button + modal mount)
  ~ web/src/layout-fixes.css (§25 preset chips, §26 modal styles)

Stage A of docs/backtest/ENGINE_READINESS.md §4. Lets admins dogfood
the backtest UX and surface bugs before any customer-facing rollout
(stages B/C/D/F).

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:
Devin 2026-05-10 10:50:59 +00:00
parent 35efa786bd
commit 1a6c0352e3
5 changed files with 375 additions and 3 deletions

View File

@ -7,6 +7,7 @@ import type {
} from '../types';
import { parseSymbolsInput, toDateInputValue } from '../utils';
import { Button, Input, Select } from '../../components/ui/Primitives';
import { HistoricalPresetPicker } from './HistoricalPresetPicker';
type SourceType = 'csv' | 'json' | 'replay' | 'kraken';
@ -35,6 +36,21 @@ const defaultTo = toDateInputValue(now);
const [timeframe, setTimeframe] = useState<BacktestTimeframe>('15m');
const [fromDate, setFromDate] = useState(defaultFrom);
const [toDate, setToDate] = useState(defaultTo);
const [activePresetId, setActivePresetId] = useState<string | null>(null);
const handlePresetSelect = (presetFrom: string, presetTo: string, presetId: string) => {
setFromDate(presetFrom);
setToDate(presetTo);
setActivePresetId(presetId);
};
const handleFromDateChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setFromDate(event.target.value);
setActivePresetId(null);
};
const handleToDateChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setToDate(event.target.value);
setActivePresetId(null);
};
const [sourceType, setSourceType] = useState<SourceType>('csv');
const [sourcePayload, setSourcePayload] = useState<any>(null);
const [sourceName, setSourceName] = useState('');
@ -163,7 +179,7 @@ const defaultTo = toDateInputValue(now);
<Input
type="date"
value={fromDate}
onChange={(event) => setFromDate(event.target.value)}
onChange={handleFromDateChange}
controlSize="sm"
/>
</label>
@ -172,11 +188,15 @@ const defaultTo = toDateInputValue(now);
<Input
type="date"
value={toDate}
onChange={(event) => setToDate(event.target.value)}
onChange={handleToDateChange}
controlSize="sm"
/>
</label>
</div>
<HistoricalPresetPicker
selectedId={activePresetId}
onSelect={handlePresetSelect}
/>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<label className="space-y-1">

View File

@ -0,0 +1,82 @@
import React from 'react';
import { createPortal } from 'react-dom';
import { X } from 'lucide-react';
import { BacktestRunnerPanel } from './BacktestRunnerPanel';
import type { TradeProfilePayload } from '../../lib/profileApi';
/**
* Admin-only modal that wraps BacktestRunnerPanel with the strategy_config
* + symbols + initial capital pre-loaded from a saved trade plan's profile.
*
* Stage A of docs/backtest/ENGINE_READINESS.md §4. Surfaces existing
* backtest infrastructure to admin users without changing the customer-
* facing experience or the production feature flag.
*/
export interface BacktestPlanModalProps {
open: boolean;
onClose: () => void;
/** The trade profile whose strategy_config + symbols + capital seed the run. */
profile: TradeProfilePayload | null;
/** The symbol from the saved-setup card (overrides profile.symbols if set). */
symbolOverride?: string;
}
export const BacktestPlanModal: React.FC<BacktestPlanModalProps> = ({
open,
onClose,
profile,
symbolOverride,
}) => {
if (!open || !profile) return null;
const profileSymbols = String(profile.symbols || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const symbols = symbolOverride
? [symbolOverride.trim().toUpperCase()]
: profileSymbols;
// Use the trade profile's allocated_capital as the starting capital so the
// backtest reflects what the customer actually configured. Falls back to
// 10k for unconfigured profiles.
const initialCapitalUsd = Number(profile.allocated_capital) > 0
? Number(profile.allocated_capital)
: 10000;
return createPortal(
<div className="backtest-plan-modal-backdrop" role="dialog" aria-modal="true" onClick={onClose}>
<div className="backtest-plan-modal-panel" onClick={(e) => e.stopPropagation()}>
<div className="backtest-plan-modal-header">
<div>
<div className="backtest-plan-modal-eyebrow">Test plan against history (admin)</div>
<h2 className="backtest-plan-modal-title">{profile.name || 'Trade plan'}</h2>
<div className="backtest-plan-modal-sub">
Symbols: <span className="font-mono">{symbols.join(', ') || '—'}</span>
{' · '}
Capital: ${initialCapitalUsd.toLocaleString()}
</div>
</div>
<button
type="button"
onClick={onClose}
className="backtest-plan-modal-close"
aria-label="Close backtest modal"
>
<X size={18} />
</button>
</div>
<div className="backtest-plan-modal-body">
<BacktestRunnerPanel
profileId={profile.id}
strategyConfig={profile.strategy_config}
symbols={symbols}
initialCapitalUsd={initialCapitalUsd}
title="Pick a date range or historical preset"
/>
</div>
</div>
</div>,
document.body
);
};

View File

@ -0,0 +1,93 @@
import React from 'react';
/**
* Preset historical event 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.
*/
export interface HistoricalEventPreset {
id: string;
label: string;
description: string;
fromDate: string; // YYYY-MM-DD
toDate: string; // YYYY-MM-DD
}
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',
},
{
id: 'covid-recovery',
label: 'COVID recovery',
description: 'Apr-Dec 2020 — fastest bull market in modern history.',
fromDate: '2020-04-01',
toDate: '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',
},
{
id: 'bear-2022',
label: '2022 bear market',
description: 'Full year — sustained drawdown across crypto + tech equities.',
fromDate: '2022-01-01',
toDate: '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',
},
];
export interface HistoricalPresetPickerProps {
/** Currently-selected preset id (or null when user is editing custom dates). */
selectedId: string | null;
/** 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[];
className?: string;
}
export const HistoricalPresetPicker: React.FC<HistoricalPresetPickerProps> = ({
selectedId,
onSelect,
presets = 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>
</div>
);
};

View File

@ -494,3 +494,145 @@
overflow-wrap: anywhere;
}
/* ---------------------------------------------------------------------------
Section 25 Historical event preset picker (Stage A backtest POC)
Used inside BacktestConfigurator. Compact chip row that lets the user
pick a pre-validated date range (COVID, war periods) instead of typing
dates manually.
--------------------------------------------------------------------------- */
.historical-preset-picker {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 4px;
}
.historical-preset-picker-label {
font-size: 10px;
font-weight: 700;
color: var(--bl-text-quiet);
text-transform: uppercase;
letter-spacing: 0.12em;
}
.historical-preset-picker-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.historical-preset-chip {
appearance: none;
-webkit-appearance: none;
font: inherit;
border: 1px solid var(--bl-border-subtle);
background: var(--bl-surface-muted);
color: var(--bl-text-secondary);
border-radius: 999px;
padding: 4px 12px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}
.historical-preset-chip:hover {
background: var(--bl-surface-highlight);
border-color: var(--bl-border);
color: var(--bl-text-primary);
}
.historical-preset-chip.is-active {
background: var(--bl-accent-muted);
border-color: var(--bl-accent);
color: var(--bl-accent);
}
.historical-preset-chip:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--bl-bg-canvas, #0b0f17),
0 0 0 4px var(--bl-focus-ring, var(--bl-accent, #5A8CFF));
}
/* ---------------------------------------------------------------------------
Section 26 Admin-only "Test against history" backtest modal (Stage A)
Lives in BacktestPlanModal.tsx. Portaled to <body> so it escapes any
parent overflow:hidden in the trade-plans card grid.
--------------------------------------------------------------------------- */
.backtest-plan-modal-backdrop {
position: fixed;
inset: 0;
z-index: 9999;
background: color-mix(in oklab, var(--bl-bg-canvas, #0b0f17) 70%, transparent);
backdrop-filter: blur(4px);
display: flex;
align-items: flex-start;
justify-content: center;
overflow-y: auto;
padding: 5vh 16px 8vh;
}
.backtest-plan-modal-panel {
width: 100%;
max-width: 960px;
background: var(--card-elevated);
border: 1px solid var(--bl-border);
border-radius: 20px;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
overflow: hidden;
display: flex;
flex-direction: column;
}
.backtest-plan-modal-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 20px 24px 16px;
border-bottom: 1px solid var(--bl-border-subtle);
background: var(--bl-surface-overlay);
}
.backtest-plan-modal-eyebrow {
font-size: 10px;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--bl-warning);
margin-bottom: 6px;
}
.backtest-plan-modal-title {
font-size: 18px;
font-weight: 700;
color: var(--foreground);
margin: 0;
line-height: 1.3;
}
.backtest-plan-modal-sub {
font-size: 12px;
color: var(--muted-foreground);
margin-top: 4px;
}
.backtest-plan-modal-close {
appearance: none;
background: transparent;
border: 1px solid var(--bl-border-subtle);
border-radius: 10px;
width: 36px;
height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--bl-text-secondary);
cursor: pointer;
flex-shrink: 0;
}
.backtest-plan-modal-close:hover {
background: var(--bl-surface-highlight);
color: var(--foreground);
}
.backtest-plan-modal-body {
padding: 20px 24px 24px;
overflow-y: auto;
max-height: calc(90vh - 120px);
}
.saved-setup-action.is-admin-only {
border-color: color-mix(in oklab, var(--bl-warning) 30%, var(--bl-border));
color: var(--bl-warning);
}
.saved-setup-action.is-admin-only:hover {
background: color-mix(in oklab, var(--bl-warning) 10%, transparent);
}

View File

@ -1,6 +1,6 @@
import { useEffect, useMemo, useReducer, useRef, useState } from 'react';
import type { ChangeEvent, FormEvent } from 'react';
import { Pencil, RefreshCw, Trash2 } from 'lucide-react';
import { History, Pencil, RefreshCw, Trash2 } from 'lucide-react';
import { useSearchParams } from 'react-router-dom';
import { useAppContext } from '../context/AppContext';
import { fetchChartBars, fetchResearchProfile } from '../lib/marketApi';
@ -25,6 +25,8 @@ import {
} from './tradePlansState';
import { useTradePlansNavigationState } from './useTradePlansNavigationState';
import { CardButton } from '@bytelyst/ui';
import { useAuth } from '../components/AuthContext';
import { BacktestPlanModal } from '../backtest/components/BacktestPlanModal';
type SimpleHolding = {
symbol: string;
@ -592,8 +594,11 @@ function describeSavedSetup(entry: ManualEntryPayload): string {
export function SimpleView() {
const { botState } = useAppContext();
const { profile: authProfile } = useAuth();
const isAdmin = authProfile?.role === 'admin';
const [searchParams, setSearchParams] = useSearchParams();
const [profiles, setProfiles] = useState<TradeProfilePayload[]>([]);
const [backtestModalSetupId, setBacktestModalSetupId] = useState<string | null>(null);
const [savedSetups, setSavedSetups] = useState<ManualEntryPayload[]>([]);
const [uiState, dispatch] = useReducer(reduceTradePlansUiState, DEFAULT_TRADE_PLANS_UI_STATE);
const [submitting, setSubmitting] = useState(false);
@ -1454,6 +1459,21 @@ export function SimpleView() {
Resume exit management
</Button>
) : null}
{isAdmin ? (
<Button
type="button"
onClick={() => setBacktestModalSetupId(entryId)}
variant="outline"
size="sm"
className="saved-setup-action is-admin-only"
title="Test this plan against historical market data (admin only)"
>
<span className="inline-flex items-center gap-2">
<History size={14} />
Test history
</span>
</Button>
) : null}
<Button
type="button"
onClick={() => handleEdit(entry)}
@ -1627,6 +1647,21 @@ export function SimpleView() {
</CardContent>
</Card>
</div>
<BacktestPlanModal
open={Boolean(backtestModalSetupId)}
onClose={() => setBacktestModalSetupId(null)}
profile={(() => {
if (!backtestModalSetupId) return null;
const setup = savedSetups.find((s) => String(s.stock_instance_id || '') === backtestModalSetupId) || null;
if (!setup) return null;
return profiles.find((p) => p.id === String(setup.profile_id || '')) || null;
})()}
symbolOverride={(() => {
if (!backtestModalSetupId) return undefined;
const setup = savedSetups.find((s) => String(s.stock_instance_id || '') === backtestModalSetupId);
return setup?.symbol ? String(setup.symbol) : undefined;
})()}
/>
</div>
);
}