main.zig cleanup / add version and rate limiting output
This commit is contained in:
parent
d7c4393398
commit
32cc139ef1
2 changed files with 79 additions and 66 deletions
|
|
@ -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(.{
|
||||
|
|
|
|||
138
src/main.zig
138
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();
|
||||
}
|
||||
}
|
||||
|
||||
log.info("refresh complete", .{});
|
||||
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;
|
||||
}
|
||||
|
||||
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 <command>
|
||||
\\
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue