"""Cash flow analysis and investment evaluation."""
from typing import List, Union
import numpy as np
from scipy.optimize import fsolve
from ..core.validators import validate_positive
[docs]
def net_present_value(
cash_flows: Union[List[float], np.ndarray],
discount_rate: float,
) -> float:
"""
Calculate net present value of cash flows.
Parameters
----------
cash_flows : array-like
Series of cash flows, with initial investment as negative value.
discount_rate : float
Discount rate (as decimal).
Returns
-------
float
Net present value.
Examples
--------
>>> cash_flows = [-100000, 30000, 40000, 50000]
>>> net_present_value(cash_flows, 0.10)
4349.34
"""
# Note: We don't validate positive here as IRR calculation may need negative rates
if not isinstance(discount_rate, (int, float)):
raise ValueError("discount_rate must be a number")
cash_flows = np.array(cash_flows)
periods = np.arange(len(cash_flows))
return float(np.sum(cash_flows / (1 + discount_rate) ** periods))
[docs]
def internal_rate_of_return(
cash_flows: Union[List[float], np.ndarray],
initial_guess: float = 0.1,
) -> float:
"""
Calculate internal rate of return for cash flows.
Parameters
----------
cash_flows : array-like
Series of cash flows, with initial investment as negative value.
initial_guess : float, default 0.1
Initial guess for IRR calculation.
Returns
-------
float
Internal rate of return (as decimal).
Examples
--------
>>> cash_flows = [-100000, 30000, 40000, 50000]
>>> internal_rate_of_return(cash_flows)
0.1627
"""
cash_flows = np.array(cash_flows)
def npv_func(rate: float) -> float:
return net_present_value(cash_flows, rate)
# Try multiple initial guesses if the first one fails
guesses = [initial_guess, 0.05, 0.15, 0.25, -0.5, 0.5, 1.0]
for guess in guesses:
try:
irr = fsolve(npv_func, guess, full_output=True)
if irr[2] == 1: # Successful convergence
result = irr[0][0]
# Verify the result makes sense
if abs(npv_func(result)) < 1e-6:
return float(result)
except (ValueError, RuntimeError, OverflowError):
continue
return float(np.nan)
[docs]
def payback_period(
cash_flows: Union[List[float], np.ndarray],
) -> float:
"""
Calculate payback period for cash flows.
Parameters
----------
cash_flows : array-like
Series of cash flows, with initial investment as negative value.
Returns
-------
float
Payback period in years.
Examples
--------
>>> cash_flows = [-100000, 30000, 40000, 50000]
>>> payback_period(cash_flows)
2.6
"""
cash_flows = np.array(cash_flows)
cumulative = np.cumsum(cash_flows)
# Find where cumulative becomes positive
positive_indices = np.where(cumulative > 0)[0]
if len(positive_indices) == 0:
return float(np.inf)
breakeven_period = positive_indices[0]
if breakeven_period == 0:
return 0.0
# Linear interpolation for more precise payback period
prev_cumulative = cumulative[breakeven_period - 1]
curr_cash_flow = cash_flows[breakeven_period]
fraction = abs(prev_cumulative) / curr_cash_flow
return float(breakeven_period + fraction)
[docs]
def discounted_payback_period(
cash_flows: Union[List[float], np.ndarray],
discount_rate: float,
) -> float:
"""
Calculate discounted payback period for cash flows.
Parameters
----------
cash_flows : array-like
Series of cash flows, with initial investment as negative value.
discount_rate : float
Discount rate (as decimal).
Returns
-------
float
Discounted payback period in years.
Examples
--------
>>> cash_flows = [-100000, 30000, 40000, 50000]
>>> discounted_payback_period(cash_flows, 0.10)
2.8
"""
validate_positive(discount_rate, "discount_rate")
cash_flows = np.array(cash_flows)
periods = np.arange(len(cash_flows))
# Calculate discounted cash flows
discounted_cf = cash_flows / (1 + discount_rate) ** periods
return payback_period(discounted_cf)
[docs]
def profitability_index(
cash_flows: Union[List[float], np.ndarray],
discount_rate: float,
) -> float:
"""
Calculate profitability index for cash flows.
Parameters
----------
cash_flows : array-like
Series of cash flows, with initial investment as negative value.
discount_rate : float
Discount rate (as decimal).
Returns
-------
float
Profitability index.
Examples
--------
>>> cash_flows = [-100000, 30000, 40000, 50000]
>>> profitability_index(cash_flows, 0.10)
1.043
"""
validate_positive(discount_rate, "discount_rate")
cash_flows = np.array(cash_flows)
initial_investment = abs(cash_flows[0]) # Make positive
# Present value of future cash flows
future_cf = cash_flows[1:]
periods = np.arange(1, len(cash_flows))
pv_future_cf = np.sum(future_cf / (1 + discount_rate) ** periods)
return float(pv_future_cf / initial_investment)