Теория графов: Применение алгоритма поиска в ширину (BFS) в торговой системе
Содержание:
- Введение
- Обзор системы и основы BFS
- Приступаем к работе
- Результаты тестирования на истории
- Заключение
Введение
Финансовые рынки на первый взгляд часто кажутся хаотичными, однако цены последовательно перемещаются из одной области в другую таким образом, что это предполагает наличие глубинной логики, а не случайности. Поэтому трейдеры и разработчики систем стремятся выявить алгоритм, которому, по-видимому, следует сам рынок при переходе между уровнями ликвидности, структуры и дисбаланса. Эта задача заключается не в прогнозировании в отрыве от контекста, а в понимании того, как прошлые ценовые взаимодействия влияют на будущие движения и как структура рынка развивается шаг за шагом во времени.
Теория графов предлагает мощную основу для приближения к этой цели, позволяя моделировать движение цен как структурированную, основанную на правилах систему, а не как последовательность отдельных свечей. Применяя поиск в ширину (BFS) с обходом уровней, структуру рынка можно исследовать постепенно, начиная с недавних свингов и возвращаясь к историческому контексту, с учетом направленного течения времени. Этот подход преобразует исторические бары в ориентированный граф точек принятия решений, позволяя систематически интерпретировать бычью и медвежью структуру и приближая алгоритмическую торговлю к отражению собственной логики поиска пути рынка.
Обзор системы и основы BFS:
| Концепция BFS | Торговая интерпретация |
|---|---|
| Узел. | Точка разворота (максимум или минимум). |
| Ребро (ориентированное). | Ход времени (от более раннего к более позднему). |
| Уровень. | Расстояние в барах или днях. |
| Корень. | Историческая привязка (начальная точка). |
| Обход. | Развитие структуры рынка. |
| Цель. | Определение бычьего или медвежьего уклона рынка. |
Советник BFS Market Structure преобразует традиционный технический анализ, применяя концепции теории графов к движению рыночных цен. Система начинает с определения значимых свинг-точек (максимумов и минимумов) на основе исторических данных о ценах, рассматривая каждый свинг как узел в ориентированном графе, где ребра представляют хронологическую последовательность. Используя поиск в ширину (BFS), начиная с настраиваемой исторической точки отсчета, алгоритм обрабатывает структуру рынка в порядке уровней — сначала анализируя ближайшие свинги, а затем переходя к более старым формациям. Каждый узел свинга классифицируется как бычий или медвежий на основе сравнительных ценовых взаимосвязей, при этом более новые структуры имеют больший вес в итоговом расчете структурного смещения. Это дает непрерывный показатель смещения в диапазоне от -1 (медвежий) до +1 (бычий), представляющий структурную эволюцию рынка, а не традиционные сигналы на основе индикаторов.
Слой принятия торговых решений фильтрует потенциальные сделки по этому структурному смещению, требуя, чтобы показатель превышал настраиваемые пороговые значения, прежде чем разрешить открытие позиций на покупку или продажу. При включении советник исполняет сделки, используя профессиональное управление ордерами через класс CTrade, с возможностью подтверждения на основе недавних пробоев свингов. Система включает три различных контекстных режима — предыдущие бары для скальпинга, предыдущие дни для свинг-трейдинга и только вчерашний день для внутридневной торговли — что позволяет адаптироваться к различным стилям торговли. Визуализация в реальном времени отображает график свингов с цветовой кодировкой узлов и соединений ребер, что делает структурный анализ прозрачным и обеспечивает мгновенную обратную связь по рыночным условиям. Такой подход создает систематическую, основанную на правилах методологию, которая концентрируется на понимании эволюции структуры рынка, а не на прогнозировании будущих движений цен.
Приступаем к работе
//+------------------------------------------------------------------+ //| BFS-EA.mq5 | //| GIT under Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com/ru/users/johnhlomohang/ | //+------------------------------------------------------------------+ #property copyright "GIT under Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/ru/users/johnhlomohang/" #property version "1.00" #include <Trade\Trade.mqh> #include <Arrays\List.mqh> //+------------------------------------------------------------------+ //| Input Parameters | //+------------------------------------------------------------------+ enum ENUM_HISTORY_MODE { MODE_PREVIOUS_BARS, // Previous Bars MODE_PREVIOUS_DAYS, // Previous Days MODE_YESTERDAY_ONLY // Yesterday Only }; input ENUM_HISTORY_MODE HistoryMode = MODE_PREVIOUS_DAYS; // History Mode input int BarsLookback = 300; // Bars Lookback input int DaysLookback = 5; // Days Lookback input int SwingPeriod = 3; // Swing Period (lookback bars) input double BiasThreshold = 0.3; // Bias Threshold input bool EnableTrading = true; // Enable Trading input double LotSize = 0.1; // Lot Size input int TakeProfit = 100; // Take Profit (points) input int StopLoss = 50; // Stop Loss (points) input bool VisualizeStructure = true; // Visualize Structure input color BullishColor = clrGreen; // Bullish Color input color BearishColor = clrRed; // Bearish Color
Начнём с блока входных параметров, который определяет, как советник воспринимает исторический рыночный контекст и преобразует его в структурированную торговую логику. Параметр ENUM_HISTORY_MODE позволяет пользователю управлять тем, будет ли алгоритм оценивать фиксированное количество прошлых баров, несколько предыдущих торговых дней или движение цен исключительно за вчерашний день, напрямую влияя на то, как строится рыночный граф. Такие параметры, как BarsLookback, DaysLookback и SwingPeriod, определяют глубину и зернистость обнаружения свингов, а BiasThreshold устанавливает минимальную структурную уверенность, необходимую для того, чтобы бычий или медвежий тренд считался валидным.
Остальные параметры отвечают за исполнение и прозрачность работы системы. Торговые параметры, такие как EnableTrading, LotSize, TakeProfit и StopLoss, отделяют логику принятия решений от управления рисками, гарантируя, что структурный анализ может функционировать независимо от правил исполнения. Настройки визуализации, включая VisualizeStructure, BullishColor и BearishColor, обеспечивают прозрачность, отображая обнаруженную структуру рынка непосредственно на графике, что позволяет трейдерам визуально подтвердить, как алгоритм классифицирует бычьи и медвежьи свинги на основе базовой логики обхода графа.
//+------------------------------------------------------------------+ //| Swing Node Structure | //+------------------------------------------------------------------+ struct SwingNode { int index; // bar index double price; // high or low price bool isHigh; // true = swing high, false = swing low bool bullish; // inferred direction int level; // BFS level datetime time; // bar time // Default constructor SwingNode(): index(0), price(0.0), isHigh(false), bullish(false), level(-1), time(0) {} // Parameterized constructor SwingNode(int idx, double prc, bool high, bool bull, int lvl, datetime t): index(idx), price(prc), isHigh(high), bullish(bull), level(lvl), time(t) {} }; //+------------------------------------------------------------------+ //| BFS Queue Item | //+------------------------------------------------------------------+ struct BFSItem { int nodeIndex; // index in nodes array int level; // BFS level }; //+------------------------------------------------------------------+ //| Edge Structure for Graph | //+------------------------------------------------------------------+ struct Edge { int from; // Source node index int to; // Destination node index }; //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ SwingNode nodes[]; // Array of swing nodes Edge edges[]; // Array of edges int edgeCount = 0; // Number of edges int nodeCount = 0; // Number of nodes double currentBias = 0.0; // Current market bias bool allowBuy = false; // Allow buy trades bool allowSell = false; // Allow sell trades datetime lastRecalcTime = 0; // Last recalculation time int lastBarCount = 0; // Last bar count CTrade trade; // Trading object for position management
Этот раздел кода определяет основные структуры данных, которые преобразуют исходные данные о движении цены в граф, подходящий для обхода поиска в ширину. Структура SwingNode представляет каждую значимую точку разворота рынка, храня индекс бара, цену, время и информацию о том, является ли это максимумом или минимумом свинга. Дополнительные атрибуты, такие как "бычий" и "уровень", позволяют каждому узлу передавать направленность и глубину BFS, что позволяет оценивать структуру рынка постепенно во времени. Инкапсулируя как структурную, так и временную информацию, каждый свинг становится значимым узлом в ориентированном рыночном графе, а не отдельным ценовым событием.
В поддержку этого представления структуры BFSItem и Edge определяют, как осуществляется обход и связность внутри графа. BFSItem обеспечивает обработку в порядке уровней, отслеживая, какой узел посещается на каждом уровне BFS, в то время как Edge явно моделирует направленную взаимосвязь между более старыми и более новыми точками разворота. Глобальные переменные затем служат операционной основой советника, храня построенный граф, поддерживая состояние обхода и преобразуя структурные смещения в действенные торговые разрешения. Вместе эти компоненты связывают теорию графов и логику исполнения, позволяя советнику системно оценивать направление рынка перед открытием сделок или фильтрацией торговых сигналов.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Validate inputs if(BarsLookback <= 0) { Print("Error: BarsLookback must be positive"); return INIT_PARAMETERS_INCORRECT; } if(DaysLookback <= 0) { Print("Error: DaysLookback must be positive"); return INIT_PARAMETERS_INCORRECT; } if(SwingPeriod <= 0) { Print("Error: SwingPeriod must be positive"); return INIT_PARAMETERS_INCORRECT; } // Set timer for periodic updates (every 5 minutes) EventSetTimer(300); // Set trading parameters trade.SetExpertMagicNumber(12345); trade.SetDeviationInPoints(10); trade.SetTypeFilling(ORDER_FILLING_FOK); // Initial calculation CalculateBFSStructure(); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Remove timer EventKillTimer(); // Clean up visualization objects if(VisualizeStructure) { RemoveVisualization(); } } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { // Check for new bar if(IsNewBar()) { CalculateBFSStructure(); // Check for trading signals if enabled if(EnableTrading) { CheckTradingSignals(); } } // Update visualization if enabled if(VisualizeStructure) { UpdateVisualization(); } } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { // Periodic recalculation (fallback) CalculateBFSStructure(); }
Здесь мы определяем жизненный цикл советника, координируя инициализацию, исполнение и очистку, обеспечивая при этом актуальность рыночной структуры на основе BFS. Во время инициализации проверяются входные параметры для предотвращения недопустимых конфигураций, подготавливаются торговые настройки и выполняется первоначальный расчет структуры BFS с установлением таймера для обеспечения периодического перерасчета. Основная логика исполнения пересчитывает структуру только на новых барах или временных интервалах для повышения эффективности, применяет торговые решения, когда это включено, и постоянно обновляет визуализацию на графике, в то время как фаза деинициализации безопасно удаляет таймеры и графические объекты для поддержания стабильности платформы.
//+------------------------------------------------------------------+ //| Calculate BFS Market Structure | //+------------------------------------------------------------------+ void CalculateBFSStructure() { // Reset values currentBias = 0.0; allowBuy = false; allowSell = false; // Step 1: Detect swing points if(!DetectSwingNodes()) { Print("Failed to detect swing nodes"); return; } // Step 2: Build directed graph (edges) BuildDirectedGraph(); // Step 3: Select BFS root based on mode int rootIndex = SelectBFSRoot(); if(rootIndex < 0) { Print("No valid root found"); return; } // Step 4: Perform BFS traversal PerformBFSTraversal(rootIndex); // Step 5: Calculate market bias CalculateMarketBias(); // Step 6: Update trading permissions UpdateTradingPermissions(); // Update last calculation time lastRecalcTime = TimeCurrent(); } //+------------------------------------------------------------------+ //| Detect Swing Nodes | //+------------------------------------------------------------------+ bool DetectSwingNodes() { // Determine how many bars to analyze int barsToAnalyze = 0; datetime startTime = 0; switch(HistoryMode) { case MODE_PREVIOUS_BARS: barsToAnalyze = BarsLookback + SwingPeriod * 2; break; case MODE_PREVIOUS_DAYS: startTime = TimeCurrent() - (DaysLookback * 86400); barsToAnalyze = iBars(_Symbol, _Period); break; case MODE_YESTERDAY_ONLY: {MqlDateTime yesterday; TimeToStruct(TimeCurrent() - 86400, yesterday); yesterday.hour = 0; yesterday.min = 0; yesterday.sec = 0; startTime = StructToTime(yesterday); barsToAnalyze = iBars(_Symbol, _Period); break;} } // Get historical data int totalBars = MathMin(barsToAnalyze, iBars(_Symbol, _Period)); if(totalBars < SwingPeriod * 2 + 10) { Print("Not enough bars for swing detection"); return false; } // Prepare arrays for highs and lows double highs[], lows[]; datetime times[]; ArrayResize(highs, totalBars); ArrayResize(lows, totalBars); ArrayResize(times, totalBars); // Copy data if(CopyHigh(_Symbol, _Period, 0, totalBars, highs) != totalBars || CopyLow(_Symbol, _Period, 0, totalBars, lows) != totalBars || CopyTime(_Symbol, _Period, 0, totalBars, times) != totalBars) { Print("Failed to copy historical data"); return false; } // Detect swing highs and lows ArrayResize(nodes, 0); nodeCount = 0; for(int i = SwingPeriod; i < totalBars - SwingPeriod; i++) { bool isSwingHigh = true; bool isSwingLow = true; // Check for swing high for(int j = 1; j <= SwingPeriod; j++) { if(highs[i] <= highs[i-j] || highs[i] <= highs[i+j]) { isSwingHigh = false; break; } } // Check for swing low for(int j = 1; j <= SwingPeriod; j++) { if(lows[i] >= lows[i-j] || lows[i] >= lows[i+j]) { isSwingLow = false; break; } } // Add swing node if found if(isSwingHigh || isSwingLow) { SwingNode node; node.index = i; node.time = times[i]; node.isHigh = isSwingHigh; if(isSwingHigh) node.price = highs[i]; else node.price = lows[i]; node.bullish = false; // Will be determined during BFS node.level = -1; // Not assigned yet ArrayResize(nodes, nodeCount + 1); nodes[nodeCount] = node; nodeCount++; } } Print("Detected ", nodeCount, " swing nodes"); return nodeCount > 0; } //+------------------------------------------------------------------+ //| Build Directed Graph | //+------------------------------------------------------------------+ void BuildDirectedGraph() { // Clear existing edges ArrayResize(edges, 0); edgeCount = 0; // Create edges from older to newer nodes (time progression) for(int i = 0; i < nodeCount; i++) { // Find the next valid swing to connect to int nextIndex = -1; // Look for the next swing of alternating type for(int j = i + 1; j < nodeCount; j++) { // Check if swing types alternate (high-low-high-low pattern) if(nodes[j].isHigh != nodes[i].isHigh) { // Check if there are no other valid swings between i and j bool validConnection = true; // Skip checking intermediate nodes for simplicity // In a more sophisticated version, we could check for // price action patterns between swings if(validConnection) { nextIndex = j; break; } } } // Add edge if found if(nextIndex > i) { Edge edge; edge.from = i; edge.to = nextIndex; ArrayResize(edges, edgeCount + 1); edges[edgeCount] = edge; edgeCount++; } } Print("Created ", edgeCount, " edges in the graph"); } //+------------------------------------------------------------------+ //| Select BFS Root Based on Mode | //+------------------------------------------------------------------+ int SelectBFSRoot() { int rootIndex = -1; datetime currentTime = TimeCurrent(); switch(HistoryMode) { case MODE_PREVIOUS_BARS: // Find the oldest swing within BarsLookback for(int i = 0; i < nodeCount; i++) { if(nodes[i].index >= BarsLookback) { rootIndex = i; break; } } break; case MODE_PREVIOUS_DAYS: { datetime startTime = currentTime - (DaysLookback * 86400); // Find first swing after startTime for(int i = 0; i < nodeCount; i++) { if(nodes[i].time >= startTime) { rootIndex = i; break; } } break; } case MODE_YESTERDAY_ONLY: { MqlDateTime yesterday; TimeToStruct(currentTime - 86400, yesterday); yesterday.hour = 0; yesterday.min = 0; yesterday.sec = 0; datetime yesterdayStart = StructToTime(yesterday); datetime todayStart = yesterdayStart + 86400; // Find first swing of yesterday for(int i = 0; i < nodeCount; i++) { if(nodes[i].time >= yesterdayStart && nodes[i].time < todayStart) { rootIndex = i; break; } } break; } } // If no root found with mode criteria, use the oldest node if(rootIndex == -1 && nodeCount > 0) { rootIndex = 0; } return rootIndex; }
В этом разделе выстраивается полный конвейер оценки структуры рынка путем координации обнаружения свингов, построения графа, обхода графа и вычисления смещения. Функция CalculateBFSStructure выступает в качестве центрального контроллера. Сначала сбрасываются все состояния направления и разрешений, а затем последовательно выполняется каждый этап анализа. Разделяя обнаружение, построение графа, обход и обновление решений на четкие шаги, советник обеспечивает согласованность, детерминированность и простоту отладки каждого перерасчета структуры рынка, а также предотвращает влияние частичных или устаревших результатов на торговые решения.
Функция DetectSwingNodes отвечает за преобразование необработанных исторических данных о ценах в значимые структурные узлы. В зависимости от выбранного режима истории она динамически определяет объем анализируемых исторических данных: фиксированное количество баров, несколько дней или только вчерашняя сессия. Далее она сканирует максимумы и минимумы цен, используя определенный период свингов, чтобы определить действительные точки разворота, обеспечивая изоляцию каждого обнаруженного свинга от окружающего шума. Эти свинговые точки становятся основными узлами графа, каждый из которых содержит точную информацию о времени, цене и структуре.
После определения узлов свингов, BuildDirectedGraph преобразует их в ориентированный рыночный граф, связывая более старые свинги с более новыми в последовательности, соответствующей ходу времени. Ребра создаются только между чередующимися типами свингов, обеспечивая реалистичную последовательность максимумов и минимумов, отражающую естественную структуру рынка. Такая направленная связность гарантирует, что граф учитывает временной поток и избегает обратных или циклических связей, что делает его подходящим для обхода по уровням и предотвращает перерисовку или логические противоречия при структурном анализе.
Функция selectBFSRoot определяет, где начинается обход, закрепляя анализ в заданном пользователем историческом контексте. В зависимости от выбранного режима, корень может представлять собой самый ранний релевантный свинг в пределах окна бара, многодневного диапазона или одной торговой сессии. Если ни один узел не удовлетворяет критериям, алгоритм безопасно переходит к самому старому доступному свингу, гарантируя непрерывность. Выбор этого корня имеет решающее значение, поскольку он определяет перспективу, с которой разворачивается BFS, напрямую влияя на то, как прошлые структуры накладываются, оцениваются и в конечном итоге преобразуются в текущее рыночное смещение.
//+------------------------------------------------------------------+ //| Perform BFS Traversal | //+------------------------------------------------------------------+ void PerformBFSTraversal(int rootIndex) { if(rootIndex < 0 || rootIndex >= nodeCount) return; // Reset node levels for(int i = 0; i < nodeCount; i++) { nodes[i].level = -1; nodes[i].bullish = false; } // Initialize queue for BFS BFSItem queue[]; int queueSize = nodeCount; ArrayResize(queue, queueSize); // Enqueue root queue[0].nodeIndex = rootIndex; queue[0].level = 0; nodes[rootIndex].level = 0; int queueStart = 0; int queueEnd = 1; // BFS traversal while(queueStart < queueEnd) { // Dequeue BFSItem current = queue[queueStart]; queueStart++; int currentIdx = current.nodeIndex; int currentLevel = current.level; // Classify node direction ClassifyNodeDirection(currentIdx); // Find and enqueue children (nodes reachable from current node) for(int e = 0; e < edgeCount; e++) { if(edges[e].from == currentIdx) { int childIdx = edges[e].to; if(nodes[childIdx].level == -1) // Not visited yet { nodes[childIdx].level = currentLevel + 1; // Enqueue child if(queueEnd < queueSize) { queue[queueEnd].nodeIndex = childIdx; queue[queueEnd].level = currentLevel + 1; queueEnd++; } } } } } } //+------------------------------------------------------------------+ //| Classify Node Direction | //+------------------------------------------------------------------+ void ClassifyNodeDirection(int nodeIndex) { if(nodeIndex <= 0) { // First node, classify based on recent price action double currentPrice = iClose(_Symbol, _Period, 0); if(nodes[nodeIndex].isHigh) { nodes[nodeIndex].bullish = (currentPrice > nodes[nodeIndex].price); } else { nodes[nodeIndex].bullish = (currentPrice < nodes[nodeIndex].price); } return; } // Find previous node of the same type int prevSameType = -1; for(int i = nodeIndex - 1; i >= 0; i--) { if(nodes[i].isHigh == nodes[nodeIndex].isHigh) { prevSameType = i; break; } } if(prevSameType >= 0) { // Compare with previous node of same type if(nodes[nodeIndex].isHigh) { // Swing high: higher high = bullish, lower high = bearish nodes[nodeIndex].bullish = (nodes[nodeIndex].price > nodes[prevSameType].price); } else { // Swing low: higher low = bullish, lower low = bearish nodes[nodeIndex].bullish = (nodes[nodeIndex].price > nodes[prevSameType].price); } } else { // No previous node of same type, use simple logic double currentPrice = iClose(_Symbol, _Period, 0); if(nodes[nodeIndex].isHigh) { nodes[nodeIndex].bullish = (currentPrice > nodes[nodeIndex].price); } else { nodes[nodeIndex].bullish = (currentPrice < nodes[nodeIndex].price); } } } //+------------------------------------------------------------------+ //| Calculate Market Bias | //+------------------------------------------------------------------+ void CalculateMarketBias() { double totalWeight = 0.0; double weightedSum = 0.0; for(int i = 0; i < nodeCount; i++) { if(nodes[i].level >= 0) // Only consider visited nodes { // Calculate weight based on level (higher weight for lower levels) double weight = 1.0 / (1.0 + nodes[i].level * 0.2); // Add to weighted sum if(nodes[i].bullish) weightedSum += weight; else weightedSum -= weight; totalWeight += weight; } } if(totalWeight > 0) { currentBias = weightedSum / totalWeight; } else { currentBias = 0.0; } Print("Market Bias: ", DoubleToString(currentBias, 3)); } //+------------------------------------------------------------------+ //| Update Trading Permissions | //+------------------------------------------------------------------+ void UpdateTradingPermissions() { allowBuy = (currentBias > BiasThreshold); allowSell = (currentBias < -BiasThreshold); Print("Trading Permissions - Buy: ", allowBuy, ", Sell: ", allowSell); } //+------------------------------------------------------------------+ //| Check Trading Signals | //+------------------------------------------------------------------+ void CheckTradingSignals() { // Check for existing positions if(PositionsTotal() > 0) { // Manage existing positions ManagePositions(); return; } // Check for new signals if(allowBuy) { // Buy signal logic double askPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double sl = askPrice - StopLoss * _Point; double tp = askPrice + TakeProfit * _Point; // Optional: Add confirmation from recent swing break if(CheckRecentSwingBreak(true)) { ExecuteBuyOrder(askPrice, sl, tp); } } else if(allowSell) { // Sell signal logic double bidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = bidPrice + StopLoss * _Point; double tp = bidPrice - TakeProfit * _Point; // Optional: Add confirmation from recent swing break if(CheckRecentSwingBreak(false)) { ExecuteSellOrder(bidPrice, sl, tp); } } } //+------------------------------------------------------------------+ //| Check Recent Swing Break | //+------------------------------------------------------------------+ bool CheckRecentSwingBreak(bool forBuy) { if(nodeCount < 2) return true; // No recent swings, allow trade // Find the most recent swing int recentSwingIndex = -1; for(int i = nodeCount - 1; i >= 0; i--) { if(nodes[i].index < 50) // Within recent 50 bars { recentSwingIndex = i; break; } } if(recentSwingIndex < 0) return true; double currentPrice = iClose(_Symbol, _Period, 0); if(forBuy) { // For buy: check if price has broken above a recent swing high if(nodes[recentSwingIndex].isHigh) { return (currentPrice > nodes[recentSwingIndex].price); } } else { // For sell: check if price has broken below a recent swing low if(!nodes[recentSwingIndex].isHigh) { return (currentPrice < nodes[recentSwingIndex].price); } } return true; } //+------------------------------------------------------------------+ //| Execute Buy Order using CTrade | //+------------------------------------------------------------------+ void ExecuteBuyOrder(double price, double sl, double tp) { // Check if we already have a buy position if(trade.Buy(LotSize, _Symbol, price, sl, tp, "BFS Buy Signal")) { Print("Buy order executed successfully"); } else { Print("Buy order failed. Error: ", GetLastError(), " - ", trade.ResultRetcodeDescription()); } } //+------------------------------------------------------------------+ //| Execute Sell Order using CTrade | //+------------------------------------------------------------------+ void ExecuteSellOrder(double price, double sl, double tp) { // Check if we already have a sell position if(trade.Sell(LotSize, _Symbol, price, sl, tp, "BFS Sell Signal")) { Print("Sell order executed successfully"); } else { Print("Sell order failed. Error: ", GetLastError(), " - ", trade.ResultRetcodeDescription()); } } //+------------------------------------------------------------------+ //| Manage Positions using CTrade | //+------------------------------------------------------------------+ void ManagePositions() { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(PositionSelectByTicket(ticket)) { string symbol = PositionGetString(POSITION_SYMBOL); long magic = PositionGetInteger(POSITION_MAGIC); if(symbol == _Symbol && magic == 12345) { // Check if we should close based on bias change if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && currentBias < -BiasThreshold) || (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && currentBias > BiasThreshold)) { // Close position due to bias reversal if(trade.PositionClose(ticket)) { Print("Position closed due to bias reversal. Ticket: ", ticket); } else { Print("Failed to close position. Error: ", GetLastError(), " - ", trade.ResultRetcodeDescription()); } } } } } }
В этом разделе мы реализуем основной модуль логического вывода советника, обходя граф структуры рынка с помощью поиска в ширину в порядке уровней. Функция PerformBFSTraversal инициализирует обход, начиная с выбранного корневого свинга, и постепенно посещает связанные свинги в хронологическом порядке, присваивая каждому узлу уровень глубины, отражающий его структурное расстояние от корня. По мере посещения каждого узла он классифицируется как бычий или медвежий в зависимости от его связи с предыдущими свингами, что гарантирует, что интерпретация направления основана на развивающейся рыночной структуре, а не на отдельных ценовых точках.
Логика ClassifyNodeDirection определяет, представляет ли свинг бычью или медвежью структуру, сравнивая его с самым последним свингом того же типа. Более высокие максимумы и более высокие минимумы интерпретируются как бычье продолжение тренда, в то время как более низкие максимумы и более низкие минимумы сигнализируют о медвежьем намерении. Если сопоставимого предыдущего свинга не существует, алгоритм использует взаимодействие текущей цены с уровнем свинга как запасной критерий, сохраняя направленный контекст даже на структурных границах. Этот подход многоуровневой классификации позволяет каждому уровню BFS нести значимую информацию о направлении, которая отражает, как структура разворачивается с течением времени.
Наконец, сигналы направления, генерируемые в процессе обхода, объединяются в единое нормализованное смещение рынка. Функция CalculateMarketBias присваивает больший вес более низким уровням BFS, гарантируя, что недавняя структура оказывает более сильное влияние, чем давняя история. Это смещение затем преобразуется в действенные разрешения через UpdateTradingPermissions, которые служат фильтром допуска к сделкам, а не безусловным сигналом на вход. Последующие функции обрабатывают подтверждение, исполнение ордеров и управление позициями, обеспечивая соответствие сделок преобладающему структурному смещению и их быстрое закрытие в случае изменения направления базового рынка.
//+------------------------------------------------------------------+ //| Update Visualization | //+------------------------------------------------------------------+ void UpdateVisualization() { RemoveVisualization(); // Draw swing nodes for(int i = 0; i < nodeCount; i++) { if(nodes[i].level >= 0 && nodes[i].index < 200) // Only show recent 200 bars { string objName = "SwingNode_" + IntegerToString(i); // Create object based on swing type if(nodes[i].isHigh) { // Draw swing high as downward arrow ObjectCreate(0, objName, OBJ_ARROW_DOWN, 0, nodes[i].time, nodes[i].price); ObjectSetInteger(0, objName, OBJPROP_ANCHOR, ANCHOR_TOP); } else { // Draw swing low as upward arrow ObjectCreate(0, objName, OBJ_ARROW_UP, 0, nodes[i].time, nodes[i].price); ObjectSetInteger(0, objName, OBJPROP_ANCHOR, ANCHOR_BOTTOM); } // Set color based on bullish/bearish classification if(nodes[i].bullish) ObjectSetInteger(0, objName, OBJPROP_COLOR, BullishColor); else ObjectSetInteger(0, objName, OBJPROP_COLOR, BearishColor); // Set size ObjectSetInteger(0, objName, OBJPROP_WIDTH, 2); // Add level annotation string labelName = "Label_" + IntegerToString(i); ObjectCreate(0, labelName, OBJ_TEXT, 0, nodes[i].time, nodes[i].price); ObjectSetString(0, labelName, OBJPROP_TEXT, "L" + IntegerToString(nodes[i].level)); ObjectSetInteger(0, labelName, OBJPROP_COLOR, clrWhite); ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); ObjectSetInteger(0, labelName, OBJPROP_ANCHOR, ANCHOR_UPPER); // Adjust label position based on swing type if(nodes[i].isHigh) ObjectSetDouble(0, labelName, OBJPROP_PRICE, nodes[i].price + 10 * _Point); else ObjectSetDouble(0, labelName, OBJPROP_PRICE, nodes[i].price - 10 * _Point); } } // Draw edges between nodes for(int e = 0; e < edgeCount; e++) { int fromIdx = edges[e].from; int toIdx = edges[e].to; // Only draw edges for visible nodes if(fromIdx < nodeCount && toIdx < nodeCount && nodes[fromIdx].index < 200 && nodes[toIdx].index < 200) { string edgeName = "Edge_" + IntegerToString(e); ObjectCreate(0, edgeName, OBJ_TREND, 0, nodes[fromIdx].time, nodes[fromIdx].price, nodes[toIdx].time, nodes[toIdx].price); ObjectSetInteger(0, edgeName, OBJPROP_COLOR, clrGray); ObjectSetInteger(0, edgeName, OBJPROP_WIDTH, 1); ObjectSetInteger(0, edgeName, OBJPROP_STYLE, STYLE_DASH); } } // Draw bias indicator string biasObjName = "BiasIndicator"; ObjectCreate(0, biasObjName, OBJ_RECTANGLE_LABEL, 0, 0, 0); ObjectSetInteger(0, biasObjName, OBJPROP_XDISTANCE, 10); ObjectSetInteger(0, biasObjName, OBJPROP_YDISTANCE, 10); ObjectSetInteger(0, biasObjName, OBJPROP_XSIZE, 150); ObjectSetInteger(0, biasObjName, OBJPROP_YSIZE, 60); ObjectSetInteger(0, biasObjName, OBJPROP_BGCOLOR, clrBlack); ObjectSetInteger(0, biasObjName, OBJPROP_BORDER_TYPE, BORDER_FLAT); ObjectSetInteger(0, biasObjName, OBJPROP_BORDER_COLOR, clrGray); // Bias value string biasValueName = "BiasValue"; ObjectCreate(0, biasValueName, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, biasValueName, OBJPROP_XDISTANCE, 20); ObjectSetInteger(0, biasValueName, OBJPROP_YDISTANCE, 20); ObjectSetString(0, biasValueName, OBJPROP_TEXT, "Bias: " + DoubleToString(currentBias, 3)); ObjectSetInteger(0, biasValueName, OBJPROP_COLOR, clrWhite); ObjectSetInteger(0, biasValueName, OBJPROP_FONTSIZE, 12); // Trading status string statusName = "TradingStatus"; ObjectCreate(0, statusName, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, statusName, OBJPROP_XDISTANCE, 20); ObjectSetInteger(0, statusName, OBJPROP_YDISTANCE, 40); string statusText = "Status: "; if(allowBuy) statusText += "BUY"; else if(allowSell) statusText += "SELL"; else statusText += "NEUTRAL"; ObjectSetString(0, statusName, OBJPROP_TEXT, statusText); ObjectSetInteger(0, statusName, OBJPROP_COLOR, clrYellow); ObjectSetInteger(0, statusName, OBJPROP_FONTSIZE, 10); } //+------------------------------------------------------------------+ //| Remove Visualization | //+------------------------------------------------------------------+ void RemoveVisualization() { // Remove all objects created by this EA int total = ObjectsTotal(0); for(int i = total - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, "SwingNode_") == 0 || StringFind(name, "Label_") == 0 || StringFind(name, "Edge_") == 0 || StringFind(name, "Bias") == 0 || StringFind(name, "TradingStatus") == 0) { ObjectDelete(0, name); } } }
Функция UpdateVisualization отвечает за преобразование внутренней структуры рынка, основанной на графах, в понятные визуальные элементы на графике в реальном времени. Она начинается с удаления ранее отображенных объектов, чтобы предотвратить наложение или устаревшую информацию, гарантируя, что отображение всегда будет отражать самый последний анализ. Каждый обнаруженный узел колебания затем отображается в виде стрелки, при этом максимумы свингов отображаются стрелками вниз, а минимумы свингов — стрелками вверх, ограничиваясь последними 200 барами для обеспечения ясности и эффективности. Цветовая кодировка применяется в зависимости от бычьего или медвежьего тренда, а рядом с каждым свингом располагается небольшая метка уровня, указывающая на его глубину BFS, визуально показывая, насколько далеко каждый узел находится от корня в структурной иерархии.
Помимо отдельных узлов, функция также визуализирует взаимосвязи между ними, рисуя пунктирные линии тренда, которые представляют собой ребра на графе, эффективно отображая, как цена переходит от одного свинга к другому с течением времени. Кроме того, на графике создается компактная панель мониторинга, отображающая текущее рыночное смещение и состояние разрешения на торговлю, например, ПОКУПКА, ПРОДАЖА или НЕЙТРАЛЬНО, что позволяет трейдерам немедленно интерпретировать структурные выводы алгоритма. Дополнительная функция RemoveVisualization действует как модуль очистки, выборочно удаляя только объекты, созданные советником на основе соглашений об именовании, что поддерживает порядок на графике и гарантирует, что каждое обновление визуализации является точным и ненавязчивым.
Результаты тестирования
Тестирование на истории было проведено на таймфрейме H1 в течение примерно 2-месячного периода тестирования (с 01 октября 2025 года по 01 декабря 2025 года) со следующими настройками:

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


Заключение
Подводя итог, мы разработали и интегрировали поиск в ширину (BFS) в торговую систему, сначала переопределив структуру рынка как граф, состоящий из максимумов и минимумов свингов, выступающих в качестве узлов, с переходами, согласованными по времени, представленными в виде направленных ребер. Свинги были обнаружены на основе исходных данных о ценах, структурированы в узлы с контекстными атрибутами, а затем связаны для отражения реалистичной последовательности максимумов и минимумов. Обход BFS был применен, начиная с тщательно выбранного корня в историческом контексте, что позволило системе поэтапно выстраивать структуру рынка. Этот обход позволил получить измеримое структурное смещение, которое затем было преобразовано в четкие торговые разрешения, в то время как специальный слой визуализации отображал узлы, ребра и информацию о смещении непосредственно на графике для обеспечения прозрачности и проверки.
В заключение, интеграция BFS в торговлю предоставляет трейдерам систематический и объективный способ интерпретации структуры рынка, заменяя субъективный анализ свингов математически обоснованной структурой. Последовательная обработка ценового движения через структурные уровни позволяет трейдерам получить более четкое представление о силе тренда, направленном смещении и допустимых торговых условиях. В целом, такой подход поможет трейдерам торговать структурированно, последовательно и ясно, согласовывая исполнение сделок с истинным иерархическим потоком рынка, а не с отдельными ценовыми движениями.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20856
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Встраивание торговой дисциплины в код (Часть 2): Разработка механизма принудительного соблюдения дневного лимита сделок на уровне всего счета в MQL5
Нейросети в трейдинге: Когнитивная инерция в анализе финансовых рынков (модуль прогнозирования)
Преодоление проблем доступности в торговых инструментах на MQL5 (Часть IV): Удаленная голосовая торговля
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования