gloomberb 0.11.2 → 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 (531) hide show
  1. package/README.md +7 -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 +108 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +134 -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 +220 -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/capabilities/chart-series.ts +3 -2
  20. package/src/cli/commands/market.ts +2 -2
  21. package/src/cli/commands/plugins.ts +39 -11
  22. package/src/cli/desktop-pane-shot.ts +285 -16
  23. package/src/cli/index.ts +1 -1
  24. package/src/cli/pane-functions/capabilities.ts +61 -31
  25. package/src/cli/pane-functions/catalog.ts +16 -4
  26. package/src/cli/pane-functions/discovery.ts +1 -0
  27. package/src/cli/pane-functions/headless.test.ts +247 -0
  28. package/src/cli/pane-functions/headless.ts +423 -0
  29. package/src/cli/pane-functions/index.test.ts +9 -0
  30. package/src/cli/pane-functions/index.ts +6 -2
  31. package/src/cli/pane-functions/options.ts +27 -2
  32. package/src/cli/pane-functions/report.ts +4 -0
  33. package/src/cli/pane-functions/resolver.ts +26 -8
  34. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  35. package/src/cli/pane-functions/screenshot.ts +236 -20
  36. package/src/cli/registry.ts +0 -22
  37. package/src/cli/restore-plugins.ts +34 -0
  38. package/src/cli/result.ts +7 -10
  39. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  40. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  41. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  42. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  43. package/src/components/chart/composite/composite-chart.tsx +301 -204
  44. package/src/components/chart/composite/format.ts +8 -2
  45. package/src/components/chart/composite/interactions.test.ts +68 -102
  46. package/src/components/chart/composite/interactions.ts +214 -264
  47. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  48. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  49. package/src/components/chart/composite/scene.ts +57 -3
  50. package/src/components/chart/composite/text-renderer.ts +3 -2
  51. package/src/components/chart/composite/time-scale.ts +138 -57
  52. package/src/components/chart/composite/types.ts +25 -1
  53. package/src/components/chart/core/data.ts +9 -13
  54. package/src/components/chart/core/date-window.ts +7 -1
  55. package/src/components/chart/core/pointer.ts +3 -0
  56. package/src/components/chart/core/types.ts +0 -45
  57. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  58. package/src/components/chart/native/kitty/index.test.ts +0 -188
  59. package/src/components/chart/native/raster/crosshair.ts +6 -5
  60. package/src/components/chart/native/raster/primitives.ts +0 -23
  61. package/src/components/chart/native/renderer-selection.ts +81 -31
  62. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  63. package/src/components/chart/static/chart-surface.tsx +150 -0
  64. package/src/components/chart/static/index.ts +8 -0
  65. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  66. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  67. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  68. package/src/components/chart/static/series.ts +49 -0
  69. package/src/components/command-bar/action-runtime.ts +1 -3
  70. package/src/components/command-bar/assist/model.test.ts +17 -2
  71. package/src/components/command-bar/assist/model.ts +40 -9
  72. package/src/components/command-bar/commands/direct/index.ts +1 -1
  73. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  74. package/src/components/command-bar/commands/registry.ts +0 -11
  75. package/src/components/command-bar/helpers.test.ts +2 -2
  76. package/src/components/command-bar/helpers.ts +2 -6
  77. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  78. package/src/components/command-bar/list/badge.test.ts +54 -0
  79. package/src/components/command-bar/list/badge.ts +62 -0
  80. package/src/components/command-bar/list/model.test.ts +103 -0
  81. package/src/components/command-bar/list/model.ts +61 -4
  82. package/src/components/command-bar/list/view.test.tsx +113 -0
  83. package/src/components/command-bar/list/view.tsx +147 -131
  84. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  85. package/src/components/command-bar/panel/body.tsx +51 -65
  86. package/src/components/command-bar/panel/index.tsx +48 -13
  87. package/src/components/command-bar/panel/layout.test.ts +139 -0
  88. package/src/components/command-bar/panel/layout.ts +65 -28
  89. package/src/components/command-bar/panel/native-surface.ts +24 -0
  90. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  91. package/src/components/command-bar/panel/runtime.ts +6 -3
  92. package/src/components/command-bar/panel/state.ts +41 -7
  93. package/src/components/command-bar/panel/types.ts +7 -1
  94. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  95. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  96. package/src/components/command-bar/routes/root/results.ts +30 -28
  97. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  98. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  99. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  100. package/src/components/command-bar/routes/root/selection.ts +1 -12
  101. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  102. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  103. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  104. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  105. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  106. package/src/components/command-bar/routing/actions.ts +1 -81
  107. package/src/components/command-bar/routing/launch-request.ts +1 -1
  108. package/src/components/command-bar/routing/list-state.ts +6 -21
  109. package/src/components/command-bar/selection-runtime.ts +1 -1
  110. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  111. package/src/components/command-bar/surface/environment.ts +8 -1
  112. package/src/components/command-bar/surface/index.test.tsx +84 -68
  113. package/src/components/command-bar/surface/index.tsx +33 -36
  114. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  115. package/src/components/command-bar/theme-picker.tsx +43 -15
  116. package/src/components/command-bar/view-model.test.ts +53 -8
  117. package/src/components/command-bar/view-model.ts +43 -14
  118. package/src/components/command-bar/workflow/tickers.ts +1 -0
  119. package/src/components/command-bar/workflow/types.ts +1 -1
  120. package/src/components/data-table/export.test.ts +21 -0
  121. package/src/components/data-table/export.ts +21 -0
  122. package/src/components/data-table/view.test.tsx +45 -6
  123. package/src/components/data-table/view.tsx +55 -9
  124. package/src/components/index.ts +10 -2
  125. package/src/components/input-search-bar.test.tsx +36 -2
  126. package/src/components/input-search-bar.tsx +18 -1
  127. package/src/components/layout/header.test.tsx +57 -10
  128. package/src/components/layout/header.tsx +292 -149
  129. package/src/components/layout/market-summary.test.ts +81 -0
  130. package/src/components/layout/market-summary.ts +109 -0
  131. package/src/components/layout/shell/chrome.ts +96 -1
  132. package/src/components/layout/shell/index.tsx +11 -3
  133. package/src/components/layout/shell/menu.ts +9 -0
  134. package/src/components/layout/shell/pane/actions.ts +29 -0
  135. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  136. package/src/components/layout/shell/shortcuts.ts +3 -0
  137. package/src/components/layout/status-bar.test.tsx +9 -8
  138. package/src/components/layout/status-bar.tsx +85 -54
  139. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  140. package/src/components/price-sparkline/index.ts +61 -34
  141. package/src/components/price-sparkline/view.tsx +1 -1
  142. package/src/components/sign-in-gate.test.tsx +128 -0
  143. package/src/components/sign-in-gate.tsx +149 -0
  144. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  145. package/src/components/ticker/badge/list.tsx +17 -3
  146. package/src/components/ticker/list-table-view.tsx +6 -3
  147. package/src/components/ui/data-table/index.tsx +22 -4
  148. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  149. package/src/components/ui/frame.tsx +28 -0
  150. package/src/components/ui/index.ts +3 -0
  151. package/src/components/ui/native-select.tsx +20 -10
  152. package/src/components/ui/select-button.tsx +138 -0
  153. package/src/components/ui/status.tsx +2 -2
  154. package/src/components/ui/table-layout.ts +1 -1
  155. package/src/components/use-double-click-activation.ts +7 -3
  156. package/src/core/app-services.ts +3 -3
  157. package/src/data/config/chart-settings.ts +4 -4
  158. package/src/data/config/layout.ts +12 -6
  159. package/src/data/config/store/index.test.ts +97 -0
  160. package/src/data/config/store/migrations.ts +41 -0
  161. package/src/data/config/store/normalize.ts +5 -3
  162. package/src/i18n/es.ts +6 -2
  163. package/src/i18n/ja.ts +6 -2
  164. package/src/i18n/ko.ts +6 -2
  165. package/src/i18n/zh-cn.ts +6 -2
  166. package/src/i18n/zh-tw.ts +6 -2
  167. package/src/layout-marketplace/gallery-terminal.tsx +126 -96
  168. package/src/layout-marketplace/gallery.test.tsx +65 -1
  169. package/src/layout-marketplace/pane-imagery.ts +1 -0
  170. package/src/layout-marketplace/payload.test.ts +17 -0
  171. package/src/layout-marketplace/payload.ts +40 -5
  172. package/src/market-data/coordinator/index.ts +3 -2
  173. package/src/market-data/coordinator/quotes.ts +173 -66
  174. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  175. package/src/pane-settings.ts +1 -1
  176. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  177. package/src/plugins/builtin/analytics/view.tsx +1 -1
  178. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  179. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  180. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  181. package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
  182. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  183. package/src/plugins/builtin/chart-composer/settings.ts +2 -13
  184. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  185. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  186. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  188. package/src/plugins/builtin/composite-plugins.ts +16 -1
  189. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  190. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  191. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  192. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  193. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  194. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  195. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  196. package/src/plugins/builtin/earnings/index.tsx +1 -0
  197. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  198. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  199. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  200. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  201. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  202. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  203. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  204. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  205. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  206. package/src/plugins/builtin/econ/index.tsx +3 -1
  207. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  208. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  209. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  210. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  211. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  212. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  213. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  214. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  215. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  216. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  217. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  218. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  219. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  220. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  221. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  222. package/src/plugins/builtin/futures/index.tsx +1 -0
  223. package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
  224. package/src/plugins/builtin/help/index.tsx +4 -0
  225. package/src/plugins/builtin/holders/index.tsx +1 -0
  226. package/src/plugins/builtin/insider/index.tsx +1 -0
  227. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
  228. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  229. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  230. package/src/plugins/builtin/market-halts/index.tsx +1 -0
  231. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  232. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  233. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  234. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  235. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  236. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  237. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  238. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  239. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  240. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  241. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  242. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  243. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  244. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  245. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  246. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  247. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  248. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  249. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  250. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  251. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  252. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  253. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  254. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  255. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  256. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  257. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  258. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  259. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  260. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  261. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  262. package/src/plugins/builtin/options/analytics.test.ts +83 -0
  263. package/src/plugins/builtin/options/analytics.ts +110 -0
  264. package/src/plugins/builtin/options/index.tsx +18 -2
  265. package/src/plugins/builtin/options/table.ts +121 -51
  266. package/src/plugins/builtin/options/types.ts +22 -15
  267. package/src/plugins/builtin/options/view.test.tsx +53 -1
  268. package/src/plugins/builtin/options/view.tsx +92 -41
  269. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  270. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  271. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  272. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  273. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  274. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  275. package/src/plugins/builtin/plugin-module.test.ts +2 -4
  276. package/src/plugins/builtin/plugin-module.ts +9 -8
  277. package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
  278. package/src/plugins/builtin/research/index.tsx +4 -0
  279. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  280. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  281. package/src/plugins/builtin/research-search/data.ts +109 -0
  282. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  283. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  284. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  285. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  286. package/src/plugins/builtin/research-search/index.tsx +85 -0
  287. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  288. package/src/plugins/builtin/research-search/model.ts +291 -0
  289. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  290. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  291. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  292. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  293. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  294. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  295. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  296. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  297. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  298. package/src/plugins/builtin/scanner/index.ts +2 -0
  299. package/src/plugins/builtin/sec/index.tsx +1 -0
  300. package/src/plugins/builtin/sectors/index.tsx +1 -0
  301. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  302. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  303. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  304. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  305. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
  306. package/src/plugins/builtin/ticker-detail/index.tsx +2 -14
  307. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  308. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  309. package/src/plugins/builtin/volatility/index.tsx +1 -1
  310. package/src/plugins/builtin/world-indices/index.tsx +1 -0
  311. package/src/plugins/builtin/world-venue-map/index.ts +27 -0
  312. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  313. package/src/plugins/builtin/world-venue-map/map.tsx +481 -0
  314. package/src/plugins/builtin/world-venue-map/model.test.ts +85 -0
  315. package/src/plugins/builtin/world-venue-map/model.ts +268 -0
  316. package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
  317. package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
  318. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  319. package/src/plugins/bundle.test.ts +109 -0
  320. package/src/plugins/bundle.ts +139 -0
  321. package/src/plugins/catalog-backend.ts +6 -44
  322. package/src/plugins/catalog-browser.test.ts +1 -0
  323. package/src/plugins/catalog-browser.ts +12 -0
  324. package/src/plugins/catalog-manifest.test.ts +43 -0
  325. package/src/plugins/catalog-manifest.ts +92 -0
  326. package/src/plugins/catalog-ui.ts +20 -4
  327. package/src/plugins/catalog.ts +9 -2
  328. package/src/plugins/current-target.ts +28 -0
  329. package/src/plugins/host-contract.ts +37 -0
  330. package/src/plugins/host-link.ts +158 -0
  331. package/src/plugins/host-modules.ts +61 -0
  332. package/src/plugins/loader.ts +41 -22
  333. package/src/plugins/ownership.ts +1 -0
  334. package/src/plugins/prediction-markets/index.tsx +1 -0
  335. package/src/plugins/registry/context.ts +3 -0
  336. package/src/plugins/registry/contributions.ts +27 -0
  337. package/src/plugins/registry/index.test.ts +25 -0
  338. package/src/plugins/registry/index.ts +8 -0
  339. package/src/plugins/registry/pane-settings.test.ts +53 -0
  340. package/src/plugins/registry/pane-settings.ts +29 -3
  341. package/src/plugins/seed.test.ts +83 -0
  342. package/src/plugins/seed.ts +80 -0
  343. package/src/public/broker.ts +31 -0
  344. package/src/public/market-data.ts +9 -0
  345. package/src/public/pane-hooks.ts +57 -0
  346. package/src/public/public-api.test.ts +285 -0
  347. package/src/public/react.ts +62 -0
  348. package/src/public/test-support.ts +23 -0
  349. package/src/public/time-series.ts +8 -0
  350. package/src/public/utils.ts +77 -0
  351. package/src/remote/layout-helpers.ts +6 -8
  352. package/src/remote/resources.ts +2 -0
  353. package/src/remote/schema.ts +1 -0
  354. package/src/remote/semantic-tree.test.tsx +105 -0
  355. package/src/remote/semantic-tree.tsx +53 -35
  356. package/src/renderers/browser/main.tsx +6 -0
  357. package/src/renderers/browser/ui-host.tsx +11 -0
  358. package/src/renderers/cloudflare/worker.test.ts +19 -0
  359. package/src/renderers/cloudflare/worker.ts +1 -1
  360. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
  361. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  362. package/src/renderers/electrobun/bun/index.ts +6 -2
  363. package/src/renderers/electrobun/shared/protocol.ts +25 -0
  364. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  365. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  366. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  367. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  368. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  369. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  370. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  371. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  372. package/src/renderers/electrobun/view/main.tsx +27 -2
  373. package/src/renderers/electrobun/view/styles.css +5 -0
  374. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  375. package/src/renderers/electrobun/view/ui-host.tsx +3 -0
  376. package/src/renderers/opentui/chart-surface.tsx +5 -1
  377. package/src/renderers/opentui/host.tsx +20 -2
  378. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  379. package/src/renderers/opentui/interaction-performance.ts +188 -0
  380. package/src/renderers/opentui/start.tsx +26 -1
  381. package/src/renderers/opentui/test-utils.tsx +13 -0
  382. package/src/renderers/opentui/toast-host.tsx +26 -2
  383. package/src/renderers/opentui/ui-host.tsx +1 -0
  384. package/src/sources/provider-router/search.test.ts +191 -0
  385. package/src/sources/provider-router/search.ts +92 -39
  386. package/src/state/app/context/index.tsx +2 -0
  387. package/src/state/app/context/selector.test.tsx +33 -1
  388. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  389. package/src/state/hooks/quote-streaming.ts +106 -30
  390. package/src/state/pane-table-export-registry.ts +50 -0
  391. package/src/theme/color-utils.ts +1 -1
  392. package/src/theme/colors.ts +50 -0
  393. package/src/theme/themes.test.ts +15 -2
  394. package/src/theme/themes.ts +19 -1
  395. package/src/tickers/search/index.test.ts +26 -0
  396. package/src/tickers/search/index.ts +42 -17
  397. package/src/tickers/search/ranking.ts +14 -0
  398. package/src/time-series/chart-data.test.ts +216 -0
  399. package/src/time-series/range.ts +12 -0
  400. package/src/time-series/resolution.test.ts +25 -26
  401. package/src/time-series/resolution.ts +44 -25
  402. package/src/time-series/resolve.ts +7 -0
  403. package/src/time-series/spec.ts +5 -5
  404. package/src/time-series/types.ts +3 -1
  405. package/src/time-series/use-chart-resolution.ts +5 -0
  406. package/src/types/config.ts +123 -3
  407. package/src/types/data-provider.ts +11 -0
  408. package/src/types/headless.ts +161 -0
  409. package/src/types/plugin.ts +104 -20
  410. package/src/ui/host.tsx +19 -1
  411. package/src/ui/index.tsx +1 -0
  412. package/src/ui/toast.tsx +8 -4
  413. package/src/utils/csv.test.ts +13 -0
  414. package/src/utils/csv.ts +37 -0
  415. package/src/utils/format.ts +35 -0
  416. package/src/utils/fuzzy-search.test.ts +94 -0
  417. package/src/utils/fuzzy-search.ts +81 -22
  418. package/src/utils/save-text-file.test.ts +24 -0
  419. package/src/utils/save-text-file.ts +36 -0
  420. package/src/utils/shortcut-labels.ts +14 -0
  421. package/src/components/chart/core/axis-format.ts +0 -157
  422. package/src/components/chart/core/index.test.ts +0 -1075
  423. package/src/components/chart/core/projection.ts +0 -357
  424. package/src/components/chart/core/render-utils.ts +0 -4
  425. package/src/components/chart/core/renderer.ts +0 -244
  426. package/src/components/chart/core/scene.ts +0 -180
  427. package/src/components/chart/core/time-axis.ts +0 -330
  428. package/src/components/chart/cursor-motion.ts +0 -47
  429. package/src/components/chart/indicators/types.ts +0 -26
  430. package/src/components/chart/market-session.ts +0 -176
  431. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  432. package/src/components/chart/multi-line/renderer.ts +0 -161
  433. package/src/components/chart/multi-line/scene.ts +0 -188
  434. package/src/components/chart/native/raster/layout.ts +0 -129
  435. package/src/components/chart/native/raster/price-chart.ts +0 -244
  436. package/src/components/chart/rendering/chart-draw.ts +0 -270
  437. package/src/components/chart/rendering/geometry.ts +0 -95
  438. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  439. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  440. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  441. package/src/components/chart/static/chart/surface.tsx +0 -358
  442. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  443. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  444. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  445. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  446. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  447. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  448. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  449. package/src/plugins/builtin/substack/api/store.ts +0 -186
  450. package/src/plugins/builtin/substack/api/types.ts +0 -42
  451. package/src/plugins/builtin/substack/api.test.ts +0 -298
  452. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  453. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  454. package/src/plugins/builtin/substack/content.ts +0 -478
  455. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  456. package/src/plugins/builtin/substack/index.tsx +0 -49
  457. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  458. package/src/plugins/builtin/substack/model.test.ts +0 -155
  459. package/src/plugins/builtin/substack/normalize.ts +0 -343
  460. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  461. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  462. package/src/plugins/builtin/substack/pane.tsx +0 -547
  463. package/src/plugins/builtin/substack/read-state.ts +0 -30
  464. package/src/plugins/builtin/substack/table.ts +0 -111
  465. package/src/plugins/builtin/substack/types.ts +0 -71
  466. package/src/plugins/builtin/substack/utils.ts +0 -131
  467. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  468. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  469. package/src/plugins/ibkr/account-cache.ts +0 -24
  470. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  471. package/src/plugins/ibkr/config.test.ts +0 -79
  472. package/src/plugins/ibkr/config.ts +0 -250
  473. package/src/plugins/ibkr/dialogs.tsx +0 -40
  474. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  475. package/src/plugins/ibkr/flex/index.ts +0 -367
  476. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  477. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  478. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  479. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  480. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  481. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  482. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  483. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  484. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  485. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  486. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  487. package/src/plugins/ibkr/gateway/history.ts +0 -120
  488. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  489. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  490. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  491. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  492. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  493. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  494. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  495. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  496. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  497. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  498. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  499. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  500. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  501. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  502. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  503. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  504. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  505. package/src/plugins/ibkr/gateway/types.ts +0 -27
  506. package/src/plugins/ibkr/index.test.tsx +0 -162
  507. package/src/plugins/ibkr/index.tsx +0 -159
  508. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  509. package/src/plugins/ibkr/instance-selection.ts +0 -36
  510. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  511. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  512. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  513. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  514. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  515. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  516. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  517. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  518. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  519. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  520. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  521. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  522. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  523. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  524. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  525. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  526. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  527. package/src/plugins/ibkr/trade/utils.ts +0 -144
  528. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  529. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  530. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  531. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -0,0 +1,344 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import {
3
+ DataTableView,
4
+ EmptyState,
5
+ InputSearchBar,
6
+ SegmentedControl,
7
+ Spinner,
8
+ type DataTableCell,
9
+ type DataTableColumn,
10
+ type DataTableKeyEvent,
11
+ type DataTableSelectionChangeReason,
12
+ type PaneFooterSegment,
13
+ } from "../../../components";
14
+ import { useShortcut } from "../../../react/input";
15
+ import { usePaneSettingValue } from "../../../state/app/context";
16
+ import { colors } from "../../../theme/colors";
17
+ import type { PaneProps } from "../../../types/plugin";
18
+ import { Box, ScrollBox, Text, type InputRenderable } from "../../../ui";
19
+ import { formatNumber } from "../../../utils/format";
20
+ import { isPlainKey } from "../../../utils/keyboard";
21
+ import { stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
22
+ import { useAutoRefresh } from "../shared/auto-refresh";
23
+ import { usePaneStatusFooter } from "../shared/pane-footer";
24
+ import { getCachedValuationBundle, loadValuationBundle } from "./client";
25
+ import { shortZoneLabel, type ValuationRangeId } from "./defs";
26
+ import { IndicatorDetail } from "./detail";
27
+ import { RANGE_OPTIONS, VALUATION_DEFAULTS } from "./settings";
28
+ import {
29
+ formatSigma,
30
+ selectValuationViews,
31
+ type IndicatorViewModel,
32
+ type ValuationBundle,
33
+ } from "./view";
34
+
35
+ /** Below this the detail sits under the table instead of beside it. */
36
+ const SPLIT_MIN_WIDTH = 108;
37
+ const LIST_WIDTH = 46;
38
+
39
+ type LoadState =
40
+ | { status: "idle" }
41
+ | { status: "loading"; previous: ValuationBundle | null }
42
+ | { status: "ready"; bundle: ValuationBundle }
43
+ | { status: "error"; message: string; previous: ValuationBundle | null };
44
+
45
+ type ColumnId = "name" | "value" | "zone" | "percentile" | "sigma";
46
+ interface Column extends DataTableColumn { id: ColumnId }
47
+
48
+ function bundleOf(state: LoadState): ValuationBundle | null {
49
+ switch (state.status) {
50
+ case "idle":
51
+ return null;
52
+ case "loading":
53
+ case "error":
54
+ return state.previous;
55
+ case "ready":
56
+ return state.bundle;
57
+ default: {
58
+ const _exhaustive: never = state;
59
+ return _exhaustive;
60
+ }
61
+ }
62
+ }
63
+
64
+ function initialLoadState(): LoadState {
65
+ const cached = getCachedValuationBundle();
66
+ return cached ? { status: "ready", bundle: cached } : { status: "idle" };
67
+ }
68
+
69
+ function matchesQuery(view: IndicatorViewModel, query: string): boolean {
70
+ if (!query) return true;
71
+ const haystack = [
72
+ view.indicator.label,
73
+ view.indicator.shortLabel,
74
+ view.indicator.description,
75
+ view.zone.label,
76
+ ].join(" ").toLowerCase();
77
+ return query.split(/\s+/).every((token) => haystack.includes(token));
78
+ }
79
+
80
+ /** Stacked mode keeps only what fits; the split has a whole column to work with. */
81
+ function buildColumns(width: number, stacked: boolean): Column[] {
82
+ const withTrend = stacked && width >= 100;
83
+ // 25 for value/zone/rich, 8 more for trend, then gutters between the columns.
84
+ const trailing = 25 + (withTrend ? 8 : 0);
85
+ const name = Math.max(11, width - trailing - 6);
86
+ return [
87
+ { id: "name", label: "INDICATOR", width: name, align: "left" },
88
+ { id: "value", label: "VALUE", width: 8, align: "right" },
89
+ { id: "zone", label: "ZONE", width: 11, align: "right" },
90
+ { id: "percentile", label: "RICH", width: 6, align: "right" },
91
+ ...(withTrend
92
+ ? [{ id: "sigma" as const, label: "TREND", width: 8, align: "right" as const }]
93
+ : []),
94
+ ];
95
+ }
96
+
97
+ function cellsFor(view: IndicatorViewModel): Record<ColumnId, DataTableCell> {
98
+ return {
99
+ name: { text: view.indicator.shortLabel, color: colors.textBright },
100
+ value: { text: view.indicator.formatValue(view.current.ratio), color: view.zone.color },
101
+ zone: { text: shortZoneLabel(view.zone.id), color: view.zone.color },
102
+ // Restated so a high number always means expensive, whichever way the
103
+ // underlying measure runs, otherwise the column cannot be read down.
104
+ percentile: { text: formatNumber(view.richPercentile, 0), color: colors.text },
105
+ sigma: { text: formatSigma(view.richSigma), color: colors.textMuted },
106
+ };
107
+ }
108
+
109
+ /**
110
+ * The table commits keyboard moves by index after a short delay. When a filter
111
+ * changes the rows in between, that index lands on a different indicator, and
112
+ * honouring it would silently rewrite the persisted setting as the user types.
113
+ * Pointer and activation commits are explicit, so they are always honoured.
114
+ */
115
+ export function shouldPersistSelection({
116
+ id,
117
+ reason,
118
+ selectionOnScreen,
119
+ knownIds,
120
+ }: {
121
+ id: string;
122
+ reason: DataTableSelectionChangeReason;
123
+ selectionOnScreen: boolean;
124
+ knownIds: readonly string[];
125
+ }): boolean {
126
+ if (!knownIds.includes(id)) return false;
127
+ return reason !== "keyboard" || selectionOnScreen;
128
+ }
129
+
130
+ export function MarketValuationPane({ focused, width, height }: PaneProps) {
131
+ const [indicatorId, setIndicatorId] = usePaneSettingValue<string>(
132
+ "indicator",
133
+ VALUATION_DEFAULTS.indicator,
134
+ );
135
+ const [range, setRange] = usePaneSettingValue<ValuationRangeId>("range", VALUATION_DEFAULTS.range);
136
+ const [state, setState] = useState<LoadState>(initialLoadState);
137
+ const [lastUpdated, setLastUpdated] = useState<number | null>(null);
138
+ const [query, setQuery] = useState("");
139
+ const [searchFocused, setSearchFocused] = useState(false);
140
+ const [searchFocusToken, setSearchFocusToken] = useState(0);
141
+ const searchInputRef = useRef<InputRenderable | null>(null);
142
+ const generation = useRef(0);
143
+
144
+ const load = useCallback(async () => {
145
+ const current = ++generation.current;
146
+ setState((previous) => ({ status: "loading", previous: bundleOf(previous) }));
147
+ try {
148
+ const next = await loadValuationBundle();
149
+ if (generation.current !== current) return;
150
+ setState({ status: "ready", bundle: next });
151
+ setLastUpdated(Date.now());
152
+ } catch (error) {
153
+ if (generation.current !== current) return;
154
+ setState((previous) => ({
155
+ status: "error",
156
+ message: error instanceof Error ? error.message : String(error),
157
+ previous: bundleOf(previous),
158
+ }));
159
+ }
160
+ }, []);
161
+
162
+ useEffect(() => { void load(); }, [load]);
163
+ const refresh = useCallback(() => { void load(); }, [load]);
164
+ useAutoRefresh(lastUpdated, refresh);
165
+
166
+ const focusSearch = useCallback(() => {
167
+ setSearchFocused(true);
168
+ setSearchFocusToken((current) => current + 1);
169
+ }, []);
170
+ const blurSearch = useCallback(() => setSearchFocused(false), []);
171
+
172
+ const handlePaneKey = useCallback((event: DataTableKeyEvent): boolean => {
173
+ if (isPlainKey(event, "/")) {
174
+ stopSearchFocusNavigation(event);
175
+ focusSearch();
176
+ return true;
177
+ }
178
+ if (isPlainKey(event, "r")) {
179
+ stopSearchFocusNavigation(event);
180
+ refresh();
181
+ return true;
182
+ }
183
+ return false;
184
+ }, [focusSearch, refresh]);
185
+
186
+ useShortcut((event) => {
187
+ if (!focused || searchFocused || event.name !== "r") return;
188
+ event.preventDefault?.();
189
+ event.stopPropagation?.();
190
+ refresh();
191
+ });
192
+
193
+ const bundle = bundleOf(state);
194
+ const views = useMemo(
195
+ () => (bundle ? selectValuationViews(bundle, range) : []),
196
+ [bundle, range],
197
+ );
198
+ const normalizedQuery = query.trim().toLowerCase();
199
+ const visible = useMemo(
200
+ () => views.filter((view) => matchesQuery(view, normalizedQuery)),
201
+ [normalizedQuery, views],
202
+ );
203
+ // Filtering narrows the list, but the detail keeps showing the chosen indicator
204
+ // until the user picks another, so typing never blanks the chart.
205
+ const selected = views.find((view) => view.indicator.id === indicatorId) ?? views[0] ?? null;
206
+ const selectionOnScreen = visible.some((view) => view.indicator.id === selected?.indicator.id);
207
+
208
+ const chooseIndicator = useCallback((
209
+ id: string,
210
+ reason: DataTableSelectionChangeReason,
211
+ ) => {
212
+ if (!shouldPersistSelection({
213
+ id,
214
+ reason,
215
+ selectionOnScreen,
216
+ knownIds: views.map((view) => view.indicator.id),
217
+ })) return;
218
+ setIndicatorId(id);
219
+ }, [selectionOnScreen, setIndicatorId, views]);
220
+
221
+ const error = state.status === "error" ? state.message : bundle?.errors[0] ?? null;
222
+ const footerInfo = useMemo<PaneFooterSegment[]>(() => {
223
+ if (!selected) return [];
224
+ const info: PaneFooterSegment[] = [
225
+ { id: "as-of", parts: [{ text: `as of ${selected.asOf}`, tone: "muted" }] },
226
+ { id: "delayed", parts: [{ text: "delayed", tone: "muted" }] },
227
+ ];
228
+ if (selected.observationStale) {
229
+ info.push({ id: "stale", parts: [{ text: "STALE", tone: "warning", bold: true }] });
230
+ }
231
+ if (normalizedQuery) {
232
+ info.push({ id: "filter", parts: [{ text: `filter: ${normalizedQuery}`, tone: "value" }] });
233
+ }
234
+ return info;
235
+ }, [normalizedQuery, selected]);
236
+
237
+ usePaneStatusFooter({
238
+ registrationId: "market-valuation",
239
+ loading: state.status === "loading",
240
+ error,
241
+ info: footerInfo,
242
+ });
243
+
244
+ if (!bundle && state.status !== "error") {
245
+ return (
246
+ <Box width={width} height={height} justifyContent="center" alignItems="center">
247
+ <Spinner label="Loading market valuation..." />
248
+ </Box>
249
+ );
250
+ }
251
+
252
+ if (!selected) {
253
+ return (
254
+ <Box width={width} height={height} padding={1} flexDirection="column" gap={1}>
255
+ <EmptyState title="Market valuation unavailable." message={error ?? undefined} />
256
+ </Box>
257
+ );
258
+ }
259
+
260
+ const split = width >= SPLIT_MIN_WIDTH;
261
+ const listWidth = split ? Math.min(LIST_WIDTH, Math.floor(width * 0.4)) : width;
262
+ const detailWidth = split ? width - listWidth : width;
263
+ const columns = buildColumns(listWidth, !split);
264
+ // Header plus every row, and one more line for the horizontal scrollbar.
265
+ const tableHeight = split
266
+ ? Math.max(3, height - 2)
267
+ : Math.min(visible.length + 2, Math.max(3, height - 12));
268
+
269
+ const list = (
270
+ <Box flexDirection="column" width={listWidth} flexShrink={0}>
271
+ <InputSearchBar
272
+ value={query}
273
+ focused={focused}
274
+ active={searchFocused}
275
+ width={listWidth}
276
+ focusToken={searchFocusToken}
277
+ inputRef={searchInputRef}
278
+ placeholder="filter indicators"
279
+ debounceMs={80}
280
+ onFocus={focusSearch}
281
+ onBlur={blurSearch}
282
+ onQueryChange={setQuery}
283
+ />
284
+ <Box flexDirection="column" width={listWidth} height={tableHeight} flexShrink={0} overflow="hidden">
285
+ <DataTableView<IndicatorViewModel, Column>
286
+ focused={focused && !searchFocused}
287
+ rootWidth={listWidth}
288
+ rootHeight={tableHeight}
289
+ columns={columns}
290
+ items={visible}
291
+ sortColumnId={null}
292
+ sortDirection="asc"
293
+ selection={{
294
+ kind: "id",
295
+ selectedId: selected.indicator.id,
296
+ getId: (view) => view.indicator.id,
297
+ onChange: (id, _item, _index, reason) => chooseIndicator(String(id), reason),
298
+ }}
299
+ onHeaderClick={() => {}}
300
+ onRootKeyDown={handlePaneKey}
301
+ getItemKey={(view) => view.indicator.id}
302
+ renderCell={(view, column) => cellsFor(view)[column.id]}
303
+ emptyStateTitle={normalizedQuery ? "No indicator matches." : error ?? "No indicators."}
304
+ />
305
+ </Box>
306
+ </Box>
307
+ );
308
+
309
+ const detail = (
310
+ <Box flexDirection="column" flexGrow={1} width={detailWidth} overflow="hidden">
311
+ <Box flexDirection="row" height={1} paddingX={1} overflow="hidden" justifyContent="flex-end">
312
+ <SegmentedControl
313
+ options={RANGE_OPTIONS}
314
+ value={range}
315
+ onChange={(value) => setRange(value as ValuationRangeId)}
316
+ />
317
+ </Box>
318
+ <ScrollBox flexGrow={1} scrollY focusable={false}>
319
+ <Box flexDirection="column" paddingBottom={1}>
320
+ <IndicatorDetail
321
+ view={selected}
322
+ width={detailWidth}
323
+ height={Math.max(12, height - (split ? 3 : tableHeight + 3))}
324
+ focused={focused && !searchFocused}
325
+ />
326
+ </Box>
327
+ </ScrollBox>
328
+ </Box>
329
+ );
330
+
331
+ return (
332
+ <Box flexDirection="column" width={width} height={height}>
333
+ <Box flexDirection={split ? "row" : "column"} flexGrow={1} overflow="hidden">
334
+ {list}
335
+ {detail}
336
+ </Box>
337
+ {error ? (
338
+ <Box height={1} paddingX={1} overflow="hidden">
339
+ <Text fg={colors.warning}>{error}</Text>
340
+ </Box>
341
+ ) : null}
342
+ </Box>
343
+ );
344
+ }
@@ -0,0 +1,12 @@
1
+ export type SeriesProvenance = "fred" | "market" | "shiller";
2
+
3
+ export interface DatedObservation {
4
+ date: string;
5
+ value: number | null;
6
+ }
7
+
8
+ export interface DatedSeries {
9
+ seriesId: string;
10
+ observations: DatedObservation[];
11
+ provenance: SeriesProvenance;
12
+ }
@@ -0,0 +1,38 @@
1
+ import type { PaneSettingsDef } from "../../../types/plugin";
2
+ import type { ValuationRangeId } from "./defs";
3
+ import { DEFAULT_INDICATOR_ID, INDICATORS } from "./indicators";
4
+
5
+ export const VALUATION_DEFAULTS = {
6
+ indicator: DEFAULT_INDICATOR_ID,
7
+ range: "25Y",
8
+ } as const satisfies { indicator: string; range: ValuationRangeId };
9
+
10
+ export const RANGE_OPTIONS = [
11
+ { value: "10Y" as const, label: "10Y" },
12
+ { value: "25Y" as const, label: "25Y" },
13
+ { value: "ALL" as const, label: "All" },
14
+ ];
15
+
16
+ export function indicatorOptions(): Array<{ label: string; value: string }> {
17
+ return INDICATORS.map((indicator) => ({ label: indicator.label, value: indicator.id }));
18
+ }
19
+
20
+ export function buildValuationSettingsDef(): PaneSettingsDef {
21
+ return {
22
+ title: "Market Valuation Settings",
23
+ fields: [
24
+ {
25
+ key: "indicator",
26
+ label: "Indicator",
27
+ type: "select",
28
+ options: indicatorOptions(),
29
+ },
30
+ {
31
+ key: "range",
32
+ label: "History",
33
+ type: "select",
34
+ options: RANGE_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
35
+ },
36
+ ],
37
+ };
38
+ }
@@ -0,0 +1,130 @@
1
+ import { apiClient } from "../../../api-client";
2
+ import type { CloudShillerPayload } from "../../../api-client";
3
+ import type { SeriesDef, ShillerField } from "./defs";
4
+ import type { DatedObservation, DatedSeries } from "./series";
5
+
6
+ /**
7
+ * Every leg goes through the Gloom Cloud proxy. Earlier revisions reached Yahoo and
8
+ * fredgraph.csv directly, which forced the pane out of the hosted browser build,
9
+ * since both are blocked there by CORS and the worker's connect-src policy.
10
+ */
11
+ export interface ValuationSourceDeps {
12
+ loadFred: (seriesId: string, limit: number) => Promise<DatedObservation[]>;
13
+ loadMarketHistory: (
14
+ symbol: string,
15
+ exchange: string,
16
+ startDate: string,
17
+ ) => Promise<DatedObservation[]>;
18
+ loadShiller: () => Promise<CloudShillerPayload>;
19
+ }
20
+
21
+ export function shillerObservations(
22
+ payload: CloudShillerPayload,
23
+ field: ShillerField,
24
+ ): DatedObservation[] {
25
+ const observations: DatedObservation[] = [];
26
+ for (const row of payload.observations) {
27
+ const value = row[field];
28
+ observations.push({
29
+ date: row.date,
30
+ value: typeof value === "number" && Number.isFinite(value) ? value : null,
31
+ });
32
+ }
33
+ if (!observations.some((entry) => entry.value != null)) {
34
+ throw new Error(`Shiller dataset has no ${field} observations`);
35
+ }
36
+ return observations;
37
+ }
38
+
39
+ export function historyToObservations(
40
+ points: ReadonlyArray<{ date: string; close: number | null }>,
41
+ ): DatedObservation[] {
42
+ const observations: DatedObservation[] = [];
43
+ for (const point of points) {
44
+ const close = point.close;
45
+ if (close == null || !Number.isFinite(close) || close <= 0) continue;
46
+ observations.push({ date: point.date.slice(0, 10), value: close });
47
+ }
48
+ if (observations.length === 0) throw new Error("No price history");
49
+ return observations;
50
+ }
51
+
52
+ export function provenanceFor(def: SeriesDef): DatedSeries["provenance"] {
53
+ switch (def.source.kind) {
54
+ case "fred":
55
+ return "fred";
56
+ case "market-history":
57
+ return "market";
58
+ case "shiller":
59
+ return "shiller";
60
+ default: {
61
+ const _exhaustive: never = def.source;
62
+ return _exhaustive;
63
+ }
64
+ }
65
+ }
66
+
67
+ export function createSourceLoader(deps: ValuationSourceDeps) {
68
+ // One Shiller fetch serves every column the registry asks for.
69
+ let shillerRequest: Promise<CloudShillerPayload> | null = null;
70
+ const shiller = () => (shillerRequest ??= deps.loadShiller().catch((error) => {
71
+ shillerRequest = null;
72
+ throw error;
73
+ }));
74
+
75
+ return async (def: SeriesDef): Promise<DatedSeries> => {
76
+ const source = def.source;
77
+ switch (source.kind) {
78
+ case "fred":
79
+ return {
80
+ seriesId: def.key,
81
+ observations: await deps.loadFred(source.seriesId, source.limit),
82
+ provenance: "fred",
83
+ };
84
+ case "market-history":
85
+ return {
86
+ seriesId: def.key,
87
+ observations: await deps.loadMarketHistory(
88
+ source.symbol,
89
+ source.exchange,
90
+ source.startDate,
91
+ ),
92
+ provenance: "market",
93
+ };
94
+ case "shiller":
95
+ return {
96
+ seriesId: def.key,
97
+ observations: shillerObservations(await shiller(), source.field),
98
+ provenance: "shiller",
99
+ };
100
+ default: {
101
+ const _exhaustive: never = source;
102
+ return _exhaustive;
103
+ }
104
+ }
105
+ };
106
+ }
107
+
108
+ export type ValuationCloudClient = Pick<
109
+ typeof apiClient,
110
+ "getCloudFredSeries" | "getCloudHistory" | "getCloudShiller"
111
+ >;
112
+
113
+ export function createCloudSourceDeps(client: ValuationCloudClient): ValuationSourceDeps {
114
+ return {
115
+ loadFred: async (seriesId, limit) => {
116
+ const data = await client.getCloudFredSeries(seriesId, { limit, sortOrder: "desc" });
117
+ return data.observations;
118
+ },
119
+ loadMarketHistory: async (symbol, exchange, startDate) => {
120
+ const response = await client.getCloudHistory(symbol, exchange, {
121
+ interval: "1d",
122
+ startDate,
123
+ });
124
+ return historyToObservations(response.data ?? []);
125
+ },
126
+ loadShiller: () => client.getCloudShiller(),
127
+ };
128
+ }
129
+
130
+ export const cloudSourceDeps: ValuationSourceDeps = createCloudSourceDeps(apiClient);
@@ -0,0 +1,89 @@
1
+ import type { RatioPoint } from "./align";
2
+ import type { IndicatorDef } from "./defs";
3
+
4
+ const MS_PER_DAY = 86_400_000;
5
+
6
+ export interface TrendFit {
7
+ model: "log" | "linear";
8
+ alpha: number;
9
+ beta: number;
10
+ sigma: number;
11
+ originMs: number;
12
+ }
13
+
14
+ function parseDateMs(date: string): number {
15
+ return Date.parse(date);
16
+ }
17
+
18
+ /**
19
+ * Least-squares fit of value against time. A log fit suits a level that compounds;
20
+ * a measure that can sit at or below zero, like an excess yield, needs the linear
21
+ * one, since taking logs would silently drop exactly its most extreme years.
22
+ */
23
+ export function fitTrend(
24
+ points: readonly RatioPoint[],
25
+ model: "log" | "linear" = "log",
26
+ ): TrendFit {
27
+ const usable = model === "log"
28
+ ? points.filter((p) => p.ratio > 0 && Number.isFinite(p.ratio))
29
+ : points.filter((p) => Number.isFinite(p.ratio));
30
+ const empty: TrendFit = { model, alpha: 0, beta: 0, sigma: 0, originMs: 0 };
31
+ if (usable.length === 0) return empty;
32
+
33
+ const originMs = parseDateMs(usable[0]!.date);
34
+ const project = (value: number) => (model === "log" ? Math.log(value) : value);
35
+ if (usable.length < 2) {
36
+ return { model, alpha: project(usable[0]!.ratio), beta: 0, sigma: 0, originMs };
37
+ }
38
+
39
+ const xs: number[] = [];
40
+ const ys: number[] = [];
41
+ for (const point of usable) {
42
+ xs.push((parseDateMs(point.date) - originMs) / MS_PER_DAY);
43
+ ys.push(project(point.ratio));
44
+ }
45
+ const n = xs.length;
46
+ let sumX = 0;
47
+ let sumY = 0;
48
+ let sumXX = 0;
49
+ let sumXY = 0;
50
+ for (let i = 0; i < n; i += 1) {
51
+ sumX += xs[i]!;
52
+ sumY += ys[i]!;
53
+ sumXX += xs[i]! * xs[i]!;
54
+ sumXY += xs[i]! * ys[i]!;
55
+ }
56
+ const denom = n * sumXX - sumX * sumX;
57
+ const beta = denom === 0 ? 0 : (n * sumXY - sumX * sumY) / denom;
58
+ const alpha = (sumY - beta * sumX) / n;
59
+ if (n < 3) return { model, alpha, beta, sigma: 0, originMs };
60
+
61
+ let residualSq = 0;
62
+ for (let i = 0; i < n; i += 1) {
63
+ const residual = ys[i]! - (alpha + beta * xs[i]!);
64
+ residualSq += residual * residual;
65
+ }
66
+ return { model, alpha, beta, sigma: Math.sqrt(residualSq / (n - 2)), originMs };
67
+ }
68
+
69
+ export function fitIndicatorTrend(
70
+ indicator: IndicatorDef,
71
+ points: readonly RatioPoint[],
72
+ ): TrendFit {
73
+ return fitTrend(points, indicator.trendModel);
74
+ }
75
+
76
+ export function trendAt(fit: TrendFit, date: string): number {
77
+ const tDays = (parseDateMs(date) - fit.originMs) / MS_PER_DAY;
78
+ const fitted = fit.alpha + fit.beta * tDays;
79
+ return fit.model === "log" ? Math.exp(fitted) : fitted;
80
+ }
81
+
82
+ export function sigmaVsTrend(fit: TrendFit, value: number, date: string): number {
83
+ if (!(fit.sigma > 0)) return 0;
84
+ if (fit.model === "linear") {
85
+ return (value - trendAt(fit, date)) / fit.sigma;
86
+ }
87
+ if (!(value > 0)) return 0;
88
+ return (Math.log(value) - Math.log(trendAt(fit, date))) / fit.sigma;
89
+ }