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
@@ -6,6 +6,11 @@ import { trySqliteBusyOperation, withSqliteBusyRetry } from "./sqlite/retry";
6
6
 
7
7
  const RESOURCE_CACHE_SOFT_ROW_LIMIT = 25_000;
8
8
  const RESOURCE_CACHE_SOFT_SIZE_LIMIT = 100 * 1024 * 1024;
9
+ const RESOURCE_CACHE_MAINTENANCE_WRITE_INTERVAL = 250;
10
+ const RESOURCE_CACHE_MAINTENANCE_BYTE_INTERVAL = Math.floor(
11
+ RESOURCE_CACHE_SOFT_SIZE_LIMIT / RESOURCE_CACHE_MAINTENANCE_WRITE_INTERVAL,
12
+ );
13
+ const RESOURCE_CACHE_MAINTENANCE_TIME_INTERVAL_MS = 5 * 60_000;
9
14
  const DEFAULT_RESOURCE_SCHEMA_VERSION = 1;
10
15
  const EMPTY_VARIANT_KEY = "";
11
16
  const EMPTY_SOURCE_KEY = "";
@@ -89,6 +94,10 @@ function sortRows(rows: ResourceCacheRow[]): ResourceCacheRow[] {
89
94
  }
90
95
 
91
96
  export class ResourceStore {
97
+ private writesSinceMaintenance = 0;
98
+ private bytesWrittenSinceMaintenance = 0;
99
+ private lastMaintenanceAt = 0;
100
+
92
101
  constructor(private readonly db: Database) {}
93
102
 
94
103
  private toCachedResource<T>(row: ResourceCacheRow): CachedResourceRecord<T> | null {
@@ -179,17 +188,38 @@ export class ResourceStore {
179
188
  const remainingSize = remaining?.total_size ?? 0;
180
189
  if (remainingCount <= RESOURCE_CACHE_SOFT_ROW_LIMIT && remainingSize <= RESOURCE_CACHE_SOFT_SIZE_LIMIT) return;
181
190
 
182
- const rowsToDelete = withSqliteBusyRetry("select cached resources to prune", () => (
191
+ const pruneCandidates = withSqliteBusyRetry("select cached resources to prune", () => (
183
192
  this.db
184
- .query<{ namespace: string; kind: string; entity_key: string; variant_key: string; source_key: string }, [number]>(
185
- `SELECT namespace, kind, entity_key, variant_key, source_key
193
+ .query<{
194
+ namespace: string;
195
+ kind: string;
196
+ entity_key: string;
197
+ variant_key: string;
198
+ source_key: string;
199
+ size_bytes: number;
200
+ }, []>(
201
+ `SELECT namespace, kind, entity_key, variant_key, source_key, size_bytes
186
202
  FROM resource_cache
187
- ORDER BY last_accessed_at ASC, fetched_at ASC
188
- LIMIT ?`,
203
+ ORDER BY last_accessed_at ASC, fetched_at ASC`,
189
204
  )
190
- .all(Math.max(Math.ceil((remainingCount - RESOURCE_CACHE_SOFT_ROW_LIMIT) + 250), 250))
205
+ .all()
191
206
  ));
192
207
 
208
+ let projectedCount = remainingCount;
209
+ let projectedSize = remainingSize;
210
+ const rowsToDelete = [] as typeof pruneCandidates;
211
+ for (const row of pruneCandidates) {
212
+ if (
213
+ projectedCount <= RESOURCE_CACHE_SOFT_ROW_LIMIT
214
+ && projectedSize <= RESOURCE_CACHE_SOFT_SIZE_LIMIT
215
+ ) {
216
+ break;
217
+ }
218
+ rowsToDelete.push(row);
219
+ projectedCount -= 1;
220
+ projectedSize -= row.size_bytes;
221
+ }
222
+
193
223
  if (rowsToDelete.length === 0) return;
194
224
  withSqliteBusyRetry("prune least recently used cached resources", () => {
195
225
  const remove = this.db.query(
@@ -205,6 +235,21 @@ export class ResourceStore {
205
235
  });
206
236
  }
207
237
 
238
+ private maintainCacheAfterWrite(now: number, sizeBytes: number): void {
239
+ this.writesSinceMaintenance += 1;
240
+ this.bytesWrittenSinceMaintenance += sizeBytes;
241
+ const maintenanceDue = this.lastMaintenanceAt === 0
242
+ || this.writesSinceMaintenance >= RESOURCE_CACHE_MAINTENANCE_WRITE_INTERVAL
243
+ || this.bytesWrittenSinceMaintenance >= RESOURCE_CACHE_MAINTENANCE_BYTE_INTERVAL
244
+ || now - this.lastMaintenanceAt >= RESOURCE_CACHE_MAINTENANCE_TIME_INTERVAL_MS;
245
+ if (!maintenanceDue) return;
246
+
247
+ this.pruneIfNeeded();
248
+ this.writesSinceMaintenance = 0;
249
+ this.bytesWrittenSinceMaintenance = 0;
250
+ this.lastMaintenanceAt = now;
251
+ }
252
+
208
253
  get<T>(key: ResourceCacheKey, options: GetResourceOptions = {}): CachedResourceRecord<T> | null {
209
254
  const row = withSqliteBusyRetry("load cached resource", () => (
210
255
  this.db
@@ -304,6 +349,10 @@ export class ResourceStore {
304
349
  const now = options.fetchedAt ?? Date.now();
305
350
  const payload = serializeJson(value);
306
351
  const provenance = options.provenance == null ? null : serializeJson(options.provenance);
352
+ const schemaVersion = options.schemaVersion ?? DEFAULT_RESOURCE_SCHEMA_VERSION;
353
+ const staleAt = now + options.cachePolicy.staleMs;
354
+ const expiresAt = now + options.cachePolicy.expireMs;
355
+ const sizeBytes = Buffer.byteLength(payload, "utf8");
307
356
  const rowKey = {
308
357
  namespace: key.namespace,
309
358
  kind: key.kind,
@@ -326,19 +375,31 @@ export class ResourceStore {
326
375
  rowKey.entityKey,
327
376
  rowKey.variantKey,
328
377
  rowKey.sourceKey,
329
- options.schemaVersion ?? DEFAULT_RESOURCE_SCHEMA_VERSION,
378
+ schemaVersion,
330
379
  payload,
331
380
  provenance,
332
381
  now,
333
- now + options.cachePolicy.staleMs,
334
- now + options.cachePolicy.expireMs,
382
+ staleAt,
383
+ expiresAt,
335
384
  now,
336
- Buffer.byteLength(payload, "utf8"),
385
+ sizeBytes,
337
386
  );
338
387
  });
339
388
 
340
- this.pruneIfNeeded();
341
- return this.get<T>(rowKey, { allowExpired: true, touch: false })!;
389
+ this.maintainCacheAfterWrite(Date.now(), sizeBytes);
390
+ return {
391
+ ...rowKey,
392
+ value: JSON.parse(payload) as T,
393
+ schemaVersion,
394
+ provenance: provenance == null ? null : JSON.parse(provenance) as JsonValue,
395
+ fetchedAt: now,
396
+ staleAt,
397
+ expiresAt,
398
+ lastAccessedAt: now,
399
+ sizeBytes,
400
+ stale: isStale(staleAt),
401
+ expired: isExpired(expiresAt),
402
+ };
342
403
  }
343
404
 
344
405
  delete(key: ResourceCacheKey): void {
@@ -4,6 +4,7 @@ import { existsSync, rmSync } from "fs";
4
4
  import { join } from "path";
5
5
  import { tmpdir } from "os";
6
6
  import { AppPersistence } from "../app-persistence";
7
+ import { ResourceStore } from "../resource-store";
7
8
 
8
9
  const tempPaths: string[] = [];
9
10
 
@@ -82,6 +83,109 @@ describe("AppPersistence", () => {
82
83
  }
83
84
  });
84
85
 
86
+ test("runs resource maintenance on the first write and at write, byte, or time intervals", () => {
87
+ const dbPath = createTempDbPath("resource-maintenance-frequency");
88
+ const persistence = new AppPersistence(dbPath);
89
+ const connection = persistence.database.connection;
90
+ let maintenanceScans = 0;
91
+ const observedConnection = new Proxy(connection, {
92
+ get(target, property) {
93
+ if (property === "query") {
94
+ return (sql: string) => {
95
+ if (sql.includes("SELECT COUNT(*) as count")) maintenanceScans += 1;
96
+ return target.query(sql);
97
+ };
98
+ }
99
+ const value = Reflect.get(target, property, target);
100
+ return typeof value === "function" ? value.bind(target) : value;
101
+ },
102
+ }) as Database;
103
+ const resources = new ResourceStore(observedConnection);
104
+ const realDateNow = Date.now;
105
+ let now = 1_000_000;
106
+ Date.now = () => now;
107
+ const write = (entityKey: string) => resources.set({
108
+ namespace: "test",
109
+ kind: "maintenance",
110
+ entityKey,
111
+ }, { entityKey }, {
112
+ cachePolicy: { staleMs: 60_000, expireMs: 120_000 },
113
+ });
114
+
115
+ try {
116
+ write("first");
117
+ expect(maintenanceScans).toBe(1);
118
+
119
+ for (let index = 0; index < 249; index++) write(`below-${index}`);
120
+ expect(maintenanceScans).toBe(1);
121
+
122
+ write("write-interval");
123
+ expect(maintenanceScans).toBe(2);
124
+
125
+ write("below-time-interval");
126
+ expect(maintenanceScans).toBe(2);
127
+
128
+ now += 5 * 60_000;
129
+ write("time-interval");
130
+ expect(maintenanceScans).toBe(3);
131
+
132
+ resources.set({
133
+ namespace: "test",
134
+ kind: "maintenance",
135
+ entityKey: "byte-interval",
136
+ }, "x".repeat(512 * 1024), {
137
+ cachePolicy: { staleMs: 60_000, expireMs: 120_000 },
138
+ });
139
+ expect(maintenanceScans).toBe(4);
140
+ } finally {
141
+ Date.now = realDateNow;
142
+ persistence.close();
143
+ }
144
+ });
145
+
146
+ test("prunes only enough least-recently-used resources to restore the byte budget", () => {
147
+ const dbPath = createTempDbPath("resource-byte-pruning");
148
+ const persistence = new AppPersistence(dbPath);
149
+ const now = Date.now();
150
+ const write = (entityKey: string, value: unknown, fetchedAt: number) => persistence.resources.set({
151
+ namespace: "test",
152
+ kind: "byte-pruning",
153
+ entityKey,
154
+ }, value, {
155
+ cachePolicy: { staleMs: 60_000, expireMs: 10 * 60_000 },
156
+ fetchedAt,
157
+ });
158
+
159
+ try {
160
+ write("oldest", { value: 1 }, now - 2);
161
+ write("newer", { value: 2 }, now - 1);
162
+ persistence.database.connection.query(
163
+ `UPDATE resource_cache
164
+ SET size_bytes = CASE entity_key
165
+ WHEN 'oldest' THEN ?
166
+ WHEN 'newer' THEN ?
167
+ ELSE size_bytes
168
+ END
169
+ WHERE namespace = 'test' AND kind = 'byte-pruning'`,
170
+ ).run(60 * 1024 * 1024, 50 * 1024 * 1024);
171
+
172
+ write("trigger", "x".repeat(512 * 1024), now);
173
+
174
+ const remaining = persistence.database.connection
175
+ .query<{ entity_key: string }, []>(
176
+ `SELECT entity_key
177
+ FROM resource_cache
178
+ WHERE namespace = 'test' AND kind = 'byte-pruning'
179
+ ORDER BY fetched_at ASC`,
180
+ )
181
+ .all()
182
+ .map((row) => row.entity_key);
183
+ expect(remaining).toEqual(["newer", "trigger"]);
184
+ } finally {
185
+ persistence.close();
186
+ }
187
+ });
188
+
85
189
  test("invalidates plugin state when schema versions differ", () => {
86
190
  const dbPath = createTempDbPath("plugin-version");
87
191
  const persistence = new AppPersistence(dbPath);
@@ -117,21 +221,6 @@ describe("AppPersistence", () => {
117
221
  persistence.close();
118
222
  });
119
223
 
120
- test("returns stable references for unchanged plugin state snapshots", () => {
121
- const dbPath = createTempDbPath("plugin-stable-snapshot");
122
- const persistence = new AppPersistence(dbPath);
123
- persistence.pluginState.set("prediction-markets", "resume:watchlist:v1", ["kalshi:ABC"], 1);
124
-
125
- const first = persistence.pluginState.get<string[]>("prediction-markets", "resume:watchlist:v1", 1);
126
- const second = persistence.pluginState.get<string[]>("prediction-markets", "resume:watchlist:v1", 1);
127
-
128
- expect(first).not.toBeNull();
129
- expect(second).not.toBeNull();
130
- if (!first || !second) throw new Error("Expected persisted plugin state");
131
- expect(second.value).toBe(first.value);
132
- persistence.close();
133
- });
134
-
135
224
  test("stores multiple plugin state records in one batch", () => {
136
225
  const dbPath = createTempDbPath("plugin-state-batch");
137
226
  const persistence = new AppPersistence(dbPath);
@@ -4,6 +4,7 @@ import {
4
4
  applyLanguagePreference,
5
5
  getLanguage,
6
6
  setLanguage,
7
+ subscribeLanguage,
7
8
  } from ".";
8
9
  import { ja } from "./ja";
9
10
  import { ko } from "./ko";
@@ -23,6 +24,22 @@ function placeholders(value: string): string[] {
23
24
  }
24
25
 
25
26
  describe("language selection", () => {
27
+ test("notifies subscribers only when the resolved language changes", () => {
28
+ const previousLanguage = getLanguage();
29
+ let notifications = 0;
30
+ const unsubscribe = subscribeLanguage(() => {
31
+ notifications += 1;
32
+ });
33
+ try {
34
+ setLanguage(previousLanguage === "ja" ? "ko" : "ja");
35
+ setLanguage(getLanguage());
36
+ expect(notifications).toBe(1);
37
+ } finally {
38
+ unsubscribe();
39
+ setLanguage(previousLanguage);
40
+ }
41
+ });
42
+
26
43
  test("keeps a valid environment override ahead of runtime preferences", () => {
27
44
  const previousOverride = process.env.GLOOMBERB_LANG;
28
45
  const previousLanguage = getLanguage();
package/src/i18n/index.ts CHANGED
@@ -77,13 +77,25 @@ function detectLanguage(): AppLanguage {
77
77
  }
78
78
 
79
79
  let currentLanguage: AppLanguage = detectLanguage();
80
+ const languageListeners = new Set<() => void>();
81
+
82
+ function updateLanguage(language: AppLanguage): void {
83
+ if (currentLanguage === language) return;
84
+ currentLanguage = language;
85
+ for (const listener of languageListeners) listener();
86
+ }
80
87
 
81
88
  export function getLanguage(): AppLanguage {
82
89
  return currentLanguage;
83
90
  }
84
91
 
85
92
  export function setLanguage(language: AppLanguage): void {
86
- currentLanguage = language;
93
+ updateLanguage(language);
94
+ }
95
+
96
+ export function subscribeLanguage(listener: () => void): () => void {
97
+ languageListeners.add(listener);
98
+ return () => languageListeners.delete(listener);
87
99
  }
88
100
 
89
101
  /**
@@ -94,13 +106,16 @@ export function setLanguage(language: AppLanguage): void {
94
106
  export function applyLanguageFromConfig(config: { language?: string } | null | undefined): void {
95
107
  const override = getEnvironmentLanguageOverride();
96
108
  if (override) {
97
- currentLanguage = override;
109
+ updateLanguage(override);
98
110
  return;
99
111
  }
100
112
  const configured = config?.language;
101
- if (!configured || configured === "auto") return;
113
+ if (!configured || configured === "auto") {
114
+ updateLanguage(detectLanguage());
115
+ return;
116
+ }
102
117
  const normalized = normalizeLanguageTag(configured);
103
- if (normalized) currentLanguage = normalized;
118
+ if (normalized) updateLanguage(normalized);
104
119
  }
105
120
 
106
121
  /**
@@ -108,8 +123,10 @@ export function applyLanguageFromConfig(config: { language?: string } | null | u
108
123
  * command). "auto" re-runs environment detection.
109
124
  */
110
125
  export function applyLanguagePreference(preference: LanguagePreference): void {
111
- currentLanguage = getEnvironmentLanguageOverride()
112
- ?? (preference === "auto" ? detectLanguage() : preference);
126
+ updateLanguage(
127
+ getEnvironmentLanguageOverride()
128
+ ?? (preference === "auto" ? detectLanguage() : preference),
129
+ );
113
130
  }
114
131
 
115
132
  /**
package/src/i18n/ja.ts CHANGED
@@ -637,8 +637,15 @@ export const ja: Record<string, string> = {
637
637
  "Advanced": "詳細設定",
638
638
  "Free": "無料",
639
639
  "Market data": "市場データ",
640
+ "US equities": "米国株",
641
+ "Options data": "オプションデータ",
640
642
  "Delayed": "遅延",
643
+ "15m delay": "15 分遅延",
641
644
  "Real-time": "リアルタイム",
645
+ "{count}-minute delayed options, upgrade for real-time":
646
+ "オプションは {count} 分遅延、リアルタイムにはアップグレード",
647
+ "real-time options": "リアルタイムオプション",
648
+ "options delayed fallback": "オプション遅延フォールバック",
642
649
  "News": "ニュース",
643
650
  "12h delay": "12 時間遅延",
644
651
  "Real-time wire": "リアルタイム速報",
@@ -827,6 +834,9 @@ export const ja: Record<string, string> = {
827
834
  // ── AI ──────────────────────────────────────────────────────
828
835
  "Select a ticker to ask AI.": "AI に質問する銘柄を選択してください。",
829
836
  "No AI CLI tools are ready:": "AI CLI ツールが準備されていません:",
837
+ "No AI providers are ready.": "利用可能な AI プロバイダーがありません。",
838
+ "No AI providers are ready. Connect an account in pane settings.": "利用可能な AI プロバイダーがありません。ペイン設定でアカウントを接続してください。",
839
+ "Open any AI pane's settings to connect an account.": "いずれかの AI ペインの設定を開いて、アカウントを接続してください。",
830
840
  "Ask questions about {ticker}. Financial data will be included as context.": "{ticker} について質問してください。財務データがコンテキストとして含まれます。",
831
841
  "You": "あなた",
832
842
  "thinking...": "考えています...",
@@ -844,6 +854,7 @@ export const ja: Record<string, string> = {
844
854
  "No supported AI CLI tools detected. Install one to run screeners.": "サポートされている AI CLI ツールが検出されません。スクリーナーを実行するには、1 つインストールしてください。",
845
855
  "Force Refreshing...": "強制更新中...",
846
856
  "Refreshing...": "更新中...",
857
+ "Refreshing…": "更新中…",
847
858
  "Stop": "停止",
848
859
  "Refresh": "更新",
849
860
  "Run Screener": "スクリーナーを実行",
@@ -860,6 +871,8 @@ export const ja: Record<string, string> = {
860
871
  "force refresh armed": "強制リフレッシュ準備完了",
861
872
  "provider": "プロバイダー",
862
873
  "new": "新規",
874
+ "cancel": "キャンセル",
875
+ "stop": "停止",
863
876
  "close tab": "タブを閉じる",
864
877
  "efresh": "更新",
865
878
  "force refresh": "強制リフレッシュ",
package/src/i18n/ko.ts CHANGED
@@ -635,8 +635,15 @@ export const ko: Record<string, string> = {
635
635
  "Advanced": "고급",
636
636
  "Free": "무료",
637
637
  "Market data": "시장 데이터",
638
+ "US equities": "미국 주식",
639
+ "Options data": "옵션 데이터",
638
640
  "Delayed": "지연",
641
+ "15m delay": "15분 지연",
639
642
  "Real-time": "실시간",
643
+ "{count}-minute delayed options, upgrade for real-time":
644
+ "옵션 데이터 {count}분 지연, 실시간으로 업그레이드",
645
+ "real-time options": "실시간 옵션",
646
+ "options delayed fallback": "옵션 지연 대체 데이터",
640
647
  "News": "뉴스",
641
648
  "12h delay": "12시간 지연",
642
649
  "Real-time wire": "실시간 속보",
@@ -825,6 +832,9 @@ export const ko: Record<string, string> = {
825
832
  // ── AI ──────────────────────────────────────────────────────
826
833
  "Select a ticker to ask AI.": "AI에 질문할 종목을 선택하세요.",
827
834
  "No AI CLI tools are ready:": "사용 가능한 AI CLI 도구가 없습니다:",
835
+ "No AI providers are ready.": "사용 가능한 AI 제공업체가 없습니다.",
836
+ "No AI providers are ready. Connect an account in pane settings.": "사용 가능한 AI 제공업체가 없습니다. 패널 설정에서 계정을 연결하세요.",
837
+ "Open any AI pane's settings to connect an account.": "AI 패널 설정을 열어 계정을 연결하세요.",
828
838
  "Ask questions about {ticker}. Financial data will be included as context.": "{ticker}에 대해 질문하세요. 재무 데이터는 컨텍스트로 포함됩니다.",
829
839
  "You": "나",
830
840
  "thinking...": "생각 중...",
@@ -842,6 +852,7 @@ export const ko: Record<string, string> = {
842
852
  "No supported AI CLI tools detected. Install one to run screeners.": "지원되는 AI CLI 도구가 감지되지 않았습니다. 스크리너를 실행하려면 하나를 설치하세요.",
843
853
  "Force Refreshing...": "강제 새로고침 중...",
844
854
  "Refreshing...": "새로고침 중...",
855
+ "Refreshing…": "새로고침 중…",
845
856
  "Stop": "중지",
846
857
  "Refresh": "새로고침",
847
858
  "Run Screener": "스크리너 실행",
@@ -858,6 +869,8 @@ export const ko: Record<string, string> = {
858
869
  "force refresh armed": "강제 새로고침 준비됨",
859
870
  "provider": "제공업체",
860
871
  "new": "새로 만들기",
872
+ "cancel": "취소",
873
+ "stop": "중지",
861
874
  "close tab": "탭 닫기",
862
875
  "efresh": "새로고침",
863
876
  "force refresh": "강제 새로고침",
@@ -0,0 +1,6 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { getLanguage, subscribeLanguage } from ".";
3
+
4
+ export function useAppLanguage() {
5
+ return useSyncExternalStore(subscribeLanguage, getLanguage, getLanguage);
6
+ }
package/src/i18n/zh-cn.ts CHANGED
@@ -637,8 +637,15 @@ export const zhCN: Record<string, string> = {
637
637
  "Advanced": "高级",
638
638
  "Free": "免费版",
639
639
  "Market data": "行情数据",
640
+ "US equities": "美股",
641
+ "Options data": "期权数据",
640
642
  "Delayed": "延迟",
643
+ "15m delay": "延迟 15 分钟",
641
644
  "Real-time": "实时",
645
+ "{count}-minute delayed options, upgrade for real-time":
646
+ "期权行情延迟 {count} 分钟,升级以获取实时数据",
647
+ "real-time options": "实时期权",
648
+ "options delayed fallback": "期权延迟备用数据",
642
649
  "News": "新闻",
643
650
  "12h delay": "延迟 12 小时",
644
651
  "Real-time wire": "实时快讯",
@@ -827,6 +834,9 @@ export const zhCN: Record<string, string> = {
827
834
  // ── AI ──────────────────────────────────────────────────────
828
835
  "Select a ticker to ask AI.": "选择一个代码向 AI 提问。",
829
836
  "No AI CLI tools are ready:": "没有就绪的 AI 命令行工具:",
837
+ "No AI providers are ready.": "没有已就绪的 AI 服务商。",
838
+ "No AI providers are ready. Connect an account in pane settings.": "没有已就绪的 AI 服务商。请在面板设置中连接账户。",
839
+ "Open any AI pane's settings to connect an account.": "打开任意 AI 面板的设置以连接账户。",
830
840
  "Ask questions about {ticker}. Financial data will be included as context.": "可以询问关于 {ticker} 的问题,金融数据会作为上下文提供。",
831
841
  "You": "你",
832
842
  "thinking...": "思考中...",
@@ -844,6 +854,7 @@ export const zhCN: Record<string, string> = {
844
854
  "No supported AI CLI tools detected. Install one to run screeners.": "未检测到受支持的 AI 命令行工具。请安装一个后运行选股器。",
845
855
  "Force Refreshing...": "强制刷新中...",
846
856
  "Refreshing...": "刷新中...",
857
+ "Refreshing…": "刷新中…",
847
858
  "Stop": "停止",
848
859
  "Refresh": "刷新",
849
860
  "Run Screener": "运行选股器",
@@ -860,6 +871,8 @@ export const zhCN: Record<string, string> = {
860
871
  "force refresh armed": "强制刷新已准备",
861
872
  "provider": "提供商",
862
873
  "new": "新建",
874
+ "cancel": "取消",
875
+ "stop": "停止",
863
876
  "close tab": "关闭标签页",
864
877
  "efresh": "刷新",
865
878
  "force refresh": "强制刷新",
package/src/i18n/zh-tw.ts CHANGED
@@ -637,8 +637,15 @@ export const zhTW: Record<string, string> = {
637
637
  "Advanced": "高階",
638
638
  "Free": "免費版",
639
639
  "Market data": "行情資料",
640
+ "US equities": "美股",
641
+ "Options data": "選擇權資料",
640
642
  "Delayed": "延遲",
643
+ "15m delay": "延遲 15 分鐘",
641
644
  "Real-time": "即時",
645
+ "{count}-minute delayed options, upgrade for real-time":
646
+ "選擇權行情延遲 {count} 分鐘,升級以取得即時資料",
647
+ "real-time options": "即時選擇權",
648
+ "options delayed fallback": "選擇權延遲備援資料",
642
649
  "News": "新聞",
643
650
  "12h delay": "延遲 12 小時",
644
651
  "Real-time wire": "即時快訊",
@@ -827,6 +834,9 @@ export const zhTW: Record<string, string> = {
827
834
  // ── AI ──────────────────────────────────────────────────────
828
835
  "Select a ticker to ask AI.": "選擇股票代號以詢問 AI。",
829
836
  "No AI CLI tools are ready:": "沒有就緒的 AI CLI 工具:",
837
+ "No AI providers are ready.": "沒有已就緒的 AI 供應商。",
838
+ "No AI providers are ready. Connect an account in pane settings.": "沒有已就緒的 AI 供應商。請在面板設定中連線帳戶。",
839
+ "Open any AI pane's settings to connect an account.": "開啟任一 AI 面板的設定以連線帳戶。",
830
840
  "Ask questions about {ticker}. Financial data will be included as context.": "可以詢問關於 {ticker} 的問題,金融資料會作為上下文提供。",
831
841
  "You": "你",
832
842
  "thinking...": "思考中...",
@@ -844,6 +854,7 @@ export const zhTW: Record<string, string> = {
844
854
  "No supported AI CLI tools detected. Install one to run screeners.": "未偵測到支援的 AI CLI 工具。請先安裝工具再執行選股器。",
845
855
  "Force Refreshing...": "強制重新整理中...",
846
856
  "Refreshing...": "重新整理中...",
857
+ "Refreshing…": "重新整理中…",
847
858
  "Stop": "停止",
848
859
  "Refresh": "重新整理",
849
860
  "Run Screener": "執行選股器",
@@ -860,6 +871,8 @@ export const zhTW: Record<string, string> = {
860
871
  "force refresh armed": "已準備強制重新整理",
861
872
  "provider": "供應商",
862
873
  "new": "新增",
874
+ "cancel": "取消",
875
+ "stop": "停止",
863
876
  "close tab": "關閉分頁",
864
877
  "efresh": "重新整理",
865
878
  "force refresh": "強制重新整理",
@@ -32,6 +32,7 @@ type RunSingleFlight = <T>(key: string, task: () => Promise<T>) => Promise<T>;
32
32
 
33
33
  interface AuxiliaryLoaderOptions<T> {
34
34
  dataProvider: DataProvider;
35
+ forceRefresh?: boolean;
35
36
  key: string;
36
37
  store: QueryStore<T>;
37
38
  ttlMs: number;
@@ -41,14 +42,16 @@ interface AuxiliaryLoaderOptions<T> {
41
42
 
42
43
  export function loadOptionsEntry(options: {
43
44
  dataProvider: DataProvider;
45
+ forceRefresh?: boolean;
44
46
  request: OptionsRequest;
45
47
  store: QueryStore<OptionsChain>;
46
48
  runSingleFlight: RunSingleFlight;
47
49
  }): Promise<QueryEntry<OptionsChain>> {
48
- const { dataProvider, request, store, runSingleFlight } = options;
50
+ const { dataProvider, forceRefresh = false, request, store, runSingleFlight } = options;
49
51
  const key = buildOptionsKey(request);
50
52
  return loadAuxiliaryEntry({
51
53
  dataProvider,
54
+ forceRefresh,
52
55
  key,
53
56
  store,
54
57
  ttlMs: OPTIONS_CACHE_TTL_MS,
@@ -213,6 +216,7 @@ export function loadFxRateEntry(options: {
213
216
 
214
217
  function loadAuxiliaryEntry<T>({
215
218
  dataProvider,
219
+ forceRefresh = false,
216
220
  key,
217
221
  store,
218
222
  ttlMs,
@@ -220,10 +224,10 @@ function loadAuxiliaryEntry<T>({
220
224
  load,
221
225
  }: AuxiliaryLoaderOptions<T>): Promise<QueryEntry<T>> {
222
226
  const current = store.get(key);
223
- if (hasFreshReadyEntry(current, ttlMs)) {
227
+ if (!forceRefresh && hasFreshReadyEntry(current, ttlMs)) {
224
228
  return Promise.resolve(current);
225
229
  }
226
- return runSingleFlight(key, async () => {
230
+ return runSingleFlight(forceRefresh ? `${key}|refresh` : key, async () => {
227
231
  store.update(key, loadingEntry);
228
232
  const startedAt = Date.now();
229
233
  try {
@@ -1,4 +1,4 @@
1
- import { isIntradayResolution, TIME_RANGE_ORDER } from "../../components/chart/core/resolution";
1
+ import { isIntradayResolution, TIME_RANGE_ORDER } from "../../time-series/resolution";
2
2
  import type { PricePoint } from "../../types/financials";
3
3
  import { isPriceHistoryStaleForCurrentWindow, normalizePriceHistory } from "../../utils/price-history";
4
4
  import type { ChartRequest, InstrumentRef } from "../request-types";
@@ -178,6 +178,32 @@ describe("MarketDataCoordinator", () => {
178
178
  expect(optionsCalls).toBe(1);
179
179
  });
180
180
 
181
+ it("force refreshes an options chain for periodic Greeks and IV updates", async () => {
182
+ let optionsCalls = 0;
183
+ const provider = createProvider({
184
+ getOptionsChain: async () => {
185
+ optionsCalls += 1;
186
+ return {
187
+ underlyingSymbol: "AAPL",
188
+ expirationDates: [1_800_000_000],
189
+ calls: [],
190
+ puts: [],
191
+ };
192
+ },
193
+ });
194
+ const coordinator = new MarketDataCoordinator(provider);
195
+ const request = {
196
+ instrument: { symbol: "AAPL", exchange: "NASDAQ" },
197
+ expirationDate: 1_800_000_000,
198
+ };
199
+
200
+ await coordinator.loadOptions(request);
201
+ await coordinator.loadOptions(request);
202
+ await coordinator.loadOptions(request, { forceRefresh: true });
203
+
204
+ expect(optionsCalls).toBe(2);
205
+ });
206
+
181
207
  it("uses cached chart data while a wider range request is loading", async () => {
182
208
  const oneYearHistory = [
183
209
  { date: new Date("2024-01-01"), close: 100 },
@@ -303,9 +303,13 @@ export class MarketDataCoordinator {
303
303
  });
304
304
  }
305
305
 
306
- async loadOptions(request: OptionsRequest): Promise<QueryEntry<OptionsChain>> {
306
+ async loadOptions(
307
+ request: OptionsRequest,
308
+ options: { forceRefresh?: boolean } = {},
309
+ ): Promise<QueryEntry<OptionsChain>> {
307
310
  return loadOptionsEntry({
308
311
  dataProvider: this.dataProvider,
312
+ forceRefresh: options.forceRefresh,
309
313
  request,
310
314
  store: this.optionsStore,
311
315
  runSingleFlight: (key, task) => this.runSingleFlight(key, task),
@@ -364,11 +368,13 @@ export class MarketDataCoordinator {
364
368
  private applyStreamQuote(instrument: InstrumentRef, quote: Quote): void {
365
369
  const key = buildQuoteKey(instrument);
366
370
  const current = this.quoteStore.get(key);
367
- const resolvedQuote = this.resolveIncomingQuote(instrument, quote);
371
+ const resolvedQuote = quote.stale === true ? quote : this.resolveIncomingQuote(instrument, quote);
368
372
  if (areStreamQuotesEquivalent(current.data ?? current.lastGoodData, resolvedQuote)) return;
369
- const receivedAt = Date.now();
370
- const attempts = [createAttempt(resolvedQuote.providerId ?? this.dataProvider.id, receivedAt, "success")];
371
- this.quoteStore.set(key, readyQuoteEntry(current, { ...resolvedQuote, receivedAt }, resolvedQuote.providerId ?? this.dataProvider.id, attempts));
373
+ const startedAt = Date.now();
374
+ const receivedAt = resolvedQuote.stale === true ? resolvedQuote.receivedAt : startedAt;
375
+ const storedQuote = receivedAt == null ? resolvedQuote : { ...resolvedQuote, receivedAt };
376
+ const attempts = [createAttempt(resolvedQuote.providerId ?? this.dataProvider.id, startedAt, "success")];
377
+ this.quoteStore.set(key, readyQuoteEntry(current, storedQuote, resolvedQuote.providerId ?? this.dataProvider.id, attempts));
372
378
  }
373
379
 
374
380
  private resolveIncomingQuote(instrument: InstrumentRef, quote: Quote): Quote {