Теория графов: Применение алгоритма эвристического поиска A* в торговле
Содержание
Введение
На реальных рынках цена формирует десятки локальных максимумов и минимумов, и быстро становится непонятно, какие из них представляют собой значимую структуру, а какие — шум. Трейдеры часто чувствуют "направление", но не могут преобразовать его в воспроизводимые правила: какие свинги важны, где проходит путь наименьшего сопротивления, какой целевой уровень реалистично достижим и какой уровень использовать для структурного стопа, чтобы он не был произвольным. Алгоритмический подход к решению этой задачи более строг: без графовой модели и числовой оценки пути невозможно последовательно фильтровать торговые сетапы и принимать воспроизводимые решения на каждом баре. В этой статье эта задача решается напрямую — мы преобразуем подтвержденные свинги в узлы графа, строим взвешенную карту (нормализованное по ATR расстояние, спред и штрафы за шум) и используем алгоритм A* для поиска оптимального пути для цены. В результате получается формальная, проверяемая структура для определения высоковероятных точек входа, структурных стопов и логичных целевых уровней для фиксации прибыли.
Обзор системы
Эвристический советник A* работает как дискретный механизм принятия решений, а не как непрерывный индикатор. На каждом новом баре он сканирует историческое движение цен, чтобы определить ключевые максимумы и минимумы свингов, которые выступают в качестве узлов в графе. Эти узлы проверяются, чтобы убедиться, что цена еще не прошла через них. Затем советник соединяет узлы ребрами, каждое ребро имеет стоимость, основанную на нормализованном ценовом расстоянии, спреде и рыночном шуме. Это преобразует ценовой график в структурированную, взвешенную карту, по которой может перемещаться алгоритм A*.

Советник строит взвешенную карту непробитых свинговых точек. Каждое соединение имеет стоимость, основанную на расстоянии и рыночном трении.
После построения графа движок A* находит оптимальный путь от узла, ближайшего к текущей цене, к целевому уровню по направлению предполагаемого тренда. Он оценивает пути, используя как фактическую пройденную стоимость (G), так и предполагаемую оставшуюся стоимость (H), всегда расширяя наиболее эффективный вариант. Окончательный путь проверяется на качество. Он должен сохранять направленную согласованность движения, оставаться в пределах допустимых значений стоимости и не содержать препятствующих свингов противоположного направления. Если все условия выполнены, советник совершает сделку, используя конечный узел в качестве тейк-профита, а предыдущий противоположный свинг — в качестве стоп-лосса.

Сделками становятся только высококачественные пути, отвечающие строгим критериям, при этом стоп-лоссы привязаны к предыдущей структуре.
Настраиваемые модули и их функции в рамках архитектуры системы:
| Модуль | Группа входных данных | Первичная функция |
|---|---|---|
| Обнаружение узлов | Обнаружение свингов | Идентифицирует локальные ценовые экстремумы, используя симметричное окно ретроспективного анализа, и фильтрует избыточные или пробитые уровни. |
| Построение графа | Модуль A* | Строит сеть связей между узлами, рассчитывая стоимость перехода на основе ATR, спреда и штрафов за шум. |
| Модуль поиска пути | Модуль A* | Выполняет эвристический поиск для нахождения наиболее эффективного маршрута через ценовую структуру к целевому значению. |
| Фильтр сигналов | Торговое решение | Проверяет путь по коэффициенту направленности, общей нормализованной стоимости и наличию структурных блокировок. |
| Менеджер рисков | Управление рисками | Рассчитывает размер позиции на основе фиксированного процента от баланса счета и расстояния до структурного стоп-лосса. |
| Визуализатор | Визуализация | Отображает узлы свингов, ребра графа и выбранный путь A* непосредственно на графике для большей прозрачности. |
Приступаем к работе
//+------------------------------------------------------------------+ //| Heuristic A_Star.mq5 | //| Git, Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com/ru/users/johnhlomohang/ | //+------------------------------------------------------------------+ #property copyright "Git, Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/ru/users/johnhlomohang/" #property version "1.00" #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| INPUT PARAMETERS | //+------------------------------------------------------------------+ //--- Swing Detection --- input group "=== Swing Detection ===" input int InpSwingLookback = 5; // Bars left/right for swing detection input int InpMaxNodes = 100; // Maximum swing nodes to keep input int InpMinSwingBars = 3; // Minimum bars between swings //--- A* Engine --- input group "=== A* Engine ===" input double InpSpreadPenalty = 1.5; // Spread cost multiplier input double InpNoisePenalty = 0.5; // Noise penalty weight input int InpATRPeriod = 14; // ATR period for normalization //--- Trade Decision --- input group "=== Trade Decision ===" input double InpMinPathScore = 0.55; // Minimum directional ratio to trade (0-1) input double InpMaxPathCost = 5.0; // Maximum total path cost (ATR multiples) input double InpSLBufferATR = 0.5; // SL buffer in ATR units //--- Risk Management --- input group "=== Risk Management ===" input double InpRiskPercent = 1.5; // Risk per trade (% of balance) input double InpMaxSpreadPoints = 40; // Max allowed spread in points input int InpMagicNumber = 20250413; // EA magic number input string InpComment = "AStarEA"; //--- Visualization --- input group "=== Visualization ===" input bool InpDrawNodes = true; // Draw swing nodes input bool InpDrawEdges = true; // Draw graph edges input bool InpDrawPath = true; // Draw A* best path input color InpHighNodeColor = clrDodgerBlue; input color InpLowNodeColor = clrOrangeRed; input color InpEdgeColor = clrDimGray; input color InpPathColor = clrLime; input int InpNodeSize = 3;
Для начала определим входные параметры, которые управляют работой советника A*. Для большей ясности и удобства использования мы сгруппируем их. Настройки обнаружения свингов определяют, как мы идентифицируем и фильтруем максимумы и минимумы свингов. Настройки модуля A* управляют расчетом стоимости пути с использованием спреда, шума и ATR. Раздел принятия решения о сделке определяет, когда путь достаточно силен для открытия сделки. Входные параметры управления рисками отвечают за определение размера позиции, лимитов спреда и идентификацию сделок. Наконец, настройки визуализации позволяют нам рисовать узлы, ребра и пути на графике с настраиваемыми цветами и размерами.
//+------------------------------------------------------------------+ //| DATA STRUCTURES | //+------------------------------------------------------------------+ struct SwingNode { double price; datetime time; bool isHigh; bool isValid; int barIndex; // bar index at detection time }; struct Edge { int from; int to; double cost; }; struct AStarNode { int index; double g; double h; double f; int parent; bool inOpen; bool inClosed; }; //+------------------------------------------------------------------+ //| GLOBAL VARS | //+------------------------------------------------------------------+ CTrade trade; SwingNode Nodes[]; Edge Edges[]; AStarNode AStar[]; int BestPath[]; // indices into Nodes[] int NodeCount = 0; int EdgeCount = 0; int PathLength = 0; datetime LastBarTime = 0; double CachedATR = 0; //--- Visualization object name prefix string OBJ_PREFIX = "ASTAR_"; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(InpMagicNumber); trade.SetDeviationInPoints(20); trade.SetTypeFilling(ORDER_FILLING_FOK); ArrayResize(Nodes, InpMaxNodes + 10); ArrayResize(Edges, (InpMaxNodes * 3) + 10); ArrayResize(AStar, InpMaxNodes + 10); ArrayResize(BestPath, InpMaxNodes + 10); Print("Heuristic A_Star EA initialized. Magic=", InpMagicNumber); return INIT_SUCCEEDED; }
Здесь мы определяем основные структуры данных, которые представляют наш граф и логику A*. Структура SwingNode хранит ключевые рыночные точки, включая цену, время, тип (максимум или минимум) и валидность. Структура ребер соединяет эти узлы и назначает стоимость движения между ними. Структура AStarNode хранит значения, необходимые для поиска пути, такие как фактическая стоимость (g), оценочная стоимость (h), общая стоимость (f) и ссылка на родительский узел. Эти структуры позволяют нам моделировать движение цены в виде взвешенного графа, который может обрабатывать алгоритм A*.
Далее мы объявляем глобальные переменные для хранения узлов, ребер и состояний A*, а также результирующего наилучшего пути. Мы отслеживаем счетчики узлов, ребер и длину пути для эффективного управления памятью. Дополнительные переменные хранят значения времени и ATR для вычислений. Функция OnInit() подготавливает советника, устанавливая параметры сделки, такие как магическое число и тип исполнения. Она также изменяет размер всех массивов в соответствии с входными ограничениями и подтверждает инициализацию сообщением в логе.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ClearVisualization(); Print("Heuristic A_Star EA removed."); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Only process on new bar if(!IsNewBar()) return; //--- Cache ATR for this bar cycle CachedATR = GetATR(); if(CachedATR <= 0) return; //--- Check spread double spreadPoints = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); if(spreadPoints > InpMaxSpreadPoints) { Print("Spread too wide: ", spreadPoints, " points. Skipping."); return; } //--- Core pipeline UpdateSwings(); // Detect swing nodes BuildGraph(); // Construct edge graph RunAStar(); // Heuristic + A* search EvaluatePath(); // Decision logic + execution DrawVisualization(); // Render to chart }
Функция OnDeinit() очищает все объекты графика, чтобы поддерживать чистоту рабочего пространства, и логирует удаление советника. В функции OnTick() мы гарантируем, что система запускается только один раз для каждого нового бара, чтобы повысить эффективность. Затем мы кэшируем значение ATR для единообразных вычислений и, прежде чем продолжить, проверяем, находится ли текущий спред в допустимых пределах.
Если все условия выполнены, мы последовательно выполняем весь конвейер обработки. Мы обнаруживаем и обновляем узлы свингов, строим структуру графа и запускаем алгоритм A* для поиска оптимального пути. Затем оцениваем путь, чтобы решить, следует ли совершать сделку. Наконец, мы отображаем узлы, ребра и выбранный путь на графике для визуализации и анализа.
//+------------------------------------------------------------------+ //| SWING DETECTION (NODE GENERATION) | //+------------------------------------------------------------------+ void UpdateSwings() { NodeCount = 0; int totalBars = MathMin(Bars(_Symbol, _Period) - InpSwingLookback - 1, InpMaxNodes * 4); double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); for(int i = InpSwingLookback; i < totalBars && NodeCount < InpMaxNodes; i++) { bool isSwingHigh = IsSwingHigh(i, InpSwingLookback); bool isSwingLow = IsSwingLow(i, InpSwingLookback); if(!isSwingHigh && !isSwingLow) continue; double nodePrice = isSwingHigh ? iHigh(_Symbol, _Period, i) : iLow(_Symbol, _Period, i); datetime nodeTime = iTime(_Symbol, _Period, i); //--- CRITICAL: Remove nodes where price has already passed through //--- (blown-through levels are invalid) if(!IsNodeValid(nodePrice, isSwingHigh, i, currentPrice)) continue; //--- Minimum separation filter — avoid duplicate adjacent swings if(NodeCount > 0) { int prevIdx = NodeCount - 1; if(Nodes[prevIdx].isHigh == isSwingHigh && MathAbs(Nodes[prevIdx].price - nodePrice) < CachedATR * 0.3) continue; } Nodes[NodeCount].price = nodePrice; Nodes[NodeCount].time = nodeTime; Nodes[NodeCount].isHigh = isSwingHigh; Nodes[NodeCount].isValid = true; Nodes[NodeCount].barIndex = i; NodeCount++; } //--- Trim to max if(NodeCount > InpMaxNodes) NodeCount = InpMaxNodes; } //+------------------------------------------------------------------+ //| Swing High | //+------------------------------------------------------------------+ bool IsSwingHigh(int bar, int n) { double h = iHigh(_Symbol, _Period, bar); for(int j = 1; j <= n; j++) { if(iHigh(_Symbol, _Period, bar - j) >= h) return false; if(iHigh(_Symbol, _Period, bar + j) >= h) return false; } //--- True if bar[i].high is greater than N bars on each side return true; } //+------------------------------------------------------------------+ //| Swing Low | //+------------------------------------------------------------------+ bool IsSwingLow(int bar, int n) { double l = iLow(_Symbol, _Period, bar); for(int j = 1; j <= n; j++) { if(iLow(_Symbol, _Period, bar - j) <= l) return false; if(iLow(_Symbol, _Period, bar + j) <= l) return false; } //--- True if bar[i].low is lower than N bars on each side return true; }
В этом разделе кода мы обрабатываем обнаружение свингов путем сканирования исторических данных о ценах и преобразования ключевых точек разворота в узлы графа. В начале функции UpdateSwings() мы сбрасываем счетчик узлов и ограничиваем количество анализируемых баров для поддержания эффективности. Для каждого бара в диапазоне мы проверяем, сформировал ли он максимум или минимум свинга по заданным условиям lookback-периода. Если обнаружен корректный свинг, мы извлекаем его цену и время. Затем мы применяем критически важный этап проверки, чтобы убедиться, что цена еще не прошла этот уровень, поскольку такие узлы больше не имеют значения для принятия будущих решений. Это позволяет поддерживать чистоту графа и концентрироваться на релевантной структуре.
Мы также применяем фильтр по интервалам, чтобы избежать слишком близкого расположения похожих свинговых точек, используя ATR в качестве динамического порогового значения. Если новый свинг слишком близок к предыдущему и того же типа, мы его игнорируем. После того, как узел проходит все проверки, мы сохраняем его данные, включая цену, время, тип и индекс бара. Вспомогательные функции IsSwingHigh() и IsSwingLow() гарантируют, что бар классифицируется как свинг только в том случае, если он выше или ниже заданного количества баров с обеих сторон. Это строгое условие помогает нам улавливать значимую структуру рынка, а не шум, что приводит к четко определенному набору узлов для модуля алгоритма A*.
//+------------------------------------------------------------------+ //| Validate node | //+------------------------------------------------------------------+ bool IsNodeValid(double price, bool isHigh, int barIndex, double currentPrice) { //--- Check bars between barIndex and bar 0 (current) for(int b = barIndex - 1; b >= 0; b--) { if(isHigh) { //--- If any close above the swing high -> node breached if(iClose(_Symbol, _Period, b) > price) return false; } else { //--- If any close below the swing low -> node breached if(iClose(_Symbol, _Period, b) < price) return false; } } return true; } //+------------------------------------------------------------------+ //| GRAPH CONSTRUCTION | //+------------------------------------------------------------------+ void BuildGraph() { EdgeCount = 0; if(NodeCount < 2) return; double spread = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID); double atr = CachedATR; for(int i = 0; i < NodeCount; i++) { //--- Connect to immediate neighbours (prev + next) for(int delta = -1; delta <= 1; delta += 2) { int j = i + delta; if(j < 0 || j >= NodeCount) continue; double edgeCost = CalculateEdgeCost(i, j, atr, spread); //--- Store edge if(EdgeCount < ArraySize(Edges) - 1) { Edges[EdgeCount].from = i; Edges[EdgeCount].to = j; Edges[EdgeCount].cost = edgeCost; EdgeCount++; } } //--- Optional cross-connections: skip one node for(int delta = -2; delta <= 2; delta += 4) { int j = i + delta; if(j < 0 || j >= NodeCount) continue; double edgeCost = CalculateEdgeCost(i, j, atr, spread) * 1.2; // slight penalty if(EdgeCount < ArraySize(Edges) - 1) { Edges[EdgeCount].from = i; Edges[EdgeCount].to = j; Edges[EdgeCount].cost = edgeCost; EdgeCount++; } } } }
В функции IsNodeValid() система проверяет, остается ли каждый узел свинга актуальным, прежде чем включить его в граф. Функция проверяет все свечи между формированием узла и текущим баром. Для свингового максимума, если цена закрытия поднимается выше этого уровня, узел считается пробитым и отклоняется. Для свингового минимума действует обратное правило: любое закрытие ниже уровня делает его недействительным. Этот процесс удаляет уровни, которые цена уже пробивала, гарантируя, что для дальнейшего анализа сохраняется только нетронутая и значимая структура.
В функции BuildGraph() акцент смещается на построение связей между валидными узлами. Процесс начинается с обнуления счетчика ребер и подтверждения наличия достаточного количества узлов для формирования графа. Каждый узел затем соединяется со своими ближайшими соседями, создавая простую и структурированную сеть путей. Дополнительные перекрестные соединения вводятся путем пропуска одного узла, что позволяет существовать альтернативным путям. Эти соединения имеют несколько более высокую стоимость, чтобы оставаться менее выгодными. Стоимость всех ребер рассчитывается с использованием ATR и спреда, что гарантирует, что граф отражает реальные рыночные условия и может эффективно использоваться алгоритмом A*.
//+------------------------------------------------------------------+ //| Calculate Edge Cost | //+------------------------------------------------------------------+ double CalculateEdgeCost(int from, int to, double atr, double spread) { double priceDiff = MathAbs(Nodes[from].price - Nodes[to].price); //--- Normalize by ATR double normalizedDist = (atr > 0) ? priceDiff / atr : priceDiff; //--- Spread cost (in ATR units) double spreadCost = (atr > 0) ? spread / atr : 0; //--- Noise penalty: penalize same-type adjacent nodes (low→low = noise) double noisePenalty = 0; if(Nodes[from].isHigh == Nodes[to].isHigh) noisePenalty = InpNoisePenalty; return normalizedDist + (InpSpreadPenalty * spreadCost) + noisePenalty; } //+------------------------------------------------------------------+ //| HEURISTIC FUNCTION | //+------------------------------------------------------------------+ double Heuristic(int current, int target) { double distance = MathAbs(Nodes[current].price - Nodes[target].price); return (CachedATR > 0) ? distance / CachedATR : distance; } //+------------------------------------------------------------------+ //| A* ENGINE | //+------------------------------------------------------------------+ void RunAStar() { PathLength = 0; if(NodeCount < 2) return; //--- Determine start: node closest to current price int startNode = FindStartNode(); //--- Determine goal: support/resistance target int goalNode = FindGoalNode(startNode); if(startNode < 0 || goalNode < 0 || startNode == goalNode) return; //--- Initialize A* state ArrayResize(AStar, NodeCount); for(int i = 0; i < NodeCount; i++) { AStar[i].index = i; AStar[i].g = 1e18; AStar[i].h = 0; AStar[i].f = 1e18; AStar[i].parent = -1; AStar[i].inOpen = false; AStar[i].inClosed = false; } AStar[startNode].g = 0; AStar[startNode].h = Heuristic(startNode, goalNode); AStar[startNode].f = AStar[startNode].h; AStar[startNode].inOpen = true; bool goalReached = false; while(true) { //--- Pick node in OPEN with lowest f int current = -1; double bestF = 1e18; for(int i = 0; i < NodeCount; i++) { if(AStar[i].inOpen && AStar[i].f < bestF) { bestF = AStar[i].f; current = i; } } if(current < 0) break; // OPEN is empty AStar[current].inOpen = false; AStar[current].inClosed = true; if(current == goalNode) { goalReached = true; break; } //--- Expand neighbours via edges for(int e = 0; e < EdgeCount; e++) { if(Edges[e].from != current) continue; int neighbor = Edges[e].to; if(neighbor < 0 || neighbor >= NodeCount) continue; if(AStar[neighbor].inClosed) continue; double tentativeG = AStar[current].g + Edges[e].cost; if(tentativeG < AStar[neighbor].g) { AStar[neighbor].g = tentativeG; AStar[neighbor].h = Heuristic(neighbor, goalNode); AStar[neighbor].f = AStar[neighbor].g + AStar[neighbor].h; AStar[neighbor].parent = current; AStar[neighbor].inOpen = true; } } } //--- Reconstruct path if goal was reached if(goalReached) { int temp[]; ArrayResize(temp, NodeCount); int len = 0; int cur = goalNode; while(cur != -1 && len < NodeCount) { temp[len++] = cur; cur = AStar[cur].parent; } //--- Reverse into BestPath ArrayResize(BestPath, len); for(int i = 0; i < len; i++) BestPath[i] = temp[len - 1 - i]; PathLength = len; } }
Функция CalculateEdgeCost() определяет, насколько дорого обходится перемещение цены между двумя узлами. Она начинает с измерения абсолютной разницы цен, а затем нормализует ее с использованием ATR для учета волатильности рынка. Добавляется стоимость спреда, отражающая реальные торговые условия, также масштабированная с помощью ATR. Далее функция применяет штраф за шум, когда два соединенных узла имеют один и тот же тип, например, "максимум к максимуму" или "минимум к минимуму", поскольку это часто отражает рваное или менее значимое движение. Итоговая стоимость объединяет все эти компоненты, создавая сбалансированный и реалистичный вес для каждого ребра в графе.
Затем функция Heuristic() вычисляет приблизительную стоимость от текущего узла до целевого узла. Она рассчитывает ценовое расстояние между двумя узлами и нормализует его, используя кэшированное значение ATR. Это обеспечивает согласованность эвристики с логикой расчета стоимости ребер и гарантирует, что расстояние оценивается относительно волатильности рынка. Благодаря этому алгоритм A* может принимать более обоснованные решения о том, какие пути, вероятно, будут эффективными, вместо того, чтобы полагаться только на разницу цен.

В функции RunAStar() система запускает процесс поиска пути A*. Она выбирает начальный узел, ближайший к текущей цене, и целевой узел на основе структуры рынка. Затем она итеративно расширяет узел с наименьшей стоимостью и обновляет родительские узлы и их стоимость до достижения цели, после чего восстанавливает окончательный путь. Этот путь представляет собой наиболее эффективный маршрут для движения цены в соответствии с определенной стоимостью и эвристической логикой.

//+------------------------------------------------------------------+ //| Find Starting Node | //+------------------------------------------------------------------+ int FindStartNode() { //--- Find node closest to current price (start) double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); int best = -1; double bestDist = 1e18; for(int i = 0; i < NodeCount; i++) { double d = MathAbs(Nodes[i].price - currentPrice); if(d < bestDist) { bestDist = d; best = i; } } return best; } //+------------------------------------------------------------------+ //| Find Goal Node | //+------------------------------------------------------------------+ int FindGoalNode(int startNode) { //--- Find goal node: nearest unbreached resistance (for buy context) or support (sell) if(startNode < 0) return -1; double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); double atr = CachedATR; //--- Count upward vs downward nodes from start to get initial bias int upCount = 0, downCount = 0; for(int i = 0; i < NodeCount; i++) { if(Nodes[i].price > currentPrice) upCount++; else downCount++; } bool biasUp = (upCount >= downCount); //--- Goal = first swing of the opposing type, furthest in direction of bias int bestGoal = -1; double bestDist = 0; for(int i = 0; i < NodeCount; i++) { if(i == startNode) continue; bool isAbove = Nodes[i].price > currentPrice; if(isAbove != biasUp) continue; // wrong side double d = MathAbs(Nodes[i].price - currentPrice); if(d > bestDist && d < atr * 8.0) // within 8 ATR range { bestDist = d; bestGoal = i; } } return bestGoal; }
В функции FindStartNode() мы определяем, где должен начаться поиск A*, находя узел, ближайший к текущей рыночной цене. Мы получаем текущую цену Bid и сравниваем ее со всеми сохраненными ценами узлов. Для каждого узла мы вычисляем абсолютное расстояние от текущей цены и отслеживаем наименьшее значение. Узел с наименьшим расстоянием выбирается в качестве отправной точки, поскольку он наилучшим образом отражает текущее положение рынка в структуре графа.
Далее в функции FindGoalNode() мы определяем, куда должен быть направлен процесс поиска пути. Сначала мы устанавливаем направленный уклон, подсчитывая, сколько узлов находится выше и ниже текущей цены. Если узлов выше больше, мы предполагаем направленность вверх; в противном случае — направленность вниз. Далее мы ищем подходящий целевой узел в этом направлении, игнорируя узлы на противоположной стороне. Среди подходящих кандидатов мы выбираем тот, который находится дальше всего от текущей цены, но все еще в разумном диапазоне, ограниченном значением, кратным ATR. Это гарантирует, что цель остается согласованной с выбранным направлением и при этом реалистично достижима в текущих рыночных условиях.
//+------------------------------------------------------------------+ //| PATH EVALUATION & TRADE DECISION | //+------------------------------------------------------------------+ void EvaluatePath() { if(PathLength < 2) return; //--- Compute directional score int upMoves = 0; int downMoves = 0; double totalCost = 0; for(int i = 1; i < PathLength; i++) { double priceDelta = Nodes[BestPath[i]].price - Nodes[BestPath[i-1]].price; if(priceDelta > 0) upMoves++; else downMoves++; } //--- Accumulate total path cost from A* g-value of goal int goalIdx = BestPath[PathLength - 1]; totalCost = AStar[goalIdx].g; double totalMoves = upMoves + downMoves; double upRatio = (totalMoves > 0) ? upMoves / totalMoves : 0.5; double downRatio = 1.0 - upRatio; //--- Check opposing node on path bool hasOpposingNode = CheckOpposingNode(); //--- Decision int signal = 0; // 0=none, 1=buy, -1=sell double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); double goalPrice = Nodes[goalIdx].price; bool pathGoesUp = goalPrice > currentPrice; bool pathGoesDown = goalPrice < currentPrice; //--- BUY conditions if(pathGoesUp && upRatio >= InpMinPathScore && totalCost <= InpMaxPathCost && !hasOpposingNode) { signal = 1; } //--- SELL conditions else if(pathGoesDown && downRatio >= InpMinPathScore && totalCost <= InpMaxPathCost && !hasOpposingNode) { signal = -1; } if(signal == 0) return; // --- SL / TP Calculation --- double atr = CachedATR; double slBuffer = atr * InpSLBufferATR; double tp = goalPrice; // TP = final node in path double sl = 0; //--- SL = node before start on path, or fallback to previous swing if(PathLength >= 3) { //--- The node just before start in path context: use node index BestPath[1] //--- reversed — actually use the swing opposite to start int prevNode = FindOppositeSwingBeforeStart(BestPath[0]); if(prevNode >= 0) sl = Nodes[prevNode].price; } //--- Fallback SL if(sl == 0) { sl = (signal == 1) ? currentPrice - (atr * 1.5 + slBuffer) : currentPrice + (atr * 1.5 + slBuffer); } else { //--- Add buffer sl = (signal == 1) ? sl - slBuffer : sl + slBuffer; } ExecuteTrade(signal, sl, tp); } //+------------------------------------------------------------------+ //| Check Opposing Node | //+------------------------------------------------------------------+ bool CheckOpposingNode() { //--- Check if path contains a high-confidence opposing node (counter-trend node) if(PathLength < 2) return false; double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); double goalPrice = Nodes[BestPath[PathLength-1]].price; bool goingUp = goalPrice > currentPrice; for(int i = 1; i < PathLength - 1; i++) { int ni = BestPath[i]; // Opposing = swing high blocking an uptrend, or swing low blocking downtrend if(goingUp && Nodes[ni].isHigh && Nodes[ni].price < goalPrice && Nodes[ni].price > currentPrice) return true; if(!goingUp && !Nodes[ni].isHigh && Nodes[ni].price > goalPrice && Nodes[ni].price < currentPrice) return true; } return false; }
В функции EvaluatePath() мы преобразуем результат A* в конкретное торговое решение, анализируя структуру вычисленного пути. Сначала мы проверяем корректность пути, а затем измеряем его направленность, подсчитывая, сколько перемещений между последовательными узлами происходит вверх, а сколько — вниз. Также мы получаем общую стоимость пути из вычисления алгоритма A*, которая отражает эффективность или “дороговизну“ пути. На основе этого мы вычисляем коэффициенты направленности и проверяем, соответствует ли путь явному перевесу в сторону покупки или продажи. Мы также дополнительно проверяем путь на наличие каких-либо противодействующих структур, которые могут блокировать движение цены, гарантируя, что мы действуем только на чистых и направленных сетапах.
Как только путь проходит все условия, мы генерируем торговый сигнал и определяем уровни исполнения. Тейк-профит устанавливается в конечном узле пути, представляя собой прогнозируемое целевое значение рынка. Стоп-лосс определяется либо структурным свингом, противоположным точке входа, либо, если это невозможно, резервным вариантом на основе волатильности с использованием ATR и буфера. Это гарантирует защиту сделки даже при неясной структуре. Функция CheckOpposingNode() усиливает безопасность, сканируя путь на наличие конфликтующих свинговых точек, которые могут прервать движение цены. Если такой узел имеется на пути следования, сделка отклоняется, чтобы избежать некачественных или заблокированных торговых ситуаций.
//+------------------------------------------------------------------+ //| Find Opposite Swing | //+------------------------------------------------------------------+ int FindOppositeSwingBeforeStart(int startNodeIdx) { //--- Find the opposite swing type just before the start node bool startIsHigh = Nodes[startNodeIdx].isHigh; for(int i = startNodeIdx + 1; i < NodeCount; i++) { if(Nodes[i].isHigh != startIsHigh) return i; } return -1; } //+------------------------------------------------------------------+ //| EXECUTION ENGINE | //+------------------------------------------------------------------+ void ExecuteTrade(int signal, double sl, double tp) { //--- Only one open trade at a time (per magic) if(HasOpenPosition()) return; double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Validate SL/TP logic if(signal == 1) { if(sl >= ask || tp <= ask) { Print("BUY: Invalid SL/TP. SL=", sl, " TP=", tp, " Ask=", ask); return; } } else { if(sl <= bid || tp >= bid) { Print("SELL: Invalid SL/TP. SL=", sl, " TP=", tp, " Bid=", bid); return; } } //--- Risk-based lot sizing double entryPrice = (signal == 1) ? ask : bid; double slDistance = MathAbs(entryPrice - sl); double lots = CalculateLotSize(slDistance); if(lots <= 0) return; //--- Normalize prices int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); sl = NormalizeDouble(sl, digits); tp = NormalizeDouble(tp, digits); bool result = false; if(signal == 1) { result = trade.Buy(lots, _Symbol, ask, sl, tp, InpComment); Print("BUY executed. Lots=", lots, " SL=", sl, " TP=", tp, " PathLen=", PathLength); } else { result = trade.Sell(lots, _Symbol, bid, sl, tp, InpComment); Print("SELL executed. Lots=", lots, " SL=", sl, " TP=", tp, " PathLen=", PathLength); } if(!result) Print("Trade failed: ", trade.ResultRetcodeDescription()); } //+------------------------------------------------------------------+ //| Calculate LotSize | //+------------------------------------------------------------------+ double CalculateLotSize(double slDistance) { //--- Position sizing: 1–2% risk per trade if(slDistance <= 0) return 0; double balance = AccountInfoDouble(ACCOUNT_BALANCE); double riskAmount = balance * InpRiskPercent / 100.0; double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); if(tickSize <= 0 || tickValue <= 0) return minLot; double slInTicks = slDistance / tickSize; double lotSize = riskAmount / (slInTicks * tickValue); lotSize = MathFloor(lotSize / lotStep) * lotStep; lotSize = MathMax(minLot, MathMin(maxLot, lotSize)); return NormalizeDouble(lotSize, 2); }
В этом разделе кода мы сначала определяем структурно значимую точку опоры стоп-лосса, используя функцию FindOppositeSwingBeforeStart(). Эта функция начинает с выбранного начального узла и просматривает список узлов вперед, чтобы найти первый свинг противоположного типа. Если начальный узел представляет собой свинг-хай, мы ищем свинг-лоу, и наоборот. Это помогает нам привязать риск к значимой рыночной структуре, а не к произвольным ценовым уровням. Если противоположный свинг не найден, функция возвращает -1, указывая на необходимость использования резервного метода управления риском в дальнейшем.
Далее мы переходим к функции ExecuteTrade(), которая обрабатывает полный процесс исполнения сделки. Перед размещением любого ордера мы убеждаемся, что для этого советника нет открытой позиции. Мы проверяем, что уровни стоп-лосса и тейк-профита логически корректны относительно текущей рыночной цены. Далее мы вычисляем расстояние риска входа и передаем его в функцию определения размера лота для определения размера позиции на основе баланса счета и процента риска. После проверки и нормализации сделка исполняется с использованием ордера на покупку или продажу, а результат логируется для подтверждения или отладки.
Мы завершаем логику исполнения с помощью функции CalculateLotSize(), которая обеспечивает корректный расчет размера позиции с учетом риска. Эта функция преобразует расстояние стоп-лосса в тиках и рассчитывает, какой объем капитала рискуется в каждой сделке. Затем она определяет соответствующий размер лота на основе баланса счета, значения тика и размера тика. Результат корректируется в соответствии с ограничениями брокера, такими как минимальный размер лота, максимальный размер лота и шаг приращения лота. Это гарантирует, что каждая сделка соответствует правилам управления рисками, поддерживая при этом стабильную экспозицию в различных рыночных условиях.
//+------------------------------------------------------------------+ //| New Bar Function | //+------------------------------------------------------------------+ bool IsNewBar() { datetime currentBar = iTime(_Symbol, _Period, 0); if(currentBar == LastBarTime) return false; LastBarTime = currentBar; return true; } //+------------------------------------------------------------------+ //| Clear Objects on the chart | //+------------------------------------------------------------------+ void ClearVisualization() { int total = ObjectsTotal(0); for(int i = total - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, OBJ_PREFIX) == 0) ObjectDelete(0, name); } } //+------------------------------------------------------------------+ //| Get ATR Handle | //+------------------------------------------------------------------+ double GetATR() { int handle = iATR(_Symbol, _Period, InpATRPeriod); if(handle == INVALID_HANDLE) return 0; double buf[1]; if(CopyBuffer(handle, 0, 0, 1, buf) <= 0) return 0; IndicatorRelease(handle); return buf[0]; }
Здесь мы рассматриваем три вспомогательные функции, поддерживающие основной алгоритм A* советника. Функция IsNewBar() гарантирует, что вся обработка происходит только один раз для каждой новой свечи, сравнивая текущее время бара с ранее сохраненным значением. Это предотвращает избыточные вычисления и повышает эффективность системы. Функция ClearVisualization() удаляет все объекты графика, созданные советником, путем сканирования существующих объектов и удаления тех, которые соответствуют определенному префиксу, обеспечивая чистое состояние графика при удалении или перезапуске советника.
Мы также включили функцию GetATR(), которая извлекает значение среднего истинного диапазона (Average True Range) для текущего символа и таймфрейма. Это делается путем создания хэндла индикатора ATR, копирования последнего значения буфера, а затем освобождения хэндла во избежание утечек памяти. Значение ATR используется во всей системе для нормализации затрат, установки буферов и контроля чувствительности к риску. Вместе эти вспомогательные функции обеспечивают эффективную работу советника и поддерживают чистое визуальное окружение.
//+------------------------------------------------------------------+ //| VISUALIZATION ENGINE | //+------------------------------------------------------------------+ void DrawVisualization() { ClearVisualization(); if(NodeCount == 0) return; //--- Draw edges first (behind nodes) if(InpDrawEdges) { for(int e = 0; e < EdgeCount; e++) { int f = Edges[e].from; int t = Edges[e].to; if(f < 0 || f >= NodeCount || t < 0 || t >= NodeCount) continue; string name = OBJ_PREFIX + "EDGE_" + IntegerToString(e); ObjectCreate(0, name, OBJ_TREND, 0, Nodes[f].time, Nodes[f].price, Nodes[t].time, Nodes[t].price); ObjectSetInteger(0, name, OBJPROP_COLOR, InpEdgeColor); ObjectSetInteger(0, name, OBJPROP_WIDTH, 1); ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } } //--- Draw best path (thick colored lines) if(InpDrawPath && PathLength >= 2) { for(int p = 0; p < PathLength - 1; p++) { int a = BestPath[p]; int b = BestPath[p + 1]; if(a < 0 || a >= NodeCount || b < 0 || b >= NodeCount) continue; string name = OBJ_PREFIX + "PATH_" + IntegerToString(p); ObjectCreate(0, name, OBJ_TREND, 0, Nodes[a].time, Nodes[a].price, Nodes[b].time, Nodes[b].price); ObjectSetInteger(0, name, OBJPROP_COLOR, InpPathColor); ObjectSetInteger(0, name, OBJPROP_WIDTH, 3); ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } } //--- Draw nodes (circles/arrows) if(InpDrawNodes) { for(int i = 0; i < NodeCount; i++) { string name = OBJ_PREFIX + "NODE_" + IntegerToString(i); color col = Nodes[i].isHigh ? InpHighNodeColor : InpLowNodeColor; int arrow = Nodes[i].isHigh ? 217 : 218; // down/up triangle arrows ObjectCreate(0, name, OBJ_ARROW, 0, Nodes[i].time, Nodes[i].price); ObjectSetInteger(0, name, OBJPROP_ARROWCODE, arrow); ObjectSetInteger(0, name, OBJPROP_COLOR, col); ObjectSetInteger(0, name, OBJPROP_WIDTH, InpNodeSize); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); //--- Highlight path nodes if(InpDrawPath) { for(int p = 0; p < PathLength; p++) { if(BestPath[p] == i) { ObjectSetInteger(0, name, OBJPROP_COLOR, InpPathColor); ObjectSetInteger(0, name, OBJPROP_WIDTH, InpNodeSize + 2); break; } } } } } ChartRedraw(); } //+------------------------------------------------------------------+
Данная функция реализует модуль визуализации для графического представления системы A* непосредственно на графике. Вначале она очищает все ранее нарисованные объекты, чтобы отображение оставалось актуальным и чистым. Затем она проверяет, существуют ли узлы, прежде чем продолжить. Если включена отрисовка ребер, мы отображаем все связи между узлами пунктирными линиями тренда, эффективно показывая полную структуру графа, представляющую рыночные взаимосвязи. Эти ребра рисуются первыми, поэтому они отображаются позади других элементов.
Затем мы отображаем оптимальный путь A*, соединяя последовательные узлы в наилучшем пути более толстыми сплошными линиями, чтобы выделить выбранный маршрут через структуру рынка. После этого мы рисуем все узлы свингов в виде стрелок, разных цветов и форм в зависимости от того, представляют ли они максимумы или минимумы. Если узел является частью оптимального пути, мы визуально выделяем его, изменяя его цвет и увеличивая размер. Такая многоуровневая визуализация позволяет нам четко различать исходную структуру рынка, полный граф и окончательный путь решения A*.
В завершение мы вызываем ChartRedraw(), которая заставляет платформу немедленно обновить график, чтобы все визуальные элементы отображались в реальном времени.

Результаты бэктестирования
Бэктестирование проводилось по XAUUSD на таймфрейме H1 за двухмесячный период (с 02 февраля 2026 года по 01 апреля 2026 года) со следующими настройками:

Кривая эквити и результаты бэктестирования показаны ниже:


Заключение
Мы преобразовали "шумный" ценовой график в формальную карту рынка и разработали практичную структуру советника MetaTrader 5, которая заменяет субъективные суждения измеримыми критериями. Рабочая схема представляет собой конвейер UpdateSwings -> BuildGraph -> RunAStar -> EvaluatePath -> ExecuteTrade: он (1) извлекает подтвержденные узлы свингов (удаляя уже пробитые уровни), (2) строит взвешенные ребра, используя нормализованное по ATR расстояние, стоимость спреда и штраф за шум, (3) находит наиболее эффективный путь с помощью алгоритма A* и (4) фильтрует и преобразует этот путь в сделку только тогда, когда выполняются формальные метрики — коэффициент направленности (доля движения вверх/вниз), общая стоимость пути (g) и отсутствие встречных препятствующих свингов. При исполнении сделки тейк-профит устанавливается в конечном узле, стоп-лосс в ближайшей противоположной структуре (с буфером ATR) либо используется резервный вариант на основе волатильности, а размер позиций определяется процентным риском.
На практике вы получаете прозрачный советник, который отображает узлы/ребра/путь на графике и совершает сделки только при соблюдении явно заданных, настраиваемых пороговых значений (период ретроспективного анализа, период ATR, штрафы за спред/шум, минимальная оценка пути, максимальная стоимость пути, % риска). Это делает результаты воспроизводимыми и простыми для бэктестирования: корректность работы и результаты можно проверить по наличию визуальных объектов (узлов/ребер/пути), по сделкам, которые соответствуют заявленным правилам принятия решений, и по возможности настройки параметров для улучшения соотношения прибыли и убытков при тех же формальных критериях.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22184
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Создание пользовательских индикаторов в MQL5 (Часть 8): Интеграция объёма для углублённого анализа рыночного профиля (Market Profile)
Нейросети в трейдинге: От единственного прогноза к пространству рыночных сценариев (ORION)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования