feat: Phase 3 Web — 业务 API(stocks/factors/backtests)+ Next.js 前端

- 后端业务 API:GET /api/stocks(搜索/分页)、GET /api/factors(因子目录)、POST /api/factor-tests 与 /api/backtests(Research Spec 驱动同步执行)、GET /api/backtests/last;Annotated 依赖注入 + CORS(dev)
- Repository 批量查询 get_range_many(研究装配一次查询,避免逐只拉取)
- 前端 frontend/web:Next.js 15(TS) + ECharts —— 总览 / 股票池 / 因子研究(IC·RankIC·分层展示) / 回测(净值·回撤·月度·持仓·未建模标注)
- 前端只消费业务 API 与标准化 BacktestResult,无 Qlib/SQL 概念泄漏
- 真实数据:同步 20 只权重股 2023-2024 日线(9680 根)支撑截面研究
- 验证:API 集成测试 8 项(DTO 校验/装配/引擎/标准结果,内存 repo 全链路)+ 全量 pytest 68 passed;前端 tsc + next build 通过;无头浏览器端到端(factors/backtest 页面渲染后端数据)
- ruff clean
This commit is contained in:
Simon
2026-09-06 17:14:33 +08:00
parent e9f59d3cf8
commit 92627f5b6b
27 changed files with 1863 additions and 7 deletions
+26
View File
@@ -0,0 +1,26 @@
/** 后端 API 客户端(开发期指向本地 FastAPI,地址见 .env.local)。 */
const BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://127.0.0.1:8000/api";
export async function apiGet<T>(path: string): Promise<T> {
const resp = await fetch(`${BASE}${path}`);
if (!resp.ok) throw new Error(`GET ${path} → ${resp.status}: ${await resp.text()}`);
return (await resp.json()) as T;
}
export async function apiPost<T>(path: string, body: unknown): Promise<T> {
const resp = await fetch(`${BASE}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!resp.ok) {
const text = await resp.text();
throw new Error(`POST ${path} → ${resp.status}: ${text.slice(0, 300)}`);
}
return (await resp.json()) as T;
}
export interface ApiError {
status: number;
message: string;
}
+74
View File
@@ -0,0 +1,74 @@
/** 与后端 domain/entities/research.py 对应的类型(DTO 镜像)。 */
export interface Stock {
symbol: string;
name: string;
industry?: string | null;
area?: string | null;
list_date: string;
delist_date?: string | null;
status: string;
}
export interface FactorMeta {
name: string;
description: string;
formula: string;
frequency: string;
lookback: number;
direction: "higher_is_better" | "lower_is_better";
}
export interface ResearchSpec {
type: "factor_test" | "backtest";
universe: { exclude_st?: boolean; min_listing_days?: number };
factors: { name: string; weight: number }[];
selection: { top_n: number };
rebalance: "weekly" | "monthly";
period: [string, string];
}
export interface CurvePoint {
date: string;
value: number;
}
export interface BacktestSummary {
start: string;
end: string;
initial_capital: number;
final_equity: number;
total_return_pct: number;
annual_return_pct: number;
sharpe: number;
max_drawdown_pct: number;
volatility_pct: number;
win_rate_pct: number;
total_trades: number;
avg_turnover_pct: number;
}
export interface BacktestResult {
summary: BacktestSummary;
equity_curve: CurvePoint[];
drawdown: CurvePoint[];
monthly_returns: { year: number; month: number; return_pct: number }[];
yearly_returns: { year: number; return_pct: number }[];
positions: { date: string; symbol: string; weight: number }[];
trades: { entry_date: string; exit_date: string; symbol: string; return_pct: number }[];
turnover_pct: number;
unimplemented: string[];
config_snapshot: Record<string, unknown>;
}
export interface FactorTestReport {
factor_name: string;
ic_mean: number;
icir: number;
rank_ic_mean: number;
positive_ratio_pct: number;
quantile_returns: { quantile: number; return_pct: number }[];
spread_quantile?: number | null;
sample_days: number;
unimplemented: string[];
}