价格速度测量方法·进阶篇
(2/3)· 肉眼看快慢总被车型大小骗,这篇用物理速度公式拆开 H1 烛形里的真实价格速率
◍ EA 初始化时先把指标句柄攥在手里
写一个 MT5 Expert Advisor,第一步不是在 OnTick 里算信号,而是 OnInit 里把运行环境和外部指标句柄先钉死。这段代码在初始化阶段就做了两道闸门:终端必须允许交易且已连接,否则直接 INIT_FAILED,避免半吊子状态下挂单。 它用 iCustom 去抓两个自定义指标——average_speed 和 CAM,路径分别是 "Speed Price\\average_speed" 和 "Speed Price\\CAM"。CAM 那句把 InpPeriodSlow 默认设成 26,对应 MACD 慢速 EMA 周期,顺带传了 ADX 周期与快速周期。句柄若返回 INVALID_HANDLE,会把错误码用 GetLastError 打出来,方便你直接在专家日志里定位是路径拼错还是指标没装。 拿到句柄后立刻用 ArrayInitialize 把 avr_speed、cam_up 这些缓冲区清成 0.0。这一步很关键:MT5 的数组不初始化就直接 CopyBuffer,可能读到上一次残留值,导致首根 K 线误触发。外汇与贵金属杠杆高,这类初始化疏漏带来的假信号可能直接吃掉本金,开 MT5 编译后建议先空跑一小时看日志零报错再上仓。
class="kw">input class="type">uint InpPeriodSlow = class="num">26; class=class="str">"cmt">// MACD Slow EMA period class=class="str">"cmt">//--- CEngine engine; CTrade trade; class=class="str">"cmt">//--- 声明指标变量和句柄 class="type">class="kw">double lot; class="type">class="kw">ulong magic_number; class="type">uint stoploss; class="type">uint takeprofit; class="type">uint slippage; class="type">int InpInd_Handle1,InpInd_Handle2; class="type">class="kw">double avr_speed[],cam_up[],cam_dn[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 初始化检查 if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { Print(InpEaComment,": 不允许进行交易!"); class="kw">return(INIT_FAILED); } if(!TerminalInfoInteger(TERMINAL_CONNECTED)) { Print(InpEaComment,": 没有连接!"); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- 取得 Average Speed 指标句柄 InpInd_Handle1=iCustom(Symbol(),InpInd_Timeframe,"Speed Price\\average_speed", InpBars, InpPrice ); if(InpInd_Handle1==INVALID_HANDLE) { Print(InpEaComment,": 获取 average_speed 句柄失败"); Print("Handle = ",InpInd_Handle1," error = ",GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- 取得 CAM 指标句柄 InpInd_Handle2=iCustom(Symbol(),InpInd_Timeframe,"Speed Price\\CAM", InpPeriodADX, InpPeriodFast, InpPeriodSlow ); if(InpInd_Handle2==INVALID_HANDLE) { Print(InpEaComment,": 获取 average_speed 句柄失败"); Print("Handle = ",InpInd_Handle2," error = ",GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- ArrayInitialize(avr_speed,class="num">0.0); ArrayInitialize(cam_up,class="num">0.0);
初始化与开仓触发的主循环骨架
EA 在 OnInit 里先把 cam_dn 数组清零,再把 avr_speed、cam_up、cam_dn 都设为时间序列(ArraySetAsSeries 置 true),这样后续下标 0 永远是最新一根 K 线。交易参数如 lot 用 NormalizeLot 夹住下限 MinimumLots,stoploss / takeprofit / slippage 直接读外部输入,trade 对象再绑定滑点、魔术码与按品种填充模式,LogLevel 关到 NO 避免刷日志。 实盘不走 OnTimer 的引擎调用,而是靠 OnTick 驱动;测试器里则在 OnTick 首行强制 engine.OnTimer() 补帧。OnTick 先判断 IsOpenedByMagic 防重复开仓,未持仓才跑 GetIndValue 取指标,再分流 BuySignal / SellSignal。 信号成立时用 CorrectStopLoss / CorrectTakeProfit 把点数止损转成绝对价,trade.Buy 或 trade.Sell 以市价 0 入场并挂 sl/tp。外汇与贵金属杠杆高,这类市价单在跳空时滑点可能吃掉预设止损,上 MT5 回测应先把 InpSlippage 调到与券商一致。
ArrayInitialize(cam_dn,class="num">0.0); ArraySetAsSeries(avr_speed,true); ArraySetAsSeries(cam_up,true); ArraySetAsSeries(cam_dn,true); class=class="str">"cmt">//--- 设置交易参数 lot=NormalizeLot(Symbol(),fmax(InpLot,MinimumLots(Symbol()))); magic_number=InpMagicNum; stoploss=InpStopLoss; takeprofit=InpTakeProfit; slippage=InpSlippage; class=class="str">"cmt">//--- trade.SetDeviationInPoints(slippage); trade.SetExpertMagicNumber(magic_number); trade.SetTypeFillingBySymbol(Symbol()); trade.SetMarginMode(); trade.LogLevel(LOG_LEVEL_NO); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计时器函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer() { if(!MQLInfoInteger(MQL_TESTER)) engine.OnTimer(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易分时函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- 如果在测试器中工作 if(MQLInfoInteger(MQL_TESTER)) engine.OnTimer(); if(!IsOpenedByMagic(InpMagicNum)) { class=class="str">"cmt">//--- 取得计算数据 if(!GetIndValue()) class="kw">return; class=class="str">"cmt">//--- if(BuySignal()) { class=class="str">"cmt">//--- 取得相对止损水平的正确止损和获利价格 class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY,class="num">0,stoploss); class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY,class="num">0,takeprofit); class=class="str">"cmt">//--- 开启买入仓位 trade.Buy(lot,Symbol(),class="num">0,sl,tp); } else if(SellSignal()) { class=class="str">"cmt">//--- 取得相对止损水平的正确止损和获利价格 class="type">class="kw">double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL,class="num">0,stoploss); class="type">class="kw">double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL,class="num">0,takeprofit); class=class="str">"cmt">//--- 开启卖出仓位 trade.Sell(lot,Symbol(),class="num">0,sl,tp); } } }
「信号判定与持仓过滤的函数骨架」
这套逻辑把「趋势速度够不够」和「转向通道有没有画出」拆成两个独立布尔,买信号要求 avr_speed[0] 不低于 InpTrendLev 且 cam_up[0] 不是空值,卖信号对称处理 cam_dn[0]。代码里没给 InpTrendLev 默认值,实盘前你必须在输入参数里显式设定,否则编译能过、信号却可能永远不触发。 GetIndValue 用三次 CopyBuffer 各取 1 根柱的数据,只要任一缓冲区拷贝返回小于 1 就判失败。注意这里拷的是 InpInd_Handle2 的 0/1 号缓冲和 InpInd_Handle1 的 0 号缓冲,句柄若未正确初始化,EA 会静默拿不到值。 IsOpenedByMagic 遍历 PositionsTotal() 返回的持仓数,按幻数计数。它依赖 SelectByIndex 做索引选仓:零售对冲账户走 PositionGetTicket,其他模式走 PositionGetSymbol,两种路径空值都直接返回 false,避免越界读持仓。 别把句柄当永远有效 指标句柄在 OnInit 之外可能失效,GetIndValue 只检查拷贝长度不检查句柄有效性。开 MT5 后建议自己在 OnInit 打印句柄值,若为 INVALID_HANDLE 要立刻 return INIT_FAILED,别等 EA 跑空。
class="type">bool BuySignal() { class="kw">return(avr_speed[class="num">0]>=InpTrendLev && cam_up[class="num">0]!=EMPTY_VALUE)?true:false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool SellSignal() { class="kw">return(avr_speed[class="num">0]>=InpTrendLev && cam_dn[class="num">0]!=EMPTY_VALUE)?true:false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 取得当前指标值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool GetIndValue() { class="kw">return(CopyBuffer(InpInd_Handle2,class="num">0,class="num">0,class="num">1,cam_up)<class="num">1 || CopyBuffer(InpInd_Handle2,class="num">1,class="num">0,class="num">1,cam_dn)<class="num">1 || CopyBuffer(InpInd_Handle1,class="num">0,class="num">0,class="num">1,avr_speed)<class="num">1 )?false:true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 使用幻数检查开启的仓位 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool IsOpenedByMagic(class="type">int MagicNumber) { class="type">int pos=class="num">0; class="type">uint total=PositionsTotal(); class=class="str">"cmt">//--- for(class="type">uint i=class="num">0; i<total; i++) { if(SelectByIndex(i)) if(PositionGetInteger(POSITION_MAGIC)==MagicNumber) pos++; } class="kw">return((pos>class="num">0)?true:false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据索引选择仓位 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool SelectByIndex(class="kw">const class="type">int index) { ENUM_ACCOUNT_MARGIN_MODE margin_mode=(ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE); class=class="str">"cmt">//--- if(margin_mode==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { class="type">class="kw">ulong ticket=PositionGetTicket(index); if(ticket==class="num">0) class="kw">return(false); } else { class="type">class="kw">string name=PositionGetSymbol(index); if(name=="") class="kw">return(false); } class=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 的输入参数 |
◍ 用跳动密度卡住假突破
这套策略把「价格动了多少点」和「用了多少笔 tick 完成」绑在一起判断,而不是单看价位。核心输入是两个阈值:InpPoints=15 表示价格至少要移动 15 点,InpTicks=15 表示这 15 点要在 15 笔 tick 内走完,两者同时成立才视作有效波动。 代码里用 MqlTick 数组分别存当前和之前的分时数据,tick_count 写死为 15,配合 tick_from=0 拉取最新 15 笔分时。这种写法能在 MT5 里直接抓到「短时间密集成交」的痕迹,外汇与贵金属波动快、点差跳变频繁,属于高风险场景,参数需按品种点值重算。 OnInit 里先拿 Symbol() 初始化 m_symbol,失败就返回 INIT_FAILED,说明品种句柄没建起来后面全白搭。开 MT5 把这段挂上 EURUSD 回测,把 InpTicks 从 15 调到 8,能明显看到触发频率变高、但止损命中率可能跟着恶化。
class="kw">input class="type">class="kw">string InpEaComment = "Strategy #class="num">2"; class=class="str">"cmt">// EA Comment class="kw">input class="type">int InpMagicNum = class="num">1111; class=class="str">"cmt">// Magic number class="kw">input class="type">class="kw">double InpLots = class="num">0.1; class=class="str">"cmt">// Lots class="kw">input class="type">uint InpStopLoss = class="num">400; class=class="str">"cmt">// StopLoss in points class="kw">input class="type">uint InpTakeProfit = class="num">500; class=class="str">"cmt">// TakeProfit in points class="kw">input ENUM_COPY_TICKS tick_flags = TICKS_INFO; class=class="str">"cmt">// Ticks resulting from Bid and/or Ask changes class="kw">input class="type">int InpPoints = class="num">15; class=class="str">"cmt">// The price should move NNN points class="kw">input class="type">uchar InpTicks = class="num">15; class=class="str">"cmt">// For XXX ticks class=class="str">"cmt">//--- 取得分时的数组 class="type">MqlTick tick_array_curr[]; class=class="str">"cmt">// 用于当前分时的分时数组 class="type">MqlTick tick_array_prev[]; class=class="str">"cmt">// 用于之前分时的分时数组 class="type">class="kw">ulong tick_from=class="num">0; class=class="str">"cmt">// 如果 tick_from=class="num">0, 给出最新的 tick_count 个分时 class="type">uint tick_count=class="num">15; class=class="str">"cmt">// 应当取得的分时数量 class=class="str">"cmt">//--- class="type">class="kw">double ExtStopLoss=class="num">0.0; class="type">class="kw">double ExtTakeProfit=class="num">0.0; class="type">class="kw">double ExtPoints=class="num">0.0; class="type">bool first_start=false; class="type">long last_trade_time=class="num">0; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if(!m_symbol.Name(Symbol())) class=class="str">"cmt">// 设置交易品种名称 class="kw">return(INIT_FAILED);
用 tick 偏移捕捉突破的第一脚
这段逻辑把前一轮 tick 缓冲和当前 tick 缓冲做时间对齐,靠 time_msc 找到重叠点,从而算出 index_new。若 index_new 有效且当前魔数下无持仓,就进入偏移比较:shift = tick_count-1-index_new-InpTicks,负值夹回 0。 核心判断是 tick_array_curr[tick_count-1].ask 减去 tick_array_curr[shift].ask 是否大于 ExtPoints。ExtPoints 来自 InpPoints*Point(),比如 InpPoints=50 在 XAUUSD 上约 5 美元距离,突破才触发 Buy。 触发后 sl/tp 按入参换算,未设则传 0.0 交给服务器默认识别。外汇与贵金属杠杆高,tick 级突破可能在流动性稀薄时滑点放大,实盘前务必在 MT5 策略测试器用真实 tick 跑一遍。 下面这段是 OnTick 里对齐与下单的骨架,可直接拷进 EA 改 InpTicks 验证灵敏度。
class="type">void OnTick() { class=class="str">"cmt">//--- 检查第一次开始 class="type">int copied=-class="num">1; if(!first_start) { copied=CopyTicks(Symbol(),tick_array_curr,tick_flags,tick_from,tick_count); if(copied!=tick_count) first_start=false; else { first_start=true; ArrayCopy(tick_array_prev,tick_array_curr); } } class=class="str">"cmt">//--- 请求分时 copied=CopyTicks(Symbol(),tick_array_curr,tick_flags,tick_from,tick_count); if(copied!=tick_count) class="kw">return; class="type">int index_new=-class="num">1; class="type">long last_time_msc=tick_array_prev[tick_count-class="num">1].time_msc; for(class="type">int i=(class="type">int)tick_count-class="num">1;i>=class="num">0;i--) { if(last_time_msc==tick_array_curr[i].time_msc) { index_new=i; class="kw">break; } } class=class="str">"cmt">//--- if(index_new!=-class="num">1 && !IsOpenedByMagic(InpMagicNum)) { class="type">int shift=(class="type">int)tick_count-class="num">1-index_new-InpTicks; class=class="str">"cmt">// 当前分时数组的偏移 shift=(shift<class="num">0)?class="num">0:shift; if(tick_array_curr[tick_count-class="num">1].ask-tick_array_curr[shift].ask>ExtPoints) { class=class="str">"cmt">//--- 开启 BUY class="type">class="kw">double sl=(InpStopLoss==class="num">0)?class="num">0.0:tick_array_curr[tick_count-class="num">1].ask-ExtStopLoss; class="type">class="kw">double tp=(InpTakeProfit==class="num">0)?class="num">0.0:tick_array_curr[tick_count-class="num">1].ask+ExtTakeProfit; m_trade.Buy(InpLots,m_symbol.Name(),tick_array_curr[tick_count-class="num">1].ask, m_symbol.NormalizePrice(sl),