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,7 +1,6 @@
1
1
  import type { DataProvider, SecFilingDocument, SecFilingItem } from "../../types/data-provider";
2
- import type { NewsArticle } from "../../news/types";
3
2
  import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../../types/financials";
4
- import type { ChartRequest, InstrumentRef, NewsRequest, OptionsRequest, SecFilingsRequest } from "../request-types";
3
+ import type { ChartRequest, InstrumentRef, OptionsRequest, SecFilingsRequest } from "../request-types";
5
4
  import { QueryStore } from "../query-store";
6
5
  import type { QueryEntry } from "../result-types";
7
6
  import {
@@ -9,7 +8,6 @@ import {
9
8
  buildChartKey,
10
9
  buildFxKey,
11
10
  buildTickerFinancialsSnapshot,
12
- buildNewsKey,
13
11
  buildOptionsKey,
14
12
  buildQuoteKey,
15
13
  buildSecContentKey,
@@ -19,6 +17,8 @@ import {
19
17
  resolveEntryData,
20
18
  toMarketDataContext,
21
19
  } from "../selectors";
20
+ import { resolveTickerFinancialsQuoteState } from "../quotes/resolution";
21
+ import { hasLikelyQuoteUnitMismatch } from "../../utils/currency-units";
22
22
  import { normalizePriceHistory } from "../../utils/price-history";
23
23
  import {
24
24
  createBaselineChartRequest,
@@ -39,7 +39,6 @@ import {
39
39
  import {
40
40
  loadArticleSummaryEntry,
41
41
  loadFxRateEntry,
42
- loadNewsEntry,
43
42
  loadOptionsEntry,
44
43
  loadSecFilingContentEntry,
45
44
  loadSecFilingDocumentsEntry,
@@ -71,7 +70,6 @@ export class MarketDataCoordinator {
71
70
  private readonly fundamentalsStore = new QueryStore<TickerFinancials["fundamentals"]>((key) => this.events.bump(key));
72
71
  private readonly statementsStore = new QueryStore<Pick<TickerFinancials, "annualStatements" | "quarterlyStatements">>((key) => this.events.bump(key));
73
72
  private readonly chartStore = new QueryStore<PricePoint[]>((key) => this.events.bump(key));
74
- private readonly newsStore = new QueryStore<NewsArticle[]>((key) => this.events.bump(key));
75
73
  private readonly optionsStore = new QueryStore<OptionsChain>((key) => this.events.bump(key));
76
74
  private readonly secFilingsStore = new QueryStore<SecFilingItem[]>((key) => this.events.bump(key));
77
75
  private readonly secDocumentsStore = new QueryStore<SecFilingDocument[]>((key) => this.events.bump(key));
@@ -122,10 +120,6 @@ export class MarketDataCoordinator {
122
120
  return this.chartStore.get(buildChartKey(request));
123
121
  }
124
122
 
125
- getNewsEntry(request: NewsRequest): QueryEntry<NewsArticle[]> {
126
- return this.newsStore.get(buildNewsKey(request));
127
- }
128
-
129
123
  getOptionsEntry(request: OptionsRequest): QueryEntry<OptionsChain> {
130
124
  return this.optionsStore.get(buildOptionsKey(request));
131
125
  }
@@ -218,6 +212,7 @@ export class MarketDataCoordinator {
218
212
  options,
219
213
  quoteStore: this.quoteStore,
220
214
  runSingleFlight: (key, task) => this.runSingleFlight(key, task),
215
+ resolveQuote: (targetInstrument, quote) => this.resolveIncomingQuote(targetInstrument, quote),
221
216
  });
222
217
  }
223
218
 
@@ -231,6 +226,7 @@ export class MarketDataCoordinator {
231
226
  options,
232
227
  quoteStore: this.quoteStore,
233
228
  runSingleFlight: (key, task) => this.runSingleFlight(key, task),
229
+ resolveQuote: (targetInstrument, quote) => this.resolveIncomingQuote(targetInstrument, quote),
234
230
  });
235
231
  }
236
232
 
@@ -307,15 +303,6 @@ export class MarketDataCoordinator {
307
303
  });
308
304
  }
309
305
 
310
- async loadNews(request: NewsRequest): Promise<QueryEntry<NewsArticle[]>> {
311
- return loadNewsEntry({
312
- dataProvider: this.dataProvider,
313
- request,
314
- store: this.newsStore,
315
- runSingleFlight: (key, task) => this.runSingleFlight(key, task),
316
- });
317
- }
318
-
319
306
  async loadOptions(request: OptionsRequest): Promise<QueryEntry<OptionsChain>> {
320
307
  return loadOptionsEntry({
321
308
  dataProvider: this.dataProvider,
@@ -377,10 +364,43 @@ export class MarketDataCoordinator {
377
364
  private applyStreamQuote(instrument: InstrumentRef, quote: Quote): void {
378
365
  const key = buildQuoteKey(instrument);
379
366
  const current = this.quoteStore.get(key);
380
- if (areStreamQuotesEquivalent(current.data ?? current.lastGoodData, quote)) return;
367
+ const resolvedQuote = this.resolveIncomingQuote(instrument, quote);
368
+ if (areStreamQuotesEquivalent(current.data ?? current.lastGoodData, resolvedQuote)) return;
381
369
  const receivedAt = Date.now();
382
- const attempts = [createAttempt(quote.providerId ?? this.dataProvider.id, receivedAt, "success")];
383
- this.quoteStore.set(key, readyQuoteEntry(current, { ...quote, receivedAt }, quote.providerId ?? this.dataProvider.id, attempts));
370
+ const attempts = [createAttempt(resolvedQuote.providerId ?? this.dataProvider.id, receivedAt, "success")];
371
+ this.quoteStore.set(key, readyQuoteEntry(current, { ...resolvedQuote, receivedAt }, resolvedQuote.providerId ?? this.dataProvider.id, attempts));
372
+ }
373
+
374
+ private resolveIncomingQuote(instrument: InstrumentRef, quote: Quote): Quote {
375
+ const snapshot = resolveEntryData(this.snapshotStore.get(buildSnapshotKey(instrument)));
376
+ if (snapshot?.quote) {
377
+ if (hasLikelyQuoteUnitMismatch(snapshot.quote, quote)) return quote;
378
+ if (
379
+ typeof snapshot.quote.lastUpdated === "number"
380
+ && typeof quote.lastUpdated === "number"
381
+ && quote.lastUpdated < snapshot.quote.lastUpdated
382
+ ) {
383
+ return quote;
384
+ }
385
+ }
386
+ const cachedFinancials = snapshot ?? this.readCachedFinancialsForInstrument(instrument);
387
+ return resolveTickerFinancialsQuoteState(cachedFinancials, quote)?.quote ?? quote;
388
+ }
389
+
390
+ private readCachedFinancialsForInstrument(instrument: InstrumentRef): TickerFinancials | null {
391
+ if (!this.dataProvider.getCachedFinancialsForTargets) return null;
392
+ const result = this.dataProvider.getCachedFinancialsForTargets([{
393
+ symbol: instrument.symbol,
394
+ exchange: instrument.exchange,
395
+ brokerId: instrument.brokerId,
396
+ brokerInstanceId: instrument.brokerInstanceId,
397
+ instrument: instrument.instrument ?? null,
398
+ }], {
399
+ allowExpired: true,
400
+ includeStaleQuotes: true,
401
+ });
402
+ if (result instanceof Promise) return null;
403
+ return result.get(instrument.symbol.trim().toUpperCase()) ?? null;
384
404
  }
385
405
  }
386
406
 
@@ -15,7 +15,7 @@ import {
15
15
  readyQuoteEntry,
16
16
  } from "./entries";
17
17
 
18
- export type QuoteSubscriptionPriority = Pick<QuoteSubscriptionTarget, "surface" | "visible" | "selected" | "weight">;
18
+ export type QuoteSubscriptionPriority = Pick<QuoteSubscriptionTarget, "route" | "surface" | "visible" | "selected" | "weight">;
19
19
 
20
20
  export interface QuoteSubscriptionEntry {
21
21
  target: QuoteSubscriptionTarget;
@@ -123,6 +123,7 @@ interface LoadQuoteEntryOptions {
123
123
  options?: CoordinatorLoadOptions;
124
124
  quoteStore: QueryStore<Quote>;
125
125
  runSingleFlight: CoordinatorSingleFlight;
126
+ resolveQuote?: (instrument: InstrumentRef, quote: Quote) => Quote;
126
127
  }
127
128
 
128
129
  export async function loadQuoteEntry({
@@ -131,6 +132,7 @@ export async function loadQuoteEntry({
131
132
  options = {},
132
133
  quoteStore,
133
134
  runSingleFlight,
135
+ resolveQuote,
134
136
  }: LoadQuoteEntryOptions): Promise<QueryEntry<Quote>> {
135
137
  const key = buildQuoteKey(instrument);
136
138
  const flightKey = options.forceRefresh ? `${key}|refresh` : key;
@@ -146,9 +148,10 @@ export async function loadQuoteEntry({
146
148
  cacheMode: options.forceRefresh ? "refresh" : "default",
147
149
  },
148
150
  );
149
- const source = quote.providerId ?? dataProvider.id;
151
+ const resolvedQuote = resolveQuote?.(instrument, quote) ?? quote;
152
+ const source = resolvedQuote.providerId ?? dataProvider.id;
150
153
  const attempts = [createAttempt(source, startedAt, "success")];
151
- return quoteStore.update(key, (current) => readyQuoteEntry(current, quote, source, attempts));
154
+ return quoteStore.update(key, (current) => readyQuoteEntry(current, resolvedQuote, source, attempts));
152
155
  } catch (error) {
153
156
  const classified = classifyError(error);
154
157
  const attempt = createAttempt(dataProvider.id, startedAt, EXPECTED_EMPTY.test(classified.message) ? "empty" : "fatal_error", classified.reasonCode, classified.message);
@@ -163,6 +166,7 @@ interface LoadQuoteBatchEntriesOptions {
163
166
  options?: CoordinatorLoadOptions;
164
167
  quoteStore: QueryStore<Quote>;
165
168
  runSingleFlight: CoordinatorSingleFlight;
169
+ resolveQuote?: (instrument: InstrumentRef, quote: Quote) => Quote;
166
170
  }
167
171
 
168
172
  export async function loadQuoteBatchEntries({
@@ -171,6 +175,7 @@ export async function loadQuoteBatchEntries({
171
175
  options = {},
172
176
  quoteStore,
173
177
  runSingleFlight,
178
+ resolveQuote,
174
179
  }: LoadQuoteBatchEntriesOptions): Promise<QueryEntry<Quote>[]> {
175
180
  const uniqueInstruments = [...new Map(instruments.map((instrument) => [buildQuoteKey(instrument), instrument] as const)).values()];
176
181
  const results = new Map<string, QueryEntry<Quote>>();
@@ -195,9 +200,10 @@ export async function loadQuoteBatchEntries({
195
200
  const instrument = misses[index];
196
201
  if (!instrument || !item.quote) return;
197
202
  const key = buildQuoteKey(instrument);
198
- const source = item.quote.providerId ?? dataProvider.id;
203
+ const quote = resolveQuote?.(instrument, item.quote) ?? item.quote;
204
+ const source = quote.providerId ?? dataProvider.id;
199
205
  const attempts = [createAttempt(source, Date.now(), "success")];
200
- results.set(key, quoteStore.update(key, (current) => readyQuoteEntry(current, item.quote!, source, attempts)));
206
+ results.set(key, quoteStore.update(key, (current) => readyQuoteEntry(current, quote, source, attempts)));
201
207
  });
202
208
  }
203
209
 
@@ -210,6 +216,7 @@ export async function loadQuoteBatchEntries({
210
216
  options,
211
217
  quoteStore,
212
218
  runSingleFlight,
219
+ resolveQuote,
213
220
  }));
214
221
  }));
215
222
 
@@ -221,7 +228,6 @@ export class QuoteSubscriptionManager {
221
228
  private nextQuoteSubscriptionId = 1;
222
229
  private quoteSubscriptionDispose: (() => void) | null = null;
223
230
  private quoteSubscriptionSignature = "";
224
- private pendingQuoteSubscriptionKeys = new Set<string>();
225
231
 
226
232
  constructor(
227
233
  private readonly dataProvider: DataProvider,
@@ -243,7 +249,6 @@ export class QuoteSubscriptionManager {
243
249
  if (existing) {
244
250
  existing.targets.set(subscriptionId, target);
245
251
  existing.target = mergeQuoteSubscriptionTargets(existing.targets.values()) ?? target;
246
- this.pendingQuoteSubscriptionKeys.add(key);
247
252
  if (existing.removeTimer) {
248
253
  clearTimeout(existing.removeTimer);
249
254
  existing.removeTimer = null;
@@ -255,7 +260,6 @@ export class QuoteSubscriptionManager {
255
260
  targets: new Map([[subscriptionId, target]]),
256
261
  removeTimer: null,
257
262
  });
258
- this.pendingQuoteSubscriptionKeys.add(key);
259
263
  }
260
264
  this.flush();
261
265
 
@@ -267,7 +271,6 @@ export class QuoteSubscriptionManager {
267
271
  const mergedTarget = mergeQuoteSubscriptionTargets(existing.targets.values());
268
272
  if (mergedTarget) {
269
273
  existing.target = mergedTarget;
270
- this.pendingQuoteSubscriptionKeys.add(key);
271
274
  this.flush();
272
275
  continue;
273
276
  }
@@ -276,7 +279,6 @@ export class QuoteSubscriptionManager {
276
279
  const current = this.quoteSubscriptions.get(key);
277
280
  if (!current || current.targets.size > 0) return;
278
281
  this.quoteSubscriptions.delete(key);
279
- this.pendingQuoteSubscriptionKeys.delete(key);
280
282
  this.flush();
281
283
  }, QUOTE_SUBSCRIPTION_REMOVE_GRACE_MS);
282
284
  }
@@ -285,13 +287,13 @@ export class QuoteSubscriptionManager {
285
287
 
286
288
  private flush(): void {
287
289
  if (!this.dataProvider.subscribeQuotes) return;
288
- this.pendingQuoteSubscriptionKeys = new Set();
289
290
 
290
291
  const activeEntries = [...this.quoteSubscriptions.entries()]
291
292
  .filter(([, entry]) => entry.targets.size > 0 || entry.removeTimer)
292
293
  .sort(([left], [right]) => left.localeCompare(right));
293
294
  const nextSignature = activeEntries.map(([key, entry]) => [
294
295
  key,
296
+ entry.target.route ?? "",
295
297
  entry.target.surface ?? "",
296
298
  entry.target.visible ? "visible" : "",
297
299
  entry.target.selected ? "selected" : "",
@@ -40,6 +40,7 @@ function quote(symbol: string, price: number, overrides: Partial<Quote> = {}): Q
40
40
 
41
41
  async function flushCoordinator(): Promise<void> {
42
42
  await Promise.resolve();
43
+ await new Promise((resolve) => setTimeout(resolve, 0));
43
44
  }
44
45
 
45
46
  describe("MarketDataCoordinator key subscriptions", () => {
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "r
2
2
  import type { SecFilingDocument, SecFilingItem } from "../types/data-provider";
3
3
  import type { OptionsChain, PricePoint, Quote, TickerFinancials } from "../types/financials";
4
4
  import type { TickerRecord } from "../types/ticker";
5
- import type { ChartRequest, InstrumentRef, OptionsRequest, SecFilingsRequest } from "./request-types";
5
+ import type { ChartRequest, InstrumentRef, OptionsRequest, SecFilingsRequest, TickerInstrumentOptions } from "./request-types";
6
6
  import { instrumentFromTicker } from "./request-types";
7
7
  import { useAppActive } from "../state/app/activity";
8
8
  import {
@@ -74,17 +74,17 @@ function stableCurrencyList(currencies: Array<string | null | undefined>): strin
74
74
  )].sort((left, right) => left.localeCompare(right));
75
75
  }
76
76
 
77
- function buildTickerFinancialsMapKey(tickers: TickerRecord[]): string {
77
+ function buildTickerFinancialsMapKey(tickers: TickerRecord[], options: TickerInstrumentOptions = {}): string {
78
78
  return tickers.map((ticker) => {
79
- const instrument = ticker.metadata.broker_contracts?.[0];
79
+ const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, options);
80
80
  return [
81
81
  ticker.metadata.ticker,
82
82
  ticker.metadata.exchange ?? "",
83
83
  instrument?.brokerId ?? "",
84
84
  instrument?.brokerInstanceId ?? "",
85
- instrument?.conId ?? "",
86
- instrument?.localSymbol ?? "",
87
- instrument?.symbol ?? "",
85
+ instrument?.instrument?.conId ?? "",
86
+ instrument?.instrument?.localSymbol ?? "",
87
+ instrument?.instrument?.symbol ?? "",
88
88
  ].join("|");
89
89
  }).join("::");
90
90
  }
@@ -122,10 +122,10 @@ export function useTickerFinancials(symbol: string | null | undefined, ticker: T
122
122
  return financials;
123
123
  }
124
124
 
125
- function buildTickerFinancialsKeys(tickers: TickerRecord[]): string[] {
125
+ function buildTickerFinancialsKeys(tickers: TickerRecord[], options: TickerInstrumentOptions = {}): string[] {
126
126
  const keys: string[] = [];
127
127
  for (const ticker of tickers) {
128
- const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
128
+ const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, options);
129
129
  if (!instrument) continue;
130
130
  keys.push(
131
131
  buildSnapshotKey(instrument),
@@ -136,17 +136,21 @@ function buildTickerFinancialsKeys(tickers: TickerRecord[]): string[] {
136
136
  return keys;
137
137
  }
138
138
 
139
- export function useTickerFinancialsMap(tickers: TickerRecord[]): Map<string, TickerFinancials> {
139
+ export function useTickerFinancialsMap(
140
+ tickers: TickerRecord[],
141
+ options: TickerInstrumentOptions = {},
142
+ ): Map<string, TickerFinancials> {
140
143
  const coordinator = getSharedMarketDataCoordinator();
141
- const tickerKey = buildTickerFinancialsMapKey(tickers);
142
- const subscriptionKeys = useMemo(() => buildTickerFinancialsKeys(tickers), [tickerKey]);
144
+ const optionsKey = options.portfolioId ?? "";
145
+ const tickerKey = buildTickerFinancialsMapKey(tickers, options);
146
+ const subscriptionKeys = useMemo(() => buildTickerFinancialsKeys(tickers, options), [optionsKey, tickerKey]);
143
147
  const keysVersion = useCoordinatorKeysVersion(subscriptionKeys);
144
148
 
145
149
  return useMemo(() => {
146
150
  if (!coordinator) return new Map<string, TickerFinancials>();
147
151
  const result = new Map<string, TickerFinancials>();
148
152
  for (const ticker of tickers) {
149
- const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
153
+ const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker, options);
150
154
  if (!instrument) continue;
151
155
  const financials = coordinator.getTickerFinancialsSync(instrument);
152
156
  if (financials) {
@@ -154,7 +158,7 @@ export function useTickerFinancialsMap(tickers: TickerRecord[]): Map<string, Tic
154
158
  }
155
159
  }
156
160
  return result;
157
- }, [coordinator, keysVersion, tickerKey, subscriptionKeys]);
161
+ }, [coordinator, keysVersion, optionsKey, tickerKey, subscriptionKeys]);
158
162
  }
159
163
 
160
164
  export function useQuoteEntry(symbol: string | null | undefined, ticker: TickerRecord | null | undefined): QueryEntry<Quote> | null {
@@ -5,7 +5,33 @@ const US_EXTENDED_HOURS_EXCHANGES = new Set(["NASDAQ", "NYSE", "AMEX", "ARCA", "
5
5
  const ALWAYS_OPEN_EXCHANGES = new Set(["CCC"]);
6
6
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
7
7
  const OVERNIGHT_CLOSE_MAX_AGE_MS = 20 * 60 * 60 * 1000;
8
+ const REGULAR_OPEN_MINUTES: Record<string, number> = {
9
+ NASDAQ: 9 * 60 + 30,
10
+ NYSE: 9 * 60 + 30,
11
+ ARCA: 9 * 60 + 30,
12
+ AMEX: 9 * 60 + 30,
13
+ BATS: 9 * 60 + 30,
14
+ TSX: 9 * 60 + 30,
15
+ TSXV: 9 * 60 + 30,
16
+ CSE: 9 * 60 + 30,
17
+ FWB2: 8 * 60,
18
+ XETRA: 8 * 60,
19
+ LSE: 8 * 60,
20
+ EPA: 9 * 60,
21
+ AMS: 9 * 60,
22
+ BRU: 9 * 60,
23
+ LIS: 8 * 60,
24
+ BIT: 9 * 60,
25
+ SFB: 9 * 60,
26
+ HEL: 10 * 60,
27
+ CPH: 9 * 60,
28
+ JPX: 9 * 60,
29
+ HKEX: 9 * 60 + 30,
30
+ TWSE: 9 * 60,
31
+ NSE: 9 * 60 + 15,
32
+ };
8
33
  const exchangeLocalDateFormatters = new Map<string, Intl.DateTimeFormat>();
34
+ const exchangeLocalTimeFormatters = new Map<string, Intl.DateTimeFormat>();
9
35
  const usSessionFormatter = new Intl.DateTimeFormat("en-US", {
10
36
  timeZone: "America/New_York",
11
37
  weekday: "short",
@@ -34,6 +60,20 @@ function getExchangeLocalDateFormatter(timeZone: string): Intl.DateTimeFormat {
34
60
  return formatter;
35
61
  }
36
62
 
63
+ function getExchangeLocalTimeFormatter(timeZone: string): Intl.DateTimeFormat {
64
+ let formatter = exchangeLocalTimeFormatters.get(timeZone);
65
+ if (!formatter) {
66
+ formatter = new Intl.DateTimeFormat("en-US", {
67
+ timeZone,
68
+ hour: "2-digit",
69
+ minute: "2-digit",
70
+ hourCycle: "h23",
71
+ });
72
+ exchangeLocalTimeFormatters.set(timeZone, formatter);
73
+ }
74
+ return formatter;
75
+ }
76
+
37
77
  function exchangeLocalDate(exchange: string, timestampMs: number): string | null {
38
78
  const timeZone = EXCHANGE_TIME_ZONES[canonicalExchange(exchange)];
39
79
  if (!timeZone) return null;
@@ -46,6 +86,23 @@ function exchangeLocalDate(exchange: string, timestampMs: number): string | null
46
86
  return `${year}-${month}-${day}`;
47
87
  }
48
88
 
89
+ function exchangeLocalMinuteOfDay(exchange: string, timestampMs: number): number | null {
90
+ const timeZone = EXCHANGE_TIME_ZONES[canonicalExchange(exchange)];
91
+ if (!timeZone) return null;
92
+
93
+ const parts = getExchangeLocalTimeFormatter(timeZone).formatToParts(new Date(timestampMs));
94
+ const hour = Number(parts.find((part) => part.type === "hour")?.value);
95
+ const minute = Number(parts.find((part) => part.type === "minute")?.value);
96
+ return Number.isFinite(hour) && Number.isFinite(minute) ? hour * 60 + minute : null;
97
+ }
98
+
99
+ function isBeforeKnownRegularOpen(exchange: string, timestampMs: number): boolean {
100
+ const canonical = canonicalExchange(exchange);
101
+ const openMinute = REGULAR_OPEN_MINUTES[canonical];
102
+ const localMinute = exchangeLocalMinuteOfDay(canonical, timestampMs);
103
+ return openMinute != null && localMinute != null && localMinute < openMinute;
104
+ }
105
+
49
106
  function isoLocalDateToUtcDay(date: string): number | null {
50
107
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
51
108
  if (!match) return null;
@@ -122,7 +179,7 @@ function isTimestampStaleForExchangeSessionUnsafe(
122
179
  if (!timestampDate || !currentDate || timestampDate === currentDate) return false;
123
180
 
124
181
  if (ALWAYS_OPEN_EXCHANGES.has(canonical)) return true;
125
- if (marketState === "REGULAR") return true;
182
+ if (marketState === "REGULAR" && !isBeforeKnownRegularOpen(canonical, now)) return true;
126
183
 
127
184
  if (isUsExtendedHoursExchange(canonical)) {
128
185
  const session = usSessionState(now);
@@ -1,5 +1,7 @@
1
1
  import type { MarketState, Quote, QuoteFieldProvenance } from "../../types/financials";
2
- import { colors } from "../../theme/colors";
2
+ import { blendHex, colors, priceColor } from "../../theme/colors";
3
+
4
+ const CLOSED_CHANGE_MUTING_RATIO = 0.55;
3
5
 
4
6
  export interface ActiveQuoteDisplay {
5
7
  price: number;
@@ -45,6 +47,22 @@ export function marketStateDot(state?: MarketState): string {
45
47
  }
46
48
  }
47
49
 
50
+ export function isClosedMarketState(state?: MarketState): boolean {
51
+ return state === "CLOSED" || state === "PREPRE" || state === "POSTPOST";
52
+ }
53
+
54
+ /** Closed prices are final snapshots, so they should not look live or directional. */
55
+ export function marketPriceColor(change: number, state?: MarketState): string {
56
+ return isClosedMarketState(state) ? colors.textDim : priceColor(change);
57
+ }
58
+
59
+ /** Preserve a closed session's direction while visually distinguishing it from a live move. */
60
+ export function marketChangeColor(change: number, state?: MarketState): string {
61
+ const directionalColor = priceColor(change);
62
+ if (!isClosedMarketState(state) || change === 0) return directionalColor;
63
+ return blendHex(directionalColor, colors.textDim, CLOSED_CHANGE_MUTING_RATIO);
64
+ }
65
+
48
66
  /** Short exchange display name */
49
67
  export function exchangeShortName(exchangeName?: string, fullExchangeName?: string): string {
50
68
  if (!exchangeName && !fullExchangeName) return "";
@@ -21,8 +21,4 @@ export class QueryStore<T> {
21
21
  this.onChange(key);
22
22
  return next;
23
23
  }
24
-
25
- values(): IterableIterator<QueryEntry<T>> {
26
- return this.entries.values();
27
- }
28
24
  }
@@ -132,7 +132,55 @@ describe("quote freshness", () => {
132
132
  ).toBe(false);
133
133
  });
134
134
 
135
- test("treats prior-date regular-session quotes as stale immediately", () => {
135
+ test("allows prior-day regular-session quotes before the local exchange reopens", () => {
136
+ expect(
137
+ isQuoteStaleForCurrentSession(
138
+ quote({
139
+ symbol: "NEO.TO",
140
+ currency: "CAD",
141
+ lastUpdated: Date.parse("2026-07-06T20:00:00Z"),
142
+ listingExchangeName: "TOR",
143
+ exchangeName: "TOR",
144
+ marketState: "REGULAR",
145
+ }),
146
+ Date.parse("2026-07-07T12:10:00Z"),
147
+ ),
148
+ ).toBe(false);
149
+ });
150
+
151
+ test("allows non-US premarket labels without active-session prices", () => {
152
+ expect(
153
+ isQuoteStaleForCurrentSession(
154
+ quote({
155
+ symbol: "NEO.TO",
156
+ currency: "CAD",
157
+ lastUpdated: Date.parse("2026-07-07T12:10:00Z"),
158
+ listingExchangeName: "TOR",
159
+ exchangeName: "TOR",
160
+ marketState: "PRE",
161
+ }),
162
+ Date.parse("2026-07-07T12:10:30Z"),
163
+ ),
164
+ ).toBe(false);
165
+ });
166
+
167
+ test("rejects US premarket labels without active-session prices", () => {
168
+ expect(
169
+ isQuoteStaleForCurrentSession(
170
+ quote({
171
+ symbol: "ALAB",
172
+ currency: "USD",
173
+ lastUpdated: Date.parse("2026-07-07T12:10:00Z"),
174
+ listingExchangeName: "NASDAQ",
175
+ exchangeName: "NASDAQ",
176
+ marketState: "PRE",
177
+ }),
178
+ Date.parse("2026-07-07T12:10:30Z"),
179
+ ),
180
+ ).toBe(true);
181
+ });
182
+
183
+ test("treats prior-date regular-session quotes as stale after the local exchange reopens", () => {
136
184
  expect(
137
185
  isQuoteStaleForCurrentSession(
138
186
  quote({
@@ -1,7 +1,15 @@
1
1
  import type { Quote } from "../../types/financials";
2
+ import { canonicalExchange } from "../../utils/exchanges";
2
3
  import { isTimestampStaleForExchangeSession } from "../market/freshness";
3
4
 
5
+ const EXTENDED_HOURS_EXCHANGES = new Set(["NASDAQ", "NYSE", "AMEX", "ARCA", "BATS"]);
6
+
7
+ function isExtendedHoursExchange(quote: Quote): boolean {
8
+ return EXTENDED_HOURS_EXCHANGES.has(canonicalExchange(quote.listingExchangeName || quote.exchangeName));
9
+ }
10
+
4
11
  function isQuoteMissingActiveSessionPrice(quote: Quote): boolean {
12
+ if (!isExtendedHoursExchange(quote)) return false;
5
13
  if ((quote.marketState === "PRE" || quote.marketState === "PREPRE") && quote.preMarketPrice == null) {
6
14
  return true;
7
15
  }