350 lines
17 KiB
Markdown
350 lines
17 KiB
Markdown
# 股市回测平台
|
||
|
||
专业的 **历史回测 + 回放式模拟** 平台(A 股为主,**不做实盘**)。
|
||
输入技术指标参数(MACD/RSI/KDJ…)→ 后端回测 → 前端可视化 K 线、指标叠加、买卖点、净值曲线与绩效。
|
||
|
||
技术选型与架构决策详见 [TECH_STACK.md](./TECH_STACK.md)。
|
||
|
||
---
|
||
|
||
## 快速启动
|
||
|
||
两个终端分别启动后端与前端(命令均从仓库根 `stock/` 执行):
|
||
|
||
```bash
|
||
# 终端 1 —— 后端 API(http://localhost:8000)
|
||
bash backend/restart_backend.sh
|
||
|
||
# 终端 2 —— 前端开发服务器(http://localhost:5173)
|
||
cd frontend
|
||
pnpm dev
|
||
```
|
||
|
||
> 后端统一走 `backend/restart_backend.sh`:自动杀旧进程树 + 剔除会崩 asyncpg 的 `SSLKEYLOGFILE` + 带热重载重启 + 日志写 `backend_run.log`。Windows cmd 下等价命令:`backend\restart_backend.cmd`。详见下文「快速开始(开发模式)」。
|
||
|
||
**首次运行前**:
|
||
1. 装依赖:后端 `cd backend && uv sync`、前端 `cd frontend && pnpm install`。
|
||
2. 后端依赖 **PostgreSQL(16/17)+ Redis 已启动**,并配好 `backend/.env`(复制 `.env.example`)。
|
||
3. 建库表:`cd backend && uv run alembic upgrade head`。
|
||
|
||
浏览器打开 http://localhost:5173 即可使用。详细说明见下文「快速开始(开发模式)」。
|
||
|
||
---
|
||
|
||
## 功能特性
|
||
|
||
**已实现(Phase 0 MVP)**
|
||
- 自定义指标参数回测(当前内置 MACD 金叉死叉策略)
|
||
- K 线主图 + 成交量 + MACD 副图(DIF/DEA/柱)
|
||
- **买卖点标注**(信号回放到 K 线,↑买 ↓卖)
|
||
- **周期切换**:日线 / 周线 / 月线 / 年线(日线为基底,应用层 pandas 聚合)
|
||
- **悬停弹框**:鼠标移到任意 K 线,显示当日 开/高/低/收、涨跌幅、振幅、成交量(手)、MACD 值(同花顺式)
|
||
- 净值曲线 + 绩效指标(总收益、最大回撤、夏普、年化波动、胜率、交易次数)
|
||
- A 股真实成本建模:印花税 0.05%(单边卖出)、过户费 0.001%(沪深双边)、佣金万1(最低5元)、T+1、100 股整手
|
||
- 回测运行注册表(每次回测落库,可复现/审计的基础)
|
||
|
||
**规划中(见 TECH_STACK.md 路线图)**
|
||
- 防过拟合体检(walk-forward / 样本外 / FDR 多重比较修正)
|
||
- 基准归因(对比沪深300/中证500:超额、信息比率、beta/alpha)
|
||
- 复权因子管道、回放式模拟盘
|
||
- 切 TimescaleDB(hypertable + Continuous Aggregates)
|
||
|
||
---
|
||
|
||
## 技术栈
|
||
|
||
| 层 | 选型 |
|
||
|---|---|
|
||
| 后端 | Python 3.12+ · FastAPI · Pydantic v2 · SQLAlchemy 2.0 (async) · uv(包管理) |
|
||
| 回测引擎 | 自研单一引擎(fast/strict 两档),指标纯 numpy/pandas(生产可换 TA-Lib) |
|
||
| 数据库 | PostgreSQL 16/17(后端强制要求)+ Redis 缓存;TimescaleDB 规划中 |
|
||
| 前端 | Vue 3.5 · Vite · TypeScript · Pinia · PrimeVue 5 · pnpm |
|
||
| 图表 | lightweight-charts 5(K 线)· ECharts 6(净值) |
|
||
|
||
---
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
stock/
|
||
├── TECH_STACK.md # 技术选型与架构决策(必读)
|
||
├── README.md # 本文件
|
||
├── backend/ # FastAPI + 回测引擎
|
||
│ ├── pyproject.toml # uv 依赖声明(pytest 在 dev 组)
|
||
│ ├── .env.example # 配置示例(数据库 / 费率)
|
||
│ ├── smoke_test.py # 后端全链路自检脚本(线上库鉴权+核心 API)
|
||
│ ├── conftest.py # pytest 根(使 tests/ 可直接 import app.*)
|
||
│ ├── tests/ # pytest 单元测试(交割单解析/复权换算/缓存/限速/事件回测纯函数)
|
||
│ ├── scripts/ # 一次性运维脚本(TDX 导入 / 复权因子回补…)
|
||
│ └── app/
|
||
│ ├── main.py # FastAPI 入口(lifespan 拉起夜间调度)
|
||
│ ├── config.py # 配置(pydantic-settings)
|
||
│ ├── db.py # async SQLAlchemy 引擎/会话
|
||
│ ├── domain.py # 领域契约(Bar/Signal/Fill/Position…)
|
||
│ ├── models.py # ORM(Candle / StockBasic / 用户数据表…)
|
||
│ ├── schemas.py # Pydantic DTO(= OpenAPI 契约)
|
||
│ ├── auth.py # Argon2 密码 + 会话(SHA-256 摘要)
|
||
│ ├── auth_api.py # 登录/登出(含按 IP 限速)
|
||
│ ├── scheduler.py # 夜间定时任务(收盘后自动同步 + 会话清理)
|
||
│ ├── cache.py # Redis 读缓存(本地层 + 版本号失效 + 熔断冷却恢复)
|
||
│ ├── indicators.py # 指标:MACD/RSI/KDJ/布林/均线(单一事实源)
|
||
│ ├── api/ # 路由包:stocks / etfs / market / backtest / screener / user + _deps 共享件
|
||
│ ├── data/ # 数据管道(tushare 适配 + 同步 + 懒加载缓存)
|
||
│ ├── screener/ # 智能选股(LLM 解析 + 全市场引擎 + 夜间同步)
|
||
│ └── backtest/ # engine / events(全市场事件回测) / strategies
|
||
└── frontend/ # Vue SPA
|
||
├── src/
|
||
│ ├── main.ts # Vue + Pinia
|
||
│ ├── api/ # 类型化客户端 + DTO 镜像
|
||
│ ├── stores/ # Pinia(鉴权/设置/同步状态)
|
||
│ ├── composables/ # 组合式工具(防抖 ref / 路由 query 同步)
|
||
│ ├── components/ # StockDetailOverlay / DetailKLine / LimitBoard / MarketOverview…
|
||
│ └── views/ # Home / Stocks / ETF / Concepts / Indexes / Screener / Backtest
|
||
└── vite.config.ts # /api 代理到 :8000
|
||
```
|
||
|
||
---
|
||
|
||
## 环境要求
|
||
|
||
| 工具 | 版本 | 说明 |
|
||
|---|---|---|
|
||
| Node.js | ≥ 20(实测 24) | 前端 |
|
||
| pnpm | ≥ 9(实测 11) | 前端包管理 |
|
||
| Python | ≥ 3.12(实测 3.14) | 后端 |
|
||
| uv | 任意(实测 0.12) | 后端包管理,[安装](https://docs.astral.sh/uv/) |
|
||
| PostgreSQL | 16/17 | 数据库(必装,后端已不再支持 SQLite) |
|
||
|
||
**安装 uv**(若未装):
|
||
```bash
|
||
pip install uv
|
||
```
|
||
|
||
---
|
||
|
||
## 快速开始(开发模式)
|
||
|
||
需要两个终端,分别跑后端与前端。
|
||
|
||
### 1) 后端
|
||
|
||
实际启动命令(一键脚本,自动杀旧进程树 + 剔除 SSLKEYLOGFILE + 热重载 + 日志写仓库根 `backend_run.log`):
|
||
|
||
```bash
|
||
uv sync # 创建 .venv 并安装依赖(仅首次)
|
||
bash backend/restart_backend.sh # 从仓库根执行(Git Bash);Windows cmd 下等价:backend\restart_backend.cmd
|
||
```
|
||
|
||
看日志:`tail -f backend_run.log`;确认起没起:`curl http://localhost:8000/api/health`。
|
||
|
||
等价的 `uv run` 写法(不杀旧进程、日志直接打到当前终端):
|
||
|
||
```bash
|
||
cd backend
|
||
env -u SSLKEYLOGFILE uv run uvicorn app.main:app --reload --port 8000
|
||
```
|
||
|
||
- **`env -u SSLKEYLOGFILE`(本机 Windows 必需)**:用户环境变量 `SSLKEYLOGFILE` 的值开头混有 U+202A 不可见控制符,asyncpg 建连时执行 `ssl.keylog_filename` 直接抛 `OSError: [Errno 22]`,uvicorn 启动即崩。Git Bash 下用 `env -u` 剔除即可;根治可 `setx SSLKEYLOGFILE "C:\Users\cirry\Desktop\fhzg.log"`(重开终端后不再需要前缀)。验证:`python -c "import os; print(repr(os.environ.get('SSLKEYLOGFILE')))"`。
|
||
- **Windows `--reload` 僵死**:watcher 偶尔改文件不重载且日志无 Reloading 行,此时只能杀进程树重启——一键脚本 `bash backend/restart_backend.sh`(Git Bash)或 `backend\restart_backend.cmd`(cmd):自动杀旧进程树(`--reload` 起 launcher→reloader→worker 三层进程)+ 带 reload 重启 + 日志写 `backend_run.log`。手动:`netstat -ano | grep :8000` 找 PID,`taskkill //PID <pid> //T //F`。
|
||
- 数据库结构由 Alembic 管理:首次部署/更新代码后先执行 `uv run alembic upgrade head`(见「初始化登录系统」)。
|
||
- 交互式 API 文档:http://localhost:8000/docs
|
||
- **夜间自动同步**:后端启动即拉起调度(`app/scheduler.py`),每日 18:05 本地时间自动跑全市场 A 股 + ETF 同步并清理过期会话;进程启动时若已过点且当日数据未落库会补跑。关闭:`.env` 设 `NIGHTLY_SYNC_ENABLED=false`,时间改 `NIGHTLY_SYNC_HOUR`。
|
||
|
||
**测试**:
|
||
```bash
|
||
uv run --directory backend pytest -q # 单元测试(交割单解析/复权换算/缓存/限速等纯函数,不碰库)
|
||
uv run --with httpx --directory backend python smoke_test.py # 全链路自检(线上库鉴权+核心 API)
|
||
```
|
||
|
||
### 2) 前端
|
||
|
||
```bash
|
||
cd frontend
|
||
pnpm install # 首次
|
||
pnpm dev # http://localhost:5173
|
||
```
|
||
|
||
- **必须用 pnpm,不要用 npm/yarn**:项目锁定 `pnpm-lock.yaml`,`node_modules` 是 pnpm 的硬链接结构,`npm install` 会写坏依赖,导致装包/构建崩溃。以后装包一律 `pnpm add <pkg>`(不要 `npm install <pkg>`)。
|
||
- 前端 `/api` 请求由 Vite 代理到后端 `:8000`(见 `vite.config.ts`),无需处理跨域。
|
||
|
||
打开 http://localhost:5173 → 选周期、改参数 → 点「开始回测」。
|
||
鼠标悬停 K 线可看当日详情弹框;切日线/周线/月线/年线;勾「fast 模式」可对比关闭费用/T+1 的差异。
|
||
|
||
---
|
||
|
||
## 配置说明
|
||
|
||
后端配置通过 `backend/.env`(复制 `.env.example`)或环境变量:
|
||
|
||
```bash
|
||
# 数据库(开发、测试、生产统一使用 PostgreSQL)
|
||
DATABASE_URL=postgresql+asyncpg://user:password@host:5432/stock
|
||
|
||
# 鉴权:本地 HTTP 为 false;线上 HTTPS 必须为 true
|
||
AUTH_COOKIE_SECURE=false
|
||
AUTH_SESSION_HOURS=12
|
||
CORS_ORIGINS=http://localhost:5173
|
||
# 生产建议关闭接口文档
|
||
EXPOSE_API_DOCS=true
|
||
|
||
# 真实数据源(Tushare Pro;免费版即可)
|
||
TUSHARE_TOKEN=你的token
|
||
DATA_ADJUST=qfq # 复权:qfq 前复权 / hfq 后复权 / 留空不复权
|
||
DATA_DEFAULT_START=20200101
|
||
|
||
# A 股交易成本(基准日 2026-08,可覆盖;默认值见 app/config.py)
|
||
# STAMP_DUTY_RATE=0.0005 # 印花税 0.05%,单边卖出
|
||
# TRANSFER_FEE_RATE=0.00001 # 过户费 0.001%,沪深双边
|
||
# COMMISSION_RATE=0.0001 # 佣金 万1
|
||
# COMMISSION_MIN=5.0 # 最低 5 元
|
||
```
|
||
|
||
### 初始化登录系统
|
||
|
||
数据库结构由 Alembic 管理。首次部署或更新代码后执行:
|
||
|
||
```bash
|
||
cd backend
|
||
uv run alembic upgrade head
|
||
```
|
||
|
||
系统不提供注册接口。使用服务器交互式命令创建唯一用户;再次执行会重置密码并吊销该用户的所有旧会话:
|
||
|
||
```bash
|
||
uv run python -m app.cli.create_user --username admin
|
||
```
|
||
|
||
密码使用 Argon2id 保存。浏览器只接收 `HttpOnly` 会话 Cookie,数据库只保存随机会话 Token 的 SHA-256 摘要。默认会话有效期 12 小时,连续 5 次登录失败后锁定 15 分钟。
|
||
|
||
生产环境必须启用 HTTPS,并设置:
|
||
|
||
```bash
|
||
AUTH_COOKIE_SECURE=true
|
||
CORS_ORIGINS=https://你的域名
|
||
EXPOSE_API_DOCS=false
|
||
```
|
||
|
||
### 切到 PostgreSQL + TimescaleDB
|
||
|
||
1. 目标库执行 `CREATE EXTENSION IF NOT EXISTS timescaledb;`
|
||
2. `DATABASE_URL=postgresql+asyncpg://...`
|
||
3. (TODO)把 `candles` 表升级为 hypertable 并配置 Continuous Aggregates 多周期预聚合(schema 已兼容,无需改表结构):
|
||
```sql
|
||
SELECT create_hypertable('candles', 'ts');
|
||
```
|
||
|
||
> 即便能装在现有 Postgres 上,建议为交易系统单独起一个 PG16/17 实例,便于调优/备份/隔离回测扫描负载。
|
||
|
||
---
|
||
|
||
## API
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|---|---|---|
|
||
| GET | `/api/health` | 健康检查 |
|
||
| POST | `/api/data/sync` | 拉取并缓存某标的日线(Tushare 主 → AKShare 兜底)。body: `{symbol, source?, force?}` |
|
||
| POST | `/api/backtest` | 跑回测(真实标的首次自动拉取并缓存;前端已改用 `/api/backtest/event` 事件回测,此为旧策略回测 API) |
|
||
|
||
个股 K 线统一走 `GET /api/screener/preview/{ts_code}`(含复权/指标预热/翻页,两级缓存)。
|
||
|
||
**回测请求示例**:
|
||
```json
|
||
POST /api/backtest
|
||
{
|
||
"symbol": "000001",
|
||
"timeframe": "1d",
|
||
"strategy": "macd_cross",
|
||
"params": { "fast": 12, "slow": 26, "signal": 9 },
|
||
"initial_cash": 100000,
|
||
"fast_mode": false
|
||
}
|
||
```
|
||
返回:`candles`(K线)、`indicators`(MACD 三线)、`signals`(买卖点)、`equity`(净值序列)、`metrics`(绩效)、`final_cash`/`final_position`。
|
||
|
||
> **关于「标的」**:标的为真实 A 股代码(如 `000001`、`600519`),首次回测时**自动经 Tushare 拉取并本地缓存**(需配置 `TUSHARE_TOKEN` + 联网),二次回测秒出。默认初始资金 100 万,足够交易高价股(如茅台)。
|
||
|
||
---
|
||
|
||
## 生产构建与部署
|
||
|
||
### 前端构建
|
||
```bash
|
||
cd frontend
|
||
pnpm build # 产物在 frontend/dist
|
||
pnpm preview # 本地预览构建产物
|
||
```
|
||
部署时把 `frontend/dist` 用任意静态服务器(nginx / caddy / 对象存储)托管,并把 `/api` 反向代理到后端。
|
||
|
||
### 后端生产运行
|
||
```bash
|
||
cd backend
|
||
uv run alembic upgrade head
|
||
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
|
||
# 或 gunicorn(Linux):uv run gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker
|
||
```
|
||
后端强制要求 `DATABASE_URL` 使用 PostgreSQL(`postgresql+asyncpg://`),已不再支持 SQLite。
|
||
|
||
### 参考:docker-compose(具备 Docker 后使用)
|
||
```yaml
|
||
services:
|
||
db:
|
||
image: timescale/timescaledb:latest-pg17
|
||
environment: { POSTGRES_PASSWORD: stock, POSTGRES_DB: stock }
|
||
ports: ["5432:5432"]
|
||
volumes: ["pgdata:/var/lib/postgresql/data"]
|
||
api:
|
||
build: ./backend
|
||
environment: { DATABASE_URL: postgresql+asyncpg://postgres:stock@db:5432/stock }
|
||
ports: ["8000:8000"]
|
||
depends_on: [db]
|
||
web:
|
||
image: nginx:alpine
|
||
volumes: ["./frontend/dist:/usr/share/nginx/html:ro", "./nginx.conf:/etc/nginx/conf.d/default.conf:ro"]
|
||
ports: ["80:80"]
|
||
depends_on: [api]
|
||
volumes: { pgdata: {} }
|
||
```
|
||
|
||
---
|
||
|
||
## 扩展指南
|
||
|
||
### 加一个指标
|
||
在 `backend/app/indicators.py` 增加纯函数(输入 close/high/low Series,输出 DataFrame),保持函数签名风格即可被策略复用。例:已内置 `macd / rsi / kdj / bollinger / ma`。
|
||
|
||
### 加一个策略
|
||
内置策略:`macd_cross`(MACD 金叉死叉)、`ma_cross`(双均线交叉)、`single_ma`(单均线,价格穿越均线)。前端下拉自动出现新策略、参数自适应。
|
||
|
||
在 `backend/app/backtest/strategies/` 新增继承 `Strategy`(`base.py`)的类,实现 `compute()`(预算指标)与 `on_bar(i, row, broker)`(决策并向 `broker` 下单),再在 `__init__.py` 的 `STRATEGIES` 注册 `{name: Class}`。策略构造参数由请求 `params(dict)` 以 `**kwargs` 传入。策略只产生买卖意图,撮合/费用交给 `PaperBroker`。
|
||
|
||
### 数据源(已集成)
|
||
真实 A 股行情已集成(`backend/app/data/`):
|
||
- `tushare_provider.py` —— 主数据源,需 `TUSHARE_TOKEN`,默认前复权(`pro_bar`,积分不足时自动退化为不复权日线)
|
||
- `akshare_provider.py` —— 兜底,免费免 token;默认**未安装**,`uv add akshare` 后自动启用
|
||
- `fetcher.py` —— 编排:Tushare 失败自动切 AKShare;拉取后写入 `candles` 表本地缓存(解耦上游停运/限频)
|
||
|
||
换复权方式改 `.env` 的 `DATA_ADJUST`;换默认拉取起点改 `DATA_DEFAULT_START`。
|
||
|
||
---
|
||
|
||
## 常见问题
|
||
|
||
- **`pnpm install` 报 `Ignored build scripts: esbuild`**:新版 pnpm 默认不运行依赖的安装脚本。已通过 `frontend/pnpm-workspace.yaml` 的 `allowBuilds: { esbuild: true, vue-demi: true }` 放行;若仍提示,运行 `pnpm approve-builds` 选择允许。
|
||
- **前端图表不显示**:确认后端已起(`/api/health` 返回 ok);浏览器控制台看是否 404/跨域(dev 应走 Vite 代理无跨域)。
|
||
- **lightweight-charts 报 `addCandlestickSeries is not a function`**:那是 v4 API。本项目用 v5 的 `chart.addSeries(CandlestickSeries, ...)`,请勿混用旧教程。
|
||
- **真实标的首次回测慢/拉取失败**:首次需联网请求 Tushare(数秒)。Tushare 对 `adj_factor` 限频(每小时 1 次),触发时自动退化为不复权日线,不影响回测。数据已本地缓存,二次回测秒出。若 Tushare 积分不足,`uv add akshare` 后将自动用 AKShare 兜底。
|
||
- **高价股(如茅台)回测 0 信号**:一手太贵买不起。默认初始资金已设为 100 万;如仍不够,在表单调高「初始资金」。
|
||
- **Python 3.14 兼容**:MVP 依赖(fastapi/sqlalchemy/pandas/numpy)均已支持;指标用纯 Python 实现,不依赖 TA-Lib 的 C 库,Windows 免编译。
|
||
- **后端启动即崩,栈在 asyncpg 深处(`OSError: [Errno 22] Invalid argument`)**:是本机环境变量 `SSLKEYLOGFILE` 值里混入的 U+202A 不可见控制符所致,不是数据库问题。Git Bash 下启动命令前加 `env -u SSLKEYLOGFILE`;或 `setx SSLKEYLOGFILE "C:\Users\cirry\Desktop\fhzg.log"` 重写该变量后重开终端。
|
||
- **uvicorn `--reload` 改代码不生效**:Windows 下 watcher 会僵死(日志无 Reloading 行,touch 无效)。`netstat -ano | grep :8000` 找到 PID 后 `taskkill //PID <pid> //T //F` 杀进程树重启。
|
||
|
||
---
|
||
|
||
## 路线图
|
||
|
||
详见 [TECH_STACK.md](./TECH_STACK.md)「分阶段落地路线」。下一步优先级:
|
||
1. 接入 Tushare 真实数据
|
||
2. 更多指标/策略(RSI/KDJ/布林策略)
|
||
3. 防过拟合体检 + 基准归因
|
||
4. 切 TimescaleDB、回测结果缓存与对比
|