Skip to content

Present Value and IRR Analysis

MoneyWarp provides comprehensive Time Value of Money (TVM) functions powered by scipy for robust numerical calculations.

💡 Design Philosophy: MoneyWarp focuses on Present Value calculations because with our Time Machine, you can simply "voyage dans le temps" to any future date and observe values directly. Future Value functions are unnecessary when you can warp to the future! 🕰️

Present Value Functions

Basic Present Value

Calculate the present value of any cash flow stream:

from money_warp import CashFlow, CashFlowItem, Money, InterestRate, present_value
from datetime import datetime

# Create a cash flow
items = [
    CashFlowItem(Money("-1000"), datetime(2024, 1, 1), "Investment", "investment"),
    CashFlowItem(Money("300"), datetime(2024, 6, 1), "Return 1", "return"),
    CashFlowItem(Money("400"), datetime(2024, 12, 1), "Return 2", "return"),
    CashFlowItem(Money("500"), datetime(2025, 6, 1), "Return 3", "return"),
]
cash_flow = CashFlow(items)

# Calculate present value at 8% annual discount rate
discount_rate = InterestRate("8% annual")
pv = present_value(cash_flow, discount_rate)
print(f"Present Value: {pv}")

Present Value of Annuities

Calculate PV of regular payment streams:

from money_warp import present_value_of_annuity

# PV of $1000 monthly payments for 12 months at 5% annual
monthly_rate = InterestRate("5% annual").to_monthly()
pv_annuity = present_value_of_annuity(
    payment_amount=Money("1000"),
    interest_rate=monthly_rate,
    periods=12
)
print(f"PV of Annuity: {pv_annuity}")

# Annuity due (payments at beginning of period)
pv_due = present_value_of_annuity(
    payment_amount=Money("1000"),
    interest_rate=monthly_rate,
    periods=12,
    payment_timing="begin"
)
print(f"PV of Annuity Due: {pv_due}")

Present Value of Perpetuities

Calculate PV of infinite payment streams:

from money_warp import present_value_of_perpetuity

# PV of $100 annual payments forever at 5%
pv_perpetuity = present_value_of_perpetuity(
    payment_amount=Money("100"),
    interest_rate=InterestRate("5% annual")
)
print(f"PV of Perpetuity: {pv_perpetuity}")  # Should be $2000

Internal Rate of Return (IRR)

Basic IRR Calculation

Find the rate where NPV equals zero:

from money_warp import irr

# Simple investment example
items = [
    CashFlowItem(Money("-1000"), datetime(2024, 1, 1), "Investment", "investment"),
    CashFlowItem(Money("1100"), datetime(2024, 12, 31), "Return", "return"),
]
cash_flow = CashFlow(items)

# Calculate IRR
investment_irr = irr(cash_flow)
print(f"IRR: {investment_irr}")  # Should be ~10%

IRR with Day-Count Convention

By default IRR uses a 365-day (commercial) year. Pass year_size to switch to the 360-day banker's convention:

from money_warp import YearSize

# Commercial year (365 days) — the default
commercial_irr = irr(cash_flow, year_size=YearSize.commercial)
print(f"IRR (365-day year): {commercial_irr}")

# Banker's year (360 days)
banker_irr = irr(cash_flow, year_size=YearSize.banker)
print(f"IRR (360-day year): {banker_irr}")  # Slightly different rate

The returned InterestRate carries the same year_size, so downstream conversions (e.g. to_daily()) stay consistent.

IRR with Custom Initial Guess

Provide a starting point for the numerical solver:

# Use custom initial guess
guess = InterestRate("15% annual")
irr_with_guess = irr(cash_flow, guess=guess)
print(f"IRR with guess: {irr_with_guess}")  # Same result, different path

Complex Cash Flow IRR

IRR works with irregular cash flows:

# Complex investment with multiple cash flows
complex_items = [
    CashFlowItem(Money("-10000"), datetime(2024, 1, 1), "Initial investment", "investment"),
    CashFlowItem(Money("2000"), datetime(2024, 3, 1), "Q1 return", "return"),
    CashFlowItem(Money("-1000"), datetime(2024, 6, 1), "Additional investment", "investment"),
    CashFlowItem(Money("3000"), datetime(2024, 9, 1), "Q3 return", "return"),
    CashFlowItem(Money("8000"), datetime(2024, 12, 31), "Final return", "return"),
]
complex_cf = CashFlow(complex_items)

complex_irr = irr(complex_cf)
print(f"Complex IRR: {complex_irr}")

Modified Internal Rate of Return (MIRR)

MIRR addresses IRR limitations by using different rates for financing and reinvestment:

from money_warp import modified_internal_rate_of_return

# MIRR with different financing and reinvestment rates
finance_rate = InterestRate("10% annual")      # Cost of borrowing
reinvestment_rate = InterestRate("6% annual")  # Reinvestment return

mirr = modified_internal_rate_of_return(
    cash_flow=complex_cf,
    finance_rate=finance_rate,
    reinvestment_rate=reinvestment_rate
)
print(f"MIRR: {mirr}")

# MIRR also accepts year_size for day-count convention
mirr_banker = modified_internal_rate_of_return(
    cash_flow=complex_cf,
    finance_rate=finance_rate,
    reinvestment_rate=reinvestment_rate,
    year_size=YearSize.banker
)
print(f"MIRR (360-day year): {mirr_banker}")

Loan Analysis Sugar Syntax

Loan Present Value

Calculate loan PV from borrower's perspective:

from datetime import date, datetime

from money_warp import InterestRate, Loan, Money

# Create a loan
loan = Loan(
    principal=Money("10000"),
    interest_rate=InterestRate("5% annual"),
    due_dates=[date(2024, 6, 1), date(2024, 12, 1)],
    disbursement_date=datetime(2024, 1, 1)
)

# Present value using loan's own rate (should be close to zero)
pv_own_rate = loan.present_value()
print(f"PV at loan's rate: {pv_own_rate}")

# Present value using different discount rate
pv_market_rate = loan.present_value(InterestRate("8% annual"))
print(f"PV at 8%: {pv_market_rate}")  # Negative from borrower's perspective

Loan IRR

Calculate loan's effective rate. The loan automatically passes its own year_size to the IRR calculation, so loans created with YearSize.banker compute IRR using a 360-day year:

from datetime import date, datetime

from money_warp import InterestRate, Loan, Money, YearSize

# Loan IRR (should equal the loan's interest rate); assumes `loan` from previous example
loan_irr = loan.irr()
print(f"Loan IRR: {loan_irr}")  # Should be ~5%

# IRR with custom guess
loan_irr_guess = loan.irr(guess=InterestRate("3% annual"))
print(f"Loan IRR with guess: {loan_irr_guess}")  # Same result

# Loan with banker's year — IRR automatically uses 360-day convention
banker_loan = Loan(
    principal=Money("10000"),
    interest_rate=InterestRate("5% annual", year_size=YearSize.banker),
    due_dates=[date(2024, 6, 1), date(2024, 12, 1)],
    disbursement_date=datetime(2024, 1, 1)
)
banker_loan_irr = banker_loan.irr()
print(f"Banker Loan IRR: {banker_loan_irr}")  # Uses 360-day year

Time Machine Integration

IRR from Specific Dates

Use the Time Machine to calculate IRR from any point in time:

from money_warp import Warp

# Calculate IRR from different time perspectives
current_irr = loan.irr()

# IRR as of a specific past date
with Warp(loan, datetime(2024, 3, 1)) as past_loan:
    past_irr = past_loan.irr()

print(f"Current IRR: {current_irr}")
print(f"IRR as of March 1: {past_irr}")

Present Value with Time Machine

# Present value from different time perspectives
current_pv = loan.present_value(InterestRate("8% annual"))

with Warp(loan, datetime(2024, 2, 1)) as past_loan:
    past_pv = past_loan.present_value(InterestRate("8% annual"))

print(f"Current PV: {current_pv}")
print(f"PV as of Feb 1: {past_pv}")

Key Features

Robust Numerics

  • Scipy-powered: Uses scipy.optimize.brentq for reliable root finding
  • Automatic bracketing: Finds sign changes in NPV function automatically
  • Fallback methods: Uses fsolve if bracketing fails
  • High precision: Maintains decimal precision throughout
  • Day-count conventions: YearSize.commercial (365) or YearSize.banker (360) for IRR and MIRR

Error Handling

  • Clear messages: Descriptive error messages for common issues
  • Convergence checking: Validates solutions within reasonable tolerance
  • Edge case handling: Handles empty cash flows, single-sign flows, etc.

Integration

  • Time Machine: All functions work seamlessly with Warp
  • Sugar syntax: Convenient methods on Loan objects
  • Type safety: Full type annotations and mypy compatibility
  • Consistent API: Similar patterns across all TVM functions

Common Patterns

Investment Analysis

# Compare investment alternatives
investments = [
    ("Project A", project_a_cashflow),
    ("Project B", project_b_cashflow),
    ("Project C", project_c_cashflow),
]

hurdle_rate = InterestRate("12% annual")

for name, cf in investments:
    pv = present_value(cf, hurdle_rate)
    irr_rate = irr(cf)
    print(f"{name}: PV={pv}, IRR={irr_rate}")

Loan Comparison

# Compare loan offers
loans = [loan_a, loan_b, loan_c]
market_rate = InterestRate("7% annual")

for i, loan in enumerate(loans, 1):
    pv = loan.present_value(market_rate)
    effective_rate = loan.irr()
    print(f"Loan {i}: PV={pv}, Effective Rate={effective_rate}")

Sensitivity Analysis

# Test different discount rates
rates = ["5% annual", "8% annual", "10% annual", "12% annual"]

for rate_str in rates:
    rate = InterestRate(rate_str)
    pv = present_value(cash_flow, rate)
    print(f"PV at {rate}: {pv}")

MoneyWarp's TVM functions provide the foundation for sophisticated financial analysis while maintaining simplicity and reliability through scipy-powered numerics.