enable early skip check when cache is fresh
This commit is contained in:
parent
6ed2ff1f20
commit
ea0dd624b3
2 changed files with 91 additions and 1 deletions
|
|
@ -94,10 +94,38 @@ pub fn run(
|
|||
return SnapshotError.PortfolioEmpty;
|
||||
}
|
||||
|
||||
// Fetch prices via the shared TTL-driven loader.
|
||||
const syms = try portfolio.stockSymbols(allocator);
|
||||
defer allocator.free(syms);
|
||||
|
||||
// Early duplicate-skip: if the cache is fully fresh, we can compute
|
||||
// as_of_date without touching the network or doing a full price load,
|
||||
// then short-circuit when today's snapshot already exists. Critically,
|
||||
// this only applies when ALL non-MM symbols have fresh metadata — a
|
||||
// single stale symbol means a refresh might bring forward a newer
|
||||
// `last_date`, which would change as_of_date and make the existing
|
||||
// snapshot file no longer a duplicate.
|
||||
if (!force and out_override == null) {
|
||||
if (try probeFreshAsOfDate(allocator, svc, syms)) |candidate| {
|
||||
var cand_buf: [10]u8 = undefined;
|
||||
const cand_str = candidate.format(&cand_buf);
|
||||
const candidate_path = try deriveSnapshotPath(allocator, portfolio_path, cand_str);
|
||||
defer allocator.free(candidate_path);
|
||||
if (std.fs.cwd().access(candidate_path, .{})) |_| {
|
||||
var msg_buf: [256]u8 = undefined;
|
||||
const msg = std.fmt.bufPrint(
|
||||
&msg_buf,
|
||||
"snapshot for {s} already exists: {s} (cache fresh, skipped without refresh)\n",
|
||||
.{ cand_str, candidate_path },
|
||||
) catch "snapshot already exists\n";
|
||||
try cli.stderrPrint(msg);
|
||||
if (!dry_run) return;
|
||||
// --dry-run falls through: the user probably wants to see
|
||||
// what would be written.
|
||||
} else |_| {}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch prices via the shared TTL-driven loader.
|
||||
var prices = std.StringHashMap(f64).init(allocator);
|
||||
defer prices.deinit();
|
||||
|
||||
|
|
@ -215,6 +243,44 @@ pub const QuoteDates = struct {
|
|||
dates: []QuoteInfo,
|
||||
};
|
||||
|
||||
/// Probe the cache to see if we can safely compute `as_of_date` without
|
||||
/// doing a full price load. Returns the candidate date only if EVERY
|
||||
/// non-MM held symbol has fresh cache metadata — a single stale symbol
|
||||
/// means a refresh could bring forward a newer `last_date` and change
|
||||
/// the answer, so we must do the full load in that case.
|
||||
///
|
||||
/// This exists purely as a fast path for the duplicate-skip check:
|
||||
/// callers that get a non-null result may safely consult
|
||||
/// `history/<date>-portfolio.srf` for an existing file without spending
|
||||
/// the ~15s network round-trip of `loadPortfolioPrices`.
|
||||
///
|
||||
/// MM symbols are allowed to be stale — their `last_date` is excluded
|
||||
/// from the mode calculation anyway.
|
||||
pub fn probeFreshAsOfDate(
|
||||
allocator: std.mem.Allocator,
|
||||
svc: *zfin.DataService,
|
||||
symbols: []const []const u8,
|
||||
) !?Date {
|
||||
if (symbols.len == 0) return null;
|
||||
|
||||
var infos = try allocator.alloc(QuoteInfo, symbols.len);
|
||||
defer allocator.free(infos);
|
||||
|
||||
for (symbols, 0..) |sym, idx| {
|
||||
const is_mm = portfolio_mod.isMoneyMarketSymbol(sym);
|
||||
// MM symbols are excluded from as_of_date computation regardless
|
||||
// of freshness, so their TTL state doesn't matter here.
|
||||
if (!is_mm and !svc.isCandleCacheFresh(sym)) return null;
|
||||
infos[idx] = .{
|
||||
.symbol = sym,
|
||||
.last_date = svc.getCachedLastDate(sym),
|
||||
.is_money_market = is_mm,
|
||||
};
|
||||
}
|
||||
|
||||
return computeAsOfDate(infos);
|
||||
}
|
||||
|
||||
/// Gather quote-date info for each symbol from the cache. Does not
|
||||
/// fetch; relies on whatever the cache has. Symbols with no candles at
|
||||
/// all get `last_date = null`.
|
||||
|
|
@ -669,6 +735,17 @@ test "computeAsOfDate: empty input returns null" {
|
|||
try testing.expect(computeAsOfDate(&.{}) == null);
|
||||
}
|
||||
|
||||
test "probeFreshAsOfDate: empty symbol list returns null without touching the service" {
|
||||
// No symbols means no refresh is needed and no date to compute. A
|
||||
// null service pointer here would be dereferenced if the function
|
||||
// touched it, so this also proves the early-return path.
|
||||
var svc: zfin.DataService = undefined;
|
||||
// Pointer not dereferenced because the function returns before the
|
||||
// loop. Using @constCast to produce a pointer of the right type
|
||||
// without zero-initializing DataService internals.
|
||||
try testing.expect((try probeFreshAsOfDate(testing.allocator, &svc, &.{})) == null);
|
||||
}
|
||||
|
||||
test "quoteDateRange: min and max skip MM symbols" {
|
||||
const d_old = Date.fromYmd(2026, 4, 17);
|
||||
const d_new = Date.fromYmd(2026, 4, 20);
|
||||
|
|
|
|||
|
|
@ -679,6 +679,19 @@ pub const DataService = struct {
|
|||
return s.readLastClose(symbol);
|
||||
}
|
||||
|
||||
/// Read the latest cached candle date for `symbol` without deserializing
|
||||
/// the full candle history. Returns null if no cached metadata exists.
|
||||
///
|
||||
/// Callers should pair this with `isCandleCacheFresh` before trusting
|
||||
/// the date: a stale cache entry can return a date from days or weeks
|
||||
/// ago, which is fine for diagnostics but wrong for anything that
|
||||
/// needs "the current market date".
|
||||
pub fn getCachedLastDate(self: *DataService, symbol: []const u8) ?Date {
|
||||
var s = self.store();
|
||||
const mr = s.readCandleMeta(symbol) orelse return null;
|
||||
return mr.meta.last_date;
|
||||
}
|
||||
|
||||
/// Estimate wait time (in seconds) before the next TwelveData API call can proceed.
|
||||
/// Returns 0 if a request can be made immediately. Returns null if no API key.
|
||||
pub fn estimateWaitSeconds(self: *DataService) ?u64 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue