gloomberb 0.3.3 → 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 +27 -16
- 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-renderer.ts +7 -2
- 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/chart.test.ts +26 -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 +484 -112
- 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 +75 -1
- package/src/plugins/ibkr/gateway-service.ts +179 -9
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/chart.test.tsx +139 -0
- package/src/plugins/prediction-markets/chart.tsx +65 -436
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +17 -5
- package/src/plugins/prediction-markets/controller.ts +141 -121
- package/src/plugins/prediction-markets/detail/outcomes.tsx +2 -6
- package/src/plugins/prediction-markets/detail/pane.tsx +4 -25
- package/src/plugins/prediction-markets/detail/shared.tsx +0 -1
- package/src/plugins/prediction-markets/detail.test.tsx +4 -0
- 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 +160 -58
- package/src/plugins/prediction-markets/pane.tsx +74 -183
- 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 +308 -3
- package/src/sources/provider-router.ts +247 -17
- 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/price-history.test.ts +25 -0
- package/src/utils/price-history.ts +29 -5
- 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
|
@@ -25,6 +25,11 @@ import { tmpdir } from "os";
|
|
|
25
25
|
import { join } from "path";
|
|
26
26
|
import { firstValueFrom, filter, take, timeout } from "rxjs";
|
|
27
27
|
import type { TimeRange } from "../../components/chart/chart-types";
|
|
28
|
+
import {
|
|
29
|
+
normalizeChartResolutionSupport,
|
|
30
|
+
type ChartResolutionSupport,
|
|
31
|
+
type ManualChartResolution,
|
|
32
|
+
} from "../../components/chart/chart-resolution";
|
|
28
33
|
import type { BrokerConnectionStatus, BrokerPosition } from "../../types/broker";
|
|
29
34
|
import type { QuoteSubscriptionTarget } from "../../types/data-provider";
|
|
30
35
|
import type { Fundamentals, FinancialStatement, Quote, PricePoint, TickerFinancials } from "../../types/financials";
|
|
@@ -290,6 +295,7 @@ const IBKR_DATA_TIMEOUT = 8_000;
|
|
|
290
295
|
const IBKR_QUOTE_STREAM_TICKS = "165,221,233";
|
|
291
296
|
|
|
292
297
|
const HISTORY_PARAMS: Record<TimeRange, { duration: string; size: BarSizeSetting }> = {
|
|
298
|
+
"1D": { duration: "1 D", size: BarSizeSetting.MINUTES_FIVE },
|
|
293
299
|
"1W": { duration: "7 D", size: BarSizeSetting.HOURS_ONE },
|
|
294
300
|
"1M": { duration: "1 M", size: BarSizeSetting.HOURS_ONE },
|
|
295
301
|
"3M": { duration: "3 M", size: BarSizeSetting.DAYS_ONE },
|
|
@@ -299,6 +305,17 @@ const HISTORY_PARAMS: Record<TimeRange, { duration: string; size: BarSizeSetting
|
|
|
299
305
|
"ALL": { duration: "10 Y", size: BarSizeSetting.MONTHS_ONE },
|
|
300
306
|
};
|
|
301
307
|
|
|
308
|
+
const IBKR_RESOLUTION_SUPPORT = normalizeChartResolutionSupport([
|
|
309
|
+
{ resolution: "1m", maxRange: "1W" },
|
|
310
|
+
{ resolution: "5m", maxRange: "1M" },
|
|
311
|
+
{ resolution: "15m", maxRange: "3M" },
|
|
312
|
+
{ resolution: "30m", maxRange: "6M" },
|
|
313
|
+
{ resolution: "1h", maxRange: "1Y" },
|
|
314
|
+
{ resolution: "1d", maxRange: "ALL" },
|
|
315
|
+
{ resolution: "1wk", maxRange: "ALL" },
|
|
316
|
+
{ resolution: "1mo", maxRange: "ALL" },
|
|
317
|
+
]);
|
|
318
|
+
|
|
302
319
|
/** Map generic bar size labels to IBKR BarSizeSetting values. */
|
|
303
320
|
const GENERIC_BAR_SIZE_MAP: Record<string, BarSizeSetting> = {
|
|
304
321
|
"1m": BarSizeSetting.MINUTES_ONE,
|
|
@@ -307,9 +324,46 @@ const GENERIC_BAR_SIZE_MAP: Record<string, BarSizeSetting> = {
|
|
|
307
324
|
"30m": BarSizeSetting.MINUTES_THIRTY,
|
|
308
325
|
"1h": BarSizeSetting.HOURS_ONE,
|
|
309
326
|
"1d": BarSizeSetting.DAYS_ONE,
|
|
310
|
-
"
|
|
327
|
+
"1wk": BarSizeSetting.WEEKS_ONE,
|
|
328
|
+
"1mo": BarSizeSetting.MONTHS_ONE,
|
|
311
329
|
};
|
|
312
330
|
|
|
331
|
+
export function parseIbkrHistoricalBarTime(value: string | number): Date {
|
|
332
|
+
if (typeof value === "number") {
|
|
333
|
+
if (!Number.isFinite(value)) return new Date(Number.NaN);
|
|
334
|
+
|
|
335
|
+
const compactDate = String(Math.trunc(value));
|
|
336
|
+
if (/^\d{8}$/.test(compactDate)) {
|
|
337
|
+
return parseIbkrHistoricalBarTime(compactDate);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return new Date(value > 10_000_000_000 ? value : value * 1000);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const trimmed = value.trim();
|
|
344
|
+
if (!trimmed) return new Date(Number.NaN);
|
|
345
|
+
|
|
346
|
+
const parsed = Date.parse(trimmed);
|
|
347
|
+
if (Number.isFinite(parsed)) {
|
|
348
|
+
return new Date(parsed);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const compactMatch = trimmed.match(/^(\d{4})(\d{2})(\d{2})(?:\D+(\d{2}):(\d{2}):(\d{2}))?/);
|
|
352
|
+
if (!compactMatch) {
|
|
353
|
+
return new Date(Number.NaN);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText] = compactMatch;
|
|
357
|
+
const year = Number(yearText);
|
|
358
|
+
const month = Number(monthText) - 1;
|
|
359
|
+
const day = Number(dayText);
|
|
360
|
+
const hour = Number(hourText ?? "0");
|
|
361
|
+
const minute = Number(minuteText ?? "0");
|
|
362
|
+
const second = Number(secondText ?? "0");
|
|
363
|
+
|
|
364
|
+
return new Date(year, month, day, hour, minute, second);
|
|
365
|
+
}
|
|
366
|
+
|
|
313
367
|
function normalizeQuoteStreamTarget(target: QuoteSubscriptionTarget): QuoteSubscriptionTarget | null {
|
|
314
368
|
const symbol = target.symbol.trim().toUpperCase();
|
|
315
369
|
if (!symbol) return null;
|
|
@@ -347,6 +401,65 @@ function hasDelayedMarketData(
|
|
|
347
401
|
].some((tickType) => marketData.has(tickType));
|
|
348
402
|
}
|
|
349
403
|
|
|
404
|
+
function firstNonSmartExchange(value?: string): string | undefined {
|
|
405
|
+
return value
|
|
406
|
+
?.split(",")
|
|
407
|
+
.map((entry) => entry.trim())
|
|
408
|
+
.find((entry) => entry.length > 0 && entry !== "SMART");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function resolveIbkrListingExchange(
|
|
412
|
+
current: Quote | undefined,
|
|
413
|
+
contract: Contract,
|
|
414
|
+
details: ContractDetails,
|
|
415
|
+
): string | undefined {
|
|
416
|
+
return current?.listingExchangeName
|
|
417
|
+
?? contract.primaryExch
|
|
418
|
+
?? firstNonSmartExchange(details.validExchanges)
|
|
419
|
+
?? (current?.exchangeName !== "SMART" ? current?.exchangeName : undefined)
|
|
420
|
+
?? (contract.exchange !== "SMART" ? contract.exchange : undefined);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function resolveIbkrRoutingExchange(current: Quote | undefined, contract: Contract): string | undefined {
|
|
424
|
+
return current?.routingExchangeName
|
|
425
|
+
?? contract.exchange
|
|
426
|
+
?? "SMART";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function ibkrVenueFields(
|
|
430
|
+
current: Quote | undefined,
|
|
431
|
+
contract: Contract,
|
|
432
|
+
details: ContractDetails,
|
|
433
|
+
): Pick<
|
|
434
|
+
Quote,
|
|
435
|
+
| "exchangeName"
|
|
436
|
+
| "fullExchangeName"
|
|
437
|
+
| "listingExchangeName"
|
|
438
|
+
| "listingExchangeFullName"
|
|
439
|
+
| "routingExchangeName"
|
|
440
|
+
| "routingExchangeFullName"
|
|
441
|
+
| "marketState"
|
|
442
|
+
| "sessionConfidence"
|
|
443
|
+
> {
|
|
444
|
+
const listingExchangeName = resolveIbkrListingExchange(current, contract, details);
|
|
445
|
+
const listingExchangeFullName = current?.listingExchangeFullName
|
|
446
|
+
?? current?.fullExchangeName
|
|
447
|
+
?? listingExchangeName;
|
|
448
|
+
const routingExchangeName = resolveIbkrRoutingExchange(current, contract);
|
|
449
|
+
const routingExchangeFullName = current?.routingExchangeFullName ?? routingExchangeName;
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
exchangeName: listingExchangeName,
|
|
453
|
+
fullExchangeName: listingExchangeFullName,
|
|
454
|
+
listingExchangeName,
|
|
455
|
+
listingExchangeFullName,
|
|
456
|
+
routingExchangeName,
|
|
457
|
+
routingExchangeFullName,
|
|
458
|
+
marketState: current?.marketState,
|
|
459
|
+
sessionConfidence: current?.sessionConfidence ?? "unknown",
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
350
463
|
interface TickByTickBidAskUpdate {
|
|
351
464
|
time: number;
|
|
352
465
|
bidPrice: number;
|
|
@@ -381,9 +494,7 @@ export function applyTickByTickAllLastToQuote(
|
|
|
381
494
|
volume: current?.volume,
|
|
382
495
|
name: current?.name ?? details.longName ?? details.marketName ?? contract.symbol,
|
|
383
496
|
lastUpdated: tick.time ? tick.time * 1000 : Date.now(),
|
|
384
|
-
|
|
385
|
-
fullExchangeName: current?.fullExchangeName ?? details.validExchanges?.split(",")[0],
|
|
386
|
-
marketState: current?.marketState ?? "REGULAR",
|
|
497
|
+
...ibkrVenueFields(current, contract, details),
|
|
387
498
|
preMarketPrice: current?.preMarketPrice,
|
|
388
499
|
preMarketChange: current?.preMarketChange,
|
|
389
500
|
preMarketChangePercent: current?.preMarketChangePercent,
|
|
@@ -1006,7 +1117,68 @@ export class IbkrGatewayService {
|
|
|
1006
1117
|
const priceDivisor = getIbkrPriceDivisor(contract, details);
|
|
1007
1118
|
|
|
1008
1119
|
return bars.map((bar) => ({
|
|
1009
|
-
date:
|
|
1120
|
+
date: parseIbkrHistoricalBarTime(bar.time),
|
|
1121
|
+
open: normalizeIbkrPriceValue(bar.open, priceDivisor),
|
|
1122
|
+
high: normalizeIbkrPriceValue(bar.high, priceDivisor),
|
|
1123
|
+
low: normalizeIbkrPriceValue(bar.low, priceDivisor),
|
|
1124
|
+
close: normalizeIbkrPriceValue(bar.close ?? bar.open ?? bar.high ?? bar.low ?? 0, priceDivisor) ?? 0,
|
|
1125
|
+
volume: bar.volume,
|
|
1126
|
+
}));
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
getChartResolutionSupport(
|
|
1131
|
+
_ticker: string,
|
|
1132
|
+
_config: IbkrGatewayConfig,
|
|
1133
|
+
_exchange?: string,
|
|
1134
|
+
_instrument?: BrokerContractRef | null,
|
|
1135
|
+
): ChartResolutionSupport[] {
|
|
1136
|
+
return IBKR_RESOLUTION_SUPPORT;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
getChartResolutionCapabilities(
|
|
1140
|
+
_ticker: string,
|
|
1141
|
+
_config: IbkrGatewayConfig,
|
|
1142
|
+
_exchange?: string,
|
|
1143
|
+
_instrument?: BrokerContractRef | null,
|
|
1144
|
+
): ManualChartResolution[] {
|
|
1145
|
+
return IBKR_RESOLUTION_SUPPORT.map((entry) => entry.resolution);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async getPriceHistoryForResolution(
|
|
1149
|
+
ticker: string,
|
|
1150
|
+
config: IbkrGatewayConfig,
|
|
1151
|
+
exchange: string,
|
|
1152
|
+
bufferRange: TimeRange,
|
|
1153
|
+
resolution: ManualChartResolution,
|
|
1154
|
+
instrument?: BrokerContractRef | null,
|
|
1155
|
+
): Promise<PricePoint[]> {
|
|
1156
|
+
const ibkrBarSize = GENERIC_BAR_SIZE_MAP[resolution];
|
|
1157
|
+
if (!ibkrBarSize) return [];
|
|
1158
|
+
|
|
1159
|
+
return this.dedup(`history:${ticker}:${exchange}:${bufferRange}:${resolution}`, async () => {
|
|
1160
|
+
await this.connect(config);
|
|
1161
|
+
const contract = await withTimeout(this.resolveContract(ticker, exchange, instrument ?? null), IBKR_DATA_TIMEOUT, "resolveContract");
|
|
1162
|
+
const params = HISTORY_PARAMS[bufferRange];
|
|
1163
|
+
const detailsPromise = withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails")
|
|
1164
|
+
.catch(() => undefined);
|
|
1165
|
+
const bars = await this.withMarketDataFallback(
|
|
1166
|
+
config,
|
|
1167
|
+
() => withTimeout(this.api!.getHistoricalData(
|
|
1168
|
+
contract,
|
|
1169
|
+
"",
|
|
1170
|
+
params.duration,
|
|
1171
|
+
ibkrBarSize,
|
|
1172
|
+
WhatToShow.TRADES,
|
|
1173
|
+
1,
|
|
1174
|
+
1,
|
|
1175
|
+
), IBKR_DATA_TIMEOUT, "getHistoricalData"),
|
|
1176
|
+
);
|
|
1177
|
+
const details = await detailsPromise;
|
|
1178
|
+
const priceDivisor = getIbkrPriceDivisor(contract, details);
|
|
1179
|
+
|
|
1180
|
+
return bars.map((bar) => ({
|
|
1181
|
+
date: parseIbkrHistoricalBarTime(bar.time),
|
|
1010
1182
|
open: normalizeIbkrPriceValue(bar.open, priceDivisor),
|
|
1011
1183
|
high: normalizeIbkrPriceValue(bar.high, priceDivisor),
|
|
1012
1184
|
low: normalizeIbkrPriceValue(bar.low, priceDivisor),
|
|
@@ -1064,7 +1236,7 @@ export class IbkrGatewayService {
|
|
|
1064
1236
|
const priceDivisor = getIbkrPriceDivisor(contract, details);
|
|
1065
1237
|
|
|
1066
1238
|
return bars.map((bar) => ({
|
|
1067
|
-
date:
|
|
1239
|
+
date: parseIbkrHistoricalBarTime(bar.time),
|
|
1068
1240
|
open: normalizeIbkrPriceValue(bar.open, priceDivisor),
|
|
1069
1241
|
high: normalizeIbkrPriceValue(bar.high, priceDivisor),
|
|
1070
1242
|
low: normalizeIbkrPriceValue(bar.low, priceDivisor),
|
|
@@ -2114,9 +2286,7 @@ export class IbkrGatewayService {
|
|
|
2114
2286
|
volume: marketData.get(IBApiTickType.VOLUME)?.value ?? marketData.get(IBApiTickType.DELAYED_VOLUME)?.value,
|
|
2115
2287
|
name: details.longName || details.marketName || contract.symbol,
|
|
2116
2288
|
lastUpdated: ingressTm,
|
|
2117
|
-
|
|
2118
|
-
fullExchangeName: details.validExchanges?.split(",")[0],
|
|
2119
|
-
marketState: "REGULAR",
|
|
2289
|
+
...ibkrVenueFields(undefined, contract, details),
|
|
2120
2290
|
bid: normalizeIbkrPriceValue(
|
|
2121
2291
|
marketData.get(IBApiTickType.BID)?.value ?? marketData.get(IBApiTickType.DELAYED_BID)?.value,
|
|
2122
2292
|
priceDivisor,
|
|
@@ -71,7 +71,7 @@ describe("pane-manager split-tree drops", () => {
|
|
|
71
71
|
const next = gridlockAllPanes(layout);
|
|
72
72
|
|
|
73
73
|
expect(next.floating).toHaveLength(0);
|
|
74
|
-
expect(getDockedPaneIds(next)).toHaveLength(
|
|
74
|
+
expect(getDockedPaneIds(next)).toHaveLength(5);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
test("gridlock infers a matching tiled layout from arranged windows", () => {
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
createContext,
|
|
3
3
|
useCallback,
|
|
4
4
|
useContext,
|
|
5
|
+
useRef,
|
|
5
6
|
useSyncExternalStore,
|
|
6
7
|
type ReactNode,
|
|
7
8
|
type SetStateAction,
|
|
@@ -101,12 +102,21 @@ export function usePluginPaneState<T>(key: string, fallback: T, paneId?: string)
|
|
|
101
102
|
const { pluginId } = usePluginRenderContext();
|
|
102
103
|
const scopedPaneId = paneId ?? usePaneInstanceId();
|
|
103
104
|
const { state, dispatch } = useAppState();
|
|
105
|
+
const stateRef = useRef(state);
|
|
106
|
+
const fallbackRef = useRef(fallback);
|
|
107
|
+
stateRef.current = state;
|
|
108
|
+
fallbackRef.current = fallback;
|
|
104
109
|
const paneState = state.paneState[scopedPaneId];
|
|
105
110
|
const value = getPluginPaneStateValue(paneState, pluginId, key, fallback);
|
|
106
111
|
|
|
107
112
|
const setValue = useCallback((nextValue: SetStateAction<T>) => {
|
|
108
|
-
const currentPaneState =
|
|
109
|
-
const currentValue = getPluginPaneStateValue(
|
|
113
|
+
const currentPaneState = stateRef.current.paneState[scopedPaneId];
|
|
114
|
+
const currentValue = getPluginPaneStateValue(
|
|
115
|
+
currentPaneState,
|
|
116
|
+
pluginId,
|
|
117
|
+
key,
|
|
118
|
+
fallbackRef.current,
|
|
119
|
+
);
|
|
110
120
|
const resolved = typeof nextValue === "function"
|
|
111
121
|
? (nextValue as (previousValue: T) => T)(currentValue)
|
|
112
122
|
: nextValue;
|
|
@@ -122,7 +132,7 @@ export function usePluginPaneState<T>(key: string, fallback: T, paneId?: string)
|
|
|
122
132
|
key,
|
|
123
133
|
value: resolved,
|
|
124
134
|
});
|
|
125
|
-
}, [dispatch,
|
|
135
|
+
}, [dispatch, key, pluginId, scopedPaneId]);
|
|
126
136
|
|
|
127
137
|
return [value, setValue];
|
|
128
138
|
}
|
|
@@ -130,6 +140,8 @@ export function usePluginPaneState<T>(key: string, fallback: T, paneId?: string)
|
|
|
130
140
|
export function usePluginState<T>(key: string, fallback: T, options?: { schemaVersion?: number }): [T, (value: SetStateAction<T>) => void] {
|
|
131
141
|
const { pluginId, runtime } = usePluginRenderContext();
|
|
132
142
|
const schemaVersion = options?.schemaVersion;
|
|
143
|
+
const fallbackRef = useRef(fallback);
|
|
144
|
+
fallbackRef.current = fallback;
|
|
133
145
|
|
|
134
146
|
const subscribe = useCallback((listener: () => void) => (
|
|
135
147
|
runtime.subscribeResumeState(pluginId, key, listener)
|
|
@@ -142,7 +154,9 @@ export function usePluginState<T>(key: string, fallback: T, options?: { schemaVe
|
|
|
142
154
|
const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
143
155
|
|
|
144
156
|
const setValue = useCallback((nextValue: SetStateAction<T>) => {
|
|
145
|
-
const currentValue =
|
|
157
|
+
const currentValue =
|
|
158
|
+
runtime.getResumeState<T>(pluginId, key, schemaVersion) ??
|
|
159
|
+
fallbackRef.current;
|
|
146
160
|
const resolved = typeof nextValue === "function"
|
|
147
161
|
? (nextValue as (previousValue: T) => T)(currentValue)
|
|
148
162
|
: nextValue;
|
|
@@ -150,7 +164,7 @@ export function usePluginState<T>(key: string, fallback: T, options?: { schemaVe
|
|
|
150
164
|
return;
|
|
151
165
|
}
|
|
152
166
|
runtime.setResumeState(pluginId, key, resolved, schemaVersion);
|
|
153
|
-
}, [
|
|
167
|
+
}, [key, pluginId, runtime, schemaVersion]);
|
|
154
168
|
|
|
155
169
|
return [value, setValue];
|
|
156
170
|
}
|
|
@@ -158,10 +172,16 @@ export function usePluginState<T>(key: string, fallback: T, options?: { schemaVe
|
|
|
158
172
|
export function usePluginConfigState<T>(key: string, fallback: T): [T, (value: SetStateAction<T>) => void] {
|
|
159
173
|
const { pluginId, runtime } = usePluginRenderContext();
|
|
160
174
|
const { state } = useAppState();
|
|
175
|
+
const stateRef = useRef(state);
|
|
176
|
+
const fallbackRef = useRef(fallback);
|
|
177
|
+
stateRef.current = state;
|
|
178
|
+
fallbackRef.current = fallback;
|
|
161
179
|
const value = (state.config.pluginConfig[pluginId]?.[key] as T | undefined) ?? fallback;
|
|
162
180
|
|
|
163
181
|
const setValue = useCallback((nextValue: SetStateAction<T>) => {
|
|
164
|
-
const currentValue =
|
|
182
|
+
const currentValue =
|
|
183
|
+
(stateRef.current.config.pluginConfig[pluginId]?.[key] as T | undefined) ??
|
|
184
|
+
fallbackRef.current;
|
|
165
185
|
const resolved = typeof nextValue === "function"
|
|
166
186
|
? (nextValue as (previousValue: T) => T)(currentValue)
|
|
167
187
|
: nextValue;
|
|
@@ -169,7 +189,7 @@ export function usePluginConfigState<T>(key: string, fallback: T): [T, (value: S
|
|
|
169
189
|
return;
|
|
170
190
|
}
|
|
171
191
|
void runtime.setConfigState(pluginId, key, resolved);
|
|
172
|
-
}, [
|
|
192
|
+
}, [key, pluginId, runtime]);
|
|
173
193
|
|
|
174
194
|
return [value, setValue];
|
|
175
195
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
+
import { createTestRenderer } from "@opentui/core/testing";
|
|
4
|
+
import { createRoot } from "@opentui/react";
|
|
5
|
+
import { act, useEffect, useReducer, useRef } from "react";
|
|
6
|
+
import {
|
|
7
|
+
AppContext,
|
|
8
|
+
PaneInstanceProvider,
|
|
9
|
+
appReducer,
|
|
10
|
+
createInitialState,
|
|
11
|
+
} from "../../state/app-context";
|
|
12
|
+
import { createDefaultConfig } from "../../types/config";
|
|
13
|
+
import { getNativeSurfaceManager } from "../../components/chart/native/surface-manager";
|
|
14
|
+
import { PredictionMarketChart } from "./chart";
|
|
15
|
+
|
|
16
|
+
const TEST_PANE_ID = "prediction-scroll:test";
|
|
17
|
+
const SURFACE_ID = `chart-surface:${TEST_PANE_ID}:compact:base`;
|
|
18
|
+
|
|
19
|
+
let testSetup: Awaited<ReturnType<typeof createTestRenderer>> | undefined;
|
|
20
|
+
let root: ReturnType<typeof createRoot> | undefined;
|
|
21
|
+
let scrollBoxRef: ScrollBoxRenderable | null = null;
|
|
22
|
+
const actEnvironment = globalThis as typeof globalThis & {
|
|
23
|
+
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function ChartScrollHarness() {
|
|
27
|
+
const [state, dispatch] = useReducer(
|
|
28
|
+
appReducer,
|
|
29
|
+
(() => {
|
|
30
|
+
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
31
|
+
config.chartPreferences.renderer = "kitty";
|
|
32
|
+
const initial = createInitialState(config);
|
|
33
|
+
initial.focusedPaneId = TEST_PANE_ID;
|
|
34
|
+
return initial;
|
|
35
|
+
})(),
|
|
36
|
+
);
|
|
37
|
+
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
scrollBoxRef = scrollRef.current;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<AppContext value={{ state, dispatch }}>
|
|
45
|
+
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
46
|
+
<scrollbox ref={scrollRef} height={10} scrollY>
|
|
47
|
+
<box flexDirection="column">
|
|
48
|
+
<box height={14}>
|
|
49
|
+
<text>filler</text>
|
|
50
|
+
</box>
|
|
51
|
+
<PredictionMarketChart
|
|
52
|
+
history={[
|
|
53
|
+
{ date: new Date("2026-04-01T00:00:00Z"), close: 0.45 },
|
|
54
|
+
{ date: new Date("2026-04-02T00:00:00Z"), close: 0.48 },
|
|
55
|
+
{ date: new Date("2026-04-03T00:00:00Z"), close: 0.51 },
|
|
56
|
+
{ date: new Date("2026-04-04T00:00:00Z"), close: 0.49 },
|
|
57
|
+
]}
|
|
58
|
+
width={60}
|
|
59
|
+
height={12}
|
|
60
|
+
range="1M"
|
|
61
|
+
onRangeSelect={() => {}}
|
|
62
|
+
/>
|
|
63
|
+
</box>
|
|
64
|
+
</scrollbox>
|
|
65
|
+
</PaneInstanceProvider>
|
|
66
|
+
</AppContext>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function flushFrames(count = 4) {
|
|
71
|
+
for (let index = 0; index < count; index += 1) {
|
|
72
|
+
await act(async () => {
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
74
|
+
await Promise.resolve();
|
|
75
|
+
await testSetup!.renderOnce();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
scrollBoxRef = null;
|
|
82
|
+
if (root) {
|
|
83
|
+
act(() => {
|
|
84
|
+
root!.unmount();
|
|
85
|
+
});
|
|
86
|
+
root = undefined;
|
|
87
|
+
}
|
|
88
|
+
if (testSetup) {
|
|
89
|
+
testSetup.renderer.destroy();
|
|
90
|
+
testSetup = undefined;
|
|
91
|
+
}
|
|
92
|
+
actEnvironment.IS_REACT_ACT_ENVIRONMENT = false;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("PredictionMarketChart kitty scrolling", () => {
|
|
96
|
+
test("creates a native chart surface when scrolled into view", async () => {
|
|
97
|
+
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
98
|
+
testSetup = await createTestRenderer({ width: 100, height: 24 });
|
|
99
|
+
(testSetup.renderer as { _capabilities: unknown })._capabilities = {
|
|
100
|
+
kitty_graphics: true,
|
|
101
|
+
};
|
|
102
|
+
(testSetup.renderer as { _resolution: unknown })._resolution = {
|
|
103
|
+
width: 1000,
|
|
104
|
+
height: 720,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
root = createRoot(testSetup.renderer);
|
|
108
|
+
act(() => {
|
|
109
|
+
root!.render(<ChartScrollHarness />);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await flushFrames();
|
|
113
|
+
|
|
114
|
+
const manager = getNativeSurfaceManager(testSetup.renderer as never) as unknown as {
|
|
115
|
+
surfaces: Map<
|
|
116
|
+
string,
|
|
117
|
+
{
|
|
118
|
+
snapshot: {
|
|
119
|
+
visibleRect: { x: number; y: number; width: number; height: number } | null;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
>;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const hiddenSurface = manager.surfaces.get(SURFACE_ID);
|
|
126
|
+
expect(hiddenSurface).toBeUndefined();
|
|
127
|
+
|
|
128
|
+
act(() => {
|
|
129
|
+
scrollBoxRef!.scrollTop = 14;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await flushFrames();
|
|
133
|
+
|
|
134
|
+
const visibleSurface = manager.surfaces.get(SURFACE_ID);
|
|
135
|
+
expect(visibleSurface).toBeDefined();
|
|
136
|
+
expect(visibleSurface?.snapshot.visibleRect).not.toBeNull();
|
|
137
|
+
expect(testSetup.captureCharFrame()).toContain("1M");
|
|
138
|
+
});
|
|
139
|
+
});
|