Crypto 5-min Up/Down — Oracle-Lag Strategy (Canonical Spec & Multi-Asset Replication)

Canonical, asset-agnostic spec for the Polymarket 5-minute Up/Down oracle-lag strategy: the validated edge, the exact signal/sizing, the producer-only architecture and money-path invariants, and the per-asset replication + rollout protocol. Supersedes scattered phase notes as the single source of truth for what the strategy is and how to clone it to a new asset.

For Agents

Status: UP lane LIVE (real money), DOWN lane dry-validating (no money) as of 2026-06-28. Repo wowjeeez/pmv2-crypto-algo at /Users/levander/coding/pmv2/crypto_algo. Isolated Supabase schema crypto_shortterm (NEVER touch pmv2 public/pm_*). Runs on polymarket-infra VM in Zurich (Frankfurt was geolocked by Polymarket).

Thesis (the edge)

Polymarket’s 5-min Up/Down binary markets settle on a slow on-chain oracle (Chainlink BTC/USD on Polygon). A faster reference feed (Binance spot) lets us predict the next Chainlink print before Polymarket’s book reprices. We trade that oracle lag: when the fast feed implies the outcome with high conviction and there’s still time before resolution, buy the corresponding token at its book ask before the market catches up.

See crypto-shortterm-strategy-design for the full P_fair model and crypto-shortterm-data-sources for the three feeds.

The edge — validated

  • UP: 86.6% backtest win-rate, 87.1% live (method sound — live matches backtest).
  • DOWN: 88.4% win-rate over 1,930 windows (backtest, scratchpad/down_backtest.py).
  • Edge is symmetric (Up and Down both work).

Signal spec (EXACT)

  • Calibrated probability p_cal = P(Up outcome), from a frozen 20-bin calibration.json (include_str! in the binary — do NOT refit for the live asset).
  • Entry regime: τ ∈ [60,150)s (time-to-resolution) AND |p_cal − 0.5| ≥ 0.30 (conviction).
    • Up: p_cal ≥ 0.80 → buy the Up token at the Up-token book ask.
    • Down: p_cal ≤ 0.20 → buy the Down token at the Down-token book ask — NEVER the synthetic complement, never 1 − up_bid.
  • One fire per window (single-fire guardrail; emitted_signals PK = window_id).

Sizing & PnL

  • Fixed shares (currently 10) via EMIT_SIZE_MULTIPLIER (global, default 1.0 = 5 shares; deploy set =2 → 10 shares ~200 Up). Sizing mechanics: crypto-shortterm-phase1-bet-sizing.
  • Binary token redeems 0 on loss. So: profit = shares×(1−price), loss = −shares×price. Small wins, big losses → the whole model depends on the high win-rate.
  • Breakeven win-rate = the fill price. EV per share = win_rate − price. At 88.4% we’re EV+ for any fill below ~0.884.

Architecture (Pattern B — producer-only, two binaries one image)

  • observe (shortterm-crypto-algo-observe): records Binance trades + on-chain Chainlink BTC/USD (Polygon dRPC) + Polymarket L2 books for both Up and Down tokens → fair value → crypto_shortterm.observations.
  • emit (shortterm-crypto-algo-emit): on a validated regime fires a pmv2-contracts schema_2 EntryOrder to NATS pmv2.order.{source}.entry (JetStream + PubAck), records to crypto_shortterm.emitted_signals. pmv2-contracts pinned at rev 1f05fff (do not bump). GAR image apps/pmv2-crypto-algo:latest.
  • @positionmanager consumes stream PMV2_ORDERS (subjects pmv2.order.<source>.entry) and executes on-chain. See position_manager.
  • source = NATS subject segment, derived 100% from order.source. One source per lane: crypto_5m_algo_up, crypto_5m_algo_down.

Money-path invariants (do NOT regress)

Money-path invariants — do NOT regress

  • C1 persist-before-publish: emit runs the emitted_signals INSERT (ON CONFLICT (window_id) DO NOTHING, check rows_affected) BEFORE publish_entry. DB is the dedup authority → no duplicate real-money trade; worst case a detectable missed trade. Lives in unit-tested fire_decision().
  • C2 fail-fast: emit + observe ABORT startup if the migration columns are absent → the migration MUST be applied BEFORE redeploy, else the binary crash-loops and halts the live lane.
  • BookView: Observation carries up/down BookView; tested fire_params(side,…) guarantees each side reads its own book/token (test fails on cross-wire).

Implementation + rationale: crypto-shortterm-down-side-money-path-fixes-2026-06-28 (C1 supersedes the old publish-then-mark ordering in crypto-shortterm-emit-guardrails).

The fill = the ONLY per-market unknown

The signal/EV math is settled and asset-agnostic. What is NOT known for any NEW token/market is the real order-book fill (ask, spread, depth) — there’s zero book history until you record it. Validate it per market: synthetic estimate was Down ask ≈ 1 − up_bid ≈ 0.82; the dry-validate measures the real book.

Per-market gate

Going live on a new lane requires: signal backtested above the fill price, AND the real book dry-validated as tradeable (fill ≈ synthetic-or-tighter, tight spread, ample depth), AND Andras’s explicit source→live flip. Never autonomous.

Edge gate — fire on edge, not conviction (the fix — shipped, default-off)

Backtested threshold (2026-06-29): EMIT_MAX_ASK = 0.85. Over the full 21-day history the −EV region is ask ≥ 0.90 (buckets: [.80,.85)+5.0¢, [.85,.90)+2.9¢, [.90,.95) −2.7¢, [.95,1.0) −3.7¢/share). 0.85 is +EV in all 6 walk-forward folds with safe margin below the ~0.89 break-even; 0.88 maximizes total dollars; 0.90 is REFUTED (readmits the −EV tail — the one-regime live data had hinted 0.90, the full backtest overturned it). The τ-cap is REFUTED: tightening τ raises win-rate but ask rises faster → EV falls; keep the regime at [60,150)s. Momentum / conviction / rollover-range all dead across the full history. Shipped: EMIT_MAX_ASK env cap in emit.rs (commit a27b852), default-off — activation (set 0.85 + redeploy) is a live-Up-lane change pending @deploy/Andras. Caveat: all these EVs use ask-at-emit = UPPER BOUND; real fills lose to latency (still unmeasured) + the 0.02 buffer + slippage — a small live tranche is the honest final check, and generalizes to every asset lane: cap the −EV ask tail, fire on edge not conviction.

Rollout protocol (new lane / market)

  1. Apply the DB migration FIRST (adds the columns C2 checks).
  2. Add the NATS permit for the new <source>.> subject (@deploy owns NKey permits).
  3. Create the Polymarket dry source row (@positionmanager).
  4. Redeploy emit/observe at the pinned commit, keep env.
  5. Dry-validate a few hours: emit fires DryRehearse (builds+signs the order, does NOT POST — no money).
  6. Assess the real book; if tradeable, propose small-live → Andras arms.

Multi-asset replication template (BTC → ETH/SOL/XRP/…)

The strategy generalizes to any asset that has (a) a Polymarket 5-min Up/Down market, (b) a slow on-chain Chainlink settlement oracle, and (c) a fast Binance reference feed.

Reuse as-is (chain-agnostic)

  • Whole architecture (observe/emit two-binary image; NATS producer → @positionmanager).
  • Money-path invariants C1 / C2 / BookView.
  • Regime structure: τ∈[60,150)s + |p_cal−0.5|≥0.30 + buy the conviction side at its own token-ask.
  • Single-fire-per-window guardrail; fixed-share sizing model.
  • Rollout + dry-validate protocol; all gotchas below.

Re-derive per asset (do NOT copy BTC’s)

  • Binance symbol (e.g. ETHUSDT, SOLUSDT) — the fast feed.
  • The Chainlink {asset}/USD feed address on Polygon — must be the exact oracle Polymarket uses to RESOLVE that market (verify against the market’s resolution source; wrong feed = wrong edge).
  • calibration.json (20-bin) — REFIT per asset. BTC’s calibration encodes BTC’s vol/lag and will be miscalibrated for another asset. Collect that asset’s own observations (observe-only), fit, then freeze.
  • Regime thresholds (τ bounds, conviction cut): start with BTC’s as a prior, but re-backtest per asset; higher-vol assets may need different cuts.
  • New sources {asset}_5m_algo_up / _down; new PM permits, dry rows, daily caps.

Validate per asset before any live money

  1. Backtest the regime on that asset’s observations — target win-rate comfortably above the expected fill price (BTC hit 86–88%).
  2. Dry-validate the real token-book fill a few hours.
  3. Andras’s explicit arm.

Per-chain subagent task shape (what to start now)

  1. Stand up an observe-only recorder for the asset (Binance symbol + verified Chainlink feed) → accrue observations. No emit, no money.
  2. After enough data: fit calibration.json, backtest the regime, confirm the edge.
  3. If it holds: build the lane (sources/permits/dry rows), dry-validate the fill.
  4. Assess → propose small-live → Andras arms.

(Step 1 — observe-only data collection — can begin immediately and in parallel across assets; trading is gated on 2–4.)

Gotchas (already bit us)

  • Polymarket token IDs are u256 (77-digit) — never parse::<u128>. (See crypto-shortterm-polymarket-token-overflow-fix.)
  • Gamma: tick = orderPriceMinTickSize, min size = orderMinSize; conditionId is a real 0x….
  • EMIT_MAX_FIRES (=1000) hydrates from emitted_signals at boot; TRUNCATE alone doesn’t reset it (restart needed) — do NOT truncate, keep the rows. (See crypto-emit-max-fires-cap-clear-2026-06-25.)
  • PM mode = min(global, source), ignores enabled — live needs BOTH global+source=live.
  • Geolock: must run from an allowed region (Zurich works; Frankfurt was blocked).
  • Fixed-share, not fixed-dollar: every win grosses shares×$1; loss = −shares×price.

Current live state (2026-06-28)

  • main 90808d0. UP lane live (10 shares, $200 cap). DOWN lane: VALIDATED 2026-06-29 (HOLD reversed — the 2026-06-28 “fails-as-built” call was a small-sample artifact). A 21-day / 4,049-fire / ~19-regime walk-forward backtest (reconstructed from recorded p_fair + the frozen calibration, 99.5% match to live fires) confirms a durable edge: win-rate ~87% (Up 86.7% / Down 88.5%, stable 0.84–0.93 day-to-day). Live’s 90–92% was a 2–4pt regime tailwind on the win-rate. The ask/EV cap is the value driver and roughly doubles per-bet EV (uncapped Up +5.3¢/share → capped +9.8¢). Down real-money arm still = Andras’s switch.