事后交易分析:在策略测试器中选择尾随停止和新的止损位
内容
概述
在上一篇文章中,我们创建了一个 EA,基于真实账户的交易结果在客户端的策略测试器中复现交易。我们添加了设置新的止损和止盈价位的功能,以便我们在测试器中测试采用不同止损大小的交易。结果出乎意料:我们扭亏为盈没,获得了与实际交易中的亏损相称的盈利。这意味着,如果我们在交易账户中使用止损和止盈价位,令我们在测试器中获利,那么在真实账户上也应当可盈利。这只是止损价位的简单更改。我想知道如果我们添加尾随停止会发生什么?事情将会如何变化?
今天,我们将若干不同的尾随停止连接到 EA,并在策略测试器中测试我们的交易。我们看看会得到什么结果。
选择和精炼持仓尾随停止
我们尝试尾随与抛物线转向和移动平均线指标连接。抛物线转向指标,从其描述和名称(SAR = 停止和逆转)推断,应当非常适合根据其数值移动停止线:
抛物线转向技术指标是为分析趋势市场而开发的。该指标基于价格图表构建。该指标类似于移动平均线,唯一的区别是抛物线转向指标以更高的加速度移动,且或许会随价格项改变其位置。该指标在看涨市场低于价格(上升趋势),当市场看跌(下降趋势)时,它在价格上方。
如果价格穿过抛物线转向指标线,则指标转向,其未来数值位于价格的另一侧。当指标发生这样的转向时,前一个周期的最高或最低价格将作为起点。当指标转弯时,它会给出趋势结束(修正阶段或横盘)、或要转向的信号。
抛物线转向指标是一款提供离场点的出色指标。当价格跌破低于 SAR 线时,多头持仓应平仓,当价格升至 SAR 线上方时,空头持仓应平仓。也就是,有必要跟踪抛物线转向指标的方向,并仅保持与抛物线转向指标方向相同的持仓。该指标往往用作尾随停止线。
如果多头开仓(即价格高于 SAR 线),则无论价格朝哪个方向走,抛物线 SAR 线都会上行。抛物线转向指标线的移动量取决于价格走势的数值。
移动平均线由于其性质滞后,也适合跟随价格作为尾随止损线。对于多头持仓,如果指标线低于价格,那么可用移动平均线数值作为尾随停止。价格将穿过移动平均线,当然此刻触发停止。对于空头持仓,如果价格低于移动平均线,停止应跟随移动平均线。
我之前写过一篇关于将连接任意尾随到 EA 的文章:《如何开发任何类型的尾随停止,并将其连接到 EA》。在那篇文章中,我建议创建尾随类,并简单地将它们连接到 EA。
我们取这个机会的优势。唯一不太适合我们的就是,每个品种、遍历持仓的循环迭代都应用自己品种、或魔幻数字。这意味着我们将不得不细化来自该文章的类。幸运的是,这并不太难。
我们从该篇文章所附的文件中下载 Trailings.mqh 文件。这是我们目前唯一需要的文件。在上一篇文章中,我们创建了一个文件夹,其中包含按成交历史进行交易的类和 EA 文件:\MQL5\Experts\TradingByHistoryDeals。保存下载的 Trailings.mqh 文件,并在 MetaEditor 中打开它。
从程序调用的主要方法是 Run()。如果我们查看它的代码,我们可看到它实现了持仓的迭代:
//+------------------------------------------------------------------+ //| Launch simple trailing with StopLoss offset from the price | //+------------------------------------------------------------------+ bool CSimpleTrailing::Run(void) { //--- if disabled, leave if(!this.m_active) return false; //--- trailing variables MqlTick tick = {}; // price structure bool res = true; // result of modification of all positions //--- check the correctness of the data by symbol if(this.m_point==0) { //--- let's try to get the data again ::ResetLastError(); this.SetSymbol(this.m_symbol); if(this.m_point==0) { ::PrintFormat("%s: Correct data was not received for the %s symbol. Error %d",__FUNCTION__, this.m_symbol, ::GetLastError()); return false; } } //--- in a loop by the total number of open positions int total =::PositionsTotal(); for(int i = total - 1; i >= 0; i--) { //--- get the ticket of the next position ulong pos_ticket =::PositionGetTicket(i); if(pos_ticket == 0) continue; //--- get the symbol and position magic string pos_symbol = ::PositionGetString(POSITION_SYMBOL); long pos_magic = ::PositionGetInteger(POSITION_MAGIC); //--- if the position does not match the filter by symbol and magic number, leave if((this.m_magic != -1 && pos_magic != this.m_magic) || (pos_symbol != this.m_symbol)) continue; //--- if failed to get the prices, move on if(!::SymbolInfoTick(this.m_symbol, tick)) continue; //--- get the position type, its opening price and StopLoss level ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE); double pos_open =::PositionGetDouble(POSITION_PRICE_OPEN); double pos_sl =::PositionGetDouble(POSITION_SL); //--- get the calculated StopLoss level double value_sl = this.GetStopLossValue(pos_type, tick); //--- if StopLoss modification conditions are suitable, modify the position stop level and add the result to the res variable if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick)) res &=this.ModifySL(pos_ticket, value_sl); } //--- at the end of the loop, return the result of modifying each position that matches the "symbol/magic" filter return res; }
如果我们为每个品种交易对象调用该方法(参见第一篇文章),我们将获得与交易中所用品种数量相等的仓位迭代循环次数。这不是最优的。我们这样做就可令程序自己循环迭代遍历所有持仓,并且将从该循环中调用这些方法。但该方法也有一个内置循环...... 这意味着我们需要在尾随类中创建另一个 Run() 方法,但指定所需移动止损的持仓单据,并从主循环中调用它。
在简单的尾随类中,声明一个新方法来获取必要的持仓的单据:
//+------------------------------------------------------------------+ //| Class of the position StopLoss simple trailing | //+------------------------------------------------------------------+ class CSimpleTrailing : public CObject { private: //--- check the criteria for modifying the StopLoss position and return the flag bool CheckCriterion(ENUM_POSITION_TYPE pos_type, double pos_open, double pos_sl, double value_sl, MqlTick &tick); //--- modify StopLoss of a position by its ticket bool ModifySL(const ulong ticket, const double stop_loss); //--- return StopLevel in points int StopLevel(void); protected: string m_symbol; // trading symbol long m_magic; // EA ID double m_point; // Symbol Point int m_digits; // Symbol digits int m_offset; // stop distance from price int m_trail_start; // profit in points for launching trailing uint m_trail_step; // trailing step uint m_spread_mlt; // spread multiplier for returning StopLevel value bool m_active; // trailing activity flag //--- calculate and return the StopLoss level of the selected position virtual double GetStopLossValue(ENUM_POSITION_TYPE pos_type, MqlTick &tick); public: //--- set trailing parameters void SetSymbol(const string symbol) { this.m_symbol = (symbol==NULL || symbol=="" ? ::Symbol() : symbol); this.m_point =::SymbolInfoDouble(this.m_symbol, SYMBOL_POINT); this.m_digits = (int)::SymbolInfoInteger(this.m_symbol, SYMBOL_DIGITS); } void SetMagicNumber(const long magic) { this.m_magic = magic; } void SetStopLossOffset(const int offset) { this.m_offset = offset; } void SetTrailingStart(const int start) { this.m_trail_start = start; } void SetTrailingStep(const uint step) { this.m_trail_step = step; } void SetSpreadMultiplier(const uint value) { this.m_spread_mlt = value; } void SetActive(const bool flag) { this.m_active = flag; } //--- return trailing parameters string Symbol(void) const { return this.m_symbol; } long MagicNumber(void) const { return this.m_magic; } int StopLossOffset(void) const { return this.m_offset; } int TrailingStart(void) const { return this.m_trail_start; } uint TrailingStep(void) const { return this.m_trail_step; } uint SpreadMultiplier(void) const { return this.m_spread_mlt; } bool IsActive(void) const { return this.m_active; } //--- launch trailing with StopLoss offset from the price bool Run(void); bool Run(const ulong pos_ticket); //--- constructors CSimpleTrailing() : m_symbol(::Symbol()), m_point(::Point()), m_digits(::Digits()), m_magic(-1), m_trail_start(0), m_trail_step(0), m_offset(0), m_spread_mlt(2) {} CSimpleTrailing(const string symbol, const long magic, const int trailing_start, const uint trailing_step, const int offset); //--- destructor ~CSimpleTrailing() {} };
我们在类的主体之外编写它的实现:
//+------------------------------------------------------------------+ //| Launch simple trailing with StopLoss offset from the price | //+------------------------------------------------------------------+ bool CSimpleTrailing::Run(const ulong pos_ticket) { //--- if trailing is disabled, or the ticket is invalid, we leave if(!this.m_active || pos_ticket==0) return false; //--- trailing variables MqlTick tick = {}; // price structure //--- check the correctness of the data by symbol ::ResetLastError(); if(this.m_point==0) { //--- let's try to get the data again this.SetSymbol(this.m_symbol); if(this.m_point==0) { ::PrintFormat("%s: Correct data was not received for the %s symbol. Error %d",__FUNCTION__, this.m_symbol, ::GetLastError()); return false; } } //--- select a position by ticket if(!::PositionSelectByTicket(pos_ticket)) { ::PrintFormat("%s: PositionSelectByTicket(%I64u) failed. Error %d",__FUNCTION__, pos_ticket, ::GetLastError()); return false; } //--- if prices could not be obtained, return 'false' if(!::SymbolInfoTick(this.m_symbol, tick)) return false; //--- get the position type, its opening price and StopLoss level ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE); double pos_open =::PositionGetDouble(POSITION_PRICE_OPEN); double pos_sl =::PositionGetDouble(POSITION_SL); //--- get the calculated StopLoss level double value_sl = this.GetStopLossValue(pos_type, tick); //--- if the conditions for modifying StopLoss are suitable, return the result of modifying the position stop if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick)) return(this.ModifySL(pos_ticket, value_sl)); //--- conditions for modification are not suitable return false; }
现在,该方法将从 Run() 方法调用,其没有形参,但其中组织了一个循环遍历所有持仓,或者直接来自传递所需票据的程序。
我们来细化 Run() 方法,不用形参。从循环中删除代码模块:
for(int i = total - 1; i >= 0; i--) { //--- get the ticket of the next position ulong pos_ticket =::PositionGetTicket(i); if(pos_ticket == 0) continue; //--- get the symbol and position magic string pos_symbol = ::PositionGetString(POSITION_SYMBOL); long pos_magic = ::PositionGetInteger(POSITION_MAGIC); //--- if the position does not match the filter by symbol and magic number, leave if((this.m_magic != -1 && pos_magic != this.m_magic) || (pos_symbol != this.m_symbol)) continue; //--- if failed to get the prices, move on if(!::SymbolInfoTick(this.m_symbol, tick)) continue; //--- get the position type, its opening price and StopLoss level ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE); double pos_open =::PositionGetDouble(POSITION_PRICE_OPEN); double pos_sl =::PositionGetDouble(POSITION_SL); //--- get the calculated StopLoss level double value_sl = this.GetStopLossValue(pos_type, tick); //--- if StopLoss modification conditions are suitable, modify the position stop level and add the result to the res variable if(this.CheckCriterion(pos_type, pos_open, pos_sl, value_sl, tick)) res &=this.ModifySL(pos_ticket, value_sl); }
添加调用新方法来替代:
//+------------------------------------------------------------------+ //| Launch simple trailing with StopLoss offset from the price | //+------------------------------------------------------------------+ bool CSimpleTrailing::Run(void) { //--- if disabled, leave if(!this.m_active) return false; //--- trailing variables bool res = true; // result of modification of all positions //--- check the correctness of the data by symbol if(this.m_point==0) { //--- let's try to get the data again ::ResetLastError(); this.SetSymbol(this.m_symbol); if(this.m_point==0) { ::PrintFormat("%s: Correct data was not received for the %s symbol. Error %d",__FUNCTION__, this.m_symbol, ::GetLastError()); return false; } } //--- in a loop by the total number of open positions int total =::PositionsTotal(); for(int i = total - 1; i >= 0; i--) { //--- get the ticket of the next position ulong pos_ticket =::PositionGetTicket(i); if(pos_ticket == 0) continue; //--- get the symbol and position magic string pos_symbol = ::PositionGetString(POSITION_SYMBOL); long pos_magic = ::PositionGetInteger(POSITION_MAGIC); //--- if the position does not match the filter by symbol and magic number, leave if((this.m_magic != -1 && pos_magic != this.m_magic) || (pos_symbol != this.m_symbol)) continue; res &=this.Run(pos_ticket); } //--- at the end of the loop, return the result of modifying each position that matches the "symbol/magic" filter return res; }
在指定数值的尾随类中,还声明指定持仓票据的新 Run() 方法:
//+------------------------------------------------------------------+ //| Trailing class based on a specified value | //+------------------------------------------------------------------+ class CTrailingByValue : public CSimpleTrailing { protected: double m_value_sl_long; // StopLoss level for long positions double m_value_sl_short; // StopLoss level for short positions //--- calculate and return the StopLoss level of the selected position virtual double GetStopLossValue(ENUM_POSITION_TYPE pos_type, MqlTick &tick); public: //--- return StopLoss level for (2) long and (2) short positions double StopLossValueLong(void) const { return this.m_value_sl_long; } double StopLossValueShort(void) const { return this.m_value_sl_short; } //--- launch trailing with the specified StopLoss offset from the price bool Run(const double value_sl_long, double value_sl_short); bool Run(const ulong pos_ticket, const double value_sl_long, double value_sl_short); //--- constructors CTrailingByValue(void) : CSimpleTrailing(::Symbol(), -1, 0, 0, 0), m_value_sl_long(0), m_value_sl_short(0) {} CTrailingByValue(const string symbol, const long magic, const int trail_start, const uint trail_step, const int trail_offset) : CSimpleTrailing(symbol, magic, trail_start, trail_step, trail_offset), m_value_sl_long(0), m_value_sl_short(0) {} //--- destructor ~CTrailingByValue(void){} };
在类主体之外编写其实现:
//+------------------------------------------------------------------+ //| Launch trailing with the specified StopLoss offset from the price| //+------------------------------------------------------------------+ bool CTrailingByValue::Run(const ulong pos_ticket,const double value_sl_long,double value_sl_short) { this.m_value_sl_long =value_sl_long; this.m_value_sl_short=value_sl_short; return CSimpleTrailing::Run(pos_ticket); }
在此,我们调用添加到简单尾随类的 Run() 方法,以及指定的持仓票据。
在该文章中查找有关尾随类的详细信息,我们从中得到尾随止损文件。
在上一篇文章中创建的品种交易类存储交易列表,以及标准库的 CTrade 交易类。为了避免修改它包含的尾随类,我们将基于它再创建一个新类,我们会把处理尾随类添加其中,
在 \MQL5\Experts\TradingByHistoryDeals\ 终端文件夹中,创建 CSymbolTradeExt 类的新文件 SymbolTradeExt.mqh。该文件应包含尾随类的文件和 CSymbolTrade 类文件。新创建的类应该从后者继承:
//+------------------------------------------------------------------+ //| SymbolTradeExt.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "Trailings.mqh" #include "SymbolTrade.mqh" class CSymbolTradeExt : public CSymbolTrade { }
当使用取自本文的尾随停止类时,我们会获得抛物线和所有标准移动平均线类型的尾随停止。该类还有一个基于指定数值的尾随函数,但我们不会在此用到它,因为它需要在自己的程序中计算停止价位,并将它们传递到尾随类所调用的 Run() 方法参数之中。例如,这可能是 ATR 指标的计算值,并将该值传递给尾随类。
我们实现尾随模式的枚举:
//+------------------------------------------------------------------+ //| SymbolTradeExt.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "Trailings.mqh" #include "SymbolTrade.mqh" enum ENUM_TRAILING_MODE // Enumeration of trailing modes { TRAILING_MODE_SIMPLE=2, // Simple trailing TRAILING_MODE_SAR, // Trailing by Parabolic SAR TRAILING_MODE_AMA, // Trailing by adjustable moving average TRAILING_MODE_DEMA, // Trailing by double exponential moving average TRAILING_MODE_FRAMA, // Trailing by fractal adaptive moving average TRAILING_MODE_MA, // Trailing by simple moving average TRAILING_MODE_TEMA, // Trailing by triple exponential moving average TRAILING_MODE_VIDYA, // Trailing by moving average with dynamic averaging period }; class CSymbolTradeExt : public CSymbolTrade { }
为什么枚举常量值以 2 而不是零开头?我们将将要创建的新 EA 还列出了测试模式:
enum ENUM_TESTING_MODE { TESTING_MODE_ORIGIN, /* Original trading */ TESTING_MODE_SLTP, /* Specified StopLoss and TakeProfit values */ };
此处有两个常量:原始交易(0),以及按指定止损单进行交易(1)。稍后,我们还将在此处添加与尾随枚举对应的新常量。正是出于这个原因,尾随枚举常量的值从数值 2 开始。
在类的私密部分中,声明一个指向尾随停止类对象的指针,和一个变量,用于存储图表周期,供尾随停止中所用指标的计算。在公开部分,我们声明了一个设置尾随参数的方法、一个启动持仓尾随的方法、构造函数、和一个析构函数:
class CSymbolTradeExt : public CSymbolTrade { private: CSimpleTrailing *m_trailing; // Trailing class object ENUM_TIMEFRAMES m_timeframe; // Timeframe for calculating the indicator for trailing public: //--- Set trailing and its parameters bool SetTrailing(const ENUM_TRAILING_MODE trailing_mode, const int data_index, const long magic, const int start, const int step, const int offset, const MqlParam ¶m[]); //--- Start a trail of the position specified by the ticket void Trailing(const ulong pos_ticket); //--- Constructor/destructor CSymbolTradeExt() : m_trailing(NULL), m_timeframe(::Period()) { this.SetSymbol(::Symbol()); } CSymbolTradeExt(const string symbol, const ENUM_TIMEFRAMES timeframe); ~CSymbolTradeExt(); };
在类构造函数中,即在初始化代码中,品种被传递至父类构造函数。形参中传递的数值是为设置指标计算时间帧,而指向尾随类对象的指针初始化为 NULL:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSymbolTradeExt::CSymbolTradeExt(const string symbol, const ENUM_TIMEFRAMES timeframe) : CSymbolTrade(symbol) { this.m_trailing=NULL; this.m_timeframe=timeframe; }
如果尾随对象被创建,则会在类析构函数中删除它:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CSymbolTradeExt::~CSymbolTradeExt() { //--- delete the created trailing object if(this.m_trailing!=NULL) delete this.m_trailing; }
由于不同类型的尾随止损会配以不同类型的指标工作,具有不同的设置参数,因此我们将通过 MqlParam 结构将所有这些参数集传递给设置尾随停止参数的方法。只是每个尾随都拥有它自己的参数集,并且结构的每个字段都将携带其指标参数的值,仅为其固有。
//+------------------------------------------------------------------+ //| Set trailing parameters | //+------------------------------------------------------------------+ bool CSymbolTradeExt::SetTrailing(const ENUM_TRAILING_MODE trailing_mode, const int data_index, const long magic, const int start, const int step, const int offset, const MqlParam ¶m[]) { //--- Set trailing parameters (only necessary structure fields are used for each indicator type) int ma_period = (int)param[0].integer_value; int ma_shift = (int)param[1].integer_value; ENUM_APPLIED_PRICE ma_price = (ENUM_APPLIED_PRICE)param[2].integer_value; ENUM_MA_METHOD ma_method = (ENUM_MA_METHOD)param[3].integer_value; int fast_ema = (int)param[4].integer_value; int slow_ema = (int)param[5].integer_value; int period_cmo = (int)param[6].integer_value; double sar_step = param[0].double_value; double sar_max = param[1].double_value; //--- depending on the trailing type, we create a trailing object //--- if the value passed as the calculation period is less than the allowed value, then each indicator is assigned its own default value switch(trailing_mode) { case TRAILING_MODE_SIMPLE : this.m_trailing=new CSimpleTrailing(this.Symbol(), magic, start, step, offset); break; case TRAILING_MODE_AMA : this.m_trailing=new CTrailingByAMA(this.Symbol(), this.m_timeframe, magic, (ma_period<1 ? 9 : ma_period), (fast_ema<1 ? 2 : fast_ema), (slow_ema<1 ? 30 : slow_ema), ma_shift, ma_price, start, step, offset); break; case TRAILING_MODE_DEMA : this.m_trailing=new CTrailingByDEMA(this.Symbol(), this.m_timeframe, magic, (ma_period==0 ? 14 : ma_period), ma_shift, ma_price, start, step, offset); break; case TRAILING_MODE_FRAMA : this.m_trailing=new CTrailingByFRAMA(this.Symbol(), this.m_timeframe, magic, (ma_period==0 ? 14 : ma_period), ma_shift, ma_price, start, step, offset); break; case TRAILING_MODE_MA : this.m_trailing=new CTrailingByMA(this.Symbol(), this.m_timeframe, magic, ma_period, (ma_period==0 ? 10 : ma_period), ma_method, ma_price, start, step, offset); break; case TRAILING_MODE_TEMA : this.m_trailing=new CTrailingByTEMA(this.Symbol(), this.m_timeframe, magic, (ma_period==0 ? 14 : ma_period), ma_shift, ma_price, start, step, offset); break; case TRAILING_MODE_VIDYA : this.m_trailing=new CTrailingByVIDYA(this.Symbol(), this.m_timeframe, magic, (period_cmo<1 ? 9 : period_cmo), (ma_period==0 ? 12 : ma_period), ma_shift, ma_price, start, step, offset); break; case TRAILING_MODE_SAR : this.m_trailing=new CTrailingBySAR(this.Symbol(), this.m_timeframe, magic, (sar_step<0.0001 ? 0.02 : sar_step), (sar_max<0.02 ? 0.2 : sar_max), start, step, offset); break; default : break; } //--- something went wrong - return 'false' if(this.m_trailing==NULL) return false; //--- all is well - make the trail active and return 'true' this.m_trailing.SetActive(true); return true; }
当针对不同的尾随类型调用此方法时,正确填充 MqlParam 结构非常重要。当我们修改 EA 使用尾随时,我们要检查这一点。
该方法按票据启动指定持仓尾随:
//+------------------------------------------------------------------+ //| Start trailing of the position specified by the ticket | //+------------------------------------------------------------------+ void CSymbolTradeExt::Trailing(const ulong pos_ticket) { if(this.m_trailing!=NULL) this.m_trailing.Run(pos_ticket); }
从 EA 的持仓循环中(取决于持仓品种),我们将从列表中获取品种交易对象。从该对象中,我们调用该方法,并把所需票据传递给该方法,并开始持仓尾随停止。如果尾随对象存在,则按指定的持仓票据调用其新的 Run() 方法。
测试不同类型的尾随停止
为了执行测试,我们使取用上一篇文章中的 TradingByHistoryDeals_SLTP.mq5 EA,并将其保存在与 TradingByHistoryDeals_Ext.mq5 相同的文件夹 \MQL5\Experts\TradingByHistoryDeals\ 之中。
包含 CSymbolTrade 类文件替换为包含 CSymbolTradeExt 类文件,包括尾随类的文件,并通过添加必要的尾随选择来扩展测试模式枚举的常量列表:
//+------------------------------------------------------------------+ //| TradingByHistoryDeals_Ext.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "SymbolTradeExt.mqh" #include "Trailings.mqh" enum ENUM_TESTING_MODE { TESTING_MODE_ORIGIN, /* Original trading */ TESTING_MODE_SLTP, /* Specified StopLoss and TakeProfit values */ TESTING_MODE_TRAIL_SIMPLE, /* Simple Trailing */ TESTING_MODE_TRAIL_SAR, /* Trailing by Parabolic SAR indicator */ TESTING_MODE_TRAIL_AMA, /* Trailing by AMA indicator */ TESTING_MODE_TRAIL_DEMA, /* Trailing by DEMA indicator */ TESTING_MODE_TRAIL_FRAMA, /* Trailing by FRAMA indicator */ TESTING_MODE_TRAIL_MA, /* Trailing by MA indicator */ TESTING_MODE_TRAIL_TEMA, /* Trailing by TEMA indicator */ TESTING_MODE_TRAIL_VIDYA, /* Trailing by VIDYA indicator */ }; //+------------------------------------------------------------------+ //| Expert | //+------------------------------------------------------------------+
在 EA 输入中,添加选择尾随参数的变量:
//+------------------------------------------------------------------+ //| Expert | //+------------------------------------------------------------------+ //--- input parameters input group " - Strategy parameters - " input string InpTestedSymbol = ""; /* The symbol being tested in the tester */ input long InpTestedMagic = -1; /* The magic number being tested in the tester */ sinput bool InpShowDataInLog = false; /* Show collected data in the log */ input group " - Stops parameters - " input ENUM_TESTING_MODE InpTestingMode = TESTING_MODE_ORIGIN; /* Testing Mode */ input int InpStopLoss = 300; /* StopLoss in points */ input int InpTakeProfit = 500; /* TakeProfit in points */ input group " - Trailing Parameters -" input bool InpSetStopLoss = true; /* Set Initial StopLoss */ input bool InpSetTakeProfit = true; /* Set Initial TakeProfit */ input int InpTrailingStart = 150; /* Trailing start */ // Profit in points to start trailing input int InpTrailingStep = 50; /* Trailing step in points */ input int InpTrailingOffset = 0; /* Trailing offset in points */ input group " - Indicator Parameters -" input ENUM_TIMEFRAMES InpIndTimeframe = PERIOD_CURRENT; /* Indicator's timeframe */ // Timeframe of the indicator used in trailing calculation input int InpMAPeriod = 0; /* MA Period */ input int InpMAShift = 0; /* MA Shift */ input int InpFastEMAPeriod = 2; /* AMA Fast EMA Period */ input int InpSlowEMAPeriod = 30; /* AMA Slow EMA Period */ input int InpCMOPeriod = 9; /* VIDYA CMO Period */ input double InpSARStep = 0.02; /* Parabolic SAR Step */ input double InpSARMax = 0.2; /* Parabolic SAR Max */ input ENUM_APPLIED_PRICE InpAppliedPrice = PRICE_CLOSE; /* MA Applied Price */ input ENUM_MA_METHOD InpMAMethod = MODE_SMA; /* MA Smoothing Method */ input int InpDataIndex = 1; /* Indicator data index */ // Bar of data received frrom the indicator
此处默认情况下,InpMAPeriod 设置为零。这样做的原因是每个类型的移动平均线都有自己的周期默认值。当该参数设置为零时,为指标设置的默认值将传递给尾随类,如果尾随用到的指标需要简单的默认值,则所有数据都将是正确的。
在整个代码中,将所有出现的 CSymbolTrade 替换为 CSymbolTradeExt。现在我们有一个新的品种交易对象类 CSymbolTradeExt,继承自之前在上一篇文章中实现的 CSymbolTrade 类。这是声明尾随类对象的类。因此,我们用新的替换了以前的类。事实上,没有必要在所有地方都这样做,而只在需要尾随之处。此处我们不深究类继承的细节,而简单地将旧类替换为新类。
将根据其票据为每笔持仓调用尾随。为了访问持仓尾随,我们需要从列表中获取持仓品种的交易对象。为此,我们将实现按品种名称从列表中返回指向交易对象指针的函数:
//+------------------------------------------------------------------+ //| Return the pointer to the symbol trading object by name | //+------------------------------------------------------------------+ CSymbolTrade *GetSymbolTrade(const string symbol, CArrayObj *list) { SymbTradeTmp.SetSymbol(symbol); list.Sort(); int index=list.Search(&SymbTradeTmp); return list.At(index); }
该函数接收必须返回交易对象的品种名称,以及指向存储品种交易对象指针列表的指针。传递给函数的品种名称设置为临时交易对象,并在列表中搜索含有该品种名称的对象索引。如是结果,将按索引从列表中返回指向所需对象的指针。如果该类对象不在列表中,则索引等于 -1,并且从列表中返回 NULL。
创建所用品种数组时,品种交易对象也会一并创建,并在其中初始化输入中指定的尾随止损。我们细化创建所用品种数组的函数:
//+------------------------------------------------------------------+ //| Creates an array of used symbols | //+------------------------------------------------------------------+ bool CreateListSymbolTrades(SDeal &array_deals[], CArrayObj *list_symbols) { bool res=true; // result MqlParam param[7]={}; // trailing parameters int total=(int)array_deals.Size(); // total number of deals in the array //--- if the deal array is empty, return 'false' if(total==0) { PrintFormat("%s: Error! Empty deals array passed",__FUNCTION__); return false; } //--- in a loop through the deal array CSymbolTradeExt *SymbolTrade=NULL; for(int i=0; i<total; i++) { //--- get the next deal and, if it is neither buy nor sell, move on to the next one SDeal deal_str=array_deals[i]; if(deal_str.type!=DEAL_TYPE_BUY && deal_str.type!=DEAL_TYPE_SELL) continue; //--- find a trading object in the list whose symbol is equal to the deal symbol string symbol=deal_str.Symbol(); SymbTradeTmp.SetSymbol(symbol); list_symbols.Sort(); int index=list_symbols.Search(&SymbTradeTmp); //--- if the index of the desired object in the list is -1, there is no such object in the list if(index==WRONG_VALUE) { //--- we create a new trading symbol object and, if creation fails, //--- add 'false' to the result and move on to the next deal SymbolTrade=new CSymbolTradeExt(symbol, InpIndTimeframe); if(SymbolTrade==NULL) { res &=false; continue; } //--- if failed to add a symbol trading object to the list, //--- delete the newly created object, add 'false' to the result //--- and we move on to the next deal if(!list_symbols.Add(SymbolTrade)) { delete SymbolTrade; res &=false; continue; } //--- initialize trailing specified in the settings in the trading object ENUM_TRAILING_MODE mode=(ENUM_TRAILING_MODE)InpTestingMode; SetTrailingParams(mode, param); SymbolTrade.SetTrailing(mode, InpDataIndex, InpTestedMagic, InpTrailingStart, InpTrailingStep, InpTrailingOffset, param); } //--- otherwise, if the trading object already exists in the list, we get it by index else { SymbolTrade=list_symbols.At(index); if(SymbolTrade==NULL) continue; } //--- if the current deal is not yet in the list of deals of the symbol trading object if(SymbolTrade.GetDealByTime(deal_str.time)==NULL) { //--- create a deal object according to its sample structure CDeal *deal=CreateDeal(deal_str); if(deal==NULL) { res &=false; continue; } //--- add the result of adding the deal object to the list of deals of a symbol trading object to the result value res &=SymbolTrade.AddDeal(deal); } } //--- return the final result of creating trading objects and adding deals to their lists return res; }
此处,我们添加了指标输入参数结构的声明,和一小块代码,其中初始化交易对象中的尾随。
实现设置指定尾随参数的特殊函数:
//+------------------------------------------------------------------+ //| Set the trailing parameters according to its selected type | //+------------------------------------------------------------------+ void SetTrailingParams(const ENUM_TRAILING_MODE mode, MqlParam ¶m[]) { //--- reset all parameters ZeroMemory(param); //--- depending on the selected trailing type, we set the indicator parameters switch(mode) { case TRAILING_MODE_SAR : param[0].type=TYPE_DOUBLE; param[0].double_value=InpSARStep; param[1].type=TYPE_DOUBLE; param[1].double_value=InpSARMax; break; case TRAILING_MODE_AMA : param[0].type=TYPE_INT; param[0].integer_value=InpMAPeriod; param[1].type=TYPE_INT; param[1].integer_value=InpMAShift; param[2].type=TYPE_INT; param[2].integer_value=InpAppliedPrice; param[4].type=TYPE_INT; param[4].integer_value=InpFastEMAPeriod; param[5].type=TYPE_INT; param[5].integer_value=InpSlowEMAPeriod; break; case TRAILING_MODE_DEMA : param[0].type=TYPE_INT; param[0].integer_value=InpMAPeriod; param[1].type=TYPE_INT; param[1].integer_value=InpMAShift; param[2].type=TYPE_INT; param[2].integer_value=InpAppliedPrice; break; case TRAILING_MODE_FRAMA : param[0].type=TYPE_INT; param[0].integer_value=InpMAPeriod; param[1].type=TYPE_INT; param[1].integer_value=InpMAShift; param[2].type=TYPE_INT; param[2].integer_value=InpAppliedPrice; break; case TRAILING_MODE_MA : param[0].type=TYPE_INT; param[0].integer_value=InpMAPeriod; param[1].type=TYPE_INT; param[1].integer_value=InpMAShift; param[2].type=TYPE_INT; param[2].integer_value=InpAppliedPrice; param[3].type=TYPE_INT; param[3].integer_value=InpMAMethod; break; case TRAILING_MODE_TEMA : param[0].type=TYPE_INT; param[0].integer_value=InpMAPeriod; param[1].type=TYPE_INT; param[1].integer_value=InpMAShift; param[2].type=TYPE_INT; param[2].integer_value=InpAppliedPrice; break; case TRAILING_MODE_VIDYA : param[0].type=TYPE_INT; param[0].integer_value=InpMAPeriod; param[1].type=TYPE_INT; param[1].integer_value=InpMAShift; param[2].type=TYPE_INT; param[2].integer_value=InpAppliedPrice; param[6].type=TYPE_INT; param[6].integer_value=InpCMOPeriod; break; case TRAILING_MODE_SIMPLE : break; default: break; } }
在此,针对每种尾随类型中所用指标,EA 输入中指定的数值都设置在结构之中。赋值到结构字段的数值会检查有效性,并在上面讨论的品种交易对象的新 CSymbolTradeExt 类中进行调整。
我们改进基于成交历史的交易功能。现在我们需要清晰地区分正在使用的测试类型。
为了测试原始交易,无需设置止损单 — 所有仓位都基于历史平仓成交。
如果测试交易使用了不同的止损单大小,您需要获得正确的大小,并为正在开立的持仓设置它们。在测试尾随停止时,没有必要设置止损,不过,为了避免大幅回撤和“超期滞留”,建议设置初始止损,然后尾随停止将根据指定的逻辑移动它们。EA 设置包含判定是否应下达初始止损单的参数:
input bool InpSetStopLoss = true; /* Set Initial StopLoss */ input bool InpSetTakeProfit = true; /* Set Initial TakeProfit */
当使用它们时,您还需要获得正确的止损大小,并为正在开立的持仓设置它们:
//+------------------------------------------------------------------+ //| Trading by history | //+------------------------------------------------------------------+ void TradeByHistory(const string symbol="", const long magic=-1) { datetime time=0; int total=ExtListSymbols.Total(); // number of trading objects in the list //--- in a loop by all symbol trading objects for(int i=0; i<total; i++) { //--- get another trading object CSymbolTradeExt *obj=ExtListSymbols.At(i); if(obj==NULL) continue; //--- get the current deal pointed to by the deal list index CDeal *deal=obj.GetDealCurrent(); if(deal==NULL) continue; //--- sort the deal by magic number and symbol if((magic>-1 && deal.Magic()!=magic) || (symbol!="" && deal.Symbol()!=symbol)) continue; //--- sort the deal by type (only buy/sell deals) ENUM_DEAL_TYPE type=deal.TypeDeal(); if(type!=DEAL_TYPE_BUY && type!=DEAL_TYPE_SELL) continue; //--- if this is a deal already handled in the tester, move on to the next one if(deal.TicketTester()>0) continue; //--- if the deal time has not yet arrived, move to the next trading object of the next symbol if(!obj.CheckTime(deal.Time())) continue; //--- in case of a market entry deal ENUM_DEAL_ENTRY entry=deal.Entry(); if(entry==DEAL_ENTRY_IN) { //--- set the sizes of stop orders depending on the stop setting method //--- stop orders are not used initially (for original trading) ENUM_ORDER_TYPE order_type=(deal.TypeDeal()==DEAL_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL); double sl=0; double tp=0; //--- in case of the mode for setting the specified stop order values if(InpTestingMode==TESTING_MODE_SLTP) { //--- get correct values for StopLoss and TakeProfit sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss); tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit); } //--- otherwise, if testing with trailing stops else { if(InpTestingMode!=TESTING_MODE_ORIGIN) { //--- if allowed in the settings, we set correct stop orders for the positions being opened if(InpSetStopLoss) sl=CorrectStopLoss(deal.Symbol(), order_type, ExtStopLoss); if(InpSetTakeProfit) tp=CorrectTakeProfit(deal.Symbol(), order_type, ExtTakeProfit); } } //--- open a position by deal type ulong ticket=(type==DEAL_TYPE_BUY ? obj.Buy(deal.Volume(), deal.Magic(), sl, tp, deal.Comment()) : type==DEAL_TYPE_SELL ? obj.Sell(deal.Volume(),deal.Magic(), sl, tp, deal.Comment()) : 0); //--- if a position is opened (we received its ticket) if(ticket>0) { //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket); //--- get the position ID in the tester and write it to the properties of the deal object long pos_id_tester=0; if(HistoryDealSelect(ticket)) { pos_id_tester=HistoryDealGetInteger(ticket, DEAL_POSITION_ID); deal.SetPosIDTester(pos_id_tester); } } } //--- in case of a market exit deal if(entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT || entry==DEAL_ENTRY_OUT_BY) { //--- get a deal a newly opened position is based on CDeal *deal_in=obj.GetDealInByPosID(deal.PositionID()); if(deal_in==NULL) continue; //--- get the position ticket in the tester from the properties of the opening deal //--- if the ticket is zero, then most likely the position in the tester is already closed ulong ticket_tester=deal_in.TicketTester(); if(ticket_tester==0) { PrintFormat("Could not get position ticket, apparently position #%I64d (#%I64d) is already closed \n", deal.PositionID(), deal_in.PosIDTester()); obj.SetNextDealIndex(); continue; } //--- if we reproduce the original trading history in the tester, if(InpTestingMode==TESTING_MODE_ORIGIN) { //--- if the position is closed by ticket if(obj.ClosePos(ticket_tester)) { //--- increase the number of deals handled by the tester and write the deal ticket in the tester to the properties of the deal object obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket_tester); } } //--- otherwise, in the tester we work with stop orders placed according to different algorithms, and closing deals are skipped; //--- accordingly, for the closing deal, we simply increase the number of deals handled by the tester and //--- write the deal ticket in the tester to the properties of the deal object else { obj.SetNumProcessedDeals(obj.NumProcessedDeals()+1); deal.SetTicketTester(ticket_tester); } } //--- if a ticket is now set in the deal object, then the deal has been successfully handled - //--- set the deal index in the list to the next deal if(deal.TicketTester()>0) { obj.SetNextDealIndex(); } } }
现在我们实现持仓尾随位置的函数:
//+------------------------------------------------------------------+ //| Trail positions | //+------------------------------------------------------------------+ void Trailing(void) { //--- variables for getting position properties long magic=-1; string symbol=""; //--- in a loop through all positions int total=PositionsTotal(); for(int i=total-1; i>=0; i--) { //--- get the ticket of the next position ulong ticket=PositionGetTicket(i); if(ticket==0) continue; //--- get the magic number and position symbol ResetLastError(); if(!PositionGetInteger(POSITION_MAGIC, magic)) { Print("PositionGetInteger() failed. Error ", GetLastError()); continue; } if(!PositionGetString(POSITION_SYMBOL, symbol)) { Print("PositionGetString() failed. Error ", GetLastError()); continue; } //--- if the position does not meet the specified conditions of the magic number and symbol, we move to the next one if((InpTestedMagic>-1 && magic!=InpTestedMagic) || (InpTestedSymbol!="" && symbol!=InpTestedSymbol)) continue; //--- get a trading object by a symbol name and call its method for trailing a position by ticket CSymbolTradeExt *obj=GetSymbolTrade(symbol, &ExtListSymbols); if(obj!=NULL) obj.Trailing(ticket); } }
此处的一切都很简单:我们在循环中选择的每笔持仓,检查其魔幻数字和品种是否与 EA 设置的输入匹配,如果这是所需持仓,我们获取该持仓品种的交易对象,指定持仓票据,并从其调用尾随。
我们实现调用 EA 的 OnTick() 处理程序的函数:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- work only in the strategy tester if(!MQLInfoInteger(MQL_TESTER)) return; //--- Trail open positions Trailing(); //--- Handle the list of deals from the file TradeByHistory(InpTestedSymbol, InpTestedMagic); }
全部安置妥当。EA 已准备就绪。完整的 EA 代码可在文章所附的文件中找到。
我们测试不同类型的尾随停止,并比较原始交易与使用不同尾随停止算法的交易结果。
当在品种图表上启动 EA 时,它会收集交易历史记录,将其写入文件,并显示指示所需测试器设置的警报 — 初始测试日期、初始存款、和杠杆:

流水账显示交易所用品种,以及每个品种执行的交易数量:
Alert: Now you can run testing Interval: 2024.09.13 - current date Initial deposit: 3000.00, leverage 1:500 Symbols used in trading: 1. AUDUSD trade object. Total deals: 222 2. EURJPY trade object. Total deals: 120 3. EURUSD trade object. Total deals: 526 4. GBPUSD trade object. Total deals: 352 5. NZDUSD trade object. Total deals: 182 6. USDCAD trade object. Total deals: 22 7. USDCHF trade object. Total deals: 250 8. USDJPY trade object. Total deals: 150 9. XAUUSD trade object. Total deals: 118
现在,我们在测试器中启动 EA,并在警报中指定推荐的测试器参数,并在流水账中复现。
原始交易:

我们看到原始交易导致亏损 658 美元。
我们尝试不同的尾随停止。允许将初始止损设置为 100 点,并在测试器中运行每笔尾随订单。
这将轮流完成,无需更改其它参数。我们看看交易如何变化。
简单尾随:

我们亏损了 746.1 美元。甚至比原始交易还要多。
我们使用指标检查尾随。
抛物线转向指标尾随:

盈利 — 541.8 美元。
现在我们看看基于不同类型移动平均线的尾随停止。
按 АМА 尾随:

盈利 — 806.5 美元。
按 DEMA 尾随:

盈利 — 1397.1 美元。
按 FRAMA 尾随:

盈利 — 1291.6 美元。
按 MA 尾随:

盈利 — 563.1 美元。
按 TEMA 尾随:

盈利 — 1355.1 美元。
按 VIDYA 尾随:

盈利 — 283.3 美元。
故此,最后一张表显示了使用不同类型的尾随停止相对于原始交易的结果:
| # | 尾随类型 | 止损大小 | 止盈大小 | 总盈利 |
|---|---|---|---|---|
| 1 | 原始交易: | 100 | 500 | - 658.0 |
| 2 | 简单尾随停止 | 最初为 100,然后据价格 100 点处停止 | 0 | - 746.1 |
| 3 | 抛物线转向指标尾随停止 | 最初为 100,然后基于指标值停止 | 0 | + 541.8 |
| 4 | 按 VIDYA 尾随停止 | 最初为 100,然后基于指标值停止 | 0 | -283.3 |
| 5 | 按 MA 尾随停止 | 最初为 100,然后基于指标值停止 | 0 | + 563.1 |
| 6 | 按 АМА 尾随停止 | 最初为 100,然后基于指标值停止 | 0 | + 806.5 |
| 7 | 按 FRAMA 尾随停止 | 最初为 100,然后基于指标值停止 | 0 | + 1291.6 |
| 8 | 按 TEMA 尾随停止 | 最初为 100,然后基于指标值停止 | 0 | + 1355.1 |
| 9 | 按 DEMA 尾随停止 | 最初为 100,然后基于指标值停止 | 0 | + 1397.1 |
如是结果,当原始交易无利可图时,与客户端中的尾随停止相当的常规简单尾随会导致更大的亏损。我们看一个基于抛物线转向指标的尾随指标,该指标定位为显示逆转和止损价位(停止并逆转)的指标。事实上,通过遵循第一根柱上指标线后面的止损价位,我们盈利了 540 美元。
现在我们看看基于第一根柱上不同类型移动平均线数值移动持仓止损的结果。按 VIDYA 指标的尾随导致亏损 280 美元,但所有其它指标均显示盈利。甚至,遵循简单移动平均线止损比沿抛物线转向指标尾随产生更大的盈利。改善交易结果的绝对“冠军”是双重指数移动平均线 — +1397 美元的盈利。
结束语
我们开发了一款 EA,允许在策略测试器中测试我们的交易,并为我们的交易风格选择最合适的尾随停止。请注意,尾随测试中用到的所有指标参数都有标准值,尾随设置是随机选择的。
最正确的解决方案是对每个单独的品种进行测试,并根据货币对的变动和波动性特征选择可接受的尾随设置。但是该 EA 没有单独的设置来为每个单独的品种设置自定义参数。但最主要的是利用策略测试器测试,展示分析和改进交易的可能性。这意味着能够深入开发 EA,从而允许我们为每个品种设置自己的参数 — 这并不困难。
所有审查过的类文件和 EA 都附于文后。还包括一个存档。一旦解压,您可立即获取在所需终端文件夹中安装好的文件进行测试。
文章中用到的程序:
| # | 名称 | 类型 | 描述 |
|---|---|---|---|
| 1 | Trailings.mqh | 类库 | 尾随类库 |
| 2 | SymbolTrade.mqh | 类库 | 交易结构和类库,品种交易类 |
| 3 | SymbolTradeExt.mqh | 类库 | 品种和尾随交易类 |
| 4 | TradingByHistoryDeals_Ext.mq5 | 智能交易系统 | 在策略测试器中复现账户上执行的成交,查看和修改成交的止损、止盈和尾随停止的 EA。 |
| 5 | MQL5.zip | ZIP 存档 | 上面显示的文件存档可以解压到客户端的 MQL5 目录之中 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/16991
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
价格行为分析工具包开发(第十六部分):引入四分之一理论(2)—— 侵入探测器智能交易系统(EA)
价格行为分析工具包开发(第 17 部分):TrendLoom EA 工具
卡尔曼滤波器在外汇均值回归策略中的应用
Трал по VIDYA:
利润 - 283.3 美元。错误:损失 - 283.3 美元。
错误:损失 283.3 美元。
文章中写的是利润的负值。
但是,减号后面的空格是不小心加上去的。