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
@@ -1,11 +1,11 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useUiCapabilities } from "../../../ui";
3
- import { usePaneSettingValue, usePaneTicker, useUpdatePaneSettings } from "../../../state/app/context";
3
+ import { usePaneAppConfig, usePaneSettingValue, usePaneTicker, useUpdatePaneSettings } from "../../../state/app/context";
4
4
  import { colors, priceColor } from "../../../theme/colors";
5
5
  import { isPlainKey } from "../../../utils/keyboard";
6
6
  import { formatCompact, formatPercentRaw } from "../../../utils/format";
7
7
  import { formatMarketPrice, formatSignedMarketPrice, liveQuoteFormatOptions } from "../../../market-data/market/format";
8
- import { formatExpDate, resolveOptionsTarget } from "../../../utils/options";
8
+ import { chainOptionContract, formatExpDate, optionContractName, resolveOptionsTarget } from "../../../utils/options";
9
9
  import {
10
10
  expiryIsoDate, findListedExpiry, formatDaysToExpiry, daysToExpiry, missingExpiryMessage, missingExpiryText, readOptionExpiration,
11
11
  } from "../../../utils/option-expiry";
@@ -34,7 +34,9 @@ import { useLiveQuoteEntries, useQuoteUpdates } from "../../../state/hooks/quote
34
34
  import { buildChartKey } from "../../../market-data/selectors";
35
35
  import type { ChartRequest } from "../../../market-data/request-types";
36
36
  import { quoteSubscriptionTargetFromTicker } from "../../../market-data/request-types";
37
- import { usePluginAppActions } from "../../runtime";
37
+ import { usePluginAppActions, usePluginBrokerActions, usePluginTickerActions } from "../../runtime";
38
+ import { brokerCanTrade, openTradeTicket } from "../broker-trading/command";
39
+ import type { OptionContract } from "../../../types/financials";
38
40
  import { OPTIONS_CALCULATOR_TEMPLATE_ID } from "../options-calculator/model";
39
41
  import { buildChainCalcParams, resolveCalcSide } from "./calc-seed";
40
42
  import { useOptionsCatalogue } from "./expiry-catalogue";
@@ -66,7 +68,7 @@ import { signedPositionDirection } from "../../../portfolio/position-metrics";
66
68
  import { optionMarketReference } from "./market-reference";
67
69
  import { useOptionsEnrichment } from "./enrichment";
68
70
  import type { OptionsEnrichmentSnapshot } from "./enrichment-model";
69
- import { optionMid, type OptionSide } from "../shared/volatility";
71
+ import { daysToExpiryFrom, optionMid, type OptionSide } from "../shared/volatility";
70
72
  import type { TickerRecord } from "../../../types/ticker";
71
73
  import type { TickerFinancials } from "../../../types/financials";
72
74
  import type { IvStats } from "../iv-history/client";
@@ -176,6 +178,9 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
176
178
  const ticker = useMemo(() => savedTicker ?? (boundSymbol ? transientTicker(boundSymbol, fallbackExchange, fallbackCurrency) : null),
177
179
  [savedTicker, boundSymbol, fallbackExchange, fallbackCurrency]);
178
180
  const { createPaneFromTemplate } = usePluginAppActions();
181
+ const { pinTicker } = usePluginTickerActions();
182
+ const { getBrokerAdapter } = usePluginBrokerActions();
183
+ const appConfig = usePaneAppConfig();
179
184
  const liveStreaming = useLiveStreamingSetting();
180
185
  // A number from the app, or the date or seconds `fn OMON --expiration` took as text.
181
186
  const [seededSetting] = usePaneSettingValue<unknown>("expiration", undefined);
@@ -238,13 +243,20 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
238
243
  const strikeWindowText = strikeWindowValue(strikeWindow);
239
244
  const initialChainEntry = useOptionsQuery(baseRequest);
240
245
  const initialChain = useResolvedEntryValue(initialChainEntry);
241
- const initialExpiration = initialChain?.expirationDates.reduce((best, expiration) => (
246
+ // A listing can still carry a date whose 16:00 ET close has passed. The chain
247
+ // never opens there, nor on a saved date that has since passed: it takes the nearest live one.
248
+ const now = Date.now();
249
+ const liveExpiration = (expiration: number) => daysToExpiryFrom(expiration, now) > 0;
250
+ const listedExpirations = initialChain?.expirationDates ?? [];
251
+ const openingExpirations = listedExpirations.some(liveExpiration) ? listedExpirations.filter(liveExpiration) : listedExpirations;
252
+ const initialExpiration = openingExpirations.length === 0 ? undefined : openingExpirations.reduce((best, expiration) => (
242
253
  parsed && Math.abs(expiration - parsed.expTs) < Math.abs(best - parsed.expTs) ? expiration : best
243
- ), initialChain.expirationDates[0]!);
254
+ ), openingExpirations[0]!);
244
255
  // A typed date names a day; the catalogue's stamp for that day is the expiry.
245
256
  const seededDate = readOptionExpiration(seededSetting);
246
- const seededExpiration = seededDate == null ? undefined
247
- : findListedExpiry(seededDate, initialChain?.expirationDates ?? []) ?? seededDate;
257
+ const seededListed = seededDate == null ? undefined : findListedExpiry(seededDate, listedExpirations) ?? seededDate;
258
+ const seededExpiration = seededListed != null && (liveExpiration(seededListed) || !openingExpirations.some(liveExpiration))
259
+ ? seededListed : undefined;
248
260
  const selectedExpiration = expirationTargetKey == null || expirationTargetKey === selectionTargetKey
249
261
  ? seededExpiration ?? initialExpiration : initialExpiration;
250
262
  const viewportKey = `${effectiveTicker}:${selectedExpiration ?? "initial"}`;
@@ -281,8 +293,11 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
281
293
  ?? (matchesSelectedExpiration(initialChain) ? initialChain : null);
282
294
  const strikesLoading = strikeChain === null && !expirationUnavailable;
283
295
  const expirationDates = useMemo(() => {
284
- return selectedExpirationMissing
296
+ const listed = selectedExpirationMissing
285
297
  ? [...availableExpirations, selectedExpiration!].sort((left, right) => left - right) : availableExpirations;
298
+ // A date that has ended has no contracts left to show; the strip leaves it out, unless every date has.
299
+ const current = (expiration: number) => daysToExpiry(expiration, Date.now()) >= 0;
300
+ return listed.some(current) ? listed.filter((expiration) => expiration === selectedExpiration || current(expiration)) : listed;
286
301
  }, [availableExpirations, selectedExpiration, selectedExpirationMissing]);
287
302
  // Keep the date strip stable while a mouse press focuses this pane. A new
288
303
  // tab list asks the web host to reveal the active tab and can move the date
@@ -468,13 +483,17 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
468
483
  : { key: viewportKey, range }
469
484
  ));
470
485
  }, [viewportKey]);
486
+ // A chain read from a broker profile has its contracts quoted by that profile too.
487
+ const quoteBrokerId = instrument?.brokerId, quoteBrokerInstanceId = instrument?.brokerInstanceId;
488
+ const optionQuoteBroker = useMemo(() => quoteBrokerId || quoteBrokerInstanceId
489
+ ? { brokerId: quoteBrokerId, brokerInstanceId: quoteBrokerInstanceId } : undefined, [quoteBrokerId, quoteBrokerInstanceId]);
471
490
  const optionQuoteTargets = useMemo(
472
491
  () => buildOptionQuoteTargets(quoteRows, {
473
492
  fallbackHeight: height,
474
493
  selectedIndex: selectedStrikeIdx,
475
494
  visibleRange: visibleStrikeRange,
476
- }),
477
- [height, quoteRows, selectedStrikeIdx, visibleStrikeRange],
495
+ }, optionQuoteBroker),
496
+ [height, quoteRows, selectedStrikeIdx, visibleStrikeRange, optionQuoteBroker],
478
497
  );
479
498
  const {
480
499
  entries: optionQuoteEntries,
@@ -497,8 +516,8 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
497
516
  // The snapshot object itself comes back while nothing streamed, so the
498
517
  // memos below only recompute when a quote actually landed.
499
518
  const liveChain = useMemo(
500
- () => strikeChain ? overlayOptionChainQuotes(strikeChain, optionQuoteEntries, optionQuoteFreshness).chain : null,
501
- [optionQuoteEntries, optionQuoteFreshness, strikeChain],
519
+ () => strikeChain ? overlayOptionChainQuotes(strikeChain, optionQuoteEntries, optionQuoteFreshness, optionQuoteBroker).chain : null,
520
+ [optionQuoteEntries, optionQuoteFreshness, strikeChain, optionQuoteBroker],
502
521
  );
503
522
  const callsByStrike = useMemo(
504
523
  () => new Map(liveChain?.calls.map((c) => [c.strike, c]) ?? []),
@@ -672,6 +691,28 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
672
691
  }, [scenarioContract, scenarioAvailable, scenarioMid, scenarioVolatility, createPaneFromTemplate, dividendYield, effectiveTicker,
673
692
  effectiveExchange, selectedSide, spot, underlying?.quote?.lastUpdated]);
674
693
 
694
+ // A contract opens as its own listing: quote, chart and Trade. It stays on
695
+ // the chain's broker while that profile is on, else it opens by its symbol.
696
+ const contractListing = useCallback((contract: OptionContract) => {
697
+ const symbol = contract.contractSymbol.trim().toUpperCase();
698
+ const currency = contract.currency || underlying?.quote?.currency || "USD";
699
+ const brokerOn = !!instrument && appConfig.brokerInstances.some((profile) => profile.id === instrument.brokerInstanceId && profile.enabled !== false);
700
+ return { symbol, selection: { instrument: brokerOn ? chainOptionContract(symbol, instrument, currency) : null,
701
+ listing: { name: optionContractName(symbol) ?? symbol, exchange: "", currency, type: "OPT" } } };
702
+ }, [appConfig.brokerInstances, instrument, underlying?.quote?.currency]);
703
+ const openContract = useCallback((contract: OptionContract | null | undefined) => {
704
+ if (!contract) return;
705
+ const { symbol, selection } = contractListing(contract);
706
+ pinTicker(symbol, selection);
707
+ }, [contractListing, pinTicker]);
708
+ const tradable = !!scenarioContract && brokerCanTrade(appConfig.brokerInstances, getBrokerAdapter, scenarioContract.contractSymbol.trim().toUpperCase(), "OPT");
709
+ const tradeContract = useCallback(() => {
710
+ if (!scenarioContract || !tradable) return;
711
+ const { symbol, selection } = contractListing(scenarioContract);
712
+ // A new contract's ticket starts on Buy; b and s choose the side in it.
713
+ openTradeTicket({ pinTicker }, symbol, { action: "BUY" }, selection);
714
+ }, [contractListing, pinTicker, scenarioContract, tradable]);
715
+
675
716
  const openSurface = useCallback(() => {
676
717
  if (!ticker || selectedExpiration == null) return;
677
718
  createPaneFromTemplate("vol-surface-pane", { symbol: ticker.metadata.ticker, ticker, instrument,
@@ -703,12 +744,14 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
703
744
  setCalcSide(sideTarget);
704
745
  }, [selectContract, selectedRow, selectedStrikeIdx, sideTarget]);
705
746
 
747
+ // With a broker that trades the contract, [t]rade opens its ticket on Buy, and Sell is chosen there.
706
748
  const footerHints = useMemo(() => [
707
749
  ...(sideTarget ? [{ id: "side", key: SIDE_KEY, label: ` ${sideTarget}`, title: sideTarget === "put" ? "Select Put" : "Select Call", onPress: switchSide }] : []),
750
+ ...(tradable ? [{ id: "trade-contract", key: "t", label: "rade", title: "Trade Contract", onPress: tradeContract }] : []),
708
751
  ...(calcParams ? [{ id: "calc", key: "c", label: "alc", onPress: openCalculator }] : []),
709
752
  ...(scenarioAvailable ? [{ id: "scenario", key: "a", label: "dd to OSA", onPress: openScenario }] : []),
710
753
  ...(ticker && selectedExpiration != null ? [{ id: "surface", key: "s", label: "urface", onPress: openSurface }] : []),
711
- ], [sideTarget, switchSide, calcParams, openCalculator, scenarioAvailable, openScenario, ticker, selectedExpiration, openSurface]);
754
+ ], [sideTarget, switchSide, tradable, tradeContract, calcParams, openCalculator, scenarioAvailable, openScenario, ticker, selectedExpiration, openSurface]);
712
755
 
713
756
  // Back to where the chain opens: the held contract's strike, else the money.
714
757
  const defaultStrikeIndex = strikes.length === 0 ? -1 : (() => {
@@ -732,8 +775,10 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
732
775
  { id: "default-strike", label: parsed ? "Go to Held Strike" : "Go to ATM Strike",
733
776
  enabled: defaultStrikeIndex >= 0, onSelect: goToDefaultStrike },
734
777
  ...(enrichment?.asOf && dialog ? [{ id: "analytics-as-of", label: "Analytics As Of…", onSelect: showAnalyticsAsOf }] : []),
778
+ { id: "open-contract", label: "Open Contract", accelerator: "Enter", enabled: !!scenarioContract, onSelect: () => openContract(scenarioContract) },
779
+ ...(tradable ? [{ id: "trade-contract", label: "Trade Contract", accelerator: "t", onSelect: tradeContract }] : []),
735
780
  ], [defaultStrikeIndex, dialog, enrichment?.asOf, expirationDates.length, goToDefaultStrike, parsed,
736
- selectAdjacentExpiration, selectedExpirationIndex, showAnalyticsAsOf]);
781
+ selectAdjacentExpiration, selectedExpirationIndex, showAnalyticsAsOf, scenarioContract, openContract, tradable, tradeContract]);
737
782
 
738
783
  const renderCell = useCallback((
739
784
  row: OptionTableRow,
@@ -756,6 +801,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
756
801
  }, [selectContract, selectedSide]);
757
802
 
758
803
  useOptionsAccessFooter({
804
+ broker: !!optionQuoteBroker,
759
805
  chain,
760
806
  error: [error, underlyingStale ? "Underlying quote stale: Greeks and calculator unavailable" : null,
761
807
  selectedContract && strikeChain && !selectedContractAvailable
@@ -789,7 +835,21 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
789
835
  // the arrows for it; in a research tab those stay with the tab strip.
790
836
  useShortcut((event) => {
791
837
  if (event.defaultPrevented || event.propagationStopped || event.targetEditable) return;
792
- if (event.ctrl || event.meta || event.alt || event.shift) return;
838
+ if (event.ctrl || event.meta || event.alt) return;
839
+ const surface = isPlainKey(event, "s");
840
+ if (surface && ticker && selectedExpiration != null) {
841
+ event.preventDefault();
842
+ event.stopPropagation();
843
+ openSurface();
844
+ return;
845
+ }
846
+ if (event.shift) return;
847
+ if (tradable && isPlainKey(event, "t") && !event.repeat) {
848
+ event.preventDefault();
849
+ event.stopPropagation();
850
+ tradeContract();
851
+ return;
852
+ }
793
853
 
794
854
  const expiryStep = isPlainKey(event, EXPIRY_PREVIOUS_KEY) || (!nestedInTabs && isPlainKey(event, "h", "left")) ? -1
795
855
  : isPlainKey(event, EXPIRY_NEXT_KEY) || (!nestedInTabs && isPlainKey(event, "l", "right")) ? 1 : 0;
@@ -799,12 +859,6 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
799
859
  selectAdjacentExpiration(expiryStep);
800
860
  return;
801
861
  }
802
- if (isPlainKey(event, "s") && ticker && selectedExpiration != null) {
803
- event.preventDefault();
804
- event.stopPropagation();
805
- openSurface();
806
- return;
807
- }
808
862
  if (isPlainKey(event, "c") && calcParams) {
809
863
  event.preventDefault();
810
864
  event.stopPropagation();
@@ -925,6 +979,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
925
979
  onVisibleRangeChange={handleVisibleStrikeRangeChange}
926
980
  getItemKey={(row) => String(row.strike)}
927
981
  renderCell={renderCell}
982
+ onActivate={(row) => openContract(selectedSide === "put" ? row.put : selectedSide === "call" ? row.call : null)}
928
983
  selectedTextOverridesCellColor
929
984
  emptyStateTitle={unavailableExpiry ?? (error && !strikeChain ? "Selected expiration unavailable." : strikesLoading ? "Loading strikes..."
930
985
  : strikeWindowResult.total > 0 ? "No strikes in this window." : "No strikes available.")}
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
2
+ import { delayedFooterSegment, FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
3
3
  import { CompositeChart } from "../../../components/chart/composite/composite-chart";
4
4
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
5
5
  import { useAsyncResource, useInputCapture } from "../../../public/react";
@@ -279,7 +279,9 @@ export function OptionsCalculatorPane({ focused, width, height }: PaneProps) {
279
279
  ...(surfaceSource && surfaceResource.loading ? [{ id: "loading", parts: [{ text: "loading surface", tone: "muted" as const }] }] : []),
280
280
  ...(surfaceSource && surface?.asOf ? [{ id: "surface-asof", parts: [{ text: `surface · ${surface.asOf}`, tone: "muted" as const }] }] : []),
281
281
  ...(effectiveSteps && effectiveSteps !== (draft.steps ?? 400) ? [{ id: "refined", parts: [{ text: `tree refined to ${effectiveSteps} steps`, tone: "muted" as const }] }] : []),
282
- ...(liveInputs ? [{ id: "market", parts: [{ text: liveInputs.delayed ? "delayed market" : "real-time market", tone: "muted" as const }] }] : []),
282
+ ...(liveInputs ? [liveInputs.delayed
283
+ ? delayedFooterSegment({ id: "market", detail: "Delayed market inputs" })
284
+ : { id: "market", parts: [{ text: "real-time market", tone: "muted" as const }] }] : []),
283
285
  ],
284
286
  hints: activeFieldId ? [] : [
285
287
  // Names the cell Enter or `e` edits, which the grid does not mark.
@@ -1,8 +1,13 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { Button, ChoiceDialog, confirmDialog, DataTableView, EmptyState, PageStackView, PaneStatusBody, QueryBar, StatGrid, statGridRows,
2
+ import { splitDelayedStatus, Button, ChoiceDialog, confirmDialog, DataTableView, EmptyState, PageStackView, PaneStatusBody, QueryBar, StatGrid, statGridRows,
3
3
  usePaneFooter, usePaneNoticeFooter, usePaneTabs, type DataTableColumn, type SelectControl, type StatItem } from "../../../components";
4
4
  import { useAsyncResource, useInputCapture, usePaneInstance, usePaneSettingValue, usePaneTicker,
5
5
  usePluginAppActions, usePluginPaneState, usePluginState, useShortcut } from "../../../public/react";
6
+ import { usePaneAppConfig } from "../../../state/app/context";
7
+ import { usePluginBrokerActions, usePluginTickerActions } from "../../runtime";
8
+ import { brokerCanTrade, brokerCanTradeStrategy, openTradeTicket } from "../broker-trading/command";
9
+ import { MAX_STRATEGY_LEGS, strategyFromLegs } from "../broker-trading/strategy";
10
+ import { optionContractName } from "../../../utils/options";
6
11
  import { Box } from "../../../ui";
7
12
  import { useDialogState } from "../../../ui/dialog";
8
13
  import { useDialog, type PromptContext } from "../../../ui/dialog";
@@ -72,13 +77,16 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
72
77
  const [consumedSeed, setConsumedSeed] = usePluginPaneState<string | null>("consumedSeed", null);
73
78
  const [localError, setLocalError] = useState<string | null>(null);
74
79
  const controller = useRef<AbortController | null>(null);
80
+ const brokerInstrument = target?.instrument ?? null;
75
81
  const explicitSpot = Number(settings.spot);
76
82
  const loadMarket = useCallback(async (force: boolean) => {
77
83
  controller.current?.abort();
78
84
  const abort = new AbortController(); controller.current = abort;
79
- return loadScenarioMarket({ instrument: { symbol: underlying, exchange },
80
- expiration: expiration ?? undefined, forceRefresh: force, signal: abort.signal });
81
- }, [underlying, exchange, expiration]);
85
+ // The chain OMON reads for this ticker, from its broker when it is linked to one: one chain, one source, one cache entry.
86
+ return loadScenarioMarket({ instrument: { symbol: underlying, exchange, brokerId: brokerInstrument?.brokerId,
87
+ brokerInstanceId: brokerInstrument?.brokerInstanceId, instrument: brokerInstrument },
88
+ expiration: expiration ?? undefined, forceRefresh: force, signal: abort.signal });
89
+ }, [underlying, exchange, expiration, brokerInstrument]);
82
90
  // Fully specified calculations and screenshot snapshots never need a quote refresh.
83
91
  const needsMarket = !frozen && (!["spot", "rate", "dividendYield"].every((key) => settings[key] != null && settings[key] !== "") || detail === "chain");
84
92
  const resource = useAsyncResource(underlying && needsMarket ? loadMarket : null);
@@ -240,6 +248,32 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
240
248
  };
241
249
  // Freezing keeps the marks shown at this moment as the scenario's inputs.
242
250
  const freeze = () => { if (live) setPosition(live.position); setFollow(false); };
251
+ // With a broker that takes them, the position trades as one order: a
252
+ // spread on the underlying's Trade tab, or one contract's own ticket. The
253
+ // live mids, else the entry prices, say whether it opens for a debit or a credit.
254
+ const { pinTicker } = usePluginTickerActions();
255
+ const { getBrokerAdapter } = usePluginBrokerActions();
256
+ const { brokerInstances } = usePaneAppConfig();
257
+ const tradeIntent = useMemo(() => {
258
+ const legs = position?.legs ?? [];
259
+ if (frozen || !underlying || !legs.length || legs.length > MAX_STRATEGY_LEGS || legs.some((leg) => leg.multiplier !== 100 || !legSymbols.get(leg.id))) return null;
260
+ const intent = strategyFromLegs(legs.map((leg) => {
261
+ const quote = live?.quotes.get(leg.id);
262
+ return { contractSymbol: legSymbols.get(leg.id)!, quantity: leg.quantity, price: quote ? optionMid(quote) ?? leg.price : leg.price };
263
+ }));
264
+ if (!intent) return null;
265
+ const tradable = intent.legs.length === 1 ? brokerCanTrade(brokerInstances, getBrokerAdapter, intent.legs[0]!.contractSymbol, "OPT")
266
+ : brokerCanTradeStrategy(brokerInstances, getBrokerAdapter, underlying, intent.legs.length);
267
+ return tradable ? intent : null;
268
+ }, [frozen, underlying, position?.legs, legSymbols, live?.quotes, brokerInstances, getBrokerAdapter]);
269
+ const trade = () => {
270
+ if (!tradeIntent) return;
271
+ const [only] = tradeIntent.legs;
272
+ if (tradeIntent.legs.length === 1 && only) {
273
+ openTradeTicket({ pinTicker }, only.contractSymbol, { action: only.action, quantity: tradeIntent.quantity * only.ratio },
274
+ { instrument: null, listing: { name: optionContractName(only.contractSymbol) ?? only.contractSymbol, exchange: "", currency: position?.currency ?? "USD", type: "OPT" } });
275
+ } else openTradeTicket({ pinTicker }, symbol ?? underlying, tradeIntent);
276
+ };
243
277
  const hints = detail === "chain" ? [{ id: "expiry", key: "d", label: "ate", onPress: () => chainExpiryControl.current?.open() }] : detail || volActive ? [] : [
244
278
  { id: "add", key: "a", label: "dd leg", onPress: addTyped },
245
279
  { id: "chain", key: "c", label: "hain", onPress: () => setDetail("chain") },
@@ -250,6 +284,7 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
250
284
  { id: "vol", key: "v", label: "ol shift", onPress: () => setVolActive(true) },
251
285
  { id: "save", key: "s", label: "ave", onPress: () => setDetail("save") }] : []),
252
286
  { id: "load", key: "b", label: "rowse saved", onPress: () => void loadSaved() },
287
+ ...(tradeIntent ? [{ id: "trade", key: "t", label: "rade", onPress: trade }] : []),
253
288
  ...(live ? [{ id: "follow", key: "f", label: "reeze", onPress: freeze }]
254
289
  : follow || frozen || frozenMarket || !storedPosition ? [] : [{ id: "follow", key: "f", label: "ollow market", onPress: () => setFollow(true) }]),
255
290
  ];
@@ -270,8 +305,8 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
270
305
  ...(resource.loading ? [{ id: "loading", parts: [{ text: "loading chain", tone: "muted" as const }] }] : []),
271
306
  ...(error ? [{ id: "error", parts: [{ text: error, tone: "warning" as const }] }] : []),
272
307
  ...(hedge.error ? [{ id: "hedge-error", parts: [{ text: hedge.error, tone: "warning" as const }] }] : []),
273
- ...(live ? [{ id: "asof", parts: [{ text: `${live.basis} · ${spotText} · ${new Date(live.observedAt).toISOString().slice(11, 19)} UTC`, tone: "muted" as const }],
274
- variants: [[{ text: shortSpotText, tone: "muted" as const }]] }]
308
+ ...(live ? splitDelayedStatus({ id: "asof", parts: [{ text: `${live.basis} · ${spotText} · ${new Date(live.observedAt).toISOString().slice(11, 19)} UTC`, tone: "muted" }],
309
+ variants: [[{ text: shortSpotText, tone: "muted" }]] })
275
310
  : position ? [{ id: "asof", parts: [{ text: `${spotText} · ${formatSpotTime(position.asOf)} · ${market?.source ? "market" : "input assumptions"}${midVolatility}`, tone: "muted" as const }],
276
311
  variants: [[{ text: shortSpotText, tone: "muted" as const }]] }] : []),
277
312
  ], hints }), [hints, position, market?.source, resource.loading, error, hedge.error, live?.basis, live?.observedAt, midVolatility, spotText, shortSpotText]);
@@ -7,6 +7,8 @@ import { isFiniteNumber } from "../../../utils/guards";
7
7
  export interface PortfolioAccountMetrics {
8
8
  dailyPnl: number;
9
9
  dailyPnlPct: number;
10
+ /** The day is the broker's own, measured from its account's prior close rather than each quote's. */
11
+ brokerDay?: true;
10
12
  unrealizedPnl: number;
11
13
  unrealizedPnlPct: number;
12
14
  realizedPnl?: number;
@@ -196,6 +198,7 @@ export function resolvePortfolioAccountMetrics(
196
198
  return {
197
199
  dailyPnl,
198
200
  dailyPnlPct,
201
+ ...(brokerDailyPnl != null ? { brokerDay: true as const } : {}),
199
202
  unrealizedPnl,
200
203
  unrealizedPnlPct,
201
204
  realizedPnl: isFiniteNumber(account?.realizedPnl) ? convertAccountValue(account.realizedPnl) : undefined,
@@ -12,6 +12,7 @@ import {
12
12
  import { formatCompact, formatPercentRaw } from "../../../../utils/format";
13
13
  import { currencyPriceMinDigits, formatMarketCostWithCurrency, formatMarketPrice, formatMarketPriceWithCurrency, formatMarketQuantity, quoteFormatOptions } from "../../../../market-data/market/format";
14
14
  import { resolvePriceBasis } from "../../../../market-data/market/price-basis";
15
+ import { combinedChangeBasisLabel, quoteChangeBasis, type QuoteChangeBasis } from "../../../../market-data/market/change-basis";
15
16
  import { getPortfolioPositionMetrics, lotCostAtTodaysRate, lotCostFxRate, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../../../portfolio/position-metrics";
16
17
  import {
17
18
  getPortfolioValuationDisplay,
@@ -56,6 +57,7 @@ import { loadPerpetualMarket } from "../../../../sources/gloomberb-cloud/perpetu
56
57
  import { listedMarketCapitalization, selectMarketCapitalization } from "../../../../utils/market-capitalization";
57
58
  import { chunked, mapWithConcurrency } from "../../../../utils/concurrency";
58
59
  import { liveMarketCapitalization } from "../../../../market-data/quotes/live-valuation";
60
+ import { delistingLabel } from "../../../../market-data/quotes/delisting";
59
61
  import { instrumentFromTicker } from "../../../../market-data/request-types";
60
62
  import { toMarketDataContext } from "../../../../market-data/selectors";
61
63
  import { cliFreshnessFooter } from "../../../../cli/result";
@@ -485,29 +487,46 @@ export async function valuePortfolioPositions({
485
487
  /**
486
488
  * The holdings' move over the day of the prices they are valued at, in the
487
489
  * portfolio's currency, and that move against their value at the close
488
- * before. Null when any priced holding's move is unknown, as the pane's Day is.
490
+ * before, with what it is measured from as the pane's Day says it ("vs Fri 16
491
+ * Oct close", "vs mixed closes"). Null when any priced holding's move is
492
+ * unknown, as the pane's Day is.
489
493
  */
490
- export function portfolioDayMove(valuation: Pick<PortfolioValuation, "positions">): { change: number; changePercent: number | null } | null {
494
+ export function portfolioDayMove(
495
+ valuation: Pick<PortfolioValuation, "positions">,
496
+ now = Date.now(),
497
+ ): { change: number; changePercent: number | null; basis: string | null } | null {
491
498
  let change = 0;
492
499
  let previousValue = 0;
500
+ const bases: Array<QuoteChangeBasis | null> = [];
493
501
  for (const entry of valuation.positions) {
494
502
  if (!entry.position) continue;
495
503
  if (!entry.dayMove) return null;
496
504
  change += entry.dayMove.change;
497
505
  previousValue += entry.dayMove.previousValue;
506
+ bases.push(quoteChangeBasis(entry.quote, { terms: entry.futures }));
498
507
  }
499
- return { change, changePercent: previousValue !== 0 ? (change / previousValue) * 100 : null };
508
+ return {
509
+ change,
510
+ changePercent: previousValue !== 0 ? (change / previousValue) * 100 : null,
511
+ basis: combinedChangeBasisLabel(bases, now),
512
+ };
500
513
  }
501
514
 
502
- /** `Day's move: +$1,165.49 (+0.51%)`, the line a book valued at the close carries; `color` tints the figures by sign. */
515
+ /**
516
+ * `Day's move: +$1,165.49 (+0.51%) vs Fri 16 Oct close`, the line a book
517
+ * valued at the close carries; `color` tints the figures by sign, `muted` the
518
+ * basis.
519
+ */
503
520
  export function dayMoveLine(
504
521
  move: ReturnType<typeof portfolioDayMove>,
505
522
  currency: string,
506
523
  color: (text: string, value: number) => string = (text) => text,
524
+ muted: (text: string) => string = (text) => text,
507
525
  ): string {
508
526
  if (!move) return "Day's move: unavailable";
509
527
  const percent = move.changePercent != null ? ` (${formatPercentRaw(move.changePercent)})` : "";
510
- return `Day's move: ${color(`${formatAllocationMoney(move.change, currency, { signed: true })}${percent}`, move.change)}`;
528
+ const basis = move.basis ? ` ${muted(move.basis)}` : "";
529
+ return `Day's move: ${color(`${formatAllocationMoney(move.change, currency, { signed: true })}${percent}`, move.change)}${basis}`;
511
530
  }
512
531
 
513
532
  /**
@@ -898,7 +917,7 @@ async function showCollectionWithMarketData(
898
917
  total: allocation.total, cash: cashExport, targetSum: allocation.targetSum,
899
918
  // Only on a book that holds a short: its weights are shares of `weightBase`, its gross exposure, not of the total.
900
919
  ...(allocation.weightBasis === "gross" ? { weightBasis: allocation.weightBasis, weightBase: allocation.weightBase } : {}),
901
- ...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null } : {}),
920
+ ...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null, dayChangeBasis: dayMove?.basis ?? null } : {}),
902
921
  unpricedCount: allocation.unpriced.length, unpricedSymbols: allocation.unpriced,
903
922
  totalUnrealizedPnl: totalPnlValue,
904
923
  annualIncome: totalIncome, knownAnnualIncome: income.knownIncome, portfolioYield,
@@ -1043,7 +1062,7 @@ async function showCollectionWithMarketData(
1043
1062
  rows,
1044
1063
  ));
1045
1064
  console.log("");
1046
- if (basis === "regular-close") console.log(dayMoveLine(dayMove, currency, colorBySign));
1065
+ if (basis === "regular-close") console.log(dayMoveLine(dayMove, currency, colorBySign, cliStyles.muted));
1047
1066
  if (valued.some((entry) => entry.position)) console.log(exposureLine(exposure, currency));
1048
1067
  const notes: string[] = [];
1049
1068
  const weightNote = weightBasisLine(allocation, currency);
@@ -1136,13 +1155,15 @@ async function showCollectionWithMarketData(
1136
1155
  const priceText = quote && headline
1137
1156
  ? colorBySign(formatListPrice(headline.price, quote, ticker.metadata.assetCategory), headline.change)
1138
1157
  : "—";
1139
- const changeText = headline ? colorBySign(formatPercentRaw(headline.changePercent), headline.change) : "—";
1158
+ // A listing that no longer trades has no day's move: its last session's is years old.
1159
+ const changeText = quote?.delisting ? "—" : headline ? colorBySign(formatPercentRaw(headline.changePercent), headline.change) : "—";
1160
+ const staleText = quote?.delisting ? delistingLabel(quote.delisting) : lastTrade ? `STALE ${lastTrade}` : "STALE";
1140
1161
  return [
1141
1162
  listingLabel(ticker),
1142
1163
  venue ?? "—",
1143
1164
  priceText,
1144
1165
  changeText,
1145
- ...stale.length > 0 ? [rowStale ? cliStyles.warning(lastTrade ? `STALE ${lastTrade}` : "STALE") : ""] : [],
1166
+ ...stale.length > 0 ? [rowStale ? cliStyles.warning(staleText) : ""] : [],
1146
1167
  ...extendedCells(extendedSessions, extended, quote, ticker.metadata.assetCategory),
1147
1168
  ...showFunding ? [funding == null ? "—" : colorBySign(formatPerpetualRate(funding, 4), funding)] : [],
1148
1169
  marketCap == null ? "—" : `${formatCompact(marketCap)} ${currency}`,
@@ -1168,7 +1189,8 @@ async function showCollectionWithMarketData(
1168
1189
  if (notes.length > 0) console.log(["", ...notes.map((note) => cliStyles.muted(note))].join("\n"));
1169
1190
  // The closing line counts the stale rows; the line under it names them.
1170
1191
  if (stale.length > 0) {
1171
- staleNote = `Stale: ${namedList(stale.map(({ ticker, lastTrade }) => `${listingLabel(ticker)}${lastTrade ? ` (last trade ${lastTrade})` : ""}`))}.`;
1192
+ staleNote = `Stale: ${namedList(stale.map(({ ticker, quote, lastTrade }) => `${listingLabel(ticker)}${quote?.delisting ? ` (${delistingLabel(quote.delisting)})`
1193
+ : lastTrade ? ` (last trade ${lastTrade})` : ""}`))}.`;
1172
1194
  }
1173
1195
  }
1174
1196
  if (freshness) console.log(`\n${cliFreshnessFooter(freshness)}${staleNote ? `\n${cliStyles.muted(staleNote)}` : ""}`);
@@ -1304,6 +1326,8 @@ async function valueWatchlistRows({
1304
1326
  // A stale quote (as the source line counts it) and the exchange-local date it last traded.
1305
1327
  stale,
1306
1328
  lastTradeDate: lastTrade,
1329
+ // A listing that no longer trades: its status and last session.
1330
+ ...quote?.delisting ? { delisting: quote.delisting } : {},
1307
1331
  },
1308
1332
  });
1309
1333
  }
@@ -444,7 +444,7 @@ async function portfolioHoldings(
444
444
  cash: allocation.cash ? finite(allocation.cash.value) : null,
445
445
  total: allocation.total,
446
446
  // At the regular close, the holdings' move from the close before.
447
- ...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null } : {}),
447
+ ...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null, dayChangeBasis: dayMove?.basis ?? null } : {}),
448
448
  },
449
449
  // Only on a book that holds a short: its weights are shares of `weightBase`, its gross exposure, not of the total.
450
450
  ...(allocation.weightBasis === "gross" ? { weightBasis: allocation.weightBasis, weightBase: allocation.weightBase } : {}),
@@ -1,4 +1,5 @@
1
- import { isStablecoinCurrency } from "../../../../utils/currency-units";
1
+ import { heldCurrencyUnit, isStablecoinCurrency } from "../../../../utils/currency-units";
2
+ import { isIndexLevel } from "../../../../market-data/market/index-level";
2
3
  import { createRowValueCache } from "../../../../components/ui/row-value-cache";
3
4
  import { PRICE_SPARKLINE_COLUMN_ID } from "../../../../components/price-sparkline/view";
4
5
  import type { AppConfig, ColumnConfig } from "../../../../types/config";
@@ -196,6 +197,27 @@ export function buildTrackedCurrencies(
196
197
  return [...currencies].filter((currency) => !isStablecoinCurrency(currency));
197
198
  }
198
199
 
200
+ /**
201
+ * Whether a row is priced in another currency than the collection's totals
202
+ * (RY in CAD beside JNJ in USD, or a US stock in a CAD portfolio), so its
203
+ * prices need their currency beside them. Pence and cents count as their
204
+ * currency, whose sign the price already carries; an index has none.
205
+ */
206
+ export function holdsOtherCurrencies(
207
+ tickers: readonly TickerRecord[],
208
+ financialsMap: ReadonlyMap<string, TickerFinancials>,
209
+ totalsCurrency: string,
210
+ ): boolean {
211
+ const totals = heldCurrencyUnit(totalsCurrency).currency;
212
+ return tickers.some((ticker) => {
213
+ const quote = financialsMap.get(ticker.metadata.ticker)?.quote;
214
+ if (isIndexLevel(quote?.instrumentType || ticker.metadata.assetCategory)) return false;
215
+ const currency = heldCurrencyUnit(ticker.metadata.positions.find((position) => position.currency)?.currency
216
+ || quote?.currency || ticker.metadata.currency).currency;
217
+ return !!currency && currency !== totals;
218
+ });
219
+ }
220
+
199
221
  export function getCollectionTypeFromConfig(config: AppConfig, collectionId: string | null): "portfolio" | "watchlist" | null {
200
222
  if (!collectionId) return null;
201
223
  if (config.portfolios.some((portfolio) => portfolio.id === collectionId)) return "portfolio";
@@ -1,4 +1,6 @@
1
- import { Box } from "../../../../ui";
1
+ import { Box, useUiCapabilities } from "../../../../ui";
2
+ import { brokerPortfolioLogo } from "../../../../brokers/logos";
3
+ import { LogoMark } from "../../../../components/ui/logo-mark";
2
4
  import { describeFundamentalMarketCap } from "../../../../utils/market-capitalization";
3
5
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
6
  import {
@@ -69,7 +71,7 @@ import { PortfolioGrid } from "../grid";
69
71
  import { useThrottledCursorSymbol } from "../use-throttled-cursor-symbol";
70
72
  import { useCursorNeighborPrefetch } from "../use-cursor-neighbor-prefetch";
71
73
  import { getSharedRegistry } from "../../../registry";
72
- import { usePluginAppActions } from "../../../runtime";
74
+ import { usePluginAppActions, usePluginBrokerActions } from "../../../runtime";
73
75
  import { isManualPortfolio } from "../../../../portfolio/mutations";
74
76
  import {
75
77
  cursorAfterRemoval,
@@ -78,8 +80,10 @@ import {
78
80
  tickerRemovalSummary,
79
81
  } from "./remove-ticker";
80
82
  import { QuickAddTickerInput, type QuickAddCollectionKind } from "../quick-add";
83
+ import { delistingLabel } from "../../../../market-data/quotes/delisting";
81
84
  import {
82
85
  buildTrackedCurrencies,
86
+ holdsOtherCurrencies,
83
87
  getCollectionTickersFromConfig,
84
88
  getCollectionTypeFromConfig,
85
89
  resolveVisibleWarmupRequirements,
@@ -196,9 +200,16 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
196
200
  // The positions stay as last synced while the broker is away, dated, with one key to bring it back.
197
201
  const offlineBrokerFooter = useOfflineBrokerFooter(brokerPortfolio ? offline : null);
198
202
  const portfolioTargets = currentPortfolio?.targetWeights;
203
+ // Totals, money columns and account values are in the portfolio's currency.
204
+ const totalsCurrency = useMemo(
205
+ () => resolvePortfolioTotalsCurrency(currentPortfolio, config.baseCurrency),
206
+ [config.baseCurrency, currentPortfolio],
207
+ );
208
+ // A boolean, so a quote tick does not rebuild the columns.
209
+ const otherCurrencies = holdsOtherCurrencies(tickers, financialsMap, totalsCurrency);
199
210
  const columns = useMemo(
200
- () => resolveVisibleColumns(paneSettings.columnIds, isPortfolioTab, { targetWeights: hasTargetWeights(currentPortfolio) }),
201
- [currentPortfolio, isPortfolioTab, paneSettings.columnIds],
211
+ () => resolveVisibleColumns(paneSettings.columnIds, isPortfolioTab, { targetWeights: hasTargetWeights(currentPortfolio), otherCurrencies }),
212
+ [currentPortfolio, isPortfolioTab, otherCurrencies, paneSettings.columnIds],
202
213
  );
203
214
  const supplementalData = usePortfolioSupplementalData(tickers, columns, appActive, financialsMap);
204
215
  // One lookup for the whole list, only while a perpetual column is on screen.
@@ -210,11 +221,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
210
221
  [columns],
211
222
  );
212
223
 
213
- // Totals, money columns and account values are in the portfolio's currency.
214
- const totalsCurrency = useMemo(
215
- () => resolvePortfolioTotalsCurrency(currentPortfolio, config.baseCurrency),
216
- [config.baseCurrency, currentPortfolio],
217
- );
218
224
  // The broker account's cash when it reports one, else the cash entered by hand.
219
225
  const cashLine = useMemo(
220
226
  () => isPortfolioTab ? resolvePortfolioCash(currentPortfolio, accountState?.account) : null,
@@ -420,10 +426,20 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
420
426
  cancelPendingCursorSymbol();
421
427
  setCurrentCollectionId(collectionId);
422
428
  }, [cancelPendingCursorSymbol, setCurrentCollectionId]);
423
- const collectionTabs = useMemo(
424
- () => visibleCollections.map((collection) => ({ label: collection.name, value: collection.id })),
425
- [visibleCollections],
426
- );
429
+ const { getBrokerAdapter, getBrokerLogo } = usePluginBrokerActions();
430
+ const desktop = useUiCapabilities().nativePaneChrome === true;
431
+ const collectionTabs = useMemo(() => visibleCollections.map((collection) => {
432
+ const portfolio = desktop && collection.kind === "portfolio"
433
+ ? config.portfolios.find((entry) => entry.id === collection.id)
434
+ : undefined;
435
+ // A broker account's tab leads with its broker's logo on the desktop and the web.
436
+ const logo = portfolio ? brokerPortfolioLogo(portfolio, config.brokerInstances, getBrokerAdapter, getBrokerLogo) : null;
437
+ return {
438
+ label: collection.name,
439
+ value: collection.id,
440
+ ...(logo ? { leading: <LogoMark src={logo.src} name={logo.name} /> } : {}),
441
+ };
442
+ }), [config.brokerInstances, config.portfolios, desktop, getBrokerAdapter, getBrokerLogo, visibleCollections]);
427
443
  const { strip: tabStrip, rows: headerHeight } = usePaneTabs(showCollectionTabs
428
444
  ? {
429
445
  tabs: collectionTabs,
@@ -716,9 +732,12 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
716
732
  ? `${cursorSymbol} market cap: ${describeFundamentalMarketCap(selectedCap.provenance)}.` : undefined;
717
733
  const multiplierNotice = isPortfolioTab && selectedTicker && hasUnknownOptionMultiplier(selectedTicker, activeCollectionId)
718
734
  ? `${cursorSymbol} has no contract multiplier; its value assumes 1x.` : undefined;
735
+ // The row's change cells say DELISTED; the notice gives its last session.
736
+ const selectedDelisting = selectedFinancials?.quote?.delisting;
737
+ const delistedNotice = selectedDelisting ? `${cursorSymbol}: ${delistingLabel(selectedDelisting)}.` : undefined;
719
738
  usePaneNoticeFooter({
720
739
  registrationId: "portfolio-list-notices",
721
- notices: [...summaryNotices, capNotice, multiplierNotice].filter((notice): notice is string => !!notice),
740
+ notices: [...summaryNotices, delistedNotice, capNotice, multiplierNotice].filter((notice): notice is string => !!notice),
722
741
  focused: focused && !quickAddFocused,
723
742
  });
724
743
  const contentHeight = Math.max(1, height - headerHeight - summaryHeight - drawerHeight - quickAddHeight);