ai: portfolio refactor

This commit is contained in:
Emil Lerch 2026-02-27 14:20:33 -08:00
parent f7505936d2
commit 73b96f7399
Signed by: lobo
GPG key ID: A7B62D657EF764F8

View file

@ -205,6 +205,107 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
}
}
// Build candle map once for historical snapshots and risk metrics.
// This avoids parsing the full candle history multiple times.
var candle_map = std.StringHashMap([]const zfin.Candle).init(allocator);
defer {
var it = candle_map.valueIterator();
while (it.next()) |v| allocator.free(v.*);
candle_map.deinit();
}
{
const stock_syms = try portfolio.stockSymbols(allocator);
defer allocator.free(stock_syms);
for (stock_syms) |sym| {
if (svc.getCachedCandles(sym)) |cs| {
try candle_map.put(sym, cs);
}
}
}
// Collect watch symbols and their prices for display.
// Includes watch lots from portfolio + symbols from separate watchlist file.
var watch_list: std.ArrayList([]const u8) = .empty;
defer watch_list.deinit(allocator);
var watch_prices = std.StringHashMap(f64).init(allocator);
defer watch_prices.deinit();
{
var watch_seen = std.StringHashMap(void).init(allocator);
defer watch_seen.deinit();
// Exclude portfolio position symbols from watchlist
for (summary.allocations) |a| {
try watch_seen.put(a.symbol, {});
}
// Watch lots from portfolio
for (portfolio.lots) |lot| {
if (lot.lot_type == .watch) {
const sym = lot.priceSymbol();
if (watch_seen.contains(sym)) continue;
try watch_seen.put(sym, {});
try watch_list.append(allocator, sym);
if (svc.getCachedLastClose(sym)) |close| {
try watch_prices.put(sym, close);
}
}
}
// Separate watchlist file (backward compat)
if (watchlist_path) |wl_path| {
const wl_data = std.fs.cwd().readFileAlloc(allocator, wl_path, 1024 * 1024) catch null;
if (wl_data) |wd| {
defer allocator.free(wd);
var wl_lines = std.mem.splitScalar(u8, wd, '\n');
while (wl_lines.next()) |line| {
const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace);
if (trimmed.len == 0 or trimmed[0] == '#') continue;
if (std.mem.indexOf(u8, trimmed, "symbol::")) |idx| {
const rest = trimmed[idx + "symbol::".len ..];
const end = std.mem.indexOfScalar(u8, rest, ',') orelse rest.len;
const sym = std.mem.trim(u8, rest[0..end], &std.ascii.whitespace);
if (sym.len > 0 and sym.len <= 10) {
if (watch_seen.contains(sym)) continue;
try watch_seen.put(sym, {});
try watch_list.append(allocator, sym);
if (svc.getCachedLastClose(sym)) |close| {
try watch_prices.put(sym, close);
}
}
}
}
}
}
}
try display(
allocator,
out,
color,
file_path,
&portfolio,
positions,
&summary,
prices,
candle_map,
watch_list.items,
watch_prices,
);
}
/// Render the full portfolio display. All data is pre-fetched; no service calls.
pub fn display(
allocator: std.mem.Allocator,
out: *std.Io.Writer,
color: bool,
file_path: []const u8,
portfolio: *const zfin.Portfolio,
positions: []const zfin.Position,
summary: *const zfin.risk.PortfolioSummary,
prices: std.StringHashMap(f64),
candle_map: std.StringHashMap([]const zfin.Candle),
watch_symbols: []const []const u8,
watch_prices: std.StringHashMap(f64),
) !void {
// Header with summary
try cli.setBold(out, color);
try out.print("\nPortfolio Summary ({s})\n", .{file_path});
@ -239,24 +340,6 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
try out.print(" Lots: {d} open, {d} closed Positions: {d} symbols\n", .{ open_lots, closed_lots, positions.len });
try cli.reset(out, color);
// Build candle map once for historical snapshots and risk metrics.
// This avoids parsing the full candle history multiple times.
var candle_map = std.StringHashMap([]const zfin.Candle).init(allocator);
defer {
var it = candle_map.valueIterator();
while (it.next()) |v| allocator.free(v.*);
candle_map.deinit();
}
{
const stock_syms = try portfolio.stockSymbols(allocator);
defer allocator.free(stock_syms);
for (stock_syms) |sym| {
if (svc.getCachedCandles(sym)) |cs| {
try candle_map.put(sym, cs);
}
}
}
// Historical portfolio value snapshots
{
if (candle_map.count() > 0) {
@ -664,67 +747,19 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
try cli.reset(out, color);
}
// Watchlist (from watch lots in portfolio + separate watchlist file)
{
var any_watch = false;
var watch_seen = std.StringHashMap(void).init(allocator);
defer watch_seen.deinit();
// Mark portfolio position symbols as seen
for (summary.allocations) |a| {
try watch_seen.put(a.symbol, {});
}
// Helper to render a watch symbol
const renderWatch = struct {
fn f(o: *std.Io.Writer, c: bool, s: *zfin.DataService, a2: std.mem.Allocator, sym: []const u8, any: *bool) !void {
_ = a2;
if (!any.*) {
try o.print("\n", .{});
try cli.setBold(o, c);
try o.print(" Watchlist:\n", .{});
try cli.reset(o, c);
any.* = true;
}
var price_str2: [16]u8 = undefined;
var ps2: []const u8 = "--";
if (s.getCachedLastClose(sym)) |close| {
ps2 = fmt.fmtMoney2(&price_str2, close);
}
try o.print(" " ++ fmt.sym_col_spec ++ " {s:>10}\n", .{ sym, ps2 });
}
}.f;
// Watch lots from portfolio
for (portfolio.lots) |lot| {
if (lot.lot_type == .watch) {
if (watch_seen.contains(lot.priceSymbol())) continue;
try watch_seen.put(lot.priceSymbol(), {});
try renderWatch(out, color, svc, allocator, lot.priceSymbol(), &any_watch);
}
}
// Separate watchlist file (backward compat)
if (watchlist_path) |wl_path| {
const wl_data = std.fs.cwd().readFileAlloc(allocator, wl_path, 1024 * 1024) catch null;
if (wl_data) |wd| {
defer allocator.free(wd);
var wl_lines = std.mem.splitScalar(u8, wd, '\n');
while (wl_lines.next()) |line| {
const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace);
if (trimmed.len == 0 or trimmed[0] == '#') continue;
if (std.mem.indexOf(u8, trimmed, "symbol::")) |idx| {
const rest = trimmed[idx + "symbol::".len ..];
const end = std.mem.indexOfScalar(u8, rest, ',') orelse rest.len;
const sym = std.mem.trim(u8, rest[0..end], &std.ascii.whitespace);
if (sym.len > 0 and sym.len <= 10) {
if (watch_seen.contains(sym)) continue;
try watch_seen.put(sym, {});
try renderWatch(out, color, svc, allocator, sym, &any_watch);
}
}
}
}
// Watchlist
if (watch_symbols.len > 0) {
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print(" Watchlist:\n", .{});
try cli.reset(out, color);
for (watch_symbols) |sym| {
var price_str: [16]u8 = undefined;
const ps: []const u8 = if (watch_prices.get(sym)) |close|
fmt.fmtMoney2(&price_str, close)
else
"--";
try out.print(" " ++ fmt.sym_col_spec ++ " {s:>10}\n", .{ sym, ps });
}
}