reuse common account filtering in portfolio
This commit is contained in:
parent
f55af318f2
commit
9337c198f4
2 changed files with 63 additions and 79 deletions
|
|
@ -309,6 +309,7 @@ pub const App = struct {
|
|||
|
||||
// Account filter state
|
||||
account_filter: ?[]const u8 = null, // active account filter (owned copy; null = all accounts)
|
||||
filtered_positions: ?[]zfin.Position = null, // positions for filtered account (from positionsForAccount)
|
||||
account_list: std.ArrayList([]const u8) = .empty, // distinct accounts from portfolio lots (borrowed from portfolio)
|
||||
account_picker_cursor: usize = 0, // cursor position in picker (0 = "All accounts")
|
||||
|
||||
|
|
@ -684,10 +685,15 @@ pub const App = struct {
|
|||
|
||||
/// Set or clear the account filter. Owns the string via allocator.
|
||||
pub fn setAccountFilter(self: *App, name: ?[]const u8) void {
|
||||
// Free the old owned copy
|
||||
if (self.account_filter) |old| self.allocator.free(old);
|
||||
if (self.filtered_positions) |fp| self.allocator.free(fp);
|
||||
self.filtered_positions = null;
|
||||
|
||||
if (name) |n| {
|
||||
self.account_filter = self.allocator.dupe(u8, n) catch null;
|
||||
if (self.portfolio) |pf| {
|
||||
self.filtered_positions = pf.positionsForAccount(self.allocator, n) catch null;
|
||||
}
|
||||
} else {
|
||||
self.account_filter = null;
|
||||
}
|
||||
|
|
@ -1347,6 +1353,7 @@ pub const App = struct {
|
|||
self.options_rows.deinit(self.allocator);
|
||||
self.account_list.deinit(self.allocator);
|
||||
if (self.account_filter) |af| self.allocator.free(af);
|
||||
if (self.filtered_positions) |fp| self.allocator.free(fp);
|
||||
if (self.watchlist_prices) |*wp| wp.deinit();
|
||||
if (self.analysis_result) |*ar| ar.deinit(self.allocator);
|
||||
if (self.classification_map) |*cm| cm.deinit();
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ pub fn loadPortfolioData(app: *App) void {
|
|||
|
||||
sortPortfolioAllocations(app);
|
||||
buildAccountList(app);
|
||||
recomputeFilteredPositions(app);
|
||||
rebuildPortfolioRows(app);
|
||||
|
||||
const summary = pf_data.summary;
|
||||
|
|
@ -288,10 +289,14 @@ pub fn rebuildPortfolioRows(app: *App) void {
|
|||
|
||||
// Count lots for this symbol (filtered by account when filter is active)
|
||||
var lcount: usize = 0;
|
||||
if (app.portfolio) |pf| {
|
||||
for (pf.lots) |lot| {
|
||||
if (lot.security_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
||||
if (matchesAccountFilter(app, lot.account)) lcount += 1;
|
||||
if (findFilteredPosition(app, a.symbol)) |pos| {
|
||||
lcount = pos.open_lots + pos.closed_lots;
|
||||
} else if (app.account_filter == null) {
|
||||
if (app.portfolio) |pf| {
|
||||
for (pf.lots) |lot| {
|
||||
if (lot.security_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
||||
lcount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -583,6 +588,26 @@ pub fn buildAccountList(app: *App) void {
|
|||
}
|
||||
}
|
||||
|
||||
/// Recompute filtered_positions when portfolio or account filter changes.
|
||||
fn recomputeFilteredPositions(app: *App) void {
|
||||
if (app.filtered_positions) |fp| app.allocator.free(fp);
|
||||
app.filtered_positions = null;
|
||||
const filter = app.account_filter orelse return;
|
||||
const pf = app.portfolio orelse return;
|
||||
app.filtered_positions = pf.positionsForAccount(app.allocator, filter) catch null;
|
||||
}
|
||||
|
||||
/// Look up a symbol in the pre-computed filtered positions.
|
||||
/// Returns null if no filter is active or the symbol isn't in the filtered account.
|
||||
fn findFilteredPosition(app: *const App, symbol: []const u8) ?zfin.Position {
|
||||
const fps = app.filtered_positions orelse return null;
|
||||
for (fps) |pos| {
|
||||
if (std.mem.eql(u8, pos.symbol, symbol) or std.mem.eql(u8, pos.lot_symbol, symbol))
|
||||
return pos;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Check if a lot matches the active account filter.
|
||||
/// Returns true if no filter is active or the lot's account matches.
|
||||
fn matchesAccountFilter(app: *const App, account: ?[]const u8) bool {
|
||||
|
|
@ -592,25 +617,11 @@ fn matchesAccountFilter(app: *const App, account: ?[]const u8) bool {
|
|||
}
|
||||
|
||||
/// Check if an allocation matches the active account filter.
|
||||
/// Uses the allocation's account field (which is "Multiple" for mixed-account positions).
|
||||
/// For "Multiple" accounts, we need to check if any lot with this symbol belongs to the filtered account.
|
||||
/// When filtered, checks against pre-computed filtered_positions.
|
||||
fn allocationMatchesFilter(app: *const App, a: zfin.valuation.Allocation) bool {
|
||||
const filter = app.account_filter orelse return true;
|
||||
// Simple case: allocation has a single account
|
||||
if (!std.mem.eql(u8, a.account, "Multiple")) {
|
||||
return std.mem.eql(u8, a.account, filter);
|
||||
}
|
||||
// "Multiple" account: check if any stock lot for this symbol belongs to the filtered account
|
||||
if (app.portfolio) |pf| {
|
||||
for (pf.lots) |lot| {
|
||||
if (lot.security_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
||||
if (lot.account) |la| {
|
||||
if (std.mem.eql(u8, la, filter)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
if (app.account_filter == null) return true;
|
||||
if (app.filtered_positions == null) return false;
|
||||
return findFilteredPosition(app, a.symbol) != null;
|
||||
}
|
||||
|
||||
/// Account-filtered view of an allocation. When a position spans multiple accounts,
|
||||
|
|
@ -624,41 +635,24 @@ const FilteredAlloc = struct {
|
|||
|
||||
/// Compute account-filtered values for an allocation.
|
||||
/// For single-account positions (or no filter), returns the allocation's own values.
|
||||
/// For "Multiple"-account positions with a filter, sums only the matching lots.
|
||||
/// For filtered views, uses pre-computed filtered_positions.
|
||||
fn filteredAllocValues(app: *const App, a: zfin.valuation.Allocation) FilteredAlloc {
|
||||
const filter = app.account_filter orelse return .{
|
||||
if (app.account_filter == null) return .{
|
||||
.shares = a.shares,
|
||||
.cost_basis = a.cost_basis,
|
||||
.market_value = a.market_value,
|
||||
.unrealized_gain_loss = a.unrealized_gain_loss,
|
||||
};
|
||||
if (!std.mem.eql(u8, a.account, "Multiple")) return .{
|
||||
.shares = a.shares,
|
||||
.cost_basis = a.cost_basis,
|
||||
.market_value = a.market_value,
|
||||
.unrealized_gain_loss = a.unrealized_gain_loss,
|
||||
};
|
||||
// Sum values from only the lots matching the filter
|
||||
var shares: f64 = 0;
|
||||
var cost: f64 = 0;
|
||||
if (app.portfolio) |pf| {
|
||||
for (pf.lots) |lot| {
|
||||
if (lot.security_type != .stock) continue;
|
||||
if (!std.mem.eql(u8, lot.priceSymbol(), a.symbol)) continue;
|
||||
if (!lot.isOpen()) continue;
|
||||
const la = lot.account orelse "";
|
||||
if (!std.mem.eql(u8, la, filter)) continue;
|
||||
shares += lot.shares;
|
||||
cost += lot.shares * lot.open_price;
|
||||
}
|
||||
if (findFilteredPosition(app, a.symbol)) |pos| {
|
||||
const mv = pos.shares * a.current_price * pos.price_ratio;
|
||||
return .{
|
||||
.shares = pos.shares,
|
||||
.cost_basis = pos.total_cost,
|
||||
.market_value = mv,
|
||||
.unrealized_gain_loss = mv - pos.total_cost,
|
||||
};
|
||||
}
|
||||
const mv = shares * a.current_price * a.price_ratio;
|
||||
return .{
|
||||
.shares = shares,
|
||||
.cost_basis = cost,
|
||||
.market_value = mv,
|
||||
.unrealized_gain_loss = mv - cost,
|
||||
};
|
||||
return .{ .shares = 0, .cost_basis = 0, .market_value = 0, .unrealized_gain_loss = 0 };
|
||||
}
|
||||
|
||||
/// Totals for the filtered account view (stocks + cash + CDs + options).
|
||||
|
|
@ -896,38 +890,20 @@ pub fn drawContent(app: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, width
|
|||
if (app.portfolio) |pf| {
|
||||
for (pf.lots) |lot| {
|
||||
if (lot.security_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
||||
const ds = lot.open_date.format(&pos_date_buf);
|
||||
const indicator = fmt.capitalGainsIndicator(lot.open_date);
|
||||
date_col = std.fmt.allocPrint(arena, "{s} {s}", .{ ds, indicator }) catch ds;
|
||||
acct_col = lot.account orelse "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Multi-lot: show account if all lots share the same one
|
||||
if (app.portfolio) |pf| {
|
||||
var common_acct: ?[]const u8 = null;
|
||||
var mixed = false;
|
||||
for (pf.lots) |lot| {
|
||||
if (lot.security_type == .stock and std.mem.eql(u8, lot.priceSymbol(), a.symbol)) {
|
||||
if (common_acct) |ca| {
|
||||
const la = lot.account orelse "";
|
||||
if (!std.mem.eql(u8, ca, la)) {
|
||||
mixed = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
common_acct = lot.account orelse "";
|
||||
if (matchesAccountFilter(app, lot.account)) {
|
||||
const ds = lot.open_date.format(&pos_date_buf);
|
||||
const indicator = fmt.capitalGainsIndicator(lot.open_date);
|
||||
date_col = std.fmt.allocPrint(arena, "{s} {s}", .{ ds, indicator }) catch ds;
|
||||
acct_col = lot.account orelse "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!mixed) {
|
||||
acct_col = common_acct orelse "";
|
||||
} else {
|
||||
acct_col = "Multiple";
|
||||
}
|
||||
}
|
||||
} else if (app.account_filter) |af| {
|
||||
acct_col = af;
|
||||
} else {
|
||||
acct_col = a.account;
|
||||
}
|
||||
|
||||
const display_weight = if (app.account_filter != null and filtered_total_for_weight > 0)
|
||||
|
|
@ -1254,6 +1230,7 @@ pub fn reloadPortfolioFile(app: *App) void {
|
|||
|
||||
sortPortfolioAllocations(app);
|
||||
buildAccountList(app);
|
||||
recomputeFilteredPositions(app);
|
||||
rebuildPortfolioRows(app);
|
||||
|
||||
// Invalidate analysis data -- it holds pointers into old portfolio memory
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue