rework framework to something much more sane
This commit is contained in:
parent
8e4dfffc3f
commit
4e6ae0ba51
22 changed files with 597 additions and 601 deletions
|
|
@ -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 <portfolio.srf> > 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 <portfolio.srf> > metadata.srf` to bootstrap
|
||||
\\classifications, then edit by hand.
|
||||
\\
|
||||
,
|
||||
.uppercase_first_arg = false,
|
||||
};
|
||||
|
||||
comptime {
|
||||
|
|
|
|||
|
|
@ -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 <N> Manual price staleness threshold (default 3)
|
||||
\\ --fidelity <CSV> Fidelity positions CSV export
|
||||
\\ ("All accounts" → Positions tab → Download)
|
||||
\\ --schwab <CSV> 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 <N> Manual price staleness threshold (default 3)
|
||||
\\ --fidelity <CSV> Fidelity positions CSV export
|
||||
\\ ("All accounts" → Positions tab → Download)
|
||||
\\ --schwab <CSV> 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 {
|
||||
|
|
|
|||
|
|
@ -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 <stats|clear>
|
||||
\\
|
||||
\\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 <stats|clear>
|
||||
\\
|
||||
\\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 {
|
||||
|
|
|
|||
|
|
@ -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 <DATE> compare DATE vs. live
|
||||
\\ zfin compare <DATE1> <DATE2> 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 <DATE> Override the before-side snapshot.
|
||||
\\ --snapshot-after <DATE> Override the after-side snapshot;
|
||||
\\ accepts `live` for the current
|
||||
\\ portfolio.
|
||||
\\ --commit-before <SPEC> Pin the before-side commit for the
|
||||
\\ attribution / contributions block.
|
||||
\\ --commit-after <SPEC> 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 <DATE> compare DATE vs. live
|
||||
\\ zfin compare <DATE1> <DATE2> 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 <DATE> Override the before-side snapshot.
|
||||
\\ --snapshot-after <DATE> Override the after-side snapshot;
|
||||
\\ accepts `live` for the current
|
||||
\\ portfolio.
|
||||
\\ --commit-before <SPEC> Pin the before-side commit for the
|
||||
\\ attribution / contributions block.
|
||||
\\ --commit-after <SPEC> Pin the after-side commit; accepts
|
||||
\\ `working` for the working copy.
|
||||
\\
|
||||
,
|
||||
.uppercase_first_arg = false,
|
||||
};
|
||||
|
||||
comptime {
|
||||
|
|
|
|||
|
|
@ -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 <DATE> commit-at-or-before(DATE) vs HEAD (or
|
||||
\\ working copy when dirty)
|
||||
\\ --since <D1> --until <D2> commit-at-or-before(D1) vs
|
||||
\\ commit-at-or-before(D2)
|
||||
\\ --until <DATE> alone rejected; window is ambiguous
|
||||
\\
|
||||
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
|
||||
\\
|
||||
\\Options:
|
||||
\\ --since <DATE> Earliest side (resolves to commit-at-
|
||||
\\ or-before).
|
||||
\\ --until <DATE> Latest side. Pair with --since.
|
||||
\\ --commit-before <SPEC> Pin the before commit directly. Same
|
||||
\\ grammar as --commit-after, minus
|
||||
\\ `working`. Useful when you committed
|
||||
\\ after your review date.
|
||||
\\ --commit-after <SPEC> 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 <DATE> commit-at-or-before(DATE) vs HEAD (or
|
||||
\\ working copy when dirty)
|
||||
\\ --since <D1> --until <D2> commit-at-or-before(D1) vs
|
||||
\\ commit-at-or-before(D2)
|
||||
\\ --until <DATE> alone rejected; window is ambiguous
|
||||
\\
|
||||
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
|
||||
\\
|
||||
\\Options:
|
||||
\\ --since <DATE> Earliest side (resolves to commit-at-
|
||||
\\ or-before).
|
||||
\\ --until <DATE> Latest side. Pair with --since.
|
||||
\\ --commit-before <SPEC> Pin the before commit directly. Same
|
||||
\\ grammar as --commit-after, minus
|
||||
\\ `working`. Useful when you committed
|
||||
\\ after your review date.
|
||||
\\ --commit-after <SPEC> 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 {
|
||||
|
|
|
|||
|
|
@ -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 <SYMBOL>
|
||||
\\
|
||||
\\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 <SYMBOL>
|
||||
\\
|
||||
\\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 {
|
||||
|
|
|
|||
|
|
@ -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 <SYMBOL>
|
||||
\\
|
||||
\\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 <SYMBOL>
|
||||
\\
|
||||
\\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 {
|
||||
|
|
|
|||
|
|
@ -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 <FILE|SYMBOL>
|
||||
\\
|
||||
\\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 <FILE|SYMBOL>
|
||||
\\
|
||||
\\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 {
|
||||
|
|
|
|||
|
|
@ -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 <SYMBOL>
|
||||
\\
|
||||
\\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 <SYMBOL>
|
||||
\\
|
||||
\\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 {
|
||||
|
|
|
|||
|
|
@ -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 <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 ────────────────────────────────────────────
|
||||
|
||||
/// 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 <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 ──────────────────────────────────────────
|
||||
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 .{};
|
||||
|
|
|
|||
|
|
@ -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 <SYMBOL> # last 30 days of candles
|
||||
\\ zfin history [flags] # portfolio-value timeline
|
||||
\\
|
||||
\\Two modes in one command. Symbol mode (positional symbol)
|
||||
\\shows the last 30 trading days of candles for that symbol.
|
||||
\\Portfolio mode (no positional, optionally with flags) reads
|
||||
\\`history/*-portfolio.srf` snapshots and renders rolling-windows
|
||||
\\returns + a braille chart + a recent-snapshots table.
|
||||
\\
|
||||
\\Portfolio-mode flags:
|
||||
\\ --since <DATE> earliest as-of date (inclusive)
|
||||
\\ --until <DATE> latest as-of date (inclusive)
|
||||
\\ --metric <name> liquid (default), illiquid, or net_worth
|
||||
\\ --resolution <name> daily | weekly | monthly | auto
|
||||
\\ (auto: daily ≤90d, weekly ≤730d, else monthly)
|
||||
\\ --limit <N> cap recent-snapshots table to N rows (default 40)
|
||||
\\ --rebuild-rollup regenerate history/rollup.srf and exit
|
||||
\\
|
||||
\\DATE accepts YYYY-MM-DD or relative shortcuts (1W/1M/1Q/1Y).
|
||||
\\
|
||||
;
|
||||
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 <SYMBOL> # last 30 days of candles
|
||||
\\ zfin history [flags] # portfolio-value timeline
|
||||
\\
|
||||
\\Two modes in one command. Symbol mode (positional symbol)
|
||||
\\shows the last 30 trading days of candles for that symbol.
|
||||
\\Portfolio mode (no positional, optionally with flags) reads
|
||||
\\`history/*-portfolio.srf` snapshots and renders rolling-windows
|
||||
\\returns + a braille chart + a recent-snapshots table.
|
||||
\\
|
||||
\\Portfolio-mode flags:
|
||||
\\ --since <DATE> earliest as-of date (inclusive)
|
||||
\\ --until <DATE> latest as-of date (inclusive)
|
||||
\\ --metric <name> liquid (default), illiquid, or net_worth
|
||||
\\ --resolution <name> daily | weekly | monthly | auto
|
||||
\\ (auto: daily ≤90d, weekly ≤730d, else monthly)
|
||||
\\ --limit <N> cap recent-snapshots table to N rows (default 40)
|
||||
\\ --rebuild-rollup regenerate history/rollup.srf and exit
|
||||
\\
|
||||
\\DATE accepts YYYY-MM-DD or relative shortcuts (1W/1M/1Q/1Y).
|
||||
\\
|
||||
,
|
||||
};
|
||||
|
||||
comptime {
|
||||
|
|
|
|||
|
|
@ -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 <CUSIP>
|
||||
\\
|
||||
\\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 <CUSIP>
|
||||
\\
|
||||
\\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 {
|
||||
|
|
|
|||
|
|
@ -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 <expr> [--real]
|
||||
\\
|
||||
\\Find the dates the portfolio first reached each of a
|
||||
\\configured set of thresholds. Two threshold modes:
|
||||
\\
|
||||
\\ Absolute dollar: 1M / 1m / 1500000 / 1.5M / 500K / 500k
|
||||
\\ Relative multiplier: 2x / 2X / 1.5x
|
||||
\\
|
||||
\\Rejects %, ≤0 dollar steps, ≤1.0x multipliers, NaN/Inf.
|
||||
\\
|
||||
\\Options:
|
||||
\\ --step <expr> Threshold step (required).
|
||||
\\ --real Deflate the series to the last full Shiller
|
||||
\\ year before detecting crossings (CPI-adjusted).
|
||||
\\ Default is nominal.
|
||||
\\
|
||||
\\Note: crossing dates are "first observed at," bounded by the
|
||||
\\source series cadence (typically weekly).
|
||||
\\
|
||||
;
|
||||
pub const meta: framework.Meta = .{
|
||||
.name = "milestones",
|
||||
.group = .portfolio,
|
||||
.synopsis = "Show portfolio threshold crossings (each $1M, doublings, etc.)",
|
||||
.help =
|
||||
\\Usage: zfin milestones --step <expr> [--real]
|
||||
\\
|
||||
\\Find the dates the portfolio first reached each of a
|
||||
\\configured set of thresholds. Two threshold modes:
|
||||
\\
|
||||
\\ Absolute dollar: 1M / 1m / 1500000 / 1.5M / 500K / 500k
|
||||
\\ Relative multiplier: 2x / 2X / 1.5x
|
||||
\\
|
||||
\\Rejects %, ≤0 dollar steps, ≤1.0x multipliers, NaN/Inf.
|
||||
\\
|
||||
\\Options:
|
||||
\\ --step <expr> Threshold step (required).
|
||||
\\ --real Deflate the series to the last full Shiller
|
||||
\\ year before detecting crossings (CPI-adjusted).
|
||||
\\ Default is nominal.
|
||||
\\
|
||||
\\Note: crossing dates are "first observed at," bounded by the
|
||||
\\source series cadence (typically weekly).
|
||||
\\
|
||||
,
|
||||
.uppercase_first_arg = false,
|
||||
};
|
||||
|
||||
comptime {
|
||||
|
|
|
|||
|
|
@ -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 <SYMBOL> [--ntm <N>]
|
||||
\\
|
||||
\\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 <N> 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 <SYMBOL> [--ntm <N>]
|
||||
\\
|
||||
\\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 <N> Show ±N strikes near the money (default: 8)
|
||||
\\
|
||||
\\Examples:
|
||||
\\ zfin options SPY
|
||||
\\ zfin options AAPL --ntm 12
|
||||
\\
|
||||
,
|
||||
};
|
||||
|
||||
comptime {
|
||||
|
|
|
|||
|
|
@ -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 <SYMBOL>
|
||||
\\
|
||||
\\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 <SYMBOL>
|
||||
\\
|
||||
\\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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 <DATE> Side-by-side compare with a
|
||||
\\ historical snapshot's projection.
|
||||
\\ --convergence Plot the model's predicted
|
||||
\\ retirement date over time as data
|
||||
\\ accumulated.
|
||||
\\ --return-backtest Plot expected_return claim over
|
||||
\\ time alongside realized forward
|
||||
\\ CAGR. Pair with `--real` for
|
||||
\\ CPI-adjusted dollars.
|
||||
\\
|
||||
\\Options:
|
||||
\\ --no-events Exclude life events from the
|
||||
\\ simulation (baseline view).
|
||||
\\ --as-of <DATE> Compute against a historical
|
||||
\\ snapshot. Auto-snaps to the
|
||||
\\ nearest-earlier snapshot.
|
||||
\\ --overlay-actuals Plot realized portfolio trajectory
|
||||
\\ from --as-of through today on top
|
||||
\\ of the percentile bands. Requires
|
||||
\\ --as-of. Ignored under --vs.
|
||||
\\ --vs <DATE> (see above)
|
||||
\\ --convergence (see above)
|
||||
\\ --return-backtest (see above)
|
||||
\\ --real With --return-backtest, render in
|
||||
\\ CPI-adjusted dollars.
|
||||
\\
|
||||
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
|
||||
\\
|
||||
;
|
||||
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 <DATE> Side-by-side compare with a
|
||||
\\ historical snapshot's projection.
|
||||
\\ --convergence Plot the model's predicted
|
||||
\\ retirement date over time as data
|
||||
\\ accumulated.
|
||||
\\ --return-backtest Plot expected_return claim over
|
||||
\\ time alongside realized forward
|
||||
\\ CAGR. Pair with `--real` for
|
||||
\\ CPI-adjusted dollars.
|
||||
\\
|
||||
\\Options:
|
||||
\\ --no-events Exclude life events from the
|
||||
\\ simulation (baseline view).
|
||||
\\ --as-of <DATE> Compute against a historical
|
||||
\\ snapshot. Auto-snaps to the
|
||||
\\ nearest-earlier snapshot.
|
||||
\\ --overlay-actuals Plot realized portfolio trajectory
|
||||
\\ from --as-of through today on top
|
||||
\\ of the percentile bands. Requires
|
||||
\\ --as-of. Ignored under --vs.
|
||||
\\ --vs <DATE> (see above)
|
||||
\\ --convergence (see above)
|
||||
\\ --return-backtest (see above)
|
||||
\\ --real With --return-backtest, render in
|
||||
\\ CPI-adjusted dollars.
|
||||
\\
|
||||
\\Date forms: YYYY-MM-DD or relative (1W/1M/1Q/1Y).
|
||||
\\
|
||||
,
|
||||
.uppercase_first_arg = false,
|
||||
};
|
||||
|
||||
comptime {
|
||||
|
|
|
|||
|
|
@ -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 <SYMBOL>
|
||||
\\
|
||||
\\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 <SYMBOL>
|
||||
\\
|
||||
\\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 {
|
||||
|
|
|
|||
|
|
@ -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/<as_of_date>-portfolio.srf`. The output records start with
|
||||
\\`kind::<meta|total|tax_type|account|lot>` 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 <path> override output path
|
||||
\\ --as-of <DATE> 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/<as_of_date>-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/<as_of_date>-portfolio.srf`. The output records start with
|
||||
\\`kind::<meta|total|tax_type|account|lot>` 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 <path> override output path
|
||||
\\ --as-of <DATE> 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/<as_of_date>-portfolio.srf` already exists and `--force`
|
||||
\\wasn't passed, the run skips with a stderr message.
|
||||
\\
|
||||
,
|
||||
.uppercase_first_arg = false,
|
||||
};
|
||||
|
||||
comptime {
|
||||
|
|
|
|||
|
|
@ -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 <SYMBOL>
|
||||
\\
|
||||
\\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 <SYMBOL>
|
||||
\\
|
||||
\\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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue