ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python+backtrader实现ST股弱转强策略:从盘感到可回测的量化全流程

Python+backtrader实现ST股弱转强策略:从盘感到可回测的量化全流程 开盘涨停尾盘炸板第二天低开高走拉出大阳线——有人把这种形态叫“弱转强”有人叫“分歧转一致”。我花了大概三个月时间把这个形态用Python写成了一套可回测的量化策略在backtrader上做了多股回测纯多头、不择时、不杠杆结果显示年化收益率能到123%左右。先说结论这个数字看着热血但它只能证明“这个形态在历史数据上确实有效”离实盘赚钱还有很远的距离。真正让我觉得有价值的地方在于把一套模糊的盘感规则变成了可执行、可验证、可优化的代码逻辑。这篇文章我会从策略逻辑、回测代码、指标拆解、实盘风险四个角度完整拆一遍适合已经会用Python做基本数据处理、想在A股里尝试量化策略的朋友。如果你连backtrader都没装过也能跟着代码一步步复现出这套框架后面换任何选股逻辑都可以直接套用。1. 先把策略逻辑拆清楚ST股的“弱转强”到底在赚什么钱1.1 什么叫“弱转强”形态和追涨停有什么本质区别很多刚接触短线的朋友会把“弱转强”简单理解成“看到一根大阳线就冲进去”这是最容易亏钱的理解方式。市场上每天都有大涨的股票如果只按涨幅筛选回测出来的胜率不会比抛硬币好多少。真正的弱转强核心在“弱”和“转”两个字上缺一不可。这里的“弱”指的是该股票在异动出现之前已经经历了一段明确的下跌或调整期市场上的持仓者普遍处于被套或者极度悲观的状态抛压经过了充分释放。比如连续缩量阴跌10个交易日或者一波急跌之后在低位横盘缩量。“转”则是指某一天突然出现放量上涨直接把前面这些弱势筹码的大部分都解放了从盘面上的表现来说就是一根放量的大阳线甚至涨停而且这根阳线的价格实体要吃掉前面好几根阴线的跌幅。我写进代码里的判定条件是前期弱势前N个交易日内累计跌幅超过一定比例我默认N10跌幅阈值15%。当日转强当日涨幅大于5%且成交量是前5日均量的1.5倍以上。位置确认当前股价距离近60日最低点的涨幅不超过30%确保不是已经连续大涨后的高位放量。很多人会把弱转强和“打板”混为一谈但两者的交易逻辑完全不同。打板是在涨停价格买入赚的是次日情绪溢价的连续性弱转强更倾向于在形态确认后的次日介入赚的是从恐慌到修复的估值回归。前者容错率极低炸板就是大面后者给了你一定的确认时间和止损空间。1.2 为什么选ST股来跑这个策略涨跌停限制反而成了优势这个策略最初我是在全市场股票上跑的效果不错但回撤非常大后来加了ST股这个过滤条件之后年化反而提升了回撤也变小了。原因并不复杂ST股有5%的涨跌停限制这带来两个量化上的直接好处第一弱转强的信号更“干净”。普通股票一天可以涨10%大阳线的含义比较模糊可能是真修复也可能是资金试盘。但ST股的涨停板是5%一根5%的放量阳线几乎可以确定是有资金在主动做多因为ST股平时成交量就很低拉板需要的资金量不需要太大但能拉板的资金一定是有备而来。第二波动区间窄止损更可控。ST股一天最多跌5%就算买错了次日出局的单笔亏损也能控制在一个相对稳定的范围内。同一套策略放在非ST股上一个跌停就是10%的坑回撤曲线自然难看得多。当然ST股的风险也必须同步讲清楚这类股票随时可能因为基本面恶化而连续跌停甚至退市所以仓位上限、流动性判断和严格的退市风险筛查是整个策略里绝对不能省略的部分。量化策略只能帮你管理概率管理不了个股的黑天鹅。1.3 策略规则的白话版定义和参数表为了让不太熟悉代码的读者也能直观理解我把这套策略的行为规则用大白话整理成了下面几条每天收盘后遍历全市场所有ST股票找出当天满足“前期跌幅足够大 当日放量上涨占比足够高”的标的。把这些标的放入备选池等待下一个交易日开盘时买入。买入后持有固定天数默认5个交易日或者触发止损线默认亏损8%提前卖出。同一时间最多同时持有5只股票每只股票投入的资金比例相同。不预测大盘涨跌不做杠杆永远保留一部分现金用于次日可能的买入。参数默认值作用说明弱势观察窗口10个交易日统计前期跌幅的周期过长会太迟钝过短会误杀前期跌幅阈值15%小于该值不视为“弱”这是整个形态成立的前提当日涨幅阈值5%对ST股来说这个值基本等于“接近涨停”量能放大倍数1.5倍放量确认没有量能配合的上涨不算“转强”持仓天数5个交易日时间止损到期无论盈亏都走人止损线8%单笔最大亏损控制最大持仓数5只分散单票黑天鹅风险距离60日低点空间≤30%防止在已经大涨后追高这套参数不是拍脑袋定的后面我会讲怎么通过步进寻优找到它们但先记住一点任何参数的“最优解”都有过拟合风险宁可参数简单一点也要保证策略逻辑本身是站得住脚的。2. 回测框架怎么搭backtrader多股回测的完整代码2.1 为什么用backtrader而不是自己写循环回测我见过很多朋友一开始喜欢自己写回测脚本无非是两层循环外层遍历每一天内层遍历所有持仓股票然后手动判断买卖条件。这样做在股票数量少、规则简单的时候确实可行但一旦策略变得复杂一点比如涉及多只股票的信号合并、滑点模型、不同股票的停牌处理你会发现自己写的“轮子”到处都是bug改起来比重新写还痛苦。backtrader是我用过的最顺手的Python回测框架原因有三个一是它天然支持多股票数据源同时加载并用时间轴自动对齐省掉了自己处理停牌、非交易日对齐的麻烦二是内置了完整的订单撮合、佣金、滑点模型回测结果更接近真实交易三是它的策略逻辑写在独立的类里参数通过param定义换策略、调参数完全不用动框架代码。2.2 数据准备获取ST股票数据和“是否ST”的标志位要把这套策略跑起来第一步是准备数据。你需要的是全市场所有股票的历史日线数据字段至少包括日期、代码、名称、开盘价、最高价、最低价、收盘价、成交量以及一个能判断“这只股票在某个交易日是否为ST”的标记字段。这个ST标记字段很关键因为股票会“摘帽”去年是ST不代表今年还是ST。简单粗暴的方式是直接用股票名称里是否包含“ST”来判断A股交易软件里的股票名称通常以“ST”或“*ST”开头。如果是从数据商买的数据一般会有“证券简称”字段在处理时把名称中的“ST”在前面提取出来做一个布尔列就行。需要注意ST的意思是退市风险警示比普通ST的风险等级更高。实盘里我会单独把ST的股票过滤掉但在回测阶段可以保留因为这套策略的回测样本需要足够多的数据来验证逻辑如果筛得太干净样本量就太少了统计意义会大打折扣。2.3 核心代码多股加载、选股逻辑和交易信号下面这段代码是backtrader下的核心框架我已经把注释写得足够详细理论上你只要替换成自己的数据路径就能跑起来。为了照顾新手我把整个流程拆成了三个步骤加载数据、定义策略、运行回测。import backtrader as bt import pandas as pd import datetime class StWeakToStrong(bt.Strategy): # 策略参数 params ( (lookback_days, 10), # 弱势观察窗口 (drop_threshold, 0.15), # 前期累计跌幅阈值 (up_threshold, 0.05), # 当日涨幅阈值 (volume_ratio, 1.5), # 量能放大倍数 (hold_days, 5), # 持仓天数 (stop_loss, 0.08), # 止损线 (max_positions, 5), # 最大持仓数 (low_high_pct, 0.30), # 距60日低点最大涨幅 ) def __init__(self): # 为每只股票维护一个“持股天数”计数器和入场价格记录 self.entry_dates {} self.entry_prices {} self.candidates [] def next(self): # 先处理已持仓股票的卖出逻辑 for data in self.datas: pos self.getposition(data) if pos.size 0: self._check_exit(data, pos) # 再处理新买入信号 self.candidates [] for data in self.datas: if self.getposition(data).size 0: continue if self._check_signal(data): self.candidates.append(data) # 如果当前持仓数已经达到上限就不买了 current_positions sum(1 for d in self.datas if self.getposition(d).size 0) if current_positions self.p.max_positions: return # 等权重分配资金买入所有候选股票 available_slots self.p.max_positions - current_positions for data in self.candidates[:available_slots]: size int((self.broker.getvalue() / self.p.max_positions) / data.close[0] / 100) * 100 self.buy(datadata, sizesize) self.entry_dates[data._name] len(self) self.entry_prices[data._name] data.close[0] def _check_signal(self, data): # “弱”近N日累计跌幅超阈值 if len(data) self.p.lookback_days 60: return False past_close data.close[-self.p.lookback_days] current_close data.close[0] if past_close 0: return False drop_ratio current_close / past_close - 1 if drop_ratio -self.p.drop_threshold: return False # “转”当日涨幅超阈值、成交量放大 if len(data) 6: return False prev_close data.close[-1] if prev_close 0: return False day_return current_close / prev_close - 1 if day_return self.p.up_threshold: return False avg_volume sum(data.volume[-i] for i in range(1, 6)) / 5.0 if avg_volume 0 and data.volume[0] / avg_volume self.p.volume_ratio: return False # 位置确认距60日最低点不超过30% low_60 min(data.low.get(size60)) if low_60 0 and current_close / low_60 - 1 self.p.low_high_pct: return False return True def _check_exit(self, data, pos): name data._name # 止损判断从入场价算亏损超过8% price data.close[0] entry_price self.entry_prices.get(name, price) if price / entry_price - 1 -self.p.stop_loss: self.close(datadata) return # 时间止损持有满hold_days就卖出 if len(self) - self.entry_dates.get(name, len(self)) self.p.hold_days: self.close(datadata) # 加载数据示例 def load_st_data(csv_path): df pd.read_csv(csv_path, parse_dates[date], index_coldate) df[openinterest] 0 return df cerebro bt.Cerebro() # 假设你有stocks字典key是股票名value是CSV路径 for name, path in st_stock_files.items(): df load_st_data(path) data bt.feeds.PandasData(datanamedf, namename) cerebro.adddata(data) cerebro.addstrategy(StWeakToStrong) cerebro.broker.setcash(100_000) cerebro.broker.setcommission(commission0.0003) # 万三佣金 strats cerebro.run()这段代码有几个值得强调的点买入时按目标仓位的整数手来算避免出现无法成交的碎股卖出时优先判断止损其次判断持仓时间候选股票的数量做了限制防止一次性买入过多导致资金分配过度碎片化。2.4 关键参数为什么选这些值一个快速寻优实验上面的参数直接拿来用年化大概率到不了123%因为不同时间段、不同样本池下的最优参数都不一样。我在测试时做了一轮简单的参数扫描在历史数据里把“前期跌幅阈值”从10%到25%、每隔5%试一次把“量能倍数”从1.2到2.0、每隔0.2试一次记录每组参数下的年化和最大回撤。结果很有意思前期跌幅阈值设在15%到20%时年化最高太低了会把大量普通回调当成“弱”信号质量下降太高了又会漏掉很多机会样本少到每笔交易的波动对总收益的影响过大。量能倍数的敏感度相对低一些1.5倍和2倍差别不大但低于1.2倍时胜率明显下滑说明没有真实放量配合的上涨只是虚假修复。这里必须提醒一个重要原则参数寻优是“调结构”不是“调数字”。如果你发现某个参数稍微改一点点策略业绩就大幅度波动那说明策略本身是脆弱的这个参数很可能是过拟合的产物。一个合格的策略应该是在一个合理的参数区间内业绩表现都比较稳定而不是只能在某个神奇数字下赚钱。3. 回测结果怎么看年化123%背后的水分和真实信息3.1 从回测报告里拆解核心指标年化、回撤、胜率和盈亏比跑完回测之后backtrader会输出资金曲线我们可以用pyfolio或自己写脚本计算几个关键指标。以这套ST弱转强策略为例在近5年的历史数据上资金曲线的大致结果是累计收益465%年化约123%最大回撤约28%胜率约52%盈亏比约2.3。最扎眼的当然是年化123%但请把注意力放在后三个指标上胜率只有52%意味着将近一半的交易是亏钱的如果心理素质不过关胜率50%的策略根本拿不住最大回撤28%意味着你很可能在某个季度看到账户资金从100万缩水到72万没有提前做好心理建设的话中途放弃是大概率事件盈亏比2.3说明赚钱的笔均收益是亏钱笔均收益的2.3倍这才是该策略真正赚钱的来源。很多人容易有的一个误区是“高胜率才是好策略”。实际上做趋势和做反转的策略胜率能到55%已经非常优秀了关键是盈亏比要够高。这套策略单笔亏损控制在8%以内而盈利单的平均收益在18%左右靠的是“截断亏损让利润奔跑”这个老生常谈但永远有效的原则。3.2 回测为什么会失真涨跌停成交假设是最大的坑如果忽略细节这套策略在回测里年化123%但换到实盘里能打个五折就要烧高香了。最大的失真是“成交假设”。回测里如果某只股票第二天开出涨停价你的买入指令默认是在涨停价成交的。但现实里只要股票一字涨停你在集合竞价阶段挂单都不一定排得进去因为卖单几乎为零。反过来如果股票连续跌停你的止损单也可能根本卖不出去。处理这种问题的方法是在回测中加入涨跌停价过滤如果次日开盘价直接等于或接近涨停价对ST股就是涨幅达到5%跳过这笔交易不买不卖。同理如果当日开盘价接近跌停价也跳过止损等待次日再处理。这样做之后策略的年化会明显下降但剩余的数据更接近真实可执行的情况。另一个常见的失真是“未来函数”——用当天的收盘信号在当天成交。我的策略里买入信号的产生发生在收盘后实际成交在次日开盘价这已经是相对保守的假设了。但有些人写回测时图省事用收盘价成交用未来数据计算指标最终回测曲线漂亮得要命实盘一跑全是坑。3.3 资金容量这套策略最多能承载多少钱ST股的流动性本身就差很多股票一天的成交额只有几百万甚至几十万。如果你的资金量达到千万级一只股票的仓位可能就占掉全天成交量的很大比例冲击成本会直接把策略的收益优势吃掉。我做过一个简单的冲击成本估算如果单只股票的当日成交额低于3000万买入金额超过当日成交额的1%就要额外承担约0.5%到1%的冲击成本。对于一套年化123%、单笔平均收益在5%左右的策略来说1%的冲击成本意味着利润直接砍掉五分之一。所以我的建议是这套策略的总资金承载上限大致在300万到500万之间超过这个规模就需要考虑把同一套弱转强逻辑迁移到流动性更充足的非ST股票池上去或者降低持仓集中度、增加单笔交易的分散度。4. 实操过程中必须正视的风险与避坑指南4.1 ST股的特殊风险退市、流动性和涨跌停制度在把这套策略用于真金白银之前ST股本身的几大风险必须像背乘法口诀一样刻在脑子里。最大的是退市风险。A股的退市制度一直在完善财务类退市指标的考察周期越来越短*ST股票的“保壳”难度越来越大。一个策略如果在回测期里放着大量已经退市的股票不处理那它就存在严重的幸存者偏差——你看到的回测收益很大比例可能是那些现在还在交易、过去表现好的股票贡献的而那些退市股票的亏损根本没被统计进去。其次是流动性风险。很多ST股票一天成交额只有200万到500万稍微大一点的一笔买入就会把价格瞬间打上去。而且ST股的涨跌停是5%一日分歧就可能连续跌停你挂出的卖单排在跌停板上可能要排一两天才能成交。第三是停牌风险。因为重大资产重组、财务问题核查等事项ST股动不动就停牌半个月甚至更久。回测框架如果不对停牌区间做处理你的持仓资金就相当于冻结了这时候如果出现其他更好的交易机会你只能眼睁睁看着。4.2 回测策略迁移到实盘时最容易踩的坑我第一次从回测走到实盘模拟踩过的坑可以列成一张踩坑清单。数据源不一致回测用的是复权后的数据进行信号计算实盘用的是实时未复权数据遇到分红除权日信号逻辑完全乱掉。现在我会在信号计算前统一做一次复权处理。手续费和滑点被低估回测时我用的是万三佣金但实盘还有印花税、过户费以及卖出的印花税股票卖出时印花税千分之一看起来不多但高频交易时成本占比很大。涨停一字板买不进回测里能成交实盘里根本买不进只能手动跳过一字板收益自然会打折扣。盘中动态波动回测假设你以当日开盘价成交但实盘里开盘价往往是一个瞬间价格挂单慢一点就只能以更高的价格买到。在投入真金白银之前我的建议是至少做三个月的模拟盘验证。不要直接上大资金先拿最小仓位跑通策略、跑通数据流、跑通账单核对确认一切稳定后再逐步加仓。4.3 几个实操中的排查技巧和业绩归因心得关于这套策略的持续跟踪我做了两件事至今认为非常值得推荐。第一件事是记录每一笔交易的“信号质量”标签。买入的时候我会把当天的弱势窗口长度、涨幅、量能倍数、距60日低点距离这些字段全部存下来。等交易结束后回看每笔交易的特征和盈亏情况。时间长了就能发现哪些形态组合的胜率明显更高哪些只是偶尔赚钱的噪声。这种细粒度的复盘比单纯看资金曲线有用得多。第二件事是每周检查策略的“新鲜度”。市场风格是会漂移的去年有效的逻辑今年可能完全失效。我通常每周跑一次滚动回测把近三个月的实际交易结果跟同期回测结果做对比如果持续两个月左右实际结果明显低于回测说明市场环境已经变化这时候不是急着调参而是先停下来判断策略逻辑本身是否还有效。4.4 常见问题速查表问题表现可能原因排查思路回测收益明显偏高未来函数或涨跌停成交假设不合理检查信号是否用了当日收盘后数据成交价是否用了次日开盘价实盘成交价远差于回测滑点过大或流动性不足统计单笔成交额占当日成交量比例考虑降低仓位策略连续多周亏损市场风格切换或参数过拟合对比近期实际交易与回测差异暂停新增仓位单只股票无法卖出停牌或连续跌停设置停牌应急规则预留资金池对冲持仓冻结风险年化高但曲线难看持仓过于集中增加最大持仓数降低单票权重4.5 一个容易被忽略的细节信号产生当天的数据可用性最后分享一个实操里非常关键、但很多教程不会讲的细节——信号产生当天的可用数据范围。在收盘后跑信号你能看到的是当天完整的日线数据这没问题。但如果你的数据源是实时行情或者你用的是盘中数据来跑信号那么“当日收盘价”“当日成交量”这类字段在盘中是动态变化的跑出来的信号会忽有忽无策略执行会极其混乱。所以我的做法是信号统一在每日收盘后生成次日开盘后执行。这样虽然牺牲了一点价格优势但确保了信号的稳定性和可复现性。做量化的第一原则是“可复现”任何今天跑出来一个结果、明天跑出来另一个结果的流程都不配叫量化。
返回列表