From 80d7f58589de594fd06a8ccfea87085ff6d49c27 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Tue, 7 Jul 2026 06:13:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20S1=20=E5=9B=9E=E6=B5=8B?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E9=A1=B5=EF=BC=88=E6=96=B0=E5=BB=BA/?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6/=E7=BB=93=E6=9E=9C=20+=20ECharts=20=E5=9B=BE?= =?UTF-8?q?=E8=A1=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api/strategy.ts、api/backtest.ts(含类型) - 回测-新建(策略下拉+动态参数表单+日期+提交) - useTask 组合式(轮询+WS 实时阶段)+ 进度页 - 结果页:统计全表 + 资金曲线 + 每日盈亏(红涨绿跌) + 成交表 + K线买卖点 - build 通过;result 接口扩 symbol/start/end 供 K线调用 --- frontend/src/api/backtest.ts | 89 +++++++++++++++ frontend/src/api/strategy.ts | 21 ++++ frontend/src/components/TradesTable.vue | 16 +++ .../src/components/charts/DailyPnlChart.vue | 35 ++++++ .../src/components/charts/EquityChart.vue | 34 ++++++ frontend/src/components/charts/KlineChart.vue | 51 +++++++++ frontend/src/composables/useTask.ts | 55 +++++++++ frontend/src/views/backtest/New.vue | 107 +++++++++++++++++- frontend/src/views/backtest/Progress.vue | 41 ++++++- frontend/src/views/backtest/Result.vue | 78 ++++++++++++- sanguo_api/routes.py | 11 +- 11 files changed, 531 insertions(+), 7 deletions(-) create mode 100644 frontend/src/api/backtest.ts create mode 100644 frontend/src/api/strategy.ts create mode 100644 frontend/src/components/TradesTable.vue create mode 100644 frontend/src/components/charts/DailyPnlChart.vue create mode 100644 frontend/src/components/charts/EquityChart.vue create mode 100644 frontend/src/components/charts/KlineChart.vue create mode 100644 frontend/src/composables/useTask.ts diff --git a/frontend/src/api/backtest.ts b/frontend/src/api/backtest.ts new file mode 100644 index 0000000..5c7ad6f --- /dev/null +++ b/frontend/src/api/backtest.ts @@ -0,0 +1,89 @@ +import { apiClient } from './client' + +export interface CtaSubmit { + symbol: string + strategy: string + params: Record + start: string + end: string +} + +export interface TaskStatus { + task_id: string + status: string + stage: string +} + +export interface EquityPoint { + date: string + balance: number +} + +export interface PnlPoint { + date: string + pnl: number +} + +export interface Trade { + datetime: string + direction: string + offset: string + price: number + volume: number + vt_symbol?: string +} + +export interface KlineBar { + datetime: string + open: number + high: number + low: number + close: number + volume: number +} + +export async function submitCta(req: CtaSubmit): Promise { + const { data } = await apiClient.post<{ task_id: string }>('/backtest/cta', req) + return data.task_id +} + +export async function getStatus(taskId: string): Promise { + const { data } = await apiClient.get(`/task/${taskId}`) + return data +} + +export interface BacktestResultInfo { + task_id: string + statistics: Record + symbol: string + start: string + end: string + strategy: string + params: Record + status: string +} + +export async function getResult(taskId: string): Promise { + const { data } = await apiClient.get(`/task/${taskId}/result`) + return data +} + +export async function getEquityCurve(taskId: string): Promise { + const { data } = await apiClient.get<{ equity_curve: EquityPoint[] }>(`/task/${taskId}/equity-curve`) + return data.equity_curve +} + +export async function getDailyPnl(taskId: string): Promise { + const { data } = await apiClient.get<{ daily_pnl: PnlPoint[] }>(`/task/${taskId}/daily-pnl`) + return data.daily_pnl +} + +export async function getTrades(taskId: string): Promise { + const { data } = await apiClient.get<{ trades: Trade[] }>(`/task/${taskId}/trades`) + return data.trades +} + +export async function getKline(symbol: string, start: string, end: string): Promise { + const { data } = await apiClient.get<{ kline: KlineBar[] }>('/kline', { params: { symbol, start, end } }) + return data.kline +} diff --git a/frontend/src/api/strategy.ts b/frontend/src/api/strategy.ts new file mode 100644 index 0000000..a8c3774 --- /dev/null +++ b/frontend/src/api/strategy.ts @@ -0,0 +1,21 @@ +import { apiClient } from './client' + +export interface StrategyItem { + name: string + class_name: string +} + +export interface StrategyParams { + parameters: string[] + defaults: Record +} + +export async function getStrategies(): Promise { + const { data } = await apiClient.get<{ strategies: StrategyItem[] }>('/strategy/list') + return data.strategies +} + +export async function getParams(name: string): Promise { + const { data } = await apiClient.get(`/strategy/${name}/params`) + return data +} diff --git a/frontend/src/components/TradesTable.vue b/frontend/src/components/TradesTable.vue new file mode 100644 index 0000000..7d4c0c7 --- /dev/null +++ b/frontend/src/components/TradesTable.vue @@ -0,0 +1,16 @@ + + + diff --git a/frontend/src/components/charts/DailyPnlChart.vue b/frontend/src/components/charts/DailyPnlChart.vue new file mode 100644 index 0000000..9a06670 --- /dev/null +++ b/frontend/src/components/charts/DailyPnlChart.vue @@ -0,0 +1,35 @@ + + + + diff --git a/frontend/src/components/charts/EquityChart.vue b/frontend/src/components/charts/EquityChart.vue new file mode 100644 index 0000000..45a7970 --- /dev/null +++ b/frontend/src/components/charts/EquityChart.vue @@ -0,0 +1,34 @@ + + + + diff --git a/frontend/src/components/charts/KlineChart.vue b/frontend/src/components/charts/KlineChart.vue new file mode 100644 index 0000000..89988d1 --- /dev/null +++ b/frontend/src/components/charts/KlineChart.vue @@ -0,0 +1,51 @@ + + + + diff --git a/frontend/src/composables/useTask.ts b/frontend/src/composables/useTask.ts new file mode 100644 index 0000000..b1cd6e5 --- /dev/null +++ b/frontend/src/composables/useTask.ts @@ -0,0 +1,55 @@ +import { ref, onUnmounted } from 'vue' +import { getStatus } from '@/api/backtest' +import { useAuthStore } from '@/stores/auth' + +export type TaskState = 'pending' | 'running' | 'done' | 'failed' | 'unknown' + +/** + * Track a task's status + stage via polling (2s) and WebSocket (real-time). + * Auto-stops on unmount. + */ +export function useTask(taskId: string) { + const status = ref('unknown') + const stage = ref('') + let timer: ReturnType | null = null + let ws: WebSocket | null = null + + async function poll(): Promise { + try { + const s = await getStatus(taskId) + status.value = s.status as TaskState + stage.value = s.stage + } catch { + /* transient — keep last known state */ + } + } + + function start(): void { + poll() + timer = setInterval(poll, 2000) + const proto = window.location.protocol === 'https:' ? 'wss' : 'ws' + const auth = useAuthStore() + const url = `${proto}://${window.location.host}/api/v1/ws/task/${taskId}?token=${auth.token}` + try { + ws = new WebSocket(url) + ws.onmessage = (ev) => { + try { + const msg = JSON.parse(ev.data) + if (msg.stage) stage.value = msg.stage + if (msg.status) status.value = msg.status + } catch { + /* ignore non-JSON keepalive frames */ + } + } + } catch { + /* WS optional — polling covers it */ + } + } + + onUnmounted(() => { + if (timer) clearInterval(timer) + if (ws) ws.close() + }) + + return { status, stage, start } +} diff --git a/frontend/src/views/backtest/New.vue b/frontend/src/views/backtest/New.vue index 053b669..db0912f 100644 --- a/frontend/src/views/backtest/New.vue +++ b/frontend/src/views/backtest/New.vue @@ -1,4 +1,107 @@ - + + diff --git a/frontend/src/views/backtest/Progress.vue b/frontend/src/views/backtest/Progress.vue index 9cea1cf..90605f9 100644 --- a/frontend/src/views/backtest/Progress.vue +++ b/frontend/src/views/backtest/Progress.vue @@ -1,4 +1,41 @@ - + + diff --git a/frontend/src/views/backtest/Result.vue b/frontend/src/views/backtest/Result.vue index eccdc4b..3beb8ed 100644 --- a/frontend/src/views/backtest/Result.vue +++ b/frontend/src/views/backtest/Result.vue @@ -1,4 +1,78 @@ - + + diff --git a/sanguo_api/routes.py b/sanguo_api/routes.py index f90ed38..907c766 100644 --- a/sanguo_api/routes.py +++ b/sanguo_api/routes.py @@ -119,7 +119,16 @@ def get_result(task_id: str): r = get_orchestrator().get_result(task_id) if r is None: raise HTTPException(status_code=404, detail="result not ready") - return {"task_id": task_id, "statistics": r.statistics} + return { + "task_id": task_id, + "statistics": r.statistics, + "symbol": r.symbol, + "start": r.start, + "end": r.end, + "strategy": r.strategy, + "params": r.params, + "status": r.status, + } @router.websocket("/ws/task/{task_id}")