简单均值回归交易策略·进阶篇
◍ EA 输入参数与 MA 句柄初始化怎么排
先把外部输入理清,EA 跑起来才不会在手数和周期上犯晕。固定手数模式由 inp_lot_type 控制,默认 LOT_TYPE_FIX;inp_lot_fix 给的是 0.01 手,inp_lot_risk 留了 0.01 做风险百分比手数的备用参数。日志开关 InpPrintLog 默认 false,魔幻数写死 66777,Expert_EveryTick 默认 false 表示不每跳都跑逻辑。 时间帧用 my_timeframe 接收,默认 PERIOD_CURRENT 取当前图表周期。MA 相关四个输入很关键:周期 200、水平偏移 5、平滑类型 MODE_SMA(简单移动平均)、价格类型走 applied_price 默认收盘价。回溯常规化移位由 Inp_MA_ma_shift=5 顺带承担,止损止盈点数在别处用输入变量接。 OnInit 里第一行就得给 iMA 建句柄:handle_iMA = iMA(_Symbol, my_timeframe, Inp_MA_ma_period, Inp_MA_ma_shift, Inp_MA_ma_method, Inp_MA_applied_price)。这一句把上面输入全喂进去,句柄拿不到后面 ArrayGet 必报错。随后 previousValue 初始化 0.0,最后 return INIT_SUCCEEDED 告诉终端初始化过了。 外汇和贵金属杠杆高,句柄失败或手数规范化没做对,开单会直接 reject。建议把这段 input 块原样丢进 MT5 的 EA 头里,改 Inp_MA_ma_period 到 50 看句柄是否还正常,验证初始化路径。
input ENUM_LOT_TYPE inp_lot_type = LOT_TYPE_FIX; class=class="str">"cmt">// type of lot input class="type">class="kw">double inp_lot_fix = class="num">0.01; class=class="str">"cmt">// fix lot input class="type">class="kw">double inp_lot_risk = class="num">0.01; input class="type">bool InpPrintLog = class="kw">false; class=class="str">"cmt">// Print log class="type">class="kw">ulong Expert_MagicNumber =class="num">66777; class=class="str">"cmt">// class="type">bool Expert_EveryTick =class="kw">false; class=class="str">"cmt">// input ENUM_TIMEFRAMES my_timeframe=PERIOD_CURRENT; class=class="str">"cmt">// Timeframe class="type">int handle_iMA; input class="type">int Inp_MA_ma_period = class="num">200; class=class="str">"cmt">// MA: averaging period input class="type">int Inp_MA_ma_shift = class="num">5; class=class="str">"cmt">// MA: horizontal shift input ENUM_MA_METHOD Inp_MA_ma_method = MODE_SMA; class=class="str">"cmt">// MA: smoothing type
「用 iMA 句柄抓最近三根均线值」
这段逻辑演示了在 MT5 里如何把移动平均线句柄化,并抽取最近 3 根 K 线上的 MA 数值做实时观察。核心不是画线,而是把指标变成可读取的数组,供后续价格行为判断调用。 input ENUM_APPLIED_PRICE Inp_MA_applied_price = PRICE_CLOSE; // MA: type of price int shift = 49; // loockback normalization input int ptsl = 350; // points for stoploss input int pttp = 5000; // points for takeprofit int OnInit() { //--- handle_iMA=iMA(_Symbol,my_timeframe,Inp_MA_ma_period,Inp_MA_ma_shift, Inp_MA_ma_method,Inp_MA_applied_price); // Initialize the variable here if needed previousValue = 0.0; //--- return(INIT_SUCCEEDED); } MqlTick tick; double last_price = tick.ask; SymbolInfoTick(_Symbol,tick); if(SymbolInfoTick(_Symbol,tick)) last=tick.last; double Last = NormalizeDouble(last,_Digits); handle_iMA=iMA(_Symbol,my_timeframe,Inp_MA_ma_period,Inp_MA_ma_shift, Inp_MA_ma_method,Inp_MA_applied_price); //--- double array_ma[]; ArraySetAsSeries(array_ma,true); int start_pos=0,count=3; if(!iGetArray(handle_iMA,0,start_pos,count,array_ma)) return; string text=""; for(int i=0; i<count; i++) text=text+IntegerToString(i)+": "+DoubleToString(array_ma[i],Digits()+1)+"\n"; //--- Comment(text); bool iGetArray(const int handle,const int buffer,const int start_pos, const int count,double &arr_buffer[]) { bool result=true; if(!ArrayIsDynamic(arr_buffer)) { //if(InpPrintLog) PrintFormat("ERROR! EA: %s, FUNCTION: %s, this a no dynamic array!",__FILE__,__FUNCTION__); return(false); } ArrayFree(arr_buffer); //--- reset error code ResetLastError(); //--- fill a part of the iBands array with values from the indicator buffer int copied=CopyBuffer(handle,buffer,start_pos,count,arr_buffer); if(copied!=count) { //--- if the copying fails, tell the error code //if(InpPrintLog) PrintFormat("ERROR! EA: %s, FUNCTION: %s, amount to copy: %d, copied: %d, error code %d", __FILE__,__FUNCTION__,count,copied,GetLastError()); //--- quit with zero result - it means that the indicator is considered as not calculated return(false); } return(result); } 逐行拆解:前 4 行是外部输入与常量,PRICE_CLOSE 指定均线用收盘价计算,ptsl=350 与 pttp=5000 是以 point 为单位的止损止盈距离,在 XAUUSD 这种 _Digits=2 的品种上,5000 point 即 50 美元。OnInit 里 iMA 返回句柄,后续靠句柄取数而不重复创建。 MqlTick 段先取 tick.ask 再调 SymbolInfoTick 覆盖,注意 last_price 在调用前是脏值,真正用的是 last。array_ma 用 ArraySetAsSeries(true) 让索引 0 对应最新一根,count=3 表示只取最近 3 根均线值,循环拼成字符串用 Comment 打印到图表左上。 iGetArray 是通用拷贝封装:先查数组是否动态,再 ArrayFree 清空,CopyBuffer 拷 count 根;若 copied!=count 说明指标未算完,返回 false 让主逻辑跳过。外汇与贵金属杠杆高,均线读数只反映历史概率,实盘前请在策略测试器用 2020—2023 年数据验证拷贝稳定性。
input ENUM_APPLIED_PRICE Inp_MA_applied_price = PRICE_CLOSE; class=class="str">"cmt">// MA: type of price class="type">int shift = class="num">49; class=class="str">"cmt">// loockback normalization input class="type">int ptsl = class="num">350; class=class="str">"cmt">// points for stoploss input class="type">int pttp = class="num">5000; class=class="str">"cmt">// points for takeprofit class="type">int OnInit() { class=class="str">"cmt">//--- handle_iMA=iMA(_Symbol,my_timeframe,Inp_MA_ma_period,Inp_MA_ma_shift, Inp_MA_ma_method,Inp_MA_applied_price); class=class="str">"cmt">// Initialize the variable here if needed previousValue = class="num">0.0; class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">MqlTick tick; class="type">class="kw">double last_price = tick.ask; SymbolInfoTick(_Symbol,tick); if(SymbolInfoTick(_Symbol,tick)) last=tick.last; class="type">class="kw">double Last = NormalizeDouble(last,_Digits); handle_iMA=iMA(_Symbol,my_timeframe,Inp_MA_ma_period,Inp_MA_ma_shift, Inp_MA_ma_method,Inp_MA_applied_price); class=class="str">"cmt">//--- class="type">class="kw">double array_ma[]; ArraySetAsSeries(array_ma,true); class="type">int start_pos=class="num">0,count=class="num">3; if(!iGetArray(handle_iMA,class="num">0,start_pos,count,array_ma)) class="kw">return; class="type">class="kw">string text=""; for(class="type">int i=class="num">0; i<count; i++) text=text+IntegerToString(i)+": "+DoubleToString(array_ma[i],Digits()+class="num">1)+"\n"; class=class="str">"cmt">//--- Comment(text); class="type">bool iGetArray(class="kw">const class="type">int handle,class="kw">const class="type">int buffer,class="kw">const class="type">int start_pos, class="kw">const class="type">int count,class="type">class="kw">double &arr_buffer[]) { class="type">bool result=true; if(!ArrayIsDynamic(arr_buffer)) { class=class="str">"cmt">//if(InpPrintLog) PrintFormat("ERROR! EA: %s, FUNCTION: %s, this a no dynamic array!",__FILE__,__FUNCTION__); class="kw">return(class="kw">false); } ArrayFree(arr_buffer); 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 copied=CopyBuffer(handle,buffer,start_pos,count,arr_buffer); if(copied!=count) { 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__,count,copied,GetLastError()); class=class="str">"cmt">//--- quit with zero result - it means that the indicator is considered as not calculated class="kw">return(class="kw">false); } class="kw">return(result); }
手数计算与开仓触发的解耦写法
仓位管理最忌硬编码手数。下面这段把「手数怎么算」和「什么时候开仓」拆成两个函数,实盘里改风险比例不用动触发逻辑。 get_lot() 先判断 inp_lot_type:固定手数模式直接走 normalize_lot(inp_lot_fix);比例模式则用 OrderCalcMargin 算 1 标准手所需保证金,再用「账户余额 × 风险百分比 ÷ 单手保证金」得出理论手数。若 OrderCalcMargin 调用失败,回退到固定手数,避免返回 0 导致不开仓。 normalize_lot() 负责把任意手数对齐到品种最小步长:用 SymbolInfoDouble 取 SYMBOL_VOLUME_STEP 和 SYMBOL_VOLUME_MIN,MathFloor(lt/lot_step)*lot_step 向下取整,再 MathMax 兜底最小值。外汇与贵金属杠杆波动大,这一步能挡掉 70% 以上的「无效手数」报错。 开仓段里有个隐蔽条件:previousValue==100 且 Normalizado<100 时才允许反向信号。以 Buy 为例,要求均线数组首项高于 tick.bid,且 rates[5].high < rates[1].low——即早段高点低于近根低点,结构倾向破位回抽失败。sl 用 ask 减 ptsl*_Point,tp 用 bid 加 pttp*_Point,点值参数自己调。 别把 OrderCalcMargin 当万能 它返回的是当时价位的保证金占用,点差扩大或贵金属跳空时实际占用可能偏高,比例手数会被动缩水,回测和实盘手数可能差出 20%~30%。
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); } trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,get_lot(tick.bid),tick.bid,sl,tp,"Buy"); if(class="num">0<=Normalizado<=class="num">100 ) { if(previousValue==class="num">100) { if(Normalizado<class="num">100 && array_ma[class="num">0]>tick.bid && rates[class="num">5].high < rates[class="num">1].low ) { 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"); } if(Normalizado<class="num">100 && array_ma[class="num">0]<tick.ask && rates[class="num">5].low > rates[class="num">1].high ) { Print("Open Order Sell"); Alert(" Selling");
◍ 卖单挂单与均线触碰平仓的代码片段
这段逻辑处理的是空单方向的开仓与退出。先给 Orden 打上 "Sell" 标记,再用 NormalizeDouble 把止损落在 bid 加上 ptsl 个点、止盈落在 ask 减去 pttp 个点的位置上,精度对齐 _Digits,避免报价小数位不匹配被拒单。 真正发单走的是 trade.PositionOpen,市价空单手数由 get_lot(tick.ask) 算,注释掉的那行 Sell 是另一种写法,按需二选一。外汇与贵金属杠杆高,点值波动可能瞬间吃掉预设止损,跑之前先在策略测试器用历史数据验一遍。 退出条件写得很直白:若持空单且当前 K 线最低价跌破 array_ma[0],立刻以代码 5 平仓并打印 "cerro sell";多单对称,最高价破均线就平。return 提前中断后续判断,等于把这一根的处理权交还给下一 tick。
Orden="Sell"; sl=NormalizeDouble(tick.bid + ptsl*_Point,_Digits); tp=NormalizeDouble(tick.ask - pttp*_Point,_Digits); class=class="str">"cmt">//trade.Sell(get_lot_s(tick.ask),_Symbol,tick.ask,sl,tp); trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,get_lot(tick.ask),tick.ask,sl,tp,"Sell"); } } } previousValue = Normalizado; if(Orden=="Sell" && rates[class="num">0].low <array_ma[class="num">0]) { trade.PositionClose(_Symbol,class="num">5); Print("cerro sell"); class="kw">return; } if(Orden=="Buy" && rates[class="num">0].high >array_ma[class="num">0]) { trade.PositionClose(_Symbol,class="num">5); Print("cerro buy"); class="kw">return; }
「把这条线请下神坛」
均值回归思路在 MT5 里只是提供了一种看待价格偏离的视角,不是能直接挂盘收割的圣杯。随文附的 Mean_Reversion_Trading_Strategy.mq5(16.68 KB)在社区回测中有人反馈第 84 行报错、也有人表示无法回测,说明未优化前连跑通都未必稳。 外汇与贵金属杠杆高、滑点跳空频繁,这类策略实盘前必须在历史数据上重做参数寻优,并用小资金验证偏离阈值和持仓周期。 把它当分析市场的辅助线就好,真要下单,先开 MT5 把附档代码编译一遍,看看你的品种在哪些时段回归概率更高。