refactor(ui): align backtest compare controls

This commit is contained in:
Saravana Achu Mac 2026-05-09 01:41:43 -07:00
parent c429488358
commit 979983d3ef

View File

@ -9,6 +9,7 @@ import type {
} from '../types'; } from '../types';
import { parseSymbolsInput, toDateInputValue } from '../utils'; import { parseSymbolsInput, toDateInputValue } from '../utils';
import { BacktestResultsDashboard } from './BacktestResultsDashboard'; import { BacktestResultsDashboard } from './BacktestResultsDashboard';
import { Button, Checkbox, Input, Select } from '../../components/ui/Primitives';
type SourceType = 'csv' | 'json' | 'replay' | 'kraken'; type SourceType = 'csv' | 'json' | 'replay' | 'kraken';
@ -256,27 +257,30 @@ export const BacktestComparePanel: React.FC<BacktestComparePanelProps> = ({ prof
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold uppercase tracking-wider text-zinc-300">Profiles</span> <span className="text-[11px] font-bold uppercase tracking-wider text-zinc-300">Profiles</span>
<div className="flex gap-2"> <div className="flex gap-2">
<button <Button
type="button" type="button"
className="rounded-md bg-white/10 px-2 py-1 text-[10px] font-bold uppercase tracking-wide text-zinc-300 hover:bg-white/20" variant="ghost"
size="sm"
className="min-h-7 px-2 text-[10px]"
onClick={() => setSelectedProfileIds(profiles.map((profile) => profile.id))} onClick={() => setSelectedProfileIds(profiles.map((profile) => profile.id))}
> >
Select All Select All
</button> </Button>
<button <Button
type="button" type="button"
className="rounded-md bg-white/10 px-2 py-1 text-[10px] font-bold uppercase tracking-wide text-zinc-300 hover:bg-white/20" variant="ghost"
size="sm"
className="min-h-7 px-2 text-[10px]"
onClick={() => setSelectedProfileIds([])} onClick={() => setSelectedProfileIds([])}
> >
Clear Clear
</button> </Button>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 max-h-[180px] overflow-auto"> <div className="grid grid-cols-1 md:grid-cols-2 gap-2 max-h-[180px] overflow-auto">
{profiles.map((profile) => ( {profiles.map((profile) => (
<label key={profile.id} className="flex items-center gap-2 rounded-lg border border-white/10 bg-zinc-900/60 px-2 py-2"> <label key={profile.id} className="flex items-center gap-2 rounded-lg border border-white/10 bg-zinc-900/60 px-2 py-2">
<input <Checkbox
type="checkbox"
checked={selectedProfileIds.includes(profile.id)} checked={selectedProfileIds.includes(profile.id)}
onChange={(event) => toggleProfile(profile.id, event.target.checked)} onChange={(event) => toggleProfile(profile.id, event.target.checked)}
/> />
@ -290,20 +294,21 @@ export const BacktestComparePanel: React.FC<BacktestComparePanelProps> = ({ prof
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Timeframe</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">Timeframe</span>
<select <Select
value={timeframe} value={timeframe}
onChange={(event) => setTimeframe(event.target.value as BacktestTimeframe)} onChange={(event) => setTimeframe(event.target.value as BacktestTimeframe)}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
> options={[
<option value="1m">1m</option> { value: '1m', label: '1m' },
<option value="15m">15m</option> { value: '15m', label: '15m' },
<option value="1h">1h</option> { value: '1h', label: '1h' },
<option value="4h">4h</option> { value: '4h', label: '4h' },
</select> ]}
/>
</label> </label>
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Historical Data Type</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">Historical Data Type</span>
<select <Select
value={sourceType} value={sourceType}
onChange={(event) => { onChange={(event) => {
const nextType = event.target.value as SourceType; const nextType = event.target.value as SourceType;
@ -316,126 +321,131 @@ export const BacktestComparePanel: React.FC<BacktestComparePanelProps> = ({ prof
setSourceName(''); setSourceName('');
} }
}} }}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
> options={[
<option value="kraken">Kraken Historical API</option> { value: 'kraken', label: 'Kraken Historical API' },
<option value="csv">CSV</option> { value: 'csv', label: 'CSV' },
<option value="json">JSON</option> { value: 'json', label: 'JSON' },
<option value="replay">Replay JSON</option> { value: 'replay', label: 'Replay JSON' },
</select> ]}
/>
</label> </label>
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">From</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">From</span>
<input <Input
type="date" type="date"
value={fromDate} value={fromDate}
onChange={(event) => setFromDate(event.target.value)} onChange={(event) => setFromDate(event.target.value)}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
/> />
</label> </label>
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">To</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">To</span>
<input <Input
type="date" type="date"
value={toDate} value={toDate}
onChange={(event) => setToDate(event.target.value)} onChange={(event) => setToDate(event.target.value)}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
/> />
</label> </label>
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Slippage (bps)</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">Slippage (bps)</span>
<input <Input
type="number" type="number"
min={0} min={0}
value={slippageBps} value={slippageBps}
onChange={(event) => setSlippageBps(Number(event.target.value))} onChange={(event) => setSlippageBps(Number(event.target.value))}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
/> />
</label> </label>
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Fee (bps)</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">Fee (bps)</span>
<input <Input
type="number" type="number"
min={0} min={0}
value={feeBps} value={feeBps}
onChange={(event) => setFeeBps(Number(event.target.value))} onChange={(event) => setFeeBps(Number(event.target.value))}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
/> />
</label> </label>
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Partial Fill %</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">Partial Fill %</span>
<input <Input
type="number" type="number"
min={1} min={1}
max={100} max={100}
value={Math.round(partialFillPct * 100)} value={Math.round(partialFillPct * 100)}
onChange={(event) => setPartialFillPct(Math.max(0.01, Math.min(1, Number(event.target.value) / 100)))} onChange={(event) => setPartialFillPct(Math.max(0.01, Math.min(1, Number(event.target.value) / 100)))}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
/> />
</label> </label>
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Signal Fill Timing</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">Signal Fill Timing</span>
<select <Select
value={fillOnNextBar ? 'next_open' : 'same_close'} value={fillOnNextBar ? 'next_open' : 'same_close'}
onChange={(event) => setFillOnNextBar(event.target.value === 'next_open')} onChange={(event) => setFillOnNextBar(event.target.value === 'next_open')}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
> options={[
<option value="next_open">Next bar open</option> { value: 'next_open', label: 'Next bar open' },
<option value="same_close">Same bar close</option> { value: 'same_close', label: 'Same bar close' },
</select> ]}
/>
</label> </label>
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Intra-candle Conflict</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">Intra-candle Conflict</span>
<select <Select
value={intraCandlePolicy} value={intraCandlePolicy}
onChange={(event) => setIntraCandlePolicy(event.target.value as BacktestIntraCandlePolicy)} onChange={(event) => setIntraCandlePolicy(event.target.value as BacktestIntraCandlePolicy)}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
> options={[
<option value="ohlc_path">OHLC path</option> { value: 'ohlc_path', label: 'OHLC path' },
<option value="stop_loss_first">Stop-loss first</option> { value: 'stop_loss_first', label: 'Stop-loss first' },
<option value="take_profit_first">Take-profit first</option> { value: 'take_profit_first', label: 'Take-profit first' },
</select> ]}
/>
</label> </label>
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Trigger Resolution</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">Trigger Resolution</span>
<select <Select
value={triggerTimeframe} value={triggerTimeframe}
onChange={(event) => setTriggerTimeframe(event.target.value as BacktestTriggerTimeframe)} onChange={(event) => setTriggerTimeframe(event.target.value as BacktestTriggerTimeframe)}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
> options={[
<option value="1m">1m trigger candles</option> { value: '1m', label: '1m trigger candles' },
<option value="off">Base timeframe only</option> { value: 'off', label: 'Base timeframe only' },
</select> ]}
/>
</label> </label>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">End-of-Window Position Handling</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">End-of-Window Position Handling</span>
<select <Select
value={forceCloseAtWindowEnd ? 'force_close' : 'open_at_end'} value={forceCloseAtWindowEnd ? 'force_close' : 'open_at_end'}
onChange={(event) => setForceCloseAtWindowEnd(event.target.value === 'force_close')} onChange={(event) => setForceCloseAtWindowEnd(event.target.value === 'force_close')}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
> options={[
<option value="open_at_end">Mark as OPEN_AT_END (Default)</option> { value: 'open_at_end', label: 'Mark as OPEN_AT_END (Default)' },
<option value="force_close">Force close at last candle</option> { value: 'force_close', label: 'Force close at last candle' },
</select> ]}
/>
</label> </label>
{sourceType === 'kraken' ? ( {sourceType === 'kraken' ? (
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Kraken Warm-up Lookback Candles</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">Kraken Warm-up Lookback Candles</span>
<input <Input
type="number" type="number"
min={100} min={100}
value={krakenLookbackCandles} value={krakenLookbackCandles}
onChange={(event) => setKrakenLookbackCandles(Math.max(100, Number(event.target.value || 0)))} onChange={(event) => setKrakenLookbackCandles(Math.max(100, Number(event.target.value || 0)))}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
/> />
</label> </label>
) : ( ) : (
<label className="space-y-1"> <label className="space-y-1">
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Upload Historical File</span> <span className="text-[10px] uppercase tracking-wider text-zinc-400">Upload Historical File</span>
<input <Input
type="file" type="file"
accept={sourceType === 'csv' ? '.csv,text/csv' : '.json,application/json'} accept={sourceType === 'csv' ? '.csv,text/csv' : '.json,application/json'}
onChange={async (event) => { onChange={async (event) => {
@ -448,7 +458,7 @@ export const BacktestComparePanel: React.FC<BacktestComparePanelProps> = ({ prof
setError(uploadError.message || 'Failed to read file'); setError(uploadError.message || 'Failed to read file');
} }
}} }}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-zinc-300" controlSize="sm"
/> />
</label> </label>
)} )}
@ -456,8 +466,7 @@ export const BacktestComparePanel: React.FC<BacktestComparePanelProps> = ({ prof
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<label className="flex items-center gap-2 text-xs text-zinc-300"> <label className="flex items-center gap-2 text-xs text-zinc-300">
<input <Checkbox
type="checkbox"
checked={useNormalizedCapital} checked={useNormalizedCapital}
onChange={(event) => setUseNormalizedCapital(event.target.checked)} onChange={(event) => setUseNormalizedCapital(event.target.checked)}
/> />
@ -468,12 +477,12 @@ export const BacktestComparePanel: React.FC<BacktestComparePanelProps> = ({ prof
{useNormalizedCapital ? 'Normalized Capital (USD)' : 'Capital Source'} {useNormalizedCapital ? 'Normalized Capital (USD)' : 'Capital Source'}
</span> </span>
{useNormalizedCapital ? ( {useNormalizedCapital ? (
<input <Input
type="number" type="number"
min={1} min={1}
value={normalizedCapitalUsd} value={normalizedCapitalUsd}
onChange={(event) => setNormalizedCapitalUsd(Number(event.target.value))} onChange={(event) => setNormalizedCapitalUsd(Number(event.target.value))}
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white" controlSize="sm"
/> />
) : ( ) : (
<div className="rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-zinc-400"> <div className="rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-zinc-400">
@ -493,18 +502,17 @@ export const BacktestComparePanel: React.FC<BacktestComparePanelProps> = ({ prof
</div> </div>
)} )}
<button <Button
type="button"
onClick={() => { void runCompare(); }} onClick={() => { void runCompare(); }}
disabled={running} disabled={running}
className={`w-full rounded-xl px-4 py-2 text-xs font-black uppercase tracking-wider transition-colors ${running size="lg"
? 'bg-zinc-800 text-zinc-500 cursor-not-allowed' className="w-full"
: 'bg-indigo-400 text-black hover:bg-indigo-300'
}`}
> >
{running {running
? `Running compare... (${runningIndex}/${selectedProfiles.length})` ? `Running compare... (${runningIndex}/${selectedProfiles.length})`
: 'Run Strategy Comparison'} : 'Run Strategy Comparison'}
</button> </Button>
</div> </div>
{rows.length > 0 && ( {rows.length > 0 && (