图论:Dijkstra(迪杰斯特拉)算法在交易中的应用·进阶篇
🧭

图论:Dijkstra(迪杰斯特拉)算法在交易中的应用·进阶篇

(2/3)· 把波动高低点当节点、价格差距当权重,最短路径算法怎么落地到 MT5 实盘诊断

偏理论进阶 第 2/3 篇
很多交易者把支撑阻力当成静态线,却忽略了价格在两个波动点之间移动的真实成本。用 Dijkstra 建模时,若把权重误设为固定值,算出的“最短路径”会严重偏离实际阻力最小的方向。

「用摆点结构给图表打节点」

这套 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,摆点会更稀疏,噪音可能减少但信号会滞后,外汇与贵金属杠杆高,参数乱调容易放大回撤风险。

MQL5 / C++
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 就能看不同截断下的链路变化。

MQL5 / C++
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 后段即可观察灰线绘制是否符合预期。

MQL5 / C++
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 不出信号。

MQL5 / C++
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);
让小布替你跑这套路径计算
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到波动节点与到达成本的实时距离表,你只管判断哪条路径值得跟。

常见问题

源节点是最近的有效波动点,也就是价格尚未突破的最新高点或低点,作为计算最短到达成本的起点。
固定点数忽略不同价格区间的移动难度,绝对差距或波动率更能反映真实转换成本,路径才贴近最小阻力方向。
可以,小布盯盘对应品种页已内置波动点建模与到达成本表,省去手动维护优先级队列的重复劳动。
算法层面已访问点不再重算,但市场可能回测,已访问仅表示当前路径推导中不再作为待评估分支,不预示未来不触及。