summaryrefslogtreecommitdiff
path: root/frontend/src/pages/ProjectResultsTab.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/pages/ProjectResultsTab.svelte')
-rw-r--r--frontend/src/pages/ProjectResultsTab.svelte108
1 files changed, 38 insertions, 70 deletions
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>