summaryrefslogtreecommitdiff
path: root/frontend/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/pages')
-rw-r--r--frontend/src/pages/ProjectDataTab.svelte9
-rw-r--r--frontend/src/pages/ProjectResultsTab.svelte108
-rw-r--r--frontend/src/pages/ProjectStrategyTab.svelte35
-rw-r--r--frontend/src/pages/RunsPage.svelte59
4 files changed, 92 insertions, 119 deletions
diff --git a/frontend/src/pages/ProjectDataTab.svelte b/frontend/src/pages/ProjectDataTab.svelte
index e3b1923..284aec7 100644
--- a/frontend/src/pages/ProjectDataTab.svelte
+++ b/frontend/src/pages/ProjectDataTab.svelte
@@ -5,6 +5,7 @@
import { listDatasets } from '../lib/client';
import { extractStatusMessage } from '../lib/state';
import type { Dataset } from '../lib/types';
+ import { Button } from '$lib/components/ui/button';
interface Props {
projectId?: string;
@@ -17,7 +18,7 @@
let error: string | null = $state(null);
// svelte-ignore state_referenced_locally
let wizardOpen = $state(showWizard);
-
+
let refreshTimer: ReturnType<typeof setInterval> | null = null;
@@ -61,9 +62,9 @@
<div class="stack">
{#if wizardOpen}
<DatasetWizard onCreated={() => void load()} />
- <button class="btn ghost" onclick={() => (wizardOpen = false)}>收起新建面板</button>
+ <Button variant="ghost" size="sm" onclick={() => (wizardOpen = false)}>收起新建面板</Button>
{:else}
- <button class="btn primary" onclick={() => (wizardOpen = true)}>新建数据集</button>
+ <Button size="sm" class="w-fit" onclick={() => (wizardOpen = true)}>新建数据集</Button>
{/if}
<h2>我的数据集</h2>
@@ -79,7 +80,7 @@
{#if !busy && !error && items.length > 0}
{#each items as ds (ds.id)}
- <DatasetCard dsId={ds.id} />
+ <DatasetCard dsId={ds.id} onRecreated={() => void load()} />
{/each}
{/if}
</div>
diff --git a/frontend/src/pages/ProjectResultsTab.svelte b/frontend/src/pages/ProjectResultsTab.svelte
index 4149213..5ff03c0 100644
--- a/frontend/src/pages/ProjectResultsTab.svelte
+++ b/frontend/src/pages/ProjectResultsTab.svelte
@@ -1,8 +1,8 @@
<script lang="ts">
import Status from '../components/Status.svelte';
import Chart from '../components/Chart.svelte';
- import type { EChartsOption, LineSeriesOption } from 'echarts';
-import { listRuns, getRun } from '../lib/client';
+ import { Button } from '$lib/components/ui/button';
+ import { listRuns, getRun } from '../lib/client';
import { extractStatusMessage } from '../lib/state';
import {
fmtDateTime,
@@ -55,73 +55,42 @@ import { listRuns, getRun } from '../lib/client';
const result = $derived<BacktestResult | null>(run?.result ?? null);
const hasEquity = $derived(!!result && result.equity.length > 0);
- const equityOption: EChartsOption = $derived.by(() => {
- if (!result || !hasEquity) return {};
- const dates = result.equity.map((p) => p.date);
- const series: LineSeriesOption[] = [
- {
- name: "策略权益",
- type: "line" as const,
- data: result.equity.map((p) => p.equity),
- showSymbol: false,
- lineStyle: { width: 1.6 },
- itemStyle: { color: "#0f766e" }
- },
- {
- name: "现金",
- type: "line" as const,
- data: result.equity.map((p) => p.cash),
- showSymbol: false,
- lineStyle: { type: "dashed", opacity: 0.65 },
- itemStyle: { color: "#94a3b8" }
- }
+ // long-format rows for the layerchart renderer ({ i, date, ...columns })
+ const equityData = $derived.by(() => {
+ if (!result || !hasEquity) return [];
+ return result.equity.map((p, i) => ({
+ i,
+ date: p.date,
+ equity: p.equity,
+ cash: p.cash,
+ benchmark: p.benchmark ?? undefined
+ }));
+ });
+
+ const hasBench = $derived(equityData.some((r) => r.benchmark !== undefined && r.benchmark !== null));
+
+ const equitySeries = $derived.by(() => {
+ if (!result || !hasEquity) return [];
+ const s = [
+ { key: 'equity', label: '策略权益', value: 'equity', color: '#0f766e' },
+ { key: 'cash', label: '现金', value: 'cash', color: '#94a3b8', dashed: true }
];
- const bench = result.equity.map((p) => (p.benchmark === null || p.benchmark === undefined ? null : p.benchmark));
- if (bench.some((x) => x !== null && x !== undefined)) {
- series.push({
- name: "基准",
- type: "line" as const,
- data: bench as number[],
- showSymbol: false,
- itemStyle: { color: "#eab308" }
- });
- }
- return {
- title: { text: undefined },
- legend: { top: 0 },
- grid: { top: 32, left: 60, right: 24, bottom: 54 },
- tooltip: { trigger: "axis" },
- xAxis: { type: "category", data: dates },
- yAxis: { type: "value", scale: true, name: "金额" },
- series
- };
+ if (hasBench) s.push({ key: 'benchmark', label: '基准', value: 'benchmark', color: '#eab308' });
+ return s;
});
- const drawdownOption: EChartsOption = $derived.by(() => {
- if (!result || !hasEquity) return {};
- const dates = result.equity.map((p) => p.date);
+ const drawdownData = $derived.by(() => {
+ if (!result || !hasEquity) return [];
let peak = -Infinity;
- const dd = result.equity.map((p) => {
+ return result.equity.map((p, i) => {
peak = Math.max(peak, p.equity);
- return -((peak - p.equity) / peak) * 100;
+ return { i, date: p.date, dd: -((peak - p.equity) / peak) * 100 };
});
- return {
- grid: { top: 24, left: 60, right: 24, bottom: 54 },
- tooltip: { trigger: "axis", valueFormatter: (v) => `${(v as number).toFixed(2)}%` },
- xAxis: { type: "category", data: dates },
- yAxis: { type: "value", name: "回撤 %", max: 0 },
- series: [
- {
- name: "回撤",
- type: "line" as const,
- data: dd,
- showSymbol: false,
- areaStyle: { color: "rgba(30, 74, 66, 0.25)" },
- lineStyle: { width: 1 },
- itemStyle: { color: "#134e4a" }
- }
- ]
- };
+ });
+
+ const drawdownSeries = $derived.by(() => {
+ if (!result || !hasEquity) return [];
+ return [{ key: 'dd', label: '回撤', value: 'dd', color: '#134e4a', area: true }];
});
function switchToRun(id: string) {
@@ -220,13 +189,13 @@ import { listRuns, getRun } from '../lib/client';
<div class="card stack">
<div class="row">
- <button class="btn small" aria-pressed={chartMode === "equity"} onclick={() => (chartMode = "equity")}>权益曲线({result.equity.length} 点)</button>
- <button class="btn small" aria-pressed={chartMode === "drawdown"} onclick={() => (chartMode = "drawdown")}>回撤曲线</button>
+ <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>
{#if chartMode === "equity"}
- <Chart option={equityOption} ariaLabel="策略权益曲线" height={340} />
+ <Chart data={equityData} series={equitySeries} ariaLabel="策略权益曲线" height={340} />
{:else}
- <Chart option={drawdownOption} ariaLabel="回撤曲线" height={300} />
+ <Chart data={drawdownData} series={drawdownSeries} ariaLabel="回撤曲线" height={300} />
{/if}
</div>
@@ -273,10 +242,9 @@ import { listRuns, getRun } from '../lib/client';
</div>
<div>
<h3>策略日志</h3>
- <button class="btn small" onclick={() => (logsOpen = !logsOpen)} aria-expanded={logsOpen}>
+ <Button variant="outline" size="sm" onclick={() => (logsOpen = !logsOpen)} aria-expanded={logsOpen}>
{logsOpen ? "收起日志" : `展开日志(${result.logs.length} 行)`}
- </button>
- {#if logsOpen}
+ </Button> {#if logsOpen}
<pre class="logs">{result.logs.join("\n")}</pre>
{/if}
</div>
diff --git a/frontend/src/pages/ProjectStrategyTab.svelte b/frontend/src/pages/ProjectStrategyTab.svelte
index 5f2e242..0df9f1a 100644
--- a/frontend/src/pages/ProjectStrategyTab.svelte
+++ b/frontend/src/pages/ProjectStrategyTab.svelte
@@ -3,6 +3,8 @@
import CodeEditor from '../components/CodeEditor.svelte';
import DiffView from '../components/DiffView.svelte';
import Modal from '../components/Modal.svelte';
+ import { Button } from '$lib/components/ui/button';
+ import { Input } from '$lib/components/ui/input';
import { projectsStore } from '../lib/listsStore.svelte';
import {
getProject,
@@ -274,7 +276,7 @@
</p>
<CodeEditor value={code} onInput={onEditorInput} readOnly={false} label="策略源码" height={430} />
<div class="spread wrap">
- <input
+ <Input
bind:value={snapshotMessage}
placeholder="快照说明,例如:加入RSI过滤器(可选)"
style="flex:2 1 240px"
@@ -282,16 +284,16 @@
if (ev.key === 'Enter') void snapshot();
}}
/>
- <button class="btn" disabled={snapshotBusy} onclick={snapshot}>
+ <Button disabled={snapshotBusy} onclick={snapshot}>
{snapshotBusy ? '创建快照…' : '保存版本快照'}
- </button>
- <button
- class="btn"
+ </Button>
+ <Button
+ variant="outline"
onclick={() => versions[1] && void compareWithDraft(versions[1])}
disabled={!versions[1]}
>
草稿与上一快照对比
- </button>
+ </Button>
</div>
<Status busy={false} error={snapshotErr} empty={snapshotMsg} />
@@ -304,8 +306,8 @@
</div>
<DiffView before={conflictServer.code} after={code} beforeLabel="服务器" afterLabel="本机" />
<div class="row">
- <button class="btn primary" onclick={keepMine}>保留本机修改(覆盖服务器)</button>
- <button class="btn" onclick={loadServer}>采用服务器版本</button>
+ <Button onclick={keepMine}>保留本机修改(覆盖服务器)</Button>
+ <Button variant="outline" onclick={loadServer}>采用服务器版本</Button>
</div>
{/if}
</div>
@@ -316,9 +318,9 @@
<label for="ai-instruction">希望 AI 帮你做什么?<span class="hint">例如:把均线周期改为两个可调参数并解释影响</span></label>
<textarea id="ai-instruction" rows="3" bind:value={instruction} placeholder="描述对策略的修改意图,AI 返回完整新源码与差异预览"></textarea>
<div class="row">
- <button class="btn primary" disabled={aiBusy || !instruction.trim()} onclick={askAI}>
+ <Button disabled={aiBusy || !instruction.trim()} onclick={askAI}>
{aiBusy ? 'AI 处理中…' : '获取建议'}
- </button>
+ </Button>
</div>
{#if aiErr}
<div class="banner error" role="alert">
@@ -341,15 +343,14 @@
<h3>与当前草稿的差异</h3>
<DiffView before={code} after={aiResult.proposed_code} beforeLabel="当前草稿" afterLabel="AI 建议" />
<div class="row">
- <button
- class="btn primary"
+ <Button
disabled={aiAcceptBusy}
onclick={acceptAI}
title={aiResult.base_generation !== baseGeneration ? '草稿代已变化,建议如需采用请重新获取' : ''}
>
{aiAcceptBusy ? '接受中…' : '接受为新版本'}
- </button>
- <button class="btn ghost" disabled={aiAcceptBusy} onclick={discardAI}>放弃建议</button>
+ </Button>
+ <Button variant="ghost" disabled={aiAcceptBusy} onclick={discardAI}>放弃建议</Button>
{#if aiResult.base_generation !== baseGeneration}
<span class="banner warn" role="alert">草稿代 {aiResult.base_generation} 与当前 {baseGeneration} 不一致,接受会失败;请重新发起说明</span>
{/if}
@@ -395,11 +396,11 @@
<td><span class="badge grey">{versionSourceLabel[v.source]}</span></td>
<td>
<div class="row">
- <button class="btn small" onclick={() => void compareWithDraft(v)}>对比当前</button>
+ <Button variant="outline" size="sm" onclick={() => void compareWithDraft(v)}>对比当前</Button>
{#if i + 1 < versions.length}
- <button class="btn small" onclick={() => void compareTwoVersions(i)}>与相邻旧版对比</button>
+ <Button variant="outline" size="sm" onclick={() => void compareTwoVersions(i)}>与相邻旧版对比</Button>
{/if}
- <button class="btn small" onclick={() => void restore(v)}>恢复为新草稿</button>
+ <Button size="sm" onclick={() => void restore(v)}>恢复为新草稿</Button>
</div>
</td>
</tr>
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}