gloomberb 0.10.2 → 0.10.4
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 +3 -3
- package/README.zh-CN.md +2 -2
- package/package.json +2 -1
- package/src/api-client/auth.ts +8 -0
- package/src/api-client/index.ts +70 -0
- package/src/api-client/types.ts +49 -0
- package/src/app/runtime/desktop-deeplink.test.ts +8 -0
- package/src/app/runtime/desktop-deeplink.ts +14 -1
- package/src/components/chart/composite/composite-chart.test.tsx +340 -17
- package/src/components/chart/composite/composite-chart.tsx +859 -68
- package/src/components/chart/composite/interactions.test.ts +78 -22
- package/src/components/chart/composite/interactions.ts +45 -17
- package/src/components/chart/composite/rasterizer.ts +8 -2
- package/src/components/chart/composite/renderers.test.ts +61 -1
- package/src/components/chart/composite/scene.ts +14 -4
- package/src/components/chart/composite/tools.test.ts +371 -0
- package/src/components/chart/composite/tools.ts +508 -0
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart/native/raster/primitives.ts +18 -0
- package/src/components/chart/price-axis-labels.tsx +42 -2
- package/src/components/chart/static/chart/axis-overlays.test.tsx +2 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +46 -0
- package/src/components/command-bar/action-runtime.ts +3 -0
- package/src/components/command-bar/assist/inventory.test.ts +93 -0
- package/src/components/command-bar/assist/inventory.ts +92 -0
- package/src/components/command-bar/assist/model.test.ts +104 -0
- package/src/components/command-bar/assist/model.ts +169 -0
- package/src/components/command-bar/assist/runtime.ts +185 -0
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/keyboard-shortcuts.ts +5 -0
- package/src/components/command-bar/list/model.ts +15 -2
- package/src/components/command-bar/list/navigation.ts +6 -0
- package/src/components/command-bar/list/view.tsx +16 -3
- package/src/components/command-bar/pane-templates/workflow.ts +1 -0
- package/src/components/command-bar/panel/body.tsx +2 -0
- package/src/components/command-bar/panel/index.tsx +3 -0
- package/src/components/command-bar/panel/runtime.ts +6 -0
- package/src/components/command-bar/panel/types.ts +1 -0
- package/src/components/command-bar/routes/root/results.ts +28 -2
- package/src/components/command-bar/routes/root/runtime.ts +66 -36
- package/src/components/command-bar/routes/root/shortcuts.ts +8 -4
- package/src/components/command-bar/routing/navigation-state.ts +17 -0
- package/src/components/command-bar/selection-runtime.ts +26 -0
- package/src/components/command-bar/surface/assist.test.tsx +276 -0
- package/src/components/command-bar/surface/index.tsx +93 -2
- package/src/components/command-bar/surface/portfolio.test.tsx +2 -1
- package/src/components/command-bar/view-model.ts +7 -1
- package/src/components/command-bar/workflow/broker.test.ts +22 -0
- package/src/components/command-bar/workflow/broker.ts +1 -0
- package/src/components/index.ts +1 -1
- package/src/components/layout/detached-pane-shell.tsx +40 -2
- package/src/components/layout/floating-pane.tsx +4 -1
- package/src/components/layout/pane/header.tsx +64 -6
- package/src/components/layout/pane/index.tsx +4 -1
- package/src/components/layout/shell/index.tsx +23 -0
- package/src/components/layout/shell/pane/layers.tsx +5 -0
- package/src/components/onboarding/account-step/chooser-panel.tsx +74 -0
- package/src/components/onboarding/account-step/form-panel.tsx +157 -0
- package/src/components/onboarding/account-step/index.tsx +58 -0
- package/src/components/onboarding/account-step/model.test.ts +67 -0
- package/src/components/onboarding/account-step/model.ts +126 -0
- package/src/components/onboarding/account-step/signed-in-panel.tsx +31 -0
- package/src/components/onboarding/onboarding-steps.tsx +91 -55
- package/src/components/onboarding/onboarding-wizard.test.tsx +73 -6
- package/src/components/onboarding/onboarding-wizard.tsx +93 -9
- package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +3 -3
- package/src/components/onboarding/wizard-account.ts +235 -0
- package/src/components/onboarding/wizard-keyboard.ts +67 -1
- package/src/components/onboarding/wizard-model.ts +2 -2
- package/src/components/ui/data-table/index.tsx +1 -0
- package/src/components/ui/data-table/opentui/index.tsx +44 -2
- package/src/components/ui/data-table/types.ts +10 -0
- package/src/components/ui/data-table/visible-range.test.ts +29 -0
- package/src/components/ui/data-table/visible-range.ts +21 -0
- package/src/components/ui/ui.test.tsx +23 -1
- package/src/i18n/es.ts +1093 -0
- package/src/i18n/index.test.ts +5 -2
- package/src/i18n/index.ts +3 -0
- package/src/i18n/ja.ts +66 -3
- package/src/i18n/ko.ts +66 -3
- package/src/i18n/languages.test.ts +4 -1
- package/src/i18n/languages.ts +12 -1
- package/src/i18n/zh-cn.ts +66 -3
- package/src/i18n/zh-tw.ts +66 -3
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/coordinator/quotes.ts +64 -1
- package/src/market-data/coordinator/subscriptions.test.ts +72 -0
- package/src/market-data/quotes/cadence.ts +2 -0
- package/src/plugins/builtin/account-management/footer.ts +9 -3
- package/src/plugins/builtin/account-management/model.ts +53 -1
- package/src/plugins/builtin/account-management/pane.tsx +107 -24
- package/src/plugins/builtin/ai/index.tsx +7 -2
- package/src/plugins/builtin/ai/screener/market-runtime.ts +4 -2
- package/src/plugins/builtin/ai/screener/pane.tsx +3 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +0 -1
- package/src/plugins/builtin/chart-composer/index.tsx +10 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +111 -44
- package/src/plugins/builtin/chart-composer/quick-add.tsx +27 -3
- package/src/plugins/builtin/chart-composer/semantic.ts +21 -0
- package/src/plugins/builtin/chat/controller/persistence.ts +5 -0
- package/src/plugins/builtin/cloud/plugin.tsx +9 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +28 -0
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +54 -0
- package/src/plugins/builtin/help/index.tsx +38 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +18 -38
- package/src/plugins/builtin/options/footer.test.ts +20 -112
- package/src/plugins/builtin/options/footer.ts +35 -78
- package/src/plugins/builtin/options/index.tsx +6 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +117 -14
- package/src/plugins/builtin/options/live-quotes.ts +93 -39
- package/src/plugins/builtin/options/view.test.tsx +5 -2
- package/src/plugins/builtin/options/view.tsx +49 -9
- package/src/plugins/builtin/portfolio-list/index.tsx +12 -4
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +17 -13
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.test.tsx +76 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.ts +78 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +131 -0
- package/src/plugins/builtin/shared/live-streaming.ts +60 -0
- package/src/plugins/builtin/shared/plan-access.test.ts +69 -0
- package/src/plugins/builtin/shared/plan-access.ts +109 -0
- package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +11 -2
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -3
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +4 -2
- package/src/plugins/registry/index.test.ts +51 -0
- package/src/plugins/registry/index.ts +12 -0
- package/src/plugins/registry/pane-settings.ts +29 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +44 -3
- package/src/renderers/electrobun/view/host/box.test.tsx +97 -0
- package/src/renderers/electrobun/view/host/box.tsx +3 -0
- package/src/renderers/electrobun/view/host/chart-surface.tsx +85 -1
- package/src/renderers/electrobun/view/host/focus-scope.test.ts +37 -0
- package/src/renderers/electrobun/view/host/focus-scope.ts +56 -0
- package/src/renderers/electrobun/view/host/input.tsx +16 -1
- package/src/renderers/electrobun/view/host/mouse.ts +1 -0
- package/src/renderers/electrobun/view/host/style.ts +1 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +4 -2
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/opentui/chart-surface.tsx +2 -0
- package/src/renderers/opentui/host.tsx +3 -0
- package/src/sources/gloomberb-cloud/index.test.ts +8 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -4
- package/src/sources/provider-router/history.test.ts +3 -3
- package/src/sources/provider-router/history.ts +15 -4
- package/src/sources/yahoo-finance/history.test.ts +47 -0
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/state/hooks/quote-streaming.test.tsx +181 -1
- package/src/state/hooks/quote-streaming.ts +93 -7
- package/src/theme/colors.ts +20 -1
- package/src/theme/themes.test.ts +3 -0
- package/src/{sources/gloomberb-cloud → time-series}/history-quality.ts +27 -3
- package/src/time-series/resolution.test.ts +51 -0
- package/src/time-series/resolution.ts +41 -0
- package/src/time-series/resolve.test.ts +207 -1
- package/src/time-series/resolve.ts +196 -29
- package/src/time-series/spec-studies.test.ts +4 -0
- package/src/time-series/spec.ts +15 -1
- package/src/time-series/types.ts +7 -0
- package/src/time-series/use-chart-resolution.test.tsx +37 -0
- package/src/time-series/use-chart-resolution.ts +17 -1
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +17 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
2
|
import type { PaneFooterSegment } from "../../../../../components";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { t, tf } from "../../../../../i18n";
|
|
4
|
+
import { useAppLanguage } from "../../../../../i18n/react";
|
|
5
|
+
import { useCloudAccessFooter } from "../../../shared/cloud-upgrade";
|
|
6
|
+
import { CLOUD_NEWS_DELAY_HOURS } from "../../../shared/plan-access";
|
|
6
7
|
import { usePaneStatusLinkFooter } from "../../../shared/pane-footer";
|
|
7
|
-
import { CLOUD_UPGRADE_URL } from "../../../shared/cloud-upgrade";
|
|
8
8
|
|
|
9
9
|
interface NewsFooterArticle {
|
|
10
10
|
source?: string | null;
|
|
@@ -20,11 +20,6 @@ interface UseNewsArticleFooterOptions {
|
|
|
20
20
|
error?: string | null;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function hasRealtimeNewsAccess(): boolean {
|
|
24
|
-
const user = apiClient.getCurrentUser();
|
|
25
|
-
return user?.emailVerified === true && user.plan === "pro";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
23
|
export function useNewsArticleFooter({
|
|
29
24
|
registrationId,
|
|
30
25
|
focused,
|
|
@@ -33,35 +28,20 @@ export function useNewsArticleFooter({
|
|
|
33
28
|
loading = false,
|
|
34
29
|
error,
|
|
35
30
|
}: UseNewsArticleFooterOptions) {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
useShortcut((event) => {
|
|
44
|
-
const key = (event.name ?? event.key ?? "").toLowerCase();
|
|
45
|
-
if (!focused || !showAccessFooter || hasRealtimeAccess || key !== "u") return;
|
|
46
|
-
event.stopPropagation();
|
|
47
|
-
event.preventDefault();
|
|
48
|
-
openUpgrade();
|
|
49
|
-
}, { scope: `${registrationId}:news-upgrade` });
|
|
31
|
+
const language = useAppLanguage();
|
|
32
|
+
const { access, segment } = useCloudAccessFooter({
|
|
33
|
+
delayLabel: tf("{count}h", { count: CLOUD_NEWS_DELAY_HOURS }),
|
|
34
|
+
focused,
|
|
35
|
+
segmentId: "news-access",
|
|
36
|
+
shortcutScope: `${registrationId}:news-upgrade`,
|
|
37
|
+
});
|
|
50
38
|
|
|
51
|
-
const accessInfo = useMemo<PaneFooterSegment[]>(() =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
parts: [{ text: "realtime news", tone: "positive" }],
|
|
58
|
-
}]
|
|
59
|
-
: [{
|
|
60
|
-
id: "news-access",
|
|
61
|
-
onPress: openUpgrade,
|
|
62
|
-
parts: [{ text: "delayed 12h, upgrade for realtime", tone: "warning" }],
|
|
63
|
-
}]
|
|
64
|
-
), [hasRealtimeAccess, openUpgrade, showAccessFooter]);
|
|
39
|
+
const accessInfo = useMemo<PaneFooterSegment[]>(() => {
|
|
40
|
+
if (access.isPayingPro) {
|
|
41
|
+
return [{ id: "news-access", parts: [{ text: t("real-time news"), tone: "positive" }] }];
|
|
42
|
+
}
|
|
43
|
+
return segment ? [segment] : [];
|
|
44
|
+
}, [access.isPayingPro, language, segment]);
|
|
65
45
|
const footerInfo = useMemo(() => [...accessInfo, ...(info ?? [])], [accessInfo, info]);
|
|
66
46
|
|
|
67
47
|
usePaneStatusLinkFooter({
|
|
@@ -1,115 +1,23 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}),
|
|
23
|
-
clientPlan: "free",
|
|
24
|
-
hasLiveQuote: false,
|
|
25
|
-
});
|
|
26
|
-
let upgrades = 0;
|
|
27
|
-
const segment = buildOptionsAccessFooterSegment(state, () => {
|
|
28
|
-
upgrades += 1;
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
expect(state).toEqual({
|
|
32
|
-
canUpgrade: true,
|
|
33
|
-
text: "15-minute delayed options, upgrade for real-time",
|
|
34
|
-
tone: "warning",
|
|
35
|
-
});
|
|
36
|
-
segment.onPress?.();
|
|
37
|
-
expect(upgrades).toBe(1);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("claims real-time only for a Pro user with a fresh live stream", () => {
|
|
41
|
-
expect(
|
|
42
|
-
resolveOptionsAccessFooterState({
|
|
43
|
-
chain: chain({
|
|
44
|
-
dataSource: "delayed",
|
|
45
|
-
delayMinutes: 15,
|
|
46
|
-
realtimeEligible: true,
|
|
47
|
-
}),
|
|
48
|
-
clientPlan: "pro",
|
|
49
|
-
hasLiveQuote: false,
|
|
50
|
-
}),
|
|
51
|
-
).toMatchObject({
|
|
52
|
-
canUpgrade: false,
|
|
53
|
-
text: "options delayed fallback",
|
|
54
|
-
tone: "warning",
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
expect(
|
|
58
|
-
resolveOptionsAccessFooterState({
|
|
59
|
-
chain: chain({
|
|
60
|
-
dataSource: "live",
|
|
61
|
-
delayMinutes: 0,
|
|
62
|
-
realtimeEligible: true,
|
|
63
|
-
}),
|
|
64
|
-
clientPlan: "pro",
|
|
65
|
-
hasLiveQuote: false,
|
|
66
|
-
}),
|
|
67
|
-
).toMatchObject({
|
|
68
|
-
canUpgrade: false,
|
|
69
|
-
text: "options delayed fallback",
|
|
70
|
-
tone: "warning",
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
expect(
|
|
74
|
-
resolveOptionsAccessFooterState({
|
|
75
|
-
chain: chain({ realtimeEligible: false }),
|
|
76
|
-
clientPlan: "pro",
|
|
77
|
-
hasLiveQuote: true,
|
|
78
|
-
}).text,
|
|
79
|
-
).toBe("real-time options");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("makes the current plan authoritative over cached chain metadata", () => {
|
|
83
|
-
expect(
|
|
84
|
-
resolveOptionsAccessFooterState({
|
|
85
|
-
chain: chain({
|
|
86
|
-
dataSource: "live",
|
|
87
|
-
delayMinutes: 0,
|
|
88
|
-
realtimeEligible: true,
|
|
89
|
-
}),
|
|
90
|
-
clientPlan: "free",
|
|
91
|
-
hasLiveQuote: true,
|
|
92
|
-
}),
|
|
93
|
-
).toMatchObject({
|
|
94
|
-
canUpgrade: true,
|
|
95
|
-
text: "15-minute delayed options, upgrade for real-time",
|
|
96
|
-
tone: "warning",
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
expect(
|
|
100
|
-
resolveOptionsAccessFooterState({
|
|
101
|
-
chain: chain({
|
|
102
|
-
dataSource: "delayed",
|
|
103
|
-
delayMinutes: 15,
|
|
104
|
-
realtimeEligible: false,
|
|
105
|
-
}),
|
|
106
|
-
clientPlan: "pro",
|
|
107
|
-
hasLiveQuote: true,
|
|
108
|
-
}),
|
|
109
|
-
).toMatchObject({
|
|
110
|
-
canUpgrade: false,
|
|
111
|
-
text: "real-time options",
|
|
112
|
-
tone: "positive",
|
|
113
|
-
});
|
|
2
|
+
import { resolveOptionsDelayLabel } from "./footer";
|
|
3
|
+
|
|
4
|
+
describe("options delay label", () => {
|
|
5
|
+
test("prefers the chain delay and falls back to the cloud default", () => {
|
|
6
|
+
expect(resolveOptionsDelayLabel({
|
|
7
|
+
underlyingSymbol: "AAPL",
|
|
8
|
+
expirationDates: [],
|
|
9
|
+
calls: [],
|
|
10
|
+
puts: [],
|
|
11
|
+
delayMinutes: 20,
|
|
12
|
+
})).toBe("20m");
|
|
13
|
+
// A live chain reports 0, and a cached chain may omit the field entirely.
|
|
14
|
+
expect(resolveOptionsDelayLabel({
|
|
15
|
+
underlyingSymbol: "AAPL",
|
|
16
|
+
expirationDates: [],
|
|
17
|
+
calls: [],
|
|
18
|
+
puts: [],
|
|
19
|
+
delayMinutes: 0,
|
|
20
|
+
})).toBe("15m");
|
|
21
|
+
expect(resolveOptionsDelayLabel(null)).toBe("15m");
|
|
114
22
|
});
|
|
115
23
|
});
|
|
@@ -1,110 +1,67 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
2
|
import type { PaneFooterSegment } from "../../../components";
|
|
3
|
-
import { apiClient } from "../../../api-client";
|
|
4
3
|
import { t, tf } from "../../../i18n";
|
|
5
|
-
import { useShortcut } from "../../../react/input";
|
|
6
4
|
import type { OptionsChain } from "../../../types/financials";
|
|
7
|
-
import {
|
|
8
|
-
import { CLOUD_UPGRADE_URL } from "../shared/cloud-upgrade";
|
|
5
|
+
import { useCloudAccessFooter } from "../shared/cloud-upgrade";
|
|
9
6
|
import { usePaneStatusFooter } from "../shared/pane-footer";
|
|
7
|
+
import { CLOUD_QUOTE_DELAY_MINUTES } from "../shared/plan-access";
|
|
8
|
+
import type { OptionQuoteCoverage, OptionQuoteCoverageStatus } from "./live-quotes";
|
|
10
9
|
|
|
11
|
-
export interface
|
|
12
|
-
canUpgrade: boolean;
|
|
10
|
+
export interface OptionsCoverageState {
|
|
13
11
|
text: string;
|
|
14
|
-
tone: "positive" | "warning";
|
|
12
|
+
tone: "muted" | "positive" | "warning";
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
clientPlan: "free" | "pro" | null;
|
|
24
|
-
hasLiveQuote: boolean;
|
|
25
|
-
}): OptionsAccessFooterState {
|
|
26
|
-
if (clientPlan !== "pro") {
|
|
27
|
-
const delayMinutes = chain?.delayMinutes && chain.delayMinutes > 0 ? chain.delayMinutes : 15;
|
|
28
|
-
return {
|
|
29
|
-
canUpgrade: true,
|
|
30
|
-
text: tf("{count}-minute delayed options, upgrade for real-time", {
|
|
31
|
-
count: delayMinutes,
|
|
32
|
-
}),
|
|
33
|
-
tone: "warning",
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (hasLiveQuote) {
|
|
38
|
-
return {
|
|
39
|
-
canUpgrade: false,
|
|
40
|
-
text: t("real-time options"),
|
|
41
|
-
tone: "positive",
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
canUpgrade: false,
|
|
47
|
-
text: t("options delayed fallback"),
|
|
48
|
-
tone: "warning",
|
|
49
|
-
};
|
|
15
|
+
/** Stream coverage the pane reports once the account is entitled to real-time options. */
|
|
16
|
+
export function resolveOptionsCoverageState(status: OptionQuoteCoverageStatus): OptionsCoverageState {
|
|
17
|
+
if (status === "live") return { text: t("real-time options"), tone: "positive" };
|
|
18
|
+
if (status === "mixed") return { text: t("mixed real-time and delayed options"), tone: "warning" };
|
|
19
|
+
if (status === "connecting") return { text: t("connecting real-time options"), tone: "muted" };
|
|
20
|
+
return { text: t("options delayed fallback"), tone: "warning" };
|
|
50
21
|
}
|
|
51
22
|
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
id: "options-access",
|
|
58
|
-
...(state.canUpgrade ? { onPress: openUpgrade } : {}),
|
|
59
|
-
parts: [{ text: state.text, tone: state.tone }],
|
|
60
|
-
};
|
|
23
|
+
export function resolveOptionsDelayLabel(chain: OptionsChain | null | undefined): string {
|
|
24
|
+
const delayMinutes = chain?.delayMinutes && chain.delayMinutes > 0
|
|
25
|
+
? chain.delayMinutes
|
|
26
|
+
: CLOUD_QUOTE_DELAY_MINUTES;
|
|
27
|
+
return tf("{count}m", { count: delayMinutes });
|
|
61
28
|
}
|
|
62
29
|
|
|
63
30
|
export function useOptionsAccessFooter({
|
|
64
31
|
chain,
|
|
65
32
|
error,
|
|
66
33
|
focused,
|
|
67
|
-
hasLiveQuote,
|
|
68
34
|
loading,
|
|
35
|
+
quoteCoverage,
|
|
69
36
|
}: {
|
|
70
37
|
chain: OptionsChain | null | undefined;
|
|
71
38
|
error?: string | null;
|
|
72
39
|
focused: boolean;
|
|
73
|
-
hasLiveQuote: boolean;
|
|
74
40
|
loading?: boolean;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
hasLiveQuote,
|
|
41
|
+
quoteCoverage: Pick<OptionQuoteCoverage, "status">;
|
|
42
|
+
}): void {
|
|
43
|
+
const { access, segment } = useCloudAccessFooter({
|
|
44
|
+
delayLabel: resolveOptionsDelayLabel(chain),
|
|
45
|
+
focused,
|
|
46
|
+
segmentId: "options-access",
|
|
47
|
+
shortcutScope: "options:upgrade",
|
|
83
48
|
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
49
|
+
// Trial accounts stream real-time too, but the countdown is the status worth the row.
|
|
50
|
+
const coverage = access.hasProAccess && !access.isTrialActive
|
|
51
|
+
? resolveOptionsCoverageState(quoteCoverage.status)
|
|
52
|
+
: null;
|
|
87
53
|
|
|
88
|
-
|
|
89
|
-
(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
openUpgrade();
|
|
95
|
-
},
|
|
96
|
-
{ scope: "options:upgrade" },
|
|
97
|
-
);
|
|
54
|
+
const info = useMemo<PaneFooterSegment[]>(() => {
|
|
55
|
+
if (coverage) {
|
|
56
|
+
return [{ id: "options-access", parts: [{ text: coverage.text, tone: coverage.tone }] }];
|
|
57
|
+
}
|
|
58
|
+
return segment ? [segment] : [];
|
|
59
|
+
}, [coverage?.text, coverage?.tone, segment]);
|
|
98
60
|
|
|
99
|
-
const info = useMemo(
|
|
100
|
-
() => [buildOptionsAccessFooterSegment(state, openUpgrade)],
|
|
101
|
-
[openUpgrade, state.canUpgrade, state.text, state.tone],
|
|
102
|
-
);
|
|
103
61
|
usePaneStatusFooter({
|
|
104
62
|
registrationId: "options",
|
|
105
63
|
loading,
|
|
106
64
|
error,
|
|
107
65
|
info,
|
|
108
66
|
});
|
|
109
|
-
return state;
|
|
110
67
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
2
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
3
3
|
import { OptionsView } from "./view";
|
|
4
|
+
import {
|
|
5
|
+
LIVE_STREAMING_QUICK_SETTING,
|
|
6
|
+
withLiveStreamingSetting,
|
|
7
|
+
} from "../shared/live-streaming";
|
|
4
8
|
|
|
5
9
|
export const optionsModule: PluginModule = {
|
|
6
10
|
panes: [
|
|
@@ -12,6 +16,8 @@ export const optionsModule: PluginModule = {
|
|
|
12
16
|
defaultPosition: "right",
|
|
13
17
|
defaultMode: "floating",
|
|
14
18
|
defaultFloatingSize: { width: 112, height: 28 },
|
|
19
|
+
quickSettings: [LIVE_STREAMING_QUICK_SETTING],
|
|
20
|
+
settings: (context) => withLiveStreamingSetting({ fields: [] }, context.settings),
|
|
15
21
|
},
|
|
16
22
|
],
|
|
17
23
|
|
|
@@ -5,8 +5,8 @@ import type { OptionTableRow } from "./types";
|
|
|
5
5
|
import {
|
|
6
6
|
buildOptionQuoteKey,
|
|
7
7
|
buildOptionQuoteTargets,
|
|
8
|
-
hasLiveOptionQuote,
|
|
9
8
|
overlayOptionRowQuotes,
|
|
9
|
+
resolveOptionQuoteCoverage,
|
|
10
10
|
} from "./live-quotes";
|
|
11
11
|
|
|
12
12
|
function contract(strike: number, side: "C" | "P"): OptionContract {
|
|
@@ -51,22 +51,92 @@ function readyQuote(quote: Quote): QueryEntry<Quote> {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
describe("options live quotes", () => {
|
|
54
|
-
test("
|
|
54
|
+
test("subscribes every visible contract plus non-visible overscan", () => {
|
|
55
55
|
const rows = Array.from({ length: 100 }, (_, index) => row(50 + index));
|
|
56
|
-
const targets = buildOptionQuoteTargets(rows,
|
|
56
|
+
const targets = buildOptionQuoteTargets(rows, {
|
|
57
|
+
fallbackHeight: 14,
|
|
58
|
+
selectedIndex: 50,
|
|
59
|
+
visibleRange: { start: 40, end: 64 },
|
|
60
|
+
});
|
|
61
|
+
const visibleTargets = targets.filter((target) => target.visible === true);
|
|
57
62
|
|
|
58
|
-
expect(targets).toHaveLength(
|
|
63
|
+
expect(targets).toHaveLength(64);
|
|
64
|
+
expect(visibleTargets).toHaveLength(48);
|
|
59
65
|
expect(
|
|
60
66
|
targets.every(
|
|
61
|
-
(target) => target.exchange === "OPTIONS" && target.surface === "options"
|
|
67
|
+
(target) => target.exchange === "OPTIONS" && target.surface === "options",
|
|
62
68
|
),
|
|
63
69
|
).toBe(true);
|
|
64
70
|
expect(targets.filter((target) => target.selected)).toHaveLength(2);
|
|
65
71
|
expect(targets.some((target) => target.symbol === rows[50]!.call!.contractSymbol)).toBe(true);
|
|
72
|
+
expect(visibleTargets.some((target) => target.symbol === rows[40]!.call!.contractSymbol)).toBe(true);
|
|
73
|
+
expect(visibleTargets.some((target) => target.symbol === rows[63]!.put!.contractSymbol)).toBe(true);
|
|
74
|
+
expect(targets.find((target) => target.symbol === rows[36]!.call!.contractSymbol)?.visible).toBe(false);
|
|
66
75
|
expect(targets.some((target) => target.symbol === rows[0]!.call!.contractSymbol)).toBe(false);
|
|
67
76
|
expect(targets.some((target) => target.symbol === rows.at(-1)!.put!.contractSymbol)).toBe(false);
|
|
68
77
|
});
|
|
69
78
|
|
|
79
|
+
test("requires fresh live stream metadata for every visible contract", () => {
|
|
80
|
+
const rows = [row(100), row(105), row(110)];
|
|
81
|
+
const targets = buildOptionQuoteTargets(rows, {
|
|
82
|
+
fallbackHeight: 14,
|
|
83
|
+
selectedIndex: 1,
|
|
84
|
+
visibleRange: { start: 1, end: 2 },
|
|
85
|
+
});
|
|
86
|
+
const freshness = {
|
|
87
|
+
chainAsOf: new Date(1_799_999_000_000).toISOString(),
|
|
88
|
+
chainDataSource: "live" as const,
|
|
89
|
+
now: 1_800_000_030_000,
|
|
90
|
+
subscriptionStartedAt: 1_799_999_500_000,
|
|
91
|
+
};
|
|
92
|
+
const visibleRow = rows[1]!;
|
|
93
|
+
const quote = (symbol: string): Quote => ({
|
|
94
|
+
symbol,
|
|
95
|
+
providerId: "gloomberb-cloud",
|
|
96
|
+
price: 2.4,
|
|
97
|
+
mark: 2.5,
|
|
98
|
+
bid: 2.45,
|
|
99
|
+
ask: 2.55,
|
|
100
|
+
currency: "USD",
|
|
101
|
+
change: 0,
|
|
102
|
+
changePercent: 0,
|
|
103
|
+
lastUpdated: 1_800_000_000_000,
|
|
104
|
+
receivedAt: 1_800_000_010_000,
|
|
105
|
+
dataSource: "live",
|
|
106
|
+
delivery: "stream",
|
|
107
|
+
stale: false,
|
|
108
|
+
});
|
|
109
|
+
const entries = new Map([
|
|
110
|
+
[buildOptionQuoteKey(visibleRow.call!.contractSymbol), readyQuote(quote(visibleRow.call!.contractSymbol))],
|
|
111
|
+
[buildOptionQuoteKey(visibleRow.put!.contractSymbol), readyQuote(quote(visibleRow.put!.contractSymbol))],
|
|
112
|
+
[buildOptionQuoteKey(rows[0]!.call!.contractSymbol), readyQuote(quote(rows[0]!.call!.contractSymbol))],
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
expect(resolveOptionQuoteCoverage(targets, entries, freshness)).toEqual({
|
|
116
|
+
fallbackCount: 0,
|
|
117
|
+
liveCount: 2,
|
|
118
|
+
status: "live",
|
|
119
|
+
totalCount: 2,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
entries.delete(buildOptionQuoteKey(visibleRow.put!.contractSymbol));
|
|
123
|
+
expect(resolveOptionQuoteCoverage(targets, entries, freshness)).toMatchObject({
|
|
124
|
+
liveCount: 1,
|
|
125
|
+
status: "mixed",
|
|
126
|
+
totalCount: 2,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
entries.clear();
|
|
130
|
+
entries.set(
|
|
131
|
+
buildOptionQuoteKey(rows[0]!.call!.contractSymbol),
|
|
132
|
+
readyQuote(quote(rows[0]!.call!.contractSymbol)),
|
|
133
|
+
);
|
|
134
|
+
expect(resolveOptionQuoteCoverage(targets, entries, {
|
|
135
|
+
...freshness,
|
|
136
|
+
now: freshness.subscriptionStartedAt + 1_000,
|
|
137
|
+
})).toMatchObject({ liveCount: 0, status: "connecting", totalCount: 2 });
|
|
138
|
+
});
|
|
139
|
+
|
|
70
140
|
test("overlays streamed quote fields while preserving chain-only Greeks and open interest", () => {
|
|
71
141
|
const original = row(100);
|
|
72
142
|
const symbol = original.call!.contractSymbol;
|
|
@@ -103,10 +173,9 @@ describe("options live quotes", () => {
|
|
|
103
173
|
impliedVolatility: 0.25,
|
|
104
174
|
openInterest: 20,
|
|
105
175
|
});
|
|
106
|
-
expect(hasLiveOptionQuote(entries, freshness)).toBe(true);
|
|
107
176
|
});
|
|
108
177
|
|
|
109
|
-
test("rejects
|
|
178
|
+
test("rejects quotes from before this subscription without comparing contracts to a global chain timestamp", () => {
|
|
110
179
|
const original = row(100);
|
|
111
180
|
const symbol = original.call!.contractSymbol;
|
|
112
181
|
const quote: Quote = {
|
|
@@ -122,6 +191,8 @@ describe("options live quotes", () => {
|
|
|
122
191
|
lastUpdated: 1_800_000_000_000,
|
|
123
192
|
receivedAt: 1_800_000_010_000,
|
|
124
193
|
dataSource: "live",
|
|
194
|
+
delivery: "stream",
|
|
195
|
+
stale: false,
|
|
125
196
|
};
|
|
126
197
|
const entries = new Map([[buildOptionQuoteKey(symbol), readyQuote(quote)]]);
|
|
127
198
|
|
|
@@ -131,16 +202,33 @@ describe("options live quotes", () => {
|
|
|
131
202
|
subscriptionStartedAt: 1_800_000_020_000,
|
|
132
203
|
};
|
|
133
204
|
expect(overlayOptionRowQuotes([original], entries, beforeSubscription)[0]!.call!.lastPrice).toBe(1);
|
|
134
|
-
|
|
205
|
+
const targets = buildOptionQuoteTargets([original], {
|
|
206
|
+
fallbackHeight: 14,
|
|
207
|
+
selectedIndex: 0,
|
|
208
|
+
visibleRange: { start: 0, end: 1 },
|
|
209
|
+
});
|
|
210
|
+
expect(resolveOptionQuoteCoverage(targets, entries, beforeSubscription).status).not.toBe("live");
|
|
135
211
|
|
|
136
|
-
const
|
|
212
|
+
const heterogeneousChainSnapshot = {
|
|
137
213
|
chainAsOf: new Date(1_800_000_005_000).toISOString(),
|
|
138
214
|
chainDataSource: "live" as const,
|
|
139
215
|
now: 1_800_000_030_000,
|
|
140
216
|
subscriptionStartedAt: 1_800_000_000_000,
|
|
141
217
|
};
|
|
142
|
-
expect(overlayOptionRowQuotes([original], entries,
|
|
143
|
-
expect(
|
|
218
|
+
expect(overlayOptionRowQuotes([original], entries, heterogeneousChainSnapshot)[0]!.call!.lastPrice).toBe(9);
|
|
219
|
+
expect(resolveOptionQuoteCoverage(targets, entries, heterogeneousChainSnapshot)).toMatchObject({
|
|
220
|
+
liveCount: 1,
|
|
221
|
+
status: "mixed",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const newerContract = row(100);
|
|
225
|
+
newerContract.call = {
|
|
226
|
+
...newerContract.call!,
|
|
227
|
+
lastUpdated: 1_800_000_005_000,
|
|
228
|
+
};
|
|
229
|
+
expect(
|
|
230
|
+
overlayOptionRowQuotes([newerContract], entries, heterogeneousChainSnapshot)[0]!.call!.lastPrice,
|
|
231
|
+
).toBe(1);
|
|
144
232
|
});
|
|
145
233
|
|
|
146
234
|
test("accepts a freshly delivered delayed quote older than the delayed chain fetch", () => {
|
|
@@ -171,7 +259,12 @@ describe("options live quotes", () => {
|
|
|
171
259
|
};
|
|
172
260
|
|
|
173
261
|
expect(overlayOptionRowQuotes([original], entries, freshness)[0]!.call!.lastPrice).toBe(2.25);
|
|
174
|
-
|
|
262
|
+
const targets = buildOptionQuoteTargets([original], {
|
|
263
|
+
fallbackHeight: 14,
|
|
264
|
+
selectedIndex: 0,
|
|
265
|
+
visibleRange: { start: 0, end: 1 },
|
|
266
|
+
});
|
|
267
|
+
expect(resolveOptionQuoteCoverage(targets, entries, freshness).status).toBe("delayed");
|
|
175
268
|
});
|
|
176
269
|
|
|
177
270
|
test("overlays a valid polled quote without reporting a live stream", () => {
|
|
@@ -202,7 +295,12 @@ describe("options live quotes", () => {
|
|
|
202
295
|
};
|
|
203
296
|
|
|
204
297
|
expect(overlayOptionRowQuotes([original], entries, freshness)[0]!.call!.lastPrice).toBe(2.5);
|
|
205
|
-
|
|
298
|
+
const targets = buildOptionQuoteTargets([original], {
|
|
299
|
+
fallbackHeight: 14,
|
|
300
|
+
selectedIndex: 0,
|
|
301
|
+
visibleRange: { start: 0, end: 1 },
|
|
302
|
+
});
|
|
303
|
+
expect(resolveOptionQuoteCoverage(targets, entries, freshness).status).toBe("delayed");
|
|
206
304
|
});
|
|
207
305
|
|
|
208
306
|
test("rejects a freshly received quote marked stale by the server", () => {
|
|
@@ -233,6 +331,11 @@ describe("options live quotes", () => {
|
|
|
233
331
|
};
|
|
234
332
|
|
|
235
333
|
expect(overlayOptionRowQuotes([original], entries, freshness)[0]!.call!.lastPrice).toBe(1);
|
|
236
|
-
|
|
334
|
+
const targets = buildOptionQuoteTargets([original], {
|
|
335
|
+
fallbackHeight: 14,
|
|
336
|
+
selectedIndex: 0,
|
|
337
|
+
visibleRange: { start: 0, end: 1 },
|
|
338
|
+
});
|
|
339
|
+
expect(resolveOptionQuoteCoverage(targets, entries, freshness).status).toBe("delayed");
|
|
237
340
|
});
|
|
238
341
|
});
|