tweak to earnings refresh based on data seen in the wild
This commit is contained in:
parent
120c51bce4
commit
375af77b58
1 changed files with 80 additions and 8 deletions
|
|
@ -934,10 +934,38 @@ pub const DataService = struct {
|
|||
return self.fetchCached(OptionsChain, symbol, null, opts);
|
||||
}
|
||||
|
||||
/// Days after an earnings report date during which a still-missing
|
||||
/// `actual` is worth chasing with a re-fetch. Past this window the
|
||||
/// gap is treated as permanent (FMP won't backfill it; earnings has
|
||||
/// no secondary source), so the cache is honored until its TTL.
|
||||
const earnings_actual_chase_days: i32 = 14;
|
||||
|
||||
/// Whether a fresh-in-cache earnings set warrants a re-fetch: true
|
||||
/// when an event whose report date has arrived (date <= today) is
|
||||
/// still missing its `actual` AND the report is recent enough
|
||||
/// (within `window_days`) that the actual could still post.
|
||||
///
|
||||
/// Earnings has no cross-provider backfill (unlike dividends/splits,
|
||||
/// which Tiingo supplements via the candle fetch), so a missing
|
||||
/// actual only arrives through a later FMP fetch. Without the recency
|
||||
/// bound a permanently-incomplete past row -- e.g. SPY's 2005-2006
|
||||
/// estimate-only rows FMP never backfills -- forces a re-fetch every
|
||||
/// run. The 30-day TTL backstops any actual slower than the window.
|
||||
fn earningsNeedsRefresh(events: []const EarningsEvent, today: Date, window_days: i32) bool {
|
||||
for (events) |ev| {
|
||||
if (ev.actual == null and !today.lessThan(ev.date) and today.days - ev.date.days <= window_days) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Fetch earnings history for a symbol.
|
||||
/// Checks cache first; fetches from FMP 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).
|
||||
/// Smart refresh: even if cache is fresh, re-fetches when a *recent*
|
||||
/// past earnings date (within `earnings_actual_chase_days`) still has
|
||||
/// no actual yet (results just came out). Older gaps are treated as
|
||||
/// permanent and honored until TTL -- see `earningsNeedsRefresh`.
|
||||
///
|
||||
/// `opts.skip_network = true` -> returns cached data even if stale,
|
||||
/// returns FetchFailed on cache miss without touching the network.
|
||||
|
|
@ -953,12 +981,12 @@ pub const DataService = struct {
|
|||
|
||||
if (!opts.force_refresh) {
|
||||
if (s.read(self.allocator, EarningsEvent, symbol, earningsPostProcess, .fresh_only)) |cached| {
|
||||
// Check if any past/today earnings event is still missing actual results.
|
||||
// If so, the announcement likely just happened - force a refresh.
|
||||
// (Suppressed when opts.skip_network - offline mode never refetches.)
|
||||
const needs_refresh = if (opts.skip_network) false else for (cached.data) |ev| {
|
||||
if (ev.actual == null and !today.lessThan(ev.date)) break true;
|
||||
} else false;
|
||||
// Re-fetch only when a recent report (within the chase
|
||||
// window) is still missing its actual; older gaps never
|
||||
// backfill, so honor the cache. Suppressed under
|
||||
// skip_network (offline mode never refetches).
|
||||
const needs_refresh = !opts.skip_network and
|
||||
earningsNeedsRefresh(cached.data, today, earnings_actual_chase_days);
|
||||
|
||||
if (!needs_refresh) {
|
||||
log.debug("{s}: earnings fresh in local cache", .{symbol});
|
||||
|
|
@ -4372,3 +4400,47 @@ test "getEtfProfile: carries holding CUSIP through the model boundary" {
|
|||
try std.testing.expectEqualStrings("999999999", holdings[0].cusip orelse return error.NoCusip);
|
||||
try std.testing.expect(holdings[0].symbol == null); // filing had no ticker
|
||||
}
|
||||
|
||||
test "earningsNeedsRefresh: recent missing actual triggers a re-fetch" {
|
||||
const today = Date.fromYmd(2026, 6, 26);
|
||||
const events = [_]EarningsEvent{
|
||||
.{ .date = Date.fromYmd(2026, 6, 20), .estimate = 1.0 }, // 6 days ago, actual not posted yet
|
||||
};
|
||||
try std.testing.expect(DataService.earningsNeedsRefresh(&events, today, 14));
|
||||
}
|
||||
|
||||
test "earningsNeedsRefresh: stale missing actual is NOT chased (the SPY case)" {
|
||||
const today = Date.fromYmd(2026, 6, 26);
|
||||
const events = [_]EarningsEvent{
|
||||
.{ .date = Date.fromYmd(2006, 5, 15), .estimate = 2.11 }, // ~20y old, FMP never backfills
|
||||
.{ .date = Date.fromYmd(2005, 2, 15), .actual = 1.81 },
|
||||
};
|
||||
try std.testing.expect(!DataService.earningsNeedsRefresh(&events, today, 14));
|
||||
}
|
||||
|
||||
test "earningsNeedsRefresh: all actuals present -> no re-fetch" {
|
||||
const today = Date.fromYmd(2026, 6, 26);
|
||||
const events = [_]EarningsEvent{
|
||||
.{ .date = Date.fromYmd(2026, 5, 20), .actual = 1.5 },
|
||||
.{ .date = Date.fromYmd(2026, 2, 20), .actual = 1.2 },
|
||||
};
|
||||
try std.testing.expect(!DataService.earningsNeedsRefresh(&events, today, 14));
|
||||
}
|
||||
|
||||
test "earningsNeedsRefresh: upcoming event without actual does not trigger" {
|
||||
const today = Date.fromYmd(2026, 6, 26);
|
||||
const events = [_]EarningsEvent{
|
||||
.{ .date = Date.fromYmd(2026, 8, 26), .estimate = 2.0 }, // future report, no actual yet
|
||||
};
|
||||
try std.testing.expect(!DataService.earningsNeedsRefresh(&events, today, 14));
|
||||
}
|
||||
|
||||
test "earningsNeedsRefresh: chase window is inclusive at the boundary" {
|
||||
const today = Date.fromYmd(2026, 6, 26);
|
||||
// Exactly 14 days ago -> still chased.
|
||||
const at_window = [_]EarningsEvent{.{ .date = Date.fromYmd(2026, 6, 12), .estimate = 1.0 }};
|
||||
try std.testing.expect(DataService.earningsNeedsRefresh(&at_window, today, 14));
|
||||
// 15 days ago -> past the window, left alone.
|
||||
const past_window = [_]EarningsEvent{.{ .date = Date.fromYmd(2026, 6, 11), .estimate = 1.0 }};
|
||||
try std.testing.expect(!DataService.earningsNeedsRefresh(&past_window, today, 14));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue