ARTICLE DETAIL

资讯详情

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

DualThrust策略源码复现与Python回测实战:从算法骨架到改进

DualThrust策略源码复现与Python回测实战:从算法骨架到改进 简介文档以量化经典DualThrust交易系统为主线面向量化交易初学者与CTA策略研究者帮助理解区间突破策略的触发机制并完成从原始模型到改进模型的代码借鉴。内容包含原始EasyLanguage源码、改进模型源码以及K1、K2、Mday、Nday、lots、offset等关键参数的完整解析交易逻辑覆盖无持仓、多头、空头三种状态下的开仓、平仓与反手规则。改进模型加入动态周期处理、滑点偏移和PlotNumeric可视化便于回测观察买卖触发位置文档还说明了BuyRange、SellRange、BuyTrig、SellTrig的计算过程以及CurrentBar、MarketPosition等代码状态变量可直接服务策略开发与参数调优同时指出震荡市效果有限需结合趋势环境使用。全包共1个PDF文件压缩后约34KB轻量易读适合作为量化策略源码笔记或改造参考。目前已有125人浏览学习可快速上手DualThrust并借鉴其改进思路。1. DualThrust是什么一个只看开盘价和前日极值的日内突破策略DualThrust是被量化交易系统从业者反复研究最多的经典日内策略之一源码只有几十行却能衍生出几十种改进模型。它的核心逻辑极简用前一日的K线算出四个价格从中推出一个波动区间Range再用今日开盘价加减K1、K2两个比例系数挂出上下两条触发轨。价格突破上轨就做多跌破下轨就做空收盘前离场。正因为结构简单它才特别适合当作“读源码、改模型、搭回测”的入门样本——不管是做股指期货、商品期货还是加密短线只要你盯盘或跑自动化DualThrust都是值得先吃透的一课也是不少量化比赛里的经典作业题。2. 拆解DualThrust的算法骨架四值、Range、上下轨和K参数2.1 HH、HC、LL、LC四个前日价格的取法与口径DualThrust的全部预测基础是前一天已经走出来的K线。它默认一个假设今天的日内波动空间大概率不会超过昨天被证明过的空间太多。所以最重要的动作就是先把昨天的“空间”量化出来。不同源码版本里四个值的定义不太一样最常见的一套是HH取前一日的最高价HC取前一日收盘价LL取前一日最低价LC取前一日开盘价。这里最容易踩坑的就是LC。有的代码里LC取前一日收盘价有的取前一日开盘价也有的把HH和HC改成过去N日极值。口径不同算出来的Range在跳空日会差出好几个档次。比如昨天高开低走、收盘接近最低点LC取开盘价时HH-LC是一个很大的正值LC取收盘价时这个值会小很多。你在PDF里看到公式时第一件事不是去背字母而是先确认它每一行到底引用了哪一列。变量经典口径常见变体影响HH前日最高价前N日最高价的最高过滤掉隔日异常极值HC前日收盘价前N日收盘价的最高偏多品种放大上行区间LL前日最低价前N日最低价的最低同理LC前日开盘价前日收盘价 / 前N日收盘价最低对跳空日的Range影响最大我一般拿到源码会先做一件事把源码的Range计算改写成一个独立函数用一小段日线数据打印出来和行情软件里手工算的结果对一下。对不上说明四值的口径里有一个跟你想的不一样宁可先在这里花半小时也别等回测跑完才发现整个信号延迟了一天。2.2 Range的计算公式和上轨/下轨的Python表达Range的计算就一行Range max(HH - LC, HC - LL)HH-LC 衡量的是“从昨天开盘到昨天最高点之间的向上空间”HC-LL 衡量的是“从昨天收盘到昨天最低点之间的向下空间”。取两者中较大的那个意思是昨天无论向上还是向下已经证明的振幅有多大今天在这个振幅内的运动都只能算“正常波动”只有超出这个波动范围才值得当作突破来交易。上轨和下轨则是以今天的开盘价为锚上轨 今日开盘价 K1 × Range 下轨 今日开盘价 - K2 × Range用Python表达出来很直接import pandas as pd import numpy as np def calc_dual_thrust_range(daily: pd.DataFrame) - pd.DataFrame: 输入日线DataFrame列: open/high/low/close 返回带 Range/upper/lower 的DataFrame LC 按经典口径取前一日的开盘价 df daily.copy() prev df[[open, high, low, close]].shift(1) hh prev[high] # 前日最高 hc prev[close] # 前日收盘 ll prev[low] # 前日最低 lc prev[open] # 前日开盘注意这里最容易改错 df[range] np.maximum(hh - lc, hc - ll) df[upper] df[open] 0.5 * df[range] df[lower] df[open] - 0.5 * df[range] return df这段代码里shift(1)的作用是把整个日线向后挪一天这样在计算2024年1月5日的上轨时用的就是1月4日的四值。np.maximum对两个Series做逐元素取最大值比手动写循环快了不止一个数量级数据量小的时候你感觉不到一旦扩展到几十个品种的多线程回测差别就出来了。2.3 K1与K2的取值逻辑对称参数与非对称参数的适用场景K1和K2是调整触发距离的旋钮。K1、K2取0.5时上轨等于开盘价加半个Range下轨等于开盘价减半个Range。这是原版比较推荐的起点含义很清楚今天只要走出昨天振幅的一半就认为是一次有效突破。K1和K2不一定要相等。K1放大、K2缩小策略会更偏多头因为做多需要突破更高的上轨说明多头信号更挑剔而空头更容易触发反过来K1缩小、K2放大就是做空更积极。单边强势的品种比如某些时段的中证500股指期货用非对称参数经常比对称参数好看但你要想清楚训练集的行情性质——用去年大牛市调出来的K1、K2放到震荡年多半直接失效。还有一种比较实用的改进是让K1、K2跟随波动率自适应。比如用ATR做参照Range本身已经包含了波动信息再除以近N日平均ATR得到一个波动放大系数把K1、K2乘上去# 用前14日平均真实波幅对K值做温和自适应 daily[tr] np.maximum( daily[high] - daily[low], np.maximum( abs(daily[high] - daily[close].shift(1)), abs(daily[low] - daily[close].shift(1)) ) ) daily[atr14] daily[tr].rolling(14).mean() # 波动放大系数今日Range相对历史ATR的比值限制在0.7~1.3之间 daily[vol_scale] (daily[range] / daily[atr14]).clip(0.7, 1.3) daily[upper] daily[open] 0.5 * daily[range] * daily[vol_scale] daily[lower] daily[open] - 0.5 * daily[range] * daily[vol_scale]注意clip(0.7, 1.3)是给波动系数上的安全锁。没有这个限制某一天因为极端行情把ATR拉得特别低range/atr可能会冲到2以上上下轨被撑得太远当天就再也没有触发机会了。这个坑我在早期的版本里踩过后来所有波动率相关的系数一律加clip。3. 用Python量化交易策略代码复现DualThrust回测最小可运行版本3.1 数据准备拿到一份能复现源码逻辑的K线数据做DualThrust回测数据是第一步。你需要至少四列开盘价、最高价、最低价、收盘价外加时间戳。理论上用日K就能判断“日内突破”因为high upper这件事用日K的最高价就能模拟出来但如果你想看真实的入场时间和滑点最好准备分钟K线。最省事的做法是拿一份1分钟K线按日期聚合成日线再用日线计算Range和上下轨然后把上下轨逐日对回分钟线去判突破。这样既保留了策略的日频决策框架又能在分钟级别还原入场点。我一般建议至少准备300个交易日的数据太短的话Range的中位数都不稳定回测结果主观性太强。一份标准的分钟K线CSV长这样datetime,open,high,low,close,volume 2024-01-03 09:01:00,3385.2,3387.0,3384.6,3386.8,1256 2024-01-03 09:02:00,3386.8,3388.4,3386.2,3387.6,1102加载并按日期切分前要先确认时区。国内期货的夜盘数据在CSV里通常是第二天的日期如果你直接把groupby(df.index.date)拿去做聚合会把夜盘成交混到前一天去第二天的开盘价也会取错。我习惯在数据清洗阶段就把夜盘归属到交易日import pandas as pd df pd.read_csv(rb_main_1min.csv, parse_dates[datetime], index_coldatetime) df df.sort_index() # 把下午15:00之后以及晚上21:00之后的交易归属到下一个自然日 df[trade_date] df.index.date night_mask (df.index.hour 20) | (df.index.hour 23) df.loc[night_mask, trade_date] df.index.normalize() pd.Timedelta(days1) df[trade_date] pd.to_datetime(df[trade_date])这里夜盘归属的处理方法不同数据源差别很大有的直接把夜盘日期写成次日有的用交易日历字段。处理完之后用df.groupby(bydf[trade_date].dt.date)聚合就不会错。3.2 核心策略代码从日线算轨到分钟K线判突破下面的代码是一个可以直接跑通的最小回测框架只做日内单方向、收盘前平仓不做反手。这是最经典的DualThrust落地版能让你先看到策略本身的收益轮廓再考虑加反向触发、加过滤这些改进。import pandas as pd import numpy as np def build_daily_bands(df: pd.DataFrame, k1: float 0.5, k2: float 0.5): 输入带 trade_date 的分钟K线DataFrame 返回日线级别的 upper / lower / range daily df.groupby(trade_date).agg( open(open, first), high(high, max), low(low, min), close(close, last) ).dropna() prev daily[[open, high, low, close]].shift(1) daily[range] np.maximum( prev[high] - prev[open], prev[close] - prev[low] ) daily[upper] daily[open] k1 * daily[range] daily[lower] daily[open] - k2 * daily[range] return daily def backtest_dual_thrust(df: pd.DataFrame, k10.5, k20.5, fee0.0002, exit_time14:55): daily_bands build_daily_bands(df, k1, k2) trades [] for day, day_df in df.groupby(trade_date): if day not in daily_bands.index: continue upper daily_bands.loc[day, upper] lower daily_bands.loc[day, lower] pos 0 # 0 空仓, 1 持多, -1 持空 entry_price 0.0 exit_price 0.0 for ts, row in day_df.iterrows(): if pos 0: # 开仓以上轨/下轨价近似成交 if row[high] upper: pos 1 entry_price upper elif row[low] lower: pos -1 entry_price lower else: # 到指定时间无条件平仓 if ts.hour int(exit_time.split(:)[0]) and \ ts.minute int(exit_time.split(:)[1]): exit_price row[close] pnl (exit_price - entry_price) * pos # 扣除双边手续费按成交金额比例 net pnl - fee * (abs(entry_price) abs(exit_price)) trades.append((day, pos, entry_price, exit_price, net)) pos 0 break # 当天始终没走到平仓时点用最后一根K线收盘价平仓 if pos ! 0: last_row day_df.iloc[-1] exit_price last_row[close] pnl (exit_price - entry_price) * pos net pnl - fee * (abs(entry_price) abs(exit_price)) trades.append((day, pos, entry_price, exit_price, net)) return pd.DataFrame(trades, columns[trade_date, pos, entry, exit, net_pnl])这个版本的几个设计选择值得说明。成交价直接用upper和lower来做近似实际盘口里突破瞬间往往要花更高的价格买入或更低的价格卖出因此后面回测里如果用手续费参数建议把滑点也并进去。平仓逻辑用的ts.hour 14 and ts.minute 55也就是说14:55之后的每一根分钟K线都可能触发平仓哪一根先到就用哪一根的收盘价不会等到14:59最后一根才动。还有一点groupby(trade_date)分组后day_df.iterrows()的循环在数据量大时效率不高但优点是每一步都看得明白方便新手对照逻辑。等策略稳定想提速再考虑用NumPy把上下轨向量化广播到分钟线。3.3 跑一次回测参数怎么调、结果怎么看在主程序里调用上面的函数if __name__ __main__: df pd.read_csv(rb_main_1min.csv, parse_dates[datetime], index_coldatetime) # 先做夜盘归属处理再把结果存回 trade_date 列 # 这里假设已经处理过 trades backtest_dual_thrust( df, k10.5, k20.5, fee0.0004, # 手续费滑点合并按成交额的0.04%约一手螺纹钢两个价位 exit_time14:55 ) if len(trades) 0: print(没有产生任何交易请检查数据日期范围和上下轨数值) return print(总交易次数:, len(trades)) print(胜率:, (trades[net_pnl] 0).mean().round(4)) print(累计盈亏:, round(trades[net_pnl].sum(), 2)) print(最大单笔亏损:, round(trades[net_pnl].min(), 2))跑出来的结果先看两件事一是交易次数如果300天只有十几次交易说明上下轨离现价太远K值偏大如果天天都触发说明K值偏小策略变成了双方向打脸。二是胜率和盈亏比组合DualThrust这类突破策略本身胜率往往不到50%靠的是少数大赚的单子覆盖多数小亏所以别单独追求胜率。fee这个参数务必要按实盘量级去设螺纹钢按手续费加滑点双边万四到万五比较现实。如果把fee设成0回测曲线再漂亮也没有参考价值。4. 改进模型的方向从“裸突破”到带过滤、带止损的完整系统4.1 趋势过滤用简单均线把震荡日的假突破过滤掉DualThrust在单边行情里表现很好但遇到高开低走、来回扫的震荡日经常上午做多下午被打止损。一个非常常见的改进模型是叠加一根中期均线做方向过滤只在收盘价位于20日均线上方时允许开多位于下方时允许开空。过滤的目的是把“逆着大趋势的突破信号”直接砍掉。加过滤的代码不需要大改框架只需要在开仓条件处多一个方向判断# 日线级别算均线并join回信号判断逻辑 daily_bands[ma20] daily_bands[close].rolling(20).mean() daily_bands[trend] np.where(daily_bands[close] daily_bands[ma20], 1, -1) # 在开仓判断里加 # 做多要求 trend 1 # 做空要求 trend -1这类过滤的价值不是提升每次交易的胜率而是降低交易频率、避开最难的震荡行情。代价是趋势刚反转时会错过前面一两次真正的突破信号。我实际测试下来均线过滤在日线级别能过滤掉差不多三分之一的交易但剩下的交易平均盈利会明显抬高。4.2 动态止损用ATR替代固定止损原版DualThrust经常被吐槽的一点是止损逻辑模糊。最简单的落地版是“开多后跌破下轨就平仓”这在空间上等于用整个Range做止损波动大的日子止损太宽波动小的日子止损又太近。改进模型里很常见的一招是引入ATR动态止损。思路是开仓后止损价不放在下轨而是放在入场价减去1.5倍ATR的位置持仓过程中止损价随ATR动态更新。ATR本身就是波动率的度量波动大的时候止损放宽波动小的时候止损收紧比固定一个距离要科学。# 承接前面算好的 atr14 # 开多时 entry_price upper stop_price entry_price - 1.5 * daily_bands.loc[day, atr14] # 持仓循环中如果最低价触及 stop_price 就平仓 stop_price max(stop_price, entry_price - 1.5 * daily_bands.loc[day, atr14])这里有一个细节stop_price max(...)是让止损只往上移动、不许往下移动的“移动止损”逻辑。很多改进模型在这里做得太激进每一根新K线都把止损往上挪结果行情正常回调就把单子扫出去了。DualThrust是高换手策略止损过紧会让交易次数暴增手续费吃掉全部利润。1.5倍ATR只是一个起点螺纹钢这类高波动品种我一般给到2~2.5倍。4.3 出场逻辑改进时间出场 vs 移动止损 vs 反向出场出场方式对整个策略的盈利质量影响极大而且没有标准答案。四种常见做法的取舍关系如下出场方式优点缺点典型适应行情收盘前无条件平仓无隔夜风险、逻辑干净尾盘波动大、被动震荡市、低波固定点数/比例止损风险可控、算得清假突破容易打止损超短线ATR移动止损跟随波动、让利润跑需要调ATR倍数单边趋势反向突破另一条轨顺势反手、仓位连续震荡市来回打脸趋势延续反向突破出场是很多改进模型喜欢加的“进阶功能”持多单时如果价格跌破下轨不仅平多还反手开空。这个逻辑在单边下跌行情里很爽因为等于多空两头吃。但在震荡市里价格可能一天内反复穿越上下轨反手交易一次都没落下手续费直接变成大头。我自己的习惯是改进模型里如果加了反手先做一个“只平仓不反手”的对照实验用来判断反手到底是贡献收益还是制造摩擦成本。4.4 多品种参数共用用一篮子标的平滑单品种回撤DualThrust单品种跑收益曲线很不稳定连着几周震荡能把前两个月的利润全部吐回去。一个低成本的改进思路是不要只盯一个品种把同样的参数跑在一篮子相关性较低的商品期货上。比如螺纹钢、聚丙烯、豆粕彼此之间基本面驱动差异大同一天几乎不可能同时出现相同的突破方向组合后的净值曲线会平滑很多。具体做法是给每个品种分配等额资金先各跑各的日线信号再把当日各品种盈亏累加成组合盈亏。要注意的是不要为每个品种单独寻优参数那样又掉进过拟合陷阱。固定一组K值跑全部品种让组合去分散风险才是更稳的方向。5. DualThrust源码复现避坑指南五个最容易翻车的细节5.1 期指连续合约的换月跳空Range被撑爆现象回测某一天突然出现一个离谱的Range当天开盘后瞬间触发上轨成交后行情却在震荡当天亏掉前面十天的利润。原因是主力合约换月前一天的最高价还是老合约的今日开盘价已经变成新合约的跳空缺口被并进了Range计算。解决对Range加一个相对历史中位数的上限截断是处理跳空最实用的办法比手工修数据要快得多# 用过去20日Range的中位数做参照超过3倍直接压回 median_range daily[range].rolling(20).median() daily[range] daily[range].clip(uppermedian_range * 3)这种做法能在不破坏原策略逻辑的前提下消除极端异常值。真正彻底的做法是用复权连续合约但复权后的价格进入实盘时实际交易的价格又不复权中间又引入新的偏差所以截断法在实战里用得更普遍。5.2 涨跌停板无法成交信号出现不代表订单成交现象回测记录显示某天空头信号在上轨被触发盈利很漂亮但实盘当天标的直接跌停你的空单根本开不出来。原因是触碰涨跌停价时盘口通常没有对手盘。解决在回测里加入涨跌停过滤规则。A股和商品期货的涨跌停幅度是公开的可以先粗略判断再精确过滤limit_up daily[close].shift(1) * (1 0.08) # 按8%涨停粗略估算 daily[limit_up_pct] daily[close] limit_up # 涨停日禁止开空、开多也要谨慎 # 跌停日禁止开多更严谨的做法是从分钟K线里判断那一根价格是否触及涨跌停价并伴随封板。但最小可用版本里用日线级别的封板标志已经能避免大部分回测失真。5.3 手续费滑点设置不当高换手策略的隐形杀手现象回测年化看着有30%实盘跑起来连10%都没有甚至亏损。仔细一查策略平均每天交易1.5次双边手续费加滑点占掉了大部分毛利润。DualThrust是突破策略突破瞬间的滑点往往比正常行情更大因为大家都在抢那一下。解决把所有成本合到一个费率里测试。我用螺纹钢时习惯按手续费万分之二加两跳滑点折算成双边费率约万分之四到万分之五然后专门做一个“高成本敏感性测试”把费率加倍看策略利润剩余情况。如果加倍之后从盈利变成亏损说明这个策略在这个品种上没有成本余量需要继续优化入场过滤而不是怀疑运气。5.4 K线时间对齐开盘价不是你想的那个开盘价现象复现别人PDF里的源码明明公式一样计算出来的上轨却和文章里贴的图对不上。最常见的偏差出在“今日开盘价”上。国内商品期货有夜盘夜盘从晚上21:00开始如果CSV里把夜盘数据记为前一天的日期当天真正的开盘价其实在晚上第二天早上9:00的那根K线不是全天第一根。解决在数据清洗阶段统一交易日归属。把晚上交易的K线归到下一个交易日这样“开盘价”才是交易所定义的那个夜盘开盘价Range和上下轨才能和行情软件对照。这个坑排查起来很花时间因为策略整体还是赚钱的只是细节对不上最容易让人误以为是参数问题然后去调参。5.5 参数过拟合改参数不如改逻辑现象在某个品种上把K1、K2分别调成0.33和0.47回测曲线比其他参数都好看但换到下一段行情就崩了。原因是DualThrust只有两个核心参数很多人会在几百上千个组合里用暴力搜索挑出历史最优组这在样本内当然好看换个时间段就原形毕露。解决调参要有约束。我的习惯是K1和K2只在一组有逻辑依据的区间里选0.3到0.7之间步长0.05选出结果后还要做参数扰动测试——把每个参数上下浮动一档看收益曲线是否连续。如果是孤峰比如K10.45特别好、K10.5就大幅亏损说明策略在这个参数点上不具备稳定性宁可回到原版的对称0.5也别用那个孤峰。逻辑上有依据的稳健参数比历史回测里的最优参数值钱得多。6. 把PDF源码变成你自己的策略三个验证方法与一个检查习惯6.1 读源码PDF的顺序先看数据流再看信号最后看参数拿到一份DualThrust源码PDF我建议按这个顺序读先找四值定义和Range公式确认LC到底是开盘价还是收盘价再找开仓条件里用的是实时价还是K线收盘价然后看平仓规则里有没有反手逻辑最后才看它给的K1、K2默认值和优化建议。前三步决定了策略的逻辑骨架最后一步是可以随意改的皮。很多人一上来就盯着参数反而把最关键的触发条件看漏了。6.2 三个验证方法样本外、参数扰动、模拟盘第一样本外验证。把数据切成三段前70%用来开发和调参中间15%用来做一次验证最后15%留作最终测试。最终测试段只能测一次不要反复回去改参数。第二参数扰动测试。把K1、K2、出场时间、ATR倍数都上下浮动20%观察绩效是否明显恶化。如果波动很大说明策略被参数绑死实盘很难复现。第三模拟盘或paper trading。用实时行情跑至少两周重点验证你的数据清洗、信号计算和日志记录全链路是否正常这一步的作用是发现数据源对齐问题而不是赚钱。6.3 上实盘前的最小改动清单检查项最小要求成本模型手续费按实盘两倍测试仍为正滑点假设每笔至少0.1~0.2个最小变动价位止损兜底行情断线时保留程序内止损逻辑操作日志每笔信号、成交、盈亏都落盘资金占用单品种保证金不超过总资金20%我个人的习惯是任何从PDF里借鉴来的改进模型第一版先砍掉一半“看起来很炫”的附加条件只保留最影响盈亏的那一个改进跑过上面三项验证确认它比原版稳定再逐步加回来。这样即使哪里出了问题你也知道是哪一次改动造成的而不是一整锅浆糊。希望帮到你。本文还有配套的精品资源点击获取
返回列表