gloomberb 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/README.md +20 -19
  2. package/bin/gloomberb +3 -0
  3. package/package.json +5 -4
  4. package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
  5. package/src/app.tsx +1365 -0
  6. package/src/brokers/sync-broker-instance.test.ts +100 -0
  7. package/src/brokers/sync-broker-instance.ts +341 -0
  8. package/src/cli/commands/plugins.ts +173 -0
  9. package/src/cli/commands/portfolio.ts +210 -0
  10. package/src/cli/commands/search.ts +120 -0
  11. package/src/cli/commands/ticker.ts +375 -0
  12. package/src/cli/commands/watchlist.ts +210 -0
  13. package/src/cli/context.ts +62 -0
  14. package/src/cli/errors.ts +13 -0
  15. package/src/cli/helpers.ts +85 -0
  16. package/src/cli/index.ts +143 -0
  17. package/src/cli/ticker-resolution.ts +39 -0
  18. package/src/cli/types.ts +15 -0
  19. package/src/cli.test.ts +361 -0
  20. package/src/components/chart/chart-data.ts +239 -0
  21. package/src/components/chart/chart-renderer.ts +1199 -0
  22. package/src/components/chart/chart-types.ts +82 -0
  23. package/src/components/chart/chart-viewport.ts +21 -0
  24. package/src/components/chart/chart.test.ts +439 -0
  25. package/src/components/chart/comparison-chart-data.test.ts +109 -0
  26. package/src/components/chart/comparison-chart-data.ts +316 -0
  27. package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
  28. package/src/components/chart/comparison-chart-renderer.ts +354 -0
  29. package/src/components/chart/comparison-stock-chart.tsx +1486 -0
  30. package/src/components/chart/cursor-motion.ts +48 -0
  31. package/src/components/chart/native/chart-rasterizer.ts +732 -0
  32. package/src/components/chart/native/kitty-adapter.ts +12 -0
  33. package/src/components/chart/native/kitty-manager.ts +142 -0
  34. package/src/components/chart/native/kitty-protocol.ts +110 -0
  35. package/src/components/chart/native/kitty-support.ts +71 -0
  36. package/src/components/chart/native/kitty.test.ts +620 -0
  37. package/src/components/chart/native/renderer-selection.ts +24 -0
  38. package/src/components/chart/native/surface-manager.ts +225 -0
  39. package/src/components/chart/native/surface-sync.ts +34 -0
  40. package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
  41. package/src/components/chart/stock-chart.test.ts +107 -0
  42. package/src/components/chart/stock-chart.tsx +1556 -0
  43. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
  44. package/src/components/command-bar/command-bar.test.tsx +1546 -0
  45. package/src/components/command-bar/command-bar.tsx +4841 -0
  46. package/src/components/command-bar/command-registry.ts +269 -0
  47. package/src/components/command-bar/helpers.test.ts +224 -0
  48. package/src/components/command-bar/helpers.ts +371 -0
  49. package/src/components/command-bar/root-shortcuts.ts +158 -0
  50. package/src/components/command-bar/view-model.test.ts +152 -0
  51. package/src/components/command-bar/view-model.ts +185 -0
  52. package/src/components/command-bar/workflow-ops.test.ts +70 -0
  53. package/src/components/command-bar/workflow-ops.ts +433 -0
  54. package/src/components/command-bar/workflow-types.ts +127 -0
  55. package/src/components/detail-feed-view.test.tsx +105 -0
  56. package/src/components/detail-feed-view.tsx +295 -0
  57. package/src/components/index.ts +32 -0
  58. package/src/components/layout/floating-pane.tsx +49 -0
  59. package/src/components/layout/header.tsx +151 -0
  60. package/src/components/layout/pane-header.tsx +44 -0
  61. package/src/components/layout/pane.tsx +49 -0
  62. package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
  63. package/src/components/layout/pixel-mouse.test.ts +40 -0
  64. package/src/components/layout/shell.test.tsx +651 -0
  65. package/src/components/layout/shell.tsx +1200 -0
  66. package/src/components/layout/status-bar.test.tsx +173 -0
  67. package/src/components/layout/status-bar.tsx +138 -0
  68. package/src/components/markdown-editor.tsx +134 -0
  69. package/src/components/markdown-text.tsx +218 -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 +347 -0
  97. package/src/data/config-store.ts +507 -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 +55 -0
  117. package/src/pane-settings.ts +73 -0
  118. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +350 -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 +460 -0
  154. package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
  155. package/src/plugins/builtin/portfolio-list/settings.ts +270 -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 +224 -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 +162 -0
  259. package/src/state/app-context.tsx +981 -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 +444 -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 +282 -0
  285. package/src/updater.ts +303 -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 -68
@@ -0,0 +1,660 @@
1
+ import {
2
+ buildPredictionCatalogResourceKey,
3
+ buildPredictionDetailResourceKey,
4
+ } from "../cache";
5
+ import {
6
+ getPolymarketCategoryTagSlugs,
7
+ matchesPredictionCategory,
8
+ resolvePredictionDisplayCategory,
9
+ } from "../categories";
10
+ import type {
11
+ PredictionBookLevel,
12
+ PredictionBookSnapshot,
13
+ PredictionCategoryId,
14
+ PredictionHistoryPoint,
15
+ PredictionMarketDetail,
16
+ PredictionMarketSummary,
17
+ PredictionSiblingMarket,
18
+ PredictionTrade,
19
+ } from "../types";
20
+ import {
21
+ fetchJson,
22
+ loadCachedPredictionResource,
23
+ parseFloatSafe,
24
+ PREDICTION_CACHE_POLICIES,
25
+ } from "./fetch";
26
+
27
+ interface PolymarketEventRecord {
28
+ id: string;
29
+ title: string;
30
+ slug?: string;
31
+ description?: string;
32
+ endDate?: string;
33
+ startDate?: string;
34
+ updatedAt?: string;
35
+ resolutionSource?: string;
36
+ openInterest?: number;
37
+ volume24hr?: number;
38
+ tags?: Array<{ label?: string; slug?: string }>;
39
+ markets?: PolymarketMarketRecord[];
40
+ }
41
+
42
+ interface PolymarketMarketRecord {
43
+ id?: string;
44
+ question: string;
45
+ conditionId?: string;
46
+ slug?: string;
47
+ groupItemTitle?: string;
48
+ description?: string;
49
+ endDate?: string;
50
+ updatedAt?: string;
51
+ createdAt?: string;
52
+ volume24hr?: number;
53
+ volumeNum?: number;
54
+ liquidityNum?: number;
55
+ spread?: number;
56
+ bestBid?: number | null;
57
+ bestAsk?: number | null;
58
+ lastTradePrice?: number | null;
59
+ outcomes?: string | string[];
60
+ outcomePrices?: string | string[];
61
+ clobTokenIds?: string | string[];
62
+ events?: PolymarketEventRecord[];
63
+ resolutionSource?: string;
64
+ active?: boolean;
65
+ closed?: boolean;
66
+ }
67
+
68
+ interface PolymarketSearchResponse {
69
+ events?: PolymarketEventRecord[] | null;
70
+ }
71
+
72
+ interface PolymarketBookResponse {
73
+ bids?: Array<{ price: string; size: string }>;
74
+ asks?: Array<{ price: string; size: string }>;
75
+ last_trade_price?: string;
76
+ }
77
+
78
+ interface PolymarketTradesResponseItem {
79
+ proxyWallet?: string;
80
+ side?: string;
81
+ asset?: string;
82
+ conditionId?: string;
83
+ size?: number;
84
+ price?: number;
85
+ timestamp?: number;
86
+ outcome?: string;
87
+ transactionHash?: string;
88
+ }
89
+
90
+ interface PolymarketHistoryResponse {
91
+ history?: Array<{ t: number; p: number }>;
92
+ }
93
+
94
+ const POLYMARKET_CATALOG_OFFSETS = [0, 200, 400];
95
+ const POLYMARKET_CATEGORY_OFFSETS = [0, 200];
96
+
97
+ function parseStringArray(
98
+ value: string | string[] | undefined,
99
+ ): string[] {
100
+ if (Array.isArray(value)) {
101
+ return value.filter((entry): entry is string => typeof entry === "string");
102
+ }
103
+ if (!value) return [];
104
+ try {
105
+ const parsed = JSON.parse(value) as unknown;
106
+ return Array.isArray(parsed)
107
+ ? parsed.filter((entry): entry is string => typeof entry === "string")
108
+ : [];
109
+ } catch {
110
+ return [];
111
+ }
112
+ }
113
+
114
+ function buildSyntheticPolymarketMarketId(
115
+ record: PolymarketMarketRecord,
116
+ event?: PolymarketEventRecord,
117
+ ): string {
118
+ const slug =
119
+ record.slug?.trim() ??
120
+ record.groupItemTitle?.trim().toLowerCase().replace(/\s+/g, "-") ??
121
+ record.question.trim().toLowerCase().replace(/\s+/g, "-");
122
+ return event?.id ? `${event.id}:${slug}` : slug;
123
+ }
124
+
125
+ function extractPolymarketSlug(url: string): string | null {
126
+ const match = /\/event\/([^/?#]+)/.exec(url);
127
+ return match?.[1] ?? null;
128
+ }
129
+
130
+ function normalizePolymarketBookLevel(level: {
131
+ price: string;
132
+ size: string;
133
+ }): PredictionBookLevel | null {
134
+ const price = parseFloatSafe(level.price);
135
+ const size = parseFloatSafe(level.size);
136
+ if (price == null || size == null) return null;
137
+ return { price, size };
138
+ }
139
+
140
+ function hydratePolymarketMarket(
141
+ record: PolymarketMarketRecord,
142
+ event: PolymarketEventRecord,
143
+ ): PolymarketMarketRecord {
144
+ return {
145
+ ...record,
146
+ events: [event],
147
+ };
148
+ }
149
+
150
+ function resolvePolymarketEventTags(
151
+ event: PolymarketEventRecord | null,
152
+ ): string[] {
153
+ return (event?.tags ?? [])
154
+ .map((tag) => tag.label?.trim())
155
+ .filter((tag): tag is string => !!tag);
156
+ }
157
+
158
+ function flattenPolymarketEvents(
159
+ events: PolymarketEventRecord[],
160
+ searchQuery = "",
161
+ categoryId: PredictionCategoryId = "all",
162
+ ): PredictionMarketSummary[] {
163
+ const normalizedQuery = searchQuery.trim().toLowerCase();
164
+ const deduped = new Map<string, PredictionMarketSummary>();
165
+
166
+ for (const event of events) {
167
+ for (const market of event.markets ?? []) {
168
+ const normalized = normalizePolymarketMarket(
169
+ hydratePolymarketMarket(market, event),
170
+ );
171
+ if (!normalized) continue;
172
+ if (normalized.status !== "open") continue;
173
+ if (
174
+ categoryId !== "all" &&
175
+ !matchesPredictionCategory(normalized, categoryId)
176
+ ) {
177
+ continue;
178
+ }
179
+ if (normalizedQuery) {
180
+ const searchText = [
181
+ normalized.title,
182
+ normalized.marketLabel,
183
+ normalized.eventLabel,
184
+ normalized.category ?? "",
185
+ normalized.marketId,
186
+ ...(normalized.tags ?? []),
187
+ ]
188
+ .join(" ")
189
+ .toLowerCase();
190
+ if (!searchText.includes(normalizedQuery)) continue;
191
+ }
192
+ deduped.set(normalized.key, normalized);
193
+ }
194
+ }
195
+
196
+ return [...deduped.values()];
197
+ }
198
+
199
+ function buildPolymarketCatalogUrl(offset: number, tagSlug?: string): string {
200
+ const url = new URL("https://gamma-api.polymarket.com/events");
201
+ url.searchParams.set("limit", "200");
202
+ url.searchParams.set("offset", String(offset));
203
+ url.searchParams.set("active", "true");
204
+ url.searchParams.set("closed", "false");
205
+ url.searchParams.set("order", "volume24hr");
206
+ url.searchParams.set("ascending", "false");
207
+ if (tagSlug) url.searchParams.set("tag_slug", tagSlug);
208
+ return url.toString();
209
+ }
210
+
211
+ function buildPolymarketSearchUrl(query: string): string {
212
+ const url = new URL("https://gamma-api.polymarket.com/public-search");
213
+ url.searchParams.set("q", query);
214
+ url.searchParams.set("limit_per_type", "40");
215
+ url.searchParams.set("search_profiles", "false");
216
+ url.searchParams.set("search_tags", "false");
217
+ url.searchParams.set("events_status", "open");
218
+ url.searchParams.set("optimized", "true");
219
+ return url.toString();
220
+ }
221
+
222
+ export function normalizePolymarketMarket(
223
+ record: PolymarketMarketRecord,
224
+ options?: {
225
+ keyOverride?: string;
226
+ },
227
+ ): PredictionMarketSummary | null {
228
+ const outcomes = parseStringArray(record.outcomes);
229
+ const prices = parseStringArray(record.outcomePrices).map((value) =>
230
+ parseFloatSafe(value),
231
+ );
232
+ const tokenIds = parseStringArray(record.clobTokenIds);
233
+ if (outcomes.length < 2 || prices.length < 2) return null;
234
+
235
+ const event = record.events?.[0];
236
+ const eventTags = resolvePolymarketEventTags(event ?? null);
237
+ const marketId = record.id ?? buildSyntheticPolymarketMarketId(record, event);
238
+ const yesIndex = outcomes.findIndex(
239
+ (outcome) => outcome.toLowerCase() === "yes",
240
+ );
241
+ const noIndex = outcomes.findIndex(
242
+ (outcome) => outcome.toLowerCase() === "no",
243
+ );
244
+ const fallbackYesIndex = yesIndex >= 0 ? yesIndex : 0;
245
+ const fallbackNoIndex = noIndex >= 0 ? noIndex : 1;
246
+ const yesPrice = prices[fallbackYesIndex] ?? null;
247
+ const noPrice =
248
+ prices[fallbackNoIndex] ??
249
+ (yesPrice != null ? Math.max(0, 1 - yesPrice) : null);
250
+ const marketLabel = record.groupItemTitle?.trim() || record.question;
251
+
252
+ return {
253
+ key: options?.keyOverride ?? `polymarket:${marketId}`,
254
+ venue: "polymarket",
255
+ marketId,
256
+ title: record.question,
257
+ marketLabel,
258
+ eventLabel: event?.title ?? record.question,
259
+ eventId: event?.id,
260
+ category: resolvePredictionDisplayCategory(eventTags),
261
+ tags: eventTags,
262
+ status:
263
+ record.closed ? "closed" : record.active === false ? "pending" : "open",
264
+ url: record.slug
265
+ ? `https://polymarket.com/event/${record.slug}`
266
+ : "https://polymarket.com",
267
+ description: record.description ?? event?.description ?? "",
268
+ endsAt: record.endDate ?? event?.endDate ?? null,
269
+ updatedAt:
270
+ record.updatedAt ??
271
+ event?.updatedAt ??
272
+ record.createdAt ??
273
+ event?.startDate ??
274
+ null,
275
+ yesPrice,
276
+ noPrice,
277
+ yesBid: record.bestBid ?? null,
278
+ yesAsk: record.bestAsk ?? null,
279
+ noBid:
280
+ record.bestBid != null
281
+ ? Math.max(0, 1 - (record.bestAsk ?? record.bestBid))
282
+ : null,
283
+ noAsk:
284
+ record.bestAsk != null
285
+ ? Math.max(0, 1 - (record.bestBid ?? record.bestAsk))
286
+ : null,
287
+ spread:
288
+ record.spread ??
289
+ (record.bestAsk != null && record.bestBid != null
290
+ ? record.bestAsk - record.bestBid
291
+ : null),
292
+ lastTradePrice: record.lastTradePrice ?? null,
293
+ volume24h: record.volume24hr ?? event?.volume24hr ?? null,
294
+ volume24hUnit: "usd",
295
+ totalVolume: record.volumeNum ?? null,
296
+ totalVolumeUnit: "usd",
297
+ openInterest: event?.openInterest ?? null,
298
+ openInterestUnit: "usd",
299
+ liquidity: record.liquidityNum ?? null,
300
+ liquidityUnit: "usd",
301
+ resolutionSource: record.resolutionSource ?? event?.resolutionSource ?? "",
302
+ yesTokenId: tokenIds[fallbackYesIndex],
303
+ noTokenId: tokenIds[fallbackNoIndex],
304
+ conditionId: record.conditionId,
305
+ };
306
+ }
307
+
308
+ function sortPolymarketMarkets(
309
+ markets: PredictionMarketSummary[],
310
+ ): PredictionMarketSummary[] {
311
+ return [...markets].sort(
312
+ (left, right) => (right.volume24h ?? 0) - (left.volume24h ?? 0),
313
+ );
314
+ }
315
+
316
+ function reconcilePolymarketSearchEvents(
317
+ searchEvents: PolymarketEventRecord[],
318
+ hydratedEvents: PolymarketEventRecord[],
319
+ ): PolymarketEventRecord[] {
320
+ if (searchEvents.length === 0) return hydratedEvents;
321
+ const hydratedById = new Map(
322
+ hydratedEvents.map((event) => [event.id, event] as const),
323
+ );
324
+ return searchEvents.map((event) => hydratedById.get(event.id) ?? event);
325
+ }
326
+
327
+ export async function loadPolymarketCatalog(
328
+ searchQuery = "",
329
+ categoryId: PredictionCategoryId = "all",
330
+ ): Promise<PredictionMarketSummary[]> {
331
+ const normalizedQuery = searchQuery.trim().toLowerCase();
332
+ return await loadCachedPredictionResource(
333
+ "catalog",
334
+ buildPredictionCatalogResourceKey("polymarket", categoryId, normalizedQuery),
335
+ async () => {
336
+ if (normalizedQuery.length > 0) {
337
+ const response = await fetchJson<PolymarketSearchResponse>(
338
+ buildPolymarketSearchUrl(normalizedQuery),
339
+ );
340
+ const searchEvents = response.events ?? [];
341
+ const hydratedEvents = (
342
+ await Promise.all(
343
+ [...new Set(searchEvents.map((event) => event.id).filter(Boolean))]
344
+ .map((eventId) => loadPolymarketEvent(eventId)),
345
+ )
346
+ ).filter((event): event is PolymarketEventRecord => event != null);
347
+ const resolvedEvents = reconcilePolymarketSearchEvents(
348
+ searchEvents,
349
+ hydratedEvents,
350
+ );
351
+ return sortPolymarketMarkets(
352
+ flattenPolymarketEvents(
353
+ resolvedEvents,
354
+ normalizedQuery,
355
+ categoryId,
356
+ ),
357
+ );
358
+ }
359
+
360
+ if (categoryId !== "all") {
361
+ const tagSlugs = getPolymarketCategoryTagSlugs(categoryId);
362
+ const categoryPages = await Promise.all(
363
+ tagSlugs.flatMap((tagSlug) =>
364
+ POLYMARKET_CATEGORY_OFFSETS.map((offset) =>
365
+ fetchJson<PolymarketEventRecord[]>(
366
+ buildPolymarketCatalogUrl(offset, tagSlug),
367
+ ).catch(() => []),
368
+ ),
369
+ ),
370
+ );
371
+ const categorized = sortPolymarketMarkets(
372
+ flattenPolymarketEvents(categoryPages.flat(), "", categoryId),
373
+ );
374
+ if (categorized.length > 0) return categorized;
375
+ }
376
+
377
+ const pages = await Promise.all(
378
+ POLYMARKET_CATALOG_OFFSETS.map((offset) =>
379
+ fetchJson<PolymarketEventRecord[]>(buildPolymarketCatalogUrl(offset)),
380
+ ),
381
+ );
382
+ return sortPolymarketMarkets(
383
+ flattenPolymarketEvents(pages.flat(), "", categoryId),
384
+ );
385
+ },
386
+ PREDICTION_CACHE_POLICIES.catalog,
387
+ );
388
+ }
389
+
390
+ async function loadPolymarketEvent(
391
+ eventId: string | undefined,
392
+ ): Promise<PolymarketEventRecord | null> {
393
+ if (!eventId) return null;
394
+ try {
395
+ return await loadCachedPredictionResource(
396
+ "rules",
397
+ `polymarket:event:${eventId}`,
398
+ async () =>
399
+ await fetchJson<PolymarketEventRecord>(
400
+ `https://gamma-api.polymarket.com/events/${eventId}`,
401
+ ),
402
+ PREDICTION_CACHE_POLICIES.rules,
403
+ );
404
+ } catch {
405
+ return null;
406
+ }
407
+ }
408
+
409
+ function findCanonicalPolymarketMarket(
410
+ event: PolymarketEventRecord,
411
+ summary: PredictionMarketSummary,
412
+ ): PolymarketMarketRecord | null {
413
+ const marketSlug = extractPolymarketSlug(summary.url);
414
+ return (
415
+ event.markets?.find((market) => {
416
+ if (summary.conditionId && market.conditionId === summary.conditionId) {
417
+ return true;
418
+ }
419
+ if (market.id && market.id === summary.marketId) {
420
+ return true;
421
+ }
422
+ if (marketSlug && market.slug === marketSlug) {
423
+ return true;
424
+ }
425
+ if (market.question === summary.title) {
426
+ return true;
427
+ }
428
+ return (
429
+ !!summary.marketLabel &&
430
+ !!market.groupItemTitle &&
431
+ market.groupItemTitle === summary.marketLabel
432
+ );
433
+ }) ?? null
434
+ );
435
+ }
436
+
437
+ async function resolvePolymarketSummary(
438
+ summary: PredictionMarketSummary,
439
+ ): Promise<{
440
+ event: PolymarketEventRecord | null;
441
+ summary: PredictionMarketSummary;
442
+ }> {
443
+ const event = await loadPolymarketEvent(summary.eventId);
444
+ if (!event) {
445
+ return { event: null, summary };
446
+ }
447
+
448
+ const eventTags = resolvePolymarketEventTags(event);
449
+ const canonicalMarket = findCanonicalPolymarketMarket(event, summary);
450
+ if (!canonicalMarket) {
451
+ return {
452
+ event,
453
+ summary: {
454
+ ...summary,
455
+ eventLabel: event.title ?? summary.eventLabel,
456
+ category:
457
+ summary.category ?? resolvePredictionDisplayCategory(eventTags),
458
+ tags: summary.tags ?? eventTags,
459
+ description: summary.description || event.description || "",
460
+ resolutionSource:
461
+ summary.resolutionSource || event.resolutionSource || "",
462
+ openInterest: event.openInterest ?? summary.openInterest,
463
+ },
464
+ };
465
+ }
466
+
467
+ const normalized = normalizePolymarketMarket(
468
+ hydratePolymarketMarket(canonicalMarket, event),
469
+ { keyOverride: summary.key },
470
+ );
471
+ return {
472
+ event,
473
+ summary: normalized ?? summary,
474
+ };
475
+ }
476
+
477
+ export async function loadPolymarketHistory(
478
+ summary: PredictionMarketSummary,
479
+ range: "1D" | "1W" | "1M" | "ALL",
480
+ ): Promise<PredictionHistoryPoint[]> {
481
+ const tokenId = summary.yesTokenId;
482
+ if (!tokenId) return [];
483
+ const interval =
484
+ range === "1D"
485
+ ? "1d"
486
+ : range === "1W"
487
+ ? "1w"
488
+ : range === "1M"
489
+ ? "1m"
490
+ : "max";
491
+ const fidelity =
492
+ range === "1D" ? 15 : range === "1W" ? 60 : range === "1M" ? 240 : 1440;
493
+
494
+ return await loadCachedPredictionResource(
495
+ "history",
496
+ `${summary.key}:${range}`,
497
+ async () => {
498
+ const response = await fetchJson<PolymarketHistoryResponse>(
499
+ `https://clob.polymarket.com/prices-history?market=${tokenId}&interval=${interval}&fidelity=${fidelity}`,
500
+ );
501
+ return (response.history ?? [])
502
+ .map((point) => ({
503
+ date: new Date(point.t * 1000),
504
+ close: point.p,
505
+ }))
506
+ .filter((point) => Number.isFinite(point.date.getTime()));
507
+ },
508
+ PREDICTION_CACHE_POLICIES.history,
509
+ );
510
+ }
511
+
512
+ export async function loadPolymarketTrades(
513
+ summary: PredictionMarketSummary,
514
+ ): Promise<PredictionTrade[]> {
515
+ if (!summary.conditionId) return [];
516
+ return await loadCachedPredictionResource(
517
+ "trades",
518
+ summary.key,
519
+ async () => {
520
+ const items = await fetchJson<PolymarketTradesResponseItem[]>(
521
+ `https://data-api.polymarket.com/trades?market=${summary.conditionId}&limit=30`,
522
+ );
523
+ return items.map((item, index) => ({
524
+ id:
525
+ item.transactionHash ??
526
+ `${summary.key}:${index}:${item.timestamp ?? 0}`,
527
+ timestamp:
528
+ typeof item.timestamp === "number"
529
+ ? item.timestamp * 1000
530
+ : Date.now(),
531
+ side: String(item.side).toUpperCase() === "SELL" ? "sell" : "buy",
532
+ outcome: String(item.outcome).toLowerCase() === "no" ? "no" : "yes",
533
+ price: typeof item.price === "number" ? item.price : 0,
534
+ size: typeof item.size === "number" ? item.size : 0,
535
+ }));
536
+ },
537
+ PREDICTION_CACHE_POLICIES.trades,
538
+ );
539
+ }
540
+
541
+ export async function loadPolymarketBook(
542
+ summary: PredictionMarketSummary,
543
+ ): Promise<PredictionBookSnapshot> {
544
+ const yesTokenId = summary.yesTokenId;
545
+ const noTokenId = summary.noTokenId;
546
+ if (!yesTokenId && !noTokenId) {
547
+ return {
548
+ yesBids: [],
549
+ yesAsks: [],
550
+ noBids: [],
551
+ noAsks: [],
552
+ lastTradePrice: summary.lastTradePrice,
553
+ };
554
+ }
555
+
556
+ return await loadCachedPredictionResource(
557
+ "book",
558
+ summary.key,
559
+ async () => {
560
+ const [yesBook, noBook] = await Promise.all([
561
+ yesTokenId
562
+ ? fetchJson<PolymarketBookResponse>(
563
+ `https://clob.polymarket.com/book?token_id=${yesTokenId}`,
564
+ ).catch(() => null)
565
+ : Promise.resolve(null),
566
+ noTokenId
567
+ ? fetchJson<PolymarketBookResponse>(
568
+ `https://clob.polymarket.com/book?token_id=${noTokenId}`,
569
+ ).catch(() => null)
570
+ : Promise.resolve(null),
571
+ ]);
572
+ return {
573
+ yesBids: (yesBook?.bids ?? [])
574
+ .map(normalizePolymarketBookLevel)
575
+ .filter((level): level is PredictionBookLevel => level != null),
576
+ yesAsks: (yesBook?.asks ?? [])
577
+ .map(normalizePolymarketBookLevel)
578
+ .filter((level): level is PredictionBookLevel => level != null),
579
+ noBids: (noBook?.bids ?? [])
580
+ .map(normalizePolymarketBookLevel)
581
+ .filter((level): level is PredictionBookLevel => level != null),
582
+ noAsks: (noBook?.asks ?? [])
583
+ .map(normalizePolymarketBookLevel)
584
+ .filter((level): level is PredictionBookLevel => level != null),
585
+ lastTradePrice:
586
+ parseFloatSafe(yesBook?.last_trade_price) ??
587
+ parseFloatSafe(noBook?.last_trade_price) ??
588
+ summary.lastTradePrice,
589
+ };
590
+ },
591
+ PREDICTION_CACHE_POLICIES.book,
592
+ );
593
+ }
594
+
595
+ export async function loadPolymarketDetail(
596
+ summary: PredictionMarketSummary,
597
+ range: "1D" | "1W" | "1M" | "ALL",
598
+ ): Promise<PredictionMarketDetail> {
599
+ return await loadCachedPredictionResource(
600
+ "detail",
601
+ buildPredictionDetailResourceKey(summary.key, range),
602
+ async () => {
603
+ const resolved = await resolvePolymarketSummary(summary);
604
+ const resolvedSummary = resolved.summary;
605
+ const [history, book, trades] = await Promise.all([
606
+ loadPolymarketHistory(resolvedSummary, range),
607
+ loadPolymarketBook(resolvedSummary),
608
+ loadPolymarketTrades(resolvedSummary),
609
+ ]);
610
+ const event = resolved.event;
611
+ const siblings: PredictionSiblingMarket[] = (event?.markets ?? [])
612
+ .map((market) =>
613
+ event
614
+ ? normalizePolymarketMarket(
615
+ hydratePolymarketMarket(market, event),
616
+ )
617
+ : null,
618
+ )
619
+ .filter(
620
+ (market): market is PredictionMarketSummary =>
621
+ market != null && market.status === "open",
622
+ )
623
+ .map((market) => ({
624
+ key: market.key,
625
+ marketId: market.marketId,
626
+ label: market.marketLabel,
627
+ yesPrice: market.yesPrice,
628
+ volume24h: market.volume24h,
629
+ }));
630
+
631
+ return {
632
+ summary: {
633
+ ...resolvedSummary,
634
+ eventLabel: event?.title ?? resolvedSummary.eventLabel,
635
+ category:
636
+ resolvedSummary.category ??
637
+ resolvePredictionDisplayCategory(resolvePolymarketEventTags(event)),
638
+ description:
639
+ resolvedSummary.description || event?.description || "",
640
+ resolutionSource:
641
+ resolvedSummary.resolutionSource || event?.resolutionSource || "",
642
+ openInterest: event?.openInterest ?? resolvedSummary.openInterest,
643
+ tags:
644
+ resolvedSummary.tags ?? resolvePolymarketEventTags(event),
645
+ },
646
+ siblings,
647
+ rules: [
648
+ resolvedSummary.description,
649
+ resolvedSummary.resolutionSource || "",
650
+ event?.description || "",
651
+ event?.resolutionSource || "",
652
+ ].filter((value) => value.trim().length > 0),
653
+ history,
654
+ book,
655
+ trades,
656
+ };
657
+ },
658
+ PREDICTION_CACHE_POLICIES.detail,
659
+ );
660
+ }