From 4e6ae0ba518ea77ee6b0be2467795c1f8c29f0db Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Mon, 18 May 2026 17:39:19 -0700 Subject: [PATCH] rework framework to something much more sane --- src/commands/analysis.zig | 35 +++--- src/commands/audit.zig | 53 +++++---- src/commands/cache.zig | 39 +++--- src/commands/compare.zig | 77 ++++++------ src/commands/contributions.zig | 77 ++++++------ src/commands/divs.zig | 34 +++--- src/commands/earnings.zig | 46 ++++---- src/commands/enrich.zig | 57 ++++----- src/commands/etf.zig | 36 +++--- src/commands/framework.zig | 209 +++++++++++++++------------------ src/commands/history.zig | 56 ++++----- src/commands/lookup.zig | 42 +++---- src/commands/milestones.zig | 51 ++++---- src/commands/options.zig | 42 +++---- src/commands/perf.zig | 48 ++++---- src/commands/portfolio.zig | 43 +++---- src/commands/projections.zig | 85 +++++++------- src/commands/quote.zig | 42 +++---- src/commands/snapshot.zig | 63 +++++----- src/commands/splits.zig | 34 +++--- src/commands/version.zig | 27 +++-- src/main.zig | 2 +- 22 files changed, 597 insertions(+), 601 deletions(-) diff --git a/src/commands/analysis.zig b/src/commands/analysis.zig index 82c5896..5b60a4b 100644 --- a/src/commands/analysis.zig +++ b/src/commands/analysis.zig @@ -7,23 +7,24 @@ const Money = @import("../Money.zig"); pub const ParsedArgs = struct {}; -pub const meta = struct { - pub const name: []const u8 = "analysis"; - pub const group: framework.Group = .portfolio; - pub const synopsis: []const u8 = "Show portfolio breakdowns by asset class, sector, geo, account, tax type"; - pub const help: []const u8 = - \\Usage: zfin analysis - \\ - \\Show portfolio analysis: equities/fixed-income split, plus - \\block-bar breakdowns by asset class, sector, geographic - \\region, account, and tax type. Reads classifications from - \\`metadata.srf` and account tax types from `accounts.srf` - \\(both in the same directory as the portfolio file). - \\ - \\Run `zfin enrich > metadata.srf` to bootstrap - \\classifications, then edit by hand. - \\ - ; +pub const meta: framework.Meta = .{ + .name = "analysis", + .group = .portfolio, + .synopsis = "Show portfolio breakdowns by asset class, sector, geo, account, tax type", + .help = + \\Usage: zfin analysis + \\ + \\Show portfolio analysis: equities/fixed-income split, plus + \\block-bar breakdowns by asset class, sector, geographic + \\region, account, and tax type. Reads classifications from + \\`metadata.srf` and account tax types from `accounts.srf` + \\(both in the same directory as the portfolio file). + \\ + \\Run `zfin enrich > metadata.srf` to bootstrap + \\classifications, then edit by hand. + \\ + , + .uppercase_first_arg = false, }; comptime { diff --git a/src/commands/audit.zig b/src/commands/audit.zig index f67c639..e7eadf6 100644 --- a/src/commands/audit.zig +++ b/src/commands/audit.zig @@ -2250,32 +2250,33 @@ pub const ParsedArgs = struct { stale_days: u32 = default_stale_days, }; -pub const meta = struct { - pub const name: []const u8 = "audit"; - pub const group: framework.Group = .hygiene; - pub const synopsis: []const u8 = "Reconcile portfolio against brokerage exports + portfolio hygiene check"; - pub const help: []const u8 = - \\Usage: zfin audit [opts] - \\ - \\Two modes in one command: - \\ - \\ Flagless: run the portfolio hygiene check — surfaces stale - \\ manual prices, account-cadence violations, and brokerage-file - \\ candidates discovered automatically. - \\ - \\ With brokerage flags: reconcile the portfolio against the - \\ given export and report discrepancies. - \\ - \\Options: - \\ --verbose Show full reconciliation output even when clean - \\ --stale-days Manual price staleness threshold (default 3) - \\ --fidelity Fidelity positions CSV export - \\ ("All accounts" → Positions tab → Download) - \\ --schwab Schwab per-account positions CSV export - \\ --schwab-summary Schwab account summary; copy from accounts - \\ summary page, paste to stdin, then ^D - \\ - ; +pub const meta: framework.Meta = .{ + .name = "audit", + .group = .hygiene, + .synopsis = "Reconcile portfolio against brokerage exports + portfolio hygiene check", + .help = + \\Usage: zfin audit [opts] + \\ + \\Two modes in one command: + \\ + \\ Flagless: run the portfolio hygiene check — surfaces stale + \\ manual prices, account-cadence violations, and brokerage-file + \\ candidates discovered automatically. + \\ + \\ With brokerage flags: reconcile the portfolio against the + \\ given export and report discrepancies. + \\ + \\Options: + \\ --verbose Show full reconciliation output even when clean + \\ --stale-days Manual price staleness threshold (default 3) + \\ --fidelity Fidelity positions CSV export + \\ ("All accounts" → Positions tab → Download) + \\ --schwab Schwab per-account positions CSV export + \\ --schwab-summary Schwab account summary; copy from accounts + \\ summary page, paste to stdin, then ^D + \\ + , + .uppercase_first_arg = false, }; comptime { diff --git a/src/commands/cache.zig b/src/commands/cache.zig index 8eb59a9..d1e4b21 100644 --- a/src/commands/cache.zig +++ b/src/commands/cache.zig @@ -13,25 +13,26 @@ pub const ParsedArgs = struct { sub: Subcommand, }; -pub const meta = struct { - pub const name: []const u8 = "cache"; - pub const group: framework.Group = .infra; - pub const synopsis: []const u8 = "Inspect or clear the local provider-data cache"; - pub const help: []const u8 = - \\Usage: zfin cache - \\ - \\Subcommands: - \\ stats List every cached symbol with per-data-type size, - \\ age, and freshness state. Stale entries (past TTL) - \\ are flagged. Includes the cusip_tickers.srf file - \\ if present. - \\ clear Delete every file under the cache directory. - \\ No confirmation; the next provider call will - \\ re-fetch everything. - \\ - \\Cache directory is `$ZFIN_CACHE_DIR` if set, else `~/.cache/zfin`. - \\ - ; +pub const meta: framework.Meta = .{ + .name = "cache", + .group = .infra, + .synopsis = "Inspect or clear the local provider-data cache", + .help = + \\Usage: zfin cache + \\ + \\Subcommands: + \\ stats List every cached symbol with per-data-type size, + \\ age, and freshness state. Stale entries (past TTL) + \\ are flagged. Includes the cusip_tickers.srf file + \\ if present. + \\ clear Delete every file under the cache directory. + \\ No confirmation; the next provider call will + \\ re-fetch everything. + \\ + \\Cache directory is `$ZFIN_CACHE_DIR` if set, else `~/.cache/zfin`. + \\ + , + .uppercase_first_arg = false, }; comptime { diff --git a/src/commands/compare.zig b/src/commands/compare.zig index 7373ea5..a6dae68 100644 --- a/src/commands/compare.zig +++ b/src/commands/compare.zig @@ -87,44 +87,45 @@ pub const ParsedArgs = struct { commit_after: ?git.CommitSpec = null, }; -pub const meta = struct { - pub const name: []const u8 = "compare"; - pub const group: framework.Group = .timeseries; - pub const synopsis: []const u8 = "Compare portfolio at two points in time (or vs. today)"; - pub const help: []const u8 = - \\Usage: - \\ zfin compare compare DATE vs. live - \\ zfin compare compare two historical dates - \\ zfin compare [--snapshot-before DATE] [--snapshot-after DATE] - \\ [--commit-before SPEC] [--commit-after SPEC] - \\ [--projections [--no-events]] - \\ - \\Compare two points in time for the portfolio: liquid totals, - \\per-symbol price moves, attribution / contributions diff. The - \\positional happy-path is what most users want; the named - \\overrides exist for the cases where the snapshot date and the - \\git-commit-at-that-date diverge (e.g. you committed two days - \\after a Sunday review). - \\ - \\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y). - \\Commit-spec forms: YYYY-MM-DD, relative, HEAD, HEAD~N, SHA, working. - \\ - \\Options: - \\ --projections Include projected-return + SWR@99% - \\ deltas. Adds ~1-2s per endpoint - \\ (Monte Carlo SWR search). - \\ --no-events (with --projections) exclude life - \\ events from the projection. - \\ --snapshot-before Override the before-side snapshot. - \\ --snapshot-after Override the after-side snapshot; - \\ accepts `live` for the current - \\ portfolio. - \\ --commit-before Pin the before-side commit for the - \\ attribution / contributions block. - \\ --commit-after Pin the after-side commit; accepts - \\ `working` for the working copy. - \\ - ; +pub const meta: framework.Meta = .{ + .name = "compare", + .group = .timeseries, + .synopsis = "Compare portfolio at two points in time (or vs. today)", + .help = + \\Usage: + \\ zfin compare compare DATE vs. live + \\ zfin compare compare two historical dates + \\ zfin compare [--snapshot-before DATE] [--snapshot-after DATE] + \\ [--commit-before SPEC] [--commit-after SPEC] + \\ [--projections [--no-events]] + \\ + \\Compare two points in time for the portfolio: liquid totals, + \\per-symbol price moves, attribution / contributions diff. The + \\positional happy-path is what most users want; the named + \\overrides exist for the cases where the snapshot date and the + \\git-commit-at-that-date diverge (e.g. you committed two days + \\after a Sunday review). + \\ + \\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y). + \\Commit-spec forms: YYYY-MM-DD, relative, HEAD, HEAD~N, SHA, working. + \\ + \\Options: + \\ --projections Include projected-return + SWR@99% + \\ deltas. Adds ~1-2s per endpoint + \\ (Monte Carlo SWR search). + \\ --no-events (with --projections) exclude life + \\ events from the projection. + \\ --snapshot-before Override the before-side snapshot. + \\ --snapshot-after Override the after-side snapshot; + \\ accepts `live` for the current + \\ portfolio. + \\ --commit-before Pin the before-side commit for the + \\ attribution / contributions block. + \\ --commit-after Pin the after-side commit; accepts + \\ `working` for the working copy. + \\ + , + .uppercase_first_arg = false, }; comptime { diff --git a/src/commands/contributions.zig b/src/commands/contributions.zig index 61c55a5..22da23e 100644 --- a/src/commands/contributions.zig +++ b/src/commands/contributions.zig @@ -187,44 +187,45 @@ pub const ParsedArgs = struct { after: ?git.CommitSpec = null, }; -pub const meta = struct { - pub const name: []const u8 = "contributions"; - pub const group: framework.Group = .timeseries; - pub const synopsis: []const u8 = "Show money added since the last recorded state in git"; - pub const help: []const u8 = - \\Usage: zfin contributions [opts] - \\ - \\Show contributions, withdrawals, and lot-level changes between - \\two points in the portfolio's git history. Four modes: - \\ - \\ No flags (default): - \\ dirty working tree: HEAD vs working copy - \\ clean working tree: HEAD~1 vs HEAD (review last commit) - \\ --since commit-at-or-before(DATE) vs HEAD (or - \\ working copy when dirty) - \\ --since --until commit-at-or-before(D1) vs - \\ commit-at-or-before(D2) - \\ --until alone rejected; window is ambiguous - \\ - \\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y). - \\ - \\Options: - \\ --since Earliest side (resolves to commit-at- - \\ or-before). - \\ --until Latest side. Pair with --since. - \\ --commit-before Pin the before commit directly. Same - \\ grammar as --commit-after, minus - \\ `working`. Useful when you committed - \\ after your review date. - \\ --commit-after Pin the after commit. SPEC accepts - \\ YYYY-MM-DD, relative (1W/1M/1Q/1Y), - \\ HEAD, HEAD~N, hex SHA, or `working` - \\ for the working copy. - \\ - \\--since and --commit-before describe the same axis; pass at most - \\one. Same for --until and --commit-after. - \\ - ; +pub const meta: framework.Meta = .{ + .name = "contributions", + .group = .timeseries, + .synopsis = "Show money added since the last recorded state in git", + .help = + \\Usage: zfin contributions [opts] + \\ + \\Show contributions, withdrawals, and lot-level changes between + \\two points in the portfolio's git history. Four modes: + \\ + \\ No flags (default): + \\ dirty working tree: HEAD vs working copy + \\ clean working tree: HEAD~1 vs HEAD (review last commit) + \\ --since commit-at-or-before(DATE) vs HEAD (or + \\ working copy when dirty) + \\ --since --until commit-at-or-before(D1) vs + \\ commit-at-or-before(D2) + \\ --until alone rejected; window is ambiguous + \\ + \\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y). + \\ + \\Options: + \\ --since Earliest side (resolves to commit-at- + \\ or-before). + \\ --until Latest side. Pair with --since. + \\ --commit-before Pin the before commit directly. Same + \\ grammar as --commit-after, minus + \\ `working`. Useful when you committed + \\ after your review date. + \\ --commit-after Pin the after commit. SPEC accepts + \\ YYYY-MM-DD, relative (1W/1M/1Q/1Y), + \\ HEAD, HEAD~N, hex SHA, or `working` + \\ for the working copy. + \\ + \\--since and --commit-before describe the same axis; pass at most + \\one. Same for --until and --commit-after. + \\ + , + .uppercase_first_arg = false, }; comptime { diff --git a/src/commands/divs.zig b/src/commands/divs.zig index f962d15..867e7aa 100644 --- a/src/commands/divs.zig +++ b/src/commands/divs.zig @@ -8,23 +8,23 @@ pub const ParsedArgs = struct { symbol: []const u8, }; -pub const meta = struct { - pub const name: []const u8 = "divs"; - pub const group: framework.Group = .symbol_lookup; - pub const synopsis: []const u8 = "Show dividend history (with TTM yield) for a symbol"; - pub const uppercase_first_arg: bool = true; - pub const help: []const u8 = - \\Usage: zfin divs - \\ - \\Show the dividend history for a symbol from Polygon.io. Cached - \\for 14 days. The TTM (trailing-twelve-month) total + yield are - \\computed against the current Yahoo quote when available. - \\ - \\Examples: - \\ zfin divs VTI # quarterly distributions + yield - \\ zfin divs T # historical AT&T dividends - \\ - ; +pub const meta: framework.Meta = .{ + .name = "divs", + .group = .symbol_lookup, + .synopsis = "Show dividend history (with TTM yield) for a symbol", + .uppercase_first_arg = true, + .help = + \\Usage: zfin divs + \\ + \\Show the dividend history for a symbol from Polygon.io. Cached + \\for 14 days. The TTM (trailing-twelve-month) total + yield are + \\computed against the current Yahoo quote when available. + \\ + \\Examples: + \\ zfin divs VTI # quarterly distributions + yield + \\ zfin divs T # historical AT&T dividends + \\ + , }; comptime { diff --git a/src/commands/earnings.zig b/src/commands/earnings.zig index 89d65c4..b643e3c 100644 --- a/src/commands/earnings.zig +++ b/src/commands/earnings.zig @@ -8,29 +8,29 @@ pub const ParsedArgs = struct { symbol: []const u8, }; -pub const meta = struct { - pub const name: []const u8 = "earnings"; - pub const group: framework.Group = .symbol_lookup; - pub const synopsis: []const u8 = "Show earnings history (with EPS surprise) and upcoming events"; - pub const uppercase_first_arg: bool = true; - pub const help: []const u8 = - \\Usage: zfin earnings - \\ - \\Show the earnings history (estimate vs. actual + surprise %) - \\and any scheduled future events for a symbol from Financial - \\Modeling Prep. Cached for 30 days; the cache is also smart- - \\refreshed when a past event is missing its `actual` field - \\(catches "results just released" cases without waiting for - \\TTL expiry). - \\ - \\Output is sorted newest-first; pipe through `| tail` for - \\oldest-first. - \\ - \\Examples: - \\ zfin earnings NVDA - \\ zfin earnings AAPL | head -8 # last two years of quarters - \\ - ; +pub const meta: framework.Meta = .{ + .name = "earnings", + .group = .symbol_lookup, + .synopsis = "Show earnings history (with EPS surprise) and upcoming events", + .uppercase_first_arg = true, + .help = + \\Usage: zfin earnings + \\ + \\Show the earnings history (estimate vs. actual + surprise %) + \\and any scheduled future events for a symbol from Financial + \\Modeling Prep. Cached for 30 days; the cache is also smart- + \\refreshed when a past event is missing its `actual` field + \\(catches "results just released" cases without waiting for + \\TTL expiry). + \\ + \\Output is sorted newest-first; pipe through `| tail` for + \\oldest-first. + \\ + \\Examples: + \\ zfin earnings NVDA + \\ zfin earnings AAPL | head -8 # last two years of quarters + \\ + , }; comptime { diff --git a/src/commands/enrich.zig b/src/commands/enrich.zig index 1e94b90..94e322a 100644 --- a/src/commands/enrich.zig +++ b/src/commands/enrich.zig @@ -12,34 +12,35 @@ pub const ParsedArgs = struct { arg: []const u8, }; -pub const meta = struct { - pub const name: []const u8 = "enrich"; - pub const group: framework.Group = .hygiene; - pub const synopsis: []const u8 = "Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)"; - pub const help: []const u8 = - \\Usage: zfin enrich - \\ - \\Bootstrap a `metadata.srf` classification file from Alpha - \\Vantage's OVERVIEW endpoint. Two modes: - \\ - \\ - File mode (path or `*.srf` suffix): enrich every stock - \\ symbol in the portfolio. Output is a complete SRF file - \\ written to stdout — redirect into metadata.srf and - \\ edit by hand for accuracy. - \\ - Symbol mode (anything else): enrich a single symbol and - \\ emit one appendable SRF line. Useful for adding to an - \\ existing metadata.srf without rerunning the whole file. - \\ - \\Caveats: Alpha Vantage's free tier is 25 requests/day. The - \\OVERVIEW data is US-domicile-biased — international ETFs - \\classify as `geo::US`. Always review the output before - \\saving as `metadata.srf`. Requires ALPHAVANTAGE_API_KEY. - \\ - \\Examples: - \\ zfin enrich portfolio.srf > metadata.srf # whole portfolio - \\ zfin enrich AAPL >> metadata.srf # single symbol append - \\ - ; +pub const meta: framework.Meta = .{ + .name = "enrich", + .group = .hygiene, + .synopsis = "Bootstrap metadata.srf from Alpha Vantage (25 req/day limit)", + .help = + \\Usage: zfin enrich + \\ + \\Bootstrap a `metadata.srf` classification file from Alpha + \\Vantage's OVERVIEW endpoint. Two modes: + \\ + \\ - File mode (path or `*.srf` suffix): enrich every stock + \\ symbol in the portfolio. Output is a complete SRF file + \\ written to stdout — redirect into metadata.srf and + \\ edit by hand for accuracy. + \\ - Symbol mode (anything else): enrich a single symbol and + \\ emit one appendable SRF line. Useful for adding to an + \\ existing metadata.srf without rerunning the whole file. + \\ + \\Caveats: Alpha Vantage's free tier is 25 requests/day. The + \\OVERVIEW data is US-domicile-biased — international ETFs + \\classify as `geo::US`. Always review the output before + \\saving as `metadata.srf`. Requires ALPHAVANTAGE_API_KEY. + \\ + \\Examples: + \\ zfin enrich portfolio.srf > metadata.srf # whole portfolio + \\ zfin enrich AAPL >> metadata.srf # single symbol append + \\ + , + .uppercase_first_arg = false, }; comptime { diff --git a/src/commands/etf.zig b/src/commands/etf.zig index cca3353..7d29c7b 100644 --- a/src/commands/etf.zig +++ b/src/commands/etf.zig @@ -8,24 +8,24 @@ pub const ParsedArgs = struct { symbol: []const u8, }; -pub const meta = struct { - pub const name: []const u8 = "etf"; - pub const group: framework.Group = .symbol_lookup; - pub const synopsis: []const u8 = "Show ETF profile (holdings, sectors, expense ratio)"; - pub const uppercase_first_arg: bool = true; - pub const help: []const u8 = - \\Usage: zfin etf - \\ - \\Show the ETF profile (expense ratio, AUM, dividend yield, - \\sector allocation, top holdings) for a fund symbol from - \\Alpha Vantage. Cached for 30 days. Leveraged funds are - \\flagged in red. - \\ - \\Examples: - \\ zfin etf VTI # broad market index - \\ zfin etf TQQQ # leveraged (warning surfaced) - \\ - ; +pub const meta: framework.Meta = .{ + .name = "etf", + .group = .symbol_lookup, + .synopsis = "Show ETF profile (holdings, sectors, expense ratio)", + .uppercase_first_arg = true, + .help = + \\Usage: zfin etf + \\ + \\Show the ETF profile (expense ratio, AUM, dividend yield, + \\sector allocation, top holdings) for a fund symbol from + \\Alpha Vantage. Cached for 30 days. Leveraged funds are + \\flagged in red. + \\ + \\Examples: + \\ zfin etf VTI # broad market index + \\ zfin etf TQQQ # leveraged (warning surfaced) + \\ + , }; comptime { diff --git a/src/commands/framework.zig b/src/commands/framework.zig index ffa180c..d31b103 100644 --- a/src/commands/framework.zig +++ b/src/commands/framework.zig @@ -11,11 +11,12 @@ //! ```zig //! pub const ParsedArgs = ...; // struct or tagged union //! -//! pub const meta = struct { -//! pub const name: []const u8 = "perf"; -//! pub const group: framework.Group = .symbol_lookup; -//! pub const synopsis: []const u8 = "Show 1y/3y/5y/10y trailing returns"; -//! pub const help: []const u8 = ...; // multi-line; rendered by `--help` +//! pub const meta: framework.Meta = .{ +//! .name = "perf", +//! .group = .symbol_lookup, +//! .synopsis = "Show 1y/3y/5y/10y trailing returns", +//! .help = "...", // multi-line; rendered by `--help` +//! .uppercase_first_arg = true, //! }; //! //! 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 --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 ──────────────────────────────────────────── /// 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 = ...;` " ++ "(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; - // ── 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( "Command module", mod_name, - meta_decl, - "name", - []const u8, - "pub const name: []const u8 = \"...\";", + Module, + "meta", + Meta, + "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 --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 ────────────────────────────────────────── validator.expectFnInferredError( @@ -301,16 +314,6 @@ pub fn validateCommandModule(comptime Module: type) void { // ── 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 /// a copy with the first byte-string uppercased. Otherwise return /// `cmd_args` unchanged. The new slice is owned by `allocator` @@ -405,11 +408,12 @@ const testing = std.testing; const ProbeModule = struct { pub const ParsedArgs = struct {}; - pub const meta = struct { - pub const name: []const u8 = "probe"; - pub const group: Group = .infra; - pub const synopsis: []const u8 = "test probe"; - pub const help: []const u8 = "Usage: zfin probe\n"; + pub const meta: Meta = .{ + .name = "probe", + .group = .infra, + .synopsis = "test probe", + .help = "Usage: zfin probe\n", + .uppercase_first_arg = false, }; 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" { const NoNewline = struct { pub const ParsedArgs = struct {}; - pub const meta = struct { - pub const name: []const u8 = "x"; - pub const group: Group = .infra; - pub const synopsis: []const u8 = "x"; - pub const help: []const u8 = "no trailing newline"; + pub const meta: Meta = .{ + .name = "x", + .group = .infra, + .synopsis = "x", + .help = "no trailing newline", + .uppercase_first_arg = false, }; pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { return .{}; @@ -470,29 +475,6 @@ test "printCommandHelp: appends newline when meta.help lacks one" { 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" { const empty: []const []const u8 = &.{}; const out = try normalizeFirstArg(testing.allocator, empty); @@ -530,11 +512,12 @@ test "printGroupedUsage: groups in canonical order with headers + synopses" { const FakeRegistry = struct { const ProbeA = struct { pub const ParsedArgs = struct {}; - pub const meta = struct { - pub const name: []const u8 = "alpha"; - pub const group: Group = .symbol_lookup; - pub const synopsis: []const u8 = "alpha synopsis"; - pub const help: []const u8 = "alpha help\n"; + pub const meta: Meta = .{ + .name = "alpha", + .group = .symbol_lookup, + .synopsis = "alpha synopsis", + .help = "alpha help\n", + .uppercase_first_arg = false, }; pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { return .{}; @@ -543,11 +526,12 @@ test "printGroupedUsage: groups in canonical order with headers + synopses" { }; const ProbeB = struct { pub const ParsedArgs = struct {}; - pub const meta = struct { - pub const name: []const u8 = "beta"; - pub const group: Group = .infra; - pub const synopsis: []const u8 = "beta synopsis"; - pub const help: []const u8 = "beta help\n"; + pub const meta: Meta = .{ + .name = "beta", + .group = .infra, + .synopsis = "beta synopsis", + .help = "beta help\n", + .uppercase_first_arg = false, }; pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { return .{}; @@ -584,11 +568,12 @@ test "printGroupedUsage: omits empty groups" { const FakeRegistry = struct { const Probe = struct { pub const ParsedArgs = struct {}; - pub const meta = struct { - pub const name: []const u8 = "only"; - pub const group: Group = .infra; - pub const synopsis: []const u8 = "only synopsis"; - pub const help: []const u8 = "h\n"; + pub const meta: Meta = .{ + .name = "only", + .group = .infra, + .synopsis = "only synopsis", + .help = "h\n", + .uppercase_first_arg = false, }; pub fn parseArgs(_: *RunCtx, _: []const []const u8) !ParsedArgs { return .{}; diff --git a/src/commands/history.zig b/src/commands/history.zig index 69c1b94..b3ab538 100644 --- a/src/commands/history.zig +++ b/src/commands/history.zig @@ -55,34 +55,34 @@ pub const ParsedArgs = union(enum) { 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 # 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 earliest as-of date (inclusive) - \\ --until latest as-of date (inclusive) - \\ --metric liquid (default), illiquid, or net_worth - \\ --resolution daily | weekly | monthly | auto - \\ (auto: daily ≤90d, weekly ≤730d, else monthly) - \\ --limit 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). - \\ - ; +pub const meta: framework.Meta = .{ + .name = "history", + .group = .symbol_lookup, + .synopsis = "Price history (symbol) or portfolio value timeline", + .uppercase_first_arg = true, + .help = + \\Usage: + \\ zfin history # 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 earliest as-of date (inclusive) + \\ --until latest as-of date (inclusive) + \\ --metric liquid (default), illiquid, or net_worth + \\ --resolution daily | weekly | monthly | auto + \\ (auto: daily ≤90d, weekly ≤730d, else monthly) + \\ --limit 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 { diff --git a/src/commands/lookup.zig b/src/commands/lookup.zig index 611c43e..83566b5 100644 --- a/src/commands/lookup.zig +++ b/src/commands/lookup.zig @@ -8,27 +8,27 @@ pub const ParsedArgs = struct { cusip: []const u8, }; -pub const meta = struct { - pub const name: []const u8 = "lookup"; - pub const group: framework.Group = .hygiene; - pub const synopsis: []const u8 = "Look up a CUSIP to ticker via OpenFIGI"; - pub const uppercase_first_arg: bool = true; - pub const help: []const u8 = - \\Usage: zfin lookup - \\ - \\Look up a CUSIP (9-char alphanumeric security identifier) to - \\its ticker via the OpenFIGI API. Successful results are cached - \\indefinitely in `cusip_tickers.srf`. OPENFIGI_API_KEY raises - \\the rate limit but the unauthenticated tier works for low - \\volume. - \\ - \\Mutual funds frequently have no OpenFIGI coverage; the - \\command surfaces that and suggests a manual portfolio entry. - \\ - \\Examples: - \\ zfin lookup 037833100 # → AAPL - \\ - ; +pub const meta: framework.Meta = .{ + .name = "lookup", + .group = .hygiene, + .synopsis = "Look up a CUSIP to ticker via OpenFIGI", + .uppercase_first_arg = true, + .help = + \\Usage: zfin lookup + \\ + \\Look up a CUSIP (9-char alphanumeric security identifier) to + \\its ticker via the OpenFIGI API. Successful results are cached + \\indefinitely in `cusip_tickers.srf`. OPENFIGI_API_KEY raises + \\the rate limit but the unauthenticated tier works for low + \\volume. + \\ + \\Mutual funds frequently have no OpenFIGI coverage; the + \\command surfaces that and suggests a manual portfolio entry. + \\ + \\Examples: + \\ zfin lookup 037833100 # → AAPL + \\ + , }; comptime { diff --git a/src/commands/milestones.zig b/src/commands/milestones.zig index fec9e71..a636ca2 100644 --- a/src/commands/milestones.zig +++ b/src/commands/milestones.zig @@ -36,31 +36,32 @@ pub const ParsedArgs = struct { 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 [--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 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). - \\ - ; +pub const meta: framework.Meta = .{ + .name = "milestones", + .group = .portfolio, + .synopsis = "Show portfolio threshold crossings (each $1M, doublings, etc.)", + .help = + \\Usage: zfin milestones --step [--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 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). + \\ + , + .uppercase_first_arg = false, }; comptime { diff --git a/src/commands/options.zig b/src/commands/options.zig index 19efa56..a1b9a9b 100644 --- a/src/commands/options.zig +++ b/src/commands/options.zig @@ -12,27 +12,27 @@ pub const ParsedArgs = struct { ntm: usize = 8, }; -pub const meta = struct { - pub const name: []const u8 = "options"; - pub const group: framework.Group = .symbol_lookup; - pub const synopsis: []const u8 = "Show options chain (all expirations) for a symbol"; - pub const uppercase_first_arg: bool = true; - pub const help: []const u8 = - \\Usage: zfin options [--ntm ] - \\ - \\Show the options chain (all expirations) for a symbol from - \\CBOE. Cached for 1 hour. The nearest monthly expiration is - \\auto-expanded with ±N strikes near the money; other - \\expirations are listed collapsed. - \\ - \\Options: - \\ --ntm Show ±N strikes near the money (default: 8) - \\ - \\Examples: - \\ zfin options SPY - \\ zfin options AAPL --ntm 12 - \\ - ; +pub const meta: framework.Meta = .{ + .name = "options", + .group = .symbol_lookup, + .synopsis = "Show options chain (all expirations) for a symbol", + .uppercase_first_arg = true, + .help = + \\Usage: zfin options [--ntm ] + \\ + \\Show the options chain (all expirations) for a symbol from + \\CBOE. Cached for 1 hour. The nearest monthly expiration is + \\auto-expanded with ±N strikes near the money; other + \\expirations are listed collapsed. + \\ + \\Options: + \\ --ntm Show ±N strikes near the money (default: 8) + \\ + \\Examples: + \\ zfin options SPY + \\ zfin options AAPL --ntm 12 + \\ + , }; comptime { diff --git a/src/commands/perf.zig b/src/commands/perf.zig index d5a5abd..e15aaeb 100644 --- a/src/commands/perf.zig +++ b/src/commands/perf.zig @@ -9,30 +9,30 @@ pub const ParsedArgs = struct { symbol: []const u8, }; -pub const meta = struct { - pub const name: []const u8 = "perf"; - pub const group: framework.Group = .symbol_lookup; - pub const synopsis: []const u8 = "Show 1y/3y/5y/10y trailing returns (Morningstar-style)"; - pub const uppercase_first_arg: bool = true; - pub const help: []const u8 = - \\Usage: zfin perf - \\ - \\Show Morningstar-style trailing returns for a symbol — 1Y, - \\3Y, 5Y, 10Y price-only and total-return CAGR plus risk - \\metrics (Sharpe, max drawdown, vol). Total returns require - \\POLYGON_API_KEY (for dividend history); price-only - \\returns work without it. - \\ - \\Two return tables are produced: - \\ - As-of: returns through the latest cached close. - \\ - Month-end: returns through the most recent calendar - \\ month-end (matches how mutual funds quote their stats). - \\ - \\Examples: - \\ zfin perf VTI # total-market index, 30+ years of data - \\ zfin perf NVDA # individual stock - \\ - ; +pub const meta: framework.Meta = .{ + .name = "perf", + .group = .symbol_lookup, + .synopsis = "Show 1y/3y/5y/10y trailing returns (Morningstar-style)", + .uppercase_first_arg = true, + .help = + \\Usage: zfin perf + \\ + \\Show Morningstar-style trailing returns for a symbol — 1Y, + \\3Y, 5Y, 10Y price-only and total-return CAGR plus risk + \\metrics (Sharpe, max drawdown, vol). Total returns require + \\POLYGON_API_KEY (for dividend history); price-only + \\returns work without it. + \\ + \\Two return tables are produced: + \\ - As-of: returns through the latest cached close. + \\ - Month-end: returns through the most recent calendar + \\ month-end (matches how mutual funds quote their stats). + \\ + \\Examples: + \\ zfin perf VTI # total-market index, 30+ years of data + \\ zfin perf NVDA # individual stock + \\ + , }; comptime { diff --git a/src/commands/portfolio.zig b/src/commands/portfolio.zig index e97636e..ae812e7 100644 --- a/src/commands/portfolio.zig +++ b/src/commands/portfolio.zig @@ -13,27 +13,28 @@ pub const ParsedArgs = struct { force_refresh: bool = false, }; -pub const meta = struct { - pub const name: []const u8 = "portfolio"; - pub const group: framework.Group = .portfolio; - pub const synopsis: []const u8 = "Load and analyze the portfolio (positions + valuations + watchlist)"; - pub const help: []const u8 = - \\Usage: zfin portfolio [--refresh] - \\ - \\Load `portfolio.srf` (cwd → ZFIN_HOME), refresh per-symbol - \\prices in parallel (server sync where ZFIN_SERVER is set, - \\else providers), and print the position table + valuations - \\+ historical-snapshot mini-tables. The watchlist (if - \\`watchlist.srf` exists) is appended to the price-load step - \\so its quotes show alongside. - \\ - \\Options: - \\ --refresh Force a re-fetch of every symbol's candles, - \\ bypassing the per-symbol TTL freshness check. - \\ Useful when you suspect cached data is wrong - \\ or after rotating providers. - \\ - ; +pub const meta: framework.Meta = .{ + .name = "portfolio", + .group = .portfolio, + .synopsis = "Load and analyze the portfolio (positions + valuations + watchlist)", + .help = + \\Usage: zfin portfolio [--refresh] + \\ + \\Load `portfolio.srf` (cwd → ZFIN_HOME), refresh per-symbol + \\prices in parallel (server sync where ZFIN_SERVER is set, + \\else providers), and print the position table + valuations + \\+ historical-snapshot mini-tables. The watchlist (if + \\`watchlist.srf` exists) is appended to the price-load step + \\so its quotes show alongside. + \\ + \\Options: + \\ --refresh Force a re-fetch of every symbol's candles, + \\ bypassing the per-symbol TTL freshness check. + \\ Useful when you suspect cached data is wrong + \\ or after rotating providers. + \\ + , + .uppercase_first_arg = false, }; comptime { diff --git a/src/commands/projections.zig b/src/commands/projections.zig index 3e97dc0..0c14935 100644 --- a/src/commands/projections.zig +++ b/src/commands/projections.zig @@ -65,48 +65,49 @@ pub const CompareArgs = struct { 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 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 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 (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). - \\ - ; +pub const meta: framework.Meta = .{ + .name = "projections", + .group = .portfolio, + .synopsis = "Retirement projections, benchmark comparison, percentile bands", + .help = + \\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 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 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 (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). + \\ + , + .uppercase_first_arg = false, }; comptime { diff --git a/src/commands/quote.zig b/src/commands/quote.zig index 21c3708..e6a0546 100644 --- a/src/commands/quote.zig +++ b/src/commands/quote.zig @@ -9,27 +9,27 @@ pub const ParsedArgs = struct { symbol: []const u8, }; -pub const meta = struct { - pub const name: []const u8 = "quote"; - pub const group: framework.Group = .symbol_lookup; - pub const synopsis: []const u8 = "Show latest quote with chart and 20-day history"; - pub const uppercase_first_arg: bool = true; - pub const help: []const u8 = - \\Usage: zfin quote - \\ - \\Show the latest real-time quote for a symbol (Yahoo / TwelveData) - \\plus a braille price chart of the last 60 candles and a table - \\of the last 20 trading days. - \\ - \\If real-time fetch fails, falls back to the cached close. The - \\Yahoo path is free and unauthenticated; TwelveData requires - \\TWELVEDATA_API_KEY. - \\ - \\Examples: - \\ zfin quote AAPL - \\ zfin quote spy # symbols are case-insensitive - \\ - ; +pub const meta: framework.Meta = .{ + .name = "quote", + .group = .symbol_lookup, + .synopsis = "Show latest quote with chart and 20-day history", + .uppercase_first_arg = true, + .help = + \\Usage: zfin quote + \\ + \\Show the latest real-time quote for a symbol (Yahoo / TwelveData) + \\plus a braille price chart of the last 60 candles and a table + \\of the last 20 trading days. + \\ + \\If real-time fetch fails, falls back to the cached close. The + \\Yahoo path is free and unauthenticated; TwelveData requires + \\TWELVEDATA_API_KEY. + \\ + \\Examples: + \\ zfin quote AAPL + \\ zfin quote spy # symbols are case-insensitive + \\ + , }; comptime { diff --git a/src/commands/snapshot.zig b/src/commands/snapshot.zig index 2e46526..0326afb 100644 --- a/src/commands/snapshot.zig +++ b/src/commands/snapshot.zig @@ -64,37 +64,38 @@ pub const ParsedArgs = struct { as_of_override: ?Date = null, }; -pub const meta = struct { - pub const name: []const u8 = "snapshot"; - pub const group: framework.Group = .timeseries; - pub const synopsis: []const u8 = "Write a daily portfolio snapshot to history/"; - pub const help: []const u8 = - \\Usage: zfin snapshot [opts] - \\ - \\Compute a portfolio snapshot for today (or a historical date with - \\`--as-of`) and write it as a discriminated SRF file under - \\`history/-portfolio.srf`. The output records start with - \\`kind::` so readers can demux on - \\that field. - \\ - \\Default mode: refresh the candle cache for held symbols, derive - \\`as_of_date` from the mode of cached candle dates, look up - \\close-on-or-before for each lot, and write atomically. - \\ - \\Options: - \\ --force overwrite existing snapshot for as_of_date - \\ --dry-run compute + print to stdout, do not write - \\ --out override output path - \\ --as-of write a snapshot for a historical date - \\ (uses git to recover portfolio state and - \\ candle cache for pricing). DATE accepts - \\ YYYY-MM-DD or relative shortcuts - \\ (1W/1M/1Q/1Y). - \\ - \\If `history/-portfolio.srf` already exists and `--force` - \\wasn't passed, the run skips with a stderr message. - \\ - ; +pub const meta: framework.Meta = .{ + .name = "snapshot", + .group = .timeseries, + .synopsis = "Write a daily portfolio snapshot to history/", + .help = + \\Usage: zfin snapshot [opts] + \\ + \\Compute a portfolio snapshot for today (or a historical date with + \\`--as-of`) and write it as a discriminated SRF file under + \\`history/-portfolio.srf`. The output records start with + \\`kind::` so readers can demux on + \\that field. + \\ + \\Default mode: refresh the candle cache for held symbols, derive + \\`as_of_date` from the mode of cached candle dates, look up + \\close-on-or-before for each lot, and write atomically. + \\ + \\Options: + \\ --force overwrite existing snapshot for as_of_date + \\ --dry-run compute + print to stdout, do not write + \\ --out override output path + \\ --as-of write a snapshot for a historical date + \\ (uses git to recover portfolio state and + \\ candle cache for pricing). DATE accepts + \\ YYYY-MM-DD or relative shortcuts + \\ (1W/1M/1Q/1Y). + \\ + \\If `history/-portfolio.srf` already exists and `--force` + \\wasn't passed, the run skips with a stderr message. + \\ + , + .uppercase_first_arg = false, }; comptime { diff --git a/src/commands/splits.zig b/src/commands/splits.zig index 7917afa..e38f7d7 100644 --- a/src/commands/splits.zig +++ b/src/commands/splits.zig @@ -8,23 +8,23 @@ pub const ParsedArgs = struct { symbol: []const u8, }; -pub const meta = struct { - pub const name: []const u8 = "splits"; - pub const group: framework.Group = .symbol_lookup; - pub const synopsis: []const u8 = "Show split history for a symbol"; - pub const uppercase_first_arg: bool = true; - pub const help: []const u8 = - \\Usage: zfin splits - \\ - \\Show the split history for a symbol from Polygon.io. Cached - \\for 14 days; subsequent calls within that window serve the - \\cached data without an API call. - \\ - \\Examples: - \\ 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), ... - \\ - ; +pub const meta: framework.Meta = .{ + .name = "splits", + .group = .symbol_lookup, + .synopsis = "Show split history for a symbol", + .uppercase_first_arg = true, + .help = + \\Usage: zfin splits + \\ + \\Show the split history for a symbol from Polygon.io. Cached + \\for 14 days; subsequent calls within that window serve the + \\cached data without an API call. + \\ + \\Examples: + \\ 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), ... + \\ + , }; comptime { diff --git a/src/commands/version.zig b/src/commands/version.zig index 13595e0..c625d70 100644 --- a/src/commands/version.zig +++ b/src/commands/version.zig @@ -19,19 +19,20 @@ pub const ParsedArgs = struct { verbose: bool = false, }; -pub const meta = struct { - pub const name: []const u8 = "version"; - pub const group: framework.Group = .infra; - pub const synopsis: []const u8 = "Show zfin version and build info"; - pub const help: []const u8 = - \\Usage: zfin version [-v|--verbose] - \\ - \\Print zfin's version + build date. With `--verbose`/`-v`, also - \\prints the Zig compiler version, build mode, build target, - \\resolved ZFIN_HOME, and cache directory — useful for bug - \\reports. - \\ - ; +pub const meta: framework.Meta = .{ + .name = "version", + .group = .infra, + .synopsis = "Show zfin version and build info", + .help = + \\Usage: zfin version [-v|--verbose] + \\ + \\Print zfin's version + build date. With `--verbose`/`-v`, also + \\prints the Zig compiler version, build mode, build target, + \\resolved ZFIN_HOME, and cache directory — useful for bug + \\reports. + \\ + , + .uppercase_first_arg = false, }; comptime { diff --git a/src/main.zig b/src/main.zig index d519caa..298fea6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -331,7 +331,7 @@ fn runCli(init: std.process.Init) !u8 { .color = color, .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) else cmd_args;