图论:Dijkstra(迪杰斯特拉)算法在交易中的应用·进阶篇
(2/3)· 把波动高低点当节点、价格差距当权重,最短路径算法怎么落地到 MT5 实盘诊断
「用摆点结构给图表打节点」
这套 EA 的思路是先不急着下单,而是把图表上的摆动高低点先识别出来,存进一个自定义结构数组里,后面才谈路径计算。 原文用 SwingPoint 结构记录每个节点的序号、时间、价格、是否高点、是否被访问、距离、是否已用、前驱索引。输入参数里 LeftBars=3、RightBars=3 表示以某根 K 线为中心,左右各比 3 根,若它的最高/最低都未被相邻覆盖,就判定为摆点;MaxSwings=50 限制最多存 50 个节点,避免数组无限膨胀。 DetectSwings 函数先用 ArrayResize(swingPoints,0) 清空,再循环 i 从 left 到 Bars(_Symbol,PERIOD_CURRENT)-right。内层两个 j 循环分别向左、向右比较 High/Low,任一方向出现更高或更低就取消 isHigh/isLow。命中后扩容数组写入——你在 MT5 里把 left/right 改成 5,摆点会更稀疏,噪音可能减少但信号会滞后,外汇与贵金属杠杆高,参数乱调容易放大回撤风险。
class="macro">#class="kw">property copyright "Copyright class="num">2025, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict class="macro">#include <Trade/Trade.mqh> CTrade trade; class=class="str">"cmt">// Input Parameters input class="type">int TakeProfit = class="num">1000; input class="type">int StopLoss = class="num">385; input class="type">class="kw">double In_Lot = class="num">0.01; input class="type">int LeftBars = class="num">3; input class="type">int RightBars = class="num">3; input class="type">int MaxSwings = class="num">50; input class="type">class="kw">double Lots = class="num">0.1; input class="type">class="kw">double PointBuffer = class="num">10; input class="type">int Slippage = class="num">5; ENUM_TIMEFRAMES TimeFrame; class=class="str">"cmt">// Node Structure class="kw">struct SwingPoint { class="type">int index; class="type">class="kw">datetime time; class="type">class="kw">double price; class="type">bool isHigh; class="type">bool visited; class="type">class="kw">double distance; class="type">bool used; class="type">int previous; }; SwingPoint swingPoints[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnInit | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { Print("Dijkstra Swing EA initialized"); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Detect swing highs and lows | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DetectSwings(class="type">int left, class="type">int right) { ArrayResize(swingPoints, class="num">0); class="type">int totalBars = Bars(_Symbol, PERIOD_CURRENT) - right; for (class="type">int i = left; i < totalBars; i++) { class="type">bool isHigh = true, isLow = true; class="type">class="kw">double high = High(i), low = Low(i); for (class="type">int j = class="num">1; j <= left; j++) { if (High(i - j) >= high) isHigh = false; if (Low(i - j) <= low) isLow = false; } for (class="type">int j = class="num">1; j <= right; j++) { if (High(i + j) >= high) isHigh = false; if (Low(i + j) <= low) isLow = false; } if (isHigh || isLow) { class="type">int idx = ArraySize(swingPoints);
用最短路把摆动点串成链
把识别出的摆动点存进数组后,下一步是计算它们之间的连通代价。下面这段 ApplyDijkstra 把每个点的初始距离设为 DBL_MAX,仅把索引 0 的距离置 0,相当于从第一个摆动点出发找全图最便宜的价格路径。 代价函数只用 MathAbs 取两点价差:swingPoints[u].price 与 swingPoints[v].price 的差绝对值。若经 u 中转比 v 当前记录的距离更短,就更新 v 的 distance 与 previous 指针,从而在数组里隐式存下一条最优摆动链。 可视化函数 VisualizeSwings 给每个点画 OBJ_ARROW:高点 arrowcode 233 染红,低点 234 染蓝。你在 MT5 加载后,图表上红蓝箭头的数量等于 ArraySize(swingPoints),若 MaxSwings 设 50,则最多出现 50 个箭头。外汇与贵金属波动剧烈,这类路径仅描述历史价格拓扑,不代表后续方向概率。 别把箭头数当信号密度。箭头只是摆动残留,真正可验证的是 previous 字段——顺着它回溯能还原一条价差累计最小的摆动序列,复制代码改 MaxSwings 就能看不同截断下的链路变化。
class="type">void ApplyDijkstra() { if (ArraySize(swingPoints) == class="num">0) class="kw">return; swingPoints[class="num">0].distance = class="num">0; for (class="type">int i = class="num">0; i < ArraySize(swingPoints); i++) { class="type">int u = -class="num">1; class="type">class="kw">double minDist = DBL_MAX; for (class="type">int j = class="num">0; j < ArraySize(swingPoints); j++) { if (!swingPoints[j].visited && swingPoints[j].distance < minDist) { minDist = swingPoints[j].distance; u = j; } } if (u == -class="num">1) break; swingPoints[u].visited = true; for (class="type">int v = class="num">0; v < ArraySize(swingPoints); v++) { if (!swingPoints[v].visited) { class="type">class="kw">double cost = MathAbs(swingPoints[u].price - swingPoints[v].price); if (swingPoints[u].distance + cost < swingPoints[v].distance) { swingPoints[v].distance = swingPoints[u].distance + cost; swingPoints[v].previous = u; } } } } } class="type">void VisualizeSwings() { for (class="type">int i = class="num">0; i < ArraySize(swingPoints); i++) { class="type">class="kw">string objName = "Swing_" + IntegerToString(i); ObjectDelete(class="num">0, objName); ObjectCreate(class="num">0, objName, OBJ_ARROW, class="num">0, swingPoints[i].time, swingPoints[i].price); ObjectSetInteger(class="num">0, objName, OBJPROP_ARROWCODE, swingPoints[i].isHigh ? class="num">233 : class="num">234); ObjectSetInteger(class="num">0, objName, OBJPROP_COLOR, swingPoints[i].isHigh ? clrRed : clrBlue); }
◍ 摆动点连线与有效性过滤的实现细节
把识别出的 swing 点连成趋势线,核心是遍历 swingPoints 数组从索引 1 开始,用前一个点的 previous 字段回溯。若 previous 不等于 -1,就先删掉旧的同名对象再建 OBJ_TREND,坐标取两点各自的 time 与 price,颜色统一 clrGray、线宽 1,这样图表上不会残留重复线段。 过滤逻辑放在 FilterAndMarkValidSwings 里:数组长度小于 2 直接返回;对每个未使用的点,向前(j 从 swingIndex-1 到 0)扫描同周期 K 线。若是 swing high 但后面出现更高 high,或 swing low 但后面出现更低 low,就判为无效并 break,否则标记 used=true 准备绘图。 辅助函数 High()、Low()、Time() 只是对 iHigh/iLow/iTime 的薄封装,默认用 _Symbol 和 _Period;而过滤扫描里显式传了 TimeFrame 变量,说明可脱离当前周期去校验历史摆动,实盘里若切周期需确认该变量已赋值,否则可能取到空值。 外汇与贵金属波动受杠杆与消息面影响,摆动点失效概率不低,上述连线仅作结构参考,开 MT5 把这段代码塞进 EA 的 OnCalculate 后段即可观察灰线绘制是否符合预期。
for (class="type">int i = class="num">1; i < ArraySize(swingPoints); i++) { class="type">int prev = swingPoints[i].previous; if (prev != -class="num">1) { class="type">class="kw">string lineName = "Line_" + IntegerToString(i); ObjectDelete(class="num">0, lineName); ObjectCreate(class="num">0, lineName, OBJ_TREND, class="num">0, swingPoints[prev].time, swingPoints[prev].price, swingPoints[i].time, swingPoints[i].price); ObjectSetInteger(class="num">0, lineName, OBJPROP_COLOR, clrGray); ObjectSetInteger(class="num">0, lineName, OBJPROP_WIDTH, class="num">1); } } } class="type">class="kw">double High(class="type">int index){class="kw">return (iHigh(_Symbol, _Period, index));} class="type">class="kw">double Low(class="type">int index){class="kw">return (iLow(_Symbol, _Period, index));} class="type">class="kw">datetime Time(class="type">int index){class="kw">return (iTime(_Symbol, _Period, index));} class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Filter and mark | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void FilterAndMarkValidSwings(SwingPoint &points[]) { class="type">int count = ArraySize(points); if(count < class="num">2) class="kw">return; for(class="type">int i = class="num">0; i < count; i++) { if(points[i].used) class="kw">continue; class="type">bool isValid = true; class="type">class="kw">double swingPrice = points[i].price; class="type">int swingIndex = points[i].index; class=class="str">"cmt">// Scan forward in time from the swing point for(class="type">int j = swingIndex - class="num">1; j >= class="num">0; j--) { class="type">class="kw">double high = iHigh(_Symbol, TimeFrame, j); class="type">class="kw">double low = iLow(_Symbol, TimeFrame, j); class=class="str">"cmt">// Invalidate swing high if price went higher later if(points[i].isHigh && high > swingPrice) { isValid = false; break; } class=class="str">"cmt">// Invalidate swing low if price went lower later if(!points[i].isHigh && low < swingPrice) { isValid = false; break; } } if(isValid) { points[i].used = true; class=class="str">"cmt">// Draw object on chart
「摆点清理与信号触发的收尾逻辑」
摆点画完之后,图表上会累积大量历史水平线。CleanOldSwingObjects 默认保留最近 100 根 K 线对应的摆动对象,超出时间跨度的 SwingHigh_ / SwingLow_ 对象直接删除,避免图形垃圾拖慢 MT5 终端渲染。 删除逻辑先取 ObjectsTotal(0) 总数,倒序遍历时用 StringFind 判断名称前缀是否为 SwingHigh_ 或 SwingLow_(返回 0 即命中),再比对 OBJPROP_TIME 与 oldestDate,早于边界就 ObjectDelete。实盘跑 EURUSD 15 分钟图时,若 keepBars 设 100,大约每 25 小时会自动清掉一批旧线。 GenerateSignalAndTrade 是整套摆动策略的出口。它要求 swingPoints 至少 2 个元素,取末位与 previous 指向的前一个摆点:末位价高于参考价倾向做多,低于则倾向做空,随后由 SetSLTP 算止损止盈、用 PositionSelect 防重复开仓。外汇与贵金属杠杆高,信号仅代表概率倾向,不代表必然成交优势。 这段代码没贴全 ExecuteTrade 内部,但开仓分支已清晰:无持仓且 isBuy 为真发 ORDER_TYPE_BUY,否则 isSell 发 ORDER_TYPE_SELL。复制进 EA 前,先确认 swingPoints 结构体的 previous 字段在前面环节被正确赋值,否则 prev==-1 会直接 return 不出信号。
class="type">class="kw">string objName = points[i].isHigh ? StringFormat("SwingHigh_%d", TimeToString(iTime(_Symbol, TimeFrame, swingIndex))) : StringFormat("SwingLow_%d", TimeToString(iTime(_Symbol, TimeFrame, swingIndex))); class="type">class="kw">color swingColor = points[i].isHigh ? clrRed : clrBlue; ObjectCreate(class="num">0, objName, OBJ_HLINE, class="num">0, class="num">0, swingPrice); ObjectSetInteger(class="num">0, objName, OBJPROP_COLOR, swingColor); ObjectSetInteger(class="num">0, objName, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(class="num">0, objName, OBJPROP_WIDTH, class="num">1); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Cleaning up old swings | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CleanOldSwingObjects(class="type">int keepBars = class="num">100) { class="type">class="kw">datetime oldestDate = iTime(_Symbol, TimeFrame, keepBars); class="type">int total = ObjectsTotal(class="num">0); for(class="type">int i = total - class="num">1; i >= class="num">0; i--) { class="type">class="kw">string name = ObjectName(class="num">0, i); if(StringFind(name, "SwingHigh_") == class="num">0 || StringFind(name, "SwingLow_") == class="num">0) { class="type">class="kw">datetime swingTime = (class="type">class="kw">datetime)ObjectGetInteger(class="num">0, name, OBJPROP_TIME); if(swingTime < oldestDate) { ObjectDelete(class="num">0, name); } } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Generate Signal & Trade | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GenerateSignalAndTrade() { if (ArraySize(swingPoints) < class="num">2) class="kw">return; class="type">int last = ArraySize(swingPoints) - class="num">1; class="type">int prev = swingPoints[last].previous; if (prev == -class="num">1) class="kw">return; class="type">class="kw">double entry = swingPoints[last].price; class="type">class="kw">double reference = swingPoints[prev].price; class="type">class="kw">double sl, tp; class="type">bool isBuy = entry > reference, isSell = entry < reference; SetSLTP(entry, reference, isBuy, sl, tp); if (PositionSelect(_Symbol)) class="kw">return; if (isBuy) ExecuteTrade(ORDER_TYPE_BUY); else if(isSell) ExecuteTrade(ORDER_TYPE_SELL);