图论:Dijkstra(迪杰斯特拉)算法在交易中的应用
概述
在本文中,我们将探讨 Dijkstra 算法的实现,该算法是图论中的一个基本概念,因其解决最短路径问题的效率而闻名。该算法传统上应用于路由和网络优化,我们将通过将价格变动建模为加权图,将其重新应用于金融市场。在这里,节点代表价格水平或时间间隔,而边则反映了在这些节点之间转换的成本(或概率)。
我们的目标是利用 Dijkstra 算法来预测下一个可能的价格数据序列,从而有效地确定价格从当前位置到未来值可能采取的“最短路径”。我们将市场动态视为一张图,旨在确定最有可能的发展轨迹,从而基于最小的阻力或成本来优化交易决策。
图论为分析复杂市场结构提供了一个强大的框架,而 Dijkstra 算法则为在其中导航提供了一种系统性的方法。通过将价格变动解释为带有权重(如波动率)的边,我们可以计算出最小化风险或最大化效率的最优路径。
预测价格数组本质上是从当前价格到未来价格水平的最短距离,为交易者提供了一种基于数据的方法来预测趋势。这种方法将算法交易与计算数学相结合,展示了经典图算法如何揭示金融时间序列数据中隐藏的机会。
Dijkstra 算法的基本原理
| 术语 | 我们的解读 |
|---|---|
| 图:节点和边的集合。 | 图: 图表的结构由波动高点和波动低点构成, 每个价格波动点都成为一个节点,它们之间的每条价格路径都成为一条边。 |
| 权重:从一个节点到另一个节点的旅行成本。 | 权重:价格在两个波动点之间移动所需的成本(或代价)。这可以是绝对价格差距。 |
| 源节点:算法的起点。 | 源节点:最近的有效波动点(价格尚未突破的最新高点/低点)。这是我们计算最短路径的起点。 |
| 已访问集合:已完成处理的节点。 | 已访问集合:算法已经评估过且不会再次考虑的所有波动点。用交易术语来说,这些是价格已经突破或已经向其移动的波动点。 |
| 距离表:记录到每个节点的最短距离。 | 距离表:将每个节点映射到从源节点到该节点的最短“到达成本”值。在交易中,它告诉你价格从当前点移动到任何其他波动点能有多便宜(或容易)。 |
分步流程:
1.初始化:
- 将到源节点的距离设置为 0。
- 将到所有其他节点的距离设置为无穷大。
- 创建一个优先级队列(或最小堆),始终选择已知距离最小的节点。
2.访问最近的未访问节点:
- 从源节点开始。
- 对于每个邻居,计算:
new_distance = distance_to_current + edge_weight
如果这个 `new_distance` 小于先前已知的距离,则更新它。
3.将当前节点标记为已访问:
- 一旦处理完毕,我们就不会再重新处理。
4.重复:
- 继续访问下一个最近的未访问节点。
- 重复此过程,直到访问所有节点或找到最短路径。
while unvisited nodes remain: select node with the smallest tentative distance for each neighbor: if new path to neighbor is shorter: update the shortest distance mark current node as visited

开始实现
//+------------------------------------------------------------------+ //| Dijkstars Algo.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property strict #include <Trade/Trade.mqh> CTrade trade;
我们首先包含 `trade.mqh` 文件,该文件通过 `CTrade` 类为我们提供了对内置 MQL5 交易功能的访问。 此类提供以编程方式进行下单、修改和平仓的方法。包含该文件后,我们创建一个名为 `trade` 的 `CTrade` 实例,我们将在整个 EA 交易系统中使用它来发送诸如 `Buy()`、`Sell()` 和 `PositionOpen()` 之类的交易命令。这种设置对于 EA 中订单执行的自动化至关重要。
// Input Parameters input int TakeProfit = 1000; input int StopLoss = 385; input double In_Lot = 0.01; input int LeftBars = 3; input int RightBars = 3; input int MaxSwings = 50; input double Lots = 0.1; input double PointBuffer = 10; input int Slippage = 5; ENUM_TIMEFRAMES TimeFrame;
在这一部分中,我们定义了 EA 交易系统的输入参数,允许交易者直接通过 EA 界面自定义关键设置。`TakeProfit` 和 `StopLoss` 设置目标盈利点数和止损点数,而 `In_Lot` 和 `Lots` 定义交易规模。`LeftBars` 和 `RighBars` 通过比较相邻 K 线来检测波动高点和低点。`MaxSwings` 限制跟踪的波动点数,而 `pointBuffer` 为止损/止盈设置额外的缓冲距离以确保安全。`Slippage` 设置订单执行期间允许的最大价格偏差,而 `TimeFrame` 指定 EA 将分析的图表周期。
// Node Structure struct SwingPoint { int index; datetime time; double price; bool isHigh; bool visited; double distance; bool used; int previous; };
该结构体定义了 SwingPoint 的蓝图,它表示基于 Dijkstra 的交易系统中的一个节点。每个波动点都包含重要信息:
- index 是找到波动的柱编号。
- time 是该柱的精确时间戳。
- price 是该波动幅度下的最高值或最低值。
- isHigh 指示它是高点(true)还是低点(false)。
- visited 有助于跟踪哪些节点已被算法处理。
- distance 存储了 Dijkstra 路径查找算法中从源节点计算出的成本。
- used 标志,表示此波动已在交易决策中使用过。
- previous 跟踪最短路径链中的前一个节点。
SwingPoint swingPoints[]; //+------------------------------------------------------------------+ //| OnInit | //+------------------------------------------------------------------+ int OnInit() { Print("Dijkstra Swing EA initialized"); return INIT_SUCCEEDED; }
这里声明了一个动态数组 `swingPoints[]`,用于存储图表上所有检测到的波动高点和低点(作为 `SwingPoint` 结构)。该数组将在整个 EA 中填充和使用,以表示价格行为图中的节点。在 `OnInit()` 函数中,EA 只是向终端打印一条消息,确认 “Dijkstra Swing EA” 已成功初始化,并返回 `INIT_SUCCEEDED` 以表示已正确启动。
//+------------------------------------------------------------------+ //| Detect swing highs and lows | //+------------------------------------------------------------------+ void DetectSwings(int left, int right) { ArrayResize(swingPoints, 0); int totalBars = Bars(_Symbol, PERIOD_CURRENT) - right; for (int i = left; i < totalBars; i++) { bool isHigh = true, isLow = true; double high = High(i), low = Low(i); for (int j = 1; j <= left; j++) { if (High(i - j) >= high) isHigh = false; if (Low(i - j) <= low) isLow = false; } for (int j = 1; j <= right; j++) { if (High(i + j) >= high) isHigh = false; if (Low(i + j) <= low) isLow = false; } if (isHigh || isLow) { int idx = ArraySize(swingPoints); ArrayResize(swingPoints, idx + 1); swingPoints[idx].index = i; swingPoints[idx].time = Time(i); swingPoints[idx].price = isHigh ? high : low; swingPoints[idx].isHigh = isHigh; swingPoints[idx].visited = false; swingPoints[idx].distance = DBL_MAX; swingPoints[idx].previous = -1; if (idx >= MaxSwings) break; } } }
`DetectSwings()` 函数通过将每根蜡烛与其相邻的柱进行比较,来识别价格图表上的波动高点和波动低点。它首先使用 `ArrayResize` 清除现有的 `SwingPoints` 数组,以确保每次调用时都能进行新的检测。它从 `left` 索引开始,循环遍历图表上的每一根柱,直到 `totalBars - right`,并检查当前柱(i)是否符合波动高点或波动低点的条件。
要确定一根柱是否为波动高点,它会检查其最高价是否高于前面 “left” 个柱和后面 “right” 个柱的最高价。同样地,如果一个波动低点的最低价低于前面和后面相邻柱的低点,则确认该波动低点。如果任一条件成立,则该柱被视为有效的波动点。这种局部比较确保只有价格的显著峰值和谷值才会被记录为波动节点。
当检测到价格波动时,它会被存储在 `SwingPoints[]` 数组中,并包含所有相关详细信息:其索引、时间、价格、是最高价还是最低价,以及路径查找的默认值(例如,`visited`、`distance` 和 `previous`)。该结构支持后续使用 Dijkstra 算法进行分析,以评估波动之间的路径概率。如果检测到的波动次数达到 `MaxSwings`,循环将提前终止,从而防止内存使用过多或出现性能问题。
//+------------------------------------------------------------------+ //| Apply Dijkstra's algorithm | //+------------------------------------------------------------------+ void ApplyDijkstra() { if (ArraySize(swingPoints) == 0) return; swingPoints[0].distance = 0; for (int i = 0; i < ArraySize(swingPoints); i++) { int u = -1; double minDist = DBL_MAX; for (int j = 0; j < ArraySize(swingPoints); j++) { if (!swingPoints[j].visited && swingPoints[j].distance < minDist) { minDist = swingPoints[j].distance; u = j; } } if (u == -1) break; swingPoints[u].visited = true; for (int v = 0; v < ArraySize(swingPoints); v++) { if (!swingPoints[v].visited) { 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; } } } } }
在这个函数中,我们实现了 Dijkstra 算法,计算从第一个波动点到所有其他波动点的最短路径,将波动高点和低点之间的价格变动视为加权图。我们首先检查是否存在任何波动点;如果数组为空,则函数立即退出。然后,它将第一个节点(起始枢轴)的 “distance” 设置为 “0”,表示路径查找过程中的源节点。
该算法进入一个循环,在每次迭代中,它选择已知 “distance” 最小的未访问波动点。该节点 `(u)` 被标记为已访问,算法将评估其所有未访问的邻居。对于每个邻居 `(v)`,它根据节点 `u` 和 `v` 之间的绝对价格差来计算从节点 `u` 移动到 `v` 的“成本”或权重。如果通过 `u` 到达 `v` 的累计成本低于其当前记录的距离,则更新 `v` 的 `distance`,并将 `u` 记录为其 `previous` 节点。
这一过程将持续进行,直至所有可到达的波动点均已被访问,或者没有更多未访问的节点可访问。函数结束时,每个波动点都保存着从源点到该最优路径的最短累计成本以及指向该最优路径上前一个节点的指针。这些信息使 EA 能够追踪近期市场结构中最有效的路径,并确定价格最有可能再次回到哪些波动点,从而为智能交易信号的生成奠定基础。
//+------------------------------------------------------------------+ //| Visualize Swing Points and Connections | //+------------------------------------------------------------------+ void VisualizeSwings() { for (int i = 0; i < ArraySize(swingPoints); i++) { string objName = "Swing_" + IntegerToString(i); ObjectDelete(0, objName); ObjectCreate(0, objName, OBJ_ARROW, 0, swingPoints[i].time, swingPoints[i].price); ObjectSetInteger(0, objName, OBJPROP_ARROWCODE, swingPoints[i].isHigh ? 233 : 234); ObjectSetInteger(0, objName, OBJPROP_COLOR, swingPoints[i].isHigh ? clrRed : clrBlue); } for (int i = 1; i < ArraySize(swingPoints); i++) { int prev = swingPoints[i].previous; if (prev != -1) { string lineName = "Line_" + IntegerToString(i); ObjectDelete(0, lineName); ObjectCreate(0, lineName, OBJ_TREND, 0, swingPoints[prev].time, swingPoints[prev].price, swingPoints[i].time, swingPoints[i].price); ObjectSetInteger(0, lineName, OBJPROP_COLOR, clrGray); ObjectSetInteger(0, lineName, OBJPROP_WIDTH, 1); } } }
`VisualizeSwings()` 函数负责将检测到的波动点及其之间的连接直接绘制在图表上,帮助交易者直观验证 EA 使用的结构和逻辑。在第一个 `for` 循环中,它遍历 `swingPoints[]` 数组,并为每个波动创建箭头对象。创建新对象之前,它会删除所有同名的现有对象,以避免混乱。每个箭头都有一个特定的符号:红色代表波动高点(箭头代码 233),蓝色代表波动低点(箭头代码 234),这使得它们在视觉上可以区分开来。
在第二个 “for” 循环中,该函数在每个波动点与其对应的 “previous” 节点(由 Dijkstra 算法确定)之间绘制线条。这些线条代表用于评估潜在交易路径的最短路径连接。同样,在绘制新线对象之前,任何同名的现有线条对象都会被删除。这些线条使用 `OBJ_TREND` 创建,并以标准宽度用灰色绘制,保持干净清晰的视觉结构。
该可视化功能通过显示识别出的波动点、它们之间的连接方式以及根据 Dijkstra 算法选择的路径,帮助验证 EA 的决策。在回测或实盘交易中,验证 EA 是否按预期分析市场结构时,此功能尤其有用。
double High(int index){return (iHigh(_Symbol, _Period, index));} double Low(int index){return (iLow(_Symbol, _Period, index));} datetime Time(int index){return (iTime(_Symbol, _Period, index));}
这三个辅助函数 `High()`、`Low()` 和 `Time()` 分别是对内置 MQL5 函数 `iHigh()`、`iLow()` 和 `iTime()` 的简单封装。它们可以方便地获取当前交易品种和时间周期内特定柱(基于给定的 “index”)的最高价、最低价和开启时间。通过使用这些简写函数,代码会变得更简洁、更易读,尤其是在波动检测或可视化过程中反复访问柱数据时。
//+------------------------------------------------------------------+ //| Filter and mark | //+------------------------------------------------------------------+ void FilterAndMarkValidSwings(SwingPoint &points[]) { int count = ArraySize(points); if(count < 2) return; for(int i = 0; i < count; i++) { if(points[i].used) continue; bool isValid = true; double swingPrice = points[i].price; int swingIndex = points[i].index; // Scan forward in time from the swing point for(int j = swingIndex - 1; j >= 0; j--) { double high = iHigh(_Symbol, TimeFrame, j); double low = iLow(_Symbol, TimeFrame, j); // Invalidate swing high if price went higher later if(points[i].isHigh && high > swingPrice) { isValid = false; break; } // Invalidate swing low if price went lower later if(!points[i].isHigh && low < swingPrice) { isValid = false; break; } } if(isValid) { points[i].used = true; // Draw object on chart string objName = points[i].isHigh ? StringFormat("SwingHigh_%d", TimeToString(iTime(_Symbol, TimeFrame, swingIndex))) : StringFormat("SwingLow_%d", TimeToString(iTime(_Symbol, TimeFrame, swingIndex))); color swingColor = points[i].isHigh ? clrRed : clrBlue; ObjectCreate(0, objName, OBJ_HLINE, 0, 0, swingPrice); ObjectSetInteger(0, objName, OBJPROP_COLOR, swingColor); ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); } } }
`FilterAndMarkValidSwings()` 函数通过识别哪些波动点仍然有效且尚未因未来的价格变动而失效,来细化波动点列表。它接受一个 `SwingPoints` 引用数组,并遍历这些引用,跳过任何已被标记为 `used` 的引用。对于每个候选波动点,它假设该点有效,然后根据历史价格行为进行验证检查,以确认价格在该波动点形成后是否已超过该波动点。
为了确定有效性,该函数会从波动的索引位置向后扫描过去的柱。对于波动高点,它会检查是否有任何未来的蜡烛图的最高点超过了它;对于波动低点,它会检查是否有任何蜡烛图的最低价低于它。如果发现这种情况,则该波动点被视为无效,因为价格实际上已经“穿过”了该波动点,因此不会对其进行标记或用于进一步的计算。如果没有发现此类条件,则波动点有效,并标记为 “used”。
对于每一个有效的波动,该函数都会在图表上绘制一条水平线来直观地标记它。该线采用虚线样式,红色表示波动高峰,蓝色表示波动低谷。该对象名称以波动类型和检测到它的柱时间生成。这种视觉反馈可以帮助交易者立即识别 EA 认为哪些波动点是强劲的且未被价格触及的,从而更容易在分析或交易过程中信任和调试逻辑。
//+------------------------------------------------------------------+ //| Cleaning up old swings | //+------------------------------------------------------------------+ void CleanOldSwingObjects(int keepBars = 100) { datetime oldestDate = iTime(_Symbol, TimeFrame, keepBars); int total = ObjectsTotal(0); for(int i = total - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, "SwingHigh_") == 0 || StringFind(name, "SwingLow_") == 0) { datetime swingTime = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME); if(swingTime < oldestDate) { ObjectDelete(0, name); } } } }
该函数负责从图表中删除过时的与波动相关的视觉元素,以保持清晰度和性能。它使用 `keepBars` 参数确定“旧”对象的阈值,该参数检索 `keepBars` 根蜡烛图之前的柱的时间戳。在此时间戳之前创建的任何波动对象都被视为已过时。然后,该函数按相反的顺序遍历图表上的所有图形对象,并检查它们的名称是否以 “SwingHigh_” 或 “SwingLow_” 开头,从而将它们识别为波动标记。
对于每个波动对象,它都会检索创建时间并将其与截止时间戳(oldestDate)进行比较。如果对象的时间较早,则使用 `ObjectDelete()` 将其从图表中删除。此程序确保图表保持简洁,仅显示最近和相关的波动点。它还有助于防止性能随时间推移而下降,尤其是在运行 EA 处理较长历史数据或在波动较大的实时市场中运行时。
//+------------------------------------------------------------------+ //| Generate Signal & Trade | //+------------------------------------------------------------------+ void GenerateSignalAndTrade() { if (ArraySize(swingPoints) < 2) return; int last = ArraySize(swingPoints) - 1; int prev = swingPoints[last].previous; if (prev == -1) return; double entry = swingPoints[last].price; double reference = swingPoints[prev].price; double sl, tp; bool isBuy = entry > reference, isSell = entry < reference; SetSLTP(entry, reference, isBuy, sl, tp); if (PositionSelect(_Symbol)) return; if (isBuy) ExecuteTrade(ORDER_TYPE_BUY); else if(isSell) ExecuteTrade(ORDER_TYPE_SELL); }
`GenerateSignalAndTrade()` 函数负责根据 Dijkstra 算法通过波动点找到的最新路径的方向生成交易信号。它首先确保至少有两个波动点可以比较,并且最新的波动点具有有效的 “previous” 节点。然后,它提取最新波动及其前一个相关波动的价格,利用它们之间的价格关系来确定交易方向:如果最新价格高于前一个价格,则发出买入信号;如果低于前一个价格,则发出卖出信号。
一旦确定了方向,该函数就会使用 `SetSLTP()` 函数计算止损和止盈水平,该函数基于波动点之间的距离来计算这些水平。在开仓之前,它会检查该交易品种是否已经存在持仓,以避免重复交易。最后,它使用 `ExecuteTrade()` 函数执行交易,并传递相应的订单类型。这种逻辑确保只有在确定了有效波动点之间清晰的、有结构支撑的方向路径时,才会执行交易。
//+------------------------------------------------------------------+ //| Calculate SL and TP based on distance to previous node | //+------------------------------------------------------------------+ void SetSLTP(double entry, double ref, bool isBuy, double &sl, double &tp) { double distance = MathAbs(entry - ref) + PointBuffer * _Point; if (isBuy) { sl = entry - distance; tp = entry + distance; } else { sl = entry + distance; tp = entry - distance; } }
`SetSLTP()` 函数根据当前入场价格与参考价格(通常是前一个波动点)之间的距离来计算交易的止损 (sl) 和止盈 (tp) 水平。它首先计算这两个点之间的绝对价格差,并为了安全起见,加上一个小的缓冲(以点为单位)。如果是买入交易,止损位设在入场价下方,止盈位设在入场价上方;如果是卖出交易,止损位设在入场价上方,止盈位设在入场价下方。这确保了风险和回报围绕波动结构对称地排列,帮助 EA 以有意义的、基于结构的止损和止盈水平跟踪价格行为。
//+------------------------------------------------------------------+ //| Execute trade with risk parameters | //+------------------------------------------------------------------+ void ExecuteTrade(ENUM_ORDER_TYPE tradeType){ double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); double price = (tradeType == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); // Convert StopLoss and TakeProfit from pips to actual price distances double sl_distance = StopLoss * point; double tp_distance = TakeProfit * point; double sl = (tradeType == ORDER_TYPE_BUY) ? price - sl_distance : price + sl_distance; double tp = (tradeType == ORDER_TYPE_BUY) ? price + tp_distance : price - tp_distance; trade.PositionOpen(_Symbol, tradeType, In_Lot, price, sl, tp, NULL); }
`ExecuteTrade()` 函数负责使用预定义的风险参数下达交易订单。它首先确定当前市场价格:买单使用 Ask 价,卖单使用 Bid 价。然后,它使用该交易品种的点数大小,将输入参数值(StopLoss 和 TakeProfit)从点数转换为实际价格距离,从而计算止损和止盈水平。根据交易是买入还是卖出,它会将止损和止盈适当地设置在入场价格的上方或下方。
最后,它使用 `CTrade` 类的 `PositionOpen()` 方法执行交易,计算出的参数包括手数、方向、入场价格、止损、止盈,没有自定义注释。这样可以确保交易无论市场走向如何,都遵循一致的风险框架。
//+------------------------------------------------------------------+ //| OnTick | //+------------------------------------------------------------------+ void OnTick() { static datetime lastBarTime = 0; datetime currentBarTime = iTime(_Symbol, _Period, 0); if (currentBarTime != lastBarTime) { lastBarTime = currentBarTime; DetectSwings(LeftBars, RightBars); ApplyDijkstra(); VisualizeSwings(); GenerateSignalAndTrade(); FilterAndMarkValidSwings(swingPoints); CleanOldSwingObjects(); } }
最后,`OnTick()` 函数是 EA 的主要执行循环,在每个新的 tick 时触发。为了避免冗余处理,它使用静态变量 `lastBarTime` 来检测是否形成了新的柱,方法是将其与当前柱的开盘时间进行比较。如果检测到新的柱,它会更新`lastBarTime`并运行核心逻辑:检测新的波动高点和低点(DetectSwings),应用 Dijkstra 算法找到波动之间的最有效路径(ApplyDijkstra),在图表上直观地显示波动及其连接(VisualiseSwings),根据路径方向生成和执行交易信号(GenerateSignalAndTrade),过滤掉无效的波动点(FilterAndMarkValidSwings),最后清理旧的波动对象以保持图表简洁(CleanOldSwingObjects)。
这种结构确保 EA 能够智能地处理市场结构。
回测结果
回测评估是在为期 2 个月的测试窗口期(2025 年 5 月 1 日至 2025 年 6 月 20 日)内,以 1 小时为时间周期进行的,输入设置如下:
- TP in points = 1000
- Stop loss = 385
- Input lots = 0.01
- Left bars = 3
- Right bars = 3
- Max swing = 50
- Point buffer = 10.0
- Slippage = 5


结论
总之,我们构建了一个功能齐全的 MQL5 EA,该 EA 使用 Dijkstra 算法将波段高点和波段低点作为图节点来解读金融市场结构。该系统检测每个新柱上的重要波动点,过滤掉价格已经突破的无效波动点,并将有效的波动点视为路径查找算法中的顶点。然后,它利用价格距离作为边的权重来计算穿过市场结构的最有效路径,从而确定价格变动的最可能方向。
基于此分析,EA 生成方向性交易信号,并根据波动点之间的距离,以适当计算的止损和止盈水平执行交易。箭头和趋势线等可视化工具用于反映检测到的波动点和计算出的路径,而清理程序则确保图表保持清晰和最新。
总之,这款 EA 超越了传统的基于指标的交易方式,它将基于图的算法集成到价格行为分析中,从而能够做出更结构化和更合乎逻辑的交易决策。通过将交易入场点与市场波动几何形状对齐,并确保每个节点除非有效否则只使用一次,该系统模拟了价格如何自然地流经支撑位和阻力位。模块化设计,具备检测、验证、路径查找、执行和可视化等功能,也使得进一步改进、扩展或回测该策略变得容易。本项目为构建一个智能、自适应的交易系统奠定了基础,该系统将价格行为视为一个可导航的网络,将数据结构理论与市场行为相结合。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18760
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
MQL5交易工具(第七部分):用于多品种持仓与账户监控的信息仪表盘
新手在交易中的10个基本错误
价格行为分析工具包开发(第 35 部分):预测模型训练与部署