# Financial Metrics Specification ## 1. Purpose Define the standard methodology CalMiner uses to compute discounted-cash-flow profitability metrics, including Net Present Value (NPV), Internal Rate of Return (IRR), and Payback Period. These calculations underpin scenario evaluation, reporting, and investment decision support within the platform. ## 2. Scope - Applies to scenario-level profitability analysis using cash flows stored in the application database. - Covers deterministic cash-flow evaluation; stochastic extensions (e.g., Monte Carlo) may overlay these metrics but should reference this specification. - Documents the assumptions implemented in `services/financial.py` and the related pytest coverage in `tests/test_financial.py`. ## 3. Inputs and Definitions | Symbol | Name | Description | Units / Domain | Notes | | -------- | ----------------------- | ------------------------------------------- | ----------------- | --------------------------------------------------------- | | $CF_t$ | Cash flow at period $t$ | Currency amount (positive or negative) | Scenario currency | Negative values typically represent investments/outflows. | | $t$ | Period index | Fractional period (0 = anchor) | Real number | Derived from explicit index or calendar date. | | $r$ | Discount rate | Decimal representation of the annual rate | $r > -1$ | Scenario configuration provides default rate. | | $m$ | Compounds per year | Compounding frequency | Positive integer | Defaults to 1 (annual). | | $RV$ | Residual value | Terminal value realised after final period | Scenario currency | Optional. | | $t_{RV}$ | Residual periods | Timing of residual value relative to anchor | Real number | Defaults to last period + 1. | ### Period Anchoring and Timing Cash flows can be supplied with either: 1. `period_index` — explicit integers/floats overriding all other timing. 2. `date` — calendar dates. The earliest dated flow anchors the timeline and subsequent dates convert the day difference into fractional periods using a 365-day year divided by `compounds_per_year`. 3. Neither — flows default to sequential periods based on input order. This aligns with `normalize_cash_flows` in `services/financial.py`, ensuring all calculations receive `(amount, periods)` tuples. ## 4. Net Present Value (NPV) ### Formula For a set of cash flows $CF_t$ with discount rate $r$ and compounding frequency $m$: $$ \text{NPV} = \sum_{t=0}^{n} \frac{CF_t}{\left(1 + \frac{r}{m}\right)^{t}} + \begin{cases} \dfrac{RV}{\left(1 + \frac{r}{m}\right)^{t_{RV}}} & \text{if residual value present} \\ 0 & \text{otherwise} \end{cases} $$ ### Implementation Notes - `discount_factor` computes $(1 + r/m)^{-t}`; NPV iterates over the normalised flows and sums `amount \* factor`. - Residual values default to one period after the final cash flow when `residual_periods` is omitted. - Empty cash-flow sequences return 0 unless a residual value is supplied. ## 5. Internal Rate of Return (IRR) ### Definition IRR is the discount rate $r$ for which NPV equals zero: $$ 0 = \sum_{t=0}^{n} \frac{CF_t}{\left(1 + \frac{r}{m}\right)^{t}} $$ ### Solver Behaviour - Newton–Raphson iteration starts from `guess` (default 10%). - Derivative instability or non-finite values trigger a fallback to a bracketed bisection search between: - Lower bound: $-0.99 \times m$ - Upper bound: $10.0$ (doubles until the root is bracketed or attempts exceed 12) - Raises `ConvergenceError` when no sign change is found or the bisection fails within the iteration budget (`max_iterations` default 100; bisection uses double this limit). - Validates that the cash-flow series includes at least one negative and one positive value; otherwise IRR is undefined and a `ValueError` is raised. ### Caveats - Multiple sign changes may yield multiple IRRs. The solver returns the root it finds within the configured bounds; scenarios must interpret the result in context. - Rates less than `-1 * m` imply nonphysical periodic rates and are excluded. ## 6. Payback Period ### Definition The payback period is the earliest period $t$ where cumulative cash flows become non-negative. With fractional interpolation (default behaviour), the period is calculated as: $$ \text{Payback} = t_{prev} + \left(\frac{-\text{Cumulative}_{prev}}{CF_t}\right) \times (t - t_{prev}) $$ where $t_{prev}$ is the previous period with negative cumulative cash flow. ### Implementation Notes - Cash flows are sorted by period to ensure chronological accumulation. - When `allow_fractional` is `False`, the function returns the first period with non-negative cumulative total without interpolation. - `PaybackNotReachedError` is raised if the cumulative total never becomes non-negative. ## 7. Examples ### Example 1: Baseline Project - Initial investment: $-1,000,000$ at period 0. - Annual inflows: 300k, 320k, 340k, 360k, 450k (periods 1-5). - Discount rate: 8% annual, `compounds_per_year = 1`. | Period | Cash Flow (currency) | | ------ | -------------------- | | 0 | -1,000,000 | | 1 | 300,000 | | 2 | 320,000 | | 3 | 340,000 | | 4 | 360,000 | | 5 | 450,000 | - `net_present_value` ≈ 205,759 - `internal_rate_of_return` ≈ 0.158 - `payback_period` ≈ 4.13 periods ### Example 2: Residual Value with Irregular Timing - Investment: -500,000 on 2024-01-01 - Cash inflows on irregular dates (2024-07-01: 180k, 2025-01-01: 200k, 2025-11-01: 260k) - Residual value 150k realised two years after final inflow - Discount rate: 10%, `compounds_per_year = 4` NPV discounts each cash flow by converting day deltas to quarterly periods. The residual is discounted at `t_{RV} = last_period + 2` (because the override is supplied). ## 8. Testing Strategy `tests/test_financial.py` exercises: - `normalize_cash_flows` with date-based, index-based, and sequential cash-flow inputs. - NPV calculations with and without residual values, including discount-rate sensitivity checks. - IRR convergence success cases, invalid inputs, and non-converging scenarios. - Payback period exact, fractional, and never-payback cases. Developers extending the financial metrics should add regression tests covering new assumptions or solver behaviour. ## 9. Integration Notes - Scenario evaluation services should pass cash flows as `CashFlow` instances to reuse the shared normalisation logic. - UI and reporting layers should display rates as percentages but supply them as decimals to the service layer. - Future Monte Carlo or sensitivity analyses can reuse the same helpers to evaluate each simulated cash-flow path. ## 10. References - Internal implementation: `calminer/services/financial.py` - Tests: `calminer/tests/test_financial.py` - Related specification: `calminer-docs/specifications/price_calculation.md` - Architecture context: `calminer-docs/architecture/08_concepts/02_data_model.md` ```} ```