sentisense 0.63.0 → 0.65.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 +50 -10
- package/dist/cli.cjs +152 -14
- package/dist/index.cjs +43 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +220 -9
- package/dist/index.d.ts +220 -9
- package/dist/index.mjs +43 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,8 +79,8 @@ npx -y sentisense@latest health
|
|
|
79
79
|
| `congress [ticker]` | Congressional disclosures, market-wide or for one symbol |
|
|
80
80
|
| `news [ticker]` | Clustered news stories with impact and tone |
|
|
81
81
|
| `flows [ticker]` | Institutional 13F flows, or one ticker's holders and notable changes |
|
|
82
|
-
| `options <ticker>` | End-of-day options positioning, IV rank, walls, unusual contracts |
|
|
83
|
-
| `screen --filter ...` | Screen the universe on Score, analyst, technical, and
|
|
82
|
+
| `options <ticker>` | End-of-day options positioning, IV rank, walls, unusual contracts; `--intraday` for the session's 15-minute series (PRO) |
|
|
83
|
+
| `screen --filter ...` | Screen the universe on Score, analyst, technical, price, and end-of-day options fields |
|
|
84
84
|
| `search <name>` | Resolve a name, alias, ticker or slug to a symbol and the entity handle |
|
|
85
85
|
|
|
86
86
|
Run `sentisense help <command>` for its flags and examples.
|
|
@@ -157,7 +157,7 @@ const client = new SentiSense({
|
|
|
157
157
|
| `apiKey` | none | Sent as `X-SentiSense-API-Key`. Required by every endpoint. |
|
|
158
158
|
| `baseUrl` | `https://app.sentisense.ai` | Override for a non-production host. |
|
|
159
159
|
| `timeout` | `30000` | Per-request timeout in milliseconds. |
|
|
160
|
-
| `maxRetries` | `3` | Retries
|
|
160
|
+
| `maxRetries` | `3` | Retries the per-minute 429 and 5xx, honouring `Retry-After`. The monthly-allowance 429 is never retried. Set `0` to fail fast. |
|
|
161
161
|
| `userAgentSuffix` | none | Appended to the User-Agent, after `sentisense-node/{version}`. |
|
|
162
162
|
|
|
163
163
|
`userAgentSuffix` is how you say what is calling on top of the SDK, so your traffic is legible in your own logs and in ours. A tool name and version works (`"my-bot/1.4"`), optionally with an agent label (`"my-bot/1.4 agent/research-desk"`). Node only, since browsers set the header themselves. Newlines are collapsed and an empty value is ignored.
|
|
@@ -183,9 +183,9 @@ Affected: every method whose declared return type is `PreviewResponse<T>`. A tes
|
|
|
183
183
|
| `institutional` | `getFlows` `getHolders` `getActivists` |
|
|
184
184
|
| `options` | `getOverview` |
|
|
185
185
|
| `politicians` | `getActivity` `getFilings` `getMembers` `getMember` |
|
|
186
|
-
| `stocks` | `getSentiment` `getKpis` `getOptionsSummary` `getOptionsHistory` |
|
|
186
|
+
| `stocks` | `getSentiment` `getKpis` `getOptionsSummary` `getOptionsHistory` `getOptionsIntradayHistory` |
|
|
187
187
|
|
|
188
|
-
The envelope itself is always an object, so test the payload rather than the response.
|
|
188
|
+
The envelope itself is always an object, so test the payload rather than the response. Three of these declare a `data` that can be null: `stocks.getOptionsSummary`, for a ticker outside the covered options universe, `options.getOverview`, before its first nightly build, and `stocks.getOptionsIntradayHistory`, on a FREE key's `PRO_REQUIRED` preview. Everywhere else `data` is an array or an object.
|
|
189
189
|
|
|
190
190
|
```typescript
|
|
191
191
|
const flows = await client.institutional.getFlows();
|
|
@@ -244,6 +244,7 @@ client.stocks.getFloat("GME") // Free float
|
|
|
244
244
|
client.stocks.getGraph("AAPL", { depth: 1 }) // Company knowledge graph: people, products, peers
|
|
245
245
|
client.stocks.getOptionsSummary("NVDA") // End-of-day options dossier
|
|
246
246
|
client.stocks.getOptionsHistory("NVDA", { window: "2y" }) // Daily options aggregates over time
|
|
247
|
+
client.stocks.getOptionsIntradayHistory("NVDA") // One session's intraday options series (PRO)
|
|
247
248
|
client.stocks.getRating("AAPL") // SentiSense Rating: score, letter, percentile, dimensions
|
|
248
249
|
client.stocks.getAISummary("AAPL", { depth: "deep" }) // AI report (PRO)
|
|
249
250
|
```
|
|
@@ -521,11 +522,29 @@ End-of-day options positioning: where implied volatility, put/call flow and skew
|
|
|
521
522
|
client.options.getOverview() // Market-wide radar, ranked
|
|
522
523
|
client.stocks.getOptionsSummary("NVDA") // One name's full dossier
|
|
523
524
|
client.stocks.getOptionsHistory("NVDA", { window: "2y" }) // That name's daily series
|
|
525
|
+
client.stocks.getOptionsIntradayHistory("NVDA", { session: "2026-10-14" }) // Every 15-minute cycle of one session (PRO)
|
|
524
526
|
```
|
|
525
527
|
|
|
526
528
|
The radar carries two separately-ranked boards: `data.rows` for stocks and `data.etfRows` for ETFs. Keep them apart. Every reading behind a row's `interestScore` is a percentile of that ticker's own trailing history, so a ranking built across both boards compares numbers measured against different baselines. The aggregates split the same way, with the `etf`-prefixed fields describing the ETF board alone.
|
|
527
529
|
|
|
528
|
-
During the trading session the dossier and the radar also carry a few intraday fields, the same on every tier: `intradayFlow` (a count of the ticker's unusual contracts so far, when the first one was flagged, and the board's own clock), `largePrintCount` and `largestPrintPctl` on the dossier, and `intradayActiveCount` with `intradayRanking` on the radar. They carry no contract, strike, price or premium, and are absent before the day's first board. `capabilities.intradayBoard` says where the contract-level board lives; its rows are served in the SentiSense app, not by the API.
|
|
530
|
+
During the trading session the dossier and the radar also carry a few intraday fields, the same on every tier: `intradayFlow` (a count of the ticker's unusual contracts so far, when the first one was flagged, and the board's own clock), `largePrintCount` and `largestPrintPctl` on the dossier, and `intradayActiveCount` with `intradayRanking` on the radar. They carry no contract, strike, price or premium, and are absent before the day's first board. On the radar, `intradayAsOf` (epoch seconds) and `intradayAsOfEt` date the board the ranking was read from. `capabilities.intradayBoard` says where the contract-level board lives; its rows are served in the SentiSense app, not by the API.
|
|
531
|
+
|
|
532
|
+
`getOptionsIntradayHistory` keeps every cycle of that board for one ticker, so you can test an intraday signal against what the board showed at each point in the day and then compare it with the session's end-of-day row from `getOptionsHistory`:
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
const day = await client.stocks.getOptionsIntradayHistory("NVDA", { session: "2026-10-14" });
|
|
536
|
+
if (day.data === null) {
|
|
537
|
+
console.log(day.upgrade?.message); // PRO only: a FREE key gets a preview with no series
|
|
538
|
+
} else {
|
|
539
|
+
for (const point of day.data.series) { // oldest cycle first
|
|
540
|
+
console.log(point.asOfEt, point.callVol, point.putVol, point.pcVol, point.unusualCount, point.atmIv);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Each point carries call and put volume so far, the put/call ratio, the unusual-contract count, the percentile of the largest qualifying contract against the ticker's own year (`maxPremiumPctl1y`) and the ATM implied volatility; readings that are unavailable are absent, never zero. Omit `session` for the session the board ran most recently, and read `data.session` for the date served. A ticker appears only on cycles where it was on the board, so gaps show in each point's `cycle`, and `missingCycles` lists cycles whose stored point could not be read. A weekend, a holiday, a ticker outside the board and an unknown symbol all return an empty `series`, not a null payload. A malformed or future date rejects with an `APIError` whose `code` is `"invalid_session"`. From the CLI: `sentisense options NVDA --intraday`, or `--session YYYY-MM-DD` for another day.
|
|
546
|
+
|
|
547
|
+
A FREE key that has spent its ten monthly dossiers receives a headline preview from `getOptionsSummary`: `asOf`, `sentiment` and six flat readings, `ivRank1y`, `atmIv`, `expectedMove1d`, `pcVol`, `pcVolPctl1y` and `maxPain`, with no `latest`, `context` or `oiWalls`. On a full dossier those six are absent and the same readings live in the nested objects, so branch on `isPreview`.
|
|
529
548
|
|
|
530
549
|
A row whose baseline is still building carries its raw readings with the percentiles and `interestScore` omitted, which means "not enough history yet" rather than "nothing interesting". `getOptionsSummary` reports an uncovered ticker as a null payload inside the usual envelope, so the check is `result.data === null`: the response object itself is always truthy, and a bare `if (summary === null)` never fires. `getOptionsHistory` reports it as an empty `series` instead, so check the array's length rather than null-checking there.
|
|
531
550
|
|
|
@@ -578,7 +597,7 @@ An empty array is the normal answer for a query that matches nothing, so branch
|
|
|
578
597
|
|
|
579
598
|
### Screener
|
|
580
599
|
|
|
581
|
-
Filter the tracked universe on the SentiSense Score, attention, analyst consensus, technicals and
|
|
600
|
+
Filter the tracked universe on the SentiSense Score, attention, analyst consensus, technicals, price and end-of-day options readings 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.
|
|
582
601
|
|
|
583
602
|
```typescript
|
|
584
603
|
client.screener.fields() // Every filterable field, both universes, with units + operators
|
|
@@ -609,6 +628,18 @@ const res = await client.screener.run({
|
|
|
609
628
|
for (const row of res.results) {
|
|
610
629
|
console.log(row.ticker, row.sentiSenseScore7D, row.analystBuyRatioPct);
|
|
611
630
|
}
|
|
631
|
+
|
|
632
|
+
// Options positioning: implied volatility in the top quarter of its own year
|
|
633
|
+
const highIv = await client.screener.run({
|
|
634
|
+
plan: {
|
|
635
|
+
filters: [{ fieldName: "IV_RANK_1Y", op: "GTE", value: 75 }],
|
|
636
|
+
sort: { fieldName: "IV_RANK_1Y", dir: "DESC" },
|
|
637
|
+
},
|
|
638
|
+
limit: 25,
|
|
639
|
+
});
|
|
640
|
+
for (const row of highIv.results) {
|
|
641
|
+
console.log(row.ticker, row.ivRank1y, row.expectedMove20d, row.optionsAsOf);
|
|
642
|
+
}
|
|
612
643
|
```
|
|
613
644
|
|
|
614
645
|
`limit` rides next to the plan rather than inside it, because a plan is a stored object and paging is a transport concern. It defaults to 100 and caps at 500. `matched` is the count before `limit` was applied, so truncation is visible. `tickers` is optional: omit it to screen the whole tracked universe, pass a list to screen a watchlist.
|
|
@@ -623,7 +654,11 @@ The Score fields (`SENTI_SCORE_7D`, `SENTI_SCORE_1M`, `SCORE_CHANGE_7D`) are the
|
|
|
623
654
|
|
|
624
655
|
On the ETF side, `CONSTITUENTS_WEIGHTED_SENTISENSE` is the holdings-weighted Score across what the fund owns and is usually the one you want; `DIRECT_SENTISENSE` is the Score from chatter about the fund ticker itself. `WEIGHT_COVERED_PCT` tells you how much of the fund's weight had constituent data behind the weighted number.
|
|
625
656
|
|
|
626
|
-
|
|
657
|
+
Stock rows also carry seven end-of-day options readings, `ivRank1y`, `pcVol`, `pcVolPctl1y`, `skew25dPctl1y`, `optionsInterestScore`, `maxUnusualPremium` and `expectedMove20d`, plus `optionsAsOf`, the session date they are from. `expectedMove20d` is a fraction of price (`0.2` is 20%) and `optionsAsOf` is a `YYYY-MM-DD` date; filter on the options fields with names like `IV_RANK_1Y`.
|
|
658
|
+
|
|
659
|
+
Every stock row carries `priceAsOf`, when the market data behind `currentPrice`, `change` and `changePercent` is from, in epoch milliseconds (the same unit as the quote endpoints). It is `null` outside regular hours, on a previous-close fallback and when no quote was available, so read `null` as unknown rather than fresh, and judge a price's age from `priceAsOf`, not `lastUpdated`. Rated stocks also carry `ratingLetter`, `ratingScore` and `ratingPercentile`; all three are `null` for an unrated one.
|
|
660
|
+
|
|
661
|
+
Prices are 15-minute delayed quotes, refreshed at request time during the regular session from a cache at most 5 minutes old. Everything else on a row comes from a snapshot rebuilt about every 20 minutes during the session, so this is not a quote feed: use `client.stocks.getQuote()` for one ticker's current quote.
|
|
627
662
|
|
|
628
663
|
## Error handling
|
|
629
664
|
|
|
@@ -636,7 +671,12 @@ try {
|
|
|
636
671
|
if (error instanceof AuthenticationError) {
|
|
637
672
|
// 401 or 403: invalid/missing API key or insufficient tier
|
|
638
673
|
} else if (error instanceof RateLimitError) {
|
|
639
|
-
|
|
674
|
+
if (error.code === "quota_exceeded") {
|
|
675
|
+
// 429: monthly allowance used up. Not retried; error.message says when it resets.
|
|
676
|
+
} else {
|
|
677
|
+
// 429: per-minute limit ("rate_limit_exceeded"), thrown once retries are used up.
|
|
678
|
+
// error.retryAfter is the server's wait in seconds.
|
|
679
|
+
}
|
|
640
680
|
}
|
|
641
681
|
}
|
|
642
682
|
```
|
|
@@ -645,7 +685,7 @@ try {
|
|
|
645
685
|
|------------|-------------|------|
|
|
646
686
|
| `AuthenticationError` | 401, 403 | Invalid API key or insufficient tier |
|
|
647
687
|
| `NotFoundError` | 404 | Resource not found |
|
|
648
|
-
| `RateLimitError` | 429 |
|
|
688
|
+
| `RateLimitError` | 429 | Per-minute limit (`code: "rate_limit_exceeded"`, retried first) or monthly allowance used up (`code: "quota_exceeded"`, thrown immediately) |
|
|
649
689
|
| `APIError` | Other 4xx/5xx | General API error |
|
|
650
690
|
|
|
651
691
|
All errors extend `SentiSenseError` and include `status`, `code`, and `message` properties.
|
package/dist/cli.cjs
CHANGED
|
@@ -66,10 +66,12 @@ var EXIT_TABLE = [
|
|
|
66
66
|
[EXIT.USAGE, "bad usage, caught before any request was sent"],
|
|
67
67
|
[EXIT.AUTH, "missing or rejected API key"],
|
|
68
68
|
[EXIT.NOT_FOUND, "no data for that symbol or identifier"],
|
|
69
|
-
[EXIT.RATE_LIMIT, "rate limited"],
|
|
69
|
+
[EXIT.RATE_LIMIT, "rate limited, or the monthly request allowance is used up"],
|
|
70
70
|
[EXIT.NETWORK, "network failure or timeout"]
|
|
71
71
|
];
|
|
72
72
|
var KEY_URL = "https://app.sentisense.ai/get-api-key";
|
|
73
|
+
var PRICING_URL = "https://app.sentisense.ai/pricing";
|
|
74
|
+
var QUOTA_EXCEEDED = "quota_exceeded";
|
|
73
75
|
var CliUsageError = class extends Error {
|
|
74
76
|
constructor(message, hint) {
|
|
75
77
|
super(message);
|
|
@@ -158,6 +160,15 @@ function classify(error, command) {
|
|
|
158
160
|
};
|
|
159
161
|
}
|
|
160
162
|
if (error instanceof RateLimitError) {
|
|
163
|
+
if (error.code === QUOTA_EXCEEDED) {
|
|
164
|
+
return {
|
|
165
|
+
exitCode: EXIT.RATE_LIMIT,
|
|
166
|
+
lines: [
|
|
167
|
+
`error: monthly request allowance used up: ${error.message}`,
|
|
168
|
+
`next: the free monthly allowance resets at the start of next month, so running it again before then will not help. PRO has no monthly cap: ${PRICING_URL}`
|
|
169
|
+
]
|
|
170
|
+
};
|
|
171
|
+
}
|
|
161
172
|
const wait = error.retryAfter ? Math.ceil(error.retryAfter) : 60;
|
|
162
173
|
return {
|
|
163
174
|
exitCode: EXIT.RATE_LIMIT,
|
|
@@ -1345,7 +1356,7 @@ var flowsCommand = {
|
|
|
1345
1356
|
};
|
|
1346
1357
|
|
|
1347
1358
|
// src/version.ts
|
|
1348
|
-
var VERSION = "0.
|
|
1359
|
+
var VERSION = "0.65.0";
|
|
1349
1360
|
|
|
1350
1361
|
// src/resources/analyst.ts
|
|
1351
1362
|
var Analyst = class {
|
|
@@ -2507,7 +2518,9 @@ var Stocks = class {
|
|
|
2507
2518
|
*
|
|
2508
2519
|
* Tiering: a PRO key always receives the full dossier. A FREE key receives it for the first
|
|
2509
2520
|
* ten calls each calendar month and a headline-only preview after that, with `isPreview`
|
|
2510
|
-
* true; calls that return a null `data` never spend that allowance.
|
|
2521
|
+
* true; calls that return a null `data` never spend that allowance. The headline preview has
|
|
2522
|
+
* no nested objects: its readings arrive flat as `data.ivRank1y`, `data.atmIv`,
|
|
2523
|
+
* `data.expectedMove1d`, `data.pcVol`, `data.pcVolPctl1y` and `data.maxPain`.
|
|
2511
2524
|
*/
|
|
2512
2525
|
async getOptionsSummary(ticker) {
|
|
2513
2526
|
return this.client.get(
|
|
@@ -2537,6 +2550,33 @@ var Stocks = class {
|
|
|
2537
2550
|
options
|
|
2538
2551
|
);
|
|
2539
2552
|
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Get one stock's or ETF's intraday options series for one session: its numbers at every
|
|
2555
|
+
* 15-minute cycle of the intraday board, oldest cycle first.
|
|
2556
|
+
*
|
|
2557
|
+
* Use it to test an intraday signal against what the board actually showed at each point in
|
|
2558
|
+
* the day, then compare it with the session's end-of-day row from
|
|
2559
|
+
* {@link Stocks.getOptionsHistory}. Each point carries call and put volume so far, the
|
|
2560
|
+
* put/call ratio, the unusual-contract count, the percentile of the largest qualifying
|
|
2561
|
+
* contract against the ticker's own year, and the ATM implied volatility. It never carries a
|
|
2562
|
+
* contract symbol, strike, price, premium, open interest or print size.
|
|
2563
|
+
*
|
|
2564
|
+
* **PRO only.** A FREE key receives the standard preview: `isPreview` true, `previewReason`
|
|
2565
|
+
* `"PRO_REQUIRED"`, an `upgrade` hint, and `data: null`.
|
|
2566
|
+
*
|
|
2567
|
+
* **No data is an empty `series`, not a null payload.** A weekend, a holiday, a session from
|
|
2568
|
+
* before the history was kept, a ticker outside the board and an unknown symbol all answer
|
|
2569
|
+
* with `data.series` empty. `data.missingCycles` lists cycles whose stored point could not be
|
|
2570
|
+
* read, which are unknown rather than absent. A malformed or future `session` rejects with
|
|
2571
|
+
* an `APIError` (status 400, code `invalid_session`). A 503 means the stored series could
|
|
2572
|
+
* not be read; it is retried like any other 503 and never answered with a partial series.
|
|
2573
|
+
*/
|
|
2574
|
+
async getOptionsIntradayHistory(ticker, options) {
|
|
2575
|
+
return this.client.get(
|
|
2576
|
+
`/api/v1/stocks/${encodeURIComponent(ticker.toUpperCase())}/options/intraday-history`,
|
|
2577
|
+
options
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2540
2580
|
/**
|
|
2541
2581
|
* Get the SentiSense Rating for one stock: where it ranks against the other stocks rated
|
|
2542
2582
|
* that day, and the six dimensions the rank is blended from.
|
|
@@ -2668,6 +2708,15 @@ var DEEP_HISTORY_FALLBACK_WAIT_S = 3;
|
|
|
2668
2708
|
var MAX_DEEP_HISTORY_WAIT_S = 30;
|
|
2669
2709
|
var MAX_RATE_LIMIT_WAIT_S = 120;
|
|
2670
2710
|
var RATE_LIMIT_FALLBACK_WAIT_S = 60;
|
|
2711
|
+
var QUOTA_EXCEEDED_CODE = "quota_exceeded";
|
|
2712
|
+
async function isQuotaExceeded(response) {
|
|
2713
|
+
try {
|
|
2714
|
+
const body = await response.clone().json();
|
|
2715
|
+
return body?.error === QUOTA_EXCEEDED_CODE;
|
|
2716
|
+
} catch {
|
|
2717
|
+
return false;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2671
2720
|
function clampRetryAfter(raw, maxWaitS) {
|
|
2672
2721
|
if (!raw) return void 0;
|
|
2673
2722
|
const parsed = Number(raw);
|
|
@@ -2753,6 +2802,9 @@ var SentiSense = class {
|
|
|
2753
2802
|
);
|
|
2754
2803
|
}
|
|
2755
2804
|
if (!response.ok) {
|
|
2805
|
+
if (response.status === 429 && await isQuotaExceeded(response)) {
|
|
2806
|
+
await this.handleErrorResponse(response);
|
|
2807
|
+
}
|
|
2756
2808
|
const isRetryable = response.status === 429 || response.status >= 500;
|
|
2757
2809
|
if (isRetryable && attempt < this.maxRetries) {
|
|
2758
2810
|
if (response.status === 429 || response.status === 503) {
|
|
@@ -3561,6 +3613,7 @@ var newsCommand = {
|
|
|
3561
3613
|
};
|
|
3562
3614
|
|
|
3563
3615
|
// src/cli/commands/options.ts
|
|
3616
|
+
var SESSION_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
3564
3617
|
function contractTone(type) {
|
|
3565
3618
|
const side = type?.toUpperCase();
|
|
3566
3619
|
if (side === "CALL") return "up";
|
|
@@ -3571,14 +3624,79 @@ function walls(rows) {
|
|
|
3571
3624
|
if (!rows || rows.length === 0) return "n/a";
|
|
3572
3625
|
return rows.map((wall) => `${fixed(wall.strike, 0)} (${humanize(wall.oi, 1)})`).join(" ");
|
|
3573
3626
|
}
|
|
3627
|
+
async function intraday(api, ticker, session) {
|
|
3628
|
+
const envelope = await api.stocks.getOptionsIntradayHistory(ticker, { session });
|
|
3629
|
+
const data = envelope.data;
|
|
3630
|
+
if (envelope.isPreview && !data) {
|
|
3631
|
+
const lines = [
|
|
3632
|
+
{ kind: "text", text: `Intraday options history for ${ticker} needs a PRO key.` }
|
|
3633
|
+
];
|
|
3634
|
+
if (envelope.upgrade?.url) {
|
|
3635
|
+
lines.push({ kind: "text", text: envelope.upgrade.url, tone: "dim" });
|
|
3636
|
+
}
|
|
3637
|
+
return { json: envelope, doc: doc(...lines) };
|
|
3638
|
+
}
|
|
3639
|
+
const series = data?.series ?? [];
|
|
3640
|
+
const served = data?.session ?? session ?? "the latest session";
|
|
3641
|
+
if (series.length === 0) {
|
|
3642
|
+
const note = await verifyTickerOnEmpty(api, ticker);
|
|
3643
|
+
return {
|
|
3644
|
+
json: envelope,
|
|
3645
|
+
doc: doc({ kind: "text", text: `No intraday options history for ${ticker} on ${served}.` }),
|
|
3646
|
+
notes: note ? [note] : void 0
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
const blocks = [
|
|
3650
|
+
{
|
|
3651
|
+
kind: "head",
|
|
3652
|
+
title: field("ticker", ticker),
|
|
3653
|
+
right: fields(
|
|
3654
|
+
field("session", served),
|
|
3655
|
+
field("board", data?.live ? "live" : "closed", data?.live ? "accent" : void 0),
|
|
3656
|
+
field("cycles", String(data?.cycles ?? series.length))
|
|
3657
|
+
)
|
|
3658
|
+
},
|
|
3659
|
+
{
|
|
3660
|
+
kind: "table",
|
|
3661
|
+
head: ["CYCLE", "TIME", "CALL VOL", "PUT VOL", "P/C", "UNUSUAL", "PCTL 1Y", "ATM IV"],
|
|
3662
|
+
align: ["right", "left", "right", "right", "right", "right", "right", "right"],
|
|
3663
|
+
rows: series.map((point) => [
|
|
3664
|
+
cell(String(point.cycle)),
|
|
3665
|
+
cell(point.asOfEt ?? ""),
|
|
3666
|
+
cell(humanize(point.callVol, 1)),
|
|
3667
|
+
cell(humanize(point.putVol, 1)),
|
|
3668
|
+
cell(fixed(point.pcVol, 2)),
|
|
3669
|
+
cell(String(point.unusualCount ?? 0)),
|
|
3670
|
+
cell(fixed(point.maxPremiumPctl1y, 1)),
|
|
3671
|
+
cell(fixed(point.atmIv, 4))
|
|
3672
|
+
])
|
|
3673
|
+
}
|
|
3674
|
+
];
|
|
3675
|
+
const missing = data?.missingCycles ?? [];
|
|
3676
|
+
if (missing.length > 0) {
|
|
3677
|
+
blocks.push({
|
|
3678
|
+
kind: "text",
|
|
3679
|
+
text: `${missing.length === 1 ? "Cycle" : "Cycles"} ${missing.join(", ")} ${missing.length === 1 ? "was" : "were"} published but could not be read, so ${ticker}'s numbers there are unknown.`,
|
|
3680
|
+
tone: "dim"
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3683
|
+
blocks.push({
|
|
3684
|
+
kind: "text",
|
|
3685
|
+
text: `Volumes are cumulative for the session, from chains delayed ${data?.delayMinutes ?? 15} minutes.`,
|
|
3686
|
+
tone: "dim"
|
|
3687
|
+
});
|
|
3688
|
+
return { json: envelope, doc: doc(...blocks) };
|
|
3689
|
+
}
|
|
3574
3690
|
var optionsCommand = {
|
|
3575
3691
|
name: "options",
|
|
3576
3692
|
summary: "End-of-day options positioning for one stock or ETF",
|
|
3577
|
-
usage: "sentisense options <ticker>",
|
|
3693
|
+
usage: "sentisense options <ticker> [--intraday] [--session YYYY-MM-DD]",
|
|
3578
3694
|
examples: [
|
|
3579
3695
|
"sentisense options NVDA",
|
|
3580
3696
|
"sentisense options SPY --full",
|
|
3581
|
-
"sentisense options NVDA --json"
|
|
3697
|
+
"sentisense options NVDA --json",
|
|
3698
|
+
"sentisense options NVDA --intraday",
|
|
3699
|
+
"sentisense options SPY --session 2026-10-14"
|
|
3582
3700
|
],
|
|
3583
3701
|
notes: [
|
|
3584
3702
|
"End of day, not live: readings describe the latest completed session and refresh the",
|
|
@@ -3589,11 +3707,28 @@ var optionsCommand = {
|
|
|
3589
3707
|
EMPTY_VERIFY_NOTE,
|
|
3590
3708
|
EMPTY_VERIFY_NOTE_2,
|
|
3591
3709
|
"A free key gets the full dossier for the first ten calls each month, then a headline",
|
|
3592
|
-
"preview. Calls that return no dossier do not count against that."
|
|
3710
|
+
"preview. Calls that return no dossier do not count against that.",
|
|
3711
|
+
"--intraday shows one session's intraday series instead: the ticker's volumes, put/call",
|
|
3712
|
+
"ratio, unusual-contract count and ATM IV at every 15-minute cycle (PRO keys only).",
|
|
3713
|
+
"--session picks the session and implies --intraday; omitted, the latest one."
|
|
3593
3714
|
],
|
|
3594
|
-
flags: {
|
|
3715
|
+
flags: {
|
|
3716
|
+
intraday: { type: "boolean", describe: "Show the session's intraday series, every 15-minute cycle (PRO)" },
|
|
3717
|
+
session: {
|
|
3718
|
+
type: "string",
|
|
3719
|
+
placeholder: "YYYY-MM-DD",
|
|
3720
|
+
describe: "Session for the intraday series (default: the latest)"
|
|
3721
|
+
}
|
|
3722
|
+
},
|
|
3595
3723
|
async run({ args, client, full }) {
|
|
3596
3724
|
const ticker = oneTicker(args, "options");
|
|
3725
|
+
const session = typeof args.flags.session === "string" ? args.flags.session : void 0;
|
|
3726
|
+
if (session !== void 0 && !SESSION_PATTERN.test(session)) {
|
|
3727
|
+
throw new CliUsageError("--session must be a date, YYYY-MM-DD.", "for example: --session 2026-10-14");
|
|
3728
|
+
}
|
|
3729
|
+
if (args.flags.intraday === true || session !== void 0) {
|
|
3730
|
+
return intraday(client(), ticker, session);
|
|
3731
|
+
}
|
|
3597
3732
|
const api = client();
|
|
3598
3733
|
const envelope = await api.stocks.getOptionsSummary(ticker);
|
|
3599
3734
|
const data = envelope.data;
|
|
@@ -3607,21 +3742,24 @@ var optionsCommand = {
|
|
|
3607
3742
|
}
|
|
3608
3743
|
const latest = data.latest ?? {};
|
|
3609
3744
|
const context = data.context ?? {};
|
|
3745
|
+
const ivRank = context.ivRank1y ?? data.ivRank1y;
|
|
3746
|
+
const pcVol = latest.pcVol ?? data.pcVol;
|
|
3747
|
+
const maxPain = data.oiWalls?.maxPain ?? data.maxPain;
|
|
3610
3748
|
const blocks = [
|
|
3611
3749
|
{
|
|
3612
3750
|
kind: "head",
|
|
3613
3751
|
title: field("ticker", ticker),
|
|
3614
3752
|
right: fields(
|
|
3615
3753
|
field("positioning", signed(data.sentiment, 2), direction(data.sentiment)),
|
|
3616
|
-
field("iv rank 1y", fixed(
|
|
3617
|
-
field("put/call vol", fixed(
|
|
3754
|
+
field("iv rank 1y", fixed(ivRank, 1)),
|
|
3755
|
+
field("put/call vol", fixed(pcVol, 2))
|
|
3618
3756
|
)
|
|
3619
3757
|
},
|
|
3620
3758
|
{
|
|
3621
3759
|
kind: "facts",
|
|
3622
3760
|
items: fields(
|
|
3623
|
-
field("ATM IV", fixed(latest.atmIv, 4)),
|
|
3624
|
-
field("Exp move 1d", fixed(latest.expectedMove1d, 4)),
|
|
3761
|
+
field("ATM IV", fixed(latest.atmIv ?? data.atmIv, 4)),
|
|
3762
|
+
field("Exp move 1d", fixed(latest.expectedMove1d ?? data.expectedMove1d, 4)),
|
|
3625
3763
|
field("Skew 25d", signed(latest.skew25d, 4)),
|
|
3626
3764
|
field("Call vol", humanize(latest.callVol, 1)),
|
|
3627
3765
|
field("Put vol", humanize(latest.putVol, 1)),
|
|
@@ -3631,7 +3769,7 @@ var optionsCommand = {
|
|
|
3631
3769
|
{
|
|
3632
3770
|
kind: "facts",
|
|
3633
3771
|
items: fields(
|
|
3634
|
-
field("Max pain", fixed(
|
|
3772
|
+
field("Max pain", fixed(maxPain, 0)),
|
|
3635
3773
|
field("Wall expiry", data.oiWalls?.expiry ?? "n/a"),
|
|
3636
3774
|
field("Call OI", humanize(latest.callOi, 1)),
|
|
3637
3775
|
field("Put OI", humanize(latest.putOi, 1)),
|
|
@@ -3653,7 +3791,7 @@ var optionsCommand = {
|
|
|
3653
3791
|
field("term structure", `60d ${fixed(latest.atmIv60, 4)} 90d ${fixed(latest.atmIv90, 4)}`),
|
|
3654
3792
|
field("net delta", humanize(latest.netDelta, 1)),
|
|
3655
3793
|
field("put/call OI", fixed(latest.pcOi, 2)),
|
|
3656
|
-
field("pc vol percentile 1y", fixed(context.pcVolPctl1y, 1)),
|
|
3794
|
+
field("pc vol percentile 1y", fixed(context.pcVolPctl1y ?? data.pcVolPctl1y, 1)),
|
|
3657
3795
|
field("skew percentile 1y", fixed(context.skewPctl1y, 1)),
|
|
3658
3796
|
field("observations 1y", fixed(context.observations1y, 0))
|
|
3659
3797
|
)
|
|
@@ -3924,7 +4062,7 @@ function parseSort(raw) {
|
|
|
3924
4062
|
}
|
|
3925
4063
|
var screenCommand = {
|
|
3926
4064
|
name: "screen",
|
|
3927
|
-
summary: "Filter the tracked universe on Score, analyst, technical, and
|
|
4065
|
+
summary: "Filter the tracked universe on Score, analyst, technical, price, and end-of-day options fields",
|
|
3928
4066
|
usage: "sentisense screen [--filter FIELD:OP:VALUE]... [--sort FIELD:DIR] [--limit N]",
|
|
3929
4067
|
examples: [
|
|
3930
4068
|
"sentisense screen --list",
|
package/dist/index.cjs
CHANGED
|
@@ -1242,7 +1242,9 @@ var Stocks = class {
|
|
|
1242
1242
|
*
|
|
1243
1243
|
* Tiering: a PRO key always receives the full dossier. A FREE key receives it for the first
|
|
1244
1244
|
* ten calls each calendar month and a headline-only preview after that, with `isPreview`
|
|
1245
|
-
* true; calls that return a null `data` never spend that allowance.
|
|
1245
|
+
* true; calls that return a null `data` never spend that allowance. The headline preview has
|
|
1246
|
+
* no nested objects: its readings arrive flat as `data.ivRank1y`, `data.atmIv`,
|
|
1247
|
+
* `data.expectedMove1d`, `data.pcVol`, `data.pcVolPctl1y` and `data.maxPain`.
|
|
1246
1248
|
*/
|
|
1247
1249
|
async getOptionsSummary(ticker) {
|
|
1248
1250
|
return this.client.get(
|
|
@@ -1272,6 +1274,33 @@ var Stocks = class {
|
|
|
1272
1274
|
options
|
|
1273
1275
|
);
|
|
1274
1276
|
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Get one stock's or ETF's intraday options series for one session: its numbers at every
|
|
1279
|
+
* 15-minute cycle of the intraday board, oldest cycle first.
|
|
1280
|
+
*
|
|
1281
|
+
* Use it to test an intraday signal against what the board actually showed at each point in
|
|
1282
|
+
* the day, then compare it with the session's end-of-day row from
|
|
1283
|
+
* {@link Stocks.getOptionsHistory}. Each point carries call and put volume so far, the
|
|
1284
|
+
* put/call ratio, the unusual-contract count, the percentile of the largest qualifying
|
|
1285
|
+
* contract against the ticker's own year, and the ATM implied volatility. It never carries a
|
|
1286
|
+
* contract symbol, strike, price, premium, open interest or print size.
|
|
1287
|
+
*
|
|
1288
|
+
* **PRO only.** A FREE key receives the standard preview: `isPreview` true, `previewReason`
|
|
1289
|
+
* `"PRO_REQUIRED"`, an `upgrade` hint, and `data: null`.
|
|
1290
|
+
*
|
|
1291
|
+
* **No data is an empty `series`, not a null payload.** A weekend, a holiday, a session from
|
|
1292
|
+
* before the history was kept, a ticker outside the board and an unknown symbol all answer
|
|
1293
|
+
* with `data.series` empty. `data.missingCycles` lists cycles whose stored point could not be
|
|
1294
|
+
* read, which are unknown rather than absent. A malformed or future `session` rejects with
|
|
1295
|
+
* an `APIError` (status 400, code `invalid_session`). A 503 means the stored series could
|
|
1296
|
+
* not be read; it is retried like any other 503 and never answered with a partial series.
|
|
1297
|
+
*/
|
|
1298
|
+
async getOptionsIntradayHistory(ticker, options) {
|
|
1299
|
+
return this.client.get(
|
|
1300
|
+
`/api/v1/stocks/${encodeURIComponent(ticker.toUpperCase())}/options/intraday-history`,
|
|
1301
|
+
options
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1275
1304
|
/**
|
|
1276
1305
|
* Get the SentiSense Rating for one stock: where it ranks against the other stocks rated
|
|
1277
1306
|
* that day, and the six dimensions the rank is blended from.
|
|
@@ -1394,7 +1423,7 @@ var Trackers = class {
|
|
|
1394
1423
|
};
|
|
1395
1424
|
|
|
1396
1425
|
// src/version.ts
|
|
1397
|
-
var VERSION = "0.
|
|
1426
|
+
var VERSION = "0.65.0";
|
|
1398
1427
|
|
|
1399
1428
|
// src/client.ts
|
|
1400
1429
|
var DEFAULT_BASE_URL = "https://app.sentisense.ai";
|
|
@@ -1406,6 +1435,15 @@ var DEEP_HISTORY_FALLBACK_WAIT_S = 3;
|
|
|
1406
1435
|
var MAX_DEEP_HISTORY_WAIT_S = 30;
|
|
1407
1436
|
var MAX_RATE_LIMIT_WAIT_S = 120;
|
|
1408
1437
|
var RATE_LIMIT_FALLBACK_WAIT_S = 60;
|
|
1438
|
+
var QUOTA_EXCEEDED_CODE = "quota_exceeded";
|
|
1439
|
+
async function isQuotaExceeded(response) {
|
|
1440
|
+
try {
|
|
1441
|
+
const body = await response.clone().json();
|
|
1442
|
+
return body?.error === QUOTA_EXCEEDED_CODE;
|
|
1443
|
+
} catch {
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1409
1447
|
function clampRetryAfter(raw, maxWaitS) {
|
|
1410
1448
|
if (!raw) return void 0;
|
|
1411
1449
|
const parsed = Number(raw);
|
|
@@ -1491,6 +1529,9 @@ var SentiSense = class {
|
|
|
1491
1529
|
);
|
|
1492
1530
|
}
|
|
1493
1531
|
if (!response.ok) {
|
|
1532
|
+
if (response.status === 429 && await isQuotaExceeded(response)) {
|
|
1533
|
+
await this.handleErrorResponse(response);
|
|
1534
|
+
}
|
|
1494
1535
|
const isRetryable = response.status === 429 || response.status >= 500;
|
|
1495
1536
|
if (isRetryable && attempt < this.maxRetries) {
|
|
1496
1537
|
if (response.status === 429 || response.status === 503) {
|