
Return Calculations
- 400 installs
- 161 repo stars
- Updated July 18, 2026
- joellewis/finance_skills
return-calculations is a finance computation skill that defines return metrics for developers who need to implement CAGR, IRR, and period gain calculations in reporting systems.
About
return-calculations is a finance computation skill that focuses on implementing common investment return metrics such as CAGR, IRR, and period gains. return-calculations helps developers choose the correct metric for a reporting requirement, define inputs and edge cases, and structure calculations for portfolio models and dashboards. return-calculations is most relevant when building analytics features where stakeholders need consistent return numbers across time ranges, cash flows, and portfolio compositions. Developers reach for return-calculations when they are wiring backend calculations into an API or ETL pipeline and want a clear mapping from financial definitions to implementable logic and outputs.
- CAGR and IRR computation steps
- Cash-flow aware return logic
- Period and annualized comparisons
- Portfolio aggregation patterns
- Reporting-ready numeric outputs
Return Calculations by the numbers
- 400 all-time installs (skills.sh)
- +15 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #253 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joellewis/finance_skills --skill return-calculationsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 400 |
|---|---|
| repo stars | ★ 161 |
| Last updated | July 18, 2026 |
| Repository | joellewis/finance_skills ↗ |
How do you calculate IRR for cash flows?
Compute investment return metrics such as CAGR, IRR, and period gains for portfolios, models, and reporting dashboards.
Who is it for?
Developers implementing finance return metrics in reporting systems or portfolio dashboards.
Skip if: Developers looking for UI components or unrelated infrastructure tooling.
When should I use this skill?
Invoke when a developer asks to compute CAGR, IRR, period gains, or portfolio return metrics for reporting.
What you get
Metric definitions, calculation approach for CAGR/IRR/period returns, and implementation-ready input/output specifications for reporting.
- metric spec
- calculation plan
- reporting outputs
Files
Return Calculations
Core Concepts
Simple (Holding Period) Return
$$R = \frac{V_{end} - V_{begin} + D}{V_{begin}}$$
where D = distributions (dividends, interest) received during the period. If V_end already reflects reinvested distributions, do not add D again.
Mean and Log Return Conventions
- Arithmetic mean
R_a = (1/n) * sum(R_i)— unbiased estimate of the expected single-period return (use for forward-looking inputs, e.g., mean-variance optimization). Always >= geometric mean; overstates realized compound growth. - Geometric mean
R_g = [prod(1 + R_i)]^(1/n) - 1— the correct measure of realized multi-period compound growth. The gap below the arithmetic mean approximatessigma^2 / 2(volatility drag). - Log return
r = ln(V_end / V_begin)— time-additive (r_total = r_1 + ... + r_n), so preferred for statistical modeling and multi-period aggregation. Convert withR_simple = e^r - 1andr = ln(1 + R_simple). Log returns are additive across time but NOT across assets.
CAGR (Compound Annual Growth Rate)
$$CAGR = \left(\frac{V_{end}}{V_{begin}}\right)^{1/n} - 1$$
where n is measured in years. The annualized geometric growth rate between two valuations with no intermediate cash flows.
Time-Weighted Return (TWR)
Chain-links sub-period returns calculated between each external cash flow, removing the effect of cash flow timing. TWR measures the manager's investment skill independent of investor deposit/withdrawal decisions, and is the GIPS standard for manager performance.
$$1 + R_{TWR} = \prod_{i=1}^{n}(1 + R_i), \qquad R_i = \frac{V_{end,i}}{V_{begin,i} + CF_i} - 1$$
Exact TWR requires a portfolio valuation on every cash flow date.
Modified Dietz Return
When valuations on each cash flow date are unavailable, Modified Dietz approximates the period return by day-weighting each external cash flow within the period:
$$R_{MD} = \frac{V_{end} - V_{begin} - CF_{net}}{V_{begin} + \sum_i CF_i \times w_i}, \qquad w_i = \frac{CD - D_i}{CD}$$
where CF_net = sum of external cash flows, CD = calendar days in the period, and D_i = day of flow i (so w_i is the fraction of the period the flow was invested). It is a money-weighted approximation; chain-linking Modified Dietz sub-period returns approximates TWR. Accuracy degrades when flows are large relative to portfolio value or markets are volatile within the period — revalue on large-flow dates instead.
Money-Weighted Return (MWR / IRR)
The internal rate of return that sets the NPV of all investor cash flows (contributions, withdrawals, and terminal value) to zero:
$$0 = \sum_{t=0}^{T} \frac{CF_t}{(1 + r)^t}$$
MWR reflects the actual investor experience because it is sensitive to the timing and magnitude of cash flows. Solved numerically (Newton-Raphson or bisection).
Annualization
$$R_{annual} = (1 + R_{period})^{periods\_per\_year} - 1$$
For example, a 2% quarterly return annualizes to (1.02)^4 - 1 = 8.24%.
Sub-Period Linking
$$(1 + R_{total}) = \prod_{i=1}^{n}(1 + R_i)$$
The foundational identity behind TWR and CAGR.
Worked Examples
Example 1: Computing CAGR from a 5-Year Investment
Given: An investment of $10,000 grows to $16,105.10 over exactly 5 years with no intermediate cash flows.
Calculate: The compound annual growth rate (CAGR).
Solution:
CAGR = (V_end / V_begin)^(1/n) - 1
CAGR = (16,105.10 / 10,000)^(1/5) - 1
CAGR = (1.610510)^(0.2) - 1
CAGR = 1.10 - 1
CAGR = 0.10 = 10%The investment grew at a compound annual rate of 10% per year.
Verification: $10,000 * (1.10)^5 = $10,000 * 1.61051 = $16,105.10
Example 2: TWR vs MWR Divergence with Poorly Timed Cash Flow
Given: A fund has the following history:
- Start of Year 1: Portfolio value = $100,000
- End of Year 1: Portfolio value = $120,000 (return = +20%)
- Start of Year 2: Investor deposits $100,000, bringing portfolio to $220,000
- End of Year 2: Portfolio value = $198,000 (return = -10%)
Calculate: Both TWR and MWR, and explain the divergence.
Solution:
Time-Weighted Return (TWR):
Sub-period 1 return: R_1 = (120,000 - 100,000) / 100,000 = +20%
Sub-period 2 return: R_2 = (198,000 - 220,000) / 220,000 = -10%
TWR (cumulative) = (1 + 0.20) * (1 + (-0.10)) - 1
= 1.20 * 0.90 - 1
= 1.08 - 1
= +8.0%
TWR (annualized) = (1.08)^(1/2) - 1 = 3.92%Money-Weighted Return (MWR / IRR): Cash flows from the investor's perspective:
- t=0: -$100,000 (initial investment)
- t=1: -$100,000 (additional deposit)
- t=2: +$198,000 (terminal value)
Solve: -100,000 + (-100,000)/(1+r) + 198,000/(1+r)^2 = 0
This is quadratic in x = 1/(1+r); the positive root gives r = -0.66815% (verifiable with the bundled script or any IRR solver).
NPV check at r = -0.0066815:
-100,000 + (-100,000)/0.9933185 + 198,000/0.9933185^2
= -100,000 - 100,672.65 + 200,672.65
= 0.00 (exact)The MWR is approximately -0.67% annualized.
Interpretation: The TWR of +3.92% annualized reflects the manager's skill: the fund gained 20% then lost 10%, netting +8% over two years. The MWR of approximately -0.67% reflects the investor's experience: more money was at risk during the losing year (Year 2) because of the large deposit, so the investor's dollar-weighted outcome was slightly negative. This divergence highlights why TWR is preferred for evaluating manager performance, while MWR better describes the specific investor's realized result.
Common Pitfalls
- Confusing arithmetic and geometric means: the arithmetic mean is always greater than or equal to the geometric mean (AM-GM inequality). Using arithmetic mean to project compounded growth overstates terminal wealth.
- Using arithmetic mean for multi-period compounding: always use geometric mean or CAGR when describing compound growth over multiple periods.
- Annualizing returns from very short periods: annualizing a 2% weekly return yields
(1.02)^52 - 1 = 180%, which amplifies noise and is misleading. Annualization is most meaningful for periods of at least one year. - Ignoring cash flow timing when TWR is appropriate: MWR conflates manager skill with investor timing decisions. Use TWR for manager evaluation.
- Double-counting dividends: if the ending value
V_endalready includes reinvested dividends, do not addDseparately in the holding period return formula. - Trusting Modified Dietz with large intra-period flows: when a single flow exceeds roughly 10% of portfolio value, revalue the portfolio on the flow date rather than day-weighting.
Running the Script
scripts/return_calculations.py provides a Returns class with static methods for every formula above (holding period return, TWR, MWR/IRR via Newton's method, Modified Dietz is straightforward to compose from these, CAGR, annualization, linking, arithmetic/geometric means, log-return conversions).
- Run:
uv run scripts/return_calculations.py(PEP 723 inline metadata resolves numpy automatically), orpython3 scripts/return_calculations.pywith numpy installed. - Bare invocation (or
--verify) prints a demo of all functions and asserts the worked-example values above (Example 1 CAGR = 10%, Example 2 TWR = +8.0% cumulative / 3.92% annualized, MWR = -0.6682%), exiting nonzero on any mismatch. --helplists the available functions and import usage.- For programmatic use, import rather than run:
from return_calculations import Returns.
Cross-References
- time-value-of-money (core plugin, Layer 0): NPV, IRR, and discounting concepts overlap with MWR calculations; owns project/loan IRR
- statistics-fundamentals (core plugin, Layer 0): Arithmetic and geometric means, return distribution analysis
# /// script
# dependencies = ["numpy"]
# requires-python = ">=3.11"
# ///
"""
Return Calculations - Layer 0 (Mathematical Foundations)
A comprehensive reference implementation for computing investment return metrics
including time-weighted returns, money-weighted returns, CAGR, annualization,
sub-period linking, and arithmetic/geometric/log return conversions.
Usage:
uv run return_calculations.py # demo + verification (default)
python return_calculations.py --verify # same as bare invocation
python return_calculations.py --help # list available functions
Dependencies:
numpy
"""
import argparse
import math
import sys
import numpy as np
class Returns:
"""Compute and convert investment return metrics.
This class provides static methods for all standard return calculations
used in investment performance measurement. All methods are stateless
and operate on the inputs provided.
"""
@staticmethod
def holding_period_return(
begin_value: float,
end_value: float,
distributions: float = 0.0,
) -> float:
"""Compute the simple holding period return.
R = (V1 - V0 + D) / V0
Args:
begin_value: Portfolio value at the start of the period (V0).
end_value: Portfolio value at the end of the period (V1).
distributions: Income received during the period (dividends,
interest, etc.). Defaults to 0.
Returns:
The holding period return as a decimal (e.g., 0.05 for 5%).
Raises:
ValueError: If begin_value is zero.
"""
if begin_value == 0:
raise ValueError("begin_value must be non-zero.")
return (end_value - begin_value + distributions) / begin_value
@staticmethod
def time_weighted_return(sub_period_returns: list[float]) -> float:
"""Compute the time-weighted return by geometrically linking sub-period returns.
TWR = prod(1 + r_i) - 1
Eliminates the impact of external cash flows, making it the standard
for evaluating investment manager performance (GIPS-compliant).
Args:
sub_period_returns: A list of sub-period returns as decimals.
Returns:
The cumulative time-weighted return as a decimal.
"""
product = np.prod([1.0 + r for r in sub_period_returns])
return float(product - 1.0)
@staticmethod
def money_weighted_return(
cash_flows: list[tuple[int, float]],
guess: float = 0.1,
) -> float:
"""Compute the money-weighted return (IRR) using Newton's method.
Finds the rate r such that:
sum( CF_t / (1 + r)^t ) = 0
Cash flows should be signed: negative for contributions (money in),
positive for withdrawals or terminal value (money out).
Args:
cash_flows: List of (time_period, cash_flow_amount) tuples.
Time periods are integers (e.g., day counts or period indices).
The first cash flow is typically the initial investment (negative),
and the last includes the terminal value (positive).
guess: Initial guess for the rate. Defaults to 0.1 (10%).
Returns:
The money-weighted return (IRR) as a decimal.
Raises:
RuntimeError: If Newton's method fails to converge.
"""
rate = guess
max_iterations = 1000
tolerance = 1e-10
for _ in range(max_iterations):
npv = 0.0
npv_deriv = 0.0
for t, cf in cash_flows:
discount = (1.0 + rate) ** t
npv += cf / discount
if t != 0:
npv_deriv -= t * cf / ((1.0 + rate) ** (t + 1))
if abs(npv) < tolerance:
return rate
if abs(npv_deriv) < 1e-15:
raise RuntimeError(
"Newton's method derivative near zero; try a different guess."
)
rate = rate - npv / npv_deriv
raise RuntimeError(
f"Newton's method did not converge after {max_iterations} iterations."
)
@staticmethod
def cagr(begin_value: float, end_value: float, years: float) -> float:
"""Compute the Compound Annual Growth Rate.
CAGR = (V_end / V_start)^(1/n) - 1
Args:
begin_value: Starting value of the investment.
end_value: Ending value of the investment.
years: Number of years in the holding period.
Returns:
The CAGR as a decimal.
Raises:
ValueError: If begin_value is zero or years is zero.
"""
if begin_value == 0:
raise ValueError("begin_value must be non-zero.")
if years == 0:
raise ValueError("years must be non-zero.")
return (end_value / begin_value) ** (1.0 / years) - 1.0
@staticmethod
def annualize(
total_return: float,
periods: float,
periods_per_year: float,
) -> float:
"""Annualize a return observed over a given number of periods.
R_annual = (1 + R_total)^(periods_per_year / periods) - 1
Args:
total_return: The cumulative return over the observation window
as a decimal.
periods: The number of periods in the observation window.
periods_per_year: The number of such periods in one year
(e.g., 252 for trading days, 12 for months, 4 for quarters).
Returns:
The annualized return as a decimal.
"""
return (1.0 + total_return) ** (periods_per_year / periods) - 1.0
@staticmethod
def link_returns(returns: list[float]) -> float:
"""Link (chain) a sequence of periodic returns into a cumulative return.
R_cumulative = (1 + r_1)(1 + r_2)...(1 + r_n) - 1
Args:
returns: A list of periodic returns as decimals.
Returns:
The cumulative linked return as a decimal.
"""
product = np.prod([1.0 + r for r in returns])
return float(product - 1.0)
@staticmethod
def arithmetic_mean(returns: list[float]) -> float:
"""Compute the arithmetic mean of a series of returns.
R_arith = sum(r_i) / n
The arithmetic mean is the best unbiased estimate of the expected
single-period return and is used as an input for mean-variance
optimization. It overstates the realized compound growth rate.
Args:
returns: A list of periodic returns as decimals.
Returns:
The arithmetic mean return as a decimal.
"""
return float(np.mean(returns))
@staticmethod
def geometric_mean(returns: list[float]) -> float:
"""Compute the geometric mean of a series of returns.
R_geom = [prod(1 + r_i)]^(1/n) - 1
The geometric mean represents the actual per-period compound growth
rate. It is always less than or equal to the arithmetic mean;
the gap approximates sigma^2 / 2.
Args:
returns: A list of periodic returns as decimals.
Returns:
The geometric mean return as a decimal.
"""
n = len(returns)
product = np.prod([1.0 + r for r in returns])
return float(product ** (1.0 / n) - 1.0)
@staticmethod
def log_return(begin_value: float, end_value: float) -> float:
"""Compute the continuously compounded (log) return.
r_log = ln(V1 / V0)
Log returns are additive across time but NOT across assets.
Args:
begin_value: Value at the start of the period.
end_value: Value at the end of the period.
Returns:
The log return as a decimal.
Raises:
ValueError: If begin_value or end_value is non-positive.
"""
if begin_value <= 0 or end_value <= 0:
raise ValueError("Both begin_value and end_value must be positive.")
return math.log(end_value / begin_value)
@staticmethod
def log_to_simple(log_ret: float) -> float:
"""Convert a log return to a simple return.
r_simple = e^(r_log) - 1
Args:
log_ret: The log (continuously compounded) return.
Returns:
The equivalent simple return as a decimal.
"""
return math.exp(log_ret) - 1.0
@staticmethod
def simple_to_log(simple_ret: float) -> float:
"""Convert a simple return to a log return.
r_log = ln(1 + r_simple)
Args:
simple_ret: The simple return as a decimal.
Returns:
The equivalent log return.
Raises:
ValueError: If simple_ret <= -1 (total loss or worse).
"""
if simple_ret <= -1.0:
raise ValueError("simple_ret must be greater than -1.")
return math.log(1.0 + simple_ret)
# ---------------------------------------------------------------------------
# Demonstration and verification
# ---------------------------------------------------------------------------
_FUNCTIONS_HELP = """\
Available functions (all static methods on the Returns class):
holding_period_return(begin_value, end_value, distributions=0.0)
time_weighted_return(sub_period_returns)
money_weighted_return(cash_flows, guess=0.1) # IRR via Newton's method
cagr(begin_value, end_value, years)
annualize(total_return, periods, periods_per_year)
link_returns(returns)
arithmetic_mean(returns)
geometric_mean(returns)
log_return(begin_value, end_value)
log_to_simple(log_ret) / simple_to_log(simple_ret)
Import usage (preferred for programmatic work):
from return_calculations import Returns
Returns.cagr(10_000, 16_105.10, 5) # -> 0.10
Running bare (or with --verify) prints a demo of every function and
asserts the worked-example values from SKILL.md, exiting nonzero on
any mismatch.
"""
def _verify() -> None:
"""Assert that key outputs match the SKILL.md worked examples."""
calc = Returns()
# SKILL.md Example 1: CAGR of $10,000 -> $16,105.10 over 5 years = 10%
cagr_val = calc.cagr(begin_value=10_000, end_value=16_105.10, years=5)
assert abs(cagr_val - 0.10) < 1e-6, f"Example 1 CAGR mismatch: {cagr_val}"
# SKILL.md Example 2: TWR of +20% then -10% = +8.0% cumulative
twr = calc.time_weighted_return([0.20, -0.10])
assert abs(twr - 0.08) < 1e-12, f"Example 2 TWR mismatch: {twr}"
twr_ann = (1.0 + twr) ** 0.5 - 1.0
assert abs(twr_ann - 0.0392) < 5e-5, f"Example 2 annualized TWR mismatch: {twr_ann}"
# SKILL.md Example 2: MWR for (-100k, -100k, +198k) = -0.6682% (-0.66815%)
mwr = calc.money_weighted_return([(0, -100_000), (1, -100_000), (2, 198_000)])
assert abs(mwr - (-0.0066815)) < 1e-6, f"Example 2 MWR mismatch: {mwr}"
print("\nVerification PASSED: outputs match SKILL.md worked examples")
print(" Example 1 CAGR: 10.0000%")
print(f" Example 2 TWR cumulative: {twr:.4%} (annualized {twr_ann:.4%})")
print(f" Example 2 MWR (IRR): {mwr:.4%}")
def _demo() -> None:
calc = Returns()
print("=" * 60)
print("Return Calculations - Reference Implementation Demo")
print("=" * 60)
# 1. Holding Period Return
hpr = calc.holding_period_return(begin_value=100_000, end_value=108_000, distributions=2_000)
print(f"\n1. Holding Period Return: {hpr:.4%}")
print(f" (Invested $100k, ended at $108k, received $2k dividends)")
# 2. Time-Weighted Return
sub_returns = [0.05, -0.02, 0.03, 0.04]
twr = calc.time_weighted_return(sub_returns)
print(f"\n2. Time-Weighted Return: {twr:.4%}")
print(f" Sub-period returns: {sub_returns}")
# 3. Money-Weighted Return (IRR)
# Invest $100k at t=0, add $50k at t=1, end value $165k at t=2
cash_flows = [(0, -100_000), (1, -50_000), (2, 165_000)]
mwr = calc.money_weighted_return(cash_flows)
print(f"\n3. Money-Weighted Return (IRR): {mwr:.4%}")
print(f" Cash flows: {cash_flows}")
# 4. CAGR
cagr_val = calc.cagr(begin_value=100_000, end_value=160_000, years=5)
print(f"\n4. CAGR (5 years, $100k -> $160k): {cagr_val:.4%}")
# 5. Annualization
monthly_return = 0.08 # 8% over 6 months
ann = calc.annualize(total_return=monthly_return, periods=6, periods_per_year=12)
print(f"\n5. Annualized Return (8% in 6 months): {ann:.4%}")
# 6. Linking Returns
monthly_rets = [0.01, 0.02, -0.005, 0.015, 0.008, -0.01]
linked = calc.link_returns(monthly_rets)
print(f"\n6. Linked Return (6 months): {linked:.4%}")
print(f" Monthly returns: {monthly_rets}")
# 7. Arithmetic vs Geometric Mean
returns = [0.10, -0.05, 0.08, -0.03, 0.12]
arith = calc.arithmetic_mean(returns)
geom = calc.geometric_mean(returns)
std = float(np.std(returns, ddof=0))
print(f"\n7. Arithmetic Mean: {arith:.4%}")
print(f" Geometric Mean: {geom:.4%}")
print(f" Difference: {arith - geom:.4%}")
print(f" sigma^2 / 2: {std**2 / 2:.4%}")
# 8. Log Returns
log_r = calc.log_return(begin_value=100, end_value=110)
simple_r = calc.log_to_simple(log_r)
back_to_log = calc.simple_to_log(simple_r)
print(f"\n8. Log Return ($100 -> $110): {log_r:.6f}")
print(f" Converted to simple: {simple_r:.6f}")
print(f" Converted back to log: {back_to_log:.6f}")
print("\n" + "=" * 60)
print("All calculations completed successfully.")
print("=" * 60)
def main() -> int:
parser = argparse.ArgumentParser(
description="Investment return calculations reference implementation.",
epilog=_FUNCTIONS_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--verify",
action="store_true",
help="run the demo and assert outputs match the SKILL.md worked "
"examples (this is also the default when run with no arguments)",
)
parser.parse_args()
# Bare invocation and --verify behave identically: demo + verification.
_demo()
try:
_verify()
except AssertionError as exc:
print(f"\nVerification FAILED: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
Related skills
How it compares
Pick this when you need correct metric definitions and implementation guidance for return calculations, not charting or UI formatting.
FAQ
What metrics does return-calculations focus on?
return-calculations focuses on investment return metrics used in reporting. return-calculations covers CAGR, IRR, and period gains so developers can align backend calculation logic with the definitions expected in portfolio models and dashboards.
When should I use IRR instead of simple period return?
return-calculations uses IRR when cash flows occur over time and a single rate is needed to summarize performance. return-calculations treats period returns as appropriate for price-based intervals, while IRR is used for sequences of contributions and withdrawals.