Implementation Notes#
Status: Draft, 2026-08-29. Built from
products/kapitallebensversicherung/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 contractual mechanics are sourced — the surplus declared as a percentage of the Deckungskapital, in four carrier wordings that agree on the base and differ on the timing and the waiting period [S7] [S18] [S9] [S3], booked into the Deckungskapital at the Bilanztermin [S9], the declared level being revisable annually and capable of being zero [S1] [S9]; the Rückkaufswert as the Deckungskapital on the Rechnungsgrundlagen der Prämienkalkulation struck at the end of the current Versicherungsperiode and floored by the five-year spreading R2, restated verbatim in the model wording [S1] and in three carrier wordings [S7] [S18] [S3]; the Stornoabzug as vereinbart, beziffert and angemessen R2 R22, quantified on three different bases by three carriers [S3] [S9] [S18] R30; the § 165 VVG Mindestversicherungsleistung test and the paid-up sum bought with the § 169 value R3 [S1] [S7]; the § 161 VVG substitution of the Rückkaufswert for the sum insured on a suicide inside three years R4 [S1]; the ending of the contract, and so of the premium, with the death payment [S7]; and both DeckRV cohort ceilings, each fixed at conclusion for the whole term R7 REG-R15 REG-R16. Every behavioural and experience assumption is a std standardization. No German insurer publishes a mortality basis, an expense loading, a commission scale, a terminal-bonus rate or a lapse rate for this product — the six carrier documents read for this library contain none of them [S3] [S4] [S5] [S7] [S9] [S18] [S11] [S12] [S13] — and the DAV tables — DAV 2008 T here — are the property of the Deutsche Aktuarvereinigung, are not public and are cited by name rather than redistributed R14 REG-R47 REG-R48. delib was drafted with HTTP egress blocked and nothing retrieved; the citations have since been re-verified, and forty-five of the forty-seven entries in
sources.mdnow record a document that was opened and read. That verifies the mechanics cited above and none of the levels below, which no retrieved document supplies. Replace the decrement, surplus and expense tables with company data before using any number here.
Run it#
python products/kapitallebensversicherung/run.py
python products/kapitallebensversicherung/run.py 8 # the Bonussystem variant
Three lines to the same thing:
import modelx as mx
model = mx.read_model("products/kapitallebensversicherung/KLV_DE_S")
model.Projection[1].result_cf()
Projection takes a point_id; Projection[1] is the worked-example anchor cell.
result_cf() returns a DataFrame indexed by the policy month t with one column per
cash flow line; result_cf_annual() sums it into policy years, which is the view the technical
notes’ worked example is stated on; and result_surplus() is the annual state behind both —
the reserve, the declared rate, the surplus base and credit, the Überschussguthaben, the accrued
terminal share and the Rückkaufswert at each anniversary. The frames are separate on purpose: a
cash flow statement whose columns do not all sum to its bottom line is one a reader has to know
which columns to skip, and a state table that moves once a year should not be printed twelve times
over.
The model and both its Spaces carry docstrings — model.doc describes the product and the
projection basis, model.Projection.doc holds the full mapping between the technical notes’
symbols and the cells names, and model.Data.doc says what each input file is and, for the
mortality table, what it is not.
Two clocks: a monthly frame over an annual product#
t is the policy month index, 0-based and counted from issue, and
proj_len() = 12 × policy_term is the frame’s exclusive end. The frame is
t = t_start() … proj_len() - 1, contiguous, with t_start() = 12 × duration_init() —
duration_init being an elapsed count of policy years, so the conversion is a multiplication.
The anchor cell publishes 300 rows, t = 0 to 299, and the in-force point 10
(duration_init = 14) 192 rows, t = 168 to 359. There is no t = proj_len() row;
pols_if(proj_len()) is defined for the closure identities and weights no cash flow.
Almost nothing on this product is monthly, and the argument of a cells says so. Cells that
state an annual account take a 0-based policy year k — the whole pricing block, all three
reserves, the § 169 value, the paid-up purchase, every part of the Überschussbeteiligung, the
Stornoabzug band and the expense inflation. Cells that state a month take t — the in
force, the claims, the premium instalments and every result_cf() column. duration(t) = t // 12
is the bridge, age(t) = age_y(duration(t)) steps on the anniversary,
policy_year(t) = duration(t) + 1 is the contractual 1-based label, and
is_anniv(t) = (t % 12 == 11) marks the month the annual machinery acts in.
The decrement rates take t and return the year’s annual rate — the vectors the notes tabulate
— while mort_rate_mth(t) and lapse_rate_mth(t) are what the recursion applies, each
1 - (1 - r)^(1/12), so twelve of each compound back to the year’s rate. That is what leaves the
whole annual layer bit-identical to the annual-step model this replaced: pols_if at every
anniversary is unchanged, and with it every reserve, every declared credit and every ledger
balance. result_surplus() is row for row the table it always was.
What a mid-year exit is paid. A claim in a non-anniversary month is paid the balances standing
at the last anniversary — av_sur_close_pp, bonus_si_close_pp, term_bonus_close_pp and
res_guar_close_pp — because the Überschussdeklaration has not happened yet. On a gezillmert
contract that makes the guaranteed leg of a surrender exactly zero through the whole first policy
year, which is the consumer fact this product is best known for and one the annual grid could not
express: it had to pay a month-0 surrender the value the coming anniversary would close at.
What that meant for the input files. No CSV value changed, in this conversion or in the earlier move to a 0-based frame. Every time-like key column keeps its own scale:
File |
Column |
Decision |
Why |
|---|---|---|---|
|
|
unchanged; read as |
A contractual, 1-based policy-year label, not the frame’s |
|
|
unchanged; read as |
The same label, on the same scale, for |
|
|
unchanged |
An elapsed count of completed policy years; it is |
|
|
unchanged, still the 1-based policy year |
0 means “never” and has to stay free. The model maps it instead: the election falls at the end of the 0-based policy year |
|
|
unchanged |
A calendar year and a cohort key, not a point on the frame |
|
|
unchanged |
The same cohort key |
|
|
unchanged |
An attained age, reached through |
cost_table.csv and freq_loading_table.csv carry no time-like column at all — but
freq_loading_table.csv’s instalments column, which the annual grid could not use for anything,
now sets the premium collection cycle.
The declared rate is a total, not an add-on — the German delta#
This is the one thing about the product a reader arriving from a US or UK participating model will get wrong, and it is a subtraction rather than an addition. The laufende Verzinsung is the Garantieverzinsung plus the laufende Zinsüberschussbeteiligung REG-R53, so on the anchor cell a declared 2,70 % — Allianz’s 2025 rate for its combined classic life-and-annuity book, as reported by the trade press R26 — against a 1,00 % guarantee R7 R15 is a 1,70 pp credit and never 2,70 pp on top of 1,00 pp:
zins_ueberschuss_rate(k) = max(0, decl_rate(k) - rechnungszins())
The interest-surplus rate is derived and never an input, and both halves of that line
are load-bearing. The outer max is what keeps the nil scenario honest: the declared
rate is then below the guarantee, which the reserve roll-forward still meets in full, so
the surplus is zero rather than negative. Model point 14 runs that path.
The base it multiplies is the Deckungskapital — “in Prozent des maßgeblichen
Deckungskapitals” [S7], the reserve “um ein Jahr mit dem Rechnungszins abgezinst” [S18] —
surplus_base_pp(k) = max(res_pp_at(k, "AFT_INT"), 0) — the closing guaranteed
reserve of the year, not the sum insured and not the premium. The inner max guards the
other end: a gezillmerte Deckungskapital is negative at issue, and a positive rate on a
negative base credits a negative surplus. On the shipped 25 ‰ basis that guard is
inert, because the base is the closing reserve and it is already +570,75 € in the first
policy year (k = 0) against an opening −1 252,53 €; it is not inert at the pre-2015 40 ‰
ceiling R7 REG-R16, where that closing reserve is −190,22 € and the credit is nil. The test
suite exercises both, the second on a 2014-cohort copy of the anchor cell rather than by
asserting a behaviour the base run does not show.
Three reserves, and the one the customer gets#
The product has three constructions and needs all three. They are the premium-paying
constructions throughout, computed on the full sum_assured over the whole remaining
term; only res_pp switches to the paid-up basis.
All four take the policy year k, not the month: the Deckungskapital of this tariff is
defined at anniversaries and the monthly frame does not interpolate it.
Cells |
What it is |
On the anchor cell at |
|---|---|---|
|
The net prospective reserve, no acquisition cost at all |
0,00 € — the equivalence principle stated as a reserve |
|
The gezillmerte Deckungskapital the insurer holds |
−1 252,53 €, exactly |
|
The § 169 Abs. 3 VVG floor: the same net reserve with the acquisition cost spread evenly over the first five contract years R2 |
−1 252,53 €, equal at duration 0 |
|
|
776,70 €, the floor already binding |
res_guar_pp reads the reserves at k + 1 because § 169 Abs. 3 strikes the value zum
Schluss der laufenden Versicherungsperiode and not at the cancellation date R2, and it
takes the maximum because the Mindestrückkaufswert is a floor on the value. On a
long gezillmert contract the floor normally binds: ann_due_prem_fut(k) / ann_due_prem_1st() falls roughly linearly over m years while max(0, 1 − k/5) reaches
zero after five, so the two coincide only at durations 0 and m. res_guar_pp(11) — the
value struck at the end of the twelfth policy year, so the reserves read at k = 12 — is the
floor’s 22 413,46 € against a Zillmer reserve of 21 722,40 €: 691,06 € the customer would lose to
a model that published the Zillmer reserve alone as the surrender value.
Model point 13 (zillmer_on = 0) makes all three coincide at every duration and the floor
slack, which is the invariance test; model point 2 (prem_term = 1) reverses the ordering
from the first anniversary, a single premium leaving almost nothing to amortise. Both are
the right answer rather than degenerate cases. Note that the acquisition cost is charged
in the premium either way: zillmer_on enters alpha_cost, a reserving quantity,
and not the pricing equation. zillmer_on = 0 is a std switch, not an observed
market option: § 4 DeckRV sets a ceiling rather than a mandate R7, and every carrier
wording read here applies the Verrechnungsverfahren [S7] [S9] [S18].
Beitragsfreistellung is not a lapse, and it can fail#
§ 165 VVG converts the contract to a reduced beitragsfreie Versicherungssumme bought
with the § 169 value, bfz_si_pp() = res_guar_pp(e) / pu_single_prem(e + 1) with
e = bfz_year() - 1 R3. bfz_year is the contractual, 1-based policy year at whose end
the election falls — the column keeps that scale because 0 has to stay free to mean “never” —
so the election year is period e and the contract is paid-up from period bfz_year onwards. The policy stays in pols_if — only a Kündigung removes it — with that
reduced sum in place of sum_assured(), no further premium and a reserve
bfz_si_pp() * pu_single_prem(k). On model point 11 the election at the end of policy year 10
(k = 9, so the last premium month is t = 119) leaves pols_if bit-identical to the anchor’s
while the paid-up sum falls to 21 403,08 € and the maturity benefit from 65 227,99 € to
31 621,11 €.
Unless the resulting sum falls below the agreed Mindestversicherungsleistung. Then
the statute obliges the insurer to pay the § 169 value instead and the election becomes
a surrender R3: lapse_rate returns 1.0 in that year, the whole cohort leaves as
claims(t, "LAPSE") and every later row is zero. Model point 12 takes that branch — a
897,49 € paid-up sum against bfz_min_si = 2 500 € std — and terminates at the end of
policy year 3, month t = 35.
That year is the one place a rate of 1.0 must not be spread over twelve months.
lapse_rate_mth is 1 - (1 - lapse_rate(t))^(1/12) everywhere except here, where twelve-th
rooting a certainty would empty the cohort in the year’s first month and pay the Kündigung
eleven months early; bfz_fails() detects the branch and lapse_rate_mth places the whole
decrement in the anniversary month instead. The election is a dated contractual act, not an
experience rate.
Because the § 169 floor generally exceeds the Zillmer reserve, the paid-up sum bought is
worth more than the Zillmer reserve released. bfz_uplift_pp is that difference,
discounted to the start of the election year, and it enters res_pp_at so
check_res_roll_fwd() still closes in the election year rather than being switched
off there — and what it then asserts is a real identity: that the paid-up purchase was
made at exactly the § 169 value.
What a surrender pays, and what § 161 substitutes#
surr_value_pp(t) = res_guar_close_pp(t) * (1 - storno_rate(duration(t)))
+ av_sur_close_pp(t)
+ term_surr_share * term_bonus_close_pp(t)
The three *_close_pp cells are the bridge between the monthly frame and the annual ledger: each
returns the balance standing at the last anniversary passed, stepping up in the month
is_anniv(t) is true. A surrender in March is paid the December figures, because the
Überschussdeklaration that would move them has not happened.
The Stornoabzug bites on the guaranteed value alone: two of the three published
deductions are struck on the Deckungskapital or on the gap between the sum insured and it
[S3] [S18] R30, and none of the three reaches the accumulated Überschussguthaben, which
§ 169 Abs. 7 VVG in any case makes payable in addition to the § 169 Abs. 3 to 6 amount
R2. So the Überschussguthaben passes through undeducted. term_surr_share = 0 in the base run — the
accrued Schlussüberschussanteil is paid at the Ablauf and on death and not on
surrender, the choice that does not invent an entitlement the sources do not describe; the
parameter is exposed rather than hard-coded because it is what would move surrender values
most.
The same amount is what a suicide inside three years is paid. § 161 VVG makes the insurer
leistungsfrei and obliges it to pay the Rückkaufswert including Überschussanteile
under § 169 R4, so the German rule is a benefit substitution and not a forfeiture —
materially unlike art. L. 132-7 of the French code, where the cover is of no effect in the
first year and there is no surrender value to fall back on. benefit_death_pp(t) is
0.98 * benefit_full_pp(t) + 0.02 * surr_value_pp(t) for duration(t) < 3 — policy years 1 to
3, which are the months t = 0 to 35 — and benefit_full_pp(t)
thereafter, on suicide_share = 0.02 std. The three-year window is a term of the
contract and so ends on the third anniversary, not at a month the monthly grid happens to
make convenient; what the monthly grid adds is that a death inside it is now paid the
Rückkaufswert of the anniversary it follows, which through the whole first policy year is
nil on the guaranteed leg. Paying nil on the excluded share would
be the error; setting the share to zero is a defensible variant.
Unlike a Risikolebensversicherung, a lapse here is a real and often large outflow — 9 112,99 € over the anchor projection against 33 365,26 € of premium — and in the final policy year the difference between a surrender and a maturity decides a payment rather than only a label. That total is 992,00 € below the annual-step model’s, and the gap is the whole point of the conversion: a surrender in a non-anniversary month is now paid the value standing at the anniversary behind it rather than the one ahead of it.
The three Überschussverwendung systems#
One surplus_credit_pp(k) and three destinations, exactly one live per model point. All three
ledgers are annual, and all three take k:
|
Ledger |
Maturity benefit |
Death benefit at |
|---|---|---|---|
|
|
65 227,99 € |
50 460,89 € |
|
|
63 562,77 € |
50 532,10 € |
|
|
52 428,98 € |
50 085,64 € |
t = 59 is the twelfth month of the fifth policy year, and the column is quoted there because
that is where the monthly model reproduces the annual one exactly. Through months 48 to 58 the
same three benefits are 50 286,07 €, 50 334,46 € and 50 053,54 € — the year’s declaration has not
been made, so av_sur_close_pp and bonus_si_close_pp still stand at the previous anniversary.
The step is a plateau and a jump, once a year, which is what a once-a-year declaration is.
That is exactly the asymmetry the sources record — “compared with the Bonussystem, the
verzinsliche Ansammlung leads to a higher payment at maturity, while the Bonussystem
produces higher death benefits” R28 — and it is arithmetic rather than coincidence: the
Ansammlung compounds at ans_rate = 2,70 % while bonus sum insured accumulates at
rechnungszins = 1,00 %, but the bonus is paid-up insurance whose whole face amount
falls due at once on death. A model that set the two rates equal would lose the
distinction, correctly. Beitragsverrechnung moves the surplus out of the benefit stream
entirely: premiums collected fall by 5 349,15 €.
Under Beitragsverrechnung the renewal commission is charged on the tariff instalment
prem_charged_inst_pp(t), not on the collected prem_inst_pp(t): the intermediary is paid on the tariff premium, the surplus offset
being a discretionary policyholder rebate the insurer may withdraw without invoking § 163
VVG at all REG-R27.
Two mortality bases over one sex-specific table#
Three quantities, deliberately not two indexings of one:
mort_rate_at_age(x)is the first-order tariff rate: it prices and it reserves, and it is a fixed unisex blend,½ q₁(M, x) + ½ q₁(F, x)std, because German new business has been unisex since 21 December 2012 REG-R34. Points 1 and 7 differ only insexand price identically at 2 004,0420 €; pricing off the policy’s own row moved that premium by 9,15 € when the model was first written that way.mort_rate_base(t)is the policy’s own sex-specific table row, andmort_rate(t) = mort_rate_base(t) × mort_be_factorwithmort_be_factor = 0.75std is the best estimate that projects. The 33 % wedge is the Sicherheitszuschlag, whose systematic release is the Risikoüberschuss REG-R47 — which this model does not compute, and which a model that reserved on the best estimate would have thrown away.res_ppis invariant tomort_be_factor;pols_deathmoves with it.rating_factoris a third thing again: the Risikozuschlag R5. It multiplies the first-order rate in the death leg of the pricing and the prospective reserve only — never the survivorship, never the benefit, never a best-estimate rate. On model point 14 it raises the Bruttobeitrag from 2 530,90 € to 2 611,45 € and leavesmort_rateand the death benefit untouched.
The table itself is a std Makeham-form proxy anchored at
mort_rate_1st(M, 37) = 0.001200 exactly, and one table serves both legs. The
direction of prudence forks — a death benefit wants mortality assumed higher than
expected, a survival benefit lower REG-R47 REG-R48 — so no single first-order table is
prudent for both. German practice resolves that in the tariff rather than in the table and
the model follows, with the compromise named rather than papered over.
The Ratenzahlungszuschlag applies to an unechte Zahlweise only#
prem_freq_load() is the table value where unterjaehrig_form is unecht — 1,000 annual,
1,020 half-yearly, 1,030 quarterly, 1,050 monthly std — and exactly 1,000 where it
is echt, because a genuine sub-annual Versicherungsperiode is not an instalment of an
annual one and carries no loading R28. Model points 4 and 5 are the same monthly contract
under the two readings and differ by 1 642,40 € of projected premium.
The other half of the Zahlweise is now real. instalments() no longer merely justifies a
loading it does not otherwise touch: prem_cycle() = 12 // instalments() is the collection
period in months, prem_due(t) is true when duration_mth(t) % prem_cycle() == 0, and
prem_inst_pp(t) = prem_paid_pp(duration(t)) / instalments() where one falls due is a single
instalment rather than a year’s premium — formed from the annual amount so the
Beitragsverrechnung offset is spread over the year exactly as the charge is. An annual payer is charged in month 0 of each policy year and nothing in
the other eleven; a monthly payer is charged every month; a quarterly payer in months 0, 3, 6
and 9. The cohort that has already died or surrendered mid-year pays nothing further, which is
why the point 4 / point 5 gap moved from 1 668,26 € to 1 642,40 € — an annual grid had to
collect the whole year’s premium from a cohort measured at the year’s start.
Inputs are external files#
The seven input CSVs live in this directory, beside run.py — not inside the model
folder. KLV_DE_S/ holds nothing but formulas:
products/kapitallebensversicherung/
model_point_table.csv <- inputs live here
mort_table.csv
lapse_table.csv
surplus_rate_table.csv
cost_table.csv
freq_loading_table.csv
deckrv_table.csv
run.py
model.md
product-spec.md <- the documents this model implements
technical-notes.md
sources.md
KLV_DE_S/ <- formulas only
__init__.py (model docstring)
_system.json
Data/__init__.py (reads the CSVs, once per model)
Projection/__init__.py (the by-policy projection)
This follows lifelib’s annuallife/TradLife_A, which keeps its input file beside the model
and reads it at run time. It is the opposite of basiclife/BasicTerm_S, which stores its
inputs inside the model through modelx’s IOSpec machinery — hence no _data/ directory
and no embedded values here at all.
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, which Projection
references as data — so each file is read once per model no matter how many policies are
projected. test_model_conventions_de.py counts the reads against the set registered in
de_registry.INPUT_FILES. Data.input_dir() resolves the location from
_model.path.parent when the model is read, so it works wherever the repository is checked
out.
The trade-off: the model is not portable on its own. Copy KLV_DE_S/ without the CSVs
and it will read fine, then fail 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 with no formula change.
Reference |
Cells |
File |
Contents and provenance |
|---|---|---|---|
|
|
|
Fourteen configurations. Point 1 is the worked-example anchor (M 37, term 25, 50 000 €, ratio 1,00, annual, 1,00 %, zillmered, |
|
|
|
First-order death rates by sex and age 0–120. std Makeham proxy |
|
|
|
Two different things by policy year: |
|
|
|
|
|
|
|
The first-order tariff loadings and the second-order expense basis on the same row, because the difference between them is the Kostenüberschuss. Ceilings cited R7 REG-R16 R29; every level std |
|
|
|
|
|
|
|
Both DeckRV ceilings by |
Every file but the model point table carries a per-row provenance column, which is this
library’s second ruling and is machine-checked.
The identities the model publishes#
Nine check_*() cells, each taking no argument and returning a bool over the whole frame,
each carrying a per-period residual beside it. The residual’s argument follows its cells’
clock: the six monthly identities carry check_*_resid(t) over policy months, and the three
that state an annual account — check_res_roll_fwd_resid(k), check_surplus_roll_fwd_resid(k)
and check_surr_floor_resid(k) — carry a residual per policy year. A Fackler recursion has
nothing to say about a policy month on a tariff whose Deckungskapital is only defined at
anniversaries, and inventing a monthly residual for it would have invented a monthly reserve.
check_net_cf() — this library’s first ruling — is
net_cf == premiums − claims_death − claims_maturity − claims_lapse − expenses − commissions, rebuilt from result_cf()’s own published columns.
The other eight: check_pols_roll_fwd(), the in-force recursion and the final year’s
maturity count; check_decrement_closure(), deaths plus surrenders plus maturities summing
to pols_if_init() by direct summation over the exit cells; check_res_roll_fwd(), the
Fackler recursion (V + P^Z + uplift)(1 + i₁) = f q₁ SD + (1 − q₁) V(k+1) computed
retrospectively on the left and prospectively on the right — the strongest single check in
the model, since it holds only if the premium, the first-order mortality, the
Rechnungszins and the prospective formula are mutually consistent;
check_surplus_roll_fwd(), whichever of the three surplus ledgers is live;
check_surr_floor(), § 169 Abs. 3; check_equivalence(), the first-order pricing
equivalence; and check_rechnungszins_cap() and check_zillmer_cap(), the two DeckRV
cohort ceilings. The last two are parameter invariants rather than roll-forward
identities, and they live in the model rather than in a build script because a German model
point’s cohort is an assumption: a 4,00 % guarantee on a 2026 issue year is not a stress,
it is a data error.
check_zillmer_cap() and check_surr_floor() are separate on purpose. § 4 DeckRV caps
how much may be zillmered — a cap on the charge — while § 169 Abs. 3 VVG fixes how
the acquisition cost is spread for the surrender floor — a floor on the value R2 R7
REG-R16 REG-R28. One search summary in the corpus conflates them; delib does not.
Modules that are off in the base run#
Three constructions are implemented and switched off, so the base run reproduces the worked example while the machinery stays visible and testable.
Module |
Switch |
Off value |
What it does |
|---|---|---|---|
Premium-shock lapse |
|
|
|
Rate-gap lapse |
|
|
|
Beteiligung an den Bewertungsreserven |
|
|
Adds |
All three enter through annual quantities, and both lapse modules move lapse_rate, the
year’s rate, rather than lapse_rate_mth — a shock to a decrement is a statement about the year
and the geometric split then carries it into the months on its own.
term_surr_share = 0 is a fourth switch of the same kind: the accrued
Schlussüberschussanteil is not paid on surrender in the base run, and raising it is what
a user who reads a carrier’s wording differently would do first. No German calibration of
any of these numbers exists in the corpus, which is why all four ship off.
Also not implemented, and stated rather than left to be discovered: no premium-default path (§§ 37 and 38 VVG were never researched, gap 20), no Widerruf decrement (§ 152 VVG, likewise), no dynamic Beitragsfreistellung take-up — the election is a deterministic model point column because the corpus gives no take-up rate at all R3 R20 — no management action on the declared rate, and no Zusatzversicherung, Kapitalwahlrecht or Dynamik.
Sign convention#
net_cf is income positive — Beiträge in, claims, expenses and commission 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-R6; nothing in this library discounts.
expenses excludes commission, which is the deliberate difference from the frlib
chassis, where the commission sits inside the expense column and is published beside it.
Here commissions is a separate line, so the six flow columns of result_cf() sum to
net_cf with no double count. Whichever convention a model takes, taking both at once is
the error, and check_net_cf() is what makes the choice checkable.
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 rates, *_pp for per-policy amounts, claims(t, kind) with an uppercase kind string,
and *_at(t, timing) — or *_at(k, timing) on the annual ledgers — for the within-period
reads. The technical notes use compact actuarial
symbols; the full mapping lives in the Projection Space docstring. Six cases needed care:
Notes |
Cells |
Why |
|---|---|---|
|
|
Three quantities: the unisex tariff rate that prices and reserves, the policy’s own sex-specific table row, and the best estimate that projects. |
|
|
One symbol, five amounts. |
|
|
The Bruttobeitrag and two pricing quantities that never become cash flows. |
(charged / paid) |
|
The year’s Zahlbeitrag before and after the Beitragsverrechnung offset, with |
|
|
The surplus half of the house account-value pair, on the Überschussguthaben — which receives surplus and never premium, so |
|
|
A decrement and a deduction, in one CSV and easy to confuse. |
The sister models that share this chassis. KLV_DE_S is the Überschussbeteiligung
chassis the rest of delib’s savings side reuses: RV_DE_S (klassische Rentenversicherung)
is the same machinery with an annuity rather than a lump sum at the Ablauf, Basis_DE_S
and Riester_DE_S add a tax wrapper and a state Zulage to it, and Index_DE_S spends the
declared surplus on an index participation instead of accumulating it. FRV_DE_S does
not share it: a unit-linked Rückkaufswert is a Zeitwert of fund units and not a
Deckungskapital R2. Across libraries the nearest relatives are lifelib’s
annuallife/TradLife_A, whose external-input layout this model copies, and frlib’s
Euro_FR_S, whose participation aux bénéfices is the same idea under a different statute
— with the difference that a French fonds euros credits a rate to an account balance while
a German endowment credits it to a reserve.
Standardizations used#
Everything in this table is std. The rule the library enforces is that every quantitative parameter is either source-tagged or marked here.
Standardization |
Value |
Rationale |
|---|---|---|
Mortality proxy and its anchor |
|
DAV 2008 T is not public and is not redistributed R14. The anchor is what makes the worked example reproduce, and is the one number a replacement must preserve |
Unisex blend |
0.5 |
New business is unisex REG-R34; no insurer publishes the portfolio mix behind its own tariff |
|
0.75 |
A 33 % first-order safety loading. The Sicherheitszuschlag level is not established for any German carrier |
Age basis |
Age last birthday at issue, stepping at the anniversary |
No located German endowment wording states one |
|
25 ‰ of the Beitragssumme |
The § 4 DeckRV ceiling is cited R7 REG-R16; sitting at it is the choice, no carrier’s actual acquisition cost being public (gap 7) |
|
3,0 % of premium; 1,5 ‰ of |
The form of the premium loading is established R28; the form of the sum-insured loading is not (gap 17). Neither level is |
Expense basis |
300 € acquisition; 45 € maintenance at 1,8 % p.a.; 120 € per claim |
No carrier charge level is established (gap 7). The one product-level cost disclosure in the corpus is a PRIIP-BIB for a different product, showing a 5,3 % annual cost impact over twenty years on its own model case [S10]. Sized so the first-year acquisition outgo modestly exceeds what the Zillmerung recovers |
Commission |
2,5 % of the Beitragssumme initial; 1,5 % renewal |
Set at the 25 ‰ zillmering ceiling R7, which is not a commission cap R29. No carrier commission rate is established anywhere in the corpus |
|
2,70 % for the whole projection |
The level is R26’s report of Allianz’s 2025 declaration for a combined classic life-and-annuity book — no carrier publishes an endowment rate — and holding it level is a modelling choice, not a forecast. |
|
0,40 % p.a. of the Deckungskapital |
No terminal-bonus rate of any kind was established, for any insurer, in any year (gap 1) |
|
2,70 %, equal to |
A market convention. It matters because |
|
0.0 |
Not inventing an entitlement the sources do not describe; and the Sicherungsbedarf has routinely exhausted the Bewertungsreserven R1 R8 |
|
10 / 7,5 / 5 / 2,5 % by duration band |
Against three carrier schedules on three incompatible bases — 0–20 % of the Deckungskapital decaying to nil over the last ten years, under collective action and a BGH remittal [S3] R22 R30; 50 € + 0,15 % of premiums × years remaining [S9]; 100 € + 0,2 % of (sum insured − reserve) [S18]. Three figures on three bases are not a market range; a declining percentage of the reserve matches one of the three and is the only shape the model’s state variables express |
|
5 / 3,5 / 2 / 6 / 2,5 %, and 0 in the final year |
The shape follows the twelve-year tax threshold R10 REG-R45; the levels are not sourced (gap 10). The final-year zero is what makes the survivors leave as a maturity |
|
0.02 |
§ 161 VVG’s rule is sourced R4; no source gives a suicide share of deaths at any age |
|
2 500 € |
§ 165 VVG’s test is sourced R3; no carrier’s Mindestversicherungsleistung was located |
Five-year spreading read as straight-line |
|
§ 169 Abs. 3’s gleichmäßige Verteilung R2 admits a five-year Zillmerung reading too, which gives a slightly lower floor at durations 1–4 and the same value from 5 |
Bilanztermin → policy-year end |
— |
Die Bayerische allocates at 31 December [S9]; on a policy-year grid that falls inside a policy year for every contract not written on 1 January. Gothaer instead allocates at the policy’s own Stammtag [S7] and VPV at the start of the policy year [S18], so the policy-year convention is one of the observed ones. The effect against a calendar-year allocation is a timing shift of up to one year |
|
switch |
§ 4 DeckRV sets a ceiling, not a mandate R7, but every carrier wording read applies the Verrechnungsverfahren at 2,5 % or, pre-2015, 4 % [S7] [S9] [S18]. The un-zillmered point exercises the invariance, not an observed market option |
Surplus Wartezeit = none |
— |
The wordings give three answers: none [S9], one year [S18], three years [S7] tariff group A and [S3]. The model takes the shortest |
DeckRV split years |
1994 and 2000 take the higher rate; 2027+ hold 1,00 % |
The published history splits both years mid-year and a year-keyed table cannot REG-R15, so |
|
1,000 / 1,020 / 1,030 / 1,050 |
The 2 / 3 / 5 % market range is cited R28; no carrier publishes its own scale |
Behaviour modules off |
|
No German calibration of either exists |
The fourteen model points |
— |
No entry age, premium level or sum-insured band was established (gap 21); the anchor is a construction from the term band plus the twelve-year tax minimum |
The quantities that are not standardizations are the two DeckRV ceilings R7 REG-R15 REG-R16, the 2,70 % declared rate itself R26, and the structural rules — the surplus base and timing, the § 169 calculation and floor, the Stornoabzug biting on the guaranteed value, the § 165 test, the § 161 substitution, premium cessation on death, and the echte / unechte distinction.
Tests#
tests/test_kapitallebensversicherung_de.py asserts all twenty-five rows — policy years 1 to
25 — of the notes’ worked example to the cent off result_cf_annual() and pols_if to six
decimals, the twelve months of policy year 1 on the monthly frame beside them, the totals at full
precision, the ten printed rows of the state table, the notes’ four independent rebuilds — the
Bruttobeitrag from the equivalence, the first anniversary’s reserve by Fackler, the year-2
surplus credit and the year-12 surrender payment from its three parts — the closure
identity, the Einmalbeitrag and Überschussverwendung variants, every check_*() and its
residual, and one test per numbered modeling pitfall: the declared rate derived and not
added, the surplus base being the reserve, the negative-base guard shown on a 40 ‰ cohort,
three reserves rather than one, the § 4 cap and the § 169 floor asserted separately, the
Stornoabzug sparing the Überschussguthaben, § 161 substituting rather than forfeiting,
Beitragsfreistellung succeeding and failing, a paid-up policy staying in force, the lapse
table’s std provenance, the premium-cessation rule applied once, the Risikozuschlag
reaching only the pricing death leg, one table serving both legs, the two mortality bases
kept apart, the surplus systems’ asymmetry, the Zahlbeitrag not being guaranteed, sex
never reaching the premium, and the Ablauf year having no surrender.
The conversion added its own group: that the annual layer is unchanged — pols_if at every
anniversary, every reserve and every ledger balance equal to the annual-step model’s — that the
two decrement rates compound rather than divide, that result_cf_annual() reproduces the annual
totals column by column, that a mid-year exit is paid the last anniversary’s balances, that the
guaranteed leg of a first-year surrender is nil, that the Beitragsfreistellung failure lands in
the anniversary month rather than being spread over twelve, and that an annual payer is charged
in one month of each policy year and a monthly payer in all twelve.
python -m pytest tests/test_kapitallebensversicherung_de.py -q