Heiken-Ashi指标与移动平均指标组合能够提供好的信号吗?·进阶篇
📘

Heiken-Ashi指标与移动平均指标组合能够提供好的信号吗?·进阶篇

第 2/2 篇

「先定信号:HA 与百周期均线的交叉触发」

要把一个想法塞进现有交易框架,第一步不是写代码,而是把买卖规则钉死:系统到底在什么时刻喊「多」、什么时刻喊「空」。规则含糊,后面回测出的曲线毫无意义。 这里给出的判定很直接——Heikin-Ashi 图表转看涨,且市场价格向上穿越 100 周期移动平均线时,做多;HA 转看跌且价格向下穿越同一条均线时,做空。两条线同时给方向,才发信号。 写算法时有个坑得提前记着:信号频率。100 周期均线在 H1 图上约覆盖 4.17 个交易日(按每周期 25 根 H1 K 线粗算),穿越本身不频繁,但 HA 的阴阳切换比普通 K 线少噪,二者叠加后触发器可能一天都不响一次。频率低不等于无效,但仓位和复盘周期得按这个节奏重调。外汇与贵金属杠杆高,低频信号更忌追单。

EURUSD 30分钟的一年半实测

在 EURUSD 的 30 分钟周期上,取 2022-01-01 至 2023-07-05 这段窗口跑这套逻辑,能直接出一份可复核的成交记录与权益曲线。 这段样本覆盖约 1.5 年、含 18 个月度切片,足以看出策略在趋势段与震荡段的不同表现,而不是只挑一段行情美化。 原文明确点出:换时段或做参数优化,结果可能更好;但凡要铺到多个交易品种,必须先逐品种优化与测试,外汇和贵金属杠杆高、滑点跳空频繁,直接套用原参数大概率翻车。

◍ SMA 与 Heiken Ashi 双指标句柄的初始化及开平仓逻辑

在 MT5 里跑这套策略,第一步是把自定义 SMA 和 Heiken Ashi 的句柄拿到手。OnInit 中用 iCustom 按当前品种和各自周期加载指标,若返回 INVALID_HANDLE 就打印错误码并 return INIT_FAILED,否则 return INIT_SUCCEEDED,这一步决定了 EA 能否正常启动。 OnTick 里先声明四个 double 数组承接 Heiken Ashi 的 O/H/L/C,并用 CopyBuffer 从指标缓冲区取数。注意 CopyBuffer 的第三个参数是起始位置、第四个是数量,原文取的是 2 而不是 1,意味着拉取的是包含紧邻前一根在内的两根烛形,用来判断颜色切换。 开仓条件写得很直白:当本根 Heiken 收盘低于开盘、且前一根开盘高于开盘,倾向开买;反之收盘高于开盘、前一根开盘低于开盘,倾向开卖。若有持仓,则价格穿过对应开仓价时平仓。外汇与贵金属杠杆高,这类信号只是概率倾向,实盘前务必在策略测试器里验证。 想改信号强度也简单——可以把开仓条件从「相邻一根反转」改成「连续同色 N 根后才动手」,或平仓改用另一条不同周期的 SMA 与价格交叉。手数计算函数会先判固定手数与否,非固定则按账户余额与风险百分比算保证金再规范化,normalize_lot 负责对齐最小步长。 别把正态当圣经:GetArray2 只是把缓冲区复制加错误检查封装了一层,返回 false 就说明 CopyBuffer 出错,排查时先看这里而不是盲目调参数。

MQL5 / C++
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   handle_iCustomMA=iCustom(_Symbol,my_timeframe,"\\folder/s where you have the indicator\\sma",MAPeriod,MAShift);
   handle_iCustomHeiken=iCustom(_Symbol,my_timeframe_Heiken,"\\folder/s where you have the indicator\\Heiken_Ashi_copy");
class=class="str">"cmt">//---
   if(handle_iCustomMA==INVALID_HANDLE)
     {
       class=class="str">"cmt">//--- tell about the failure and output the error code
       PrintFormat("Failed to create handle of the iCustom indicator for the symbol %s/%s, error code %d",
                   _Symbol,
                   EnumToString(my_timeframe),
                   GetLastError());
       class=class="str">"cmt">//--- the indicator is stopped early
       class="kw">return(INIT_FAILED);
     }
   if(handle_iCustomHeiken==INVALID_HANDLE)
     {
       class=class="str">"cmt">//--- tell about the failure and output the error code
       PrintFormat("Failed to create handle of the iCustom indicator for the symbol %s/%s, error code %d",
                   _Symbol,
                   EnumToString(my_timeframe_Heiken),
                   GetLastError());
       class=class="str">"cmt">//--- the indicator is stopped early
       class="kw">return(INIT_FAILED);
     }
   class="kw">return(INIT_SUCCEEDED);
   }
class="type">void OnTick()
  {
   class="type">class="kw">double heikenAshiOpen[], heikenAshiHigh[], heikenAshiLow[], heikenAshiClose[];
   CopyBuffer(handle_iCustomHeiken,class="num">0,class="num">0,class="num">2,heikenAshiOpen);
   CopyBuffer(handle_iCustomHeiken,class="num">1,class="num">0,class="num">2,heikenAshiHigh);
   CopyBuffer(handle_iCustomHeiken,class="num">2,class="num">0,class="num">2,heikenAshiLow);
   CopyBuffer(handle_iCustomHeiken,class="num">3,class="num">0,class="num">2,heikenAshiClose);
   Comment("heikenAshiOpen ",DoubleToString(heikenAshiOpen[class="num">0],_Digits),
           "\n heikenAshiHigh ",DoubleToString(heikenAshiHigh[class="num">0],_Digits),
           "\n heikenAshiLow ",DoubleToString(heikenAshiLow[class="num">0],_Digits),

「用HA阴阳反转卡进出场」

这段逻辑把平滑K线(Heiken Ashi)的收/开关系当成触发器:当本根HA收盘小于开盘、且前一根收盘大于开盘,视为下跌转上涨的反转信号,倾向触发市价买。 对应卖单则是本根收大于开、前一根收小于开,属于上涨转下跌的概率性反转,代码里直接 Print 并 Alert 后通过 trade.PositionOpen 下卖单。 止损止盈用 ptsl、pttp 乘 _Point 再 NormalizeDouble 到品种小数位,例如买单 sl = ask - ptsl*_Point、tp = bid + pttp*_Point,外汇与贵金属杠杆高,点值波动可能瞬间扫掉止损。 若已有持仓且方向为 Sell,当最新 HA 收小于开时调用 PositionClose(_Symbol,5) 平掉,这里的 5 是异步平仓超时毫秒数,实盘里可改大避免网络延迟拒单。 把下面片段直接贴进 EA 的 OnTick 后半段,把 heikenAshiOpen/Close 数组和 ptsl/pttp 参数接好,就能在 MT5 策略测试器里跑出信号频次。

MQL5 / C++
  "\n heikenAshiClose ",DoubleToString(heikenAshiClose[class="num">0],_Digits));
class=class="str">"cmt">//---
  class="type">MqlTick tick;
  class="type">class="kw">double last_price = tick.ask;
  SymbolInfoTick(_Symbol,tick);
  class="type">int total = PositionsTotal();
class=class="str">"cmt">//---
class=class="str">"cmt">// Retrieve the current value
  class="type">MqlTradeResult  result;
  class="type">MqlRates rates[];
class=class="str">"cmt">//---
  class="type">class="kw">double array_ma[];
  ArraySetAsSeries(array_ma,true);
  class="type">int start_pos2=class="num">0,count2=class="num">5;
  if(!iGetArray2(handle_iCustomMA,class="num">0,start_pos2,count2,array_ma))
    class="kw">return;
class=class="str">"cmt">//------------------------------------------------------------------------------
    {
      if(DoubleToString(heikenAshiClose[class="num">0],_Digits) < DoubleToString(heikenAshiOpen[class="num">0],_Digits) && (DoubleToString(heikenAshiClose[class="num">1],_Digits) > DoubleToString(heikenAshiOpen[class="num">1],_Digits)))
        {
         Print("Open Order Buy");
         Alert(" Buying");
         Orden="Buy";
         sl=NormalizeDouble(tick.ask - ptsl*_Point,_Digits);
         tp=NormalizeDouble(tick.bid + pttp*_Point,_Digits);
         trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,get_lot(tick.bid),tick.bid,sl,tp,"Buy");
         class="kw">return;
        }
    }
    {
      if(DoubleToString(heikenAshiClose[class="num">0],_Digits) > DoubleToString(heikenAshiOpen[class="num">0],_Digits) && (DoubleToString(heikenAshiClose[class="num">1],_Digits) < DoubleToString(heikenAshiOpen[class="num">1],_Digits)))
        {
         Print("Open Order Sell");
         Alert(" Selling");
         Orden="Sell";
         sl=NormalizeDouble(tick.bid + ptsl*_Point,_Digits);
         tp=NormalizeDouble(tick.ask - pttp*_Point,_Digits);
         trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,get_lot(tick.ask),tick.ask,sl,tp,"Sell");
         class="kw">return;
        }
    }
  if(total>class="num">0)
    {
      if(Orden=="Sell" &&(DoubleToString(heikenAshiClose[class="num">0],_Digits) < DoubleToString(heikenAshiOpen[class="num">0],_Digits) ))
        {
         trade.PositionClose(_Symbol,class="num">5);
         Print("cerró sell");

手算手数与安全平仓的收尾逻辑

这段代码片段承接前面 Heiken Ashi 信号判定,先把反方向持仓清掉:当 Orden 标记为 Sell 且当前 HA 收盘小于开盘时,直接 PositionClose 平掉卖单并打印日志;Buy 方向同理,HA 收盘大于开盘才平多单,平仓超时参数写死为 5 毫秒级等待。 gel_lot 函数区分了固定手数与风险百分比两种模式。固定模式直接 normalize_lot 返回;风险模式调用 OrderCalcMargin 算 1 标准手所需保证金,再用余额乘以风险百分比除以单手保证金得出手数,若计算失败则回退到固定手数 inp_lot_fix。 normalize_lot 是手数落地的关键:取 SYMBOL_VOLUME_STEP 做 MathFloor 向下对齐,再用 SYMBOL_VOLUME_MIN 做 MathMax 保底,避免下单手数低于经纪商最小值被拒。外汇与贵金属杠杆波动大,这套对齐能降低无效订单概率。 iGetArray2 封装了 CopyBuffer 的健壮取法:先判动态数组,失败即报错返回 false;拷贝数量不符也打印错误码并返回 false,只有 copied2==count2 才认数据可用。把这段直接贴进 MT5 策略测试器,能验证 HA 平仓与手数计算是否符合你的经纪商规则。

MQL5 / C++
    class="kw">return;
      }
      if(Orden=="Buy" &&(DoubleToString(heikenAshiClose[class="num">0],_Digits) > DoubleToString(heikenAshiOpen[class="num">0],_Digits) ))
        {
         trade.PositionClose(_Symbol,class="num">5);
         Print("cerró buy");
         class="kw">return;
        }
     }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double get_lot(class="type">class="kw">double price)
  {
   if(inp_lot_type==LOT_TYPE_FIX)
      class="kw">return(normalize_lot(inp_lot_fix));
   class="type">class="kw">double one_lot_margin;
   if(!OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,class="num">1.0,price,one_lot_margin))
      class="kw">return(inp_lot_fix);
   class="kw">return(normalize_lot((AccountInfoDouble(ACCOUNT_BALANCE)*(inp_lot_risk/class="num">100))/ one_lot_margin));
  }
class="type">class="kw">double normalize_lot(class="type">class="kw">double lt)
  {
   class="type">class="kw">double lot_step = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
   lt = MathFloor(lt / lot_step) * lot_step;
   class="type">class="kw">double lot_minimum = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   lt = MathMax(lt, lot_minimum);
   class="kw">return(lt);
  }
class="type">bool iGetArray2(const class="type">int handle2,const class="type">int buffer2,const class="type">int start_pos2,
                const class="type">int count2,class="type">class="kw">double &arr_buffer2[])
  {
   class="type">bool result2=true;
   if(!ArrayIsDynamic(arr_buffer2))
     {
      class=class="str">"cmt">//if(InpPrintLog)
      PrintFormat("ERROR! EA: %s, FUNCTION: %s, this a no dynamic array!",__FILE__,__FUNCTION__);
      class="kw">return(false);
     }
   ArrayFree(arr_buffer2);
class=class="str">"cmt">//--- reset error code
   ResetLastError();
class=class="str">"cmt">//--- fill a part of the iBands array with values from the indicator buffer
   class="type">int copied2=CopyBuffer(handle2,buffer2,start_pos2,count2,arr_buffer2);
   if(copied2!=count2)
     {
      class=class="str">"cmt">//--- if the copying fails, tell the error code
      class=class="str">"cmt">//if(InpPrintLog)
      PrintFormat("ERROR! EA: %s, FUNCTION: %s, amount to copy: %d, copied: %d, error code %d",
                  __FILE__,__FUNCTION__,count2,copied2,GetLastError());
      class=class="str">"cmt">//--- quit with zero result - it means that the indicator is considered as not calculated
      class="kw">return(false);
     }
   class="kw">return(result2);
  }

◍ 把这条线请下神坛

HA 叠 MA 的框架跑通了,但别把它当成圣杯。回测里那次获利只是在 15 分钟图上用固定手数跑出来的单一样本,换到 XAUUSD 这种点差跳动的品种上,信号频率和质量都可能塌一半。 连续 HA 烛形后触发、调 MA 回看周期、甚至把实体宽度当趋势强度写进公式——这些全是可改的旋钮,不是必赚开关。外汇和贵金属杠杆高,实盘前先用策略测试器把手数规范(volume step、min volume)和止损点数(按小数位规范化)逐项核对。 真要落地,就开 MT5 把 Heiken_Ashi_strat.mq5 里的 CopyBuffer 调用和 OrderSend 条件拆开看,跑一遍再说。策略是工具,不是预言。

常见问题

一年半实测中该组合能过滤部分噪声,但信号仅倾向提示趋势延续,外汇高风险下仍需结合仓位控制验证。
当 HA 柱由阳转阴且价格跌破均线视为离场,由阴转阳且站上均线视为进场,按反转根确认而非实时毛刺。
小布可在对应品种页内置该组合诊断,自动标注交叉与反转并推送提醒,你只需复核决策即可。
按账户净值与单笔风险百分比倒推手数,预留滑点空间,贵金属波动大须调低杠杆暴露。
实测中价格多次假穿透该线,它只是概率过滤工具,震荡市失效概率高,需下神坛当辅助而非依据。