surface market-aware candle cache ttls
This commit is contained in:
parent
f3c16907df
commit
fccb67698f
2 changed files with 73 additions and 14 deletions
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
83
src/main.zig
83
src/main.zig
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue