migrate history to new cli framework
This commit is contained in:
parent
63f5cc445b
commit
1228d50ce6
2 changed files with 126 additions and 47 deletions
|
|
@ -32,6 +32,7 @@ const std = @import("std");
|
|||
const srf = @import("srf");
|
||||
const zfin = @import("../root.zig");
|
||||
const cli = @import("common.zig");
|
||||
const framework = @import("framework.zig");
|
||||
const atomic = @import("../atomic.zig");
|
||||
const timeline = @import("../analytics/timeline.zig");
|
||||
const history = @import("../history.zig");
|
||||
|
|
@ -40,6 +41,54 @@ const view = @import("../views/history.zig");
|
|||
const fmt = cli.fmt;
|
||||
const Date = @import("../Date.zig");
|
||||
|
||||
/// Parsed args for `zfin history`. Tagged union since the command has
|
||||
/// two genuinely different modes:
|
||||
///
|
||||
/// - Symbol mode: `zfin history <SYMBOL>` shows the last 30 days of
|
||||
/// candles for one symbol. Triggered when `args[0]` exists and
|
||||
/// doesn't start with `-`.
|
||||
/// - Portfolio mode: `zfin history [flags]` shows the portfolio-value
|
||||
/// timeline derived from `history/*-portfolio.srf` snapshots.
|
||||
/// Triggered when there are no positional args (or only flags).
|
||||
pub const ParsedArgs = union(enum) {
|
||||
symbol: []const u8,
|
||||
portfolio: PortfolioOpts,
|
||||
};
|
||||
|
||||
pub const meta = struct {
|
||||
pub const name: []const u8 = "history";
|
||||
pub const group: framework.Group = .symbol_lookup;
|
||||
pub const synopsis: []const u8 = "Price history (symbol) or portfolio value timeline";
|
||||
pub const uppercase_first_arg: bool = true;
|
||||
pub const help: []const u8 =
|
||||
\\Usage:
|
||||
\\ zfin history <SYMBOL> # last 30 days of candles
|
||||
\\ zfin history [flags] # portfolio-value timeline
|
||||
\\
|
||||
\\Two modes in one command. Symbol mode (positional symbol)
|
||||
\\shows the last 30 trading days of candles for that symbol.
|
||||
\\Portfolio mode (no positional, optionally with flags) reads
|
||||
\\`history/*-portfolio.srf` snapshots and renders rolling-windows
|
||||
\\returns + a braille chart + a recent-snapshots table.
|
||||
\\
|
||||
\\Portfolio-mode flags:
|
||||
\\ --since <DATE> earliest as-of date (inclusive)
|
||||
\\ --until <DATE> latest as-of date (inclusive)
|
||||
\\ --metric <name> liquid (default), illiquid, or net_worth
|
||||
\\ --resolution <name> daily | weekly | monthly | auto
|
||||
\\ (auto: daily ≤90d, weekly ≤730d, else monthly)
|
||||
\\ --limit <N> cap recent-snapshots table to N rows (default 40)
|
||||
\\ --rebuild-rollup regenerate history/rollup.srf and exit
|
||||
\\
|
||||
\\DATE accepts YYYY-MM-DD or relative shortcuts (1W/1M/1Q/1Y).
|
||||
\\
|
||||
;
|
||||
};
|
||||
|
||||
comptime {
|
||||
framework.validateCommandModule(@This());
|
||||
}
|
||||
|
||||
pub const Error = error{
|
||||
UnexpectedArg,
|
||||
InvalidFlagValue,
|
||||
|
|
@ -116,35 +165,39 @@ pub fn parsePortfolioOpts(as_of: zfin.Date, args: []const []const u8) Error!Port
|
|||
return opts;
|
||||
}
|
||||
|
||||
/// Entry point. Dispatches to symbol mode or portfolio mode based on
|
||||
/// the first argument.
|
||||
pub fn run(
|
||||
io: std.Io,
|
||||
allocator: std.mem.Allocator,
|
||||
svc: *zfin.DataService,
|
||||
portfolio_path: []const u8,
|
||||
args: []const []const u8,
|
||||
as_of: zfin.Date,
|
||||
color: bool,
|
||||
out: *std.Io.Writer,
|
||||
) !void {
|
||||
if (args.len > 0 and args[0].len > 0 and args[0][0] != '-') {
|
||||
try runSymbol(io, svc, args[0], as_of, color, out);
|
||||
return;
|
||||
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
|
||||
if (cmd_args.len > 0 and cmd_args[0].len > 0 and cmd_args[0][0] != '-') {
|
||||
if (cmd_args.len > 1) {
|
||||
try cli.stderrPrint(ctx.io, "Error: 'history' symbol mode takes only the symbol argument\n");
|
||||
return error.UnexpectedArg;
|
||||
}
|
||||
return .{ .symbol = cmd_args[0] };
|
||||
}
|
||||
|
||||
const opts = parsePortfolioOpts(as_of, args) catch |err| {
|
||||
const opts = parsePortfolioOpts(ctx.today, cmd_args) catch |err| {
|
||||
switch (err) {
|
||||
error.UnexpectedArg => try cli.stderrPrint(io, "Error: unknown flag in 'history'. See --help.\n"),
|
||||
error.MissingFlagValue => try cli.stderrPrint(io, "Error: flag requires a value.\n"),
|
||||
error.InvalidFlagValue => try cli.stderrPrint(io, "Error: invalid flag value.\n"),
|
||||
error.UnknownMetric => try cli.stderrPrint(io, "Error: unknown --metric. Valid: net_worth, liquid, illiquid.\n"),
|
||||
error.UnknownResolution => try cli.stderrPrint(io, "Error: unknown --resolution. Valid: daily, weekly, monthly, auto.\n"),
|
||||
error.UnexpectedArg => try cli.stderrPrint(ctx.io, "Error: unknown flag in 'history'. See --help.\n"),
|
||||
error.MissingFlagValue => try cli.stderrPrint(ctx.io, "Error: flag requires a value.\n"),
|
||||
error.InvalidFlagValue => try cli.stderrPrint(ctx.io, "Error: invalid flag value.\n"),
|
||||
error.UnknownMetric => try cli.stderrPrint(ctx.io, "Error: unknown --metric. Valid: net_worth, liquid, illiquid.\n"),
|
||||
error.UnknownResolution => try cli.stderrPrint(ctx.io, "Error: unknown --resolution. Valid: daily, weekly, monthly, auto.\n"),
|
||||
}
|
||||
return err;
|
||||
};
|
||||
return .{ .portfolio = opts };
|
||||
}
|
||||
|
||||
try runPortfolio(io, allocator, portfolio_path, opts, color, out);
|
||||
/// Entry point. Dispatches to symbol mode or portfolio mode based on
|
||||
/// the parsed args.
|
||||
pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||
const svc = ctx.svc orelse return error.MissingDataService;
|
||||
switch (parsed) {
|
||||
.symbol => |sym| try runSymbol(ctx.io, svc, sym, ctx.today, ctx.color, ctx.out),
|
||||
.portfolio => |opts| {
|
||||
const pf = ctx.resolvePortfolioPath();
|
||||
defer pf.deinit(ctx.allocator);
|
||||
try runPortfolio(ctx.io, ctx.allocator, pf.path, opts, ctx.color, ctx.out);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── Symbol mode (legacy) ─────────────────────────────────────
|
||||
|
|
@ -627,6 +680,54 @@ fn renderCascadingTable(
|
|||
|
||||
const testing = std.testing;
|
||||
|
||||
test "parseArgs: positional symbol → .symbol variant" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
ctx.today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{"AAPL"};
|
||||
const parsed = try parseArgs(&ctx, &args);
|
||||
switch (parsed) {
|
||||
.symbol => |s| try std.testing.expectEqualStrings("AAPL", s),
|
||||
.portfolio => try std.testing.expect(false),
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: empty args → .portfolio variant with defaults" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
ctx.today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{};
|
||||
const parsed = try parseArgs(&ctx, &args);
|
||||
switch (parsed) {
|
||||
.symbol => try std.testing.expect(false),
|
||||
.portfolio => |opts| {
|
||||
try std.testing.expect(opts.since == null);
|
||||
try std.testing.expect(opts.until == null);
|
||||
try std.testing.expectEqual(timeline.Metric.liquid, opts.metric);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: --since flag → .portfolio variant" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
ctx.today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{ "--since", "2026-01-01" };
|
||||
const parsed = try parseArgs(&ctx, &args);
|
||||
switch (parsed) {
|
||||
.symbol => try std.testing.expect(false),
|
||||
.portfolio => |opts| try std.testing.expect(opts.since.?.eql(Date.fromYmd(2026, 1, 1))),
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: symbol mode rejects extra positional" {
|
||||
var ctx: framework.RunCtx = undefined;
|
||||
ctx.io = std.testing.io;
|
||||
ctx.today = Date.fromYmd(2026, 5, 9);
|
||||
const args = [_][]const u8{ "AAPL", "MSFT" };
|
||||
try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
|
||||
}
|
||||
|
||||
test "parsePortfolioOpts: defaults" {
|
||||
const o = try parsePortfolioOpts(zfin.Date.fromYmd(2026, 5, 8), &.{});
|
||||
try testing.expect(o.since == null);
|
||||
|
|
|
|||
26
src/main.zig
26
src/main.zig
|
|
@ -15,6 +15,7 @@ const command_modules = .{
|
|||
// Per-symbol lookups
|
||||
.perf = @import("commands/perf.zig"),
|
||||
.quote = @import("commands/quote.zig"),
|
||||
.history = @import("commands/history.zig"),
|
||||
.divs = @import("commands/divs.zig"),
|
||||
.splits = @import("commands/splits.zig"),
|
||||
.options = @import("commands/options.zig"),
|
||||
|
|
@ -468,30 +469,7 @@ fn runCli(init: std.process.Init) !u8 {
|
|||
cmd_args = owned;
|
||||
}
|
||||
|
||||
if (std.mem.eql(u8, command, "history")) {
|
||||
// Two modes in one command:
|
||||
// zfin history <SYMBOL> → candle history for a symbol (legacy)
|
||||
// zfin history [flags] → portfolio timeline from history/*.srf
|
||||
//
|
||||
// Only portfolio mode needs portfolio.srf; symbol mode must keep
|
||||
// working in directories without a configured portfolio. Dispatch
|
||||
// at this level so that constraint is visible here, not buried
|
||||
// inside the command.
|
||||
const is_symbol_mode = cmd_args.len > 0 and cmd_args[0].len > 0 and cmd_args[0][0] != '-';
|
||||
if (is_symbol_mode) {
|
||||
commands.history.run(io, allocator, &svc, "", cmd_args, today, color, out) catch |err| switch (err) {
|
||||
error.UnexpectedArg, error.MissingFlagValue, error.InvalidFlagValue, error.UnknownMetric => return 1,
|
||||
else => return err,
|
||||
};
|
||||
} else {
|
||||
const pf = resolveUserPath(io, allocator, config, globals.portfolio_path, zfin.Config.default_portfolio_filename);
|
||||
defer if (pf.resolved) |r| r.deinit(allocator);
|
||||
commands.history.run(io, allocator, &svc, pf.path, cmd_args, today, color, out) catch |err| switch (err) {
|
||||
error.UnexpectedArg, error.MissingFlagValue, error.InvalidFlagValue, error.UnknownMetric => return 1,
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
} else if (std.mem.eql(u8, command, "portfolio")) {
|
||||
if (std.mem.eql(u8, command, "portfolio")) {
|
||||
// Parse --refresh flag; reject any other token (including old
|
||||
// positional FILE, which is now a global -p).
|
||||
var force_refresh = false;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue