Compare commits
No commits in common. "5052492ffd49ebdc11b209da1b86bbaf7d365e5e" and "913996072ee06c81121f50657e325ce8a56856e6" have entirely different histories.
5052492ffd
...
913996072e
2 changed files with 6 additions and 64 deletions
|
|
@ -30,9 +30,9 @@ pub const PerformanceResult = struct {
|
||||||
|
|
||||||
/// Compute total return from adjusted close prices.
|
/// Compute total return from adjusted close prices.
|
||||||
/// Candles must be sorted by date ascending.
|
/// Candles must be sorted by date ascending.
|
||||||
/// `from` snaps backward (last trading day on/before), `to` snaps backward.
|
/// `from` snaps forward (first trading day on/after), `to` snaps backward.
|
||||||
pub fn totalReturnFromAdjClose(candles: []const Candle, from: Date, to: Date) ?PerformanceResult {
|
pub fn totalReturnFromAdjClose(candles: []const Candle, from: Date, to: Date) ?PerformanceResult {
|
||||||
return totalReturnFromAdjCloseSnap(candles, from, to, .backward);
|
return totalReturnFromAdjCloseSnap(candles, from, to, .forward);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as totalReturnFromAdjClose but both dates snap backward
|
/// Same as totalReturnFromAdjClose but both dates snap backward
|
||||||
|
|
@ -146,20 +146,6 @@ 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 {
|
||||||
|
|
@ -253,14 +239,13 @@ fn findNearestCandle(candles: []const Candle, target: Date, direction: SearchDir
|
||||||
const candidate = switch (direction) {
|
const candidate = switch (direction) {
|
||||||
// First candle on or after target
|
// First candle on or after target
|
||||||
.forward => if (lo < candles.len) candles[lo] else return null,
|
.forward => if (lo < candles.len) candles[lo] else return null,
|
||||||
// Last candle on or before target; if target is before all data,
|
// Last candle on or before target
|
||||||
// fall back to first candle (snap distance check will reject if too far)
|
|
||||||
.backward => if (lo < candles.len and candles[lo].date.eql(target))
|
.backward => if (lo < candles.len and candles[lo].date.eql(target))
|
||||||
candles[lo]
|
candles[lo]
|
||||||
else if (lo > 0)
|
else if (lo > 0)
|
||||||
candles[lo - 1]
|
candles[lo - 1]
|
||||||
else
|
else
|
||||||
candles[0],
|
return null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reject if the snap distance exceeds tolerance
|
// Reject if the snap distance exceeds tolerance
|
||||||
|
|
@ -582,40 +567,3 @@ 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);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -596,14 +596,8 @@ pub const DataService = struct {
|
||||||
|
|
||||||
if (self.getDividends(symbol)) |div_result| {
|
if (self.getDividends(symbol)) |div_result| {
|
||||||
divs = div_result.data;
|
divs = div_result.data;
|
||||||
// adj_close returns are the primary total return source (accounts for
|
asof_total = performance.trailingReturnsWithDividends(c, div_result.data);
|
||||||
// splits + dividends). Dividend-reinvestment is only used as a fallback
|
me_total = performance.trailingReturnsMonthEndWithDividends(c, div_result.data, today);
|
||||||
// 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 .{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue