gloomberb 0.10.0 → 0.10.2

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 (353) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
  274. package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
  275. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  276. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  277. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  278. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  279. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  280. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  281. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  282. package/src/renderers/electrobun/view/update-host.ts +1 -1
  283. package/src/renderers/opentui/chart-surface.tsx +2 -1
  284. package/src/renderers/opentui/dialog-host.tsx +20 -4
  285. package/src/renderers/opentui/image/loader.ts +12 -2
  286. package/src/renderers/opentui/input-host.tsx +7 -49
  287. package/src/renderers/opentui/start.tsx +2 -0
  288. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  289. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  290. package/src/sources/gloomberb-cloud/index.ts +42 -20
  291. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  292. package/src/sources/provider-router/cache.ts +1 -1
  293. package/src/sources/provider-router/document-routes.ts +10 -5
  294. package/src/sources/provider-router/financials.test.ts +14 -0
  295. package/src/sources/provider-router/financials.ts +6 -1
  296. package/src/sources/provider-router/history.test.ts +163 -1
  297. package/src/sources/provider-router/history.ts +278 -347
  298. package/src/sources/provider-router/index.ts +2 -2
  299. package/src/sources/provider-router/routing.test.ts +39 -0
  300. package/src/sources/provider-router/routing.ts +24 -11
  301. package/src/sources/provider-router/supplemental.ts +37 -17
  302. package/src/sources/sec-edgar/content.ts +2 -12
  303. package/src/sources/sec-edgar.ts +2 -13
  304. package/src/sources/yahoo-finance/history.ts +2 -2
  305. package/src/sources/yahoo-finance/options.ts +5 -0
  306. package/src/sources/yahoo-finance.ts +2 -2
  307. package/src/state/app/bootstrap.ts +2 -2
  308. package/src/state/app/context/index.tsx +2 -2
  309. package/src/state/hooks/quote-streaming.ts +66 -1
  310. package/src/state/hooks/session-snapshot.ts +2 -2
  311. package/src/state/selectors.ts +0 -9
  312. package/src/sync/controller.test.ts +98 -0
  313. package/src/sync/controller.ts +29 -5
  314. package/src/sync/react.ts +2 -2
  315. package/src/sync/types.ts +2 -2
  316. package/src/theme/colors.test.ts +10 -0
  317. package/src/theme/colors.ts +75 -78
  318. package/src/theme/theme-context.test.tsx +67 -0
  319. package/src/theme/theme-context.tsx +9 -5
  320. package/src/tickers/open-target.ts +2 -2
  321. package/src/tickers/search/upsert.ts +2 -2
  322. package/src/time-series/alignment.ts +0 -7
  323. package/src/time-series/chart-data.ts +145 -0
  324. package/src/time-series/date-window.ts +44 -0
  325. package/src/time-series/history-window.ts +21 -0
  326. package/src/time-series/hooks.test.ts +59 -0
  327. package/src/time-series/hooks.ts +33 -12
  328. package/src/time-series/market-resolution.ts +2 -2
  329. package/src/time-series/market.ts +1 -1
  330. package/src/time-series/range.ts +9 -0
  331. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  332. package/src/time-series/resolve.test.ts +203 -1
  333. package/src/time-series/resolve.ts +96 -18
  334. package/src/time-series/spec-studies.test.ts +15 -0
  335. package/src/time-series/spec.ts +2 -2
  336. package/src/time-series/studies.ts +5 -2
  337. package/src/time-series/types.ts +3 -1
  338. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  339. package/src/time-series/use-chart-resolution.ts +46 -4
  340. package/src/types/broker.ts +2 -2
  341. package/src/types/data-provider.ts +3 -3
  342. package/src/types/financials.ts +12 -0
  343. package/src/types/plugin.ts +2 -2
  344. package/src/ui/context-menu.tsx +3 -2
  345. package/src/ui/dialog.tsx +2 -2
  346. package/src/utils/html-entities.ts +3 -5
  347. package/src/utils/main-thread-monitor.ts +1 -1
  348. package/src/utils/price-history.ts +5 -5
  349. package/src/version.ts +3 -1
  350. package/src/components/chart/core/viewport.test.ts +0 -8
  351. package/src/components/chart/core/viewport.ts +0 -24
  352. package/src/time-series/index.ts +0 -14
  353. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -0,0 +1,238 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { OptionContract, Quote } from "../../../types/financials";
3
+ import type { QueryEntry } from "../../../market-data/result-types";
4
+ import type { OptionTableRow } from "./types";
5
+ import {
6
+ buildOptionQuoteKey,
7
+ buildOptionQuoteTargets,
8
+ hasLiveOptionQuote,
9
+ overlayOptionRowQuotes,
10
+ } from "./live-quotes";
11
+
12
+ function contract(strike: number, side: "C" | "P"): OptionContract {
13
+ return {
14
+ contractSymbol: `AAPL260731${side}${String(strike * 1000).padStart(8, "0")}`,
15
+ strike,
16
+ currency: "USD",
17
+ lastPrice: 1,
18
+ change: 0,
19
+ percentChange: 0,
20
+ volume: 10,
21
+ openInterest: 20,
22
+ bid: 0.9,
23
+ ask: 1.1,
24
+ impliedVolatility: 0.25,
25
+ inTheMoney: false,
26
+ expiration: 1_785_456_000,
27
+ lastTradeDate: 1_785_000_000,
28
+ };
29
+ }
30
+
31
+ function row(strike: number): OptionTableRow {
32
+ return {
33
+ strike,
34
+ call: contract(strike, "C"),
35
+ put: contract(strike, "P"),
36
+ isPositionStrike: false,
37
+ };
38
+ }
39
+
40
+ function readyQuote(quote: Quote): QueryEntry<Quote> {
41
+ return {
42
+ phase: "ready",
43
+ data: quote,
44
+ lastGoodData: quote,
45
+ source: quote.providerId ?? null,
46
+ fetchedAt: quote.lastUpdated,
47
+ staleAt: null,
48
+ error: null,
49
+ attempts: [],
50
+ };
51
+ }
52
+
53
+ describe("options live quotes", () => {
54
+ test("bounds selected-expiry subscriptions around the current strike", () => {
55
+ const rows = Array.from({ length: 100 }, (_, index) => row(50 + index));
56
+ const targets = buildOptionQuoteTargets(rows, 50, 14);
57
+
58
+ expect(targets).toHaveLength(16);
59
+ expect(
60
+ targets.every(
61
+ (target) => target.exchange === "OPTIONS" && target.surface === "options" && target.visible === true,
62
+ ),
63
+ ).toBe(true);
64
+ expect(targets.filter((target) => target.selected)).toHaveLength(2);
65
+ expect(targets.some((target) => target.symbol === rows[50]!.call!.contractSymbol)).toBe(true);
66
+ expect(targets.some((target) => target.symbol === rows[0]!.call!.contractSymbol)).toBe(false);
67
+ expect(targets.some((target) => target.symbol === rows.at(-1)!.put!.contractSymbol)).toBe(false);
68
+ });
69
+
70
+ test("overlays streamed quote fields while preserving chain-only Greeks and open interest", () => {
71
+ const original = row(100);
72
+ const symbol = original.call!.contractSymbol;
73
+ const quote: Quote = {
74
+ symbol,
75
+ providerId: "gloomberb-cloud",
76
+ price: 2.4,
77
+ mark: 2.5,
78
+ bid: 2.45,
79
+ ask: 2.55,
80
+ currency: "USD",
81
+ change: 0,
82
+ changePercent: 0,
83
+ lastUpdated: 1_800_000_000_000,
84
+ receivedAt: 1_800_000_010_000,
85
+ dataSource: "live",
86
+ delivery: "stream",
87
+ };
88
+ const entries = new Map([[buildOptionQuoteKey(symbol), readyQuote(quote)]]);
89
+ const freshness = {
90
+ chainAsOf: new Date(1_799_999_000_000).toISOString(),
91
+ chainDataSource: "live" as const,
92
+ now: 1_800_000_030_000,
93
+ subscriptionStartedAt: 1_799_999_500_000,
94
+ };
95
+
96
+ const overlaid = overlayOptionRowQuotes([original], entries, freshness)[0]!.call!;
97
+
98
+ expect(overlaid).toMatchObject({
99
+ lastPrice: 2.5,
100
+ bid: 2.45,
101
+ ask: 2.55,
102
+ lastUpdated: 1_800_000_000_000,
103
+ impliedVolatility: 0.25,
104
+ openInterest: 20,
105
+ });
106
+ expect(hasLiveOptionQuote(entries, freshness)).toBe(true);
107
+ });
108
+
109
+ test("rejects cached quotes from before this subscription or the current chain", () => {
110
+ const original = row(100);
111
+ const symbol = original.call!.contractSymbol;
112
+ const quote: Quote = {
113
+ symbol,
114
+ providerId: "gloomberb-cloud",
115
+ price: 9,
116
+ mark: 9,
117
+ bid: 8.9,
118
+ ask: 9.1,
119
+ currency: "USD",
120
+ change: 0,
121
+ changePercent: 0,
122
+ lastUpdated: 1_800_000_000_000,
123
+ receivedAt: 1_800_000_010_000,
124
+ dataSource: "live",
125
+ };
126
+ const entries = new Map([[buildOptionQuoteKey(symbol), readyQuote(quote)]]);
127
+
128
+ const beforeSubscription = {
129
+ chainAsOf: new Date(1_799_999_000_000).toISOString(),
130
+ now: 1_800_000_030_000,
131
+ subscriptionStartedAt: 1_800_000_020_000,
132
+ };
133
+ expect(overlayOptionRowQuotes([original], entries, beforeSubscription)[0]!.call!.lastPrice).toBe(1);
134
+ expect(hasLiveOptionQuote(entries, beforeSubscription)).toBe(false);
135
+
136
+ const olderThanChain = {
137
+ chainAsOf: new Date(1_800_000_005_000).toISOString(),
138
+ chainDataSource: "live" as const,
139
+ now: 1_800_000_030_000,
140
+ subscriptionStartedAt: 1_800_000_000_000,
141
+ };
142
+ expect(overlayOptionRowQuotes([original], entries, olderThanChain)[0]!.call!.lastPrice).toBe(1);
143
+ expect(hasLiveOptionQuote(entries, olderThanChain)).toBe(false);
144
+ });
145
+
146
+ test("accepts a freshly delivered delayed quote older than the delayed chain fetch", () => {
147
+ const original = row(100);
148
+ const symbol = original.call!.contractSymbol;
149
+ const quote: Quote = {
150
+ symbol,
151
+ providerId: "gloomberb-cloud",
152
+ price: 2.2,
153
+ mark: 2.25,
154
+ bid: 2.2,
155
+ ask: 2.3,
156
+ currency: "USD",
157
+ change: 0,
158
+ changePercent: 0,
159
+ lastUpdated: 1_800_000_000_000,
160
+ receivedAt: 1_800_000_910_000,
161
+ dataSource: "delayed",
162
+ delivery: "poll",
163
+ stale: false,
164
+ };
165
+ const entries = new Map([[buildOptionQuoteKey(symbol), readyQuote(quote)]]);
166
+ const freshness = {
167
+ chainAsOf: new Date(1_800_000_900_000).toISOString(),
168
+ chainDataSource: "delayed" as const,
169
+ now: 1_800_000_930_000,
170
+ subscriptionStartedAt: 1_800_000_905_000,
171
+ };
172
+
173
+ expect(overlayOptionRowQuotes([original], entries, freshness)[0]!.call!.lastPrice).toBe(2.25);
174
+ expect(hasLiveOptionQuote(entries, freshness)).toBe(false);
175
+ });
176
+
177
+ test("overlays a valid polled quote without reporting a live stream", () => {
178
+ const original = row(100);
179
+ const symbol = original.call!.contractSymbol;
180
+ const quote: Quote = {
181
+ symbol,
182
+ providerId: "gloomberb-cloud",
183
+ price: 2.4,
184
+ mark: 2.5,
185
+ bid: 2.45,
186
+ ask: 2.55,
187
+ currency: "USD",
188
+ change: 0,
189
+ changePercent: 0,
190
+ lastUpdated: 1_800_000_000_000,
191
+ receivedAt: 1_800_000_010_000,
192
+ dataSource: "live",
193
+ delivery: "poll",
194
+ stale: false,
195
+ };
196
+ const entries = new Map([[buildOptionQuoteKey(symbol), readyQuote(quote)]]);
197
+ const freshness = {
198
+ chainAsOf: new Date(1_799_999_000_000).toISOString(),
199
+ chainDataSource: "live" as const,
200
+ now: 1_800_000_030_000,
201
+ subscriptionStartedAt: 1_799_999_500_000,
202
+ };
203
+
204
+ expect(overlayOptionRowQuotes([original], entries, freshness)[0]!.call!.lastPrice).toBe(2.5);
205
+ expect(hasLiveOptionQuote(entries, freshness)).toBe(false);
206
+ });
207
+
208
+ test("rejects a freshly received quote marked stale by the server", () => {
209
+ const original = row(100);
210
+ const symbol = original.call!.contractSymbol;
211
+ const quote: Quote = {
212
+ symbol,
213
+ providerId: "gloomberb-cloud",
214
+ price: 9,
215
+ mark: 9,
216
+ bid: 8.9,
217
+ ask: 9.1,
218
+ currency: "USD",
219
+ change: 0,
220
+ changePercent: 0,
221
+ lastUpdated: 1_800_000_900_000,
222
+ receivedAt: 1_800_000_910_000,
223
+ dataSource: "live",
224
+ delivery: "poll",
225
+ stale: true,
226
+ };
227
+ const entries = new Map([[buildOptionQuoteKey(symbol), readyQuote(quote)]]);
228
+ const freshness = {
229
+ chainAsOf: new Date(1_800_000_800_000).toISOString(),
230
+ chainDataSource: "live" as const,
231
+ now: 1_800_000_930_000,
232
+ subscriptionStartedAt: 1_800_000_905_000,
233
+ };
234
+
235
+ expect(overlayOptionRowQuotes([original], entries, freshness)[0]!.call!.lastPrice).toBe(1);
236
+ expect(hasLiveOptionQuote(entries, freshness)).toBe(false);
237
+ });
238
+ });
@@ -0,0 +1,147 @@
1
+ import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
2
+ import type { OptionContract, Quote } from "../../../types/financials";
3
+ import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors";
4
+ import type { QueryEntry } from "../../../market-data/result-types";
5
+ import type { OptionTableRow } from "./types";
6
+
7
+ export const OPTIONS_QUOTE_EXCHANGE = "OPTIONS";
8
+ export const OPTIONS_CHAIN_REFRESH_INTERVAL_MS = 10 * 60_000;
9
+ export const OPTIONS_STREAM_FRESHNESS_MS = 2 * 60_000;
10
+
11
+ const OPTIONS_STREAM_MAX_TARGETS = 16;
12
+ const OPTIONS_STREAM_MAX_ROWS = OPTIONS_STREAM_MAX_TARGETS / 2;
13
+ const OPTIONS_STREAM_OVERSCAN_ROWS = 4;
14
+
15
+ export interface OptionsQuoteFreshness {
16
+ chainAsOf?: string;
17
+ chainDataSource?: "live" | "delayed";
18
+ now: number;
19
+ subscriptionStartedAt: number;
20
+ }
21
+
22
+ function isFiniteNumber(value: number | undefined): value is number {
23
+ return typeof value === "number" && Number.isFinite(value);
24
+ }
25
+
26
+ function optionStreamRowCount(height: number): number {
27
+ const visibleRows = Math.max(1, Math.floor(height) - 3);
28
+ return Math.min(
29
+ OPTIONS_STREAM_MAX_ROWS,
30
+ visibleRows + OPTIONS_STREAM_OVERSCAN_ROWS,
31
+ );
32
+ }
33
+
34
+ function optionStreamWindow(totalRows: number, selectedIndex: number, height: number): [number, number] {
35
+ const count = Math.min(totalRows, optionStreamRowCount(height));
36
+ const clampedIndex = Math.max(0, Math.min(selectedIndex, Math.max(0, totalRows - 1)));
37
+ const start = Math.max(0, Math.min(clampedIndex - Math.floor(count / 2), totalRows - count));
38
+ return [start, start + count];
39
+ }
40
+
41
+ export function buildOptionQuoteTargets(
42
+ rows: readonly OptionTableRow[],
43
+ selectedIndex: number,
44
+ height: number,
45
+ ): QuoteSubscriptionTarget[] {
46
+ if (rows.length === 0) return [];
47
+ const [start, end] = optionStreamWindow(rows.length, selectedIndex, height);
48
+ const targets = new Map<string, QuoteSubscriptionTarget>();
49
+
50
+ for (let index = start; index < end; index += 1) {
51
+ const row = rows[index];
52
+ if (!row) continue;
53
+ const selected = index === selectedIndex;
54
+ const distance = Math.abs(index - selectedIndex);
55
+ for (const contract of [row.call, row.put]) {
56
+ const symbol = contract?.contractSymbol.trim().toUpperCase();
57
+ if (!symbol) continue;
58
+ targets.set(symbol, {
59
+ symbol,
60
+ exchange: OPTIONS_QUOTE_EXCHANGE,
61
+ surface: "options",
62
+ visible: true,
63
+ selected,
64
+ weight: Math.max(50, 100 - distance),
65
+ });
66
+ }
67
+ }
68
+
69
+ return [...targets.values()];
70
+ }
71
+
72
+ export function buildOptionQuoteKey(contractSymbol: string): string {
73
+ return buildQuoteKey({
74
+ symbol: contractSymbol,
75
+ exchange: OPTIONS_QUOTE_EXCHANGE,
76
+ });
77
+ }
78
+
79
+ export function overlayOptionContractQuote(
80
+ contract: OptionContract | undefined,
81
+ quote: Quote | null | undefined,
82
+ ): OptionContract | undefined {
83
+ if (!contract || !quote) return contract;
84
+ if (contract.lastUpdated != null && quote.lastUpdated < contract.lastUpdated) return contract;
85
+
86
+ const streamedPrice = isFiniteNumber(quote.mark) ? quote.mark : quote.price;
87
+ return {
88
+ ...contract,
89
+ lastPrice: isFiniteNumber(streamedPrice) ? streamedPrice : contract.lastPrice,
90
+ bid: isFiniteNumber(quote.bid) ? quote.bid : contract.bid,
91
+ ask: isFiniteNumber(quote.ask) ? quote.ask : contract.ask,
92
+ lastUpdated: quote.lastUpdated,
93
+ };
94
+ }
95
+
96
+ function freshOptionQuote(entry: QueryEntry<Quote> | undefined, freshness: OptionsQuoteFreshness): Quote | null {
97
+ const quote = resolveEntryData(entry);
98
+ if (!quote || quote.stale === true) return null;
99
+ const receivedAt = quote.receivedAt ?? 0;
100
+ if (
101
+ !Number.isFinite(receivedAt) ||
102
+ receivedAt <= freshness.subscriptionStartedAt ||
103
+ freshness.now - receivedAt > OPTIONS_STREAM_FRESHNESS_MS
104
+ ) {
105
+ return null;
106
+ }
107
+ const chainAsOf = freshness.chainAsOf ? Date.parse(freshness.chainAsOf) : Number.NaN;
108
+ if (
109
+ freshness.chainDataSource === "live" &&
110
+ quote.dataSource === "live" &&
111
+ Number.isFinite(chainAsOf) &&
112
+ Number.isFinite(quote.lastUpdated) &&
113
+ quote.lastUpdated < chainAsOf
114
+ ) {
115
+ return null;
116
+ }
117
+ return quote;
118
+ }
119
+
120
+ export function overlayOptionRowQuotes(
121
+ rows: readonly OptionTableRow[],
122
+ quoteEntries: ReadonlyMap<string, QueryEntry<Quote>>,
123
+ freshness: OptionsQuoteFreshness,
124
+ ): OptionTableRow[] {
125
+ return rows.map((row) => ({
126
+ ...row,
127
+ call: overlayOptionContractQuote(
128
+ row.call,
129
+ row.call ? freshOptionQuote(quoteEntries.get(buildOptionQuoteKey(row.call.contractSymbol)), freshness) : null,
130
+ ),
131
+ put: overlayOptionContractQuote(
132
+ row.put,
133
+ row.put ? freshOptionQuote(quoteEntries.get(buildOptionQuoteKey(row.put.contractSymbol)), freshness) : null,
134
+ ),
135
+ }));
136
+ }
137
+
138
+ export function hasLiveOptionQuote(
139
+ quoteEntries: ReadonlyMap<string, QueryEntry<Quote>>,
140
+ freshness: OptionsQuoteFreshness,
141
+ ): boolean {
142
+ for (const entry of quoteEntries.values()) {
143
+ const quote = freshOptionQuote(entry, freshness);
144
+ if (quote?.dataSource === "live" && quote.delivery === "stream") return true;
145
+ }
146
+ return false;
147
+ }
@@ -6,7 +6,8 @@ import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../../
6
6
  import { AppContext, PaneInstanceProvider, createInitialState } from "../../../state/app/context";
7
7
  import { createTestDataProvider } from "../../../test-support/data-provider";
8
8
  import { cloneLayout, createDefaultConfig } from "../../../types/config";
9
- import type { OptionContract, OptionsChain, TickerFinancials } from "../../../types/financials";
9
+ import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
10
+ import type { OptionContract, OptionsChain, Quote, TickerFinancials } from "../../../types/financials";
10
11
  import type { TickerRecord } from "../../../types/ticker";
11
12
  import { formatExpDate } from "../../../utils/options";
12
13
  import { OptionsView } from "./view";
@@ -163,6 +164,75 @@ test("defaults the table around the nearest strike to the current quote", async
163
164
  expect(frame).not.toContain(" 50 ");
164
165
  });
165
166
 
167
+ test("streams a bounded options window and overlays live contract quotes", async () => {
168
+ const strikes = Array.from({ length: 100 }, (_, index) => 50 + index);
169
+ const subscriptions: QuoteSubscriptionTarget[][] = [];
170
+ let emitQuote:
171
+ | ((target: QuoteSubscriptionTarget, quote: Quote) => void)
172
+ | null = null;
173
+ const provider = createTestDataProvider({
174
+ getOptionsChain: async () => makeChain(strikes, 120),
175
+ subscribeQuotes: (targets, onQuote) => {
176
+ subscriptions.push(targets);
177
+ emitQuote = onQuote;
178
+ return () => {};
179
+ },
180
+ });
181
+ const coordinator = new MarketDataCoordinator(provider);
182
+ setSharedMarketDataCoordinator(coordinator);
183
+
184
+ await act(async () => {
185
+ testSetup = await testRender(
186
+ <OptionsHarness ticker={makeTicker("AAPL")} quotePrice={120.2} />,
187
+ {
188
+ width: 124,
189
+ height: 16,
190
+ },
191
+ );
192
+ });
193
+ await renderSettled();
194
+
195
+ const targets = subscriptions.at(-1) ?? [];
196
+ expect(targets.length).toBeGreaterThan(0);
197
+ expect(targets.length).toBeLessThanOrEqual(80);
198
+ expect(
199
+ targets.every(
200
+ (target) => target.exchange === "OPTIONS" && target.surface === "options",
201
+ ),
202
+ ).toBe(true);
203
+ const selectedCall = targets.find(
204
+ (target) => target.symbol === makeContract(120, "C").contractSymbol,
205
+ );
206
+ expect(selectedCall).toMatchObject({ selected: true });
207
+
208
+ await act(async () => {
209
+ emitQuote?.(selectedCall!, {
210
+ symbol: selectedCall!.symbol,
211
+ providerId: "gloomberb-cloud",
212
+ price: 99.9,
213
+ mark: 99.99,
214
+ bid: 99.98,
215
+ ask: 100,
216
+ currency: "USD",
217
+ change: 0,
218
+ changePercent: 0,
219
+ lastUpdated: Date.now(),
220
+ dataSource: "live",
221
+ });
222
+ await Promise.resolve();
223
+ await testSetup!.renderOnce();
224
+ });
225
+ await renderSettled();
226
+
227
+ expect(
228
+ coordinator.getQuoteEntry({
229
+ symbol: selectedCall!.symbol,
230
+ exchange: "OPTIONS",
231
+ }).data?.mark,
232
+ ).toBe(99.99);
233
+ expect(testSetup!.captureCharFrame()).toContain("99.99");
234
+ });
235
+
166
236
  test("lets the expiration tab row use the full available width", async () => {
167
237
  const expirationDates = Array.from({ length: 9 }, (_, index) => (
168
238
  Math.floor(Date.UTC(2026, index, 20) / 1000)
@@ -5,21 +5,27 @@ import { colors } from "../../../theme/colors";
5
5
  import { isPlainKey } from "../../../utils/keyboard";
6
6
  import { formatExpDate, resolveOptionsTarget } from "../../../utils/options";
7
7
  import { useOptionsQuery, useResolvedEntryValue } from "../../../market-data/hooks";
8
- import { DataTableView, Spinner, Tabs, usePaneFooter, type DataTableKeyEvent } from "../../../components";
8
+ import { DataTableView, Spinner, Tabs, type DataTableKeyEvent } from "../../../components";
9
9
  import { useShortcut } from "../../../react/input";
10
+ import { useLiveQuoteEntries } from "../../../state/hooks/quote-streaming";
10
11
  import {
11
12
  OPTION_COLUMNS,
12
13
  buildStrikeList,
13
14
  findNearestStrikeIndex,
14
- formatIv,
15
- formatStrikeLabel,
16
15
  optionColumnColor,
17
16
  renderOptionCell,
18
17
  resolveDefaultStrikeTarget,
19
18
  } from "./table";
20
19
  import type { OptionColumn, OptionTableRow, OptionsViewProps } from "./types";
20
+ import {
21
+ buildOptionQuoteTargets,
22
+ hasLiveOptionQuote,
23
+ OPTIONS_CHAIN_REFRESH_INTERVAL_MS,
24
+ overlayOptionRowQuotes,
25
+ } from "./live-quotes";
26
+ import { useOptionsAccessFooter } from "./footer";
21
27
 
22
- export function OptionsView({ width, focused, onCapture = () => {} }: OptionsViewProps) {
28
+ export function OptionsView({ width, height, focused, onCapture = () => {} }: OptionsViewProps) {
23
29
  const { ticker, financials } = usePaneTicker();
24
30
  const [expIdx, setExpIdx] = useState(0);
25
31
  const [strikeIdx, setStrikeIdx] = useState(0);
@@ -52,6 +58,7 @@ export function OptionsView({ width, focused, onCapture = () => {} }: OptionsVie
52
58
  baseRequest && selectedExpiration != null
53
59
  ? { ...baseRequest, expirationDate: selectedExpiration }
54
60
  : null,
61
+ { refreshIntervalMs: OPTIONS_CHAIN_REFRESH_INTERVAL_MS },
55
62
  );
56
63
  const expirationChain = useResolvedEntryValue(expirationChainEntry);
57
64
  const chain = expirationChain ?? initialChain;
@@ -118,38 +125,46 @@ export function OptionsView({ width, focused, onCapture = () => {} }: OptionsVie
118
125
  () => new Map(chain?.puts.map((p) => [p.strike, p]) ?? []),
119
126
  [chain],
120
127
  );
121
- const rows = useMemo<OptionTableRow[]>(() => strikes.map((strike) => ({
128
+ const snapshotRows = useMemo<OptionTableRow[]>(() => strikes.map((strike) => ({
122
129
  strike,
123
130
  call: callsByStrike.get(strike),
124
131
  put: putsByStrike.get(strike),
125
132
  isPositionStrike: !!parsed && Math.abs(strike - parsed.strike) < 0.01,
126
133
  })), [callsByStrike, parsed, putsByStrike, strikes]);
127
- const selectedStrike = strikes[strikeIdx];
128
- const selectedCall = selectedStrike != null ? callsByStrike.get(selectedStrike) : undefined;
129
- const selectedPut = selectedStrike != null ? putsByStrike.get(selectedStrike) : undefined;
134
+ const optionQuoteTargets = useMemo(
135
+ () => buildOptionQuoteTargets(snapshotRows, strikeIdx, height),
136
+ [height, snapshotRows, strikeIdx],
137
+ );
138
+ const {
139
+ entries: optionQuoteEntries,
140
+ freshnessNow,
141
+ subscriptionStartedAt,
142
+ } = useLiveQuoteEntries(optionQuoteTargets);
143
+ const optionQuoteFreshness = useMemo(
144
+ () => ({
145
+ chainAsOf: chain?.asOf,
146
+ chainDataSource: chain?.dataSource,
147
+ now: freshnessNow,
148
+ subscriptionStartedAt,
149
+ }),
150
+ [chain?.asOf, chain?.dataSource, freshnessNow, subscriptionStartedAt],
151
+ );
152
+ const rows = useMemo(
153
+ () => overlayOptionRowQuotes(snapshotRows, optionQuoteEntries, optionQuoteFreshness),
154
+ [optionQuoteEntries, optionQuoteFreshness, snapshotRows],
155
+ );
130
156
  const optionColumns: OptionColumn[] = OPTION_COLUMNS.map((column) => ({
131
157
  ...column,
132
158
  headerColor: optionColumnColor(column.id, colors.panel),
133
159
  }));
134
160
 
135
- usePaneFooter("options", () => {
136
- const info = [
137
- ...(selectedExpiration != null ? [{ id: "exp", parts: [{ text: formatExpDate(selectedExpiration), tone: "muted" as const }] }] : []),
138
- ...(selectedStrike != null ? [{ id: "strike", parts: [{ text: `Strike ${formatStrikeLabel(selectedStrike)}`, tone: "value" as const, bold: true }] }] : []),
139
- ...(selectedCall ? [{ id: "call-iv", parts: [{ text: "Call IV", tone: "label" as const }, { text: formatIv(selectedCall.impliedVolatility), tone: "value" as const }] }] : []),
140
- ...(selectedPut ? [{ id: "put-iv", parts: [{ text: "Put IV", tone: "label" as const }, { text: formatIv(selectedPut.impliedVolatility), tone: "value" as const }] }] : []),
141
- ...(loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
142
- ...(error ? [{ id: "error", parts: [{ text: "error", tone: "warning" as const }] }] : []),
143
- ];
144
- return info.length > 0 ? { info } : null;
145
- }, [
161
+ useOptionsAccessFooter({
162
+ chain,
146
163
  error,
164
+ focused,
165
+ hasLiveQuote: hasLiveOptionQuote(optionQuoteEntries, optionQuoteFreshness),
147
166
  loading,
148
- selectedCall?.impliedVolatility,
149
- selectedExpiration,
150
- selectedPut?.impliedVolatility,
151
- selectedStrike,
152
- ]);
167
+ });
153
168
 
154
169
  useEffect(() => {
155
170
  setStrikeIdx((index) => {