Как Трейдер определяет КАЧЕСТВО стратегии

 
В эту тему были перенесены комментарии ...
 

Качество стратегии нельзя определять только по прибыли в тестере.
Хорошая стратегия — это не та, что просто показала высокий доход на истории, а та, которая одновременно показывает:

  • прибыльность;
  • контролируемую просадку;
  • достаточное число сделок;
  • нормальное соотношение прибыль/убыток;
  • устойчивость поведения на более свежем участке истории.

    Поэтому в OnTester()  использую критерий, который:

    • считает результат по полностью закрытым позициям, а не по отдельным кускам выхода;
    • отбрасывает слабые варианты по PF, Sharpe, Recovery, drawdown;
    • учитывает realized RR;
    • дополнительно проверяет, не ухудшилась ли торговая активность в 2025 году по сравнению с предыдущими полными годами.

    То есть задача этого кода — искать не “самую красивую доходность”, а более качественную, устойчивую и пригодную к реальной торговле стратегию.

    Ниже — самодостаточный OnTester -блок для MQL5 с комментариями на русском языке. Он сделан как quality-first критерий оптимизации: считает сделки по полностью закрытым позициям, использует единый источник данных для RR bonus и 2025 consistency , применяет мягкий штраф до 5% дефицита и ступенчатый штраф выше 5%. OnTester() предназначен для пользовательского критерия оптимизации.

    //+------------------------------------------------------------------+
    //|                  OnTester: QUALITY Strategy Score                |
    //|                 Универсальный блок для любого EA                 |
    //|                     MQL5 / Custom max criterion                  |
    //+------------------------------------------------------------------+
    
    // --------------------------- НАСТРОЙКИ -----------------------------
    
    input group "OnTester | Quality Strategy"
    
    // Фильтр по символу:
    // true  = считать только текущий символ (_Symbol)
    // false = считать все символы, которые торговал EA в тесте
    input bool   InpOT_UseCurrentSymbolOnly          = true;
    
    // Фильтр по magic:
    // -1 = не фильтровать по magic
    // любое другое значение = учитывать только сделки с этим magic
    input long   InpOT_MagicFilter                   = -1;
    
    // Включить контроль торговой активности 2025 против средних полных лет до 2025
    input bool   InpOT_EnableYear2025Consistency     = true;
    
    // Бонус, если количество закрытых позиций в 2025 >= среднему по предыдущим полным годам
    input double InpOT_Year2025BonusFactor           = 0.15;  // +15%
    
    // Шаг ступенчатого штрафа при дефиците > 5%
    // Пример: 0.10 = -10% к score за каждый начатый шаг 10% дефицита
    input double InpOT_Year2025PenaltyStep           = 0.10;
    
    // Минимальный допустимый множитель после штрафа
    // 0.20 = оставить не менее 20% исходного score
    input double InpOT_Year2025PenaltyMinFactor      = 0.20;
    
    // Печатать диагностические сообщения по годовой консистентности
    input bool   InpOT_YearlyDiagnostics             = false;
    
    // ------------------------ ЖЕСТКИЕ ФИЛЬТРЫ --------------------------
    
    // Минимум полностью закрытых позиций для допуска результата
    input int    InpOT_MinClosedPositions            = 30;
    
    // Минимально допустимый Profit Factor
    input double InpOT_MinProfitFactor               = 1.15;
    
    // Минимально допустимый Recovery Factor
    input double InpOT_MinRecoveryFactor             = 0.80;
    
    // Минимально допустимый Sharpe Ratio
    input double InpOT_MinSharpeRatio                = 0.10;
    
    // Максимально допустимый относительный equity drawdown, %
    input double InpOT_MaxEquityDDRelPercent         = 25.0;
    
    // Минимально допустимый реализованный RR (avgWin / avgLossAbs), 0 = отключить
    input double InpOT_MinRealizedRR                 = 0.0;
    
    // Максимальный бонус за реализованный RR
    input double InpOT_MaxRRBonus                    = 0.05;  // максимум +5%
    
    // ------------------------ ВСПОМОГАТЕЛЬНОЕ --------------------------
    
    // Безопасный clamp для double
    double QTS_ClampD(const double v, const double lo, const double hi)
      {
       if(v < lo) return lo;
       if(v > hi) return hi;
       return v;
      }
    
    // Безопасное чтение TesterStatistics()
    // В тестере функция определена для OnTester()/OnDeinit(); вне этого контекста результат не гарантирован. :contentReference[oaicite:1]{index=1}
    double QTS_TestStatSafe(const ENUM_STATISTICS stat)
      {
       double v = TesterStatistics(stat);
    
       if(!MathIsValidNumber(v))
          return 0.0;
    
       if(v == DBL_MAX || v == -DBL_MAX)
          return 0.0;
    
       return v;
      }
    
    // Создание datetime
    datetime QTS_MakeDateTime(const int y, const int mon, const int day,
                              const int hh=0, const int mm=0, const int ss=0)
      {
       MqlDateTime dt;
       dt.year = y;
       dt.mon  = mon;
       dt.day  = day;
       dt.hour = hh;
       dt.min  = mm;
       dt.sec  = ss;
       return StructToTime(dt);
      }
    
    datetime QTS_YearStart(const int y)
      {
       return QTS_MakeDateTime(y, 1, 1, 0, 0, 0);
      }
    
    // Прокси диапазона теста:
    // начало = самый старый доступный бар текущего символа/таймфрейма,
    // конец  = TimeCurrent() на момент завершения теста.
    // Это переносимое приближение, когда явные границы теста в код не переданы.
    void QTS_GetTesterRange(datetime &fromT, datetime &toT)
      {
       toT = TimeCurrent();
    
       int bars = Bars(_Symbol, (ENUM_TIMEFRAMES)_Period);
       if(bars <= 0)
         {
          fromT = 0;
          return;
         }
    
       fromT = iTime(_Symbol, (ENUM_TIMEFRAMES)_Period, bars - 1);
      }
    
    bool QTS_IsFullCalendarYearCovered(const int year, const datetime fromT, const datetime toT)
      {
       datetime y0 = QTS_YearStart(year);
       datetime y1 = QTS_YearStart(year + 1);
    
       if(fromT == 0 || toT == 0)
          return false;
    
       return (fromT <= y0 && toT >= y1);
      }
    
    int QTS_FindYearIndex(const int &years[], const int y)
      {
       int n = ArraySize(years);
       for(int i = 0; i < n; i++)
          if(years[i] == y)
             return i;
       return -1;
      }
    
    long QTS_GetYearCountOrZero(const int &years[], const long &counts[], const int y)
      {
       int idx = QTS_FindYearIndex(years, y);
       if(idx < 0) return 0;
       if(idx >= ArraySize(counts)) return 0;
       return counts[idx];
      }
    
    int QTS_FindULongIndex(const ulong &vals[], const ulong v)
      {
       int n = ArraySize(vals);
       for(int i = 0; i < n; i++)
          if(vals[i] == v)
             return i;
       return -1;
      }
    
    // ----------------- АГРЕГАЦИЯ ПО ПОЛНОСТЬЮ ЗАКРЫТЫМ ПОЗИЦИЯМ -----------------
    //
    // Логика:
    // 1) Берем историю сделок EA.
    // 2) Фильтруем по символу и/или magic, если это включено.
    // 3) Берем только выходные deal'ы позиции: OUT / INOUT / OUT_BY.
    // 4) Группируем по DEAL_POSITION_ID.
    // 5) Суммируем net PnL по позиции:
    //    DEAL_PROFIT + DEAL_COMMISSION + DEAL_SWAP + DEAL_FEE
    // 6) Каждую полностью закрытую позицию считаем ОДИН раз.
    // 7) Год позиции = время последнего выходного deal'а этой позиции.
    //
    // DEAL_POSITION_ID, DEAL_ENTRY_* и DEAL_FEE доступны как свойства сделок истории MQL5. :contentReference[oaicite:2]{index=2}
    void QTS_BuildClosedPositionStats(const string symbol_filter,
                                      const bool   use_symbol_filter,
                                      const long   magic_filter,
                                      long &wins,
                                      long &losses,
                                      long &breakeven,
                                      long &totalClosed,
                                      double &avgWinNet,
                                      double &avgLossAbsNet,
                                      int &years[],
                                      long &counts[])
      {
       wins          = 0;
       losses        = 0;
       breakeven     = 0;
       totalClosed   = 0;
       avgWinNet     = 0.0;
       avgLossAbsNet = 0.0;
    
       ArrayResize(years,  0);
       ArrayResize(counts, 0);
    
       datetime to = TimeCurrent();
       if(!HistorySelect(0, to))
          return;
    
       // Массивы для агрегации по position_id
       ulong    posIds[];
       double   posNet[];
       datetime posLastExit[];
    
       ArrayResize(posIds,      0);
       ArrayResize(posNet,      0);
       ArrayResize(posLastExit, 0);
    
       int deals = HistoryDealsTotal();
       for(int i = 0; i < deals; i++)
         {
          ulong ticket = HistoryDealGetTicket(i);
          if(ticket == 0)
             continue;
    
          string sym = HistoryDealGetString(ticket, DEAL_SYMBOL);
          if(use_symbol_filter && sym != symbol_filter)
             continue;
    
          long mg = (long)HistoryDealGetInteger(ticket, DEAL_MAGIC);
          if(magic_filter >= 0 && mg != magic_filter)
             continue;
    
          long entry = (long)HistoryDealGetInteger(ticket, DEAL_ENTRY);
          if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_INOUT && entry != DEAL_ENTRY_OUT_BY)
             continue;
    
          ulong posId = (ulong)HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
          if(posId == 0)
             continue;
    
          datetime tt = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
          if(tt <= 0)
             continue;
    
          double p   = HistoryDealGetDouble(ticket, DEAL_PROFIT);
          double c   = HistoryDealGetDouble(ticket, DEAL_COMMISSION);
          double sw  = HistoryDealGetDouble(ticket, DEAL_SWAP);
          double fee = HistoryDealGetDouble(ticket, DEAL_FEE);
    
          double net = p + c + sw + fee;
          if(!MathIsValidNumber(net))
             continue;
    
          int idx = QTS_FindULongIndex(posIds, posId);
          if(idx < 0)
            {
             int sz = ArraySize(posIds);
    
             ArrayResize(posIds,      sz + 1);
             ArrayResize(posNet,      sz + 1);
             ArrayResize(posLastExit, sz + 1);
    
             posIds[sz]      = posId;
             posNet[sz]      = net;
             posLastExit[sz] = tt;
            }
          else
            {
             posNet[idx] += net;
             if(tt > posLastExit[idx])
                posLastExit[idx] = tt;
            }
         }
    
       double sumWin     = 0.0;
       double sumLossAbs = 0.0;
    
       int nPos = ArraySize(posIds);
       for(int i = 0; i < nPos; i++)
         {
          double   net = posNet[i];
          datetime tt  = posLastExit[i];
    
          if(tt <= 0 || !MathIsValidNumber(net))
             continue;
    
          totalClosed++;
    
          if(net > 0.0)
            {
             wins++;
             sumWin += net;
            }
          else if(net < 0.0)
            {
             losses++;
             sumLossAbs += (-net);
            }
          else
            {
             breakeven++;
            }
    
          MqlDateTime md;
          TimeToStruct(tt, md);
          int y = md.year;
          if(y <= 0)
             continue;
    
          int yidx = QTS_FindYearIndex(years, y);
          if(yidx < 0)
            {
             int sz = ArraySize(years);
             ArrayResize(years,  sz + 1);
             ArrayResize(counts, sz + 1);
             years[sz]  = y;
             counts[sz] = 1;
            }
          else
            {
             counts[yidx]++;
            }
         }
    
       if(wins > 0)
          avgWinNet = sumWin / (double)wins;
    
       if(losses > 0)
          avgLossAbsNet = sumLossAbs / (double)losses;
      }
    
    // ---------------------- 2025 CONSISTENCY MULTIPLIER ----------------------
    //
    // Идея:
    // - если 2025 покрыт полностью,
    // - и есть база: среднее число закрытых позиций по полным годам до 2025,
    // то:
    //   * если 2025 >= baseline avg -> бонус,
    //   * если дефицит <= 5%       -> мягкий линейный штраф,
    //   * если дефицит > 5%        -> более строгий ступенчатый штраф.
    //
    // Это именно проверка КОНСИСТЕНТНОСТИ ТОРГОВОЙ АКТИВНОСТИ,
    // а не полной стабильности доходности.
    double QTS_Year2025ConsistencyMultiplier(const int &years[],
                                             const long &counts[],
                                             const double scoreForDiag)
      {
       if(!InpOT_EnableYear2025Consistency)
          return 1.0;
    
       datetime fromT = 0, toT = 0;
       QTS_GetTesterRange(fromT, toT);
    
       // Применяем только если 2025 полностью покрыт диапазоном теста
       if(!QTS_IsFullCalendarYearCovered(2025, fromT, toT))
          return 1.0;
    
       MqlDateTime mdFrom;
       TimeToStruct(fromT, mdFrom);
    
       int startYear = mdFrom.year;
       if(startYear <= 0)
          startYear = 2000;
    
       double sum = 0.0;
       int    n   = 0;
    
       for(int y = startYear; y <= 2024; y++)
         {
          if(QTS_IsFullCalendarYearCovered(y, fromT, toT))
            {
             long c = QTS_GetYearCountOrZero(years, counts, y);
             sum += (double)c;
             n++;
            }
         }
    
       if(n <= 0)
          return 1.0;
    
       double avgPrev = sum / (double)n;
       if(avgPrev <= 0.0)
          return 1.0;
    
       long c2025 = QTS_GetYearCountOrZero(years, counts, 2025);
    
       // Бонус, если 2025 не хуже средней базы
       if((double)c2025 >= avgPrev)
         {
          double mult = 1.0 + MathMax(0.0, InpOT_Year2025BonusFactor);
    
          if(InpOT_YearlyDiagnostics)
            {
             PrintFormat("[QTS][Y2025_BONUS] score=%.2f | c2025=%d | avgPrev=%.2f | mult=%.3f",
                         scoreForDiag, (int)c2025, avgPrev, mult);
            }
    
          return mult;
         }
    
       double ratio   = (double)c2025 / avgPrev; // < 1
       double deficit = 1.0 - ratio;
       double mult    = 1.0;
    
       // До 5% дефицита штраф мягкий и линейный
       if(deficit <= 0.05)
         {
          mult = 1.0 - deficit;
         }
       else
         {
          // Выше 5% - более жесткий ступенчатый штраф
          double step = MathMax(0.0001, InpOT_Year2025PenaltyStep);
          int steps = (int)MathCeil(deficit / step);
          if(steps < 1)
             steps = 1;
    
          mult = 1.0 - (double)steps * step;
         }
    
       double minMult = QTS_ClampD(InpOT_Year2025PenaltyMinFactor, 0.0, 1.0);
       if(mult < minMult)
          mult = minMult;
    
       if(InpOT_YearlyDiagnostics)
         {
          PrintFormat("[QTS][Y2025_PENALTY] score=%.2f | c2025=%d | avgPrev=%.2f | ratio=%.3f | deficit=%.3f | mult=%.3f",
                      scoreForDiag, (int)c2025, avgPrev, ratio, deficit, mult);
         }
    
       return mult;
      }
    
    // ------------------------------ OnTester ------------------------------
    //
    // Принцип:
    // 1) Сначала жестко отсекаем слабые/некачественные результаты.
    // 2) Потом считаем интегральный score.
    // 3) Затем применяем малый бонус за realized RR.
    // 4) Затем применяем корректировку по консистентности 2025.
    //
    // В генетической оптимизации MQL5 пользовательский критерий сортируется по убыванию:
    // чем БОЛЬШЕ значение OnTester(), тем "лучше" проход для Custom max. :contentReference[oaicite:3]{index=3}
    double OnTester()
      {
       // --- Статистика тестера ---
       double initDep  = QTS_TestStatSafe(STAT_INITIAL_DEPOSIT);
       if(initDep <= 0.0)
          initDep = 1.0;
    
       double netProfit = QTS_TestStatSafe(STAT_PROFIT);
       double ddRelPct  = MathMax(0.0, QTS_TestStatSafe(STAT_EQUITY_DDREL_PERCENT));
       double pf        = QTS_ClampD(QTS_TestStatSafe(STAT_PROFIT_FACTOR),   0.0, 50.0);
       double rec       = QTS_ClampD(QTS_TestStatSafe(STAT_RECOVERY_FACTOR), 0.0, 50.0);
       double sharpe    = QTS_TestStatSafe(STAT_SHARPE_RATIO);
    
       // --- Единый источник данных для RR и годовой консистентности ---
       string symbol_filter = _Symbol;
       bool   use_symbol    = InpOT_UseCurrentSymbolOnly;
    
       long   wins = 0, losses = 0, breakeven = 0, totalClosed = 0;
       double avgWinNet = 0.0, avgLossAbsNet = 0.0;
       int    years[];
       long   counts[];
    
       QTS_BuildClosedPositionStats(symbol_filter,
                                    use_symbol,
                                    InpOT_MagicFilter,
                                    wins, losses, breakeven, totalClosed,
                                    avgWinNet, avgLossAbsNet,
                                    years, counts);
    
       // Если по агрегированным позициям ничего нет, пробуем подстраховаться статистикой тестера
       long totalTradesStat = (long)MathRound(QTS_TestStatSafe(STAT_TRADES));
       if(totalClosed <= 0 && totalTradesStat > 0)
          totalClosed = totalTradesStat;
    
       // --- Реализованный RR ---
       double rr = 0.0;
       if(avgLossAbsNet > 0.0)
          rr = avgWinNet / avgLossAbsNet;
       else if(avgWinNet > 0.0 && losses == 0)
          rr = 10.0; // "нет убыточных закрытых позиций" -> очень хороший RR, но дальше бонус будет ограничен
    
       // ------------------ ЖЕСТКИЕ ФИЛЬТРЫ КАЧЕСТВА ------------------
    
       if(totalClosed < InpOT_MinClosedPositions)
          return -1e12 - (double)(InpOT_MinClosedPositions - totalClosed);
    
       if(netProfit <= 0.0)
          return -1e12 - MathAbs(netProfit) - ddRelPct * 1000.0;
    
       if(pf < InpOT_MinProfitFactor)
          return -9e11 - (InpOT_MinProfitFactor - pf) * 1e6;
    
       if(rec < InpOT_MinRecoveryFactor)
          return -8e11 - (InpOT_MinRecoveryFactor - rec) * 1e6;
    
       if(sharpe < InpOT_MinSharpeRatio)
          return -7e11 - (InpOT_MinSharpeRatio - sharpe) * 1e6;
    
       if(ddRelPct > InpOT_MaxEquityDDRelPercent)
          return -6e11 - (ddRelPct - InpOT_MaxEquityDDRelPercent) * 1e6;
    
       if(InpOT_MinRealizedRR > 0.0 && rr < InpOT_MinRealizedRR)
          return -5e11 - (InpOT_MinRealizedRR - rr) * 1e6;
    
       // ------------------ БАЗОВЫЙ QUALITY SCORE ------------------
       //
       // ВАЖНО:
       // Здесь НЕТ дублирования через growth / avgTrade / winTrades/lossTrades.
       // Оставлены только более "ортогональные" компоненты:
       // - net profit относительно стартового капитала,
       // - Profit Factor,
       // - Recovery Factor,
       // - Sharpe,
       // - размер выборки (closed positions),
       // - штраф за drawdown.
       double netPos = MathMax(0.0, netProfit);
       double shPos  = MathMax(0.0, sharpe);
    
       double score =
          1200.0 * MathLog(1.0 + netPos / initDep)              // основной вклад прибыли
          +  300.0 * MathLog(1.0 + pf)                          // качество прибыли
          +  240.0 * MathLog(1.0 + rec)                         // восстановление после просадок
          +  180.0 * MathLog(1.0 + shPos)                       // риск-скорректированная доходность
          +   35.0 * MathLog(1.0 + (double)totalClosed)         // предпочтение большему числу закрытых позиций
          -   28.0 * ddRelPct;                                  // штраф за относительную equity-просадку
    
       // ------------------ БОНУС ЗА REALIZED RR ------------------
       //
       // Бонус небольшой и ограниченный, чтобы не "сломать" базовый score.
       // Работает только если RR > 1 и растет по мере увеличения RR.
       double rr_pref = 0.0;
       if(rr > 1.0)
          rr_pref = QTS_ClampD((rr - 1.0) / 2.0, 0.0, 1.0);  // RR>=3 -> полный rr_pref=1
    
       double conf = QTS_ClampD((double)totalClosed / 50.0, 0.0, 1.0);
       double rr_bonus = 1.0 + QTS_ClampD(InpOT_MaxRRBonus, 0.0, 1.0) * rr_pref * conf;
    
       score *= rr_bonus;
    
       // ------------------ КОНСИСТЕНТНОСТЬ 2025 ------------------
       //
       // Применяем только к уже "достойным" проходам.
       if(score > 0.0)
         {
          double ymult = QTS_Year2025ConsistencyMultiplier(years, counts, score);
          score *= ymult;
         }
    
       return score;
      }
     
    Lilita Bogachkova #:
    Как долго Вас не было
     
    Ivan Butko #:
    Как долго Вас не было
    Я была здесь, просто не проявляла инициативы, чтобы кому-либо о себе сообщить. 😏