ai: portfolio sorting

This commit is contained in:
Emil Lerch 2026-02-26 14:10:19 -08:00
parent 635e0931f9
commit bbb11c29e1
Signed by: lobo
GPG key ID: A7B62D657EF764F8
6 changed files with 371 additions and 35 deletions

View file

@ -108,6 +108,8 @@ pub const Allocation = struct {
unrealized_return: f64,
/// True if current_price came from a manual override rather than live API data.
is_manual_price: bool = false,
/// Account name (from lots; "Multiple" if lots span different accounts).
account: []const u8 = "",
};
/// Compute portfolio summary given positions and current prices.
@ -145,6 +147,7 @@ pub fn portfolioSummary(
.unrealized_pnl = mv - pos.total_cost,
.unrealized_return = if (pos.total_cost > 0) (mv / pos.total_cost) - 1.0 else 0,
.is_manual_price = if (manual_prices) |mp| mp.contains(pos.symbol) else false,
.account = pos.account,
});
}

View file

@ -1160,6 +1160,13 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
};
defer summary.deinit(allocator);
// Sort allocations alphabetically by symbol
std.mem.sort(zfin.risk.Allocation, summary.allocations, {}, struct {
fn f(_: void, a: zfin.risk.Allocation, b: zfin.risk.Allocation) bool {
return std.mem.lessThan(u8, a.symbol, b.symbol);
}
}.f);
// Include non-stock assets in the grand total
const cash_total = portfolio.totalCash();
const cd_total = portfolio.totalCdFaceValue();
@ -1218,10 +1225,10 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
// Column headers
try out.print("\n", .{});
try setFg(out, color, CLR_MUTED);
try out.print(" {s:<6} {s:>8} {s:>10} {s:>10} {s:>16} {s:>14} {s:>8} {s:>13} {s}\n", .{
try out.print(" " ++ fmt.sym_col_spec ++ " {s:>8} {s:>10} {s:>10} {s:>16} {s:>14} {s:>8} {s:>13} {s}\n", .{
"Symbol", "Shares", "Avg Cost", "Price", "Market Value", "Gain/Loss", "Weight", "Date", "Account",
});
try out.print(" {s:->6} {s:->8} {s:->10} {s:->10} {s:->16} {s:->14} {s:->8} {s:->13} {s:->8}\n", .{
try out.print(" " ++ std.fmt.comptimePrint("{{s:->{d}}}", .{fmt.sym_col_width}) ++ " {s:->8} {s:->10} {s:->10} {s:->16} {s:->14} {s:->8} {s:->13} {s:->8}\n", .{
"", "", "", "", "", "", "", "", "",
});
try reset(out, color);
@ -1261,7 +1268,7 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
date_col_len = written.len;
}
try out.print(" {s:<6} {d:>8.1} {s:>10} ", .{
try out.print(" " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} ", .{
a.symbol, a.shares, fmt.fmtMoney2(&cost_buf2, a.avg_cost),
});
if (a.is_manual_price) try setFg(out, color, CLR_YELLOW);
@ -1515,28 +1522,26 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
try out.print(" Cash\n", .{});
try reset(out, color);
try setFg(out, color, CLR_MUTED);
try out.print(" {s:<20} {s:>14}\n", .{ "Account", "Balance" });
try out.print(" {s:->20} {s:->14}\n", .{ "", "" });
var cash_hdr_buf: [80]u8 = undefined;
try out.print("{s}\n", .{fmt.fmtCashHeader(&cash_hdr_buf)});
var cash_sep_buf: [80]u8 = undefined;
try out.print("{s}\n", .{fmt.fmtCashSep(&cash_sep_buf)});
try reset(out, color);
for (portfolio.lots) |lot| {
if (lot.lot_type != .cash) continue;
var cash_buf: [24]u8 = undefined;
const acct2: []const u8 = lot.account orelse "Unknown";
try out.print(" {s:<20} {s:>14}\n", .{
acct2,
fmt.fmtMoney(&cash_buf, lot.shares),
});
var row_buf: [80]u8 = undefined;
try out.print("{s}\n", .{fmt.fmtCashRow(&row_buf, acct2, lot.shares)});
}
// Cash total
var sep_buf: [80]u8 = undefined;
try setFg(out, color, CLR_MUTED);
try out.print(" {s:->20} {s:->14}\n", .{ "", "" });
try out.print("{s}\n", .{fmt.fmtCashSep(&sep_buf)});
try reset(out, color);
var cash_total_buf: [24]u8 = undefined;
var total_buf: [80]u8 = undefined;
try setBold(out, color);
try out.print(" {s:>20} {s:>14}\n", .{
"TOTAL", fmt.fmtMoney(&cash_total_buf, portfolio.totalCash()),
});
try out.print("{s}\n", .{fmt.fmtCashTotal(&total_buf, portfolio.totalCash())});
try reset(out, color);
}
@ -1569,7 +1574,7 @@ fn cmdPortfolio(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.Da
ps2 = fmt.fmtMoney2(&price_str2, candles2[candles2.len - 1].close);
}
}
try o.print(" {s:<6} {s:>10}\n", .{ sym, ps2 });
try o.print(" " ++ fmt.sym_col_spec ++ " {s:>10}\n", .{ sym, ps2 });
}
}.f;
@ -1667,7 +1672,7 @@ fn printCliLotRow(out: anytype, color: bool, lot: zfin.Lot, current_price: f64)
const lot_sign: []const u8 = if (gl >= 0) "+" else "-";
try setFg(out, color, CLR_MUTED);
try out.print(" {s:<6} {d:>8.1} {s:>10} {s:>10} {s:>16} ", .{
try out.print(" " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} {s:>10} {s:>16} ", .{
status_str, lot.shares, fmt.fmtMoney2(&lot_price_buf, lot.open_price), "", "",
});
try reset(out, color);

View file

@ -9,6 +9,103 @@ const Candle = @import("models/candle.zig").Candle;
const OptionContract = @import("models/option.zig").OptionContract;
const Lot = @import("models/portfolio.zig").Lot;
// Layout constants
/// Width of the symbol column in portfolio view (CLI + TUI).
pub const sym_col_width = 7;
/// Comptime format spec for left-aligned symbol column, e.g. "{s:<7}".
pub const sym_col_spec = std.fmt.comptimePrint("{{s:<{d}}}", .{sym_col_width});
/// Width of the account name column in cash section (CLI + TUI).
pub const cash_acct_width = 30;
/// Format the cash section column header: " Account Balance"
pub fn fmtCashHeader(buf: []u8) []const u8 {
const w = cash_acct_width;
var pos: usize = 0;
@memcpy(buf[0..2], " ");
pos += 2;
const acct_label = "Account";
@memcpy(buf[pos..][0..acct_label.len], acct_label);
@memset(buf[pos + acct_label.len ..][0 .. w - acct_label.len], ' ');
pos += w;
buf[pos] = ' ';
pos += 1;
const bal_label = "Balance";
const bal_pad = if (bal_label.len < 14) 14 - bal_label.len else 0;
@memset(buf[pos..][0..bal_pad], ' ');
pos += bal_pad;
@memcpy(buf[pos..][0..bal_label.len], bal_label);
pos += bal_label.len;
return buf[0..pos];
}
/// Format a cash row: " account_name $1,234.56"
/// Returns a slice of `buf`.
pub fn fmtCashRow(buf: []u8, account: []const u8, amount: f64) []const u8 {
var money_buf: [24]u8 = undefined;
const money = fmtMoney(&money_buf, amount);
const w = cash_acct_width;
// " {name:<w} {money:>14}"
const prefix = " ";
var pos: usize = 0;
@memcpy(buf[pos..][0..prefix.len], prefix);
pos += prefix.len;
const name_len = @min(account.len, w);
@memcpy(buf[pos..][0..name_len], account[0..name_len]);
if (name_len < w) @memset(buf[pos + name_len ..][0 .. w - name_len], ' ');
pos += w;
buf[pos] = ' ';
pos += 1;
// Right-align money in 14 chars
const money_pad = if (money.len < 14) 14 - money.len else 0;
@memset(buf[pos..][0..money_pad], ' ');
pos += money_pad;
@memcpy(buf[pos..][0..money.len], money);
pos += money.len;
return buf[0..pos];
}
/// Format the cash total separator line.
pub fn fmtCashSep(buf: []u8) []const u8 {
const w = cash_acct_width;
var pos: usize = 0;
@memcpy(buf[0..2], " ");
pos += 2;
@memset(buf[pos..][0..w], '-');
pos += w;
buf[pos] = ' ';
pos += 1;
@memset(buf[pos..][0..14], '-');
pos += 14;
return buf[0..pos];
}
/// Format the cash total row.
pub fn fmtCashTotal(buf: []u8, total: f64) []const u8 {
var money_buf: [24]u8 = undefined;
const money = fmtMoney(&money_buf, total);
const w = cash_acct_width;
var pos: usize = 0;
@memcpy(buf[0..2], " ");
pos += 2;
// Right-align "TOTAL" in w chars
const label = "TOTAL";
const label_pad = w - label.len;
@memset(buf[pos..][0..label_pad], ' ');
pos += label_pad;
@memcpy(buf[pos..][0..label.len], label);
pos += label.len;
buf[pos] = ' ';
pos += 1;
const money_pad = if (money.len < 14) 14 - money.len else 0;
@memset(buf[pos..][0..money_pad], ' ');
pos += money_pad;
@memcpy(buf[pos..][0..money.len], money);
pos += money.len;
return buf[0..pos];
}
// Number formatters
/// Format a dollar amount with commas and 2 decimals: $1,234.56

View file

@ -108,6 +108,8 @@ pub const Position = struct {
closed_lots: u32,
/// Total realized P&L from closed lots
realized_pnl: f64,
/// Account name (shared across lots, or "Multiple" if mixed).
account: []const u8 = "",
};
/// A portfolio is a collection of lots.
@ -211,7 +213,15 @@ pub const Portfolio = struct {
.open_lots = 0,
.closed_lots = 0,
.realized_pnl = 0,
.account = lot.account orelse "",
};
} else {
// Track account: if lots have different accounts, mark as "Multiple"
const existing = entry.value_ptr.account;
const new_acct = lot.account orelse "";
if (existing.len > 0 and !std.mem.eql(u8, existing, "Multiple") and !std.mem.eql(u8, existing, new_acct)) {
entry.value_ptr.account = "Multiple";
}
}
if (lot.isOpen()) {
entry.value_ptr.shares += lot.shares;

View file

@ -39,6 +39,9 @@ pub const Action = enum {
options_filter_9,
chart_timeframe_next,
chart_timeframe_prev,
sort_col_next,
sort_col_prev,
sort_reverse,
};
pub const KeyCombo = struct {
@ -117,6 +120,9 @@ const default_bindings = [_]Binding{
.{ .action = .options_filter_9, .key = .{ .codepoint = '9', .mods = .{ .ctrl = true } } },
.{ .action = .chart_timeframe_next, .key = .{ .codepoint = ']' } },
.{ .action = .chart_timeframe_prev, .key = .{ .codepoint = '[' } },
.{ .action = .sort_col_next, .key = .{ .codepoint = '>' } },
.{ .action = .sort_col_prev, .key = .{ .codepoint = '<' } },
.{ .action = .sort_reverse, .key = .{ .codepoint = 'o' } },
};
pub fn defaults() KeyMap {

View file

@ -17,6 +17,48 @@ const ascii_g = blk: {
break :blk table;
};
/// Build a fixed-display-width column header label with optional sort indicator.
/// The indicator (/, 3 bytes, 1 display column) replaces a padding space so total
/// display width stays constant. Indicator always appears on the left side.
/// `left` controls text alignment (left-aligned vs right-aligned).
fn colLabel(buf: []u8, name: []const u8, comptime col_width: usize, left: bool, indicator: ?[]const u8) []const u8 {
const ind = indicator orelse {
// No indicator: plain padded label
if (left) {
@memset(buf[0..col_width], ' ');
@memcpy(buf[0..name.len], name);
return buf[0..col_width];
} else {
@memset(buf[0..col_width], ' ');
const offset = col_width - name.len;
@memcpy(buf[offset..][0..name.len], name);
return buf[0..col_width];
}
};
// Indicator always on the left, replacing one padding space.
// total display cols = col_width, byte length = col_width - 1 + ind.len
const total_bytes = col_width - 1 + ind.len;
if (total_bytes > buf.len) return name;
if (left) {
// "▲Name " indicator, text, then spaces
@memcpy(buf[0..ind.len], ind);
@memcpy(buf[ind.len..][0..name.len], name);
const content_len = ind.len + name.len;
if (content_len < total_bytes) @memset(buf[content_len..total_bytes], ' ');
} else {
// " ▲Name" spaces, indicator, then text
const pad = col_width - name.len - 1;
@memset(buf[0..pad], ' ');
@memcpy(buf[pad..][0..ind.len], ind);
@memcpy(buf[pad + ind.len ..][0..name.len], name);
}
return buf[0..total_bytes];
}
// Portfolio column layout: gain/loss column start position (display columns).
// prefix(4) + sym(sym_col_width+1) + shares(9) + avgcost(11) + price(11) + mv(17) = 4 + sym_col_width + 49
const gl_col_start: usize = 4 + fmt.sym_col_width + 49;
fn glyph(ch: u8) []const u8 {
if (ch < 128) return ascii_g[ch];
return " ";
@ -55,6 +97,57 @@ const InputMode = enum {
help,
};
/// Sort field for portfolio columns.
const PortfolioSortField = enum {
symbol,
shares,
avg_cost,
price,
market_value,
gain_loss,
weight,
account,
pub fn label(self: PortfolioSortField) []const u8 {
return switch (self) {
.symbol => "Symbol",
.shares => "Shares",
.avg_cost => "Avg Cost",
.price => "Price",
.market_value => "Market Value",
.gain_loss => "Gain/Loss",
.weight => "Weight",
.account => "Account",
};
}
pub fn next(self: PortfolioSortField) ?PortfolioSortField {
const fields = std.meta.fields(PortfolioSortField);
const idx: usize = @intFromEnum(self);
if (idx + 1 >= fields.len) return null;
return @enumFromInt(idx + 1);
}
pub fn prev(self: PortfolioSortField) ?PortfolioSortField {
const idx: usize = @intFromEnum(self);
if (idx == 0) return null;
return @enumFromInt(idx - 1);
}
};
const SortDirection = enum {
asc,
desc,
pub fn flip(self: SortDirection) SortDirection {
return if (self == .asc) .desc else .asc;
}
pub fn indicator(self: SortDirection) []const u8 {
return if (self == .asc) "" else "";
}
};
/// A row in the portfolio view -- position header, lot detail, or special sections.
const PortfolioRow = struct {
kind: Kind,
@ -135,6 +228,8 @@ const App = struct {
portfolio_header_lines: usize = 0, // number of styled lines before data rows
portfolio_line_to_row: [256]usize = [_]usize{0} ** 256, // maps styled line index -> portfolio_rows index
portfolio_line_count: usize = 0, // total styled lines in portfolio view
portfolio_sort_field: PortfolioSortField = .symbol, // current sort column
portfolio_sort_dir: SortDirection = .asc, // current sort direction
// Options navigation (inline expand/collapse like portfolio)
options_cursor: usize = 0, // selected row in flattened options view
@ -271,6 +366,34 @@ const App = struct {
}
if (self.active_tab == .portfolio and mouse.row > 0) {
const content_row = @as(usize, @intCast(mouse.row)) + self.scroll_offset;
// Click on column header row -> sort by that column
if (self.portfolio_header_lines > 0 and content_row == self.portfolio_header_lines - 1) {
// Column boundaries derived from sym_col_width (sw).
// prefix(4) + Symbol(sw+1) + Shares(8+1) + AvgCost(10+1) + Price(10+1) + MV(16+1) + G/L(14+1) + Weight(8)
const sw = fmt.sym_col_width;
const col = @as(usize, @intCast(mouse.col));
const new_field: ?PortfolioSortField =
if (col < 4 + sw + 1) .symbol
else if (col < 4 + sw + 10) .shares
else if (col < 4 + sw + 21) .avg_cost
else if (col < 4 + sw + 32) .price
else if (col < 4 + sw + 49) .market_value
else if (col < 4 + sw + 64) .gain_loss
else if (col < 4 + sw + 73) .weight
else if (col < 4 + sw + 87) null // Date (not sortable)
else .account;
if (new_field) |nf| {
if (nf == self.portfolio_sort_field) {
self.portfolio_sort_dir = self.portfolio_sort_dir.flip();
} else {
self.portfolio_sort_field = nf;
self.portfolio_sort_dir = if (nf == .symbol or nf == .account) .asc else .desc;
}
self.sortPortfolioAllocations();
self.rebuildPortfolioRows();
return ctx.consumeAndRedraw();
}
}
if (content_row >= self.portfolio_header_lines and self.portfolio_rows.items.len > 0) {
const line_idx = content_row - self.portfolio_header_lines;
if (line_idx < self.portfolio_line_count and line_idx < self.portfolio_line_to_row.len) {
@ -531,6 +654,36 @@ const App = struct {
return ctx.consumeAndRedraw();
}
},
.sort_col_next => {
if (self.active_tab == .portfolio) {
if (self.portfolio_sort_field.next()) |new_field| {
self.portfolio_sort_field = new_field;
self.portfolio_sort_dir = if (new_field == .symbol or new_field == .account) .asc else .desc;
self.sortPortfolioAllocations();
self.rebuildPortfolioRows();
}
return ctx.consumeAndRedraw();
}
},
.sort_col_prev => {
if (self.active_tab == .portfolio) {
if (self.portfolio_sort_field.prev()) |new_field| {
self.portfolio_sort_field = new_field;
self.portfolio_sort_dir = if (new_field == .symbol or new_field == .account) .asc else .desc;
self.sortPortfolioAllocations();
self.rebuildPortfolioRows();
}
return ctx.consumeAndRedraw();
}
},
.sort_reverse => {
if (self.active_tab == .portfolio) {
self.portfolio_sort_dir = self.portfolio_sort_dir.flip();
self.sortPortfolioAllocations();
self.rebuildPortfolioRows();
return ctx.consumeAndRedraw();
}
},
}
}
@ -933,6 +1086,7 @@ const App = struct {
}
self.portfolio_summary = summary;
self.sortPortfolioAllocations();
self.rebuildPortfolioRows();
if (self.symbol.len == 0 and summary.allocations.len > 0) {
@ -975,6 +1129,31 @@ const App = struct {
}
}
fn sortPortfolioAllocations(self: *App) void {
if (self.portfolio_summary) |s| {
const SortCtx = struct {
field: PortfolioSortField,
dir: SortDirection,
fn lessThan(ctx: @This(), a: zfin.risk.Allocation, b: zfin.risk.Allocation) bool {
const lhs = if (ctx.dir == .asc) a else b;
const rhs = if (ctx.dir == .asc) b else a;
return switch (ctx.field) {
.symbol => std.mem.lessThan(u8, lhs.symbol, rhs.symbol),
.shares => lhs.shares < rhs.shares,
.avg_cost => lhs.avg_cost < rhs.avg_cost,
.price => lhs.current_price < rhs.current_price,
.market_value => lhs.market_value < rhs.market_value,
.gain_loss => lhs.unrealized_pnl < rhs.unrealized_pnl,
.weight => lhs.weight < rhs.weight,
.account => std.mem.lessThan(u8, lhs.account, rhs.account),
};
}
};
std.mem.sort(zfin.risk.Allocation, s.allocations, SortCtx{ .field = self.portfolio_sort_field, .dir = self.portfolio_sort_dir }, SortCtx.lessThan);
}
}
fn rebuildPortfolioRows(self: *App) void {
self.portfolio_rows.clearRetainingCapacity();
@ -1538,6 +1717,7 @@ const App = struct {
}
self.portfolio_summary = summary;
self.sortPortfolioAllocations();
self.rebuildPortfolioRows();
if (missing > 0) {
@ -1662,13 +1842,27 @@ const App = struct {
buf[row * width + ci] = .{ .char = .{ .grapheme = graphemes[ci] }, .style = s };
}
} else {
for (0..@min(line.text.len, width)) |ci| {
// UTF-8 aware rendering: byte index and column index tracked separately
var col: usize = 0;
var bi: usize = 0;
while (bi < line.text.len and col < width) {
var s = line.style;
// Apply alt_style for the gain/loss column range
if (line.alt_style) |alt| {
if (ci >= line.alt_start and ci < line.alt_end) s = alt;
if (col >= line.alt_start and col < line.alt_end) s = alt;
}
buf[row * width + ci] = .{ .char = .{ .grapheme = glyph(line.text[ci]) }, .style = s };
const byte = line.text[bi];
if (byte < 0x80) {
// ASCII: single byte, single column
buf[row * width + col] = .{ .char = .{ .grapheme = ascii_g[byte] }, .style = s };
bi += 1;
} else {
// Multi-byte UTF-8: determine sequence length
const seq_len: usize = if (byte >= 0xF0) 4 else if (byte >= 0xE0) 3 else if (byte >= 0xC0) 2 else 1;
const end = @min(bi + seq_len, line.text.len);
buf[row * width + col] = .{ .char = .{ .grapheme = line.text[bi..end] }, .style = s };
bi = end;
}
col += 1;
}
}
}
@ -1759,8 +1953,29 @@ const App = struct {
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
// Column header (4-char prefix to match arrow(2)+star(2) in data rows)
const hdr = try std.fmt.allocPrint(arena, " {s:<6} {s:>8} {s:>10} {s:>10} {s:>16} {s:>14} {s:>8} {s:>13} {s}", .{
"Symbol", "Shares", "Avg Cost", "Price", "Market Value", "Gain/Loss", "Weight", "Date", "Account",
// Active sort column gets a sort indicator within the column width
const sf = self.portfolio_sort_field;
const si = self.portfolio_sort_dir.indicator();
// Build column labels with indicator embedded in padding
// Left-aligned cols: "Name▲ " Right-aligned cols: " ▼Price"
var sym_hdr_buf: [16]u8 = undefined;
var shr_hdr_buf: [16]u8 = undefined;
var avg_hdr_buf: [16]u8 = undefined;
var prc_hdr_buf: [16]u8 = undefined;
var mv_hdr_buf: [24]u8 = undefined;
var gl_hdr_buf: [24]u8 = undefined;
var wt_hdr_buf: [16]u8 = undefined;
const sym_hdr = colLabel(&sym_hdr_buf, "Symbol", fmt.sym_col_width, true, if (sf == .symbol) si else null);
const shr_hdr = colLabel(&shr_hdr_buf, "Shares", 8, false, if (sf == .shares) si else null);
const avg_hdr = colLabel(&avg_hdr_buf, "Avg Cost", 10, false, if (sf == .avg_cost) si else null);
const prc_hdr = colLabel(&prc_hdr_buf, "Price", 10, false, if (sf == .price) si else null);
const mv_hdr = colLabel(&mv_hdr_buf, "Market Value", 16, false, if (sf == .market_value) si else null);
const gl_hdr = colLabel(&gl_hdr_buf, "Gain/Loss", 14, false, if (sf == .gain_loss) si else null);
const wt_hdr = colLabel(&wt_hdr_buf, "Weight", 8, false, if (sf == .weight) si else null);
const acct_ind: []const u8 = if (sf == .account) si else "";
const hdr = try std.fmt.allocPrint(arena, " {s} {s} {s} {s} {s} {s} {s} {s:>13} {s}{s}", .{
sym_hdr, shr_hdr, avg_hdr, prc_hdr, mv_hdr, gl_hdr, wt_hdr, "Date", acct_ind, "Account",
});
try lines.append(arena, .{ .text = hdr, .style = th.headerStyle() });
@ -1840,7 +2055,7 @@ const App = struct {
}
}
const text = try std.fmt.allocPrint(arena, "{s}{s}{s:<6} {d:>8.1} {s:>10} {s:>10} {s:>16} {s:>14} {d:>7.1}% {s:>13} {s}", .{
const text = try std.fmt.allocPrint(arena, "{s}{s}" ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} {s:>10} {s:>16} {s:>14} {d:>7.1}% {s:>13} {s}", .{
arrow, star, a.symbol, a.shares, cost_str, price_str, mv_str, pnl_str, a.weight * 100.0, date_col, acct_col,
});
@ -1855,8 +2070,8 @@ const App = struct {
.text = text,
.style = base_style,
.alt_style = gl_style,
.alt_start = 59,
.alt_end = 59 + 14,
.alt_start = gl_col_start,
.alt_end = gl_col_start + 14,
});
}
}
@ -1889,7 +2104,7 @@ const App = struct {
const indicator = fmt.capitalGainsIndicator(lot.open_date);
const lot_date_col = try std.fmt.allocPrint(arena, "{s} {s}", .{ date_str, indicator });
const acct_col: []const u8 = lot.account orelse "";
const text = try std.fmt.allocPrint(arena, " {s:<6} {d:>8.1} {s:>10} {s:>10} {s:>16} {s:>14} {s:>8} {s:>13} {s}", .{
const text = try std.fmt.allocPrint(arena, " " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} {s:>10} {s:>16} {s:>14} {s:>8} {s:>13} {s}", .{
status_str, lot.shares, lot_price_str, "", "", lot_gl_str, "", lot_date_col, acct_col,
});
const base_style = if (is_cursor) th.selectStyle() else th.mutedStyle();
@ -1898,8 +2113,8 @@ const App = struct {
.text = text,
.style = base_style,
.alt_style = gl_col_style,
.alt_start = 59,
.alt_end = 59 + 14,
.alt_start = gl_col_start,
.alt_end = gl_col_start + 14,
});
}
},
@ -1913,7 +2128,7 @@ const App = struct {
break :blk @as([]const u8, "--");
} else "--";
const star2: []const u8 = if (is_active_sym) "* " else " ";
const text = try std.fmt.allocPrint(arena, " {s}{s:<6} {s:>8} {s:>10} {s:>10} {s:>16} {s:>14} {s:>8} {s:>13}", .{
const text = try std.fmt.allocPrint(arena, " {s}" ++ fmt.sym_col_spec ++ " {s:>8} {s:>10} {s:>10} {s:>16} {s:>14} {s:>8} {s:>13}", .{
star2, row.symbol, "--", "--", ps, "--", "--", "watch", "",
});
const row_style = if (is_cursor) th.selectStyle() else th.contentStyle();
@ -2000,11 +2215,9 @@ const App = struct {
},
.cash_row => {
if (row.lot) |lot| {
var cash_amt_buf: [24]u8 = undefined;
const text = try std.fmt.allocPrint(arena, " {s:<20} {s:>14}", .{
row.symbol, // account name
fmt.fmtMoney(&cash_amt_buf, lot.shares),
});
var cash_row_buf: [80]u8 = undefined;
const row_text = fmt.fmtCashRow(&cash_row_buf, row.symbol, lot.shares);
const text = try std.fmt.allocPrint(arena, " {s}", .{row_text});
const row_style5 = if (is_cursor) th.selectStyle() else th.mutedStyle();
try lines.append(arena, .{ .text = text, .style = row_style5 });
}
@ -2992,11 +3205,13 @@ const App = struct {
"Scroll to bottom", "Page down", "Page up", "Select next",
"Select prev", "Expand/collapse", "Select symbol", "Change symbol (search)",
"This help", "Edit portfolio/watchlist",
"Reload portfolio from disk",
"Toggle all calls (options)", "Toggle all puts (options)",
"Filter +/- 1 NTM", "Filter +/- 2 NTM", "Filter +/- 3 NTM",
"Filter +/- 4 NTM", "Filter +/- 5 NTM", "Filter +/- 6 NTM",
"Filter +/- 7 NTM", "Filter +/- 8 NTM", "Filter +/- 9 NTM",
"Chart: next timeframe", "Chart: prev timeframe",
"Sort: next column", "Sort: prev column", "Sort: reverse order",
};
for (actions, 0..) |action, ai| {