summaryrefslogtreecommitdiff
path: root/frontend/src/pages
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-18 15:49:44 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-18 15:49:44 +0800
commit375584174fb7191a57c5423fea45924ad6d812d5 (patch)
tree76042d5d38b8aafc8bb7b30caa6f8e04fc3305c1 /frontend/src/pages
parentfc952a2876362bf834a60c35ef0f70b0278df197 (diff)
downloadstrategy-lab-375584174fb7191a57c5423fea45924ad6d812d5.tar.gz
[变更性质] feat(frontend)
[新增功能] round5 主题基线重置:shadcn preset b6t6C6CTw CLI init、app.css 仅保留 tailwind + shadcn 主题变量层 [实现方案] 删除 round3 自定义 token/布局原语/legacy 别名;theme.ts 删除;mobileCss 守门测试重写 [影响范围] src/app.css、components.json、lib/utils.ts、lib/theme.ts、测试
Diffstat (limited to 'frontend/src/pages')
-rw-r--r--frontend/src/pages/ProjectResultsTab.svelte165
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>