From 88241b2c7bb94dfa110a0165a0f223ceab1b0d04 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 19 Mar 2026 14:25:13 -0700 Subject: [PATCH] portfolio cli/tui cleanup --- src/commands/portfolio.zig | 33 ++++++++++++++------------------- src/main.zig | 2 +- src/tui/portfolio_tab.zig | 16 +++++++++++++--- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/commands/portfolio.zig b/src/commands/portfolio.zig index 63ebdb5..4cb18ea 100644 --- a/src/commands/portfolio.zig +++ b/src/commands/portfolio.zig @@ -3,7 +3,7 @@ const zfin = @import("../root.zig"); const cli = @import("common.zig"); const fmt = cli.fmt; -pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, file_path: []const u8, watchlist_path: ?[]const u8, force_refresh: bool, color: bool, out: *std.Io.Writer) !void { +pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, file_path: []const u8, watchlist_path: ?[]const u8, force_refresh: bool, color: bool, out: *std.Io.Writer) !void { // Load portfolio from SRF file const data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch |err| { try cli.stderrPrint("Error reading portfolio file: "); @@ -56,9 +56,6 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer const all_syms_count = syms.len + watch_syms.items.len; if (all_syms_count > 0) { - if (config.twelvedata_key == null) { - try cli.stderrPrint("Warning: TWELVEDATA_API_KEY not set. Cannot fetch current prices.\n"); - } // Progress callback for per-symbol output var progress_ctx = cli.LoadProgress{ @@ -201,11 +198,11 @@ pub fn display( const gl_abs = if (summary.unrealized_gain_loss >= 0) summary.unrealized_gain_loss else -summary.unrealized_gain_loss; try out.print(" Value: {s} Cost: {s} ", .{ fmt.fmtMoneyAbs(&val_buf, summary.total_value), fmt.fmtMoneyAbs(&cost_buf, summary.total_cost) }); try cli.setGainLoss(out, color, summary.unrealized_gain_loss); - if (summary.unrealized_gain_loss >= 0) { - try out.print("Gain/Loss: +{s} ({d:.1}%)", .{ fmt.fmtMoneyAbs(&gl_buf, gl_abs), summary.unrealized_return * 100.0 }); - } else { - try out.print("Gain/Loss: -{s} ({d:.1}%)", .{ fmt.fmtMoneyAbs(&gl_buf, gl_abs), summary.unrealized_return * 100.0 }); - } + try out.print("Gain/Loss: {c}{s} ({d:.1}%)", .{ + @as(u8, if (summary.unrealized_gain_loss >= 0) '+' else '-'), + fmt.fmtMoneyAbs(&gl_buf, gl_abs), + summary.unrealized_return * 100.0, + }); try cli.reset(out, color); try out.print("\n", .{}); } @@ -369,11 +366,10 @@ pub fn display( "", "", "", "TOTAL", fmt.fmtMoneyAbs(&total_mv_buf, summary.total_value), }); try cli.setGainLoss(out, color, summary.unrealized_gain_loss); - if (summary.unrealized_gain_loss >= 0) { - try out.print("+{s:>13}", .{fmt.fmtMoneyAbs(&total_gl_buf, gl_abs)}); - } else { - try out.print("-{s:>13}", .{fmt.fmtMoneyAbs(&total_gl_buf, gl_abs)}); - } + try out.print("{c}{s:>13}", .{ + @as(u8, if (summary.unrealized_gain_loss >= 0) '+' else '-'), + fmt.fmtMoneyAbs(&total_gl_buf, gl_abs), + }); try cli.reset(out, color); try out.print(" {s:>7}\n", .{"100.0%"}); } @@ -382,11 +378,10 @@ pub fn display( var rpl_buf: [24]u8 = undefined; const rpl_abs = if (summary.realized_gain_loss >= 0) summary.realized_gain_loss else -summary.realized_gain_loss; try cli.setGainLoss(out, color, summary.realized_gain_loss); - if (summary.realized_gain_loss >= 0) { - try out.print("\n Realized P&L: +{s}\n", .{fmt.fmtMoneyAbs(&rpl_buf, rpl_abs)}); - } else { - try out.print("\n Realized P&L: -{s}\n", .{fmt.fmtMoneyAbs(&rpl_buf, rpl_abs)}); - } + try out.print("\n Realized P&L: {c}{s}\n", .{ + @as(u8, if (summary.realized_gain_loss >= 0) '+' else '-'), + fmt.fmtMoneyAbs(&rpl_buf, rpl_abs), + }); try cli.reset(out, color); } diff --git a/src/main.zig b/src/main.zig index d5a4b94..7debe8a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -183,7 +183,7 @@ pub fn main() !u8 { file_path = args[pi]; } } - try commands.portfolio.run(allocator, config, &svc, file_path, watchlist_path, force_refresh, color, out); + try commands.portfolio.run(allocator, &svc, file_path, watchlist_path, force_refresh, color, out); } else if (std.mem.eql(u8, command, "lookup")) { if (args.len < 3) { try cli.stderrPrint("Error: 'lookup' requires a CUSIP argument\n"); diff --git a/src/tui/portfolio_tab.zig b/src/tui/portfolio_tab.zig index fc194f1..92797cd 100644 --- a/src/tui/portfolio_tab.zig +++ b/src/tui/portfolio_tab.zig @@ -34,6 +34,16 @@ const gl_col_start: usize = col_end_market_value; // ── Data loading ────────────────────────────────────────────── +/// Load portfolio data: prices, summary, candle map, and historical snapshots. +/// +/// Call paths: +/// 1. First tab visit: loadTabData() → here (guarded by portfolio_loaded flag) +/// 2. Manual refresh (r/F5): refreshCurrentTab() clears portfolio_loaded → loadTabData() → here +/// 3. Disk reload (R): reloadPortfolioFile() — separate function, cache-only, no network +/// +/// On first call, uses prefetched_prices (populated before TUI started). +/// On refresh, fetches live via svc.loadPrices. Tab switching skips this +/// entirely because the portfolio_loaded guard in loadTabData() short-circuits. pub fn loadPortfolioData(self: *App) void { self.portfolio_loaded = true; self.freePortfolioSummary(); @@ -894,7 +904,7 @@ fn drawWelcomeScreen(self: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, wi .{ .text = "", .style = th.contentStyle() }, .{ .text = " Portfolio mode:", .style = th.contentStyle() }, .{ .text = " zfin -p portfolio.srf Load a portfolio file", .style = th.mutedStyle() }, - .{ .text = try std.fmt.allocPrint(arena, " portfolio.srf Auto-loaded from cwd if present", .{}), .style = th.mutedStyle() }, + .{ .text = " portfolio.srf Auto-loaded from cwd if present", .style = th.mutedStyle() }, .{ .text = "", .style = th.contentStyle() }, .{ .text = " Navigation:", .style = th.contentStyle() }, .{ .text = " h / l Previous / next tab", .style = th.mutedStyle() }, @@ -906,8 +916,8 @@ fn drawWelcomeScreen(self: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, wi .{ .text = " q Quit", .style = th.mutedStyle() }, .{ .text = "", .style = th.contentStyle() }, .{ .text = " Sample portfolio.srf:", .style = th.contentStyle() }, - .{ .text = " symbol::VTI,shares::100,open_date::2024-01-15,open_price::220.50", .style = th.dimStyle() }, - .{ .text = " symbol::AAPL,shares::50,open_date::2024-03-01,open_price::170.00", .style = th.dimStyle() }, + .{ .text = " symbol::VTI,shares::100,open_date::2024-01-15,open_price::220.50", .style = th.mutedStyle() }, + .{ .text = " symbol::AAPL,shares::50,open_date::2024-03-01,open_price::170.00", .style = th.mutedStyle() }, }; try self.drawStyledContent(arena, buf, width, height, &welcome_lines); }