gloomberb 0.9.7 → 0.9.9

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 (276) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.test.ts +3 -3
  4. package/src/api-client/index.ts +48 -6
  5. package/src/api-client/request.test.ts +63 -0
  6. package/src/api-client/request.ts +53 -5
  7. package/src/api-client/types.ts +45 -2
  8. package/src/app/pane-runtime/layout-placement.ts +0 -1
  9. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  10. package/src/app/runtime/desktop-deeplink.ts +496 -0
  11. package/src/app.tsx +34 -0
  12. package/src/brokers/account-cache.ts +2 -10
  13. package/src/brokers/broker-portfolio-sync.ts +0 -1
  14. package/src/brokers/broker-ticker-sync.ts +20 -16
  15. package/src/brokers/remote-broker-adapter.ts +3 -0
  16. package/src/capabilities/registry.test.ts +25 -0
  17. package/src/capabilities/registry.ts +22 -5
  18. package/src/cli/commands/automation.ts +1 -3
  19. package/src/cli/commands/command-utils.ts +0 -7
  20. package/src/cli/commands/overview.ts +2 -2
  21. package/src/cli/commands/plugins.ts +34 -17
  22. package/src/cli/index.ts +2 -2
  23. package/src/cli.test.ts +0 -157
  24. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  25. package/src/components/chart/bar-chart-renderer.ts +111 -55
  26. package/src/components/chart/comparison/renderer.ts +0 -1
  27. package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
  28. package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
  29. package/src/components/chart/core/index.test.ts +0 -24
  30. package/src/components/chart/core/renderer.ts +0 -4
  31. package/src/components/chart/core/resolution.test.ts +0 -17
  32. package/src/components/chart/core/resolution.ts +0 -26
  33. package/src/components/chart/core/scroll.test.ts +0 -6
  34. package/src/components/chart/core/scroll.ts +0 -22
  35. package/src/components/chart/core/viewport.test.ts +2 -23
  36. package/src/components/chart/core/viewport.ts +0 -27
  37. package/src/components/chart/market-session.ts +1 -1
  38. package/src/components/chart/native/surface/hooks.ts +10 -3
  39. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  40. package/src/components/chart/stock-chart/data-runtime.ts +0 -1
  41. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  42. package/src/components/chart/stock-chart/footer.ts +68 -13
  43. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  44. package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
  45. package/src/components/command-bar/helpers.test.ts +1 -80
  46. package/src/components/command-bar/helpers.ts +1 -36
  47. package/src/components/command-bar/pane-settings.ts +1 -1
  48. package/src/components/command-bar/pane-templates/items.ts +11 -3
  49. package/src/components/command-bar/panel/runtime.ts +0 -2
  50. package/src/components/command-bar/routes/root/results.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
  52. package/src/components/command-bar/surface/index.test.tsx +52 -16
  53. package/src/components/command-bar/surface/index.tsx +0 -1
  54. package/src/components/command-bar/workflow/ops.ts +81 -20
  55. package/src/components/feed-data-table/stack-view.test.tsx +36 -130
  56. package/src/components/feed-data-table/stack-view.tsx +0 -1
  57. package/src/components/index.ts +1 -0
  58. package/src/components/layout/shell/index.test.tsx +2 -113
  59. package/src/components/layout/shell/index.tsx +0 -1
  60. package/src/components/layout/shell/menu.ts +0 -1
  61. package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
  62. package/src/components/onboarding/onboarding-wizard.tsx +3 -3
  63. package/src/components/onboarding/wizard-keyboard.ts +2 -23
  64. package/src/components/price-selector-dialog.tsx +3 -3
  65. package/src/components/ticker/badge/text.tsx +0 -1
  66. package/src/components/toggle-list.tsx +29 -73
  67. package/src/components/ui/button.tsx +4 -1
  68. package/src/components/ui/checkbox.tsx +76 -0
  69. package/src/components/ui/choice-dialog.tsx +15 -3
  70. package/src/components/ui/frame.tsx +1 -1
  71. package/src/components/ui/index.ts +2 -0
  72. package/src/components/ui/message-composer.tsx +4 -0
  73. package/src/components/ui/native-select.tsx +4 -2
  74. package/src/components/ui/page-stack-view.tsx +1 -1
  75. package/src/components/ui/ui.test.tsx +0 -78
  76. package/src/core/state/app/layout-reducer.ts +0 -1
  77. package/src/data/config/layout.ts +3 -3
  78. package/src/data/resource-store.ts +6 -0
  79. package/src/data/sqlite/cache.test.ts +5 -0
  80. package/src/data/ticker-store.ts +0 -7
  81. package/src/market-data/coordinator/auxiliary.ts +1 -44
  82. package/src/market-data/coordinator/entries.ts +0 -1
  83. package/src/market-data/coordinator/events.test.ts +49 -0
  84. package/src/market-data/coordinator/events.ts +25 -4
  85. package/src/market-data/coordinator/index.test.ts +223 -10
  86. package/src/market-data/coordinator/index.ts +41 -21
  87. package/src/market-data/coordinator/quotes.ts +13 -11
  88. package/src/market-data/coordinator/subscriptions.test.ts +1 -0
  89. package/src/market-data/hooks.tsx +17 -13
  90. package/src/market-data/market/freshness.ts +58 -1
  91. package/src/market-data/market/status.ts +19 -1
  92. package/src/market-data/query-store.ts +0 -4
  93. package/src/market-data/quotes/freshness.test.ts +49 -1
  94. package/src/market-data/quotes/freshness.ts +8 -0
  95. package/src/market-data/quotes/resolution.test.ts +185 -0
  96. package/src/market-data/quotes/resolution.ts +96 -10
  97. package/src/market-data/request-types.test.ts +61 -1
  98. package/src/market-data/request-types.ts +42 -8
  99. package/src/market-data/result-types.ts +0 -11
  100. package/src/market-data/selectors.ts +1 -5
  101. package/src/news/aggregator.ts +5 -1
  102. package/src/pane-settings.test.ts +0 -18
  103. package/src/plugins/builtin/account-management/footer.ts +2 -5
  104. package/src/plugins/builtin/account-management/form-components.tsx +175 -59
  105. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  106. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  107. package/src/plugins/builtin/account-management/model.ts +259 -23
  108. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  109. package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
  110. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  111. package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
  112. package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
  113. package/src/plugins/builtin/buildout/format.ts +2 -11
  114. package/src/plugins/builtin/chat/chat.test.tsx +127 -1
  115. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  116. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  117. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  118. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  119. package/src/plugins/builtin/chat/content/index.tsx +153 -16
  120. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  121. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  122. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  123. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  124. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  125. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  126. package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
  127. package/src/plugins/builtin/chat/controller/index.ts +0 -10
  128. package/src/plugins/builtin/chat/controller.test.ts +5 -5
  129. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  131. package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
  132. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  133. package/src/plugins/builtin/chat/test-harness.tsx +22 -12
  134. package/src/plugins/builtin/cloud/plugin.tsx +8 -1
  135. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
  136. package/src/plugins/builtin/congress-trades/model.ts +2 -7
  137. package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
  138. package/src/plugins/builtin/correlation/index.tsx +4 -1
  139. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  140. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  141. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  142. package/src/plugins/builtin/earnings/index.tsx +0 -9
  143. package/src/plugins/builtin/econ/index.tsx +1 -5
  144. package/src/plugins/builtin/help/index.tsx +2 -2
  145. package/src/plugins/builtin/holders/pane.tsx +0 -5
  146. package/src/plugins/builtin/insider/index.tsx +1 -8
  147. package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
  148. package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
  149. package/src/plugins/builtin/market-movers/index.tsx +0 -2
  150. package/src/plugins/builtin/news/index.tsx +0 -1
  151. package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
  152. package/src/plugins/builtin/options/view.test.tsx +1 -38
  153. package/src/plugins/builtin/plugin-groups.ts +17 -3
  154. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
  155. package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
  156. package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
  157. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  158. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  159. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  160. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  161. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  162. package/src/plugins/builtin/sec/filing-documents.ts +0 -11
  163. package/src/plugins/builtin/sec/index.tsx +0 -2
  164. package/src/plugins/builtin/substack/article-detail.tsx +1 -1
  165. package/src/plugins/builtin/thirteenf/api.ts +0 -15
  166. package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
  167. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
  168. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  169. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  170. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  171. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  172. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  173. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
  174. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  175. package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
  176. package/src/plugins/builtin/world-indices/index.tsx +0 -4
  177. package/src/plugins/builtin/world-indices/model.ts +0 -7
  178. package/src/plugins/ibkr/account-cache.ts +1 -1
  179. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  180. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  181. package/src/plugins/ibkr/portfolio-performance.ts +1 -1
  182. package/src/plugins/ibkr/trade/utils.ts +1 -7
  183. package/src/plugins/loader.ts +2 -1
  184. package/src/plugins/prediction-markets/detail.test.tsx +0 -34
  185. package/src/plugins/registry/context.ts +15 -0
  186. package/src/plugins/registry/contributions.ts +22 -11
  187. package/src/plugins/registry/index.test.ts +44 -0
  188. package/src/plugins/registry/index.ts +75 -26
  189. package/src/remote/controller.ts +1 -7
  190. package/src/remote/json-patch.ts +15 -1
  191. package/src/remote/server.ts +16 -6
  192. package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
  193. package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
  194. package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
  195. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
  196. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  197. package/src/renderers/electrobun/bun/index.ts +42 -0
  198. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  199. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  200. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
  201. package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
  202. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  203. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  204. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  205. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  206. package/src/renderers/electrobun/view/host/mouse.ts +0 -11
  207. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  208. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  209. package/src/renderers/electrobun/view/http-fetch.ts +15 -2
  210. package/src/renderers/electrobun/view/main.tsx +3 -0
  211. package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
  212. package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
  213. package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
  214. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  215. package/src/renderers/opentui/host.tsx +11 -3
  216. package/src/renderers/opentui/input-host.tsx +5 -1
  217. package/src/sources/gloomberb-cloud/index.test.ts +0 -72
  218. package/src/sources/gloomberb-cloud/index.ts +1 -30
  219. package/src/sources/gloomberb-cloud/news.ts +0 -12
  220. package/src/sources/provider-router/batches.ts +11 -3
  221. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  222. package/src/sources/provider-router/financial-routes.ts +77 -5
  223. package/src/sources/provider-router/financials.test.ts +49 -0
  224. package/src/sources/provider-router/financials.ts +40 -3
  225. package/src/sources/provider-router/index.test.ts +314 -0
  226. package/src/sources/provider-router/primary.ts +17 -3
  227. package/src/sources/provider-router/search.ts +5 -0
  228. package/src/sources/provider-router/streaming.ts +6 -1
  229. package/src/sources/provider-router/supplemental.ts +4 -3
  230. package/src/sources/sec-edgar.test.ts +42 -0
  231. package/src/sources/sec-edgar.ts +1 -2
  232. package/src/sources/yahoo-finance/history.ts +5 -1
  233. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  234. package/src/sources/yahoo-finance/mappers.ts +2 -1
  235. package/src/sources/yahoo-finance/options.ts +59 -14
  236. package/src/sources/yahoo-finance/quote-summary.ts +4 -1
  237. package/src/sources/yahoo-finance/requests.ts +2 -1
  238. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  239. package/src/sources/yahoo-finance.test.ts +40 -21
  240. package/src/sources/yahoo-finance.ts +19 -10
  241. package/src/state/app/context/index.test.tsx +0 -81
  242. package/src/state/app/context/selector.test.tsx +6 -2
  243. package/src/state/hooks/quote-streaming.test.ts +14 -1
  244. package/src/state/hooks/quote-streaming.ts +5 -4
  245. package/src/sync/controller.test.ts +31 -0
  246. package/src/sync/controller.ts +300 -0
  247. package/src/sync/core-contributors.test.ts +251 -0
  248. package/src/sync/core-contributors.ts +400 -0
  249. package/src/sync/profile-analytics.ts +39 -0
  250. package/src/sync/react.ts +53 -0
  251. package/src/sync/types.ts +77 -0
  252. package/src/test-support/data-provider.ts +0 -1
  253. package/src/types/desktop-deeplink.ts +7 -0
  254. package/src/types/plugin.ts +3 -0
  255. package/src/ui/host.tsx +15 -1
  256. package/src/ui/index.tsx +2 -0
  257. package/src/updater.test.ts +0 -21
  258. package/src/updater.ts +20 -16
  259. package/src/utils/async-deadline.ts +26 -0
  260. package/src/utils/exchanges.test.ts +11 -1
  261. package/src/utils/exchanges.ts +5 -1
  262. package/src/utils/selection-clipboard.ts +2 -2
  263. package/src/version.ts +1 -1
  264. package/src/components/layout/pane/header.test.tsx +0 -39
  265. package/src/plugins/builtin/chat/channels.test.ts +0 -28
  266. package/src/plugins/builtin/chat/index.tsx +0 -4
  267. package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
  268. package/src/plugins/builtin/earnings/index.test.ts +0 -12
  269. package/src/plugins/builtin/help/index.test.tsx +0 -242
  270. package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
  271. package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
  272. package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
  273. package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
  274. package/src/renderers/electrobun/view/host/text.test.ts +0 -22
  275. package/src/renderers/opentui/dialog-host.test.tsx +0 -44
  276. /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
@@ -1,5 +1,5 @@
1
1
  import type { MarketDataRequestContext } from "../../types/data-provider";
2
- import type { OptionContract, OptionsChain, Quote } from "../../types/financials";
2
+ import type { MarketState, OptionContract, OptionsChain, Quote } from "../../types/financials";
3
3
  import { parseOptionSymbol } from "../../utils/options";
4
4
  import { getYahooSymbolsToTry } from "./symbols";
5
5
 
@@ -14,16 +14,40 @@ interface LoadYahooOptionsChainOptions {
14
14
 
15
15
  interface GetYahooOptionQuoteOptions {
16
16
  context?: MarketDataRequestContext;
17
- getOptionsChain: (
17
+ getOptionsChainResult: (
18
18
  ticker: string,
19
19
  exchange?: string,
20
20
  expirationDate?: number,
21
21
  context?: MarketDataRequestContext,
22
- ) => Promise<OptionsChain>;
22
+ ) => Promise<YahooOptionsChainResult>;
23
23
  providerId: string;
24
24
  ticker: string;
25
25
  }
26
26
 
27
+ export interface YahooOptionsChainResult {
28
+ chain: OptionsChain;
29
+ underlyingMarketState?: MarketState;
30
+ }
31
+
32
+ const MARKET_STATES = new Set<MarketState>([
33
+ "PRE",
34
+ "REGULAR",
35
+ "POST",
36
+ "PREPRE",
37
+ "POSTPOST",
38
+ "CLOSED",
39
+ ]);
40
+
41
+ function normalizeYahooMarketState(value: unknown): MarketState | undefined {
42
+ const normalized = typeof value === "string" ? value.toUpperCase() as MarketState : undefined;
43
+ return normalized && MARKET_STATES.has(normalized) ? normalized : undefined;
44
+ }
45
+
46
+ function deriveOptionMarketState(underlyingMarketState?: MarketState): MarketState | undefined {
47
+ if (!underlyingMarketState) return undefined;
48
+ return underlyingMarketState === "REGULAR" ? "REGULAR" : "CLOSED";
49
+ }
50
+
27
51
  function mapYahooOptionContract(raw: Record<string, any>): OptionContract {
28
52
  return {
29
53
  contractSymbol: raw.contractSymbol ?? "",
@@ -43,12 +67,12 @@ function mapYahooOptionContract(raw: Record<string, any>): OptionContract {
43
67
  };
44
68
  }
45
69
 
46
- export async function loadYahooOptionsChain({
70
+ export async function loadYahooOptionsChainResult({
47
71
  exchange,
48
72
  expirationDate,
49
73
  fetchJsonWithCrumb,
50
74
  ticker,
51
- }: LoadYahooOptionsChainOptions): Promise<OptionsChain> {
75
+ }: LoadYahooOptionsChainOptions): Promise<YahooOptionsChainResult> {
52
76
  const symbolsToTry = getYahooSymbolsToTry(ticker, exchange);
53
77
  let lastError: any;
54
78
 
@@ -62,6 +86,7 @@ export async function loadYahooOptionsChain({
62
86
  result?: Array<{
63
87
  underlyingSymbol?: string;
64
88
  expirationDates?: number[];
89
+ quote?: { marketState?: unknown };
65
90
  options?: Array<{
66
91
  calls?: Array<Record<string, any>>;
67
92
  puts?: Array<Record<string, any>>;
@@ -75,10 +100,13 @@ export async function loadYahooOptionsChain({
75
100
 
76
101
  const opts = result.options?.[0];
77
102
  return {
78
- underlyingSymbol: result.underlyingSymbol ?? symbol,
79
- expirationDates: result.expirationDates ?? [],
80
- calls: (opts?.calls ?? []).map((contract) => mapYahooOptionContract(contract)),
81
- puts: (opts?.puts ?? []).map((contract) => mapYahooOptionContract(contract)),
103
+ chain: {
104
+ underlyingSymbol: result.underlyingSymbol ?? symbol,
105
+ expirationDates: result.expirationDates ?? [],
106
+ calls: (opts?.calls ?? []).map((contract) => mapYahooOptionContract(contract)),
107
+ puts: (opts?.puts ?? []).map((contract) => mapYahooOptionContract(contract)),
108
+ },
109
+ underlyingMarketState: normalizeYahooMarketState(result.quote?.marketState),
82
110
  };
83
111
  } catch (err) {
84
112
  lastError = err;
@@ -87,15 +115,26 @@ export async function loadYahooOptionsChain({
87
115
  throw lastError || new Error(`No options chain for ${ticker}`);
88
116
  }
89
117
 
118
+ export async function loadYahooOptionsChain(
119
+ options: LoadYahooOptionsChainOptions,
120
+ ): Promise<OptionsChain> {
121
+ return (await loadYahooOptionsChainResult(options)).chain;
122
+ }
123
+
90
124
  export async function getYahooOptionQuote({
91
125
  context,
92
- getOptionsChain,
126
+ getOptionsChainResult,
93
127
  providerId,
94
128
  ticker,
95
129
  }: GetYahooOptionQuoteOptions): Promise<Quote> {
96
130
  const parsed = parseOptionSymbol(ticker);
97
131
  if (!parsed) throw new Error(`Unsupported option symbol ${ticker}`);
98
- const chain = await getOptionsChain(parsed.underlying, "", parsed.expTs, context);
132
+ const { chain, underlyingMarketState } = await getOptionsChainResult(
133
+ parsed.underlying,
134
+ "",
135
+ parsed.expTs,
136
+ context,
137
+ );
99
138
  const contracts = parsed.side === "C" ? chain.calls : chain.puts;
100
139
  const contract = contracts.find((candidate) =>
101
140
  Math.abs(candidate.strike - parsed.strike) < 0.001 &&
@@ -110,6 +149,12 @@ export async function getYahooOptionQuote({
110
149
  : contract.ask > 0
111
150
  ? contract.ask
112
151
  : undefined;
152
+ const marketState = deriveOptionMarketState(underlyingMarketState);
153
+ const lastUpdated = mark != null
154
+ ? Date.now()
155
+ : contract.lastTradeDate > 0
156
+ ? contract.lastTradeDate * 1000
157
+ : Date.now();
113
158
  return {
114
159
  symbol: ticker,
115
160
  providerId,
@@ -122,13 +167,13 @@ export async function getYahooOptionQuote({
122
167
  ask: contract.ask,
123
168
  mark,
124
169
  name: contract.contractSymbol,
125
- lastUpdated: contract.lastTradeDate > 0
126
- ? contract.lastTradeDate * 1000
127
- : Date.now(),
170
+ lastUpdated,
128
171
  exchangeName: "OPTIONS",
129
172
  fullExchangeName: "OPTIONS",
130
173
  listingExchangeName: "OPTIONS",
131
174
  listingExchangeFullName: "OPTIONS",
175
+ marketState,
176
+ sessionConfidence: marketState ? "derived" : "unknown",
132
177
  dataSource: "delayed",
133
178
  };
134
179
  }
@@ -29,7 +29,10 @@ interface YahooQuoteSummaryOptions {
29
29
  }
30
30
 
31
31
  interface YahooCorporateActionsOptions extends YahooQuoteSummaryOptions {
32
- fetchChart: (symbol: string, range: string, interval?: string) => Promise<ChartResult>;
32
+ fetchChart: (symbol: string, range: string, interval?: string) => Promise<{
33
+ meta: NonNullable<ChartResult["meta"]>;
34
+ events?: ChartResult["events"];
35
+ }>;
33
36
  }
34
37
 
35
38
  export async function loadYahooHolders({
@@ -65,6 +65,7 @@ export async function fetchYahooExtendedHoursData(
65
65
  http: YahooHttpClient,
66
66
  symbol: string,
67
67
  meta: NonNullable<ChartResult["meta"]>,
68
+ regularClose?: number,
68
69
  ): Promise<ExtendedHoursData> {
69
70
  const marketState = deriveMarketState(meta);
70
71
  if (marketState !== "PRE" && marketState !== "POST") return {};
@@ -80,7 +81,7 @@ export async function fetchYahooExtendedHoursData(
80
81
  const result = data.chart?.result?.[0];
81
82
  if (!result?.timestamp?.length) return {};
82
83
  const closes = result.indicators?.quote?.[0]?.close || [];
83
- return extractExtendedHoursPrices(meta, result.timestamp, closes, marketState);
84
+ return extractExtendedHoursPrices(meta, result.timestamp, closes, marketState, regularClose);
84
85
  } catch {
85
86
  return {};
86
87
  }
@@ -42,6 +42,7 @@ interface YahooSnapshotLoaders {
42
42
  fetchExtendedHoursData: (
43
43
  symbol: string,
44
44
  meta: NonNullable<ChartResult["meta"]>,
45
+ regularClose?: number,
45
46
  ) => Promise<ExtendedHoursData>;
46
47
  fetchQuoteSupplement: (
47
48
  symbol: string,
@@ -132,7 +133,12 @@ export async function loadYahooTickerFinancials(
132
133
  const changePct = prev ? (change / prev) * 100 : 0;
133
134
 
134
135
  const marketState = deriveMarketState(meta);
135
- const extHours = await loaders.fetchExtendedHoursData(symbol, meta);
136
+ const extendedHoursBase = quoteSupplement.previousClose ?? prev;
137
+ const extHours = await loaders.fetchExtendedHoursData(
138
+ symbol,
139
+ meta,
140
+ extendedHoursBase == null ? undefined : extendedHoursBase * currencyDivisor,
141
+ );
136
142
  if (currencyDivisor !== 1) {
137
143
  normalizeExtendedHoursPrices(extHours, currencyDivisor);
138
144
  }
@@ -207,7 +213,12 @@ export async function loadYahooQuote(
207
213
  const change = prev != null ? price - prev : 0;
208
214
 
209
215
  const marketState = deriveMarketState(meta);
210
- const extHours = await loaders.fetchExtendedHoursData(symbol, meta);
216
+ const extendedHoursBase = quoteSupplement.previousClose ?? prev;
217
+ const extHours = await loaders.fetchExtendedHoursData(
218
+ symbol,
219
+ meta,
220
+ extendedHoursBase == null ? undefined : extendedHoursBase * currencyDivisor,
221
+ );
211
222
  if (currencyDivisor !== 1) {
212
223
  normalizeExtendedHoursPrices(extHours, currencyDivisor);
213
224
  }
@@ -135,28 +135,38 @@ describe("YahooFinanceClient exchange aliases", () => {
135
135
 
136
136
  test("maps IBKR option symbols to Yahoo option quote marks", async () => {
137
137
  const provider = new YahooFinanceClient() as any;
138
- provider.getOptionsChain = async (ticker: string, _exchange: string, expirationDate: number) => ({
139
- underlyingSymbol: ticker,
140
- expirationDates: [expirationDate],
141
- calls: [{
142
- contractSymbol: "AMD270917C00230000",
143
- strike: 230,
144
- currency: "USD",
145
- lastPrice: 251,
146
- change: 1.5,
147
- percentChange: 0.6,
148
- volume: 10,
149
- openInterest: 20,
150
- bid: 250.25,
151
- ask: 254.5,
152
- impliedVolatility: 0.4,
153
- inTheMoney: false,
154
- expiration: expirationDate,
155
- lastTradeDate: 1_800_000_000,
156
- }],
157
- puts: [],
138
+ const expiration = 1_821_139_200;
139
+ let underlyingMarketState = "POST";
140
+ provider.http.fetchJsonWithCrumb = async () => ({
141
+ optionChain: {
142
+ result: [{
143
+ underlyingSymbol: "AMD",
144
+ expirationDates: [expiration],
145
+ quote: { marketState: underlyingMarketState },
146
+ options: [{
147
+ calls: [{
148
+ contractSymbol: "AMD270917C00230000",
149
+ strike: 230,
150
+ currency: "USD",
151
+ lastPrice: 251,
152
+ change: 1.5,
153
+ percentChange: 0.6,
154
+ volume: 10,
155
+ openInterest: 20,
156
+ bid: 250.25,
157
+ ask: 254.5,
158
+ impliedVolatility: 0.4,
159
+ inTheMoney: false,
160
+ expiration,
161
+ lastTradeDate: 1_800_000_000,
162
+ }],
163
+ puts: [],
164
+ }],
165
+ }],
166
+ },
158
167
  });
159
168
 
169
+ const requestedAt = Date.now();
160
170
  const quote = await provider.getQuote("AMD 270917C00230000");
161
171
 
162
172
  expect(quote).toMatchObject({
@@ -166,8 +176,17 @@ describe("YahooFinanceClient exchange aliases", () => {
166
176
  bid: 250.25,
167
177
  ask: 254.5,
168
178
  providerId: "yahoo",
179
+ marketState: "CLOSED",
180
+ sessionConfidence: "derived",
181
+ });
182
+ expect(quote.lastUpdated).toBeGreaterThanOrEqual(requestedAt);
183
+
184
+ underlyingMarketState = "REGULAR";
185
+ const regularQuote = await provider.getQuote("AMD 270917C00230000");
186
+ expect(regularQuote).toMatchObject({
187
+ marketState: "REGULAR",
188
+ sessionConfidence: "derived",
169
189
  });
170
- expect(quote.lastUpdated).toBe(1_800_000_000_000);
171
190
  });
172
191
 
173
192
  test("preserves analyst rating price targets from upgrade history", async () => {
@@ -31,6 +31,7 @@ import {
31
31
  import {
32
32
  getYahooOptionQuote,
33
33
  loadYahooOptionsChain,
34
+ loadYahooOptionsChainResult,
34
35
  } from "./yahoo-finance/options";
35
36
  import {
36
37
  loadYahooAnalystResearch,
@@ -103,8 +104,12 @@ export class YahooFinanceClient implements DataProvider {
103
104
  }
104
105
 
105
106
  /** Fetch extended hours data using 1d intraday chart with pre/post market included */
106
- private async fetchExtendedHoursData(symbol: string, meta: NonNullable<ChartResult["meta"]>) {
107
- return fetchYahooExtendedHoursData(this.http, symbol, meta);
107
+ private async fetchExtendedHoursData(
108
+ symbol: string,
109
+ meta: NonNullable<ChartResult["meta"]>,
110
+ regularClose?: number,
111
+ ) {
112
+ return fetchYahooExtendedHoursData(this.http, symbol, meta, regularClose);
108
113
  }
109
114
 
110
115
  private async fetchTimeseries(symbol: string, types: string[], period1 = "2010-01-01") {
@@ -151,8 +156,13 @@ export class YahooFinanceClient implements DataProvider {
151
156
  if (parseOptionSymbol(ticker)) {
152
157
  return getYahooOptionQuote({
153
158
  context,
154
- getOptionsChain: (underlying, requestExchange, expirationDate, requestContext) => (
155
- this.getOptionsChain(underlying, requestExchange, expirationDate, requestContext)
159
+ getOptionsChainResult: (underlying, requestExchange, expirationDate) => (
160
+ loadYahooOptionsChainResult({
161
+ exchange: requestExchange ?? "",
162
+ expirationDate,
163
+ fetchJsonWithCrumb: (url) => this.http.fetchJsonWithCrumb(url),
164
+ ticker: underlying,
165
+ })
156
166
  ),
157
167
  providerId: this.id,
158
168
  ticker,
@@ -185,13 +195,12 @@ export class YahooFinanceClient implements DataProvider {
185
195
  fromCurrency = normalized;
186
196
  if (fromCurrency === "USD") return 1;
187
197
 
188
- try {
189
- const { meta, history } = await this.fetchChart(`${fromCurrency}USD=X`, "1mo");
190
- const rate = meta.regularMarketPrice || history[history.length - 1]?.close || 1;
191
- return rate;
192
- } catch {
193
- return 1;
198
+ const { meta, history } = await this.fetchChart(`${fromCurrency}USD=X`, "1mo");
199
+ const rate = meta.regularMarketPrice ?? history[history.length - 1]?.close;
200
+ if (typeof rate !== "number" || !Number.isFinite(rate) || rate <= 0) {
201
+ throw new Error(`No exchange rate data for ${fromCurrency}/USD`);
194
202
  }
203
+ return rate;
195
204
  }
196
205
 
197
206
  /** Search for a ticker by name/symbol - uses direct fetch (no retry) for speed */
@@ -4,87 +4,6 @@ import { appReducer, createInitialState } from "./index";
4
4
  import { removePane } from "../../../plugins/pane-manager";
5
5
 
6
6
  describe("appReducer command bar state", () => {
7
- test("opens the command bar with an explicit query", () => {
8
- const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
9
- const next = appReducer(state, { type: "SET_COMMAND_BAR", open: true, query: "Sync Broker Account" });
10
-
11
- expect(next.commandBarOpen).toBe(true);
12
- expect(next.commandBarQuery).toBe("Sync Broker Account");
13
- });
14
-
15
- test("clears the command bar query when closing", () => {
16
- const state = appReducer(
17
- createInitialState(createDefaultConfig("/tmp/gloomberb-test")),
18
- { type: "SET_COMMAND_BAR", open: true, query: "Disconnect Broker Account" },
19
- );
20
- const next = appReducer(state, { type: "SET_COMMAND_BAR", open: false });
21
-
22
- expect(next.commandBarOpen).toBe(false);
23
- expect(next.commandBarQuery).toBe("");
24
- });
25
-
26
- test("stores and clears command bar launch requests", () => {
27
- const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
28
- const opened = appReducer(initial, {
29
- type: "SET_COMMAND_BAR",
30
- open: true,
31
- launch: { kind: "plugin-command", commandId: "set-alert" },
32
- });
33
- const relaunched = appReducer(opened, {
34
- type: "SET_COMMAND_BAR",
35
- open: true,
36
- launch: { kind: "plugin-command", commandId: "set-alert" },
37
- });
38
- const closed = appReducer(relaunched, { type: "SET_COMMAND_BAR", open: false });
39
-
40
- expect(opened.commandBarOpen).toBe(true);
41
- expect(opened.commandBarLaunchRequest).toEqual({
42
- kind: "plugin-command",
43
- commandId: "set-alert",
44
- sequence: 1,
45
- });
46
- expect(relaunched.commandBarLaunchRequest?.sequence).toBe(2);
47
- expect(closed.commandBarLaunchRequest).toBeNull();
48
- });
49
-
50
- test("stores ticker search command bar launch requests", () => {
51
- const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
52
- const opened = appReducer(initial, {
53
- type: "SET_COMMAND_BAR",
54
- open: true,
55
- launch: { kind: "ticker-search", query: "" },
56
- });
57
-
58
- expect(opened.commandBarOpen).toBe(true);
59
- expect(opened.commandBarLaunchRequest).toEqual({
60
- kind: "ticker-search",
61
- query: "",
62
- sequence: 1,
63
- });
64
- });
65
-
66
- test("toggle opens blank and clears on close", () => {
67
- const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
68
- const opened = appReducer(initial, { type: "TOGGLE_COMMAND_BAR" });
69
- const closed = appReducer({ ...opened, commandBarQuery: "stale" }, { type: "TOGGLE_COMMAND_BAR" });
70
-
71
- expect(opened.commandBarOpen).toBe(true);
72
- expect(opened.commandBarQuery).toBe("");
73
- expect(closed.commandBarOpen).toBe(false);
74
- expect(closed.commandBarQuery).toBe("");
75
- });
76
-
77
- test("updates the live command bar query without closing the overlay", () => {
78
- const state = appReducer(
79
- createInitialState(createDefaultConfig("/tmp/gloomberb-test")),
80
- { type: "SET_COMMAND_BAR", open: true, query: "PL " },
81
- );
82
- const next = appReducer(state, { type: "SET_COMMAND_BAR_QUERY", query: "PL notes" });
83
-
84
- expect(next.commandBarOpen).toBe(true);
85
- expect(next.commandBarQuery).toBe("PL notes");
86
- });
87
-
88
7
  test("re-shows the gridlock tip and refreshes its sequence on every trigger", () => {
89
8
  const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
90
9
 
@@ -82,8 +82,12 @@ function createDesktopBridge(
82
82
  }
83
83
 
84
84
  describe("pane selectors", () => {
85
- afterEach(() => {
86
- testSetup?.renderer.destroy();
85
+ afterEach(async () => {
86
+ if (testSetup) {
87
+ await act(async () => {
88
+ testSetup!.renderer.destroy();
89
+ });
90
+ }
87
91
  testSetup = undefined;
88
92
  capturedDispatch = null;
89
93
  applyTheme("amber");
@@ -1,5 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { buildQuoteStreamSubscriptionKey } from "./quote-streaming";
2
+ import {
3
+ buildQuoteStreamSubscriptionKey,
4
+ normalizeQuoteStreamSubscriptionTarget,
5
+ } from "./quote-streaming";
3
6
 
4
7
  describe("buildQuoteStreamSubscriptionKey", () => {
5
8
  test("includes broker context so identical symbols can stream independently", () => {
@@ -28,4 +31,14 @@ describe("buildQuoteStreamSubscriptionKey", () => {
28
31
  expect(workKey).not.toBe(personalKey);
29
32
  });
30
33
 
34
+ test("keeps saved exchange aliases in stream targets", () => {
35
+ const target = normalizeQuoteStreamSubscriptionTarget({
36
+ symbol: "lpk",
37
+ exchange: "ibis",
38
+ });
39
+
40
+ expect(target?.symbol).toBe("LPK");
41
+ expect(target?.exchange).toBe("IBIS");
42
+ expect(buildQuoteStreamSubscriptionKey(target!)).toContain("LPK|IBIS");
43
+ });
31
44
  });
@@ -2,18 +2,18 @@ import { useEffect } from "react";
2
2
  import { useAppActive } from "../app/activity";
3
3
  import type { QuoteSubscriptionTarget } from "../../types/data-provider";
4
4
  import { debugLog } from "../../utils/debug-log";
5
- import { canonicalExchange, normalizeSymbol } from "../../utils/exchanges";
5
+ import { normalizeSymbol } from "../../utils/exchanges";
6
6
  import { getSharedMarketDataCoordinator } from "../../market-data/coordinator";
7
7
 
8
8
  const quoteStreamLog = debugLog.createLogger("quote-stream");
9
9
 
10
- function normalizeTarget(target: QuoteSubscriptionTarget): QuoteSubscriptionTarget | null {
10
+ export function normalizeQuoteStreamSubscriptionTarget(target: QuoteSubscriptionTarget): QuoteSubscriptionTarget | null {
11
11
  const symbol = normalizeSymbol(target.symbol);
12
12
  if (!symbol) return null;
13
13
  return {
14
14
  ...target,
15
15
  symbol,
16
- exchange: canonicalExchange(target.exchange),
16
+ exchange: target.exchange?.trim().toUpperCase() ?? "",
17
17
  };
18
18
  }
19
19
 
@@ -43,7 +43,7 @@ export function useQuoteStreaming(targets: QuoteSubscriptionTarget[]): void {
43
43
 
44
44
  const normalizedEntries = new Map<string, QuoteSubscriptionTarget>();
45
45
  for (const target of targets) {
46
- const normalized = normalizeTarget(target);
46
+ const normalized = normalizeQuoteStreamSubscriptionTarget(target);
47
47
  if (!normalized) continue;
48
48
  const key = buildQuoteStreamSubscriptionKey(normalized);
49
49
  normalizedEntries.set(key, normalized);
@@ -74,6 +74,7 @@ export function useQuoteStreaming(targets: QuoteSubscriptionTarget[]): void {
74
74
  instrument: target.context?.instrument ?? null,
75
75
  },
76
76
  priority: {
77
+ route: target.route,
77
78
  surface: target.surface,
78
79
  visible: target.visible,
79
80
  selected: target.selected,
@@ -0,0 +1,31 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { AppState } from "../core/state/app/state";
3
+ import type { TickerRepository } from "../data/ticker-repository";
4
+ import { CloudSyncController } from "./controller";
5
+ import type { SyncTransport } from "./types";
6
+
7
+ test("does not push local state when the initial pull fails", async () => {
8
+ let pushes = 0;
9
+ const transport: SyncTransport = {
10
+ id: "failing-pull",
11
+ isAvailable: () => true,
12
+ pullSnapshot: () => Promise.reject(new Error("pull failed")),
13
+ pushSnapshot: async () => {
14
+ pushes += 1;
15
+ return { revision: 1, updatedAt: new Date().toISOString() };
16
+ },
17
+ };
18
+ const controller = new CloudSyncController();
19
+ controller.setRuntime({
20
+ state: {} as AppState,
21
+ dispatch: () => {},
22
+ tickerRepository: {} as TickerRepository,
23
+ getContributors: () => [],
24
+ getTransport: () => ({ pluginId: "test", transport }),
25
+ });
26
+
27
+ await controller.requestSync({ force: true });
28
+
29
+ expect(pushes).toBe(0);
30
+ expect(controller.getStatus()).toMatchObject({ phase: "error", error: "pull failed" });
31
+ });