gloomberb 0.1.0 → 0.3.1

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 (311) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +68 -121
  3. package/bin/gloomberb +3 -0
  4. package/package.json +13 -4
  5. package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
  6. package/src/app.tsx +1330 -0
  7. package/src/brokers/sync-broker-instance.test.ts +100 -0
  8. package/src/brokers/sync-broker-instance.ts +341 -0
  9. package/src/cli/commands/plugins.ts +173 -0
  10. package/src/cli/commands/portfolio.ts +210 -0
  11. package/src/cli/commands/search.ts +120 -0
  12. package/src/cli/commands/ticker.ts +375 -0
  13. package/src/cli/commands/watchlist.ts +210 -0
  14. package/src/cli/context.ts +62 -0
  15. package/src/cli/errors.ts +13 -0
  16. package/src/cli/helpers.ts +85 -0
  17. package/src/cli/index.ts +143 -0
  18. package/src/cli/ticker-resolution.ts +39 -0
  19. package/src/cli/types.ts +15 -0
  20. package/src/cli.test.ts +361 -0
  21. package/src/components/chart/chart-data.ts +239 -0
  22. package/src/components/chart/chart-renderer.ts +1199 -0
  23. package/src/components/chart/chart-types.ts +82 -0
  24. package/src/components/chart/chart-viewport.ts +21 -0
  25. package/src/components/chart/chart.test.ts +439 -0
  26. package/src/components/chart/comparison-chart-data.test.ts +109 -0
  27. package/src/components/chart/comparison-chart-data.ts +316 -0
  28. package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
  29. package/src/components/chart/comparison-chart-renderer.ts +354 -0
  30. package/src/components/chart/comparison-stock-chart.tsx +1486 -0
  31. package/src/components/chart/cursor-motion.ts +48 -0
  32. package/src/components/chart/native/chart-rasterizer.ts +732 -0
  33. package/src/components/chart/native/kitty-adapter.ts +12 -0
  34. package/src/components/chart/native/kitty-manager.ts +142 -0
  35. package/src/components/chart/native/kitty-protocol.ts +110 -0
  36. package/src/components/chart/native/kitty-support.ts +71 -0
  37. package/src/components/chart/native/kitty.test.ts +620 -0
  38. package/src/components/chart/native/renderer-selection.ts +24 -0
  39. package/src/components/chart/native/surface-manager.ts +225 -0
  40. package/src/components/chart/native/surface-sync.ts +34 -0
  41. package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
  42. package/src/components/chart/stock-chart.test.ts +107 -0
  43. package/src/components/chart/stock-chart.tsx +1556 -0
  44. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
  45. package/src/components/command-bar/command-bar.test.tsx +1524 -0
  46. package/src/components/command-bar/command-bar.tsx +4823 -0
  47. package/src/components/command-bar/command-registry.ts +261 -0
  48. package/src/components/command-bar/helpers.test.ts +224 -0
  49. package/src/components/command-bar/helpers.ts +371 -0
  50. package/src/components/command-bar/root-shortcuts.ts +158 -0
  51. package/src/components/command-bar/view-model.test.ts +152 -0
  52. package/src/components/command-bar/view-model.ts +185 -0
  53. package/src/components/command-bar/workflow-ops.test.ts +70 -0
  54. package/src/components/command-bar/workflow-ops.ts +433 -0
  55. package/src/components/command-bar/workflow-types.ts +127 -0
  56. package/src/components/detail-feed-view.test.tsx +105 -0
  57. package/src/components/detail-feed-view.tsx +295 -0
  58. package/src/components/index.ts +32 -0
  59. package/src/components/layout/floating-pane.tsx +49 -0
  60. package/src/components/layout/header.tsx +129 -0
  61. package/src/components/layout/pane-header.tsx +44 -0
  62. package/src/components/layout/pane.tsx +49 -0
  63. package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
  64. package/src/components/layout/pixel-mouse.test.ts +40 -0
  65. package/src/components/layout/shell.test.tsx +625 -0
  66. package/src/components/layout/shell.tsx +1200 -0
  67. package/src/components/layout/status-bar.test.tsx +173 -0
  68. package/src/components/layout/status-bar.tsx +138 -0
  69. package/src/components/markdown-editor.tsx +134 -0
  70. package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
  71. package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
  72. package/src/components/pane-settings-dialog.tsx +468 -0
  73. package/src/components/pane-template-wizard.test.tsx +52 -0
  74. package/src/components/pane-template-wizard.tsx +199 -0
  75. package/src/components/price-selector-dialog.tsx +195 -0
  76. package/src/components/spinner.tsx +2 -0
  77. package/src/components/tab-bar.tsx +17 -0
  78. package/src/components/ticker-badge-text.test.tsx +160 -0
  79. package/src/components/ticker-badge-text.tsx +58 -0
  80. package/src/components/ticker-badge.tsx +88 -0
  81. package/src/components/ticker-list-table.tsx +188 -0
  82. package/src/components/toggle-list.tsx +74 -0
  83. package/src/components/ui/button.tsx +77 -0
  84. package/src/components/ui/data-table.tsx +210 -0
  85. package/src/components/ui/external-link.tsx +22 -0
  86. package/src/components/ui/fields.tsx +187 -0
  87. package/src/components/ui/frame.tsx +73 -0
  88. package/src/components/ui/index.ts +53 -0
  89. package/src/components/ui/list-view.tsx +166 -0
  90. package/src/components/ui/loading.tsx +70 -0
  91. package/src/components/ui/status.tsx +83 -0
  92. package/src/components/ui/tabs.tsx +47 -0
  93. package/src/components/ui/toggle.tsx +164 -0
  94. package/src/components/ui/ui.test.tsx +134 -0
  95. package/src/data/app-persistence.ts +25 -0
  96. package/src/data/config-store.test.ts +310 -0
  97. package/src/data/config-store.ts +469 -0
  98. package/src/data/plugin-state-store.ts +102 -0
  99. package/src/data/resource-store.ts +323 -0
  100. package/src/data/session-store.ts +52 -0
  101. package/src/data/sqlite-cache.test.ts +103 -0
  102. package/src/data/sqlite-database.ts +62 -0
  103. package/src/data/sqlite-json.ts +19 -0
  104. package/src/data/ticker-repository.ts +45 -0
  105. package/src/data/ticker-store.ts +46 -0
  106. package/src/index.tsx +67 -0
  107. package/src/market-data/coordinator.test.ts +369 -0
  108. package/src/market-data/coordinator.ts +580 -0
  109. package/src/market-data/hooks.tsx +234 -0
  110. package/src/market-data/query-store.ts +28 -0
  111. package/src/market-data/request-types.test.ts +64 -0
  112. package/src/market-data/request-types.ts +70 -0
  113. package/src/market-data/result-types.ts +56 -0
  114. package/src/market-data/selectors.ts +166 -0
  115. package/src/market-data/trace.ts +7 -0
  116. package/src/pane-settings.test.ts +71 -0
  117. package/src/pane-settings.ts +73 -0
  118. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +338 -0
  119. package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
  120. package/src/plugins/builtin/ai/index.test.tsx +139 -0
  121. package/src/plugins/builtin/ai/index.tsx +86 -0
  122. package/src/plugins/builtin/ai/providers.ts +99 -0
  123. package/src/plugins/builtin/ai/runner.ts +82 -0
  124. package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
  125. package/src/plugins/builtin/ai/screener-contract.ts +174 -0
  126. package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
  127. package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
  128. package/src/plugins/builtin/ai/settings.ts +79 -0
  129. package/src/plugins/builtin/ai/ticker-context.ts +64 -0
  130. package/src/plugins/builtin/ai/utils.ts +71 -0
  131. package/src/plugins/builtin/ask-ai.test.tsx +258 -0
  132. package/src/plugins/builtin/ask-ai.tsx +9 -0
  133. package/src/plugins/builtin/chat-controller.test.ts +217 -0
  134. package/src/plugins/builtin/chat-controller.ts +340 -0
  135. package/src/plugins/builtin/chat.test.tsx +451 -0
  136. package/src/plugins/builtin/chat.tsx +620 -0
  137. package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
  138. package/src/plugins/builtin/comparison-chart.tsx +169 -0
  139. package/src/plugins/builtin/data-providers.ts +16 -0
  140. package/src/plugins/builtin/debug.tsx +351 -0
  141. package/src/plugins/builtin/help.test.tsx +110 -0
  142. package/src/plugins/builtin/help.tsx +281 -0
  143. package/src/plugins/builtin/layout-manager.tsx +264 -0
  144. package/src/plugins/builtin/manual-entry.tsx +25 -0
  145. package/src/plugins/builtin/news.tsx +143 -0
  146. package/src/plugins/builtin/notes-files.ts +58 -0
  147. package/src/plugins/builtin/notes.tsx +396 -0
  148. package/src/plugins/builtin/options-availability.test.tsx +179 -0
  149. package/src/plugins/builtin/options-availability.ts +158 -0
  150. package/src/plugins/builtin/options.tsx +364 -0
  151. package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
  152. package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
  153. package/src/plugins/builtin/portfolio-list/metrics.ts +436 -0
  154. package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
  155. package/src/plugins/builtin/portfolio-list/settings.ts +280 -0
  156. package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
  157. package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
  158. package/src/plugins/builtin/portfolio-list.test.ts +74 -0
  159. package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
  160. package/src/plugins/builtin/portfolio-metrics.test.ts +153 -0
  161. package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
  162. package/src/plugins/builtin/sec.tsx +250 -0
  163. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
  164. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
  165. package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
  166. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
  167. package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
  168. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
  169. package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
  170. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
  171. package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
  172. package/src/plugins/builtin/yahoo.ts +18 -0
  173. package/src/plugins/event-bus.ts +37 -0
  174. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
  175. package/src/plugins/ibkr/account-cache.test.ts +101 -0
  176. package/src/plugins/ibkr/account-cache.ts +137 -0
  177. package/src/plugins/ibkr/broker-adapter.ts +168 -0
  178. package/src/plugins/ibkr/config.test.ts +68 -0
  179. package/src/plugins/ibkr/config.ts +262 -0
  180. package/src/plugins/ibkr/dialogs.tsx +83 -0
  181. package/src/plugins/ibkr/flex.test.ts +107 -0
  182. package/src/plugins/ibkr/flex.ts +267 -0
  183. package/src/plugins/ibkr/fundamental-parser.ts +149 -0
  184. package/src/plugins/ibkr/gateway-helpers.ts +28 -0
  185. package/src/plugins/ibkr/gateway-selection.ts +51 -0
  186. package/src/plugins/ibkr/gateway-service.test.ts +450 -0
  187. package/src/plugins/ibkr/gateway-service.ts +2228 -0
  188. package/src/plugins/ibkr/index.test.tsx +183 -0
  189. package/src/plugins/ibkr/index.tsx +153 -0
  190. package/src/plugins/ibkr/instance-selection.test.ts +121 -0
  191. package/src/plugins/ibkr/instance-selection.ts +36 -0
  192. package/src/plugins/ibkr/price-normalization.test.ts +38 -0
  193. package/src/plugins/ibkr/price-normalization.ts +59 -0
  194. package/src/plugins/ibkr/trade-badge.tsx +27 -0
  195. package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
  196. package/src/plugins/ibkr/trade-tab.tsx +1050 -0
  197. package/src/plugins/ibkr/trade-utils.test.ts +175 -0
  198. package/src/plugins/ibkr/trade-utils.ts +149 -0
  199. package/src/plugins/ibkr/trading-pane.tsx +438 -0
  200. package/src/plugins/ibkr/trading-state.test.ts +109 -0
  201. package/src/plugins/ibkr/trading-state.ts +339 -0
  202. package/src/plugins/loader.ts +71 -0
  203. package/src/plugins/pane-manager.test.ts +103 -0
  204. package/src/plugins/pane-manager.ts +974 -0
  205. package/src/plugins/plugin-persistence.ts +85 -0
  206. package/src/plugins/plugin-runtime.test.tsx +185 -0
  207. package/src/plugins/plugin-runtime.tsx +175 -0
  208. package/src/plugins/prediction-markets/cache.ts +114 -0
  209. package/src/plugins/prediction-markets/categories.ts +223 -0
  210. package/src/plugins/prediction-markets/chart.tsx +512 -0
  211. package/src/plugins/prediction-markets/columns.ts +133 -0
  212. package/src/plugins/prediction-markets/controller-data.ts +614 -0
  213. package/src/plugins/prediction-markets/controller.ts +725 -0
  214. package/src/plugins/prediction-markets/detail/book.tsx +118 -0
  215. package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
  216. package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
  217. package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
  218. package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
  219. package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
  220. package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
  221. package/src/plugins/prediction-markets/detail.test.tsx +47 -0
  222. package/src/plugins/prediction-markets/index.tsx +102 -0
  223. package/src/plugins/prediction-markets/keyboard.ts +64 -0
  224. package/src/plugins/prediction-markets/launch.test.ts +126 -0
  225. package/src/plugins/prediction-markets/launch.ts +240 -0
  226. package/src/plugins/prediction-markets/metrics.ts +311 -0
  227. package/src/plugins/prediction-markets/navigation.ts +90 -0
  228. package/src/plugins/prediction-markets/outcome-order.ts +19 -0
  229. package/src/plugins/prediction-markets/pane.test.tsx +425 -0
  230. package/src/plugins/prediction-markets/pane.tsx +294 -0
  231. package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
  232. package/src/plugins/prediction-markets/rows.ts +229 -0
  233. package/src/plugins/prediction-markets/services/fetch.ts +97 -0
  234. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
  235. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
  236. package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
  237. package/src/plugins/prediction-markets/settings.ts +164 -0
  238. package/src/plugins/prediction-markets/status.ts +24 -0
  239. package/src/plugins/prediction-markets/table.tsx +84 -0
  240. package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
  241. package/src/plugins/prediction-markets/types.ts +186 -0
  242. package/src/plugins/registry.ts +633 -0
  243. package/src/sources/exchange-rates.ts +18 -0
  244. package/src/sources/gloomberb-cloud.test.ts +199 -0
  245. package/src/sources/gloomberb-cloud.ts +326 -0
  246. package/src/sources/provider-errors.ts +19 -0
  247. package/src/sources/provider-router.test.ts +1476 -0
  248. package/src/sources/provider-router.ts +1405 -0
  249. package/src/sources/sec-edgar.test.ts +286 -0
  250. package/src/sources/sec-edgar.ts +526 -0
  251. package/src/sources/yahoo-finance.test.ts +14 -0
  252. package/src/sources/yahoo-finance.ts +1017 -0
  253. package/src/state/app-activity.test.ts +41 -0
  254. package/src/state/app-activity.ts +106 -0
  255. package/src/state/app-bootstrap.test.ts +471 -0
  256. package/src/state/app-bootstrap.ts +308 -0
  257. package/src/state/app-context.test.ts +406 -0
  258. package/src/state/app-context.test.tsx +132 -0
  259. package/src/state/app-context.tsx +969 -0
  260. package/src/state/selectors.ts +67 -0
  261. package/src/state/session-persistence.test.ts +128 -0
  262. package/src/state/session-persistence.ts +161 -0
  263. package/src/state/ticker-refresh-queue.test.ts +46 -0
  264. package/src/state/ticker-refresh-queue.ts +40 -0
  265. package/src/state/use-inline-tickers.ts +174 -0
  266. package/src/state/use-quote-streaming.test.ts +37 -0
  267. package/src/state/use-quote-streaming.test.tsx +70 -0
  268. package/src/state/use-quote-streaming.ts +80 -0
  269. package/src/state/use-session-snapshot.ts +84 -0
  270. package/src/test-support/data-provider.ts +22 -0
  271. package/src/theme/color-utils.ts +67 -0
  272. package/src/theme/colors.ts +128 -0
  273. package/src/theme/themes.test.ts +45 -0
  274. package/src/theme/themes.ts +491 -0
  275. package/src/types/broker.ts +105 -0
  276. package/src/types/config.ts +434 -0
  277. package/src/types/data-provider.ts +81 -0
  278. package/src/types/financials.ts +142 -0
  279. package/src/types/instrument.ts +29 -0
  280. package/src/types/persistence.ts +21 -0
  281. package/src/types/plugin.ts +354 -0
  282. package/src/types/ticker.ts +58 -0
  283. package/src/types/trading.ts +85 -0
  284. package/src/updater.test.ts +162 -0
  285. package/src/updater.ts +247 -0
  286. package/src/utils/api-client.ts +666 -0
  287. package/src/utils/ascii-chart.ts +172 -0
  288. package/src/utils/broker-instances.ts +47 -0
  289. package/src/utils/cli-output.ts +83 -0
  290. package/src/utils/currency-units.test.ts +49 -0
  291. package/src/utils/currency-units.ts +82 -0
  292. package/src/utils/debug-log.ts +133 -0
  293. package/src/utils/exchange-rate-map.ts +8 -0
  294. package/src/utils/format.ts +158 -0
  295. package/src/utils/fuzzy-search.ts +34 -0
  296. package/src/utils/market-status.ts +95 -0
  297. package/src/utils/options.ts +86 -0
  298. package/src/utils/price-history.ts +39 -0
  299. package/src/utils/quote-time.test.ts +22 -0
  300. package/src/utils/quote-time.ts +50 -0
  301. package/src/utils/sec.test.ts +55 -0
  302. package/src/utils/sec.ts +45 -0
  303. package/src/utils/ticker-list.test.ts +22 -0
  304. package/src/utils/ticker-list.ts +35 -0
  305. package/src/utils/ticker-metadata.ts +51 -0
  306. package/src/utils/ticker-search.test.ts +227 -0
  307. package/src/utils/ticker-search.ts +764 -0
  308. package/src/utils/ticker-tokenizer.test.ts +48 -0
  309. package/src/utils/ticker-tokenizer.ts +72 -0
  310. package/src/version.ts +1 -0
  311. package/install.mjs +0 -69
@@ -0,0 +1,764 @@
1
+ import type { SearchRequestContext, DataProvider } from "../types/data-provider";
2
+ import type { InstrumentSearchResult } from "../types/instrument";
3
+ import type { TickerRecord } from "../types/ticker";
4
+ import type { TickerMetadata } from "../types/ticker";
5
+ import type { TickerRepository } from "../data/ticker-repository";
6
+
7
+ export type TickerSearchInstrumentClass = "equity" | "fund" | "derivative" | "other";
8
+ export type TickerSearchCategory = "Saved" | "Primary Listing" | "Other Listings" | "Funds & Derivatives";
9
+
10
+ const FUND_TYPES = new Set(["ETF", "ETN", "ETP", "FUND", "MUTUALFUND", "CEF", "CLOSEDEND"]);
11
+ const DERIVATIVE_TYPES = new Set(["OPT", "OPTION", "OPTIONS", "FUT", "FUTURE", "FUTURES", "WARRANT", "WARRANTS", "RIGHT", "RIGHTS"]);
12
+ const EQUITY_TYPES = new Set(["STK", "STOCK", "EQUITY", "COMMONSTOCK", "COMMON STOCK", "ADR", "ORDINARYSHARES", "ORDINARY SHARES"]);
13
+
14
+ const EXCHANGE_HINT_ALIASES: Record<string, string[]> = {
15
+ NASDAQ: ["NASDAQ", "NMS"],
16
+ NYSE: ["NYSE", "NYSE ARCA", "ARCA"],
17
+ AMEX: ["AMEX"],
18
+ TSX: ["TSX", "TORONTO"],
19
+ TORONTO: ["TORONTO", "TSX"],
20
+ XETRA: ["XETRA"],
21
+ TSE: ["TOKYO STOCK EXCHANGE", "TOKYO", "JPX", "TSE"],
22
+ TOKYO: ["TOKYO STOCK EXCHANGE", "TOKYO", "JPX", "TSE"],
23
+ JPX: ["JPX", "TOKYO STOCK EXCHANGE", "TOKYO"],
24
+ LSE: ["LSE", "LONDON"],
25
+ HKEX: ["HKEX", "HONG KONG"],
26
+ SWISS: ["SWISS", "SIX"],
27
+ BUE: ["BUENOS AIRES", "BUE"],
28
+ BUENOS: ["BUENOS AIRES", "BUE"],
29
+ AIRES: ["BUENOS AIRES", "BUE"],
30
+ };
31
+
32
+ const ASSET_HINT_MAP: Record<string, TickerSearchInstrumentClass> = {
33
+ STOCK: "equity",
34
+ STK: "equity",
35
+ EQUITY: "equity",
36
+ SHARE: "equity",
37
+ SHARES: "equity",
38
+ COMMON: "equity",
39
+ ETF: "fund",
40
+ ETN: "fund",
41
+ ETP: "fund",
42
+ FUND: "fund",
43
+ OPTION: "derivative",
44
+ OPTIONS: "derivative",
45
+ CALL: "derivative",
46
+ PUT: "derivative",
47
+ WARRANT: "derivative",
48
+ WARRANTS: "derivative",
49
+ FUTURE: "derivative",
50
+ FUTURES: "derivative",
51
+ };
52
+
53
+ const SHARE_CLASS_SUFFIXES = new Set(["A", "B", "C", "D", "K"]);
54
+
55
+ export interface TickerSearchRankableItem {
56
+ id: string;
57
+ label: string;
58
+ detail: string;
59
+ kind: string;
60
+ category: string;
61
+ right?: string;
62
+ symbol?: string;
63
+ saved?: boolean;
64
+ instrumentClass?: TickerSearchInstrumentClass;
65
+ exchangeLabel?: string;
66
+ primaryExchangeLabel?: string;
67
+ searchAliases?: string[];
68
+ }
69
+
70
+ export interface TickerSearchCandidate extends TickerSearchRankableItem {
71
+ category: TickerSearchCategory;
72
+ kind: "ticker" | "search";
73
+ symbol: string;
74
+ saved: boolean;
75
+ instrumentClass: TickerSearchInstrumentClass;
76
+ searchAliases: string[];
77
+ ticker?: TickerRecord;
78
+ result?: InstrumentSearchResult;
79
+ }
80
+
81
+ export type ResolvedTickerSearch =
82
+ | { kind: "local"; symbol: string; ticker: TickerRecord }
83
+ | { kind: "provider"; symbol: string; result: InstrumentSearchResult };
84
+
85
+ interface SearchQueryIntent {
86
+ normalizedQuery: string;
87
+ compactQuery: string;
88
+ companyQuery: string;
89
+ exchangeHints: string[];
90
+ assetPreference: TickerSearchInstrumentClass | null;
91
+ }
92
+
93
+ export function normalizeTickerInput(activeTicker: string | null, arg?: string): string | null {
94
+ const explicitTicker = arg?.trim().toUpperCase();
95
+ if (explicitTicker) return explicitTicker;
96
+ return activeTicker;
97
+ }
98
+
99
+ export function createLocalTickerSearchCandidates(
100
+ tickers: Iterable<TickerRecord>,
101
+ providerHints: ReadonlyMap<string, InstrumentSearchResult> = new Map(),
102
+ ): TickerSearchCandidate[] {
103
+ return Array.from(tickers).map((ticker) => {
104
+ const symbol = normalizeTickerSymbol(ticker.metadata.ticker);
105
+ const hint = providerHints.get(symbol);
106
+ return {
107
+ id: `goto:${ticker.metadata.ticker}`,
108
+ label: ticker.metadata.ticker,
109
+ symbol,
110
+ detail: resolveLocalSearchName(ticker, hint),
111
+ right: hint?.exchange || ticker.metadata.exchange,
112
+ exchangeLabel: hint?.exchange || ticker.metadata.exchange,
113
+ primaryExchangeLabel: hint?.primaryExchange,
114
+ category: "Saved",
115
+ kind: "ticker",
116
+ saved: true,
117
+ instrumentClass: classifyInstrumentKind(hint?.brokerContract?.secType || hint?.type || ticker.metadata.assetCategory),
118
+ searchAliases: buildSymbolAliases(symbol),
119
+ ticker,
120
+ result: hint,
121
+ };
122
+ });
123
+ }
124
+
125
+ export function createProviderTickerSearchCandidates(
126
+ searchResults: InstrumentSearchResult[],
127
+ localTickers: ReadonlyMap<string, TickerRecord>,
128
+ ): TickerSearchCandidate[] {
129
+ return searchResults.map((result) => {
130
+ const symbol = getSearchResultSymbol(result);
131
+ const saved = localTickers.has(symbol);
132
+ return {
133
+ id: `search:${result.symbol}`,
134
+ label: symbol,
135
+ symbol,
136
+ detail: [result.name, result.brokerLabel, result.type].filter(Boolean).join(" | "),
137
+ right: result.exchange || result.primaryExchange || result.type || undefined,
138
+ exchangeLabel: result.exchange,
139
+ primaryExchangeLabel: result.primaryExchange,
140
+ category: saved ? "Saved" : "Other Listings",
141
+ kind: "search",
142
+ saved,
143
+ instrumentClass: classifyInstrumentKind(result.brokerContract?.secType || result.type),
144
+ searchAliases: buildSearchResultAliases(result),
145
+ result,
146
+ };
147
+ });
148
+ }
149
+
150
+ export async function searchTickerCandidates({
151
+ query,
152
+ tickers,
153
+ dataProvider,
154
+ searchContext,
155
+ localLimit = 6,
156
+ totalLimit = 8,
157
+ }: {
158
+ query: string;
159
+ tickers: ReadonlyMap<string, TickerRecord>;
160
+ dataProvider: DataProvider;
161
+ searchContext?: SearchRequestContext;
162
+ localLimit?: number;
163
+ totalLimit?: number;
164
+ }): Promise<TickerSearchCandidate[]> {
165
+ return buildTickerSearchCandidates({
166
+ query,
167
+ tickers,
168
+ providerResults: await searchProviderResults(dataProvider, query, searchContext),
169
+ localLimit,
170
+ totalLimit,
171
+ });
172
+ }
173
+
174
+ export function buildTickerSearchCandidates({
175
+ query,
176
+ tickers,
177
+ providerResults,
178
+ localLimit = 6,
179
+ totalLimit = 8,
180
+ }: {
181
+ query: string;
182
+ tickers: ReadonlyMap<string, TickerRecord>;
183
+ providerResults: InstrumentSearchResult[];
184
+ localLimit?: number;
185
+ totalLimit?: number;
186
+ }): TickerSearchCandidate[] {
187
+ const providerHints = buildProviderHintMap(providerResults);
188
+ const localItems = rankTickerSearchItems(
189
+ createLocalTickerSearchCandidates(tickers.values(), providerHints),
190
+ query,
191
+ );
192
+ const providerItems = createProviderTickerSearchCandidates(providerResults, tickers);
193
+ const ranked = rankTickerSearchItems([...localItems, ...providerItems], query);
194
+ return limitTickerSearchCandidates(assignTickerSearchCategories(ranked), totalLimit, localLimit);
195
+ }
196
+
197
+ export async function resolveTickerSearch({
198
+ query,
199
+ activeTicker,
200
+ tickers,
201
+ dataProvider,
202
+ searchContext,
203
+ }: {
204
+ query?: string;
205
+ activeTicker: string | null;
206
+ tickers: ReadonlyMap<string, TickerRecord>;
207
+ dataProvider: DataProvider;
208
+ searchContext?: SearchRequestContext;
209
+ }): Promise<ResolvedTickerSearch | null> {
210
+ const symbol = normalizeTickerInput(activeTicker, query);
211
+ if (!symbol) return null;
212
+
213
+ const local = tickers.get(symbol)
214
+ ?? findExactTickerSearchMatch(createLocalTickerSearchCandidates(tickers.values()), symbol)?.ticker
215
+ ?? null;
216
+ if (local) {
217
+ return { kind: "local", symbol: local.metadata.ticker, ticker: local };
218
+ }
219
+
220
+ const providerItems = createProviderTickerSearchCandidates(
221
+ await searchProviderResults(dataProvider, symbol, searchContext),
222
+ tickers,
223
+ );
224
+ const exactMatch = findExactTickerSearchMatch(providerItems, symbol);
225
+ if (!exactMatch?.result) return null;
226
+
227
+ return {
228
+ kind: "provider",
229
+ symbol: exactMatch.symbol,
230
+ result: exactMatch.result,
231
+ };
232
+ }
233
+
234
+ export async function upsertTickerFromSearchResult(
235
+ tickerRepository: TickerRepository,
236
+ result: InstrumentSearchResult,
237
+ ): Promise<{ ticker: TickerRecord; created: boolean }> {
238
+ const symbol = getSearchResultSymbol(result);
239
+ let ticker = await tickerRepository.loadTicker(symbol);
240
+ const created = !ticker;
241
+
242
+ if (!ticker) {
243
+ const metadata: TickerMetadata = {
244
+ ticker: symbol,
245
+ exchange: result.exchange,
246
+ currency: result.currency || result.brokerContract?.currency || "USD",
247
+ name: result.name || symbol,
248
+ assetCategory: result.brokerContract?.secType || result.type || undefined,
249
+ broker_contracts: result.brokerContract ? [result.brokerContract] : [],
250
+ portfolios: [],
251
+ watchlists: [],
252
+ positions: [],
253
+ custom: {},
254
+ tags: [],
255
+ };
256
+ ticker = await tickerRepository.createTicker(metadata);
257
+ } else {
258
+ const changed = mergeTickerMetadataFromSearchResult(ticker.metadata, result);
259
+ const existingContracts = ticker.metadata.broker_contracts ?? [];
260
+ if (result.brokerContract) {
261
+ const nextContracts = [...existingContracts];
262
+ const hasContract = nextContracts.some((contract) =>
263
+ contract.brokerId === result.brokerContract!.brokerId
264
+ && contract.brokerInstanceId === result.brokerContract!.brokerInstanceId
265
+ && contract.conId === result.brokerContract!.conId
266
+ && contract.localSymbol === result.brokerContract!.localSymbol
267
+ );
268
+ if (!hasContract) {
269
+ nextContracts.push(result.brokerContract);
270
+ ticker.metadata.broker_contracts = nextContracts;
271
+ }
272
+ }
273
+ if (changed || ticker.metadata.broker_contracts !== existingContracts) {
274
+ await tickerRepository.saveTicker(ticker);
275
+ }
276
+ }
277
+
278
+ return { ticker, created };
279
+ }
280
+
281
+ export function findExactTickerSearchMatch<T extends Pick<TickerSearchRankableItem, "label"> & Partial<TickerSearchRankableItem>>(
282
+ items: T[],
283
+ query: string,
284
+ ): T | null {
285
+ const aliasForms = buildSymbolAliases(query);
286
+ const normalizedAliases = new Set(aliasForms.map((value) => normalizeSearchText(value)));
287
+ const compactAliases = new Set(aliasForms.map((value) => compactSearchText(value)));
288
+
289
+ return items.find((item) =>
290
+ getItemSearchAliases(item).some((alias) =>
291
+ normalizedAliases.has(normalizeSearchText(alias))
292
+ || compactAliases.has(compactSearchText(alias))
293
+ )
294
+ ) ?? null;
295
+ }
296
+
297
+ export function rankTickerSearchItems<T extends Pick<TickerSearchRankableItem, "id" | "label" | "detail" | "kind" | "category" | "right"> & Partial<TickerSearchRankableItem>>(
298
+ items: T[],
299
+ query: string,
300
+ ): T[] {
301
+ const intent = analyzeSearchQuery(query);
302
+ if (!intent.normalizedQuery) return items;
303
+
304
+ const ranked = items
305
+ .map((item, index) => {
306
+ const normalizedLabel = normalizeSearchText(item.label);
307
+ const normalizedDetail = normalizeSearchText(item.detail);
308
+ const normalizedRight = normalizeSearchText(item.right || "");
309
+ const textQueries = [intent.normalizedQuery, intent.companyQuery].filter(Boolean);
310
+ const labelScore = maxScoreForQueries(textQueries, normalizedLabel, {
311
+ exact: 24_000,
312
+ prefix: 18_000,
313
+ substring: 14_000,
314
+ fuzzy: 7_000,
315
+ });
316
+ const detailScore = Math.max(
317
+ maxScoreForQueries(textQueries, normalizedDetail, {
318
+ exact: 4_500,
319
+ prefix: 3_800,
320
+ substring: 2_600,
321
+ fuzzy: 600,
322
+ }),
323
+ maxScoreForQueries(textQueries, normalizedRight, {
324
+ exact: 1_200,
325
+ prefix: 1_000,
326
+ substring: 700,
327
+ fuzzy: 100,
328
+ }),
329
+ );
330
+ const aliasScore = getItemSearchAliases(item)
331
+ .reduce((best, alias) => Math.max(best, scoreSearchAlias(intent, alias)), 0);
332
+ const textScore = labelScore + detailScore + aliasScore;
333
+ const saved = isSavedSearchItem(item);
334
+ const priorityScore = scoreAssetPreference(intent, item.instrumentClass)
335
+ + scoreExchangePreference(intent, item)
336
+ + scoreListingPriority(item);
337
+ return {
338
+ item,
339
+ index,
340
+ normalizedSymbol: normalizeSearchText(item.symbol || item.label),
341
+ textScore,
342
+ score: textScore + priorityScore + (textScore > 0 && saved ? 900 : 0),
343
+ };
344
+ });
345
+
346
+ const matchedLocalSymbols = new Set(
347
+ ranked
348
+ .filter(({ item, textScore }) => textScore > 0 && item.kind === "ticker")
349
+ .map(({ normalizedSymbol }) => normalizedSymbol),
350
+ );
351
+
352
+ const filtered = ranked
353
+ .filter(({ item, normalizedSymbol, textScore }) => {
354
+ if (textScore <= 0) return false;
355
+ if (item.kind !== "search") return true;
356
+ return !matchedLocalSymbols.has(normalizedSymbol);
357
+ })
358
+ .sort((a, b) => {
359
+ if (b.score !== a.score) return b.score - a.score;
360
+ const aSaved = isSavedSearchItem(a.item);
361
+ const bSaved = isSavedSearchItem(b.item);
362
+ if (aSaved !== bSaved) return aSaved ? -1 : 1;
363
+ if (a.item.label.length !== b.item.label.length) return a.item.label.length - b.item.label.length;
364
+ return a.index - b.index;
365
+ });
366
+
367
+ const deduped: T[] = [];
368
+ const seen = new Set<string>();
369
+ for (const entry of filtered) {
370
+ const key = getTickerSearchDedupKey(entry.item);
371
+ if (seen.has(key)) continue;
372
+ seen.add(key);
373
+ deduped.push(entry.item);
374
+ }
375
+ return deduped;
376
+ }
377
+
378
+ function buildProviderHintMap(searchResults: InstrumentSearchResult[]): Map<string, InstrumentSearchResult> {
379
+ const hints = new Map<string, InstrumentSearchResult>();
380
+ for (const result of searchResults) {
381
+ const symbol = getSearchResultSymbol(result);
382
+ const existing = hints.get(symbol);
383
+ if (!existing || getProviderHintRichness(result) > getProviderHintRichness(existing)) {
384
+ hints.set(symbol, result);
385
+ }
386
+ }
387
+ return hints;
388
+ }
389
+
390
+ async function searchProviderResults(
391
+ dataProvider: DataProvider,
392
+ query: string,
393
+ searchContext?: SearchRequestContext,
394
+ ): Promise<InstrumentSearchResult[]> {
395
+ const merged: InstrumentSearchResult[] = [];
396
+ const seen = new Set<string>();
397
+
398
+ for (const searchQuery of buildProviderSearchQueries(query)) {
399
+ let results: InstrumentSearchResult[] = [];
400
+ try {
401
+ results = await dataProvider.search(searchQuery, searchContext);
402
+ } catch {
403
+ results = [];
404
+ }
405
+
406
+ for (const result of results) {
407
+ const key = buildProviderSearchResultKey(result);
408
+ if (seen.has(key)) continue;
409
+ seen.add(key);
410
+ merged.push(result);
411
+ }
412
+ }
413
+
414
+ return merged;
415
+ }
416
+
417
+ function buildProviderSearchQueries(query: string): string[] {
418
+ const trimmedQuery = query.trim();
419
+ if (!trimmedQuery) return [];
420
+
421
+ const queries = new Set<string>([trimmedQuery]);
422
+ for (const alias of buildSymbolAliases(trimmedQuery)) queries.add(alias);
423
+ for (const alias of buildCompactShareClassAliases(trimmedQuery)) queries.add(alias);
424
+ return [...queries].slice(0, 4);
425
+ }
426
+
427
+ function buildCompactShareClassAliases(query: string): string[] {
428
+ const normalized = normalizeTickerSymbol(query);
429
+ if (!/^[A-Z]{4,5}$/.test(normalized)) return [];
430
+ const shareClass = normalized.slice(-1);
431
+ if (!SHARE_CLASS_SUFFIXES.has(shareClass)) return [];
432
+ const base = normalized.slice(0, -1);
433
+ if (base.length < 2) return [];
434
+ return [`${base}-${shareClass}`, `${base}.${shareClass}`];
435
+ }
436
+
437
+ function buildProviderSearchResultKey(result: InstrumentSearchResult): string {
438
+ return [
439
+ normalizeTickerSymbol(result.symbol),
440
+ normalizeSearchText(result.exchange || ""),
441
+ normalizeSearchText(result.type || ""),
442
+ normalizeSearchText(result.primaryExchange || ""),
443
+ normalizeSearchText(result.currency || ""),
444
+ ].join("|");
445
+ }
446
+
447
+ function getProviderHintRichness(result: InstrumentSearchResult): number {
448
+ let score = 0;
449
+ if (result.name) score += Math.min(120, result.name.length);
450
+ if (result.exchange) score += 60;
451
+ if (result.primaryExchange) score += 40;
452
+ if (result.currency) score += 20;
453
+ return score;
454
+ }
455
+
456
+ function assignTickerSearchCategories<T extends TickerSearchCandidate>(items: T[]): T[] {
457
+ const hasSavedListing = items.some((item) =>
458
+ item.saved && item.instrumentClass !== "fund" && item.instrumentClass !== "derivative"
459
+ );
460
+ let assignedPrimaryListing = hasSavedListing;
461
+
462
+ return items.map((item) => {
463
+ if (item.saved || item.kind === "ticker") {
464
+ return { ...item, category: "Saved" };
465
+ }
466
+ if (item.instrumentClass === "fund" || item.instrumentClass === "derivative") {
467
+ return { ...item, category: "Funds & Derivatives" };
468
+ }
469
+ if (!assignedPrimaryListing) {
470
+ assignedPrimaryListing = true;
471
+ return { ...item, category: "Primary Listing" };
472
+ }
473
+ return { ...item, category: "Other Listings" };
474
+ }) as T[];
475
+ }
476
+
477
+ function limitTickerSearchCandidates<T extends TickerSearchCandidate>(
478
+ items: T[],
479
+ totalLimit: number,
480
+ savedLimit: number,
481
+ ): T[] {
482
+ const limited: T[] = [];
483
+ let savedCount = 0;
484
+
485
+ for (const item of items) {
486
+ if (limited.length >= totalLimit) break;
487
+ if (item.category === "Saved") {
488
+ if (savedCount >= savedLimit) continue;
489
+ savedCount += 1;
490
+ }
491
+ limited.push(item);
492
+ }
493
+
494
+ return limited;
495
+ }
496
+
497
+ function resolveLocalSearchName(ticker: TickerRecord, hint?: InstrumentSearchResult): string {
498
+ if (hint?.name && isLowQualityTickerName(ticker.metadata.name, ticker.metadata.ticker)) {
499
+ return hint.name;
500
+ }
501
+ return ticker.metadata.name;
502
+ }
503
+
504
+ function mergeTickerMetadataFromSearchResult(metadata: TickerMetadata, result: InstrumentSearchResult): boolean {
505
+ let changed = false;
506
+ const nextName = result.name?.trim();
507
+ const nextExchange = result.exchange?.trim();
508
+ const nextCurrency = (result.currency || result.brokerContract?.currency || "").trim();
509
+ const nextAssetCategory = (result.brokerContract?.secType || result.type || "").trim();
510
+
511
+ if (nextName && shouldReplaceTickerName(metadata.name, metadata.ticker, nextName)) {
512
+ metadata.name = nextName;
513
+ changed = true;
514
+ }
515
+ if (nextExchange && !metadata.exchange) {
516
+ metadata.exchange = nextExchange;
517
+ changed = true;
518
+ }
519
+ if (nextCurrency && !metadata.currency) {
520
+ metadata.currency = nextCurrency;
521
+ changed = true;
522
+ }
523
+ if (nextAssetCategory && shouldReplaceAssetCategory(metadata.assetCategory, nextAssetCategory)) {
524
+ metadata.assetCategory = nextAssetCategory;
525
+ changed = true;
526
+ }
527
+
528
+ return changed;
529
+ }
530
+
531
+ function shouldReplaceTickerName(currentName: string, symbol: string, nextName: string): boolean {
532
+ if (!currentName.trim()) return true;
533
+ if (currentName.trim() === nextName.trim()) return false;
534
+ return isLowQualityTickerName(currentName, symbol) && !isLowQualityTickerName(nextName, symbol);
535
+ }
536
+
537
+ function isLowQualityTickerName(name: string, symbol: string): boolean {
538
+ const trimmedName = name.trim();
539
+ if (!trimmedName) return true;
540
+ const normalizedName = normalizeSearchText(trimmedName);
541
+ return buildSymbolAliases(symbol).some((alias) =>
542
+ normalizedName === normalizeSearchText(alias)
543
+ || compactSearchText(trimmedName) === compactSearchText(alias)
544
+ );
545
+ }
546
+
547
+ function shouldReplaceAssetCategory(currentCategory: string | undefined, nextCategory: string): boolean {
548
+ if (!currentCategory?.trim()) return true;
549
+ const currentClass = classifyInstrumentKind(currentCategory);
550
+ const nextClass = classifyInstrumentKind(nextCategory);
551
+ return currentClass === "equity" && nextClass !== "equity";
552
+ }
553
+
554
+ function analyzeSearchQuery(query: string): SearchQueryIntent {
555
+ const normalizedQuery = normalizeSearchText(query);
556
+ const compactQuery = compactSearchText(query);
557
+ const tokens = normalizedQuery.split(" ").filter(Boolean);
558
+ const exchangeHints = Array.from(new Set(tokens.filter((token) => token in EXCHANGE_HINT_ALIASES)));
559
+
560
+ const assetHints = Array.from(new Set(
561
+ tokens
562
+ .map((token) => ASSET_HINT_MAP[token])
563
+ .filter((token): token is TickerSearchInstrumentClass => token != null),
564
+ ));
565
+ const assetPreference = assetHints.length === 1 ? assetHints[0] : null;
566
+
567
+ const companyTokens = tokens.filter((token) => !(token in EXCHANGE_HINT_ALIASES) && !(token in ASSET_HINT_MAP));
568
+ const companyQuery = companyTokens.join(" ");
569
+
570
+ return {
571
+ normalizedQuery,
572
+ compactQuery,
573
+ companyQuery,
574
+ exchangeHints,
575
+ assetPreference,
576
+ };
577
+ }
578
+
579
+ function normalizeSearchText(text: string): string {
580
+ return text.trim().toUpperCase().replace(/[^A-Z0-9]+/g, " ").trim();
581
+ }
582
+
583
+ function compactSearchText(text: string): string {
584
+ return text.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "");
585
+ }
586
+
587
+ function normalizeTickerSymbol(symbol: string): string {
588
+ return symbol.trim().toUpperCase();
589
+ }
590
+
591
+ function getSearchResultSymbol(result: InstrumentSearchResult): string {
592
+ return normalizeTickerSymbol(result.brokerContract?.localSymbol || result.symbol);
593
+ }
594
+
595
+ function buildSearchResultAliases(result: InstrumentSearchResult): string[] {
596
+ const aliases = new Set(buildSymbolAliases(result.symbol));
597
+ const resolvedSymbol = getSearchResultSymbol(result);
598
+ for (const alias of buildSymbolAliases(resolvedSymbol)) aliases.add(alias);
599
+ if (result.brokerContract?.symbol) {
600
+ for (const alias of buildSymbolAliases(result.brokerContract.symbol)) aliases.add(alias);
601
+ }
602
+ return [...aliases];
603
+ }
604
+
605
+ function buildSymbolAliases(symbol: string): string[] {
606
+ const normalizedSymbol = normalizeTickerSymbol(symbol);
607
+ if (!normalizedSymbol) return [];
608
+
609
+ const searchText = normalizeSearchText(normalizedSymbol);
610
+ const aliases = new Set<string>([
611
+ normalizedSymbol,
612
+ searchText,
613
+ compactSearchText(normalizedSymbol),
614
+ ]);
615
+
616
+ if (searchText.includes(" ")) {
617
+ aliases.add(searchText.replace(/ /g, "."));
618
+ aliases.add(searchText.replace(/ /g, "-"));
619
+ }
620
+
621
+ return [...aliases].filter(Boolean);
622
+ }
623
+
624
+ function getItemSearchAliases(item: Pick<TickerSearchRankableItem, "label"> & Partial<TickerSearchRankableItem>): string[] {
625
+ const aliases = item.searchAliases && item.searchAliases.length > 0
626
+ ? item.searchAliases
627
+ : buildSymbolAliases(item.symbol || item.label);
628
+ return aliases.length > 0 ? aliases : [item.label];
629
+ }
630
+
631
+ function classifyInstrumentKind(rawType?: string): TickerSearchInstrumentClass {
632
+ const normalizedType = normalizeSearchText(rawType || "");
633
+ if (!normalizedType) return "other";
634
+ if (FUND_TYPES.has(normalizedType)) return "fund";
635
+ if (DERIVATIVE_TYPES.has(normalizedType)) return "derivative";
636
+ if (EQUITY_TYPES.has(normalizedType)) return "equity";
637
+ if (normalizedType.includes("ETF") || normalizedType.includes("FUND")) return "fund";
638
+ if (normalizedType.includes("OPT") || normalizedType.includes("FUT") || normalizedType.includes("WARRANT")) return "derivative";
639
+ if (normalizedType.includes("EQUITY") || normalizedType.includes("STOCK") || normalizedType.includes("STK")) return "equity";
640
+ return "other";
641
+ }
642
+
643
+ function maxScoreForQueries(
644
+ queries: string[],
645
+ value: string,
646
+ weights: { exact: number; prefix: number; substring: number; fuzzy: number },
647
+ ): number {
648
+ let best = 0;
649
+ for (const query of queries) {
650
+ best = Math.max(best, scoreSearchField(query, value, weights));
651
+ }
652
+ return best;
653
+ }
654
+
655
+ function scoreSearchAlias(intent: SearchQueryIntent, alias: string): number {
656
+ if (!alias) return 0;
657
+ const normalizedAlias = normalizeSearchText(alias);
658
+ const compactAlias = compactSearchText(alias);
659
+
660
+ let score = scoreSearchField(intent.normalizedQuery, normalizedAlias, {
661
+ exact: 8_000,
662
+ prefix: 5_000,
663
+ substring: 3_400,
664
+ fuzzy: 1_200,
665
+ });
666
+
667
+ if (intent.companyQuery && intent.companyQuery !== intent.normalizedQuery) {
668
+ score = Math.max(score, scoreSearchField(intent.companyQuery, normalizedAlias, {
669
+ exact: 6_000,
670
+ prefix: 4_000,
671
+ substring: 2_600,
672
+ fuzzy: 800,
673
+ }));
674
+ }
675
+
676
+ if (intent.compactQuery && compactAlias) {
677
+ if (compactAlias === intent.compactQuery) {
678
+ score = Math.max(score, 40_000 - compactAlias.length);
679
+ } else if (compactAlias.startsWith(intent.compactQuery)) {
680
+ score = Math.max(score, 6_000 - compactAlias.length);
681
+ }
682
+ }
683
+
684
+ return score;
685
+ }
686
+
687
+ function scoreAssetPreference(intent: SearchQueryIntent, instrumentClass?: TickerSearchInstrumentClass): number {
688
+ const itemClass = instrumentClass || "other";
689
+ if (!intent.assetPreference) {
690
+ if (itemClass === "equity") return 400;
691
+ if (itemClass === "fund") return -250;
692
+ if (itemClass === "derivative") return -500;
693
+ return 0;
694
+ }
695
+
696
+ if (itemClass === intent.assetPreference) return 2_400;
697
+ if (itemClass === "other") return -300;
698
+ return -1_200;
699
+ }
700
+
701
+ function scoreExchangePreference(
702
+ intent: SearchQueryIntent,
703
+ item: Pick<TickerSearchRankableItem, "right"> & Partial<TickerSearchRankableItem>,
704
+ ): number {
705
+ if (intent.exchangeHints.length === 0) return 0;
706
+
707
+ const exchangeTexts = [
708
+ item.exchangeLabel,
709
+ item.primaryExchangeLabel,
710
+ item.right,
711
+ ]
712
+ .map((value) => normalizeSearchText(value || ""))
713
+ .filter(Boolean);
714
+
715
+ if (exchangeTexts.length === 0) return -400;
716
+
717
+ const matchesHint = intent.exchangeHints.some((hint) =>
718
+ (EXCHANGE_HINT_ALIASES[hint] ?? [hint]).some((alias) => {
719
+ const normalizedAlias = normalizeSearchText(alias);
720
+ return exchangeTexts.some((exchangeText) => exchangeText.includes(normalizedAlias));
721
+ })
722
+ );
723
+
724
+ return matchesHint ? 2_000 : -800;
725
+ }
726
+
727
+ function scoreListingPriority(item: Pick<TickerSearchRankableItem, "label"> & Partial<TickerSearchRankableItem>): number {
728
+ const instrumentClass = item.instrumentClass || "other";
729
+ if (instrumentClass !== "equity") return 0;
730
+ let score = 180;
731
+ if (item.label.includes(".")) score -= 140;
732
+ if (item.label.length <= 5) score += 120;
733
+ return score;
734
+ }
735
+
736
+ function isSavedSearchItem(item: Pick<TickerSearchRankableItem, "kind" | "category"> & Partial<TickerSearchRankableItem>): boolean {
737
+ return item.saved === true || item.kind === "ticker" || item.category === "Saved" || item.category === "Open";
738
+ }
739
+
740
+ function scoreSearchField(query: string, value: string, weights: { exact: number; prefix: number; substring: number; fuzzy: number }): number {
741
+ if (!query || !value) return 0;
742
+ if (value === query) return weights.exact - value.length;
743
+ if (value.startsWith(query)) return weights.prefix - value.length;
744
+
745
+ const substringIndex = value.indexOf(query);
746
+ if (substringIndex >= 0) {
747
+ return weights.substring - substringIndex * 25 - value.length;
748
+ }
749
+
750
+ let qi = 0;
751
+ let score = 0;
752
+ for (let i = 0; i < value.length && qi < query.length; i++) {
753
+ if (value[i] !== query[qi]) continue;
754
+ score += i === 0 || value[i - 1] === " " ? 10 : 2;
755
+ qi += 1;
756
+ }
757
+ return qi === query.length ? weights.fuzzy + score : 0;
758
+ }
759
+
760
+ function getTickerSearchDedupKey(item: Pick<TickerSearchRankableItem, "id" | "kind" | "label" | "detail" | "right"> & Partial<TickerSearchRankableItem>): string {
761
+ if (item.kind !== "ticker" && item.kind !== "search") return item.id;
762
+ const qualifier = normalizeSearchText(item.right || item.detail.split("|").at(-1) || "");
763
+ return `${normalizeSearchText(item.symbol || item.label)}|${qualifier}`;
764
+ }