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.
- package/README.md +2 -0
- package/package.json +1 -2
- package/src/api-client/auth.ts +2 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/index.ts +10 -2
- package/src/api-client/request.ts +4 -1
- package/src/api-client/research-activity.ts +61 -0
- package/src/api-client/types.ts +2 -1
- package/src/cli/commands/plugins.ts +4 -3
- package/src/cli/desktop-pane-shot.ts +79 -10
- package/src/cli/pane-functions/capabilities.ts +76 -23
- package/src/cli/pane-functions/cloud-session.test.ts +54 -0
- package/src/cli/pane-functions/cloud-session.ts +38 -0
- package/src/cli/pane-functions/dom.test.ts +41 -0
- package/src/cli/pane-functions/dom.ts +199 -0
- package/src/cli/pane-functions/index.test.ts +13 -2
- package/src/cli/pane-functions/index.ts +9 -5
- package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
- package/src/cli/pane-functions/intraday-shot.ts +334 -0
- package/src/cli/pane-functions/report.test.ts +29 -1
- package/src/cli/pane-functions/report.ts +68 -7
- package/src/cli/pane-functions/screenshot.ts +195 -42
- package/src/cli/restore-plugins.ts +10 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -1
- package/src/components/chart/composite/composite-chart.tsx +87 -21
- package/src/components/chart/composite/rasterizer.ts +33 -0
- package/src/components/chart/composite/renderers.test.ts +51 -4
- package/src/components/chart/composite/scene.test.ts +128 -2
- package/src/components/chart/composite/scene.ts +92 -16
- package/src/components/chart/composite/text-renderer.ts +13 -0
- package/src/components/chart/composite/time-scale.ts +30 -10
- package/src/components/chart/composite/types.ts +26 -0
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +63 -9
- package/src/components/command-bar/surface/test-harness.tsx +23 -4
- package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
- package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
- package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
- package/src/components/onboarding/onboarding-wizard.tsx +49 -14
- package/src/data/config/store/index.test.ts +23 -0
- package/src/data/config/store/node.ts +7 -1
- package/src/data/config/store/normalize.ts +4 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/cds/client.test.ts +20 -0
- package/src/plugins/builtin/cds/client.ts +13 -3
- package/src/plugins/builtin/cds/headless.test.ts +30 -0
- package/src/plugins/builtin/cds/headless.ts +113 -0
- package/src/plugins/builtin/cds/index.tsx +4 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chat/controller/channels.ts +2 -1
- package/src/plugins/builtin/chat/controller/index.ts +13 -5
- package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
- package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
- package/src/plugins/builtin/chat/controller/state.ts +1 -0
- package/src/plugins/builtin/chat/controller.test.ts +260 -4
- package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
- package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
- package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
- package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
- package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
- package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
- package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
- package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
- package/src/plugins/builtin/congress-trades/client.ts +11 -0
- package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
- package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
- package/src/plugins/builtin/congress-trades/headless.ts +145 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
- package/src/plugins/builtin/credit-conditions/client.ts +1 -1
- package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
- package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
- package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
- package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
- package/src/plugins/builtin/dividend-yield/model.ts +4 -22
- package/src/plugins/builtin/dividend-yield/view.ts +17 -0
- package/src/plugins/builtin/earnings/headless.test.ts +82 -0
- package/src/plugins/builtin/earnings/headless.ts +162 -0
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/data.ts +29 -8
- package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
- package/src/plugins/builtin/earnings-calls/model.ts +238 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
- package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
- package/src/plugins/builtin/fear-greed/headless.ts +84 -0
- package/src/plugins/builtin/fear-greed/index.tsx +4 -0
- package/src/plugins/builtin/holders/client.ts +29 -0
- package/src/plugins/builtin/holders/headless.test.ts +58 -0
- package/src/plugins/builtin/holders/headless.ts +162 -0
- package/src/plugins/builtin/holders/index.tsx +14 -8
- package/src/plugins/builtin/holders/pane.tsx +7 -1
- package/src/plugins/builtin/insider/client.ts +50 -0
- package/src/plugins/builtin/insider/headless.test.ts +122 -0
- package/src/plugins/builtin/insider/headless.ts +117 -0
- package/src/plugins/builtin/insider/index.tsx +21 -50
- package/src/plugins/builtin/insider/insider-data.ts +1 -1
- package/src/plugins/builtin/insider/model.ts +72 -0
- package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
- package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
- package/src/plugins/builtin/market-movers/client.ts +76 -0
- package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
- package/src/plugins/builtin/market-movers/headless.ts +73 -0
- package/src/plugins/builtin/market-movers/index.tsx +11 -51
- package/src/plugins/builtin/news/headless.test.ts +138 -0
- package/src/plugins/builtin/news/headless.ts +190 -0
- package/src/plugins/builtin/news/index.tsx +13 -9
- package/src/plugins/builtin/news/wire/index.tsx +2 -1
- package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
- package/src/plugins/builtin/research/analyst-headless.ts +172 -0
- package/src/plugins/builtin/research/analyst-model.ts +219 -0
- package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
- package/src/plugins/builtin/research/client.ts +12 -0
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
- package/src/plugins/builtin/research/event-model.ts +245 -0
- package/src/plugins/builtin/research/events-headless.test.ts +32 -0
- package/src/plugins/builtin/research/events-headless.ts +87 -0
- package/src/plugins/builtin/research/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +32 -18
- package/src/plugins/builtin/research-search/pane.tsx +4 -0
- package/src/plugins/builtin/scanner/headless.test.ts +59 -0
- package/src/plugins/builtin/scanner/headless.ts +125 -0
- package/src/plugins/builtin/scanner/index.ts +4 -0
- package/src/plugins/builtin/sec/client.ts +16 -0
- package/src/plugins/builtin/sec/headless.test.ts +72 -0
- package/src/plugins/builtin/sec/headless.ts +69 -0
- package/src/plugins/builtin/sec/index.tsx +20 -75
- package/src/plugins/builtin/sec/model.ts +89 -0
- package/src/plugins/builtin/sectors/client.ts +54 -0
- package/src/plugins/builtin/sectors/headless.test.ts +28 -0
- package/src/plugins/builtin/sectors/headless.ts +80 -0
- package/src/plugins/builtin/sectors/index.tsx +6 -51
- package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
- package/src/plugins/builtin/short-interest/client.ts +5 -2
- package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
- package/src/plugins/builtin/short-interest/headless.ts +111 -0
- package/src/plugins/builtin/short-interest/index.tsx +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +13 -11
- package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
- package/src/plugins/builtin/thirteenf/headless.ts +286 -0
- package/src/plugins/builtin/thirteenf/index.tsx +4 -0
- package/src/plugins/builtin/thirteenf/model.ts +13 -0
- package/src/plugins/builtin/thirteenf/table.ts +3 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
- package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
- package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
- package/src/plugins/builtin/volatility/client.ts +16 -3
- package/src/plugins/builtin/volatility/headless.test.ts +35 -0
- package/src/plugins/builtin/volatility/headless.ts +75 -0
- package/src/plugins/builtin/volatility/index.tsx +4 -0
- package/src/plugins/builtin/world-indices/client.ts +43 -0
- package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
- package/src/plugins/builtin/world-indices/headless.ts +109 -0
- package/src/plugins/builtin/world-indices/index.tsx +4 -0
- package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
- package/src/plugins/builtin/yield-curve/headless.ts +67 -0
- package/src/plugins/builtin/yield-curve/index.tsx +6 -8
- package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
- package/src/plugins/bundle.test.ts +58 -1
- package/src/plugins/bundle.ts +4 -2
- package/src/plugins/catalog-backend.test.ts +26 -0
- package/src/plugins/catalog-backend.ts +9 -2
- package/src/plugins/catalog-browser.test.ts +0 -13
- package/src/plugins/catalog-browser.ts +4 -0
- package/src/plugins/catalog-manifest.test.ts +15 -0
- package/src/plugins/catalog-manifest.ts +12 -5
- package/src/plugins/catalog-ui.ts +0 -6
- package/src/plugins/host-contract.ts +9 -0
- package/src/plugins/host-modules.ts +12 -0
- package/src/plugins/loader.ts +63 -15
- package/src/plugins/prediction-markets/headless.test.ts +119 -0
- package/src/plugins/prediction-markets/headless.ts +255 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/seed.ts +3 -0
- package/src/public/broker.ts +6 -0
- package/src/remote/schema.test.ts +65 -0
- package/src/remote/schema.ts +322 -39
- package/src/remote/types.ts +49 -2
- package/src/renderers/browser/app-services.ts +3 -0
- package/src/renderers/browser/config-host.ts +24 -8
- package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
- package/src/renderers/browser/deeplink-bridge.ts +15 -3
- package/src/renderers/browser/main.tsx +3 -1
- package/src/renderers/browser/research-entry.test.ts +8 -0
- package/src/renderers/browser/research-entry.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
- package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
- package/src/renderers/electrobun/view/build-assets.ts +0 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
- package/src/sources/gloomberb-cloud/news.ts +1 -0
- package/src/sync/core-contributors.test.ts +111 -0
- package/src/sync/core-contributors.ts +155 -1
- package/src/time-series/resolve.test.ts +118 -1
- package/src/time-series/resolve.ts +14 -16
- package/src/types/config.ts +2 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
- package/src/plugins/broker-sync/native-loader.ts +0 -10
- package/src/plugins/broker-sync/normalize.ts +0 -308
- package/src/plugins/broker-sync/public.ts +0 -122
- package/src/plugins/broker-sync/robinhood-native.ts +0 -367
- package/src/plugins/broker-sync/robinhood.ts +0 -114
- package/src/plugins/broker-sync/simplefin-native.ts +0 -201
- package/src/plugins/broker-sync/simplefin.ts +0 -70
- 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:
|
|
390
|
-
bufferedSeries:
|
|
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
|
-
|
|
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.
|
package/src/types/config.ts
CHANGED
|
@@ -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
|
-
}
|