surface market-aware candle cache ttls
All checks were successful
Generic zig build / build (push) Successful in 1m44s
Generic zig build / deploy (push) Successful in 16s

This commit is contained in:
Emil Lerch 2026-06-25 15:44:09 -07:00
parent f3c16907df
commit fccb67698f
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 73 additions and 14 deletions

View file

@ -14,8 +14,8 @@
.hash = "httpz-0.0.0-PNVzrLjJCAD37S0CcrXpsjSqr86hVjK0rsALTDJ98AAJ",
},
.zfin = .{
.url = "git+https://git.lerch.org/lobo/zfin#d7a86cd63901e8c823bdeedd548e5beb9759ea9c",
.hash = "zfin-0.0.0-J-B21qcGPABbosCyx3cN-gMOFEN1ZbqrRZpS6WUQtLzC",
.url = "git+https://git.lerch.org/lobo/zfin#3cedde20eb9ca2eaa6917575acd09052831a4917",
.hash = "zfin-0.0.0-J-B21qbTSQDZ5XJC--ny2zSX0nmJ1_cfRj5WsB8Byoxh",
},
},
}

View file

@ -556,12 +556,18 @@ fn writeNewPortfolio(io: std.Io, allocator: std.mem.Allocator, path: []const u8,
// Refresh command
fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process.Environ.Map) !void {
fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process.Environ.Map) !u8 {
var config = zfin.Config.fromEnv(io, allocator, environ);
defer config.deinit();
var svc = zfin.DataService.init(io, allocator, config);
defer svc.deinit();
// wall-clock required: the provider-lag check compares each symbol's
// newest cached bar against the most recent session the market should
// already have data for (see zfin.market.candleFreshness). Captured
// once so the whole run shares a consistent "now".
const now_s = std.Io.Timestamp.now(io, .real).toSeconds();
const portfolio_path = environ.get("ZFIN_PORTFOLIO") orelse "portfolio.srf";
const data = std.Io.Dir.cwd().readFileAlloc(io, portfolio_path, allocator, .limited(10 * 1024 * 1024)) catch {
@ -598,6 +604,7 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
var success_count: u32 = 0;
var fail_count: u32 = 0;
var lag_count: u32 = 0;
// Warm the EDGAR ticker maps once per refresh run. They're
// ~3-5 MB each, cached for 30 days; warming guarantees the
@ -638,6 +645,26 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
if (svc.getCandles(sym, .{})) |result| {
defer result.deinit();
try stdout.print("candles ok ({s})", .{@tagName(result.source)});
// Provider-data-lag check: did we end up with the latest bar
// the market should have posted by now? A `.lagging` bar is
// merely unposted -> flag it so the run exits EX_TEMPFAIL and
// cron retries. An `.overdue` bar is almost certainly an
// un-modeled closure (e.g. Good Friday) -> note it, no retry.
if (result.data.len > 0) {
const last = result.data[result.data.len - 1].date;
var date_buf: [10]u8 = undefined;
const ds = std.fmt.bufPrint(&date_buf, "{f}", .{last}) catch "?";
switch (zfin.market.candleFreshness(now_s, zfin.market.classify(sym), last)) {
.lagging => {
lag_count += 1;
try stdout.print(" LAGGING (latest {s})", .{ds});
log.info("provider data lag: {s} latest bar {s}; newer session due but unposted", .{ sym, ds });
},
.overdue => log.info("{s}: latest bar {s} overdue past grace window; assuming market closure (no retry)", .{ sym, ds }),
.current => {},
}
}
} else |err| {
try stdout.print("candles FAILED ({s})", .{@errorName(err)});
sym_ok = false;
@ -752,15 +779,29 @@ fn refresh(io: std.Io, allocator: std.mem.Allocator, environ: *const std.process
if (sym_ok) success_count += 1 else fail_count += 1;
}
try stdout.print("\nRefresh complete: {d} ok, {d} failed\n", .{ success_count, fail_count });
try stdout.print("\nRefresh complete: {d} ok, {d} failed, {d} lagging\n", .{ success_count, fail_count, lag_count });
try stdout.flush();
if (fail_count > 0) return error.RefreshFailed;
return refreshExit(fail_count, lag_count);
}
/// Map a refresh run's failure/lag counts to a process exit code:
/// 0 - every symbol current and fetched cleanly
/// 75 - EX_TEMPFAIL: no hard failures, but at least one symbol's
/// just-closed bar hadn't posted yet (provider lag); cron should
/// retry shortly
/// 1 - at least one hard failure (fetch error)
/// Hard failure dominates lag - if anything failed outright that's the
/// code the operator needs to act on.
fn refreshExit(fail_count: u32, lag_count: u32) u8 {
if (fail_count > 0) return 1;
if (lag_count > 0) return 75;
return 0;
}
// Main
pub fn main(init: std.process.Init) !void {
pub fn main(init: std.process.Init) !u8 {
const allocator = init.gpa;
const io = init.io;
const environ = init.environ_map;
@ -769,8 +810,8 @@ pub fn main(init: std.process.Init) !void {
defer allocator.free(args);
if (args.len < 2) {
printUsage();
return;
try printUsage(io);
return 1;
}
const command = args[1];
@ -826,15 +867,19 @@ pub fn main(init: std.process.Init) !void {
log.info("listening on port {d}", .{port});
try server.listen();
} else if (std.mem.eql(u8, command, "refresh")) {
try refresh(io, init.arena.allocator(), environ);
return try refresh(io, init.arena.allocator(), environ);
} else {
printUsage();
try printUsage(io);
}
return 0;
}
fn printUsage() void {
std.debug.print("zfin-server {s}\n", .{version});
std.debug.print(
fn printUsage(io: std.Io) !void {
var buf: [2048]u8 = undefined;
var fw = std.Io.File.stderr().writer(io, &buf);
const w = &fw.interface;
try w.print("zfin-server {s}\n", .{version});
try w.writeAll(
\\Usage: zfin-server <command>
\\
\\Commands:
@ -848,7 +893,13 @@ fn printUsage() void {
\\ FINNHUB_API_KEY Finnhub API key
\\ ALPHAVANTAGE_API_KEY Alpha Vantage API key
\\
, .{});
\\refresh exit codes:
\\ 0 all tracked symbols current
\\ 75 provider data lag (a just-closed bar not yet posted) - retry soon
\\ 1 one or more hard failures
\\
);
try w.flush();
}
// Tests
@ -899,3 +950,11 @@ test "shouldLogRequest: custom threshold respected" {
try std.testing.expect(!shouldLogRequest(1500, 200, 2000));
try std.testing.expect(shouldLogRequest(2500, 200, 2000));
}
test "refreshExit: hard failure dominates, then lag, else clean" {
try std.testing.expectEqual(@as(u8, 0), refreshExit(0, 0));
try std.testing.expectEqual(@as(u8, 75), refreshExit(0, 3));
try std.testing.expectEqual(@as(u8, 1), refreshExit(2, 0));
// A hard failure outranks lag.
try std.testing.expectEqual(@as(u8, 1), refreshExit(1, 5));
}