fix portfolio account totals/honor as_of date
This commit is contained in:
parent
e68372036d
commit
efb05801cb
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 ClassificationMap = @import("../models/classification.zig").ClassificationMap;
|
||||||
const LotType = @import("../models/portfolio.zig").LotType;
|
const LotType = @import("../models/portfolio.zig").LotType;
|
||||||
const Portfolio = @import("../models/portfolio.zig").Portfolio;
|
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%)
|
/// A single slice of a breakdown (e.g., "Technology" -> 25.3%)
|
||||||
pub const BreakdownItem = struct {
|
pub const BreakdownItem = struct {
|
||||||
|
|
@ -157,6 +158,11 @@ pub const AnalysisResult = struct {
|
||||||
/// `classifications` is the metadata file data.
|
/// `classifications` is the metadata file data.
|
||||||
/// `portfolio` is the full portfolio (for cash/CD/illiquid totals).
|
/// `portfolio` is the full portfolio (for cash/CD/illiquid totals).
|
||||||
/// `account_map` is optional account tax type metadata.
|
/// `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(
|
pub fn analyzePortfolio(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
allocations: []const Allocation,
|
allocations: []const Allocation,
|
||||||
|
|
@ -164,6 +170,7 @@ pub fn analyzePortfolio(
|
||||||
portfolio: Portfolio,
|
portfolio: Portfolio,
|
||||||
total_portfolio_value: f64,
|
total_portfolio_value: f64,
|
||||||
account_map: ?AccountMap,
|
account_map: ?AccountMap,
|
||||||
|
as_of: ?Date,
|
||||||
) !AnalysisResult {
|
) !AnalysisResult {
|
||||||
// Accumulators: label -> dollar amount
|
// Accumulators: label -> dollar amount
|
||||||
var ac_map = std.StringHashMap(f64).init(allocator);
|
var ac_map = std.StringHashMap(f64).init(allocator);
|
||||||
|
|
@ -222,9 +229,13 @@ pub fn analyzePortfolio(
|
||||||
price_lookup.put(alloc.symbol, alloc.current_price) catch {};
|
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| {
|
for (portfolio.lots) |lot| {
|
||||||
if (!lot.isOpen()) continue;
|
if (!lot.lotIsOpenAsOf(reference_date)) continue;
|
||||||
const acct = lot.account orelse continue;
|
const acct = lot.account orelse continue;
|
||||||
const value: f64 = switch (lot.security_type) {
|
const value: f64 = switch (lot.security_type) {
|
||||||
.stock => blk: {
|
.stock => blk: {
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, file_path: []co
|
||||||
portfolio,
|
portfolio,
|
||||||
pf_data.summary.total_value,
|
pf_data.summary.total_value,
|
||||||
acct_map_opt,
|
acct_map_opt,
|
||||||
|
null, // null => use wall-clock today (interactive, not backfill)
|
||||||
) catch {
|
) catch {
|
||||||
try cli.stderrPrint("Error computing analysis.\n");
|
try cli.stderrPrint("Error computing analysis.\n");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -625,18 +625,13 @@ fn buildSnapshot(
|
||||||
// fails we still emit the snapshot with empty tax_type/account
|
// fails we still emit the snapshot with empty tax_type/account
|
||||||
// sections rather than failing the whole capture.
|
// sections rather than failing the whole capture.
|
||||||
//
|
//
|
||||||
// Known limitation: `runAnalysis` uses `isOpen()` (wall-clock today)
|
// `analyzePortfolio` is passed `as_of` so per-account/tax-type
|
||||||
// for its per-account/tax-type roll-ups rather than
|
// roll-ups filter lots via `lotIsOpenAsOf(as_of)` — matches the
|
||||||
// `lotIsOpenAsOf(as_of)`. For backfill dates where lots have
|
// backfill semantics used for the headline totals.
|
||||||
// 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.
|
|
||||||
var tax_types: []TaxTypeRow = &.{};
|
var tax_types: []TaxTypeRow = &.{};
|
||||||
var accounts: []AccountRow = &.{};
|
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;
|
var a = result;
|
||||||
defer a.deinit(allocator);
|
defer a.deinit(allocator);
|
||||||
|
|
||||||
|
|
@ -774,6 +769,7 @@ fn runAnalysis(
|
||||||
portfolio_path: []const u8,
|
portfolio_path: []const u8,
|
||||||
svc: *zfin.DataService,
|
svc: *zfin.DataService,
|
||||||
summary: zfin.valuation.PortfolioSummary,
|
summary: zfin.valuation.PortfolioSummary,
|
||||||
|
as_of: Date,
|
||||||
) !zfin.analysis.AnalysisResult {
|
) !zfin.analysis.AnalysisResult {
|
||||||
const dir_end = if (std.mem.lastIndexOfScalar(u8, portfolio_path, std.fs.path.sep)) |idx| idx + 1 else 0;
|
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]});
|
const meta_path = try std.fmt.allocPrint(allocator, "{s}metadata.srf", .{portfolio_path[0..dir_end]});
|
||||||
|
|
@ -795,6 +791,7 @@ fn runAnalysis(
|
||||||
portfolio.*,
|
portfolio.*,
|
||||||
summary.total_value,
|
summary.total_value,
|
||||||
acct_map_opt,
|
acct_map_opt,
|
||||||
|
as_of,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ fn loadDataFinish(app: *App, pf: zfin.Portfolio, summary: zfin.valuation.Portfol
|
||||||
pf,
|
pf,
|
||||||
summary.total_value,
|
summary.total_value,
|
||||||
app.account_map,
|
app.account_map,
|
||||||
|
null, // null => use wall-clock today (interactive, not backfill)
|
||||||
) catch {
|
) catch {
|
||||||
app.setStatus("Error computing analysis");
|
app.setStatus("Error computing analysis");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue