summaryrefslogtreecommitdiff
path: root/frontend/src/pages/ProjectBacktestTab.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/pages/ProjectBacktestTab.svelte')
-rw-r--r--frontend/src/pages/ProjectBacktestTab.svelte136
1 files changed, 75 insertions, 61 deletions
diff --git a/frontend/src/pages/ProjectBacktestTab.svelte b/frontend/src/pages/ProjectBacktestTab.svelte
index a9e7799..b640189 100644
--- a/frontend/src/pages/ProjectBacktestTab.svelte
+++ b/frontend/src/pages/ProjectBacktestTab.svelte
@@ -17,6 +17,9 @@
import { Badge } from '$lib/components/ui/badge';
import * as Card from '$lib/components/ui/card';
import * as Table from '$lib/components/ui/table';
+ import * as Select from '$lib/components/ui/select';
+ import { Input } from '$lib/components/ui/input';
+ import { Label } from '$lib/components/ui/label';
interface Props {
projectId: string;
@@ -189,8 +192,6 @@
}
}
-
-
async function onCancel() {
if (!latest) return;
cancelBusy = true;
@@ -223,79 +224,92 @@
const canRerun = $derived(latest?.status === 'succeeded' || latest?.status === 'failed' || latest?.status === 'cancelled');
</script>
-<div class="stack">
- <Card.Root size="sm" class="stack-sec">
- <h2>发起回测</h2>
+<div class="flex flex-col gap-3">
+ <Card.Root size="sm" class="p-4 flex flex-col gap-3">
+ <h2 class="text-lg font-semibold">发起回测</h2>
<Status busy={busyDatasets} error={cfgErr} busyText="检查就绪的数据集…" empty={null} />
{#if !busyDatasets && datasetsReady.length === 0 && !cfgErr}
<Status busy={false} empty="没有就绪的数据集" emptyHint="先在「数据」标签创建并等待数据集状态变为【就绪】,再回到这里发起回测" />
{/if}
{#if datasetsReady.length > 0}
- <div class="grid3 wrap-3">
- <label>
- 数据集
- <select bind:value={datasetId}>
- <option value="" disabled>选择数据集…</option>
- {#each datasetsReady as d (d.id)}
- <option value={d.id}>
- {d.name ?? d.id.slice(0, 8)} · {d.request.start_date}~{d.request.end_date} · {d.request.instruments.map((i) => i.symbol).join(',')}
- </option>
- {/each}
- </select>
- </label>
- <label>
- 起始资金
- <input class="num" type="number" bind:value={capital} min="1" step="1000" />
- </label>
- <div class="row2">
- <label>
- 佣金率
- <input class="num" type="number" bind:value={commission} min="0" max="0.1" step="0.0001" />
- </label>
- <label>
- 滑点率
- <input class="num" type="number" bind:value={slippage} min="0" max="0.1" step="0.0001" />
- </label>
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
+ <div>
+ <Label for="bt-dataset">数据集</Label>
+ <Select.Root
+ type="single"
+ value={datasetId}
+ onValueChange={(v) => (datasetId = v)}
+ >
+ <Select.Trigger data-testid="dataset-select" id="bt-dataset" class="w-full">
+ {datasetId
+ ? (() => {
+ const d = datasetsReady.find((x) => x.id === datasetId);
+ return d ? `${d.name ?? d.id.slice(0, 8)} · ${d.request.start_date}~${d.request.end_date}` : '选择数据集…';
+ })()
+ : '选择数据集…'}
+ </Select.Trigger>
+ <Select.Content>
+ {#each datasetsReady as d (d.id)}
+ <Select.Item value={d.id}>
+ {d.name ?? d.id.slice(0, 8)} · {d.request.start_date}~{d.request.end_date} · {d.request.instruments.map((i) => i.symbol).join(',')}
+ </Select.Item>
+ {/each}
+ </Select.Content>
+ </Select.Root>
+ </div>
+ <div>
+ <Label for="bt-capital">起始资金</Label>
+ <Input id="bt-capital" class="tabular-nums" type="number" bind:value={capital} min="1" step="1000" />
+ </div>
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
+ <div>
+ <Label for="bt-commission">佣金率</Label>
+ <Input id="bt-commission" class="tabular-nums" type="number" bind:value={commission} min="0" max="0.1" step="0.0001" />
+ </div>
+ <div>
+ <Label for="bt-slippage">滑点率</Label>
+ <Input id="bt-slippage" class="tabular-nums" type="number" bind:value={slippage} min="0" max="0.1" step="0.0001" />
+ </div>
+ </div>
+ <div>
+ <Label for="bt-benchmark">基准代码(可选)</Label>
+ <Input id="bt-benchmark" bind:value={benchmark} placeholder="例如 000300.SH 指数代码" />
+ </div>
+ <div class="sm:col-span-2 lg:col-span-3">
+ <Label for="bt-params">策略参数(JSON,可选)</Label>
+ <textarea id="bt-params" class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50" rows="1" bind:value={paramsText} placeholder="输入 JSON 参数,例如 fast/slow 周期"></textarea>
</div>
- <label>
- 基准代码(可选)
- <input bind:value={benchmark} placeholder="例如 000300.SH 指数代码" />
- </label>
- <label>
- 策略参数(JSON,可选)
- <textarea rows="1" bind:value={paramsText} placeholder='输入 JSON 参数,例如 fast/slow 周期'></textarea>
- </label>
</div>
{#if needAck}
- <Alert variant="default" class="bg-[var(--warn-soft)] border-[#f0ddb4]">
+ <Alert variant="default">
<span aria-hidden="true">⚠</span>
<div>
该数据集存在覆盖/预警信息,需要明确知晓后才能启动:
- <ul>
+ <ul class="list-disc pl-5 mt-1">
{#each datasetWarnMsgs as m, i (i)}<li>{m}</li>{/each}
</ul>
- <label class="checkline">
+ <label class="inline-flex items-center gap-2 mt-1">
<input type="checkbox" bind:checked={acknowledged} />
我已知晓并用当前数据继续
</label>
</div>
</Alert>
{/if}
- <div class="row">
+ <div class="flex items-center gap-2 flex-wrap">
<Button onclick={startRun} disabled={!datasetId}>开始回测</Button>
{#if selectedDataset}
- <span class="hint">本次回测与该数据集、当前策略快照一同记录,可按原始条件重跑</span>
+ <span class="text-sm text-muted-foreground">本次回测与该数据集、当前策略快照一同记录,可按原始条件重跑</span>
{/if}
</div>
<Status busy={false} error={cfgErr} empty={actionMsg} />
{/if}
</Card.Root>
- <Card.Root size="sm" class="stack-sec">
- <div class="spread">
- <h2>最近一次回测</h2>
+ <Card.Root size="sm" class="p-4 flex flex-col gap-3">
+ <div class="flex flex-wrap justify-between items-center gap-2">
+ <h2 class="text-lg font-semibold">最近一次回测</h2>
{#if latest}
- <span class="row">
+ <span class="flex items-center gap-2">
<Badge variant="outline">Run 分支 {latest.version_id?.slice(0,8) ?? '—'}</Badge>
<Badge variant={runStatusBadge[latest.status] === 'ok' ? 'secondary' : runStatusBadge[latest.status] === 'warn' ? 'outline' : 'destructive'}>{runStatusLabel[latest.status]}</Badge>
</span>
@@ -309,33 +323,33 @@
empty={null}
error={latestErr}
/>
- <div class="rows-grid">
+ <div class="overflow-x-auto">
<Table.Root>
<Table.Body>
<Table.Row><Table.Head>启动时间</Table.Head><Table.Cell>{fmtDateTime(latest.created_at)}</Table.Cell></Table.Row>
{#if latest.started_at}<Table.Row><Table.Head>开始执行</Table.Head><Table.Cell>{fmtDateTime(latest.started_at)}</Table.Cell></Table.Row>{/if}
{#if latest.finished_at}<Table.Row><Table.Head>结束时间</Table.Head><Table.Cell>{fmtDateTime(latest.finished_at)}</Table.Cell></Table.Row>{/if}
- <Table.Row><Table.Head>代码版本</Table.Head><Table.Cell><span class="num">{latest.version_id.slice(0, 10)}</span></Table.Cell></Table.Row>
- <Table.Row><Table.Head>数据集</Table.Head><Table.Cell><span class="num">{latest.dataset_id?.slice(0, 8)}…</span></Table.Cell></Table.Row>
+ <Table.Row><Table.Head>代码版本</Table.Head><Table.Cell><span class="tabular-nums">{latest.version_id.slice(0, 10)}</span></Table.Cell></Table.Row>
+ <Table.Row><Table.Head>数据集</Table.Head><Table.Cell><span class="tabular-nums">{latest.dataset_id?.slice(0, 8)}…</span></Table.Cell></Table.Row>
<Table.Row><Table.Head>资金</Table.Head><Table.Cell>{formatMoney(latest.config.capital)} · 佣金 {fmtPct(latest.config.commission, 4)} · 滑点 {fmtPct(latest.config.slippage, 4)}</Table.Cell></Table.Row>
</Table.Body>
</Table.Root>
</div>
{#if latest.error}
- <Alert variant="destructive" class="bg-[var(--danger-soft)] border-[#f2c7c7]"><span aria-hidden="true">✕</span><span>{latest.error}</span></Alert>
+ <Alert variant="destructive"><span aria-hidden="true">✕</span><span>{latest.error}</span></Alert>
{/if}
{#if latest.result?.warnings?.length}
- <Alert variant="default" class="bg-[var(--warn-soft)] border-[#f0ddb4]">
+ <Alert variant="default">
<span aria-hidden="true">⚠</span>
- <ul class="plain-list">
+ <ul class="list-disc pl-5 mt-0.5">
{#each latest.result.warnings as w, i (i)}<li>{w}</li>{/each}
</ul>
</Alert>
{/if}
{#if latest.status === 'succeeded' && latest.result}
- <Button variant="outline" href={`#/projects/${projectId}/results?run=${latest.id}`}>查看完整结果与图表</Button>
+ <Button variant="outline" href={`#/projects/${projectId}/results?run=${latest.id}`} class="w-fit">查看完整结果与图表</Button>
{/if}
- <div class="row">
+ <div class="flex items-center gap-2 flex-wrap">
{#if canCancel}
<Button variant="destructive" onclick={onCancel} disabled={cancelBusy}>
{cancelBusy ? '取消中…' : '取消回测'}
@@ -352,11 +366,11 @@
{/if}
</Card.Root>
- <div class="stack">
- <h2>回测历史</h2>
+ <div class="flex flex-col gap-3">
+ <h2 class="text-lg font-semibold">回测历史</h2>
<Status busy={historyBusy} error={historyErr} busyText="载入历史…" empty={null} />
{#if history && history.length > 0}
- <div class="px-3 py-2 rounded-[var(--radius-lg)] border border-border bg-card">
+ <Card.Root size="sm">
<Table.Root>
<Table.Header>
<Table.Row>
@@ -373,7 +387,7 @@
<Table.Cell>{fmtDateTime(h.created_at)}</Table.Cell>
<Table.Cell>{h.status === 'succeeded' ? '完成' : h.status === 'failed' ? '失败' : h.status === 'running' ? '执行中' : h.status === 'cancelled' ? '已取消' : '排队中'}</Table.Cell>
<Table.Cell>{formatMoney(h.config.capital)} · {fmtPct(h.config.commission, 4)} / {fmtPct(h.config.slippage, 4)}</Table.Cell>
- <Table.Cell class="num">{h.dataset_id?.slice(0, 8)}…</Table.Cell>
+ <Table.Cell class="tabular-nums">{h.dataset_id?.slice(0, 8)}…</Table.Cell>
<Table.Cell>
<Button variant="outline" size="sm" href={`#/projects/${projectId}/results?run=${h.id}`}>结果</Button>
</Table.Cell>
@@ -381,7 +395,7 @@
{/each}
</Table.Body>
</Table.Root>
- </div>
+ </Card.Root>
{/if}
</div>
- </div>
+</div>