Skip to content

Generated file. Source: docs/ODS_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 filename is kept as requested: ODS_logic.md. 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-03-23

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: 3 PLN per bond

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 and net interest must return to cash at that point

BR-OTS-004 Reinvestment

  • Requirement:
  • after natural redemption, all available cash 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
  • 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
  • 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
  • positions
  • purchaseEvents
  • totalTaxPaid
  • totalEarlyRedemptionCosts
  • yearTaxPaid
  • yearEarlyRedemptionCosts

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 --> F[Add principal and net interest to cash]
    F --> G[Reinvest cash into full new bonds]
    D -- No --> H[Keep positions active]
    G --> 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]
    J --> K
    K --> L{Is month divisible by 12?}
    L -- Yes --> M[Build yearly result]
    L -- No --> N[Continue]
    M --> N
    N --> O{More months?}
    O -- Yes --> C
    O -- No --> P[Build final simulation result]

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)
  • taxPaid = grossInterest * taxRate
  • netCashInflow = principal + grossInterest - taxPaid

Then:

  • add netCashInflow to cash
  • remove redeemed batch from positions

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

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 should be used to buy as many full new bonds as possible

Current Implementation:

After all natural quarterly redemptions are settled:

  1. compute:
  2. reinvestedBondCount = floor(cash / bondUnitPrice)
  3. if reinvestedBondCount > 0 and current month is not the final month:
  4. create new batch with:
    • bonds = reinvestedBondCount
    • purchaseMonth = currentMonth
  5. reduce cash by purchased nominal value

This means:

  • principal from mature batches is reinvested
  • net interest 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
  • 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)
  • taxPaid = grossInterest * taxRate
  • earlyRedemptionCost = bonds * bond.earlyRedemptionCost
  • cash inflow:
  • principal + grossInterest - taxPaid - earlyRedemptionCost

Then:

  • add this amount to cash
  • clear all positions

Important:

  • partial-quarter interest is proportional
  • early redemption cost is charged per bond
  • 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
  • activeBondCount
  • grossValue
  • liquidationValue
  • taxPaid
  • earlyRedemptionCost
  • hadNaturalRedemption
  • hadEarlyRedemption

grossValue

Current meaning:

  • cash
  • 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 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
tax = grossInterest * 0.19
quarterNet = bonds * 100 + grossInterest - tax

Early settlement at final non-quarter month

grossInterest = bonds * 100 * annualRate * heldMonths/12
tax = grossInterest * 0.19
cost = bonds * 3
earlyNet = bonds * 100 + grossInterest - tax - cost

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

Quarter 1:

  • gross interest = 10 * 100 * 0.025 * 3/12 = 6.25
  • tax = 6.25 * 0.19 = 1.1875
  • cash after redemption = 1005.0625
  • buy 10 new bonds
  • carry 5.0625 PLN cash

Quarter 2:

  • redeem 10 bonds
  • receive another 1005.0625
  • add previous cash remainder
  • buy 10 new bonds
  • carry larger cash remainder

End of year:

  • current tested expected value:
  • netValue = 1020.25
  • nominalProfit = 20.25

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

This is a forced early exit.

  • proportional interest for 1/12 year
  • tax is charged on that proportional interest
  • 3 PLN early redemption cost applies

Current tested expected value:

  • finalNetValue = 97.16875

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 activeBondCount
        +number grossValue
        +number liquidationValue
        +number taxPaid
        +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 finalGrossValue
        +number finalNetValue
        +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
  • final totals must match the last snapshot
  • totalTaxPaid must equal the sum of actual tax events
  • 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

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
  • ODS_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