options and cd display cleanup (TUI)
This commit is contained in:
parent
31dd551efe
commit
9de40e8219
2 changed files with 52 additions and 27 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue