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, symbol: []const u8, color: bool, out: *std.Io.Writer) !void { const result = svc.getEtfProfile(symbol) catch |err| switch (err) { zfin.DataError.NoApiKey => { try cli.stderrPrint("Error: ALPHAVANTAGE_API_KEY not set. Get a free key at https://alphavantage.co\n"); return; }, else => { try cli.stderrPrint("Error fetching ETF profile.\n"); return; }, }; const profile = result.data; defer { if (profile.holdings) |h| { for (h) |holding| { if (holding.symbol) |s| allocator.free(s); allocator.free(holding.name); } allocator.free(h); } if (profile.sectors) |s| { for (s) |sec| allocator.free(sec.sector); allocator.free(s); } } if (result.source == .cached) try cli.stderrPrint("(using cached ETF profile)\n"); try printProfile(profile, symbol, color, out); } pub fn printProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, out: *std.Io.Writer) !void { try cli.setBold(out, color); try out.print("\nETF Profile: {s}\n", .{symbol}); try cli.reset(out, color); try out.print("========================================\n", .{}); if (profile.expense_ratio) |er| { try out.print(" Expense Ratio: {d:.2}%\n", .{er * 100.0}); } if (profile.net_assets) |na| { try out.print(" Net Assets: ${s}\n", .{std.mem.trimRight(u8, &fmt.fmtLargeNum(na), &.{' '})}); } if (profile.dividend_yield) |dy| { try out.print(" Dividend Yield: {d:.2}%\n", .{dy * 100.0}); } if (profile.portfolio_turnover) |pt| { try out.print(" Portfolio Turnover: {d:.1}%\n", .{pt * 100.0}); } if (profile.inception_date) |d| { var db: [10]u8 = undefined; try out.print(" Inception Date: {s}\n", .{d.format(&db)}); } if (profile.leveraged) { try cli.setFg(out, color, cli.CLR_NEGATIVE); try out.print(" Leveraged: YES\n", .{}); try cli.reset(out, color); } if (profile.total_holdings) |th| { try out.print(" Total Holdings: {d}\n", .{th}); } // Sectors if (profile.sectors) |sectors| { if (sectors.len > 0) { try cli.setBold(out, color); try out.print("\n Sector Allocation:\n", .{}); try cli.reset(out, color); for (sectors) |sec| { try cli.setFg(out, color, cli.CLR_ACCENT); try out.print(" {d:>5.1}%", .{sec.weight * 100.0}); try cli.reset(out, color); try out.print(" {s}\n", .{sec.sector}); } } } // Top holdings if (profile.holdings) |holdings| { if (holdings.len > 0) { try cli.setBold(out, color); try out.print("\n Top Holdings:\n", .{}); try cli.reset(out, color); try cli.setFg(out, color, cli.CLR_MUTED); try out.print(" {s:>6} {s:>7} {s}\n", .{ "Symbol", "Weight", "Name" }); try out.print(" {s:->6} {s:->7} {s:->30}\n", .{ "", "", "" }); try cli.reset(out, color); for (holdings) |h| { if (h.symbol) |s| { try cli.setFg(out, color, cli.CLR_ACCENT); try out.print(" {s:>6}", .{s}); try cli.reset(out, color); try out.print(" {d:>6.2}% {s}\n", .{ h.weight * 100.0, h.name }); } else { try out.print(" {s:>6} {d:>6.2}% {s}\n", .{ "--", h.weight * 100.0, h.name }); } } } } try out.print("\n", .{}); } // ── Tests ──────────────────────────────────────────────────── test "printProfile minimal ETF no color" { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); const profile: zfin.EtfProfile = .{ .symbol = "VTI", .expense_ratio = 0.0003, .dividend_yield = 0.015, .total_holdings = 3500, }; try printProfile(profile, "VTI", 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, "Expense Ratio") != null); try std.testing.expect(std.mem.indexOf(u8, out, "0.03%") != null); try std.testing.expect(std.mem.indexOf(u8, out, "3500") != null); // No ANSI when color=false try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null); } test "printProfile leveraged ETF shows warning" { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); const profile: zfin.EtfProfile = .{ .symbol = "TQQQ", .expense_ratio = 0.0095, .leveraged = true, }; try printProfile(profile, "TQQQ", false, &w); const out = w.buffered(); try std.testing.expect(std.mem.indexOf(u8, out, "Leveraged") != null); try std.testing.expect(std.mem.indexOf(u8, out, "YES") != null); }