gloomberb 0.9.8 → 0.9.10
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/api-client/index.test.ts +3 -3
- package/src/api-client/index.ts +0 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +0 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- package/src/app.tsx +1 -0
- package/src/brokers/account-cache.ts +2 -10
- package/src/brokers/broker-portfolio-sync.ts +0 -1
- package/src/brokers/broker-ticker-sync.ts +20 -16
- package/src/brokers/remote-broker-adapter.ts +3 -0
- package/src/capabilities/registry.test.ts +25 -0
- package/src/capabilities/registry.ts +22 -5
- package/src/cli/commands/automation.ts +1 -3
- package/src/cli/commands/command-utils.ts +0 -7
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/plugins.ts +34 -17
- package/src/cli/index.ts +2 -2
- package/src/cli.test.ts +0 -157
- package/src/components/chart/comparison/renderer.ts +0 -1
- package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
- package/src/components/chart/core/index.test.ts +0 -24
- package/src/components/chart/core/renderer.ts +0 -4
- package/src/components/chart/core/resolution.test.ts +0 -17
- package/src/components/chart/core/resolution.ts +0 -26
- package/src/components/chart/core/scroll.test.ts +0 -6
- package/src/components/chart/core/scroll.ts +0 -22
- package/src/components/chart/core/viewport.test.ts +2 -23
- package/src/components/chart/core/viewport.ts +0 -27
- package/src/components/chart/market-session.ts +1 -1
- package/src/components/chart/native/surface/hooks.ts +10 -3
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
- package/src/components/command-bar/helpers.test.ts +1 -80
- package/src/components/command-bar/helpers.ts +1 -36
- package/src/components/command-bar/pane-settings.ts +1 -1
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +0 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/feed-data-table/stack-view.test.tsx +36 -130
- package/src/components/feed-data-table/stack-view.tsx +0 -1
- package/src/components/layout/shell/index.test.tsx +2 -113
- package/src/components/layout/shell/index.tsx +0 -1
- package/src/components/layout/shell/menu.ts +0 -1
- package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
- package/src/components/onboarding/onboarding-wizard.tsx +3 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -23
- package/src/components/price-selector-dialog.tsx +3 -3
- package/src/components/ticker/badge/text.tsx +0 -1
- package/src/components/ui/page-stack-view.tsx +1 -1
- package/src/components/ui/ui.test.tsx +0 -78
- package/src/core/state/app/layout-reducer.ts +0 -1
- package/src/data/config/layout.ts +3 -3
- package/src/data/resource-store.ts +6 -0
- package/src/data/sqlite/cache.test.ts +5 -0
- package/src/data/ticker-store.ts +0 -7
- package/src/market-data/coordinator/auxiliary.ts +1 -44
- package/src/market-data/coordinator/entries.ts +0 -1
- package/src/market-data/coordinator/index.test.ts +0 -10
- package/src/market-data/coordinator/index.ts +1 -18
- package/src/market-data/coordinator/quotes.ts +0 -6
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- package/src/market-data/quotes/resolution.ts +2 -2
- package/src/market-data/request-types.ts +0 -5
- package/src/market-data/result-types.ts +0 -11
- package/src/market-data/selectors.ts +1 -5
- package/src/news/aggregator.ts +5 -1
- package/src/pane-settings.test.ts +0 -18
- package/src/plugins/builtin/account-management/form-components.tsx +0 -120
- package/src/plugins/builtin/account-management/model.ts +3 -27
- package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
- package/src/plugins/builtin/buildout/format.ts +2 -11
- package/src/plugins/builtin/chat/chat.test.tsx +2 -1
- package/src/plugins/builtin/chat/content/index.tsx +1 -2
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -1
- package/src/plugins/builtin/chat/controller/index.ts +0 -10
- package/src/plugins/builtin/chat/controller.test.ts +5 -5
- package/src/plugins/builtin/chat/sidebar.test.tsx +15 -5
- package/src/plugins/builtin/chat/test-harness.tsx +2 -2
- package/src/plugins/builtin/cloud/plugin.tsx +1 -1
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
- package/src/plugins/builtin/congress-trades/model.ts +2 -7
- package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
- package/src/plugins/builtin/earnings/index.tsx +0 -9
- package/src/plugins/builtin/econ/index.tsx +1 -5
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/pane.tsx +0 -5
- package/src/plugins/builtin/insider/index.tsx +1 -8
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
- package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -2
- package/src/plugins/builtin/news/index.tsx +0 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
- package/src/plugins/builtin/options/view.test.tsx +1 -38
- package/src/plugins/builtin/plugin-groups.ts +17 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
- package/src/plugins/builtin/portfolio-list/index.test.ts +1 -24
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- package/src/plugins/builtin/sec/filing-documents.ts +0 -11
- package/src/plugins/builtin/sec/index.tsx +0 -2
- package/src/plugins/builtin/substack/article-detail.tsx +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +0 -15
- package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
- package/src/plugins/builtin/world-indices/index.tsx +0 -4
- package/src/plugins/builtin/world-indices/model.ts +0 -7
- package/src/plugins/ibkr/account-cache.ts +1 -1
- package/src/plugins/ibkr/portfolio-performance.ts +1 -1
- package/src/plugins/ibkr/trade/utils.ts +1 -7
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/prediction-markets/detail.test.tsx +0 -34
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +44 -26
- package/src/remote/controller.ts +1 -7
- package/src/remote/json-patch.ts +15 -1
- package/src/remote/server.ts +16 -6
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
- package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- package/src/renderers/electrobun/view/host/mouse.ts +0 -11
- package/src/renderers/electrobun/view/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
- package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
- package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
- package/src/renderers/opentui/host.tsx +11 -3
- package/src/renderers/opentui/input-host.tsx +5 -1
- package/src/sources/gloomberb-cloud/index.test.ts +0 -72
- package/src/sources/gloomberb-cloud/index.ts +1 -30
- package/src/sources/gloomberb-cloud/news.ts +0 -12
- package/src/sources/provider-router/financial-routes.ts +25 -3
- package/src/sources/provider-router/index.test.ts +167 -0
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/options.ts +59 -14
- package/src/sources/yahoo-finance/quote-summary.ts +4 -1
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +13 -8
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +6 -2
- package/src/sync/controller.test.ts +151 -0
- package/src/sync/controller.ts +92 -57
- package/src/sync/core-contributors.ts +45 -21
- package/src/sync/react.ts +6 -8
- package/src/sync/types.ts +3 -0
- package/src/test-support/data-provider.ts +0 -1
- package/src/ui/host.tsx +2 -1
- package/src/ui/index.tsx +1 -0
- package/src/updater.test.ts +0 -21
- package/src/updater.ts +20 -16
- package/src/utils/async-deadline.ts +26 -0
- package/src/utils/selection-clipboard.ts +2 -2
- package/src/version.ts +1 -1
- package/src/components/layout/pane/header.test.tsx +0 -39
- package/src/plugins/builtin/chat/channels.test.ts +0 -28
- package/src/plugins/builtin/chat/index.tsx +0 -4
- package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
- package/src/plugins/builtin/earnings/index.test.ts +0 -12
- package/src/plugins/builtin/help/index.test.tsx +0 -242
- package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
- package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
- package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
- package/src/renderers/electrobun/view/host/text.test.ts +0 -22
- package/src/renderers/opentui/dialog-host.test.tsx +0 -44
- /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MarketDataRequestContext } from "../../types/data-provider";
|
|
2
|
-
import type { OptionContract, OptionsChain, Quote } from "../../types/financials";
|
|
2
|
+
import type { MarketState, OptionContract, OptionsChain, Quote } from "../../types/financials";
|
|
3
3
|
import { parseOptionSymbol } from "../../utils/options";
|
|
4
4
|
import { getYahooSymbolsToTry } from "./symbols";
|
|
5
5
|
|
|
@@ -14,16 +14,40 @@ interface LoadYahooOptionsChainOptions {
|
|
|
14
14
|
|
|
15
15
|
interface GetYahooOptionQuoteOptions {
|
|
16
16
|
context?: MarketDataRequestContext;
|
|
17
|
-
|
|
17
|
+
getOptionsChainResult: (
|
|
18
18
|
ticker: string,
|
|
19
19
|
exchange?: string,
|
|
20
20
|
expirationDate?: number,
|
|
21
21
|
context?: MarketDataRequestContext,
|
|
22
|
-
) => Promise<
|
|
22
|
+
) => Promise<YahooOptionsChainResult>;
|
|
23
23
|
providerId: string;
|
|
24
24
|
ticker: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export interface YahooOptionsChainResult {
|
|
28
|
+
chain: OptionsChain;
|
|
29
|
+
underlyingMarketState?: MarketState;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const MARKET_STATES = new Set<MarketState>([
|
|
33
|
+
"PRE",
|
|
34
|
+
"REGULAR",
|
|
35
|
+
"POST",
|
|
36
|
+
"PREPRE",
|
|
37
|
+
"POSTPOST",
|
|
38
|
+
"CLOSED",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function normalizeYahooMarketState(value: unknown): MarketState | undefined {
|
|
42
|
+
const normalized = typeof value === "string" ? value.toUpperCase() as MarketState : undefined;
|
|
43
|
+
return normalized && MARKET_STATES.has(normalized) ? normalized : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function deriveOptionMarketState(underlyingMarketState?: MarketState): MarketState | undefined {
|
|
47
|
+
if (!underlyingMarketState) return undefined;
|
|
48
|
+
return underlyingMarketState === "REGULAR" ? "REGULAR" : "CLOSED";
|
|
49
|
+
}
|
|
50
|
+
|
|
27
51
|
function mapYahooOptionContract(raw: Record<string, any>): OptionContract {
|
|
28
52
|
return {
|
|
29
53
|
contractSymbol: raw.contractSymbol ?? "",
|
|
@@ -43,12 +67,12 @@ function mapYahooOptionContract(raw: Record<string, any>): OptionContract {
|
|
|
43
67
|
};
|
|
44
68
|
}
|
|
45
69
|
|
|
46
|
-
export async function
|
|
70
|
+
export async function loadYahooOptionsChainResult({
|
|
47
71
|
exchange,
|
|
48
72
|
expirationDate,
|
|
49
73
|
fetchJsonWithCrumb,
|
|
50
74
|
ticker,
|
|
51
|
-
}: LoadYahooOptionsChainOptions): Promise<
|
|
75
|
+
}: LoadYahooOptionsChainOptions): Promise<YahooOptionsChainResult> {
|
|
52
76
|
const symbolsToTry = getYahooSymbolsToTry(ticker, exchange);
|
|
53
77
|
let lastError: any;
|
|
54
78
|
|
|
@@ -62,6 +86,7 @@ export async function loadYahooOptionsChain({
|
|
|
62
86
|
result?: Array<{
|
|
63
87
|
underlyingSymbol?: string;
|
|
64
88
|
expirationDates?: number[];
|
|
89
|
+
quote?: { marketState?: unknown };
|
|
65
90
|
options?: Array<{
|
|
66
91
|
calls?: Array<Record<string, any>>;
|
|
67
92
|
puts?: Array<Record<string, any>>;
|
|
@@ -75,10 +100,13 @@ export async function loadYahooOptionsChain({
|
|
|
75
100
|
|
|
76
101
|
const opts = result.options?.[0];
|
|
77
102
|
return {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
chain: {
|
|
104
|
+
underlyingSymbol: result.underlyingSymbol ?? symbol,
|
|
105
|
+
expirationDates: result.expirationDates ?? [],
|
|
106
|
+
calls: (opts?.calls ?? []).map((contract) => mapYahooOptionContract(contract)),
|
|
107
|
+
puts: (opts?.puts ?? []).map((contract) => mapYahooOptionContract(contract)),
|
|
108
|
+
},
|
|
109
|
+
underlyingMarketState: normalizeYahooMarketState(result.quote?.marketState),
|
|
82
110
|
};
|
|
83
111
|
} catch (err) {
|
|
84
112
|
lastError = err;
|
|
@@ -87,15 +115,26 @@ export async function loadYahooOptionsChain({
|
|
|
87
115
|
throw lastError || new Error(`No options chain for ${ticker}`);
|
|
88
116
|
}
|
|
89
117
|
|
|
118
|
+
export async function loadYahooOptionsChain(
|
|
119
|
+
options: LoadYahooOptionsChainOptions,
|
|
120
|
+
): Promise<OptionsChain> {
|
|
121
|
+
return (await loadYahooOptionsChainResult(options)).chain;
|
|
122
|
+
}
|
|
123
|
+
|
|
90
124
|
export async function getYahooOptionQuote({
|
|
91
125
|
context,
|
|
92
|
-
|
|
126
|
+
getOptionsChainResult,
|
|
93
127
|
providerId,
|
|
94
128
|
ticker,
|
|
95
129
|
}: GetYahooOptionQuoteOptions): Promise<Quote> {
|
|
96
130
|
const parsed = parseOptionSymbol(ticker);
|
|
97
131
|
if (!parsed) throw new Error(`Unsupported option symbol ${ticker}`);
|
|
98
|
-
const chain = await
|
|
132
|
+
const { chain, underlyingMarketState } = await getOptionsChainResult(
|
|
133
|
+
parsed.underlying,
|
|
134
|
+
"",
|
|
135
|
+
parsed.expTs,
|
|
136
|
+
context,
|
|
137
|
+
);
|
|
99
138
|
const contracts = parsed.side === "C" ? chain.calls : chain.puts;
|
|
100
139
|
const contract = contracts.find((candidate) =>
|
|
101
140
|
Math.abs(candidate.strike - parsed.strike) < 0.001 &&
|
|
@@ -110,6 +149,12 @@ export async function getYahooOptionQuote({
|
|
|
110
149
|
: contract.ask > 0
|
|
111
150
|
? contract.ask
|
|
112
151
|
: undefined;
|
|
152
|
+
const marketState = deriveOptionMarketState(underlyingMarketState);
|
|
153
|
+
const lastUpdated = mark != null
|
|
154
|
+
? Date.now()
|
|
155
|
+
: contract.lastTradeDate > 0
|
|
156
|
+
? contract.lastTradeDate * 1000
|
|
157
|
+
: Date.now();
|
|
113
158
|
return {
|
|
114
159
|
symbol: ticker,
|
|
115
160
|
providerId,
|
|
@@ -122,13 +167,13 @@ export async function getYahooOptionQuote({
|
|
|
122
167
|
ask: contract.ask,
|
|
123
168
|
mark,
|
|
124
169
|
name: contract.contractSymbol,
|
|
125
|
-
lastUpdated
|
|
126
|
-
? contract.lastTradeDate * 1000
|
|
127
|
-
: Date.now(),
|
|
170
|
+
lastUpdated,
|
|
128
171
|
exchangeName: "OPTIONS",
|
|
129
172
|
fullExchangeName: "OPTIONS",
|
|
130
173
|
listingExchangeName: "OPTIONS",
|
|
131
174
|
listingExchangeFullName: "OPTIONS",
|
|
175
|
+
marketState,
|
|
176
|
+
sessionConfidence: marketState ? "derived" : "unknown",
|
|
132
177
|
dataSource: "delayed",
|
|
133
178
|
};
|
|
134
179
|
}
|
|
@@ -29,7 +29,10 @@ interface YahooQuoteSummaryOptions {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
interface YahooCorporateActionsOptions extends YahooQuoteSummaryOptions {
|
|
32
|
-
fetchChart: (symbol: string, range: string, interval?: string) => Promise<
|
|
32
|
+
fetchChart: (symbol: string, range: string, interval?: string) => Promise<{
|
|
33
|
+
meta: NonNullable<ChartResult["meta"]>;
|
|
34
|
+
events?: ChartResult["events"];
|
|
35
|
+
}>;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export async function loadYahooHolders({
|
|
@@ -135,28 +135,38 @@ describe("YahooFinanceClient exchange aliases", () => {
|
|
|
135
135
|
|
|
136
136
|
test("maps IBKR option symbols to Yahoo option quote marks", async () => {
|
|
137
137
|
const provider = new YahooFinanceClient() as any;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
138
|
+
const expiration = 1_821_139_200;
|
|
139
|
+
let underlyingMarketState = "POST";
|
|
140
|
+
provider.http.fetchJsonWithCrumb = async () => ({
|
|
141
|
+
optionChain: {
|
|
142
|
+
result: [{
|
|
143
|
+
underlyingSymbol: "AMD",
|
|
144
|
+
expirationDates: [expiration],
|
|
145
|
+
quote: { marketState: underlyingMarketState },
|
|
146
|
+
options: [{
|
|
147
|
+
calls: [{
|
|
148
|
+
contractSymbol: "AMD270917C00230000",
|
|
149
|
+
strike: 230,
|
|
150
|
+
currency: "USD",
|
|
151
|
+
lastPrice: 251,
|
|
152
|
+
change: 1.5,
|
|
153
|
+
percentChange: 0.6,
|
|
154
|
+
volume: 10,
|
|
155
|
+
openInterest: 20,
|
|
156
|
+
bid: 250.25,
|
|
157
|
+
ask: 254.5,
|
|
158
|
+
impliedVolatility: 0.4,
|
|
159
|
+
inTheMoney: false,
|
|
160
|
+
expiration,
|
|
161
|
+
lastTradeDate: 1_800_000_000,
|
|
162
|
+
}],
|
|
163
|
+
puts: [],
|
|
164
|
+
}],
|
|
165
|
+
}],
|
|
166
|
+
},
|
|
158
167
|
});
|
|
159
168
|
|
|
169
|
+
const requestedAt = Date.now();
|
|
160
170
|
const quote = await provider.getQuote("AMD 270917C00230000");
|
|
161
171
|
|
|
162
172
|
expect(quote).toMatchObject({
|
|
@@ -166,8 +176,17 @@ describe("YahooFinanceClient exchange aliases", () => {
|
|
|
166
176
|
bid: 250.25,
|
|
167
177
|
ask: 254.5,
|
|
168
178
|
providerId: "yahoo",
|
|
179
|
+
marketState: "CLOSED",
|
|
180
|
+
sessionConfidence: "derived",
|
|
181
|
+
});
|
|
182
|
+
expect(quote.lastUpdated).toBeGreaterThanOrEqual(requestedAt);
|
|
183
|
+
|
|
184
|
+
underlyingMarketState = "REGULAR";
|
|
185
|
+
const regularQuote = await provider.getQuote("AMD 270917C00230000");
|
|
186
|
+
expect(regularQuote).toMatchObject({
|
|
187
|
+
marketState: "REGULAR",
|
|
188
|
+
sessionConfidence: "derived",
|
|
169
189
|
});
|
|
170
|
-
expect(quote.lastUpdated).toBe(1_800_000_000_000);
|
|
171
190
|
});
|
|
172
191
|
|
|
173
192
|
test("preserves analyst rating price targets from upgrade history", async () => {
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import {
|
|
32
32
|
getYahooOptionQuote,
|
|
33
33
|
loadYahooOptionsChain,
|
|
34
|
+
loadYahooOptionsChainResult,
|
|
34
35
|
} from "./yahoo-finance/options";
|
|
35
36
|
import {
|
|
36
37
|
loadYahooAnalystResearch,
|
|
@@ -155,8 +156,13 @@ export class YahooFinanceClient implements DataProvider {
|
|
|
155
156
|
if (parseOptionSymbol(ticker)) {
|
|
156
157
|
return getYahooOptionQuote({
|
|
157
158
|
context,
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
getOptionsChainResult: (underlying, requestExchange, expirationDate) => (
|
|
160
|
+
loadYahooOptionsChainResult({
|
|
161
|
+
exchange: requestExchange ?? "",
|
|
162
|
+
expirationDate,
|
|
163
|
+
fetchJsonWithCrumb: (url) => this.http.fetchJsonWithCrumb(url),
|
|
164
|
+
ticker: underlying,
|
|
165
|
+
})
|
|
160
166
|
),
|
|
161
167
|
providerId: this.id,
|
|
162
168
|
ticker,
|
|
@@ -189,13 +195,12 @@ export class YahooFinanceClient implements DataProvider {
|
|
|
189
195
|
fromCurrency = normalized;
|
|
190
196
|
if (fromCurrency === "USD") return 1;
|
|
191
197
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
} catch {
|
|
197
|
-
return 1;
|
|
198
|
+
const { meta, history } = await this.fetchChart(`${fromCurrency}USD=X`, "1mo");
|
|
199
|
+
const rate = meta.regularMarketPrice ?? history[history.length - 1]?.close;
|
|
200
|
+
if (typeof rate !== "number" || !Number.isFinite(rate) || rate <= 0) {
|
|
201
|
+
throw new Error(`No exchange rate data for ${fromCurrency}/USD`);
|
|
198
202
|
}
|
|
203
|
+
return rate;
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
/** Search for a ticker by name/symbol - uses direct fetch (no retry) for speed */
|
|
@@ -4,87 +4,6 @@ import { appReducer, createInitialState } from "./index";
|
|
|
4
4
|
import { removePane } from "../../../plugins/pane-manager";
|
|
5
5
|
|
|
6
6
|
describe("appReducer command bar state", () => {
|
|
7
|
-
test("opens the command bar with an explicit query", () => {
|
|
8
|
-
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
9
|
-
const next = appReducer(state, { type: "SET_COMMAND_BAR", open: true, query: "Sync Broker Account" });
|
|
10
|
-
|
|
11
|
-
expect(next.commandBarOpen).toBe(true);
|
|
12
|
-
expect(next.commandBarQuery).toBe("Sync Broker Account");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test("clears the command bar query when closing", () => {
|
|
16
|
-
const state = appReducer(
|
|
17
|
-
createInitialState(createDefaultConfig("/tmp/gloomberb-test")),
|
|
18
|
-
{ type: "SET_COMMAND_BAR", open: true, query: "Disconnect Broker Account" },
|
|
19
|
-
);
|
|
20
|
-
const next = appReducer(state, { type: "SET_COMMAND_BAR", open: false });
|
|
21
|
-
|
|
22
|
-
expect(next.commandBarOpen).toBe(false);
|
|
23
|
-
expect(next.commandBarQuery).toBe("");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("stores and clears command bar launch requests", () => {
|
|
27
|
-
const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
28
|
-
const opened = appReducer(initial, {
|
|
29
|
-
type: "SET_COMMAND_BAR",
|
|
30
|
-
open: true,
|
|
31
|
-
launch: { kind: "plugin-command", commandId: "set-alert" },
|
|
32
|
-
});
|
|
33
|
-
const relaunched = appReducer(opened, {
|
|
34
|
-
type: "SET_COMMAND_BAR",
|
|
35
|
-
open: true,
|
|
36
|
-
launch: { kind: "plugin-command", commandId: "set-alert" },
|
|
37
|
-
});
|
|
38
|
-
const closed = appReducer(relaunched, { type: "SET_COMMAND_BAR", open: false });
|
|
39
|
-
|
|
40
|
-
expect(opened.commandBarOpen).toBe(true);
|
|
41
|
-
expect(opened.commandBarLaunchRequest).toEqual({
|
|
42
|
-
kind: "plugin-command",
|
|
43
|
-
commandId: "set-alert",
|
|
44
|
-
sequence: 1,
|
|
45
|
-
});
|
|
46
|
-
expect(relaunched.commandBarLaunchRequest?.sequence).toBe(2);
|
|
47
|
-
expect(closed.commandBarLaunchRequest).toBeNull();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("stores ticker search command bar launch requests", () => {
|
|
51
|
-
const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
52
|
-
const opened = appReducer(initial, {
|
|
53
|
-
type: "SET_COMMAND_BAR",
|
|
54
|
-
open: true,
|
|
55
|
-
launch: { kind: "ticker-search", query: "" },
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
expect(opened.commandBarOpen).toBe(true);
|
|
59
|
-
expect(opened.commandBarLaunchRequest).toEqual({
|
|
60
|
-
kind: "ticker-search",
|
|
61
|
-
query: "",
|
|
62
|
-
sequence: 1,
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("toggle opens blank and clears on close", () => {
|
|
67
|
-
const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
68
|
-
const opened = appReducer(initial, { type: "TOGGLE_COMMAND_BAR" });
|
|
69
|
-
const closed = appReducer({ ...opened, commandBarQuery: "stale" }, { type: "TOGGLE_COMMAND_BAR" });
|
|
70
|
-
|
|
71
|
-
expect(opened.commandBarOpen).toBe(true);
|
|
72
|
-
expect(opened.commandBarQuery).toBe("");
|
|
73
|
-
expect(closed.commandBarOpen).toBe(false);
|
|
74
|
-
expect(closed.commandBarQuery).toBe("");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("updates the live command bar query without closing the overlay", () => {
|
|
78
|
-
const state = appReducer(
|
|
79
|
-
createInitialState(createDefaultConfig("/tmp/gloomberb-test")),
|
|
80
|
-
{ type: "SET_COMMAND_BAR", open: true, query: "PL " },
|
|
81
|
-
);
|
|
82
|
-
const next = appReducer(state, { type: "SET_COMMAND_BAR_QUERY", query: "PL notes" });
|
|
83
|
-
|
|
84
|
-
expect(next.commandBarOpen).toBe(true);
|
|
85
|
-
expect(next.commandBarQuery).toBe("PL notes");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
7
|
test("re-shows the gridlock tip and refreshes its sequence on every trigger", () => {
|
|
89
8
|
const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
90
9
|
|
|
@@ -82,8 +82,12 @@ function createDesktopBridge(
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
describe("pane selectors", () => {
|
|
85
|
-
afterEach(() => {
|
|
86
|
-
testSetup
|
|
85
|
+
afterEach(async () => {
|
|
86
|
+
if (testSetup) {
|
|
87
|
+
await act(async () => {
|
|
88
|
+
testSetup!.renderer.destroy();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
87
91
|
testSetup = undefined;
|
|
88
92
|
capturedDispatch = null;
|
|
89
93
|
applyTheme("amber");
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
appReducer,
|
|
4
|
+
createInitialState,
|
|
5
|
+
type AppAction,
|
|
6
|
+
type AppState,
|
|
7
|
+
} from "../core/state/app/state";
|
|
8
|
+
import type { TickerRepository } from "../data/ticker-repository";
|
|
9
|
+
import { createDefaultConfig } from "../types/config";
|
|
10
|
+
import {
|
|
11
|
+
__syncContributorInternalsForTests,
|
|
12
|
+
coreConfigSyncContributor,
|
|
13
|
+
} from "./core-contributors";
|
|
14
|
+
import { CloudSyncController } from "./controller";
|
|
15
|
+
import {
|
|
16
|
+
SYNC_SNAPSHOT_SCHEMA_VERSION,
|
|
17
|
+
type SyncContributor,
|
|
18
|
+
type SyncSnapshot,
|
|
19
|
+
type SyncSnapshotResponse,
|
|
20
|
+
type SyncTransport,
|
|
21
|
+
} from "./types";
|
|
22
|
+
|
|
23
|
+
test("does not push local state when the initial pull fails", async () => {
|
|
24
|
+
let pushes = 0;
|
|
25
|
+
const transport: SyncTransport = {
|
|
26
|
+
id: "failing-pull",
|
|
27
|
+
isAvailable: () => true,
|
|
28
|
+
pullSnapshot: () => Promise.reject(new Error("pull failed")),
|
|
29
|
+
pushSnapshot: async () => {
|
|
30
|
+
pushes += 1;
|
|
31
|
+
return { revision: 1, updatedAt: new Date().toISOString() };
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const controller = new CloudSyncController();
|
|
35
|
+
controller.setRuntime({
|
|
36
|
+
getState: () => ({} as AppState),
|
|
37
|
+
dispatch: () => {},
|
|
38
|
+
tickerRepository: {} as TickerRepository,
|
|
39
|
+
getContributors: () => [],
|
|
40
|
+
getTransport: () => ({ pluginId: "test", transport }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await controller.requestSync({ force: true });
|
|
44
|
+
|
|
45
|
+
expect(pushes).toBe(0);
|
|
46
|
+
expect(controller.getStatus()).toMatchObject({ phase: "error", error: "pull failed" });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("keeps startup layout changes while serializing pull and push", async () => {
|
|
50
|
+
let state = createInitialState(createDefaultConfig("/tmp/gloomberb-sync-controller-test"));
|
|
51
|
+
const dispatch = (action: AppAction) => {
|
|
52
|
+
state = appReducer(state, action);
|
|
53
|
+
};
|
|
54
|
+
let resolvePull!: (response: SyncSnapshotResponse) => void;
|
|
55
|
+
const deferredPull = new Promise<SyncSnapshotResponse>((resolve) => {
|
|
56
|
+
resolvePull = resolve;
|
|
57
|
+
});
|
|
58
|
+
let pulls = 0;
|
|
59
|
+
const pushes: Array<{
|
|
60
|
+
snapshot: SyncSnapshot;
|
|
61
|
+
options?: { baseRevision?: number | null };
|
|
62
|
+
}> = [];
|
|
63
|
+
const transport: SyncTransport = {
|
|
64
|
+
id: "deferred-pull",
|
|
65
|
+
isAvailable: () => true,
|
|
66
|
+
pullSnapshot: () => {
|
|
67
|
+
pulls += 1;
|
|
68
|
+
return deferredPull;
|
|
69
|
+
},
|
|
70
|
+
pushSnapshot: async (snapshot, options) => {
|
|
71
|
+
pushes.push({ snapshot, options });
|
|
72
|
+
return { revision: 8, updatedAt: "2026-07-13T10:00:08.000Z" };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const contributor: SyncContributor = {
|
|
76
|
+
...coreConfigSyncContributor,
|
|
77
|
+
apply: (payload, context) => {
|
|
78
|
+
const config = __syncContributorInternalsForTests.mergeConfigPayload(
|
|
79
|
+
context.state.config,
|
|
80
|
+
payload,
|
|
81
|
+
context.baselineState.config,
|
|
82
|
+
);
|
|
83
|
+
if (config) context.dispatch({ type: "SET_CONFIG", config });
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const controller = new CloudSyncController();
|
|
87
|
+
controller.setRuntime({
|
|
88
|
+
getState: () => state,
|
|
89
|
+
dispatch,
|
|
90
|
+
tickerRepository: {} as TickerRepository,
|
|
91
|
+
getContributors: () => [{ pluginId: "test", contributor }],
|
|
92
|
+
getTransport: () => ({ pluginId: "test", transport }),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const startupSync = controller.requestSync({ reason: "startup" });
|
|
96
|
+
const startupPane = {
|
|
97
|
+
instanceId: "help:startup",
|
|
98
|
+
paneId: "help",
|
|
99
|
+
binding: { kind: "none" as const },
|
|
100
|
+
};
|
|
101
|
+
const localLayout = {
|
|
102
|
+
...state.config.layout,
|
|
103
|
+
instances: [...state.config.layout.instances, startupPane],
|
|
104
|
+
floating: [
|
|
105
|
+
...state.config.layout.floating,
|
|
106
|
+
{ instanceId: startupPane.instanceId, x: 4, y: 3, width: 60, height: 20 },
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
dispatch({
|
|
110
|
+
type: "SET_CONFIG",
|
|
111
|
+
config: {
|
|
112
|
+
...state.config,
|
|
113
|
+
layout: localLayout,
|
|
114
|
+
layouts: state.config.layouts.map((saved, index) => (
|
|
115
|
+
index === state.config.activeLayoutIndex ? { ...saved, layout: localLayout } : saved
|
|
116
|
+
)),
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const queuedPush = controller.requestSync({ reason: "state-change" });
|
|
120
|
+
|
|
121
|
+
const remoteConfig = createDefaultConfig("/remote/path-is-not-synced");
|
|
122
|
+
remoteConfig.theme = "green";
|
|
123
|
+
resolvePull({
|
|
124
|
+
snapshot: {
|
|
125
|
+
schemaVersion: SYNC_SNAPSHOT_SCHEMA_VERSION,
|
|
126
|
+
appId: "gloomberb",
|
|
127
|
+
clientId: "remote-client",
|
|
128
|
+
createdAt: "2026-07-13T10:00:07.000Z",
|
|
129
|
+
contributors: {
|
|
130
|
+
"core.config": {
|
|
131
|
+
schemaVersion: 1,
|
|
132
|
+
updatedAt: "2026-07-13T10:00:07.000Z",
|
|
133
|
+
payload: __syncContributorInternalsForTests.collectCoreConfigPayload(remoteConfig),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
revision: 7,
|
|
138
|
+
updatedAt: "2026-07-13T10:00:07.000Z",
|
|
139
|
+
});
|
|
140
|
+
await Promise.all([startupSync, queuedPush]);
|
|
141
|
+
|
|
142
|
+
const pushedConfig = pushes[0]?.snapshot.contributors["core.config"]?.payload as {
|
|
143
|
+
layout: AppState["config"]["layout"];
|
|
144
|
+
};
|
|
145
|
+
expect(pulls).toBe(1);
|
|
146
|
+
expect(pushes).toHaveLength(1);
|
|
147
|
+
expect(pushes[0]?.options).toEqual({ baseRevision: 7 });
|
|
148
|
+
expect(state.config.theme).toBe("green");
|
|
149
|
+
expect(state.config.layout.instances).toContainEqual(startupPane);
|
|
150
|
+
expect(pushedConfig.layout.instances).toContainEqual(startupPane);
|
|
151
|
+
});
|