gloomberb 0.4.0 → 0.5.0

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 (218) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/app.tsx +184 -69
  4. package/src/cli/commands/ticker.ts +45 -15
  5. package/src/cli/helpers.ts +4 -2
  6. package/src/cli/registry.test.ts +42 -35
  7. package/src/cli.test.ts +4 -2
  8. package/src/components/chart/chart-control-hint.tsx +39 -0
  9. package/src/components/chart/chart-controller.test.ts +110 -0
  10. package/src/components/chart/chart-controller.ts +364 -2
  11. package/src/components/chart/chart-data.ts +177 -29
  12. package/src/components/chart/chart-indicator-selector.tsx +53 -0
  13. package/src/components/chart/chart-pane-settings.ts +20 -0
  14. package/src/components/chart/chart-renderer.ts +220 -30
  15. package/src/components/chart/chart-resolution.test.ts +67 -0
  16. package/src/components/chart/chart-resolution.ts +74 -4
  17. package/src/components/chart/chart-scroll.test.ts +69 -0
  18. package/src/components/chart/chart-scroll.ts +104 -0
  19. package/src/components/chart/chart-types.ts +12 -0
  20. package/src/components/chart/chart-viewport.test.ts +5 -1
  21. package/src/components/chart/chart-viewport.ts +1 -1
  22. package/src/components/chart/chart.test.ts +249 -10
  23. package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
  24. package/src/components/chart/comparison-chart-renderer.ts +40 -3
  25. package/src/components/chart/comparison-stock-chart.tsx +218 -123
  26. package/src/components/chart/indicators/bands.test.ts +50 -0
  27. package/src/components/chart/indicators/bands.ts +29 -0
  28. package/src/components/chart/indicators/moving-averages.test.ts +54 -0
  29. package/src/components/chart/indicators/moving-averages.ts +42 -0
  30. package/src/components/chart/indicators/options.test.ts +32 -0
  31. package/src/components/chart/indicators/options.ts +107 -0
  32. package/src/components/chart/indicators/oscillators.test.ts +79 -0
  33. package/src/components/chart/indicators/oscillators.ts +92 -0
  34. package/src/components/chart/indicators/types.ts +36 -0
  35. package/src/components/chart/native/chart-rasterizer.ts +51 -0
  36. package/src/components/chart/native/kitty.test.ts +68 -0
  37. package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
  38. package/src/components/chart/stock-chart.test.ts +239 -0
  39. package/src/components/chart/stock-chart.tsx +2257 -372
  40. package/src/components/command-bar/command-bar.test.tsx +337 -5
  41. package/src/components/command-bar/command-bar.tsx +296 -120
  42. package/src/components/command-bar/helpers.ts +1 -1
  43. package/src/components/command-bar/root-shortcuts.ts +33 -4
  44. package/src/components/command-bar/view-model.test.ts +3 -3
  45. package/src/components/command-bar/view-model.ts +4 -4
  46. package/src/components/command-bar/workflow-ops.ts +10 -0
  47. package/src/components/layout/floating-pane.tsx +3 -1
  48. package/src/components/layout/header.tsx +5 -4
  49. package/src/components/layout/pane-sizing.ts +6 -0
  50. package/src/components/layout/pane.tsx +5 -1
  51. package/src/components/layout/shell.test.tsx +263 -32
  52. package/src/components/layout/shell.tsx +10 -6
  53. package/src/components/layout/status-bar.test.tsx +16 -7
  54. package/src/components/layout/status-bar.tsx +4 -1
  55. package/src/components/markdown-editor.tsx +1 -0
  56. package/src/components/pane-settings-dialog.tsx +13 -152
  57. package/src/components/pane-template-wizard.tsx +1 -0
  58. package/src/components/price-selector-dialog.tsx +12 -7
  59. package/src/components/ticker-badge-text.test.tsx +50 -0
  60. package/src/components/ticker-badge-text.tsx +17 -2
  61. package/src/components/ticker-badge.tsx +2 -2
  62. package/src/components/ticker-list-table.tsx +62 -2
  63. package/src/components/toggle-list.tsx +15 -2
  64. package/src/components/ui/data-table.tsx +82 -5
  65. package/src/components/ui/external-link.tsx +48 -11
  66. package/src/components/ui/fields.tsx +2 -1
  67. package/src/components/ui/index.ts +8 -1
  68. package/src/components/ui/multi-select-chips.tsx +69 -0
  69. package/src/components/ui/multi-select-dialog.tsx +267 -0
  70. package/src/components/ui/multi-select.ts +78 -0
  71. package/src/components/ui/ui.test.tsx +343 -1
  72. package/src/data/plugin-state-store.ts +30 -19
  73. package/src/data/resource-store.ts +133 -103
  74. package/src/data/session-store.ts +19 -12
  75. package/src/data/sqlite-cache.test.ts +37 -2
  76. package/src/data/sqlite-database.ts +8 -2
  77. package/src/data/sqlite-retry.test.ts +63 -0
  78. package/src/data/sqlite-retry.ts +66 -0
  79. package/src/data/ticker-store.ts +29 -18
  80. package/src/index.tsx +6 -1
  81. package/src/market-data/coordinator.test.ts +114 -5
  82. package/src/market-data/coordinator.ts +70 -1
  83. package/src/market-data/hooks.test.tsx +73 -2
  84. package/src/market-data/hooks.tsx +16 -4
  85. package/src/notifications/app-notifier.test.ts +177 -0
  86. package/src/notifications/app-notifier.ts +241 -0
  87. package/src/pane-settings.test.ts +2 -0
  88. package/src/plugins/builtin/ai/index.test.tsx +1 -1
  89. package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
  90. package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
  91. package/src/plugins/builtin/ai/ticker-context.ts +3 -2
  92. package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
  93. package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
  94. package/src/plugins/builtin/alerts/index.test.tsx +357 -0
  95. package/src/plugins/builtin/alerts/index.tsx +519 -0
  96. package/src/plugins/builtin/alerts/types.ts +14 -0
  97. package/src/plugins/builtin/analytics/index.test.tsx +300 -0
  98. package/src/plugins/builtin/analytics/index.tsx +775 -0
  99. package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
  100. package/src/plugins/builtin/analytics/metrics.ts +155 -0
  101. package/src/plugins/builtin/ask-ai.test.tsx +2 -0
  102. package/src/plugins/builtin/chat-controller.test.ts +92 -18
  103. package/src/plugins/builtin/chat-controller.ts +40 -11
  104. package/src/plugins/builtin/chat.test.tsx +715 -4
  105. package/src/plugins/builtin/chat.tsx +420 -119
  106. package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
  107. package/src/plugins/builtin/comparison-chart.tsx +1 -2
  108. package/src/plugins/builtin/correlation/compute.test.ts +147 -0
  109. package/src/plugins/builtin/correlation/compute.ts +115 -0
  110. package/src/plugins/builtin/correlation/index.tsx +385 -0
  111. package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
  112. package/src/plugins/builtin/correlation/settings.test.ts +34 -0
  113. package/src/plugins/builtin/correlation/settings.ts +95 -0
  114. package/src/plugins/builtin/debug.tsx +36 -50
  115. package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
  116. package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
  117. package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
  118. package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
  119. package/src/plugins/builtin/earnings/index.tsx +337 -0
  120. package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
  121. package/src/plugins/builtin/econ/calendar-source.ts +121 -0
  122. package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
  123. package/src/plugins/builtin/econ/fred-client.ts +79 -0
  124. package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
  125. package/src/plugins/builtin/econ/index.tsx +999 -0
  126. package/src/plugins/builtin/econ/types.ts +13 -0
  127. package/src/plugins/builtin/fred-settings.ts +10 -0
  128. package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
  129. package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
  130. package/src/plugins/builtin/help.test.tsx +1 -5
  131. package/src/plugins/builtin/help.tsx +1 -16
  132. package/src/plugins/builtin/insider/index.tsx +417 -0
  133. package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
  134. package/src/plugins/builtin/insider/insider-data.ts +49 -0
  135. package/src/plugins/builtin/layout-manager.tsx +33 -16
  136. package/src/plugins/builtin/market-movers/index.tsx +573 -0
  137. package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
  138. package/src/plugins/builtin/market-movers/screener.ts +105 -0
  139. package/src/plugins/builtin/options.tsx +4 -3
  140. package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
  141. package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
  142. package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
  143. package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
  144. package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
  145. package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
  146. package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
  147. package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
  148. package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
  149. package/src/plugins/builtin/sec.tsx +114 -7
  150. package/src/plugins/builtin/sectors/index.tsx +359 -0
  151. package/src/plugins/builtin/sectors/sector-data.ts +18 -0
  152. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
  153. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
  154. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
  155. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
  156. package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
  157. package/src/plugins/builtin/world-indices/index.tsx +410 -0
  158. package/src/plugins/builtin/world-indices/indices.ts +52 -0
  159. package/src/plugins/builtin/yield-curve/index.tsx +242 -0
  160. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
  161. package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
  162. package/src/plugins/catalog.ts +22 -0
  163. package/src/plugins/gridlock-notification.ts +15 -0
  164. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  165. package/src/plugins/ibkr/gateway-service.test.ts +22 -0
  166. package/src/plugins/ibkr/gateway-service.ts +6 -0
  167. package/src/plugins/ibkr/index.test.tsx +1 -1
  168. package/src/plugins/ibkr/index.tsx +8 -2
  169. package/src/plugins/ibkr/trade-tab.tsx +23 -5
  170. package/src/plugins/ibkr/trade-utils.test.ts +1 -1
  171. package/src/plugins/ibkr/trade-utils.ts +7 -6
  172. package/src/plugins/ibkr/trading-pane.tsx +8 -7
  173. package/src/plugins/plugin-runtime.test.tsx +2 -0
  174. package/src/plugins/plugin-runtime.tsx +10 -0
  175. package/src/plugins/prediction-markets/controller-data.ts +129 -14
  176. package/src/plugins/prediction-markets/controller.ts +1 -1
  177. package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
  178. package/src/plugins/prediction-markets/index.tsx +5 -1
  179. package/src/plugins/prediction-markets/pane.test.tsx +78 -0
  180. package/src/plugins/prediction-markets/pane.tsx +7 -3
  181. package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
  182. package/src/plugins/prediction-markets/services/fetch.ts +17 -7
  183. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  184. package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
  185. package/src/plugins/prediction-markets/test-helpers.tsx +22 -5
  186. package/src/plugins/registry.ts +45 -13
  187. package/src/sources/provider-router.test.ts +3 -2
  188. package/src/sources/sec-edgar.ts +13 -1
  189. package/src/sources/yahoo-finance.ts +75 -1
  190. package/src/state/app-context-selector.test.tsx +17 -1
  191. package/src/state/app-context.test.tsx +24 -0
  192. package/src/state/app-context.tsx +29 -6
  193. package/src/theme/colors.ts +5 -0
  194. package/src/theme/themes.ts +25 -0
  195. package/src/types/config.test.ts +107 -0
  196. package/src/types/config.ts +30 -1
  197. package/src/types/data-provider.ts +13 -0
  198. package/src/types/plugin.ts +35 -1
  199. package/src/updater.test.ts +2 -0
  200. package/src/utils/api-client.ts +12 -0
  201. package/src/utils/format.test.ts +11 -1
  202. package/src/utils/format.ts +86 -8
  203. package/src/utils/inline-content-tokenizer.test.ts +22 -0
  204. package/src/utils/inline-content-tokenizer.ts +76 -0
  205. package/src/utils/link-tokenizer.test.ts +27 -0
  206. package/src/utils/link-tokenizer.ts +93 -0
  207. package/src/utils/market-format.test.ts +109 -0
  208. package/src/utils/market-format.ts +299 -0
  209. package/src/utils/market-status.ts +3 -2
  210. package/src/utils/selection-clipboard.test.ts +43 -0
  211. package/src/utils/selection-clipboard.ts +88 -0
  212. package/src/utils/terminal-input-reset.test.ts +50 -0
  213. package/src/utils/terminal-input-reset.ts +24 -0
  214. package/src/utils/throttled-fetch.test.ts +145 -0
  215. package/src/utils/throttled-fetch.ts +212 -0
  216. package/src/utils/ticker-tokenizer.test.ts +31 -0
  217. package/src/utils/ticker-tokenizer.ts +7 -42
  218. package/src/version.ts +1 -1
@@ -0,0 +1,63 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { resolveFredMapping, getFredSeriesId, getRelatedTickers } from "./fred-series-map";
3
+
4
+ describe("resolveFredMapping", () => {
5
+ test("maps exact US event titles", () => {
6
+ const m = resolveFredMapping("CPI m/m", "US");
7
+ expect(m).not.toBeNull();
8
+ expect(m!.seriesId).toBe("CPIAUCSL");
9
+ expect(m!.relatedTickers).toContain("TIP");
10
+ });
11
+
12
+ test("case insensitive matching", () => {
13
+ expect(resolveFredMapping("cpi m/m", "US")?.seriesId).toBe("CPIAUCSL");
14
+ expect(resolveFredMapping("CPI M/M", "US")?.seriesId).toBe("CPIAUCSL");
15
+ });
16
+
17
+ test("strips prefixes for fuzzy match", () => {
18
+ expect(resolveFredMapping("Final GDP q/q", "US")?.seriesId).toBe("GDP");
19
+ expect(resolveFredMapping("Prelim GDP q/q", "US")?.seriesId).toBe("GDP");
20
+ expect(resolveFredMapping("Advance GDP q/q", "US")?.seriesId).toBe("GDP");
21
+ });
22
+
23
+ test("returns null for non-US events", () => {
24
+ expect(resolveFredMapping("CPI m/m", "EU")).toBeNull();
25
+ expect(resolveFredMapping("CPI m/m", "GB")).toBeNull();
26
+ });
27
+
28
+ test("returns null for unmapped events", () => {
29
+ expect(resolveFredMapping("President Trump Speaks", "US")).toBeNull();
30
+ expect(resolveFredMapping("Bank Holiday", "US")).toBeNull();
31
+ });
32
+
33
+ test("maps FOMC and Fed events", () => {
34
+ expect(resolveFredMapping("Federal Funds Rate", "US")?.seriesId).toBe("FEDFUNDS");
35
+ });
36
+
37
+ test("maps commodity indicators", () => {
38
+ expect(resolveFredMapping("Crude Oil Inventories", "US")?.seriesId).toBe("WCOILWTICO");
39
+ expect(resolveFredMapping("Natural Gas Storage", "US")?.seriesId).toBe("NATURALGAS");
40
+ });
41
+ });
42
+
43
+ describe("getFredSeriesId", () => {
44
+ test("returns series ID for known events", () => {
45
+ expect(getFredSeriesId("Unemployment Rate", "US")).toBe("UNRATE");
46
+ });
47
+
48
+ test("returns null for unknown events", () => {
49
+ expect(getFredSeriesId("Some Random Event", "US")).toBeNull();
50
+ });
51
+ });
52
+
53
+ describe("getRelatedTickers", () => {
54
+ test("returns related tickers for known events", () => {
55
+ const tickers = getRelatedTickers("CPI m/m", "US");
56
+ expect(tickers).toContain("TIP");
57
+ expect(tickers).toContain("DX-Y.NYB");
58
+ });
59
+
60
+ test("returns empty array for unknown events", () => {
61
+ expect(getRelatedTickers("Unknown Event", "US")).toEqual([]);
62
+ });
63
+ });
@@ -0,0 +1,79 @@
1
+ import { createThrottledFetch, type ThrottledFetchClient } from "../../../utils/throttled-fetch";
2
+
3
+ const FRED_BASE = "https://api.stlouisfed.org/fred";
4
+
5
+ // FRED free tier: 120 requests per minute, but be conservative
6
+ const fredFetch = createThrottledFetch({
7
+ requestsPerMinute: 30,
8
+ maxRetries: 2,
9
+ timeoutMs: 15_000,
10
+ });
11
+
12
+ export interface FredObservation {
13
+ date: string; // "2024-01-01"
14
+ value: number | null; // null for missing/revised data
15
+ }
16
+
17
+ export interface FredSeriesInfo {
18
+ id: string;
19
+ title: string;
20
+ units: string;
21
+ frequency: string;
22
+ seasonalAdjustment: string;
23
+ source: string;
24
+ notes: string;
25
+ }
26
+
27
+ export async function fetchFredObservations(
28
+ apiKey: string,
29
+ seriesId: string,
30
+ options?: { startDate?: string; endDate?: string; limit?: number; sortOrder?: "asc" | "desc" },
31
+ ): Promise<FredObservation[]> {
32
+ const params = new URLSearchParams({
33
+ series_id: seriesId,
34
+ api_key: apiKey,
35
+ file_type: "json",
36
+ sort_order: options?.sortOrder ?? "desc",
37
+ });
38
+ if (options?.startDate) params.set("observation_start", options.startDate);
39
+ if (options?.endDate) params.set("observation_end", options.endDate);
40
+ if (options?.limit) params.set("limit", String(options.limit));
41
+
42
+ const url = `${FRED_BASE}/series/observations?${params}`;
43
+ const data = await fredFetch.fetchJson<any>(url);
44
+
45
+ if (!Array.isArray(data?.observations)) return [];
46
+
47
+ return data.observations
48
+ .map((obs: any) => ({
49
+ date: obs.date,
50
+ value: obs.value === "." ? null : parseFloat(obs.value),
51
+ }))
52
+ .filter((obs: FredObservation) => obs.value !== null && !isNaN(obs.value as number));
53
+ }
54
+
55
+ export async function fetchFredSeriesInfo(
56
+ apiKey: string,
57
+ seriesId: string,
58
+ ): Promise<FredSeriesInfo | null> {
59
+ const params = new URLSearchParams({
60
+ series_id: seriesId,
61
+ api_key: apiKey,
62
+ file_type: "json",
63
+ });
64
+ const url = `${FRED_BASE}/series?${params}`;
65
+ const data = await fredFetch.fetchJson<any>(url);
66
+
67
+ const series = data?.seriess?.[0];
68
+ if (!series) return null;
69
+
70
+ return {
71
+ id: series.id,
72
+ title: series.title ?? seriesId,
73
+ units: series.units ?? "",
74
+ frequency: series.frequency ?? "",
75
+ seasonalAdjustment: series.seasonal_adjustment ?? "",
76
+ source: "", // FRED doesn't include source in this endpoint
77
+ notes: series.notes ?? "",
78
+ };
79
+ }
@@ -0,0 +1,88 @@
1
+ interface FredMapping {
2
+ seriesId: string;
3
+ /** How to display the value: "level" shows the raw number, "change" shows month-over-month or quarter-over-quarter percent change */
4
+ displayMode: "level" | "change";
5
+ relatedTickers: string[];
6
+ }
7
+
8
+ // Normalized event title → FRED mapping
9
+ const SERIES_MAP: Record<string, FredMapping> = {
10
+ "cpi m/m": { seriesId: "CPIAUCSL", displayMode: "change", relatedTickers: ["TIP", "DX-Y.NYB", "^TNX"] },
11
+ "core cpi m/m": { seriesId: "CPILFESL", displayMode: "change", relatedTickers: ["TIP", "DX-Y.NYB", "^TNX"] },
12
+ "cpi y/y": { seriesId: "CPIAUCSL", displayMode: "level", relatedTickers: ["TIP", "DX-Y.NYB"] },
13
+ "core cpi y/y": { seriesId: "CPILFESL", displayMode: "level", relatedTickers: ["TIP", "DX-Y.NYB"] },
14
+ "ppi m/m": { seriesId: "PPIACO", displayMode: "change", relatedTickers: ["DX-Y.NYB"] },
15
+ "core pce price index m/m": { seriesId: "PCEPILFE", displayMode: "change", relatedTickers: ["TIP", "DX-Y.NYB", "^TNX"] },
16
+ "pce price index m/m": { seriesId: "PCEPI", displayMode: "change", relatedTickers: ["TIP", "DX-Y.NYB"] },
17
+ "final gdp q/q": { seriesId: "GDP", displayMode: "change", relatedTickers: ["SPY", "DX-Y.NYB"] },
18
+ "advance gdp q/q": { seriesId: "GDP", displayMode: "change", relatedTickers: ["SPY", "DX-Y.NYB"] },
19
+ "prelim gdp q/q": { seriesId: "GDP", displayMode: "change", relatedTickers: ["SPY", "DX-Y.NYB"] },
20
+ "gdp q/q": { seriesId: "GDP", displayMode: "change", relatedTickers: ["SPY", "DX-Y.NYB"] },
21
+ "unemployment rate": { seriesId: "UNRATE", displayMode: "level", relatedTickers: ["SPY", "DX-Y.NYB"] },
22
+ "unemployment claims": { seriesId: "ICSA", displayMode: "level", relatedTickers: ["SPY"] },
23
+ "non-farm employment change": { seriesId: "PAYEMS", displayMode: "change", relatedTickers: ["SPY", "DX-Y.NYB", "^TNX"] },
24
+ "adp non-farm employment change": { seriesId: "NPPTTL", displayMode: "change", relatedTickers: ["SPY"] },
25
+ "retail sales m/m": { seriesId: "RSAFS", displayMode: "change", relatedTickers: ["XRT", "SPY"] },
26
+ "core retail sales m/m": { seriesId: "RSFSXMV", displayMode: "change", relatedTickers: ["XRT", "SPY"] },
27
+ "ism manufacturing pmi": { seriesId: "NAPM", displayMode: "level", relatedTickers: ["SPY", "XLI"] },
28
+ "consumer confidence": { seriesId: "UMCSENT", displayMode: "level", relatedTickers: ["SPY"] },
29
+ "prelim uom consumer sentiment": { seriesId: "UMCSENT", displayMode: "level", relatedTickers: ["SPY"] },
30
+ "revised uom consumer sentiment": { seriesId: "UMCSENT", displayMode: "level", relatedTickers: ["SPY"] },
31
+ "cb consumer confidence": { seriesId: "CSCICP03USM665S", displayMode: "level", relatedTickers: ["SPY"] },
32
+ "federal funds rate": { seriesId: "FEDFUNDS", displayMode: "level", relatedTickers: ["^TNX", "TLT", "DX-Y.NYB"] },
33
+ "crude oil inventories": { seriesId: "WCOILWTICO", displayMode: "level", relatedTickers: ["CL=F", "USO"] },
34
+ "natural gas storage": { seriesId: "NATURALGAS", displayMode: "level", relatedTickers: ["NG=F", "UNG"] },
35
+ "housing starts": { seriesId: "HOUST", displayMode: "level", relatedTickers: ["XHB", "ITB"] },
36
+ "building permits": { seriesId: "PERMIT", displayMode: "level", relatedTickers: ["XHB", "ITB"] },
37
+ "existing home sales": { seriesId: "EXHOSLUSM495S", displayMode: "level", relatedTickers: ["XHB"] },
38
+ "new home sales": { seriesId: "HSN1F", displayMode: "level", relatedTickers: ["XHB", "ITB"] },
39
+ "durable goods orders m/m": { seriesId: "DGORDER", displayMode: "change", relatedTickers: ["XLI", "SPY"] },
40
+ "core durable goods orders m/m": { seriesId: "ADXTNO", displayMode: "change", relatedTickers: ["XLI"] },
41
+ "factory orders m/m": { seriesId: "AMTMNO", displayMode: "change", relatedTickers: ["XLI"] },
42
+ "trade balance": { seriesId: "BOPGSTB", displayMode: "level", relatedTickers: ["DX-Y.NYB"] },
43
+ "industrial production m/m": { seriesId: "INDPRO", displayMode: "change", relatedTickers: ["XLI", "SPY"] },
44
+ "capacity utilization rate": { seriesId: "TCU", displayMode: "level", relatedTickers: ["XLI"] },
45
+ "personal income m/m": { seriesId: "PI", displayMode: "change", relatedTickers: ["SPY"] },
46
+ "personal spending m/m": { seriesId: "PCE", displayMode: "change", relatedTickers: ["XRT", "SPY"] },
47
+ "current account": { seriesId: "NETFI", displayMode: "level", relatedTickers: ["DX-Y.NYB"] },
48
+ "import prices m/m": { seriesId: "IR", displayMode: "change", relatedTickers: ["DX-Y.NYB"] },
49
+ "export prices m/m": { seriesId: "IQ", displayMode: "change", relatedTickers: ["DX-Y.NYB"] },
50
+ "jolts job openings": { seriesId: "JTSJOL", displayMode: "level", relatedTickers: ["SPY"] },
51
+ "nonfarm productivity q/q": { seriesId: "OPHNFB", displayMode: "change", relatedTickers: ["SPY"] },
52
+ "unit labor costs q/q": { seriesId: "ULCNFB", displayMode: "change", relatedTickers: ["SPY", "^TNX"] },
53
+ };
54
+
55
+ function normalizeEventTitle(title: string): string {
56
+ return title.toLowerCase().trim();
57
+ }
58
+
59
+ export function resolveFredMapping(eventTitle: string, country: string): FredMapping | null {
60
+ // Only US events have FRED data
61
+ if (country !== "US" && country !== "USD") return null;
62
+
63
+ const normalized = normalizeEventTitle(eventTitle);
64
+
65
+ // Direct match
66
+ if (SERIES_MAP[normalized]) return SERIES_MAP[normalized];
67
+
68
+ // Fuzzy: try removing common prefixes
69
+ for (const prefix of ["final ", "prelim ", "revised ", "flash ", "advance "]) {
70
+ const stripped = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : null;
71
+ if (stripped && SERIES_MAP[stripped]) return SERIES_MAP[stripped];
72
+ }
73
+
74
+ // Fuzzy: try partial match on key words
75
+ for (const [key, mapping] of Object.entries(SERIES_MAP)) {
76
+ if (normalized.includes(key) || key.includes(normalized)) return mapping;
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ export function getRelatedTickers(eventTitle: string, country: string): string[] {
83
+ return resolveFredMapping(eventTitle, country)?.relatedTickers ?? [];
84
+ }
85
+
86
+ export function getFredSeriesId(eventTitle: string, country: string): string | null {
87
+ return resolveFredMapping(eventTitle, country)?.seriesId ?? null;
88
+ }