Тема
Tech Debt Backlog (open items)
Открытые tech-debt задачи. RESOLVED и архивно-связанные (стратегии, отключённые по
ADR-024) перенесены вdocs/history/tech-debt-resolved.md.Last updated: 2026-08-26.
Заметки к контексту (последние)
2026-08-22 (снятие backtest/replay/runtime simulation) — боевой
pm-engineоставляет только live/probe и фактическую телеметрию торговых сессий. Трассировка ордеров и решений — часть этой телеметрии и не снимается. Старые тела записей ниже оставлены для истории; упоминанияSimOrderClient, replay, capture и симуляторных проверок в них не являются текущими требованиями или планом работ.2026-08-17 (сквозной тест зависит от боевой страницы состояния биржи) —
end_to_end_rediscovery_liveness::rediscovered_market_becomes_tradable_end_to_endкраснеет, когда у Polymarket идут работы. Диагноз доведён до первоисточника, дефекта в коде НЕТ.Цепочка (журнал прогона +
status.polymarket.com/summary.jsonс боевого узла): тест поднимает НАСТОЯЩИЙ бинарь движка, у которого опрос страницы состояния «всегда включён» и работает в обоих режимах. В 21:15:00Z началось окно работTrading Gateway(status=INPROGRESS, страница отдаётHASISSUES). Через секунду после старта движок штатно перешёлhealthy → degraded(window_definitive=1) и закрыл НОВЫЕ BUY-входы. Дальше воронка входа даёт однозначную атрибуцию:blocked_exchange_degraded= 100 % оценок,entry_attempts=0. Само повторное обнаружение при этом ОТРАБОТАЛО — рынок B допущен координатором, принят продюсером,tracked_tokens=4,markets_evaluatedненулевой.Пока статус-страница удерживает активные работы, CLOB circuit breaker не допускает восстановительную пробу вообще. После снятия удержания входы откроет только успешное полное authenticated-чтение, допускаемое не чаще раза в 60 с. Поэтому тест с боевой статус-страницей по-прежнему зависит от внешнего эксплуатационного состояния и во время работ пройти не может.
Дефект — в устройстве теста, а не в движке: сквозной тест, поднимающий боевой бинарь, наследует внешнее эксплуатационное состояние поставщика. Чужое окно работ красит репозиторий. Плюс собственное сообщение теста уводит в сторону: оно называет четыре причины («координатор не поднят / допуск потерял первый кадр / продюсер проигнорировал поток / отображения не опубликованы»), и ни одна из них не верна.
Требуемое действие: сделать адрес страницы состояния подставляемым и увести его в тестовом стенде на заглушку со здоровым ответом — тогда тест проверяет повторное обнаружение, а не операционный календарь Polymarket. Отдельно — переписать сообщение о падении так, чтобы оно называло стадию воронки, на которой вход срезан.
Эксплуатационное следствие, не тестовое: пока работы идут, боевой движок тоже в деградации (
pm_engine_clob_health_state = 2на 2026-08-17 21:4xZ) и новых входов не откроет. Запуск стенда в это окно даст ноль входов — по замыслу, а не по ошибке.2026-07-22 (cutover-реконсиляция) — переход pm-bot → pm-engine ЗАВЕРШЁН: крейты
pm-bot/pm-dashboardудалены (см. PM-ENGINE-1 ниже — теперь по факту завершён; PM-ENGINE-4 «gated на cutover» разгейчен). Любые якоряcrates/pm-bot/src/...:NNNв отложенных/закрытых записях ниже ссылаются на УДАЛЁННУЮ раскладку pm-bot и не воспроизводимы построчно; текущий directional-код — подcrates/pm-engine/src/strategies/directional/, dashboard —pm-engine-dashboard. Якоряcrates/pm-cli/*иcrates/pm-bot-events/*— ЖИВЫЕ крейты, валидны. Тела записей не переписывались (agreed технические записи; статус-строки — историко-момент).2026-05-20 — backlog разделён: open vs resolved/archived. Все бывшие записи по
bucket-arb/maker/maker-rebate/merge-arb/paired-maker-rebate/single-token-mmперенесены в историю как ARCHIVED-BY-024.2026-05-06 — Spec-Driven Coverage Phase 0 sign-off: создана
feature-liveness-matrix.mdиtest-coverage-matrix.md; 22 новых TD (TD-35..TD-56) для silent-broken-suspect LIV-IDs покрыты картой и трекаются в матрицах напрямую, не дублируются здесь.2026-05-04 — ADR-009 Phase 9: удалена legacy capture/replay инфраструктура (~5 KLOC). Frozen-v1 Parquet fixtures — единственный источник истины для replay-тестов.
Полная история сводок и закрытий — в docs/history/tech-debt-resolved.md.
Implementation queue (user-approved scope)
(пусто — с момента X2.5/2026-05-03 все ранее-зачерчённые работы завершены; новые открытые задачи ниже в Deferred to roadmap)
Deferred to roadmap (архитектура задокументирована)
| # | Item | Decision | Architecture note |
|---|---|---|---|
| PM-ENGINE-PERIODIC-RECONCILIATION | Полный периодический reconcile локальных orders/positions/fills с authoritative exchange state во время работы процесса. | OPEN — NOT IMPLEMENTED. Bootstrap reconcile и user-WS reconnect reconcile покрывают старт/переподключение, но не являются постоянным self-healing циклом. | Принятый дизайн: periodic-reconciliation-design.md. Нужны отдельный OpenSpec change, bounded cadence/locks, метрики, read-only probe и проверка по фактической телеметрии; документация не должна называть этот контур живым до реализации. |
| FE-3 | Multi-strategy per process | DEFERRED — pending WebUI configurator (см. ADR-015 D10). | User-described future architecture: parent strategy manager + per-strategy child processes + wallet selection из list of available wallets. Hard prerequisites — FE-1 (multi-wallet), FE-2 (SQLite config), WebUI strategy configurator. Tech-lead reference: архивированный pm-orchestrator (multi-strategy execution shape) — ADR-015 явно указывает archive как salvage target для spawn/supervision layer. FE-3 получит собственный ADR при подъёме работы. |
| FE-5 reinvestment follow-up | PnL accruing to budget limit | SUPERSEDED — reinvest_realized_pnl уже является явным companion canonical-лимита 3; старый provisioning-time wallet predicate удалён 2026-08-01. | Dashboard больше не сравнивает budget envelope с моментным балансом кошелька. Runtime truth: total_budget_micro_usd + явный reinvest_realized_pnl в config.db; отдельного скрытого FE-5 механизма нет. Историческое решение сохранено в docs/history/. |
| ADR-030 directional entry debounce | Восстановить entry-debounce как pm-engine capability, если механизм снова понадобится. | DEFERRED — NOT IMPLEMENTED. Старый код и counters удалены вместе с pm-bot; в DirectionalConfig, форме и runtime их нет. | Требует нового OpenSpec change с typed config.db контрактом, детерминированными тестами решений и operator metrics. Старые pm-bot пути годятся только как исторический материал, не как описание текущего поведения. |
| Backtest backlog | Отложенные вопросы инфраструктуры бэктеста (BT-1, BT-2, IB-1, IB-2) и её audit-checklist. | СНЯТО 2026-08-21 — вместе с самим бэктестом (решение оператора, composable-portfolio-strategy-domain §D13); документ docs/backtest/backtest-backlog.md удалён, предмета у пунктов больше нет. | — |
| TD-RTDS-TOXIC-1 | Toxic-flow active cancel не безопасен для Harvester: raw public prints не исключают собственный wallet; window и cancel скоупятся по condition_id, а не token/outcome; BUY pressure может снять profit SELL; exit-presence перевыставит его без hold-down, что допускает churn; detailed RTDS/toxic metrics не проецируются в wire. | OPEN — active cancellation PROHIBITED; log_only=true. Требуются own-wallet exclusion, token/outcome-normalized aggregation and cancellation, Harvester exit policy, bounded hysteresis/cooldown, operator-grade wire metrics и проверка по фактической телеметрии форвардных сессий. | Полный audit и implementation seams: docs/integrations/rtds.md «Harvester activation status and verified limitations». Capability visibility contract: openspec/changes/hide-disabled-rtds-surfaces/. |
| bootstrap-shutdown-safety-5.5 | pm-cli bot reconcile-dry-run standalone — план-only прогон bootstrap pipeline без запуска bot процесса. Сейчас стаб (crates/pm-cli/src/main.rs:1385-1402) указывает на cargo run -p pm-bot -- --dry-run как поддерживаемую альтернативу. Full standalone требует extraction BootstrapServices ctor (CLOB credentials + BotConfig resolution) в reusable библиотеку. | DEFERRED — operator unblocked via pm-bot --dry-run. | Tracked by add-bootstrap-shutdown-safety §5.5 (closed as design-recorded). Wire-stable CLI surface уже на месте; стаб без mutation safe. |
| bootstrap-shutdown-safety-7.5 | Dashboard /api/lifecycle/status read-only route — JSON-эквивалент pm_lifecycle_state gauge для SPA баннеров. | DEFERRED — frontend developer scope. | Текущий observable surface достаточен для operator workflow: SSE LifecycleStateChanged + LifecycleHalted events (crates/pm-bot-events/src/lib.rs:883-925) уже консьюмятся local-bot SSE подписчиками; halt-сигнал оператору покрыт in-process AlertEngine → Telegram, а gauge pm_engine_lifecycle_state экспортируется на /metrics (:31340) для будущего внешнего alerting (стек не развёрнут). REST endpoint нужен только для cold-load dashboard главной страницы — нужен после первого live restart-loop инцидента. Trigger: frontend developer task сразу после merge change'а. |
| bootstrap-shutdown-safety-7.6 | Dashboard Vue page /bootstrap-audit — list of bootstraps + auto-heal actions с deep-link на отдельный bootstrap_id. | DEFERRED — frontend developer scope. | Read-only сервис уже есть: pm-cli bot reconcile-history --limit N (crates/pm-cli/src/main.rs:1296-1382). Web страница — UX polish, не корректность. Trigger: frontend developer task после merge. Зависит от §7.5 (endpoint, который страница потребляет). |
| bootstrap-shutdown-safety-8.3 | WebUI Strategy edit view — surface bootstrap/shutdown knobs (bootstrap_reconcile_timeout_seconds, auto_heal_action_cap, shutdown_cancel_timeout_seconds, etc.) в StrategyForm.vue. | DEFERRED — frontend developer scope. | Knobs сейчас tunable через env на restart, что и есть production workflow per feedback_strategy_config_via_ui (config.db) — для bot-wide knobs, не strategy-specific, env остаётся каноничным. Surface в WebUI нужен только если operator хочет менять через UI без редеплоя; не блокер для production. |
| PM-ENGINE-1 | Полный rewrite торгового движка: pm-bot → pm-engine (новый workspace member), hexagonal архитектура, 4 доменные таблицы (signals/orders/positions/events), новый Strategy SDK, переписывание directional/Harvester с нуля, миграция wire-контракта SSE/REST 1:1, реализация GAP-01 (shares_per_market_max), реализация GAP-03 (reinvest_realized_pnl в UI), single-writer гейт ast-grep в CI для всех трёх writer'ов. Финал: pm-bot полностью удаляется, harvester-live переезжает на pm-engine с чистой engine-state.db. | IN PROGRESS — Phase A-F done (commits 680cad3..0f3e785); Phase G/H/Q pending. | Полный SSOT в docs/target/ (engine.md / storage.md / wire-contract.md / risk-and-emergency.md / extension-points.md). Канонический openspec change openspec/changes/unify-orders-positions-signals-events/. Phase A-F (skeleton + engine core) реализованы: ports trait declarations; strategy SDK (Strategy trait + StrategyContext); directional/Harvester skeleton (typed config, FSM phases); wire layer (BotEvent re-export + реальные маппинги для SignalEmitted/OrderPlaced/OrderCancelled/OrderRejected/PositionOpened/PositionClosed); EventBus + outbox pattern; SignalRegistry (single writer signals); OrderRouter (allocation-id pattern + все 3 canonical RISK-LIMITS — GAP-01 active, лимит 2 active, лимит 3 active + reinvest companion; исходный scope содержал четвёртый лимит по доле кошелька в виде fail-closed заглушки — GAP-02 ОТМЕНЁН 2026-07-28 решением оператора, заглушка и ключ max_wallet_pct_bp удалены); EmergencyGuard (in-memory kill-switch, persistent state = PM-ENGINE-2); PositionRegistry (single writer positions + write-through cache + PositionQuery trait). 33 unit-тестов зелёные. Phase G — concrete adapters (clob/live через pm-sdk, clob/sim matching engine, orderbook merge, storage SQLite). Phase H — lifecycle FSM (из заархивированного add-bootstrap-shutdown-safety). Phase Q — actual business logic в hooks DirectionalStrategy. После Phase G+H+Q → cutover как отдельный operational event. |
| PM-ENGINE-2 | EmergencyGuard persistent kill-switch state + pm-cli risk reset-kill-switch CLI. Сейчас (Phase F step 6b) kill switch in-memory: EmergencyGuard::activate_kill_switch ставит флаг в Mutex<HashMap>, рестарт процесса очищает state. Per docs/target/risk-and-emergency.md:85-91 + ADR-031: финальная модель — таблица kill_switch_state в engine-state.db, чтение на старте, очистка через pm-cli risk reset-kill-switch при остановленном процессе (CLI fail-fast если процесс жив). Активация требует storage adapter (Phase G) и pm-cli wiring (Phase H). | DEFERRED — Phase G + H pm-engine. | Текущая in-memory модель удовлетворяет якорь A6 (project_canonical_vision: halt = restart-only) на уровне семантики (рестарт восстанавливает торговлю) — но не сохраняет audit kill-switch активации между рестартами. Это GAP относительно SSOT, не блокер pm-engine compile/test, открыт до Phase G. |
| PM-ENGINE-3 | SimOrderClient partial-fill state hydration: при загрузке open orders из БД, для строки в статусе PARTIAL текущий OrderRow shape (initial INSERT fields) НЕ несёт колонок filled_shares / filled_value_micro_usd — sim hydration воспроизводит full size_shares как remaining, игнорируя уже исполненную часть. После рестарта sim'а на partial-fill orders matching engine видит больше remaining shares чем реально доступно. Требуется либо расширить OrderRow (добавить filled-поля и хранить в insert/update), либо отдельный PersistencePort::load_open_orders_with_fills() API. | DEFERRED — Phase G follow-up. | Не блокирует SIGKILL survival scenarios (которые требуют только наличие open orders в matching book) — реализованный в step 7c liveness test twelve_open_orders_survive_drop_and_restart проходит. GAP проявится только когда matching engine начнёт обрабатывать post-restart partial-fill orders с реальным flow. До Phase Q (business logic) — не критично. Origin: backend-developer escalation в step 7c (commit 3b432c4). |
| PM-ENGINE-4 | Migrate pm-cli + pm-bot + pm-engine SimOrderClient bootstrap off legacy pm-sdk::clob::SdkOrderClient to pm-sdk::clob::PoolOrderClient (Phase G P5 adapter поверх ClobClientPool). After migration: remove #[deprecated] attribute со SdkOrderClient, удалить тип + его тесты + все #[allow(deprecated, reason = "PM-ENGINE-4 cleanup")] патчи (callsites сейчас в crates/pm-bot/src/main.rs:1340, crates/pm-bot/src/application/order_manager.rs:451, плюс pub use re-exports в crates/pm-sdk/src/clob/mod.rs и crates/pm-sdk/src/lib.rs). Acceptance: zero references to SdkOrderClient в workspace, all order paths route через ClobClientPool с per-wallet instances (architect OQ-3), integration tests confirm budget isolation across wallets. Также рассмотреть расширение OrderClient trait surface для FOK/FAK TIF (P5 PoolOrderClient сейчас отвечает NotImplemented — pool TIF знает только GTC/GTD), sell_or_fill, buy, list_orders, get_order, get_balance (P5 оставлены на trait default NotImplemented — pool surface уже имеет get_open_orders / get_trades напрямую, но через trait не проброшены, чтобы не плодить double-wiring до миграции legacy callsites). | DEFERRED — gated на pm-bot → pm-engine cutover (ADR-024 directional/Harvester migration). Premature migration risks destabilizing live pm-bot during cutover window. | Track: software-architect (plan refresh после Phase P6+), backend-developer (impl), qa-engineer (cutover validation). Origin: pm-sdk Phase G P5 (worktree-agent-a797d4c7dc18d7449). PoolOrderClient адаптер: crates/pm-sdk/src/clob/pool_order_client.rs (12 unit tests зелёные, debug + release). Phase P5 deliverable summary: place_limit_order через pool с pre-resolved tick_size + neg_risk=true; cancel_order + cancel_all_orders через pool; PoolError → ClobError mapping (Saturated/External429 → RateLimited, ShuttingDown/WorkerDied → Sdk, Clob passthrough). |
| HARV-WS-1 | In-place WS resubscribe не получает initial book snapshot: added-asset не получает initial dump (Polymarket шлёт его только на полный reconnect), price_change приходит до book → дельта дропается (drop-site scanner.rs, счётчик ws_delta_before_snapshot_total). REST resync (ws_reconnect_resync_burst) триггерится только на WsEvent::Reconnected, не на added-resubscribe; reconciliation pool добирает added-asset round-robin'ом (eventual), приоритетного fetch нет. | OBSERVABILITY SHIPPED (0ea4819), fetch-оптимизация DEFERRED (data-gated) — adversarial-дебат: severity низкая (held-позиции НЕ теряют книгу mid-session — FSM-tracked токены остаются в current; пустая книга gated в check_exits/check_fills; capital-safe; дроп = задержка инициализации в фазе presubscribe/entry, не corruption и не slep stop-loss открытой позиции). | Добавлены метрики blind-window для data-driven решения: pm_added_to_first_book_seconds{phase} (p50/95/99), pm_ws_delta_before_snapshot_by_phase_total{phase} (presubscribe vs in_entry_window), pm_reconciliation_queue_len_at_added. Priority-fetch (оживить NEW_ASSET_RESERVE под governor) реализовать ТОЛЬКО если метрики покажут существенный blind-window p95 / queue 100+. Мёртвый knob NEW_ASSET_RESERVE_PER_10S (форма burst-quota без поведения) приведён к честной документации как headroom бюджета (0ea4819; арифметика не тронута). При малых живых очередях round-robin gap (91-454ms) ≈ HTTP RTT из Финляндии. Cross-link: [[project_ws_self_healing_decision]] (ADR-035 V1+V3). |
| HARV-RECONCILE-1 | tri-state KPI для unknown-winner при реконсиляции stale-позиций (commit 6fa337c, BUG E Tier-2). close_stale_unresolved при недоказуемом winner букает exit_price=BOT_STALE_POSITION_UNRESOLVED_EXIT_PRICE (default 0.0) → realized=−cost → loss_count += 1 (writer.rs:884, is_win = realized > 0 на :867). realized==0 ТОЖЕ идёт в loss. Значит win-rate KPI искажается реальными убытками по позициям, чей исход НЕИЗВЕСТЕН (мог быть win). | DEFERRED — не блокер dry-run (реинвест читает realized=−cost, что лишь снижает бюджет — консервативно безопасно; разморозка независима). | Целевое (consensus codex+architect): tri-state — списать impairment для capital safety, но классифицировать UNKNOWN_RECOVERED и ИСКЛЮЧИТЬ из win/loss. Требует отдельного writer code-path (не knob). Закрыть до того, как win-rate станет драйвером авто-решений. Cross-link: memory project_harvester_partial_proceeds_signflip. |
| HARV-RECONCILE-2 | Приоритет resolution-booking над stale-policy при гонке (commit 9e85bf2). Если WS-resolved handle_resolution (с истинным winner → exit 1.0/0.0) ПРОИГРАЕТ гонку эвикции Tier-2 close_stale_unresolved (order_manager.rs:2631), позиция закрывается по unresolved-policy цене, а не по фактическому winner → неверный локальный realized + пропущен on-chain redeem (capital восстановим через идемпотентный redeem --all, не потеря). | DEFERRED — редкая гонка, capital-safe, не блокер. | Fix: close_stale_unresolved должен re-check resolved-статус (single-market fetch) ПЕРЕД победной эвикцией; если рынок реально resolved — делегировать handle_resolution (booking по winner). Не трогая concurrency-инвариант «проигравший=no-op» из settle_terminal_close. Независимая валидация: codex-debater 2026-06-01. |
| PM-ENGINE-H-DEFERRED-POSITIONS-RECONCILE | Bootstrap reconciler positions+pnl 3-way diff. Phase H2 включил только open-orders diff (local PersistencePort vs remote OrderClient::list_open_orders). Positions reconcile нуждается в remote source truth — нет в текущем pm-engine port set'е (list_positions отсутствует на OrderClient; pm-sdk Data API gated by data feature но pm-engine не consumer). | DEFERRED — Phase Q (business logic) или позднее. | Мёртвые BootstrapConfig.reconcile_positions / reconcile_pnl и одноимённые env-ручки удалены: хранить включаемую настройку без поведения запрещено. Будущая реализация должна одновременно добавить DataPort или расширить OrderClient, рабочее поведение и только затем конфигурационную поверхность. | | PM-ENGINE-H-LIVE-GET-ORDER-DETAIL | LiveOrderClient::get_order_detail возвращает OrderError::Upstream(...) — ClobClientPool (P4 surface) не предоставляет single-order REST endpoint, только get_open_orders / get_trades / cancel. Underlying SDK polymarket_client_sdk::clob::Client::order(id) существует, но pool worker не маршрутизирует single-order запросы. Bootstrap reconciler в этом случае halts с HaltReason::ReconcileInconclusive (fail-loud, per feedback_no_silent_backlog + feedback_no_dust_threshold_finance) вместо silent выбора filled vs cancelled. | DEFERRED — pm-sdk Phase P extension. | Trigger: добавить PoolRequest::GetOrder { order_id, reply } в crates/pm-sdk/src/clob/pool/request.rs + handler в pool/worker.rs + публичный ClobClientPool::get_order(id, intent). На pm-engine side LiveOrderClient::get_order_detail перепишется на pool call (см. crates/pm-engine/src/adapters/clob/live/client.rs get_order_detail). Phase H2 audit-row маркер reconcile_inconclusive уникален и наблюдаем через pm_lifecycle_halt_total{reason="reconcile_inconclusive"}. | | PM-ENGINE-H-CLI-RECONCILE-HISTORY | pm-cli engine reconcile-history --limit N — read-only SELECT bootstrap_audit + bootstrap_actions из engine-state.db. Аналог в pm-bot реализован (pm-cli bot reconcile-history); pm-engine emerging как отдельный binary потребует параллельной CLI surface. | DEFERRED — frontend / operator scope. | Backend (Phase H2) уже expose PersistencePort::fetch_bootstrap_history(limit) и fetch_bootstrap_actions(bootstrap_id) (crates/pm-engine/src/ports/persistence.rs). Pm-cli wiring отдельной задачей после cutover; до cutover operator может использовать pm-cli bot reconcile-history. | | PM-ENGINE-H-SHUTDOWN-ORPHANS-WIRE | ShutdownCoordinator::cancel_all_with_deadline при orphan_count > 0 сейчас логирует ERROR + возвращает ShutdownReport.orphan_count для exit-code (1 при orphans, 0 если чисто), но не публикует BotEvent — pm-bot-events wire contract не содержит BotEvent::ShutdownOrphans { order_ids } variant. Dashboard banner о orphan'ах при graceful shutdown отсутствует. | DEFERRED — pm-bot-events sealed (Phase H4 не extends wire). | Trigger: extend pm-bot-events::BotEvent enum с ShutdownOrphans { v, ts, order_ids: Vec<String> } variant + bump wire version. На pm-engine side добавить event_bus.publish_bot_event(...) после орфана-логирования в lifecycle/shutdown.rs:404-413. ShutdownReport.orphan_count остаётся public API. | | PM-ENGINE-H-BOOTSTRAP-COMPLETED-WIRE | BootstrapReconciler::run success path сейчас логирует INFO + возвращает BootstrapReport caller'у. FSM transition Reconciling→Ready публикует BotEvent::LifecycleStateChanged (Phase H4), но не отдельный BotEvent::BootstrapCompleted с action_count. Dashboard видит только смену state, не количество heal-actions. | DEFERRED — pm-bot-events sealed (Phase H4 не extends wire). | Trigger: extend pm-bot-events::BotEvent enum с BootstrapCompleted { v, ts, bootstrap_id, action_count } variant. Architect plan Q3 — defer Phase I. На pm-engine side добавить publish после успешного transition_to(Ready) в lifecycle/orchestration.rs. | | PM-ENGINE-9 | apply_terminal_sell realized-PnL inflation при partial terminal sell. realized_pnl = cum_exit_value − entry_cost (crates/pm-engine/src/engine/position_registry.rs, apply_terminal_sell) корректен ТОЛЬКО когда terminal sell полностью закрывает позицию (fill_shares == state.size_shares, leftover=0). Если позиция ВЫРОСЛА после постановки exit-ордера (grow-while-selling), terminal fill < размера позиции → списывается ПОЛНЫЙ entry-cost против ЧАСТИЧНОЙ exit-выручки → раздутый убыток → заниженный winrate (verdict-corrupting). Origin: audit #7/#9/#12 2026-06-29 (#7/#12 REFUTED, #9 confirmed-latent). | DEFERRED (формула), НО «латентный» — ОПРОВЕРГНУТО ЗАМЕРОМ. Прежняя оценка «текущий Harvester ⇒ leftover всегда 0» НЕВЕРНА: partial fill входа даёт позицию с odd-lot размером, продажа квантуется вниз до лота CLOB (quantize_sell_size), и хвост остаётся. Боевые срабатывания: 3156 / 9031 / 1817 микро-долей (2026-07-25…27) и 8204 (2026-08-12) — ВСЕ меньше лота. 2026-08-12 остаток разделён на два класса (openspec terminal-sell-residual-dust-accounting): МЕНЬШЕ лота ⇒ непродаваем, останова входов нет, остаток остаётся в positions.size_shares и уходит в существующий контур выкупа при разрешении рынка; лот и БОЛЬШЕ ⇒ сторож учёта срабатывает как раньше. | Искажение realized НЕ исправлено ни в одном из классов: списывается ПОЛНЫЙ entry-cost против ЧАСТИЧНОЙ выручки. Для непродаваемого остатка величина ограничена сверху долей лот / размер позиции (боевые числа 8204 / 7 998 204 = 0.103 %). Correct fix ЗАВИСИТ от terminal REASON: order-done (FOK-kill/GTC-cancel) → keep leftover open + re-exit; market-resolved → settle leftover via redemption. Реализовать proportional-realized вместе с grow-while-selling работой Ф5. До тех пор: НЕ доверять EV/вердикту на данных, содержащих guard-лог AUDIT #9 (он остался только для класса «остаток ≥ лота»; непродаваемый остаток пишется WARN-строкой и счётчиком pm_engine_terminal_sell_stranded_dust_total). Cross-link: memory project_engine_verdict_bugs_2026-06-24. | | PM-ENGINE-10 | Directional entry (maker BUY, GTD buy_order_ttl_seconds=600, post_only, тонкая книга) может partial-fill'иться на размер МЕНЬШЕ min_size_shares до истечения TTL — образуется dust-позиция, которую target/stop не может продать. Форвард-окно 2026-06-29→07-01: 3/5 позиций стали dust; retry-шторм уже устранён. Origin: [[project_dust_position_retry_spam_2026-07-01]]. | ОТКРЫТО, риск принят для режима малого бюджета 2026-07-31. Двухлотовый пол, введённый 2026-07-30, гарантировал budget headroom на добор, но при штатных $5/рынок, лоте 5 и цене 0.65–0.95 скрыл фактический минимум 10 и заблокировал 100% входов. По операторскому решению вход снова использует единственный пол min_size_shares; DCA остаётся best-effort строго внутри budget_per_market. Dust-детекция, подавление retry-шторма и redeem-path сохранены. | Строгий бюджет одного лота математически не может одновременно гарантировать резерв ещё одного полного лота. Следующее структурное решение требует отдельного выбора execution semantics (например, lifecycle частично исполненного GTC), но не должно молча повышать риск-лимит или снова останавливать все малые входы. | | PM-ENGINE-11 | DCA self-churn → позиция может застрять недоинвестированной НАВСЕГДА (dca_enabled=true на текущем LIVE VPS-конфиге — АКТИВНО). try_dca_replace вызывался на КАЖДЫЙ partial-fill уже открытой позиции (grow_existing_position не различает "новый partial того же ещё живого ордера" от "отдельный DCA-leg") — безусловно cancel'ил ещё живой resting BUY и переставлял по тому же pinned target_buy_price. Побочные эффекты: TTL сбрасывался на полный интервал заново на каждый partial; потеря очереди в стакане; dca_seq инкрементировался на само-погоню. Если замена после cancel падала — Err ⇒ warn!() БЕЗ retry, и т.к. try_dca_replace вызывался ТОЛЬКО из fill-driven on_position_change(Accumulated) — budget headroom простаивал до резолюции рынка. Тот же архитектурный паттерн, что был на exit-стороне (PM-ENGINE-10). Origin: [[project_broad_defect_hunt_2026-07-01]] (workflow-verified, adversarial CONFIRMED). | ИСПРАВЛЕНО (2026-07-01, ветка fix/dust-stranded-exit-suppression, коммит после b85a8fc) — обсуждено с codex-debater (5 раундов, поймал 2 внутренних противоречия собственных рекомендаций, скорректировал). MVP: (B) try_dca_replace теперь skip'ает cancel+replace если BUY уже resting для сигнала (self-churn guard); (A) новый periodic ensure_dca_presence хук (аналог ensure_primary_exit), вызывается из on_market_data, восстанавливает ТОЛЬКО отсутствующий BUY (никогда не cancel'ит/не репрайсит живой), throttled 30с после неудачной попытки (dca_backoff в runtime_state.rs, DCA_RESTORE_BACKOFF_MS). НЕ включено в MVP (эскалировано дебатером как алгоритм/риск-решение, НЕ реализовано): TTL-carryover semantics, repricing pinned target_price / отмена resting BUY по band drift, manual per-signal suspend. Верифицировано: 2 интеграционных теста (directional_dca_self_churn_suppression.rs) с дисконфирмацией через git-stash (оба падали на pre-fix коде, точно воспроизводя self-churn и отсутствие restore), полный pm-engine 889/0 + it-сьют, clippy чисто, cargo test --workspace зелёный. НЕ задеплоено (Ф3 config-freeze). | Overspend-гипотеза (два одновременных BUY) REFUTED дебатером — single-threaded strategy-driver архитектура (biased tokio::select!) исключает гонку между periodic-хуком и event-driven путём by construction, отдельный actor-guard не потребовался. Эскалированные пункты (TTL/repricing/suspend) ждут отдельного согласования пользователя. | | PM-ENGINE-12 | Redemption: позиция может НАВСЕГДА остаться нередимленной, если рынок резолвится в узком окне вокруг рестарта процесса (deploy/OOM/crash). ClosureWatcher.tracked — чисто in-memory (closure_watcher.rs:113), рестарт очищает; discover_markets() (gamma.rs:363) явно исключает closed=true рынки → незатреканный закрытый рынок НИКОГДА не попадёт обратно в tracked → ClosureEvent не эмитится → on_closure/mark_redeem_pending не срабатывает → redeem_status остаётся NotRequired бессрочно. ResolutionSweep (sqlite.rs:1568-1571) не покрывает (сканит только уже-REDEEMED с NULL realized_pnl, не OPEN/SELLING). Достижимость подтверждена независимо: частые OOM-рестарты/redeploy — задокументированная операционная история проекта. Origin: [[project_broad_defect_hunt_2026-07-01]] (workflow-verified, CONFIRMED live). | ИСПРАВЛЕНО (2026-07-03, ветка fix/dust-stranded-exit-suppression) — root cause сужен чтением кода (не нужен новый periodic-механизм): ClosureWatcher::poll_once() перечитывает tracked СВЕЖИМ на каждый тик (нет snapshot при старте loop'а) и одинаково трактует "уже был закрыт когда затрекан" и "закрылся только что" — единственная проблема была в том, что позиция вообще никогда не попадала в tracked после рестарта. Фикс: main.rs сразу после position_registry.hydrate() ре-трекает PositionRegistry::held_condition_ids() (уже существующий метод — тот же, что discovery_coordinator использует для anti-blind-stop орderbook force-subscribe) в closure_watcher. Обсуждено с codex-debater (3 раунда, изолированный cwd) — MUST-verify чеклист (poll живой per-тик, track() без side-effects, mark_pending идемпотентен и вызывается только из single-threaded strategy-driver — 1 call site strategy_context.rs:977, существующий poll детектит "уже закрыт до track()") лично подтверждён чтением кода. 2 новых интеграционных теста (closure_watcher_restart_retrack.rs): restart_without_retrack_never_detects_closure (bug repro — без wiring poll_once() никогда не эмитит) + restart_with_retrack_detects_closure_on_first_poll (fix proof — с wiring эмитит на первом же poll). cargo test -p pm-engine 889+373/0, clippy чисто, cargo test --workspace/clippy --workspace зелёные. Архивный blind-spot (archived=false фильтр в list_closed_by_condition_ids) НЕ покрыт — предсуществующее ограничение всего redemption-пути (тот же вызов лежит и под RedemptionService/ResolutionSweep), не хуже и не лучше после этого фикса, отдельный гораздо более редкий edge case. НЕ задеплоено (Ф3 config-freeze). | Codex также предложил усилить mark_pending's read-then-write в conditional UPDATE с проверкой affected-rows вместо read-then-write на уровне приложения — НЕ реализовано (вне scope этого фикса, текущая архитектура уже безопасна: единственный caller — single-threaded strategy-driver), задокументировано как nice-to-have follow-up если архитектура когда-либо перестанет быть single-threaded для этого пути. | | PM-ENGINE-13 | Три отдельных находки. (a) LDI-гейт — полный no-op на живом VPS-конфиге прямо сейчас (min_ldi_for_entry=0.0 + min_phase_for_entry="Uncertain" — обе тавтологии; подтверждено на VPS config_live.db от 2026-06-29). Похоже на осознанный выбор для форвард-сбора без LDI-фильтра, но материально влияет на интерпретацию Ф3-окна (entries сейчас НЕ гейтятся LDI). Origin: [[project_broad_defect_hunt_2026-07-01]]. (b) Redemption backoff off-by-one — ПЕРЕПРОВЕРЕНО 2026-07-03, REFUTED (не баг) — код делает ровно 3 попытки с 2 backoff-паузами, корректная семантика max_attempts=3; doc-комментарий RedeemRetryPolicy уточнён. (c) closure-watcher poll_interval/broadcast_capacity — ИСПРАВЛЕНО 2026-07-03, см. ниже. | (a) РАТИФИЦИРОВАНО 2026-07-04: LDI-гейт ВЫКЛЮЧЕН ОСОЗНАННО (оба порога пропускающие) — Ф3 собирается без LDI-фильтра намеренно, чтобы решить о фильтре на данных; включение — отдельное решение после EV-вердикта, до/после не смешивать в одной выборке. Задокументировано в SSOT (docs/strategies/directional-harvester.md §LDI entry gate). (b) REFUTED. (c) ИСПРАВЛЕНО (ветка fix/dust-stranded-exit-suppression) — GammaPollConfig.poll_interval_secs/.broadcast_capacity (Option, #[serde(default)], bounds-checked zero-reject) реально читаются main.rs в ClosureWatcherConfig и в trading-plane broadcast-канал; absent ⇒ byte-identical прежним хардкодам (10с/256). Оба конца anti-drift контракта (config_form_contract.rs+golden, strategyFormSchemas.ts+GAMMA_POLL_PAYLOAD_FIELDS в strategyFormJson.ts) синхронизированы. cargo test -p pm-engine 892+373/0, bun test 464/1 (1 fail — pre-existing, не связан), clippy --workspace+cargo test --workspace зелёные. Задеплоено на dry-run инстанс harvester-engine 2026-07-03 (scripts/deploy-vps.sh engine). | (a) решить: оставить LDI-гейт выключенным сознательно (задокументировать намерение) или включить обратно перед следующим forward-окном — пользователь пока отложил explicit-решение. (c) burst-eviction-loss race (broadcast-буфер может переполниться при массовых закрытиях, RecvError::Lagged без reconciliation) остаётся неизмеренным отдельным риском — broadcast_capacity теперь настраиваемый, но сам race не устранён; кандидат на отдельную оценку под реальной нагрузкой. | | PM-ENGINE-14 | UI-баг расследование (оператор 2026-07-03: «конфигурация невалидная», боится редактировать через UI). 4 находки, все подтверждены чтением кода + живыми данными config.db инстанса harvester-engine (read-only). (a) P0 — молчаливая потеря regime. expandDottedKeysToJson (strategyFormJson.ts) строит definition ИСКЛЮЧИТЕЛЬНО из ключей формы; regime.* сознательно не в форме (advanced TCHS-гейт), но save-путь не мёржил с оригинальным definition → любое сохранение через StrategyEdit стёрло бы regime, если он когда-либо будет задан (сейчас ни у одной строки не задан — латентно, не сработало). (b) P1 — запись не атомарна. StrategyConfigStore::update_field/set_enabled (pm-common/strategy_config.rs) делали row-UPDATE и audit-INSERT двумя независимыми autocommit-запросами (НЕ в транзакции, вопреки doc-комментарию); multi-field PUT (budget+wallet+definition+enabled разом) — 3-4 таких независимых commit'а подряд, каждый может упасть независимо. (c) P1 — битый stop-loss в списке. StrategiesView.vue читает стоп-лосс через легаси плоский ключ (StrategyEntry::stop_loss_bid_threshold()), которого в pm-engine-нативных (nested) строках не существует — колонка «Stop-Loss» показывает «выключен» для реально активного стоп-лосса (harvester-engine-v2 живой пример); форма редактирования тем временем читает верно — расхождение список↔форма и есть, вероятно, источник ощущения «невалидно». (d) P1 — TS anti-drift контракт не в CI. config-contract.test.ts не входил ни в npm test (package.json glob не покрывал components/__tests__/), ни в CI (.github/workflows/*.yml — ноль frontend-шагов вообще). Origin: agent-investigation 2026-07-03 (read-only, VPS+код). | (a)/(b)/(c) ИСПРАВЛЕНЫ (ветка fix/dust-stranded-exit-suppression). (a) StrategyEditView.vue мёржит expandDottedKeysToJson-вывод ПОВЕРХ оригинального row.definition (паттерн setReinvest) — form может менять только показанные ключи, остальное сохраняется. (b) update_field/новый update_fields_atomic (весь multi-field batch — ОДНА транзакция) в pm-common; оба dashboard-хендлера (pm-engine-dashboard И pm-dashboard, идентичный баг в обоих) переведены на update_fields_atomic. (c) pm_engine_stop_loss_mirror() — fallback на nested stop_loss.* когда легаси плоский ключ пуст, оба конца (pm-engine-dashboard/routes/api/strategies.rs). (d) package.json test-glob расширен на весь src/ (был только views/__tests__/) — CI-часть НЕ сделана: pnpm install/bun install для ОБОИХ SPA (pm-engine-dashboard-web И pm-dashboard-web) упадёт на GitHub Actions runner — @polymarket-bot/ui-components объявлен как link:../../../../WebstormProjects/... (абсолютный путь вне репозитория, есть только на машине разработчика) — структурный блокер, обнаружен как побочный эффект, НЕ pre-existing задокументированный факт; отдельная задача (вендорить/опубликовать пакет или staged CI-стаб), не решена. Расширение glob'а вскрыло ПРЕДСУЩЕСТВУЮЩИЙ несвязанный provider fail (strategy_no_price_data is referenced in MainView.vue — метрика есть в wire-типе и в store, но нигде не рендерится в MainView.vue; bun test теперь честно 464/1, было скрыто узким glob'ом). cargo test -p pm-common -p pm-engine-dashboard -p pm-dashboard: 364+98+98+82+82/0, clippy чисто. | CI-wiring ЗАКРЫТ 2026-07-04: ui-components вендорен в монорепо (frontend/ui-components, pnpm workspace, коммит f6ea39b3), добавлен .github/workflows/frontend.yml (pnpm install --frozen-lockfile + build + bun test обоих SPA). Устаревший тест strategy_no_price_data is referenced in MainView.vue УДАЛЁН — он требовал рендерить на pm-engine-дашборде метрику, которую движок структурно не эмитит (bot_event_map.rs жёстко None, вместо неё гранулярные funnel-счётчики); рендер вечно-пустой плитки = dead UI (RULE-NEW-01). Оба SPA зелёные: pm-dashboard-web 337/0, pm-engine-dashboard-web 464/0. strategy_no_price_data — реальный, но мелкий observability-гэп (метрика собирается, не показывается) — не реализовано (нужен UI-дизайн-выбор, куда добавить stat-card), не блокирует остальное. | | PM-ENGINE-15 | Primary-exit (target-exit @exit_price, post_only GTC SELL) retry-spam при конвергенции книги (2026-07-03, пользователь: «разобраться»). Живая dry-run позиция pos-85503 (harvester-engine): best_bid дошёл до 0.999, фиксированная exit_price_milli=990 (0.99) — resting post_only SELL там немедленно скрещивается → биржа (sim-клиент, dry-run) отклоняет OrderError::PostOnlyCrossing КАЖДЫЙ тик, БЕЗ backoff (ensure_primary_exit не имел book-awareness вообще). Подтверждено логами (3467 REJECTED-строк за 6.9ч, продолжалось в реальном времени) + прямым чтением live CLOB book (asks:[], bids стеной до 0.999) + кодом (emit_primary_exit/ensure_primary_exit — единый catch-all на Err, ни dust-precheck-стиля классификации, ни backoff для этого класса ошибки). Экономически позиция НЕ в убытке (глубоко ITM), но exit-инвариант нарушен: пока блокировка длится, позиция незапланированно докатывается до резолюции/редимпшена — прямо противоречит правилу проекта «харвестер выходит на ≈0.99, не досиживает». Origin: agent-investigation + codex-debater (3 раунда, advisor поймал 3 дыры в MVP-черновике до кода — переформулировка "нет доступного действия"→"есть, бот сам себе запрещает через post_only"). | ИСПРАВЛЕНО (ветка fix/dust-stranded-exit-suppression) — ОСНОВНОЙ механизм: локальный precheck внутри ensure_primary_exit (best_bid >= exit_price → не отправлять, 0 round-trip к бирже, 0 строк в БД) — бит-в-бит копия sim-мэтчера (adapters/clob/sim/matching.rs:140, intent_crosses_book, Side::Sell => bids.any(price >= intent.price)); ошибка биржи (OrderError::PostOnlyCrossing) — узкий safety-net на race/устаревший snapshot, НЕ основной путь. Видимость НЕ гасится: первый WARN всегда, дальше периодический (не каждый тик, не тишина) раз в PRIMARY_EXIT_CROSSING_WARN_INTERVAL_MS=600_000 (10 мин) — состояние длящееся часами это реальный сбой инварианта, не рутинный шум. Новое per-signal state exit_crossing_blocked (runtime_state.rs) + счётчик exit_blocked_would_cross (EntryGateCounters). 3 новых интеграционных теста (directional_exit_crossing_suppression.rs) с дисконфирмацией (временно отключил precheck веткой if false && ..., 2 из 3 тестов упали воспроизводя баг, третий — regression-guard для легитимного exit — остался зелёным как и ожидалось; precheck восстановлен). cargo test -p pm-engine 896+376/0, clippy+cargo test --workspace зелёные. НЕ задеплоено на момент коммита. | Оба открытых вопроса РАТИФИЦИРОВАНЫ пользователем 2026-07-03, без дальнейших действий. (1) Live-путь safety-net: оставить как есть — реальный формат reason-строки биржи не угадываем, замаппим в типизированный вариант только после первого настоящего live-инцидента с реальным текстом ошибки в логе, не раньше. (2) Taker-fallback НЕ реализуется: exit-ордер — лимитный с семантикой ПОЛА (минимально приемлемая цена, исполнение по НЕЙ ИЛИ ЛЮБОЙ ВЫШЕ приемлемо, конкретная цена значения не имеет — пользователь явно подтвердил это как задуманную семантику, не баг). Repricing уже отклонён как нерабочий (пустой ask-side). Оператор явно принимает, что случаи best_bid >= exit_price будут ДОХОДИТЬ ДО REDEEM (позиция глубоко ITM в момент блокировки по построению — редимпшен для такой позиции не хуже, обычно лучше любой цены ниже 1.00) — это принятое поведение, не дефект, дальнейшая работа не требуется. | | PM-ENGINE-16 | SimOrderClient tape-driven maker-fill (apply_trade_print, Ф2 sim-maker-fill) стемпил OrderEvent::Fill/PartialFill.ts как self.clock.now_ms() (processing time), НЕ как реальный exchange-timestamp принта, который его вызвал. На живом dry-run (2026-07-03) сходящаяся форензика (реконструкция реальных trade-принтов через публичный Polymarket Data API + прямая арифметика по 2 fill'ам одного ордера + corroboration через orderbook-freshness — независимое отклонённое exit-ордер 16мс спустя доказывает, что book-канал был свежим пока trade-tape-канал уже отставал) показала окно полной тишины 97с, внутри которого был записан fill, и промежуток 109с между 2 fill'ами того же ордера — превышающий 87-секундное окно, где вообще мог существовать причинный реальный принт. Т.е. first_fill_at/fill-таймстампы в БД могут отставать от реального момента исполнения на минуты под ingestion lag, БЕЗ наблюдаемого сигнала (цена/размер филла, предположительно, верны — расходится только ts). Root cause: main.rs's live OrderbookUpdate::Trade match arm откидывал WS-принта собственный ts через .. при конструировании TapeTrade, вынуждая apply_trade_print подставлять clock.now_ms(). Independent Explore-verified: pm-engine's --capture/replay путь ИММУНЕН (captures/ReplayFrame::Trade{ts_ms} несут сырой exchange ts НАД местом, где он отбрасывается на live hot path) — pm-analytics бэктест-анализ этим багом не затронут. Также подтверждено: НИ ОДИН live-decision код не читает записанный fill ts обратно (чисто data-accuracy finding, ноль decision-impact). Origin: workflow-investigation (sim-fill-timing-delay), 2026-07-03. | ИСПРАВЛЕНО (ветка fix/dust-stranded-exit-suppression) — TapeTrade получил поле ts: UnixMillis (собственный exchange-ts принта, UnixMillis(0) = документированный sentinel "нет реального ts" → fallback на clock.now_ms(), сохраняет старое поведение byte-for-byte для вызовов без tape-контекста). Прокинут на ОБОИХ production-путях: main.rs live OrderbookUpdate::Trade (было .. — отбрасывало) и replay/mod.rs (ReplayFrame::Trade{ts_ms} → TapeTrade.ts). apply_trade_print (adapters/clob/sim/client.rs) стемпит apply_match_outcome этим ts вместо clock.now_ms(), guard > 0. 2 новых интеграционных теста (sim_order_client_matching.rs: tape_fill_stamps_trade_exchange_ts_not_processing_time + tape_fill_falls_back_to_clock_when_trade_ts_is_sentinel_zero) с дисконфирмацией (временный if false && trade.ts.0 > 0 — первый тест упал воспроизводя баг ровно с ожидаемыми значениями left: UnixMillis(9999000) right: UnixMillis(1500000), второй остался зелёным как и ожидалось; фикс восстановлен). cargo test -p pm-engine полностью зелёный (378 it + lib), clippy -p pm-engine --all-targets чисто, cargo test --workspace/clippy --workspace --all-targets оба зелёные (0 failed по всем крейтам). НЕ задеплоено на момент коммита (чистый data-accuracy фикс, latitude на деплой — решение оператора). | Систематическая проверка НЕ проведена: workflow-синтез рекомендовал новую forensics.sh подкоманду, сверяющую КАЖДЫЙ fill Ф3-окна с ближайшим причинным реальным принтом (детектор одноразовости vs системности паттерна) — не реализовано в этой сессии, кандидат на отдельную задачу. Отдельный, НЕ связанный, capture-формат pm-bot (CaptureFrame/ts_ns, crates/pm-bot/src/persistence/capture/schema.rs) потенциально подвержен тому же классу бага, если используется для бэктеста — флагировано, НЕ исследовано (out of scope, pm-bot capture не участвует в текущей pm-analytics/pm-engine backtest-цепочке). | | PM-ENGINE-17 | Risk-панель (kill-switch) — двойная поломка, найдена broad-hunt workflow 2026-07-04, лично верифицирована чтением кода. (a) Все ТРИ писателя risk_kill_switch (pm-cli risk configure, pm-dashboard/routes/api/risk.rs, pm-engine-dashboard/routes/api/risk.rs) проталкивали JSON-ОБЪЕКТ через generic update_field("definition.<key>") (pm-common/strategy_config.rs), который by design хранит всякое значение как string-scalar («everything is a string», env-parity) → double-encoded text; единственный читатель (StrategyEntryExt::risk_kill_switch(), pm-bot main.rs:368 при старте) требует is_object() → всегда None. Оператор жмёт «настроить», получает 200 OK, конфиг мёртв. Подтверждено на живых VPS БД: 3 строки с json_type(definition,'$.risk_kill_switch')='text' (harvester@engine, harvester-weather+harvester-daily@live). (b) Панель /api/risk/* в pm-engine-dashboard управляла pm_bot::domain::risk::KillSwitchStore — механизмом, который pm-engine НЕ читает вообще (grep: 0 ссылок; у движка свой EmergencyGuard, in-memory, restart-only per A6/ADR-031) — ложный интерфейс: панель «защиты» с нулевым эффектом на реальный движок. | ИСПРАВЛЕНО 2026-07-04. (a) Новый JSON-aware StrategyConfigStore::update_definition_json_key (одна транзакция + audit-row, invalid JSON → ошибка без частичной записи); оба живых писателя (pm-cli, pm-dashboard) переведены на него; 4 unit-теста в pm-common + spec-driven roundtrip-тест scenario_configure_roundtrip_survives_real_write_path (pm-bot risk_engine_e2e: реальный write-путь → реальный startup-loader; legacy-путь пришпилен как «невидим читателю» regression-guard'ом). (b) Панель в pm-engine-dashboard УДАЛЕНА целиком (routes/api/risk.rs, RiskView.vue, stores/riskEngine.ts, роут, кнопка «Risk» в StrategiesView) с пояснением в routes/mod.rs; честная панель возможна только после PM-ENGINE-2 (persistent kill_switch_state). stores/risk.ts (Sharpe/Sortino метрики из филлов) НЕ тронут — это другой, легитимный модуль. | Осталось: (1) три испорченных text-ключа на VPS БД — после деплоя дашборда самовосстановятся при повторном configure; 3 битые text-ключа УДАЛЕНЫ 2026-07-04 (оператор: «удалить ключи»); бэкапы .bak-killswitch-20260704 на VPS. WARN при старте pm-bot исчезнет. kill-switch остаётся ненастроенным (dry-run, защита симулируется); при необходимости настраивается заново через починенную панель. (2) pm-cli risk reset/status по-прежнему pm-bot-механизм — корректно для pm-bot, неприменимо к pm-engine (документировано здесь). | | PM-ENGINE-18 | Replay никогда не вызывал Strategy::on_tick — систематическая слепота бэктеста к tick-driven решениям (найдено broad-hunt 2026-07-04, лично верифицировано: в replay/mod.rs ноль вызовов on_tick; live StrategyDriver зовёт его с wall-clock интервала tick_interval()=1s НЕЗАВИСИМО от market data). Пропадали: (1) safety-net debounce стоп-лосса на ТИХОМ стакане — live стопит через ~debounce_seconds после коллапса даже без свежих фреймов, replay ждал следующего фрейма этого токена (возможно, вечно) → занижение убыточных стопов в P&L бэктеста, прямой удар по достоверности EV-вердикта; (2) WAF watchlist timeout sweep; (3) process_due_redeems. | ИСПРАВЛЕНО 2026-07-04 — live-parity синтез тиков в EngineReplayDriver::run(): перед каждым фреймом добиваются все tick-границы <= frame.ts (первый тик немедленно на start_ms — зеркало tokio::interval), каждый тик = clock.set → poll_ttl_expiry (паритет 1Hz TtlWatchdog: GTD-экспирация в тихом окне на tick-гранулярности) → on_tick (с тем же ldi_forced-враппингом, что market-data путь) → drain_and_route. Реплей заканчивается на ts последнего фрейма — хвостовые тики НЕ выдумываются (нет данных книги за концом capture). Новое поле EngineBacktestReport.ticks_fired + fold в determinism digest (расхождение тик-планирования проваливает ×2-assert, не проходит молча). 2 новых теста (silent_gap_stop_loss_fires_via_synthesized_ticks + негативный контроль negctl_..._window_ends_before_debounce — окно кончается до истечения debounce → стоп НЕ стреляет, доказывает что SELL именно от тиков ВНУТРИ окна) с дисконфирмацией (while false && → позитивный упал на ticks_fired==0, контроль остался зелёным); все 13 replay-тестов зелёные включая real-capture 21k frames (runtime не деградировал) и golden fill_parity_golden_dry_run_equals_replay. | Существующие replay-digest'ы сменились (алгоритм реплея изменён) — паритет-контракт «10× идентичный SHA256 одного бинаря» не задет. Замечание: capture-окна, записанные ДО этого фикса, при переигрывании теперь дают ДРУГОЙ (более честный) P&L — сравнения со старыми replay-отчётами некорректны без пере-прогона. | | PM-ENGINE-19 | Redemption on-chain edge cases (broad-hunt 2026-07-04, workflow-verified 3-скептик голосованием, лично НЕ перепроверено — оба VPS сейчас dry-run, латентно до live). (a) redemption_service.rs:339: перед блокирующим redemption_port.redeem().await в БД не пишется in-flight состояние; load_due_redeems фильтрует строго status='PENDING'. Креш процесса во время ожидания receipt → рестарт повторяет redeem → видит нулевые балансы (первая попытка сожгла токены) → bail!("zero balance") → классифицируется Terminal → позиция навсегда Failed, хотя редим прошёл on-chain. (b) provider.rs:58: wait_receipt() на 60s-таймауте возвращает Ok("...(pending, 60s timeout)") — RedemptionService на любой Ok ставит терминальный Confirmed без повторной проверки статуса on-chain; обычная задержка майнинга Polygon >60s даёт мусор вместо tx_hash (если замайнилась) или незамеченный реверт (если нет). (c) redemption_service.rs:346: mark_redeemed упал после успешного on-chain redeem → redeem_due_at не продвинут → ретрай без backoff, tx_hash не логируется. | ФАЗА-1 РЕАЛИЗОВАНА 2026-07-04 (коммиты 0a308b65 + 2c74cf4f + b786e924; дизайн ратифицирован codex-debater, 2 раунда, полный консенсус, thread 019f2b83). Реализовано ВСЁ из фаза-1 списка ниже: durable Submitted до вызова порта + redeem_attempted_at маркер, recovery-sweep (окно 180s по redeem_submitted_at — ВАЖНО: staleness в домене часов СЕРВИСА, не SQL wall-clock updated_at; тот остался только optimistic-токеном равенства — дефект пойман самостоятельно при написании теста sweep'а), потолок recovery=10 → громкий terminal, двухфазный reconciled-confirm (15s re-check между наблюдениями, retry-бюджет не тратится) с confirmation_source='reconciled_by_zero_balance' и NULL tx_hash, первопопыточный zero-balance остался громким Failed (детектор фантомов), wait_receipt_strict в pm-sdk (timeout/reorg=Err, 7 confirmations + canonical re-fetch по хэшу) на обоих redeem-путях (approve/wrap на legacy, документировано). 4 поведенческих + 4 примитивных теста, дисконфирмация обеих веток (3 упали воспроизводя классы бага, контроль зелёный). Workspace 4435/0, clippy чисто. НЕ задеплоено. Отступление от дебата (документировано честно): 5 отдельных audit-event-строк НЕ добавлены — переходы наблюдаемы через tracing WARN/ERROR + существующие BotEvents + колонки (attempted_at/recovery_count/confirmation_source); полный event-набор — вместе с фазой-2. Ratified design (для истории): Фаза-1 (dry-run-safe) блокеры: (1) durable dangerous-section marker redeem_attempted_at (в одной tx с Submitted, НЕ сбрасывается при откате в Pending; retry_count НЕ евидентность); (2) разбить схлопнутые CAS — durable Submitted ДО вызова SDK; (3) nullable tx_hash + колонка confirmation_source (onchain_tx|reconciled_by_zero_balance) — никакой синтетики в tx_hash; (4) zero-balance-сигнатуры ПОСЛЕ marker → Confirmed как reconciled + event (+ повторный balanceOf через 5-15s перед reconciled confirm); первая попытка БЕЗ marker → громкий Failed (детектор фантомных позиций); (5) recovery Submitted→Pending НЕ тратит retry-бюджет — отдельный recovery_count с потолком + terminal event; recovery-CAS с optimistic updated_at-token, окно 180s; (6) wait_receipt_strict в pm-sdk: timeout→Transient-err, revert→Terminal-err, success ждёт min_confirmations 5-10 (canonical re-check, не таймер); approve/wrap на legacy; (7) 5 audit-событий на переходы; (8) verification: single-threaded redeem loop (no spawn). Q4: pre-check в redeem_standard НЕ добавлять (контрактная идемпотентность лучше). | ТРЕБУЕТ SIGN-OFF ПОЛЬЗОВАТЕЛЯ до live: (а) Фаза-2 = HARD BLOCKER включения live-редимпшена: split SDK send/wait boundary (persist tx_hash сразу после send), recovery по known-hash проверяет receipt, верификация single-writer-per-wallet, canonical-re-check финальности — без фазы-2 консенсус дебата аннулируется; (б) остаточный риск: ложный reconciled confirm при внешнем redeem (оператор/другой процесс) — неустраним в фазе-1, финансово нейтрален (mark_redeemed не бучит PnL), честно маркирован confirmation_source — принять семантику; (в) гипотеза F2 ПРОВЕРЕНА 2026-07-04 и ОПРОВЕРГНУТА: оба инстанса используют ОДИН кошелёк live (общий wallets.db; БД состояний раздельные) — перед live-редимпшеном обязательна развязка: либо отдельные кошельки, либо вывод pm-bot из эксплуатации (плановый Harvester-v1 путь), либо межпроцессный lock. Также shared-wallet повышает частоту сценария «внешний redeem» для reconciled-confirm — дизайн это уже покрывает (нейтрально, маркировано). | | PM-ENGINE-20 | RISK-LIMIT 2 (budget-per-market) слеп к уже открытым позициям (broad-hunt 2026-07-04, workflow-verified, лично НЕ перепроверено). order_router.rs:441: единственный из канонических лимитов, считавший только эфемерные allocations (удаляются при терминале ордера), НЕ читая PositionRegistry. Сценарий: вход token A на весь per-market бюджет исполнился → аллокация освобождена, позиция открыта; позже sibling-token B того же condition_id проходит гейты → размеряется на ПОЛНЫЙ лимит заново — лимит-2 не видит открытый A. Backstop — только RISK-LIMIT 3 (total budget). | ИСПРАВЛЕНО 2026-07-04 (после codex-debater консенсуса, Q6/Q7 дизайн-дебата). Новый PositionQuery::sum_open_entry_cost_for_market (cache-итерация, инвариант «в cache только активные» вместо хардкод-фильтра статусов); лимит-2: projected = positions + allocations + intent — зеркало конвенции лимита-3, overlap на partial-fill консервативен (завышает занятость). Дебатер-блокеры закрыты: DCA-сайзинг проверен лично (try_dca_replace считает от budget_per_market − spent с floor-округлением → фикс не режет DCA-леги, строгое > не срабатывает на точном заполнении); гонка release-vs-position проверена (в handle_order_event позиция пишется в cache ДО release аллокации → окно даёт overlap, не undercount). 2 unit-теста с дисконфирмацией (без позиций в projected оба падают, резервационный контроль зелёный). Workspace 4425/0, clippy чисто. | Nice-to-have от дебатера (не блокер): индекс/read-model для query в hot path — cache-итерация уже O(активные позиции), на текущих объёмах несущественно. | | PM-ENGINE-21 | Неограниченный рост памяти в live-компонентах (broad-hunt 2026-07-04, workflow-verified, лично НЕ перепроверено; паттерн прежнего OOM price_level_lifetimes). (a) adapters/clob/live/registry.rs:132: OrderRegistry не эвиктит терминальные ордера — HashMap'ы растут весь lifetime процесса, mark_terminal меняет статус in-place, записи не удаляются. (b) strategies/directional/runtime_state.rs:433: cooldown_until (ConditionId→timestamp после stop-loss) insert-only, не чистится после истечения. | ИСПРАВЛЕНО 2026-07-04. (a) TTL-ретенция терминалов в OrderRegistry: 10 минут (поздние WS-филлы cancel-vs-fill гонки ещё коррелируемы), затем согласованная эвикция всех трёх структур (state/uid-маппинг/fill-дедуп пары) под одним lock'ом; prune амортизирован на register_placed/mark_terminal (реактивный паттерн, без таймеров). (b) cooldown_until: lazy-prune истёкших на каждой новой записи (сигнатура set_cooldown_until получила now). 2 unit-теста, дисконфирмация (a) (prune отключён → тест упал). | Только live-плоскость: sim/replay пересоздаются per-run. | | PM-ENGINE-22 | cancel() пишет Cancelled безусловно после REST-ack; поздний Fill молча дропается персист-гейтом (broad-hunt 2026-07-04, workflow-verified). order_router.rs:953: после client.cancel().await статус строки → CANCELLED на основе PRE-await чтения; конкурентный Fill/PartialFill того же ордера (нормальная CLOB-гонка cancel-vs-fill) затем натыкается в persist_order_event на гейт find_open_order_by_uid (status IN NEW/PLACED/PARTIAL) → молчаливый no-op. Позиция учтена ВЕРНО (D3 fallback fetch_released_snapshot доводит fill до PositionRegistry), но строка orders навсегда врёт: status=CANCELLED, устаревшие filled_shares → расхождение orders↔positions в БД. Триггер-путь: try_reactive_reprice (strategy.rs:1522) — рутинный cancel-on-drift. | ИСПРАВЛЕНО 2026-07-04. Late-fill патч в persist_order_event: Fill/PartialFill для терминальной CANCELLED/EXPIRED строки (новый find_order_any_status_with_accounting, default-реализация Ok(None) для стабов) корректирует filled_shares/filled_value/first_fill_at; полный объём исправляет статус на FILLED (исполнение опередило отмену на бирже); идемпотентно к повторной доставке (cumulative-гейт); громкий WARN-лог как маркер. Позиционный учёт не тронут (D3 fallback уже работал верно). Интеграционный тест на реальном storage с дисконфирмацией (патч отключён → строка осталась «CANCELLED», ложь воспроизведена). | Форензика (positions cross-check) остаётся второй линией. | | PM-BOT-NEGRISK-REDEEM-1 | HIGH (pm-bot, retiring, СЕЙЧАС dry-run): neg-risk auto_redeem передаёт (held, paired) как (yes_id, no_id) вслепую → выигрышная NO-side позиция НЕ редимится, средства ЗАСТРЕВАЮТ. crates/pm-bot/src/io/onchain/mod.rs:60-65 primary Some(paired)-ветка: (yes_id, no_id) = (info.token_id, paired) с ложным комментарием «pass both and let the on-chain call sort balances». pm_sdk::redeem_neg_risk строит amounts=[balanceOf(yes_id), balanceOf(no_id)] и зовёт NegRiskAdapter.redeemPositions(condition, amounts) — контракт биндит amounts ПОЗИЦИОННО к каноническому [YES(0),NO(1)], ИГНОРИРУЯ переданные token_id. Если held = NO-сторона (выигрышный NO-фаворит — ровно универсум Harvester «No»!): yes_id=NO → amounts=[balanceOf(NO)=noBal, balanceOf(YES)=0]=[noBal,0] → контракт тянет noBal каноническо-YES (там 0) → ERC-1155 REVERT. Guard yes_bal.is_zero() && no_bal.is_zero() НЕ ловит (yes_bal=noBal>0). auto_redeem (mod.rs:145-153) глотает revert как warn → SILENT. Только YES-side редимятся. None-ветка (Data API fallback, упорядочивает по outcomeIndex) — КОРРЕКТНА. Origin: defect-hunt 2026-07-04, лично верифицирован код-путь; контракт-behavior выведен из github Polymarket/neg-risk-ctf-adapter + логики позиционного binding (НЕ сверен эмпирически против deployed contract). | OPEN — документирован, НЕ пропатчен автономно. pm-engine (активный Harvester) НЕ ЗАДЕТ — RedemptionService (redemption_service.rs:328-332) маппит yes/no по явному outcome.eq_ignore_ascii_case("Yes"/"No"). pm-bot выводится (ADR-024) + dry-run → не emergency; средства застревают (редимятся позже), не теряются. НЕ патчил живой on-chain путь ночью: (1) контракт-claim эмпирически не сверен (правило: verify on-chain against production); (2) live-bot financial path — неверный фикс опаснее; (3) retiring. | ТРЕБУЕТ ОПЕРАТОРА: (а) сверить binding NegRiskAdapter.redeemPositions эмпирически (testnet/mainnet-fork NO-side redeem); (б) если подтверждено — фикс: neg-risk auto_redeem через Data-API-упорядоченный (yes,no) как pm-engine, ИЛИ yes/no по outcome-label в primary ветке; проверить pm-cli redeem --all на тот же баг; (в) ИЛИ признать retiring и редимить neg-risk-NO вручную. Cross-ref: PM-ENGINE-19. | | PM-ENGINE-24 | pm-dashboard не обрабатывает --version — бутит полный сервер. Найдено 2026-07-04 при post-deploy smoke: на VPS host:31337 пять недель (с 26 мая) висел зомби-процесс /home/deploy/pm-backtest/bin/pm-dashboard --version со старым бинарём — след старой probe-команды; затенял localhost-диагностику (публичный путь не задет: cloudflared → контейнеры через pm-tunnel-net, health-гейт деплой-скрипта — через docker exec pm-caddy). Зомби убит вручную 2026-07-04. | ИСПРАВЛЕНО 2026-07-04: ранний --version/-V guard (print+exit до любой инициализации) в pm-dashboard, pm-engine-dashboard И pm-bot (тот же класс: ручной парсер игнорировал незнакомые флаги и бутил бота). pm-engine/pm-cli на clap — неизвестный флаг и так завершается ошибкой, guard не нужен. Проверено живым запуском: pm-dashboard --version печатает и выходит. Workspace 4435/0. | Задеплоится со следующим деплоем соответствующих бинарей. | | PM-ENGINE-23 | forensics.sh ev: OPEN/SELLING позиции без забуканного PnL резолвятся через ТЕКУЩУЮ цену Gamma (forensics.sh:763: closed=false, held_price>=0.5 ⇒ won), без provenance-метки — смещает w вверх для ещё не остановленных позиций (позиция, которая ЗАВТРА словит стоп, сегодня считается winner'ом). Один из трёх скептиков счёл это осознанным anti-survivorship trade-off (открытые проигравшие ОБЯЗАНЫ учитываться — GARBAGE-IN правило), но отсутствие маркировки «резолвлено по текущей цене, не терминально» делает выборку немаркированно-смешанной. | ИСПРАВЛЕНО 2026-07-04. Каждый исход в ev маркируется source: terminal|gamma_proxy; сводка провенанса на stderr + WARN при доле proxy >20%; при наличии proxy вердикт считается ДВАЖДЫ (полный набор → stdout, контракт не изменён; terminal-only → stderr) с громким «ВЕРДИКТ НЕ ГОТОВ» при смене знака edge_margin. Учёт НЕ менялся (anti-survivorship сохранён); пайп в pm-analytics очищает provenance-поле — контракт harvester-ev байт-в-байт. Offline-smoke 20/0. | Cross-ref: CLAUDE.md EV-раздел (никакого survivorship, открытые проигравшие учитываются). | | PM-ENGINE-29 | Reconcile-heal терминального ордера освобождает budget-резервацию БЕЗ книжения позиции → недоучёт бюджета на MISSED-FILL сценарии (латентный, в УЖЕ ЗАДЕПЛОЕННОМ коде). Механизм ПОДТВЕРЖДЁН (адверсариальный ревью 2026-07-14, Consequence 2 codex-debate + личная верификация кода): (1) apply_mark_filled/apply_mark_cancelled (heal_executor.rs) патчат ТОЛЬКО order-строку (GATE-1: все heal-события position_id:None); (2) BackfillPositionFromTrades → apply_deferred = NotImplemented, а bootstrap-сверка позиций отсутствует — reconcile НИКОГДА не книжит позицию; (3) budget-резервация (allocations) рехидрируется из load_open_orders (только NEW/PLACED/PARTIAL) — терминальный ордер выпадает → резервация освобождается; (4) позиция создаётся ТОЛЬКО live fill-event путём (strategy_driver::apply_buy_fill). Итог: если fill был ПРОПУЩЕН (WS-gap / рестарт во время fill'а — ровно то, что bootstrap-reconcile призван ловить) → reconcile помечает ордер FILLED/CANCELLED(partial) → резервация released, позиция НЕ создана → RISK-LIMIT 2/3 недоучитывают потраченный кэш → рынок выглядит пустым → повторный вход реальными деньгами (double-entry) + переаллокация. Ограничения по доле баланса кошелька в системе НЕТ (лимит удалён 2026-07-28 как никогда не работавший) → on-chain backstop'а против этого недоучёта НЕТ и не появится сам собой. Существует в absent-пути с commit 88a04036 (НЕ вносится фиксом дыры №2 — PM-ENGINE-28 gated-E специально избегает этот класс, релизя резервацию лишь при executed==0). | OPEN (полный фикс ЭСКАЛИРОВАН, не патчить автономно) — money-safe INTERIM РЕАЛИЗОВАН 2026-07-17 (ветка fix/restart-integrity, одобрено оператором). Interim закрыл: тихий release резервации терминального BUY с executed>0 без забукированных филлов — в ТРЁХ точках: (1) OrderRouter::recover_from_db — после open-order рехидрации скан load_terminal_buy_orders_with_unbooked_fills (терминальный BUY, filled_shares>0, NOT EXISTS positions.entry_order_uid И NOT EXISTS fill-событий evt-fill-{uid}-t…/-c… — DCA-guard из адверсариального ревью 2026-07-17: grow-путь книжит в позицию ПЕРВОГО входа, но fill-события несут uid самого DCA-ордера, их наличие = доказанный букинг; range-сравнение по UNIQUE-индексу events.event_uid, не LIKE) → резервация alloc-held-{uid} на НАЛИТУЮ долю cost'а (cost×filled/size) ДЕРЖИТСЯ в allocated/RISK-LIMIT 2/3 (повторный вход на рынок блокируется), НЕ в view-cache; (2) runtime — driver step-3 через guarded_terminal_release: non-fill терминал BUY filled>0 без букинга (has_booked_fills_for_order = позиция ЛИБО fill-события) ⇒ hold вместо release; persistence-ошибка ⇒ SELL релизится (сторона известна из Reservation.side без БД), BUY — money-safe hold БЕЗ held-флага: следующее terminal-событие переоценивает предикат (саморазрешение без таймеров), иначе recover на рестарте; (3) self-cancel — OrderRouter::cancel шаг 6 идёт через тот же guarded-предикат (окно accounting-drain: букинг упал, строка отражена — раньше релизился напрямую). Алярмы: ERROR rule=terminal_budget_held (LogSink+TG; bootstrap — ОДНО агрегированное сообщение count+список uid, повтор на каждом bootstrap = reminder; runtime — per-order с дедупом флагом) + counter pm_engine_terminal_budget_held_total{origin}. Семантика release held: (а) появление позиции с entry_order_uid = этому ордеру — live-букинг филла (terminal Fill освобождает безусловно: driver step-2 книжит ДО step-3) либо следующий bootstrap-рескан (NOT EXISTS исключает строку); (б) операторское вмешательство: сверить филлы форензикой (forensics.sh positions), при реальном филле — вручную INSERT позиции по фактическому исполнению (следующий bootstrap освободит), при отсутствии филла на бирже — выправить orders.filled_shares=0 по CLOB; тихого release НЕТ. Отличия от дебата: TTL→degraded НЕ реализован (упрощение: held до разрешения, reminder-алярмы вместо эскалации по таймеру). Известные неточности interim'а: предикат бинарный — частично-забукированный ордер с ДОПОЛНИТЕЛЬНО пропущенным филлом release'ится (букинг существует, missed-доля не различима без пошёрного учёта); DCA-ложный-hold ЗАКРЫТ fill-events guard'ом (ревью 2026-07-17, был BLOCKER: успешный DCA-BUY навсегда попадал в held-выборку на каждом рестарте — двойной учёт allocated + алярм-спам при dca_enabled). До полного фикса остаётся: idempotent position-reconcile из reconcile-fill (книжение позиции по reconcile-данным — PM-ENGINE-H-DEFERRED-POSITIONS-RECONCILE / TIER-1A), пошёрный учёт missed-доли; apply_deferred=NotImplemented, а bootstrap-сверка позиций отсутствует. | Верификация reachability: искать в live orders/positions расхождение (order FILLED без соответствующей позиции после рестарта) — теперь это делает сам движок (алярм terminal_budget_held на bootstrap). Тесты: SQL-предикат на реальном SQLite (+release-условие по появлению позиции), recover-hold/release, guarded-release (hold+alarm-дедуп / zero-fill release=28-путь / Fill release / SELL release). Cross-ref: PM-ENGINE-28 (gated-E соблюдает reservation-инвариант — НЕ изменён), PM-ENGINE-35 (гидрация registry, ИСПРАВЛЕНО), PM-ENGINE-20 (RISK-LIMIT 2 позиции). | | PM-ENGINE-28 | Stale-PLACED строка пережила reconcile: CLOB detail-эндпоинт отдаёт «open» для давно снятого ордера → вечный Skip. Live-факт 2026-07-13 (Warsaw exit): рестарт 13:23:26 (через 1с после cancel→NotFound) — list ещё СОДЕРЖАЛ ордер → совпадение по pid → не discrepancy; рестарт 14:14:34 — diff корректно дал LocalOpenRemoteAbsent, правило запросило get_order_detail и CLOB ответил status="open" спустя 51 минуту после реального снятия (list=absent, cancel=NotFound, книга пустая — несогласованность эндпоинтов на стороне Polymarket). Ветка «open/partial» была безусловным Skip в расчёте на секундный REST-lag → строка PLACED навсегда. Warn-след был («absent-local-order remote re-appeared on detail probe — skip», 14:14:34) — найден grep'ом при расследовании. | ИСПРАВЛЕНО 2026-07-14 (по одобрению оператора): ветка detail=«open/partial» при list=absent теперь делает probe-cancel (cancel_by_exchange_id): Ok ⇒ ордер жил и снят; NotFound ⇒ ордера нет; оба исхода ⇒ MarkLocalOrderCancelled (+partial из detail). Безопасно: локально ордер и так числится открытым, отмена «своего открытого» штатна (exit переэмитится exit-presence guarantee); race с fill исправит late-fill патч PM-ENGINE-22. Транспортный сбой probe ⇒ Skip (не halt). Тесты: stale-open→probe-Ok, partial→probe-NotFound (+partial сохранён), probe-network-err→Skip, unknown-status→Halt, pid=None→no-discrepancy (5 новых; дыры покрытия из аудита 2026-07-14 закрыты). Смежный фикс: shutdown cancel-NotFound патчит строку CANCELLED (87dee310); stale-строка отремонтирована SQL. | Сценарий «list содержит терминальный ордер» — ЧАСТИЧНО ЗАКРЫТ 2026-07-14 (gated-E). Разбит по reservation-инварианту (codex-debate hole2, 4 раунда + адверсариальный ревью 20 агентов/12 находок): budget-резервацию reconcile может освобождать ТОЛЬКО при доказанном executed==0, т.к. position-reconcile из reconcile-fill глобально DEFERRED (см. PM-ENGINE-29). collect_discrepancies исключает из open-set (ДО обеих секций diff) ТОЛЬКО терминально-CANCELED zero-fill (size_matched==0) записи → local-строка становится LocalOpenRemoteAbsent → уже закалённый probe-cancel absent-путь → MarkCancelled. Reconcile СХОДИТСЯ (фильтр не пускает терминал в секцию-2 → нет RemoteOpenLocalUnknown-churn/halt на повторе — верифицировано двух-run тестом с production-executor). Это money-safe и покрывает реальный инцидент (Warsaw = SELL exit cancel, executed==0). Тесты: 5 IT (detail-cancelled, stale-open→probe-cancel, convergence, partial-not-filtered, matched-not-filtered) + unit-предикат. Первая версия (direct-heal нового терминального статуса) ОТВЕРГНУТА ревью: несходящаяся (RemoteOpenLocalUnknown→halt на повторе) + release резервации без книжения позиции (недоучёт бюджета). ОСТАЮТСЯ самозаживающимися-но-НЕ-авто-закрытыми (честный потолок, НЕ халтура): MATCHED / CANCELED-с-executed>0 / stale-«LIVE» держатся консервативно (резервация НЕ освобождается без доказанного zero-fill; строка живёт до ухода ордера из list или до position-reconcile). Полное авто-закрытие блокировано отсутствием idempotent position-reconcile → PM-ENGINE-29. | | PM-ENGINE-27 | Наш redeem-путь ходит в LEGACY NegRiskAdapter → выплата в USDC.e ВНЕ торгового pUSD-баланса; официальный pUSD-нативный адаптер существует и нами НЕ используется. Live-факт 2026-07-13: pm-cli redeem NYC-виннера выплатил $4.9987 в USDC.e (receipt: to=NegRiskAdapter 0xd91e80cf, CTF burn → WCOL → USDC.e; pUSD/Onramp в tx отсутствуют). ПЕРЕПРОВЕРКА по требованию оператора: Polymarket ИМЕЕТ официальные «thin collateral adapter contracts for pUSD-native CTF actions» (split/merge/redeem, «receives USDC.e … wraps it into pUSD, and returns pUSD to your wallet automatically» — docs.polymarket.com/trading/ctf/redeem). Адреса из ОФИЦИАЛЬНОГО py-sdk PRODUCTION (верифицированы on-chain, bytecode есть): collateral_adapter=0xAdA100Db00Ca00073811820692005400218FcE1f (std CTF), neg_risk_collateral_adapter=0xadA2005600Dec949baf300f4C6120000bDB6eAab; Solidity: Polymarket/ctf-exchange-v2 src/adapters/{CtfCollateralAdapter,NegRiskCtfCollateralAdapter}.sol. Требуется однократный approve (ERC20 + ERC1155 setApprovalForAll на адаптер — см. py-sdk approvals.py). Бонус-находка: в env есть auto_redeem_operator=0xa1200000d0002264C9a1698e001292D00E1b00af. | ИСПРАВЛЕНО 2026-07-13 (d83ed304 feat(pm-sdk): pUSD-нативный redemption через официальные collateral-адаптеры, задеплоено в тот же день). Live-верифицирован трижды: 2026-07-14 позиции 219/221 (нулевые выплаты проигравших — путь отработал), 2026-07-15/17 позиция 227 REDEEMED +0.008426 pUSD (tx 0xbef5d2d7…, денежный мост эпохи сходится в pUSD до $0.00003 — форензика wf_8261b409). Запись была stale до 2026-07-17 (нарушение fix-rules-in-docs-immediately — статус не обновили при деплое). | Верификация фикса: redeem тест-позиции через адаптер → pUSD Transfer в receipt; сверка баланса до µ$. Cross-ref: PM-BOT-NEGRISK-REDEEM-1 (позиционный binding amounts — проверить, как адаптер принимает amounts, та же ловушка). | | PM-ENGINE-26 | Тейкер-комиссия биржи не букируется: orders.fees_micro_usd всегда 0, realized завышен на fee. Live-прецедент 2026-07-13 (Cape Town stop-FOK, реальные деньги): SELL FOK 5.99 @ 0.692 booked value $4.145080, фактическое зачисление $4.081250 — недостача $0.063830. Ground truth: on-chain fill-событие V2 Exchange несёт fee=63830 µUSD; формула доков (docs.polymarket.com/trading/fees) fee = C × rate × p × (1−p) USDC round-5dp, taker-only («Makers are never charged fees»), rate = API base_fee / 20000 (калибровка: Weather base_fee=1000 ⇔ доковская ставка 0.05; live-сэмпл бит-в-бит; согласуется с py-clob-client issue #326). Maker-филлы дня сверены с балансом до µ$ — maker fee=0 подтверждён деньгами. | ИСПРАВЛЕНО 2026-07-13 (по приказу оператора): (1) pm-sdk: ClobExecutor::get_fee_rate_bps + pool get_fee_rate_bps_for (V1-SDK-кэш); (2) ставка пинится на submit в LiveOrderState.taker_fee_rate_bps (fail-loud при недоступности endpoint'а); (3) apply_fill_delta(is_taker) считает fee только для taker-кандидатов (t.taker_order_id), WS+REST пути; (4) OrderEvent::{Fill,PartialFill}.fee_micro_usd (#[serde(default)] — старые capture-ленты совместимы), value остаётся GROSS; (5) persist аккумулирует orders.fees_micro_usd; (6) позиции получают NET-кэш (BUY: value+fee; SELL: value−fee) → realized = правда денег. Тесты: live-регрессия 63830 бит-в-бит, симметрия p(1−p), round5-края, taker-carries/maker-zero IT. Sim = fee 0 по умолчанию; c 2026-07-15 (F7 постмортема 2026-07-14) sim умеет ОПЦИОНАЛЬНУЮ taker-fee модель той же формулой/округлением (SSOT adapters/clob/fees.rs): dry-run ENGINE_SIM_TAKER_FEE_BPS, replay --sim-taker-fee-bps; absent = fee 0 + WARN. См. docs/backtest/sim-fidelity-limits.md. Известное ограничение: FOK-филл, доставленный ТОЛЬКО Order::Update-фреймом (без Trade), букирует fee=0 — REST-reconcile с taker_order_id вторая линия. | Follow-up: forensics ev учитывает fees_micro_usd в w*/edge; исторический Cape Town realized −0.816393 НЕ ретро-исправлен на −0.880223 (одна позиция, задокументировано здесь; править вместе с решением по PM-ENGINE-27). | | PM-ENGINE-30 | Shared-allocation release снимает snapshot in-flight стоп-ордера (oversell-окно). Терминал resting exit @exit_price и стоп-SELL шарят одну alloc-exit-{signal}; submit_allocated безусловно ре-биндит bound_order_uid, release() терминального события удаляет snapshot ТЕКУЩЕГО bound uid. Интерливинг «@0.99 терминалится, пока стоп-FAK in flight» ослепляет in-flight guard → возможен второй параллельный стоп-SELL на тот же остаток (live-исход — reject по балансу, не потеря денег; sim — oversold halt). Частота повышена FAK kill→re-emit циклами капитуляции. Origin: adversarial review волны 1 (2026-07-14), ПОДТВЕРЖДЁН кодом, отложен repair-агентом как нетривиальный. | OPEN — требует отдельного change + sign-off. Наивный фикс (чистить snapshot по order_uid) создаёт худший режим: терминал FAK позже no-op'ит release без cleanup → вечный in-flight guard → стоп подавлен навсегда. Нужен редизайн: per-order release + tolerant reservation lifecycle + тест-матрица reconciliation/hydrate/self-cancel. | Файлы: order_router.rs (release :974, submit_allocated :753), strategy.rs in-flight guard (:1158-1176). | | PM-ENGINE-31 | Re-fire guard стопа (in-flight ордер) без staleness-таймаута. Live Ok-ack с потерянным терминальным событием (класс PM-ENGINE-28: CLOB detail «open» 51 мин) оставляет snapshot Placed → каждая подтверждённая оценка видит stop_in_flight=true → ноль ре-эмитов до реконсиляции; всё это время позиция без стопа в подтверждённом обвале. Sim терминал не теряет никогда — асимметричный failure mode. Origin: adversarial review волны 1, ПОДТВЕРЖДЁН, отложен консервативно (самолечение = reconciliation-пул + probe-cancel 88a04036). | ИСПРАВЛЕНО 2026-07-17 (вариант (b), одобрен оператором в предеплойной волне): guard дополнен staleness-probe'ом БЕЗ нового config-knob'а (RULE-NEW-01) — Placed/Partial-снимок стоп-SELL'а старше структурной константы STOP_REFIRE_PROBE_TTL_MS (30s = 30 тик-циклов, 3× щедрого BALANCE_SETTLING_BOUND_MS; обоснование в doc-комментарии) на подтверждённой оценке инициирует probe-cancel через машинерию PM-ENGINE-28: OrderRouter::probe_cancel_stale → cancel_by_exchange_id по durable pid (registry после рестарта пуст, PM-ENGINE-35). Ok ⇒ ордер жил и снят; NotFound ⇒ ордера нет; оба ⇒ общий book_cancelled_row (тот же хвост, что у cancel: строка CANCELLED + event + publish, БЕЗ release — release-семантика у caller'ов разная) + release_probed_order ⇒ guard отпускает, капитуляция ре-эмитится в той же оценке. Транспортный сбой ⇒ guard удержан, retry на следующей оценке, throttled WARN. New-снимок (нет ack'а) не probe'ится. Race с fill'ом исправит late-fill патч PM-ENGINE-22 (та же семантика, что reconcile-probe). Защитная ветка терминальной строки при живом snapshot'е чистит ТОЛЬКО view (reservation — зона release()-путей, PM-ENGINE-29 не задета). Счётчики stop_refire_probe_total/_cleared (runtime_state, в SSE не маппятся — отдельное изменение контракта), throttled INFO/WARN. Тесты: probe-Ok/NotFound → ре-эмит + строка CANCELLED, транспорт → guard удержан + retry, свежий in-flight (<TTL) → no probe (нет cancel-спама), sim-инвариант в регрессионном kill→re-emit (ноль probe-вызовов/счётчиков — sim-поведение бит-в-бит) + 2 unit (terminal-row view-only, no-pid defer). Ревью-упрочнение (адверсариальное ревью предеплойной волны, 3 major закрыты): (M1) probe «Ok» от cancel_by_exchange_id НЕ доказывает zero-fill (pm-sdk выбрасывает тело CancelOrdersResponse) → перед finalize_cancelled_order дизамбигуация через get_order_detail: size_matched > filled_shares ⇒ StopProbeOutcome::ExecutedUnbooked (патч filled_shares, строка остаётся PLACED, резервация ДЕРЖИТСЯ) ⇒ стратегия включает accounting-drain, sticky-подавляет ре-эмит (mark_stop_probe_escalated) — капитуляция НЕ продаёт замаскированно-проданное; только доказанный size_matched ≤ filled_shares ⇒ CANCELLED + guarded release. (M2) shared exit-allocation: release_probed_order резолвит alloc ТОЛЬКО если bound_order_uid == probed, чужой живой bind не трогает; стратегия после Ok перечитывает in-flight набор перед FIRE (не слепой fall-through) — конкурентные FAK при reprice-интерливинге исключены. (M3) рестарт: placed_at добавлен в OrderRow/load_open_orders* и копируется при рехидрации (был None → staleness-предикат вечно false → probe мёртв после рестарта, т.е. ровно Warsaw-профиль); IT-тест рехидрации стейл-PLACED → probe срабатывает. Плюс: persistent probe-fail streak → RefireWedged stop-alarm (не только WARN). | В sim probe-путь структурно недостижим (in-process терминал не теряется). Live-верификация: grep stop refire probe + counters после следующего wedge-инцидента. | | PM-ENGINE-32 | Хвосты волны 1 стоп-контура: (а) deploy-grace толерантность к stale stop_loss.max_loss_milli ключу; (б) seed metrics.total_usd отдавал realized вместо total budget; (в) admission = memoryless tick-race, а не FIFO из дизайн-дебата selection-layer (Munich-27 ждал 49 мин, слот взял London) + два бакета одного события (Munich 27°C и 30°C) одновременно проходят d_fav≥2 — оба вопроса в дизайн-дебат Ф3 (ранжирование поверх решённого fill-слоя, лимит «1 бакет на событие»). | (а)/(б) ИСПРАВЛЕНЫ 2026-07-17 (fix/predeploy-smalls): (а) StopLossConfig получил стандартный deny_unknown_fields (как Selection/CircuitBreaker), grace-тест заменён тестом отклонения stale-ключа, schema-артефакт регенерирован; (б) collect_metrics_snapshot маппит total_usd из budget_card_triple (тот же OrderRouter::budget_snapshot, что SSE-тик и TG-footer — одна арифметика), boundary-тест бюджет 500 → 500.0. (в) OPEN — дизайн-дебат перед re-enable масштабирования. | Origin: incident-2026-07-14 + funnel-расследование wf_da6befa6. | | PM-ENGINE-33 | Reconcile после user-WS reconnect синтетически отменяет полностью исполненный на бирже ордер; «price quarantined» неканоническая fill-цена оставляет фантомный residual. Live-факт 2026-07-15 (Guangzhou, поз. 227): fill-принт пришёл с ценой 0.989999938118429 (15 дробных знаков, вне 4dp-канона) → correlator quarantine → локальный учёт 4.991574/5 sh при бирже «исполнен полностью»; при reconnect-reconcile ордер absent в снапшоте открытых → diff_absent (reconcile.rs:422-435) выставил синтетический Cancelled(Reconciled) на PARTIAL-строку. Residual 0.0084sh доехал до резолюции, REDEEMED, ущерб ~$0 — но orders-строка врёт (CANCELLED вместо FILLED), и residual-класс на не-redeem рынке остался бы пылью. | (а) ИСПРАВЛЕН 2026-07-17 (fix/predeploy-smalls): correlator (ingest_fill_price) канонизирует precision-класс (>4dp, в диапазоне [0,1]) банковским округлением (MidpointNearestEven — направление-нейтрально для BUY и SELL филлов, ошибка ≤ ½ pip, float-ε класс восстанавливает ровно сеточную цену сделки) к pip-сетке; объём принта книжится ПОЛНОСТЬЮ (Trade taker/maker + Order-UPDATE пути); counter pm_engine_fill_price_canonicalized_total + throttled WARN (60s); подлинно некорректная цена (вне [0,1]) отбраковывается — range-check добавлен явно (raw-конвертер диапазон не проверял, ≤4dp out-of-range ранее книжился бы сырым). Тесты: прецедент 0.989999938118429 → 0.99/объём 5/5/counter=1; 1.5 → карантин; UPDATE-путь. Residual-зеркала в reconcile.rs (quarantine_reconcile_price, site=reconcile) НЕ тронуты — тот же volume-loss класс, чинить вместе с (б). (б) OPEN: diff_absent для PARTIAL-строки с executed>0 — сверять filled с биржевым detail до синтеза Cancelled. | Origin: incident-2026-07-15-stop-whipsaw-losses.md (форензика wf_8261b409). Cross-ref: PM-ENGINE-22 (late-fill патч), PM-ENGINE-28. | | PM-ENGINE-34 | Settling-окно после каждого entry-филла: первый эмит primary exit отклоняется CLOB («balance settling», not enough balance), 4–5с позиция без exit И без стопа. Систематика 2026-07-15: все 11 входов дали REJECTED первый SELL в ту же секунду, retry через 4–6с ставил exit успешно. Окно мало, но это ровно момент toxic-филла (adverse selection: обвал часто начинается сразу после нашего филла — Qingdao 07-14 через 6 мин, Beijing 07-15 через 4 мин). | OPEN (низкий приоритет). Направления: pre-signed exit с отложенным submit по settling-ack; либо принять как irreducible CLOB-лаг и задокументировать в sim-fidelity-limits (sim этого окна не имеет). | Origin: incident-2026-07-15. | | PM-ENGINE-35 | OrderRegistry (fill-коррелятор) не переживает рестарт процесса: in-flight ордер после рестарта теряет букинг остатка филла. Registry конструируется пустым единственным продовым call-site (adapters/clob/live/client.rs:141), bootstrap-reconcile его НЕ гидрирует (heal-действия косметически патчат orders-строку, BackfillPositionFromTrades = no-op заглушка PM-ENGINE-H-DEFERRED-POSITIONS-RECONCILE). WS Trade/Update-кадры по ордеру, открытому до рестарта, падают в unknown exchange_id → push_pending навсегда (дренаж только на register_placed того же exchange_id). Cross-path идемпотентность fill-фикса 2026-07-17 переживает WS-реконнект, но НЕ процесс-рестарт. Найдено адверсариальным ревью fix/fill-pipeline-exactly-once (2026-07-17); предсуществующий разрыв, НЕ регрессия фикса. | ИСПРАВЛЕНО 2026-07-17 (ветка fix/restart-integrity, одобрено оператором): LiveOrderClient::new гидрирует registry активными (NEW/PLACED/PARTIAL) ордерами из orders ДО spawn'а user-WS (нулевая гонка с кадрами): hydrate_registry_from_open_orders — filled_shares → водяной знак max_update_size_matched (= cumulative_filled_shares; кламп [0, size] + warn на порченой строке filled>size — иначе ложный InvariantViolation→drain), cum_from_trades=0 (пре-рестартные Trade-кадры потеряны, user-WS без replay) — max-семантика добукирует ровно недостающие shares парой живых кадров Trade+UPDATE, exactly-once (FillIdentity/has_event_uid на booking-слое). Строки без polymarket_order_id нерегистрируемы (ack-pending, остаются на reconciler'е). Ограничение: taker_fee_rate_bps не персистится → 0 у восстановленных (Harvester = post-only maker; gap виден через pm_engine_fill_fee_unbooked_total). Тесты: tests/it/order_registry_restart_hydration.rs (реальное SQLite, PARTIAL 2/5 → книжит ровно 3, терминалы/ack-pending не регистрируются). | Cross-ref: PM-ENGINE-29, PM-ENGINE-28. | | PM-ENGINE-36 | Own-fill не исключается из анонимного confirm-потока стоп-контура при ENGINE_RTDS_ENABLED=false (live-дефолт). Наш собственный стоп-FAK-филл попадает в recent_sell_trades как обычный прямой SELL-принт без own-метки и может квалифицировать/корроборировать ПОВТОРНЫЙ выстрел по остатку той же позиции (partial-капитуляция → re-emit). Actor-tagged own-exclusion существует только в RTDS-буфере (adapters/rtds/mod.rs), выключенном на live. Практический эффект ограничен: re-fire происходит внутри УЖЕ подтверждённого независимым принтом обвала — эквивалент задуманного цикла капитуляции «без пола»; двухисточниковая корроборация 2026-07-17 частично страхует (5sh < strong_shares → безусловное правило не проходит). Найдено адверсариальным ревью fix/stop-confirm-corroboration. | OPEN (низкий приоритет). Направление: стратегия ЗНАЕТ свои филлы из order-событий — вести короткий own-fills буфер (ts, price, size) и исключать совпадающие принты из qualifying/witness наборов; либо включить RTDS-путь на live (отдельное решение — cutover-runbook). | Cross-ref: ревью 2026-07-17, incident-2026-07-15. | | PM-ENGINE-37 | Свидетели confirm-корроборации жили в общем count-based ring-буфере (500 событий всех типов) и при обвале вымывались PriceChange-шумом. Это позволяло ранее vetoed low-print стать default-consent подтверждением после вытеснения higher witness. | IMPLEMENTED, PENDING FORWARD SMOKE (lossless-stop-confirm-trade-window): stop-confirm/flow переключены на отдельную trade-only историю без count-cap до untrack; >500 quote и >500 trade регрессии, Atlanta veto, fail-closed incomplete window, processed-МНОЖЕСТВО потреблённых идентичностей (HashSet<TradeIngestId> — курсор был рассмотрен и ОТКЛОНЁН, design.md 7.5) и partial-FAK latch покрыты тестами. Не помечать RESOLVED до продового smoke метрик retained rows/bytes и отсутствия incomplete/default-consent fire. Остаточный долг (переписан по факту кода 2026-08-04): (1) разрывы RTDS ПОДКЛЮЧЕНЫ — RtdsActorBuffer::record_connection_gap / close_connection пишут Reconnect / ParserLoss / SourceSilent и счётчик pm_engine_stop_trade_source_gaps_total{source="rtds"}; у АНОНИМНОГО источника подключены только Reconnect и Overflow, а производителей ParserLoss / SourceSilent у него нет — рыночный WS не сообщает ни о сбоях разбора, ни о собственной тишине (это и есть остаточное ограничение, оно сужено в спецификации, а не умолчано); (2) resubscribe как класс разрыва БОЛЬШЕ НЕ СУЩЕСТВУЕТ — вариант снят с типа задачей 8.16 (повторная подписка не рвёт непрерывность); (3) покрытие принадлежит ПОДПИСКЕ, а не первому принту токена: эпоху wildcard-соединения открывает первый доставленный кадр и она распространяется на все токены подписки, поэтому прежняя оговорка «actor-source conservatively incomplete до первого print» недействительна; (4) RTDS включать нельзя до закрытия семи предпосылок задачи 10.9 — они требуют живых измерений. | Cross-ref: ревью fix/stop-confirm-corroboration 2026-07-17; openspec/changes/lossless-stop-confirm-trade-window/. | | PM-ENGINE-25 | Continuous LIVE ошибочно наследовал probe-only $5 BUY / $6 SELL fuse и останавливал торговлю при большем валидном config.db budget. Инцидент 2026-07-25: router авторизовал ~$19.x BUY при budget_per_market=$20, live adapter локально отклонил все попытки ProbeFuseViolation. | IMPLEMENTED, PENDING BOUNDED LIVE VERIFICATION (safe-budget-derived-live-submit-fuse) — fixed caps оставлены только --probe; continuous LIVE получает immutable router authorization, сверяет полный intent fingerprint и точные canonical↔wire price/size/notional до POST. Новый env/CLI/config cap не добавлен, состав canonical RISK-LIMITS не расширен, SELL авторизуется по точному exit size и не entry-budget-capped. Не закрывать как RESOLVED до успешного BUY >$5 + соответствующего exit на VPS с чистыми integrity counters. | Rollback: previous binary + budget_per_market <= $5 до restart. Origin: live-by-default enablement 2026-07-13; incident 2026-07-25. | | PM-ENGINE-38 | Возвращённые on-chain средства по незабукированным филлам НЕ отражены в realized PnL — книги расходятся с фактом на +$4.86. 2026-07-26 на кошельке live обнаружены 5 холдингов без единой строки в positions: биржа исполнила BUY (потрачено ~$22.14), движок записал filled=0, CANCELLED (4 из 5 — через операторскую отмену /api/local/orders/{id}/cancel, 1 — «expired»; сделка на бирже проходила через 2-35 с ПОСЛЕ постановки, цена и размер совпадают с ордером). Все 5 рынков резолвнулись в пользу позиции. Средства ВОЗВРАЩЕНЫ 2026-07-26 через pm-cli redeem --all (27 акций → $27.00; tx 0xcb968d66…, 0x3a97b478…, 0x44ab395d…, 0xa62da4b7…, 0xebdf3a29…, все status=0x1). Но т.к. строк в positions не было и нет, движок не видит НИ расхода $22.14, НИ поступления $27.00: realized PnL показывает $8.09 по 46 закрытым, чистая прибыль +$4.86 отсутствует в книгах. Косвенно виден только баланс кошелька (pUSD), который движок читает для бюджета/лимита «% кошелька». Смежно: реконсилятор rule_resolve_absent_local_order → MarkLocalOrderCancelled(partial_fill_shares: null) обнулил filled_shares у ордера, налитого биржей на 5.99313 акции (строка ордера лжёт, ломает форензик-инвариант «реальный EXIT = orders SELL FILLED»); bootstrap-реконсиляция 2026-07-24T10:55 отчиталась action_count=0 при пяти непокрытых холдингах. Верифицировано тремя независимыми способами: перечень холдингов data-api (sizeThreshold=0), пер-токенная проверка отсутствия строк, on-chain balanceOf (два независимых RPC-узла). | OPEN (детект/предотвращение) — проводка задним числом ОТМЕНЕНА оператором 2026-07-27 (статистика будет сброшена перед новым запуском, историческое расхождение не исправляется). Ручная правка живой engine.db была отклонена как money-unsafe. Требование orphaned-holding-recovery (openspec unbooked-fill-detection-and-recovery) сохраняется для БУДУЩИХ сирот. Полная сверка по цепочке 2026-07-27 (сошлась до цента): за период 07-23 15:43 → 07-27 08:00 портфель 330.79 → 402.39 (+71.60) при внешнем притоке +71.84 ⇒ фактический результат торговли −$0.24, то есть рост баланса — это ВНЕСЁННЫЕ средства, а не прибыль. Книги движка показывают −4.31 ⇒ мимо учёта +4.07 (= незабукированные филлы: −22.14 расход / +27.00 возврат, минус застрявшие остатки). Контрольное окно 07-26 20:25 → сейчас закрылось точно (ожидалось 402.39, факт 402.39, расхождение −0.00): рост кэша +124.33 — чистая распродажа инвентаря, внешних поступлений нет. Источник притока (внешний перевод либо обмен USDC.e→pUSD) по цепочке НЕ подтверждён: free-tier RPC не отдаёт исторические логи, обозреватель за Cloudflare. Величина +71.84 опирается на озвученную оператором базу $330; результат торговли −$0.24 от этой базы НЕ зависит (она сокращается в арифметике). | Корень (почему филл не забукирован) НЕ установлен — блокирующее исследование в задачах 1.1-1.3 того же change'а. Детект специфицирован там же (wallet-holdings-reconciliation): направление «холдинг → позиция» обязательно, т.к. AccountingGuard срабатывает только на шве маршрутизации fill-события и к этому классу слеп по построению, а сверка «реестр → цепочка» зелёная по всем известным позициям и всё равно пропускает дыру. Порядок приземления: детект → правдивость записи → возврат. Прежний хвост «и только потом косметический фикс terminal_sell_leftover» СНЯТ 2026-08-12 решением оператора: фикс приземлён отдельно (openspec terminal-sell-residual-dust-accounting), гасит только подкласс «остаток МЕНЬШЕ лота CLOB», а fill_booking_error / sell_booking_lost / correlator_overfill / остаток ≥ лота продолжают поднимать сторож — «единственным индикатором» этот подкласс не был, и к осиротевшим холдингам сторож слеп по построению. Cross-ref: PM-ENGINE-10 (partial-fill-into-dust), PM-ENGINE-35 (registry не переживает рестарт). | | PM-ENGINE-39 |В РАБОТЕ — заявка fill-accounting-idempotency (заведена; носитель отметки идемпотентности выбран решением оператора 2026-08-19 после того, как два прежних варианта были опровергнуты чтением кода). ЗАКРЫВАЕТСЯ правкой: накопление стоимости и комиссии стало идемпотентным по идентичности филла (FillIdentity, отметка в новой таблице order_fill_accounting в ТОЙ ЖЕ транзакции, что патч orders); попутно закрыт недоучёт комиссии на пути позднего филла (patch_late_fill_after_terminal вовсе не патчил fees_micro_usd); появилась сверка накопителя с суммой учтённых филлов на bootstrap со счётчиком, записью журнала и границей выборки. ОСТАЁТСЯ ОТКРЫТЫМ: исторические строки, долитые ДО выката, — у них отметок нет, сверка внутри движка их не видит и честно считает отдельным числом «вне выборки»; их покрывает forensics.sh accounting-preconditions --engine, а решение о проводке принимает оператор (прецедент PM-ENGINE-38: правка живой engine.db отклонена как money-unsafe). | OPEN — решение оператора не принято (находка побочная, всплыла при замерах 9.1/9.2 change'а open-position-realized-pnl-so-far-column; openspec-заявка НЕ заведена). Фикс потребует идемпотентности накопления стоимости и комиссии по идентичности филла плюс сверки существующих строк. | Деньги не задеты, учёт — задет. OrderRouter::budget_snapshot считает из positions.sum_open_entry_cost + стоимости открытых заявок, filled_value в бюджет не входит; pm-engine-dashboard и pm-bot-events колонку не читают вовсе. Портится аналитика по ордерам и heal_executor, который берёт max(price×shares, накопленное) и потому ЗАКРЕПЛЯЕТ завышенное значение вместо исправления. Побочно ослабляет форензику: способ B2 подкоманды (разность entry_cost − filled_value) опирается на этот агрегат, поэтому позиция с настоящей комиссией И завышенным filled_value прочиталась бы как беспошлинная — оговорка внесена в шапку подкоманды и в scripts/analysis/README.md. Перепроверка: та же подкоманда, блок расхождения печатает price_pip и производную price×shares как дискриминатор двух путей порчи. Cross-ref: PM-ENGINE-38 (незабукированные филлы), PM-ENGINE-22 (поздний филл после отмены). | | PM-ENGINE-40 | Реплей считает филлы БЕЗ комиссии, бой — с комиссией; отчёты бэктеста систематически оптимистичнее живой торговли. crates/pm-engine/src/replay/mod.rs передаёт в реестр MicroUsd(0) вместо комиссии события — три вызова apply_buy_fill/apply_sell_fill с комментарием «replay GROSS-ориентирован, fee=0» (проверено чтением кода 2026-08-16, строки ~2801/3082/3102). Пока комиссия на wire-строке была всегда нулевой (см. PM-ENGINE-39 и хвост 8.9 change'а position-drawer-fills-table), расхождения не существовало. После выката фикса комиссии живой SELL несёт реальную величину: замер на позиции 237 — $0.0487 при валовых $1.4756, то есть 3.3 % выручки продажи. Реплей эту величину не вычитает, значит exit_value и realized в его отчётах завышены на сумму комиссий продаж. BUY у боевой стратегии maker (post_only=true) и комиссии не платит, поэтому перекос односторонний — в пользу более радужного результата. | OPEN — решения оператора нет. Фикс: передавать комиссию события в реестр так же, как это делает StrategyDriver (fill_routing_args → route_fill_to_registry). Затрагивает CI-гейт паритета parity.yml: отчёты сдвинутся, и эталонные digest'ы придётся переснять — это часть работы, а не побочный эффект. | Практическое следствие: НЕ сравнивать доходность реплея с боевой напрямую до фикса; расхождение растёт с числом taker-выходов. Cross-ref: PM-ENGINE-39 (идемпотентность денежных агрегатов), position-drawer-fills-table задача 7.7 (там же найдено). | | PM-ENGINE-41 | Результат отдельной продажи в drawer'е считается от ЕДИНОЙ средней входа позиции, а не от средней НА МОМЕНТ филла. Величина показывается второй колонкой списка событий (position-drawer-fills-table) и точна, пока все покупки предшествуют всем продажам: сумма столбца тогда сходится с итогом позиции. При перемешанном DCA (докупка между продажами) ранние продажи получают смещённый вклад — знак итога это не меняет, но per-fill числа неверны. dca_enabled=true в боевой конфигурации, то есть случай достижим. | DEFERRED — ворота оператора НЕ открывались (секция 6 задач change'а position-drawer-fills-table: опциональное avg_entry_at_fill в BotEvent::Fill/SimFill → PositionRegistry → FillRow). Работа спроектирована и оценена, но требует расширения контракта провода ради точности отображения — решение принимает оператор. | Пока ворота закрыты, ограничение названо в спеке dashboard-position-details («величина ПРОИЗВОДНАЯ… при перемешанном DCA ранние продажи получают смещённый вклад») и в doc-комментарии sellOutcomeUsd. Итог позиции от этого не зависит: он приходит с провода отдельной величиной. | | TD-MD-CEILING-1 | Признак насыщения выведен, но взят МГНОВЕННЫМ замером раз в секунду — после снятия ограничителя диспетчера этого больше не хватает. Оба независимых измерения, которых требовала прежняя редакция строки, СУЩЕСТВУЮТ с коммита 60ed6e00 (волна 2), и прежняя формулировка «ни глубина очереди, ни доля схлопнутых не выведены ни на одну поверхность» ОПРОВЕРГНУТА чтением кода: глубину и ёмкость очереди публикует MarketDataQueueProbe::publish_gauges (pm_engine_market_data_queue_depth / _capacity, проводка main.rs:1801, вызов metrics_pump.rs:484), схлопнутые кадры считает pm_engine_market_data_coalesced_dropped_total (clob_ws.rs:420) плюс поле лога metrics-pump'а. Что действительно осталось: DEFAULT_METRICS_TICK_INTERVAL_MS = 1000, и замер МГНОВЕННЫЙ. При ограничителе очередь держалась полной устойчиво, поэтому секундная выборка её ловила. После перестановки (dispatcher-arm-priority-replaces-ceiling) темп вычитывания кратно выше темпа поступления, очередь нормально пуста и наполняется ВСПЛЕСКАМИ (например, на время одной отправки ордера из хука — десятки миллисекунд), а всплеск длиной ~70-90 мс секундная выборка застаёт примерно в одном случае из двенадцати. Влияние: оператор увидит устойчивый ноль и не отличит «запас есть» от «регулярно упираемся всплесками». Требуемое действие: публиковать НАИБОЛЬШУЮ занятость с прошлой публикации (high-water-mark, сброс на чтении) рядом с мгновенной, а не вместо неё; отношение входящих кадров к доставленным снимкам по-прежнему не атрибутируется, и вычитание одного вклада из другого негодно в принципе — источники связаны противодавлением (полная очередь market_data → продюсер ждёт на отправке → заполняется DOWNSTREAM_BUFFER → emit-ветвь forwarder'а голодает → доля схлопнутых растёт). | PARTIALLY IMPLEMENTED — поверхность выбрана и работает (ряд pm_engine_* + лог metrics-pump'а); открыт только переход от мгновенного замера к high-water-mark. Проверка полезности порога требует вывода инстанса из drain-режима. | Механизм и его следствия зафиксированы: docs/target/engine.md, раздел «Тракт доставки рыночных данных стратегии»; capability market-data-ingestion-capacity (openspec/changes/market-data-throughput-ceiling/). Затронет crates/pm-engine/src/adapters/orderbook/{coalesce,clob_ws}.rs. Поле SSE потребует liveness-теста на наблюдаемой границе (образцы — crates/pm-engine/tests/it/entry_funnel_*_liveness.rs); ряд pm_engine_* — не потребует. | | TD-MD-CEILING-2 | Стоимость хука on_market_data измерена только на холостом ходу: перцентили сняты в окне с нулевым числом отправок (окно exec пусто). Влияние: верхняя оценка стоимости одного вызова хука измерением НЕ подтверждена — StrategyDriver::invoke_hook дожидается хук целиком, а хук на пути входа может выполнить чтение из БД, полную транзакцию записи ордера с fsync и сетевой POST в CLOB; SLOW_HOOK_THRESHOLD (1 с) только логирует. После снятия ограничителя диспетчера именно эта величина задаёт верхнюю границу темпа доставки, поэтому без неё пропускная способность тракта не предсказуема, а аргумент «процессор дешёвый» не доказан. Требуемое действие: снять e2e_processing / exec под нагрузкой с реальными отправками и зафиксировать режим инстанса. | DEFERRED — требует вывода инстанса из операторского drain-режима; торговое решение, отдельно от фиксации знания. | Нормировано требованием Throughput tradeoff rests on loaded measurement (capability market-data-ingestion-capacity). Процедура замера и оговорки полей — docs/target/engine.md. | | TD-MD-CEILING-3 | Исправление пропускной способности выбрано и реализовано: ветвь рыночных данных переставлена в КОНЕЦ списка biased, счётчик market_data_streak с предикатом allow_market_data и константа MAX_MARKET_DATA_STREAK = 64 удалены. Вариант выбран потому, что порядок объявления ветвей строго сильнее счётчика: biased опрашивает список сверху вниз на каждый poll, поэтому непрерывно готовые рыночные данные не способны отобрать управление ни у одной ветви выше. Ни одного из перечисленных ранее компромиссов вариант не требует: приоритизации рынков нет, MARKET_DATA_CHANNEL_BUFFER не тронут, денежные механизмы остались в ветви tick, а её приоритет стал ВЫШЕ рыночных данных. Остаток по строке: форвард-замер (возраст снимка, темп доставки, e2e_processing под нагрузкой) не снят — прежние e2e_p50 ≈ 3.0 с / e2e_p95 ≈ 4.0 с были следствием снятого ограничителя и недействительны. | IMPLEMENTED, PENDING FORWARD MEASUREMENT. ★Выкат на торгующий инстанс ЗАПРЕЩЁН до закрытия волны B (directional-dca-fill-progress-and-dust-terminal, flow-imbalance-stop-confirmation): снятие ограничителя умножает число вызовов on_market_data, а вместе с ним частоту попыток повторного входа и срабатываний стопа. | Изменение: openspec/changes/dispatcher-arm-priority-replaces-ceiling/. Механизм и процедура замера — docs/target/engine.md, раздел «Тракт доставки рыночных данных стратегии». Инвариант закреплён crates/pm-engine/tests/it/strategy_driver_arm_priority.rs — три теста порядка ветвей и ОТДЕЛЬНЫЙ денежный тест периода ветви tick под непрерывно пополняемой очередью снимков (tick_keeps_its_cadence_under_a_continuously_refilled_snapshot_queue): без него перенос ветви tick ниже рыночных данных проходит все тесты порядка, ухудшая период тика в 47-134 раза. Ожидаемый побочный эффект замера: markets_evaluated вырастет кратно, поэтому все доли воронки, снятые ДО перестановки, с послеперестановочными не сопоставимы. | | TD-STOP-OBS-1 | Телеграм-алярмы стоп-контура не описаны ни в одном рантбуке, и популяция алярма StopKill НЕ совпадает с популяцией счётчика stop_loss_kills. В crates/pm-engine/src/observability/stop_alarms.rs живут пять классов — StopKill (stop_loss_kill), NoBidsHold (stop_loss_no_bids_hold), CapitulationPartial (stop_loss_capitulation_partial), UnrealizedDrawdown (position_unrealized_drawdown), RefireWedged (stop_refire_wedged) — с троттлингом STOP_ALARM_THROTTLE_MS = 15 мин на пару (позиция, класс). В alerts-and-monitoring.md о них нет ни строки: оператор получает сообщение в Telegram и не имеет документа, объясняющего класс, порог и ожидаемое действие. Расхождение популяций (сверено по коду strategies/directional/strategy.rs): sync-канал (ошибка отправки капитуляционного FAK) шлёт StopKill всегда, async-канал (терминальная отмена) — ТОЛЬКО при cumulative_filled == 0, тогда как счётчик stop_loss_kills инкрементируется в обоих каналах, включая убийство остатка после частичного филла (там уходит CapitulationPartial, а не StopKill). Оператор, сверяющий число алярмов с числом в часовой сводке, обязан знать, что это разные множества, иначе недостача алярмов читается как «алярмы не доходят». | OPEN — документационный пробел, код не трогается. Требуемое действие: раздел в alerts-and-monitoring.md с пятью классами, троттлингом и явной оговоркой о несовпадении популяций StopKill / stop_loss_kills. | Зафиксировано при чистке стоп-поверхности воронки входа: openspec/changes/exit-reason-overlay-on-fills-chart/design.md §B6.1. Само изменение этот пробел не закрывает — оно снимает девять стоп-счётчиков с панели воронки, а не описывает алярмы. | | TD-STOP-OBS-2 | Часовая INFO-сводка воронки молчит до часа после КАЖДОГО перезапуска движка — ровно в окне, где перезапуск чаще всего связан с инцидентом. RuntimeState::note_funnel_summary (crates/pm-engine/src/strategies/directional/runtime_state.rs) на первом тике только ОТКРЫВАЕТ окно и возвращает None; первая печать наступает лишь по истечении FUNNEL_SUMMARY_LOG_INTERVAL_MS = 1 ч (strategies/directional/strategy.rs). Дельты не теряются — первый тик уже попал в аккумулятор, — но канал расследования стоп-инцидентов (двенадцать полей стоп-контура в docker logs, единственная поверхность, переживающая перезагрузку страницы) недоступен до истечения первого окна. Влияние усиливается тем, что после снятия девяти стоп-счётчиков с панели воронки часовая сводка остаётся единственным их потребителем. | OPEN. Направления (решение не принято): печать частичного окна при завершении процесса; печать первого окна по укороченному интервалу после старта; операторская команда «показать текущее окно». Любой вариант меняет формат/частоту лог-строки — согласовывать до реализации. | Механизм зафиксирован: openspec/changes/exit-reason-overlay-on-fills-chart/design.md §B6.2. Граница объёма того изменения явно защищает часовую сводку от правок, поэтому пробел закрывается отдельно. | | TD-STOP-OBS-4 | Неудавшаяся попытка выхода не наблюдаема ни на одной поверхности — кандидат на отдельное изменение. Наложение причин закрытия на графике потока показывает только СОСТОЯВШИЕСЯ закрытия: стоп, убитый биржей без филла, закрытой позиции не создаёт и в наложение не попадает по построению. Реконструкция по таблице ордеров недостаточна: stop_loss_kills инкрементируется в ветке ошибки emit_order, а ошибка может произойти ДО вставки строки ордера (в пути есть точки раннего возврата), поэтому «нет строки» не означает «не было попытки». На живых данных инстанса контрпримеров пока нет — 36 стоповых ордеров с 2026-07-23 (28 REJECTED с нулевым филлом, 6 FILLED, 2 CANCELLED с частичным филлом), у всех заполнен terminal_at, — но код такую дыру допускает. Дополнительно: правило «терминальный статус, не FILLED, при частичном филле» даёт верное КОЛИЧЕСТВО частичных капитуляций, но неверное действующее лицо — обе живые строки терминализованы оператором и внутренним probe движка, а не биржей; единственный дискриминатор — reject_reason. | OPEN — кандидат на отдельное изменение «наблюдаемость неудавшихся выходов». Объём при подъёме: durable-след попытки выхода, не зависящий от того, дошёл ли путь до вставки строки ордера; отдельно — атрибуция действующего лица терминализации. | Граница объёма зафиксирована: openspec/changes/exit-reason-overlay-on-fills-chart/design.md §A5. Подпись слоя наложения намеренно говорит «закрытия позиций», а не «выходы», чтобы панель не создавала впечатление полноты. | | TD-CFG-PCTKIND-1 | После снятия RISK-LIMIT «доля кошелька» вид поля конфигуратора percent-from-bp-optional остался без единого потребителя, и два подтеста конфигуратора сняты без замены. Снятый knob был ЕДИНСТВЕННЫМ полем этого вида, поэтому вместе с ним обнулилось и покрытие: подтест «процент в humanizeFieldValue» (configurator-ux-preview.test.ts) и подтест «типичное значение процента» (configurator-ux-typical.test.ts) снесены, а не переписаны на живое поле. Переписать их напрямую нельзя: inputUnitSuffix и ELIGIBLE_KINDS в typicalExample.ts перечисляют только percent-from-bp-optional, тогда как живые процентные поля имеют вид percent-from-bp и не несут ни суффикса, ни examples; придумывать «типичный процент» за оператора запрещено правилом файла (значение берётся ТОЛЬКО из examples). Асимметрия двух видов существовала ДО удаления — снятие лимита её лишь обнажило. | OPEN — отложено осознанно, не молча. Влияние: ограничено конфигуратором, торгового поведения не затрагивает; риск — процентное поле может отрендериться без суффикса единиц или без подсказки типичного значения, и ни один тест этого не поймает. | Ремонт — одной правкой: слить percent-from-bp-optional и percent-from-bp в один вид, после чего вернуть оба подтеста на живое поле. Каскад при удалении вида: FieldRenderer.vue (6 мест), typicalExample.ts, strategyFormJson.ts, schema-contract.test.ts (3 ветки case), strategy-form-coverage.test.ts:363 (прямо утверждает наличие строки вида в исходнике). Места снятых подтестов размечены комментариями в обоих тест-файлах. | | TD-STOP-EVID-1 | Поле MarketDataSnapshot.recent_sell_trades потеряло последнего РЕШАЮЩЕГО читателя и осталось зеркалом, которое пишут, но по которому не судят. До lossless-stop-confirm-trade-window стоп-путь directional читал ленту принтов из этого поля снимка; теперь он берёт всё из одного атомарного запроса MarketStateStore::stop_trade_window (D7). Поле продолжает НАПОЛНЯТЬСЯ продьюсером (main.rs, replay/mod.rs) и потребляться реплеем/фикстурным корпусом плюс строкой лога продьюсера, поэтому «мёртвым» в смысле RULE-NEW-01 оно не является, но денежных читателей у него больше нет. Почему не удалено ЗДЕСЬ: удаление переносит границу RECENT_SELL_TRADES_WINDOW_MS, которая ВАЛИДИРУЕТ конфиг-ключ stop_loss.confirm_corroboration_lookback_secs (1..300 с). Это отдельный обзор валидации конфигурации: перенос границы меняет диапазон допустимых значений боевой ручки, а её нарушение обнаруживается только fail-fast'ом на старте. | DEFERRED — осознанно, с назначенным продолжением. Следующее изменение обязано сделать ОБА шага одной работой: снять поле снимка вместе с продьюсером и перенести бюджет-границу confirm_corroboration_lookback_secs на владельца, который остаётся (окно причинного запроса стоп-кадра). Оставить поле и переносить границу по отдельности запрещено — между шагами ключ окажется без валидатора. | Решение зафиксировано: openspec/changes/lossless-stop-confirm-trade-window/design.md D7 («Two different objects are involved here and they have different fates») и задача 8.14. Точки продьюсера и остающиеся потребители перечислены там же. | | TD-STOP-EVID-2 | Трейт pm_common::trade_print::SellPrintSource имеет ровно ОДНУ реализацию и НИ ОДНОГО боевого потребителя во всём workspace. Реализация одна — RtdsActorBuffer; после задачи 7.8 прямой путь стопа ходит в буфер напрямую (RtdsActorBuffer::stop_window), а метод трейта recent_sell_prints зовут только два юнит-теста самого буфера. То есть шов существует, но абстракцией никого не связывает. | DEFERRED — чистка ЧУЖОГО крейта, вне объёма lossless-stop-confirm-trade-window. Требуемое действие следующего изменения: либо снять трейт вместе с реализацией и перевести два юнит-теста на прямой метод буфера, либо назвать будущего потребителя явно. Молча оставлять шов без читателя запрещено политикой техдолга — поэтому строка заведена, а не пропущена. | Найдено попутно в задаче 7.8, зафиксировано в openspec/changes/lossless-stop-confirm-trade-window/tasks.md 8.5. | | TD-TICK-AMOUNT-1 | Округление денежной СУММЫ на проводе расходится с документацией биржи. Документация предписывает для произведения price × size «round it up first to Amount decimals + 4, then down to Amount decimals», а crates/pm-sdk/src/clob/wire.rs:299-328 произведение УСЕКАЕТ. Это сторона СУММЫ, а не цены: правило ценовой сетки (order-price-tick-grid-conformance) её не покрывает и намеренно не втягивает в свой объём. | OPEN — решения оператора нет. Требуется отдельное изменение с замером: отвергает ли биржа суммы, полученные усечением, и на каких разрядностях. | Найдено при разборе order-price-tick-grid-conformance (Impact, координация). Прецедент того же класса — live-инцидент 2026-07-13 (Cape Town): odd-lot размер давал усечённое произведение и бесконечный reject-шторм «breaks minimum tick size rule», хотя сама цена была на сетке. | | TD-TICK-AMOUNT-2 | Вечный кэш шага у внешнего клиента биржи задаёт разрядность усечения суммы и не обновляется после смены шага. polymarket-client-sdk-0.4.4/src/clob/client.rs:754-778 заполняет DashMap<U256, TickSize> ПЕРВОЙ заявкой по токену и не инвалидирует его никогда: вызовов invalidate_internal_caches (client.rs:500-504) и set_tick_size (client.rs:523-525) в crates/ — НОЛЬ (проверено 2026-08-19). Разрядность усечения берётся из tick_size.scale() (wire.rs:311), поэтому после смены шага 0.001 → 0.01 цена квантуется верно, а сумма усекается по УСТАРЕВШЕЙ разрядности. | OPEN — решения оператора нет. Отвергает ли биржа такие суммы — НЕ ПРОВЕРЕНО (clob.polymarket.com из рабочей среды недоступен). Средство исправления в API клиента есть (set_tick_size). | Названо в order-price-tick-grid-conformance (design C5a, R9). Для ЦЕНЫ этот кэш источником не является и им быть не может — это закреплено оракулом crates/pm-engine/tests/tick_size_single_source.rs. | | TD-TICK-RESTING-1 | Судьба УЖЕ ЛЕЖАЩЕЙ заявки при огрублении шага не проверена. Переход 0.001 → 0.01 делает лежащую цель выхода 0.9x5 внесеточной. Документация биржи говорит только об отказе на ПРИЁМЕ заявки и молчит о покоящейся. | OPEN — замер невыполним из рабочей среды (clob.polymarket.com недоступен). Требуется наблюдение боевого события смены шага на рынке с нашим лежащим выходом плюс сверка состояния заявки на бирже. | order-price-tick-grid-conformance (R7) закрывает только ВЫПУСК новых заявок и повторный выпуск сохранённых цен; перестановки уже лежащего выхода при смене шага изменение НЕ вводит. От ответа зависит, нужна ли она вообще. |
Documentation gaps
Телеграм-алярмы стоп-контура — см. TD-STOP-OBS-1 выше (пять классов из
crates/pm-engine/src/observability/stop_alarms.rsне описаны вalerts-and-monitoring.md).Ссылка на удалённый
latencySeverity.tsв активной чужой карточке (оставленоobservability-surface-split, 2026-08-26). Строка 175 файлаopenspec/changes/open-positions-redeem-pending-tabs/design.mdприводитsrc/utils/latencySeverity.ts:3как пример устаревшего комментария эпохи до SPA. Файл удалён вместе с секцией латентности, поэтому пример указывает в никуда.Влияние — низкое, и вот почему. Ссылка приведена как ИЛЛЮСТРАЦИЯ, а не как описание поведения: карточка доказывает ею, что оставшиеся в
src/вхождения/local— это API-префикс и старые комментарии, а не маршрут. Вывод карточки от исчезновения примера не меняется. Живой пример того же класса —src/stores/risk.ts:27, ссылающийся на удалённый крейтpm-dashboard.Почему не исправлено в том же пакете. Правка чужой карточки втягивает её в кросс-модельное ревью замысла: её схема
spec-drivenего не требует, иscripts/design-review-gate.sh checkотказывает с требованием перевести карточку наspec-driven-crossreviewи пройти ОБА круга. Это чужой объём и два чужих круга ревью ради одной ссылки; правка была внесена, а затем откачена коммитомrevert(openspec): не трогать чужую карточку ради ссылки на удалённый файл.Владелец и действие. Карточка
open-positions-redeem-pending-tabs; при её следующем касании заменить пример наsrc/stores/risk.ts:27.
Cross-references
- История закрытых TDs:
docs/history/tech-debt-resolved.md. - SSOT entry-point:
docs/INDEX.md. docs/operations/runbook.md§Pre-flight checklist — current pre-flight (directional-only).ADR-024(directional-first architecture) — пивот, после которого все strategy-specific TDs архивированы.- Backtest, replay и сырая запись потока рынков сняты 2026-08-22. Текущий источник диагностики — фактическая телеметрия боевых сессий, включая трассировку ордеров и решений; см.
docs/target/observability.md.
Долг, оставленный unified-trading-gate-and-entry-readiness (2026-08-25)
Пакет свёл остановку торгового цикла в одну точку и вынес прогрев входа отдельным механизмом. Ниже — то, что осталось НЕ сделанным сознательно. Каждый факт перепроверен по коду после независимого ревью: первая редакция этого раздела содержала три неверных утверждения, и они здесь исправлены, а не смягчены.
feed_healthживёт внутри монитора здоровья биржи. Писатель аналитики остаётся ПОЛЕМ детектора доступа к CLOB (engine/exchange_health.rs) — чужая сущность, которую, в отличие от прогрева, не выносили. Уточнение против первой редакции: рассылка детектора уже держит ПРОИЗВОЛЬНОЕ число подписчиков (listeners: Mutex<Vec<TransitionListener>>), поэтому новый потребитель поля не добавляет — утверждение «пятый потребитель даст пятое поле и пятую ветвь» было неверным. Долг сводится к принадлежности: аналитика не должна быть полем детектора биржи.- Нет отсечки устаревшей рассылки в
adapters/status_page.rs. Структурный близнец того, что чинилось в мониторе здоровья: сбор под замком, рассылка и денежное действие — вне его. Гонки нет ТОЛЬКО потому, что рассыльщик единственный (spawn_status_page_poller); обещание порядка держится на этой единственности, а не на структуре. - Отказы резервации НЕ порождают строку заявки.
persist_rejectedбоевого вызывающего не имеет: три вызова, и все тестовые — один unit (engine/order_router.rs) и два интеграционных (tests/it/reject_taxonomy_metrics.rs,tests/it/order_event_slug_own_column.rs). Уточнение против первой редакции: вызывающих три, а не один, и касается это ВСЕХ вариантовReservationDenied—DrainMode,RiskLimit,ExchangeDegraded,EntryWarmupPending,LifecycleHalt(черезEmergencyGuard),PositionIterationCapacity,MarketClosed,StaleData,AuthorizationMismatch. Каталог кодов вalerts-and-monitoring.mdперечисляет их как коды отказа — верно про домен кодов, но не про наличие строк вorders. Форензика по отказам резервации в базе ПУСТА. Владелец разрыва —order-reject-taxonomy-observability. - Классификатор исходов набора данных покрывает не все рубежи.
entry_pattern_outcome(strategies/directional/strategy.rs) не имеет явных ветвей для части существующих рубежей цепочкиGATES— напримерpost_only_crossingиmatching_engine_restart. Они попадают вEntryDecisionOutcome::GateUnclassified, то есть в данных значение означает «рубеж известен цепочке, но здесь не классифицирован», а не обязательно дефект. По причинам ОСТАНОВКИ полнота обеспечена тестомevery_stop_kind_has_a_funnel_row_and_a_stage; для остальных рубежей проверки полноты нет.
Снято из первой редакции как неверное: пункт «пустой слот прогрева на денежном пути не наблюдаем». Слота больше не существует: AdmissionControl ВСЕГДА держит Arc<EntryReadiness>, а EntryReadiness::new стартует в состоянии «не взводился ни разу», поэтому первый же вопрос отвечает «прогревается», а не «готов». Утверждение описывало снятый механизм.