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
@@ -0,0 +1,383 @@
1
+ import { useEffect, useRef, useSyncExternalStore } from "react";
2
+ import type { BrokerAdapter } from "../../../types/broker";
3
+ import type { BrokerInstanceConfig } from "../../../types/config";
4
+ import type { AppNotificationRequest } from "../../../types/plugin";
5
+ import type { BrokerOrder } from "../../../types/trading";
6
+ import { brokerOrderDone, brokerOrderStatus } from "./orders-data";
7
+ import { orderInstrumentLabel } from "./strategy";
8
+
9
+ /** Reconcile with open orders this often while the broker pushes changes. */
10
+ const LIVE_RECONCILE_MS = 60_000;
11
+ /** Without pushes: poll this often while an order works, and this often otherwise. */
12
+ const WORKING_POLL_MS = 5_000;
13
+ const IDLE_POLL_MS = 30_000;
14
+ /** After failed reads: wait this long, doubling each time, up to the cap. */
15
+ const FAILED_READ_MS = 5_000;
16
+ const FAILED_READ_MAX_MS = 60_000;
17
+ /** An order that ended stays in the feed this long, so a ticket and the drawer can show how. */
18
+ const DONE_RETENTION_MS = 10 * 60_000;
19
+ /** An order first seen already ended is news only if it ended this recently. */
20
+ const RECENT_END_MS = 2 * 60_000;
21
+ const SEEN_LIMIT = 1_000;
22
+
23
+ export interface BrokerOrderFeedState {
24
+ /** Working orders, and those that ended while watched, newest first. */
25
+ orders: readonly BrokerOrder[];
26
+ /** The first read of open orders has answered. */
27
+ loaded: boolean;
28
+ /** The broker pushes changes now; otherwise the feed polls. */
29
+ live: boolean;
30
+ /** The last read failed; `orders` are the last known. */
31
+ failed: boolean;
32
+ updatedAt: number;
33
+ /** Counts fills seen, so a view of executions knows when to read them again. */
34
+ fills: number;
35
+ }
36
+
37
+ const EMPTY: BrokerOrderFeedState = Object.freeze({ orders: Object.freeze([]) as readonly BrokerOrder[], loaded: false, live: false, failed: false, updatedAt: 0, fills: 0 });
38
+
39
+ /** One order through cancels and replacements: its profile, account and host handle. */
40
+ function orderFeedKey(instanceId: string, order: Pick<BrokerOrder, "accountId" | "orderId">): string {
41
+ return `${instanceId}:${order.accountId ?? ""}:${order.orderId}`;
42
+ }
43
+
44
+ interface Seen { filled: number; done: boolean }
45
+
46
+ interface OrderToast { title: string; type: AppNotificationRequest["type"] }
47
+
48
+ /**
49
+ * What an order's change says to someone not looking at it: a fill, partial
50
+ * fill, cancellation, rejection or expiry, once. A first sighting is news only
51
+ * when the order ended moments ago; an order already seen ended says nothing.
52
+ */
53
+ function orderChangeToast(before: Seen | undefined, order: BrokerOrder, now: number): OrderToast | null {
54
+ const status = brokerOrderStatus(order.status);
55
+ const done = brokerOrderDone(order.status);
56
+ if (before?.done) return null;
57
+ if (!before && (!done || now - order.updatedAt > RECENT_END_MS)) return null;
58
+ const filledMore = Number.isFinite(order.filled) && order.filled > (before?.filled ?? 0);
59
+ if (status === "Filled" || (filledMore && order.filled >= order.quantity)) return { title: "Order filled", type: "success" };
60
+ if (filledMore) return { title: "Partial fill", type: "success" };
61
+ if (status === "Cancelled") return { title: "Order cancelled", type: "info" };
62
+ if (status === "Rejected") return { title: "Order rejected", type: "error" };
63
+ if (status === "Expired") return { title: "Order expired", type: "info" };
64
+ return null;
65
+ }
66
+
67
+ export function orderToastBody(order: BrokerOrder): string {
68
+ const symbol = orderInstrumentLabel(order);
69
+ const qty = (value: number) => value.toLocaleString("en-US", { maximumFractionDigits: 4 });
70
+ const filled = order.filled > 0 && Number.isFinite(order.avgFillPrice) ? ` · ${qty(order.filled)} filled at ${order.avgFillPrice!.toLocaleString("en-US", { maximumFractionDigits: 4 })}` : "";
71
+ const limit = order.orderType === "MKT" ? "market" : `${order.orderType === "STP" ? "stop" : "limit"} ${(order.limitPrice ?? order.stopPrice)?.toLocaleString("en-US", { maximumFractionDigits: 4 }) ?? ""}`.trim();
72
+ return `${order.action === "BUY" ? "Buy" : "Sell"} ${qty(order.quantity)} ${symbol} ${limit}${filled}`;
73
+ }
74
+
75
+ /**
76
+ * One profile's orders: read from open orders when it connects, then kept
77
+ * current by the broker's order stream when it has one, with a slow
78
+ * reconcile, or by polling open orders when it has none or the stream is
79
+ * quiet. An order that leaves the open list is asked for by `getOrderStatus`,
80
+ * since absence alone proves neither a fill nor a cancel; one the broker
81
+ * cannot confirm stays, as UNKNOWN. A profile with trading off is read when
82
+ * it connects and on refresh, never polled.
83
+ */
84
+ class ProfileOrderFeed {
85
+ private orders = new Map<string, BrokerOrder>();
86
+ private state: BrokerOrderFeedState = EMPTY;
87
+ private holders = 0;
88
+ private connected = false;
89
+ private live = false;
90
+ private reading = false;
91
+ private rereadAfter = false;
92
+ private baselined = false;
93
+ private generation = 0;
94
+ private failures = 0;
95
+ private timer?: ReturnType<typeof setTimeout>;
96
+ private stopStatus?: () => void;
97
+ private stopStream?: () => void;
98
+
99
+ constructor(
100
+ readonly broker: BrokerAdapter,
101
+ private instance: BrokerInstanceConfig,
102
+ private readonly feed: BrokerOrderFeed,
103
+ ) {}
104
+
105
+ get snapshot(): BrokerOrderFeedState { return this.state; }
106
+
107
+ retain(instance: BrokerInstanceConfig): void {
108
+ this.instance = instance;
109
+ if (this.holders++ > 0) {
110
+ // Trading turned on (a new profile config) starts the polling it stopped.
111
+ if (this.timer === undefined && !this.reading) this.schedule();
112
+ return;
113
+ }
114
+ this.stopStatus = this.broker.subscribeStatus?.(this.instance, () => this.observe()) ?? undefined;
115
+ this.observe();
116
+ }
117
+
118
+ release(): boolean {
119
+ if (--this.holders > 0) return false;
120
+ this.generation++;
121
+ this.stopStatus?.(); this.stopStatus = undefined;
122
+ this.disconnect();
123
+ return true;
124
+ }
125
+
126
+ refresh(): void { if (this.connected) void this.read(); }
127
+
128
+ /** Stops reading and listening, whoever still holds it. */
129
+ stop(): void {
130
+ this.holders = 0;
131
+ this.generation++;
132
+ this.stopStatus?.(); this.stopStatus = undefined;
133
+ this.disconnect();
134
+ }
135
+
136
+ /**
137
+ * An order this app placed or changed, as its broker answered; recorded so
138
+ * the feed does not toast it again. A report the feed already has is the
139
+ * broker's own and stays.
140
+ */
141
+ note(order: BrokerOrder): void {
142
+ if (this.orders.has(orderFeedKey(this.instance.id, order))) this.feed.observeChange(this.instance.id, order, true);
143
+ else this.apply(order, "local");
144
+ }
145
+
146
+ private observe(): void {
147
+ let state: string | undefined;
148
+ try { state = this.broker.getStatus?.(this.instance).state; } catch { state = undefined; }
149
+ const connected = state === "connected";
150
+ if (connected === this.connected) return;
151
+ this.connected = connected;
152
+ if (!connected) { this.disconnect(); this.publish(); return; }
153
+ this.baselined = false;
154
+ this.failures = 0;
155
+ if (this.broker.subscribeOrders) {
156
+ try {
157
+ this.stopStream = this.broker.subscribeOrders(this.instance, {
158
+ order: (order) => this.apply(order, "push"),
159
+ live: (live) => {
160
+ if (live === this.live) return;
161
+ this.live = live;
162
+ // Coming back, the stream may have missed changes: read the open list once.
163
+ if (live) void this.read(); else this.schedule();
164
+ this.publish();
165
+ },
166
+ });
167
+ } catch { this.stopStream = undefined; }
168
+ }
169
+ void this.read();
170
+ }
171
+
172
+ private disconnect(): void {
173
+ if (this.timer !== undefined) clearTimeout(this.timer);
174
+ this.timer = undefined;
175
+ this.stopStream?.(); this.stopStream = undefined;
176
+ this.connected = false;
177
+ this.live = false;
178
+ }
179
+
180
+ private schedule(): void {
181
+ if (this.timer !== undefined) clearTimeout(this.timer);
182
+ this.timer = undefined;
183
+ if (!this.connected || this.holders === 0 || !this.tradingOn()) return;
184
+ // An unconfirmed order is asked about again at the idle pace, not every few seconds.
185
+ const working = [...this.orders.values()].some((order) => !brokerOrderDone(order.status) && brokerOrderStatus(order.status) !== "UNKNOWN");
186
+ const cadence = this.live ? LIVE_RECONCILE_MS : working ? WORKING_POLL_MS : IDLE_POLL_MS;
187
+ const backoff = this.failures ? Math.min(FAILED_READ_MAX_MS, FAILED_READ_MS * 2 ** (this.failures - 1)) : 0;
188
+ this.timer = setTimeout(() => { this.timer = undefined; void this.read(); }, Math.max(cadence, backoff));
189
+ }
190
+
191
+ private tradingOn(): boolean {
192
+ try { return this.broker.getTradingCapabilities?.(this.instance)?.enabled === true; } catch { return false; }
193
+ }
194
+
195
+ private async read(): Promise<void> {
196
+ if (!this.broker.listOpenOrders || !this.connected) return;
197
+ if (this.reading) { this.rereadAfter = true; return; }
198
+ this.reading = true;
199
+ const generation = this.generation;
200
+ try {
201
+ const open = await this.broker.listOpenOrders(this.instance);
202
+ if (generation !== this.generation || !this.connected) return;
203
+ const baseline = !this.baselined;
204
+ const listed = new Set<string>();
205
+ for (const order of open) {
206
+ listed.add(orderFeedKey(this.instance.id, order));
207
+ this.apply(order, baseline ? "baseline" : "poll", false);
208
+ }
209
+ // A working order missing from the list ended somehow; only the broker can say how.
210
+ const missing = [...this.orders].filter(([key, order]) => !listed.has(key) && !brokerOrderDone(order.status));
211
+ for (const [key, order] of missing) {
212
+ // A broker with no status read says nothing beyond its open list.
213
+ if (!this.broker.getOrderStatus) { this.orders.delete(key); continue; }
214
+ const status = await this.broker.getOrderStatus(this.instance, order.orderId).catch(() => undefined);
215
+ if (generation !== this.generation) return;
216
+ if (status) this.apply(status, baseline ? "baseline" : "poll", false);
217
+ // Null is the broker unable to confirm it, not an end: keep it, unconfirmed, at its last report's time,
218
+ // so the next read that lists it or a status that answers replaces it.
219
+ else if (status === null && brokerOrderStatus(order.status) !== "UNKNOWN") this.apply({ ...order, status: "UNKNOWN" }, baseline ? "baseline" : "poll", false);
220
+ }
221
+ this.failures = 0;
222
+ this.baselined = true;
223
+ this.prune();
224
+ this.publish({ failed: false, loaded: true });
225
+ } catch {
226
+ if (generation === this.generation) { this.failures++; this.publish({ failed: true, loaded: true }); }
227
+ } finally {
228
+ this.reading = false;
229
+ if (generation === this.generation) {
230
+ if (this.rereadAfter) { this.rereadAfter = false; void this.read(); } else this.schedule();
231
+ }
232
+ }
233
+ }
234
+
235
+ private apply(order: BrokerOrder, source: "baseline" | "poll" | "push" | "local", publish = true): void {
236
+ const key = orderFeedKey(this.instance.id, order);
237
+ const previous = this.orders.get(key);
238
+ // A read that started before a push can answer with the older state.
239
+ if (previous && source === "poll" && order.updatedAt < previous.updatedAt) return;
240
+ if (previous && brokerOrderDone(previous.status) && !brokerOrderDone(order.status)) return;
241
+ this.orders.set(key, order);
242
+ // What was seen before decides a toast, so a first read after launch, where every order is new, says nothing.
243
+ this.feed.observeChange(this.instance.id, order, source === "local");
244
+ if (publish) { this.prune(); this.publish(); }
245
+ // An order that started working needs the faster cadence without a stream.
246
+ if (!this.live && !brokerOrderDone(order.status) && (!previous || brokerOrderDone(previous.status))) this.schedule();
247
+ }
248
+
249
+ private prune(): void {
250
+ const now = Date.now();
251
+ for (const [key, order] of this.orders) {
252
+ if (brokerOrderDone(order.status) && now - order.updatedAt > DONE_RETENTION_MS) this.orders.delete(key);
253
+ }
254
+ }
255
+
256
+ private publish(changes: Partial<BrokerOrderFeedState> = {}): void {
257
+ const orders = [...this.orders.values()].sort((a, b) => b.updatedAt - a.updatedAt);
258
+ this.state = Object.freeze({ ...this.state, ...changes, orders, live: this.live, updatedAt: Date.now(), fills: this.feed.fillCount(this.instance.id) });
259
+ this.feed.changed();
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Every trading profile's orders, shared by the order drawer, the Orders pane,
265
+ * Overview and the ticket, and the source of the app's order toasts. Views
266
+ * hold a profile while mounted; the plugin holds every connected trading
267
+ * profile for as long as the app runs, so a fill toasts with no ticket open.
268
+ */
269
+ export class BrokerOrderFeed {
270
+ private readonly profiles = new Map<string, ProfileOrderFeed>();
271
+ private readonly listeners = new Set<() => void>();
272
+ private readonly seen = new Map<string, Seen>();
273
+ private readonly fills = new Map<string, number>();
274
+ private notifier?: (notification: AppNotificationRequest) => void;
275
+
276
+ /** Toasts go through the app's notifications once the plugin is set up. */
277
+ setNotifier(notify: ((notification: AppNotificationRequest) => void) | undefined): void { this.notifier = notify; }
278
+
279
+ hold(broker: BrokerAdapter, instance: BrokerInstanceConfig): () => void {
280
+ let profile = this.profiles.get(instance.id);
281
+ if (!profile || profile.broker !== broker) {
282
+ profile = new ProfileOrderFeed(broker, instance, this);
283
+ this.profiles.set(instance.id, profile);
284
+ }
285
+ const held = profile;
286
+ held.retain(instance);
287
+ let released = false;
288
+ return () => {
289
+ if (released) return;
290
+ released = true;
291
+ if (held.release() && this.profiles.get(instance.id) === held) this.profiles.delete(instance.id);
292
+ };
293
+ }
294
+
295
+ /** Lets go of every profile and forgets every order seen, as when the plugin is disposed. */
296
+ clear(): void {
297
+ for (const profile of this.profiles.values()) profile.stop();
298
+ this.profiles.clear();
299
+ this.seen.clear();
300
+ this.fills.clear();
301
+ this.changed();
302
+ }
303
+
304
+ snapshot(instanceId: string): BrokerOrderFeedState { return this.profiles.get(instanceId)?.snapshot ?? EMPTY; }
305
+
306
+ refresh(instanceId: string): void { this.profiles.get(instanceId)?.refresh(); }
307
+
308
+ /** A placement or change the ticket already announced: the feed records it without a toast. */
309
+ note(instanceId: string, order: BrokerOrder): void {
310
+ const profile = this.profiles.get(instanceId);
311
+ if (profile) profile.note(order);
312
+ else this.observeChange(instanceId, order, true);
313
+ }
314
+
315
+ subscribe = (listener: () => void): (() => void) => { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; };
316
+
317
+ /** @internal */
318
+ changed(): void { for (const listener of [...this.listeners]) listener(); }
319
+
320
+ /** @internal */
321
+ fillCount(instanceId: string): number { return this.fills.get(instanceId) ?? 0; }
322
+
323
+ /** @internal Records an order's state, and toasts its change unless `quiet`. */
324
+ observeChange(instanceId: string, order: BrokerOrder, quiet: boolean): void {
325
+ const key = orderFeedKey(instanceId, order);
326
+ const before = this.seen.get(key);
327
+ const filled = Number.isFinite(order.filled) ? Math.max(order.filled, before?.filled ?? 0) : before?.filled ?? 0;
328
+ this.seen.delete(key);
329
+ this.seen.set(key, { filled, done: (before?.done ?? false) || brokerOrderDone(order.status) });
330
+ if (this.seen.size > SEEN_LIMIT) this.seen.delete(this.seen.keys().next().value!);
331
+ if (filled > (before?.filled ?? 0)) this.fills.set(instanceId, (this.fills.get(instanceId) ?? 0) + 1);
332
+ if (quiet || !this.notifier) return;
333
+ const toast = orderChangeToast(before, order, Date.now());
334
+ if (toast) this.notifier({ ...toast, desktop: "when-inactive", body: orderToastBody(order) });
335
+ }
336
+ }
337
+
338
+ export const brokerOrderFeed = new BrokerOrderFeed();
339
+
340
+ /** A profile's orders from the shared feed, held while the view is mounted and connected. */
341
+ export function useBrokerOrderFeed(broker: BrokerAdapter | null | undefined, instance: BrokerInstanceConfig | null | undefined, active = true): BrokerOrderFeedState {
342
+ const instanceId = instance?.id;
343
+ useEffect(() => {
344
+ if (!active || !broker || !instance || !broker.listOpenOrders) return;
345
+ return brokerOrderFeed.hold(broker, instance);
346
+ }, [active, broker, instance]);
347
+ return useSyncExternalStore(brokerOrderFeed.subscribe, () => instanceId ? brokerOrderFeed.snapshot(instanceId) : EMPTY, () => EMPTY);
348
+ }
349
+
350
+ /**
351
+ * Keeps an order result in step with the feed. A report the feed held when
352
+ * the result was set (placed, changed, cancelled or read) is older news than
353
+ * the result unless it says the order ended; reports after it apply as they
354
+ * come. `apply` answers the result it leaves.
355
+ */
356
+ export function useFeedOrderResult(feed: BrokerOrderFeedState, instanceId: string, result: BrokerOrder | undefined, apply: (order: BrokerOrder) => BrokerOrder | undefined): void {
357
+ const track = useRef<{ result?: BrokerOrder; known?: BrokerOrder }>({});
358
+ useEffect(() => {
359
+ if (!result || result.orderId === 0) { track.current = {}; return; }
360
+ const key = orderFeedKey(instanceId, result);
361
+ const latest = feed.orders.find((order) => orderFeedKey(instanceId, order) === key);
362
+ if (result !== track.current.result) track.current = { result, known: latest && !brokerOrderDone(latest.status) ? latest : undefined };
363
+ if (!latest || latest === track.current.known) return;
364
+ track.current = { result: apply(latest), known: latest };
365
+ }, [feed.orders, result, instanceId, apply]);
366
+ }
367
+
368
+ /** Several profiles' orders, in the order given; for views that span every trading profile. */
369
+ export function useBrokerOrderFeeds(sources: readonly { broker: BrokerAdapter; instance: BrokerInstanceConfig }[], active = true): readonly BrokerOrderFeedState[] {
370
+ useEffect(() => {
371
+ if (!active) return;
372
+ const releases = sources.filter(({ broker }) => broker.listOpenOrders).map(({ broker, instance }) => brokerOrderFeed.hold(broker, instance));
373
+ return () => { for (const release of releases) release(); };
374
+ }, [active, sources]);
375
+ const cache = useRef<readonly BrokerOrderFeedState[]>([]);
376
+ const read = () => {
377
+ const next = sources.map(({ instance }) => brokerOrderFeed.snapshot(instance.id));
378
+ const previous = cache.current;
379
+ if (next.length !== previous.length || next.some((state, index) => state !== previous[index])) cache.current = next;
380
+ return cache.current;
381
+ };
382
+ return useSyncExternalStore(brokerOrderFeed.subscribe, read, read);
383
+ }
@@ -0,0 +1,133 @@
1
+ import type { BrokerAdapter } from "../../../types/broker";
2
+ import type { BrokerInstanceConfig } from "../../../types/config";
3
+ import type { BrokerExecution, BrokerOrder, BrokerOrderRequest } from "../../../types/trading";
4
+ import type { Quote } from "../../../types/financials";
5
+ import { formatNumber } from "../../../utils/format";
6
+
7
+ export interface BrokerOrdersSnapshot {
8
+ brokerInstanceId: string;
9
+ accountId: string;
10
+ orders: BrokerOrder[];
11
+ executions: BrokerExecution[];
12
+ executionKind: "fills" | "order-summaries" | "unspecified";
13
+ updatedAt: number;
14
+ errors: string[];
15
+ notes: string[];
16
+ }
17
+
18
+ export function brokerOrderKey(order: BrokerOrder): string {
19
+ return `${order.accountId ?? ""}:${order.brokerOrderId ?? order.orderId}`;
20
+ }
21
+
22
+ /**
23
+ * The ticket's latest word on an order merged into the broker's list, first. A replacement takes a new broker ID
24
+ * but keeps the order's handle: it stands for the listed original while that is being replaced, or is older.
25
+ */
26
+ export function withLatestOrder(orders: readonly BrokerOrder[], latest: BrokerOrder): BrokerOrder[] {
27
+ const key = brokerOrderKey(latest);
28
+ const listed = orders.find((order) => brokerOrderKey(order) === key)
29
+ ?? orders.find((order) => order.accountId === latest.accountId && order.orderId === latest.orderId);
30
+ if (!listed) return [latest, ...orders];
31
+ const replacing = brokerOrderKey(listed) !== key && /^PENDING[\s_-]?REPLACE$/i.test(listed.status);
32
+ return replacing || listed.updatedAt <= latest.updatedAt ? [latest, ...orders.filter((order) => order !== listed)] : [...orders];
33
+ }
34
+
35
+ export function brokerOrderStatus(status: string): string {
36
+ const key = status.toUpperCase().replace(/[\s_-]/g, "");
37
+ if (["SUBMITTED", "PRESUBMITTED", "OPEN", "NEW", "ACCEPTED", "WORKING", "REPLACED"].includes(key)) return "Working";
38
+ // Working at its old terms until the broker takes the change.
39
+ if (key === "PENDINGREPLACE") return "Replacing";
40
+ // Sent, not yet acknowledged by the venue (Aries PENDING_NEW).
41
+ if (key === "PENDINGNEW") return "Submitted";
42
+ if (["PARTIALLYFILLED", "PARTIALFILL", "PARTIAL"].includes(key)) return "Partially filled";
43
+ if (["PENDINGCANCEL", "CANCELPENDING"].includes(key)) return "Pending cancel";
44
+ if (["CANCELLED", "CANCELED"].includes(key)) return "Cancelled";
45
+ if (key === "FILLED") return "Filled";
46
+ if (["REJECTED", "INACTIVE"].includes(key)) return "Rejected";
47
+ if (key === "EXPIRED") return "Expired";
48
+ if (key === "UNKNOWN") return "UNKNOWN";
49
+ return status || "UNKNOWN";
50
+ }
51
+
52
+ const DONE = new Set(["Cancelled", "Filled", "Rejected", "Expired"]);
53
+ /** Whether a broker status, or its label, is final: cancelled, filled, rejected or expired. */
54
+ export function brokerOrderDone(status: string): boolean {
55
+ return DONE.has(brokerOrderStatus(status));
56
+ }
57
+
58
+ export function canChangeBrokerOrder(order: BrokerOrder): boolean {
59
+ const status = brokerOrderStatus(order.status);
60
+ return !DONE.has(status) && status !== "Pending cancel" && status !== "UNKNOWN" && order.remaining > 0;
61
+ }
62
+
63
+ export const formatOrderQuantity = (value: number) => Number.isFinite(value) ? formatNumber(value, Number.isInteger(value) ? 0 : 4) : "--";
64
+ export const formatOrderPrice = (value: number | undefined) => value != null && Number.isFinite(value) ? formatNumber(value, 2) : "--";
65
+ /** Execution sides as brokers write them: BUY, BOT or B. */
66
+ export const isBuySide = (side: string) => ["BUY", "BOT", "B"].includes(side.toUpperCase());
67
+
68
+ /**
69
+ * Both the pane and report use the same account projection, including partial
70
+ * failures. The pane passes the order feed's open orders, so only activity is
71
+ * read here; the report reads both.
72
+ */
73
+ export async function loadBrokerOrdersSnapshot(
74
+ broker: BrokerAdapter,
75
+ instance: BrokerInstanceConfig,
76
+ accountId: string,
77
+ signal?: AbortSignal,
78
+ openOrders?: { orders: readonly BrokerOrder[]; failed: boolean },
79
+ ): Promise<BrokerOrdersSnapshot> {
80
+ if (!accountId) throw new Error("Choose an account first.");
81
+ signal?.throwIfAborted();
82
+ const results = await Promise.allSettled([
83
+ openOrders ? (openOrders.failed ? Promise.reject(new Error("unavailable")) : Promise.resolve([...openOrders.orders]))
84
+ : Promise.resolve().then(() => broker.listOpenOrders ? broker.listOpenOrders(instance) : null),
85
+ Promise.resolve().then(() => broker.listExecutions ? broker.listExecutions(instance) : null),
86
+ ]);
87
+ signal?.throwIfAborted();
88
+ const errors: string[] = [];
89
+ const notes: string[] = [];
90
+ const [open, activity] = results;
91
+ if (open.status === "rejected") errors.push("Open orders are unavailable. Refresh to try again.");
92
+ else if (open.value === null) notes.push("This broker does not report open orders.");
93
+ if (activity.status === "rejected") errors.push("Recent activity is unavailable. Refresh to try again.");
94
+ else if (activity.value === null) notes.push("This broker does not report recent activity.");
95
+ const orders = open.status === "fulfilled" ? open.value ?? [] : [];
96
+ const executions = activity.status === "fulfilled" ? activity.value ?? [] : [];
97
+ const belongs = (row: BrokerOrder | BrokerExecution) => row.accountId === accountId
98
+ && (!row.brokerInstanceId || row.brokerInstanceId === instance.id)
99
+ && (!row.contract.brokerInstanceId || row.contract.brokerInstanceId === instance.id);
100
+ if ([...orders, ...executions].some((row) => !row.accountId)) notes.push("Rows without an account identity are omitted.");
101
+ return {
102
+ brokerInstanceId: instance.id,
103
+ accountId,
104
+ orders: orders.filter(belongs).sort((a, b) => b.updatedAt - a.updatedAt),
105
+ executions: executions.filter(belongs).sort((a, b) => b.time - a.time),
106
+ executionKind: broker.getTradingCapabilities?.(instance).executionKind ?? "unspecified",
107
+ updatedAt: Date.now(), errors, notes,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * The wheel, arrow and +/- step for a price when the broker declares no tick:
113
+ * a cent, or a hundredth of a cent under a dollar, never finer than the
114
+ * decimals the broker accepts. Five accepted decimals are not a $0.00001 tick,
115
+ * and stepping by one moved a $343 limit by nothing anyone could see.
116
+ */
117
+ /**
118
+ * The price an order's value is estimated at: its limit, or its stop for a stop order, else
119
+ * the side of the book a market order takes, the ask to buy and the bid to sell.
120
+ */
121
+ export function orderEstimatePrice(request: Pick<BrokerOrderRequest, "orderType" | "action" | "limitPrice" | "stopPrice">,
122
+ quote: Pick<Quote, "price" | "bid" | "ask"> | undefined): number | undefined {
123
+ if (["LMT", "STP LMT"].includes(request.orderType) && request.limitPrice !== undefined) return request.limitPrice;
124
+ if (request.orderType === "STP" && request.stopPrice !== undefined) return request.stopPrice;
125
+ const side = request.action === "SELL" ? quote?.bid : quote?.ask;
126
+ return side !== undefined && Number.isFinite(side) && side > 0 ? side : quote?.price;
127
+ }
128
+
129
+ export function ticketEditingPriceStep(price: number | undefined, decimals?: number): number {
130
+ const step = price !== undefined && Number.isFinite(price) && price > 0 && price < 1 ? 0.0001 : 0.01;
131
+ const finest = decimals !== undefined && Number.isInteger(decimals) && decimals >= 0 && decimals <= 18 ? 10 ** -decimals : 0;
132
+ return Math.max(step, finest);
133
+ }