State as of 2026-06-22

The wx-signal engine is deployed, live, and publishing real signals to NATS (unchanged from 2026-06-18). The big change since then: the pmv2 monolith is being split into independent service repos with NATS as the spine and a shared pmv2-contracts crate as the seam, and weather has signed off on a v2 trade wire-contract (the limit arm). Producer code to emit v2 is scoped but NOT yet built. The dry-rehearsal (capital gate) is still pending — it was blocked by a “too close to resolution” gate, now fixed on the consumer side (i64::MIN exemption, PM @ 43023ba), needs a redeploy + a fresh US lock to complete. Supersedes the stale “code not started” status in weather-bet.

TL;DR — where everything is

piecestate
StrategyValidated GO on US fillable cohort (UNCHANGED) — 94% win / +29.9% mean / +17.4% 80%-LCB on the ~44% fillable (≤0.95) cohort. weather-strategy-validated-plan + honest re-backtest below
RepoRENAMED + MOVED (2026-06-22)github.com/wowjeeez/pmv2-weather-us-algo (PRIVATE), local /Users/levander/coding/pmv2/us_weather_algo/. Was wowjeeez/weather-bet @ /Users/levander/coding/weather_bet
Engine (wx-signal)Live in --loop on polymarket-infra (GCP), pin a2fa5ee, branch feat/wx-signal-engine. Validated lock logic unchanged
PublishingConfirmed live — real acked signals (Atlanta, Chicago) on the old weather.signals stream. v2 not yet wired
Architecturepmv2 monolith RETIRING → split into service repos (producers → executor → notify), NATS spine, shared pmv2-contracts. See pmv2-rearchitecture
Wire contractv2 signed off (weather limit arm) — supersedes old docs/signal-schema.md v1 envelope. Producer code to emit it = PENDING
Executor (Position Manager)pmv2-position-manager (@deploy / @positionmanager) — consumes NATS directly, gates, signs EIP-712, POSTs CLOB. Mode = OFF (not live-armed)
Capital gate (dry-test)PENDING — was blocked by the “too close to resolution” liveness_gate; FIXED consumer-side (i64::MIN exemption, PM @ 43023ba). Needs redeploy + a FRESH US afternoon lock to fire the 🧪 DRY-RUN alert, then cross-check

The strategy (obs-driven late-settlement) — UNCHANGED, still GO

Polymarket “highest temperature in {city} on {date}” markets resolve on the day’s actual high at a named ICAO airport (whole °F US), winning bucket → $1. The market is slow to reprice a high that’s already physically set: the daily high pins mid-afternoon, but the market takes time to mark the winning bucket up. The engine detects the high is in, from live METAR, before the market catches up.

Lock rule (the core, validated, unchanged): poll METAR per-minute → running whole-°F max → lock = first obs with station-local hour ≥ 15:00 (LOCK_HOUR) where the running max hasn’t risen for 90 min (NO_RISE_SECS=5400) → buy that bucket’s YES only if fillable ≤ 0.95 (else skip and retry) → hold to $1. Mirrors research/strategy/engine.py byte-for-byte (80 golden fixtures).

The strategy did not change

The pmv2 re-architecture changes how the signal is delivered and executed, NOT what the signal is. The lock logic, the ≤0.95 fillability gate, the US-only GO verdict, and the validated edge are all unchanged. The load-bearing unknown remains fills (what the dry rehearsal measures).

Honest re-backtest verdict (2026-06-16, unchanged)

A clean causal backtest (research/strategy/clean_backtest.py, report research/reports/strategy-revalidation-honest-2026-06-16.md) corrected the earlier inflated +10.7%:

  • US cohort (NYC/Chicago/Miami/Atlanta), 151 locked days: reliability 96.7% all-locked. But ~53% of locked days are already > 0.95 at lock (locked out of the near-certain winners). On the ~44% fillable (entry ≤ 0.95) cohort: 94% win, +29.9% mean return, +17.4% 80%-LCB — a real, positive edge on a smaller day-count.
  • Intl °C cohort (London/Seoul/Paris): NO-GO for the lock-and-hold entry — 60.8% tradeable reliability, return 80% CI (−13.3%, +31.1%). 1°C buckets too tight. (Asian markets are now a separate lane — weather_as / pmv2-weather-as-algo — not part of this repo’s GO.)
  • The load-bearing unknown is fills — the backtest prices at prices-history mid + a small haircut; real fillability at ≤0.95 is what the dry-rehearsal measures.

Repo rename + move (2026-06-22)

Every old reference is stale

  • GitHub: github.com/wowjeeez/weather-betgithub.com/wowjeeez/pmv2-weather-us-algo (PRIVATE).
  • Local path: /Users/levander/coding/weather_bet/Users/levander/coding/pmv2/us_weather_algo/.

This repo is now one of a sibling set of pmv2 service repos under /Users/levander/coding/pmv2/: us_weather_algo (this), as_weather_algo, cohort_algo, crypto_algo, position_manager, telegram_connector. On GitHub: pmv2-weather-us-algo, pmv2-weather-as-algo, pmv2-cohort-algo, pmv2-crypto-algo, pmv2-position-manager, pmv2-telegram-connector, and the shared pmv2-contracts.

The engine (crates/wx-signal) — unchanged

Rust workspace crate. Polls aviationweather.gov METAR (live) / IEM ASOS (replay), computes the lock, maps to the Gamma bucket/token, prices it (CLOB /price/midpoint/book ladder, dust floor MIN_VALID_PRICE=0.02), and dual-writes: Supabase weather_signals (atomic INSERT + pg_notify, ON CONFLICT(market_slug) dedup) and a NATS JetStream publisher.

  • Engine files (lock.rs, price.rs, nats.rs, sink.rs, signal.rs, main.rs) and the validated lock logic are unchanged by the re-arch.
  • --smoke flag: on-demand connect → publish one synthetic signal → ack, no Supabase/lock needed (the deploy-host e2e proof).
  • Skip-on-untradeable: when no confirmed ask ≤ 0.95, it emits nothing (logs untradeable: ... skipping emit) — never defaults to a 0.95 cap. Verified live (Chicago skipped once its bucket drifted > 0.95).
  • Telemetry: OTLP → SigNoz, service.name=wx-signal. Useful log bodies: lock_detected, no_lock, nats publish (with acked bool), untradeable: ... skipping emit, outside US afternoon window; sleeping.

Architecture (the pmv2 split) — NEW

The monolith (polymarket_fetch) is retiring, split into independent service repos with NATS JetStream as the spine and a shared pmv2-contracts crate as the seam. Dependencies are unidirectional: producers → executor → notify. Full design in pmv2-rearchitecture.

producers (weather=THIS / weather_as / cohort=first_mover_core / crypto=crypto_shortterm_latency_test)
   → PMV2_ORDERS / pmv2.order.<source>.<entry|exit>
   → Position Manager: ONE durable pull consumer on pmv2.order.>  (explicit ack, Nats-Msg-Id dedup)
   → gate (off/dry/live + caps + breaker) → sign EIP-712 → POST clob.polymarket.com → record/settle
  • Producers emit READY orders and own ALL gating — PM has no GammaClient. Weather does its lock + ≤0.95 fillability and emits only orders it has decided to take.
  • Position Manager = the executor (@deploy / @positionmanager): polymarket_fetch’s consume + exec moves here. Consumes NATS directly (no separate runner), gates, signs EIP-712, POSTs clob.polymarket.com, records/settles. Mode currently OFF (not live-armed).
  • Notification service: dedicated, centralizes the ~25–30 scattered Telegram send-sites behind a NATS notify subject. Owner TBD.
  • pmv2-contracts (PUBLISHED, github.com/wowjeeez/pmv2-contracts): serde + rust_decimal only; holds the envelope types, the Pricing enum (limit | derived), Side, SCHEMA_VERSION = 2, and subject builders. source is a FREE String, not an enum — adding a lane = publish the source string + insert a pmv2_autotrade_sources DB row. Producers + PM vendor it so the wire can’t drift.

Supersedes the old single-stream pipeline

The previous design (single stream WEATHER_SIGNALS / subject weather.signals, a hand-tracked docs/signal-schema.md envelope, cohort riding the same stream on cohort.signals) is being replaced by the one-PMV2_ORDERS-stream, source-routed-subjects, shared-contracts model above. The old envelope is documented in this repo’s docs/signal-schema.md (still v1) and is now historical.

The wire contract (v2 — weather’s limit arm) — NEW, signed off

Signed off by weather (2026-06-22) — supersedes the v1 envelope

Weather emits a READY BUY entry-order to pmv2.order.weather.entry. Full field table + PM placement semantics in [[pmv2-rearchitecture#trade-wire-contract-v2—weathers-limit-arm|Trade wire-contract v2 — weather’s limit arm]]. Summary:

  • common: id = uuid5("{source}|{market_slug}|{action}") (dedup key = Nats-Msg-Id), source="weather" (EXACT pmv2_autotrade_sources key — a wrong value silently routes to Mode::Off), schema_version=2, produced_at, valid_until_ts (past → Term), market{condition_id, outcome_index, token_id, title, event_slug, slug, + neg_risk/tick/min as HINTS}, origin_ref (= our Supabase weather_signals row id), side="BUY".
  • pricing = “limit”: limit_price (≤ 0.95, hard cap), size_shares (weather-sized).
  • PM placement: FAK marketable-limit at floor_to_tick(min(limit_price, best_ask)) — fills ≤ our cap; a near-certain bucket whose ask > 0.95 simply won’t fill (correct, we never overpay). PM re-resolves neg_risk/tick/min authoritatively; sized-out (shares < min) → Skip.
  • ENTRY-ONLY: weather holds to settlement → no exit-signal (exit = resolution → $1). The ...exit subject is cohort/crypto only.
  • Weather context (city, bucket_label, observed_high_f, lock_ts, reason) stays OFF the execution wire — it lives in the Supabase weather_signals row, reachable via origin_ref (PM ignores unknown fields).

Sizing moved to producers (NEW)

Weather now sizes its own orders (sizing moved out of the executor):

size_shares = clamp(floor(notional / limit_price), min_size = 5, cap)

Default notional = $50 (configurable); PM’s global caps + breaker remain the hard backstop.

wx-signal producer adaptation — PENDING (not built yet)

The v2 producer code change is scoped but NOT done

It was blocked on pmv2-contracts, which now exists (published), so it is unblocked. The change:

  • rename actionside, max_pricelimit_price
  • fold the flat market fields into market{}
  • add pricing = "limit" + size_shares
  • bump schema_version 1 → 2
  • switch subject weather.signalspmv2.order.weather.entry
  • add a sizing module (notional → shares, default $50)
  • vendor pmv2-contracts

The existing engine (crates/wx-signal: lock.rs, price.rs, nats.rs, sink.rs) and the validated lock logic are unchanged by this. Until this ships, docs/signal-schema.md in the repo is still the v1 envelope.

The dry-test / “too close to resolution” gate saga — NEW

The dry rehearsal (the capital gate) was blocked by the PM consumer’s liveness_gate (“too close to resolution”). This is a structural collision with the strategy, now fixed consumer-side.

Root cause

The lock strategy signals NEAR resolution by design — it locks mid-afternoon and the market resolves end-of-local-day, so every signal lands ~6–9h out and trips a gate meant to reject signals too close to settlement. The gate skips when secs_to_resolution < min. The first Directive exemption set min = 0, which only exempts POSITIVE secs. But the re-fired STALE Jun-19 NYC market is now PAST resolution (negative secs), so it kept skipping. A FRESH signal (positive secs) already passes with min = 0.

Fix — i64::MIN full exemption for Directive sources

Landed in Position Manager main @ 43023ba (consumer side, NOT this repo). Using i64::MIN as the threshold for Directive sources fully exempts them — including stale replays with negative secs.

Two earlier theories were WRONG — recorded as ruled out

  • (a) weather’s “ask > max_price” — ruled out: PM caps at min(limit_price, best_ask) and still builds the order, so this would have alerted, not silently skipped.
  • (b) “best_ask = None / no liquidity” — ruled out. The real cause was the liveness gate’s negative-secs handling above.

Live evidence (from SigNoz, 2026-06-17) — historical

  • nats publish acked=true for highest-temperature-in-atlanta-on-june-17 (16:56 EDT) and highest-temperature-in-chicago-on-june-17 (17:27 CDT) — first real signals, on the old weather.signals stream.
  • Pre-DNS-fix (6-17 AM) one publish timed out 3× (NATS publish ack failed: timed out); clean since @deploy switched NATS_URL to host.docker.internal.

Two weather lanes

  • US (source = weather, THIS repo pmv2-weather-us-algo) — NYC/Chicago/Miami/Atlanta, the validated-GO cohort.
  • AS / Asian (source = weather_as, a SEPARATE repo/session pmv2-weather-as-algo, ~$10–30 notional) — see pmv2-reorg-and-weather-as-2026-06-22.

They are deliberately separate repos/lanes (not a shared engine). They share the contract (pmv2-contracts) and the executor (PM), nothing else.

Team / ownership

  • @deploy / @positionmanager owns Position Manager (the executor) + provisioned NATS (on polymarket-infra, loopback/tailnet, NKey, GH-secret seeds) + runs the wx-signal daemon. The i64::MIN gate fix is on PM’s side (43023ba).
  • weather (this project) owns pmv2-weather-us-algo: the wx-signal producer (pure signal, no CLOB secrets), the v2 contract sign-off, and the strategy/backtest.
  • weather-AS owns pmv2-weather-as-algo (separate session).
  • cohort / crypto own their producer repos.

Open items / next steps

Next steps (in order)

  1. Complete the dry rehearsal (capital gate). Needs: redeploy PM 43023ba + a FRESH US afternoon lock (~16:00–18:00 ET) → the 🧪 DRY-RUN alert fires → cross-check market/bucket vs the lock → capital gate cleared. (The gate-skip blocker is fixed; this is the remaining step.)
  2. Build the wx-signal v2 producer adaptation (unblocked now that pmv2-contracts is published) — emit the v2 READY entry-order to pmv2.order.weather.entry, add the sizing module, vendor pmv2-contracts. See the PENDING section above.
  3. Then the live arm (operator flips weather source mode off → dry → live in pmv2_autotrade_sources).
  4. Publish-retry gap (carried over) — publish is best-effort, gated on the Supabase insert; an ack-timeout leaves the signal in Supabase but not on the stream, no retry. Close via consumer-side Supabase reconciliation or publisher re-publish-until-acked. Low-risk since the DNS fix.

Key refs