引言:为什么学习量化交易?
量化交易(Quantitative Trading)是一种数据驱动的投资方法,利用数学模型、统计分析和算法自动化决策,而不是主观判断。它适合A股市场的高频波动,帮助捕捉Alpha(超额收益)。作为入门实践,我们构建一个完整工具:后端 用Python + Qlib实现策略回测;API 用FastAPI桥接;前端用React + Tailwind CSS + Recharts可视化仪表盘。
这个项目基于Microsoft开源的Qlib框架(AI导向量化平台),聚焦沪深300/个股策略(如Alpha158因子 + LightGBM模型 + TopK)。开发过程从环境搭建到迭代,耗时2小时(含调试),最终实现一键回测+曲线显示。案例:分析跃岭股份 (002725.SZ),年化超额7.57%。
学习目标:
- 理解量化流程:数据 → 模型 → 回测 → 风险评估。
- 实践工具链:Python生态 + Web全栈。
- 风险提醒:历史回测不代表未来,实盘需风控。
1. 环境准备
1.1 Python环境(Conda + Qlib源代码)
使用Conda避免pip依赖冲突。Python 3.10兼容Qlib。
bash
# 创建环境
conda create -n qlib_a股 python=3.10
conda activate qlib_a股
# 克隆Qlib源代码(开发模式,便于修改)
git clone https://github.com/microsoft/qlib.git
cd qlib
# 批量安装依赖(优先conda,避免pip版本错)
conda install -c conda-forge numpy pandas pyyaml scipy scikit-learn tqdm tensorboard requests numba fire ruamel.yaml cython setuptools-scm wheel redis black flake8 mypy pre-commit
pip install --upgrade cython
pip install -e '.[dev]' # 单引号防zsh glob错误
# A股数据(沪深Bin格式,2005-2024)
wget https://github.com/chenditc/investment_data/releases/latest/download/qlib_bin.tar.gz
mkdir -p ~/.qlib/qlib_data/cn_data
tar -zxvf qlib_bin.tar.gz -C ~/.qlib/qlib_data/cn_data --strip-components=1
rm qlib_bin.tar.gz
# 初始化Qlib
python -c "import qlib; qlib.init(provider_uri='~/.qlib/qlib_data/cn_data', region='cn')"
常见坑 & 修复:
- pip "versions: none":用conda install + 清华镜像(pip.conf:
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
)。 - zsh glob:pip install -e '.[dev]'(单引号)。
- setuptools_scm错:
conda install -c conda-forge setuptools-scm
。
1.2 前端环境(Bun + Vite + React TS)
Bun 比 npm 快 10x,Vite 热重载秒级。
bash
# 创建Vite React TS项目
bun create vite@latest frontend --template react-ts
cd frontend
# 安装依赖
bun add recharts axios # 图表 + API
bun add -d tailwindcss@^3.4.0 postcss autoprefixer # Tailwind v3(避v4坑)
# 初始化Tailwind
bunx tailwindcss init -p
配置(postcss.config.cjs用CommonJS避ESM错):
-
tailwind.config.js :
js/** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {} }, plugins: [], darkMode: 'class', }
-
postcss.config.cjs (重命名.js为.cjs):
jsmodule.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }
-
src/index.css :
css@tailwind base; @tailwind components; @tailwind utilities;
坑 & 修复:PostCSS "module is not defined" → 重命名.cjs;content []空 → 加src路径。
2. 后端开发:Qlib量化引擎
2.1 核心引擎:quant_engine.py
Qlib 全流程:数据(Alpha158因子)→ 模型(LightGBM)→ 策略(TopK)→ 回测(沪深300基准)。用 workflow 记录实验,风险用risk_analysis。
完整代码(backend/quant_engine.py):
python
import qlib
from qlib.constant import REG_CN
from qlib.data import D
from qlib.contrib.model.gbdt import LGBModel
from qlib.contrib.strategy import TopkDropoutStrategy
from qlib.backtest import backtest, executor
from qlib.utils import init_instance_by_config
from qlib.workflow import R
from qlib.workflow.record_temp import SignalRecord, PortAnaRecord
from qlib.contrib.evaluate import risk_analysis
import pandas as pd
import warnings
warnings.filterwarnings('ignore', category=RuntimeWarning) # 忽略数据空slice警告
# 初始化Qlib
qlib.init(provider_uri='~/.qlib/qlib_data/cn_data', region=REG_CN)
# 配置(沪深300全市场)
config = {
'dataset': {'class': 'qlib.data.dataset.DatasetH', 'module_path': 'qlib.data.dataset', 'kwargs': {'handler': {'class': 'Alpha158', 'module_path': 'qlib.contrib.data.handler', 'kwargs': {'start_time': '2010-01-01', 'end_time': '2025-10-16', 'fit_start_time': '2010-01-01', 'fit_end_time': '2017-12-31', 'instruments': 'csi300'}}, 'segments': {'train': ('2010-01-01', '2017-12-31'), 'valid': ('2018-01-01', '2019-12-31'), 'test': ('2020-01-01', '2025-10-16')}}},
'model': {'class': 'LGBModel', 'module_path': 'qlib.contrib.model.gbdt', 'kwargs': {'loss': 'mse', 'colsample_bytree': 0.8879, 'learning_rate': 0.0421, 'subsample': 0.8789, 'lambda_l1': 205.6999, 'lambda_l2': 34.2895, 'max_depth': 8, 'num_leaves': 210, 'num_threads': 20}},
'strategy': {'class': 'TopkDropoutStrategy', 'module_path': 'qlib.contrib.strategy', 'kwargs': {'topk': 50, 'n_drop': 5, 'signal': '<PRED>', 'risk_degree': 0.95}},
'backtest': {'start_time': '2020-01-01', 'end_time': '2025-10-16', 'account': 100000000, 'benchmark': 'SH000300', 'exchange_kwargs': {'limit_threshold': 0.095, 'deal_price': 'close', 'open_cost': 0.0005, 'close_cost': 0.0015, 'min_cost': 5}},
'executor': {'class': 'SimulatorExecutor', 'module_path': 'qlib.backtest.executor', 'kwargs': {'time_per_step': 'day', 'generate_portfolio_metrics': True}},
}
def run_quant_workflow():
exp_name = "A股_Alpha158_LGB"
with R.start(experiment_name=exp_name):
model = init_instance_by_config(config['model'])
dataset = init_instance_by_config(config['dataset'])
model.fit(dataset)
R.save_objects(trained_model=model)
rec = R.get_recorder()
sr = SignalRecord(model, dataset, rec)
sr.generate()
port_analysis_config = {
"backtest": config["backtest"],
"strategy": config["strategy"],
"executor": config["executor"]
}
par = PortAnaRecord(rec, port_analysis_config)
par.generate()
# 加载报告
report_df = rec.load_object('portfolio_analysis/report_normal_1day.pkl')
print("报告df列名:", report_df.columns.tolist()) # 调试
# 通用列匹配(避版本差)
strategy_col = next((col for col in report_df.columns if 'return' in col.lower() and 'bench' not in col.lower()), 'return')
bench_col = next((col for col in report_df.columns if 'bench' in col.lower()), 'bench')
cost_col = next((col for col in report_df.columns if 'cost' in col.lower()), None)
strategy_return = report_df[strategy_col]
bench_return = report_df[bench_col]
cost = report_df[cost_col] if cost_col else pd.Series(0, index=report_df.index)
daily_excess = strategy_return - bench_return - cost
num_days = len(daily_excess)
annualized_return = (1 + daily_excess).prod() ** (252 / num_days) - 1
risk_dict = risk_analysis(daily_excess)
risk_metrics = {
'sharpe': risk_dict.get('Sharpe') or (daily_excess.mean() / daily_excess.std() * (252 ** 0.5) if daily_excess.std() > 0 else 0),
'volatility': risk_dict.get('Volatility') or (daily_excess.std() * (252 ** 0.5)),
'max_drawdown': risk_dict.get('MaxDD') or ((1 + daily_excess).cumprod().expanding().max() - (1 + daily_excess).cumprod()).max() * -1,
'win_rate': risk_dict.get('WinRate') or (daily_excess > 0).mean()
}
return {
'excess_return': daily_excess.to_dict(),
'risk_metrics': risk_metrics,
'annualized_return': float(annualized_return)
}
if __name__ == "__main__":
result = run_quant_workflow()
print(result)
运行 :python quant_engine.py
(10min首次,输出年化7.57%、Sharpe~1.0)。
坑 & 修复:
- 导入路径:
from qlib.contrib.strategy import TopkDropoutStrategy
(去.strategy)。 - 基准:'SH000300'(非'CSI300')。
- Recorder加载:指定exp_name。
- 报告列:通用匹配'return'/'bench'/'cost'。
- 源代码patch:qlib/backtest/report.py第17行
from qlib.tests.config import CSI300_BENCH
(绝对导入)。
2.2 API桥接:api_server.py
FastAPI暴露/run_backtest(异步任务 + 进度轮询)。
完整代码(backend/api_server.py):
python
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__))) # 添加backend路径
from fastapi import FastAPI, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from uuid import uuid4
from collections import defaultdict
from typing import Dict, Any
import time
from quant_engine import run_quant_workflow # 导入引擎
app = FastAPI(title="Qlib A股量化API")
# CORS:允许Vite端口
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 全局任务状态
tasks: Dict[str, Dict[str, Any]] = defaultdict(dict)
def run_quant_async(task_id: str):
"""异步跑Qlib"""
tasks[task_id]['status'] = 'running'
tasks[task_id]['progress'] = 0.0
try:
# 模拟进度(实际可加Qlib回调)
for i in range(100):
time.sleep(0.1)
tasks[task_id]['progress'] = (i + 1) / 100.0
result = run_quant_workflow()
tasks[task_id]['result'] = result
tasks[task_id]['status'] = 'completed'
tasks[task_id]['progress'] = 1.0
except Exception as e:
tasks[task_id]['error'] = str(e)
tasks[task_id]['status'] = 'error'
tasks[task_id]['progress'] = 0.0
@app.get("/run_backtest")
def trigger_backtest(background_tasks: BackgroundTasks):
task_id = str(uuid4())
background_tasks.add_task(run_quant_async, task_id)
return {"task_id": task_id, "status": "started"}
@app.get("/task/{task_id}")
def get_task_status(task_id: str):
if task_id not in tasks:
return {"status": "not_found", "error": "Task not found"}
return tasks[task_id]
@app.get("/health")
def health_check():
return {"status": "Qlib A股系统运行中"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)
运行 :uvicorn api_server:app --reload --host 127.0.0.1 --port 8000
。
坑 & 修复:Network Error → 用127.0.0.1绑定;超时 → 异步 + 轮询。
3. 前端开发:React仪表盘
3.1 核心组件:App.tsx
异步拉API数据,进度条 + 卡片 + 曲线。
完整代码(frontend/src/App.tsx):
tsx
import React, { useEffect, useState } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import axios from 'axios';
interface BacktestData {
excess_return: { [date: string]: number };
risk_metrics: { sharpe?: number; volatility?: number; max_drawdown?: number; win_rate?: number };
annualized_return: number;
}
interface TaskStatus {
status: 'started' | 'running' | 'completed' | 'error';
progress?: number;
result?: BacktestData;
error?: string;
}
function App() {
const [data, setData] = useState<BacktestData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [taskId, setTaskId] = useState<string | null>(null);
const [progress, setProgress] = useState(0);
useEffect(() => {
let interval: NodeJS.Timeout;
axios.get('http://127.0.0.1:8000/run_backtest')
.then(res => {
const taskIdRes = res.data.task_id;
console.log('任务ID:', taskIdRes);
setTaskId(taskIdRes);
setLoading(true);
interval = setInterval(() => {
if (taskIdRes) {
axios.get(`http://127.0.0.1:8000/task/${taskIdRes}`)
.then(statusRes => {
console.log('任务状态:', statusRes.data);
const status: TaskStatus = statusRes.data;
setProgress(status.progress ? status.progress * 100 : 0);
if (status.status === 'completed' && status.result) {
setData(status.result);
setLoading(false);
clearInterval(interval);
} else if (status.status === 'error') {
setError(status.error || '任务失败');
setLoading(false);
clearInterval(interval);
}
})
.catch(err => {
console.error('轮询错误:', err);
setError('轮询失败');
setLoading(false);
clearInterval(interval);
});
}
}, 10000); // 10s轮询
return () => clearInterval(interval);
})
.catch(err => {
console.error('启动任务失败:', err);
setError('API启动失败: ' + err.message);
setLoading(false);
});
return () => {
if (interval) clearInterval(interval);
};
}, []);
if (loading) {
return (
<div className="flex flex-col justify-center items-center h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 text-gray-900 dark:text-white">
<div className="text-2xl mb-4 font-semibold">生成回测中... 进度: {progress.toFixed(0)}%</div>
<div className="w-96 bg-gray-200 rounded-full h-4">
<div
className="bg-gradient-to-r from-blue-600 to-indigo-600 h-4 rounded-full transition-all duration-300 shadow-md"
style={{ width: `${progress}%` }}
></div>
</div>
<p className="mt-4 text-sm text-gray-500">预计10min(首次训练)</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors shadow-lg"
>
取消重试
</button>
</div>
);
}
if (error) {
return (
<div className="flex flex-col justify-center items-center h-screen bg-gradient-to-br from-red-50 to-red-100 text-red-700">
<p className="text-xl mb-4 font-semibold">{error}</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
>
重试
</button>
</div>
);
}
const chartData = Object.entries(data?.excess_return || {}).slice(-100).map(([date, val]) => ({ date, return: val }));
return (
<div className="min-h-screen bg-gradient-to-br from-white to-gray-50 dark:from-gray-900 dark:to-gray-800 text-gray-900 dark:text-white p-8">
<h1 className="text-4xl font-bold mb-8 text-center text-indigo-600 dark:text-indigo-400">Qlib A股量化仪表盘</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-blue-50 dark:bg-blue-900/20 p-6 rounded-xl shadow-md border border-blue-200 dark:border-blue-800">
<h2 className="text-xl font-semibold text-blue-700 dark:text-blue-300 mb-2">年化收益</h2>
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400">{(data?.annualized_return * 100).toFixed(2)}%</p>
</div>
<div className="bg-green-50 dark:bg-green-900/20 p-6 rounded-xl shadow-md border border-green-200 dark:border-green-800">
<h2 className="text-xl font-semibold text-green-700 dark:text-green-300 mb-2">夏普比率</h2>
<p className="text-3xl font-bold text-green-600 dark:text-green-400">{data?.risk_metrics.sharpe?.toFixed(2) || 'N/A'}</p>
</div>
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-6 rounded-xl shadow-md border border-yellow-200 dark:border-yellow-800">
<h2 className="text-xl font-semibold text-yellow-700 dark:text-yellow-300 mb-2">最大回撤</h2>
<p className="text-3xl font-bold text-yellow-600 dark:text-yellow-400">{data?.risk_metrics.max_drawdown?.toFixed(2) || 'N/A'}</p>
</div>
<div className="bg-purple-50 dark:bg-purple-900/20 p-6 rounded-xl shadow-md border border-purple-200 dark:border-purple-800">
<h2 className="text-xl font-semibold text-purple-700 dark:text-purple-300 mb-2">胜率</h2>
<p className="text-3xl font-bold text-purple-600 dark:text-purple-400">{data?.risk_metrics.win_rate ? (data.risk_metrics.win_rate * 100).toFixed(1) + '%' : 'N/A'}</p>
</div>
</div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200">回测收益曲线</h2>
<ResponsiveContainer width="100%" height={400}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey="date" stroke="#6b7280" />
<YAxis stroke="#6b7280" />
<Tooltip formatter={(value) => [`${(value * 100).toFixed(2)}%`, '超额收益']} />
<Legend />
<Line type="monotone" dataKey="return" stroke="#3b82f6" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
}
export default App;
运行 :bun run dev
(localhost:5173)。

坑 & 修复:Network Error → 127.0.0.1:8000;超时 → 异步轮询10s;样式无 → postcss.config.cjs + content路径。
4. 集成测试 & 迭代
4.1 全链路测试
- 后端:
python quant_engine.py
→ dict输出(年化7.57%)。 - API:
uvicorn api_server:app --reload --host 127.0.0.1 --port 8000
→ /run_backtest返回task_id,/task/{id}进度→数据。 - 前端:刷新 → 进度条→卡片+曲线(hover%)。
4.2 迭代示例:跃岭股份 (002725.SZ) 个股策略
分析:当前15.26元,低估(P/E 15x),短期观望,中期持有(目标18元)。风险:汽车周期,机会:出口订单。
Qlib迭代:单股RSI均值回归(RSI<30买>70卖 + Alpha158)。
更新quant_engine.py config(instruments='002725',topk=1),exp_name="跃岭股份_RSI_Alpha158",end_time='2025-10-16'。运行:年化12%、Sharpe1.1。
扩展:加MACD handler(qlib.contrib.data.handler.MACD),或多股池(instruments='auto_parts')。
结语:量化入门心得
这个项目让我从0到1掌握量化全链路:Qlib的松耦合组件让策略迭代简单,前端可视化让结果直观。年化7.57%是起点,实盘需调参+风控。继续迭代:加Tushare实时数据、RL执行优化。量化是工具,不是魔法------数据+纪律=Alpha!
资源 :Qlib GitHub、Investopedia量化指南。欢迎反馈迭代!
(记录日期:2025-10-16)