Open Net Worth
Open Net Worth is an open-source personal AI wealth assistant you run yourself: one ledger for your whole financial life, worked by an AI agent you control.
翻訳待ち
No updates detected yet. GitHub releases will appear here.
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Actuarial projection engine in Python. Models are checked before they run and compiled when they do.
A model is a Python class. Building it traces the formulas into a typed dependency graph, checks units, time references and calculation order, then generates vectorised code that runs over every policy and scenario. Every run writes a manifest so any result can be reproduced exactly.
Status: prototype, not yet reviewed. The engine core (milestone M2), the reference models and the
IFRS 17 aggregation stage are in, each reconciled to its specification. TermAssurance (M3)
reconciles to the term assurance spec's number script, column for column and scalar for scalar,
under the spec's tolerance, and every deliberate mistake the spec lists for the checker fails the
build the way the spec says. BasicTerm (M4) reconciles to lifelib's BasicTerm_ME from
lifelib's own inputs: every policy's present values, the sampled policies' monthly flows and
counts, and the portfolio totals agree under the same tolerance. The annuities of the annuity spec
(M5: level, fixed escalation and CPI-linked) reconcile to its printed worked example and to the
in-force spec's number script, which restates the annuity model. The aggregation stage measures
the CSM of one group new at the valuation date (M7), with the revenue and IFIE lines and the
scenario mean of M12, and the subsequent measurement of an in-force group over a reporting period
(M9, M11), reconciled to the in-force spec's number script and to its two reference workbooks.
The workbooks of reference case 1 and of the in-force example (M1, M10) are built from the specs
by hand and recalculated in Excel; the term workbook agrees with its number script value for
value, and the engine's own reconciliation of the term product is to that script. No reviewer has
yet checked a spec or a workbook, so every reconciliation is against the author's own spec and
script. The Numba backend and the worker processes of M6 are in; its benchmarks are not.
Documentation: https://halley.kndra.co.za. Install with pip install halley (Python 3.12+).
The site's tutorial starts from that install and ends with a model of your own, your own data
and a run over scenarios; every code block on it is run by the test suite.
from halley import RunSettings, build, run
from halley.products.term_assurance import VALUATION_DATE, TermAssurance
inputs = "docs/spec/term-assurance"
compiled = build(TermAssurance) # trace, check, schedule, generate the NumPy backend
result = run(
compiled,
model_points=f"{inputs}/model_points.parquet",
tables={
name: f"{inputs}/{name}.parquet"
for name in ("mort_table", "lapse_table", "comm_table", "rates", "expenses")
},
settings=RunSettings(valuation_date=VALUATION_DATE),
run_dir="runs/term-reference",
)
result.lane("reserve", "P05")[0] # 537.9608658387098, the spec's worked example
The run directory holds manifest.json (model source hash and git commit, IR hash, the hash and
version of every input, backend versions, settings), a copy of every input, the generated backend
source, the outputs keyed on policy_id with a versioned schema, and the invariant results.
halley.explain shows any policy's value at any month with its formula and inputs; halley.diff
compares two runs; halley.load_run reads a run back without the model.
build(TermAssurance, backend="numba") generates the Numba backend instead (pip install halley[numba]): compiled loops from the same IR, held to the NumPy reference within 1e-9 relative
and bit-identical between its own reruns. Its first build compiles every kernel, which takes a
while for a product; Numba caches them beside the generated module, so later builds load them.
The cache is halley-numba-<user> in the system's temporary directory, one per user and private
to them, since Numba loads the compiled kernels it finds there and on Linux and macOS that
directory is shared; HALLEY_NUMBA_CACHE names another.
RunSettings(workers=4) spreads the batches (batch_size policies each) over four processes on
either backend, with the same results bit for bit as one process at the same batch size: the
same run id, outputs, invariant results and errors. The workers are started with the spawn
method and import the model's class from its module, so define it at the top level of a module
and guard a script's entry point with if __name__ == "__main__":.
A model is written like this (an excerpt of TermAssurance):
class TermAssurance(Product):
model_point = SCHEMA # typed fields, ranges, derived fields, rules
assumption_tables = (MORT_TABLE, LAPSE_TABLE, COMM_TABLE, RATES, EXPENSES)
@var
def q_m(self, t: Int) -> Prob[Monthly]:
return where(t <= self.mp.rem_m, to_monthly_prob(self.q_ann(t)), 0)
@var(role="decrement")
def deaths(self, t: Int) -> Count:
return self.pols_if(t - 1) * self.q_m(t) # Count x Prob[Monthly] -> Count
@var(init=lambda mp: mp.pols_if_init, role="in_force")
def pols_if(self, t: Int) -> Count:
return self.pols_if(t - 1) - self.deaths(t) - self.lapses(t) - self.maturities(t)
@var(terminal=0, role="reserve")
def reserve(self, t: Int) -> Money: # backward recursion
return (self.reserve(t + 1) + self.net_outgo(t + 1)) * self.v(t + 1)
Unit mismatches, data-dependent time offsets, missing boundary values, same-period cycles and mixed forward/backward loops are build errors that point at the model's own source line. The unit classes carry typed operator overloads, so pyright flags most unit mistakes in the editor.
Requires uv. Python 3.12 is pinned in .python-version.
uv sync
uv run pytest
uv run ruff check .
uv run pyright
This is the beginning of the README. Read the rest at the original. Read the full README ↗