gloomberb 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (489) hide show
  1. package/README.md +6 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +103 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +129 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +196 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/cli/commands/plugins.ts +39 -11
  20. package/src/cli/desktop-pane-shot.ts +285 -16
  21. package/src/cli/index.ts +1 -1
  22. package/src/cli/pane-functions/capabilities.ts +58 -28
  23. package/src/cli/pane-functions/catalog.ts +16 -4
  24. package/src/cli/pane-functions/discovery.ts +1 -0
  25. package/src/cli/pane-functions/headless.test.ts +247 -0
  26. package/src/cli/pane-functions/headless.ts +423 -0
  27. package/src/cli/pane-functions/index.test.ts +9 -0
  28. package/src/cli/pane-functions/index.ts +6 -2
  29. package/src/cli/pane-functions/options.ts +27 -2
  30. package/src/cli/pane-functions/report.ts +4 -0
  31. package/src/cli/pane-functions/resolver.ts +26 -8
  32. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  33. package/src/cli/pane-functions/screenshot.ts +236 -20
  34. package/src/cli/restore-plugins.ts +34 -0
  35. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  36. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  37. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  38. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  39. package/src/components/chart/composite/composite-chart.tsx +301 -204
  40. package/src/components/chart/composite/format.ts +8 -2
  41. package/src/components/chart/composite/interactions.test.ts +68 -102
  42. package/src/components/chart/composite/interactions.ts +214 -264
  43. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  44. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  45. package/src/components/chart/composite/scene.ts +57 -3
  46. package/src/components/chart/composite/text-renderer.ts +3 -2
  47. package/src/components/chart/composite/time-scale.ts +138 -57
  48. package/src/components/chart/composite/types.ts +25 -1
  49. package/src/components/chart/core/data.ts +9 -13
  50. package/src/components/chart/core/date-window.ts +7 -1
  51. package/src/components/chart/core/pointer.ts +3 -0
  52. package/src/components/chart/core/types.ts +0 -45
  53. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  54. package/src/components/chart/native/kitty/index.test.ts +0 -188
  55. package/src/components/chart/native/raster/crosshair.ts +6 -5
  56. package/src/components/chart/native/raster/primitives.ts +0 -23
  57. package/src/components/chart/native/renderer-selection.ts +81 -31
  58. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  59. package/src/components/chart/static/chart-surface.tsx +150 -0
  60. package/src/components/chart/static/index.ts +8 -0
  61. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  62. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  63. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  64. package/src/components/chart/static/series.ts +49 -0
  65. package/src/components/command-bar/action-runtime.ts +1 -3
  66. package/src/components/command-bar/assist/model.test.ts +17 -2
  67. package/src/components/command-bar/assist/model.ts +40 -9
  68. package/src/components/command-bar/commands/direct/index.ts +1 -1
  69. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  70. package/src/components/command-bar/commands/registry.ts +0 -11
  71. package/src/components/command-bar/helpers.test.ts +2 -2
  72. package/src/components/command-bar/helpers.ts +2 -6
  73. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  74. package/src/components/command-bar/list/badge.test.ts +54 -0
  75. package/src/components/command-bar/list/badge.ts +62 -0
  76. package/src/components/command-bar/list/model.test.ts +103 -0
  77. package/src/components/command-bar/list/model.ts +61 -4
  78. package/src/components/command-bar/list/view.test.tsx +113 -0
  79. package/src/components/command-bar/list/view.tsx +147 -131
  80. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  81. package/src/components/command-bar/panel/body.tsx +51 -65
  82. package/src/components/command-bar/panel/index.tsx +48 -13
  83. package/src/components/command-bar/panel/layout.test.ts +139 -0
  84. package/src/components/command-bar/panel/layout.ts +65 -28
  85. package/src/components/command-bar/panel/native-surface.ts +24 -0
  86. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  87. package/src/components/command-bar/panel/runtime.ts +6 -3
  88. package/src/components/command-bar/panel/state.ts +41 -7
  89. package/src/components/command-bar/panel/types.ts +7 -1
  90. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  91. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  92. package/src/components/command-bar/routes/root/results.ts +30 -28
  93. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  94. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  95. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  96. package/src/components/command-bar/routes/root/selection.ts +1 -12
  97. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  98. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  99. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  100. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  101. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  102. package/src/components/command-bar/routing/actions.ts +1 -81
  103. package/src/components/command-bar/routing/launch-request.ts +1 -1
  104. package/src/components/command-bar/routing/list-state.ts +6 -21
  105. package/src/components/command-bar/selection-runtime.ts +1 -1
  106. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  107. package/src/components/command-bar/surface/environment.ts +8 -1
  108. package/src/components/command-bar/surface/index.test.tsx +84 -68
  109. package/src/components/command-bar/surface/index.tsx +33 -36
  110. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  111. package/src/components/command-bar/theme-picker.tsx +43 -15
  112. package/src/components/command-bar/view-model.test.ts +53 -8
  113. package/src/components/command-bar/view-model.ts +43 -14
  114. package/src/components/command-bar/workflow/tickers.ts +1 -0
  115. package/src/components/command-bar/workflow/types.ts +1 -1
  116. package/src/components/data-table/view.test.tsx +45 -6
  117. package/src/components/data-table/view.tsx +55 -9
  118. package/src/components/index.ts +10 -2
  119. package/src/components/input-search-bar.test.tsx +36 -2
  120. package/src/components/input-search-bar.tsx +18 -1
  121. package/src/components/layout/header.test.tsx +57 -10
  122. package/src/components/layout/header.tsx +292 -149
  123. package/src/components/layout/market-summary.test.ts +81 -0
  124. package/src/components/layout/market-summary.ts +109 -0
  125. package/src/components/layout/shell/chrome.ts +96 -1
  126. package/src/components/layout/shell/index.tsx +11 -3
  127. package/src/components/layout/shell/menu.ts +9 -0
  128. package/src/components/layout/shell/pane/actions.ts +29 -0
  129. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  130. package/src/components/layout/shell/shortcuts.ts +3 -0
  131. package/src/components/layout/status-bar.test.tsx +9 -8
  132. package/src/components/layout/status-bar.tsx +85 -54
  133. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  134. package/src/components/price-sparkline/index.ts +61 -34
  135. package/src/components/price-sparkline/view.tsx +1 -1
  136. package/src/components/sign-in-gate.test.tsx +128 -0
  137. package/src/components/sign-in-gate.tsx +149 -0
  138. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  139. package/src/components/ticker/badge/list.tsx +17 -3
  140. package/src/components/ticker/list-table-view.tsx +6 -3
  141. package/src/components/ui/data-table/index.tsx +3 -2
  142. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  143. package/src/components/ui/frame.tsx +28 -0
  144. package/src/components/ui/index.ts +3 -0
  145. package/src/components/ui/native-select.tsx +20 -10
  146. package/src/components/ui/select-button.tsx +138 -0
  147. package/src/components/ui/status.tsx +2 -2
  148. package/src/components/ui/table-layout.ts +1 -1
  149. package/src/components/use-double-click-activation.ts +7 -3
  150. package/src/core/app-services.ts +3 -3
  151. package/src/data/config/layout.ts +12 -6
  152. package/src/data/config/store/index.test.ts +97 -0
  153. package/src/data/config/store/migrations.ts +41 -0
  154. package/src/data/config/store/normalize.ts +5 -3
  155. package/src/i18n/es.ts +6 -2
  156. package/src/i18n/ja.ts +6 -2
  157. package/src/i18n/ko.ts +6 -2
  158. package/src/i18n/zh-cn.ts +6 -2
  159. package/src/i18n/zh-tw.ts +6 -2
  160. package/src/layout-marketplace/pane-imagery.ts +1 -0
  161. package/src/layout-marketplace/payload.test.ts +17 -0
  162. package/src/layout-marketplace/payload.ts +40 -5
  163. package/src/market-data/coordinator/index.ts +3 -2
  164. package/src/market-data/coordinator/quotes.ts +173 -66
  165. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  166. package/src/pane-settings.ts +1 -1
  167. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  168. package/src/plugins/builtin/analytics/view.tsx +1 -1
  169. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  170. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  171. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  172. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  173. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  174. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  175. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  176. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  177. package/src/plugins/builtin/composite-plugins.ts +14 -1
  178. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  179. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  180. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  181. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  182. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  183. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  184. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  185. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  186. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  187. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  188. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  189. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  190. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  191. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  192. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  193. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  194. package/src/plugins/builtin/econ/index.tsx +2 -1
  195. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  196. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  197. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  198. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  199. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  200. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  201. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  202. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  203. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  204. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  205. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  206. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  207. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  208. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  209. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  210. package/src/plugins/builtin/help/index.tsx +4 -0
  211. package/src/plugins/builtin/holders/index.tsx +1 -0
  212. package/src/plugins/builtin/insider/index.tsx +1 -0
  213. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  214. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  215. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  216. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  217. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  218. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  219. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  220. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  221. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  222. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  223. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  224. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  225. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  226. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  227. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  228. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  229. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  230. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  231. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  232. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  233. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  234. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  235. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  236. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  237. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  238. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  239. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  240. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  241. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  242. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  243. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  244. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  245. package/src/plugins/builtin/options/index.tsx +1 -0
  246. package/src/plugins/builtin/options/view.test.tsx +29 -1
  247. package/src/plugins/builtin/options/view.tsx +1 -22
  248. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  249. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  250. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  251. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  252. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  253. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  254. package/src/plugins/builtin/plugin-module.ts +9 -1
  255. package/src/plugins/builtin/research/index.tsx +4 -0
  256. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  257. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  258. package/src/plugins/builtin/research-search/data.ts +109 -0
  259. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  260. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  261. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  262. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  263. package/src/plugins/builtin/research-search/index.tsx +85 -0
  264. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  265. package/src/plugins/builtin/research-search/model.ts +291 -0
  266. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  267. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  268. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  269. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  270. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  271. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  272. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  273. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  274. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  275. package/src/plugins/builtin/sec/index.tsx +1 -0
  276. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  277. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  278. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  279. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  280. package/src/plugins/builtin/ticker-detail/index.tsx +0 -14
  281. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  282. package/src/plugins/builtin/volatility/index.tsx +1 -1
  283. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  284. package/src/plugins/builtin/world-venue-map/map.tsx +213 -69
  285. package/src/plugins/builtin/world-venue-map/model.test.ts +32 -0
  286. package/src/plugins/builtin/world-venue-map/model.ts +134 -13
  287. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  288. package/src/plugins/bundle.test.ts +109 -0
  289. package/src/plugins/bundle.ts +139 -0
  290. package/src/plugins/catalog-browser.test.ts +1 -0
  291. package/src/plugins/catalog-browser.ts +10 -0
  292. package/src/plugins/catalog-manifest.test.ts +43 -0
  293. package/src/plugins/catalog-manifest.ts +92 -0
  294. package/src/plugins/catalog-ui.ts +20 -4
  295. package/src/plugins/catalog.ts +9 -2
  296. package/src/plugins/current-target.ts +28 -0
  297. package/src/plugins/host-contract.ts +37 -0
  298. package/src/plugins/host-link.ts +158 -0
  299. package/src/plugins/host-modules.ts +61 -0
  300. package/src/plugins/loader.ts +41 -22
  301. package/src/plugins/ownership.ts +1 -0
  302. package/src/plugins/prediction-markets/index.tsx +1 -0
  303. package/src/plugins/registry/context.ts +3 -0
  304. package/src/plugins/registry/contributions.ts +27 -0
  305. package/src/plugins/registry/index.test.ts +25 -0
  306. package/src/plugins/registry/index.ts +8 -0
  307. package/src/plugins/registry/pane-settings.ts +6 -13
  308. package/src/plugins/seed.test.ts +83 -0
  309. package/src/plugins/seed.ts +80 -0
  310. package/src/public/broker.ts +31 -0
  311. package/src/public/market-data.ts +9 -0
  312. package/src/public/pane-hooks.ts +57 -0
  313. package/src/public/public-api.test.ts +285 -0
  314. package/src/public/react.ts +62 -0
  315. package/src/public/test-support.ts +23 -0
  316. package/src/public/time-series.ts +8 -0
  317. package/src/public/utils.ts +77 -0
  318. package/src/remote/layout-helpers.ts +6 -8
  319. package/src/remote/resources.ts +2 -0
  320. package/src/remote/schema.ts +1 -0
  321. package/src/remote/semantic-tree.test.tsx +105 -0
  322. package/src/remote/semantic-tree.tsx +53 -35
  323. package/src/renderers/browser/main.tsx +6 -0
  324. package/src/renderers/cloudflare/worker.test.ts +19 -0
  325. package/src/renderers/cloudflare/worker.ts +1 -1
  326. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  327. package/src/renderers/electrobun/bun/index.ts +5 -2
  328. package/src/renderers/electrobun/shared/protocol.ts +21 -0
  329. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  330. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  331. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  332. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  333. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  334. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  335. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  336. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  337. package/src/renderers/electrobun/view/main.tsx +27 -2
  338. package/src/renderers/electrobun/view/styles.css +5 -0
  339. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  340. package/src/renderers/opentui/chart-surface.tsx +5 -1
  341. package/src/renderers/opentui/host.tsx +16 -2
  342. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  343. package/src/renderers/opentui/interaction-performance.ts +188 -0
  344. package/src/renderers/opentui/start.tsx +26 -1
  345. package/src/renderers/opentui/test-utils.tsx +13 -0
  346. package/src/renderers/opentui/toast-host.tsx +26 -2
  347. package/src/renderers/opentui/ui-host.tsx +1 -0
  348. package/src/sources/provider-router/search.test.ts +191 -0
  349. package/src/sources/provider-router/search.ts +92 -39
  350. package/src/state/app/context/index.tsx +2 -0
  351. package/src/state/app/context/selector.test.tsx +33 -1
  352. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  353. package/src/state/hooks/quote-streaming.ts +106 -30
  354. package/src/state/pane-table-export-registry.ts +19 -0
  355. package/src/theme/color-utils.ts +1 -1
  356. package/src/theme/colors.ts +50 -0
  357. package/src/theme/themes.test.ts +15 -2
  358. package/src/theme/themes.ts +19 -1
  359. package/src/tickers/search/index.test.ts +26 -0
  360. package/src/tickers/search/index.ts +42 -17
  361. package/src/tickers/search/ranking.ts +14 -0
  362. package/src/time-series/chart-data.test.ts +216 -0
  363. package/src/time-series/resolution.test.ts +25 -26
  364. package/src/time-series/resolution.ts +41 -10
  365. package/src/time-series/resolve.ts +7 -0
  366. package/src/time-series/types.ts +3 -1
  367. package/src/time-series/use-chart-resolution.ts +5 -0
  368. package/src/types/config.ts +123 -3
  369. package/src/types/data-provider.ts +11 -0
  370. package/src/types/headless.ts +161 -0
  371. package/src/types/plugin.ts +102 -0
  372. package/src/ui/host.tsx +12 -1
  373. package/src/ui/index.tsx +1 -0
  374. package/src/ui/toast.tsx +8 -4
  375. package/src/utils/format.ts +35 -0
  376. package/src/utils/fuzzy-search.test.ts +94 -0
  377. package/src/utils/fuzzy-search.ts +81 -22
  378. package/src/utils/shortcut-labels.ts +14 -0
  379. package/src/components/chart/core/axis-format.ts +0 -157
  380. package/src/components/chart/core/index.test.ts +0 -1075
  381. package/src/components/chart/core/projection.ts +0 -357
  382. package/src/components/chart/core/render-utils.ts +0 -4
  383. package/src/components/chart/core/renderer.ts +0 -244
  384. package/src/components/chart/core/scene.ts +0 -180
  385. package/src/components/chart/core/time-axis.ts +0 -330
  386. package/src/components/chart/cursor-motion.ts +0 -47
  387. package/src/components/chart/indicators/types.ts +0 -26
  388. package/src/components/chart/market-session.ts +0 -176
  389. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  390. package/src/components/chart/multi-line/renderer.ts +0 -161
  391. package/src/components/chart/multi-line/scene.ts +0 -188
  392. package/src/components/chart/native/raster/layout.ts +0 -129
  393. package/src/components/chart/native/raster/price-chart.ts +0 -244
  394. package/src/components/chart/rendering/chart-draw.ts +0 -270
  395. package/src/components/chart/rendering/geometry.ts +0 -95
  396. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  397. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  398. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  399. package/src/components/chart/static/chart/surface.tsx +0 -358
  400. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  401. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  402. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  403. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  404. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  405. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  406. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  407. package/src/plugins/builtin/substack/api/store.ts +0 -186
  408. package/src/plugins/builtin/substack/api/types.ts +0 -42
  409. package/src/plugins/builtin/substack/api.test.ts +0 -298
  410. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  411. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  412. package/src/plugins/builtin/substack/content.ts +0 -478
  413. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  414. package/src/plugins/builtin/substack/index.tsx +0 -49
  415. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  416. package/src/plugins/builtin/substack/model.test.ts +0 -155
  417. package/src/plugins/builtin/substack/normalize.ts +0 -343
  418. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  419. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  420. package/src/plugins/builtin/substack/pane.tsx +0 -547
  421. package/src/plugins/builtin/substack/read-state.ts +0 -30
  422. package/src/plugins/builtin/substack/table.ts +0 -111
  423. package/src/plugins/builtin/substack/types.ts +0 -71
  424. package/src/plugins/builtin/substack/utils.ts +0 -131
  425. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  426. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  427. package/src/plugins/ibkr/account-cache.ts +0 -24
  428. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  429. package/src/plugins/ibkr/config.test.ts +0 -79
  430. package/src/plugins/ibkr/config.ts +0 -250
  431. package/src/plugins/ibkr/dialogs.tsx +0 -40
  432. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  433. package/src/plugins/ibkr/flex/index.ts +0 -367
  434. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  435. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  436. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  437. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  438. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  439. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  440. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  441. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  442. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  443. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  444. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  445. package/src/plugins/ibkr/gateway/history.ts +0 -120
  446. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  447. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  448. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  449. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  450. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  451. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  452. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  453. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  454. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  455. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  456. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  457. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  458. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  459. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  460. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  461. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  462. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  463. package/src/plugins/ibkr/gateway/types.ts +0 -27
  464. package/src/plugins/ibkr/index.test.tsx +0 -162
  465. package/src/plugins/ibkr/index.tsx +0 -159
  466. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  467. package/src/plugins/ibkr/instance-selection.ts +0 -36
  468. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  469. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  470. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  471. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  472. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  473. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  474. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  475. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  476. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  477. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  478. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  479. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  480. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  481. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  482. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  483. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  484. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  485. package/src/plugins/ibkr/trade/utils.ts +0 -144
  486. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  487. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  488. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  489. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -0,0 +1,350 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ BUFFETT_INDICATOR,
4
+ EXCESS_CAPE_YIELD,
5
+ INDICATORS,
6
+ SHILLER_CAPE,
7
+ SP500_DIVIDEND_YIELD,
8
+ TOBINS_Q,
9
+ alignToDenominator,
10
+ buildValuationSeries,
11
+ chartYearLabels,
12
+ classifyZone,
13
+ fitTrend,
14
+ meanRatio,
15
+ niceDomain,
16
+ projectChart,
17
+ projectView,
18
+ resolveIndicatorArg,
19
+ sigmaVsTrend,
20
+ sliceByRange,
21
+ trendAt,
22
+ zoneScaleBands,
23
+ zoneScaleFraction,
24
+ zoneScaleValueAt,
25
+ type DatedSeries,
26
+ type IndicatorBuild,
27
+ type RatioPoint,
28
+ type ScaledObs,
29
+ } from "./model";
30
+
31
+ function scaled(date: string, value: number): ScaledObs {
32
+ return { date, value };
33
+ }
34
+
35
+ function series(observations: Array<{ date: string; value: number | null }>): DatedSeries {
36
+ return { seriesId: "test", observations, provenance: "fred" };
37
+ }
38
+
39
+ function ratioPoint(date: string, ratio: number): RatioPoint {
40
+ return { date, ratio };
41
+ }
42
+
43
+ describe("alignToDenominator", () => {
44
+ const denominator = [scaled("2024-01-01", 20_000), scaled("2024-04-01", 22_000)];
45
+
46
+ test("midpoint between two quarterlies is linear", () => {
47
+ const points = alignToDenominator(
48
+ BUFFETT_INDICATOR,
49
+ [scaled("2024-02-15", 40_000)],
50
+ denominator,
51
+ true,
52
+ );
53
+ const t0 = Date.parse("2024-01-01");
54
+ const t1 = Date.parse("2024-04-01");
55
+ const t = Date.parse("2024-02-15");
56
+ const expected = 20_000 + ((t - t0) / (t1 - t0)) * 2_000;
57
+ expect(points[0]!.denominatorBillions).toBeCloseTo(expected, 6);
58
+ expect(points[0]!.ratio).toBeCloseTo((40_000 / expected) * 100, 6);
59
+ });
60
+
61
+ test("flat-forwards past the last denominator print", () => {
62
+ const points = alignToDenominator(
63
+ BUFFETT_INDICATOR,
64
+ [scaled("2024-06-01", 44_000)],
65
+ denominator,
66
+ true,
67
+ );
68
+ expect(points[0]!.denominatorBillions).toBe(22_000);
69
+ });
70
+
71
+ test("drops numerator dates before the first denominator print", () => {
72
+ const points = alignToDenominator(
73
+ BUFFETT_INDICATOR,
74
+ [scaled("2023-12-01", 39_000), scaled("2024-02-01", 40_000)],
75
+ denominator,
76
+ true,
77
+ );
78
+ expect(points.map((p) => p.date)).toEqual(["2024-02-01"]);
79
+ });
80
+
81
+ test("omits dollar levels when the ratio has none worth showing", () => {
82
+ const points = alignToDenominator(
83
+ SP500_DIVIDEND_YIELD,
84
+ [scaled("2024-02-15", 80)],
85
+ [scaled("2024-01-01", 4000)],
86
+ false,
87
+ );
88
+ expect(points[0]!.numeratorBillions).toBeUndefined();
89
+ expect(points[0]!.ratio).toBeCloseTo(2, 6);
90
+ });
91
+ });
92
+
93
+ describe("buildValuationSeries", () => {
94
+ test("scales each leg into billions before dividing", () => {
95
+ const legs = new Map<string, DatedSeries>([
96
+ ["NCBEILQ027S", series([{ date: "2024-01-01", value: 60_000_000 }])],
97
+ ["TNWMVBSNNCB", series([{ date: "2024-01-01", value: 40_000_000 }])],
98
+ ]);
99
+ const built = buildValuationSeries(TOBINS_Q, legs);
100
+ expect(built.points[0]!.numeratorBillions).toBeCloseTo(60_000, 6);
101
+ expect(built.points[0]!.ratio).toBeCloseTo(1.5, 6);
102
+ });
103
+
104
+ test("a direct indicator uses its single leg as the value", () => {
105
+ const legs = new Map<string, DatedSeries>([
106
+ ["SHILLER_CAPE", series([
107
+ { date: "2026-07-01", value: 40.6 },
108
+ { date: "2026-08-01", value: 41.2 },
109
+ ])],
110
+ ]);
111
+ const built = buildValuationSeries(SHILLER_CAPE, legs);
112
+ expect(built.points.map((p) => p.ratio)).toEqual([40.6, 41.2]);
113
+ expect(built.points[0]!.numeratorBillions).toBeUndefined();
114
+ });
115
+
116
+ test("ratioScale turns a decimal fraction into a percent", () => {
117
+ const legs = new Map<string, DatedSeries>([
118
+ ["SHILLER_ECY", series([{ date: "2026-08-01", value: 0.0097 }])],
119
+ ]);
120
+ expect(buildValuationSeries(EXCESS_CAPE_YIELD, legs).points[0]!.ratio).toBeCloseTo(0.97, 6);
121
+ });
122
+
123
+ test("throws when a leg is missing", () => {
124
+ expect(() => buildValuationSeries(SHILLER_CAPE, new Map())).toThrow("missing");
125
+ });
126
+ });
127
+
128
+ describe("fitTrend", () => {
129
+ test("log model recovers a compounding growth rate", () => {
130
+ const originMs = Date.parse("2000-01-01");
131
+ const betaPerDay = 0.03 / 365.25;
132
+ const points: RatioPoint[] = [];
133
+ for (let year = 0; year <= 20; year += 1) {
134
+ const date = `${2000 + year}-01-01`;
135
+ const tDays = (Date.parse(date) - originMs) / 86_400_000;
136
+ points.push(ratioPoint(date, 80 * Math.exp(betaPerDay * tDays)));
137
+ }
138
+ const fit = fitTrend(points, "log");
139
+ expect(fit.beta).toBeCloseTo(betaPerDay, 8);
140
+ const outlierDate = points[10]!.date;
141
+ const outlier = trendAt(fit, outlierDate) * Math.exp(2);
142
+ expect(sigmaVsTrend({ ...fit, sigma: 0.05 }, outlier, outlierDate)).toBeCloseTo(40, 8);
143
+ });
144
+
145
+ test("linear model keeps the negative years a log fit would drop", () => {
146
+ const points = [
147
+ ratioPoint("2000-01-01", -2),
148
+ ratioPoint("2001-01-01", 0),
149
+ ratioPoint("2002-01-01", 2),
150
+ ratioPoint("2003-01-01", 4),
151
+ ];
152
+ const linear = fitTrend(points, "linear");
153
+ expect(linear.model).toBe("linear");
154
+ // Leap years make the day spacing uneven, so the fit lands near, not on, the ends.
155
+ expect(trendAt(linear, "2000-01-01")).toBeCloseTo(-2, 1);
156
+ expect(trendAt(linear, "2003-01-01")).toBeCloseTo(4, 1);
157
+ // The log fit sees only the two positive points, so its origin moves.
158
+ expect(fitTrend(points, "log").originMs).toBe(Date.parse("2002-01-01"));
159
+ });
160
+
161
+ test("sigma is zero for a value the model cannot place", () => {
162
+ const fit = fitTrend([ratioPoint("2020-01-01", 1), ratioPoint("2021-01-01", 2)], "log");
163
+ expect(sigmaVsTrend(fit, -1, "2021-01-01")).toBe(0);
164
+ });
165
+ });
166
+
167
+ describe("classifyZone", () => {
168
+ test("Buffett boundaries are half-open at 75, 90, 115, 135", () => {
169
+ expect(classifyZone(BUFFETT_INDICATOR, 74.999).id).toBe("significantly-undervalued");
170
+ expect(classifyZone(BUFFETT_INDICATOR, 90).id).toBe("fair");
171
+ expect(classifyZone(BUFFETT_INDICATOR, 135).id).toBe("significantly-overvalued");
172
+ });
173
+
174
+ test("a yield-shaped indicator reads the other way round", () => {
175
+ // A high excess yield means stocks are cheap, unlike a high price ratio.
176
+ expect(classifyZone(EXCESS_CAPE_YIELD, 0.5).id).toBe("significantly-overvalued");
177
+ expect(classifyZone(EXCESS_CAPE_YIELD, 3.3).id).toBe("fair");
178
+ expect(classifyZone(EXCESS_CAPE_YIELD, 8).id).toBe("significantly-undervalued");
179
+ expect(classifyZone(SP500_DIVIDEND_YIELD, 1.1).id).toBe("significantly-overvalued");
180
+ expect(classifyZone(SP500_DIVIDEND_YIELD, 7).id).toBe("significantly-undervalued");
181
+ });
182
+ });
183
+
184
+ describe("zone scale geometry", () => {
185
+ test("bands span min to max with classifyZone's colors, for every indicator", () => {
186
+ for (const indicator of INDICATORS) {
187
+ const bands = zoneScaleBands(indicator);
188
+ expect(bands[0]!.from).toBe(indicator.zoneScale.min);
189
+ expect(bands.at(-1)!.to).toBe(indicator.zoneScale.max);
190
+ for (const band of bands) {
191
+ expect(band.color).toBe(classifyZone(indicator, (band.from + band.to) / 2).color);
192
+ }
193
+ }
194
+ });
195
+
196
+ test("edges land on equal fractions and round-trip, including a negative floor", () => {
197
+ for (const indicator of INDICATORS) {
198
+ const edges = indicator.zoneScale.edges;
199
+ const bands = edges.length - 1;
200
+ for (let index = 0; index <= bands; index += 1) {
201
+ const edge = edges[index]!;
202
+ expect(zoneScaleFraction(indicator, edge)).toBeCloseTo(index / bands, 8);
203
+ expect(zoneScaleValueAt(indicator, index / bands)).toBeCloseTo(edge, 6);
204
+ }
205
+ }
206
+ expect(zoneScaleFraction(EXCESS_CAPE_YIELD, -3)).toBe(0);
207
+ expect(zoneScaleFraction(EXCESS_CAPE_YIELD, -99)).toBe(0);
208
+ });
209
+ });
210
+
211
+ describe("niceDomain", () => {
212
+ test("snaps up to whole grid steps", () => {
213
+ expect(niceDomain(0, 142, 150)).toEqual({ min: 0, max: 150 });
214
+ expect(niceDomain(0, 214, 150)).toEqual({ min: 0, max: 300 });
215
+ expect(niceDomain(0, 1.82, 0.5)).toEqual({ min: 0, max: 2 });
216
+ });
217
+
218
+ test("opens a floor when the measure goes negative", () => {
219
+ expect(niceDomain(-2.6, 12, 5)).toEqual({ min: -5, max: 15 });
220
+ });
221
+ });
222
+
223
+ describe("projectChart", () => {
224
+ const points = [
225
+ ratioPoint("2020-01-01", 70),
226
+ ratioPoint("2021-01-01", 100),
227
+ ratioPoint("2022-01-01", 160),
228
+ ];
229
+
230
+ test("draws the reference and the mean as separate labelled lines", () => {
231
+ const mean = meanRatio(points);
232
+ const chart = projectChart(BUFFETT_INDICATOR, points, mean);
233
+ expect(chart.markers.map((marker) => marker.label)).toEqual(["parity", "mean"]);
234
+ expect(chart.referenceLines.map((line) => line.value)).toEqual([100, mean]);
235
+ expect(mean).not.toBeCloseTo(100, 6);
236
+ });
237
+
238
+ test("colors each segment from its own zone", () => {
239
+ const chart = projectChart(BUFFETT_INDICATOR, points, meanRatio(points));
240
+ expect(chart.lineColors)
241
+ .toEqual(points.map((p) => classifyZone(BUFFETT_INDICATOR, p.ratio).color));
242
+ });
243
+
244
+ test("keeps a negative reference on screen", () => {
245
+ const negative = [ratioPoint("2020-01-01", -2), ratioPoint("2021-01-01", 1)];
246
+ const chart = projectChart(EXCESS_CAPE_YIELD, negative, meanRatio(negative));
247
+ expect(chart.yDomain.min).toBeLessThan(0);
248
+ });
249
+ });
250
+
251
+ describe("chartYearLabels", () => {
252
+ test("keeps unique years in order and thins long spans", () => {
253
+ const points = Array.from({ length: 60 }, (_, index) => ({
254
+ date: new Date(`${1970 + index}-06-01`),
255
+ open: 1,
256
+ high: 1,
257
+ low: 1,
258
+ close: 1,
259
+ volume: 0,
260
+ }));
261
+ const labels = chartYearLabels(points);
262
+ expect(labels.length).toBeLessThanOrEqual(8);
263
+ expect(labels[0]).toBe("1970");
264
+ expect([...labels]).toEqual([...new Set(labels)]);
265
+ });
266
+ });
267
+
268
+ describe("projectView", () => {
269
+ const points: RatioPoint[] = [];
270
+ for (let year = 1990; year <= 2026; year += 1) {
271
+ points.push(ratioPoint(`${year}-01-01`, 60 + (year - 1990) * 3));
272
+ }
273
+ const build: IndicatorBuild = {
274
+ indicator: BUFFETT_INDICATOR,
275
+ series: { indicatorId: "buffett", points, vintageDate: "2026-01-01" },
276
+ trend: fitTrend(points, "log"),
277
+ };
278
+
279
+ test("range changes the chart window but not the headline stats", () => {
280
+ const short = projectView(build, "10Y", { nowMs: Date.parse("2026-02-01") });
281
+ const all = projectView(build, "ALL", { nowMs: Date.parse("2026-02-01") });
282
+ expect(short.current).toEqual(all.current);
283
+ expect(short.percentile).toBe(all.percentile);
284
+ expect(short.allTimeHigh).toEqual(all.allTimeHigh);
285
+ expect(short.chart.points.length).toBeLessThan(all.chart.points.length);
286
+ });
287
+
288
+ test("staleness uses the indicator's own cadence", () => {
289
+ // Three months past the last print: overdue daily, normal for quarterly Z.1.
290
+ const nowMs = Date.parse("2026-04-01");
291
+ expect(projectView(build, "ALL", { nowMs }).observationStale).toBe(true);
292
+ const quarterly: IndicatorBuild = { ...build, indicator: TOBINS_Q };
293
+ expect(projectView(quarterly, "ALL", { nowMs }).observationStale).toBe(false);
294
+ expect(projectView(quarterly, "ALL", { nowMs: Date.parse("2026-09-02") }).observationStale)
295
+ .toBe(false);
296
+ expect(projectView(quarterly, "ALL", { nowMs: Date.parse("2027-02-01") }).observationStale)
297
+ .toBe(true);
298
+ });
299
+
300
+ test("a direct indicator has no vintage label to show", () => {
301
+ const direct: IndicatorBuild = { ...build, indicator: SHILLER_CAPE };
302
+ expect(projectView(direct, "ALL").vintageLabel).toBeNull();
303
+ expect(projectView(build, "ALL").vintageLabel).toContain("GDP as of");
304
+ });
305
+ });
306
+
307
+ describe("richness normalisation", () => {
308
+ const rising: RatioPoint[] = [];
309
+ for (let year = 2000; year <= 2026; year += 1) {
310
+ rising.push(ratioPoint(`${year}-01-01`, 1 + (year - 2000) * 0.2));
311
+ }
312
+
313
+ test("a yield at its historic low reads as the richest, not the cheapest", () => {
314
+ // Same rising series, read as a price ratio and as a yield.
315
+ const asPrice = projectView({
316
+ indicator: BUFFETT_INDICATOR,
317
+ series: { indicatorId: "b", points: rising, vintageDate: "2026-01-01" },
318
+ trend: fitTrend(rising, "log"),
319
+ }, "ALL");
320
+ const asYield = projectView({
321
+ indicator: SP500_DIVIDEND_YIELD,
322
+ series: { indicatorId: "d", points: rising, vintageDate: "2026-01-01" },
323
+ trend: fitTrend(rising, "log"),
324
+ }, "ALL");
325
+
326
+ expect(asPrice.percentile).toBeCloseTo(asYield.percentile, 8);
327
+ // The raw percentile is identical, but a high yield is cheap.
328
+ expect(asPrice.richPercentile).toBeCloseTo(100, 6);
329
+ expect(asYield.richPercentile).toBeCloseTo(0, 6);
330
+ expect(Math.sign(asPrice.richSigma)).toBe(-Math.sign(asYield.richSigma));
331
+ expect(asYield.richSigma).toBeCloseTo(-asYield.sigmaVsTrend, 8);
332
+ });
333
+ });
334
+
335
+ describe("sliceByRange", () => {
336
+ test("falls back to the whole series when a window leaves under two points", () => {
337
+ expect(sliceByRange([ratioPoint("1990-01-01", 60), ratioPoint("1991-01-01", 62)], "10Y"))
338
+ .toHaveLength(2);
339
+ });
340
+ });
341
+
342
+ describe("resolveIndicatorArg", () => {
343
+ test("matches ids, short labels and prefixes so VAL <name> deep-links", () => {
344
+ expect(resolveIndicatorArg("cape")?.id).toBe("shiller-cape");
345
+ expect(resolveIndicatorArg("buffett")?.id).toBe("buffett");
346
+ expect(resolveIndicatorArg("tobin q")?.id).toBe("tobins-q");
347
+ expect(resolveIndicatorArg("")).toBeNull();
348
+ expect(resolveIndicatorArg("nonsense")).toBeNull();
349
+ });
350
+ });
@@ -0,0 +1,8 @@
1
+ export * from "./defs";
2
+ export * from "./indicators";
3
+ export * from "./series";
4
+ export * from "./align";
5
+ export * from "./trend";
6
+ export * from "./chart-projection";
7
+ export * from "./view";
8
+ export * from "./sources";
@@ -0,0 +1,247 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { act } from "react";
3
+ import { PaneFooterProvider } from "../../../components/layout/pane/footer";
4
+ import {
5
+ attachValuationPersistence,
6
+ hydrateValuationSeries,
7
+ resetValuationPersistence,
8
+ } from "./cache";
9
+ import { testRender } from "../../../renderers/opentui/test-utils";
10
+ import {
11
+ AppContext,
12
+ createInitialState,
13
+ PaneInstanceProvider,
14
+ } from "../../../state/app/context";
15
+ import { MemoryPluginPersistence } from "../../../test-support/plugin-persistence";
16
+ import { cloneLayout, createDefaultConfig } from "../../../types/config";
17
+ import { MarketValuationPane, shouldPersistSelection } from "./pane";
18
+
19
+ let setup: Awaited<ReturnType<typeof testRender>> | undefined;
20
+
21
+ function obs(values: Array<[string, number]>) {
22
+ return values.map(([date, value]) => ({ date, value }));
23
+ }
24
+
25
+ /** Wilshire 76.2T over GDP 32.5T is 235%; the Z.1 pair is 38.0T / 40.0T, or 0.95. */
26
+ const LEGS: Array<[string, Array<{ date: string; value: number }>]> = [
27
+ ["W5000", obs([
28
+ ["2024-01-02", 60_000],
29
+ ["2025-01-02", 68_000],
30
+ ["2026-06-15", 76_200],
31
+ ])],
32
+ ["GDP", obs([
33
+ ["2024-01-01", 28_000],
34
+ ["2025-01-01", 30_000],
35
+ ["2026-04-01", 32_500],
36
+ ])],
37
+ ["M2SL", obs([
38
+ ["2024-01-01", 21_000],
39
+ ["2025-01-01", 22_000],
40
+ ["2026-07-01", 23_200],
41
+ ])],
42
+ ["NCBEILQ027S", obs([
43
+ ["2024-01-01", 30_000_000],
44
+ ["2025-01-01", 35_000_000],
45
+ ["2026-01-01", 38_000_000],
46
+ ])],
47
+ ["TNWMVBSNNCB", obs([
48
+ ["2024-01-01", 38_000_000],
49
+ ["2025-01-01", 39_200_000],
50
+ ["2026-01-01", 40_000_000],
51
+ ])],
52
+ ["BOGZ1FL153064486Q", obs([
53
+ ["2024-01-01", 38.0],
54
+ ["2025-01-01", 42.0],
55
+ ["2026-01-01", 45.8],
56
+ ])],
57
+ ["BOGZ1FL663067003Q", obs([
58
+ ["2024-01-01", 600_000],
59
+ ["2025-01-01", 620_000],
60
+ ["2026-01-01", 650_000],
61
+ ])],
62
+ ["CPROFIT", obs([["2024-01-01", 3_400], ["2025-01-01", 4_200], ["2026-01-01", 4_800]])],
63
+ ["SHILLER_CAPE", obs([
64
+ ["2024-01-01", 33.2],
65
+ ["2025-01-01", 38.1],
66
+ ["2026-08-01", 41.2],
67
+ ])],
68
+ ["SHILLER_ECY", obs([
69
+ ["2024-01-01", 0.021],
70
+ ["2025-01-01", 0.013],
71
+ ["2026-08-01", 0.0097],
72
+ ])],
73
+ ["SHILLER_DIVIDEND", obs([
74
+ ["2024-01-01", 70],
75
+ ["2025-01-01", 76],
76
+ ["2026-08-01", 83],
77
+ ])],
78
+ ["SHILLER_PRICE", obs([
79
+ ["2024-01-01", 4800],
80
+ ["2025-01-01", 6000],
81
+ ["2026-08-01", 7600],
82
+ ])],
83
+ ];
84
+
85
+ async function settle() {
86
+ await act(async () => {
87
+ for (let index = 0; index < 8; index += 1) {
88
+ await Promise.resolve();
89
+ await setup!.renderOnce();
90
+ }
91
+ });
92
+ for (let index = 0; index < 3; index += 1) {
93
+ await act(async () => {
94
+ await Promise.resolve();
95
+ await setup!.renderOnce();
96
+ });
97
+ }
98
+ }
99
+
100
+ const TEST_PANE_ID = "valuation:test";
101
+
102
+ async function renderPane(settings: Record<string, unknown> = {}, width = 128) {
103
+ const layout = {
104
+ dockRoot: { kind: "pane" as const, instanceId: TEST_PANE_ID },
105
+ instances: [{ instanceId: TEST_PANE_ID, paneId: "market-valuation", settings }],
106
+ floating: [],
107
+ detached: [],
108
+ };
109
+ const state = createInitialState({
110
+ ...createDefaultConfig("/tmp/gloomberb-valuation-test"),
111
+ layout,
112
+ layouts: [{ name: "Default", layout: cloneLayout(layout) }],
113
+ });
114
+ setup = await testRender(
115
+ <AppContext value={{ state, dispatch: () => {} }}>
116
+ <PaneInstanceProvider paneId={TEST_PANE_ID}>
117
+ <PaneFooterProvider>
118
+ {() => (
119
+ <MarketValuationPane
120
+ paneId={TEST_PANE_ID}
121
+ paneType="market-valuation"
122
+ focused
123
+ width={width}
124
+ height={40}
125
+ />
126
+ )}
127
+ </PaneFooterProvider>
128
+ </PaneInstanceProvider>
129
+ </AppContext>,
130
+ { width, height: 40 },
131
+ );
132
+ await settle();
133
+ return setup.captureCharFrame();
134
+ }
135
+
136
+ beforeEach(() => {
137
+ resetValuationPersistence();
138
+ attachValuationPersistence(new MemoryPluginPersistence());
139
+ hydrateValuationSeries(LEGS);
140
+ });
141
+
142
+ afterEach(async () => {
143
+ if (setup) {
144
+ await act(async () => setup?.renderer.destroy());
145
+ setup = undefined;
146
+ }
147
+ resetValuationPersistence();
148
+ });
149
+
150
+ describe("MarketValuationPane", () => {
151
+ test("summarizes every indicator in one table, each in its own units", async () => {
152
+ const frame = await renderPane();
153
+ // A percent, a bare multiple, and two yields all sit in one VALUE column.
154
+ for (const label of ["Buffett", "CAPE", "Tobin Q", "Equity alloc", "Div yield", "Cap / M2"]) {
155
+ expect(frame).toContain(label);
156
+ }
157
+ expect(frame).toContain("234%");
158
+ expect(frame).toContain("41.2");
159
+ expect(frame).toContain("0.95");
160
+ expect(frame).toContain("329%");
161
+ });
162
+
163
+ test("wide panes put the list beside the detail, narrow ones stack it", async () => {
164
+ const split = await renderPane({}, 128);
165
+ // In the split the chart shares a line with the list rows.
166
+ expect(split).toMatch(/Buffett.*\n/);
167
+ const stacked = await renderPane({}, 92);
168
+ expect(stacked).toContain("Buffett");
169
+ expect(stacked).toContain("Cap / M2");
170
+ });
171
+
172
+ test("the filter narrows the list without blanking the detail", async () => {
173
+ const frame = await renderPane();
174
+ expect(frame).toContain("filter indicators");
175
+ });
176
+
177
+ test("a yield reads cheap when it is high, unlike a price ratio", async () => {
178
+ const frame = await renderPane();
179
+ // Both Buffett at 234% and an excess CAPE yield of 1.0% mean expensive.
180
+ expect(frame).toMatch(/Buffett\s+234%\s+Sig\. over/);
181
+ expect(frame).toMatch(/ERP \(ECY\)\s+1\.0%\s+Sig\. over/);
182
+ });
183
+
184
+ test("detail follows the selected indicator without repeating the row", async () => {
185
+ const frame = await renderPane({ indicator: "tobins-q" });
186
+ expect(frame).toContain("Equities");
187
+ expect(frame).toContain("Net worth");
188
+ expect(frame).toContain("replacement cost");
189
+ expect(frame).not.toContain("Mkt cap");
190
+ // The row above already names the indicator and its zone.
191
+ expect(frame).not.toContain("Fair Valued");
192
+ });
193
+
194
+ test("a direct indicator shows no dollar levels row", async () => {
195
+ const frame = await renderPane({ indicator: "shiller-cape" });
196
+ expect(frame).toContain("ten years of real earnings");
197
+ expect(frame).not.toContain("Mkt cap");
198
+ expect(frame).not.toContain("Equities");
199
+ });
200
+
201
+ test("draws the mean apart from the reference line", async () => {
202
+ const frame = await renderPane();
203
+ expect(frame).toContain("parity");
204
+ expect(frame).toContain("mean");
205
+ });
206
+ });
207
+
208
+ describe("shouldPersistSelection", () => {
209
+ const knownIds = ["buffett", "shiller-cape", "tobins-q"];
210
+
211
+ test("keeps the setting when a filter moved the rows under a pending commit", () => {
212
+ // The row the keyboard commit resolves to is real, but the user never chose it.
213
+ expect(shouldPersistSelection({
214
+ id: "tobins-q",
215
+ reason: "keyboard",
216
+ selectionOnScreen: false,
217
+ knownIds,
218
+ })).toBe(false);
219
+ });
220
+
221
+ test("honours an explicit click even while the selection is filtered away", () => {
222
+ expect(shouldPersistSelection({
223
+ id: "shiller-cape",
224
+ reason: "pointer",
225
+ selectionOnScreen: false,
226
+ knownIds,
227
+ })).toBe(true);
228
+ });
229
+
230
+ test("accepts ordinary keyboard movement", () => {
231
+ expect(shouldPersistSelection({
232
+ id: "shiller-cape",
233
+ reason: "keyboard",
234
+ selectionOnScreen: true,
235
+ knownIds,
236
+ })).toBe(true);
237
+ });
238
+
239
+ test("never persists an indicator that is not in the registry", () => {
240
+ expect(shouldPersistSelection({
241
+ id: "nonsense",
242
+ reason: "pointer",
243
+ selectionOnScreen: true,
244
+ knownIds,
245
+ })).toBe(false);
246
+ });
247
+ });