自适应交易系统以及它们在 MetaTrader 5 客户端中的运用·综合运用
📘

自适应交易系统以及它们在 MetaTrader 5 客户端中的运用·综合运用

第 3/3 篇

把策略封装成类:以均线为例看虚拟交易骨架

在自适应 EA 里,每一种交易逻辑都应该收进一个独立的策略类,而不是把判断写死在主循环。CStrategyMA 就是典型样本:它继承自 CSampleStrategy,专门承载「按移动平均线做虚拟交易」的全部功能。 受保护段只放三样东西——m_handle 存 iMA 指标句柄,m_period 存均线周期,m_values[] 用来在 CheckTradeConditions 里接指标实时值。公共段则暴露三个函数:Initialization 建指标、Deinitialization 释放句柄、CheckTradeConditions 出信号。 信号机制很直白:CheckTradeConditions 根据价格与指标状态算出 new_state,再用 GetSignalState 取虚拟仓当前状态;两者不一致就调父类的 SetSignalState 纠正。可传的四类信号是 SIGNAL_OPEN_LONG、SIGNAL_OPEN_SHORT、SIGNAL_CLOSE_LONG、SIGNAL_CLOSE_SHORT。外汇与贵金属杠杆高,虚拟信号仅用于策略筛选,实盘前务必在 MT5 用历史数据回测验证。 CStrategyStoch 的结构和它几乎一样,唯一差别是指标换成 iStochastic、初始化参数不同、信号逻辑跟着改。想把自己的策略塞进自适应框架,就照这个模板重写类,再丢进 m_all_strategies 容器即可。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                  CStrategyMA.mqh |
class=class="str">"cmt">//|                 Copyright class="num">2010, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                      [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2010, MetaQuotes Software Corp."
class="macro">#class="kw">property link        "[MQL5官方文档]
class="macro">#include <CSampleStrategy.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CStrategyMA 类实现了根据移动平均策略       |
class=class="str">"cmt">//| 进行虚拟交易                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CStrategyMA : class="kw">public CSampleStrategy
  {
class="kw">protected:
   class="type">int               m_handle;     class=class="str">"cmt">// 移动平均 (iMA) 指标的句柄
   class="type">int               m_period;     class=class="str">"cmt">// 移动平均指标的周期
   class="type">class="kw">double            m_values[];   class=class="str">"cmt">// 保存指标值的数组
class="kw">public:
   class=class="str">"cmt">// 初始化策略
   class="type">int               Initialization(class="type">int period,class="type">bool virtual_trade_flag);
   class=class="str">"cmt">// 去初始化策略
   class="type">int               Deinitialization();
   class=class="str">"cmt">// 检查交易条件并开启虚拟仓位
   class="type">bool             CheckTradeConditions();
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 策略初始化方法                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CStrategyMA::Initialization(class="type">int period,class="type">bool virtual_trade_flag)
  {
   class=class="str">"cmt">// 设置移动平均周期
   m_period=period;
   class=class="str">"cmt">// 设置虚拟交易标志
   SetVirtualTradeFlag(virtual_trade_flag);
   class=class="str">"cmt">//设置数组索引方式为时间序列
   ArraySetAsSeries(m_rates,true);
   ArraySetAsSeries(m_values,true);
     class=class="str">"cmt">//创建指标获得句柄
   m_handle=iMA(_Symbol,_Period,m_period,class="num">0,MODE_EMA,PRICE_CLOSE);
   if(m_handle<class="num">0)
     {
      Alert("创建 MA 指标错误 - 错误代码: ",GetLastError(),"!!");

「双条件过滤的均线信号判定」

策略在去初始化时先平掉虚拟仓位再释放指标句柄,避免 MT5 退出时残留对象占用内存。 交易条件检查从最近 3 根 K 线入手:CopyRates 取 0~3 根历史,CopyBuffer 取均线缓冲 0~3 点。若任一复制失败直接弹窗报错并返回 false,实盘里这能帮你快速定位是品种停盘还是句柄失效。 买入需同时满足 MA 三线向上(m_values[0]>m_values[1]>m_values[2])且前一根收盘价高于对应 MA;卖出对称反向。仅当双条件齐备才置 SIGNAL_OPEN,若已有持仓则只要触发反向单条件就发平仓信号——这种「开仓严、平仓宽」的逻辑在震荡市可能减少来回打脸。 外汇与贵金属杠杆高,虚拟仓位转实盘前务必在策略测试器用 2020—2023 年数据跑一遍,观察信号翻转频率再调参。

MQL5 / C++
class="type">int CStrategyMA::Deinitialization()
  {
   Position_CloseOpenedPosition(m_position);
   IndicatorRelease(m_handle);
   class="kw">return(class="num">0);
  };
class="type">bool CStrategyMA::CheckTradeConditions()
  {
   RecalcPositionProperties(m_position);
   class="type">class="kw">double p_close;
   if(CopyRates(_Symbol,_Period,class="num">0,class="num">3,m_rates)<class="num">0)
    {
     Alert("复制历史数据错误 - 错误:",GetLastError(),"!!");
     class="kw">return(false);
    }
   p_close=m_rates[class="num">1].close;
   if(CopyBuffer(m_handle,class="num">0,class="num">0,class="num">3,m_values)<class="num">0)
    {
     Alert("复制移动平均指标缓冲区错误 - 错误代码:",GetLastError());
     class="kw">return(false);
    }
   class="type">bool buy_condition_1=(m_values[class="num">0]>m_values[class="num">1]) && (m_values[class="num">1]>m_values[class="num">2]);
   class="type">bool buy_condition_2=(p_close>m_values[class="num">1]);
   class="type">bool sell_condition_1=(m_values[class="num">0]<m_values[class="num">1]) && (m_values[class="num">1]<m_values[class="num">2]);
   class="type">bool sell_condition_2=(p_close<m_values[class="num">1]);
   class="type">int new_state=class="num">0;
   if(buy_condition_1  &&  buy_condition_2) new_state=SIGNAL_OPEN_LONG;
   if(sell_condition_1 && sell_condition_2) new_state=SIGNAL_OPEN_SHORT;
   if((GetSignalState()==SIGNAL_OPEN_SHORT) && (buy_condition_1 || buy_condition_2)) new_state=SIGNAL_CLOSE_SHORT;
   if((GetSignalState()==SIGNAL_OPEN_LONG) && (sell_condition_1 || sell_condition_2)) new_state=SIGNAL_CLOSE_LONG;
   if(GetSignalState()!=new_state)
    {
     SetSignalState(new_state);
    }
   class="kw">return(true);
  };

◍ 用随机振荡指标搭一个虚拟交易类

在 MT5 里做策略原型,最省事的办法是先继承一个通用策略基类,把指标逻辑塞进子类。下面这段把随机振荡指标(Stochastic)封装成 CStrategyStoch,专门用来跑虚拟交易,不碰真实账户资金,外汇和贵金属品种都可加载,但两者杠杆高、滑点跳空频繁,虚拟回测和实盘表现可能有明显偏差。 类里先声明了几个核心成员:m_handle 存 iStochastic 的句柄,m_period_k / m_period_d / m_period_slowing 分别对应 %K 周期、%D 平滑、慢速平滑;m_main_line 和 m_signal_line 两个数组接指标主线与信号线数值。初始化时由外部传入三个周期参数和是否虚拟交易的开关。 Initialization 方法里先把周期和虚拟标志写进成员,随后用 ArraySetAsSeries 把价格数组与指标数组都设为时间序列(最新柱在索引 0)。接着调用 iStochastic(_Symbol,_Period, m_period_k,m_period_d,m_period_slowing, MODE_SMA, STO_LOWHIGH) 拿句柄,若返回负值就弹 Alert 并报 GetLastError() 后退出。 Deinitialization 负责平仓并释放句柄:先 Position_CloseOpenedPosition 清掉虚拟仓位,再 IndicatorRelease(m_handle) 交还系统资源。CheckTradeConditions 是策略主循环入口,留了注释位等具体交叉判断逻辑填充。 开 MT5 按这个结构建个 mqh,把周期设成 5/3/3 加载到 XAUUSD 的 M15 上跑虚拟单,能直观看到随机指标交叉在贵金属上的触发密度。

MQL5 / C++
class="macro">#include <CSampleStrategy.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CStrategyStoch 根据随机振荡指标的交叉线     |
class=class="str">"cmt">//| 策略进行虚拟交易   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CStrategyStoch : class="kw">public CSampleStrategy
  {
class="kw">protected:
   class="type">int                m_handle;             class=class="str">"cmt">// 随机振荡指标的句柄 (iStochastic)
   class="type">int                m_period_k;           class=class="str">"cmt">// K-周期 (用于计算的柱数)
   class="type">int                m_period_d;           class=class="str">"cmt">// D-周期 (首要平滑周期)
   class="type">int                m_period_slowing;     class=class="str">"cmt">// 最终平滑
   class="type">class="kw">double             m_main_line[];        class=class="str">"cmt">// 保存指标值的数组
   class="type">class="kw">double             m_signal_line[];      class=class="str">"cmt">// 保存指标值的数组
class="kw">public:
   class=class="str">"cmt">// 初始化策略
   class="type">int                Initialization(class="type">int period_k,class="type">int period_d,class="type">int period_slowing,class="type">bool virtual_trade_flag);
   class=class="str">"cmt">// 去初始化策略
   class="type">int                Deinitialization();
   class=class="str">"cmt">// 检查交易策略,开启虚拟仓位
   class="type">bool               CheckTradeConditions();
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 策略初始化方法                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CStrategyStoch::Initialization(class="type">int period_k,class="type">int period_d,class="type">int period_slowing,class="type">bool virtual_trade_flag)
  {
  class=class="str">"cmt">// 设置振荡指标周期
   m_period_k=period_k;
   m_period_d=period_d;
   m_period_slowing=period_slowing;
  class=class="str">"cmt">// 设置虚拟交易标志
   SetVirtualTradeFlag(virtual_trade_flag);
  class=class="str">"cmt">// 设置数组索引方式为时间序列
   ArraySetAsSeries(m_rates,true);
   ArraySetAsSeries(m_main_line,true);
   ArraySetAsSeries(m_signal_line,true);
  class=class="str">"cmt">// 创建指标获得句柄
   m_handle=iStochastic(_Symbol,_Period,m_period_k,m_period_d,m_period_slowing,MODE_SMA,STO_LOWHIGH);
   if(m_handle<class="num">0)
     {
      Alert("创建随机振荡指标错误 - 错误代码: ",GetLastError(),"!!");
      class="kw">return(-class="num">1);
     }
   class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 策略去初始化方法                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CStrategyStoch::Deinitialization()
  {
  class=class="str">"cmt">// 关闭所有开启仓位
   Position_CloseOpenedPosition(m_position);
  class=class="str">"cmt">// 释放指标句柄
   IndicatorRelease(m_handle);
   class="kw">return(class="num">0);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查交易条件开启虚拟仓位       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CStrategyStoch::CheckTradeConditions()
  {
  class=class="str">"cmt">// 调用函数重新计算仓位属性

用前三根柱判随机指标穿线信号

这段逻辑只抓最近 3 根柱的微观交叉,不做长周期拟合。先通过 CopyRates 拉取当前品种、当前周期、从 0 偏移共 3 根柱的数据进 m_rates,失败直接弹 Alert 并返回 false,避免脏数据继续跑。 紧接着用 CopyBuffer 分别取随机振荡指标主线(0 号缓冲)与信号线(1 号缓冲)各 3 个值。任一缓冲区复制不到 3 个点也立刻报错退出——这是实盘里最常见的「指标句柄未就绪」坑,贵金属跳空时尤其容易触发。 穿线判定只看索引 2 和 1:buy_condition 要求前前柱信号线低于主线、前一根反转为信号线高于主线,即自下而上金叉;sell_condition 反之。注意这里没有用柱 0(最新未收线柱),只用已收盘的柱 1 与柱 2,回测与直播信号偏差更小。 状态机部分把穿线结果映射到开平:空仓遇买条件置 SIGNAL_OPEN_LONG,遇卖条件置 SIGNAL_OPEN_SHORT;若已有反向持仓则切到平仓状态。最后仅当新状态与 GetSignalState() 不同才调用 SetSignalState 写一次,减少冗余刷新。外汇与贵金属杠杆高,这类微观交叉在震荡市可能频繁假突破,建议拿到 MT5 用 EURUSD 的 M15 先跑一周验证触发频率。

MQL5 / C++
  RecalcPositionProperties(m_position);
  class="type">class="kw">double p_close;
 class=class="str">"cmt">// 获得最后3个柱的历史数据 
  if(CopyRates(_Symbol,_Period,class="num">0,class="num">3,m_rates)<class="num">0)
    {
      Alert("复制历史数据错误 - 错误:",GetLastError(),"!!");
      class="kw">return(false);
    }
 class=class="str">"cmt">// 复制前一个柱的收盘价 (柱 class="num">1)
  p_close=m_rates[class="num">1].close; class=class="str">"cmt">// 前1柱的收盘价        
  if((CopyBuffer(m_handle,class="num">0,class="num">0,class="num">3,m_main_line)<class="num">3) || (CopyBuffer(m_handle,class="num">1,class="num">0,class="num">3,m_signal_line)<class="num">3))
    {
      Alert("复制随机振荡指标缓冲区错误 - 错误代码:",GetLastError());
      class="kw">return(false);
    }
  class=class="str">"cmt">// 买入条件: 从下往上主线穿过信号线
  class="type">bool buy_condition=((m_signal_line[class="num">2]<m_main_line[class="num">2]) && (m_signal_line[class="num">1]>m_main_line[class="num">1]));
  class=class="str">"cmt">// 卖出条件: 从上往下主线穿过信号线
  class="type">bool sell_condition=((m_signal_line[class="num">2]>m_main_line[class="num">2]) && (m_signal_line[class="num">1]<m_main_line[class="num">1]));
  class="type">int new_state=class="num">0;
  if(buy_condition) new_state=SIGNAL_OPEN_LONG;
  if(sell_condition) new_state=SIGNAL_OPEN_SHORT;
  if((GetSignalState()==SIGNAL_OPEN_SHORT) && (buy_condition)) new_state=SIGNAL_CLOSE_SHORT;
  if((GetSignalState()==SIGNAL_OPEN_LONG) && (sell_condition)) new_state=SIGNAL_CLOSE_LONG;
  if(GetSignalState()!=new_state)
    {
      SetSignalState(new_state);
    }
  class="kw">return(true);
};

「反趋势镜像与最差策略反转的实测边界」

横盘段里均线系统会集体失效。原文图3显示,1月8日至1月20日无趋势期间,基于MA_3到MA_93的10个趋势策略全部出现虚拟亏损,自适应系统因此直接停手。 补上10个CStrategyMAinv类实例——信号条件相同,但SIGNAL_OPEN_LONG/SHORT与SIGNAL_CLOSE_LONG/SHORT互换位置,相当于把原策略做镜像。图5的净值曲线表明,原趋势策略吃亏的那段,反趋势实例在开局就兜住了不该发生的亏损;图6进一步标出系统实际切到反趋势信号的时段。 别把正态当圣经:镜像法能跑通,不是因为反趋势本身安全,而是自适应容器里同时养了两套互搏的逻辑,市场自己挑当时有效的那套。外汇与贵金属杠杆高,这种互搏结构只降低空窗期磨损,不消除单边反转爆仓风险。 把最差策略信号反转未必有用。把CAdaptiveStrategy里Expert_OnTick()的ArrayMaximum换成ArrayMinimum,BestDirection乘-1,再注释掉亏损时禁开仓的限制,图7给出结果:最差策略多为MA_3(周期3),其曲线和自适应净值呈反向,但系统整体财务结果平淡。复制并反转最差信号,没有带来有效性改进。 堆几百条均线更是坑。下面代码把循环扩到100,周期从3起每步+10,全塞进m_all_strategies;闭合均线必然反复交叉,领先者不停换,系统频繁切换状态。策略测试器“结果”选项卡比对能看出特征变差,所以别拿带平仓参数的多周期均线群去搭自适应底座。

MQL5 / C++
class=class="str">"cmt">//if((Performance[BestStrategyIndex]<class="num">0) && (RealPositionDirection()==class="num">0)) {class="kw">return;}
  for(class="type">int i=class="num">0;i<class="num">100;i++)
    {
      CStrategyMA *t_StrategyMA;
      t_StrategyMA=new CStrategyMA;
      if(t_StrategyMA==NULL)
        {
         class="kw">delete m_all_strategies;
         Print("创建 CStrategyMA 类型对象错误");
         class="kw">return(-class="num">1);
        }
      class=class="str">"cmt">//设置每个策略的周期
      class="type">int period=class="num">3+i*class="num">10;
      class=class="str">"cmt">// 初始化策略
      t_StrategyMA.Initialization(period,true);
      class=class="str">"cmt">// 设置策略详细内容
      t_StrategyMA.SetStrategyInfo(_Symbol,"[MA_"+IntegerToString(period)+"]",period,"Moving Averages "+IntegerToString(period));
      class=class="str">"cmt">//把策略对象添加到 m_all_strategies 数组中
      m_all_strategies.Add(t_StrategyMA);
    }

◍ 自适应EA的仓位与绩效调校落点

m_all_strategies 容器能塞进上千个策略实例,参数组合随便加;但历史回测里我们统一用 0.1 手跑(类代码里也是写死的),想拿真金白银去拼自动交易擂台,资金管理系统才是分水岭。 CSampleStrategy 留了虚函数 MoneyManagement_CalculateLots,仓位管理可以读 m_deals_history[] 里的虚拟成交统计。比如上笔虚拟单盈利就加倍下注、亏损就缩量,改返回值即可,不用动框架。 StrategyPerformance() 算策略效率,原版只取 m_virtual_equity - m_initial_balance。想更狠可叠 entry_eff / exit_eff / trade_eff,或直接用最近三次 pos_Profit 均值——这些字段在虚拟交易时自动记。注意这函数只在 CSampleStrategy 改,所有自适应策略必须共用同一套评判。 止损靠 Set_Stops 设虚拟止损点,触发自动平虚拟仓;文末 2.2 的均线类里这行被注释了,要测就取消注释。系统跑偏时调 SetVirtualCumulativeProfit(0) 配合 CloseVirtualPosition() 周期归零,例如每 N 根 K 线清一次。 USDJPY H1 上 2010.1.4–8.20 的图显示:前十策略里若没一个盈利,自适应也救不了;起步阶段所有策略齐亏、EA 停手,等某个变领先才切过去。建议 CAdaptiveStrategy::Expert_OnTick 里加根柱计数器,前若干根禁止实盘,躲开选策略的混乱期。外汇贵金属杠杆高,自适应只是概率择优,不是免死金牌。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 此函数返回策略推荐的交易量                    |  
class=class="str">"cmt">//| 当前交易量作为参数传入                        |  
class=class="str">"cmt">//| 交易量可能依赖于:                             |  
class=class="str">"cmt">//| current m_virtual_balance 和 m_virtual_equity           |  
class=class="str">"cmt">//| 当前交易的统计 (位于 m_deals_history 中)         |  
class=class="str">"cmt">//| 或者您想要的任何其他事情                                      |  
class=class="str">"cmt">//| 如果一个策略不需要修改交易量                  |  
class=class="str">"cmt">//| 您可以返回传入的交易量: class="kw">return(trade_volume); |  
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class="type">class="kw">double CSampleStrategy::MoneyManagement_CalculateLots(class="type">class="kw">double trade_volume)
  {
  class=class="str">"cmt">//返回我们收到的值 
  class="kw">return(trade_volume);
  }
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class=class="str">"cmt">//| StrategyPerformance 函数- 策略效率的函数 |
class=class="str">"cmt">//+-----------------------------------------------------------------------+ 
class="type">class="kw">double CSampleStrategy::StrategyPerformance()
  {
 class=class="str">"cmt">//返回策略效率
 class=class="str">"cmt">//本案中是当前时刻净值和初始余额
 class=class="str">"cmt">//之间的数量差异, 
 class=class="str">"cmt">//也就是通过策略获得的资产
  class="type">class="kw">double performance=(m_virtual_equity-m_initial_balance);
  class="kw">return(performance);
  }
class="type">class="kw">double CSampleStrategy::StrategyPerformance()
  {
 class=class="str">"cmt">//如果有交易,把最后三次结果相乘
  if(m_virtual_deals_total>class="num">0)
    {
      class="type">int avdeals=MathRound(MathMin(class="num">3,m_virtual_deals_total));
      class="type">class="kw">double sumprofit=class="num">0;
      for(class="type">int j=class="num">0; j<avdeals; j++)
        {
         sumprofit+=m_deals_history[m_virtual_deals_total-class="num">1-j].profit;
        }
     class="type">class="kw">double performance=sumprofit/avdeals;
    }
    class="kw">return(performance);
  }
class=class="str">"cmt">// 设置累计"虚拟利润"的值
class="type">void              SetVirtualCumulativeProfit(class="type">class="kw">double cumulative_profit) { m_virtual_cumulative_profit=cumulative_perofit; };
class=class="str">"cmt">//关闭所有的虚拟持仓
class="type">void              CloseVirtualPosition();

把虚拟策略跑成实盘之前

这套思路的核心是让成百上千个策略在容器内各自跑虚拟仓位,实盘只跟当时最赚钱的那条信号走。MQL5 标准库里的数据类与交易类把架构压得很薄,扩展新策略基本就是挂个类的事,不用动主循环。 调试版压缩包(12.78 KB)会在运行期吐出文本报告,记录每个策略虚拟净值曲线——你下载后在 MT5 策略测试器里跑一遍,就能看见哪些分支长期向下弯。外汇和贵金属杠杆高,虚拟胜出策略切换实盘时滑点和佣金可能吃掉大部分优势,概率上未必比单策略稳。 有交易者从 2015 年就用移动窗口替代每根 K 线重选,结果比原文更差;他提到要把交易结果拆成「正向 / 仅亏佣金 / 亏超佣金」三类,只有第三类才值得反转逻辑。你若想验证,先开个模拟账户把调试报告拉满,再谈真金白银。

常见问题

先写一个均线取值方法,再用虚拟下单函数记录开平仓点与盈亏,不真实发单即可在回测里看信号骨架是否站得住。
价格穿均线且另一周期均线同向时才算触发,单独穿线容易假突破,建议用两根不同周期均线交叉确认。
小布可读取你写的信号逻辑,自动跑历史品种页诊断并标出穿线误报区,省去手动逐根看柱。
仅看前三根能滤掉部分毛刺,但遇震荡仍会连续触发,最好叠加重仓方向或波动率门槛再上。
在单边市里镜像反转最大回撤常超30%,仅适合区间品种低仓位试,外汇贵金属高风险须严控手数。