DualThrust策略深度解析:开盘区间突破的数学原理与Python实现
2026/9/17 18:35:59 网站建设 项目流程

简介:这份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, k1=0.5, k2=0.5, lots=1): 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, None

on_bar接收当前分钟K线的四个价格以及当天日线计算出的两个 Range。注意触发判断使用high >= open_ + buy_triggerlow <= 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,观察收益衰减斜率,衰减不平滑说明策略本身并不依赖精确价位,适合实盘执行。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询