Skip to content

150: Restore paired-merge strategy as layer ​

Identity ​

  • Status: archived — code removed from the workspace build, restoration undesigned
  • Spawned from: paired-merge-strategy-archival (operator decision 2026-09-07)
  • Original module: crates/pm-engine/src/strategies/paired_merge/ (6751 LOC across six files) + crates/pm-engine/src/ports/paired_admission.rs (118 LOC) + crates/pm-engine/src/engine/paired_rediscovery.rs and its _tests.rs (1402 LOC combined) — 8271 LOC total at archive
  • Recovery: git show 210f52d7:<path> for each of the following ten files (git anchor 210f52d7 is the commit this archival branched from, taken before the archival's own commits exist)
    • git show 210f52d7:crates/pm-engine/src/strategies/paired_merge/mod.rs
    • git show 210f52d7:crates/pm-engine/src/strategies/paired_merge/config.rs
    • git show 210f52d7:crates/pm-engine/src/strategies/paired_merge/admission.rs
    • git show 210f52d7:crates/pm-engine/src/strategies/paired_merge/runtime_config.rs
    • git show 210f52d7:crates/pm-engine/src/strategies/paired_merge/runtime_state.rs
    • git show 210f52d7:crates/pm-engine/src/strategies/paired_merge/strategy.rs
    • git show 210f52d7:crates/pm-engine/src/ports/paired_admission.rs
    • git show 210f52d7:crates/pm-engine/src/engine/paired_rediscovery.rs
    • git show 210f52d7:crates/pm-engine/src/engine/paired_rediscovery_tests.rs
    • git show 210f52d7:docs/strategies/spread-mm-yield-calculator.md (the EV math behind the strategy — not a code file, removed by the same archival because a live SoT directory is the wrong home for a document about an archived strategy)
  • Target taxonomy: paired YES+NO market-making, strategy_type='paired-merge' (operator-facing name: spread-mm)
  • Original design: no ADR tracks this algorithm. It was designed and shipped through the openspec change spread-mm-strategy, frozen at docs/history/openspec/changes/2026-08-31-spread-mm-strategy/. That card is the first source for every design decision behind the strategy (mechanism, admission gate, rediscovery loop, EV model) — this plan does not repeat them
  • Type: restoration (recovery — real code existed, was wired end to end, and ran in production)

Context ​

Paired-merge quoted both legs (YES and NO) of a binary market passively, held the matched set to resolution, and took the spread as edge: YES_bid + NO_bid = 1 − spread. It was not a design sketch — it was fully wired through the config loader, pm-cli, the dashboard API, and the engine's composition root, with its own admission gate (ports::paired_admission) and an isolated periodic rediscovery loop (engine::paired_rediscovery) separate from the main strategy dispatch path.

The strategy was archived by a standalone operator decision on 2026-09-07 — not part of the ADR-024/F4 wave that archived bucket-arb, maker, maker-rebate, merge-arb, and single-token-mm. Two reasons drove it, both independent of the mechanism's own merit:

  • ADR-024 already restricts pm-engine to one active strategy (directional/Harvester). Paired-merge's own earlier openspec cycle (spread-mm-strategy) had re-widened the engine's type gate to accept a second strategy_type past that restriction — a governance gap that this archival closes, not a defect in the trading logic itself.
  • Its presence forced unrelated engine work to keep accounting for paired-merge-specific branches: three concurrent cards (stop-exit-residual-elimination, orderbook-freshness-observability, confirmed-coverage-forbids-unsellable-verdict) each ran into it to some degree before this archival.

The only production run of the strategy — instance spread-mm-v1 on the VPS, 22–23.07.2026 — placed 49 BUY orders and had every one of them rejected by the exchange (status='REJECTED'), leaving zero rows in positions and zero fills. The container has not existed since 23.07. (Live query against the VPS, 2026-09-07: sqlite3 …/spread-mm/data/engine.db "SELECT side, status, COUNT(*) FROM orders GROUP BY side, status" → BUY|REJECTED|49; SELECT COUNT(*) FROM positions → 0.) The archival does not clean up that instance or its config.db row — that cleanup is a separate operator decision.

Restoration scope ​

  • A fresh implementation of the paired-quoting mechanism as an extension-layer strategy per ADR-025, not a resurrection of the removed module as a parallel in-tree branch — the removed code predates the extension points API and was wired directly into the engine's composition root and its own isolated rediscovery loop, both of which are exactly the kind of coupling ADR-025 exists to remove.
  • The admission gate (evaluate_paired_admission / filter_admitted, ~490 lines, 12 tests at archive) is the most reusable piece: it was already pure functions behind a narrow port (ports::paired_admission::PairedAdmission), not entangled with engine internals.
  • The paired YES/NO matcher (resolve_pair, lived on ports::market_data at archive) and the strategy config shape (PairedMergeConfig, typed JSON under strategies.definition per ADR-015 — same discipline as directional.*) restore largely as-is; the mechanism itself did not change between design and archival.
  • The isolated engine::paired_rediscovery periodic loop needs re-justifying, not reusing verbatim — see Out of scope.

Prerequisites ​

  • ADR-025 ACCEPTED — extension points API stabilized on the one live strategy (directional/Harvester), same general contract as every other item in this backlog.
  • pm-engine SDK (openspec unify-orders-positions-signals-events) ready as a hosting platform, same general contract as every other item in this backlog.
  • The zero-fill production run (49/49 BUY rejected) must be root-caused before any restoration work starts. Restoring the same admission and quoting logic without knowing why every order the strategy placed was rejected would repeat a strategy that never produced a single fill.
  • The EV model backing the strategy (docs/strategies/spread-mm-yield-calculator.md at archive) is no longer a live document — it is recoverable at the git anchor above, but restoration must re-derive and re-validate it against forward data before any live use, per the project's standing EV-validation rule (no ride-to-resolution counterfactuals, no survivorship, resolved outcomes only).
  • The governance gap that let an earlier openspec cycle widen the engine's supported-type gate past ADR-024 must not repeat: any restoration proposal has to get ADR-025 acceptance (or an explicit ADR-024 amendment) before touching SUPPORTED_STRATEGY_KINDS or STRATEGY_TYPES again.

Out of scope ​

  • Restoring the strategy in its archived form without first explaining the 49/49 rejected BUY orders — that is a defect investigation, not a restoration task.
  • Restoring it as a second strategy dispatched in parallel to directional outside the ADR-025 extension API — that is the exact coupling this archival removed.
  • Reusing the isolated engine::paired_rediscovery periodic-loop design unexamined. It existed as a separate cycle from the main strategy dispatch specifically because paired-merge needed to re-evaluate admission independently of per-tick strategy evaluation; whether that isolation is still the right shape once an extension API exists is a restoration-time design question, not a given.

Acceptance criteria ​

  • Strategy trait implementation (or its ADR-025 successor) green under the same contract-test pattern used for directional.
  • Admission-gate contract test ported and green (the 12 existing unit tests at archive are a starting inventory, not a target to hit exactly).
  • EV model re-validated on post-restoration forward data, per the project's Harvester EV reference model, before any claim of profitability.
  • A named, evidence-backed answer for why the only production run rejected 100% of its BUY orders, checked against the restored admission/sizing logic.
  • VERIFIED markers on restored behavior per the project's silent-feature-broken guard.
  • ADR-025 ACCEPTED (or an ADR-024 amendment) recorded before the type gate accepts paired-merge again.

Dependencies ​

  • Hard blocker, shared with every item in this backlog: ADR-025 extension points API ACCEPTED.
  • Hard blocker, shared with every item in this backlog: pm-engine SDK hosting platform ready.
  • Sequencing against merge-arb (_plans/147-restore-merge-arb-as-layer.md, priority #1 in this backlog) is an operator decision, not a technical dependency — the two strategies do not share removed infrastructure beyond the general ADR-025/SDK prerequisites above.