options and cd display cleanup (TUI)

This commit is contained in:
Emil Lerch 2026-03-31 15:47:36 -07:00
parent 31dd551efe
commit 9de40e8219
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 52 additions and 27 deletions

View file

@ -385,6 +385,15 @@ pub fn lotMaturitySortFn(_: void, a: Lot, b: Lot) bool {
return ad < bd; return ad < bd;
} }
/// Sort lots by maturity date (earliest first), then by symbol name.
/// Lots without maturity sort last.
pub fn lotMaturityThenSymbolSortFn(_: void, a: Lot, b: Lot) bool {
const ad = if (a.maturity_date) |d| d.days else std.math.maxInt(i32);
const bd = if (b.maturity_date) |d| d.days else std.math.maxInt(i32);
if (ad != bd) return ad < bd;
return std.mem.lessThan(u8, a.symbol, b.symbol);
}
/// Summary of DRIP (dividend reinvestment) lots for a single ST or LT bucket. /// Summary of DRIP (dividend reinvestment) lots for a single ST or LT bucket.
pub const DripSummary = struct { pub const DripSummary = struct {
lot_count: usize = 0, lot_count: usize = 0,

View file

@ -413,33 +413,28 @@ pub fn rebuildPortfolioRows(app: *App) void {
} }
} }
// Options section (filtered by account when filter is active) // Options section (sorted by expiration date, then symbol; filtered by account)
if (app.portfolio) |pf| { if (app.portfolio) |pf| {
if (pf.hasType(.option)) { if (pf.hasType(.option)) {
var has_matching = false; var option_lots: std.ArrayList(zfin.Lot) = .empty;
if (app.account_filter != null) { defer option_lots.deinit(app.allocator);
for (pf.lots) |lot| { for (pf.lots) |lot| {
if (lot.security_type == .option and matchesAccountFilter(app, lot.account)) { if (lot.security_type == .option and matchesAccountFilter(app, lot.account)) {
has_matching = true; option_lots.append(app.allocator, lot) catch continue;
break;
}
} }
} else {
has_matching = true;
} }
if (has_matching) { if (option_lots.items.len > 0) {
app.portfolio_rows.append(app.allocator, .{ app.portfolio_rows.append(app.allocator, .{
.kind = .section_header, .kind = .section_header,
.symbol = "Options", .symbol = "Options",
}) catch {}; }) catch {};
for (pf.lots) |lot| { std.mem.sort(zfin.Lot, option_lots.items, {}, fmt.lotMaturityThenSymbolSortFn);
if (lot.security_type == .option and matchesAccountFilter(app, lot.account)) { for (option_lots.items) |lot| {
app.portfolio_rows.append(app.allocator, .{ app.portfolio_rows.append(app.allocator, .{
.kind = .option_row, .kind = .option_row,
.symbol = lot.symbol, .symbol = lot.symbol,
.lot = lot, .lot = lot,
}) catch continue; }) catch continue;
}
} }
} }
} }
@ -1022,7 +1017,7 @@ pub fn drawContent(app: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, width
// Add column headers for each section type // Add column headers for each section type
if (std.mem.eql(u8, row.symbol, "Options")) { if (std.mem.eql(u8, row.symbol, "Options")) {
const col_hdr = try std.fmt.allocPrint(arena, " {s:<30} {s:>6} {s:>12} {s:>14} {s}", .{ const col_hdr = try std.fmt.allocPrint(arena, " {s:<30} {s:>6} {s:>12} {s:>14} {s}", .{
"Contract", "Qty", "Cost/Ctrct", "Total Cost", "Account", "Contract", "Qty", "Cost/Ctrct", "Premium", "Account",
}); });
try lines.append(arena, .{ .text = col_hdr, .style = th.mutedStyle() }); try lines.append(arena, .{ .text = col_hdr, .style = th.mutedStyle() });
} else if (std.mem.eql(u8, row.symbol, "Certificates of Deposit")) { } else if (std.mem.eql(u8, row.symbol, "Certificates of Deposit")) {
@ -1034,22 +1029,41 @@ pub fn drawContent(app: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, width
}, },
.option_row => { .option_row => {
if (row.lot) |lot| { if (row.lot) |lot| {
// Options: symbol (description), qty (contracts), cost/contract, cost basis, account // Options: symbol (description), qty (contracts), cost/contract, premium (+/-), account
const qty = lot.shares; // negative = short const qty = lot.shares; // negative = short
const cost_per = lot.open_price; // per-contract cost const cost_per = lot.open_price; // per-contract cost
const total_cost = @abs(qty) * cost_per; const total_premium = @abs(qty) * cost_per * lot.multiplier;
// Short = received premium (+), Long = paid premium (-)
const received = qty < 0;
var cost_buf3: [24]u8 = undefined; var cost_buf3: [24]u8 = undefined;
var total_buf: [24]u8 = undefined; var prem_val_buf: [24]u8 = undefined;
const prem_money = fmt.fmtMoneyAbs(&prem_val_buf, total_premium);
var prem_buf: [20]u8 = undefined;
const prem_str = if (received)
std.fmt.bufPrint(&prem_buf, "+{s}", .{prem_money}) catch "?"
else
std.fmt.bufPrint(&prem_buf, "-{s}", .{prem_money}) catch "?";
const acct_col2: []const u8 = lot.account orelse ""; const acct_col2: []const u8 = lot.account orelse "";
// Column layout: 4 + 30 + 1 + 6 + 1 + 12 + 1 = 55 (premium start)
const prem_col_start: usize = 55;
const text = try std.fmt.allocPrint(arena, " {s:<30} {d:>6.0} {s:>12} {s:>14} {s}", .{ const text = try std.fmt.allocPrint(arena, " {s:<30} {d:>6.0} {s:>12} {s:>14} {s}", .{
lot.symbol, lot.symbol,
qty, qty,
fmt.fmtMoneyAbs(&cost_buf3, cost_per), fmt.fmtMoneyAbs(&cost_buf3, cost_per),
fmt.fmtMoneyAbs(&total_buf, total_cost), prem_str,
acct_col2, acct_col2,
}); });
const row_style2 = if (is_cursor) th.selectStyle() else th.contentStyle(); const today = fmt.todayDate();
try lines.append(arena, .{ .text = text, .style = row_style2 }); const is_expired = if (lot.maturity_date) |md| md.lessThan(today) else false;
const row_style2 = if (is_cursor) th.selectStyle() else if (is_expired) th.mutedStyle() else th.contentStyle();
const prem_style = if (is_cursor) th.selectStyle() else if (is_expired) th.mutedStyle() else if (received) th.positiveStyle() else th.negativeStyle();
try lines.append(arena, .{
.text = text,
.style = row_style2,
.alt_style = prem_style,
.alt_start = prem_col_start,
.alt_end = prem_col_start + 14,
});
} }
}, },
.cd_row => { .cd_row => {
@ -1075,7 +1089,9 @@ pub fn drawContent(app: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, width
note_display, note_display,
acct_col3, acct_col3,
}); });
const row_style3 = if (is_cursor) th.selectStyle() else th.contentStyle(); const today = fmt.todayDate();
const is_expired = if (lot.maturity_date) |md| md.lessThan(today) else false;
const row_style3 = if (is_cursor) th.selectStyle() else if (is_expired) th.mutedStyle() else th.contentStyle();
try lines.append(arena, .{ .text = text, .style = row_style3 }); try lines.append(arena, .{ .text = text, .style = row_style3 });
} }
}, },