gloomberb 0.12.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -2
  3. package/src/api-client/auth.ts +2 -1
  4. package/src/api-client/index.test.ts +34 -0
  5. package/src/api-client/index.ts +10 -2
  6. package/src/api-client/request.ts +4 -1
  7. package/src/api-client/research-activity.ts +61 -0
  8. package/src/api-client/types.ts +2 -1
  9. package/src/cli/commands/plugins.ts +4 -3
  10. package/src/cli/desktop-pane-shot.ts +79 -10
  11. package/src/cli/pane-functions/capabilities.ts +76 -23
  12. package/src/cli/pane-functions/cloud-session.test.ts +54 -0
  13. package/src/cli/pane-functions/cloud-session.ts +38 -0
  14. package/src/cli/pane-functions/dom.test.ts +41 -0
  15. package/src/cli/pane-functions/dom.ts +199 -0
  16. package/src/cli/pane-functions/index.test.ts +13 -2
  17. package/src/cli/pane-functions/index.ts +9 -5
  18. package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
  19. package/src/cli/pane-functions/intraday-shot.ts +334 -0
  20. package/src/cli/pane-functions/report.test.ts +29 -1
  21. package/src/cli/pane-functions/report.ts +68 -7
  22. package/src/cli/pane-functions/screenshot.ts +195 -42
  23. package/src/cli/restore-plugins.ts +10 -3
  24. package/src/components/chart/composite/composite-chart.test.tsx +68 -1
  25. package/src/components/chart/composite/composite-chart.tsx +87 -21
  26. package/src/components/chart/composite/rasterizer.ts +33 -0
  27. package/src/components/chart/composite/renderers.test.ts +51 -4
  28. package/src/components/chart/composite/scene.test.ts +128 -2
  29. package/src/components/chart/composite/scene.ts +92 -16
  30. package/src/components/chart/composite/text-renderer.ts +13 -0
  31. package/src/components/chart/composite/time-scale.ts +30 -10
  32. package/src/components/chart/composite/types.ts +26 -0
  33. package/src/components/command-bar/assist/runtime.ts +38 -30
  34. package/src/components/command-bar/surface/assist.test.tsx +63 -9
  35. package/src/components/command-bar/surface/test-harness.tsx +23 -4
  36. package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
  37. package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
  38. package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
  39. package/src/components/onboarding/onboarding-wizard.tsx +49 -14
  40. package/src/data/config/store/index.test.ts +23 -0
  41. package/src/data/config/store/node.ts +7 -1
  42. package/src/data/config/store/normalize.ts +4 -0
  43. package/src/news/types.ts +1 -0
  44. package/src/plugins/builtin/cds/client.test.ts +20 -0
  45. package/src/plugins/builtin/cds/client.ts +13 -3
  46. package/src/plugins/builtin/cds/headless.test.ts +30 -0
  47. package/src/plugins/builtin/cds/headless.ts +113 -0
  48. package/src/plugins/builtin/cds/index.tsx +4 -0
  49. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
  50. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  51. package/src/plugins/builtin/chat/controller/channels.ts +2 -1
  52. package/src/plugins/builtin/chat/controller/index.ts +13 -5
  53. package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
  54. package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
  55. package/src/plugins/builtin/chat/controller/state.ts +1 -0
  56. package/src/plugins/builtin/chat/controller.test.ts +260 -4
  57. package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
  58. package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
  59. package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
  60. package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
  61. package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
  62. package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
  63. package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
  64. package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
  65. package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
  66. package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
  67. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
  68. package/src/plugins/builtin/cloud/plugin.tsx +2 -0
  69. package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
  70. package/src/plugins/builtin/congress-trades/client.ts +11 -0
  71. package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
  72. package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
  73. package/src/plugins/builtin/congress-trades/headless.ts +145 -0
  74. package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
  75. package/src/plugins/builtin/credit-conditions/client.ts +1 -1
  76. package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
  77. package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
  78. package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
  79. package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
  80. package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
  81. package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
  82. package/src/plugins/builtin/dividend-yield/model.ts +4 -22
  83. package/src/plugins/builtin/dividend-yield/view.ts +17 -0
  84. package/src/plugins/builtin/earnings/headless.test.ts +82 -0
  85. package/src/plugins/builtin/earnings/headless.ts +162 -0
  86. package/src/plugins/builtin/earnings/index.tsx +2 -0
  87. package/src/plugins/builtin/earnings-calls/data.ts +29 -8
  88. package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
  89. package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
  90. package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
  91. package/src/plugins/builtin/earnings-calls/model.ts +238 -0
  92. package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
  93. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
  94. package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
  95. package/src/plugins/builtin/fear-greed/headless.ts +84 -0
  96. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  97. package/src/plugins/builtin/holders/client.ts +29 -0
  98. package/src/plugins/builtin/holders/headless.test.ts +58 -0
  99. package/src/plugins/builtin/holders/headless.ts +162 -0
  100. package/src/plugins/builtin/holders/index.tsx +14 -8
  101. package/src/plugins/builtin/holders/pane.tsx +7 -1
  102. package/src/plugins/builtin/insider/client.ts +50 -0
  103. package/src/plugins/builtin/insider/headless.test.ts +122 -0
  104. package/src/plugins/builtin/insider/headless.ts +117 -0
  105. package/src/plugins/builtin/insider/index.tsx +21 -50
  106. package/src/plugins/builtin/insider/insider-data.ts +1 -1
  107. package/src/plugins/builtin/insider/model.ts +72 -0
  108. package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
  109. package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
  110. package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
  111. package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
  112. package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
  113. package/src/plugins/builtin/market-movers/client.ts +76 -0
  114. package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
  115. package/src/plugins/builtin/market-movers/headless.ts +73 -0
  116. package/src/plugins/builtin/market-movers/index.tsx +11 -51
  117. package/src/plugins/builtin/news/headless.test.ts +138 -0
  118. package/src/plugins/builtin/news/headless.ts +190 -0
  119. package/src/plugins/builtin/news/index.tsx +13 -9
  120. package/src/plugins/builtin/news/wire/index.tsx +2 -1
  121. package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
  122. package/src/plugins/builtin/research/analyst-headless.ts +172 -0
  123. package/src/plugins/builtin/research/analyst-model.ts +219 -0
  124. package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
  125. package/src/plugins/builtin/research/client.ts +12 -0
  126. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
  127. package/src/plugins/builtin/research/event-model.ts +245 -0
  128. package/src/plugins/builtin/research/events-headless.test.ts +32 -0
  129. package/src/plugins/builtin/research/events-headless.ts +87 -0
  130. package/src/plugins/builtin/research/headless.test.ts +93 -0
  131. package/src/plugins/builtin/research/headless.ts +161 -0
  132. package/src/plugins/builtin/research/index.tsx +32 -18
  133. package/src/plugins/builtin/research-search/pane.tsx +4 -0
  134. package/src/plugins/builtin/scanner/headless.test.ts +59 -0
  135. package/src/plugins/builtin/scanner/headless.ts +125 -0
  136. package/src/plugins/builtin/scanner/index.ts +4 -0
  137. package/src/plugins/builtin/sec/client.ts +16 -0
  138. package/src/plugins/builtin/sec/headless.test.ts +72 -0
  139. package/src/plugins/builtin/sec/headless.ts +69 -0
  140. package/src/plugins/builtin/sec/index.tsx +20 -75
  141. package/src/plugins/builtin/sec/model.ts +89 -0
  142. package/src/plugins/builtin/sectors/client.ts +54 -0
  143. package/src/plugins/builtin/sectors/headless.test.ts +28 -0
  144. package/src/plugins/builtin/sectors/headless.ts +80 -0
  145. package/src/plugins/builtin/sectors/index.tsx +6 -51
  146. package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
  147. package/src/plugins/builtin/short-interest/client.ts +5 -2
  148. package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
  149. package/src/plugins/builtin/short-interest/headless.ts +111 -0
  150. package/src/plugins/builtin/short-interest/index.tsx +15 -9
  151. package/src/plugins/builtin/thirteenf/data.ts +13 -11
  152. package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
  153. package/src/plugins/builtin/thirteenf/headless.ts +286 -0
  154. package/src/plugins/builtin/thirteenf/index.tsx +4 -0
  155. package/src/plugins/builtin/thirteenf/model.ts +13 -0
  156. package/src/plugins/builtin/thirteenf/table.ts +3 -15
  157. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
  158. package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
  159. package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
  160. package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
  161. package/src/plugins/builtin/volatility/client.ts +16 -3
  162. package/src/plugins/builtin/volatility/headless.test.ts +35 -0
  163. package/src/plugins/builtin/volatility/headless.ts +75 -0
  164. package/src/plugins/builtin/volatility/index.tsx +4 -0
  165. package/src/plugins/builtin/world-indices/client.ts +43 -0
  166. package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
  167. package/src/plugins/builtin/world-indices/headless.ts +109 -0
  168. package/src/plugins/builtin/world-indices/index.tsx +4 -0
  169. package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
  170. package/src/plugins/builtin/yield-curve/headless.ts +67 -0
  171. package/src/plugins/builtin/yield-curve/index.tsx +6 -8
  172. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
  173. package/src/plugins/bundle.test.ts +58 -1
  174. package/src/plugins/bundle.ts +4 -2
  175. package/src/plugins/catalog-backend.test.ts +26 -0
  176. package/src/plugins/catalog-backend.ts +9 -2
  177. package/src/plugins/catalog-browser.test.ts +0 -13
  178. package/src/plugins/catalog-browser.ts +4 -0
  179. package/src/plugins/catalog-manifest.test.ts +15 -0
  180. package/src/plugins/catalog-manifest.ts +12 -5
  181. package/src/plugins/catalog-ui.ts +0 -6
  182. package/src/plugins/host-contract.ts +9 -0
  183. package/src/plugins/host-modules.ts +12 -0
  184. package/src/plugins/loader.ts +63 -15
  185. package/src/plugins/prediction-markets/headless.test.ts +119 -0
  186. package/src/plugins/prediction-markets/headless.ts +255 -0
  187. package/src/plugins/prediction-markets/index.tsx +2 -0
  188. package/src/plugins/seed.ts +3 -0
  189. package/src/public/broker.ts +6 -0
  190. package/src/remote/schema.test.ts +65 -0
  191. package/src/remote/schema.ts +322 -39
  192. package/src/remote/types.ts +49 -2
  193. package/src/renderers/browser/app-services.ts +3 -0
  194. package/src/renderers/browser/config-host.ts +24 -8
  195. package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
  196. package/src/renderers/browser/deeplink-bridge.ts +15 -3
  197. package/src/renderers/browser/main.tsx +3 -1
  198. package/src/renderers/browser/research-entry.test.ts +8 -0
  199. package/src/renderers/browser/research-entry.ts +7 -0
  200. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
  201. package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
  202. package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
  203. package/src/renderers/electrobun/view/build-assets.ts +0 -2
  204. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
  205. package/src/sources/gloomberb-cloud/news.ts +1 -0
  206. package/src/sync/core-contributors.test.ts +111 -0
  207. package/src/sync/core-contributors.ts +155 -1
  208. package/src/time-series/resolve.test.ts +118 -1
  209. package/src/time-series/resolve.ts +14 -16
  210. package/src/types/config.ts +2 -0
  211. package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
  212. package/src/plugins/broker-sync/native-loader.ts +0 -10
  213. package/src/plugins/broker-sync/normalize.ts +0 -308
  214. package/src/plugins/broker-sync/public.ts +0 -122
  215. package/src/plugins/broker-sync/robinhood-native.ts +0 -367
  216. package/src/plugins/broker-sync/robinhood.ts +0 -114
  217. package/src/plugins/broker-sync/simplefin-native.ts +0 -201
  218. package/src/plugins/broker-sync/simplefin.ts +0 -70
  219. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
@@ -384,10 +384,16 @@ export function seedChartResolutionResult(
384
384
  if (resolved?.points.length) series.push(resolved);
385
385
  });
386
386
  if (series.length === 0) return null;
387
+ // Studies ride along with the seed. Without them the chart briefly drops to
388
+ // base series alone whenever it falls back here, and every panel a study
389
+ // owns loses its rows until the first real resolve lands.
390
+ const seeded = spec.studies.length > 0
391
+ ? [...series, ...resolveStudies(series, spec.studies).series]
392
+ : series;
387
393
  return {
388
- series,
389
- legendSeries: series,
390
- bufferedSeries: series,
394
+ series: seeded,
395
+ legendSeries: seeded,
396
+ bufferedSeries: seeded,
391
397
  loading: false,
392
398
  errors: [],
393
399
  warnings: [],
@@ -805,13 +811,6 @@ function prepareBaseSeriesForStudies(
805
811
  );
806
812
  }
807
813
 
808
- function rawCalculationSeries(series: ResolvedSeries, bounds: DateBounds): ResolvedSeries {
809
- if (bounds.start !== null && bounds.end !== null) {
810
- return clipSeriesToWindow(series, new Date(bounds.start), new Date(bounds.end));
811
- }
812
- return { ...series, points: filterPoints(series.points, bounds) };
813
- }
814
-
815
814
  function scalarBaseline(series: ResolvedSeries, bounds: DateBounds): number | null {
816
815
  const points = filterPoints(series.points, bounds);
817
816
  for (const point of points) {
@@ -1148,15 +1147,14 @@ export async function resolveChartSpecData(
1148
1147
  ? requestVisibleBounds
1149
1148
  : followLatestMarketObservation(initialVisibleBounds, rawSeries);
1150
1149
  const resolution = initialResolution;
1151
- const studyBounds = bounds.end !== null
1152
- && initialCalculationBounds.end !== null
1153
- && bounds.end > initialCalculationBounds.end
1154
- ? { ...initialCalculationBounds, end: bounds.end }
1155
- : initialCalculationBounds;
1156
1150
  const baseSeries = rawSeries
1157
1151
  .filter((entry) => visibleSeriesIds.has(entry.id))
1158
1152
  .map((entry) => prepareBaseSeriesForStudies(entry, bounds, false, requestVisibleBounds));
1159
- const calculationSeries = rawSeries.map((entry) => rawCalculationSeries(entry, studyBounds));
1153
+ // Studies run over the same loaded history their base series carries. Clipping
1154
+ // them to the requested window instead left a study with no observations
1155
+ // wherever the accumulated buffer had already been panned past, so a study's
1156
+ // panel emptied out mid-pan and only refilled once the next fetch landed.
1157
+ const calculationSeries = rawSeries;
1160
1158
  let resolved = baseSeries;
1161
1159
 
1162
1160
  // Study outputs are appended by the pure engine before the final viewport clip.
@@ -120,6 +120,8 @@ export type OnboardingStage =
120
120
  | "welcome"
121
121
  | "portfolio"
122
122
  | "add-ticker"
123
+ | "research"
124
+ | "verify"
123
125
  | "account"
124
126
  | "upgrade"
125
127
  | "ready";
@@ -1,188 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { buildBrokerProfileConfig, validateBrokerProfileValues } from "../../brokers/profile-form";
3
- import { normalizeRobinhoodSnapshot, normalizeSimpleFinSnapshot } from "./normalize";
4
- import { loadPublicPortfolio } from "./public";
5
- import { requireRobinhoodPositionTools } from "./robinhood-native";
6
- import { decodeSimpleFinSetupToken } from "./simplefin-native";
7
- import { simpleFinBroker } from "./simplefin";
8
-
9
- describe("broker position normalization", () => {
10
- test("normalizes nested Robinhood accounts and positions without duplicate records", () => {
11
- const snapshot = normalizeRobinhoodSnapshot(
12
- { accounts: [{ account_number: "RH-1", account_type: "ROTH_IRA", currency: "USD" }] },
13
- [{ result: { positions: [{
14
- accountNumber: "RH-1",
15
- instrument: { symbol: "hood", name: "Robinhood Markets" },
16
- quantity: "2.5",
17
- total_cost: "100",
18
- market_value: "125",
19
- }] } }],
20
- );
21
-
22
- expect(snapshot.accounts).toEqual([expect.objectContaining({
23
- accountId: "RH-1",
24
- name: "Roth Ira",
25
- })]);
26
- expect(snapshot.positions).toEqual([expect.objectContaining({
27
- accountId: "RH-1",
28
- ticker: "HOOD",
29
- shares: 2.5,
30
- avgCost: 40,
31
- markPrice: 50,
32
- marketValue: 125,
33
- })]);
34
- });
35
-
36
- test("normalizes only SimpleFIN accounts that contain investment holdings", () => {
37
- const snapshot = normalizeSimpleFinSnapshot({ accounts: [
38
- { id: "cash", conn_id: "bank", name: "Checking", currency: "USD", holdings: [] },
39
- {
40
- id: "invest",
41
- conn_id: "broker",
42
- name: "Roth IRA",
43
- currency: "USD",
44
- balance: "750",
45
- holdings: [{ symbol: "VTI", shares: "3", cost_basis: "600", market_value: "750" }],
46
- },
47
- ] });
48
-
49
- expect(snapshot.accounts).toHaveLength(1);
50
- expect(snapshot.accounts[0]?.accountId).toBe("broker:invest");
51
- expect(snapshot.positions[0]).toEqual(expect.objectContaining({
52
- ticker: "VTI",
53
- shares: 3,
54
- avgCost: 200,
55
- markPrice: 250,
56
- }));
57
- });
58
-
59
- test("stores shorts as absolute shares and weighted-averages merged short lots", () => {
60
- const snapshot = normalizeRobinhoodSnapshot(
61
- { accounts: [{ account_number: "RH-1", account_type: "INDIVIDUAL" }] },
62
- [{ result: { positions: [
63
- {
64
- accountNumber: "RH-1",
65
- instrument: { symbol: "TSLA" },
66
- quantity: "-10",
67
- average_cost: "100",
68
- market_value: "-800",
69
- },
70
- {
71
- accountNumber: "RH-1",
72
- instrument: { symbol: "TSLA" },
73
- quantity: "-5",
74
- average_cost: "110",
75
- market_value: "-400",
76
- },
77
- ] } }],
78
- );
79
-
80
- expect(snapshot.positions).toEqual([expect.objectContaining({
81
- ticker: "TSLA",
82
- shares: 15,
83
- avgCost: 1550 / 15,
84
- side: "short",
85
- })]);
86
- });
87
- });
88
-
89
- describe("broker authentication boundaries", () => {
90
- test("Public sync uses one token request and read-only portfolio requests", async () => {
91
- const calls: Array<{ url: string; method: string }> = [];
92
- const fetchImpl = (async (input: string | URL | Request, init?: RequestInit) => {
93
- const url = String(input);
94
- calls.push({ url, method: init?.method ?? "GET" });
95
- if (url.endsWith("/personal/access-tokens")) {
96
- return Response.json({ accessToken: "temporary-token" });
97
- }
98
- if (url.endsWith("/trading/account")) {
99
- return Response.json({ accounts: [{ accountId: "PUB-1", accountType: "BROKERAGE" }] });
100
- }
101
- return Response.json({
102
- accountId: "PUB-1",
103
- accountType: "BROKERAGE",
104
- totalAccountValue: "110",
105
- buyingPower: { buyingPower: "10" },
106
- positions: [{
107
- instrument: { symbol: "AAPL", name: "Apple", type: "EQUITY" },
108
- quantity: "1",
109
- currentValue: "100",
110
- lastPrice: { lastPrice: "100" },
111
- costBasis: { unitCost: "80", gainValue: "20" },
112
- }],
113
- });
114
- }) as typeof fetch;
115
-
116
- const snapshot = await loadPublicPortfolio("public-secret", fetchImpl);
117
-
118
- expect(calls.map(({ method }) => method)).toEqual(["POST", "GET", "GET"]);
119
- expect(calls.every(({ url }) => url.startsWith("https://api.public.com/"))).toBe(true);
120
- expect(snapshot.positions[0]).toEqual(expect.objectContaining({
121
- ticker: "AAPL",
122
- assetCategory: "STK",
123
- avgCost: 80,
124
- }));
125
- });
126
-
127
- test("Robinhood accepts only the two position-sync tools", () => {
128
- const selected = requireRobinhoodPositionTools([
129
- { name: "get_accounts", annotations: { readOnlyHint: true } },
130
- { name: "get_equity_positions", annotations: { readOnlyHint: true } },
131
- { name: "place_equity_order", annotations: { readOnlyHint: false } },
132
- ]);
133
- expect([...selected.keys()]).toEqual(["get_accounts", "get_equity_positions"]);
134
- expect(() => requireRobinhoodPositionTools([
135
- { name: "get_accounts", annotations: { readOnlyHint: true } },
136
- { name: "get_equity_positions", annotations: { readOnlyHint: false } },
137
- ])).toThrow("read-only get_equity_positions");
138
- });
139
-
140
- test("aggregates identical same-account lots instead of keeping only the last row", () => {
141
- const snapshot = normalizeRobinhoodSnapshot(
142
- { accounts: [{ account_number: "RH-1", currency: "USD" }] },
143
- { positions: [
144
- {
145
- accountNumber: "RH-1",
146
- instrument: { symbol: "HOOD" },
147
- quantity: "2",
148
- total_cost: "80",
149
- market_value: "100",
150
- },
151
- {
152
- accountNumber: "RH-1",
153
- instrument: { symbol: "HOOD" },
154
- quantity: "3",
155
- total_cost: "150",
156
- market_value: "150",
157
- },
158
- ] },
159
- );
160
-
161
- expect(snapshot.positions).toEqual([expect.objectContaining({
162
- ticker: "HOOD",
163
- shares: 5,
164
- avgCost: 46,
165
- marketValue: 250,
166
- })]);
167
- });
168
-
169
- test("SimpleFIN accepts URL-safe setup tokens and rejects non-HTTPS claims", () => {
170
- const claimUrl = "https://bridge.simplefin.org/simplefin/claim/demo";
171
- const token = Buffer.from(claimUrl).toString("base64url");
172
- expect(decodeSimpleFinSetupToken(token)).toBe(claimUrl);
173
- expect(() => decodeSimpleFinSetupToken(Buffer.from("http://localhost/claim").toString("base64")))
174
- .toThrow("not valid");
175
- });
176
-
177
- test("SimpleFIN keeps a claimed access URL when a user edits the profile", () => {
178
- const previous = {
179
- id: "simplefin-main",
180
- brokerType: "simplefin",
181
- label: "SimpleFIN",
182
- enabled: true,
183
- config: { setupToken: "", accessUrl: "https://user:password@example.com/simplefin" },
184
- };
185
- expect(validateBrokerProfileValues(simpleFinBroker, { setupToken: "" }, previous)).toBeNull();
186
- expect(buildBrokerProfileConfig(simpleFinBroker, { setupToken: "" }, previous)).toEqual(previous.config);
187
- });
188
- });
@@ -1,10 +0,0 @@
1
- export type RobinhoodNativeModule = typeof import("./robinhood-native");
2
- export type SimpleFinNativeModule = typeof import("./simplefin-native");
3
-
4
- export function loadRobinhoodNativeModule(): Promise<RobinhoodNativeModule> {
5
- return import("./robinhood-native");
6
- }
7
-
8
- export function loadSimpleFinNativeModule(): Promise<SimpleFinNativeModule> {
9
- return import("./simplefin-native");
10
- }
@@ -1,308 +0,0 @@
1
- import type { BrokerPosition } from "../../types/broker";
2
- import type { BrokerAccount } from "../../types/trading";
3
-
4
- export interface BrokerPortfolioSnapshot {
5
- accounts: BrokerAccount[];
6
- positions: BrokerPosition[];
7
- }
8
-
9
- type UnknownRecord = Record<string, unknown>;
10
-
11
- function record(value: unknown): UnknownRecord | null {
12
- return value !== null && typeof value === "object" && !Array.isArray(value)
13
- ? value as UnknownRecord
14
- : null;
15
- }
16
-
17
- function text(...values: unknown[]): string {
18
- for (const value of values) {
19
- if (typeof value === "string" && value.trim()) return value.trim();
20
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
21
- }
22
- return "";
23
- }
24
-
25
- function numberValue(...values: unknown[]): number | undefined {
26
- for (const value of values) {
27
- const parsed = typeof value === "number"
28
- ? value
29
- : typeof value === "string"
30
- ? Number(value.replaceAll(",", ""))
31
- : Number.NaN;
32
- if (Number.isFinite(parsed)) return parsed;
33
- }
34
- return undefined;
35
- }
36
-
37
- function nested(source: UnknownRecord, key: string): UnknownRecord {
38
- return record(source[key]) ?? {};
39
- }
40
-
41
- function allRecords(value: unknown, output: UnknownRecord[] = [], seen = new Set<object>()): UnknownRecord[] {
42
- if (value === null || typeof value !== "object" || seen.has(value)) return output;
43
- seen.add(value);
44
- if (Array.isArray(value)) {
45
- for (const item of value) allRecords(item, output, seen);
46
- return output;
47
- }
48
- const item = value as UnknownRecord;
49
- output.push(item);
50
- for (const child of Object.values(item)) allRecords(child, output, seen);
51
- return output;
52
- }
53
-
54
- function accountId(source: UnknownRecord): string {
55
- return text(
56
- source.accountId,
57
- source.account_id,
58
- source.accountNumber,
59
- source.account_number,
60
- source.brokerageAccountId,
61
- source.brokerage_account_id,
62
- );
63
- }
64
-
65
- function titleCase(value: string): string {
66
- return value.toLowerCase().replace(/(^|[_\s-])([a-z])/g, (_match, prefix: string, letter: string) => (
67
- `${prefix === "_" ? " " : prefix}${letter.toUpperCase()}`
68
- ));
69
- }
70
-
71
- function uniqueSnapshot(accounts: BrokerAccount[], positions: BrokerPosition[]): BrokerPortfolioSnapshot {
72
- const uniqueAccounts = [...new Map(accounts.map((account) => [account.accountId, account])).values()];
73
- const knownAccounts = new Set(uniqueAccounts.map((account) => account.accountId));
74
- for (const position of positions) {
75
- if (!position.accountId || knownAccounts.has(position.accountId)) continue;
76
- knownAccounts.add(position.accountId);
77
- uniqueAccounts.push({
78
- accountId: position.accountId,
79
- name: position.accountId,
80
- currency: position.currency,
81
- });
82
- }
83
- return { accounts: uniqueAccounts, positions: mergeIdenticalPositions(positions.map(withCanonicalShares)) };
84
- }
85
-
86
- function withCanonicalShares(position: BrokerPosition): BrokerPosition {
87
- const side = position.side ?? (position.shares < 0 ? "short" : "long");
88
- const shares = Math.abs(position.shares);
89
- if (shares === position.shares && side === position.side) return position;
90
- return { ...position, shares, side };
91
- }
92
-
93
- function mergeIdenticalPositions(positions: BrokerPosition[]): BrokerPosition[] {
94
- const merged = new Map<string, BrokerPosition>();
95
- for (const position of positions) {
96
- const key = [
97
- position.accountId ?? "",
98
- position.ticker,
99
- position.assetCategory ?? "",
100
- position.exchange ?? "",
101
- position.brokerContract?.conId ?? "",
102
- ].join(":");
103
- const existing = merged.get(key);
104
- if (!existing) {
105
- merged.set(key, position);
106
- continue;
107
- }
108
- const shares = existing.shares + position.shares;
109
- const existingCost = (existing.avgCost ?? 0) * existing.shares;
110
- const nextCost = (position.avgCost ?? 0) * position.shares;
111
- merged.set(key, {
112
- ...existing,
113
- shares,
114
- avgCost: shares !== 0 ? (existingCost + nextCost) / shares : existing.avgCost,
115
- marketValue: sumOptional(existing.marketValue, position.marketValue),
116
- unrealizedPnl: sumOptional(existing.unrealizedPnl, position.unrealizedPnl),
117
- });
118
- }
119
- return [...merged.values()];
120
- }
121
-
122
- function sumOptional(left?: number, right?: number): number | undefined {
123
- if (left == null && right == null) return undefined;
124
- return (left ?? 0) + (right ?? 0);
125
- }
126
-
127
- export function normalizeRobinhoodSnapshot(accountsPayload: unknown, positionsPayload: unknown): BrokerPortfolioSnapshot {
128
- const accounts = allRecords(accountsPayload).flatMap((item): BrokerAccount[] => {
129
- const id = accountId(item);
130
- if (!id) return [];
131
- const type = text(item.accountType, item.account_type, item.type);
132
- const currency = text(item.currency, item.baseCurrency, item.base_currency, "USD").toUpperCase();
133
- return [{
134
- accountId: id,
135
- name: text(item.name, item.accountName, item.account_name, type && titleCase(type), id),
136
- currency,
137
- netLiquidation: numberValue(item.totalValue, item.total_value, item.portfolioValue, item.portfolio_value),
138
- buyingPower: numberValue(item.buyingPower, item.buying_power),
139
- }];
140
- });
141
-
142
- const positions = allRecords(positionsPayload).flatMap((item): BrokerPosition[] => {
143
- const instrument = nested(item, "instrument");
144
- const account = nested(item, "account");
145
- const costBasis = record(item.costBasis) ?? record(item.cost_basis) ?? {};
146
- const lastPrice = record(item.lastPrice) ?? record(item.last_price) ?? {};
147
- const symbol = text(item.symbol, item.ticker, instrument.symbol).toUpperCase();
148
- const shares = numberValue(item.quantity, item.shares, item.qty);
149
- if (!symbol || shares == null || shares === 0) return [];
150
- const totalCost = numberValue(
151
- item.totalCost,
152
- item.total_cost,
153
- typeof item.costBasis === "object" ? undefined : item.costBasis,
154
- typeof item.cost_basis === "object" ? undefined : item.cost_basis,
155
- costBasis.totalCost,
156
- costBasis.total_cost,
157
- );
158
- const avgCost = numberValue(
159
- item.averageCost,
160
- item.average_cost,
161
- item.averageBuyPrice,
162
- item.average_buy_price,
163
- item.costBasisPerShare,
164
- item.cost_basis_per_share,
165
- costBasis.unitCost,
166
- costBasis.unit_cost,
167
- totalCost != null ? totalCost / Math.abs(shares) : undefined,
168
- );
169
- const marketValue = numberValue(item.marketValue, item.market_value, item.currentValue, item.current_value);
170
- const markPrice = numberValue(
171
- item.markPrice,
172
- item.mark_price,
173
- item.price,
174
- item.lastPrice,
175
- item.last_price,
176
- lastPrice.lastPrice,
177
- lastPrice.last_price,
178
- lastPrice.price,
179
- marketValue != null ? marketValue / Math.abs(shares) : undefined,
180
- );
181
- return [{
182
- ticker: symbol,
183
- exchange: text(item.exchange, instrument.exchange, "SMART").toUpperCase(),
184
- shares,
185
- avgCost,
186
- currency: text(item.currency, instrument.currency, "USD").toUpperCase(),
187
- accountId: accountId(item) || accountId(account) || undefined,
188
- name: text(item.name, item.description, instrument.name, symbol),
189
- assetCategory: "STK",
190
- markPrice,
191
- marketValue,
192
- unrealizedPnl: numberValue(item.unrealizedPnl, item.unrealized_pnl, item.unrealizedGain, item.unrealized_gain),
193
- side: shares < 0 ? "short" : "long",
194
- }];
195
- });
196
-
197
- return uniqueSnapshot(accounts, positions);
198
- }
199
-
200
- export function normalizePublicSnapshot(accountsPayload: unknown, portfolioPayloads: unknown[]): BrokerPortfolioSnapshot {
201
- const accountRecords = record(accountsPayload)?.accounts;
202
- const accounts = (Array.isArray(accountRecords) ? accountRecords : []).flatMap((value): BrokerAccount[] => {
203
- const item = record(value);
204
- if (!item) return [];
205
- const id = accountId(item);
206
- if (!id) return [];
207
- const type = text(item.accountType, item.account_type);
208
- return [{ accountId: id, name: type ? `Public ${titleCase(type)}` : `Public ${id}`, currency: "USD" }];
209
- });
210
-
211
- const positions: BrokerPosition[] = [];
212
- for (const value of portfolioPayloads) {
213
- const portfolio = record(value);
214
- if (!portfolio) continue;
215
- const id = accountId(portfolio);
216
- const knownAccount = accounts.find((account) => account.accountId === id);
217
- if (knownAccount) {
218
- knownAccount.netLiquidation = numberValue(portfolio.totalAccountValue, portfolio.total_account_value);
219
- knownAccount.totalCashValue = numberValue(portfolio.cash);
220
- knownAccount.buyingPower = numberValue(nested(portfolio, "buyingPower").buyingPower);
221
- }
222
- const rawPositions = Array.isArray(portfolio.positions) ? portfolio.positions : [];
223
- for (const rawPosition of rawPositions) {
224
- const item = record(rawPosition);
225
- if (!item) continue;
226
- const instrument = nested(item, "instrument");
227
- const lastPrice = nested(item, "lastPrice");
228
- const costBasis = nested(item, "costBasis");
229
- const symbol = text(instrument.symbol, item.symbol).toUpperCase();
230
- const shares = numberValue(item.quantity);
231
- if (!symbol || shares == null || shares === 0) continue;
232
- positions.push({
233
- ticker: symbol,
234
- exchange: "SMART",
235
- shares,
236
- avgCost: numberValue(costBasis.unitCost, costBasis.unit_cost),
237
- currency: "USD",
238
- accountId: id || undefined,
239
- name: text(instrument.name, symbol),
240
- assetCategory: text(instrument.type, "STK").toUpperCase() === "EQUITY" ? "STK" : text(instrument.type, "STK").toUpperCase(),
241
- markPrice: numberValue(lastPrice.lastPrice, lastPrice.last_price),
242
- marketValue: numberValue(item.currentValue, item.current_value),
243
- unrealizedPnl: numberValue(costBasis.gainValue, costBasis.gain_value),
244
- percentOfNav: numberValue(item.percentOfPortfolio, item.percent_of_portfolio),
245
- dateAcquired: text(item.openedAt, item.opened_at) || undefined,
246
- side: shares < 0 ? "short" : "long",
247
- });
248
- }
249
- }
250
- return uniqueSnapshot(accounts, positions);
251
- }
252
-
253
- export function normalizeSimpleFinSnapshot(payload: unknown): BrokerPortfolioSnapshot {
254
- const root = record(payload);
255
- const rawAccounts = Array.isArray(root?.accounts) ? root.accounts : [];
256
- const accounts: BrokerAccount[] = [];
257
- const positions: BrokerPosition[] = [];
258
- for (const rawAccount of rawAccounts) {
259
- const account = record(rawAccount);
260
- if (!account) continue;
261
- const id = text(account.id);
262
- const holdings = Array.isArray(account.holdings) ? account.holdings : [];
263
- if (!id || holdings.length === 0) continue;
264
- const currency = text(account.currency, "USD").toUpperCase();
265
- accounts.push({
266
- accountId: `${text(account.conn_id)}:${id}`,
267
- name: text(account.name, id),
268
- currency,
269
- netLiquidation: numberValue(account.balance),
270
- updatedAt: (numberValue(account["balance-date"]) ?? 0) * 1000 || undefined,
271
- });
272
- const normalizedAccountId = accounts.at(-1)!.accountId;
273
- for (const rawHolding of holdings) {
274
- const holding = record(rawHolding);
275
- if (!holding) continue;
276
- const symbol = text(holding.symbol, holding.ticker, holding.code).toUpperCase();
277
- const shares = numberValue(holding.shares, holding.quantity, holding.units);
278
- const marketValue = numberValue(holding.market_value, holding.marketValue, holding.value);
279
- if (!symbol || shares == null || shares === 0) continue;
280
- const costBasis = numberValue(holding.cost_basis, holding.costBasis);
281
- const avgCost = numberValue(
282
- holding.purchase_price,
283
- holding.average_price,
284
- holding.average_cost,
285
- costBasis != null ? costBasis / Math.abs(shares) : undefined,
286
- );
287
- positions.push({
288
- ticker: symbol,
289
- exchange: text(holding.exchange, "SMART").toUpperCase(),
290
- shares,
291
- avgCost,
292
- currency: text(holding.currency, currency).toUpperCase(),
293
- accountId: normalizedAccountId,
294
- name: text(holding.description, holding.name, symbol),
295
- assetCategory: text(holding.type, holding.asset_type, "STK").toUpperCase(),
296
- markPrice: numberValue(
297
- holding.price,
298
- holding.current_price,
299
- holding.market_price,
300
- marketValue != null ? marketValue / Math.abs(shares) : undefined,
301
- ),
302
- marketValue,
303
- side: shares < 0 ? "short" : "long",
304
- });
305
- }
306
- }
307
- return uniqueSnapshot(accounts, positions);
308
- }