diff options
Diffstat (limited to 'frontend/src/pages/ProjectBacktestTab.svelte')
| -rw-r--r-- | frontend/src/pages/ProjectBacktestTab.svelte | 136 |
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> |
