From a15b9cf471a1412cdcabd57b8b27471ffb024137 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Tue, 30 Jun 2026 16:38:48 -0700 Subject: [PATCH] count a negative cache entry as cache - we will never see data from this provider --- src/cache/store.zig | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/cache/store.zig b/src/cache/store.zig index 18002eb..c0443ec 100644 --- a/src/cache/store.zig +++ b/src/cache/store.zig @@ -1236,7 +1236,18 @@ pub const Store = struct { } /// Check if candle metadata is fresh using the embedded `#!expires=` directive. + /// + /// A negative cache entry ("this symbol has no candle data on any + /// provider") counts as fresh, so the portfolio price fast-path + /// (`isCandleCacheFresh`) does not re-fetch it on every run. The + /// marker lives in `candles_daily.srf` - written by `writeNegative` + /// on a permanent NotFound, or synced verbatim from a `ZFIN_SERVER` - + /// and is honored there directly because `candles_meta.srf` is never + /// created for a no-data symbol. Cleared by `--refresh-data=force` / + /// `cache clear`. pub fn isCandleMetaFresh(self: *Store, symbol: []const u8) bool { + if (self.isNegative(symbol, .candles_daily)) return true; + const raw = self.readRaw(symbol, .candles_meta) catch return false; const data = raw orelse return false; defer self.allocator.free(data); @@ -3332,6 +3343,38 @@ test "Store.read does not self-heal an intact candles_daily" { try std.testing.expectError(error.FileNotFound, std.Io.Dir.cwd().access(std.testing.io, torn_dir_path, .{})); } +test "isCandleMetaFresh: negative candles_daily counts as fresh even with no candles_meta" { + const io = std.testing.io; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + const dir_path = try tmp.dir.realPathFileAlloc(io, ".", testing.allocator); + defer testing.allocator.free(dir_path); + + var store = Store.init(io, testing.allocator, dir_path); + + // A symbol with no candle data on any provider (e.g. crypto like + // DOGE-USD that Tiingo can't serve): the negative marker is written + // to candles_daily.srf and candles_meta.srf is never created. It must + // read as fresh so the portfolio price fast-path doesn't re-fetch it + // (and re-sync from a ZFIN_SERVER) on every run. + try store.ensureSymbolDir("DOGE-USD"); + store.writeNegative("DOGE-USD", .candles_daily); + try std.testing.expect(store.isCandleMetaFresh("DOGE-USD")); + + // A real, unexpired candles_meta is still fresh... + try store.ensureSymbolDir("AAA"); + try store.writeRaw("AAA", .candles_meta, "#!srfv1\n#!expires=9999999999\n#!created=1777000000\nlast_close:num:1.0,last_date::2026-04-22\n"); + try std.testing.expect(store.isCandleMetaFresh("AAA")); + + // ...and a past-due one is not. + try store.ensureSymbolDir("BBB"); + try store.writeRaw("BBB", .candles_meta, "#!srfv1\n#!expires=1000000000\n#!created=999999999\nlast_close:num:1.0,last_date::2026-04-22\n"); + try std.testing.expect(!store.isCandleMetaFresh("BBB")); + + // A symbol with no candle files at all is not fresh. + try std.testing.expect(!store.isCandleMetaFresh("ZZZ")); +} + test "Store.dataTypeFor maps model types correctly" { try std.testing.expectEqual(DataType.candles_daily, Store.dataTypeFor(Candle)); try std.testing.expectEqual(DataType.dividends, Store.dataTypeFor(Dividend));