gloomberb 0.9.7 → 0.9.8

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 (126) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/auth.ts +20 -0
  3. package/src/api-client/index.ts +48 -0
  4. package/src/api-client/types.ts +45 -0
  5. package/src/app/runtime/desktop-deeplink.test.ts +87 -0
  6. package/src/app/runtime/desktop-deeplink.ts +496 -0
  7. package/src/app.tsx +34 -0
  8. package/src/components/chart/bar-chart-renderer.test.ts +87 -12
  9. package/src/components/chart/bar-chart-renderer.ts +111 -55
  10. package/src/components/chart/static/bar-chart-surface.tsx +104 -20
  11. package/src/components/chart/stock-chart/footer.test.ts +65 -0
  12. package/src/components/chart/stock-chart/footer.ts +68 -13
  13. package/src/components/chart/stock-chart/resolved.tsx +18 -4
  14. package/src/components/command-bar/pane-templates/items.ts +11 -3
  15. package/src/components/command-bar/routes/root/results.ts +1 -1
  16. package/src/components/command-bar/surface/index.test.tsx +52 -0
  17. package/src/components/command-bar/workflow/ops.ts +81 -20
  18. package/src/components/index.ts +1 -0
  19. package/src/components/toggle-list.tsx +29 -73
  20. package/src/components/ui/button.tsx +4 -1
  21. package/src/components/ui/checkbox.tsx +76 -0
  22. package/src/components/ui/choice-dialog.tsx +15 -3
  23. package/src/components/ui/frame.tsx +1 -1
  24. package/src/components/ui/index.ts +2 -0
  25. package/src/components/ui/message-composer.tsx +4 -0
  26. package/src/components/ui/native-select.tsx +4 -2
  27. package/src/market-data/coordinator/events.test.ts +49 -0
  28. package/src/market-data/coordinator/events.ts +25 -4
  29. package/src/market-data/coordinator/index.test.ts +223 -0
  30. package/src/market-data/coordinator/index.ts +40 -3
  31. package/src/market-data/coordinator/quotes.ts +13 -5
  32. package/src/market-data/hooks.tsx +17 -13
  33. package/src/market-data/market/freshness.ts +58 -1
  34. package/src/market-data/quotes/freshness.test.ts +49 -1
  35. package/src/market-data/quotes/freshness.ts +8 -0
  36. package/src/market-data/quotes/resolution.test.ts +185 -0
  37. package/src/market-data/quotes/resolution.ts +94 -8
  38. package/src/market-data/request-types.test.ts +61 -1
  39. package/src/market-data/request-types.ts +42 -3
  40. package/src/plugins/builtin/account-management/footer.ts +2 -5
  41. package/src/plugins/builtin/account-management/form-components.tsx +260 -24
  42. package/src/plugins/builtin/account-management/keyboard.ts +32 -2
  43. package/src/plugins/builtin/account-management/model.test.ts +99 -4
  44. package/src/plugins/builtin/account-management/model.ts +263 -3
  45. package/src/plugins/builtin/account-management/pane.tsx +784 -155
  46. package/src/plugins/builtin/chat/chat.test.tsx +125 -0
  47. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
  48. package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
  49. package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
  50. package/src/plugins/builtin/chat/content/composer.tsx +84 -1
  51. package/src/plugins/builtin/chat/content/index.tsx +154 -16
  52. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
  53. package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
  54. package/src/plugins/builtin/chat/content/mentions.ts +88 -0
  55. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
  56. package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
  57. package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
  58. package/src/plugins/builtin/chat/content/transcript.tsx +0 -4
  59. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
  60. package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
  61. package/src/plugins/builtin/chat/sidebar.test.tsx +69 -2
  62. package/src/plugins/builtin/chat/sidebar.tsx +26 -2
  63. package/src/plugins/builtin/chat/test-harness.tsx +21 -11
  64. package/src/plugins/builtin/cloud/plugin.tsx +7 -0
  65. package/src/plugins/builtin/correlation/index.tsx +4 -1
  66. package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
  67. package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
  68. package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
  69. package/src/plugins/builtin/portfolio-list/index.test.ts +51 -1
  70. package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
  71. package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
  72. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
  73. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
  74. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
  75. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +43 -7
  76. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
  77. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
  78. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
  79. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
  80. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
  81. package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
  82. package/src/plugins/ibkr/gateway/market-data.ts +12 -5
  83. package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
  84. package/src/plugins/registry/context.ts +15 -0
  85. package/src/plugins/registry/index.ts +31 -0
  86. package/src/renderers/electrobun/bun/index.ts +42 -0
  87. package/src/renderers/electrobun/shared/protocol.ts +5 -0
  88. package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
  89. package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
  90. package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
  91. package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
  92. package/src/renderers/electrobun/view/host/box.tsx +0 -2
  93. package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
  94. package/src/renderers/electrobun/view/host/text.tsx +1 -2
  95. package/src/renderers/electrobun/view/main.tsx +3 -0
  96. package/src/renderers/electrobun/view/ui-host.tsx +2 -0
  97. package/src/sources/provider-router/batches.ts +11 -3
  98. package/src/sources/provider-router/cached-financials.test.ts +123 -0
  99. package/src/sources/provider-router/financial-routes.ts +55 -5
  100. package/src/sources/provider-router/financials.test.ts +49 -0
  101. package/src/sources/provider-router/financials.ts +40 -3
  102. package/src/sources/provider-router/index.test.ts +147 -0
  103. package/src/sources/provider-router/primary.ts +17 -3
  104. package/src/sources/provider-router/streaming.ts +6 -1
  105. package/src/sources/sec-edgar.test.ts +42 -0
  106. package/src/sources/sec-edgar.ts +1 -2
  107. package/src/sources/yahoo-finance/mappers.test.ts +29 -0
  108. package/src/sources/yahoo-finance/mappers.ts +2 -1
  109. package/src/sources/yahoo-finance/requests.ts +2 -1
  110. package/src/sources/yahoo-finance/snapshots.ts +13 -2
  111. package/src/sources/yahoo-finance.ts +6 -2
  112. package/src/state/hooks/quote-streaming.test.ts +14 -1
  113. package/src/state/hooks/quote-streaming.ts +5 -4
  114. package/src/sync/controller.ts +296 -0
  115. package/src/sync/core-contributors.test.ts +251 -0
  116. package/src/sync/core-contributors.ts +400 -0
  117. package/src/sync/profile-analytics.ts +39 -0
  118. package/src/sync/react.ts +53 -0
  119. package/src/sync/types.ts +77 -0
  120. package/src/types/desktop-deeplink.ts +7 -0
  121. package/src/types/plugin.ts +3 -0
  122. package/src/ui/host.tsx +14 -1
  123. package/src/ui/index.tsx +1 -0
  124. package/src/utils/exchanges.test.ts +11 -1
  125. package/src/utils/exchanges.ts +5 -1
  126. package/src/version.ts +1 -1
@@ -1,6 +1,7 @@
1
1
  import type { ChoiceDialogChoice } from "../../../components";
2
- import type { AccountProfile } from "../../../api-client";
2
+ import type { AccountProfile, PublicPortfolioAnalytics } from "../../../api-client";
3
3
  import type { Portfolio, TickerRecord } from "../../../types/ticker";
4
+ import { formatNumber } from "../../../utils/format";
4
5
 
5
6
  export type AccountFieldKey =
6
7
  | "username"
@@ -13,7 +14,12 @@ export type AccountFieldKey =
13
14
  | "xAccount"
14
15
  | "acceptUnknownDms"
15
16
  | "sharedPortfolioId"
16
- | "passwordAction";
17
+ | "weeklyRoundupEnabled"
18
+ | "positionAlertsEnabled"
19
+ | "emailAlertsOffAction"
20
+ | "upgradeAction"
21
+ | "passwordAction"
22
+ | "deleteAccountAction";
17
23
 
18
24
  export interface AccountDraft {
19
25
  username: string;
@@ -26,6 +32,24 @@ export interface AccountDraft {
26
32
  xAccount: string;
27
33
  sharedPortfolioId: string;
28
34
  acceptUnknownDms: boolean;
35
+ weeklyRoundupEnabled: boolean;
36
+ positionAlertsEnabled: boolean;
37
+ }
38
+
39
+ export interface ProfileAnalyticsPreviewMetric {
40
+ id: string;
41
+ label: string;
42
+ value: string;
43
+ detail?: string;
44
+ tone?: "positive" | "negative" | "muted";
45
+ }
46
+
47
+ export interface ProfileAnalyticsPreview {
48
+ status: "off" | "missing" | "empty" | "pending" | "ready";
49
+ title: string;
50
+ subtitle: string;
51
+ metrics: ProfileAnalyticsPreviewMetric[];
52
+ publicAnalytics: PublicPortfolioAnalytics | null;
29
53
  }
30
54
 
31
55
  export const BASE_FIELD_ORDER: AccountFieldKey[] = [
@@ -39,7 +63,12 @@ export const BASE_FIELD_ORDER: AccountFieldKey[] = [
39
63
  "xAccount",
40
64
  "acceptUnknownDms",
41
65
  "sharedPortfolioId",
66
+ "weeklyRoundupEnabled",
67
+ "positionAlertsEnabled",
68
+ "emailAlertsOffAction",
69
+ "upgradeAction",
42
70
  "passwordAction",
71
+ "deleteAccountAction",
43
72
  ];
44
73
 
45
74
  export const NO_PORTFOLIO_VALUE = "__none__";
@@ -56,6 +85,8 @@ export function profileToDraft(profile: AccountProfile | null): AccountDraft {
56
85
  xAccount: profile?.xAccount ?? "",
57
86
  sharedPortfolioId: profile?.sharedPortfolioId ?? "",
58
87
  acceptUnknownDms: profile?.acceptUnknownDms === true,
88
+ weeklyRoundupEnabled: profile?.weeklyRoundupEnabled === false ? false : true,
89
+ positionAlertsEnabled: profile?.positionAlertsEnabled === false ? false : true,
59
90
  };
60
91
  }
61
92
 
@@ -92,7 +123,7 @@ export function buildPortfolioChoices(portfolios: Portfolio[], holdingCounts: Re
92
123
  id: portfolio.id,
93
124
  label: portfolio.name,
94
125
  detail: `${holdingCounts[portfolio.id] ?? 0} tickers`,
95
- description: "Shares this portfolio's YTD % and SPY Beta on your public profile.",
126
+ description: "Shares this portfolio's 1Y return and SPY Beta on your public profile.",
96
127
  })),
97
128
  ];
98
129
  }
@@ -105,3 +136,232 @@ export function selectedPortfolioLabel(portfolios: Portfolio[], value: string):
105
136
  if (!value) return "None";
106
137
  return portfolios.find((portfolio) => portfolio.id === value)?.name ?? value;
107
138
  }
139
+
140
+ function signedReturn(value: number): string {
141
+ const percent = value * 100;
142
+ return `${percent >= 0 ? "+" : ""}${formatNumber(percent, 2)}%`;
143
+ }
144
+
145
+ function signedTone(value: number | null | undefined): ProfileAnalyticsPreviewMetric["tone"] {
146
+ if (value == null || !Number.isFinite(value)) return "muted";
147
+ if (value > 0) return "positive";
148
+ if (value < 0) return "negative";
149
+ return "muted";
150
+ }
151
+
152
+ function finiteMetric(value: number | null | undefined): number | null {
153
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
154
+ }
155
+
156
+ function normalizePublicAnalytics(analytics: PublicPortfolioAnalytics | null | undefined): PublicPortfolioAnalytics | null {
157
+ const normalized: PublicPortfolioAnalytics = {
158
+ oneYearReturn: finiteMetric(analytics?.oneYearReturn),
159
+ spyBeta: finiteMetric(analytics?.spyBeta),
160
+ };
161
+ return normalized.oneYearReturn != null || normalized.spyBeta != null ? normalized : null;
162
+ }
163
+
164
+ function buildPublicAnalyticsMetrics(analytics: PublicPortfolioAnalytics): ProfileAnalyticsPreviewMetric[] {
165
+ const metrics: ProfileAnalyticsPreviewMetric[] = [];
166
+ if (analytics.oneYearReturn != null) {
167
+ metrics.push({
168
+ id: "one-year",
169
+ label: "1Y",
170
+ value: signedReturn(analytics.oneYearReturn),
171
+ tone: signedTone(analytics.oneYearReturn),
172
+ });
173
+ }
174
+ if (analytics.spyBeta != null) {
175
+ metrics.push({
176
+ id: "beta",
177
+ label: "SPY Beta",
178
+ value: formatNumber(analytics.spyBeta, 2),
179
+ });
180
+ }
181
+ return metrics;
182
+ }
183
+
184
+ export function getPortfolioPositionTickers(
185
+ tickers: ReadonlyMap<string, TickerRecord>,
186
+ portfolioId: string,
187
+ ): TickerRecord[] {
188
+ return [...tickers.values()].filter((ticker) => (
189
+ ticker.metadata.positions.some((position) => position.portfolio === portfolioId)
190
+ ));
191
+ }
192
+
193
+ export function computeCumulativeReturn(
194
+ returns: Array<{ dateKey: string; value: number }>,
195
+ options?: { sinceDateKey?: string },
196
+ ): number | null {
197
+ const filtered = options?.sinceDateKey
198
+ ? returns.filter((point) => point.dateKey >= options.sinceDateKey!)
199
+ : returns;
200
+ if (filtered.length === 0) return null;
201
+ const value = filtered.reduce((acc, point) => (
202
+ Number.isFinite(point.value) ? acc * (1 + point.value) : acc
203
+ ), 1) - 1;
204
+ return Number.isFinite(value) ? value : null;
205
+ }
206
+
207
+ export function buildProfileAnalyticsPreview({
208
+ beta,
209
+ portfolio,
210
+ portfolioTickers,
211
+ selectedPortfolioId,
212
+ oneYearReturn,
213
+ }: {
214
+ beta: number | null;
215
+ portfolio: Portfolio | null;
216
+ portfolioTickers: TickerRecord[];
217
+ selectedPortfolioId: string;
218
+ oneYearReturn: number | null;
219
+ }): ProfileAnalyticsPreview {
220
+ if (!selectedPortfolioId) {
221
+ return {
222
+ status: "off",
223
+ title: "No public portfolio analytics",
224
+ subtitle: "Choose a portfolio to preview the public profile metrics.",
225
+ metrics: [],
226
+ publicAnalytics: null,
227
+ };
228
+ }
229
+
230
+ if (!portfolio) {
231
+ return {
232
+ status: "missing",
233
+ title: "Portfolio not found",
234
+ subtitle: selectedPortfolioId,
235
+ metrics: [],
236
+ publicAnalytics: null,
237
+ };
238
+ }
239
+
240
+ if (portfolioTickers.length === 0) {
241
+ return {
242
+ status: "empty",
243
+ title: portfolio.name,
244
+ subtitle: "No positions to preview yet.",
245
+ metrics: [],
246
+ publicAnalytics: null,
247
+ };
248
+ }
249
+
250
+ const publicAnalytics: PublicPortfolioAnalytics = {
251
+ oneYearReturn: finiteMetric(oneYearReturn),
252
+ spyBeta: finiteMetric(beta),
253
+ };
254
+ const hasSharedMetric = publicAnalytics.oneYearReturn != null || publicAnalytics.spyBeta != null;
255
+
256
+ return {
257
+ status: "ready",
258
+ title: portfolio.name,
259
+ subtitle: "",
260
+ publicAnalytics: hasSharedMetric ? publicAnalytics : null,
261
+ metrics: [
262
+ {
263
+ id: "one-year",
264
+ label: "1Y",
265
+ value: oneYearReturn == null ? "Pending" : signedReturn(oneYearReturn),
266
+ detail: oneYearReturn == null ? "needs price history" : undefined,
267
+ tone: signedTone(oneYearReturn),
268
+ },
269
+ {
270
+ id: "beta",
271
+ label: "SPY Beta",
272
+ value: beta == null ? "Pending" : formatNumber(beta, 2),
273
+ detail: beta == null ? "needs SPY overlap" : undefined,
274
+ tone: beta == null ? "muted" : undefined,
275
+ },
276
+ ],
277
+ };
278
+ }
279
+
280
+ export function buildPublishedProfileAnalyticsPreview({
281
+ analytics,
282
+ draftProfilePublic,
283
+ portfolio,
284
+ profileLoaded,
285
+ savedProfilePublic,
286
+ savedSharedPortfolioId,
287
+ selectedPortfolioId,
288
+ syncing,
289
+ }: {
290
+ analytics: PublicPortfolioAnalytics | null | undefined;
291
+ draftProfilePublic: boolean;
292
+ portfolio: Portfolio | null;
293
+ profileLoaded: boolean;
294
+ savedProfilePublic: boolean;
295
+ savedSharedPortfolioId: string;
296
+ selectedPortfolioId: string;
297
+ syncing: boolean;
298
+ }): ProfileAnalyticsPreview {
299
+ if (!selectedPortfolioId) {
300
+ return {
301
+ status: "off",
302
+ title: "No public portfolio analytics",
303
+ subtitle: "Choose a portfolio to publish 1Y return and SPY Beta.",
304
+ metrics: [],
305
+ publicAnalytics: null,
306
+ };
307
+ }
308
+
309
+ if (!portfolio) {
310
+ return {
311
+ status: "missing",
312
+ title: "Portfolio not found",
313
+ subtitle: selectedPortfolioId,
314
+ metrics: [],
315
+ publicAnalytics: null,
316
+ };
317
+ }
318
+
319
+ if (!profileLoaded) {
320
+ return {
321
+ status: "pending",
322
+ title: portfolio.name,
323
+ subtitle: "Loading published metrics.",
324
+ metrics: [],
325
+ publicAnalytics: null,
326
+ };
327
+ }
328
+
329
+ if (draftProfilePublic !== savedProfilePublic || selectedPortfolioId !== savedSharedPortfolioId) {
330
+ return {
331
+ status: "pending",
332
+ title: portfolio.name,
333
+ subtitle: "Save profile to update published metrics.",
334
+ metrics: [],
335
+ publicAnalytics: null,
336
+ };
337
+ }
338
+
339
+ if (!savedProfilePublic) {
340
+ return {
341
+ status: "off",
342
+ title: "No public portfolio analytics",
343
+ subtitle: "Public profile is off.",
344
+ metrics: [],
345
+ publicAnalytics: null,
346
+ };
347
+ }
348
+
349
+ const publicAnalytics = normalizePublicAnalytics(analytics);
350
+ if (!publicAnalytics) {
351
+ return {
352
+ status: "pending",
353
+ title: portfolio.name,
354
+ subtitle: syncing ? "Syncing published metrics." : "Waiting for published metrics.",
355
+ metrics: [],
356
+ publicAnalytics: null,
357
+ };
358
+ }
359
+
360
+ return {
361
+ status: "ready",
362
+ title: portfolio.name,
363
+ subtitle: "",
364
+ metrics: buildPublicAnalyticsMetrics(publicAnalytics),
365
+ publicAnalytics,
366
+ };
367
+ }