
简介这份PDF系统梳理了量化经典DualThrust交易策略的原始实现与改进模型适合量化交易初学者和实盘策略开发者阅读。文档不仅解释了买入/卖出触发点计算、K1/K2参数、Mday/Nday窗口等核心逻辑还贴出可借鉴的代码片段展示改进版如何引入offset滑点处理、PlotNumeric信号绘制以及更灵活的时间周期适配。同时结合多头、空头与无持仓三种状态说明开仓、平仓及反手的具体条件对趋势市场下的自动化交易有直接参考价值。资源为单个PDF文档压缩包大小34KB内容紧凑便于通读。已有125人学习下载对于想快速理解DualThrust并做策略落地的人来说是一份高性价比的参考笔记。1. DualThrust 不是又一个突破策略而是把“波动范围”拆成了可调的两套尺子DualThrust 是量化交易圈流传最久的开盘区间突破策略之一和 Aberration、海龟、菲阿里四价并称经典。它的核心并不是简单画出高低点而是用两个不同天数的窗口分别计算“做多方”和“做空方”的波动幅度再通过 K1/K2 调整出两个不对称的触发距离。你不需要预测行情方向只要价格从当日开盘价跑出足够远系统就顺着突破方向进场。它对单边趋势日非常敏感对低波动横盘日则天然过滤。这个改进模型的代表性在于原始代码把区间计算放在当前K线上改进版引入日线数据引用、滑点偏移和可视化输出让策略在 5 分钟级别也能稳定落地。如果你正在写自己的 CTA 框架、做日内期货或 ETF 突破回测这篇文章会把 DualThrust 的数学核心拆开再给出一份可直接迁移到 Python 的版本最后聊几个不调参也能提升稳健性的细节。2. 区间计算的数学核心与原始 EasyLanguage 源码逐行拆解2.1 HH、LL、HC、LC 分别代表哪四个极端值DualThrust 的所有公式都建立在四个变量上窗口期内的最高价的最高值 HH、最低价的最低值 LL、收盘价的最高值 HC、收盘价的最低值 LC。窗口由参数 Mday 和 Nday 指定分别用于计算卖出区间和买入区间。变量计算逻辑作用HHHighest(High, Mday)Mday 周期内最高价的极大值反映多头极端LLLowest(Low, Mday)Mday 周期内最低价的极小值反映空头极端HCHighest(Close, Mday)Mday 周期内收盘价的极大值反映收在何处的多头力量LCLowest(Close, Mday)Mday 周期内收盘价的极小值反映收在何处的空头力量这四个值合起来描述了一个窗口内的“价格包络带”。原始代码用Highest(High, Mday)注意它取的是 High 序列在 Mday 根K线窗口内的最大值而不是单根K线的最高价。收盘价的最高最低同样重要因为收盘价往往代表多空结算位置比瞬时高低点更有连续性。2.2 BuyRange 与 SellRange 为什么都取大区间先看原始代码的核心段落If (HH - LC) (HC - LL) Then SellRange HH - LC Else SellRange HC - LL End这段在 Mday 窗口内计算卖出范围等价于SellRange Max(HH - LC, HC - LL)。为什么不是直接取 HH - LL因为 HH - LL 包含一根K线上的极端振幅容易被长上影或长下影污染。而 HH - LC 表示“最高价的极端值”与“收盘价的最低值”之间的跨度中间已经过滤掉了同根K线的影线交叉噪音HC - LL 同理。取这两者中的较大值等于把“最高点到最低收盘”和“最高收盘到最低点”两条跨度的并集范围作为波动包络。后面 Nday 窗口的代码完全一样只是赋值给了BuyRange。命名上容易误会SellRange对应的是卖出触发点的距离BuyRange对应买入触发点的距离并不是由持仓方向决定的。2.3 触发信号与反手逻辑实战中的触发条件在代码里是这样写的BuyTrig K1 * BuyRange SellTrig K2 * SellRange If MarketPosition 0 Then Begin Buy at Open of next bar BuyTrig Stop Sell at Open of next bar - SellTrig Stop End If MarketPosition -1 Then Begin Buy at Open of next bar BuyTrig Stop End If MarketPosition 1 Then Begin Sell at Open of next bar - SellTrig Stop End这里的执行锚点是下一根K线的开盘价。比如现价位于开盘价下方当下一根K线开盘后如果价格向上穿越开盘价 BuyTrig就以 Stop 单价格买入向下跌破开盘价 - SellTrig则以 Stop 单价格卖出。无持仓时同时埋多空两个方向价格先触碰哪个就成交哪个。持有空头时只挂买入停止单一旦上破买入触发线就平空反多持有多头时只挂卖出停止单下破卖出触发线就平多反空。这里有个容易被忽略的点因为使用了下一根K线开盘价原始策略实际上在每根K线结束时才检查并挂单天然带一根K线的滞后。对日线或小时线无影响对分钟周期就会明显吃延迟。2.4 关键参数表与默认值参数默认值含义影响K10.5买入触发距离系数越大买入触发越远交易越少K20.5卖出触发距离系数越大卖出触发越远做空越保守Mday1卖出范围计算窗口控制卖出区间的参考天数Nday1买入范围计算窗口控制买入区间的参考天数lots1每次下单手数仓位管理offset0滑点偏移实际发单价格偏离触发价的数量默认值取 0.5 意味着触发点位于开盘价与包络线上轨的正中间。把 K1、K2 同步调大到 0.8 就会过滤掉更多小突破但也会错过不少启动行情。Mday 和 Nday 如果设成 2 或 3波动包络会叠加更多历史信息更能适应慢速行情但信号也会明显变钝。3. 改进模型关键差异日线数据引用、当日开盘锚点与滑点偏移3.1 从当前BAR计算改为引用HighD(1)的跨周期数据改进模型的代码片段这样写If(CurrentBar 44 * Max(Mday, Nday)) Then Begin i_offset offset * MinMove * PriceScale; HH Highest(HighD(1), Mday); HC Highest(CloseD(1), Mday); LL Lowest(LowD(1), Mday); LC Lowest(CloseD(1), Mday); If((HH - LC) (HC - LL)) Then SellRange HH - LC Else SellRange HC - LL; HH Highest(HighD(1), Nday); HC Highest(CloseD(1), Nday); LL Lowest(LowD(1), Nday); LC Lowest(CloseD(1), Nday); If((HH - LC) (HC - LL)) Then BuyRange HH - LC Else BuyRange HC - LL; BuyTrig K1 * BuyRange; SellTrig K2 * SellRange; BuyPosition OpenD(0) BuyTrig; SellPosition OpenD(0) - SellTrig; PlotNumeric(BuyPosition, BuyPosition); PlotNumeric(SellPosition, SellPosition); If(MarketPosition 1) Then Begin If(Low SellPosition) Then SellShort(lots, Min(Open, SellPosition) - i_offset); Return; End End这段代码基于 MultiCharts / TradeStation 系列语法。关键变化是使用了HighD(1)、CloseD(1)、OpenD(0)这类日线跨周期函数。HighD(1)取的是昨日完整日线的最高价而不是当前5分钟K线上滑动的最高值。这样无论你在5分钟、15分钟还是小时线上运行区间来源都统一到日线级别避免了“在1分钟K线上用过去1天分钟数据计算”带来的大量重复计算和信号不稳定问题。CurrentBar 44 * Max(Mday, Nday)这个条件很多人看不懂。它是在保证当前已经积累了至少Max(Mday, Nday)个完整交易日的日线数据。44 是按国内商品期货每天大约44根5分钟K线估算的如果你切换到1分钟周期这个数要改成约 240。改进模型把这层保护写出来是为了避免建仓不足时Highest函数取到空数据。3.2 为什么触发锚点从“下一根开盘价”改成“当日开盘价”原始代码用Open of next bar也就是等当前K线收盘后用下一根K线的开盘价作为计算基准。这在分钟周期上有两个问题一是信号滞后一根K线实盘遇到秒级行情时成交价和理论价差很远二是如果当前K线是日内最后一根下一根K线可能已经是第二天信号逻辑会跨日漂移。改进后的模型直接用OpenD(0)表示“今天这根日K的开盘价”也就是交易日开盘价。在日内交易中开盘价是市场多空共识的基准点用它加减触发距离天然适合捕捉日内趋势爆发。价格从开盘价向上跑出 BuyTrig 就做多向下跑出 SellTrig 就做空不受K线切换影响。代码中PlotNumeric把 BuyPosition、SellPosition 绘制在图表上可以直观看到两条价格轨道方便人工核对触发点是否合理。3.3 offset 滑点偏移的计算与下单保护i_offset offset * MinMove * PriceScale;MinMove是最小变动单位比如某期货合约是 0.2 个指数点PriceScale是价格缩放比例常见为1。二者相乘得到“一跳”的价格。offset通常设为 1 或 2表示发单价格比理论触发价保守一跳到两跳。代码中的SellShort(lots, Min(Open, SellPosition) - i_offset)对多头平仓反手场景做了保护。当Low SellPosition时用Min(Open, SellPosition)作为参考价再减去偏移。这避免开盘价已经大幅跳空低于触发线时仍然用理论 SellPosition 挂单导致无法成交。实际做空限价单应该贴着更有利的方向挂所以取开盘价和触发价中更保守的一个。这个细节是原始策略没有的也是改进模型中最重要的实盘适配点。4. 用 Python 重写 DualThrust从公式到可回测的日内引擎4.1 用 Pandas 计算日线级别的 BuyRange 和 SellRange在 Python 里复刻 DualThrust最稳妥的方式是先用日线数据计算好每日的 BuyRange 和 SellRange再合并到分钟K线上。核心就是昨天之前的日线数据不能混入当天未收盘数据。import pandas as pd import numpy as np def dualthrust_daily_ranges(daily: pd.DataFrame, Mday: int 1, Nday: int 1): df daily.copy() # 计算 HH, HC, LL, LC都基于截至昨日的数据 df[HH] df[High].rolling(Mday).max().shift(1) df[HC] df[Close].rolling(Mday).max().shift(1) df[LL] df[Low].rolling(Mday).min().shift(1) df[LC] df[Close].rolling(Mday).min().shift(1) candle_a df[HH] - df[LC] candle_b df[HC] - df[LL] df[SellRange] np.maximum(candle_a, candle_b) df[HH2] df[High].rolling(Nday).max().shift(1) df[HC2] df[Close].rolling(Nday).max().shift(1) df[LL2] df[Low].rolling(Nday).min().shift(1) df[LC2] df[Close].rolling(Nday).min().shift(1) df[BuyRange] np.maximum(df[HH2] - df[LC2], df[HC2] - df[LL2]) return df[[SellRange, BuyRange]]代码里最关键的是shift(1)。它把所有窗口计算后移一天模拟的是改进模型中HighD(1)的效果。如果没有 shift当天的rolling(Mday).max()会包含当天尚未走完的日线数据在盘中实时计算时就会出现未来函数。这里candle_a对应HH - LCcandle_b对应HC - LL取较大值就是 DualThrust 的 Range 公式。4.2 日内信号状态机实现把日线计算的 Range 合并到分钟K线后需要一套状态机来判定触发和平仓。以下是一个最小实现class DualThrustIntraday: def __init__(self, k10.5, k20.5, lots1): self.k1 k1 self.k2 k2 self.lots lots self.position 0 # 0 空仓, 1 多, -1 空 def on_bar(self, open_, high, low, close, buy_range, sell_range): buy_trigger self.k1 * buy_range sell_trigger self.k2 * sell_range if self.position 0: if high open_ buy_trigger: self.position 1 entry open_ buy_trigger return BUY, entry if low open_ - sell_trigger: self.position -1 entry open_ - sell_trigger return SELL, entry elif self.position 1: if low open_ - sell_trigger: # 多头平仓或反手这里先平多 self.position 0 return SELL_TO_CLOSE, open_ - sell_trigger elif self.position -1: if high open_ buy_trigger: self.position 0 return BUY_TO_CLOSE, open_ buy_trigger return None, Noneon_bar接收当前分钟K线的四个价格以及当天日线计算出的两个 Range。注意触发判断使用high open_ buy_trigger和low open_ - sell_trigger对应 EasyLanguage 中的 Stop 单触发条件。实际回测如果使用K线收盘价判断会漏掉很多盘中瞬时突破正确做法是用high/low作为是否触达的判定再用当时的触发价作为成交假设。上面的状态机是你最基础的原型。改进版中多头持仓时价格下破 SellPosition 是平多并反手做空这里统一平仓更安全实盘时再按自己的双向逻辑补齐。4.3 参数扫描与稳健性检查回测时不能一次性只测一组 K1、K2至少要做一个二维参数网格。最小示例如下results [] for k1 in [0.3, 0.4, 0.5, 0.6, 0.7]: for k2 in [0.3, 0.4, 0.5, 0.6, 0.7]: sharpe run_backtest(k1, k2) results.append((k1, k2, sharpe))把run_backtest替换成自己的回测循环统计每日收益、最大回撤和夏普。值得注意的不是找到夏普最高的那组参数而是看高收益参数周围是否形成连续区域。如果只有孤立的 (0.3, 0.7) 表现特别好而相邻参数全部亏损这个点大概率是过拟合出来的实盘不值得信任。5. 最后的细节用动态 K 值跟波动率绑定而不是盯着回测曲线调参DualThrust 最容易被新手拿来当参数挖掘机但真正决定生命周期的是 K1/K2 是否和市场波动率保持匹配。一个长期有效的做法是把 K1、K2 写成前一日真实波幅的函数atr_prev daily[ATR].shift(1) daily_range_prev daily[High].shift(1) - daily[Low].shift(1) # 波动越小K值越小触发越灵敏波动越大K值越大避免频繁误杀 k1_dynamic np.clip(0.5 * atr_prev / daily_range_prev, 0.3, 0.9) k2_dynamic np.clip(0.5 * atr_prev / daily_range_prev, 0.3, 0.9)这里的逻辑是如果昨天振幅很小说明市场刚进入收缩状态特色突破日往往出现在振幅收窄到极致之后。这个时候把 K 值下调能让触发距离更近不错过启动点。反过来如果昨天振幅剧烈放大K 值跟着放大过滤掉噪音突破。动态 K 值之外还需要验证触发逻辑的稳定性。建议在回测框架中做一次“滞一日触发测试”把每日的 BuyPosition、SellPosition 计算整体延后一天看策略是否仍然盈利。如果收益大幅依赖“当天开盘后立刻计算并触发”的假设那说明代码里可能混入了日内未收盘数据这是 DualThrust 改进模型中最容易踩的坑。另一个实用技巧是把 offset 从 0 扫到 4观察收益衰减斜率衰减不平滑说明策略本身并不依赖精确价位适合实盘执行。本文还有配套的精品资源点击获取