enable early skip check when cache is fresh
This commit is contained in:
parent
edd9fc0c74
commit
75570419c5
2 changed files with 91 additions and 1 deletions
|
|
@ -94,10 +94,38 @@ pub fn run(
|
||||||
return SnapshotError.PortfolioEmpty;
|
return SnapshotError.PortfolioEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch prices via the shared TTL-driven loader.
|
|
||||||
const syms = try portfolio.stockSymbols(allocator);
|
const syms = try portfolio.stockSymbols(allocator);
|
||||||
defer allocator.free(syms);
|
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);
|
var prices = std.StringHashMap(f64).init(allocator);
|
||||||
defer prices.deinit();
|
defer prices.deinit();
|
||||||
|
|
||||||
|
|
@ -215,6 +243,44 @@ pub const QuoteDates = struct {
|
||||||
dates: []QuoteInfo,
|
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
|
/// Gather quote-date info for each symbol from the cache. Does not
|
||||||
/// fetch; relies on whatever the cache has. Symbols with no candles at
|
/// fetch; relies on whatever the cache has. Symbols with no candles at
|
||||||
/// all get `last_date = null`.
|
/// all get `last_date = null`.
|
||||||
|
|
@ -669,6 +735,17 @@ test "computeAsOfDate: empty input returns null" {
|
||||||
try testing.expect(computeAsOfDate(&.{}) == 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" {
|
test "quoteDateRange: min and max skip MM symbols" {
|
||||||
const d_old = Date.fromYmd(2026, 4, 17);
|
const d_old = Date.fromYmd(2026, 4, 17);
|
||||||
const d_new = Date.fromYmd(2026, 4, 20);
|
const d_new = Date.fromYmd(2026, 4, 20);
|
||||||
|
|
|
||||||
|
|
@ -679,6 +679,19 @@ pub const DataService = struct {
|
||||||
return s.readLastClose(symbol);
|
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.
|
/// 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.
|
/// Returns 0 if a request can be made immediately. Returns null if no API key.
|
||||||
pub fn estimateWaitSeconds(self: *DataService) ?u64 {
|
pub fn estimateWaitSeconds(self: *DataService) ?u64 {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue