centralize portfolio value
This commit is contained in:
parent
518af59717
commit
3171be6f70
3 changed files with 128 additions and 40 deletions
|
|
@ -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, .{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue