clean up performance.zig

This commit is contained in:
Emil Lerch 2026-04-21 12:34:10 -07:00
parent 0cb3b2948b
commit fbe8320399
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 30 additions and 9 deletions

View file

@ -2,6 +2,7 @@ const std = @import("std");
const Date = @import("../models/date.zig").Date;
const Candle = @import("../models/candle.zig").Candle;
const Dividend = @import("../models/dividend.zig").Dividend;
const portfolio = @import("../models/portfolio.zig");
/// Minimum holding period (in years) before annualizing returns.
/// Set below 1.0 to handle trading-day snap (e.g. a "1-year" lookback
@ -89,11 +90,14 @@ fn totalReturnWithDividendsSnap(
to: Date,
start_dir: SearchDirection,
) ?PerformanceResult {
// When `from` predates all cached candles, the only safe shortcut is
// for stable-NAV funds: if the earliest candle we have is at $1, we
// can extrapolate backward and synthesize a $1 candle at `from`.
// Reuse `stableNavCandle` so the synthesized candle has the same
// shape as the one used elsewhere.
const start = findNearestCandle(candles, from, start_dir) orelse
// Stable-NAV fund (e.g. money market): synthesize start candle at $1
// when candle history doesn't reach back far enough.
if (candles.len > 0 and from.lessThan(candles[0].date) and candles[0].close == 1.0)
stableNavCandle(from)
portfolio.stableNavCandle(from)
else
return null;
const end = findNearestCandle(candles, to, .backward) orelse return null;
@ -111,7 +115,7 @@ fn totalReturnWithDividendsSnap(
// For stable-NAV funds, dividends before candle history use $1.
const price_candle = findNearestCandle(candles, div.ex_date, .backward) orelse
if (start.close == 1.0)
stableNavCandle(div.ex_date)
portfolio.stableNavCandle(div.ex_date)
else
continue;
if (price_candle.close > 0) {
@ -131,11 +135,9 @@ fn totalReturnWithDividendsSnap(
};
}
/// Synthesize a candle with stable $1 NAV for a given date.
/// Used for money market funds whose NAV is always $1.
fn stableNavCandle(date: Date) Candle {
return .{ .date = date, .open = 1, .high = 1, .low = 1, .close = 1, .adj_close = 1, .volume = 0 };
}
// Stable-NAV candle synthesis lives in src/models/portfolio.zig
// (`portfolio.stableNavCandle`) so every caller agrees on the shape
// of a synthesized $1 candle. Performance-only heuristics stay here.
/// Convenience: compute 1yr, 3yr, 5yr, 10yr trailing returns from adjusted close.
/// Uses the last available date as the endpoint.

View file

@ -1,5 +1,14 @@
const std = @import("std");
const Date = @import("date.zig").Date;
const Candle = @import("candle.zig").Candle;
/// Synthesize a stable-NAV (= $1) candle for a given date. Used when
/// historical price data for a money-market fund doesn't reach back as
/// far as the period under analysis the close is known to be $1 by
/// construction, so we can extrapolate backward without inventing data.
pub fn stableNavCandle(date: Date) Candle {
return .{ .date = date, .open = 1, .high = 1, .low = 1, .close = 1, .adj_close = 1, .volume = 0 };
}
/// Type of holding in a portfolio lot.
pub const LotType = enum {
@ -806,3 +815,13 @@ test "totalForAccount" {
const total = portfolio.totalForAccount(allocator, "IRA", prices);
try std.testing.expectApproxEqAbs(@as(f64, 44500.0), total, 0.01);
}
test "stableNavCandle: fills all fields at $1" {
const c = stableNavCandle(Date.fromYmd(2026, 4, 1));
try std.testing.expectEqual(@as(f64, 1), c.close);
try std.testing.expectEqual(@as(f64, 1), c.open);
try std.testing.expectEqual(@as(f64, 1), c.high);
try std.testing.expectEqual(@as(f64, 1), c.low);
try std.testing.expectEqual(@as(f64, 1), c.adj_close);
try std.testing.expectEqual(@as(u64, 0), c.volume);
}