English Deutsch 日本語
preview
Теория графов: Применение алгоритма поиска в ширину (BFS) в торговой системе

Теория графов: Применение алгоритма поиска в ширину (BFS) в торговой системе

MetaTrader 5Примеры |
38 0
Hlomohang John Borotho
Hlomohang John Borotho

Содержание:

  1. Введение
  2. Обзор системы и основы BFS
  3. Приступаем к работе
  4. Результаты тестирования на истории
  5. Заключение 


Введение

Финансовые рынки на первый взгляд часто кажутся хаотичными, однако цены последовательно перемещаются из одной области в другую таким образом, что это предполагает наличие глубинной логики, а не случайности. Поэтому трейдеры и разработчики систем стремятся выявить алгоритм, которому, по-видимому, следует сам рынок при переходе между уровнями ликвидности, структуры и дисбаланса. Эта задача заключается не в прогнозировании в отрыве от контекста, а в понимании того, как прошлые ценовые взаимодействия влияют на будущие движения и как структура рынка развивается шаг за шагом во времени.

Теория графов предлагает мощную основу для приближения к этой цели, позволяя моделировать движение цен как структурированную, основанную на правилах систему, а не как последовательность отдельных свечей. Применяя поиск в ширину (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 года) со следующими настройками:

Input parameters

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

Equity Curve

Back Test


Заключение

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

В заключение, интеграция BFS в торговлю предоставляет трейдерам систематический и объективный способ интерпретации структуры рынка, заменяя субъективный анализ свингов математически обоснованной структурой. Последовательная обработка ценового движения через структурные уровни позволяет трейдерам получить более четкое представление о силе тренда, направленном смещении и допустимых торговых условиях. В целом, такой подход поможет трейдерам торговать структурированно, последовательно и ясно, согласовывая исполнение сделок с истинным иерархическим потоком рынка, а не с отдельными ценовыми движениями. 

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20856

Прикрепленные файлы |
BFS-EA.mq5 (30.2 KB)
Встраивание торговой дисциплины в код (Часть 2): Разработка механизма принудительного соблюдения дневного лимита сделок на уровне всего счета в MQL5 Встраивание торговой дисциплины в код (Часть 2): Разработка механизма принудительного соблюдения дневного лимита сделок на уровне всего счета в MQL5
Мы разработали систему, которая устанавливает ежедневный лимит на количество сделок, чтобы вы соблюдали свои торговые правила. Она отслеживает все совершенные сделки на счете и автоматически вмешивается при достижении установленного лимита, предотвращая дальнейшую активность. Благодаря интеграции контроля непосредственно в платформу, система обеспечивает поддержание дисциплины даже при повышении рыночного давления.
Нейросети в трейдинге: Когнитивная инерция в анализе финансовых рынков (модуль прогнозирования) Нейросети в трейдинге: Когнитивная инерция в анализе финансовых рынков (модуль прогнозирования)
В статье продолжается адаптация фреймворка CogDriver к анализу финансовых рынков. После построения рыночной сцены и временно согласованной памяти переходим к созданию Forecast Head — модуля прогнозирования. Рассматривается механизм рекуррентного уточнения прогноза, сочетание Cross- и Self-Attention, проблема позиционного кодирования и диагностические признаки, помогающие оценивать устойчивость будущей торговой гипотезы.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Преодоление проблем доступности в торговых инструментах на MQL5 (Часть IV): Удаленная голосовая торговля Преодоление проблем доступности в торговых инструментах на MQL5 (Часть IV): Удаленная голосовая торговля
Узнайте о практическом способе выполнения сделок в MetaTrader 5 из голосовых сообщений Telegram с помощью промежуточного ПО на Python и советника MQL5, выступающего в качестве HTTP-клиента. В статье рассматриваются архитектура, опрос WebRequest, очереди в памяти, парсинг JSON с удалением нулевых символов-терминаторов и ограниченная грамматика команд с размером лота по умолчанию 0,001. Вы настроите среду и проверите задержку передачи данных, подходящую для мобильных интернет-соединений.