make ux more useful
This commit is contained in:
parent
6fb582e3da
commit
c3c9c21556
1 changed files with 170 additions and 96 deletions
|
|
@ -521,14 +521,17 @@ 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 (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 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} {s}\n", .{
|
||||
"Account", "PF Cash", "BR Cash", "PF Total", "BR Total", "Status",
|
||||
try out.print(" {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);
|
||||
|
||||
|
|
@ -553,49 +556,70 @@ fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, o
|
|||
else
|
||||
"--";
|
||||
|
||||
var status_buf: [64]u8 = undefined;
|
||||
const status: []const u8 = blk: {
|
||||
if (r.account_name.len == 0) break :blk "Unmapped";
|
||||
const cash_ok = if (r.cash_delta) |d| @abs(d) < 1.0 else true;
|
||||
const total_ok = if (r.total_delta) |d| @abs(d) < 1.0 else true;
|
||||
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;
|
||||
const total_ok = if (r.total_delta) |d| @abs(d) < 1.0 else true;
|
||||
if (is_real_mismatch) discrepancy_count += 1;
|
||||
|
||||
if (cash_ok and total_ok) break :blk "ok";
|
||||
|
||||
if (!cash_ok) {
|
||||
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 {
|
||||
// Account label
|
||||
try out.print(" ", .{});
|
||||
if (is_unmapped) {
|
||||
try cli.setFg(out, color, cli.CLR_WARNING);
|
||||
}
|
||||
try out.print("{s}\n", .{status});
|
||||
try cli.reset(out, color);
|
||||
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);
|
||||
}
|
||||
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;
|
||||
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);
|
||||
|
||||
if (@abs(grand_delta) < 1.0) {
|
||||
try cli.setFg(out, color, cli.CLR_POSITIVE);
|
||||
try out.print(" ok", .{});
|
||||
// no delta
|
||||
} else {
|
||||
try cli.setFg(out, color, cli.CLR_WARNING);
|
||||
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);
|
||||
}
|
||||
try cli.reset(out, color);
|
||||
try out.print("\n", .{});
|
||||
|
||||
if (discrepancy_count > 0) {
|
||||
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 cli.reset(out, color);
|
||||
} else {
|
||||
try cli.setFg(out, color, cli.CLR_POSITIVE);
|
||||
try out.print(" All accounts match\n", .{});
|
||||
try out.print(" {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", .{});
|
||||
|
|
@ -848,7 +873,10 @@ pub fn compareAccounts(
|
|||
|
||||
fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.Writer) !void {
|
||||
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 out.print("========================================\n\n", .{});
|
||||
|
||||
|
|
@ -875,7 +903,7 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.
|
|||
// 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", .{
|
||||
"Symbol", "PF Shares", "BR Shares", "PF Price", "BR Price", "Status",
|
||||
"Symbol", "PF Shares", "BR Shares", "PF Price", "BR Price", "",
|
||||
});
|
||||
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 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
|
||||
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|
|
||||
(std.fmt.bufPrint(&br_shares_buf, "{d:.3}", .{s}) catch "?")
|
||||
else
|
||||
"--";
|
||||
|
||||
const pf_price_str: []const u8 = if (cmp.is_cash)
|
||||
"--"
|
||||
""
|
||||
else if (cmp.portfolio_price) |p|
|
||||
(std.fmt.bufPrint(&pf_price_buf, "{d:.2}", .{p}) catch "?")
|
||||
else
|
||||
"--";
|
||||
|
||||
const br_price_str: []const u8 = if (cmp.is_cash)
|
||||
"--"
|
||||
""
|
||||
else if (cmp.brokerage_price) |p|
|
||||
(std.fmt.bufPrint(&br_price_buf, "{d:.2}", .{p}) catch "?")
|
||||
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;
|
||||
const status: []const u8 = blk: {
|
||||
if (cmp.only_in_brokerage) break :blk "Brokerage only";
|
||||
if (cmp.only_in_portfolio) break :blk "Portfolio only";
|
||||
|
||||
const shares_ok = if (cmp.shares_delta) |d| @abs(d) < 0.01 else true;
|
||||
const value_ok = if (cmp.value_delta) |d| @abs(d) < 1.0 else true;
|
||||
|
||||
if (shares_ok and value_ok) break :blk "ok";
|
||||
if (is_cash_mismatch) {
|
||||
if (cmp.value_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 (!shares_ok) {
|
||||
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";
|
||||
}
|
||||
}
|
||||
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;
|
||||
const delta_str = fmt.fmtMoneyAbs(&delta_buf, @abs(d));
|
||||
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
|
||||
const is_ok = std.mem.eql(u8, status, "ok");
|
||||
if (!is_ok) discrepancy_count += 1;
|
||||
// Status color: real mismatches in warning, stale values muted, ok is blank
|
||||
const status_color: enum { warning, muted, none } = blk: {
|
||||
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} ", .{
|
||||
cmp.symbol, pf_shares_str, br_shares_str, pf_price_str, br_price_str,
|
||||
});
|
||||
if (is_ok) {
|
||||
try cli.setFg(out, color, cli.CLR_POSITIVE);
|
||||
} else {
|
||||
try cli.setFg(out, color, cli.CLR_WARNING);
|
||||
// Print symbol
|
||||
try out.print(" {s:<24} ", .{cmp.symbol});
|
||||
|
||||
// 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 => {},
|
||||
}
|
||||
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 cli.reset(out, color);
|
||||
if (status_color != .none) try cli.reset(out, color);
|
||||
}
|
||||
|
||||
// Account totals
|
||||
|
|
@ -965,15 +1036,19 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.
|
|||
});
|
||||
try cli.reset(out, color);
|
||||
|
||||
if (@abs(acct.total_delta) < 1.0) {
|
||||
try cli.setFg(out, color, cli.CLR_POSITIVE);
|
||||
try out.print("ok\n", .{});
|
||||
const delta = acct.total_delta;
|
||||
if (@abs(delta) < 1.0) {
|
||||
try out.print("\n", .{});
|
||||
} else {
|
||||
try cli.setFg(out, color, cli.CLR_WARNING);
|
||||
const sign: []const u8 = if (acct.total_delta >= 0) "+" else "-";
|
||||
try out.print("Delta {s}{s}\n", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(acct.total_delta)) });
|
||||
const sign: []const u8 = if (delta >= 0) "+" else "-";
|
||||
if (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}\n", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(delta)) });
|
||||
try cli.reset(out, color);
|
||||
}
|
||||
try cli.reset(out, color);
|
||||
try out.print("\n", .{});
|
||||
|
||||
total_portfolio += acct.portfolio_total;
|
||||
|
|
@ -994,23 +1069,22 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.
|
|||
try cli.reset(out, color);
|
||||
|
||||
if (@abs(grand_delta) < 1.0) {
|
||||
try cli.setFg(out, color, cli.CLR_POSITIVE);
|
||||
try out.print(" ok", .{});
|
||||
// no delta text needed
|
||||
} else {
|
||||
try cli.setFg(out, color, cli.CLR_WARNING);
|
||||
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);
|
||||
}
|
||||
try cli.reset(out, color);
|
||||
try out.print("\n", .{});
|
||||
|
||||
if (discrepancy_count > 0) {
|
||||
try cli.setFg(out, color, cli.CLR_WARNING);
|
||||
try out.print(" {d} discrepancies found\n", .{discrepancy_count});
|
||||
try cli.reset(out, color);
|
||||
} else {
|
||||
try cli.setFg(out, color, cli.CLR_POSITIVE);
|
||||
try out.print(" All positions match\n", .{});
|
||||
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 out.print("\n", .{});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue