Post-review money-path correctness fix pass on feat/down-side — the branch implementing the both-sides plan (adds a DOWN trading side to the LIVE UP-only Polymarket 5m BTC producer). Headline is a persist-before-publish dedup invariant. Gates green; NOT pushed, NOT deployed, no live-DB writes.
For Agents
Repo
wowjeeez/pmv2-crypto-algo@/Users/levander/coding/pmv2/crypto_algo, branchfeat/down-side, 3 commits (0d5324fingest warn-logging,1aef0efprobe_window,90808d0observe core) — NOT pushed/deployed. C1 invariant: theemitted_signalsINSERT (ON CONFLICT (window_id) DO NOTHING) runs BEFORE the NATS publish — the written DB row is the dedup authority. Decision is a pure helperfire_decision(Option<u64>) -> {Publish|SkipDuplicate|SkipError}. Never a duplicate real-money trade; worst case is a detectable missed trade.Observationnow carriesup: BookView, down: BookView(not 16 flat fields); per-side selection via purefire_params(side, obs, market).FireSidedeleted →select_side(p_cal) -> Option<Side>(≥0.80 Up, ≤0.20 Down). Regime gate FROZEN: τ∈[60,150)s ∧ |p_cal−0.5|≥0.30. Startup fail-fastprobe_schema_columnrequires migration0006. Tooling: run clippy/fmt viartk proxy(the rtk hook mangles-- -D warnings).
C1 — Persist-before-publish (the money-path invariant)
The most important change. emit.rs runs the emitted_signals INSERT (ON CONFLICT (window_id) DO NOTHING) before publishing to NATS. The written DB row — not an in-memory set — is the dedup authority.
The branch logic is a pure, unit-tested helper on the INSERT’s rows_affected:
fire_decision(Option<u64>) -> FireDecision // { Publish, SkipDuplicate, SkipError }
None (INSERT errored) => SkipError // log + NO publish; retry next tick
Some(0) (window already claimed) => SkipDuplicate // NO publish
Some(1) (claim owned) => Publish
Invariant: never a duplicate real-money trade
A publish failure that happens after a successful claim is logged loudly and never retried — the written row is the detectable record of a MISSED trade. Worst case is therefore a detectable missed trade, never a duplicate spend.
Conservative spend accounting (deliberate)
In-memory
fired_windows/total_firesare advanced right after the successful claim, before the publish attempt — so a hung or failed publish can never re-attempt the same window. Spend is over-counted (never under-counted) by construction.
Supersedes the earlier publish-then-mark ordering
C1 inverts the prior rule from commit
bc38734(crypto-shortterm-emit-guardrails, “mark fired only after publish AND db-insert both succeed, else retry next tick”). The trade-off moved from “never lose a signal” (retry on publish failure, but risks duplicate spend on a deploy/crash) to “never duplicate real money” (claim first; a post-claim publish failure is an accepted, detectable missed trade).
Persist-before-publish decision flow
flowchart TD Cand["Fire candidate<br/>regime ∧ select_side≠None"] --> Ins["INSERT emitted_signals<br/>ON CONFLICT(window_id)<br/>DO NOTHING"] Ins --> Dec{"fire_decision<br/>(rows_affected)"} Dec -->|"None — INSERT err"| Err["SkipError<br/>log, no publish,<br/>retry next tick"] Dec -->|"Some(0) — claimed"| Dup["SkipDuplicate<br/>no publish"] Dec -->|"Some(1) — claim owned"| Claim["advance fired_windows /<br/>total_fires, THEN publish"] Claim --> Pub{"NATS publish"} Pub -->|ok| Done["fired<br/>(real-money trade)"] Pub -->|fail| Miss["log LOUD, never retry<br/>row = detectable<br/>MISSED trade"] style Ins fill:#264653,stroke:#2a9d8f,color:#fff style Miss fill:#3d2020,stroke:#a33,color:#fff style Done fill:#2d2d2d,stroke:#888,color:#fff style Dup fill:#2d2d2d,stroke:#888,color:#fff style Err fill:#2d2d2d,stroke:#888,color:#fff
BookView refactor — kills the “Down-reads-UP-book” bug class
observe::Observationnow carriesup: BookView, down: BookView(8 fields each) instead of 16 flatpm_*/down_*fields.book_cols()returns aBookView— this killed the positional 8-tupleBookColstransposition hazard (a mis-ordered tuple silently swapping columns).- emit selects per-side params via a pure, unit-tested
fire_params(side, obs, market)— this closes the copy-paste “Down reads the UP book” bug class. A test assertsDown ⇒ down book / token_down / down_outcome_index / SOURCE_DOWN / "Down".
The recorder/SQL boundary stays FLAT
recorder::Row::Observationstays flat;BookViewis mapped to flat fields inobserve/src/main.rs. Do NOT pushBookViewacross the recorder/SQL boundary — the struct grouping is an in-engine convenience only.
select_side — FireSide deleted
- The
FireSideenum is deleted. - Replaced by
select_side(p_cal) -> Option<types::Side>, simplified to two threshold arms:>= 0.80→ Up,<= 0.20→ Down, elseNone. - The regime gate is unchanged and frozen: τ ∈ [60,150)s AND |p_cal − 0.5| ≥ 0.30 (so a fire requires p_cal ≥ 0.80 or ≤ 0.20 by construction).
C2 — startup fail-fast schema probe
observe::probe_schema_column(pool, table, col)aborts startup with a clear “apply migration 0006” error when a required column is missing — instead of letting every INSERT fail inside the hot loop.- emit probes
emitted_signals.source; observe probesobservations.pm_down_ask.
Test-integrity / DRY
is_regime()andmax_price()are now shared betweenemit.rsand the lib tests — so a test can no longer pass while the real gate regresses (previously the tests had hand-copied reimplementations).- Deleted stale UP-only tautology tests (
up_only_fires_p_cal_above_half,down_skipped_p_cal_below_half) and the hand-copiedis_candidate_test/compute_max_price.
Down-book error visibility
ingestpoll_book’s three error arms now log atwarn!(weredebug!, which is suppressed at prodRUST_LOG=info) with side + token — so a Down-book outage is observable in production instead of silent.
probe_window diagnostic
- The diagnostic now treats
source=crypto_5m_algo_downrows as expected: they storep_cal = P(up)next to the Down ask, which legitimately differs (it previously looked like a mismatch). - The
p_calcolumn meaning is unchanged — it is always P(up), for both sides.
Tooling gotcha — rtk mangles clippy/fmt flags
Run clippy & fmt via
rtk proxyThe rtk cargo hook mangles
cargo clippy --all-targets -- -D warnings— it passes-Dto rustc as a filename, producing a"multiple input filenames"error. Use:rtk proxy cargo clippy --all-targets -- -D warnings rtk proxy cargo fmt --check
cargo fmt --checkis affected the same way — also run it viartk proxy.
State
- 3 commits on
feat/down-side:0d5324f(ingest warn-logging),1aef0ef(probe_window),90808d0(observe core). - Whole-workspace gates green: build clean, 84 tests passed (3 ignored live-smoke), clippy
-D warningsclean,fmt --checkclean. - NOT pushed, NOT deployed, no live-DB writes.
- This is the post-review fix pass on top of the both-sides feature work.
Related
- crypto-shortterm-crypto-5m-algo-both-sides-2026-06-27 — the both-sides DECISION + PLAN this branch implements (DOWN side enabled + source split
crypto_5m_algo_up/crypto_5m_algo_down, migration0006). - crypto-shortterm-emit-guardrails — the existing money-safety guardrail set this pass extends; C1 supersedes that note’s publish-then-mark ordering.
- crypto-shortterm-handover-2026-06-26 — current live state (producer LIVE, UP-only, dry-validate passed); the Down-side deferred upgrade this branch delivers.
- crypto-shortterm-phase1-emit — the emit binary + Directive/EntryOrder path being modified.
- crypto-shortterm — project index.