gloomberb 0.4.1 → 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 (207) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/app.tsx +168 -65
  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 +292 -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/header.tsx +2 -1
  48. package/src/components/layout/shell.test.tsx +186 -27
  49. package/src/components/layout/shell.tsx +5 -4
  50. package/src/components/layout/status-bar.test.tsx +16 -7
  51. package/src/components/layout/status-bar.tsx +4 -1
  52. package/src/components/markdown-editor.tsx +1 -0
  53. package/src/components/pane-settings-dialog.tsx +13 -152
  54. package/src/components/pane-template-wizard.tsx +1 -0
  55. package/src/components/price-selector-dialog.tsx +12 -7
  56. package/src/components/ticker-badge-text.test.tsx +50 -0
  57. package/src/components/ticker-badge-text.tsx +17 -2
  58. package/src/components/ticker-badge.tsx +2 -2
  59. package/src/components/ticker-list-table.tsx +54 -1
  60. package/src/components/toggle-list.tsx +15 -2
  61. package/src/components/ui/data-table.tsx +60 -3
  62. package/src/components/ui/external-link.tsx +48 -11
  63. package/src/components/ui/fields.tsx +2 -1
  64. package/src/components/ui/index.ts +8 -1
  65. package/src/components/ui/multi-select-chips.tsx +69 -0
  66. package/src/components/ui/multi-select-dialog.tsx +267 -0
  67. package/src/components/ui/multi-select.ts +78 -0
  68. package/src/components/ui/ui.test.tsx +343 -1
  69. package/src/data/plugin-state-store.ts +30 -19
  70. package/src/data/resource-store.ts +133 -103
  71. package/src/data/session-store.ts +19 -12
  72. package/src/data/sqlite-cache.test.ts +37 -2
  73. package/src/data/sqlite-database.ts +8 -2
  74. package/src/data/sqlite-retry.test.ts +63 -0
  75. package/src/data/sqlite-retry.ts +66 -0
  76. package/src/data/ticker-store.ts +29 -18
  77. package/src/index.tsx +6 -1
  78. package/src/market-data/coordinator.test.ts +114 -5
  79. package/src/market-data/coordinator.ts +70 -1
  80. package/src/market-data/hooks.test.tsx +73 -2
  81. package/src/market-data/hooks.tsx +16 -4
  82. package/src/notifications/app-notifier.test.ts +177 -0
  83. package/src/notifications/app-notifier.ts +241 -0
  84. package/src/pane-settings.test.ts +2 -0
  85. package/src/plugins/builtin/ai/index.test.tsx +1 -1
  86. package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
  87. package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
  88. package/src/plugins/builtin/ai/ticker-context.ts +3 -2
  89. package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
  90. package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
  91. package/src/plugins/builtin/alerts/index.test.tsx +357 -0
  92. package/src/plugins/builtin/alerts/index.tsx +519 -0
  93. package/src/plugins/builtin/alerts/types.ts +14 -0
  94. package/src/plugins/builtin/analytics/index.test.tsx +300 -0
  95. package/src/plugins/builtin/analytics/index.tsx +775 -0
  96. package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
  97. package/src/plugins/builtin/analytics/metrics.ts +155 -0
  98. package/src/plugins/builtin/ask-ai.test.tsx +2 -0
  99. package/src/plugins/builtin/chat-controller.test.ts +62 -18
  100. package/src/plugins/builtin/chat-controller.ts +23 -12
  101. package/src/plugins/builtin/chat.test.tsx +715 -4
  102. package/src/plugins/builtin/chat.tsx +415 -119
  103. package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
  104. package/src/plugins/builtin/comparison-chart.tsx +1 -2
  105. package/src/plugins/builtin/correlation/compute.test.ts +147 -0
  106. package/src/plugins/builtin/correlation/compute.ts +115 -0
  107. package/src/plugins/builtin/correlation/index.tsx +385 -0
  108. package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
  109. package/src/plugins/builtin/correlation/settings.test.ts +34 -0
  110. package/src/plugins/builtin/correlation/settings.ts +95 -0
  111. package/src/plugins/builtin/debug.tsx +36 -50
  112. package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
  113. package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
  114. package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
  115. package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
  116. package/src/plugins/builtin/earnings/index.tsx +337 -0
  117. package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
  118. package/src/plugins/builtin/econ/calendar-source.ts +121 -0
  119. package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
  120. package/src/plugins/builtin/econ/fred-client.ts +79 -0
  121. package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
  122. package/src/plugins/builtin/econ/index.tsx +999 -0
  123. package/src/plugins/builtin/econ/types.ts +13 -0
  124. package/src/plugins/builtin/fred-settings.ts +10 -0
  125. package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
  126. package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
  127. package/src/plugins/builtin/help.test.tsx +1 -5
  128. package/src/plugins/builtin/help.tsx +1 -16
  129. package/src/plugins/builtin/insider/index.tsx +417 -0
  130. package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
  131. package/src/plugins/builtin/insider/insider-data.ts +49 -0
  132. package/src/plugins/builtin/layout-manager.tsx +24 -16
  133. package/src/plugins/builtin/market-movers/index.tsx +573 -0
  134. package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
  135. package/src/plugins/builtin/market-movers/screener.ts +105 -0
  136. package/src/plugins/builtin/options.tsx +4 -3
  137. package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
  138. package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
  139. package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
  140. package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
  141. package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
  142. package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
  143. package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
  144. package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
  145. package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
  146. package/src/plugins/builtin/sec.tsx +114 -7
  147. package/src/plugins/builtin/sectors/index.tsx +359 -0
  148. package/src/plugins/builtin/sectors/sector-data.ts +18 -0
  149. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
  150. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
  151. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
  152. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
  153. package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
  154. package/src/plugins/builtin/world-indices/index.tsx +410 -0
  155. package/src/plugins/builtin/world-indices/indices.ts +52 -0
  156. package/src/plugins/builtin/yield-curve/index.tsx +242 -0
  157. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
  158. package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
  159. package/src/plugins/catalog.ts +22 -0
  160. package/src/plugins/gridlock-notification.ts +15 -0
  161. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  162. package/src/plugins/ibkr/index.test.tsx +1 -1
  163. package/src/plugins/ibkr/index.tsx +1 -1
  164. package/src/plugins/ibkr/trade-tab.tsx +23 -5
  165. package/src/plugins/ibkr/trade-utils.test.ts +1 -1
  166. package/src/plugins/ibkr/trade-utils.ts +7 -6
  167. package/src/plugins/ibkr/trading-pane.tsx +8 -7
  168. package/src/plugins/plugin-runtime.test.tsx +2 -0
  169. package/src/plugins/plugin-runtime.tsx +10 -0
  170. package/src/plugins/prediction-markets/controller-data.ts +8 -0
  171. package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
  172. package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
  173. package/src/plugins/prediction-markets/services/fetch.ts +13 -7
  174. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  175. package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
  176. package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
  177. package/src/plugins/registry.ts +22 -13
  178. package/src/sources/provider-router.test.ts +3 -2
  179. package/src/sources/sec-edgar.ts +13 -1
  180. package/src/sources/yahoo-finance.ts +75 -1
  181. package/src/state/app-context-selector.test.tsx +17 -1
  182. package/src/state/app-context.test.tsx +24 -0
  183. package/src/state/app-context.tsx +29 -6
  184. package/src/theme/colors.ts +5 -0
  185. package/src/theme/themes.ts +25 -0
  186. package/src/types/config.test.ts +107 -0
  187. package/src/types/config.ts +30 -1
  188. package/src/types/data-provider.ts +13 -0
  189. package/src/types/plugin.ts +35 -1
  190. package/src/utils/format.test.ts +11 -1
  191. package/src/utils/format.ts +86 -8
  192. package/src/utils/inline-content-tokenizer.test.ts +22 -0
  193. package/src/utils/inline-content-tokenizer.ts +76 -0
  194. package/src/utils/link-tokenizer.test.ts +27 -0
  195. package/src/utils/link-tokenizer.ts +93 -0
  196. package/src/utils/market-format.test.ts +109 -0
  197. package/src/utils/market-format.ts +299 -0
  198. package/src/utils/market-status.ts +3 -2
  199. package/src/utils/selection-clipboard.test.ts +43 -0
  200. package/src/utils/selection-clipboard.ts +88 -0
  201. package/src/utils/terminal-input-reset.test.ts +50 -0
  202. package/src/utils/terminal-input-reset.ts +24 -0
  203. package/src/utils/throttled-fetch.test.ts +145 -0
  204. package/src/utils/throttled-fetch.ts +212 -0
  205. package/src/utils/ticker-tokenizer.test.ts +31 -0
  206. package/src/utils/ticker-tokenizer.ts +7 -42
  207. package/src/version.ts +1 -1
@@ -0,0 +1,212 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import type { PersistedResourceValue } from "../../../types/persistence";
3
+ import type { PluginPersistence } from "../../../types/plugin";
4
+ import {
5
+ attachYieldCurvePersistence,
6
+ isInverted,
7
+ loadYieldCurve,
8
+ parseYieldPoints,
9
+ resetYieldCurvePersistence,
10
+ TREASURY_MATURITIES,
11
+ YIELD_CURVE_CACHE_POLICY,
12
+ type YieldPoint,
13
+ } from "./treasury-data";
14
+
15
+ class MemoryPluginPersistence implements PluginPersistence {
16
+ private readonly resources = new Map<string, PersistedResourceValue<unknown>>();
17
+ private readonly state = new Map<string, { schemaVersion: number; value: unknown }>();
18
+
19
+ getState<T = unknown>(key: string, options?: { schemaVersion?: number }): T | null {
20
+ const record = this.state.get(key);
21
+ if (!record) return null;
22
+ if (options?.schemaVersion != null && record.schemaVersion !== options.schemaVersion) return null;
23
+ return record.value as T;
24
+ }
25
+
26
+ setState(key: string, value: unknown, options?: { schemaVersion?: number }): void {
27
+ this.state.set(key, { schemaVersion: options?.schemaVersion ?? 1, value });
28
+ }
29
+
30
+ deleteState(key: string): void {
31
+ this.state.delete(key);
32
+ }
33
+
34
+ getResource<T = unknown>(
35
+ kind: string,
36
+ key: string,
37
+ options?: { sourceKey?: string; schemaVersion?: number; allowExpired?: boolean },
38
+ ): PersistedResourceValue<T> | null {
39
+ const record = this.resources.get(this.resourceKey(kind, key, options?.sourceKey));
40
+ if (!record) return null;
41
+ if (options?.schemaVersion != null && record.schemaVersion !== options.schemaVersion) return null;
42
+ const next = this.withFreshness(record);
43
+ if (!options?.allowExpired && next.expired) return null;
44
+ return next as PersistedResourceValue<T>;
45
+ }
46
+
47
+ setResource<T = unknown>(
48
+ kind: string,
49
+ key: string,
50
+ value: T,
51
+ options: {
52
+ cachePolicy: { staleMs: number; expireMs: number };
53
+ sourceKey?: string;
54
+ schemaVersion?: number;
55
+ provenance?: unknown;
56
+ },
57
+ ): PersistedResourceValue<T> {
58
+ const now = Date.now();
59
+ const record: PersistedResourceValue<T> = {
60
+ value,
61
+ fetchedAt: now,
62
+ staleAt: now + options.cachePolicy.staleMs,
63
+ expiresAt: now + options.cachePolicy.expireMs,
64
+ sourceKey: options.sourceKey ?? "",
65
+ schemaVersion: options.schemaVersion ?? 1,
66
+ provenance: options.provenance,
67
+ stale: false,
68
+ expired: false,
69
+ };
70
+ this.resources.set(this.resourceKey(kind, key, options.sourceKey), record);
71
+ return record;
72
+ }
73
+
74
+ deleteResource(kind: string, key: string, options?: { sourceKey?: string }): void {
75
+ this.resources.delete(this.resourceKey(kind, key, options?.sourceKey));
76
+ }
77
+
78
+ seedResource<T>(
79
+ kind: string,
80
+ key: string,
81
+ value: T,
82
+ options: { sourceKey?: string; stale?: boolean; expired?: boolean } = {},
83
+ ): void {
84
+ const now = Date.now();
85
+ const record: PersistedResourceValue<T> = {
86
+ value,
87
+ fetchedAt: now - 60_000,
88
+ staleAt: options.stale ? now - 1 : now + YIELD_CURVE_CACHE_POLICY.staleMs,
89
+ expiresAt: options.expired ? now - 1 : now + YIELD_CURVE_CACHE_POLICY.expireMs,
90
+ sourceKey: options.sourceKey ?? "",
91
+ schemaVersion: 1,
92
+ stale: !!options.stale,
93
+ expired: !!options.expired,
94
+ };
95
+ this.resources.set(this.resourceKey(kind, key, options.sourceKey), record);
96
+ }
97
+
98
+ private resourceKey(kind: string, key: string, sourceKey = ""): string {
99
+ return `${kind}:${key}:${sourceKey}`;
100
+ }
101
+
102
+ private withFreshness<T>(record: PersistedResourceValue<T>): PersistedResourceValue<T> {
103
+ const now = Date.now();
104
+ return {
105
+ ...record,
106
+ stale: now >= record.staleAt,
107
+ expired: now >= record.expiresAt,
108
+ };
109
+ }
110
+ }
111
+
112
+ const SAMPLE_POINTS: YieldPoint[] = [
113
+ { maturity: "2Y", maturityYears: 2, yield: 4.5 },
114
+ { maturity: "10Y", maturityYears: 10, yield: 4.3 },
115
+ ];
116
+
117
+ afterEach(() => {
118
+ resetYieldCurvePersistence();
119
+ });
120
+
121
+ describe("parseYieldPoints", () => {
122
+ test("filters out null yields", () => {
123
+ const points: YieldPoint[] = [
124
+ { maturity: "2Y", maturityYears: 2, yield: 4.5 },
125
+ { maturity: "5Y", maturityYears: 5, yield: null },
126
+ { maturity: "10Y", maturityYears: 10, yield: 4.3 },
127
+ ];
128
+ const result = parseYieldPoints(points);
129
+ expect(result).toHaveLength(2);
130
+ expect(result[0]!.maturity).toBe("2Y");
131
+ expect(result[1]!.maturity).toBe("10Y");
132
+ });
133
+
134
+ test("returns empty for all nulls", () => {
135
+ expect(parseYieldPoints([{ maturity: "2Y", maturityYears: 2, yield: null }])).toEqual([]);
136
+ });
137
+ });
138
+
139
+ describe("isInverted", () => {
140
+ test("detects inverted curve (2Y > 10Y)", () => {
141
+ const points: YieldPoint[] = [
142
+ { maturity: "2Y", maturityYears: 2, yield: 5.0 },
143
+ { maturity: "10Y", maturityYears: 10, yield: 4.3 },
144
+ ];
145
+ expect(isInverted(points)).toBe(true);
146
+ });
147
+
148
+ test("normal curve is not inverted", () => {
149
+ const points: YieldPoint[] = [
150
+ { maturity: "2Y", maturityYears: 2, yield: 4.0 },
151
+ { maturity: "10Y", maturityYears: 10, yield: 4.5 },
152
+ ];
153
+ expect(isInverted(points)).toBe(false);
154
+ });
155
+
156
+ test("returns false when data is missing", () => {
157
+ expect(isInverted([])).toBe(false);
158
+ expect(isInverted([{ maturity: "2Y", maturityYears: 2, yield: 4.0 }])).toBe(false);
159
+ });
160
+ });
161
+
162
+ describe("TREASURY_MATURITIES", () => {
163
+ test("has 10 maturities in ascending order", () => {
164
+ expect(TREASURY_MATURITIES).toHaveLength(10);
165
+ for (let i = 1; i < TREASURY_MATURITIES.length; i++) {
166
+ expect(TREASURY_MATURITIES[i]!.years).toBeGreaterThan(TREASURY_MATURITIES[i - 1]!.years);
167
+ }
168
+ });
169
+ });
170
+
171
+ describe("loadYieldCurve cache", () => {
172
+ test("uses a fresh plugin resource cache without fetching", async () => {
173
+ const persistence = new MemoryPluginPersistence();
174
+ attachYieldCurvePersistence(persistence);
175
+ persistence.setResource("treasury-yield-curve", "latest", SAMPLE_POINTS, {
176
+ sourceKey: "fred",
177
+ cachePolicy: YIELD_CURVE_CACHE_POLICY,
178
+ });
179
+ let fetchCalls = 0;
180
+
181
+ const points = await loadYieldCurve("key", {
182
+ fetcher: async () => {
183
+ fetchCalls += 1;
184
+ throw new Error("unexpected fetch");
185
+ },
186
+ });
187
+
188
+ expect(points).toEqual(SAMPLE_POINTS);
189
+ expect(fetchCalls).toBe(0);
190
+ });
191
+
192
+ test("falls back to stale plugin resource cache when refresh fails", async () => {
193
+ const persistence = new MemoryPluginPersistence();
194
+ attachYieldCurvePersistence(persistence);
195
+ persistence.seedResource("treasury-yield-curve", "latest", SAMPLE_POINTS, {
196
+ sourceKey: "fred",
197
+ stale: true,
198
+ });
199
+ let fetchCalls = 0;
200
+
201
+ const points = await loadYieldCurve("key", {
202
+ force: true,
203
+ fetcher: async () => {
204
+ fetchCalls += 1;
205
+ throw new Error("offline");
206
+ },
207
+ });
208
+
209
+ expect(points).toEqual(SAMPLE_POINTS);
210
+ expect(fetchCalls).toBe(1);
211
+ });
212
+ });
@@ -0,0 +1,107 @@
1
+ import { fetchFredObservations } from "../econ/fred-client";
2
+ import type { PersistedResourceValue } from "../../../types/persistence";
3
+ import type { PluginPersistence } from "../../../types/plugin";
4
+
5
+ export interface YieldPoint {
6
+ maturity: string; // "1M", "3M", "6M", "1Y", "2Y", "5Y", "7Y", "10Y", "20Y", "30Y"
7
+ maturityYears: number; // 0.083, 0.25, 0.5, 1, 2, 5, 7, 10, 20, 30
8
+ yield: number | null; // percent, e.g., 4.29
9
+ }
10
+
11
+ export const TREASURY_MATURITIES: Array<{ maturity: string; years: number; seriesId: string }> = [
12
+ { maturity: "1M", years: 1/12, seriesId: "DGS1MO" },
13
+ { maturity: "3M", years: 0.25, seriesId: "DGS3MO" },
14
+ { maturity: "6M", years: 0.5, seriesId: "DGS6MO" },
15
+ { maturity: "1Y", years: 1, seriesId: "DGS1" },
16
+ { maturity: "2Y", years: 2, seriesId: "DGS2" },
17
+ { maturity: "5Y", years: 5, seriesId: "DGS5" },
18
+ { maturity: "7Y", years: 7, seriesId: "DGS7" },
19
+ { maturity: "10Y", years: 10, seriesId: "DGS10" },
20
+ { maturity: "20Y", years: 20, seriesId: "DGS20" },
21
+ { maturity: "30Y", years: 30, seriesId: "DGS30" },
22
+ ];
23
+
24
+ const CACHE_KIND = "treasury-yield-curve";
25
+ const CACHE_KEY = "latest";
26
+ const CACHE_SOURCE = "fred";
27
+ const CACHE_SCHEMA_VERSION = 1;
28
+
29
+ export const YIELD_CURVE_CACHE_POLICY = {
30
+ staleMs: 15 * 60 * 1000,
31
+ expireMs: 7 * 24 * 60 * 60 * 1000,
32
+ } as const;
33
+
34
+ let yieldCurvePersistence: PluginPersistence | null = null;
35
+
36
+ export function attachYieldCurvePersistence(persistence: PluginPersistence): void {
37
+ yieldCurvePersistence = persistence;
38
+ }
39
+
40
+ export function resetYieldCurvePersistence(): void {
41
+ yieldCurvePersistence = null;
42
+ }
43
+
44
+ function readYieldCurveCache(options?: {
45
+ allowExpired?: boolean;
46
+ }): PersistedResourceValue<YieldPoint[]> | null {
47
+ return yieldCurvePersistence?.getResource<YieldPoint[]>(CACHE_KIND, CACHE_KEY, {
48
+ sourceKey: CACHE_SOURCE,
49
+ schemaVersion: CACHE_SCHEMA_VERSION,
50
+ allowExpired: options?.allowExpired,
51
+ }) ?? null;
52
+ }
53
+
54
+ function writeYieldCurveCache(points: YieldPoint[]): void {
55
+ yieldCurvePersistence?.setResource(CACHE_KIND, CACHE_KEY, points, {
56
+ sourceKey: CACHE_SOURCE,
57
+ schemaVersion: CACHE_SCHEMA_VERSION,
58
+ cachePolicy: YIELD_CURVE_CACHE_POLICY,
59
+ });
60
+ }
61
+
62
+ export async function fetchYieldCurve(apiKey: string): Promise<YieldPoint[]> {
63
+ const results = await Promise.allSettled(
64
+ TREASURY_MATURITIES.map(async ({ maturity, years, seriesId }) => {
65
+ const obs = await fetchFredObservations(apiKey, seriesId, { limit: 1, sortOrder: "desc" });
66
+ const value = obs[0]?.value ?? null;
67
+ return { maturity, maturityYears: years, yield: value };
68
+ }),
69
+ );
70
+ return results.map((r, i) => {
71
+ if (r.status === "fulfilled") return r.value;
72
+ return { maturity: TREASURY_MATURITIES[i]!.maturity, maturityYears: TREASURY_MATURITIES[i]!.years, yield: null };
73
+ });
74
+ }
75
+
76
+ export async function loadYieldCurve(
77
+ apiKey: string,
78
+ options?: {
79
+ force?: boolean;
80
+ fetcher?: (apiKey: string) => Promise<YieldPoint[]>;
81
+ },
82
+ ): Promise<YieldPoint[]> {
83
+ const freshCache = readYieldCurveCache();
84
+ if (!options?.force && freshCache && !freshCache.stale) {
85
+ return freshCache.value;
86
+ }
87
+
88
+ try {
89
+ const points = await (options?.fetcher ?? fetchYieldCurve)(apiKey);
90
+ writeYieldCurveCache(points);
91
+ return points;
92
+ } catch (error) {
93
+ const staleCache = freshCache ?? readYieldCurveCache({ allowExpired: true });
94
+ if (staleCache) return staleCache.value;
95
+ throw error;
96
+ }
97
+ }
98
+
99
+ export function parseYieldPoints(points: YieldPoint[]): YieldPoint[] {
100
+ return points.filter((p) => p.yield !== null);
101
+ }
102
+
103
+ export function isInverted(points: YieldPoint[]): boolean {
104
+ const y2 = points.find((p) => p.maturity === "2Y")?.yield;
105
+ const y10 = points.find((p) => p.maturity === "10Y")?.yield;
106
+ return y2 != null && y10 != null && y2 > y10;
107
+ }
@@ -12,10 +12,21 @@ import { aiPlugin } from "./builtin/ai";
12
12
  import { gloomberbCloudPlugin } from "./builtin/chat";
13
13
  import { helpPlugin } from "./builtin/help";
14
14
  import { comparisonChartPlugin } from "./builtin/comparison-chart";
15
+ import { econCalendarPlugin } from "./builtin/econ";
16
+ import { worldIndicesPlugin } from "./builtin/world-indices";
17
+ import { marketMoversPlugin } from "./builtin/market-movers";
15
18
  import { debugPlugin } from "./builtin/debug";
16
19
  import { layoutManagerPlugin } from "./builtin/layout-manager";
17
20
  import { yahooPlugin } from "./builtin/yahoo";
18
21
  import { predictionMarketsPlugin } from "./prediction-markets";
22
+ import { correlationPlugin } from "./builtin/correlation";
23
+ import { analyticsPlugin } from "./builtin/analytics";
24
+ import { insiderPlugin } from "./builtin/insider";
25
+ import { alertsPlugin } from "./builtin/alerts";
26
+ import { fxMatrixPlugin } from "./builtin/fx-matrix";
27
+ import { yieldCurvePlugin } from "./builtin/yield-curve";
28
+ import { sectorsPlugin } from "./builtin/sectors";
29
+ import { earningsPlugin } from "./builtin/earnings";
19
30
 
20
31
  export interface PluginCatalogEntry {
21
32
  plugin: GloomPlugin;
@@ -40,6 +51,17 @@ export const builtinPlugins: GloomPlugin[] = [
40
51
  helpPlugin,
41
52
  comparisonChartPlugin,
42
53
  predictionMarketsPlugin,
54
+ correlationPlugin,
55
+ analyticsPlugin,
56
+ insiderPlugin,
57
+ econCalendarPlugin,
58
+ worldIndicesPlugin,
59
+ marketMoversPlugin,
60
+ alertsPlugin,
61
+ fxMatrixPlugin,
62
+ yieldCurvePlugin,
63
+ sectorsPlugin,
64
+ earningsPlugin,
43
65
  debugPlugin,
44
66
  ];
45
67
 
@@ -0,0 +1,15 @@
1
+ import type { AppNotificationRequest } from "../types/plugin";
2
+
3
+ export function notifyGridlockComplete(
4
+ notify: (notification: AppNotificationRequest) => void,
5
+ onRevert: () => void,
6
+ ): void {
7
+ notify({
8
+ body: "Retiled all panes",
9
+ type: "success",
10
+ action: {
11
+ label: "Revert",
12
+ onClick: onRevert,
13
+ },
14
+ });
15
+ }
@@ -1,11 +1,8 @@
1
1
  // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
2
 
3
3
  exports[`renders the compact trade tab layout 1`] = `
4
- " Trade AMD · Advanced Micro Devices, Inc.
5
- $199.80 -3.97 · Bid 199.70 · Ask 199.90 · Spd 0.20
6
-
7
- Broker Account Net Liq
8
- Paper Gateway DU123456 —
4
+ " Trade AMD · Advanced Micro Devices, Inc. Broker Account Net Liq
5
+ $199.8 -3.97 · Bid 199.7 · Ask 199.9 · Spd 0.2 Paper Gateway DU123456 —
9
6
 
10
7
 
11
8
  Next Submit order Ticket Standby Refresh r
@@ -31,5 +28,8 @@ exports[`renders the compact trade tab layout 1`] = `
31
28
  │ Preview p Submit Order Enter │
32
29
  ╰────────────────────────────────────────────────────────────────────────────────────╯
33
30
 
31
+
32
+
33
+
34
34
  "
35
35
  `;
@@ -111,7 +111,7 @@ function setupIbkrPlugin(config: AppConfig) {
111
111
  emit: () => {},
112
112
  showWidget: () => {},
113
113
  hideWidget: () => {},
114
- showToast: (message: string) => { toasts.push(message); },
114
+ notify: ({ body }: { body: string }) => { toasts.push(body); },
115
115
  } satisfies GloomPluginContext;
116
116
 
117
117
  void ibkrPlugin.setup?.(ctx);
@@ -20,7 +20,7 @@ let lastSelectedTickerSymbol: string | null = null;
20
20
 
21
21
  function ensureIbkrTradingProfile(ctx: GloomPluginContext): boolean {
22
22
  if (hasIbkrTradingProfiles(ctx.getConfig())) return true;
23
- ctx.showToast("Connect a Gateway / TWS IBKR profile first.", { type: "info" });
23
+ ctx.notify({ body: "Connect a Gateway / TWS IBKR profile first.", type: "info" });
24
24
  return false;
25
25
  }
26
26
 
@@ -8,7 +8,8 @@ import { useAppState, usePaneCollection, usePaneInstanceId, usePaneTicker } from
8
8
  import { colors, hoverBg } from "../../theme/colors";
9
9
  import type { DetailTabProps } from "../../types/plugin";
10
10
  import type { BrokerOrderRequest, BrokerOrderType } from "../../types/trading";
11
- import { formatCurrency, formatNumber } from "../../utils/format";
11
+ import { formatCurrency } from "../../utils/format";
12
+ import { formatMarketPrice, formatMarketQuantity } from "../../utils/market-format";
12
13
  import { getBrokerInstance } from "../../utils/broker-instances";
13
14
  import { isGatewayConfigured } from "./config";
14
15
  import { ChoiceDialog, InputDialog } from "./dialogs";
@@ -401,6 +402,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
401
402
  label={label}
402
403
  currentValue={currentValue}
403
404
  quote={financials?.quote}
405
+ assetCategory={ticker?.metadata.assetCategory}
404
406
  />
405
407
  ),
406
408
  });
@@ -818,7 +820,7 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
818
820
  )}
819
821
  </box>
820
822
  <box height={1}>
821
- <text fg={colors.textMuted}>{formatQuoteSummary(financials?.quote)}</text>
823
+ <text fg={colors.textMuted}>{formatQuoteSummary(financials?.quote, { assetCategory: ticker.metadata.assetCategory })}</text>
822
824
  </box>
823
825
  </box>
824
826
 
@@ -946,7 +948,11 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
946
948
  {renderFieldPill({
947
949
  id: "quantity",
948
950
  label: "Qty [q]",
949
- value: formatNumber(ticketState.draft.quantity, 0),
951
+ value: formatMarketQuantity(ticketState.draft.quantity, {
952
+ assetCategory: ticker.metadata.assetCategory,
953
+ contractSecType: activeContract.secType,
954
+ maxWidth: fieldTextWidth,
955
+ }),
950
956
  widthOverride: orderFieldWidth,
951
957
  onPress: () => {
952
958
  editNumericField("Quantity", ticketState.draft.quantity, (value) => {
@@ -957,7 +963,13 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
957
963
  {showLimit && renderFieldPill({
958
964
  id: "limitPrice",
959
965
  label: "Limit [l]",
960
- value: ticketState.draft.limitPrice != null ? ticketState.draft.limitPrice.toFixed(2) : "—",
966
+ value: ticketState.draft.limitPrice != null
967
+ ? formatMarketPrice(ticketState.draft.limitPrice, {
968
+ assetCategory: ticker.metadata.assetCategory,
969
+ contractSecType: activeContract.secType,
970
+ maxWidth: fieldTextWidth,
971
+ })
972
+ : "—",
961
973
  widthOverride: orderFieldWidth,
962
974
  onPress: () => {
963
975
  editPriceField("Limit Price", ticketState.draft.limitPrice, (value) => {
@@ -968,7 +980,13 @@ export function TradeTab({ focused, width, onCapture }: DetailTabProps) {
968
980
  {showStop && renderFieldPill({
969
981
  id: "stopPrice",
970
982
  label: "Stop [x]",
971
- value: ticketState.draft.stopPrice != null ? ticketState.draft.stopPrice.toFixed(2) : "—",
983
+ value: ticketState.draft.stopPrice != null
984
+ ? formatMarketPrice(ticketState.draft.stopPrice, {
985
+ assetCategory: ticker.metadata.assetCategory,
986
+ contractSecType: activeContract.secType,
987
+ maxWidth: fieldTextWidth,
988
+ })
989
+ : "—",
972
990
  widthOverride: orderFieldWidth,
973
991
  onPress: () => {
974
992
  editPriceField("Stop Price", ticketState.draft.stopPrice, (value) => {
@@ -125,7 +125,7 @@ describe("trade-utils", () => {
125
125
  bid: 190.2,
126
126
  ask: 190.3,
127
127
  lastUpdated: Date.now(),
128
- })).toContain("Spd 0.10");
128
+ })).toContain("Spd 0.1");
129
129
  expect(formatPreviewSummary(null)).toContain("Preview required before submit");
130
130
  expect(formatPreviewSummary({
131
131
  initMarginBefore: 12000,
@@ -5,6 +5,7 @@ import type { TickerRecord } from "../../types/ticker";
5
5
  import type { BrokerAccount, BrokerOrderPreview, BrokerOrderType } from "../../types/trading";
6
6
  import { colors } from "../../theme/colors";
7
7
  import { formatCompact, formatCurrency } from "../../utils/format";
8
+ import { formatMarketPrice, formatMarketPriceWithCurrency, formatSignedMarketPrice, type AssetDisplayContext } from "../../utils/market-format";
8
9
  import { getConfiguredIbkrGatewayInstances } from "./instance-selection";
9
10
 
10
11
  export type TradeTone = "neutral" | "accent" | "positive" | "negative";
@@ -71,13 +72,13 @@ export function formatContractLabel(contract: BrokerContractRef): string {
71
72
  return `${base}${suffix}`;
72
73
  }
73
74
 
74
- export function formatQuoteSummary(quote?: Quote): string {
75
+ export function formatQuoteSummary(quote?: Quote, formatContext: AssetDisplayContext = {}): string {
75
76
  if (!quote) return "No broker quote loaded";
76
- const change = `${quote.change >= 0 ? "+" : ""}${quote.change.toFixed(2)}`;
77
- const parts = [`${formatCurrency(quote.price, quote.currency)} ${change}`];
78
- if (quote.bid != null) parts.push(`Bid ${quote.bid.toFixed(2)}`);
79
- if (quote.ask != null) parts.push(`Ask ${quote.ask.toFixed(2)}`);
80
- if (quote.bid != null && quote.ask != null) parts.push(`Spd ${(quote.ask - quote.bid).toFixed(2)}`);
77
+ const change = formatSignedMarketPrice(quote.change, formatContext);
78
+ const parts = [`${formatMarketPriceWithCurrency(quote.price, quote.currency, formatContext)} ${change}`];
79
+ if (quote.bid != null) parts.push(`Bid ${formatMarketPrice(quote.bid, formatContext)}`);
80
+ if (quote.ask != null) parts.push(`Ask ${formatMarketPrice(quote.ask, formatContext)}`);
81
+ if (quote.bid != null && quote.ask != null) parts.push(`Spd ${formatMarketPrice(quote.ask - quote.bid, formatContext)}`);
81
82
  return parts.join(" · ");
82
83
  }
83
84
 
@@ -9,6 +9,7 @@ import { colors, priceColor } from "../../theme/colors";
9
9
  import type { PaneProps } from "../../types/plugin";
10
10
  import type { Quote } from "../../types/financials";
11
11
  import { formatCurrency, padTo } from "../../utils/format";
12
+ import { formatMarketPrice, formatMarketQuantity } from "../../utils/market-format";
12
13
  import { getBrokerInstance } from "../../utils/broker-instances";
13
14
  import { getSharedRegistry } from "../registry";
14
15
  import { isGatewayConfigured } from "./config";
@@ -354,12 +355,12 @@ export function TradingPane({ focused, width, height }: PaneProps) {
354
355
  const selected = index === tradeState.selectedOpenOrderIndex;
355
356
  const orderSymbol = order.contract.symbol;
356
357
  const orderQuote = getOrderQuote(orderSymbol);
357
- const bidStr = orderQuote?.bid != null ? orderQuote.bid.toFixed(2) : "---";
358
- const askStr = orderQuote?.ask != null ? orderQuote.ask.toFixed(2) : "---";
358
+ const bidStr = orderQuote?.bid != null ? formatMarketPrice(orderQuote.bid, { contractSecType: order.contract.secType, maxWidth: 6 }) : "---";
359
+ const askStr = orderQuote?.ask != null ? formatMarketPrice(orderQuote.ask, { contractSecType: order.contract.secType, maxWidth: 6 }) : "---";
359
360
  const orderPrice = order.limitPrice != null
360
- ? order.limitPrice.toFixed(2)
361
+ ? formatMarketPrice(order.limitPrice, { contractSecType: order.contract.secType, maxWidth: 9 })
361
362
  : order.stopPrice != null
362
- ? order.stopPrice.toFixed(2)
363
+ ? formatMarketPrice(order.stopPrice, { contractSecType: order.contract.secType, maxWidth: 9 })
363
364
  : "MKT";
364
365
  return (
365
366
  <box
@@ -379,7 +380,7 @@ export function TradingPane({ focused, width, height }: PaneProps) {
379
380
  {padTo(order.action, 5)}
380
381
  {padTo(order.contract.localSymbol || order.contract.symbol, 14)}
381
382
  {padTo(order.status, 10)}
382
- {padTo(String(order.remaining), 5, "right")}
383
+ {padTo(formatMarketQuantity(order.remaining, { contractSecType: order.contract.secType, maxWidth: 5 }), 5, "right")}
383
384
  {" "}
384
385
  {padTo(orderPrice, 9)}
385
386
  {padTo(`B:${bidStr}`, 10)}
@@ -415,9 +416,9 @@ export function TradingPane({ focused, width, height }: PaneProps) {
415
416
  <text fg={priceColor(execution.side.toUpperCase() === "BOT" ? 1 : -1)}>
416
417
  {padTo(execution.side, 5)}
417
418
  {padTo(execution.contract.localSymbol || execution.contract.symbol, 18)}
418
- {padTo(String(execution.shares), 6, "right")}
419
+ {padTo(formatMarketQuantity(execution.shares, { contractSecType: execution.contract.secType, maxWidth: 6 }), 6, "right")}
419
420
  {" "}
420
- {execution.price.toFixed(2)}
421
+ {formatMarketPrice(execution.price, { contractSecType: execution.contract.secType })}
421
422
  </text>
422
423
  </box>
423
424
  ))
@@ -59,6 +59,8 @@ describe("plugin runtime hooks", () => {
59
59
  const listeners = new Map<string, Set<() => void>>();
60
60
 
61
61
  const runtime: PluginRuntimeAccess = {
62
+ pinTicker() {},
63
+ navigateTicker() {},
62
64
  subscribeResumeState(pluginId, key, listener) {
63
65
  const listenerKey = `${pluginId}:${key}`;
64
66
  if (!listeners.has(listenerKey)) listeners.set(listenerKey, new Set());
@@ -10,6 +10,8 @@ import {
10
10
  import { useAppState, usePaneInstanceId, type PaneRuntimeState } from "../state/app-context";
11
11
 
12
12
  export interface PluginRuntimeAccess {
13
+ pinTicker(symbol: string, options?: { floating?: boolean; paneType?: string }): void;
14
+ navigateTicker(symbol: string): void;
13
15
  subscribeResumeState(pluginId: string, key: string, listener: () => void): () => void;
14
16
  getResumeState<T = unknown>(pluginId: string, key: string, schemaVersion?: number): T | null;
15
17
  setResumeState(pluginId: string, key: string, value: unknown, schemaVersion?: number): void;
@@ -51,6 +53,14 @@ function usePluginRenderContext(): PluginRenderContextValue {
51
53
  return context;
52
54
  }
53
55
 
56
+ export function usePluginTickerActions() {
57
+ const { runtime } = usePluginRenderContext();
58
+ return {
59
+ pinTicker: runtime.pinTicker,
60
+ navigateTicker: runtime.navigateTicker,
61
+ };
62
+ }
63
+
54
64
  export function getPluginPaneStateValue<T>(
55
65
  paneState: PaneRuntimeState | undefined,
56
66
  pluginId: string,
@@ -76,6 +76,14 @@ function formatPredictionLoadError(
76
76
  return `${venueLabel} is unavailable right now.`;
77
77
  }
78
78
 
79
+ if (
80
+ /socket connection|socket hang up|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|fetch failed|network connection|connection closed/i.test(
81
+ message,
82
+ )
83
+ ) {
84
+ return `${venueLabel} is unavailable right now.`;
85
+ }
86
+
79
87
  return `${fallback} ${message}`;
80
88
  }
81
89
 
@@ -1,6 +1,5 @@
1
- import { TextAttributes } from "@opentui/core";
2
1
  import { colors } from "../../../theme/colors";
3
- import { openUrl } from "../../../components/ui/external-link";
2
+ import { ExternalLinkText } from "../../../components/ui/external-link";
4
3
 
5
4
  export function truncatePredictionText(
6
5
  value: string,
@@ -19,10 +18,12 @@ export function SummaryLink({
19
18
  maxLength: number;
20
19
  }) {
21
20
  return (
22
- <box height={1} onMouseDown={() => openUrl(url)}>
23
- <text fg={colors.textBright} attributes={TextAttributes.UNDERLINE}>
24
- {truncatePredictionText(url, maxLength)}
25
- </text>
21
+ <box height={1}>
22
+ <ExternalLinkText
23
+ url={url}
24
+ label={truncatePredictionText(url, maxLength)}
25
+ color={colors.textBright}
26
+ />
26
27
  </box>
27
28
  );
28
29
  }