fix wording on benchmarks
This commit is contained in:
parent
6bd063d51f
commit
eae4299ad3
4 changed files with 97 additions and 20 deletions
|
|
@ -66,6 +66,15 @@ cash, CDs, options, and anything with no `metadata.srf` row -- so a raw
|
||||||
sleeve, and would not be comparable against the portfolio row (which is
|
sleeve, and would not be comparable against the portfolio row (which is
|
||||||
renormalized). Both rows therefore describe invested assets only.
|
renormalized). Both rows therefore describe invested assets only.
|
||||||
|
|
||||||
|
The row labels say so, and they are renormalized to match: a leg reads
|
||||||
|
`SPYM (89.4% of benchmark)`, not its raw portfolio weight, so that
|
||||||
|
multiplying the printed weights by the printed returns reproduces the
|
||||||
|
printed blend. The blend row names the share of the portfolio it covers,
|
||||||
|
`Benchmark (94.9% of portfolio)`, and omits that clause entirely once
|
||||||
|
coverage reaches 99.5%. A leg's raw portfolio weight is still recoverable
|
||||||
|
as `of benchmark x of portfolio` -- here `0.894 x 0.949 = 84.9%`, which
|
||||||
|
is the figure the `Target allocation` line reports.
|
||||||
|
|
||||||
The year columns are **total return** (dividend-reinvested). The `Week`
|
The year columns are **total return** (dividend-reinvested). The `Week`
|
||||||
column is price-only, because dividends over seven days are negligible;
|
column is price-only, because dividends over seven days are negligible;
|
||||||
the table header says so.
|
the table header says so.
|
||||||
|
|
|
||||||
|
|
@ -892,27 +892,36 @@ pub fn runBands(
|
||||||
"", "1 Year", "3 Year", "5 Year", "10 Year", "Week",
|
"", "1 Year", "3 Year", "5 Year", "10 Year", "Week",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build return rows via view model
|
// Build return rows via view model. Weights are renormalized to the blend,
|
||||||
|
// matching what `comparison.*_returns` were actually computed from.
|
||||||
|
const bw = view.benchmarkWeights(ctx.stock_pct, ctx.bond_pct);
|
||||||
|
|
||||||
var spy_bufs: [5][16]u8 = undefined;
|
var spy_bufs: [5][16]u8 = undefined;
|
||||||
var spy_label_buf: [32]u8 = undefined;
|
var spy_label_buf: [48]u8 = undefined;
|
||||||
const spy_row = view.buildReturnRow(
|
const spy_row = view.buildReturnRow(
|
||||||
view.fmtBenchmarkLabel(&spy_label_buf, ctx.config.benchmarkStock(), ctx.stock_pct * 100),
|
view.fmtBenchmarkLabel(&spy_label_buf, ctx.config.benchmarkStock(), bw.stock),
|
||||||
comparison.stock_returns,
|
comparison.stock_returns,
|
||||||
&spy_bufs,
|
&spy_bufs,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
var agg_bufs: [5][16]u8 = undefined;
|
var agg_bufs: [5][16]u8 = undefined;
|
||||||
var agg_label_buf: [32]u8 = undefined;
|
var agg_label_buf: [48]u8 = undefined;
|
||||||
const agg_row = view.buildReturnRow(
|
const agg_row = view.buildReturnRow(
|
||||||
view.fmtBenchmarkLabel(&agg_label_buf, ctx.config.benchmarkBond(), ctx.bond_pct * 100),
|
view.fmtBenchmarkLabel(&agg_label_buf, ctx.config.benchmarkBond(), bw.bond),
|
||||||
comparison.bond_returns,
|
comparison.bond_returns,
|
||||||
&agg_bufs,
|
&agg_bufs,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
var bench_bufs: [5][16]u8 = undefined;
|
var bench_bufs: [5][16]u8 = undefined;
|
||||||
const bench_row = view.buildReturnRow("Benchmark", comparison.benchmark_returns, &bench_bufs, true);
|
var bench_label_buf: [48]u8 = undefined;
|
||||||
|
const bench_row = view.buildReturnRow(
|
||||||
|
view.fmtBenchmarkAggregateLabel(&bench_label_buf, bw.covered),
|
||||||
|
comparison.benchmark_returns,
|
||||||
|
&bench_bufs,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
var port_bufs: [5][16]u8 = undefined;
|
var port_bufs: [5][16]u8 = undefined;
|
||||||
const port_row = view.buildReturnRow("Your Portfolio", comparison.portfolio_returns, &port_bufs, true);
|
const port_row = view.buildReturnRow("Your Portfolio", comparison.portfolio_returns, &port_bufs, true);
|
||||||
|
|
|
||||||
|
|
@ -1137,11 +1137,13 @@ fn buildHeaderSection(state: *State, app: *App, arena: std.mem.Allocator, lines:
|
||||||
.style = th.headerStyle(),
|
.style = th.headerStyle(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return rows
|
// Return rows. Weights renormalized to the blend - see `benchmarkWeights`.
|
||||||
|
const bw = view.benchmarkWeights(stock_pct, pctx.bond_pct);
|
||||||
|
|
||||||
var spy_bufs: [5][16]u8 = undefined;
|
var spy_bufs: [5][16]u8 = undefined;
|
||||||
var spy_label_buf: [32]u8 = undefined;
|
var spy_label_buf: [48]u8 = undefined;
|
||||||
const spy_row = view.buildReturnRow(
|
const spy_row = view.buildReturnRow(
|
||||||
view.fmtBenchmarkLabel(&spy_label_buf, config.benchmarkStock(), stock_pct * 100),
|
view.fmtBenchmarkLabel(&spy_label_buf, config.benchmarkStock(), bw.stock),
|
||||||
comparison.stock_returns,
|
comparison.stock_returns,
|
||||||
&spy_bufs,
|
&spy_bufs,
|
||||||
false,
|
false,
|
||||||
|
|
@ -1149,9 +1151,9 @@ fn buildHeaderSection(state: *State, app: *App, arena: std.mem.Allocator, lines:
|
||||||
try appendReturnRow(lines, arena, th, spy_row);
|
try appendReturnRow(lines, arena, th, spy_row);
|
||||||
|
|
||||||
var agg_bufs: [5][16]u8 = undefined;
|
var agg_bufs: [5][16]u8 = undefined;
|
||||||
var agg_label_buf: [32]u8 = undefined;
|
var agg_label_buf: [48]u8 = undefined;
|
||||||
const agg_row = view.buildReturnRow(
|
const agg_row = view.buildReturnRow(
|
||||||
view.fmtBenchmarkLabel(&agg_label_buf, config.benchmarkBond(), pctx.bond_pct * 100),
|
view.fmtBenchmarkLabel(&agg_label_buf, config.benchmarkBond(), bw.bond),
|
||||||
comparison.bond_returns,
|
comparison.bond_returns,
|
||||||
&agg_bufs,
|
&agg_bufs,
|
||||||
false,
|
false,
|
||||||
|
|
@ -1159,7 +1161,13 @@ fn buildHeaderSection(state: *State, app: *App, arena: std.mem.Allocator, lines:
|
||||||
try appendReturnRow(lines, arena, th, agg_row);
|
try appendReturnRow(lines, arena, th, agg_row);
|
||||||
|
|
||||||
var bench_bufs: [5][16]u8 = undefined;
|
var bench_bufs: [5][16]u8 = undefined;
|
||||||
const bench_row = view.buildReturnRow("Benchmark", comparison.benchmark_returns, &bench_bufs, true);
|
var bench_label_buf: [48]u8 = undefined;
|
||||||
|
const bench_row = view.buildReturnRow(
|
||||||
|
view.fmtBenchmarkAggregateLabel(&bench_label_buf, bw.covered),
|
||||||
|
comparison.benchmark_returns,
|
||||||
|
&bench_bufs,
|
||||||
|
true,
|
||||||
|
);
|
||||||
try appendReturnRow(lines, arena, th, bench_row);
|
try appendReturnRow(lines, arena, th, bench_row);
|
||||||
|
|
||||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
|
|
@ -1887,11 +1895,13 @@ fn buildLines(state: *State, app: *App, arena: std.mem.Allocator) ![]const Style
|
||||||
.style = th.headerStyle(),
|
.style = th.headerStyle(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return rows
|
// Return rows. Weights renormalized to the blend - see `benchmarkWeights`.
|
||||||
|
const bw = view.benchmarkWeights(stock_pct, ctx.bond_pct);
|
||||||
|
|
||||||
var spy_bufs: [5][16]u8 = undefined;
|
var spy_bufs: [5][16]u8 = undefined;
|
||||||
var spy_label_buf: [32]u8 = undefined;
|
var spy_label_buf: [48]u8 = undefined;
|
||||||
const spy_row = view.buildReturnRow(
|
const spy_row = view.buildReturnRow(
|
||||||
view.fmtBenchmarkLabel(&spy_label_buf, config.benchmarkStock(), stock_pct * 100),
|
view.fmtBenchmarkLabel(&spy_label_buf, config.benchmarkStock(), bw.stock),
|
||||||
comparison.stock_returns,
|
comparison.stock_returns,
|
||||||
&spy_bufs,
|
&spy_bufs,
|
||||||
false,
|
false,
|
||||||
|
|
@ -1899,9 +1909,9 @@ fn buildLines(state: *State, app: *App, arena: std.mem.Allocator) ![]const Style
|
||||||
try appendReturnRow(&lines, arena, th, spy_row);
|
try appendReturnRow(&lines, arena, th, spy_row);
|
||||||
|
|
||||||
var agg_bufs: [5][16]u8 = undefined;
|
var agg_bufs: [5][16]u8 = undefined;
|
||||||
var agg_label_buf: [32]u8 = undefined;
|
var agg_label_buf: [48]u8 = undefined;
|
||||||
const agg_row = view.buildReturnRow(
|
const agg_row = view.buildReturnRow(
|
||||||
view.fmtBenchmarkLabel(&agg_label_buf, config.benchmarkBond(), ctx.bond_pct * 100),
|
view.fmtBenchmarkLabel(&agg_label_buf, config.benchmarkBond(), bw.bond),
|
||||||
comparison.bond_returns,
|
comparison.bond_returns,
|
||||||
&agg_bufs,
|
&agg_bufs,
|
||||||
false,
|
false,
|
||||||
|
|
@ -1909,7 +1919,13 @@ fn buildLines(state: *State, app: *App, arena: std.mem.Allocator) ![]const Style
|
||||||
try appendReturnRow(&lines, arena, th, agg_row);
|
try appendReturnRow(&lines, arena, th, agg_row);
|
||||||
|
|
||||||
var bench_bufs: [5][16]u8 = undefined;
|
var bench_bufs: [5][16]u8 = undefined;
|
||||||
const bench_row = view.buildReturnRow("Benchmark", comparison.benchmark_returns, &bench_bufs, true);
|
var bench_label_buf: [48]u8 = undefined;
|
||||||
|
const bench_row = view.buildReturnRow(
|
||||||
|
view.fmtBenchmarkAggregateLabel(&bench_label_buf, bw.covered),
|
||||||
|
comparison.benchmark_returns,
|
||||||
|
&bench_bufs,
|
||||||
|
true,
|
||||||
|
);
|
||||||
try appendReturnRow(&lines, arena, th, bench_row);
|
try appendReturnRow(&lines, arena, th, bench_row);
|
||||||
|
|
||||||
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
||||||
|
|
|
||||||
|
|
@ -144,9 +144,52 @@ pub fn fmtAllocationNote(buf: []u8, target_stock_pct: ?f64, current_stock_pct: f
|
||||||
return .{ .text = text, .style = style };
|
return .{ .text = text, .style = style };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format the stock benchmark label with weight.
|
/// Benchmark weights as the blend actually uses them.
|
||||||
|
///
|
||||||
|
/// `deriveAllocationSplit` returns stock/bond as fractions of the WHOLE
|
||||||
|
/// portfolio, so they do not sum to 1 - the remainder is cash, CDs, options and
|
||||||
|
/// anything with no `metadata.srf` row. `blendReturns` then renormalizes to the
|
||||||
|
/// weight present (`benchmark.blendOptional`), which is what stops a cash sleeve
|
||||||
|
/// dragging the benchmark down against a portfolio return that excludes it too.
|
||||||
|
///
|
||||||
|
/// The labels have to agree with that arithmetic. Printing the raw 84.9%/10.0%
|
||||||
|
/// beside a row computed from 89.5%/10.5% invites the reader to multiply out and
|
||||||
|
/// get 17.14% where the row says 18.06%. Both numbers are correct and the
|
||||||
|
/// mismatch is entirely in the caption - the same trap that already cost an hour
|
||||||
|
/// on the `--as-of` table and is why `review` prints that table's date.
|
||||||
|
///
|
||||||
|
/// Nothing is lost by renormalizing the caption: the raw portfolio weight of
|
||||||
|
/// either leg is `stock/100 * covered`.
|
||||||
|
pub const BenchmarkWeights = struct {
|
||||||
|
/// Share of the BENCHMARK. Sums to 100 with `bond`.
|
||||||
|
stock: f64,
|
||||||
|
bond: f64,
|
||||||
|
/// Share of the PORTFOLIO the benchmark covers.
|
||||||
|
covered: f64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn benchmarkWeights(stock_pct: f64, bond_pct: f64) BenchmarkWeights {
|
||||||
|
const w = stock_pct + bond_pct;
|
||||||
|
if (w <= 0) return .{ .stock = 0, .bond = 0, .covered = 0 };
|
||||||
|
return .{
|
||||||
|
.stock = stock_pct / w * 100.0,
|
||||||
|
.bond = bond_pct / w * 100.0,
|
||||||
|
.covered = w * 100.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format the benchmark leg label with its weight *within the benchmark*.
|
||||||
pub fn fmtBenchmarkLabel(buf: []u8, symbol: []const u8, weight_pct: f64) []const u8 {
|
pub fn fmtBenchmarkLabel(buf: []u8, symbol: []const u8, weight_pct: f64) []const u8 {
|
||||||
return std.fmt.bufPrint(buf, "{s} ({d:.1}% weight)", .{ symbol, weight_pct }) catch symbol;
|
return std.fmt.bufPrint(buf, "{s} ({d:.1}% of benchmark)", .{ symbol, weight_pct }) catch symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Label for the blended row. Discloses how much of the portfolio the blend
|
||||||
|
/// covers, but only when it is materially short of all of it - on a fully
|
||||||
|
/// classified stock/bond portfolio there is nothing to disclose and the bare
|
||||||
|
/// word is less noise.
|
||||||
|
pub fn fmtBenchmarkAggregateLabel(buf: []u8, covered_pct: f64) []const u8 {
|
||||||
|
if (covered_pct >= 99.5) return "Benchmark";
|
||||||
|
return std.fmt.bufPrint(buf, "Benchmark ({d:.1}% of portfolio)", .{covered_pct}) catch "Benchmark";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Precomputed projection data (shared by CLI and TUI) ────────
|
// ── Precomputed projection data (shared by CLI and TUI) ────────
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue