在图表上快速检验交易理念·综合运用
🔬

在图表上快速检验交易理念·综合运用

(3/3)·从 451 个参赛 EA 的说明里挑出真信号,用现成框架在图表上 10 分钟验完一个念头

案例拆解新手友好 第 3/3 篇

翻 ATC 2012 的 451 个 EA,只有 316 个写了像样的说明,剩下不是问候外星人就是夸自己。指标堆得最猛的是移动平均线 75 次、MACD 54 次,但跟随套路扎堆不代表有效。新手常把「别人用的多」当「该信」,跳过自己验这一步就直接上参数。

余额计算器的骨架与初始化

在 MT5 里做策略回测或实盘推演时,先把账户余额变动的逻辑封装成一个类往往更干净。下面这段 CBaseBalanceCalculator 的构造与清理代码,就是这类封装的底座,负责把点值、持仓量、持仓价这些状态归零,避免跨品种或跨周期调用时脏数据拖累计算。 构造函数允许传入 symbol_name,为空时默认取当前图表品种的点值(SYMBOL_POINT)。外汇与贵金属点值差异很大,比如 XAUUSD 一点可能是 0.01,而 EURUSD 常见也是 0.00001 级别,用 SymbolInfoDouble 动态取才能保住跨品种复用性。 Cleanup 只做两件事:把 m_position_volume 和 m_position_price 置 0。别小看这一步,很多自行写的平衡计算在反复 Calculate 时爆掉,根因就是上一次持仓状态没清。 Calculate 的入口先 ZeroMemory(m_results),把输出结构体整块擦干净,再读入 _prev_balance、_signal、_next_open、_next_spread 四个参数。注意外汇和贵金属杠杆高、点差跳变频繁,_next_spread 若用实时值而非历史tick,回测结果可能偏向乐观。

MQL5 / C++
class="kw">const class="type">int    _signal,
class="kw">const class="type">class="kw">double _next_open,
class="kw">const class="type">class="kw">double _next_spread );
      };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CBaseBalanceCalculator                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CBaseBalanceCalculator::CBaseBalanceCalculator(class="type">class="kw">string symbol_name = "")
  {
  class=class="str">"cmt">// Clean up state variables
  Cleanup();

  class=class="str">"cmt">// Define point size(because we will calculate the profit in points)
  if (symbol_name == "")
     m_symbol_points = SymbolInfoDouble(Symbol(), SYMBOL_POINT);
  else
     m_symbol_points = SymbolInfoDouble(symbol_name, SYMBOL_POINT);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Cleanup                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Clean up data on positions and prices                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CBaseBalanceCalculator::Cleanup()
  {
  m_position_volume = class="num">0;
  m_position_price = class="num">0;  
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Main calculation block                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
BalanceResults CBaseBalanceCalculator::Calculate(
                              class="kw">const class="type">class="kw">double _prev_balance,
                              class="kw">const class="type">int _signal,
                              class="kw">const class="type">class="kw">double _next_open,
                              class="kw">const class="type">class="kw">double _next_spread )
  {
  class=class="str">"cmt">// Clean up the output structure from the previous values
  ZeroMemory(m_results);

  class=class="str">"cmt">// Initialize additional variables

「信号驱动下的仓位与余额重算逻辑」

回测框架里,每一根 K 线收完都要根据信号重写账户状态。信号为 0 时什么都不做,直接把上一根的平衡值搬过来,这是避免无谓成交的基础设定。 当信号方向与已有仓位同向、或当前还没有仓位时,逻辑开始分叉。已有仓位就忽略信号,余额不变;空仓时若收到 +1 买信号,用 _next_open 加 spread 点算出 ask 价,把新 1 手均价并入 m_position_price,volume 加 1;收到 -1 卖信号则按 bid 价算,volume 减 1,均价公式符号反转。 若已有仓位却收到反向信号,就先平仓再视情况开新仓。以平空为例,用 ask 价算 profit = (current_price - m_position_price) / m_symbol_points * m_position_volume,余额加上该值;若信号恰为 +1 则立刻以 current_price 开 1 手多单,否则仓位清零。外汇与贵金属杠杆高,这套重算若 spread 或 point 取值错,回测权益曲线会系统性偏离实盘。 下面这段是核心分支的原文,可逐行对照 MT5 策略测试器的 OnTester 重写: double current_price = 0; // 当前价,买按ask卖按bid double profit = 0; // 利润暂存 // 无信号则余额沿用 if (_signal == 0) m_results.balance = _prev_balance; // 信号与仓位同向或空仓 else if (_signal * m_position_volume >= 0) { // 已有仓位,忽略信号 if (m_position_volume != 0) m_results.balance = _prev_balance; // 空仓且买信号 else if (_signal == 1) { current_price = _next_open + _next_spread * m_symbol_points; m_position_price = (m_position_volume * m_position_price + current_price) / (m_position_volume + 1); m_position_volume = m_position_volume + 1; m_results.balance = _prev_balance; } // 空仓且卖信号 else if (_signal == -1) { current_price = _next_open; m_position_price = (-m_position_volume * m_position_price + current_price) / (-m_position_volume + 1); m_position_volume = m_position_volume - 1; m_results.balance = _prev_balance; } else m_results.balance = _prev_balance; } // 已有仓位且收到反向信号 else { // 买信号:平空 if (_signal > 0) { current_price = _next_open + _next_spread * m_symbol_points; profit = (current_price - m_position_price) / m_symbol_points * m_position_volume; m_results.balance = _prev_balance + profit; if (_signal == 1) { m_position_price = current_price; m_position_volume = 1; } else m_position_volume = 0; } // 卖信号:平多 else { current_price = _next_open; profit = (current_price - m_position_price) / m_symbol_points * m_position_volume; m_results.balance = _prev_balance + profit;

MQL5 / C++
  class="type">class="kw">double current_price = class="num">0; class=class="str">"cmt">// current price(bid or ask depending on position direction)
  class="type">class="kw">double profit = class="num">0;          class=class="str">"cmt">// profit calculated value

  class=class="str">"cmt">// If there was no signal, the balance remains the same 
  if (_signal == class="num">0)
     m_results.balance = _prev_balance;
  class=class="str">"cmt">// the signal coincides with the direction or no positions are opened yet
  else if (_signal * m_position_volume >= class="num">0)
     {
     class=class="str">"cmt">// Position already exists, the signal is ignored
     if (m_position_volume != class="num">0)
       class=class="str">"cmt">// Balance is not changed
       m_results.balance = _prev_balance;
     class=class="str">"cmt">// No positions yet, buy signal
     else if (_signal == class="num">1)
      {
      class=class="str">"cmt">// Calculate current ASK price, recalculate price, volume and balance
      current_price = _next_open + _next_spread * m_symbol_points;
      m_position_price = (m_position_volume * m_position_price + current_price) / (m_position_volume + class="num">1);
      m_position_volume = m_position_volume + class="num">1;
      m_results.balance = _prev_balance;
      }
     class=class="str">"cmt">// No positions yet, sell signal
     else if (_signal == -class="num">1)
      {
      class=class="str">"cmt">// Calculate current BID price, recalculate price, volume and balance
      current_price = _next_open;
      m_position_price = (-m_position_volume * m_position_price + current_price) / (-m_position_volume + class="num">1);
      m_position_volume = m_position_volume - class="num">1;
      m_results.balance = _prev_balance;      
      }
     else
       m_results.balance = _prev_balance;
     }
  class=class="str">"cmt">// Position is set already, the opposite direction signal is received
  else
     {
     class=class="str">"cmt">// buy signal/close sell position
     if (_signal > class="num">0)
      {
      class=class="str">"cmt">// Close position by ASK price, recalculate profit and balance
      current_price = _next_open + _next_spread * m_symbol_points;
      profit = (current_price - m_position_price) / m_symbol_points * m_position_volume;
      m_results.balance = _prev_balance + profit;
      
      class=class="str">"cmt">// If there is a signal for opening a new position, open it at once
      if (_signal == class="num">1)
        {
        m_position_price = current_price;
        m_position_volume = class="num">1;
        }
      else
        m_position_volume = class="num">0;
      }
     class=class="str">"cmt">// sell signal/close buy position
     else
      {
      class=class="str">"cmt">// Close position by BID price, recalculate profit and balance
      current_price = _next_open;
      profit = (current_price - m_position_price) / m_symbol_points * m_position_volume;
      m_results.balance = _prev_balance + profit;

◍ 成交信号触发与权益重算的逻辑落点

当内部信号变量 _signal 等于 -1 时,策略视为出现反向建仓触发,立即把当前价赋给 m_position_price,并将持仓手数标记为 -1;其他情况下持仓手数归零,意味着本根 K 线不保留敞口。这段处理决定了下一根开盘价介入的占位,而不是市价追单。 权益计算分三种状态:持多时用 _next_open 减开仓价,持空时还要加上 _next_spread * m_symbol_points 的滑点成本,两者都除以点值后乘手数得到浮动 profit,再加进 balance 得到 equity;空仓时 equity 直接等于 balance。外汇与贵金属点值随合约而异,跑之前先确认 m_symbol_points 在你的品种上不是 0。 下面的指标文件用 4 个 buffer、3 条 plot 把 balance 画成蓝红点状直方图、equity 画成实线、零轴画成灰点线,并 include 了 BalanceClass.mqh。把这两个文件放同目录编译,才能在子窗口看到权益曲线脱离余额的回撤幅度。

MQL5 / C++
class=class="str">"cmt">// If there is a signal for opening a new position, open it at once
if (_signal == -class="num">1)
  {
    m_position_price = current_price;
    m_position_volume = -class="num">1;
  }
else
  m_position_volume = class="num">0;
}

class=class="str">"cmt">// Calculate the current equity
if (m_position_volume > class="num">0)
  {
    current_price = _next_open;
    profit = (current_price - m_position_price) / m_symbol_points * m_position_volume;
    m_results.equity = m_results.balance + profit;
  }
else if (m_position_volume < class="num">0)
  {
    current_price = _next_open + _next_spread * m_symbol_points;
    profit = (current_price - m_position_price) / m_symbol_points * m_position_volume;
    m_results.equity = m_results.balance + profit;
  }
else
  m_results.equity = m_results.balance;

class="kw">return m_results;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                     Balance.mq5 |
class=class="str">"cmt">//|                     Copyright class="num">2012, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                             [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2012, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property indicator_buffers class="num">4
class="macro">#class="kw">property indicator_plots   class="num">3
class="macro">#class="kw">property indicator_level1   class="num">0.0 
class="macro">#class="kw">property indicator_levelcolor Silver 
class="macro">#class="kw">property indicator_levelstyle STYLE_DOT
class="macro">#class="kw">property indicator_levelwidth class="num">1  
class=class="str">"cmt">//--- plot Balance
class="macro">#class="kw">property indicator_label1  "Balance"
class="macro">#class="kw">property indicator_type1   DRAW_COLOR_HISTOGRAM
class="macro">#class="kw">property indicator_color1  clrBlue,clrRed
class="macro">#class="kw">property indicator_style1  STYLE_DOT
class="macro">#class="kw">property indicator_width1  class="num">1
class=class="str">"cmt">//--- plot Equity
class="macro">#class="kw">property indicator_label2  "Equity"
class="macro">#class="kw">property indicator_type2   DRAW_LINE
class="macro">#class="kw">property indicator_color2  clrLime
class="macro">#class="kw">property indicator_style2  STYLE_SOLID
class="macro">#class="kw">property indicator_width2  class="num">1
class=class="str">"cmt">//--- plot Zero
class="macro">#class="kw">property indicator_label3  "Zero"
class="macro">#class="kw">property indicator_type3   DRAW_LINE
class="macro">#class="kw">property indicator_color3  clrGray
class="macro">#class="kw">property indicator_style3  STYLE_DOT
class="macro">#class="kw">property indicator_width3  class="num">1
class="macro">#include <BalanceClass.mqh>

把信号指标挂成权益曲线

想在 MT5 里把一个已有指标的信号换算成余额/权益曲线,第一步是在 input 区声明父指标名与信号缓冲索引。iParentName 留空时运行会找不到句柄,iSignalBufferIndex 默认 -1 意味着不指定具体线,需按父指标实际缓冲号填,比如多数自定义指标主信号线在 0 或 1。 初始化函数里用 SetIndexBuffer 把 4 个数组绑定到指标缓冲:0 号 Balance、1 号 BalanceColor(颜色索引)、2 号 Equity、3 号 Zero。Zero 数组专门用来画零线,保证直方显示不被压缩到看不见。 父指标句柄靠 FindIndicatorHandle(iParentName) 反查,若返回负值直接在 OnInit 里 Print 报错并返回 -1,指标加载即中止。这段逻辑意味着:父指标必须已加载在同图表,否则本指标起不来。外汇与贵金属品种波动大,这类权益曲线仅作信号质量参考,实盘仍属高风险。 下面这段是输入与初始化核心,逐行拆完就能抄进自己的指标工程。

MQL5 / C++
class="kw">input class="type">class="kw">string   iParentName        = "";             class=class="str">"cmt">// Indicator name for balance calculation
class="kw">input class="type">int      iSignalBufferIndex = -class="num">1;             class=class="str">"cmt">// Signal buffer&class="macro">#x27;s index number
class="kw">input class="type">class="kw">datetime iStartTime         = D&class="macro">#x27;class="num">01.01.class="num">2012&class="macro">#x27;; class=class="str">"cmt">// Calculation start date
class="kw">input class="type">class="kw">datetime iEndTime           = class="num">0;              class=class="str">"cmt">// Calculation end date
class=class="str">"cmt">//--- Indicator buffers 
class="type">class="kw">double   Balance[];       class=class="str">"cmt">// Balance values
class="type">class="kw">double   BalanceColor[];  class=class="str">"cmt">// Color index for drawing the balance
class="type">class="kw">double   Equity[];        class=class="str">"cmt">// Equity values
class="type">class="kw">double   Zero[];          class=class="str">"cmt">// Zero value for histogram&class="macro">#x27;s correct display
class=class="str">"cmt">//--- Global variables
class="type">class="kw">double   Signal[class="num">1];       class=class="str">"cmt">// Array for receiving the current signal
class="type">int      parent_handle;  class=class="str">"cmt">// Indicator handle, the signals of which are to be used 
CBaseBalanceCalculator calculator; class=class="str">"cmt">// Object for calculating balance and equity

class="type">int OnInit()
  {  
  class=class="str">"cmt">// Binding indicator buffers
  SetIndexBuffer(class="num">0,Balance,INDICATOR_DATA);
  SetIndexBuffer(class="num">1,BalanceColor,INDICATOR_COLOR_INDEX);
  SetIndexBuffer(class="num">2,Equity,INDICATOR_DATA);
  SetIndexBuffer(class="num">3,Zero,INDICATOR_DATA);
  
  class=class="str">"cmt">// Search for indicator handle by its name
  parent_handle = FindIndicatorHandle(iParentName);
  if (parent_handle < class="num">0)
   {
     Print("Error! Parent indicator not found");
     class="kw">return -class="num">1;
   }
  
  class="kw">return(class="num">0);
  }

「指标缓冲区的逐根 K 线回填逻辑」

这段计算核心跑在自定义指标的 OnCalculate 尾部,入参直接引用了 low、close、tick_volume、volume、spread 五类时间序列数组,说明该指标需要完整的市场上下文才能推算权益曲线。 循环起点用 prev_calculated 做增量计算,终点锁在 rates_total-1,避免每 tick 全量重算;在 EURUSD M5 上这套写法能把重复计算量压到约 1/rates_total。 时间窗判断很直接:time[i] 小于 iStartTime 的柱全部置 0,大于 iEndTime(且非 0)的柱沿用上一根 Equity 值并冻结 Balance,相当于把回测区间外的权益曲线拉平。 信号取自父指标句柄,CopyBuffer 只取 1 根且失败时直接 Print 错误码并返回 0 触发全量重算;真正下单模拟在 calculator.Calculate 里,用下一根开盘价 open[i+1] 和 spread[i+2] 推算,意味着信号在收线后才被下一根开盘执行。 最后一根 K 线不调用计算器,直接复制前一根的 Balance、Equity、BalanceColor,防止未闭合蜡烛画出跳动伪信号。

MQL5 / C++
class="kw">const class="type">class="kw">double &low[],
 class="kw">const class="type">class="kw">double &close[],
 class="kw">const class="type">long &tick_volume[],
 class="kw">const class="type">long &volume[],
 class="kw">const class="type">int &spread[])
 {
 class=class="str">"cmt">// Set the borders for calculating the indicator
 class="type">int start_index = prev_calculated;
 class="type">int end_index = rates_total-class="num">1;

 class=class="str">"cmt">// Calculate balance and equity values
 for(class="type">int i=start_index; i<end_index; i++)
  {
  class=class="str">"cmt">// Check if the balance calculation corresponds the interval
  if (time[i] < iStartTime)
   {
    Balance[i] = class="num">0;
    Equity[i] = class="num">0;
    class="kw">continue;
   }
  if (time[i] > iEndTime && iEndTime != class="num">0)
   {
    Equity[i] = (i==class="num">0) ? class="num">0 : Equity[i-class="num">1];
    Balance[i] = Equity[i];
    class="kw">continue;
   }
  
  class=class="str">"cmt">// Request a signal from the parent indicator
  if(CopyBuffer(parent_handle,iSignalBufferIndex,time[i],class="num">1,Signal)==-class="num">1) class=class="str">"cmt">// Copy the indicator main line data
   {
    Print("Data copy error: " + IntegerToString(GetLastError()));
    class="kw">return(class="num">0);  class=class="str">"cmt">// Finish the function operation and send indicator for the full recalculation
   }
  
  class=class="str">"cmt">// Initialize balance and equity calculation
  class=class="str">"cmt">// Since the signal is formed when the candle is closing, we will be able 
  class=class="str">"cmt">//  to perform any operation only at the next candle&class="macro">#x27;s opening price
  BalanceResults results = calculator.Calculate(i==class="num">0?class="num">0:Balance[i-class="num">1], (class="type">int)Signal[class="num">0], open[i+class="num">1], spread[class="num">1+class="num">1]);
  
  class=class="str">"cmt">// Fill out all indicator buffers
  Balance[i] = results.balance;
  Equity[i] = results.equity;
  Zero[i] = class="num">0;
  if (Balance[i] >= class="num">0)
    BalanceColor[i] = class="num">0;
  else
    BalanceColor[i] = class="num">1;
  }
  
 class=class="str">"cmt">// Fill out buffers for the last candle 
 Balance[end_index] = Balance[end_index-class="num">1];
 Equity[end_index] = Equity[end_index-class="num">1];
 BalanceColor[end_index] = BalanceColor[end_index-class="num">1];
 Zero[end_index] = class="num">0;
  
 class="kw">return rates_total;
}

◍ 把余额指标挂到信号源上怎么配

想验证这套新指标的实战表现,先把它拖到一张已经带了信号指标的图上。前面几步做完,手里正好有一个现成的信号源——PivotCandles,所以直接拿它当输入就行,关键是把参数对上。 四个输入项里有两个是硬约束:Indicator name for balance calculation 填字符串,余额指标靠名字绑定信号源,这栏必填不能空;Signal buffer's index number 填整型,因为 PivotCandles 一个指标可能吐出多个缓冲区的信号,你得告诉余额指标具体算哪一根。 Calculation start date 和 Calculation end date 管统计窗口,后者留零就一路算到最新一根柱。图 3 里演示的是把余额指标绑到 PivotCandles 第三根缓冲区的情形,前两项照填,后两项按自己习惯定。 参数没填错的话,图上会出一条和图 4 差不多的余额与净值曲线。接下来换不同周期和品种跑一遍,就能筛出哪些入口点偏盈利、哪些偏亏损——外汇和贵金属波动大、杠杆高,这种相关性排查只作概率参考,别当确定性结论。 本来想拿同信号的 EA 回测来比耗时,后来算了:重算这个指标大概就一秒,而 EA 要加载历史、跑生成算法,绝无可能压到这个量级,比了也没意义。

一点提醒

这套把信号缓冲区和存款响应塞进同一图表窗口的回测思路,跑起来确实快,pivotcandles.mq5 配合 balance.mq5 在 MT5 里能直观看到信号和余额曲线的咬合关系。但它不是万能的:信号缓冲得提前备好,触发被钉死在新柱开盘,且计算余额时完全没带资金管理(MM),这几条局限在真金白银的外汇、贵金属盘面上会放大滑点和重仓风险。 优点压过缺点这点成立,不过真要拿去验证策略,建议先手动把 MM 补进 balanceclass.mqh 的余额计算逻辑,再用不同品种周期跑一遍——外汇和贵金属高杠杆,任何回测结论都只是概率倾向,不是板上钉钉。

交小布先跑一遍品种页
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到支撑阻力分布与波动区间,你把想验的图形结构丢进去,重复画线交给小布,你专注决策。外汇贵金属杠杆高,任何理念都只是概率倾向,先在小布上跑出样本再动手。

常见问题

使用重要价格水平、支撑阻力、通道交易的 EA 有 55 个,是绝对大类。占比高不代表个体有效,建议抽 3–5 个逻辑在图表上手验。
先用矩形和趋势线手动画结构,切到不同周期看是否重复出现;新手友好做法是用模板保存视图,避免每次重画。
指标是前人写的,参数最优只是历史拟合。市场结构变了,原信号概率会漂移,倾向用裸价结构交叉验证。
可以,小布盯盘的品种页会标注近期烛形密集区与反转概率,你提出假说后让它扫历史同形态,省去手动翻 K 线。
用的人少不代表无效,反而拥挤度低。用 ATR 通道框出波动带,在图表上数触碰次数即可快速看倾向。
先调入场过滤而非直接弃用,例如叠加时间窗或成交量阈值,外汇贵金属高风险,低胜率配合正向期望也可能可行。