From 32cc139ef14228e28f9bb6e8f541f370d6163150 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Wed, 11 Mar 2026 10:45:48 -0700 Subject: [PATCH] main.zig cleanup / add version and rate limiting output --- build.zig | 7 +++ src/main.zig | 138 +++++++++++++++++++++++++++------------------------ 2 files changed, 79 insertions(+), 66 deletions(-) diff --git a/build.zig b/build.zig index 576c256..f8f6474 100644 --- a/build.zig +++ b/build.zig @@ -1,9 +1,15 @@ const std = @import("std"); +const GitVersion = @import("GitVersion.zig"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + // Get the current git version embedded so we can output at startup + const version = GitVersion.getVersion(b, .{}); + const build_options = b.addOptions(); + build_options.addOption([]const u8, "version", version); + const zfin_dep = b.dependency("zfin", .{ .target = target, .optimize = optimize, @@ -17,6 +23,7 @@ pub fn build(b: *std.Build) void { const imports: []const std.Build.Module.Import = &.{ .{ .name = "zfin", .module = zfin_dep.module("zfin") }, .{ .name = "httpz", .module = httpz_dep.module("httpz") }, + .{ .name = "build_options", .module = build_options.createModule() }, }; const exe = b.addExecutable(.{ diff --git a/src/main.zig b/src/main.zig index e4dae1b..959cb78 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,7 +9,9 @@ const std = @import("std"); const zfin = @import("zfin"); const httpz = @import("httpz"); +const build_options = @import("build_options"); +const version = build_options.version; const log = std.log.scoped(.@"zfin-server"); // ── App ────────────────────────────────────────────────────── @@ -109,7 +111,7 @@ fn handleSymbols(_: *App, _: *httpz.Request, res: *httpz.Response) !void { fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void { const raw_symbol = req.param("symbol") orelse { - res.status = 400; + res.status = 404; res.body = "Missing symbol"; return; }; @@ -145,10 +147,10 @@ fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void { const date_str = candles[candles.len - 1].date.format(&date_buf); const risk = zfin.risk.computeRisk(candles, zfin.risk.default_risk_free_rate); - const r1y = annualizedOrTotal(ret.one_year); - const r3y = annualizedOrTotal(ret.three_year); - const r5y = annualizedOrTotal(ret.five_year); - const r10y = annualizedOrTotal(ret.ten_year); + const r1y = if (ret.one_year) |r| r.annualized_return else null; + const r3y = if (ret.three_year) |r| r.annualized_return else null; + const r5y = if (ret.five_year) |r| r.annualized_return else null; + const r10y = if (ret.ten_year) |r| r.annualized_return else null; const vol = if (risk) |r| r.volatility else null; // Check if XML requested @@ -185,11 +187,11 @@ fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void { , .{ symbol, date_str, - fmtJsonNum(arena, r1y), - fmtJsonNum(arena, r3y), - fmtJsonNum(arena, r5y), - fmtJsonNum(arena, r10y), - fmtJsonNum(arena, vol), + fmtPct(arena, r1y), + fmtPct(arena, r3y), + fmtPct(arena, r5y), + fmtPct(arena, r10y), + fmtPct(arena, vol), }); } @@ -223,7 +225,7 @@ fn handleSrfFile(app: *App, req: *httpz.Request, res: *httpz.Response, filename: const arena = res.arena; const symbol = try upperDupe(arena, raw_symbol); - const path = try std.fmt.allocPrint(arena, "{s}/{s}/{s}", .{ app.config.cache_dir, symbol, filename }); + const path = try std.fs.path.join(arena, &.{ app.config.cache_dir, symbol, filename }); const content = std.fs.cwd().readFileAlloc(arena, path, 10 * 1024 * 1024) catch { res.status = 404; res.body = "Cache file not found"; @@ -259,23 +261,12 @@ fn upperDupe(allocator: std.mem.Allocator, s: []const u8) ![]u8 { return d; } -fn annualizedOrTotal(result: ?zfin.performance.PerformanceResult) ?f64 { - if (result) |r| return r.annualized_return orelse r.total_return; - return null; -} - /// Format as percentage string for XML (e.g., 0.1234 -> "12.34000") fn fmtPct(arena: std.mem.Allocator, value: ?f64) []const u8 { if (value) |v| return std.fmt.allocPrint(arena, "{d:.5}", .{v * 100.0}) catch "null"; return "null"; } -/// Format for JSON: number or null literal -fn fmtJsonNum(arena: std.mem.Allocator, value: ?f64) []const u8 { - if (value) |v| return std.fmt.allocPrint(arena, "{d:.5}", .{v * 100.0}) catch "null"; - return "null"; -} - /// Append a watch lot for the given symbol to the portfolio SRF file, /// unless it already exists. Best-effort — errors are logged, not fatal. fn appendWatchSymbol(symbol: []const u8) !void { @@ -376,25 +367,70 @@ fn refresh(allocator: std.mem.Allocator) !void { } } - log.info("refreshing {d} symbols...", .{symbols.count()}); + const stdout_file = std.fs.File.stdout(); + var buf: [4096]u8 = undefined; + var writer = stdout_file.writer(&buf); + const stdout = &writer.interface; + + try stdout.print("zfin-server {s}\n", .{version}); + try stdout.print("Refreshing {d} symbols from {s}\n", .{ symbols.count(), portfolio_path }); + try stdout.flush(); + + var success_count: u32 = 0; + var fail_count: u32 = 0; var it = symbols.iterator(); while (it.next()) |entry| { const sym = entry.key_ptr.*; - log.info(" {s}", .{sym}); - _ = svc.getCandles(sym) catch |err| { - log.warn(" {s}: candles: {}", .{ sym, err }); - }; - _ = svc.getDividends(sym) catch |err| { - log.warn(" {s}: dividends: {}", .{ sym, err }); - }; - _ = svc.getEarnings(sym) catch |err| { - log.warn(" {s}: earnings: {}", .{ sym, err }); - }; + // Check if we need to wait for rate limits + if (svc.estimateWaitSeconds()) |wait| { + if (wait > 0) { + try stdout.print(" (rate limit -- waiting {d}s)\n", .{wait}); + try stdout.flush(); + } + } + + try stdout.print("{s}: ", .{sym}); + try stdout.flush(); + + var sym_ok = true; + + // Candles + if (svc.getCandles(sym)) |result| { + allocator.free(result.data); + try stdout.print("candles ok", .{}); + } else |err| { + try stdout.print("candles FAILED ({s})", .{@errorName(err)}); + sym_ok = false; + } + + // Dividends + if (svc.getDividends(sym)) |result| { + allocator.free(result.data); + try stdout.print(", dividends ok", .{}); + } else |err| { + try stdout.print(", dividends FAILED ({s})", .{@errorName(err)}); + sym_ok = false; + } + + // Earnings + if (svc.getEarnings(sym)) |result| { + allocator.free(result.data); + try stdout.print(", earnings ok", .{}); + } else |err| { + try stdout.print(", earnings FAILED ({s})", .{@errorName(err)}); + sym_ok = false; + } + + try stdout.print("\n", .{}); + try stdout.flush(); + + if (sym_ok) success_count += 1 else fail_count += 1; } - log.info("refresh complete", .{}); + try stdout.print("\nRefresh complete: {d} ok, {d} failed\n", .{ success_count, fail_count }); + try stdout.flush(); } // ── Main ───────────────────────────────────────────────────── @@ -448,6 +484,7 @@ pub fn main() !void { router.get("/:symbol/earnings", handleEarnings, .{}); router.get("/:symbol/options", handleOptions, .{}); + log.info("zfin-server {s}", .{version}); log.info("listening on port {d}", .{port}); try server.listen(); } else if (std.mem.eql(u8, command, "refresh")) { @@ -458,6 +495,7 @@ pub fn main() !void { } fn printUsage() void { + std.debug.print("zfin-server {s}\n", .{version}); std.debug.print( \\Usage: zfin-server \\ @@ -487,40 +525,8 @@ test "fmtPct" { try std.testing.expect(std.mem.startsWith(u8, result, "12.34")); } -test "fmtJsonNum" { - var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena_state.deinit(); - const arena = arena_state.allocator(); - - try std.testing.expectEqualStrings("null", fmtJsonNum(arena, null)); - const result = fmtJsonNum(arena, 0.1234); - try std.testing.expect(std.mem.startsWith(u8, result, "12.34")); -} - -test "wantsXml" { - // Tested through handleReturns query parsing -} - test "upperDupe" { const result = try upperDupe(std.testing.allocator, "aapl"); defer std.testing.allocator.free(result); try std.testing.expectEqualStrings("AAPL", result); } - -test "annualizedOrTotal" { - try std.testing.expect(annualizedOrTotal(null) == null); - const with_ann = zfin.performance.PerformanceResult{ - .total_return = 0.5, - .annualized_return = 0.15, - .from = .{ .days = 0 }, - .to = .{ .days = 365 }, - }; - try std.testing.expectApproxEqAbs(@as(f64, 0.15), annualizedOrTotal(with_ann).?, 0.001); - const without_ann = zfin.performance.PerformanceResult{ - .total_return = 0.08, - .annualized_return = null, - .from = .{ .days = 0 }, - .to = .{ .days = 180 }, - }; - try std.testing.expectApproxEqAbs(@as(f64, 0.08), annualizedOrTotal(without_ann).?, 0.001); -}