@bloomai/cli 0.1.4 → 0.1.5

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 +75 -22
  2. package/dist/index.js +616 -87
  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,19 +197,39 @@ 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
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
  }
@@ -604,6 +626,40 @@ function registerEarningsCommand(program2) {
604
626
  });
605
627
  }
606
628
 
629
+ // src/commands/earnings-calendar.ts
630
+ function registerEarningsCalendarCommand(program2) {
631
+ addGlobalOptions(
632
+ program2.command("earnings-calendar").description(
633
+ "Upcoming earnings dates for symbols (default 28 days)"
634
+ ).argument("<symbols...>", "Ticker symbols").option("--days <n>", "Look-ahead window in days (1-90)", "28")
635
+ ).action(
636
+ async (symbols, opts) => {
637
+ const days = parseIntSafe(opts.days, 28);
638
+ const syms = symbols.map((s) => s.toUpperCase()).join(",");
639
+ const data = await callRest("/markets/earnings-calendar/", {
640
+ symbols: syms,
641
+ days: String(days)
642
+ });
643
+ const items = data.results ?? [];
644
+ outputResult(items, opts, (d, o) => {
645
+ const rows = d;
646
+ if (rows.length === 0) return "(no upcoming earnings)";
647
+ return makeTable(
648
+ ["Date", "Symbol", "Name", "Est. EPS", "Actual EPS"],
649
+ rows.map((r) => [
650
+ r.earnings_date ?? "\u2014",
651
+ r.symbol,
652
+ r.name,
653
+ r.eps_estimate != null ? `$${r.eps_estimate}` : "\u2014",
654
+ r.eps_actual != null ? `$${r.eps_actual}` : "\u2014"
655
+ ]),
656
+ o
657
+ );
658
+ });
659
+ }
660
+ );
661
+ }
662
+
607
663
  // src/commands/technicals.ts
608
664
  var SECTIONS2 = ["technical_indicators", "options_analysis", "insider_activity"];
609
665
  function registerTechnicalsCommand(program2) {
@@ -1010,111 +1066,570 @@ function registerMaCommand(program2) {
1010
1066
  );
1011
1067
  }
1012
1068
 
1013
- // src/commands/search.ts
1014
- function registerSearchCommand(program2) {
1069
+ // src/commands/backtest.ts
1070
+ function registerBacktestCommand(program2) {
1015
1071
  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 })
1072
+ 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")
1073
+ ).action(async (opts) => {
1074
+ const data = await callRest("backtest/", {
1075
+ symbols: opts.symbols,
1076
+ allocations: opts.allocations,
1077
+ benchmark: opts.benchmark,
1078
+ strategy: opts.strategy
1039
1079
  });
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));
1080
+ outputResult(data, opts, formatBacktest);
1047
1081
  });
1048
1082
  }
1049
- function formatSearch(data, opts) {
1050
- if (!data || typeof data !== "object") return String(data);
1083
+ function formatBacktest(data) {
1051
1084
  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)
1085
+ if (!obj || typeof obj !== "object") return JSON.stringify(data, null, 2);
1086
+ const intervals = obj.interval_results ?? obj;
1087
+ const rows = [];
1088
+ for (const [interval, stats] of Object.entries(intervals)) {
1089
+ if (typeof stats !== "object" || !stats) continue;
1090
+ const s = stats;
1091
+ rows.push([
1092
+ interval,
1093
+ fmt(s.total_return),
1094
+ fmt(s.benchmark_return),
1095
+ fmt(s.alpha ?? s.excess_return),
1096
+ fmtNum(s.sharpe),
1097
+ fmt(s.max_drawdown)
1098
+ ]);
1099
+ }
1100
+ if (rows.length === 0) return JSON.stringify(data, null, 2);
1101
+ return makeTable(["Interval", "Return", "Benchmark", "Alpha", "Sharpe", "Max DD"], rows);
1102
+ }
1103
+ function fmt(v) {
1104
+ if (v == null || v === "") return "-";
1105
+ const n = Number(v);
1106
+ if (isNaN(n)) return String(v);
1107
+ return `${(n * 100).toFixed(2)}%`;
1108
+ }
1109
+ function fmtNum(v) {
1110
+ if (v == null || v === "") return "-";
1111
+ const n = Number(v);
1112
+ if (isNaN(n)) return String(v);
1113
+ return n.toFixed(3);
1114
+ }
1115
+
1116
+ // src/commands/correlation.ts
1117
+ function registerCorrelationCommand(program2) {
1118
+ addGlobalOptions(
1119
+ 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)")
1120
+ ).action(async (symbols, opts) => {
1121
+ const params = {
1122
+ symbols: symbols.map((s) => s.toUpperCase()).join(",")
1123
+ };
1124
+ if (opts.start) params.start_date = opts.start;
1125
+ if (opts.end) params.end_date = opts.end;
1126
+ const data = await callRest("correlation/", params);
1127
+ outputResult(data, opts, formatCorrelation);
1128
+ });
1129
+ }
1130
+ function formatCorrelation(data) {
1131
+ const obj = data;
1132
+ const matrix = obj?.correlation;
1133
+ if (!matrix) return JSON.stringify(data, null, 2);
1134
+ const symbols = Object.keys(matrix);
1135
+ const rows = symbols.map((s) => [s, ...symbols.map((s2) => {
1136
+ const v = matrix[s]?.[s2];
1137
+ return v != null ? v.toFixed(3) : "-";
1138
+ })]);
1139
+ return makeTable(["", ...symbols], rows);
1140
+ }
1141
+
1142
+ // src/commands/risk.ts
1143
+ function registerRiskCommand(program2) {
1144
+ addGlobalOptions(
1145
+ program2.command("risk").description("Risk factors for a stock").argument("<symbol>", "Ticker symbol")
1146
+ ).action(async (symbol, opts) => {
1147
+ const data = await callRest(`investments/${symbol.toUpperCase()}/risk_factors/`);
1148
+ outputResult(data, opts, formatRisk);
1149
+ });
1150
+ }
1151
+ function formatRisk(data) {
1152
+ if (!Array.isArray(data)) return JSON.stringify(data, null, 2);
1153
+ if (data.length === 0) return "(no risk factors found)";
1154
+ const rows = data.map((r) => [
1155
+ String(r.category ?? r.risk_type ?? "-"),
1156
+ truncate3(String(r.description ?? r.text ?? "-"), 80)
1059
1157
  ]);
1060
- return makeTable(["#", "Title", "URL", "Snippet"], rows, opts);
1158
+ return makeTable(["Category", "Description"], rows);
1159
+ }
1160
+ function truncate3(s, max) {
1161
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1061
1162
  }
1062
1163
 
1063
- // src/commands/scrape.ts
1064
- function registerScrapeCommand(program2) {
1164
+ // src/commands/drawdown.ts
1165
+ function registerDrawdownCommand(program2) {
1065
1166
  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);
1167
+ 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)")
1168
+ ).action(async (opts) => {
1169
+ const data = await callRest("portfolios/drawdown/", {
1170
+ symbols: opts.symbols,
1171
+ allocations: opts.allocations
1172
+ });
1173
+ outputResult(data, opts, formatDrawdown);
1174
+ });
1175
+ }
1176
+ function formatDrawdown(data) {
1177
+ const obj = data;
1178
+ if (!obj || typeof obj !== "object") return JSON.stringify(data, null, 2);
1179
+ const intervals = obj.interval_results ?? obj;
1180
+ const rows = [];
1181
+ for (const [interval, dd] of Object.entries(intervals)) {
1182
+ if (typeof dd !== "object" || !dd) continue;
1183
+ const d = dd;
1184
+ let troughDate = "-";
1185
+ let recoveryDate = "not recovered";
1186
+ const plotData = d.plot_data;
1187
+ if (plotData && plotData.length > 0) {
1188
+ let minDD = 0;
1189
+ let minIdx = -1;
1190
+ for (let i = 0; i < plotData.length; i++) {
1191
+ if (plotData[i].drawdown < minDD) {
1192
+ minDD = plotData[i].drawdown;
1193
+ minIdx = i;
1194
+ }
1195
+ }
1196
+ if (minIdx >= 0) {
1197
+ troughDate = plotData[minIdx].date;
1198
+ for (let i = minIdx + 1; i < plotData.length; i++) {
1199
+ if (plotData[i].drawdown >= 0) {
1200
+ recoveryDate = plotData[i].date;
1201
+ break;
1202
+ }
1203
+ }
1204
+ }
1071
1205
  }
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);
1206
+ rows.push([
1207
+ interval,
1208
+ d.max_drawdown != null ? `${(Number(d.max_drawdown) * 100).toFixed(2)}%` : "-",
1209
+ troughDate,
1210
+ recoveryDate
1211
+ ]);
1212
+ }
1213
+ if (rows.length === 0) return JSON.stringify(data, null, 2);
1214
+ return makeTable(["Interval", "Max Drawdown", "Trough Date", "Recovery"], rows);
1215
+ }
1216
+
1217
+ // src/commands/macro.ts
1218
+ function registerMacroCommand(program2) {
1219
+ addGlobalOptions(
1220
+ 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")
1221
+ ).action(async (opts) => {
1222
+ const args = {};
1223
+ if (opts.indicators) {
1224
+ args.indicators = opts.indicators;
1076
1225
  }
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);
1226
+ const data = await callTool("get_macro_indicators", args);
1227
+ outputResult(data, opts, formatMacro);
1228
+ });
1229
+ }
1230
+ function formatMacro(data) {
1231
+ const obj = data;
1232
+ const rows = [];
1233
+ for (const [name, raw] of Object.entries(obj)) {
1234
+ const d = raw;
1235
+ if (d.error) {
1236
+ rows.push([name, "-", "-", "-", "-"]);
1237
+ continue;
1238
+ }
1239
+ const points = d.data;
1240
+ if (!points || points.length === 0) {
1241
+ rows.push([name, "-", "-", "-", "-"]);
1242
+ continue;
1089
1243
  }
1090
- const data = await response.json();
1091
- outputResult(data, opts, (d, o) => formatScrape(d, o, opts.maxChars));
1244
+ const latest = points[0];
1245
+ const prev = points.length > 1 ? points[1] : null;
1246
+ const latestVal = latest.value;
1247
+ const prevVal = prev?.value ?? "-";
1248
+ const change = latestVal !== "." && prevVal !== "-" && prevVal !== "." ? (Number(latestVal) - Number(prevVal)).toFixed(2) : "-";
1249
+ rows.push([name, String(d.unit ?? ""), latestVal, latest.date, change]);
1250
+ }
1251
+ return makeTable(["Indicator", "Unit", "Latest", "Date", "Change"], rows);
1252
+ }
1253
+
1254
+ // src/commands/sectors.ts
1255
+ function registerSectorsCommand(program2) {
1256
+ addGlobalOptions(
1257
+ program2.command("sectors").description("Sector performance across time periods")
1258
+ ).action(async (opts) => {
1259
+ const data = await callTool("get_sector_performance", {});
1260
+ outputResult(data, opts, formatSectors);
1092
1261
  });
1093
1262
  }
1094
- function formatScrape(data, _opts, maxChars) {
1095
- if (!data || typeof data !== "object") return String(data);
1263
+ var PERIOD_KEYS = [
1264
+ "Rank A: Real-Time Performance",
1265
+ "Rank B: 1 Day Performance",
1266
+ "Rank C: 5 Day Performance",
1267
+ "Rank D: 1 Month Performance",
1268
+ "Rank E: 3 Month Performance",
1269
+ "Rank F: Year-to-Date (YTD) Performance",
1270
+ "Rank G: 1 Year Performance",
1271
+ "Rank H: 3 Year Performance",
1272
+ "Rank I: 5 Year Performance",
1273
+ "Rank J: 10 Year Performance"
1274
+ ];
1275
+ var PERIOD_LABELS = [
1276
+ "Real-Time",
1277
+ "1 Day",
1278
+ "5 Day",
1279
+ "1 Month",
1280
+ "3 Month",
1281
+ "YTD",
1282
+ "1 Year",
1283
+ "3 Year",
1284
+ "5 Year",
1285
+ "10 Year"
1286
+ ];
1287
+ function formatSectors(data) {
1096
1288
  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}
1289
+ const sectors = /* @__PURE__ */ new Set();
1290
+ for (const key of PERIOD_KEYS) {
1291
+ const period = obj[key];
1292
+ if (period) {
1293
+ for (const sector of Object.keys(period)) sectors.add(sector);
1294
+ }
1295
+ }
1296
+ if (sectors.size === 0) return JSON.stringify(data, null, 2);
1297
+ const sectorList = Array.from(sectors).sort();
1298
+ const rows = sectorList.map((sector) => {
1299
+ const vals = PERIOD_KEYS.map((key) => {
1300
+ const v = obj[key]?.[sector];
1301
+ return v ?? "-";
1302
+ });
1303
+ return [sector, ...vals];
1304
+ });
1305
+ return makeTable(["Sector", ...PERIOD_LABELS], rows);
1306
+ }
1101
1307
 
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)";
1308
+ // src/commands/dcf.ts
1309
+ function registerDcfCommand(program2) {
1310
+ addGlobalOptions(
1311
+ program2.command("dcf").description("Discounted cash flow valuation").argument("<symbol>", "Ticker symbol")
1312
+ ).action(async (symbol, opts) => {
1313
+ const sym = symbol.toUpperCase();
1314
+ const result = await callTool("get_dcf_valuation", { symbol: sym });
1315
+ outputResult(result, opts, () => formatDcf(result, sym));
1316
+ });
1317
+ }
1318
+ function formatDcf(data, symbol) {
1319
+ const rows = [];
1320
+ const obj = data;
1321
+ const dcf = obj?.dcf;
1322
+ const d = Array.isArray(dcf) ? dcf[0] : dcf;
1323
+ if (d && typeof d === "object") {
1324
+ const price = Number(d.Stock_Price ?? d.stockPrice ?? 0);
1325
+ const dcfVal = Number(d.DCF ?? d.dcf ?? 0);
1326
+ const upside = price > 0 ? ((dcfVal - price) / price * 100).toFixed(1) : "-";
1327
+ rows.push(["Current Price", `$${price.toFixed(2)}`]);
1328
+ rows.push(["DCF Value", `$${dcfVal.toFixed(2)}`]);
1329
+ rows.push(["Upside/Downside", `${upside}%`]);
1330
+ }
1331
+ if (rows.length === 0) return `No DCF data available for ${symbol}`;
1332
+ return makeTable(["Metric", "Value"], rows);
1333
+ }
1334
+
1335
+ // src/commands/holders.ts
1336
+ function registerHoldersCommand(program2) {
1337
+ addGlobalOptions(
1338
+ 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")
1339
+ ).action(async (symbol, opts) => {
1340
+ const sym = symbol.toUpperCase();
1341
+ const limit = parseIntSafe(opts.limit, 15);
1342
+ const args = { symbol: sym };
1343
+ if (opts.type) args.holder_type = opts.type;
1344
+ const data = await callTool("get_holders", args);
1345
+ outputResult(data, opts, () => formatHolders(data, limit));
1346
+ });
1347
+ }
1348
+ function formatHolders(data, limit) {
1349
+ const parts = [];
1350
+ const addSection = (title, list) => {
1351
+ const items = list;
1352
+ if (items && items.length > 0) {
1353
+ const rows = items.slice(0, limit).map((h) => [
1354
+ String(h.holder ?? "-"),
1355
+ fmtNum2(h.shares),
1356
+ h.dateReported ? String(h.dateReported).slice(0, 10) : "-",
1357
+ fmtNum2(h.change)
1358
+ ]);
1359
+ parts.push(`${title}
1360
+ ${makeTable(["Holder", "Shares", "Date", "Change"], rows)}`);
1108
1361
  }
1362
+ };
1363
+ addSection("INSTITUTIONAL HOLDERS", data.institutional);
1364
+ addSection("MUTUAL FUND HOLDERS", data.mutual_fund);
1365
+ return parts.length > 0 ? parts.join("\n\n") : "(no holder data found)";
1366
+ }
1367
+ function fmtNum2(v) {
1368
+ if (v == null) return "-";
1369
+ const n = Number(v);
1370
+ if (isNaN(n)) return String(v);
1371
+ if (Math.abs(n) >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
1372
+ if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
1373
+ if (Math.abs(n) >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1374
+ return n.toLocaleString();
1375
+ }
1376
+
1377
+ // src/commands/short-interest.ts
1378
+ function registerShortInterestCommand(program2) {
1379
+ addGlobalOptions(
1380
+ program2.command("short-interest").description("Short interest history").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Number of records", "10")
1381
+ ).action(async (symbol, opts) => {
1382
+ const sym = symbol.toUpperCase();
1383
+ const limit = parseIntSafe(opts.limit, 10);
1384
+ const data = await callTool("get_short_interest", { symbol: sym, limit });
1385
+ outputResult(data, opts, formatShort);
1386
+ });
1387
+ }
1388
+ function formatShort(data) {
1389
+ if (!Array.isArray(data) || data.length === 0) return "(no short interest data)";
1390
+ const rows = data.map((r) => [
1391
+ String(r.date ?? "-"),
1392
+ fmtNum3(r.shortInterest),
1393
+ String(r.daysToCover ?? r.daysTocover ?? "-"),
1394
+ String(r.shortInterestRatio ?? "-"),
1395
+ fmtNum3(r.shortPriorMonthInterest ?? r.shortPriorMonth),
1396
+ r.changePercent != null ? `${Number(r.changePercent).toFixed(2)}%` : "-"
1397
+ ]);
1398
+ return makeTable(["Date", "Short Interest", "Days to Cover", "Short Ratio", "Prior Month", "Change %"], rows);
1399
+ }
1400
+ function fmtNum3(v) {
1401
+ if (v == null) return "-";
1402
+ const n = Number(v);
1403
+ if (isNaN(n)) return String(v);
1404
+ if (Math.abs(n) >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
1405
+ if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
1406
+ if (Math.abs(n) >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1407
+ return n.toLocaleString();
1408
+ }
1409
+
1410
+ // src/commands/insider.ts
1411
+ function registerInsiderCommand(program2) {
1412
+ addGlobalOptions(
1413
+ program2.command("insider").description("Insider trading activity (SEC Form 4)").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Max transactions to show", "20")
1414
+ ).action(async (symbol, opts) => {
1415
+ const sym = symbol.toUpperCase();
1416
+ const limit = parseIntSafe(opts.limit, 20);
1417
+ const data = await callTool("get_insider_trading", { symbol: sym, limit });
1418
+ outputResult(data, opts, formatInsider);
1419
+ });
1420
+ }
1421
+ function formatInsider(data) {
1422
+ if (!Array.isArray(data) || data.length === 0) return "(no insider trading data)";
1423
+ const rows = data.map((t) => {
1424
+ const val = t.securitiesTransacted && t.price ? `$${(Number(t.securitiesTransacted) * Number(t.price)).toLocaleString()}` : "-";
1425
+ return [
1426
+ String(t.filingDate ?? t.transactionDate ?? "-").slice(0, 10),
1427
+ String(t.reportingName ?? "-"),
1428
+ String(t.typeOfOwner ?? "-"),
1429
+ String(t.transactionType ?? t.acquistionOrDisposition ?? "-"),
1430
+ fmtNum4(t.securitiesTransacted),
1431
+ t.price != null ? `$${Number(t.price).toFixed(2)}` : "-",
1432
+ val
1433
+ ];
1434
+ });
1435
+ return makeTable(["Date", "Name", "Title", "Type", "Shares", "Price", "Value"], rows);
1436
+ }
1437
+ function fmtNum4(v) {
1438
+ if (v == null) return "-";
1439
+ const n = Number(v);
1440
+ if (isNaN(n)) return String(v);
1441
+ return n.toLocaleString();
1442
+ }
1443
+
1444
+ // src/commands/dividends.ts
1445
+ function registerDividendsCommand(program2) {
1446
+ addGlobalOptions(
1447
+ 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)")
1448
+ ).action(async (symbol, opts) => {
1449
+ const sym = symbol.toUpperCase();
1450
+ const limit = parseIntSafe(opts.limit, 20);
1451
+ const args = { symbol: sym, limit };
1452
+ if (opts.from) args.from_date = opts.from;
1453
+ const data = await callTool("get_dividends", args);
1454
+ outputResult(data, opts, formatDividends);
1455
+ });
1456
+ }
1457
+ function formatDividends(data) {
1458
+ if (!Array.isArray(data) || data.length === 0) return "(no dividend data)";
1459
+ const rows = data.map((d) => [
1460
+ String(d.date ?? "-"),
1461
+ d.value != null ? `$${Number(d.value).toFixed(4)}` : "-",
1462
+ String(d.declarationDate ?? "-"),
1463
+ String(d.recordDate ?? "-"),
1464
+ String(d.paymentDate ?? "-"),
1465
+ String(d.period ?? "-")
1466
+ ]);
1467
+ return makeTable(["Ex-Date", "Amount", "Declaration", "Record", "Payment", "Period"], rows);
1468
+ }
1469
+
1470
+ // src/commands/splits.ts
1471
+ function registerSplitsCommand(program2) {
1472
+ addGlobalOptions(
1473
+ program2.command("splits").description("Stock split history").argument("<symbol>", "Ticker symbol").option("--from <date>", "Start date (YYYY-MM-DD)")
1474
+ ).action(async (symbol, opts) => {
1475
+ const sym = symbol.toUpperCase();
1476
+ const args = { symbol: sym };
1477
+ if (opts.from) args.from_date = opts.from;
1478
+ const data = await callTool("get_splits", args);
1479
+ outputResult(data, opts, formatSplits);
1480
+ });
1481
+ }
1482
+ function formatSplits(data) {
1483
+ if (!Array.isArray(data) || data.length === 0) return "(no stock splits found)";
1484
+ const rows = data.map((s) => [
1485
+ String(s.date ?? "-"),
1486
+ String(s.split ?? "-")
1487
+ ]);
1488
+ return makeTable(["Date", "Split Ratio"], rows);
1489
+ }
1490
+
1491
+ // src/commands/ipo.ts
1492
+ function registerIpoCommand(program2) {
1493
+ addGlobalOptions(
1494
+ program2.command("ipo").description("Upcoming and recent IPOs").option("--from <date>", "Start date (YYYY-MM-DD)").option("--to <date>", "End date (YYYY-MM-DD)")
1495
+ ).action(async (opts) => {
1496
+ const args = {};
1497
+ if (opts.from) args.from_date = opts.from;
1498
+ if (opts.to) args.to_date = opts.to;
1499
+ const data = await callTool("get_ipo_calendar", args);
1500
+ outputResult(data, opts, formatIpos);
1501
+ });
1502
+ }
1503
+ function formatIpos(data) {
1504
+ const ipos = Array.isArray(data) ? data : data?.ipos;
1505
+ if (!Array.isArray(ipos) || ipos.length === 0) return "(no IPO data found)";
1506
+ const rows = ipos.map((ipo) => [
1507
+ String(ipo.date ?? ipo.start_date ?? "-"),
1508
+ String(ipo.name ?? ipo.company ?? "-"),
1509
+ String(ipo.code ?? ipo.ticker ?? ipo.symbol ?? "-"),
1510
+ String(ipo.exchange ?? "-"),
1511
+ ipo.price_from && ipo.price_to ? `$${ipo.price_from}-$${ipo.price_to}` : ipo.price ? `$${ipo.price}` : "-",
1512
+ ipo.shares ? fmtNum5(ipo.shares) : "-"
1513
+ ]);
1514
+ return makeTable(["Date", "Company", "Symbol", "Exchange", "Price Range", "Shares"], rows);
1515
+ }
1516
+ function fmtNum5(v) {
1517
+ if (v == null) return "-";
1518
+ const n = Number(v);
1519
+ if (isNaN(n)) return String(v);
1520
+ if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1521
+ return n.toLocaleString();
1522
+ }
1523
+
1524
+ // src/commands/compare.ts
1525
+ function registerCompareCommand(program2) {
1526
+ addGlobalOptions(
1527
+ program2.command("compare").description("Side-by-side comparison of stocks").argument("<symbols...>", "Ticker symbols (2-10)")
1528
+ ).action(async (symbols, opts) => {
1529
+ const syms = symbols.map((s) => s.toUpperCase()).slice(0, 10);
1530
+ const data = await callTool("get_symbols_metadata", { symbols: syms });
1531
+ outputResult(data, opts, (d) => formatComparison(d, syms));
1532
+ });
1533
+ }
1534
+ function formatComparison(data, symbols) {
1535
+ const obj = data;
1536
+ const list = obj?.symbols ?? (Array.isArray(data) ? data : null);
1537
+ if (!Array.isArray(list) || list.length === 0) return JSON.stringify(data, null, 2);
1538
+ const metrics = [
1539
+ ["Price", "latest_price", "$"],
1540
+ ["Market Cap", "market_cap", "cap"],
1541
+ ["P/E", "pe_ratio", ""],
1542
+ ["EPS", "eps", "$"],
1543
+ ["Dividend Yield", "dividend_yield", "%"],
1544
+ ["52W High", "week_52_high", "$"],
1545
+ ["52W Low", "week_52_low", "$"],
1546
+ ["Beta", "beta", ""],
1547
+ ["Sector", "sector", ""]
1548
+ ];
1549
+ const bySymbol = {};
1550
+ for (const item of list) {
1551
+ const i = item;
1552
+ const meta = i.basic_metadata ?? i;
1553
+ const sym = String(meta.symbol ?? i.symbol ?? "");
1554
+ bySymbol[sym] = meta;
1555
+ }
1556
+ const headers = ["Metric", ...symbols];
1557
+ const rows = metrics.map(([label, key, fmt2]) => {
1558
+ const vals = symbols.map((s) => {
1559
+ const meta = bySymbol[s];
1560
+ if (!meta) return "-";
1561
+ const v = meta[key];
1562
+ if (v == null) return "-";
1563
+ if (fmt2 === "$") return `$${Number(v).toFixed(2)}`;
1564
+ if (fmt2 === "%") return `${(Number(v) * 100).toFixed(2)}%`;
1565
+ if (fmt2 === "cap") return fmtCap(v);
1566
+ return String(v);
1567
+ });
1568
+ return [label, ...vals];
1569
+ });
1570
+ return makeTable(headers, rows);
1571
+ }
1572
+ function fmtCap(v) {
1573
+ const n = Number(v);
1574
+ if (isNaN(n)) return String(v);
1575
+ if (n >= 1e12) return `$${(n / 1e12).toFixed(2)}T`;
1576
+ if (n >= 1e9) return `$${(n / 1e9).toFixed(2)}B`;
1577
+ if (n >= 1e6) return `$${(n / 1e6).toFixed(0)}M`;
1578
+ return `$${n.toLocaleString()}`;
1579
+ }
1580
+
1581
+ // src/commands/ratings.ts
1582
+ function registerRatingsCommand(program2) {
1583
+ addGlobalOptions(
1584
+ program2.command("ratings").description("Analyst ratings, grades, and price targets").argument("<symbol>", "Ticker symbol").option("--limit <n>", "Max records to show", "20")
1585
+ ).action(async (symbol, opts) => {
1586
+ const sym = symbol.toUpperCase();
1587
+ const limit = parseIntSafe(opts.limit, 20);
1588
+ const result = await callTool("get_analyst_ratings", { symbol: sym, limit });
1589
+ outputResult(result, opts, () => {
1590
+ const obj = result;
1591
+ return formatRatings(obj.grades, obj.estimates);
1592
+ });
1593
+ });
1594
+ }
1595
+ function formatRatings(grades, estimates) {
1596
+ const parts = [];
1597
+ if (Array.isArray(grades) && grades.length > 0) {
1598
+ const rows = grades.map((g) => [
1599
+ String(g.date ?? "-").slice(0, 10),
1600
+ String(g.gradingCompany ?? "-"),
1601
+ String(g.previousGrade ?? "-"),
1602
+ String(g.newGrade ?? "-"),
1603
+ String(g.action ?? "-")
1604
+ ]);
1605
+ parts.push("ANALYST GRADES\n" + makeTable(["Date", "Firm", "Previous", "New", "Action"], rows));
1109
1606
  }
1110
- return body;
1607
+ if (Array.isArray(estimates) && estimates.length > 0) {
1608
+ const rows = estimates.map((e) => [
1609
+ String(e.date ?? "-").slice(0, 10),
1610
+ e.estimatedRevenueAvg != null ? fmtNum6(e.estimatedRevenueAvg) : "-",
1611
+ e.estimatedEpsAvg != null ? `$${Number(e.estimatedEpsAvg).toFixed(2)}` : "-",
1612
+ String(e.numberAnalystEstimatedRevenue ?? e.numberAnalystsEstimatedRevenue ?? "-")
1613
+ ]);
1614
+ parts.push("ANALYST ESTIMATES\n" + makeTable(["Period", "Est. Revenue", "Est. EPS", "# Analysts"], rows));
1615
+ }
1616
+ return parts.length > 0 ? parts.join("\n\n") : "(no ratings data found)";
1617
+ }
1618
+ function fmtNum6(v) {
1619
+ if (v == null) return "-";
1620
+ const n = Number(v);
1621
+ if (isNaN(n)) return String(v);
1622
+ if (Math.abs(n) >= 1e12) return `$${(n / 1e12).toFixed(2)}T`;
1623
+ if (Math.abs(n) >= 1e9) return `$${(n / 1e9).toFixed(2)}B`;
1624
+ if (Math.abs(n) >= 1e6) return `$${(n / 1e6).toFixed(0)}M`;
1625
+ return `$${n.toLocaleString()}`;
1111
1626
  }
1112
1627
 
1113
1628
  // src/index.ts
1114
1629
  var program = new Command();
1115
1630
  var __dirname = path.dirname(fileURLToPath(import.meta.url));
1116
1631
  var pkgPath = path.join(__dirname, "..", "package.json");
1117
- var compiledVersion = "0.1.4".length > 0 ? "0.1.4" : void 0;
1632
+ var compiledVersion = "0.1.5".length > 0 ? "0.1.5" : void 0;
1118
1633
  var envVersion = process.env.BLOOM_CLI_VERSION ?? process.env.npm_package_version;
1119
1634
  var fallbackVersion = compiledVersion ?? "unknown";
1120
1635
  var cliVersion = envVersion ?? fallbackVersion;
@@ -1139,6 +1654,7 @@ registerMarketCommand(program);
1139
1654
  registerCollectionsCommand(program);
1140
1655
  registerScreenCommand(program);
1141
1656
  registerEarningsCommand(program);
1657
+ registerEarningsCalendarCommand(program);
1142
1658
  registerTechnicalsCommand(program);
1143
1659
  registerOptionsCommand(program);
1144
1660
  registerPortfolioCommand(program);
@@ -1149,8 +1665,21 @@ registerTranscriptCommand(program);
1149
1665
  registerAiTradesCommand(program);
1150
1666
  registerAiPortfolioCommand(program);
1151
1667
  registerMaCommand(program);
1152
- registerSearchCommand(program);
1153
- registerScrapeCommand(program);
1668
+ registerBacktestCommand(program);
1669
+ registerCorrelationCommand(program);
1670
+ registerRiskCommand(program);
1671
+ registerDrawdownCommand(program);
1672
+ registerMacroCommand(program);
1673
+ registerSectorsCommand(program);
1674
+ registerDcfCommand(program);
1675
+ registerHoldersCommand(program);
1676
+ registerShortInterestCommand(program);
1677
+ registerInsiderCommand(program);
1678
+ registerDividendsCommand(program);
1679
+ registerSplitsCommand(program);
1680
+ registerIpoCommand(program);
1681
+ registerCompareCommand(program);
1682
+ registerRatingsCommand(program);
1154
1683
  program.parseAsync().catch((err) => {
1155
1684
  console.error("Error:", err.message || err);
1156
1685
  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.5",
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
  }