make ux more useful

This commit is contained in:
Emil Lerch 2026-04-10 08:38:51 -07:00
parent 6fb582e3da
commit c3c9c21556
Signed by: lobo
GPG key ID: A7B62D657EF764F8

View file

@ -521,14 +521,17 @@ pub fn compareSchwabSummary(
fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, out: *std.Io.Writer) !void { fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, out: *std.Io.Writer) !void {
try cli.setBold(out, color); try cli.setBold(out, color);
try out.print("\nSchwab Account Audit (summary)\n", .{}); 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.reset(out, color);
try out.print("========================================\n\n", .{}); try out.print("========================================\n\n", .{});
// Column headers // Column headers
try cli.setFg(out, color, cli.CLR_MUTED); try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:<24} {s:>14} {s:>14} {s:>14} {s:>14} {s}\n", .{ try out.print(" {s:<24} {s:>14} {s:>14} {s:>14} {s:>14}\n", .{
"Account", "PF Cash", "BR Cash", "PF Total", "BR Total", "Status", "Account", "PF Cash", "BR Cash", "PF Total", "BR Total",
}); });
try cli.reset(out, color); try cli.reset(out, color);
@ -553,49 +556,70 @@ fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, o
else else
"--"; "--";
var status_buf: [64]u8 = undefined; const cash_ok = if (r.cash_delta) |d| @abs(d) < 1.0 else true;
const status: []const u8 = blk: { const total_ok = if (r.total_delta) |d| @abs(d) < 1.0 else true;
if (r.account_name.len == 0) break :blk "Unmapped"; const is_unmapped = r.account_name.len == 0;
const is_real_mismatch = !cash_ok or is_unmapped;
const cash_ok = if (r.cash_delta) |d| @abs(d) < 1.0 else true; if (is_real_mismatch) discrepancy_count += 1;
const total_ok = if (r.total_delta) |d| @abs(d) < 1.0 else true;
if (cash_ok and total_ok) break :blk "ok"; // Account label
try out.print(" ", .{});
if (!cash_ok) { if (is_unmapped) {
if (r.cash_delta) |d| {
var delta_buf: [24]u8 = undefined;
const sign: []const u8 = if (d >= 0) "+" else "-";
break :blk std.fmt.bufPrint(&status_buf, "Cash {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) }) catch "Cash mismatch";
}
}
if (!total_ok) {
if (r.total_delta) |d| {
var delta_buf: [24]u8 = undefined;
const sign: []const u8 = if (d >= 0) "+" else "-";
break :blk std.fmt.bufPrint(&status_buf, "Total {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) }) catch "Total mismatch";
}
}
break :blk "Mismatch";
};
const is_ok = std.mem.eql(u8, status, "ok");
if (!is_ok) discrepancy_count += 1;
try out.print(" {s:<24} {s:>14} {s:>14} {s:>14} {s:>14} ", .{
label,
fmt.fmtMoneyAbs(&pf_cash_buf, r.portfolio_cash),
br_cash_str,
fmt.fmtMoneyAbs(&pf_total_buf, r.portfolio_total),
br_total_str,
});
if (is_ok) {
try cli.setFg(out, color, cli.CLR_POSITIVE);
} else {
try cli.setFg(out, color, cli.CLR_WARNING); try cli.setFg(out, color, cli.CLR_WARNING);
} }
try out.print("{s}\n", .{status}); try out.print("{s:<24}", .{label});
try cli.reset(out, color); 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);
}
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});
// 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);
}
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);
} 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);
} 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 out.print("\n", .{});
grand_pf += r.portfolio_total; grand_pf += r.portfolio_total;
if (r.schwab_total) |t| grand_br += t; if (r.schwab_total) |t| grand_br += t;
@ -616,23 +640,24 @@ fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, o
try cli.reset(out, color); try cli.reset(out, color);
if (@abs(grand_delta) < 1.0) { if (@abs(grand_delta) < 1.0) {
try cli.setFg(out, color, cli.CLR_POSITIVE); // no delta
try out.print(" ok", .{});
} else { } else {
try cli.setFg(out, color, cli.CLR_WARNING);
const sign: []const u8 = if (grand_delta >= 0) "+" 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 out.print(" delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&grand_delta_buf, @abs(grand_delta)) });
try cli.reset(out, color);
} }
try cli.reset(out, color);
try out.print("\n", .{}); try out.print("\n", .{});
if (discrepancy_count > 0) { if (discrepancy_count > 0) {
try cli.setFg(out, color, cli.CLR_WARNING); try cli.setFg(out, color, cli.CLR_WARNING);
try out.print(" {d} discrepancies — drill down with: zfin audit --schwab <account.csv>\n", .{discrepancy_count}); try out.print(" {d} {s} — drill down with: zfin audit --schwab <account.csv>\n", .{
try cli.reset(out, color); discrepancy_count, if (discrepancy_count == 1) @as([]const u8, "mismatch") else @as([]const u8, "mismatches"),
} else { });
try cli.setFg(out, color, cli.CLR_POSITIVE);
try out.print(" All accounts match\n", .{});
try cli.reset(out, color); try cli.reset(out, color);
} }
try out.print("\n", .{}); try out.print("\n", .{});
@ -848,7 +873,10 @@ pub fn compareAccounts(
fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.Writer) !void { fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.Writer) !void {
try cli.setBold(out, color); try cli.setBold(out, color);
try out.print("\nPortfolio Audit\n", .{}); 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.reset(out, color);
try out.print("========================================\n\n", .{}); try out.print("========================================\n\n", .{});
@ -875,7 +903,7 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.
// Column headers // Column headers
try cli.setFg(out, color, cli.CLR_MUTED); try cli.setFg(out, color, cli.CLR_MUTED);
try out.print(" {s:<24} {s:>12} {s:>12} {s:>10} {s:>10} {s}\n", .{ try out.print(" {s:<24} {s:>12} {s:>12} {s:>10} {s:>10} {s}\n", .{
"Symbol", "PF Shares", "BR Shares", "PF Price", "BR Price", "Status", "Symbol", "PF Shares", "BR Shares", "PF Price", "BR Price", "",
}); });
try cli.reset(out, color); try cli.reset(out, color);
@ -885,42 +913,66 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.
var pf_price_buf: [16]u8 = undefined; var pf_price_buf: [16]u8 = undefined;
var br_price_buf: [16]u8 = undefined; var br_price_buf: [16]u8 = undefined;
const pf_shares_str = if (cmp.is_cash) // Classify this row
"--" const shares_ok = if (cmp.shares_delta) |d| @abs(d) < 0.01 else !cmp.only_in_brokerage;
const is_cash_mismatch = cmp.is_cash and (if (cmp.value_delta) |d| @abs(d) >= 1.0 else false);
const is_real_mismatch = !shares_ok or cmp.only_in_brokerage or cmp.only_in_portfolio or is_cash_mismatch;
if (is_real_mismatch) discrepancy_count += 1;
// Format share strings
const pf_shares_str: []const u8 = if (cmp.is_cash)
(fmt.fmtMoneyAbs(&pf_shares_buf, cmp.portfolio_value))
else else
std.fmt.bufPrint(&pf_shares_buf, "{d:.3}", .{cmp.portfolio_shares}) catch "?"; std.fmt.bufPrint(&pf_shares_buf, "{d:.3}", .{cmp.portfolio_shares}) catch "?";
const br_shares_str = if (cmp.is_cash) const br_shares_str: []const u8 = if (cmp.is_cash)
"--" (if (cmp.brokerage_value) |v| fmt.fmtMoneyAbs(&br_shares_buf, v) else "--")
else if (cmp.brokerage_shares) |s| else if (cmp.brokerage_shares) |s|
(std.fmt.bufPrint(&br_shares_buf, "{d:.3}", .{s}) catch "?") (std.fmt.bufPrint(&br_shares_buf, "{d:.3}", .{s}) catch "?")
else else
"--"; "--";
const pf_price_str: []const u8 = if (cmp.is_cash) const pf_price_str: []const u8 = if (cmp.is_cash)
"--" ""
else if (cmp.portfolio_price) |p| else if (cmp.portfolio_price) |p|
(std.fmt.bufPrint(&pf_price_buf, "{d:.2}", .{p}) catch "?") (std.fmt.bufPrint(&pf_price_buf, "{d:.2}", .{p}) catch "?")
else else
"--"; "--";
const br_price_str: []const u8 = if (cmp.is_cash) const br_price_str: []const u8 = if (cmp.is_cash)
"--" ""
else if (cmp.brokerage_price) |p| else if (cmp.brokerage_price) |p|
(std.fmt.bufPrint(&br_price_buf, "{d:.2}", .{p}) catch "?") (std.fmt.bufPrint(&br_price_buf, "{d:.2}", .{p}) catch "?")
else else
"--"; "--";
// Determine status // Determine PF Shares color (relative to brokerage)
const shares_color: enum { normal, positive, negative, warning } = blk: {
if (cmp.only_in_portfolio) break :blk .warning;
if (cmp.is_cash and is_cash_mismatch) {
break :blk if (cmp.value_delta.? > 0) .negative else .positive;
}
if (shares_ok) break :blk .normal;
if (cmp.shares_delta) |d| {
break :blk if (d > 0) .negative else .positive;
}
break :blk .normal;
};
// Determine status text
var status_buf: [64]u8 = undefined; var status_buf: [64]u8 = undefined;
const status: []const u8 = blk: { const status: []const u8 = blk: {
if (cmp.only_in_brokerage) break :blk "Brokerage only"; if (cmp.only_in_brokerage) break :blk "Brokerage only";
if (cmp.only_in_portfolio) break :blk "Portfolio only"; if (cmp.only_in_portfolio) break :blk "Portfolio only";
const shares_ok = if (cmp.shares_delta) |d| @abs(d) < 0.01 else true; if (is_cash_mismatch) {
const value_ok = if (cmp.value_delta) |d| @abs(d) < 1.0 else true; if (cmp.value_delta) |d| {
var delta_buf: [24]u8 = undefined;
if (shares_ok and value_ok) break :blk "ok"; const sign: []const u8 = if (d >= 0) "+" else "-";
break :blk std.fmt.bufPrint(&status_buf, "Cash {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) }) catch "Cash mismatch";
}
}
if (!shares_ok) { if (!shares_ok) {
if (cmp.shares_delta) |d| { if (cmp.shares_delta) |d| {
@ -928,31 +980,50 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.
break :blk std.fmt.bufPrint(&status_buf, "Shares {s}{d:.3}", .{ sign, d }) catch "Shares mismatch"; break :blk std.fmt.bufPrint(&status_buf, "Shares {s}{d:.3}", .{ sign, d }) catch "Shares mismatch";
} }
} }
if (!value_ok) {
if (cmp.value_delta) |d| { // Shares match show value delta (stale price) if any, muted
if (cmp.value_delta) |d| {
if (@abs(d) >= 1.0) {
var delta_buf: [24]u8 = undefined; var delta_buf: [24]u8 = undefined;
const delta_str = fmt.fmtMoneyAbs(&delta_buf, @abs(d));
const sign: []const u8 = if (d >= 0) "+" else "-"; const sign: []const u8 = if (d >= 0) "+" else "-";
break :blk std.fmt.bufPrint(&status_buf, "Value {s}{s}", .{ sign, delta_str }) catch "Value mismatch"; break :blk std.fmt.bufPrint(&status_buf, "Value {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) }) catch "";
} }
} }
break :blk "Mismatch";
break :blk "";
}; };
// Color the status // Status color: real mismatches in warning, stale values muted, ok is blank
const is_ok = std.mem.eql(u8, status, "ok"); const status_color: enum { warning, muted, none } = blk: {
if (!is_ok) discrepancy_count += 1; if (is_real_mismatch) break :blk .warning;
if (status.len > 0) break :blk .muted;
break :blk .none;
};
try out.print(" {s:<24} {s:>12} {s:>12} {s:>10} {s:>10} ", .{ // Print symbol
cmp.symbol, pf_shares_str, br_shares_str, pf_price_str, br_price_str, try out.print(" {s:<24} ", .{cmp.symbol});
});
if (is_ok) { // Print PF Shares with color
try cli.setFg(out, color, cli.CLR_POSITIVE); switch (shares_color) {
} else { .positive => try cli.setFg(out, color, cli.CLR_POSITIVE),
try cli.setFg(out, color, cli.CLR_WARNING); .negative => try cli.setFg(out, color, cli.CLR_NEGATIVE),
.warning => try cli.setFg(out, color, cli.CLR_WARNING),
.normal => {},
}
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 => {},
} }
try out.print("{s}\n", .{status}); try out.print("{s}\n", .{status});
try cli.reset(out, color); if (status_color != .none) try cli.reset(out, color);
} }
// Account totals // Account totals
@ -965,15 +1036,19 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.
}); });
try cli.reset(out, color); try cli.reset(out, color);
if (@abs(acct.total_delta) < 1.0) { const delta = acct.total_delta;
try cli.setFg(out, color, cli.CLR_POSITIVE); if (@abs(delta) < 1.0) {
try out.print("ok\n", .{}); try out.print("\n", .{});
} else { } else {
try cli.setFg(out, color, cli.CLR_WARNING); const sign: []const u8 = if (delta >= 0) "+" else "-";
const sign: []const u8 = if (acct.total_delta >= 0) "+" else "-"; if (delta >= 0) {
try out.print("Delta {s}{s}\n", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(acct.total_delta)) }); try cli.setFg(out, color, cli.CLR_NEGATIVE);
} else {
try cli.setFg(out, color, cli.CLR_POSITIVE);
}
try out.print("Delta {s}{s}\n", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(delta)) });
try cli.reset(out, color);
} }
try cli.reset(out, color);
try out.print("\n", .{}); try out.print("\n", .{});
total_portfolio += acct.portfolio_total; total_portfolio += acct.portfolio_total;
@ -994,23 +1069,22 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.
try cli.reset(out, color); try cli.reset(out, color);
if (@abs(grand_delta) < 1.0) { if (@abs(grand_delta) < 1.0) {
try cli.setFg(out, color, cli.CLR_POSITIVE); // no delta text needed
try out.print(" ok", .{});
} else { } else {
try cli.setFg(out, color, cli.CLR_WARNING);
const sign: []const u8 = if (grand_delta >= 0) "+" 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 out.print(" delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&grand_delta_buf, @abs(grand_delta)) });
try cli.reset(out, color);
} }
try cli.reset(out, color);
try out.print("\n", .{}); try out.print("\n", .{});
if (discrepancy_count > 0) { if (discrepancy_count > 0) {
try cli.setFg(out, color, cli.CLR_WARNING); try cli.setFg(out, color, cli.CLR_WARNING);
try out.print(" {d} discrepancies found\n", .{discrepancy_count}); 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);
} else {
try cli.setFg(out, color, cli.CLR_POSITIVE);
try out.print(" All positions match\n", .{});
try cli.reset(out, color); try cli.reset(out, color);
} }
try out.print("\n", .{}); try out.print("\n", .{});