Building a Model Inventory for SR 11-7
A model inventory for SR 11-7 is the single authoritative register of every model a firm relies on, and this page builds one as a typed Python record: a Pydantic schema capturing owner, purpose, risk tier, last-validation date, and upstream and downstream dependencies, plus a risk-tiering function that derives the tier from materiality and complexity. It is the foundational artefact of the SR 11-7 Model Risk Governance framework and part of the wider Regulatory Architecture & Compliance Mapping discipline.
The Problem
SR 11-7 requires firms to maintain a comprehensive inventory of models in use, under development, or recently retired, carrying enough information for management to gauge aggregate model risk — but most inventories live in a spreadsheet that is complete on audit day and stale by the following week. A spreadsheet cannot refuse a record with a missing owner, cannot compute a risk tier consistently across a hundred models, and cannot notice that a model’s last validation has aged past its cadence. The fix is to make the inventory record a validated data structure, so that an entry is either well-formed and tierable and dependency-linked, or it is rejected at the point of registration. Encoding the record with Validating Actuarial Input Schemas with Pydantic turns the inventory from a document into an enforceable contract.
A Minimal Working Example
The example below defines an inventory record, derives its risk tier from materiality and complexity scores, and computes when the model is next due for validation. It uses Pydantic v2 and runs as-is:
from __future__ import annotations
from datetime import date, timedelta
from enum import IntEnum
from pydantic import BaseModel, Field, field_validator, model_validator
class RiskTier(IntEnum):
TIER_3 = 3 # low materiality / low complexity
TIER_2 = 2 # moderate
TIER_1 = 1 # high materiality / high complexity
# Maximum interval between full validations, by tier (SR 11-7 proportionality).
REVALIDATION_DAYS: dict[RiskTier, int] = {
RiskTier.TIER_1: 365,
RiskTier.TIER_2: 730,
RiskTier.TIER_3: 1095,
}
def assign_risk_tier(materiality: float, complexity: float) -> RiskTier:
"""Map a materiality/complexity pair to an SR 11-7 risk tier.
materiality and complexity are each scored 0.0-1.0. The tier is driven by
the higher of the two so a small but intricate model is not under-rated.
"""
score = max(materiality, complexity)
if score >= 0.66:
return RiskTier.TIER_1
if score >= 0.33:
return RiskTier.TIER_2
return RiskTier.TIER_3
class ModelInventoryRecord(BaseModel):
model_id: str = Field(pattern=r"^[A-Z0-9_]{4,40}$")
name: str
owner: str = Field(min_length=1)
purpose: str = Field(min_length=10) # intended use, documented
materiality: float = Field(ge=0.0, le=1.0)
complexity: float = Field(ge=0.0, le=1.0)
tier: RiskTier
last_validation_date: date
upstream_dependencies: list[str] = Field(default_factory=list)
downstream_dependencies: list[str] = Field(default_factory=list)
@field_validator("last_validation_date")
@classmethod
def not_in_future(cls, v: date) -> date:
if v > date.today():
raise ValueError("last_validation_date cannot be in the future")
return v
@model_validator(mode="after")
def tier_matches_scores(self) -> "ModelInventoryRecord":
expected = assign_risk_tier(self.materiality, self.complexity)
if self.tier != expected:
raise ValueError(
f"declared tier {self.tier.name} contradicts scores "
f"(expected {expected.name}) — possible tier inflation"
)
return self
@property
def next_validation_due(self) -> date:
return self.last_validation_date + timedelta(days=REVALIDATION_DAYS[self.tier])
@property
def is_overdue(self) -> bool:
return date.today() > self.next_validation_due
if __name__ == "__main__":
record = ModelInventoryRecord(
model_id="RESERVE_PBR_V4",
name="Principle-Based Reserve Engine",
owner="valuation.actuary@carrier.example",
purpose="Computes VM-20 minimum reserve for term and universal life.",
materiality=0.9,
complexity=0.8,
tier=RiskTier.TIER_1,
last_validation_date=date(2025, 9, 30),
upstream_dependencies=["MORTALITY_TBL_2025", "ECON_SCENARIO_GEN"],
downstream_dependencies=["CAPITAL_MODEL_RBC"],
)
print(record.model_id, record.tier.name, "overdue:", record.is_overdue)
How It Works, Block by Block
RiskTier as an IntEnum makes severity orderable. Tier 1 is the highest risk, and encoding it as the integer 1 lets code sort and compare tiers naturally — a scan for “every model at or above Tier 2” is a simple numeric filter. The three tiers are the standard SR 11-7 proportionality bands: the most demanding validation for the models that can do the most damage, a lighter touch for the rest.
assign_risk_tier drives the tier off the higher of two scores. Materiality (how much money or how large a decision rides on the model) and complexity (how intricate and error-prone it is) are scored independently on a 0-to-1 scale, and the tier follows the maximum of the two. That choice is deliberate: a model that is small in dollar terms but mathematically involved — a bespoke stochastic optimiser feeding a modest line — is exactly the kind of thing a materiality-only rating under-rates, and the max guards against it.
The ModelInventoryRecord schema fails closed. Every field carries a constraint the register must satisfy: model_id matches a fixed identifier pattern, owner cannot be blank, and purpose must be a real sentence rather than a placeholder, because “documented intended use” is a specific SR 11-7 requirement and an empty purpose is the most common way an inventory becomes useless. A record that violates any constraint raises a ValidationError at construction and never reaches the register.
The model_validator rejects tier inflation. After field validation, tier_matches_scores recomputes the expected tier from the scores and refuses the record if the declared tier disagrees. This closes the oldest loophole in model governance — an owner declaring a high-risk model as Tier 3 to dodge annual revalidation. The tier a record claims must be the tier its own scores imply.
next_validation_due and is_overdue turn the cadence into a queryable property. Rather than tracking review dates by hand, the record derives its own deadline from its tier and reports whether that deadline has passed. Sweeping the whole inventory for overdue models becomes a one-line comprehension over is_overdue.
Edge Cases and Production Hardening
Orphaned dependencies break consumers silently. A record lists upstream_dependencies by model_id, but nothing yet guarantees those identifiers resolve to live records. When an upstream model is retired or renamed, its consumers keep pointing at a model_id that no longer exists, and the dependency graph is quietly broken. Validate referential integrity across the whole inventory:
def find_orphaned_dependencies(
inventory: dict[str, ModelInventoryRecord],
) -> dict[str, list[str]]:
"""Return, per model, any upstream dependency id absent from the register."""
known = set(inventory)
orphans: dict[str, list[str]] = {}
for model_id, record in inventory.items():
missing = [dep for dep in record.upstream_dependencies if dep not in known]
if missing:
orphans[model_id] = missing
return orphans
Run this after every registration and before any model executes; a non-empty result is a governance gap that should block reliance until the dependency is restored or the reference removed.
Overdue validation must be detected, not remembered. An inventory that depends on someone remembering to check review dates will drift out of compliance the first quarter attention lapses. Because is_overdue is intrinsic to the record, a scheduled sweep surfaces the backlog automatically:
def overdue_models(
inventory: dict[str, ModelInventoryRecord],
) -> list[tuple[str, RiskTier, date]]:
"""List overdue models worst-tier-first for remediation triage."""
overdue = [
(rec.model_id, rec.tier, rec.next_validation_due)
for rec in inventory.values()
if rec.is_overdue
]
return sorted(overdue, key=lambda row: (row[1], row[2]))
Sorting by tier then due date puts the highest-risk, longest-overdue models at the top of the remediation queue.
Tier inflation returns through the back door. The schema blocks a mis-declared tier at registration, but scores themselves can be gamed — an owner nudges a materiality score just under 0.66 to slip a genuinely high-risk model into Tier 2. Defend against it by having the validation function, not the model owner, own the scoring, and by periodically re-scoring high-consumer models: any model that many others depend on downstream is materially important almost by definition, regardless of the score it was given.
Compliance Note
This inventory is the concrete expression of the governance, policies, and controls element of SR 11-7, which directs firms to maintain a comprehensive set of information for models implemented for use, under development, or recently retired. The owner, purpose, tier, and validation-date fields are precisely the attributes the guidance expects an inventory to carry, and the tier-driven cadence operationalises its call for validation intensity proportionate to model risk. The same record structure maps cleanly onto the Canadian regime: the risk-rating and independent-review expectations captured here satisfy the equivalent controls a life insurer works through in the Step-by-Step OSFI Model Validation Checklist for Life Insurers, so a single inventory can serve both supervisors.
Frequently Asked Questions
What fields must an SR 11-7 model inventory record contain?
At a minimum a stable model identifier, the model’s name, its owner, its documented intended purpose, a risk tier, the date of its last validation, and its upstream and downstream dependencies. SR 11-7 wants enough information for management to understand aggregate model risk, so owner, purpose, and tier are non-negotiable — an entry missing any of them cannot support tiering, scheduling, or dependency analysis.
How should a model’s risk tier be assigned?
From materiality and complexity together, driven by the higher of the two so that a small but intricate model is not under-rated. Materiality captures how consequential the model’s output is; complexity captures how error-prone the model is. The validation function, not the model’s owner, should set the scores, and any model many others depend on should be treated as material regardless of its raw score.
How do you detect overdue model validations automatically?
Derive each record’s next-validation deadline from its tier’s revalidation cadence and expose a property that reports whether today has passed that deadline. A scheduled sweep over the inventory then lists every overdue model without anyone having to track dates manually, and sorting the result by tier and due date triages remediation worst-case first.
What is an orphaned dependency and why does it matter?
An orphaned dependency is an upstream model identifier a record points to that no longer resolves to a live inventory entry, usually because that model was retired or renamed. It matters because the consuming model then runs on stale or missing inputs while appearing healthy. Checking referential integrity across the whole inventory after every change catches the break before the model is relied upon.
Related
- SR 11-7 Model Risk Governance — the framework this inventory is the foundation of.
- Validating Actuarial Input Schemas with Pydantic — the schema-enforcement technique the record is built on.
- Step-by-Step OSFI Model Validation Checklist for Life Insurers — the Canadian controls this inventory also satisfies.
- Regulatory Architecture & Compliance Mapping — the wider discipline this record sits within.
Up a level: SR 11-7 Model Risk Governance · Regulatory Architecture & Compliance Mapping
Regulatory references: Board of Governors of the Federal Reserve System SR 11-7 (Guidance on Model Risk Management), section on model inventory and governance.