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 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 price fields |
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 on 429 and 5xx, honouring `Retry-After`. Set `0` to fail fast. |
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. Two of these declare a `data` that can be null: `stocks.getOptionsSummary`, for a ticker outside the covered options universe, and `options.getOverview`, before its first nightly build. Everywhere else `data` is an array or an object.
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 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.
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
- Screens read a snapshot that refreshes every 20 minutes, so this is not a quote feed. Use `client.stocks.getQuote()` for current quotes.
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
- // 429: quota exceeded
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 | Quota exceeded |
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.63.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(context.ivRank1y, 1)),
3617
- field("put/call vol", fixed(latest.pcVol, 2))
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(data.oiWalls?.maxPain, 0)),
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 price fields",
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.63.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) {