cohort_algo now emits the v2 pmv2 trade wire-contract (schema_version 2) on pmv2.order.first_mover_core.{entry,exit} instead of its old cohort.signals schema_v1 envelope. This is the cutover that v2 cutover — planned, BLOCKED was waiting on — the pmv2-contracts crate now exists, so it was vendored and the producer migrated. Shipped to github.com/wowjeeez/pmv2-cohort-algo main @ 0986c4d (6 commits 131b0d50986c4d). Gate green, mode still OFF.

For Agents — state at a glance

  • Repo: private github.com/wowjeeez/pmv2-cohort-algo, main @ 0986c4d (the cutover is 6 commits 131b0d50986c4d).
  • What changed: producer stopped emitting cohort.signals schema_v1; now emits schema_version 2 on the v2 subjects. The old cohort.signals subject + signals/wire.rs + signals/signal.rs were DELETED (dead after the swap).
  • Subjects: pmv2.order.first_mover_core.entry (entries) and pmv2.order.first_mover_core.exit (exits). source_id = "first_mover_core".
  • Contract source of truth: the shared pmv2-contracts crate, vendored as a path dep at crates/pmv2-contracts (canonical = github.com/wowjeeez/pmv2-contracts). serde + rust_decimal only.
  • Cohort is the derived pricing arm: entries carry Pricing::Derived{reference_price, conviction, source_score, size_usd}. The Pricing::Limit arm is for weather/crypto, NOT cohort.
  • Producer now OWNS sizing + signal-level gating (ported from the retired executor): signals/sizing.rs + signals/gates.rs.
  • Telegram routing flipped: Notifier::for_lane now builds the Outbound variant (→ telegram.outbound, consumed by telegram_connector) when NATS is configured; Direct-send is fallback only when NATS is absent.
  • Gate: cargo build clean, 307 tests pass workspace-wide, clippy clean, fmt clean. Dormant-safe (no POLYGON_WSS_URL / NATS_URL ⇒ watch + publish early-return, no hang).
  • NOT live. Mode is OFF. There are 4 PRE-LIVE gating gaps (below) that MUST be closed before arming — PM cannot catch them (it has no GammaClient).

What shipped — the cutover in pieces

This supersedes the schema_v1 emit path documented in cohort-algo-component and cohort-firstmover-nats-migration. The behavior of detecting first-mover entries / cohort exits is unchanged; only the wire envelope, subjects, sizing ownership, and gating ownership changed.

1. Vendored the pmv2-contracts crate

The shared contract types are now a workspace member at crates/pmv2-contracts (path dep). Canonical upstream = github.com/wowjeeez/pmv2-contracts. The crate is deliberately minimal — serde + rust_decimal only, no async/DB/CLOB deps — so every fleet component can vendor it cheaply. This resolves the long-standing blocker: you can’t vendor a contract that doesn’t exist, and now it does.

2. New subjects + schema_version 2

DirectionSubjectPayload typeschema_version
Entrypmv2.order.first_mover_core.entryEntryOrder (Pricing::Derived)2
Exitpmv2.order.first_mover_core.exitExitSignal (lean)2

source_id = "first_mover_core" — the EXACT DB key, unchanged from schema_v1. (Getting source wrong silently routes to Mode::Off at the executor — see The v2 trade wire-contract babylon 290 292.)

One entry envelope, two pricing arms — cohort is derived

The v2 EntryOrder carries a Pricing discriminator:

  • Pricing::Derived{reference_price: Decimal, conviction: f64, source_score: Option<f64>, size_usd: Decimal} ← cohort uses THIS. PM derives the limit price from the live book, does $→shares, applies lot-size.
  • Pricing::Limit{limit_price, size_shares} ← for the weather/crypto producers (they compute their own limit). NOT cohort.

The exit payload is the lean ExitSignal{schema_version, kind, id, source, produced_at, valid_until_ts, market{condition_id, outcome_index, …}, origin_ref}. PM resolves OUR position row from (condition_id, outcome_index) + origin_ref and full-exits it. The origin_ref is load-bearing — an empty one = the position can’t be auto-closed = capital trap.

3. Deleted the dead schema_v1 surface

The old cohort.signals subject, signals/wire.rs (the SCHEMA_VERSION_MAX=1 serializer), and signals/signal.rs (the old TradeSignal / cohort_signal_id / dedup_key types) were removed — they were dead after the contract swap. This is a clean replacement, not a dual-emit.

4. Producer-owned sizing (signals/sizing.rs)

size_usd is now computed in the producer and put on the wire (cohort is the derived arm, so the producer owns size — PM only derives the limit and does $→shares). The formula was PORTED from the retired executor (the same validated sizing the old engine used):

score_weight = clamp(score, 0, 1) * 10 + 1
raw          = base_usd * score_weight * conviction
size_usd     = clamp(raw, min_size_usd, max_size_usd)

Portfolio caps were deliberately EXCLUDED from the producer

The port brought over the per-signal sizing math ONLY. Portfolio caps (total exposure, daily spend, max open positions, max copies/event) stay with PM — it owns the ledger and is the sole counter of exposure. The producer sizes a single signal; the executor decides whether that size fits the portfolio. This matches the schema split (PM is the sole counter of exposure/open positions).

5. Producer-owned signal-level gating (signals/gates.rs)

Per babylon decision #292, gating is split by owner:

Gating split (babylon #292)

  • PRODUCER (cohort) owns SIGNAL-LEVEL gates: score_threshold, min_conviction, entry_price_band, time_to_resolution, ev_floor. Knobs read from SignalConfig (loaded from the pmv2_autotrade_config table).
  • PM owns mode / breaker / portfolio-caps.

The rationale: PM has no GammaClient — it cannot re-run anything that needs market metadata / category / book context. So whatever gating depends on that data MUST happen in the producer before publish.

6. valid_until_ts — first-mover edge decays fast

Entries set valid_until_ts = Some(now + 300s) (5 minutes) — the first-mover edge is perishable, so a stale entry should expire rather than execute late. Exits set valid_until_ts = None (an exit should always be actionable; you never want to “expire” a close).

7. Telegram routing flipped to the NATS outbound plane

Notifier::for_lane now constructs the Outbound variant — publishing to the telegram.outbound NATS subject (consumed by telegram_connector, the fleet’s sole bot-token holder) — when NATS is configured. Direct-send Telegram is now the fallback only (when NATS is absent). This completes the telegram.outbound seam that was prepared-but-defaulted-to-Direct in Current state vs the v2 cutover.

PRE-LIVE gating gaps — MUST close before arming

CLOSED 2026-06-23 ( 0a31f45e444af0) — all four gaps are now fixed

All four gaps below were CLOSED in a follow-on branch (feat/pre-live-gating, ff-merged to main @ e444af0): category_filter + chase + market-liveness implemented FAIL-CLOSED, the degenerate ev_floor_gate DROPPED. A read-only ClobReadClient was re-added to polymarket-data for the best-ask + market-status reads. Full detail: cohort-pre-live-signal-gating. (cohort is still mode OFF; live-arming now waits only on PM’s migrator inserting the first_mover_core source row + operator go-ahead.)

Four gating gaps — NOT blocking the cutover (mode is OFF), but REQUIRED before live

These are flagged, not fixed. They do not block the cutover because the lane is OFF and produces nothing live. But they MUST be closed before arming, because PM has no GammaClient and will NOT catch them — if cohort doesn’t gate on these, nothing does.

  1. category_filter gate — NOT implemented. Needs a per-event category fetch + a port of CategoryFilter / passes_filter. Without it, the lane can’t restrict to allowed categories.
  2. chase / max_entry_premium_bps gate — NOT implemented. Needs a live best-ask fetch to bound how far above reference the entry will chase.
  3. market open / active / accepting-orders status flags — NOT implemented. Needs a market-status fetch; without it the producer can emit entries for markets that aren’t accepting orders.
  4. ev_floor_gate is the OLD DEGENERATE logic — must be REPLACED. The ported gate is not a true EV calculation: size_usd cancels out, so it reduces to min_round_trip_edge_bps > 200 compared against a fixed 2% cost. It is a placeholder shaped like an EV floor, not a real one. A genuine EV floor must replace it before live.

Gaps 1–3 need data PM can’t provide; gap 4 needs real EV math. All four are producer-side because the executor cannot re-derive them.

How this relates to the prior notes

For Agents — supersession map

  • cohort-algo-component documents cohort_algo’s standing architecture and (at 2e150b1) the v2 cutover as “planned, BLOCKED.” THIS note is that cutover, now COMPLETE (at 0986c4d). The component note’s “Current state vs the v2 cutover” section is now historical for the schema_v1 emit + Direct-default-notifier description.
  • cohort-firstmover-nats-migration defined the schema_v1 first-mover ENTRY lane (action:Buy on cohort.signals). The subject + envelope it describes are now REPLACED; its lessons (JetStream-PubAck publish, “dropped publish = permanent miss”, the origin_ref fail-closed parse) still hold and carried into the v2 path.
  • cohort-signals-service-split-and-exit-signals defined the cohort EXIT action:Sell publish + the exit-discriminator dedup. The v2 ExitSignal is the lean successor; origin_ref is the field PM maps the Sell back to OUR row.
  • pmv2-position-manager-nats-consumer is the consumer side; the executor consumes these subjects directly (no runner).