@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.
- package/README.md +75 -22
- package/dist/index.js +616 -87
- 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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
|
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
|
|
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
|
-
###
|
|
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
|
-
#
|
|
170
|
-
bloom
|
|
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
|
-
#
|
|
173
|
-
bloom
|
|
224
|
+
# Correlation matrix
|
|
225
|
+
bloom correlation AAPL MSFT GOOG AMZN
|
|
174
226
|
|
|
175
|
-
#
|
|
176
|
-
bloom
|
|
227
|
+
# Drawdown analysis
|
|
228
|
+
bloom drawdown --symbols AAPL,MSFT --allocations 0.5,0.5
|
|
177
229
|
|
|
178
|
-
#
|
|
179
|
-
bloom
|
|
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
|
|
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
|
}
|
|
@@ -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/
|
|
1014
|
-
function
|
|
1069
|
+
// src/commands/backtest.ts
|
|
1070
|
+
function registerBacktestCommand(program2) {
|
|
1015
1071
|
addGlobalOptions(
|
|
1016
|
-
program2.command("
|
|
1017
|
-
).action(async (
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
-
|
|
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
|
|
1050
|
-
if (!data || typeof data !== "object") return String(data);
|
|
1083
|
+
function formatBacktest(data) {
|
|
1051
1084
|
const obj = data;
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const rows =
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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(["
|
|
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/
|
|
1064
|
-
function
|
|
1164
|
+
// src/commands/drawdown.ts
|
|
1165
|
+
function registerDrawdownCommand(program2) {
|
|
1065
1166
|
addGlobalOptions(
|
|
1066
|
-
program2.command("
|
|
1067
|
-
).action(async (
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
|
1091
|
-
|
|
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
|
-
|
|
1095
|
-
|
|
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
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1153
|
-
|
|
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.
|
|
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
|
}
|