- 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.
7.2 KiB
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.pyand the related pytest coverage intests/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:
period_index— explicit integers/floats overriding all other timing.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 bycompounds_per_year.- 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_factorcomputes $(1 + r/m)^{-t}; NPV iterates over the normalised flows and sumsamount * factor`.- Residual values default to one period after the final cash flow when
residual_periodsis 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)
- Lower bound:
- Raises
ConvergenceErrorwhen no sign change is found or the bisection fails within the iteration budget (max_iterationsdefault 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
ValueErroris 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 * mimply 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_fractionalisFalse, the function returns the first period with non-negative cumulative total without interpolation. PaybackNotReachedErroris raised if the cumulative total never becomes non-negative.
7. Examples
Example 1: Baseline Project
- Initial investment:
-1,000,000at 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,759internal_rate_of_return≈ 0.158payback_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_flowswith 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
CashFlowinstances 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