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.

Important: ROR now has two distinct production paths:

  • reinvest
  • uses the shared monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.ts family
  • oko
  • uses the shared Excel-aligned helper excel-like-oko.ts: src/app/domain/bond-engine/strategies/shared/excel-like-oko.ts
  • reproduces the workbook-style monthly liquidation checkpoints, exchange at 99.90, protection window for the early-redemption fee, and separate OKO balance growth

Document Metadata

  • Document status: handoff / implementation specification
  • Scope: current ROR engine, including both the reinvestment path and the Excel-aligned OKO path
  • Primary code references:
  • ror.ts: src/app/domain/bond-engine/strategies/ror.ts
  • monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.ts
  • excel-like-oko.ts: src/app/domain/bond-engine/strategies/shared/excel-like-oko.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-04-04

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 or transfer to OKO
  • 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: catalog-driven, currently 4.00%
  • 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 can either go to reinvestment cash or to a separate OKO balance
  • 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: 0.50 PLN per bond
  • exchange price used by the Excel-aligned OKO path: 99.90 PLN
  • protection window used by the Excel-aligned OKO path: 1 month

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.

For reinvest it uses the shared monthly-income strategy family. For oko it uses a separate Excel-aligned rollover helper.

  • shared module:
  • monthly-income.ts: src/app/domain/bond-engine/strategies/monthly-income.ts
  • Excel-like OKO helper:
  • excel-like-oko.ts: src/app/domain/bond-engine/strategies/shared/excel-like-oko.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{interestPayoutMode}
    D -->|reinvest| E[simulateMonthlyIncomeBond]
    D -->|oko| F[simulateExcelLikeOkoBond]
    E --> G[Month snapshots]
    E --> H[Purchase events]
    E --> I[Yearly results]
    F --> H
    F --> I

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
  • optional OKO transfer and monthly OKO interest accrual
  • 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.00%
  • 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
  • cashAccountBalance
  • 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.00%
  5. later months -> referenceRate
  6. Depending on interestPayoutMode, add net interest to reinvestment cash or transfer it to OKO.
  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 0.50 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 0.50 PLN per bond
  • does not add extra monthly interest beyond what the engine already paid during simulated months

OKO Mode

When interestPayoutMode = oko:

  • the engine switches to the Excel-aligned monthly liquidation model
  • monthly net interest is transferred to a separate OKO balance
  • OKO accrues interest monthly using the configured annual rate
  • OKO interest is taxed on an ongoing basis
  • at natural maturity the next cycle uses:
  • exchanged value of redeemed bonds at 99.90
  • additional full hundreds from the existing OKO balance
  • the maturity-month transfer to OKO contains:
  • the current monthly net coupon
  • the exchange bonus coming from the 99.90 purchase price
  • OKO funds do not otherwise participate in reinvestment decisions
  • yearly rows in this mode represent workbook-style year-end liquidation checkpoints, not only carried portfolio state

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 0.50 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