document methodology differences between history and projections

This commit is contained in:
Emil Lerch 2026-05-16 14:37:30 -07:00
parent ad81adf05d
commit a0715b615c
Signed by: lobo
GPG key ID: A7B62D657EF764F8
4 changed files with 26 additions and 4 deletions

View file

@ -360,6 +360,15 @@ pub fn renderPortfolio(
fn renderWindowsBlock(out: *std.Io.Writer, color: bool, ws: timeline.WindowSet) !void {
if (ws.rows.len == 0) return;
// Methodology note. The values in this block are
// snapshot-to-snapshot Liquid deltas (or whichever metric is
// focused) they include contributions, withdrawals, and
// weight drift, distinct from the `projections` benchmark
// table which reports price-only weighted returns and so will
// disagree on weeks with significant cash movement or
// rebalancing.
try cli.printFg(out, color, cli.CLR_MUTED, " (snapshot-to-snapshot Δ; includes contributions, withdrawals, weight drift)\n", .{});
// Header row: " Change Δ % % / yr"
// Widths pinned to view.windows_*_width constants (12 / 18 / 10 / 10).
// Hard-coded here for format-string brevity; changes to those

View file

@ -266,6 +266,19 @@ pub fn run(
}
try out.print("\n", .{});
// Section title. Includes a methodology note so a reader
// comparing these numbers against the `history` tab's window
// table doesn't get tripped up by the (legitimate)
// disagreement: this table reports each row as a weighted
// price-only return per period (per-symbol price change ×
// current weight, summed) so SPY/AGG/Benchmark/Your
// Portfolio rows are apples-to-apples; history's window
// table reports snapshot-to-snapshot Liquid value deltas
// that include contributions, withdrawals, and weight drift.
try cli.setBold(out, color);
try out.print("Benchmark comparison (price-only weighted return)\n", .{});
try cli.reset(out, color);
// Header row
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",

View file

@ -1373,7 +1373,7 @@ fn appendWindowsBlock(
const today = points[points.len - 1].as_of_date;
const ws = try timeline.computeWindowSet(arena, points, metric, today);
try lines.append(arena, .{ .text = " Change", .style = th.headerStyle() });
try lines.append(arena, .{ .text = " Change (snapshot-to-snapshot Δ)", .style = th.headerStyle() });
const header_line = try std.fmt.allocPrint(
arena,
@ -1741,7 +1741,7 @@ test "renderHistoryLines: renders windows + chart + table in correct order" {
var chart_idx: ?usize = null;
var table_idx: ?usize = null;
for (lines, 0..) |l, i| {
if (std.mem.eql(u8, std.mem.trim(u8, l.text, " "), "Change")) windows_idx = i;
if (std.mem.indexOf(u8, l.text, "Change") != null) windows_idx = i;
if (std.mem.indexOf(u8, l.text, "Chart: Liquid") != null) chart_idx = i;
if (std.mem.indexOf(u8, l.text, "Recent snapshots") != null) table_idx = i;
}

View file

@ -829,7 +829,7 @@ fn buildHeaderSection(state: *State, app: *App, arena: std.mem.Allocator, lines:
const stock_pct = pctx.stock_pct;
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
try lines.append(arena, .{ .text = " Benchmark Comparison", .style = th.headerStyle() });
try lines.append(arena, .{ .text = " Benchmark Comparison (price-only weighted return)", .style = th.headerStyle() });
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
// Column headers
@ -1222,7 +1222,7 @@ fn buildLines(state: *State, app: *App, arena: std.mem.Allocator) ![]const Style
// Header
try lines.append(arena, .{
.text = " Benchmark Comparison",
.text = " Benchmark Comparison (price-only weighted return)",
.style = th.headerStyle(),
});
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });