Generated file. Source:
docs/bond_engine_architecture.mdEdit the source document and runnpm run docs:syncto refresh this published copy.
Bond Engine Architecture¶
This document describes the current high-level architecture of the bond calculation engine.
Goal¶
The engine is intentionally split into small product families instead of one giant generic calculator.
Why:
- different bonds have materially different cash-flow behavior
- reusable mechanics should live in shared family engines
- product-specific rules should stay in thin strategy modules
- selection/eligibility rules should stay outside financial simulation
Current Strategy Map¶
Custom quarterly strategy¶
OTS- code:
ots.ts: src/app/domain/bond-engine/strategies/ots.ts
- reason:
- quarter-based redemption and reinvestment model is unique enough to stay separate
- current implementation (as of 2026-04-08):
- separates principal (reinvestment pool) from after-tax interest (OKO account) at each quarterly redemption
- both sources are considered when determining reinvestment capacity
- reinvestment logic consolidates OKO balance back to cash
Shared monthly-income family¶
- family engine:
monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.ts- specializations:
RORror.ts: src/app/domain/bond-engine/strategies/ror.ts
DORdor.ts: src/app/domain/bond-engine/strategies/dor.ts
- shared behavior:
- monthly interest payouts
- monthly tax
- purchase history
- reinvestment decisions
- profitability guard for late reinvestment
- note: the Excel-aligned
OKOpath forRORandDORis routed throughexcel-like-oko.ts: src/app/domain/bond-engine/strategies/shared/excel-like-oko.ts, not through the plain reinvestment family
Shared annual-accumulation family¶
- family engine:
annual-accumulation.ts: src/app/domain/bond-engine/strategies/annual-accumulation.ts- specializations:
TOStos.ts: src/app/domain/bond-engine/strategies/tos.ts
EDOedo.ts: src/app/domain/bond-engine/strategies/edo.ts
ROSros.ts: src/app/domain/bond-engine/strategies/ros.ts
RODrod.ts: src/app/domain/bond-engine/strategies/rod.ts
- shared behavior:
- yearly compounding inside the batch
- no payout during cycle
- workbook-style rollover only after redemption
- residual redemption cash transferred to a monthly-accruing cash account
- early-redemption settlement where the fee reduces the taxable profit basis before tax is calculated
- purchase history
- redemption history
- note: this family now also uses
exchangePriceand the residual cash account to stay aligned with the workbook forTOS,EDO,ROS, andROD
Shared annual-payout family¶
- family engine:
annual-payout.ts: src/app/domain/bond-engine/strategies/annual-payout.ts- specializations:
COIcoi.ts: src/app/domain/bond-engine/strategies/coi.ts
- shared behavior:
- yearly interest payout to reinvestment cash
- yearly tax on paid interest
- no capitalization inside the batch
- final early redemption of remaining principal after already-paid yearly interest
- purchase history
- payout history
- redemption history
- note: the Excel-aligned
OKOpath forCOIis routed throughexcel-like-oko.ts: src/app/domain/bond-engine/strategies/shared/excel-like-oko.ts
Shared Excel-Aligned OKO Helper¶
- helper:
excel-like-oko.ts: src/app/domain/bond-engine/strategies/shared/excel-like-oko.ts- data source:
- every bond now has
exchangePriceinbonds.json: src/app/data/bonds.json - current users:
RORDORCOI- shared behavior:
- separate
OKObalance with monthly account accrual and tax - workbook-style liquidation checkpoints
- exchange at
exchangePrice, currently99.90 - extra bond purchases from full
100 PLNchunks already sitting onOKO - protection window limiting the fee in the first months where the product defines it
Data-model rule:
exchangePriceis now a catalog parameter for every bond- when a product does not have an effective exchange discount in the modeled logic, the catalog stores
100 - the annual-accumulation family also consumes that parameter during rollover after natural maturity
bonds.jsonkeeps only canonical source parameters shared by every bond entry- presentation strings are derived in
bondCatalog.ts: src/app/data/bondCatalog.tsso they do not have to be scraped or duplicated in the JSON file
Shared Early-Redemption Rule¶
Where a strategy realizes profit only at redemption, the engine uses one shared settlement rule:
taxableProfit = max(0, grossValue - principalValue - earlyRedemptionCost)taxPaid = taxableProfit * taxRatenetValue = grossValue - earlyRedemptionCost - taxPaid
This keeps the engine aligned with the current project interpretation taken from Treasury-bond materials:
- the early-redemption fee reduces the taxable profit basis
- the fee is not deducted only after tax
Current shared helper:
early-redemption.ts: src/app/domain/bond-engine/strategies/shared/early-redemption.ts
Routing¶
Product dispatch is handled by:
strategy-registry.ts: src/app/domain/bond-engine/strategy-registry.ts
This is intentional. Routing should not grow as a long if/else chain inside calculation logic.
Strategy Factories¶
Each specialization exports its product strategy factory.
Examples:
createRorMonthlyIncomeStrategy: src/app/domain/bond-engine/strategies/ror.tscreateTosAnnualAccumulationStrategy: src/app/domain/bond-engine/strategies/tos.tscreateCoiAnnualPayoutStrategy: src/app/domain/bond-engine/strategies/coi.ts
These factories are the single source of truth for:
- runtime strategy configuration
- test strategy configuration
- public strategy identifiers
Tests should reuse them instead of duplicating inline configs.
Selection vs Calculation Boundary¶
Eligibility and selection rules are outside the financial engine.
Examples:
ROS/RODavailability for800+- top-of-page bond selector and floating quick-picker in the results area both write to the same
selectedBondsstate - current stage only gates availability; it does not yet enforce the purchase amount cap tied to received
800+benefits - sanitizing selected bonds
- selecting only allowed instruments for the current params
These rules belong in:
selection.ts: src/app/domain/bond-engine/selection.ts
Current product limitations for these rules should stay aligned with:
This boundary is important:
- strategies simulate cash flows
- selection decides whether a bond may be chosen at all
- incomplete eligibility rules that are consciously postponed should stay explicitly documented here, not be implied as fully implemented
Do not move product eligibility rules into shared engines or bond strategy modules.
Future Assistant Input Layer¶
If the product introduces a guided assistant that asks the user questions and fills the calculator for them:
- it should be treated as an input orchestration layer above the existing form and store
- it should map answers into the same normalized input model used by the manual calculator flow
- it must not introduce a second calculation path
- it must not implement financial rules on its own
This means:
- the assistant collects intent and parameters
- the existing store/form shape remains the operational source of truth for inputs
- the same bond engine produces the final result regardless of whether inputs came from manual entry or assistant dialogue
Simulation Event Contract¶
The engine exposes optional event ledgers via:
BondSimulationDetails: src/app/types/bonds.ts
Possible event families:
purchaseEventsreinvestmentDecisionsredemptionEventspayoutEvents
Not every strategy uses every event type.
That is expected:
- monthly-income uses reinvestment decisions and may expose
cashAccountEvents - annual-payout uses payout events and may expose
cashAccountEvents - annual-accumulation does not use payout events, but it may expose non-zero
cashAccountBalancein yearly and final results because rollover residue is stored on the residual cash account
Canonical Result Model¶
The public output of every strategy should be treated as a canonical result contract, not only as a UI payload.
Current code contract:
YearlyResultBondCalculationResultBondSimulationDetails
Current source of truth for its semantics:
Why this matters:
- the same result contract feeds chart, summary, yearly table, CSV export, and future persistence features
- it is one of the main architectural boundaries between engine internals and the rest of the product
- future roadmap stages such as saved simulations, inflation prediction history, recurring investing, and assistant flows depend on having enough output resolution here
- current runtime persistence flow now spans:
src/app/domain/persistence/repository.tssrc/app/domain/persistence/service.tssrc/app/domain/prediction-history/repository.tssrc/app/domain/prediction-history/service.tssrc/app/domain/portfolio/types.ts
Design rule:
- engine internals may be richer than the canonical result contract
- analysis models should sit above the canonical result contract when the product needs more than one defended interpretation of the same result
- UI-specific adapters should sit above either the canonical result or the analysis-model layer, but must not redefine semantics locally
- but the canonical result contract must remain the stable shared layer between them
Current implementation direction:
- canonical result types live in
src/app/domain/result-model/types.ts - thin presentation adapters live in
src/app/domain/result-model/view-adapters.ts - future interpretation / analysis models should live in
src/app/domain/analysis-models/ - the implemented interpretation switch is backed by one global
analysisModestate and is exposed through small UI controls in the chart and yearly-table sections rather than through component-local semantics - future saved-simulation contracts live in
src/app/domain/persistence/types.ts - runtime saved-simulation repository lives in
src/app/domain/persistence/repository.ts - shared finalization helpers for strategy families live in
src/app/domain/bond-engine/strategies/shared/finalization.ts - yearly-table comparison rows are also derived in
src/app/domain/result-model/view-adapters.ts, so the UI does not have to recreate comparison-grouping semantics locally
Maintenance Rules¶
When changing a bond family:
- update the shared engine if the rule is genuinely shared
- update the thin specialization if the rule is product-specific
- reuse the strategy factory in tests
- update family docs and product docs together
- update
init_prompt.md: ai/init_prompt.mdif product rules changed
When adding a new bond:
- first decide whether it fits an existing family
- if not, create a new family rather than forcing exceptions into an old one
Current Outcome¶
The codebase now has a complete product map:
OTS-> custom quarterlyROR,DOR-> monthly-incomeTOS,EDO,ROS,ROD-> annual-accumulationCOI-> annual-payout
This is the intended long-term architecture for maintainability.