Нужно сделать meta-labeling
Первичный алгоритм уже говорит LONG/SHORT по кроссу, а второй слой должен решить, стоит ли этот конкретный сигнал исполнять. Такой подход хорошо соответствует задаче повышения precision вторичным классификатором. (SSRN)
Главный вывод
Лучшее решение я вижу как трёхконтурный классификатор:
1. CHOP / WHIPSAW detector
Определяет вероятность, что сам кросс возник внутри пилы.
2. TREND continuation expert
Определяет качество обычного трендового кросса.
3. CAPITULATION / REVERSAL expert
Отдельно распознаёт ситуацию «сильная агрессия против будущего сигнала → цена перестала отвечать на агрессию → кросс обратно».
И уже затем:
[
PASS = CAPITULATION_PASS
\quad OR \quad
(\neg CHOP \land TREND_PASS)
]
То есть капитуляционный эксперт получает право отменять запрет диапазонного фильтра.
Именно этого сейчас не хватает вашей архитектуре.
—
1. Сначала я бы исправил несколько вещей в текущем v4
Есть вещи важнее оптимизации весов.
Критическая проблема №1 — исторический lookahead в медианах
Сейчас:
CopyRates(_Symbol, _Period, 2, InpMedLookback, r);
GetMedians() вообще не получает shift.
Это означает:
- для live-сигнала
shift=1 всё примерно логично;
- но когда вы пересчитываете старый сигнал, например
shift=80,
- его
medVol/medRange всё равно строятся по барам около текущего момента графика.
Следовательно, старый сигнал использует данные, которые для него находились в будущем.
Это чистый lookahead и он способен полностью испортить результаты оптимизации.
Нужно:
GetMedians(int signalShift, ...)
CopyRates(_Symbol, _Period, signalShift + 1, InpMedLookback, r);
Это я бы исправил до получения любого обучающего CSV.
—
2. CumDelta сейчас математически ориентирована назад
Посмотрите:
cumArr[0] = barDeltas[0];
for(int i = 1; i <= InpCDLookback; i++)
cumArr[i] = cumArr[i - 1] + barDeltas[i];
f.currentCum = cumArr[0];
При этом:
barDeltas[0] — сигнальный бар;
barDeltas[1] — предыдущий;
barDeltas[60] — самый старый.
Получается, вы строите CumDelta из настоящего назад в прошлое.
А затем считаете:
position =
(currentCum - minCum) / (maxCum-minCum)
В такой конструкции currentCum фактически равен одной дельте сигнального бара, а min/max относятся к суммированию в обратном времени.
Для вашей идеи:
агрессия вниз → экстремальная CumDelta → LONG
это особенно опасно: положение 0/1 может означать совсем не то, что предполагает комментарий.
Правильнее
Строить путь хронологически:
старейший бар
↓
...
↓
предыдущий
↓
сигнальный бар
Примерно:
double cum = 0;
for(int i=InpCDLookback; i>=0; --i)
{
cum += barDeltas[i];
...
}
currentCum = cum;
После этого position действительно означает положение текущей CumDelta относительно её траектории за последние 60 баров.
—
3. И дивергенция CumDelta вследствие этого тоже подозрительна
Сейчас:
double cumChg = cumArr[0] - cumArr[InpCDLookback];
Но при существующем направлении массива это примерно:
delta текущего бара
-
сумма текущего + предыдущих баров
то есть по сути отрицательная сумма прошлого участка.
Поэтому bullDiv/bearDiv я бы пока вообще не использовал как значимый фактор, пока CumDelta не переписана.
—
4. InpMinTrades на самом деле сейчас проверяет не количество сделок
У вас:
classified += vol;
А затем:
bool trustOF = (c.classified >= InpMinTrades);
При этом параметр называется:
InpMinTrades = 30; // мин. сделок
То есть сейчас это минимальный классифицированный объём, а не 30 trades.
Я бы хранил отдельно:
trade_count
classified_trade_count
classified_volume
flagged_trade_count
flagged_volume
и два качества:
[
FlagQuality_{count}=
\frac{flaggedTrades}{trades}
]
[
FlagQuality_{vol}=
\frac{flaggedVolume}{classifiedVolume}
]
Флаги BUY/SELL действительно обозначают направление сделки в MQL5; CopyTicksRange возвращает тики в хронологическом порядке. (MQL5)
—
5. У вас уже считается очень полезный признак — но нигде не используется
В CLiteCandle есть:
classified
flagged
и flagged увеличивается именно для биржевых BUY/SELL flags.
Это практически готовый:
exchange_flag_share
[
Q =
\frac{flagged}{classified}
]
Я бы включил его обязательно.
Например:
Q > 0.90 OF HIGH quality
0.60–0.90 medium
< 0.60 low
Но это начальные исследовательские зоны, не финальные пороги.
При низком Q модель должна меньше доверять trapped/absorption/initiative, построенным из tick-rule.
—
6. CSV сейчас не экспортирует полный вектор признаков
Сейчас в лог реально попадают:
- score;
- volRatio;
- dRatio;
- regime;
- cdPos;
- cdRev;
- cdDiv;
- cdVel;
- why;
- blk.
Хотя в EvaluateBar() рассчитываются:
- ini;
- absFor;
- absAgainst;
- divFor;
- divAgainst;
- effortFail;
- close_pos;
- push;
- pushRatio;
- minFast/maxFast;
- и другие.
Для настоящего исследования это нужно исправить первым делом.
Нельзя оптимизировать признаки, которых нет в датасете.
—
7. Самый сильный новый блок — признаки самой «пилы»
Я бы поставил их выше footprint по приоритету.
Потому что ваша болезнь возникает так:
fast
+
\ /
---0--- → многочисленные смены знака
/ \
-
Это структура самого генератора сигнала.
Я бы добавил минимум 8 признаков
7.1. CrossDensity
Количество переходов через 0 за последние W баров:
[
CrossDensity_W =
\frac{N_{cross,W}}{W}
]
Например W:
8
16
32
Это один из первых признаков, которые я проверил бы на участках:
- 14.08 14:00–14:30;
- 14.08 15:10–15:30.
—
7.2. BarsSinceCross
Сколько баров прошло после предыдущего кросса.
1–2 бара → подозрение на пилу
10–20 → совсем другой контекст
Именно как feature, а не hard debounce.
—
7.3. ZeroDwell
Какую долю последних W баров fast находилась около нуля:
[ZeroDwell = \ { |fast|<(\epsilon)}{W}]
Причём ε лучше сделать адаптивным:
[
\epsilon = k \cdot Median(|fast|)
]
а не абсолютным ±N.
—
7.4. ExcursionNormalized
Сейчас:
InpExcursionDepth = 25
Я бы отказался от абсолютного 25 в ML-векторе.
Лучше:
[
FastExcZ =
\frac{max|fast|}
{Median(|fast|,60)+\epsilon}
]
Глубокая нормализованная экскурсия гораздо стабильнее между разной волатильностью.
—
7.5. Price Efficiency Ratio
Очень сильный кандидат против диапазона:
[
ER_W =
\frac{|Close_t-Close_{t-W}|}
{\sum_{i=1}^{W}|Close_i-Close_{i-1}|}
]
В тренде:
ER → 1
В пиле:
ER → 0
Я бы считал:
ER8
ER16
ER32
—
7.6. Sign persistence
Например:
доля fast>0 за последние W
максимальная длина одной последовательности знака
—
7.7. Slow slope
Не только:
slow > 0
slow < 0
как сейчас.
А:
[
SlowSlope =
\frac{slow_t-slow_{t-k}}
{\sigma(slow)}
]
Плоская slow около нуля + много кроссов fast = очень хороший кандидат CHOP.
—
7.8. Fast/Slow separation
[
Sep =
\frac{|fast-slow|}
{rollingScaleFast}
]
Низкая separation + частые кроссы обычно совсем другой режим, чем импульсное расхождение линий.
—
8. Я бы полностью убрал статический debounce = 3 как обязательный фильтр
Сейчас каждый новый кросс в течение трёх баров уничтожается:
if(debounce)
passed = false;
и при этом s_last_signal_time=t обновляется для каждого кросса, независимо от того, прошёл предыдущий сигнал или был отфильтрован.
Это очень грубо.
Я бы сделал:
bars_since_cross
crosses_last_10
crosses_last_20
признаками модели.
А после обучения, если выяснится:
barsSinceCross <= 2
AND
crossDensity16 > X
AND
not capitulation
— тогда уже можно сделать оптимизированный debounce.
И обязательно:
Capitulation override.
Даже если предыдущий кросс был два бара назад:
CAP probability > Tcap
→ PASS
—
9. Капитуляцию я бы определял иначе
Ваша идея с push правильная, но одного CumDelta position мало.
Для LONG мне нужен примерно такой процесс:
1. Активные продажи
↓
2. Сильная отрицательная delta
↓
3. Цена первоначально падает
↓
4. Продажи продолжаются
↓
5. Но каждый следующий блок продаж даёт всё меньше движения вниз
↓
6. Возникают trapped sellers / absorption / reclaim
↓
7. Fast пересекает 0 вверх
Самый важный момент — пункт 5.
—
10. Новый сильный признак: Aggression → Price Impact Efficiency
Например для последних 6 баров:
[
ImpactEfficiency =
\frac{|\Delta Price|}
{|\Sigma Delta|}
]
или лучше последовательность:
impact1
impact2
...
impact6
Если:
sell delta огромная
а:
цена перестаёт двигаться вниз
это куда ближе к настоящей капитуляции, чем просто:
CumDelta < percentile25
Исследования микроструктуры действительно показывают сильную связь order-flow imbalance с краткосрочным движением цены, поэтому изменение эффективности воздействия потока на цену — логичное направление feature engineering. (arXiv)
—
11. Ещё лучше — считать marginal impact
Допустим:
бар -6 delta=-100 price=-8 ticks
бар -5 delta=-150 price=-7
бар -4 delta=-200 price=-5
бар -3 delta=-250 price=-3
бар -2 delta=-300 price=-1
Продажи усиливаются:
-100 → -300
а результат уменьшается:
-8 → -1 tick
Вот это я бы назвал почти идеальным кандидатом:
exhaustion / capitulation
Можно сделать:
[
ExhaustionSlope =
slope\left(
\frac{|return_i|}
{|delta_i|+\epsilon}
\right)
]
—
12. Push тоже нужно нормализовать иначе
Сейчас:
[
pushRatio =
\frac{|push|}
{span(CumDelta_{60})}
]
Я бы одновременно считал:
PushZ
[
PushZ =
\frac{\Sigma delta_6}
{Median(|\Sigma delta_6|)_{60}}
]
и:
DeltaRun
число подряд баров с delta против будущего сигнала
и:
DeltaAcceleration
delta6 > delta5 > delta4 ...
по абсолютной агрессии.
Так модель отличит:
один случайный отрицательный бар
от:
продолжительной капитуляции продавцов.
—
13. Отдельно нужен Reclaim
Для LONG:
агрессивная продажа была в нижней части footprint
+
close вернулся выше зоны агрессии
То есть не просто:
negative delta
а:
negative delta BELOW close
Это у вас частично уже есть через trapped sellers.
Я бы добавил количественный:
[
Reclaim =
\frac{Close-P_{aggrCentroid}}
{Range}
]
где P_aggrCentroid — центр масс sell aggression.
Для SHORT зеркально.
—
14. Stacked imbalance — да, но несколько иначе
Я бы не использовал просто:
есть / нет
А:
stack_count
max_stack
stack_volume_share
stack_location
stack_distance_from_close
Например для LONG-реверса особенно интересно:
3–5 уровней sell imbalance внизу
+
цена закрылась выше них
Это совершенно иной сигнал, чем:
sell imbalance внизу
+
close на low
—
15. Hard blockers я бы почти полностью убрал
Сейчас:
AbsAgainst
TrappedAgainst
являются абсолютным veto.
И:
hardBlock =
AbsAgainst ||
TrappedAgainst ||
...
Для ML-задачи это слишком рано.
Hard blockers должны быть только такие:
NoTicks
BrokenTicks
FutureDataDetected
InsufficientHistory
CorruptedPrice
invalid model inputs
очень низкое качество данных
А:
AbsAgainst
TrappedAgainst
CounterRegime
NoAggr
EffortFail
должны быть числовыми признаками.
Потому что, например:
AbsAgainst=0.65
может быть смертельным для одного трендового сигнала и совершенно нормальным в другом микро-контексте.
—
16. Архитектура модели, которую я бы выбрал
Не одна логрегрессия.
Gate 1 — DataQuality
Выход:
GOOD
DEGRADED
INVALID
—
Gate 2 — CHOP probability
[
P_{chop}
]
На признаках:
CrossDensity
BarsSinceCross
ZeroDwell
ER8/16/32
SlowSlope
FastSlowSep
ATR compression
range compression
—
Expert A — Trend
[
P_{trend-valid}
]
Признаки:
regime alignment
slow slope
price efficiency
delta3/delta6
volume impulse
initiative
close position
OF continuation
distance to structure
—
Expert B — Capitulation
[
P_{cap-valid}
]
Признаки:
opposite push Z
delta run
delta acceleration
fast excursion Z
CumDelta position
impact decay
trapped aggressors
absorption
reclaim
stacked extreme imbalances
delta reversal
—
17. Решение можно сделать очень понятным
Например:
if DataQuality == INVALID
FILTER
else if Pcap >= 0.72
PASS_CAP
else if Pchop >= 0.70
FILTER_CHOP
else if Ptrend >= 0.64
PASS_TREND
else
FILTER_LOW_CONF
0.72 / 0.70 / 0.64 здесь лишь пример структуры, не предлагаю сейчас считать их рабочими порогами.
Финальные значения должен дать out-of-sample.
—
18. Почему мне нравится эта схема больше единого boosting
Допустим одновременно:
crossDensity = очень плохой
но:
sellPush = экстремальный
impactDecay = экстремальный
trappedSellers = сильный
reclaim = сильный
Один score начинает торговаться сам с собой:
-1.5 chop
+0.7 delta
+1.0 trapped
-0.5 regime
...
И итог 0.95 vs threshold 1.0 фактически определяется случайным подбором весов.
В трёхэкспертной схеме смысл остаётся:
Да, рынок выглядит диапазонным. Но это отдельный высококачественный capitulation reversal, поэтому пропустить.
Именно это соответствует вашей задаче.
—
19. Какой ML я бы использовал
При 2–4 неделях данных я бы не начинал с нейросетей.
Количество независимых пересечений, скорее всего, намного меньше количества баров.
Benchmark 1
Regularized logistic regression.
Нужна как прозрачная базовая модель.
Benchmark 2
Gradient boosted trees:
LightGBM / XGBoost / CatBoost
с:
малой глубиной
сильной регуляризацией
ограничением числа деревьев
Benchmark 3
GAM / piecewise logistic model.
Потом сравнить.
Если boosting превосходит logreg совсем немного — я бы оставил более простую модель.
—
20. Но обучать надо не одну модель на всех сигналах
Я бы сначала детерминированно разделил кандидатов:
CAP candidate
TREND candidate
OTHER
Причём тип определяется только информацией до/на сигнальном баре, никогда исходом сделки.
После этого:
CAP expert → valid/invalid
TREND expert → valid/invalid
Это защитит редкие реверсы от доминирования обычных кроссов.
—
21. Метрика должна быть шире precision/recall
Ваш приоритет:
убрать серии ложных сигналов.
Стандартный precision этого полностью не отражает.
Предположим две модели:
A
5 false signals
в пяти разных днях.
B
5 false signals
подряд за 15 минут.
Precision одинаков.
Для торговли — совсем не одинаковый результат.
Я бы добавил:
FP_cluster_count
max_FP_streak
mean_FP_streak
FP_per_30min
worst_30min_FP
P95_FP_per_hour
И особенно:
Whipsaw survival rate
[
WSR =
1-\frac{FP_{filter}}
{FP_{base}}
]
на заранее размеченных chop-зонах.
—
22. Отдельная recall-метрика капитуляции
Общий Recall недостаточен.
Нужно три цифры:
Recall_all
Recall_trend
Recall_capitulation
И objective:
[
maximize Precision
]
при условиях, например:
[
Recall_{trend}\ge R_T
]
[
Recall_{cap}\ge R_C
]
Причём для вашей задачи R_C можно сделать очень высоким.
—
23. Я бы штрафовал серии FP прямо при подборе threshold
Например:
первый FP:
cost = 1
второй подряд:
cost = 2
третий:
cost = 3
четвёртый:
cost = 4
То есть objective будет сознательно предпочитать модель, которая гасит пилу целиком, вместо модели, убирающей случайные отдельные ложные входы.
—
24. Labeling я бы немного расширил
Ваше:
TP R ticks
SL S ticks
horizon N
хорошо.
Но в исследовательской таблице я бы оставлял ещё:
MFE_ticks
MAE_ticks
time_to_MFE
time_to_MAE
which_barrier_first
bars_to_barrier
И три исхода:
WIN
LOSS
TIMEOUT
А уже потом при необходимости:
WIN = 1
LOSS + TIMEOUT = 0
Почему?
Потому что:
сразу -S
и
0 движения за N баров
— совершенно разные типы плохого сигнала.
Модель может различать их.
—
25. Barrier label нужно считать по тикам
Особенно если R/S небольшие.
Если в одном баре:
High > TP
Low < SL
по OHLC вы не знаете, что произошло первым.
У вас уже есть тик-история, поэтому label можно считать строго:
entry tick
→ последующая последовательность ticks
→ первый touch R или S
Это убирает неоднозначность.
—
26. Entry тоже нужно формализовать
Сигнал известен только после закрытия бара.
Следовательно для реалистичной разметки:
signal confirmed at close
↓
entry = first tradable tick after close
а не:
entry = Close сигнального бара
Иначе появится небольшой, но систематический оптимизм.
—
27. Validation — только по времени
Случайный train/test split нельзя использовать как главный результат для такого датасета.
Я бы делал:
Train: дни 1–10
Validate: 11–13
Test: 14–16
затем
Train: 2–11
Validate: 12–14
Test: 15–17
...
И отдельный untouched holdout.
Для пересекающихся горизонтов меток полезно purge samples, чьи label intervals пересекают test interval; именно это предназначено для снижения утечки между train и validation. (SSRN)
—
28. Я бы обязательно валидировал «по дням»
Не только:
overall precision = 71%
а:
| День | signals | pass | TP | FP | precision | recall | CAP recall | max FP streak |
| —- | ——: | —: | -: | -: | ——–: | —–: | ———: | ————: |
Если:
80%
79%
77%
18%
83%
то средний результат скрывает нестабильность.
—
29. Сессионный POC/VAH/VAL — полезен, но есть ловушка
Если сигнал возник в 14:10, нельзя использовать:
POC всей сессии, рассчитанный в 20:00
Это lookahead.
Допустимы:
previous-session POC/VAH/VAL
или:
developing POC/VAH/VAL
рассчитанные только до 14:10.
—
30. Я бы добавил proximity к структуре
Не только абсолютное расстояние:
distance_to_POC
distance_to_VAH
distance_to_VAL
distance_to_previous_high
distance_to_previous_low
а signed directional distance.
Для LONG:
сколько тиков до ближайшего сопротивления
сколько пространства снизу уже отвергнуто
Полезный показатель:
[
RoomToTarget =
\frac{distance\ to\ next\ resistance}
{ATR}
]
Трендовый LONG прямо под VAH и LONG после reclaim VAL — это два разных контекста.
—
31. Time-of-day я бы использовал циклически
Не:
hour=14
а:
[
sin(2\pi t/T),
\quad
cos(2\pi t/T)
]
и отдельно категориальные зоны:
session_open
mid_session
lunch
evening
closing
Но только если на нескольких неделях подтвердится edge.
—
32. Если доступен настоящий DOM — это следующий уровень
Сейчас ваш footprint строится из совершённых сделок.
Настоящий L2 позволяет видеть ещё:
- добавление лимитной ликвидности;
- снятие ликвидности;
- bid/ask queue;
- depletion;
- refill;
- imbalance нескольких уровней.
Исследования показывают, что multi-level order-flow/queue imbalance может содержать дополнительную информацию о краткосрочном движении цены. (arXiv)
В MQL5 для поддерживаемых инструментов DOM можно получать через MarketBookAdd/MarketBookGet и OnBookEvent; при этом MetaQuotes прямо отмечает, что DOM доступен не для всех символов. (MQL5)
Если ваш биржевой инструмент отдаёт полноценный стакан, я бы добавил второй этап проекта:
queue imbalance
refill
cancel rate
depletion
microprice
multi-level OFI
Это будет содержательнее ещё одного RSI-фильтра.
—
33. Что я бы НЕ добавлял сейчас
Пока не стал бы усложнять:
RSI
Stochastic
MACD
десятки свечных паттернов
Hurst
нейросеть
Transformer
Основная проблема у вас хорошо определена.
Нужно дать модели признаки:
1. Почему произошёл кросс?
2. Рынок сейчас пилит или движется?
3. Кто был агрессором перед кроссом?
4. Получал ли агрессор результат?
5. Был ли он пойман?
—
34. Мой приоритет признаков
Я бы исследовал в таком порядке:
| Приоритет | Группа | Ожидаемая роль |
| 1 | CrossDensity / BarsSinceCross | непосредственно ловит пилу |
| 2 | ER8/16/32 | trend vs range |
| 3 | Fast excursion normalized | отличает настоящий возврат от шума около 0 |
| 4 | Slow slope / zero dwell | режим генератора |
| 5 | Delta push Z / delta run | капитуляция |
| 6 | Delta→price impact decay | истощение агрессора |
| 7 | Reclaim/trapped centroid | подтверждение реверса |
| 8 | Initiative/absorption | локальное OF подтверждение |
| 9 | developing profile | location |
| 10 | time of day | conditional prior |
| 11 | stacked imbalances | дополнительное подтверждение |
| 12 | DOM/MLOFI | если есть качественный L2 |
—
35. Как должен выглядеть итоговый production pipeline
CROSS
│
▼
┌──────────────┐
│ Data quality │
└──────┬───────┘
│
bad ──────┴──────► FILTER
│ good
▼
┌──────────────┐
│ CAP expert │
└──────┬───────┘
│
Pcap high?
│ │
YES NO
│ │
▼ ▼
PASS CAP ┌──────────┐
│ CHOP gate│
└────┬─────┘
│
chop high?
│ │
YES NO
│ │
▼ ▼
FILTER TREND
│
Ptrend high?
│ │
YES NO
│ │
▼ ▼
PASS FILTER
Это, на мой взгляд, значительно лучше одного линейного score.
—
36. Что бы я сделал с вашим кодом прямо сейчас
В таком порядке:
Этап 1 — исправить достоверность данных
- Исправить historical
GetMedians(shift).
- Полностью исправить направление CumDelta.
- Пересчитать CumDelta divergence.
- Разделить
trade_count и classified_volume.
- Добавить
flagged_share.
- Убрать арифметическое
t-period при запросе предыдущих баров и использовать реальные Time[shift+1], Time[shift+2], особенно вокруг разрывов/сессий.
- Логировать отсутствие/неполноту тиков отдельно от отрицательного торгового решения.
Этап 2 — сделать настоящий dataset logger
Не score, а raw features.
Этап 3 — добавить CHOP-блок
CrossDensity8/16/32
BarsSinceCross
ZeroDwell
ER8/16/32
FastExcZ
SlowSlopeZ
FastSlowSepZ
Этап 4 — добавить CAP-блок
PushZ
DeltaRun
DeltaAcceleration
ImpactEfficiency
ImpactDecay
Reclaim
AggressorCentroid
StackCount
Этап 5 — удалить рыночные hard blockers из research версии.
Этап 6 — собрать все crossings за 2–4 недели.
Этап 7 — построить logreg + boosting.
Этап 8 — отдельная оптимизация thresholds под:
Precision ↑
CAP recall ≈ максимально
Trend recall не хуже заданного значения
FP streak ↓↓↓
—
Самое важное
Я бы пока вообще не занимался подбором текущих +1.5, -2.0, +0.7, threshold=1.0. Сейчас это преждевременно.
В существующем v4 сначала нужно исправить временную корректность CumDelta/медиан и расширить CSV. После этого главным новым элементом сделать CHOP detector по геометрии кроссов, а капитуляцию вывести в отдельный expert, чтобы она могла пройти даже тогда, когда рынок формально выглядит диапазонным.
Именно такая архитектура, на мой взгляд, имеет наибольшие шансы решить обе ваши задачи одновременно: резко сократить серии ложных кроссов и не задушить редкие качественные реверсы.