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 contractual mechanics are sourced from retrieved clause text — the Deckungskapital as the premium net of risk and expense cover accumulated at the Rechnungszins [S8] [S11], the Höchstzillmersatz of 25 ‰ at § 4 Abs. 1 DeckRV R7 REG-R16, the § 169 Abs. 3 surrender floor and the § 169 Abs. 5 Stornoabzug conditions R1 REG-R28, the § 165 paid-up rule and its minimum-benefit branch R2, the death-benefit designs [S4] [S8] [S9] R24, the conversion at
max(garantierter, aktueller) Rentenfaktor[S9] [S14] [S18], the Bewertungsreserven crystallisation at the transition to annuity payment § 153 Abs. 4 VVG R4 [S4], and the Rentengarantiezeit [S1] [S4] [S9]. Every level in this model is still a standardization, but that is now a choice and not a necessity: a retrieval pass on 2026-08-30 established a current declared surplus rate [S15] and a Rentenfaktor market range R19 R24, and the model does not use them — see Where the model diverges from a retrieved document below. No charge parameter, expense or behavioural rate is established at any German carrier, and the DAV tables — DAV 2004 R here R12 R13 — are the property of the Deutsche Aktuarvereinigung, are not public, and are cited by name rather than redistributed. Replace the decrement, charge and rate tables with company data before drawing any conclusion from the numbers.
Run it#
python products/klassische_rentenversicherung/run.py
python products/klassische_rentenversicherung/run.py 6 # the 2,75 % legacy vintage
Three lines to the same thing:
import modelx as mx
model = mx.read_model("products/klassische_rentenversicherung/RV_DE_S")
model.Projection[1].result_cf()
Projection takes a point_id; Projection[1] is the worked-example anchor cell,
DE-RV-0001. 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_pols() is the annual state behind both — the
rates, the premium decomposition, the three account balances per policy and at fund level, the year’s
credits, the surrender value and the death benefit, 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 mapping between the notes’ symbols and the
cells names, and model.Data.doc says what each input file is and, for the three proxies,
what it is not.
Two clocks: a monthly frame over a mostly annual product#
t is the policy month index, 0-based and counted from issue, and
proj_len() = 12 × proj_len_y() is the frame’s exclusive end, with
proj_len_y() = omega_age() − issue_age the number of policy years. 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 852 rows, t = 0 to 851, seventy-one policy years running to attained age 121; the
in-force point 6 (duration_init = 20) opens at t = 240. A life annuity has no term, so the
horizon is the age at which the annuitant cannot survive further.
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 premium and its whole decomposition, the
Deckungskapital, the Ansammlungsguthaben, the § 169 Abs. 3 spread account, the declared rate and
the interest surplus, the surrender value, the death benefit, the Beitragsfreistellung election and
the Überschussrente step. Cells that state a month take t — the in force, the three
decrements, the claims, the annuity instalment, the expenses and every result_cf() column.
duration(t) = t // 12 is the bridge, policy_year(t) = duration(t) + 1 is the contractual 1-based
label, age(t) and calendar_year(t) both step on the anniversary, 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
and result_pols() prints — 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, on all
fourteen model points: pols_if at every anniversary, both account balances, the § 169 floor, the
surrender value, the conversion capital and the premium income are unchanged, and result_pols() is
row for row the table it was.
What the finer grid changed. Three things, and the largest is the one the product is named for:
Annual grid |
Monthly grid |
Why |
|
|---|---|---|---|
|
23 485,03 € |
23 115,89 € |
The Rente is monthly and is now paid monthly, in advance. Inside the Rentengarantiezeit the two agree to the cent, the count being fixed; the whole −369,14 € is in the survivor-weighted tail |
|
1 038,91 € / 10 670,70 € |
1 022,04 € / 10 688,30 € |
Deaths and surrenders compete month by month rather than once a year. The total exits at every anniversary are unchanged; the split is not |
|
1 669,77 € |
1 646,77 € |
A policy leaving mid-year bears administration for the months it was there |
What it did not change. The premium: § 12 Abs. 1 VVG makes the Versicherungsperiode the year
for this tariff and the Beitrag is payable in advance for it, so prem_due(t) is true in the first
month of each policy year and nowhere else, and premium income is identical. And what a claim is
paid: § 169 Abs. 3 VVG strikes the value zum Schluss der laufenden Versicherungsperiode and not
at the cancellation date, so a policy leaving in any month of policy year k is paid cv_pp(k) or
db_pp(k) — the year-end value it paid the year’s premium in advance for. What moved is when the
claim falls and how many policies are exposed to it. (KLV_DE_S is the contrast: there the
echt reading makes the period genuinely monthly, two model points differ in nothing else, and the
instalment stream is in the frame.)
Two rates of 1 are certainties rather than rates and are placed in the anniversary month instead
of being twelfth-rooted: the terminal q = 1 of the mortality proxy, which is the table’s closure
convention, and the § 165 cash-out, which is a dated contractual act. Spreading either geometrically
would empty the cohort eleven months early.
Time-like inputs. No CSV value changed, in this conversion or in the earlier move to a 0-based
frame. lapse_table.csv’s duration is the contractual policy year, 1-based, and the model reads
it through policy_year(t) = duration(t) + 1 — the § 20 EStG twelve-year step stays at duration 12,
which the model reads across months 132 to 143. decl_rate_table.csv’s calendar_year and the age
keys of mort_table.csv and rentenfaktor_table.csv are absolute and unaffected. On the model point
table, duration_init is an elapsed count of years and is k_start(), with t_start() twelve
times it; aufschub_y, prem_term_y and rgz_years are counts of years, so the comparisons
moved (t < 12n for the accumulation phase, 12m guaranteed instalments) rather than the values;
and pup_year stays the 1-based contractual policy year of the Beitragsfreistellung, with 0
meaning “never”, which the model maps to the paid-up policy year k = pup_year − 1.
freq_load_table.csv’s n_instalments column remains documentation: see The
Ratenzahlungszuschlag, below.
The declared rate contains the guarantee — the German delta#
This is the one thing a reader arriving from a US or UK account-value model will get wrong, and it is the first listed modeling pitfall. The German laufende Verzinsung is the Garantieverzinsung plus the laufende Zinsüberschussbeteiligung REG-R53, not a surplus paid on top of the guarantee. So:
bonus_rate(k) = max(0, decl_rate(k) − int_rate_guar())
and the two credits together deliver the declared rate on the post-premium Deckungskapital
and never more. On the anchor cell, the first policy year (k = 0):
rate |
on |
amount |
|
|---|---|---|---|
|
1,00 % |
1 600,6317 € |
16,0063 € |
interest-surplus term of |
1,55 % |
1 600,6317 € |
24,8098 € |
together |
2,55 % |
1 600,6317 € |
40,8161 € |
A model that credits 1,00 % and a further 2,55 % puts 56,82 € into year one instead of 40,82 €, and reaches 63 768,69 € of accumulated value at the Rentenbeginn against the correct 58 788,98 € — 8,5 % too much, the whole error sitting in the Ansammlungsguthaben, 12 698,26 € against 7 718,55 €.
The mirror image is model point 6, a 2,75 % legacy vintage against the same 2,55 % declaration:
bonus_rate(k) is zero in every policy year while int_credited_pp(k) is the largest in the table.
A contract already guaranteed more than the insurer is declaring receives no interest surplus at
all — a real German result, and what the max(0, .) exists to produce. Over point 6’s five
remaining accumulation years the model credits 9 187,70 € of guaranteed interest and 399,55 € of
surplus, every euro of it the declared rate on the Ansammlungsguthaben’s own balance.
What the model does not do is decompose the declaration into its four German components:
Zinsüberschuss, Risikoüberschuss, Kostenüberschuss and Schlussüberschussanteil are one
declared rate here. Only the first was established for this product R24, the other three
belong to the kapitallebensversicherung file that shares this chassis, and inventing a split
would be inventing three rates.
The guarantee vintage is a model-point attribute#
int_rate_guar() reads the model point, not a Reference. The lock is statutory: § 2 Abs. 2 Satz 1
DeckRV, “Bei Versicherungsverträgen mit Zinsgarantie gilt der von einem Versicherungsunternehmen zum
Zeitpunkt des Vertragsabschlusses verwendete Rechnungszins für die Berechnung der
Deckungsrückstellung für die gesamte Laufzeit des Vertrages” R7 REG-R14. So a German life book is
a layered stack of guarantee vintages rather than one rate: points 1, 6 and 14 credit 1,00 %,
2,75 % and 0,90 % in one run. One carrier’s own packs show the stack forming — DAV 2004R at 1,25 % in
Fassung 07/2015 [S5] [S6] and at 1,00 % in Fassung 01/2025 and 01/2026 [S7] [S16] [S4].
Re-running point 6 on a single global 1,00 % rate shows how that error surfaces: its Deckungskapital at Rentenbeginn falls 7,7 %, from 82 833,38 € to 76 439,87 €, its Ansammlungsguthaben rises 156 %, from 3 629,35 € to 9 292,76 €, and the conversion capital moves by −0,8 %. The vintage error is a misallocation between the two accounts, not a hole in the total, which is why it survives a reasonableness check on the headline.
The within-year order, which no source fixes#
Premium in advance, then the charges, then the Rechnungszins on what is left:
av_pp_at(k, "AFT_PREM") = av_pp(k) + prem_to_av_pp(k) − charge_from_av_pp(k)
int_credited_pp(k) = int_rate_guar() × av_pp_at(k, "AFT_PREM")
av_pp_at(k, "AFT_INT") = av_pp_at(k, "AFT_PREM") + int_credited_pp(k)
All three take a policy year. The Rechnungszins is credited per Versicherungsjahr and the monthly grid does not interpolate it: there is no monthly Deckungskapital in this tariff, and a model that invented one would be inventing a crediting rule no wording states.
This ordering is a standardization. No document in the corpus fixes the sequence of premium
credit, charge deduction and interest accrual, and it is the most consequential such choice in
the model: crediting interest on the opening balance alone changes year-one interest by the
whole of i × (S(1) − C(1)), 16,01 € of a 1 616,64 € closing balance on the anchor cell.
Two further conventions are std. charge_risk_pp and charge_admin_pp are struck on
start-of-year balances, or the recursion is circular — a risk charge on the post-charge
balance depends on itself. And charges are met from the premium where there is one and from
the Deckungskapital where there is not, which makes a Beitragsfreistellung cost something
instead of being free.
The Ratenzahlungszuschlag is the whole of what the Zahlweise does#
Instalments are not modelled, on the monthly grid as on the annual one: freq_load() charges the
loaded annual amount in the first month of the policy year, prem_due(t) is
duration_mth(t) % 12 == 0, and n_instalments stays documentation — so a monthly payer here pays
1,050 × the annual premium once a year, not twelve times.
That is a decision rather than an omission, and three things fix it. § 12 Abs. 1 VVG makes the Versicherungsperiode the year where premiums are not measured in shorter periods. The Deckungskapital the whole model turns on is defined at anniversaries and nowhere between them. And § 169 Abs. 3 VVG pays a mid-period exit the value struck at the end of that period — so a premium payable in advance for the year and a value earned for the year are the consistent pair, and splitting the premium without splitting the value would credit a policy with a year it did not pay for. Modelling the instalments would need an unearned-premium convention on mid-year exits that no source in this product’s corpus establishes.
KLV_DE_S is the contrast, and the reason this is a considered choice: there the echt / unecht
distinction turns on whether the Versicherungsperiode is genuinely monthly, two model points differ
in nothing else, and the instalment stream is therefore in the frame.
The § 169 Abs. 3 floor, carried as a difference#
The surrender value is floored at the Deckungskapital that results from spreading the charged acquisition costs evenly over the first five contract years — § 169 Abs. 3 Satz 1 VVG at article level R1, restated verbatim by five carriers [S1] [S4] [S8] [S9] [S11] REG-R28. The two accounts differ only in that charge, so the model carries the difference, not a second full recursion:
spread_diff_pp_at(k, "AFT_INT") = (Δ(k) + charge_acq_pp(k) − charge_acq_spread_pp(k)) × (1 + i)
with gamma and rho taken at the same euro amount in both std, which is what makes the
difference exact. Two consequences are not obvious. The difference is large in the first five
years — on the anchor cell the whole 1 275,00 € is taken in the first year against 255,00 € — and it
never returns to zero, because the spread account earns the Rechnungszins on what has not
yet been deducted. So the floor sits above the tariff Deckungskapital at every duration;
whether it binds depends on the Ansammlungsguthaben and Stornoabzug beside it.
The floor is the § 169 Abs. 3 Deckungskapital alone: profit shares sit on top of the
statutory minimum rather than inside it, the reading § 165 Abs. 2’s “surrender value …
including profit shares” supports R2. That lets both branches of
cv_pp(k) = max(cv_tariff_pp(k), cv_floor_pp(k)) be exercised on the anchor cell alone — the
floor binds through k = 3, at 2 646,84 € against a tariff 1 608,62 € in the first year, and stops
binding at t = 4. The alternative reading, in which the floor also carries the
Ansammlungsguthaben, is not implemented; it would bind at every duration.
The Stornoabzug is a flat percentage of the pre-deduction value with no duration term,
the shape § 169 Abs. 5 allows: a deduction is permitted only if agreed, quantified and
appropriate, and one for not-yet-amortised Abschluss- und Vertriebskosten is void R1 — a
duration-graded deduction unwinding over the first years would be exactly the void kind.
Whatever it is set to, cv_pp cannot fall below cv_floor_pp.
Beitragsfreistellung is an election, not a decrement#
Beitragsfreistellung is a deterministic election in the contractual policy year pup_year
— policy year k = pup_year − 1 — rather than a rate: a
scalar per-policy account cannot carry two sub-populations with different Deckungskapital,
and no source establishes a rate. Both § 165 VVG branches R2 are implemented and exercised:
Conversion (model point 7).
prem_pp(k) = 0from the paid-up year, the Deckungskapital is reset topup_value_pp()— the § 169 Abs. 3–5 value, 30 303,91 € against a zillmered 30 261,45 € — the Ansammlungsguthaben is untouched,spread_diff_ppgoes to zero because the two accounts have merged, andcharge_admin_ppswitches fromgamma_ratetogamma_pup_rate. No Stornoabzug is taken on this route std: Abs. 5 is drafted for a payout on Kündigung, and here the contract continues.Cash-out (model point 8). Where the paid-up annuity would fall below the Mindestversicherungsleistung — 5,45 € a month against a 30,00 € threshold std — § 165 has the contract cashed out at the surrender value including profit shares instead of made paid-up, so the whole surviving cohort leaves in the last month of policy year
k = pup_year − 2. That is one of the two placeslapse_ratereturns 1 rather than a rate, andlapse_rate_mthplaces the certainty in the anniversary month rather than spreading it geometrically: § 165 makes the cash-out fall at the end of the Versicherungsperiode, not a twelfth of the way into it.
pup_uplift(k) is booked in the transition year k = pup_year − 2, weighted by
pols_if(12 (pup_year − 1)), because that is the year whose roll-forward needs it; it is real money
— 28,39 € on point 7 — and publishing it is what lets check_av_roll_fwd() close there.
A Beitragsfreistellung is not a lapse: the paid-up contract keeps its guarantee vintage
and its guaranteed Rentenfaktor and pays a reduced benefit, while the surrendered one is
gone for cash. On point 7 the conversion itself moves no policy — lapse_rate in policy year 9 is
the ordinary duration-9 table rate of 3,5 %, not 1. What is not true is that surrender ceases:
a beitragsfrei contract keeps its § 168 VVG Kündigung right, so claims_lapse stays
positive from the paid-up year on, 764,60 € in policy year 10.
The Rentenbeginn: three things at one instant#
Everything happens at the end of the last accumulation month t = 12n − 1, where
n = aufschub_y is the deferment period in years, on the survivors of that month’s decrements. It is
an anniversary, so every quantity below is the annual-step model’s to the last bit:
capital_gross_pp = av_pp_at(n−1, "AFT_INT") + av_sur_pp_at(n−1, "AFT_INT")
capital_conv_pp = max(guar_capital_pp, capital_gross_pp + val_reserve_pp)
annuity_rate_appl = max(annuity_rate_guar, annuity_rate_curr)
annuity_guar_mth_pp = capital_conv_pp / 10 000 × annuity_rate_appl
On the anchor cell that is max(0,00; 58 788,98 + 881,83) = 59 670,82 € converted at
max(28,00; 32,00) = 32,00 into a garantierte Rente of 190,9466 € a month.
The applied factor is a written option on the insurer’s own future annuity tariff, and the
deterministic path does not price it. Both branches ship: the current factor wins on the anchor
cell, and on point 13 the guarantee binds over a low scenario while its guar_capital_pp
floor of 60 000,00 € binds at the same time. A model applying the guaranteed factor alone
understates the anchor’s annuity by 12,5 %, the annuity scaling linearly in f.
val_reserve_pp is the Bewertungsreserven crystallisation, which § 153 Abs. 3 Satz 2 VVG makes
hälftig and which § 153 Abs. 4 VVG puts, for annuities specifically, at the Beendigung der
Ansparphase R4; [S4] § 3 Abs. 2 and [S15] apply it in those terms. The
rate is a placeholder, and the continuing participation during the payout phase that the
same source establishes is not modelled. The commuting policyholders receive
capital_conv_pp, the same capital the annuitants convert: the corpus gives no basis for
paying them less, and inventing one would be a charge no source supports.
The Rentengarantiezeit is paid to the dead#
Inside the guarantee window the instalment is due whether or not the annuitant is alive R17 R24, so the annuity is weighted by the annuitised count and not by survivors:
pols_annuity(t) = pols_annuitization(12n−1) for 12n ≤ t < 12n + 12m
= pols_if(t) for t ≥ 12n + 12m
The window is 12m guaranteed instalments, not m annual lumps — which is what a
Rentengarantiezeit is, the Rente being a monthly payment, and a thing the annual grid could only
approximate. On the anchor cell the two counts differ over t = 204 … 323 and coincide from
t = 324. In policy year 27, the last guaranteed year, pols_annuity holds at 0,336143 in all
twelve months while pols_if falls to 0,307366 by the last of them, so the year’s outgo is the full
862,65 € and not the roughly 800 € a survivor weighting would give.
Because the count is fixed inside the window, the annual and monthly grids pay the same money to the cent there; they part company from policy year 28, where twelve monthly measurements of a falling count are not one annual one, and that is where the whole −369,14 € of the conversion sits.
check_annuity_guarantee() states the identity as
pols_annuity(t) = max(pols_if(t), 1{12n ≤ t < 12n+12m} × pols_annuitization(12n−1)), which holds
because pols_if(t) ≤ pols_annuitization(12n−1) throughout the payout phase — and stating it that
way is what makes the check independent of the definition it checks.
No death benefit after the Rentenbeginn — a modelled choice, not an evidential gap#
db_pp(k) is zero for every k ≥ n, on every model point, and a test asserts it. The reason has
changed. Beitragsrückgewähr in der Rentenbezugsphase — the refund of premiums less instalments
received, on death after the Rentenbeginn — was recorded as unmentioned by any source in this
corpus. It is in [S4] § 1 Abs. 5, offered as an alternative to the Rentengarantiezeit: the premiums
paid less rider premiums and less annuities already received, the deduction taken only at the
inception-guaranteed annuity level, with the claim extinguishing once instalments exceed premiums,
and with the surplus attributable to it accumulated at interest until it is paid (§ 3 Abs. 7).
Implementing it means a second post-Rentenbeginn benefit path, a running refund balance and a new
decrement interaction, all of which move the worked example and the golden tests — so it is
recorded as a known omission and left out of this pass. The two mechanics that are modelled or
deliberately off remain the Rentengarantiezeit [S1] [S4] [S9], which is modelled, and the
survivor’s-annuity rider [S10], which is off and which in any case begins only after a guarantee
period expires. Deaths still happen in the payout phase: they move pols_if, end the annuity
outside the guarantee window and carry an expense_claim_pp settlement cost, and they pay no benefit.
Inputs are external files#
The eight input CSVs live in this directory, beside run.py — not inside the model
folder. RV_DE_S/ holds nothing but formulas:
products/klassische_rentenversicherung/
model_point_table.csv mort_table.csv decl_rate_table.csv <- inputs live here,
rentenfaktor_table.csv charge_table.csv lapse_table.csv beside run.py and
freq_load_table.csv param_table.csv run.py the four documents
RV_DE_S/ <- formulas only
__init__.py _system.json (model docstring)
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, and 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 however many policies are projected, and the
conventions suite 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 wherever the
repository is checked out.
Reference |
Cells |
File |
What it carries |
|---|---|---|---|
|
|
|
Fourteen policies, thirty columns. Point 1 is the worked-example anchor cell. The one file exempt from the |
|
|
|
|
|
|
|
The declared laufende Verzinsung by scenario and calendar year, 2005–2060, clamped at both ends. |
|
|
|
The aktueller Rentenfaktor by scenario and attained age at Rentenbeginn, rising 2,5 % per year of age. |
|
|
|
Two charge sets, |
|
|
|
Annual surrender by contractual policy year ( |
|
|
|
The Ratenzahlungszuschlag: 1,000 / 1,020 / 1,030 / 1,050 std, and |
|
|
|
Every scalar that is neither a charge nor a rate table — the four expense levels and their inflation, |
The trade-off: the model is not portable on its own — copy RV_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.mort_file at another
same-schema file and the projection follows, with no formula change. Every file but the model
point table carries a per-row provenance column, this library’s second ruling — an
assumption says on its own row where it came from, and the conventions suite checks it.
The published identities#
Nine check_* cells travel with the model, each taking no argument and returning a bool over the
whole frame, each with a residual beside it. The residual’s argument follows its cells’ clock:
five monthly identities carry check_*_resid(t) over policy months, and four that state an annual
account — the two account roll-forwards, the premium split and the § 169 floor — carry
check_*_resid(k), one per policy year. A monthly residual for the Deckungskapital
roll-forward would first have had to invent a monthly reserve, which is the error the two-clock split
exists to make impossible. (check_annuity_conv() is a scalar identity evaluated at one point, and
is the ninth.)
check_net_cf() — delib’s first ruling, the identity in one line:
net_cf(t) = premiums(t) − claims_death(t) − claims_lapse(t) − claims_commutation(t)
− annuity_payments(t) − expenses(t)
rebuilt from result_cf()’s own columns rather than from the cells, together with
liability_cf(t) = −net_cf(t) exactly. It fails if a published column and the headline number
ever stop being the same arithmetic — the failure it exists to catch.
Check |
What it closes |
|---|---|
|
the cash flow statement above, and the sign convention |
|
|
|
exits summed directly plus |
|
|
|
|
|
every euro of premium is saved or spent on a charge, and every euro of charge is met from the premium or from the account |
|
|
|
the conversion arithmetic, |
|
the Rentengarantiezeit weighting, stated with a |
Modules that are implemented and off in the base run#
Module |
Switch |
Off value |
What it does when switched on |
|---|---|---|---|
Dynamik (Anpassungsversicherung) |
|
|
Grows the scheduled premium by |
Beitragsfreistellung |
|
|
The § 165 election, in both branches — conversion on point 7, cash-out on point 8 R2 |
Kapitalwahlrecht |
|
|
The commutation take-up. |
Death benefit including surplus |
|
|
Adds the Ansammlungsguthaben to the death benefit, the “premiums plus the attributable Überschussbeteiligung” form. On on points 4 and 12 R24 |
Guaranteed contract value |
|
|
The minimum guaranteed contract value floor at conversion. Binds on point 13 at 60 000,00 € [S9] |
Payout-phase administration charge |
|
recorded at 1,5 %, never applied |
Would deduct a charge from each instalment. It is not applied because the Rentenfaktor is exogenous here and already carries the tariff’s payout loading; deducting again charges it twice |
|
— |
diagnostic only |
The monthly annuity-due factor on the shipped proxy at the guarantee basis, 273,7821 on the anchor, so the Rentenfaktor the proxy would imply is |
Not implemented at all, and named here rather than left to be discovered: no Bonusrente ledger; no Zuzahlung (gap 15); no survivor’s-annuity or BU rider [S10]; no § 163 VVG adjustment of the guaranteed Rentenfaktor, recorded as a model risk instead R3 R17; no dynamic surrender, because the surrenderer forfeits a guaranteed Rentenfaktor struck on old bases and a rate-gap formula does not capture that; no premium-default path, although § 166 VVG makes German lapse a three-way decrement in reality; no Wiederinkraftsetzung; no continuing Bewertungsreserven participation in the payout phase; and no tax, so the Ertragsanteil R5 and the Halbeinkünfteverfahren R6 appear in the documents and not in the model.
Sign convention#
net_cf is income positive — premiums in, benefits, annuity instalments and expenses out —
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. A Solvency II best estimate is
Σ v(t) × liability_cf(t) plus a risk margin REG-R1 REG-R4; nothing here discounts.
av, av_sur, prem_to_av, int_credited and bonus_credited are state movements
reported, not cash flows summed: they move money inside the contract, never cross the boundary,
and move once a policy year, so they are columns of result_pols() rather than of the cash flow
statement. The six that do cross it are premiums, the three claims_*, annuity_payments and
expenses — exactly what check_net_cf() reconciles, and the whole of result_cf()’s flow
columns.
Naming#
Cells follow lifelib’s basiclife/BasicTerm_S first and savings/CashValue_SE second
wherever those models have an analogue: pols_* for policy counts, av_* for account
values, plural nouns for cash flows, *_rate for rates, *_pp for per-policy amounts,
claims(t, kind) with an uppercase kind string, av_pp_at(k, timing) for the within-year
reads of the annual accounts and pols_if_at(t, timing) for the within-month reads of the
population. The technical notes use compact actuarial symbols; the full mapping lives in the
Projection Space docstring. Five cases needed care:
Notes |
Cells |
Why |
|---|---|---|
|
|
Two different quantities with deliberately similar names. |
|
|
A charge is a tariff deduction that moves money inside the contract and produces no cash flow; an expense is the insurer’s own outgo and is a cash flow. |
|
|
Two accounts, not one balance split in two. The Deckungskapital carries the guarantee and is credited at |
|
|
They differ inside the Rentengarantiezeit and nowhere else. |
|
|
The library’s two speeds, not two spellings of one rate. The unsuffixed name is the annual rate of the policy year, which is what the notes tabulate and |
The chassis this model shares. KLV_DE_S (products/kapitallebensversicherung) is the
same Überschussbeteiligung and Deckungskapital machinery with a maturity benefit where
this one has a conversion, and is the primary home of the four-component surplus split.
Sofort_DE_S (products/sofortrente) is this model’s payout phase as a product in its own
right — which is why an immediate-annuity document is direct evidence for a deferred
contract’s conversion basis [S13] [S16]. Index_DE_S and FRV_DE_S replace the crediting
mechanic and keep the conversion. Names that mean the same thing across all of them:
model_point, proj_len, proj_len_y, t_start, k_start, duration, duration_mth,
is_anniv, policy_year, age, calendar_year, pols_if, pols_if_at, pols_death,
pols_lapse, mort_rate, mort_rate_mth, lapse_rate, lapse_rate_mth, prem_pp, premiums,
av_pp, av_pp_at, prem_to_av_pp, claims, expenses, net_cf, liability_cf, result_cf,
result_cf_annual.
Standardizations used#
Everything below is std — chosen where the sources vary, are proprietary or are silent.
Value |
Rationale |
|
|---|---|---|
Mortality proxy |
Gompertz |
DAV 2004 R is DAV property and is not redistributable R12 R13. The proxy keeps the generational structure, which is the part that matters, and none of the values; the improvement shape is a deliberate simplification of the Starttrend / Zieltrend construction, documented as one rather than presented as a replication |
|
1.15, above one on purpose |
For an annuity, prudence means assuming mortality lower than expected, so the first-order table sits below best estimate. Only the level margin is reproduced; the real one runs in level and trend |
Rentenfaktor paths |
|
Anchors chosen so both branches of |
Declared rate paths |
|
Inside the only public market-average pair the library has for 2025 REG-R53; a scenario rather than a forecast. A carrier’s 2026 declaration is now on the record and is higher — 3,00 % total credited interest before the Rentenbeginn, 3,35 % during it [S15] |
Surplus on the Ansammlungsguthaben |
the full declared rate on its own balance |
No source splits the side account’s crediting from the main declaration |
|
4,0 % of premium; 20 bp; 30 bp p.a. |
No German carrier publishes a charge loading for this product (gap 14). The premium-free rate is higher because a paid-up contract still bears administration cost |
Use of the Höchstzillmersatz cap |
the cap itself, 25 ‰ / 40 ‰ |
The cap is statutory — § 4 Abs. 1 Satz 2 DeckRV, “Der Zillmersatz darf 25 Promille der Summe aller Prämien nicht überschreiten” R7 REG-R16, restated by four carriers as “2,5 % der Beiträge” [S1] [S4] [S8] [S9]; charging exactly it is the standardization, and it is what makes the year-one Sparbeitrag thin and the § 169 Abs. 3 floor bite |
|
2,0 % on |
§ 169 Abs. 5’s three conditions are cited R1. The earlier rationale was wrong: a duration-graded deduction is not what Abs. 5 voids — it voids a deduction for unamortised acquisition costs — and the retrieved wordings show duration tapering is ordinary market practice [S11] § 34 Abs. 4–5. Observed forms are none [S8] [S9], a flat 250 EUR [S4], or tapering percentages [S11]. The flat rate is a simplification of that spread |
|
30,00 € a month |
The § 165 Mindestversicherungsleistung requirement is statutory, its level contractual; two carriers set 25,00 € a month [S4] [S9] and one 600,00 € a year for a partial surrender [S8]. 30,00 € is chosen so one model point trips the cash-out branch |
|
1,5 %, recorded and not applied |
The Rentenfaktor is exogenous and already carries the tariff’s payout loading |
|
1,5 % of the accumulated value |
The hälftige participation (§ 153 Abs. 3 Satz 2) and the Rentenbeginn crystallisation (§ 153 Abs. 4, annuities only) are statutory R4 and applied by [S4] and [S15]; no amount, ratio or reserve level is established anywhere |
Überschussrente |
|
The three payout systems and their directions are established R19 R20 R24; no level, rate or split is |
Expenses |
400,00 € acquisition, 45,00 € p.a. accumulation, 30,00 € p.a. payout, 120,00 € per settlement, 2,0 % inflation |
No German carrier publishes an expense assumption (gap 14) |
|
1,000 / 1,020 / 1,030 / 1,050 |
No carrier’s Ratenzahlungszuschlag was established (gap 14) |
Surrender table |
6,0 % falling to 3,0 %, with a 6,0 % step at duration 12 |
No German Stornoquote for this product was established (gap 20). The duration-12 shape is argued from the § 20 Abs. 1 Nr. 6 EStG threshold; every level is a placeholder |
Within-year order and charge incidence |
premium, then charges, then interest; |
No document in the corpus fixes the sequence. Start-of-year incidence keeps the recursion acyclic; premium-first incidence is what makes a paid-up contract pay for itself |
§ 169 Abs. 3 floor scope |
the Deckungskapital alone, and no Stornoabzug on the paid-up route |
Profit shares sit on top of the statutory minimum, the reading § 165 Abs. 2 supports; the alternative is named and not implemented. Abs. 5 is drafted for a payout on Kündigung, and on the paid-up route the contract continues |
Annuity timing |
one instalment at the start of each month, in advance |
The basis is established — “monatlich, jeweils zum Monatsersten” [S9] § 1 Abs. 1 — and the monthly grid now models it as stated, so the annual-grid compression this row used to record is gone. What remains std is the exact payment date within the month, which no source fixes |
Kapitalabfindung amount, decrement order, no notice period |
|
The corpus gives no basis for paying commuters less. No source fixes the decrement order. Notice periods are established and are not modelled: three years, or twelve years / five months, at Zurich [S4] § 2 Abs. 2–3; twelve years at CosmosDirekt [S8]; two months at Mecklenburgische [S14] |
Beitragsfreistellung |
a deterministic election on the model point |
A scalar account cannot carry two sub-populations, and no rate is established |
|
30 % base |
The real decision is a tax comparison and this model computes no tax, so the rate stands in for a calculation it does not perform (gap 20) |
|
121; 1e-9 relative; all fourteen |
The terminal age fixes |
The only quantities that are not standardizations are the Höchstzillmersatz ceiling of § 4
DeckRV R7 REG-R16, the five-year spread of acquisition costs in § 169 Abs. 3 R1 REG-R28,
the 2005 mortality base year R13, and the structural rules: the Deckungskapital recursion
[S8] [S11], max(garantierter, aktueller) [S9] [S14] [S18], the conversion capital including
surplus and Bewertungsreserven and floored at the guaranteed contract value [S9], the § 165
branches R2, the § 169 floor and Stornoabzug conditions R1, the death-benefit forms
[S4] [S8] [S9] R24, and the Rentengarantiezeit [S1] [S4] [S9].
Where the model diverges from a retrieved document#
The 2026-08-30 retrieval pass established four things the model does not implement. None of them was changed in that pass, because each moves the worked example and the golden tests, and that is a deliberate decision rather than a documentation edit. They are listed here so a user recalibrating the model knows where to start.
What a document says |
What the model does |
|---|---|
Beitragsrückgewähr während der Rentenzahlungszeit is offered as an alternative to the Rentengarantiezeit: premiums paid, less rider premiums, less annuities already received at their inception-guaranteed level, the claim lapsing once instalments exceed premiums [S4] § 1 Abs. 5 |
|
The Rentenfaktor market range for 2025 is 24,33–27,18 guaranteed and 27,27–30,40 current, by deferment term to age 67 R24; the current-factor average was 25,97 in 2022 R19 |
|
A carrier’s Überschussverteilung 2026 credits 3,00 % in total before the Rentenbeginn and 3,35 % during it, against 2,25 % and 2,5 % for 2025 [S15] |
|
The Stornoabzug is either absent [S8] [S9], a flat 250 EUR waived at age 62 or after twenty years [S4], or percentages of the Deckungskapital tapering linearly to nil over the last ten years of the Aufschubzeit [S11] |
|
One further difference is a refinement rather than a divergence: [S9] applies
max(garantierter, aktueller) at every monthly instalment, while the model applies it once at
the Rentenbeginn. On a deterministic path with a single conversion event the two coincide — and the
monthly grid now at least puts the two on the same footing, the instalment being a real row of the
frame rather than a twelfth of an annual lump.
Tests#
tests/test_klassische_rentenversicherung_de.py asserts the notes’ worked example — all
seventeen accumulation rows, k = 0 … 16, and the six sampled payout rows k = 17, 26, 27, 39, 54
and 70, read off result_cf_annual(), money to the cent and pols_if to six decimals — the
twelve months of policy year 1 on the monthly frame beside them, the frame’s shape on the 0-based
month index (list(index) == list(range(852)) on the anchor, index[-1] == proj_len() - 1 on every
point tested), the totals at full precision against the rounded-cell sums, the four columns the
conversion moved against the annual-step model’s own totals, the three independent rebuilds below
the table, the closure split, both documented variants (the Einmalbeitrag form and the 2,75 %
legacy vintage), all nine check_* identities with their residuals — monthly for five, per policy
year for four — and one test per listed modeling pitfall: the declared rate containing the guarantee, the within-year
order, max(garantierter, aktueller), the Rentengarantiezeit weighting, Beitragsfreistellung
against lapse, charges against expenses, the § 169 Abs. 3 floor, the Stornoabzug’s shape, the
two mortality bases, the generational surface, the zero net amount at risk, the unapplied payout
charge, the absent post-Rentenbeginn death benefit, the Kapitalwahlrecht leaving no account
behind, the guarantee vintage, unisex pricing, the Beitragssumme surviving a
Beitragsfreistellung, and the untruncated payout phase.
The conversion added its own assertions rather than a separate section: that the annual layer is
unchanged — every account balance, the § 169 floor, the surrender value, the conversion capital
and the premium income equal to the annual-step model’s — that result_cf_annual() regroups the
monthly frame rather than reprojecting it, that both decrement rates compound back to the year
rather than dividing by twelve, that the two certainties (the terminal q = 1 and the § 165
cash-out) fall in the anniversary month, that the Rentengarantiezeit is 12m instalments, and
that the annuity is one instalment paid in advance each month.
The whole-model-point-table sweep is not here: the conventions suite owns the 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_klassische_rentenversicherung_de.py -q
python -m pytest lifelib/libraries/delib/tests/test_model_conventions_de.py -q -k RV_DE_S