document difference between live and snapshot when on weekends
This commit is contained in:
parent
a0715b615c
commit
10be2ee86a
4 changed files with 55 additions and 46 deletions
30
TODO.md
30
TODO.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue