use new common cli helpers throughout
All checks were successful
Generic zig build / build (push) Successful in 2m3s
Generic zig build / deploy (push) Successful in 15s

This commit is contained in:
Emil Lerch 2026-05-01 16:22:19 -07:00
parent d15939a9ad
commit 94311f6ff7
Signed by: lobo
GPG key ID: A7B62D657EF764F8
15 changed files with 169 additions and 458 deletions

View file

@ -86,9 +86,7 @@ pub fn display(result: zfin.analysis.AnalysisResult, stock_pct: f64, bond_pct: f
const label_width = fmt.analysis_label_width;
const bar_width = fmt.analysis_bar_width;
try cli.setBold(out, color);
try out.print("\nPortfolio Analysis ({s})\n", .{file_path});
try cli.reset(out, color);
try cli.printBold(out, color, "\nPortfolio Analysis ({s})\n", .{file_path});
try out.print("========================================\n\n", .{});
// Equities vs Fixed Income summary
@ -97,9 +95,7 @@ pub fn display(result: zfin.analysis.AnalysisResult, stock_pct: f64, bond_pct: f
var fi_buf: [24]u8 = undefined;
const eq_dollars = fmt.fmtMoneyAbs(&eq_buf, stock_pct * total_value);
const fi_dollars = fmt.fmtMoneyAbs(&fi_buf, bond_pct * total_value);
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" Equities {d:.1}% ({s}) / Fixed Income {d:.1}% ({s})\n\n", .{ stock_pct * 100, eq_dollars, bond_pct * 100, fi_dollars });
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " Equities {d:.1}% ({s}) / Fixed Income {d:.1}% ({s})\n\n", .{ stock_pct * 100, eq_dollars, bond_pct * 100, fi_dollars });
}
const sections = [_]struct { items: []const zfin.analysis.BreakdownItem, title: []const u8 }{
@ -113,23 +109,18 @@ pub fn display(result: zfin.analysis.AnalysisResult, stock_pct: f64, bond_pct: f
for (sections, 0..) |sec, si| {
if (si > 0 and sec.items.len == 0) continue;
if (si > 0) try out.print("\n", .{});
// Bold + header color reset at end of printFg clears both.
try cli.setBold(out, color);
try cli.setFg(out, color, cli.CLR_HEADER);
try out.print("{s}\n", .{sec.title});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_HEADER, "{s}\n", .{sec.title});
try printBreakdownSection(out, sec.items, label_width, bar_width, color);
}
// Unclassified
if (result.unclassified.len > 0) {
try out.print("\n", .{});
try cli.setFg(out, color, cli.CLR_WARNING);
try out.print(" Unclassified (not in metadata.srf)\n", .{});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_WARNING, " Unclassified (not in metadata.srf)\n", .{});
for (result.unclassified) |sym| {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s}\n", .{sym});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " {s}\n", .{sym});
}
}

View file

@ -618,20 +618,14 @@ pub fn compareSchwabSummary(
}
fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, out: *std.Io.Writer) !void {
try cli.setBold(out, color);
try out.print("\nSchwab Account Audit", .{});
try cli.reset(out, color);
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" (brokerage is source of truth)\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, "\nSchwab Account Audit", .{});
try cli.printFg(out, color, cli.CLR_MUTED, " (brokerage is source of truth)\n", .{});
try out.print("========================================\n\n", .{});
// Column headers
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:<24} {s:>14} {s:>14} {s:>14} {s:>14}\n", .{
try cli.printFg(out, color, cli.CLR_MUTED, " {s:<24} {s:>14} {s:>14} {s:>14} {s:>14}\n", .{
"Account", "PF Cash", "BR Cash", "PF Total", "BR Total",
});
try cli.reset(out, color);
var grand_pf: f64 = 0;
var grand_br: f64 = 0;
@ -664,21 +658,19 @@ fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, o
// Account label
try out.print(" ", .{});
if (is_unmapped) {
try cli.setFg(out, color, cli.CLR_WARNING);
try cli.printFg(out, color, cli.CLR_WARNING, "{s:<24}", .{label});
} else {
try out.print("{s:<24}", .{label});
}
try out.print("{s:<24}", .{label});
if (is_unmapped) try cli.reset(out, color);
// PF Cash colored if mismatched (brokerage is truth)
try out.print(" ", .{});
if (!cash_ok) {
if (r.cash_delta.? > 0)
try cli.setFg(out, color, cli.CLR_NEGATIVE)
else
try cli.setFg(out, color, cli.CLR_POSITIVE);
const rgb = if (r.cash_delta.? > 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE;
try cli.printFg(out, color, rgb, "{s:>14}", .{fmt.fmtMoneyAbs(&pf_cash_buf, r.portfolio_cash)});
} else {
try out.print("{s:>14}", .{fmt.fmtMoneyAbs(&pf_cash_buf, r.portfolio_cash)});
}
try out.print("{s:>14}", .{fmt.fmtMoneyAbs(&pf_cash_buf, r.portfolio_cash)});
if (!cash_ok) try cli.reset(out, color);
// BR Cash
try out.print(" {s:>14}", .{br_cash_str});
@ -686,36 +678,28 @@ fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, o
// PF Total colored if not just stale prices
try out.print(" ", .{});
if (!total_ok and !cash_ok) {
if (r.total_delta.? > 0)
try cli.setFg(out, color, cli.CLR_NEGATIVE)
else
try cli.setFg(out, color, cli.CLR_POSITIVE);
const rgb = if (r.total_delta.? > 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE;
try cli.printFg(out, color, rgb, "{s:>14}", .{fmt.fmtMoneyAbs(&pf_total_buf, r.portfolio_total)});
} else {
try out.print("{s:>14}", .{fmt.fmtMoneyAbs(&pf_total_buf, r.portfolio_total)});
}
try out.print("{s:>14}", .{fmt.fmtMoneyAbs(&pf_total_buf, r.portfolio_total)});
if (!total_ok and !cash_ok) try cli.reset(out, color);
// BR Total
try out.print(" {s:>14}", .{br_total_str});
// Status
if (is_unmapped) {
try cli.setFg(out, color, cli.CLR_WARNING);
try out.print(" Unmapped", .{});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_WARNING, " Unmapped", .{});
} else if (!cash_ok) {
var delta_buf: [24]u8 = undefined;
const d = r.cash_delta.?;
const sign: []const u8 = if (d >= 0) "+" else "-";
try cli.setFg(out, color, cli.CLR_WARNING);
try out.print(" Cash {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) });
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_WARNING, " Cash {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) });
} else if (!total_ok) {
var delta_buf: [24]u8 = undefined;
const d = r.total_delta.?;
const sign: []const u8 = if (d >= 0) "+" else "-";
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" Value {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) });
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " Value {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) });
}
try out.print("\n", .{});
@ -730,33 +714,24 @@ fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, o
var grand_delta_buf: [24]u8 = undefined;
const grand_delta = grand_br - grand_pf;
try cli.setBold(out, color);
try out.print(" Total: portfolio {s} schwab {s}", .{
try cli.printBold(out, color, " Total: portfolio {s} schwab {s}", .{
fmt.fmtMoneyAbs(&pf_grand_buf, grand_pf),
fmt.fmtMoneyAbs(&br_grand_buf, grand_br),
});
try cli.reset(out, color);
if (@abs(grand_delta) < 1.0) {
// no delta
} else {
const sign: []const u8 = if (grand_delta >= 0) "+" else "-";
if (grand_delta >= 0) {
try cli.setFg(out, color, cli.CLR_NEGATIVE);
} else {
try cli.setFg(out, color, cli.CLR_POSITIVE);
}
try out.print(" delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&grand_delta_buf, @abs(grand_delta)) });
try cli.reset(out, color);
const rgb = if (grand_delta >= 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE;
try cli.printFg(out, color, rgb, " delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&grand_delta_buf, @abs(grand_delta)) });
}
try out.print("\n", .{});
if (discrepancy_count > 0) {
try cli.setFg(out, color, cli.CLR_WARNING);
try out.print(" {d} {s} — drill down with: zfin audit --schwab <account.csv>\n", .{
try cli.printFg(out, color, cli.CLR_WARNING, " {d} {s} — drill down with: zfin audit --schwab <account.csv>\n", .{
discrepancy_count, if (discrepancy_count == 1) @as([]const u8, "mismatch") else @as([]const u8, "mismatches"),
});
try cli.reset(out, color);
}
try out.print("\n", .{});
}
@ -1120,12 +1095,8 @@ fn displayRatioSuggestions(
if (!has_header) {
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print(" Ratio updates", .{});
try cli.reset(out, color);
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" (for portfolio.srf)\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, " Ratio updates", .{});
try cli.printFg(out, color, cli.CLR_MUTED, " (for portfolio.srf)\n", .{});
has_header = true;
}
@ -1137,16 +1108,10 @@ fn displayRatioSuggestions(
const drift_str = std.fmt.bufPrint(&drift_buf, "{d:.2}%", .{drift_pct}) catch "?";
try out.print(" {s:<16} ", .{lot_sym});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("ticker {s:<6}", .{price_sym});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, "ticker {s:<6}", .{price_sym});
try out.print(" ratio {s} -> ", .{cur_str});
try cli.setBold(out, color);
try out.print("{s}", .{sug_str});
try cli.reset(out, color);
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" ({s} drift)\n", .{drift_str});
try cli.reset(out, color);
try cli.printBold(out, color, "{s}", .{sug_str});
try cli.printFg(out, color, cli.CLR_MUTED, " ({s} drift)\n", .{drift_str});
break; // One suggestion per symbol
}
@ -1159,12 +1124,8 @@ fn displayRatioSuggestions(
// Display
fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.Writer) !void {
try cli.setBold(out, color);
try out.print("\nPortfolio Audit", .{});
try cli.reset(out, color);
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" (brokerage is source of truth)\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, "\nPortfolio Audit", .{});
try cli.printFg(out, color, cli.CLR_MUTED, " (brokerage is source of truth)\n", .{});
try out.print("========================================\n\n", .{});
var total_portfolio: f64 = 0;
@ -1174,26 +1135,18 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.
for (results) |acct| {
// Account header
try cli.setBold(out, color);
if (acct.account_name.len > 0) {
try out.print(" {s}", .{acct.account_name});
try cli.reset(out, color);
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" ({s}, #{s})\n", .{ acct.brokerage_name, acct.account_number });
try cli.printBold(out, color, " {s}", .{acct.account_name});
try cli.printFg(out, color, cli.CLR_MUTED, " ({s}, #{s})\n", .{ acct.brokerage_name, acct.account_number });
} else {
try out.print(" {s} #{s}", .{ acct.brokerage_name, acct.account_number });
try cli.reset(out, color);
try cli.setFg(out, color, cli.CLR_WARNING);
try out.print(" (unmapped — add account_number to accounts.srf)\n", .{});
try cli.printBold(out, color, " {s} #{s}", .{ acct.brokerage_name, acct.account_number });
try cli.printFg(out, color, cli.CLR_WARNING, " (unmapped — add account_number to accounts.srf)\n", .{});
}
try cli.reset(out, color);
// Column headers
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:<24} {s:>12} {s:>12} {s:>10} {s:>10} {s}\n", .{
try cli.printFg(out, color, cli.CLR_MUTED, " {s:<24} {s:>12} {s:>12} {s:>10} {s:>10} {s}\n", .{
"Symbol", "PF Shares", "BR Shares", "PF Price", "BR Price", "",
});
try cli.reset(out, color);
for (acct.comparisons) |cmp| {
var pf_shares_buf: [16]u8 = undefined;
@ -1297,57 +1250,44 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.
// Print PF Shares with color
switch (shares_color) {
.positive => try cli.setFg(out, color, cli.CLR_POSITIVE),
.negative => try cli.setFg(out, color, cli.CLR_NEGATIVE),
.warning => try cli.setFg(out, color, cli.CLR_WARNING),
.normal => {},
.positive => try cli.printFg(out, color, cli.CLR_POSITIVE, "{s:>12}", .{pf_shares_str}),
.negative => try cli.printFg(out, color, cli.CLR_NEGATIVE, "{s:>12}", .{pf_shares_str}),
.warning => try cli.printFg(out, color, cli.CLR_WARNING, "{s:>12}", .{pf_shares_str}),
.normal => try out.print("{s:>12}", .{pf_shares_str}),
}
try out.print("{s:>12}", .{pf_shares_str});
if (shares_color != .normal) try cli.reset(out, color);
// Print BR Shares, prices
try out.print(" {s:>12} {s:>10} {s:>10} ", .{ br_shares_str, pf_price_str, br_price_str });
// Print status
switch (status_color) {
.warning => try cli.setFg(out, color, cli.CLR_WARNING),
.muted => try cli.setFg(out, color, cli.CLR_MUTED),
.none => {},
.warning => try cli.printFg(out, color, cli.CLR_WARNING, "{s}\n", .{status}),
.muted => try cli.printFg(out, color, cli.CLR_MUTED, "{s}\n", .{status}),
.none => try out.print("{s}\n", .{status}),
}
try out.print("{s}\n", .{status});
if (status_color != .none) try cli.reset(out, color);
}
// Account totals
var pf_total_buf: [24]u8 = undefined;
var br_total_buf: [24]u8 = undefined;
var delta_buf: [24]u8 = undefined;
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:<24} {s:>12} {s:>12} {s:>10} {s:>10} ", .{
try cli.printFg(out, color, cli.CLR_MUTED, " {s:<24} {s:>12} {s:>12} {s:>10} {s:>10} ", .{
"", "", "", fmt.fmtMoneyAbs(&pf_total_buf, acct.portfolio_total), fmt.fmtMoneyAbs(&br_total_buf, acct.brokerage_total),
});
try cli.reset(out, color);
const adj_delta = acct.total_delta - acct.option_value_delta;
if (@abs(adj_delta) < 1.0) {
// no delta text needed
} else {
const sign: []const u8 = if (adj_delta >= 0) "+" else "-";
if (adj_delta >= 0) {
try cli.setFg(out, color, cli.CLR_NEGATIVE);
} else {
try cli.setFg(out, color, cli.CLR_POSITIVE);
}
try out.print("Delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(adj_delta)) });
try cli.reset(out, color);
const rgb = if (adj_delta >= 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE;
try cli.printFg(out, color, rgb, "Delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(adj_delta)) });
}
if (@abs(acct.option_value_delta) >= 1.0) {
var opt_delta_buf: [24]u8 = undefined;
const opt_sign: []const u8 = if (acct.option_value_delta >= 0) "+" else "-";
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" (options {s}{s})", .{ opt_sign, fmt.fmtMoneyAbs(&opt_delta_buf, @abs(acct.option_value_delta)) });
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " (options {s}{s})", .{ opt_sign, fmt.fmtMoneyAbs(&opt_delta_buf, @abs(acct.option_value_delta)) });
}
try out.print("\n\n", .{});
@ -1363,39 +1303,28 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.
const grand_delta = total_brokerage - total_portfolio;
const grand_adj_delta = grand_delta - total_option_delta;
try cli.setBold(out, color);
try out.print(" Total: portfolio {s} brokerage {s}", .{
try cli.printBold(out, color, " Total: portfolio {s} brokerage {s}", .{
fmt.fmtMoneyAbs(&pf_grand_buf, total_portfolio),
fmt.fmtMoneyAbs(&br_grand_buf, total_brokerage),
});
try cli.reset(out, color);
if (@abs(grand_adj_delta) < 1.0) {
// no delta text needed
} else {
const sign: []const u8 = if (grand_adj_delta >= 0) "+" else "-";
if (grand_adj_delta >= 0) {
try cli.setFg(out, color, cli.CLR_NEGATIVE);
} else {
try cli.setFg(out, color, cli.CLR_POSITIVE);
}
try out.print(" delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&grand_delta_buf, @abs(grand_adj_delta)) });
try cli.reset(out, color);
const rgb = if (grand_adj_delta >= 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE;
try cli.printFg(out, color, rgb, " delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&grand_delta_buf, @abs(grand_adj_delta)) });
}
if (@abs(total_option_delta) >= 1.0) {
var opt_grand_buf: [24]u8 = undefined;
const opt_sign: []const u8 = if (total_option_delta >= 0) "+" else "-";
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" (options {s}{s})", .{ opt_sign, fmt.fmtMoneyAbs(&opt_grand_buf, @abs(total_option_delta)) });
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " (options {s}{s})", .{ opt_sign, fmt.fmtMoneyAbs(&opt_grand_buf, @abs(total_option_delta)) });
}
try out.print("\n", .{});
if (discrepancy_count > 0) {
try cli.setFg(out, color, cli.CLR_WARNING);
try out.print(" {d} {s} to investigate\n", .{ discrepancy_count, if (discrepancy_count == 1) @as([]const u8, "mismatch") else @as([]const u8, "mismatches") });
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_WARNING, " {d} {s} to investigate\n", .{ discrepancy_count, if (discrepancy_count == 1) @as([]const u8, "mismatch") else @as([]const u8, "mismatches") });
}
try out.print("\n", .{});
}
@ -1644,9 +1573,7 @@ fn runHygieneCheck(
};
defer account_map.deinit();
try cli.setBold(out, color);
try out.print(" Portfolio hygiene\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, " Portfolio hygiene\n", .{});
// Section 1: Stale manual prices
@ -1665,9 +1592,7 @@ fn runHygieneCheck(
if (!header_shown) {
try out.print("\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" Stale manual prices (>{d} days — --stale-days to configure)\n", .{stale_days});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " Stale manual prices (>{d} days — --stale-days to configure)\n", .{stale_days});
header_shown = true;
}
@ -1685,18 +1610,12 @@ fn runHygieneCheck(
date_str,
});
const clr = stalenessColor(age_days, stale_days);
try cli.setFg(out, color, clr);
try out.print("({d} days)\n", .{@as(u32, @intCast(age_days))});
try cli.reset(out, color);
try cli.printFg(out, color, clr, "({d} days)\n", .{@as(u32, @intCast(age_days))});
}
if (!header_shown) {
try out.print("\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" Stale manual prices (>{d} days)\n", .{stale_days});
try cli.reset(out, color);
try cli.setFg(out, color, cli.CLR_POSITIVE);
try out.print(" (none)\n", .{});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " Stale manual prices (>{d} days)\n", .{stale_days});
try cli.printFg(out, color, cli.CLR_POSITIVE, " (none)\n", .{});
}
}
@ -1851,34 +1770,25 @@ fn runHygieneCheck(
if (!overdue_header_shown) {
try out.print("\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" Accounts overdue for update (weekly default — set update_cadence in accounts.srf)\n", .{});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " Accounts overdue for update (weekly default — set update_cadence in accounts.srf)\n", .{});
overdue_header_shown = true;
}
try out.print(" {s:<32} {s:<10}", .{ acct_name, cadence.label() });
if (age_days) |ad| {
const clr = stalenessColor(ad, threshold_days);
try cli.setFg(out, color, clr);
try out.print("last updated {d} days ago\n", .{@as(u32, @intCast(ad))});
try cli.printFg(out, color, clr, "last updated {d} days ago\n", .{@as(u32, @intCast(ad))});
} else {
try cli.setFg(out, color, cli.CLR_NEGATIVE);
try out.print("no update history found\n", .{});
try cli.printFg(out, color, cli.CLR_NEGATIVE, "no update history found\n", .{});
}
try cli.reset(out, color);
}
// Display accounts updated in working copy
if (updated_accounts.items.len > 0) {
try out.print("\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" Accounts updated (working copy)\n", .{});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " Accounts updated (working copy)\n", .{});
for (updated_accounts.items) |acct| {
try cli.setFg(out, color, cli.CLR_POSITIVE);
try out.print(" {s}\n", .{acct});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_POSITIVE, " {s}\n", .{acct});
}
}
}
@ -1915,9 +1825,7 @@ fn runHygieneCheck(
// Display discovered files
if (all_files.items.len > 0) {
try out.print("\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" Brokerage files (last {d} hours)\n", .{audit_file_max_age_hours});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " Brokerage files (last {d} hours)\n", .{audit_file_max_age_hours});
for (all_files.items) |f| {
const kind_label: []const u8 = switch (f.kind) {
.fidelity_csv => "fidelity",
@ -1955,9 +1863,7 @@ fn runHygieneCheck(
}
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print(" Reconciliation\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, " Reconciliation\n", .{});
for (all_files.items) |f| {
const file_data = std.fs.cwd().readFileAlloc(allocator, f.path, 10 * 1024 * 1024) catch continue;
@ -1979,9 +1885,7 @@ fn runHygieneCheck(
for (results) |r| {
if (r.account_name.len > 0) acct_count += 1;
}
try cli.setFg(out, color, cli.CLR_POSITIVE);
try out.print(" schwab summary: {d} accounts, no discrepancies\n", .{acct_count});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_POSITIVE, " schwab summary: {d} accounts, no discrepancies\n", .{acct_count});
}
},
.fidelity_csv => {
@ -1999,9 +1903,7 @@ fn runHygieneCheck(
try displayResults(results, color, out);
try displayRatioSuggestions(results, portfolio, prices, color, out);
} else {
try cli.setFg(out, color, cli.CLR_POSITIVE);
try out.print(" fidelity: {d} accounts, no discrepancies\n", .{results.len});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_POSITIVE, " fidelity: {d} accounts, no discrepancies\n", .{results.len});
// Always show ratio suggestions even in compact mode
try displayRatioSuggestions(results, portfolio, prices, color, out);
}
@ -2021,9 +1923,7 @@ fn runHygieneCheck(
try displayResults(results, color, out);
try displayRatioSuggestions(results, portfolio, prices, color, out);
} else {
try cli.setFg(out, color, cli.CLR_POSITIVE);
try out.print(" schwab: {d} accounts, no discrepancies\n", .{results.len});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_POSITIVE, " schwab: {d} accounts, no discrepancies\n", .{results.len});
try displayRatioSuggestions(results, portfolio, prices, color, out);
}
},

View file

@ -399,15 +399,13 @@ fn renderCompare(out: *std.Io.Writer, color: bool, cv: view.CompareView) !void {
const now_str = view.nowLabel(cv, &now_buf);
// Header
try cli.setFg(out, color, cli.CLR_HEADER);
try cli.setBold(out, color);
try out.print("Portfolio comparison: {s} → {s} ({d} day{s})\n", .{
try cli.printFg(out, color, cli.CLR_HEADER, "Portfolio comparison: {s} → {s} ({d} day{s})\n", .{
then_str,
now_str,
cv.days_between,
view.dayPlural(cv.days_between),
});
try cli.reset(out, color);
try out.print("\n", .{});
// Totals line two-color: muted "then → now", intent-colored delta/pct.

View file

@ -728,9 +728,7 @@ fn printReport(out: *std.Io.Writer, report: *const Report, label: []const u8, co
// Header
try cli.setBold(out, color);
try cli.setFg(out, color, h_color);
try out.writeAll("Portfolio contributions report\n");
try cli.reset(out, color);
try cli.printFg(out, color, h_color, "Portfolio contributions report\n", .{});
try cli.setFg(out, color, mut_color);
try out.writeAll(" ");
try out.writeAll(label);
@ -739,9 +737,7 @@ fn printReport(out: *std.Io.Writer, report: *const Report, label: []const u8, co
// If nothing changed at all, say so explicitly and return.
if (report.changes.len == 0) {
try cli.setFg(out, color, mut_color);
try out.writeAll(" No changes detected.\n");
try cli.reset(out, color);
try cli.printFg(out, color, mut_color, " No changes detected.\n", .{});
return;
}
@ -909,9 +905,7 @@ fn printReport(out: *std.Io.Writer, report: *const Report, label: []const u8, co
// Grand totals
try cli.setBold(out, color);
try cli.setFg(out, color, h_color);
try out.writeAll("Totals\n");
try cli.reset(out, color);
try cli.printFg(out, color, h_color, "Totals\n", .{});
var buf1: [32]u8 = undefined;
var buf2: [32]u8 = undefined;
var buf3: [32]u8 = undefined;
@ -1040,13 +1034,9 @@ fn printSummaryCell(out: *std.Io.Writer, label: []const u8, v: f64, color: bool)
var buf: [32]u8 = undefined;
try out.print("{s} ", .{label});
if (v == 0) {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("{s:>12}", .{"-"});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, "{s:>12}", .{"-"});
} else {
try cli.setFg(out, color, cli.CLR_POSITIVE);
try out.print("{s:>12}", .{fmt.fmtMoneyAbs(&buf, v)});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_POSITIVE, "{s:>12}", .{fmt.fmtMoneyAbs(&buf, v)});
}
}

View file

@ -28,15 +28,11 @@ pub fn run(svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io
}
pub fn display(dividends: []const zfin.Dividend, symbol: []const u8, current_price: ?f64, color: bool, out: *std.Io.Writer) !void {
try cli.setBold(out, color);
try out.print("\nDividend History for {s}\n", .{symbol});
try cli.reset(out, color);
try cli.printBold(out, color, "\nDividend History for {s}\n", .{symbol});
try out.print("========================================\n", .{});
if (dividends.len == 0) {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" No dividends found.\n\n", .{});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " No dividends found.\n\n", .{});
return;
}

View file

@ -34,15 +34,11 @@ pub fn run(svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io
}
pub fn display(events: []const zfin.EarningsEvent, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
try cli.setBold(out, color);
try out.print("\nEarnings History for {s}\n", .{symbol});
try cli.reset(out, color);
try cli.printBold(out, color, "\nEarnings History for {s}\n", .{symbol});
try out.print("========================================\n", .{});
if (events.len == 0) {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" No earnings data found.\n\n", .{});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " No earnings data found.\n\n", .{});
return;
}
@ -59,16 +55,14 @@ pub fn display(events: []const zfin.EarningsEvent, symbol: []const u8, color: bo
var row_buf: [128]u8 = undefined;
const row = fmt.fmtEarningsRow(&row_buf, e);
if (row.is_future) {
try cli.setFg(out, color, cli.CLR_MUTED);
} else if (row.is_positive) {
try cli.setFg(out, color, cli.CLR_POSITIVE);
} else {
try cli.setFg(out, color, cli.CLR_NEGATIVE);
}
const rgb = if (row.is_future)
cli.CLR_MUTED
else if (row.is_positive)
cli.CLR_POSITIVE
else
cli.CLR_NEGATIVE;
try out.print("{s}", .{row.text});
try cli.reset(out, color);
try cli.printFg(out, color, rgb, "{s}", .{row.text});
try out.print("\n", .{});
}

View file

@ -24,9 +24,7 @@ pub fn run(svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io
}
pub fn printProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
try cli.setBold(out, color);
try out.print("\nETF Profile: {s}\n", .{symbol});
try cli.reset(out, color);
try cli.printBold(out, color, "\nETF Profile: {s}\n", .{symbol});
try out.print("========================================\n", .{});
if (profile.expense_ratio) |er| {
@ -46,9 +44,7 @@ pub fn printProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, o
try out.print(" Inception Date: {s}\n", .{d.format(&db)});
}
if (profile.leveraged) {
try cli.setFg(out, color, cli.CLR_NEGATIVE);
try out.print(" Leveraged: YES\n", .{});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_NEGATIVE, " Leveraged: YES\n", .{});
}
if (profile.total_holdings) |th| {
try out.print(" Total Holdings: {d}\n", .{th});
@ -57,15 +53,11 @@ pub fn printProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, o
// Sectors
if (profile.sectors) |sectors| {
if (sectors.len > 0) {
try cli.setBold(out, color);
try out.print("\n Sector Allocation:\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, "\n Sector Allocation:\n", .{});
for (sectors) |sec| {
var title_buf: [64]u8 = undefined;
const name = fmt.toTitleCase(&title_buf, sec.name);
try cli.setFg(out, color, cli.CLR_ACCENT);
try out.print(" {d:>5.1}%", .{sec.weight * 100.0});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_ACCENT, " {d:>5.1}%", .{sec.weight * 100.0});
try out.print(" {s}\n", .{name});
}
}
@ -74,18 +66,14 @@ pub fn printProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, o
// Top holdings
if (profile.holdings) |holdings| {
if (holdings.len > 0) {
try cli.setBold(out, color);
try out.print("\n Top Holdings:\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, "\n Top Holdings:\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:>6} {s:>7} {s}\n", .{ "Symbol", "Weight", "Name" });
try out.print(" {s:->6} {s:->7} {s:->30}\n", .{ "", "", "" });
try cli.reset(out, color);
for (holdings) |h| {
if (h.symbol) |s| {
try cli.setFg(out, color, cli.CLR_ACCENT);
try out.print(" {s:>6}", .{s});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_ACCENT, " {s:>6}", .{s});
try out.print(" {d:>6.2}% {s}\n", .{ h.weight * 100.0, h.name });
} else {
try out.print(" {s:>6} {d:>6.2}% {s}\n", .{ "--", h.weight * 100.0, h.name });

View file

@ -166,9 +166,7 @@ fn runSymbol(
}
pub fn displaySymbol(candles: []const zfin.Candle, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
try cli.setBold(out, color);
try out.print("\nPrice History for {s} (last 30 days)\n", .{symbol});
try cli.reset(out, color);
try cli.printBold(out, color, "\nPrice History for {s} (last 30 days)\n", .{symbol});
try out.print("========================================\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("{s:>12} {s:>10} {s:>10} {s:>10} {s:>10} {s:>12}\n", .{
@ -182,11 +180,9 @@ pub fn displaySymbol(candles: []const zfin.Candle, symbol: []const u8, color: bo
for (candles) |candle| {
var db: [10]u8 = undefined;
var vb: [32]u8 = undefined;
try cli.setGainLoss(out, color, if (candle.close >= candle.open) 1.0 else -1.0);
try out.print("{s:>12} {d:>10.2} {d:>10.2} {d:>10.2} {d:>10.2} {s:>12}\n", .{
try cli.printGainLoss(out, color, if (candle.close >= candle.open) 1.0 else -1.0, "{s:>12} {d:>10.2} {d:>10.2} {d:>10.2} {d:>10.2} {s:>12}\n", .{
candle.date.format(&db), candle.open, candle.high, candle.low, candle.close, fmt.fmtIntCommas(&vb, candle.volume),
});
try cli.reset(out, color);
}
try out.print("\n{d} trading days\n\n", .{candles.len});
}
@ -288,9 +284,7 @@ pub fn renderPortfolio(
resolution_override: ?timeline.Resolution,
row_limit: usize,
) !void {
try cli.setBold(out, color);
try out.print("\nPortfolio Timeline — {s}\n", .{focus_metric.label()});
try cli.reset(out, color);
try cli.printBold(out, color, "\nPortfolio Timeline — {s}\n", .{focus_metric.label()});
try out.print("========================================\n", .{});
// Windows block
@ -407,9 +401,7 @@ fn renderTable(
var rlabel_buf: [32]u8 = undefined;
const rlabel = view.fmtResolutionLabel(&rlabel_buf, resolution_override, resolution);
try cli.setBold(out, color);
try out.print(" Recent snapshots {s}\n", .{rlabel});
try cli.reset(out, color);
try cli.printBold(out, color, " Recent snapshots {s}\n", .{rlabel});
try cli.setFg(out, color, cli.CLR_MUTED);
// Column order: Liquid Illiquid Net Worth (components sum to total).

View file

@ -5,9 +5,7 @@ const isCusipLike = @import("../models/portfolio.zig").isCusipLike;
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []const u8, color: bool, out: *std.Io.Writer) !void {
if (!isCusipLike(cusip)) {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("Note: '{s}' doesn't look like a CUSIP (expected 9 alphanumeric chars with digits)\n", .{cusip});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, "Note: '{s}' doesn't look like a CUSIP (expected 9 alphanumeric chars with digits)\n", .{cusip});
}
try cli.stderrPrint("Looking up via OpenFIGI...\n");
@ -41,33 +39,22 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []const
pub fn display(result: zfin.CusipResult, cusip: []const u8, color: bool, out: *std.Io.Writer) !void {
if (result.ticker) |ticker| {
try cli.setBold(out, color);
try out.print("{s}", .{cusip});
try cli.reset(out, color);
try cli.printBold(out, color, "{s}", .{cusip});
try out.print(" -> ", .{});
try cli.setFg(out, color, cli.CLR_ACCENT);
try out.print("{s}", .{ticker});
try cli.reset(out, color);
try out.print("\n", .{});
try cli.printFg(out, color, cli.CLR_ACCENT, "{s}\n", .{ticker});
if (result.name) |name| {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" Name: {s}\n", .{name});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " Name: {s}\n", .{name});
}
if (result.security_type) |st| {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" Type: {s}\n", .{st});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " Type: {s}\n", .{st});
}
try out.print("\n To use in portfolio: ticker::{s}\n", .{ticker});
} else {
try out.print("No ticker found for CUSIP '{s}'\n", .{cusip});
if (result.name) |name| {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" Name: {s}\n", .{name});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " Name: {s}\n", .{name});
}
try out.print("\n Tip: For mutual funds, OpenFIGI often has no coverage.\n", .{});
try out.print(" Add manually: symbol::{s},ticker::XXXX,...\n", .{cusip});

View file

@ -30,9 +30,7 @@ pub fn run(svc: *zfin.DataService, symbol: []const u8, ntm: usize, color: bool,
pub fn display(out: *std.Io.Writer, chains: []const zfin.OptionsChain, symbol: []const u8, ntm: usize, color: bool) !void {
if (chains.len == 0) return;
try cli.setBold(out, color);
try out.print("\nOptions Chain for {s}\n", .{symbol});
try cli.reset(out, color);
try cli.printBold(out, color, "\nOptions Chain for {s}\n", .{symbol});
try out.print("========================================\n", .{});
if (chains[0].underlying_price) |price| {
var price_buf: [24]u8 = undefined;
@ -98,9 +96,7 @@ pub fn printSection(
is_calls: bool,
color: bool,
) !void {
try cli.setBold(out, color);
try out.print(" {s}\n", .{label});
try cli.reset(out, color);
try cli.printBold(out, color, " {s}\n", .{label});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:>10} {s:>10} {s:>10} {s:>10} {s:>10} {s:>8} {s:>8}\n", .{
"Strike", "Last", "Bid", "Ask", "Volume", "OI", "IV",

View file

@ -24,9 +24,7 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const
const today = fmt.todayDate();
const month_end = today.lastDayOfPriorMonth();
try cli.setBold(out, color);
try out.print("\nTrailing Returns for {s}\n", .{symbol});
try cli.reset(out, color);
try cli.printBold(out, color, "\nTrailing Returns for {s}\n", .{symbol});
try out.print("========================================\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("Data points: {d} (", .{c.len});
@ -48,25 +46,19 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const
// -- As-of-date returns --
{
var db: [10]u8 = undefined;
try cli.setBold(out, color);
try out.print("\nAs-of {s}:\n", .{end_date.format(&db)});
try cli.reset(out, color);
try cli.printBold(out, color, "\nAs-of {s}:\n", .{end_date.format(&db)});
}
try printReturnsTable(out, result.asof_price, if (has_divs) result.asof_total else null, color);
// -- Month-end returns --
{
var db: [10]u8 = undefined;
try cli.setBold(out, color);
try out.print("\nMonth-end ({s}):\n", .{month_end.format(&db)});
try cli.reset(out, color);
try cli.printBold(out, color, "\nMonth-end ({s}):\n", .{month_end.format(&db)});
}
try printReturnsTable(out, result.me_price, if (has_divs) result.me_total else null, color);
if (!has_divs) {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("\nSet POLYGON_API_KEY for total returns with dividend reinvestment.\n", .{});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, "\nSet POLYGON_API_KEY for total returns with dividend reinvestment.\n", .{});
}
// -- Risk metrics --
@ -124,29 +116,21 @@ pub fn printReturnsTable(
);
if (price_arr[i] != null) {
try cli.setGainLoss(out, color, if (row.price_positive) 1.0 else -1.0);
try cli.printGainLoss(out, color, if (row.price_positive) 1.0 else -1.0, " {s:>13}", .{row.price_str});
} else {
try cli.setFg(out, color, cli.CLR_MUTED);
try cli.printFg(out, color, cli.CLR_MUTED, " {s:>13}", .{row.price_str});
}
try out.print(" {s:>13}", .{row.price_str});
try cli.reset(out, color);
if (has_total) {
if (row.total_str) |ts| {
try cli.setGainLoss(out, color, if (row.price_positive) 1.0 else -1.0);
try out.print(" {s:>13}", .{ts});
try cli.reset(out, color);
try cli.printGainLoss(out, color, if (row.price_positive) 1.0 else -1.0, " {s:>13}", .{ts});
} else {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:>13}", .{"N/A"});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " {s:>13}", .{"N/A"});
}
}
if (row.suffix.len > 0) {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("{s}", .{row.suffix});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, "{s}", .{row.suffix});
}
try out.print("\n", .{});
}
@ -165,9 +149,7 @@ pub fn printRiskTable(out: *std.Io.Writer, tr: zfin.risk.TrailingRisk, color: bo
}
if (!any) return;
try cli.setBold(out, color);
try out.print("\nRisk Metrics (monthly returns):\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, "\nRisk Metrics (monthly returns):\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("{s:>22} {s:>14} {s:>14} {s:>14}\n", .{ "", "Volatility", "Sharpe", "Max DD" });
@ -181,13 +163,9 @@ pub fn printRiskTable(out: *std.Io.Writer, tr: zfin.risk.TrailingRisk, color: bo
if (risk_arr[i]) |rm| {
try out.print(" {d:>12.1}%", .{rm.volatility * 100.0});
try out.print(" {d:>13.2}", .{rm.sharpe});
try cli.setFg(out, color, cli.CLR_NEGATIVE);
try out.print(" {d:>12.1}%", .{rm.max_drawdown * 100.0});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_NEGATIVE, " {d:>12.1}%", .{rm.max_drawdown * 100.0});
} else {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:>13} {s:>13} {s:>13}", .{ "", "", "" });
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " {s:>13} {s:>13} {s:>13}", .{ "", "", "" });
}
try out.print("\n", .{});
}

View file

@ -4,11 +4,6 @@ const cli = @import("common.zig");
const fmt = cli.fmt;
const views = @import("../views/portfolio_sections.zig");
/// Map a semantic StyleIntent to CLI ANSI foreground color.
fn setIntentFg(out: *std.Io.Writer, color: bool, intent: fmt.StyleIntent) !void {
try cli.setStyleIntent(out, color, intent);
}
pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, file_path: []const u8, watchlist_path: ?[]const u8, force_refresh: bool, color: bool, out: *std.Io.Writer) !void {
// Load portfolio from SRF file
var loaded = cli.loadPortfolio(allocator, file_path) orelse return;
@ -153,9 +148,7 @@ pub fn display(
) !void {
const summary = &pf_data.summary;
// Header with summary
try cli.setBold(out, color);
try out.print("\nPortfolio Summary ({s})\n", .{file_path});
try cli.reset(out, color);
try cli.printBold(out, color, "\nPortfolio Summary ({s})\n", .{file_path});
try out.print("========================================\n", .{});
// Summary bar
@ -165,13 +158,11 @@ pub fn display(
var gl_buf: [24]u8 = undefined;
const gl_abs = if (summary.unrealized_gain_loss >= 0) summary.unrealized_gain_loss else -summary.unrealized_gain_loss;
try out.print(" Value: {s} Cost: {s} ", .{ fmt.fmtMoneyAbs(&val_buf, summary.total_value), fmt.fmtMoneyAbs(&cost_buf, summary.total_cost) });
try cli.setGainLoss(out, color, summary.unrealized_gain_loss);
try out.print("Gain/Loss: {c}{s} ({d:.1}%)", .{
try cli.printGainLoss(out, color, summary.unrealized_gain_loss, "Gain/Loss: {c}{s} ({d:.1}%)", .{
@as(u8, if (summary.unrealized_gain_loss >= 0) '+' else '-'),
fmt.fmtMoneyAbs(&gl_buf, gl_abs),
summary.unrealized_return * 100.0,
});
try cli.reset(out, color);
try out.print("\n", .{});
}
@ -182,9 +173,7 @@ pub fn display(
if (lot.security_type != .stock) continue;
if (lot.isOpen()) open_lots += 1 else closed_lots += 1;
}
try cli.setFg(out, color, cli.CLR_MUTED);
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.printFg(out, color, cli.CLR_MUTED, " Lots: {d} open, {d} closed Positions: {d} symbols\n", .{ open_lots, closed_lots, positions.len });
// Historical portfolio value snapshots
{
@ -306,26 +295,20 @@ pub fn display(
if (!drip.st.isEmpty()) {
var drip_buf: [128]u8 = undefined;
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s}\n", .{fmt.fmtDripSummary(&drip_buf, "ST", drip.st)});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " {s}\n", .{fmt.fmtDripSummary(&drip_buf, "ST", drip.st)});
}
if (!drip.lt.isEmpty()) {
var drip_buf2: [128]u8 = undefined;
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s}\n", .{fmt.fmtDripSummary(&drip_buf2, "LT", drip.lt)});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " {s}\n", .{fmt.fmtDripSummary(&drip_buf2, "LT", drip.lt)});
}
}
}
}
// Totals line
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:->6} {s:->8} {s:->10} {s:->10} {s:->16} {s:->14} {s:->8}\n", .{
try cli.printFg(out, color, cli.CLR_MUTED, " {s:->6} {s:->8} {s:->10} {s:->10} {s:->16} {s:->14} {s:->8}\n", .{
"", "", "", "", "", "", "",
});
try cli.reset(out, color);
{
var total_mv_buf: [24]u8 = undefined;
var total_gl_buf: [24]u8 = undefined;
@ -333,24 +316,20 @@ pub fn display(
try out.print(" {s:>6} {s:>8} {s:>10} {s:>10} {s:>16} ", .{
"", "", "", "TOTAL", fmt.fmtMoneyAbs(&total_mv_buf, summary.total_value),
});
try cli.setGainLoss(out, color, summary.unrealized_gain_loss);
try out.print("{c}{s:>13}", .{
try cli.printGainLoss(out, color, summary.unrealized_gain_loss, "{c}{s:>13}", .{
@as(u8, if (summary.unrealized_gain_loss >= 0) '+' else '-'),
fmt.fmtMoneyAbs(&total_gl_buf, gl_abs),
});
try cli.reset(out, color);
try out.print(" {s:>7}\n", .{"100.0%"});
}
if (summary.realized_gain_loss != 0) {
var rpl_buf: [24]u8 = undefined;
const rpl_abs = if (summary.realized_gain_loss >= 0) summary.realized_gain_loss else -summary.realized_gain_loss;
try cli.setGainLoss(out, color, summary.realized_gain_loss);
try out.print("\n Realized P&L: {c}{s}\n", .{
try cli.printGainLoss(out, color, summary.realized_gain_loss, "\n Realized P&L: {c}{s}\n", .{
@as(u8, if (summary.realized_gain_loss >= 0) '+' else '-'),
fmt.fmtMoneyAbs(&rpl_buf, rpl_abs),
});
try cli.reset(out, color);
}
// Options section
@ -359,9 +338,7 @@ pub fn display(
defer prepared_opts.deinit();
if (prepared_opts.items.len > 0) {
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print(" Options\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, " Options\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(views.OptionsLayout.header ++ "\n", views.OptionsLayout.header_labels);
try out.print(views.OptionsLayout.separator ++ "\n", views.OptionsLayout.separator_fills);
@ -374,21 +351,15 @@ pub fn display(
const prem_start = po.premium_col_start;
const prem_end = @min(prem_start + views.OptionsLayout.premium_w, text.len);
// Pre-premium portion
try setIntentFg(out, color, po.row_style);
try out.print("{s}", .{text[0..prem_start]});
try cli.printIntent(out, color, po.row_style, "{s}", .{text[0..prem_start]});
// Premium column
try setIntentFg(out, color, po.premium_style);
try out.print("{s}", .{text[prem_start..prem_end]});
try cli.printIntent(out, color, po.premium_style, "{s}", .{text[prem_start..prem_end]});
// Post-premium portion (account)
try setIntentFg(out, color, po.row_style);
if (prem_end < text.len) try out.print("{s}", .{text[prem_end..]});
try cli.reset(out, color);
if (prem_end < text.len) try cli.printIntent(out, color, po.row_style, "{s}", .{text[prem_end..]});
try out.print("\n", .{});
}
// Options total
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:->30} {s:->6} {s:->12} {s:->14}\n", .{ "", "", "", "" });
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " {s:->30} {s:->6} {s:->12} {s:->14}\n", .{ "", "", "", "" });
var opt_total_buf: [24]u8 = undefined;
try out.print(" {s:>30} {s:>6} {s:>12} {s:>14}\n", .{
"", "", "TOTAL", fmt.fmtMoneyAbs(&opt_total_buf, opt_total_premium),
@ -402,9 +373,7 @@ pub fn display(
defer prepared_cds.deinit();
if (prepared_cds.items.len > 0) {
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print(" Certificates of Deposit\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, " Certificates of Deposit\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(views.CDsLayout.header ++ "\n", views.CDsLayout.header_labels);
try out.print(views.CDsLayout.separator ++ "\n", views.CDsLayout.separator_fills);
@ -413,14 +382,10 @@ pub fn display(
var cd_section_total: f64 = 0;
for (prepared_cds.items) |pc| {
cd_section_total += pc.lot.shares;
try setIntentFg(out, color, pc.row_style);
try out.print("{s}\n", .{pc.text});
try cli.reset(out, color);
try cli.printIntent(out, color, pc.row_style, "{s}\n", .{pc.text});
}
// CD total
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:->12} {s:->14}\n", .{ "", "" });
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " {s:->12} {s:->14}\n", .{ "", "" });
var cd_total_buf: [24]u8 = undefined;
try out.print(" {s:>12} {s:>14}\n", .{
"TOTAL", fmt.fmtMoneyAbs(&cd_total_buf, cd_section_total),
@ -431,9 +396,7 @@ pub fn display(
// Cash section
if (portfolio.hasType(.cash)) {
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print(" Cash\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, " Cash\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
var cash_hdr_buf: [80]u8 = undefined;
try out.print("{s}\n", .{fmt.fmtCashHeader(&cash_hdr_buf)});
@ -449,21 +412,15 @@ pub fn display(
}
// Cash total
var sep_buf: [80]u8 = undefined;
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("{s}\n", .{fmt.fmtCashSep(&sep_buf)});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, "{s}\n", .{fmt.fmtCashSep(&sep_buf)});
var total_buf: [80]u8 = undefined;
try cli.setBold(out, color);
try out.print("{s}\n", .{fmt.fmtCashTotal(&total_buf, portfolio.totalCash())});
try cli.reset(out, color);
try cli.printBold(out, color, "{s}\n", .{fmt.fmtCashTotal(&total_buf, portfolio.totalCash())});
}
// Illiquid assets section
if (portfolio.hasType(.illiquid)) {
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print(" Illiquid Assets\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, " Illiquid Assets\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
var il_hdr_buf: [80]u8 = undefined;
try out.print("{s}\n", .{fmt.fmtIlliquidHeader(&il_hdr_buf)});
@ -478,13 +435,9 @@ pub fn display(
}
// Illiquid total
var il_sep_buf2: [80]u8 = undefined;
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("{s}\n", .{fmt.fmtIlliquidSep(&il_sep_buf2)});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, "{s}\n", .{fmt.fmtIlliquidSep(&il_sep_buf2)});
var il_total_buf: [80]u8 = undefined;
try cli.setBold(out, color);
try out.print("{s}\n", .{fmt.fmtIlliquidTotal(&il_total_buf, portfolio.totalIlliquid())});
try cli.reset(out, color);
try cli.printBold(out, color, "{s}\n", .{fmt.fmtIlliquidTotal(&il_total_buf, portfolio.totalIlliquid())});
}
// Net Worth (if illiquid assets exist)
@ -495,21 +448,17 @@ pub fn display(
var liq_buf: [24]u8 = undefined;
var il_buf: [24]u8 = undefined;
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print(" Net Worth: {s} (Liquid: {s} Illiquid: {s})\n", .{
try cli.printBold(out, color, " Net Worth: {s} (Liquid: {s} Illiquid: {s})\n", .{
fmt.fmtMoneyAbs(&nw_buf, net_worth),
fmt.fmtMoneyAbs(&liq_buf, summary.total_value),
fmt.fmtMoneyAbs(&il_buf, illiquid_total),
});
try cli.reset(out, color);
}
// 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);
try cli.printBold(out, color, " Watchlist:\n", .{});
for (watch_symbols) |sym| {
var price_str: [16]u8 = undefined;
const ps: []const u8 = if (watch_prices.get(sym)) |close|
@ -530,9 +479,7 @@ pub fn display(
if (tr.three_year) |metrics| {
if (!any_risk) {
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print(" Risk Metrics (3-Year, monthly returns):\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, " Risk Metrics (3-Year, monthly returns):\n", .{});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:>6} {s:>10} {s:>8} {s:>10}\n", .{
"Symbol", "Volatility", "Sharpe", "Max DD",
@ -546,9 +493,7 @@ pub fn display(
try out.print(" {s:>6} {d:>9.1}% {d:>8.2} ", .{
a.display_symbol, metrics.volatility * 100.0, metrics.sharpe,
});
try cli.setFg(out, color, cli.CLR_NEGATIVE);
try out.print("{d:>9.1}%", .{metrics.max_drawdown * 100.0});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_NEGATIVE, "{d:>9.1}%", .{metrics.max_drawdown * 100.0});
try out.print("\n", .{});
}
}
@ -576,17 +521,11 @@ pub fn printLotRow(out: *std.Io.Writer, color: bool, lot: zfin.Lot, current_pric
var lot_mv_buf: [24]u8 = undefined;
const lot_mv = fmt.fmtMoneyAbs(&lot_mv_buf, lot.shares * use_price);
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} {s:>10} {s:>16} ", .{
try cli.printFg(out, color, cli.CLR_MUTED, " " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} {s:>10} {s:>16} ", .{
status_str, lot.shares, fmt.fmtMoneyAbs(&lot_price_buf, lot.open_price), "", lot_mv,
});
try cli.reset(out, color);
try cli.setGainLoss(out, color, gl);
try out.print("{s}{s:>13}", .{ lot_sign, lot_gl_money });
try cli.reset(out, color);
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:>8} {s} {s} {s}\n", .{ "", date_str, indicator, acct_col });
try cli.reset(out, color);
try cli.printGainLoss(out, color, gl, "{s}{s:>13}", .{ lot_sign, lot_gl_money });
try cli.printFg(out, color, cli.CLR_MUTED, " {s:>8} {s} {s} {s}\n", .{ "", date_str, indicator, acct_col });
}
// Tests

View file

@ -130,14 +130,12 @@ pub fn run(
const comparison = ctx.comparison;
try out.print("\n", .{});
try cli.setBold(out, color);
if (resolution) |r| {
var buf: [10]u8 = undefined;
try out.print("Projections (as of {s})\n", .{r.actual.format(&buf)});
try cli.printBold(out, color, "Projections (as of {s})\n", .{r.actual.format(&buf)});
} else {
try out.print("Projections ({s})\n", .{file_path});
try cli.printBold(out, color, "Projections ({s})\n", .{file_path});
}
try cli.reset(out, color);
try out.print("========================================\n", .{});
// If auto-snapped, print a muted note so the user knows the
@ -147,24 +145,20 @@ pub fn run(
const diff = r.requested.days - r.actual.days;
var req_buf: [10]u8 = undefined;
var act_buf: [10]u8 = undefined;
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("(requested {s}; nearest snapshot: {s}, {d} day{s} earlier)\n", .{
try cli.printFg(out, color, cli.CLR_MUTED, "(requested {s}; nearest snapshot: {s}, {d} day{s} earlier)\n", .{
r.requested.format(&req_buf),
r.actual.format(&act_buf),
diff,
fmt.dayPlural(diff),
});
try cli.reset(out, color);
}
}
try out.print("\n", .{});
// Header row
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("{s: <32}{s: >8}{s: >9}{s: >9}{s: >10}{s: >9}\n", .{
try cli.printFg(out, color, cli.CLR_MUTED, "{s: <32}{s: >8}{s: >9}{s: >9}{s: >10}{s: >9}\n", .{
"", "1 Year", "3 Year", "5 Year", "10 Year", "Week",
});
try cli.reset(out, color);
// Build return rows via view model
var spy_bufs: [5][16]u8 = undefined;
@ -208,9 +202,7 @@ pub fn run(
{
var buf: [16]u8 = undefined;
const cell = view.fmtReturnCell(&buf, comparison.conservative_return);
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("{s: <32}{s: >8}\n", .{ "Conservative estimate", cell.text });
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, "{s: <32}{s: >8}\n", .{ "Conservative estimate", cell.text });
}
// Target allocation note
@ -218,9 +210,7 @@ pub fn run(
var note_buf: [128]u8 = undefined;
if (view.fmtAllocationNote(&note_buf, ctx.config.target_stock_pct, ctx.stock_pct)) |note| {
try out.print("\n", .{});
try cli.setStyleIntent(out, color, note.style);
try out.print("{s}\n", .{note.text});
try cli.reset(out, color);
try cli.printIntent(out, color, note.style, "{s}\n", .{note.text});
}
}
@ -230,9 +220,7 @@ pub fn run(
if (ctx.data.bands[last_idx]) |b| {
if (b.len >= 2) {
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print("Median Portfolio Value ({d}-Year, 99% withdrawal)\n\n", .{horizons[last_idx]});
try cli.reset(out, color);
try cli.printBold(out, color, "Median Portfolio Value ({d}-Year, 99% withdrawal)\n\n", .{horizons[last_idx]});
// Synthesize candles from median values
const candles = try va.alloc(zfin.Candle, b.len);
@ -267,9 +255,7 @@ pub fn run(
// Terminal portfolio value
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print("Terminal Portfolio Value (nominal, at 99% withdrawal rate)\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, "Terminal Portfolio Value (nominal, at 99% withdrawal rate)\n", .{});
try out.print("{s}\n", .{try view.buildHeaderRow(va, horizons, view.terminal_col_width)});
@ -277,16 +263,12 @@ pub fn run(
const p_styles = [_]view.StyleIntent{ .muted, .normal, .muted };
for (p_labels, p_styles, 0..) |plabel, pstyle, pi| {
const row = try view.buildPercentileRow(va, plabel, pi, ctx.data.bands, pstyle);
try cli.setStyleIntent(out, color, row.style);
try out.print("{s}\n", .{row.text});
try cli.reset(out, color);
try cli.printIntent(out, color, row.style, "{s}\n", .{row.text});
}
// Safe withdrawal table
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print("Safe Withdrawal (FIRECalc historical simulation)\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, "Safe Withdrawal (FIRECalc historical simulation)\n", .{});
// Header row
try out.print("{s}\n", .{try view.buildHeaderRow(va, horizons, view.withdrawal_col_width)});
@ -295,9 +277,7 @@ pub fn run(
for (confidence_levels, 0..) |conf, ci| {
const wr_rows = try view.buildWithdrawalRows(va, conf, horizons, ctx.data.withdrawals, ci);
try out.print("{s}\n", .{wr_rows.amount.text});
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print("{s}\n", .{wr_rows.rate.text});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, "{s}\n", .{wr_rows.rate.text});
}
// Life events summary as-of mode uses ages-as-of-as_of; live
@ -309,14 +289,10 @@ pub fn run(
const ages_ref_date = if (resolution) |r| r.actual else fmt.todayDate();
const ages = ctx.config.currentAgesAsOf(ages_ref_date);
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print("Life Events\n", .{});
try cli.reset(out, color);
try cli.printBold(out, color, "Life Events\n", .{});
for (events) |*ev| {
const line = try view.fmtEventLine(va, ev, &ages);
try cli.setStyleIntent(out, color, line.style);
try out.print("{s}\n", .{line.text});
try cli.reset(out, color);
try cli.printIntent(out, color, line.style, "{s}\n", .{line.text});
}
}
}
@ -386,14 +362,12 @@ fn writeReturnRow(out: *std.Io.Writer, color: bool, row: view.ReturnRow) !void {
}
fn writeCell(out: *std.Io.Writer, color: bool, cell: view.ReturnCell, width: usize) !void {
try cli.setStyleIntent(out, color, cell.style);
switch (width) {
8 => try out.print("{s: >8}", .{cell.text}),
9 => try out.print("{s: >9}", .{cell.text}),
10 => try out.print("{s: >10}", .{cell.text}),
else => try out.print("{s}", .{cell.text}),
8 => try cli.printIntent(out, color, cell.style, "{s: >8}", .{cell.text}),
9 => try cli.printIntent(out, color, cell.style, "{s: >9}", .{cell.text}),
10 => try cli.printIntent(out, color, cell.style, "{s: >10}", .{cell.text}),
else => try cli.printIntent(out, color, cell.style, "{s}", .{cell.text}),
}
try cli.reset(out, color);
}
// Tests

View file

@ -86,9 +86,7 @@ pub fn display(allocator: std.mem.Allocator, candles: []const zfin.Candle, quote
const change = price - prev_close;
const pct = (change / prev_close) * 100.0;
var chg_buf: [64]u8 = undefined;
try cli.setGainLoss(out, color, change);
try out.print(" Change: {s}\n", .{fmt.fmtPriceChange(&chg_buf, change, pct)});
try cli.reset(out, color);
try cli.printGainLoss(out, color, change, " Change: {s}\n", .{fmt.fmtPriceChange(&chg_buf, change, pct)});
}
}
@ -107,22 +105,16 @@ pub fn display(allocator: std.mem.Allocator, candles: []const zfin.Candle, quote
// Recent history table (last 20 candles)
if (candles.len > 0) {
try out.print("\n", .{});
try cli.setBold(out, color);
try out.print(" Recent History:\n", .{});
try cli.reset(out, color);
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:>12} {s:>10} {s:>10} {s:>10} {s:>10} {s:>12}\n", .{
try cli.printBold(out, color, " Recent History:\n", .{});
try cli.printFg(out, color, cli.CLR_MUTED, " {s:>12} {s:>10} {s:>10} {s:>10} {s:>10} {s:>12}\n", .{
"Date", "Open", "High", "Low", "Close", "Volume",
});
try cli.reset(out, color);
const start_idx = if (candles.len > 20) candles.len - 20 else 0;
for (candles[start_idx..]) |candle| {
var row_buf: [128]u8 = undefined;
const day_gain = candle.close >= candle.open;
try cli.setGainLoss(out, color, if (day_gain) 1.0 else -1.0);
try out.print("{s}\n", .{fmt.fmtCandleRow(&row_buf, candle)});
try cli.reset(out, color);
try cli.printGainLoss(out, color, if (day_gain) 1.0 else -1.0, "{s}\n", .{fmt.fmtCandleRow(&row_buf, candle)});
}
try out.print("\n {d} trading days shown\n", .{candles[start_idx..].len});
}

View file

@ -22,15 +22,11 @@ pub fn run(svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io
}
pub fn display(splits: []const zfin.Split, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
try cli.setBold(out, color);
try out.print("\nSplit History for {s}\n", .{symbol});
try cli.reset(out, color);
try cli.printBold(out, color, "\nSplit History for {s}\n", .{symbol});
try out.print("========================================\n", .{});
if (splits.len == 0) {
try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" No splits found.\n\n", .{});
try cli.reset(out, color);
try cli.printFg(out, color, cli.CLR_MUTED, " No splits found.\n\n", .{});
return;
}