From 724a13a0123c407450ae0336bd716375b4406573 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 12 Mar 2026 09:58:00 -0700 Subject: [PATCH] centralize annualized return calc --- TODO.md | 2 -- src/analytics/performance.zig | 23 +++++++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/TODO.md b/TODO.md index 3576efa..5085e0a 100644 --- a/TODO.md +++ b/TODO.md @@ -11,10 +11,8 @@ Example: 500 shares of AMZN at $225, with 3 sold calls at $220 strike. ## Human review of analytics modules AI review complete; human review still needed for: -- `src/analytics/performance.zig` — Morningstar-style trailing returns - `src/analytics/risk.zig` — Sharpe, volatility, max drawdown, portfolio summary - `src/analytics/indicators.zig` — SMA, Bollinger Bands, RSI -- `src/models/classification.zig` — Sector/geo/asset-class metadata parsing Known issues from AI review: - `risk.zig` uses population variance (divides by n) instead of sample diff --git a/src/analytics/performance.zig b/src/analytics/performance.zig index 2763e1a..f893e59 100644 --- a/src/analytics/performance.zig +++ b/src/analytics/performance.zig @@ -3,6 +3,19 @@ const Date = @import("../models/date.zig").Date; const Candle = @import("../models/candle.zig").Candle; const Dividend = @import("../models/dividend.zig").Dividend; +/// Minimum holding period (in years) before annualizing returns. +/// Set below 1.0 to handle trading-day snap (e.g. a "1-year" lookback +/// that lands on 362 days due to weekends). +const min_annualize_years = 0.95; + +/// Compute CAGR from a total return over a given number of years. +/// Returns null for periods shorter than `min_annualize_years` where +/// extrapolating to a full year would be misleading. +inline fn annualizedReturn(total: f64, years: f64) ?f64 { + if (years < min_annualize_years) return null; + return std.math.pow(f64, 1.0 + total, 1.0 / years) - 1.0; +} + /// Performance calculation results, Morningstar-style. pub const PerformanceResult = struct { /// Total return over the period (e.g., 0.25 = 25%) @@ -40,10 +53,7 @@ fn totalReturnFromAdjCloseSnap(candles: []const Candle, from: Date, to: Date, st return .{ .total_return = total, - .annualized_return = if (years >= 0.95) - std.math.pow(f64, 1.0 + total, 1.0 / years) - 1.0 - else - null, + .annualized_return = annualizedReturn(total, years), .from = start.date, .to = end.date, }; @@ -104,10 +114,7 @@ fn totalReturnWithDividendsSnap( return .{ .total_return = total, - .annualized_return = if (years >= 0.95) - std.math.pow(f64, 1.0 + total, 1.0 / years) - 1.0 - else - null, + .annualized_return = annualizedReturn(total, years), .from = start.date, .to = end.date, };