diff --git a/src/models/portfolio.zig b/src/models/portfolio.zig index 47298d8..c2f734c 100644 --- a/src/models/portfolio.zig +++ b/src/models/portfolio.zig @@ -711,3 +711,98 @@ test "positionsForAccount excludes closed-only symbols" { try std.testing.expectEqualStrings("XLV", pos_b[0].symbol); try std.testing.expectApproxEqAbs(@as(f64, 50.0), pos_b[0].shares, 0.01); } + +test "isOpen respects maturity_date" { + const past = Date.fromYmd(2024, 1, 1); + const future = Date.fromYmd(2099, 12, 31); + + const expired_option = Lot{ + .symbol = "AAPL 01/01/2024 150 C", + .shares = -1, + .open_date = Date.fromYmd(2023, 6, 1), + .open_price = 5.0, + .security_type = .option, + .maturity_date = past, + }; + try std.testing.expect(!expired_option.isOpen()); + + const active_option = Lot{ + .symbol = "AAPL 12/31/2099 150 C", + .shares = -1, + .open_date = Date.fromYmd(2023, 6, 1), + .open_price = 5.0, + .security_type = .option, + .maturity_date = future, + }; + try std.testing.expect(active_option.isOpen()); + + const closed_option = Lot{ + .symbol = "AAPL 12/31/2099 150 C", + .shares = -1, + .open_date = Date.fromYmd(2023, 6, 1), + .open_price = 5.0, + .security_type = .option, + .maturity_date = future, + .close_date = Date.fromYmd(2024, 6, 1), + }; + try std.testing.expect(!closed_option.isOpen()); + + const stock = Lot{ + .symbol = "AAPL", + .shares = 100, + .open_date = Date.fromYmd(2023, 1, 1), + .open_price = 150.0, + }; + try std.testing.expect(stock.isOpen()); +} + +test "nonStockValueForAccount" { + const allocator = std.testing.allocator; + const future = Date.fromYmd(2099, 12, 31); + const past = Date.fromYmd(2024, 1, 1); + + var lots = [_]Lot{ + .{ .symbol = "AAPL", .shares = 100, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 150.0, .account = "IRA" }, + .{ .symbol = "", .shares = 5000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 1.0, .security_type = .cash, .account = "IRA" }, + .{ .symbol = "CD123", .shares = 50000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 1.0, .security_type = .cd, .account = "IRA", .maturity_date = future }, + .{ .symbol = "AAPL 12/31/2099 200 C", .shares = -2, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 3.50, .security_type = .option, .account = "IRA", .maturity_date = future, .multiplier = 100 }, + .{ .symbol = "AAPL 01/01/2024 180 C", .shares = -1, .open_date = Date.fromYmd(2023, 6, 1), .open_price = 4.0, .security_type = .option, .account = "IRA", .maturity_date = past, .multiplier = 100 }, + .{ .symbol = "", .shares = 1000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 1.0, .security_type = .cash, .account = "Other" }, + }; + + const portfolio = Portfolio{ .lots = &lots, .allocator = allocator }; + + // cash(5000) + cd(50000) + open option(2*3.50*100=700) = 55700 + // expired option excluded + const ns = portfolio.nonStockValueForAccount("IRA"); + try std.testing.expectApproxEqAbs(@as(f64, 55700.0), ns, 0.01); + + const ns_other = portfolio.nonStockValueForAccount("Other"); + try std.testing.expectApproxEqAbs(@as(f64, 1000.0), ns_other, 0.01); +} + +test "totalForAccount" { + const allocator = std.testing.allocator; + const future = Date.fromYmd(2099, 12, 31); + + var lots = [_]Lot{ + .{ .symbol = "AAPL", .shares = 100, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 150.0, .account = "IRA" }, + .{ .symbol = "MSFT", .shares = 50, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 300.0, .account = "IRA" }, + .{ .symbol = "", .shares = 2000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 1.0, .security_type = .cash, .account = "IRA" }, + .{ .symbol = "CD456", .shares = 10000, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 1.0, .security_type = .cd, .account = "IRA", .maturity_date = future }, + .{ .symbol = "AAPL C", .shares = -1, .open_date = Date.fromYmd(2024, 1, 1), .open_price = 5.0, .security_type = .option, .account = "IRA", .maturity_date = future, .multiplier = 100 }, + }; + + const portfolio = Portfolio{ .lots = &lots, .allocator = allocator }; + + var prices = std.StringHashMap(f64).init(allocator); + defer prices.deinit(); + try prices.put("AAPL", 170.0); + // MSFT not in prices — should fall back to avg_cost (300.0) + + // stocks: AAPL(100*170=17000) + MSFT(50*300=15000) = 32000 + // non-stock: cash(2000) + cd(10000) + option(1*5*100=500) = 12500 + // total = 44500 + const total = portfolio.totalForAccount(allocator, "IRA", prices); + try std.testing.expectApproxEqAbs(@as(f64, 44500.0), total, 0.01); +}