From 6a680a2381fc078ebb3255b1aec587c4813efdc8 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Fri, 6 Mar 2026 15:24:24 -0800 Subject: [PATCH] switch earnings cache ttl to 30 days + day after earnings --- README.md | 6 ++++-- src/cache/store.zig | 4 ++-- src/service.zig | 18 ++++++++++++++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2edbd9f..a569040 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ zfin aggregates data from multiple free-tier APIs. Each provider is used for the | Dividends | Polygon | `POLYGON_API_KEY` | 5 req/min | 7 days | | Splits | Polygon | `POLYGON_API_KEY` | 5 req/min | 7 days | | Options chains | CBOE | None required | ~30 req/min (self-imposed) | 1 hour | -| Earnings | Finnhub | `FINNHUB_API_KEY` | 60 req/min | 24 hours | +| Earnings | Finnhub | `FINNHUB_API_KEY` | 60 req/min | 30 days* | | ETF profiles | Alpha Vantage | `ALPHAVANTAGE_API_KEY` | 25 req/day | 30 days | ### TwelveData @@ -127,10 +127,12 @@ Cache files use [SRF](https://github.com/lobo/srf) (Simple Record Format), a lin | Dividends | 7 days | Declared well in advance | | Splits | 7 days | Rare corporate events | | Options | 1 hour | Prices change continuously during market hours | -| Earnings | 24 hours | Quarterly events, estimates update periodically | +| Earnings | 30 days* | Quarterly events; smart refresh after announcements | | ETF profiles | 30 days | Holdings/weights change slowly | | Quotes | Never cached | Intended for live price checks | +\* **Earnings smart refresh:** Even within the 30-day TTL, cached earnings are automatically re-fetched when an earnings date has passed but the cache still has no actual results for it. This ensures results appear promptly after an announcement without wasteful daily polling. + Manual refresh (`r` / `F5` in TUI) invalidates the cache for the current tab's data before re-fetching. ### Rate limiting diff --git a/src/cache/store.zig b/src/cache/store.zig index a02e405..77b26c4 100644 --- a/src/cache/store.zig +++ b/src/cache/store.zig @@ -28,8 +28,8 @@ pub const Ttl = struct { pub const splits: i64 = 7 * 24 * 3600; /// Options chains refresh hourly pub const options: i64 = 3600; - /// Earnings refresh daily - pub const earnings: i64 = 24 * 3600; + /// Earnings refresh monthly, with smart refresh after announcements + pub const earnings: i64 = 30 * 24 * 3600; /// ETF profiles refresh monthly pub const etf_profile: i64 = 30 * 24 * 3600; }; diff --git a/src/service.zig b/src/service.zig index aec5a2e..8121f63 100644 --- a/src/service.zig +++ b/src/service.zig @@ -248,20 +248,34 @@ pub const DataService = struct { /// Fetch earnings history for a symbol (5 years back, 1 year forward). /// Checks cache first; fetches from Finnhub if stale/missing. + /// Smart refresh: even if cache is fresh, re-fetches when a past earnings + /// date has no actual results yet (i.e. results just came out). pub fn getEarnings(self: *DataService, symbol: []const u8) DataError!struct { data: []EarningsEvent, source: Source, timestamp: i64 } { var s = self.store(); + const today = todayDate(); const cached_raw = s.readRaw(symbol, .earnings) catch return DataError.CacheError; if (cached_raw) |data| { defer self.allocator.free(data); if (cache.Store.isFreshData(data, self.allocator)) { const events = cache.Store.deserializeEarnings(self.allocator, data) catch null; - if (events) |e| return .{ .data = e, .source = .cached, .timestamp = s.getMtime(symbol, .earnings) orelse std.time.timestamp() }; + if (events) |e| { + // Check if any past/today earnings event is still missing actual results. + // If so, the announcement likely just happened — force a refresh. + const needs_refresh = for (e) |ev| { + if (ev.actual == null and !today.lessThan(ev.date)) break true; + } else false; + + if (!needs_refresh) { + return .{ .data = e, .source = .cached, .timestamp = s.getMtime(symbol, .earnings) orelse std.time.timestamp() }; + } + // Stale: free cached events and re-fetch below + self.allocator.free(e); + } } } var fh = try self.getFinnhub(); - const today = todayDate(); const from = today.subtractYears(5); const to = today.addDays(365);