refactor(ui): align backtest configurator controls
This commit is contained in:
parent
f8b2107bff
commit
c429488358
@ -6,6 +6,7 @@ import type {
|
|||||||
BacktestTriggerTimeframe
|
BacktestTriggerTimeframe
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { parseSymbolsInput, toDateInputValue } from '../utils';
|
import { parseSymbolsInput, toDateInputValue } from '../utils';
|
||||||
|
import { Button, Input, Select } from '../../components/ui/Primitives';
|
||||||
|
|
||||||
type SourceType = 'csv' | 'json' | 'replay' | 'kraken';
|
type SourceType = 'csv' | 'json' | 'replay' | 'kraken';
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ export const BacktestConfigurator: React.FC<BacktestConfiguratorProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const defaultFrom = toDateInputValue(now - (30 * 24 * 60 * 60 * 1000));
|
const defaultFrom = toDateInputValue(now - (30 * 24 * 60 * 60 * 1000));
|
||||||
const defaultTo = toDateInputValue(now);
|
const defaultTo = toDateInputValue(now);
|
||||||
|
|
||||||
const [symbolsInput, setSymbolsInput] = useState((initialSymbols || []).join(', '));
|
const [symbolsInput, setSymbolsInput] = useState((initialSymbols || []).join(', '));
|
||||||
const [timeframe, setTimeframe] = useState<BacktestTimeframe>('15m');
|
const [timeframe, setTimeframe] = useState<BacktestTimeframe>('15m');
|
||||||
@ -136,42 +137,43 @@ export const BacktestConfigurator: React.FC<BacktestConfiguratorProps> = ({
|
|||||||
<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">Symbols</span>
|
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Symbols</span>
|
||||||
<input
|
<Input
|
||||||
value={symbolsInput}
|
value={symbolsInput}
|
||||||
onChange={(event) => setSymbolsInput(event.target.value)}
|
onChange={(event) => setSymbolsInput(event.target.value)}
|
||||||
placeholder="BTC/USDT, ETH/USDT"
|
placeholder="BTC/USDT, ETH/USDT"
|
||||||
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white focus:outline-none focus:border-emerald-400/50"
|
controlSize="sm"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<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 focus:outline-none focus:border-emerald-400/50"
|
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">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 focus:outline-none focus:border-emerald-400/50"
|
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 focus:outline-none focus:border-emerald-400/50"
|
controlSize="sm"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -179,43 +181,43 @@ export const BacktestConfigurator: React.FC<BacktestConfiguratorProps> = ({
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||||
<label className="space-y-1">
|
<label className="space-y-1">
|
||||||
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Initial Capital (USD)</span>
|
<span className="text-[10px] uppercase tracking-wider text-zinc-400">Initial Capital (USD)</span>
|
||||||
<input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
value={initialCapital}
|
value={initialCapital}
|
||||||
onChange={(event) => setInitialCapital(Number(event.target.value))}
|
onChange={(event) => setInitialCapital(Number(event.target.value))}
|
||||||
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white focus:outline-none focus:border-emerald-400/50"
|
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 focus:outline-none focus:border-emerald-400/50"
|
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 focus:outline-none focus:border-emerald-400/50"
|
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 focus:outline-none focus:border-emerald-400/50"
|
controlSize="sm"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -223,48 +225,52 @@ export const BacktestConfigurator: React.FC<BacktestConfiguratorProps> = ({
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
<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 focus:outline-none focus:border-emerald-400/50"
|
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 focus:outline-none focus:border-emerald-400/50"
|
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 focus:outline-none focus:border-emerald-400/50"
|
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>
|
||||||
<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 focus:outline-none focus:border-emerald-400/50"
|
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>
|
||||||
<div className="rounded-lg border border-white/10 bg-zinc-900/70 px-3 py-2">
|
<div className="rounded-lg border border-white/10 bg-zinc-900/70 px-3 py-2">
|
||||||
<p className="text-[10px] uppercase tracking-wider text-zinc-500">Replay Semantics</p>
|
<p className="text-[10px] uppercase tracking-wider text-zinc-500">Replay Semantics</p>
|
||||||
@ -277,7 +283,7 @@ export const BacktestConfigurator: React.FC<BacktestConfiguratorProps> = ({
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 items-end">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 items-end">
|
||||||
<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) => {
|
||||||
setSourceType(event.target.value as SourceType);
|
setSourceType(event.target.value as SourceType);
|
||||||
@ -289,29 +295,30 @@ export const BacktestConfigurator: React.FC<BacktestConfiguratorProps> = ({
|
|||||||
setSourceName('');
|
setSourceName('');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 text-xs text-white focus:outline-none focus:border-emerald-400/50"
|
controlSize="sm"
|
||||||
>
|
options={[
|
||||||
<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' },
|
||||||
<option value="kraken">Kraken Historical API</option>
|
{ value: 'kraken', label: 'Kraken Historical API' },
|
||||||
</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 focus:outline-none focus:border-emerald-400/50"
|
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) => {
|
||||||
@ -323,7 +330,7 @@ export const BacktestConfigurator: React.FC<BacktestConfiguratorProps> = ({
|
|||||||
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 focus:outline-none focus:border-emerald-400/50"
|
controlSize="sm"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
@ -339,16 +346,15 @@ export const BacktestConfigurator: React.FC<BacktestConfiguratorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
|
type="button"
|
||||||
onClick={() => { void handleRun(); }}
|
onClick={() => { void handleRun(); }}
|
||||||
disabled={running}
|
disabled={running}
|
||||||
className={`w-full rounded-xl px-4 py-2 text-xs font-black uppercase tracking-wider transition-colors ${running
|
className="w-full"
|
||||||
? 'bg-zinc-800 text-zinc-500 cursor-not-allowed'
|
size="lg"
|
||||||
: 'bg-emerald-400 text-black hover:bg-emerald-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{running ? 'Running Backtest...' : 'Run Backtest'}
|
{running ? 'Running Backtest...' : 'Run Backtest'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user