ai: add tests to portfolio
This commit is contained in:
parent
73b96f7399
commit
24924460d2
1 changed files with 284 additions and 0 deletions
|
|
@ -833,3 +833,287 @@ pub fn printLotRow(out: *std.Io.Writer, color: bool, lot: zfin.Lot, current_pric
|
|||
try out.print(" {s:>8} {s} {s} {s}\n", .{ "", date_str, indicator, acct_col });
|
||||
try cli.reset(out, color);
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
/// Helper: build a minimal portfolio for testing.
|
||||
/// Returns lots as a stack-allocated array and a Portfolio that references them.
|
||||
/// Caller must NOT call deinit() since lots are stack-allocated.
|
||||
fn testPortfolio(lots: []const zfin.Lot) zfin.Portfolio {
|
||||
return .{
|
||||
.lots = @constCast(lots),
|
||||
.allocator = testing.allocator,
|
||||
};
|
||||
}
|
||||
|
||||
fn testSummary(allocations: []zfin.risk.Allocation) zfin.risk.PortfolioSummary {
|
||||
var total_value: f64 = 0;
|
||||
var total_cost: f64 = 0;
|
||||
var unrealized_pnl: f64 = 0;
|
||||
for (allocations) |a| {
|
||||
total_value += a.market_value;
|
||||
total_cost += a.cost_basis;
|
||||
unrealized_pnl += a.unrealized_pnl;
|
||||
}
|
||||
return .{
|
||||
.total_value = total_value,
|
||||
.total_cost = total_cost,
|
||||
.unrealized_pnl = unrealized_pnl,
|
||||
.unrealized_return = if (total_cost > 0) unrealized_pnl / total_cost else 0,
|
||||
.realized_pnl = 0,
|
||||
.allocations = allocations,
|
||||
};
|
||||
}
|
||||
|
||||
test "display shows header and summary" {
|
||||
var buf: [8192]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
var lots = [_]zfin.Lot{
|
||||
.{ .symbol = "AAPL", .shares = 10, .open_date = zfin.Date.fromYmd(2023, 1, 15), .open_price = 150.0 },
|
||||
.{ .symbol = "GOOG", .shares = 5, .open_date = zfin.Date.fromYmd(2023, 6, 1), .open_price = 120.0 },
|
||||
};
|
||||
var portfolio = testPortfolio(&lots);
|
||||
|
||||
var positions = [_]zfin.Position{
|
||||
.{ .symbol = "AAPL", .shares = 10, .avg_cost = 150.0, .total_cost = 1500.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 },
|
||||
.{ .symbol = "GOOG", .shares = 5, .avg_cost = 120.0, .total_cost = 600.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 },
|
||||
};
|
||||
|
||||
var allocs = [_]zfin.risk.Allocation{
|
||||
.{ .symbol = "AAPL", .display_symbol = "AAPL", .shares = 10, .avg_cost = 150.0, .current_price = 175.0, .market_value = 1750.0, .cost_basis = 1500.0, .weight = 0.745, .unrealized_pnl = 250.0, .unrealized_return = 0.167 },
|
||||
.{ .symbol = "GOOG", .display_symbol = "GOOG", .shares = 5, .avg_cost = 120.0, .current_price = 140.0, .market_value = 700.0, .cost_basis = 600.0, .weight = 0.255, .unrealized_pnl = 100.0, .unrealized_return = 0.167 },
|
||||
};
|
||||
var summary = testSummary(&allocs);
|
||||
|
||||
var prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer prices.deinit();
|
||||
try prices.put("AAPL", 175.0);
|
||||
try prices.put("GOOG", 140.0);
|
||||
|
||||
var candle_map = std.StringHashMap([]const zfin.Candle).init(testing.allocator);
|
||||
defer candle_map.deinit();
|
||||
|
||||
var watch_prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer watch_prices.deinit();
|
||||
|
||||
const watch_syms: []const []const u8 = &.{};
|
||||
|
||||
try display(testing.allocator, &w, false, "test.srf", &portfolio, &positions, &summary, prices, candle_map, watch_syms, watch_prices);
|
||||
const out = w.buffered();
|
||||
|
||||
// Header present
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Portfolio Summary (test.srf)") != null);
|
||||
// Symbols present
|
||||
try testing.expect(std.mem.indexOf(u8, out, "AAPL") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "GOOG") != null);
|
||||
// Column headers present
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Symbol") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Market Value") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Gain/Loss") != null);
|
||||
// TOTAL line present
|
||||
try testing.expect(std.mem.indexOf(u8, out, "TOTAL") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "100.0%") != null);
|
||||
// No ANSI codes
|
||||
try testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
|
||||
}
|
||||
|
||||
test "display with watchlist" {
|
||||
var buf: [8192]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
var lots = [_]zfin.Lot{
|
||||
.{ .symbol = "VTI", .shares = 20, .open_date = zfin.Date.fromYmd(2022, 3, 1), .open_price = 200.0 },
|
||||
};
|
||||
var portfolio = testPortfolio(&lots);
|
||||
|
||||
var positions = [_]zfin.Position{
|
||||
.{ .symbol = "VTI", .shares = 20, .avg_cost = 200.0, .total_cost = 4000.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 },
|
||||
};
|
||||
|
||||
var allocs = [_]zfin.risk.Allocation{
|
||||
.{ .symbol = "VTI", .display_symbol = "VTI", .shares = 20, .avg_cost = 200.0, .current_price = 220.0, .market_value = 4400.0, .cost_basis = 4000.0, .weight = 1.0, .unrealized_pnl = 400.0, .unrealized_return = 0.1 },
|
||||
};
|
||||
var summary = testSummary(&allocs);
|
||||
|
||||
var prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer prices.deinit();
|
||||
try prices.put("VTI", 220.0);
|
||||
|
||||
var candle_map = std.StringHashMap([]const zfin.Candle).init(testing.allocator);
|
||||
defer candle_map.deinit();
|
||||
|
||||
// Watchlist with prices
|
||||
const watch_syms: []const []const u8 = &.{ "TSLA", "NVDA" };
|
||||
var watch_prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer watch_prices.deinit();
|
||||
try watch_prices.put("TSLA", 250.50);
|
||||
try watch_prices.put("NVDA", 800.25);
|
||||
|
||||
try display(testing.allocator, &w, false, "test.srf", &portfolio, &positions, &summary, prices, candle_map, watch_syms, watch_prices);
|
||||
const out = w.buffered();
|
||||
|
||||
// Watchlist header and symbols
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Watchlist:") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "TSLA") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "NVDA") != null);
|
||||
}
|
||||
|
||||
test "display with options section" {
|
||||
var buf: [8192]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
var lots = [_]zfin.Lot{
|
||||
.{ .symbol = "SPY", .shares = 50, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 400.0 },
|
||||
.{ .symbol = "SPY 240119C00450000", .shares = 2, .open_date = zfin.Date.fromYmd(2023, 6, 1), .open_price = 5.50, .lot_type = .option },
|
||||
};
|
||||
var portfolio = testPortfolio(&lots);
|
||||
|
||||
var positions = [_]zfin.Position{
|
||||
.{ .symbol = "SPY", .shares = 50, .avg_cost = 400.0, .total_cost = 20000.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 },
|
||||
};
|
||||
|
||||
var allocs = [_]zfin.risk.Allocation{
|
||||
.{ .symbol = "SPY", .display_symbol = "SPY", .shares = 50, .avg_cost = 400.0, .current_price = 450.0, .market_value = 22500.0, .cost_basis = 20000.0, .weight = 1.0, .unrealized_pnl = 2500.0, .unrealized_return = 0.125 },
|
||||
};
|
||||
var summary = testSummary(&allocs);
|
||||
// Include option cost in totals (like run() does)
|
||||
summary.total_value += portfolio.totalOptionCost();
|
||||
summary.total_cost += portfolio.totalOptionCost();
|
||||
|
||||
var prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer prices.deinit();
|
||||
try prices.put("SPY", 450.0);
|
||||
|
||||
var candle_map = std.StringHashMap([]const zfin.Candle).init(testing.allocator);
|
||||
defer candle_map.deinit();
|
||||
var watch_prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer watch_prices.deinit();
|
||||
const watch_syms: []const []const u8 = &.{};
|
||||
|
||||
try display(testing.allocator, &w, false, "test.srf", &portfolio, &positions, &summary, prices, candle_map, watch_syms, watch_prices);
|
||||
const out = w.buffered();
|
||||
|
||||
// Options section present
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Options") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "SPY 240119C00450000") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Cost/Ctrct") != null);
|
||||
}
|
||||
|
||||
test "display with CDs and cash" {
|
||||
var buf: [8192]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
var lots = [_]zfin.Lot{
|
||||
.{ .symbol = "VTI", .shares = 10, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 200.0 },
|
||||
.{ .symbol = "912828ZT0", .shares = 10000, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 100.0, .lot_type = .cd, .rate = 4.5, .maturity_date = zfin.Date.fromYmd(2025, 6, 15) },
|
||||
.{ .symbol = "CASH", .shares = 5000, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 0, .lot_type = .cash, .account = "Brokerage" },
|
||||
};
|
||||
var portfolio = testPortfolio(&lots);
|
||||
|
||||
var positions = [_]zfin.Position{
|
||||
.{ .symbol = "VTI", .shares = 10, .avg_cost = 200.0, .total_cost = 2000.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 },
|
||||
};
|
||||
|
||||
var allocs = [_]zfin.risk.Allocation{
|
||||
.{ .symbol = "VTI", .display_symbol = "VTI", .shares = 10, .avg_cost = 200.0, .current_price = 220.0, .market_value = 2200.0, .cost_basis = 2000.0, .weight = 1.0, .unrealized_pnl = 200.0, .unrealized_return = 0.1 },
|
||||
};
|
||||
var summary = testSummary(&allocs);
|
||||
summary.total_value += portfolio.totalCash() + portfolio.totalCdFaceValue();
|
||||
summary.total_cost += portfolio.totalCash() + portfolio.totalCdFaceValue();
|
||||
|
||||
var prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer prices.deinit();
|
||||
try prices.put("VTI", 220.0);
|
||||
|
||||
var candle_map = std.StringHashMap([]const zfin.Candle).init(testing.allocator);
|
||||
defer candle_map.deinit();
|
||||
var watch_prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer watch_prices.deinit();
|
||||
const watch_syms: []const []const u8 = &.{};
|
||||
|
||||
try display(testing.allocator, &w, false, "test.srf", &portfolio, &positions, &summary, prices, candle_map, watch_syms, watch_prices);
|
||||
const out = w.buffered();
|
||||
|
||||
// CDs section present
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Certificates of Deposit") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "912828ZT0") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "4.50%") != null);
|
||||
|
||||
// Cash section present
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Cash") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Brokerage") != null);
|
||||
}
|
||||
|
||||
test "display realized PnL shown when nonzero" {
|
||||
var buf: [8192]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
var lots = [_]zfin.Lot{
|
||||
.{ .symbol = "MSFT", .shares = 10, .open_date = zfin.Date.fromYmd(2022, 1, 1), .open_price = 300.0 },
|
||||
.{ .symbol = "MSFT", .shares = 5, .open_date = zfin.Date.fromYmd(2022, 6, 1), .open_price = 280.0, .close_date = zfin.Date.fromYmd(2023, 6, 1), .close_price = 350.0 },
|
||||
};
|
||||
var portfolio = testPortfolio(&lots);
|
||||
|
||||
var positions = [_]zfin.Position{
|
||||
.{ .symbol = "MSFT", .shares = 10, .avg_cost = 300.0, .total_cost = 3000.0, .open_lots = 1, .closed_lots = 1, .realized_pnl = 350.0 },
|
||||
};
|
||||
|
||||
var allocs = [_]zfin.risk.Allocation{
|
||||
.{ .symbol = "MSFT", .display_symbol = "MSFT", .shares = 10, .avg_cost = 300.0, .current_price = 400.0, .market_value = 4000.0, .cost_basis = 3000.0, .weight = 1.0, .unrealized_pnl = 1000.0, .unrealized_return = 0.333 },
|
||||
};
|
||||
var summary = testSummary(&allocs);
|
||||
summary.realized_pnl = 350.0;
|
||||
|
||||
var prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer prices.deinit();
|
||||
try prices.put("MSFT", 400.0);
|
||||
|
||||
var candle_map = std.StringHashMap([]const zfin.Candle).init(testing.allocator);
|
||||
defer candle_map.deinit();
|
||||
var watch_prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer watch_prices.deinit();
|
||||
const watch_syms: []const []const u8 = &.{};
|
||||
|
||||
try display(testing.allocator, &w, false, "test.srf", &portfolio, &positions, &summary, prices, candle_map, watch_syms, watch_prices);
|
||||
const out = w.buffered();
|
||||
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Realized P&L") != null);
|
||||
}
|
||||
|
||||
test "display empty watchlist not shown" {
|
||||
var buf: [8192]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
var lots = [_]zfin.Lot{
|
||||
.{ .symbol = "VTI", .shares = 10, .open_date = zfin.Date.fromYmd(2023, 1, 1), .open_price = 200.0 },
|
||||
};
|
||||
var portfolio = testPortfolio(&lots);
|
||||
|
||||
var positions = [_]zfin.Position{
|
||||
.{ .symbol = "VTI", .shares = 10, .avg_cost = 200.0, .total_cost = 2000.0, .open_lots = 1, .closed_lots = 0, .realized_pnl = 0 },
|
||||
};
|
||||
|
||||
var allocs = [_]zfin.risk.Allocation{
|
||||
.{ .symbol = "VTI", .display_symbol = "VTI", .shares = 10, .avg_cost = 200.0, .current_price = 220.0, .market_value = 2200.0, .cost_basis = 2000.0, .weight = 1.0, .unrealized_pnl = 200.0, .unrealized_return = 0.1 },
|
||||
};
|
||||
var summary = testSummary(&allocs);
|
||||
|
||||
var prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer prices.deinit();
|
||||
try prices.put("VTI", 220.0);
|
||||
|
||||
var candle_map = std.StringHashMap([]const zfin.Candle).init(testing.allocator);
|
||||
defer candle_map.deinit();
|
||||
var watch_prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer watch_prices.deinit();
|
||||
const watch_syms: []const []const u8 = &.{};
|
||||
|
||||
try display(testing.allocator, &w, false, "test.srf", &portfolio, &positions, &summary, prices, candle_map, watch_syms, watch_prices);
|
||||
const out = w.buffered();
|
||||
|
||||
// Watchlist header should NOT appear when there are no watch symbols
|
||||
try testing.expect(std.mem.indexOf(u8, out, "Watchlist") == null);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue