@gloom-sh/terminal 0.18.5 → 0.19.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.
Files changed (436) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +140 -7
  3. package/src/api-client/cot.ts +6 -2
  4. package/src/api-client/credit-documents.ts +6 -0
  5. package/src/api-client/data.ts +17 -6
  6. package/src/api-client/equity-screener.ts +24 -0
  7. package/src/api-client/fund-facts.ts +45 -4
  8. package/src/api-client/index.ts +11 -0
  9. package/src/api-client/market-discovery.ts +10 -0
  10. package/src/api-client/market-movers.ts +7 -0
  11. package/src/api-client/members.ts +1 -1
  12. package/src/api-client/perps.ts +43 -0
  13. package/src/api-client/research-activity.ts +3 -2
  14. package/src/api-client/types.ts +70 -2
  15. package/src/app/pane-runtime/plugin-bindings.ts +9 -5
  16. package/src/app/runtime/broker-import.ts +0 -2
  17. package/src/app/runtime/plugin-bindings.ts +73 -56
  18. package/src/architecture/pane-tabs-allowlist.json +3 -0
  19. package/src/architecture/ttl-caches-allowlist.json +19 -0
  20. package/src/brokers/sync-broker-instance.ts +2 -8
  21. package/src/capabilities/chart-series.ts +1 -1
  22. package/src/cli/commands/crypto-hints.ts +40 -24
  23. package/src/cli/commands/market.ts +72 -26
  24. package/src/cli/commands/options-chain.ts +48 -7
  25. package/src/cli/commands/options-leaps.ts +9 -3
  26. package/src/cli/commands/overview.ts +61 -12
  27. package/src/cli/commands/search.ts +21 -1
  28. package/src/cli/commands/themes.ts +11 -2
  29. package/src/cli/commands/ticker.ts +55 -34
  30. package/src/cli/company-name-hint.ts +41 -0
  31. package/src/cli/earnings-calendar.ts +1 -1
  32. package/src/cli/errors.ts +8 -0
  33. package/src/cli/glossary.ts +25 -2
  34. package/src/cli/history-rows.ts +57 -9
  35. package/src/cli/index.ts +2 -0
  36. package/src/cli/listing-arg.ts +180 -52
  37. package/src/cli/output-file.ts +68 -0
  38. package/src/cli/pane-functions/capabilities.ts +3 -0
  39. package/src/cli/pane-functions/catalog.ts +80 -7
  40. package/src/cli/pane-functions/dom.ts +13 -3
  41. package/src/cli/pane-functions/freshness.ts +86 -3
  42. package/src/cli/pane-functions/function-help.ts +13 -10
  43. package/src/cli/pane-functions/headless.ts +17 -7
  44. package/src/cli/pane-functions/index.ts +49 -14
  45. package/src/cli/pane-functions/market-session.ts +66 -4
  46. package/src/cli/pane-functions/options.ts +19 -1
  47. package/src/cli/pane-functions/resolver.ts +19 -8
  48. package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
  49. package/src/cli/pane-functions/screenshot-types.ts +5 -0
  50. package/src/cli/quote-answer.ts +22 -11
  51. package/src/cli/ticker-resolution.ts +3 -2
  52. package/src/components/chart/composite/composite-chart.tsx +4 -0
  53. package/src/components/chart/composite/interactions.ts +6 -0
  54. package/src/components/chart/composite/legend.tsx +45 -7
  55. package/src/components/chart/composite/panel-gestures.ts +33 -0
  56. package/src/components/chart/composite/panel-surface.tsx +52 -3
  57. package/src/components/chart/composite/rasterizer.ts +24 -13
  58. package/src/components/chart/composite/scene.ts +39 -4
  59. package/src/components/chart/composite/series-cues.ts +14 -0
  60. package/src/components/chart/composite/text-renderer.ts +9 -2
  61. package/src/components/chart/composite/tool-catalog.ts +36 -0
  62. package/src/components/chart/composite/tools.ts +203 -44
  63. package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
  64. package/src/components/command-bar/assist/inventory.ts +2 -1
  65. package/src/components/command-bar/commands/direct/index.ts +28 -13
  66. package/src/components/command-bar/pane-templates/items.ts +3 -2
  67. package/src/components/command-bar/workflow/collection-actions.ts +3 -3
  68. package/src/components/onboarding/onboarding-wizard.tsx +4 -1
  69. package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
  70. package/src/components/onboarding/wizard-broker-sync.ts +0 -1
  71. package/src/components/onboarding/wizard-positions.ts +3 -3
  72. package/src/components/ticker/list-table-view.tsx +4 -1
  73. package/src/components/ui/choice-dialog.tsx +23 -18
  74. package/src/components/ui/choice-groups.ts +18 -0
  75. package/src/components/ui/data-table/opentui/index.tsx +9 -7
  76. package/src/components/ui/data-table/opentui/model.ts +26 -1
  77. package/src/components/ui/data-table/types.ts +2 -0
  78. package/src/components/ui/list-view.tsx +8 -4
  79. package/src/components/ui/query-bar-keys.tsx +2 -0
  80. package/src/components/ui/query-bar.tsx +1 -0
  81. package/src/components/ui/select-button.tsx +5 -1
  82. package/src/components/ui/select-field.tsx +1 -0
  83. package/src/components/ui/table-layout.ts +8 -5
  84. package/src/core/app-runtime.ts +2 -0
  85. package/src/core/app-services.ts +2 -2
  86. package/src/data/config/chart-settings.ts +1 -1
  87. package/src/data/json-ticker-repository.ts +17 -8
  88. package/src/data/ticker-repository.ts +16 -9
  89. package/src/data/ticker-writes.ts +81 -0
  90. package/src/data/ttl-cache.ts +165 -0
  91. package/src/docs/snapshot.json +71 -137
  92. package/src/i18n/es.ts +18 -2
  93. package/src/i18n/ja.ts +18 -2
  94. package/src/i18n/ko.ts +18 -2
  95. package/src/i18n/zh-cn.ts +18 -2
  96. package/src/i18n/zh-tw.ts +18 -2
  97. package/src/market-data/depositary-pair.ts +2 -2
  98. package/src/market-data/fund-facts.ts +50 -0
  99. package/src/market-data/market/change-basis.ts +16 -3
  100. package/src/market-data/market/format.ts +25 -0
  101. package/src/market-data/market/freshness.ts +2 -1
  102. package/src/market-data/options-alternatives.ts +81 -4
  103. package/src/news/aggregator.ts +113 -9
  104. package/src/news/cloud-session.ts +21 -0
  105. package/src/news/hooks.ts +1 -0
  106. package/src/news/news-model.ts +1 -0
  107. package/src/news/types.ts +11 -0
  108. package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
  109. package/src/plugins/builtin/alerts/alert-log.ts +33 -0
  110. package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
  111. package/src/plugins/builtin/alerts/cli.ts +336 -77
  112. package/src/plugins/builtin/alerts/command.ts +25 -1
  113. package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
  114. package/src/plugins/builtin/alerts/fired-log.ts +73 -9
  115. package/src/plugins/builtin/alerts/format.ts +75 -5
  116. package/src/plugins/builtin/alerts/headless.ts +24 -6
  117. package/src/plugins/builtin/alerts/history-rows.ts +3 -2
  118. package/src/plugins/builtin/alerts/history.ts +1 -1
  119. package/src/plugins/builtin/alerts/index.tsx +79 -16
  120. package/src/plugins/builtin/alerts/metric-command.ts +77 -4
  121. package/src/plugins/builtin/alerts/metric-data.ts +54 -13
  122. package/src/plugins/builtin/alerts/metric-model.ts +163 -14
  123. package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
  124. package/src/plugins/builtin/alerts/pane.tsx +61 -23
  125. package/src/plugins/builtin/alerts/quotes.ts +6 -0
  126. package/src/plugins/builtin/alerts/storage.ts +12 -8
  127. package/src/plugins/builtin/alerts/types.ts +4 -0
  128. package/src/plugins/builtin/analytics/index.tsx +2 -1
  129. package/src/plugins/builtin/analytics/risk-client.ts +16 -5
  130. package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
  131. package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
  132. package/src/plugins/builtin/analytics/risk-model.ts +55 -9
  133. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  134. package/src/plugins/builtin/attention/index.tsx +2 -1
  135. package/src/plugins/builtin/attention/listing.ts +14 -0
  136. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  137. package/src/plugins/builtin/buildout/cells.tsx +12 -4
  138. package/src/plugins/builtin/buildout/format.ts +38 -0
  139. package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
  140. package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
  141. package/src/plugins/builtin/buildout/table-model.ts +23 -2
  142. package/src/plugins/builtin/builtin-plugin-meta.ts +6 -2
  143. package/src/plugins/builtin/changelog/index.tsx +9 -6
  144. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
  145. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  146. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
  147. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
  148. package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
  149. package/src/plugins/builtin/chart-composer/headless.ts +6 -1
  150. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  151. package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
  152. package/src/plugins/builtin/chart-composer/settings.ts +27 -0
  153. package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
  154. package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
  155. package/src/plugins/builtin/clinical-trials/model.ts +0 -9
  156. package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
  157. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
  158. package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
  159. package/src/plugins/builtin/company-attention/building.tsx +5 -1
  160. package/src/plugins/builtin/company-attention/client.ts +8 -9
  161. package/src/plugins/builtin/company-attention/evidence.ts +4 -1
  162. package/src/plugins/builtin/company-attention/moves.tsx +5 -1
  163. package/src/plugins/builtin/company-attention/pane.tsx +9 -2
  164. package/src/plugins/builtin/company-attention/signals.tsx +5 -1
  165. package/src/plugins/builtin/correlation/headless.ts +13 -5
  166. package/src/plugins/builtin/correlation/history.ts +22 -0
  167. package/src/plugins/builtin/cot/client.ts +22 -18
  168. package/src/plugins/builtin/cot/headless.ts +15 -10
  169. package/src/plugins/builtin/cot/model.ts +14 -1
  170. package/src/plugins/builtin/cot/pane.tsx +22 -14
  171. package/src/plugins/builtin/credit-boards/client.ts +6 -10
  172. package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
  173. package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
  174. package/src/plugins/builtin/credit-conditions/model.ts +4 -0
  175. package/src/plugins/builtin/credit-conditions/series.ts +3 -1
  176. package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
  177. package/src/plugins/builtin/credit-documents/export.ts +173 -0
  178. package/src/plugins/builtin/credit-documents/headless.ts +7 -7
  179. package/src/plugins/builtin/credit-documents/model.ts +104 -14
  180. package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
  181. package/src/plugins/builtin/custom-view/sources.ts +2 -0
  182. package/src/plugins/builtin/dividend-yield/client.ts +131 -23
  183. package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
  184. package/src/plugins/builtin/dividend-yield/model.ts +8 -5
  185. package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
  186. package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
  187. package/src/plugins/builtin/dividend-yield/types.ts +20 -2
  188. package/src/plugins/builtin/dividend-yield/view.ts +30 -2
  189. package/src/plugins/builtin/docs/targets.ts +5 -1
  190. package/src/plugins/builtin/earnings/board-model.ts +16 -15
  191. package/src/plugins/builtin/earnings/board.tsx +62 -71
  192. package/src/plugins/builtin/earnings/headless.ts +40 -4
  193. package/src/plugins/builtin/earnings/scope.ts +31 -43
  194. package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
  195. package/src/plugins/builtin/earnings-calls/format.ts +31 -2
  196. package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
  197. package/src/plugins/builtin/earnings-calls/model.ts +6 -1
  198. package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
  199. package/src/plugins/builtin/econ/types.ts +2 -0
  200. package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
  201. package/src/plugins/builtin/econ-statistics/headless.ts +6 -4
  202. package/src/plugins/builtin/econ-statistics/stats.ts +36 -9
  203. package/src/plugins/builtin/equity-screener/categories.ts +179 -0
  204. package/src/plugins/builtin/equity-screener/client.ts +4 -2
  205. package/src/plugins/builtin/equity-screener/headless.ts +144 -13
  206. package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
  207. package/src/plugins/builtin/equity-screener/model.ts +99 -10
  208. package/src/plugins/builtin/equity-screener/pane.tsx +68 -13
  209. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  210. package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
  211. package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
  212. package/src/plugins/builtin/fx-matrix/client.ts +6 -3
  213. package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
  214. package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
  215. package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -3
  216. package/src/plugins/builtin/gpu/headless.ts +8 -2
  217. package/src/plugins/builtin/help/function-index.ts +4 -1
  218. package/src/plugins/builtin/help/index.tsx +1 -0
  219. package/src/plugins/builtin/help/shortcut-model.ts +1 -1
  220. package/src/plugins/builtin/holders/fund-holders.ts +51 -0
  221. package/src/plugins/builtin/holders/headless.ts +18 -1
  222. package/src/plugins/builtin/iv-history/client.ts +52 -11
  223. package/src/plugins/builtin/iv-history/format.ts +36 -23
  224. package/src/plugins/builtin/iv-history/headless.ts +33 -18
  225. package/src/plugins/builtin/iv-history/index.tsx +2 -2
  226. package/src/plugins/builtin/iv-history/model.ts +163 -27
  227. package/src/plugins/builtin/iv-history/pane.tsx +10 -6
  228. package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
  229. package/src/plugins/builtin/iv-history/universe.ts +5 -1
  230. package/src/plugins/builtin/jobs/client.ts +25 -29
  231. package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
  232. package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
  233. package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
  234. package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
  235. package/src/plugins/builtin/macro-day/release-table.ts +2 -2
  236. package/src/plugins/builtin/market-heatmap/data.ts +8 -33
  237. package/src/plugins/builtin/market-movers/client.ts +25 -7
  238. package/src/plugins/builtin/market-movers/headless.ts +32 -12
  239. package/src/plugins/builtin/market-movers/index.tsx +26 -14
  240. package/src/plugins/builtin/market-movers/min-float.ts +74 -0
  241. package/src/plugins/builtin/market-movers/model.ts +4 -1
  242. package/src/plugins/builtin/market-movers/screener.ts +14 -0
  243. package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
  244. package/src/plugins/builtin/market-movers/session-table.ts +4 -3
  245. package/src/plugins/builtin/market-movers/table.ts +17 -5
  246. package/src/plugins/builtin/members/breadth-pane.tsx +2 -1
  247. package/src/plugins/builtin/news/index.tsx +1 -0
  248. package/src/plugins/builtin/news/wire/categories.ts +19 -3
  249. package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
  250. package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
  251. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
  252. package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
  253. package/src/plugins/builtin/openfda/client.ts +14 -2
  254. package/src/plugins/builtin/openfda/index.tsx +3 -2
  255. package/src/plugins/builtin/openfda/model.ts +9 -0
  256. package/src/plugins/builtin/openfda/pane.tsx +42 -6
  257. package/src/plugins/builtin/options/analytics.ts +29 -5
  258. package/src/plugins/builtin/options/enrichment.ts +17 -5
  259. package/src/plugins/builtin/options/table.ts +5 -2
  260. package/src/plugins/builtin/options/types.ts +2 -0
  261. package/src/plugins/builtin/options/view.tsx +17 -4
  262. package/src/plugins/builtin/options-positioning/client.ts +11 -0
  263. package/src/plugins/builtin/options-positioning/headless.ts +21 -8
  264. package/src/plugins/builtin/options-positioning/index.tsx +1 -1
  265. package/src/plugins/builtin/options-positioning/model.ts +78 -9
  266. package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
  267. package/src/plugins/builtin/options-scenario/client.ts +116 -47
  268. package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
  269. package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
  270. package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
  271. package/src/plugins/builtin/options-scenario/csp.ts +8 -2
  272. package/src/plugins/builtin/options-scenario/index.tsx +9 -7
  273. package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
  274. package/src/plugins/builtin/pe-band/model.ts +12 -49
  275. package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
  276. package/src/plugins/builtin/perps/cascade.ts +236 -0
  277. package/src/plugins/builtin/perps/client.ts +56 -2
  278. package/src/plugins/builtin/perps/headless.ts +65 -9
  279. package/src/plugins/builtin/perps/index.tsx +7 -5
  280. package/src/plugins/builtin/perps/model.ts +18 -4
  281. package/src/plugins/builtin/perps/pane.tsx +19 -22
  282. package/src/plugins/builtin/perps/status-line.ts +25 -0
  283. package/src/plugins/builtin/plugin-module.ts +5 -1
  284. package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
  285. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
  286. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
  287. package/src/plugins/builtin/portfolio-list/cli/render.ts +7 -5
  288. package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
  289. package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
  290. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
  291. package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
  292. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
  293. package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
  294. package/src/plugins/builtin/primary-dealers/client.ts +13 -38
  295. package/src/plugins/builtin/realized-vol/index.tsx +2 -2
  296. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  297. package/src/plugins/builtin/relative-rotation/client.ts +18 -4
  298. package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
  299. package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
  300. package/src/plugins/builtin/relative-rotation/model.ts +16 -40
  301. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
  302. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
  303. package/src/plugins/builtin/research/event-model.ts +10 -8
  304. package/src/plugins/builtin/research/headless.ts +1 -1
  305. package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
  306. package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
  307. package/src/plugins/builtin/research/relative-valuation-pane.tsx +7 -5
  308. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
  309. package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
  310. package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
  311. package/src/plugins/builtin/scanner/flow-model.ts +50 -4
  312. package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
  313. package/src/plugins/builtin/scanner/flow-session.ts +146 -0
  314. package/src/plugins/builtin/scanner/headless.ts +204 -35
  315. package/src/plugins/builtin/scanner/index.ts +16 -5
  316. package/src/plugins/builtin/scanner/live-headless.ts +27 -3
  317. package/src/plugins/builtin/scanner/live-model.ts +11 -0
  318. package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
  319. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  320. package/src/plugins/builtin/seasonality/headless.ts +10 -1
  321. package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
  322. package/src/plugins/builtin/sec/fund-facts.ts +116 -34
  323. package/src/plugins/builtin/sec/headless.ts +2 -1
  324. package/src/plugins/builtin/series-inventory.ts +2 -0
  325. package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
  326. package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
  327. package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
  328. package/src/plugins/builtin/shared/earnings-report.ts +2 -2
  329. package/src/plugins/builtin/shared/job-posting.ts +1 -0
  330. package/src/plugins/builtin/shared/members-menu.tsx +3 -2
  331. package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
  332. package/src/plugins/builtin/shared/report-freshness.ts +3 -2
  333. package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
  334. package/src/plugins/builtin/shared/ticker-company.ts +77 -0
  335. package/src/plugins/builtin/shared/use-quote-board.ts +2 -1
  336. package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
  337. package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
  338. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
  339. package/src/plugins/builtin/themes/model.ts +16 -7
  340. package/src/plugins/builtin/themes/pane.tsx +5 -5
  341. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
  342. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
  343. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
  344. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
  345. package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
  346. package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
  347. package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
  348. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
  349. package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
  350. package/src/plugins/builtin/time-sales/pane.tsx +31 -19
  351. package/src/plugins/builtin/treasury-daily/client.ts +41 -5
  352. package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
  353. package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
  354. package/src/plugins/builtin/treasury-daily/model.ts +50 -3
  355. package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
  356. package/src/plugins/builtin/vol-surface/client.ts +3 -13
  357. package/src/plugins/builtin/vol-surface/headless.ts +3 -3
  358. package/src/plugins/builtin/vol-surface/index.tsx +1 -1
  359. package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
  360. package/src/plugins/builtin/volatility/index.tsx +2 -2
  361. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  362. package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
  363. package/src/plugins/catalog-manifest.ts +2 -1
  364. package/src/plugins/marketplace/model.ts +18 -5
  365. package/src/portfolio/futures-positions.ts +6 -3
  366. package/src/portfolio/position-quantity.ts +15 -9
  367. package/src/remote/resources.ts +2 -1
  368. package/src/renderers/dom/data-table/row.tsx +26 -0
  369. package/src/renderers/dom/desktop/list-view.tsx +13 -10
  370. package/src/renderers/dom/desktop/query-bar.tsx +4 -2
  371. package/src/renderers/dom/desktop/select-field.tsx +4 -2
  372. package/src/renderers/dom/host/chart-surface.tsx +21 -0
  373. package/src/renderers/electrobun/bun/desktop/app-name.ts +3 -3
  374. package/src/renderers/electrobun/bun/desktop/backend.ts +12 -0
  375. package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
  376. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
  377. package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
  378. package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
  379. package/src/renderers/electrobun/shared/app-name.ts +13 -0
  380. package/src/renderers/electrobun/shared/protocol.ts +10 -0
  381. package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
  382. package/src/renderers/electrobun/view/main.tsx +24 -1
  383. package/src/renderers/electrobun/view/news-source.ts +3 -1
  384. package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
  385. package/src/renderers/electrobun/view/view-ids.ts +85 -0
  386. package/src/renderers/opentui/start.tsx +5 -0
  387. package/src/series/registry.ts +1 -0
  388. package/src/series/types.ts +6 -0
  389. package/src/series/validate.ts +1 -0
  390. package/src/sources/gloomberb-cloud/index.ts +1 -0
  391. package/src/sources/gloomberb-cloud/news.ts +47 -7
  392. package/src/sources/provider-router/financials.ts +15 -3
  393. package/src/sources/provider-router/index.ts +8 -5
  394. package/src/state/ticker-cleanup.ts +50 -0
  395. package/src/sync/controller.ts +4 -3
  396. package/src/sync/core-contributors.ts +90 -31
  397. package/src/telemetry/stall-reports-dom.ts +38 -0
  398. package/src/telemetry/stall-reports-node.ts +75 -0
  399. package/src/telemetry/stall-reports.ts +214 -0
  400. package/src/test-support/preload.ts +14 -0
  401. package/src/theme/themes.ts +10 -0
  402. package/src/tickers/company-name.ts +44 -3
  403. package/src/tickers/listing-venues.ts +15 -3
  404. package/src/tickers/terminal-codes.ts +19 -0
  405. package/src/time-series/alignment.ts +1 -1
  406. package/src/time-series/economic.ts +18 -4
  407. package/src/time-series/field-catalog.ts +1 -1
  408. package/src/time-series/filed-trailing-eps.ts +197 -0
  409. package/src/time-series/fundamentals.ts +44 -1
  410. package/src/time-series/ice-credit.ts +3 -0
  411. package/src/time-series/price-comparison.ts +30 -2
  412. package/src/time-series/resolve-context.ts +3 -1
  413. package/src/time-series/resolve.ts +56 -4
  414. package/src/time-series/session-bar.ts +11 -0
  415. package/src/time-series/spec.ts +3 -2
  416. package/src/time-series/studies.ts +8 -2
  417. package/src/time-series/total-return.ts +160 -0
  418. package/src/time-series/transforms.ts +2 -3
  419. package/src/time-series/types.ts +22 -1
  420. package/src/types/financials.ts +32 -1
  421. package/src/types/headless.ts +21 -3
  422. package/src/types/news-source.ts +1 -0
  423. package/src/types/plugin.ts +9 -0
  424. package/src/ui/host.tsx +3 -0
  425. package/src/updater/failure.ts +4 -1
  426. package/src/utils/cli-output.ts +42 -1
  427. package/src/utils/crypto-pair.ts +44 -0
  428. package/src/utils/forward-dividend.ts +2 -0
  429. package/src/utils/fundamentals.ts +24 -3
  430. package/src/utils/fx-market-hours.ts +26 -3
  431. package/src/utils/main-thread-stall.ts +35 -2
  432. package/src/utils/market-capitalization.ts +24 -1
  433. package/src/utils/price-history.ts +7 -1
  434. package/src/utils/shell-word.ts +8 -0
  435. package/src/utils/trailing-dividend.ts +53 -4
  436. package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
@@ -23,6 +23,7 @@ import {
23
23
  } from "../../../components";
24
24
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
25
25
  import {
26
+ useAssetData,
26
27
  useDebouncedPluginPaneState,
27
28
  usePaneSettingValue,
28
29
  usePaneTitle,
@@ -34,6 +35,7 @@ import type { PaneProps } from "../../../types/plugin";
34
35
  import { Box, type ScrollBoxRenderable } from "../../../ui";
35
36
  import { isPlainArrowUp, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
36
37
  import { toneColor, withoutQuietColumns } from "../shared/research-cells";
38
+ import { looksLikeTicker, useTickerCompanySearch } from "../shared/ticker-company";
37
39
  import { OpenFdaClient, parseOpenFdaDate } from "./client";
38
40
  import {
39
41
  activeFilters,
@@ -41,6 +43,7 @@ import {
41
43
  FILTER_DEFAULT,
42
44
  recordColumns,
43
45
  recordTone,
46
+ termsCellText,
44
47
  type ColumnId,
45
48
  type FilterKey,
46
49
  type FilterState,
@@ -75,6 +78,16 @@ const formatDate = (date: Date | null): string => (date ? date.toISOString().sli
75
78
 
76
79
  const capitalize = (text: string): string => text.charAt(0).toUpperCase() + text.slice(1);
77
80
 
81
+ /** A search written as a ticker is, in capitals (`AZN`, `BRK.B`); `cat` or `azn` is a word to search for. */
82
+ const writtenAsTicker = (term: string): boolean => looksLikeTicker(term) && term === term.toUpperCase();
83
+
84
+ /** What a search that matched nothing points to, by what each dataset is searched on. */
85
+ const EMPTY_SEARCH_HINTS: Record<OpenFdaDataset, string> = {
86
+ drug: "Try the drug's brand or generic name…",
87
+ device: "Try the device or manufacturer name…",
88
+ recall: "Try the firm or product name…",
89
+ };
90
+
78
91
  /** A generic's record lists every labeler; the cell counts them instead of naming one. */
79
92
  function labelerText(record: DrugReport): string | null {
80
93
  if (record.manufacturer) return record.manufacturer;
@@ -95,13 +108,19 @@ function cellFor(record: OpenFdaRecord, column: ColumnId, colors: ThemeColors):
95
108
  return text ? { text, color: record.manufacturer ? colors.text : colors.textMuted } : missing;
96
109
  }
97
110
  if (column === "role") return toned(record.role ? capitalize(record.role) : null);
98
- if (column === "reactions") return record.reactions.length > 0 ? { text: record.reactions.join(", "), color: colors.text } : missing;
111
+ // The row names the first few; its export, like the record, keeps every one.
112
+ if (column === "reactions") {
113
+ return record.reactions.length > 0 ? { text: termsCellText(record.reactions), value: record.reactions.join(", "), color: colors.text } : missing;
114
+ }
99
115
  if (column === "outcome") return toned(record.outcome);
100
116
  break;
101
117
  case "device":
102
118
  if (column === "product") return { text: record.product, color: colors.textBright };
103
119
  if (column === "manufacturer") return record.manufacturer ? { text: record.manufacturer, color: colors.text } : missing;
104
- if (column === "problems") return record.productProblems.length > 0 ? { text: record.productProblems.join(", "), color: colors.text } : missing;
120
+ if (column === "problems") {
121
+ return record.productProblems.length > 0
122
+ ? { text: termsCellText(record.productProblems), value: record.productProblems.join(", "), color: colors.text } : missing;
123
+ }
105
124
  if (column === "event") return toned(record.eventType);
106
125
  break;
107
126
  case "recall":
@@ -201,6 +220,7 @@ const isDataset = (value: unknown): value is OpenFdaDataset => DATASETS.some((en
201
220
 
202
221
  export function OpenFdaPane({ width, height, focused }: PaneProps) {
203
222
  const client = useMemo(() => new OpenFdaClient(), []);
223
+ const dataProvider = useAssetData();
204
224
  const [storedQuery] = usePaneSettingValue("query", "");
205
225
  const initialQuery = String(storedQuery ?? "").trim();
206
226
  const [query, setQuery] = usePluginPaneState("query", initialQuery);
@@ -278,9 +298,25 @@ export function OpenFdaPane({ width, height, focused }: PaneProps) {
278
298
  setOpenItemId(null);
279
299
  }, [setFilterState, setOpenItemId, setSelectedId]);
280
300
 
281
- const loading = records.loading && rows.length === 0;
282
- const error = records.error?.message ?? null;
283
- usePaneRefreshKey(records.reload, { focused, enabled: !searchFocused && !openItemId });
301
+ // `FDA AZN`: FDA records name the firm and its products, never a symbol, so a
302
+ // ticker that finds nothing is searched as its company's name (AstraZeneca),
303
+ // as TRIAL reads one, and the search box then shows it. Only a term written in
304
+ // capitals, as a ticker is (the search box or `fn FDA AZN`): a word such as
305
+ // "cat" stays the search it is. Each term is looked up once.
306
+ const companySearch = useTickerCompanySearch(dataProvider, term, {
307
+ eligible: records.status === "loaded" && rows.length === 0 && !records.error && writtenAsTicker(term),
308
+ key: term,
309
+ onCompany: updateQuery,
310
+ });
311
+ const retryCompanySearch = companySearch.retry;
312
+
313
+ const loading = (records.loading && rows.length === 0) || companySearch.resolving;
314
+ const error = records.error?.message ?? companySearch.error;
315
+ const reload = useCallback(() => {
316
+ retryCompanySearch();
317
+ records.reload();
318
+ }, [records, retryCompanySearch]);
319
+ usePaneRefreshKey(reload, { focused, enabled: !searchFocused && !openItemId });
284
320
 
285
321
  const linkRecord = openRecord ?? selected;
286
322
  const info = useMemo<PaneFooterSegment[]>(() => [
@@ -420,7 +456,7 @@ export function OpenFdaPane({ width, height, focused }: PaneProps) {
420
456
  resetScrollKey={`${dataset}:${term}:${filtersKey}`}
421
457
  selectedTextOverridesCellColor
422
458
  emptyStateTitle={term ? `No ${datasetLabel.toLowerCase()} match ${term}.` : `No ${datasetLabel.toLowerCase()}.`}
423
- emptyStateHint="Press / to search…"
459
+ emptyStateHint={term ? EMPTY_SEARCH_HINTS[dataset] : "Press / to search…"}
424
460
  />
425
461
  );
426
462
  }
@@ -51,8 +51,26 @@ export interface ChainVolatilities {
51
51
  /** Valuation instant the volatilities were solved at, reused for the Greeks. */
52
52
  valuationTime: number;
53
53
  byStrike: ReadonlyMap<number, number>;
54
+ /**
55
+ * Strikes in `byStrike` whose volatility was not solved: with no two-sided
56
+ * out-of-the-money quote to solve from, it is read off the neighbouring solved
57
+ * strikes (flat beyond the last of them). Shown marked, never as a solved figure.
58
+ */
59
+ filled: ReadonlySet<number>;
54
60
  }
55
61
 
62
+ /**
63
+ * How a chain's IV is solved, said under the chain in a report and in the pane's
64
+ * footer while a strike's IV is filled: marked "~", it is read off the
65
+ * neighbouring solved strikes, not solved from a quote.
66
+ */
67
+ export const IV_FILLED_NOTE = "~ IV filled from neighbouring strikes";
68
+ export const IV_FILLED_SHORT = "~ IV filled";
69
+ export const IV_FILLED_DETAIL = "Each strike's IV is solved from the midpoint of its out-of-the-money option. A strike with no two-sided quote there "
70
+ + "has its IV read off the nearest solved strikes, flat beyond the last one, and its cell leads with ~.";
71
+ /** The report's line on how IV was solved, with what "~" marks when a strike is filled. */
72
+ export const ivSolvedNote = (anyFilled: boolean) => `IV solved from quote mids${anyFilled ? ", ~ filled from neighbouring strikes" : ""}`;
73
+
56
74
  function tightestQuotes(contracts: readonly OptionContract[], expiration: number): Map<number, OptionContract> {
57
75
  const quotes = new Map<number, OptionContract>();
58
76
  for (const contract of contracts) {
@@ -78,6 +96,7 @@ export function solveChainVolatilities(
78
96
  ): ChainVolatilities {
79
97
  const valuationTime = optionQuoteValuationTime(chain, now);
80
98
  const byStrike = new Map<number, number>();
99
+ const filled = new Set<number>();
81
100
  // A venue that marks every contract (Deribit) states its volatility: the out-of-the-money side's at each strike.
82
101
  if ([...chain.calls, ...chain.puts].some((contract) => contract.mark)) {
83
102
  for (const [side, contracts] of [["call", chain.calls], ["put", chain.puts]] as const) {
@@ -88,9 +107,9 @@ export function solveChainVolatilities(
88
107
  if (outOfTheMoney || !byStrike.has(contract.strike)) byStrike.set(contract.strike, mark.iv);
89
108
  }
90
109
  }
91
- return { valuationTime, byStrike };
110
+ return { valuationTime, byStrike, filled };
92
111
  }
93
- if (!positive(spot)) return { valuationTime, byStrike };
112
+ if (!positive(spot)) return { valuationTime, byStrike, filled };
94
113
  const rate = optionChainRate(chain);
95
114
  const carry = Number.isFinite(dividendYield) ? dividendYield! : 0;
96
115
  const expirations = new Set([...chain.calls, ...chain.puts].map((contract) => contract.expiration));
@@ -115,7 +134,8 @@ export function solveChainVolatilities(
115
134
  ? solve("call", calls.get(strike)) : solve("put", puts.get(strike)));
116
135
  // Without a two-sided out-of-the-money quote a strike's time value sits inside
117
136
  // the tick, and the in-the-money quote is intrinsic plus spread noise. Read its
118
- // volatility off the neighbouring solved strikes so delta stays monotonic.
137
+ // volatility off the neighbouring solved strikes so delta stays monotonic, and
138
+ // say it was filled: it is no market's volatility.
119
139
  strikes.forEach((strike, index) => {
120
140
  let volatility = solved[index];
121
141
  if (volatility == null) {
@@ -128,10 +148,14 @@ export function solveChainVolatilities(
128
148
  ? low + (high - low) * (strike - strikes[below]!) / (strikes[above]! - strikes[below]!)
129
149
  : low ?? high;
130
150
  }
131
- if (volatility != null) byStrike.set(strike, volatility);
151
+ if (volatility != null) {
152
+ byStrike.set(strike, volatility);
153
+ if (solved[index] == null) filled.add(strike);
154
+ else filled.delete(strike);
155
+ }
132
156
  });
133
157
  }
134
- return { valuationTime, byStrike };
158
+ return { valuationTime, byStrike, filled };
135
159
  }
136
160
 
137
161
  function atmImpliedVolatility(volatilities: ChainVolatilities, spot: number | undefined): number | null {
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { createTtlCache, type TtlCache } from "../../../data/ttl-cache";
2
3
  import type { InstrumentRef } from "../../../market-data/request-types";
3
4
  import type { QueryEntry } from "../../../market-data/result-types";
4
5
  import { buildOptionsKey, resolveEntryData } from "../../../market-data/selectors";
@@ -15,6 +16,17 @@ const OPTIONS_LIVE_ANALYTICS_INTERVAL_MS = 1_000;
15
16
  const LIVE_ANALYTICS_MAX_LOAD = 0.05;
16
17
  /** The Treasury curve is daily; a refreshed chain reuses the one it already loaded. */
17
18
  const TREASURY_REUSE_MS = 30 * 60_000;
19
+ const TREASURY_CURVE = "treasury";
20
+
21
+ interface Selection {
22
+ key: string | null;
23
+ cache: OptionsEnrichmentCache;
24
+ curve: TtlCache<YieldPoint[]>;
25
+ }
26
+
27
+ const newSelection = (key: string | null): Selection => ({
28
+ key, cache: {}, curve: createTtlCache<YieldPoint[]>({ ttlMs: TREASURY_REUSE_MS }),
29
+ });
18
30
 
19
31
  interface EnrichmentResult {
20
32
  key: string;
@@ -59,9 +71,9 @@ export function useOptionsEnrichment(input: {
59
71
  const current = useRef(input);
60
72
  current.current = input;
61
73
  // Per selection: the adjacent model and the Treasury curve outlive a chain refresh.
62
- const store = useRef<{ key: string | null; cache: OptionsEnrichmentCache; curve: { value: YieldPoint[]; at: number } | null }>(
63
- { key: null, cache: {}, curve: null });
64
- if (store.current.key !== key) store.current = { key, cache: {}, curve: null };
74
+ const [first] = useState(() => newSelection(key));
75
+ const store = useRef(first);
76
+ if (store.current.key !== key) store.current = newSelection(key);
65
77
  const [result, setResult] = useState<EnrichmentResult | null>(null);
66
78
  const resultRef = useRef(result);
67
79
  resultRef.current = result;
@@ -71,7 +83,7 @@ export function useOptionsEnrichment(input: {
71
83
  const request = current.current;
72
84
  const identity = { key, entryKey, catalogueKey };
73
85
  const selection = store.current;
74
- const curve = selection.curve && Date.now() - selection.curve.at < TREASURY_REUSE_MS ? selection.curve.value : undefined;
86
+ const curve = selection.curve.peek(TREASURY_CURVE);
75
87
  let projection: OptionsEnrichmentProjection | null = null;
76
88
  // A refresh of the same expiry swaps in its analytics complete, never
77
89
  // through a partial projection that would blank the strip for a moment.
@@ -93,7 +105,7 @@ export function useOptionsEnrichment(input: {
93
105
  }).then((snapshot) => {
94
106
  if (controller.signal.aborted) return;
95
107
  if (projection && !projection.curveLoading && !projection.treasuryError && projection.curve?.length && !curve) {
96
- selection.curve = { value: [...projection.curve], at: Date.now() };
108
+ selection.curve.set(TREASURY_CURVE, [...projection.curve]);
97
109
  }
98
110
  setResult({ ...identity, loading: false, carried: false, snapshot, projection, error: null });
99
111
  }).catch((error: unknown) => {
@@ -163,10 +163,13 @@ export function findNearestStrikeIndex(strikes: number[], targetStrike: number):
163
163
  return bestIndex;
164
164
  }
165
165
 
166
- export function formatIv(value: number | undefined): string {
166
+ export function formatIv(value: number | undefined, filled = false): string {
167
167
  // Providers send 0 for contracts with no implied volatility; "0.0%" would read
168
168
  // as a real quote of zero vol.
169
169
  if (value == null || !Number.isFinite(value) || value <= 0) return "\u2014";
170
+ // A filled figure leads with "~": it was read off the neighbouring strikes, not solved from a quote.
171
+ // Without the decimal it still fits the column at 100% and over.
172
+ if (filled) return value >= 0.9995 ? `~${Math.round(value * 100)}%` : `~${(value * 100).toFixed(1)}%`;
170
173
  return `${(value * 100).toFixed(1)}%`;
171
174
  }
172
175
 
@@ -305,7 +308,7 @@ function formatOptionContractCell(
305
308
  case "openInterest":
306
309
  return formatCompact(contract.openInterest, { fixedDecimals: true });
307
310
  case "iv":
308
- return formatIv(row.impliedVolatility);
311
+ return formatIv(row.impliedVolatility, row.impliedVolatilityFilled);
309
312
  case "delta":
310
313
  return formatGreek(greeks?.delta);
311
314
  case "gamma":
@@ -32,6 +32,8 @@ export interface OptionTableRow {
32
32
  put?: OptionContract;
33
33
  /** Solved from quote midpoints; the call and put at a strike share it. */
34
34
  impliedVolatility?: number;
35
+ /** `impliedVolatility` was filled from neighbouring strikes, not solved from a quote (see `ChainVolatilities.filled`). */
36
+ impliedVolatilityFilled?: boolean;
35
37
  callGreeks?: OptionValuation;
36
38
  putGreeks?: OptionValuation;
37
39
  /** Time value as a share of spot per year (see `carry.ts`); null when it cannot be read. */
@@ -44,7 +44,7 @@ import type { OptionContract } from "../../../types/financials";
44
44
  import { OPTIONS_CALCULATOR_TEMPLATE_ID } from "../options-calculator/model";
45
45
  import { buildChainCalcParams, resolveCalcSide } from "./calc-seed";
46
46
  import { useOptionsCatalogue } from "./expiry-catalogue";
47
- import { calculateOptionGreeks, calculateOptionsSummary, solveChainVolatilities, type OptionsSummary } from "./analytics";
47
+ import { calculateOptionGreeks, calculateOptionsSummary, IV_FILLED_DETAIL, IV_FILLED_NOTE, IV_FILLED_SHORT, solveChainVolatilities, type OptionsSummary } from "./analytics";
48
48
  import {
49
49
  DEFAULT_OPTION_FIELD_IDS,
50
50
  buildStrikeList,
@@ -578,6 +578,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
578
578
  call,
579
579
  put,
580
580
  impliedVolatility: volatilities?.byStrike.get(strike),
581
+ impliedVolatilityFilled: volatilities?.filled.has(strike) ?? false,
581
582
  callGreeks: volatilities ? calculateOptionGreeks(call, "call", greeksSpot, dividendYield, volatilities) : undefined,
582
583
  putGreeks: volatilities ? calculateOptionGreeks(put, "put", greeksSpot, dividendYield, volatilities) : undefined,
583
584
  callExtrinsicPerYear: call && volatilities ? optionCarry(call, "call", greeksSpot, volatilities.valuationTime)?.extrinsicPerYear ?? null : null,
@@ -622,9 +623,21 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
622
623
  + `${new Date(footerSpot.observedAt).toISOString().slice(11, 16)} UTC` : null,
623
624
  strikeChain && !listedOpenInterest.loading ? openInterestAsOf?.date ? `OI ${openInterestAsOf.date.slice(5)}` : "OI undated" : null]
624
625
  .filter(Boolean).join(" · ");
625
- usePaneFooter("options-basis", () => ({ info: basisText ? [{ id: "greeks-basis",
626
- parts: [{ text: basisText, tone: "muted" as const }],
627
- variants: [[{ text: shortBasisText, tone: "muted" as const }], []] }] : [] }), [basisText, shortBasisText]);
626
+ // How IV was solved is said while the chain shows a figure that was not: a strike with no two-sided
627
+ // out-of-the-money quote has its IV read off the neighbouring strikes, and its cell leads with "~".
628
+ const ivFilledShown = optionFieldIds.includes("iv") && rows.some((row) => row.impliedVolatilityFilled && row.impliedVolatility != null);
629
+ // A report of the pane (`gloom fn OMON`) says it too.
630
+ useRemoteUiNode(ivFilledShown ? {
631
+ role: "report-notice",
632
+ label: "IV filled",
633
+ getMetadata: () => ({ text: IV_FILLED_NOTE, key: "ivFilled", value: true }),
634
+ } : null);
635
+ usePaneFooter("options-basis", () => ({ info: [
636
+ ...(basisText ? [{ id: "greeks-basis", parts: [{ text: basisText, tone: "muted" as const }],
637
+ variants: [[{ text: shortBasisText, tone: "muted" as const }], []] }] : []),
638
+ ...(ivFilledShown ? [{ id: "iv-filled", parts: [{ text: IV_FILLED_NOTE, tone: "muted" as const }],
639
+ title: IV_FILLED_DETAIL, variants: [[{ text: IV_FILLED_SHORT, tone: "muted" as const }]] }] : []),
640
+ ] }), [basisText, shortBasisText, ivFilledShown]);
628
641
  // Figures that summarise the whole expiry move at most once a second, so a
629
642
  // busy stream reads as a steady strip rather than flicker.
630
643
  // A new expiry, a refreshed snapshot or the underlying price arriving or
@@ -42,6 +42,8 @@ export interface OpenInterestPayload {
42
42
  tracked: boolean;
43
43
  spot: number | null;
44
44
  spotAsOf: string | null;
45
+ /** `close` when spot is a regular session's close at `spotAsOf`: a cash index outside its session. Absent from older servers. */
46
+ spotBasis?: "close";
45
47
  delayed: boolean;
46
48
  expiries: ExpiryOpenInterest[];
47
49
  expiry: string | null;
@@ -82,6 +84,8 @@ export interface GammaPayload {
82
84
  oiDate: string | null;
83
85
  spot: number | null;
84
86
  spotAsOf: string | null;
87
+ /** `close` when spot is a regular session's close at `spotAsOf`: a cash index outside its session. Absent from older servers. */
88
+ spotBasis?: "close";
85
89
  delayed: boolean;
86
90
  /** The one expiry measured, or null for all of them. */
87
91
  expiry: string | null;
@@ -99,7 +103,14 @@ export interface GammaPayload {
99
103
  strikes: GammaStrike[];
100
104
  /** Each priced expiry's totals, from the same legs as `total`; absent from older servers. */
101
105
  byExpiry?: GammaExpiry[];
106
+ /** When the quotes behind the volatilities were taken: the expiry's own, or the newest priced one's for all. */
102
107
  quotesAsOf: string | null;
108
+ /**
109
+ * The instant gamma is evaluated at, which time to expiry runs from: now
110
+ * while the quotes are current, the spot's time with a closed session's
111
+ * quotes (before the open). Absent from older servers.
112
+ */
113
+ gammaAsOf?: string | null;
103
114
  warnings: string[];
104
115
  /** The venue listing the options, when it is not the US exchanges: Deribit. */
105
116
  venue?: string;
@@ -18,6 +18,7 @@ import {
18
18
  formatCountChange,
19
19
  formatDistance,
20
20
  formatGamma,
21
+ formatGammaTimes,
21
22
  formatLevel,
22
23
  formatPositioningSpot,
23
24
  formatPayout,
@@ -44,15 +45,19 @@ const wall = (value: number | null, spot: number | null) =>
44
45
  value == null ? "--" : `${formatStrikeLabel(value)}, ${formatDistance(value, spot)} vs spot`;
45
46
 
46
47
  /**
47
- * What the report opens with: spot and the quote time gamma was priced at,
48
- * apart from the open interest session, then the figures the GEX tab shows.
49
- * Exports keep each figure whole; the text says them as the pane does.
48
+ * What the report opens with: spot, the instant gamma is evaluated at and
49
+ * when the quotes behind its volatilities were taken, apart from the open
50
+ * interest session, then the figures the GEX tab shows. Exports keep each
51
+ * figure whole; the text says them as the pane does.
50
52
  */
51
53
  function summary(openInterest: OpenInterestPayload, dealer: GammaPayload | null): { entries: HeadlessPaneEntry[]; textEntries: HeadlessPaneEntry[] } {
52
54
  // Gamma is priced at its own read of spot; the figures and their distances use that one.
53
55
  const spot = dealer?.spot ?? openInterest.spot;
54
56
  const spotAsOf = dealer ? dealer.spotAsOf : openInterest.spotAsOf;
57
+ const spotBasis = dealer ? dealer.spotBasis : openInterest.spotBasis;
55
58
  const venue = openInterest.venue;
59
+ // Older servers send no gamma time: their quote time stands alone, as it did.
60
+ const gammaTimes = dealer ? formatGammaTimes(dealer.gammaAsOf, dealer.quotesAsOf) : null;
56
61
  // A venue that publishes open interest live (Deribit) dates it to the read, not to a session.
57
62
  const oiAsOf = venue && openInterest.oiAsOf ? openInterest.oiAsOf : null;
58
63
  const contract = openInterest.contract;
@@ -61,15 +66,20 @@ function summary(openInterest: OpenInterestPayload, dealer: GammaPayload | null)
61
66
  ...(venue ? [{ key: "venue", label: "Venue", value: venue }] : []),
62
67
  { key: "spot", label: "Spot", value: spot },
63
68
  { key: "spotAsOf", label: "Spot as of", value: spotAsOf },
64
- ...(dealer ? [{ key: "quotesAsOf", label: "Quotes as of", value: dealer.quotesAsOf }] : []),
69
+ // An index outside its session: spot is the close, spotAsOf its time.
70
+ ...(spotBasis ? [{ key: "spotBasis", label: "Spot basis", value: spotBasis }] : []),
71
+ ...(dealer && gammaTimes ? [{ key: "gammaAsOf", label: "Gamma at", value: dealer.gammaAsOf ?? null },
72
+ { key: "quotesAsOf", label: "Vols from", value: dealer.quotesAsOf }]
73
+ : dealer ? [{ key: "quotesAsOf", label: "Quotes as of", value: dealer.quotesAsOf }] : []),
65
74
  { key: "oiDate", label: "Open interest as of", value: oiAsOf ?? openInterest.oiDate },
66
75
  ...(contract ? [{ key: "contract", label: "Contract", value: contract }] : []),
67
76
  ...(settles ? [{ key: "settles", label: "Expiries settle", value: settles }] : []),
68
77
  ];
69
78
  const textEntries: HeadlessPaneEntry[] = [
70
79
  ...(venue ? [{ label: "Venue", value: venue }] : []),
71
- { label: "Spot", value: spot, formatted: formatPositioningSpot(spot, spotAsOf, venue) },
72
- ...(dealer ? [{ label: "Quotes as of", value: dealer.quotesAsOf, formatted: time(dealer.quotesAsOf) }] : []),
80
+ { label: "Spot", value: spot, formatted: formatPositioningSpot(spot, spotAsOf, venue, spotBasis) },
81
+ ...(dealer && gammaTimes ? [{ label: "Gamma at", value: dealer.gammaAsOf ?? null, formatted: gammaTimes }]
82
+ : dealer ? [{ label: "Quotes as of", value: dealer.quotesAsOf, formatted: time(dealer.quotesAsOf) }] : []),
73
83
  { label: "Open interest as of", value: oiAsOf ?? openInterest.oiDate, formatted: oiAsOf ? `${time(oiAsOf)}, live` : openInterest.oiDate ?? "--" },
74
84
  ...(contract ? [{ label: "Contract", value: contract, formatted: `${contract.size} ${contract.unit}; OI counts ${contract.unit}` }] : []),
75
85
  ...(settles ? [{ label: "Expiries settle", value: settles, formatted: `${settles}, the day each expiry names` }] : []),
@@ -227,13 +237,16 @@ export const optionsPositioningHeadless = (initialTab: PositioningTab): Headless
227
237
  previousOiDate: openInterest.previousOiDate,
228
238
  spot,
229
239
  spotAsOf: openInterest.spotAsOf,
240
+ ...(openInterest.spotBasis ? { spotBasis: openInterest.spotBasis } : {}),
230
241
  delayed: openInterest.delayed,
231
242
  ...(openInterest.venue ? { venue: openInterest.venue, oiAsOf: openInterest.oiAsOf ?? null, contract: openInterest.contract ?? null } : {}),
232
243
  ...(dealer instanceof Error ? {} : {
233
- gamma: { expiry: dealer.expiry ?? ALL_EXPIRIES, spot: dealer.spot, spotAsOf: dealer.spotAsOf, total: dealer.total, band: dealer.band,
244
+ gamma: { expiry: dealer.expiry ?? ALL_EXPIRIES, spot: dealer.spot, spotAsOf: dealer.spotAsOf,
245
+ ...(dealer.spotBasis ? { spotBasis: dealer.spotBasis } : {}), total: dealer.total, band: dealer.band,
234
246
  bandWithBoxes: dealer.bandWithBoxes ?? null, boxes: dealer.boxes ?? [], flip: dealer.flip,
235
247
  callWall: walls?.callWall ?? null, putWall: walls?.putWall ?? null,
236
- unit: "dollars of dealer delta per 1% move", assumption: "dealers long calls, short puts", quotesAsOf: dealer.quotesAsOf },
248
+ unit: "dollars of dealer delta per 1% move", assumption: "dealers long calls, short puts",
249
+ gammaAsOf: dealer.gammaAsOf ?? null, quotesAsOf: dealer.quotesAsOf },
237
250
  }),
238
251
  methodology: METHODOLOGY,
239
252
  },
@@ -65,7 +65,7 @@ export const optionsPositioningModule: PluginModule = {
65
65
  id: "options-positioning-gex",
66
66
  label: "Dealer Gamma",
67
67
  description: "Net dealer gamma by strike, its total, flip level, call and put walls and the range for the dealer assumption.",
68
- keywords: ["gex", "gamma", "dealer gamma", "gamma exposure", "gamma flip", "call wall", "put wall"],
68
+ keywords: ["gex", "gamma", "dealer gamma", "gamma exposure", "gamma flip", "zero gamma", "gamma level", "call wall", "put wall", "0dte"],
69
69
  shortcut: "GEX",
70
70
  }, "gex"),
71
71
  ],
@@ -1,7 +1,15 @@
1
1
  import { formatCompact, formatCompactAmount } from "../../../utils/format";
2
2
  import { formatExpDate, formatStrikeLabel } from "../../../utils/options";
3
3
  import { formatUtcTime } from "../../../utils/utc-time";
4
- import { formatModelSpotPrice, modelSpotLabel, modelSpotStamp, modelSpotStampVariants, observedModelSpot } from "../options/model-spot";
4
+ import {
5
+ formatModelSpotPrice,
6
+ formatSpotTime,
7
+ modelSpotLabel,
8
+ modelSpotStamp,
9
+ modelSpotStampVariants,
10
+ observedModelSpot,
11
+ type OptionModelSpot,
12
+ } from "../options/model-spot";
5
13
 
6
14
  export type PositioningTab = "strikes" | "expiries" | "gex";
7
15
  export const POSITIONING_TABS: { value: PositioningTab; label: string }[] = [
@@ -100,14 +108,27 @@ export function formatRatio(value: number | null | undefined): string {
100
108
  export function formatLevel(value: number | null | undefined): string {
101
109
  return value == null || !Number.isFinite(value) ? "--" : formatModelSpotPrice(value);
102
110
  }
111
+ /**
112
+ * The spot of a positioning answer as the options panes stamp theirs: the
113
+ * regular session's close when the server says so (a cash index outside its
114
+ * session, which has no extended hours), otherwise the session its time
115
+ * falls in.
116
+ */
117
+ function positioningModelSpot(spot: number, at: number, basis?: "close"): OptionModelSpot {
118
+ return basis === "close"
119
+ ? { price: spot, observedAt: at, valuationTime: at, basis: "close", session: null }
120
+ : observedModelSpot(spot, at);
121
+ }
122
+
103
123
  /**
104
124
  * The spot Gloom Cloud read the distances and dealer gamma at, stamped as
105
125
  * every options pane stamps its spot (`modelSpotStamp`), as a stat cell
106
- * carries it: `pre-market Wed 21 Oct 09:35 UTC`, its shorter forms, then
107
- * none, so the band keeps the columns it has in regular hours. Nothing for
108
- * the regular session's price.
126
+ * carries it: `pre-market Wed 21 Oct 09:35 UTC`, `close Tue 20 Oct` for an
127
+ * index outside its session, their shorter forms, then none, so the band
128
+ * keeps the columns it has in regular hours. Nothing for the regular
129
+ * session's price.
109
130
  */
110
- export function positioningSpotDetail(spot: number | null | undefined, spotAsOf: string | null | undefined, venue?: string):
131
+ export function positioningSpotDetail(spot: number | null | undefined, spotAsOf: string | null | undefined, venue?: string, basis?: "close"):
111
132
  { detail?: string; detailVariants?: string[] } {
112
133
  const at = spotAsOf ? Date.parse(spotAsOf) : Number.NaN;
113
134
  if (spot == null || !Number.isFinite(at)) return {};
@@ -116,17 +137,62 @@ export function positioningSpotDetail(spot: number | null | undefined, spotAsOf:
116
137
  const time = `${new Date(at).toISOString().slice(11, 16)} UTC`;
117
138
  return { detail: `index ${time}`, detailVariants: [`index ${time}`, time, ""] };
118
139
  }
119
- const observed = observedModelSpot(spot, at);
140
+ const observed = positioningModelSpot(spot, at, basis);
120
141
  const stamp = modelSpotStamp(observed);
121
142
  return stamp ? { detail: stamp, detailVariants: [...modelSpotStampVariants(observed), ""] } : {};
122
143
  }
123
144
 
124
- /** The spot as a report says it: `770.88 pre-market Wed 21 Oct 09:35 UTC`, `770.10 last Wed 21 Oct 14:05 UTC`. */
125
- export function formatPositioningSpot(spot: number | null | undefined, spotAsOf: string | null | undefined, venue?: string): string {
145
+ /** The spot as a report says it: `770.88 pre-market Wed 21 Oct 09:35 UTC`, `770.10 last Wed 21 Oct 14:05 UTC`, `7,722.60 close Tue 27 Oct`. */
146
+ export function formatPositioningSpot(spot: number | null | undefined, spotAsOf: string | null | undefined, venue?: string, basis?: "close"): string {
126
147
  const at = spotAsOf ? Date.parse(spotAsOf) : Number.NaN;
127
148
  if (spot == null || !Number.isFinite(spot)) return "--";
128
149
  if (venue) return Number.isFinite(at) ? `${formatLevel(spot)} ${venue} index ${formatUtcTime(new Date(at).toISOString())}` : formatLevel(spot);
129
- return Number.isFinite(at) ? `${formatLevel(spot)} ${modelSpotLabel(observedModelSpot(spot, at))}` : formatLevel(spot);
150
+ return Number.isFinite(at) ? `${formatLevel(spot)} ${modelSpotLabel(positioningModelSpot(spot, at, basis))}` : formatLevel(spot);
151
+ }
152
+
153
+ const instant = (value: string | null | undefined) => {
154
+ const at = value ? Date.parse(value) : Number.NaN;
155
+ return Number.isFinite(at) ? at : null;
156
+ };
157
+ /** `Wed 08:34 UTC`. */
158
+ const dayAndTime = (at: number) => formatSpotTime(at).replace(/^(\w{3}) \d{1,2} \w{3} /, "$1 ");
159
+ /** `08:34 UTC`. */
160
+ const timeOnly = (at: number) => `${new Date(at).toISOString().slice(11, 16)} UTC`;
161
+
162
+ /**
163
+ * When dealer gamma is evaluated and when the quotes behind its volatilities
164
+ * were taken, as a report line: `Wed 28 Oct 08:34 UTC, vols from Tue 27 Oct
165
+ * 20:14 UTC`. Before the open that is the pre-market spot's time with the last
166
+ * session's volatilities. Null without a gamma time (an older server, which
167
+ * dates its quotes alone).
168
+ */
169
+ export function formatGammaTimes(gammaAsOf: string | null | undefined, quotesAsOf: string | null | undefined): string | null {
170
+ const gamma = instant(gammaAsOf);
171
+ if (gamma == null) return null;
172
+ const quotes = instant(quotesAsOf);
173
+ return quotes == null ? formatSpotTime(gamma) : `${formatSpotTime(gamma)}, vols from ${formatSpotTime(quotes)}`;
174
+ }
175
+
176
+ /**
177
+ * The same as a GEX figure: `Gamma at Wed 08:34 UTC`, detail `vols from Tue
178
+ * 20:14 UTC` (the day left out when it is the gamma's), with shorter forms
179
+ * that keep the quote time. An older server's answer, with no gamma time,
180
+ * dates its quotes alone. Null with neither.
181
+ */
182
+ export function gammaTimesFigure(gammaAsOf: string | null | undefined, quotesAsOf: string | null | undefined):
183
+ { label: string; value: string; detail?: string; detailVariants?: string[] } | null {
184
+ const gamma = instant(gammaAsOf);
185
+ const quotes = instant(quotesAsOf);
186
+ if (gamma == null) return quotes == null ? null : { label: "Quotes as of", value: dayAndTime(quotes) };
187
+ if (quotes == null) return { label: "Gamma at", value: dayAndTime(gamma) };
188
+ const sameDay = new Date(gamma).toISOString().slice(0, 10) === new Date(quotes).toISOString().slice(0, 10);
189
+ const quoted = sameDay ? timeOnly(quotes) : dayAndTime(quotes);
190
+ return {
191
+ label: "Gamma at",
192
+ value: dayAndTime(gamma),
193
+ detail: `vols from ${quoted}`,
194
+ detailVariants: [`vols from ${quoted}`, `vols ${quoted}`, `vols ${quoted.replace(/ UTC$/, "")}`],
195
+ };
130
196
  }
131
197
 
132
198
  /** Where a level sits against spot: +0.6%. */
@@ -208,6 +274,9 @@ export function chartWindow<T extends { strike: number }>(
208
274
  /** One full-width numeric domain the chart reads as time; strikes are never labelled as dates. */
209
275
  const COORDINATE_SPAN = 1_000_000_000_000;
210
276
 
277
+ /** Where a chart date made by `toDate` sits across the plot, 0 to 1. */
278
+ export const axisRatioOfDate = (date: Date) => date.getTime() / COORDINATE_SPAN;
279
+
211
280
  export interface NumericAxis {
212
281
  /** Where x sits across the plot, 0 at the left edge and 1 at the right. */
213
282
  ratio: (x: number) => number;