Structuring the VM-20 Actuarial Memorandum

The VM-20 actuarial memorandum — formally the PBR Actuarial Report demanded by VM-31 — is the narrative that turns a pile of reserve numbers into a defensible filing, and it fails inspection far more often for a missing section than for a wrong figure. This page shows how to assemble that report as a structured object from model metadata, enforcing the sections VM-31 names before anyone starts writing, and it sits inside the wider NAIC VM-20 Compliance Frameworks discipline while drawing on the same lineage produced when you map actuarial models to NAIC VM-20 requirements.

The Problem

VM-31 does not accept a memorandum as a free-form essay; it enumerates the components a PBR Actuarial Report must contain, and an examiner reads that report as a checklist against the Valuation Manual. The report has to open with a reserve summary that states the Net Premium Reserve, Deterministic Reserve, and Stochastic Reserve and the minimum reserve they resolve to, then document the assumptions and their margins, describe the model and its cash-flow engine, and close with the appointed actuary’s certifications. When a company drafts this by hand in a word processor, the failure mode is structural: a section is skipped, an assumption is quoted without its margin, or a certification is signed against a model version that has since changed. The remedy is to treat the memorandum as a typed data structure built from the model’s own metadata, so a required section that has no content is a caught error rather than a gap an examiner finds first.

A Minimal Working Example

The pattern is a schema of required sections, a builder that populates each section from model metadata, and a completeness check that refuses to emit a memorandum with an empty required field. It has no framework dependencies and runs as-is:

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field, asdict
from datetime import date, datetime, timezone

# VM-31 names the components a PBR Actuarial Report must contain. Encoding
# them as an ordered contract makes "did we include that section?" executable.
REQUIRED_SECTIONS: tuple[str, ...] = (
    "reserve_summary",
    "assumptions_and_margins",
    "model_description",
    "cash_flow_model",
    "certifications",
)


@dataclass
class MemoSection:
    """One VM-31 report section and the metadata that traces it."""

    key: str
    heading: str
    body: str
    source_refs: list[str] = field(default_factory=list)

    def is_populated(self) -> bool:
        return bool(self.body.strip()) and bool(self.source_refs)


@dataclass
class ActuarialMemorandum:
    valuation_date: date
    appointed_actuary: str
    assumption_set_version: str
    model_version: str
    sections: dict[str, MemoSection] = field(default_factory=dict)

    def missing_sections(self) -> list[str]:
        gaps = []
        for key in REQUIRED_SECTIONS:
            section = self.sections.get(key)
            if section is None or not section.is_populated():
                gaps.append(key)
        return gaps

    def seal(self) -> str:
        payload = json.dumps(
            {
                "valuation_date": self.valuation_date.isoformat(),
                "model_version": self.model_version,
                "assumption_set_version": self.assumption_set_version,
                "sections": {k: asdict(v) for k, v in self.sections.items()},
            },
            sort_keys=True,
        )
        return hashlib.sha256(payload.encode()).hexdigest()


def build_memorandum(model_metadata: dict) -> ActuarialMemorandum:
    memo = ActuarialMemorandum(
        valuation_date=model_metadata["valuation_date"],
        appointed_actuary=model_metadata["appointed_actuary"],
        assumption_set_version=model_metadata["assumption_set_version"],
        model_version=model_metadata["model_version"],
    )
    reserves = model_metadata["reserves"]
    memo.sections["reserve_summary"] = MemoSection(
        key="reserve_summary",
        heading="Reserve Summary",
        body=(
            f"Net Premium Reserve {reserves['net_premium_reserve']:,.2f}; "
            f"Deterministic Reserve {reserves['deterministic_reserve']:,.2f}; "
            f"Stochastic Reserve {reserves['stochastic_reserve']:,.2f}; "
            f"minimum reserve {reserves['minimum_reserve']:,.2f}."
        ),
        source_refs=["VM-20 Section 2"],
    )
    memo.sections["assumptions_and_margins"] = MemoSection(
        key="assumptions_and_margins",
        heading="Assumptions and Margins",
        body="; ".join(
            f"{a['name']}: anticipated {a['anticipated']}, margin {a['margin']}"
            for a in model_metadata["assumptions"]
        ),
        source_refs=[a["name"] for a in model_metadata["assumptions"]],
    )
    memo.sections["model_description"] = MemoSection(
        key="model_description",
        heading="Model Description",
        body=model_metadata.get("model_description", ""),
        source_refs=[memo.model_version] if model_metadata.get("model_description") else [],
    )
    memo.sections["cash_flow_model"] = MemoSection(
        key="cash_flow_model",
        heading="Cash-Flow Model",
        body=model_metadata.get("cash_flow_description", ""),
        source_refs=["VM-20 Section 7"] if model_metadata.get("cash_flow_description") else [],
    )
    memo.sections["certifications"] = MemoSection(
        key="certifications",
        heading="Certifications",
        body=model_metadata.get("certification_text", ""),
        source_refs=[memo.appointed_actuary] if model_metadata.get("certification_text") else [],
    )
    return memo


if __name__ == "__main__":
    metadata = {
        "valuation_date": date(2024, 12, 31),
        "appointed_actuary": "J. Okafor, FSA, MAAA",
        "assumption_set_version": "2024Q4.3",
        "model_version": "proj-engine-7.2.1",
        "reserves": {
            "net_premium_reserve": 1_000_000.00,
            "deterministic_reserve": 1_050_000.00,
            "stochastic_reserve": 1_100_000.00,
            "minimum_reserve": 1_100_000.00,
        },
        "assumptions": [
            {"name": "mortality_table", "anticipated": "2017 CSO", "margin": "+4%"},
            {"name": "lapse_rate", "anticipated": "6.0%", "margin": "-1.0%"},
        ],
        "model_description": "Seriatim first-principles projection of a term block.",
        "cash_flow_description": "Monthly premiums, benefits, and expenses to run-off.",
        "certification_text": "I certify these reserves meet VM-20 as of the valuation date.",
    }
    memo = build_memorandum(metadata)
    gaps = memo.missing_sections()
    print("missing:", gaps)
    if not gaps:
        print("sealed:", memo.seal()[:16])
Memorandum assembly: model metadata to a sealed PBR Actuarial Report through a completeness gate Model metadata flows into a section builder that emits the five required VM-31 sections, then a completeness gate blocks any run with an unpopulated required section, and a sealed PBR Actuarial Report leaves only when the gate passes. Model metadata reserves + assumptions Section builder 5 VM-31 sections Summary, model, cash flow, certs typed sections All required populated? Sealed PBR Actuarial Report SHA-256 sealed
Model metadata is assembled into the five VM-31 sections, gated on completeness, and sealed before filing.

How It Works, Block by Block

REQUIRED_SECTIONS encodes VM-31 as an ordered contract. VM-31 does not leave the report’s shape to preference: it requires the reserve summary, the assumptions and their margins, the model description, the cash-flow model, and the certifications. Declaring those keys as a tuple — in the order an examiner reads them — makes “is that section present?” a set operation rather than a proof-reading pass. When VM-31 is amended, the contract changes in one place and every memorandum built afterward inherits the new requirement.

MemoSection.is_populated refuses to count an empty shell. A heading with no body, or a body with no source reference, is exactly the kind of hollow section that passes a casual skim and fails an examination. Requiring both a non-empty body and at least one source_refs entry means a section only counts as complete when it actually cites the assumption, clause, or model version behind it — so traceability is a precondition of completeness, not an afterthought.

build_memorandum populates each section from metadata, never from prose typed twice. The reserve summary is formatted directly from the reserve figures, the assumptions section is built from the assumption records with their margins attached, and the certification text is bound to the named appointed actuary. Because every section is derived from the model’s own metadata, the memorandum cannot silently disagree with the model it describes — the numbers in the summary are the numbers the engine produced.

missing_sections is the pre-signature gate. It walks the required contract in order and returns every key whose section is absent or unpopulated. Wire this into the assembly step and a memorandum missing its certification, or quoting an assumption with no margin, is stopped before it is signed. Only once the gate returns an empty list does seal hash the whole structure, binding the report to the exact model version and assumption set that produced it — the same reconstruction inputs the clause-tagging map carries on each reserve figure.

Edge Cases and Production Hardening

A missing certification passes structure but fails the filing. The most common gap is a memorandum whose certification section is present as a heading but whose certification_text is empty because the appointed actuary has not yet signed off. Because is_populated demands both a body and a source reference, the empty certification is caught, but you can make the failure more precise by asserting the certification carries a named signer and a signing date rather than boilerplate:

def assert_certification(memo: ActuarialMemorandum) -> None:
    cert = memo.sections.get("certifications")
    if cert is None or not cert.is_populated():
        raise ValueError("VM-31 certification section is missing or unsigned")
    if memo.appointed_actuary not in cert.source_refs:
        raise ValueError("certification is not attributed to the appointed actuary")

Assumption-to-section traceability breaks when an assumption is quoted without its margin. VM-20 reserves are computed on prudent estimates — an anticipated experience assumption plus an explicit margin — and a memorandum that lists the anticipated lapse_rate but drops the margin misrepresents the reserve basis. Building the assumptions section from records that pair anticipated with margin forces both to travel together, and the source_refs list gives each assumption a name an examiner can trace back to the assumption governance that set it. The margins themselves are governed upstream by Assumption Validation & Rule Engine Design, and the memorandum should quote the version those rules stamped.

Versioning the memo is not optional when the model moves. A memorandum sealed against proj-engine-7.2.1 becomes a liability the moment the engine is patched, because the report now attests to cash flows the current model no longer produces. Store the model_version and assumption_set_version inside the sealed payload — as the example does — so any later re-run either reproduces the same seal or announces, loudly, that the report and the model have diverged. Persist each sealed memorandum immutably and stage it alongside the other artifacts when assembling examiner-ready filing packages, so the version that was signed is the version an examiner receives.

Compliance Note

The structure enforced here is VM-31 made mechanical: the PBR Actuarial Report requirement obliges the appointed actuary to document the reserve components, the assumptions and margins, the model and its cash-flow projection, and the certifications, and to make each traceable to its source. The source_refs on every section and the sealed model_version are the concrete expression of that traceability. Layered on top is ASOP No. 41 (Actuarial Communications), which governs any report an actuary issues: it requires that the actuary identify themselves, state the reliance and any deviations, and ensure the communication is clear enough that another qualified actuary could assess the work. The certification section, attributed to a named appointed actuary and sealed against a specific model version, is where ASOP No. 41’s identification and clarity duties meet VM-31’s content requirements. For a cross-border carrier, the same section-driven discipline can feed the OSFI Model Risk Management Guidelines reporting expectations without maintaining a second, divergent narrative.

Frequently Asked Questions

What sections does VM-31 require in the actuarial memorandum?

VM-31 requires the PBR Actuarial Report to document the reserve summary, the assumptions and their margins, the model description, the cash-flow model, and the appointed actuary’s certifications. Encoding these as an ordered required-section contract lets a builder verify every one is present and populated before the report is sealed.

How do you enforce that no required section is left empty?

Give each section a completeness test that demands both a non-empty body and at least one source reference, then walk the required-section contract and collect any key that is absent or unpopulated. The memorandum is only sealed when that list of gaps is empty, so a hollow heading is caught as an error rather than found by an examiner.

Why does the memorandum need to record the model version?

Because a report sealed against one model version silently misrepresents the reserves once the engine is patched. Storing the model version and assumption set version inside the sealed payload means any later re-run either reproduces the same seal or flags that the report and the model have diverged, which is exactly the reproducibility VM-31 expects.

Which standard governs the actuarial communication itself?

ASOP No. 41 (Actuarial Communications) governs the report as a communication: it requires the actuary to identify themselves, disclose reliance and any deviations, and write clearly enough that another qualified actuary could assess the work. It sits alongside VM-31, which specifies the content the report must contain.

Up a level: NAIC VM-20 Compliance Frameworks · Regulatory Architecture & Compliance Mapping


Regulatory references: NAIC Valuation Manual VM-31 (PBR Actuarial Report); Actuarial Standards Board ASOP No. 41 (Actuarial Communications).