...

Протокол проектирования эксперта институционального уровня на MQL5: от гипотезы до Governance

Протокол проектирования эксперта институционального уровня на MQL5: от гипотезы до Governance

Автор: Масловский Дмитрий, ООО «ПОРТАЛ ИНВЕСТОРОВ» (Минск), основатель 1A.by. 15+ лет на финансовых рынках, специализация – институциональные алгосистемы на C++, Python, MQL5.

Введение: проблема, которую решает статья

Большинство советников для MetaTrader 5 умирают по одной и той же причине. Их автор открывает MetaEditor, пишет OnTick(), накидывает пару индикаторов, прогоняет на истории за 5 лет, видит красивую восходящую кривую и запускает на реальный счёт. Через месяц – просадка, слиппедж в новостях, смена рыночного режима, и советник отправляется в архив.

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

Эта статья – не про то, «какой индикатор лучше». Она про протокол мышления, который превращает набор правил в институциональную торговую систему. И про то, как этот протокол ложится на архитектуру MQL5.

1. Что такое эксперт на самом деле

Эксперт – это не «стратегия» и не «набор индикаторов». Эксперт – это формализованная гипотеза о неэффективности рынка, обёрнутая в инженерный контур:

Data → State → Alpha → Meta → Risk → Execution → Portfolio → Ops → Governance

Если убрать любой слой, это уже не институциональный эксперт, а retail-советник. MQL5 даёт всё необходимое, чтобы пройти этот путь: ООП, тиковые данные, тестер стратегий, OnTradeTransaction, таймеры, работу с файлами, SQLite, WebRequest и алерты. Но инструменты не заменяют методологию.

2. Четыре доказательства, без которых эксперт не существует

Экономическое. Почему прибыль вообще должна быть? Кто на другой стороне? Почему он системно ошибается? Почему это преимущество не исчезает? Если ответа нет – это не стратегия, а кривая на графике.

Статистическое. Edge подтверждён вне выборки? Учтено множественное тестирование? Издержки, спред, слиппедж, свопы? Разные режимы рынка? Если нет – вы оптимизировали шум.

Инженерное. Сигнал можно исполнить? Хватает ликвидности? Какой спред в новости? Какая задержка? Что будет при проскальзывании? Если edge исчезает при реальном исполнении – это не edge.

Эксплуатационное. Систему можно вести? Есть мониторинг, алерты, восстановление, аудит, kill switch? Если советник упал ночью и никто не узнал – это не система.

Нет любого из четырёх – дальше идти нельзя.

3. Универсальные слои эксперта

Слой Вопрос Что должно быть в MQL5
Data Какие данные? Цена, объём, время, макро, order flow
State Что сейчас на рынке? Режим: тренд, флет, кризис, низкая ликвидность
Alpha Где преимущество? Правило или модель, объясняющая edge
Meta Торговать ли сигнал? Калибровка вероятности, фильтр ложных входов
Risk Сколько терять? Размер позиции, лимиты, portfolio heat, kill switch
Execution Как войти/выйти? Лимит, TWAP/VWAP/POV, контроль спреда и слиппеджа
Portfolio Как сочетать? Корреляции, экспозиция, лимиты на инструмент и сессию
Ops Как жить? VPS, heartbeat, логи, алерты, reconciliation
Governance Кто меняет? Версионирование, approval, аудит, отчётность

4. Ворота G0–G12: протокол создания

Этап Вопрос Артефакт Критерий выхода
G0. Идея Какую неэффективность эксплуатируем? Гипотеза на 1 страницу Есть экономический смысл
G1. Спецификация Что покупаем/продаём? Паспорт эксперта Формализованы входы, выходы, ограничения
G2. Данные Хватает ли данных? Data dictionary Нет look-ahead, дыр, survivorship
G3. Признаки Что видит модель? Feature store Признаки стабильны и версионированы
G4. Alpha Где сигнал? Модель/правила Edge виден на train и вне его
G5. Meta Когда не торговать? Калибратор Порог динамический, не подогнан
G6. Risk Сколько риска? Risk Governor Лимиты, DD-throttle, kill switch
G7. Execution Как исполнять? Execution Engine Учтены спред, слиппедж, latency
G8. Валидация Не самообман? Отчёт CPCV/DSR/MC Устойчивость вне выборки
G9. Paper Работает ли в реальном времени? Paper log Отклонение от бэктеста < 20%
G10. Live pilot Можно ли доверять? Live report Лимиты не нарушены
G11. Scale Можно ли увеличивать? Capacity report Edge не разрушается от объёма
G12. Governance Кто контролирует? Регламент Есть аудит, версии, алерты

5. Паспорт эксперта: шаблон

  1. Название и версия.
  2. Гипотеза: почему edge существует.
  3. Вселенная: инструменты, сессии, таймфреймы.
  4. Данные: источники, частота, задержки.
  5. Режимы: как определяются и как меняют поведение.
  6. Сигнал: вход, подтверждение, отмена.
  7. Meta-фильтр: когда сигнал игнорируется.
  8. Риск: риск на сделку, portfolio heat, дневной и недельный лимит.
  9. Исполнение: типы ордеров, спред, слиппедж, news blackout.
  10. Выходы: TP, SL, trailing, time stop, partial close.
  11. Валидация: методы, метрики, stress-сценарии.
  12. Эксплуатация: VPS, мониторинг, алерты, восстановление.
  13. Аудит: логи, reconciliation, отчётность.
  14. Ограничения: где эксперт не работает.
  15. План вывода из эксплуатации: когда отключаем.

6. Архитектура на MQL5

В MQL5 удобно разделить советник на классы. Каждый слой изолирован: CRiskManager не знает, как считается сигнал. CExecutionEngine не знает, почему позиция открыта. CLogger фиксирует всё.

6.1. Главный файл советника

//+------------------------------------------------------------------+
//|                                              AurumCore1000.mq5   |
//|                        Протокол институционального эксперта      |
//+------------------------------------------------------------------+
#property copyright "Aurum Core 1000"
#property version   "1.00"
#property strict

#include "CDataPlane.mqh"
#include "CRegimeEngine.mqh"
#include "CSignalEngine.mqh"
#include "CMetaFilter.mqh"
#include "CRiskGovernor.mqh"
#include "CExecutionEngine.mqh"
#include "CPortfolioManager.mqh"
#include "CLogger.mqh"

CDataPlane         g_data;
CRegimeEngine      g_regime;
CSignalEngine      g_signal;
CMetaFilter        g_meta;
CRiskGovernor      g_risk;
CExecutionEngine   g_exec;
CPortfolioManager  g_portfolio;
CLogger            g_logger;

input double InpRiskPercent      = 0.5;    // Риск на сделку, %
input double InpMaxPortfolioHeat = 3.0;    // Максимальная совокупная нагрузка, %
input double InpDailyLossLimit   = 1.5;    // Дневной лимит потерь, %
input double InpWeeklyLossLimit  = 4.0;    // Недельный лимит потерь, %
input double InpMaxDDThrottle    = 6.0;    // После этой просадки риск режется вдвое
input double InpKillSwitchDD     = 10.0;   // Аварийное отключение
input int    InpHeartbeatSeconds = 60;     // Период heartbeat

int OnInit()
{
   if(!g_logger.Init("AurumCore1000", true))    return INIT_FAILED;
   if(!g_data.Init(_Symbol, PERIOD_H1))         return INIT_FAILED;
   if(!g_regime.Init())                         return INIT_FAILED;
   if(!g_signal.Init())                         return INIT_FAILED;
   if(!g_meta.Init())                           return INIT_FAILED;
   if(!g_risk.Init(InpRiskPercent,
                   InpMaxPortfolioHeat,
                   InpDailyLossLimit,
                   InpWeeklyLossLimit,
                   InpMaxDDThrottle,
                   InpKillSwitchDD))            return INIT_FAILED;
   if(!g_exec.Init())                           return INIT_FAILED;
   if(!g_portfolio.Init())                      return INIT_FAILED;

   EventSetTimer(InpHeartbeatSeconds);
   g_logger.Info("AurumCore1000 initialized. Protocol G0-G12 online.");
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   EventKillTimer();
   g_logger.Info(StringFormat("Deinit. Reason: %d", reason));
   g_logger.Shutdown();
}

void OnTick()
{
   if(!g_risk.CanTrade())       return;
   if(!g_data.Update())         return;

   g_regime.Update(g_data);
   if(!g_regime.TradeAllowed()) return;

   SSignal signal;
   if(!g_signal.Generate(g_data, g_regime, signal)) return;

   double prob = g_meta.Calibrate(signal, g_data, g_regime);
   if(prob < g_meta.DynamicThreshold(g_data, g_regime)) return;

   double lots = g_risk.CalcSize(prob, g_data, g_regime);
   if(lots <= 0.0) return;

   SExecutionPlan plan;
   if(!g_exec.BuildPlan(lots, signal, g_data, g_regime, plan)) return;

   g_exec.Execute(plan);
   g_logger.Audit(signal, prob, lots, plan);
}

void OnTimer()
{
   g_risk.CheckLimits();
   g_portfolio.Rebalance();
   g_logger.Heartbeat();
}

void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest     &request,
                        const MqlTradeResult      &result)
{
   g_logger.AuditTransaction(trans, request, result);
}

6.2. Data Plane

//+------------------------------------------------------------------+
//|                                                  CDataPlane.mqh  |
//+------------------------------------------------------------------+
class CDataPlane
{
private:
   string          m_symbol;
   ENUM_TIMEFRAMES m_tf;
   int             m_h_adx, m_h_atr, m_h_rsi, m_h_bb;
   datetime        m_last_bar_time;

public:
   bool Init(const string symbol, const ENUM_TIMEFRAMES tf)
   {
      m_symbol = symbol;
      m_tf     = tf;
      m_h_adx  = iADX(symbol, tf, 14);
      m_h_atr  = iATR(symbol, tf, 14);
      m_h_rsi  = iRSI(symbol, tf, 14, PRICE_CLOSE);
      m_h_bb   = iBands(symbol, tf, 20, 0, 2.0, PRICE_CLOSE);
      return (m_h_adx != INVALID_HANDLE && m_h_atr != INVALID_HANDLE);
   }

   bool Update()
   {
      datetime t = iTime(m_symbol, m_tf, 0);
      if(t == m_last_bar_time) return true;
      m_last_bar_time = t;
      return true;
   }

   double GetADX(const int shift = 0)
   {
      double buf[];
      if(CopyBuffer(m_h_adx, 0, shift, 1, buf) != 1) return 0.0;
      return buf[0];
   }

   double GetATR(const int shift = 0)
   {
      double buf[];
      if(CopyBuffer(m_h_atr, 0, shift, 1, buf) != 1) return 0.0;
      return buf[0];
   }

   double GetRealizedVol(const int bars = 20)
   {
      double closes[];
      if(CopyClose(m_symbol, m_tf, 0, bars + 1, closes) != bars + 1) return 0.0;
      double sum = 0.0;
      for(int i = 1; i <= bars; i++)
      {
         double r = MathLog(closes[i-1] / closes[i]);
         sum += r * r;
      }
      return MathSqrt(sum / bars);
   }

   double GetSpread()
   {
      MqlTick tick;
      if(!SymbolInfoTick(m_symbol, tick)) return 0.0;
      return (tick.ask - tick.bid) / SymbolInfoDouble(m_symbol, SYMBOL_POINT);
   }
};

6.3. Regime Engine

//+------------------------------------------------------------------+
//|                                              CRegimeEngine.mqh   |
//+------------------------------------------------------------------+
enum ENUM_REGIME
{
   REGIME_TREND_UP   = 0,
   REGIME_TREND_DOWN = 1,
   REGIME_RANGE      = 2,
   REGIME_CRISIS     = 3,
   REGIME_ILLIQUID   = 4
};

class CRegimeEngine
{
private:
   ENUM_REGIME m_current;
   double      m_confidence;

public:
   bool Init()
   {
      m_current    = REGIME_RANGE;
      m_confidence = 0.0;
      return true;
   }

   void Update(CDataPlane &data)
   {
      double adx    = data.GetADX();
      double rvol   = data.GetRealizedVol(20);
      double spread = data.GetSpread();

      if(rvol > 0.005 || spread > 50.0)
      {
         m_current    = REGIME_CRISIS;
         m_confidence = 0.9;
         return;
      }

      if(spread > 20.0)
      {
         m_current    = REGIME_ILLIQUID;
         m_confidence = 0.7;
         return;
      }

      if(adx > 25.0)
      {
         m_current    = REGIME_TREND_UP;
         m_confidence = MathMin(1.0, adx / 50.0);
         return;
      }

      m_current    = REGIME_RANGE;
      m_confidence = 1.0 - adx / 25.0;
   }

   bool TradeAllowed() const
   {
      if(m_current == REGIME_CRISIS || m_current == REGIME_ILLIQUID)
         return false;
      return true;
   }

   ENUM_REGIME Get() const { return m_current; }
   double GetConfidence() const { return m_confidence; }
};

6.4. Signal Engine

//+------------------------------------------------------------------+
//|                                              CSignalEngine.mqh   |
//+------------------------------------------------------------------+
struct SSignal
{
   int      direction;
   double   raw_score;
   datetime time;
};

class CSignalEngine
{
public:
   bool Init() { return true; }

   bool Generate(CDataPlane &data, CRegimeEngine &regime, SSignal &out)
   {
      out.direction = 0;
      out.raw_score = 0.0;
      out.time      = TimeCurrent();

      double adx = data.GetADX();

      if(regime.Get() == REGIME_TREND_UP && adx > 30.0)
      {
         out.direction = 1;
         out.raw_score = 0.5 + adx / 100.0;
         return true;
      }
      return false;
   }
};

6.5. Meta Filter

//+------------------------------------------------------------------+
//|                                                CMetaFilter.mqh   |
//+------------------------------------------------------------------+
class CMetaFilter
{
private:
   double Sigmoid(double x) { return 1.0 / (1.0 + MathExp(-x)); }

public:
   bool Init() { return true; }

   double Calibrate(const SSignal &s, CDataPlane &data, CRegimeEngine &regime)
   {
      double raw  = s.raw_score;
      double conf = regime.GetConfidence();
      double score = raw * (0.5 + 0.5 * conf);
      return Sigmoid(score * 4.0 - 2.0);
   }

   double DynamicThreshold(CDataPlane &data, CRegimeEngine &regime)
   {
      double base   = 0.65;
      double rvol   = data.GetRealizedVol(20);
      double spread = data.GetSpread();

      if(regime.Get() == REGIME_RANGE) base += 0.10;
      if(rvol > 0.002)                 base += 0.05;
      if(spread > 15.0)                base += 0.10;

      return MathMin(base, 0.95);
   }
};

6.6. Risk Governor

//+------------------------------------------------------------------+
//|                                              CRiskGovernor.mqh   |
//+------------------------------------------------------------------+
class CRiskGovernor
{
private:
   double m_risk_percent;
   double m_max_portfolio_heat;
   double m_daily_loss_limit;
   double m_weekly_loss_limit;
   double m_dd_throttle;
   double m_kill_switch_dd;
   double m_day_start_equity;
   double m_week_start_equity;
   double m_peak_equity;
   bool   m_kill_switch_triggered;

public:
   bool Init(const double risk_pct,
             const double max_heat,
             const double daily_lim,
             const double weekly_lim,
             const double dd_throttle,
             const double kill_dd)
   {
      m_risk_percent        = risk_pct;
      m_max_portfolio_heat  = max_heat;
      m_daily_loss_limit    = daily_lim;
      m_weekly_loss_limit   = weekly_lim;
      m_dd_throttle         = dd_throttle;
      m_kill_switch_dd      = kill_dd;

      m_day_start_equity    = AccountInfoDouble(ACCOUNT_EQUITY);
      m_week_start_equity   = m_day_start_equity;
      m_peak_equity         = m_day_start_equity;
      m_kill_switch_triggered = false;
      return true;
   }

   bool CanTrade()
   {
      if(m_kill_switch_triggered) return false;

      double eq = AccountInfoDouble(ACCOUNT_EQUITY);
      if(eq > m_peak_equity) m_peak_equity = eq;

      double dd = (m_peak_equity - eq) / m_peak_equity * 100.0;
      if(dd >= m_kill_switch_dd) { m_kill_switch_triggered = true; return false; }

      double daily_loss  = (m_day_start_equity - eq) / m_day_start_equity * 100.0;
      if(daily_loss >= m_daily_loss_limit) return false;

      double weekly_loss = (m_week_start_equity - eq) / m_week_start_equity * 100.0;
      if(weekly_loss >= m_weekly_loss_limit) return false;

      return true;
   }

   double CalcSize(const double prob, CDataPlane &data, CRegimeEngine &regime)
   {
      double eq       = AccountInfoDouble(ACCOUNT_EQUITY);
      double atr      = data.GetATR();
      double risk_usd = eq * m_risk_percent / 100.0;

      double dd = (m_peak_equity - eq) / m_peak_equity * 100.0;
      if(dd >= m_dd_throttle) risk_usd *= 0.5;

      double atr_normalized = MathMax(atr, SymbolInfoDouble(_Symbol, SYMBOL_POINT) * 100);
      double tick_value     = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);

      double lots = risk_usd / (atr_normalized * tick_value);
      return NormalizeLot(lots);
   }

   void CheckLimits()
   {
      static int last_day = -1;
      MqlDateTime now;
      TimeToStruct(TimeCurrent(), now);
      if(now.day != last_day)
      {
         m_day_start_equity = AccountInfoDouble(ACCOUNT_EQUITY);
         last_day = now.day;
      }
   }

private:
   double NormalizeLot(double lots)
   {
      double min_lot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
      double max_lot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
      double step_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
      lots = MathFloor(lots / step_lot) * step_lot;
      return MathMax(min_lot, MathMin(max_lot, lots));
   }
};

6.7. Execution Engine

//+------------------------------------------------------------------+
//|                                          CExecutionEngine.mqh    |
//+------------------------------------------------------------------+
struct SExecutionPlan
{
   int    direction;
   double lots;
   double entry_price;
   double sl_price;
   double tp_price;
};

class CExecutionEngine
{
private:
   CTrade m_trade;
   double m_max_spread_points;
   double m_max_slippage_points;

public:
   bool Init()
   {
      m_trade.SetExpertMagicNumber(20250918);
      m_max_spread_points   = 20.0;
      m_max_slippage_points = 10.0;
      return true;
   }

   bool BuildPlan(const double lots,
                  const SSignal &signal,
                  CDataPlane &data,
                  CRegimeEngine &regime,
                  SExecutionPlan &plan)
   {
      if(data.GetSpread() > m_max_spread_points) return false;

      MqlTick tick;
      if(!SymbolInfoTick(_Symbol, tick)) return false;

      double atr = data.GetATR();
      plan.direction   = signal.direction;
      plan.lots        = lots;
      plan.entry_price = (signal.direction > 0) ? tick.ask : tick.bid;
      plan.sl_price    = (signal.direction > 0)
                       ? plan.entry_price - atr * 2.0
                       : plan.entry_price + atr * 2.0;
      plan.tp_price    = (signal.direction > 0)
                       ? plan.entry_price + atr * 4.0
                       : plan.entry_price - atr * 4.0;
      return true;
   }

   bool Execute(const SExecutionPlan &plan)
   {
      m_trade.SetDeviationInPoints((ulong)m_max_slippage_points);
      if(plan.direction > 0)
         return m_trade.Buy(plan.lots, _Symbol, plan.entry_price,
                            plan.sl_price, plan.tp_price, "AurumCore1000");
      return m_trade.Sell(plan.lots, _Symbol, plan.entry_price,
                          plan.sl_price, plan.tp_price, "AurumCore1000");
   }
};

6.8. Portfolio Manager

//+------------------------------------------------------------------+
//|                                          CPortfolioManager.mqh   |
//+------------------------------------------------------------------+
class CPortfolioManager
{
public:
   bool Init() { return true; }

   void Rebalance()
   {
      // Контроль совокупной экспозиции, корреляций, лимитов на сессии
   }

   double GetPortfolioHeat()
   {
      double heat = 0.0;
      for(int i = PositionsTotal() - 1; i >= 0; i--)
      {
         ulong ticket = PositionGetTicket(i);
         if(ticket == 0) continue;
         if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;

         double sl   = PositionGetDouble(POSITION_SL);
         double open = PositionGetDouble(POSITION_PRICE_OPEN);
         double vol  = PositionGetDouble(POSITION_VOLUME);
         if(sl <= 0.0) continue;

         double risk = MathAbs(open - sl) * vol *
                       SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
         heat += risk;
      }
      double eq = AccountInfoDouble(ACCOUNT_EQUITY);
      return (eq > 0.0) ? heat / eq * 100.0 : 0.0;
   }
};

6.9. Logger

//+------------------------------------------------------------------+
//|                                                    CLogger.mqh   |
//+------------------------------------------------------------------+
class CLogger
{
private:
   int    m_file;
   string m_name;
   bool   m_enabled;

public:
   bool Init(const string name, const bool enabled)
   {
      m_name    = name;
      m_enabled = enabled;
      m_file    = INVALID_HANDLE;
      if(!m_enabled) return true;

      string filename = StringFormat("%s_%s.csv", name,
                                     TimeToString(TimeCurrent(), TIME_DATE));
      m_file = FileOpen(filename, FILE_WRITE | FILE_CSV | FILE_ANSI, ';');
      if(m_file == INVALID_HANDLE) return false;

      FileWrite(m_file, "time", "event", "symbol", "direction",
                "prob", "lots", "entry", "sl", "tp", "comment");
      FileFlush(m_file);
      return true;
   }

   void Info(const string msg) { Print("[INFO] ", msg); }

   void Heartbeat()
   {
      if(!m_enabled) return;
      FileWrite(m_file,
                TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS),
                "HEARTBEAT", _Symbol, "", "", "", "", "", "",
                StringFormat("equity=%.2f", AccountInfoDouble(ACCOUNT_EQUITY)));
      FileFlush(m_file);
   }

   void Audit(const SSignal &s, const double prob, const double lots,
              const SExecutionPlan &plan)
   {
      if(!m_enabled) return;
      FileWrite(m_file,
                TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS),
                "SIGNAL", _Symbol, IntegerToString(s.direction),
                DoubleToString(prob, 4), DoubleToString(lots, 2),
                DoubleToString(plan.entry_price, _Digits),
                DoubleToString(plan.sl_price, _Digits),
                DoubleToString(plan.tp_price, _Digits),
                "AurumCore1000");
      FileFlush(m_file);
   }

   void AuditTransaction(const MqlTradeTransaction &trans,
                         const MqlTradeRequest     &request,
                         const MqlTradeResult      &result)
   {
      if(!m_enabled) return;
      FileWrite(m_file,
                TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS),
                "TRADE_TRANS", trans.symbol,
                IntegerToString((int)trans.type), "",
                DoubleToString(trans.volume, 2),
                DoubleToString(trans.price, _Digits),
                "", "",
                StringFormat("retcode=%d", result.retcode));
      FileFlush(m_file);
   }

   void Shutdown()
   {
      if(m_file != INVALID_HANDLE) { FileClose(m_file); m_file = INVALID_HANDLE; }
   }
};

7. Валидация: как не обмануть себя

  • Walk-forward: оптимизация на одном окне, проверка на следующем.
  • Purged K-Fold и Combinatorial Purged CV: исключить утечку данных.
  • Deflated Sharpe Ratio: поправка на множественное тестирование.
  • Monte Carlo: прогон сделок в случайном порядке, проверка просадки.
  • Stress-тесты: 2008, 2013, 2020, 2022, аномальный спред, задержки.
  • Тиковые данные: тест на реальных тиках, а не на барах.
  • Forward test: демо или минимальный реальный счёт без подгонки.
Метрика Порог
Sharpe (после издержек) > 2.5
Sortino > 3.0
Calmar > 2.0
Max DD (базовый риск) < 10%
Profit Factor > 2.0
Recovery Factor > 5
Устойчивость к 2× спреду пройдено
Устойчивость к 2× слиппеджу пройдено
Положительные walk-forward окна > 70%
Deflated Sharpe > 2
Live vs backtest deviation < 20%
Capacity $1–10 млн без разрушения edge

8. Риск-менеджмент: где чаще всего ломаются

Риск на сделку 2%, а тем более 26% – это retail. Институциональный диапазон: 0.25–1.0% на сделку.

  • Volatility targeting: размер позиции обратно пропорционален ATR.
  • Portfolio heat: максимум 3–6% одновременно.
  • Daily loss limit: 1.5%.
  • Weekly loss limit: 4%.
  • Max DD throttle: после 6–8% просадки риск режется вдвое.
  • Kill switch: при 10% DD, аномальном спреде, latency > N мс.
  • VaR/CVaR: оценка хвостового риска.

Всё это уже реализовано в CRiskGovernor выше.

9. Исполнение: то, что убивает edge

  • Лимитные ордера по умолчанию.
  • TWAP/VWAP/POV для крупного объёма.
  • Slippage cap: если цена ушла хуже модели – отмена.
  • Max spread filter.
  • News blackout за 5–15 минут до CPI/FOMC/NFP.
  • Контроль проскальзывания и сверка с бэктестом.

10. Типичные ошибки мышления

  • Начинать с кода, а не с гипотезы.
  • Оптимизировать на истории и называть это edge.
  • Игнорировать издержки, спред, слиппедж.
  • Забывать про смену режимов.
  • Использовать мартингейл, сетки, усреднение без лимитов.
  • Считать доходность без просадки.
  • Путать агрессивный риск с институциональным уровнем.
  • Не иметь kill switch.
  • Не вести аудит.
  • Не планировать вывод из эксплуатации.

11. Жизненный цикл любого эксперта

Идея → Спецификация → Данные → Alpha → Risk → Execution
     → Валидация → Paper → Live pilot → Scale → Monitor → Decay → Retire

Эксперт – это не файл. Это живой контур, который стареет, ломается и требует управления.

Заключение

MQL5 – мощная среда. Но она не спасает от плохой методологии. Если вы пишете советника, проверьте себя по протоколу:

  1. Есть ли экономический смысл?
  2. Формализована ли гипотеза?
  3. Хватает ли данных?
  4. Есть ли regime engine?
  5. Есть ли meta-фильтр?
  6. Есть ли risk governor?
  7. Есть ли execution engine?
  8. Пройдена ли валидация вне выборки?
  9. Есть ли paper trading?
  10. Есть ли kill switch и аудит?

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

Архитектура AurumCore1000 из этой статьи – не готовый продукт, а каркас. Каждый слой можно и нужно развивать: заменять примитивный regime engine на HMM, калибровку на isotonic regression, добавлять order flow, макро-фильтры, portfolio layer для мультиинструментной торговли. Но принцип остаётся неизменным: методология → архитектура → код, а не наоборот.

Об авторе

Масловский Дмитрий – учредитель и директор ООО «ПОРТАЛ ИНВЕСТОРОВ» (Минск, Беларусь, УНП 193847467), основатель инвестиционного портала 1A.by.

Алготрейдер и разработчик с опытом более 15 лет. Автор статей на Investing.com и других международных ресурсах. Специализация: автоматизированные торговые системы институционального уровня на C++, Python, MQL5. Принципиально не использует сетки, мартингейл и розничный индикаторный шум. Работает на основе математического моделирования и анализа межрыночных взаимосвязей.

На MQL5 Market представляет линейку продуктов компании.

Торговля на финансовых рынках сопряжена с высоким риском. Исторические показатели не гарантируют будущих результатов. Данный материал носит методологический характер и не является инвестиционной рекомендацией. Приведённые примеры кода – учебный каркас, а не готовый к эксплуатации продукт.© Масловский Дмитрий, ООО «ПОРТАЛ ИНВЕСТОРОВ», 1A.by. Все права защищены.

Разработка и продвижение сайтов webseed.ru

Регистрация и участие в сообществе инвесторов доступно только для резидентов РБ

Отказ от ответственности, обработка персональных данных, правила пользования сайтом, политика конфиденциальности

Информация, представленная на данном сайте (1а.by), носит исключительно ознакомительный характер, не является индивидуальной инвестиционной рекомендацией, финансовым советом, призывом к действию либо публичной офертой. Компания (ООО “ПОРТАЛ ИНВЕСТОРОВ”) не несет ответственности за любые решения, принимаемые клиентами или посетителями сайта на оснвое размещенных на нем материалов. Все инвестиционные операции оусществляются пользователями самостоятельно и на их собственный риск.

Обработка персональных данных

Политика конфиденциальности

Правила пользования сайтом

Отказ от отвественности

Регистрация и участие в сообществе инвесторов доступно для регионов СНГ: Беларусь, Россия, Казахстан, Узбекистан, Армения, Таджикистан, Кыргызстан и др.

Обратите внимание, что создание аккаунта, разблокировка преимущества членства или использование бонусов доступно для лиц старше 18 лет.

Курсы и обучение от компаний-партнеров (создан специальный раздел, где есть возможность обучать начинающих инвесторов, не ограниченные возможности по добавлению, как платных, так и бесплатных курсов)​

Доска объявлений (где участники могут размещать объявления по продаже и поиску бизнеса, инвестиций в стартапы)

Свое сообщество (участник может создавать свое сообщество, делиться контентом и добавлять свою группy)

Социальная сеть (находите друзей, общайтесь и делитесь своими успехами)

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

Магазин инвесторов (где продаются разные полезности для инвесторов)

Контент (участник может делиться своими публикациями в мире инвестиций (статьи, новости, блог)

Сообщество (совместные онлайн и офлайн встречи и мероприятия)