portfolio cli/tui cleanup

This commit is contained in:
Emil Lerch 2026-03-19 14:25:13 -07:00
parent b4f3857cef
commit 88241b2c7b
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 28 additions and 23 deletions

View file

@ -3,7 +3,7 @@ const zfin = @import("../root.zig");
const cli = @import("common.zig"); const cli = @import("common.zig");
const fmt = cli.fmt; 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 // Load portfolio from SRF file
const data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch |err| { const data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch |err| {
try cli.stderrPrint("Error reading portfolio file: "); 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; const all_syms_count = syms.len + watch_syms.items.len;
if (all_syms_count > 0) { 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 // Progress callback for per-symbol output
var progress_ctx = cli.LoadProgress{ 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; 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 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); try cli.setGainLoss(out, color, summary.unrealized_gain_loss);
if (summary.unrealized_gain_loss >= 0) { try out.print("Gain/Loss: {c}{s} ({d:.1}%)", .{
try out.print("Gain/Loss: +{s} ({d:.1}%)", .{ fmt.fmtMoneyAbs(&gl_buf, gl_abs), summary.unrealized_return * 100.0 }); @as(u8, if (summary.unrealized_gain_loss >= 0) '+' else '-'),
} else { fmt.fmtMoneyAbs(&gl_buf, gl_abs),
try out.print("Gain/Loss: -{s} ({d:.1}%)", .{ fmt.fmtMoneyAbs(&gl_buf, gl_abs), summary.unrealized_return * 100.0 }); summary.unrealized_return * 100.0,
} });
try cli.reset(out, color); try cli.reset(out, color);
try out.print("\n", .{}); try out.print("\n", .{});
} }
@ -369,11 +366,10 @@ pub fn display(
"", "", "", "TOTAL", fmt.fmtMoneyAbs(&total_mv_buf, summary.total_value), "", "", "", "TOTAL", fmt.fmtMoneyAbs(&total_mv_buf, summary.total_value),
}); });
try cli.setGainLoss(out, color, summary.unrealized_gain_loss); try cli.setGainLoss(out, color, summary.unrealized_gain_loss);
if (summary.unrealized_gain_loss >= 0) { try out.print("{c}{s:>13}", .{
try out.print("+{s:>13}", .{fmt.fmtMoneyAbs(&total_gl_buf, gl_abs)}); @as(u8, if (summary.unrealized_gain_loss >= 0) '+' else '-'),
} else { fmt.fmtMoneyAbs(&total_gl_buf, gl_abs),
try out.print("-{s:>13}", .{fmt.fmtMoneyAbs(&total_gl_buf, gl_abs)}); });
}
try cli.reset(out, color); try cli.reset(out, color);
try out.print(" {s:>7}\n", .{"100.0%"}); try out.print(" {s:>7}\n", .{"100.0%"});
} }
@ -382,11 +378,10 @@ pub fn display(
var rpl_buf: [24]u8 = undefined; var rpl_buf: [24]u8 = undefined;
const rpl_abs = if (summary.realized_gain_loss >= 0) summary.realized_gain_loss else -summary.realized_gain_loss; 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); try cli.setGainLoss(out, color, summary.realized_gain_loss);
if (summary.realized_gain_loss >= 0) { try out.print("\n Realized P&L: {c}{s}\n", .{
try out.print("\n Realized P&L: +{s}\n", .{fmt.fmtMoneyAbs(&rpl_buf, rpl_abs)}); @as(u8, if (summary.realized_gain_loss >= 0) '+' else '-'),
} else { fmt.fmtMoneyAbs(&rpl_buf, rpl_abs),
try out.print("\n Realized P&L: -{s}\n", .{fmt.fmtMoneyAbs(&rpl_buf, rpl_abs)}); });
}
try cli.reset(out, color); try cli.reset(out, color);
} }

View file

@ -183,7 +183,7 @@ pub fn main() !u8 {
file_path = args[pi]; 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")) { } else if (std.mem.eql(u8, command, "lookup")) {
if (args.len < 3) { if (args.len < 3) {
try cli.stderrPrint("Error: 'lookup' requires a CUSIP argument\n"); try cli.stderrPrint("Error: 'lookup' requires a CUSIP argument\n");

View file

@ -34,6 +34,16 @@ const gl_col_start: usize = col_end_market_value;
// Data loading // 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 { pub fn loadPortfolioData(self: *App) void {
self.portfolio_loaded = true; self.portfolio_loaded = true;
self.freePortfolioSummary(); self.freePortfolioSummary();
@ -894,7 +904,7 @@ fn drawWelcomeScreen(self: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, wi
.{ .text = "", .style = th.contentStyle() }, .{ .text = "", .style = th.contentStyle() },
.{ .text = " Portfolio mode:", .style = th.contentStyle() }, .{ .text = " Portfolio mode:", .style = th.contentStyle() },
.{ .text = " zfin -p portfolio.srf Load a portfolio file", .style = th.mutedStyle() }, .{ .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 = "", .style = th.contentStyle() },
.{ .text = " Navigation:", .style = th.contentStyle() }, .{ .text = " Navigation:", .style = th.contentStyle() },
.{ .text = " h / l Previous / next tab", .style = th.mutedStyle() }, .{ .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 = " q Quit", .style = th.mutedStyle() },
.{ .text = "", .style = th.contentStyle() }, .{ .text = "", .style = th.contentStyle() },
.{ .text = " Sample portfolio.srf:", .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::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.dimStyle() }, .{ .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); try self.drawStyledContent(arena, buf, width, height, &welcome_lines);
} }