fix portfolio account totals/honor as_of date
This commit is contained in:
parent
58c20a4de9
commit
9364532c9b
4 changed files with 21 additions and 11 deletions
|
|
@ -9,6 +9,7 @@ const ClassificationEntry = @import("../models/classification.zig").Classificati
|
|||
const ClassificationMap = @import("../models/classification.zig").ClassificationMap;
|
||||
const LotType = @import("../models/portfolio.zig").LotType;
|
||||
const Portfolio = @import("../models/portfolio.zig").Portfolio;
|
||||
const Date = @import("../models/date.zig").Date;
|
||||
|
||||
/// A single slice of a breakdown (e.g., "Technology" -> 25.3%)
|
||||
pub const BreakdownItem = struct {
|
||||
|
|
@ -157,6 +158,11 @@ pub const AnalysisResult = struct {
|
|||
/// `classifications` is the metadata file data.
|
||||
/// `portfolio` is the full portfolio (for cash/CD/illiquid totals).
|
||||
/// `account_map` is optional account tax type metadata.
|
||||
/// `as_of` is the date against which lot open/closed status is
|
||||
/// evaluated. Pass `null` to use wall-clock today (the default for
|
||||
/// interactive commands); historical snapshot backfill passes the
|
||||
/// target date so lots opened/closed/matured between `as_of` and today
|
||||
/// are counted correctly.
|
||||
pub fn analyzePortfolio(
|
||||
allocator: std.mem.Allocator,
|
||||
allocations: []const Allocation,
|
||||
|
|
@ -164,6 +170,7 @@ pub fn analyzePortfolio(
|
|||
portfolio: Portfolio,
|
||||
total_portfolio_value: f64,
|
||||
account_map: ?AccountMap,
|
||||
as_of: ?Date,
|
||||
) !AnalysisResult {
|
||||
// Accumulators: label -> dollar amount
|
||||
var ac_map = std.StringHashMap(f64).init(allocator);
|
||||
|
|
@ -222,9 +229,13 @@ pub fn analyzePortfolio(
|
|||
price_lookup.put(alloc.symbol, alloc.current_price) catch {};
|
||||
}
|
||||
|
||||
// Account breakdown from individual lots (avoids "Multiple" aggregation issue)
|
||||
// Account breakdown from individual lots (avoids "Multiple" aggregation issue).
|
||||
// Use `lotIsOpenAsOf(as_of)` when provided so backfilled snapshots
|
||||
// correctly include/exclude lots based on the target date rather
|
||||
// than wall-clock today. `isOpen()` = `lotIsOpenAsOf(today)`.
|
||||
const reference_date = as_of orelse Date.fromEpoch(std.time.timestamp());
|
||||
for (portfolio.lots) |lot| {
|
||||
if (!lot.isOpen()) continue;
|
||||
if (!lot.lotIsOpenAsOf(reference_date)) continue;
|
||||
const acct = lot.account orelse continue;
|
||||
const value: f64 = switch (lot.security_type) {
|
||||
.stock => blk: {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, file_path: []co
|
|||
portfolio,
|
||||
pf_data.summary.total_value,
|
||||
acct_map_opt,
|
||||
null, // null => use wall-clock today (interactive, not backfill)
|
||||
) catch {
|
||||
try cli.stderrPrint("Error computing analysis.\n");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -625,18 +625,13 @@ fn buildSnapshot(
|
|||
// fails we still emit the snapshot with empty tax_type/account
|
||||
// sections rather than failing the whole capture.
|
||||
//
|
||||
// Known limitation: `runAnalysis` uses `isOpen()` (wall-clock today)
|
||||
// for its per-account/tax-type roll-ups rather than
|
||||
// `lotIsOpenAsOf(as_of)`. For backfill dates where lots have
|
||||
// matured/closed/opened between `as_of` and today, the per-account
|
||||
// totals can be off by the affected lots. The headline
|
||||
// net_worth/liquid/illiquid totals are NOT affected — those flow
|
||||
// through `positionsAsOf(as_of)` which honors the target date.
|
||||
// Fixing analysis to be as_of-aware is tracked separately.
|
||||
// `analyzePortfolio` is passed `as_of` so per-account/tax-type
|
||||
// roll-ups filter lots via `lotIsOpenAsOf(as_of)` — matches the
|
||||
// backfill semantics used for the headline totals.
|
||||
var tax_types: []TaxTypeRow = &.{};
|
||||
var accounts: []AccountRow = &.{};
|
||||
|
||||
if (runAnalysis(allocator, portfolio, portfolio_path, svc, summary)) |result| {
|
||||
if (runAnalysis(allocator, portfolio, portfolio_path, svc, summary, as_of)) |result| {
|
||||
var a = result;
|
||||
defer a.deinit(allocator);
|
||||
|
||||
|
|
@ -774,6 +769,7 @@ fn runAnalysis(
|
|||
portfolio_path: []const u8,
|
||||
svc: *zfin.DataService,
|
||||
summary: zfin.valuation.PortfolioSummary,
|
||||
as_of: Date,
|
||||
) !zfin.analysis.AnalysisResult {
|
||||
const dir_end = if (std.mem.lastIndexOfScalar(u8, portfolio_path, std.fs.path.sep)) |idx| idx + 1 else 0;
|
||||
const meta_path = try std.fmt.allocPrint(allocator, "{s}metadata.srf", .{portfolio_path[0..dir_end]});
|
||||
|
|
@ -795,6 +791,7 @@ fn runAnalysis(
|
|||
portfolio.*,
|
||||
summary.total_value,
|
||||
acct_map_opt,
|
||||
as_of,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ fn loadDataFinish(app: *App, pf: zfin.Portfolio, summary: zfin.valuation.Portfol
|
|||
pf,
|
||||
summary.total_value,
|
||||
app.account_map,
|
||||
null, // null => use wall-clock today (interactive, not backfill)
|
||||
) catch {
|
||||
app.setStatus("Error computing analysis");
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue