{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:cede:schema:price-response:v0",
  "title": "CEDE price response, v0",
  "description": "The result of the Price verb (SPEC.md section 3.3): expected loss, loading components, technical premium, and the COMPLETE assumption set as first-class response fields, not footnotes. SPEC.md section 3.3 and section 4 (P3) are normative for the semantics; this file encodes them as constraints, and where the two disagree SPEC.md wins and this file has a bug. Two constraints carry the perimeter directly: technical_price.basis is the constant 'technical' and technical_price.disclosure is a fixed sentence, so no response can present the output as a price at which anyone stands ready to transact, and no revision of this schema can quietly reword the label.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "run_id",
    "verb",
    "model",
    "subject",
    "expected_loss",
    "technical_price",
    "assumptions"
  ],
  "properties": {
    "schema_version": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
      "description": "Version of this response schema the payload conforms to."
    },
    "run_id": {
      "type": "string",
      "minLength": 1,
      "description": "Identity of this price run. Content-addressed in v0: the same object priced against the same snapshot by the same model version yields the same id, because SPEC section 2A requires a result to replay byte for byte."
    },
    "verb": {
      "const": "price",
      "description": "Which of the seven verbs (SPEC section 3) produced this."
    },
    "model": {
      "$ref": "#/$defs/model_label"
    },
    "subject": {
      "$ref": "#/$defs/subject"
    },
    "expected_loss": {
      "$ref": "#/$defs/money",
      "description": "Modelled annual expected loss to the structure, scaled to the cover period. The unloaded number, stated separately from the technical price so the loadings are visible as loadings."
    },
    "technical_price": {
      "$ref": "#/$defs/technical_price"
    },
    "assumptions": {
      "$ref": "#/$defs/assumptions"
    }
  },
  "$defs": {
    "money": {
      "type": "object",
      "additionalProperties": false,
      "required": ["amount", "currency"],
      "description": "A currency-tagged amount, as on the risk object (risk-object.v0 money).",
      "properties": {
        "amount": { "type": "number", "minimum": 0 },
        "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }
      }
    },
    "model_label": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "version"],
      "description": "SPEC section 4 P9: every result is labelled with the model and version that produced it. Both are required here so the label cannot be omitted.",
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "version": { "type": "string", "minLength": 1 }
      }
    },
    "subject": {
      "type": "object",
      "additionalProperties": false,
      "required": ["schema_version", "peril", "trigger_type", "index"],
      "description": "What was priced. object_id is absent for an object that has not been persisted (the risk object's own id is server-assigned).",
      "properties": {
        "object_id": { "type": "string", "minLength": 1 },
        "schema_version": {
          "type": "string",
          "minLength": 1,
          "description": "schema_version of the risk object that was priced."
        },
        "peril": { "type": "string", "minLength": 1 },
        "trigger_type": { "type": "string", "minLength": 1 },
        "index": { "$ref": "#/$defs/index_label" }
      }
    },
    "index_label": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "version"],
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "version": { "type": "string", "minLength": 1 }
      }
    },
    "technical_price": {
      "type": "object",
      "additionalProperties": false,
      "required": ["basis", "amount", "currency", "rate_on_line", "assumptions_ref", "disclosure"],
      "description": "The technical premium. Shaped so it drops straight into risk-object.v0 financial_structure.premium, which requires the same basis constant and the same mandatory assumptions_ref.",
      "properties": {
        "basis": {
          "const": "technical",
          "description": "SPEC section 4 P3, in the schema itself: every price output is labelled technical. A constant, so it cannot be omitted or varied."
        },
        "amount": { "type": "number", "minimum": 0 },
        "currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
        "rate_on_line": {
          "type": "number",
          "minimum": 0,
          "description": "Technical premium divided by the structure's limit."
        },
        "assumptions_ref": {
          "type": "string",
          "minLength": 1,
          "description": "Pointer to the assumption set that produced this amount. In v0 the assumptions travel in the same payload and this is the run_id."
        },
        "disclosure": {
          "const": "Technical price: an actuarial estimate produced by analytics software from the assumptions stated in this response. It is not a price at which any party stands ready to transact, and it carries no capacity, no acceptance and no validity period.",
          "description": "SPEC section 4 P3 and section 3.3, pinned as a constant. Fixing the exact sentence is deliberate: a free-text field could be reworded into an implication of transactability one careless edit at a time, and a constant cannot."
        }
      }
    },
    "assumptions": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "data_sources",
        "period_of_record",
        "method",
        "event_selection",
        "frequency",
        "burn",
        "loadings",
        "price_build_up",
        "cover_period",
        "currency_basis",
        "limitations"
      ],
      "description": "The complete assumption set SPEC section 3.3 requires as first-class fields: model versions, data vintages, event set, loadings, currency basis. Every entry is named; nothing is a footnote and nothing is implied.",
      "properties": {
        "data_sources": {
          "type": "array",
          "minItems": 1,
          "description": "Every pinned snapshot read, with the vintage and the sha256 of the exact bytes.",
          "items": { "$ref": "#/$defs/data_source" }
        },
        "period_of_record": { "$ref": "#/$defs/period_of_record" },
        "method": { "$ref": "#/$defs/method" },
        "event_selection": {
          "description": "What the model measured, in the shape of the thing it measured. A model reading an event catalogue exposes the event set (event_selection); a model reading a station-measured daily series exposes the measurement, the transform and the window it measured in each year (index_selection). One branch or the other, never a blend: a rainfall window described in magnitude fields would be a document that reads as an earthquake, which is the failure ADR-0028 exists to prevent.",
          "oneOf": [
            { "$ref": "#/$defs/event_selection" },
            { "$ref": "#/$defs/index_selection" }
          ]
        },
        "frequency": {
          "type": "array",
          "minItems": 1,
          "description": "Empirical occurrence frequency at each band of the payout function.",
          "items": { "$ref": "#/$defs/frequency_band" }
        },
        "burn": { "$ref": "#/$defs/burn" },
        "loadings": {
          "type": "array",
          "description": "Each loading itemised: what it is called, what it is applied to, at what rate, on what base, for how much, and why. An empty array means an unloaded technical price, which is a statement, not an omission.",
          "items": { "$ref": "#/$defs/loading" }
        },
        "price_build_up": { "$ref": "#/$defs/price_build_up" },
        "cover_period": { "$ref": "#/$defs/cover_period" },
        "currency_basis": { "$ref": "#/$defs/currency_basis" },
        "limitations": {
          "type": "array",
          "minItems": 1,
          "description": "What this price does not account for, in plain sentences. Required non-empty: no model priced from forty years of catalogue data has nothing to disclose, and a silent limitation is the failure mode SPEC section 3.3 exists to prevent.",
          "items": { "type": "string", "minLength": 1 }
        }
      }
    },
    "data_source": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "name", "publisher", "kind", "version", "vintage", "sha256", "licence"],
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "name": { "type": "string", "minLength": 1 },
        "publisher": { "type": "string", "minLength": 1 },
        "kind": {
          "type": "string",
          "enum": [
            "quake_catalogue",
            "storm_track_set",
            "station_network",
            "footprint_provider",
            "outage_feed",
            "other"
          ]
        },
        "version": { "type": "string", "minLength": 1 },
        "vintage": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
        "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
        "query_url": { "type": "string", "minLength": 1 },
        "record_count": { "type": "integer", "minimum": 0 },
        "licence": { "type": "string", "minLength": 1 }
      }
    },
    "period_of_record": {
      "type": "object",
      "additionalProperties": false,
      "required": ["start", "end", "years"],
      "description": "The historical window the burn was computed over — the answer to 'over what history?', which SPEC section 3.3 requires stated rather than assumed.",
      "properties": {
        "start": { "type": "string", "minLength": 1 },
        "end": { "type": "string", "minLength": 1 },
        "years": { "type": "number", "exclusiveMinimum": 0 }
      }
    },
    "method": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "family", "description", "occurrence_basis", "clustering", "reinstatements_considered", "ratio_precision"],
      "description": "The burn/frequency method, named and described. family says which of the two families this is, so a reader never has to infer it from the name.",
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "family": {
          "type": "string",
          "enum": ["burn", "frequency_severity", "hybrid"]
        },
        "description": { "type": "string", "minLength": 1 },
        "occurrence_basis": { "type": "string", "minLength": 1 },
        "clustering": {
          "type": "object",
          "additionalProperties": false,
          "required": ["rule", "window"],
          "properties": {
            "rule": { "type": "string", "minLength": 1 },
            "window": { "type": "string", "minLength": 1 }
          }
        },
        "reinstatements_considered": {
          "type": "boolean",
          "description": "False in v0. Stated rather than silent, because a structure carrying reinstatements is priced here as if it did not."
        },
        "ratio_precision": {
          "type": "string",
          "minLength": 1,
          "description": "The quantum every ratio in this response (burn rate, standard deviation, frequencies, payout ratios, period factor) is rounded to before it is reported or used, half away from zero. Stated because determinism is a product property here (SPEC section 2A): the same inputs must produce the same bytes on every machine, and an unrounded division is where that guarantee leaks."
        }
      }
    },
    "event_selection": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "box",
        "minimum_magnitude",
        "magnitude_basis",
        "events_in_snapshot",
        "events_in_box",
        "qualifying_occurrences",
        "occurrences"
      ],
      "description": "The event set: which events the index saw and which of them triggered. Every count is exposed so the arithmetic can be re-done by hand from the snapshot.",
      "properties": {
        "box": {
          "type": "object",
          "additionalProperties": false,
          "required": ["min_latitude", "max_latitude", "min_longitude", "max_longitude", "bounds"],
          "properties": {
            "min_latitude": { "type": "number", "minimum": -90, "maximum": 90 },
            "max_latitude": { "type": "number", "minimum": -90, "maximum": 90 },
            "min_longitude": { "type": "number", "minimum": -180, "maximum": 180 },
            "max_longitude": { "type": "number", "minimum": -180, "maximum": 180 },
            "bounds": {
              "type": "string",
              "minLength": 1,
              "description": "Whether an event exactly on an edge is inside. Stated because it decides events."
            }
          }
        },
        "max_focal_depth_km": { "type": ["number", "null"] },
        "minimum_magnitude": { "type": "number" },
        "magnitude_basis": { "type": "string", "minLength": 1 },
        "events_in_snapshot": { "type": "integer", "minimum": 0 },
        "events_in_box": { "type": "integer", "minimum": 0 },
        "qualifying_occurrences": { "type": "integer", "minimum": 0 },
        "occurrences": {
          "type": "array",
          "description": "Every occurrence that would have paid, listed. Not a sample: the full set behind the burn.",
          "items": { "$ref": "#/$defs/occurrence" }
        }
      }
    },
    "index_selection": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "measurement",
        "transform",
        "index_unit",
        "attachment_level",
        "days_in_snapshot",
        "days_reported",
        "days_missing",
        "measured_years",
        "unmeasured_years",
        "windows"
      ],
      "description": "The measurement set of an index model: which days the index saw, what it did with them, and what each year measured. The counterpart of event_selection for a station-measured index, and exposed to the same standard — every count is here so the burn can be re-computed by hand from the pinned snapshot.",
      "properties": {
        "measurement": {
          "type": "object",
          "additionalProperties": false,
          "required": ["variable", "unit", "statistic", "feed_kind"],
          "description": "What was measured, and off which kind of feed. feed_kind is the pinned snapshot's own file kind — the thing that decides which reader parses the publisher's bytes — and is stated separately from the data-source kind so the two alphabets cannot be confused.",
          "properties": {
            "variable": { "type": "string", "minLength": 1 },
            "unit": { "type": "string", "minLength": 1 },
            "statistic": { "type": "string", "minLength": 1 },
            "feed_kind": { "type": "string", "minLength": 1 }
          }
        },
        "transform": {
          "type": "object",
          "additionalProperties": false,
          "required": ["type", "window_days", "alignment"],
          "description": "How daily values became one index level per year.",
          "properties": {
            "type": { "type": "string", "minLength": 1 },
            "window_days": { "type": "integer", "minimum": 1 },
            "alignment": { "type": "string", "minLength": 1 }
          }
        },
        "index_unit": {
          "type": "string",
          "minLength": 1,
          "description": "The unit of the index level, which is the unit the payout function's levels and the attachment are read in. Not always the measurement's unit: a day-count transform measures a rainfall series in days."
        },
        "attachment_level": {
          "type": "number",
          "description": "The index level at which the structure first pays, in index_unit."
        },
        "days_in_snapshot": { "type": "integer", "minimum": 0 },
        "days_reported": {
          "type": "integer",
          "minimum": 0,
          "description": "Days inside the period of record the publisher actually reported. Compared against days_missing rather than assumed equal to the calendar."
        },
        "days_missing": {
          "type": "integer",
          "minimum": 0,
          "description": "Days the publisher did not report. A gap is disclosed, never interpolated (SPEC.md section 3.5)."
        },
        "measured_years": { "type": "integer", "minimum": 0 },
        "unmeasured_years": {
          "type": "array",
          "description": "Years with no complete window to measure. They still count as zero-payout years in the burn, which is what the burn's denominator says they are.",
          "items": { "type": "integer" }
        },
        "windows": {
          "type": "array",
          "minItems": 1,
          "description": "Every year's measurement, the zero years included: not a sample, the full set behind the burn.",
          "items": { "$ref": "#/$defs/index_window" }
        }
      }
    },
    "index_window": {
      "type": "object",
      "additionalProperties": false,
      "required": ["year", "unit", "payout_ratio", "days_reported", "days_in_year"],
      "description": "One year as an index model measured it. level and the measured dates are absent from a year the feed could not be measured in, and unmeasured_reason says why in that case.",
      "properties": {
        "year": { "type": "integer" },
        "level": { "type": "number" },
        "unit": { "type": "string", "minLength": 1 },
        "payout_ratio": { "type": "number", "minimum": 0, "maximum": 1 },
        "measured_from": { "type": "string", "minLength": 1 },
        "measured_to": { "type": "string", "minLength": 1 },
        "days_reported": { "type": "integer", "minimum": 0 },
        "days_in_year": { "type": "integer", "minimum": 1 },
        "unmeasured_reason": { "type": "string", "minLength": 1 }
      }
    },
    "occurrence": {
      "type": "object",
      "additionalProperties": false,
      "required": ["time", "magnitude", "latitude", "longitude", "payout_ratio"],
      "properties": {
        "event_id": { "type": "string", "minLength": 1 },
        "time": { "type": "string", "minLength": 1 },
        "magnitude": { "type": "number" },
        "magnitude_type": { "type": "string", "minLength": 1 },
        "depth_km": { "type": ["number", "null"] },
        "latitude": { "type": "number" },
        "longitude": { "type": "number" },
        "payout_ratio": { "type": "number", "minimum": 0, "maximum": 1 },
        "clustered_event_count": { "type": "integer", "minimum": 1 }
      }
    },
    "frequency_band": {
      "type": "object",
      "additionalProperties": false,
      "required": ["level", "unit", "payout_ratio", "occurrences", "annual_frequency", "return_period_years"],
      "properties": {
        "label": { "type": "string", "minLength": 1 },
        "level": { "type": "number" },
        "unit": { "type": "string", "minLength": 1 },
        "payout_ratio": { "type": "number", "minimum": 0, "maximum": 1 },
        "occurrences": { "type": "integer", "minimum": 0 },
        "annual_frequency": { "type": "number", "minimum": 0 },
        "return_period_years": {
          "type": ["number", "null"],
          "description": "Null where the band has no occurrence in the period of record — an unobserved band has no empirical return period, and printing a large number there would invent one."
        }
      }
    },
    "burn": {
      "type": "object",
      "additionalProperties": false,
      "required": ["burn_rate", "standard_deviation", "years", "triggering_years", "annual_payout_ratios"],
      "description": "The burning-cost calculation, with its year-by-year working shown.",
      "properties": {
        "burn_rate": {
          "type": "number",
          "minimum": 0,
          "description": "Mean annual payout ratio over the period of record: the expected loss as a fraction of the limit, before any period scaling."
        },
        "standard_deviation": {
          "type": "number",
          "minimum": 0,
          "description": "Population standard deviation of the annual payout ratios. The volatility loading reads this."
        },
        "years": { "type": "integer", "exclusiveMinimum": 0 },
        "triggering_years": { "type": "integer", "minimum": 0 },
        "annual_payout_ratios": {
          "type": "array",
          "minItems": 1,
          "description": "One entry per year of the period of record, including the zero years. The zeros are the evidence: a burn rate computed from the loss years alone is a different and wrong number.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["year", "payout_ratio"],
            "properties": {
              "year": { "type": "integer" },
              "payout_ratio": { "type": "number", "minimum": 0, "maximum": 1 }
            }
          }
        }
      }
    },
    "loading": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "name",
        "basis",
        "rate",
        "applies_to",
        "base",
        "amount",
        "arithmetic",
        "rationale"
      ],
      "description": "One loading, with the whole of its arithmetic: the rate, the money the rate multiplies, the terms that money is made of, and the resulting amount. rate x base.amount is amount exactly — the base is published as it was used, rounded to the currency's minor unit, so a reader checking the itemisation is checking the numbers the price was computed from rather than re-deriving them (CEDE-213).",
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "basis": { "type": "string", "minLength": 1 },
        "rate": { "type": "number", "minimum": 0 },
        "applies_to": {
          "type": "string",
          "minLength": 1,
          "description": "The quantity the rate multiplies, named. The same quantity as a number is `base`; this field is its name, and a name alone is not enough to re-compute anything, which is why both are present."
        },
        "base": { "$ref": "#/$defs/derived_amount" },
        "amount": { "$ref": "#/$defs/money" },
        "arithmetic": {
          "type": "string",
          "minLength": 1,
          "description": "The one multiplication this loading is, written out in the digits this response prints: `rate x base = amount`. Prose for a reader; every number in it is also a field."
        },
        "rationale": { "type": "string", "minLength": 1 }
      }
    },
    "derived_amount": {
      "type": "object",
      "additionalProperties": false,
      "required": ["amount", "currency", "operation", "terms", "arithmetic"],
      "description": "An amount of money together with the named terms it was computed from. `operation` says how the terms combine: `product` multiplies them, `sum` adds them. The terms are the values as this response publishes them elsewhere, so the combination reproduces `amount` exactly.",
      "properties": {
        "amount": { "type": "number" },
        "currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
        "operation": { "type": "string", "enum": ["product", "sum"] },
        "terms": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["name", "value"],
            "properties": {
              "name": { "type": "string", "minLength": 1 },
              "value": { "type": "number" },
              "currency": {
                "type": "string",
                "pattern": "^[A-Z]{3}$",
                "description": "Present when the term is money. A rate, a ratio or a period factor is a pure number and carries none."
              }
            }
          }
        },
        "arithmetic": {
          "type": "string",
          "minLength": 1,
          "description": "The combination written out in the digits this response prints, each term followed by its name."
        }
      }
    },
    "price_build_up": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "currency",
        "expected_loss",
        "steps",
        "technical_price",
        "statement"
      ],
      "description": "The path from the expected loss to the technical price, as a running total (CEDE-213). Everything here is derivable from `expected_loss`, `loadings` and `technical_price`, and that is the point: an itemisation a reader has to reconstruct is an itemisation two readers reconstruct differently. The first step is the expected loss and each later step adds one loading, in the order the loadings are applied, so the last running total is the technical price.",
      "properties": {
        "currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
        "expected_loss": {
          "type": "object",
          "additionalProperties": false,
          "required": ["amount", "base"],
          "description": "The unloaded number and the three terms it is the product of: the burn rate, the limit and the cover period factor. Each of those is published in its own right elsewhere in this assumption set; here they are together, in one multiplication.",
          "properties": {
            "amount": { "$ref": "#/$defs/money" },
            "base": { "$ref": "#/$defs/derived_amount" }
          }
        },
        "steps": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["component", "amount", "running_total"],
            "properties": {
              "component": { "type": "string", "minLength": 1 },
              "amount": { "$ref": "#/$defs/money" },
              "running_total": { "$ref": "#/$defs/money" }
            }
          }
        },
        "technical_price": { "$ref": "#/$defs/money" },
        "statement": {
          "type": "string",
          "minLength": 1,
          "description": "The reconciliation in one sentence, so a reader who takes nothing else from this object takes the rule."
        }
      }
    },
    "cover_period": {
      "type": "object",
      "additionalProperties": false,
      "required": ["inception", "expiry", "days", "annualisation_basis", "factor"],
      "description": "The period being priced and how the annual burn was scaled to it.",
      "properties": {
        "inception": { "type": "string", "minLength": 1 },
        "expiry": { "type": "string", "minLength": 1 },
        "timezone": { "type": "string", "minLength": 1 },
        "days": { "type": "number", "exclusiveMinimum": 0 },
        "annualisation_basis": { "type": "string", "minLength": 1 },
        "factor": { "type": "number", "exclusiveMinimum": 0 }
      }
    },
    "currency_basis": {
      "type": "object",
      "additionalProperties": false,
      "required": ["currency", "minor_units", "rounding", "fx_conversion_applied"],
      "properties": {
        "currency": { "type": "string", "pattern": "^[A-Z]{3}$" },
        "minor_units": { "type": "integer", "minimum": 0 },
        "rounding": { "type": "string", "minLength": 1 },
        "fx_conversion_applied": {
          "type": "boolean",
          "description": "False in v0: every amount in this response is denominated in the structure's limit currency and no exchange rate enters the arithmetic. A price that had converted a currency would have to say so here and name the rate it used."
        }
      }
    }
  }
}
