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",
|
.hash = "httpz-0.0.0-PNVzrLjJCAD37S0CcrXpsjSqr86hVjK0rsALTDJ98AAJ",
|
||||||
},
|
},
|
||||||
.zfin = .{
|
.zfin = .{
|
||||||
.url = "git+https://git.lerch.org/lobo/zfin#d7a86cd63901e8c823bdeedd548e5beb9759ea9c",
|
.url = "git+https://git.lerch.org/lobo/zfin#3cedde20eb9ca2eaa6917575acd09052831a4917",
|
||||||
.hash = "zfin-0.0.0-J-B21qcGPABbosCyx3cN-gMOFEN1ZbqrRZpS6WUQtLzC",
|
.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 ──────────────────────────────────────────
|
// ── 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);
|
var config = zfin.Config.fromEnv(io, allocator, environ);
|
||||||
defer config.deinit();
|
defer config.deinit();
|
||||||
var svc = zfin.DataService.init(io, allocator, config);
|
var svc = zfin.DataService.init(io, allocator, config);
|
||||||
defer svc.deinit();
|
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 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 {
|
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 success_count: u32 = 0;
|
||||||
var fail_count: u32 = 0;
|
var fail_count: u32 = 0;
|
||||||
|
var lag_count: u32 = 0;
|
||||||
|
|
||||||
// Warm the EDGAR ticker maps once per refresh run. They're
|
// Warm the EDGAR ticker maps once per refresh run. They're
|
||||||
// ~3-5 MB each, cached for 30 days; warming guarantees the
|
// ~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| {
|
if (svc.getCandles(sym, .{})) |result| {
|
||||||
defer result.deinit();
|
defer result.deinit();
|
||||||
try stdout.print("candles ok ({s})", .{@tagName(result.source)});
|
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| {
|
} else |err| {
|
||||||
try stdout.print("candles FAILED ({s})", .{@errorName(err)});
|
try stdout.print("candles FAILED ({s})", .{@errorName(err)});
|
||||||
sym_ok = false;
|
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;
|
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();
|
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 ─────────────────────────────────────────────────────
|
// ── Main ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn main(init: std.process.Init) !void {
|
pub fn main(init: std.process.Init) !u8 {
|
||||||
const allocator = init.gpa;
|
const allocator = init.gpa;
|
||||||
const io = init.io;
|
const io = init.io;
|
||||||
const environ = init.environ_map;
|
const environ = init.environ_map;
|
||||||
|
|
@ -769,8 +810,8 @@ pub fn main(init: std.process.Init) !void {
|
||||||
defer allocator.free(args);
|
defer allocator.free(args);
|
||||||
|
|
||||||
if (args.len < 2) {
|
if (args.len < 2) {
|
||||||
printUsage();
|
try printUsage(io);
|
||||||
return;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = args[1];
|
const command = args[1];
|
||||||
|
|
@ -826,15 +867,19 @@ pub fn main(init: std.process.Init) !void {
|
||||||
log.info("listening on port {d}", .{port});
|
log.info("listening on port {d}", .{port});
|
||||||
try server.listen();
|
try server.listen();
|
||||||
} else if (std.mem.eql(u8, command, "refresh")) {
|
} else if (std.mem.eql(u8, command, "refresh")) {
|
||||||
try refresh(io, init.arena.allocator(), environ);
|
return try refresh(io, init.arena.allocator(), environ);
|
||||||
} else {
|
} else {
|
||||||
printUsage();
|
try printUsage(io);
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn printUsage() void {
|
fn printUsage(io: std.Io) !void {
|
||||||
std.debug.print("zfin-server {s}\n", .{version});
|
var buf: [2048]u8 = undefined;
|
||||||
std.debug.print(
|
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>
|
\\Usage: zfin-server <command>
|
||||||
\\
|
\\
|
||||||
\\Commands:
|
\\Commands:
|
||||||
|
|
@ -848,7 +893,13 @@ fn printUsage() void {
|
||||||
\\ FINNHUB_API_KEY Finnhub API key
|
\\ FINNHUB_API_KEY Finnhub API key
|
||||||
\\ ALPHAVANTAGE_API_KEY Alpha Vantage 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 ────────────────────────────────────────────────────
|
// ── Tests ────────────────────────────────────────────────────
|
||||||
|
|
@ -899,3 +950,11 @@ test "shouldLogRequest: custom threshold respected" {
|
||||||
try std.testing.expect(!shouldLogRequest(1500, 200, 2000));
|
try std.testing.expect(!shouldLogRequest(1500, 200, 2000));
|
||||||
try std.testing.expect(shouldLogRequest(2500, 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