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 {