Skip to content

Generated file. Source: docs/ROR_logic.md Edit the source document and run npm run docs:sync to refresh this published copy.

ROR Logic

This file documents the current ROR calculation engine implementation.

Document Metadata

  • Document status: handoff / implementation specification
  • Scope: current ROR engine and the shared monthly-income strategy family it uses
  • Primary code references:
  • monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.ts
  • ror.ts: src/app/domain/bond-engine/strategies/ror.ts
  • Related tests:
  • monthly-income.strategy.test.ts: tests/monthly-income.strategy.test.ts
  • ror.strategy.test.ts: tests/ror.strategy.test.ts
  • Related prompt source:
  • init_prompt.md: ai/init_prompt.md
  • Last reviewed against code: 2026-03-23

Purpose

This document is intended to let another programmer:

  • understand how ROR is modeled today
  • understand which logic is shared with future monthly-income bonds like DOR
  • modify the code without breaking monthly payout or reinvestment behavior

Scope

Included in scope:

  • shared monthly-income engine behavior
  • ROR monthly rate rules
  • monthly interest payout and taxation
  • monthly reinvestment
  • reinvestment profitability guard near the end of the horizon
  • 12-month redemption cycle
  • final early redemption
  • monthly snapshots
  • yearly result derivation
  • purchase-event history

Out of scope:

  • DOR specialization details
  • other bond families
  • UI rendering decisions

Product Definition

Current ROR engine assumptions:

  • single bond nominal value: 100 PLN
  • cycle length: 12 months
  • month 1 annualized rate: 4.25%
  • months 2-12 annualized rate: NBP + 0.00%
  • interest is calculated and paid every month
  • each monthly interest payment is taxed immediately at 19%
  • paid net interest goes to cash and can be reinvested immediately
  • reinvestment is allowed only when a newly bought bond is expected to be non-loss-making over the remaining horizon
  • natural redemption returns principal after 12 months
  • early redemption cost: 3 PLN per bond

Implemented rounding policy:

  • internal monetary values are normalized to 0.001 PLN
  • final summary outputs are rounded to 0.01 PLN

Architecture

ROR no longer uses the generic yearly calculator.

Instead it uses a shared monthly-income strategy family:

  • shared module:
  • monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.ts
  • ROR specialization:
  • ror.ts: src/app/domain/bond-engine/strategies/ror.ts

Design intent:

  • common monthly payout mechanics live in one place
  • bond-specific rules stay small and explicit
  • the same engine can later support DOR
flowchart LR
    A[calculateBondResult] --> B{bondId}
    B -->|ROR| C[calculateRorBondResult]
    C --> D[simulateMonthlyIncomeBond]
    D --> E[Month snapshots]
    D --> F[Purchase events]
    D --> G[Yearly results]
    D --> H[Final totals]

Shared Monthly-Income Contract

The shared monthly-income engine expects:

  • cycle length in months
  • monthly annual-rate resolver
  • standard bond definition
  • investment amount and horizon
  • reference rate and inflation inputs
  • tax rate and bond unit price

The shared engine provides:

  • monthly simulation
  • monthly interest posting
  • monthly tax posting
  • batch-based redemption
  • purchase-event ledger
  • reinvestment-decision ledger
  • yearly snapshots
  • final profitability summary

ROR Specialization

ROR configures the shared engine with:

  • strategyId = monthly-income-ror
  • cycleLengthMonths = 12
  • rate resolver:
  • cycleMonth === 1 -> 4.25%
  • otherwise -> referenceRate + 0.00%

So the ROR module contains only product-specific policy, while all monthly mechanics remain shared.

Internal State

Core shared state:

  • cash
  • positions
  • purchaseEvents
  • totalTaxPaid
  • totalEarlyRedemptionCosts
  • yearTaxPaid
  • yearEarlyRedemptionCosts

Each active position stores:

  • bonds
  • purchaseMonth

Monthly Flow

For each month m from 1 to totalMonths:

  1. For every active batch, calculate monthly interest.
  2. Use the batch age to determine the month inside the 12-month cycle.
  3. Apply the ROR rate rule:
  4. first month in cycle -> 4.25%
  5. later months -> referenceRate
  6. Add net interest to cash after tax.
  7. Redeem any batch that reached age 12.
  8. Reinvest available cash into full new ROR bonds if cash >= 100 and this is not the terminal month.
  9. If this is the final month and active batches remain, early redeem them by returning principal minus 3 PLN per bond.
  10. Store end-of-month snapshot.
flowchart TD
    A[Start month] --> B[Calculate monthly interest for each active batch]
    B --> C[Post tax and net interest to cash]
    C --> D[Redeem batches aged 12 months]
    D --> E[Add redeemed principal to cash]
    E --> F{cash >= 100 and month < final?}
    F -- Yes --> G[Buy new batch and store purchase event]
    F -- No --> H[Skip purchase]
    G --> I{final month and active positions remain?}
    H --> I
    I -- Yes --> J[Apply early redemption to remaining batches]
    I -- No --> K[Build month snapshot]
    J --> K

Interest Formula

For one batch in one month:

grossInterest = bonds * 100 * (annualRatePercent / 100 / 12)
taxPaid = grossInterest * 0.19
netInterest = grossInterest - taxPaid
cash += netInterest

annualRatePercent depends on the batch month inside its 12-month cycle.

Redemption Rules

Natural redemption:

  • happens when ageInMonths === 12
  • only principal is returned at that moment, because monthly interest has already been paid during the cycle

Final early redemption:

  • happens only in the terminal month
  • applies to every still-active batch
  • adds principal to cash
  • subtracts 3 PLN per bond
  • does not add extra monthly interest beyond what the engine already paid during simulated months

Reinvestment Profitability Guard

ROR does not automatically buy every late bond candidate.

Current rule:

  • if a newly purchased bond can still reach its natural 12-month maturity before the simulation ends, reinvestment is allowed
  • otherwise the engine estimates the net monthly interest that one new bond would earn until the simulation end
  • if that expected net interest is lower than the 3 PLN early redemption cost, reinvestment is blocked

Why:

  • ROR pays interest monthly, but a late purchase can still be net unprofitable because the fixed early redemption cost is large relative to a short remaining holding period

Implementation location:

  • monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.ts
  • ror.ts: src/app/domain/bond-engine/strategies/ror.ts

Purchase Event Ledger

The shared engine stores BondPurchaseEvent items for:

  • initial allocation
  • reinvestment purchases

Each event includes:

  • month
  • purchasedBondCount
  • cashBeforePurchase
  • cashAfterPurchase
  • sourceBondCount
  • additionalBondCountFromEarnings
  • activeBondCountAfterPurchase
  • reason

Important interpretation:

  • for ROR, reinvestment can happen even without a natural redemption that month, because interest is paid monthly
  • therefore sourceBondCount can be 0 while additionalBondCountFromEarnings > 0
  • this is the key signal that interest cash alone crossed a new 100 PLN threshold

Reinvestment Decision Ledger

The shared engine also stores reinvestment decisions for future reporting.

Each decision contains:

  • month
  • requestedBondCount
  • approvedBondCount
  • blockedBondCount
  • remainingMonthsAfterPurchase
  • canReachNaturalMaturity
  • expectedNetInterestPerBond
  • earlyRedemptionCostPerBond
  • reason

Interpretation:

  • approved
  • the requested reinvestment was allowed
  • insufficient-remaining-profitability
  • the engine intentionally left cash uninvested because a late bond purchase would likely lose money after early redemption cost

Snapshot Semantics

Monthly snapshots are created after:

  • monthly interest payout
  • natural redemptions
  • reinvestment
  • final early redemption if applicable

Snapshot fields:

  • grossValue
  • cash plus nominal value of active batches
  • liquidationValue
  • cash plus nominal value of active batches minus hypothetical early redemption costs for active batches
  • taxPaid
  • tax actually charged that month
  • earlyRedemptionCost
  • early redemption cost actually charged that month

Because ROR pays interest monthly, there is no unpaid accrued interest left between month-end snapshots in the current model.

Yearly Result Semantics

Yearly rows are created at months divisible by 12.

Current rule:

  • intermediate yearly grossValue = carried value + already realized taxes/costs
  • intermediate yearly netValue = carried portfolio value
  • final yearly netValue = liquidationValue

Why:

  • intermediate years should preserve both:
  • carried value after realized taxes
  • gross value before realized taxes/costs
  • final year should show actual realizable outcome

Public Result Contract

calculateRorBondResult(...) returns a normal BondCalculationResult.

In addition, it exposes strategy details through:

  • simulationDetails.strategy = monthly-income-ror
  • simulationDetails.purchaseEvents
  • simulationDetails.reinvestmentDecisions

That keeps the app API stable while preserving detailed simulation data for future reporting.

Tested Coverage

Current automated ROR tests cover:

  • shared profitability-guard approval and blocking boundaries
  • shared profitability-guard sensitivity to low-rate vs high-rate tails
  • first-month special rate
  • switch to reference-rate-based monthly rate
  • 12-month natural redemption
  • 1-month final early redemption
  • interest-driven reinvestment threshold with enough remaining horizon
  • late-horizon reinvestment blocked by profitability guard
  • naturally redeemed principal being kept in cash when the remaining tail is still unprofitable
  • initial purchase-event recording
  • reinvestment-decision recording
  • public engine routing through calculateBondResult

Test entry point:

  • npm run test:monthly-income
  • npm run test:ror

Invariants

The current implementation is expected to satisfy:

  • grossValue >= liquidationValue for every month snapshot
  • cash >= 0 for every month snapshot
  • no early redemption cost on natural 12-month redemption
  • final totals match the last month snapshot after final rounding

Change Management

If ROR logic changes, update together:

  • monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.ts
  • ror.ts: src/app/domain/bond-engine/strategies/ror.ts
  • ror.strategy.test.ts: tests/ror.strategy.test.ts
  • ROR_logic.md
  • init_prompt.md: ai/init_prompt.md if the product rules change

Future Extension Path

This design is intentionally shaped so DOR can reuse the same monthly-income engine by changing only:

  • cycle length from 12 to 24
  • first-month rate
  • later-month margin above NBP