91 lines
4.2 KiB
Zig
91 lines
4.2 KiB
Zig
//! Fidelity reconciler for the `audit` command.
|
|
//!
|
|
//! Fidelity exports a single "all accounts" positions CSV. Parsing
|
|
//! lives in `brokerage/fidelity.zig`; this module wires that parser
|
|
//! into the shared per-account comparison engine in `common.zig`.
|
|
//! Because the Fidelity export is a plain per-account positions list,
|
|
//! the only Fidelity-specific knowledge here is "use the Fidelity CSV
|
|
//! parser" and "the institution key is `fidelity`" — everything else
|
|
//! (comparison, display) is shared.
|
|
|
|
const std = @import("std");
|
|
const zfin = @import("../../root.zig");
|
|
const analysis = @import("../../analytics/analysis.zig");
|
|
const Date = @import("../../Date.zig");
|
|
const common = @import("common.zig");
|
|
const fidelity_parser = @import("../../brokerage/fidelity.zig");
|
|
|
|
/// Parse a Fidelity positions CSV and reconcile it against the
|
|
/// portfolio. Returns owned `AccountComparison` results (free each
|
|
/// `.comparisons` slice, then the results slice). String fields in
|
|
/// the results borrow from `csv_data`, which must outlive them.
|
|
///
|
|
/// Propagates the parser's errors (`EmptyFile` / `UnexpectedHeader`)
|
|
/// and allocation failures so the caller can decide between a
|
|
/// user-facing message (explicit `--fidelity`) and silently skipping
|
|
/// the file (flagless auto-reconcile).
|
|
pub fn reconcile(
|
|
allocator: std.mem.Allocator,
|
|
portfolio: zfin.Portfolio,
|
|
csv_data: []const u8,
|
|
account_map: analysis.AccountMap,
|
|
prices: std.StringHashMap(f64),
|
|
as_of: Date,
|
|
) ![]common.AccountComparison {
|
|
const positions = try fidelity_parser.parseCsv(allocator, csv_data);
|
|
// The result strings borrow from `csv_data`, not from this slice,
|
|
// so freeing the slice array here is safe.
|
|
defer allocator.free(positions);
|
|
return common.compareAccounts(allocator, portfolio, positions, account_map, "fidelity", prices, as_of);
|
|
}
|
|
|
|
// ── Tests ────────────────────────────────────────────────────
|
|
|
|
const portfolio_mod = @import("../../models/portfolio.zig");
|
|
|
|
test "reconcile: parses Fidelity CSV and matches against portfolio" {
|
|
const allocator = std.testing.allocator;
|
|
|
|
const csv =
|
|
"Account Number,Account Name,Symbol,Description,Quantity,Last Price,Last Price Change,Current Value,Today's Gain/Loss Dollar,Today's Gain/Loss Percent,Total Gain/Loss Dollar,Total Gain/Loss Percent,Percent Of Account,Cost Basis Total,Average Cost Basis,Type\n" ++
|
|
"Z123,Individual,AAPL,APPLE INC,100,$150.00,+$2.00,$15000.00,+$200.00,+1.35%,+$5000.00,+50.00%,100%,$10000.00,$100.00,Margin,\n";
|
|
|
|
var lots = [_]portfolio_mod.Lot{
|
|
.{ .symbol = "AAPL", .shares = 100, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 100.0, .account = "Sample Brokerage" },
|
|
};
|
|
const portfolio = portfolio_mod.Portfolio{ .lots = &lots, .allocator = allocator };
|
|
|
|
var entries = [_]analysis.AccountTaxEntry{
|
|
.{ .account = "Sample Brokerage", .tax_type = .taxable, .institution = "fidelity", .account_number = "Z123" },
|
|
};
|
|
const acct_map = analysis.AccountMap{ .entries = &entries, .allocator = allocator };
|
|
|
|
var prices = std.StringHashMap(f64).init(allocator);
|
|
defer prices.deinit();
|
|
try prices.put("AAPL", 150.0);
|
|
|
|
const results = try reconcile(allocator, portfolio, csv, acct_map, prices, Date.fromYmd(2026, 5, 8));
|
|
defer {
|
|
for (results) |r| allocator.free(r.comparisons);
|
|
allocator.free(results);
|
|
}
|
|
|
|
try std.testing.expectEqual(@as(usize, 1), results.len);
|
|
try std.testing.expectEqualStrings("Sample Brokerage", results[0].account_name);
|
|
// 100 shares @ $150 on both sides → no discrepancy.
|
|
try std.testing.expect(!results[0].has_discrepancies);
|
|
}
|
|
|
|
test "reconcile: propagates parser errors" {
|
|
const allocator = std.testing.allocator;
|
|
const portfolio = portfolio_mod.Portfolio{ .lots = &.{}, .allocator = allocator };
|
|
var entries = [_]analysis.AccountTaxEntry{};
|
|
const acct_map = analysis.AccountMap{ .entries = &entries, .allocator = allocator };
|
|
var prices = std.StringHashMap(f64).init(allocator);
|
|
defer prices.deinit();
|
|
|
|
try std.testing.expectError(
|
|
error.EmptyFile,
|
|
reconcile(allocator, portfolio, "", acct_map, prices, Date.fromYmd(2026, 5, 8)),
|
|
);
|
|
}
|