diff options
Diffstat (limited to 'frontend/src/pages/ProjectResultsTab.svelte')
| -rw-r--r-- | frontend/src/pages/ProjectResultsTab.svelte | 165 |
1 files changed, 95 insertions, 70 deletions
diff --git a/frontend/src/pages/ProjectResultsTab.svelte b/frontend/src/pages/ProjectResultsTab.svelte index 82a7009..181d6a4 100644 --- a/frontend/src/pages/ProjectResultsTab.svelte +++ b/frontend/src/pages/ProjectResultsTab.svelte @@ -19,6 +19,8 @@ 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 * as Tabs from '$lib/components/ui/tabs'; import type { Run, BacktestResult } from '../lib/types'; interface Props { @@ -113,14 +115,19 @@ const mr = $derived(result ? metricsRow(result.metrics) : []); // 投入口径 reporting(任务 A):投入/持仓估值/回报,口径注明不含闲置现金 const cap = $derived(result ? capitalReporting(result) : null); + + const tile = 'gap-1 min-w-0'; + const tileLabel = 'text-xs text-muted-foreground'; + const tileValue = 'text-2xl font-bold tabular-nums text-foreground leading-tight break-all'; + const metricValueProps = 'data-testid="metric-value"'; </script> -<div class="stack"> - <div class="spread"> - <h2>回测结果</h2> +<div class="flex flex-col gap-3"> + <div class="flex flex-wrap justify-between items-center gap-2"> + <h2 class="text-lg font-semibold">回测结果</h2> {#if run} - <span class="row"> - <span class="hint">Run {run.id.slice(0, 8)}…</span> + <span class="flex items-center gap-2"> + <span class="text-sm text-muted-foreground">Run {run.id.slice(0, 8)}…</span> <Badge variant={runStatusBadge[run.status] === 'ok' ? 'secondary' : runStatusBadge[run.status] === 'warn' ? 'outline' : 'destructive'}>{runStatusLabel[run.status]}</Badge> </span> {/if} @@ -134,53 +141,66 @@ {#if run && !busy && !error} {#if history.length > 1} - <div class="row"> - <label for="runsel" class="hint" style="margin:0">查看其他回测:</label> - <select id="runsel" style="max-width:320px" value={run?.id ?? ""} onchange={(e) => switchToRun((e.target as HTMLSelectElement).value)}> - {#each history as h (h.id)} - <option value={h.id}>{fmtDateTime(h.created_at)} · {runStatusLabel[h.status]}</option> - {/each} - </select> + <div class="flex items-center gap-2"> + <span class="text-sm text-muted-foreground">查看其他回测:</span> + <Select.Root + type="single" + value={run?.id ?? ''} + onValueChange={(v) => switchToRun(v)} + > + <Select.Trigger data-testid="runsel" class="w-72 max-w-[320px]"> + {#snippet children()} + <span> + {history.find((h) => h.id === run?.id)?.created_at && fmtDateTime(history.find((h) => h.id === run?.id)!.created_at)} · {history.find((h) => h.id === run?.id) && runStatusLabel[history.find((h) => h.id === run?.id)!.status]} + </span> + {/snippet} + </Select.Trigger> + <Select.Content> + {#each history as h (h.id)} + <Select.Item value={h.id}>{fmtDateTime(h.created_at)} · {runStatusLabel[h.status]}</Select.Item> + {/each} + </Select.Content> + </Select.Root> </div> {/if} {#if run.error} - <Alert variant="destructive" class="bg-[var(--danger-soft)] border-[#f2c7c7]"><span aria-hidden="true">✕</span><span>{run.error}</span></Alert> + <Alert variant="destructive"><span aria-hidden="true">✕</span><span>{run.error}</span></Alert> {/if} {#if !result} <Status busy={false} empty="该回测尚无结果" emptyHint={runStatusBadge[run.status] === "danger" ? "见错误提示" : "回测仍在执行中,稍后刷新"} /> {:else} {#if result.warnings?.length} - <Alert variant="default" class="bg-[var(--warn-soft)] border-[#f0ddb4]"> + <Alert variant="default"> <span aria-hidden="true">⚠</span> - <ul class="ul-plain"> + <ul class="list-disc pl-5 mt-0.5"> {#each result.warnings as w, i (i)}<li>{w}</li>{/each} </ul> </Alert> {/if} - <Card.Root size="sm"> + <Card.Root size="sm" class="p-4 flex flex-col gap-3"> {#if result.metrics} {@const mrow = result.metrics} - <div class="metric-grid"> - <Card.Root size="sm" class="m-tile"><div class="mlabel">总收益率</div><div class="mvalue">{fmtPct(mrow.total_return)}</div></Card.Root> - <Card.Root size="sm" class="m-tile"><div class="mlabel">年化收益率</div><div class="mvalue">{fmtPct(mrow.annual_return)}</div></Card.Root> - <Card.Root size="sm" class="m-tile"><div class="mlabel">最大回撤</div><div class="mvalue">{fmtPct(mrow.max_drawdown)}</div></Card.Root> - <Card.Root size="sm" class="m-tile"><div class="mlabel">夏普比率</div><div class="mvalue">{fmtNum(mrow.sharpe)}</div></Card.Root> - <Card.Root size="sm" class="m-tile"><div class="mlabel">平仓回合数</div><div class="mvalue">{mrow.trade_count ?? "—"}</div></Card.Root> - <Card.Root size="sm" class="m-tile"><div class="mlabel">期末权益</div><div class="mvalue">{formatMoney(mrow.final_equity)}</div></Card.Root> - </div> + <div class="grid grid-cols-2 md:grid-cols-3 gap-2.5"> + <Card.Root size="sm" class={tile}><div class={tileLabel}>总收益率</div><div class={tileValue} data-testid="metric-value">{fmtPct(mrow.total_return)}</div></Card.Root> + <Card.Root size="sm" class={tile}><div class={tileLabel}>年化收益率</div><div class={tileValue} data-testid="metric-value">{fmtPct(mrow.annual_return)}</div></Card.Root> + <Card.Root size="sm" class={tile}><div class={tileLabel}>最大回撤</div><div class={tileValue} data-testid="metric-value">{fmtPct(mrow.max_drawdown)}</div></Card.Root> + <Card.Root size="sm" class={tile}><div class={tileLabel}>夏普比率</div><div class={tileValue} data-testid="metric-value">{fmtNum(mrow.sharpe)}</div></Card.Root> + <Card.Root size="sm" class={tile}><div class={tileLabel}>平仓回合数</div><div class={tileValue} data-testid="metric-value">{mrow.trade_count ?? "—"}</div></Card.Root> + <Card.Root size="sm" class={tile}><div class={tileLabel}>期末权益</div><div class={tileValue} data-testid="metric-value">{formatMoney(mrow.final_equity)}</div></Card.Root> + </div> {/if} {#if cap} - <div class="metric-grid" data-testid="capital-reporting"> - <Card.Root size="sm" class="m-tile"><div class="mlabel">累计投入</div><div class="mvalue">{formatMoney(cap.invested)}</div></Card.Root> - <Card.Root size="sm" class="m-tile"><div class="mlabel">最终持仓估值</div><div class="mvalue">{formatMoney(cap.holdingValue)}</div></Card.Root> - <Card.Root size="sm" class="m-tile"><div class="mlabel">投入回报率</div><div class="mvalue">{cap.returnPct ?? '—'}</div></Card.Root> + <div class="grid grid-cols-2 md:grid-cols-3 gap-2.5" data-testid="capital-reporting"> + <Card.Root size="sm" class={tile}><div class={tileLabel}>累计投入</div><div class={tileValue} data-testid="metric-value">{formatMoney(cap.invested)}</div></Card.Root> + <Card.Root size="sm" class={tile}><div class={tileLabel}>最终持仓估值</div><div class={tileValue} data-testid="metric-value">{formatMoney(cap.holdingValue)}</div></Card.Root> + <Card.Root size="sm" class={tile}><div class={tileLabel}>投入回报率</div><div class={tileValue} data-testid="metric-value">{cap.returnPct ?? '—'}</div></Card.Root> </div> - <p class="hint">投入口径不含初始账户闲置现金:累计投入 = 首日现金 − 末日现金;持仓估值 = 末日持仓 × 最新收盘价。</p> + <p class="text-sm text-muted-foreground">投入口径不含初始账户闲置现金:累计投入 = 首日现金 − 末日现金;持仓估值 = 末日持仓 × 最新收盘价。</p> {/if} - <p class="hint"> + <p class="text-sm text-muted-foreground"> 平仓回合数为已完成的开仓并全部平仓的完整回合(1 个回合 = 1 次平仓),不含撤单与未平仓的单笔成交;成交记录中的“金额”为真实成交额(数量 × 成交价),非持仓成本。结果非投资建议;未完全模拟流动性/涨跌停等限制。 引擎 {result.engine?.name} {result.engine?.version} {#if result.elapsed_ms}<span> · 耗时 {(result.elapsed_ms / 1000).toFixed(1)}s</span>{/if} @@ -188,11 +208,13 @@ </p> </Card.Root> - <Card.Root size="sm" class="stack"> - <div class="row"> - <Button variant={chartMode === "equity" ? "default" : "outline"} size="sm" aria-pressed={chartMode === "equity"} onclick={() => (chartMode = "equity")}>权益曲线({result.equity.length} 点)</Button> - <Button variant={chartMode === "drawdown" ? "default" : "outline"} size="sm" aria-pressed={chartMode === "drawdown"} onclick={() => (chartMode = "drawdown")}>回撤曲线</Button> - </div> + <Card.Root size="sm" class="p-4 flex flex-col gap-3"> + <Tabs.Root value={chartMode} onValueChange={(v) => (chartMode = v as 'equity' | 'drawdown')} class="w-fit"> + <Tabs.List variant="line"> + <Tabs.Trigger value="equity">权益曲线({result.equity.length} 点)</Tabs.Trigger> + <Tabs.Trigger value="drawdown">回撤曲线</Tabs.Trigger> + </Tabs.List> + </Tabs.Root> {#if chartMode === "equity"} <Chart data={equityData} series={equitySeries} ariaLabel="策略权益曲线" height={340} extras={[{ key: 'dd', label: '回撤', color: '#134e4a' }]} /> {:else} @@ -200,59 +222,62 @@ {/if} </Card.Root> - <Card.Root size="sm" class="stack"> - <h2>订单与持仓记录</h2> - <div class="cols-2"> + <Card.Root size="sm" class="p-4 flex flex-col gap-4"> + <h2 class="text-lg font-semibold m-0">订单与持仓记录</h2> + <div class="grid md:grid-cols-[1.4fr_1fr] gap-4 items-start"> <div> - <h3>成交记录 ({result.trades.length})</h3> + <h3 class="text-base font-semibold">成交记录 ({result.trades.length})</h3> {#if result.trades.length > 0} - <Table.Root> - <Table.Header> - <Table.Row> - <Table.Head>日期</Table.Head> - <Table.Head>标的</Table.Head> - <Table.Head>方向</Table.Head> - <Table.Head>数量</Table.Head> - <Table.Head>价格</Table.Head> - <Table.Head>金额</Table.Head> - <Table.Head>佣金</Table.Head> - </Table.Row> - </Table.Header> - <Table.Body> - {#each result.trades.slice(0, 80) as t, i (i)} + <div class="overflow-x-auto"> + <Table.Root> + <Table.Header> <Table.Row> - <Table.Cell>{t.date}</Table.Cell> - <Table.Cell class="num">{t.symbol}</Table.Cell> - <Table.Cell>{t.side === "buy" ? "买入" : "卖出"}</Table.Cell> - <Table.Cell class="num">{t.quantity}</Table.Cell> - <Table.Cell class="num">{fmtNum(t.price, 3)}</Table.Cell> - <Table.Cell class="num">{fmtNum(t.value)}</Table.Cell> - <Table.Cell class="num">{fmtNum(t.commission, 4)}</Table.Cell> + <Table.Head>日期</Table.Head> + <Table.Head>标的</Table.Head> + <Table.Head>方向</Table.Head> + <Table.Head>数量</Table.Head> + <Table.Head>价格</Table.Head> + <Table.Head>金额</Table.Head> + <Table.Head>佣金</Table.Head> </Table.Row> - {/each} - </Table.Body> - </Table.Root> + </Table.Header> + <Table.Body> + {#each result.trades.slice(0, 80) as t, i (i)} + <Table.Row> + <Table.Cell>{t.date}</Table.Cell> + <Table.Cell class="tabular-nums">{t.symbol}</Table.Cell> + <Table.Cell>{t.side === "buy" ? "买入" : "卖出"}</Table.Cell> + <Table.Cell class="tabular-nums">{t.quantity}</Table.Cell> + <Table.Cell class="tabular-nums">{fmtNum(t.price, 3)}</Table.Cell> + <Table.Cell class="tabular-nums">{fmtNum(t.value)}</Table.Cell> + <Table.Cell class="tabular-nums">{fmtNum(t.commission, 4)}</Table.Cell> + </Table.Row> + {/each} + </Table.Body> + </Table.Root> + </div> {#if result.trades.length > 80} - <p class="hint">仅显示前 80 条,共 {result.trades.length} 条</p> + <p class="text-sm text-muted-foreground">仅显示前 80 条,共 {result.trades.length} 条</p> {/if} {:else} - <p class="hint">本区间未发生成交</p> + <p class="text-sm text-muted-foreground">本区间未发生成交</p> {/if} </div> <div> - <h3>策略日志</h3> + <h3 class="text-base font-semibold">策略日志</h3> <Button variant="outline" size="sm" onclick={() => (logsOpen = !logsOpen)} aria-expanded={logsOpen}> {logsOpen ? "收起日志" : `展开日志(${result.logs.length} 行)`} </Button> {#if logsOpen} - <pre class="logs">{result.logs.join("\n")}</pre> + <pre class="font-mono text-sm p-2 bg-muted rounded-md overflow-x-auto whitespace-pre-wrap break-all mt-2">{result.logs.join("\n")}</pre> {/if} </div> </div> <div> - <h3>数据与引擎 - <span class="hint">本轮使用的引擎版本、耗时与消耗内存均在上方备注。</span> + <h3 class="text-base font-semibold"> + 数据与引擎 + <span class="text-sm text-muted-foreground font-normal">本轮使用的引擎版本、耗时与消耗内存均在上方备注。</span> </h3> - <p class="hint"> + <p class="text-sm text-muted-foreground"> 结果由对应策略代码、配置、数据清单(数据哈希 {result.data_manifest_hash?.slice(0, 12)})以及当时引擎共同决定。 </p> </div> |
