Skip to content

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

ODS Logic

This file documents the current OTS calculation engine implementation.

The actual bond covered here is OTS (3-miesieczne obligacje skarbowe).

Document Metadata

  • Document status: handoff / implementation specification
  • Scope: current OTS engine only
  • Primary code reference:
  • ots.ts: src/app/domain/bond-engine/strategies/ots.ts
  • Related tests:
  • ots.strategy.test.ts: tests/ots.strategy.test.ts
  • Related prompt source:
  • init_prompt.md: ai/init_prompt.md
  • Last reviewed against code: 2026-04-08
  • Latest update: 2026-04-08
  • Added separate cash account (OKO) for after-tax interest earnings
  • Principal and interest separated in quarterly redemption
  • Reinvestment logic now includes OKO balance

Status

This is an implementation document, not a legal or product-source document.

It describes:

  • what the current code does
  • which modeling decisions are explicit
  • which semantics are still product assumptions

If the product specification changes, this file should be updated together with:

  • the strategy implementation
  • the automated tests
  • any user-facing documentation

Purpose

This document is intended to let another programmer:

  • understand the business logic of the OTS strategy
  • understand the technical model used by the engine
  • understand the meaning of outputs used by the UI
  • change the code safely with clear traceability to tests

Scope

Included in scope:

  • OTS monthly simulation
  • quarterly redemption logic
  • reinvestment logic
  • purchase-event history for future reporting
  • final partial-quarter settlement
  • monthly snapshots
  • yearly result derivation
  • profitability summary outputs

Out of scope:

  • other bond types
  • generic bond engine architecture outside the OTS strategy
  • legal validation of Treasury bond product rules
  • UI layout and presentation details
  • future non-annual user-input horizons

Glossary

  • bond unit price
  • nominal value of one bond, currently 100 PLN
  • batch
  • one purchase lot containing some number of bonds, all bought in the same month
  • natural redemption
  • standard settlement after a full 3-month OTS cycle
  • early redemption
  • settlement before completing the full 3-month cycle
  • cash
  • liquid uninvested amount currently not stored in active bond batches
  • gross value
  • valuation of cash plus active nominal value plus accrued gross interest
  • liquidation value
  • hypothetical or actual exit value after applying tax and early redemption cost where relevant
  • carried value
  • value of a still-running portfolio without forcing premature liquidation
  • quarter-aligned horizon
  • simulation horizon where totalMonths % 3 === 0
  • year-aligned horizon
  • simulation horizon where totalMonths % 12 === 0

Product Definition

Current OTS engine assumptions:

  • single bond nominal value: 100 PLN
  • bond duration: 3 months
  • interest rate: fixed, annualized
  • current catalog rate: 2.50%
  • capitalization: none
  • interest payment timing: only at redemption
  • tax rate: 19%
  • early redemption cost: 0 PLN per bond in the current bond catalog

Implemented rounding policy:

  • internal engine monetary values are rounded to 0.001 PLN
  • final summary outputs are rounded to 0.01 PLN
  • percentage display formatting remains a presentation concern outside this strategy

Current business interpretation in code:

  • a natural OTS cycle lasts exactly 3 months
  • before month 3 there is no cash payout
  • at month 3 the position is redeemed, taxed, and converted back to cash
  • available cash is then reinvested into as many full bonds as possible

Requirement / Implementation / Assumption Model

Each important rule in this document should be read in one of three categories:

  • Requirement
  • expected business behavior the engine must satisfy
  • Current Implementation
  • how the code currently realizes that behavior
  • Open Assumption
  • behavior currently chosen in code but still needing stronger product/legal confirmation

Business Requirements

BR-OTS-001 Product Duration

  • Requirement:
  • OTS must be modeled as a 3-month product, not a yearly product.

BR-OTS-002 Interest Timing

  • Requirement:
  • interest is not paid monthly
  • interest is realized only at redemption

BR-OTS-003 Natural Redemption

  • Requirement:
  • each batch must redeem after exactly 3 months
  • principal must return to reinvestment cash
  • net interest (after tax) must be accumulated in the cash account (OKO)

BR-OTS-004 Reinvestment

  • Requirement:
  • after natural redemption, all available cash from both reinvestment pool and accumulated interest (OKO account) should be used to buy as many full new bonds as possible

BR-OTS-005 Cash Remainder

  • Requirement:
  • any amount below 100 PLN that cannot buy a new bond remains in cash

BR-OTS-006 Early Redemption at Final Partial Quarter

  • Requirement:
  • if simulation ends before a batch reaches 3 months, that batch must be settled proportionally
  • any configured early redemption cost must apply

BR-OTS-007 Taxes

  • Requirement:
  • tax is charged on interest realized at settlement

BR-OTS-008 Monthly Simulation Axis

  • Requirement:
  • the internal engine must simulate month by month

BR-OTS-009 Yearly Reporting

  • Requirement:
  • the engine must expose annual reporting values for the rest of the app

BR-OTS-010 Deterministic Results

  • Requirement:
  • same inputs must always produce the same outputs

BR-OTS-011 Purchase History Retention

  • Requirement:
  • the engine must retain purchase events created during the simulation so future reporting layers can present them without replaying the simulation

Engine Scope

The OTS path is intentionally separate from the generic yearly bond calculation path.

Reason:

  • OTS is quarter-based, not year-based
  • reinvestment happens every quarter
  • yearly simulation would distort both tax timing and reinvestment timing

So the engine:

  • simulates month-by-month
  • settles natural redemptions every 3 months
  • emits monthly snapshots
  • emits purchase events for initial allocation and reinvestment actions
  • derives yearly rows from those monthly snapshots

Input Contract

simulateOtsBond(input) expects:

  • bondId
  • unit: enum-like bond identifier
  • expected current value: OTS
  • bond
  • unit: bond catalog definition
  • must contain valid OTS parameters
  • initialAmount
  • unit: PLN
  • expected range: >= 0
  • totalMonths
  • unit: months
  • expected range: integer >= 0
  • inflationRatePercent
  • unit: percent, e.g. 2.5 means 2.5%
  • config.bondUnitPrice
  • unit: PLN
  • current value: 100
  • config.taxRate
  • unit: decimal fraction
  • current value: 0.19

Relevant runtime types:

  • OtsSimulationInput
  • OtsMonthSnapshot
  • OtsSimulationResult
  • BondPurchaseEvent

Output Contract

simulateOtsBond(input) returns:

  • monthSnapshots
  • one item for each simulated month
  • yearlyResults
  • one item for each completed annual cut point only
  • purchaseEvents
  • one item for each initial allocation or quarterly reinvestment purchase
  • totalTaxPaid
  • cumulative tax actually charged during the simulation
  • totalEarlyRedemptionCosts
  • cumulative early redemption costs actually charged during the simulation
  • totalCashAccountInterestPaid
  • cumulative gross interest earned and accumulated in the OKO account
  • totalCashAccountTaxPaid
  • cumulative tax charged on interest accumulated in the OKO account
  • finalCashAccountBalance
  • final balance of the cash account (OKO) at simulation end
  • finalGrossValue
  • gross valuation of the final month snapshot
  • finalNetValue
  • final liquidation outcome of the simulation
  • totalNominalProfit
  • finalNetValue - initialAmount
  • totalRealProfit
  • inflation-adjusted profit vs initialAmount
  • irr
  • internal rate of return on final net value over totalMonths / 12
  • cagr
  • compound annual growth rate on final net value over totalMonths / 12

calculateOtsBondResult(...) additionally exposes the same purchase history through the shared reusable engine contract:

  • simulationDetails.strategy
  • current value: ots-quarterly-rollover
  • simulationDetails.purchaseEvents
  • mirrors simulateOtsBond(...).purchaseEvents

Preconditions

The strategy assumes:

  • initialAmount >= 0
  • totalMonths >= 0
  • bondUnitPrice > 0
  • taxRate >= 0
  • the bond object contains valid OTS data

The strategy does not currently enforce product-schema validation by itself. That validation is expected to happen at the catalog/interface layer.

Internal State

Core internal state:

  • cash
  • liquid uninvested amount available for bond purchases
  • cashAccountBalance
  • accumulated after-tax interest stored in the OKO account (cash account)
  • positions
  • purchaseEvents
  • totalTaxPaid
  • totalEarlyRedemptionCosts
  • totalCashAccountTaxPaid
  • cumulative taxes on interest in the OKO account
  • totalCashAccountInterestPaid
  • cumulative gross interest earned
  • yearTaxPaid
  • yearEarlyRedemptionCosts
  • yearCashAccountTaxPaid
  • yearly tax on OKO account interest (reset at year boundaries)

Each active position stores:

  • bonds: number of bonds in the batch
  • purchaseMonth: month when the batch was created

Why batches exist:

  • each quarterly purchase becomes a separate lot
  • each lot can be at a different age
  • this is necessary for correct quarter-end and partial-final-quarter settlement

Important distinction:

  • positions are the invested lots
  • cash is the liquid remainder not currently invested
  • purchaseEvents is the reporting ledger of buy actions taken during simulation

The engine derives totals from:

  • cash mutations
  • purchase events
  • per-month tax/cost events
  • per-year aggregated tax/cost counters

High-Level Flow

flowchart TD
    A[Start simulation] --> B[Buy initial full bonds]
    B --> C[Loop month = 1..totalMonths]
    C --> D{Is month divisible by 3?}
    D -- Yes --> E[Settle all mature 3-month positions]
    E --> E2["Split redemption:<br/>Principal → cash<br/>Net interest → OKO"]
    E2 --> F[Compute available for reinvestment<br/>cash + cashAccountBalance]
    F --> G[Reinvest into full new bonds]
    G --> G2["Clear OKO balance<br/>after purchase"]
    D -- No --> H[Keep positions active]
    G2 --> I{Is this the final month and not quarter-end?}
    H --> I
    I -- Yes --> J[Early redeem all remaining positions]
    I -- No --> K[Build month snapshot<br/>with OKO details]
    J --> K
    K --> L{Final month?}
    L -- Yes --> L2["Consolidate OKO to cash<br/>for final valuation"]
    L2 --> M[Build final result]
    L -- No --> N{Month divisible by 12?}
    N -- Yes --> O[Build yearly result]
    N -- No --> P[Continue]
    O --> P
    P --> Q{More months?}
    Q -- Yes --> C
    Q -- No --> M

Interest Distribution Model

The OTS engine implements a two-account model to properly track interest flows:

Account Roles

Reinvestment Pool (cash) - Holds principal amounts from bond redemptions - Used for purchasing new bonds - Any remainder below 100 PLN persists here - Carries forward to next quarter

Cash Account (cashAccountBalance) - Holds after-tax interest earned from quarterly redemptions - Does not directly purchase bonds, but feeds into reinvestment pool - Tax on interest is tracked separately from principal-level tax - Consolidated to cash at the final month for correct final valuation

Quarterly Settlement Distribution

When a batch of bonds matures after exactly 3 months:

Gross Interest = bonds × 100 × annualRate × (3/12)
Tax on Interest = Gross Interest × 0.19
Net Interest = Gross Interest - Tax on Interest
Principal = bonds × 100

Distribution:
  cash += Principal
  cashAccountBalance += Net Interest
  totalTaxPaid += Tax on Interest

Reinvestment Sourcing

At each quarterly redemption point, reinvestment uses both sources:

Available for Reinvestment = cash + cashAccountBalance
New Bonds = floor(Available / 100)
Remaining = Available % 100

After Purchase:
  cash = Remaining
  cashAccountBalance = 0

This design ensures: - Interest can compound through reinvestment - Tax impact is transparent and tracked separately - Final valuation is accurate

Diagram of a complete quarterly cycle:

sequenceDiagram
    participant B as Active Bonds
    participant C as Cash Pool
    participant O as OKO Account
    participant N as New Bonds

    B->>B: Hold for 3 months
    B->>C: Principal (at maturity)
    B->>O: Net Interest (after tax)
    C->>C: Old leftovers continue
    C->>N: Combine with OKO
    O->>N: Contribute to purchase pool
    N->>B: Create new batch

Initialization

Current Implementation:

  1. cash = initialAmount
  2. buy as many full bonds as possible:
  3. initialBondCount = floor(cash / bondUnitPrice)
  4. create one initial batch if initialBondCount > 0
  5. reduce cash by purchased nominal value

Example for 1050 PLN:

  • buy 10 bonds
  • cash = 50 PLN
  • create batch:
  • bonds = 10
  • purchaseMonth = 0
  • create purchase event:
  • month = 0
  • reason = initial-allocation
  • purchasedBondCount = 10
  • sourceBondCount = 0
  • additionalBondCountFromEarnings = 0
  • cashBeforePurchase = 1050
  • cashAfterPurchase = 50

If initialAmount < 100 PLN:

  • no bonds are purchased
  • all value remains in cash
  • the simulation still runs, but there are no active positions

Natural Quarterly Redemption

Current Implementation:

Every month divisible by 3:

  1. inspect all active positions
  2. compute age:
  3. ageInMonths = currentMonth - purchaseMonth
  4. if ageInMonths === 3, redeem that position naturally

Natural redemption formula:

  • grossInterest = bonds * bondUnitPrice * annualRate * (3 / 12)
  • taxPaidOnInterest = grossInterest * taxRate
  • netInterest = grossInterest - taxPaidOnInterest
  • principal = bonds * bondUnitPrice

Interest distribution:

  • principal is added to cash (for reinvestment)
  • netInterest is added to cashAccountBalance (OKO account with after-tax interest)
  • taxPaidOnInterest is tracked in monthTaxPaid and totalTaxPaid

Important:

  • there is no monthly interest payout
  • accrued interest exists economically before maturity, but cash appears only at settlement
  • redemption is batch-based, not global-account based
  • different batches may redeem in the same month
  • principal and interest are now separated into different accounts for proper tracking

For reinvestment reporting, the engine also computes:

  • sourceBondCount
  • how many matured bonds produced the reinvestment cash pool in that month
  • additionalBondCountFromEarnings
  • purchasedBondCount - sourceBondCount, floored at 0
  • this explicitly captures when earnings and leftover cash were enough to buy extra bonds

Reinvestment Rule

Requirement:

  • all available cash from both reinvestment pool and accumulated interest (OKO account) should be used to buy as many full new bonds as possible

Current Implementation:

After all natural quarterly redemptions are settled:

  1. compute total available:
  2. availableCashForReinvestment = cash + cashAccountBalance
  3. compute:
  4. reinvestedBondCount = floor(availableCashForReinvestment / bondUnitPrice)
  5. if reinvestedBondCount > 0 and current month is not the final month:
  6. create new batch with:
    • bonds = reinvestedBondCount
    • purchaseMonth = currentMonth
  7. reduce both accounts:
    • reinvestmentCost = reinvestedBondCount * bondUnitPrice
    • cash += cashAccountBalance - reinvestmentCost
    • cashAccountBalance = 0

This means:

  • principal from mature batches is reinvested
  • accumulated after-tax interest from OKO is reinvested
  • old leftover cash is also used
  • any remainder below 100 PLN stays in cash

Important:

  • reinvestment happens after all mature batches in the month are settled
  • both cash and OKO are considered when determining reinvestment capacity
  • OKO is emptied and its funds merged into cash after purchase
  • reinvestment is skipped in the final month, because the engine is terminating

Final Month Early Redemption

Requirement:

  • only non-mature active batches at the end of the simulation use the early-redemption path

Current Implementation:

If the final month is not quarter-aligned:

  • all remaining active positions are redeemed early

For each remaining position:

  • ageInMonths = finalMonth - purchaseMonth
  • grossInterest = bonds * bondUnitPrice * annualRate * (ageInMonths / 12)
  • earlyRedemptionCost = bonds * bond.earlyRedemptionCost
  • grossValue = principal + grossInterest
  • taxableProfit = max(0, grossValue - principal - earlyRedemptionCost)
  • taxPaid = taxableProfit * taxRate
  • cash inflow:
  • grossValue - earlyRedemptionCost - taxPaid

Then:

  • add this amount to cash
  • clear all positions

Important:

  • partial-quarter interest is proportional
  • early redemption cost is charged per bond when the catalog defines one
  • this happens only at final non-quarter-aligned termination

If the final month is quarter-aligned:

  • no early redemption path is used
  • all naturally maturing batches are settled via the standard quarterly path
  • no active positions should remain after settlement

Accrued Interest Function

The helper used by the strategy is logically:

accruedInterest = bonds * bondUnitPrice * annualRateDecimal * (monthsHeld / 12)

This is used in two contexts:

  • real settlement of a batch at natural or early redemption
  • snapshot valuation of active positions

Monthly Snapshot Semantics

For every simulated month the strategy produces OtsMonthSnapshot.

Fields:

  • month
  • cash
  • cashAccountBalance
  • balance of the OKO account (accumulated after-tax interest)
  • activeBondCount
  • grossValue
  • liquidationValue
  • taxPaid
  • tax actually charged in that month (on any redemptions)
  • cashAccountTaxPaid
  • tax charged in that month on interest accumulated in OKO
  • earlyRedemptionCost
  • hadNaturalRedemption
  • hadEarlyRedemption

grossValue

Current meaning:

  • cash
  • plus cashAccountBalance (OKO account with after-tax interest)
  • plus nominal value of all active batches
  • plus accrued but not yet paid gross interest on active batches

This is a valuation view, not a realized-cash view.

liquidationValue

Current meaning:

  • cash
  • plus cashAccountBalance (OKO account)
  • plus nominal value of all active batches
  • plus accrued interest on active batches
  • minus tax that would be due if those active batches were liquidated now
  • minus early redemption cost that would be due if those active batches were liquidated now

This is a hypothetical immediate-exit value.

Event fields

  • taxPaid: tax actually charged in that month
  • earlyRedemptionCost: early redemption cost actually charged in that month
  • hadNaturalRedemption: at least one natural quarterly redemption occurred in that month
  • hadEarlyRedemption: early redemption occurred in that month

Snapshot timing:

  • the monthly snapshot is created after that month’s settlement and reinvestment logic
  • therefore the snapshot represents end-of-month state, not beginning-of-month state

Purchase Event Ledger

The engine stores a purchase-event ledger for future reporting.

Each BondPurchaseEvent contains:

  • kind
  • current value: purchase
  • month
  • 0 for initial allocation
  • quarter-end month for reinvestment
  • purchasedBondCount
  • how many full bonds were bought in that action
  • bondUnitPrice
  • currently 100 PLN
  • cashBeforePurchase
  • available cash immediately before the purchase
  • cashAfterPurchase
  • uninvested cash remainder immediately after the purchase
  • sourceBondCount
  • number of matured bonds that generated the reinvestment pool
  • 0 for the initial allocation event
  • additionalBondCountFromEarnings
  • number of extra bonds created beyond simple like-for-like rollover
  • 0 when reinvestment only restores the same bond count
  • activeBondCountAfterPurchase
  • count of active bonds immediately after the purchase action
  • reason
  • initial-allocation or reinvestment

Interpretation rules:

  • there is at most one purchase event in a given month
  • quarter-end months can still have a reinvestment event with additionalBondCountFromEarnings = 0
  • the first event with additionalBondCountFromEarnings > 0 is the first point where accumulated earnings plus leftover cash bought extra bonds
  • no purchase event is recorded in the final month if the simulation ends after settlement instead of reinvesting

Yearly Snapshot Semantics

Yearly rows are created only for months divisible by 12.

This has an important consequence:

  • if totalMonths < 12, the simulation returns monthly snapshots and final totals
  • but it returns no yearly rows

So:

  • monthSnapshots always reflect the true simulated timeline
  • purchaseEvents always reflect the true simulated purchase timeline
  • yearlyResults exist only for completed annual cut points
sequenceDiagram
    participant M as Monthly Engine
    participant S as Month Snapshot
    participant Y as Yearly Result

    M->>S: Compute month 12 snapshot
    S->>Y: grossValue = snapshot.grossValue
    alt final year
        S->>Y: netValue = snapshot.liquidationValue
    else intermediate year
        S->>Y: netValue = snapshot.grossValue
    end
    S->>Y: attach year tax and early redemption totals
    S->>Y: derive nominal and real profits

Current rule:

  • for non-final yearly rows:
  • grossValue = carried value + already realized taxes/costs
  • netValue = carried portfolio value
  • for final yearly row:
  • netValue = liquidationValue

Why:

  • intermediate years should represent carried portfolio value, not fake forced liquidation
  • intermediate years should still preserve gross-before-tax semantics
  • final row should represent actual final realizable outcome

This rule was introduced to fix a bug where OTS showed negative year-1 nominal profit for quarter-aligned multi-year cases.

Practical meaning:

  • yearly grossValue is always a valuation number
  • yearly netValue is a reporting number whose meaning depends on whether the row is intermediate or final

That semantic difference is intentional in the current implementation and should be preserved unless product requirements explicitly change.

Final Result Semantics

Final result fields:

  • finalGrossValue = lastMonthSnapshot.grossValue
  • finalNetValue = lastMonthSnapshot.liquidationValue
  • totalNominalProfit = finalNetValue - initialAmount
  • totalRealProfit = finalRealValue - initialAmount
  • irr = calculateIrr(initialAmount, finalNetValue, totalMonths / 12)
  • cagr = calculateCagr(initialAmount, finalNetValue, totalMonths / 12)

Observation:

  • for quarter-aligned final horizons, finalNetValue is the realized final outcome
  • for non-quarter-aligned final horizons, finalNetValue includes proportional interest and early redemption costs

Important distinction:

  • finalGrossValue is the last gross valuation
  • finalNetValue is the final liquidation outcome

For quarter-aligned horizons, those two values may still differ at snapshot level conceptually, but in practice finalNetValue is the number used for final profitability metrics.

Mathematical Summary

Precision and Rounding Policy

This policy is now implemented in code.

Rules:

  • intermediate monetary calculations are normalized to 0.001 PLN
  • monthly snapshots store monetary values at 0.001 PLN
  • yearly result rows store monetary values at 0.001 PLN
  • purchase-event cash values are stored at 0.001 PLN
  • final summary values are rounded to 0.01 PLN

Current implementation helpers:

  • money.ts: src/app/domain/bond-engine/money.ts

Implemented functions:

  • roundCurrencyInternal(value) -> 0.001 PLN
  • roundCurrencyFinal(value) -> 0.01 PLN

Practical consequence:

  • detailed engine state preserves more precision for simulation continuity
  • final business-facing totals are rounded to grosz
  • sum of yearly rows may differ from displayed final totals by a few thousandths before final rounding

Natural quarter settlement

grossInterest = bonds * 100 * annualRate * 3/12
taxPaidOnInterest = grossInterest * 0.19
netInterest = grossInterest - taxPaidOnInterest
principal = bonds * 100

outcome:
- principal → cash (reinvestment account)
- netInterest → cashAccountBalance (OKO account)
- taxPaidOnInterest → tracked in month and total tax counters

Early settlement at final non-quarter month

grossInterest = bonds * 100 * annualRate * heldMonths/12
cost = bonds * configuredEarlyRedemptionCost
taxableProfit = max(0, grossInterest - cost)
tax = taxableProfit * 0.19
earlyNet = bonds * 100 + grossInterest - cost - tax

Reinvestment

newBondCount = floor(cash / 100)
cashRemainder = cash - newBondCount * 100

Worked Examples

Example A: 1000 PLN, 12 months, 0% inflation

Initial state:

  • buy 10 bonds
  • cash = 0
  • cashAccountBalance = 0

Quarter 1 (month 3):

  • gross interest = 10 * 100 * 0.025 * 3/12 = 6.25
  • tax on interest = 6.25 * 0.19 = 1.1875
  • net interest = 4.0625
  • principal = 1000

Distribution:

  • cash += 1000 → cash now 1000
  • cashAccountBalance += 4.0625 → OKO now 4.0625
  • totalTaxPaid += 1.1875

Reinvestment (month 3):

  • available = 1000 + 4.0625 = 1004.0625
  • buy 10 new bonds (at 100 each)
  • cash = 4.0625 (remainder)
  • cashAccountBalance = 0 (merged into cash for purchase)

Quarter 2:

  • redeem 10 bonds
  • receive next 4.0625 net interest to OKO
  • available for reinvestment = 4.0625 + 4.0625 = 8.125
  • can't buy more bonds yet

End of year (quarter 4):

  • current tested expected value:
  • netValue = 1020.25 (approximately)
  • nominalProfit = 20.25
  • cashAccountBalance = 0 (consolidated at final month)

Example B: 100 PLN, 1 month, 0% inflation

This is a forced early exit.

  • proportional interest for 1/12 year
  • tax is charged on profit remaining after any configured early-redemption cost
  • the current catalog applies 0 PLN early redemption cost

Current tested expected value:

  • finalNetValue = 100.17

Example C: threshold reinvestment

Case:

  • starting amount large enough that quarter proceeds plus leftover cash cross one additional 100 PLN boundary

Example:

  • 20000 PLN
  • after first quarter the simulation buys 201 bonds, not 200

Reason:

  • net quarter proceeds plus remainder exceed 20100 PLN

Stored event semantics:

  • the month-3 purchase event records:
  • sourceBondCount = 200
  • purchasedBondCount = 201
  • additionalBondCountFromEarnings = 1

Mermaid State Model

classDiagram
    class OtsPosition {
        +number bonds
        +number purchaseMonth
    }

    class OtsMonthSnapshot {
        +number month
        +number cash
        +number cashAccountBalance
        +number activeBondCount
        +number grossValue
        +number liquidationValue
        +number taxPaid
        +number cashAccountTaxPaid
        +number earlyRedemptionCost
        +boolean hadNaturalRedemption
        +boolean hadEarlyRedemption
    }

    class BondPurchaseEvent {
        +string kind
        +number month
        +number purchasedBondCount
        +number bondUnitPrice
        +number cashBeforePurchase
        +number cashAfterPurchase
        +number sourceBondCount
        +number additionalBondCountFromEarnings
        +number activeBondCountAfterPurchase
        +string reason
    }

    class OtsSimulationResult {
        +OtsMonthSnapshot[] monthSnapshots
        +YearlyResult[] yearlyResults
        +BondPurchaseEvent[] purchaseEvents
        +number totalTaxPaid
        +number totalEarlyRedemptionCosts
        +number totalCashAccountTaxPaid
        +number totalCashAccountInterestPaid
        +number finalGrossValue
        +number finalNetValue
        +number finalCashAccountBalance
        +number totalNominalProfit
        +number totalRealProfit
        +number irr
        +number cagr
    }

    OtsSimulationResult --> OtsMonthSnapshot
    OtsSimulationResult --> OtsPosition
    OtsSimulationResult --> BondPurchaseEvent

Interface and Data Flow

flowchart LR
    A[Bond catalog OTS definition] --> B[simulateOtsBond]
    C[Investment params] --> B
    D[Engine config] --> B
    B --> E[Month snapshots]
    B --> F[Purchase events]
    B --> G[Yearly results]
    B --> H[Final totals]
    B --> I[IRR / CAGR]

Invariants

The current implementation is expected to satisfy:

  • grossValue >= liquidationValue for every monthly snapshot
  • cash >= 0 for every monthly snapshot
  • cashAccountBalance >= 0 for every monthly snapshot
  • final totals must match the last snapshot
  • totalTaxPaid must equal the sum of actual tax events (on interest at redemption)
  • totalCashAccountTaxPaid must equal the sum of taxes on OKO interest
  • totalEarlyRedemptionCosts must equal the sum of actual early-redemption events
  • if final month is quarter-aligned, no active positions remain after final settlement
  • if the yearly row is not final, it must not report false early-redemption losses caused by hypothetical liquidation

Edge Cases

The current engine explicitly supports these edge cases:

  • initialAmount < 100 PLN
  • leftover cash from initial purchase
  • reinvestment thresholds where remainder plus net proceeds buys an extra bond
  • stored purchase history for both initial allocation and reinvestment
  • final horizon ending after 1 or 2 months into a quarter
  • large starting principal producing many batches over time

The current engine does not yet formalize:

  • legal/product confirmation of the exact proportional-interest rule for every early-redemption case
  • alternative reporting semantics for intermediate yearly netValue

Tested Coverage

The current automated OTS tests cover:

  • sub-100 PLN no-purchase case
  • exact 3, 6, 12 month quarter-aligned cases
  • multiple starting bond counts on quarter end
  • leftover cash carry-over
  • initial purchase-event recording
  • reinvestment threshold just below and above extra-bond purchase
  • first stored event with additionalBondCountFromEarnings > 0
  • no reinvestment event recorded on the terminal month
  • partial final quarter exits for 1 and 2 months
  • yearly regression for 1000 PLN / 10 years
  • internal consistency between month snapshots, yearly rows, and totals
  • gross vs liquidation invariants across a scenario grid

Current test entry point:

  • npm run test:ots

Test intent:

  • deterministic example tests protect known business scenarios
  • matrix tests protect thresholds and multi-scenario consistency
  • invariant tests protect refactors from silently breaking accounting relationships

Requirement-to-Test Traceability

High-level traceability:

  • BR-OTS-001, BR-OTS-002, BR-OTS-003
  • covered by quarter-end tests and closed-form quarter settlement tests
  • BR-OTS-004, BR-OTS-005
  • covered by reinvestment-threshold and leftover-cash tests
  • BR-OTS-006
  • covered by 1 month and 2 month final partial-quarter tests
  • BR-OTS-007
  • covered by tax-total consistency tests and quarter formula checks
  • BR-OTS-008
  • covered indirectly by month-snapshot and partial-quarter tests
  • BR-OTS-009
  • covered by yearly regression and yearly consistency tests
  • BR-OTS-010
  • covered by deterministic fixed-input assertions
  • BR-OTS-011
  • covered by purchase-event recording tests and public-contract exposure tests

Implementation Changes (2026-04-08)

A major refactoring was completed to separate interest flows from principal flows:

What Changed

Before: - At quarterly redemption, principal and net interest both fed into the same cash pool - Reinvestment was computed from cash only - No separate tracking of interest as it accumulated

After: - At quarterly redemption, principal → cash (reinvestment pool) and net interest → cashAccountBalance (OKO account) - Reinvestment considers both cash + cashAccountBalance - Tax on interest is tracked separately from principal-level tax - totalCashAccountInterestPaid and totalCashAccountTaxPaid are now exposed as output - Final month consolidates OKO back to cash for correct final valuation

Why This Matters

This separation enables: 1. Proper interest tracking - distinction between earned interest and reinvested principal 2. Future flexibility - other strategies can now use similar OKO-account patterns 3. Correct reinvestment reporting - shows what came from principal vs. accumulated interest 4. Tax transparency - makes tax on interest explicit and separate

Code Impact

Files updated: - ots.ts - Refactored natural redemption handling, reinvestment logic, final consolidation - ots.strategy.test.ts - All 24 tests passing with new behavior

Test Status

All 24 OTS tests pass:

✅ Exact quarter-end settlements
✅ Multiple starting bond counts
✅ Quarterly reinvestments with no leftover bonds
✅ Leftover cash preservation and consolidation
✅ Multi-year profitability progression
✅ Reinvestment on 100 PLN threshold
✅ Partial quarter exits
✅ Invariant relationships (gross ≥ liquidation)
✅ Accounting consistency across snapshots

Known Design Decisions

These are currently implemented decisions, not universal finance truths:

  1. Tax is computed as raw percentage multiplication without explicit cent rounding rules.
  2. Monetary values are normalized to 0.001 PLN during engine execution.
  3. Final summary outputs are rounded to 0.01 PLN.
  4. Early redemption cost is modeled proportionally per active bond on final non-quarter liquidation.
  5. Intermediate yearly netValue is treated as carried portfolio value, not hypothetical forced liquidation.
  6. Final result uses liquidation semantics.

These should be treated as contract points for the current engine until product requirements say otherwise.

Open Assumptions / Questions

These areas still need stronger product or legal confirmation:

  • whether intermediate annual netValue should be carried value or hypothetical liquidation value in product language
  • whether proportional early-settlement interest exactly matches official product behavior in every edge case
  • whether UI should ever expose sub-annual horizons directly

Known Risks / Follow-Ups

Remaining non-trivial risks are mostly specification risks, not algorithm-coverage risks:

  • legal/product confirmation may still be needed for exact early-redemption semantics
  • yearly reporting semantics may need separate product approval because netValue has different meaning for intermediate vs final rows
  • if future UI allows non-annual horizons, yearly-only summary assumptions will need extension

Change Management

If OTS logic changes, update together:

  • ots.ts: src/app/domain/bond-engine/strategies/ots.ts
  • ots.strategy.test.ts: tests/ots.strategy.test.ts
  • OTS_logic.md
  • init_prompt.md: ai/init_prompt.md if the product spec changes

Do not change only one of these in isolation unless the change is purely editorial.

Handoff Checklist

Before another programmer changes OTS, they should:

  1. Read this document fully.
  2. Read ots.ts: src/app/domain/bond-engine/strategies/ots.ts.
  3. Run npm run test:ots.
  4. Identify whether the intended change is:
  5. a bug fix
  6. a business rule change
  7. a reporting semantics change
  8. If it is a business rule change, align prompt/spec/tests/code together.
  9. Re-run:
  10. npm run test:ots
  11. npm run build

Suggested Future Extensions

If this engine evolves further, this document should be extended with:

  • formal examples with manual calculations in table form
  • per-test traceability table with exact test names
  • comparison with ROR/DOR, TOS, COI, and EDO/ROS/ROD strategy families