Skip to content
CRATON Docs Get a free API key

Data

The storm-report box

Hail, thunderstorm wind and tornadoes, priced off the reports people made of them. This page is the fourth parametric shape Craton prices: a parametric_cat_in_a_box trigger that names a pinned storm-report extract instead of an earthquake catalog, with the box written in peril.region and the ladder written in the report's own unit. By the end you will have a Dallas-Fort Worth hail object, its thirty-year 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.


On this page

What a storm-report box is

NOAA's Storm Events Database records the severe weather the National Weather Service hears about: a hailstone measured against a coin, a tree down with a gust estimated from it, a tornado rated after a damage survey. Craton pins extracts of it, one feed per event type and metro box, 1996 to 2025, and a report-box structure asks: what was the largest report of this kind inside this box, year by year?

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 report box declares Why
trigger.type parametric_cat_in_a_box A box and a ladder, like the earthquake shape.
trigger.data_sources[0].kind storm_report_set This is the member that chooses the report model over the earthquake one. It is checked against the feed's own manifest before a byte is read.
peril.code severe_convective_storm The canonical peril for hail, thunderstorm wind and tornado.

The index itself is four decisions:

  • The measurement is the one the feed publishes, named exactly: reported_hail_size_in in in on a hail feed, reported_wind_speed_kt in kt on a wind feed, reported_tornado_rating_ef in EF (the 0 to 5 rating) on a tornado feed, with statistic: "max". A variable the feed does not publish is refused by name.
  • The window is the year, written P1Y. A report box pays at most once a year; any other duration is refused.
  • The ladder is a step function in the feed's unit, and the thresholds array must carry a threshold labeled attachment at its lowest level.
  • The box must sit inside the extract. Each feed's coverage names its box; a structure box reaching outside it is refused with the edge it crossed, because it would miss reports that really were made.

For each year, the largest report inside the box that carries a value sets the year's level, the ladder turns it into the year's payout ratio, and the burn rate is the mean ratio over every year, the years with no report included at zero. The model is cede/parametric-report-box, and it names itself on every result.


Dallas-Fort Worth hail, end to end

One object: a box over the Dallas-Fort Worth metro (32.5N to 33.2N, 97.5W to 96.5W), attaching at 2.75 in (a baseball) for a quarter of the limit and paying all of it at 4.0 in (a softball). The block signs up for an account of its own, stores the object, replays it over every year of the extract, and prices the same terms.

Run this in an empty directory.

Shell Runs in CI
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.
CEDE_API_KEY=$(curl -sS --fail-with-body \
  -H "Content-Type: application/json" \
  -d '{"label": "storm report walkthrough"}' \
  "$CEDE_BASE_URL/signup" | field api_key.secret)
export CEDE_API_KEY
auth="Authorization: Bearer $CEDE_API_KEY"

# 2. The hail-box object. The box is the peril region; the ladder is in
#     inches, the unit the hail feed publishes.
cat > dfw-hail.json <<'JSON'
{
  "schema_version": "0.1.0",
  "status": "draft",
  "exposure": {
    "kind": "index",
    "source_fidelity": { "unmapped_columns": [], "guessed_units": [], "ambiguous_rows": [] }
  },
  "peril": {
    "code": "severe_convective_storm",
    "region": {
      "description": "Dallas-Fort Worth hail box: 32.5-33.2N, 97.5-96.5W.",
      "bounding_geometry": {
        "type": "Polygon",
        "coordinates": [[[-97.5, 32.5], [-96.5, 32.5], [-96.5, 33.2],
                         [-97.5, 33.2], [-97.5, 32.5]]]
      }
    }
  },
  "financial_structure": {
    "limit": { "amount": 10000000, "currency": "USD" },
    "attachment": { "value": 2.75, "unit": "in", "index_ref": "dfw-hail-box" }
  },
  "trigger": {
    "type": "parametric_cat_in_a_box",
    "index": {
      "name": "dfw-hail-box",
      "version": "1.0.0",
      "description": "Largest reported hail size inside the Dallas-Fort Worth box, per year.",
      "measurement": { "variable": "reported_hail_size_in", "unit": "in", "statistic": "max" },
      "aggregation_window": { "duration": "P1Y" },
      "thresholds": [
        { "label": "attachment", "level": 2.75, "unit": "in" },
        { "label": "exhaustion", "level": 4.0, "unit": "in" }
      ],
      "payout_function": {
        "type": "step",
        "points": [ { "level": 2.75, "payout_ratio": 0.25 },
                    { "level": 4.0, "payout_ratio": 1 } ],
        "maximum_payout_ratio": 1
      }
    },
    "data_sources": [
      { "id": "ncei-hail-dallas",
        "name": "NOAA Storm Events hail reports, Dallas-Fort Worth extract",
        "kind": "storm_report_set",
        "version": "2026-09-26", "vintage": "2026-09-26" }
    ]
  },
  "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 @dfw-hail.json "$CEDE_BASE_URL/objects" | field id)
structure=$(curl -sS --fail-with-body -X POST -H "$auth" \
  "$CEDE_BASE_URL/objects/$object_id/structures")
backtest_path=$(printf '%s' "$structure" | field links.backtest)

# 3. Replay it over every year of the extract.
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"))
statistics = artifact["statistics"]
print("paying years:", statistics["triggering_years"], "of", statistics["years"])
for row in artifact["years"]:
    if row["triggered"]:
        peak = row["peak_report"]
        print(" ", row["year"], row["max_value"], row["index_unit"],
              "report", peak["event_id"], peak["time"], "->", row["payout_ratio"])
print("replayed by", artifact["model"]["name"], artifact["model"]["version"])
'

# 4. Price the same terms.
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"]
selection = assumptions["event_selection"]
print("technical price:", premium["amount"], premium["currency"],
      "- rate on line", premium["rate_on_line"])
print("reports inside the box:", selection["reports_in_box"],
      "of", selection["reports_in_extract"], "in the extract")
print("priced by", price["model"]["name"], price["model"]["version"])
'
echo "done: the priced object is at /objects/$object_id"

If the last line printed, every paying year above names the report that set it, by the publisher's own event id, which you can look up in NOAA's database.

Expect the rate on line to be high. Hail of 2.75 inches or more was reported inside this box in 21 of the 30 years, and 4 inches or more in 13 of them, so a structure that attaches at a baseball pays most years and is priced that way. A smaller box, a higher attachment, or a flatter ladder prices lower, and the backtest above is the quickest way to see by how much before you price.


The pinned extracts

Eighteen feeds: hail, thunderstorm wind and tornado for six metro boxes, each three degrees on a side and wider than any box you would price inside it.

Metro Box Feeds
Dallas-Fort Worth 31.3 to 34.3N, 98.5 to 95.5W ncei-hail-dallas, ncei-wind-dallas, ncei-tornado-dallas
Oklahoma City 34.4 to 37.4N, 99.0 to 96.0W ncei-hail-oklahoma-city, ncei-wind-oklahoma-city, ncei-tornado-oklahoma-city
Denver and the Front Range 38.2 to 41.2N, 106.2 to 103.2W ncei-hail-denver, ncei-wind-denver, ncei-tornado-denver
Minneapolis-St. Paul 43.5 to 46.5N, 95.0 to 92.0W ncei-hail-minneapolis, ncei-wind-minneapolis, ncei-tornado-minneapolis
Chicago 40.4 to 43.4N, 89.4 to 86.4W ncei-hail-chicago, ncei-wind-chicago, ncei-tornado-chicago
Atlanta 32.3 to 35.3N, 86.0 to 83.0W ncei-hail-atlanta, ncei-wind-atlanta, ncei-tornado-atlanta

All are version 2026-09-26, 1996 to 2025, public domain. The pinned data feeds lists each one's report counts and license. The record starts in 1996 because that is when the database began recording every event type to one standard.

A three-degree box is too wide to price as it stands: in every one of them, hail of two inches or more was reported in almost every year. Price a metro inside it, as the block above does.


What a report-based burn is not

Everything below ships in assumptions.limitations on every result.

Reports are observations, not a measured field. A report exists where someone saw the hail, found the damage or read an instrument and told the National Weather Service. A storm that crossed the box where nobody was looking left no report, and reporting practice varies by forecast office and over time: each feed's coverage states how its yearly report count moved between the late 1990s and the 2020s: Chicago's hail reports doubled, and Atlanta's fell to about a quarter. A structure reads each year's largest report, which is steadier than a count, but it inherits this basis.

Sizes, speeds and ratings are estimates. Hail size is usually judged against a familiar object (a quarter, a golf ball, a baseball), so sizes cluster on those values. Most thunderstorm wind speeds are estimated from damage; a minority are measured. A tornado's rating comes from a damage survey, so one over open country rates low whatever its winds, the scale changed from F to Enhanced Fujita in 2007, and an unrated tornado is never read. Each tornado is placed at its touchdown point.

A report is a point. The box is a test on where reports were made, not on where hail fell or wind blew. The smaller the box, the larger that basis risk.

A year pays once. The largest report of the year is the one the year pays on; a second storm adds nothing, and reinstatements are not priced.

This page as Markdown, byte for byte: /storm-reports.md