Skip to content

Test Coverage Matrix — Trading Logic ​

ИСТОРИЧЕСКИЙ ДОКУМЕНТ. Матрица фиксирует прежнюю replay/dry-run/capture архитектуру и не задаёт текущие ворота после отказа от этих подсистем 2026-08-22. Действующие требования находятся в openspec/specs/; тесты реального order lifecycle и сохранность factual order trace остаются обязательными.

Перенесён из docs/operations/ в docs/specs/ в SSOT-конвергенции II (2026-05-20). Re-mapping rule-IDs на новый docs/strategies/directional-harvester.md. Архивные стратегии (binary-arb / bucket-arb / maker / maker-rebate / merge-arb / single-token-mm) удалены из таблиц per ADR-024 ACCEPTED 2026-05-13 — они вернутся как layers поверх extension API (ADR-025 PROPOSED), их rule-IDs будут пересобраны заново после стабилизации pm-engine.

Single source of truth для spec-driven тестов. Каждый rule-ID — business rule, выведенный из ADR / spec / CLAUDE.md / memory. Тест ссылается на rule-ID; failing test = баг в коде, не в тесте. Code-derived tautology запрещён — см. memory feedback_spec_driven_tests.

Условные обозначения ​

  • ID — стабильный идентификатор правила (G- global, DIR- directional/Harvester).
  • Rule — формулировка инварианта одной строкой, на русском.
  • Source — ADR-NNN, docs/strategies/directional-harvester.md §<секция>, CLAUDE.md "<block>" или memory:<entry>.
  • Expected — наблюдаемое поведение при соблюдении правила.
  • Negative case — наблюдаемое поведение при нарушении (fail-fast / skip / reject / no-op).
  • Test type — U unit, I integration scenario, P proptest, R replay 10× determinism, E e2e dry-run capture.
  • Test refs — TBD до первого маппинга.
  • Status — unmapped | partial | mapped | green | gap.

Group G: Global cross-strategy invariants ​

IDRuleSourceExpectedNegative caseTest typeTest refsStatus
G-3Dry-run использует тот же pipeline что и live, отличие — только OrderClient adapterADR-010SimOrderClient подменяется в DI, decision-граф идентичен liveDry-run минует валидацию → расхождение с live → банI + RTBDunmapped
G-4Decision enum обрабатывается exhaustive, без дефолтных ветокADR-007Стратегия реализует match по всем вариантам DecisionНовый вариант Decision без обновления стратегии → compile errorUTBDunmapped
G-5Один процесс pm-engine = одна активная стратегия (directional)ADR-024активная стратегия resolves один раз (config.db per ADR-015/024)Запуск с другой стратегией → fail-fast (per ADR-024 directional-only)UTBDunmapped
G-6Все [REQUIRED] env-ключи проверяются fail-fast при стартеCLAUDE.md "Fail-fast policy"Отсутствующий REQUIRED-ключ → panic с полным missing-key listМолчаливое значение по умолчанию → запрещеноUTBDunmapped
G-7BOT_STRATEGY_LABEL имеет приоритет над .env при выборе strategy configADR-015Когда LABEL задан, конфиг читается из config.db rowLABEL задан, но строки нет → fail-fast, не fallback на .envU + ITBDunmapped
G-8INV-3: при одновременно заданных wallet+strategy LABEL strategy.wallet_label == BOT_WALLET_LABELADR-015Запуск проходит только при совпадении labelsНесовпадение → fail-fastUTBDunmapped
G-9Webhook принимает только запросы с валидной HMAC-SHA256 подписьюADR-003Подпись над raw body с WEBHOOK_SECRET; mismatch → 401Запрос без X-Signature или с неверной подписью → 401U + ITBDunmapped
G-10Webhook идемпотентен по signal_idADR-003Повтор того же signal_id → 200 без повторного запуска scannerПовтор приводит к двойному ордеру → нарушениеITBDunmapped
G-11Webhook применяет IP allowlist если WEBHOOK_IP_ALLOWLIST непустADR-003Запрос с IP не из списка → 403 до проверки подписиПустой allowlist при включённом WEBHOOK_ENABLED → bind на loopback по умолчаниюUTBDunmapped
G-12RTDS детектирует toxic-flow окно по правилам из ADR-004ADR-004Окно открывается при срабатывании детектора и закрывается по timeoutFalse positive шторм на любые измененияU + PTBDunmapped
G-13Active-cancel при toxic-flow выполняется только если RTDS_TOXIC_FLOW_ACTIVE_CANCEL_ENABLED=trueADR-004LOG_ONLY=true → emit метрика, ордера живутActive-cancel срабатывает в LOG_ONLY режиме → нарушениеITBDunmapped
G-14Replay сессии 10 раз подряд даёт одинаковый SHA-256 артефактов (СНЯТО 2026-08-21 вместе с бэктестом)ADR-009CI parity gate сравнивает hash 10 прогоновРасхождение → CI failRTBDunmapped
G-15Capture фиксирует все эмитированные Decision (в т.ч. Hold/Skip)ADR-009Каждый Decision сериализуется в JSONL.zstdDecision эмитен в live, но не в capture → пропуск трассыETBDunmapped
G-17neg_risk gating консистентен между фильтром рынка и стратегиейdocs/strategies/directional-harvester.md §Целевые рынкиГейт neg_risk настраивается per-strategy; стратегия не получает рынок с несовместимым флагомСтратегия получает рынок несовместимый с её gate → skip + warnUTBDunmapped
G-18Merge/Redeem для neg_risk идёт через NegRiskAdapter.mergePositions/redeemPositions, НЕ через standard CTFmemory:neg_risk_merge_path + docs/strategies/directional-harvester.md §Целевые рынкиneg_risk=true → tx через NegRiskAdapter (bytes32,uint256)Использован CTF на neg_risk → on-chain revertITBDunmapped
G-19Redeem на резолвлении проходит через корректный adapter (CTF vs NegRiskAdapter)memory:neg_risk_merge_pathВыбор adapter зависит от neg_risk флага рынкаWrong-adapter call → tx revert и пропуск выплатыITBDunmapped
G-20Бюджет на рынок резервируется атомарно (no double-spend между тиками)docs/strategies/directional-harvester.md §Целевая прибыль и риск (budget_per_market)Резерв через единую budget-предикату до отправки ордераГонка двух тиков → суммарный allocation > budgetU + PTBDunmapped
G-22Перед placement стратегия проверяет accepting_orders=true через ClosureWatcherdocs/strategies/directional-harvester.md §Reactive closure detection (ClosureWatcher)is_closed(asset_id) сверяется до entry; accepting_orders=false или closed → skipОрдер уходит на closed asset → CLOB reject + noisy retryUTBDunmapped
G-23Размер ордера ≥ MIN_SIZE_SHARES (5 акций — CLOB rule)docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 6, min_size_shares = 5)Расчётный size <5 → no-op skipОрдер на size=4 → CLOB reject 400UTBDunmapped
G-25Recovery восстанавливает open orders + open positions из CLOB/journal при рестартеADR-007После рестарта стратегия видит свои открытые позиции/ордера и не дублирует ихRecovery игнорирует open state → дубль ордеров и over-allocationITBDunmapped
G-26Запись fill в journal происходит до вставки нового ордера (race-safety)ADR-007Fill зафиксирован → следующий тик видит обновлённый stateInsert order перед записью fill → возможен over-budgetUTBDunmapped
G-28Stale book → стратегия пропускает тик, без принятия решения по устаревшим даннымADR-007 + ADR-022Возраст snapshot > порога → Decision::HoldРешение по stale book → mispricingUTBDunmapped
G-29Каждое Decision логируется с trace_id для последующей корреляцииADR-009trace_id присутствует в JSONL и в логахDecision без trace_id → невозможно сопоставить capture с liveETBDunmapped
G-30SymbolicClock монотонно возрастает без скачков (реплей снят 2026-08-21; часы сохранены для тестов)ADR-009Каждое следующее событие имеет ts >= предыдущегоНазад во времени → replay-парность ломаетсяR + PTBDunmapped
G-31Schema конфигов имеет version и фейлится на несовместимом значенииADR-015На несовместимой версии бот не стартуетSilent миграция → запрещеноUTBDunmapped
G-32На горячем пути JSON-сериализация только через sonic-rs (не serde_json)memory:sonic_rsHot path code использует sonic_rs::to_string/from_strserde_json в hot path → CI grep gate падаетUTBDunmapped

Group DIR: directional / Harvester ​

Источник правды: docs/strategies/directional-harvester.md. Дополнительные источники указаны в колонке Source.

IDRuleSourceExpectedNegative caseTest typeTest refsStatus
DIR-1Стратегия применяется к binary рынкам (market.tokens.len() == 2); поддерживаются и standard CTF, и neg-risk-wrappeddocs/strategies/directional-harvester.md §Целевые рынкиMulti-outcome без бинарной обёртки → отфильтрован; neg_risk-флаг — конфигурируемый gate, не hardcoded skipMulti-outcome рынок проходит в стратегию → нарушение domainUTBDunmapped
DIR-2Стратегия торгует одной стороной (single-side YES OR NO) на одном маркете за раз, без перекрёстных хеджейdocs/strategies/directional-harvester.md §КонтекстНа любой момент времени по одному cid открыта позиция только на одном из двух токеновОткрытие позиции на обоих токенах одного cid одновременно → нарушениеUTBDunmapped
DIR-3Выбор стороны входа определяется probability-моделью winning outcomedocs/strategies/directional-harvester.md §Product Vision §1Сторона = winning outcome по probability-моделиПокупка losing outcome → нарушениеU + PTBDunmapped
DIR-4Вход разрешён только если probability winning outcome ≥ 0.95docs/strategies/directional-harvester.md §Product Vision §1 + §Целевая прибыль и рискprob < 0.95 → skipВход при prob ниже порога → нарушениеUTBDunmapped
DIR-5Вход разрешён только если best_ask ∈ [0.95, 0.98]docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 1)best_ask вне полосы → skipВход при ask=0.99 или ask=0.94 → нарушениеUTBDunmapped
DIR-6Максимальная цена fill = 0.98 (DIRECTIONAL_MAX_ENTRY_PRICE)docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 3)fill_price > 0.98 → не входимФактический fill выше cap → нарушениеUTBDunmapped
DIR-7Внутри полосы [0.95, 0.98] действует cheapest-first preference; периодическая discovery использует per-tick warmup buffer, sorted ascending по best_ask, greedy-budget-bounded под canonical RISK-LIMIT 3 (total_budget_micro_usd в strategies.definition)docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 1) + capability directional-entry-bufferИз двух одновременно admitted кандидатов первым выбирается тот, у которого best_ask меньшеПроизвольный порядок выбора → нарушениеUTBDunmapped
DIR-8Entry-window gate driven by capability outcome-tradability-detection: finish_type ∈ {EndDate, GstOffset, GstLocalMidnight} + optional entry_window_seconds (too-early) + late_entry_seconds (too-late)docs/strategies/directional-harvester.md §Anchor source и entry-window gatesecs_to_anchor > entry_window_seconds → RefuseTooEarly; secs_to_anchor < late_entry_seconds → RefuseTooLate; оба None → PredictiveGateDisabledВход в рынок с late_entry_seconds=3600 и secs_to_anchor=60 → нарушениеUTBDunmapped
DIR-9budget_per_market ограничивает суммарную экспозицию по одному рынку (live: $5.00)docs/strategies/directional-harvester.md §Целевая прибыль и риск + §Жизненный цикл сделки (инвариант 6)Σ(allocations на market_id) ≤ budget_per_marketПревышение лимита → over-allocationU + PTBDunmapped
DIR-10Размер ордера ≥ min_size_shares = 5 (CLOB rule)docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 6)size < 5 → skipОрдер на 4 акции → CLOB rejectUTBDunmapped
DIR-11После первого fill публикуется SELL @ exit_price = 0.99 для total накопленной позиции согласно execution.target_exitdocs/strategies/directional-harvester.md §Жизненный цикл сделкиПервый fill → intent с target price/size и точной policy ролироль игнорирует policy или exit не выставлен → нарушениеU + Irole-intent testsmapped
DIR-12Параметры профиля Harvester: вход [0.95, 0.98], exit GTC @ 0.99, stop-loss bid threshold 0.80, cooldown 5h, debounce 5sdocs/strategies/directional-harvester.md §Жизненный цикл сделки (инварианты 1, 5, 9)Параметры применяются именно так с 2026-05-16 (тогда инстанс harvester-live на pm-bot; с 2026-07-22 боевой инстанс — harvester-engine)Иные числа → расхождение с authoritative VPS specUTBDunmapped
DIR-13evaluate_periodic сканирует MarketCache каждые BOT_FALLBACK_SCAN_SECS для discovery + 10-минутный redeem backstopdocs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 8) + memory:directional_periodic_scan_implementedPeriodic tick срабатывает; backstop эмитит Decision::Redeem для cids с filled_at старше 600s, отсутствующих в active_markets()Periodic scan молчит → Harvester не находит рынки / застрявшие позиции не редимятсяITBDunmapped
DIR-14Stop-loss config-driven (ADR-027): непрерывный пробой выполняет one-owner handoff и SELL @ current best_bid согласно execution.stop_exitdocs/strategies/directional-harvester.md §Жизненный цикл сделкиTrigger/debounce не меняются; order form точно соответствует policy ролиhard-coded FAK/FOK или обход owner/inventory guard → нарушениеU + Idirectional role-intent testsmapped
DIR-15DCA включено: на каждый fill — additional BUY в пределах budget_per_market; exit пересчитывается на total accumulated position (recompute_exit_on_fill = true)docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 6)Top-up BUY эмитится после fill пока остаётся budget; exit GTC переразмещается на новый total sizeDCA отсутствует при остатке budget или exit не пересчитан → нарушениеU + ITBDunmapped
DIR-16DCA top-up не превышает budget_per_market (общий cap включая первоначальный fill)docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 6)Суммарная экспозиция после DCA ≤ budget_per_marketDCA превышает budget → нарушение G-20U + PTBDunmapped
DIR-17Sweep / discovery режим: без webhook сигналов стратегия находит подходящие рынки через evaluate_periodic + market filter (MARKET_TAG_FILTER=103040 для weather operationally)docs/strategies/directional-harvester.md §Жизненный цикл сделки + инвариант 10Periodic scan находит admissible markets без external триггераDiscovery молчит без webhook → нарушениеITBDunmapped
DIR-18Entry-debounce ADR-030 deferred: capability and knobs are absent from current pm-enginedocs/strategies/directional-harvester.md §ADR-030Typed config, form and runtime contain no debounce mirror; a future return requires a new capabilityA dead/hidden debounce key is accepted or behavior is claimed live without codeU + RTBDunmapped
DIR-19Market filter (MARKET_TAG_FILTER / per-strategy filters) применяется до entry-логикиdocs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 10)Не подходящий market filter → skipИгнорирование фильтра → шумные ордераUTBDunmapped
DIR-20Auto-redeem hybrid: WS-event-driven primary path (Strategy::on_market_resolved → Decision::Redeem) + 10-минутный periodic backstopdocs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 8)Resolved позиция редимится либо через WS-event, либо через backstop; обе ветки идемпотентныResolved позиция не редимится → потеря выплатыITBDunmapped
DIR-21Recovery: open positions / exit GTC ордера / PendingEntry восстанавливаются после рестарта без дублейdocs/strategies/directional-harvester.md §Жизненный цикл сделки + ADR-007TP-ордер не дублируется после рестарта; shadow-map восстановленДубль TP → over-sellITBDunmapped
DIR-22Webhook seed запускает directional re-evaluation для целевого рынкаADR-003 + docs/strategies/directional-harvester.md §Текущие параметрыСигнал триггерит вход или DCA если admission gates пройденыСигнал игнорируется → нарушеноITBDunmapped
DIR-23Toxic-flow active-cancel отменяет именно exit GTC ордер (если active-cancel включён)ADR-004Cancel применяется к exit GTC SELL @ 0.99; holdings (актив) остаютсяCancel закрывает позицию рыночно → нарушениеITBDunmapped
DIR-24На CLOB-reject стратегия применяет backoff, не выставляет тот же ордер немедленноADR-007Reject → задержка перед retryБесконечный retry → rate-limit banUTBDunmapped
DIR-25Stale book → tick пропускается, Decision::HoldADR-007 + ADR-022 (unified OrderbookStore с timestamp guard)Стратегия читает orderbook через OrderbookStore::full_orderbook(); возраст snapshot > порога → HoldВход по stale → mispricingUTBDunmapped
DIR-26Decision::Hold не приводит к сайд-эффектамADR-007Никаких ордеров/cancel'ов на HoldHold пишет в DB или CLOB → нарушениеUTBDunmapped
DIR-27Dry-run = live pipeline (SimOrderClient swap)ADR-010 + docs/strategies/directional-harvester.md §Target BehaviorDecision-граф идентичен live; режим определяется bound адаптером OrderRouterDry-run skip-ает гейты → расхождение с liveI + RTBDunmapped
DIR-28Capture фиксирует все Decision-ы, включая HoldADR-009Все Decision сериализуютсяHold выпадает из capture → пропуск трассыETBDunmapped
DIR-29Hot path сериализация только через sonic-rsmemory:sonic_rssonic_rs в hot pathserde_json в decision-сериализации → grep gate failUTBDunmapped
DIR-30Replay determinism 10× для directional/Harvester (СНЯТО 2026-08-21 вместе с бэктестом)ADR-009SHA-256 идентичен 10 раз подрядЛюбое расхождение → CI failRTBDunmapped
DIR-31Каждая из пяти ролей имеет обязательную tagged execution-policy: limit_gtc, limit_gtd {post_only,ttl_seconds>=180}, fak, fok. Только BUY taker-capable роли требуют свой fee cap; глобальных execution-ключей нет. Live GTD wire = now + 60 + N.docs/strategies/directional-harvester.md + capabilities directional-order-execution-policy, directional-order-ttlresolver точно выдаёт четыре CLOB-формы; отсутствие роли/TTL/cap и мёртвые поля fail-fasthard-coded tuple, inheritance, скрытый default или незаконная комбинация → violationU + Iresolver/config/schema/form/WatchArm FOK testsmapped

Сводка ​

ГруппаКол-во правилИсточник spec
G (global)25ADR-003/004/007/009/010/015/022/024, CLAUDE.md, memory
DIR (directional/Harvester)32docs/strategies/directional-harvester.md + ADR-027/030/031 + memory
Итого57—

Архивные группы (BA bucket-arb / MK maker / MR maker-rebate / MA merge-arb / single-token-mm) удалены целиком per ADR-024 — 108 rule-IDs убраны из active scope. Они вернутся как layers поверх extension API после стабилизации pm-engine (ADR-025 PROPOSED, см. backlog в docs/tests/backlog/restore-*-as-layer.md).

Spec gaps (агрегировано) ​

  • Numeric defaults для entry_window_seconds / late_entry_seconds per anchor source — capability spec не фиксирует, правило DIR-8 проверяет монотонность.
  • Паритетные ворота CI сняты 2026-08-21 вместе с бэктестом; детерминизм РЕШЕНИЙ держит crates/pm-engine/tests/it/directional_decision_trace_golden.rs.
  • GAP-01 (shares_per_market_max) и GAP-03 (reinvest_realized_pnl surfacing) реализованы. GAP-02 (% от баланса кошелька) отменён 2026-07-28 — лимита нет, покрытие не требуется.
  • Stop-loss FOK slippage trade-off — наблюдаемо до 1¢-44¢ ниже threshold на gap-down, принято за гарантию исполнения.

Версия и статус ​

  • Версия документа: v2 (2026-05-20, SSOT-конвергенция II).
  • Все правила в статусе unmapped — следующий шаг QA Lead: проставить Test refs к существующим тестам и обновить статус.
  • Любое изменение spec (новый ADR, обновление directional-harvester.md) обязано обновить соответствующий rule-ID и/или добавить новый ID, но не переиспользовать удалённый ID.