Skip to content

Commit 8a17601

Browse files
committed
added annualized_return
1 parent 626a904 commit 8a17601

File tree

5 files changed

+58
-3
lines changed

5 files changed

+58
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- `mean_excess` and `std_excess` helpers for allocation‑free mean and standard deviation of differences.
88
- `cagr` function for calculating the compound annual growth rate of a series of returns, with support for simple and log returns.
9+
- `annualized_return` function for calculating the annualized return of a series of returns, with support for simple and log returns.
910

1011
### Changed
1112

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ total_return(returns::AbstractVector; method=:log)
2222

2323
cagr(returns::AbstractVector, periods_per_year::Real; method=:simple)
2424

25+
annualized_return(returns::AbstractVector, periods_per_year::Real)
26+
2527
volatility(returns::AbstractVector; multiplier=1.0)
2628

2729
information_ratio(asset_returns::AbstractVector, benchmark_returns::Real; multiplier=1.0)

src/RiskPerf.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export adjusted_sharpe_ratio,
1919
simple_returns,
2020
total_return,
2121
cagr,
22+
annualized_return,
2223
mean_excess,
2324
std_excess,
2425
lower_partial_moment,

src/returns.jl

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -498,9 +498,7 @@ function cagr(returns::AbstractVector, periods_per_year::Real; method::Symbol=:s
498498
n == 0 && return 0.0
499499
ppy = float(periods_per_year)
500500
(!isfinite(ppy) || ppy <= 0) && throw(
501-
ArgumentError(
502-
"periods_per_year must be positive and finite (got $(periods_per_year)).",
503-
),
501+
ArgumentError("periods_per_year must be positive and finite (got $(periods_per_year)).")
504502
)
505503
years = n / ppy
506504
T = promote_type(eltype(returns), Float64)
@@ -522,3 +520,40 @@ function cagr(returns::AbstractVector, periods_per_year::Real; method::Symbol=:s
522520

523521
throw(ArgumentError("Invalid method $(method). Use :simple or :log."))
524522
end
523+
524+
"""
525+
annualized_return(returns::AbstractVector, periods_per_year::Real)
526+
527+
Compute the arithmetic annualized return by scaling the average periodic return.
528+
This matches the common "expected annual return" metric on fact sheets where the
529+
per-period arithmetic mean is multiplied by the observation frequency.
530+
531+
# Arguments
532+
- `returns`: Vector of periodic simple (arithmetic) returns.
533+
- `periods_per_year`: Number of periods per calendar year (e.g. 252, 12, 52).
534+
535+
# Formula
536+
537+
Let `μ = mean(returns)` and `k = periods_per_year`. Then
538+
539+
``AnnualizedReturn = μ × k``
540+
541+
# Edge Cases
542+
- Returns `0.0` if `returns` is empty.
543+
- Throws `ArgumentError` when `periods_per_year` is non-positive or non-finite.
544+
"""
545+
function annualized_return(returns::AbstractVector, periods_per_year::Real)
546+
n = length(returns)
547+
n == 0 && return 0.0
548+
ppy = float(periods_per_year)
549+
(!isfinite(ppy) || ppy <= 0) && throw(
550+
ArgumentError("periods_per_year must be positive and finite (got $(periods_per_year)).")
551+
)
552+
T = promote_type(eltype(returns), Float64)
553+
s = zero(T)
554+
@inbounds for r in returns
555+
s += T(r)
556+
end
557+
μ = s / T(n)
558+
μ * T(ppy)
559+
end

test/returns.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,19 @@ end
116116
@test_throws ArgumentError cagr(monthly_r, -12)
117117
@test_throws ArgumentError cagr(monthly_r, Inf)
118118
end
119+
120+
@testitem "annualized_return" begin
121+
using Test
122+
using RiskPerf
123+
124+
monthly = fill(0.01, 12)
125+
@test annualized_return(monthly, 12) 0.12 atol = 1e-12
126+
127+
mixed = [0.01, -0.02, 0.03]
128+
expected = sum(mixed) / length(mixed) * 12
129+
@test annualized_return(mixed, 12) expected atol = 1e-12
130+
131+
@test annualized_return(Float64[], 12) == 0.0
132+
@test_throws ArgumentError annualized_return(mixed, 0)
133+
@test_throws ArgumentError annualized_return(mixed, Inf)
134+
end

0 commit comments

Comments
 (0)