Implementation Notes#
Status: Draft, 2026-08-29. Built from
products/riester_rente/technical-notes.md; the product it
implements is specified in product-spec.md; the sources both rest on
are listed in sources.md.
This is a mechanics demonstration, not a pricing or reserving result. What is sourced is the statute: who is zulageberechtigt R7; the Grundzulage, Kinderzulage and Berufseinsteiger-Bonus structure R9; the § 86 Mindesteigenbeitrag with its 4 % rate, 2 100 € ceiling, 60 € Sockelbeitrag floor and proportional Kürzung R10; the ZfA payment lag R11; the Beitragserhaltungszusage, the 30 % Teilkapitalauszahlung cap, the five-year floor under acquisition-cost spreading and the Wechselrecht R1; the Kleinbetragsrenten-Abfindung R15; and the consequences of a Kündigung R14. What is not sourced is what a carrier chooses. Every statutory citation above was read in the canonical XML in the 2026-08-30 provenance pass — this library was drafted with no retrieval available at all and re-verified against the primary documents afterwards — but on the carrier side that pass found wordings, not levels: no declared rate, no Rentenfaktor level, no charge scale representative of the market and no Stornoquote was established at any house for any year [S5] [S7] [S8] [S16]. Two Rechnungszinsen and one full numbered charge basis are now in hand from single tariffs [S4] [S6], and the Standardizations used table below says at each row where the composite differs from them; one tariff is not a market, so every carrier level here stays a std standardization. The DAV tables the decrements stand in for (DAV 2008 T, DAV 2004 R) are the property of the Deutsche Aktuarvereinigung, are not public, and are cited by name rather than shipped REG-R47 REG-R48 REG-R49. Replace the charge, surplus and decrement tables with company data before drawing any conclusion from the numbers.
Run it#
python products/riester_rente/run.py
python products/riester_rente/run.py 11 # the cell on which the guarantee binds
python products/riester_rente/run.py 5 # the cell that commutes rather than annuitising
Three lines to the same thing:
import modelx as mx
model = mx.read_model("products/riester_rente/Riester_DE_S")
model.Projection[1].result_cf()
Projection takes a point_id; Projection[1] is the worked-example anchor cell — a female
life aged 50 at the 1 January 2027 valuation date, three contract years in force, Rentenbeginn
at 67, one child born in 2010. result_cf() returns a DataFrame indexed by the 0-based
projection month t, 0 … 731 — proj_len() = 732 rows — with fourteen columns;
result_cf_annual() sums it into projection years, which is the view the technical notes
print and the one directly comparable to the annual-step model this replaced; and
result_acct() is the annual state, where the account, the guarantee accumulator, the
interest credit and the subsidy chain live. model.doc describes the product and its two
phases, model.Projection.doc maps the notes’ symbols to the cells names, and
model.Data.doc says what each input file is and what a replacement must preserve.
Which clock a cells is on, and what the monthly grid buys#
The argument says which. t is a projection month on everything that happens on a date:
the in force and the three decrements, the claims, the expenses, the commission and the
Rente instalments. k = proj_year(t) = t // 12 is a projection year on everything the
contract and the AltZertG state per year: the Eigenbeitrag, the Zulage and its ZfA lag, the
two charges, the declared rate, both account balances, the Beitragsgarantie accumulator and
the conversion. proj_year(t) is the only place the two meet.
mort_rate(t), lapse_rate(t) and transfer_rate(t) are the annual rates of the year
the month falls in, and mort_rate_mth, lapse_rate_mth and transfer_rate_mth are the
geometric twelfths the recursion applies, so twelve months compound back to each annual rate
exactly and pols_if(12k) is the annual-step model’s pols_if(k) to the last bit. The whole
accumulation is therefore unchanged: every contribution, both balances, the guarantee
accumulator, the capital at Rentenbeginn, the Garantielücke and the Kleinbetragsrente
verdict are bit-identical on all thirteen model points.
What the finer grid buys is two things. The Rente. The AltZertG requires a lifelong
monthly benefit and the Rentenfaktor is quoted in euro a month; the annual-step model
booked twelve instalments together at the start of each payout year on that year’s opening
count, which paid a life that died in the first month of a year for the whole of it.
annuity_month_pp() is now paid to whoever is alive that month — 92,89 € a month on the
anchor — which takes 361,74 € off its annuity outgo and 573,50 € off model point 12’s,
whose Rentengarantiezeit is zero and whose count therefore falls from the first instalment.
The guarantee window itself becomes 12m instalments. And the split of the exits. In the
annual model the three accumulation decrements ran in sequence at one year end — mortality
first on the whole cohort, then surrender on its survivors, then transfer on what two
decrements had already thinned. Month by month they compete, which moves 5,62 € off the
anchor’s death outgo and 2,64 € off its surrender outgo and puts 8,30 € onto its transfers,
while the survivorship at every anniversary is unchanged. Expenses fall 11,72 € for the
related reason that a policy exiting in the fourth month of a contract year now bears four
twelfths of that year’s maintenance rather than all of it.
What the grid deliberately does not change is the contribution. The Ratenzuschlag is
how a German tariff prices a fractionated payment mode — it loads the amount, not the
contribution year — so prem_due(t) puts the whole year’s Eigenbeitrag in the first month
of a projection year whatever prem_freq says. The Zulage is not fractionated at all: the
ZfA pays the provider once a year, and it lands in the same month.
The Zulage is a contribution, and it arrives a year late#
The Zulage is paid by the Zentrale Zulagenstelle für Altersvermögen to the provider,
credited to the contract, counted in the Beitragsgarantie, invested, and taxed at the end
like any other contribution R8 R11; it never reaches the saver’s bank account. So zulagen
is a positive income column of result_cf(), published beside premiums and never folded
into it: on model point 5 the state pays 1 926,26 € against the saver’s 609,80 € over the whole
projection — 76 % of the contribution — and a statement that netted the two could not say so.
Three cells carry the subsidy and they are three different amounts, 175,00 €, 175,00 € and
475,00 € on the anchor at k = 2: zulage_entitlement_pp(k), the full § 84/85 entitlement of
contribution year k R9; zulage_granted_pp(k), the same after the § 86 proportional
Kürzung R10; and zulage_pp(k), the cash credited in k, which is
zulage_granted_pp(k − 1) because the ZfA pays in arrear R11. All three are annual, and the
monthly grid gives the payment a month rather than a different lag.
There are two lags and they are different lags. income_ref(k) looks back one calendar
year, because § 86 strikes the minimum on the previous year’s earnings; zulage_pp(k) looks
back one projection year, because the ZfA pays in arrear. One offset applied twice
reproduces neither. The consequence is in the anchor’s frame: between k = 2 and k = 3
zulagen falls from 455,13 € to 164,93 € while premiums rises from 1 507,08 € to
1 515,34 €, because the § 86 minimum is 4 % of income less the entitlement — a Zulage that
stops is a contribution the saver must make good.
zulage_pp(k_conv()) is not zero: contributions stop at k_conv() − 1 and the Zulage they
earned lands in the conversion year — 134,33 € on the anchor at k = 17, in month 204 — where it is
credited, guaranteed and converted before the guarantee is tested. check_zulage_lag() pins
all three cases. The § 10a Sonderausgabenabzug and the Günstigerprüfung top-up have no
cells and no column R6: only the Zulage reaches the policy, the § 10a advantage being a
personal tax refund, and modelling it here would credit the contract with money that never
arrives.
The 100 % Beitragsgarantie, and the one moment it is tested#
guar_pp(k) accumulates contributions — the Eigenbeitrag, the Zulagen credited, and any
unsubsidised contribution — less the biometric carve-out, and never accrues interest, the
Beitragserhaltungszusage being nominal R1. It is annual, like the contributions it counts:
G(k+1) = G(k) + E(k) + Z(k) + contrib_extra_pp − κ(k)
κ(k) = min(rider_prem_pp, 0.20 · (E + Z + extra + rider))
It counts Zulagen credited, in the year they are credited, not entitlements in the year
they are earned; it counts unsubsidised contributions, because the undertaking is on the
Altersvorsorgebeiträge paid in and does not distinguish the tax pools R1 — on model
point 8 it steps by 3 000,00 € a year against an entitlement stuck at 175,00 €; and the
carve-out is capped, so model point 9’s 400,00 € rider premium on a 1 200,00 €
contribution carves out 0.20 × 1 200,00 = 240,00 € and no more REG-R43.
The guarantee is tested exactly once, at k_conv(), where capital_conv_pp() is
max(account_conv_pp(), guar_pp(k_conv() + 1)). On model point 11 — a seven-year deferral on
the low declared-rate path — the account reaches 20 481,72 € against a 21 000,00 € guarantee,
so garantieluecke_conv_pp() = 518,28 €, 2,5 % of the capital, funded out of the insurer’s
own resources: the product’s signature output, and a Riester model on which it is never
positive has demonstrated nothing.
garantieluecke_pp(k) is published at every k and is a diagnostic: the anchor opens
358,94 € under water, peaks at 567,69 € and closes at k = 6, the normal state of a charged
contract. db_pp, cv_pp and transfer_value_pp are deliberately not floored at it —
the guarantee is a promise about Rentenbeginn, not about a policy that leaves before it.
Whether the Schlussüberschussanteil and Bewertungsreserven share may close a shortfall is
still unsettled (gap 9) — AltZertG § 1 Abs. 5 puts all three inside the gebildetes Kapital
for a transfer, and the guarantee clause of § 1 Abs. 1 Satz 1 Nr. 3 names no components at
all, so the retrieved documents are silent rather than contrary. Counting them, as
account_conv_pp() does, is the provider-favourable reading, and on the anchor’s own deferral at the low rate that choice is the difference
between a Garantielücke of zero and one of 506,56 €.
The account is two balances and one credited rate#
dk_pp is the Deckungskapital, surplus_acct_pp the Überschussguthaben, av_total_pp their
sum. All three are annual — one contribution, two charges and one interest credit a year — and
the split is guarantee accounting, not two investment strategies: the whole account
grows at the declared j(k) and D is carved out of it as the part i guarantees.
int_guar_pp(k) = i · (D(k) + S(k))
int_surplus_pp(k) = (j(k) − i) · (D(k) + S(k)) + j(k) · U(k)
int_credited_pp(k) = j(k) · (D(k) + S(k) + U(k)) exactly
j already includes i REG-R53. Adding the declared rate to the guaranteed one is the
German arithmetic error this arrangement makes impossible; setting j = i collapses the
Deckungskapital leg of int_surplus_pp to zero, which is the check that they are not being
added. int_credited is reported, not summed into net_cf:
it moves money inside the account, not across the insurer’s boundary, and on the anchor it
totals 7 544,45 € — adding it would report the cell’s undiscounted deficit as 90,49 € instead
of 7 453,96 €. On the monthly grid it is not a result_cf() column at all: it moves once a
year, like the two balances it moves between, so it sits in result_acct() with them.
Charges, and a Sparbeitrag that can go negative#
The AltZertG requires acquisition costs to be spread over at least five years R1 — a
tighter cap on Zillmerung than anything the VVG imposes on a Schicht-3 contract. So
acq_charge_pp(k) is one fifth of acq_charge_rate × beitragssumme in contract years 1 to 5
and zero after: on the anchor, 168,00 € at k = 0 and k = 1 and nothing from k = 2.
It never appears in result_cf(), being a deduction before the account, but 168,00 € of the
488,90 € rise in the Sparbeitrag between k = 1 and k = 2 is the charge ending rather
than the contribution rising.
The charge runs whether or not contributions are paid: on model point 10, beitragsfrei
from k = 3, prem_to_av_pp(3) = 175,00 − 168,00 − 19,00 = −12,00 € and the
Deckungskapital falls — the cost-spreading rule, not a modelling artefact, and the reason
prem_to_av_pp is documented as possibly negative rather than clamped at zero.
The Ratenzuschlag is a charge and never a credit: the saver pays E(k) × φ, only E(k)
reaches the Sparbeitrag base and the guarantee, contrib_total_pp is the cash received
and so carries the loading, and admin_charge_pp deducts it straight back out while striking
its percentage on the unloaded E + Z + extra. Deducting it in both places — which the
notes’ drafted S = C − K_a − K_v with an unloaded C did — makes the Sparbeitrag fall with
the payment frequency, the opposite of the product fact.
It is also why the contribution keeps the annual grid on a monthly frame. φ prices a
fractionated mode by loading the amount rather than by moving the contribution year, so
prem_due(t) puts the whole year’s Eigenbeitrag in the first month of a projection year
whatever prem_freq says; splitting the cash into instalments and keeping φ would charge
for the deferral twice. The Zulage is not fractionated at all — the ZfA pays the provider once
a year — so it lands in the same month.
Conversion: the Rentenfaktor, the lump sum and the Kleinbetragsrente#
ä = Σ_{k ≥ 0} v^k · k p(x(T), τ(T)) − 11/24 first-order basis, factor 1.00
R_c = (1 − rentenfaktor_margin) · 10 000 / (12 · ä)
R = max(R_g, R_c)
On the anchor ä = 20,87222879 at age 67 in calendar 2044 — the basis is generational, so
the conversion happens on its own conversion year’s mortality — and R_c = 27,947822,
below the guaranteed R_g = 29,00, so the guaranteed factor applies. The two are
independent, one a contract term struck at inception and the other a function of the shipped
table, and the model states which is authoritative rather than leaving it to be inferred. The
whole payout loading sits in rentenfaktor_margin (30 % std) rather than being split
between the factor and each instalment, which would double-count; payout administration is an
explicit expense_annuity flow instead. check_conversion() asserts
rentenfaktor_curr() · 12 · ann_factor() = (1 − rentenfaktor_margin) · 10 000 on every model
point, whether or not the current factor applies. teilkapital_pp() is the elected share
clamped at the statutory 30 % R1: 13 726,91 € on the anchor, leaving 32 029,47 € to annuitise
at 92,885458 € a month.
The Kleinbetragsrente commutation is computed, not assumed. is_kleinbetrag() tests
the annuity the model has actually produced, so the commutation rate on a book is an
output — which, given how much of the German book runs at the Sockelbeitrag, is the
right way round. Two of the three standardizations inside it were contradicted by documents
read on 2026-08-30 and have deliberately not been changed. The threshold is 1,5 % of the
monthly Bezugsgröße, § 93 Abs. 3 Satz 2 Nr. 1 EStG R15 — 59,33 € on the Bezugsgröße
this model uses, against the 39,55 € it implements. And the test is applied to the annuity
payable after the elected lump sum, where the GDV model wording says “Eine Abfindung
erfolgt nicht, wenn die Leistung nur aufgrund einer Teilkapitalauszahlung … auf eine
Kleinbetragsrente sinkt” [S2], so it belongs before it (gap 7). Both errors run the same
way — too few commutations, too long a tail — and correcting either moves the worked example
and the golden tests, which is why neither was made in a provenance pass. The third
standardization stands: the threshold is held flat in nominal terms while the Bezugsgröße
is reset annually, which on a long deferral understates the commutation rate further. What
the documents confirm is that commutation is the provider’s option, which is how the
model exercises it [S2] [S4]. Model points 4, 5, 10 and 13
commute. A commuted contract pays claims_commutation and no claims_lumpsum and no
claims_annuity — an Abfindung is the whole capital in one payment — and pols_if is zero
from t_conv() + 1 because it discharges the contract outright. The verdict is unchanged by
the monthly grid on every one of the thirteen points, the capital it tests being an annual
quantity struck at Rentenbeginn.
The payout phase, and the Rentengarantiezeit#
The projection does not stop at Rentenbeginn: the account is extinguished there and the
lifelong Leibrente runs to omega_age = 110 on the second-order generational annuitant
basis, because a model that stopped at conversion would not have modelled the benefit the
AltZertG requires R1. The Rente is paid one instalment a month, which is what the
AltZertG requires and what the Rentenfaktor quotes: annuity_month_pp() = 92,885458 € on the
anchor, paid in advance from t = t_conv() = 204 to whoever pols_annuity_pay(t) says is
paid. annuity_pp(k) is the annual reporting figure the twelve sum to and is nobody’s payment.
The Rentengarantiezeit changes who is paid, never how much: pols_annuity_pay(t) is
pols_conv() while t − t_conv() < 12 · rentengarantie_years() — 120 instalments on the
anchor, t = 204 … 323 — and pols_if(t) afterwards, and annuity_pp(k) does not read
rentengarantie_years() at all.
k |
17 |
18 |
… |
26 |
27 |
28 |
|---|---|---|---|---|---|---|
|
0.767588 |
0.762677 |
… |
0.701403 |
0.690013 |
0.677530 |
|
0.767588 |
0.767588 |
… |
0.767588 |
0.690013 |
0.677530 |
|
855,57 |
855,57 |
… |
855,57 |
762,71 |
748,18 |
claims_annuity is exactly 855,57 € in each of those ten years although a tenth of the
annuitants have died — and those ten years are the annual-step model’s to the cent, because
inside the guarantee window the count is fixed and twelve monthly instalments on a fixed count
are one annual payment on it. It is the years after the window that the finer grid moves:
762,71 € against 769,11 € at k = 27, because the instalment now stops with the month of death
rather than being paid for the whole year of it. Over the anchor’s payout phase that is
361,74 €; on model point 12, which carries no guarantee period at all and so has a falling
count from the first instalment, it is 573,50 €. The approximation the annual grid needed —
twelve instalments in one amount at the start of the payout year, overstating by roughly
½ · q(x) · 12R for a life dying during the year — is gone, and the level was always right,
the factor carrying the Woolhouse −11/24 correction.
Four exits, and why a transfer is not a surrender#
Cells |
Decrement |
Benefit |
Charge retained |
|---|---|---|---|
|
|
|
none |
|
|
|
the 2 % Stornoabzug |
|
|
|
the flat 50,00 € |
the commuted cohort at |
— |
|
none |
The decrement takes a month and the benefit an annual year: every exit of contract year
k releases the same end-of-year account value, which is where the account is struck, so the
month decides when a benefit is paid and not how much — and a contract year’s exits release
exactly what the annual-step model released. The three rates now compete month by month,
where the annual model ran them in sequence at one year end, which moves 5,62 € off the
anchor’s death outgo and 2,64 € off its surrender outgo onto 8,30 € of transfers.
A Kündigung and an Anbieterwechsel are separate decrements, not two spellings of one. The
transfer pays the full account less a flat charge with no Stornoabzug and carries none of the
schädliche Verwendung consequences a surrender does R1 R14, so transfer_rate sits
above lapse_rate at every duration: over the anchor’s projection 11,48 % of the cohort
transfers out against 7,65 % that surrenders. Collapsing the two would apply a percentage charge
where a flat one belongs and would attribute a repayment of every Zulage and every § 10a relief
to an exit that has none. exit_charge_pp(t) is the residue that makes the account roll forward
exactly — 0,12 € in the anchor’s first month and 1,48 € over its first contract year, because
the account an exiting policy releases either
leaves as a benefit or stays with the insurer; dropping it leaves exactly that residual in
check_av_roll_fwd_resid(0), the usual way the identity fails.
Beitragsfreistellung is not a decrement. It is the book’s dominant exit R25 and a
state change: pols_if is continuous across it, the account keeps rolling, the guarantee
accumulator freezes once the last Zulage has landed, the Zulage stream stops. It is a
per-model-point switch (bfs_year) rather than a rate, because a paid-up policy and a
premium-paying one have different account values and different guarantee accumulators from the
moment they diverge, and a scalar projection cannot carry two of each without doubling every
recursion. Model point 10 shows the mechanic on one policy; a book projection needs the cohort
split, and the notes say so under Key sensitivities.
Inputs are external files#
The eight input CSVs live in this directory, beside run.py — not inside the model
folder. Riester_DE_S/ holds nothing but formulas:
products/riester_rente/
model_point_table.csv mort_table_accum.csv annuity_mort_table.csv <- inputs live here
lapse_table.csv zulage_schedule.csv income_schedule.csv
surplus_scenario.csv freq_loading.csv
run.py model.md product-spec.md technical-notes.md sources.md
Riester_DE_S/ <- formulas only
__init__.py _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 inputs beside the model and
reads them at run time — the opposite of basiclife/BasicTerm_S, which stores them 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, reached through Projection’s data
Reference, so each file is read once per model however many policies are projected — the
conventions suite counts the reads. Data.input_dir() resolves the location from
_model.path.parent at run time, so the model works wherever the repository sits.
Each file has one string Reference and one reader cells on Data, named alike:
model_point_file → model_point_table(), mort_accum_file → mort_table_accum(),
annuity_mort_file → annuity_mort_table(), lapse_file → lapse_table(), zulage_file →
zulage_schedule(), income_file → income_schedule(), surplus_file →
surplus_scenario() and freq_loading_file → freq_loading().
The trade-off: the model is not portable on its own — copy Riester_DE_S/ without the CSVs
and it reads fine, then fails on first evaluation. What you gain is a diff that shows logic
changes only, and an input that can be swapped in place: point Data.freq_loading_file at
another same-schema file and the projection follows with no formula change, which is how the
frequency-loading invariance is tested.
File |
Contents |
Provenance |
|---|---|---|
|
Thirteen model points, twenty-six columns. Point 1 is the worked-example anchor; 2–13 exercise the at-inception reconciliation, both Kinderzulage rates at once, the Sockelbeitrag floor, the |
configuration, and the one file exempt from the provenance rule |
|
Accumulation death rates by attained age 16–110 |
std proxy for DAV 2008 T REG-R48, |
|
Annuitant base rates and improvement scale, ages 55–110 |
std generational proxy for DAV 2004 R REG-R49. What a replacement may not drop is that it is two-dimensional. The anchor is |
|
|
std, and no observed range exists (gap 16). Transfer above surrender at every duration, and that ordering is itself the assertion |
|
|
R9 REG-R42. Exogenous because Kindergeld is a household fact the contract does not observe — the most awkward feature of this product for a per-policy model |
|
Contribution-liable earnings by schedule id and |
std 2 % nominal growth paths plus a |
|
|
std, the largest single lever in the model and the least supported (gap 12). The rate includes the Rechnungszins REG-R53 |
|
The Ratenzuschlag multiplier by payment frequency |
std 1.0000 / 1.0100 / 1.0200 / 1.0300, a charge and never a credit |
How the time-like input columns are keyed#
The model’s own time index is 0-based, so each time-like column in the inputs had to be
placed on one side or the other of that line, and this is where it landed. All four of them
sit on the annual clock — they are annual quantities of an annual contract, and the move
to a monthly grid re-keyed none of them: each is read through k rather than t.
File |
Column |
Decision |
Why |
|---|---|---|---|
|
|
The model’s own annual index |
|
|
|
The model’s own annual index |
|
|
|
The model’s own annual index |
|
|
|
Unchanged, 1 … 60 |
It is a contractual contract-year band, not the frame’s |
|
|
Unchanged |
Attained age, not a time index |
|
|
Unchanged |
Not a time index |
|
|
Unchanged |
An elapsed count of completed contract years, 0-based by nature |
|
|
Shifted with the frame: point 10 moves from |
It is a point on the frame’s own annual axis, compared as |
The identities the model checks#
check_net_cf() — delib’s first ruling — is the cash flow statement’s own reconciliation,
in one line, on result_cf() row t:
net_cf = premiums + zulagen − claims_death − claims_lapse − claims_transfer
− claims_lumpsum − claims_commutation − claims_annuity − expenses − commissions
Every term is read from the published frame, not from the cells behind it, which is what
makes it a reconciliation of what the model publishes rather than a restatement of net_cf’s
own expression: a column that is in the frame and not in the total, or in the total twice, or
that has drifted from the kind behind it, leaves a residual here. int_credited is
deliberately outside the identity — it moves money inside the account, not across the
insurer’s boundary — and on the monthly grid it is not even a column of the frame, which
removes the tempting error of adding it. Five more checks sit beside it, and the conventions
suite calls all six on every model point.
The residual’s argument follows its cells’ clock. Two take a month, because they are statements about payments and about the policy ledger, and four take a projection year, because the account, the guarantee accumulator, the conversion and the ZfA lag move once a year and have nothing to say about a month. Calling one with the other’s index is a category error rather than a rounding question.
Check |
Clock |
What it catches |
|---|---|---|
|
month |
The line above: a column in the frame and not in the total, or in it twice |
|
month |
A misindexed decrement recursion — including the annual rate applied where the monthly one belongs, which would project twelve years of decrement in one — and, through a closure identity built by direct summation over the exit cells, a commuted cohort that leaves uncounted |
|
year |
The account an exit releases not being counted — a Stornoabzug looks like income rather than like account released. It closes whatever the split of a year’s exits between the three decrements, because all three release the same annual end-of-year account value |
|
year |
The entitlement added instead of the credit; interest added to a nominal guarantee; the unsubsidised limb dropped; the 20 % carve-out cap not binding |
|
year |
The guarantee not applied; the capital not fully disposed of between lump sum, annuity capital and Abfindung; a Rentenfaktor inconsistent with the annuity basis; and an annual annuity amount reaching a monthly frame, since |
|
year |
The two lags collapsed into one, or the final contribution year’s Zulage dropped |
Modules that are off in the base run#
Everything the product carries is implemented; the anchor is the plain cell, so the worked example reproduces while the machinery stays visible and testable.
Module |
Switch |
Off value on the anchor |
On at |
|---|---|---|---|
Unsubsidised second contribution pool — enters the account and the guarantee while drawing no Zulage R12 |
|
|
point 8, 900,00 € |
Biometric-rider carve-out — capped at 20 % of total contributions REG-R43; never a cash flow here |
|
|
point 9, 400,00 € |
Beitragsfreistellung — contribution and Zulage stop, the account rolls on, the acquisition charge keeps biting |
|
|
point 10, |
§ 86 proportional Kürzung — halves the contribution and, in proportion, the subsidy R10 |
|
|
point 7, 0.50 |
Ratenzuschlag — raises |
|
|
points 3, 4, 6, 7, 10, 13 |
Berufseinsteiger-Bonus — the once-in-a-lifetime addition to the Grundzulage R9 |
the |
|
point 6, 200,00 € inside a 375,00 € opening credit |
The low declared-rate stress — the only lever deciding whether the guarantee costs anything |
|
|
point 11, |
Teilkapitalauszahlung election, and the Rentengarantiezeit that pays on |
|
|
point 12, at 0.00 and 0 |
Two Space References are worth naming because a user will want to move them: zulage_lag = 1,
which is the statutory timing rather than a convention — §§ 88 to 90 EStG put the credit in the
following year and AltvPIBV § 9 Abs. 3 dates it to 15 May of that year R5 R11, closing
gap 6, so what zulage_lag standardizes is only the annual-grid compression; and
mort_be_factor = 0.80 beside annuity_mort_be_factor = 1.15, which run in opposite
directions because the direction of prudence forks by product — a first-order death table
assumes mortality higher than expected, a first-order annuity table lower REG-R47.
Three constructions in the sources are not implemented rather than implemented and switched off. The Auszahlungsplan mit Restverrentung R1 belongs to the fund and bank chassis; Wohn-Riester is absent in both limbs — no Eigenheimbetrag decrement, no certified Darlehen, no Wohnförderkonto, the last because it is a notional tax-bookkeeping account carrying no cash flow at all R13; and there is no surplus in payment, the wedge between the first- and second-order annuity bases being a Risikoüberschuss this model does not distribute.
Sign convention#
net_cf is income positive — contributions and Zulagen in, the six kinds of benefit,
expenses and commission 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) over the relevant risk-free term
structure plus a risk margin REG-R5 REG-R6; nothing in this library discounts.
expenses and commissions are separate columns and net_cf subtracts each exactly once —
frlib’s reading, where expenses was the total and contained the commission, is not the reading
here, because the notes’ worked example prints both as parts; int_credited is a state movement
and is in neither. The shape to expect is a modest positive net_cf every
accumulation year — 1 505,37 € at t = 0 — then −11 276,67 € in the conversion year as the
Teilkapitalauszahlung leaves in one payment, then a long thin annuity tail, for an
undiscounted total of −7 827,39 €.
Naming#
Cells follow lifelib’s basiclife/BasicTerm_S where that model has an analogue and
savings/CashValue_SE for the account-value chassis: pols_* for policy counts, plural nouns
for cash flows, *_rate for rates, *_pp for per-policy amounts, claims(t, kind) with an
uppercase kind, pols_if_at(t, timing) and av_total_pp_at(t, timing) for the within-year
reads, prem_to_av_pp for the contribution credited to the account. The full symbol map lives
in the Projection docstring. Seven cases needed care:
Notes |
Cells |
Why |
|---|---|---|
|
|
Three subsidy amounts and the product turns on the difference between them; and the notes’ |
|
|
The lifelib name for the premium credited to an account value. May be negative, which is the point of model point 10 |
|
|
Guarantee accounting, not two strategies. Kept apart because |
|
|
Not |
|
|
An accumulator tested once, and the shortfall it produces. |
|
|
Inside the Rentengarantiezeit the instalment is paid on a count that is not the in-force |
Three sister models share a chassis and the names mean the same thing on all of them.
RV_DE_S, the klassische aufgeschobene private Rentenversicherung, is the same
general-account accumulation and the same conversion at a guaranteed Rentenfaktor with none
of the Schicht-2 apparatus, and is the primary home for the dk_pp / surplus_acct_pp
recursion and for § 169 VVG. Basis_DE_S is the Schicht-1 sibling — same nachgelagerte
Besteuerung, same annuitisation constraint, no Zulagen, no Beitragsgarantie, no lump sum.
Sofort_DE_S is the payout contract this model’s second phase now runs on the same monthly grid as.
Two model point columns drive nothing and are carried anyway: sex, which is reporting only
because AltZertG § 1 Abs. 1 Satz 1 Nr. 2 requires an “unabhängig vom Geschlecht berechnete”
benefit and every retrieved wording says so R23 [S2] [S4] [S6], the 2006 vintage date
itself being [unverified] — its absence from every formula is the assertion worth making — and issue_age, which enters only through
age(0) = issue_age + duration_init.
Standardizations used#
Everything in this list is std. The statutory half of this product is not a composite at all; the carrier half still is one, because the re-verification pass established carrier structure and almost no carrier level — the rows below say, one by one, which of them a retrieved document has since confirmed, contradicted or left untouched.
Standardization |
Value |
Rationale |
|---|---|---|
Both decrement tables, their slopes and the two best-estimate factors |
|
DAV 2008 T and DAV 2004 R are proprietary and not redistributed REG-R47 REG-R48 REG-R49. The slopes are placeholders; the generational structure of the second is not optional; the anchors a replacement must preserve are |
Rechnungszins; laufende Verzinsung |
0,25 % on the anchor and 0,90 % on point 3; |
The Höchstzinssatz caps the rate the Deckungsrückstellung is computed at, not what a policy may guarantee, and DeckRV § 2 Abs. 2 fixes the rate used at conclusion for the contract’s whole term R22 REG-R14. Two real carrier choices now exist — 1,25 % on a 01.15 tariff [S4] and 0,9 % on a 01.01.2025 tariff, below the 1,00 % cap of its vintage [S6] — which is direct evidence for the “may guarantee less” limb. No carrier declaration was established (gap 12); |
Risikoüberschuss and Kostenüberschuss; Schlussüberschussanteil; Bewertungsreserven share, and counting the last two toward the guarantee |
zero; 2,0 % of contributions credited; 1,0 % of the account |
The accumulation risk result is nil by construction — the death benefit is the account value, so there is no sum at risk — and no cost result was established; the two terminal levels are unestablished and whether either may close a shortfall is unsettled (gap 9), so counting them is the provider-favourable reading |
Acquisition charge; administration charge and its base |
2,5 % of |
The five-year floor is statutory, and the statute’s own qualifier — the spreading applies only “soweit sie nicht als Prozentsatz von den Altersvorsorgebeiträgen abgezogen werden” — is why every retrieved wording takes the charge on a Zulage once at inflow R1 [S2] [S4] [S6]. Gap 14 is closed: German tariffs do charge the Zulagen. The base is therefore no longer a standardization; only the rates are, and the one tariff in hand charges 1,0 % acquisition on Eigenbeiträge and 2,1 % / 6,0 % administration on contributions / Zulagen [S4] — the composite’s single 4,0 % is contradicted, and unchanged here |
Frequency loading, as a charge; Stornoabzug; transfer charge |
1.0000 / 1.0100 / 1.0200 / 1.0300; 2,0 % of the account; 50,00 € flat |
Gap 8 closes: the statutory cap is 150,00 €, AltZertG § 1 Abs. 1 Satz 3 R1, so the 50,00 € sits inside it — and is exactly what one fund provider charges [S9] while one insurer charges nothing [S4]. One Ratenzuschlag scale is now observed and has a different mechanic: +1,0 / +2,0 / +3,0 percentage points on the administration rate [S4]. One Stornoabzug is now observed and likewise: an interest-linked 0/5/10/15 % market-value adjustment running off over the last ten years of deferral [S6]. § 169 Abs. 5 VVG requires any deduction to be “vereinbart, beziffert und angemessen” REG-R28 |
Rentenfaktor margin; annuitisation interest; the guaranteed factor; and |
30 %; 1,00 %; 29,00 € per 10 000 € per month |
The construction is now established in a Riester wording and the level is not. Debeka defines a guaranteed factor “je 10.000 Euro Guthaben” monthly, on a 0,1 % Rechnungszins and its own unisex table, compared with the current factor, “Die höhere Rente wird ausgezahlt (Günstigerprüfung)” [S6] — the model’s construction exactly. But the design is not universal: neither the GDV model wording nor the CosmosDirekt wording has a Rentenfaktor at all, agreeing the annuity at inception instead [S2] [S4]. No level at any carrier was established (gap 9) |
Kleinbetragsrente threshold and the basis of the test; the one-year Zulage cash lag; 2,0 % p.a. nominal income growth |
39,55 € a month, flat in nominal terms, applied after the lump sum |
The first two are no longer standardizations but known errors, recorded and not fixed. The threshold is 1,5 % of the monthly Bezugsgröße, § 93 Abs. 3 Satz 2 Nr. 1 EStG — 59,33 € here R15 — and the test belongs before the lump sum [S2]; both of the model’s choices push toward fewer commutations and a longer-tailed liability (gap 7). Gap 6 closes: AltvPIBV § 9 Abs. 3 has the Zulage credited on 15 May after the contribution year R5, so only the annual-grid compression is standardized. The growth rate is a round real-plus-inflation number that decides when the 2 100 € ceiling binds |
Surrender and transfer rates, 0,8 / 0,6 / 0,4 % and 1,2 / 0,9 / 0,6 % by duration band; expenses and commission; the 30 % Teilkapitalauszahlung take-up |
30,00 € maintenance inflating at 2,0 %; 24,00 € per annuitant; 80,00 € per claim; 150,00 € + 2,0 % of |
No German Riester behavioural rate was established (gap 16), and the transfer-above-surrender ordering is an argument from the statutory consequences rather than from data. No German insurer publishes a unit cost. The maintenance figure carries the Zulage administration — Dauerzulageantrag, annual ZfA exchange, Leistungsmitteilung — a real product-specific cost. German commentary reports the lump sum as the usual election, and gap 10 records that this rests on nothing |
Timing, processing and decrement order |
Contribution and Zulage in the year’s first month, interest at its end, decrements at the end of each month, conversion at |
No source fixes the ordering inside a period, so it is stated to be compared line by line. The annuity’s level is right because the factor carries the Woolhouse correction, and the monthly grid now gets its timing right too; what stays std is vorschüssig against nachschüssig |
|
— |
The omega forces the decrement closure to be exact rather than approximate; the seeds are std, and the notes record that |
The quantities that are not standardizations are the statutory ones, and after the 2026-08-30 provenance pass every one of them has been read verbatim in the canonical statutory XML rather than taken on general knowledge: the 175,00 € / 185,00 € / 300,00 € Zulagen and the 200,00 € bonus, §§ 84 and 85 EStG R9; the 4 % / 2 100,00 € / 60,00 € Mindesteigenbeitrag arithmetic and the proportional Kürzung, § 86 Abs. 1 Sätze 2, 4, 5 and 6 with § 10a Abs. 1 Satz 1 R6 R10; the ZfA arrear, §§ 88 to 90 EStG with the 15 May crediting convention of AltvPIBV § 9 Abs. 3 R5 R11; the Beitragserhaltungszusage and the 20 % biometric carve-out, AltZertG § 1 Abs. 1 Satz 1 Nr. 3 R1 REG-R43; the 30 % Teilkapitalauszahlung cap, the five-year cost-spreading floor, the earliest Rentenbeginn of 62 (with the 60 in § 14 Abs. 2, not § 1) and the Wechselrecht with its 150 € charge ceiling R1; and the structural rules — the Zulage credited to the contract by the provider (§ 90 Abs. 2), the guarantee tested once, benefits gross of the Rückzahlungsbetrag the provider merely withholds and remits (§ 94 Abs. 1), and unisex pricing (§ 1 Abs. 1 Satz 1 Nr. 2) R23.
Two things in that list are statutory in the model and contractual in the world, and the distinction is worth keeping. That the Zulagen count toward the guarantee is not in the AltZertG, which speaks of Altersvorsorgebeiträge — what the saver pays, EStG § 82 R8; it is in the wordings, all of which promise “die gezahlten Beiträge und die uns zugeflossenen staatlichen Zulagen” [S2] [S4] [S6]. And the death benefit equal to the account is a std in this table that the GDV model wording makes its own title — “mit Auszahlung des Deckungskapitals bei Tod” [S2] — so it is standardized here only in the sense that a different carrier could write something else.
Tests#
tests/test_riester_rente_de.py asserts every row of the notes’ worked-example table to the cent
and pols_if to six decimals, the payout phase’s selected rows, the full-precision totals over
all sixty-one periods against the four-cent difference a sum of rounded cells gives, the notes’
four independent rebuilds — the first period t = 0 from the statute up, the conversion year, the
aggregate account roll-forward with its exit charge, and the four-way decrement closure to
1.00000000 — and the two variants, model point 11’s binding Garantielücke and model point 5’s
commuting Sockelbeitrag cell.
Beyond that it asserts one test per numbered modeling pitfall: the two subsidy lags kept
apart; the final contribution year’s Zulage credited at t_conv(); the § 86 Kürzung
proportional; the Zulage as a separate positive income column; no Günstigerprüfung cells;
both Kinderzulage rates at once; the guarantee tested only at Rentenbeginn and no benefit
floored at it; the 20 % carve-out cap binding; unsubsidised contributions inside the guarantee;
the declared rate including and not added to the Rechnungszins; the frequency loading charged
and never credited; the acquisition charge spread over five contract years and continuing
through a Beitragsfreistellung; transfer separated from surrender; Beitragsfreistellung as
a state change; two mortality bases in opposite directions with a generational annuity table;
the Kleinbetragsrente tested on the post-lump-sum annuity against a flat threshold; the
Rentengarantiezeit changing the count and never the amount; and every benefit gross of the
Rückzahlungsbetrag. The whole-model-point-table sweep belongs to
tests/test_model_conventions_de.py, which owns the library’s single sweep.
python -m pytest lifelib/libraries/delib/tests/test_riester_rente_de.py -q