fix bug in market-aware cache ttl. The function+tests is a bit overkill, but this is hard...
All checks were successful
Generic zig build / build (push) Successful in 4m52s
Generic zig build / publish-macos (push) Successful in 12s
Generic zig build / deploy (push) Successful in 18s

This commit is contained in:
Emil Lerch 2026-06-26 16:28:53 -07:00
parent 375af77b58
commit 0cd01dd452
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 67 additions and 2 deletions

View file

@ -315,6 +315,26 @@ pub fn candleFreshness(now_s: i64, kind: InstrumentKind, last_cached: Date) Cand
return .overdue;
}
/// Whether a candle fetch is warranted for `kind` as of `now_s`, given
/// `last_cached` (the newest bar already in the cache). True when a
/// newer bar is due (`candleFreshness` is `.lagging` or `.overdue`);
/// false when the cache already holds the latest *available* bar - a
/// weekend/holiday/pre-close gap, or genuinely caught up - so the caller
/// should just bump the TTL to the next boundary without hitting the
/// network.
///
/// This is the market-aware gate for getCandles' "do I need to fetch?"
/// decision. It deliberately shares `candleFreshness`'s availability
/// math so the fetch decision and the lag report cannot disagree. A
/// naive `last_cached + 1 >= today` calendar check would skip the fetch
/// for a just-closed session whose bar is due (last_cached == yesterday)
/// while `candleFreshness` simultaneously flagged it `.lagging`, freezing
/// the cache on the stale bar until the next boundary. Pure given
/// `now_s`, so it is fully deterministic for tests.
pub fn shouldRefresh(now_s: i64, kind: InstrumentKind, last_cached: Date) bool {
return candleFreshness(now_s, kind, last_cached) != .current;
}
/// Expiry to stamp on candle meta after an *incremental* fetch on a
/// stale entry returned zero new bars. Maps `candleFreshness` to a
/// boundary: a `.lagging` bar retries soon (`short_retry_s`); `.current`
@ -737,6 +757,41 @@ test "candleFreshness mutual_fund: late NAV is lagging" {
try testing.expectEqual(CandleFreshness.current, candleFreshness(now, .mutual_fund, Date.fromYmd(2025, 6, 16)));
}
test "shouldRefresh equity: just-closed session with only yesterday's bar -> refresh" {
// Fri 2025-06-13, 17:00 ET: Friday's bar is due (past the 16:55
// boundary) but the cache only holds Thursday 06-12. The old naive
// `last_cached + 1 >= today` check skipped this fetch and froze the
// cache until Monday; shouldRefresh must say yes.
const now = etLocalToUtc(Date.fromYmd(2025, 6, 13), 17 * std.time.s_per_hour);
try testing.expect(shouldRefresh(now, .equity, Date.fromYmd(2025, 6, 12)));
}
test "shouldRefresh equity: already holding the just-closed bar -> no refresh" {
const now = etLocalToUtc(Date.fromYmd(2025, 6, 13), 17 * std.time.s_per_hour);
try testing.expect(!shouldRefresh(now, .equity, Date.fromYmd(2025, 6, 13)));
}
test "shouldRefresh equity: pre-close, yesterday's bar is still the latest -> no refresh" {
// Fri 2025-06-13, 10:00 ET: before the 16:55 boundary, Thursday's bar
// is still the latest available. (The case the old check got right.)
const now = etLocalToUtc(Date.fromYmd(2025, 6, 13), 10 * std.time.s_per_hour);
try testing.expect(!shouldRefresh(now, .equity, Date.fromYmd(2025, 6, 12)));
}
test "shouldRefresh equity: weekend gap holding Friday's bar -> no refresh" {
// Sun 2025-06-15: nothing newer than Friday is due over the weekend,
// so no wasteful fetch (the old calendar check would have fetched).
const now = etLocalToUtc(Date.fromYmd(2025, 6, 15), 12 * std.time.s_per_hour);
try testing.expect(!shouldRefresh(now, .equity, Date.fromYmd(2025, 6, 13)));
}
test "shouldRefresh mutual_fund: weekend morning holding latest NAV -> no refresh" {
// Sat 2025-06-14, 05:00 ET: Friday's NAV (data_date 06-13) posted this
// morning; holding it means nothing newer is due.
const now = etLocalToUtc(Date.fromYmd(2025, 6, 14), 5 * std.time.s_per_hour);
try testing.expect(!shouldRefresh(now, .mutual_fund, Date.fromYmd(2025, 6, 13)));
}
test "fmtClockET: 12-hour rendering with EST/EDT and AM/PM" {
var buf: [16]u8 = undefined;
// Afternoon (EST, winter): 14:34 ET.

View file

@ -809,8 +809,18 @@ pub const DataService = struct {
// this stale path (next post-close / NAV-availability time).
const expires = market.nextCandleExpiry(now_s, kind);
// If last cached date is today or later, just refresh the TTL (meta only)
if (!fetch_from.lessThan(today)) {
// Only skip the fetch when we already hold the latest
// *available* bar (weekend/holiday/pre-close gap, or
// caught up): just bump the TTL. Gating on the
// market-aware `shouldRefresh` -- not a naive
// `last_cached + 1 >= today` calendar check -- keeps this
// decision consistent with the `candleFreshness` lag
// report. The old calendar check skipped the fetch when
// `last_date` was merely yesterday, so a just-closed
// session's due bar got cached as "fresh" until the next
// boundary while the lag check reported it lagging (the
// Friday-17:00 deadlock that exited 75 every retry).
if (!market.shouldRefresh(now_s, kind, m.last_date)) {
s.updateCandleMeta(symbol, m.last_close, m.last_date, m.provider, m.fail_count, expires);
if (s.read(self.allocator, Candle, symbol, null, .any)) |r|
return .{ .data = r.data, .source = .cached, .timestamp = std.Io.Timestamp.now(self.io, .real).toSeconds(), .allocator = self.allocator };