
经典策略重塑(第12部分):欧元兑美元(EURUSD)突破交易策略
在本文中,我们将一起在MQL5中构建一套交易策略。我们要实现一个突破交易策略,并通过迭代改进,逐步释放其全部潜能。下面先来讨论一下该策略的一些具体逻辑。
我们将专注于欧元兑美元货币对,关注在H1周期上交易产生的波动。突破策略的第一步,是先记录当前欧元兑美元的“最高价”和“最低价”。随着时间推移,我们等待价格完全走出初始记录(包括开盘和收盘)的这一通道之外。
当这种情况出现时,策略会判定市场存在方向性倾向:价格大概率会继续朝突破方向移动。但此刻我们并不会立即入场。只有在“倾向被确认”后,我们才会开仓。一旦价格完全在突破初始通道的K线极值点之外开盘并收盘,若价格位于通道上方,我们将做多;否则,将做空。
如果仅靠上述逻辑,系统会过度交易。因此需引入额外的强弱过滤指标,剔除潜在亏损信号。移动平均线(MA)可快速识别趋势方向。
我们将设计系统首先监测当前所交易市场的实时价格,随后观察价格突破通道的方向,以及该突破是否得到后续价格走势的验证。若观察到的突破与突破后的价格走势一致,我们将利用移动平均线来择机执行订单。
当快速移动平均线位于慢速移动平均线上方时,我们倾向于做多;相反则做空。所有交易将通过平均真实波幅(ATR)指标动态更新止损和止盈设置。
我们将在H1时间框架下,对2020年1月1日至2024年11月30日期间的交易策略进行测试。
技术指标设置如下:
- 快速移动平均线:5周期指数移动平均线(EMA),应用于收盘价。
- 慢速移动平均线:60周期指数移动平均线(EMA),应用于收盘价。
- 平均真实波幅(ATR):14周期ATR指标。
图例1:突破交易应用的初始状态。
一段时间后,价格水平将最终在通道外开盘并收盘。这一极值点将构成我们的方向性倾向,即我们预期市场将延续的方向。若价格后续收盘价低于该倾向点(做多时)或高于该倾向点(做空时),则倾向得以确认;否则,我们将不执行任何交易。
图例2:交易应用已识别出市场的方向性倾向。
若价格走势确认我们的倾向,则我们有信心在市场中开仓。我们的策略初始为趋势跟踪。若价格在突破通道上方,我们将寻找做多机会。
图例3:方向性倾向确认后开仓。
MQL5入门指南
本交易应用基于交易逻辑与基础技术分析概念构建。以下重点说明代码中的核心模块。
系统模块 | 设计目的 |
---|---|
常量与参数 | 为确保所有测试的一致性,我们将固定交易算法的某些参数,例如:移动平均线的周期、交易手数和止损与止盈的宽度。 |
全局变量 | 这些变量在代码不同部分被调用,需确保每次引用时指向同一数值。应用的部分全局变量包括:通道的上下轨、市场方向性倾向和其他技术指标数值。 |
我们还需要在交易应用中定义其他关键变量,以跟踪市场状态。下面介绍重要变量及其用途。
变量 | 设计目的 |
---|---|
方向性倾向 | 方向性倾向参数用于标识价格当前移动的方向,当趋势为看涨时,取值为 1;当趋势为看跌时,取值为 -1。否则设为0(无明确方向)。 |
移动平均线 | 快速移动平均线(ma_f)与慢速移动平均线(ma_s)用于判断趋势方向。如果ma_f[0] > ma_s[0] 且当前价格(c)位于快速平均线上方,则触发做多信号。如果ma_f[0] < ma_s[0] 且当前价格位于慢速平均线下方,则触发做空信号。 |
突破 | 当价格突破通道边界(上轨或下轨)时,确定市场移动方向(即方向性倾向)。 |
突破水平 | 突破水平指示我们预期市场未来延续的方向。如果价格突破上轨,则市场情绪转为看涨。 |
信号确认 | 我们所有的交易需经信号确认后方可执行。如果突破后市场保持原有方向,则信号有效。如果确认失效,可调整仓位或对现有持仓平仓。 |
订单管理 | 交易方向取决于当前市场的方向性倾向。看涨趋势(bias = 1)时,发送指令:Trade.Buy(vol, Symbol(), ask, channel_low, 0, "Volatility Doctor AI");看跌趋势(bias = -1)时,发送指令:Trade.Sell(vol, Symbol(), bid, channel_high, 0, "Volatility Doctor AI")。 |
止损 | 初始止损位设置:做多时设为通道下轨(channel_low);做空时设为通道上轨(channel_high);后续根据平均真实波幅(ATR)动态调整。 |
至此,策略各模块的概念框架已清晰。接下来让我们开始构建这套交易策略。首先,必须明确交易应用的各项细节。
//+------------------------------------------------------------------+ //| MTF Channel 2.mq5 | //| Gamuchirai Zororo Ndawana | //| https://www.mql5.com/en/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com/en/gamuchiraindawa" #property version "1.00"
现在加载交易库。
//+------------------------------------------------------------------+ //| Library | //+------------------------------------------------------------------+ #include <Trade/Trade.mqh> CTrade Trade;
为我们的交易应用程序定义常量,例如部分技术指标的周期。
//+------------------------------------------------------------------+ //| Constants | //+------------------------------------------------------------------+ const int ma_f_period = 5; //Slow MA const int ma_s_period = 60; //Slow MA
接下来,我们为终端用户定义可调整的输入参数。由于已经将技术指标固定,终端用户无需面对大量参数。
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "Money Management" input int lot_multiple = 5; //Lot Multiple input int atr_multiple = 5; //ATR Multiple
我们将在几乎整个程序中使用的全局变量。
//+------------------------------------------------------------------+ //| Global varaibles | //+------------------------------------------------------------------+ double channel_high = 0; double channel_low = 0; double o,h,l,c; int bias = 0; double bias_level = 0; int confirmation = 0; double vol,bid,ask,initial_sl; int atr_handler,ma_fast,ma_slow; double atr[],ma_f[],ma_s[]; double bo_h,bo_l;
当交易程序首次加载时,我们将调用一个专用函数来加载技术指标,并准备所需的其他市场数据。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- setup(); //--- return(INIT_SUCCEEDED); }
当不再使用EA时,我们应该释放所有不再需要的资源。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(atr_handler); IndicatorRelease(ma_fast); IndicatorRelease(ma_slow); }
每当收到最新报价时,我们先更新全局变量,再检查是否存在新的交易机会。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- If we have positions open if(PositionsTotal() > 0) manage_setup(); //--- Keep track of time static datetime timestamp; datetime time = iTime(Symbol(),PERIOD_CURRENT,0); if(timestamp != time) { //--- Time Stamp timestamp = time; if(PositionsTotal() == 0) find_setup(); } }
下列函数将负责加载技术指标并获取市场数据。
//+---------------------------------------------------------------+ //| Load our technical indicators and market data | //+---------------------------------------------------------------+ void setup(void) { channel_high = iHigh(Symbol(),PERIOD_M30,1); channel_low = iLow(Symbol(),PERIOD_M30,1); vol = lot_multiple * SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN); ObjectCreate(0,"Channel High",OBJ_HLINE,0,0,channel_high); ObjectCreate(0,"Channel Low",OBJ_HLINE,0,0,channel_low); atr_handler = iATR(Symbol(),PERIOD_CURRENT,14); ma_fast = iMA(Symbol(),PERIOD_CURRENT,ma_f_period,0,MODE_EMA,PRICE_CLOSE); ma_slow = iMA(Symbol(),PERIOD_CURRENT,ma_s_period,0,MODE_EMA,PRICE_CLOSE); }
当首次加载策略时,我们会记录当前市场的最高价与最低价。这样一来,后续观察到的任何价格都能与初始价位进行对比,从而具备参考基准。
//+---------------------------------------------------------------+ //| Update channel | //+---------------------------------------------------------------+ void update_channel(double new_high, double new_low) { channel_high = new_high; channel_low = new_low; ObjectDelete(0,"Channel High"); ObjectDelete(0,"Channel Low"); ObjectCreate(0,"Channel High",OBJ_HLINE,0,0,channel_high); ObjectCreate(0,"Channel Low",OBJ_HLINE,0,0,channel_low); }
如果存在未平仓头寸,我们就需根据当前情况更新止损与止盈值。具体做法:以平均真实波幅(ATR)的倍数来动态调整风险参数,使止损/止盈始终贴合市场最新波动水平。
//+---------------------------------------------------------------+ //| Manage setup | //+---------------------------------------------------------------+ void manage_setup(void) { bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); CopyBuffer(atr_handler,0,0,1,atr); Print("Managing Position"); if(PositionSelect(Symbol())) { Print("Position Found"); initial_sl = PositionGetDouble(POSITION_SL); } if(bias == 1) { Print("Position Buy"); double new_sl = (ask - (atr[0] * atr_multiple)); Print("Initial: ",initial_sl,"\nNew: ",new_sl); if(initial_sl < new_sl) { Trade.PositionModify(Symbol(),new_sl,0); Print("DONE"); } } if(bias == -1) { Print("Position Sell"); double new_sl = (bid + (atr[0] * atr_multiple)); Print("Initial: ",initial_sl,"\nNew: ",new_sl); if(initial_sl > new_sl) { Trade.PositionModify(Symbol(),new_sl,0); Print("DONE"); } } }
如果我们没有持仓,则按先前规则寻找交易机会。先确认强劲的价格走势突破初始通道。随后,如果价格继续同向运行,且不再回到刚形成的突破通道内,便足以触发我们下单。
//+---------------------------------------------------------------+ //| Find Setup | //+---------------------------------------------------------------+ void find_setup(void) { //--- We are updating the system o = iOpen(Symbol(),PERIOD_CURRENT,1); h = iHigh(Symbol(),PERIOD_CURRENT,1); l = iLow(Symbol(),PERIOD_CURRENT,1); c = iClose(Symbol(),PERIOD_CURRENT,1); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); CopyBuffer(atr_handler,0,0,1,atr); CopyBuffer(ma_fast,0,0,1,ma_f); CopyBuffer(ma_slow,0,0,1,ma_s); //--- If we have no market bias if(bias == 0) { //--- Our bias is bullish if ( (o > channel_high) && (h > channel_high) && (l > channel_high) && (c > channel_high) ) { bias = 1; bias_level = h; bo_h = h; bo_l = l; mark_bias(h); } //--- Our bias is bearish if ( (o < channel_low) && (h < channel_low) && (l < channel_low) && (c < channel_low) ) { bias = -1; bias_level = l; bo_h = h; bo_l = l; mark_bias(l); } } //--- Is our bias valid? if(bias != 0) { //--- Our bearish bias has been violated if ( (o > channel_high) && (h > channel_high) && (l > channel_high) && (c > channel_high) && (bias == -1) ) { forget_bias(); } //--- Our bullish bias has been violated if ( (o < channel_low) && (h < channel_low) && (l < channel_low) && (c < channel_low) && (bias == 1) ) { forget_bias(); } //--- Our bullish bias has been violated if ( ((o < channel_high) && (c > channel_low)) ) { forget_bias(); } //--- Check if we have confirmation if((confirmation == 0) && (bias != 0)) { //--- Check if we are above the bias level if ( (o > bias_level) && (h > bias_level) && (l > bias_level) && (c > bias_level) && (bias == 1) ) { confirmation = 1; } //--- Check if we are below the bias level if ( (o < bias_level) && (h < bias_level) && (l < bias_level) && (c < bias_level) && (bias == -1) ) { confirmation = 1; } } } //--- Check if our confirmation is still valid if(confirmation == 1) { //--- Our bias is bullish if(bias == 1) { //--- Confirmation is lost if we fall beneath the breakout level if ( (o < bias_level) && (h < bias_level) && (l < bias_level) && (c < bias_level) ) { confirmation = 0; } } //--- Our bias is bearish if(bias == -1) { //--- Confirmation is lost if we rise above the breakout level if ( (o > bias_level) && (h > bias_level) && (l > bias_level) && (c > bias_level) ) { confirmation = 0; } } } //--- Do we have a setup? if((confirmation == 1) && (bias == 1)) { if(ma_f[0] > ma_s[0]) { if(c > ma_f[0]) { Trade.Buy(vol,Symbol(),ask,channel_low,0,"Volatility Doctor AI"); initial_sl = channel_low; } } } if((confirmation == 1) && (bias == -1)) { if(ma_f[0] < ma_s[0]) { if(c < ma_s[0]) { Trade.Sell(vol,Symbol(),bid,channel_high,0,"Volatility Doctor AI"); initial_sl = channel_high; } } } Comment("O: ",o,"\nH: ",h,"\nL: ",l,"\nC:",c,"\nC H: ",channel_high,"\nC L:",channel_low,"\nBias: ",bias,"\nBias Level: ",bias_level,"\nConfirmation: ",confirmation,"\nMA F: ",ma_f[0],"\nMA S: ",ma_s[0]); }
当价格突破初始通道后,我们将标记那根突破K线创造的极值价位。该极值即为我们判定方向的方向性倾向水平。
//+---------------------------------------------------------------+ //| Mark our bias levels | //+---------------------------------------------------------------+ void mark_bias(double f_level) { ObjectCreate(0,"Bias",OBJ_HLINE,0,0,f_level);the }
最后,若价格在突破通道后再次回落到原通道之内,我们将判定旧通道失效,并把新通道位置更新为突破K线所形成的高低区间。
//+---------------------------------------------------------------+ //| Forget our bias levels | //+---------------------------------------------------------------+ void forget_bias() { update_channel(bo_h,bo_l); bias = 0; bias_level = 0; confirmation = 0; ObjectDelete(0,"Bias"); } //+------------------------------------------------------------------+
我们现已准备好对该突破交易策略进行回测。我将此EA命名为 “MTF Channel 2”,代表多周期通道。我选择了欧元兑美元测作为测试交易品种,H1时间框架。测试时间与之前指定的日期一致。请注意,这三项设置在全部3批测试中均保持不变。
图例4:我们首次回测所使用的初始设置。
这些并非全部参数。我们还启用了“随机延迟”,以模拟真实交易中可能出现的不同网络延迟。并选择“基于真实 tick”的建模方式,力求获得最接近真实交易的回测体验。
图例5:用于测试策略的第二批设置。
我们将把EA的所有系统设置固定不变,以确保在所有测试中保持一致。通过保持相同的设置,我们能够单独评估“更优交易规则”本身带来的盈利效果。
图例6:我们的资金管理设置。
让我们看一下策略的实际运行效果。在下方的图例7 中,截图右侧显示了应用程序内部用于决策的变量。请注意,只有当确认标识(confirmation)设为1时,系统才会真正下单交易。
图例7:欧元兑美元货币对交易策略的回溯测试。
遗憾的是,我们可以看到该策略一直在亏损。这表明策略还有改进的空间。
图例8:查看与回溯测试相关的图表。
让我们更详细地了解一下我们刚刚进行的测试。我们可以清楚地看到,我们的策略总共识别出53笔交易,其中70%的交易是亏损的。我们的夏普比率(Sharpe ratio)为负。这些业绩指标表现欠佳。
另一方面,我们的平均盈利大于平均亏损,这是个好现象。让我们看看如何能表现得更好。我们希望对总亏损和平均亏损进行更严格的控制,同时最大化平均盈利和盈利交易的比例。
图例9:回溯测试的详细信息。
改进首次测试结果
在观察回溯测试时,看到EA反复犯同样的错误,着实令人沮丧。我们的大部分亏损都是因为在价格的无意义波动中进行了交易,而这些波动恰好满足了所有的条件。唯一的解决办法是选择更好的条件,这些条件能够自然地区分市场中的弱势和强势波动。
我们有一个选择,就是将欧元和美元的表现与一个共同的基准进行比较。我们可以使用英镑(GBP)作为这个基准。在决定开仓之前,我们会比较欧元兑英镑(EURGBP)和英镑兑美元(GBPUSD)这两个货币对的表现。也就是说,如果我们在图表上观察到欧元兑美元处于强劲的上涨趋势中,我们也希望看到欧元兑英镑同样处于上涨趋势,并且希望英镑兑美元也处于上涨趋势。
换句话说,如果欧元兑美元的价格水平让我们觉得欧元相对于美元正在升值,那么只有当我们同时观察到欧元相对于英镑也在升值,而美元相对于英镑同时在贬值时,我们才会更有信心。这种三方三角汇率关系,有望帮助我们识别虚假突破。我们的推理是,同时影响所有三个市场的波动,可能是真正强劲的波动,我们可能从中获利。
我们将添加几行代码,对目前构建的原始交易策略进行修改。为了实现我们所设想的变化,我们将首先创建新的全局变量,以跟踪欧元兑英镑和英镑兑美元的价格。我们还需要对另外两个市场应用技术指标,以便跟踪这些市场中各自的走势。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ double channel_high = 0; double channel_low = 0; double o,h,l,c; int bias = 0; double bias_level = 0; int confirmation = 0; double vol,bid,ask,initial_sl; int atr_handler,ma_fast,ma_slow; double atr[],ma_f[],ma_s[]; double bo_h,bo_l; int last_trade_state,current_state; int eurgbp_willr, gbpusd_willr; string symbols[] = {"EURGBP","GBPUSD"};
当EA首次加载时,我们需要执行一些额外的步骤,以跟踪我们用作基准的货币对的价格走势。这些更新将在初始化(setup)函数中实现。
//+---------------------------------------------------------------+ //| Load our technical indicators and market data | //+---------------------------------------------------------------+ void setup(void) { //--- Select the symbols we need SymbolSelect("EURGBP",true); SymbolSelect("GBPUSD",true); //--- Reset our last trade state last_trade_state = 0; //--- Mark the current high and low channel_high = iHigh("EURUSD",PERIOD_M30,1); channel_low = iLow("EURUSD",PERIOD_M30,1); ObjectCreate(0,"Channel High",OBJ_HLINE,0,0,channel_high); ObjectCreate(0,"Channel Low",OBJ_HLINE,0,0,channel_low); //--- Our trading volums vol = lot_multiple * SymbolInfoDouble("EURUSD",SYMBOL_VOLUME_MIN); //--- Our technical indicators atr_handler = iATR("EURUSD",PERIOD_CURRENT,14); eurgbp_willr = iWPR(symbols[0],PERIOD_CURRENT,wpr_period); gbpusd_willr = iWPR(symbols[1],PERIOD_CURRENT,wpr_period); ma_fast = iMA("EURUSD",PERIOD_CURRENT,ma_f_period,0,MODE_EMA,PRICE_CLOSE); ma_slow = iMA("EURUSD",PERIOD_CURRENT,ma_s_period,0,MODE_EMA,PRICE_CLOSE); }
同样地,当交易应用程序不再使用时,我们还需要释放一些额外的技术指标资源。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(eurgbp_willr); IndicatorRelease(gbpusd_willr); IndicatorRelease(atr_handler); IndicatorRelease(ma_fast); IndicatorRelease(ma_slow); }
我们的OnTick函数将保持不变。不过,它所调用的函数将会发生改变。首先,每当更新通道时,我们必须在跟踪的三个市场中同时更新通道。一个在欧元兑美元市场,第二个在欧元兑英镑市场,最后一个在英镑兑美元市场。
//+---------------------------------------------------------------+ //| Update channel | //+---------------------------------------------------------------+ void update_channel(double new_high, double new_low) { channel_high = new_high; channel_low = new_low; ObjectDelete(0,"Channel High"); ObjectDelete(0,"Channel Low"); ObjectCreate(0,"Channel High",OBJ_HLINE,0,0,channel_high); ObjectCreate(0,"Channel Low",OBJ_HLINE,0,0,channel_low); }
程序的大部分代码保持不变,我们做出的最重大改变是,现在要求交易应用程序在决定开仓之前,需检查另外两个市场的走势。基本面的分析如果让我们确信,在欧元兑美元上观察到的突破可能确实有强劲势头支撑,那么我们就会开仓。这些更新将体现在“查找交易设置”(find setup)函数中。
您还会注意到,该函数调用了一个新函数,而这个函数在之前的突破策略应用程序版本中并未定义。这个额外的确认函数将检查两个基准市场是否符合我们的基本面交易条件。
//+---------------------------------------------------------------+ //| Find Setup | //+---------------------------------------------------------------+ void find_setup(void) { //--- I have omitted code pieces that were unchanged //--- Do we have a setup? if((confirmation == 1) && (bias == 1) && (current_state != last_trade_state)) { if(ma_f[0] > ma_s[0]) { if(c > ma_f[0]) { if(additional_confirmation(1)) { Trade.Buy(vol,"EURUSD",ask,channel_low,0,"Volatility Doctor"); initial_sl = channel_low; last_trade_state = 1; } } } } if((confirmation == 1) && (bias == -1) && (current_state != last_trade_state)) { if(ma_f[0] < ma_s[0]) { if(c < ma_s[0]) { if(additional_confirmation(-1)) { Trade.Sell(vol,"EURUSD",bid,channel_high,0,"Volatility Doctor"); initial_sl = channel_high; last_trade_state = -1; } } } } }
该函数应该能帮助我们区分市场噪音和真正的强势信号。通过在其他相关市场中寻求确认,我们希望能始终挑选出最具潜力的强势交易机会。
//+---------------------------------------------------------------+ //| Check for true strength | //+---------------------------------------------------------------+ bool additional_confirmation(int flag) { //--- Do we have additional confirmation from our benchmark pairs? //--- Record the average change in the EURGBP and GBPUSD Market vector eurgbp_willr_f = vector::Zeros(1); vector gbpusd_willr_f = vector::Zeros(1); eurgbp_willr_f.CopyIndicatorBuffer(eurgbp_willr,0,0,1); gbpusd_willr_f.CopyIndicatorBuffer(gbpusd_willr,0,0,1); if((flag == 1) && (eurgbp_willr_f[0] > -50) && (gbpusd_willr_f[0] < -50)) return(true); if((flag == -1) && (eurgbp_willr_f[0] < -50) && (gbpusd_willr_f[0] > -50)) return(true); Print("EURGBP WPR: ",eurgbp_willr_f[0],"\nGBPUSD WPR: ",gbpusd_willr_f[0]); return(false); }
我们这个版本的应用程序将命名为“多时间框架(MTF)欧元兑美元通道策略”。我们最初创建的版本更具通用性,可以轻松用于交易终端中的任何其他品种。然而,此版本将使用欧元兑英镑和英镑兑美元货币对作为基准,因此它更为专业化,仅旨在交易欧元兑美元货币对。大家会注意到,我们的测试条件与第一次测试完全相同。我们将使用与第一次测试相同的时间框架和时间段来进行这次回溯测试,即从2020年1月1日到2024年11月30日。
图例10:欧元兑美元通道突破策略回溯测试的首批参数设置。
如果您打算按照我此处演示的步骤进行设置,请注意,如果将建模选项设置为“基于真实报价的每tick建模”,则可能会因您的网络连接状况而耗费较长时间,因为MT5终端需要从您的经纪商处请求大量数据,以尽可能真实地模拟市场。因此,如果该过程需要几分钟才能完成,请不必惊慌,且切勿在过程中关闭计算机。
图例11:我们需要确保第二批参数设置与首次测试中使用的设置完全一致。
使用1的手数倍数意味着我的所有交易都将以最小手数进行。如果我们的系统能在最小手数下实现盈利,那么增加手数倍数将对我们大有裨益。然而,如果我们的系统在最小手数下都无法盈利,那么增加手数规模将毫无意义。
图例12:我们将用于控制应用程序行为的参数。
现在,我们可以看一下交易系统在历史数据上的表现如何。请注意,此版本的系统会同时监控三个市场。首先,我们会始终跟踪欧元兑美元货币对,以便从中获取交易方向偏好。
图例13:我们的系统在欧元兑美元货币对上的运行情况。
只有当观察到英镑兑美元和欧元兑英镑货币对呈现如图14和图15所示的相反趋势时,我们才会开仓。我们将使用威廉指标(WPR)来判断这两个市场的趋势。如果WPR指标高于50水平,我们认为趋势为多头(看涨)。
图例14:我们的首个确认货币对——英镑兑美元
在此情况下,我们发现了一个买入欧元兑美元的交易机会。我们之所以能识别出这一机会,是因为这两个市场的威廉指标(WPR)读数分别位于50水平的两侧。这种不平衡状态,很可能会引发市场波动,对于任何突破策略而言都是理想的市场环境。
图例15:我们的第二个基准货币对。
下图9展示了模拟交易账户余额随时间的变化情况。我们的目标是深入了解策略失败的原因,以便尝试改进其薄弱环节。
图例16:绘制账户余额随时间变化的曲线图。
遗憾的是,我们对系统所做的更改降低了交易应用程序的盈利能力。我们的平均亏损和盈利金额均以相同幅度增加。而盈利交易的比例则略有下降。
图例17:回溯测试的详细结果。
最终改进尝试
我们未能在最关键的盈利能力方面取得改善。与其强行将我们的观点强加给市场,不如让计算机学会比我们更有效地运用移动平均线。我们对于如何有效交易的观点在一定程度上存在偏见。
另一方面,如果我们允许计算机学习收盘价与移动平均线之间的关系,那么计算机就能制定自己的交易规则,并根据其对未来走势的预期进行交易,而非像我们迄今为止所采用的那样被动反应式交易。
开始前,我编写了一个脚本,帮助我们提取历史市场数据。只需将脚本拖放到我们想要交易的市场上即可开始。该脚本将为您获取市场数据,同时还会以我们的交易应用程序中使用的相同格式,获取策略所需的两条移动平均线数据。
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" #property script_show_inputs //+------------------------------------------------------------------+ //| Script Inputs | //+------------------------------------------------------------------+ input int size = 100000; //How much data should we fetch? //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_f_handler,ma_s_handler; double ma_f_reading[],ma_s_reading[]; //+------------------------------------------------------------------+ //| On start function | //+------------------------------------------------------------------+ void OnStart() { //--- Load indicator ma_s_handler = iMA(Symbol(),PERIOD_CURRENT,60,0,MODE_EMA,PRICE_CLOSE); ma_f_handler = iMA(Symbol(),PERIOD_CURRENT,5,0,MODE_EMA,PRICE_CLOSE); //--- Load the indicator values CopyBuffer(ma_f_handler,0,0,size,ma_f_reading); CopyBuffer(ma_s_handler,0,0,size,ma_s_reading); ArraySetAsSeries(ma_f_reading,true); ArraySetAsSeries(ma_s_reading,true); //--- File name string file_name = "Market Data " + Symbol() +" MA Cross" + " As Series.csv"; //--- Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i= size;i>=0;i--) { if(i == size) { FileWrite(file_handle,"Time","Open","High","Low","Close","MA 5","MA 60"); } else { FileWrite(file_handle,iTime(Symbol(),PERIOD_CURRENT,i), iOpen(Symbol(),PERIOD_CURRENT,i), iHigh(Symbol(),PERIOD_CURRENT,i), iLow(Symbol(),PERIOD_CURRENT,i), iClose(Symbol(),PERIOD_CURRENT,i), ma_f_reading[i], ma_s_reading[i] ); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+
使用Python分析数据
既然已经将市场数据以CSV格式准备好,我们现在就可以开始构建一个人工智能(AI)模型,希望该模型能够帮助我们预测并避开虚假突破。
import pandas as pd import numpy as np from sklearn.model_selection import TimeSeriesSplit,cross_val_score from sklearn.linear_model import Ridge from sklearn.metrics import mean_squared_error import matplotlib.pyplot as plt import seaborn as sns
读取我们之前提取的市场数据。请注意我数据框中的“时间”(Time)列,您会发现我记录的最后一条数据日期是2019年4月18日。这是有意为之的。回想一下,之前两次测试的起始日期都是2020年1月1日。这意味着我们并没有通过给模型提供测试的全部答案来欺骗自己。
#Define the forecast horizon look_ahead = 24 #Read in the data data = pd.read_csv('Market Data EURUSD MA Cross As Series.csv') #Drop the last 4 years data = data.iloc[:(-24 * 365 * 4),:] data.reset_index(drop=True,inplace=True) #Label the data data['Target'] = data['Close'].shift(-look_ahead) data['MA 5 Target'] = data['MA 5'].shift(-look_ahead) data['MA 5 Close Target'] = data['Target'] - data['MA 5 Target'] data['MA 60 Target'] = data['MA 60'].shift(-look_ahead) data['MA 60 Close Target'] = data['Target'] - data['MA 60 Target'] data.dropna(inplace=True) data.reset_index(drop=True,inplace=True) data
图例18:我们的历史市场数据。
让我们来测试一下,在欧元兑美元市场中,移动平均线是否仍然比价格本身更容易预测。为了验证我们的假设,我们将训练30个相同的神经网络,逐一预测三个目标。首先,我们将预测未来价格、5周期移动平均线和60周期移动平均线。所有目标都将预测未来24个时间步长。首先,我们将直接记录预测价格的准确率。
#Classical error classical_error = [] epochs = 1000 for i in np.arange(0,30): model = MLPRegressor(hidden_layer_sizes=(10,4),max_iter=epochs,early_stopping=False,solver='lbfgs') classical_error.append(np.mean(np.abs(cross_val_score(model,data.loc[:,['Open','High','Low','Close']],data.loc[:,'Target'],cv=tscv,scoring='neg_mean_squared_error'))))
接下来,我们将记录预测5个周期移动平均线的准确率。
#MA Cross Over error ma_5_error = [] for i in np.arange(0,30): model = MLPRegressor(hidden_layer_sizes=(10,4),max_iter=epochs,early_stopping=False,solver='lbfgs') ma_5_error.append(np.mean(np.abs(cross_val_score(model,data.loc[:,['Open','High','Low','Close','MA 5']],data.loc[:,'MA 5 Target'],cv=tscv,scoring='neg_mean_squared_error'))))
最后,我们将记录预测60个周期移动平均线的准确率。
#New error ma_60_error = [] for i in np.arange(0,30): model = MLPRegressor(hidden_layer_sizes=(10,4),max_iter=10000,early_stopping=False,solver='lbfgs') ma_60_error.append(np.mean(np.abs(cross_val_score(model,data.loc[:,['Open','High','Low','Close','MA 60']],data.loc[:,'MA 60 Target'],cv=tscv,scoring='neg_mean_squared_error'))))
当我们绘制出结果,从图12可以看出,预测60个周期移动平均线在我们系统中产生的误差最大,而预测5个周期移动平均线产生的误差比直接预测价格要小。
plt.plot(classical_error) plt.plot(ma_5_error) plt.plot(ma_60_error) plt.legend(['OHLC','MA 5 ','MA 60']) plt.axhline(np.mean(classical_error),color='blue',linestyle='--') plt.axhline(np.mean(ma_5_error),color='orange',linestyle='--') plt.axhline(np.mean(ma_60_error),color='green',linestyle='--') plt.grid() plt.ylabel('Cross Validated Error') plt.xlabel('Iteration') plt.title('Comparing Different The Error Associated With Different Targets') plt.show()
图例19:可视化不同目标预测所关联的误差。
现在,让我们尝试导出一个模型,以供我们的交易应用程序使用。导入我们需要的库。
import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType from sklearn.neural_network import MLPRegressor
明确我们所需的模型。针对此任务,我将使用两种模型。由于短期移动平均线较易预测,我将采用简单的岭回归(Ridge)模型进行预测。然而,我们的60个周期移动平均线预测起来颇具挑战性。因此,我将使用神经网络来预测长期移动平均线。
ma_5_model = Ridge() ma_5_model.fit(data[['Open','High','Low','Close','MA 5']],data['MA 5 Target']) ma_5_height_model = Ridge() ma_5_height_model.fit(data[['Open','High','Low','Close','MA 5']],data['MA 5 Close Target']) ma_60_model = Ridge() ma_60_model.fit(data[['Open','High','Low','Close','MA 60']],data['MA 60 Target']) ma_60_height_model = Ridge() ma_60_height_model.fit(data[['Open','High','Low','Close','MA 60']],data['MA 60 Close Target'])
准备导出为ONNX格式。
initial_type = [('float_input', FloatTensorType([1, 5]))] ma_5_onx = convert_sklearn(ma_5_model, initial_types=initial_type, target_opset=12 ) ma_5_height_onx = convert_sklearn(ma_5_height_model, initial_types=initial_type, target_opset=12 ) ma_60_height_onx = convert_sklearn(ma_60_height_model, initial_types=initial_type, target_opset=12 ) ma_60_onx = convert_sklearn(ma_60_model, initial_types=initial_type, target_opset=12 )
保存为ONNX格式。
onnx.save(ma_5_onx,'eurchf_ma_5_model.onnx') onnx.save(ma_60_onx,'eurchf_ma_60_model.onnx') onnx.save(ma_5_height_onx,'eurusd_ma_5_height_model.onnx') onnx.save(ma_60_height_onx,'eurusd_ma_60_height_model.onnx')
MQL5中的最终更新
让我们应用新模型,看看它们能否帮助我们过滤掉市场中的虚假突破信号。我们需要进行的第一个更新是导入刚刚创建的ONNX模型。
//+------------------------------------------------------------------+ //| MTF Channel 2.mq5 | //| Gamuchirai Zororo Ndawana | //| https://www.mql5.com/en/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Zororo Ndawana" #property link "https://www.mql5.com/en/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| ONNX Resources | //+------------------------------------------------------------------+ #resource "\\Files\\eurusd_ma_5_model.onnx" as const uchar eurusd_ma_5_buffer[]; #resource "\\Files\\eurusd_ma_60_model.onnx" as const uchar eurusd_ma_60_buffer[]; #resource "\\Files\\eurusd_ma_5_height_model.onnx" as const uchar eurusd_ma_5_height_buffer[]; #resource "\\Files\\eurusd_ma_60_height_model.onnx" as const uchar eurusd_ma_60_height_buffer[];
接下来,我们需要创建一些与模型相关的新变量。
//+------------------------------------------------------------------+ //| Global varaibles | //+------------------------------------------------------------------+ int bias = 0; int state = 0; int confirmation = 0; int last_cross_over_state = 0; int atr_handler,ma_fast,ma_slow; int last_trade_state,current_state; long ma_5_model; long ma_60_model; long ma_5_height_model; long ma_60_height_model; double channel_high = 0; double channel_low = 0; double o,h,l,c; double bias_level = 0; double vol,bid,ask,initial_sl; double atr[],ma_f[],ma_s[]; double bo_h,bo_l; vectorf ma_5_forecast = vectorf::Zeros(1); vectorf ma_60_forecast = vectorf::Zeros(1); vectorf ma_5_height_forecast = vectorf::Zeros(1); vectorf ma_60_height_forecast = vectorf::Zeros(1);
我们必须扩展初始化程序,使其现在能够设置好ONNX模型。
//+---------------------------------------------------------------+ //| Load our technical indicators and market data | //+---------------------------------------------------------------+ void setup(void) { //--- Reset our last trade state last_trade_state = 0; //--- Mark the current high and low channel_high = iHigh("EURUSD",PERIOD_M30,1); channel_low = iLow("EURUSD",PERIOD_M30,1); ObjectCreate(0,"Channel High",OBJ_HLINE,0,0,channel_high); ObjectCreate(0,"Channel Low",OBJ_HLINE,0,0,channel_low); //--- Our trading volums vol = lot_multiple * SymbolInfoDouble("EURUSD",SYMBOL_VOLUME_MIN); //--- Our technical indicators atr_handler = iATR("EURUSD",PERIOD_CURRENT,14); ma_fast = iMA("EURUSD",PERIOD_CURRENT,ma_f_period,0,MODE_EMA,PRICE_CLOSE); ma_slow = iMA("EURUSD",PERIOD_CURRENT,ma_s_period,0,MODE_EMA,PRICE_CLOSE); //--- Setup our ONNX models //--- Define our ONNX model ulong input_shape [] = {1,5}; ulong output_shape [] = {1,1}; //--- Create the model ma_5_model = OnnxCreateFromBuffer(eurusd_ma_5_buffer,ONNX_DEFAULT); ma_60_model = OnnxCreateFromBuffer(eurusd_ma_60_buffer,ONNX_DEFAULT); ma_5_height_model = OnnxCreateFromBuffer(eurusd_ma_5_height_buffer,ONNX_DEFAULT); ma_60_height_model = OnnxCreateFromBuffer(eurusd_ma_60_height_buffer,ONNX_DEFAULT); //--- Store our models in a list long onnx_models[] = {ma_5_model,ma_5_height_model,ma_60_model,ma_60_height_model}; //--- Loop over the models and set them up for(int i = 0; i < 4; i++) { if(onnx_models[i] == INVALID_HANDLE) { Comment("Failed to load AI module correctly: Invalid handle"); } //--- Validate I/O if(!OnnxSetInputShape(onnx_models[i],0,input_shape)) { Comment("Failed to set input shape correctly: Wrong input shape ",GetLastError()," Actual shape: ",OnnxGetInputCount(ma_5_model)); } if(!OnnxSetOutputShape(onnx_models[i],0,output_shape)) { Comment("Failed to load AI module correctly: Wrong output shape ",GetLastError()," Actual shape: ",OnnxGetOutputCount(ma_5_model)); } } }
如果系统不再使用,我们应该释放不再需要的资源。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Free the resources we don't need IndicatorRelease(atr_handler); IndicatorRelease(ma_fast); IndicatorRelease(ma_slow); OnnxRelease(ma_5_model); OnnxRelease(ma_5_height_model); OnnxRelease(ma_60_model); OnnxRelease(ma_60_height_model); }
当我们接收到更新后的价格时,与之前相比此处唯一的重大区别在于,我们还将尝试从AI模型中获取预测结果。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Keep track of time static datetime timestamp; datetime time = iTime(Symbol(),PERIOD_CURRENT,0); if(timestamp != time) { //--- Time Stamp timestamp = time; //--- Update system variables update(); //--- Make a new prediction model_predict(); if(PositionsTotal() == 0) { state = 0; find_setup(); } } //--- If we have positions open if(PositionsTotal() > 0) manage_setup(); }
我们需要在MQL5中定义一个函数,该函数负责从ONNX模型中获取预测结果。
//+------------------------------------------------------------------+ //| Get a prediction from our model | //+------------------------------------------------------------------+ void model_predict(void) { //--- Moving average inputs float a = (float) ma_f[0]; float b = (float) ma_s[0]; //--- Price quotes float op = (float) iOpen("EURUSD",PERIOD_H1,0); float hi = (float) iHigh("EURUSD",PERIOD_H1,0); float lo = (float) iLow("EURUSD",PERIOD_H1,0); float cl = (float) iClose("EURUSD",PERIOD_H1,0); //--- ONNX inputs vectorf fast_inputs = {op,hi,lo,cl,a}; vectorf slow_inputs = {op,hi,lo,cl,b}; Print("Fast inputs: ",fast_inputs); Print("Slow inputs: ",slow_inputs); //--- Inference OnnxRun(ma_5_model,ONNX_DATA_TYPE_FLOAT,fast_inputs,ma_5_forecast); OnnxRun(ma_5_height_model,ONNX_DATA_TYPE_FLOAT,fast_inputs,ma_5_height_forecast); OnnxRun(ma_60_model,ONNX_DEFAULT,slow_inputs,ma_60_forecast); OnnxRun(ma_60_height_model,ONNX_DATA_TYPE_FLOAT,fast_inputs,ma_60_height_forecast); }
我们最后所做的更改会影响策略选择交易的方式。现在,我们的策略不会再一味地盲目入场,而是会根据其所学到的价格与移动平均线之间的关系来执行交易。我们的交易应用程序现在具有灵活性,即使交易方向与我们认为的市场趋势相反,它也可以进行买卖操作。
请注意,这里调用了一个新函数“valid setup”,该函数仅在突破条件成立时返回“true”。
//+---------------------------------------------------------------+ //| Find a setup | //+---------------------------------------------------------------+ void find_setup(void) { //--- I have skipped parts of the code that remained the same if(valid_setup()) { //--- Both models are forecasting rising prices if((c < (ma_60_forecast[0] + ma_60_height_forecast[0])) && (c < (ma_5_forecast[0] + ma_5_height_forecast[0]))) { if(last_trade_state != 1) { Trade.Buy(vol,"EURUSD",ask,0,0,"Volatility Doctor"); initial_sl = channel_low; last_trade_state = 1; last_cross_over_state = current_state; } } //--- Both models are forecasting falling prices if((c > (ma_60_forecast[0] + ma_60_height_forecast[0])) && (c > (ma_5_forecast[0] + ma_5_height_forecast[0]))) { if(last_trade_state != -1) { Trade.Sell(vol,"EURUSD",bid,0,0,"Volatility Doctor"); initial_sl = channel_high; last_trade_state = -1; last_cross_over_state = current_state; } } }
检查我们是否已突破通道。如果已突破,该函数将返回“true”,否则返回“false”。
//+---------------------------------------------------------------+ //| Do we have a valid setup? | //+---------------------------------------------------------------+ bool valid_setup(void) { return(((confirmation == 1) && (bias == -1) && (current_state != last_cross_over_state)) || ((confirmation == 1) && (bias == 1) && (current_state != last_cross_over_state))); }
我相信到现在为止,您应该已经对我们将在回测中指定的设置有所了解。请记住,保持这些设置的一致性非常重要,这样我们才能将盈利能力的变化与我们对交易规则所做的修改关联起来,从而进行准确分析。
图例20:我们将用于对最新交易策略进行回测的部分设置。
请回想一下,我们的模型仅训练至2019年,但是测试是从2020年开始的。因此,我们正在紧密模拟如果我们过去设计了这个系统,实际会发生的情况。
图例21:我们将用于对最新交易策略进行回测的第二批设置。
同样地,我们在所有三次测试中使用的设置都是相同的。
图例22:我们在最后一次测试中用于控制应用程序的设置。
现在,我们可以看到基于模型的突破交易应用程序在欧元兑美元货币对上的实际运行情况。请记住,在训练模型时,这些数据均未展示给模型。
图例23:我们基于模型的突破策略最终版本的实际运行情况。
从下图23中可见,我们最终成功纠正了模型自始至终存在的负收益斜率特征,现在策略开始实现盈利增长。
图例24:我们基于新模型的策略进行回测的结果。
我们的目标是提高平均利润,同时降低亏损交易的比例,当前已经做到了这一点。在首次测试中,我们的总亏损为498美元,第二次测试中为403美元,而如今已降至298美元。与此同时,首次测试的总利润为378美元,而在本次最终测试中则为341美元。显然,我们所做的修改在保持总利润几乎不变的同时,降低了总亏损。在最初的系统中,我们70%的交易都是亏损的。然而,在新系统中,只有55%的交易是亏损的。
图例25:我们基于模型的策略的详细回测结果。
结论
突破行情可能是一天中最优的交易时机。然而,要正确识别突破行情,其挑战性不容小觑。在本文中,我们携手构建了属于自己的突破交易策略。为了提升策略的盈利能力,我们为其增添了更多筛选条件。或许,突破策略并不完全适用于欧元兑美元市场,我们可能需要换个角度来审视这个市场。然而,成功构建一个突破交易策略所需的时间和精力远超本文所分享的内容,但我们在文中提出的思路,或许值得您在追求成功的道路上加以考量。
文件名 | 描述 |
---|---|
MQL5 欧元兑美元 AI相关内容 | 用于构建欧元兑美元市场模型的Jupyter笔记本。 |
欧元兑美元60个周期移动平均线模型 | 用于预测60个周期移动平均线的ONNX模型。 |
欧元兑美元60个周期移动平均线差值模型 | 用于预测未来收盘价与未来60个周期移动平均线之间差值的ONNX模型。 |
欧元兑美元5个周期移动平均线模型 | 旨在预测5个周期移动平均线的ONNX模型。 |
欧元兑美元5个周期移动平均线差值模型 | 用于预测未来收盘价与未来5个周期移动平均线之间差值的ONNX模型。 |
多时间框架通道策略2 | 突破策略的首次实现。 |
多时间框架通道策略2应用于欧元兑美元 | 突破策略的第二次实现,实现过程使用了基准货币对的确认信号。 |
基于AI的多时间框架通道策略2应用于欧元兑美元 | 突破策略的第三次实现,实现过程基于模型构建。 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16569
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。



您好,Aliasandr,这个问题很好,因为您是对的!最好先进行优化,然后根据历史数据选择最佳周期。不过,这本身就是一个不同的问题,值得关注细节。
我们已经介绍了如何使用威廉百分比范围来使用机器学习进行期数选择,我们还介绍了如何使用流形学习一次性使用所有期数,这两篇文章各有不同。