English Русский Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
“EA 交易”运行期间平衡曲线斜率的控制

“EA 交易”运行期间平衡曲线斜率的控制

MetaTrader 5交易 | 9 十二月 2013, 13:03
2 880 0
Dmitriy Skub
Dmitriy Skub

简介

本文讲述的是,通过创建反馈来改善“EA 交易”性能的一种方法。本例中的反馈将基于平衡曲线斜率的测量。斜率的控制会通过调节交易量自动执行。“EA 交易”可行交易的模式如下:削减交易量、有效手数(根据最初调整)以及中间交易量。工作模式自动切换。

反馈链中采用了不同的调节特性:步进、带延迟的步进、线性。它允许将平衡曲线斜率控制系统调整为某特定系统特性。

主旨在于实现交易者决策制定过程的自动化,同时监测自己的交易系统。在不适宜的工作期间降低风险是合理的。返回正常工作模式后,风险又可以恢复到最初水平。

当然了,此系统并非什么万能灵药,也不会将一个亏损的“EA 交易”转变成一个盈利的“EA 交易”。某种程序上,这是“EA 交易” MM (资金管理)的一个补充,避免其在某个账户出现过大损失。

本文中包含一个库,它允许将此函数嵌入到任何“EA 交易”代码中。

操作原理

我们来深入了解一下控制平衡曲线斜率的系统操作原理。假设我们已有一个交易的“EA 交易”。则其假想的平衡曲线如下所示:

平衡曲线斜率控制系统的操作原理

图 1. 平衡曲线斜率控制系统的操作原理

交易操作使用恒定交易量的“EA 交易”的平衡初始曲线如上所示。已平仓交易则用红点标示。我们将这些点用一条曲线连接起来,从而展示出交易期间“EA 交易”的平衡变动(黑色粗线)。

现在,我们要持续追踪此线斜率与时间轴的角度(用蓝色细线表示)。或者更确切地说,根据某信号每个交易开盘之前,我们都会通过两个之前的平仓交易(为方便说明,或者是通过两个交易),计算坡度。如果坡度变得小于指定值,则我们的控制系统开始工作;它会根据计算得出的角度值和指定的调节函数减少交易量。

按这种方式,如果交易进入一个未成功的周期,则交易量会从 Vmax. 降至 Vmin.Т3...Т5 交易周期)。过了 Т5 点之后,则以最小指定交易量执行交易 - 使用交易量拒绝模式。一旦“EA 交易”的盈利能力恢复、且平衡曲线的坡度升至指定值以上,则交易量开始增加。这发生于 Т8...Т10 区间。Т10 点之后,交易量操作恢复至初始状态 Vmax。

作为此类调节结果产生的平衡曲线,显示于图 1 下部。您可以看出,最初从 B1B2 的减值已经减少,并变成从 B1B2*。您还能注意到,利润在恢复最大交易量周期 Т8...Т10 内稍有降低 - 此为问题的另一面。

如以最小指定量执行交易,则平衡曲线的对应部分会被绿色高亮。黄色则表明该部分从最大到最小交易量及最小到最大交易量的转换。这里有多个可能的转换变量:

  • 步进 - 交易量以离散步进从最大到最小以及从最小到最大进行变换;
  • 线性 - 交易量根据调节区间内平衡曲线的坡度线性变化;
  • 带延迟的步进 - 从最大转换到最小交易量并返回,于不同坡度值处执行。

我们用图来说明:

调节特性的类型

图 2. 调节特性的类型

调节特性会影响到控制系统的率值 - 启用/禁用延迟,从最大转换到最小交易量并返回的过程。建议基于试验选择达成最佳测试结果时的特性。

由此,我们利用基于平衡曲线坡度的反馈,来强化该交易系统。注意:这种交易量的调节仅适于未将交易量作为交易系统本身一部分的那些系统。比如说,如果采用的是马丁格尔 (Martingale) 原则,则不能在未改变初始“EA 交易”的情况下直接使用该系统。

此外,我们需要将注意力投向下述重点:

  • 平衡线斜率的管理效力,直接取决于正常操作模式下有效交易量同交易量拒绝模式下交易量的比率。该率值越大,管理效力越高。正因如此,初始有效交易量应明显大于最低可能交易量。
  • “EA 交易”平衡的涨落变更的平均周期,应远大于该控制系统的反应时间。否则,该系统就不能完成平衡曲线斜率的调节。反应时间的平均周期率值越大,系统效力就越大。此要求关系到每一个自动调节系统。

利用面向对象编程于 MQL5 中的实施

我们来编写一个实现上述方法的库。为此,我们要用到 MQL5 的一项新功能 - 面向对象方法。此方法允许您未来轻松开发和扩展我们的库,且无需从头重新编写大部分的代码。

TradeSymbol 类

因为已在新型 MetaTrader 5 平台中实施了多货币测试,所以我们需要一个类,并将带有任何有效交易品种的整个操作封装其中。它允许在多货币“EA 交易”中使用此库。此类不会直接影响到控制系统,它是辅助性的。所以,此类会搭配有效交易品种一同操作。

//---------------------------------------------------------------------
//  工作交易品种的操作:
//---------------------------------------------------------------------
class TradeSymbol
{
private:
  string  trade_symbol;                          // 工作交易品种

private:
  double  min_trade_volume;                      // 交易操作的最小交易量
  double  max_trade_volume;                      // 交易操作的最大交易量
  double  min_trade_volume_step;                 // 最小交易量增量
  double  max_total_volume;                      // 最大交易量增量
  double  symbol_point;                          // 点数大小
  double  symbol_tick_size;                      // 最小价格增量
  int     symbol_digits;                        // 小数点之后位数

protected:

public:
  void    RefreshSymbolInfo( );                  // 刷新工作交易品种的市场情报
  void    SetTradeSymbol( string _symbol );      // 设置/改变 工作交易品种
  string  GetTradeSymbol( );                     // 获取工作交易品种
  double  GetMaxTotalLots( );                    // 获得最大累积量
  double  GetPoints( double _delta );            // 得到价格变化点数

public:
  double  NormalizeLots( double _requied_lot );  // 得到归一化交易量
  double  NormalizePrice( double _org_price );   // 得到归一化的价格,同时考虑报价的逐步变化

public:
  void    TradeSymbol( );                       // 构造器
  void    ~TradeSymbol( );                      // 析构器
};

该类的结构非常简单。目的就是按照某指定交易品种获取、存储并处理当前的市场信息。主要的方法为 TradeSymbol::RefreshSymbolInfoTradeSymbol::NormalizeLots TradeSymbol::NormalizePrice。我们一个一个地来研究。

TradeSymbol::RefreshSymbolInfo 方法旨在按有效交易品种刷新市场信息。

//---------------------------------------------------------------------
//  刷新工作交易品种的市场信息:
//---------------------------------------------------------------------
void
TradeSymbol::RefreshSymbolInfo( )
{
//  如果工作交易品种未设置, 不做任何事:
  if( GetTradeSymbol( ) == NULL )
  {
    return;
  }

//  计算归一化交易量的必要参数:
  min_trade_volume = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_VOLUME_MIN );
  max_trade_volume = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_VOLUME_MAX );
  min_trade_volume_step = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_VOLUME_STEP );

  max_total_volume = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_VOLUME_LIMIT );

  symbol_point = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_POINT );
  symbol_tick_size = SymbolInfoDouble( GetTradeSymbol( ), SYMBOL_TRADE_TICK_SIZE );
  symbol_digits = ( int )SymbolInfoInteger( GetTradeSymbol( ), SYMBOL_DIGITS );
}

注意曾被多种方法用到的一个重要之处。由于当前 MQL5 的实现不允许使用带参数的构造函数,所以,您必须为有效交易品种的初始设置调用下述方法:

void    SetTradeSymbol( string _symbol );      // 设置/改变 工作交易品种

TradeSymbol::NormalizeLots 方法用于获取一个正确的标准化交易量。我们知道,仓位不能小于经纪人所允许的最小可能值。仓位的最小变动幅度亦由经纪人确定,且可能有所区别。此方法会返回距底值最接近的交易量值。

它还会检查假定仓位的交易量是否超过了经纪人所允许的最大值。

//---------------------------------------------------------------------
//  获得标准化的交易量:
//---------------------------------------------------------------------
//  - 输入必要的交易量;
//  - 输出标准化交易量;
//---------------------------------------------------------------------
double
TradeSymbol::NormalizeLots( double _requied_lots )
{
  double   lots, koeff;
  int      nmbr;

//  如果工作交易品种未设置, 不做任何事:
  if( GetTradeSymbol( ) == NULL )
  {
    return( 0.0 );
  }

  if( this.min_trade_volume_step > 0.0 )
  {
    koeff = 1.0 / min_trade_volume_step;
    nmbr = ( int )MathLog10( koeff );
  }
  else
  {
    koeff = 1.0 / min_trade_volume;
    nmbr = 2;
  }
  lots = MathFloor( _requied_lots * koeff ) / koeff;

//  交易量下限:
  if( lots < min_trade_volume )
  {
    lots = min_trade_volume;
  }

//  交易量上限:
  if( lots > max_trade_volume )
  {
    lots = max_trade_volume;
  }

  lots = NormalizeDouble( lots, nmbr );
  return( lots );
}

TradeSymbol::NormalizePrice 方法用于获取正确的标准化价格。由于必须为给定的交易品种确定小数点后的有效位数(价格精度),我们需要删节价格。此外,有些交易品种(比如期货)的价格变化最小幅度大于一个点。因此,我们需要数倍于最小离散值的价格值。

//---------------------------------------------------------------------
//  考虑价格改变步骤的正常价格:
//---------------------------------------------------------------------
double
TradeSymbol::NormalizePrice( double _org_price )
{
//  每步报价的最小点数:
  double  min_price_step = NormalizeDouble( symbol_tick_size / symbol_point, 0 );

  double  norm_price = NormalizeDouble( NormalizeDouble(( NormalizeDouble( _org_price / symbol_point, 0 )) / min_price_step, 0 ) * min_price_step * symbol_point, symbol_digits );
  return( norm_price );
}

必要的非标准化价格已输入该函数。它会返回最接近必要价格的标准化价格。

其它方法的目的详见相关注释,无需进一步讲解。

TBalanceHistory 类

此类旨在操作账户的余额历史,看名称就清楚了。它也是下述许多类的基类。此类的主要目的,就是访问“EA 交易”的交易历史。此外,您可以按有效交易品种、“幻数”、“EA 交易”监控的开始日期(或同时按上述所有三种元素)过滤历史。

//---------------------------------------------------------------------
//  结余历史的操作:
//---------------------------------------------------------------------
class TBalanceHistory
{
private:
  long      current_magic;            // 当存取历史合约时的 "幻数"  ( 0 - 任意数字 )
  long      current_type;             // 合约类型 ( -1 - 全部 )
  int       current_limit_history;   // 历史深度限制 ( 0 - 所有历史 )
  datetime   monitoring_begin_date;   // 监视合约历史的开始时间
  int       real_trades;             // 已执行的实际交易数量

protected:
  TradeSymbol  trade_symbol;          // 工作交易品种的操作

protected:
//  "原始" 数组:
  double    org_datetime_array[ ];                                                                                                                                                      // 交易日期时间
  double    org_result_array[ ];                                                                                                                                                                // 交易结果

//  以时间归组的数据数组:
  double    group_datetime_array[ ];                                                                                                                                            // 交易日期时间
  double    group_result_array[ ];                                                                                                                                                      // 交易结果

  double    last_result_array[ ];     // 保存最后交易结果的数组 ( Y 轴上的点数 )
  double    last_datetime_array[ ];   // 存最后交易时间的数组 ( X 轴上的点数 )

private:
  void      SortMasterSlaveArray( double& _m[ ], double& _s[ ] );  // 同步两个升序数组

public:
  void      SetTradeSymbol( string _symbol );                      // 设置/改变 工作交易品种
  string    GetTradeSymbol( );                                    // 获取工作交易品种
  void      RefreshSymbolInfo( );                                 // 刷新工作交易品种的市场情报
  void      SetMonitoringBeginDate( datetime _dt );                // 设置监视开始时间
  datetime  GetMonitoringBeginDate( );                            // 获取监视开始时间
  void      SetFiltrParams( long _magic, long _type = -1, int _limit = 0 );// 设置合约过滤参数

public:
// 得到最后交易结果:
  int       GetTradeResultsArray( int _max_trades );

public:
  void      TBalanceHistory( );       // 构造器
  void      ~TBalanceHistory( );      // 析构器
};

读取最新交易与历史结果的过滤设置,利用 TBalanceHistory::SetFiltrParams 方法来完成。它具有以下输入参数:

  • _magic           - 应于历史中读取的交易“幻数”。如果指定零值,则带有任何“幻数”的交易都会被读取。
  • _type             - 应被读取的交易类型。可拥有下述值 - DEAL_TYPE_BUY (仅限读取长线交易), DEAL_TYPE_SELL (仅限读取短线交易)以及 -1 (读取长线短线两种交易)。
  • _limit             - 限制被分析交易历史的深度。如其等于零,则所有可用历史均被分析。

默认情况下,创建 TBalanceHistory 类的对象时,会设置下述值:_magic = 0, _type = -1, _limit = 0。

此类的主要方法为 TBalanceHistory::GetTradeResultsArray。该方法旨在利用最后交易的结果,来填充类成员数组 last_result_arraylast_datetime_array。它具有以下输入参数:

  • _max_trades - 应由历史读取并写入输出数组的交易的最大数量。因为我们至少需要两个点才能计算坡度,所以该值不能少于 2。如果该值等于零,则对可用的整个交易历史进行分析。实际上,平衡曲线斜率计算所需点数是在此指定。
//---------------------------------------------------------------------
//  读取数组最近(按时间)交易的结果:
//---------------------------------------------------------------------
//  - 返回实际读取交易数量但不超过指定数量;
//---------------------------------------------------------------------
int
TBalanceHistory::GetTradeResultsArray( int _max_trades )
{
  int       index, limit, count;
  long      deal_type, deal_magic, deal_entry;
  datetime   deal_close_time, current_time;
  ulong     deal_ticket;                        // 合约的单号
  double    trade_result;
  string    symbol, deal_symbol;

  real_trades = 0;

//  交易数量不得少于 2:
  if( _max_trades < 2 )
  {
    return( 0 );
  }

//  如果工作交易品种未指定, 不做任何事:
  symbol = trade_symbol.GetTradeSymbol( );
  if( symbol == NULL )
  {
    return( 0 );
  }

//  请求从指定时间至当前时刻的历史合约与订单:
  if( HistorySelect( monitoring_begin_date, TimeCurrent( )) != true )
  {
    return( 0 );
  }

//  计算交易数量:
  count = HistoryDealsTotal( );

//  如果历史交易数量少于必要数量, 则退出:
  if( count < _max_trades )
  {
    return( 0 );
  }

//  如果历史交易数量多于必要数量, 则限制它们:
  if( current_limit_history > 0 && count > current_limit_history )
  {
    limit = count - current_limit_history;
  }
  else
  {
    limit = 0;
  }

//  如果需要, 调整 "原始" 数组的维度至指定的交易数量:
  if(( ArraySize( org_datetime_array )) != ( count - limit ))
  {
    ArrayResize( org_datetime_array, count - limit );
    ArrayResize( org_result_array, count - limit );
  }

//  以历史交易填充 "原始" 数组:
  real_trades = 0;
  for( index = count - 1; index >= limit; index-- )
  {
    deal_ticket = HistoryDealGetTicket( index );

//  如果合约未平仓, 不要继续:
    deal_entry = HistoryDealGetInteger( deal_ticket, DEAL_ENTRY );
    if( deal_entry != DEAL_ENTRY_OUT )
    {
      continue;
    }

//  检查合约的 "幻数",如果必要:
    deal_magic = HistoryDealGetInteger( deal_ticket, DEAL_MAGIC );
    if( current_magic != 0 && deal_magic != current_magic )
    {
      continue;
    }

//  检查合约交易品种:
    deal_symbol = HistoryDealGetString( deal_ticket, DEAL_SYMBOL );
    if( symbol != deal_symbol )
    {
      continue;
    }
                
//  检查合约类型,如果必要:
    deal_type = HistoryDealGetInteger( deal_ticket, DEAL_TYPE );
    if( current_type != -1 && deal_type != current_type )
    {
      continue;
    }
    else if( current_type == -1 && ( deal_type != DEAL_TYPE_BUY && deal_type != DEAL_TYPE_SELL ))
    {
      continue;
    }
                
//  检查合约平仓时间:
    deal_close_time = ( datetime )HistoryDealGetInteger( deal_ticket, DEAL_TIME );
    if( deal_close_time < monitoring_begin_date )
    {
      continue;
    }

//  所以, 我们可以读其它交易:
    org_datetime_array[ real_trades ] = deal_close_time / 60;
    org_result_array[ real_trades ] = HistoryDealGetDouble( deal_ticket, DEAL_PROFIT ) / HistoryDealGetDouble( deal_ticket, DEAL_VOLUME );
    real_trades++;
  }

//  如果此时少于必要交易数, 返回:
  if( real_trades < _max_trades )
  {
    return( 0 );
  }

  count = real_trades;

//  以订单关闭时间对 "原始" 数组排序:
  SortMasterSlaveArray( org_datetime_array, org_result_array );

// 如果必要, 调整 group 数组的维度至指定的点数:
  if(( ArraySize( group_datetime_array )) != count )
  {
    ArrayResize( group_datetime_array, count );
    ArrayResize( group_result_array, count );
  }
  ArrayInitialize( group_datetime_array, 0.0 );
  ArrayInitialize( group_result_array, 0.0 );

//  以分组数据填充输出数组 ( 平仓时间为标识的分组 ):
  for( index = 0; index < count; index++ )
  {
//  得到其它交易:
    deal_close_time = ( datetime )org_datetime_array[ index ];
    trade_result = org_result_array[ index ];

//  现在检查是否在输出数组中存在时间相同:
    current_time = ( datetime )group_datetime_array[ real_trades ];
    if( current_time > 0 && MathAbs( current_time - deal_close_time ) > 0.0 )
    {
      real_trades++;                      // 移动指针至下一个元素
      group_result_array[ real_trades ] = trade_result;
      group_datetime_array[ real_trades ] = deal_close_time;
    }
    else
    {
      group_result_array[ real_trades ] += trade_result;
      group_datetime_array[ real_trades ] = deal_close_time;
    }
  }
  real_trades++;                          // 现在是唯一元素的数量

//  如果此时少于必要交易数, 退出:
  if( real_trades < _max_trades )
  {
    return( 0 );
  }

  if( ArraySize( last_result_array ) != _max_trades )
  {
    ArrayResize( last_result_array, _max_trades );
    ArrayResize( last_datetime_array, _max_trades );
  }

//  以逆向索引写积累的数据到输出数组:
  for( index = 0; index < _max_trades; index++ )
  {
    last_result_array[ _max_trades - 1 - index ] = group_result_array[ index ];
    last_datetime_array[ _max_trades - 1 - index ] = group_datetime_array[ index ];
  }

//  在输出数组中,以积累的总和取代单个交易的结果:
  for( index = 1; index < _max_trades; index++ )
  {
    last_result_array[ index ] += last_result_array[ index - 1 ];
  }

  return( _max_trades );
}

开始时要执行强制性检查 - 是否已指定有效交易品种,以及输入参数是否正确。

然后,我们再读取从指定日期到当前时刻的交易与订单历史。利用下述部分代码实现:

//  请求从指定时间至当前时刻的历史合约与订单:
  if( HistorySelect( monitoring_begin_date, TimeCurrent( )) != true )
  {
    return( 0 );
  }

//  计算交易数量:
  count = HistoryDealsTotal( );

//  如果历史交易数量少于必要数量, 则退出:
  if( count < _max_trades )
  {
    return( 0 );
  }

此外,还要检查历史交易的总数。如果少于指定数值,则没必要再执行进一步的行动了。“原始”数组一准备好,利用来自交易历史的信息进行填充的周期就马上执行。完成方式如下:

//  以历史交易填充 "原始" 数组:
  real_trades = 0;
  for( index = count - 1; index >= limit; index-- )
  {
    deal_ticket = HistoryDealGetTicket( index );

//  如果交易未关闭, 不要继续:
    deal_entry = HistoryDealGetInteger( deal_ticket, DEAL_ENTRY );
    if( deal_entry != DEAL_ENTRY_OUT )
    {
      continue;
    }

//  检查合约的 "幻数",如果必要:
    deal_magic = HistoryDealGetInteger( deal_ticket, DEAL_MAGIC );
    if( _magic != 0 && deal_magic != _magic )
    {
      continue;
    }

//  检查合约交易品种:
    deal_symbol = HistoryDealGetString( deal_ticket, DEAL_SYMBOL );
    if( symbol != deal_symbol )
    {
      continue;
    }
                
//  检查合约类型,如果必要:
    deal_type = HistoryDealGetInteger( deal_ticket, DEAL_TYPE );
    if( _type != -1 && deal_type != _type )
    {
      continue;
    }
    else if( _type == -1 && ( deal_type != DEAL_TYPE_BUY && deal_type != DEAL_TYPE_SELL ))
    {
      continue;
    }
                
//  检查合约平仓时间:
    deal_close_time = ( datetime )HistoryDealGetInteger( deal_ticket, DEAL_TIME );
    if( deal_close_time < monitoring_begin_date )
    {
      continue;
    }

//  所以, 我们可以读其它交易:
    org_datetime_array[ real_trades ] = deal_close_time / 60;
    org_result_array[ real_trades ] = HistoryDealGetDouble( deal_ticket, DEAL_PROFIT ) / HistoryDealGetDouble( deal_ticket, DEAL_VOLUME );
    real_trades++;
  }

//  如果此时少于必要交易数, 退出:
  if( real_trades < _max_trades )
  {
    return( 0 );
  }

开始时,利用 HistoryDealGetTicket 函数读取历史交易的价格跳动;而进一步的交易详情,则是利用获得的价格跳动来完成。因为我们只对已平仓交易感兴趣(我们要分析平衡),所以首先检查交易类型。为此要调用带有 DEAL_ENTRY 参数的 HistoryDealGetInteger 函数。如果此函数返回 DEAL_ENTRY_OUT,则其为某仓位的收盘。

继交易“幻数”之后检查的是交易的类型(即指定方法的输入参数)和交易的品种。如果交易的所有参数都符合要求,则检查最后一个参数 - 交易收盘的时间。完成方式如下:

//  检查合约平仓时间:
    deal_close_time = ( datetime )HistoryDealGetInteger( deal_ticket, DEAL_TIME );
    if( deal_close_time < monitoring_begin_date )
    {
      continue;
    }

将交易的日期/时间与给定的历史监测开始的日期/时间进行对比。如果交易的日期/时间大于给定的数据,则我们读取到数组的交易 - 读取按点数计的交易结果,以及按分钟计的交易时间(本例中是收盘时间)。此后,读取交易的计数器 real_trades 就会增长,且周期继续。

一旦“原始”数组已填充了必要的信息量,我们就应对存储交易收盘时间的数组进行排序。同时,我们还需要将对应的收盘时间保存于 org_datetime_array 数组,将交易结果保存于 org_result_array 数组。要利用专用的写入方法完成:

TBalanceHistory::SortMasterSlaveArray( double& _master[ ], double& _slave[ ] )。第一个参数是 _master - 按升序排列的一个数组。第二个参数为 _slave - 数组的元素应与第一个数组元素同时移动。排序则通过 "bubble" 方法来实现。

经过所有上述操作之后,我们拥有了两个带时间的数组以及按时间排序的交易结果。因为平衡曲线上对应每个时刻(X 轴上的点)的只有一个点(Y 轴上的点),所以,我们需要将带有同一收盘时间的数组元素(如果有的话)归组。利用下述部分代码执行此操作:

//  以分组数据填充输出数组 ( 平仓时间为标识的分组 ):
  real_trades = 0;
  for( index = 0; index < count; index++ )
  {
//  得到其它交易:
    deal_close_time = ( datetime )org_datetime_array[ index ];
    trade_result = org_result_array[ index ];

//  现在检查是否在输出数组中存在时间相同:
    current_time = ( datetime )group_datetime_array[ real_trades ];
    if( current_time > 0 && MathAbs( current_time - deal_close_time ) > 0.0 )
    {
      real_trades++;                      // 移动指针至下一个元素
      group_result_array[ real_trades ] = trade_result;
      group_datetime_array[ real_trades ] = deal_close_time;
    }
    else
    {
      group_result_array[ real_trades ] += trade_result;
      group_datetime_array[ real_trades ] = deal_close_time;
    }
  }
  real_trades++;                          // 现在是唯一元素的数量

实际上,所有带有“相同”收盘时间的交易都被汇总于此。结果被写入到 TBalanceHistory::group_datetime_array (收盘时间)和 TBalanceHistory::group_result_array (交易结果)数组。此后,我们就得到了两个带有独特元素的排序数组。本例中的时间 ID 被认定为一分钟之内。该转变可以通过图形方式生动说明:

带相同时间的交易分组

图 3. 带相同时间的交易分组

一分钟之内的所有交易(图左侧),都被归入带时间舍入和结果汇总的一个组中。它允许平滑收盘交易的时间“颤振”,并提高调节的稳定性。

此后,您需要再完成所获数组的两次转变。反转元素的顺序,让最早的交易对应零元素;再用累积和(即余额)替换各次交易的结果。利用下述部分代码实现:

//  以逆向索引写积累的数据到输出数组:
  for( index = 0; index < _max_trades; index++ )
  {
    last_result_array[ _max_trades - 1 - index ] = group_result_array[ index ];
    last_datetime_array[ _max_trades - 1 - index ] = group_datetime_array[ index ];
  }

//  在输出数组中,以积累的总和取代单个交易的结果:
  for( index = 1; index < _max_trades; index++ )
  {
    last_result_array[ index ] += last_result_array[ index - 1 ];
  }

TBalanceSlope 类

此类旨在利用某账户的平衡曲线执行操作。此类由 TBalanceHistory 类而来,且继承了其所有的受保护与公共数据及方法。我们仔细研究一下它的结构:

//---------------------------------------------------------------------
//  结余曲线操作:
//---------------------------------------------------------------------
class TBalanceSlope : public TBalanceHistory
{
private:
  double    current_slope;               // 结余曲线的斜率的当前角度
  int       slope_count_points;          // 计算斜率角度的点数 ( 交易 )
        
private:
  double    LR_koeff_A, LR_koeff_B;      // 直线回归方程的比率
  double    LR_points_array[ ];          // 直线回归方程点的数组

private:
  void      CalcLR( double& X[ ], double& Y[ ] );  // 计算直线回归方程

public:
  void      SetSlopePoints( int _number );        // 设置计算斜率角度的点数
  double    CalcSlope( );                         // 计算斜率角度

public:
  void      TBalanceSlope( );                     // 构造器
  void      ~TBalanceSlope( );                    // 析构器
};

我们会通过为平衡曲线上指定的点数(交易量)绘制的线性回归线的坡度,确定平衡曲线的坡度。由此,首先我们需要计算下述形式的直线回归方程:A*x + B. 用下述方法完成这个任务:

//---------------------------------------------------------------------
//  计算直线回归方程式:
//---------------------------------------------------------------------
//  输入参数:
//    X[ ] - X轴数列值;
//    Y[ ] - Y轴数量值;
//---------------------------------------------------------------------
void
TBalanceSlope::CalcLR( double& X[ ], double& Y[ ] )
{
  double    mo_X = 0, mo_Y = 0, var_0 = 0, var_1 = 0;
  int       i;
  int       size = ArraySize( X );
  double    nmb = ( double )size;

//  如果点数小于 2, 则曲线不可计算:
  if( size < 2 )
  {
    return;
  }

  for( i = 0; i < size; i++ )
  {
    mo_X += X[ i ];
    mo_Y += Y[ i ];
  }
  mo_X /= nmb;
  mo_Y /= nmb;

  for( i = 0; i < size; i++ )
  {
    var_0 += ( X[ i ] - mo_X ) * ( Y[ i ] - mo_Y );
    var_1 += ( X[ i ] - mo_X ) * ( X[ i ] - mo_X );
  }

//  A 值的系数:
  if( var_1 != 0.0 )
  {
    LR_koeff_A = var_0 / var_1;
  }
  else
  {
    LR_koeff_A = 0.0;
  }

//  B 值的系数:
  LR_koeff_B = mo_Y - LR_koeff_A * mo_X;

//  填充附在回归线上的点数组:
  ArrayResize( LR_points_array, size );
  for( i = 0; i < size; i++ )
  {
    LR_points_array[ i ] = LR_koeff_A * X[ i ] + LR_koeff_B;
  }
}

这里我们采用最小二乘法来计算回归线位置相对于初始数据的最小误差。存储 Y 坐标(在被计算的线上)的数组亦被填充。此数组并非用于当前,而是着眼于未来开发。

给定类中使用的主要方法为 TBalanceSlope::CalcSlope。它会返回平衡曲线的坡度 - 按指定的最后交易的数量计算。其实现如下:

//---------------------------------------------------------------------
//  计算斜角:
//---------------------------------------------------------------------
double
TBalanceSlope::CalcSlope( )
{
//  从历史交易中取得交易结果:
  int      nmb = GetTradeResultsArray( slope_count_points );
  if( nmb < slope_count_points )
  {
    return( 0.0 );
  }

//  以最后交易结果计算回归线:
  CalcLR( last_datetime_array, last_result_array );
  current_slope = LR_koeff_A;

  return( current_slope );
}

首先,分析指定的平衡曲线最后点数。为此,调用基类的相关方法 TBalanceSlope::GetTradeResultsArray。如果读取点的数量少于指定,则计算回归线。这一动作通过 TBalanceSlope::CalcLR 方法来完成。而上一步填充的 last_result_arraylast_datetime_array 数组(属于基类),则用作自变量。

其余的方法都很简单,也就无需赘述了。

TBalanceSlopeControl 类

它属于基类,会通过修改有效交易量来管理平衡曲线的斜率。此类由 TBalanceSlope 类衍生而来,且继承了其所有的受保护与公共数据及方法。此类的唯一目的,就是根据平衡曲线的当前坡度,计算当前的有效交易量。我们来仔细研究一下:

//---------------------------------------------------------------------
//  管理结余曲线的倾斜率:
//---------------------------------------------------------------------
enum LotsState
{
  LOTS_NORMAL = 1,            // 正常交易量的交易模式
  LOTS_REJECTED = -1,         // 低交易量的交易模式
  LOTS_INTERMEDIATE = 0,      // 中间交易量的交易模式
};
//---------------------------------------------------------------------
class TBalanceSlopeControl : public TBalanceSlope
{
private:
  double    min_slope;          // 对应交易量拒绝模式的倾角
  double    max_slope;          // 对应交易量正常模式的倾角
  double    centr_slope;        // 对应交易量无迟滞切换模式的倾角

private:
  ControlType  control_type;    // 调节功能的类型

private:
  double    rejected_lots;      // 交易量拒绝模式
  double    normal_lots;        // 交易量正常模式
  double    intermed_lots;      // 交易量中间模式

private:
  LotsState current_lots_state; // 当前交易量模式

public:
  void      SetControlType( ControlType _control );  // 设置调节特征类型
  void      SetControlParams( double _min_slope, double _max_slope, double _centr_slope );

public:
  double    CalcTradeLots( double _min_lots, double _max_lots );  // 获取交易量

protected:
  double    CalcIntermediateLots( double _min_lots, double _max_lots, double _slope );

public:
  void      TBalanceSlopeControl( );   // 构造器
  void      ~TBalanceSlopeControl( );  // 析构器
};

在计算当前交易量之前,我们需要设置初始参数。如此则需调用下述方法:

  void      SetControlType( ControlType _control );  // 设置调节特征类型

Input parameter_control - 此为调节特性的类型。它可以拥有下述值:

  • STEP_WITH_HYSTERESISH      - 带有延迟调节的步进特性;
  • STEP_WITHOUT_HYSTERESIS  - 不带有延迟调节的步进特性;
  • LINEAR                                   - 线性调节特性;
  • NON_LINEAR                           - 非线性调节特性(未于本版本中实现);

  void      SetControlParams( double _min_slope, double _max_slope, double _centr_slope );

输入参数如下:

  • _min_slope - 与带有最小交易量的交易对应的平衡曲线坡度;
  • _max_slope - 与带有最大交易量的交易对应的平衡曲线坡度;
  • _centr_slope - 与不带延迟步进调节特性对应的平衡曲线坡度;

交易量利用下述方法计算:

//---------------------------------------------------------------------
//  获得交易量:
//---------------------------------------------------------------------
double
TBalanceSlopeControl::CalcTradeLots( double _min_lots, double _max_lots )
{
//  尝试计算结余曲线的斜率:
  double    current_slope = CalcSlope( );

//  如果指定交易额度不能积累, 以最小的交易量:
  if( GetRealTrades( ) < GetSlopePoints( ))
  {
    current_lots_state = LOTS_REJECTED;
    rejected_lots = trade_symbol.NormalizeLots( _min_lots );
    return( rejected_lots );
  }

// 如果调节功能无迟滞:
  if( control_type == STEP_WITHOUT_HYSTERESIS )
  {
    if( current_slope < centr_slope )
    {
      current_lots_state = LOTS_REJECTED;
      rejected_lots = trade_symbol.NormalizeLots( _min_lots );
      return( rejected_lots );
    }
    else
    {
      current_lots_state = LOTS_NORMAL;
      normal_lots = trade_symbol.NormalizeLots( _max_lots );
      return( normal_lots );
    }
  }

//  如果结余曲线的线形回归斜率小于许可:
  if( current_slope < min_slope )
  {
    current_lots_state = LOTS_REJECTED;
    rejected_lots = trade_symbol.NormalizeLots( _min_lots );
    return( rejected_lots );
  }

//  如果结余曲线的线形回归斜率大于指定:
  if( current_slope > max_slope )
  {
    current_lots_state = LOTS_NORMAL;
    normal_lots = trade_symbol.NormalizeLots( _max_lots );
    return( normal_lots );
  }

//  如果结余曲线的线形回归斜率在边界之内 (中间状态):
  current_lots_state = LOTS_INTERMEDIATE;

//  计算中间交易量的值:
  intermed_lots = CalcIntermediateLots( _min_lots, _max_lots, current_slope );
  intermed_lots = trade_symbol.NormalizeLots( intermed_lots );

  return( intermed_lots );
}

TBalanceSlopeControl::CalcTradeLots 方法实施的主要有效点如下:

  • 直至累积到指定的最小交易量,再以最小交易量交易。这样合情合理,因为在您刚刚设置好的时候,“EA 交易”所处的周期(是否可盈利)还都是未知。
  • 如果是不带延迟步进的调节函数,则通过 TBalanceSlopeControl::SetControlParams 方法设置交易模式之间的切换角度,只能采用 _centr_slope 参数。_min_slope_max_slope 参数被忽略。为此,要通过 MetaTrader 5 策略测试程序中的这个参数来执行正确优化。

根据计算得出的坡度,利用最小、最大或中间交易量执行交易。中间交易量通过一种简单方法计算 - TBalanceSlopeControl::CalcIntermediateLots。此为受保护方法,且于类中使用。其代码显示如下:

//---------------------------------------------------------------------
//  计算中间交易量:
//---------------------------------------------------------------------
double
TBalanceSlopeControl::CalcIntermediateLots( double _min_lots, double _max_lots, double _slope )
{
  double    lots;

// 如果调节功能迟滞:
  if( control_type == STEP_WITH_HYSTERESISH )
  {
    if( current_lots_state == LOTS_REJECTED && _slope > min_slope && _slope < max_slope )
    {
      lots = _min_lots;
    }
    else if( current_lots_state == LOTS_NORMAL && _slope > min_slope && _slope < max_slope )
    {
      lots = _max_lots;
    }
  }
// 如果调节功能是线性:
  else if( control_type == LINEAR )
  {
    double  a = ( _max_lots - _min_lots ) / ( max_slope - min_slope );
    double  b = normal_lots - a * .max_slope;
    lots = a * _slope + b;
  }
// 如果调节功能是非线性 (尚未实现):
  else if( control_type == NON_LINEAR )
  {
    lots = _min_lots;
  }
// 如果调节功能未知:
  else
  {
    lots = _min_lots;
  }

  return( lots );
}

此类的其它方法无需赘述。

将此系统嵌入“EA 交易”示例

我们一步一步地来研究一下“EA 交易”中平衡曲线斜率控制系统的实施过程。


第 1 步 - 添加将已有库连接至“EA 交易”的相关说明:

#include  <BalanceSlopeControl.mqh>


第 2 步 - 添加外部变量,用于“EA 交易”平衡线斜率控制系统的参数设置:

//---------------------------------------------------------------------
//  控制结余曲线倾斜率的系统的参数;
//---------------------------------------------------------------------
enum SetLogic 
{
  No = 0,
  Yes = 1,
};
//---------------------------------------------------------------------
input SetLogic     UseAutoBalanceControl = No;
//---------------------------------------------------------------------
input ControlType  BalanceControlType = STEP_WITHOUT_HYSTERESIS;
//---------------------------------------------------------------------
//  最后交易额度用于计算结余曲线的 LR:
input int          TradesNumberToCalcLR = 3;
//---------------------------------------------------------------------
//  LR 斜率,降低交易量至最小:
input double       LRKoeffForRejectLots = -0.030;
//---------------------------------------------------------------------
//  LR 斜率,恢复正常交易模式:
input double       LRKoeffForRestoreLots = 0.050;
//---------------------------------------------------------------------
//  LR 斜率,工作在中间模式:
input double       LRKoeffForIntermedLots = -0.020;
//---------------------------------------------------------------------
//  降低初始交易量至指定值,当 LR 倾斜向下
input double       RejectedLots = 0.10;
//---------------------------------------------------------------------
//  正常工作交易量,在资金管理为固定交易量模式:
input double       NormalLots = 1.0;

第 3 步 - 将 TBalanceSlopeControl 类型的对象添加到“EA 交易”:

TBalanceSlopeControl  BalanceControl;

可在“EA 交易”的开头、函数定义的前面,添加此声明。

第 4 步 - 将平衡曲线控制系统的初始化代码,添加到“EA 交易”的 OnInit 函数:

//  调整我们控制结余曲线斜率的系统:
  BalanceControl.SetTradeSymbol( Symbol( ));
  BalanceControl.SetControlType( BalanceControlType );
  BalanceControl.SetControlParams( LRKoeffForRejectLots, LRKoeffForRestoreLots, LRKoeffForIntermedLots );
  BalanceControl.SetSlopePoints( TradesNumberToCalcLR );
  BalanceControl.SetFiltrParams( 0, -1, 0 );
  BalanceControl.SetMonitoringBeginDate( 0 );

第 5 步 - 将刷新当前市场信息方法的调用添加到“EA 交易”的 OnTick 函数:

//  刷新市场情报:
  BalanceControl.RefreshSymbolInfo( );

此方法的调用可被添加至 OnTick 函数的开头,或是在有无新柱检查之后(针对带有该检查功能的“EA 交易”)。

第 6 步 - 在开仓代码之前,添加当前交易量计算代码:

  if( UseAutoBalanceControl == Yes )
  {
    current_lots = BalanceControl.CalcTradeLots( RejectedLots, NormalLots );
  }
  else
  {
    current_lots = NormalLots;
  }

如在“EA 交易”中使用了“资金管理系统”,则您不再是写入 NormalLots,而是 TBalanceSlopeControl::CalcTradeLots 方法 - 通过“EA 交易” MM 系统计算的当前交易量。

带有上述内置系统的“EA 交易”测试用 BSCS-TestExpert.mq5 已于本文随附。其操作原理基于 CCI 指标的水平相交。此“EA 交易”仅为测试用途开发,不适于真实账户使用。我们要在 EURUSD 的 H4 时间表 (2008.07.01 - 2010.09.01) 对其进行测试。

我们来分析一下此 EA 的运行结果。斜率控制系统禁用情况下的平衡变化图表如下所示。想要查看,请为 UseAutoBalanceControl 外部参数设置 No

平衡变化初始图表

图 4. 平衡变化初始图表

现在将 UseAutoBalanceControl 外部参数设置为 Yes ,再测试“EA 交易”。您就会得到平衡斜率控制系统已启用的图表。

控制系统已启用的平衡变化图表

图 5. 控制系统已启用的平衡变化图表

您可以看到,上图表(图 4)的大多数周期看起来都像有过削减,而下图表(图 5)则显得扁平。这是我们系统运行的结果。您可以对比“EA 交易”运行的主要参数:

 参数
 UseAutoBalanceControl = No  UseAutoBalanceControl = Yes
总净利润:  18 378.00 17 261.73
获利系数:  1.47 1.81
回收系数:  2.66  3.74
预计获利:  117.81 110.65
 平衡绝对减值: 1 310.50 131.05
 资产净值绝对减值:  1 390.50 514.85
 平衡最大减值:  5 569.50 (5.04%) 3 762.15 (3.35%)
 资产净值最大减值: 6 899.50 (6.19%)
4 609.60 (4.08%)

对比得出的最佳参数会用绿色高亮。利润与预计获利略有减少;这是调节的另一方面,作为有效交易量状态之间切换延迟的一个结果显示。总的来说,“EA 交易”的运行效率有所提升。尤其是减值与利润系数方面的改善。

总结

我发现了改善此系统的几种方式:
  • 在“EA 交易”进入某个不利运行周期时,使用虚拟交易。如此一来,不会对正常的有效交易量造成任何影响。它还能降低减值。
  • 采用更多复杂的算法来确定“EA 交易”运行的当前状态(是否盈利)。比如说,我们可以尝试针对此分析应用一个神经网络。当然,这种情况也需要更进一步的探索。

由此,我们认定了系统的原理和运行结果,从而能够改善“EA 交易”的质量特性。某些情况下,与资金管理系统的联合运行还允许在不增长风险的情况下提高盈利能力。

再提醒您一次: 没有什么辅助性系统能够将您亏损“EA 交易”扭转为盈利“EA 交易”。

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/145

附加的文件 |
bscs-testexpert.mq5 (15.77 KB)
查找错误和记录 查找错误和记录
MetaEditor 5 具备调试功能。但是在编写 MQL5 程序时,您通常都希望不要显示个别的值,而是测试与在线工作期间出现的所有信息。如果日志文件内容庞大,所需信息快速便捷检索自动化的重要性就显而易见了。本文中,我们会研究 MQL5 程序中查找错误的方式以及记录方法。我们也会简单地记录到文件中,并了解一款方便日志查看的简单程序 - LogMon。
通过指定的幻数计算总持仓量的最佳方法 通过指定的幻数计算总持仓量的最佳方法
本文探讨了与指定交易品种和幻数有关的总持仓量的计算问题。所提议的方法仅请求交易历史记录的最少必要部分,在总持仓量等于零时查找最接近的时间,并用最新的交易进行计算。还考虑了客户端全局变量的处理。
New Bar (新柱)事件处理程序 New Bar (新柱)事件处理程序
MQL5 编程语言处理问题的能力已达到一个全新的水平。即便是那些已有此类解决方案的任务,也因为面向对象编程而进阶到一个更高的水平。本文中,我们会举一个检查图表中新柱的特别简单的例子,而且,它已经转化成为一种相当强大且用途多样的工具。什么工具?到文中找答案吧。
一个使用命名管道在 MetaTrader 5 客户端之间进行通信的无 DLL 解决方案 一个使用命名管道在 MetaTrader 5 客户端之间进行通信的无 DLL 解决方案
本文说明如何使用命名管道在 MetaTrader 5 客户端之间实施进程间通信。为使用命名管道而开发了 CNamedPipes 类。为了测试其使用以及测量连接吞吐能力,提供了价格变动指标、服务器和客户端脚本。命名管道的使用足以应对实时报价。