
用 Python 揭秘均值回归策略:你的收益从何而来?
量化交易的生死线:用 Python 算出你的最优下注比例
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 500 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
很多刚接触量化交易的 Python 学习者,往往从最简单的「双均线交叉」入手:短期均线上穿长期均线就买入,下穿就卖出。这个思路简单,但它只看价格方向,忽略了市场的另一半信息——成交量。
今天要拆解的是一个更进阶的思路:量价分析(Volume Spread Analysis,简称 VSA)。它的核心理念很朴素:同样一根上涨的 K 线,放巨量拉出的大阳线和缩量磨出的小阳线,含义完全不同;收在最高点附近和收在最低点附近,透露的信息也天差地别。
我们会结合一个真实回测案例来讲解:在日线上运行一年,该策略最终录得 43.25% 的收益,而同期买入并持有反而亏损 48.06%,两者相差 91.31 个百分点。下面就用 Python 和 Backtrader 一步步把它讲清楚。
传统均线策略只盯着价格,而 VSA 策略在每根 K 线上会同时观察四类信息:
high - low 衡量 K 线的振幅,再和近期平均振幅对比,判断这根 K 线是「宽幅」还是「窄幅」。把这四个维度组合起来,策略就能识别出一系列经典的量价形态,例如「停止量」「多头强势」「空头弱势」「高潮量」「测试支撑」「测试阻力」等。
它的目标不是抓住每一次波动,而是只在成交量、振幅、收盘位置、趋势方向四者达成一致时才出手。
Backtrader 里习惯用 params 元组来集中管理可调参数。这个策略暴露出来的参数如下:
params = ( ('volume_period', 7), # 计算平均成交量的窗口长度 ('volume_threshold', 1.2), # 成交量放大到多少倍算「高量」 ('spread_period', 7), # 计算平均振幅的窗口长度 ('spread_threshold', 1.2), # 振幅超过多少倍算「宽幅」 ('trend_period', 30), # 判断趋势用的均线周期 ('climax_volume_mult', 2.0), # 放量到多少倍算「高潮量」 ('test_volume_mult', 0.5), # 成交量萎缩到多少倍算「低量」 ('trail_stop_pct', 0.05), # 入场后的移动止损比例(5%))把参数集中定义有两个好处:一是逻辑清晰、方便阅读;二是后续做参数优化(optimization)时,Backtrader 可以直接对这些参数做网格搜索。
策略先把 OHLCV(开高低收量)数据取出来,并计算两个核心派生量——价差和收盘位置:
self.high = self.data.highself.low = self.data.lowself.close = self.data.closeself.open = self.data.openself.volume = self.data.volume# 价差:最高价减最低价,衡量这根 K 线的振幅self.spread = self.high - self.low# 收盘位置:收盘价在整根 K 线区间中的相对高度,取值 0~1# 越接近 1 说明收得越高,越接近 0 说明收得越低self.close_position = (self.close - self.low) / (self.high - self.low)小提示:
(high - low)有可能为 0(比如一字板行情),实盘代码里最好加上除零保护,否则会报错。
# 成交量的移动平均,用来判断放量还是缩量self.volume_ma = bt.indicators.SMA(self.volume, period=self.params.volume_period)# 振幅的移动平均,用来判断宽幅还是窄幅self.spread_ma = bt.indicators.SMA(self.spread, period=self.params.spread_period)# 收盘价的移动平均,作为趋势判断的基准线self.trend_ma = bt.indicators.SMA(self.close, period=self.params.trend_period)# 当前成交量相对近期平均量的比值volume_ratio = self.volume[0] / self.volume_ma[0]if volume_ratio >= self.params.climax_volume_mult: return 'climax' # 高潮量:放量到 2 倍以上elif volume_ratio >= self.params.volume_threshold: return 'high' # 高量:放量到 1.2 倍以上elif volume_ratio <= self.params.test_volume_mult: return 'low' # 低量:萎缩到 0.5 倍以下else: return 'normal' # 正常量spread_ratio = self.spread[0] / self.spread_ma[0]if spread_ratio >= self.params.spread_threshold: return 'wide' # 宽幅 K 线elif spread_ratio <= (2 - self.params.spread_threshold): return 'narrow' # 窄幅 K 线else: return 'normal'这里 2 - spread_threshold 是个巧妙的对称写法:阈值取 1.2 时,宽幅门槛是 1.2 倍,窄幅门槛就是 0.8 倍,围绕「1 倍」上下对称。
# 收盘位置分类close_pos = self.close_position[0]if close_pos >= 0.7: return 'high' # 收在高位elif close_pos <= 0.3: return 'low' # 收在低位else: return 'middle' # 收在中间# 趋势方向判断if self.close[0] > self.trend_ma[0]: return 'up' # 价格在趋势均线之上,判定为上升趋势elif self.close[0] < self.trend_ma[0]: return 'down' # 价格在趋势均线之下,判定为下降趋势else: return 'sideways' # 横盘有了上面几个分类标签,就可以拼装出具体的量价形态。举两个例子:
停止量(stopping volume):下跌趋势中突然放出高潮量,但 K 线并没有收弱,说明抛压可能被大资金接走:
if ( volume_class == 'climax' and # 高潮量 trend == 'down' and # 处于下降趋势 is_down_bar and # 这是一根下跌 K 线 close_class in ['middle', 'high'] # 但收盘没有收在低位): return 'stopping_volume', 4 # 返回形态名和强度分(满分 4)高潮顶(bearish climax):上升趋势中放巨量拉出宽幅大阳线并收在高位,往往是多头力竭的信号:
if ( volume_class == 'climax' and # 高潮量 spread_class == 'wide' and # 宽幅 K 线 close_class == 'high' and # 收在高位 is_up_bar and # 这是一根上涨 K 线 trend == 'up' # 处于上升趋势): return 'climax', 4策略还会回看最近 3 根 K 线作为「背景」加分,判断当前信号是否有连续放量的配合:
context_score = 0for i in range(1, 4): # 回看前 1~3 根 K 线 if len(self.data) <= i: continue prev_volume = self.volume[-i] if prev_volume > self.volume_ma[-i]: # 若历史某根放量 context_score += 1 # 背景分加 1信号强度加上背景分,只有总分足够高才允许交易:
pattern, strength = self.detect_vsa_patterns()if pattern is None or strength < 2: # 没有形态或强度太弱,直接跳过 returncontext = self.check_background_context()total_strength = strength + context # 形态强度 + 背景分if total_strength < 3: # 综合分不足,放弃 return看多形态开多仓、平空仓;看空形态则相反:
# 看多形态:可以开多仓,或平掉已有空仓if pattern in ['no_demand', 'no_supply', 'stopping_volume', 'strength', 'test_support', 'effort_down']: if self.position.size < 0: # 当前持有空仓 self.order = self.close() # 先平空 elif not self.position: # 当前空仓(无持仓) if total_strength >= 4 or pattern in ['stopping_volume', 'no_supply']: self.order = self.buy() # 开多仓最后用移动止损控制风险:入场后自动挂一个比成交价低 5% 的止损单。
if order.isbuy() and self.position.size > 0: self.entry_price = order.executed.price # 止损价 = 成交价 × (1 - 5%) self.trail_stop_price = order.executed.price * (1 - self.params.trail_stop_pct) self.stop_order = self.sell( exectype=bt.Order.Stop, # 止损单类型 price=self.trail_stop_price, )至此一个完整的交易闭环就成型了:分类 K 线 → 识别形态 → 计算综合强度 → 达标才入场 → 移动止损管理。
在 ETH-USD 日线、时间跨度一年的数据上运行,最终结果如下:
值得注意的是,这个成绩不是靠一笔运气好的持仓撑起来的:全程完成了 12 笔已平仓交易,收尾时没有留下任何未平仓头寸,胜率约三分之二。资金从 10000 美元增长到约 14324.86 美元,而同期直接持有 ETH 则大幅下跌,最大回撤也仅 11.98%,成功规避了单边下跌的大部分风险。
冷静提醒:单一品种、单一时间段、样本仅 12 笔交易的回测,统计意义有限。收益亮眼不代表可以直接实盘,务必再验证手续费、滑点、不同市场环境下的稳健性。
这个 VSA 案例的价值,不在于它跑出了多高的收益,而在于它示范了一种比单纯看价格更立体的思路:把成交量、振幅、收盘位置、趋势背景当作交易的「上下文」来综合判断,而不是只盯着价格方向。
对 Python 量化学习者来说,可以带走三点:
params 统一管理旋钮,用打分机制过滤弱信号,代码会更清晰、更易扩展。沿着这条路,你还可以继续尝试云图突破、ZigZag 枢轴、波动率突破、均值回归、动量过滤、趋势跟踪离场等更多思路,逐步搭建属于自己的策略库。
本文所有内容仅用于学习和研究,不构成任何投资建议。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐