最近在看一些经典的技术分析书籍,道氏理论作为技术分析的鼻祖,总是被反复提及。但是说实话,每次看到那些手工画线的图表,我都觉得太主观了。作为一个习惯了用代码解决问题的人,我想试试能不能把道氏理论的核心思想用程序实现出来。
优宽量化包含完整的商品期货量化策略调试环境和完整的数据,就拿来练手。这篇文章记录了我的整个探索过程,包括踩过的坑和一些想法。
在开始编程之前,我想先说说为什么要选择道氏理论作为量化实现的目标。
道氏理论不仅仅是一个交易策略,它是现代技术分析的基石。查尔斯·道(Charles Dow)在19世纪末提出这个理论时,为整个技术分析领域奠定了基础。几乎所有后来的技术分析方法,无论是移动平均线、RSI、MACD,还是各种形态分析,都能从道氏理论中找到影子。
这就像学武功一样,如果你连基本功都没有扎实,再花哨的招式也是花架子。在量化交易中也是如此,如果你对最基础的理论都没有深入理解,那些复杂的机器学习算法和神经网络,很可能就是在拟合历史数据的噪音。
道氏理论最大的价值在于它的简洁性和普适性。它不依赖复杂的数学公式,不需要大量的指标组合,就能抓住市场的本质规律。这种简洁性让它在过去一百多年里依然适用,无论是股票、商品期货,还是现在的数字货币市场。
更重要的是,道氏理论强调的是对市场结构的理解,而不是对价格的预测。这种思维方式对于量化交易来说特别有价值,因为它教会我们关注概率,而不是确定性。
道氏理论其实很简单,就是说市场有三种趋势:
核心思想就是:确定主要趋势后,在次要趋势的回调中找机会入场。
听起来很简单,但用代码实现就发现问题了。什么叫”主要趋势”?什么叫”回调”?这些都需要量化定义。
在开始编程之前,我重新梳理了道氏理论的六个基本原则:
最开始,我天真地认为道氏理论就是简单的趋势跟踪,于是写了一个基于双移动平均线的策略:
ma_short = ta.sma(close, 20)
ma_long = ta.sma(close, 50)
trend = ma_short > ma_long ? 1 : -1
结果可想而知,这个策略在震荡市中被打得体无完肤。我很快意识到,道氏理论绝不是简单的移动平均线穿越。
既然道氏理论强调高点和低点的关系,我开始尝试识别波峰波谷。最初我用了一个非常简单的方法:
// 错误的方法
is_peak = high > high[1] and high > high[-1]
is_valley = low < low[1] and low < low[-1]
这个方法的问题很快就暴露了:每个小的价格波动都会被识别为波峰波谷,产生大量噪音。我意识到需要更强的过滤条件。
我开始使用固定的时间窗口来寻找局部极值:
lookback = 5
is_peak = high == ta.highest(high, lookback * 2 + 1)
is_valley = low == ta.lowest(low, lookback * 2 + 1)
这个方法好了一些,但还是有问题。固定的时间窗口无法适应市场的变化,在快速波动的时候会错过重要的转折点,在缓慢波动的时候又会产生太多信号。
我尝试根据市场的波动性来动态调整时间窗口:
volatility = ta.atr(14)
dynamic_lookback = int(volatility / ta.sma(volatility, 50) * 10)
这个想法不错,但实现起来很复杂,而且效果也不稳定。我开始怀疑是不是方向错了。
经过几次失败后,我决定回到Pine Script的经典函数:ta.pivothigh()
和 ta.pivotlow()
。这两个函数实际上就是在寻找局部极值,但它们的实现更加稳定。
pivotLookback = 10
pivotHighPrice = ta.pivothigh(high, pivotLookback, pivotLookback)
pivotLowPrice = ta.pivotlow(low, pivotLookback, pivotLookback)
这个lookback参数很关键。设置得太小,会找到很多无意义的小波动;设置得太大,又会错过重要的转折点。我试了很多数值,发现10-15之间比较合适。
为了找到最优的lookback参数,我做了大量的回测。在不同的品种上,最优参数差别很大:
我发现,活跃品种需要更短的lookback,不活跃的品种需要更长的lookback。但这又带来了一个新问题:如何让策略自适应不同的品种?
有了关键点,下一步就是判断趋势。我写了个简单的逻辑:
var float lastPivotHigh = na, var float prevPivotHigh = na
var float lastPivotLow = na, var float prevPivotLow = na
if not na(pivotHighPrice)
prevPivotHigh := lastPivotHigh
lastPivotHigh := pivotHighPrice
if not na(pivotLowPrice)
prevPivotLow := lastPivotLow
lastPivotLow := pivotLowPrice
然后判断趋势:
var int trendDirection = 0
var int prevTrendDirection = 0
if not na(lastPivotHigh) and not na(prevPivotHigh) and not na(lastPivotLow) and not na(prevPivotLow)
isUptrend = lastPivotHigh > prevPivotHigh and lastPivotLow > prevPivotLow
isDowntrend = lastPivotHigh < prevPivotHigh and lastPivotLow < prevPivotLow
if isUptrend and trendDirection != 1
prevTrendDirection := trendDirection
trendDirection := 1
else if isDowntrend and trendDirection != -1
prevTrendDirection := trendDirection
trendDirection := -1
这里有个问题,就是需要至少两个高点和两个低点才能判断趋势,所以策略开始时会有一段空白期。但是加入了对趋势变化的记录,这样可以检测到趋势的反转信号。
有了趋势方向,接下来就是找入场点。道氏理论的思路是在主要趋势确立后,等待次要趋势的回调。
我用了一个简单的EMA来识别回调:
pullbackEmaLength = 21
pullbackEma = ta.ema(close, pullbackEmaLength)
bool buyPullback = isUptrendConfirmed and ta.crossunder(low, pullbackEma)
bool sellRally = isDowntrendConfirmed and ta.crossover(high, pullbackEma)
这个想法是:在上升趋势中,如果价格回调到EMA附近,就是买入机会。下降趋势中反之。
为了过滤震荡市的假信号,我加入了ADX指标:
[diPlus, diMinus, adx] = ta.dmi(14, 14)
bool adxTrendOk = adx > adxThreshold
bool goLong = buyPullback and adxTrendOk
bool goShort = sellRally and adxTrendOk
只有当ADX大于阈值时才考虑入场,这样可以确保市场处于趋势状态。
这次改进的重点就是风险管理。原来的策略在平仓方面确实有些问题,这次做了几个重要改进:
原来用low <= stopLossPrice
和high >= stopLossPrice
来判断止损,但这样可能在价格快速变动时错过平仓机会。现在改用收盘价:
// 多头止损
if close <= stopLossPrice
strategy.close("多头", comment="止损")
// 空头止损
if close >= stopLossPrice
strategy.close("空头", comment="止损")
这是一个重要的新功能。当趋势发生反转时,即使没有触及止损或止盈,也要主动平仓:
// 趋势反转信号
bool trendReversalToDown = enableTrendReversal and prevTrendDirection == 1 and trendDirection == -1
bool trendReversalToUp = enableTrendReversal and prevTrendDirection == -1 and trendDirection == 1
// 趋势反转平仓
if strategy.position_size > 0 and trendReversalToDown
strategy.close("多头", comment="趋势反转")
if strategy.position_size < 0 and trendReversalToUp
strategy.close("空头", comment="趋势反转")
当第一个止盈目标达到后,将止损移动到盈亏平衡点,锁定利润:
// 止盈1触发后移动止损
else if not tp1_hit and close >= takeProfitPrice1
strategy.close("多头", comment="止盈1", qty_percent=qtyPercentTP1)
tp1_hit := true
stopLossPrice := entryPrice // 移动止损到入场价
保持原来的分级止盈逻辑,但改进了执行顺序:
if strategy.position_size > 0
if close <= stopLossPrice
strategy.close("多头", comment="止损")
tp1_hit := false
else if trendReversalToDown
strategy.close("多头", comment="趋势反转")
tp1_hit := false
else if not tp1_hit and close >= takeProfitPrice1
strategy.close("多头", comment="止盈1", qty_percent=qtyPercentTP1)
tp1_hit := true
stopLossPrice := entryPrice
else if tp1_hit and close >= takeProfitPrice2
strategy.close("多头", comment="止盈2")
tp1_hit := false
为了更好地观察策略的执行情况,加入了止损止盈线的显示:
// 显示止损止盈线
plot(strategy.position_size != 0 ? stopLossPrice : na, "止损线", color=color.red, linewidth=2)
plot(strategy.position_size != 0 ? takeProfitPrice1 : na, "止盈1", color=color.green, linewidth=1)
plot(strategy.position_size != 0 ? takeProfitPrice2 : na, "止盈2", color=color.green, linewidth=2)
这样在图表上可以清楚地看到当前持仓的风险控制线,有助于理解策略的执行逻辑。
把所有的改进整合起来,就是这个增强版策略:
/*backtest
start: 2024-07-18 09:00:00
end: 2025-07-17 15:00:00
period: 1h
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","balance":10000000}]
args: [["ContractType","cu888",360008]]
*/
//@version=5
strategy("道氏理论期货策略-改进版",
shorttitle="Dow Theory Enhanced",
overlay=true)
// 参数设置
pivotLookback = input.int(10, "关键点回看周期")
pullbackEmaLength = input.int(21, "回调EMA周期")
riskRewardRatio1 = input.float(1.5, "止盈1倍数")
riskRewardRatio2 = input.float(3.0, "止盈2倍数")
qtyPercentTP1 = input.int(50, "止盈1仓位比例")
adxThreshold = input.float(25, "ADX阈值")
enableTrendReversal = input.bool(true, "启用趋势反转平仓")
// 技术指标
pivotHighPrice = ta.pivothigh(high, pivotLookback, pivotLookback)
pivotLowPrice = ta.pivotlow(low, pivotLookback, pivotLookback)
pullbackEma = ta.ema(close, pullbackEmaLength)
[diPlus, diMinus, adx] = ta.dmi(14, 14)
// 趋势识别
var float lastPivotHigh = na, var float prevPivotHigh = na
var float lastPivotLow = na, var float prevPivotLow = na
if not na(pivotHighPrice)
prevPivotHigh := lastPivotHigh
lastPivotHigh := pivotHighPrice
if not na(pivotLowPrice)
prevPivotLow := lastPivotLow
lastPivotLow := pivotLowPrice
var int trendDirection = 0
var int prevTrendDirection = 0
if not na(lastPivotHigh) and not na(prevPivotHigh) and not na(lastPivotLow) and not na(prevPivotLow)
isUptrend = lastPivotHigh > prevPivotHigh and lastPivotLow > prevPivotLow
isDowntrend = lastPivotHigh < prevPivotHigh and lastPivotLow < prevPivotLow
if isUptrend and trendDirection != 1
prevTrendDirection := trendDirection
trendDirection := 1
else if isDowntrend and trendDirection != -1
prevTrendDirection := trendDirection
trendDirection := -1
// 入场条件
bool isUptrendConfirmed = trendDirection == 1
bool isDowntrendConfirmed = trendDirection == -1
bool buyPullback = isUptrendConfirmed and ta.crossunder(low, pullbackEma)
bool sellRally = isDowntrendConfirmed and ta.crossover(high, pullbackEma)
bool adxTrendOk = adx > adxThreshold
bool goLong = buyPullback and adxTrendOk
bool goShort = sellRally and adxTrendOk
// 趋势反转信号
bool trendReversalToDown = enableTrendReversal and prevTrendDirection == 1 and trendDirection == -1
bool trendReversalToUp = enableTrendReversal and prevTrendDirection == -1 and trendDirection == 1
// 风险管理
var float stopLossPrice = na
var float takeProfitPrice1 = na
var float takeProfitPrice2 = na
var bool tp1_hit = false
var float entryPrice = na
// 开仓
if strategy.position_size == 0
tp1_hit := false
if goLong
stopLossPrice := lastPivotLow
entryPrice := close
riskSize = close - stopLossPrice
if riskSize > 0
takeProfitPrice1 := close + (riskSize * riskRewardRatio1)
takeProfitPrice2 := close + (riskSize * riskRewardRatio2)
strategy.entry("多头", strategy.long)
if goShort
stopLossPrice := lastPivotHigh
entryPrice := close
riskSize = stopLossPrice - close
if riskSize > 0
takeProfitPrice1 := close - (riskSize * riskRewardRatio1)
takeProfitPrice2 := close - (riskSize * riskRewardRatio2)
strategy.entry("空头", strategy.short)
// 改进的平仓逻辑
if strategy.position_size > 0 // 多头持仓
// 止损 - 使用close价格而不是low,更及时
if close <= stopLossPrice
strategy.close("多头", comment="止损")
tp1_hit := false
// 趋势反转平仓
else if trendReversalToDown
strategy.close("多头", comment="趋势反转")
tp1_hit := false
// 止盈1
else if not tp1_hit and close >= takeProfitPrice1
strategy.close("多头", comment="止盈1", qty_percent=qtyPercentTP1)
tp1_hit := true
// 移动止损到盈亏平衡点
stopLossPrice := entryPrice
// 止盈2
else if tp1_hit and close >= takeProfitPrice2
strategy.close("多头", comment="止盈2")
tp1_hit := false
if strategy.position_size < 0 // 空头持仓
// 止损 - 使用close价格而不是high,更及时
if close >= stopLossPrice
strategy.close("空头", comment="止损")
tp1_hit := false
// 趋势反转平仓
else if trendReversalToUp
strategy.close("空头", comment="趋势反转")
tp1_hit := false
// 止盈1
else if not tp1_hit and close <= takeProfitPrice1
strategy.close("空头", comment="止盈1", qty_percent=qtyPercentTP1)
tp1_hit := true
// 移动止损到盈亏平衡点
stopLossPrice := entryPrice
// 止盈2
else if tp1_hit and close <= takeProfitPrice2
strategy.close("空头", comment="止盈2")
tp1_hit := false
// 画图
plot(pullbackEma, "回调EMA", color=color.orange)
plotshape(pivotHighPrice, style=shape.xcross, location=location.absolute, color=color.red, size=size.tiny)
plotshape(pivotLowPrice, style=shape.xcross, location=location.absolute, color=color.blue, size=size.tiny)
// 显示止损止盈线
plot(strategy.position_size != 0 ? stopLossPrice : na, "止损线", color=color.red, linewidth=2)
plot(strategy.position_size != 0 ? takeProfitPrice1 : na, "止盈1", color=color.green, linewidth=1)
plot(strategy.position_size != 0 ? takeProfitPrice2 : na, "止盈2", color=color.green, linewidth=2)
黄金期货:趋势性较强,自20240807持有多仓一直至今,显示出完整的趋势捕捉能力。
农产品期货:季节性特征明显,需要调整时间周期参数以适应不同的波动特征
趋势反转开关:在强趋势市场中可以关闭趋势反转平仓,在震荡市中建议开启
ADX阈值:活跃品种可以设置得低一些(20-25),不活跃品种设置得高一些(25-30)
止盈比例:可以根据品种的波动特征调整,高波动品种可以设置更大的止盈倍数
时间周期:根据品种特征,长趋势品种可以设置较大周期,波段品种选择较短周期
虽然这个改进版本在某些品种的回测中表现不错,但必须诚实地指出策略的局限性:
开仓手数较少,较难满足开仓条件:这是道氏理论量化实现的一个重要缺陷。由于策略要求同时满足多个条件(趋势确认、回调到位、ADX过滤等),导致实际开仓机会远比预期的少。在某些时间段内,策略可能几个月都没有一次交易机会。
这对于追求高频交易或稳定现金流的投资者来说是个问题。在资金利用率方面,长期空仓状态显然不够理想。
经典理论适用于实盘,不一定完全准确:道氏理论诞生于一个多世纪前,当时的市场结构、参与者构成、信息传播速度都与现在大不相同。现代市场中的高频交易、程序化交易、各种衍生品工具的存在,都可能让经典理论的适用性打折扣。
特别是在短期时间框架内,市场噪音和随机性往往会掩盖道氏理论所揭示的规律。这就需要我们在应用时保持理性和谨慎。
需要人为的趋势判断:尽管我们努力将道氏理论量化,但在实际应用中,仍然需要大量的人工判断。比如:
这些问题都无法完全通过代码解决,需要交易者具备一定的市场经验和判断能力。对于完全依赖程序化交易的投资者来说,这可能是个挑战。
需要人工挑选时间和品种:正如黄金期货实现了完整的趋势捕捉所示,策略的成功很大程度上依赖于品种的选择和时机的把握。这带来几个问题:
需要实时跟进品种特性设置相关参数:不同品种需要不同的参数设置,这在实际应用中是个重大挑战:
除了技术层面的局限性,还有心理层面的挑战:
面对这些局限性,我建议:
虽然这个改进版本在回测中表现不错,但记住几点:
回测不等于实盘:真实交易中还有滑点、延迟等因素
参数需要定期检查:市场在变化,参数也要相应调整
风险控制永远第一:无论策略多么优秀,都要严格控制仓位和风险
保持学习心态:量化交易是一个不断学习和改进的过程
量化交易从来不是一个完美的解决方案,它只是我们理解和参与市场的一种工具。道氏理论的量化实现让我们对经典理论有了更深的理解,也让我们意识到理论与实践之间的差距。
真正的价值不在于找到一个完美的策略,而在于通过这个过程培养正确的交易思维:关注概率而非确定性,重视风险而非收益,保持谦逊而非自负。
希望这次的探索经验对大家有所帮助。量化交易的魅力就在于可以不断优化和完善,每一次改进都是对市场理解的深化。