重构经典策略(第十五部分):日线突破交易策略
在本系列文章中,我们经常使用机器学习模型来提升交易策略表现。但我们常常忽略一点:这类模型会对现有数据做出大量先验假设。此外,传统统计学习无法帮我们证明,我们想要建模的这些关联关系在真实市场中是否真的存在。
反观人工交易者,在常年与市场打交道的过程中,悟出了一种可以被称为市场逻辑的底层本质。这种认知,也就是我们所说的市场逻辑,只能从实战经验中获得。我们希望能够把人工交易者总结的经验融入数值驱动的交易程序中。早在计算机诞生之前,主观交易者就活跃在金融市场。因此,他们多年总结并奉为圭臬的经验法则里,很可能蕴含着有价值的真知,这些法则值得我们拿来使用。
如前文所述,我们通常借助机器学习算法挖掘规律,用历史数据推演未来。但我们几乎总是默认:这样的关联真实存在,并且能够从数据中学习得到。我们很少花时间先去验证这些关系是否真实成立。而主观交易者为了生存,必须去寻找能够支撑长期交易生涯的可靠市场规律。从这个角度来看,可以说人工交易者完成了前期的人工探索工作,为机器学习模型打下基础。
因此,我们希望搭建起机器学习与金融交易之间的桥梁,利用交易者在多年市场实践中总结的经验与启发式规则。交易者普遍遵循的这些规则,可以作为一套框架,让模型以更结构化的方式学习金融市场。不必让模型从零开始挖掘市场关系;我们可以复用那些经过时间检验有效的原理。这相当于给模型一个良好起点,而不是让它白手起家。
本次测试最重要的环节,就是验证这套基础策略的有效性。为此,我们选取了一套知名的突破策略,核心依据是连续交易日之间的价格关系。这套策略本质上相当简单。
我们的交易策略概述
在每个交易日开始时,我们标记出前一日的最高价与最低价。

图 1:绘制前一日高低价位线,搭建日内突破策略
当价格向上突破前一日高点时,我们做空,止盈位设为前一日低点。反之,当价格向下跌破前一日低点时,我们做多,止盈位设为前一日高点。本质上这是一套逆势交易策略,每日只寻找一次突破交易机会。

图 2:价格首次击穿前一日极值价位,触发开仓信号
有意思的是,即便没有预测模型、也不使用复杂神经网络,对金融市场运行规律的深刻理解本身就是一笔无价的财富。这套日内突破策略的原始版本,尽管看起来朴素,仅依靠主观交易者沉淀下来的市场逻辑,就能取得媲美深度神经网络的胜率。

图 3:价格击穿另一侧极值价位时策略平仓。但从上图可以看出,该策略存在一定误差
我们同时发现,该策略交易风格过于激进,稳定性差。如图 3 所示,策略预设的平仓条件并不总能完全达成。因此必须对策略进行修改,控制其净值波动。这里所说的波动,指回测过程中账户盈利的起伏变化。
在后续章节中我们会看到:首先,我们严格按照大多数交易者熟知的原始描述,实现最简版本的交易策略,以此建立基准绩效。之后对策略进行修改,对比优化前后的差异,确保调整方向符合预期。本次优化从三个核心维度改动策略:
盈利能力原始策略在五年回测周期内仅盈利 40 美元。无论从哪个角度看,这个结果都不算亮眼。但值得关注的是,全部交易中 68% 都是盈利的。对于长达五年的样本外测试而言,这个胜率相当出色,尤其是该策略没有可调参数,完全依靠市场逻辑与交易直觉。遗憾的是,单笔亏损的平均金额大于单笔盈利,最终只得到微薄的 40 美元收益。
经过多轮迭代优化后,同一回测周期内,改进版策略盈利 162 美元,提升幅度达 300%。除此之外,夏普比率由原来的 0.13 提升至 0.40,相比原版提升 207%。读者请注意:以最小手数固定下单,是为了在更苛刻条件下检验该交易程序是否具备真实能力。
交易效率原始策略一共执行 886 笔交易,才赚到 40 美元;而优化版本仅需 246 笔交易,即可盈利 162 美元。这意味着优化后的策略交易频次降低 72%,减少不必要的市场暴露,同时获得更高收益。这是所有交易程序梦寐以求的特性:承担更少风险,获取更多收益。
原始策略单笔平均盈利 3.64 美元,单笔平均亏损 7.87 美元。我们改善了这个问题。优化版本单笔平均盈利 14.82 美元,同时单笔平均亏损基本维持在 7.95 美元。
交易胜率遗憾的是,上述改动提升了盈利能力与交易效率,但对胜率造成负面影响。盈利交易占比从原版的 68% 下降至优化版的 37%,胜率降幅 45%。高胜率原本是原始策略最吸引人的特点之一,想要人工调参,同时不牺牲胜率,难度很大。
因此,我们可以考虑引入反馈控制器来解决这个问题,正如前文所演示(此处附链接)。我们同时认为,继续测试更多久经实战的主观交易规则很有价值,从中筛选天然具备高胜率的策略,就像我们本次的原始策略一样。通过这种方式,这些策略能够为机器学习模型提供一套结构化框架,学习人工交易直觉的优势与局限。下面正式开始。
在 MQL5 中实现
和我们大多数交易程序一样,首先定义策略所需的全局变量。我们需要标识变量,用来跟踪两个关键价位:前一日最高价、前一日最低价。同时要在图表上绘制水平横线标记这两个价位,因此还需要字符串变量存储价位线条的名称。
//+------------------------------------------------------------------+ //| UB 2.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ double last_high,last_low; string h,l; bool rest; int atr_handler; double atr[];
同样,我们引入交易库,用于在回测期间管理持仓。
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
程序首次加载时,我们会为这两个跟踪价位设置名称标识,为风险管理设置平均真实波幅(ATR)指标,最后初始化一个名为 rest 的系统标记。顾名思义,在 rest 标记设为 true 之前,系统会持续寻找交易机会。一旦 rest 为 true,系统就会停止寻找新的开仓机会,转而只管理已开立的持仓。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- h="high"; l="low"; rest = false; atr_handler = iATR(Symbol(),PERIOD_H8,4*14); //--- return(INIT_SUCCEEDED); }
当程序不再使用时,我们需要释放 ATR 指标资源,完成清理,将内存交还给系统其他模块使用。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(atr_handler); }
在每个交易日的起始阶段,我们会保存前一个交易日的最高价与最低价。
//--- datetime cd = iTime(Symbol(),PERIOD_D1,0); static datetime ds; if(cd != ds) { ds = cd; last_high = iHigh(Symbol(),PERIOD_D1,1); last_low = iLow(Symbol(),PERIOD_D1,1); if((rest==true) && (PositionsTotal() == 0)) rest = false; Comment("Last High: ",last_high,"\nLast Low: ",last_low); ObjectDelete(0,l); ObjectDelete(0,h); ObjectCreate(0,h,OBJ_HLINE,0,0,last_high); ObjectCreate(0,l,OBJ_HLINE,0,0,last_low); CopyBuffer(atr_handler,0,0,1,atr); }
随着当日行情推进,当价格首次突破极值价位时,我们开立逆势仓位;我通常认为,第一次突破往往揭示了当日更真实的运行方向。
datetime ch = iTime(Symbol(),PERIOD_H1,0); static datetime hs; if(ch != hs) { hs = ch; double bid,ask,close,padding; close = iClose(Symbol(),PERIOD_H1,0); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); padding = atr[0] * 2; if(rest == false) { if(PositionsTotal() == 0) { if(close<last_low) Trade.Buy(0.01,Symbol(),ask,(bid-(padding)),last_high); else if(close>last_high) Trade.Sell(0.01,Symbol(),bid,(ask+(padding)),last_low); rest = true; } } } } //+------------------------------------------------------------------+
这套策略主要基于交易经验直觉,所以逻辑组件不多。至此,交易程序的基础环境搭建完毕,我们可以开始对这个交易程序进行回测。
我们先选取五年的欧元兑美元(EUR/USD)历史数据,以此观察程序的运行表现。

图 4:使用原始版本交易策略,建立基准绩效水平
随后,我们设置延迟选项,启用随机延迟,用来模拟真实交易中执行延迟的不可预测性。

图 5:启用随机延迟设置,保证模拟环境可靠
绘制账户净值随时间变化曲线后可以看到:这套策略虽然朴素简单,净值整体确实向上。但净值曲线波动的峰谷差距过大,我们需要增加控制手段,降低策略激进程度,让交易行为更可控。

图 6:原始策略生成的净值曲线稳定性不足,波动剧烈
即便如此,原始策略在五年回测中总共只盈利 40 美元。但亮眼之处在于胜率极高:策略所有成交订单里,68% 的交易都是盈利的。可问题在于,平均盈利单笔收益小于平均亏损,亏损幅度几乎是盈利的两倍。

图 7:原始策略详细统计结果,最突出特点就是天然的高盈利订单占比
策略优化改进
因此,想要改进原始策略,我们需要创造性地思考策略分析市场的方式,寻找可以预判行情的方法,而不是等待价格全天运行直至突破目标价位。经过思考,我想到可以在每个交易日的第一个小时,对比当日开盘价与前一日高低极值点之间的价差。
通过这个方法,我们就能得到昨日高低点与新开盘价之间的缺口大小。我们的思路是:价格倾向于朝着缺口更大的一侧运行。规则如下:如果当日开盘价更靠近前一日低点,则做多。反之,如果开盘价更靠近前一日高点,则做空。

图 8:可视化展示我们对原始策略做出的优化方案
引入这项改进后,策略总净利润相比原始的 40 美元有所提升。我们无需等待传统策略的突破条件,就可以预判当日行情方向。只需要小幅修改开仓规则:现在我们要计算当前收盘价(以首小时收盘价近似当日早盘位置)距离前一日高点、低点的绝对价差。如果到高点的价差更大,则开多单;否则开空单。每开一笔仓位后,重置标记参数;只有这笔交易了结之后,程序才会继续寻找新交易机会。
if(rest == false) { if(PositionsTotal() == 0) { //High Gap Is Bigger if(MathAbs(close - last_high) > MathAbs(close - last_low)) Trade.Buy(0.01,Symbol(),ask,(bid-(padding)),last_high); rest = true; //Low Gap Is Bigger else if(MathAbs(close - last_high) < MathAbs(close - last_low)) Trade.Sell(0.01,Symbol(),bid,(ask+(padding)),last_low); rest = true; } }
组合在一起,我们新的程序如下:
//+------------------------------------------------------------------+ //| UB 2.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ double last_high,last_low; string h,l; bool rest; int atr_handler; double atr[]; //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- h="high"; l="low"; rest = false; atr_handler = iATR(Symbol(),PERIOD_H8,4*14); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(atr_handler); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- datetime cd = iTime(Symbol(),PERIOD_D1,0); static datetime ds; if(cd != ds) { ds = cd; last_high = iHigh(Symbol(),PERIOD_D1,1); last_low = iLow(Symbol(),PERIOD_D1,1); if((rest==true) && (PositionsTotal() == 0)) rest = false; Comment("Last High: ",last_high,"\nLast Low: ",last_low); ObjectDelete(0,l); ObjectDelete(0,h); ObjectCreate(0,h,OBJ_HLINE,0,0,last_high); ObjectCreate(0,l,OBJ_HLINE,0,0,last_low); CopyBuffer(atr_handler,0,0,1,atr); } datetime ch = iTime(Symbol(),PERIOD_H1,0); static datetime hs; if(ch != hs) { hs = ch; last_high = iHigh(Symbol(),PERIOD_D1,1); last_low = iLow(Symbol(),PERIOD_D1,1); double bid,ask,close,padding; close = iClose(Symbol(),PERIOD_H1,0); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); padding = atr[0] * 2; if(rest == false) { if(PositionsTotal() == 0) { if(MathAbs(close - last_high) > MathAbs(close - last_low)) Trade.Buy(0.01,Symbol(),ask,(bid-(padding)),last_high); else if(MathAbs(close - last_high) < MathAbs(close - last_low)) Trade.Sell(0.01,Symbol(),bid,(ask+(padding)),last_low); rest = true; } } } } //+------------------------------------------------------------------+
整合全部改动之后,我们的新版程序如下所示。优化后策略生成的净值曲线创出原版从未达到过的新高。总净利润从原先的 40 美元上涨至 148 美元。但我们已经能够观察到,盈利交易的占比开始出现下滑。

图 9:可视化展示原始策略经过优化之后生成的净值曲线
一方面,策略交易效率得到提升,多赚了 108 美元。原版策略需要完成 886 笔交易才能赚到 40 美元,而新版本仅需 469 笔交易就实现 148 美元收益。策略的改进方向是正确的,但仍然还有进一步优化的空间。

图 10:对比原版策略,本次优化效果的统计分析
继续做出更多改进
为继续探究优化思路,请看下面示意图。图中绿色矩形代表向上缺口,蓝色矩形代表向下缺口。我们目前的理论假设是:价格会朝着缺口更大的方向运行。当前策略是,一旦价格击穿前一日的极值价位就执行止盈。

图 11:绿色、蓝色矩形分别代表昨日的高点价差和低点价差
但我们可以看到,价格并不总能完全突破这些极值点位;有时价格接近该位置,却始终无法完成突破。

图 12:原版策略的平仓条件,无法适配所有市场行情
因此我们可以思考:不要把止盈位设置在前一日极值的外侧,而是设置在极值内侧,例如,在前一日高点下方,按一定 ATR 比例设置止盈。这样一来,只要价格足够靠近目标位置,我们依然可以落袋为安。
除此之外,当持仓朝着有利于我们的方向运行时,我们希望把止损向上收紧,尽可能多地锁定利润。为实现该效果,我们修改交易规则:不再给止损和止盈设置相同的缓冲偏移量,止盈使用比止损更小的 ATR 系数。这样,止损可以预留更大容错空间,而止盈设置得更紧凑,以此抓取行情动量。
在持仓存续期间,我们还会做条件判断,检查新的止损建议位置是否比当前止损更有利。理想情况下,只要条件允许,我们希望止损位始终绑定最新的极值价格。
if(ch != hs) { hs = ch; last_high = iHigh(Symbol(),PERIOD_D1,1); last_low = iLow(Symbol(),PERIOD_D1,1); double bid,ask,close,padding; close = iClose(Symbol(),PERIOD_H1,0); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); padding = atr[0] * 2; if(rest == false) { if(PositionsTotal() == 0) { if((MathAbs(close - last_high) > MathAbs(close - last_low))) Trade.Buy(0.01,Symbol(),ask,(bid-(padding)),last_high+(padding*0.9)); else if((MathAbs(close - last_high) < MathAbs(close - last_low))) Trade.Sell(0.01,Symbol(),bid,(ask+(padding)),last_low-(padding*0.9)); rest = true; } } if(rest == true) { if(PositionsTotal() > 0) { if(PositionSelectByTicket(PositionGetTicket(0))) { double sl,tp; sl = PositionGetDouble(POSITION_SL); tp = PositionGetDouble(POSITION_TP); //--- Buy if(sl < tp) { if(last_low > sl) Trade.PositionModify(Symbol(),last_low,tp); } if(tp < sl) { if(last_high < sl) Trade.PositionModify(Symbol(),last_high,tp); } } } } }
把以上所有改动整合起来,就得到了目前为止程序的最终版本:
//+------------------------------------------------------------------+ //| UB 2.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ double last_high,last_low; string h,l; bool rest; int atr_handler; double atr[]; //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- h="high"; l="low"; rest = false; atr_handler = iATR(Symbol(),PERIOD_H8,4*14); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(atr_handler); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- datetime cd = iTime(Symbol(),PERIOD_D1,0); static datetime ds; if(cd != ds) { ds = cd; last_high = iHigh(Symbol(),PERIOD_D1,1); last_low = iLow(Symbol(),PERIOD_D1,1); if((rest==true) && (PositionsTotal() == 0)) rest = false; Comment("Last High: ",last_high,"\nLast Low: ",last_low); ObjectDelete(0,l); ObjectDelete(0,h); ObjectCreate(0,h,OBJ_HLINE,0,0,last_high); ObjectCreate(0,l,OBJ_HLINE,0,0,last_low); CopyBuffer(atr_handler,0,0,1,atr); } datetime ch = iTime(Symbol(),PERIOD_H1,0); static datetime hs; if(ch != hs) { hs = ch; last_high = iHigh(Symbol(),PERIOD_D1,1); last_low = iLow(Symbol(),PERIOD_D1,1); double bid,ask,close,padding; close = iClose(Symbol(),PERIOD_H1,0); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); padding = atr[0] * 2; if(rest == false) { if(PositionsTotal() == 0) { if((MathAbs(close - last_high) > MathAbs(close - last_low))) Trade.Buy(0.01,Symbol(),ask,(bid-(padding)),last_high+(padding*0.9)); else if((MathAbs(close - last_high) < MathAbs(close - last_low))) Trade.Sell(0.01,Symbol(),bid,(ask+(padding)),last_low-(padding*0.9)); rest = true; } } if(rest == true) { if(PositionsTotal() > 0) { if(PositionSelectByTicket(PositionGetTicket(0))) { double sl,tp; sl = PositionGetDouble(POSITION_SL); tp = PositionGetDouble(POSITION_TP); //--- Buy if(sl < tp) { if(last_low > sl) Trade.PositionModify(Symbol(),last_low,tp); } if(tp < sl) { if(last_high < sl) Trade.PositionModify(Symbol(),last_high,tp); } } } } } }
此时的净值曲线相比前两个策略版本,能够攀升至更高位置。盈亏之间的波动幅度,也得到了更好的控制。

图 13:这套交易策略最终版本生成的净值曲线,具备原始版本所没有的理想形态
查看详细统计数据可以发现,总净利润再度提升,从上一版的 148 美元增加到本版本的 162 美元。但盈利交易占比继续下降,同时也低于原始策略。这表明我们的优化调整,不仅影响盈利订单,也会以我们不希望的方式作用于亏损订单。
这凸显出一个更深层次的难题:单纯依靠人工经验直觉,很难持续优化基于主观交易逻辑搭建的策略。想要取得实质性突破,我们必须考虑人工判断之外的其他优化手段。

图 14:仔细分析后我们发现,各项修改同时对系统的交易胜率带来负面影响
结论
人工交易者能在金融市场中长期生存,并非依靠运气,也不是一笔一笔盲目试错。出于生存压力和对亏损的规避本能,他们不得不去寻找真实、可靠的市场规律。如今这一严谨思路常常被忽视:机器学习领域的从业者一味追求更大规模的数据、更复杂的模型,却不去反思各项指标是否真的代表有效的进展。
我们的研究提出另一条思路。将策略建立在主观交易总结的市场逻辑之上,我们从已经验证有效的基础起步,而非建立在盲目的假设之上。这些策略的残差,也就是策略本身产生的误差,会成为机器学习模型绝佳的训练素材。如此一来,我们不再要求算法从零去挖掘市场关联;而是为模型提供一套输入与输出已经建立联系的框架。
这种思路转变,也更贴近模型所依赖的统计假设。固定策略在变化的市场中产生的输出,近似于稳健学习所要求的独立同分布(IID)条件。现实市场永远无法达到完美的独立同分布状态,但该方法能够向这个理想条件靠拢,让模型不再脆弱,可信度更高。我们认为,以人类总结的市场逻辑作为起点,再利用这套逻辑产生的误差来训练模型,可以让机器学习站在更高的起点开展学习。
| 文件名 | 文件详情 |
|---|---|
| Daily Breakout Strategy V1.mq5 | 策略最初的激进版本,拥有很高的初始交易胜率。 |
| Daily Breakout Strategy V2.mq5 | 交易程序的第二版,胜率略有下降,但盈利能力大幅提升。 |
| Daily Breakout Strategy V3.mq5 | 策略最终版本,实现最高收益,但交易胜率最低。 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/19130
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
梦境优化算法(DOA)
决斗者算法
神经网络在交易中的应用:概率时间序列预测(K²VAE)
MQL5自优化智能交易系统(第十二部分):利用矩阵分解构建线性分类器