sentisense 0.51.0 → 0.52.0
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 +25 -1
- package/dist/cli.cjs +442 -51
- package/dist/index.cjs +17 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +39 -2
- package/dist/index.d.ts +39 -2
- package/dist/index.mjs +17 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,7 +71,8 @@ npx -y sentisense@latest health
|
|
|
71
71
|
| `quote <ticker>...` | Price, day range, 52-week range, market cap, P/E. One request per ticker |
|
|
72
72
|
| `sentiment <ticker>` | SentiSense Score, tone, attention, per-source breakdown, `--days N` history |
|
|
73
73
|
| `mood` | Composite market sentiment, the signals behind it, and the sector map |
|
|
74
|
-
| `analysts <ticker>` | Consensus, price target band, recent upgrades and downgrades |
|
|
74
|
+
| `analysts <ticker>` | Consensus, price target band, recent upgrades and downgrades. `--coverage` for who covers it, by firm |
|
|
75
|
+
| `analyst <slug>` | One analyst: their firms, their coverage book, and `--calls` for their price target notes |
|
|
75
76
|
| `earnings [ticker]` | Forward calendar with no ticker, per-quarter analysis with one (`earnings AAPL`) |
|
|
76
77
|
| `insiders <ticker>` | Filed Form 4 transactions, including whether they were pre-planned |
|
|
77
78
|
| `insights <ticker>` | Generated signals, filterable by `--urgency` and `--type` |
|
|
@@ -80,9 +81,22 @@ npx -y sentisense@latest health
|
|
|
80
81
|
| `flows [ticker]` | Institutional 13F flows, or one ticker's holders and notable changes |
|
|
81
82
|
| `options <ticker>` | End-of-day options positioning, IV rank, walls, unusual contracts |
|
|
82
83
|
| `screen --filter ...` | Screen the universe on Score, analyst, technical, and price fields |
|
|
84
|
+
| `search <name>` | Resolve a name, alias, ticker or slug to a symbol and the entity handle |
|
|
83
85
|
|
|
84
86
|
Run `sentisense help <command>` for its flags and examples.
|
|
85
87
|
|
|
88
|
+
Three of these chain into each other. Start from a name, land on a person:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx -y sentisense@latest search Tesla --type company # "Tesla" -> TSLA, plus the entity slug
|
|
92
|
+
npx -y sentisense@latest analysts NVDA --coverage # who covers it, by firm, with analyst slugs
|
|
93
|
+
npx -y sentisense@latest analyst quinn-bolton --calls # that analyst's firms, book, and notes
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`analyst` takes a slug, not a name: slugs are lowercase and hyphenated, and every named analyst in a `--coverage` row carries the one that addresses them. A name is rejected before a request is spent. What comes back is call history, not accuracy scoring: there is no hit rate, no ranking, and nothing in it rates the person.
|
|
97
|
+
|
|
98
|
+
Ranking on `search` is the API's own, and a company can sort below its own products, so pass `--type company` when what you want is the issuer.
|
|
99
|
+
|
|
86
100
|
### Output modes
|
|
87
101
|
|
|
88
102
|
Readable in a terminal, plain text when piped, and exact API JSON on request:
|
|
@@ -474,8 +488,18 @@ All five are optional: a response served before they shipped omits them. For the
|
|
|
474
488
|
```typescript
|
|
475
489
|
client.marketMood.get() // Composite market sentiment with sub-signals
|
|
476
490
|
client.kb.getPopularEntities() // Most-tracked entities
|
|
491
|
+
client.kb.searchEntities("Tesla") // Resolve a name, alias, ticker or slug to what we track
|
|
477
492
|
```
|
|
478
493
|
|
|
494
|
+
Entity search is resolution, not enumeration: the query must be at least 2 characters, the match count is capped at 25, and it returns a bare `EntitySearchResult[]` rather than a `PreviewResponse` envelope. Each hit carries `name`, `type`, the `ticker` for a listed entity (`null` for everything else), and the `urlSlug` the metric endpoints address that entity by, which is the only way to get a handle for a person, product or topic with no ticker.
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
const hits = await client.kb.searchEntities("Tesla", { type: "company", limit: 5 });
|
|
498
|
+
const symbol = hits.find((hit) => hit.ticker)?.ticker; // "TSLA"
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
An empty array is the normal answer for a query that matches nothing, so branch on `length` rather than catching.
|
|
502
|
+
|
|
479
503
|
### Screener
|
|
480
504
|
|
|
481
505
|
Filter the tracked universe on the SentiSense Score, attention, analyst consensus, technicals and price in one query. Screening on analyst ratings alone is something a dozen free tools do; screening on analyst ratings *where the Score disagrees* is not.
|
package/dist/cli.cjs
CHANGED
|
@@ -77,6 +77,14 @@ var UnknownTickerError = class extends Error {
|
|
|
77
77
|
this.ticker = ticker;
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
|
+
var NoMatchError = class extends Error {
|
|
81
|
+
constructor(query2, hint) {
|
|
82
|
+
super(`nothing matches "${query2}".`);
|
|
83
|
+
this.name = "NoMatchError";
|
|
84
|
+
this.query = query2;
|
|
85
|
+
this.hint = hint;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
80
88
|
var MissingKeyError = class extends Error {
|
|
81
89
|
constructor() {
|
|
82
90
|
super("no API key configured.");
|
|
@@ -109,6 +117,12 @@ function classify(error, command) {
|
|
|
109
117
|
]
|
|
110
118
|
};
|
|
111
119
|
}
|
|
120
|
+
if (error instanceof NoMatchError) {
|
|
121
|
+
return {
|
|
122
|
+
exitCode: EXIT.NOT_FOUND,
|
|
123
|
+
lines: [`error: ${error.message}`, `next: ${error.hint}`]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
112
126
|
if (error instanceof CliUsageError) {
|
|
113
127
|
return {
|
|
114
128
|
exitCode: EXIT.USAGE,
|
|
@@ -184,48 +198,6 @@ function classify(error, command) {
|
|
|
184
198
|
};
|
|
185
199
|
}
|
|
186
200
|
|
|
187
|
-
// src/cli/ticker.ts
|
|
188
|
-
function rejectSurplus(args, name, allowed) {
|
|
189
|
-
if (args.positionals.length <= allowed) return;
|
|
190
|
-
throw new CliUsageError(
|
|
191
|
-
`${name} takes one ticker, and got ${args.positionals.length}.`,
|
|
192
|
-
"run it once per ticker. Only quote accepts more than one."
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
function oneTicker(args, name) {
|
|
196
|
-
rejectSurplus(args, name, 1);
|
|
197
|
-
const ticker = args.positionals[0];
|
|
198
|
-
if (!ticker) {
|
|
199
|
-
throw new CliUsageError(
|
|
200
|
-
`${name} needs a ticker.`,
|
|
201
|
-
`for example: sentisense ${name} NVDA`
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
return ticker.toUpperCase();
|
|
205
|
-
}
|
|
206
|
-
function optionalTicker(args, name) {
|
|
207
|
-
rejectSurplus(args, name, 1);
|
|
208
|
-
return args.positionals[0]?.toUpperCase();
|
|
209
|
-
}
|
|
210
|
-
function rejectPositionals(args, name) {
|
|
211
|
-
if (args.positionals.length === 0) return;
|
|
212
|
-
throw new CliUsageError(
|
|
213
|
-
`${name} takes no ticker, and got ${args.positionals.length}.`,
|
|
214
|
-
`run "sentisense help ${name}" for what it accepts.`
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
async function verifyTickerOnEmpty(api, ticker) {
|
|
218
|
-
try {
|
|
219
|
-
await api.stocks.getQuote(ticker);
|
|
220
|
-
return void 0;
|
|
221
|
-
} catch (error) {
|
|
222
|
-
if (error instanceof NotFoundError) throw new UnknownTickerError(ticker);
|
|
223
|
-
return `could not verify ${ticker}, so the empty result is unconfirmed`;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
var EMPTY_VERIFY_NOTE = "An empty result verifies the ticker before reporting no data, so a typo exits 4 rather";
|
|
227
|
-
var EMPTY_VERIFY_NOTE_2 = "than looking like a company with nothing to report.";
|
|
228
|
-
|
|
229
201
|
// src/cli/render/doc.ts
|
|
230
202
|
function cell(text, tone) {
|
|
231
203
|
return tone ? { text, tone } : { text };
|
|
@@ -322,34 +294,264 @@ function truncate(text, max) {
|
|
|
322
294
|
return `${text.slice(0, Math.max(0, max - 3))}...`;
|
|
323
295
|
}
|
|
324
296
|
|
|
297
|
+
// src/cli/commands/analyst.ts
|
|
298
|
+
var DEFAULT_CALLS = 25;
|
|
299
|
+
var NOT_A_SCORECARD = "Call history, not accuracy scoring: no hit rate, no ranking, and nothing here rates the person.";
|
|
300
|
+
function oneSlug(positionals) {
|
|
301
|
+
if (positionals.length > 1) {
|
|
302
|
+
throw new CliUsageError(
|
|
303
|
+
`analyst takes one slug, and got ${positionals.length}.`,
|
|
304
|
+
"run it once per analyst."
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const raw = positionals[0];
|
|
308
|
+
if (!raw) {
|
|
309
|
+
throw new CliUsageError(
|
|
310
|
+
"analyst needs a slug.",
|
|
311
|
+
'for example: sentisense analyst dan-ives. Slugs come from "sentisense analysts <ticker> --coverage".'
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (/\s/.test(raw)) {
|
|
315
|
+
throw new CliUsageError(
|
|
316
|
+
`analyst takes a slug, not a name, and got "${raw}".`,
|
|
317
|
+
'slugs are lowercase and hyphenated, such as dan-ives. Every named analyst in "sentisense analysts <ticker> --coverage" carries one.'
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return raw.toLowerCase();
|
|
321
|
+
}
|
|
322
|
+
var analystCommand = {
|
|
323
|
+
name: "analyst",
|
|
324
|
+
summary: "One analyst: the firms they publish under, what they cover, and their calls",
|
|
325
|
+
usage: "sentisense analyst <slug> [--calls] [--limit N]",
|
|
326
|
+
examples: [
|
|
327
|
+
"sentisense analyst dan-ives",
|
|
328
|
+
"sentisense analyst dan-ives --calls",
|
|
329
|
+
"sentisense analyst dan-ives --calls --limit 50 --full",
|
|
330
|
+
"sentisense analyst dan-ives --json"
|
|
331
|
+
],
|
|
332
|
+
notes: [
|
|
333
|
+
"Takes a slug, not a name. Slugs are lowercase and hyphenated, and every named analyst",
|
|
334
|
+
'in "sentisense analysts <ticker> --coverage" carries the one that addresses them here.',
|
|
335
|
+
"--calls appends their price target notes, newest first.",
|
|
336
|
+
"firstSeen and lastSeen bound the notes on record, not employment: the most recent firm",
|
|
337
|
+
"is where they last published, not necessarily where they work today.",
|
|
338
|
+
NOT_A_SCORECARD,
|
|
339
|
+
"A slug that matches no analyst exits 4."
|
|
340
|
+
],
|
|
341
|
+
flags: {
|
|
342
|
+
calls: { type: "boolean", describe: "Append their price target notes, newest first" },
|
|
343
|
+
limit: { type: "number", placeholder: "N", describe: `Calls to request, 1 to 200 (default ${DEFAULT_CALLS})` }
|
|
344
|
+
},
|
|
345
|
+
async run({ args, client, full }) {
|
|
346
|
+
const slug = oneSlug(args.positionals);
|
|
347
|
+
const wantCalls = args.flags.calls === true;
|
|
348
|
+
const limit = typeof args.flags.limit === "number" ? args.flags.limit : void 0;
|
|
349
|
+
const api = client();
|
|
350
|
+
const profileEnvelope = await api.analyst.profile(slug);
|
|
351
|
+
const callsEnvelope = wantCalls ? await api.analyst.calls(slug, limit === void 0 ? void 0 : { limit }) : null;
|
|
352
|
+
const data = profileEnvelope.data;
|
|
353
|
+
const blocks = [
|
|
354
|
+
{
|
|
355
|
+
kind: "head",
|
|
356
|
+
title: field("analyst", data?.name ?? slug),
|
|
357
|
+
subtitle: field("slug", data?.slug ?? slug),
|
|
358
|
+
right: fields(
|
|
359
|
+
field("notes", String(data?.noteCount ?? 0)),
|
|
360
|
+
field("tickers", String(data?.tickerCount ?? 0))
|
|
361
|
+
)
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
kind: "facts",
|
|
365
|
+
items: fields(
|
|
366
|
+
field("Most recent firm", data?.mostRecentFirm ?? "n/a"),
|
|
367
|
+
field("First seen", data?.firstSeen ?? "n/a"),
|
|
368
|
+
field("Last seen", data?.lastSeen ?? "n/a")
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
];
|
|
372
|
+
const firms = data?.firms ?? [];
|
|
373
|
+
if (firms.length > 0) {
|
|
374
|
+
blocks.push({ kind: "blank" });
|
|
375
|
+
blocks.push({
|
|
376
|
+
kind: "table",
|
|
377
|
+
head: ["FIRM", "FIRST NOTE", "LAST NOTE", "CURRENT"],
|
|
378
|
+
rows: firms.map((firm) => [
|
|
379
|
+
cell(truncate(firm.firm, full ? 40 : 28)),
|
|
380
|
+
cell(firm.firstSeen),
|
|
381
|
+
cell(firm.lastSeen),
|
|
382
|
+
cell(firm.mostRecent ? "yes" : "")
|
|
383
|
+
])
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const book = data?.coverage ?? [];
|
|
387
|
+
if (book.length > 0) {
|
|
388
|
+
const shown = full ? book : book.slice(0, 10);
|
|
389
|
+
blocks.push({ kind: "blank" });
|
|
390
|
+
blocks.push({
|
|
391
|
+
kind: "table",
|
|
392
|
+
head: ["TICKER", "NOTES", "LAST NOTE", "TARGET", "FIRM"],
|
|
393
|
+
align: ["left", "right", "left", "right", "left"],
|
|
394
|
+
rows: shown.map((entry) => [
|
|
395
|
+
cell(entry.ticker),
|
|
396
|
+
cell(String(entry.noteCount)),
|
|
397
|
+
cell(entry.lastNote ?? ""),
|
|
398
|
+
cell(entry.latestPriceTarget === null ? "" : money(entry.latestPriceTarget)),
|
|
399
|
+
cell(truncate(entry.latestFirm ?? "", full ? 40 : 24))
|
|
400
|
+
])
|
|
401
|
+
});
|
|
402
|
+
const total = profileEnvelope.totalCount ?? data?.tickerCount ?? book.length;
|
|
403
|
+
if (shown.length < total) {
|
|
404
|
+
blocks.push({
|
|
405
|
+
kind: "text",
|
|
406
|
+
text: `Showing ${shown.length} of ${total} covered tickers. Add --full for the rest.`,
|
|
407
|
+
tone: "dim"
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (callsEnvelope) {
|
|
412
|
+
const calls = callsEnvelope.data ?? [];
|
|
413
|
+
blocks.push({ kind: "blank" });
|
|
414
|
+
if (calls.length === 0) {
|
|
415
|
+
blocks.push({ kind: "text", text: "No price target notes on record for this analyst." });
|
|
416
|
+
} else {
|
|
417
|
+
const shown = full ? calls : calls.slice(0, 15);
|
|
418
|
+
blocks.push({
|
|
419
|
+
kind: "table",
|
|
420
|
+
head: ["DATE", "TICKER", "FIRM", "TARGET", "PRICE THEN", "PUBLISHER"],
|
|
421
|
+
align: ["left", "left", "left", "right", "right", "left"],
|
|
422
|
+
rows: shown.map((call) => [
|
|
423
|
+
cell(call.publishedDate),
|
|
424
|
+
cell(call.ticker),
|
|
425
|
+
cell(truncate(call.firm, full ? 40 : 22)),
|
|
426
|
+
cell(call.priceTarget === null ? "" : money(call.priceTarget)),
|
|
427
|
+
cell(call.priceWhenPosted === null ? "" : money(call.priceWhenPosted)),
|
|
428
|
+
cell(truncate(call.newsPublisher ?? "", full ? 40 : 20))
|
|
429
|
+
])
|
|
430
|
+
});
|
|
431
|
+
const total = callsEnvelope.totalCount ?? calls.length;
|
|
432
|
+
if (shown.length < total) {
|
|
433
|
+
blocks.push({
|
|
434
|
+
kind: "text",
|
|
435
|
+
text: `Showing ${shown.length} of ${total} calls. Add --full, or --limit to ask for more.`,
|
|
436
|
+
tone: "dim"
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (callsEnvelope.isPreview) {
|
|
441
|
+
blocks.push({
|
|
442
|
+
kind: "text",
|
|
443
|
+
text: "Preview response: a free key reads the first 25 calls, a PRO key pages the whole history.",
|
|
444
|
+
tone: "dim"
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (profileEnvelope.isPreview) {
|
|
449
|
+
blocks.push({
|
|
450
|
+
kind: "text",
|
|
451
|
+
text: "Preview response: the coverage book is trimmed to the 5 most recent tickers on a free key.",
|
|
452
|
+
tone: "dim"
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
blocks.push({ kind: "text", text: NOT_A_SCORECARD, tone: "dim" });
|
|
456
|
+
return {
|
|
457
|
+
json: callsEnvelope ? { profile: profileEnvelope, calls: callsEnvelope } : profileEnvelope,
|
|
458
|
+
doc: doc(...blocks)
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/cli/ticker.ts
|
|
464
|
+
function rejectSurplus(args, name, allowed) {
|
|
465
|
+
if (args.positionals.length <= allowed) return;
|
|
466
|
+
throw new CliUsageError(
|
|
467
|
+
`${name} takes one ticker, and got ${args.positionals.length}.`,
|
|
468
|
+
"run it once per ticker. Only quote accepts more than one."
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
function oneTicker(args, name) {
|
|
472
|
+
rejectSurplus(args, name, 1);
|
|
473
|
+
const ticker = args.positionals[0];
|
|
474
|
+
if (!ticker) {
|
|
475
|
+
throw new CliUsageError(
|
|
476
|
+
`${name} needs a ticker.`,
|
|
477
|
+
`for example: sentisense ${name} NVDA`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return ticker.toUpperCase();
|
|
481
|
+
}
|
|
482
|
+
function optionalTicker(args, name) {
|
|
483
|
+
rejectSurplus(args, name, 1);
|
|
484
|
+
return args.positionals[0]?.toUpperCase();
|
|
485
|
+
}
|
|
486
|
+
function rejectPositionals(args, name) {
|
|
487
|
+
if (args.positionals.length === 0) return;
|
|
488
|
+
throw new CliUsageError(
|
|
489
|
+
`${name} takes no ticker, and got ${args.positionals.length}.`,
|
|
490
|
+
`run "sentisense help ${name}" for what it accepts.`
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
async function verifyTickerOnEmpty(api, ticker) {
|
|
494
|
+
try {
|
|
495
|
+
await api.stocks.getQuote(ticker);
|
|
496
|
+
return void 0;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (error instanceof NotFoundError) throw new UnknownTickerError(ticker);
|
|
499
|
+
return `could not verify ${ticker}, so the empty result is unconfirmed`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
var EMPTY_VERIFY_NOTE = "An empty result verifies the ticker before reporting no data, so a typo exits 4 rather";
|
|
503
|
+
var EMPTY_VERIFY_NOTE_2 = "than looking like a company with nothing to report.";
|
|
504
|
+
|
|
325
505
|
// src/cli/commands/analysts.ts
|
|
326
506
|
var DEFAULT_ACTIONS = 5;
|
|
507
|
+
var DEFAULT_COVERAGE_ROWS = 15;
|
|
327
508
|
function actionTone(actionType) {
|
|
328
509
|
if (actionType === "UPGRADE") return "up";
|
|
329
510
|
if (actionType === "DOWNGRADE") return "down";
|
|
330
511
|
return void 0;
|
|
331
512
|
}
|
|
513
|
+
function primaryAnalyst(row) {
|
|
514
|
+
const named = row.latestNote?.analyst;
|
|
515
|
+
const match = (named ? row.analysts.find((analyst) => analyst.name === named) : void 0) ?? row.analysts[0];
|
|
516
|
+
const name = match?.name ?? named ?? "";
|
|
517
|
+
const extra = row.analysts.length > 1 ? ` +${row.analysts.length - 1}` : "";
|
|
518
|
+
return { name: name ? `${name}${extra}` : "", slug: match?.slug ?? "" };
|
|
519
|
+
}
|
|
332
520
|
var analystsCommand = {
|
|
333
521
|
name: "analysts",
|
|
334
522
|
summary: "Wall Street consensus, price target band, and recent rating changes",
|
|
335
|
-
usage: "sentisense analysts <ticker> [--days N]",
|
|
523
|
+
usage: "sentisense analysts <ticker> [--coverage] [--days N] [--limit N]",
|
|
336
524
|
examples: [
|
|
337
525
|
"sentisense analysts NVDA",
|
|
338
526
|
"sentisense analysts NVDA --days 180 --full",
|
|
527
|
+
"sentisense analysts NVDA --coverage",
|
|
528
|
+
"sentisense analysts NVDA --coverage --days 180 --limit 30",
|
|
339
529
|
"sentisense analysts NVDA --json"
|
|
340
530
|
],
|
|
341
531
|
notes: [
|
|
342
532
|
"The price target band and analyst count come back in full on every key.",
|
|
343
533
|
"The buy / hold / sell distribution and the longer action history are PRO.",
|
|
344
|
-
"A ticker with no analyst coverage exits 4 rather than returning an empty band."
|
|
534
|
+
"A ticker with no analyst coverage exits 4 rather than returning an empty band.",
|
|
535
|
+
"--coverage answers who covers the ticker instead: one row per firm, the analyst that",
|
|
536
|
+
"firm last published under, their latest target, and the firm's current rating. The slug",
|
|
537
|
+
'on a row is what "sentisense analyst <slug>" takes.',
|
|
538
|
+
"A firm can cover a ticker on a rating action alone, so a blank target is a real reading",
|
|
539
|
+
"rather than missing data, and a blank analyst means the note named nobody.",
|
|
540
|
+
"--limit trims the rows printed under --coverage; the API returns the whole book either",
|
|
541
|
+
"way, and --full prints all of it."
|
|
345
542
|
],
|
|
346
543
|
flags: {
|
|
347
|
-
|
|
544
|
+
coverage: { type: "boolean", describe: "Show who covers the ticker, one row per firm" },
|
|
545
|
+
days: { type: "number", placeholder: "N", describe: "Days of rating history (default 90)" },
|
|
546
|
+
limit: { type: "number", placeholder: "N", describe: `Firm rows to print with --coverage (default ${DEFAULT_COVERAGE_ROWS})` }
|
|
348
547
|
},
|
|
349
548
|
async run({ args, client, full }) {
|
|
350
549
|
const ticker = oneTicker(args, "analysts");
|
|
351
550
|
const lookbackDays = typeof args.flags.days === "number" ? args.flags.days : void 0;
|
|
352
551
|
const api = client();
|
|
552
|
+
if (args.flags.coverage === true) {
|
|
553
|
+
return coverageRun({ api, ticker, lookbackDays, full, args });
|
|
554
|
+
}
|
|
353
555
|
const notes = [];
|
|
354
556
|
const [consensus, actions] = await Promise.all([
|
|
355
557
|
api.analyst.consensus(ticker),
|
|
@@ -428,6 +630,91 @@ var analystsCommand = {
|
|
|
428
630
|
return { json: { consensus, actions }, doc: doc(...blocks), notes };
|
|
429
631
|
}
|
|
430
632
|
};
|
|
633
|
+
async function coverageRun({ api, ticker, lookbackDays, full, args }) {
|
|
634
|
+
const envelope = await api.analyst.coverage(
|
|
635
|
+
ticker,
|
|
636
|
+
lookbackDays === void 0 ? void 0 : { lookbackDays }
|
|
637
|
+
);
|
|
638
|
+
const data = envelope.data;
|
|
639
|
+
const rows = data?.coverage ?? [];
|
|
640
|
+
const limit = typeof args.flags.limit === "number" ? args.flags.limit : DEFAULT_COVERAGE_ROWS;
|
|
641
|
+
const shown = full ? rows : rows.slice(0, Math.max(1, limit));
|
|
642
|
+
const blocks = [
|
|
643
|
+
{
|
|
644
|
+
kind: "head",
|
|
645
|
+
title: field("ticker", ticker),
|
|
646
|
+
right: fields(
|
|
647
|
+
field("firms", String(data?.firmCount ?? rows.length)),
|
|
648
|
+
field("named analysts", String(data?.namedAnalystCount ?? 0)),
|
|
649
|
+
data?.windowDays === void 0 ? void 0 : field("window", `${data.windowDays}d`)
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
];
|
|
653
|
+
const buckets = data?.ratingBuckets;
|
|
654
|
+
if (buckets) {
|
|
655
|
+
blocks.push({
|
|
656
|
+
kind: "facts",
|
|
657
|
+
items: fields(
|
|
658
|
+
field("Buy", String(buckets.buy), buckets.buy > 0 ? "up" : void 0),
|
|
659
|
+
field("Hold", String(buckets.hold)),
|
|
660
|
+
field("Sell", String(buckets.sell), buckets.sell > 0 ? "down" : void 0),
|
|
661
|
+
field("Unrated", String(buckets.unrated)),
|
|
662
|
+
field("Total", String(buckets.total))
|
|
663
|
+
)
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
blocks.push({
|
|
667
|
+
kind: "facts",
|
|
668
|
+
items: fields(
|
|
669
|
+
field("Notes", String(data?.noteCount ?? 0)),
|
|
670
|
+
field("Attributed", String(data?.attributedNoteCount ?? 0)),
|
|
671
|
+
field("Unattributed", String(data?.unattributedNoteCount ?? 0)),
|
|
672
|
+
field("Rating only", String(data?.ratingOnlyFirmCount ?? 0)),
|
|
673
|
+
data?.asOf === void 0 ? void 0 : field("As of", data.asOf)
|
|
674
|
+
)
|
|
675
|
+
});
|
|
676
|
+
if (shown.length === 0) {
|
|
677
|
+
blocks.push({ kind: "text", text: "No firm published a target or a rating in this window." });
|
|
678
|
+
} else {
|
|
679
|
+
blocks.push({ kind: "blank" });
|
|
680
|
+
blocks.push({
|
|
681
|
+
kind: "table",
|
|
682
|
+
head: ["FIRM", "ANALYST", "SLUG", "TARGET", "DATE", "RATING", "ACTION"],
|
|
683
|
+
align: ["left", "left", "left", "right", "left", "left", "left"],
|
|
684
|
+
rows: shown.map((row) => {
|
|
685
|
+
const analyst = primaryAnalyst(row);
|
|
686
|
+
return [
|
|
687
|
+
cell(truncate(row.firm, full ? 40 : 24)),
|
|
688
|
+
cell(truncate(analyst.name, full ? 32 : 20)),
|
|
689
|
+
cell(truncate(analyst.slug, full ? 32 : 20)),
|
|
690
|
+
cell(row.latestNote?.priceTarget === null || row.latestNote?.priceTarget === void 0 ? "" : money(row.latestNote.priceTarget)),
|
|
691
|
+
cell(row.latestNote?.publishedDate ?? row.firmRating?.date ?? ""),
|
|
692
|
+
cell(row.firmRating?.rating ?? ""),
|
|
693
|
+
cell(row.firmRating?.actionType ?? "", actionTone(row.firmRating?.actionType))
|
|
694
|
+
];
|
|
695
|
+
})
|
|
696
|
+
});
|
|
697
|
+
const total = data?.firmCount ?? rows.length;
|
|
698
|
+
if (shown.length < total) {
|
|
699
|
+
blocks.push({
|
|
700
|
+
kind: "text",
|
|
701
|
+
text: `Showing ${shown.length} of ${total} firms. Add --full for the rest.`,
|
|
702
|
+
tone: "dim"
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (data?.attributionNote) {
|
|
707
|
+
blocks.push({ kind: "text", text: data.attributionNote, tone: "dim" });
|
|
708
|
+
}
|
|
709
|
+
if (envelope.isPreview) {
|
|
710
|
+
blocks.push({
|
|
711
|
+
kind: "text",
|
|
712
|
+
text: "Preview response: the counts describe the whole window, the rows are trimmed on a free key.",
|
|
713
|
+
tone: "dim"
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return { json: envelope, doc: doc(...blocks) };
|
|
717
|
+
}
|
|
431
718
|
|
|
432
719
|
// src/cli/config.ts
|
|
433
720
|
var import_node_fs = require("fs");
|
|
@@ -1037,7 +1324,7 @@ var flowsCommand = {
|
|
|
1037
1324
|
};
|
|
1038
1325
|
|
|
1039
1326
|
// src/version.ts
|
|
1040
|
-
var VERSION = "0.
|
|
1327
|
+
var VERSION = "0.52.0";
|
|
1041
1328
|
|
|
1042
1329
|
// src/resources/analyst.ts
|
|
1043
1330
|
var Analyst = class {
|
|
@@ -1206,8 +1493,8 @@ var Documents = class {
|
|
|
1206
1493
|
);
|
|
1207
1494
|
}
|
|
1208
1495
|
/** Smart search with natural language query parsing. The rows are in `documents`. */
|
|
1209
|
-
async search(
|
|
1210
|
-
return this.client.get("/api/v1/documents/search", { query, ...options });
|
|
1496
|
+
async search(query2, options) {
|
|
1497
|
+
return this.client.get("/api/v1/documents/search", { query: query2, ...options });
|
|
1211
1498
|
}
|
|
1212
1499
|
/** Get latest document metrics from a source type. The rows are in `documents`. */
|
|
1213
1500
|
async getBySource(source, options) {
|
|
@@ -1675,6 +1962,22 @@ var KB = class {
|
|
|
1675
1962
|
async getPopularEntities() {
|
|
1676
1963
|
return this.client.get("/api/v1/kb/entities/popular");
|
|
1677
1964
|
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Resolve a name, alias, ticker or slug to the entities we track, best match first.
|
|
1967
|
+
*
|
|
1968
|
+
* This is resolution, not enumeration: the query must be at least 2 characters and the
|
|
1969
|
+
* result count is capped, so it answers "which handle did the user mean" rather than
|
|
1970
|
+
* dumping the graph. Use it when someone typed "Tesla" and the rest of your code needs
|
|
1971
|
+
* `TSLA`, or when you need the `urlSlug` an entity's metric series is addressed by.
|
|
1972
|
+
*
|
|
1973
|
+
* Returns a bare array, not a `PreviewResponse` envelope, and an empty array is the
|
|
1974
|
+
* normal answer for a query that matches nothing.
|
|
1975
|
+
*
|
|
1976
|
+
* @param q What the user typed. At least 2 characters, or the API answers 400.
|
|
1977
|
+
*/
|
|
1978
|
+
async searchEntities(q, options) {
|
|
1979
|
+
return this.client.get("/api/v1/kb/entities/search", { q, ...options });
|
|
1980
|
+
}
|
|
1678
1981
|
};
|
|
1679
1982
|
|
|
1680
1983
|
// src/resources/marketMood.ts
|
|
@@ -3622,6 +3925,90 @@ var screenCommand = {
|
|
|
3622
3925
|
}
|
|
3623
3926
|
};
|
|
3624
3927
|
|
|
3928
|
+
// src/cli/commands/search.ts
|
|
3929
|
+
var TYPES = ["person", "company", "product", "organization", "etf", "topic", "country"];
|
|
3930
|
+
function query(positionals) {
|
|
3931
|
+
const text = positionals.join(" ").trim();
|
|
3932
|
+
if (!text) {
|
|
3933
|
+
throw new CliUsageError("search needs something to look for.", "for example: sentisense search Tesla");
|
|
3934
|
+
}
|
|
3935
|
+
if (text.length < 2) {
|
|
3936
|
+
throw new CliUsageError(
|
|
3937
|
+
`search needs at least 2 characters, and got "${text}".`,
|
|
3938
|
+
"for example: sentisense search Tesla"
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3941
|
+
return text;
|
|
3942
|
+
}
|
|
3943
|
+
var searchCommand = {
|
|
3944
|
+
name: "search",
|
|
3945
|
+
summary: "Resolve a name, alias, ticker or slug to the entities we track",
|
|
3946
|
+
usage: "sentisense search <name> [--type <kind>] [--limit N]",
|
|
3947
|
+
examples: [
|
|
3948
|
+
"sentisense search Tesla",
|
|
3949
|
+
"sentisense search 'Elon Musk' --type person",
|
|
3950
|
+
"sentisense search nvidia --limit 5",
|
|
3951
|
+
"sentisense search Tesla --json"
|
|
3952
|
+
],
|
|
3953
|
+
notes: [
|
|
3954
|
+
"For when you have a name and the rest of your pipeline needs a symbol: search Tesla,",
|
|
3955
|
+
"get TSLA, then run quote or sentiment on it.",
|
|
3956
|
+
"The slug column is the handle the metric endpoints address an entity by, which is the",
|
|
3957
|
+
"only place to get it for a person, product or topic that has no ticker.",
|
|
3958
|
+
`--type narrows to one kind: ${TYPES.join(", ")}.`,
|
|
3959
|
+
"Ranking is the API's, and a company can sort below its own products, so reach for",
|
|
3960
|
+
"--type company when what you want is the issuer. A query that matches nothing exits 4."
|
|
3961
|
+
],
|
|
3962
|
+
flags: {
|
|
3963
|
+
type: { type: "string", placeholder: "kind", describe: `Narrow to one kind: ${TYPES.join(", ")}` },
|
|
3964
|
+
limit: { type: "number", placeholder: "N", describe: "Matches to return, 1 to 25 (default 10)" }
|
|
3965
|
+
},
|
|
3966
|
+
async run({ args, client, full }) {
|
|
3967
|
+
const text = query(args.positionals);
|
|
3968
|
+
const type = typeof args.flags.type === "string" ? args.flags.type.toLowerCase() : void 0;
|
|
3969
|
+
if (type && !TYPES.includes(type)) {
|
|
3970
|
+
throw new CliUsageError(
|
|
3971
|
+
`--type takes one of ${TYPES.join(", ")}, got "${type}".`,
|
|
3972
|
+
"for example: --type company"
|
|
3973
|
+
);
|
|
3974
|
+
}
|
|
3975
|
+
const limit = typeof args.flags.limit === "number" ? args.flags.limit : void 0;
|
|
3976
|
+
const api = client();
|
|
3977
|
+
const results = await api.kb.searchEntities(text, {
|
|
3978
|
+
...type ? { type } : {},
|
|
3979
|
+
...limit === void 0 ? {} : { limit }
|
|
3980
|
+
});
|
|
3981
|
+
if (results.length === 0) {
|
|
3982
|
+
throw new NoMatchError(
|
|
3983
|
+
text,
|
|
3984
|
+
type ? `nothing of type ${type} matched. Try dropping --type, or fewer characters.` : "try fewer characters, another spelling, or the ticker itself."
|
|
3985
|
+
);
|
|
3986
|
+
}
|
|
3987
|
+
const blocks = [
|
|
3988
|
+
{
|
|
3989
|
+
kind: "head",
|
|
3990
|
+
title: field("query", text),
|
|
3991
|
+
right: fields(
|
|
3992
|
+
field("matches", String(results.length)),
|
|
3993
|
+
type === void 0 ? void 0 : field("type", type)
|
|
3994
|
+
)
|
|
3995
|
+
},
|
|
3996
|
+
{ kind: "blank" },
|
|
3997
|
+
{
|
|
3998
|
+
kind: "table",
|
|
3999
|
+
head: ["SYMBOL", "NAME", "TYPE", "SLUG"],
|
|
4000
|
+
rows: results.map((hit) => [
|
|
4001
|
+
cell(hit.ticker ?? "", hit.ticker ? "accent" : void 0),
|
|
4002
|
+
cell(truncate(hit.name, full ? 60 : 34)),
|
|
4003
|
+
cell(hit.type ?? ""),
|
|
4004
|
+
cell(truncate(hit.urlSlug ?? "", full ? 60 : 34))
|
|
4005
|
+
])
|
|
4006
|
+
}
|
|
4007
|
+
];
|
|
4008
|
+
return { json: results, doc: doc(...blocks) };
|
|
4009
|
+
}
|
|
4010
|
+
};
|
|
4011
|
+
|
|
3625
4012
|
// src/cli/commands/sentiment.ts
|
|
3626
4013
|
var DEFAULT_DAYS = 30;
|
|
3627
4014
|
function toneForDirection(value) {
|
|
@@ -3784,6 +4171,7 @@ var COMMANDS = [
|
|
|
3784
4171
|
sentimentCommand,
|
|
3785
4172
|
moodCommand,
|
|
3786
4173
|
analystsCommand,
|
|
4174
|
+
analystCommand,
|
|
3787
4175
|
earningsCommand,
|
|
3788
4176
|
insidersCommand,
|
|
3789
4177
|
insightsCommand,
|
|
@@ -3791,7 +4179,8 @@ var COMMANDS = [
|
|
|
3791
4179
|
newsCommand,
|
|
3792
4180
|
flowsCommand,
|
|
3793
4181
|
optionsCommand,
|
|
3794
|
-
screenCommand
|
|
4182
|
+
screenCommand,
|
|
4183
|
+
searchCommand
|
|
3795
4184
|
];
|
|
3796
4185
|
var COMMAND_NAMES = COMMANDS.map((command) => command.name);
|
|
3797
4186
|
function findCommand(name) {
|
|
@@ -3806,7 +4195,8 @@ var EXAMPLE = {
|
|
|
3806
4195
|
quote: "sentisense quote NVDA AAPL",
|
|
3807
4196
|
sentiment: "sentisense sentiment NVDA --days 30",
|
|
3808
4197
|
mood: "sentisense mood",
|
|
3809
|
-
analysts: "sentisense analysts NVDA",
|
|
4198
|
+
analysts: "sentisense analysts NVDA --coverage",
|
|
4199
|
+
analyst: "sentisense analyst dan-ives --calls",
|
|
3810
4200
|
earnings: "sentisense earnings --week next",
|
|
3811
4201
|
insiders: "sentisense insiders NVDA",
|
|
3812
4202
|
insights: "sentisense insights NVDA",
|
|
@@ -3814,7 +4204,8 @@ var EXAMPLE = {
|
|
|
3814
4204
|
news: "sentisense news NVDA",
|
|
3815
4205
|
flows: "sentisense flows NVDA",
|
|
3816
4206
|
options: "sentisense options NVDA",
|
|
3817
|
-
screen: "sentisense screen --filter SENTI_SCORE_7D:GTE:13"
|
|
4207
|
+
screen: "sentisense screen --filter SENTI_SCORE_7D:GTE:13",
|
|
4208
|
+
search: "sentisense search Tesla"
|
|
3818
4209
|
};
|
|
3819
4210
|
function pad(text, width) {
|
|
3820
4211
|
return text.length >= width ? text : text + " ".repeat(width - text.length);
|
package/dist/index.cjs
CHANGED
|
@@ -710,6 +710,22 @@ var KB = class {
|
|
|
710
710
|
async getPopularEntities() {
|
|
711
711
|
return this.client.get("/api/v1/kb/entities/popular");
|
|
712
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* Resolve a name, alias, ticker or slug to the entities we track, best match first.
|
|
715
|
+
*
|
|
716
|
+
* This is resolution, not enumeration: the query must be at least 2 characters and the
|
|
717
|
+
* result count is capped, so it answers "which handle did the user mean" rather than
|
|
718
|
+
* dumping the graph. Use it when someone typed "Tesla" and the rest of your code needs
|
|
719
|
+
* `TSLA`, or when you need the `urlSlug` an entity's metric series is addressed by.
|
|
720
|
+
*
|
|
721
|
+
* Returns a bare array, not a `PreviewResponse` envelope, and an empty array is the
|
|
722
|
+
* normal answer for a query that matches nothing.
|
|
723
|
+
*
|
|
724
|
+
* @param q What the user typed. At least 2 characters, or the API answers 400.
|
|
725
|
+
*/
|
|
726
|
+
async searchEntities(q, options) {
|
|
727
|
+
return this.client.get("/api/v1/kb/entities/search", { q, ...options });
|
|
728
|
+
}
|
|
713
729
|
};
|
|
714
730
|
|
|
715
731
|
// src/resources/marketMood.ts
|
|
@@ -1226,7 +1242,7 @@ var Trackers = class {
|
|
|
1226
1242
|
};
|
|
1227
1243
|
|
|
1228
1244
|
// src/version.ts
|
|
1229
|
-
var VERSION = "0.
|
|
1245
|
+
var VERSION = "0.52.0";
|
|
1230
1246
|
|
|
1231
1247
|
// src/client.ts
|
|
1232
1248
|
var DEFAULT_BASE_URL = "https://app.sentisense.ai";
|