migrate milestones to new cli framework
This commit is contained in:
parent
b3207684a7
commit
c572f6be63
2 changed files with 109 additions and 57 deletions
|
|
@ -20,6 +20,7 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const zfin = @import("../root.zig");
|
const zfin = @import("../root.zig");
|
||||||
const cli = @import("common.zig");
|
const cli = @import("common.zig");
|
||||||
|
const framework = @import("framework.zig");
|
||||||
const fmt = @import("../format.zig");
|
const fmt = @import("../format.zig");
|
||||||
const Money = @import("../Money.zig");
|
const Money = @import("../Money.zig");
|
||||||
const history = @import("../history.zig");
|
const history = @import("../history.zig");
|
||||||
|
|
@ -27,6 +28,45 @@ const Date = @import("../Date.zig");
|
||||||
const milestones = @import("../analytics/milestones.zig");
|
const milestones = @import("../analytics/milestones.zig");
|
||||||
const shiller = @import("../data/shiller.zig");
|
const shiller = @import("../data/shiller.zig");
|
||||||
|
|
||||||
|
pub const ParsedArgs = struct {
|
||||||
|
/// Raw `--step` value (e.g. `"1M"`, `"2x"`). Resolved into a typed
|
||||||
|
/// step inside `run` so `parseStep`'s detailed error reporting can
|
||||||
|
/// surface to the user with the right framing.
|
||||||
|
step_raw: []const u8,
|
||||||
|
real: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const meta = struct {
|
||||||
|
pub const name: []const u8 = "milestones";
|
||||||
|
pub const group: framework.Group = .portfolio;
|
||||||
|
pub const synopsis: []const u8 = "Show portfolio threshold crossings (each $1M, doublings, etc.)";
|
||||||
|
pub const help: []const u8 =
|
||||||
|
\\Usage: zfin milestones --step <expr> [--real]
|
||||||
|
\\
|
||||||
|
\\Find the dates the portfolio first reached each of a
|
||||||
|
\\configured set of thresholds. Two threshold modes:
|
||||||
|
\\
|
||||||
|
\\ Absolute dollar: 1M / 1m / 1500000 / 1.5M / 500K / 500k
|
||||||
|
\\ Relative multiplier: 2x / 2X / 1.5x
|
||||||
|
\\
|
||||||
|
\\Rejects %, ≤0 dollar steps, ≤1.0x multipliers, NaN/Inf.
|
||||||
|
\\
|
||||||
|
\\Options:
|
||||||
|
\\ --step <expr> Threshold step (required).
|
||||||
|
\\ --real Deflate the series to the last full Shiller
|
||||||
|
\\ year before detecting crossings (CPI-adjusted).
|
||||||
|
\\ Default is nominal.
|
||||||
|
\\
|
||||||
|
\\Note: crossing dates are "first observed at," bounded by the
|
||||||
|
\\source series cadence (typically weekly).
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
framework.validateCommandModule(@This());
|
||||||
|
}
|
||||||
|
|
||||||
pub const RunError = error{
|
pub const RunError = error{
|
||||||
UnexpectedArg,
|
UnexpectedArg,
|
||||||
MissingStep,
|
MissingStep,
|
||||||
|
|
@ -37,79 +77,60 @@ pub const RunError = error{
|
||||||
} || std.Io.Reader.Error || std.Io.Writer.Error || std.fs.File.OpenError ||
|
} || std.Io.Reader.Error || std.Io.Writer.Error || std.fs.File.OpenError ||
|
||||||
std.posix.RealPathError;
|
std.posix.RealPathError;
|
||||||
|
|
||||||
const usage_text =
|
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
|
||||||
\\Usage: zfin milestones --step <expr> [--real]
|
|
||||||
\\
|
|
||||||
\\ --step <expr> Threshold step. Examples:
|
|
||||||
\\ 1M / 1m / 1500000 / 1.5M (absolute dollar)
|
|
||||||
\\ 500K / 500k (absolute thousands)
|
|
||||||
\\ 2x / 2X / 1.5x (relative multiplier)
|
|
||||||
\\ Rejects %, <=0, <=1.0x, NaN, Inf.
|
|
||||||
\\ --real Deflate the series to the last full Shiller year
|
|
||||||
\\ before detecting crossings (CPI-adjusted dollars).
|
|
||||||
\\ Default is nominal.
|
|
||||||
\\ -h, --help Show this help.
|
|
||||||
\\
|
|
||||||
\\Note: crossing dates are "first observed at," bounded by the
|
|
||||||
\\source series cadence (typically weekly).
|
|
||||||
\\
|
|
||||||
;
|
|
||||||
|
|
||||||
pub fn run(
|
|
||||||
io: std.Io,
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
svc: *zfin.DataService,
|
|
||||||
portfolio_path: []const u8,
|
|
||||||
args: []const []const u8,
|
|
||||||
today: Date,
|
|
||||||
color: bool,
|
|
||||||
out: *std.Io.Writer,
|
|
||||||
) !void {
|
|
||||||
_ = svc; // milestones reads local files only
|
|
||||||
_ = today; // current behavior: detection bounded by series, no "now"
|
|
||||||
var step_str: ?[]const u8 = null;
|
var step_str: ?[]const u8 = null;
|
||||||
var want_real = false;
|
var want_real = false;
|
||||||
|
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
while (i < args.len) : (i += 1) {
|
while (i < cmd_args.len) : (i += 1) {
|
||||||
const a = args[i];
|
const a = cmd_args[i];
|
||||||
if (std.mem.eql(u8, a, "--step")) {
|
if (std.mem.eql(u8, a, "--step")) {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i >= args.len) {
|
if (i >= cmd_args.len) {
|
||||||
try cli.stderrPrint(io, "Error: --step requires an argument\n");
|
try cli.stderrPrint(ctx.io, "Error: --step requires an argument\n");
|
||||||
return error.MissingStep;
|
return error.MissingStep;
|
||||||
}
|
}
|
||||||
step_str = args[i];
|
step_str = cmd_args[i];
|
||||||
} else if (std.mem.eql(u8, a, "--real")) {
|
} else if (std.mem.eql(u8, a, "--real")) {
|
||||||
want_real = true;
|
want_real = true;
|
||||||
} else if (std.mem.eql(u8, a, "-h") or std.mem.eql(u8, a, "--help")) {
|
|
||||||
try out.writeAll(usage_text);
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
try cli.stderrPrint(io, "Error: unknown argument to 'milestones': ");
|
try cli.stderrPrint(ctx.io, "Error: unknown argument to 'milestones': ");
|
||||||
try cli.stderrPrint(io, a);
|
try cli.stderrPrint(ctx.io, a);
|
||||||
try cli.stderrPrint(io, "\n");
|
try cli.stderrPrint(ctx.io, "\n");
|
||||||
return error.UnexpectedArg;
|
return error.UnexpectedArg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const step_input = step_str orelse {
|
const step_raw = step_str orelse {
|
||||||
try cli.stderrPrint(io, "Error: --step is required\n");
|
try cli.stderrPrint(ctx.io, "Error: --step is required\n");
|
||||||
try cli.stderrPrint(io, usage_text);
|
try cli.stderrPrint(ctx.io, meta.help);
|
||||||
return error.MissingStep;
|
return error.MissingStep;
|
||||||
};
|
};
|
||||||
|
return .{ .step_raw = step_raw, .real = want_real };
|
||||||
|
}
|
||||||
|
|
||||||
const step = milestones.parseStep(step_input) catch |err| {
|
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 want_real = parsed.real;
|
||||||
|
|
||||||
|
const step = milestones.parseStep(parsed.step_raw) catch |err| {
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
const msg = std.fmt.bufPrint(
|
const msg = std.fmt.bufPrint(
|
||||||
&buf,
|
&buf,
|
||||||
"Error: cannot parse --step '{s}': {s}\n",
|
"Error: cannot parse --step '{s}': {s}\n",
|
||||||
.{ step_input, @errorName(err) },
|
.{ parsed.step_raw, @errorName(err) },
|
||||||
) catch "Error: invalid --step\n";
|
) catch "Error: invalid --step\n";
|
||||||
try cli.stderrPrint(io, msg);
|
try cli.stderrPrint(io, msg);
|
||||||
return error.InvalidStep;
|
return error.InvalidStep;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pf = ctx.resolvePortfolioPath();
|
||||||
|
defer pf.deinit(allocator);
|
||||||
|
const portfolio_path = pf.path;
|
||||||
|
|
||||||
// Load merged series.
|
// Load merged series.
|
||||||
var series_owned = try loadMergedSeries(io, allocator, portfolio_path);
|
var series_owned = try loadMergedSeries(io, allocator, portfolio_path);
|
||||||
defer series_owned.deinit(allocator);
|
defer series_owned.deinit(allocator);
|
||||||
|
|
@ -339,6 +360,45 @@ fn renderTable(
|
||||||
|
|
||||||
// ── Tests ────────────────────────────────────────────────────
|
// ── Tests ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test "parseArgs: --step and --real" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{ "--step", "1M", "--real" };
|
||||||
|
const parsed = try parseArgs(&ctx, &args);
|
||||||
|
try std.testing.expectEqualStrings("1M", parsed.step_raw);
|
||||||
|
try std.testing.expect(parsed.real);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: --step only" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{ "--step", "2x" };
|
||||||
|
const parsed = try parseArgs(&ctx, &args);
|
||||||
|
try std.testing.expectEqualStrings("2x", parsed.step_raw);
|
||||||
|
try std.testing.expect(!parsed.real);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: missing --step errors" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{};
|
||||||
|
try std.testing.expectError(error.MissingStep, parseArgs(&ctx, &args));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: --step without value errors" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{"--step"};
|
||||||
|
try std.testing.expectError(error.MissingStep, parseArgs(&ctx, &args));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: unknown flag errors" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{ "--step", "1M", "--bogus" };
|
||||||
|
try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
|
||||||
|
}
|
||||||
|
|
||||||
test "loadMergedSeries: empty when no history" {
|
test "loadMergedSeries: empty when no history" {
|
||||||
const io = std.testing.io;
|
const io = std.testing.io;
|
||||||
var tmp_dir = std.testing.tmpDir(.{});
|
var tmp_dir = std.testing.tmpDir(.{});
|
||||||
|
|
|
||||||
14
src/main.zig
14
src/main.zig
|
|
@ -22,6 +22,9 @@ const command_modules = .{
|
||||||
.earnings = @import("commands/earnings.zig"),
|
.earnings = @import("commands/earnings.zig"),
|
||||||
.etf = @import("commands/etf.zig"),
|
.etf = @import("commands/etf.zig"),
|
||||||
|
|
||||||
|
// Portfolio analysis
|
||||||
|
.milestones = @import("commands/milestones.zig"),
|
||||||
|
|
||||||
// Data hygiene
|
// Data hygiene
|
||||||
.enrich = @import("commands/enrich.zig"),
|
.enrich = @import("commands/enrich.zig"),
|
||||||
.lookup = @import("commands/lookup.zig"),
|
.lookup = @import("commands/lookup.zig"),
|
||||||
|
|
@ -721,17 +724,6 @@ fn runCli(init: std.process.Init) !u8 {
|
||||||
=> return 1,
|
=> return 1,
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
} else if (std.mem.eql(u8, command, "milestones")) {
|
|
||||||
const pf = resolveUserPath(io, allocator, config, globals.portfolio_path, zfin.Config.default_portfolio_filename);
|
|
||||||
defer if (pf.resolved) |r| r.deinit(allocator);
|
|
||||||
commands.milestones.run(io, allocator, &svc, pf.path, cmd_args, today, color, out) catch |err| switch (err) {
|
|
||||||
error.UnexpectedArg,
|
|
||||||
error.MissingStep,
|
|
||||||
error.InvalidStep,
|
|
||||||
error.NoData,
|
|
||||||
=> return 1,
|
|
||||||
else => return err,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
try cli.stderrPrint(io, "Unknown command. Run 'zfin help' for usage.\n");
|
try cli.stderrPrint(io, "Unknown command. Run 'zfin help' for usage.\n");
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue