gloomberb 0.3.7 → 0.4.0
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 +19 -8
- package/package.json +1 -1
- package/src/app.tsx +29 -65
- package/src/cli/base-converter.ts +31 -0
- package/src/cli/commands/search.ts +11 -3
- package/src/cli/commands/ticker.ts +22 -11
- package/src/cli/context.ts +27 -32
- package/src/cli/helpers.ts +1 -4
- package/src/cli/index.ts +140 -123
- package/src/cli/registry.test.ts +226 -0
- package/src/cli/registry.ts +201 -0
- package/src/cli.test.ts +219 -0
- package/src/components/chart/chart-controller.ts +309 -0
- package/src/components/chart/chart-data.ts +5 -6
- package/src/components/chart/chart-pane-settings.ts +27 -0
- package/src/components/chart/chart-resolution.test.ts +56 -0
- package/src/components/chart/chart-resolution.ts +362 -0
- package/src/components/chart/chart-types.ts +12 -4
- package/src/components/chart/chart-viewport.test.ts +25 -0
- package/src/components/chart/chart-viewport.ts +28 -0
- package/src/components/chart/comparison-chart-data.ts +20 -27
- package/src/components/chart/comparison-stock-chart.tsx +374 -99
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
- package/src/components/chart/stock-chart.tsx +455 -106
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +650 -5
- package/src/components/command-bar/command-bar.tsx +359 -63
- package/src/components/command-bar/command-registry.ts +8 -0
- package/src/components/command-bar/helpers.test.ts +23 -1
- package/src/components/command-bar/helpers.ts +14 -5
- package/src/components/command-bar/pane-template-display.ts +12 -0
- package/src/components/command-bar/root-shortcuts.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +4 -2
- package/src/components/detail-feed-view.test.tsx +124 -49
- package/src/components/detail-feed-view.tsx +396 -160
- package/src/components/layout/floating-pane.tsx +1 -1
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/shell.tsx +62 -50
- package/src/components/layout/status-bar.test.tsx +66 -0
- package/src/components/layout/status-bar.tsx +26 -5
- package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
- package/src/components/onboarding/onboarding-wizard.tsx +5 -6
- package/src/components/ticker-list-table.test.tsx +83 -0
- package/src/components/ticker-list-table.tsx +174 -97
- package/src/components/ui/data-table.tsx +27 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/page-stack-view.tsx +60 -0
- package/src/components/use-double-click-activation.ts +31 -0
- package/src/index.tsx +10 -10
- package/src/market-data/coordinator.test.ts +92 -4
- package/src/market-data/coordinator.ts +32 -5
- package/src/market-data/hooks.test.tsx +120 -0
- package/src/market-data/hooks.tsx +53 -16
- package/src/market-data/request-types.ts +4 -2
- package/src/market-data/selectors.ts +24 -58
- package/src/plugins/builtin/chat-controller.test.ts +370 -3
- package/src/plugins/builtin/chat-controller.ts +310 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +175 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- package/src/plugins/builtin/news.tsx +1 -11
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
- package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
- package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
- package/src/plugins/builtin/sec.tsx +1 -11
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
- package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
- package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
- package/src/plugins/catalog.ts +65 -0
- package/src/plugins/ibkr/broker-adapter.ts +28 -0
- package/src/plugins/ibkr/gateway-service.test.ts +45 -1
- package/src/plugins/ibkr/gateway-service.ts +141 -7
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +11 -3
- package/src/plugins/prediction-markets/controller.ts +141 -121
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/prediction-markets/launch.ts +51 -16
- package/src/plugins/prediction-markets/metrics.ts +9 -5
- package/src/plugins/prediction-markets/pane.test.tsx +119 -57
- package/src/plugins/prediction-markets/pane.tsx +74 -153
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +265 -3
- package/src/sources/provider-router.ts +239 -11
- package/src/sources/yahoo-finance.test.ts +26 -0
- package/src/sources/yahoo-finance.ts +54 -3
- package/src/state/app-context-selector.test.tsx +99 -0
- package/src/state/app-context.test.ts +18 -0
- package/src/state/app-context.tsx +170 -65
- package/src/state/selectors.ts +25 -0
- package/src/theme/colors.ts +8 -8
- package/src/theme/themes.test.ts +2 -0
- package/src/theme/themes.ts +22 -10
- package/src/types/broker.ts +21 -0
- package/src/types/config.ts +17 -1
- package/src/types/data-provider.ts +18 -0
- package/src/types/financials.ts +31 -1
- package/src/types/plugin.ts +70 -0
- package/src/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +35 -12
- package/src/utils/back-navigation.ts +27 -0
- package/src/utils/format.test.ts +14 -0
- package/src/utils/format.ts +3 -3
- package/src/utils/market-status.ts +46 -1
- package/src/utils/quote-freshness.ts +127 -0
- package/src/utils/quote-resolution.test.ts +277 -0
- package/src/utils/quote-resolution.ts +641 -0
- package/src/utils/slugify.ts +3 -0
- package/src/utils/timestamp.ts +25 -0
- package/src/version.ts +1 -1
- package/src/cli/commands/portfolio.ts +0 -210
- package/src/cli/commands/watchlist.ts +0 -210
- package/src/plugins/builtin/data-providers.ts +0 -16
- package/src/sources/exchange-rates.ts +0 -18
- package/src/utils/ascii-chart.ts +0 -172
- package/src/utils/ticker-tokenizer.test.ts +0 -48
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createContext,
|
|
3
|
+
useCallback,
|
|
3
4
|
useContext,
|
|
4
5
|
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
5
7
|
useMemo,
|
|
6
8
|
useReducer,
|
|
7
9
|
useRef,
|
|
10
|
+
useSyncExternalStore,
|
|
8
11
|
type SetStateAction,
|
|
9
12
|
type ReactNode,
|
|
10
13
|
} from "react";
|
|
@@ -12,7 +15,7 @@ import type { SessionStore } from "../data/session-store";
|
|
|
12
15
|
import { saveConfig } from "../data/config-store";
|
|
13
16
|
import { applyTheme } from "../theme/colors";
|
|
14
17
|
import { isBrokerPortfolioId } from "../utils/broker-instances";
|
|
15
|
-
import {
|
|
18
|
+
import { resolveTickerFinancialsQuoteState } from "../utils/quote-resolution";
|
|
16
19
|
import {
|
|
17
20
|
cloneLayout,
|
|
18
21
|
DEFAULT_LAYOUT,
|
|
@@ -146,10 +149,6 @@ function shouldPreserveUnknownCollectionId(collectionId: string | undefined): bo
|
|
|
146
149
|
return isBrokerPortfolioId(collectionId);
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
function hasLikelyPriceUnitMismatch(current: Quote | undefined, next: Quote): boolean {
|
|
150
|
-
return hasLikelyQuoteUnitMismatch(current, next);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
152
|
function getConfiguredCollectionId(config: AppConfig, instance: PaneInstanceConfig): string {
|
|
154
153
|
const candidates = [
|
|
155
154
|
instance.params?.collectionId,
|
|
@@ -287,12 +286,6 @@ function nextRecentTickers(current: string[], symbol: string | null): string[] {
|
|
|
287
286
|
return [symbol, ...current.filter((entry) => entry !== symbol)].slice(0, 50);
|
|
288
287
|
}
|
|
289
288
|
|
|
290
|
-
function shouldPreserveExistingQuote(current: Quote | undefined, next: Quote): boolean {
|
|
291
|
-
return current?.dataSource === "live"
|
|
292
|
-
&& current.providerId !== "gloomberb-cloud"
|
|
293
|
-
&& next.providerId === "gloomberb-cloud";
|
|
294
|
-
}
|
|
295
|
-
|
|
296
289
|
function syncLayouts(layouts: SavedLayout[], activeLayoutIndex: number, layout: LayoutConfig): SavedLayout[] {
|
|
297
290
|
return layouts.map((savedLayout, index) => (
|
|
298
291
|
index === activeLayoutIndex ? { ...savedLayout, layout: cloneLayout(layout) } : savedLayout
|
|
@@ -392,6 +385,20 @@ function removeHistoryIndex(layoutHistory: Record<number, LayoutHistoryEntry>, r
|
|
|
392
385
|
return next;
|
|
393
386
|
}
|
|
394
387
|
|
|
388
|
+
/** If paneId is a floating pane, bump its zIndex to the top. */
|
|
389
|
+
function bringFloatingToFront(layout: LayoutConfig, paneId: string): LayoutConfig {
|
|
390
|
+
const entry = layout.floating.find((e) => e.instanceId === paneId);
|
|
391
|
+
if (!entry) return layout;
|
|
392
|
+
const maxZ = layout.floating.reduce((max, e) => Math.max(max, e.zIndex ?? 50), 0);
|
|
393
|
+
if ((entry.zIndex ?? 50) >= maxZ) return layout; // already on top
|
|
394
|
+
return {
|
|
395
|
+
...layout,
|
|
396
|
+
floating: layout.floating.map((e) =>
|
|
397
|
+
e.instanceId === paneId ? { ...e, zIndex: maxZ + 1 } : e,
|
|
398
|
+
),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
395
402
|
function withFocusedPane(state: AppState, config: AppConfig): AppState {
|
|
396
403
|
const normalizedLayout = normalizePaneLayout(config.layout);
|
|
397
404
|
const nextConfig = normalizedLayout === config.layout
|
|
@@ -451,30 +458,14 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
451
458
|
|
|
452
459
|
case "SET_FINANCIALS": {
|
|
453
460
|
const financials = new Map(state.financials);
|
|
454
|
-
financials.set(action.symbol, action.data);
|
|
461
|
+
financials.set(action.symbol, resolveTickerFinancialsQuoteState(action.data)!);
|
|
455
462
|
return { ...state, financials };
|
|
456
463
|
}
|
|
457
464
|
|
|
458
465
|
case "MERGE_QUOTE": {
|
|
459
|
-
const current = state.financials.get(action.symbol);
|
|
460
|
-
if (
|
|
461
|
-
shouldPreserveExistingQuote(current?.quote, action.quote)
|
|
462
|
-
|| hasLikelyPriceUnitMismatch(current?.quote, action.quote)
|
|
463
|
-
) {
|
|
464
|
-
return state;
|
|
465
|
-
}
|
|
466
466
|
const financials = new Map(state.financials);
|
|
467
|
-
financials.
|
|
468
|
-
|
|
469
|
-
quarterlyStatements: current?.quarterlyStatements ?? [],
|
|
470
|
-
priceHistory: current?.priceHistory ?? [],
|
|
471
|
-
fundamentals: current?.fundamentals,
|
|
472
|
-
profile: current?.profile,
|
|
473
|
-
quote: {
|
|
474
|
-
...(current?.quote ?? {}),
|
|
475
|
-
...action.quote,
|
|
476
|
-
},
|
|
477
|
-
});
|
|
467
|
+
const current = state.financials.get(action.symbol);
|
|
468
|
+
financials.set(action.symbol, resolveTickerFinancialsQuoteState(current, action.quote)!);
|
|
478
469
|
return { ...state, financials };
|
|
479
470
|
}
|
|
480
471
|
|
|
@@ -482,7 +473,7 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
482
473
|
if (action.financials.size === 0) return state;
|
|
483
474
|
const financials = new Map(state.financials);
|
|
484
475
|
for (const [symbol, data] of action.financials) {
|
|
485
|
-
financials.set(symbol, data);
|
|
476
|
+
financials.set(symbol, resolveTickerFinancialsQuoteState(data)!);
|
|
486
477
|
}
|
|
487
478
|
return { ...state, financials };
|
|
488
479
|
}
|
|
@@ -724,19 +715,30 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
724
715
|
});
|
|
725
716
|
}
|
|
726
717
|
|
|
727
|
-
case "FOCUS_PANE":
|
|
718
|
+
case "FOCUS_PANE": {
|
|
719
|
+
const layout = bringFloatingToFront(state.config.layout, action.paneId);
|
|
720
|
+
const config = layout !== state.config.layout
|
|
721
|
+
? { ...state.config, layout, layouts: syncLayouts(state.config.layouts, state.config.activeLayoutIndex, layout) }
|
|
722
|
+
: state.config;
|
|
728
723
|
return {
|
|
729
724
|
...state,
|
|
725
|
+
config,
|
|
730
726
|
focusedPaneId: action.paneId,
|
|
731
727
|
recentTickers: nextRecentTickers(state.recentTickers, resolveTickerForPane(state, action.paneId)),
|
|
732
728
|
};
|
|
729
|
+
}
|
|
733
730
|
|
|
734
731
|
case "FOCUS_NEXT": {
|
|
735
732
|
if (action.paneOrder.length === 0) return state;
|
|
736
733
|
const currentIndex = state.focusedPaneId ? action.paneOrder.indexOf(state.focusedPaneId) : -1;
|
|
737
734
|
const nextPaneId = action.paneOrder[(currentIndex + 1) % action.paneOrder.length]!;
|
|
735
|
+
const layout = bringFloatingToFront(state.config.layout, nextPaneId);
|
|
736
|
+
const config = layout !== state.config.layout
|
|
737
|
+
? { ...state.config, layout, layouts: syncLayouts(state.config.layouts, state.config.activeLayoutIndex, layout) }
|
|
738
|
+
: state.config;
|
|
738
739
|
return {
|
|
739
740
|
...state,
|
|
741
|
+
config,
|
|
740
742
|
focusedPaneId: nextPaneId,
|
|
741
743
|
recentTickers: nextRecentTickers(state.recentTickers, resolveTickerForPane(state, nextPaneId)),
|
|
742
744
|
};
|
|
@@ -746,8 +748,13 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
746
748
|
if (action.paneOrder.length === 0) return state;
|
|
747
749
|
const currentIndex = state.focusedPaneId ? action.paneOrder.indexOf(state.focusedPaneId) : 0;
|
|
748
750
|
const nextPaneId = action.paneOrder[(currentIndex - 1 + action.paneOrder.length) % action.paneOrder.length]!;
|
|
751
|
+
const layout = bringFloatingToFront(state.config.layout, nextPaneId);
|
|
752
|
+
const config = layout !== state.config.layout
|
|
753
|
+
? { ...state.config, layout, layouts: syncLayouts(state.config.layouts, state.config.activeLayoutIndex, layout) }
|
|
754
|
+
: state.config;
|
|
749
755
|
return {
|
|
750
756
|
...state,
|
|
757
|
+
config,
|
|
751
758
|
focusedPaneId: nextPaneId,
|
|
752
759
|
recentTickers: nextRecentTickers(state.recentTickers, resolveTickerForPane(state, nextPaneId)),
|
|
753
760
|
};
|
|
@@ -755,6 +762,9 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
755
762
|
|
|
756
763
|
case "UPDATE_PANE_STATE": {
|
|
757
764
|
const current = state.paneState[action.paneId] ?? {};
|
|
765
|
+
if (Object.keys(action.patch).every((key) => Object.is(current[key], action.patch[key]))) {
|
|
766
|
+
return state;
|
|
767
|
+
}
|
|
758
768
|
const nextState = { ...current, ...action.patch };
|
|
759
769
|
const recentTickers = Object.prototype.hasOwnProperty.call(action.patch, "cursorSymbol")
|
|
760
770
|
? nextRecentTickers(state.recentTickers, typeof nextState.cursorSymbol === "string" ? nextState.cursorSymbol : null)
|
|
@@ -799,20 +809,81 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
799
809
|
}
|
|
800
810
|
}
|
|
801
811
|
|
|
802
|
-
interface
|
|
812
|
+
interface AppContextStoreValue {
|
|
813
|
+
dispatch: React.Dispatch<AppAction>;
|
|
814
|
+
getState: () => AppState;
|
|
815
|
+
subscribe: (listener: () => void) => () => void;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
interface AppContextLegacyValue {
|
|
803
819
|
state: AppState;
|
|
804
820
|
dispatch: React.Dispatch<AppAction>;
|
|
805
821
|
}
|
|
806
822
|
|
|
823
|
+
type AppContextValue = AppContextStoreValue | AppContextLegacyValue;
|
|
824
|
+
|
|
807
825
|
export const AppContext = createContext<AppContextValue | null>(null);
|
|
808
826
|
const PaneContext = createContext<string | null>(null);
|
|
809
827
|
|
|
810
|
-
|
|
828
|
+
function isAppStoreContextValue(context: AppContextValue): context is AppContextStoreValue {
|
|
829
|
+
return "getState" in context && "subscribe" in context;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function useRequiredAppContext(): AppContextValue {
|
|
811
833
|
const context = useContext(AppContext);
|
|
812
834
|
if (!context) throw new Error("useAppState must be used within AppProvider");
|
|
813
835
|
return context;
|
|
814
836
|
}
|
|
815
837
|
|
|
838
|
+
export function useAppState(): AppContextLegacyValue {
|
|
839
|
+
const context = useRequiredAppContext();
|
|
840
|
+
if (!isAppStoreContextValue(context)) {
|
|
841
|
+
return context;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const state = useSyncExternalStore(context.subscribe, context.getState, context.getState);
|
|
845
|
+
return useMemo(() => ({ state, dispatch: context.dispatch }), [context.dispatch, state]);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
export function useAppDispatch(): React.Dispatch<AppAction> {
|
|
849
|
+
return useRequiredAppContext().dispatch;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function useAppSelector<T>(selector: (state: AppState) => T): T {
|
|
853
|
+
const context = useRequiredAppContext();
|
|
854
|
+
const selectorRef = useRef(selector);
|
|
855
|
+
selectorRef.current = selector;
|
|
856
|
+
const lastSnapshotRef = useRef<{ selection: T; themeId: string } | undefined>(undefined);
|
|
857
|
+
|
|
858
|
+
if (!isAppStoreContextValue(context)) {
|
|
859
|
+
return selector(context.state);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const snapshot = useSyncExternalStore(
|
|
863
|
+
context.subscribe,
|
|
864
|
+
() => {
|
|
865
|
+
const state = context.getState();
|
|
866
|
+
const selection = selectorRef.current(state);
|
|
867
|
+
const themeId = state.config.theme;
|
|
868
|
+
const previous = lastSnapshotRef.current;
|
|
869
|
+
if (previous && Object.is(previous.selection, selection) && previous.themeId === themeId) {
|
|
870
|
+
return previous;
|
|
871
|
+
}
|
|
872
|
+
const nextSnapshot = { selection, themeId };
|
|
873
|
+
lastSnapshotRef.current = nextSnapshot;
|
|
874
|
+
return nextSnapshot;
|
|
875
|
+
},
|
|
876
|
+
() => {
|
|
877
|
+
const state = context.getState();
|
|
878
|
+
return {
|
|
879
|
+
selection: selectorRef.current(state),
|
|
880
|
+
themeId: state.config.theme,
|
|
881
|
+
};
|
|
882
|
+
},
|
|
883
|
+
);
|
|
884
|
+
return snapshot.selection;
|
|
885
|
+
}
|
|
886
|
+
|
|
816
887
|
export function PaneInstanceProvider({ paneId, children }: { paneId: string; children: ReactNode }) {
|
|
817
888
|
return <PaneContext value={paneId}>{children}</PaneContext>;
|
|
818
889
|
}
|
|
@@ -824,20 +895,23 @@ export function usePaneInstanceId(): string {
|
|
|
824
895
|
}
|
|
825
896
|
|
|
826
897
|
export function usePaneInstance(): PaneInstanceConfig | null {
|
|
827
|
-
const { state } = useAppState();
|
|
828
898
|
const paneId = useContext(PaneContext);
|
|
829
|
-
return paneId ? findPaneInstance(state.config.layout, paneId) ?? null : null;
|
|
899
|
+
return useAppSelector((state) => (paneId ? findPaneInstance(state.config.layout, paneId) ?? null : null));
|
|
830
900
|
}
|
|
831
901
|
|
|
832
902
|
export function usePaneTicker(paneId?: string) {
|
|
833
|
-
const
|
|
834
|
-
const
|
|
835
|
-
const
|
|
903
|
+
const paneContextId = useContext(PaneContext);
|
|
904
|
+
const focusedPaneId = useAppSelector((state) => state.focusedPaneId);
|
|
905
|
+
const scopedPaneId = paneId ?? paneContextId ?? focusedPaneId;
|
|
906
|
+
const symbol = useAppSelector((state) => (
|
|
836
907
|
scopedPaneId ? resolveTickerForPane(state, scopedPaneId) : null
|
|
837
|
-
)
|
|
838
|
-
const ticker =
|
|
908
|
+
));
|
|
909
|
+
const ticker = useAppSelector((state) => (
|
|
839
910
|
symbol ? state.tickers.get(symbol) ?? null : null
|
|
840
|
-
)
|
|
911
|
+
));
|
|
912
|
+
const cachedFinancials = useAppSelector((state) => (
|
|
913
|
+
symbol ? state.financials.get(symbol) ?? null : null
|
|
914
|
+
));
|
|
841
915
|
const marketFinancials = useTickerFinancials(symbol, ticker);
|
|
842
916
|
|
|
843
917
|
return useMemo(() => {
|
|
@@ -845,9 +919,9 @@ export function usePaneTicker(paneId?: string) {
|
|
|
845
919
|
return {
|
|
846
920
|
symbol,
|
|
847
921
|
ticker,
|
|
848
|
-
financials: marketFinancials ??
|
|
922
|
+
financials: marketFinancials ?? cachedFinancials,
|
|
849
923
|
};
|
|
850
|
-
}, [marketFinancials, scopedPaneId,
|
|
924
|
+
}, [cachedFinancials, marketFinancials, scopedPaneId, symbol, ticker]);
|
|
851
925
|
}
|
|
852
926
|
|
|
853
927
|
export function useSelectedTicker(paneId?: string) {
|
|
@@ -855,40 +929,46 @@ export function useSelectedTicker(paneId?: string) {
|
|
|
855
929
|
}
|
|
856
930
|
|
|
857
931
|
export function useFocusedTicker() {
|
|
858
|
-
const
|
|
859
|
-
const
|
|
860
|
-
const
|
|
932
|
+
const symbol = useAppSelector((state) => getFocusedTickerSymbol(state));
|
|
933
|
+
const ticker = useAppSelector((state) => (symbol ? state.tickers.get(symbol) ?? null : null));
|
|
934
|
+
const cachedFinancials = useAppSelector((state) => (symbol ? state.financials.get(symbol) ?? null : null));
|
|
861
935
|
const marketFinancials = useTickerFinancials(symbol, ticker);
|
|
862
936
|
return useMemo(() => {
|
|
863
937
|
return {
|
|
864
938
|
symbol,
|
|
865
939
|
ticker,
|
|
866
|
-
financials: marketFinancials ??
|
|
940
|
+
financials: marketFinancials ?? cachedFinancials,
|
|
867
941
|
};
|
|
868
|
-
}, [
|
|
942
|
+
}, [cachedFinancials, marketFinancials, symbol, ticker]);
|
|
869
943
|
}
|
|
870
944
|
|
|
871
945
|
export function usePaneCollection(paneId?: string) {
|
|
872
|
-
const
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
946
|
+
const paneContextId = useContext(PaneContext);
|
|
947
|
+
const focusedPaneId = useAppSelector((state) => state.focusedPaneId);
|
|
948
|
+
const scopedPaneId = paneId ?? paneContextId ?? focusedPaneId;
|
|
949
|
+
const collectionId = useAppSelector((state) => (
|
|
950
|
+
scopedPaneId ? resolveCollectionForPane(state, scopedPaneId) : null
|
|
951
|
+
));
|
|
952
|
+
const portfolio = useAppSelector((state) => (
|
|
953
|
+
collectionId ? state.config.portfolios.find((entry) => entry.id === collectionId) ?? null : null
|
|
954
|
+
));
|
|
955
|
+
const watchlist = useAppSelector((state) => (
|
|
956
|
+
collectionId ? state.config.watchlists.find((entry) => entry.id === collectionId) ?? null : null
|
|
957
|
+
));
|
|
958
|
+
return useMemo(() => (
|
|
959
|
+
{ collectionId, portfolio, watchlist }
|
|
960
|
+
), [collectionId, portfolio, watchlist]);
|
|
883
961
|
}
|
|
884
962
|
|
|
885
963
|
export function usePaneStateValue<T>(key: string, fallback: T, paneId?: string): [T, (value: T) => void] {
|
|
886
|
-
const
|
|
964
|
+
const dispatch = useAppDispatch();
|
|
887
965
|
const scopedPaneId = paneId ?? usePaneInstanceId();
|
|
888
|
-
const value = (state
|
|
889
|
-
|
|
966
|
+
const value = useAppSelector((state) => (
|
|
967
|
+
(state.paneState[scopedPaneId]?.[key] as T | undefined) ?? fallback
|
|
968
|
+
));
|
|
969
|
+
const setValue = useCallback((nextValue: T) => {
|
|
890
970
|
dispatch({ type: "UPDATE_PANE_STATE", paneId: scopedPaneId, patch: { [key]: nextValue } });
|
|
891
|
-
};
|
|
971
|
+
}, [dispatch, key, scopedPaneId]);
|
|
892
972
|
return [value, setValue];
|
|
893
973
|
}
|
|
894
974
|
|
|
@@ -967,6 +1047,24 @@ export function AppProvider({
|
|
|
967
1047
|
createInitialState(config, initialSessionSnapshot)
|
|
968
1048
|
));
|
|
969
1049
|
const previousRecentTickers = useRef(state.recentTickers);
|
|
1050
|
+
const stateRef = useRef(state);
|
|
1051
|
+
const listenersRef = useRef(new Set<() => void>());
|
|
1052
|
+
const storeRef = useRef<AppContextStoreValue | null>(null);
|
|
1053
|
+
|
|
1054
|
+
stateRef.current = state;
|
|
1055
|
+
|
|
1056
|
+
if (!storeRef.current) {
|
|
1057
|
+
storeRef.current = {
|
|
1058
|
+
dispatch,
|
|
1059
|
+
getState: () => stateRef.current,
|
|
1060
|
+
subscribe: (listener) => {
|
|
1061
|
+
listenersRef.current.add(listener);
|
|
1062
|
+
return () => {
|
|
1063
|
+
listenersRef.current.delete(listener);
|
|
1064
|
+
};
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
970
1068
|
|
|
971
1069
|
useEffect(() => {
|
|
972
1070
|
if (previousRecentTickers.current !== state.recentTickers) {
|
|
@@ -975,7 +1073,14 @@ export function AppProvider({
|
|
|
975
1073
|
}
|
|
976
1074
|
}, [state.config, state.recentTickers]);
|
|
977
1075
|
|
|
1076
|
+
useLayoutEffect(() => {
|
|
1077
|
+
stateRef.current = state;
|
|
1078
|
+
for (const listener of listenersRef.current) {
|
|
1079
|
+
listener();
|
|
1080
|
+
}
|
|
1081
|
+
}, [state]);
|
|
1082
|
+
|
|
978
1083
|
usePersistSessionSnapshot(sessionStore, state, APP_SESSION_ID, APP_SESSION_SCHEMA_VERSION);
|
|
979
1084
|
|
|
980
|
-
return <AppContext value={
|
|
1085
|
+
return <AppContext value={storeRef.current}>{children}</AppContext>;
|
|
981
1086
|
}
|
package/src/state/selectors.ts
CHANGED
|
@@ -35,6 +35,31 @@ export function getCollectionTickers(state: AppState, collectionId: string | nul
|
|
|
35
35
|
return [];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function getCollectionTickerCount(state: AppState, collectionId: string | null): number {
|
|
39
|
+
if (!collectionId) return 0;
|
|
40
|
+
|
|
41
|
+
let count = 0;
|
|
42
|
+
if (state.config.portfolios.some((portfolio) => portfolio.id === collectionId)) {
|
|
43
|
+
for (const ticker of state.tickers.values()) {
|
|
44
|
+
if (ticker.metadata.portfolios.includes(collectionId)) {
|
|
45
|
+
count += 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return count;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (state.config.watchlists.some((watchlist) => watchlist.id === collectionId)) {
|
|
52
|
+
for (const ticker of state.tickers.values()) {
|
|
53
|
+
if (ticker.metadata.watchlists.includes(collectionId)) {
|
|
54
|
+
count += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return count;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
export function getCollectionName(state: AppState, collectionId: string | null): string {
|
|
39
64
|
if (!collectionId) return "";
|
|
40
65
|
const portfolio = state.config.portfolios.find((entry) => entry.id === collectionId);
|
package/src/theme/colors.ts
CHANGED
|
@@ -86,26 +86,26 @@ export function commandBarSelectedText(): string {
|
|
|
86
86
|
|
|
87
87
|
/** Background for docked pane bodies */
|
|
88
88
|
export function paneBg(focused: boolean): string {
|
|
89
|
-
if (focused) return colors.bg;
|
|
90
|
-
return colors.panel;
|
|
89
|
+
if (focused) return blendHex(colors.bg, colors.borderFocused, 0.06);
|
|
90
|
+
return blendHex(colors.panel, colors.border, 0.08);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/** Background for floating pane bodies — elevated above docked panes */
|
|
94
94
|
export function floatingPaneBg(focused: boolean): string {
|
|
95
|
-
if (focused) return blendHex(colors.bg, colors.
|
|
96
|
-
return blendHex(colors.panel, colors.border, 0.
|
|
95
|
+
if (focused) return blendHex(colors.bg, colors.borderFocused, 0.08);
|
|
96
|
+
return blendHex(colors.panel, colors.border, 0.18);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/** Background for pane title bars */
|
|
100
100
|
export function paneTitleBg(focused: boolean): string {
|
|
101
|
-
if (focused) return blendHex(colors.bg, colors.borderFocused, 0.
|
|
102
|
-
return blendHex(colors.panel, colors.border, 0.
|
|
101
|
+
if (focused) return blendHex(colors.bg, colors.borderFocused, 0.22);
|
|
102
|
+
return blendHex(colors.panel, colors.border, 0.15);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/** Background for floating pane title bars */
|
|
106
106
|
export function floatingPaneTitleBg(focused: boolean): string {
|
|
107
|
-
if (focused) return blendHex(colors.bg, colors.borderFocused, 0.
|
|
108
|
-
return blendHex(colors.panel, colors.border, 0.
|
|
107
|
+
if (focused) return blendHex(colors.bg, colors.borderFocused, 0.25);
|
|
108
|
+
return blendHex(colors.panel, colors.border, 0.25);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/** Title text color for panes */
|
package/src/theme/themes.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { DEFAULT_THEME, themes } from "./themes";
|
|
|
6
6
|
const BODY_TEXT_MIN = 4.5;
|
|
7
7
|
const SUBTLE_TEXT_MIN = 3.6;
|
|
8
8
|
const CHROME_TEXT_MIN = 4.5;
|
|
9
|
+
const SELECTED_SURFACE_MIN = 1.75;
|
|
9
10
|
|
|
10
11
|
function assertMinContrast(themeId: string, label: string, fg: string, bg: string, min: number): void {
|
|
11
12
|
const ratio = contrastRatio(fg, bg);
|
|
@@ -28,6 +29,7 @@ describe("theme contrast", () => {
|
|
|
28
29
|
assertMinContrast(id, `positive/${surfaceLabel}`, theme.positive, surface, SUBTLE_TEXT_MIN);
|
|
29
30
|
assertMinContrast(id, `negative/${surfaceLabel}`, theme.negative, surface, SUBTLE_TEXT_MIN);
|
|
30
31
|
assertMinContrast(id, `neutral/${surfaceLabel}`, theme.neutral, surface, SUBTLE_TEXT_MIN);
|
|
32
|
+
assertMinContrast(id, `selected/${surfaceLabel}`, theme.selected, surface, SELECTED_SURFACE_MIN);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
assertMinContrast(id, "headerText/header", theme.headerText, theme.header, BODY_TEXT_MIN);
|
package/src/theme/themes.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface Theme {
|
|
|
28
28
|
commandBorder: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const BODY_TEXT_MIN = 4.5;
|
|
32
|
+
const SUBTLE_TEXT_MIN = 3.6;
|
|
33
|
+
const SELECTED_SURFACE_MIN = 1.75;
|
|
34
|
+
|
|
31
35
|
function highestMinimumContrast(surfaces: readonly string[], candidates: readonly string[]): string {
|
|
32
36
|
return candidates.reduce((best, candidate) => {
|
|
33
37
|
const bestScore = Math.min(...surfaces.map((surface) => contrastRatio(best, surface)));
|
|
@@ -38,53 +42,60 @@ function highestMinimumContrast(surfaces: readonly string[], candidates: readonl
|
|
|
38
42
|
|
|
39
43
|
function normalizeTheme(theme: Theme): Theme {
|
|
40
44
|
const bodySurfaces = [theme.bg, theme.panel] as const;
|
|
45
|
+
const bodyContrastExtreme = highestMinimumContrast(bodySurfaces, ["#ffffff", "#000000"]);
|
|
41
46
|
const text = blendForContrastOnSurfaces(
|
|
42
47
|
theme.text,
|
|
43
48
|
bodySurfaces,
|
|
44
49
|
highestMinimumContrast(bodySurfaces, [theme.textBright, theme.headerText, "#f2f2f2"]),
|
|
45
|
-
|
|
50
|
+
BODY_TEXT_MIN,
|
|
46
51
|
);
|
|
47
52
|
const textDim = blendForContrastOnSurfaces(
|
|
48
53
|
theme.textDim,
|
|
49
54
|
bodySurfaces,
|
|
50
55
|
highestMinimumContrast(bodySurfaces, [text, theme.textBright, "#d0d7de"]),
|
|
51
|
-
|
|
56
|
+
BODY_TEXT_MIN,
|
|
52
57
|
);
|
|
53
58
|
const textMuted = blendForContrastOnSurfaces(
|
|
54
59
|
theme.textMuted,
|
|
55
60
|
bodySurfaces,
|
|
56
61
|
highestMinimumContrast(bodySurfaces, [textDim, text, "#c0c7d1"]),
|
|
57
|
-
|
|
62
|
+
SUBTLE_TEXT_MIN,
|
|
58
63
|
);
|
|
59
64
|
const positive = blendForContrastOnSurfaces(
|
|
60
65
|
theme.positive,
|
|
61
66
|
bodySurfaces,
|
|
62
67
|
blendHex(theme.positive, theme.textBright, 0.5),
|
|
63
|
-
|
|
68
|
+
SUBTLE_TEXT_MIN,
|
|
64
69
|
);
|
|
65
70
|
const negative = blendForContrastOnSurfaces(
|
|
66
71
|
theme.negative,
|
|
67
72
|
bodySurfaces,
|
|
68
73
|
blendHex(theme.negative, theme.textBright, 0.45),
|
|
69
|
-
|
|
74
|
+
SUBTLE_TEXT_MIN,
|
|
70
75
|
);
|
|
71
76
|
const neutral = blendForContrastOnSurfaces(
|
|
72
77
|
theme.neutral,
|
|
73
78
|
bodySurfaces,
|
|
74
79
|
highestMinimumContrast(bodySurfaces, [textDim, text, "#c0c7d1"]),
|
|
75
|
-
|
|
80
|
+
SUBTLE_TEXT_MIN,
|
|
81
|
+
);
|
|
82
|
+
const selected = blendForContrastOnSurfaces(
|
|
83
|
+
theme.selected,
|
|
84
|
+
bodySurfaces,
|
|
85
|
+
blendHex(theme.selected, bodyContrastExtreme, 0.62),
|
|
86
|
+
SELECTED_SURFACE_MIN,
|
|
76
87
|
);
|
|
77
88
|
const headerText = blendForContrast(
|
|
78
89
|
theme.headerText,
|
|
79
90
|
theme.header,
|
|
80
91
|
higherContrast(text, theme.textBright, theme.header),
|
|
81
|
-
|
|
92
|
+
BODY_TEXT_MIN,
|
|
82
93
|
);
|
|
83
94
|
const selectedText = blendForContrast(
|
|
84
95
|
theme.selectedText,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
selected,
|
|
97
|
+
highestMinimumContrast([selected], [text, theme.textBright, "#f2f2f2"]),
|
|
98
|
+
BODY_TEXT_MIN,
|
|
88
99
|
);
|
|
89
100
|
|
|
90
101
|
return {
|
|
@@ -96,6 +107,7 @@ function normalizeTheme(theme: Theme): Theme {
|
|
|
96
107
|
negative,
|
|
97
108
|
neutral,
|
|
98
109
|
headerText,
|
|
110
|
+
selected,
|
|
99
111
|
selectedText,
|
|
100
112
|
};
|
|
101
113
|
}
|
package/src/types/broker.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Quote, TickerFinancials, PricePoint, OptionsChain } from "./financials";
|
|
2
2
|
import type { TimeRange } from "../components/chart/chart-types";
|
|
3
|
+
import type { ChartResolutionSupport, ManualChartResolution } from "../components/chart/chart-resolution";
|
|
3
4
|
import type { BrokerInstanceConfig } from "./config";
|
|
4
5
|
import type { QuoteSubscriptionTarget } from "./data-provider";
|
|
5
6
|
import type { BrokerContractRef, InstrumentSearchResult } from "./instrument";
|
|
@@ -78,8 +79,28 @@ export interface BrokerAdapter {
|
|
|
78
79
|
getTickerFinancials?(ticker: string, instance: BrokerInstanceConfig, exchange?: string, instrument?: BrokerContractRef | null): Promise<TickerFinancials>;
|
|
79
80
|
getQuote?(ticker: string, instance: BrokerInstanceConfig, exchange?: string, instrument?: BrokerContractRef | null): Promise<Quote>;
|
|
80
81
|
getPriceHistory?(ticker: string, instance: BrokerInstanceConfig, exchange: string, range: TimeRange, instrument?: BrokerContractRef | null): Promise<PricePoint[]>;
|
|
82
|
+
getPriceHistoryForResolution?(
|
|
83
|
+
ticker: string,
|
|
84
|
+
instance: BrokerInstanceConfig,
|
|
85
|
+
exchange: string,
|
|
86
|
+
bufferRange: TimeRange,
|
|
87
|
+
resolution: ManualChartResolution,
|
|
88
|
+
instrument?: BrokerContractRef | null,
|
|
89
|
+
): Promise<PricePoint[]>;
|
|
81
90
|
/** Fetch higher-resolution price data for a specific date window (e.g. when zoomed in). */
|
|
82
91
|
getDetailedPriceHistory?(ticker: string, instance: BrokerInstanceConfig, exchange: string, startDate: Date, endDate: Date, barSize: string, instrument?: BrokerContractRef | null): Promise<PricePoint[]>;
|
|
92
|
+
getChartResolutionSupport?(
|
|
93
|
+
ticker: string,
|
|
94
|
+
instance: BrokerInstanceConfig,
|
|
95
|
+
exchange?: string,
|
|
96
|
+
instrument?: BrokerContractRef | null,
|
|
97
|
+
): Promise<ChartResolutionSupport[]> | ChartResolutionSupport[];
|
|
98
|
+
getChartResolutionCapabilities?(
|
|
99
|
+
ticker: string,
|
|
100
|
+
instance: BrokerInstanceConfig,
|
|
101
|
+
exchange?: string,
|
|
102
|
+
instrument?: BrokerContractRef | null,
|
|
103
|
+
): Promise<ManualChartResolution[]> | ManualChartResolution[];
|
|
83
104
|
getOptionsChain?(ticker: string, instance: BrokerInstanceConfig, exchange?: string, expirationDate?: number, instrument?: BrokerContractRef | null): Promise<OptionsChain>;
|
|
84
105
|
subscribeQuotes?(
|
|
85
106
|
instance: BrokerInstanceConfig,
|
package/src/types/config.ts
CHANGED
|
@@ -149,7 +149,13 @@ export const DEFAULT_LAYOUT: LayoutConfig = {
|
|
|
149
149
|
kind: "split",
|
|
150
150
|
axis: "horizontal",
|
|
151
151
|
ratio: 0.4,
|
|
152
|
-
first: {
|
|
152
|
+
first: {
|
|
153
|
+
kind: "split",
|
|
154
|
+
axis: "vertical",
|
|
155
|
+
ratio: 0.5,
|
|
156
|
+
first: { kind: "pane", instanceId: "portfolio-list:main" },
|
|
157
|
+
second: { kind: "pane", instanceId: "chat:main" },
|
|
158
|
+
},
|
|
153
159
|
second: { kind: "pane", instanceId: "ticker-detail:main" },
|
|
154
160
|
},
|
|
155
161
|
instances: [
|
|
@@ -172,9 +178,19 @@ export const DEFAULT_LAYOUT: LayoutConfig = {
|
|
|
172
178
|
settings: {
|
|
173
179
|
hideTabs: false,
|
|
174
180
|
lockedTabId: "overview",
|
|
181
|
+
chartRangePreset: "5Y",
|
|
182
|
+
chartResolution: "1wk",
|
|
175
183
|
},
|
|
176
184
|
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
177
185
|
},
|
|
186
|
+
{
|
|
187
|
+
instanceId: "chat:main",
|
|
188
|
+
paneId: "chat",
|
|
189
|
+
settings: {
|
|
190
|
+
hideTabs: false,
|
|
191
|
+
},
|
|
192
|
+
binding: { kind: "none" },
|
|
193
|
+
},
|
|
178
194
|
],
|
|
179
195
|
floating: [],
|
|
180
196
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Quote, Fundamentals, TickerFinancials, PricePoint, OptionsChain } from "./financials";
|
|
2
2
|
import type { TimeRange } from "../components/chart/chart-types";
|
|
3
|
+
import type { ChartResolutionSupport, ManualChartResolution } from "../components/chart/chart-resolution";
|
|
3
4
|
import type { BrokerContractRef, InstrumentSearchResult } from "./instrument";
|
|
4
5
|
import type { CachePolicyMap } from "./persistence";
|
|
5
6
|
|
|
@@ -71,8 +72,25 @@ export interface DataProvider {
|
|
|
71
72
|
/** Fetch article summary/description by URL (lazy-loaded on selection) */
|
|
72
73
|
getArticleSummary(url: string): Promise<string | null>;
|
|
73
74
|
getPriceHistory(ticker: string, exchange: string, range: TimeRange, context?: MarketDataRequestContext): Promise<PricePoint[]>;
|
|
75
|
+
getPriceHistoryForResolution?(
|
|
76
|
+
ticker: string,
|
|
77
|
+
exchange: string,
|
|
78
|
+
bufferRange: TimeRange,
|
|
79
|
+
resolution: ManualChartResolution,
|
|
80
|
+
context?: MarketDataRequestContext,
|
|
81
|
+
): Promise<PricePoint[]>;
|
|
74
82
|
/** Fetch higher-resolution price data for a specific date window (e.g. when zoomed in). */
|
|
75
83
|
getDetailedPriceHistory?(ticker: string, exchange: string, startDate: Date, endDate: Date, barSize: string, context?: MarketDataRequestContext): Promise<PricePoint[]>;
|
|
84
|
+
getChartResolutionSupport?(
|
|
85
|
+
ticker: string,
|
|
86
|
+
exchange?: string,
|
|
87
|
+
context?: MarketDataRequestContext,
|
|
88
|
+
): Promise<ChartResolutionSupport[]> | ChartResolutionSupport[];
|
|
89
|
+
getChartResolutionCapabilities?(
|
|
90
|
+
ticker: string,
|
|
91
|
+
exchange?: string,
|
|
92
|
+
context?: MarketDataRequestContext,
|
|
93
|
+
): Promise<ManualChartResolution[]> | ManualChartResolution[];
|
|
76
94
|
getOptionsChain?(ticker: string, exchange?: string, expirationDate?: number, context?: MarketDataRequestContext): Promise<OptionsChain>;
|
|
77
95
|
subscribeQuotes?(
|
|
78
96
|
targets: QuoteSubscriptionTarget[],
|