English
preview
MQL5 Bootstrap (IV): Вспомогательные средства для трейлинг-стопов и переноса стоп-лосса в безубыток

MQL5 Bootstrap (IV): Вспомогательные средства для трейлинг-стопов и переноса стоп-лосса в безубыток

MetaTrader 5 — Трейдинг |
50 0
Omega J Msigwa
Omega J Msigwa

Содержание


Введение

В предыдущей статье мы добавили в наш проект Bootstrap упрощенные функции для работы с новостями. В этой статье мы создадим типовые вспомогательные средства для трейлинг-стопов и переноса стоп-лосса в безубыток.

При разработке советников или вспомогательных средств управления торговлей разработчики сталкиваются с одними и теми же задачами: как безопасно перемещать стоп-лоссы, соблюдая ограничения брокера на стоп-уровни; как избежать переноса в обратном направлении при изменении значений индикаторов; как правильно преобразовывать целевые денежные величины в ценовые расстояния; как единообразно применять логику к позициям BUY и SELL, разным символам и магическим номерам. Эти повторяющиеся задачи часто приводят к дублированию кода, трудноуловимым ошибкам и потере времени. В этой статье представлен набор переиспользуемых, единообразно спроектированных компонентов для трейлинг-стопов и переноса стоп-лосса в безубыток, которые можно подключить к любому проекту на базе Bootstrap. Эти вспомогательные средства включают типовые защитные механизмы (валидацию уровней, защиту от переноса в обратном направлении, фильтрацию по символу и магическому номеру, корректное преобразование денежных значений в ценовые расстояния). Благодаря этому вы можете внедрить правила трейлинга и переноса стоп-лосса в безубыток в OnTick с минимальными усилиями и доработками.

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

По определению:

Трейлинг-стоп — модификация обычного ордера стоп-лосс, при которой цена срабатывания стоп-лосса автоматически корректируется по мере движения рыночной цены в прибыльном направлении.

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


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

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


Что такое трейлинг-стоп

Чтобы понять, что такое трейлинг-стоп, сначала необходимо разобраться в двух важных понятиях:

01 — Величина трейлинг-стопа

Это расстояние, которое вы хотите поддерживать между текущей рыночной ценой и уровнем стоп-лосса.

Например, предположим, что вы открываете позицию на покупку по EURUSD по цене 1,1000 с трейлинг-стопом в 50 пунктов:

  • Когда цена поднимется до 1,1050, стоп-лосс переместится на уровень 1,1000, сохраняя расстояние в 50 пунктов.
  • А когда цена поднимется до 1,1075, стоп-лосс переместится на уровень 1,1025, снова оставаясь на 50 пунктов ниже текущей цены.

Величина трейлинг-стопа определяет, на каком расстоянии от рыночной цены будет следовать ваш стоп-лосс.

02 — Величина шага трейлинга

Величина шага трейлинга определяет минимальное изменение цены, необходимое для повторного обновления стоп-лосса.

Например:

  • Трейлинг-стоп = 50 пунктов
  • Шаг трейлинга = 10 пунктов

По мере изменения цены:

Текущая цена Стоп-лосс Действие
1,1050 1,1000 Начальный трейлинг.
1,1055 1,1000 Без изменений (цена изменилась всего на 5 пунктов).
1,1060 1,1010 Стоп-лосс обновлен (цена изменилась на 10 пунктов).
1,1065 1,1010 Без изменений (цена изменилась на 5 пунктов).
1,1070 1,1020 Стоп-лосс обновлен (цена выросла на 10 пунктов (шаг); расстояние до стоп-лосса сохранено — 50 пунктов).

Проще говоря:

Трейлинг-стоп определяет, на каком расстоянии от текущей цены должен оставаться стоп-лосс.

А шаг трейлинга определяет, насколько должна измениться цена, прежде чем стоп-лосс будет снова перенесен.


Трейлинг-стоп с фиксированными значениями

Наиболее распространенный вариант трейлинг-стопа использует фиксированные расстояния, которые обычно задаются во входных параметрах советника.

Такой подход позволяет трейдерам задать расстояние трейлинга и величину шага, которые программа использует при корректировке стоп-лосса.

input uint TRAILING_STEP = 200;
input uint TRAILING_STOP = 20;

Ниже приведена вспомогательная функция для решения этой задачи.

//+------------------------------------------------------------------+
//| Trail open positions using a fixed-point trailing stop.          |
//|                                                                  |
//| Once a position has moved in profit by at least `trail_points`,  |
//| the stop loss is automatically adjusted to remain                |
//| `step_points` behind the current market price. The stop loss is  |
//| updated only when the new level differs from the current one and |
//| satisfies the broker's stop level requirements.                  |
//|                                                                  |
//| Parameters:                                                      |
//|   symbol       - Trading symbol whose positions will be managed. |
//|   trail_points - Minimum profit (in points) required before      |
//|                  trailing begins.                                |
//|   step_points  - Distance (in points) maintained between the     |
//|                  current market price and the stop loss.         |
//|   magic        - Magic number used to filter positions. Specify  |
//|                  -1 to trail all positions on the symbol.        |
//|                                                                  |
//+------------------------------------------------------------------+
void TrailingFixedPoints::TrailStops(string symbol, double trail_points, double step_points, long magic = -1)
  {
   double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
   CPositionInfo pos;

   CTrade trade;

//---

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if(!pos.SelectByIndex(i))
         continue;

      if(pos.Symbol() != symbol)
         continue;

      long pos_magic = pos.Magic();
      if(magic != -1)
         if(pos_magic != magic)
            continue;

      double sl = pos.StopLoss(), tp = pos.TakeProfit(), open_price = pos.PriceOpen();

      ulong ticket = pos.Ticket();
      ENUM_POSITION_TYPE type = pos.PositionType();

      switch(type)
        {
         case  POSITION_TYPE_BUY:
           {
            //--- Ensure the position has moved enough
            double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
            if(bid - open_price < trail_points * point)
               continue;

            double new_sl = bid - step_points * point;

            if(!isPositionModificationSameLevels(ticket, new_sl, tp))
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_BUY, new_sl, tp, symbol))
               continue;

            trade.PositionModify(ticket, new_sl, tp);
           }
         break;
         case  POSITION_TYPE_SELL:
           {
            double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);

            //--- Ensure the position has moved enough
            if(open_price - ask < trail_points * point)
               continue;

            double new_sl = ask + step_points * point;

            if(!isPositionModificationSameLevels(ticket, new_sl, tp))
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_SELL, new_sl, tp, symbol))
               continue;

            trade.PositionModify(ticket, new_sl, tp);
           }
         break;
         default:
            break;
        }
     }
  }

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

Для позиции на покупку:

if(bid - open_price < trail_points * point)
  continue;

Для позиции на продажу:

if(open_price - ask < trail_points * point)
  continue;

После выполнения условия активации стоп-лосс пересчитывается с использованием значения step_points. Для позиций BUY стоп-лосс устанавливается ниже текущей цены Bid, а для позиций SELL — выше текущей цены Ask.

Значение step_points определяет расстояние между текущей рыночной ценой и новым уровнем стоп-лосса, позволяя ему следовать за рынком с заранее заданным безопасным расстоянием.

Пример использования:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

   if(!m_symbol.RefreshRates())
      return;

//---

   double lots = m_symbol.LotsMin();
   double ask = m_symbol.Ask(), bid = m_symbol.Bid();
   double pts = m_symbol.Point();

//---

   if(!PositionExistsByType(POSITION_TYPE_BUY))
      m_trade.Buy(lots, sym, ask, ask - STOPLOSS * pts);

   CTrailingFixedPoints::TrailStops(sym, TRAILING_STOP, TRAILING_STEP, MAGIC_NUMBER);
  }

Вывод:

trailing stop fixed points


Трейлинг-стоп на основе скользящей средней

Один из самых простых и эффективных способов динамической корректировки стоп-лосса — использование индикатора скользящей средней в качестве ориентира для определения новых уровней стоп-лосса.

Скользящая средняя отражает постоянно обновляемый ценовой уровень, который следует за рыночным трендом. Если использовать скользящую среднюю как ориентир для трейлинг-стопа, стоп-лосс может автоматически смещаться в направлении тренда, оставляя позицию открытой, пока рынок остается благоприятным.

//+------------------------------------------------------------------+
//| Trail open positions using a Moving Average.                     |
//|                                                                  |
//| The stop loss is adjusted to the current value of a Moving       |
//| Average indicator.                                               |
//|                                                                  |
//| Parameters:                                                      |
//|   handle - Handle of the Moving Average indicator created using  |
//|            iMA() or IndicatorCreate().                           |
//|   symbol - Trading symbol whose positions will be managed.       |
//|   magic  - Magic number used to filter positions. Specify -1 to  |
//|            trail all positions on the specified symbol.          |
//|                                                                  |
//+------------------------------------------------------------------+
void CTrailingMA::TrailStops(int handle, string symbol, long magic = -1)
  {
   double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
   CPositionInfo pos;

   CTrade trade;

//---

   double ma[];
   if(!CopyBuffer(handle, 0, 0, 1, ma))
      return;

   double ma_value = ma[0];

//---

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if(!pos.SelectByIndex(i))
         continue;

      if(pos.Symbol() != symbol)
         continue;

      double sl = pos.StopLoss(),
             open_price = pos.PriceOpen();

      ulong ticket = pos.Ticket();
      ENUM_POSITION_TYPE type = pos.PositionType();
      double tp = pos.TakeProfit();

      //---

      switch(type)
        {
         case  POSITION_TYPE_BUY:
           {
            if(!isPositionModificationSameLevels(ticket, ma_value, tp))
               continue;

            //--- Modify only when the moving average is above the current sl
            if(sl != 0 && ma_value <= sl)
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_BUY, ma_value, tp, symbol))
               continue;

            trade.PositionModify(ticket, ma_value, tp);
           }
         break;
         case  POSITION_TYPE_SELL:
           {
            if(!isPositionModificationSameLevels(ticket, ma_value, tp))
               continue;

            //--- Modify only when the moving average is below the current sl
            if(sl != 0 && ma_value >= sl)
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_SELL, ma_value, tp, symbol))
               continue;

            trade.PositionModify(ticket, ma_value, tp);
           }
         break;
         default:
            break;
        }
     }
  }

Чтобы пользователи могли выбрать подходящий вариант индикатора, указанная выше статическая функция принимает хэндл индикатора скользящей средней.

Поскольку скользящая средняя может изменить направление на противоположное, нужно предотвращать перенос стоп-лосса в направлении, не соответствующем типу позиции. Например, при таких модификациях расстояние до стоп-лосса позиции на покупку увеличивается, поскольку он перемещается вслед за снижающейся скользящей средней.

Для позиции на покупку:

//--- Modify only when the moving average is above the current sl
if(sl != 0 && ma_value <= sl)
  continue;

Для позиции на продажу:

//--- Modify only when the moving average is below the current sl
if(sl != 0 && ma_value >= sl)
  continue;

Приведенные выше операторы if (условия) предотвращают то, что я называю "обратной модификацией".

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

class CTrailingMA
  {
protected:
   int               m_handle;
   long              m_magic;
   string            m_symbol;

public:
                     CTrailingMA(const int handle, const string symbol, const long magic=-1);
                    ~CTrailingMA(void);

   void              TrailStops();
   static void       TrailStops(int handle, string symbol, long magic = -1);
  };

Пример использования:

#include <Bootstrap\Trailing\TrailingMA.mqh>
#include <Bootstrap\positions.mqh>
#include <Trade\SymbolInfo.mqh>

CTrade m_trade;
CSymbolInfo m_symbol;

#define sym Symbol()
#define MAGIC_NUMBER 112233
int ma_handle;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   m_symbol.Name(sym);
   m_trade.SetExpertMagicNumber(MAGIC_NUMBER);
   m_trade.SetTypeFillingBySymbol(sym);
   m_trade.SetDeviationInPoints(100);

//---

   ma_handle = iMA(sym, PERIOD_CURRENT, 20, 0, MODE_SMA, PRICE_CLOSE);
   if(ma_handle == INVALID_HANDLE)
     {
      printf("Invalid MA handle. Error = %d", GetLastError());
      return INIT_FAILED;
     }

   ChartIndicatorAdd(0, 0, ma_handle); //Attach the indicator to the chart

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

   if(!m_symbol.RefreshRates())
      return;

//---

   double lots = m_symbol.LotsMin();
   double ask = m_symbol.Ask(), bid = m_symbol.Bid();


   if(!PositionExistsByType(POSITION_TYPE_BUY))
      m_trade.Buy(lots, sym, ask);

   CTrailingMA::TrailStops(ma_handle, sym, MAGIC_NUMBER); //Trailing stop based on the moving average
  }

Результаты:

trailing stop moving average


Трейлинг-стоп на основе индикатора Average True Range (ATR)

Индикатор ATR широко используется для определения уровней стоп-лосса и тейк-профита. Поскольку этот индикатор отражает текущую волатильность рынка, он подходит для автоматической корректировки стоп-лосса.

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

//+------------------------------------------------------------------+
//| Trail open positions using the Average True Range (ATR).         |
//|                                                                  |
//| Trailing is based on the current market volatility as measured   |
//| by the Average True Range (ATR). 
//|                                                                  |
//| Parameters:                                                      |
//|   handle              - Handle of the ATR indicator created      |
//|                         using iATR() or IndicatorCreate().       |
//|   symbol              - Trading symbol whose positions will be   |
//|                         managed.                                 |
//|   stop_atr_multiplier - ATR multiple required before trailing    |
//|                         is activated.                            |
//|   step_atr_multiplier - ATR multiple maintained between the      |
//|                         current market price and the stop loss.  |
//|   magic               - Magic number used to filter positions.   |
//|                         Specify -1 to trail all positions on     |
//|                         the symbol.                              |
//|                                                                  |
//+------------------------------------------------------------------+
void CTrailingATR::TrailStops(int handle,
                              string symbol,
                              double stop_atr_multiplier = 0.5,
                              double step_atr_multiplier = 0.2,
                              long magic = -1)
  {
   CPositionInfo pos;
   CTrade trade;

//---

   double atr_buff[];
   if(!CopyBuffer(handle, 0, 0, 1, atr_buff))
      return;

   double atr_value = atr_buff[0];

//---

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if(!pos.SelectByIndex(i))
         continue;

      if(pos.Symbol() != symbol)
         continue;

      long pos_magic = pos.Magic();
      if(magic != -1)
         if(pos_magic != magic)
            continue;

      double sl = pos.StopLoss(), tp = pos.TakeProfit(), open_price = pos.PriceOpen();
      double sl_gap = fabs(open_price - sl);

      //---

      ulong ticket = pos.Ticket();
      ENUM_POSITION_TYPE type = pos.PositionType();

      switch(type)
        {
         case  POSITION_TYPE_BUY:
           {
            //--- Ensure the position has moved enough
            double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
            if(bid - open_price < atr_value * stop_atr_multiplier)
               continue;

            double new_sl = bid - atr_value * step_atr_multiplier;

            if(sl != 0 && new_sl <= sl)
               continue;

            if(!isPositionModificationSameLevels(ticket, new_sl, tp))
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_BUY, new_sl, tp, symbol))
               continue;

            trade.PositionModify(ticket, new_sl, tp);
           }
         break;
         case  POSITION_TYPE_SELL:
           {
            double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);

            //--- Ensure the positoin has moved enough
            if(open_price - ask < atr_value * stop_atr_multiplier)
               continue;

            double new_sl = ask + atr_value * step_atr_multiplier;

            if(sl != 0 && new_sl >= sl)
               continue;

            if(!isPositionModificationSameLevels(ticket, new_sl, tp))
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_SELL, new_sl, tp, symbol))
               continue;

            trade.PositionModify(ticket, new_sl, tp);
           }
         break;
         default:
            break;
        }
     }
  }

Как и рассмотренный ранее трейлинг-стоп на основе скользящей средней, трейлинг-стоп на основе ATR должен предотвращать перенос стоп-лосса в обратном направлении.

Поскольку значение ATR изменяется вместе с волатильностью рынка, рассчитанное расстояние до стоп-лосса также может увеличиваться или уменьшаться. При росте волатильности повторный расчет ATR может дать уровень стоп-лосса, более удаленный от текущей цены. Такая модификация ослабила бы защиту стоп-лоссом и увеличила бы риск по позиции.

Чтобы этого избежать, стоп-лосс корректируется только тогда, когда новый уровень усиливает текущую защиту.

Для позиции на покупку:

if(sl != 0 && new_sl <= sl)
  continue;

Для позиции на продажу:

if(sl != 0 && new_sl >= sl)
  continue;

Пример использования:

#include <Bootstrap\Trailing\TrailingATR.mqh>
#include <Bootstrap\positions.mqh>
#include <Trade\SymbolInfo.mqh>

CTrade m_trade;
CSymbolInfo m_symbol;

#define sym Symbol()
#define MAGIC_NUMBER 112233
int atr_handle;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   m_symbol.Name(sym);
   m_trade.SetExpertMagicNumber(MAGIC_NUMBER);
   m_trade.SetTypeFillingBySymbol(sym);
   m_trade.SetDeviationInPoints(100);
   
//---

   atr_handle = iATR(sym, PERIOD_CURRENT, 13);

   if(atr_handle == INVALID_HANDLE)
     {
      printf("Invalid ATR handle. Error = %d", GetLastError());
      return INIT_FAILED;
     }

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

   if(!m_symbol.RefreshRates())
      return;

//---

   double lots = m_symbol.LotsMin();
   double ask = m_symbol.Ask(), bid = m_symbol.Bid();
   double pts = m_symbol.Point();

//---

   if(!PositionExistsByType(POSITION_TYPE_BUY))
      m_trade.Buy(lots, sym, ask, ask - STOPLOSS * pts);

   CTrailingATR::TrailStops(atr_handle, sym, 1, 0.4);
  }

Результаты:

trailing stop atr indicator


Трейлинг-стоп на основе индикатора Parabolic SAR

Parabolic SAR (Stop and Reverse) — вполне эффективный трендовый индикатор. Его можно использовать для корректировки трейлинг-стопа с тем же подходом, который мы применяли к индикатору скользящей средней.

//+------------------------------------------------------------------+
//| Trail open positions using the Parabolic SAR indicator.          |
//|                                                                  |
//| The stop loss is adjusted to the latest value of the Parabolic   |
//| SAR (Stop and Reverse) indicator.				     |
//|                                                                  |
//| Parameters:                                                      |
//|   handle - Handle of the Parabolic SAR indicator created using   |
//|            iSAR() or IndicatorCreate().                          |
//|   symbol - Trading symbol whose positions will be managed.       |
//|   magic  - Magic number used to filter positions. Specify -1 to  |
//|            trail all positions on the specified symbol.          |
//|                                                                  |
//+------------------------------------------------------------------+
void CTrailingSAR::TrailStops(int handle, string symbol, long magic = -1)
  {
   double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
   CPositionInfo pos;

   CTrade trade;

//---

   double sar_buff[];
   if(!CopyBuffer(handle, 0, 0, 1, sar_buff))
      return;

   double sar_value = sar_buff[0];

//---

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if(!pos.SelectByIndex(i))
         continue;

      if(pos.Symbol() != symbol)
         continue;

      double sl = pos.StopLoss(), tp = pos.TakeProfit(), open_price = pos.PriceOpen();

      ulong ticket = pos.Ticket();
      ENUM_POSITION_TYPE type = pos.PositionType();

      //---

      switch(type)
        {
         case  POSITION_TYPE_BUY:
           {
            if(!isPositionModificationSameLevels(ticket, sar_value, tp))
               continue;

            //--- Modify only when the SAR is above the current sl
            if(sl != 0 && sar_value <= sl)
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_BUY, sar_value, tp, symbol))
               continue;

            trade.PositionModify(ticket, sar_value, tp);
           }
         break;
         case  POSITION_TYPE_SELL:
           {
            if(!isPositionModificationSameLevels(ticket, sar_value, tp))
               continue;

            //--- Modify only when the SAR is below the current sl
            if(sl != 0 && sar_value >= sl)
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_SELL, sar_value, tp, symbol))
               continue;

            trade.PositionModify(ticket, sar_value, tp);
           }
         break;
         default:
            break;
        }
     }
  }

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

Для позиции SELL значение SAR остается выше текущей цены и смещается вниз по мере дальнейшего снижения рынка.

Пример использования:

#include <Bootstrap\Trailing\TrailingSAR.mqh>
#include <Bootstrap\positions.mqh>
#include <Trade\SymbolInfo.mqh>

CTrade m_trade;
CSymbolInfo m_symbol;

#define sym Symbol()
#define MAGIC_NUMBER 112233
int sar_handle;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   m_symbol.Name(sym);
   m_trade.SetExpertMagicNumber(MAGIC_NUMBER);
   m_trade.SetTypeFillingBySymbol(sym);
   m_trade.SetDeviationInPoints(100);

//---

   sar_handle = iSAR(sym, PERIOD_CURRENT, 0.02, 0.2);
   if(sar_handle == INVALID_HANDLE)
     {
      printf("Invalid SAR handle. Error = %d", GetLastError());
      return INIT_FAILED;
     }

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

   if(!m_symbol.RefreshRates())
      return;

//---

   double lots = m_symbol.LotsMin();
   double ask = m_symbol.Ask(), bid = m_symbol.Bid();
   double pts = m_symbol.Point();

//---

   if(!PositionExistsByType(POSITION_TYPE_BUY))
      m_trade.Buy(lots, sym, ask, ask - STOPLOSS * pts);

   CTrailingSAR::TrailStops(sar_handle, sym, MAGIC_NUMBER);
 
  }

Результаты:

trailing stop parabolic SAR


Трейлинг-стоп в денежном выражении

Хотя этот метод встречается реже, чем рассмотренные ранее, некоторые трейдеры предпочитают управлять трейлинг-стопами, ориентируясь на фактический денежный результат позиции. Таким образом, вместо корректировки стоп-лосса по пипсам, пунктам или значениям индикаторов этот подход использует суммы в валюте счета.

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

//+------------------------------------------------------------------+
//| Trail open positions based on monetary profit.                   |
//|                                                                  |
//| This trailing stop method adjusts the stop loss based on the     |
//| actual monetary value of the position rather than a fixed price  |
//| distance, indicator value, or number of points. Once a position  |
//| reaches the specified profit threshold, the stop loss is moved   |
//| to maintain a defined monetary distance from the current market  |
//| price.                                                           |
//|                                                                  |
//| Parameters:                                                      |
//|   symbol           - Trading symbol whose positions will be      |
//|                      managed.                                    |
//|   activation_money - Minimum profit in account currency required |
//|                      before the trailing stop is activated.      |
//|   trail_money      - Amount of profit in account currency to     |
//|                      maintain as the trailing distance.          |
//|   magic            - Magic number used to filter positions.      |
//|                      Specify -1 to trail all positions on the    |
//|                      specified symbol.                           |
//|                                                                  |
//+------------------------------------------------------------------+
void CTrailingMoney::TrailStops(string symbol,
                                double activation_money,
                                double trail_money,
                                long magic = -1)
  {
   CPositionInfo pos;
   CTrade trade;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if(!pos.SelectByIndex(i))
         continue;

      if(pos.Symbol() != symbol)
         continue;

      if(magic != -1 && pos.Magic() != magic)
         continue;

      double profit = pos.Profit();

      // Wait until the desired profit is reached
      if(profit < activation_money)
         continue;

      double volume     = pos.Volume();
      double tick_size  = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
      double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);

      if(tick_size <= 0 || tick_value <= 0)
         continue;

      // Convert money into a price distance
      double distance = trail_money * tick_size / (tick_value * volume);

      double tp = pos.TakeProfit();
      double sl = pos.StopLoss();
      ulong ticket = pos.Ticket();

      int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);

      switch(pos.PositionType())
        {
         case POSITION_TYPE_BUY:
           {
            double bid = SymbolInfoDouble(symbol, SYMBOL_BID);

            double new_sl = NormalizeDouble(bid - distance, digits);

            if(sl != 0 && new_sl <= sl)
               continue;

            if(!isPositionModificationSameLevels(ticket, new_sl, tp))
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_BUY, new_sl, tp, symbol))
               continue;

            if(!trade.PositionModify(ticket, new_sl, tp))
               Print(trade.ResultRetcodeDescription());
           }
         break;

         case POSITION_TYPE_SELL:
           {
            double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
            double new_sl = NormalizeDouble(ask + distance, digits);

            if(sl != 0 && new_sl >= sl)
               continue;

            if(!isPositionModificationSameLevels(ticket, new_sl, tp))
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_SELL, new_sl, tp, symbol))
               continue;

            if(!trade.PositionModify(ticket, new_sl, tp))
               Print(trade.ResultRetcodeDescription());
           }
         break;
        }
     }
  }

Например, зададим порог активации трейлинга 5 USD, а денежный шаг трейлинга — 1 USD:

#include <Bootstrap\Trailing\TrailingMoney.mqh>
#include <Bootstrap\positions.mqh>
#include <Trade\SymbolInfo.mqh>

CTrade m_trade;
CSymbolInfo m_symbol;

#define sym Symbol()
#define MAGIC_NUMBER 112233
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   m_symbol.Name(sym);
   m_trade.SetExpertMagicNumber(MAGIC_NUMBER);
   m_trade.SetTypeFillingBySymbol(sym);
   m_trade.SetDeviationInPoints(100);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

   if(!m_symbol.RefreshRates())
      return;

//---

   double lots = m_symbol.LotsMin();
   double ask = m_symbol.Ask(), bid = m_symbol.Bid();
   double pts = m_symbol.Point();

//---

   if(!PositionExistsByType(POSITION_TYPE_BUY))
      m_trade.Buy(lots, sym, ask, ask - STOPLOSS * pts);

   CTrailingMoney::TrailStops(sym, 5, 1, MAGIC_NUMBER);
  }

Результаты:

trailing stop based on money



Периодический трейлинг-стоп

В отличие от рассмотренных выше методов, периодический трейлинг-стоп работает на основе времени. Вместо корректировки стоп-лосса по движению рынка этот метод со временем сокращает расстояние до стоп-лосса.

//+------------------------------------------------------------------+
//| Trail open positions at fixed time intervals.                    |
//|                                                                  |
//| This trailing stop method updates the stop loss only after a     |
//| specified amount of time has elapsed since the last position     |
//| modification. 
//|                                                                  |
//| Parameters:                                                      |
//|   interval_seconds - Minimum time in seconds required between    |
//|                      consecutive stop loss modifications.        |
//|   trail_step       - Number of points by which the stop loss is  |
//|                      moved on each update.                       |
//|   symbol           - Trading symbol whose positions will be      |
//|                      managed.                                    |
//|   magic            - Magic number used to filter positions.      |
//|                      Specify -1 to trail all positions on the    |
//|                      specified symbol.                           |
//|                                                                  |
//+------------------------------------------------------------------+
void CTrailingPeriodic::TrailStops(ulong interval_seconds, double trail_step, string symbol, long magic = -1)
  {
   CPositionInfo pos;
   CTrade trade;

   double point = SymbolInfoDouble(symbol, SYMBOL_POINT);

//---

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if(!pos.SelectByIndex(i))
         continue;

      if(pos.Symbol() != symbol)
         continue;

      long pos_magic = pos.Magic();
      if(magic != -1)
         if(pos_magic != magic)
            continue;

      double sl = pos.StopLoss(), tp = pos.TakeProfit(), open_price = pos.PriceOpen();
      double sl_gap = fabs(open_price - sl);

      //---

      ulong ticket = pos.Ticket();
      ENUM_POSITION_TYPE type = pos.PositionType();
      ulong open_time = (ulong)pos.Time();
      ulong update_time = pos.TimeUpdate();

      //--- modify after a specified number of seconds has passed
      ulong time_diff = (long)TimeCurrent() - update_time;
      
      if(time_diff < interval_seconds)
         continue;

      switch(type)
        {
         case  POSITION_TYPE_BUY:
           {
            double new_sl = sl + trail_step * point; //increase the stoploss by a given number of points

            if(sl != 0 && new_sl <= sl)
               continue;

            if(!isPositionModificationSameLevels(ticket, new_sl, tp))
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_BUY, new_sl, tp, symbol))
               continue;

            trade.PositionModify(ticket, new_sl, tp);
           }
         break;
         case  POSITION_TYPE_SELL:
           {
            double new_sl = sl - trail_step * point; //reduce the stoploss by a given number of points

            if(sl != 0 && new_sl >= sl)
               continue;

            if(!isPositionModificationSameLevels(ticket, new_sl, tp))
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_SELL, new_sl, tp, symbol))
               continue;

            trade.PositionModify(ticket, new_sl, tp);
           }
         break;
         default:
            break;
        }
     }
  }

Например, каждый час для позиции BUY вы сокращаете расстояние до стоп-лосса на 100 пунктов.

#include <Bootstrap\Trailing\TrailingPeriodic.mqh>
#include <Bootstrap\positions.mqh>
#include <Trade\SymbolInfo.mqh>

CTrade m_trade;
CSymbolInfo m_symbol;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   m_symbol.Name(sym);
   m_trade.SetExpertMagicNumber(MAGIC_NUMBER);
   m_trade.SetTypeFillingBySymbol(sym);
   m_trade.SetDeviationInPoints(100);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

   if(!m_symbol.RefreshRates())
      return;

//---

   double lots = m_symbol.LotsMin();
   double ask = m_symbol.Ask(), bid = m_symbol.Bid();
   double pts = m_symbol.Point();

//---

   if(!PositionExistsByType(POSITION_TYPE_BUY))
      m_trade.Buy(lots, sym, ask, ask - STOPLOSS * pts);

   CTrailingPeriodic::TrailStops(3600, 100, sym, MAGIC_NUMBER);
  }

Результаты:

periodic trailing stop


Что такое перенос стоп-лосса в безубыток

В отличие от трейлинг-стопа, который постоянно корректирует стоп-лосс по мере движения рынка в вашу пользу, перенос стоп-лосса в безубыток выполняется однократно. Как только прибыль по позиции достигает заданного уровня, стоп-лосс переносится на цену открытия сделки или немного за нее с использованием необязательного смещения.

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

Предположим, вы открываете позицию BUY по цене 1,10000 с начальным стоп-лоссом на уровне 1,09500.

Если прибыль по сделке составит 300 пунктов, ваш советник сможет перенести стоп-лосс на уровень 1,10000, обеспечив закрытие сделки без убытка при любом последующем развороте рынка.

В качестве альтернативы вы можете перенести стоп-лосс на уровень 1,10020, зафиксировав небольшую прибыль и оставив сделке пространство для дальнейшего развития.

В проекте Bootstrap предусмотрены методы, аналогичные методам трейлинг-стопа; рассмотрим их ниже.


Перенос стоп-лосса в безубыток по фиксированному числу пунктов

Метод переноса стоп-лосса в безубыток по фиксированному числу пунктов использует два основных параметра, определяющих, когда и куда следует перенести стоп-лосс.

  • activation_points — минимальная прибыль в пунктах, необходимая для переноса стоп-лосса в безубыток. Как только нереализованная прибыль достигает этого порога, стоп-лосс переносится с исходного уровня на уровень безубытка.
  • offset_points — количество пунктов, добавляемых к цене открытия или вычитаемых из нее при установке нового стоп-лосса. Это позволяет трейдеру зафиксировать небольшую прибыль вместо того, чтобы переносить стоп-лосс точно на цену входа.

//+------------------------------------------------------------------+
//| Adjusts the stoploss to position's opening price + offset_points |
//|                                                                  |
//| Parameters:                                                      |
//|   symbol       - Trading symbol whose positions will be managed. |
//|   activation_points - Minimum profit (in points) required before |
//|                  break-even begins.                              |
//|   offset_points  - Distance (in points) to add to the new        |
//|                  stoploss.                                       |
//|   magic        - Magic number used to filter positions. Specify  |
//|                  -1 to trail all positions on the symbol.        |
//|                                                                  |
//+------------------------------------------------------------------+
void CBreakEvenFixedPoints::BreakEven(double activation_points, double offset_points, string symbol, long magic = -1)
  {
   double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
   CPositionInfo pos;

   CTrade trade;

//---

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if(!pos.SelectByIndex(i))
         continue;

      if(pos.Symbol() != symbol)
         continue;

      long pos_magic = pos.Magic();
      if(magic != -1)
         if(pos_magic != magic)
            continue;

      double sl = pos.StopLoss(), tp = pos.TakeProfit(), open_price = pos.PriceOpen();

      ulong ticket = pos.Ticket();
      ENUM_POSITION_TYPE type = pos.PositionType();

      switch(type)
        {
         case  POSITION_TYPE_BUY:
           {
            //--- Ensure the position has moved enough
            double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
            if(bid - open_price <= activation_points * point)
               continue;

            double new_sl = open_price + offset_points * point; //breakeven sl

            //--- Prevent unwanted modifications
            if(new_sl <= open_price)
               continue;

            if(!isPositionModificationSameLevels(ticket, new_sl, tp))
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_BUY, new_sl, tp, symbol))
               continue;

            trade.PositionModify(ticket, new_sl, tp);
           }
         break;
         case  POSITION_TYPE_SELL:
           {
            double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);

            //--- Ensure the postion has moved enough
            if(open_price - ask <= activation_points * point)
               continue;

            double new_sl = open_price - offset_points * point;

            //--- Prevent unwanted modifications
            if(new_sl >= open_price)
               continue;

            if(!isPositionModificationSameLevels(ticket, new_sl, tp))
               continue;

            if(!isValidStoploss_Takeprofit(ORDER_TYPE_SELL, new_sl, tp, symbol))
               continue;

            trade.PositionModify(ticket, new_sl, tp);
           }
         break;
         default:
            break;
        }
     }
  }

Например:

#include <Bootstrap\BreakEven\BreakEvenFixedPoints.mqh>
#include <Bootstrap\positions.mqh>
#include <Trade\SymbolInfo.mqh>

CTrade m_trade;
CSymbolInfo m_symbol;

#define sym Symbol()
#define MAGIC_NUMBER 112233
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
input group "BreakEven";
input uint ACTIVATION_POINTS = 200;
input uint OFFSET = 20;
input group "Orders";
input uint STOPLOSS = 500;
input uint TAKEPROFIT = 500;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   m_symbol.Name(sym);
   m_trade.SetExpertMagicNumber(MAGIC_NUMBER);
   m_trade.SetTypeFillingBySymbol(sym);
   m_trade.SetDeviationInPoints(100);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

   if(!m_symbol.RefreshRates())
      return;

//---

   double lots = m_symbol.LotsMin();
   double ask = m_symbol.Ask(), bid = m_symbol.Bid();
   double pts = m_symbol.Point();

//---

   if(!PositionExistsByType(POSITION_TYPE_BUY))
      m_trade.Buy(lots, sym, ask, ask - STOPLOSS * pts, ask + TAKEPROFIT * pts);

   CBreakEvenFixedPoints::BreakEven(ACTIVATION_POINTS, OFFSET, sym, MAGIC_NUMBER);
  }

Результаты:

breakeven fixed points


Перенос стоп-лосса в безубыток с использованием денежных значений

Перенос стоп-лосса в безубыток по денежным значениям работает аналогично переносу по фиксированным значениям в пунктах. Однако вместо прибыли в пунктах или ценового расстояния используется фактическая сумма прибыли в валюте счета.

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

Используются два основных параметра:

  • activation_money — минимальная прибыль в валюте счета, необходимая для переноса стоп-лосса в безубыток. Прибыль по позиции должна достичь этого уровня до любой модификации стоп-лосса.
  • offset_money — сумма прибыли в валюте счета, которую нужно зафиксировать после переноса стоп-лосса в безубыток. При нулевом значении стоп-лосс переносится точно на цену открытия, а при положительном фиксируется дополнительная прибыль.

Например, при пороге активации 10 долларов и смещении 5 долларов.

Когда нереализованная прибыль по позиции достигает 10 долларов, срабатывает логика переноса стоп-лосса в безубыток. Затем стоп-лосс перемещается на уровень, при котором в случае разворота рынка сделка закроется с прибылью примерно 5 долларов, а не станет убыточной.

#include <Bootstrap\BreakEven\BreakEvenMoney.mqh>
#include <Bootstrap\positions.mqh>
#include <Trade\SymbolInfo.mqh>

CTrade m_trade;
CSymbolInfo m_symbol;

#define sym Symbol()
#define MAGIC_NUMBER 112233
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
input group "Orders";
input uint STOPLOSS = 500;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   m_symbol.Name(sym);
   m_trade.SetExpertMagicNumber(MAGIC_NUMBER);
   m_trade.SetTypeFillingBySymbol(sym);
   m_trade.SetDeviationInPoints(100);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

   if(!m_symbol.RefreshRates())
      return;

//---

   double lots = m_symbol.LotsMin();
   double ask = m_symbol.Ask(), bid = m_symbol.Bid();
   double pts = m_symbol.Point();

//---

   if(!PositionExistsByType(POSITION_TYPE_BUY))
      m_trade.Buy(lots, sym, ask, ask - STOPLOSS * pts);

   CBreakEvenMoney::BreakEven(10, 5, sym, MAGIC_NUMBER);
  }

Результаты:

breakeven monetary terms



Заключение

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

Не существует единственного "лучшего" метода трейлинга или переноса стоп-лосса в безубыток. Для стратегии следования за трендом может быть полезен трейлинг-стоп на основе ATR или Parabolic SAR, а для скальпинговой системы — фиксированные значения в пунктах или периодические корректировки. Аналогичным образом трейдер, управляющий рисками в валюте счета, может счесть перенос стоп-лосса в безубыток по денежным значениям более интуитивно понятным, чем расчеты в пунктах.

Повторим: цель MQL5 Bootstrap — не предложить универсальное торговое решение, а упростить разработку с помощью надежных базовых компонентов, которые разработчики могут комбинировать и расширять. Выделение логики управления сделками в переиспользуемые классы упрощает сопровождение, тестирование и доработку советников.

Обнаружили проблему или хотите улучшить проект? Ознакомьтесь с этим репозиторием GitHub.


Таблица вложений

Имя файла Описание и использование
Include\Bootstrap\BreakEven\BreakEvenFixedPoints.mqh Содержит вспомогательные средства для переноса стоп-лосса в безубыток по фиксированным значениям в пунктах.
Include\Bootstrap\BreakEven\BreakEvenMoney.mqh
Содержит вспомогательные средства для переноса стоп-лосса в безубыток на основе прибыли в денежном выражении.
Include\Bootstrap\Trailing\TrailingATR.mqh
Содержит вспомогательные средства для трейлинг-стопов на основе ATR.
Include\Bootstrap\Trailing\TrailingFixedPoints.mqh
Содержит вспомогательные средства для трейлинг-стопов с фиксированным расстоянием в пунктах.
Include\Bootstrap\Trailing\TrailingMA.mqh
Содержит вспомогательные средства для трейлинг-стопов на основе скользящей средней.
Include\Bootstrap\Trailing\TrailingMoney.mqh  Содержит вспомогательные средства для трейлинг-стопов в денежном выражении.
Include\Bootstrap\Trailing\TrailingPeriodic.mqh  Содержит вспомогательные средства для периодических трейлинг-стопов.
Include\Bootstrap\Trailing\TrailingSAR.mqh  Содержит вспомогательные средства для трейлинг-стопов на основе индикатора Parabolic SAR.
Experts\Bootstrap\BreakEven\BreakEven Test.mq5 Советник для тестирования рассмотренных выше вспомогательных функций переноса стоп-лосса в безубыток.
Experts\Bootstrap\BreakEven\Trailing Stops Test.mq5  Советник для тестирования рассмотренных выше функций трейлинга.


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

Прикрепленные файлы |
MQL5.zip (21.04 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Декомпозиция частичной информации: Когда два индикатора вместе информативнее, чем каждый по отдельности Декомпозиция частичной информации: Когда два индикатора вместе информативнее, чем каждый по отдельности
Мы представляем библиотеку декомпозиции частичной информации для MQL5, которая разделяет информацию двух источников о целевой переменной на четыре атома: два уникальных (по одному для каждого источника), общий и синергетический. В реализации используются квантильное разбиение на бины, предвычисленные логарифмы и подгонка по принципу максимальной энтропии (для I_ccs). Результаты сопоставляются с нулевой моделью на основе блочных перестановок, поскольку на конечных выборках оценки атомов превышают ноль. Используйте ее для отбора пар индикаторов и проверки статистической значимости, включая поправку на множественные сравнения на уровне семейства тестов.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Рыночная микроструктура в MQL5 (Часть 8): Оценка силы микротренда Рыночная микроструктура в MQL5 (Часть 8): Оценка силы микротренда
В восьмой части добавляется побарная оценка силы микротренда для NQ M1. Функция GetMicroTrendStrength() формирует непрерывную композитную оценку в диапазоне [-1, +1] из согласованности EMA, нормализованного по ATR положения цены, согласованности наклонов и объема. Штраф за противоречие подавляет конфликты между согласованностью EMA и положением цены. Сессионно-адаптивные пороги масштабируются в зависимости от показателя достоверности из седьмой части, регулируя частоту сигналов в разных режимах. Результаты включают метку с семью состояниями, бинарный сигнал и проверку устойчивости, откалиброванные по 514 нью-йоркским сессиям (май 2024 г. - май 2026 г.).