gloomberb 0.9.7 → 0.9.9

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