count a negative cache entry as cache - we will never see data from this provider

This commit is contained in:
Emil Lerch 2026-06-30 16:38:48 -07:00
parent cc5030d70c
commit a15b9cf471
Signed by: lobo
GPG key ID: A7B62D657EF764F8

43
src/cache/store.zig vendored
View file

@ -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));