fix bug in market-aware cache ttl. The function+tests is a bit overkill, but this is hard...
This commit is contained in:
parent
375af77b58
commit
0cd01dd452
2 changed files with 67 additions and 2 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue