const std = @import("std"); const zfin = @import("../root.zig"); const cli = @import("common.zig"); const fmt = cli.fmt; pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, config: zfin.Config, symbol: []const u8, color: bool, out: *std.Io.Writer) !void { const result = svc.getDividends(symbol) catch |err| switch (err) { zfin.DataError.NoApiKey => { try cli.stderrPrint("Error: POLYGON_API_KEY not set. Get a free key at https://polygon.io\n"); return; }, else => { try cli.stderrPrint("Error fetching dividend data.\n"); return; }, }; defer allocator.free(result.data); if (result.source == .cached) try cli.stderrPrint("(using cached dividend data)\n"); // Fetch current price for yield calculation var current_price: ?f64 = null; if (config.twelvedata_key) |td_key| { var td = zfin.TwelveData.init(allocator, td_key); defer td.deinit(); if (td.fetchQuote(allocator, symbol)) |qr_val| { var qr = qr_val; defer qr.deinit(); if (qr.parse(allocator)) |q_val| { var q = q_val; defer q.deinit(); current_price = q.close(); } else |_| {} } else |_| {} } try display(result.data, symbol, current_price, color, out); } pub fn display(dividends: []const zfin.Dividend, symbol: []const u8, current_price: ?f64, color: bool, out: *std.Io.Writer) !void { try cli.setBold(out, color); try out.print("\nDividend History for {s}\n", .{symbol}); try cli.reset(out, color); try out.print("========================================\n", .{}); if (dividends.len == 0) { try cli.setFg(out, color, cli.CLR_MUTED); try out.print(" No dividends found.\n\n", .{}); try cli.reset(out, color); return; } try cli.setFg(out, color, cli.CLR_MUTED); try out.print("{s:>12} {s:>10} {s:>12} {s:>6} {s:>10}\n", .{ "Ex-Date", "Amount", "Pay Date", "Freq", "Type", }); try out.print("{s:->12} {s:->10} {s:->12} {s:->6} {s:->10}\n", .{ "", "", "", "", "", }); try cli.reset(out, color); const today = fmt.todayDate(); const one_year_ago = today.subtractYears(1); var total: f64 = 0; var ttm: f64 = 0; for (dividends) |div| { var ex_buf: [10]u8 = undefined; try out.print("{s:>12} {d:>10.4}", .{ div.ex_date.format(&ex_buf), div.amount }); if (div.pay_date) |pd| { var pay_buf: [10]u8 = undefined; try out.print(" {s:>12}", .{pd.format(&pay_buf)}); } else { try out.print(" {s:>12}", .{"--"}); } if (div.frequency) |f| { try out.print(" {d:>6}", .{f}); } else { try out.print(" {s:>6}", .{"--"}); } try out.print(" {s:>10}\n", .{@tagName(div.distribution_type)}); total += div.amount; if (!div.ex_date.lessThan(one_year_ago)) ttm += div.amount; } try out.print("\n{d} dividends, total: ${d:.4}\n", .{ dividends.len, total }); try cli.setFg(out, color, cli.CLR_ACCENT); try out.print("TTM dividends: ${d:.4}", .{ttm}); if (current_price) |cp| { if (cp > 0) { const yield = (ttm / cp) * 100.0; try out.print(" (yield: {d:.2}% at ${d:.2})", .{ yield, cp }); } } try cli.reset(out, color); try out.print("\n\n", .{}); } // ── Tests ──────────────────────────────────────────────────── test "display shows dividend data with yield" { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); const divs = [_]zfin.Dividend{ .{ .ex_date = .{ .days = 20000 }, .amount = 0.88, .distribution_type = .regular }, .{ .ex_date = .{ .days = 19900 }, .amount = 0.88, .distribution_type = .regular }, }; try display(&divs, "VTI", 250.0, false, &w); const out = w.buffered(); try std.testing.expect(std.mem.indexOf(u8, out, "VTI") != null); try std.testing.expect(std.mem.indexOf(u8, out, "0.8800") != null); try std.testing.expect(std.mem.indexOf(u8, out, "2 dividends") != null); try std.testing.expect(std.mem.indexOf(u8, out, "TTM") != null); try std.testing.expect(std.mem.indexOf(u8, out, "yield") != null); } test "display shows empty message" { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); const divs = [_]zfin.Dividend{}; try display(&divs, "BRK.A", null, false, &w); const out = w.buffered(); try std.testing.expect(std.mem.indexOf(u8, out, "No dividends found") != null); } test "display without price omits yield" { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); const divs = [_]zfin.Dividend{ .{ .ex_date = .{ .days = 20000 }, .amount = 1.50, .distribution_type = .regular }, }; try display(&divs, "T", null, false, &w); const out = w.buffered(); try std.testing.expect(std.mem.indexOf(u8, out, "yield") == null); try std.testing.expect(std.mem.indexOf(u8, out, "1 dividends") != null); } test "display no ANSI without color" { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); const divs = [_]zfin.Dividend{ .{ .ex_date = .{ .days = 20000 }, .amount = 0.50, .distribution_type = .regular }, }; try display(&divs, "SPY", 500.0, false, &w); const out = w.buffered(); try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null); }