gloomberb 0.18.1 → 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 (307) 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/types.ts +5 -0
  8. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  9. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  10. package/src/app/runtime/plugin-bindings.ts +6 -2
  11. package/src/brokers/logos.ts +66 -0
  12. package/src/brokers/profile-form.ts +3 -0
  13. package/src/brokers/remote-broker-adapter.ts +13 -3
  14. package/src/capabilities/index.ts +1 -1
  15. package/src/capabilities/service-capabilities.ts +7 -1
  16. package/src/cli/base-converter.ts +60 -18
  17. package/src/cli/commands/market.ts +281 -154
  18. package/src/cli/commands/news-search.ts +35 -8
  19. package/src/cli/commands/system.ts +35 -1
  20. package/src/cli/commands/ticker.ts +100 -50
  21. package/src/cli/currency-hints.ts +27 -0
  22. package/src/cli/fx-pair.ts +41 -4
  23. package/src/cli/glossary.ts +57 -4
  24. package/src/cli/helpers.ts +36 -3
  25. package/src/cli/index.ts +4 -1
  26. package/src/cli/known-gaps.ts +37 -1
  27. package/src/cli/listing-arg.ts +37 -2
  28. package/src/cli/pane-functions/catalog.ts +3 -1
  29. package/src/cli/pane-functions/discovery.ts +15 -1
  30. package/src/cli/pane-functions/dom.ts +9 -4
  31. package/src/cli/pane-functions/freshness.ts +4 -4
  32. package/src/cli/pane-functions/function-help.ts +27 -3
  33. package/src/cli/pane-functions/headless.ts +6 -5
  34. package/src/cli/pane-functions/index.ts +3 -2
  35. package/src/cli/pane-functions/lock-note.ts +7 -4
  36. package/src/cli/pane-functions/resolver.ts +2 -1
  37. package/src/cli/quote-answer.ts +179 -0
  38. package/src/cli/series-search.ts +2 -1
  39. package/src/cli/types.ts +4 -0
  40. package/src/components/chart/composite/composite-chart.tsx +11 -2
  41. package/src/components/chart/composite/format.ts +13 -1
  42. package/src/components/chart/composite/level-labels.ts +30 -0
  43. package/src/components/chart/composite/levels.ts +17 -6
  44. package/src/components/chart/composite/panel-surface.tsx +58 -8
  45. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  46. package/src/components/index.ts +2 -0
  47. package/src/components/layout/pane/content.tsx +16 -14
  48. package/src/components/layout/pane/footer/index.tsx +22 -3
  49. package/src/components/layout/pane/footer/model.ts +99 -4
  50. package/src/components/layout/pane/header-tabs.tsx +15 -1
  51. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  52. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  53. package/src/components/onboarding/wizard-model.ts +10 -3
  54. package/src/components/ui/button.tsx +11 -4
  55. package/src/components/ui/choice-dialog.tsx +8 -5
  56. package/src/components/ui/display.tsx +11 -6
  57. package/src/components/ui/fields.tsx +100 -20
  58. package/src/components/ui/icon.tsx +6 -1
  59. package/src/components/ui/list-row-lines.tsx +87 -0
  60. package/src/components/ui/list-view.tsx +14 -0
  61. package/src/components/ui/logo-mark.tsx +64 -0
  62. package/src/components/ui/number-step.ts +54 -0
  63. package/src/components/ui/select-button.tsx +5 -4
  64. package/src/components/ui/select-field.tsx +3 -3
  65. package/src/components/ui/status.tsx +17 -3
  66. package/src/components/ui/tabs.tsx +9 -1
  67. package/src/components/ui/toggle.tsx +17 -3
  68. package/src/core/state/app/instrument.ts +8 -3
  69. package/src/data/config/store/normalize.ts +4 -1
  70. package/src/data/plugin-cache.ts +32 -6
  71. package/src/docs/snapshot.json +5 -5
  72. package/src/i18n/es.ts +9 -3
  73. package/src/i18n/ja.ts +9 -3
  74. package/src/i18n/ko.ts +9 -3
  75. package/src/i18n/zh-cn.ts +10 -4
  76. package/src/i18n/zh-tw.ts +10 -4
  77. package/src/market-data/commodity-names.ts +80 -0
  78. package/src/market-data/coordinator/auxiliary.ts +3 -1
  79. package/src/market-data/coordinator/chart.ts +3 -1
  80. package/src/market-data/futures-board.ts +38 -1
  81. package/src/market-data/futures-contract.ts +91 -0
  82. package/src/market-data/fx-closes.ts +78 -1
  83. package/src/market-data/market/change-basis.ts +219 -0
  84. package/src/market-data/market/format.ts +28 -12
  85. package/src/market-data/market/freshness.ts +38 -0
  86. package/src/market-data/market/status.ts +55 -9
  87. package/src/market-data/market/year-to-date.ts +46 -0
  88. package/src/market-data/quotes/contributions.ts +12 -0
  89. package/src/market-data/quotes/delisting.ts +34 -0
  90. package/src/market-data/quotes/live-valuation.ts +23 -0
  91. package/src/market-data/quotes/resolution.ts +8 -2
  92. package/src/market-data/tax-equivalent.ts +10 -2
  93. package/src/notifications/app-notifier.ts +2 -2
  94. package/src/notifications/notification-log.ts +8 -6
  95. package/src/plugins/builtin/alerts/cli.ts +26 -18
  96. package/src/plugins/builtin/alerts/headless.ts +1 -1
  97. package/src/plugins/builtin/alerts/index.tsx +13 -4
  98. package/src/plugins/builtin/attention/trending.tsx +8 -6
  99. package/src/plugins/builtin/backtest/client.ts +5 -2
  100. package/src/plugins/builtin/backtest/window.ts +16 -3
  101. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  102. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  103. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  104. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  105. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  106. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  107. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  108. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  109. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  110. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  111. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  112. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  113. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  114. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  115. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  116. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  117. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  118. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  119. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  120. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  121. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  122. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  123. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  124. package/src/plugins/builtin/buildout/format.ts +1 -0
  125. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  126. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  127. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  128. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  129. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  130. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  131. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  132. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  133. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  134. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  135. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  136. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  137. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  138. package/src/plugins/builtin/chat/index.tsx +1 -1
  139. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  140. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  141. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  142. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  143. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  144. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  145. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  146. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  147. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  148. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  149. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  150. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  151. package/src/plugins/builtin/composite-plugins.ts +3 -2
  152. package/src/plugins/builtin/cot/headless.ts +1 -1
  153. package/src/plugins/builtin/cot/index.tsx +1 -1
  154. package/src/plugins/builtin/cot/model.ts +35 -12
  155. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  156. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  157. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  158. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  159. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  160. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  161. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  162. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  163. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  164. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  165. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  166. package/src/plugins/builtin/econ/index.tsx +31 -4
  167. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  168. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  169. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  170. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  171. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  172. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  173. package/src/plugins/builtin/exposure/client.ts +4 -1
  174. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  175. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  176. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  177. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  178. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  179. package/src/plugins/builtin/futures/table.tsx +6 -3
  180. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  181. package/src/plugins/builtin/insider/base-values.ts +72 -0
  182. package/src/plugins/builtin/insider/headless.ts +29 -4
  183. package/src/plugins/builtin/jobs/headless.ts +2 -2
  184. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  185. package/src/plugins/builtin/market-halts/model.ts +31 -6
  186. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  187. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  188. package/src/plugins/builtin/members/pane.tsx +8 -3
  189. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  190. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  191. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  192. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  193. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  194. package/src/plugins/builtin/options/footer.ts +25 -20
  195. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  196. package/src/plugins/builtin/options/view.tsx +77 -22
  197. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  198. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  199. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  200. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  201. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  202. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  203. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  204. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  205. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  206. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  207. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  208. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  209. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  210. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  211. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  212. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  213. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  214. package/src/plugins/builtin/scanner/feed.ts +6 -1
  215. package/src/plugins/builtin/scanner/headless.ts +1 -1
  216. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  217. package/src/plugins/builtin/series-inventory.ts +1 -0
  218. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  219. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  220. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  221. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  222. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  223. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  224. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  225. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  226. package/src/plugins/builtin/themes/headless.ts +8 -2
  227. package/src/plugins/builtin/themes/model.ts +43 -10
  228. package/src/plugins/builtin/themes/pane.tsx +8 -6
  229. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  230. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  231. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  232. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  233. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  234. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  235. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  236. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  237. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  238. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  239. package/src/plugins/builtin-editorial.ts +1 -1
  240. package/src/plugins/compat.ts +3 -0
  241. package/src/plugins/external-runtime.ts +5 -0
  242. package/src/plugins/loader.ts +6 -0
  243. package/src/plugins/plugin-icon.ts +61 -0
  244. package/src/plugins/registry/host-actions.ts +2 -1
  245. package/src/plugins/registry/index.ts +9 -1
  246. package/src/plugins/runtime/context.tsx +5 -0
  247. package/src/plugins/runtime/index.tsx +3 -0
  248. package/src/portfolio/column-values.ts +13 -4
  249. package/src/portfolio/cost-fx.ts +5 -28
  250. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  251. package/src/public/broker.ts +8 -0
  252. package/src/public/market-data.ts +5 -0
  253. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  254. package/src/renderers/dom/desktop/controls.tsx +39 -7
  255. package/src/renderers/dom/desktop/icons.tsx +14 -0
  256. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  257. package/src/renderers/dom/desktop/menu.tsx +10 -1
  258. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  259. package/src/renderers/dom/host/style.ts +6 -1
  260. package/src/renderers/dom/host/tabs.tsx +6 -0
  261. package/src/renderers/dom/styles.css +12 -5
  262. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  263. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  264. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  265. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  266. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  267. package/src/series/validate.ts +2 -2
  268. package/src/sources/gloomberb-cloud/index.ts +50 -17
  269. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  270. package/src/sources/history-result.ts +3 -0
  271. package/src/sources/provider-errors.ts +3 -1
  272. package/src/sources/provider-router/history.ts +49 -6
  273. package/src/sources/provider-router/index.ts +2 -1
  274. package/src/state/app/activity.ts +13 -0
  275. package/src/sync/controller.ts +4 -0
  276. package/src/sync/core-contributors.ts +62 -11
  277. package/src/sync/local-only.ts +121 -0
  278. package/src/sync/react.ts +6 -0
  279. package/src/sync/types.ts +3 -0
  280. package/src/tickers/listing-fate.ts +27 -8
  281. package/src/tickers/search/index.ts +24 -3
  282. package/src/time-series/price-comparison.ts +35 -0
  283. package/src/time-series/resolve.ts +52 -11
  284. package/src/time-series/spec.ts +1 -1
  285. package/src/time-series/types.ts +5 -0
  286. package/src/types/broker.ts +56 -1
  287. package/src/types/config.ts +6 -0
  288. package/src/types/data-provider.ts +5 -1
  289. package/src/types/financials.ts +23 -0
  290. package/src/types/headless.ts +12 -2
  291. package/src/types/instrument.ts +4 -0
  292. package/src/types/plugin.ts +24 -6
  293. package/src/types/price-history.ts +6 -1
  294. package/src/types/trading.ts +67 -0
  295. package/src/ui/host.tsx +4 -0
  296. package/src/utils/base-currency.ts +108 -0
  297. package/src/utils/currency-names.ts +128 -0
  298. package/src/utils/currency-pair.ts +2 -1
  299. package/src/utils/currency-units.ts +32 -1
  300. package/src/utils/dom-screenshot.ts +22 -9
  301. package/src/utils/exchange-notation.generated.ts +360 -0
  302. package/src/utils/exchanges.ts +15 -322
  303. package/src/utils/format.ts +31 -1
  304. package/src/utils/instrument-identity.ts +38 -1
  305. package/src/utils/options.ts +46 -1
  306. package/src/utils/price-history.ts +4 -0
  307. package/src/utils/reported-money.ts +8 -4
@@ -0,0 +1,153 @@
1
+ import type { Quote } from "../../../types/financials";
2
+ import type { BrokerContractRef } from "../../../types/instrument";
3
+ import type { BrokerOrderLeg, BrokerOrderRequest } from "../../../types/trading";
4
+ import { chainOptionContract, formatExpDate, optionContractIdentity } from "../../../utils/options";
5
+
6
+ type Side = "BUY" | "SELL";
7
+
8
+ /** A strategy leg as a builder holds it: an OCC contract, signed contracts, and a premium per share to tell debit from credit. */
9
+ export interface StrategyLegInput { contractSymbol: string; quantity: number; price?: number }
10
+
11
+ /** A leg a ticket opens with, before it is bound to a broker profile. */
12
+ export interface StrategyIntentLeg { contractSymbol: string; action: Side; ratio: number }
13
+
14
+ export interface StrategyIntent { action: Side; quantity: number; legs: StrategyIntentLeg[] }
15
+
16
+ /** The most legs any broker's multi-leg order takes. */
17
+ export const MAX_STRATEGY_LEGS = 4;
18
+
19
+ const gcd = (a: number, b: number): number => b ? gcd(b, a % b) : a;
20
+ const flip = (action: Side): Side => action === "BUY" ? "SELL" : "BUY";
21
+
22
+ /**
23
+ * A builder's legs as one order: each leg keeps its side, the common factor of
24
+ * the contract counts becomes the spread quantity, and the entry premiums say
25
+ * whether opening it pays a debit (BUY) or receives a credit (SELL). Null when
26
+ * a quantity is not a whole number of contracts.
27
+ */
28
+ export function strategyFromLegs(inputs: readonly StrategyLegInput[]): StrategyIntent | null {
29
+ const legs = inputs.filter((leg) => leg.quantity !== 0);
30
+ if (!legs.length || legs.some((leg) => !Number.isSafeInteger(leg.quantity) || !leg.contractSymbol.trim())) return null;
31
+ const quantity = legs.map((leg) => Math.abs(leg.quantity)).reduce(gcd);
32
+ const net = legs.every((leg) => Number.isFinite(leg.price)) ? legs.reduce((sum, leg) => sum + leg.quantity * leg.price!, 0) : 0;
33
+ return {
34
+ action: net < 0 ? "SELL" : "BUY", quantity,
35
+ legs: legs.map((leg) => ({ contractSymbol: leg.contractSymbol.trim().toUpperCase(), action: leg.quantity > 0 ? "BUY" : "SELL", ratio: Math.abs(leg.quantity) / quantity })),
36
+ };
37
+ }
38
+
39
+ /** The opposite trade: every leg the other way, and a debit becomes a credit. */
40
+ export function reverseStrategy<T extends { action: Side; legs?: readonly { action: Side }[] }>(order: T): T {
41
+ return { ...order, action: flip(order.action), ...(order.legs ? { legs: order.legs.map((leg) => ({ ...leg, action: flip(leg.action) })) } : {}) };
42
+ }
43
+
44
+ /** The order's contract: the underlying's symbol on the same profile, as a combo. */
45
+ export function strategyContract(underlying: BrokerContractRef, currency?: string): BrokerContractRef {
46
+ return { brokerId: underlying.brokerId, ...(underlying.brokerInstanceId ? { brokerInstanceId: underlying.brokerInstanceId } : {}),
47
+ symbol: underlying.symbol.trim().toUpperCase(), secType: "BAG", exchange: "SMART", currency: currency || underlying.currency || "USD", multiplier: "100" };
48
+ }
49
+
50
+ /** Binds a strategy's legs to the underlying's broker profile; null when one is not a listed contract of that underlying. */
51
+ export function strategyOrderLegs(legs: readonly StrategyIntentLeg[], underlying: BrokerContractRef, currency?: string): BrokerOrderLeg[] | null {
52
+ const bound = legs.map((leg) => {
53
+ const contract = chainOptionContract(leg.contractSymbol, underlying, currency || underlying.currency || "USD");
54
+ return contract && contract.symbol === underlying.symbol.trim().toUpperCase() ? { contract, action: leg.action, ratio: leg.ratio } : null;
55
+ });
56
+ return bound.every((leg): leg is BrokerOrderLeg => leg !== null) ? bound : null;
57
+ }
58
+
59
+ export function isStrategyOrder(order: { contract: BrokerContractRef; legs?: readonly unknown[] }): boolean {
60
+ return !!order.legs?.length || order.contract.secType?.toUpperCase() === "BAG";
61
+ }
62
+
63
+ /**
64
+ * The spread's quote from its legs' quotes, as the debit side reads it: bid is
65
+ * what selling one spread receives at once, ask what buying one pays. A credit
66
+ * order sells that debit spread, so it reads the bid, as a sell does.
67
+ */
68
+ export function strategyQuote(request: Pick<BrokerOrderRequest, "action" | "legs" | "contract">, quotes: readonly (Quote | undefined)[]): Quote | null {
69
+ const legs = request.legs ?? [];
70
+ if (!legs.length || quotes.length !== legs.length) return null;
71
+ let bid = 0, ask = 0;
72
+ for (const [index, leg] of legs.entries()) {
73
+ const quote = quotes[index];
74
+ if (!quote || !Number.isFinite(quote.bid) || !Number.isFinite(quote.ask)) return null;
75
+ // The debit side buys what a BUY order buys, and buys back what a SELL order sells.
76
+ const buys = (leg.action === "BUY") === (request.action === "BUY");
77
+ bid += buys ? leg.ratio * quote.bid! : -leg.ratio * quote.ask!;
78
+ ask += buys ? leg.ratio * quote.ask! : -leg.ratio * quote.bid!;
79
+ }
80
+ const round = (value: number) => Math.round(value * 1e6) / 1e6;
81
+ const present = quotes as Quote[];
82
+ return {
83
+ symbol: request.contract.symbol, name: strategyName(legs), price: round((bid + ask) / 2), bid: round(bid), ask: round(ask),
84
+ change: NaN, changePercent: NaN, currency: request.contract.currency ?? present[0]!.currency,
85
+ lastUpdated: Math.min(...present.map((quote) => quote.lastUpdated)),
86
+ dataSource: present.every((quote) => quote.dataSource === "live") ? "live" : "delayed",
87
+ stale: present.some((quote) => quote.stale === true), marketState: present[0]!.marketState,
88
+ };
89
+ }
90
+
91
+ interface ParsedLeg { underlying: string; expiry: string; right: "C" | "P"; strike: number; action: Side; ratio: number }
92
+
93
+ function parseLeg(leg: { contract?: BrokerContractRef; contractSymbol?: string; action: Side; ratio: number }): ParsedLeg | null {
94
+ const identity = leg.contract ? optionContractIdentity(leg.contract) : optionContractIdentity({ brokerId: "", symbol: leg.contractSymbol ?? "", secType: "OPT" });
95
+ const [underlying, expiry, right, strike] = identity?.split("|") ?? [];
96
+ return underlying && expiry && (right === "C" || right === "P") ? { underlying, expiry, right, strike: Number(strike), action: leg.action, ratio: leg.ratio } : null;
97
+ }
98
+
99
+ type NamedLeg = { contract?: BrokerContractRef; contractSymbol?: string; action: Side; ratio: number };
100
+
101
+ /** "AAPL call vertical", "SPY iron condor", or "AAPL 3-leg spread" when the shape has no common name. */
102
+ export function strategyName(legs: readonly NamedLeg[]): string {
103
+ const underlying = parseLeg(legs[0] ?? { action: "BUY", ratio: 1 })?.underlying;
104
+ return [underlying, strategyShape(legs)].filter(Boolean).join(" ");
105
+ }
106
+
107
+ /** The shape alone, for a list already on its underlying: "call vertical". */
108
+ export function strategyShape(legs: readonly NamedLeg[]): string {
109
+ const parsed = legs.map(parseLeg);
110
+ const named = (shape: string) => shape;
111
+ if (parsed.some((leg) => !leg) || new Set(parsed.map((leg) => leg!.underlying)).size > 1) return named(`${legs.length}-leg spread`);
112
+ const all = parsed as ParsedLeg[];
113
+ const same = <K extends keyof ParsedLeg>(key: K, set = all) => new Set(set.map((leg) => leg[key])).size === 1;
114
+ const right = (leg: ParsedLeg) => leg.right === "C" ? "call" : "put";
115
+ const [a, b] = all;
116
+ if (all.length === 2 && a && b) {
117
+ const opposite = a.action !== b.action;
118
+ if (same("right") && opposite && a.ratio !== b.ratio && same("expiry")) return named(`${right(a)} ratio spread`);
119
+ if (same("right") && opposite && same("expiry")) return named(`${right(a)} vertical`);
120
+ if (same("right") && opposite && same("strike")) return named(`${right(a)} calendar`);
121
+ if (same("right") && opposite) return named(`${right(a)} diagonal`);
122
+ if (!same("right") && !opposite && same("expiry")) return named(same("strike") ? "straddle" : "strangle");
123
+ }
124
+ if (all.length === 3 && same("right") && same("expiry")) {
125
+ const [low, middle, high] = all.toSorted((x, y) => x.strike - y.strike);
126
+ if (low && middle && high && low.action === high.action && middle.action !== low.action && middle.ratio === low.ratio + high.ratio
127
+ && Math.abs(middle.strike - low.strike - (high.strike - middle.strike)) < 1e-9) return named(`${right(low)} butterfly`);
128
+ }
129
+ if (all.length === 4 && same("expiry") && all.filter((leg) => leg.right === "C").length === 2) {
130
+ const calls = all.filter((leg) => leg.right === "C").toSorted((x, y) => x.strike - y.strike);
131
+ const puts = all.filter((leg) => leg.right === "P").toSorted((x, y) => x.strike - y.strike);
132
+ // Short the inner strikes, long the wings (or the reverse): the two verticals of an iron condor.
133
+ if (calls[0]!.action !== calls[1]!.action && puts[0]!.action !== puts[1]!.action && calls[0]!.action === puts[1]!.action
134
+ && puts[1]!.strike <= calls[0]!.strike) return named(puts[1]!.strike === calls[0]!.strike ? "iron butterfly" : "iron condor");
135
+ }
136
+ return named(`${all.length}-leg spread`);
137
+ }
138
+
139
+ /** "Buy 1 · Oct 26 '26 345 Call": a leg's side, contracts per spread times the spread count, and its contract. */
140
+ export function strategyLegLabel(leg: BrokerOrderLeg, spreads = 1): { side: string; contracts: string; contract: string } {
141
+ const parsed = parseLeg(leg);
142
+ const contracts = Number.isFinite(spreads) && spreads > 0 ? leg.ratio * spreads : leg.ratio;
143
+ if (!parsed) return { side: leg.action === "BUY" ? "Buy" : "Sell", contracts: String(contracts), contract: leg.contract.localSymbol || leg.contract.symbol };
144
+ const expiry = formatExpDate(Date.UTC(Number(parsed.expiry.slice(0, 4)), Number(parsed.expiry.slice(4, 6)) - 1, Number(parsed.expiry.slice(6, 8))) / 1000);
145
+ const strike = parsed.strike.toLocaleString("en-US", { maximumFractionDigits: 3 });
146
+ return { side: leg.action === "BUY" ? "Buy" : "Sell", contracts: String(contracts), contract: `${expiry} ${strike} ${parsed.right === "C" ? "Call" : "Put"}` };
147
+ }
148
+
149
+ /** An order's instrument in a row or toast: the strategy's name for a spread, else the contract's symbol. */
150
+ export function orderInstrumentLabel(order: { contract: BrokerContractRef; legs?: readonly BrokerOrderLeg[] }): string {
151
+ if (order.legs?.length) return strategyName(order.legs);
152
+ return order.contract.secType?.toUpperCase() === "BAG" ? `${order.contract.symbol} spread` : order.contract.localSymbol || order.contract.symbol;
153
+ }
@@ -0,0 +1,117 @@
1
+ import { FigureText } from "../../../components/ui/figure";
2
+ import type { ReactNode } from "react";
3
+ import { KeyValueRow, Notice, RatioBar, SectionHeading } from "../../../components";
4
+ import { useThemeColors } from "../../../theme/theme-context";
5
+ import { Box, Text, TextAttributes, useUiCapabilities } from "../../../ui";
6
+ import type { TicketViewModel } from "./ticket";
7
+ import { strategyLegLabel } from "./strategy";
8
+ import { orderEstimatePrice } from "./orders-data";
9
+
10
+ export const ticketNumber = (value: number | undefined) => value === undefined || !Number.isFinite(value) ? "Unavailable" : value.toLocaleString("en-US", { maximumFractionDigits: 4 });
11
+ export const ticketMoney = (value: number | undefined, currency = "USD") => value === undefined || !Number.isFinite(value) ? "Not reported" : new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 2 }).format(value);
12
+ export const ticketTypeName: Record<string, string> = { MKT: "Market", LMT: "Limit", STP: "Stop", "STP LMT": "Stop limit" };
13
+ export const ticketTifName: Record<string, string> = { DAY: "Day", GTC: "Good til cancelled", IOC: "Immediate or cancel", FOK: "Fill or kill", EXTENDED_HOURS: "Extended hours", AT_THE_OPENING: "At the opening", AT_THE_CLOSE: "At the close" };
14
+ /** `selectable`: a review or receipt whose figures someone may copy by hand. */
15
+ export function TicketSection({ children, width, title, selectable = false }: { children: ReactNode; width?: number; title?: string; selectable?: boolean }) {
16
+ const native = useUiCapabilities().nativePaneChrome;
17
+ return <Box width={width} flexDirection="column" flexShrink={0} gap={native ? 0.35 : 0} data-gloom-selectable={selectable ? "true" : undefined}>
18
+ {title ? <SectionHeading title={title} /> : null}{children}
19
+ </Box>;
20
+ }
21
+ export function TicketReceiptRow({ label, value, color, width, emphasis = false }: { label: string; value: string; width: number; color?: string; emphasis?: boolean }) {
22
+ return <KeyValueRow label={label} value={value} width={width} labelWidth={Math.min(24, Math.floor(width * 0.48))} align="right" color={color} emphasis={emphasis} />;
23
+ }
24
+ export function TicketOrderHeadline({ model: m, large = false }: { model: TicketViewModel; large?: boolean }) {
25
+ const c = useThemeColors(); const draft = m.draft; const side = draft?.action === "SELL" ? "Sell" : "Buy";
26
+ return <Box flexDirection="row" alignItems="center" gap={1} flexWrap="wrap">
27
+ <FigureText part={large ? "value" : "sub"} fg={side === "Buy" ? c.positive : c.negative} style={large ? undefined : { fontSize: "var(--gloom-type-heading)", fontWeight: "var(--gloom-weight-bold)" }}>{side}</FigureText>
28
+ <FigureText part={large ? "value" : "sub"} style={large ? undefined : { fontSize: "var(--gloom-type-heading)", fontWeight: "var(--gloom-weight-bold)" }}>{`${ticketNumber(draft?.quantity)} ${m.symbol}`}</FigureText>
29
+ </Box>;
30
+ }
31
+ /** A spread's legs as they fill: side, contracts across all spreads, contract, and each leg's mid at this broker. */
32
+ /** `inset`: in the order form, the legs line up with the fields' labels. */
33
+ export function TicketLegs({ model: m, width, inset = false }: { model: TicketViewModel; width: number; inset?: boolean }) {
34
+ const c = useThemeColors();
35
+ const native = useUiCapabilities().nativePaneChrome;
36
+ const draft = m.draft;
37
+ if (!draft?.legs?.length) return null;
38
+ const pad = inset && !native ? 2 : 0;
39
+ return <Box flexDirection="column" width={width} paddingLeft={pad} data-gloom-selectable="true">
40
+ {draft.legs.map((leg, index) => {
41
+ const label = strategyLegLabel(leg, draft.quantity);
42
+ const quote = m.legQuotes?.[index];
43
+ const mid = quote && Number.isFinite(quote.bid) && Number.isFinite(quote.ask) ? (quote.bid! + quote.ask!) / 2 : undefined;
44
+ return <Box key={index} flexDirection="row" gap={1}>
45
+ <Text fg={leg.action === "BUY" ? c.positive : c.negative} attributes={TextAttributes.BOLD}>{label.side}</Text>
46
+ <Text fg={c.text}>{label.contracts}</Text>
47
+ <Box flexGrow={1} minWidth={0}><Text fg={c.text} truncate>{label.contract}</Text></Box>
48
+ {mid !== undefined ? <Text fg={c.textDim}>{mid.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 4 })}</Text> : null}
49
+ </Box>;
50
+ })}
51
+ </Box>;
52
+ }
53
+ export function TicketOrderSummary({ model: m, width, compact = false, receipt = false }: { model: TicketViewModel; width: number; compact?: boolean; receipt?: boolean }) {
54
+ const c = useThemeColors(); const draft = m.draft; const account = m.accounts.find((a) => a.accountId === m.accountId);
55
+ // A ticker opened by its symbol alone can carry an empty currency, which no formatter takes.
56
+ const currency = draft?.contract.currency || account?.currency || "USD";
57
+ const px = draft ? orderEstimatePrice(draft, m.quote) : undefined;
58
+ const total = m.phase === "result" ? m.preview?.estimatedCost === undefined ? undefined : Math.abs(m.preview.estimatedCost) : draft && px !== undefined ? px * draft.quantity * Number(draft.contract.multiplier ?? 1) : undefined;
59
+ const strategy = !!draft?.legs?.length;
60
+ const unit = strategy ? "spreads" : draft?.contract.secType === "OPT" ? "contracts" : "shares";
61
+ // A spread changes each leg's position, which its legs list.
62
+ const positionAfter = !strategy && m.position !== undefined && draft ? m.position + (draft.action === "SELL" ? -draft.quantity : draft.quantity) : undefined;
63
+ const powerAfter = m.preview?.buyingPowerAfter;
64
+ const sideColor = draft?.action === "SELL" ? c.negative : c.positive;
65
+ if (compact && receipt) return <Box flexDirection="column">
66
+ <Text fg={c.textDim}>Buying power · broker preview</Text>
67
+ <TicketReceiptRow label={powerAfter === undefined ? "Impact" : "After order"} value={ticketMoney(powerAfter ?? m.preview?.buyingPowerImpact, currency)} width={width} />
68
+ </Box>;
69
+ if (compact) return <Box flexDirection="row" justifyContent="space-between" paddingX={1} backgroundColor={c.panel}>
70
+ <Text fg={sideColor} attributes={TextAttributes.BOLD}>{`${draft?.action ?? "BUY"} ${ticketNumber(draft?.quantity)} ${m.symbol}`}</Text>
71
+ <Text fg={c.textBright} attributes={TextAttributes.BOLD}>{`Est. ${ticketMoney(total, currency)}`}</Text>
72
+ </Box>;
73
+ const rowWidth = width;
74
+ if (receipt) return <TicketSection width={width} title="Account impact" selectable>
75
+ <Text fg={c.textDim}>After this order fills</Text>
76
+ {strategy ? <Box paddingY={1}><TicketLegs model={m} width={rowWidth} /></Box> : <>
77
+ <Box paddingY={1}><FigureText>{positionAfter === undefined ? "Unknown position" : `${ticketNumber(positionAfter)} ${unit}`}</FigureText></Box>
78
+ <TicketReceiptRow label="Current position" value={ticketNumber(m.position)} width={rowWidth} />
79
+ <TicketReceiptRow label="Order quantity" value={`${draft?.action === "SELL" ? "−" : "+"}${ticketNumber(draft?.quantity)}`} color={sideColor} width={rowWidth} />
80
+ </>}
81
+ <Box marginTop={1}><SectionHeading title="Buying power · broker preview" /></Box>
82
+ <TicketReceiptRow label="Before order" value={ticketMoney(m.preview?.buyingPowerBefore ?? account?.buyingPower, currency)} width={rowWidth} />
83
+ <TicketReceiptRow label="Broker impact" value={ticketMoney(m.preview?.buyingPowerImpact, currency)} width={rowWidth} />
84
+ <TicketReceiptRow label="After order" value={ticketMoney(m.preview?.buyingPowerAfter, currency)} width={rowWidth} />
85
+ </TicketSection>;
86
+ return <TicketSection width={width} title="Order summary" selectable>
87
+ <TicketOrderHeadline model={m} large />
88
+ <Text fg={c.textDim}>{`${ticketTypeName[draft?.orderType ?? "LMT"]} · ${ticketTifName[draft?.tif ?? "DAY"] ?? draft?.tif}${draft?.outsideRth ? " · Outside RTH" : ""}`}</Text>
89
+ <Box flexDirection="column" paddingY={1}><Text fg={c.textDim}>Estimated order value</Text><FigureText>{ticketMoney(total, currency)}</FigureText></Box>
90
+ {strategy ? <TicketLegs model={m} width={rowWidth} /> : null}
91
+ <TicketReceiptRow label="Quantity" value={`${ticketNumber(draft?.quantity)} ${unit}`} width={rowWidth} />
92
+ <TicketReceiptRow label={strategy ? draft?.action === "SELL" ? "Net credit" : "Net debit" : "Price"} value={draft?.orderType === "MKT" ? "At market" : ticketMoney(px, currency)} width={rowWidth} />
93
+ {m.phase === "result" ? <Text fg={c.textDim}>Estimates from order preview</Text> : null}
94
+ <TicketReceiptRow label="Fees / commission" value={m.preview || m.phase === "result" ? `${ticketMoney(m.preview?.fees, currency)} / ${ticketMoney(m.preview?.commission, currency)}` : "At review"} width={rowWidth} />
95
+ <TicketReceiptRow label="Broker power after" value={ticketMoney(powerAfter, currency)} width={rowWidth} />
96
+ {strategy ? null : <TicketReceiptRow label="Position after" value={positionAfter === undefined ? "Not reported" : `${ticketNumber(positionAfter)} ${unit}`} width={rowWidth} />}
97
+ {m.phase !== "result" ? <Box marginTop={1}><Notice variant="callout" tone="warning">{m.quoteData !== "realtime" ? "Delayed broker quote. Execution prices may differ." : "Final cost and availability are checked at review."}</Notice></Box> : null}
98
+ </TicketSection>;
99
+ }
100
+ export function TicketStatus({ model: m, width }: { model: TicketViewModel; width: number }) {
101
+ const native = useUiCapabilities().nativePaneChrome;
102
+ const c = useThemeColors(); const status = m.result?.status.toUpperCase() ?? "UNKNOWN";
103
+ const filled = status === "FILLED"; const partial = status === "PARTIALLY_FILLED"; const active = !["UNKNOWN", "REJECTED", "CANCELLED", "CANCELED", "PENDING_CANCEL"].includes(status);
104
+ const currentStep = filled ? 2 : ["SUBMITTED", "PENDING_NEW"].includes(status) ? 0 : 1;
105
+ return <Box flexDirection="column" gap={native ? 0.35 : 0}>
106
+ {active ? <Box flexDirection="row" gap={1}>{["Submitted", "Working", "Filled"].map((label, index) =>
107
+ <Text key={label} fg={index <= currentStep ? c.positive : c.textDim} attributes={index === currentStep ? TextAttributes.BOLD : undefined}>{`${index ? "· " : ""}${label}`}</Text>)}</Box> : null}
108
+ {partial || filled ? <>
109
+ <RatioBar width={Math.max(10, width)} ratio={(m.result?.filled ?? 0) / Math.max(1, m.result?.quantity ?? 1)} color={c.positive} track thickness={6} />
110
+ <Text fg={c.text} wrapText>{`${ticketNumber(m.result?.filled)} of ${ticketNumber(m.result?.quantity)} filled · Avg. ${ticketMoney(m.result?.avgFillPrice, m.draft?.contract.currency)}`}</Text>
111
+ </> : null}
112
+ {status === "UNKNOWN" ? <Notice variant="callout" tone="warning">
113
+ {m.result?.orderId ? "Not confirmed yet. Checking with the broker." : "Not confirmed. Check open orders before placing it again."}</Notice> : null}
114
+ {status === "REJECTED" ? <Notice variant="callout" tone="negative">{m.error ?? m.result?.warningText ?? "The broker rejected this order. Check the order before trying again."}</Notice> : null}
115
+ {status === "PENDING_CANCEL" ? <Notice variant="callout" tone="warning">Cancellation requested. The order can still fill until the broker confirms.</Notice> : null}
116
+ </Box>;
117
+ }