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 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 is the field-by-field reference for the block this page writes; a technical price, from nothing 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.
On this page
What a track-gate structure is
The station-measured named-storm structures on the pinned data feeds 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_ktinkt, withstatistic: "max", named exactly as the archive publishes it (what each pinned 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
stepfunction in kt, and thethresholdsarray must carry a threshold labeledattachmentat 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 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.
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 timestampThe 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 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, 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: 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: the track archive beside every other pinned snapshot: coverage, license, period of record, and the measurement variable to copy.
- A technical price, from nothing: the price response field by field: the loadings, the build-up, and how to check the arithmetic with a pocket calculator.
- Backtest a structure: 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.