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
|
@@ -2,7 +2,8 @@ import { existsSync, mkdirSync } from "fs";
|
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { createAppServices, type AppServices } from "../../../../core/app-services";
|
|
5
|
-
import {
|
|
5
|
+
import { loadDesktopBackendPlugins } from "../../../../plugins/catalog-backend";
|
|
6
|
+
import { restoreExtractedPlugins } from "../../../../cli/restore-plugins";
|
|
6
7
|
import type { AppSessionSnapshot } from "../../../../core/state/session-persistence";
|
|
7
8
|
import {
|
|
8
9
|
getDataDir,
|
|
@@ -133,13 +134,19 @@ export async function initializeDesktopBackend<TRpc>(
|
|
|
133
134
|
});
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
|
|
137
|
+
const initialConfig = await initDataDir(await resolveDesktopDataDir());
|
|
138
|
+
// Restore plugins that moved into their own repositories before the catalog
|
|
139
|
+
// is read, the same as the terminal does at startup. Without it a
|
|
140
|
+
// desktop-only user silently loses a feature the day it is extracted, since
|
|
141
|
+
// nothing else installs it for them.
|
|
142
|
+
const seededPlugins = await restoreExtractedPlugins();
|
|
143
|
+
options.setCurrentConfig(seededPlugins ? { ...initialConfig, seededPlugins } : initialConfig);
|
|
137
144
|
const config = options.getCurrentConfig();
|
|
138
145
|
if (!config) throw new Error("Desktop config failed to initialize.");
|
|
139
146
|
|
|
140
147
|
const services = createAppServices({
|
|
141
148
|
config,
|
|
142
|
-
plugins:
|
|
149
|
+
plugins: await loadDesktopBackendPlugins(),
|
|
143
150
|
});
|
|
144
151
|
options.setServices(services);
|
|
145
152
|
await services.ready;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { readdir, stat } from "fs/promises";
|
|
1
|
+
import { readdir, rm, stat } from "fs/promises";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
|
|
5
5
|
import type { DesktopExternalPluginBundle } from "../shared/protocol";
|
|
6
6
|
import { bundleExternalPlugin, pluginBundleCacheDir } from "../../../plugins/bundle";
|
|
7
7
|
import { linkHostPackages } from "../../../plugins/host-link";
|
|
8
|
-
import { getPluginsDir, resolvePluginEntry } from "../../../plugins/loader";
|
|
8
|
+
import { getPluginCacheDir, getPluginsDir, isPluginDirectory, resolvePluginEntry } from "../../../plugins/loader";
|
|
9
9
|
import type { GloomPlugin } from "../../../types/plugin";
|
|
10
10
|
import { debugLog } from "../../../utils/debug-log";
|
|
11
11
|
|
|
@@ -60,15 +60,31 @@ async function readPluginMetadata(entryFile: string): Promise<GloomPlugin | null
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Bundles used to be cached inside the plugins folder, where the CLI listed
|
|
65
|
+
* `.cache` as an installed plugin and tried to `git pull` it. It is only a
|
|
66
|
+
* cache, so the old copy is dropped rather than migrated.
|
|
67
|
+
*/
|
|
68
|
+
async function removeLegacyBundleCache(pluginsDir: string): Promise<void> {
|
|
69
|
+
const legacy = join(pluginsDir, ".cache");
|
|
70
|
+
if (!existsSync(legacy)) return;
|
|
71
|
+
try {
|
|
72
|
+
await rm(legacy, { recursive: true, force: true });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
log.error(`Could not remove the legacy bundle cache: ${error}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
export async function collectExternalPluginBundles(): Promise<DesktopExternalPluginBundle[]> {
|
|
64
79
|
const pluginsDir = getPluginsDir();
|
|
65
80
|
if (!existsSync(pluginsDir)) return [];
|
|
66
81
|
|
|
67
|
-
|
|
82
|
+
await removeLegacyBundleCache(pluginsDir);
|
|
83
|
+
const outDir = pluginBundleCacheDir(getPluginCacheDir());
|
|
68
84
|
const bundles: DesktopExternalPluginBundle[] = [];
|
|
69
85
|
|
|
70
86
|
for (const entry of await readdir(pluginsDir, { withFileTypes: true })) {
|
|
71
|
-
if (!entry.isDirectory() || entry.name
|
|
87
|
+
if (!entry.isDirectory() || !isPluginDirectory(entry.name)) continue;
|
|
72
88
|
const pluginDir = join(pluginsDir, entry.name);
|
|
73
89
|
|
|
74
90
|
const entryFile = await resolvePluginEntry(pluginDir);
|
|
@@ -23,8 +23,6 @@ const COMMON_ALIAS_RULES: AliasRule[] = [
|
|
|
23
23
|
["./kitty/support", "components/chart/native/renderer-selection.ts", "native-stubs/chart/kitty-support.ts"],
|
|
24
24
|
["native/surface/manager", "native-stubs/chart/surface-manager.ts"],
|
|
25
25
|
["native/surface/sync", "native-stubs/chart/surface-sync.ts"],
|
|
26
|
-
["./native-loader", "plugins/broker-sync/robinhood.ts", "native-stubs/broker-sync-native-loader.ts"],
|
|
27
|
-
["./native-loader", "plugins/broker-sync/simplefin.ts", "native-stubs/broker-sync-native-loader.ts"],
|
|
28
26
|
];
|
|
29
27
|
|
|
30
28
|
export function electrobunViewPath(...parts: string[]): string {
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
getPresetResolution,
|
|
27
27
|
normalizeChartResolutionSupport,
|
|
28
28
|
TIME_RANGE_ORDER,
|
|
29
|
+
type ManualChartResolution,
|
|
29
30
|
} from "../../../time-series/resolution";
|
|
30
31
|
import type { TimeRange } from "../../../components/chart/core/types";
|
|
31
32
|
import type { AppConfig } from "../../../types/config";
|
|
@@ -35,7 +36,7 @@ import type {
|
|
|
35
36
|
} from "../../../core/app-service-ports";
|
|
36
37
|
import type { CachedResourceRecord, ResourceCacheKey, SetResourceOptions } from "../../../data/resource-store";
|
|
37
38
|
import type { CachedFinancialsTarget, DataProvider, QuoteSubscriptionTarget } from "../../../types/data-provider";
|
|
38
|
-
import type { OptionsChain, TickerFinancials } from "../../../types/financials";
|
|
39
|
+
import type { OptionsChain, PricePoint, TickerFinancials } from "../../../types/financials";
|
|
39
40
|
import type { TickerRecord } from "../../../types/ticker";
|
|
40
41
|
import type { AppState, PaneRuntimeState } from "../../../core/state/app/state";
|
|
41
42
|
import type { PaneDef } from "../../../types/plugin";
|
|
@@ -50,6 +51,19 @@ import { chartSeriesSourceKey } from "../../../capabilities";
|
|
|
50
51
|
import { apiClient, setCloudApiFetchTransport } from "../../../api-client";
|
|
51
52
|
import { createGloomberbCloudProvider } from "../../../sources/gloomberb-cloud";
|
|
52
53
|
|
|
54
|
+
interface CliPaneShotIntradayHistory {
|
|
55
|
+
symbol: string;
|
|
56
|
+
exchange: string;
|
|
57
|
+
rangePreset: "1D" | "1W";
|
|
58
|
+
resolution: ManualChartResolution;
|
|
59
|
+
requestedSession: string | null;
|
|
60
|
+
sessionDates: string[];
|
|
61
|
+
points: PricePoint[];
|
|
62
|
+
start: string | null;
|
|
63
|
+
end: string | null;
|
|
64
|
+
unavailableReason: string | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
interface CliPaneShotPayload {
|
|
54
68
|
config: AppConfig;
|
|
55
69
|
paneId: string;
|
|
@@ -57,6 +71,7 @@ interface CliPaneShotPayload {
|
|
|
57
71
|
heightCells: number;
|
|
58
72
|
tickers: TickerRecord[];
|
|
59
73
|
financials: Array<[string, TickerFinancials]>;
|
|
74
|
+
intradayHistories: CliPaneShotIntradayHistory[];
|
|
60
75
|
optionsChains: Array<[string, OptionsChain]>;
|
|
61
76
|
fredSeries: Array<[string, FredSeriesCacheEntry]>;
|
|
62
77
|
valuationSeries: Array<[string, DatedObservation[]]>;
|
|
@@ -187,8 +202,11 @@ function isShotLoadingTextVisible(): boolean {
|
|
|
187
202
|
* price presets passed.
|
|
188
203
|
*/
|
|
189
204
|
function revivePayloadDates(payload: CliPaneShotPayload): void {
|
|
190
|
-
|
|
191
|
-
|
|
205
|
+
const histories = [
|
|
206
|
+
...payload.financials.map(([, data]) => data.priceHistory),
|
|
207
|
+
...(payload.intradayHistories ?? []).map((entry) => entry.points),
|
|
208
|
+
];
|
|
209
|
+
for (const history of histories) {
|
|
192
210
|
if (!Array.isArray(history)) continue;
|
|
193
211
|
for (const point of history) {
|
|
194
212
|
if (!(point.date instanceof Date)) point.date = new Date(point.date as unknown as string);
|
|
@@ -266,6 +284,16 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
|
|
|
266
284
|
financials.set(canonicalTickerKey(instrument.symbol, instrument.exchange), data);
|
|
267
285
|
if (!instrument.exchange) financials.set(normalizeSymbol(instrument.symbol), data);
|
|
268
286
|
}
|
|
287
|
+
const intradayHistories = new Map<string, CliPaneShotIntradayHistory>();
|
|
288
|
+
for (const entry of payload.intradayHistories ?? []) {
|
|
289
|
+
intradayHistories.set(canonicalTickerKey(entry.symbol, entry.exchange), entry);
|
|
290
|
+
intradayHistories.set(normalizeSymbol(entry.symbol), entry);
|
|
291
|
+
}
|
|
292
|
+
const intradayHistory = (symbol: string, exchange?: string) => (
|
|
293
|
+
intradayHistories.get(canonicalTickerKey(symbol, exchange))
|
|
294
|
+
?? intradayHistories.get(normalizeSymbol(symbol))
|
|
295
|
+
);
|
|
296
|
+
|
|
269
297
|
const optionsChains = new Map<string, OptionsChain>();
|
|
270
298
|
for (const [key, data] of payload.optionsChains ?? []) {
|
|
271
299
|
const instrument = parsePublicTickerKey(key);
|
|
@@ -345,14 +373,39 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
|
|
|
345
373
|
return resolveShotWork(null);
|
|
346
374
|
},
|
|
347
375
|
getPriceHistory(ticker, exchange, range) {
|
|
348
|
-
return trackShotWork(Promise.resolve().then(() =>
|
|
349
|
-
|
|
350
|
-
|
|
376
|
+
return trackShotWork(Promise.resolve().then(() => {
|
|
377
|
+
const intraday = intradayHistory(ticker, exchange);
|
|
378
|
+
if (intraday?.unavailableReason) throw new Error(intraday.unavailableReason);
|
|
379
|
+
return clipPriceHistoryToRange(
|
|
380
|
+
intraday ? intraday.points : getFinancials(ticker, exchange).priceHistory ?? [],
|
|
381
|
+
range,
|
|
382
|
+
);
|
|
383
|
+
}));
|
|
384
|
+
},
|
|
385
|
+
getPriceHistoryForResolution(ticker, exchange, bufferRange, resolution) {
|
|
386
|
+
return trackShotWork(Promise.resolve().then(() => {
|
|
387
|
+
const intraday = intradayHistory(ticker, exchange);
|
|
388
|
+
if (intraday?.unavailableReason) throw new Error(intraday.unavailableReason);
|
|
389
|
+
if (intraday && intraday.resolution !== resolution) return [];
|
|
390
|
+
return clipPriceHistoryToRange(
|
|
391
|
+
intraday ? intraday.points : getFinancials(ticker, exchange).priceHistory ?? [],
|
|
392
|
+
bufferRange,
|
|
393
|
+
);
|
|
394
|
+
}));
|
|
351
395
|
},
|
|
352
|
-
|
|
353
|
-
return trackShotWork(Promise.resolve().then(() =>
|
|
354
|
-
|
|
355
|
-
|
|
396
|
+
getDetailedPriceHistory(ticker, exchange, startDate, endDate, resolution) {
|
|
397
|
+
return trackShotWork(Promise.resolve().then(() => {
|
|
398
|
+
const intraday = intradayHistory(ticker, exchange);
|
|
399
|
+
if (intraday?.unavailableReason) throw new Error(intraday.unavailableReason);
|
|
400
|
+
if (intraday && intraday.resolution !== resolution) return [];
|
|
401
|
+
const start = startDate.getTime();
|
|
402
|
+
const end = endDate.getTime();
|
|
403
|
+
const points = intraday?.points ?? getFinancials(ticker, exchange).priceHistory ?? [];
|
|
404
|
+
return points.filter((point) => {
|
|
405
|
+
const timestamp = point.date.getTime();
|
|
406
|
+
return timestamp >= start && timestamp < end;
|
|
407
|
+
});
|
|
408
|
+
}));
|
|
356
409
|
},
|
|
357
410
|
getChartResolutionSupport() {
|
|
358
411
|
return resolveShotWork(SHOT_CHART_RESOLUTION_SUPPORT);
|
|
@@ -129,6 +129,99 @@ describe("core sync contributors", () => {
|
|
|
129
129
|
}
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
test("preserves local broker identity when applying sanitized portfolios", () => {
|
|
133
|
+
const config = createDefaultConfig("/tmp/gloomberb-sync-broker-identity-test");
|
|
134
|
+
config.portfolios = [{
|
|
135
|
+
id: "broker:demo-live:ACCOUNT-1",
|
|
136
|
+
name: "Primary",
|
|
137
|
+
currency: "USD",
|
|
138
|
+
brokerId: "demo",
|
|
139
|
+
brokerInstanceId: "demo-live",
|
|
140
|
+
brokerAccountId: "ACCOUNT-1",
|
|
141
|
+
lastSyncedAt: 100,
|
|
142
|
+
}];
|
|
143
|
+
|
|
144
|
+
const collected = __syncContributorInternalsForTests.collectCoreConfigPayload(config) as {
|
|
145
|
+
portfolios: Array<Record<string, unknown>>;
|
|
146
|
+
};
|
|
147
|
+
expect(collected.portfolios[0]).not.toHaveProperty("brokerInstanceId");
|
|
148
|
+
expect(collected.portfolios[0]).not.toHaveProperty("brokerAccountId");
|
|
149
|
+
|
|
150
|
+
const merged = __syncContributorInternalsForTests.mergeConfigPayload(config, {
|
|
151
|
+
portfolios: [{
|
|
152
|
+
id: "broker:demo-live:ACCOUNT-1",
|
|
153
|
+
name: "Primary",
|
|
154
|
+
currency: "USD",
|
|
155
|
+
brokerId: "demo",
|
|
156
|
+
lastSyncedAt: 200,
|
|
157
|
+
}],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(merged?.portfolios).toEqual([{
|
|
161
|
+
id: "broker:demo-live:ACCOUNT-1",
|
|
162
|
+
name: "Primary",
|
|
163
|
+
currency: "USD",
|
|
164
|
+
brokerId: "demo",
|
|
165
|
+
brokerInstanceId: "demo-live",
|
|
166
|
+
brokerAccountId: "ACCOUNT-1",
|
|
167
|
+
lastSyncedAt: 200,
|
|
168
|
+
}]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("does not reintroduce older unlinked broker portfolios from cloud", () => {
|
|
172
|
+
const config = createDefaultConfig("/tmp/gloomberb-sync-broker-identity-test");
|
|
173
|
+
config.portfolios = [
|
|
174
|
+
{
|
|
175
|
+
id: "main",
|
|
176
|
+
name: "Main",
|
|
177
|
+
currency: "USD",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "broker:demo-live:ACCOUNT-1",
|
|
181
|
+
name: "Primary",
|
|
182
|
+
currency: "USD",
|
|
183
|
+
brokerId: "demo",
|
|
184
|
+
brokerInstanceId: "demo-live",
|
|
185
|
+
brokerAccountId: "ACCOUNT-1",
|
|
186
|
+
lastSyncedAt: 200,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "broker:demo-live:ACCOUNT-2",
|
|
190
|
+
name: "Secondary",
|
|
191
|
+
currency: "USD",
|
|
192
|
+
brokerId: "demo",
|
|
193
|
+
brokerInstanceId: "demo-live",
|
|
194
|
+
brokerAccountId: "ACCOUNT-2",
|
|
195
|
+
lastSyncedAt: 200,
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
const merged = __syncContributorInternalsForTests.mergeConfigPayload(config, {
|
|
200
|
+
portfolios: [
|
|
201
|
+
{ id: "main", name: "Main", currency: "USD" },
|
|
202
|
+
{ id: "broker:demo-live:ACCOUNT-1", name: "Primary", currency: "USD", brokerId: "demo", lastSyncedAt: 200 },
|
|
203
|
+
{ id: "broker:demo-live:ACCOUNT-2", name: "Secondary", currency: "USD", brokerId: "demo", lastSyncedAt: 200 },
|
|
204
|
+
{ id: "broker:demo-old:ACCOUNT-1", name: "Primary", currency: "USD", brokerId: "demo", lastSyncedAt: 100 },
|
|
205
|
+
{ id: "broker:demo-old:ACCOUNT-2", name: "Secondary", currency: "USD", brokerId: "demo", lastSyncedAt: 100 },
|
|
206
|
+
{ id: "broker:demo-old:REMOVED", name: "Removed", currency: "USD", brokerId: "demo", lastSyncedAt: 100 },
|
|
207
|
+
{ id: "broker:demo-other:ACCOUNT-3", name: "Remote", currency: "USD", brokerId: "demo", lastSyncedAt: 300 },
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(merged?.portfolios).toEqual([
|
|
212
|
+
config.portfolios[0],
|
|
213
|
+
config.portfolios[1],
|
|
214
|
+
config.portfolios[2],
|
|
215
|
+
{
|
|
216
|
+
id: "broker:demo-other:ACCOUNT-3",
|
|
217
|
+
name: "Remote",
|
|
218
|
+
currency: "USD",
|
|
219
|
+
brokerId: "demo",
|
|
220
|
+
lastSyncedAt: 300,
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
|
|
132
225
|
test("keeps resumable onboarding local until the guide is complete", async () => {
|
|
133
226
|
const config = createDefaultConfig("/tmp/gloomberb-sync-test");
|
|
134
227
|
config.onboardingComplete = false;
|
|
@@ -258,6 +351,24 @@ describe("core sync contributors", () => {
|
|
|
258
351
|
expect((payload as any).tickers[0].quote.weekReferencePrice).toBe(125);
|
|
259
352
|
expect((payload as any).tickers[0].quote.weekChangePercent).toBe(20);
|
|
260
353
|
expect((payload as any).analyticsByPortfolio.main.oneYearReturn).toBe(0.42);
|
|
354
|
+
|
|
355
|
+
const saved: TickerRecord[] = [];
|
|
356
|
+
const sanitizedTickerPayload = { tickers: (payload as any).tickers };
|
|
357
|
+
await coreCollectionsSyncContributor.apply?.(sanitizedTickerPayload, {
|
|
358
|
+
baselinePayload: sanitizedTickerPayload,
|
|
359
|
+
baselineState: state,
|
|
360
|
+
state,
|
|
361
|
+
getState: () => state,
|
|
362
|
+
isCurrent: () => true,
|
|
363
|
+
dispatch: () => {},
|
|
364
|
+
tickerRepository: { saveTicker: async (record: TickerRecord) => { saved.push(record); } },
|
|
365
|
+
} as unknown as Parameters<NonNullable<typeof coreCollectionsSyncContributor.apply>>[1]);
|
|
366
|
+
|
|
367
|
+
expect(saved[0]?.metadata.positions[0]).toMatchObject({
|
|
368
|
+
brokerInstanceId: "broker-id",
|
|
369
|
+
brokerAccountId: "account-id",
|
|
370
|
+
brokerContractId: 42,
|
|
371
|
+
});
|
|
261
372
|
});
|
|
262
373
|
|
|
263
374
|
test("keeps a position written while the app was closed", async () => {
|
|
@@ -58,6 +58,116 @@ function sanitizePortfolio(portfolio: Portfolio): Portfolio {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function brokerAccountIdFromPortfolioId(portfolioId: string): string | undefined {
|
|
62
|
+
const [kind, instanceId, ...accountParts] = portfolioId.split(":");
|
|
63
|
+
if (kind !== "broker" || !instanceId) return undefined;
|
|
64
|
+
const accountId = accountParts.join(":").trim();
|
|
65
|
+
return accountId && accountId !== "default" ? accountId : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function brokerPortfolioKey(brokerId: string | undefined, accountId: string | undefined): string | undefined {
|
|
69
|
+
if (!brokerId || !accountId) return undefined;
|
|
70
|
+
return [brokerId, accountId].join("\u0000");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function portfolioSyncTimestamp(portfolio: Portfolio): number {
|
|
74
|
+
return typeof portfolio.lastSyncedAt === "number" && Number.isFinite(portfolio.lastSyncedAt)
|
|
75
|
+
? portfolio.lastSyncedAt
|
|
76
|
+
: 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function latestBrokerSyncByType(portfolios: Portfolio[], linkedOnly: boolean): Map<string, number> {
|
|
80
|
+
const latest = new Map<string, number>();
|
|
81
|
+
for (const portfolio of portfolios) {
|
|
82
|
+
if (!portfolio.brokerId || (linkedOnly && !portfolio.brokerInstanceId)) continue;
|
|
83
|
+
const timestamp = portfolioSyncTimestamp(portfolio);
|
|
84
|
+
latest.set(portfolio.brokerId, Math.max(latest.get(portfolio.brokerId) ?? 0, timestamp));
|
|
85
|
+
}
|
|
86
|
+
return latest;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Broker identity is deliberately excluded from cloud payloads, but it is
|
|
91
|
+
* still local state that must survive applying a sanitized portfolio back to
|
|
92
|
+
* this device. Match by the stable portfolio ID first, then by the account
|
|
93
|
+
* suffix in that stable ID. A newer local broker snapshot also wins over old
|
|
94
|
+
* unlinked records left in a cloud snapshot from before a reset.
|
|
95
|
+
*/
|
|
96
|
+
function mergeLocalBrokerPortfolioIdentity(
|
|
97
|
+
localPortfolios: Portfolio[],
|
|
98
|
+
syncedPortfolios: Portfolio[],
|
|
99
|
+
): Portfolio[] {
|
|
100
|
+
const localById = new Map(localPortfolios.map((portfolio) => [portfolio.id, portfolio] as const));
|
|
101
|
+
const localByBrokerAccount = new Map<string, Portfolio[]>();
|
|
102
|
+
for (const portfolio of localPortfolios) {
|
|
103
|
+
if (!portfolio.brokerInstanceId) continue;
|
|
104
|
+
const key = brokerPortfolioKey(portfolio.brokerId, portfolio.brokerAccountId);
|
|
105
|
+
if (!key) continue;
|
|
106
|
+
const entries = localByBrokerAccount.get(key) ?? [];
|
|
107
|
+
entries.push(portfolio);
|
|
108
|
+
localByBrokerAccount.set(key, entries);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const exactIncomingIds = new Set(
|
|
112
|
+
syncedPortfolios
|
|
113
|
+
.map((portfolio) => portfolio.id)
|
|
114
|
+
.filter((portfolioId) => localById.has(portfolioId)),
|
|
115
|
+
);
|
|
116
|
+
const localLatestByBroker = latestBrokerSyncByType(localPortfolios, true);
|
|
117
|
+
const syncedLatestByBroker = latestBrokerSyncByType(syncedPortfolios, false);
|
|
118
|
+
const merged: Portfolio[] = [];
|
|
119
|
+
const seenIds = new Set<string>();
|
|
120
|
+
|
|
121
|
+
for (const portfolio of syncedPortfolios) {
|
|
122
|
+
const localByStableId = localById.get(portfolio.id);
|
|
123
|
+
const accountId = brokerAccountIdFromPortfolioId(portfolio.id);
|
|
124
|
+
const accountCandidates = localByBrokerAccount.get(brokerPortfolioKey(portfolio.brokerId, accountId) ?? "");
|
|
125
|
+
const local = localByStableId?.brokerInstanceId && localByStableId.brokerId === portfolio.brokerId
|
|
126
|
+
? localByStableId
|
|
127
|
+
: accountCandidates?.length === 1
|
|
128
|
+
? accountCandidates[0]
|
|
129
|
+
: undefined;
|
|
130
|
+
|
|
131
|
+
if (local) {
|
|
132
|
+
// Prefer an exact stable-ID entry when an older duplicate appears first
|
|
133
|
+
// in the remote array.
|
|
134
|
+
if (portfolio.id !== local.id && exactIncomingIds.has(local.id)) continue;
|
|
135
|
+
if (seenIds.has(local.id)) continue;
|
|
136
|
+
|
|
137
|
+
const localIsNewer = portfolioSyncTimestamp(local) > portfolioSyncTimestamp(portfolio);
|
|
138
|
+
merged.push(localIsNewer
|
|
139
|
+
? local
|
|
140
|
+
: {
|
|
141
|
+
...portfolio,
|
|
142
|
+
id: local.id,
|
|
143
|
+
brokerInstanceId: local.brokerInstanceId,
|
|
144
|
+
brokerAccountId: local.brokerAccountId,
|
|
145
|
+
});
|
|
146
|
+
seenIds.add(local.id);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const localLatest = portfolio.brokerId ? localLatestByBroker.get(portfolio.brokerId) : undefined;
|
|
151
|
+
if (localLatest !== undefined && localLatest > portfolioSyncTimestamp(portfolio)) continue;
|
|
152
|
+
merged.push(portfolio);
|
|
153
|
+
seenIds.add(portfolio.id);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// If the newer local broker snapshot is absent from the cloud array
|
|
157
|
+
// altogether, keep it instead of treating the older array as authoritative.
|
|
158
|
+
for (const portfolio of localPortfolios) {
|
|
159
|
+
if (!portfolio.brokerInstanceId || seenIds.has(portfolio.id)) continue;
|
|
160
|
+
const localLatest = portfolio.brokerId ? localLatestByBroker.get(portfolio.brokerId) : undefined;
|
|
161
|
+
const syncedLatest = portfolio.brokerId ? syncedLatestByBroker.get(portfolio.brokerId) ?? 0 : 0;
|
|
162
|
+
if (localLatest !== undefined && localLatest > syncedLatest) {
|
|
163
|
+
merged.push(portfolio);
|
|
164
|
+
seenIds.add(portfolio.id);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return merged;
|
|
169
|
+
}
|
|
170
|
+
|
|
61
171
|
function sanitizeWatchlist(watchlist: Watchlist): Watchlist {
|
|
62
172
|
return {
|
|
63
173
|
id: watchlist.id,
|
|
@@ -93,6 +203,38 @@ function sanitizePosition(position: TickerPosition): TickerPosition {
|
|
|
93
203
|
};
|
|
94
204
|
}
|
|
95
205
|
|
|
206
|
+
function brokerPositionKey(position: Pick<TickerPosition, "portfolio" | "broker" | "side">): string {
|
|
207
|
+
return [position.portfolio, position.broker, position.side ?? ""].join("\u0000");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Keep broker position identity local while applying public cloud fields. */
|
|
211
|
+
function mergeLocalBrokerPositionIdentity(
|
|
212
|
+
localPositions: TickerPosition[],
|
|
213
|
+
syncedPositions: TickerPosition[],
|
|
214
|
+
): TickerPosition[] {
|
|
215
|
+
const localByKey = new Map<string, TickerPosition[]>();
|
|
216
|
+
for (const position of localPositions) {
|
|
217
|
+
if (!position.brokerInstanceId) continue;
|
|
218
|
+
const key = brokerPositionKey(position);
|
|
219
|
+
const entries = localByKey.get(key) ?? [];
|
|
220
|
+
entries.push(position);
|
|
221
|
+
localByKey.set(key, entries);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return syncedPositions.map((position) => {
|
|
225
|
+
const entries = localByKey.get(brokerPositionKey(position));
|
|
226
|
+
const local = entries?.shift();
|
|
227
|
+
if (!local) return position;
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
...position,
|
|
231
|
+
brokerInstanceId: local.brokerInstanceId,
|
|
232
|
+
brokerAccountId: local.brokerAccountId,
|
|
233
|
+
brokerContractId: local.brokerContractId,
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
96
238
|
function pricePointTime(point: { date: Date | string | number }): number {
|
|
97
239
|
const value = point.date;
|
|
98
240
|
if (value instanceof Date) return value.getTime();
|
|
@@ -405,7 +547,12 @@ function mergeConfigPayload(
|
|
|
405
547
|
|
|
406
548
|
assign("baseCurrency");
|
|
407
549
|
assign("refreshIntervalMinutes");
|
|
408
|
-
|
|
550
|
+
if ("portfolios" in payload && canApply("portfolios")) {
|
|
551
|
+
const syncedPortfolios = payload.portfolios;
|
|
552
|
+
next.portfolios = Array.isArray(syncedPortfolios)
|
|
553
|
+
? mergeLocalBrokerPortfolioIdentity(config.portfolios, syncedPortfolios as Portfolio[])
|
|
554
|
+
: syncedPortfolios as AppConfig["portfolios"];
|
|
555
|
+
}
|
|
409
556
|
assign("watchlists");
|
|
410
557
|
assign("disabledSources");
|
|
411
558
|
assign("theme");
|
|
@@ -532,9 +679,16 @@ export const coreCollectionsSyncContributor: SyncContributor = {
|
|
|
532
679
|
// Local edits the cloud has never seen (CLI positions, offline changes)
|
|
533
680
|
// win here and are uploaded by the push that follows this pull.
|
|
534
681
|
if (tickerChangedSinceLastSync(current, lastSyncedTickers)) continue;
|
|
682
|
+
const syncedPositions = Array.isArray(rawTicker.positions)
|
|
683
|
+
? mergeLocalBrokerPositionIdentity(
|
|
684
|
+
current?.metadata.positions ?? [],
|
|
685
|
+
rawTicker.positions as TickerPosition[],
|
|
686
|
+
)
|
|
687
|
+
: current?.metadata.positions ?? [];
|
|
535
688
|
const metadata = hydrateTickerMetadata({
|
|
536
689
|
...current?.metadata,
|
|
537
690
|
...rawTicker,
|
|
691
|
+
positions: syncedPositions,
|
|
538
692
|
broker_contracts: current?.metadata.broker_contracts ?? [],
|
|
539
693
|
});
|
|
540
694
|
const record: TickerRecord = { metadata };
|
|
@@ -3,7 +3,12 @@ import type { FredSeriesData, FredSeriesLoadResult } from "../data/fred-series";
|
|
|
3
3
|
import { createTestDataProvider } from "../test-support/data-provider";
|
|
4
4
|
import type { TickerFinancials } from "../types/financials";
|
|
5
5
|
import { CHART_SPEC_VERSION, type ChartSpec } from "./types";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ChartResolveCache,
|
|
8
|
+
mergePriceHistoryWindows,
|
|
9
|
+
resolveChartSpecData,
|
|
10
|
+
seedChartResolutionResult,
|
|
11
|
+
} from "./resolve";
|
|
7
12
|
import { chartQuoteOverrideKeyForSource } from "./live-quotes";
|
|
8
13
|
import { chartSeriesSourceKey } from "../capabilities";
|
|
9
14
|
import { buildCustomChartPreset } from "../plugins/builtin/chart-composer/presets";
|
|
@@ -26,6 +31,118 @@ const fredLoad = (
|
|
|
26
31
|
});
|
|
27
32
|
|
|
28
33
|
describe("resolveChartSpecData", () => {
|
|
34
|
+
test("seeds study series so their panels survive the wait for real data", async () => {
|
|
35
|
+
const date = new Date("2026-01-05T00:00:00.000Z");
|
|
36
|
+
const seeded = seedChartResolutionResult(
|
|
37
|
+
{
|
|
38
|
+
version: CHART_SPEC_VERSION,
|
|
39
|
+
viewport: { range: "1M", resolution: "auto" },
|
|
40
|
+
panels: [{ id: "main" }, { id: "volume" }],
|
|
41
|
+
series: [{
|
|
42
|
+
id: "price",
|
|
43
|
+
source: {
|
|
44
|
+
kind: "security",
|
|
45
|
+
instrument: { symbol: "TEST", exchange: "NASDAQ" },
|
|
46
|
+
fieldId: "market.ohlcv",
|
|
47
|
+
},
|
|
48
|
+
style: "candles",
|
|
49
|
+
transform: "raw",
|
|
50
|
+
axis: "left",
|
|
51
|
+
panelId: "main",
|
|
52
|
+
interpolation: "none",
|
|
53
|
+
}],
|
|
54
|
+
studies: [{
|
|
55
|
+
id: "volume",
|
|
56
|
+
kind: "volume",
|
|
57
|
+
inputSeriesIds: ["price"],
|
|
58
|
+
parameters: {},
|
|
59
|
+
panelId: "volume",
|
|
60
|
+
axis: "left",
|
|
61
|
+
}],
|
|
62
|
+
},
|
|
63
|
+
new Map([[
|
|
64
|
+
chartQuoteOverrideKeyForSource({
|
|
65
|
+
kind: "security",
|
|
66
|
+
instrument: { symbol: "TEST", exchange: "NASDAQ" },
|
|
67
|
+
fieldId: "market.ohlcv",
|
|
68
|
+
}),
|
|
69
|
+
[{ date, open: 10, high: 12, low: 9, close: 11, volume: 5_000 }],
|
|
70
|
+
]]),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const volume = seeded?.series.find((entry) => entry.panelId === "volume");
|
|
74
|
+
expect(volume?.points.map((point) => point.value)).toEqual([5_000]);
|
|
75
|
+
expect(seeded?.bufferedSeries).toBe(seeded?.series);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("keeps study output over the whole buffered history its base series carries", async () => {
|
|
79
|
+
const history = Array.from({ length: 900 }, (_, day) => {
|
|
80
|
+
const date = new Date(Date.UTC(2024, 0, 1) + day * 86_400_000);
|
|
81
|
+
return date.getUTCDay() === 0 || date.getUTCDay() === 6
|
|
82
|
+
? null
|
|
83
|
+
: { date, open: 100, high: 101, low: 99, close: 100 + (day % 7), volume: 1_000 + day };
|
|
84
|
+
}).filter((point): point is NonNullable<typeof point> => point !== null);
|
|
85
|
+
const provider = createTestDataProvider({
|
|
86
|
+
getTickerFinancials: async () => emptyFinancials(),
|
|
87
|
+
getChartResolutionSupport: () => [{ resolution: "1d", maxRange: "ALL" }],
|
|
88
|
+
getPriceHistoryForResolution: async () => history,
|
|
89
|
+
getDetailedPriceHistory: async () => history,
|
|
90
|
+
});
|
|
91
|
+
const spec: ChartSpec = {
|
|
92
|
+
version: CHART_SPEC_VERSION,
|
|
93
|
+
viewport: { range: "1Y", resolution: "1d" },
|
|
94
|
+
panels: [{ id: "main" }, { id: "volume", label: "Volume", height: 0.24 }],
|
|
95
|
+
series: [{
|
|
96
|
+
id: "price",
|
|
97
|
+
source: {
|
|
98
|
+
kind: "security",
|
|
99
|
+
instrument: { symbol: "TEST", exchange: "NASDAQ" },
|
|
100
|
+
fieldId: "market.close",
|
|
101
|
+
},
|
|
102
|
+
style: "line",
|
|
103
|
+
transform: "raw",
|
|
104
|
+
axis: "left",
|
|
105
|
+
panelId: "main",
|
|
106
|
+
interpolation: "none",
|
|
107
|
+
}],
|
|
108
|
+
studies: [{
|
|
109
|
+
id: "volume",
|
|
110
|
+
kind: "volume",
|
|
111
|
+
inputSeriesIds: ["price"],
|
|
112
|
+
parameters: {},
|
|
113
|
+
panelId: "volume",
|
|
114
|
+
axis: "left",
|
|
115
|
+
}],
|
|
116
|
+
};
|
|
117
|
+
const sources = {
|
|
118
|
+
dataProvider: provider,
|
|
119
|
+
now: new Date("2026-06-15T00:00:00.000Z"),
|
|
120
|
+
loadFredSeries: async () => fredLoad(),
|
|
121
|
+
};
|
|
122
|
+
const cache = new ChartResolveCache();
|
|
123
|
+
|
|
124
|
+
await resolveChartSpecData(spec, sources, cache);
|
|
125
|
+
// Panning back accumulates older bars into the buffer. The study has to
|
|
126
|
+
// follow, or it empties out wherever the base series has already been.
|
|
127
|
+
const panned = await resolveChartSpecData(spec, sources, cache, {
|
|
128
|
+
requestViewport: {
|
|
129
|
+
start: new Date("2024-02-01T00:00:00.000Z"),
|
|
130
|
+
end: new Date("2024-08-01T00:00:00.000Z"),
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const span = (id: string) => {
|
|
135
|
+
const entry = panned.bufferedSeries?.find((candidate) => candidate.id === id);
|
|
136
|
+
return entry && entry.points.length > 0
|
|
137
|
+
? {
|
|
138
|
+
first: entry.points[0]!.date.toISOString(),
|
|
139
|
+
last: entry.points.at(-1)!.date.toISOString(),
|
|
140
|
+
}
|
|
141
|
+
: null;
|
|
142
|
+
};
|
|
143
|
+
expect(span("volume")).toEqual(span("price"));
|
|
144
|
+
});
|
|
145
|
+
|
|
29
146
|
test("routes futures and Treasury aliases through the existing market and FRED pipelines", async () => {
|
|
30
147
|
const marketRequests: string[] = [];
|
|
31
148
|
const fredRequests: string[] = [];
|