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. /// symbols expire each day instead of all in lockstep.
/// ///
/// - 8% on the longer-TTL types (classification 90d, /// - 8% on the longer-TTL types (classification 90d,
/// etf_metrics 90d, entity_facts 30d, ticker maps 30d). /// etf_metrics 90d, entity_facts 30d, earnings 30d,
/// Same thundering-herd defense; smaller percentage /// ticker maps 30d). Same thundering-herd defense;
/// because the absolute spread on a 30d/90d base is /// smaller percentage because the absolute spread on a
/// already large in days. /// 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 /// - 0% on options: short-TTL enough that jitter would
/// have natural cadence spread or are short-TTL enough /// exceed meaningful drift.
/// that jitter would exceed meaningful drift.
pub fn ttl(self: DataType) TtlSpec { pub fn ttl(self: DataType) TtlSpec {
return switch (self) { return switch (self) {
.dividends => .{ .seconds = Ttl.dividends, .jitter_pct = 11 }, .dividends => .{ .seconds = Ttl.dividends, .jitter_pct = 11 },
.splits => .{ .seconds = Ttl.splits, .jitter_pct = 11 }, .splits => .{ .seconds = Ttl.splits, .jitter_pct = 11 },
.options => .{ .seconds = Ttl.options }, .options => .{ .seconds = Ttl.options },
.earnings => .{ .seconds = Ttl.earnings }, .earnings => .{ .seconds = Ttl.earnings, .jitter_pct = 8 },
.classification => .{ .seconds = Ttl.classification, .jitter_pct = 8 }, .classification => .{ .seconds = Ttl.classification, .jitter_pct = 8 },
.etf_metrics => .{ .seconds = Ttl.etf_metrics, .jitter_pct = 8 }, .etf_metrics => .{ .seconds = Ttl.etf_metrics, .jitter_pct = 8 },
.entity_facts => .{ .seconds = Ttl.entity_facts, .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(Ttl.splits, spl.seconds);
try std.testing.expectEqual(@as(u8, 11), spl.jitter_pct); 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(); const cls = DataType.classification.ttl();
try std.testing.expectEqual(Ttl.classification, cls.seconds); try std.testing.expectEqual(Ttl.classification, cls.seconds);
try std.testing.expectEqual(@as(u8, 8), cls.jitter_pct); 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(Ttl.tickers_companies, tc.seconds);
try std.testing.expectEqual(@as(u8, 8), tc.jitter_pct); 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(Ttl.options, DataType.options.ttl().seconds);
try std.testing.expectEqual(@as(u8, 0), DataType.options.ttl().jitter_pct); 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 // candles_daily, candles_meta, and meta have their own writers
// (`cacheCandles`, `writeNegative`); calling .ttl() on them is // (`cacheCandles`, `writeNegative`); calling .ttl() on them is

View file

@ -1004,7 +1004,11 @@ pub const DataService = struct {
return DataError.FetchFailed; 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 }; return .{ .data = fetched, .source = .fetched, .timestamp = std.Io.Timestamp.now(self.io, .real).toSeconds(), .allocator = self.allocator };
} }