Skip to content

Raw-as-reported

The rule is one line: the first observation for a given (station, observed_at, report_type) is final. No corrections, no overwrites, no backfills. If NWS issues a correction an hour later, the original number is still what the market settled on. So the original number is still what lives in the archive.

Corrected-in-place (wrong)
# Most weather archives silently overwrite
# corrections. Your settlement replay drifts.
obs = archive.get("NYC", at="2026-04-02T12:00Z")
obs.temp_f
# 71  ← NWS correction issued Apr 4
#       (original report was 69)
Raw-as-reported (right)
# Mostly Right preserves what was actually
# reported at query time. Replay is exact.
obs = client.observations(
  "NYC", as_of="2026-04-02T12:00:00Z"
)
obs[0].temp_f
# 69  ← original METAR, as received

A Kalshi weather contract settles on the number the NWS reported at the settlement time. If at 14:51Z the NYC station broadcast a temperature of 52°F, that contract settled on 52. An hour later, the station may publish a correction saying it was actually 51. Too late. The market already paid out.

If your backtest archive quietly replaced 52 with 51, your model now believes a contract settled on 51. When you size the next position against that belief, you are trading on a market that never existed.

Your backtest is only useful if it reproduces the data the market traded on. That is what raw-as-reported guarantees.

For each row insert:

if row.key NOT in archive:
insert row
else:
skip # first-write wins

The key is (station, observed_at, report_type). A later-arriving row with the same key is discarded, not merged. No update path exists for an observation once it lands.

This is the opposite of standard data-engineering hygiene. In most pipelines, data quality means “catch errors and fix them.” Here, data quality means “replay the record exactly as it was the first time anyone saw it.”

We do not overwrite, but we do not throw away the correction either. When AWC publishes a COR or RTD METAR (coded correction), we ingest it as a new row with a different observation_type and, if one is given, a different observed_at. If the correction carries the same observed_at as the original, it lands in an audit table, not the primary observation table.

A public audit-query surface is on the roadmap. For now, corrections are stored but not exposed through the SDK. Settlement always uses the original observation_type="METAR" row.

Because rows are immutable once stored, as_of queries are stable forever:

# Query A, run today:
a = client.observations("NYC", as_of="2026-04-02T12:00:00Z")
# Query A, run six months from today:
a_later = client.observations("NYC", as_of="2026-04-02T12:00:00Z")
# These return the same rows. Bit-for-bit.
assert a == a_later

You can cache indefinitely. You can version your model against a fixed data_version token and know that re-running with that token months later produces the same inputs. See data_version for the exact guarantee.

05 · Where the rule applies, and where it does not

Section titled “05 · Where the rule applies, and where it does not”
  • Observations. Covered, without exception. Even if we find a bug in our parser six months from now, the rows already stored do not get rewritten. We fix the parser forward and leave the archive alone.
  • Forecasts. Not covered in the same way. A forecast is a prediction about a future window. The same forecast model issues new runs every few hours. Each run is a new row keyed by issued_at. We keep every issuance, so replay of “what did the NBM think at 06Z?” works the same way.
  • Climate aggregates. Not covered. CLIMATE rollups are derivations, not observations. They can be re-derived on schema changes without breaking the settlement story, because the underlying observation rows they derived from are still immutable.

The single rule, applied to the single place where it matters, gives you the one property you actually need: a backtest you can trust against a market that actually cleared.