# 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](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.

---

## 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.

<!-- 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.
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](feeds.md) 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.
