在图表上快速检验交易理念·进阶篇
🔍

在图表上快速检验交易理念·进阶篇

(2/3)· 从 451 个 EA 说明里扒出真实策略分布,教你把脑中念头直接丢进图表验尸

案例拆解 第 2/3 篇
多数人把指标当圣经,却没意识到 MACD 被 54 个参赛 EA 沿用只是路径依赖。用规则代替思考,优化参数不等于验证理念。先把念头画在图上,比写三百行代码更早暴露漏洞。

◍ 新K线入列前的数组与计数门槛

处理新蜡烛的第一步,是把当前这根bar的字段逐个写进 m_candles[0]。时间、开高低收、tick_volume、real_volume、spread 一共 8 个 double/long/int 字段,必须对齐结构体成员,否则后续分析会读到脏数据。 另一种重载走 MqlRates 引用直接整体赋值:m_candles[0] = candle,比逐字段写更省事,但前提是调用方已经把 spread 等字段填好。两种入口都先跑 PrepareArrayForNewCandle() 做位移或扩容,再判计数。 关键门槛在 m_handled_candles_count 与 m_history_depth 的比较:前者小于后者时函数直接 return 0,不触发 DoAnalizeNewCandle()。也就是说,历史深度没攒够,枢轴判定逻辑一律不跑——在 MT5 里把 m_history_depth 设成 20,就意味着前 19 根新烛都是空转。 外汇与贵金属波动受杠杆与消息面影响大,这类计数门槛只是数据准备环节,不代表任何方向概率,实盘前请在策略测试器里用真实点差验证 return 0 的过渡期长度。

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">// Prepare the array for the new candle
  PrepareArrayForNewCandle();
  class=class="str">"cmt">// Fill out the current value of the candle
  m_candles[class="num">0].time       = time;
  m_candles[class="num">0].open       = open;
  m_candles[class="num">0].high       = high;
  m_candles[class="num">0].low        = low;
  m_candles[class="num">0].close      = close;
  m_candles[class="num">0].tick_volume  = tick_volume;
  m_candles[class="num">0].real_volume  = volume;
  m_candles[class="num">0].spread       = spread;
  class=class="str">"cmt">// Check if there is enough data for calculation
  if (m_handled_candles_count < m_history_depth)
     class="kw">return class="num">0;
  else
     class="kw">return DoAnalizeNewCandle();
  }
class="type">int CPivotCandlesClass::AnalizeNewCandle(class="type">MqlRates& candle)
  {
  class=class="str">"cmt">// Prepare the array for the new candle  
  PrepareArrayForNewCandle();
  class=class="str">"cmt">// Add the candle 
  m_candles[class="num">0] = candle;
  class=class="str">"cmt">// Check if there is enough data for calculation
  if (m_handled_candles_count < m_history_depth)
     class="kw">return class="num">0;
  else
     class="kw">return DoAnalizeNewCandle();
  }
class="type">void CPivotCandlesClass::PrepareArrayForNewCandle()
  {

均值与极值判定的代码落地

在 PivotCandles 类里,PrepareCalculation 负责把最新一根蜡烛塞进计算前的状态准备。它先备份上一根 MA 值到 m_prev_ma_value,再把 m_ma_value、m_volatility 归零,并将 m_is_highest / m_is_lowest 预设为 true,等待循环里被证伪。 循环按 m_history_depth 跑一遍:前 iMAPeriod 根收盘价累加进 price_sum 用于算均线;前 iVolatilityCandlesCount 根的高低价差累加进 m_volatility;前 iPrevCandlesCount 根则逐根比对,只要当前 candle[0] 的 high 不高于某历史根 high,或 low 不低于某历史根 low,对应布尔位就被翻成 false。 最终 m_ma_value = price_sum / iMAPeriod,m_volatility 除以 iVolatilityCandlesCount 得到平均波动,再调 CheckCandleSize 给 m_candle_pattern 赋值。外汇与贵金属波动剧烈,这类极值判断只代表历史窗口内的相对位置,转向概率而非确定性。 CheckCandleSize 用 full height 减 body height 的视角去识别锤子线与流星线,返回 0/1/-1 三态。你可以直接把下面这段拷进 MT5 的 EA 或指标里,改 iMAPeriod 和 iVolatilityCandlesCount 看信号密度变化。

MQL5 / C++
  class=class="str">"cmt">// Shift the array by one position to write the new value there
  ArrayCopy(m_candles, m_candles, class="num">1, class="num">0, m_history_depth-class="num">1);
  
  class=class="str">"cmt">// Increase the counter of added candles
  m_handled_candles_count++;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CalcMAValue                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class=class="str">"cmt">//| Calculate the current values of the Moving Average, volatility     |
class=class="str">"cmt">//|   and the value extremality                                       | 
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class="type">void CPivotCandlesClass::PrepareCalculation()
  {
  class=class="str">"cmt">// Store the previous value
  m_prev_ma_value = m_ma_value;
  m_ma_value = class="num">0;
  
  m_is_highest = true; 	class=class="str">"cmt">// check if the current candle is the highest one
  m_is_lowest = true;		class=class="str">"cmt">// check if the current candle is the lowest one
  m_volatility = class="num">0;		class=class="str">"cmt">// average volatility
  
  class="type">class="kw">double price_sum = class="num">0; class=class="str">"cmt">// Variable for storing the sum
  for (class="type">int i=class="num">0; i<m_history_depth; i++)
    {
      if (i<iMAPeriod)
        price_sum += m_candles[i].close;
      if (i>class="num">0 && i<=iVolatilityCandlesCount)
        m_volatility += m_candles[i].high - m_candles[i].low;
      if (i>class="num">0 && i<=iPrevCandlesCount)
      {
       m_is_highest = m_is_highest && (m_candles[class="num">0].high > m_candles[i].high);
       m_is_lowest = m_is_lowest && (m_candles[class="num">0].low < m_candles[i].low);
      }
    }
  m_ma_value = price_sum / iMAPeriod;
  m_volatility /= iVolatilityCandlesCount;
  
  m_candle_pattern = CheckCandleSize(m_candles[class="num">0]);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CheckCandleSize                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if the candle sizes comply with the patterns                 |
class=class="str">"cmt">//| The function returns:                                              |
class=class="str">"cmt">//|   class="num">0 - if the candle does not comply with the patterns              |
class=class="str">"cmt">//|   class="num">1 - if "hammer" pattern is detected                              |
class=class="str">"cmt">//|   -class="num">1 - if "shooting star" pattern is detected                      |
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class="type">int CPivotCandlesClass::CheckCandleSize(class="type">MqlRates &candle)
  {
  class="type">class="kw">double candle_height=candle.high-candle.low;            class=class="str">"cmt">// candle&class="macro">#x27;s full height
  class="type">class="kw">double candle_body=MathAbs(candle.close-candle.open); class=class="str">"cmt">// candle&class="macro">#x27;s body height

「锤子线与流星线的代码判定逻辑」

识别单根 K 线形态时,先用实体占整根高度的比例过滤噪音:若 candle_body/candle_height*100.0 大于 iMaxBodySize,直接返回 0,说明实体过大不纳入 pivot 候选。 随后分别算上影线和下影线高度。上影 = 最高价减去开收二者最大值,下影 = 开收二者最小值减去最低价。若上影占比 ≤ iMaxShadowSize,判定为锤子线返回 1;下影占比 ≤ 该阈值则判流星线返回 -1,其余情况返回 0。 真正下单信号在 DoAnalizeNewCandle 里合成。离场看均线穿越:前一根收在均线上方、当前收在下方,给 signal=2 平空;反向穿越给 -2 平多。 入场需先满足波动门槛 high-low >= iVolatilityPercent/100.0 * m_volatility。在此基础上,流星线且为区间最高、收在均线上方,给 -1 做空倾向;锤子线且为区间最低、收在均线下方,给 1 做多倾向。外汇与贵金属杠杆高,信号仅为概率倾向,实盘前务必在 MT5 用历史数据跑一遍验证参数敏感性。

MQL5 / C++
  class=class="str">"cmt">// Check if the candle has a small body
  if(candle_body/candle_height*class="num">100.0>iMaxBodySize)
      class="kw">return class="num">0;
  class="type">class="kw">double candle_top_shadow=candle.high-MathMax(candle.open,candle.close);   class=class="str">"cmt">// candle upper shadow height
  class="type">class="kw">double candle_bottom_shadow=MathMin(candle.open,candle.close)-candle.low; class=class="str">"cmt">// candle bottom shadow height
  class=class="str">"cmt">// If the upper shadow is very small, that indicates the "hammer" pattern
  if(candle_top_shadow/candle_height*class="num">100.0<=iMaxShadowSize)
      class="kw">return class="num">1;
  class=class="str">"cmt">// If the bottom shadow is very small, that indicates the "shooting star" pattern
  else if(candle_bottom_shadow/candle_height*class="num">100.0<=iMaxShadowSize)
      class="kw">return -class="num">1;
  else
      class="kw">return class="num">0;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| DoAnalizeNewCandle                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Real analysis of compliance with the patterns                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CPivotCandlesClass::DoAnalizeNewCandle()
  {
  class=class="str">"cmt">// Prepare data for analyzing the current situation
  PrepareCalculation();

  class=class="str">"cmt">// Process prepared data and set the exit signal
  class="type">int signal = class="num">0;

  class=class="str">"cmt">///////////////////////////////////////////////////////////////////
  class=class="str">"cmt">// EXIT SIGNALS                                                              //
  class=class="str">"cmt">///////////////////////////////////////////////////////////////////
  class=class="str">"cmt">// If price crosses the moving average downwards, class="type">short position is closed
  if(m_candles[class="num">1].close > m_prev_ma_value && m_candles[class="num">0].close < m_ma_value)
      signal = class="num">2;
  class=class="str">"cmt">// If price crosses the moving average upwards, class="type">long position is closed
  else if (m_candles[class="num">1].close < m_prev_ma_value && m_candles[class="num">0].close > m_ma_value)
      signal = -class="num">2;

  class=class="str">"cmt">///////////////////////////////////////////////////////////////////
  class=class="str">"cmt">// ENTRY SIGNALS                                                            //
  class=class="str">"cmt">///////////////////////////////////////////////////////////////////
  class=class="str">"cmt">// Check if the minimum volatility condition is met
  if (m_candles[class="num">0].high - m_candles[class="num">0].low >= iVolatilityPercent / class="num">100.0 * m_volatility)
   {
   class=class="str">"cmt">// Checks for "shooting star" pattern
   if (m_candle_pattern < class="num">0 && m_is_highest && m_candles[class="num">0].close > m_ma_value)
       signal = -class="num">1;
   class=class="str">"cmt">// Checks for "hammer" pattern
   else if (m_candle_pattern > class="num">0 && m_is_lowest && m_candles[class="num">0].close < m_ma_value)
       signal = class="num">1;
   }

  class="kw">return signal;
  }

◍ 指标缓冲区与绘图层的绑定细节

这套枢轴蜡烛指标在主图窗口跑了 4 个缓冲区,但只对外画 2 条图:一条慢均线 SlowMA(实线 AliceBlue),一条带颜色的箭头信号 ChartSignal(宽度 3,配 5 色)。 代码里 #property indicator_buffers 4#property indicator_plots 2 直接定死了这个结构——多出来的两个缓冲不画图,只给内部计算或着色用。 OnInit 里把 0 号缓冲绑给 SMA、1 号绑给 ChartSignal 作图数据;2 号 SignalColor 是颜色索引缓冲,3 号 Signal 是计算缓冲不显示。最后 PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0) 把箭头图的空值设为 0,避免 0 价位点被误画。 开 MT5 把这段贴进自定义指标,改 indicator_color2 的五色顺序,就能直观区分不同方向的枢轴信号。外汇和贵金属波动剧烈,这类信号仅作概率参考,实盘须自担高风险。

MQL5 / C++
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_chart_window
class=class="str">"cmt">// Use four buffers, while drawing two
class="macro">#class="kw">property indicator_buffers class="num">4
class="macro">#class="kw">property indicator_plots  class="num">2
class=class="str">"cmt">//--- plot SlowMA
class="macro">#class="kw">property indicator_label1  "SlowMA"
class="macro">#class="kw">property indicator_type1   DRAW_LINE
class="macro">#class="kw">property indicator_color1  clrAliceBlue
class="macro">#class="kw">property indicator_style1  STYLE_SOLID
class="macro">#class="kw">property indicator_width1  class="num">1
class=class="str">"cmt">//--- plot ChartSignal
class="macro">#class="kw">property indicator_label2  "ChartSignal"
class="macro">#class="kw">property indicator_type2   DRAW_COLOR_ARROW
class="macro">#class="kw">property indicator_color2  clrLightSalmon,clrOrangeRed,clrBlack,clrSteelBlue,clrLightBlue
class="macro">#class="kw">property indicator_style2  STYLE_SOLID
class="macro">#class="kw">property indicator_width2  class="num">3
class="macro">#include <PivotCandlesClass.mqh>
class=class="str">"cmt">//--- Indicator buffers
class="type">class="kw">double   SMA[];          class=class="str">"cmt">// Values of the Moving Average
class="type">class="kw">double   Signal[];       class=class="str">"cmt">// Signal values
class="type">class="kw">double   ChartSignal[];  class=class="str">"cmt">// Location of signals on the chart
class="type">class="kw">double   SignalColor[];  class=class="str">"cmt">// Signal class="type">color array
class=class="str">"cmt">//--- Calculation class
CPivotCandlesClass PivotCandlesClass;
class="type">int OnInit()
  {
class=class="str">"cmt">//--- indicator buffers mapping
   SetIndexBuffer(class="num">0,SMA,INDICATOR_DATA);
   SetIndexBuffer(class="num">1,ChartSignal,INDICATOR_DATA);
   SetIndexBuffer(class="num">2,SignalColor,INDICATOR_COLOR_INDEX);
   SetIndexBuffer(class="num">3,Signal,INDICATOR_CALCULATIONS);
class=class="str">"cmt">//--- set class="num">0 as an empty value
   PlotIndexSetDouble(class="num">1,PLOT_EMPTY_VALUE,class="num">0);
   class="kw">return(class="num">0);
  }

逐根 K 线里的信号与均线回填

指标的核心计算落在 OnCalculate 里,MT5 每来一根新柱或重算历史都会调它。prev_calculated 为 0 代表首次运行或历史重载,这时必须清掉 PivotCandlesClass 里的旧状态,否则会拿上一轮行情的残留数据去判信号。 循环从 prev_calculated 跑到 rates_total-2(end_calc_edge),对每根 K 线调 AnalizeNewCandle 拿到 signal,值域是 -2 到 2。signal 写进 Signal 缓冲区,同时把 PivotCandlesClass.MAValue() 的均线值写进 SMA。 图表上的箭头位置按信号正负挂:signal<0 贴在 high[i] 上方,signal>0 贴在 low[i] 下方,否则归 0 不画。颜色索引是 0..4,所以用 signal+2 把 [-2..2] 平移对齐,负号信号对应冷色、正号对应暖色。 末根未闭合 K 线(end_calc_edge)的 SMA 直接复制前一根,避免均线在实时 Tick 上跳变。外汇与贵金属杠杆高、滑点大,这类信号仅作概率参考,实盘前请在 MT5 策略测试器用至少 3 个月 tick 数据验证。

MQL5 / C++
class="type">int OnCalculate(class="kw">const class="type">int rates_total,
                class="kw">const class="type">int prev_calculated,
                class="kw">const class="type">class="kw">datetime &time[],
                class="kw">const class="type">class="kw">double &open[],
                class="kw">const class="type">class="kw">double &high[],
                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">// 首次计算或历史更新时,清理计算对象
   if (prev_calculated == class="num">0)
      PivotCandlesClass.CleanupHistory();

   class="type">int end_calc_edge = rates_total-class="num">1;  
   if (prev_calculated >= end_calc_edge)
      class="kw">return end_calc_edge;

   for(class="type">int i=prev_calculated; i<end_calc_edge; i++)
     {
      class=class="str">"cmt">// 分析新K线,返回-class="num">2..2的信号
      class="type">int signal = PivotCandlesClass.AnalizeNewCandle(time[i],open[i],high[i],low[i],close[i],tick_volume[i],volume[i],spread[i]);
      Signal[i] = signal;
      SMA[i] = PivotCandlesClass.MAValue();

      class=class="str">"cmt">// 信号处理完,往图表上摆
      class=class="str">"cmt">// 设定信号显示位置...
      if (signal < class="num">0)
         ChartSignal[i]=high[i];
      else if (signal > class="num">0)
         ChartSignal[i]=low[i];
      else
         ChartSignal[i]=class="num">0;
      class=class="str">"cmt">// ..以及颜色
      class=class="str">"cmt">// 信号范围[-class="num">2..class="num">2],颜色索引[class="num">0..class="num">4],做对齐
      SignalColor[i]=signal+class="num">2;
     }

   class=class="str">"cmt">// 末根均线沿用前一根,防止陡降
   SMA[end_calc_edge] = SMA[end_calc_edge-class="num">1];
   class=class="str">"cmt">//--- 返回下次调用的 prev_calculated
   class="kw">return(end_calc_edge);
  }

「用结构体把账户浮盈算清楚」

想在 EA 里一次返回余额和净值,又不想用一堆出参,可以直接定义一个轻量结构体。下面这段把 balance 和 equity 打包进 BalanceResults,函数末尾一句 return 就能把两个值都丢出来,比分别传引用干净。 实际跑起来,FindIndicatorHandle 会先抓主图窗口总数:ChartGetInteger(0,CHART_WINDOWS_TOTAL) 在多数 MT5 配置下返回 1~多个子窗口。它从最后一个窗口倒序扫,用 ChartIndicatorsTotal 拿每个窗口挂的指标数,再靠 ChartIndicatorName 比对名字,命中就 ChartIndicatorGet 返回句柄,没找到统一回 -1。 基类 CBaseBalanceCalculator 把当前仓位量、开仓价、点值、计算结果都收进私有成员。m_symbol_points 这个点值在外汇和贵金属上随品种跳动,XAUUSD 一个点通常 0.01 美元/盎司量级,杠杆和高波动下净值回撤可能很快,账户风险始终不低。 把这段贴进 MT5 的 MQ5 文件,编译后挂到 EURUSD 或 XAUUSD 图表,改 _name 传你自己的指标名,就能在专家日志里验证句柄是否抓对。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Common structures                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// Structure for returning calculation results 
class=class="str">"cmt">// using only class="kw">return command;
class="kw">struct BalanceResults
  {
   class="type">class="kw">double balance;
   class="type">class="kw">double equity;
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Common function                                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//   Function for searching for the indicator handle by its name
class="type">int FindIndicatorHandle(class="type">class="kw">string _name)
  {
   class=class="str">"cmt">// Receive the number of open charts
   class="type">int windowsCount = (class="type">int)ChartGetInteger(class="num">0,CHART_WINDOWS_TOTAL);

   class=class="str">"cmt">// Search all of them
   for(class="type">int w=windowsCount-class="num">1; w>=class="num">0; w--)
     {
      class=class="str">"cmt">// How many indicators are attached to the current chart
      class="type">int indicatorsCount = ChartIndicatorsTotal(class="num">0,w);
      class=class="str">"cmt">// Search by all chart indicators
      for(class="type">int i=class="num">0;i<indicatorsCount;i++)
        {
         class="type">class="kw">string name = ChartIndicatorName(class="num">0,w,i);
         class=class="str">"cmt">// If such an indicator is found, class="kw">return its handle
         if (name == _name)
            class="kw">return ChartIndicatorGet(class="num">0,w,name);
        }
     }   
   
   class=class="str">"cmt">// If there is no such an indicator, class="kw">return the incorrect handle 
   class="kw">return -class="num">1;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Base calculation class                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CBaseBalanceCalculator
  {
class="kw">private:
   class="type">class="kw">double            m_position_volume; class=class="str">"cmt">// Current open position volume
   class="type">class="kw">double            m_position_price;   class=class="str">"cmt">// Position opening price
   class="type">class="kw">double            m_symbol_points;    class=class="str">"cmt">// Value of one point for the current symbol
   BalanceResults    m_results;          class=class="str">"cmt">// Calculation results
class="kw">public:
   class="type">void              CBaseBalanceCalculator(class="type">class="kw">string symbol_name = "");
   class="type">void              Cleanup();
   BalanceResults    Calculate( class="kw">const class="type">class="kw">double _prev_balance,
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到策略分布与图形结构命中率,你专注决策而非手算统计。

常见问题

数据显示图形结构类有 55 例、神经网络仅 7 例,新手更易从支撑阻力等直观结构入手,而非训练黑箱网络。
可用矩形或水平线对象手动圈定,再叠加历史 K 线观察触碰反应,不必立刻写 EA 也能完成初筛。
说明均线系仍是主流基底,但高复用不代表高胜率,需结合价格行为看其失效频段。
可以,小布的品种页会把常见结构命中与波动窗口列出,你据此判断念头是否值得写进回测。
316 例有意义说明外,其余多是问候语,且开发者常隐瞒具体组合,只能靠指标绝对量反推倾向。