Качество стратегии нельзя определять только по прибыли в тестере.
Хорошая стратегия — это не та, что просто показала высокий доход на истории, а та, которая одновременно показывает:
- прибыльность;
- контролируемую просадку;
- достаточное число сделок;
- нормальное соотношение прибыль/убыток;
- устойчивость поведения на более свежем участке истории.
Поэтому в 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; }
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования