finradar-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. finradar_cli/__init__.py +1 -0
  2. finradar_cli/__main__.py +4 -0
  3. finradar_cli/_generated/__init__.py +96 -0
  4. finradar_cli/_generated/cmd_13dg.py +23 -0
  5. finradar_cli/_generated/cmd_13f.py +291 -0
  6. finradar_cli/_generated/cmd_billing.py +68 -0
  7. finradar_cli/_generated/cmd_core.py +38 -0
  8. finradar_cli/_generated/cmd_cusip.py +124 -0
  9. finradar_cli/_generated/cmd_download.py +23 -0
  10. finradar_cli/_generated/cmd_facts.py +38 -0
  11. finradar_cli/_generated/cmd_financials.py +141 -0
  12. finradar_cli/_generated/cmd_holders.py +53 -0
  13. finradar_cli/_generated/cmd_index.py +21 -0
  14. finradar_cli/_generated/cmd_insider.py +322 -0
  15. finradar_cli/_generated/cmd_market.py +23 -0
  16. finradar_cli/_generated/cmd_ownership.py +111 -0
  17. finradar_cli/_generated/cmd_ownership_analytics.py +38 -0
  18. finradar_cli/_generated/cmd_positions.py +83 -0
  19. finradar_cli/_generated/cmd_pricing.py +53 -0
  20. finradar_cli/_generated/cmd_progress.py +23 -0
  21. finradar_cli/_generated/cmd_search.py +171 -0
  22. finradar_cli/_generated/cmd_sec.py +141 -0
  23. finradar_cli/_generated/cmd_status.py +36 -0
  24. finradar_cli/_generated/cmd_ticker.py +92 -0
  25. finradar_cli/_generated/cmd_user.py +21 -0
  26. finradar_cli/_generated/command_index.json +1308 -0
  27. finradar_cli/_generated/completion/commands.json +373 -0
  28. finradar_cli/app.py +103 -0
  29. finradar_cli/commands/__init__.py +0 -0
  30. finradar_cli/commands/api.py +124 -0
  31. finradar_cli/commands/auth_cmds.py +64 -0
  32. finradar_cli/commands/meta.py +83 -0
  33. finradar_cli/commands/update_cmd.py +234 -0
  34. finradar_cli/core/__init__.py +0 -0
  35. finradar_cli/core/auth.py +178 -0
  36. finradar_cli/core/client.py +95 -0
  37. finradar_cli/core/coerce.py +59 -0
  38. finradar_cli/core/config.py +80 -0
  39. finradar_cli/core/curation.py +17 -0
  40. finradar_cli/core/errors.py +44 -0
  41. finradar_cli/core/oauth.py +206 -0
  42. finradar_cli/core/output.py +49 -0
  43. finradar_cli/core/spec.py +42 -0
  44. finradar_cli/data/openapi.json +12797 -0
  45. finradar_cli/marquee.toml +37 -0
  46. finradar_cli/type_overrides.toml +6 -0
  47. finradar_cli-0.1.0.dist-info/METADATA +19 -0
  48. finradar_cli-0.1.0.dist-info/RECORD +50 -0
  49. finradar_cli-0.1.0.dist-info/WHEEL +4 -0
  50. finradar_cli-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .app import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -0,0 +1,96 @@
1
+ """GENERATED by generate.py — do not edit. Run `make generate`."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+
6
+ from . import cmd_13dg
7
+ from . import cmd_13f
8
+ from . import cmd_billing
9
+ from . import cmd_core
10
+ from . import cmd_cusip
11
+ from . import cmd_download
12
+ from . import cmd_facts
13
+ from . import cmd_financials
14
+ from . import cmd_holders
15
+ from . import cmd_index
16
+ from . import cmd_insider
17
+ from . import cmd_market
18
+ from . import cmd_ownership
19
+ from . import cmd_ownership_analytics
20
+ from . import cmd_positions
21
+ from . import cmd_pricing
22
+ from . import cmd_progress
23
+ from . import cmd_search
24
+ from . import cmd_sec
25
+ from . import cmd_status
26
+ from . import cmd_ticker
27
+ from . import cmd_user
28
+
29
+
30
+ def register(app: typer.Typer) -> None:
31
+ _g_13dg = typer.Typer(no_args_is_help=True)
32
+ app.add_typer(_g_13dg, name='13dg')
33
+ cmd_13dg.build(_g_13dg)
34
+ _g_13f = typer.Typer(no_args_is_help=True)
35
+ app.add_typer(_g_13f, name='13f')
36
+ cmd_13f.build(_g_13f)
37
+ _g_billing = typer.Typer(no_args_is_help=True)
38
+ app.add_typer(_g_billing, name='billing')
39
+ cmd_billing.build(_g_billing)
40
+ _g_core = typer.Typer(no_args_is_help=True)
41
+ app.add_typer(_g_core, name='core')
42
+ cmd_core.build(_g_core)
43
+ _g_cusip = typer.Typer(no_args_is_help=True)
44
+ app.add_typer(_g_cusip, name='cusip')
45
+ cmd_cusip.build(_g_cusip)
46
+ _g_download = typer.Typer(no_args_is_help=True)
47
+ app.add_typer(_g_download, name='download')
48
+ cmd_download.build(_g_download)
49
+ _g_facts = typer.Typer(no_args_is_help=True)
50
+ app.add_typer(_g_facts, name='facts')
51
+ cmd_facts.build(_g_facts)
52
+ _g_financials = typer.Typer(no_args_is_help=True)
53
+ app.add_typer(_g_financials, name='financials')
54
+ cmd_financials.build(_g_financials)
55
+ _g_holders = typer.Typer(no_args_is_help=True)
56
+ app.add_typer(_g_holders, name='holders')
57
+ cmd_holders.build(_g_holders)
58
+ _g_index = typer.Typer(no_args_is_help=True)
59
+ app.add_typer(_g_index, name='index')
60
+ cmd_index.build(_g_index)
61
+ _g_insider = typer.Typer(no_args_is_help=True)
62
+ app.add_typer(_g_insider, name='insider')
63
+ cmd_insider.build(_g_insider)
64
+ _g_market = typer.Typer(no_args_is_help=True)
65
+ app.add_typer(_g_market, name='market')
66
+ cmd_market.build(_g_market)
67
+ _g_ownership = typer.Typer(no_args_is_help=True)
68
+ app.add_typer(_g_ownership, name='ownership')
69
+ cmd_ownership.build(_g_ownership)
70
+ _g_ownership_analytics = typer.Typer(no_args_is_help=True)
71
+ app.add_typer(_g_ownership_analytics, name='ownership-analytics')
72
+ cmd_ownership_analytics.build(_g_ownership_analytics)
73
+ _g_positions = typer.Typer(no_args_is_help=True)
74
+ app.add_typer(_g_positions, name='positions')
75
+ cmd_positions.build(_g_positions)
76
+ _g_pricing = typer.Typer(no_args_is_help=True)
77
+ app.add_typer(_g_pricing, name='pricing')
78
+ cmd_pricing.build(_g_pricing)
79
+ _g_progress = typer.Typer(no_args_is_help=True)
80
+ app.add_typer(_g_progress, name='progress')
81
+ cmd_progress.build(_g_progress)
82
+ _g_search = typer.Typer(no_args_is_help=True)
83
+ app.add_typer(_g_search, name='search')
84
+ cmd_search.build(_g_search)
85
+ _g_sec = typer.Typer(no_args_is_help=True)
86
+ app.add_typer(_g_sec, name='sec')
87
+ cmd_sec.build(_g_sec)
88
+ _g_status = typer.Typer(no_args_is_help=True)
89
+ app.add_typer(_g_status, name='status')
90
+ cmd_status.build(_g_status)
91
+ _g_ticker = typer.Typer(no_args_is_help=True)
92
+ app.add_typer(_g_ticker, name='ticker')
93
+ cmd_ticker.build(_g_ticker)
94
+ _g_user = typer.Typer(no_args_is_help=True)
95
+ app.add_typer(_g_user, name='user')
96
+ cmd_user.build(_g_user)
@@ -0,0 +1,23 @@
1
+ """GENERATED by generate.py — do not edit. Run `make generate`."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+ from finradar_cli.commands.api import run_request
6
+
7
+
8
+ def build(parent: typer.Typer) -> None:
9
+ _sub_form_13d = typer.Typer(no_args_is_help=True)
10
+ parent.add_typer(_sub_form_13d, name='form-13d')
11
+
12
+ @_sub_form_13d.command('create', help='Form 13D [cost 0]')
13
+ def cmd_post_api_v1_form_13d(ctx: typer.Context, query_ticker: str = typer.Option(None, '--query.ticker', help="Issuer trading symbol (e.g. 'TWTR'). Case-insensitive."), query_issuerCik: str = typer.Option(None, '--query.issuerCik', help='Issuer CIK number. Leading zeros are stripped automatically.'), query_filerCik: str = typer.Option(None, '--query.filerCik', help='Filer (reporting person) CIK. Leading zeros are stripped automatically.'), query_filerName: str = typer.Option(None, '--query.filerName', help="Partial match on filer name (case-insensitive). E.g. 'Elon Musk'."), query_minOwnership: str = typer.Option(None, '--query.minOwnership', help='Minimum percent of class owned. E.g. 5.0 for >= 5%.'), query_formType: str = typer.Option(None, '--query.formType', help="Filter by form type: 'SC 13D', 'SC 13D/A', 'SC 13G', 'SC 13G/A'. Both SC and SCHEDULE variants accepted."), query_dateRange_from: str = typer.Option(None, '--query.dateRange.from', help="Start date filter (ISO format, e.g. '2025-01-01')."), query_dateRange_to: str = typer.Option(None, '--query.dateRange.to', help="End date filter (ISO format, e.g. '2026-03-14')."), from_: str = typer.Option(None, '--from', help='Pagination offset.'), size: str = typer.Option(None, '--size', help='Page size (max 50).'), sort: str = typer.Option(None, '--sort', help='Sort specification. Supported fields: filedAt, percentOfClass, sharesOwned.'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
14
+ url = '/api/v1/form-13d'
15
+ _pp: dict[str, str] = {}
16
+ for _k, _v in _pp.items():
17
+ url = url.replace("{" + _k + "}", str(_v))
18
+ _kv: list[str] = []
19
+ _body: dict[str, str | None] = {'query.ticker': query_ticker, 'query.issuerCik': query_issuerCik, 'query.filerCik': query_filerCik, 'query.filerName': query_filerName, 'query.minOwnership': query_minOwnership, 'query.formType': query_formType, 'query.dateRange.from': query_dateRange_from, 'query.dateRange.to': query_dateRange_to, 'from': from_, 'size': size, 'sort': sort}
20
+ for _name, _val in _body.items():
21
+ if _val is not None:
22
+ _kv.append(f"{_name}={_val}")
23
+ run_request(ctx.obj, 'POST', url, _kv, data=data)
@@ -0,0 +1,291 @@
1
+ """GENERATED by generate.py — do not edit. Run `make generate`."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+ from finradar_cli.commands.api import run_request
6
+
7
+
8
+ def build(parent: typer.Typer) -> None:
9
+ _sub_bulk = typer.Typer(no_args_is_help=True)
10
+ parent.add_typer(_sub_bulk, name='bulk')
11
+ _sub_by_industry = typer.Typer(no_args_is_help=True)
12
+ parent.add_typer(_sub_by_industry, name='by-industry')
13
+ _sub_by_sector = typer.Typer(no_args_is_help=True)
14
+ parent.add_typer(_sub_by_sector, name='by-sector')
15
+ _sub_by_ticker = typer.Typer(no_args_is_help=True)
16
+ parent.add_typer(_sub_by_ticker, name='by-ticker')
17
+ _sub_cover_pages = typer.Typer(no_args_is_help=True)
18
+ parent.add_typer(_sub_cover_pages, name='cover-pages')
19
+ _sub_cross_filing_signals = typer.Typer(no_args_is_help=True)
20
+ parent.add_typer(_sub_cross_filing_signals, name='cross-filing-signals')
21
+ _sub_filers = typer.Typer(no_args_is_help=True)
22
+ parent.add_typer(_sub_filers, name='filers')
23
+ _sub_filing_stats = typer.Typer(no_args_is_help=True)
24
+ parent.add_typer(_sub_filing_stats, name='filing-stats')
25
+ _sub_form_13f = typer.Typer(no_args_is_help=True)
26
+ parent.add_typer(_sub_form_13f, name='form-13f')
27
+ _sub_fund = typer.Typer(no_args_is_help=True)
28
+ parent.add_typer(_sub_fund, name='fund')
29
+ _sub_fund_performance = typer.Typer(no_args_is_help=True)
30
+ parent.add_typer(_sub_fund_performance, name='fund-performance')
31
+ _sub_funds = typer.Typer(no_args_is_help=True)
32
+ parent.add_typer(_sub_funds, name='funds')
33
+ _sub_holdings = typer.Typer(no_args_is_help=True)
34
+ parent.add_typer(_sub_holdings, name='holdings')
35
+ _sub_holdings_analytics = typer.Typer(no_args_is_help=True)
36
+ parent.add_typer(_sub_holdings_analytics, name='holdings-analytics')
37
+ _sub_latest = typer.Typer(no_args_is_help=True)
38
+ parent.add_typer(_sub_latest, name='latest')
39
+ _sub_market_rotation = typer.Typer(no_args_is_help=True)
40
+ parent.add_typer(_sub_market_rotation, name='market-rotation')
41
+ _sub_search = typer.Typer(no_args_is_help=True)
42
+ parent.add_typer(_sub_search, name='search')
43
+ _sub_form_13f_search = typer.Typer(no_args_is_help=True)
44
+ _sub_form_13f.add_typer(_sub_form_13f_search, name='search')
45
+
46
+ @_sub_bulk.command('create', help='Form 13F Fund Bulk [cost 0]')
47
+ def cmd_post_api_v1_form_13f_fund_bulk(ctx: typer.Context, period: str = typer.Option(None, '--period', help='Report period YYYY-MM-DD. Applied to all funds.; example: 2025Q4'), detail: str = typer.Option(None, '--detail', help="'summary' or 'full'. Summary returns top 10 holdings only."), size: str = typer.Option(None, '--size', help='Holdings per page (only for detail=full). Accepted alias: `limit`.; example: 50'), from_: str = typer.Option(None, '--from', help='Pagination offset (only for detail=full). Accepted alias: `offset`.; example: 0'), ciks: str = typer.Option(None, '--ciks', help='Array of CIK strings, max 20. e.g. ["1067983", "1649339"]'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
48
+ url = '/api/v1/form-13f/fund/bulk'
49
+ _pp: dict[str, str] = {}
50
+ for _k, _v in _pp.items():
51
+ url = url.replace("{" + _k + "}", str(_v))
52
+ _kv: list[str] = []
53
+ _body: dict[str, str | None] = {'period': period, 'detail': detail, 'size': size, 'from': from_, 'ciks': ciks}
54
+ for _name, _val in _body.items():
55
+ if _val is not None:
56
+ _kv.append(f"{_name}={_val}")
57
+ run_request(ctx.obj, 'POST', url, _kv, data=data)
58
+
59
+ @_sub_by_industry.command('get', help='Form 13F Aggregation By Industry [cost 0]')
60
+ def cmd_get_api_v1_form_13f_aggregation_by_industry(ctx: typer.Context, quarter: str = typer.Option(None, '--quarter', help='Report period as `YYYY-MM-DD` (any quarter-end date). Defaults to the latest quarter present in `sec_13f_sector_summary`.; example: 2025-12-31'), compare: str = typer.Option(None, '--compare', help='Set to `prev` to include per-industry quarter-over-quarter deltas: `flow_change`, `prev_momentum_label`, `momentum_shift`, `fund_count_change`, `value_change_pct`. Omitted/any-other-value → `compare` is `null` per row.; example: prev'), limit: str = typer.Option(None, '--limit', help='Top-N inflows AND Top-N outflows to return when no `sector` filter is set (so the response carries up to `2 × limit` industries). When `sector` is set, the inflow/outflow split is skipped and `limit` caps total industries returned. Integer (1-200) or the string `all` (returns the full industry universe — ~150 rows). Default 50.; example: 20'), sort_by: str = typer.Option(None, '--sort_by', help='`fund_flow` (absolute USD net flow) or `significance` (`flow_significance_pct = fund_flow / total_value × 100`). Inflows sort descending; outflows sort ascending by signed value (most negative first). With `sector` filter, all industries sort by `abs(metric)` descending. Legacy alias `sort` is accepted for cross-endpoint consistency with `/by-sector`.; example: significance'), sector: str = typer.Option(None, '--sector', help='Optional drill-down. When set, restricts the response to industries within this sector and skips the inflow/outflow split (returns a single ranked list). Sector name must match the canonical Sharadar label exactly (e.g. `Technology`, `Healthcare`, `Financial Services`).; example: Technology'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
61
+ url = '/api/v1/form-13f/aggregation/by-industry'
62
+ _pp: dict[str, str] = {}
63
+ for _k, _v in _pp.items():
64
+ url = url.replace("{" + _k + "}", str(_v))
65
+ _kv: list[str] = []
66
+ _body: dict[str, str | None] = {'quarter': quarter, 'compare': compare, 'limit': limit, 'sort_by': sort_by, 'sector': sector}
67
+ for _name, _val in _body.items():
68
+ if _val is not None:
69
+ _kv.append(f"{_name}={_val}")
70
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
71
+
72
+ @_sub_by_sector.command('get', help='Form 13F Aggregation By Sector [cost 0]')
73
+ def cmd_get_api_v1_form_13f_aggregation_by_sector(ctx: typer.Context, quarter: str = typer.Option(None, '--quarter', help='Report period YYYY-MM-DD (e.g. 2025-12-31). Defaults to latest available quarter.; example: 2025Q4'), compare: str = typer.Option(None, '--compare', help="Set to 'prev' to include quarter-over-quarter deltas per sector: flow_change, momentum_shift, fund_count_change, value_change_pct.; example: false"), industry_limit: str = typer.Option(None, '--industry_limit', help="Max industries per direction (inflows + outflows) per sector. Use 'all' for full breakdown. Range: 1-100."), sort: str = typer.Option(None, '--sort', help="'fund_flow' (absolute $) or 'significance' (flow_significance_pct). Applies to both sector and industry ordering.; example: filed_at"), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
74
+ url = '/api/v1/form-13f/aggregation/by-sector'
75
+ _pp: dict[str, str] = {}
76
+ for _k, _v in _pp.items():
77
+ url = url.replace("{" + _k + "}", str(_v))
78
+ _kv: list[str] = []
79
+ _body: dict[str, str | None] = {'quarter': quarter, 'compare': compare, 'industry_limit': industry_limit, 'sort': sort}
80
+ for _name, _val in _body.items():
81
+ if _val is not None:
82
+ _kv.append(f"{_name}={_val}")
83
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
84
+
85
+ @_sub_by_ticker.command('get', help='Form 13F Aggregation By Ticker [cost 0]')
86
+ def cmd_get_api_v1_form_13f_aggregation_by_ticker(ctx: typer.Context, quarter: str = typer.Option(None, '--quarter', help='Quarter-end date (YYYY-MM-DD). Defaults to latest available quarter.; example: 2025Q4'), sort_by: str = typer.Option(None, '--sort_by', help="Column to sort by. Options: symbol, name_of_issuer, fund_count, fund_count_prev, fund_change, fund_change_pct, total_value, fund_flow, share_change, share_change_pct, total_shares_curr, total_shares_prev, new_buys_count, closed_count, increased_count, decreased_count, unchanged_count, new_buys_shares, increased_shares, decreased_shares, unchanged_shares, closed_shares, increased_delta_shares, decreased_delta_shares, increased_pct_shares, decreased_pct_shares, market_cap, close_price, shares_outstanding, first_traded, fund_flow_mcap_pct. Deprecated alias: 'sort'.; example: filed_at"), sort_order: str = typer.Option(None, '--sort_order', help="Sort direction: 'asc' or 'desc'. Deprecated alias: 'order'.; example: desc"), limit: str = typer.Option(None, '--limit', help='Page size (max 10000). Raised from 500 after enrichment materialization (#245).; example: 20'), offset: str = typer.Option(None, '--offset', help='Pagination offset.; example: 0'), security_group: str = typer.Option(None, '--security_group', help="Filter by security group: 'Stocks', 'ETFs', 'Preferred', 'Warrants', 'Debt', 'Other'. Comma-separated for multiple. Omit for all.; example: Stocks"), is_etf: str = typer.Option(None, '--is_etf', help="(Deprecated -- use security_group instead) 'true' = ETFs only, 'false' = Stocks only.; example: false"), sector: str = typer.Option(None, '--sector', help="Exact sector name filter (e.g. 'Technology', 'Healthcare').; example: Technology"), industry: str = typer.Option(None, '--industry', help="Exact industry name filter (e.g. 'Semiconductors').; example: Consumer Electronics"), search: str = typer.Option(None, '--search', help='Case-insensitive substring search on ticker, company name, CUSIP, sector, or industry.; example: apple'), min_market_cap: str = typer.Option(None, '--min_market_cap', help='Lower bound on market capitalization in USD.; example: 1000000000'), max_market_cap: str = typer.Option(None, '--max_market_cap', help='Upper bound on market capitalization in USD.; example: 1000000000'), min_total_value: str = typer.Option(None, '--min_total_value', help='Lower bound on total 13F market value held in USD.; example: 50000000'), max_total_value: str = typer.Option(None, '--max_total_value', help='Upper bound on total 13F market value held in USD.; example: 50000000'), min_shares_outstanding: str = typer.Option(None, '--min_shares_outstanding', help='Lower bound on shares outstanding (canonical key; legacy alias `float`).; example: 10000000'), max_shares_outstanding: str = typer.Option(None, '--max_shares_outstanding', help='Upper bound on shares outstanding (canonical key; legacy alias `float`).; example: 10000000'), min_float: str = typer.Option(None, '--min_float', help='Legacy alias of `min_shares_outstanding`.; example: 10000000'), max_float: str = typer.Option(None, '--max_float', help='Legacy alias of `max_shares_outstanding`.; example: 10000000'), min_fund_flow: str = typer.Option(None, '--min_fund_flow', help='Lower bound on net fund flow in USD.; example: 1000000'), max_fund_flow: str = typer.Option(None, '--max_fund_flow', help='Upper bound on net fund flow in USD.; example: 1000000'), min_fund_flow_mcap_pct: str = typer.Option(None, '--min_fund_flow_mcap_pct', help='Lower bound on fund flow as a fraction of market cap.; example: 0.05'), max_fund_flow_mcap_pct: str = typer.Option(None, '--max_fund_flow_mcap_pct', help='Upper bound on fund flow as a fraction of market cap.; example: 0.05'), min_fund_count: str = typer.Option(None, '--min_fund_count', help='Lower bound on number of funds holding the ticker.; example: 10'), max_fund_count: str = typer.Option(None, '--max_fund_count', help='Upper bound on number of funds holding the ticker.; example: 10'), min_fund_change: str = typer.Option(None, '--min_fund_change', help='Lower bound on quarter-over-quarter change in holder count.; example: 5'), max_fund_change: str = typer.Option(None, '--max_fund_change', help='Upper bound on quarter-over-quarter change in holder count.; example: 5'), min_fund_change_pct: str = typer.Option(None, '--min_fund_change_pct', help='Lower bound on quarter-over-quarter holder-count change as a fraction.; example: 0.10'), max_fund_change_pct: str = typer.Option(None, '--max_fund_change_pct', help='Upper bound on quarter-over-quarter holder-count change as a fraction.; example: 0.10'), min_share_change: str = typer.Option(None, '--min_share_change', help='Lower bound on quarter-over-quarter change in shares held.; example: 100000'), max_share_change: str = typer.Option(None, '--max_share_change', help='Upper bound on quarter-over-quarter change in shares held.; example: 100000'), min_share_change_pct: str = typer.Option(None, '--min_share_change_pct', help='Lower bound on quarter-over-quarter share change as a fraction.; example: 0.10'), max_share_change_pct: str = typer.Option(None, '--max_share_change_pct', help='Upper bound on quarter-over-quarter share change as a fraction.; example: 0.10'), min_first_traded: str = typer.Option(None, '--min_first_traded', help='Lower bound on first-traded date, compared lexicographically as YYYY-MM-DD.; example: 2015-01-01'), max_first_traded: str = typer.Option(None, '--max_first_traded', help='Upper bound on first-traded date, compared lexicographically as YYYY-MM-DD.; example: 2015-01-01'), include_ranges: str = typer.Option(None, '--include_ranges', help="Set to 'true' to include filter_ranges in response (min/max for each filterable column). Request this once on page load, not on every paginate/sort. Enriched column ranges (market_cap, shares_outstanding, fund_flow_mcap_pct, first_traded) are omitted for performance -- only native summary column ranges are returned."), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
87
+ url = '/api/v1/form-13f/aggregation/by-ticker'
88
+ _pp: dict[str, str] = {}
89
+ for _k, _v in _pp.items():
90
+ url = url.replace("{" + _k + "}", str(_v))
91
+ _kv: list[str] = []
92
+ _body: dict[str, str | None] = {'quarter': quarter, 'sort_by': sort_by, 'sort_order': sort_order, 'limit': limit, 'offset': offset, 'security_group': security_group, 'is_etf': is_etf, 'sector': sector, 'industry': industry, 'search': search, 'min_market_cap': min_market_cap, 'max_market_cap': max_market_cap, 'min_total_value': min_total_value, 'max_total_value': max_total_value, 'min_shares_outstanding': min_shares_outstanding, 'max_shares_outstanding': max_shares_outstanding, 'min_float': min_float, 'max_float': max_float, 'min_fund_flow': min_fund_flow, 'max_fund_flow': max_fund_flow, 'min_fund_flow_mcap_pct': min_fund_flow_mcap_pct, 'max_fund_flow_mcap_pct': max_fund_flow_mcap_pct, 'min_fund_count': min_fund_count, 'max_fund_count': max_fund_count, 'min_fund_change': min_fund_change, 'max_fund_change': max_fund_change, 'min_fund_change_pct': min_fund_change_pct, 'max_fund_change_pct': max_fund_change_pct, 'min_share_change': min_share_change, 'max_share_change': max_share_change, 'min_share_change_pct': min_share_change_pct, 'max_share_change_pct': max_share_change_pct, 'min_first_traded': min_first_traded, 'max_first_traded': max_first_traded, 'include_ranges': include_ranges}
93
+ for _name, _val in _body.items():
94
+ if _val is not None:
95
+ _kv.append(f"{_name}={_val}")
96
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
97
+
98
+ @_sub_cover_pages.command('create', help='Form 13F Cover Pages [cost 10]')
99
+ def cmd_post_api_v1_form_13f_cover_pages(ctx: typer.Context, query: str = typer.Option(None, '--query', help='Lucene-like query. Same field-scoped syntax as `/holdings` but WITHOUT the holdings-row filters (`holdings.ticker`, `holdings.cusip` not supported here — use `/holdings` for those). Common: `cik:1067983` (single fund), `formType:"13F-HR/A"` (amendments only), `filedAt:[2025-01-01 TO 2025-12-31]` (year range), `accessionNo:0001067983-24-000123` (one specific filing). Combine with `AND`/`OR`/`NOT`.'), from_: str = typer.Option(None, '--from', help='Zero-based pagination offset (NUMBER of filings to skip). Increment by `size` to walk pages. Capped at 10000 server-side.'), size: str = typer.Option(None, '--size', help='Number of filings to return per page, capped at 50 server-side. Each row is small (cover-page metadata only) so payload size stays bounded — listing 50 filings here is ~10KB; the same 50 in `/holdings` could be 5-50MB.'), sort: str = typer.Option(None, '--sort', help='Sort specification array (sec-api.io shape). Each element is `{ <field>: { order: \'asc\' | \'desc\' } }`. Common: `[{"filedAt": {"order": "desc"}}]` (most recent first; default), `[{"periodOfReport": {"order": "asc"}}]` (chronological for filing-history tabs).'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
100
+ url = '/api/v1/form-13f/cover-pages'
101
+ _pp: dict[str, str] = {}
102
+ for _k, _v in _pp.items():
103
+ url = url.replace("{" + _k + "}", str(_v))
104
+ _kv: list[str] = []
105
+ _body: dict[str, str | None] = {'query': query, 'from': from_, 'size': size, 'sort': sort}
106
+ for _name, _val in _body.items():
107
+ if _val is not None:
108
+ _kv.append(f"{_name}={_val}")
109
+ run_request(ctx.obj, 'POST', url, _kv, data=data)
110
+
111
+ @_sub_cross_filing_signals.command('get', help='Cross-Filing Convergence Signals [cost 0]')
112
+ def cmd_get_api_v1_form_13f_cross_filing_signals(ctx: typer.Context, quarter: str = typer.Option(None, '--quarter', help='Quarter end date YYYY-MM-DD (e.g. 2025-12-31). Defaults to latest available.; example: 2025Q4'), window_before: str = typer.Option(None, '--window_before', help='Days before quarter start to look for insider purchases (0-90).'), window_after: str = typer.Option(None, '--window_after', help='Days after quarter end to look for insider purchases (0-90).'), limit: str = typer.Option(None, '--limit', help='Max results (1-100).; example: 20'), min_insider_buyers: str = typer.Option(None, '--min_insider_buyers', help='Minimum distinct insider buyers to include in results.'), min_fund_flow: str = typer.Option(None, '--min_fund_flow', help='Minimum institutional fund flow ($) to include in results.'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
113
+ url = '/api/v1/form-13f/cross-filing-signals'
114
+ _pp: dict[str, str] = {}
115
+ for _k, _v in _pp.items():
116
+ url = url.replace("{" + _k + "}", str(_v))
117
+ _kv: list[str] = []
118
+ _body: dict[str, str | None] = {'quarter': quarter, 'window_before': window_before, 'window_after': window_after, 'limit': limit, 'min_insider_buyers': min_insider_buyers, 'min_fund_flow': min_fund_flow}
119
+ for _name, _val in _body.items():
120
+ if _val is not None:
121
+ _kv.append(f"{_name}={_val}")
122
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
123
+
124
+ @_sub_filers.command('get', help='Form 13F Filers [cost 10]')
125
+ def cmd_get_api_v1_form_13f_filers(ctx: typer.Context, q: str = typer.Option(None, '--q', help="Free-text search across (1) fund/registrant name, (2) CIK (exact-match), (3) signature_name (the authorized signer on the most recent 13F filing — useful for finding 'who manages this fund'), AND (4) the curated `filer_aliases` table that maps well-known fund managers to their fund CIKs (e.g. `buffett` → Berkshire Hathaway, `ackman` → Pershing Square, `dalio` → Bridgewater, `burry` → Scion Asset Management). Case-insensitive. Omit `q` to list top filers by `filing_count` (descending).; example: buffett"), limit: str = typer.Option(None, '--limit', help='Maximum filers returned (up to 10,000). The 10K cap supports client-side preloading the full filer universe for instant in-browser autocomplete (StockCircle-style UX). For typical query-driven autocomplete, 20-50 is plenty; for full-list preload use 10000.; example: 20'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
126
+ url = '/api/v1/form-13f/filers'
127
+ _pp: dict[str, str] = {}
128
+ for _k, _v in _pp.items():
129
+ url = url.replace("{" + _k + "}", str(_v))
130
+ _kv: list[str] = []
131
+ _body: dict[str, str | None] = {'q': q, 'limit': limit}
132
+ for _name, _val in _body.items():
133
+ if _val is not None:
134
+ _kv.append(f"{_name}={_val}")
135
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
136
+
137
+ @_sub_filing_stats.command('get', help='Form 13F Filing Stats [cost 0]')
138
+ def cmd_get_api_v1_form_13f_filing_stats(ctx: typer.Context, period: str = typer.Option(None, '--period', help='Quarter-end date (YYYY-MM-DD), e.g. 2025-12-31 for Q4 2025. Defaults to the last completed quarter when between filing seasons, or the current filing period during active filing season.; example: 2025Q4'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
139
+ url = '/api/v1/form-13f/filing-stats'
140
+ _pp: dict[str, str] = {}
141
+ for _k, _v in _pp.items():
142
+ url = url.replace("{" + _k + "}", str(_v))
143
+ _kv: list[str] = []
144
+ _body: dict[str, str | None] = {'period': period}
145
+ for _name, _val in _body.items():
146
+ if _val is not None:
147
+ _kv.append(f"{_name}={_val}")
148
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
149
+
150
+ @_sub_form_13f_search.command('get', help='Form 13F Search [cost 0]')
151
+ def cmd_get_api_v1_form_13f_search(ctx: typer.Context, q: str = typer.Option(None, '--q', help='Search by fund name, symbol, or CIK. Empty returns all active funds. Min 2 chars.; example: apple'), limit: str = typer.Option(None, '--limit', help='Max results (up to 5000); example: 20'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
152
+ url = '/api/v1/form-13f/search'
153
+ _pp: dict[str, str] = {}
154
+ for _k, _v in _pp.items():
155
+ url = url.replace("{" + _k + "}", str(_v))
156
+ _kv: list[str] = []
157
+ _body: dict[str, str | None] = {'q': q, 'limit': limit}
158
+ for _name, _val in _body.items():
159
+ if _val is not None:
160
+ _kv.append(f"{_name}={_val}")
161
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
162
+
163
+ @_sub_fund.command('cusip-history', help='Form 13F Fund CUSIP History [cost 0]')
164
+ def cmd_get_api_v1_form_13f_fund_cik_cusip_cusip_history(ctx: typer.Context, cik: str = typer.Argument(..., help='Fund CIK in 10-char zero-padded form (e.g. `0001067983` for Berkshire Hathaway, `0001536411` for Duquesne Family Office). Server-side normalization handles 6/7/10-char input forms (the endpoint accepts `1067983` and pads it for you).; example: 0001067983'), cusip: str = typer.Argument(..., help='Canonical 9-character CUSIP for the security (`current_cusip` column in `normalized_holdings_deduped`). Pass the `cusip` (or `canonicalCusip` for chained-CUSIP fallback) from any holdings row on [GET /api/v1/form-13f/fund/{cik}](/docs/institutional-holdings/form-13f-api/get-form-13f-fund-cik). Validated server-side: alphanumeric only, ≤ 12 chars — anything else returns 400 BAD_REQUEST.; example: 037833100'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
165
+ url = '/api/v1/form-13f/fund/{cik}/cusip/{cusip}/history'
166
+ _pp: dict[str, str] = {'cik': cik, 'cusip': cusip}
167
+ for _k, _v in _pp.items():
168
+ url = url.replace("{" + _k + "}", str(_v))
169
+ _kv: list[str] = []
170
+ _body: dict[str, str | None] = {}
171
+ for _name, _val in _body.items():
172
+ if _val is not None:
173
+ _kv.append(f"{_name}={_val}")
174
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
175
+
176
+ @_sub_fund.command('periods', help='Form 13F Fund Periods [cost 0]')
177
+ def cmd_get_api_v1_form_13f_fund_cik_periods(ctx: typer.Context, cik: str = typer.Argument(..., help='Fund CIK (e.g. 0001067983); example: 0000320193'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
178
+ url = '/api/v1/form-13f/fund/{cik}/periods'
179
+ _pp: dict[str, str] = {'cik': cik}
180
+ for _k, _v in _pp.items():
181
+ url = url.replace("{" + _k + "}", str(_v))
182
+ _kv: list[str] = []
183
+ _body: dict[str, str | None] = {}
184
+ for _name, _val in _body.items():
185
+ if _val is not None:
186
+ _kv.append(f"{_name}={_val}")
187
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
188
+
189
+ @_sub_fund.command('show', help='Form 13F Fund [cost 0]')
190
+ def cmd_get_api_v1_form_13f_fund_cik(ctx: typer.Context, cik: str = typer.Argument(..., help='Fund CIK (e.g. 0001067983); example: 0000320193'), period: str = typer.Option(None, '--period', help='Report period date YYYY-MM-DD (e.g. 2024-09-30). Defaults to latest.; example: 2025Q4'), limit: str = typer.Option(None, '--limit', help='Holdings per page (1-200). Accepted alias: `size`.; example: 50'), offset: str = typer.Option(None, '--offset', help='Pagination offset. Accepted alias: `from`.; example: 0'), include_options: str = typer.Option(None, '--include_options', help="When `true`, response includes options positions (puts/calls) alongside long-equity holdings — `holdings[].is_option=true` and `holdings[].put_call='Put'/'Call'` mark each option row. Default `false` returns long-equity-only (matches pre-Phase-68 behavior of every existing caller — hedge-fund-search inline reports, screener, basket aggregator, /13f WebSocket). Cache key is keyed by this flag, so the two response shapes cache independently. Most filers carry zero options (Berkshire, Vanguard, BlackRock); options-heavy books include Scion Asset Mgmt (CIK 1649339), Pershing Square, etc.; example: true"), include_analytics: str = typer.Option(None, '--include_analytics', help="Phase 69 Plan 05 (since v3.23.0). When `true`, each `holdings[]` row gains 11 per-position cost-basis analytics fields: `avg_buy_price`, `current_price`, `cost_basis`, `market_value`, `unrealized_pnl`, `unrealized_pnl_pct`, `holding_period_days`, `holding_period_quarters`, `first_added`, `last_share_delta`, `last_transaction_quarter` (plus `sector` when /fund/{cik}'s row doesn't already carry one). Same VWAP-of-quarterly-closes methodology as the now-deprecated `/holdings-analytics` route — see that endpoint's responseSchema entries for per-field semantics, units, and null cases. Default `false` returns the byte-identical pre-Phase-69 response shape (no new fields). Cache key includes this flag as a dimension so the four combinations (no flags / opts / analytics / both) cache independently. If `CostBasisService` raises for an exotic CIK, the analytics decoration is skipped with a warning log — the default `/fund/{cik}` response is never broken by an analytics failure. Same opt-in flag pattern as `?include_options=true` (PR #149).; example: true"), options_only: str = typer.Option(None, '--options_only', help="Phase 73-02 (since v3.24.0). When `true`, response carries ONLY options rows (`holdings[].is_option=true` with `put_call='Put'/'Call'`) — equity is excluded. Lets clients fetch the Options-tab payload for `/filer/<slug>-<cik>/` without pulling the full 2K-5K-row equity book (~50 options rows for option-heavy filers like Duquesne / Scion / Pershing Square; 0 rows for Berkshire / Vanguard / BlackRock who report no options). Overrides `include_options` when both are sent (`options_only` is a strict subset). Default `false` keeps the byte-identical pre-Phase-73-02 behavior for every existing caller. Cache key partitions independently — flipping the flag triggers a cold path on first request, with `:options-only` suffix replacing `:opts` so the three response shapes (default / `:opts` / `:options-only`) never collide. Pairs with the Phase 73-01 matview-dedup fix (PR #181, since v3.24.0) which recovered ~1.49M previously-hidden option rows for filers holding common+options on the same CUSIP — both shipped together so options-heavy filer pages render the correct count in one efficient call.; example: true"), sort_by: str = typer.Option(None, '--sort_by', help='Phase 72-02 + 72-03 (since v3.23.0). Server-side sort column applied to the FULL `holdings[]` list BEFORE pagination — i.e. cross-page sort, not per-page. Default `value` (alias `market_value`) sorts by position USD value descending. Whitelist: `ticker`, `name`, `share_type`, `value`/`market_value`, `rank`, `percent_portfolio`, `shares`, `status`, `change_percent` (the 9 columns visible on the filer page) PLUS — when `include_analytics=true` is set — 9 analytics-decorator fields: `unrealized_pnl_pct`, `unrealized_pnl`, `cost_basis`, `avg_buy_price`, `current_price`, `holding_period_days`, `holding_period_quarters`, `first_added`, `last_transaction_quarter`. Date keys (`first_added`, `last_transaction_quarter`) compare lexicographically — equivalent to chronological for ISO `YYYY-MM-DD`. Invalid `sort_by` silently falls back to `value desc` so callers never see 4xx for typos. Cache key partitions on `(sort_by, sort_order)`; the historic default (`value`, `desc`) omits the suffix so pre-Phase-72 cache entries remain byte-for-byte valid. Powers the column-header click-to-sort behavior on the `/filer/<slug>-<cik>/` page for BlackRock-class filers (5K+ holdings) where client-side sort is impossibly large.; example: unrealized_pnl_pct'), sort_order: str = typer.Option(None, '--sort_order', help='Phase 72-02 (since v3.23.0). Sort direction: `asc` (ascending) or `desc` (descending). Default `desc`. Invalid values silently fall back to `desc`. Cache key includes this dimension when combined with non-default `sort_by`.; example: asc'), status: str = typer.Option(None, '--status', help="PR #285 (since v3.37.0). Server-side filter that restricts the `holdings[]` list to a single position-classification bucket — applied AFTER `_compute_analysis` assigns each holding its `status`, BEFORE sort + pagination. Whitelist: `all` (default — no filter, byte-for-byte pre-PR behavior), `new`, `increased`, `decreased`, `unchanged`, `closed`. Unknown values silently fall through unchanged (same convention as `sort_by`). `closed` is special-cased — closed positions are absent from the current `holdings` list (they were in the prior quarter but not current), so the handler synthesizes standard-shape rows from the `closed_list` analysis bucket with `shares=0`, `value=0`, `change_percent=-100`, `status='Closed'`, and the prior values preserved as `shares_sold` / `value_sold` so the frontend can render them into the same holdings table. `meta.pagination.total` reflects the FILTERED count (so the infinite-scroll holdings table on `/filer/<slug>-<cik>/` paginates correctly across status pills); `analysis.*.total` continues to reflect the UNFILTERED per-category totals (so the pill counts stay accurate regardless of which filter is active). Cache key partitions on `status`; the default `all` omits the suffix so pre-PR cache entries remain byte-for-byte valid. Mirrors the precedent on `GET /api/v1/tickers/{ticker}/holders?status=`.; example: new"), include_activity: str = typer.Option(None, '--include_activity', help="Phase 68.3 (since v3.39.0). When `true`, the response gains a fund-level `activity` object (see the `activity` responseSchema entry) with WhaleWisdom-parity quarter-over-quarter portfolio metrics: prior-quarter market value + % change, inflow/outflow USD, net-flow %, turnover % and turnover-alt %, the new / sold-out / increased / decreased / unchanged position counts, and average holding period (in quarters) for the top-10 / top-20 / all positions by value. Computed REQUEST-TIME from the current + prior-quarter holdings the report already loads — no extra quarter fan-out — plus at most ONE indexed lookup into the `sec_13f_holdings_analytics` warm cache for the time-held averages. Fund-wide regardless of any `status=` filter (the metrics are snapshotted before `status=` narrows `holdings[]`). On a fund's first 13F filing the prior-dependent metrics are `null`. The three `time_held_*` fields are best-effort: `null` until the Phase 79 cost-basis analytics cache is warm for this `(cik, period)` (the other activity scalars are always populated). Applies to `detail=full` only — the `summary` shape omits the block. Default `false` returns the byte-identical pre-Phase-68.3 response (no `activity` key). Cache key partitions on this flag (`:activity` suffix) so activity / non-activity responses cache independently and every pre-flag entry stays valid. Same opt-in flag pattern as `include_options` / `include_analytics`.; example: true"), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
191
+ url = '/api/v1/form-13f/fund/{cik}'
192
+ _pp: dict[str, str] = {'cik': cik}
193
+ for _k, _v in _pp.items():
194
+ url = url.replace("{" + _k + "}", str(_v))
195
+ _kv: list[str] = []
196
+ _body: dict[str, str | None] = {'period': period, 'limit': limit, 'offset': offset, 'include_options': include_options, 'include_analytics': include_analytics, 'options_only': options_only, 'sort_by': sort_by, 'sort_order': sort_order, 'status': status, 'include_activity': include_activity}
197
+ for _name, _val in _body.items():
198
+ if _val is not None:
199
+ _kv.append(f"{_name}={_val}")
200
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
201
+
202
+ @_sub_fund_performance.command('get', help='Fund Performance Ranking [cost 0]')
203
+ def cmd_get_api_v1_form_13f_fund_performance(ctx: typer.Context, quarter: str = typer.Option(None, '--quarter', help='Quarter end date (YYYY-MM-DD). Defaults to latest quarter with data. Use `data.available_quarters` (returned on `offset=0`) to populate a quarter-selector dropdown without a second round-trip.; example: 2025Q4'), cik: str = typer.Option(None, '--cik', help='Single fund CIK for detail mode. Returns position-level attribution for this fund.; example: 0000320193'), q: str = typer.Option(None, '--q', help='Free-text search across the 13F filer universe — matches the SAME pool as [GET /api/v1/form-13f/filers](/docs/institutional-holdings/form-13f-api/get-form-13f-filers). Resolves against `company_name`, padded CIK, unpadded CIK, `signature_name`, and the `filer_aliases` famous-manager table (`buffett` → Berkshire, `ackman` → Pershing, `dalio` → Bridgewater). Empty matches return `data.leaderboard=[]` (NOT 404) so the frontend can keep `quarter_metadata` + `pagination` visible. Since v3.26.0 (PR #185).; example: buffett'), has_returns: str = typer.Option(None, '--has_returns', help="When `true`, restricts results to funds where `return_1y IS NOT NULL` — drops the ~75% rollup coverage gap (funds whose multi-period rollup hasn't landed yet). Default `false` keeps the full filer universe browsable. FinRadar's leaderboard UI toggles this on by default. Since v3.26.0 (PR #185).; example: true"), sort_by: str = typer.Option(None, '--sort_by', help="Sort column. Whitelist mirrors `_build_fund_perf_dict`'s emitted fields so anything the frontend renders is server-side-sortable. Allowed values: `portfolio_return_pct` (default), `portfolio_value`, `holdings_count`, `price_coverage_pct`, `return_1y`, `return_3y`, `return_5y`, `return_10y`, `return_inception`, `sp500_return_1y`, `sp500_return_3y`, `sp500_return_5y`, `sp500_return_10y`, `alpha_1y`, `alpha_3y`, `alpha_5y`, `alpha_10y`, `portfolio_turnover`, `buy_value`, `sell_value`, `company_name`. Anything else returns 400 INVALID_PARAM. Legacy alias `sort` is still accepted. Multi-period return / S&P benchmark / alpha / turnover / buy-value / sell-value / company-name added in v3.26.0 (PR #185).; example: return_1y"), sort_order: str = typer.Option(None, '--sort_order', help='Sort direction: `desc` (default) or `asc`. Legacy alias `order` is still accepted.; example: desc'), limit: str = typer.Option(None, '--limit', help='Results per page (max 100).; example: 20'), offset: str = typer.Option(None, '--offset', help='Pagination offset. `data.available_quarters` is returned ONLY on the first page (`offset=0`) — infinite-scroll continuations skip it to stay lean.; example: 0'), min_holdings: str = typer.Option(None, '--min_holdings', help='Minimum holdings count for eligibility (default 10).'), min_aum: str = typer.Option(None, '--min_aum', help='Minimum AUM in dollars for eligibility (default $100M).'), include_all: str = typer.Option(None, '--include_all', help='If true, include funds below eligibility thresholds (default false).'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
204
+ url = '/api/v1/form-13f/fund-performance'
205
+ _pp: dict[str, str] = {}
206
+ for _k, _v in _pp.items():
207
+ url = url.replace("{" + _k + "}", str(_v))
208
+ _kv: list[str] = []
209
+ _body: dict[str, str | None] = {'quarter': quarter, 'cik': cik, 'q': q, 'has_returns': has_returns, 'sort_by': sort_by, 'sort_order': sort_order, 'limit': limit, 'offset': offset, 'min_holdings': min_holdings, 'min_aum': min_aum, 'include_all': include_all}
210
+ for _name, _val in _body.items():
211
+ if _val is not None:
212
+ _kv.append(f"{_name}={_val}")
213
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
214
+
215
+ @_sub_funds.command('get', help='Form 13F Funds [cost 0]')
216
+ def cmd_get_api_v1_form_13f_funds(ctx: typer.Context, limit: str = typer.Option(None, '--limit', help='Max results (up to 10,000); example: 20'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
217
+ url = '/api/v1/form-13f/funds'
218
+ _pp: dict[str, str] = {}
219
+ for _k, _v in _pp.items():
220
+ url = url.replace("{" + _k + "}", str(_v))
221
+ _kv: list[str] = []
222
+ _body: dict[str, str | None] = {'limit': limit}
223
+ for _name, _val in _body.items():
224
+ if _val is not None:
225
+ _kv.append(f"{_name}={_val}")
226
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
227
+
228
+ @_sub_holdings.command('query', help='Form 13F Holdings [cost 10]')
229
+ def cmd_post_api_v1_form_13f_holdings(ctx: typer.Context, query: str = typer.Option(None, '--query', help='Lucene-like query string. Field-scoped: `cik:1067983` (filer CIK, with or without zero-padding), `periodOfReport:2024-09-30` (exact quarter end), `periodOfReport:[2024-01-01 TO 2024-12-31]` (range), `filedAt:[A TO B]` (filing date range), `formType:"13F-HR"`, `holdings.ticker:AAPL` (chain-aware), `holdings.cusip:037833100` (chain-aware), `accessionNo:0001067983-24-000123`. Combine with `AND`/`OR`/`NOT`. Bare-word queries are matched against issuer name.'), from_: str = typer.Option(None, '--from', help='Zero-based pagination offset (NUMBER of filings to skip — NOT a date despite the name). Increment by `size` to walk pages: `from=0` → page 1, `from=50` → page 2 (when `size=50`). Capped at 10000 server-side; for deeper backfills use date-range chunking via `periodOfReport:[A TO B]`.'), size: str = typer.Option(None, '--size', help='Number of filings (NOT individual holdings rows) to return per page, capped at 50 server-side. Each filing can carry 1-3000+ holdings rows so total payload size scales with `size × avg_holdings_per_filing`. For Berkshire-shaped books expect ~50 holdings per filing; for index-fund books (Vanguard, BlackRock) expect ~3000+.'), sort: str = typer.Option(None, '--sort', help='Sort specification array (sec-api.io shape). Each element is `{ <field>: { order: \'asc\' | \'desc\' } }`. Common: `[{"filedAt": {"order": "desc"}}]` (most recent first), `[{"periodOfReport": {"order": "asc"}}]` (chronological for time-series). Defaults to `filedAt DESC` when omitted.'), includeChanges: str = typer.Option(None, '--includeChanges', help='When `true` AND `holdings.ticker` is set in the query, adds QoQ change fields to each ticker-matching holding: `changeShares` (int), `changePercent` (float), `status` (`New`/`Increased`/`Decreased`/`Unchanged`/`Closed`). Also returns `closedPositions` array (positions present in prior quarter but absent now) and `previousPeriod` (quarter used for comparison). Uses canonical CUSIP matching via `cusip_normalized` so cross-quarter corporate actions (mergers, ticker changes, share-class collapses) are correctly tracked. ~30-50% latency overhead per call.'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
230
+ url = '/api/v1/form-13f/holdings'
231
+ _pp: dict[str, str] = {}
232
+ for _k, _v in _pp.items():
233
+ url = url.replace("{" + _k + "}", str(_v))
234
+ _kv: list[str] = []
235
+ _body: dict[str, str | None] = {'query': query, 'from': from_, 'size': size, 'sort': sort, 'includeChanges': includeChanges}
236
+ for _name, _val in _body.items():
237
+ if _val is not None:
238
+ _kv.append(f"{_name}={_val}")
239
+ run_request(ctx.obj, 'POST', url, _kv, data=data)
240
+
241
+ @_sub_holdings_analytics.command('get', help='Form 13F Fund Holdings Analytics [cost 0]')
242
+ def cmd_get_api_v1_form_13f_fund_cik_holdings_analytics(ctx: typer.Context, cik: str = typer.Argument(..., help='Fund CIK in 10-char zero-padded form (e.g. `0001067983` for Berkshire Hathaway, `0001364742` for BlackRock, `0000884144` for Vanguard, `0001037389` for Renaissance Technologies). Server-side normalization handles 6/7/10-char input forms.; example: 0001067983'), sort_by: str = typer.Option(None, '--sort_by', help="Sort key for `holdings[]`. Allowed values: `market_value`, `unrealized_pnl_pct`, `holding_period_days`, `avg_buy_price`, `cost_basis`. Returns 400 INVALID_PARAM on any other value. Use `holding_period_days` for a 'longest-held positions' view; `unrealized_pnl_pct` for a 'biggest gainers/losers' view."), order: str = typer.Option(None, '--order', help='Sort order: `desc` (default) or `asc`. Combined with `sort_by` — e.g. `sort_by=unrealized_pnl_pct&order=asc` surfaces the worst-performing positions first.'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
243
+ url = '/api/v1/form-13f/fund/{cik}/holdings-analytics'
244
+ _pp: dict[str, str] = {'cik': cik}
245
+ for _k, _v in _pp.items():
246
+ url = url.replace("{" + _k + "}", str(_v))
247
+ _kv: list[str] = []
248
+ _body: dict[str, str | None] = {'sort_by': sort_by, 'order': order}
249
+ for _name, _val in _body.items():
250
+ if _val is not None:
251
+ _kv.append(f"{_name}={_val}")
252
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
253
+
254
+ @_sub_latest.command('get', help='Form 13F Latest [cost 0]')
255
+ def cmd_get_api_v1_form_13f_latest(ctx: typer.Context, limit: str = typer.Option(None, '--limit', help='Number of filings to return (max 100); example: 20'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
256
+ url = '/api/v1/form-13f/latest'
257
+ _pp: dict[str, str] = {}
258
+ for _k, _v in _pp.items():
259
+ url = url.replace("{" + _k + "}", str(_v))
260
+ _kv: list[str] = []
261
+ _body: dict[str, str | None] = {'limit': limit}
262
+ for _name, _val in _body.items():
263
+ if _val is not None:
264
+ _kv.append(f"{_name}={_val}")
265
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
266
+
267
+ @_sub_market_rotation.command('get', help='Market Rotation Signals [cost 0]')
268
+ def cmd_get_api_v1_form_13f_market_rotation(ctx: typer.Context, quarter: str = typer.Option(None, '--quarter', help='Quarter end date YYYY-MM-DD (e.g. 2025-12-31). Defaults to latest available.; example: 2025Q4'), compare: str = typer.Option(None, '--compare', help="Set to 'prev' to include QoQ comparison with previous quarter. Shows rotation shift (e.g., 'Strong Growth -> Moderate Value').; example: false"), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
269
+ url = '/api/v1/form-13f/market-rotation'
270
+ _pp: dict[str, str] = {}
271
+ for _k, _v in _pp.items():
272
+ url = url.replace("{" + _k + "}", str(_v))
273
+ _kv: list[str] = []
274
+ _body: dict[str, str | None] = {'quarter': quarter, 'compare': compare}
275
+ for _name, _val in _body.items():
276
+ if _val is not None:
277
+ _kv.append(f"{_name}={_val}")
278
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
279
+
280
+ @_sub_search.command('get', help='Form 13F Holdings Search [cost 0]')
281
+ def cmd_get_api_v1_form_13f_holdings_search(ctx: typer.Context, q: str = typer.Option(None, '--q', help='Ticker symbol (e.g. AAPL) or CUSIP (e.g. 037833100). Min 2 chars.; example: apple'), limit: str = typer.Option(None, '--limit', help='Max fund results (up to 100); example: 20'), period: str = typer.Option(None, '--period', help='Report period filter (YYYY-MM-DD). Defaults to latest.; example: 2025Q4'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
282
+ url = '/api/v1/form-13f/holdings/search'
283
+ _pp: dict[str, str] = {}
284
+ for _k, _v in _pp.items():
285
+ url = url.replace("{" + _k + "}", str(_v))
286
+ _kv: list[str] = []
287
+ _body: dict[str, str | None] = {'q': q, 'limit': limit, 'period': period}
288
+ for _name, _val in _body.items():
289
+ if _val is not None:
290
+ _kv.append(f"{_name}={_val}")
291
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
@@ -0,0 +1,68 @@
1
+ """GENERATED by generate.py — do not edit. Run `make generate`."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+ from finradar_cli.commands.api import run_request
6
+
7
+
8
+ def build(parent: typer.Typer) -> None:
9
+ _sub_history = typer.Typer(no_args_is_help=True)
10
+ parent.add_typer(_sub_history, name='history')
11
+ _sub_invoices = typer.Typer(no_args_is_help=True)
12
+ parent.add_typer(_sub_invoices, name='invoices')
13
+ _sub_plans = typer.Typer(no_args_is_help=True)
14
+ parent.add_typer(_sub_plans, name='plans')
15
+ _sub_subscription = typer.Typer(no_args_is_help=True)
16
+ parent.add_typer(_sub_subscription, name='subscription')
17
+
18
+ @_sub_history.command('get', help='Billing History [cost 0]')
19
+ def cmd_get_api_v1_billing_history(ctx: typer.Context, limit: str = typer.Option(None, '--limit', help="Maximum transactions to return per page (capped at 100 server-side). Most callers want 20-50 for typical 'last N transactions' display; for full-history exports use cursor pagination via `offset`.; example: 20"), offset: str = typer.Option(None, '--offset', help='Zero-based pagination offset (NUMBER of transactions to skip from the top). Increment by `limit` to walk pages: `offset=0` page 1, `offset=20` page 2 (when `limit=20`).; example: 0'), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
20
+ url = '/api/v1/billing/history'
21
+ _pp: dict[str, str] = {}
22
+ for _k, _v in _pp.items():
23
+ url = url.replace("{" + _k + "}", str(_v))
24
+ _kv: list[str] = []
25
+ _body: dict[str, str | None] = {'limit': limit, 'offset': offset}
26
+ for _name, _val in _body.items():
27
+ if _val is not None:
28
+ _kv.append(f"{_name}={_val}")
29
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
30
+
31
+ @_sub_invoices.command('get', help='Billing Invoices [cost 0]')
32
+ def cmd_get_api_v1_billing_invoices(ctx: typer.Context, limit: str = typer.Option(None, '--limit', help="Max invoices to return per page. Default 10. Stripe enforces an upper bound of 100 server-side — values above are silently truncated by Stripe. For a user's full invoice history, paginate via Stripe's `starting_after` cursor (NOT exposed by this endpoint — use POST /api/v1/account/billing-portal for full history).; example: 20"), data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
33
+ url = '/api/v1/billing/invoices'
34
+ _pp: dict[str, str] = {}
35
+ for _k, _v in _pp.items():
36
+ url = url.replace("{" + _k + "}", str(_v))
37
+ _kv: list[str] = []
38
+ _body: dict[str, str | None] = {'limit': limit}
39
+ for _name, _val in _body.items():
40
+ if _val is not None:
41
+ _kv.append(f"{_name}={_val}")
42
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
43
+
44
+ @_sub_plans.command('get', help='Plans [cost 0]')
45
+ def cmd_get_api_v1_plans(ctx: typer.Context, data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
46
+ url = '/api/v1/plans/'
47
+ _pp: dict[str, str] = {}
48
+ for _k, _v in _pp.items():
49
+ url = url.replace("{" + _k + "}", str(_v))
50
+ _kv: list[str] = []
51
+ _body: dict[str, str | None] = {}
52
+ for _name, _val in _body.items():
53
+ if _val is not None:
54
+ _kv.append(f"{_name}={_val}")
55
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
56
+
57
+ @_sub_subscription.command('get', help='Billing Subscription [cost 0]')
58
+ def cmd_get_api_v1_billing_subscription(ctx: typer.Context, data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
59
+ url = '/api/v1/billing/subscription'
60
+ _pp: dict[str, str] = {}
61
+ for _k, _v in _pp.items():
62
+ url = url.replace("{" + _k + "}", str(_v))
63
+ _kv: list[str] = []
64
+ _body: dict[str, str | None] = {}
65
+ for _name, _val in _body.items():
66
+ if _val is not None:
67
+ _kv.append(f"{_name}={_val}")
68
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
@@ -0,0 +1,38 @@
1
+ """GENERATED by generate.py — do not edit. Run `make generate`."""
2
+ from __future__ import annotations
3
+
4
+ import typer
5
+ from finradar_cli.commands.api import run_request
6
+
7
+
8
+ def build(parent: typer.Typer) -> None:
9
+ _sub_facts = typer.Typer(no_args_is_help=True)
10
+ parent.add_typer(_sub_facts, name='facts')
11
+ _sub_stats = typer.Typer(no_args_is_help=True)
12
+ parent.add_typer(_sub_stats, name='stats')
13
+
14
+ @_sub_facts.command('get', help='Facts [cost 10]')
15
+ def cmd_get_api_v1_facts(ctx: typer.Context, data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
16
+ url = '/api/v1/facts'
17
+ _pp: dict[str, str] = {}
18
+ for _k, _v in _pp.items():
19
+ url = url.replace("{" + _k + "}", str(_v))
20
+ _kv: list[str] = []
21
+ _body: dict[str, str | None] = {}
22
+ for _name, _val in _body.items():
23
+ if _val is not None:
24
+ _kv.append(f"{_name}={_val}")
25
+ run_request(ctx.obj, 'GET', url, _kv, data=data)
26
+
27
+ @_sub_stats.command('get', help='Stats [cost 0]')
28
+ def cmd_get_api_v1_stats(ctx: typer.Context, data: str = typer.Option(None, "--data", help="JSON body or @file.json")):
29
+ url = '/api/v1/stats'
30
+ _pp: dict[str, str] = {}
31
+ for _k, _v in _pp.items():
32
+ url = url.replace("{" + _k + "}", str(_v))
33
+ _kv: list[str] = []
34
+ _body: dict[str, str | None] = {}
35
+ for _name, _val in _body.items():
36
+ if _val is not None:
37
+ _kv.append(f"{_name}={_val}")
38
+ run_request(ctx.obj, 'GET', url, _kv, data=data)