diff --git a/src/cache/store.zig b/src/cache/store.zig index 12d4967..18002eb 100644 --- a/src/cache/store.zig +++ b/src/cache/store.zig @@ -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 diff --git a/src/service.zig b/src/service.zig index d8ea4c8..2cfb8ed 100644 --- a/src/service.zig +++ b/src/service.zig @@ -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 }; }