利用指标实时优化智能交易系统·进阶篇
(2/3)· 当历史优化参数随行情漂移失效,让指标在实时线程里替 EA 跑出当下最优解
◍ 句柄获取失败就直接退初始化
在 MT5 自定义指标里,RSI、WPR、ADX 三个底层句柄必须在 OnInit 阶段拿到,任一返回 INVALID_HANDLE 就说明当前图表品种或周期不支持该调用,此时应打印错误码并 return(INIT_FAILED),否则后续CopyBuffer会全盘错乱。 下面这段是 WPR 与 ADX 的获取及失败处理,结构同 RSI:先 iWPR(Symbol(),PERIOD_CURRENT,WPRPeriod) 取句柄,若等于 INVALID_HANDLE 则 Print 出句柄值和 GetLastError(),再 return(INIT_FAILED)。ADX 同理用 iADX 并校验。 拿到三个句柄后,要把 9 个计算缓冲(概率、成交次数、信号、盈利因子等)通过 SetIndexBuffer(0~8,...) 映射到 INDICATOR_CALCULATIONS,并对每个缓冲和 rsi/wpr/adx 数组执行 ArraySetAsSeries(...,true) 让索引 0 对应最新 K 线。最后清掉 Deals 容器、复位 last_deal=0、设短名,返回 INIT_SUCCEEDED 才算初始化过关。外汇与贵金属品种波动大,句柄失败常发生在非常规周期切换时,建议手动切周期验证。
wpr_handle=iWPR(Symbol(),PERIOD_CURRENT,WPRPeriod); if(wpr_handle==INVALID_HANDLE) { Print("Test Indicator",": Failed to get WPR handle"); Print("Handle = ",wpr_handle," error = ",GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- 获取 ADX 指标句柄 adx_handle=iADX(Symbol(),PERIOD_CURRENT,ADXPeriod); if(adx_handle==INVALID_HANDLE) { Print("Test Indicator",": Failed to get ADX handle"); Print("Handle = ",adx_handle," error = ",GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- 指标缓存区映射 SetIndexBuffer(class="num">0,Buffer_Probability,INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">1,Buffer_DealsCount,INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">2,Buffer_TradeSignal,INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">3,Buffer_ProfitFactor,INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">4,Buffer_ProfitCount,INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">5,Buffer_TakeProfit,INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">6,Buffer_StopLoss,INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">7,Buffer_DealsCountCurrent,INDICATOR_CALCULATIONS); SetIndexBuffer(class="num">8,Buffer_ProfitCountCurrent,INDICATOR_CALCULATIONS); ArraySetAsSeries(Buffer_Probability,true); ArraySetAsSeries(Buffer_ProfitFactor,true); ArraySetAsSeries(Buffer_TradeSignal,true); ArraySetAsSeries(Buffer_DealsCount,true); ArraySetAsSeries(Buffer_ProfitCount,true); ArraySetAsSeries(Buffer_TakeProfit,true); ArraySetAsSeries(Buffer_StopLoss,true); ArraySetAsSeries(Buffer_DealsCountCurrent,true); ArraySetAsSeries(Buffer_ProfitCountCurrent,true); class=class="str">"cmt">//--- ArraySetAsSeries(rsi,true); ArraySetAsSeries(wpr,true); ArraySetAsSeries(adx,true); Deals.Clear(); last_deal=class="num">0; class=class="str">"cmt">//--- IndicatorSetString(INDICATOR_SHORTNAME,"Test Indicator"); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); }
指标缓冲拉取与OnCalculate的初始化边界
多指标协同计算时,第一步必须确认三个缓冲都拿到了数据。下面这段判断若 WPR、ADX、RSI 任意一个 CopyBuffer 返回值 ≤0,直接 return -1 终止,避免后续用空数组算出垃圾信号。 depth 随后被重定义为三个数组尺寸的最小值,这是防止越界的最朴素做法——实际跑下来,若某一指标 handle 在切换周期时失效,depth 可能从请求的 500 缩到 0,信号直接断流。 OnCalculate 里对 prev_calculated<=0 的分支做了全缓冲清零,并用 HistoryDepth 限制首次加载长度。注意 Buffer_TakeProfit 初始化乘的是 _Point 而非 _Digits 换算后的绝对价,意味着 TakeProfit 参数单位是点,黄金和欧美在同一参数下绝对距离差十几倍,外汇贵金属高风险,参数移植前先核对点值。 数组倒序循环从 total-3 起步而非 total-1,给指标滞后留了 3 根的余量,否则第 0 根用未定稿的 close 算概率会抖动。
if(CopyBuffer(wpr_handle,MAIN_LINE,class="num">0,depth,wpr)<=class="num">0 || CopyBuffer(adx_handle,MAIN_LINE,class="num">0,depth,adx)<=class="num">0 || CopyBuffer(rsi_handle,MAIN_LINE,class="num">0,depth,rsi)<=class="num">0) class="kw">return -class="num">1; depth=MathMin(ArraySize(rsi),MathMin(ArraySize(wpr),ArraySize(adx))); class=class="str">"cmt">//--- class="kw">return depth; } 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">//--- class="type">int total=rates_total-prev_calculated; if(prev_calculated<=class="num">0) { total=fmin(total,HistoryDepth); class=class="str">"cmt">//--- ArrayInitialize(Buffer_Probability,class="num">0); ArrayInitialize(Buffer_ProfitFactor,class="num">0); ArrayInitialize(Buffer_TradeSignal,class="num">0); ArrayInitialize(Buffer_DealsCount,class="num">0); ArrayInitialize(Buffer_ProfitCount,class="num">0); ArrayInitialize(Buffer_TakeProfit,TakeProfit*_Point); ArrayInitialize(Buffer_StopLoss,StopLoss*_Point); ArrayInitialize(Buffer_DealsCountCurrent,class="num">0); ArrayInitialize(Buffer_ProfitCountCurrent,class="num">0); } if(total>class="num">0) { total=MathMin(GetIndValue(total+class="num">2),rates_total); if(total<=class="num">0) class="kw">return prev_calculated; if(!ArraySetAsSeries(open,true) || !ArraySetAsSeries(high,true) || !ArraySetAsSeries(low,true) || !ArraySetAsSeries(close,true) || !ArraySetAsSeries(time,true) || !ArraySetAsSeries(spread,true)) class="kw">return prev_calculated; for(class="type">int i=total-class="num">3;i>=class="num">0;i--) { Buffer_TakeProfit[i]=TakeProfit*_Point;
「回测引擎里的挂单与跨周期撮合」
这段逻辑干的事很直接:在非 M1 周期上,用历史 K 线的 OHLC 去回放已生成的虚拟订单。先给缓冲区写止损距离(StopLoss*_Point)并把成交与盈利计数清零,只有当前 bar 时间晚于上一次真实成交时才允许产生新信号。 买信号触发时,开盘价加了 spread[_Point] 作为入场,止损在下方 StopLoss 点、止盈在上方 TakeProfit 点;卖信号则不加 spread,直接以 open[i] 入场,止损在上、止盈在下。两种情况下都用 new CDeal 塞进 Deals 容器,并在 Buffer_TradeSignal 写 1 或 -1。 关键在周期对齐:若当前不是 PERIOD_M1(PeriodSeconds()!=60),就用 CopyRates 拉 M1 数据,从 time[i] 到下一根 bar 时间(或 TimeCurrent)逐根回放。CheckDeals 拿每根 M1 的 open/high/low/close 去判定平仓,closed 与 profit 累计后回流到缓冲区。 开 MT5 把这段塞进自定义指标,把 StopLoss、TakeProfit 设成输入参数,切到 H1 加载,就能看到 M1 撮合下的信号缓冲和理论平仓分布——外汇与贵金属杠杆高,回测盈利不代表实盘概率。
Buffer_StopLoss[i]=StopLoss*_Point; Buffer_DealsCount[i]=Buffer_DealsCountCurrent[i]=class="num">0; Buffer_ProfitCount[i]=Buffer_ProfitCountCurrent[i]=class="num">0; if(last_deal<time[i]) { if(BuySignal(i)) { class="type">class="kw">double open_price=open[i]+spread[i]*_Point; class="type">class="kw">double sl=open_price-StopLoss*_Point; class="type">class="kw">double tp=open_price+TakeProfit*_Point; CDeal *temp=new CDeal(_Symbol,rates_total-i,POSITION_TYPE_BUY,time[i],open_price,sl,tp); if(temp!=NULL) Deals.Add(temp); Buffer_TradeSignal[i]=class="num">1; } else class=class="str">"cmt">/*BuySignal*/ if(SellSignal(i)) { class="type">class="kw">double open_price=open[i]; class="type">class="kw">double sl=open_price+StopLoss*_Point; class="type">class="kw">double tp=open_price-TakeProfit*_Point; CDeal *temp=new CDeal(_Symbol,rates_total-i,POSITION_TYPE_SELL,time[i],open_price,sl,tp); if(temp!=NULL) Deals.Add(temp); Buffer_TradeSignal[i]=-class="num">1; } else class=class="str">"cmt">/*SellSignal*/ Buffer_TradeSignal[i]=class="num">0; } if(Deals.Total()>class="num">0) { if(PeriodSeconds()!=class="num">60) { class="type">MqlRates rates[]; class="type">int rat=CopyRates(_Symbol,PERIOD_M1,time[i],(i>class="num">0 ? time[i-class="num">1] : TimeCurrent()),rates); class="type">int closed=class="num">0, profit=class="num">0; for(class="type">int r=class="num">0;(r<rat && Deals.Total()>class="num">0);r++) { CheckDeals(rates[r].open,rates[r].high,rates[r].low,rates[r].close,rates[r].spread,rates[r].time,closed,profit); if(closed>class="num">0)
◍ 滚动窗口下的胜率与盈亏比递推
这段逻辑处理的是非 60 秒周期与 60 秒周期下,历史成交笔数、盈利笔数的滚动累加。若当前图表周期不是 1 分钟(PeriodSeconds()!=60),代码直接对每根 K 线调用 CheckDeals 把 closed、profit 计入当前柱的缓冲区;若是 1 分钟周期且 rat<0,则按方向分别补算并累加,保证不同时间框架下统计口径一致。 真正的关键在后面的滑动窗口归一。以 Buffer_DealsCount[i] 为例,它等于上一窗口值加上当前柱新增、再减去 HistoryDepth 根之前的柱贡献,用 NormalizeDouble(...,0) 取整,相当于一个长度为 HistoryDepth 的 deque 式计数,避免每根重算全历史。 胜率 Buffer_Probability 用了 EMA 式平滑:pr=2/(AveragePeriod-1),当样本数 ≥ AveragePeriod 时,当前胜率权重为 pr、前一柱胜率权重为 1-pr。若样本不足则直接用 Buffer_ProfitCount/Buffer_DealsCount*100 作裸值。 盈亏比 Buffer_ProfitFactor 在亏损笔数多于盈利笔数时才计算:temp=(盈利*TakeProfit)/(StopLoss*(总-盈利)),同样按 pr 做递推平滑。外汇与贵金属杠杆高,这类统计只反映历史样本倾向,实盘信号可能随点差跳变失效,开 MT5 把 HistoryDepth 与 AveragePeriod 调小到 50/20 可直观看缓冲区的递推抖动。
{
Buffer_DealsCountCurrent[i]+=closed;
Buffer_ProfitCountCurrent[i]+=profit;
}
}
if(rat<class="num">0)
{
CheckDeals(open[i],high[i],low[i],close[i],spread[i],time[i],closed,profit);
Buffer_DealsCountCurrent[i]+=closed;
Buffer_ProfitCountCurrent[i]+=profit;
}
}
else class=class="str">"cmt">/* PeriodSeconds()!=class="num">60 */
{
class="type">int closed=class="num">0, profit=class="num">0;
CheckDeals(open[i],high[i],low[i],close[i],spread[i],time[i],closed,profit);
Buffer_DealsCountCurrent[i]+=closed;
Buffer_ProfitCountCurrent[i]+=profit;
}
} class=class="str">"cmt">/* Deals.Total()>class="num">0 */
Buffer_DealsCount[i+class="num">1]=NormalizeDouble(Buffer_DealsCount[i+class="num">2]+Buffer_DealsCountCurrent[i+class="num">1]-((i+HistoryDepth+class="num">1)<rates_total ? Buffer_DealsCountCurrent[i+HistoryDepth+class="num">1] : class="num">0),class="num">0);
Buffer_ProfitCount[i+class="num">1]=NormalizeDouble(Buffer_ProfitCount[i+class="num">2]+Buffer_ProfitCountCurrent[i+class="num">1]-((i+HistoryDepth+class="num">1)<rates_total ? Buffer_ProfitCountCurrent[i+HistoryDepth+class="num">1] : class="num">0),class="num">0);
Buffer_DealsCount[i]=NormalizeDouble(Buffer_DealsCount[i+class="num">1]+Buffer_DealsCountCurrent[i]-((i+HistoryDepth)<rates_total ? Buffer_DealsCountCurrent[i+HistoryDepth] : class="num">0),class="num">0);
Buffer_ProfitCount[i]=NormalizeDouble(Buffer_ProfitCount[i+class="num">1]+Buffer_ProfitCountCurrent[i]-((i+HistoryDepth)<rates_total ? Buffer_ProfitCountCurrent[i+HistoryDepth] : class="num">0),class="num">0);
if(Buffer_DealsCount[i]>class="num">0)
{
class="type">class="kw">double pr=class="num">2.0/(AveragePeriod-class="num">1.0);
Buffer_Probability[i]=((i+class="num">1)<rates_total && Buffer_Probability[i+class="num">1]>class="num">0 && Buffer_DealsCount[i+class="num">1]>=AveragePeriod ? Buffer_ProfitCount[i]/Buffer_DealsCount[i]*class="num">100*pr+Buffer_Probability[i+class="num">1]*(class="num">1-pr) : Buffer_ProfitCount[i]/Buffer_DealsCount[i]*class="num">100);
if(Buffer_DealsCount[i]>Buffer_ProfitCount[i])
{
class="type">class="kw">double temp=(Buffer_ProfitCount[i]*TakeProfit)/(StopLoss*(Buffer_DealsCount[i]-Buffer_ProfitCount[i]));
Buffer_ProfitFactor[i]=((i+class="num">1)<rates_total && Buffer_ProfitFactor[i+class="num">1]>class="num">0 ? temp*pr+Buffer_ProfitFactor[i+class="num">1]*(class="num">1-pr) : temp);逐笔成交的平仓判定逻辑
这段函数把历史挂着的模拟单按当前 K 线的 open/high/low/close 与 spread 逐一回放,决定哪些单在该根棒内被扫平。它返回 bool 表示整体执行是否干净,同时通过引用参数 closed 与 profit 把平仓总数和其中盈利单数交回上层,方便后续直接算胜率。 函数开头先清零 closed、profit 并把 result 置 true,随后用 for 循环遍历 Deals 容器里的每一笔。若发现指针失效就尝试 Delete 并回退下标,删不掉就把 result 改成 false 并继续;时间晚于当前 time 的单直接跳过,不参加本根判定。 核心判定分两路:先 deal.Tick(open,spread) 试平,命中就 closed++,利润为正则 profit++,然后删对象;不中再按多空类型走 switch。买单调 low 试止损、high 试止盈,卖单逻辑对称。每平一笔都做指针有效性复核再 delete,避免悬垂指针。 在 MT5 里把这段直接塞进自定义指标或 EA 的回测回路,把 Deals 换成你自己的持仓队列,就能看到某根棒实际扫掉几单、盈利几单——外汇与贵金属杠杆高,回测过关不代表实盘概率同分布,点差跳空都可能让 closed 统计偏离。
class="type">bool CheckDeals(class="type">class="kw">double open,class="type">class="kw">double high,class="type">class="kw">double low,class="type">class="kw">double close,class="type">class="kw">double spread,class="type">class="kw">datetime time,class="type">int &closed, class="type">int &profit) { closed=class="num">0; profit=class="num">0; class="type">bool result=true; for(class="type">int i=class="num">0;i<Deals.Total();i++) { CDeal *deal=Deals.At(i); if(CheckPointer(deal)==POINTER_INVALID) { if(Deals.Delete(i)) i--; else result=class="kw">false; class="kw">continue; } if(deal.GetTime()>time) class="kw">continue; if(deal.Tick(open,spread)) { closed++; if(deal.GetProfit()>class="num">0) profit++; if(Deals.Delete(i)) i--; if(CheckPointer(deal)!=POINTER_INVALID) class="kw">delete deal; class="kw">continue; } class="kw">switch(deal.Type()) { case POSITION_TYPE_BUY: if(deal.Tick(low,spread)) { closed++; if(deal.GetProfit()>class="num">0) profit++; if(Deals.Delete(i)) i--; if(CheckPointer(deal)!=POINTER_INVALID) class="kw">delete deal; class="kw">continue; } if(deal.Tick(high,spread)) { closed++; if(deal.GetProfit()>class="num">0) profit++; if(Deals.Delete(i)) i--; if(CheckPointer(deal)!=POINTER_INVALID)
「卖单平仓分支的指针回收细节」
上面这段是持仓遍历里针对 POSITION_TYPE_SELL 的处理尾段。逻辑上先尝试用 high 加 spread 去触碰止盈,命中就 closed++ 并统计 profit;没命中再用 low 加 spread 试一次,同样命中就计数。 每一处成功删除都先对 Deals 数组做 Delete(i) 并把 i 自减,避免漏检移位后的元素;随后用 CheckPointer(deal)!=POINTER_INVALID 判断对象有效性,再 delete deal 释放内存,最后 continue 跳到下一轮。 若两个 Tick 分支都没触发,则 break 退出 switch,由外层循环继续推进。这一写法在 MT5 回测中可将因悬空指针导致的异常概率压到接近零,建议你直接把这段贴进 EA 的持仓扫描函数里跑一遍 EURUSD 的 M15 历史数据验证。
class="kw">delete deal; class="kw">continue; } break; case POSITION_TYPE_SELL: if(deal.Tick(high,spread)) { closed++; if(deal.GetProfit()>class="num">0) profit++; if(Deals.Delete(i)) i--; if(CheckPointer(deal)!=POINTER_INVALID) class="kw">delete deal; class="kw">continue; } if(deal.Tick(low,spread)) { closed++; if(deal.GetProfit()>class="num">0) profit++; if(Deals.Delete(i)) i--; if(CheckPointer(deal)!=POINTER_INVALID) class="kw">delete deal; class="kw">continue; } break; } } class=class="str">"cmt">//--- class="kw">return result; }