rework framework to something much more sane

This commit is contained in:
Emil Lerch 2026-05-18 17:39:19 -07:00
parent 3b02c97153
commit 825419c7c3
22 changed files with 597 additions and 601 deletions

View file

@ -7,23 +7,24 @@ const Money = @import("../Money.zig");
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "analysis"; .name = "analysis",
pub const group: framework.Group = .portfolio; .group = .portfolio,
pub const synopsis: []const u8 = "Show portfolio breakdowns by asset class, sector, geo, account, tax type"; .synopsis = "Show portfolio breakdowns by asset class, sector, geo, account, tax type",
pub const help: []const u8 = .help =
\\Usage: zfin analysis \\Usage: zfin analysis
\\ \\
\\Show portfolio analysis: equities/fixed-income split, plus \\Show portfolio analysis: equities/fixed-income split, plus
\\block-bar breakdowns by asset class, sector, geographic \\block-bar breakdowns by asset class, sector, geographic
\\region, account, and tax type. Reads classifications from \\region, account, and tax type. Reads classifications from
\\`metadata.srf` and account tax types from `accounts.srf` \\`metadata.srf` and account tax types from `accounts.srf`
\\(both in the same directory as the portfolio file). \\(both in the same directory as the portfolio file).
\\ \\
\\Run `zfin enrich <portfolio.srf> > metadata.srf` to bootstrap \\Run `zfin enrich <portfolio.srf> > metadata.srf` to bootstrap
\\classifications, then edit by hand. \\classifications, then edit by hand.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -2250,32 +2250,33 @@ pub const ParsedArgs = struct {
stale_days: u32 = default_stale_days, stale_days: u32 = default_stale_days,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "audit"; .name = "audit",
pub const group: framework.Group = .hygiene; .group = .hygiene,
pub const synopsis: []const u8 = "Reconcile portfolio against brokerage exports + portfolio hygiene check"; .synopsis = "Reconcile portfolio against brokerage exports + portfolio hygiene check",
pub const help: []const u8 = .help =
\\Usage: zfin audit [opts] \\Usage: zfin audit [opts]
\\ \\
\\Two modes in one command: \\Two modes in one command:
\\ \\
\\ Flagless: run the portfolio hygiene check — surfaces stale \\ Flagless: run the portfolio hygiene check — surfaces stale
\\ manual prices, account-cadence violations, and brokerage-file \\ manual prices, account-cadence violations, and brokerage-file
\\ candidates discovered automatically. \\ candidates discovered automatically.
\\ \\
\\ With brokerage flags: reconcile the portfolio against the \\ With brokerage flags: reconcile the portfolio against the
\\ given export and report discrepancies. \\ given export and report discrepancies.
\\ \\
\\Options: \\Options:
\\ --verbose Show full reconciliation output even when clean \\ --verbose Show full reconciliation output even when clean
\\ --stale-days <N> Manual price staleness threshold (default 3) \\ --stale-days <N> Manual price staleness threshold (default 3)
\\ --fidelity <CSV> Fidelity positions CSV export \\ --fidelity <CSV> Fidelity positions CSV export
\\ ("All accounts" → Positions tab → Download) \\ ("All accounts" → Positions tab → Download)
\\ --schwab <CSV> Schwab per-account positions CSV export \\ --schwab <CSV> Schwab per-account positions CSV export
\\ --schwab-summary Schwab account summary; copy from accounts \\ --schwab-summary Schwab account summary; copy from accounts
\\ summary page, paste to stdin, then ^D \\ summary page, paste to stdin, then ^D
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -13,25 +13,26 @@ pub const ParsedArgs = struct {
sub: Subcommand, sub: Subcommand,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "cache"; .name = "cache",
pub const group: framework.Group = .infra; .group = .infra,
pub const synopsis: []const u8 = "Inspect or clear the local provider-data cache"; .synopsis = "Inspect or clear the local provider-data cache",
pub const help: []const u8 = .help =
\\Usage: zfin cache <stats|clear> \\Usage: zfin cache <stats|clear>
\\ \\
\\Subcommands: \\Subcommands:
\\ stats List every cached symbol with per-data-type size, \\ stats List every cached symbol with per-data-type size,
\\ age, and freshness state. Stale entries (past TTL) \\ age, and freshness state. Stale entries (past TTL)
\\ are flagged. Includes the cusip_tickers.srf file \\ are flagged. Includes the cusip_tickers.srf file
\\ if present. \\ if present.
\\ clear Delete every file under the cache directory. \\ clear Delete every file under the cache directory.
\\ No confirmation; the next provider call will \\ No confirmation; the next provider call will
\\ re-fetch everything. \\ re-fetch everything.
\\ \\
\\Cache directory is `$ZFIN_CACHE_DIR` if set, else `~/.cache/zfin`. \\Cache directory is `$ZFIN_CACHE_DIR` if set, else `~/.cache/zfin`.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -87,44 +87,45 @@ pub const ParsedArgs = struct {
commit_after: ?git.CommitSpec = null, commit_after: ?git.CommitSpec = null,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "compare"; .name = "compare",
pub const group: framework.Group = .timeseries; .group = .timeseries,
pub const synopsis: []const u8 = "Compare portfolio at two points in time (or vs. today)"; .synopsis = "Compare portfolio at two points in time (or vs. today)",
pub const help: []const u8 = .help =
\\Usage: \\Usage:
\\ zfin compare <DATE> compare DATE vs. live \\ zfin compare <DATE> compare DATE vs. live
\\ zfin compare <DATE1> <DATE2> compare two historical dates \\ zfin compare <DATE1> <DATE2> compare two historical dates
\\ zfin compare [--snapshot-before DATE] [--snapshot-after DATE] \\ zfin compare [--snapshot-before DATE] [--snapshot-after DATE]
\\ [--commit-before SPEC] [--commit-after SPEC] \\ [--commit-before SPEC] [--commit-after SPEC]
\\ [--projections [--no-events]] \\ [--projections [--no-events]]
\\ \\
\\Compare two points in time for the portfolio: liquid totals, \\Compare two points in time for the portfolio: liquid totals,
\\per-symbol price moves, attribution / contributions diff. The \\per-symbol price moves, attribution / contributions diff. The
\\positional happy-path is what most users want; the named \\positional happy-path is what most users want; the named
\\overrides exist for the cases where the snapshot date and the \\overrides exist for the cases where the snapshot date and the
\\git-commit-at-that-date diverge (e.g. you committed two days \\git-commit-at-that-date diverge (e.g. you committed two days
\\after a Sunday review). \\after a Sunday review).
\\ \\
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y). \\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
\\Commit-spec forms: YYYY-MM-DD, relative, HEAD, HEAD~N, SHA, working. \\Commit-spec forms: YYYY-MM-DD, relative, HEAD, HEAD~N, SHA, working.
\\ \\
\\Options: \\Options:
\\ --projections Include projected-return + SWR@99% \\ --projections Include projected-return + SWR@99%
\\ deltas. Adds ~1-2s per endpoint \\ deltas. Adds ~1-2s per endpoint
\\ (Monte Carlo SWR search). \\ (Monte Carlo SWR search).
\\ --no-events (with --projections) exclude life \\ --no-events (with --projections) exclude life
\\ events from the projection. \\ events from the projection.
\\ --snapshot-before <DATE> Override the before-side snapshot. \\ --snapshot-before <DATE> Override the before-side snapshot.
\\ --snapshot-after <DATE> Override the after-side snapshot; \\ --snapshot-after <DATE> Override the after-side snapshot;
\\ accepts `live` for the current \\ accepts `live` for the current
\\ portfolio. \\ portfolio.
\\ --commit-before <SPEC> Pin the before-side commit for the \\ --commit-before <SPEC> Pin the before-side commit for the
\\ attribution / contributions block. \\ attribution / contributions block.
\\ --commit-after <SPEC> Pin the after-side commit; accepts \\ --commit-after <SPEC> Pin the after-side commit; accepts
\\ `working` for the working copy. \\ `working` for the working copy.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -187,44 +187,45 @@ pub const ParsedArgs = struct {
after: ?git.CommitSpec = null, after: ?git.CommitSpec = null,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "contributions"; .name = "contributions",
pub const group: framework.Group = .timeseries; .group = .timeseries,
pub const synopsis: []const u8 = "Show money added since the last recorded state in git"; .synopsis = "Show money added since the last recorded state in git",
pub const help: []const u8 = .help =
\\Usage: zfin contributions [opts] \\Usage: zfin contributions [opts]
\\ \\
\\Show contributions, withdrawals, and lot-level changes between \\Show contributions, withdrawals, and lot-level changes between
\\two points in the portfolio's git history. Four modes: \\two points in the portfolio's git history. Four modes:
\\ \\
\\ No flags (default): \\ No flags (default):
\\ dirty working tree: HEAD vs working copy \\ dirty working tree: HEAD vs working copy
\\ clean working tree: HEAD~1 vs HEAD (review last commit) \\ clean working tree: HEAD~1 vs HEAD (review last commit)
\\ --since <DATE> commit-at-or-before(DATE) vs HEAD (or \\ --since <DATE> commit-at-or-before(DATE) vs HEAD (or
\\ working copy when dirty) \\ working copy when dirty)
\\ --since <D1> --until <D2> commit-at-or-before(D1) vs \\ --since <D1> --until <D2> commit-at-or-before(D1) vs
\\ commit-at-or-before(D2) \\ commit-at-or-before(D2)
\\ --until <DATE> alone rejected; window is ambiguous \\ --until <DATE> alone rejected; window is ambiguous
\\ \\
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y). \\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
\\ \\
\\Options: \\Options:
\\ --since <DATE> Earliest side (resolves to commit-at- \\ --since <DATE> Earliest side (resolves to commit-at-
\\ or-before). \\ or-before).
\\ --until <DATE> Latest side. Pair with --since. \\ --until <DATE> Latest side. Pair with --since.
\\ --commit-before <SPEC> Pin the before commit directly. Same \\ --commit-before <SPEC> Pin the before commit directly. Same
\\ grammar as --commit-after, minus \\ grammar as --commit-after, minus
\\ `working`. Useful when you committed \\ `working`. Useful when you committed
\\ after your review date. \\ after your review date.
\\ --commit-after <SPEC> Pin the after commit. SPEC accepts \\ --commit-after <SPEC> Pin the after commit. SPEC accepts
\\ YYYY-MM-DD, relative (1W/1M/1Q/1Y), \\ YYYY-MM-DD, relative (1W/1M/1Q/1Y),
\\ HEAD, HEAD~N, hex SHA, or `working` \\ HEAD, HEAD~N, hex SHA, or `working`
\\ for the working copy. \\ for the working copy.
\\ \\
\\--since and --commit-before describe the same axis; pass at most \\--since and --commit-before describe the same axis; pass at most
\\one. Same for --until and --commit-after. \\one. Same for --until and --commit-after.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -8,23 +8,23 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "divs"; .name = "divs",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show dividend history (with TTM yield) for a symbol"; .synopsis = "Show dividend history (with TTM yield) for a symbol",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin divs <SYMBOL> \\Usage: zfin divs <SYMBOL>
\\ \\
\\Show the dividend history for a symbol from Polygon.io. Cached \\Show the dividend history for a symbol from Polygon.io. Cached
\\for 14 days. The TTM (trailing-twelve-month) total + yield are \\for 14 days. The TTM (trailing-twelve-month) total + yield are
\\computed against the current Yahoo quote when available. \\computed against the current Yahoo quote when available.
\\ \\
\\Examples: \\Examples:
\\ zfin divs VTI # quarterly distributions + yield \\ zfin divs VTI # quarterly distributions + yield
\\ zfin divs T # historical AT&T dividends \\ zfin divs T # historical AT&T dividends
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -8,29 +8,29 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "earnings"; .name = "earnings",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show earnings history (with EPS surprise) and upcoming events"; .synopsis = "Show earnings history (with EPS surprise) and upcoming events",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin earnings <SYMBOL> \\Usage: zfin earnings <SYMBOL>
\\ \\
\\Show the earnings history (estimate vs. actual + surprise %) \\Show the earnings history (estimate vs. actual + surprise %)
\\and any scheduled future events for a symbol from Financial \\and any scheduled future events for a symbol from Financial
\\Modeling Prep. Cached for 30 days; the cache is also smart- \\Modeling Prep. Cached for 30 days; the cache is also smart-
\\refreshed when a past event is missing its `actual` field \\refreshed when a past event is missing its `actual` field
\\(catches "results just released" cases without waiting for \\(catches "results just released" cases without waiting for
\\TTL expiry). \\TTL expiry).
\\ \\
\\Output is sorted newest-first; pipe through `| tail` for \\Output is sorted newest-first; pipe through `| tail` for
\\oldest-first. \\oldest-first.
\\ \\
\\Examples: \\Examples:
\\ zfin earnings NVDA \\ zfin earnings NVDA
\\ zfin earnings AAPL | head -8 # last two years of quarters \\ zfin earnings AAPL | head -8 # last two years of quarters
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -12,34 +12,35 @@ pub const ParsedArgs = struct {
arg: []const u8, arg: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "enrich"; .name = "enrich",
pub const group: framework.Group = .hygiene; .group = .hygiene,
pub const synopsis: []const u8 = "Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)"; .synopsis = "Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)",
pub const help: []const u8 = .help =
\\Usage: zfin enrich <FILE|SYMBOL> \\Usage: zfin enrich <FILE|SYMBOL>
\\ \\
\\Bootstrap a `metadata.srf` classification file from Alpha \\Bootstrap a `metadata.srf` classification file from Alpha
\\Vantage's OVERVIEW endpoint. Two modes: \\Vantage's OVERVIEW endpoint. Two modes:
\\ \\
\\ - File mode (path or `*.srf` suffix): enrich every stock \\ - File mode (path or `*.srf` suffix): enrich every stock
\\ symbol in the portfolio. Output is a complete SRF file \\ symbol in the portfolio. Output is a complete SRF file
\\ written to stdout — redirect into metadata.srf and \\ written to stdout — redirect into metadata.srf and
\\ edit by hand for accuracy. \\ edit by hand for accuracy.
\\ - Symbol mode (anything else): enrich a single symbol and \\ - Symbol mode (anything else): enrich a single symbol and
\\ emit one appendable SRF line. Useful for adding to an \\ emit one appendable SRF line. Useful for adding to an
\\ existing metadata.srf without rerunning the whole file. \\ existing metadata.srf without rerunning the whole file.
\\ \\
\\Caveats: Alpha Vantage's free tier is 25 requests/day. The \\Caveats: Alpha Vantage's free tier is 25 requests/day. The
\\OVERVIEW data is US-domicile-biased — international ETFs \\OVERVIEW data is US-domicile-biased — international ETFs
\\classify as `geo::US`. Always review the output before \\classify as `geo::US`. Always review the output before
\\saving as `metadata.srf`. Requires ALPHAVANTAGE_API_KEY. \\saving as `metadata.srf`. Requires ALPHAVANTAGE_API_KEY.
\\ \\
\\Examples: \\Examples:
\\ zfin enrich portfolio.srf > metadata.srf # whole portfolio \\ zfin enrich portfolio.srf > metadata.srf # whole portfolio
\\ zfin enrich AAPL >> metadata.srf # single symbol append \\ zfin enrich AAPL >> metadata.srf # single symbol append
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -8,24 +8,24 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "etf"; .name = "etf",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show ETF profile (holdings, sectors, expense ratio)"; .synopsis = "Show ETF profile (holdings, sectors, expense ratio)",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin etf <SYMBOL> \\Usage: zfin etf <SYMBOL>
\\ \\
\\Show the ETF profile (expense ratio, AUM, dividend yield, \\Show the ETF profile (expense ratio, AUM, dividend yield,
\\sector allocation, top holdings) for a fund symbol from \\sector allocation, top holdings) for a fund symbol from
\\Alpha Vantage. Cached for 30 days. Leveraged funds are \\Alpha Vantage. Cached for 30 days. Leveraged funds are
\\flagged in red. \\flagged in red.
\\ \\
\\Examples: \\Examples:
\\ zfin etf VTI # broad market index \\ zfin etf VTI # broad market index
\\ zfin etf TQQQ # leveraged (warning surfaced) \\ zfin etf TQQQ # leveraged (warning surfaced)
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -11,11 +11,12 @@
//! ```zig //! ```zig
//! pub const ParsedArgs = ...; // struct or tagged union //! pub const ParsedArgs = ...; // struct or tagged union
//! //!
//! pub const meta = struct { //! pub const meta: framework.Meta = .{
//! pub const name: []const u8 = "perf"; //! .name = "perf",
//! pub const group: framework.Group = .symbol_lookup; //! .group = .symbol_lookup,
//! pub const synopsis: []const u8 = "Show 1y/3y/5y/10y trailing returns"; //! .synopsis = "Show 1y/3y/5y/10y trailing returns",
//! pub const help: []const u8 = ...; // multi-line; rendered by `--help` //! .help = "...", // multi-line; rendered by `--help`
//! .uppercase_first_arg = true,
//! }; //! };
//! //!
//! pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs; //! pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs;
@ -72,6 +73,58 @@ pub const Group = enum {
} }
}; };
// ── Meta ──────────────────────────────────────────────────────
/// Per-command metadata. Every command module declares
/// `pub const meta: framework.Meta = .{ ... };` with all fields
/// populated. The compiler enforces the contract via the field
/// types; `validateCommandModule` reduces to a single
/// `expectDeclWithType` check on the declaration's type.
///
/// Fields are deliberately required (no defaults) so command
/// authors think about each one rather than relying on framework
/// defaults. In particular, `uppercase_first_arg` deserves an
/// explicit decision per command — see field doc-comment.
pub const Meta = struct {
/// User-facing subcommand name. Must match the field name used
/// for this command in the `command_modules` registry literal
/// (the dispatcher matches by `f.name`).
name: []const u8,
/// Workflow-grouping tag for the `zfin help` sectioned output.
/// See `Group` for the canonical taxonomy.
group: Group,
/// One-line description rendered next to the command name in
/// `zfin help`.
synopsis: []const u8,
/// Multi-line help text rendered by `zfin <cmd> --help`. The
/// dispatcher emits this verbatim; include the usage line,
/// flag descriptions, and any caveats. Trailing newline is
/// added automatically if missing.
help: []const u8,
/// Whether to uppercase `cmd_args[0]` before calling
/// `parseArgs`. Symbol-taking commands (perf, quote, divs,
/// splits, options, earnings, etf, history-symbol-mode,
/// lookup) set this to `true` so users can type
/// `zfin perf aapl` and have it normalized to `AAPL`.
/// Non-symbol commands (cache, version, portfolio, ...) set
/// it to `false`.
///
/// The dispatcher's `normalizeFirstArg` honors a "skip when
/// arg starts with `-`" rule, so flag-leading invocations
/// like `zfin history --since 2026-01-01` pass through
/// unchanged regardless of this setting.
///
/// No default — every command author must make an explicit
/// choice. This is metadata about command shape, not an
/// optional opt-in; the `false` answer is just as load-bearing
/// as `true`.
uppercase_first_arg: bool,
};
// ── Refresh policy ──────────────────────────────────────────── // ── Refresh policy ────────────────────────────────────────────
/// Cache-freshness policy for the invocation. Defined here ahead of /// Cache-freshness policy for the invocation. Defined here ahead of
@ -218,62 +271,22 @@ pub fn validateCommandModule(comptime Module: type) void {
@compileError("Command module `" ++ mod_name ++ "` is missing `pub const ParsedArgs = ...;` " ++ @compileError("Command module `" ++ mod_name ++ "` is missing `pub const ParsedArgs = ...;` " ++
"(use `pub const ParsedArgs = struct {};` if the command takes no arguments)"); "(use `pub const ParsedArgs = struct {};` if the command takes no arguments)");
} }
if (!@hasDecl(Module, "meta")) {
@compileError("Command module `" ++ mod_name ++ "` is missing `pub const meta = struct { ... };` " ++
"(see src/commands/framework.zig for the full contract)");
}
const meta_decl = Module.meta;
const ParsedArgs = Module.ParsedArgs; const ParsedArgs = Module.ParsedArgs;
// ── meta fields ──────────────────────────────────────── // ── meta ───────────────────────────────────────────────
// One check covers all five fields. The compiler enforces
// field types via `Meta`'s declaration; missing fields
// surface as Zig's native "missing field" comptime errors
// pointing at the call site.
validator.expectDeclWithType( validator.expectDeclWithType(
"Command module", "Command module",
mod_name, mod_name,
meta_decl, Module,
"name", "meta",
[]const u8, Meta,
"pub const name: []const u8 = \"...\";", "pub const meta: framework.Meta = .{ .name = \"...\", .group = ..., .synopsis = \"...\", .help = \"...\", .uppercase_first_arg = ... };",
); );
validator.expectDeclWithType(
"Command module",
mod_name,
meta_decl,
"group",
Group,
"pub const group: framework.Group = .symbol_lookup;",
);
validator.expectDeclWithType(
"Command module",
mod_name,
meta_decl,
"synopsis",
[]const u8,
"pub const synopsis: []const u8 = \"one-line description for `zfin help`\";",
);
validator.expectDeclWithType(
"Command module",
mod_name,
meta_decl,
"help",
[]const u8,
"pub const help: []const u8 = \"multi-line text for `zfin <cmd> --help`\";",
);
// Optional: `uppercase_first_arg`. Symbol-taking commands
// (perf, quote, divs, ...) set this to `true` so the
// dispatcher uppercases `cmd_args[0]` before calling
// `parseArgs`. When omitted, defaults to false.
if (@hasDecl(meta_decl, "uppercase_first_arg")) {
validator.expectDeclWithType(
"Command module",
mod_name,
meta_decl,
"uppercase_first_arg",
bool,
"pub const uppercase_first_arg: bool = true; // (optional, defaults to false)",
);
}
// ── parseArgs ────────────────────────────────────────── // ── parseArgs ──────────────────────────────────────────
validator.expectFnInferredError( validator.expectFnInferredError(
@ -301,16 +314,6 @@ pub fn validateCommandModule(comptime Module: type) void {
// ── Help rendering ──────────────────────────────────────────── // ── Help rendering ────────────────────────────────────────────
/// Whether a command module opts into having its first positional
/// argument uppercased before `parseArgs` runs. Symbol-taking
/// commands set `meta.uppercase_first_arg = true` so users can type
/// `zfin perf aapl` and have it normalize to `AAPL`. Honors the
/// "skip when the arg is a flag" rule (i.e. starts with `-`).
pub fn uppercasesFirstArg(comptime Module: type) bool {
if (!@hasDecl(Module.meta, "uppercase_first_arg")) return false;
return Module.meta.uppercase_first_arg;
}
/// If `cmd_args[0]` exists, is non-empty, and isn't a flag, return /// If `cmd_args[0]` exists, is non-empty, and isn't a flag, return
/// a copy with the first byte-string uppercased. Otherwise return /// a copy with the first byte-string uppercased. Otherwise return
/// `cmd_args` unchanged. The new slice is owned by `allocator` /// `cmd_args` unchanged. The new slice is owned by `allocator`
@ -405,11 +408,12 @@ const testing = std.testing;
const ProbeModule = struct { const ProbeModule = struct {
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: Meta = .{
pub const name: []const u8 = "probe"; .name = "probe",
pub const group: Group = .infra; .group = .infra,
pub const synopsis: []const u8 = "test probe"; .synopsis = "test probe",
pub const help: []const u8 = "Usage: zfin probe\n"; .help = "Usage: zfin probe\n",
.uppercase_first_arg = false,
}; };
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
@ -453,11 +457,12 @@ test "printCommandHelp: writes meta.help and ensures trailing newline" {
test "printCommandHelp: appends newline when meta.help lacks one" { test "printCommandHelp: appends newline when meta.help lacks one" {
const NoNewline = struct { const NoNewline = struct {
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: Meta = .{
pub const name: []const u8 = "x"; .name = "x",
pub const group: Group = .infra; .group = .infra,
pub const synopsis: []const u8 = "x"; .synopsis = "x",
pub const help: []const u8 = "no trailing newline"; .help = "no trailing newline",
.uppercase_first_arg = false,
}; };
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{}; return .{};
@ -470,29 +475,6 @@ test "printCommandHelp: appends newline when meta.help lacks one" {
try testing.expectEqualStrings("no trailing newline\n", w.buffered()); try testing.expectEqualStrings("no trailing newline\n", w.buffered());
} }
test "uppercasesFirstArg: defaults to false when meta omits the field" {
try testing.expect(!uppercasesFirstArg(ProbeModule));
}
test "uppercasesFirstArg: honors meta.uppercase_first_arg when present" {
const M = struct {
pub const ParsedArgs = struct {};
pub const meta = struct {
pub const name: []const u8 = "m";
pub const group: Group = .symbol_lookup;
pub const synopsis: []const u8 = "m";
pub const help: []const u8 = "m";
pub const uppercase_first_arg: bool = true;
};
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{};
}
pub fn run(_: *RunCtx, _: ParsedArgs) !void {}
};
comptime validateCommandModule(M);
try testing.expect(uppercasesFirstArg(M));
}
test "normalizeFirstArg: empty args returns slice unchanged" { test "normalizeFirstArg: empty args returns slice unchanged" {
const empty: []const []const u8 = &.{}; const empty: []const []const u8 = &.{};
const out = try normalizeFirstArg(testing.allocator, empty); const out = try normalizeFirstArg(testing.allocator, empty);
@ -530,11 +512,12 @@ test "printGroupedUsage: groups in canonical order with headers + synopses" {
const FakeRegistry = struct { const FakeRegistry = struct {
const ProbeA = struct { const ProbeA = struct {
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: Meta = .{
pub const name: []const u8 = "alpha"; .name = "alpha",
pub const group: Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "alpha synopsis"; .synopsis = "alpha synopsis",
pub const help: []const u8 = "alpha help\n"; .help = "alpha help\n",
.uppercase_first_arg = false,
}; };
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{}; return .{};
@ -543,11 +526,12 @@ test "printGroupedUsage: groups in canonical order with headers + synopses" {
}; };
const ProbeB = struct { const ProbeB = struct {
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: Meta = .{
pub const name: []const u8 = "beta"; .name = "beta",
pub const group: Group = .infra; .group = .infra,
pub const synopsis: []const u8 = "beta synopsis"; .synopsis = "beta synopsis",
pub const help: []const u8 = "beta help\n"; .help = "beta help\n",
.uppercase_first_arg = false,
}; };
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{}; return .{};
@ -584,11 +568,12 @@ test "printGroupedUsage: omits empty groups" {
const FakeRegistry = struct { const FakeRegistry = struct {
const Probe = struct { const Probe = struct {
pub const ParsedArgs = struct {}; pub const ParsedArgs = struct {};
pub const meta = struct { pub const meta: Meta = .{
pub const name: []const u8 = "only"; .name = "only",
pub const group: Group = .infra; .group = .infra,
pub const synopsis: []const u8 = "only synopsis"; .synopsis = "only synopsis",
pub const help: []const u8 = "h\n"; .help = "h\n",
.uppercase_first_arg = false,
}; };
pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs {
return .{}; return .{};

View file

@ -55,34 +55,34 @@ pub const ParsedArgs = union(enum) {
portfolio: PortfolioOpts, portfolio: PortfolioOpts,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "history"; .name = "history",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Price history (symbol) or portfolio value timeline"; .synopsis = "Price history (symbol) or portfolio value timeline",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: \\Usage:
\\ zfin history <SYMBOL> # last 30 days of candles \\ zfin history <SYMBOL> # last 30 days of candles
\\ zfin history [flags] # portfolio-value timeline \\ zfin history [flags] # portfolio-value timeline
\\ \\
\\Two modes in one command. Symbol mode (positional symbol) \\Two modes in one command. Symbol mode (positional symbol)
\\shows the last 30 trading days of candles for that symbol. \\shows the last 30 trading days of candles for that symbol.
\\Portfolio mode (no positional, optionally with flags) reads \\Portfolio mode (no positional, optionally with flags) reads
\\`history/*-portfolio.srf` snapshots and renders rolling-windows \\`history/*-portfolio.srf` snapshots and renders rolling-windows
\\returns + a braille chart + a recent-snapshots table. \\returns + a braille chart + a recent-snapshots table.
\\ \\
\\Portfolio-mode flags: \\Portfolio-mode flags:
\\ --since <DATE> earliest as-of date (inclusive) \\ --since <DATE> earliest as-of date (inclusive)
\\ --until <DATE> latest as-of date (inclusive) \\ --until <DATE> latest as-of date (inclusive)
\\ --metric <name> liquid (default), illiquid, or net_worth \\ --metric <name> liquid (default), illiquid, or net_worth
\\ --resolution <name> daily | weekly | monthly | auto \\ --resolution <name> daily | weekly | monthly | auto
\\ (auto: daily ≤90d, weekly ≤730d, else monthly) \\ (auto: daily ≤90d, weekly ≤730d, else monthly)
\\ --limit <N> cap recent-snapshots table to N rows (default 40) \\ --limit <N> cap recent-snapshots table to N rows (default 40)
\\ --rebuild-rollup regenerate history/rollup.srf and exit \\ --rebuild-rollup regenerate history/rollup.srf and exit
\\ \\
\\DATE accepts YYYY-MM-DD or relative shortcuts (1W/1M/1Q/1Y). \\DATE accepts YYYY-MM-DD or relative shortcuts (1W/1M/1Q/1Y).
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -8,27 +8,27 @@ pub const ParsedArgs = struct {
cusip: []const u8, cusip: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "lookup"; .name = "lookup",
pub const group: framework.Group = .hygiene; .group = .hygiene,
pub const synopsis: []const u8 = "Look up a CUSIP to ticker via OpenFIGI"; .synopsis = "Look up a CUSIP to ticker via OpenFIGI",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin lookup <CUSIP> \\Usage: zfin lookup <CUSIP>
\\ \\
\\Look up a CUSIP (9-char alphanumeric security identifier) to \\Look up a CUSIP (9-char alphanumeric security identifier) to
\\its ticker via the OpenFIGI API. Successful results are cached \\its ticker via the OpenFIGI API. Successful results are cached
\\indefinitely in `cusip_tickers.srf`. OPENFIGI_API_KEY raises \\indefinitely in `cusip_tickers.srf`. OPENFIGI_API_KEY raises
\\the rate limit but the unauthenticated tier works for low \\the rate limit but the unauthenticated tier works for low
\\volume. \\volume.
\\ \\
\\Mutual funds frequently have no OpenFIGI coverage; the \\Mutual funds frequently have no OpenFIGI coverage; the
\\command surfaces that and suggests a manual portfolio entry. \\command surfaces that and suggests a manual portfolio entry.
\\ \\
\\Examples: \\Examples:
\\ zfin lookup 037833100 # → AAPL \\ zfin lookup 037833100 # → AAPL
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -36,31 +36,32 @@ pub const ParsedArgs = struct {
real: bool = false, real: bool = false,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "milestones"; .name = "milestones",
pub const group: framework.Group = .portfolio; .group = .portfolio,
pub const synopsis: []const u8 = "Show portfolio threshold crossings (each $1M, doublings, etc.)"; .synopsis = "Show portfolio threshold crossings (each $1M, doublings, etc.)",
pub const help: []const u8 = .help =
\\Usage: zfin milestones --step <expr> [--real] \\Usage: zfin milestones --step <expr> [--real]
\\ \\
\\Find the dates the portfolio first reached each of a \\Find the dates the portfolio first reached each of a
\\configured set of thresholds. Two threshold modes: \\configured set of thresholds. Two threshold modes:
\\ \\
\\ Absolute dollar: 1M / 1m / 1500000 / 1.5M / 500K / 500k \\ Absolute dollar: 1M / 1m / 1500000 / 1.5M / 500K / 500k
\\ Relative multiplier: 2x / 2X / 1.5x \\ Relative multiplier: 2x / 2X / 1.5x
\\ \\
\\Rejects %, ≤0 dollar steps, ≤1.0x multipliers, NaN/Inf. \\Rejects %, ≤0 dollar steps, ≤1.0x multipliers, NaN/Inf.
\\ \\
\\Options: \\Options:
\\ --step <expr> Threshold step (required). \\ --step <expr> Threshold step (required).
\\ --real Deflate the series to the last full Shiller \\ --real Deflate the series to the last full Shiller
\\ year before detecting crossings (CPI-adjusted). \\ year before detecting crossings (CPI-adjusted).
\\ Default is nominal. \\ Default is nominal.
\\ \\
\\Note: crossing dates are "first observed at," bounded by the \\Note: crossing dates are "first observed at," bounded by the
\\source series cadence (typically weekly). \\source series cadence (typically weekly).
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -12,27 +12,27 @@ pub const ParsedArgs = struct {
ntm: usize = 8, ntm: usize = 8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "options"; .name = "options",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show options chain (all expirations) for a symbol"; .synopsis = "Show options chain (all expirations) for a symbol",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin options <SYMBOL> [--ntm <N>] \\Usage: zfin options <SYMBOL> [--ntm <N>]
\\ \\
\\Show the options chain (all expirations) for a symbol from \\Show the options chain (all expirations) for a symbol from
\\CBOE. Cached for 1 hour. The nearest monthly expiration is \\CBOE. Cached for 1 hour. The nearest monthly expiration is
\\auto-expanded with ±N strikes near the money; other \\auto-expanded with ±N strikes near the money; other
\\expirations are listed collapsed. \\expirations are listed collapsed.
\\ \\
\\Options: \\Options:
\\ --ntm <N> Show ±N strikes near the money (default: 8) \\ --ntm <N> Show ±N strikes near the money (default: 8)
\\ \\
\\Examples: \\Examples:
\\ zfin options SPY \\ zfin options SPY
\\ zfin options AAPL --ntm 12 \\ zfin options AAPL --ntm 12
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -9,30 +9,30 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "perf"; .name = "perf",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show 1y/3y/5y/10y trailing returns (Morningstar-style)"; .synopsis = "Show 1y/3y/5y/10y trailing returns (Morningstar-style)",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin perf <SYMBOL> \\Usage: zfin perf <SYMBOL>
\\ \\
\\Show Morningstar-style trailing returns for a symbol — 1Y, \\Show Morningstar-style trailing returns for a symbol — 1Y,
\\3Y, 5Y, 10Y price-only and total-return CAGR plus risk \\3Y, 5Y, 10Y price-only and total-return CAGR plus risk
\\metrics (Sharpe, max drawdown, vol). Total returns require \\metrics (Sharpe, max drawdown, vol). Total returns require
\\POLYGON_API_KEY (for dividend history); price-only \\POLYGON_API_KEY (for dividend history); price-only
\\returns work without it. \\returns work without it.
\\ \\
\\Two return tables are produced: \\Two return tables are produced:
\\ - As-of: returns through the latest cached close. \\ - As-of: returns through the latest cached close.
\\ - Month-end: returns through the most recent calendar \\ - Month-end: returns through the most recent calendar
\\ month-end (matches how mutual funds quote their stats). \\ month-end (matches how mutual funds quote their stats).
\\ \\
\\Examples: \\Examples:
\\ zfin perf VTI # total-market index, 30+ years of data \\ zfin perf VTI # total-market index, 30+ years of data
\\ zfin perf NVDA # individual stock \\ zfin perf NVDA # individual stock
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -13,27 +13,28 @@ pub const ParsedArgs = struct {
force_refresh: bool = false, force_refresh: bool = false,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "portfolio"; .name = "portfolio",
pub const group: framework.Group = .portfolio; .group = .portfolio,
pub const synopsis: []const u8 = "Load and analyze the portfolio (positions + valuations + watchlist)"; .synopsis = "Load and analyze the portfolio (positions + valuations + watchlist)",
pub const help: []const u8 = .help =
\\Usage: zfin portfolio [--refresh] \\Usage: zfin portfolio [--refresh]
\\ \\
\\Load `portfolio.srf` (cwd → ZFIN_HOME), refresh per-symbol \\Load `portfolio.srf` (cwd → ZFIN_HOME), refresh per-symbol
\\prices in parallel (server sync where ZFIN_SERVER is set, \\prices in parallel (server sync where ZFIN_SERVER is set,
\\else providers), and print the position table + valuations \\else providers), and print the position table + valuations
\\+ historical-snapshot mini-tables. The watchlist (if \\+ historical-snapshot mini-tables. The watchlist (if
\\`watchlist.srf` exists) is appended to the price-load step \\`watchlist.srf` exists) is appended to the price-load step
\\so its quotes show alongside. \\so its quotes show alongside.
\\ \\
\\Options: \\Options:
\\ --refresh Force a re-fetch of every symbol's candles, \\ --refresh Force a re-fetch of every symbol's candles,
\\ bypassing the per-symbol TTL freshness check. \\ bypassing the per-symbol TTL freshness check.
\\ Useful when you suspect cached data is wrong \\ Useful when you suspect cached data is wrong
\\ or after rotating providers. \\ or after rotating providers.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -65,48 +65,49 @@ pub const CompareArgs = struct {
as_of: ?Date = null, as_of: ?Date = null,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "projections"; .name = "projections",
pub const group: framework.Group = .portfolio; .group = .portfolio,
pub const synopsis: []const u8 = "Retirement projections, benchmark comparison, percentile bands"; .synopsis = "Retirement projections, benchmark comparison, percentile bands",
pub const help: []const u8 = .help =
\\Usage: zfin projections [opts] \\Usage: zfin projections [opts]
\\ \\
\\Default mode: percentile-bands view of the portfolio's \\Default mode: percentile-bands view of the portfolio's
\\projected value over the configured horizon (`projections.srf`), \\projected value over the configured horizon (`projections.srf`),
\\plus benchmark comparison (SPY/AGG) and safe-withdrawal \\plus benchmark comparison (SPY/AGG) and safe-withdrawal
\\dollars at multiple horizons / confidence levels. \\dollars at multiple horizons / confidence levels.
\\ \\
\\Three alternate sub-modes (mutually exclusive): \\Three alternate sub-modes (mutually exclusive):
\\ --vs <DATE> Side-by-side compare with a \\ --vs <DATE> Side-by-side compare with a
\\ historical snapshot's projection. \\ historical snapshot's projection.
\\ --convergence Plot the model's predicted \\ --convergence Plot the model's predicted
\\ retirement date over time as data \\ retirement date over time as data
\\ accumulated. \\ accumulated.
\\ --return-backtest Plot expected_return claim over \\ --return-backtest Plot expected_return claim over
\\ time alongside realized forward \\ time alongside realized forward
\\ CAGR. Pair with `--real` for \\ CAGR. Pair with `--real` for
\\ CPI-adjusted dollars. \\ CPI-adjusted dollars.
\\ \\
\\Options: \\Options:
\\ --no-events Exclude life events from the \\ --no-events Exclude life events from the
\\ simulation (baseline view). \\ simulation (baseline view).
\\ --as-of <DATE> Compute against a historical \\ --as-of <DATE> Compute against a historical
\\ snapshot. Auto-snaps to the \\ snapshot. Auto-snaps to the
\\ nearest-earlier snapshot. \\ nearest-earlier snapshot.
\\ --overlay-actuals Plot realized portfolio trajectory \\ --overlay-actuals Plot realized portfolio trajectory
\\ from --as-of through today on top \\ from --as-of through today on top
\\ of the percentile bands. Requires \\ of the percentile bands. Requires
\\ --as-of. Ignored under --vs. \\ --as-of. Ignored under --vs.
\\ --vs <DATE> (see above) \\ --vs <DATE> (see above)
\\ --convergence (see above) \\ --convergence (see above)
\\ --return-backtest (see above) \\ --return-backtest (see above)
\\ --real With --return-backtest, render in \\ --real With --return-backtest, render in
\\ CPI-adjusted dollars. \\ CPI-adjusted dollars.
\\ \\
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y). \\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -9,27 +9,27 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "quote"; .name = "quote",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show latest quote with chart and 20-day history"; .synopsis = "Show latest quote with chart and 20-day history",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin quote <SYMBOL> \\Usage: zfin quote <SYMBOL>
\\ \\
\\Show the latest real-time quote for a symbol (Yahoo / TwelveData) \\Show the latest real-time quote for a symbol (Yahoo / TwelveData)
\\plus a braille price chart of the last 60 candles and a table \\plus a braille price chart of the last 60 candles and a table
\\of the last 20 trading days. \\of the last 20 trading days.
\\ \\
\\If real-time fetch fails, falls back to the cached close. The \\If real-time fetch fails, falls back to the cached close. The
\\Yahoo path is free and unauthenticated; TwelveData requires \\Yahoo path is free and unauthenticated; TwelveData requires
\\TWELVEDATA_API_KEY. \\TWELVEDATA_API_KEY.
\\ \\
\\Examples: \\Examples:
\\ zfin quote AAPL \\ zfin quote AAPL
\\ zfin quote spy # symbols are case-insensitive \\ zfin quote spy # symbols are case-insensitive
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -64,37 +64,38 @@ pub const ParsedArgs = struct {
as_of_override: ?Date = null, as_of_override: ?Date = null,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "snapshot"; .name = "snapshot",
pub const group: framework.Group = .timeseries; .group = .timeseries,
pub const synopsis: []const u8 = "Write a daily portfolio snapshot to history/"; .synopsis = "Write a daily portfolio snapshot to history/",
pub const help: []const u8 = .help =
\\Usage: zfin snapshot [opts] \\Usage: zfin snapshot [opts]
\\ \\
\\Compute a portfolio snapshot for today (or a historical date with \\Compute a portfolio snapshot for today (or a historical date with
\\`--as-of`) and write it as a discriminated SRF file under \\`--as-of`) and write it as a discriminated SRF file under
\\`history/<as_of_date>-portfolio.srf`. The output records start with \\`history/<as_of_date>-portfolio.srf`. The output records start with
\\`kind::<meta|total|tax_type|account|lot>` so readers can demux on \\`kind::<meta|total|tax_type|account|lot>` so readers can demux on
\\that field. \\that field.
\\ \\
\\Default mode: refresh the candle cache for held symbols, derive \\Default mode: refresh the candle cache for held symbols, derive
\\`as_of_date` from the mode of cached candle dates, look up \\`as_of_date` from the mode of cached candle dates, look up
\\close-on-or-before for each lot, and write atomically. \\close-on-or-before for each lot, and write atomically.
\\ \\
\\Options: \\Options:
\\ --force overwrite existing snapshot for as_of_date \\ --force overwrite existing snapshot for as_of_date
\\ --dry-run compute + print to stdout, do not write \\ --dry-run compute + print to stdout, do not write
\\ --out <path> override output path \\ --out <path> override output path
\\ --as-of <DATE> write a snapshot for a historical date \\ --as-of <DATE> write a snapshot for a historical date
\\ (uses git to recover portfolio state and \\ (uses git to recover portfolio state and
\\ candle cache for pricing). DATE accepts \\ candle cache for pricing). DATE accepts
\\ YYYY-MM-DD or relative shortcuts \\ YYYY-MM-DD or relative shortcuts
\\ (1W/1M/1Q/1Y). \\ (1W/1M/1Q/1Y).
\\ \\
\\If `history/<as_of_date>-portfolio.srf` already exists and `--force` \\If `history/<as_of_date>-portfolio.srf` already exists and `--force`
\\wasn't passed, the run skips with a stderr message. \\wasn't passed, the run skips with a stderr message.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -8,23 +8,23 @@ pub const ParsedArgs = struct {
symbol: []const u8, symbol: []const u8,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "splits"; .name = "splits",
pub const group: framework.Group = .symbol_lookup; .group = .symbol_lookup,
pub const synopsis: []const u8 = "Show split history for a symbol"; .synopsis = "Show split history for a symbol",
pub const uppercase_first_arg: bool = true; .uppercase_first_arg = true,
pub const help: []const u8 = .help =
\\Usage: zfin splits <SYMBOL> \\Usage: zfin splits <SYMBOL>
\\ \\
\\Show the split history for a symbol from Polygon.io. Cached \\Show the split history for a symbol from Polygon.io. Cached
\\for 14 days; subsequent calls within that window serve the \\for 14 days; subsequent calls within that window serve the
\\cached data without an API call. \\cached data without an API call.
\\ \\
\\Examples: \\Examples:
\\ zfin splits AAPL # 4:1 (2020-08-31), 7:1 (2014-06-09) \\ zfin splits AAPL # 4:1 (2020-08-31), 7:1 (2014-06-09)
\\ zfin splits NVDA # 10:1 (2024-06-10), 4:1 (2021-07-20), ... \\ zfin splits NVDA # 10:1 (2024-06-10), 4:1 (2021-07-20), ...
\\ \\
; ,
}; };
comptime { comptime {

View file

@ -19,19 +19,20 @@ pub const ParsedArgs = struct {
verbose: bool = false, verbose: bool = false,
}; };
pub const meta = struct { pub const meta: framework.Meta = .{
pub const name: []const u8 = "version"; .name = "version",
pub const group: framework.Group = .infra; .group = .infra,
pub const synopsis: []const u8 = "Show zfin version and build info"; .synopsis = "Show zfin version and build info",
pub const help: []const u8 = .help =
\\Usage: zfin version [-v|--verbose] \\Usage: zfin version [-v|--verbose]
\\ \\
\\Print zfin's version + build date. With `--verbose`/`-v`, also \\Print zfin's version + build date. With `--verbose`/`-v`, also
\\prints the Zig compiler version, build mode, build target, \\prints the Zig compiler version, build mode, build target,
\\resolved ZFIN_HOME, and cache directory — useful for bug \\resolved ZFIN_HOME, and cache directory — useful for bug
\\reports. \\reports.
\\ \\
; ,
.uppercase_first_arg = false,
}; };
comptime { comptime {

View file

@ -331,7 +331,7 @@ fn runCli(init: std.process.Init) !u8 {
.color = color, .color = color,
.out = out, .out = out,
}; };
const dispatched_args = if (comptime cmd_framework.uppercasesFirstArg(Module)) const dispatched_args = if (comptime Module.meta.uppercase_first_arg)
try cmd_framework.normalizeFirstArg(allocator, cmd_args) try cmd_framework.normalizeFirstArg(allocator, cmd_args)
else else
cmd_args; cmd_args;