diff --git a/src/analytics/risk.zig b/src/analytics/risk.zig index 4ec074e..abed49e 100644 --- a/src/analytics/risk.zig +++ b/src/analytics/risk.zig @@ -84,11 +84,11 @@ pub const PortfolioSummary = struct { /// Total cost basis of open positions total_cost: f64, /// Total unrealized P&L - unrealized_pnl: f64, + unrealized_gain_loss: f64, /// Total unrealized return (decimal) unrealized_return: f64, /// Total realized P&L from closed lots - realized_pnl: f64, + realized_gain_loss: f64, /// Per-symbol breakdown allocations: []Allocation, @@ -99,7 +99,7 @@ pub const PortfolioSummary = struct { /// Adjust the summary to include non-stock assets (cash, CDs, options) in the totals. /// Cash and CDs add equally to value and cost (no gain/loss). /// Options add at cost basis (no live pricing). - /// This keeps unrealized_pnl correct (only stocks contribute market gains) + /// This keeps unrealized_gain_loss correct (only stocks contribute market gains) /// but dilutes the return% against the full portfolio cost base. pub fn adjustForNonStockAssets(self: *PortfolioSummary, portfolio: @import("../models/portfolio.zig").Portfolio) void { const cash_total = portfolio.totalCash(); @@ -109,7 +109,7 @@ pub const PortfolioSummary = struct { self.total_value += non_stock; self.total_cost += non_stock; if (self.total_cost > 0) { - self.unrealized_return = self.unrealized_pnl / self.total_cost; + self.unrealized_return = self.unrealized_gain_loss / self.total_cost; } // Reweight allocations against grand total if (self.total_value > 0) { @@ -131,7 +131,7 @@ pub const Allocation = struct { market_value: f64, cost_basis: f64, weight: f64, // fraction of total portfolio - unrealized_pnl: f64, + unrealized_gain_loss: f64, unrealized_return: f64, /// True if current_price came from a manual override rather than live API data. is_manual_price: bool = false, @@ -161,7 +161,7 @@ pub fn portfolioSummary( const mv = pos.shares * price; total_value += mv; total_cost += pos.total_cost; - total_realized += pos.realized_pnl; + total_realized += pos.realized_gain_loss; // For CUSIPs with a note, derive a short display label from the note. const display = if (fmt.isCusipLike(pos.symbol) and pos.note != null) @@ -178,7 +178,7 @@ pub fn portfolioSummary( .market_value = mv, .cost_basis = pos.total_cost, .weight = 0, // filled below - .unrealized_pnl = mv - pos.total_cost, + .unrealized_gain_loss = mv - pos.total_cost, .unrealized_return = if (pos.total_cost > 0) (mv / pos.total_cost) - 1.0 else 0, .is_manual_price = if (manual_prices) |mp| mp.contains(pos.symbol) else false, .account = pos.account, @@ -195,9 +195,9 @@ pub fn portfolioSummary( return .{ .total_value = total_value, .total_cost = total_cost, - .unrealized_pnl = total_value - total_cost, + .unrealized_gain_loss = total_value - total_cost, .unrealized_return = if (total_cost > 0) (total_value / total_cost) - 1.0 else 0, - .realized_pnl = total_realized, + .realized_gain_loss = total_realized, .allocations = try allocs.toOwnedSlice(allocator), }; } @@ -564,22 +564,22 @@ test "adjustForNonStockAssets" { }; const pf = Portfolio{ .lots = &lots, .allocator = std.testing.allocator }; var allocs = [_]Allocation{ - .{ .symbol = "VTI", .display_symbol = "VTI", .shares = 10, .avg_cost = 200, .current_price = 220, .market_value = 2200, .cost_basis = 2000, .weight = 1.0, .unrealized_pnl = 200, .unrealized_return = 0.1 }, + .{ .symbol = "VTI", .display_symbol = "VTI", .shares = 10, .avg_cost = 200, .current_price = 220, .market_value = 2200, .cost_basis = 2000, .weight = 1.0, .unrealized_gain_loss = 200, .unrealized_return = 0.1 }, }; var summary = PortfolioSummary{ .total_value = 2200, .total_cost = 2000, - .unrealized_pnl = 200, + .unrealized_gain_loss = 200, .unrealized_return = 0.1, - .realized_pnl = 0, + .realized_gain_loss = 0, .allocations = &allocs, }; summary.adjustForNonStockAssets(pf); // non_stock = 5000 + 10000 + (2*5) = 15010 try std.testing.expectApproxEqAbs(@as(f64, 17210), summary.total_value, 0.01); try std.testing.expectApproxEqAbs(@as(f64, 17010), summary.total_cost, 0.01); - // unrealized_pnl unchanged (200), unrealized_return = 200 / 17010 - try std.testing.expectApproxEqAbs(@as(f64, 200), summary.unrealized_pnl, 0.01); + // unrealized_gain_loss unchanged (200), unrealized_return = 200 / 17010 + try std.testing.expectApproxEqAbs(@as(f64, 200), summary.unrealized_gain_loss, 0.01); try std.testing.expectApproxEqAbs(@as(f64, 200.0 / 17010.0), summary.unrealized_return, 0.001); // Weight recomputed against new total try std.testing.expectApproxEqAbs(@as(f64, 2200.0 / 17210.0), allocs[0].weight, 0.001); @@ -594,8 +594,8 @@ test "buildFallbackPrices" { .{ .symbol = "CUSIP1", .shares = 5, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 100, .price = 105.5 }, }; var positions = [_]Position{ - .{ .symbol = "AAPL", .shares = 10, .avg_cost = 150, .total_cost = 1500, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 }, - .{ .symbol = "CUSIP1", .shares = 5, .avg_cost = 100, .total_cost = 500, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 }, + .{ .symbol = "AAPL", .shares = 10, .avg_cost = 150, .total_cost = 1500, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0 }, + .{ .symbol = "CUSIP1", .shares = 5, .avg_cost = 100, .total_cost = 500, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0 }, }; var prices = std.StringHashMap(f64).init(alloc); defer prices.deinit(); diff --git a/src/commands/portfolio.zig b/src/commands/portfolio.zig index 351174c..3b9d671 100644 --- a/src/commands/portfolio.zig +++ b/src/commands/portfolio.zig @@ -233,10 +233,10 @@ pub fn display( var val_buf: [24]u8 = undefined; var cost_buf: [24]u8 = undefined; var gl_buf: [24]u8 = undefined; - const gl_abs = if (summary.unrealized_pnl >= 0) summary.unrealized_pnl else -summary.unrealized_pnl; + 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.fmtMoney(&val_buf, summary.total_value), fmt.fmtMoney(&cost_buf, summary.total_cost) }); - try cli.setGainLoss(out, color, summary.unrealized_pnl); - if (summary.unrealized_pnl >= 0) { + try cli.setGainLoss(out, color, summary.unrealized_gain_loss); + if (summary.unrealized_gain_loss >= 0) { try out.print("Gain/Loss: +{s} ({d:.1}%)", .{ fmt.fmtMoney(&gl_buf, gl_abs), summary.unrealized_return * 100.0 }); } else { try out.print("Gain/Loss: -{s} ({d:.1}%)", .{ fmt.fmtMoney(&gl_buf, gl_abs), summary.unrealized_return * 100.0 }); @@ -310,9 +310,9 @@ pub fn display( var cost_buf2: [24]u8 = undefined; var price_buf2: [24]u8 = undefined; var gl_val_buf: [24]u8 = undefined; - const gl_abs = if (a.unrealized_pnl >= 0) a.unrealized_pnl else -a.unrealized_pnl; + const gl_abs = if (a.unrealized_gain_loss >= 0) a.unrealized_gain_loss else -a.unrealized_gain_loss; const gl_money = fmt.fmtMoney(&gl_val_buf, gl_abs); - const sign: []const u8 = if (a.unrealized_pnl >= 0) "+" else "-"; + const sign: []const u8 = if (a.unrealized_gain_loss >= 0) "+" else "-"; // Date + ST/LT for single-lot positions var date_col: [24]u8 = .{' '} ** 24; @@ -332,7 +332,7 @@ pub fn display( }); try out.print("{s:>10}", .{fmt.fmtMoney2(&price_buf2, a.current_price)}); try out.print(" {s:>16} ", .{fmt.fmtMoney(&mv_buf, a.market_value)}); - try cli.setGainLoss(out, color, a.unrealized_pnl); + try cli.setGainLoss(out, color, a.unrealized_gain_loss); try out.print("{s}{s:>13}", .{ sign, gl_money }); if (a.is_manual_price) { try cli.setFg(out, color, cli.CLR_WARNING); @@ -421,12 +421,12 @@ pub fn display( { var total_mv_buf: [24]u8 = undefined; var total_gl_buf: [24]u8 = undefined; - const gl_abs = if (summary.unrealized_pnl >= 0) summary.unrealized_pnl else -summary.unrealized_pnl; + const gl_abs = if (summary.unrealized_gain_loss >= 0) summary.unrealized_gain_loss else -summary.unrealized_gain_loss; try out.print(" {s:>6} {s:>8} {s:>10} {s:>10} {s:>16} ", .{ "", "", "", "TOTAL", fmt.fmtMoney(&total_mv_buf, summary.total_value), }); - try cli.setGainLoss(out, color, summary.unrealized_pnl); - if (summary.unrealized_pnl >= 0) { + try cli.setGainLoss(out, color, summary.unrealized_gain_loss); + if (summary.unrealized_gain_loss >= 0) { try out.print("+{s:>13}", .{fmt.fmtMoney(&total_gl_buf, gl_abs)}); } else { try out.print("-{s:>13}", .{fmt.fmtMoney(&total_gl_buf, gl_abs)}); @@ -435,11 +435,11 @@ pub fn display( try out.print(" {s:>7}\n", .{"100.0%"}); } - if (summary.realized_pnl != 0) { + if (summary.realized_gain_loss != 0) { var rpl_buf: [24]u8 = undefined; - const rpl_abs = if (summary.realized_pnl >= 0) summary.realized_pnl else -summary.realized_pnl; - try cli.setGainLoss(out, color, summary.realized_pnl); - if (summary.realized_pnl >= 0) { + 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.fmtMoney(&rpl_buf, rpl_abs)}); } else { try out.print("\n Realized P&L: -{s}\n", .{fmt.fmtMoney(&rpl_buf, rpl_abs)}); @@ -729,18 +729,18 @@ fn testPortfolio(lots: []const zfin.Lot) zfin.Portfolio { fn testSummary(allocations: []zfin.risk.Allocation) zfin.risk.PortfolioSummary { var total_value: f64 = 0; var total_cost: f64 = 0; - var unrealized_pnl: f64 = 0; + var unrealized_gain_loss: f64 = 0; for (allocations) |a| { total_value += a.market_value; total_cost += a.cost_basis; - unrealized_pnl += a.unrealized_pnl; + unrealized_gain_loss += a.unrealized_gain_loss; } return .{ .total_value = total_value, .total_cost = total_cost, - .unrealized_pnl = unrealized_pnl, - .unrealized_return = if (total_cost > 0) unrealized_pnl / total_cost else 0, - .realized_pnl = 0, + .unrealized_gain_loss = unrealized_gain_loss, + .unrealized_return = if (total_cost > 0) unrealized_gain_loss / total_cost else 0, + .realized_gain_loss = 0, .allocations = allocations, }; } @@ -756,13 +756,13 @@ test "display shows header and summary" { var portfolio = testPortfolio(&lots); var positions = [_]zfin.Position{ - .{ .symbol = "AAPL", .shares = 10, .avg_cost = 150.0, .total_cost = 1500.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 }, - .{ .symbol = "GOOG", .shares = 5, .avg_cost = 120.0, .total_cost = 600.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 }, + .{ .symbol = "AAPL", .shares = 10, .avg_cost = 150.0, .total_cost = 1500.0, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0 }, + .{ .symbol = "GOOG", .shares = 5, .avg_cost = 120.0, .total_cost = 600.0, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0 }, }; var allocs = [_]zfin.risk.Allocation{ - .{ .symbol = "AAPL", .display_symbol = "AAPL", .shares = 10, .avg_cost = 150.0, .current_price = 175.0, .market_value = 1750.0, .cost_basis = 1500.0, .weight = 0.745, .unrealized_pnl = 250.0, .unrealized_return = 0.167 }, - .{ .symbol = "GOOG", .display_symbol = "GOOG", .shares = 5, .avg_cost = 120.0, .current_price = 140.0, .market_value = 700.0, .cost_basis = 600.0, .weight = 0.255, .unrealized_pnl = 100.0, .unrealized_return = 0.167 }, + .{ .symbol = "AAPL", .display_symbol = "AAPL", .shares = 10, .avg_cost = 150.0, .current_price = 175.0, .market_value = 1750.0, .cost_basis = 1500.0, .weight = 0.745, .unrealized_gain_loss = 250.0, .unrealized_return = 0.167 }, + .{ .symbol = "GOOG", .display_symbol = "GOOG", .shares = 5, .avg_cost = 120.0, .current_price = 140.0, .market_value = 700.0, .cost_basis = 600.0, .weight = 0.255, .unrealized_gain_loss = 100.0, .unrealized_return = 0.167 }, }; var summary = testSummary(&allocs); @@ -808,11 +808,11 @@ test "display with watchlist" { var portfolio = testPortfolio(&lots); var positions = [_]zfin.Position{ - .{ .symbol = "VTI", .shares = 20, .avg_cost = 200.0, .total_cost = 4000.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 }, + .{ .symbol = "VTI", .shares = 20, .avg_cost = 200.0, .total_cost = 4000.0, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0 }, }; var allocs = [_]zfin.risk.Allocation{ - .{ .symbol = "VTI", .display_symbol = "VTI", .shares = 20, .avg_cost = 200.0, .current_price = 220.0, .market_value = 4400.0, .cost_basis = 4000.0, .weight = 1.0, .unrealized_pnl = 400.0, .unrealized_return = 0.1 }, + .{ .symbol = "VTI", .display_symbol = "VTI", .shares = 20, .avg_cost = 200.0, .current_price = 220.0, .market_value = 4400.0, .cost_basis = 4000.0, .weight = 1.0, .unrealized_gain_loss = 400.0, .unrealized_return = 0.1 }, }; var summary = testSummary(&allocs); @@ -850,11 +850,11 @@ test "display with options section" { var portfolio = testPortfolio(&lots); var positions = [_]zfin.Position{ - .{ .symbol = "SPY", .shares = 50, .avg_cost = 400.0, .total_cost = 20000.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 }, + .{ .symbol = "SPY", .shares = 50, .avg_cost = 400.0, .total_cost = 20000.0, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0 }, }; var allocs = [_]zfin.risk.Allocation{ - .{ .symbol = "SPY", .display_symbol = "SPY", .shares = 50, .avg_cost = 400.0, .current_price = 450.0, .market_value = 22500.0, .cost_basis = 20000.0, .weight = 1.0, .unrealized_pnl = 2500.0, .unrealized_return = 0.125 }, + .{ .symbol = "SPY", .display_symbol = "SPY", .shares = 50, .avg_cost = 400.0, .current_price = 450.0, .market_value = 22500.0, .cost_basis = 20000.0, .weight = 1.0, .unrealized_gain_loss = 2500.0, .unrealized_return = 0.125 }, }; var summary = testSummary(&allocs); // Include option cost in totals (like run() does) @@ -892,11 +892,11 @@ test "display with CDs and cash" { var portfolio = testPortfolio(&lots); var positions = [_]zfin.Position{ - .{ .symbol = "VTI", .shares = 10, .avg_cost = 200.0, .total_cost = 2000.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 }, + .{ .symbol = "VTI", .shares = 10, .avg_cost = 200.0, .total_cost = 2000.0, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0 }, }; var allocs = [_]zfin.risk.Allocation{ - .{ .symbol = "VTI", .display_symbol = "VTI", .shares = 10, .avg_cost = 200.0, .current_price = 220.0, .market_value = 2200.0, .cost_basis = 2000.0, .weight = 1.0, .unrealized_pnl = 200.0, .unrealized_return = 0.1 }, + .{ .symbol = "VTI", .display_symbol = "VTI", .shares = 10, .avg_cost = 200.0, .current_price = 220.0, .market_value = 2200.0, .cost_basis = 2000.0, .weight = 1.0, .unrealized_gain_loss = 200.0, .unrealized_return = 0.1 }, }; var summary = testSummary(&allocs); summary.total_value += portfolio.totalCash() + portfolio.totalCdFaceValue(); @@ -936,14 +936,14 @@ test "display realized PnL shown when nonzero" { var portfolio = testPortfolio(&lots); var positions = [_]zfin.Position{ - .{ .symbol = "MSFT", .shares = 10, .avg_cost = 300.0, .total_cost = 3000.0, .open_lots = 1, .closed_lots = 1, .realized_pnl = 350.0 }, + .{ .symbol = "MSFT", .shares = 10, .avg_cost = 300.0, .total_cost = 3000.0, .open_lots = 1, .closed_lots = 1, .realized_gain_loss = 350.0 }, }; var allocs = [_]zfin.risk.Allocation{ - .{ .symbol = "MSFT", .display_symbol = "MSFT", .shares = 10, .avg_cost = 300.0, .current_price = 400.0, .market_value = 4000.0, .cost_basis = 3000.0, .weight = 1.0, .unrealized_pnl = 1000.0, .unrealized_return = 0.333 }, + .{ .symbol = "MSFT", .display_symbol = "MSFT", .shares = 10, .avg_cost = 300.0, .current_price = 400.0, .market_value = 4000.0, .cost_basis = 3000.0, .weight = 1.0, .unrealized_gain_loss = 1000.0, .unrealized_return = 0.333 }, }; var summary = testSummary(&allocs); - summary.realized_pnl = 350.0; + summary.realized_gain_loss = 350.0; var prices = std.StringHashMap(f64).init(testing.allocator); defer prices.deinit(); @@ -971,11 +971,11 @@ test "display empty watchlist not shown" { var portfolio = testPortfolio(&lots); var positions = [_]zfin.Position{ - .{ .symbol = "VTI", .shares = 10, .avg_cost = 200.0, .total_cost = 2000.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 }, + .{ .symbol = "VTI", .shares = 10, .avg_cost = 200.0, .total_cost = 2000.0, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0 }, }; var allocs = [_]zfin.risk.Allocation{ - .{ .symbol = "VTI", .display_symbol = "VTI", .shares = 10, .avg_cost = 200.0, .current_price = 220.0, .market_value = 2200.0, .cost_basis = 2000.0, .weight = 1.0, .unrealized_pnl = 200.0, .unrealized_return = 0.1 }, + .{ .symbol = "VTI", .display_symbol = "VTI", .shares = 10, .avg_cost = 200.0, .current_price = 220.0, .market_value = 2200.0, .cost_basis = 2000.0, .weight = 1.0, .unrealized_gain_loss = 200.0, .unrealized_return = 0.1 }, }; var summary = testSummary(&allocs); diff --git a/src/models/portfolio.zig b/src/models/portfolio.zig index b7fa155..a5abf2e 100644 --- a/src/models/portfolio.zig +++ b/src/models/portfolio.zig @@ -80,12 +80,15 @@ pub const Lot = struct { return self.shares * current_price; } - pub fn realizedPnl(self: Lot) ?f64 { + /// Realized gain/loss for a closed lot: shares * (close_price - open_price). + /// Returns null if the lot is still open. + pub fn realizedGainLoss(self: Lot) ?f64 { const cp = self.close_price orelse return null; return self.shares * (cp - self.open_price); } - pub fn unrealizedPnl(self: Lot, current_price: f64) f64 { + /// Unrealized gain/loss for an open lot at the given market price. + pub fn unrealizedGainLoss(self: Lot, current_price: f64) f64 { return self.shares * (current_price - self.open_price); } @@ -110,7 +113,7 @@ pub const Position = struct { /// Number of closed lots closed_lots: u32, /// Total realized P&L from closed lots - realized_pnl: f64, + realized_gain_loss: f64, /// Account name (shared across lots, or "Multiple" if mixed). account: []const u8 = "", /// Note from the first lot (e.g. "VANGUARD TARGET 2035"). @@ -222,7 +225,7 @@ pub const Portfolio = struct { .total_cost = 0, .open_lots = 0, .closed_lots = 0, - .realized_pnl = 0, + .realized_gain_loss = 0, .account = lot.account orelse "", .note = lot.note, }; @@ -240,7 +243,7 @@ pub const Portfolio = struct { entry.value_ptr.open_lots += 1; } else { entry.value_ptr.closed_lots += 1; - entry.value_ptr.realized_pnl += lot.realizedPnl() orelse 0; + entry.value_ptr.realized_gain_loss += lot.realizedGainLoss() orelse 0; } } @@ -272,11 +275,11 @@ pub const Portfolio = struct { } /// Total realized P&L from all closed stock lots. - pub fn totalRealizedPnl(self: Portfolio) f64 { + pub fn totalRealizedGainLoss(self: Portfolio) f64 { var total: f64 = 0; for (self.lots) |lot| { if (lot.lot_type == .stock) { - if (lot.realizedPnl()) |pnl| total += pnl; + if (lot.realizedGainLoss()) |pnl| total += pnl; } } return total; @@ -353,8 +356,8 @@ test "lot basics" { try std.testing.expect(lot.isOpen()); try std.testing.expectApproxEqAbs(@as(f64, 1500.0), lot.costBasis(), 0.01); try std.testing.expectApproxEqAbs(@as(f64, 2000.0), lot.marketValue(200.0), 0.01); - try std.testing.expectApproxEqAbs(@as(f64, 500.0), lot.unrealizedPnl(200.0), 0.01); - try std.testing.expect(lot.realizedPnl() == null); + try std.testing.expectApproxEqAbs(@as(f64, 500.0), lot.unrealizedGainLoss(200.0), 0.01); + try std.testing.expect(lot.realizedGainLoss() == null); } test "closed lot" { @@ -367,7 +370,7 @@ test "closed lot" { .close_price = 200.0, }; try std.testing.expect(!lot.isOpen()); - try std.testing.expectApproxEqAbs(@as(f64, 500.0), lot.realizedPnl().?, 0.01); + try std.testing.expectApproxEqAbs(@as(f64, 500.0), lot.realizedGainLoss().?, 0.01); try std.testing.expectApproxEqAbs(@as(f64, 0.3333), lot.returnPct(0), 0.001); } @@ -398,7 +401,7 @@ test "portfolio positions" { try std.testing.expectApproxEqAbs(@as(f64, 15.0), aapl.?.shares, 0.01); try std.testing.expectEqual(@as(u32, 2), aapl.?.open_lots); try std.testing.expectEqual(@as(u32, 1), aapl.?.closed_lots); - try std.testing.expectApproxEqAbs(@as(f64, 75.0), aapl.?.realized_pnl, 0.01); // 3 * (155-130) + try std.testing.expectApproxEqAbs(@as(f64, 75.0), aapl.?.realized_gain_loss, 0.01); // 3 * (155-130) } test "LotType label and fromString" { @@ -451,8 +454,8 @@ test "Portfolio totals" { // totalCostBasis: only open stock lots -> 10 * 150 = 1500 try std.testing.expectApproxEqAbs(@as(f64, 1500.0), portfolio.totalCostBasis(), 0.01); - // totalRealizedPnl: closed stock lots -> 5 * (160-140) = 100 - try std.testing.expectApproxEqAbs(@as(f64, 100.0), portfolio.totalRealizedPnl(), 0.01); + // totalRealizedGainLoss: closed stock lots -> 5 * (160-140) = 100 + try std.testing.expectApproxEqAbs(@as(f64, 100.0), portfolio.totalRealizedGainLoss(), 0.01); // totalCash try std.testing.expectApproxEqAbs(@as(f64, 50000.0), portfolio.totalCash(), 0.01); // totalIlliquid diff --git a/src/tui.zig b/src/tui.zig index 47a6e86..ae59351 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -1244,7 +1244,7 @@ const App = struct { .avg_cost => lhs.avg_cost < rhs.avg_cost, .price => lhs.current_price < rhs.current_price, .market_value => lhs.market_value < rhs.market_value, - .gain_loss => lhs.unrealized_pnl < rhs.unrealized_pnl, + .gain_loss => lhs.unrealized_gain_loss < rhs.unrealized_gain_loss, .weight => lhs.weight < rhs.weight, .account => std.mem.lessThan(u8, lhs.account, rhs.account), }; @@ -2036,12 +2036,12 @@ const App = struct { var gl_buf: [24]u8 = undefined; const val_str = fmt.fmtMoney(&val_buf, s.total_value); const cost_str = fmt.fmtMoney(&cost_buf, s.total_cost); - const gl_abs = if (s.unrealized_pnl >= 0) s.unrealized_pnl else -s.unrealized_pnl; + const gl_abs = if (s.unrealized_gain_loss >= 0) s.unrealized_gain_loss else -s.unrealized_gain_loss; const gl_str = fmt.fmtMoney(&gl_buf, gl_abs); const summary_text = try std.fmt.allocPrint(arena, " Value: {s} Cost: {s} Gain/Loss: {s}{s} ({d:.1}%)", .{ - val_str, cost_str, if (s.unrealized_pnl >= 0) @as([]const u8, "+") else @as([]const u8, "-"), gl_str, s.unrealized_return * 100.0, + val_str, cost_str, if (s.unrealized_gain_loss >= 0) @as([]const u8, "+") else @as([]const u8, "-"), gl_str, s.unrealized_return * 100.0, }); - const summary_style = if (s.unrealized_pnl >= 0) th.positiveStyle() else th.negativeStyle(); + const summary_style = if (s.unrealized_gain_loss >= 0) th.positiveStyle() else th.negativeStyle(); try lines.append(arena, .{ .text = summary_text, .style = summary_style }); // "as of" date indicator @@ -2137,12 +2137,12 @@ const App = struct { const is_expanded = is_multi and row.pos_idx < self.expanded.len and self.expanded[row.pos_idx]; const arrow: []const u8 = if (!is_multi) " " else if (is_expanded) "v " else "> "; const star: []const u8 = if (is_active_sym) "* " else " "; - const pnl_pct = if (a.cost_basis > 0) (a.unrealized_pnl / a.cost_basis) * 100.0 else @as(f64, 0); + const pnl_pct = if (a.cost_basis > 0) (a.unrealized_gain_loss / a.cost_basis) * 100.0 else @as(f64, 0); var gl_val_buf: [24]u8 = undefined; - const gl_abs = if (a.unrealized_pnl >= 0) a.unrealized_pnl else -a.unrealized_pnl; + const gl_abs = if (a.unrealized_gain_loss >= 0) a.unrealized_gain_loss else -a.unrealized_gain_loss; const gl_money = fmt.fmtMoney(&gl_val_buf, gl_abs); var pnl_buf: [20]u8 = undefined; - const pnl_str = if (a.unrealized_pnl >= 0) + const pnl_str = if (a.unrealized_gain_loss >= 0) std.fmt.bufPrint(&pnl_buf, "+{s}", .{gl_money}) catch "?" else std.fmt.bufPrint(&pnl_buf, "-{s}", .{gl_money}) catch "?";