document difference between live and snapshot when on weekends
All checks were successful
Generic zig build / build (push) Successful in 2m4s
Generic zig build / deploy (push) Successful in 17s

This commit is contained in:
Emil Lerch 2026-05-16 15:36:22 -07:00
parent a0715b615c
commit 10be2ee86a
Signed by: lobo
GPG key ID: A7B62D657EF764F8
4 changed files with 55 additions and 46 deletions

30
TODO.md
View file

@ -548,36 +548,6 @@ gain. Possible fixes are discussed in the "Contributions diff" TODO
below — option C there (per-account `cash_is_contribution`) would
make manually-entered ESPP-style cash additions count correctly.
## Investigate: small dollar-value discrepancy between consecutive `compare` runs
**Symptom:** Last week's `zfin compare 1W` reported a "now" Total
Investable Assets that didn't match this week's `zfin compare 1W`
"then" value by a small but non-zero amount (low single-digit
thousands). The two numbers should be identical — both are reading
the same week-ago history snapshot file.
Candidates to investigate:
- Snapshot file mutated after last week's run (re-snapshot? manual
edit?). Check `git log -- history/<date>-portfolio.srf` and
`git diff` against any prior version if tracked.
- Live cache prices changed between runs and the snapshot path is
somehow falling through to live prices for some symbols. The
snapshot SHOULD be self-contained (shares × snapshotted price),
but verify by re-running `zfin compare <date> <date>` (same date
both ends) and confirming the same number both ways.
- Last week's reported "now" was computed against a working-copy
portfolio that was later edited (reconciliation tweak post-run),
while this week's "then" reads the committed snapshot. Cross-check
against any archived report output from last week.
- `price_ratio` or `adj_close` semantics differing between code paths
(the REPORT.md §2 caveat about commit-side using current prices
for DRIP/rollup deltas is a known inconsistency in attribution —
may or may not apply here).
If the source is the working-copy-vs-snapshot mismatch (third bullet),
that's a workflow issue, not a bug — but worth confirming.
## Investigate: detailed 401(k) contributions data source
Found a more detailed contributions screen on at least one

View file

@ -605,7 +605,7 @@ const LiveSide = struct {
fn renderCompare(out: *std.Io.Writer, color: bool, cv: view.CompareView, proj: ?ProjectionsBlock) !void {
var then_buf: [10]u8 = undefined;
var now_buf: [10]u8 = undefined;
var now_buf: [24]u8 = undefined;
const then_str = std.fmt.bufPrint(&then_buf, "{f}", .{cv.then_date}) catch "????-??-??";
const now_str = view.nowLabel(cv, &now_buf);
@ -811,7 +811,7 @@ test "renderCompare: basic output includes expected elements" {
const out = stream.buffered();
// Header
try testing.expect(std.mem.indexOf(u8, out, "2024-01-15 → today") != null);
try testing.expect(std.mem.indexOf(u8, out, "2024-01-15 → 2024-01-25 (live)") != null);
try testing.expect(std.mem.indexOf(u8, out, "(10 days)") != null);
// Totals
try testing.expect(std.mem.indexOf(u8, out, "Liquid:") != null);
@ -826,7 +826,7 @@ test "renderCompare: basic output includes expected elements" {
try testing.expect(std.mem.indexOf(u8, out, "(3 added, 1 removed since 2024-01-15 — hidden)") != null);
}
test "renderCompare: two-snapshot mode shows real date, not 'today'" {
test "renderCompare: two-snapshot mode shows real date, no (live) marker" {
const cv = view.CompareView{
.then_date = Date.fromYmd(2024, 1, 15),
.now_date = Date.fromYmd(2024, 3, 15),
@ -845,7 +845,7 @@ test "renderCompare: two-snapshot mode shows real date, not 'today'" {
const out = stream.buffered();
try testing.expect(std.mem.indexOf(u8, out, "2024-01-15 → 2024-03-15") != null);
try testing.expect(std.mem.indexOf(u8, out, "today") == null);
try testing.expect(std.mem.indexOf(u8, out, "(live)") == null);
try testing.expect(std.mem.indexOf(u8, out, "No symbols held throughout") != null);
// No "hidden" line when both counts are zero
try testing.expect(std.mem.indexOf(u8, out, "hidden") == null);

View file

@ -1549,7 +1549,7 @@ pub fn renderCompareLines(
// Header
var then_buf: [10]u8 = undefined;
var now_buf: [10]u8 = undefined;
var now_buf: [24]u8 = undefined;
const then_iso = std.fmt.bufPrint(&then_buf, "{f}", .{cv.then_date}) catch "????-??-??";
const now_iso = compare_view.nowLabel(cv, &now_buf);
@ -2005,7 +2005,7 @@ test "renderCompareLines: emits header, totals, symbols, hidden-count, footer" {
try testing.expect(saw_footer);
}
test "renderCompareLines: live-now shows 'today' not a date" {
test "renderCompareLines: live-now shows date with (live) marker" {
var arena = std.heap.ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const a = arena.allocator();
@ -2024,13 +2024,13 @@ test "renderCompareLines: live-now shows 'today' not a date" {
};
const lines = try renderCompareLines(a, th, cv, " Esc or 'c' to return to timeline");
var saw_today = false;
var saw_live_marker = false;
var saw_no_symbols = false;
for (lines) |l| {
if (std.mem.indexOf(u8, l.text, "→ today") != null) saw_today = true;
if (std.mem.indexOf(u8, l.text, "2024-03-15 (live)") != null) saw_live_marker = true;
if (std.mem.indexOf(u8, l.text, "No symbols held throughout") != null) saw_no_symbols = true;
}
try testing.expect(saw_today);
try testing.expect(saw_live_marker);
try testing.expect(saw_no_symbols);
}

View file

@ -828,10 +828,49 @@ pub fn buildTotalsCells(
}
/// Format the "now" side label for the header. Snapshot-now shows
/// the date; live-now shows the literal "today". `buf` backs the
/// date case; caller must keep it alive.
pub fn nowLabel(cv: CompareView, buf: *[10]u8) []const u8 {
if (cv.now_is_live) return "today";
/// the date alone; live-now shows the date with a `(live)` marker
/// so readers comparing this run against a later run (which would
/// read today's value from a snapshot file) understand why the
/// numbers might drift slightly.
///
/// **Why the marker matters.** When `compare 1W` runs with a live
/// "now", the value it shows for "now" is computed against today's
/// state of `portfolio.srf` plus today's cached prices. Next week,
/// when the same user runs `compare 1W` again, this week's value
/// becomes "then" but is read from the snapshot file (e.g.
/// `history/<DATE>-portfolio.srf`) that was captured for the
/// snapshot's `as_of` date, not the date the user actually ran
/// `compare`. The two values can disagree slightly because:
///
/// - **Date-arg drift.** Live `compare` on day T evaluates
/// `positionsForAccount(today=T)`, `totalCash(T)`, etc.
/// `snapshot --as-of T-1` evaluates the same against T-1. Any
/// lot whose `open_date` or `close_date` falls between those
/// two dates contributes a non-zero delta even when prices are
/// identical (e.g. a Saturday-dated RSU credit that's
/// in-scope for live Saturday-`compare` but not for the prior
/// Friday's snapshot).
/// - **Working-copy edits between runs.** Edits to
/// `portfolio.srf` made after the live `compare` run but
/// before the next `snapshot` capture are reflected in the
/// snapshot but not in the prior live "now" value (or vice
/// versa).
/// - **Cache refresh on a trading day.** When markets are open,
/// cached candle prices may refresh between the live `compare`
/// and the eventual `snapshot` capture. (On weekends and
/// holidays this source vanishes; the other two still apply.)
///
/// The `(live)` marker tells the reader "this number is ephemeral —
/// the corresponding snapshot value may differ." Without it, users
/// reasonably assumed last week's "now" should equal this week's
/// "then" verbatim and were surprised by a few-thousand-dollar drift
/// on a multi-million-dollar portfolio.
///
/// `buf` backs both cases; caller must keep it alive.
pub fn nowLabel(cv: CompareView, buf: *[24]u8) []const u8 {
if (cv.now_is_live) {
return std.fmt.bufPrint(buf, "{f} (live)", .{cv.now_date}) catch "today (live)";
}
return std.fmt.bufPrint(buf, "{f}", .{cv.now_date}) catch "????-??-??";
}
@ -880,7 +919,7 @@ test "buildTotalsCells: wires through the right formatters" {
try testing.expectEqual(StyleIntent.positive, cells.style);
}
test "nowLabel: live shows 'today', snapshot shows date" {
test "nowLabel: live shows date with (live) marker, snapshot shows date alone" {
const cv_live = CompareView{
.then_date = Date.fromYmd(2024, 1, 15),
.now_date = Date.fromYmd(2024, 3, 15),
@ -892,8 +931,8 @@ test "nowLabel: live shows 'today', snapshot shows date" {
.added_count = 0,
.removed_count = 0,
};
var buf: [10]u8 = undefined;
try testing.expectEqualStrings("today", nowLabel(cv_live, &buf));
var buf: [24]u8 = undefined;
try testing.expectEqualStrings("2024-03-15 (live)", nowLabel(cv_live, &buf));
const cv_snap = CompareView{
.then_date = Date.fromYmd(2024, 1, 15),