use adj_close returns in most cases (accounting for splits and dividends

This commit is contained in:
Emil Lerch 2026-03-24 05:14:30 -07:00
parent 913996072e
commit 070f2352f4
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 59 additions and 2 deletions

View file

@ -146,6 +146,20 @@ pub const TrailingReturns = struct {
ten_year: ?PerformanceResult = null, ten_year: ?PerformanceResult = null,
}; };
/// Fill gaps in `primary` with results from `fallback`.
/// Used when adj_close returns (which account for splits + dividends) are the
/// primary source, but may return null for some periods (e.g. candle history
/// too short). The fallback typically dividend-reinvestment with stable-NAV
/// synthesis can cover those gaps.
pub fn withFallback(primary: TrailingReturns, fallback: TrailingReturns) TrailingReturns {
return .{
.one_year = primary.one_year orelse fallback.one_year,
.three_year = primary.three_year orelse fallback.three_year,
.five_year = primary.five_year orelse fallback.five_year,
.ten_year = primary.ten_year orelse fallback.ten_year,
};
}
/// Trailing returns from exact calendar date N years ago to latest candle date. /// Trailing returns from exact calendar date N years ago to latest candle date.
/// Start dates snap forward to the next trading day (e.g., weekend Monday). /// Start dates snap forward to the next trading day (e.g., weekend Monday).
pub fn trailingReturns(candles: []const Candle) TrailingReturns { pub fn trailingReturns(candles: []const Candle) TrailingReturns {
@ -567,3 +581,40 @@ test "stable-NAV synthesis -- non-$1 fund does not synthesize" {
const result = totalReturnWithDividends(&candles, &divs, Date.fromYmd(2021, 1, 1), Date.fromYmd(2026, 1, 1)); const result = totalReturnWithDividends(&candles, &divs, Date.fromYmd(2021, 1, 1), Date.fromYmd(2026, 1, 1));
try std.testing.expect(result == null); try std.testing.expect(result == null);
} }
test "withFallback -- fills null periods from fallback" {
const d1 = Date.fromYmd(2020, 1, 1);
const d2 = Date.fromYmd(2025, 1, 1);
const primary: TrailingReturns = .{
.one_year = .{ .total_return = 0.10, .annualized_return = 0.10, .from = d1, .to = d2 },
.three_year = .{ .total_return = 0.50, .annualized_return = 0.15, .from = d1, .to = d2 },
.five_year = null,
.ten_year = null,
};
const fallback: TrailingReturns = .{
.one_year = .{ .total_return = 0.08, .annualized_return = 0.08, .from = d1, .to = d2 },
.three_year = .{ .total_return = 0.60, .annualized_return = 0.17, .from = d1, .to = d2 },
.five_year = .{ .total_return = 0.80, .annualized_return = 0.12, .from = d1, .to = d2 },
.ten_year = .{ .total_return = 0.50, .annualized_return = 0.04, .from = d1, .to = d2 },
};
const merged = withFallback(primary, fallback);
// one_year: primary has data, keeps it (not overwritten by fallback)
try std.testing.expectApproxEqAbs(@as(f64, 0.10), merged.one_year.?.annualized_return.?, 0.001);
// three_year: primary has data, keeps it
try std.testing.expectApproxEqAbs(@as(f64, 0.15), merged.three_year.?.annualized_return.?, 0.001);
// five_year: primary null, filled from fallback
try std.testing.expectApproxEqAbs(@as(f64, 0.12), merged.five_year.?.annualized_return.?, 0.001);
// ten_year: primary null, filled from fallback
try std.testing.expectApproxEqAbs(@as(f64, 0.04), merged.ten_year.?.annualized_return.?, 0.001);
}
test "withFallback -- both null stays null" {
const a: TrailingReturns = .{};
const b: TrailingReturns = .{};
const merged = withFallback(a, b);
try std.testing.expect(merged.one_year == null);
try std.testing.expect(merged.ten_year == null);
}

View file

@ -596,8 +596,14 @@ pub const DataService = struct {
if (self.getDividends(symbol)) |div_result| { if (self.getDividends(symbol)) |div_result| {
divs = div_result.data; divs = div_result.data;
asof_total = performance.trailingReturnsWithDividends(c, div_result.data); // adj_close returns are the primary total return source (accounts for
me_total = performance.trailingReturnsMonthEndWithDividends(c, div_result.data, today); // splits + dividends). Dividend-reinvestment is only used as a fallback
// for periods where adj_close returns null (e.g. candle history too short
// for stable-NAV funds like money markets).
const asof_div = performance.trailingReturnsWithDividends(c, div_result.data);
asof_total = performance.withFallback(asof_price, asof_div);
const me_div = performance.trailingReturnsMonthEndWithDividends(c, div_result.data, today);
me_total = performance.withFallback(me_price, me_div);
} else |_| {} } else |_| {}
return .{ return .{