migrate projections to new cli framework (restore audit command functionally)
This commit is contained in:
parent
62d058bb11
commit
a690d55c2b
2 changed files with 378 additions and 110 deletions
|
|
@ -11,6 +11,7 @@
|
|||
const std = @import("std");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const framework = @import("framework.zig");
|
||||
const fmt = cli.fmt;
|
||||
const Date = zfin.Date;
|
||||
const Money = @import("../Money.zig");
|
||||
|
|
@ -30,6 +31,235 @@ const shiller = @import("../data/shiller.zig");
|
|||
const stock_benchmark = "SPY";
|
||||
const bond_benchmark = "AGG";
|
||||
|
||||
/// Tagged-union args for the four projection sub-modes. Mutually-
|
||||
/// exclusive flag combos (--convergence with --vs, --real with
|
||||
/// non-backtest, etc.) are rejected at parse time so each variant
|
||||
/// here is a self-contained mode.
|
||||
pub const ParsedArgs = union(enum) {
|
||||
/// Default: percentile bands view. `as_of` selects the snapshot
|
||||
/// (or today for live). `overlay_actuals` plots realized
|
||||
/// trajectory on top.
|
||||
bands: BandsArgs,
|
||||
/// `--vs <DATE>`: side-by-side compare of two projections.
|
||||
compare: CompareArgs,
|
||||
/// `--convergence`: plot the spreadsheet's predicted retirement
|
||||
/// date over time. No knobs.
|
||||
convergence,
|
||||
/// `--return-backtest [--real]`: plot expected_return vs realized
|
||||
/// forward-CAGR.
|
||||
return_backtest: struct { real: bool },
|
||||
};
|
||||
|
||||
pub const BandsArgs = struct {
|
||||
events_enabled: bool = true,
|
||||
/// `null` means live (today). Non-null = historical snapshot.
|
||||
as_of: ?Date = null,
|
||||
overlay_actuals: bool = false,
|
||||
};
|
||||
|
||||
pub const CompareArgs = struct {
|
||||
events_enabled: bool = true,
|
||||
vs_date: Date,
|
||||
/// "Now" side. Null = today (live); non-null = the `--as-of`
|
||||
/// date the user paired with `--vs`.
|
||||
as_of: ?Date = null,
|
||||
};
|
||||
|
||||
pub const meta = struct {
|
||||
pub const name: []const u8 = "projections";
|
||||
pub const group: framework.Group = .portfolio;
|
||||
pub const synopsis: []const u8 = "Retirement projections, benchmark comparison, percentile bands";
|
||||
pub const help: []const u8 =
|
||||
\\Usage: zfin projections [opts]
|
||||
\\
|
||||
\\Default mode: percentile-bands view of the portfolio's
|
||||
\\projected value over the configured horizon (`projections.srf`),
|
||||
\\plus benchmark comparison (SPY/AGG) and safe-withdrawal
|
||||
\\dollars at multiple horizons / confidence levels.
|
||||
\\
|
||||
\\Three alternate sub-modes (mutually exclusive):
|
||||
\\ --vs <DATE> Side-by-side compare with a
|
||||
\\ historical snapshot's projection.
|
||||
\\ --convergence Plot the model's predicted
|
||||
\\ retirement date over time as data
|
||||
\\ accumulated.
|
||||
\\ --return-backtest Plot expected_return claim over
|
||||
\\ time alongside realized forward
|
||||
\\ CAGR. Pair with `--real` for
|
||||
\\ CPI-adjusted dollars.
|
||||
\\
|
||||
\\Options:
|
||||
\\ --no-events Exclude life events from the
|
||||
\\ simulation (baseline view).
|
||||
\\ --as-of <DATE> Compute against a historical
|
||||
\\ snapshot. Auto-snaps to the
|
||||
\\ nearest-earlier snapshot.
|
||||
\\ --overlay-actuals Plot realized portfolio trajectory
|
||||
\\ from --as-of through today on top
|
||||
\\ of the percentile bands. Requires
|
||||
\\ --as-of. Ignored under --vs.
|
||||
\\ --vs <DATE> (see above)
|
||||
\\ --convergence (see above)
|
||||
\\ --return-backtest (see above)
|
||||
\\ --real With --return-backtest, render in
|
||||
\\ CPI-adjusted dollars.
|
||||
\\
|
||||
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
|
||||
\\
|
||||
;
|
||||
};
|
||||
|
||||
comptime {
|
||||
framework.validateCommandModule(@This());
|
||||
}
|
||||
|
||||
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
|
||||
const io = ctx.io;
|
||||
const today = ctx.today;
|
||||
|
||||
var events_enabled = true;
|
||||
var as_of: ?Date = null;
|
||||
var vs_date: ?Date = null;
|
||||
var overlay_actuals = false;
|
||||
var convergence = false;
|
||||
var return_backtest = false;
|
||||
var real_mode = false;
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < cmd_args.len) : (i += 1) {
|
||||
const a = cmd_args[i];
|
||||
if (std.mem.eql(u8, a, "--no-events")) {
|
||||
events_enabled = false;
|
||||
} else if (std.mem.eql(u8, a, "--overlay-actuals")) {
|
||||
overlay_actuals = true;
|
||||
} else if (std.mem.eql(u8, a, "--convergence")) {
|
||||
convergence = true;
|
||||
} else if (std.mem.eql(u8, a, "--return-backtest")) {
|
||||
return_backtest = true;
|
||||
} else if (std.mem.eql(u8, a, "--real")) {
|
||||
real_mode = true;
|
||||
} else if (std.mem.eql(u8, a, "--as-of") or std.mem.eql(u8, a, "--vs")) {
|
||||
if (i + 1 >= cmd_args.len) {
|
||||
try cli.stderrPrint(io, "Error: ");
|
||||
try cli.stderrPrint(io, a);
|
||||
try cli.stderrPrint(io, " requires a value (YYYY-MM-DD, N[WMQY], or 'live').\n");
|
||||
return error.MissingFlagValue;
|
||||
}
|
||||
const value = cmd_args[i + 1];
|
||||
const parsed_date = cli.parseAsOfDate(value, today) catch |err| {
|
||||
var buf: [256]u8 = undefined;
|
||||
const msg = cli.fmtAsOfParseError(&buf, value, err);
|
||||
try cli.stderrPrint(io, msg);
|
||||
try cli.stderrPrint(io, "\n");
|
||||
return error.InvalidFlagValue;
|
||||
};
|
||||
if (parsed_date) |d| {
|
||||
if (d.days > today.days) {
|
||||
try cli.stderrPrint(io, "Error: date is in the future.\n");
|
||||
return error.InvalidFlagValue;
|
||||
}
|
||||
if (std.mem.eql(u8, a, "--as-of")) {
|
||||
as_of = d;
|
||||
} else {
|
||||
vs_date = d;
|
||||
}
|
||||
}
|
||||
// null (= "live") is ignored — leaves flag unset, same
|
||||
// as not passing the flag at all.
|
||||
i += 1;
|
||||
} else {
|
||||
try cli.stderrPrint(io, "Error: unexpected argument to 'projections': ");
|
||||
try cli.stderrPrint(io, a);
|
||||
try cli.stderrPrint(io, "\n");
|
||||
return error.UnexpectedArg;
|
||||
}
|
||||
}
|
||||
|
||||
// Mutually-exclusive view flags. Forecast-evaluation flags
|
||||
// (`--convergence`, `--return-backtest`) replace the default
|
||||
// bands view entirely; combining them with each other,
|
||||
// `--vs`, or `--overlay-actuals` is rejected.
|
||||
if (convergence and return_backtest) {
|
||||
try cli.stderrPrint(io, "Error: --convergence and --return-backtest are mutually exclusive.\n");
|
||||
return error.MutuallyExclusive;
|
||||
}
|
||||
if ((convergence or return_backtest) and vs_date != null) {
|
||||
try cli.stderrPrint(io, "Error: --convergence/--return-backtest cannot be combined with --vs.\n");
|
||||
return error.MutuallyExclusive;
|
||||
}
|
||||
if ((convergence or return_backtest) and overlay_actuals) {
|
||||
try cli.stderrPrint(io, "Error: --convergence/--return-backtest cannot be combined with --overlay-actuals.\n");
|
||||
return error.MutuallyExclusive;
|
||||
}
|
||||
if (real_mode and !return_backtest) {
|
||||
try cli.stderrPrint(io, "Error: --real only applies to --return-backtest.\n");
|
||||
return error.MutuallyExclusive;
|
||||
}
|
||||
|
||||
if (convergence) return ParsedArgs{ .convergence = {} };
|
||||
if (return_backtest) return ParsedArgs{ .return_backtest = .{ .real = real_mode } };
|
||||
if (vs_date) |d| {
|
||||
return ParsedArgs{ .compare = .{
|
||||
.events_enabled = events_enabled,
|
||||
.vs_date = d,
|
||||
.as_of = as_of,
|
||||
} };
|
||||
}
|
||||
return ParsedArgs{ .bands = .{
|
||||
.events_enabled = events_enabled,
|
||||
.as_of = as_of,
|
||||
.overlay_actuals = overlay_actuals,
|
||||
} };
|
||||
}
|
||||
|
||||
pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||
const io = ctx.io;
|
||||
const allocator = ctx.allocator;
|
||||
const out = ctx.out;
|
||||
const color = ctx.color;
|
||||
const today = ctx.today;
|
||||
|
||||
const pf = ctx.resolvePortfolioPath();
|
||||
defer pf.deinit(allocator);
|
||||
const file_path = pf.path;
|
||||
|
||||
switch (parsed) {
|
||||
.convergence => try runConvergence(io, allocator, file_path, color, out),
|
||||
.return_backtest => |args| try runReturnBacktest(io, allocator, file_path, args.real, color, out),
|
||||
.compare => |args| {
|
||||
const svc = ctx.svc orelse return error.MissingDataService;
|
||||
try runCompare(
|
||||
io,
|
||||
allocator,
|
||||
svc,
|
||||
file_path,
|
||||
args.events_enabled,
|
||||
args.vs_date,
|
||||
args.as_of orelse today,
|
||||
args.as_of != null,
|
||||
color,
|
||||
out,
|
||||
);
|
||||
},
|
||||
.bands => |args| {
|
||||
const svc = ctx.svc orelse return error.MissingDataService;
|
||||
try runBands(
|
||||
io,
|
||||
allocator,
|
||||
svc,
|
||||
file_path,
|
||||
args.events_enabled,
|
||||
args.as_of orelse today,
|
||||
args.as_of != null,
|
||||
today,
|
||||
args.overlay_actuals,
|
||||
color,
|
||||
out,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// How an as-of date resolved against the history directory. The CLI
|
||||
/// uses this to render a single header that tells the user what
|
||||
/// actually got loaded (exact hit, nearest-earlier, or straight-up
|
||||
|
|
@ -50,7 +280,7 @@ const AsOfResolution = struct {
|
|||
liquid: f64 = 0,
|
||||
};
|
||||
|
||||
/// Run projections.
|
||||
/// Run percentile-bands projection (the default mode).
|
||||
///
|
||||
/// `as_of` is the reference date for ages, horizons, and snapshot
|
||||
/// windows. `from_snapshot` selects the data source:
|
||||
|
|
@ -58,7 +288,7 @@ const AsOfResolution = struct {
|
|||
/// today as `as_of`.
|
||||
/// - `true`: historical mode. Load the snapshot at-or-before
|
||||
/// `as_of` from the history dir.
|
||||
pub fn run(
|
||||
pub fn runBands(
|
||||
io: std.Io,
|
||||
allocator: std.mem.Allocator,
|
||||
svc: *zfin.DataService,
|
||||
|
|
@ -1106,14 +1336,149 @@ fn renderEarliestBlock(out: *std.Io.Writer, color: bool, va: std.mem.Allocator,
|
|||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────
|
||||
//
|
||||
// The projections simulation and rendering are covered by the
|
||||
// view-model tests in `src/views/projections.zig` and the analytics
|
||||
// tests in `src/analytics/`. These tests focus on the CLI-surface
|
||||
// behaviour that `run` is responsible for: as-of snapshot resolution,
|
||||
// exact/nearest/miss branching, and error reporting.
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
fn parseArgsForTest(today: Date, args: []const []const u8) !ParsedArgs {
|
||||
var ctx: framework.RunCtx = .{
|
||||
.io = std.testing.io,
|
||||
.allocator = std.testing.allocator,
|
||||
.gpa = std.testing.allocator,
|
||||
.environ_map = undefined,
|
||||
.config = .{ .cache_dir = "" },
|
||||
.svc = null,
|
||||
.globals = .{},
|
||||
.today = today,
|
||||
.now_s = 0,
|
||||
.color = false,
|
||||
.out = undefined,
|
||||
};
|
||||
return parseArgs(&ctx, args);
|
||||
}
|
||||
|
||||
test "parseArgs: empty → bands variant with defaults" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const parsed = try parseArgsForTest(today, &.{});
|
||||
switch (parsed) {
|
||||
.bands => |b| {
|
||||
try testing.expect(b.events_enabled);
|
||||
try testing.expect(b.as_of == null);
|
||||
try testing.expect(!b.overlay_actuals);
|
||||
},
|
||||
else => try testing.expect(false),
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: --as-of populates bands.as_of" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{ "--as-of", "2026-04-01" };
|
||||
const parsed = try parseArgsForTest(today, &args);
|
||||
switch (parsed) {
|
||||
.bands => |b| try testing.expect(b.as_of.?.eql(Date.fromYmd(2026, 4, 1))),
|
||||
else => try testing.expect(false),
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: --no-events disables events on bands" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{"--no-events"};
|
||||
const parsed = try parseArgsForTest(today, &args);
|
||||
switch (parsed) {
|
||||
.bands => |b| try testing.expect(!b.events_enabled),
|
||||
else => try testing.expect(false),
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: --vs produces compare variant" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{ "--vs", "2026-04-01" };
|
||||
const parsed = try parseArgsForTest(today, &args);
|
||||
switch (parsed) {
|
||||
.compare => |c| try testing.expect(c.vs_date.eql(Date.fromYmd(2026, 4, 1))),
|
||||
else => try testing.expect(false),
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: --vs + --as-of carries both into compare variant" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{ "--vs", "2026-03-01", "--as-of", "2026-04-01" };
|
||||
const parsed = try parseArgsForTest(today, &args);
|
||||
switch (parsed) {
|
||||
.compare => |c| {
|
||||
try testing.expect(c.vs_date.eql(Date.fromYmd(2026, 3, 1)));
|
||||
try testing.expect(c.as_of.?.eql(Date.fromYmd(2026, 4, 1)));
|
||||
},
|
||||
else => try testing.expect(false),
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: --convergence produces convergence variant" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{"--convergence"};
|
||||
const parsed = try parseArgsForTest(today, &args);
|
||||
try testing.expectEqual(std.meta.Tag(ParsedArgs).convergence, std.meta.activeTag(parsed));
|
||||
}
|
||||
|
||||
test "parseArgs: --return-backtest produces return_backtest variant" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{"--return-backtest"};
|
||||
const parsed = try parseArgsForTest(today, &args);
|
||||
switch (parsed) {
|
||||
.return_backtest => |rb| try testing.expect(!rb.real),
|
||||
else => try testing.expect(false),
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: --return-backtest --real" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{ "--return-backtest", "--real" };
|
||||
const parsed = try parseArgsForTest(today, &args);
|
||||
switch (parsed) {
|
||||
.return_backtest => |rb| try testing.expect(rb.real),
|
||||
else => try testing.expect(false),
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: --convergence + --return-backtest mutually exclusive" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{ "--convergence", "--return-backtest" };
|
||||
try testing.expectError(error.MutuallyExclusive, parseArgsForTest(today, &args));
|
||||
}
|
||||
|
||||
test "parseArgs: --convergence + --vs rejected" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{ "--convergence", "--vs", "2026-04-01" };
|
||||
try testing.expectError(error.MutuallyExclusive, parseArgsForTest(today, &args));
|
||||
}
|
||||
|
||||
test "parseArgs: --real without --return-backtest rejected" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{"--real"};
|
||||
try testing.expectError(error.MutuallyExclusive, parseArgsForTest(today, &args));
|
||||
}
|
||||
|
||||
test "parseArgs: future --as-of rejected" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{ "--as-of", "2027-01-01" };
|
||||
try testing.expectError(error.InvalidFlagValue, parseArgsForTest(today, &args));
|
||||
}
|
||||
|
||||
test "parseArgs: unknown flag errors" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{"--bogus"};
|
||||
try testing.expectError(error.UnexpectedArg, parseArgsForTest(today, &args));
|
||||
}
|
||||
|
||||
test "parseArgs: --overlay-actuals carries into bands" {
|
||||
const today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{ "--as-of", "2026-04-01", "--overlay-actuals" };
|
||||
const parsed = try parseArgsForTest(today, &args);
|
||||
switch (parsed) {
|
||||
.bands => |b| try testing.expect(b.overlay_actuals),
|
||||
else => try testing.expect(false),
|
||||
}
|
||||
}
|
||||
|
||||
const snapshot_model = @import("../models/snapshot.zig");
|
||||
const snapshot = @import("snapshot.zig");
|
||||
|
||||
|
|
@ -1280,7 +1645,7 @@ test "run: as_of with no snapshots returns without error (stderr-only)" {
|
|||
var stream = std.Io.Writer.fixed(&buf);
|
||||
|
||||
const d = Date.fromYmd(2026, 3, 13);
|
||||
try run(io, testing.allocator, &svc, pf, false, d, true, d, false, false, &stream);
|
||||
try runBands(io, testing.allocator, &svc, pf, false, d, true, d, false, false, &stream);
|
||||
|
||||
// No body output because the resolution failed — the stderr
|
||||
// message is swallowed by `cli.stderrPrint` and doesn't land in
|
||||
|
|
@ -1312,7 +1677,7 @@ test "run: as_of with matching snapshot produces body output" {
|
|||
|
||||
var buf: [32_768]u8 = undefined;
|
||||
var stream = std.Io.Writer.fixed(&buf);
|
||||
try run(io, testing.allocator, &svc, pf, false, d, true, d, false, false, &stream);
|
||||
try runBands(io, testing.allocator, &svc, pf, false, d, true, d, false, false, &stream);
|
||||
|
||||
const out = stream.buffered();
|
||||
// Header should call out the as-of date explicitly.
|
||||
|
|
@ -1343,7 +1708,7 @@ test "run: as_of auto-snap surfaces muted 'nearest' note" {
|
|||
var stream = std.Io.Writer.fixed(&buf);
|
||||
|
||||
const requested = Date.fromYmd(2026, 3, 13);
|
||||
try run(io, testing.allocator, &svc, pf, false, requested, true, requested, false, false, &stream);
|
||||
try runBands(io, testing.allocator, &svc, pf, false, requested, true, requested, false, false, &stream);
|
||||
|
||||
const out = stream.buffered();
|
||||
try testing.expect(std.mem.indexOf(u8, out, "as of 2026-03-12") != null);
|
||||
|
|
|
|||
101
src/main.zig
101
src/main.zig
|
|
@ -25,6 +25,7 @@ const command_modules = .{
|
|||
// Portfolio analysis
|
||||
.portfolio = @import("commands/portfolio.zig"),
|
||||
.analysis = @import("commands/analysis.zig"),
|
||||
.projections = @import("commands/projections.zig"),
|
||||
.milestones = @import("commands/milestones.zig"),
|
||||
|
||||
// Time-series & journaling
|
||||
|
|
@ -33,6 +34,7 @@ const command_modules = .{
|
|||
.contributions = @import("commands/contributions.zig"),
|
||||
|
||||
// Data hygiene
|
||||
.audit = @import("commands/audit.zig"),
|
||||
.enrich = @import("commands/enrich.zig"),
|
||||
.lookup = @import("commands/lookup.zig"),
|
||||
|
||||
|
|
@ -482,105 +484,6 @@ fn runCli(init: std.process.Init) !u8 {
|
|||
if (std.mem.eql(u8, command, "portfolio")) {
|
||||
// Parse --refresh flag; reject any other token (including old
|
||||
// positional FILE, which is now a global -p).
|
||||
} else if (std.mem.eql(u8, command, "projections")) {
|
||||
var events_enabled = true;
|
||||
var as_of: ?zfin.Date = null;
|
||||
var vs_date: ?zfin.Date = null;
|
||||
var overlay_actuals = false;
|
||||
var convergence = false;
|
||||
var return_backtest = false;
|
||||
var real_mode = false;
|
||||
var i: usize = 0;
|
||||
while (i < cmd_args.len) : (i += 1) {
|
||||
const a = cmd_args[i];
|
||||
if (std.mem.eql(u8, a, "--no-events")) {
|
||||
events_enabled = false;
|
||||
} else if (std.mem.eql(u8, a, "--overlay-actuals")) {
|
||||
overlay_actuals = true;
|
||||
} else if (std.mem.eql(u8, a, "--convergence")) {
|
||||
convergence = true;
|
||||
} else if (std.mem.eql(u8, a, "--return-backtest")) {
|
||||
return_backtest = true;
|
||||
} else if (std.mem.eql(u8, a, "--real")) {
|
||||
real_mode = true;
|
||||
} else if (std.mem.eql(u8, a, "--as-of") or std.mem.eql(u8, a, "--vs")) {
|
||||
if (i + 1 >= cmd_args.len) {
|
||||
try cli.stderrPrint(io, "Error: ");
|
||||
try cli.stderrPrint(io, a);
|
||||
try cli.stderrPrint(io, " requires a value (YYYY-MM-DD, N[WMQY], or 'live').\n");
|
||||
return 1;
|
||||
}
|
||||
const value = cmd_args[i + 1];
|
||||
const parsed = cli.parseAsOfDate(value, today) catch |err| {
|
||||
var buf: [256]u8 = undefined;
|
||||
const msg = cli.fmtAsOfParseError(&buf, value, err);
|
||||
try cli.stderrPrint(io, msg);
|
||||
try cli.stderrPrint(io, "\n");
|
||||
return 1;
|
||||
};
|
||||
if (parsed) |d| {
|
||||
if (d.days > today.days) {
|
||||
try cli.stderrPrint(io, "Error: date is in the future.\n");
|
||||
return 1;
|
||||
}
|
||||
if (std.mem.eql(u8, a, "--as-of")) {
|
||||
as_of = d;
|
||||
} else {
|
||||
vs_date = d;
|
||||
}
|
||||
}
|
||||
// null (= "live") is ignored — leaves flag unset, same
|
||||
// as not passing the flag at all.
|
||||
i += 1; // consume the value
|
||||
} else {
|
||||
try reportUnexpectedArg(io, "projections", a);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Mutually-exclusive view flags. Forecast-evaluation flags
|
||||
// (`--convergence`, `--return-backtest`) replace the default
|
||||
// bands view entirely; combining them with each other,
|
||||
// `--vs`, or `--overlay-actuals` is rejected.
|
||||
if (convergence and return_backtest) {
|
||||
try cli.stderrPrint(io, "Error: --convergence and --return-backtest are mutually exclusive.\n");
|
||||
return 1;
|
||||
}
|
||||
if ((convergence or return_backtest) and vs_date != null) {
|
||||
try cli.stderrPrint(io, "Error: --convergence/--return-backtest cannot be combined with --vs.\n");
|
||||
return 1;
|
||||
}
|
||||
if ((convergence or return_backtest) and overlay_actuals) {
|
||||
try cli.stderrPrint(io, "Error: --convergence/--return-backtest cannot be combined with --overlay-actuals.\n");
|
||||
return 1;
|
||||
}
|
||||
if (real_mode and !return_backtest) {
|
||||
try cli.stderrPrint(io, "Error: --real only applies to --return-backtest.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (as_of != null and vs_date == null) {
|
||||
// Single-date mode: view that snapshot only.
|
||||
}
|
||||
const pf = resolveUserPath(io, allocator, config, globals.portfolio_path, zfin.Config.default_portfolio_filename);
|
||||
defer if (pf.resolved) |r| r.deinit(allocator);
|
||||
if (convergence) {
|
||||
try commands.projections.runConvergence(io, allocator, pf.path, color, out);
|
||||
} else if (return_backtest) {
|
||||
try commands.projections.runReturnBacktest(io, allocator, pf.path, real_mode, color, out);
|
||||
} else if (vs_date) |d| {
|
||||
// Compare mode. `as_of` (if set) designates the "now"
|
||||
// side — otherwise now is live. `--vs` alone compares
|
||||
// live against a historical date; `--vs X --as-of Y`
|
||||
// compares two historical dates with Y being the later
|
||||
// one.
|
||||
if (overlay_actuals) {
|
||||
try cli.stderrPrint(io, "Note: --overlay-actuals is ignored in --vs compare mode.\n");
|
||||
}
|
||||
try commands.projections.runCompare(io, allocator, &svc, pf.path, events_enabled, d, as_of orelse today, as_of != null, color, out);
|
||||
} else {
|
||||
try commands.projections.run(io, allocator, &svc, pf.path, events_enabled, as_of orelse today, as_of != null, today, overlay_actuals, color, out);
|
||||
}
|
||||
} else {
|
||||
try cli.stderrPrint(io, "Unknown command. Run 'zfin help' for usage.\n");
|
||||
return 1;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue