@bloomai/cli 0.1.3 → 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.
- package/README.md +102 -8
- package/SKILL.md +10 -0
- package/dist/index.js +755 -8
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -34,27 +34,71 @@ 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` |
|
|
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
|
+
|---------|-------------|---------|
|
|
50
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` |
|
|
51
70
|
| `trades` | AI trade evaluation | `bloom trades "Buy AAPL: strong AI catalysts"` |
|
|
52
71
|
| `catalysts` | Forward-looking value drivers | `bloom catalysts AAPL MSFT` |
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
|
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` |
|
|
56
94
|
| `ai-portfolio` | AI model portfolio positions & performance | `bloom ai-portfolio --model openai` |
|
|
57
95
|
|
|
96
|
+
## Environment Variables
|
|
97
|
+
|
|
98
|
+
| Variable | Required for | Description |
|
|
99
|
+
|----------|-------------|-------------|
|
|
100
|
+
| `BLOOM_API_KEY` | All commands | Bloom API key (`bloom auth login`) |
|
|
101
|
+
|
|
58
102
|
## Global Flags
|
|
59
103
|
|
|
60
104
|
Every command supports:
|
|
@@ -117,6 +161,21 @@ bloom transcript AAPL "AI" "services" "margins" --max 20 -o /tmp/bloom/transcrip
|
|
|
117
161
|
jq '.matches[] | {speaker, text}' /tmp/bloom/transcript.json
|
|
118
162
|
```
|
|
119
163
|
|
|
164
|
+
### Moving Averages
|
|
165
|
+
```bash
|
|
166
|
+
# Default: 100-day SMA + 100-day EMA
|
|
167
|
+
bloom ma AAPL
|
|
168
|
+
|
|
169
|
+
# Custom periods
|
|
170
|
+
bloom ma AAPL --sma 50,100,200 --ema 20,100
|
|
171
|
+
|
|
172
|
+
# Last 50 data points, save to file
|
|
173
|
+
bloom ma AAPL --sma 200 --limit 50 -o /tmp/bloom/aapl-ma.json
|
|
174
|
+
|
|
175
|
+
# Extract latest values
|
|
176
|
+
jq '.indicators | to_entries[] | {(.key): .value | to_entries | last | .value}' /tmp/bloom/aapl-ma.json
|
|
177
|
+
```
|
|
178
|
+
|
|
120
179
|
### AI Arena (InvestingArena — 3 AIs with real money)
|
|
121
180
|
```bash
|
|
122
181
|
# All AI models' recent trades
|
|
@@ -138,6 +197,41 @@ bloom ai-portfolio --model gemini
|
|
|
138
197
|
jq '[.[] | {model: .ai_model, return_pct, account_value}]' /tmp/bloom/ai-portfolio.json
|
|
139
198
|
```
|
|
140
199
|
|
|
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
|
|
220
|
+
```bash
|
|
221
|
+
# Backtest a portfolio
|
|
222
|
+
bloom backtest --symbols AAPL,MSFT,GOOG --allocations 0.4,0.3,0.3 -o /tmp/bloom/backtest.json
|
|
223
|
+
|
|
224
|
+
# Correlation matrix
|
|
225
|
+
bloom correlation AAPL MSFT GOOG AMZN
|
|
226
|
+
|
|
227
|
+
# Drawdown analysis
|
|
228
|
+
bloom drawdown --symbols AAPL,MSFT --allocations 0.5,0.5
|
|
229
|
+
|
|
230
|
+
# Macro overview
|
|
231
|
+
bloom macro --indicators GDP,CPI,UNEMPLOYMENT
|
|
232
|
+
bloom sectors
|
|
233
|
+
```
|
|
234
|
+
|
|
141
235
|
## Tips
|
|
142
236
|
|
|
143
237
|
- Use `-o` to download data to files, then `jq` to extract. Saves context tokens.
|
package/SKILL.md
CHANGED
|
@@ -50,6 +50,16 @@ jq '.data[] | select(.revenue_growth > 0.1)' /tmp/bloom/aapl-revenue.json
|
|
|
50
50
|
| `transcript` | Search earnings call transcripts | `bloom transcript AAPL "AI" "services"` |
|
|
51
51
|
| `ai-trades` | Recent AI model trade decisions (InvestingArena) | `bloom ai-trades --model claude --limit 5` |
|
|
52
52
|
| `ai-portfolio` | AI model portfolio positions & performance | `bloom ai-portfolio --model openai` |
|
|
53
|
+
| `search` | Web search via Serper (Google) | `bloom search "nvidia earnings Q1 2026"` |
|
|
54
|
+
| `scrape` | Scrape a web page via Firecrawl | `bloom scrape https://example.com` |
|
|
55
|
+
|
|
56
|
+
## Environment Variables
|
|
57
|
+
|
|
58
|
+
| Variable | Required for | Description |
|
|
59
|
+
|----------|-------------|-------------|
|
|
60
|
+
| `BLOOM_API_KEY` | All financial commands | Bloom API key (`bloom auth login`) |
|
|
61
|
+
| `SERPER_API_KEY` | `bloom search` | Serper.dev API key for web search |
|
|
62
|
+
| `FIRECRAWL_API_KEY` | `bloom scrape` | Firecrawl API key for web scraping |
|
|
53
63
|
|
|
54
64
|
## Global Flags
|
|
55
65
|
|
package/dist/index.js
CHANGED
|
@@ -110,7 +110,8 @@ function registerAuthCommand(program2) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// src/api.ts
|
|
113
|
-
var
|
|
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
|
|
169
|
-
if (
|
|
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 (
|
|
195
|
+
if (fmt2 === "csv") {
|
|
174
196
|
emit(toCSV(data), opts);
|
|
175
197
|
return;
|
|
176
198
|
}
|
|
@@ -452,7 +474,9 @@ var CHART_TYPES = [
|
|
|
452
474
|
"net_earnings",
|
|
453
475
|
"net_earnings_growth",
|
|
454
476
|
"free_cash_flow",
|
|
455
|
-
"free_cash_flow_growth"
|
|
477
|
+
"free_cash_flow_growth",
|
|
478
|
+
"stock_based_compensation",
|
|
479
|
+
"stock_based_compensation_growth"
|
|
456
480
|
];
|
|
457
481
|
function registerFinancialsCommand(program2) {
|
|
458
482
|
addGlobalOptions(
|
|
@@ -511,12 +535,13 @@ function formatSentiment(data) {
|
|
|
511
535
|
// src/commands/market.ts
|
|
512
536
|
function registerMarketCommand(program2) {
|
|
513
537
|
addGlobalOptions(
|
|
514
|
-
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")
|
|
538
|
+
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")
|
|
515
539
|
).action(async (opts) => {
|
|
516
540
|
const data = await callTool("get_market_map", {
|
|
517
541
|
map_type: opts.type,
|
|
518
542
|
period: opts.period,
|
|
519
|
-
limit: parseIntSafe(opts.limit, 24)
|
|
543
|
+
limit: parseIntSafe(opts.limit, 24),
|
|
544
|
+
universe: opts.universe
|
|
520
545
|
});
|
|
521
546
|
outputResult(data, opts);
|
|
522
547
|
});
|
|
@@ -601,6 +626,40 @@ function registerEarningsCommand(program2) {
|
|
|
601
626
|
});
|
|
602
627
|
}
|
|
603
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
|
+
|
|
604
663
|
// src/commands/technicals.ts
|
|
605
664
|
var SECTIONS2 = ["technical_indicators", "options_analysis", "insider_activity"];
|
|
606
665
|
function registerTechnicalsCommand(program2) {
|
|
@@ -895,11 +954,682 @@ function formatPortfolios(data, opts) {
|
|
|
895
954
|
return lines.join("\n");
|
|
896
955
|
}
|
|
897
956
|
|
|
957
|
+
// src/commands/ma.ts
|
|
958
|
+
function formatTable(data, limit) {
|
|
959
|
+
const { symbol, current_price, indicators } = data;
|
|
960
|
+
const labels = Object.keys(indicators);
|
|
961
|
+
if (labels.length === 0) {
|
|
962
|
+
console.log(`No indicator data returned for ${symbol}`);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const allDates = /* @__PURE__ */ new Set();
|
|
966
|
+
for (const label of labels) {
|
|
967
|
+
const ind = indicators[label];
|
|
968
|
+
if ("error" in ind) continue;
|
|
969
|
+
for (const date of Object.keys(ind)) {
|
|
970
|
+
allDates.add(date);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const sortedDates = Array.from(allDates).sort((a, b) => b.localeCompare(a)).slice(0, limit).reverse();
|
|
974
|
+
if (sortedDates.length === 0) {
|
|
975
|
+
console.log(`No data points available for ${symbol}`);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const dateWidth = 12;
|
|
979
|
+
const colWidth = 12;
|
|
980
|
+
const header = "Date".padEnd(dateWidth) + labels.map((l) => l.toUpperCase().padStart(colWidth)).join("");
|
|
981
|
+
const divider = "-".repeat(dateWidth + colWidth * labels.length);
|
|
982
|
+
console.log(`
|
|
983
|
+
${symbol}${current_price != null ? ` (current: $${current_price.toFixed(2)})` : ""}`);
|
|
984
|
+
console.log(divider);
|
|
985
|
+
console.log(header);
|
|
986
|
+
console.log(divider);
|
|
987
|
+
for (const date of sortedDates) {
|
|
988
|
+
let row = date.padEnd(dateWidth);
|
|
989
|
+
for (const label of labels) {
|
|
990
|
+
const ind = indicators[label];
|
|
991
|
+
if ("error" in ind) {
|
|
992
|
+
row += "N/A".padStart(colWidth);
|
|
993
|
+
} else {
|
|
994
|
+
const val = ind[date];
|
|
995
|
+
row += (val != null ? val.toFixed(2) : "\u2014").padStart(colWidth);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
console.log(row);
|
|
999
|
+
}
|
|
1000
|
+
console.log(divider);
|
|
1001
|
+
console.log(`Showing ${sortedDates.length} data point(s)
|
|
1002
|
+
`);
|
|
1003
|
+
}
|
|
1004
|
+
function registerMaCommand(program2) {
|
|
1005
|
+
addGlobalOptions(
|
|
1006
|
+
program2.command("ma").description("Moving averages (SMA/EMA) for a symbol").argument("<symbol>", "Ticker symbol (e.g., AAPL)").option("--sma <periods>", "Comma-separated SMA periods (e.g., 50,100,200)").option("--ema <periods>", "Comma-separated EMA periods (e.g., 20,50)").option("--limit <n>", "Number of data points to display (default: 20)", "20")
|
|
1007
|
+
).action(
|
|
1008
|
+
async (symbol, opts) => {
|
|
1009
|
+
const indicators = [];
|
|
1010
|
+
if (opts.sma) {
|
|
1011
|
+
for (const p of opts.sma.split(",")) {
|
|
1012
|
+
const period = p.trim();
|
|
1013
|
+
if (period) indicators.push(`sma_${period}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (opts.ema) {
|
|
1017
|
+
for (const p of opts.ema.split(",")) {
|
|
1018
|
+
const period = p.trim();
|
|
1019
|
+
if (period) indicators.push(`ema_${period}`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (indicators.length === 0) {
|
|
1023
|
+
indicators.push("sma_100", "ema_100");
|
|
1024
|
+
}
|
|
1025
|
+
const args = {
|
|
1026
|
+
symbol: symbol.toUpperCase(),
|
|
1027
|
+
indicators
|
|
1028
|
+
};
|
|
1029
|
+
const data = await callTool("get_moving_averages", args);
|
|
1030
|
+
const limit = parseInt(opts.limit, 10) || 20;
|
|
1031
|
+
let parsed = null;
|
|
1032
|
+
try {
|
|
1033
|
+
parsed = typeof data === "string" ? JSON.parse(data) : data;
|
|
1034
|
+
} catch {
|
|
1035
|
+
}
|
|
1036
|
+
if (parsed?.indicators && typeof parsed.indicators === "object") {
|
|
1037
|
+
const allDates = /* @__PURE__ */ new Set();
|
|
1038
|
+
for (const ind of Object.values(parsed.indicators)) {
|
|
1039
|
+
if ("error" in ind) continue;
|
|
1040
|
+
for (const date of Object.keys(ind)) {
|
|
1041
|
+
allDates.add(date);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
const keepDates = new Set(
|
|
1045
|
+
Array.from(allDates).sort((a, b) => b.localeCompare(a)).slice(0, limit)
|
|
1046
|
+
);
|
|
1047
|
+
for (const [label, ind] of Object.entries(parsed.indicators)) {
|
|
1048
|
+
if ("error" in ind) continue;
|
|
1049
|
+
const filtered = {};
|
|
1050
|
+
for (const [date, val] of Object.entries(ind)) {
|
|
1051
|
+
if (keepDates.has(date)) filtered[date] = val;
|
|
1052
|
+
}
|
|
1053
|
+
parsed.indicators[label] = filtered;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (opts.output || opts.format || opts.raw || !process.stdout.isTTY) {
|
|
1057
|
+
outputResult(parsed ?? data, opts);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (parsed?.indicators && typeof parsed.indicators === "object") {
|
|
1061
|
+
formatTable(parsed, limit);
|
|
1062
|
+
} else {
|
|
1063
|
+
outputResult(data, opts);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/commands/backtest.ts
|
|
1070
|
+
function registerBacktestCommand(program2) {
|
|
1071
|
+
addGlobalOptions(
|
|
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
|
|
1079
|
+
});
|
|
1080
|
+
outputResult(data, opts, formatBacktest);
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
function formatBacktest(data) {
|
|
1084
|
+
const obj = data;
|
|
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)
|
|
1157
|
+
]);
|
|
1158
|
+
return makeTable(["Category", "Description"], rows);
|
|
1159
|
+
}
|
|
1160
|
+
function truncate3(s, max) {
|
|
1161
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// src/commands/drawdown.ts
|
|
1165
|
+
function registerDrawdownCommand(program2) {
|
|
1166
|
+
addGlobalOptions(
|
|
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
|
+
}
|
|
1205
|
+
}
|
|
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;
|
|
1225
|
+
}
|
|
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;
|
|
1243
|
+
}
|
|
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);
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
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) {
|
|
1288
|
+
const obj = data;
|
|
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
|
+
}
|
|
1307
|
+
|
|
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)}`);
|
|
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));
|
|
1606
|
+
}
|
|
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()}`;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
898
1628
|
// src/index.ts
|
|
899
1629
|
var program = new Command();
|
|
900
1630
|
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
901
1631
|
var pkgPath = path.join(__dirname, "..", "package.json");
|
|
902
|
-
var compiledVersion = "0.1.
|
|
1632
|
+
var compiledVersion = "0.1.5".length > 0 ? "0.1.5" : void 0;
|
|
903
1633
|
var envVersion = process.env.BLOOM_CLI_VERSION ?? process.env.npm_package_version;
|
|
904
1634
|
var fallbackVersion = compiledVersion ?? "unknown";
|
|
905
1635
|
var cliVersion = envVersion ?? fallbackVersion;
|
|
@@ -924,6 +1654,7 @@ registerMarketCommand(program);
|
|
|
924
1654
|
registerCollectionsCommand(program);
|
|
925
1655
|
registerScreenCommand(program);
|
|
926
1656
|
registerEarningsCommand(program);
|
|
1657
|
+
registerEarningsCalendarCommand(program);
|
|
927
1658
|
registerTechnicalsCommand(program);
|
|
928
1659
|
registerOptionsCommand(program);
|
|
929
1660
|
registerPortfolioCommand(program);
|
|
@@ -933,6 +1664,22 @@ registerPositionCommand(program);
|
|
|
933
1664
|
registerTranscriptCommand(program);
|
|
934
1665
|
registerAiTradesCommand(program);
|
|
935
1666
|
registerAiPortfolioCommand(program);
|
|
1667
|
+
registerMaCommand(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);
|
|
936
1683
|
program.parseAsync().catch((err) => {
|
|
937
1684
|
console.error("Error:", err.message || err);
|
|
938
1685
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bloomai/cli",
|
|
3
|
-
"version": "0.1.
|
|
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
|
}
|