Files
calminer-docs/specifications/financial_metrics.md
zwitschi 29f16139a3 feat: documentation update
- Completed export workflow implementation (query builders, CSV/XLSX serializers, streaming API endpoints, UI modals, automated tests).
- Added export modal UI and client script to trigger downloads directly from dashboard.
- Documented import/export field mapping and usage guidelines in FR-008.
- Updated installation guide with export environment variables, dependencies, and CLI/CI usage instructions.
2025-11-11 18:34:02 +01:00

185 lines
7.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
- NewtonRaphson 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`
```}
```