update pnl -> gain_loss
This commit is contained in:
parent
25a7d06d40
commit
9819c93cfe
4 changed files with 72 additions and 69 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
14
src/tui.zig
14
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 "?";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue