Python 量化
Data sources, backtesting skeleton, and the traps that make backtests lie.
Key PointsA backtest without survivorship-bias handling, fees and slippage is fiction — fix those three before tuning anything.
Get Data
For A-share data, akshare (free) or tushare (token) are common. Pull daily bars into a DataFrame first.
Python
import akshare as ak
df = ak.stock_zh_a_hist(symbol="600519", period="daily",
start_date="20240101", end_date="20241231", adjust="qfq")
print(df.columns.tolist())
print(df.tail(2)[["日期", "收盘"]])Output
['日期', '股票代码', '开盘', '收盘', '最高', '最低', '成交量', '成交额', '振幅', '涨跌幅', '涨跌额', '换手率']
日期 收盘
239 2024-12-30 1524.00
240 2024-12-31 1521.00A Backtest Skeleton
A minimal backtest is signal → position → returns. Compute returns first, tune later.
Python
import pandas as pd
close = df["收盘"].astype(float)
ret = close.pct_change().fillna(0)
signal = (close > close.rolling(20).mean()).astype(int) # 20 日均线上穿
pos = signal.shift(1).fillna(0) # 次日开盘执行,避免未来函数
strat = pos * ret - 0.0003 * pos.diff().abs().fillna(0) # 扣双边手续费
nav = (1 + strat).cumprod()
print(round(nav.iloc[-1], 3))
print(round(strat.mean() / strat.std() * 252 ** 0.5, 2)) # 年化夏普Output
1.183
0.87Traps
| Trap | Effect | Fix |
|---|---|---|
| Look-ahead bias | Uses tomorrow's data today | Shift signals by one bar |
| Survivorship bias | Only delisted-free stocks tested | Include delisted universe |
| No fees/slippage | Returns inflated 2-5% a year | Subtract commission and slippage |
| Overfitting | Parameters fit noise | Out-of-sample and walk-forward |
Any backtest above 30% annualised is a code bug until proven otherwise. Validate out-of-sample before paper trading.
取数据
A 股行情常用 akshare(免费)或 tushare(需 token)。先把日线拉成 DataFrame,后面的计算都在它上面做。
Python
import akshare as ak
df = ak.stock_zh_a_hist(symbol="600519", period="daily",
start_date="20240101", end_date="20241231", adjust="qfq")
print(df.columns.tolist())
print(df.tail(2)[["日期", "收盘"]])Output
['日期', '股票代码', '开盘', '收盘', '最高', '最低', '成交量', '成交额', '振幅', '涨跌幅', '涨跌额', '换手率']
日期 收盘
239 2024-12-30 1524.00
240 2024-12-31 1521.00回测骨架
最小可用回测就是「信号 → 持仓 → 收益」三步。先算收益再看指标,不要一上来就调参数。
Python
import pandas as pd
close = df["收盘"].astype(float)
ret = close.pct_change().fillna(0)
signal = (close > close.rolling(20).mean()).astype(int) # 20 日均线上穿
pos = signal.shift(1).fillna(0) # 次日开盘执行,避免未来函数
strat = pos * ret - 0.0003 * pos.diff().abs().fillna(0) # 扣双边手续费
nav = (1 + strat).cumprod()
print(round(nav.iloc[-1], 3))
print(round(strat.mean() / strat.std() * 252 ** 0.5, 2)) # 年化夏普Output
1.183
0.87陷阱清单
| 陷阱 | 后果 | 修法 |
|---|---|---|
| 未来函数 | 用到了当天之后的数据 | 信号统一延后一根 K 线 |
| 幸存者偏差 | 只用没退市的股票 | 把退市股票纳入样本 |
| 忽略成本 | 年化虚高 2~5% | 扣手续费与滑点 |
| 过拟合 | 参数拟合了噪声 | 样本外与滚动验证 |
回测年化 30% 以上先怀疑代码,而不是怀疑自己发现了圣杯。先做样本外验证,再上模拟盘。