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}")