The Index_DE_S Model#
Reference liability cash flow model for the German Indexpolice.
Index_DE_S is the executable counterpart of
products/indexpolice/technical-notes.md in the lifelib-products library. It projects
gross best-estimate liability cash flows, undiscounted, for a single-policy model point
of an indexgebundene Rentenversicherung — the deferred private annuity whose capital
sits in the insurer’s Sicherungsvermögen under a guarantee and whose annually declared
Überschuss is not credited as interest but spent as an option budget buying one
year of index participation — on a monthly grid, from inception (or from the
valuation date of an in-force point) to Rentenbeginn.
Four things make this the Indexpolice model rather than a re-labelled unit-linked one.
The capital is in the general account and cannot fall from the index. There is no Anlagestock, no unit price and no fund value anywhere in this model. The policyholder holds a claim measured in euros, the Deckungskapital rolls forward by a recursion, and a bad Indexjahr credits zero rather than taking anything away. Reading the contract as unit-linked is the first listed modeling pitfall, and it is the single most common misunderstanding of the product.
The payoff is a sum of capped monthly returns, floored once at the year. Each month’s
return is capped above at C and not floored below; the twelve are summed, not
compounded; and the sum alone is floored at zero. The asymmetry is the whole product:
truncating every right tail while leaving every left tail intact means a year in which
the index rose can credit nothing. The shipped index path reproduces the research
file’s two constructed Indexjahre at k = 8 and k = 9 (policy years 9 and 10) —
Example A credits 8.90 % of the base, and Example B credits zero on a year whose
compounded index return was +6.4402 %.
One budget, two arms, and the policyholder elects between them each year. The
declared surplus rate is either spent on options (opt_budget_pp) or credited as
interest (surplus_credit_pp), never both and never neither:
check_surplus_alloc() asserts that identity at every t. The annual Wahlrecht is
a behavioural assumption, carried as an election path w(t) in an external table.
Whatever is credited is locked in. A credit, once made, is permanently part of the
guaranteed capital, enters the base of every later Indexjahr, and can never be lost —
the Höchststandsicherung. What ratchets is the ledger of credits, not the account
balance: with the reserve charge at or above the guaranteed rate the balance itself falls
in a year that credits nothing, which is why check_lock_in() asserts monotone
guar_cap_pp and non-negative credits and says nothing about av_pp.
Spaces. The model contains two:
DataReads the eight input CSVs and holds their filename References. It takes no parameters, so each file is read once per model.
ProjectionThe by-policy projection, parameterized by
point_id:Projection[1]is an ItemSpace projecting model point 1. It reaches the input tables through itsdataReference, which resolves to the singleDataSpace.
The split matters for more than tidiness. Because Projection is parameterized, every
Projection[N] is a separate ItemSpace with its own cells cache; readers placed there
would re-read every file for every policy. In Data they are evaluated once, however
many policies are projected.
Input data is external: CSVs in the model folder’s parent directory, read at run time rather than stored inside the model. The model folder itself holds no data, so the model and its inputs must travel together.
Projection basis. Monthly steps over an annual contract, so the model runs on two
clocks and the argument of a cells says which. t counts policy months from issue
and is 0-based; duration(t) = t // 12 is the 0-based policy year k,
policy_year(t) = duration(t) + 1 the contractual label, and
proj_len() = 12 x proj_len_y() the exclusive end of the frame, with
proj_len_y() = ann_start_age - entry_age. A new-business point starts at t = 0, an
in-force point at t = 12 x dur_init.
Everything that makes this product what it is stays annual and takes ``k``, because the
contract is annual: the Indexjahr is twelve months, the surplus is declared once a year,
the Wahlrecht is exercised once a year and the credit is struck once a year, at its end,
to the survivors only. The premium falls at the start of the policy year — the
Versicherungsperiode of this tariff — and the account, the ledger and the guaranteed
capital roll once a year. The in force, the two decrements, the claims and the expenses take
t, so a death or a surrender now falls in the month it happens and is paid the policy
year’s own amount.
What the finer grid earns here is the Indexjahr itself: its twelve monthly returns were
always the mechanic but lived inside a single cells, and index_month(t),
index_return_mth(t) and index_return_capped_mth(t) now put them on the frame, one
row each, so capped above and not floored below can be read month by month. It also dates
the forfeiture — a surrender in month 7 of an Indexjahr loses that year’s credit, and the
incentive to surrender just after a year closes rather than just before is now in the
projection instead of only in the prose.
The decrements carry the library’s two speeds — mort_rate(t) and lapse_rate(t) are
the annual rates of the policy year, mort_rate_mth and lapse_rate_mth the
geometric twelfths the recursion applies — so the annual layer is bit-identical to the
annual-step model this replaced on all thirteen model points: the account, every
Indexgutschrift, the Höchststandsicherung ledger, the guaranteed capital, the surrender
value and premium income are unchanged. What moved is the split of a year’s exits between
death and surrender, and the expenses a mid-year leaver bears.
What is sourced and what is not, stated without softening. The mechanics are firm and are cited to the statutes that govern them: the index participation is a form of Überschussverwendung under § 153 VVG with no independent statutory footing, the capital is in the Sicherungsvermögen, the guarantee falls due at Rentenbeginn in the Neue Klassik architecture, the Rückkaufswert is a reserve floored by the five-year acquisition-cost spread of § 169 Abs. 3 VVG, and the Stornoabzug must be agreed, quantified and appropriate. No level is sourced at all. Direct HTTP egress was blocked in the build environment and the session’s search budget was exhausted before this product was researched, so no Cap, no Partizipationsquote, no declared surplus rate, no charge, no lapse rate and no commercial-envelope parameter was established for any German carrier. Every such number here is [std] with a stated rationale, and the DAV tables (DAV 2008 T, DAV 2004 R) are proprietary, are cited by name and are never shipped. This model is a mechanics demonstration, not a pricing or reserving result. Replace the decrement, charge and index tables with company data before drawing any conclusion from the output.
Model points. Thirteen, covering both premium forms, all four payment frequencies,
both payoff designs, all three index paths, all four election paths, both Kapitalwahlrecht
elections, both Stornoabzug settings, two in-force points, four Rechnungszins cohorts
and four Garantieniveaus. Model point 1 is the anchor cell of the worked example in the
technical notes; model point 8 is an in-force cell whose first projected Indexjahr is
k = 8, so it reproduces the research file’s Examples A and B on a 50,000.00 EUR base.
Verification. tests/test_indexpolice_de.py asserts the notes’ twenty-seven-year
worked example to the cent off result_cf_annual() and pols_if to six decimals, the
twelve months of an Indexjahr on the monthly frame beside it, and one test per listed
modeling pitfall. Six check_*() cells travel with the model and are called on every
model point by the library’s conventions suite: check_net_cf and
check_pols_roll_fwd monthly, and check_av_roll_fwd, check_surplus_alloc,
check_lock_in and check_index_credit per policy year, because the constructions
they check are annual.
Example
>>> import modelx as mx
>>> model = mx.read_model("products/indexpolice/Index_DE_S")
>>> model.Projection[1].result_cf()