@bloomai/cli 0.1.4 → 0.1.6

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 (3) hide show
  1. package/README.md +76 -22
  2. package/dist/index.js +756 -99
  3. package/package.json +5 -3
package/README.md CHANGED
@@ -34,37 +34,70 @@ jq '.data[] | select(.revenue_growth > 0.1)' /tmp/bloom/aapl-revenue.json
34
34
 
35
35
  ## Commands
36
36
 
37
+ ### Core Research
37
38
  | Command | What it does | Example |
38
39
  |---------|-------------|---------|
39
40
  | `info` | Company fundamentals, ratings, peers | `bloom info AAPL MSFT` |
40
41
  | `news` | Recent news headlines | `bloom news AAPL --limit 5` |
41
42
  | `price` | Historical price data | `bloom price AAPL --timeframes 1d,1m,1y` |
42
43
  | `financials` | Revenue, margins, cash flow | `bloom financials AAPL --type revenue` |
43
- | `sentiment` | Market mood (AAII, Fear&Greed, VIX) | `bloom sentiment` |
44
- | `market` | Market map, sectors, movers | `bloom market --type top_movers` |
45
- | `collections` | Curated stock lists by theme | `bloom collections --section VALUE` |
46
- | `screen` | Filter stocks by metrics | `bloom screen "market_cap > 100B" "pe_ratio < 25"` |
47
44
  | `earnings` | Earnings history & estimates | `bloom earnings AAPL` |
45
+ | `earnings-calendar` | Upcoming earnings dates | `bloom earnings-calendar --days 7` |
48
46
  | `technicals` | RSI, MAs, patterns, options flow | `bloom technicals AAPL` |
49
47
  | `ma` | Raw moving average data (SMA/EMA) | `bloom ma AAPL --sma 50,100,200 --ema 20` |
50
- | `options-history` | Options sentiment over time | `bloom options-history AAPL --days 30` |
48
+ | `options-history` | Options sentiment evolution over time | `bloom options-history AAPL` |
49
+ | `transcript` | Search earnings call transcripts | `bloom transcript AAPL "AI" "services"` |
50
+ | `compare` | Side-by-side stock comparison | `bloom compare AAPL MSFT GOOG` |
51
+
52
+ ### Valuation & Risk
53
+ | Command | What it does | Example |
54
+ |---------|-------------|---------|
55
+ | `dcf` | Discounted cash flow valuation | `bloom dcf AAPL` |
56
+ | `risk` | Risk factors from SEC 10-K filings | `bloom risk AAPL` |
57
+ | `ratings` | Analyst grades & price targets | `bloom ratings AAPL` |
58
+ | `short-interest` | Short interest history | `bloom short-interest AAPL` |
59
+ | `holders` | Institutional & mutual fund holders | `bloom holders AAPL` |
60
+ | `insider` | SEC Form 4 insider trading | `bloom insider AAPL` |
61
+
62
+ ### Portfolio & Strategy
63
+ | Command | What it does | Example |
64
+ |---------|-------------|---------|
51
65
  | `portfolio` | Portfolio risk analysis | `bloom portfolio '{"AAPL":0.5,"MSFT":0.5}'` |
66
+ | `backtest` | Portfolio backtest against benchmark | `bloom backtest --symbols AAPL,MSFT --allocations 0.5,0.5` |
67
+ | `correlation` | Correlation matrix for symbols | `bloom correlation AAPL MSFT GOOG` |
68
+ | `drawdown` | Portfolio drawdown analysis | `bloom drawdown --symbols AAPL,MSFT --allocations 0.5,0.5` |
69
+ | `position-size` | Kelly criterion position sizing | `bloom position-size --bull 0.6 --bear -0.15 --conviction 8 --value 100000` |
52
70
  | `trades` | AI trade evaluation | `bloom trades "Buy AAPL: strong AI catalysts"` |
53
71
  | `catalysts` | Forward-looking value drivers | `bloom catalysts AAPL MSFT` |
54
- | `position-size` | Kelly criterion sizing | `bloom position-size --bull 0.6 --bear -0.15 --conviction 8 --value 100000` |
55
- | `transcript` | Search earnings call transcripts | `bloom transcript AAPL "AI" "services"` |
56
- | `ai-trades` | Recent AI model trade decisions (InvestingArena) | `bloom ai-trades --model claude --limit 5` |
72
+
73
+ ### Market & Macro
74
+ | Command | What it does | Example |
75
+ |---------|-------------|---------|
76
+ | `sentiment` | Market mood (AAII, Fear&Greed, VIX) | `bloom sentiment` |
77
+ | `market` | Market map, sectors, movers | `bloom market --type top_movers` |
78
+ | `macro` | GDP, CPI, unemployment, fed funds, treasury yields | `bloom macro --indicators GDP,CPI` |
79
+ | `sectors` | Sector performance across time periods | `bloom sectors` |
80
+ | `screen` | Filter stocks by metrics | `bloom screen "market_cap > 100B" "pe_ratio < 25"` |
81
+ | `collections` | Curated stock lists by theme | `bloom collections --section VALUE` |
82
+
83
+ ### Corporate Events
84
+ | Command | What it does | Example |
85
+ |---------|-------------|---------|
86
+ | `dividends` | Dividend history | `bloom dividends AAPL` |
87
+ | `splits` | Stock split history | `bloom splits AAPL` |
88
+ | `ipo` | Upcoming and recent IPOs | `bloom ipo --from 2026-05-01` |
89
+
90
+ ### AI Arena
91
+ | Command | What it does | Example |
92
+ |---------|-------------|---------|
93
+ | `ai-trades` | Recent AI model trade decisions | `bloom ai-trades --model claude --limit 5` |
57
94
  | `ai-portfolio` | AI model portfolio positions & performance | `bloom ai-portfolio --model openai` |
58
- | `search` | Web search via Serper (Google) | `bloom search "nvidia earnings Q1 2026"` |
59
- | `scrape` | Scrape a web page via Firecrawl | `bloom scrape https://example.com` |
60
95
 
61
96
  ## Environment Variables
62
97
 
63
98
  | Variable | Required for | Description |
64
99
  |----------|-------------|-------------|
65
- | `BLOOM_API_KEY` | All financial commands | Bloom API key (`bloom auth login`) |
66
- | `SERPER_API_KEY` | `bloom search` | Serper.dev API key for web search |
67
- | `FIRECRAWL_API_KEY` | `bloom scrape` | Firecrawl API key for web scraping |
100
+ | `BLOOM_API_KEY` | All commands | Bloom API key (`bloom auth login`) |
68
101
 
69
102
  ## Global Flags
70
103
 
@@ -164,23 +197,44 @@ bloom ai-portfolio --model gemini
164
197
  jq '[.[] | {model: .ai_model, return_pct, account_value}]' /tmp/bloom/ai-portfolio.json
165
198
  ```
166
199
 
167
- ### Web Research (Search + Scrape)
200
+ ### Valuation & Risk Research
201
+ ```bash
202
+ # DCF valuation
203
+ bloom dcf AAPL -o /tmp/bloom/aapl-dcf.json
204
+
205
+ # Risk factors from 10-K filing
206
+ bloom risk AAPL -o /tmp/bloom/aapl-risks.json
207
+
208
+ # Analyst ratings and price targets
209
+ bloom ratings AAPL
210
+
211
+ # Who owns this stock?
212
+ bloom holders AAPL --limit 10
213
+ bloom insider AAPL
214
+
215
+ # Short interest
216
+ bloom short-interest AAPL
217
+ ```
218
+
219
+ ### Portfolio Analytics
168
220
  ```bash
169
- # Search the web
170
- bloom search "nvidia earnings Q1 2026" --limit 5 -o /tmp/bloom/search.json
221
+ # Backtest a portfolio
222
+ bloom backtest --symbols AAPL,MSFT,GOOG --allocations 0.4,0.3,0.3 -o /tmp/bloom/backtest.json
171
223
 
172
- # Search for news specifically
173
- bloom search "AAPL product launch" --type news --limit 10
224
+ # Correlation matrix
225
+ bloom correlation AAPL MSFT GOOG AMZN
174
226
 
175
- # Scrape a page for full content
176
- bloom scrape https://example.com/article -o /tmp/bloom/article.md
227
+ # Drawdown analysis
228
+ bloom drawdown --symbols AAPL,MSFT --allocations 0.5,0.5
177
229
 
178
- # Truncate long pages
179
- bloom scrape https://example.com/article --max-chars 5000
230
+ # Macro overview
231
+ bloom macro --indicators GDP,CPI,UNEMPLOYMENT
232
+ bloom sectors
180
233
  ```
181
234
 
182
235
  ## Tips
183
236
 
237
+ - **Every command has `--help` with examples.** Run `bloom <command> --help` to see usage patterns, available options, and practical examples.
184
238
  - Use `-o` to download data to files, then `jq` to extract. Saves context tokens.
185
239
  - Batch multiple tickers: `bloom info AAPL MSFT GOOG` (up to 10).
186
240
  - Use `bloom screen` first to narrow your universe, then deep-dive with other commands.
package/dist/index.js CHANGED
@@ -110,7 +110,8 @@ function registerAuthCommand(program2) {
110
110
  }
111
111
 
112
112
  // src/api.ts
113
- var MCP_URL = "https://api.getbloom.app/mcp/";
113
+ var API_BASE = "https://api.getbloom.app";
114
+ var MCP_URL = `${API_BASE}/mcp/`;
114
115
  var requestId = 0;
115
116
  async function callTool(toolName, args) {
116
117
  const apiKey = requireApiKey();
@@ -150,6 +151,27 @@ async function callTool(toolName, args) {
150
151
  return text;
151
152
  }
152
153
  }
154
+ async function callRest(path2, params) {
155
+ const apiKey = requireApiKey();
156
+ const url = new URL(path2, API_BASE);
157
+ if (params) {
158
+ for (const [k, v] of Object.entries(params)) {
159
+ url.searchParams.set(k, v);
160
+ }
161
+ }
162
+ const res = await fetch(url.toString(), {
163
+ method: "GET",
164
+ headers: {
165
+ Authorization: `Bearer ${apiKey}`,
166
+ "Content-Type": "application/json"
167
+ }
168
+ });
169
+ if (!res.ok) {
170
+ const text = await res.text().catch(() => "");
171
+ throw new Error(`API error ${res.status}: ${text || res.statusText}`);
172
+ }
173
+ return res.json();
174
+ }
153
175
 
154
176
  // src/format.ts
155
177
  import Table from "cli-table3";
@@ -165,12 +187,12 @@ function outputResult(data, opts, tableFormatter) {
165
187
  emit(JSON.stringify(data, null, 2), opts);
166
188
  return;
167
189
  }
168
- const fmt = resolveFormat(opts);
169
- if (fmt === "json") {
190
+ const fmt2 = resolveFormat(opts);
191
+ if (fmt2 === "json") {
170
192
  emit(JSON.stringify(data, null, 2), opts);
171
193
  return;
172
194
  }
173
- if (fmt === "csv") {
195
+ if (fmt2 === "csv") {
174
196
  emit(toCSV(data), opts);
175
197
  return;
176
198
  }
@@ -354,7 +376,12 @@ function formatSection(obj, indent = "") {
354
376
  function registerNewsCommand(program2) {
355
377
  addGlobalOptions(
356
378
  program2.command("news").description("Recent news from Benzinga channels").argument("<symbols...>", "Ticker symbols").option("--limit <n>", "Max articles (default: 15)", "15")
357
- ).action(async (symbols, opts) => {
379
+ ).addHelpText("after", `
380
+ Examples:
381
+ $ bloom news AAPL Latest Apple news
382
+ $ bloom news AAPL MSFT GOOGL News for multiple stocks
383
+ $ bloom news TSLA --limit 5 Last 5 Tesla articles
384
+ `).action(async (symbols, opts) => {
358
385
  const data = await callTool("get_news_for_symbols", {
359
386
  symbols: symbols.map((s) => s.toUpperCase()),
360
387
  limit: parseIntSafe(opts.limit, 15)
@@ -390,7 +417,13 @@ function formatNews(data, opts) {
390
417
  function registerPriceCommand(program2) {
391
418
  addGlobalOptions(
392
419
  program2.command("price").description("Historical price data").argument("<symbol>", "Ticker symbol").option("--timeframes <tf>", "Comma-separated: 1d,1w,1m,3m,6m,ytd,1y,2y,5y", "1d,1m,1y").option("--ohlc", "Include full OHLC data")
393
- ).action(async (symbol, opts) => {
420
+ ).addHelpText("after", `
421
+ Examples:
422
+ $ bloom price AAPL Default timeframes (1d, 1m, 1y)
423
+ $ bloom price TSLA --timeframes 1d,5y Daily and 5-year price history
424
+ $ bloom price MSFT --ohlc Include open/high/low/close
425
+ $ bloom price AAPL -f csv -o prices.csv Export to CSV file
426
+ `).action(async (symbol, opts) => {
394
427
  const data = await callTool("get_price_charts", {
395
428
  symbol: symbol.toUpperCase(),
396
429
  timeframes: opts.timeframes.split(",").map((t) => t.trim()),
@@ -459,7 +492,13 @@ var CHART_TYPES = [
459
492
  function registerFinancialsCommand(program2) {
460
493
  addGlobalOptions(
461
494
  program2.command("financials").description("Quarterly/annual financial data with YoY growth").argument("<symbol>", "Ticker symbol").option("--type <type>", "Chart type: " + CHART_TYPES.join(", "), "revenue")
462
- ).action(async (symbol, opts) => {
495
+ ).addHelpText("after", `
496
+ Examples:
497
+ $ bloom financials AAPL Revenue (default)
498
+ $ bloom financials TSLA --type operating_margin Operating margin trend
499
+ $ bloom financials MSFT --type free_cash_flow Free cash flow history
500
+ $ bloom financials NVDA --type net_earnings_growth Earnings growth rate
501
+ `).action(async (symbol, opts) => {
463
502
  const data = await callTool("get_financial_charts", {
464
503
  symbol: symbol.toUpperCase(),
465
504
  chart_type: opts.type
@@ -478,7 +517,11 @@ var SECTIONS = [
478
517
  function registerSentimentCommand(program2) {
479
518
  addGlobalOptions(
480
519
  program2.command("sentiment").description("Market mood: AAII, Fear&Greed, VIX, momentum").option("--sections <s>", "Comma-separated: " + SECTIONS.join(","))
481
- ).action(async (opts) => {
520
+ ).addHelpText("after", `
521
+ Examples:
522
+ $ bloom sentiment All sentiment indicators
523
+ $ bloom sentiment --sections aaii_sentiment,cnn_fear_greed Just AAII and Fear & Greed
524
+ `).action(async (opts) => {
482
525
  const args = {};
483
526
  if (opts.sections) {
484
527
  args.sections = opts.sections.split(",").map((s) => s.trim());
@@ -514,7 +557,14 @@ function formatSentiment(data) {
514
557
  function registerMarketCommand(program2) {
515
558
  addGlobalOptions(
516
559
  program2.command("market").description("Market performance, sectors, global").option("--type <type>", "top_movers|sector_breakdown|global_markets|major_indexes", "top_movers").option("--period <p>", "1d|1w|1m|3m|6m|ytd|1y|5y", "1d").option("--limit <n>", "Max stocks for top_movers", "24").option("--universe <u>", "all|sp500|international|crypto|community", "all")
517
- ).action(async (opts) => {
560
+ ).addHelpText("after", `
561
+ Examples:
562
+ $ bloom market Today's top movers
563
+ $ bloom market --type sector_breakdown Sector performance
564
+ $ bloom market --type global_markets Global market overview
565
+ $ bloom market --type top_movers --period 1w Weekly top movers
566
+ $ bloom market --universe sp500 --limit 10 Top 10 S&P 500 movers
567
+ `).action(async (opts) => {
518
568
  const data = await callTool("get_market_map", {
519
569
  map_type: opts.type,
520
570
  period: opts.period,
@@ -529,7 +579,13 @@ function registerMarketCommand(program2) {
529
579
  function registerCollectionsCommand(program2) {
530
580
  addGlobalOptions(
531
581
  program2.command("collections").description("Curated stock collections by theme").option("--section <s>", "GET_STARTED|VALUE|TRENDS|BEAT_UP|FAMOUS_INVESTORS|HIGH_FLYERS|SECTORS|GEO|LEADERBOARDS", "GET_STARTED").option("--limit <n>", "Max collections", "6")
532
- ).action(async (opts) => {
582
+ ).addHelpText("after", `
583
+ Examples:
584
+ $ bloom collections Default collections
585
+ $ bloom collections --section VALUE Value investing picks
586
+ $ bloom collections --section FAMOUS_INVESTORS Famous investor portfolios
587
+ $ bloom collections --section TRENDS --limit 3 Top 3 trending collections
588
+ `).action(async (opts) => {
533
589
  const data = await callTool("get_collections", {
534
590
  section: opts.section,
535
591
  limit: parseIntSafe(opts.limit, 6)
@@ -596,7 +652,11 @@ function formatScreen(data, opts) {
596
652
  function registerEarningsCommand(program2) {
597
653
  addGlobalOptions(
598
654
  program2.command("earnings").description("Earnings history, estimates, surprises").argument("<symbols...>", "Ticker symbols (up to 5)")
599
- ).action(async (symbols, opts) => {
655
+ ).addHelpText("after", `
656
+ Examples:
657
+ $ bloom earnings AAPL Apple earnings history and surprises
658
+ $ bloom earnings AAPL MSFT GOOGL Compare earnings for multiple stocks
659
+ `).action(async (symbols, opts) => {
600
660
  const data = await callTool("analyze_earnings", {
601
661
  symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5)
602
662
  });
@@ -604,12 +664,60 @@ function registerEarningsCommand(program2) {
604
664
  });
605
665
  }
606
666
 
667
+ // src/commands/earnings-calendar.ts
668
+ function registerEarningsCalendarCommand(program2) {
669
+ addGlobalOptions(
670
+ program2.command("earnings-calendar").description(
671
+ "Upcoming earnings dates (all companies, or filter by symbols)"
672
+ ).argument("[symbols...]", "Ticker symbols (omit to show all)").option("--days <n>", "Look-ahead window in days (1-90)", "28")
673
+ ).addHelpText("after", `
674
+ Examples:
675
+ $ bloom earnings-calendar AAPL MSFT GOOGL Upcoming earnings for these stocks
676
+ $ bloom earnings-calendar TSLA --days 7 Earnings within the next week
677
+ $ bloom earnings-calendar NVDA --days 90 Earnings within 90 days
678
+ `).action(
679
+ async (symbols, opts) => {
680
+ const days = parseIntSafe(opts.days, 28);
681
+ const params = { days: String(days) };
682
+ if (symbols.length > 0) {
683
+ params.symbols = symbols.map((s) => s.toUpperCase()).join(",");
684
+ }
685
+ const data = await callRest(
686
+ "/markets/earnings-calendar/",
687
+ params
688
+ );
689
+ const items = data.results ?? [];
690
+ outputResult(items, opts, (d, o) => {
691
+ const rows = d;
692
+ if (rows.length === 0) return "(no upcoming earnings)";
693
+ return makeTable(
694
+ ["Date", "Symbol", "Name", "Est. EPS", "Actual EPS"],
695
+ rows.map((r) => [
696
+ r.earnings_date ?? "\u2014",
697
+ r.symbol,
698
+ r.name,
699
+ r.eps_estimate != null ? `$${r.eps_estimate}` : "\u2014",
700
+ r.eps_actual != null ? `$${r.eps_actual}` : "\u2014"
701
+ ]),
702
+ o
703
+ );
704
+ });
705
+ }
706
+ );
707
+ }
708
+
607
709
  // src/commands/technicals.ts
608
710
  var SECTIONS2 = ["technical_indicators", "options_analysis", "insider_activity"];
609
711
  function registerTechnicalsCommand(program2) {
610
712
  addGlobalOptions(
611
713
  program2.command("technicals").description("RSI, MAs, patterns, options flow, insider trades").argument("<symbols...>", "Ticker symbols (up to 5)").option("--sections <s>", "Comma-separated: " + SECTIONS2.join(","))
612
- ).action(async (symbols, opts) => {
714
+ ).addHelpText("after", `
715
+ Examples:
716
+ $ bloom technicals AAPL All technical data
717
+ $ bloom technicals TSLA --sections technical_indicators RSI, MAs, patterns only
718
+ $ bloom technicals NVDA --sections options_analysis Options flow only
719
+ $ bloom technicals AAPL --sections insider_activity Insider trades only
720
+ `).action(async (symbols, opts) => {
613
721
  const args = {
614
722
  symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5)
615
723
  };
@@ -625,7 +733,13 @@ function registerTechnicalsCommand(program2) {
625
733
  function registerOptionsCommand(program2) {
626
734
  addGlobalOptions(
627
735
  program2.command("options-history").description("Options sentiment evolution over time").argument("<symbol>", "Ticker symbol").option("--days <n>", "Lookback days (7-90)", "30").option("--weekly", "Use weekly sampling (default)").option("--daily", "Use daily data").option("--greeks", "Include advanced Greeks analysis")
628
- ).action(
736
+ ).addHelpText("after", `
737
+ Examples:
738
+ $ bloom options-history AAPL 30-day options sentiment (default)
739
+ $ bloom options-history TSLA --days 90 90-day lookback
740
+ $ bloom options-history NVDA --daily Daily granularity
741
+ $ bloom options-history AAPL --greeks Include advanced Greeks analysis
742
+ `).action(
629
743
  async (symbol, opts) => {
630
744
  const data = await callTool("analyze_options_history", {
631
745
  symbol: symbol.toUpperCase(),
@@ -661,7 +775,11 @@ function registerPortfolioCommand(program2) {
661
775
  function registerTradesCommand(program2) {
662
776
  addGlobalOptions(
663
777
  program2.command("trades").description("AI trade evaluation").argument("<trades...>", 'Trade descriptions: "Buy AAPL 30% because..." "Sell MSFT 20% because..."')
664
- ).action(async (tradeStrs, opts) => {
778
+ ).addHelpText("after", `
779
+ Examples:
780
+ $ bloom trades "Buy AAPL 30% because of strong iPhone sales"
781
+ $ bloom trades "Sell MSFT 20% to take profits" "Buy GOOGL 15% on AI momentum"
782
+ `).action(async (tradeStrs, opts) => {
665
783
  const trades = tradeStrs.map(parseTrade);
666
784
  const data = await callTool("analyze_trades", { trades });
667
785
  outputResult(data, opts);
@@ -694,7 +812,11 @@ Format: "Buy AAPL 30% because thesis" or JSON object`
694
812
  function registerCatalystsCommand(program2) {
695
813
  addGlobalOptions(
696
814
  program2.command("catalysts").description("Forward-looking value catalysts").argument("<symbols...>", "Ticker symbols (up to 5)")
697
- ).action(async (symbols, opts) => {
815
+ ).addHelpText("after", `
816
+ Examples:
817
+ $ bloom catalysts AAPL Upcoming catalysts for Apple
818
+ $ bloom catalysts TSLA NVDA AMZN Catalysts for multiple stocks
819
+ `).action(async (symbols, opts) => {
698
820
  const data = await callTool("generate_catalysts", {
699
821
  symbols: symbols.map((s) => s.toUpperCase()).slice(0, 5)
700
822
  });
@@ -840,7 +962,12 @@ function truncate2(s, max) {
840
962
  function registerAiPortfolioCommand(program2) {
841
963
  addGlobalOptions(
842
964
  program2.command("ai-portfolio").description("Current portfolio positions held by AI models (InvestingArena live accounts)").option("--model <m>", "Filter by AI model: claude|openai|gemini")
843
- ).action(async (opts) => {
965
+ ).addHelpText("after", `
966
+ Examples:
967
+ $ bloom ai-portfolio All AI model portfolios
968
+ $ bloom ai-portfolio --model claude Claude's current positions
969
+ $ bloom ai-portfolio --model openai OpenAI's current positions
970
+ `).action(async (opts) => {
844
971
  const args = {};
845
972
  if (opts.model) args.ai_model = opts.model;
846
973
  const data = await callTool("get_ai_trade_ideas", args);
@@ -1010,111 +1137,627 @@ function registerMaCommand(program2) {
1010
1137
  );
1011
1138
  }
1012
1139
 
1013
- // src/commands/search.ts
1014
- function registerSearchCommand(program2) {
1140
+ // src/commands/backtest.ts
1141
+ function registerBacktestCommand(program2) {
1015
1142
  addGlobalOptions(
1016
- program2.command("search").description("Web search via Serper (Google)").argument("<query...>", "Search query (joined into one string)").option("--limit <n>", "Max results (default: 10)", "10").option("--type <type>", "Search type: web, news, images (default: web)", "web")
1017
- ).action(async (query, opts) => {
1018
- const apiKey = process.env.SERPER_API_KEY;
1019
- if (!apiKey) {
1020
- console.error("Error: SERPER_API_KEY environment variable is required.");
1021
- process.exit(1);
1022
- }
1023
- const q = query.join(" ");
1024
- const limit = parseIntSafe(opts.limit, 10);
1025
- const searchType = opts.type || "web";
1026
- const validTypes = /* @__PURE__ */ new Set(["web", "news", "images"]);
1027
- if (!validTypes.has(searchType)) {
1028
- console.error(`Error: Invalid search type "${searchType}". Must be one of: web, news, images`);
1029
- process.exit(1);
1030
- }
1031
- const endpoint = searchType === "web" ? "search" : searchType;
1032
- const response = await fetch(`https://google.serper.dev/${endpoint}`, {
1033
- method: "POST",
1034
- headers: {
1035
- "X-API-KEY": apiKey,
1036
- "Content-Type": "application/json"
1037
- },
1038
- body: JSON.stringify({ q, num: limit })
1143
+ program2.command("backtest").description("Backtest a portfolio against a benchmark").requiredOption("--symbols <s>", "Comma-separated symbols (e.g., AAPL,MSFT)").requiredOption("--allocations <a>", "Comma-separated weights (e.g., 0.5,0.5)").option("--benchmark <b>", "Benchmark symbol (default: SPY)", "SPY").option("--strategy <s>", "Strategy: buy_and_hold", "buy_and_hold")
1144
+ ).action(async (opts) => {
1145
+ const data = await callRest("backtest/", {
1146
+ symbols: opts.symbols,
1147
+ allocations: opts.allocations,
1148
+ benchmark: opts.benchmark,
1149
+ strategy: opts.strategy
1039
1150
  });
1040
- if (!response.ok) {
1041
- const text = await response.text();
1042
- console.error(`Serper API error (${response.status}): ${text}`);
1043
- process.exit(1);
1044
- }
1045
- const data = await response.json();
1046
- outputResult(data, opts, (d, o) => formatSearch(d, o));
1151
+ outputResult(data, opts, formatBacktest);
1047
1152
  });
1048
1153
  }
1049
- function formatSearch(data, opts) {
1050
- if (!data || typeof data !== "object") return String(data);
1154
+ function formatBacktest(data) {
1051
1155
  const obj = data;
1052
- const organic = obj.organic || obj.news || obj.images || [];
1053
- if (!Array.isArray(organic) || organic.length === 0) return "(no results)";
1054
- const rows = organic.map((r) => [
1055
- String(r.position ?? ""),
1056
- String(r.title || "").slice(0, 60),
1057
- String(r.link || "").slice(0, 60),
1058
- String(r.snippet || r.description || "").slice(0, 80)
1156
+ if (!obj || typeof obj !== "object") return JSON.stringify(data, null, 2);
1157
+ const intervals = obj.interval_results ?? obj;
1158
+ const rows = [];
1159
+ for (const [interval, stats] of Object.entries(intervals)) {
1160
+ if (typeof stats !== "object" || !stats) continue;
1161
+ const s = stats;
1162
+ rows.push([
1163
+ interval,
1164
+ fmt(s.total_return),
1165
+ fmt(s.benchmark_return),
1166
+ fmt(s.alpha ?? s.excess_return),
1167
+ fmtNum(s.sharpe),
1168
+ fmt(s.max_drawdown)
1169
+ ]);
1170
+ }
1171
+ if (rows.length === 0) return JSON.stringify(data, null, 2);
1172
+ return makeTable(["Interval", "Return", "Benchmark", "Alpha", "Sharpe", "Max DD"], rows);
1173
+ }
1174
+ function fmt(v) {
1175
+ if (v == null || v === "") return "-";
1176
+ const n = Number(v);
1177
+ if (isNaN(n)) return String(v);
1178
+ return `${(n * 100).toFixed(2)}%`;
1179
+ }
1180
+ function fmtNum(v) {
1181
+ if (v == null || v === "") return "-";
1182
+ const n = Number(v);
1183
+ if (isNaN(n)) return String(v);
1184
+ return n.toFixed(3);
1185
+ }
1186
+
1187
+ // src/commands/correlation.ts
1188
+ function registerCorrelationCommand(program2) {
1189
+ addGlobalOptions(
1190
+ program2.command("correlation").description("Correlation matrix for a set of symbols").argument("<symbols...>", "Ticker symbols (2+)").option("--start <date>", "Start date (YYYY-MM-DD)").option("--end <date>", "End date (YYYY-MM-DD)")
1191
+ ).addHelpText("after", `
1192
+ Examples:
1193
+ $ bloom correlation AAPL MSFT GOOGL Correlation matrix
1194
+ $ bloom correlation AAPL TSLA --start 2025-01-01 Since Jan 2025
1195
+ $ bloom correlation SPY QQQ IWM --start 2024-01-01 --end 2024-12-31 Full year
1196
+ `).action(async (symbols, opts) => {
1197
+ const params = {
1198
+ symbols: symbols.map((s) => s.toUpperCase()).join(",")
1199
+ };
1200
+ if (opts.start) params.start_date = opts.start;
1201
+ if (opts.end) params.end_date = opts.end;
1202
+ const data = await callRest("correlation/", params);
1203
+ outputResult(data, opts, formatCorrelation);
1204
+ });
1205
+ }
1206
+ function formatCorrelation(data) {
1207
+ const obj = data;
1208
+ const matrix = obj?.correlation;
1209
+ if (!matrix) return JSON.stringify(data, null, 2);
1210
+ const symbols = Object.keys(matrix);
1211
+ const rows = symbols.map((s) => [s, ...symbols.map((s2) => {
1212
+ const v = matrix[s]?.[s2];
1213
+ return v != null ? v.toFixed(3) : "-";
1214
+ })]);
1215
+ return makeTable(["", ...symbols], rows);
1216
+ }
1217
+
1218
+ // src/commands/risk.ts
1219
+ function registerRiskCommand(program2) {
1220
+ addGlobalOptions(
1221
+ program2.command("risk").description("Risk factors for a stock").argument("<symbol>", "Ticker symbol")
1222
+ ).addHelpText("after", `
1223
+ Examples:
1224
+ $ bloom risk AAPL Risk factors for Apple
1225
+ $ bloom risk TSLA Risk factors for Tesla
1226
+ `).action(async (symbol, opts) => {
1227
+ const data = await callRest(`investments/${symbol.toUpperCase()}/risk_factors/`);
1228
+ outputResult(data, opts, formatRisk);
1229
+ });
1230
+ }
1231
+ function formatRisk(data) {
1232
+ if (!Array.isArray(data)) return JSON.stringify(data, null, 2);
1233
+ if (data.length === 0) return "(no risk factors found)";
1234
+ const rows = data.map((r) => [
1235
+ String(r.category ?? r.risk_type ?? "-"),
1236
+ truncate3(String(r.description ?? r.text ?? "-"), 80)
1059
1237
  ]);
1060
- return makeTable(["#", "Title", "URL", "Snippet"], rows, opts);
1238
+ return makeTable(["Category", "Description"], rows);
1239
+ }
1240
+ function truncate3(s, max) {
1241
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1061
1242
  }
1062
1243
 
1063
- // src/commands/scrape.ts
1064
- function registerScrapeCommand(program2) {
1244
+ // src/commands/drawdown.ts
1245
+ function registerDrawdownCommand(program2) {
1065
1246
  addGlobalOptions(
1066
- program2.command("scrape").description("Scrape a web page via Firecrawl").argument("<url>", "URL to scrape").option("--max-chars <n>", "Truncate output to N characters")
1067
- ).action(async (url, opts) => {
1068
- if (opts.maxChars !== void 0 && isNaN(parseInt(opts.maxChars, 10))) {
1069
- console.error("Error: --max-chars must be a number");
1070
- process.exit(1);
1247
+ program2.command("drawdown").description("Portfolio drawdown analysis").requiredOption("--symbols <s>", "Comma-separated symbols (e.g., AAPL,MSFT)").requiredOption("--allocations <a>", "Comma-separated weights (e.g., 0.5,0.5)")
1248
+ ).action(async (opts) => {
1249
+ const data = await callRest("portfolios/drawdown/", {
1250
+ symbols: opts.symbols,
1251
+ allocations: opts.allocations
1252
+ });
1253
+ outputResult(data, opts, formatDrawdown);
1254
+ });
1255
+ }
1256
+ function formatDrawdown(data) {
1257
+ const obj = data;
1258
+ if (!obj || typeof obj !== "object") return JSON.stringify(data, null, 2);
1259
+ const intervals = obj.interval_results ?? obj;
1260
+ const rows = [];
1261
+ for (const [interval, dd] of Object.entries(intervals)) {
1262
+ if (typeof dd !== "object" || !dd) continue;
1263
+ const d = dd;
1264
+ let troughDate = "-";
1265
+ let recoveryDate = "not recovered";
1266
+ const plotData = d.plot_data;
1267
+ if (plotData && plotData.length > 0) {
1268
+ let minDD = 0;
1269
+ let minIdx = -1;
1270
+ for (let i = 0; i < plotData.length; i++) {
1271
+ if (plotData[i].drawdown < minDD) {
1272
+ minDD = plotData[i].drawdown;
1273
+ minIdx = i;
1274
+ }
1275
+ }
1276
+ if (minIdx >= 0) {
1277
+ troughDate = plotData[minIdx].date;
1278
+ for (let i = minIdx + 1; i < plotData.length; i++) {
1279
+ if (plotData[i].drawdown >= 0) {
1280
+ recoveryDate = plotData[i].date;
1281
+ break;
1282
+ }
1283
+ }
1284
+ }
1071
1285
  }
1072
- const apiKey = process.env.FIRECRAWL_API_KEY;
1073
- if (!apiKey) {
1074
- console.error("Error: FIRECRAWL_API_KEY environment variable is required.");
1075
- process.exit(1);
1286
+ rows.push([
1287
+ interval,
1288
+ d.max_drawdown != null ? `${(Number(d.max_drawdown) * 100).toFixed(2)}%` : "-",
1289
+ troughDate,
1290
+ recoveryDate
1291
+ ]);
1292
+ }
1293
+ if (rows.length === 0) return JSON.stringify(data, null, 2);
1294
+ return makeTable(["Interval", "Max Drawdown", "Trough Date", "Recovery"], rows);
1295
+ }
1296
+
1297
+ // src/commands/macro.ts
1298
+ function registerMacroCommand(program2) {
1299
+ addGlobalOptions(
1300
+ program2.command("macro").description("Macroeconomic indicators (GDP, CPI, unemployment, fed funds, treasury, inflation)").option("--indicators <list>", "Comma-separated: GDP,CPI,UNEMPLOYMENT,FED_FUNDS,TREASURY_10Y,INFLATION")
1301
+ ).addHelpText("after", `
1302
+ Examples:
1303
+ $ bloom macro All macro indicators
1304
+ $ bloom macro --indicators GDP,CPI,UNEMPLOYMENT Specific indicators only
1305
+ $ bloom macro --indicators FED_FUNDS,TREASURY_10Y Interest rates
1306
+ `).action(async (opts) => {
1307
+ const args = {};
1308
+ if (opts.indicators) {
1309
+ args.indicators = opts.indicators;
1076
1310
  }
1077
- const response = await fetch("https://api.firecrawl.dev/v1/scrape", {
1078
- method: "POST",
1079
- headers: {
1080
- Authorization: `Bearer ${apiKey}`,
1081
- "Content-Type": "application/json"
1082
- },
1083
- body: JSON.stringify({ url, formats: ["markdown"] })
1084
- });
1085
- if (!response.ok) {
1086
- const text = await response.text();
1087
- console.error(`Firecrawl API error (${response.status}): ${text}`);
1088
- process.exit(1);
1311
+ const data = await callTool("get_macro_indicators", args);
1312
+ outputResult(data, opts, formatMacro);
1313
+ });
1314
+ }
1315
+ function formatMacro(data) {
1316
+ const obj = data;
1317
+ const rows = [];
1318
+ for (const [name, raw] of Object.entries(obj)) {
1319
+ const d = raw;
1320
+ if (d.error) {
1321
+ rows.push([name, "-", "-", "-", "-"]);
1322
+ continue;
1323
+ }
1324
+ const points = d.data;
1325
+ if (!points || points.length === 0) {
1326
+ rows.push([name, "-", "-", "-", "-"]);
1327
+ continue;
1089
1328
  }
1090
- const data = await response.json();
1091
- outputResult(data, opts, (d, o) => formatScrape(d, o, opts.maxChars));
1329
+ const latest = points[0];
1330
+ const prev = points.length > 1 ? points[1] : null;
1331
+ const latestVal = latest.value;
1332
+ const prevVal = prev?.value ?? "-";
1333
+ const change = latestVal !== "." && prevVal !== "-" && prevVal !== "." ? (Number(latestVal) - Number(prevVal)).toFixed(2) : "-";
1334
+ rows.push([name, String(d.unit ?? ""), latestVal, latest.date, change]);
1335
+ }
1336
+ return makeTable(["Indicator", "Unit", "Latest", "Date", "Change"], rows);
1337
+ }
1338
+
1339
+ // src/commands/sectors.ts
1340
+ function registerSectorsCommand(program2) {
1341
+ addGlobalOptions(
1342
+ program2.command("sectors").description("Sector performance across time periods")
1343
+ ).addHelpText("after", `
1344
+ Examples:
1345
+ $ bloom sectors Sector performance across all time periods
1346
+ $ bloom sectors -f json Output as JSON
1347
+ `).action(async (opts) => {
1348
+ const data = await callTool("get_sector_performance", {});
1349
+ outputResult(data, opts, formatSectors);
1092
1350
  });
1093
1351
  }
1094
- function formatScrape(data, _opts, maxChars) {
1095
- if (!data || typeof data !== "object") return String(data);
1352
+ var PERIOD_KEYS = [
1353
+ "Rank A: Real-Time Performance",
1354
+ "Rank B: 1 Day Performance",
1355
+ "Rank C: 5 Day Performance",
1356
+ "Rank D: 1 Month Performance",
1357
+ "Rank E: 3 Month Performance",
1358
+ "Rank F: Year-to-Date (YTD) Performance",
1359
+ "Rank G: 1 Year Performance",
1360
+ "Rank H: 3 Year Performance",
1361
+ "Rank I: 5 Year Performance",
1362
+ "Rank J: 10 Year Performance"
1363
+ ];
1364
+ var PERIOD_LABELS = [
1365
+ "Real-Time",
1366
+ "1 Day",
1367
+ "5 Day",
1368
+ "1 Month",
1369
+ "3 Month",
1370
+ "YTD",
1371
+ "1 Year",
1372
+ "3 Year",
1373
+ "5 Year",
1374
+ "10 Year"
1375
+ ];
1376
+ function formatSectors(data) {
1096
1377
  const obj = data;
1097
- const inner = obj.data || obj;
1098
- const content = String(inner.markdown || inner.text || inner.content || "(no content)");
1099
- const title = inner.metadata ? String(inner.metadata.title || "") : "";
1100
- const header = title ? `# ${title}
1378
+ const sectors = /* @__PURE__ */ new Set();
1379
+ for (const key of PERIOD_KEYS) {
1380
+ const period = obj[key];
1381
+ if (period) {
1382
+ for (const sector of Object.keys(period)) sectors.add(sector);
1383
+ }
1384
+ }
1385
+ if (sectors.size === 0) return JSON.stringify(data, null, 2);
1386
+ const sectorList = Array.from(sectors).sort();
1387
+ const rows = sectorList.map((sector) => {
1388
+ const vals = PERIOD_KEYS.map((key) => {
1389
+ const v = obj[key]?.[sector];
1390
+ return v ?? "-";
1391
+ });
1392
+ return [sector, ...vals];
1393
+ });
1394
+ return makeTable(["Sector", ...PERIOD_LABELS], rows);
1395
+ }
1396
+
1397
+ // src/commands/dcf.ts
1398
+ function registerDcfCommand(program2) {
1399
+ addGlobalOptions(
1400
+ program2.command("dcf").description("Discounted cash flow valuation").argument("<symbol>", "Ticker symbol")
1401
+ ).addHelpText("after", `
1402
+ Examples:
1403
+ $ bloom dcf AAPL Discounted cash flow valuation for Apple
1404
+ $ bloom dcf TSLA DCF valuation for Tesla
1405
+ `).action(async (symbol, opts) => {
1406
+ const sym = symbol.toUpperCase();
1407
+ const result = await callTool("get_dcf_valuation", { symbol: sym });
1408
+ outputResult(result, opts, () => formatDcf(result, sym));
1409
+ });
1410
+ }
1411
+ function formatDcf(data, symbol) {
1412
+ const rows = [];
1413
+ const obj = data;
1414
+ const dcf = obj?.dcf;
1415
+ const d = Array.isArray(dcf) ? dcf[0] : dcf;
1416
+ if (d && typeof d === "object") {
1417
+ const price = Number(d.Stock_Price ?? d.stockPrice ?? 0);
1418
+ const dcfVal = Number(d.DCF ?? d.dcf ?? 0);
1419
+ const upside = price > 0 ? ((dcfVal - price) / price * 100).toFixed(1) : "-";
1420
+ rows.push(["Current Price", `$${price.toFixed(2)}`]);
1421
+ rows.push(["DCF Value", `$${dcfVal.toFixed(2)}`]);
1422
+ rows.push(["Upside/Downside", `${upside}%`]);
1423
+ }
1424
+ if (rows.length === 0) return `No DCF data available for ${symbol}`;
1425
+ return makeTable(["Metric", "Value"], rows);
1426
+ }
1101
1427
 
1102
- ` : "";
1103
- const body = header + content;
1104
- if (maxChars) {
1105
- const limit = parseIntSafe(maxChars, body.length);
1106
- if (body.length > limit) {
1107
- return body.slice(0, limit) + "\n\n... (truncated)";
1428
+ // src/commands/holders.ts
1429
+ function registerHoldersCommand(program2) {
1430
+ addGlobalOptions(
1431
+ program2.command("holders").description("Institutional and mutual fund holders").argument("<symbol>", "Ticker symbol").option("--type <t>", "institutional or mutual-fund (default: both)").option("--limit <n>", "Max holders to show", "15")
1432
+ ).addHelpText("after", `
1433
+ Examples:
1434
+ $ bloom holders AAPL All holders (institutional + mutual fund)
1435
+ $ bloom holders TSLA --type institutional Institutional holders only
1436
+ $ bloom holders NVDA --limit 5 Top 5 holders
1437
+ `).action(async (symbol, opts) => {
1438
+ const sym = symbol.toUpperCase();
1439
+ const limit = parseIntSafe(opts.limit, 15);
1440
+ const args = { symbol: sym };
1441
+ if (opts.type) args.holder_type = opts.type;
1442
+ const data = await callTool("get_holders", args);
1443
+ outputResult(data, opts, () => formatHolders(data, limit));
1444
+ });
1445
+ }
1446
+ function formatHolders(data, limit) {
1447
+ const parts = [];
1448
+ const addSection = (title, list) => {
1449
+ const items = list;
1450
+ if (items && items.length > 0) {
1451
+ const rows = items.slice(0, limit).map((h) => [
1452
+ String(h.holder ?? "-"),
1453
+ fmtNum2(h.shares),
1454
+ h.dateReported ? String(h.dateReported).slice(0, 10) : "-",
1455
+ fmtNum2(h.change)
1456
+ ]);
1457
+ parts.push(`${title}
1458
+ ${makeTable(["Holder", "Shares", "Date", "Change"], rows)}`);
1108
1459
  }
1460
+ };
1461
+ addSection("INSTITUTIONAL HOLDERS", data.institutional);
1462
+ addSection("MUTUAL FUND HOLDERS", data.mutual_fund);
1463
+ return parts.length > 0 ? parts.join("\n\n") : "(no holder data found)";
1464
+ }
1465
+ function fmtNum2(v) {
1466
+ if (v == null) return "-";
1467
+ const n = Number(v);
1468
+ if (isNaN(n)) return String(v);
1469
+ if (Math.abs(n) >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
1470
+ if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
1471
+ if (Math.abs(n) >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1472
+ return n.toLocaleString();
1473
+ }
1474
+
1475
+ // src/commands/short-interest.ts
1476
+ function registerShortInterestCommand(program2) {
1477
+ addGlobalOptions(
1478
+ program2.command("short-interest").description("Short interest history").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Number of records", "10")
1479
+ ).addHelpText("after", `
1480
+ Examples:
1481
+ $ bloom short-interest GME Short interest history for GameStop
1482
+ $ bloom short-interest AMC --limit 20 Last 20 short interest records
1483
+ `).action(async (symbol, opts) => {
1484
+ const sym = symbol.toUpperCase();
1485
+ const limit = parseIntSafe(opts.limit, 10);
1486
+ const data = await callTool("get_short_interest", { symbol: sym, limit });
1487
+ outputResult(data, opts, formatShort);
1488
+ });
1489
+ }
1490
+ function formatShort(data) {
1491
+ if (!Array.isArray(data) || data.length === 0) return "(no short interest data)";
1492
+ const rows = data.map((r) => [
1493
+ String(r.date ?? "-"),
1494
+ fmtNum3(r.shortInterest),
1495
+ String(r.daysToCover ?? r.daysTocover ?? "-"),
1496
+ String(r.shortInterestRatio ?? "-"),
1497
+ fmtNum3(r.shortPriorMonthInterest ?? r.shortPriorMonth),
1498
+ r.changePercent != null ? `${Number(r.changePercent).toFixed(2)}%` : "-"
1499
+ ]);
1500
+ return makeTable(["Date", "Short Interest", "Days to Cover", "Short Ratio", "Prior Month", "Change %"], rows);
1501
+ }
1502
+ function fmtNum3(v) {
1503
+ if (v == null) return "-";
1504
+ const n = Number(v);
1505
+ if (isNaN(n)) return String(v);
1506
+ if (Math.abs(n) >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
1507
+ if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
1508
+ if (Math.abs(n) >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1509
+ return n.toLocaleString();
1510
+ }
1511
+
1512
+ // src/commands/insider.ts
1513
+ function registerInsiderCommand(program2) {
1514
+ addGlobalOptions(
1515
+ program2.command("insider").description("Insider trading activity (SEC Form 4)").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Max transactions to show", "20")
1516
+ ).addHelpText("after", `
1517
+ Examples:
1518
+ $ bloom insider AAPL Recent insider trades for Apple
1519
+ $ bloom insider TSLA --limit 5 Last 5 insider transactions
1520
+ `).action(async (symbol, opts) => {
1521
+ const sym = symbol.toUpperCase();
1522
+ const limit = parseIntSafe(opts.limit, 20);
1523
+ const data = await callTool("get_insider_trading", { symbol: sym, limit });
1524
+ outputResult(data, opts, formatInsider);
1525
+ });
1526
+ }
1527
+ function formatInsider(data) {
1528
+ if (!Array.isArray(data) || data.length === 0) return "(no insider trading data)";
1529
+ const rows = data.map((t) => {
1530
+ const val = t.securitiesTransacted && t.price ? `$${(Number(t.securitiesTransacted) * Number(t.price)).toLocaleString()}` : "-";
1531
+ return [
1532
+ String(t.filingDate ?? t.transactionDate ?? "-").slice(0, 10),
1533
+ String(t.reportingName ?? "-"),
1534
+ String(t.typeOfOwner ?? "-"),
1535
+ String(t.transactionType ?? t.acquistionOrDisposition ?? "-"),
1536
+ fmtNum4(t.securitiesTransacted),
1537
+ t.price != null ? `$${Number(t.price).toFixed(2)}` : "-",
1538
+ val
1539
+ ];
1540
+ });
1541
+ return makeTable(["Date", "Name", "Title", "Type", "Shares", "Price", "Value"], rows);
1542
+ }
1543
+ function fmtNum4(v) {
1544
+ if (v == null) return "-";
1545
+ const n = Number(v);
1546
+ if (isNaN(n)) return String(v);
1547
+ return n.toLocaleString();
1548
+ }
1549
+
1550
+ // src/commands/dividends.ts
1551
+ function registerDividendsCommand(program2) {
1552
+ addGlobalOptions(
1553
+ program2.command("dividends").description("Dividend history for a stock").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Number of records", "20").option("--from <date>", "Start date (YYYY-MM-DD)")
1554
+ ).addHelpText("after", `
1555
+ Examples:
1556
+ $ bloom dividends KO Dividend history for Coca-Cola
1557
+ $ bloom dividends AAPL --limit 8 Last 8 dividends
1558
+ $ bloom dividends JNJ --from 2024-01-01 Dividends since Jan 2024
1559
+ `).action(async (symbol, opts) => {
1560
+ const sym = symbol.toUpperCase();
1561
+ const limit = parseIntSafe(opts.limit, 20);
1562
+ const args = { symbol: sym, limit };
1563
+ if (opts.from) args.from_date = opts.from;
1564
+ const data = await callTool("get_dividends", args);
1565
+ outputResult(data, opts, formatDividends);
1566
+ });
1567
+ }
1568
+ function formatDividends(data) {
1569
+ if (!Array.isArray(data) || data.length === 0) return "(no dividend data)";
1570
+ const rows = data.map((d) => [
1571
+ String(d.date ?? "-"),
1572
+ d.value != null ? `$${Number(d.value).toFixed(4)}` : "-",
1573
+ String(d.declarationDate ?? "-"),
1574
+ String(d.recordDate ?? "-"),
1575
+ String(d.paymentDate ?? "-"),
1576
+ String(d.period ?? "-")
1577
+ ]);
1578
+ return makeTable(["Ex-Date", "Amount", "Declaration", "Record", "Payment", "Period"], rows);
1579
+ }
1580
+
1581
+ // src/commands/splits.ts
1582
+ function registerSplitsCommand(program2) {
1583
+ addGlobalOptions(
1584
+ program2.command("splits").description("Stock split history").argument("<symbol>", "Ticker symbol").option("--from <date>", "Start date (YYYY-MM-DD)")
1585
+ ).addHelpText("after", `
1586
+ Examples:
1587
+ $ bloom splits AAPL Stock split history for Apple
1588
+ $ bloom splits TSLA --from 2020-01-01 Splits since 2020
1589
+ `).action(async (symbol, opts) => {
1590
+ const sym = symbol.toUpperCase();
1591
+ const args = { symbol: sym };
1592
+ if (opts.from) args.from_date = opts.from;
1593
+ const data = await callTool("get_splits", args);
1594
+ outputResult(data, opts, formatSplits);
1595
+ });
1596
+ }
1597
+ function formatSplits(data) {
1598
+ if (!Array.isArray(data) || data.length === 0) return "(no stock splits found)";
1599
+ const rows = data.map((s) => [
1600
+ String(s.date ?? "-"),
1601
+ String(s.split ?? "-")
1602
+ ]);
1603
+ return makeTable(["Date", "Split Ratio"], rows);
1604
+ }
1605
+
1606
+ // src/commands/ipo.ts
1607
+ function registerIpoCommand(program2) {
1608
+ addGlobalOptions(
1609
+ program2.command("ipo").description("Upcoming and recent IPOs").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)")
1610
+ ).addHelpText("after", `
1611
+ Examples:
1612
+ $ bloom ipo Recent and upcoming IPOs
1613
+ $ bloom ipo --from 2025-01-01 --to 2025-06-30 IPOs in H1 2025
1614
+ `).action(async (opts) => {
1615
+ const args = {};
1616
+ if (opts.from) args.from_date = opts.from;
1617
+ if (opts.to) args.to_date = opts.to;
1618
+ const data = await callTool("get_ipo_calendar", args);
1619
+ outputResult(data, opts, formatIpos);
1620
+ });
1621
+ }
1622
+ function formatIpos(data) {
1623
+ const ipos = Array.isArray(data) ? data : data?.ipos;
1624
+ if (!Array.isArray(ipos) || ipos.length === 0) return "(no IPO data found)";
1625
+ const rows = ipos.map((ipo) => [
1626
+ String(ipo.date ?? ipo.start_date ?? "-"),
1627
+ String(ipo.name ?? ipo.company ?? "-"),
1628
+ String(ipo.code ?? ipo.ticker ?? ipo.symbol ?? "-"),
1629
+ String(ipo.exchange ?? "-"),
1630
+ ipo.price_from && ipo.price_to ? `$${ipo.price_from}-$${ipo.price_to}` : ipo.price ? `$${ipo.price}` : "-",
1631
+ ipo.shares ? fmtNum5(ipo.shares) : "-"
1632
+ ]);
1633
+ return makeTable(["Date", "Company", "Symbol", "Exchange", "Price Range", "Shares"], rows);
1634
+ }
1635
+ function fmtNum5(v) {
1636
+ if (v == null) return "-";
1637
+ const n = Number(v);
1638
+ if (isNaN(n)) return String(v);
1639
+ if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1640
+ return n.toLocaleString();
1641
+ }
1642
+
1643
+ // src/commands/compare.ts
1644
+ function registerCompareCommand(program2) {
1645
+ addGlobalOptions(
1646
+ program2.command("compare").description("Side-by-side comparison of stocks").argument("<symbols...>", "Ticker symbols (2-10)")
1647
+ ).addHelpText("after", `
1648
+ Examples:
1649
+ $ bloom compare AAPL MSFT Side-by-side comparison
1650
+ $ bloom compare AAPL MSFT GOOGL AMZN Compare big tech stocks
1651
+ $ bloom compare TSLA RIVN LCID Compare EV manufacturers
1652
+ `).action(async (symbols, opts) => {
1653
+ const syms = symbols.map((s) => s.toUpperCase()).slice(0, 10);
1654
+ const data = await callTool("get_symbols_metadata", { symbols: syms });
1655
+ outputResult(data, opts, (d) => formatComparison(d, syms));
1656
+ });
1657
+ }
1658
+ function formatComparison(data, symbols) {
1659
+ const obj = data;
1660
+ const list = obj?.symbols ?? (Array.isArray(data) ? data : null);
1661
+ if (!Array.isArray(list) || list.length === 0) return JSON.stringify(data, null, 2);
1662
+ const metrics = [
1663
+ ["Price", "latest_price", "$"],
1664
+ ["Market Cap", "market_cap", "cap"],
1665
+ ["P/E", "pe_ratio", ""],
1666
+ ["EPS", "eps", "$"],
1667
+ ["Dividend Yield", "dividend_yield", "%"],
1668
+ ["52W High", "week_52_high", "$"],
1669
+ ["52W Low", "week_52_low", "$"],
1670
+ ["Beta", "beta", ""],
1671
+ ["Sector", "sector", ""]
1672
+ ];
1673
+ const bySymbol = {};
1674
+ for (const item of list) {
1675
+ const i = item;
1676
+ const meta = i.basic_metadata ?? i;
1677
+ const sym = String(meta.symbol ?? i.symbol ?? "");
1678
+ bySymbol[sym] = meta;
1679
+ }
1680
+ const headers = ["Metric", ...symbols];
1681
+ const rows = metrics.map(([label, key, fmt2]) => {
1682
+ const vals = symbols.map((s) => {
1683
+ const meta = bySymbol[s];
1684
+ if (!meta) return "-";
1685
+ const v = meta[key];
1686
+ if (v == null) return "-";
1687
+ if (fmt2 === "$") return `$${Number(v).toFixed(2)}`;
1688
+ if (fmt2 === "%") return `${(Number(v) * 100).toFixed(2)}%`;
1689
+ if (fmt2 === "cap") return fmtCap(v);
1690
+ return String(v);
1691
+ });
1692
+ return [label, ...vals];
1693
+ });
1694
+ return makeTable(headers, rows);
1695
+ }
1696
+ function fmtCap(v) {
1697
+ const n = Number(v);
1698
+ if (isNaN(n)) return String(v);
1699
+ if (n >= 1e12) return `$${(n / 1e12).toFixed(2)}T`;
1700
+ if (n >= 1e9) return `$${(n / 1e9).toFixed(2)}B`;
1701
+ if (n >= 1e6) return `$${(n / 1e6).toFixed(0)}M`;
1702
+ return `$${n.toLocaleString()}`;
1703
+ }
1704
+
1705
+ // src/commands/ratings.ts
1706
+ function registerRatingsCommand(program2) {
1707
+ addGlobalOptions(
1708
+ program2.command("ratings").description("Analyst ratings, grades, and price targets").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Max records to show", "20")
1709
+ ).addHelpText("after", `
1710
+ Examples:
1711
+ $ bloom ratings AAPL Analyst ratings and price targets
1712
+ $ bloom ratings NVDA --limit 5 Last 5 analyst actions
1713
+ `).action(async (symbol, opts) => {
1714
+ const sym = symbol.toUpperCase();
1715
+ const limit = parseIntSafe(opts.limit, 20);
1716
+ const result = await callTool("get_analyst_ratings", { symbol: sym, limit });
1717
+ outputResult(result, opts, () => {
1718
+ const obj = result;
1719
+ return formatRatings(obj.grades, obj.estimates);
1720
+ });
1721
+ });
1722
+ }
1723
+ function formatRatings(grades, estimates) {
1724
+ const parts = [];
1725
+ if (Array.isArray(grades) && grades.length > 0) {
1726
+ const rows = grades.map((g) => [
1727
+ String(g.date ?? "-").slice(0, 10),
1728
+ String(g.gradingCompany ?? "-"),
1729
+ String(g.previousGrade ?? "-"),
1730
+ String(g.newGrade ?? "-"),
1731
+ String(g.action ?? "-")
1732
+ ]);
1733
+ parts.push("ANALYST GRADES\n" + makeTable(["Date", "Firm", "Previous", "New", "Action"], rows));
1109
1734
  }
1110
- return body;
1735
+ if (Array.isArray(estimates) && estimates.length > 0) {
1736
+ const rows = estimates.map((e) => [
1737
+ String(e.date ?? "-").slice(0, 10),
1738
+ e.estimatedRevenueAvg != null ? fmtNum6(e.estimatedRevenueAvg) : "-",
1739
+ e.estimatedEpsAvg != null ? `$${Number(e.estimatedEpsAvg).toFixed(2)}` : "-",
1740
+ String(e.numberAnalystEstimatedRevenue ?? e.numberAnalystsEstimatedRevenue ?? "-")
1741
+ ]);
1742
+ parts.push("ANALYST ESTIMATES\n" + makeTable(["Period", "Est. Revenue", "Est. EPS", "# Analysts"], rows));
1743
+ }
1744
+ return parts.length > 0 ? parts.join("\n\n") : "(no ratings data found)";
1745
+ }
1746
+ function fmtNum6(v) {
1747
+ if (v == null) return "-";
1748
+ const n = Number(v);
1749
+ if (isNaN(n)) return String(v);
1750
+ if (Math.abs(n) >= 1e12) return `$${(n / 1e12).toFixed(2)}T`;
1751
+ if (Math.abs(n) >= 1e9) return `$${(n / 1e9).toFixed(2)}B`;
1752
+ if (Math.abs(n) >= 1e6) return `$${(n / 1e6).toFixed(0)}M`;
1753
+ return `$${n.toLocaleString()}`;
1111
1754
  }
1112
1755
 
1113
1756
  // src/index.ts
1114
1757
  var program = new Command();
1115
1758
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
1116
1759
  var pkgPath = path.join(__dirname, "..", "package.json");
1117
- var compiledVersion = "0.1.4".length > 0 ? "0.1.4" : void 0;
1760
+ var compiledVersion = "0.1.6".length > 0 ? "0.1.6" : void 0;
1118
1761
  var envVersion = process.env.BLOOM_CLI_VERSION ?? process.env.npm_package_version;
1119
1762
  var fallbackVersion = compiledVersion ?? "unknown";
1120
1763
  var cliVersion = envVersion ?? fallbackVersion;
@@ -1139,6 +1782,7 @@ registerMarketCommand(program);
1139
1782
  registerCollectionsCommand(program);
1140
1783
  registerScreenCommand(program);
1141
1784
  registerEarningsCommand(program);
1785
+ registerEarningsCalendarCommand(program);
1142
1786
  registerTechnicalsCommand(program);
1143
1787
  registerOptionsCommand(program);
1144
1788
  registerPortfolioCommand(program);
@@ -1149,8 +1793,21 @@ registerTranscriptCommand(program);
1149
1793
  registerAiTradesCommand(program);
1150
1794
  registerAiPortfolioCommand(program);
1151
1795
  registerMaCommand(program);
1152
- registerSearchCommand(program);
1153
- registerScrapeCommand(program);
1796
+ registerBacktestCommand(program);
1797
+ registerCorrelationCommand(program);
1798
+ registerRiskCommand(program);
1799
+ registerDrawdownCommand(program);
1800
+ registerMacroCommand(program);
1801
+ registerSectorsCommand(program);
1802
+ registerDcfCommand(program);
1803
+ registerHoldersCommand(program);
1804
+ registerShortInterestCommand(program);
1805
+ registerInsiderCommand(program);
1806
+ registerDividendsCommand(program);
1807
+ registerSplitsCommand(program);
1808
+ registerIpoCommand(program);
1809
+ registerCompareCommand(program);
1810
+ registerRatingsCommand(program);
1154
1811
  program.parseAsync().catch((err) => {
1155
1812
  console.error("Error:", err.message || err);
1156
1813
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloomai/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CLI tool for Bloom financial data - download market data to files for analysis",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "tsup",
16
- "dev": "tsup --watch"
16
+ "dev": "tsup --watch",
17
+ "test": "vitest run"
17
18
  },
18
19
  "keywords": [
19
20
  "finance",
@@ -30,6 +31,7 @@
30
31
  "devDependencies": {
31
32
  "@types/node": "^22.13.0",
32
33
  "tsup": "^8.4.0",
33
- "typescript": "^5.7.0"
34
+ "typescript": "^5.7.0",
35
+ "vitest": "^4.1.5"
34
36
  }
35
37
  }