centralize portfolio value

This commit is contained in:
Emil Lerch 2026-04-11 10:17:29 -07:00
parent 518af59717
commit 3171be6f70
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 128 additions and 40 deletions

View file

@ -487,25 +487,7 @@ pub fn compareSchwabSummary(
if (portfolio_acct) |pa| { if (portfolio_acct) |pa| {
pf_cash = portfolio.cashForAccount(pa); pf_cash = portfolio.cashForAccount(pa);
pf_total = portfolio.totalForAccount(allocator, pa, prices);
const acct_positions = portfolio.positionsForAccount(allocator, pa) catch &.{};
defer allocator.free(acct_positions);
for (acct_positions) |pos| {
const price = prices.get(pos.symbol) orelse pos.avg_cost;
pf_total += pos.shares * price * pos.price_ratio;
}
// Add cash, CDs, options for this account
for (portfolio.lots) |lot| {
const lot_acct = lot.account orelse continue;
if (!std.mem.eql(u8, lot_acct, pa)) continue;
switch (lot.security_type) {
.cash => pf_total += lot.shares,
.cd => pf_total += lot.shares,
.option => pf_total += @abs(lot.shares) * lot.open_price * lot.multiplier,
else => {},
}
}
} }
const cash_delta = if (sa.cash) |sc| sc - pf_cash else null; const cash_delta = if (sa.cash) |sc| sc - pf_cash else null;
@ -791,6 +773,7 @@ pub fn compareAccounts(
const acct_positions = portfolio.positionsForAccount(allocator, portfolio_acct_name.?) catch &.{}; const acct_positions = portfolio.positionsForAccount(allocator, portfolio_acct_name.?) catch &.{};
defer allocator.free(acct_positions); defer allocator.free(acct_positions);
var found_stock = false;
for (acct_positions) |pos| { for (acct_positions) |pos| {
if (!std.mem.eql(u8, pos.symbol, bp.symbol) and if (!std.mem.eql(u8, pos.symbol, bp.symbol) and
!std.mem.eql(u8, pos.lot_symbol, bp.symbol)) !std.mem.eql(u8, pos.lot_symbol, bp.symbol))
@ -801,6 +784,30 @@ pub fn compareAccounts(
pf_value = pos.shares * price * pos.price_ratio; pf_value = pos.shares * price * pos.price_ratio;
try matched_symbols.put(pos.symbol, {}); try matched_symbols.put(pos.symbol, {});
try matched_symbols.put(pos.lot_symbol, {}); try matched_symbols.put(pos.lot_symbol, {});
found_stock = true;
}
if (!found_stock) {
for (portfolio.lots) |lot| {
const lot_acct = lot.account orelse continue;
if (!std.mem.eql(u8, lot_acct, portfolio_acct_name.?)) continue;
if (!lot.isOpen()) continue;
if (!std.mem.eql(u8, lot.symbol, bp.symbol)) continue;
switch (lot.security_type) {
.cd => {
pf_shares += lot.shares;
pf_value += lot.shares;
pf_price = 1.0;
},
.option => {
pf_shares += lot.shares;
pf_value += @abs(lot.shares) * lot.open_price * lot.multiplier;
pf_price = lot.open_price * lot.multiplier;
},
else => {},
}
}
if (pf_shares != 0) try matched_symbols.put(bp.symbol, {});
} }
} }
@ -864,6 +871,63 @@ pub fn compareAccounts(
.only_in_portfolio = true, .only_in_portfolio = true,
}); });
} }
// Portfolio-only CDs and options
for (portfolio.lots) |lot| {
const lot_acct = lot.account orelse continue;
if (!std.mem.eql(u8, lot_acct, pa)) continue;
if (!lot.isOpen()) continue;
if (lot.security_type != .cd and lot.security_type != .option) continue;
if (matched_symbols.contains(lot.symbol)) continue;
try matched_symbols.put(lot.symbol, {});
var pf_shares: f64 = 0;
var pf_value: f64 = 0;
var pf_price: ?f64 = null;
var is_cd = false;
// Aggregate all lots with same symbol in this account
for (portfolio.lots) |lot2| {
const la2 = lot2.account orelse continue;
if (!std.mem.eql(u8, la2, pa)) continue;
if (!lot2.isOpen()) continue;
if (!std.mem.eql(u8, lot2.symbol, lot.symbol)) continue;
switch (lot2.security_type) {
.cd => {
pf_shares += lot2.shares;
pf_value += lot2.shares;
pf_price = 1.0;
is_cd = true;
},
.option => {
pf_shares += lot2.shares;
pf_value += @abs(lot2.shares) * lot2.open_price * lot2.multiplier;
pf_price = lot2.open_price * lot2.multiplier;
},
else => {},
}
}
if (pf_value != 0 or pf_shares != 0) {
portfolio_total += pf_value;
has_discrepancies = true;
try comparisons.append(allocator, .{
.symbol = lot.symbol,
.portfolio_shares = pf_shares,
.brokerage_shares = null,
.portfolio_price = pf_price,
.brokerage_price = null,
.portfolio_value = pf_value,
.brokerage_value = null,
.shares_delta = null,
.value_delta = null,
.is_cash = is_cd,
.only_in_brokerage = false,
.only_in_portfolio = true,
});
}
}
} }
try results.append(allocator, .{ try results.append(allocator, .{

View file

@ -93,7 +93,12 @@ pub const Lot = struct {
} }
pub fn isOpen(self: Lot) bool { pub fn isOpen(self: Lot) bool {
return self.close_date == null; if (self.close_date != null) return false;
if (self.maturity_date) |mat| {
const today = Date.fromEpoch(std.time.timestamp());
if (!today.lessThan(mat)) return false;
}
return true;
} }
pub fn costBasis(self: Lot) f64 { pub fn costBasis(self: Lot) f64 {
@ -378,6 +383,41 @@ pub const Portfolio = struct {
return total; return total;
} }
/// Total value of non-stock holdings (cash, CDs, options) for a single account.
/// Only includes open lots (respects close_date and maturity_date).
pub fn nonStockValueForAccount(self: Portfolio, account_name: []const u8) f64 {
var total: f64 = 0;
for (self.lots) |lot| {
if (!lot.isOpen()) continue;
const lot_acct = lot.account orelse continue;
if (!std.mem.eql(u8, lot_acct, account_name)) continue;
switch (lot.security_type) {
.cash => total += lot.shares,
.cd => total += lot.shares,
.option => total += @abs(lot.shares) * lot.open_price * lot.multiplier,
else => {},
}
}
return total;
}
/// Total value of an account: stocks (priced from the given map, falling back to avg_cost)
/// plus cash, CDs, and options. Only includes open lots.
pub fn totalForAccount(self: Portfolio, allocator: std.mem.Allocator, account_name: []const u8, prices: std.StringHashMap(f64)) f64 {
var total: f64 = 0;
const acct_positions = self.positionsForAccount(allocator, account_name) catch return self.nonStockValueForAccount(account_name);
defer allocator.free(acct_positions);
for (acct_positions) |pos| {
const price = prices.get(pos.symbol) orelse pos.avg_cost;
total += pos.shares * price * pos.price_ratio;
}
total += self.nonStockValueForAccount(account_name);
return total;
}
/// Total cost basis of all open stock lots. /// Total cost basis of all open stock lots.
pub fn totalCostBasis(self: Portfolio) f64 { pub fn totalCostBasis(self: Portfolio) f64 {
var total: f64 = 0; var total: f64 = 0;

View file

@ -719,7 +719,7 @@ const FilteredTotals = struct {
/// Compute total value and cost across all asset types for the active account filter. /// Compute total value and cost across all asset types for the active account filter.
/// Returns {0, 0} if no filter is active. /// Returns {0, 0} if no filter is active.
fn computeFilteredTotals(app: *const App) FilteredTotals { fn computeFilteredTotals(app: *const App) FilteredTotals {
if (app.account_filter == null) return .{ .value = 0, .cost = 0 }; const af = app.account_filter orelse return .{ .value = 0, .cost = 0 };
var value: f64 = 0; var value: f64 = 0;
var cost: f64 = 0; var cost: f64 = 0;
if (app.portfolio_summary) |s| { if (app.portfolio_summary) |s| {
@ -732,25 +732,9 @@ fn computeFilteredTotals(app: *const App) FilteredTotals {
} }
} }
if (app.portfolio) |pf| { if (app.portfolio) |pf| {
for (pf.lots) |lot| { const ns = pf.nonStockValueForAccount(af);
if (!matchesAccountFilter(app, lot.account)) continue; value += ns;
switch (lot.security_type) { cost += ns;
.cash => {
value += lot.shares;
cost += lot.shares;
},
.cd => {
value += lot.shares;
cost += lot.shares;
},
.option => {
const opt_cost = @abs(lot.shares) * lot.open_price;
value += opt_cost;
cost += opt_cost;
},
else => {},
}
}
} }
return .{ .value = value, .cost = cost }; return .{ .value = value, .cost = cost };
} }