For Agents

Reverse-chronological session log. Newest entries at top, grouped by date (## YYYY-MM-DD). Each bullet: one piece of work, short summary, wikilinks to docs touched. Updated by obsidian-documenter on every project doc write. Read by historian at bootstrap.

2026-07-02

  • Full-family strategy synthesis (14-agent workflow, adversarially skeptic’d) → per-lane VERDICTS. US taker lock-and-hold PILOT NOW (1,500; BLOCKER = position_manager fee_usd=0 fix; pass gate n≥50 fills & roi_lcb_80>0 net of true fees & median slip ≤2c & wr≥75%; honest EV −2c…+8c/8 median fillable-within-3c depth, whales sweep the liquid moment in 1 min — london 0.72→0.90 behind a 300 on ≥50% of days AND first sweeper >15 min post-lock). EU maker/LR PILOT after infra (madrid+paris quoting vacuums 12–18c spreads, 150–200, no modal quoting 15:00–18:00). Asia RESEARCH-MORE, no capital (Seoul FOLD at 65% lockout; CN/Tokyo decided by one binding lock-timed IEM replay + a day of book snapshots, bar ≥+0.10/1–3k/day US-wide. Synthesis file: /Users/levander/coding/pmv2/research/weather-strategy-synthesis-2026-07-02.mdweather-strategy-verdicts-2026-07-02
  • Prior beliefs OVERTURNED (live-verified): weather LR pools EXIST (~55/mkt/day; seoul-lowest 1,010, munich 703 — prior “no LR on weather” was wrong); resolution stations pinned engine-critical (paris = LFPB Le Bourget NOT LFPG, london = EGLC City NOT Heathrow, denver = Buckley SFB, istanbul/moscow/tel-aviv via NOAA weather.gov, HK = HKO 0.1°C); fee_usd=0 bug in position_manager place_order overstates live ROI 0.45–0.67%/side (true fee 0.05·p·(1−p)); api.weather.com public web key polls the WU resolution truth directly (~30-min, every city — ToS-gray, keep METAR fallback); universe is ~7× bigger (48 cities, 41 new, + a lowest-temperature series whose seoul pool is the biggest); °C boundary gate fixable via national tenths feeds (DWD/FMI/IMGW/JMA-AMeDAS/SYNOP-OGIMET) for 7/8 EU cities (london/seoul are the gaps) — weather-prior-corrections-2026-07-02
  • Methodology trap CAUGHT — proxy-timed entries inflated EV ~2–3×; two headline results overturned. The fresh Jun 14–Jul 1 backtest gated on the 15:30-local modal price but entered earlier prints (mean 7.5c below conviction; 57/59 US + 29/31 AS trades affected): AS +0.455→+0.145/. Also test-set selection: the 0.90 cap, flatten-at-18:00, and PT-0.92 rejection were all chosen on their own scoring window. Binding rules: entries at-or-after ALL gating info exists; never tune a parameter on its scoring window. Survives: gate direction (wr 65→85–92% monotone), hold-to-settle, and the original clean causal backtest (n=67, +29.9%/trade, real IEM locks — weakness is fills, not signal). Same failure class as the 1.96× convergence-conditioning and the win-label price-picker — weather-lookahead-trap-2026-07-02

2026-06-25

  • AS-lane DRY order-publish ARMED (~20:15Z): pin 9a07960e5521c4 + env WEATHER_AS_PUBLISH=1. First time the AS lane has a path to publish to NATS. New pin is env-gated, default OFF at the binary level — on gate_admit, scan() publishes the pmv2-contracts entry envelope to pmv2.order.weather_as.entry, once per market/day (self-measure dedup leverages the same ledger as the M9 fixes). Boot version line: e5521c4 feat(weather-as): env-gated order publish on gate-admit (WEATHER_AS_PUBLISH, default off). Money safety intact: weather_as=dry_run, global dry_run, PM gates at build+sign — NO POST hits the exchange; envelopes flow into NATS so downstream can dogfood the message shape but the chain dead-ends at the signer. Env push via rtk proxy cat secrets/pmv2-weather-as-algo.env | rtk proxy gh secret set PMV2_WEATHER_AS_ALGO_ENV -R wowjeeez/terraform ([[gcp-terraform-ansible-gotchas|gotcha #28]]); verified inside container via docker exec printenv | grep WEATHER_AS_PUBLISH. NATS NKey already had pmv2.order.weather_as.> PUB permit from pin 5db34c0; PMV2_ORDERS wildcard pmv2.order.> captures the subject — no NATS-side change. First scan post-arm: 11 cities scanned, 1 locked (paris, gate_blocked → no admit), published=0 (normal idle), settled=1; arm is hot, first publish fires on next gate_admit. Kill switch: unset env + re-push secret + redeploy — weather-as-dry-publish-arm-2026-06-25, weather-as-deploy-pattern-b-2026-06-23

2026-06-24

  • Live --loop crash FIXED — Python %Z strptime can’t parse abbreviated zone names (@ 7939da2). Found in PRODUCTION by @deploy in babylon weather-shadow #442: the live pmv2-weather-as-algo --loop container spammed loop_error: time data '2026-06-23 15:56 PDT' does not match format '%Y-%m-%d %H:%M %Z' (also JST, CST). Root cause = a strftime→strptime round-trip through %Z: rehearse strftime’s the station-local lock time INTO trace["lock_local_time"], then shadow_engine.lock_from_trace re-parsed it with datetime.strptime(s, "%Y-%m-%d %H:%M %Z") — but Python’s %Z strptime CANNOT parse abbreviated zone names (PDT/JST/CST; only UTC/GMT/numeric offsets) → ValueError on EVERY locked city → exception propagated out of scan() and aborted the whole scan tick → the live service recorded nothing on locked ticks. Fix: don’t round-trip through a %Z string — derive the local datetime directly from data in hand: datetime.fromtimestamp(trace["lock_ts"], tz=timezone.utc).astimezone(ZoneInfo(st["tz"])). The lock_local_time strftime field stays as a human-readable trace value (strftime is fine; only the strptime parse-back was broken); build_signal uses that datetime only for an HH:MM display in its reason, so a tz-aware local datetime is correct. Lessons: (1) never round-trip a localized datetime through strftime('%Z')strptime('%Z') — carry epoch + IANA zone and reconstruct via fromtimestamp(ts, utc).astimezone(ZoneInfo(zone)); (2) test-coverage gap that shipped it — every existing test MOCKED lock_from_trace itself, so the round-trip inside it was never exercised end-to-end; when you mock the function under test, the real internal path goes uncovered — added regression tests across the real JST + PDT paths; (3) a win for deploy-mode-OFF-first — the forward dry-run (mode OFF, zero economic risk) surfaced a real bug before any capital. Full suite 46 tests green on Python 3.14; fix pushed main @ 7939da2, new image in GAR, awaiting @deploy’s container bump — weather-as-selfmeasure-pnl-fixes-2026-06-23

2026-06-23

  • Self-measure P&L CORRECTNESS fixes — resolves HANDOVER PART 2 findings M7/M8/M9 + the L7 cache bug (all in selfmeasure.py + config.py + shadow_engine._measure_gated; TDD + multi-agent adversarial review, 44 tests green on Python 3.14). M7 (fill realism): reject fills above the producer’s ACTUAL limit min(0.95, current_at_lock + PRICE_BUMP), not just the 0.95 PRICE_CAP; limit_price threaded lock → _measure_gatedrecord_signalrealizable_fill (reject fill > limit_price + 1e-9; PRICE_CAP backstop when limit_price None for old records). M8 (P&L leak + void economics): closed-but-non-binary terminals were settled immediately as void at terminal mid — wrong twice ((a) closed:true can be mid-dispute pre-0/1 → premature mislabel; (b) a real Polymarket void REFUNDS stake, P&L≈0, not a mid payout). Fix: never infer void from price — _resolve_payoff returns binary only when closed&binary else None (pending); settle force-settles a still-unresolved row as a STAKE-REFUND void (payoff=our_fill_price → gross_pnl 0, net_pnl=-cost) only after MAX_SETTLE_AGE_DAYS (default 7). ALSO L7: _RES_CACHE was slug-keyed storing the whole event token map gated only on the queried token’s closed-ness → resolving one closed bucket cached OPEN siblings forever; now keyed per (slug, token), cached only when that token is resolved, open tokens re-fetch. M9 (cost honesty): records BOTH gross_pnl and net_pnl; slippage mode-aware (live=0 correct, fill is book ask w/ spread; replay fills are /prices-history MIDPOINTS so SLIPPAGE_REPLAY models half-spread); new config knobs POLYMARKET_FEE/SLIPPAGE_LIVE/SLIPPAGE_REPLAY/MAX_SETTLE_AGE_DAYS. Lessons: void=stake refund not mid payout; don’t infer resolution from outcomePrices alone; per-event caches poison sibling tokens (key per (event,token)); replay fills are spreadless midpoints (cost must be mode-aware). Open follow-up: void-age clock uses lock_ts (harmless on live --loop; switch to max(record_ts, entry_ts) if backfill/replay-of-old-locks is added). Unblocks the deploy-note P&L-trust warning — weather-as-selfmeasure-pnl-fixes-2026-06-23
  • AS-lane run-shape FLIPPED — from two OnCalendar cron jobs (--scan+--settle) to a continuous kind=service running python shadow_engine.py --loop. Why: (1) the fleet-wide #354 startup-announce directive converged (every pmv2 service announces on boot to telegram.outbound) + deploy is minting a weather_as NKey with PUB on telegram.outbound (#366) → a continuous service announces cleanly ONCE on boot, an OnCalendar cron would announce every 30-min run or not at all; (2) symmetry with weather-US, which also pivoted to Python (as_weather_algo format) and runs --loop kind=service. --loop internally = a cohort scan every SCAN_INTERVAL_SECS (default 1800) + a daily settle once per UTC day at/after SETTLE_HOUR_UTC (default 12), both env-tunable. Boot announce publishes the canonical telegram.outbound envelope (#362: msg_id/text/chat/parse_mode/kind) with text component: pmv2-weather-as-algo started with version: <sha> <title>; sha/title from GIT_SHA/GIT_TITLE Docker build-args (wired in Dockerfile + build-push.yml per deploy #358). CONSEQUENCE: the lane is NO LONGER NATS-free — boot announce needs NATS_URL + the weather_as NKey seed (NATS_NKEY_SEED, PUB telegram.outbound); order publishing stays OFF (no --live), NKey only for the announce until the publish leg arms; announce is best-effort (missing creds → startup_announce:false, never crashes). Built TDD, full suite green (22 tests on Python 3.14): shadow_engine.py (loop/_announce_startup/_git_version + --loop wired), publisher.py (DRY _connect_and_publish + publish_telegram_outbound), Dockerfile (ARG/ENV GIT_SHA/GIT_TITLE, CMD --loop), build-push.yml (gitmeta step + build-args); correction posted to deploy in weather-shadow (reply to #349). Still pending from deploy/Andras: WIF provider + SA literals for CI, the weather_as NKey, Andras’s terraform ci_github_repos grant — weather-as-deploy-pattern-b-2026-06-23
  • pmv2 contract invariant captured — position_manager keys on the wire source value, NOT the agent/service/repo handle (so handle renames are free). From @positionmanager babylon pmv2 #340. 3-way invariant that MUST stay equal: producer’s emitted wire source == pmv2_autotrade_sources.source_id (operator DB row) == NATS subject segment in pmv2.order.<source>.entry; any drift → PM sees unknown source → no sources row → lane silently fail-closes to Mode::Off (no orders, no error). Locked: US source=weather (pmv2.order.weather.entry), AS source=weather_as (pmv2.order.weather_as.entry). Renaming a source_id = coordinated 3-way change (producer emit + operator sources row + subject) → ping @positionmanager FIRST. GOTCHA: handle != wire source. AS lane is consistent (handle weather_as, source weather_as — born that way) but US lane intentionally does NOT match — handle/repo become weather_us while wire source STAYS weather. Do NOT “fix” the US source to weather_us to match the handle; that’s the 3-way change that fail-closes US to Off. weather_us must coordinate with @positionmanager before any source_id change (acked AS-side in pmv2 #344) — pmv2-source-id-3way-invariant-2026-06-23
  • AS-lane deploy convention DECIDED + implemented — containerized “Pattern B” via the apps Ansible role, run as TWO cron jobs (scan + settle), NOT a continuous service. Answered by @deploy in babylon weather-shadow #338. Image → GAR europe-west3-docker.pkg.dev/<project>/levandor-apps/pmv2-weather-as-algo:<sha>; deploy = deploys/apps.yml entry + dispatch deploy-apps.yml; the role auto-injects OTel env (OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318, OTEL_SERVICE_NAME=<entry>) → OTel is zero-config once the Python OTLP SDK is initialized in main(). Run shape = kind=job+OnCalendar because the strategy is windowed/entry-only. scan job python shadow_engine.py --scan (NEW entrypoint, built TDD test_scan.py 6 tests, reuses lock_from_trace+_measure_gated): sweeps the 12-station Asian cohort (skips iem_id=null), per-city lock→gate→size→self-measure record, NO publish, idempotent (selfmeasure dedups by (yes_token, entry_ts) deterministic lock ts), run every ~30 min UTC ~06:00–15:00. settle job --settle once daily ~16:00 UTC. Container artifacts in-repo: Dockerfile (python:3.13-slim — gate pickle needs scikit-learn 1.9.0/pandas 3.0.3/CPython 3.13+; non-root; ships gate/gate_model.pkl+asia_stations.json), .dockerignore, .github/workflows/build-push.yml (WIF→GAR sha+latest). pytest in new requirements-dev.txt, kept OUT of lean requirements.txt. Dry-run jobs need NO secrets (public Gamma/CLOB/IEM only; NATS publish leg dormant; env_file empty until publish arms). WARNING: accumulated self-measured P&L validates the PLUMBING not the EDGE — still affected by open code-review bugs M7/M8/M9 (fill-vs-limit, non-binary-terminal leakage, cost=0); don’t trust the numbers until fixed (HANDOVER.md PART 2). Pending from @deploy/Andras: reusable-workflow ref, WIF provider resource, deploy SA email, GCP project id, tagging policy, @Andras authorizing the WIF repo-allowlist grant (terraform.tfvars) — weather-as-deploy-pattern-b-2026-06-23

2026-06-22

  • AS lane gets its own babylon coordination identity (weather_as). Provisioned 2026-06-22 via /babylon:init for as_weather_algo/pmv2-weather-as-algo (source=weather_as); token in a project-local, gitignored, 0600 .mcp.json that shadows the plugin global babylon MCP per-repo (NO global BABYLON_TOKEN env). Part of splitting the old combined weather handle into weather_as (Asian) + weather_us (US, not yet on babylon). Subscribed: general, pmv2, weather-shadow, weather-jetstream. Prior coordination under weather: weather-shadow #302 (OPEN — @deploy deploy-convention Q for the Python producer), general #306 (lane align), general #313 (deploy SigNoz/OTel conventions), pmv2 #315 (contracts conform). Transition: general #335 (handle-transition status), weather-shadow #336 (re-anchored #302), general #337 (acked #313 — producer will run OTEL_SERVICE_NAME=pmv2-weather-as-algo before --live). Open blockers: #302 deploy convention, PMV2_ORDERS stream + operator pmv2_autotrade_sources row source_id=weather_as, OTel instrumentation (print-only now) before live — weather-as-babylon-identity-2026-06-22
  • pmv2 RE-ARCHITECTURE — monolith polymarket_fetch retiring → split into independent service repos (Andras’s direction via a Miro board). NATS JetStream becomes the spine; a shared pmv2-contracts crate is the seam; deps unidirectional producers → executor → notify. PRODUCERS emit signals: weather-US (THIS, source=weather), weather-AS (source=weather_as, separate repo/session), cohort (source=first_mover_core), crypto (source=crypto_shortterm_latency_test) (+ future @dionysus/@shipping lanes). POSITION MANAGER = the executor (pmv2-position-manager, @deploy/@positionmanager): consumes NATS DIRECTLY (no separate runner), gates (3-layer off/dry/live + caps + breaker), signs EIP-712, POSTs clob.polymarket.com, records/settles — polymarket_fetch’s consume+exec moves here; has NO GammaClient (producers own ALL gating); Mode currently OFF. NOTIFICATION SERVICE (dedicated, owner TBD) centralizes the ~25–30 scattered Telegram send-sites behind a NATS notify subject. Transport: stream PMV2_ORDERS over pmv2.order.<source>.<entry|exit>; PM binds ONE durable pull consumer on pmv2.order.> (explicit ack, Nats-Msg-Id=id dedup, needs a duplicate_window). Supersedes the old single-stream WEATHER_SIGNALS/weather.signals + hand-tracked docs/signal-schema.md design — pmv2-rearchitecture
  • pmv2-contracts crate PUBLISHED (github.com/wowjeeez/pmv2-contracts). serde + rust_decimal only; holds the envelope types, a Pricing discriminator enum (limit | derived), Side, SCHEMA_VERSION=2, and subject builders. source is a FREE String, NOT an enum — adding a lane = publish the new source string + insert an operator pmv2_autotrade_sources DB row (no contracts release). Producers + PM vendor it so the wire can’t drift. GOTCHA: a wrong/typo’d source silently routes to Mode::Off (acked-and-skipped, no error) — pmv2-rearchitecture
  • Trade wire-contract v2 SIGNED OFF (weather’s limit arm) — supersedes the old docs/signal-schema.md “Directive”/v1 envelope. Weather emits a READY BUY entry-order to pmv2.order.weather.entry: common id=uuid5("{source}|{market_slug}|{action}") (=Nats-Msg-Id), source="weather" (EXACT DB key), 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. PM places FAK marketable-limit at floor_to_tick(min(limit_price, best_ask)) (fills ≤ cap; ask>0.95 simply won’t fill = correct), re-resolves neg_risk/tick/min authoritatively, sized-out (shares<min)→Skip. ENTRY-ONLY (weather holds to settlement → no exit-signal; the ...exit path is cohort/crypto only). SIZING moved to producers: weather now sizes size_shares = clamp(floor(notional/limit_price), min_size=5, cap), default notional $50; PM caps/breaker = hard backstop. Weather context (city/bucket_label/observed_high_f/lock_ts/reason) stays OFF the wire (lives in the Supabase row via origin_ref; PM ignores unknown fields) — pmv2-rearchitecture
  • wx-signal v2 producer adaptation = PENDING (not built). Scoped but NOT done; was blocked on pmv2-contracts (now published → unblocked): rename action→side, max_price→limit_price, fold flat fields into market{}, add pricing="limit"+size_shares, schema_version→2, switch subject weather.signalspmv2.order.weather.entry, add a sizing module, vendor pmv2-contracts. The existing engine (crates/wx-signal: lock/price/nats/sink) + validated lock logic are UNCHANGED; docs/signal-schema.md in-repo is still v1 — weather-bet-live-status
  • Dry-test “too close to resolution” gate saga → FIXED (i64::MIN exemption, Position Manager main @ 43023ba). The dry rehearsal (capital gate) was blocked by the PM consumer’s liveness_gate: the lock strategy signals NEAR resolution by design (lock mid-afternoon, market resolves end-of-local-day → every signal lands ~6–9h out). Root cause: 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 PAST resolution (negative secs) so it kept skipping (a FRESH signal already passes with min=0). FIX = i64::MIN full exemption for Directive sources (also unblocks stale replays). Two earlier theories RULED OUT: (a) weather “ask>max_price” — PM caps min(limit_price,best_ask) and still builds → would’ve alerted; (b) “best_ask=None / no liquidity”. STATUS: dry-test completion PENDING — needs redeploy of 43023ba + a FRESH US afternoon lock → 🧪 DRY-RUN alert fires → cross-check → capital gate cleared — weather-bet-live-status
  • Repo RENAMED + MOVED. GitHub github.com/wowjeeez/weather-betgithub.com/wowjeeez/pmv2-weather-us-algo (PRIVATE); local /Users/levander/coding/weather_bet/Users/levander/coding/pmv2/us_weather_algo/. Now one of a sibling set of pmv2 service repos under /Users/levander/coding/pmv2/: us_weather_algo, as_weather_algo, cohort_algo, crypto_algo, position_manager, telegram_connector (GitHub: pmv2-weather-us-algo, pmv2-weather-as-algo, pmv2-cohort-algo, pmv2-crypto-algo, pmv2-position-manager, pmv2-telegram-connector, pmv2-contracts) — weather-bet-live-status
  • AS-lane current-state note written — the weather_as producer + full pmv2 reorg from the Asian-weather side. New authoritative note capturing: (1) the multi-producer → single-executor reorg — position_manager = the SINGLE execution authority (consume NATS → gate off/dry/live + caps/breaker → sign → POST → record; polymarket_fetch retires), producers (cohort_algo, crypto_algo, us_weather_algo, as_weather_algo) own strategy+sizing and publish READY entry-orders; the pmv2-contracts wire-contract (ONE envelope, pricing discriminator limit/derived, subjects pmv2.order.<source>.<entry|exit>, weather entry-only, TTR gate exempt for Directive); (2) repos — weather-bet → pmv2-weather-us-algo (/coding/pmv2/us_weather_algo, US + research) + new PRIVATE pmv2-weather-as-algo (/coding/pmv2/as_weather_algo, committed e9415dc); (3) the AS lane (source=weather_as, subject pmv2.order.weather_as.entry, lean pipeline observe IEM→causal lock→match→convergence-gate ≥0.68→size beats per-city +0.09 / ungated +0.06 on backtest, but single-season (~11d June), thin (n 14–19/latency), convergence-selected (optimistic ceiling), book reprices past us ~40–50%, NEVER RUN LIVE, end-of-stage-3, dry-run is next; (5) pending — research moving into the AS repo, pmv2-contracts then the publisher rewrite, @deploy guidance, then the dry-run. Companion to the US-side pmv2-rearchitecturepmv2-reorg-and-weather-as-2026-06-22
  • Win-label re-score promoted into a full vault note (was report-only) + given the reorg banner. weather-gate-winlabel-2026-06-19 — the discharge of shadow-rehearsal caveat #1 (“re-score the gate against a true WIN label”): selecting on P(WIN) instead of P(convergence) collapses the headline EV ~+0.14/** at matched 8.5% admit, and the win-label model loses to a per-city win-rate lookup (+0.0939), an ex-ante-only model (+0.0772), and ties ungated (+0.0553)entry_price alone (OOS AUC 0.7136) beats the win-model (0.6838). Signature of a disguised price-picker; CONDITIONAL-GO downgraded to LIVE-MEASUREMENT-ONLY (no capital, no Rust until a live loop beats an ungated/per-city baseline). Win base rate ≈0.83 (favorites); true-resolution OOS coverage 74.7% (not 93.6%), per-city 47–100% and biased with the signal. Research-only build_win_gate intentionally excluded from the as_weather_algo producer — weather-gate-winlabel-2026-06-19
  • Banners + key-reference fixes across the six AS-lane research notes so the vault is internally consistent: added an UPDATE 2026-06-22 callout to weather-asia-shadow-rehearsal-2026-06-19, weather-exante-gate-2026-06-19, weather-netcost-precheck-2026-06-19, weather-lag-distribution-2026-06-19, weather-gate-winlabel-2026-06-19, weather-top50-decypher-2026-06-19 noting the lane is now source=weather_as on pmv2.order.weather_as.entry in pmv2-weather-as-algo (/coding/pmv2/as_weather_algo) with research under pmv2-weather-us-algo (/coding/pmv2/us_weather_algo). Fixed load-bearing stale refs (weather_shadowweather_as, weather.shadowpmv2.order.weather_as.entry, research/shadow/ + weather_bet paths → the new repos); marked the shadow-rehearsal’s caveat-#1 “re-score” open item as DONE (→ the win-label note); reframed the net-cost city-universe-mismatch warning as addressed by the AS lane; superseded the top50’s design-only copytrade-rides-WEATHER_SIGNALS note with the cohort_algo producer; cross-linked all six into the current-state note — pmv2-reorg-and-weather-as-2026-06-22

2026-06-19

  • FINAL fillability verdict — unbiased full-population reprice-lag study → weak-GO, materially weaker than the first pass. Measured EVERY cheap-Yes buy (0.55–0.95) across the top-50 — no highest-paid-40 cap that biased the prior cohort — 28,664 entries, all 6 shards, adversarially critic-reviewed. Headline downgrade: realized EX-ANTE hit rate ~34.4% (unconditional reach@15 = 0.344), NOT the 63–67% conditional figure, because ~49% of cheap entries never reprice to 0.97 (conditional reach@15 = 0.675; the 1.96× gap is the never-converge haircut). Size for ~65% per-entry miss. Thesis FLIP (most robust finding): winners pay a ~2-tick PREMIUM, not a discount — same-token/same-instant fair_gap p25 +0.00 / p50 +0.02 / p75 +0.06, 75.1% above book → a convergence-timing edge bought slightly rich, NOT cheap-fill (the decypher’s discount withdrawal was correct). Binding economic gate = net-of-cost recovery of the +0.02 premium after taker fee + slippage — a GO precondition, answerable only in the live dry rehearsal; do NOT wait for paid<book (present only ~25% of the time). Gate must be EX-ANTE (city/session/METAR) — observed lag is forward-measured → look-ahead, critic-killed; lag distribution (converged median 37.1min, p25 8.2, p75 81.6, p90 127.6) is descriptive for fill-window sizing only. Per-wallet unbiased lag medians: HondaCivic 8.6 / vip68 10.7 (both INSIDE the 15-min window → can’t gate per-wallet) / Happening9014 20.6 (clears → “casualty” retracted, was a 120/180 artifact). City universe (the gate primitive): tier-A core = london, nyc, seoul, hong-kong, atlanta, paris, wuhan (+ tier-B lagos/seattle/los-angeles cap-to-CI); thin/provisional held out = istanbul (n=96)/sao-paulo/lucknow/wellington; fast-repricing EXCLUDED = denver/manila/karachi/taipei/tel-aviv/etc. Methodology integrity: 120-vs-180 horizon bug fixed (aggregator now re-derives at 180, horizon-authoritative — wx-lag-pipeline-bug); shard_2 (16.7%) dropped then restored (n 23,907→28,664), aggregate barely moved (conv 0.511→0.509, reach@15 0.677→0.675) → robustness confirmed, per-city no longer provisional. Report research/reports/weather-lag-distribution-2026-06-19.md; data research/copytrade/top50/lag/lag_distribution.jsonweather-lag-distribution-2026-06-19
  • DATA-PIPELINE BUG in the repricing-lag study: converged baked at 120-min, report published 180-min, and the 180-min numbers were NEVER persisted. The 6-shard lag pipeline (research/copytrade/top50/lag/: measure_shard_N.py per shard, aggregate_lag.py aggregator) computes reprice_lag_min at a 180-min window but baked the converged boolean at 120 min (measure_shard_1.py:105 lag<=120.0, shard_3.py:96, shard_4.py:110 reprice_lag(...,120)) → the exact 120/180 mismatch the report’s R3 says was “fixed.” Empirically proven: every row with reprice_lag_min ∈ (120,180] has converged=False in all 5 shipped measured_shard_*.json (0,1,3,4,5). The report’s 180-min headline (convergence 0.511, conditional reach@15 0.677, median lag 37.03) was re-derived at analysis time from reprice_lag_min and never written backlag_distribution.json on disk still showed STALE 120-min numbers (0.4492 / 0.6331 / 29.56). Key insight: 180-min convergence is fully recoverable WITHOUT re-pulling CLOB because reprice_lag_min is already a clean 180-min measurement (converged@180 == reprice_lag_min is not None); re-derivation exactly reproduced the report (0.5109 / 0.6774 / 37.03, p25=8.37/p75=81.99/p90=127.26, unconditional 0.3461, exactly 1,475 entries gained 120→180). Fix: (a) measure_shard_2.py:113 threshold 120.0→180.0; (b) aggregate_lag.py now computes convergence at aggregation time via is_converged(e) against CONVERGE_HORIZON_MIN=180.0 (lines 51/54–56/61), making the aggregator authoritative on the horizon and immune to stale baked flags in any shard — the DRY fix, matching how the report’s numbers were actually derived. Gotcha: NO lib.py exists (task referenced one) — CLOB-fetch logic is inlined (and drifted) per measure_shard_N.py. Each shard ~4,807 entries; shard_2 (16.7% of population) missing its measured output, concentrated in headline cities (london 510, nyc 443, seoul 292, hong-kong 278, atlanta 169, paris 164). Report research/reports/weather-lag-distribution-2026-06-19.mdwx-lag-pipeline-bug
  • Top-50 copy-trade decypher (v2 of copytrade-top-weather-traders) → “capacity, not return”; ≤0.95 fillable edge plausible but NOT proven. Official Polymarket weather leaderboard (category=weather, month+all-time union) → 87 candidates → 65-wallet activity gate (min 20 resolved markets, min 17.44M → $1.52M reconstructed PnL** (8.7% cap-weighted, 11.5% median ROI); ROI inversely correlates with absorbable capital — late-settlement whales (n=22) hold 70% of capital at the lowest ROI (6.2%), longshots (n=4) highest ROI (55.5%) on trivial size → whales win on turnover + execution at the close, not edge-per-dollar. Own-edge leans NEGATIVE: the 99c carry is NOT fillable for us; the candidate edge is the repricing-lag layer (cheap-Yes 0.55–0.85 post-METAR-lock pre-reprice), but only 3/10 cohort wallets clear the latency bar, reprice-lag is right-skewed (typical observed <15min vs 20–50min population medians → a 20-min gate rarely fires), and the “cheap fill on a laggy book” claim is contradicted trade-by-trade (winners paid ABOVE book in 11–13/15 → possibly risk-taking not lag-harvest). Dry-rehearsal gate NOT answered affirmatively — reframed as the experiment to settle discount-to-fair vs residual-risk. Copy-trade doctrine = “detect, don’t mirror” (we lose the latency race → detect the lock from our own METAR feed earlier, cohort = confirmation): Tier A copyable (Happening9014, vip68, EngineOfHondaCivic-marginal, WeatherTraderBot, 0x686880, 0bot, cmcbrown, Pityok1) vs Tier B confirmation-only carry whales (NoonienSoong, 1-800-LIQUIDITY). Design-only copytrade NATS source spec: rides existing WEATHER_SIGNALS stream, source-routed, cohort-first-mover K-of-N in window W, max_price ≤0.95, UUIDv5 Nats-Msg-Id dedup. All per-leg PnL is FIFO-inferred (no settlement outcomes in raw data) → diverges both ways (ColdMath 170× under; longshots ~1/3); 3,500-trade/wallet cap truncates whales; fillability capped at 40 entries/wallet (highest-paid → biased samples). Corrected from v1: BeefSlayer = mid-range directional (not 0.99 whale), ColdMath = recon-failure (not negative control). Report research/reports/weather-top50-decypher.md; data research/copytrade/top50/{top50_profiles.json,fillability.json}weather-top50-decypher-2026-06-19
  • LOWEST-temperature late-settlement = NO-GO (do NOT build the lowest mode). Polymarket runs BOTH highest AND lowest daily temp markets; our engine only trades highest. Clean causal backtest of the symmetric LOW play (research/strategy/clean_backtest_low.py, results clean_low_results.json): running-MIN, morning no-FALL lock, lowest-temperature-… slug. US cohort nyc+miami, 66 locked days (atlanta/chicago had NO lowest markets on Gamma). Verdict NO-GO: reliability caps at 0.803 all-locked / 0.667 tradeable; return mean −6.1% / 80%-LCB −23.1% — BOTH gates fail. Root cause = structural asymmetry: the daily MAX is reliably mid-afternoon (15:00+90min-no-rise lock is safe), but the daily MIN is NOT time-pinned — an evening cold front pushes the min below the dawn low ~20% of the time (12 of 13 misses were post-lock falls), which no lock-hour avoids (sweep flat across 7–12h, since the min is firmly set by 07:00). Confirms the highest play is special; stay highest-only — weather-bet-live-status
  • June-18 had NO highest market (one-off Polymarket gap) → 0 signals despite 4/4 US cities locking correctly (ATL 87 / MIA 92 / NYC 89 / CHI 72 °F). Gamma: highest june-16/17/19 all exist, june-18 highest MISSING, june-18 lowest present. Engine healthy (locked all 4 on live data), just nothing to trade; the dry rehearsal couldn’t fire. This is what surfaced the lowest-market avenue (→ the NO-GO backtest above) — weather-bet-live-status

2026-06-18

  • wx-signal LIVE + publishing real signals; dry-rehearsal armed. Engine deployed on polymarket-infra (GCP) in --loop, pin a2fa5ee (pushed github.com/wowjeeez/weather-bet, branch feat/wx-signal-engine). SigNoz confirms real acked publishes 2026-06-17 — Atlanta (16:56 EDT) + Chicago (17:27 CDT) — plus correct skip-on-untradeable once a bucket drifts >0.95. End-to-end NATS pipeline: publisher → JetStream WEATHER_SIGNALS/weather.signalspmv2 durable consumer (built by @polymarket, dormant mode=off). General source-routed envelope (docs/signal-schema.md, cohort rides the same stream with zero weather change); deterministic signal_id = uuid5("{source}|{market_slug}|{action}"); separate publisher/consumer NKey identities. At the dry-rehearsal gate (operator flipping weather→dry; next US-afternoon lock = the rehearsal, no live order). Open: publish-retry gap — best-effort publish gated on the Supabase insert, an ack-timeout leaves the signal in Supabase but not on the stream with no retry (flagged to @polymarket: consumer Supabase-reconcile vs publisher re-publish-until-acked). Full snapshot — weather-bet-live-status
  • ⭐ Live ~400% lag observed on London (EU °C) → automate intl capture (NEXT STEP). The °C NO-GO holds for the late-settlement lock-and-hold (enter near resolution = expensive, 60.8% reliability), but a 400% lag implies a cheap early entry (~0.20) on a strong obs-lead — a different +EV profile (even ~70% reliability at 0.20 entry is hugely +EV). Hypothesis: intl is exploitable via a distinct early-cheap-entry variant, which needs its own backtest before building. This is the next build direction — weather-bet-live-status

2026-06-17

  • Honest re-backtest → GO on US fillable cohort, NO-GO intl °C. 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 (NYC/Chicago/Miami/Atlanta), 151 locked days: 96.7% all-locked reliability, but ~53% already >0.95 at lock (locked out of the easy winners); on the ~44% fillable (≤0.95) cohort: 94% win, +29.9% mean return, +17.4% 80%-LCB — real positive edge, smaller day-count. Intl °C: NO-GO (60.8% tradeable reliability, return 80% CI (−13.3%, +31.1%)). Load-bearing unknown = real fills (what the dry-rehearsal measures) — weather-bet-live-status
  • wx-signal engine built + twice max-effort code-reviewed + hardened, committed + pushed. Rust crate mirrors engine.py (80/80 golden parity). Headline fix: reversed a max_price-defaults-to-0.95 bug into skip-on-untradeable (emit nothing when no confirmed ask ≤0.95, retry next tick). Atomic Supabase INSERT+pg_notify in one tx; dual-write NATS publisher (dormant until NATS_URL set); --smoke on-demand e2e flag; serde tolerant to Gamma string/number drift; one shared reqwest::Client; connect-sink-once — weather-bet-live-status

2026-06-16

  • Captured async-nats 0.49 NKey-auth API + edition-2024 set_var gotcha (from wiring NKey auth into wx-signal, crates/wx-signal/src/nats.rs). (1) NKey seed auth = ConnectOptions::with_nkey(seed: String)sync + infallible (returns Self), unlike with_credentials_file(path).await (async, io::Result); builder form ConnectOptions::new().nkey(seed); gated behind the nkeys feature which IS in async-nats’ default set; seed is validated lazily at connect time (nkeys::KeyPair::from_seed + nonce sign → bad seed = ConnectError Authentication). SignalPublisher::connect() picks auth by precedence NATS_NKEY_SEED > NATS_CREDS > no-auth, reads NATS_URL first → returns None (dormant) if unset before any auth/network work, never logs secrets. (2) GOTCHA: std::env::set_var/remove_var became unsafe fn in edition 2024, and this workspace’s [workspace.lints.rust] unsafe_code = "forbid" makes them an absolute dead end in tests — even unsafe { } won’t compile (forbid can’t be overridden by #[allow]). Workaround for the dormancy test: assert NATS_URL unset + connect().await.is_none() (holds regardless of seed, since URL is read first) without ever mutating env; plus clippy-pedantic used_underscore_binding fires if you USE a _-prefixed binding — wx-signal-nats-nkey-auth
  • VALIDATED weather strategy plan → late-settlement / repricing-lag is the GO play. Deep-dive (full report research/reports/weather-trader-deep-dive.md, 36.5K) reframes everything: Polymarket closes the book at a FIXED 12:00:00Z for every city → lands at different local times. Asia (UTC+9) closes 21:00 local = day OVER, bucket already ~1.0 (no edge); US (07-08:00 local) and London (13:00) close BEFORE the afternoon peak, so the winning bucket trades only 0.40-0.57 at close = genuine forecast game, 0.43-0.61 gap to 2.88M/2.07M at 0.998 median entry — revealed-preference, can’t withdraw losing capital); early-forecast real but tiny (2%); round-trip/sell-the-pop = lowest PnL, AVOID. Edge quantified across 113 full-coverage market-days: winning bucket first crosses 0.90 with ~6-8c left (~3h out) → ~1.5c at 2h → ~0.4c at 1h; >=0.90 in final 2-3h resolved YES 55/55 = 100% (risk-free once daily high observed). Whales leave 4-8c on the table (wait for 0.99, ~0.5c at size) → alpha = take the 0.90-0.95 crossing they decline, gated to final 2-3h (the one false positive — Beijing 29C@0.93 then 30C won — was a degree-boundary day acted on 6h early → the time gate is load-bearing). Entry rule: poll resolution station METAR per-minute → running whole-degree daily max → high pinned + final 2-3h → BUY 0.90-0.95 crossing → hold to 750 bond). Wrong-station traps: Dallas=KDAL not DFW, Houston=KHOU, Denver=KBKF, Moscow=UUWW, Chicago=KORD, skip Hong Kong (0.1°C city station). TWO products: global=Wunderground (we trade this) vs US/CFTC=NWS CLI/KNYC. Resources: repos suislanchez/polymarket-kalshi-weather-bot (441★), alteregoeth-ai/weatherbot (332★), warproxxx/poly_data (2.1k★), py-clob-client + real-time-data-client, avwx-engine; real PM weather leaderboard (gopfan2 ~277k); IEM ASOS archive (backtest ground-truth); ColdMath autopsy (polymarketweather.com). NEXT (validate-first): Python backtest w/ real multi-city Wunderground/METAR resolution feed to confirm 6-8c edge OOS → then build live Rust nowcaster on the validated rule — weather-strategy-validated-plan
  • Copy-trade recon → the edge is nowcasting the winning bucket, NOT buy-cheap/sell-pop. Reverse-engineered on-chain trades (data-api.polymarket.com/trades?user=<addr>, ~3,500 most-recent each, offset cap 3000) for the top weather traders off weatherbot.fi /api/leaderboard (13 listed but ranks GENERAL PnL not weather; Poligarch & russell110320 trade ~0 weather → ~10 real ones, trading dozens of cities). Three archetypes: (1) late near-certain settlement = the whales / real money — HondaCivic 1 (+ BeefSlayer/aboss/ColdMath); edge = capacity+fast execution at close, low variance; (2) early forecast value — Railbird (favorites ~0.85 at open), dpnd (median 33h pre-resolution, cheap/mid), Poligarch; (3) round-trip/sell-the-pop = the user’s original idea — ONLY huskyvs (47% round-trip) & ShyGuy1 (34%), and huskyvs is LAST in PnL ($18K, rank 12). Killer example: HondaCivic London 18°C Apr 30 — BUY 7,523@0.328 then SELL 4,001@0.999 six minutes later (bucket repriced 0.33→1.00 intraday because they saw it win first). Independently confirms the drift arc: naive drift NO-GO −28% → forecast-conditioning priced-in/breakeven → live nowcast = only surviving edge, and the top weather trader makes their money exactly there. Script+data: research/copytrade/{analyze.py,profiles.json,raw/}. NEXT: scope a nowcast strategy (entry rule, obs source/latency, repricing-lag backtest off cached price curves) — copytrade-top-weather-traders

2026-06-15

  • Intraday drift study v2 → forecast-conditioned is a live CANDIDATE. Re-analysis off the same 112-market-day cache (no re-fetch): forecast proximity is a real winner-predictor (unlikely 10–25¢, NEAR |dist|≤1°C of lead-2 forecast: n=125, 20.0% win, blended +1.2% after costs vs FAR −19.9%; win-rate peaks at dist 0/+1 ~21%, collapses to ~5% at ≤−2°C → buy AT/just-ABOVE forecasted high). Exit timing dominates: same NEAR universe, first-+50%-pop → +1.2% but hold to 18:00 local convergence → +9.6% (80% LCB −6.9%, hi +26.4%). Liquidity NOT the blocker (median bucket vol ~1K). Progression buy-blind ≈−15% → near-forecast +1% → near-forecast+18:00 +9.6%; two levers stack. CANDIDATE not GO (n=125, single month, LCB still negative). Confirms 2026-06-11 nowcast/information-timing pivot. NEXT: extend window to several months (tighten CI), tighten to dist∈{0,+1} + sweep exit hour 16–20h (free off cache), add live intraday nowcast — intraday-drift-study-naive-no-go
  • Intraday drift study → naive buy-open/sell-pop is NO-GO. 112 market-days (28 dates × 4 cities; London/Paris/Seoul/Beijing max-temp), 1,224 buckets w/ intraday curves + lead-0/1/2 Open-Meteo forecasts + realized highs (cache research/data/, report research/report.md, code on research/intraday-drift). Drift is REAL (unlikely 10–25¢ cohort: 49% hit ≥+50% peak, median MFE 10¢, peaks 17–19h local) but pre-registered test on ultimately-LOST unlikely buckets w/ +50% causal exit after spread costs = mean net −27% (80% boot CI −32%..−22%, n=422) due to survivorship bias (~half decay to zero, can’t pick winners at open). Same exit on ultimately-WIN buckets = +54% → edge is in PREDICTING winners (forecast-distance), not the drift; confirms the 2026-06-11 nowcast/information-timing pivot. NEXT: condition on ±1°C of lead-2 forecast + time-based exits around 17–19h peak (runs off cache in seconds) — intraday-drift-study-naive-no-go
  • Added wx-research crate skeleton to workspace (branch research/intraday-drift): 7 empty module stubs (cache, charts, fetch, metrics, report, slugs, types), new workspace deps plotters 0.3 + csv 1, builds cleanly (86 crates) — wx-research-crate
  • Added ClobPriceHistory to wx-market/src/prices_history.rs: read-only CLOB price-history client, two public async methods (history interval-capped, history_window unix-timestamp windowed), 3-retry / 200ms-base backoff, TDD with wiremock — wx-market-prices-history
  • Added TradesClient to wx-market/src/trades.rs: read-only data-api trades client, page(asset, limit, offset) single-page fetch + all_by_asset(asset) auto-paginator (PAGE=500, cap 20k), de_flex_i64 flex deserializer for string/number timestamp API drift, 3-retry / 200ms linear backoff, wiremock test passing — wx-market-trades-client

2026-06-12

  • Fixed resolve_one to use TWC as primary resolution source with IEM as cross-check fallback; was silently skipping ~half of markets due to IEM lag/flakiness; 3 regression tests added, 25/25 green — fix-resolve-twc-primary-source

2026-06-09

  • Storage migrated SQLite → Supabase Postgres (dedicated schema weather_bet), killing the ephemeral-storage footgun (missing WXBET_DB_PATH → data loss on every container recreate). Captured the durable fleet knowledge: dedicated-schema isolation convention, idempotent embedded schema.sql via raw_sql (deliberately NOT sqlx::migrate!), the SQLite→Postgres strict-typing gotchas ($N placeholders, DOUBLE PRECISION for f64, BIGINTi64, CURRENT_DATE+::date casts, INNER JOIN LATERAL for latest-per-group), the session-pooler-:5432-NEVER-:6543 connection rule, and the testcontainers-behind-a-testkit-feature test harness (88/88 green vs real Postgres). Code complete + verified (clippy/fmt clean), NOT yet committed; @deploy handed off (add SUPABASE_DB_URL, drop WXBET_DB_PATH + /var/lib/weather-bet volume). — weather-bet-supabase-migration

2026-06-07

  • Created weather-bet project; documented verified Polymarket weather market structure (11 neg-risk 1°C buckets, ICAO/Wunderground whole-°C resolution, Gamma API endpoints, observed liquidity) — polymarket-weather-market-structure
  • Initialized project overview — weather-bet