Heiken-Ashi指标与移动平均指标组合能够提供好的信号吗?·进阶篇
「先定信号: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 出错,排查时先看这里而不是盲目调参数。
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 策略测试器里跑出信号频次。
"\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 平仓与手数计算是否符合你的经纪商规则。
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 条件拆开看,跑一遍再说。策略是工具,不是预言。