diff --git a/src/main.zig b/src/main.zig index b4acff4..0980dd9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -64,8 +64,13 @@ fn handleHelp(_: *App, _: *httpz.Request, res: *httpz.Response) !void { \\ GET /{SYMBOL}/options Raw SRF cache file \\ GET /symbols List of tracked symbols \\ + \\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 + \\ \\XML example (LibreCalc): - \\ =FILTERXML(WEBSERVICE("http://host/AAPL/returns?fmt=xml"),"//trailing1YearReturn") + \\ =FILTERXML(WEBSERVICE("http://host/AAPL/returns?fmt=xml"),"//total10YearReturn") \\ ; } @@ -143,17 +148,42 @@ fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void { return; } - const ret = zfin.performance.trailingReturns(candles); + // Price-only returns (from adjusted close) + const price_ret = zfin.performance.trailingReturns(candles); var date_buf: [10]u8 = undefined; const date_str = candles[candles.len - 1].date.format(&date_buf); const risk = zfin.risk.trailingRisk(candles); - 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 p1y = if (price_ret.one_year) |r| r.annualized_return else null; + 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; + // Total returns with dividend reinvestment (non-fatal if dividends unavailable). + // trailing* fields use total return when available, falling back to price-only. + var t1y: ?f64 = p1y; + var t3y: ?f64 = p3y; + var t5y: ?f64 = p5y; + var t10y: ?f64 = p10y; + + if (app.svc.getDividends(symbol)) |div_result| { + defer zfin.Dividend.freeSlice(app.allocator, div_result.data); + const total = zfin.performance.trailingReturnsWithDividends(candles, div_result.data); + if (total.one_year) |r| { + t1y = r.annualized_return; + } + if (total.three_year) |r| { + t3y = r.annualized_return; + } + if (total.five_year) |r| { + t5y = r.annualized_return; + } + if (total.ten_year) |r| { + t10y = r.annualized_return; + } + } else |_| {} + // Check if XML requested if (q.get("fmt")) |fmt| { if (std.ascii.eqlIgnoreCase(fmt, "xml")) { @@ -166,16 +196,24 @@ fn handleReturns(app: *App, req: *httpz.Request, res: *httpz.Response) !void { \\ {s} \\ {s} \\ {s} + \\ {s} + \\ {s} + \\ {s} + \\ {s} \\ {s} \\ \\ , .{ symbol, date_str, - fmtPct(arena, r1y), - fmtPct(arena, r3y), - fmtPct(arena, r5y), - fmtPct(arena, r10y), + fmtPct(arena, t1y), + fmtPct(arena, t3y), + fmtPct(arena, t5y), + fmtPct(arena, t10y), + fmtPct(arena, p1y), + fmtPct(arena, p3y), + fmtPct(arena, p5y), + fmtPct(arena, p10y), fmtPct(arena, vol), }); return; @@ -184,14 +222,18 @@ 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},"volatility":{s}}} + \\{{"ticker":"{s}","returnDate":"{s}","trailing1YearReturn":{s},"trailing3YearReturn":{s},"trailing5YearReturn":{s},"trailing10YearReturn":{s},"price1YearReturn":{s},"price3YearReturn":{s},"price5YearReturn":{s},"price10YearReturn":{s},"volatility":{s}}} , .{ symbol, date_str, - fmtPct(arena, r1y), - fmtPct(arena, r3y), - fmtPct(arena, r5y), - fmtPct(arena, r10y), + fmtPct(arena, t1y), + fmtPct(arena, t3y), + fmtPct(arena, t5y), + fmtPct(arena, t10y), + fmtPct(arena, p1y), + fmtPct(arena, p3y), + fmtPct(arena, p5y), + fmtPct(arena, p10y), fmtPct(arena, vol), }); }