add jitter to earnings
All checks were successful
Generic zig build / build (push) Successful in 4m39s
Generic zig build / publish-macos (push) Successful in 12s
Generic zig build / deploy (push) Successful in 19s

This commit is contained in:
Emil Lerch 2026-06-25 16:12:31 -07:00
parent 3cedde20eb
commit 72e874f052
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 22 additions and 13 deletions

29
src/cache/store.zig vendored
View file

@ -217,20 +217,23 @@ pub const DataType = enum {
/// symbols expire each day instead of all in lockstep.
///
/// - 8% on the longer-TTL types (classification 90d,
/// etf_metrics 90d, entity_facts 30d, ticker maps 30d).
/// Same thundering-herd defense; smaller percentage
/// because the absolute spread on a 30d/90d base is
/// already large in days.
/// etf_metrics 90d, entity_facts 30d, earnings 30d,
/// ticker maps 30d). Same thundering-herd defense;
/// smaller percentage because the absolute spread on a
/// 30d/90d base is already large in days. Earnings TTL is
/// fetch-anchored (not report-date-anchored), so a whole
/// portfolio fetched together would re-expire in lockstep
/// at the 30d boundary without jitter; 8% (~2.4d) breaks
/// that up.
///
/// - 0% on the rest. options/earnings either
/// have natural cadence spread or are short-TTL enough
/// that jitter would exceed meaningful drift.
/// - 0% on options: short-TTL enough that jitter would
/// exceed meaningful drift.
pub fn ttl(self: DataType) TtlSpec {
return switch (self) {
.dividends => .{ .seconds = Ttl.dividends, .jitter_pct = 11 },
.splits => .{ .seconds = Ttl.splits, .jitter_pct = 11 },
.options => .{ .seconds = Ttl.options },
.earnings => .{ .seconds = Ttl.earnings },
.earnings => .{ .seconds = Ttl.earnings, .jitter_pct = 8 },
.classification => .{ .seconds = Ttl.classification, .jitter_pct = 8 },
.etf_metrics => .{ .seconds = Ttl.etf_metrics, .jitter_pct = 8 },
.entity_facts => .{ .seconds = Ttl.entity_facts, .jitter_pct = 8 },
@ -3001,7 +3004,7 @@ test "DataType.ttl returns correct seconds and jitter policy" {
try std.testing.expectEqual(Ttl.splits, spl.seconds);
try std.testing.expectEqual(@as(u8, 11), spl.jitter_pct);
// 8% jitter: classification, etf_metrics, entity_facts, ticker maps.
// 8% jitter: classification, etf_metrics, entity_facts, earnings, ticker maps.
const cls = DataType.classification.ttl();
try std.testing.expectEqual(Ttl.classification, cls.seconds);
try std.testing.expectEqual(@as(u8, 8), cls.jitter_pct);
@ -3022,11 +3025,13 @@ test "DataType.ttl returns correct seconds and jitter policy" {
try std.testing.expectEqual(Ttl.tickers_companies, tc.seconds);
try std.testing.expectEqual(@as(u8, 8), tc.jitter_pct);
// No jitter: short-TTL types.
const ern = DataType.earnings.ttl();
try std.testing.expectEqual(Ttl.earnings, ern.seconds);
try std.testing.expectEqual(@as(u8, 8), ern.jitter_pct);
// No jitter: options (short-TTL).
try std.testing.expectEqual(Ttl.options, DataType.options.ttl().seconds);
try std.testing.expectEqual(@as(u8, 0), DataType.options.ttl().jitter_pct);
try std.testing.expectEqual(Ttl.earnings, DataType.earnings.ttl().seconds);
try std.testing.expectEqual(@as(u8, 0), DataType.earnings.ttl().jitter_pct);
// candles_daily, candles_meta, and meta have their own writers
// (`cacheCandles`, `writeNegative`); calling .ttl() on them is

View file

@ -1004,7 +1004,11 @@ pub const DataService = struct {
return DataError.FetchFailed;
};
s.write(EarningsEvent, symbol, fetched, .{ .seconds = cache.Ttl.earnings });
// Delegate to the centralized TTL policy (DataType.earnings.ttl)
// so the per-key jitter applies - a bare `.{ .seconds = ... }`
// here would re-expire a whole portfolio in lockstep at the 30d
// boundary.
s.write(EarningsEvent, symbol, fetched, cache.DataType.earnings.ttl());
return .{ .data = fetched, .source = .fetched, .timestamp = std.Io.Timestamp.now(self.io, .real).toSeconds(), .allocator = self.allocator };
}