Тема
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 —
Uunit,Iintegration scenario,Pproptest,Rreplay 10× determinism,Ee2e dry-run capture. - Test refs —
TBDдо первого маппинга. - Status —
unmapped|partial|mapped|green|gap.
Group G: Global cross-strategy invariants
| ID | Rule | Source | Expected | Negative case | Test type | Test refs | Status |
|---|---|---|---|---|---|---|---|
| G-3 | Dry-run использует тот же pipeline что и live, отличие — только OrderClient adapter | ADR-010 | SimOrderClient подменяется в DI, decision-граф идентичен live | Dry-run минует валидацию → расхождение с live → бан | I + R | TBD | unmapped |
| G-4 | Decision enum обрабатывается exhaustive, без дефолтных веток | ADR-007 | Стратегия реализует match по всем вариантам Decision | Новый вариант Decision без обновления стратегии → compile error | U | TBD | unmapped |
| G-5 | Один процесс pm-engine = одна активная стратегия (directional) | ADR-024 | активная стратегия resolves один раз (config.db per ADR-015/024) | Запуск с другой стратегией → fail-fast (per ADR-024 directional-only) | U | TBD | unmapped |
| G-6 | Все [REQUIRED] env-ключи проверяются fail-fast при старте | CLAUDE.md "Fail-fast policy" | Отсутствующий REQUIRED-ключ → panic с полным missing-key list | Молчаливое значение по умолчанию → запрещено | U | TBD | unmapped |
| G-7 | BOT_STRATEGY_LABEL имеет приоритет над .env при выборе strategy config | ADR-015 | Когда LABEL задан, конфиг читается из config.db row | LABEL задан, но строки нет → fail-fast, не fallback на .env | U + I | TBD | unmapped |
| G-8 | INV-3: при одновременно заданных wallet+strategy LABEL strategy.wallet_label == BOT_WALLET_LABEL | ADR-015 | Запуск проходит только при совпадении labels | Несовпадение → fail-fast | U | TBD | unmapped |
| G-9 | Webhook принимает только запросы с валидной HMAC-SHA256 подписью | ADR-003 | Подпись над raw body с WEBHOOK_SECRET; mismatch → 401 | Запрос без X-Signature или с неверной подписью → 401 | U + I | TBD | unmapped |
| G-10 | Webhook идемпотентен по signal_id | ADR-003 | Повтор того же signal_id → 200 без повторного запуска scanner | Повтор приводит к двойному ордеру → нарушение | I | TBD | unmapped |
| G-11 | Webhook применяет IP allowlist если WEBHOOK_IP_ALLOWLIST непуст | ADR-003 | Запрос с IP не из списка → 403 до проверки подписи | Пустой allowlist при включённом WEBHOOK_ENABLED → bind на loopback по умолчанию | U | TBD | unmapped |
| G-12 | RTDS детектирует toxic-flow окно по правилам из ADR-004 | ADR-004 | Окно открывается при срабатывании детектора и закрывается по timeout | False positive шторм на любые изменения | U + P | TBD | unmapped |
| G-13 | Active-cancel при toxic-flow выполняется только если RTDS_TOXIC_FLOW_ACTIVE_CANCEL_ENABLED=true | ADR-004 | LOG_ONLY=true → emit метрика, ордера живут | Active-cancel срабатывает в LOG_ONLY режиме → нарушение | I | TBD | unmapped |
| G-14 | Replay сессии 10 раз подряд даёт одинаковый SHA-256 артефактов (СНЯТО 2026-08-21 вместе с бэктестом) | ADR-009 | CI parity gate сравнивает hash 10 прогонов | Расхождение → CI fail | R | TBD | unmapped |
| G-15 | Capture фиксирует все эмитированные Decision (в т.ч. Hold/Skip) | ADR-009 | Каждый Decision сериализуется в JSONL.zstd | Decision эмитен в live, но не в capture → пропуск трассы | E | TBD | unmapped |
| G-17 | neg_risk gating консистентен между фильтром рынка и стратегией | docs/strategies/directional-harvester.md §Целевые рынки | Гейт neg_risk настраивается per-strategy; стратегия не получает рынок с несовместимым флагом | Стратегия получает рынок несовместимый с её gate → skip + warn | U | TBD | unmapped |
| G-18 | Merge/Redeem для neg_risk идёт через NegRiskAdapter.mergePositions/redeemPositions, НЕ через standard CTF | memory:neg_risk_merge_path + docs/strategies/directional-harvester.md §Целевые рынки | neg_risk=true → tx через NegRiskAdapter (bytes32,uint256) | Использован CTF на neg_risk → on-chain revert | I | TBD | unmapped |
| G-19 | Redeem на резолвлении проходит через корректный adapter (CTF vs NegRiskAdapter) | memory:neg_risk_merge_path | Выбор adapter зависит от neg_risk флага рынка | Wrong-adapter call → tx revert и пропуск выплаты | I | TBD | unmapped |
| G-20 | Бюджет на рынок резервируется атомарно (no double-spend между тиками) | docs/strategies/directional-harvester.md §Целевая прибыль и риск (budget_per_market) | Резерв через единую budget-предикату до отправки ордера | Гонка двух тиков → суммарный allocation > budget | U + P | TBD | unmapped |
| G-22 | Перед placement стратегия проверяет accepting_orders=true через ClosureWatcher | docs/strategies/directional-harvester.md §Reactive closure detection (ClosureWatcher) | is_closed(asset_id) сверяется до entry; accepting_orders=false или closed → skip | Ордер уходит на closed asset → CLOB reject + noisy retry | U | TBD | unmapped |
| 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 400 | U | TBD | unmapped |
| G-25 | Recovery восстанавливает open orders + open positions из CLOB/journal при рестарте | ADR-007 | После рестарта стратегия видит свои открытые позиции/ордера и не дублирует их | Recovery игнорирует open state → дубль ордеров и over-allocation | I | TBD | unmapped |
| G-26 | Запись fill в journal происходит до вставки нового ордера (race-safety) | ADR-007 | Fill зафиксирован → следующий тик видит обновлённый state | Insert order перед записью fill → возможен over-budget | U | TBD | unmapped |
| G-28 | Stale book → стратегия пропускает тик, без принятия решения по устаревшим данным | ADR-007 + ADR-022 | Возраст snapshot > порога → Decision::Hold | Решение по stale book → mispricing | U | TBD | unmapped |
| G-29 | Каждое Decision логируется с trace_id для последующей корреляции | ADR-009 | trace_id присутствует в JSONL и в логах | Decision без trace_id → невозможно сопоставить capture с live | E | TBD | unmapped |
| G-30 | SymbolicClock монотонно возрастает без скачков (реплей снят 2026-08-21; часы сохранены для тестов) | ADR-009 | Каждое следующее событие имеет ts >= предыдущего | Назад во времени → replay-парность ломается | R + P | TBD | unmapped |
| G-31 | Schema конфигов имеет version и фейлится на несовместимом значении | ADR-015 | На несовместимой версии бот не стартует | Silent миграция → запрещено | U | TBD | unmapped |
| G-32 | На горячем пути JSON-сериализация только через sonic-rs (не serde_json) | memory:sonic_rs | Hot path code использует sonic_rs::to_string/from_str | serde_json в hot path → CI grep gate падает | U | TBD | unmapped |
Group DIR: directional / Harvester
Источник правды: docs/strategies/directional-harvester.md. Дополнительные источники указаны в колонке Source.
| ID | Rule | Source | Expected | Negative case | Test type | Test refs | Status |
|---|---|---|---|---|---|---|---|
| DIR-1 | Стратегия применяется к binary рынкам (market.tokens.len() == 2); поддерживаются и standard CTF, и neg-risk-wrapped | docs/strategies/directional-harvester.md §Целевые рынки | Multi-outcome без бинарной обёртки → отфильтрован; neg_risk-флаг — конфигурируемый gate, не hardcoded skip | Multi-outcome рынок проходит в стратегию → нарушение domain | U | TBD | unmapped |
| DIR-2 | Стратегия торгует одной стороной (single-side YES OR NO) на одном маркете за раз, без перекрёстных хеджей | docs/strategies/directional-harvester.md §Контекст | На любой момент времени по одному cid открыта позиция только на одном из двух токенов | Открытие позиции на обоих токенах одного cid одновременно → нарушение | U | TBD | unmapped |
| DIR-3 | Выбор стороны входа определяется probability-моделью winning outcome | docs/strategies/directional-harvester.md §Product Vision §1 | Сторона = winning outcome по probability-модели | Покупка losing outcome → нарушение | U + P | TBD | unmapped |
| DIR-4 | Вход разрешён только если probability winning outcome ≥ 0.95 | docs/strategies/directional-harvester.md §Product Vision §1 + §Целевая прибыль и риск | prob < 0.95 → skip | Вход при prob ниже порога → нарушение | U | TBD | unmapped |
| DIR-5 | Вход разрешён только если best_ask ∈ [0.95, 0.98] | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 1) | best_ask вне полосы → skip | Вход при ask=0.99 или ask=0.94 → нарушение | U | TBD | unmapped |
| DIR-6 | Максимальная цена fill = 0.98 (DIRECTIONAL_MAX_ENTRY_PRICE) | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 3) | fill_price > 0.98 → не входим | Фактический fill выше cap → нарушение | U | TBD | unmapped |
| 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 меньше | Произвольный порядок выбора → нарушение | U | TBD | unmapped |
| DIR-8 | Entry-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 gate | secs_to_anchor > entry_window_seconds → RefuseTooEarly; secs_to_anchor < late_entry_seconds → RefuseTooLate; оба None → PredictiveGateDisabled | Вход в рынок с late_entry_seconds=3600 и secs_to_anchor=60 → нарушение | U | TBD | unmapped |
| DIR-9 | budget_per_market ограничивает суммарную экспозицию по одному рынку (live: $5.00) | docs/strategies/directional-harvester.md §Целевая прибыль и риск + §Жизненный цикл сделки (инвариант 6) | Σ(allocations на market_id) ≤ budget_per_market | Превышение лимита → over-allocation | U + P | TBD | unmapped |
| DIR-10 | Размер ордера ≥ min_size_shares = 5 (CLOB rule) | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 6) | size < 5 → skip | Ордер на 4 акции → CLOB reject | U | TBD | unmapped |
| DIR-11 | После первого fill публикуется SELL @ exit_price = 0.99 для total накопленной позиции согласно execution.target_exit | docs/strategies/directional-harvester.md §Жизненный цикл сделки | Первый fill → intent с target price/size и точной policy роли | роль игнорирует policy или exit не выставлен → нарушение | U + I | role-intent tests | mapped |
| DIR-12 | Параметры профиля Harvester: вход [0.95, 0.98], exit GTC @ 0.99, stop-loss bid threshold 0.80, cooldown 5h, debounce 5s | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инварианты 1, 5, 9) | Параметры применяются именно так с 2026-05-16 (тогда инстанс harvester-live на pm-bot; с 2026-07-22 боевой инстанс — harvester-engine) | Иные числа → расхождение с authoritative VPS spec | U | TBD | unmapped |
| DIR-13 | evaluate_periodic сканирует MarketCache каждые BOT_FALLBACK_SCAN_SECS для discovery + 10-минутный redeem backstop | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 8) + memory:directional_periodic_scan_implemented | Periodic tick срабатывает; backstop эмитит Decision::Redeem для cids с filled_at старше 600s, отсутствующих в active_markets() | Periodic scan молчит → Harvester не находит рынки / застрявшие позиции не редимятся | I | TBD | unmapped |
| DIR-14 | Stop-loss config-driven (ADR-027): непрерывный пробой выполняет one-owner handoff и SELL @ current best_bid согласно execution.stop_exit | docs/strategies/directional-harvester.md §Жизненный цикл сделки | Trigger/debounce не меняются; order form точно соответствует policy роли | hard-coded FAK/FOK или обход owner/inventory guard → нарушение | U + I | directional role-intent tests | mapped |
| DIR-15 | DCA включено: на каждый 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 size | DCA отсутствует при остатке budget или exit не пересчитан → нарушение | U + I | TBD | unmapped |
| DIR-16 | DCA top-up не превышает budget_per_market (общий cap включая первоначальный fill) | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 6) | Суммарная экспозиция после DCA ≤ budget_per_market | DCA превышает budget → нарушение G-20 | U + P | TBD | unmapped |
| DIR-17 | Sweep / discovery режим: без webhook сигналов стратегия находит подходящие рынки через evaluate_periodic + market filter (MARKET_TAG_FILTER=103040 для weather operationally) | docs/strategies/directional-harvester.md §Жизненный цикл сделки + инвариант 10 | Periodic scan находит admissible markets без external триггера | Discovery молчит без webhook → нарушение | I | TBD | unmapped |
| DIR-18 | Entry-debounce ADR-030 deferred: capability and knobs are absent from current pm-engine | docs/strategies/directional-harvester.md §ADR-030 | Typed config, form and runtime contain no debounce mirror; a future return requires a new capability | A dead/hidden debounce key is accepted or behavior is claimed live without code | U + R | TBD | unmapped |
| DIR-19 | Market filter (MARKET_TAG_FILTER / per-strategy filters) применяется до entry-логики | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 10) | Не подходящий market filter → skip | Игнорирование фильтра → шумные ордера | U | TBD | unmapped |
| DIR-20 | Auto-redeem hybrid: WS-event-driven primary path (Strategy::on_market_resolved → Decision::Redeem) + 10-минутный periodic backstop | docs/strategies/directional-harvester.md §Жизненный цикл сделки (инвариант 8) | Resolved позиция редимится либо через WS-event, либо через backstop; обе ветки идемпотентны | Resolved позиция не редимится → потеря выплаты | I | TBD | unmapped |
| DIR-21 | Recovery: open positions / exit GTC ордера / PendingEntry восстанавливаются после рестарта без дублей | docs/strategies/directional-harvester.md §Жизненный цикл сделки + ADR-007 | TP-ордер не дублируется после рестарта; shadow-map восстановлен | Дубль TP → over-sell | I | TBD | unmapped |
| DIR-22 | Webhook seed запускает directional re-evaluation для целевого рынка | ADR-003 + docs/strategies/directional-harvester.md §Текущие параметры | Сигнал триггерит вход или DCA если admission gates пройдены | Сигнал игнорируется → нарушено | I | TBD | unmapped |
| DIR-23 | Toxic-flow active-cancel отменяет именно exit GTC ордер (если active-cancel включён) | ADR-004 | Cancel применяется к exit GTC SELL @ 0.99; holdings (актив) остаются | Cancel закрывает позицию рыночно → нарушение | I | TBD | unmapped |
| DIR-24 | На CLOB-reject стратегия применяет backoff, не выставляет тот же ордер немедленно | ADR-007 | Reject → задержка перед retry | Бесконечный retry → rate-limit ban | U | TBD | unmapped |
| DIR-25 | Stale book → tick пропускается, Decision::Hold | ADR-007 + ADR-022 (unified OrderbookStore с timestamp guard) | Стратегия читает orderbook через OrderbookStore::full_orderbook(); возраст snapshot > порога → Hold | Вход по stale → mispricing | U | TBD | unmapped |
| DIR-26 | Decision::Hold не приводит к сайд-эффектам | ADR-007 | Никаких ордеров/cancel'ов на Hold | Hold пишет в DB или CLOB → нарушение | U | TBD | unmapped |
| DIR-27 | Dry-run = live pipeline (SimOrderClient swap) | ADR-010 + docs/strategies/directional-harvester.md §Target Behavior | Decision-граф идентичен live; режим определяется bound адаптером OrderRouter | Dry-run skip-ает гейты → расхождение с live | I + R | TBD | unmapped |
| DIR-28 | Capture фиксирует все Decision-ы, включая Hold | ADR-009 | Все Decision сериализуются | Hold выпадает из capture → пропуск трассы | E | TBD | unmapped |
| DIR-29 | Hot path сериализация только через sonic-rs | memory:sonic_rs | sonic_rs в hot path | serde_json в decision-сериализации → grep gate fail | U | TBD | unmapped |
| DIR-30 | Replay determinism 10× для directional/Harvester (СНЯТО 2026-08-21 вместе с бэктестом) | ADR-009 | SHA-256 идентичен 10 раз подряд | Любое расхождение → CI fail | R | TBD | unmapped |
| 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-ttl | resolver точно выдаёт четыре CLOB-формы; отсутствие роли/TTL/cap и мёртвые поля fail-fast | hard-coded tuple, inheritance, скрытый default или незаконная комбинация → violation | U + I | resolver/config/schema/form/WatchArm FOK tests | mapped |
Сводка
| Группа | Кол-во правил | Источник spec |
|---|---|---|
| G (global) | 25 | ADR-003/004/007/009/010/015/022/024, CLAUDE.md, memory |
| DIR (directional/Harvester) | 32 | docs/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_secondsper 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_pnlsurfacing) реализованы. 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.