gloomberb 0.18.0 → 0.18.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 (365) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/market-movers.ts +7 -0
  8. package/src/api-client/types.ts +14 -0
  9. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  10. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  11. package/src/app/runtime/plugin-bindings.ts +6 -2
  12. package/src/brokers/logos.ts +66 -0
  13. package/src/brokers/profile-form.ts +3 -0
  14. package/src/brokers/remote-broker-adapter.ts +13 -3
  15. package/src/capabilities/index.ts +1 -1
  16. package/src/capabilities/service-capabilities.ts +7 -1
  17. package/src/cli/base-converter.ts +60 -18
  18. package/src/cli/commands/fred-series.ts +15 -5
  19. package/src/cli/commands/market.ts +290 -159
  20. package/src/cli/commands/news-search.ts +35 -8
  21. package/src/cli/commands/overview.ts +31 -6
  22. package/src/cli/commands/system.ts +35 -1
  23. package/src/cli/commands/ticker.ts +105 -52
  24. package/src/cli/currency-hints.ts +27 -0
  25. package/src/cli/fx-pair.ts +41 -4
  26. package/src/cli/glossary.ts +57 -4
  27. package/src/cli/helpers.ts +36 -3
  28. package/src/cli/index.ts +4 -1
  29. package/src/cli/known-gaps.ts +37 -1
  30. package/src/cli/listing-arg.ts +37 -2
  31. package/src/cli/pane-functions/catalog.ts +3 -1
  32. package/src/cli/pane-functions/discovery.ts +15 -1
  33. package/src/cli/pane-functions/dom.ts +9 -4
  34. package/src/cli/pane-functions/freshness.ts +4 -4
  35. package/src/cli/pane-functions/function-help.ts +31 -7
  36. package/src/cli/pane-functions/headless.ts +6 -5
  37. package/src/cli/pane-functions/index.ts +3 -2
  38. package/src/cli/pane-functions/lock-note.ts +7 -4
  39. package/src/cli/pane-functions/resolver.ts +2 -1
  40. package/src/cli/quote-answer.ts +179 -0
  41. package/src/cli/series-search.ts +2 -1
  42. package/src/cli/symbol-list.ts +51 -16
  43. package/src/cli/types.ts +4 -0
  44. package/src/components/chart/composite/composite-chart.tsx +22 -7
  45. package/src/components/chart/composite/format.ts +13 -1
  46. package/src/components/chart/composite/level-labels.ts +30 -0
  47. package/src/components/chart/composite/levels.ts +17 -6
  48. package/src/components/chart/composite/panel-surface.tsx +64 -16
  49. package/src/components/chart/composite/price-axis-labels.tsx +2 -1
  50. package/src/components/chart/composite/types.ts +4 -0
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  52. package/src/components/command-bar/workflow/collection-actions.ts +4 -0
  53. package/src/components/index.ts +2 -0
  54. package/src/components/layout/pane/content.tsx +16 -14
  55. package/src/components/layout/pane/footer/index.tsx +22 -3
  56. package/src/components/layout/pane/footer/model.ts +99 -4
  57. package/src/components/layout/pane/header-tabs.tsx +15 -1
  58. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  59. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  60. package/src/components/onboarding/wizard-model.ts +10 -3
  61. package/src/components/onboarding/wizard-positions.ts +8 -0
  62. package/src/components/ui/button.tsx +11 -4
  63. package/src/components/ui/choice-dialog.tsx +8 -5
  64. package/src/components/ui/display.tsx +11 -6
  65. package/src/components/ui/fields.tsx +100 -20
  66. package/src/components/ui/icon.tsx +6 -1
  67. package/src/components/ui/list-row-lines.tsx +87 -0
  68. package/src/components/ui/list-view.tsx +14 -0
  69. package/src/components/ui/logo-mark.tsx +64 -0
  70. package/src/components/ui/number-step.ts +54 -0
  71. package/src/components/ui/select-button.tsx +5 -4
  72. package/src/components/ui/select-field.tsx +3 -3
  73. package/src/components/ui/status.tsx +17 -3
  74. package/src/components/ui/tabs.tsx +9 -1
  75. package/src/components/ui/toggle.tsx +17 -3
  76. package/src/core/state/app/instrument.ts +8 -3
  77. package/src/data/config/store/normalize.ts +4 -1
  78. package/src/data/plugin-cache.ts +32 -6
  79. package/src/docs/snapshot.json +57 -22
  80. package/src/i18n/es.ts +9 -3
  81. package/src/i18n/ja.ts +9 -3
  82. package/src/i18n/ko.ts +9 -3
  83. package/src/i18n/zh-cn.ts +10 -4
  84. package/src/i18n/zh-tw.ts +10 -4
  85. package/src/market-data/commodity-names.ts +80 -0
  86. package/src/market-data/coordinator/auxiliary.ts +3 -1
  87. package/src/market-data/coordinator/chart.ts +3 -1
  88. package/src/market-data/futures-board.ts +38 -1
  89. package/src/market-data/futures-contract.ts +91 -0
  90. package/src/market-data/fx-closes.ts +78 -1
  91. package/src/market-data/market/change-basis.ts +219 -0
  92. package/src/market-data/market/format.ts +28 -12
  93. package/src/market-data/market/freshness.ts +38 -0
  94. package/src/market-data/market/status.ts +55 -9
  95. package/src/market-data/market/year-to-date.ts +46 -0
  96. package/src/market-data/quotes/contributions.ts +12 -0
  97. package/src/market-data/quotes/delisting.ts +34 -0
  98. package/src/market-data/quotes/live-valuation.ts +23 -0
  99. package/src/market-data/quotes/resolution.ts +8 -2
  100. package/src/market-data/tax-equivalent.ts +10 -2
  101. package/src/notifications/app-notifier.ts +2 -2
  102. package/src/notifications/notification-log.ts +8 -6
  103. package/src/plugins/builtin/alerts/cli.ts +26 -18
  104. package/src/plugins/builtin/alerts/headless.ts +1 -1
  105. package/src/plugins/builtin/alerts/index.tsx +13 -4
  106. package/src/plugins/builtin/analytics/index.tsx +1 -1
  107. package/src/plugins/builtin/analytics/risk-client.ts +44 -11
  108. package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
  109. package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
  110. package/src/plugins/builtin/analytics/risk-model.ts +79 -16
  111. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  112. package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
  113. package/src/plugins/builtin/attention/trending.tsx +8 -6
  114. package/src/plugins/builtin/backtest/client.ts +5 -2
  115. package/src/plugins/builtin/backtest/window.ts +16 -3
  116. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  117. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  118. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  119. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  120. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  121. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  122. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  123. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  124. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  125. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  126. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  127. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  128. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  129. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  130. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  131. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  132. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  133. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  134. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  135. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  136. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  137. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  138. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  139. package/src/plugins/builtin/buildout/format.ts +1 -0
  140. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  141. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  142. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  143. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  144. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  145. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  146. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  147. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  148. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  149. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  150. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  151. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  152. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  153. package/src/plugins/builtin/chat/index.tsx +1 -1
  154. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  155. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  156. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  157. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
  158. package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
  159. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  160. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  161. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  162. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  163. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  164. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  165. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  166. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  167. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  168. package/src/plugins/builtin/composite-plugins.ts +3 -2
  169. package/src/plugins/builtin/cot/headless.ts +1 -1
  170. package/src/plugins/builtin/cot/index.tsx +1 -1
  171. package/src/plugins/builtin/cot/model.ts +35 -12
  172. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  173. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  174. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  175. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  176. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  177. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  178. package/src/plugins/builtin/custom-view/loader.ts +3 -2
  179. package/src/plugins/builtin/custom-view/pane.tsx +5 -4
  180. package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
  181. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  182. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  183. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  184. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  185. package/src/plugins/builtin/econ/calendar-model.ts +38 -2
  186. package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
  187. package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
  188. package/src/plugins/builtin/econ/index.tsx +31 -4
  189. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  190. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  191. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  192. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  193. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  194. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  195. package/src/plugins/builtin/executives/data.ts +24 -8
  196. package/src/plugins/builtin/executives/index.tsx +2 -18
  197. package/src/plugins/builtin/executives/pane.tsx +34 -10
  198. package/src/plugins/builtin/exposure/client.ts +4 -1
  199. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  200. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  201. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  202. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  203. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  204. package/src/plugins/builtin/futures/table.tsx +6 -3
  205. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  206. package/src/plugins/builtin/insider/base-values.ts +72 -0
  207. package/src/plugins/builtin/insider/headless.ts +29 -4
  208. package/src/plugins/builtin/jobs/headless.ts +2 -2
  209. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  210. package/src/plugins/builtin/market-halts/model.ts +31 -6
  211. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  212. package/src/plugins/builtin/market-movers/client.ts +2 -2
  213. package/src/plugins/builtin/market-movers/index.tsx +0 -3
  214. package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
  215. package/src/plugins/builtin/market-movers/session.ts +25 -10
  216. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  217. package/src/plugins/builtin/members/pane.tsx +8 -3
  218. package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
  219. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  220. package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
  221. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
  222. package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
  223. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  224. package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
  225. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  226. package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
  227. package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
  228. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  229. package/src/plugins/builtin/notes/cli.ts +33 -8
  230. package/src/plugins/builtin/notes/export.ts +49 -3
  231. package/src/plugins/builtin/notes/files.ts +14 -3
  232. package/src/plugins/builtin/notes/index.tsx +3 -3
  233. package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
  234. package/src/plugins/builtin/notes/store.ts +13 -3
  235. package/src/plugins/builtin/options/footer.ts +25 -20
  236. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  237. package/src/plugins/builtin/options/view.tsx +77 -22
  238. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  239. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  240. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  241. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
  242. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
  243. package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
  244. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
  245. package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
  246. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  247. package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
  248. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  249. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
  250. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
  251. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  252. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  253. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  254. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  255. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  256. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  257. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  258. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  259. package/src/plugins/builtin/scanner/feed.ts +6 -1
  260. package/src/plugins/builtin/scanner/headless.ts +1 -1
  261. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  262. package/src/plugins/builtin/series-inventory.ts +1 -0
  263. package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
  264. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  265. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  266. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  267. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  268. package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
  269. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  270. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  271. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  272. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  273. package/src/plugins/builtin/themes/headless.ts +8 -2
  274. package/src/plugins/builtin/themes/model.ts +43 -10
  275. package/src/plugins/builtin/themes/pane.tsx +8 -6
  276. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  277. package/src/plugins/builtin/thirteenf/data.ts +5 -1
  278. package/src/plugins/builtin/thirteenf/headless.ts +3 -1
  279. package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
  280. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  281. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  282. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  283. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  284. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  285. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  286. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  287. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  288. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  289. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  290. package/src/plugins/builtin-editorial.ts +1 -1
  291. package/src/plugins/compat.ts +3 -0
  292. package/src/plugins/external-runtime.ts +5 -0
  293. package/src/plugins/loader.ts +6 -0
  294. package/src/plugins/plugin-icon.ts +61 -0
  295. package/src/plugins/registry/host-actions.ts +2 -1
  296. package/src/plugins/registry/index.ts +9 -1
  297. package/src/plugins/runtime/context.tsx +5 -0
  298. package/src/plugins/runtime/index.tsx +3 -0
  299. package/src/portfolio/column-values.ts +13 -4
  300. package/src/portfolio/cost-fx.ts +61 -0
  301. package/src/portfolio/mutations.ts +16 -2
  302. package/src/portfolio/position-metrics.ts +45 -4
  303. package/src/portfolio/position-quantity.ts +43 -0
  304. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  305. package/src/public/broker.ts +8 -0
  306. package/src/public/market-data.ts +5 -0
  307. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  308. package/src/renderers/dom/desktop/controls.tsx +39 -7
  309. package/src/renderers/dom/desktop/icons.tsx +14 -0
  310. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  311. package/src/renderers/dom/desktop/menu.tsx +10 -1
  312. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  313. package/src/renderers/dom/host/style.ts +6 -1
  314. package/src/renderers/dom/host/tabs.tsx +6 -0
  315. package/src/renderers/dom/styles.css +12 -5
  316. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  317. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  318. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  319. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  320. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  321. package/src/series/validate.ts +2 -2
  322. package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
  323. package/src/sources/gloomberb-cloud/index.ts +50 -17
  324. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  325. package/src/sources/history-result.ts +3 -0
  326. package/src/sources/provider-errors.ts +3 -1
  327. package/src/sources/provider-router/history.ts +49 -6
  328. package/src/sources/provider-router/index.ts +2 -1
  329. package/src/state/app/activity.ts +13 -0
  330. package/src/sync/controller.ts +4 -0
  331. package/src/sync/core-contributors.ts +63 -11
  332. package/src/sync/local-only.ts +121 -0
  333. package/src/sync/react.ts +6 -0
  334. package/src/sync/types.ts +3 -0
  335. package/src/team/collections.ts +2 -0
  336. package/src/tickers/listing-fate.ts +27 -8
  337. package/src/tickers/metadata.ts +10 -0
  338. package/src/tickers/search/index.ts +24 -3
  339. package/src/time-series/price-comparison.ts +35 -0
  340. package/src/time-series/resolve.ts +52 -11
  341. package/src/time-series/spec.ts +1 -1
  342. package/src/time-series/types.ts +5 -0
  343. package/src/types/broker.ts +56 -1
  344. package/src/types/config.ts +6 -0
  345. package/src/types/data-provider.ts +5 -1
  346. package/src/types/financials.ts +23 -0
  347. package/src/types/headless.ts +12 -2
  348. package/src/types/instrument.ts +4 -0
  349. package/src/types/plugin.ts +24 -6
  350. package/src/types/price-history.ts +6 -1
  351. package/src/types/ticker.ts +9 -0
  352. package/src/types/trading.ts +67 -0
  353. package/src/ui/host.tsx +4 -0
  354. package/src/utils/base-currency.ts +108 -0
  355. package/src/utils/currency-names.ts +128 -0
  356. package/src/utils/currency-pair.ts +2 -1
  357. package/src/utils/currency-units.ts +41 -1
  358. package/src/utils/dom-screenshot.ts +22 -9
  359. package/src/utils/exchange-notation.generated.ts +360 -0
  360. package/src/utils/exchanges.ts +15 -322
  361. package/src/utils/format.ts +31 -1
  362. package/src/utils/instrument-identity.ts +38 -1
  363. package/src/utils/options.ts +46 -1
  364. package/src/utils/price-history.ts +4 -0
  365. package/src/utils/reported-money.ts +8 -4
@@ -14,24 +14,26 @@ import {
14
14
  validateBrokerProfileValues,
15
15
  type BrokerProfileDraft,
16
16
  } from "../../../brokers/profile-form";
17
+ import { brokerDirectoryLogo } from "../../../brokers/logos";
17
18
  import { getSignedInBrokers, refreshSignedInBrokers, subscribeSignedInBrokers } from "../../../brokers/signed-in/catalog";
18
19
  import type { SignedInBroker } from "../../../brokers/signed-in/client";
19
20
  import { connectSignedInBrokerProfile } from "../../../brokers/signed-in/connect";
20
21
  import { SIGNED_IN_BROKER_TYPE } from "../../../brokers/signed-in/profile";
21
22
  import { runBrokerSignIn, type BrokerSignInOutcome } from "../../../brokers/signed-in/sign-in";
22
23
  import { BrokerConnectView, promptGloomSignIn, useBrokerSignInAttempt } from "../../../brokers/signed-in/sign-in-dialog";
23
- import { Button, ListView, useFieldRing } from "../../../components";
24
+ import { Button, confirmDialog, ListView, useFieldRing } from "../../../components";
24
25
  import { showCollectionInPortfolioPane } from "../../../components/command-bar/pane-actions";
25
26
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
26
27
 
27
28
  import { listCursorMove, type ListViewItem } from "../../../components/ui/list-view";
29
+ import { LogoMark } from "../../../components/ui/logo-mark";
28
30
  import { t, tf } from "../../../i18n";
29
31
  import { useShortcut, type KeyEventLike } from "../../../react/input";
30
32
  import { useAppDispatch, useAppGetState } from "../../../state/app/context";
31
33
  import { colors } from "../../../theme/colors";
32
34
  import type { BrokerAdapter } from "../../../types/broker";
33
35
  import type { BrokerInstanceConfig } from "../../../types/config";
34
- import { Box, ScrollBox, Text, type ScrollBoxRenderable } from "../../../ui";
36
+ import { Box, ScrollBox, Text, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
35
37
  import { useDialog } from "../../../ui/dialog";
36
38
  import { isPlainKey } from "../../../utils/keyboard";
37
39
  import { usePluginAppActions, usePluginBrokerActions } from "../../runtime";
@@ -191,7 +193,13 @@ export function useBrokerAddFlow({
191
193
  setBusy(t("Connecting broker…"));
192
194
  let instanceId: string;
193
195
  try {
194
- const instance = await createBrokerInstance(adapter.id, label, buildBrokerProfileConfig(adapter, draft.values));
196
+ const config = buildBrokerProfileConfig(adapter, draft.values);
197
+ const proposed = { id: "new-profile", brokerType: adapter.id, label, enabled: true, config };
198
+ if (adapter.getTradingCapabilities?.(proposed).enabled && !await confirmDialog(dialog, {
199
+ title: "Enable trading?", body: "Orders can commit real money. Start with a simulation account. Every order needs review and confirmation; LIVE orders also need a typed confirmation. You can turn trading off in Brokers at any time.",
200
+ confirmLabel: "Enable trading", confirmVariant: "danger",
201
+ })) { setBusy(null); return; }
202
+ const instance = await createBrokerInstance(adapter.id, label, config, { localOnly: draft.localOnly });
195
203
  instanceId = instance.id;
196
204
  } catch (error) {
197
205
  setBusy(null);
@@ -202,7 +210,7 @@ export function useBrokerAddFlow({
202
210
  } finally {
203
211
  committingRef.current = false;
204
212
  }
205
- }, [createBrokerInstance, setBusy, setMessage, syncNewProfile]);
213
+ }, [createBrokerInstance, dialog, setBusy, setMessage, syncNewProfile]);
206
214
 
207
215
  /** Shows one connect attempt in the pane until it ends, then says how. */
208
216
  const showAttempt = useCallback((flowId: number, write: boolean) => new Promise<BrokerSignInOutcome>((resolve) => {
@@ -360,6 +368,8 @@ export function BrokerAddFlowContent({
360
368
  scope: string;
361
369
  }) {
362
370
  const { flow } = addFlow;
371
+ const desktop = useUiCapabilities().nativePaneChrome === true;
372
+ const { getBrokerLogo } = usePluginBrokerActions();
363
373
  if (!flow) return <Box flexGrow={1} />;
364
374
  const { step } = flow;
365
375
 
@@ -369,7 +379,15 @@ export function BrokerAddFlowContent({
369
379
  focused={focused}
370
380
  label={addFlow.title}
371
381
  cursor={flow.cursor}
372
- items={flow.directory.map((entry) => ({ id: entry.key, label: entry.name, detail: brokerMethodSummary(entry) }))}
382
+ items={flow.directory.map((entry) => ({
383
+ id: entry.key,
384
+ label: entry.name,
385
+ // The desktop says how it connects under the name, beside its logo.
386
+ ...(desktop
387
+ ? { description: brokerMethodSummary(entry), leading: <LogoMark src={brokerDirectoryLogo(entry, getBrokerLogo)} name={entry.name} size="lead" /> }
388
+ : { detail: brokerMethodSummary(entry) }),
389
+ }))}
390
+ rowDescriptions={desktop}
373
391
  emptyMessage="No connectable brokers are installed."
374
392
  onCursorChange={addFlow.setCursor}
375
393
  onChoose={(index) => {
@@ -446,6 +464,7 @@ function ChoiceList({
446
464
  label,
447
465
  cursor,
448
466
  items,
467
+ rowDescriptions,
449
468
  emptyMessage,
450
469
  onCursorChange,
451
470
  onChoose,
@@ -454,10 +473,12 @@ function ChoiceList({
454
473
  label: string;
455
474
  cursor: number;
456
475
  items: ListViewItem[];
476
+ rowDescriptions?: boolean;
457
477
  emptyMessage?: string;
458
478
  onCursorChange: (index: number) => void;
459
479
  onChoose: (index: number) => void;
460
480
  }) {
481
+ const desktop = useUiCapabilities().nativePaneChrome === true;
461
482
  const selectedIndex = Math.min(Math.max(0, cursor), Math.max(0, items.length - 1));
462
483
  useShortcut((event: KeyEventLike) => {
463
484
  const move = listCursorMove(event, 10);
@@ -475,7 +496,8 @@ function ChoiceList({
475
496
  }, { enabled: focused });
476
497
 
477
498
  return (
478
- <Box flexDirection="column" flexGrow={1} paddingX={1}>
499
+ // The desktop keeps the pane's inset above the first row, as on its sides.
500
+ <Box flexDirection="column" flexGrow={1} paddingX={1} style={desktop ? { paddingTop: "var(--gloom-inset)" } : undefined}>
479
501
  <ListView
480
502
  items={items}
481
503
  selectedIndex={selectedIndex}
@@ -485,6 +507,11 @@ function ChoiceList({
485
507
  scrollable
486
508
  flexGrow={1}
487
509
  remoteLabel={label}
510
+ rowDescriptions={rowDescriptions}
511
+ // The desktop sets the rows on the pane at its inset, and the pointer
512
+ // moves the one highlight the keys move. A two-line row has its own
513
+ // room above and below, so those rows sit one under the next.
514
+ {...(desktop ? { surface: "plain" as const, bgColor: "transparent", selectOnHover: true, ...(rowDescriptions ? { rowGap: 0 } : {}) } : {})}
488
515
  />
489
516
  </Box>
490
517
  );
@@ -508,9 +535,9 @@ function DeviceProfileForm({
508
535
  }) {
509
536
  const { adapter, draft, activeKey } = step;
510
537
  const fields = getVisibleBrokerConfigFields(adapter, draft.values);
511
- const keys = useMemo<BrokerEditKey[]>(() => ["label", ...fields.map((field) => field.key)], [fields]);
538
+ const keys = useMemo<BrokerEditKey[]>(() => ["label", ...fields.map((field) => field.key), "localOnly"], [fields]);
512
539
  const selectKeys = useMemo<ReadonlySet<BrokerEditKey>>(
513
- () => new Set(fields.filter((field) => field.type === "select").map((field) => field.key)),
540
+ () => new Set(["localOnly", ...fields.filter((field) => field.type === "select").map((field) => field.key)]),
514
541
  [fields],
515
542
  );
516
543
  const { updateDraft, connectDevice, leave } = addFlow;
@@ -533,6 +560,10 @@ function DeviceProfileForm({
533
560
  }, [activeKey, keys, updateDraft]);
534
561
 
535
562
  const cycleSelect = useCallback((key: BrokerEditKey, direction: -1 | 1) => {
563
+ if (key === "localOnly") {
564
+ updateDraft((current) => ({ draft: { ...current.draft, localOnly: !current.draft.localOnly } }));
565
+ return;
566
+ }
536
567
  const options = fields.find((field) => field.key === key)?.options ?? [];
537
568
  if (options.length === 0) return;
538
569
  updateDraft((current) => {
@@ -570,6 +601,7 @@ function DeviceProfileForm({
570
601
  rowRef={rowRef}
571
602
  onActiveKeyChange={setActiveKey}
572
603
  onLabelChange={(label) => updateDraft((current) => ({ draft: { ...current.draft, label } }))}
604
+ onLocalOnlyChange={(localOnly) => updateDraft((current) => ({ draft: { ...current.draft, localOnly } }))}
573
605
  onValueChange={(key, value) => updateDraft((current) => ({
574
606
  draft: { ...current.draft, values: { ...current.draft.values, [key]: value } },
575
607
  }))}
@@ -17,7 +17,7 @@ import type { BrokerManagerMessage } from "./pane-actions";
17
17
  import { signInEndNote } from "./sign-in-end";
18
18
  import { stateColor } from "./table";
19
19
 
20
- export type BrokerEditKey = "label" | "enabled" | string;
20
+ export type BrokerEditKey = "label" | "enabled" | "localOnly" | string;
21
21
 
22
22
  type RowRef = (node: BoxRenderable | null) => void;
23
23
 
@@ -116,6 +116,7 @@ export function BrokerProfileForm({
116
116
  onActiveKeyChange,
117
117
  onLabelChange,
118
118
  onEnabledChange,
119
+ onLocalOnlyChange,
119
120
  onValueChange,
120
121
  onSubmit,
121
122
  onCancel,
@@ -139,6 +140,7 @@ export function BrokerProfileForm({
139
140
  onLabelChange: (label: string) => void;
140
141
  /** Editing only: a new profile starts enabled. */
141
142
  onEnabledChange?: (enabled: boolean) => void;
143
+ onLocalOnlyChange: (localOnly: boolean) => void;
142
144
  onValueChange: (key: string, value: string) => void;
143
145
  onSubmit: () => void;
144
146
  onCancel: () => void;
@@ -188,6 +190,25 @@ export function BrokerProfileForm({
188
190
  onSubmit={onSubmit}
189
191
  />
190
192
  ))}
193
+ <Box ref={rowRef("localOnly")} flexDirection="column" onMouseDown={() => onActiveKeyChange("localOnly")}>
194
+ <FieldLabel label={t("Account data")} active={activeKey === "localOnly"} />
195
+ <SegmentedControl
196
+ value={draft.localOnly ? "local" : "sync"}
197
+ focused={paneFocused && activeKey === "localOnly"}
198
+ options={[
199
+ { label: t("Sync with Gloom Cloud"), value: "sync" },
200
+ { label: t("Keep on this device"), value: "local" },
201
+ ]}
202
+ onChange={(value) => onLocalOnlyChange(value === "local")}
203
+ allowEditable
204
+ shortcutScope={scope}
205
+ />
206
+ {draft.localOnly ? (
207
+ <Text fg={colors.textMuted} wrapText>
208
+ {t("Daily roundups, the weekly brief, position alerts, the mobile app and Ask Gloom will go without this account.")}
209
+ </Text>
210
+ ) : null}
211
+ </Box>
191
212
  <Box flexDirection="row" gap={1}>
192
213
  <Button label={submitLabel} shortcut="Enter" variant="primary" onPress={onSubmit} disabled={!!busy} />
193
214
  <Button label={t("Cancel")} shortcut="Esc" variant="secondary" onPress={onCancel} disabled={!!busy} />
@@ -216,6 +237,7 @@ export function BrokerDetailContent({
216
237
  onActiveEditKeyChange,
217
238
  onDraftLabelChange,
218
239
  onDraftEnabledChange,
240
+ onDraftLocalOnlyChange,
219
241
  onDraftValueChange,
220
242
  onSaveEdit,
221
243
  onCancelEdit,
@@ -237,6 +259,7 @@ export function BrokerDetailContent({
237
259
  onActiveEditKeyChange: (key: BrokerEditKey) => void;
238
260
  onDraftLabelChange: (label: string) => void;
239
261
  onDraftEnabledChange: (enabled: boolean) => void;
262
+ onDraftLocalOnlyChange: (localOnly: boolean) => void;
240
263
  onDraftValueChange: (key: string, value: string) => void;
241
264
  onSaveEdit: () => void;
242
265
  onCancelEdit: () => void;
@@ -282,6 +305,7 @@ export function BrokerDetailContent({
282
305
  <Text fg={colors.warning} width={width} wrapText>{signInEndNote(row.signInEndsAt)}</Text>
283
306
  )}
284
307
  <Text fg={colors.textMuted}>{tf("Last sync {time}", { time: formatRelativeAge(row.lastSyncedAt) })}</Text>
308
+ {row.instance.localOnly ? <Text fg={colors.textMuted}>{t("Account data stays on this device, out of Gloom Cloud")}</Text> : null}
285
309
  <Text fg={colors.textMuted}>{tf("Status updated {time}", { time: formatRelativeAge(row.updatedAt) })}</Text>
286
310
  <Box height={1} />
287
311
 
@@ -303,6 +327,7 @@ export function BrokerDetailContent({
303
327
  onActiveKeyChange={onActiveEditKeyChange}
304
328
  onLabelChange={onDraftLabelChange}
305
329
  onEnabledChange={onDraftEnabledChange}
330
+ onLocalOnlyChange={onDraftLocalOnlyChange}
306
331
  onValueChange={onDraftValueChange}
307
332
  onSubmit={onSaveEdit}
308
333
  onCancel={onCancelEdit}
@@ -23,7 +23,7 @@ import {
23
23
  } from "../../../state/app/context";
24
24
  import type { BrokerAdapter } from "../../../types/broker";
25
25
  import type { PaneProps } from "../../../types/plugin";
26
- import { Box } from "../../../ui";
26
+ import { Box, useUiCapabilities } from "../../../ui";
27
27
  import { getCurrentPluginTarget } from "../../current-target";
28
28
  import { usePluginBrokerActions, usePluginPaneState } from "../../runtime";
29
29
  import type { PluginModule } from "../plugin-module";
@@ -50,7 +50,8 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
50
50
  const config = usePaneAppConfig();
51
51
  const paneId = usePaneInstanceId();
52
52
  const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
53
- const { getBrokerAdapter } = usePluginBrokerActions();
53
+ const { getBrokerAdapter, getBrokerLogo } = usePluginBrokerActions();
54
+ const desktop = useUiCapabilities().nativePaneChrome === true;
54
55
  // The profile is remembered by id so a reload lands on the same row.
55
56
  const [selectedId, setSelectedId] = usePluginPaneState<string | null>("selectedId", null);
56
57
  const [detailOpen, setDetailOpen] = usePluginPaneState<boolean>("detailOpen", false);
@@ -81,8 +82,8 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
81
82
  const signedInBrokers = useSyncExternalStore(subscribeSignedInBrokers, getSignedInBrokers, getSignedInBrokers);
82
83
  const { ends: signInEnds, reload: reloadSignInEnds } = useSignInEnds(config.brokerInstances);
83
84
  const rows = useMemo(
84
- () => buildBrokerProfileRows(config, adapters, brokerAccounts, signInEnds),
85
- [adapters, brokerAccounts, config, language, signInEnds, signedInBrokers, statusVersion],
85
+ () => buildBrokerProfileRows(config, adapters, brokerAccounts, signInEnds, getBrokerLogo),
86
+ [adapters, brokerAccounts, config, getBrokerLogo, language, signInEnds, signedInBrokers, statusVersion],
86
87
  );
87
88
  const selectedIndex = Math.max(0, rows.findIndex((row) => row.id === selectedId));
88
89
  const selectedRow = rows[selectedIndex] ?? null;
@@ -91,11 +92,11 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
91
92
  ? getVisibleBrokerConfigFields(selectedRow.adapter, editDraft.values)
92
93
  : [];
93
94
  const editKeys = useMemo<BrokerEditKey[]>(
94
- () => ["label", "enabled", ...editFields.map((field) => field.key)],
95
+ () => ["label", "enabled", ...editFields.map((field) => field.key), "localOnly"],
95
96
  [editFields],
96
97
  );
97
98
  const editSelectKeys = useMemo<ReadonlySet<BrokerEditKey>>(
98
- () => new Set(["enabled", ...editFields.filter((field) => field.type === "select").map((field) => field.key)]),
99
+ () => new Set(["enabled", "localOnly", ...editFields.filter((field) => field.type === "select").map((field) => field.key)]),
99
100
  [editFields],
100
101
  );
101
102
  const editScope = `broker-edit:${paneId}`;
@@ -200,8 +201,8 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
200
201
  });
201
202
 
202
203
  const cycleEditSelect = useCallback((key: BrokerEditKey, direction: -1 | 1) => {
203
- if (key === "enabled") {
204
- setEditDraft((current) => current ? { ...current, enabled: !current.enabled } : current);
204
+ if (key === "enabled" || key === "localOnly") {
205
+ setEditDraft((current) => current ? { ...current, [key]: !current[key] } : current);
205
206
  return;
206
207
  }
207
208
  const options = editFields.find((field) => field.key === key)?.options ?? [];
@@ -229,6 +230,11 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
229
230
  const tableWidth = Math.max(24, width);
230
231
  const columns = useMemo(() => buildBrokerColumns(tableWidth), [language, tableWidth]);
231
232
 
233
+ const renderCell = useCallback(
234
+ (row: BrokerProfileRow, column: BrokerColumn) => renderBrokerCell(row, column, desktop),
235
+ [desktop],
236
+ );
237
+
232
238
  const openSelectedDetail = useCallback((_index: number, row: BrokerProfileRow) => {
233
239
  setSelectedId(row.id);
234
240
  setEditDraft(null);
@@ -256,6 +262,10 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
256
262
  setEditDraft((current) => current ? { ...current, enabled } : current);
257
263
  }, []);
258
264
 
265
+ const updateDraftLocalOnly = useCallback((localOnly: boolean) => {
266
+ setEditDraft((current) => current ? { ...current, localOnly } : current);
267
+ }, []);
268
+
259
269
  const saveCurrentEdit = useCallback(() => {
260
270
  saveEdit().catch(() => {});
261
271
  }, [saveEdit]);
@@ -288,6 +298,7 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
288
298
  onActiveEditKeyChange={setActiveEditKey}
289
299
  onDraftLabelChange={updateDraftLabel}
290
300
  onDraftEnabledChange={updateDraftEnabled}
301
+ onDraftLocalOnlyChange={updateDraftLocalOnly}
291
302
  onDraftValueChange={updateDraftValue}
292
303
  onSaveEdit={saveCurrentEdit}
293
304
  onCancelEdit={cancelEdit}
@@ -319,7 +330,7 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
319
330
  sortColumnId={null}
320
331
  sortDirection="asc"
321
332
  getItemKey={(row) => row.id}
322
- renderCell={renderBrokerCell}
333
+ renderCell={renderCell}
323
334
  // The footer's a adds one.
324
335
  emptyContent={(
325
336
  <Box width="100%" paddingX={1} paddingY={1}>
@@ -1,3 +1,4 @@
1
+ import { brokerProfileLogo, type BrokerPluginLogo } from "../../../brokers/logos";
1
2
  import type { BrokerAdapter, BrokerConnectionStatus } from "../../../types/broker";
2
3
  import type { AppConfig, BrokerInstanceConfig } from "../../../types/config";
3
4
  import type { BrokerAccount } from "../../../types/trading";
@@ -27,6 +28,8 @@ export interface BrokerProfileRow {
27
28
  accountIds: string[];
28
29
  /** When the broker ends this profile's sign-in, once Gloom says it is time to connect again. */
29
30
  signInEndsAt: number | null;
31
+ /** The broker's logo for the desktop and the web (brokers/logos.ts); null draws its initial. */
32
+ logo: string | null;
30
33
  instance: BrokerInstanceConfig;
31
34
  adapter: BrokerAdapter | null;
32
35
  }
@@ -111,6 +114,7 @@ export function buildBrokerProfileRows(
111
114
  adapters: ReadonlyMap<string, BrokerAdapter | null>,
112
115
  brokerAccounts: Record<string, BrokerAccount[]>,
113
116
  signInEnds: SignInEnds = new Map(),
117
+ pluginLogo: BrokerPluginLogo = () => null,
114
118
  ): BrokerProfileRow[] {
115
119
  return config.brokerInstances.map((instance) => {
116
120
  const adapter = adapters.get(instance.brokerType) ?? null;
@@ -147,6 +151,7 @@ export function buildBrokerProfileRows(
147
151
  signInEndsAt: state.state === "error" || state.state === "disabled" || state.state === "unavailable"
148
152
  ? null
149
153
  : signInEnds.get(instance.id) ?? null,
154
+ logo: brokerProfileLogo(instance, adapter, pluginLogo),
150
155
  instance,
151
156
  adapter,
152
157
  };
@@ -93,9 +93,16 @@ export function useBrokerManagerActions({
93
93
  try {
94
94
  setBusy(t("Saving…"));
95
95
  const nextConfig = buildBrokerProfileConfig(selectedRow.adapter, editDraft.values, selectedRow.instance);
96
+ const wasEnabled = selectedRow.adapter.getTradingCapabilities?.(selectedRow.instance).enabled === true;
97
+ const willEnable = selectedRow.adapter.getTradingCapabilities?.({ ...selectedRow.instance, config: nextConfig }).enabled === true;
98
+ if (!wasEnabled && willEnable && !await confirmDialog(dialog, {
99
+ title: "Enable trading?", body: "Orders can commit real money. Start with a simulation account. Every order needs review and confirmation; LIVE orders also need a typed confirmation. You can turn trading off in this profile at any time.",
100
+ confirmLabel: "Enable trading", confirmVariant: "danger",
101
+ })) return;
96
102
  await updateBrokerInstance(selectedRow.id, nextConfig, {
97
103
  label,
98
104
  enabled: editDraft.enabled,
105
+ localOnly: editDraft.localOnly,
99
106
  replaceConfig: true,
100
107
  });
101
108
  setEditDraft(null);
@@ -105,7 +112,7 @@ export function useBrokerManagerActions({
105
112
  } finally {
106
113
  setBusy(null);
107
114
  }
108
- }, [editDraft, selectedRow, setEditDraft, updateBrokerInstance]);
115
+ }, [dialog, editDraft, selectedRow, setEditDraft, updateBrokerInstance]);
109
116
 
110
117
  const connectSelected = useCallback(async () => {
111
118
  if (!selectedRow) return;
@@ -1,4 +1,6 @@
1
+ import { createElement } from "react";
1
2
  import { TextAttributes } from "../../../ui";
3
+ import { LogoMark } from "../../../components/ui/logo-mark";
2
4
  import type { DataTableCell, DataTableColumn } from "../../../components";
3
5
  import { colors } from "../../../theme/colors";
4
6
  import { formatRelativeAge } from "../../../utils/datetime-format";
@@ -37,13 +39,15 @@ export function buildBrokerColumns(width: number): BrokerColumn[] {
37
39
  ];
38
40
  }
39
41
 
40
- export function renderBrokerCell(row: BrokerProfileRow, column: BrokerColumn): DataTableCell {
42
+ /** `desktop`: the profile leads with its broker's logo, which the terminal does not draw. */
43
+ export function renderBrokerCell(row: BrokerProfileRow, column: BrokerColumn, desktop = false): DataTableCell {
41
44
  switch (column.id) {
42
45
  case "profile":
43
46
  return {
44
47
  text: row.label,
45
48
  color: colors.text,
46
49
  attributes: TextAttributes.BOLD,
50
+ leading: desktop ? createElement(LogoMark, { src: row.logo, name: row.brokerName }) : undefined,
47
51
  };
48
52
  case "status":
49
53
  // A sign-in the broker is about to end replaces "Connected" until it is renewed.
@@ -0,0 +1,91 @@
1
+ import { useCallback, useImperativeHandle, useMemo, useRef, useState, type Ref } from "react";
2
+ import { Badge, Button, DialogFrame, DisclosureMarker, ListView, type SelectControl } from "../../../components";
3
+ import { listCursorMove } from "../../../components/ui/list-view";
4
+ import { useThemeColors } from "../../../theme/theme-context";
5
+ import { blendForContrast, higherContrast } from "../../../theme/color-utils";
6
+ import type { BrokerTradingMode } from "../../../types/trading";
7
+ import { Box, Text, TextAttributes, useUiCapabilities } from "../../../ui";
8
+ import { useDialog, useDialogKeyboard, type PromptContext } from "../../../ui/dialog";
9
+
10
+ export interface BrokerAccountChoice {
11
+ value: string;
12
+ label: string;
13
+ profileLabel?: string;
14
+ tradingMode?: BrokerTradingMode;
15
+ }
16
+
17
+ export interface BrokerAccountPickerProps {
18
+ accounts: readonly BrokerAccountChoice[];
19
+ value?: string;
20
+ onChange(value: string): void;
21
+ disabled?: boolean;
22
+ controlRef?: Ref<SelectControl>;
23
+ active?: boolean;
24
+ width?: number;
25
+ showMode?: boolean;
26
+ }
27
+
28
+ export function accountModeLabel(mode: BrokerTradingMode | undefined): string {
29
+ return mode === "simulation" ? "SIMULATION" : mode === "live" ? "LIVE" : "MODE UNKNOWN";
30
+ }
31
+
32
+ function BrokerAccountChoiceDialog({ accounts, value, resolve }: PromptContext<string> & Pick<BrokerAccountPickerProps, "accounts" | "value">) {
33
+ const colors = useThemeColors();
34
+ const [index, setIndex] = useState(() => accounts.findIndex((account) => account.value === value));
35
+ // Read the latest key movement, not the last rendered account, on Enter.
36
+ const cursor = useRef(index);
37
+ const selectIndex = (next: number) => { cursor.current = next; setIndex(next); };
38
+ useDialogKeyboard((event) => {
39
+ event.stopPropagation();
40
+ const move = listCursorMove(event, 5);
41
+ if (move) { event.preventDefault(); selectIndex(move(accounts, cursor.current)); }
42
+ else if (event.name === "return" || event.name === "enter") { event.preventDefault(); const selected = accounts[cursor.current]; if (selected) resolve(selected.value); }
43
+ else if (event.name === "escape") { event.preventDefault(); resolve(""); }
44
+ }, { allowEditable: true });
45
+ return <DialogFrame title="Choose an account" subtitle="Simulation accounts first. Choose before trading." footer="↑↓ choose · Enter select · Esc back" onClose={() => resolve("")}>
46
+ <Box width={64} flexDirection="column">
47
+ <ListView items={accounts.map((account) => ({ id: account.value, label: account.label, detail: accountModeLabel(account.tradingMode) }))}
48
+ selectedIndex={index} onSelect={selectIndex} onActivate={(item) => resolve(item.id)} selectOnHover
49
+ height={Math.min(Math.max(accounts.length, 1), 6) * 2} rowHeight={2} rowGap={0} scrollable={accounts.length > 6}
50
+ surface="framed" emptyMessage="No accounts available." remoteLabel="Broker accounts"
51
+ renderRow={(_item, state, rowIndex) => {
52
+ const account = accounts[rowIndex]!;
53
+ return <Box flexDirection="row" alignItems="flex-start" gap={1} width="100%" minWidth={0}>
54
+ <Box width={1}>{state.selected ? <DisclosureMarker expanded={false} color={colors.textBright} /> : null}</Box>
55
+ <Box flexDirection="column" flexGrow={1} minWidth={0}>
56
+ <Text fg={colors.text} attributes={state.selected ? TextAttributes.BOLD : undefined} truncate wrapMode="none">{account.label}</Text>
57
+ <Text fg={colors.textDim} truncate wrapMode="none">{account.profileLabel ?? "Broker account"}</Text>
58
+ </Box>
59
+ <Box flexShrink={0}><Badge label={accountModeLabel(account.tradingMode)} tone={account.tradingMode === "simulation" ? "accent" : "negative"} /></Box>
60
+ </Box>;
61
+ }} />
62
+ </Box>
63
+ </DialogFrame>;
64
+ }
65
+
66
+ /** Keeps native and terminal choices visually identical, including account mode badges. */
67
+ export function BrokerAccountPicker({ accounts, value, onChange, disabled = false, controlRef, active, width, showMode = false }: BrokerAccountPickerProps) {
68
+ const native = useUiCapabilities().nativePaneChrome;
69
+ const dialog = useDialog();
70
+ const colors = useThemeColors();
71
+ const choices = useMemo(() => [...accounts].sort((a, b) => Number(b.tradingMode === "simulation") - Number(a.tradingMode === "simulation")), [accounts]);
72
+ const current = accounts.find((account) => account.value === value);
73
+ const modeColor = blendForContrast(current?.tradingMode === "simulation" ? colors.positive : colors.negative, colors.bg, higherContrast("#000000", "#ffffff", colors.bg), 4.5);
74
+ const choose = useRef<(next: string | undefined) => void>(() => {});
75
+ choose.current = (next) => { if (!disabled && next && next !== value && accounts.some((account) => account.value === next)) onChange(next); };
76
+ const opened = useRef(false);
77
+ const open = useCallback(() => {
78
+ if (disabled || opened.current || !choices.length) return;
79
+ opened.current = true;
80
+ void dialog.prompt<string>({ closeOnClickOutside: true, style: { width: 72 }, content: (context) => <BrokerAccountChoiceDialog {...context} accounts={choices} value={value} /> })
81
+ .then((next) => choose.current(next)).catch(() => {}).finally(() => { opened.current = false; });
82
+ }, [choices, dialog, disabled, value]);
83
+ useImperativeHandle(controlRef, () => ({ open }), [open]);
84
+ return <Button label="Choose account" variant="quiet" flush active={active} disabled={disabled || !choices.length} width={width} onPress={open} stopPropagation>
85
+ <Box flexDirection="row" gap={1} flexGrow={1} minWidth={0} alignItems="center">
86
+ <Text fg={disabled ? colors.textMuted : colors.text} truncate wrapMode="none" flexShrink={1} minWidth={0}>{`${active && !native ? "> " : ""}${current?.label ?? "Choose an account"}`}</Text>
87
+ {showMode && current ? <Text fg={modeColor} attributes={TextAttributes.BOLD}>{current.tradingMode === "simulation" ? "SIM" : accountModeLabel(current.tradingMode)}</Text> : null}
88
+ <DisclosureMarker expanded color={colors.textDim} />
89
+ </Box>
90
+ </Button>;
91
+ }
@@ -0,0 +1,54 @@
1
+ import type { CompositeChartLevel } from "../../../components/chart/composite/levels";
2
+ import type { ThemeColors } from "../../../theme/colors";
3
+ import type { BrokerContractRef } from "../../../types/instrument";
4
+ import type { BrokerOrder, BrokerOrderRequest } from "../../../types/trading";
5
+ import { brokerOrderKey, brokerOrderStatus, canChangeBrokerOrder, ticketEditingPriceStep, withLatestOrder } from "./orders-data";
6
+ import { sameBrokerContract } from "../../../utils/instrument-identity";
7
+ import { isStrategyOrder } from "./strategy";
8
+
9
+ export function ticketOrderMatches(order: Pick<BrokerOrder, "accountId" | "brokerInstanceId" | "contract" | "legs">, contract: BrokerContractRef, profileId: string, accountId?: string): boolean {
10
+ if (!accountId || order.accountId !== accountId || order.brokerInstanceId && order.brokerInstanceId !== profileId
11
+ || order.contract.brokerInstanceId && order.contract.brokerInstanceId !== profileId || order.contract.brokerId !== contract.brokerId) return false;
12
+ const held = order.contract;
13
+ // A spread is its underlying's order: the stock's ticket and a spread ticket on it both list it.
14
+ const spreadTicket = contract.secType?.toUpperCase() === "BAG";
15
+ if (isStrategyOrder(order) || spreadTicket) {
16
+ return isStrategyOrder(order) && held.symbol.trim().toUpperCase() === contract.symbol.trim().toUpperCase()
17
+ && (spreadTicket || !contract.secType || ["STK", "ETF"].includes(contract.secType.toUpperCase()));
18
+ }
19
+ return sameBrokerContract(held, contract);
20
+ }
21
+
22
+ /** A read-only projection. No broker operation is attached to a chart level. */
23
+ export function ticketChartLevels({ draft, editing, orders, latestOrder, avgCost, position, colors, priceStep, priceDecimals }: {
24
+ draft?: BrokerOrderRequest; editing: boolean; orders: readonly BrokerOrder[]; latestOrder?: BrokerOrder;
25
+ avgCost?: number; position?: number; colors: ThemeColors; priceStep?: number; priceDecimals?: number;
26
+ }): CompositeChartLevel[] {
27
+ const levels: CompositeChartLevel[] = [];
28
+ // The broker's tick, or the ticket's editing step: a cent, a hundredth of a cent below a dollar. Accepted
29
+ // decimals are a limit, not a tick: five of them do not make $719.60745 an average anyone reads.
30
+ const tick = (value: number) => priceStep !== undefined && Number.isFinite(priceStep) && priceStep > 0 ? priceStep : ticketEditingPriceStep(value, priceDecimals);
31
+ // An order price shows every digit it was entered with; an average is arithmetic, so it rounds to the tick.
32
+ const add = (id: string, value: number | undefined, label: string, color: string, dashed = false, averaged = false) => {
33
+ if (value === undefined || !Number.isFinite(value) || value <= 0) return;
34
+ const minimumFractionDigits = Math.min(18, tick(value).toLocaleString("en-US", { useGrouping: false, maximumFractionDigits: 18 }).split(".")[1]?.length ?? 0);
35
+ const maximumFractionDigits = averaged ? minimumFractionDigits : Math.max(8, minimumFractionDigits);
36
+ const axisLabel = value.toLocaleString("en-US", { minimumFractionDigits, maximumFractionDigits });
37
+ levels.push({ id, value, axisLabel, label, color, editable: false, actionable: false, dashed });
38
+ };
39
+ if (draft && editing) {
40
+ const color = draft.action === "SELL" ? colors.negative : colors.positive;
41
+ if (["LMT", "STP LMT"].includes(draft.orderType)) add("ticket:limit", draft.limitPrice, "Draft limit", color, true);
42
+ if (["STP", "STP LMT"].includes(draft.orderType)) add("ticket:stop", draft.stopPrice, "Draft stop", color, true);
43
+ }
44
+ const projected = latestOrder ? withLatestOrder(orders, latestOrder) : orders;
45
+ for (const order of projected) {
46
+ if (!canChangeBrokerOrder(order)) continue;
47
+ const label = `${order.action === "BUY" ? "Buy" : "Sell"} ${order.remaining} ${({ "Partially filled": "partial", Replacing: "replacing" } as Record<string, string>)[brokerOrderStatus(order.status)] ?? "working"}`;
48
+ const color = order.action === "SELL" ? colors.negative : colors.positive;
49
+ add(`order:${brokerOrderKey(order)}`, order.limitPrice ?? order.stopPrice, label, color);
50
+ if (order.orderType === "STP LMT") add(`stop:${brokerOrderKey(order)}`, order.stopPrice, `${label} stop`, color);
51
+ }
52
+ if (position && avgCost !== undefined) add("position:average", avgCost, "Average cost", colors.textDim, false, true);
53
+ return levels;
54
+ }