# The track-gate structure

A gate drawn on the map, over an archive of whole storms. This page is the
third parametric shape Craton prices: a `parametric_index` trigger that
names a pinned **best-track storm archive** instead of a daily series, with
the gate written in `peril.region` and the ladder written in the archive's own
unit. By the end you will have a complete Gulf-of-Mexico gate object, its
175-season history, and a technical price for it, all from one block.

Read [the quickstart](quickstart.md) first if you have not: it explains
`CEDE_BASE_URL`, the `Authorization: Bearer` header and the job pattern every
Craton verb answers with. [The trigger object](trigger.md) is the field-by-field
reference for the block this page writes; [a technical price, from
nothing](price.md) reads the price response field by field. This page is about
one shape: what a track gate is, the smallest object that prices as one, and
what a best-track burn does and does not measure.

---

## What a track-gate structure is

The station-measured named-storm structures on [the pinned data
feeds](feeds.md) attach on wind at a point: one grid cell's daily maximum,
whether or not a storm caused it. A track-gate structure asks a different
question of different bytes: **did a storm's recorded track cross this region,
and how strong was it while inside?** The archive it reads is a best-track
catalog (every storm as a named sequence of position-and-intensity fixes),
so the structure can be written about a *region* rather than a point, and its
level is the storm's maximum sustained wind, not a cell average.

Three declarations route a structure to this model, and each is re-checked
against the pinned bytes before any of them is read:

| Member | What a track gate declares | Why |
| --- | --- | --- |
| `trigger.type` | `parametric_index` | It is an index structure: a measurement, a window, a ladder. |
| `trigger.data_sources[0].kind` | `storm_track_set` | This is the member that chooses the track model over the station one. The kind decides which reader parses the publisher's bytes, so it is checked against the feed's own manifest before a byte is read. |
| `peril.region.bounding_geometry` | the gate | A single-ring, axis-aligned GeoJSON `Polygon`, the same rectangle rule as the earthquake box, read by the same parser. Every edge is inclusive: a fix exactly on an edge is inside the gate. |

The index itself is four decisions:

* **The measurement** is `track_maximum_wind_kt` in `kt`, with `statistic:
  "max"`, named exactly as the archive publishes it ([what each pinned feed
  publishes](feeds.md#the-measurement-variable-each-feed-publishes) is the
  table). The model computes one statistic: the season's highest qualifying
  wind inside the gate.
* **The window is the season**, written `P1Y`. A track-gate structure pays at
  most once per season of the archive; a finer window would need an occurrence
  rule this model does not have, so any other duration is refused by name.
* **The ladder is a `step` function in kt**, and the `thresholds` array must
  carry a threshold labeled `attachment` at the ladder's lowest level. For
  this model that label is required, not documentation, because the attachment
  is the one level every reader of the structure reasons from.
* **What qualifies**: a fix counts only while the system is at tropical status
  (`HU`, `TS`, `TD`; the archive keeps the extratropical and wave stages too,
  distinguishable by status) and only when the publisher estimated a wind for
  it. A missing wind is a gap, never a zero.

For each season of the archive's period of record, the highest qualifying
in-gate wind sets the season's index level, the step function turns that level
into the season's payout ratio, and the burn rate is the mean ratio over
**every** season, the seasons that measured nothing included, at zero,
because dropping them would shorten the denominator and flatter the structure.
The model is `cede/parametric-track-gate`, and it names itself on every
result.

The archive behind the block below is `hurdat2-atlantic` at version
`2026-09-02`: the Atlantic best-track database, 1851 to 2025, 175 seasons,
public domain. [The pinned data feeds](feeds.md) has its coverage and license,
and `GET /feeds` is the same list from the deployment you are pointed at.

Five archives are pinned, and a gate names the one that covers it:

| Archive | Covers | Seasons |
| --- | --- | --- |
| `hurdat2-atlantic` | The North Atlantic, the Gulf of Mexico and the Caribbean (NHC) | 1851 to 2025 |
| `hurdat2-pacific` | The Northeast and North Central Pacific, including Hawaii and Mexico's Pacific coast (NHC and CPHC) | 1949 to 2025 |
| `ibtracs-west-pacific` | Japan, Korea, China's east and south coasts, Taiwan, the Philippines, Vietnam and Guam (JTWC) | 1980 to 2024 |
| `ibtracs-north-indian` | The Bay of Bengal and the Arabian Sea: India, Bangladesh, Myanmar, Sri Lanka, Pakistan and Oman (JTWC) | 1980 to 2024 |
| `ibtracs-southern-hemisphere` | Madagascar, Mozambique, Mauritius and Reunion, Australia, New Caledonia, Vanuatu, Fiji, Tonga and Samoa (JTWC) | 1981 to 2024 |

The three `ibtracs-*` archives are the US Joint Typhoon Warning Center's best
tracks as NOAA's IBTrACS carries them, rewritten in the HURDAT2 layout, so
every archive here reads the same way and every wind is the same measurement:
the 1-minute maximum sustained wind, not the 10-minute wind the region's own
meteorological agency publishes (which reads lower). A southern-hemisphere
season runs from July to June and is named by the year it ends in. They stop
at 2024 because IBTrACS still carries 2025 as provisional.

---

## The Gulf gate, end to end

One object: a gate over the Gulf of Mexico (24.5N to 32N, 98W to 79W), attaching at
64 kt (the wind at which the archive's own status ladder calls a system a
hurricane) and exhausting at 113 kt. The block signs up for an account of its
own, stores the object, cuts a structure from it, replays the structure over
every season of the archive, and prices the same terms.

Run this in an empty directory.

<!-- cede:runnable -->

```bash
set -euo pipefail

: "${CEDE_BASE_URL:?export CEDE_BASE_URL first, see the quickstart}"

# Read one field out of a JSON document on stdin: `field a.b.0.c`.
field() {
  python3 -c 'import json, sys
document = json.load(sys.stdin)
for step in sys.argv[1].split("."):
    document = document[int(step)] if step.isdigit() else document[step]
print(document)' "$1"
}

# Poll a job until it stops moving, leaving its last document in a file.
wait_for() {
  local job_id="$1" into="$2" state=unknown
  for _ in $(seq 1 300); do
    curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/jobs/$job_id" > "$into"
    state=$(field status < "$into")
    case "$state" in succeeded|failed) break ;; esac
    sleep 0.2
  done
  if [ "$state" != succeeded ]; then
    printf 'the job did not succeed: %s\n' "$(cat "$into")" >&2
    return 1
  fi
}

# 1. An account of this block's own. No credential needed to ask for one.
CEDE_API_KEY=$(curl -sS --fail-with-body \
  -H "Content-Type: application/json" \
  -d '{"label": "track gate walkthrough"}' \
  "$CEDE_BASE_URL/signup" | field api_key.secret)
export CEDE_API_KEY
auth="Authorization: Bearer $CEDE_API_KEY"
printf 'your key (shown once): %s\n' "$CEDE_API_KEY"

# 2. The Gulf-gate object, whole. The gate is the peril region; the ladder
#     is written in the archive's own unit, kt, and its rungs sit on the
#     archive's intensity steps. The attachment appears three times (the
#     financial structure, the attachment threshold, the lowest rung), and
#     the model refuses the object unless all three agree.
cat > gulf-gate.json <<'JSON'
{
  "schema_version": "0.1.0",
  "status": "draft",
  "exposure": {
    "kind": "index",
    "source_fidelity": { "unmapped_columns": [], "guessed_units": [], "ambiguous_rows": [] }
  },
  "peril": {
    "code": "tropical_cyclone",
    "region": {
      "description": "Gulf gate: 24.5-32N, 98-79W.",
      "bounding_geometry": {
        "type": "Polygon",
        "coordinates": [[[-98.0, 24.5], [-79.0, 24.5], [-79.0, 32.0],
                         [-98.0, 32.0], [-98.0, 24.5]]]
      }
    }
  },
  "financial_structure": {
    "limit": { "amount": 10000000, "currency": "USD" },
    "attachment": { "value": 64, "unit": "kt", "index_ref": "gulf-track-gate" }
  },
  "trigger": {
    "type": "parametric_index",
    "index": {
      "name": "gulf-track-gate",
      "version": "1.0.0",
      "description": "Highest best-track maximum sustained wind at tropical status inside the Gulf gate, per season.",
      "measurement": { "variable": "track_maximum_wind_kt", "unit": "kt", "statistic": "max" },
      "aggregation_window": { "duration": "P1Y" },
      "thresholds": [
        { "label": "attachment", "level": 64, "unit": "kt" },
        { "label": "exhaustion", "level": 113, "unit": "kt" }
      ],
      "payout_function": {
        "type": "step",
        "points": [ { "level": 64, "payout_ratio": 0.25 },
                    { "level": 83, "payout_ratio": 0.5 },
                    { "level": 96, "payout_ratio": 0.75 },
                    { "level": 113, "payout_ratio": 1 } ],
        "maximum_payout_ratio": 1
      }
    },
    "data_sources": [
      { "id": "hurdat2-atlantic",
        "name": "HURDAT2 Atlantic hurricane database, best track",
        "kind": "storm_track_set",
        "version": "2026-09-02", "vintage": "2026-09-02" }
    ]
  },
  "period": {
    "inception": "2026-06-01T00:00:00-05:00",
    "expiry": "2027-06-01T00:00:00-05:00",
    "timezone": "America/Chicago"
  }
}
JSON

object_id=$(curl -sS --fail-with-body -H "$auth" \
  -H "Content-Type: application/json" \
  --data-binary @gulf-gate.json "$CEDE_BASE_URL/objects" | field id)
echo "object: $object_id"

# 3. Cut the structure a verb evaluates. No body: the object's own terms.
structure=$(curl -sS --fail-with-body -X POST -H "$auth" \
  "$CEDE_BASE_URL/objects/$object_id/structures")
structure_id=$(printf '%s' "$structure" | field id)
backtest_path=$(printf '%s' "$structure" | field links.backtest)
echo "structure: $structure_id"

# 4. Replay it: every season of the pinned archive, through the same reader
#     Price is about to use.
backtest_job=$(curl -sS --fail-with-body -X POST -H "$auth" \
  "$CEDE_BASE_URL$backtest_path" | field id)
wait_for "$backtest_job" backtest-job.json
artifact_path=$(field result.backtest.links.self < backtest-job.json)
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL$artifact_path" > backtest.json

python3 -c '
import json
artifact = json.load(open("backtest.json"))
window, statistics = artifact["window"], artifact["statistics"]
print("window:", window["from_year"], "to", window["to_year"],
      "-", statistics["years"], "seasons")
print("paying seasons:", statistics["triggering_years"],
      "of", statistics["years"])
print("largest season:", statistics["largest_annual_payout"]["amount"],
      statistics["largest_annual_payout"]["currency"])
print("burning cost:", statistics["burning_cost"]["annual_payout"]["amount"],
      statistics["burning_cost"]["annual_payout"]["currency"], "a season",
      "- burn rate", statistics["burning_cost"]["burn_rate"])
print("replayed by", artifact["model"]["name"], artifact["model"]["version"])
print(artifact["disclosure"])
'

# 5. Price the same terms. The burn under this number is the burn the
#     replay just showed season by season, read through the same code.
price_job=$(curl -sS --fail-with-body -X POST -H "$auth" \
  "$CEDE_BASE_URL/objects/$object_id/price" | field id)
wait_for "$price_job" price-job.json

python3 -c '
import json
price = json.load(open("price-job.json"))["result"]["price"]
premium, assumptions = price["technical_price"], price["assumptions"]
burn, selection = assumptions["burn"], assumptions["event_selection"]
gate = selection["gate"]
print("technical price:", premium["amount"], premium["currency"],
      "- rate on line", premium["rate_on_line"])
print("priced by", price["model"]["name"], price["model"]["version"],
      "-", burn["triggering_years"], "of", burn["years"],
      "seasons of the record would have paid")
print("gate: latitudes", gate["min_latitude"], "to", gate["max_latitude"],
      "- longitudes", gate["min_longitude"], "to", gate["max_longitude"])
print("storms through the gate:", selection["storms_through_gate"],
      "- fixes inside it:", selection["fixes_in_gate"])
print("seasons that measured nothing:", len(selection["zero_seasons"]),
      "of", burn["years"], "- still in the denominator")
print("limitations stated on the result:", len(assumptions["limitations"]))
print(premium["disclosure"])
'
echo "done: the priced object is at /objects/$object_id"
```

If the last line printed, you have a season-by-season history and a technical
price for the same gate, and the number of paying seasons printed by each is
the same number. Backtest and Price read a track structure through one
reader, so there is no second burn to reconcile.

---

## What came back: the track selection

The price and the replay both carry the model's working in the shape of the
thing it measured: storms and fixes, not days and not point events. On the
price it is `assumptions.event_selection`; every count a reader needs to redo
the arithmetic by hand from the pinned snapshot is there:

```
gate                  the rectangle as evaluated, with the edge rule stated:
                      inclusive on every edge
measurement           variable, unit, statistic, and the pinned snapshot's own
                      feed kind (the thing that decided which reader parsed
                      the publisher's bytes)
status_basis          which fixes counted, in words: tropical status only, and
                      a fix whose wind the publisher never estimated is a gap,
                      never a zero
storms_in_archive     every storm the snapshot holds, and fixes_in_archive
                      every fix: the whole archive, so the gate's share of it
                      can be seen against the total
storms_through_gate   storms with at least one qualifying fix inside the gate,
fixes_in_gate         and the qualifying fixes themselves, summed and per
                      season
measured_seasons      seasons with at least one qualifying in-gate fix
zero_seasons          every season that measured nothing, listed by year, and
                      still in the burn at zero, never dropped
seasons               one row per season of the record: the season maximum in
                      kt, the payout ratio it read off the ladder, the counts
                      behind it, and (for a measured season) the storm that
                      set the maximum, by the publisher's own id and name, and
                      the fix that set it, to the timestamp
```

The `seasons` rows are the full set, not a sample: the burn's denominator is
their count, and each paying season names the storm you can look up in the
publisher's own database. The replay artifact carries the same selection
beside its year-by-year rows.

---

## What a best-track burn is not

Everything below ships in `assumptions.limitations` on every result, so a
reader holding only the response is told. It is repeated here because the
honest reading of this archive is the difference between a track gate and a
number that merely looks like one.

**Best track is a considered estimate, not a live measurement.** Each fix is
the publisher's post-season analysis of position and intensity, smoothed and
revised as reanalysis proceeds, and the wind on it is the storm's maximum
sustained wind *anywhere in its circulation*, not a wind measured at any
insured location. A 96 kt season maximum means the archive judged some part of
some storm reached 96 kt somewhere inside the gate; it does not mean 96 kt
crossed any particular site. A structure written on this archive inherits that
basis, and a Saffir-Simpson category written as a rung is shorthand for the
archive's wind field, nothing else.

**The early record undercounts.** Before aircraft and satellite
reconnaissance, a storm that crossed this gate at sea may simply not be in the
archive, and one that is may carry no wind estimate. Zeros in the
nineteenth-century seasons are therefore part missing observation, not all
calm, and a burn over the whole 175-season record understates the frequency
by whatever was never recorded. The zero seasons are listed on every result so
you can see where they sit.

**A gate is a geometric test on six-hourly fixes.** Best track publishes fixes
at (mostly) six-hour intervals; a storm can cross a small gate between two
fixes and appear in this history as if it never entered. The payout is decided
by the recorded fixes, not by the storm's continuous path, and the model does
not quantify that gap. The smaller the gate, the larger it is: a gate the
size of a harbor measures almost nothing.

**One payout per season.** The season pays on its single highest in-gate fix.
A second hurricane through the same gate in the same season adds nothing to
the burn, and reinstatements are not priced.

**It is one vintage of a revised record.** A later vintage can move a fix's
wind across a threshold (or add a storm) and turn a zero season into a
paying one. The vintage this result read is named in `data_sources`, with the
sha256 of the exact bytes.

And the standing rule both other models share: a ladder that sits entirely
above everything the archive ever recorded inside the gate is **refused, not
priced at zero**: the refusal names the highest season maximum in the record
and the storm that set it, so the fix is a number and not a search. A history
with no payouts in it is a description, and the replay will show it to you as
one.

---

## When it does not work

Every one of these arrives as a `failed` job carrying `422
unpriceable_object` (or `422 unbacktestable_structure` from the replay), with
the message shown. [The error reference](errors.md) is the full list of
refusal shapes.

| The message says | What to change |
| --- | --- |
| `trigger.data_sources[0] declares kind …; the track-gate model reads storm_track_set sources` | The kind is what routes a structure to this model. Write `"kind": "storm_track_set"` (over a track archive only). |
| `snapshot … is a … feed; the track-gate model reads a storm_track_catalogue` | The declared kind and the pinned feed disagree: the id names a feed that is not a track archive. Name one that is. |
| `a … archive publishes track_maximum_wind_kt; this index measures …` | The variable is not one the archive publishes. Copy it from [the feeds table](feeds.md#the-measurement-variable-each-feed-publishes), exactly. |
| `index.measurement declares track_maximum_wind_kt in …, but a … archive publishes it in kt` | The unit is the archive's, and a disagreement is refused rather than converted. |
| `index.measurement.statistic is …; the track-gate model computes one statistic (max` | The season maximum is the only transform this model has. |
| `aggregation_window.duration is …; a track-gate index pays at most once per season …, so the window is the season, written P1Y` | Write `"duration": "P1Y"`, and leave `alignment` out (or `calendar_day`). |
| `this index needs a threshold labelled 'attachment'` | For this model the attachment threshold is required, not documentation. Add it at the ladder's lowest level. |
| `threshold … is stated in …; every level of a track-gate ladder is a maximum sustained wind in kt` | A level in another unit would move the attachment by a weather class if converted silently, so it is refused instead. |
| `a track-gate structure is priced from peril.region.bounding_geometry (the gate its storms must cross), which this object does not carry` | Add the gate: a single-ring, axis-aligned, closed polygon. |
| `financial_structure.attachment is … but the payout function first pays at …` | The attachment appears three times on a track structure and all three must agree. |
| `no year of the pinned record would have paid` | The ladder sits above every season maximum the gate ever measured. The message names the highest one and the storm that set it; attach at or below it, or replay the structure to see the same history with no payout in it. |

---

## Where to go next

* [The trigger object](trigger.md): every member of the trigger block, field
  by field, and the smallest trigger of each shape that prices (this shape
  included).
* [The pinned data feeds](feeds.md): the track archive beside every other
  pinned snapshot: coverage, license, period of record, and the measurement
  variable to copy.
* [A technical price, from nothing](price.md): the price response field by
  field: the loadings, the build-up, and how to check the arithmetic with a
  pocket calculator.
* [Backtest a structure](backtest.md): the replay pattern this page used,
  walked slowly on the other two shapes.

---

*The example on this page is extracted verbatim and executed against a live
environment on every change to Craton. If the gate stopped measuring or the
season arithmetic moved, the build would stop too.*
