Детекторы и торговые паттерны¶
Здесь описано что именно считается аномалией с точки зрения рыночной микроструктуры: какие метрики, окна времени и пороги участвуют. Реализация — класс SignalDetector в модуле detector_core.py; числовые дефолты — в conf/detectors.yaml (плюс per_instrument и автоматические overrides).
Общая логика¶
Почти все «числовые» сигналы строятся одинаково:
- На каждый инструмент ведётся скользящая история последних значений метрики (размер буфера —
baseline_points, актуальные числа — вconf/detectors.yaml). - Раз в
sample_every_secondsсекунд (если пришло событие нужного типа) в историю добавляется новая точка — текущее значение метрики за окно. - Пока в истории меньше
min_baseline_pointsточек, сигнал не выдаётся (нужна хоть какая-то база для сравнения). - Базовое значение и «выбросность» считаются так: берётся среднее
baselineпо истории и стандартное отклонение по тем же точкам; z-score =(текущее значение − baseline) / std. Если std пренебрежимо мал, используется защита от деления на ноль (в коде большое или нулевое z в зависимости от сравнения с baseline). - Срабатывание: z-score ≥ порога из YAML для данного типа сигнала (
*_zscore_threshold). - Если в YAML задано
min_relative_metric_excursion> 0 и модульbaselineне меньше1e-9, дополнительно требуется относительное отклонение|metric − baseline| / |baseline|не ниже этого порога. Это отсекает ситуации «огромный z при почти нулевом std», когда метрика лишь чуть выше плоской базы. - Cooldown
alert_cooldown_seconds: между двумя алертами одного и того жеsignal_typeдля того же инструмента должно пройти не меньше заданного времени (чтобы не спамить при застрявшем выбросе).
Отдельно: сигнал комбо использует свои интервалы свежести и cooldown (combo_*), см. ниже.
Опционально в рантайме детектора: delivery policy (SIGNAL_DELIVERY_*) решает, отправлять ли сигнал во внешний webhook/Telegram. В Postgres/Kafka записываются все обогащённые сигналы; в payload добавляются delivery_status, delivery_reason и delivery_rule.
Текущий Telegram-gate намеренно строже записи в хранилище: volume_spike и trade_rate_spike требуют качества и z-score одновременно, price_jump требует extreme quality/z или подтверждения nearby trade activity, а standalone liquidity-сигналы подавляются без соседнего volume/trade/combo. SIGNAL_DELIVERY_MAX_PER_HOUR и SIGNAL_DELIVERY_INSTRUMENT_COOLDOWN_SECONDS проверяются по Postgres history и in-memory state, поэтому рестарт detector не сбрасывает throttle.
Человекочитаемая интерпретация¶
После detector-срабатывания слой enrichment добавляет в payload_json блок interpretation:
headline_ru— короткая строка для Telegram и таблицы админки: что именно произошло без чтения сырого payload.direction— направление или сторона, если оно применимо:up/down,bid/ask,long/short,buy/sell,activity,liquidity.facts— компактные парыlabel/value/keyдля Signal Cockpit detail.
Примеры:
| Signal | Что добавляется |
|---|---|
price_jump |
направление, start_price, current_price, price_change, price_change_pct, signed bps. |
volume_spike |
лоты, штуки/контракты через lot, оценочный оборот quantity * lot * price, последняя цена. |
trade_rate_spike |
число сделок, оборот окна, средний чек сделки. |
spread_widening |
bid/ask, spread в цене и bps, объёмы верхних уровней. |
orderbook_imbalance |
dominant side, bid/ask share, объёмы top-N уровней. |
microstructure_combo_* |
score/min_score и компоненты, которые дали баллы. |
Это не меняет пороги и количество сигналов: поля нужны для объяснимости в Telegram, webhook, API и админке.
Источники данных (типы событий потока)¶
Тип события (event_type) |
Торговый смысл | Что обновляет детектор |
|---|---|---|
trade |
Лента сделок | Окно сделок, цены, объёмы; частота дискретизации для объёма/числа сделок; дельта по направлению (buy/sell) для combo |
last_price |
Последняя цена без полной ленты | Только траектория цены в price_window → движение в bps |
orderbook |
Снимок стакана | Лучший bid/ask, объёмы на верхних уровнях → спред и имбаланс |
trading_status |
Режим торгов инструмента | Дискретное изменение статуса (аукцион, остановка и т.п. — в терминах API) |
open_interest |
ОИ по инструменту | История OI → open_interest_spike при ненулевом пороге |
candle |
Закрытая свеча | Диапазон свечи в bps → candle_range_spike при ненулевом пороге |
market_values |
Unary GetMarketValues (эмиттер / тот же raw-топик) |
Не даёт сигналов; обновляет последний снимок для unary_context в payload сигнала, если attach_unary_context_to_signals: true |
tech_analysis |
Unary GetTechAnalysis |
То же, что market_values |
Свечи из стрима (candle) участвуют в детекторе при включённой подписке; unary-снимки не заменяют стрим, а дополняют контекст при записи в общий raw-топик.
Список сигналов (signal_type)¶
1. volume_spike — всплеск объёма в ленте¶
Торговый паттерн: за короткий интервал наблюдается аномально высокий суммарный объём сделок в лотах относительно «обычного» поведения этого же инструмента на этом горизонте.
Метрика: сумма полей quantity по всем сделкам в окне trade_window_seconds (горизонт задаётся в YAML).
Порог: z-score ≥ volume_zscore_threshold (значение в YAML) и при необходимости условие min_relative_metric_excursion.
Интерпретация: часто связано с пробоем уровня, крупным участником, новостным импульсом или ликвидациями; само по себе не указывает направление — только на необычную активность.
2. trade_rate_spike — всплеск частоты сделок¶
Торговый паттерн: за то же окно trade_window_seconds проходит необычно много отдельных сделок (высокая «тиковая» активность), даже если суммарный объём в штуках других метрик умеренный.
Метрика: число записей сделок в окне (float для единообразия с историей).
Порог: z-score ≥ trade_count_zscore_threshold (значение в YAML).
Интерпретация: характерно для HFT/алгоритмической перестрелки, аукционной фазы, резкого сужения спреда с активным квотированием. В паре с объёмом помогает отличить «много мелких сделок» от «мало крупных».
3. price_jump — резкое ценовое движение в окне¶
Торговый паттерн: цена сдвинулась на необычно большую величину за горизонт price_window_seconds по сравнению с историей таких же движений.
Метрика: пусть (p_0) — самая ранняя цена в окне (первая точка в очереди price_points), (p_t) — текущая цена (из сделки или last_price). Тогда движение в базисных пунктах:
[ \text{move_bps} = \left| \frac{p_t - p_0}{p_0} \right| \times 10\,000 ]
Дополнительный фильтр: если задано price_move_absolute_threshold_bps > 0, то пока движение меньше этого порога, сигнал не рассматривается (отсекает шум до «экономически значимого» шага). Нуль в YAML — фильтр выключен. Автоматически порог часто подставляет Dagster (threshold_recalc_job, см. orchestration_defs.py) или legacy-tinvest-threshold-cron в detectors.overrides.yaml из средней почасовой волатильности.
Порог: z-score по истории значений move_bps ≥ price_return_zscore_threshold (значение в YAML).
Интерпретация: классический импульс / гэп внутри сессии / резкий рерайт котировок. Направление в summary не разводится отдельно — метрика симметрична по модулю.
4. spread_widening — расширение спреда¶
Торговый паттерн: разрыв между лучшим bid и ask (относительно середины котировки) стал статистически шире, чем в недавней истории для этого инструмента.
Метрика: по стакану берутся лучшие цены bid/ask; середина (mid = (bid+ask)/2). Спред в bps:
[ \text{spread_bps} = \frac{ask - bid}{mid} \times 10\,000 ]
Дискретизация — не на каждый тик стакана, а с шагом sample_every_seconds для канала orderbook.
Порог: z-score ≥ spread_zscore_threshold (значение в YAML).
Интерпретация: часто падает ликвидность, участники убирают лимитки перед новостью, повышается неопределённость, возможен рыночный стресс. Для отдельных инструментов широкий спред может быть нормой — поэтому используется именно относительный z-score к своей истории.
5. orderbook_imbalance — дисбаланс верхних уровней стакана¶
Торговый паттерн: на первых N уровнях bid и ask (order_book_depth_levels, по умолчанию 5) суммарный объём с одной стороны заметно перевешивает другую; дополнительно проверяется, что модуль дисбаланса достаточно велик по модулю до входа в z-score.
Метрика: суммируются количества на первых N bid и первых N ask; (B) и (A) — эти суммы, (T = B+A). Тогда:
[ \text{imbalance_abs} = \left| \frac{B - A}{T} \right| ]
(доля перевеса bid или ask от общего объёма на вершине книги). Отдельно в состоянии хранится доля (B/T) для combo.
Условие «аномальности»: сначала imbalance_abs ≥ imbalance_absolute_threshold (дефолт 0.65) — иначе даже большой z-score не считается торгово значимым.
Порог: z-score по истории imbalance_abs ≥ imbalance_zscore_threshold (значение в YAML).
Интерпретация: давление на цену со стороны лимитной ликвидности — перекос заявок у края книги; часто читают как краткосрочный бычий/медвежий уклон до исполнения противоположным потоком агрессоров. Это не ордерфлоу по сделкам, а именно снимок стакана.
5a. obi_dynamics — скачок OBI на глубине L2¶
Смысл: по первым N уровням стакана (order_book_depth_levels, по умолчанию 5) считается (OBI = (B-A)/(B+A)). Между дискретными сэмплами orderbook (как и для спреда/имбаланса — sample_every_seconds) измеряется (\Delta OBI); при (|\Delta|) выше obi_delta_absolute_threshold значение (|\Delta|) попадает в z-score историю.
Включение: obi_dynamics_enabled: true.
Замечание: сырой OBI зашумлён (отмены, мигание лимиток); в литературе 2024–2025 часто предлагают фильтрацию стакана и модели типа Hawkes/OFI — см. docs/research_log.md.
5b. orderbook_spoofing_bid_pull / orderbook_spoofing_ask_pull — эвристика «снятой стены»¶
Торговый паттерн: на двух последовательных снимках стакана (разница по времени ≤ spoofing_max_gap_seconds) с одной стороны была крупная стена относительно противоположной стороны (отношение объёмов L3 ≥ spoofing_wall_ratio и абсолют ≥ spoofing_min_wall_qty), затем объём этой стороны резко упал (доля снятия ≥ spoofing_qty_drop_ratio), при этом mid почти не сдвинулся (≤ spoofing_max_mid_move_bps).
Включение: spoofing_enabled: true в detectors.yaml (по умолчанию выключено).
Интерпретация: грубая подсказка на отмену крупных лимиток у лучшей цены (часто обсуждаемый сценарий «спуфинга» в широком смысле). Не детектирует направление сделки и не заменяет полноценный анализ L2/L3 с идентификаторами заявок — только эвристика по суммарным количествам трёх уровней.
5c. aggressive_trade_burst — пачка мелких принтов как один агрессор¶
Смысл: за окно trade_burst_window_ms (по умолчанию 100 мс) накапливаются направленные сделки (buy или sell по полю direction). Если число принтов ≥ trade_burst_min_trades и суммарный |qty| ≥ trade_burst_min_abs_qty, считаем, что прошёл синтетический агрессивный блок.
Включение: trade_burst_enabled: true (осторожно на шумных тикерах).
5d. lead_lag_divergence — лидер vs ведомый¶
Смысл: для пары (leader, follower) из YAML (lead_lag.pairs) сравнивается диапазон mid в bps за lead_lag_window_seconds: если у лидера движение ≥ lead_lag_leader_move_bps, а у ведомого ≤ lead_lag_follower_max_bps, на событии ведомого генерируется сигнал отставания.
Включение: lead_lag_enabled: true + заполненный блок lead_lag в detectors.yaml.
5e. open_interest_spike — всплеск открытого интереса (фьючерсы)¶
Источник: поток open_interest в нормализованных событиях (если брокер отдаёт).
Порог: z-score ≥ open_interest_zscore_threshold в YAML; 0 — детектор не обрабатывает этот тип.
5f. candle_range_spike — необычно широкая свеча¶
Источник: candle с включёнными свечами в instruments.yaml.
Метрика: ((high - low) / open \times 10\,000) bps.
Порог: z-score ≥ candle_range_zscore_threshold; 0 — выключено.
5g. price_near_limit_band — близость mid к лимиту дня¶
Источник: поля limit_up / limit_down в payload стакана.
Условие: расстояние mid до ближайшего лимита в bps ≤ limit_band_warning_bps (0 — выключено).
5h. orderbook_snapshot_inconsistent¶
Срабатывает при is_consistent: false в снимке стакана, если signal_orderbook_inconsistent: true в YAML.
5i. market_access_changed¶
Смена флагов limit_order_available_flag / market_order_available_flag в событии trading_status (если track_market_access_flags: true).
6. trading_status_changed — смена торгового статуса¶
Торговый паттерн: инструмент перешёл из одного режима торгов в другой (например, нормальные торги ↔ аукцион / ограничения — конкретные строки статуса приходят из API).
Метрика: дискретное событие (в сигнале metric_value условно 1.0, z-score 0).
Порог: нет z-score; действует обычный cooldown для этого типа сигнала.
Интерпретация: режимный риск, возможные гэпы после аукциона, ограничение маржинальной торговли и т.д. Полезно для фильтрации ложных ценовых/объёмных аномалий вокруг режимных переходов.
7. microstructure_combo_long / microstructure_combo_short¶
Торговый паттерн: композитный признак «напряжённой» микроструктуры: одновременно (в окне свежести combo_freshness_seconds) участвуют несколько факторов — широкий спред, высокая частота сделок, перекос стакана в сторону long или short, накопленная агрессивная дельта по ленте.
Включается флагом combo_enabled в YAML.
Компоненты и баллы (дефолты из detectors.yaml):
| Условие | Смысл | Баллы long | Баллы short |
|---|---|---|---|
Недавно был активен сигнал / состояние по spread_widening |
Спред разошёлся | combo_spread_points (1) |
те же |
Недавно был активен trade_rate_spike |
Высокая частота сделок | combo_tick_rate_points (2) |
те же |
Имбаланс активен и доля bid (B/T) ≥ combo_imbalance_long_threshold (0.80) |
Давление bid у верха книги | combo_imbalance_points (1) |
0 |
Имбаланс активен и (B/T) ≤ combo_imbalance_short_threshold (0.20) |
Давление ask | 0 | combo_imbalance_points (1) |
Сумма знаковых объёмов сделок в trade_window ≥ combo_delta_min_abs_qty |
Преобладание покупок агрессором | combo_delta_points (2) |
0 |
Та же сумма ≤ −combo_delta_min_abs_qty |
Преобладание продаж агрессором | 0 | combo_delta_points (2) |
«Недавно активен» для spread/tick_rate/imbalance означает: для соответствующего базового сигнала обновлялось last_active_at не позже чем combo_freshness_seconds назад от времени текущего события (не обязательно что был именно алерт — важно прохождение порога z-score на шаге вычисления).
Итог: если long_score ≥ combo_min_score (дефолт 6) и прошёл combo_alert_cooldown_seconds с прошлого combo-long — выдаётся microstructure_combo_long; аналогично для short.
Payload: в payload_json есть структурированный блок combo_detail: flags (какие условия реально сработали), points_awarded (сколько баллов дал каждый компонент), scores (long/short на момент оценки), thresholds (пороги из конфига), плюс imbalance_ratio, signed_delta_qty, freshness_seconds. Поля верхнего уровня (score, min_score, spread_active, …) дублируют ключевые значения для обратной совместимости и быстрых фильтров.
Интерпретация: это не самостоятельная торговая модель, а флаг необычного совпадения микроструктурных стрессов; может предшествовать движению или наоборот — быстро схлопываться. Направление long/short в названии — по правилам комбинации (стакан + дельта), а не прогноз цены.
Серьёзность (severity)¶
Для сигналов с ненулевым z-score глубина выброса мапится на целое severity (1–3): выше z → выше уровень (пороги в коде: примерно ≥6 → 3, ≥4 → 2, иначе 1). У дискретных событий (статус, combo с z=0) severity задаётся правилами конкретного типа.
Связь с конфигом и cron¶
- Все окна и пороги —
conf/detectors.yaml, точечные переопределения —per_instrumentтам же или вdetectors.overrides.yaml. price_move_absolute_threshold_bpsтипично поддерживает расписание Dagster (или legacy threshold-cron): под каждый инструмент подставляется масштаб волатильности по часовым свечам, чтобыprice_jumpне срабатывал на шум, типичный для данного тикера.
Ограничения (важно для трейдинга)¶
- Состояние целиком в памяти процесса детектора; при рестарте база z-score «обнуляется».
- Нет учёта сессии, новостного календаря, корреляций между инструментами — каждый тикер отдельно.
- Имбаланс и спред считаются по верхушке стакана (3 уровня) и лучшим ценам; глубокая книга не моделируется.
- Направление сделки для дельты зависит от поля направления в payload сделки; при отсутствии/неизвестном значении дельта в combo может не давать баллов.
Для правок формул и новых типов сигналов смотрите detector_core.py и тесты tests/test_detector.py.