For Agents

Strategy C, Stage 0 — the free, local, ~1-week terrestrial-AIS tracking pre-check — is BUILT, fully reviewed, and run-ready (2026-06-08). It is the cheap go/no-go gate that decides whether a live AIS feed can track the PortWatch weekly transit count closely enough (within ~2 ships) to be worth building the expensive Stage 1 (fleet + satellite). New modules live in the existing hormuz_bet crate (~/coding/hormuz_bet); two new binaries: the ais-collect daemon and track-report. Final state: cargo clippy --all-targets -- -D warnings clean; cargo test --lib = 46 passed / 5 ignored; build warning-free. Built subagent-driven (5 units, each two-stage subagent review + a final holistic review). Not a git repo (owner rule: never commit / git init). It has NOT been run for real yet — it must first collect ~7–10 days on an always-on host, and two reality-coupled field-name checks must be confirmed against live payloads before the counts are trusted. This realises the Strategy C plan whose kill-tests passed in hormuz-bet-basis-risk; it is a separate build from the Spec #1 backtest crate in hormuz-bet-build-spec1.

Stage 0 of hormuz-bet Strategy C: a one-week, zero-cost calibration harness that measures how well a cheap terrestrial AIS feed can nowcast the PortWatch weekly count, before any money or satellite spend is committed.

Build Status (2026-06-08)

Strategy C, Stage 0 — “free, local, ~1-week terrestrial-AIS tracking pre-check” — is BUILT, fully reviewed, and run-ready. It is not yet run for real (collection + two field-name confirmations come first; see below).

What “built” means here

  • New modules added to the existing hormuz_bet crate, project dir /Users/levander/coding/hormuz_bet (same crate as hormuz-bet-build-spec1, not a new project).
  • Built subagent-driven: 5 units, each with a two-stage subagent review, plus a final holistic review over the whole stage.
  • Final verified state:
    • cargo clippy --all-targets -- -D warningsclean.
    • cargo test --lib46 passed / 5 ignored (the 5 ignored are the network/reality-gated live tests).
    • Build is warning-free.
  • Not a git repo. Per the owner rule, the project is never committed or git init-ed.

Spec / plan provenance (exact paths, verified):

  • Plan: docs/superpowers/plans/2026-06-08-hormuz-strategy-c-stage0.md
  • Spec: docs/superpowers/specs/2026-06-08-hormuz-strategy-c-ais-calibration-design.md

What Stage 0 Decides

Strategy C is the Phase-2 AIS nowcast of the weekly TOTAL / 7-day-MA buckets (its kill-tests passed → GO conditional in hormuz-bet-basis-risk). The one open gate from those kill-tests was: can a live AIS track reproduce the PortWatch n_total within ~2–4 ships? — terrestrial receivers may undercount mid-strait. Stage 0 answers exactly that gate, for free, before committing to the expensive Stage 1 (fleet + satellite).

The gate, in one line

Run the collector ~7–10 days on an always-on machine, then run track-report after the next PortWatch Tuesday drop covers the collected days, and read the verdict:

  • GO — de-biased 7-day-sum error within ~2-ship margin AND the stationarity gap is stationary AND ballpark ok → green-light Stage 1 (fleet + satellite).
  • NO-GOkill Strategy C (or escalate to better data).
  • INCONCLUSIVE — keep collecting; the harness gains power as more days accrue.

Architecture As Built — Modules

All new modules live inside the existing hormuz_bet crate. The data path is: AIS observations → in-polygon transit detection + dedup → our daily counts in SQLite → compared against the first-published PortWatch snapshot → calibration verdict.

geo — geometry

  • Ray-cast point-in-polygon test.
  • A provisional Hormuz box (geo::hormuz_zone) — currently a rectangle lon 56.20–56.95 / lat 26.25–26.70. This is a best-guess box pending owner sign-off (see Open Items).

ais — feed ingestion

  • An Observation type, an AisSource trait (so feeds are swappable), and an aisstream.io WSS client implementing it.

transit — PortWatch’s counting rule, reproduced

  • Implements the PortWatch transit-call rule: in-polygon position + 48h per-MMSI UTC dedup (one vessel counted once per 48h window) + a teleport / sanity spoof filter (rejects physically impossible jumps) + a vessel-type cachedaily counts.

cstore — local calibration store (SQLite)

  • our_daily_counts — our reconstructed daily transit counts.
  • pw_snapshots — every PortWatch snapshot, with first-published = the earliest snapshot for a given day (not the latest revision). This is the scoring target.

track — the calibration verdict

  • De-biased 7-day-SUM error (window_sum_error over pairs.windows(7)), residual autocorrelation, a ballpark check, stationarity-vs-GFW, and the final verdict. (The two existential fixes below live here.)

gfw — Global Fishing Watch dark-event proxy

  • Pulls GFW dark-event data as a proxy for AIS jamming / outage intensity, used by the stationarity gate.

portwatch::snapshot — daily PortWatch snapshot

  • Takes a daily PortWatch snapshot, reusing the Spec #1 PortWatch client (the portwatch/ client from hormuz-bet-build-spec1).

Binaries

  • ais-collect — the always-on collector daemon (ingests the WSS feed, writes observations / daily counts).
  • track-report — run after a PortWatch drop; prints the calibration verdict.

The Two Existential Red-Team Fixes (wired into the live verdict path)

These two are the difference between a calibration that catches a fatal flaw and one that hides it. Both are wired into the live verdict path, not just tests.

Fix 1 — GO gates on the de-biased 7-day CUMULATIVE SUM error, NOT daily error

A systematic undercount (terrestrial feed missing some mid-strait vessels every day) is small per day but accumulates over the week — and the market resolves on the weekly sum, so a daily-error gate would wave through a feed that is quietly losing ships all week. Fix: GO gates on the de-biased 7-day cumulative SUM errorwindow_sum_error computed over pairs.windows(7) — so an accumulating systematic undercount is caught, not averaged away.

Fix 2 — a non-stationary jamming gap forces NO-GO before the GO gate

If the gap between our AIS count and PortWatch is drifting (e.g. GPS jamming is worsening, so our feed degrades over time), then a de-bias term fitted on past days will not hold in future — the calibration is unstable. Fix: a non-stationary gap (|stationarity_r| ≥ 0.6 against the GFW jamming proxy) forces NO-GO before the GO gate is even evaluated. A stationary, predictable gap can be de-biased; a drifting one cannot, so it must kill the strategy rather than be corrected.

Other Correctness Properties (all wired)

  • First-published scoring — calibration scores against the earliest PortWatch snapshot for a day, not the latest revision. (You must beat the number as it first published, since that is what resolves; later revisions are hindsight.)
  • Coverage-% exclusion — days with sub-0.5 coverage are dropped, never zero-filled. A partially-observed day must not masquerade as a low-count day.
  • Silent-dead-feed hardening — a zero-message UTC day forces coverage = 0 (so it is excluded, not read as a genuine zero-ship day), plus a live [FEED SILENT] warning after ~2 min of no messages so a dead collector is noticed immediately rather than silently corrupting the calibration.

Two reality-coupled verification steps before trusting any counts (do on first run)

Two sets of field names were transcribed from spec best-guess and MUST be confirmed against live payloads before the counts mean anything:

  1. The aisstream.io envelope field names + bbox corner order.
  2. The GFW v3 Events API dataset id / request body / response schema.

Both are exercised by the #[ignore] tests, which need a free AISSTREAM_API_KEY and a GFW_API_TOKEN:

cargo test -- --ignored --nocapture

If the live envelope differs from the best-guess, fix the field mapping before reading any track-report output.

How To Run The Pre-check

Stage 0 run sequence (needs an always-on host + free API keys)

# 0. one-time: confirm the two reality-coupled schemas against live payloads
cargo test -- --ignored --nocapture       # needs AISSTREAM_API_KEY + GFW_API_TOKEN
 
# 1. collect ~7–10 days on an always-on machine (daemon)
cargo run --bin ais-collect
 
# 2. AFTER the next PortWatch Tuesday drop covers the collected days:
cargo run --bin track-report              # prints GO / NO-GO / INCONCLUSIVE

GO → green-light Stage 1 (fleet + satellite). NO-GO → kill Strategy C (or escalate data). INCONCLUSIVE → keep collecting.

Open Owner Items

Two human decisions before / during the run

  1. Sign off / refine the provisional polygongeo::hormuz_zone is currently a box (lon 56.20–56.95 / lat 26.25–26.70). Confirm or tighten it so the in-polygon transit rule matches the PortWatch chokepoint6 definition (see hormuz-bet-portwatch-data).
  2. Choose the always-on host for the ais-collect daemon (the ~1-week collection needs uninterrupted uptime; gaps become excluded coverage).
  • hormuz-bet — project index; Strategy C is the Phase-2 AIS-nowcast play, and this is its Stage 0 gate.
  • hormuz-bet-basis-risk — where the Strategy C kill-tests passed (GO conditional); the single open gate (“can live AIS track PortWatch n_total within ~2–4 ships?”) is exactly what Stage 0 measures. Also the source of the dark-vessel / jamming / spoofing hazard that the teleport filter and GFW stationarity gate defend against.
  • hormuz-bet-build-spec1 — the Spec #1 backtest crate in the same repo; Stage 0 reuses its portwatch/ client for the daily snapshot. (Separate build, same hormuz_bet crate.)
  • hormuz-bet-ais-providers — the Phase-2 feed landscape (aisstream, GFW, terrestrial-only caveat) this stage instantiates.
  • hormuz-bet-portwatch-data — the chokepoint6 definition the provisional polygon must match, and the snapshot source for first-published scoring.
  • hormuz-bet-backtesting — the validation discipline (first-published, coverage exclusion, no zero-fill) this calibration mirrors.