preview
Ассоциативные правила и теория вероятностей на Форекс: Фильтр сигналов без нейросетей и без магии

Ассоциативные правила и теория вероятностей на Форекс: Фильтр сигналов без нейросетей и без магии

MetaTrader 5Торговые системы |
88 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Введение

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

Идея такая: вместо того чтобы заставлять модель "думать" как нейросеть, мы собираем статистику повторяющихся рыночных состояний. Если рынок часто ведёт себя определённым образом после комбинации признаков, система запоминает это как правило. Например: если RSI находится в зоне перепроданности, цена ниже EMA, волатильность высокая, а час относится к европейской сессии, то через N баров цена чаще растёт, чем падает. Это не предсказание будущего в мистическом смысле. Это аккуратная статистическая оценка: что обычно происходило после похожих условий в истории.

Такой подход хорошо подходит для торговых роботов, потому что он не требует Python, файлов весов, внешних библиотек и тяжёлого обучения. Всё можно сделать прямо внутри советника: собрать признаки, дискретизировать их в события, посчитать частоты, оценить вероятности и использовать результат как последний фильтр перед входом.


Что такое ассоциативное правило в трейдинге

В классическом анализе данных ассоциативное правило выглядит так:

A => B

Где A — набор условий, а B — следствие. В магазине это может быть правило: если человек покупает хлеб и сыр, он часто покупает масло. На Форекс структура такая же, только вместо товаров используются рыночные события:

RSI_LOW + PRICE_BELOW_EMA + ATR_HIGH => FUTURE_UP

Это означает: в истории были бары, где одновременно выполнялись условия RSI_LOW, PRICE_BELOW_EMA и ATR_HIGH. После части таких баров цена через заданный горизонт была выше. Нас интересует не само правило как красивая фраза, а его численные характеристики: поддержка, доверие, прирост вероятности и устойчивость на отложенном участке истории.

Понятие Формула Смысл для Форекса
Support count(A and B) / N Как часто правило вообще встречается в истории
Confidence count(A and B) / count(A) Вероятность следствия B при условии A
Base rate count(B) / N Базовая вероятность роста или падения без условий
Lift confidence(A => B) / base_rate(B) Насколько правило лучше обычной частоты рынка
Edge confidence - base_rate Чистое вероятностное преимущество правила

Главная ошибка новичка — смотреть только на confidence. Если правило сработало 3 раза из 3, confidence будет 100%, но это ничего не доказывает: выборка слишком маленькая. Поэтому правило должно проходить минимальную поддержку: например, не меньше 30, 50 или 100 исторических случаев. Иначе робот начнёт верить случайным совпадениям.


Почему это не обычный индикатор

Обычный индикатор возвращает одно значение: RSI, EMA, ATR, наклон, сигнал или цвет. Ассоциативная система работает иначе. Она смотрит на комбинацию событий. Одно событие само по себе может быть слабым, но вместе с другими оно становится информативным.

Например, перепроданность RSI сама по себе не гарантирует разворот. Цена может долго оставаться в перепроданности на сильном тренде. Но если перепроданность совпала с растущей волатильностью, удалением от EMA, замедлением импульса и определённой торговой сессией, то такая комбинация уже может иметь статистический характер. Ассоциативные правила как раз ищут не один индикатор, а устойчивые связки.

В этом смысле подход похож на ручную логику опытного трейдера. Трейдер редко думает одним числом. Он говорит: "RSI низко, цена далеко от средней, ATR расширился, на Европе часто бывает откат". Ассоциативные правила переводят такую логику в формальные частоты и вероятности.


Как превратить рынок в набор событий

Чтобы использовать ассоциативные правила, непрерывные значения нужно превратить в дискретные события. RSI 31.7, ATR 0.00124 и наклон EMA 0.00018 неудобны для правил. Зато удобно сказать: RSI_LOW, ATR_HIGH, EMA_UP. Это называется дискретизация.

# Источник Событие Условие Смысл
0 RSI RSI_LOW RSI < 35 Рынок близок к перепроданности
1 RSI RSI_HIGH RSI > 65 Рынок близок к перекупленности
2 EMA PRICE_ABOVE_EMA Close > EMA(50) Цена выше средней
3 EMA PRICE_BELOW_EMA Close < EMA(50) Цена ниже средней
4 ATR ATR_HIGH ATR / Close > порога Режим повышенной волатильности
5 Momentum MOM_UP Close[shift] > Close[shift+6] Импульс направлен вверх
6 Momentum MOM_DOWN Close[shift] < Close[shift+6] Импульс направлен вниз
7 Время SESSION_EU 8 <= hour < 17 Европейская сессия

Важно: события должны быть достаточно грубыми. Если разбить рынок на сотни редких условий, каждое правило будет встречаться слишком мало раз. Тогда статистика станет красивой, но бесполезной. Для первой версии лучше использовать 8–20 устойчивых событий и правила длиной 2–4 события.


Метка: что считается успешным исходом

Следствие правила должно быть измеримым. Самый простой вариант — направление будущего движения через InpRuleHorizon баров:

FUTURE_UP   = Close[shift - Horizon] > Close[shift]
FUTURE_DOWN = Close[shift - Horizon] < Close[shift]

В series-индексации MQL5 более новый бар имеет меньший shift. Поэтому будущий бар для исторического примера — это shift − Horizon. Если shift больше Horizon, соответствующий будущий бар уже закрыт и доступен в истории. На этапе обучения исход известен; на живом рынке используется только закрытый бар shift=1, и обращение к будущим данным исключено.

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

//+------------------------------------------------------------------+
//| Метка будущего направления для исторического бара shift.         |
//| Возвращает 1 для роста, 0 для падения. Мелкий шум отсекается.    |
//+------------------------------------------------------------------+
bool RuleLabel(int shift, int &label)
  {
   if(shift < InpRuleHorizon+1) return(false);

   double nowClose[], futureClose[];
   ArraySetAsSeries(nowClose,true);
   ArraySetAsSeries(futureClose,true);

   if(CopyClose(_Symbol,_Period,shift,1,nowClose)<1) return(false);
   if(CopyClose(_Symbol,_Period,shift-InpRuleHorizon,1,futureClose)<1) return(false);

   double movePts = (futureClose[0]-nowClose[0])/_Point;
   if(MathAbs(movePts) < InpRuleMinMovePts) return(false);

   label = (movePts > 0.0) ? 1 : 0;
   return(true);
  }


Кодирование событий в MQL5

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

enum ENUM_RULE_EVENT
  {
   EV_RSI_LOW         = 0,
   EV_RSI_HIGH        = 1,
   EV_PRICE_ABOVE_EMA  = 2,
   EV_PRICE_BELOW_EMA  = 3,
   EV_ATR_HIGH        = 4,
   EV_MOM_UP          = 5,
   EV_MOM_DOWN        = 6,
   EV_SESSION_EU      = 7,
   EV_SESSION_US      = 8
  };

int EventBit(int eventId)
  {
   return(1 << eventId);
  }

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

//+------------------------------------------------------------------+
//| Сбор событий рынка на сдвиге shift.                              |
//| Возвращает битовую маску: каждый включённый бит = событие.       |
//+------------------------------------------------------------------+
bool BuildMarketEvents(int shift, int &mask)
  {
   mask = 0;

   double rsi[]; ArraySetAsSeries(rsi,true);
   double atr[]; ArraySetAsSeries(atr,true);
   double ema[]; ArraySetAsSeries(ema,true);
   double cl[];  ArraySetAsSeries(cl,true);

   if(CopyBuffer(g_rsiHandle,0,shift,1,rsi)<1) return(false);
   if(CopyBuffer(g_atrHandle,0,shift,1,atr)<1) return(false);
   if(CopyBuffer(g_emaHandle,0,shift,1,ema)<1) return(false);
   if(CopyClose(_Symbol,_Period,shift,7,cl)<7) return(false);

   if(rsi[0] < InpRsiLow)  mask |= EventBit(EV_RSI_LOW);
   if(rsi[0] > InpRsiHigh) mask |= EventBit(EV_RSI_HIGH);

   if(cl[0] > ema[0]) mask |= EventBit(EV_PRICE_ABOVE_EMA);
   if(cl[0] < ema[0]) mask |= EventBit(EV_PRICE_BELOW_EMA);

   double atrNorm = atr[0] / cl[0];
   if(atrNorm > InpAtrHighNorm) mask |= EventBit(EV_ATR_HIGH);

   if(cl[0] > cl[6]) mask |= EventBit(EV_MOM_UP);
   if(cl[0] < cl[6]) mask |= EventBit(EV_MOM_DOWN);

   datetime bt = iTime(_Symbol,_Period,shift);
   MqlDateTime mt; TimeToStruct(bt,mt);
   if(mt.hour >= 8  && mt.hour < 17) mask |= EventBit(EV_SESSION_EU);
   if(mt.hour >= 13 && mt.hour < 22) mask |= EventBit(EV_SESSION_US);

   return(true);
  }


От событий к правилам

Если на баре включено несколько событий, из них можно построить комбинации. Например, маска содержит RSI_LOW, PRICE_BELOW_EMA, ATR_HIGH и SESSION_EU. Тогда система может проверить правила длиной 2 и 3:

RSI_LOW + PRICE_BELOW_EMA
RSI_LOW + ATR_HIGH
PRICE_BELOW_EMA + ATR_HIGH
RSI_LOW + PRICE_BELOW_EMA + ATR_HIGH
RSI_LOW + ATR_HIGH + SESSION_EU

Чем длиннее правило, тем точнее описание состояния, но тем меньше исторических примеров. Поэтому для боевого советника часто достаточно правил длиной 2–3. Правила длиной 4 можно использовать осторожно и только при большой истории.

В MQL5 удобно хранить статистику правила в структуре: маска условия, сколько раз условие встречалось, сколько раз после него был рост, сколько раз было падение. Потом из этих счётчиков считаются вероятности.

struct SRuleStat
  {
   int mask;
   int total;
   int up;
   int down;
  };

SRuleStat g_rules[];
int      g_ruleCount = 0;
int      g_baseUp = 0;
int      g_baseDown = 0;

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

//+------------------------------------------------------------------+
//| Найти правило по маске или создать новое.                        |
//+------------------------------------------------------------------+
int FindOrAddRule(int ruleMask)
  {
   for(int i=0;i<g_ruleCount;i++)
     if(g_rules[i].mask == ruleMask)
       return(i);

   ArrayResize(g_rules,g_ruleCount+1);
   g_rules[g_ruleCount].mask  = ruleMask;
   g_rules[g_ruleCount].total = 0;
   g_rules[g_ruleCount].up    = 0;
   g_rules[g_ruleCount].down  = 0;

   g_ruleCount++;
   return(g_ruleCount-1);
  }

//+------------------------------------------------------------------+
//| Добавить наблюдение: условие ruleMask и будущая метка label.     |
//+------------------------------------------------------------------+
void AddRuleObservation(int ruleMask, int label)
  {
   if(ruleMask == 0) return;

   int id = FindOrAddRule(ruleMask);
   g_rules[id].total++;

   if(label == 1) g_rules[id].up++;
   else          g_rules[id].down++;
  }


Генерация комбинаций

Можно начать с правил длиной 1, 2 и 3. Правила длиной 1 дают базовую диагностику. Правила длиной 2–3 уже описывают связки рыночных условий. Ниже — простой вариант генерации комбинаций из включённых событий.

//+------------------------------------------------------------------+
//| Разложить маску событий в список активных eventId.               |
//+------------------------------------------------------------------+
int ExtractEvents(int mask, int &events[])
  {
   ArrayResize(events,0);
   int n=0;

   for(int e=0;e<16;e++)
     {
       if((mask & EventBit(e)) != 0)
         {
           ArrayResize(events,n+1);
           events[n]=e;
           n++;
       }
     }
   return(n);
  }

//+------------------------------------------------------------------+
//| Добавить все правила длиной 1, 2 и 3 для одного бара.            |
//+------------------------------------------------------------------+
void AddCombinations(int eventMask, int label)
  {
   int ev[];
   int n = ExtractEvents(eventMask,ev);

   //--- правила из одного события
   for(int a=0;a<n;a++)
     AddRuleObservation(EventBit(ev[a]),label);

   //--- правила из двух событий
   for(int a=0;a<n;a++)
     for(int b=a+1;b<n;b++)
       {
         int m = EventBit(ev[a]) | EventBit(ev[b]);
         AddRuleObservation(m,label);
       }

   //--- правила из трёх событий
   for(int a=0;a<n;a++)
     for(int b=a+1;b<n;b++)
       for(int c=b+1;c<n;c++)
         {
           int m = EventBit(ev[a]) | EventBit(ev[b]) | EventBit(ev[c]);
           AddRuleObservation(m,label);
         }
  }


Обучение правил на истории

Обучение здесь — это не градиентный спуск и не подбор весов. Мы просто проходим по истории, строим события на каждом баре, формируем метку будущего направления и обновляем счётчики правил. Это делает систему очень прозрачной: любую вероятность можно объяснить количеством исторических случаев.

//+------------------------------------------------------------------+
//| Обучение ассоциативных правил на истории.                        |
//+------------------------------------------------------------------+
void TrainRules()
  {
   ArrayResize(g_rules,0);
   g_ruleCount = 0;
   g_baseUp    = 0;
   g_baseDown  = 0;

   int fromShift = InpRuleHorizon + 1;
   int maxBars   = Bars(_Symbol,_Period);
   int toShift   = MathMin(maxBars-60, InpRuleHorizon + InpRuleLookback);

   if(toShift <= fromShift+50)
     {
       Print("RULES: мало истории для обучения");
       return;
     }

   int used = 0;

   for(int shift=fromShift; shift<=toShift; shift++)
     {
       int label;
       if(!RuleLabel(shift,label)) continue;

       int eventMask;
       if(!BuildMarketEvents(shift,eventMask)) continue;
       if(eventMask == 0) continue;

       if(label == 1) g_baseUp++;
       else         g_baseDown++;

       AddCombinations(eventMask,label);
       used++;
   }

   Print("RULES: examples=",used," rules=",g_ruleCount,
       " baseUp=",g_baseUp," baseDown=",g_baseDown);
  }

После такого обучения в памяти советника появляется таблица правил. Каждое правило можно оценить по вероятности роста, вероятности падения, поддержке, lift и edge. Именно эти числа используются в торговом фильтре.


Теория вероятностей: как не обмануть себя

В трейдинге опасно смотреть на голые проценты. Если правило встречалось 5 раз и 4 раза после него был рост, вероятность роста равна 80%. Но статистически это слабое основание. Если правило встречалось 200 раз и 120 раз после него был рост, вероятность всего 60%, но такое правило может быть надёжнее, потому что у него больше наблюдений.

Поэтому в фильтре нужны три защиты:

  1. Минимальная поддержка. Правило не используется, пока оно не встретилось хотя бы InpMinRuleCount раз.
  2. Сглаживание вероятности. Вместо сырого up/total лучше использовать сглаженную оценку, чтобы редкие правила не получали экстремальные значения.
  3. Сравнение с базовой вероятностью. Правило должно быть лучше обычной частоты роста или падения на рынке.

Для сглаживания удобно использовать оценку Лапласа:

P(up | A) = (up + alpha) / (total + 2 * alpha)

Если alpha = 1, то правило с 0/1 уже не даст 0% или 100%. Оно будет сдержаннее. Это важно, потому что Форекс шумный, а случайные экстремальные оценки быстро приводят к переобучению.

//+------------------------------------------------------------------+
//| Сглаженная вероятность роста по правилу.                         |
//+------------------------------------------------------------------+
double RuleProbUp(const SRuleStat &r)
  {
   double a = InpLaplaceAlpha;
   return((r.up + a) / (r.total + 2.0*a));
  }

//+------------------------------------------------------------------+
//| Базовая вероятность роста по всей обучающей истории.             |
//+------------------------------------------------------------------+
double BaseProbUp()
  {
   double a = InpLaplaceAlpha;
   return((g_baseUp + a) / (g_baseUp + g_baseDown + 2.0*a));
  }


Выбор лучшего правила на текущем рынке

На текущем закрытом баре советник строит маску событий. Затем он ищет все правила, чьи условия входят в текущую маску. Например, если сейчас включены RSI_LOW, PRICE_BELOW_EMA и ATR_HIGH, то подходят правила RSI_LOW, PRICE_BELOW_EMA, RSI_LOW+ATR_HIGH и так далее.

Из подходящих правил можно выбрать лучшее по score. Простейшая оценка:

scoreUp = (P(up | A) - BaseP(up)) * log(1 + total)

Вероятностное преимущество умножается на логарифм числа наблюдений. Так редкое правило с красивой вероятностью не перебивает более устойчивое правило с нормальной поддержкой.

//+------------------------------------------------------------------+
//| Проверяет, является ли ruleMask подмножеством currentMask.       |
//+------------------------------------------------------------------+
bool IsSubMask(int ruleMask, int currentMask)
  {
   return((ruleMask & currentMask) == ruleMask);
  }

//+------------------------------------------------------------------+
//| Найти лучшую вероятностную оценку текущего состояния.            |
//| Возвращает confidence: + вверх, - вниз.                          |
//+------------------------------------------------------------------+
bool GetRuleConfidence(int currentMask, double &confidence)
  {
   confidence = 0.0;

   double baseUp = BaseProbUp();
   double bestScore = 0.0;
   bool found = false;

   for(int i=0;i<g_ruleCount;i++)
     {
       SRuleStat r = g_rules[i];
       if(r.total < InpMinRuleCount) continue;
       if(!IsSubMask(r.mask,currentMask)) continue;

       double pUp = RuleProbUp(r);
       double edgeUp = pUp - baseUp;
       double score = MathAbs(edgeUp) * MathLog(1.0 + r.total);

       if(score > bestScore)
         {
           bestScore = score;
           confidence = edgeUp;
           found = true;
       }
   }

   return(found);
  }

Здесь confidence — не вероятность сама по себе, а преимущество относительно базовой частоты. Если confidence положительная, подходящее правило говорит в пользу роста. Если отрицательная — в пользу падения. Такой формат удобен для торгового фильтра: BUY проходит при достаточно положительной confidence, SELL — при достаточно отрицательной.


Интеграция в торговый фильтр

Ассоциативные правила лучше использовать не как самостоятельную кнопку входа, а как gate-фильтр. Основной сигнал может приходить от дивергенции RSI, SMC-зоны, пробоя, отката к EMA или любой другой логики. Правила не открывают сделку сами. Они только отвечают на вопрос: подтверждает ли статистика направление кандидата?

//+------------------------------------------------------------------+
//| Фильтр ассоциативных правил.                                     |
//| dir = +1 для BUY, dir = -1 для SELL.                             |
//+------------------------------------------------------------------+
bool RulesAccept(int dir)
  {
   if(!InpUseRules) return(true);
   if(g_ruleCount <= 0) return(InpRulesFailOpen);

   int mask;
   if(!BuildMarketEvents(1,mask)) return(InpRulesFailOpen);

   double conf;
   if(!GetRuleConfidence(mask,conf)) return(InpRulesFailOpen);

   bool ok = (dir == 1) ? (conf >=  InpRuleThreshold)
               : (conf <= -InpRuleThreshold);
   return(ok);
  }

Так получается двойное подтверждение. Сначала формальная торговая логика находит кандидата. Потом вероятностный слой проверяет, были ли похожие состояния в истории и в какую сторону они чаще отрабатывали. Если направление кандидата совпадает со статистическим преимуществом, сигнал проходит. Если нет — отклоняется.


Параметры системы

Параметр По умолчанию Комментарий
InpUseRules true Включить фильтр ассоциативных правил
InpRuleLookback 3000 Сколько баров истории использовать для подсчёта правил
InpRuleHorizon 12 Через сколько баров измерять будущий исход
InpRuleMinMovePts 20 Минимальное движение для метки, чтобы отсечь шум
InpMinRuleCount 50 Минимальная поддержка правила
InpRuleThreshold 0.08 Минимальное преимущество вероятности относительно базы
InpLaplaceAlpha 1.0 Сглаживание вероятностей, защита от редких экстремальных правил
InpRetrainRulesBars 250 Как часто пересобирать статистику правил
InpRulesFailOpen false false = не торговать, если правила не готовы
input group "=== Ассоциативные правила ==="
input bool   InpUseRules         = true;   // Включить фильтр правил
input int    InpRuleLookback     = 3000;   // Баров истории для правил
input int    InpRuleHorizon      = 12;     // Горизонт будущего исхода
input double InpRuleMinMovePts   = 20.0;   // Минимальное движение для метки
input int    InpMinRuleCount    = 50;     // Минимальная поддержка правила
input double InpRuleThreshold   = 0.08;   // Порог преимущества вероятности
input double InpLaplaceAlpha    = 1.0;    // Сглаживание Лапласа
input int    InpRetrainRulesBars= 250;    // Переобучать каждые N баров
input bool   InpRulesFailOpen   = false;  // Торговать, если правил нет

input group "=== Дискретизация событий ==="
input double InpRsiLow        = 35.0;
input double InpRsiHigh       = 65.0;
input double InpAtrHighNorm    = 0.0012;


Валидация: где чаще всего появляется самообман

Ассоциативные правила очень легко переобучить. Достаточно добавить слишком много событий, слишком длинные комбинации или слишком маленькую минимальную поддержку — и система найдёт красивые правила даже в случайном шуме. Поэтому нужна честная валидация.

Правильная схема для временного ряда:

  1. Старые 70–80% истории используются для построения правил.
  2. Новые 20–30% истории используются только для проверки.
  3. На валидации правила не пересчитываются и не подгоняются.
  4. Сравнивается не только точность направления, но и поведение торговой системы: прибыль, просадка, матожидание, серия убытков.

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

Для диагностики полезно выводить в лог лучшие правила. Не для ручной подгонки, а для проверки здравого смысла. Если лучшее правило встречалось 51 раз и даёт 98% роста, это подозрительно. Если правило встречалось 400 раз и даёт 58% при базовых 50%, это выглядит реалистичнее.

//+------------------------------------------------------------------+
//| Печать лучших правил для диагностики.                            |
//+------------------------------------------------------------------+
void PrintTopRules()
  {
   double baseUp = BaseProbUp();

   for(int i=0;i<g_ruleCount;i++)
     {
       if(g_rules[i].total < InpMinRuleCount) continue;

       double pUp = RuleProbUp(g_rules[i]);
       double edge = pUp - baseUp;
       double lift = (baseUp > 0.0) ? pUp/baseUp : 0.0;

       if(MathAbs(edge) >= InpRuleThreshold)
         Print("RULE mask=",g_rules[i].mask,
             " total=",g_rules[i].total,
             " pUp=",DoubleToString(pUp,3),
             " edge=",DoubleToString(edge,3),
             " lift=",DoubleToString(lift,3));
   }
  }


Чем ассоциативные правила отличаются от модели Маркова и MLP

Модель Маркова обычно оценивает переходы между состояниями: из состояния A рынок чаще переходит в B или C. MLP пытается выучить нелинейную зависимость между признаками и будущей меткой. Ассоциативные правила находятся между ними: они не строят сложную функцию, но умеют находить устойчивые комбинации условий.

Метод Что хранит Плюсы Минусы
Модель Маркова Матрицу переходов между состояниями Простой, быстрый, понятный Сильно зависит от качества кодирования состояний
MLP Веса нейросети Ловит нелинейные зависимости Менее прозрачен, требует обучения и контроля переобучения
Ассоциативные правила Частоты комбинаций событий и исходов Прозрачные вероятности, легко объяснить каждое решение Плохо работает при слишком редких комбинациях

Сильная сторона правил — объяснимость. Можно открыть лог и увидеть: сигнал BUY прошёл, потому что текущая комбинация событий встречалась 184 раза, рост после неё был в 61% случаев, базовая вероятность роста была 51%, edge составил 10 процентных пунктов. Для торгового робота это очень ценно: фильтр не выглядит чёрным ящиком.


Практическая логика входа

В реальной системе порядок фильтров может быть таким:

  1. Базовая торговая логика находит кандидата: BUY или SELL.
  2. Риск-менеджер проверяет лимиты, просадку, спред, количество позиций и торговое время.
  3. HTF-фильтр проверяет направление старшего таймфрейма.
  4. Ассоциативные правила проверяют статистическое преимущество текущей комбинации событий.
  5. Если все фильтры согласны, советник открывает сделку или уровень сетки.

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

bool CheckSignal(int &dir)
  {
   if(!FindBaseSetup(dir))      return(false);
   if(!RiskManagerAllows())    return(false);
   if(!HtfFilterAllows(dir))   return(false);
   if(!RulesAccept(dir))       return(false);

   return(true);
  }


Бэктест: что реально получилось на истории

Теория и красивые формулы — это только половина дела. Правила должны показать себя на тестере, и лучше сразу честно смотреть на цифры, а не только на факт, что советник вообще торгует. Ниже — результат прогона на EURUSD, H1, за один месяц (2026.06.01–2026.07.01) в MetaTrader 5 Strategy Tester, брокер RoboForex-Pro.


Параметр запуска Значение
Символ / таймфрейм EURUSD, H1
Период теста 2026.06.01 – 2026.07.01
InpRuleLookback / InpRuleHorizon 5000 / 24
InpRuleMinMovePts 240
InpMinRuleCount / InpRuleThreshold 50 / 0.08
InpRetrainRulesBars 20
Лот / цель по прибыли 0.1 / 1.0 USD за позицию
InpOneTradeAtATime true
Депозит / плечо 300 000 USD / 1:500

Итоговые метрики тестера:

Метрика Значение
Всего сделок / входов (Total Trades / Deals) 51 позиция, 102 сделки
Короткие / длинные позиции (won %) 17 sell (100%), 34 buy (100%)
Прибыльные / убыточные 51 из 51 (100%) прибыльных, 0 убыточных
Чистая прибыль 129.95 USD
Gross Profit / Gross Loss 129.95 USD / 0.00 USD
Profit Factor не определён (0.00 в отчёте — Gross Loss равен нулю)
Expected Payoff 2.55 USD на сделку
Recovery Factor / Sharpe Ratio 1.46 / 3.94
Максимальная просадка по эквити 89.20 USD (0.03% от депозита в 300 000)
Максимальная просадка по балансу 0.00 USD (0.00%)
Крупнейшая прибыльная / убыточная сделка 11.50 USD / 0.00 USD
Время удержания позиции (мин / сред. / макс.) 0:00:40 / 5:48:47 / 84:00:40

На первый взгляд цифры выглядят даже эффектнее, чем в предыдущем прогоне: 51 закрытая позиция вместо девяти, чистая прибыль почти 130 USD, Sharpe около 3.94. Но именно здесь особенно важна та же критичность, которая обсуждалась в разделе про валидацию правил, и здесь она обнажает проблему ещё нагляднее, чем раньше.

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

Второе: Profit Factor не определён, потому что Gross Loss равен нулю — ни одной убыточной сделки на 51 позицию. Это прямое следствие механики выхода: позиции по-прежнему закрываются только по достижению фиксированной цели в 1 USD (цель в этом прогоне понижена с 2 до 1 USD), защитного стопа как не было, так и нет. Позиция, идущая против системы, не закрывается принудительно — она просто остаётся открытой, пока цена не развернётся в нужную сторону или пока не закончится тест. Формальный результат "0% убыточных сделок" — это не доказательство эджа, а особенность конструкции: убыток в такой схеме почти никогда не фиксируется, он просто откладывается.

Третье: разброс времени удержания резко вырос и указывает на скрытый риск. Минимальное время удержания — 40 секунд, но максимальное — 84 часа (около 3,5 суток), при среднем в 5 часов 48 минут. Это значит, что часть позиций подолгу "пересиживает" просадку в ожидании плюса в 1 USD. Просадка по эквити в 89.20 USD выглядит крошечной на фоне депозита в 300 000 USD (0.03%), но это результат искусственно большого депозита и маленького лота 0.1, а не показатель реальной устойчивости стратегии: при депозите, сопоставимом с реальным торговым счётом, та же самая "пересиженная" на 3,5 суток сделка дала бы куда более заметную просадку в процентах. Recovery Factor 1.46 и Sharpe 3.94 тоже нужно читать с этой поправкой: они посчитаны по кривой эквити, на которой формально ни разу не зафиксирован убыток, а значит, оба показателя завышены относительно того, что увидит счёт с принудительным стопом.

Прежде чем считать результат подтверждением идеи, стоит закрыть те же три пробела, что и раньше, — с поправкой на новые цифры:

  1. Увеличить период и добавить walk-forward. Один месяц и 51 сделка — заметный шаг вперёд по сравнению с девятью сделками, но всё ещё недостаточно для окончательного вывода. Нужен прогон минимум на 1–2 года истории с разбивкой 70/30 на обучение и вневыборочную проверку, как описано в разделе про валидацию.
  2. Добавить защитный стоп или лимит по времени в сделке. Закрытие только по цели в 1 USD без какого-либо ограничения риска — это опасная конструкция для реальной торговли, и разброс времени удержания до 84 часов это подтверждает напрямую: убыточная сделка не имеет формальной границы ни по деньгам, ни по времени. Даже фиксированный SL или тайм-стоп через N баров сделает тест честнее и безопаснее.
  3. Проверить устойчивость правил при переобучении. В этом прогоне InpRetrainRulesBars=20 — статистика правил пересобирается заметно чаще, чем в первой версии (была 250). Стоит сравнить, насколько сильно меняется набор "хороших" правил между такими частыми переобучениями — если он скачет, edge, скорее всего, ловит шум, а не устойчивую закономерность.

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


Подводные камни

Редкие правила. Самая опасная ловушка — правило, которое почти не встречалось, но показало красивую статистику. Минимальная поддержка обязательна. Для короткой истории лучше меньше событий и короче правила.

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

Слишком много параметров. Если оптимизировать RSI-пороги, ATR-порог, горизонт, минимальное движение, длину правил и threshold одновременно, можно легко подогнать систему под тестер. Параметров должно быть немного, а проверка должна идти на отложенной истории.

Путаница вероятности и прибыли. Вероятность направления не равна прибыльности. Правило может чаще угадывать направление, но давать маленькую среднюю прибыль и редкие большие убытки. Поэтому после вероятностной валидации обязательно проверяется торговая статистика: profit factor, max drawdown, expected payoff, средняя прибыль, средний убыток и серии.

Заглядывание в будущее. Все события должны считаться только по данным бара shift и более старым данным. Метка может смотреть вперёд только на историческом обучении. На живом баре используется только последний закрытый бар shift=1.


Куда расширять

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

Раздельные правила для BUY и SELL-сетапов. Можно считать не просто FUTURE_UP/FUTURE_DOWN, а успешность конкретного торгового сценария: достигнут TP раньше SL или нет. Это ближе к реальной торговле, чем простое направление через N баров.

Свежесть правила. Старым наблюдениям можно давать меньший вес. Тогда система будет быстрее адаптироваться к смене режима, но станет чувствительнее к шуму.

Ансамбль с MLP. Ассоциативные правила можно объединить с нейросетью: MLP даёт непрерывную оценку направления, правила дают объяснимое статистическое подтверждение. Если оба фильтра согласны, вход сильнее. Если расходятся, лот можно уменьшить или пропустить сделку.

Отдельная таблица правил по сессиям. Для Forex это особенно полезно. Азия, Европа и Америка часто ведут себя по-разному. Вместо одного общего набора правил можно хранить отдельные счётчики по сессиям.


Заключение

Мы превратили идею «рынок как повторяющиеся состояния» в прикладной MQL5‑модуль: дискретизация признаков в события, кодировка битовой маской, сбор частот комбинаций и привязка к метке будущего движения. Важные защитные механизмы встроены по умолчанию: минимальная поддержка, сглаживание Лапласа и сравнение с базовой вероятностью. На выходе читатель получает готовые интерфейсы: TrainRules — сбор статистики на окне истории, GetRuleConfidence — оценка текущей маски в виде edge/confidence, RulesAccept(dir) — финальный gate для BUY/SELL. Обязательные практики перед боевой эксплуатацией: разделение истории по времени (70/30 train/validation), запрет перемешивания бара, тестирование с реальными ограничениями риска (фиксированные SL или тайм‑стопы) и длительный прогон по разным режимам рынка.

Помните: правила — не генератор сигналов, а подтверждающий фильтр; критерий готовности — стабильные правила с достаточной support и устойчивым преимуществом на валидации. Коротко: формализуем события и метки → считаем статистику и защищаем оценки → выбираем confidence на текущем баре → применяем RulesAccept(dir).


Название файла Описание файла
GridSurvivor_AssociationRules.mq5 Советник или фильтр с ассоциативными правилами и вероятностной оценкой направления на истории Forex
Прикрепленные файлы |
Создание пользовательских индикаторов в MQL5 (Часть 10): Дополнение футпринт-графика информационным блоком настроений по объёму по каждому бару Создание пользовательских индикаторов в MQL5 (Часть 10): Дополнение футпринт-графика информационным блоком настроений по объёму по каждому бару
В статье показано, как улучшить MQL5-индикатор футпринта: над каждой свечой добавляется компактный информационный блок, отражающий настроения по объёму и в котором отображаются чистая дельта, общий объём и процентное соотношение покупок и продаж. Мы реализуем сглаженную отрисовку с суперсэмплингом, скруглённые углы с помощью растеризации дуг и четырёхугольников, а также попиксельное альфа-смешивание. Вспомогательные инструменты включают преобразование цветов в ARGB, сканлайновую растеризацию и понижающую дискретизацию бокс-фильтром. Блок позволяет быстро оценивать настроения по объёму и остаётся разборчивым при разных уровнях масштаба.
Нейросети в трейдинге: От единственного прогноза к пространству рыночных сценариев (кодовая книга сценариев) Нейросети в трейдинге: От единственного прогноза к пространству рыночных сценариев (кодовая книга сценариев)
Статья описывает переход к многосценарному прогнозу на базе ORION: создаётся кодовая книга рыночных прототипов с EMA-памятью и слой, объединяющий генератор, роутер и оценку неопределённости. Такой модуль формирует траектории, их априорные вероятности и допустимый разброс, что позволяет учитывать альтернативные продолжения рынка при проектировании торговых решений.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Создание пользовательских индикаторов в MQL5 (Часть 9): Футпринт-график с отслеживанием объёма по ценовым уровням Создание пользовательских индикаторов в MQL5 (Часть 9): Футпринт-график с отслеживанием объёма по ценовым уровням
В этой статье мы создадим в MQL5 индикатор футпринта, который агрегирует потиковый объём по дискретизированным ценовым уровням и поддерживает режимы отображения Bid vs Ask (bid против ask) и Delta (дельта). Наложение на базе Canvas отрисовывает текст объёма с цветовым масштабированием, выравнивает его по свечам и обновляется на каждом тике. Вы узнаете, как сортировать ценовые уровни, нормализовать максимальные значения для цветового отображения и выполнять быструю перерисовку при масштабировании, прокрутке и изменении размера графика, чтобы анализировать распределение объёма и преобладание агрессивных покупателей или продавцов внутри каждого бара.