gloomberb 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (392) hide show
  1. package/README.md +38 -15
  2. package/README.zh-CN.md +12 -12
  3. package/package.json +10 -3
  4. package/src/api-client/auth.ts +14 -10
  5. package/src/api-client/data.ts +54 -0
  6. package/src/api-client/index.test.ts +124 -0
  7. package/src/api-client/index.ts +139 -4
  8. package/src/api-client/paths.ts +89 -0
  9. package/src/api-client/request.ts +15 -2
  10. package/src/api-client/socket-health.test.ts +1 -0
  11. package/src/api-client/socket.ts +2 -1
  12. package/src/api-client/types.ts +163 -0
  13. package/src/app/pane-runtime/index.tsx +4 -1
  14. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  15. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  16. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  17. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  18. package/src/app/runtime/desktop-deeplink.ts +66 -2
  19. package/src/app/runtime/desktop-menu.ts +3 -0
  20. package/src/app/runtime/plugin-bindings.ts +30 -28
  21. package/src/app/runtime/update.ts +8 -6
  22. package/src/app.tsx +21 -7
  23. package/src/architecture/import-boundaries.test.ts +5 -1
  24. package/src/capabilities/factories.ts +4 -0
  25. package/src/cli/commands/automation.test.ts +9 -0
  26. package/src/cli/commands/automation.ts +22 -73
  27. package/src/cli/index.ts +10 -6
  28. package/src/cli/registry.ts +1 -1
  29. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  30. package/src/components/chart/composite/composite-chart.tsx +16 -12
  31. package/src/components/chart/composite/format.test.ts +34 -0
  32. package/src/components/chart/composite/format.ts +33 -10
  33. package/src/components/chart/composite/scene.ts +0 -21
  34. package/src/components/chart/composite/tools.ts +49 -0
  35. package/src/components/chart/core/axis-format.ts +13 -3
  36. package/src/components/chart/core/index.test.ts +7 -0
  37. package/src/components/chart/core/renderer.ts +3 -4
  38. package/src/components/command-bar/commands/direct/index.ts +4 -0
  39. package/src/components/command-bar/commands/registry.ts +8 -1
  40. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  41. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  42. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  43. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  44. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  45. package/src/components/command-bar/surface/index.test.tsx +6 -6
  46. package/src/components/command-bar/surface/index.tsx +4 -1
  47. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  48. package/src/components/command-bar/view-model.test.ts +2 -1
  49. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  50. package/src/components/command-bar/workflow/ops.ts +1 -1
  51. package/src/components/company-logo.test.ts +19 -0
  52. package/src/components/company-logo.tsx +60 -0
  53. package/src/components/feed-data-table/stack-view.tsx +7 -1
  54. package/src/components/index.ts +1 -0
  55. package/src/components/layout/detached-pane-shell.tsx +85 -15
  56. package/src/components/layout/header.tsx +5 -5
  57. package/src/components/layout/pane/title.ts +31 -3
  58. package/src/components/layout/shell/active-drag.ts +4 -11
  59. package/src/components/layout/shell/drag/index.ts +4 -20
  60. package/src/components/layout/shell/drag/runtime.ts +3 -4
  61. package/src/components/layout/shell/index.test.tsx +17 -3
  62. package/src/components/layout/shell/index.tsx +89 -31
  63. package/src/components/layout/shell/menu.test.ts +37 -1
  64. package/src/components/layout/shell/menu.ts +14 -0
  65. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  66. package/src/components/layout/shell/pane/actions.ts +0 -5
  67. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  68. package/src/components/layout/shell/shortcuts.ts +6 -3
  69. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  70. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  71. package/src/components/layout/status-bar.test.tsx +62 -61
  72. package/src/components/layout/status-bar.tsx +53 -75
  73. package/src/components/onboarding/account-step/form-panel.tsx +4 -2
  74. package/src/components/onboarding/account-step/index.tsx +1 -1
  75. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  76. package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
  77. package/src/components/onboarding/onboarding-wizard.tsx +13 -5
  78. package/src/components/onboarding/wizard-account.ts +4 -23
  79. package/src/components/table-view-shared.test.ts +11 -0
  80. package/src/components/table-view-shared.tsx +26 -0
  81. package/src/components/ui/data-table/index.tsx +19 -1
  82. package/src/components/ui/fields.tsx +5 -1
  83. package/src/core/state/app/layout-reducer.ts +36 -0
  84. package/src/core/state/app/layout.ts +10 -5
  85. package/src/core/state/app/state.ts +0 -13
  86. package/src/core/state/app/types.ts +6 -4
  87. package/src/data/config/chart-settings.ts +1 -1
  88. package/src/data/config/layout.ts +2 -0
  89. package/src/data/config/store/index.test.ts +27 -6
  90. package/src/data/config/store/normalize.ts +3 -5
  91. package/src/data/plugin-state-store.ts +11 -0
  92. package/src/data/sqlite/cache.test.ts +10 -0
  93. package/src/i18n/es.ts +13 -5
  94. package/src/i18n/ja.ts +13 -5
  95. package/src/i18n/ko.ts +13 -5
  96. package/src/i18n/zh-cn.ts +13 -5
  97. package/src/i18n/zh-tw.ts +13 -5
  98. package/src/layout-marketplace/api.test.ts +54 -0
  99. package/src/layout-marketplace/api.ts +65 -0
  100. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  101. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  102. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  103. package/src/layout-marketplace/gallery.test.tsx +84 -0
  104. package/src/layout-marketplace/gallery.tsx +323 -0
  105. package/src/layout-marketplace/geometry.test.ts +47 -0
  106. package/src/layout-marketplace/geometry.ts +80 -0
  107. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  108. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  109. package/src/layout-marketplace/model.test.ts +118 -0
  110. package/src/layout-marketplace/model.ts +171 -0
  111. package/src/layout-marketplace/name-dialog.tsx +60 -0
  112. package/src/layout-marketplace/pane-imagery.ts +85 -0
  113. package/src/layout-marketplace/pane.tsx +20 -0
  114. package/src/layout-marketplace/payload.test.ts +246 -0
  115. package/src/layout-marketplace/payload.ts +624 -0
  116. package/src/layout-marketplace/use-marketplace.ts +63 -0
  117. package/src/market-data/coordinator/index.test.ts +2 -2
  118. package/src/market-data/coordinator/index.ts +1 -1
  119. package/src/market-data/hooks.tsx +0 -41
  120. package/src/market-data/market/format.test.ts +7 -0
  121. package/src/market-data/market/freshness.ts +5 -2
  122. package/src/market-data/quotes/freshness.test.ts +16 -0
  123. package/src/news/aggregator.test.ts +92 -3
  124. package/src/news/aggregator.ts +98 -18
  125. package/src/news/hooks.test.tsx +2 -0
  126. package/src/news/hooks.ts +27 -1
  127. package/src/news/news-model.ts +5 -2
  128. package/src/news/types.ts +7 -0
  129. package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
  130. package/src/plugins/broker-sync/normalize.ts +42 -5
  131. package/src/plugins/builtin/account-management/pane.tsx +5 -5
  132. package/src/plugins/builtin/ai/index.tsx +18 -2
  133. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  134. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  135. package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
  136. package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
  137. package/src/plugins/builtin/alerts/index.test.tsx +49 -6
  138. package/src/plugins/builtin/alerts/index.tsx +11 -6
  139. package/src/plugins/builtin/alerts/pane.tsx +51 -9
  140. package/src/plugins/builtin/alerts/quotes.ts +11 -3
  141. package/src/plugins/builtin/alerts/types.ts +1 -0
  142. package/src/plugins/builtin/analytics/index.tsx +11 -1
  143. package/src/plugins/builtin/analytics/pane-model.ts +6 -4
  144. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  145. package/src/plugins/builtin/buildout/model/index.ts +1 -1
  146. package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
  147. package/src/plugins/builtin/cds/client.test.ts +83 -0
  148. package/src/plugins/builtin/cds/client.ts +67 -0
  149. package/src/plugins/builtin/cds/index.tsx +41 -0
  150. package/src/plugins/builtin/cds/model.test.ts +232 -0
  151. package/src/plugins/builtin/cds/model.ts +401 -0
  152. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  153. package/src/plugins/builtin/cds/pane.tsx +358 -0
  154. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  155. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  156. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  157. package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
  158. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  159. package/src/plugins/builtin/chat/controller/channels.ts +7 -1
  160. package/src/plugins/builtin/chat/controller/index.ts +14 -9
  161. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  162. package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
  163. package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
  164. package/src/plugins/builtin/chat/controller/send.ts +1 -3
  165. package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
  166. package/src/plugins/builtin/chat/controller/state.ts +1 -3
  167. package/src/plugins/builtin/chat/controller/storage.ts +0 -2
  168. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  169. package/src/plugins/builtin/chat/controller/view.ts +2 -2
  170. package/src/plugins/builtin/chat/controller.test.ts +142 -14
  171. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  172. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  173. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  174. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  175. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  176. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  177. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  178. package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
  179. package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
  180. package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
  181. package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
  182. package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
  183. package/src/plugins/builtin/cloud/browser.tsx +9 -0
  184. package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
  185. package/src/plugins/builtin/cloud/device-signin.ts +2 -8
  186. package/src/plugins/builtin/cloud/index.tsx +2 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +14 -24
  188. package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
  189. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  190. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  191. package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
  192. package/src/plugins/builtin/composite-plugins.ts +3 -1
  193. package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
  194. package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
  195. package/src/plugins/builtin/congress-trades/model.ts +69 -1
  196. package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
  197. package/src/plugins/builtin/connections/index.ts +3 -0
  198. package/src/plugins/builtin/dividend-yield/client.ts +18 -0
  199. package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
  200. package/src/plugins/builtin/earnings/index.tsx +5 -3
  201. package/src/plugins/builtin/earnings/model.test.ts +19 -1
  202. package/src/plugins/builtin/earnings/model.ts +17 -15
  203. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  204. package/src/plugins/builtin/econ/index.test.tsx +6 -1
  205. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  206. package/src/plugins/builtin/help/index.tsx +2 -2
  207. package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
  208. package/src/plugins/builtin/insider/index.tsx +27 -11
  209. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  210. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  211. package/src/plugins/builtin/news/index.tsx +11 -6
  212. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
  213. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
  214. package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
  215. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  216. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  217. package/src/plugins/builtin/news/wire/index.tsx +3 -3
  218. package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
  219. package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
  220. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
  221. package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
  222. package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
  223. package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
  224. package/src/plugins/builtin/notes/files.ts +80 -0
  225. package/src/plugins/builtin/notes/index.tsx +3 -0
  226. package/src/plugins/builtin/notes/sync.test.ts +49 -0
  227. package/src/plugins/builtin/notes/sync.ts +98 -0
  228. package/src/plugins/builtin/options/index.tsx +1 -0
  229. package/src/plugins/builtin/options/view.test.tsx +25 -3
  230. package/src/plugins/builtin/options/view.tsx +1 -0
  231. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
  232. package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
  233. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  234. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  235. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  236. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  237. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  238. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
  239. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  240. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
  241. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  242. package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
  243. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  244. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
  245. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
  246. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
  247. package/src/plugins/builtin/research/index.test.ts +14 -0
  248. package/src/plugins/builtin/research/index.tsx +29 -0
  249. package/src/plugins/builtin/sec/index.tsx +24 -9
  250. package/src/plugins/builtin/sectors/index.tsx +14 -0
  251. package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
  252. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  253. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  254. package/src/plugins/builtin/shared/public-share.ts +23 -0
  255. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  256. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  257. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  258. package/src/plugins/builtin/substack/api/cache.ts +7 -3
  259. package/src/plugins/builtin/substack/api/loaders.ts +36 -12
  260. package/src/plugins/builtin/substack/api/types.ts +2 -0
  261. package/src/plugins/builtin/substack/pane-state.ts +1 -1
  262. package/src/plugins/builtin/substack/pane.tsx +55 -12
  263. package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
  264. package/src/plugins/builtin/thirteenf/api.ts +40 -18
  265. package/src/plugins/builtin/thirteenf/data.ts +15 -7
  266. package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
  267. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  268. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
  269. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
  270. package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
  271. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  273. package/src/plugins/catalog-browser.test.ts +78 -0
  274. package/src/plugins/catalog-browser.ts +136 -0
  275. package/src/plugins/gridlock-notification.ts +2 -1
  276. package/src/plugins/ibkr/flex/index.test.ts +19 -0
  277. package/src/plugins/ibkr/flex/index.ts +1 -1
  278. package/src/plugins/pane-manager/gridlock.ts +87 -3
  279. package/src/plugins/pane-manager.test.ts +30 -0
  280. package/src/plugins/pane-manager.ts +6 -1
  281. package/src/plugins/prediction-markets/chart.tsx +2 -2
  282. package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
  283. package/src/plugins/prediction-markets/controller/data.ts +7 -1
  284. package/src/plugins/prediction-markets/controller/index.ts +9 -2
  285. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  286. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  287. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  288. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  289. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  290. package/src/plugins/prediction-markets/index.tsx +8 -4
  291. package/src/plugins/prediction-markets/navigation.ts +0 -1
  292. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  293. package/src/plugins/prediction-markets/pane.tsx +12 -0
  294. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  295. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
  296. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
  297. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  298. package/src/plugins/registry/index.ts +2 -0
  299. package/src/plugins/ticker-navigation.test.ts +24 -0
  300. package/src/plugins/ticker-navigation.ts +40 -0
  301. package/src/remote/schema.ts +1 -1
  302. package/src/renderers/browser/app-services.ts +61 -0
  303. package/src/renderers/browser/cloud-transport.test.ts +68 -0
  304. package/src/renderers/browser/cloud-transport.ts +48 -0
  305. package/src/renderers/browser/config-host.ts +58 -0
  306. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  307. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  308. package/src/renderers/browser/error-boundary.tsx +23 -0
  309. package/src/renderers/browser/main.tsx +64 -0
  310. package/src/renderers/browser/persistence.ts +74 -0
  311. package/src/renderers/browser/storage.test.ts +66 -0
  312. package/src/renderers/browser/storage.ts +50 -0
  313. package/src/renderers/browser/ticker-repository.ts +59 -0
  314. package/src/renderers/browser/ui-host.tsx +40 -0
  315. package/src/renderers/cloudflare/worker.test.ts +136 -0
  316. package/src/renderers/cloudflare/worker.ts +116 -0
  317. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  318. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  319. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  320. package/src/renderers/electrobun/view/app-services.ts +14 -3
  321. package/src/renderers/electrobun/view/build-assets.ts +12 -10
  322. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  323. package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
  324. package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
  325. package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
  326. package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
  327. package/src/renderers/electrobun/view/host/box.tsx +11 -6
  328. package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
  329. package/src/renderers/electrobun/view/host/input.tsx +11 -7
  330. package/src/renderers/electrobun/view/host/native.ts +0 -5
  331. package/src/renderers/electrobun/view/main.tsx +2 -0
  332. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  333. package/src/renderers/electrobun/view/styles.css +107 -7
  334. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  335. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  336. package/src/renderers/electrobun/view/ui-host.tsx +6 -137
  337. package/src/renderers/opentui/start.tsx +2 -0
  338. package/src/renderers/share/layout-view.test.tsx +58 -0
  339. package/src/renderers/share/layout-view.tsx +133 -0
  340. package/src/renderers/share/main.tsx +102 -0
  341. package/src/renderers/share/styles.css +54 -0
  342. package/src/renderers/share/view.test.tsx +100 -0
  343. package/src/renderers/share/view.tsx +150 -0
  344. package/src/shares/api.test.ts +105 -0
  345. package/src/shares/api.ts +127 -0
  346. package/src/shares/chart-snapshot.test.ts +34 -0
  347. package/src/shares/chart-snapshot.ts +28 -0
  348. package/src/shares/location.test.ts +19 -0
  349. package/src/shares/location.ts +11 -0
  350. package/src/shares/pane.test.ts +110 -0
  351. package/src/shares/pane.ts +54 -0
  352. package/src/shares/payload.ts +177 -0
  353. package/src/sources/gloomberb-cloud/index.test.ts +61 -1
  354. package/src/sources/gloomberb-cloud/index.ts +86 -14
  355. package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
  356. package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
  357. package/src/sources/provider-router/history.test.ts +2 -2
  358. package/src/sources/provider-router/news.test.ts +34 -0
  359. package/src/sources/provider-router/news.ts +15 -1
  360. package/src/sources/sec-edgar.test.ts +59 -0
  361. package/src/sources/sec-edgar.ts +63 -23
  362. package/src/sources/yahoo-finance/mappers.ts +3 -0
  363. package/src/sources/yahoo-finance/snapshots.ts +10 -2
  364. package/src/sources/yahoo-finance/symbols.test.ts +10 -0
  365. package/src/sources/yahoo-finance/symbols.ts +13 -8
  366. package/src/sources/yahoo-finance.test.ts +1 -1
  367. package/src/sources/yahoo-finance.ts +6 -2
  368. package/src/state/app/bootstrap.ts +6 -7
  369. package/src/state/app/context/index.test.ts +41 -1
  370. package/src/state/app/context/index.test.tsx +34 -16
  371. package/src/state/selectors-ui.ts +0 -8
  372. package/src/sync/baseline.ts +32 -0
  373. package/src/sync/controller.test.ts +124 -1
  374. package/src/sync/controller.ts +54 -12
  375. package/src/sync/core-contributors.test.ts +44 -0
  376. package/src/sync/core-contributors.ts +59 -10
  377. package/src/sync/react.ts +22 -2
  378. package/src/sync/types.ts +12 -0
  379. package/src/theme/font-scale.ts +0 -11
  380. package/src/tickers/search/index.test.ts +27 -0
  381. package/src/tickers/search/upsert.ts +10 -0
  382. package/src/types/capability-route-source.ts +2 -1
  383. package/src/types/config.ts +35 -5
  384. package/src/types/desktop-menu.ts +1 -0
  385. package/src/types/plugin.ts +36 -0
  386. package/src/ui/context-menu.tsx +1 -1
  387. package/src/ui/host.tsx +4 -0
  388. package/src/ui/toast.tsx +2 -0
  389. package/src/utils/back-navigation.ts +0 -4
  390. package/src/utils/price-history.ts +11 -10
  391. package/src/utils/sec.test.ts +6 -0
  392. package/src/utils/sec.ts +1 -1
@@ -0,0 +1,98 @@
1
+ import type { SyncContributor } from "../../../sync/types";
2
+ import type { NoteFileEntry, NotesFiles, QuickNoteEntry } from "./files";
3
+
4
+ // The server rejects snapshots over 1.5MB, and notes share it with portfolios.
5
+ const MAX_NOTE_CHARS = 100_000;
6
+ const MAX_NOTES_CHARS = 400_000;
7
+
8
+ interface SyncedNote {
9
+ key: string;
10
+ text: string;
11
+ updatedAt: number;
12
+ }
13
+
14
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
15
+ return value != null && typeof value === "object" && !Array.isArray(value);
16
+ }
17
+
18
+ function parseNotes(value: unknown): SyncedNote[] {
19
+ if (!Array.isArray(value)) return [];
20
+ return value.flatMap((entry) => (
21
+ isPlainObject(entry) && typeof entry.key === "string" && typeof entry.text === "string"
22
+ ? [{
23
+ key: entry.key,
24
+ text: entry.text,
25
+ updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt)
26
+ ? entry.updatedAt
27
+ : 0,
28
+ }]
29
+ : []
30
+ ));
31
+ }
32
+
33
+ function parseQuickNotes(value: unknown): QuickNoteEntry[] {
34
+ if (!Array.isArray(value)) return [];
35
+ return value.flatMap((entry) => (
36
+ isPlainObject(entry) && typeof entry.id === "string" && typeof entry.title === "string"
37
+ ? [{
38
+ id: entry.id,
39
+ title: entry.title,
40
+ updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : undefined,
41
+ }]
42
+ : []
43
+ ));
44
+ }
45
+
46
+ function withinBudget(entries: NoteFileEntry[]): SyncedNote[] {
47
+ const notes: SyncedNote[] = [];
48
+ let total = 0;
49
+ for (const entry of entries) {
50
+ if (entry.text.length > MAX_NOTE_CHARS) continue;
51
+ if (total + entry.text.length > MAX_NOTES_CHARS) break;
52
+ total += entry.text.length;
53
+ notes.push(entry);
54
+ }
55
+ return notes;
56
+ }
57
+
58
+ /**
59
+ * Notes live in files rather than app state, so they carry their own
60
+ * timestamps and merge per note: newest write wins, identical text is left
61
+ * alone so the two sides converge instead of ping-ponging.
62
+ *
63
+ * ponytail: deletions do not propagate, a note deleted on one device comes
64
+ * back from the cloud. Add tombstones if that turns out to matter.
65
+ */
66
+ export function createNotesSyncContributor(notesFiles: NotesFiles): SyncContributor {
67
+ return {
68
+ id: "notes",
69
+ schemaVersion: 1,
70
+ collect: async () => ({
71
+ // list() is newest first, so the budget keeps the notes worked on last.
72
+ notes: withinBudget(await notesFiles.list()),
73
+ quickNotes: await notesFiles.loadQuickNotesIndex(),
74
+ }),
75
+ apply: async (payload, { isCurrent }) => {
76
+ if (!isPlainObject(payload)) return;
77
+ const local = new Map((await notesFiles.list()).map((entry) => [entry.key, entry]));
78
+
79
+ for (const remote of parseNotes(payload.notes)) {
80
+ if (!isCurrent()) return;
81
+ const current = local.get(remote.key);
82
+ if (current && (current.text === remote.text || current.updatedAt >= remote.updatedAt)) continue;
83
+ await notesFiles.save(remote.key, remote.text);
84
+ }
85
+
86
+ if (!isCurrent()) return;
87
+ const merged = new Map((await notesFiles.loadQuickNotesIndex()).map((entry) => [entry.id, entry]));
88
+ let changed = false;
89
+ for (const remote of parseQuickNotes(payload.quickNotes)) {
90
+ const current = merged.get(remote.id);
91
+ if (current && (current.updatedAt ?? 0) >= (remote.updatedAt ?? 0)) continue;
92
+ merged.set(remote.id, remote);
93
+ changed = true;
94
+ }
95
+ if (changed) await notesFiles.saveQuickNotesIndex([...merged.values()]);
96
+ },
97
+ };
98
+ }
@@ -50,6 +50,7 @@ export const optionsModule: PluginModule = {
50
50
  description: "Options chain for the selected ticker.",
51
51
  keywords: ["options", "chain", "calls", "puts", "omon"],
52
52
  shortcut: "OMON",
53
+ publicShare: true,
53
54
  }),
54
55
  ],
55
56
 
@@ -1,5 +1,5 @@
1
1
  import { afterEach, expect, test } from "bun:test";
2
- import { act } from "react";
2
+ import { act, useState } from "react";
3
3
  import type { ScrollBoxRenderable } from "@opentui/core";
4
4
  import { testRender } from "../../../renderers/opentui/test-utils";
5
5
  import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../../market-data/coordinator";
@@ -17,6 +17,7 @@ import { OptionsView } from "./view";
17
17
  const TEST_PANE_ID = "ticker-detail:options-test";
18
18
 
19
19
  let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
20
+ let setOptionsQuotePrice: ((price: number) => void) | null = null;
20
21
 
21
22
  function makeTicker(symbol: string): TickerRecord {
22
23
  return {
@@ -125,6 +126,12 @@ function OptionsHarness({
125
126
  );
126
127
  }
127
128
 
129
+ function RealtimeOptionsHarness({ ticker }: { ticker: TickerRecord }) {
130
+ const [quotePrice, setQuotePrice] = useState(120.2);
131
+ setOptionsQuotePrice = setQuotePrice;
132
+ return <OptionsHarness ticker={ticker} quotePrice={quotePrice} />;
133
+ }
134
+
128
135
  async function renderSettled() {
129
136
  for (let i = 0; i < 4; i += 1) {
130
137
  await act(async () => {
@@ -141,6 +148,7 @@ afterEach(async () => {
141
148
  });
142
149
  testSetup = undefined;
143
150
  }
151
+ setOptionsQuotePrice = null;
144
152
  setSharedMarketDataCoordinator(null);
145
153
  });
146
154
 
@@ -168,7 +176,7 @@ test("defaults the table around the nearest strike to the current quote", async
168
176
  expect(frame).not.toContain(" 50 ");
169
177
  });
170
178
 
171
- test("streams every visible option row and overlays live contract quotes", async () => {
179
+ test("streams live quotes without resetting manual scroll", async () => {
172
180
  const strikes = Array.from({ length: 100 }, (_, index) => 50 + index);
173
181
  const subscriptions: QuoteSubscriptionTarget[][] = [];
174
182
  let emitQuote:
@@ -187,7 +195,7 @@ test("streams every visible option row and overlays live contract quotes", async
187
195
 
188
196
  await act(async () => {
189
197
  testSetup = await testRender(
190
- <OptionsHarness ticker={makeTicker("AAPL")} quotePrice={120.2} />,
198
+ <RealtimeOptionsHarness ticker={makeTicker("AAPL")} />,
191
199
  {
192
200
  width: 124,
193
201
  height: 16,
@@ -238,6 +246,20 @@ test("streams every visible option row and overlays live contract quotes", async
238
246
  }).data?.mark,
239
247
  ).toBe(99.99);
240
248
  expect(testSetup!.captureCharFrame()).toContain("99.99");
249
+
250
+ const bodyScroll = testSetup!.renderer.root.findDescendantById("options-table-body-scroll") as ScrollBoxRenderable;
251
+ await act(async () => {
252
+ bodyScroll.scrollTo(0);
253
+ await testSetup!.renderOnce();
254
+ });
255
+ await renderSettled();
256
+
257
+ await act(async () => {
258
+ setOptionsQuotePrice?.(120.3);
259
+ await testSetup!.renderOnce();
260
+ });
261
+ await renderSettled();
262
+ expect(bodyScroll.scrollTop).toBe(0);
241
263
  });
242
264
 
243
265
  test("lets the expiration tab row use the full available width", async () => {
@@ -465,6 +465,7 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
465
465
  sortDirection="asc"
466
466
  onHeaderClick={() => {}}
467
467
  onTableMouseDown={enterInteractive}
468
+ onBodyScrollActivity={() => { userSelectedStrikeRef.current = true; }}
468
469
  visibleRangeKey={viewportKey}
469
470
  onVisibleRangeChange={handleVisibleStrikeRangeChange}
470
471
  getItemKey={(row) => String(row.strike)}
@@ -38,6 +38,28 @@ describe("resolvePortfolioAccountMetrics", () => {
38
38
  });
39
39
  });
40
40
 
41
+ test("converts broker account money into the app base currency", () => {
42
+ const account: BrokerAccount = {
43
+ accountId: "DU12345",
44
+ name: "DU12345",
45
+ currency: "EUR",
46
+ netLiquidation: 10_000,
47
+ grossPositionValue: 8_000,
48
+ dailyPnl: 100,
49
+ unrealizedPnl: 400,
50
+ realizedPnl: -50,
51
+ };
52
+ const toUsd = (value: number) => value * 1.1;
53
+
54
+ const metrics = resolvePortfolioAccountMetrics(createTotals(), account, toUsd);
55
+ expect(metrics.dailyPnl).toBeCloseTo(110);
56
+ expect(metrics.dailyPnlPct).toBeCloseTo(110 / 10_890 * 100);
57
+ expect(metrics.unrealizedPnl).toBeCloseTo(440);
58
+ expect(metrics.unrealizedPnlPct).toBeCloseTo(5.5);
59
+ expect(metrics.realizedPnl).toBeCloseTo(-55);
60
+ expect(resolvePortfolioMarketValue(createTotals(), account, toUsd)).toBeCloseTo(8_800);
61
+ });
62
+
41
63
  test("falls back to reconstructed portfolio totals when broker P&L is missing", () => {
42
64
  expect(resolvePortfolioAccountMetrics(createTotals(), null)).toEqual({
43
65
  dailyPnl: 50,
@@ -17,9 +17,12 @@ function percentChange(value: number, previousValue: number): number {
17
17
  return previousValue !== 0 ? (value / previousValue) * 100 : 0;
18
18
  }
19
19
 
20
- export function resolveBrokerPortfolioMarketValue(account?: BrokerAccount | null): number | null {
20
+ export function resolveBrokerPortfolioMarketValue(
21
+ account?: BrokerAccount | null,
22
+ convertAccountValue: (value: number) => number = (value) => value,
23
+ ): number | null {
21
24
  if (finiteNumber(account?.grossPositionValue)) {
22
- return account.grossPositionValue;
25
+ return convertAccountValue(account.grossPositionValue);
23
26
  }
24
27
  return null;
25
28
  }
@@ -27,24 +30,26 @@ export function resolveBrokerPortfolioMarketValue(account?: BrokerAccount | null
27
30
  export function resolvePortfolioMarketValue(
28
31
  totals: PortfolioSummaryTotals,
29
32
  account?: BrokerAccount | null,
33
+ convertAccountValue: (value: number) => number = (value) => value,
30
34
  ): number {
31
- return resolveBrokerPortfolioMarketValue(account) ?? totals.totalMktValue;
35
+ return resolveBrokerPortfolioMarketValue(account, convertAccountValue) ?? totals.totalMktValue;
32
36
  }
33
37
 
34
38
  export function resolvePortfolioAccountMetrics(
35
39
  totals: PortfolioSummaryTotals,
36
40
  account?: BrokerAccount | null,
41
+ convertAccountValue: (value: number) => number = (value) => value,
37
42
  ): PortfolioAccountMetrics {
38
- const brokerDailyPnl = finiteNumber(account?.dailyPnl) ? account.dailyPnl : null;
43
+ const brokerDailyPnl = finiteNumber(account?.dailyPnl) ? convertAccountValue(account.dailyPnl) : null;
39
44
  const dailyPnl = brokerDailyPnl ?? totals.dailyPnl;
40
45
  const previousNetLiquidation = brokerDailyPnl != null && finiteNumber(account?.netLiquidation)
41
- ? account.netLiquidation - dailyPnl
46
+ ? convertAccountValue(account.netLiquidation) - dailyPnl
42
47
  : null;
43
48
  const dailyPnlPct = previousNetLiquidation != null
44
49
  ? percentChange(dailyPnl, previousNetLiquidation)
45
50
  : totals.dailyPnlPct;
46
51
 
47
- const brokerUnrealizedPnl = finiteNumber(account?.unrealizedPnl) ? account.unrealizedPnl : null;
52
+ const brokerUnrealizedPnl = finiteNumber(account?.unrealizedPnl) ? convertAccountValue(account.unrealizedPnl) : null;
48
53
  const unrealizedPnl = brokerUnrealizedPnl ?? totals.unrealizedPnl;
49
54
  const unrealizedPnlPct = totals.totalCostBasis !== 0
50
55
  ? percentChange(unrealizedPnl, totals.totalCostBasis)
@@ -55,6 +60,6 @@ export function resolvePortfolioAccountMetrics(
55
60
  dailyPnlPct,
56
61
  unrealizedPnl,
57
62
  unrealizedPnlPct,
58
- realizedPnl: finiteNumber(account?.realizedPnl) ? account.realizedPnl : undefined,
63
+ realizedPnl: finiteNumber(account?.realizedPnl) ? convertAccountValue(account.realizedPnl) : undefined,
59
64
  };
60
65
  }
@@ -142,11 +142,13 @@ async function showCollectionWithMarketData(
142
142
  const multiplier = position.multiplier ?? 1;
143
143
  const quoteCurrency = quote?.currency ?? ticker.metadata.currency ?? baseCurrency;
144
144
  const positionCurrency = position.currency ?? quoteCurrency;
145
+ const direction = position.side === "short" || position.shares < 0 ? -1 : 1;
146
+ const magnitude = Math.abs(position.shares);
145
147
  const currentValueBase = quote
146
- ? await toBase(Math.abs(position.shares) * quote.price * multiplier, quoteCurrency)
148
+ ? await toBase(magnitude * quote.price * multiplier, quoteCurrency)
147
149
  : null;
148
- const costBasisBase = await toBase(position.shares * position.avgCost * multiplier, positionCurrency);
149
- const pnl = currentValueBase != null ? currentValueBase - costBasisBase : null;
150
+ const costBasisBase = await toBase(magnitude * position.avgCost * multiplier, positionCurrency);
151
+ const pnl = currentValueBase != null ? direction * (currentValueBase - costBasisBase) : null;
150
152
  if (pnl != null) totalPnl += pnl;
151
153
 
152
154
  rows.push([
@@ -26,6 +26,7 @@ import {
26
26
  getPortfolioPositionMetrics,
27
27
  resolveBrokerFallbackMarketValue,
28
28
  resolveBrokerFallbackPnl,
29
+ signedQuoteUnrealizedPnl,
29
30
  } from "./position-metrics";
30
31
 
31
32
  export interface ColumnContext {
@@ -332,7 +333,11 @@ export function getColumnValue(
332
333
  return { text: "—" };
333
334
  case "pnl":
334
335
  if (activeQuote && totalPriceUnits !== 0) {
335
- const pnl = toBaseQuote(Math.abs(totalPriceUnits) * activeQuote.price) - toBasePosition(totalCost);
336
+ const pnl = signedQuoteUnrealizedPnl(
337
+ toBaseQuote(Math.abs(totalPriceUnits) * activeQuote.price),
338
+ toBasePosition(totalCost),
339
+ totalPriceUnits,
340
+ );
336
341
  return { text: `${pnl >= 0 ? "+" : ""}${formatCompact(pnl)}`, color: priceColor(pnl) };
337
342
  }
338
343
  if (brokerFallbackPnl != null) {
@@ -344,7 +349,8 @@ export function getColumnValue(
344
349
  if (activeQuote && totalCost !== 0) {
345
350
  const marketValue = toBaseQuote(Math.abs(totalPriceUnits) * activeQuote.price);
346
351
  const costBasis = toBasePosition(totalCost);
347
- const percent = costBasis !== 0 ? ((marketValue - costBasis) / costBasis) * 100 : 0;
352
+ const pnl = signedQuoteUnrealizedPnl(marketValue, costBasis, totalPriceUnits);
353
+ const percent = costBasis !== 0 ? (pnl / costBasis) * 100 : 0;
348
354
  return { text: formatPercentRaw(percent), color: priceColor(percent) };
349
355
  }
350
356
  if (brokerFallbackPnl != null && totalCost !== 0) {
@@ -530,7 +536,11 @@ export function getSortValue(
530
536
  return null;
531
537
  case "pnl":
532
538
  if (activeQuote && totalPriceUnits !== 0) {
533
- return toBaseQuote(Math.abs(totalPriceUnits) * activeQuote.price) - toBasePosition(totalCost);
539
+ return signedQuoteUnrealizedPnl(
540
+ toBaseQuote(Math.abs(totalPriceUnits) * activeQuote.price),
541
+ toBasePosition(totalCost),
542
+ totalPriceUnits,
543
+ );
534
544
  }
535
545
  if (brokerFallbackPnl != null) {
536
546
  return toBasePosition(brokerFallbackPnl);
@@ -540,7 +550,8 @@ export function getSortValue(
540
550
  if (activeQuote && totalCost !== 0) {
541
551
  const marketValue = toBaseQuote(Math.abs(totalPriceUnits) * activeQuote.price);
542
552
  const costBasis = toBasePosition(totalCost);
543
- return costBasis !== 0 ? ((marketValue - costBasis) / costBasis) * 100 : null;
553
+ const pnl = signedQuoteUnrealizedPnl(marketValue, costBasis, totalPriceUnits);
554
+ return costBasis !== 0 ? (pnl / costBasis) * 100 : null;
544
555
  }
545
556
  if (brokerFallbackPnl != null && totalCost !== 0) {
546
557
  const costBasis = toBasePosition(totalCost);
@@ -7,7 +7,7 @@ import { colors } from "../../../theme/colors";
7
7
  import type { BrokerConnectionStatus } from "../../../types/broker";
8
8
  import type { Portfolio } from "../../../types/ticker";
9
9
  import type { BrokerAccount } from "../../../types/trading";
10
- import { formatCompact, padTo } from "../../../utils/format";
10
+ import { convertCurrency, formatCompact, padTo } from "../../../utils/format";
11
11
  import { formatMarketQuantity } from "../../../market-data/market/format";
12
12
  import { getBrokerInstance } from "../../../utils/broker-instances";
13
13
  import { usePluginBrokerActions } from "../../runtime";
@@ -90,14 +90,24 @@ export function PortfolioCashMarginDrawer({
90
90
  onToggle,
91
91
  width,
92
92
  height,
93
+ baseCurrency,
94
+ exchangeRates,
93
95
  }: {
94
96
  accountState: ResolvedPortfolioAccountState;
95
97
  expanded: boolean;
96
98
  onToggle: () => void;
97
99
  width: number;
98
100
  height: number;
101
+ baseCurrency: string;
102
+ exchangeRates: Map<string, number>;
99
103
  }) {
100
- const previewText = `${accountState.visibleCashBalances.length} ccy · Cash ${formatCompact(accountState.account.totalCashValue)} · ${accountState.sourceLabel}`;
104
+ const convertAccountValue = (value: number) => convertCurrency(
105
+ value,
106
+ accountState.account.currency || baseCurrency,
107
+ baseCurrency,
108
+ exchangeRates,
109
+ );
110
+ const previewText = `${accountState.visibleCashBalances.length} ccy · Cash ${formatCompact(convertAccountValue(accountState.account.totalCashValue ?? 0))} · ${accountState.sourceLabel}`;
101
111
  const drawerHeight = Math.max(1, height);
102
112
 
103
113
  if (!expanded) {
@@ -124,7 +134,7 @@ export function PortfolioCashMarginDrawer({
124
134
  );
125
135
  }
126
136
 
127
- const metricSegments = buildDrawerMetricSegments(accountState.account, width);
137
+ const metricSegments = buildDrawerMetricSegments(accountState.account, width, convertAccountValue);
128
138
  const currencyRowsHeight = Math.max(1, drawerHeight - 2);
129
139
 
130
140
  return (
@@ -47,7 +47,15 @@ export const portfolioListModule: PluginModule = {
47
47
  defaultPosition: "left",
48
48
  defaultMode: "floating",
49
49
  defaultWidth: "40%",
50
+ tickerSource: true,
50
51
  quickSettings: [LIVE_STREAMING_QUICK_SETTING],
52
+ portableShare: {
53
+ private: {
54
+ params: ["collectionId"],
55
+ settings: ["collectionId", "visibleCollectionIds"],
56
+ state: ["collectionId", "collectionSorts"],
57
+ },
58
+ },
51
59
  settings: (context) => withLiveStreamingSetting(
52
60
  buildPortfolioPaneSettingsDef(
53
61
  context.config,
@@ -199,7 +199,7 @@ describe("portfolio-metrics", () => {
199
199
 
200
200
  expect(totals.totalMktValue).toBeCloseTo(58803.06, 2);
201
201
  expect(totals.totalCostBasis).toBeCloseTo(50950.7295, 4);
202
- expect(totals.unrealizedPnl).toBeCloseTo(7852.3305, 4);
202
+ expect(totals.unrealizedPnl).toBeCloseTo(7852.33, 4);
203
203
  expect(totals.hasPositions).toBe(true);
204
204
  });
205
205
 
@@ -277,6 +277,36 @@ describe("portfolio-metrics", () => {
277
277
  });
278
278
  });
279
279
 
280
+ test("signs short unrealized P&L from a live quote", () => {
281
+ const ticker = createTicker({
282
+ positions: [{ portfolio: "main", shares: 10, avgCost: 100, broker: "ibkr", side: "short" }],
283
+ });
284
+ const financials = createFinancials({
285
+ quote: { price: 80, change: -20, changePercent: -20, previousClose: 100 },
286
+ });
287
+ const dayPnlColumn: ColumnConfig = { id: "day_pnl", label: "DAY", width: 10, align: "right", format: "compact" };
288
+ const pnlColumn: ColumnConfig = { id: "pnl", label: "P&L", width: 10, align: "right", format: "compact" };
289
+
290
+ expect(getSortValue(dayPnlColumn, ticker, financials, defaultColumnContext)).toBe(200);
291
+ expect(getSortValue(pnlColumn, ticker, financials, defaultColumnContext)).toBe(200);
292
+ expect(getColumnValue(pnlColumn, ticker, financials, defaultColumnContext)).toEqual({
293
+ text: "+200",
294
+ color: expect.any(String),
295
+ });
296
+ expect(calculatePortfolioSummaryTotals(
297
+ [ticker],
298
+ new Map([["AAPL", financials]]),
299
+ "USD",
300
+ new Map([["USD", 1]]),
301
+ true,
302
+ "main",
303
+ )).toMatchObject({
304
+ totalMktValue: 800,
305
+ dailyPnl: 200,
306
+ unrealizedPnl: 200,
307
+ });
308
+ });
309
+
280
310
  test("formats derived quote and position columns", () => {
281
311
  const ticker = createTicker({
282
312
  sector: "Technology",
@@ -431,6 +431,7 @@ async function renderHiddenChangePctSortWarmup(options: { staleCachedSiveSnapsho
431
431
  previousClose: 0,
432
432
  listingExchangeName: "NASDAQ",
433
433
  lastUpdated: Date.now() - 24 * 60 * 60_000,
434
+ stale: true,
434
435
  }),
435
436
  },
436
437
  }]);
@@ -6,7 +6,7 @@ import {
6
6
  type DataTableKeyEvent,
7
7
  type TickerListVisibleRange,
8
8
  } from "../../../../components";
9
- import { usePluginTickerActions } from "../../../runtime";
9
+ import { useTickerSourceActivate } from "../../shared/ticker-source";
10
10
  import { useFxRatesMap, useTickerFinancialsMap } from "../../../../market-data/hooks";
11
11
  import { useAppActive } from "../../../../state/app/activity";
12
12
  import {
@@ -20,7 +20,6 @@ import {
20
20
  import { selectEffectiveExchangeRates } from "../../../../utils/exchange-rate-map";
21
21
  import type { TickerRecord } from "../../../../types/ticker";
22
22
  import type { PaneProps } from "../../../../types/plugin";
23
- import { TICKER_RESEARCH_PANE_ID } from "../../../../types/config";
24
23
  import { calculatePortfolioSummaryTotals, resolveCollectionSortPreference, type ColumnContext } from "../metrics";
25
24
  import {
26
25
  PortfolioCashMarginDrawer,
@@ -55,7 +54,7 @@ import { useLiveStreamingSetting } from "../../shared/live-streaming";
55
54
  import { useThrottledTickerOrder } from "../use-throttled-ticker-order";
56
55
 
57
56
  export function PortfolioListPane({ focused, width, height }: PaneProps) {
58
- const { pinTicker } = usePluginTickerActions();
57
+ const activateTicker = useTickerSourceActivate();
59
58
  const paneInstance = usePaneInstance();
60
59
  const appActive = useAppActive();
61
60
  const config = useAppSelector((state) => state.config);
@@ -238,9 +237,9 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
238
237
  setSortPreference({ columnId, direction: "asc" });
239
238
  }, [activeSort.columnId, activeSort.direction, setSortPreference]);
240
239
 
241
- const openTickerFloating = useCallback((symbol: string) => {
242
- pinTicker(symbol, { floating: true, paneType: TICKER_RESEARCH_PANE_ID });
243
- }, [pinTicker]);
240
+ const openTickerFloating = useCallback((symbol: string, options?: { newPane?: boolean }) => {
241
+ activateTicker(symbol, { floating: true, newPane: options?.newPane });
242
+ }, [activateTicker]);
244
243
 
245
244
  const toggleViewMode = useCallback(() => {
246
245
  if (!isPortfolioTab) return;
@@ -266,7 +265,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
266
265
  event.stopPropagation?.();
267
266
  const ticker = sortedTickers[safeSelectedIdx];
268
267
  if (ticker) {
269
- openTickerFloating(ticker.metadata.ticker);
268
+ flushCursorSymbol(ticker.metadata.ticker);
269
+ openTickerFloating(ticker.metadata.ticker, { newPane: true });
270
270
  }
271
271
  return true;
272
272
  }
@@ -288,6 +288,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
288
288
  return false;
289
289
  }, [
290
290
  cashDrawerExpanded,
291
+ flushCursorSymbol,
291
292
  focused,
292
293
  isPortfolioTab,
293
294
  openTickerFloating,
@@ -475,6 +476,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
475
476
  onToggle={() => setCashDrawerExpanded(!cashDrawerExpanded)}
476
477
  width={Math.max(0, width - 2)}
477
478
  height={drawerHeight}
479
+ baseCurrency={config.baseCurrency}
480
+ exchangeRates={effectiveExchangeRates}
478
481
  />
479
482
  </Box>
480
483
  )}
@@ -71,7 +71,12 @@ export function usePortfolioPaneStreaming({
71
71
  const streamTargets = useMemo(() => (
72
72
  sortedTickers
73
73
  .map((ticker) => {
74
- const target = quoteSubscriptionTargetFromTicker(ticker, ticker.metadata.ticker, "provider", instrumentOptions);
74
+ const target = quoteSubscriptionTargetFromTicker(
75
+ ticker,
76
+ ticker.metadata.ticker,
77
+ isPortfolioTab && (ticker.metadata.broker_contracts?.length ?? 0) > 0 ? "broker" : "provider",
78
+ instrumentOptions,
79
+ );
75
80
  if (!target) return null;
76
81
  const selected = ticker.metadata.ticker === cursorSymbol;
77
82
  const visible = priorityStreamSymbols.has(ticker.metadata.ticker);
@@ -27,6 +27,24 @@ function normalizePositionMultiplier(multiplier: number | undefined): number {
27
27
  : 1;
28
28
  }
29
29
 
30
+ export function signedPositionDirection(position: {
31
+ shares: number;
32
+ side?: "long" | "short";
33
+ }): 1 | -1 {
34
+ if (position.side === "short") return -1;
35
+ if (position.side === "long") return 1;
36
+ return position.shares < 0 ? -1 : 1;
37
+ }
38
+
39
+ /** Quote-path unrealized P&L. Shorts profit when market value falls below cost. */
40
+ export function signedQuoteUnrealizedPnl(
41
+ absMarketValue: number,
42
+ cost: number,
43
+ totalPriceUnits: number,
44
+ ): number {
45
+ return (totalPriceUnits < 0 ? -1 : 1) * (absMarketValue - cost);
46
+ }
47
+
30
48
  function resolvePositionCostMultiplier(
31
49
  position: TickerRecord["metadata"]["positions"][number],
32
50
  ): number {
@@ -37,7 +55,7 @@ function resolvePositionCostMultiplier(
37
55
  return priceMultiplier;
38
56
  }
39
57
 
40
- const costWithoutMultiplier = position.shares * position.avgCost;
58
+ const costWithoutMultiplier = Math.abs(position.shares) * position.avgCost;
41
59
  const costWithMultiplier = costWithoutMultiplier * priceMultiplier;
42
60
  const withoutMultiplierError = Math.abs((position.marketValue - costWithoutMultiplier) - position.unrealizedPnl);
43
61
  const withMultiplierError = Math.abs((position.marketValue - costWithMultiplier) - position.unrealizedPnl);
@@ -65,15 +83,16 @@ export function getPortfolioPositionMetrics(
65
83
  let brokerPnl = 0;
66
84
  let hasBrokerPnl = false;
67
85
  for (const position of tabPositions) {
68
- const direction = position.side === "short" ? -1 : 1;
86
+ const direction = signedPositionDirection(position);
87
+ const magnitude = Math.abs(position.shares);
69
88
  const priceMultiplier = normalizePositionMultiplier(position.multiplier);
70
89
  const costMultiplier = resolvePositionCostMultiplier(position);
71
90
  multiplierHint = Math.max(multiplierHint, priceMultiplier, costMultiplier);
72
91
 
73
- totalShares += position.shares * direction;
74
- totalCost += position.shares * position.avgCost * costMultiplier;
75
- totalCostUnits += position.shares * costMultiplier;
76
- totalPriceUnits += position.shares * priceMultiplier * direction;
92
+ totalShares += magnitude * direction;
93
+ totalCost += magnitude * position.avgCost * costMultiplier;
94
+ totalCostUnits += magnitude * costMultiplier;
95
+ totalPriceUnits += magnitude * priceMultiplier * direction;
77
96
 
78
97
  if (position.marketValue != null) {
79
98
  brokerMktValue += position.marketValue;