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 131b0d5→0986c4d). 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 commits131b0d5→0986c4d).- What changed: producer stopped emitting
cohort.signalsschema_v1; now emits schema_version 2 on the v2 subjects. The oldcohort.signalssubject +signals/wire.rs+signals/signal.rswere DELETED (dead after the swap).- Subjects:
pmv2.order.first_mover_core.entry(entries) andpmv2.order.first_mover_core.exit(exits).source_id = "first_mover_core".- Contract source of truth: the shared
pmv2-contractscrate, vendored as a path dep atcrates/pmv2-contracts(canonical =github.com/wowjeeez/pmv2-contracts). serde + rust_decimal only.- Cohort is the
derivedpricing arm: entries carryPricing::Derived{reference_price, conviction, source_score, size_usd}. ThePricing::Limitarm 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_lanenow builds theOutboundvariant (→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
| Direction | Subject | Payload type | schema_version |
|---|---|---|---|
| Entry | pmv2.order.first_mover_core.entry | EntryOrder (Pricing::Derived) | 2 |
| Exit | pmv2.order.first_mover_core.exit | ExitSignal (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
derivedThe v2
EntryOrdercarries aPricingdiscriminator:
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_refand full-exits it. Theorigin_refis 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 fromSignalConfig(loaded from thepmv2_autotrade_configtable).- 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 (
0a31f45→e444af0) — all four gaps are now fixedAll four gaps below were CLOSED in a follow-on branch (
feat/pre-live-gating, ff-merged tomain@e444af0):category_filter+ chase + market-liveness implemented FAIL-CLOSED, the degenerateev_floor_gateDROPPED. A read-onlyClobReadClientwas re-added topolymarket-datafor 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 thefirst_mover_coresource 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
GammaClientand will NOT catch them — if cohort doesn’t gate on these, nothing does.
category_filtergate — NOT implemented. Needs a per-event category fetch + a port ofCategoryFilter/passes_filter. Without it, the lane can’t restrict to allowed categories.- chase /
max_entry_premium_bpsgate — NOT implemented. Needs a live best-ask fetch to bound how far above reference the entry will chase.- 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.
ev_floor_gateis the OLD DEGENERATE logic — must be REPLACED. The ported gate is not a true EV calculation:size_usdcancels out, so it reduces tomin_round_trip_edge_bps > 200compared 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 (at0986c4d). 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:Buyoncohort.signals). The subject + envelope it describes are now REPLACED; its lessons (JetStream-PubAck publish, “dropped publish = permanent miss”, theorigin_reffail-closed parse) still hold and carried into the v2 path.- cohort-signals-service-split-and-exit-signals defined the cohort EXIT
action:Sellpublish + the exit-discriminator dedup. The v2ExitSignalis the lean successor;origin_refis 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).
Related
- cohort-pre-live-signal-gating — the follow-on that CLOSES the 4 PRE-LIVE gating gaps this note flagged (category_filter / chase / liveness implemented FAIL-CLOSED; degenerate
ev_floor_gatedropped; read-onlyClobReadClientre-added for best-ask + market-status) - cohort-algo-component — the standalone component this cutover lands in; its “v2 cutover — planned, BLOCKED” section is the thing this note resolves
- cohort-firstmover-nats-migration — the schema_v1 ENTRY lane the v2
EntryOrderreplaces (PubAck-publish + dropped-publish lessons carried forward) - cohort-signals-service-split-and-exit-signals — the schema_v1 EXIT
action:Selllane the lean v2ExitSignalreplaces (theorigin_refcapital-trap lesson) - pmv2-position-manager-nats-consumer — the executor that CONSUMES these subjects directly; the
ON CONFLICT(dedup_key)idempotency - position-manager-interface-design — the EXECUTOR; the schema/ownership split (PM owns mode/breaker/caps + the ledger; producer owns sizing + signal gates)
- telegram-connector-outbound-design — the
telegram.outboundplane the flippedNotifier::Outboundnow publishes to - cohort-algo-shared-db-migrator-ownership — the companion #331 decision (PM owns the
pmv2_autotrade_*migrator; cohort drops that DDL) — relevant because cohort READSpmv2_autotrade_configfor both sizing and gating knobs - pmv2-fleet-infra-observability-nats-conventions — fleet NATS/OTel conventions these subjects follow (
Nats-Msg-Iddedup, own-durable consumers) - pmv2-autotrade-engine-design — the locked engine design these signals feed (now wholly executor-side)