Implementation Notes#
Status: Draft, 2026-08-29. Built from
technical-notes.md; the product it implements is specified in
product-spec.md.
This is a mechanics demonstration, not a pricing or reserving result. The mechanics are the established German ones and each carries the instrument it must be checked against — the Berufsunfähigkeit definition, its 50 % degree and its Sechs-Monats-Fiktion [S1] [S12] R1, the Anerkenntnis and Nachprüfung frame with its three-month run-off R2 R3, the Beitragsbefreiung as core cover rather than an option [S1] [S2], the Brutto / Zahlbeitrag pair and the Beitragsverrechnung behind it R10 R14 [S6] [S12], the unisex rule REG-R34, and the absence of any death, maturity or surrender cash flow [S1] R8 R9. The six months is the Fiktion’s period, not the Prognosezeitraum’s: the latter is set by each carrier and the GDV model conditions leave it blank. Every level is a standardization. The DAV 1997 family and DAV 2008 T are the property of the Deutsche Aktuarvereinigung, are not public and are not redistributed here R16 R17 REG-R50 REG-R48; no Produktinformationsblatt was obtained, so no carrier’s BU charge structure is on the record here — the disclosure itself exists, VVG-InfoV § 2 requiring it in euro, and it is only the Effektivkosten figure that a pure risk contract does not carry R12 [S14]; and no German BU rate card of any kind was obtained, so the Bruttobeitrag is an output of a stated first-order basis rather than a table lookup. Replace the decrement, charge and premium bases with company data before drawing any conclusion from the numbers. On provenance: delib was drafted under a policy that blocked all egress, on the authoring model’s own knowledge disciplined by std and
[unverified]tags, and its citations have since been re-verified against the primary documents;sources.mdrecords per entry what was opened and what was not, 26 of this product’s 43 entries carryingRetrieved: yesand 17 stillRetrieved: no.
Run it#
python products/berufsunfaehigkeit/run.py
python products/berufsunfaehigkeit/run.py 4 # the Beitragsdynamik variant
python products/berufsunfaehigkeit/run.py 7 # an in-force policy already in claim
import modelx as mx
model = mx.read_model("products/berufsunfaehigkeit/BU_DE_S")
model.Projection[1].result_cf()
Projection takes a point_id; Projection[1] is the worked-example anchor cell.
result_cf() returns a tidy DataFrame indexed by policy month t carrying the three
state ledgers, the premium-paying count and one column per cash flow line, and
result_states() publishes the transitions, rates and per-policy amounts beside it.
model.Projection.doc holds the full mapping from the notes’ symbols to the cells names, and
model.Data.doc says what a replacement for each biometric table must preserve.
The grid is monthly and t is 0-based: t = 0 is the first projected month — the
month of inception for a new-business point, the valuation month for an in-force one — and
proj_len() is the number of projected months, the exclusive end of the frame, so
the frame is range(proj_len()): t = 0 … proj_len() − 1, and result_cf().index[-1] == proj_len() − 1. On the anchor cell proj_len() = 12 × (67 − 30) = 444, i.e. 444 rows
(t = 0 … 443) in about a second. The contractual, 1-based policy year is derived, not
indexed by: policy_year(t) = duration_mth(t) // 12 + 1.
Time-like CSV columns. No input file is keyed by the projection month t, so no CSV
value moved with this frame change. lapse_table.csv is keyed by policy_year 1–5 with row
6 the ultimate — a contractual 1-based label read through policy_year(t), so the file is
left alone. claim_duration_table.csv is keyed by dur_year 1–10 with row 11 the ultimate,
the claim year since onset, on the 1-based claim-duration clock z and independent of
t; unchanged. inception_table.csv and mortality_table.csv are keyed by attained age,
reached through age(t); unchanged. In model_point_table.csv, duration_init_months and
claim_duration_init are elapsed counts, already 0-based by nature, and neither is a
point on the frame’s axis — the frame always opens at t = 0 and carries the elapsed
duration as an offset inside duration_mth(t) — so both are unchanged.
Four ledgers, one return arc, and § 174 in arithmetic#
This is what a reader arriving from RLV_DE_S or BasicTerm_S will get wrong, and it is why
the product is worth modelling. A BU contract is a multi-state model with a return arc,
not a decrement model:
aktiv --inception--> leistungspflichtig --Nachprüfung--> run-off (3 months) --> aktiv
| | |
death, lapse death death
pols_actv, pols_dis and pols_runoff are the three ledgers and pols_if is their total.
Death and lapse are the only exits, so pols_if(t+1) = pols_if(t) − pols_death(t) − pols_lapse(t), and inception, recovery and reactivation are internal transfers that must
not appear in that identity — putting them there is how a multi-state model silently loses
mass, invisibly in the cash flows.
The run-off ledger is § 174 VVG in arithmetic R3 REG-R29: where the insurer establishes that
its liability has ceased it remains obliged to pay to the end of the third month after the
notice reaches the policyholder, so a recovery does not stop the annuity in the month it
happens. pols_recovery(t) feeds run-off slot 1, the slots roll at active-lives mortality —
these lives have recovered and are no longer impaired — and only the slot-3 survivors rejoin
pols_actv as pols_reactivation(t). On the anchor cell the tail is 206,41 € of the 13 151,35 €
of BU-Rente, 1,6 % of all benefit: small in aggregate, structural in kind. A model
returning a recovery straight to the active ledger loses three monthly Renten per recovery and
fails check_runoff_roll_fwd() immediately.
The run-off carries amounts as well as counts: runoff_val(t, k) is the slot population
times the BU-Rente it is still being paid, because the cohorts terminating in one month came
in on different Renten and have crossed different numbers of onset anniversaries. A cohort
keeps the BU-Rente it was on at the Nachprüfung date and receives no further
Leistungsdynamik std — three months is inside one onset anniversary in every realistic
case — which removes a second duration dimension at no measurable cost. The disabled ledger
carries a value vector for the same reason: the month’s benefit is one sum over a slice.
dis_cohorts(t) and runoff_cohorts(t) are list-valued cells, a cost decision: a
two-argument recursion over (t, z) would be nearly two hundred thousand separate cells on the
anchor cell where this is one per month with a loop inside, and the notes’ two-dimensional
objects stay addressable as pols_dis_dur(t, z), pols_runoff_slot(t, k) and runoff_val(t, k).
Two things this deliberately does not do. It does not separate recovery from konkrete
Verweisung — both end the benefit through the same Nachprüfung with the same run-off and no
public data separates them R3 R29, so recov_rate(z) is exactly one
claim-termination-other-than-death rate — and it carries no age-at-disablement dimension on
reactivation, which DAV 1997 RI does R16.
The Bruttobeitrag is derived, not read#
The *_first cells are a second projection, not a variant of the first: the same
four-ledger chain on Rechnungsgrundlagen erster Ordnung — inception × 1,30, reactivation
× 0,70, disabled-lives mortality × 0,80, active-lives mortality × 0,80, no lapse — run
over the contract’s original term from entry_age and indexed by s rather than t.
Prudence for a disability product means a claim that starts more often, ends less often and
lasts longer; it also means fewer premium-paying lives lost, because here an active death and
a lapse both release a liability and a prudent basis does not anticipate a favourable event.
The equivalence is linear in P, because both the acquisition and the proportional
administration loadings are proportional to it:
P = (PV_rente + PV_wgh + PV_cost + PV_admin)
/ ( PV_prem x (1 - admin_prem_rate) - acq_rate x BS_unit )
= (24,452.4895291302 + 531.1897520089 + 335.6805156244 + 544.5174674852)
/ (29.0716529817 x 0.91 - 0.025 x 37)
= 25,863.8772130176 / 25.5302042134 = 1,013.0697368527 EUR p.a.
so the monthly instalment is P × 1,05 / 12 = 88,6436 € and the Zahlbeitrag 0,70 × that
= 62,0505 €. The recursion is acyclic: no decrement depends on the premium, so nothing
in the pv_* cells depends on P. The equivalence is struck before the Risikozuschlag
and without lapse, deliberately on both counts, and PV_prem is struck on P / 12 in
every month — so freq_load is a genuine loading on the tariff premium rather than a
re-expression of it. Running the shadow from inception rather than from the valuation date is
what gives an in-force point the premium its contract was struck at: model point 6 is model
point 1 fifteen years on, and the two price identically at 1 013,0697 € p.a. Model point 13
supplies gross_prem_ann = 2 400,00 € instead, so the override branch ships too.
Two escalations, two clocks#
escalates |
steps on |
cells |
|
|---|---|---|---|
Beitragsdynamik |
the insured BU-Rente and the annual Bruttobeitrag, before any claim |
the policy anniversary |
|
Leistungsdynamik |
the BU-Rente in payment |
the anniversary of the onset |
|
rente_pay_pp(t, z) = bu_rente_pp(t − z) × (1 + g_L)^((z − 1) // 12): the insured amount at
the moment of onset, which for a cohort at duration z in month t is month t − z,
escalated on each anniversary of that onset. Cohorts z = 1 … 12 are paid what they came in
on and z = 13 opens the first escalated year. Escalating the amount in payment on the
policy anniversary is the wrong clock and is a numbered pitfall.
The dynamik form is model point 4, and it carries a departure from market practice recorded
rather than corrected: German insurers price each increment at the attained age reached, so a
given increase buys less than proportional cover. This model escalates premium and insured
BU-Rente by the same g_B and prices the whole stream by one equivalence at inception —
internally consistent, acyclic, and understating what the market would charge.
Two rating multipliers, and only one of them touches a claim#
occ_factor(κ) loads the inception rate:inc_rate(t) = inc_rate_base(t) × occ_factor() × accept_factor × au_uplift(), and those are the only three multipliers on it. So it moves every claim and every decrement and reaches the premium only through the equivalence. Model point 3 is the anchor at BG4:inc_ratescales by exactly 3,00 while the premium scales by 2,932141, slightly below it, because the flat administration and assessment charges do not scale with the risk.risk_factor(ρ) loads the Bruttobeitrag alone. Model point 11 carries 1,50: its premium and surplus credit scale by exactly 1,50 while every decrement, claim and claim expense is bit-for-bit what it would be at 1,00. A Risikozuschlag prices an individually assessed impairment the base table does not carry and this model does not carry either, so a loaded contract is projected above its own modelled cost. The direction is stated, not corrected.
accept_factor = 0,80 is the Anerkennungsquote and multiplies the inception rate, not
the benefit: a declined claim generates no annuity at all rather than a smaller one. The
shipped table is gross of declinature, so a user substituting one already net of it must
set the factor to 1,00 or the effect is counted twice REG-R53.
sex is a model-point attribute for reporting only and must not price: sex-differentiated
premiums and benefits have been unlawful in Germany for contracts written from 21 December 2012
R15 REG-R34. Model points 1 and 2 differ in sex alone, and their frames are identical.
The Leistungsendalter stops the benefit and holds the mass#
cover_end_age and benefit_end_age are separate contractual terms, not synonyms. Model point 9
carries cover to 67 and benefit to 63: from attained age 63 the BU-Rente and the
claim-maintenance cost are exactly zero while the premium runs on for four more years, collecting
a further 2 244,03 €.
What is easy to get wrong is the population. The mass is held, not deleted: the ledgers
keep rolling past benefit_end_age, so check_states() and check_pols_roll_fwd() still close
across the boundary and pols_if(t) is continuous through it, where deleting the disabled
cohorts breaks both identities at once. Those lives do not resume paying premium std —
they are still berufsunfähig, and the Beitragsbefreiung is read as keyed to the state
rather than to the payment. The alternative reading is defensible and is named.
Four absences are product facts#
No death benefit. An SBU pays nothing on death, before or during a claim [S1], so
pols_death(t)is a decrement and never a cash flow, and there is noclaims_deathcolumn.No maturity benefit. Survival to the Endalter pays nothing; a claim still in payment at the horizon simply stops. That is true of the GDV model conditions and of the modelled product; it is not universal — one retrieved carrier’s AVB grants a surplus-financed Schlusszahlung at expiry where no BU arose [S12]. That is a surplus application, not a guarantee, and this model carries no surplus account to fund one.
No cash value. § 169 VVG through § 176 gives this contract a Rückkaufswert — the AVB pay it “entsprechend § 169 des Versicherungsvertragsgesetzes (VVG)” [S1] — and § 165 a beitragsfreie BU-Rente R8 R9 R5 REG-R28; both are the release of a reserve this model deliberately does not compute, and both are small, the conditions themselves warning that the premium parts available to build it are “sehr gering” against premiums paid [S6].
claims(t, "LAPSE")therefore exists, returns zero at everytand is published as a zero column; there is noav_pp_at, no surrender cells and no paid-up state. The zero states the scope; a missing column would hide it.No acknowledged state. § 173’s once-only befristetes Anerkenntnis would justify one R2, but this model pays from onset and does not model the Leistungsprüfung delay, so acknowledgement is a timing event with no cash-flow consequence — right in amount, early.
Inputs are external files#
The seven input CSVs live in this directory, beside run.py, and BU_DE_S/ holds nothing
but formulas — __init__.py, _system.json, Data/__init__.py and Projection/__init__.py, no
_data/, no IOSpec, no embedded values. This follows lifelib’s annuallife/TradLife_A, which
keeps its inputs beside the model; it is the opposite of basiclife/BasicTerm_S, which stores
its inputs inside the model.
Read once, in Data#
Projection is parameterized by point_id, so every Projection[N] is a separate ItemSpace
with its own cells cache; readers placed there would re-read every file for every policy. They
live instead in an unparameterized Data Space that Projection reaches through a data
Reference, so each file is read once per model however many policies are projected, and a test
counts the reads against a registered file set. Data.input_dir() resolves the location from
_model.path.parent when the model is read, so it works from any checkout.
The trade-off: the model is not portable on its own — copy BU_DE_S/ without the CSVs and
it reads fine, then fails on first evaluation. What you gain is that a diff of the model shows
logic changes only, and an input can be swapped in place: point Data.mortality_file at
another same-schema file and the projection follows, with no formula change.
Reference |
Cells |
File |
Contents and provenance |
|---|---|---|---|
|
|
|
Thirteen model points. Point 1 is the worked-example anchor cell (aktiv / F30 / BG1 / 1 500 € a month / cover and benefit to 67 / no Karenzzeit / monthly). Points 2–13 exercise the unisex twin, the occupational factor, the |
|
|
|
Annual Invalidisierungswahrscheinlichkeit by attained age 18–66. std two-slope Gompertz proxy |
|
|
|
|
|
|
|
|
|
|
|
The five Berufsgruppen with loadings and labels: BG1 1,00, BG2 1,40, BG3 2,10, BG4 3,00, BG5 4,50. std, anchored at 1,00 for office and 3,00 for the reference manual class inside the recalled 2×–4× band [S6]. Carrier classifications are not comparable with one another |
|
|
|
Annual Stornoquote by policy year 1–5, row 6 the ultimate: 4,0 / 4,0 / 3,5 / 3,0 / 2,5 / 2,0 %. std, and low by the standards of every other delib product — a product fact, not a modelling choice, because cover cannot be replaced once health has changed [S16]. The 30-day Widerruf sits inside year 1 REG-R23 |
|
|
|
|
Every file but the model point table carries a provenance column, one tag per row —
delib’s second ruling, and it is machine-checked.
The published identities#
Seven check_*() cells, each a no-argument bool over all t with a per-t residual.
check_net_cf — delib ruling 1, in one line:
net_cf(t) = prem_zahl_pp(t) × pols_prem(t) − claims(t,"BU_RENTE") − claims(t,"REINTEGRATION") − claims(t,"LAPSE") − expenses(t) − claim_expenses(t).
The premium leg is deliberately rebuilt from the Zahlbeitrag actually billed times the
premium-paying count rather than from premiums(t) − surplus_credit(t), which makes it a real
reconciliation instead of a restatement of net_cf’s own formula: it crosses the Brutto /
Zahl split and fails if the premium is weighted by pols_if instead of pols_prem.
Check |
Identity |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
check_pols_roll_fwd is trivially zero by construction — pols_if is defined by exactly
that recursion — and is published because it is the notes’ own identity. check_states is the
one that is not trivial: pols_if is built off the two exits rather than as the sum of the
three ledgers, so comparing it against them catches a life that leaves one ledger without
arriving in another, or arrives in two. The first fixes the definition, the second tests it.
Modules that are off in the base run#
Module |
Switch |
Off value |
What it does |
|---|---|---|---|
AU-Klausel |
|
|
Multiplies the inception rate. Model point 10 has the clause on with the uplift at 1,00, so the switch is demonstrably inert. The clause is documented — six months of continuous certified Arbeitsunfähigkeit, or four plus a specialist prognosis; the AU pension equal to the BU-Rente; a cap of 24 months per contract at one carrier and up to 36 at another; set off against a later BU award [S4] [S6] [S8] [S12]. What no retrieved source quantifies is the loading it puts on incidence, so shipping a number would still be an invention |
Beitragsdynamik |
|
|
Escalates the insured BU-Rente and the annual Bruttobeitrag together on each policy anniversary; 3 % on model point 4 |
Wiedereingliederungshilfe |
|
|
Monthly Renten paid on each completed run-off |
Leistungsdynamik |
|
|
In-claim escalation on each onset anniversary |
Risikozuschlag |
|
|
A multiplier on the Bruttobeitrag alone |
Premium override |
|
|
Model point 13 supplies 2 400,00 € p.a. instead |
Model point 12 is the anchor with both escalations off: its equivalence gives 865,95 € against the anchor’s 1 013,07 €, so the Leistungsdynamik and the Wiedereingliederungshilfe together are worth 147,12 € p.a., 14,5 % of the Bruttobeitrag — not additively, because both are paid out of the same claim population.
Three constructions the notes describe are not implemented, each because doing so would
stack an unsourced assumption on an already-std basis: lapse selection (strongly
selective in BU, so a non-selective rate understates the surviving book’s inception rate —
direction known, size not); premium-shock lapse (take-up of the Beitragsdynamik increases
is folded into the effective beitragsdyn_rate instead, which keeps the equivalence
acyclic); and the Nachversicherungsgarantie, needing both a take-up assumption and an
anti-selection loading on the incremental cover.
Sign convention#
net_cf is income positive — the Bruttobeitrag in, the Beitragsverrechnung, claims and
expenses out — which is the notes’ own orientation and the library-wide sign. liability_cf
publishes the same stream outgo-positive, liability_cf(t) = −net_cf(t) exactly, and both are
columns of result_cf() so the identity is verifiable in the frame rather than only in prose. A
Solvency II best estimate is Σ v(t) × liability_cf(t) over the relevant risk-free term
structure, plus a risk margin REG-R1 REG-R2 REG-R4; nothing in this library discounts,
and rechnungszins appears only inside the equivalence.
The shape to expect is a large first-month strain — the whole acquisition charge falls in month
0, 937,09 € of the 946,57 € of expense against an 88,64 € instalment, so net_cf(0) = −884,58 € — then thin positive margins that decay, cross zero between months 264 and 265 at attained
age 52, and reach −76,85 € in the last month. That crossing is the Deckungsrückstellung this
model does not compute being built and run down.
expenses is administration only — acquisition, the proportional loading and the flat charge.
The Leistungsbearbeitungskosten are claim_expenses, separate because they scale with
claims rather than policies; commission is not a line at all, sitting inside acq_rate.
Naming#
Cells follow lifelib’s basiclife/BasicTerm_S and savings/CashValue_SE wherever those models
have an analogue: pols_* for policy counts, plural nouns for cash flows, *_rate for annual
rates with *_rate_mth for their monthly equivalents, *_pp for per-policy amounts,
claims(t, kind) with an uppercase kind string, pols_if_at(t, timing) for the end-of-month
read, and check_*() / check_*_resid(t) for the identities. The full symbol mapping lives in
the Projection Space docstring.
The monthly cohort-vector chassis is shared with frlib’s Dep_FR_S (assurance
dépendance): dis_cohorts ↔ dep_cohorts, pols_dis_dur(t, z) ↔ pols_part_dur /
pols_tot_dur, seed_claim_dur ↔ seed_dur, and cohort_len, rente_pay_pp(t, z),
pols_prem and pols_recovery mean the same thing on both. pols_runoff_slot is this
model’s counterpart of Dep_FR_S’s pols_red — a small holding ledger a naive
implementation omits, which is a first-order error in both. Inside this library,
Pflege_DE_S is the other monthly multi-state model and shares the vocabulary —
pols_prem, pols_if_at, check_states, check_pols_roll_fwd — but not the cohort
vectors: its ledgers (pols_karenz(t, g, z), pols_grad(t, g), pols_pg(t, g),
pols_reactiv) are indexed by Pflegegrad rather than by claim duration.
Five names needed care:
Notes |
Cells |
Why |
|---|---|---|
|
|
One loads the inception rate and moves every claim; the other loads the Bruttobeitrag and moves nothing else |
|
|
The insured BU-Rente on the policy clock against the amount in payment on the onset clock |
|
|
In force against premium-paying: the difference is the Beitragsbefreiung |
|
|
The table rate and the rate after κ, α and υ, published separately so a substituted table already net of declinature is visible |
|
|
A count and a value: the run-off carries frozen BU-Renten, so both are needed |
status, sex, au_klausel and claim_duration_init drive little or nothing in the base
parameterization and are exposed as documented cells rather than dropped: a silently missing
column is worse than an inert one.
Standardizations used#
Everything in this table is std. The product is unusually std-heavy and that is the correct outcome, not a defect: the mechanics are well established and cited above, and it is only the levels that no retrievable document supplies.
Standardization |
Value |
Rationale |
|---|---|---|
Inception table |
|
DAV 1997 I is not public and is not shipped R16. The shape is what the research establishes — flat to 30, moderate through the forties, sharply accelerating after — and the level is anchored so the worked example reproduces exactly |
Occupational loadings |
BG1 1,00 … BG5 4,50 |
One base table with occupational loadings is how German BU pricing works; the 1,00 / 3,00 anchors sit inside the recalled 2×–4× manual/office band, the rest interpolated geometrically. No retrieved document supports the classification: the NÜRNBERGER AVB was read in full for the 2026-08-30 pass and never uses the word Berufsgruppe, and no Berufsgruppenverzeichnis is published at a public address [S6] |
Reactivation table |
0,250 → 0,006 by claim year |
DAV 1997 RI is not shipped R16. Front-loading is the established shape; the levels are construction, and there is no age-at-disablement dimension, which the real table has |
Active-lives mortality |
|
DAV 2008 T is not shipped R17. An insured-lives Todesfall-character shape, not a population table |
Disabled-lives mortality |
exactly 4,00 × the active column, times select factors 3,0 / 2,0 / 1,6 / 1,4 / 1,3 / 1,2 |
DAV 1997 TI is not shipped R16. Defining the column from the active one rather than rounding its own formula independently makes |
|
0,80 |
The Anerkennungsquote, recalled at 75–80 % R21 R20 |
|
1,00 everywhere |
No source quantifies the AU-Klausel’s effect on incidence. An inert switch is honest; an invented loading is not |
Lapse table |
4,0 % falling to 2,0 % |
No German insurer publishes a BU Stornoquote. The level is low, which is a product fact [S16]; lapse selection is not modelled, and the direction of that error is one-sided |
Monthly conversion |
|
One convention applied uniformly to |
Processing order |
mortality → lapse → inception; then disabled deaths → terminations; then the run-off |
Taking incidence first gives |
First-order loads |
1,30 / 0,70 / 0,80 / 0,80, no lapse |
Prudence for a disability product forks: higher incidence, lower reactivation, lower disabled and active mortality, and no anticipation of a favourable decrement |
|
1,00 % p.a. |
The Höchstrechnungszins. The figure is sourced: DeckRV § 2 Abs. 1 fixes it “auf 1 Prozent”, and § 2 Abs. 2 makes the rate used at conclusion apply for the whole term R13. The 1 January 2025 commencement is not in the consolidated text and stays |
|
2,5 % of the Beitragssumme, once at issue |
Sits at the § 4 DeckRV Höchstzillmersatz: “Der Zillmersatz darf 25 Promille der Summe aller Prämien nicht überschreiten” R13 REG-R16, restated by both retrieved AVB as 2,5 % of the premiums payable over the term [S1] [S6]. The ceiling is the only sourced number in the charge structure, and the level is a choice to sit at it |
|
9 % of the Bruttobeitrag |
A construction, and not because the disclosure does not exist: VVG-InfoV § 2 Abs. 1 Nr. 1 with Abs. 2 and Abs. 4 requires a German BU insurer to state the calculated acquisition costs and the administration costs in euro, and one AVB points the customer to the Produktinformationsblatt for them R12 [S6]. No PIB was obtained [S13], so the level here is a choice. A pure risk contract does separately lack an Effektivkosten figure, § 2 Abs. 1 Nr. 9 confining that to contracts whose obligation is certain [S14] |
|
18,00 € p.a., charged 1/12 monthly, uninflated |
A German Verwaltungskostenzuschlag is fixed in the tariff at conclusion |
|
800,00 € per inception / 12,00 € per month in payment |
The BU-specific charge a modeller from a term-life background forgets. Flat euro amounts, which is why a heavier class carries a premium below the ratio of its inception rates |
|
1,00 / 1,02 / 1,03 / 1,05 |
The recalled German Ratenzahlungszuschlag ladder |
|
0,70, held constant |
Midpoint of the recalled 0,60–0,75 common range inside a 0,50–0,80 band |
|
2 % p.a. |
Midpoint of the recalled 1–3 % menu. A BU model without in-claim escalation misses the product’s dominant long-duration sensitivity |
|
6, paid on the completed run-off |
Recalled range 3–12 monthly Renten. Paying it on every recovery instead overstates it by exactly the run-off’s own mortality — 418,79 € against 409,61 € on the anchor cell |
Run-off values not escalated |
no Leistungsdynamik inside the three months |
Three months is inside one onset anniversary in every realistic case, and it removes a second duration dimension |
Karenzzeit premium |
still payable inside it |
The Beitragsbefreiung runs with the benefit, so a life not yet paid still pays |
Leistungsendalter premium |
not resumed after it |
The waiver is read as keyed to the state. The alternative reading is defensible and is named |
Age basis |
age last birthday advancing at the policy anniversary |
The model carries no dates; a date-based implementation carries a fractional offset of at most one year |
|
one equivalence at inception on the whole escalating stream |
Internally consistent, acyclic, and not the market’s annual-repricing practice — recorded rather than corrected |
The thirteen model points |
— |
Configuration rather than observation: no rate card, no commercial envelope and no Berufsgruppenverzeichnis was obtained. One envelope datum did arrive: a retrieved AVB caps total BU, EU and Grundfähigkeit entitlement at 60 % of regular annual gross income [S9], which the anchor’s 1 500 €/month is well inside |
The only quantities in the model that are not standardizations are the structural rules: the
three-month run-off R3, the Beitragsbefreiung while the BU-Rente is in payment [S1] [S2],
the Brutto / Zahlbeitrag pair and its immediate credit R10 R14, the unisex rule R15,
the zero lapse benefit and the absence of a death and a maturity benefit [S1] R8 R9, and the
§ 4 DeckRV ceiling the acquisition charge sits at R13 REG-R16. Each of those is now backed by a
document that was read: the statutes as canonical XML, the contractual rules in the GDV model
conditions and four carrier AVB — see sources.md.
Tests#
tests/test_berufsunfaehigkeit_de.py asserts the frame itself — list(result_cf().index) == list(range(proj_len())), so 444 rows t = 0 … 443 on the anchor cell, ending at the last
index proj_len() − 1 — then all eighteen printed rows of the notes’ worked
example to the cent and the state ledgers to six decimals, the full-precision totals against
the sum-of-rounded-cells the notes also print, the derived Bruttobeitrag of 1 013,0697 € p.a.
reached two independent ways, month 0 rebuilt term by term with a calculator, the first
inception and the first BU-Rente from the annual rates, the § 174 run-off traced through one
cohort, the four-way decrement closure, the Brutto / Zahl ratio surviving aggregation, the
Beitragsdynamik variant’s twelve printed rows and totals, the seven check_* identities with
their residuals, and one test per numbered modeling pitfall — eighteen of them. The
whole-model-point-table sweep is not here: tests/test_model_conventions_de.py owns the
library’s single sweep, because a model point’s first evaluation is the most expensive thing in
the run.
python -m pytest lifelib/libraries/delib/tests/test_berufsunfaehigkeit_de.py -q
python -m pytest lifelib/libraries/delib/tests/test_model_conventions_de.py -q -k BU_DE_S