summaryrefslogtreecommitdiff
path: root/frontend/src/pages/RunsPage.svelte
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-18 08:26:57 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-18 08:26:57 +0800
commit45ecb4e9dfe225bc982e9af9315590bcb0783df7 (patch)
treee046f42ace89c09d60aa4f07db8450507364fdb5 /frontend/src/pages/RunsPage.svelte
parent33cac3eef7d9962d8803cd812553549d6f555516 (diff)
downloadstrategy-lab-45ecb4e9dfe225bc982e9af9315590bcb0783df7.tar.gz
feat(frontend): 结果/实验/策略/数据页迁移 shadcn 与新 Chart API
[变更性质] 四个业务标签页与数据集卡片迁移 shadcn 组件,图表消费切换到 layerchart 长格式 API,功能与数据语义不变。 [新增功能] 结果页权益曲线(策略权益/现金/基准,基准可缺省)与回撤曲线(desc 面积)改由 Chart.svelte data/series 渲染;实验记录页多回测归一对比 曲线改为快速长格式行构造。空数据行允许 undefined 列(基准缺口), Chart data 类型放宽 [key:string]: string|number|undefined。 [实现方案] RunsPage:查看入口改 shadcn Button(href 语义保留 hash 路由), 对比图数据由 compareOption(echarts option)化为 compare。 ProjectResultsTab:图表开关、日志折叠用 Button;equityData/ drawdownData $derived 计算峰值回撤。ProjectStrategyTab:快照输入、 冲突/AI/版本操作迁移 Button/Input。ProjectDataTab 与 DatasetCard: shadcn 组件 + 保留 banner error/warn/info、retry-row 等 legacy class, "使用相同请求重新获取"重试入口与 onRecreated 逻辑不变,配套 datasetCardRetry/datasetCardWarnings/projectDataTabRecreated 回归 测试。resultsAccounting.test.ts:layerchart 在 jsdom 挂载约 3 秒, suite 全量下易撞默认 5s 超时,首测提高至 20s。 [影响范围] npm run check 0 error、58 tests 全绿、npm run build 通过;真浏览器 375/768/1440 图表页零溢出(artifacts/ui-shadcn 截图与探针 JSON)。
Diffstat (limited to 'frontend/src/pages/RunsPage.svelte')
-rw-r--r--frontend/src/pages/RunsPage.svelte59
1 files changed, 31 insertions, 28 deletions
diff --git a/frontend/src/pages/RunsPage.svelte b/frontend/src/pages/RunsPage.svelte
index bd77892..154fd67 100644
--- a/frontend/src/pages/RunsPage.svelte
+++ b/frontend/src/pages/RunsPage.svelte
@@ -2,7 +2,7 @@
import Page from '../components/Page.svelte';
import Status from '../components/Status.svelte';
import Chart from '../components/Chart.svelte';
- import type { EChartsOption } from 'echarts';
+ import { Button } from '$lib/components/ui/button';
import { listRuns } from '../lib/client';
import { extractStatusMessage } from '../lib/state';
import { fmtDateTime, runStatusLabel, runStatusBadge, fmtPct, fmtNum, formatMoney } from '../lib/format';
@@ -40,31 +40,34 @@
else chosenIds = [...chosenIds.slice(-3), id];
}
- const compareOption: EChartsOption = $derived.by(() => {
- if (comparable.length === 0) return {};
- const series = comparable.map((r, idx) => {
- const eq = r.result?.equity ?? [];
- const base = eq[0]?.equity ?? 1;
- const colors = ['#0f766e', '#b45309', '#1d4ed8', '#8b1d8b'];
- return {
- name: `Run ${r.id.slice(0, 6)}.`,
- type: 'line' as const,
- data: eq.map((p) => p.equity / base),
- showSymbol: false,
- lineStyle: { width: 1.5 },
- itemStyle: { color: colors[idx % colors.length] }
- };
- });
- let xLen = Math.max(...comparable.map((r) => (r.result?.equity ?? []).length), 0);
- const longest = comparable.reduce((best, r) => ((r.result?.equity ?? []).length > best.length ? r.result!.equity : best), [] as { date: string }[]);
- return {
- legend: { top: 0 },
- grid: { top: 32, left: 64, right: 20, bottom: 50 },
- tooltip: { trigger: 'axis' },
- xAxis: { type: 'category', data: longest.map((p) => p.date) },
- yAxis: { type: 'value', scale: true, name: '归一净值' },
- series
- };
+ // long-format rows for the layerchart renderer: one row per index, one
+ // column per selected run, first-day-normalized equity
+ const compare = $derived.by(() => {
+ if (comparable.length === 0) return { data: [], series: [] };
+ const colors = ['#0f766e', '#b45309', '#1d4ed8', '#8b1d8b'];
+ const flat = comparable.map((r, i) => ({
+ run: r,
+ eq: r.result?.equity ?? [],
+ key: `s${i}`,
+ color: colors[i % colors.length]
+ }));
+ const len = Math.max(0, ...flat.map((f) => f.eq.length));
+ const data: { [key: string]: string | number; i: number }[] = [];
+ for (let i = 0; i < len; i++) {
+ const row: { [key: string]: string | number; i: number } = { i, date: flat[0].eq[i]?.date ?? String(i) };
+ for (const f of flat) {
+ const base = f.eq[0]?.equity ?? 1;
+ if (f.eq[i]) row[f.key] = f.eq[i].equity / base;
+ }
+ data.push(row);
+ }
+ const series = flat.map((f) => ({
+ key: f.key,
+ label: `Run ${f.run.id.slice(0, 6)}.`,
+ value: f.key,
+ color: f.color
+ }));
+ return { data, series };
});
const configDiffRows = $derived.by(() => {
@@ -133,7 +136,7 @@
<td>{r.result ? fmtPct(r.result.metrics.total_return) : '—'}</td>
<td>{r.result ? fmtPct(r.result.metrics.max_drawdown) : '—'}</td>
<td>
- <a class="btn small" href={`#/projects/${r.project_id}/results?run=${r.id}`}>查看</a>
+ <Button size="sm" variant="outline" href={`#/projects/${r.project_id}/results?run=${r.id}`}>查看</Button>
</td>
</tr>
{/each}
@@ -168,7 +171,7 @@
{#if comparable.length > 1}
<div class="card stack">
<h2>收益曲线对齐比较(首日归一)</h2>
- <Chart option={compareOption} ariaLabel="多回测对比曲线" height={340} />
+ <Chart data={compare.data} series={compare.series} ariaLabel="多回测对比曲线" height={340} />
<p class="hint">起点对齐为 1,仅叠加不同起点的曲线时只在各自区间内展示。</p>
</div>
{:else}