clean up excessive numbers of parameters
This commit is contained in:
parent
f2f0b53009
commit
7769155e76
2 changed files with 146 additions and 86 deletions
|
|
@ -411,10 +411,13 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||||
arena,
|
arena,
|
||||||
svc,
|
svc,
|
||||||
portfolio_path,
|
portfolio_path,
|
||||||
events_enabled,
|
.{
|
||||||
then_date,
|
.events_enabled = events_enabled,
|
||||||
proj_now_date,
|
.vs_date = then_date,
|
||||||
!now_is_live,
|
.now_date = proj_now_date,
|
||||||
|
.now_from_snapshot = !now_is_live,
|
||||||
|
.refresh = ctx.globals.refresh_policy,
|
||||||
|
},
|
||||||
) catch |err| blk: {
|
) catch |err| blk: {
|
||||||
// Projections computation failed — fall back to compare
|
// Projections computation failed — fall back to compare
|
||||||
// output without the block. User still gets the core
|
// output without the block. User still gets the core
|
||||||
|
|
|
||||||
|
|
@ -234,10 +234,13 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||||
allocator,
|
allocator,
|
||||||
svc,
|
svc,
|
||||||
file_path,
|
file_path,
|
||||||
args.events_enabled,
|
.{
|
||||||
args.vs_date,
|
.events_enabled = args.events_enabled,
|
||||||
args.as_of orelse today,
|
.vs_date = args.vs_date,
|
||||||
args.as_of != null,
|
.now_date = args.as_of orelse today,
|
||||||
|
.now_from_snapshot = args.as_of != null,
|
||||||
|
.refresh = ctx.globals.refresh_policy,
|
||||||
|
},
|
||||||
color,
|
color,
|
||||||
out,
|
out,
|
||||||
);
|
);
|
||||||
|
|
@ -249,11 +252,14 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||||
allocator,
|
allocator,
|
||||||
svc,
|
svc,
|
||||||
file_path,
|
file_path,
|
||||||
args.events_enabled,
|
.{
|
||||||
args.as_of orelse today,
|
.events_enabled = args.events_enabled,
|
||||||
args.as_of != null,
|
.as_of = args.as_of orelse today,
|
||||||
today,
|
.from_snapshot = args.as_of != null,
|
||||||
args.overlay_actuals,
|
.today = today,
|
||||||
|
.overlay_actuals = args.overlay_actuals,
|
||||||
|
.refresh = ctx.globals.refresh_policy,
|
||||||
|
},
|
||||||
color,
|
color,
|
||||||
out,
|
out,
|
||||||
);
|
);
|
||||||
|
|
@ -289,16 +295,41 @@ const AsOfResolution = struct {
|
||||||
/// today as `as_of`.
|
/// today as `as_of`.
|
||||||
/// - `true`: historical mode. Load the snapshot at-or-before
|
/// - `true`: historical mode. Load the snapshot at-or-before
|
||||||
/// `as_of` from the history dir.
|
/// `as_of` from the history dir.
|
||||||
|
/// Per-call configuration for `runBands`. Bundled because the
|
||||||
|
/// call already had nine context-plus-config parameters and adding
|
||||||
|
/// `refresh` would push it past the readable-positional threshold.
|
||||||
|
/// Same rationale as `KeyComparisonOptions` — see its doc-block.
|
||||||
|
pub const BandsOptions = struct {
|
||||||
|
/// Whether simulated lifecycle events (RMDs, lump-sum
|
||||||
|
/// withdrawals, Social Security) are baked into the
|
||||||
|
/// projection.
|
||||||
|
events_enabled: bool,
|
||||||
|
/// Reference date for the projection. When `from_snapshot`
|
||||||
|
/// is false this also doubles as "today" for cash/CD
|
||||||
|
/// computation paths that resolve from the live portfolio.
|
||||||
|
as_of: Date,
|
||||||
|
/// True when `as_of` came from `--as-of`. Selects the
|
||||||
|
/// historical-snapshot resolution path; otherwise the
|
||||||
|
/// live-portfolio path is used.
|
||||||
|
from_snapshot: bool,
|
||||||
|
/// The actual current calendar day. Used for the live-side
|
||||||
|
/// composition and cash totals when `from_snapshot` is
|
||||||
|
/// false. May equal `as_of` in the live case.
|
||||||
|
today: Date,
|
||||||
|
/// Whether to overlay an actual-history series on the
|
||||||
|
/// projection percentile bands.
|
||||||
|
overlay_actuals: bool,
|
||||||
|
/// Refresh policy threaded through the live "now" price
|
||||||
|
/// load. Has no effect when `from_snapshot = true`.
|
||||||
|
refresh: framework.RefreshPolicy = .auto,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn runBands(
|
pub fn runBands(
|
||||||
io: std.Io,
|
io: std.Io,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
svc: *zfin.DataService,
|
svc: *zfin.DataService,
|
||||||
file_path: []const u8,
|
file_path: []const u8,
|
||||||
events_enabled: bool,
|
opts: BandsOptions,
|
||||||
as_of: Date,
|
|
||||||
from_snapshot: bool,
|
|
||||||
today: Date,
|
|
||||||
overlay_actuals: bool,
|
|
||||||
color: bool,
|
color: bool,
|
||||||
out: *std.Io.Writer,
|
out: *std.Io.Writer,
|
||||||
) !void {
|
) !void {
|
||||||
|
|
@ -332,8 +363,8 @@ pub fn runBands(
|
||||||
var live_pf_data: ?cli.PortfolioData = null;
|
var live_pf_data: ?cli.PortfolioData = null;
|
||||||
defer if (live_pf_data) |*p| p.deinit(allocator);
|
defer if (live_pf_data) |*p| p.deinit(allocator);
|
||||||
|
|
||||||
if (from_snapshot) {
|
if (opts.from_snapshot) {
|
||||||
resolution = resolveAsOfSnapshot(io, va, file_path, as_of) catch |err| switch (err) {
|
resolution = resolveAsOfSnapshot(io, va, file_path, opts.as_of) catch |err| switch (err) {
|
||||||
error.NoSnapshot => return,
|
error.NoSnapshot => return,
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
|
|
@ -349,7 +380,7 @@ pub fn runBands(
|
||||||
&snap_bundle.?.snap,
|
&snap_bundle.?.snap,
|
||||||
resolution.?.actual,
|
resolution.?.actual,
|
||||||
svc,
|
svc,
|
||||||
events_enabled,
|
opts.events_enabled,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Imported-only as-of: need today's portfolio composition
|
// Imported-only as-of: need today's portfolio composition
|
||||||
|
|
@ -361,22 +392,24 @@ pub fn runBands(
|
||||||
// the composition we ALWAYS want today's mix (the only
|
// the composition we ALWAYS want today's mix (the only
|
||||||
// composition we know). Pass `today` for the cash/CD
|
// composition we know). Pass `today` for the cash/CD
|
||||||
// computation.
|
// computation.
|
||||||
live_loaded = cli.loadPortfolio(io, allocator, file_path, today) orelse return;
|
live_loaded = cli.loadPortfolio(io, allocator, file_path, opts.today) orelse return;
|
||||||
const lp = &live_loaded.?;
|
const lp = &live_loaded.?;
|
||||||
|
|
||||||
|
// Route through the shared loader so `--refresh-data`
|
||||||
|
// propagates here. Pre-bundle this was a silent
|
||||||
|
// cache-only loop; behavior change is intentional.
|
||||||
var prices = std.StringHashMap(f64).init(allocator);
|
var prices = std.StringHashMap(f64).init(allocator);
|
||||||
defer prices.deinit();
|
defer prices.deinit();
|
||||||
for (lp.positions) |pos| {
|
{
|
||||||
if (pos.shares <= 0) continue;
|
var load_result = cli.loadPortfolioPrices(io, svc, lp.syms, &.{}, opts.refresh, color);
|
||||||
if (svc.getCachedCandles(pos.symbol)) |cs| {
|
defer load_result.deinit();
|
||||||
defer cs.deinit();
|
var it = load_result.prices.iterator();
|
||||||
if (cs.data.len > 0) {
|
while (it.next()) |entry| {
|
||||||
try prices.put(pos.symbol, cs.data[cs.data.len - 1].close);
|
prices.put(entry.key_ptr.*, entry.value_ptr.*) catch {};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
live_pf_data = cli.buildPortfolioData(allocator, lp.portfolio, lp.positions, lp.syms, &prices, svc, today) catch |err| switch (err) {
|
live_pf_data = cli.buildPortfolioData(allocator, lp.portfolio, lp.positions, lp.syms, &prices, svc, opts.today) catch |err| switch (err) {
|
||||||
error.NoAllocations, error.SummaryFailed => {
|
error.NoAllocations, error.SummaryFailed => {
|
||||||
try cli.stderrPrint(io, "Error computing portfolio summary for imported-only as-of.\n");
|
try cli.stderrPrint(io, "Error computing portfolio summary for imported-only as-of.\n");
|
||||||
return;
|
return;
|
||||||
|
|
@ -390,32 +423,33 @@ pub fn runBands(
|
||||||
portfolio_dir,
|
portfolio_dir,
|
||||||
live_pf_data.?.summary.allocations,
|
live_pf_data.?.summary.allocations,
|
||||||
live_pf_data.?.summary.total_value,
|
live_pf_data.?.summary.total_value,
|
||||||
lp.portfolio.totalCash(today),
|
lp.portfolio.totalCash(opts.today),
|
||||||
lp.portfolio.totalCdFaceValue(today),
|
lp.portfolio.totalCdFaceValue(opts.today),
|
||||||
resolution.?.liquid,
|
resolution.?.liquid,
|
||||||
resolution.?.actual,
|
resolution.?.actual,
|
||||||
svc,
|
svc,
|
||||||
events_enabled,
|
opts.events_enabled,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
live_loaded = cli.loadPortfolio(io, allocator, file_path, as_of) orelse return;
|
live_loaded = cli.loadPortfolio(io, allocator, file_path, opts.as_of) orelse return;
|
||||||
const lp = &live_loaded.?;
|
const lp = &live_loaded.?;
|
||||||
|
|
||||||
// Prices from cache — matches pre-as-of behavior exactly.
|
// Route through the shared loader so `--refresh-data`
|
||||||
|
// propagates here. Pre-bundle this was a silent
|
||||||
|
// cache-only loop; behavior change is intentional.
|
||||||
var prices = std.StringHashMap(f64).init(allocator);
|
var prices = std.StringHashMap(f64).init(allocator);
|
||||||
defer prices.deinit();
|
defer prices.deinit();
|
||||||
for (lp.positions) |pos| {
|
{
|
||||||
if (pos.shares <= 0) continue;
|
var load_result = cli.loadPortfolioPrices(io, svc, lp.syms, &.{}, opts.refresh, color);
|
||||||
if (svc.getCachedCandles(pos.symbol)) |cs| {
|
defer load_result.deinit();
|
||||||
defer cs.deinit();
|
var it = load_result.prices.iterator();
|
||||||
if (cs.data.len > 0) {
|
while (it.next()) |entry| {
|
||||||
try prices.put(pos.symbol, cs.data[cs.data.len - 1].close);
|
prices.put(entry.key_ptr.*, entry.value_ptr.*) catch {};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
live_pf_data = cli.buildPortfolioData(allocator, lp.portfolio, lp.positions, lp.syms, &prices, svc, as_of) catch |err| switch (err) {
|
live_pf_data = cli.buildPortfolioData(allocator, lp.portfolio, lp.positions, lp.syms, &prices, svc, opts.as_of) catch |err| switch (err) {
|
||||||
error.NoAllocations, error.SummaryFailed => {
|
error.NoAllocations, error.SummaryFailed => {
|
||||||
try cli.stderrPrint(io, "Error computing portfolio summary.\n");
|
try cli.stderrPrint(io, "Error computing portfolio summary.\n");
|
||||||
return;
|
return;
|
||||||
|
|
@ -429,11 +463,11 @@ pub fn runBands(
|
||||||
portfolio_dir,
|
portfolio_dir,
|
||||||
live_pf_data.?.summary.allocations,
|
live_pf_data.?.summary.allocations,
|
||||||
live_pf_data.?.summary.total_value,
|
live_pf_data.?.summary.total_value,
|
||||||
lp.portfolio.totalCash(as_of),
|
lp.portfolio.totalCash(opts.as_of),
|
||||||
lp.portfolio.totalCdFaceValue(as_of),
|
lp.portfolio.totalCdFaceValue(opts.as_of),
|
||||||
svc,
|
svc,
|
||||||
events_enabled,
|
opts.events_enabled,
|
||||||
as_of,
|
opts.as_of,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -443,11 +477,11 @@ pub fn runBands(
|
||||||
// user passes `--overlay-actuals` without `--as-of`, warn and
|
// user passes `--overlay-actuals` without `--as-of`, warn and
|
||||||
// continue without the overlay; the overlay against today-as-now
|
// continue without the overlay; the overlay against today-as-now
|
||||||
// is meaningless because the future hasn't happened yet.
|
// is meaningless because the future hasn't happened yet.
|
||||||
if (overlay_actuals) {
|
if (opts.overlay_actuals) {
|
||||||
if (!from_snapshot) {
|
if (!opts.from_snapshot) {
|
||||||
try cli.stderrPrint(io, "Note: --overlay-actuals requires --as-of; ignoring.\n");
|
try cli.stderrPrint(io, "Note: --overlay-actuals requires --as-of; ignoring.\n");
|
||||||
} else if (resolution) |r| {
|
} else if (resolution) |r| {
|
||||||
ctx.overlay_actuals = loadOverlayActuals(io, va, file_path, r.actual, today) catch |err| blk: {
|
ctx.overlay_actuals = loadOverlayActuals(io, va, file_path, r.actual, opts.today) catch |err| blk: {
|
||||||
// Non-fatal — the projection still renders without
|
// Non-fatal — the projection still renders without
|
||||||
// the overlay. Surface the error so the user can fix
|
// the overlay. Surface the error so the user can fix
|
||||||
// their history dir but don't block the report.
|
// their history dir but don't block the report.
|
||||||
|
|
@ -575,7 +609,7 @@ pub fn runBands(
|
||||||
|
|
||||||
// ── Accumulation phase / Earliest retirement blocks ──────────
|
// ── Accumulation phase / Earliest retirement blocks ──────────
|
||||||
try renderAccumulationBlock(out, color, va, ctx);
|
try renderAccumulationBlock(out, color, va, ctx);
|
||||||
try renderEarliestBlock(out, color, va, ctx, as_of);
|
try renderEarliestBlock(out, color, va, ctx, opts.as_of);
|
||||||
|
|
||||||
// ── Braille chart: median portfolio value ─────────────────────
|
// ── Braille chart: median portfolio value ─────────────────────
|
||||||
if (horizons.len > 0) {
|
if (horizons.len > 0) {
|
||||||
|
|
@ -621,7 +655,7 @@ pub fn runBands(
|
||||||
// pointer so the user knows where to find it. (We do NOT gate on
|
// pointer so the user knows where to find it. (We do NOT gate on
|
||||||
// ctx.overlay_actuals being non-null — even when the overlay was
|
// ctx.overlay_actuals being non-null — even when the overlay was
|
||||||
// requested but had no data, the user benefits from the tip.)
|
// requested but had no data, the user benefits from the tip.)
|
||||||
if (overlay_actuals and from_snapshot) {
|
if (opts.overlay_actuals and opts.from_snapshot) {
|
||||||
try cli.printFg(out, color, cli.CLR_MUTED, " (Overlay rendered in TUI only — run `zfin interactive`, set as-of with `d`, then press `o`.)\n", .{});
|
try cli.printFg(out, color, cli.CLR_MUTED, " (Overlay rendered in TUI only — run `zfin interactive`, set as-of with `d`, then press `o`.)\n", .{});
|
||||||
try cli.printFg(out, color, cli.CLR_MUTED, " Caveat: overlay tracks trajectory, not SWR validity.\n", .{});
|
try cli.printFg(out, color, cli.CLR_MUTED, " Caveat: overlay tracks trajectory, not SWR validity.\n", .{});
|
||||||
}
|
}
|
||||||
|
|
@ -659,7 +693,7 @@ pub fn runBands(
|
||||||
{
|
{
|
||||||
const events = ctx.config.getEvents();
|
const events = ctx.config.getEvents();
|
||||||
if (events.len > 0) {
|
if (events.len > 0) {
|
||||||
const ages_ref_date = if (resolution) |r| r.actual else as_of;
|
const ages_ref_date = if (resolution) |r| r.actual else opts.as_of;
|
||||||
const ages = ctx.config.currentAges(ages_ref_date);
|
const ages = ctx.config.currentAges(ages_ref_date);
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
try cli.printBold(out, color, "Life Events\n", .{});
|
try cli.printBold(out, color, "Life Events\n", .{});
|
||||||
|
|
@ -755,10 +789,7 @@ pub fn runCompare(
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
svc: *zfin.DataService,
|
svc: *zfin.DataService,
|
||||||
file_path: []const u8,
|
file_path: []const u8,
|
||||||
events_enabled: bool,
|
opts: KeyComparisonOptions,
|
||||||
vs_date: Date,
|
|
||||||
now_date: Date,
|
|
||||||
now_from_snapshot: bool,
|
|
||||||
color: bool,
|
color: bool,
|
||||||
out: *std.Io.Writer,
|
out: *std.Io.Writer,
|
||||||
) !void {
|
) !void {
|
||||||
|
|
@ -766,7 +797,7 @@ pub fn runCompare(
|
||||||
defer arena_state.deinit();
|
defer arena_state.deinit();
|
||||||
const va = arena_state.allocator();
|
const va = arena_state.allocator();
|
||||||
|
|
||||||
const result = computeKeyComparison(io, allocator, va, svc, file_path, events_enabled, vs_date, now_date, now_from_snapshot) catch |err| switch (err) {
|
const result = computeKeyComparison(io, allocator, va, svc, file_path, opts) catch |err| switch (err) {
|
||||||
error.NoSnapshot, error.PortfolioLoadFailed => return,
|
error.NoSnapshot, error.PortfolioLoadFailed => return,
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
|
|
@ -780,7 +811,7 @@ pub fn runCompare(
|
||||||
const days_between = if (result.now_resolution) |nr|
|
const days_between = if (result.now_resolution) |nr|
|
||||||
nr.actual.days - result.resolution.actual.days
|
nr.actual.days - result.resolution.actual.days
|
||||||
else
|
else
|
||||||
now_date.days - result.resolution.actual.days;
|
opts.now_date.days - result.resolution.actual.days;
|
||||||
|
|
||||||
try cli.printBold(out, color, "Projections comparison: {s} → {s} ({d} day{s})\n", .{
|
try cli.printBold(out, color, "Projections comparison: {s} → {s} ({d} day{s})\n", .{
|
||||||
then_str,
|
then_str,
|
||||||
|
|
@ -962,6 +993,32 @@ pub const KeyComparisonResult = struct {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Per-call configuration for `computeKeyComparison`. Bundled into
|
||||||
|
/// a struct because the call already had eight context-plus-config
|
||||||
|
/// parameters and adding `refresh` would push it to ten — past the
|
||||||
|
/// point where positional args are readable. Required fields have
|
||||||
|
/// no defaults; optional knobs (currently just `refresh`) carry
|
||||||
|
/// sensible defaults so most callers can leave them out.
|
||||||
|
pub const KeyComparisonOptions = struct {
|
||||||
|
/// Whether simulated lifecycle events (RMDs, lump-sum
|
||||||
|
/// withdrawals, Social Security) are baked into the
|
||||||
|
/// projection. The "then" and "now" sides both honor this.
|
||||||
|
events_enabled: bool,
|
||||||
|
/// The earlier date — historical snapshot resolution.
|
||||||
|
vs_date: Date,
|
||||||
|
/// The later date — either live or another snapshot,
|
||||||
|
/// controlled by `now_from_snapshot`.
|
||||||
|
now_date: Date,
|
||||||
|
/// When true, both sides resolve from snapshots. When
|
||||||
|
/// false, the "now" side loads the live portfolio and
|
||||||
|
/// fetches current prices.
|
||||||
|
now_from_snapshot: bool,
|
||||||
|
/// Refresh policy threaded through the live "now" price
|
||||||
|
/// load. Has no effect when `now_from_snapshot = true`
|
||||||
|
/// (snapshots don't fetch prices).
|
||||||
|
refresh: framework.RefreshPolicy = .auto,
|
||||||
|
};
|
||||||
|
|
||||||
/// Compute the "then" vs "now" key metrics for `--vs` and the
|
/// Compute the "then" vs "now" key metrics for `--vs` and the
|
||||||
/// `compare --projections` embedded block.
|
/// `compare --projections` embedded block.
|
||||||
///
|
///
|
||||||
|
|
@ -989,10 +1046,7 @@ pub fn computeKeyComparison(
|
||||||
va: std.mem.Allocator,
|
va: std.mem.Allocator,
|
||||||
svc: *zfin.DataService,
|
svc: *zfin.DataService,
|
||||||
file_path: []const u8,
|
file_path: []const u8,
|
||||||
events_enabled: bool,
|
opts: KeyComparisonOptions,
|
||||||
vs_date: Date,
|
|
||||||
now_date: Date,
|
|
||||||
now_from_snapshot: bool,
|
|
||||||
) !KeyComparisonResult {
|
) !KeyComparisonResult {
|
||||||
const dir_end = if (std.mem.lastIndexOfScalar(u8, file_path, std.fs.path.sep)) |idx| idx + 1 else 0;
|
const dir_end = if (std.mem.lastIndexOfScalar(u8, file_path, std.fs.path.sep)) |idx| idx + 1 else 0;
|
||||||
const portfolio_dir = file_path[0..dir_end];
|
const portfolio_dir = file_path[0..dir_end];
|
||||||
|
|
@ -1008,14 +1062,14 @@ pub fn computeKeyComparison(
|
||||||
svc,
|
svc,
|
||||||
file_path,
|
file_path,
|
||||||
portfolio_dir,
|
portfolio_dir,
|
||||||
events_enabled,
|
opts.events_enabled,
|
||||||
vs_date,
|
opts.vs_date,
|
||||||
&then_resolution,
|
&then_resolution,
|
||||||
&then_snap,
|
&then_snap,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Now side — either another snapshot or the live portfolio.
|
// Now side — either another snapshot or the live portfolio.
|
||||||
if (now_from_snapshot) {
|
if (opts.now_from_snapshot) {
|
||||||
var now_resolution: AsOfResolution = undefined;
|
var now_resolution: AsOfResolution = undefined;
|
||||||
var now_snap: history.LoadedSnapshot = undefined;
|
var now_snap: history.LoadedSnapshot = undefined;
|
||||||
const now_ctx = loadAsOfContext(
|
const now_ctx = loadAsOfContext(
|
||||||
|
|
@ -1025,8 +1079,8 @@ pub fn computeKeyComparison(
|
||||||
svc,
|
svc,
|
||||||
file_path,
|
file_path,
|
||||||
portfolio_dir,
|
portfolio_dir,
|
||||||
events_enabled,
|
opts.events_enabled,
|
||||||
now_date,
|
opts.now_date,
|
||||||
&now_resolution,
|
&now_resolution,
|
||||||
&now_snap,
|
&now_snap,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
|
|
@ -1039,7 +1093,7 @@ pub fn computeKeyComparison(
|
||||||
.now = extractKeyMetrics(now_ctx),
|
.now = extractKeyMetrics(now_ctx),
|
||||||
.resolution = then_resolution,
|
.resolution = then_resolution,
|
||||||
.now_resolution = now_resolution,
|
.now_resolution = now_resolution,
|
||||||
.events_enabled = events_enabled,
|
.events_enabled = opts.events_enabled,
|
||||||
.retained_then = then_snap,
|
.retained_then = then_snap,
|
||||||
.retained_now = now_snap,
|
.retained_now = now_snap,
|
||||||
.retained_allocator = allocator,
|
.retained_allocator = allocator,
|
||||||
|
|
@ -1047,25 +1101,28 @@ pub fn computeKeyComparison(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live "now" side — mirrors `run()`'s live path.
|
// Live "now" side — mirrors `run()`'s live path.
|
||||||
var loaded = cli.loadPortfolio(io, allocator, file_path, now_date) orelse {
|
var loaded = cli.loadPortfolio(io, allocator, file_path, opts.now_date) orelse {
|
||||||
then_snap.deinit(allocator);
|
then_snap.deinit(allocator);
|
||||||
return error.PortfolioLoadFailed;
|
return error.PortfolioLoadFailed;
|
||||||
};
|
};
|
||||||
defer loaded.deinit(allocator);
|
defer loaded.deinit(allocator);
|
||||||
|
|
||||||
|
// Route through the shared loader so `--refresh-data` propagates
|
||||||
|
// here too. Pre-bundle this was a silent cache-only loop, which
|
||||||
|
// diverged from every other multi-symbol command. Watchlist syms
|
||||||
|
// aren't relevant for projections so we pass an empty slice.
|
||||||
var prices = std.StringHashMap(f64).init(allocator);
|
var prices = std.StringHashMap(f64).init(allocator);
|
||||||
defer prices.deinit();
|
defer prices.deinit();
|
||||||
for (loaded.positions) |pos| {
|
{
|
||||||
if (pos.shares <= 0) continue;
|
var load_result = cli.loadPortfolioPrices(io, svc, loaded.syms, &.{}, opts.refresh, false);
|
||||||
if (svc.getCachedCandles(pos.symbol)) |cs| {
|
defer load_result.deinit();
|
||||||
defer cs.deinit();
|
var it = load_result.prices.iterator();
|
||||||
if (cs.data.len > 0) {
|
while (it.next()) |entry| {
|
||||||
try prices.put(pos.symbol, cs.data[cs.data.len - 1].close);
|
prices.put(entry.key_ptr.*, entry.value_ptr.*) catch {};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var pf_data = cli.buildPortfolioData(allocator, loaded.portfolio, loaded.positions, loaded.syms, &prices, svc, now_date) catch |err| switch (err) {
|
var pf_data = cli.buildPortfolioData(allocator, loaded.portfolio, loaded.positions, loaded.syms, &prices, svc, opts.now_date) catch |err| switch (err) {
|
||||||
error.NoAllocations, error.SummaryFailed => {
|
error.NoAllocations, error.SummaryFailed => {
|
||||||
then_snap.deinit(allocator);
|
then_snap.deinit(allocator);
|
||||||
try cli.stderrPrint(io, "Error computing portfolio summary.\n");
|
try cli.stderrPrint(io, "Error computing portfolio summary.\n");
|
||||||
|
|
@ -1084,11 +1141,11 @@ pub fn computeKeyComparison(
|
||||||
portfolio_dir,
|
portfolio_dir,
|
||||||
pf_data.summary.allocations,
|
pf_data.summary.allocations,
|
||||||
pf_data.summary.total_value,
|
pf_data.summary.total_value,
|
||||||
loaded.portfolio.totalCash(now_date),
|
loaded.portfolio.totalCash(opts.now_date),
|
||||||
loaded.portfolio.totalCdFaceValue(now_date),
|
loaded.portfolio.totalCdFaceValue(opts.now_date),
|
||||||
svc,
|
svc,
|
||||||
events_enabled,
|
opts.events_enabled,
|
||||||
now_date,
|
opts.now_date,
|
||||||
);
|
);
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
|
|
@ -1096,7 +1153,7 @@ pub fn computeKeyComparison(
|
||||||
.now = extractKeyMetrics(now_ctx),
|
.now = extractKeyMetrics(now_ctx),
|
||||||
.resolution = then_resolution,
|
.resolution = then_resolution,
|
||||||
.now_resolution = null,
|
.now_resolution = null,
|
||||||
.events_enabled = events_enabled,
|
.events_enabled = opts.events_enabled,
|
||||||
.retained_then = then_snap,
|
.retained_then = then_snap,
|
||||||
.retained_now = null,
|
.retained_now = null,
|
||||||
.retained_allocator = allocator,
|
.retained_allocator = allocator,
|
||||||
|
|
@ -1646,7 +1703,7 @@ test "run: as_of with no snapshots returns without error (stderr-only)" {
|
||||||
var stream = std.Io.Writer.fixed(&buf);
|
var stream = std.Io.Writer.fixed(&buf);
|
||||||
|
|
||||||
const d = Date.fromYmd(2026, 3, 13);
|
const d = Date.fromYmd(2026, 3, 13);
|
||||||
try runBands(io, testing.allocator, &svc, pf, false, d, true, d, false, false, &stream);
|
try runBands(io, testing.allocator, &svc, pf, .{ .events_enabled = false, .as_of = d, .from_snapshot = true, .today = d, .overlay_actuals = false }, false, &stream);
|
||||||
|
|
||||||
// No body output because the resolution failed — the stderr
|
// No body output because the resolution failed — the stderr
|
||||||
// message is swallowed by `cli.stderrPrint` and doesn't land in
|
// message is swallowed by `cli.stderrPrint` and doesn't land in
|
||||||
|
|
@ -1678,7 +1735,7 @@ test "run: as_of with matching snapshot produces body output" {
|
||||||
|
|
||||||
var buf: [32_768]u8 = undefined;
|
var buf: [32_768]u8 = undefined;
|
||||||
var stream = std.Io.Writer.fixed(&buf);
|
var stream = std.Io.Writer.fixed(&buf);
|
||||||
try runBands(io, testing.allocator, &svc, pf, false, d, true, d, false, false, &stream);
|
try runBands(io, testing.allocator, &svc, pf, .{ .events_enabled = false, .as_of = d, .from_snapshot = true, .today = d, .overlay_actuals = false }, false, &stream);
|
||||||
|
|
||||||
const out = stream.buffered();
|
const out = stream.buffered();
|
||||||
// Header should call out the as-of date explicitly.
|
// Header should call out the as-of date explicitly.
|
||||||
|
|
@ -1709,7 +1766,7 @@ test "run: as_of auto-snap surfaces muted 'nearest' note" {
|
||||||
var stream = std.Io.Writer.fixed(&buf);
|
var stream = std.Io.Writer.fixed(&buf);
|
||||||
|
|
||||||
const requested = Date.fromYmd(2026, 3, 13);
|
const requested = Date.fromYmd(2026, 3, 13);
|
||||||
try runBands(io, testing.allocator, &svc, pf, false, requested, true, requested, false, false, &stream);
|
try runBands(io, testing.allocator, &svc, pf, .{ .events_enabled = false, .as_of = requested, .from_snapshot = true, .today = requested, .overlay_actuals = false }, false, &stream);
|
||||||
|
|
||||||
const out = stream.buffered();
|
const out = stream.buffered();
|
||||||
try testing.expect(std.mem.indexOf(u8, out, "as of 2026-03-12") != null);
|
try testing.expect(std.mem.indexOf(u8, out, "as of 2026-03-12") != null);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue