gloomberb 0.9.8 → 0.9.9
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/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 +31 -0
- package/src/sync/controller.ts +21 -17
- 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,31 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import type { AppState } from "../core/state/app/state";
|
|
3
|
+
import type { TickerRepository } from "../data/ticker-repository";
|
|
4
|
+
import { CloudSyncController } from "./controller";
|
|
5
|
+
import type { SyncTransport } from "./types";
|
|
6
|
+
|
|
7
|
+
test("does not push local state when the initial pull fails", async () => {
|
|
8
|
+
let pushes = 0;
|
|
9
|
+
const transport: SyncTransport = {
|
|
10
|
+
id: "failing-pull",
|
|
11
|
+
isAvailable: () => true,
|
|
12
|
+
pullSnapshot: () => Promise.reject(new Error("pull failed")),
|
|
13
|
+
pushSnapshot: async () => {
|
|
14
|
+
pushes += 1;
|
|
15
|
+
return { revision: 1, updatedAt: new Date().toISOString() };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
const controller = new CloudSyncController();
|
|
19
|
+
controller.setRuntime({
|
|
20
|
+
state: {} as AppState,
|
|
21
|
+
dispatch: () => {},
|
|
22
|
+
tickerRepository: {} as TickerRepository,
|
|
23
|
+
getContributors: () => [],
|
|
24
|
+
getTransport: () => ({ pluginId: "test", transport }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await controller.requestSync({ force: true });
|
|
28
|
+
|
|
29
|
+
expect(pushes).toBe(0);
|
|
30
|
+
expect(controller.getStatus()).toMatchObject({ phase: "error", error: "pull failed" });
|
|
31
|
+
});
|
package/src/sync/controller.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Dispatch } from "react";
|
|
2
2
|
import type { AppAction, AppState } from "../core/state/app/state";
|
|
3
3
|
import type { TickerRepository } from "../data/ticker-repository";
|
|
4
|
+
import { stableStringify } from "../remote/revision";
|
|
4
5
|
import {
|
|
5
6
|
SYNC_SNAPSHOT_SCHEMA_VERSION,
|
|
6
7
|
type RegisteredSyncContributor,
|
|
@@ -37,15 +38,6 @@ function nowIso(): string {
|
|
|
37
38
|
return new Date().toISOString();
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function stableStringify(value: unknown): string {
|
|
41
|
-
if (value == null || typeof value !== "object") return JSON.stringify(value);
|
|
42
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
43
|
-
const entries = Object.entries(value as Record<string, unknown>)
|
|
44
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
45
|
-
.map(([key, child]) => `${JSON.stringify(key)}:${stableStringify(child)}`);
|
|
46
|
-
return `{${entries.join(",")}}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
41
|
function resolveClientId(): string {
|
|
50
42
|
const storage = globalThis.localStorage;
|
|
51
43
|
const existing = storage?.getItem(CLIENT_ID_STORAGE_KEY);
|
|
@@ -55,13 +47,14 @@ function resolveClientId(): string {
|
|
|
55
47
|
return id;
|
|
56
48
|
}
|
|
57
49
|
|
|
58
|
-
class CloudSyncController {
|
|
50
|
+
export class CloudSyncController {
|
|
59
51
|
private runtime: SyncRuntime | null = null;
|
|
60
52
|
private contributors = new Map<string, RegisteredSyncContributor>();
|
|
61
53
|
private transports = new Map<string, RegisteredSyncTransport>();
|
|
62
54
|
private listeners = new Set<() => void>();
|
|
63
55
|
private pushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
64
56
|
private inFlight: Promise<void> | null = null;
|
|
57
|
+
private syncQueued = false;
|
|
65
58
|
private clientId: string | null = null;
|
|
66
59
|
private lastSignature: string | null = null;
|
|
67
60
|
private lastPulledAtMs = 0;
|
|
@@ -116,6 +109,7 @@ class CloudSyncController {
|
|
|
116
109
|
this.runtime = null;
|
|
117
110
|
if (this.pushTimer) clearTimeout(this.pushTimer);
|
|
118
111
|
this.pushTimer = null;
|
|
112
|
+
this.syncQueued = false;
|
|
119
113
|
}
|
|
120
114
|
|
|
121
115
|
subscribe(listener: () => void): () => void {
|
|
@@ -141,14 +135,21 @@ class CloudSyncController {
|
|
|
141
135
|
}
|
|
142
136
|
if (this.pushTimer) clearTimeout(this.pushTimer);
|
|
143
137
|
this.pushTimer = setTimeout(() => {
|
|
138
|
+
this.pushTimer = null;
|
|
144
139
|
void this.requestSync({ reason });
|
|
145
140
|
}, PUSH_DEBOUNCE_MS);
|
|
146
141
|
}
|
|
147
142
|
|
|
148
143
|
async requestSync(options: { reason?: string; force?: boolean } = {}): Promise<void> {
|
|
149
|
-
if (this.inFlight)
|
|
150
|
-
|
|
144
|
+
if (this.inFlight) {
|
|
145
|
+
this.syncQueued = true;
|
|
146
|
+
return this.inFlight;
|
|
147
|
+
}
|
|
148
|
+
const run = this.syncOnce(options).finally(async () => {
|
|
151
149
|
this.inFlight = null;
|
|
150
|
+
if (!this.syncQueued) return;
|
|
151
|
+
this.syncQueued = false;
|
|
152
|
+
await this.requestSync({ reason: "queued-state-change" });
|
|
152
153
|
});
|
|
153
154
|
this.inFlight = run;
|
|
154
155
|
return run;
|
|
@@ -164,8 +165,9 @@ class CloudSyncController {
|
|
|
164
165
|
}
|
|
165
166
|
const currentMs = Date.now();
|
|
166
167
|
if (!options.force && currentMs - this.lastPulledAtMs < PULL_MIN_INTERVAL_MS) return;
|
|
167
|
-
this.
|
|
168
|
-
|
|
168
|
+
if (await this.runPull(runtime, transport.transport)) {
|
|
169
|
+
this.lastPulledAtMs = currentMs;
|
|
170
|
+
}
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
private async syncOnce(options: { reason?: string; force?: boolean }): Promise<void> {
|
|
@@ -178,7 +180,7 @@ class CloudSyncController {
|
|
|
178
180
|
}
|
|
179
181
|
const transport = transportRegistration.transport;
|
|
180
182
|
if (!this.hasPulledForTransport.has(transport.id)) {
|
|
181
|
-
await this.runPull(runtime, transport);
|
|
183
|
+
if (!await this.runPull(runtime, transport)) return;
|
|
182
184
|
this.hasPulledForTransport.add(transport.id);
|
|
183
185
|
}
|
|
184
186
|
|
|
@@ -206,7 +208,7 @@ class CloudSyncController {
|
|
|
206
208
|
}
|
|
207
209
|
}
|
|
208
210
|
|
|
209
|
-
private async runPull(runtime: SyncRuntime, transport: SyncTransport): Promise<
|
|
211
|
+
private async runPull(runtime: SyncRuntime, transport: SyncTransport): Promise<boolean> {
|
|
210
212
|
this.setStatus({ phase: "syncing", transportId: transport.id, error: null });
|
|
211
213
|
try {
|
|
212
214
|
const response = await transport.pullSnapshot();
|
|
@@ -218,7 +220,7 @@ class CloudSyncController {
|
|
|
218
220
|
lastSyncAt: response.updatedAt,
|
|
219
221
|
error: null,
|
|
220
222
|
});
|
|
221
|
-
if (!response.snapshot) return;
|
|
223
|
+
if (!response.snapshot) return true;
|
|
222
224
|
for (const entry of runtime.getContributors()) {
|
|
223
225
|
const payload = response.snapshot.contributors[entry.contributor.id]?.payload;
|
|
224
226
|
if (payload === undefined || !entry.contributor.apply) continue;
|
|
@@ -229,12 +231,14 @@ class CloudSyncController {
|
|
|
229
231
|
tickerRepository: runtime.tickerRepository,
|
|
230
232
|
});
|
|
231
233
|
}
|
|
234
|
+
return true;
|
|
232
235
|
} catch (error) {
|
|
233
236
|
this.setStatus({
|
|
234
237
|
phase: "error",
|
|
235
238
|
transportId: transport.id,
|
|
236
239
|
error: error instanceof Error ? error.message : String(error),
|
|
237
240
|
});
|
|
241
|
+
return false;
|
|
238
242
|
}
|
|
239
243
|
}
|
|
240
244
|
|
|
@@ -14,7 +14,6 @@ export function createTestDataProvider(overrides: Partial<DataProvider> = {}): D
|
|
|
14
14
|
getQuote: async () => unused<Quote>("getQuote"),
|
|
15
15
|
getExchangeRate: async () => 1,
|
|
16
16
|
search: async () => [] satisfies InstrumentSearchResult[],
|
|
17
|
-
getNews: async () => [],
|
|
18
17
|
getArticleSummary: async () => null,
|
|
19
18
|
getPriceHistory: async () => [],
|
|
20
19
|
...overrides,
|
package/src/ui/host.tsx
CHANGED
|
@@ -101,6 +101,7 @@ interface ScrollBarRenderable {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export interface ScrollBoxRenderable {
|
|
104
|
+
width?: number;
|
|
104
105
|
scrollTop: number;
|
|
105
106
|
scrollLeft?: number;
|
|
106
107
|
scrollHeight: number;
|
|
@@ -272,7 +273,7 @@ export interface HostCheckboxProps {
|
|
|
272
273
|
disabled?: boolean;
|
|
273
274
|
active?: boolean;
|
|
274
275
|
description?: string;
|
|
275
|
-
width?: number;
|
|
276
|
+
width?: number | string;
|
|
276
277
|
variant?: "default" | "desktop";
|
|
277
278
|
}
|
|
278
279
|
|
package/src/ui/index.tsx
CHANGED
package/src/updater.test.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { tmpdir } from "os";
|
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { gzipSync } from "zlib";
|
|
6
6
|
import {
|
|
7
|
-
canSelfUpdate,
|
|
8
7
|
checkForUpdate,
|
|
9
8
|
checkForUpdateDetailed,
|
|
10
9
|
detectUpdateAction,
|
|
@@ -153,26 +152,6 @@ describe("resolveSelfUpdateTargetPath", () => {
|
|
|
153
152
|
});
|
|
154
153
|
});
|
|
155
154
|
|
|
156
|
-
describe("canSelfUpdate", () => {
|
|
157
|
-
test("returns false for manual update releases", () => {
|
|
158
|
-
expect(canSelfUpdate({
|
|
159
|
-
updateAction: { kind: "manual", command: "bun install -g gloomberb@latest" },
|
|
160
|
-
})).toBe(false);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("returns true for self-update releases", () => {
|
|
164
|
-
expect(canSelfUpdate({
|
|
165
|
-
updateAction: { kind: "self" },
|
|
166
|
-
})).toBe(true);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("returns true for desktop update releases", () => {
|
|
170
|
-
expect(canSelfUpdate({
|
|
171
|
-
updateAction: { kind: "desktop" },
|
|
172
|
-
})).toBe(true);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
155
|
describe("update host", () => {
|
|
177
156
|
test("delegates update checks to an installed host", async () => {
|
|
178
157
|
setUpdateHost({
|