by 雪隐_上班了 from juejin.cn/user/143341...
欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权。
1. 为什么要量化回测?(别只靠玄学)
在搞技术前,先回答一个灵魂拷问:你这交易策略,到底是真牛逼,还是你觉得它牛逼?
1.1 避免"马后炮"综合症
你是不是经常这样:看到一只股票涨了20%,一拍大腿:"我早就知道会涨!" 然后开始编十个理由,像极了考试后对答案------永远觉得自己选对了。
量化回测就是你的时光机 。它在每一个历史时间点上,只用当时已经发生的数据做决策,绝不偷看未来。没有未来函数,没有作弊,跟高考一样公平。
1.2 用数据说话,而不是靠直觉
一个交易想法,如果不经过几百只股票、几年数据的毒打,那就只是**"我觉得"**。量化回测能让你:
- 在几百只股票、好几年的行情里验证策略------相当于请历史当考官
- 看到胜率、夏普比率、最大回撤等硬核指标------这些数字比你的第六感靠谱100倍
- 区分策略是真能赚钱 ,还是刚好踩了狗屎运
1.3 参数调优?让电脑替你熬夜
手工交易时,想测试一下均线用10天还是20天好?你得画图、算账、掉头发。量化回测一秒跑完所有组合:
- 快速比较不同参数下的表现,像选手机套餐一样简单
- 发现策略在哪些市场环境下会扑街(这就是传说中的过拟合检测)
- 自动加上最大回撤止损等风控规则------亏到一定比例,策略自己喊停
1.4 从"我想赚钱"到"我知道策略能不能赚钱"
量化回测帮你回答一个终极问题:
在过去 X 年里,这个策略在 YY% 的市场环境下,有没有稳定盈利过?
如果一个策略在过去5年、各种熊市牛市里一直在亏钱,你还指望它未来帮你买房?别做梦了。
2. Backtrader 简介------回测界的瑞士军刀
Backtrader 是一个纯 Python 写的回测框架,界面简洁,设计模块化,就像乐高。相比其他回测工具,它有几个优点:
- 设计相当直观 :核心概念叫
Cerebro(大脑),你往里面塞数据、策略、经纪商,它就能跑起来------跟指挥你的宠物狗去捡球一样自然 - 策略就是一个类 :继承
bt.Strategy,在__init__里定义指标,在next()里写买卖逻辑。会写类就会写策略 - 内置一大堆分析器:夏普比率、最大回撤、交易统计......开箱即食,不用自己造轮子
- 直接喂 Pandas DataFrame:不需要格式转换,DataFrame 塞进去就能跑,懒人福音
3. 核心概念------大脑、饲料、经纪人,还有你
3.1 Cerebro(大脑)------总指挥官
Cerebro 是 Backtrader 的总控,负责:
- 加载数据源(你的 K 线饲料)
- 添加策略(你的交易想法)
- 设置经纪商(模拟券商,收你佣金)
- 运行回测(点一下,开始跑)
- 收集结果(赔了赚了,一目了然)
python
cerebro = bt.Cerebro()
cerebro.addstrategy(MyStrategy) # 把你的策略加进去
cerebro.adddata(bt.feeds.PandasData(dataname=df)) # 喂数据
cerebro.broker.setcash(1000000) # 给你100万虚拟启动资金(羡慕吧)
cerebro.broker.setcommission(commission=0.0002) # 佣金万二,比某些真券商还便宜
cerebro.addsizer(bt.sizers.PercentSizer, percents=95) # 每次用95%的资金梭哈(保守派可以改小)
results = cerebro.run() # 3、2、1,发射!
3.2 Data Feed(数据源)------K 线的自助餐
Backtrader 支持 N 种数据源,最常用的就是 PandasData,直接把 pandas DataFrame 往里扔:
python
cerebro.adddata(bt.feeds.PandasData(dataname=df))
DataFrame 的 index 必须是 datetime ,列名要叫 open、high、low、close、volume(大小写不敏感)。不按规矩来,它就不理你。
3.3 Strategy(策略)------你的摇钱树脚本
策略类继承 bt.Strategy,核心方法就四个:
| 方法 | 作用 | 吐个槽 |
|---|---|---|
__init__ |
初始化指标,只执行一次 | 像买车,只买一次 |
next |
每个 K 线执行一次 | 像开车,每根 K 线踩一脚 |
notify_order |
订单状态变了就回调 | "报告,你的挂单成交了!" |
notify_trade |
交易变了就回调 | "报告,你的买卖亏了/赚了!" |
关键属性:
self.data.close:收盘价序列(别拿它当数组用,它是特殊对象)self.data.close[0]:最新收盘价(0表示当前,-1表示上一个)self.position:当前持仓数量(亏光了就是0)self.buy()/self.close():买入 / 卖出(别点反了)
3.4 Broker(经纪商)------雁过拔毛的中间商
经纪商模拟真实交易所,负责:
- 记录你还有多少现金,持了多少股
- 每次交易扣佣金(万恶的中间商)
- 执行你的买卖订单(不滑点,但现实会滑)
python
cerebro.broker.setcash(1000000) # 初始资金100万(虚拟的,别激动)
cerebro.broker.setcommission(commission=0.0002) # 佣金万分之二
cerebro.addsizer(bt.sizers.PercentSizer, percents=95) # 每次拿95%的钱去买,留5%吃手续费
3.5 Analyzer(分析器)------赛后技术统计
回测跑完了,到底赚了没?赚了多少?亏了多惨?交给分析器:
python
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe", riskfreerate=0.02)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")
跑完之后,通过 strat.analyzers.sharpe.get_analysis() 提取,跟查考试成绩一样。
4. 数据加载------把 K 线喂给 Backtrader
4.1 数据来源:偷懒用 HTTP API
回测需要历史 K 线数据。我们直接用项目里的 005_QMT_server 提供的 HTTP API,省得自己去找数据(懒人必备)。
| 端点 | 说明 |
|---|---|
GET /api/history?code=600519&period=1d&start=2024-01-01&end=2025-12-31 |
获取贵州茅台的历史日K线 |
返回字段:date、open、high、low、close、volume、amount。够用了,又不是做高频。
4.2 数据获取函数------用 requests 优雅地白嫖
python
API_BASE = "http://127.0.0.1:8080"
def load_data(stock_code, start_date=None, end_date=None, count=500):
"""从 005 QMT API 获取 K 线数据(别把服务打挂了)"""
url = f"{API_BASE}/api/history"
params = {
"code": stock_code,
"period": "1d",
"start": start_date or "",
"end": end_date or "",
}
resp = requests.get(url, params=params, timeout=30)
resp.raise_for_status() # 出错了就直接报错,不藏着掖着
data = resp.json()
records = data.get("records", [])
df = pd.DataFrame(records)
df["date"] = pd.to_datetime(df["date"])
df.set_index("date", inplace=True)
df.sort_index(inplace=True)
if count > 0 and len(df) > count:
df = df.tail(count) # 只取最近 N 根,免得电脑冒烟
return df
4.3 DataFrame 格式要求------别让 Backtrader 闹脾气
PandasData 对 DataFrame 的格式有洁癖,必须长这样:
| 字段 | 说明 | 备注 |
|---|---|---|
| index (datetime) | 日期时间 | 必须是 datetime 类型,字符串它不认 |
| open | 开盘价 | 别填成收盘价 |
| high | 最高价 | 别小于 low |
| low | 最低价 | 别大于 high |
| close | 收盘价 | 最重要的列 |
| volume | 成交量 | 整数就行,不要求单位 |
5. 五种基础策略------从捡硬币到追涨杀跌
这里给你准备了五个经典策略,从"幼儿园级别"到"小学毕业"。你可以像吃自助餐一样挑着用。
5.1 双均线策略(DoubleMAStrategy)------金叉买,死叉卖,简单粗暴
原理:短期均线上穿长期均线就买,下穿就卖。没有比这更经典的了。
python
class DoubleMAStrategy(bt.Strategy):
params = (("fast", 10), ("slow", 30))
def __init__(self):
self.ma_fast = bt.indicators.SMA(self.data.close, period=self.p.fast)
self.ma_slow = bt.indicators.SMA(self.data.close, period=self.p.slow)
self.crossover = bt.indicators.CrossOver(self.ma_fast, self.ma_slow)
def next(self):
if not self.position:
if self.crossover > 0:
self.buy()
elif self.crossover < 0:
self.close()
参数说明:
fast=10:快速均线周期(10日,短线敏感)slow=30:慢速均线周期(30日,长线稳重)
吐槽:这个策略在趋势行情里很爽,在震荡行情里会被来回打脸。打脸打多了就习惯了。
5.2 MACD 策略(MACDStrategy)------别人恐惧我贪婪,别人贪婪我......看 MACD
原理:MACD 线与信号线金叉买入,死叉卖出。高级一点的均线策略。
python
class MACDStrategy(bt.Strategy):
def __init__(self):
self.macd = bt.indicators.MACD(
self.data.close, period_me1=12, period_me2=26, period_signal=9
)
self.crossover = bt.indicators.CrossOver(self.macd.macd, self.macd.signal)
def next(self):
if not self.position:
if self.crossover > 0:
self.buy()
elif self.crossover < 0:
self.close()
MACD 参数:
period_me1=12:快速 EMA(12 日)period_me2=26:慢速 EMA(26 日)period_signal=9:信号线(9 日)
吐槽:MACD 几乎人手一个,但用它赚到钱的人......嗯,你猜。
5.3 RSI 策略(RSIStrategy)------跌多了买,涨多了卖,像个老股民
原理:RSI 低于 30 算超卖,买!高于 70 算超买,卖!典型的均值回归思维。
python
class RSIStrategy(bt.Strategy):
params = ("period", 14), ("upper", 70), ("lower", 30)
def __init__(self):
self.rsi = bt.indicators.RSI(self.data.close, period=self.p.period)
def next(self):
if not self.position:
if self.rsi < self.p.lower:
self.buy()
elif self.rsi > self.p.upper:
self.close()
参数说明:
period=14:RSI 周期(传统值 14)lower=30:超卖线(低于 30 就捡便宜)upper=70:超买线(高于 70 就止盈)
吐槽:这个策略在震荡市里如鱼得水,但在单边牛市中你会卖飞,在单边熊市中你会抄底抄在半山腰。人生总是有舍有得。
5.4 布林带策略(BBandsStrategy)------下轨买,上轨卖,像根皮筋
原理:价格碰到下轨(超卖)就买,碰到上轨(超买)就卖。也是均值回归。
python
class BBandsStrategy(bt.Strategy):
params = (("period", 20), ("devfactor", 2.0))
def __init__(self):
self.bbands = bt.indicators.BollingerBands(
self.data.close, period=self.p.period, devfactor=self.p.devfactor
)
def next(self):
if not self.position:
if self.data.close < self.bbands.bot:
self.buy()
elif self.data.close > self.bbands.top:
self.close()
参数说明:
period=20:中轨均线周期(20 日)devfactor=2.0:标准差倍数(2 倍,传统值)
布林带三轨:
bbands.top:上轨 = 中轨 + 2σbbands.mid:中轨 = 20 日均线bbands.bot:下轨 = 中轨 - 2σ
吐槽:布林带在震荡行情里很好用,但如果遇到趋势突破,价格会沿着上轨一直涨,你卖飞后会想打自己。所以很多人在突破上轨时反而追买------那是另一套玩法了。
5.5 动量策略(MomentumStrategy)------追涨杀跌,简单直接
原理:动量 > 0(价格比 N 天前高)就买,动量 < 0 就卖。典型的趋势跟踪。
python
class MomentumStrategy(bt.Strategy):
params = (("period", 10),)
def __init__(self):
self.mom = bt.indicators.Momentum(self.data.close, period=self.p.period)
def next(self):
if not self.position:
if self.mom > 0:
self.buy()
elif self.mom < 0:
self.close()
参数说明:
period=10:动量计算周期(10 日)
动量计算:当前价格 - N 日前价格 > 0 即为正。
吐槽:这个策略在牛市里舒服得不要不要的,但在震荡市里会频繁止损------今天买,明天卖,像极了韭菜的日常。
5.6 策略注册表------点菜一样选策略
所有策略放到一个字典里,方便你按名字调用:
python
STRATEGIES = {
"double_ma": DoubleMAStrategy,
"macd": MACDStrategy,
"rsi": RSIStrategy,
"bbands": BBandsStrategy,
"momentum": MomentumStrategy,
}
用的时候:
python
strat_cls = STRATEGIES.get(strategy_name, DoubleMAStrategy)
cerebro.addstrategy(strat_cls)
6. 策略轮动(ADX 市场 Regime 切换)------打不过就加入,市场变你也变
单一策略就像一把锤子,看什么都像钉子。然而市场有时是趋势,有时是震荡------你需要一个工具箱。
6.1 核心思想------看菜吃饭,量体裁衣
| 市场状态 | ADX 值 | 应该用的策略 | 策略类型 |
|---|---|---|---|
| 趋势市 | ADX > 25 | MACD | 趋势跟踪(顺势而为) |
| 震荡市 | ADX ≤ 25 | RSI | 均值回归(高抛低吸) |
ADX 就是市场的"狂躁指数":值越高,趋势越猛;值越低,市场在打酱油。
6.2 RotationStrategy 实现------一个策略统治所有市场
python
class RotationStrategy(bt.Strategy):
params = (
("adx_period", 14),
("adx_threshold", 25),
("rsi_period", 14),
("rsi_lower", 35),
("rsi_upper", 65),
("max_drawdown_stop", 0.08),
("grace_bars", 5),
("position_pct", 0.50),
)
参数说明(别怕,一个一个来):
adx_period=14:ADX 计算周期(14 天,传统值)adx_threshold=25:趋势与震荡的分界线(ADX > 25 算趋势)rsi_lower=35/rsi_upper=65:放宽的 RSI 阈值(相比基础版的 30/70,更温和一点)max_drawdown_stop=0.08:最大回撤止损 8% ------ 亏 8% 就强制离场grace_bars=5:买入后前 5 天不检查回撤(防止刚买就遇到小波动误触)position_pct=0.50:每次最多用 50% 的资金(不梭哈,活得久)
6.3 ADX 指标------市场的"情绪温度计"
ADX(Average Directional Index)只告诉你趋势强不强,不告诉你方向。
- ADX 上升 → 趋势在加强(无论涨跌)
- ADX 下降 → 市场在震荡
python
self.adx = bt.indicators.ADX(period=self.p.adx_threshold)
6.4 最大回撤止损------保住你的韭菜根
在 next() 里实时监控账户总值的回撤:
python
current_value = self.broker.getvalue()
if current_value > self.max_value:
self.max_value = current_value
if self.bars_held >= self.p.grace_bars:
drawdown = (self.max_value - current_value) / self.max_value
if drawdown >= self.p.max_drawdown_stop:
self.stopped = True
if self.position:
self.close()
逻辑:记录持仓期间的最高市值,过了保护期后,如果从最高点回撤达到 8%,立刻清仓并停止后续交易。这叫"断臂求生",总比全亏完强。
6.5 完整交易逻辑------人话版
python
def next(self):
# 1. 更新最高市值
current_value = self.broker.getvalue()
if current_value > self.max_value:
self.max_value = current_value
# 2. 检查是否需要止损
if self.stopped:
return
if self.bars_held >= self.p.grace_bars:
drawdown = (self.max_value - current_value) / self.max_value
if drawdown >= self.p.max_drawdown_stop:
self.stopped = True
if self.position:
self.close()
return
# 3. 判断当前市场状态
adx_val = self.adx[0]
trend_mode = adx_val > self.p.adx_threshold
# 4. 如果空仓:根据市场状态选择开仓策略
if not self.position:
self.bars_held = 0
if trend_mode:
# 趋势市 → 用 MACD 金叉买入
if self.crossover_macd > 0:
size = int((self.broker.getvalue() * self.p.position_pct) / self.data.close[0])
if size > 0:
self.buy(size=size)
else:
# 震荡市 → 用 RSI 超卖买入
if self.rsi < self.p.rsi_lower:
size = int((self.broker.getvalue() * self.p.position_pct) / self.data.close[0])
if size > 0:
self.buy(size=size)
# 5. 如果持仓:根据市场状态决定平仓
else:
self.bars_held += 1
if trend_mode:
# 趋势市 → MACD 死叉卖出
if self.crossover_macd < 0:
self.close()
else:
# 震荡市 → RSI 超买卖出
if self.rsi > self.p.rsi_upper:
self.close()
7. 运行回测与结果解读------是骡子是马拉出来遛遛
7.1 运行单策略回测
打开终端,输入一行命令:
bash
python scripts/run_backtest.py 600519 2024-01-01 2025-12-31 macd
格式:股票代码 开始日期 结束日期 策略名
可选策略:double_ma、macd、rsi、bbands、momentum
7.2 运行策略轮动回测
bash
python scripts/run_rotation_backtest.py 600519 2022-01-01 2025-12-31 --max-dd 0.08 --pos-pct 0.50
可选参数:
--max-dd:最大回撤止损比例,默认 0.08(8%)--pos-pct:仓位上限,默认 0.50(50%)
7.3 输出结果示例------看看你是赚是亏
json
{
"stock_code": "600519",
"strategy": "macd",
"start_date": "2024-01-02",
"end_date": "2025-12-31",
"trading_days": 489,
"initial_cash": 1000000,
"final_value": 1250000.00,
"total_return": 0.25,
"annual_return": 0.1175,
"max_drawdown": -0.082,
"sharpe_ratio": 1.35,
"total_trades": 12,
"win_rate": 0.6667
}
7.4 关键指标解读------这些数字代表什么意思?
| 指标 | 说明 | 怎样算好? |
|---|---|---|
total_return |
总收益率(25% 就是说 100 万变 125 万) | 越高越好,但别贪心 |
annual_return |
年化收益率(把总收益换算到每年) | 比银行定期高就算及格 |
max_drawdown |
最大回撤(历史上最多亏了多少) | -8% 比 -30% 舒服多了 |
sharpe_ratio |
夏普比率(每承担一单位风险赚多少超额收益) | >1 优秀,>2 牛逼 |
total_trades |
总交易次数 | 太多可能手续费爆炸 |
win_rate |
胜率(赚钱的交易占比) | 高胜率不一定赚钱,还要看盈亏比 |
stopped_early |
是否触发了止损 | 触发了说明风控起了作用,好事 |
7.5 回测框架流程图------一张图看懂全过程
scss
run_backtest()
├── load_data() → 从 API 白嫖 K 线数据
├── Cerebro 配置
│ ├── addstrategy() → 把你选好的策略塞进去
│ ├── adddata() → 把数据塞进去
│ ├── broker.setcash() → 给你虚拟资金(随便写,反正不是真的)
│ ├── broker.setcommission() → 让中间商赚点差价(佣金)
│ ├── addsizer() → 决定每次下注多大
│ └── addanalyzer() → 装好赛后统计工具
├── cerebro.run() → 点火,开跑!
└── 提取分析结果 → 输出 JSON,高兴或悲伤
附录:策略参数一览(方便你抄作业)
| 策略 | 参数 | 默认值 | 说明 |
|---|---|---|---|
| DoubleMAStrategy | fast | 10 | 快速均线周期(越短越敏感) |
| slow | 30 | 慢速均线周期(越长越迟钝) | |
| MACDStrategy | period_me1 | 12 | 快速 EMA 周期 |
| period_me2 | 26 | 慢速 EMA 周期 | |
| period_signal | 9 | 信号线周期 | |
| RSIStrategy | period | 14 | RSI 周期 |
| upper | 70 | 超买线(卖点) | |
| lower | 30 | 超卖线(买点) | |
| BBandsStrategy | period | 20 | 布林带中轨周期 |
| devfactor | 2.0 | 标准差倍数(轨道宽度) | |
| MomentumStrategy | period | 10 | 动量周期 |
| RotationStrategy | adx_period | 14 | ADX 周期 |
| adx_threshold | 25 | 趋势分界线(>25 算趋势) | |
| rsi_lower | 35 | 震荡市买入 RSI 阈值(更宽松) | |
| rsi_upper | 65 | 震荡市卖出 RSI 阈值 | |
| max_drawdown_stop | 0.08 | 最大回撤止损线(8%) | |
| grace_bars | 5 | 买入后保护期(5 根 K 线) | |
| position_pct | 0.50 | 单次最大仓位(50%) |
写在最后
这篇文章如果对你有帮助,请点赞 + 评论 ,让我知道你没有被 K 线折磨死。
如果有哪里写得不够风趣,欢迎吐槽------毕竟,亏钱的路上有你,我就没那么孤独了。😄
Happy backtesting! 愿你回测暴富,实盘不亏。