gloomberb 0.7.3 → 0.7.5
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/package.json +1 -1
- package/src/app.tsx +59 -30
- package/src/components/chart/chart-renderer.ts +109 -32
- package/src/components/chart/chart-types.ts +18 -0
- package/src/components/chart/chart.test.ts +152 -5
- package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
- package/src/components/chart/comparison-chart-renderer.ts +50 -11
- package/src/components/chart/comparison-stock-chart.tsx +45 -8
- package/src/components/chart/market-session.ts +187 -0
- package/src/components/chart/native/chart-rasterizer.ts +65 -2
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +96 -0
- package/src/components/chart/stock-chart.tsx +36 -8
- package/src/components/chart/time-axis-label.tsx +56 -2
- package/src/components/command-bar/command-bar.test.tsx +1 -1
- package/src/components/command-bar/command-bar.tsx +26 -2
- package/src/components/command-bar/list-model.ts +8 -4
- package/src/components/command-bar/view-model.test.ts +20 -0
- package/src/components/command-bar/view-model.ts +15 -3
- package/src/components/layout/shell.test.tsx +26 -0
- package/src/components/layout/shell.tsx +69 -21
- package/src/components/ticker-list-table.tsx +4 -15
- package/src/core/state/session-persistence.ts +25 -0
- package/src/market-data/coordinator-subscriptions.test.ts +102 -18
- package/src/market-data/coordinator.test.ts +122 -2
- package/src/market-data/coordinator.ts +192 -55
- package/src/market-data/hooks.test.tsx +31 -0
- package/src/market-data/hooks.tsx +1 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
- package/src/plugins/builtin/chat-controller.ts +8 -7
- package/src/plugins/builtin/chat.test.tsx +55 -0
- package/src/plugins/builtin/chat.tsx +20 -6
- package/src/plugins/builtin/holders/index.test.ts +30 -1
- package/src/plugins/builtin/holders/index.tsx +7 -3
- package/src/plugins/builtin/market-movers/index.tsx +72 -40
- package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
- package/src/plugins/builtin/news-wire/index.tsx +0 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/pane.tsx +26 -9
- package/src/plugins/builtin/portfolio-list.test.tsx +29 -7
- package/src/plugins/builtin/sectors/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/pane.tsx +18 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -1
- package/src/plugins/builtin/world-indices/index.tsx +58 -19
- package/src/plugins/pane-manager.test.ts +31 -0
- package/src/plugins/pane-manager.ts +32 -6
- package/src/renderers/electrobun/bun/index.ts +83 -3
- package/src/renderers/electrobun/shared/protocol.ts +8 -1
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -0
- package/src/renderers/electrobun/view/fatal-screen.tsx +118 -0
- package/src/renderers/electrobun/view/main.tsx +38 -24
- package/src/renderers/electrobun/view/styles.css +48 -4
- package/src/sources/gloomberb-cloud.test.ts +55 -2
- package/src/sources/gloomberb-cloud.ts +180 -29
- package/src/sources/provider-router.test.ts +567 -10
- package/src/sources/provider-router.ts +323 -93
- package/src/sources/yahoo-finance.test.ts +37 -0
- package/src/sources/yahoo-finance.ts +48 -2
- package/src/state/app-bootstrap.test.ts +50 -23
- package/src/state/app-bootstrap.ts +26 -10
- package/src/state/session-persistence.test.ts +5 -3
- package/src/state/use-inline-tickers.ts +3 -0
- package/src/state/use-quote-streaming.ts +11 -0
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +1 -0
- package/src/utils/api-client.test.ts +120 -0
- package/src/utils/api-client.ts +66 -19
- package/src/utils/exchanges.test.ts +16 -0
- package/src/utils/exchanges.ts +33 -0
- package/src/utils/options.ts +1 -1
- package/src/utils/price-history.test.ts +28 -1
- package/src/utils/price-history.ts +16 -0
- package/src/utils/quote-freshness.test.ts +150 -0
- package/src/utils/quote-freshness.ts +42 -1
- package/src/utils/quote-resolution.ts +2 -0
- package/src/utils/quote-time.test.ts +34 -0
- package/src/utils/quote-time.ts +18 -5
- package/src/utils/ticker-search.ts +72 -1
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/** @jsxImportSource react */
|
|
3
|
+
import { Component, useMemo, useState, type ErrorInfo, type ReactNode } from "react";
|
|
4
|
+
import { backendRequest, requestElectrobunRestart } from "./backend-rpc";
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface Window {
|
|
8
|
+
__gloomRenderFatalError?: (error: unknown, details?: string) => void;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface DesktopFatalScreenProps {
|
|
13
|
+
title?: string;
|
|
14
|
+
error: unknown;
|
|
15
|
+
details?: string;
|
|
16
|
+
source: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ElectrobunErrorBoundaryState {
|
|
20
|
+
hasError: boolean;
|
|
21
|
+
error: unknown;
|
|
22
|
+
details?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatFatalError(error: unknown, details?: string): string {
|
|
26
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
27
|
+
return [message, details].filter((value): value is string => Boolean(value)).join("\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function DesktopFatalScreen({
|
|
31
|
+
title = "Gloomberb crashed",
|
|
32
|
+
error,
|
|
33
|
+
details,
|
|
34
|
+
source,
|
|
35
|
+
}: DesktopFatalScreenProps) {
|
|
36
|
+
const [status, setStatus] = useState<string | null>(null);
|
|
37
|
+
const errorText = useMemo(() => formatFatalError(error, details), [details, error]);
|
|
38
|
+
|
|
39
|
+
const reloadWindow = () => {
|
|
40
|
+
window.location.reload();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const restartApp = () => {
|
|
44
|
+
setStatus("Restart requested...");
|
|
45
|
+
try {
|
|
46
|
+
requestElectrobunRestart({
|
|
47
|
+
source,
|
|
48
|
+
reason: errorText.slice(0, 240),
|
|
49
|
+
});
|
|
50
|
+
window.setTimeout(() => {
|
|
51
|
+
setStatus("If the app stays open, quit it and open it again.");
|
|
52
|
+
}, 2500);
|
|
53
|
+
} catch (restartError) {
|
|
54
|
+
setStatus(`Restart request failed: ${formatFatalError(restartError)}`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const copyError = async () => {
|
|
59
|
+
try {
|
|
60
|
+
if (navigator.clipboard?.writeText) {
|
|
61
|
+
await navigator.clipboard.writeText(errorText);
|
|
62
|
+
} else {
|
|
63
|
+
await backendRequest("host.copyText", { text: errorText });
|
|
64
|
+
}
|
|
65
|
+
setStatus("Error copied.");
|
|
66
|
+
} catch (copyError) {
|
|
67
|
+
setStatus(`Copy failed: ${formatFatalError(copyError)}`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="gloom-fatal">
|
|
73
|
+
<h1>{title}</h1>
|
|
74
|
+
<div className="gloom-fatal-actions">
|
|
75
|
+
<button type="button" data-variant="primary" onClick={restartApp}>Restart app</button>
|
|
76
|
+
<button type="button" onClick={reloadWindow}>Reload window</button>
|
|
77
|
+
<button type="button" onClick={copyError}>Copy error</button>
|
|
78
|
+
</div>
|
|
79
|
+
{status && <div className="gloom-fatal-status" aria-live="polite">{status}</div>}
|
|
80
|
+
<pre>{errorText}</pre>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class ElectrobunErrorBoundary extends Component<
|
|
86
|
+
{ children: ReactNode },
|
|
87
|
+
ElectrobunErrorBoundaryState
|
|
88
|
+
> {
|
|
89
|
+
override state: ElectrobunErrorBoundaryState = { hasError: false, error: null };
|
|
90
|
+
|
|
91
|
+
static getDerivedStateFromError(error: unknown): ElectrobunErrorBoundaryState {
|
|
92
|
+
return { hasError: true, error };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override componentDidCatch(error: unknown, errorInfo: ErrorInfo): void {
|
|
96
|
+
console.error("[desktop-recovery] renderer error boundary", error, errorInfo.componentStack);
|
|
97
|
+
this.setState({
|
|
98
|
+
hasError: true,
|
|
99
|
+
error,
|
|
100
|
+
details: errorInfo.componentStack ?? undefined,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override render(): ReactNode {
|
|
105
|
+
if (this.state.hasError) {
|
|
106
|
+
return (
|
|
107
|
+
<DesktopFatalScreen
|
|
108
|
+
title="Gloomberb crashed"
|
|
109
|
+
error={this.state.error}
|
|
110
|
+
details={this.state.details}
|
|
111
|
+
source="react-error-boundary"
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return this.props.children;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
installElectrobunHttpFetchTransport,
|
|
16
16
|
} from "./http-fetch";
|
|
17
17
|
import { installElectrobunUpdateHost } from "./update-host";
|
|
18
|
+
import { DesktopFatalScreen, ElectrobunErrorBoundary } from "./fatal-screen";
|
|
18
19
|
import { WebInputHostProvider } from "./input-host";
|
|
19
20
|
import { webNativeRenderer } from "./native-renderer";
|
|
20
21
|
import { WebToastHostProvider } from "./toast-host";
|
|
@@ -27,16 +28,32 @@ const rootElement = document.getElementById("root");
|
|
|
27
28
|
if (!rootElement) {
|
|
28
29
|
throw new Error("Missing root element");
|
|
29
30
|
}
|
|
31
|
+
const appRootElement = rootElement;
|
|
30
32
|
|
|
31
|
-
const root = createRoot(
|
|
33
|
+
const root = createRoot(appRootElement);
|
|
32
34
|
const bootLog = debugLog.createLogger("electrobun-web-boot");
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
appRootElement.tabIndex = -1;
|
|
35
37
|
root.render(<div className="gloom-loading">Starting Gloomberb...</div>);
|
|
36
38
|
|
|
39
|
+
function renderFatalError(error: unknown, details?: string, title = "Gloomberb failed to start"): void {
|
|
40
|
+
root.render(
|
|
41
|
+
<DesktopFatalScreen
|
|
42
|
+
title={title}
|
|
43
|
+
error={error}
|
|
44
|
+
details={details}
|
|
45
|
+
source="renderer-fatal"
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
window.__gloomRenderFatalError = (error, details) => {
|
|
51
|
+
renderFatalError(error, details, "Gloomberb crashed");
|
|
52
|
+
};
|
|
53
|
+
|
|
37
54
|
function focusWebSurface(): void {
|
|
38
55
|
window.focus();
|
|
39
|
-
|
|
56
|
+
appRootElement.focus({ preventScroll: true });
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
function requestStartupFocus(): void {
|
|
@@ -69,21 +86,23 @@ async function boot() {
|
|
|
69
86
|
const desktopApplicationMenuBridge = createApplicationMenuBridge();
|
|
70
87
|
measurePerfAsync("startup.electrobun.root-render", async () => {
|
|
71
88
|
root.render(
|
|
72
|
-
<
|
|
73
|
-
<
|
|
74
|
-
<
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
<ElectrobunErrorBoundary>
|
|
90
|
+
<UiHostProvider ui={webUiHost} renderer={webRendererHost} nativeRenderer={webNativeRenderer}>
|
|
91
|
+
<WebInputHostProvider>
|
|
92
|
+
<WebToastHostProvider>
|
|
93
|
+
<WebDialogHostProvider>
|
|
94
|
+
<App
|
|
95
|
+
config={config}
|
|
96
|
+
desktopWindowBridge={desktopWindowBridge}
|
|
97
|
+
desktopApplicationMenuBridge={desktopApplicationMenuBridge}
|
|
98
|
+
desktopSnapshot={desktopSnapshot}
|
|
99
|
+
desktopThemePreview={init.desktopThemePreview}
|
|
100
|
+
/>
|
|
101
|
+
</WebDialogHostProvider>
|
|
102
|
+
</WebToastHostProvider>
|
|
103
|
+
</WebInputHostProvider>
|
|
104
|
+
</UiHostProvider>
|
|
105
|
+
</ElectrobunErrorBoundary>,
|
|
87
106
|
);
|
|
88
107
|
});
|
|
89
108
|
requestStartupFocus();
|
|
@@ -96,10 +115,5 @@ async function boot() {
|
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
boot().catch((error) => {
|
|
99
|
-
|
|
100
|
-
<div className="gloom-fatal">
|
|
101
|
-
<h1>Gloomberb failed to start</h1>
|
|
102
|
-
<pre>{error instanceof Error ? error.stack ?? error.message : String(error)}</pre>
|
|
103
|
-
</div>,
|
|
104
|
-
);
|
|
118
|
+
renderFatalError(error);
|
|
105
119
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
@keyframes gloom-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
2
|
-
html, body, #root { width: 100%; height: 100%; min-height: 100%; overflow: hidden; background: var(--gloom-bg); }
|
|
2
|
+
html, body, #root { width: 100%; height: 100%; min-height: 100%; overflow: hidden; background: var(--gloom-bg, #272a34); }
|
|
3
3
|
#root:focus { outline: none; }
|
|
4
4
|
body {
|
|
5
5
|
--cell-w: 8px;
|
|
@@ -8,7 +8,7 @@ body {
|
|
|
8
8
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
9
9
|
font-size: 12px;
|
|
10
10
|
line-height: var(--cell-h);
|
|
11
|
-
color: var(--gloom-text);
|
|
11
|
+
color: var(--gloom-text, #f2f2f2);
|
|
12
12
|
cursor: default;
|
|
13
13
|
user-select: none;
|
|
14
14
|
color-scheme: dark;
|
|
@@ -284,8 +284,52 @@ body.gloom-dragging {
|
|
|
284
284
|
top: 50%;
|
|
285
285
|
height: 1px;
|
|
286
286
|
}
|
|
287
|
-
.gloom-loading, .gloom-fatal {
|
|
288
|
-
|
|
287
|
+
.gloom-loading, .gloom-fatal {
|
|
288
|
+
padding: 24px;
|
|
289
|
+
color: var(--gloom-text, #f2f2f2);
|
|
290
|
+
background: var(--gloom-bg, #272a34);
|
|
291
|
+
}
|
|
292
|
+
.gloom-fatal {
|
|
293
|
+
display: flex;
|
|
294
|
+
flex-direction: column;
|
|
295
|
+
gap: 14px;
|
|
296
|
+
overflow: auto;
|
|
297
|
+
}
|
|
298
|
+
.gloom-fatal h1 {
|
|
299
|
+
margin: 0 0 6px;
|
|
300
|
+
font-size: 28px;
|
|
301
|
+
line-height: 1.25;
|
|
302
|
+
}
|
|
303
|
+
.gloom-fatal-actions {
|
|
304
|
+
display: flex;
|
|
305
|
+
flex-wrap: wrap;
|
|
306
|
+
gap: 8px;
|
|
307
|
+
}
|
|
308
|
+
.gloom-fatal button {
|
|
309
|
+
border: 1px solid var(--gloom-border, #5b6070);
|
|
310
|
+
border-radius: 6px;
|
|
311
|
+
padding: 5px 10px;
|
|
312
|
+
color: var(--gloom-text, #f2f2f2);
|
|
313
|
+
background: var(--gloom-panel, #1f222b);
|
|
314
|
+
}
|
|
315
|
+
.gloom-fatal button:hover {
|
|
316
|
+
background: color-mix(in srgb, var(--gloom-text-bright, #fff) 10%, var(--gloom-panel, #1f222b));
|
|
317
|
+
}
|
|
318
|
+
.gloom-fatal button[data-variant="primary"] {
|
|
319
|
+
border-color: var(--gloom-border-focused, #8cb4ff);
|
|
320
|
+
color: var(--gloom-bg, #111318);
|
|
321
|
+
background: var(--gloom-border-focused, #8cb4ff);
|
|
322
|
+
}
|
|
323
|
+
.gloom-fatal-status {
|
|
324
|
+
min-height: var(--cell-h);
|
|
325
|
+
color: var(--gloom-text-dim, #a7acba);
|
|
326
|
+
}
|
|
327
|
+
.gloom-fatal pre {
|
|
328
|
+
margin: 0;
|
|
329
|
+
white-space: pre-wrap;
|
|
330
|
+
color: var(--gloom-negative, #ff5f5f);
|
|
331
|
+
user-select: text;
|
|
332
|
+
}
|
|
289
333
|
.gloom-toast-viewport {
|
|
290
334
|
position: fixed;
|
|
291
335
|
right: 16px;
|
|
@@ -20,6 +20,7 @@ const originalGetCloudQuote = apiClient.getCloudQuote.bind(apiClient);
|
|
|
20
20
|
const originalGetCloudHolders = apiClient.getCloudHolders.bind(apiClient);
|
|
21
21
|
const originalGetCloudAnalystResearch = apiClient.getCloudAnalystResearch.bind(apiClient);
|
|
22
22
|
const originalGetCloudCorporateActions = apiClient.getCloudCorporateActions.bind(apiClient);
|
|
23
|
+
const originalGetCloudOptionsChain = apiClient.getCloudOptionsChain.bind(apiClient);
|
|
23
24
|
const originalGetCloudNews = apiClient.getCloudNews.bind(apiClient);
|
|
24
25
|
const originalGetCloudNewsStory = apiClient.getCloudNewsStory.bind(apiClient);
|
|
25
26
|
const originalSubscribeQuotes = apiClient.subscribeQuotes.bind(apiClient);
|
|
@@ -78,6 +79,7 @@ afterEach(() => {
|
|
|
78
79
|
apiClient.getCloudHolders = originalGetCloudHolders;
|
|
79
80
|
apiClient.getCloudAnalystResearch = originalGetCloudAnalystResearch;
|
|
80
81
|
apiClient.getCloudCorporateActions = originalGetCloudCorporateActions;
|
|
82
|
+
apiClient.getCloudOptionsChain = originalGetCloudOptionsChain;
|
|
81
83
|
apiClient.getCloudNews = originalGetCloudNews;
|
|
82
84
|
apiClient.getCloudNewsStory = originalGetCloudNewsStory;
|
|
83
85
|
apiClient.subscribeQuotes = originalSubscribeQuotes;
|
|
@@ -103,8 +105,8 @@ describe("GloomberbCloudProvider", () => {
|
|
|
103
105
|
const history = await provider.getDetailedPriceHistory(
|
|
104
106
|
"AAPL",
|
|
105
107
|
"NASDAQ",
|
|
106
|
-
new Date(2026
|
|
107
|
-
new Date(2026
|
|
108
|
+
new Date("2026-03-27T14:00:00Z"),
|
|
109
|
+
new Date("2026-03-27T16:00:00Z"),
|
|
108
110
|
"15m",
|
|
109
111
|
);
|
|
110
112
|
|
|
@@ -119,6 +121,7 @@ describe("GloomberbCloudProvider", () => {
|
|
|
119
121
|
});
|
|
120
122
|
expect(history).toHaveLength(1);
|
|
121
123
|
expect(history[0]?.close).toBe(250.12);
|
|
124
|
+
expect(history[0]?.date.toISOString()).toBe("2026-03-27T14:15:00.000Z");
|
|
122
125
|
});
|
|
123
126
|
|
|
124
127
|
test("normalizes daily detailed history requests to 1day", async () => {
|
|
@@ -173,6 +176,11 @@ describe("GloomberbCloudProvider", () => {
|
|
|
173
176
|
expect(history[0]?.close).toBe(250.12);
|
|
174
177
|
});
|
|
175
178
|
|
|
179
|
+
test("does not advertise unsupported 45 minute chart resolution", () => {
|
|
180
|
+
const provider = new GloomberbCloudProvider();
|
|
181
|
+
expect(provider.getChartResolutionCapabilities()).not.toContain("45m");
|
|
182
|
+
});
|
|
183
|
+
|
|
176
184
|
test("normalizes sub-unit cloud quotes to their main currency", async () => {
|
|
177
185
|
apiClient.ensureVerifiedSession = async () => verifiedUser;
|
|
178
186
|
apiClient.getCloudQuote = async () => ({
|
|
@@ -319,6 +327,51 @@ describe("GloomberbCloudProvider", () => {
|
|
|
319
327
|
expect(actions.earnings[0]?.surprisePercent).toBe(6.37);
|
|
320
328
|
});
|
|
321
329
|
|
|
330
|
+
test("fetches options chains from the cloud market endpoint", async () => {
|
|
331
|
+
apiClient.ensureVerifiedSession = async () => verifiedUser;
|
|
332
|
+
|
|
333
|
+
const requestArgs: { current: { symbol: string; exchange?: string; expirationDate?: number } | null } = { current: null };
|
|
334
|
+
apiClient.getCloudOptionsChain = async (symbol, exchange, expirationDate) => {
|
|
335
|
+
requestArgs.current = { symbol, exchange, expirationDate };
|
|
336
|
+
return {
|
|
337
|
+
status: "success",
|
|
338
|
+
data: {
|
|
339
|
+
providerId: "gloomberb-cloud",
|
|
340
|
+
underlyingSymbol: "AMD",
|
|
341
|
+
expirationDates: [1_800_000_000],
|
|
342
|
+
calls: [{
|
|
343
|
+
contractSymbol: "AMD270917C00230000",
|
|
344
|
+
strike: 230,
|
|
345
|
+
currency: "USD",
|
|
346
|
+
lastPrice: 11,
|
|
347
|
+
change: 1,
|
|
348
|
+
percentChange: 10,
|
|
349
|
+
volume: 10,
|
|
350
|
+
openInterest: 20,
|
|
351
|
+
bid: 10,
|
|
352
|
+
ask: 12,
|
|
353
|
+
impliedVolatility: 0.4,
|
|
354
|
+
inTheMoney: false,
|
|
355
|
+
expiration: 1_800_000_000,
|
|
356
|
+
lastTradeDate: 1_799_000_000,
|
|
357
|
+
}],
|
|
358
|
+
puts: [],
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const provider = new GloomberbCloudProvider();
|
|
364
|
+
const chain = await provider.getOptionsChain("AMD", "NASDAQ", 1_800_000_000);
|
|
365
|
+
|
|
366
|
+
expect(requestArgs.current).toEqual({
|
|
367
|
+
symbol: "AMD",
|
|
368
|
+
exchange: "NASDAQ",
|
|
369
|
+
expirationDate: 1_800_000_000,
|
|
370
|
+
});
|
|
371
|
+
expect(chain.underlyingSymbol).toBe("AMD");
|
|
372
|
+
expect(chain.calls[0]?.contractSymbol).toBe("AMD270917C00230000");
|
|
373
|
+
});
|
|
374
|
+
|
|
322
375
|
test("preserves original target context when streaming quotes", () => {
|
|
323
376
|
let unsubscribeCalled = false;
|
|
324
377
|
const seenQuotes: Array<{ price: number; currency: string }> = [];
|