diff --git a/src/main.zig b/src/main.zig index 0980dd9..18f8ec9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -67,7 +67,9 @@ fn handleHelp(_: *App, _: *httpz.Request, res: *httpz.Response) !void { \\Returns fields: \\ trailing{1,3,5,10}YearReturn Total return with dividend reinvestment \\ price{1,3,5,10}YearReturn Price-only return (from adjusted close) - \\ volatility 3-year annualized volatility + \\ volatility Longest-term available annualized volatility + \\ volatilityTerm Period (years) of the volatility field + \\ volatility{1,3,5,10}Year Per-period annualized volatility \\ \\XML example (LibreCalc): \\ =FILTERXML(WEBSERVICE("http://host/AAPL/returns?fmt=xml"),"//total10YearReturn") @@ -158,7 +160,16 @@ fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void { const p3y = if (price_ret.three_year) |r| r.annualized_return else null; const p5y = if (price_ret.five_year) |r| r.annualized_return else null; const p10y = if (price_ret.ten_year) |r| r.annualized_return else null; - const vol = if (risk.three_year) |r| r.volatility else null; + + // Per-period volatility + const v1y = if (risk.one_year) |r| r.volatility else null; + const v3y = if (risk.three_year) |r| r.volatility else null; + const v5y = if (risk.five_year) |r| r.volatility else null; + const v10y = if (risk.ten_year) |r| r.volatility else null; + + // Longest-term volatility convenience fields + const vol_best = v10y orelse v5y orelse v3y orelse v1y; + const vol_term: ?u8 = if (v10y != null) 10 else if (v5y != null) 5 else if (v3y != null) 3 else if (v1y != null) 1 else null; // Total returns with dividend reinvestment (non-fatal if dividends unavailable). // trailing* fields use total return when available, falling back to price-only. @@ -201,6 +212,11 @@ fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void { \\ {s} \\ {s} \\ {s} + \\ {s} + \\ {s} + \\ {s} + \\ {s} + \\ {s} \\ \\ , .{ @@ -214,7 +230,12 @@ fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void { fmtPct(arena, p3y), fmtPct(arena, p5y), fmtPct(arena, p10y), - fmtPct(arena, vol), + fmtPct(arena, vol_best), + fmtInt(arena, vol_term), + fmtPct(arena, v1y), + fmtPct(arena, v3y), + fmtPct(arena, v5y), + fmtPct(arena, v10y), }); return; } @@ -222,7 +243,7 @@ fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void { res.content_type = httpz.ContentType.JSON; res.body = try std.fmt.allocPrint(arena, - \\{{"ticker":"{s}","returnDate":"{s}","trailing1YearReturn":{s},"trailing3YearReturn":{s},"trailing5YearReturn":{s},"trailing10YearReturn":{s},"price1YearReturn":{s},"price3YearReturn":{s},"price5YearReturn":{s},"price10YearReturn":{s},"volatility":{s}}} + \\{{"ticker":"{s}","returnDate":"{s}","trailing1YearReturn":{s},"trailing3YearReturn":{s},"trailing5YearReturn":{s},"trailing10YearReturn":{s},"price1YearReturn":{s},"price3YearReturn":{s},"price5YearReturn":{s},"price10YearReturn":{s},"volatility":{s},"volatilityTerm":{s},"volatility1Year":{s},"volatility3Year":{s},"volatility5Year":{s},"volatility10Year":{s}}} , .{ symbol, date_str, @@ -234,7 +255,12 @@ fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void { fmtPct(arena, p3y), fmtPct(arena, p5y), fmtPct(arena, p10y), - fmtPct(arena, vol), + fmtPct(arena, vol_best), + fmtInt(arena, vol_term), + fmtPct(arena, v1y), + fmtPct(arena, v3y), + fmtPct(arena, v5y), + fmtPct(arena, v10y), }); } @@ -323,6 +349,12 @@ fn fmtPct(arena: std.mem.Allocator, value: ?f64) []const u8 { return "null"; } +/// Format an optional integer, or "null" if absent. +fn fmtInt(arena: std.mem.Allocator, value: ?u8) []const u8 { + if (value) |v| return std.fmt.allocPrint(arena, "{d}", .{v}) 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 {