ai: portfolio refactor
This commit is contained in:
parent
f7505936d2
commit
73b96f7399
1 changed files with 114 additions and 79 deletions
|
|
@ -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
|
// Header with summary
|
||||||
try cli.setBold(out, color);
|
try cli.setBold(out, color);
|
||||||
try out.print("\nPortfolio Summary ({s})\n", .{file_path});
|
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 out.print(" Lots: {d} open, {d} closed Positions: {d} symbols\n", .{ open_lots, closed_lots, positions.len });
|
||||||
try cli.reset(out, color);
|
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
|
// Historical portfolio value snapshots
|
||||||
{
|
{
|
||||||
if (candle_map.count() > 0) {
|
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);
|
try cli.reset(out, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watchlist (from watch lots in portfolio + separate watchlist file)
|
// Watchlist
|
||||||
{
|
if (watch_symbols.len > 0) {
|
||||||
var any_watch = false;
|
try out.print("\n", .{});
|
||||||
var watch_seen = std.StringHashMap(void).init(allocator);
|
try cli.setBold(out, color);
|
||||||
defer watch_seen.deinit();
|
try out.print(" Watchlist:\n", .{});
|
||||||
|
try cli.reset(out, color);
|
||||||
// Mark portfolio position symbols as seen
|
for (watch_symbols) |sym| {
|
||||||
for (summary.allocations) |a| {
|
var price_str: [16]u8 = undefined;
|
||||||
try watch_seen.put(a.symbol, {});
|
const ps: []const u8 = if (watch_prices.get(sym)) |close|
|
||||||
}
|
fmt.fmtMoney2(&price_str, close)
|
||||||
|
else
|
||||||
// Helper to render a watch symbol
|
"--";
|
||||||
const renderWatch = struct {
|
try out.print(" " ++ fmt.sym_col_spec ++ " {s:>10}\n", .{ sym, ps });
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue