gloomberb 0.9.6 → 0.9.8
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/auth.ts +20 -0
- package/src/api-client/index.ts +48 -0
- package/src/api-client/types.ts +45 -0
- package/src/app/runtime/desktop-deeplink.test.ts +87 -0
- package/src/app/runtime/desktop-deeplink.ts +496 -0
- package/src/app.tsx +34 -0
- package/src/components/chart/bar-chart-renderer.test.ts +87 -12
- package/src/components/chart/bar-chart-renderer.ts +111 -55
- package/src/components/chart/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/footer.test.ts +65 -0
- package/src/components/chart/stock-chart/footer.ts +68 -13
- package/src/components/chart/stock-chart/resolved.tsx +18 -4
- package/src/components/command-bar/pane-templates/items.ts +11 -3
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/surface/index.test.tsx +52 -0
- package/src/components/command-bar/workflow/ops.ts +81 -20
- package/src/components/index.ts +1 -0
- package/src/components/toggle-list.tsx +29 -73
- package/src/components/ui/button.tsx +4 -1
- package/src/components/ui/checkbox.tsx +76 -0
- package/src/components/ui/choice-dialog.tsx +15 -3
- package/src/components/ui/frame.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/message-composer.tsx +4 -0
- package/src/components/ui/native-select.tsx +4 -2
- package/src/market-data/coordinator/events.test.ts +49 -0
- package/src/market-data/coordinator/events.ts +25 -4
- package/src/market-data/coordinator/index.test.ts +223 -0
- package/src/market-data/coordinator/index.ts +40 -3
- package/src/market-data/coordinator/quotes.ts +13 -5
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/quotes/freshness.test.ts +49 -1
- package/src/market-data/quotes/freshness.ts +8 -0
- package/src/market-data/quotes/resolution.test.ts +185 -0
- package/src/market-data/quotes/resolution.ts +94 -8
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -3
- package/src/pane-settings.test.ts +1 -0
- package/src/plugins/builtin/account-management/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +260 -24
- package/src/plugins/builtin/account-management/keyboard.ts +32 -2
- package/src/plugins/builtin/account-management/model.test.ts +99 -4
- package/src/plugins/builtin/account-management/model.ts +263 -3
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- package/src/plugins/builtin/chat/chat.test.tsx +125 -0
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
- package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
- package/src/plugins/builtin/chat/content/composer.tsx +84 -1
- package/src/plugins/builtin/chat/content/index.tsx +154 -16
- package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
- package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
- package/src/plugins/builtin/chat/content/mentions.ts +88 -0
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
- package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -4
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
- package/src/plugins/builtin/chat/sidebar.test.tsx +69 -2
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +21 -11
- package/src/plugins/builtin/cloud/plugin.tsx +7 -0
- package/src/plugins/builtin/correlation/index.tsx +4 -1
- package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
- package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
- package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
- package/src/plugins/builtin/portfolio-list/index.test.ts +102 -1
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
- package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +62 -1
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +414 -8
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +24 -32
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +131 -22
- package/src/plugins/builtin/portfolio-list/settings.ts +36 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +43 -7
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/ibkr/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- package/src/plugins/registry/context.ts +15 -0
- package/src/plugins/registry/index.ts +31 -0
- package/src/renderers/electrobun/bun/index.ts +42 -0
- package/src/renderers/electrobun/shared/protocol.ts +5 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
- package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
- package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
- package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
- package/src/renderers/electrobun/view/host/box.tsx +0 -2
- package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
- package/src/renderers/electrobun/view/host/text.tsx +1 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- package/src/renderers/electrobun/view/ui-host.tsx +2 -0
- package/src/sources/provider-router/batches.ts +11 -3
- package/src/sources/provider-router/cached-financials.test.ts +123 -0
- package/src/sources/provider-router/financial-routes.ts +55 -5
- package/src/sources/provider-router/financials.test.ts +49 -0
- package/src/sources/provider-router/financials.ts +40 -3
- package/src/sources/provider-router/index.test.ts +147 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -1
- package/src/sources/yahoo-finance/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.ts +6 -2
- package/src/state/hooks/quote-streaming.test.ts +14 -1
- package/src/state/hooks/quote-streaming.ts +5 -4
- package/src/sync/controller.ts +296 -0
- package/src/sync/core-contributors.test.ts +251 -0
- package/src/sync/core-contributors.ts +400 -0
- package/src/sync/profile-analytics.ts +39 -0
- package/src/sync/react.ts +53 -0
- package/src/sync/types.ts +77 -0
- package/src/types/config.ts +1 -0
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +14 -1
- package/src/ui/index.tsx +1 -0
- package/src/utils/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- package/src/version.ts +1 -1
|
@@ -279,6 +279,7 @@ function PortfolioHarness({
|
|
|
279
279
|
stateMutator,
|
|
280
280
|
runtime = createTestPluginRuntime(),
|
|
281
281
|
paneHeight = 24,
|
|
282
|
+
paneFocused = true,
|
|
282
283
|
}: {
|
|
283
284
|
config: AppConfig;
|
|
284
285
|
collectionId: string;
|
|
@@ -290,6 +291,7 @@ function PortfolioHarness({
|
|
|
290
291
|
stateMutator?: (state: ReturnType<typeof createInitialState>) => void;
|
|
291
292
|
runtime?: PluginRuntimeAccess;
|
|
292
293
|
paneHeight?: number;
|
|
294
|
+
paneFocused?: boolean;
|
|
293
295
|
}) {
|
|
294
296
|
const initialState = createPortfolioState(config, collectionId, expanded, {
|
|
295
297
|
ticker,
|
|
@@ -309,7 +311,7 @@ function PortfolioHarness({
|
|
|
309
311
|
<PortfolioPane
|
|
310
312
|
paneId={TEST_PANE_ID}
|
|
311
313
|
paneType="portfolio-list"
|
|
312
|
-
focused
|
|
314
|
+
focused={paneFocused}
|
|
313
315
|
width={100}
|
|
314
316
|
height={paneHeight}
|
|
315
317
|
/>
|
|
@@ -323,11 +325,162 @@ async function flushFrame() {
|
|
|
323
325
|
await act(async () => {
|
|
324
326
|
await Promise.resolve();
|
|
325
327
|
await testSetup!.renderOnce();
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
326
329
|
await Promise.resolve();
|
|
327
330
|
await testSetup!.renderOnce();
|
|
328
331
|
});
|
|
329
332
|
}
|
|
330
333
|
|
|
334
|
+
function makeSortWarmupQuote(symbol: string): Quote {
|
|
335
|
+
return makeQuote({
|
|
336
|
+
symbol,
|
|
337
|
+
price: symbol === "SIVE" ? 46.7 : 100,
|
|
338
|
+
change: symbol === "SIVE" ? -9.6 : 0,
|
|
339
|
+
changePercent: symbol === "SIVE" ? -17.05 : 0,
|
|
340
|
+
previousClose: symbol === "SIVE" ? 56.3 : 100,
|
|
341
|
+
listingExchangeName: "NASDAQ",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function makeSortWarmupBrokerTicker(portfolioId: string, symbol: string, index: number): TickerRecord {
|
|
346
|
+
return makeTicker({
|
|
347
|
+
ticker: symbol,
|
|
348
|
+
name: symbol,
|
|
349
|
+
portfolios: [portfolioId],
|
|
350
|
+
positions: [{
|
|
351
|
+
portfolio: portfolioId,
|
|
352
|
+
shares: 10,
|
|
353
|
+
avgCost: 100,
|
|
354
|
+
currency: "USD",
|
|
355
|
+
broker: "ibkr",
|
|
356
|
+
brokerInstanceId: "ibkr-live",
|
|
357
|
+
brokerAccountId: "DU12345",
|
|
358
|
+
brokerContractId: 10_000 + index,
|
|
359
|
+
markPrice: 100,
|
|
360
|
+
}],
|
|
361
|
+
broker_contracts: [{
|
|
362
|
+
brokerId: "ibkr",
|
|
363
|
+
brokerInstanceId: "ibkr-live",
|
|
364
|
+
symbol,
|
|
365
|
+
localSymbol: symbol,
|
|
366
|
+
exchange: "SMART",
|
|
367
|
+
primaryExchange: "NASDAQ",
|
|
368
|
+
conId: 10_000 + index,
|
|
369
|
+
}],
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function renderHiddenChangePctSortWarmup(options: { staleCachedSiveSnapshot?: boolean } = {}) {
|
|
374
|
+
const portfolioId = "broker:ibkr-live:DU12345";
|
|
375
|
+
const config = createPortfolioConfigWithColumns(
|
|
376
|
+
portfolioId,
|
|
377
|
+
["ticker", "price", "change_pct", "latency"],
|
|
378
|
+
[createBrokerInstance("gateway", "ibkr-live")],
|
|
379
|
+
);
|
|
380
|
+
const requestedSnapshots: string[] = [];
|
|
381
|
+
const provider: DataProvider = {
|
|
382
|
+
id: "test-provider",
|
|
383
|
+
name: "Test Provider",
|
|
384
|
+
async getTickerFinancials(symbol) {
|
|
385
|
+
requestedSnapshots.push(symbol);
|
|
386
|
+
return {
|
|
387
|
+
annualStatements: [],
|
|
388
|
+
quarterlyStatements: [],
|
|
389
|
+
priceHistory: [],
|
|
390
|
+
quote: makeSortWarmupQuote(symbol),
|
|
391
|
+
};
|
|
392
|
+
},
|
|
393
|
+
async getQuote(symbol) {
|
|
394
|
+
return makeSortWarmupQuote(symbol);
|
|
395
|
+
},
|
|
396
|
+
async getExchangeRate() {
|
|
397
|
+
return 1;
|
|
398
|
+
},
|
|
399
|
+
async search() {
|
|
400
|
+
return [];
|
|
401
|
+
},
|
|
402
|
+
async getArticleSummary() {
|
|
403
|
+
return null;
|
|
404
|
+
},
|
|
405
|
+
async getPriceHistory() {
|
|
406
|
+
return [];
|
|
407
|
+
},
|
|
408
|
+
subscribeQuotes() {
|
|
409
|
+
return () => {};
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
413
|
+
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
414
|
+
|
|
415
|
+
const tickers = Array.from({ length: 29 }, (_, index) => makeSortWarmupBrokerTicker(portfolioId, `T${String(index).padStart(2, "0")}`, index));
|
|
416
|
+
const sive = makeSortWarmupBrokerTicker(portfolioId, "SIVE", 29);
|
|
417
|
+
if (options.staleCachedSiveSnapshot) {
|
|
418
|
+
const siveInstrument = instrumentFromTicker(sive, "SIVE", { portfolioId });
|
|
419
|
+
if (!siveInstrument) throw new Error("expected SIVE instrument");
|
|
420
|
+
sharedCoordinator.primeCachedFinancials([{
|
|
421
|
+
instrument: siveInstrument,
|
|
422
|
+
financials: {
|
|
423
|
+
annualStatements: [],
|
|
424
|
+
quarterlyStatements: [],
|
|
425
|
+
priceHistory: [],
|
|
426
|
+
quote: makeQuote({
|
|
427
|
+
symbol: "SIVE",
|
|
428
|
+
price: 56.3,
|
|
429
|
+
change: 56.3,
|
|
430
|
+
changePercent: 100,
|
|
431
|
+
previousClose: 0,
|
|
432
|
+
listingExchangeName: "NASDAQ",
|
|
433
|
+
lastUpdated: Date.now() - 24 * 60 * 60_000,
|
|
434
|
+
}),
|
|
435
|
+
},
|
|
436
|
+
}]);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
testSetup = await testRender(
|
|
440
|
+
<PortfolioHarness
|
|
441
|
+
config={config}
|
|
442
|
+
collectionId={portfolioId}
|
|
443
|
+
stateMutator={(state) => {
|
|
444
|
+
state.tickers = new Map([...tickers, sive].map((entry) => [entry.metadata.ticker, entry]));
|
|
445
|
+
state.financials = new Map(tickers.map((entry, index) => [
|
|
446
|
+
entry.metadata.ticker,
|
|
447
|
+
{
|
|
448
|
+
annualStatements: [],
|
|
449
|
+
quarterlyStatements: [],
|
|
450
|
+
priceHistory: [],
|
|
451
|
+
quote: makeQuote({
|
|
452
|
+
symbol: entry.metadata.ticker,
|
|
453
|
+
price: 100 + index,
|
|
454
|
+
change: index,
|
|
455
|
+
changePercent: index,
|
|
456
|
+
listingExchangeName: "NASDAQ",
|
|
457
|
+
}),
|
|
458
|
+
},
|
|
459
|
+
]));
|
|
460
|
+
state.paneState[TEST_PANE_ID] = {
|
|
461
|
+
collectionId: portfolioId,
|
|
462
|
+
cursorSymbol: "T00",
|
|
463
|
+
cashDrawerExpanded: false,
|
|
464
|
+
collectionSorts: {
|
|
465
|
+
[portfolioId]: { columnId: "change_pct", direction: "asc" },
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}}
|
|
469
|
+
paneHeight={12}
|
|
470
|
+
/>,
|
|
471
|
+
{ width: 100, height: 12 },
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
await flushFrame();
|
|
475
|
+
const beforeFrame = testSetup.captureCharFrame();
|
|
476
|
+
await act(async () => {
|
|
477
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
478
|
+
});
|
|
479
|
+
await flushFrame();
|
|
480
|
+
|
|
481
|
+
return { beforeFrame, frame: testSetup.captureCharFrame(), requestedSnapshots };
|
|
482
|
+
}
|
|
483
|
+
|
|
331
484
|
afterEach(async () => {
|
|
332
485
|
if (testSetup) {
|
|
333
486
|
testSetup.renderer.destroy();
|
|
@@ -690,6 +843,13 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
690
843
|
test("renders portfolio grid from portfolio table values and opens the selected ticker", async () => {
|
|
691
844
|
const portfolioId = "broker:ibkr-flex:DU12345";
|
|
692
845
|
const config = createPortfolioConfig(portfolioId, [createBrokerInstance("flex")]);
|
|
846
|
+
const instance = config.layout.instances.find((entry) => entry.instanceId === TEST_PANE_ID);
|
|
847
|
+
if (instance) {
|
|
848
|
+
instance.settings = {
|
|
849
|
+
...(instance.settings ?? {}),
|
|
850
|
+
viewMode: "grid",
|
|
851
|
+
};
|
|
852
|
+
}
|
|
693
853
|
const pinned: Array<{ symbol: string; options: { floating?: boolean; paneType?: string } | undefined }> = [];
|
|
694
854
|
const runtime = createTestPluginRuntime({
|
|
695
855
|
pinTicker: (symbol, options) => {
|
|
@@ -702,12 +862,6 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
702
862
|
config={config}
|
|
703
863
|
collectionId={portfolioId}
|
|
704
864
|
runtime={runtime}
|
|
705
|
-
stateMutator={(state) => {
|
|
706
|
-
state.paneState[TEST_PANE_ID] = {
|
|
707
|
-
...state.paneState[TEST_PANE_ID],
|
|
708
|
-
viewMode: "grid",
|
|
709
|
-
};
|
|
710
|
-
}}
|
|
711
865
|
/>,
|
|
712
866
|
{ width: 100, height: 12 },
|
|
713
867
|
);
|
|
@@ -715,7 +869,6 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
715
869
|
await flushFrame();
|
|
716
870
|
|
|
717
871
|
const frame = testSetup.captureCharFrame();
|
|
718
|
-
expect(frame).toContain("Grid");
|
|
719
872
|
expect(frame).toContain("AAPL");
|
|
720
873
|
expect(frame).toContain("1.3k");
|
|
721
874
|
expect(frame).toContain("+4.17%");
|
|
@@ -728,6 +881,32 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
728
881
|
expect(pinned).toEqual([{ symbol: "AAPL", options: { floating: true, paneType: TICKER_RESEARCH_PANE_ID } }]);
|
|
729
882
|
});
|
|
730
883
|
|
|
884
|
+
test("keeps watchlists in table view when grid is saved on the pane", async () => {
|
|
885
|
+
const config = createManualCollectionConfig("watchlist");
|
|
886
|
+
const instance = config.layout.instances.find((entry) => entry.instanceId === TEST_PANE_ID);
|
|
887
|
+
if (instance) {
|
|
888
|
+
instance.settings = {
|
|
889
|
+
...(instance.settings ?? {}),
|
|
890
|
+
viewMode: "grid",
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
testSetup = await testRender(
|
|
895
|
+
<PortfolioHarness
|
|
896
|
+
config={config}
|
|
897
|
+
collectionId="watchlist"
|
|
898
|
+
ticker={makeTicker({ portfolios: [], watchlists: ["watchlist"], positions: [] })}
|
|
899
|
+
/>,
|
|
900
|
+
{ width: 100, height: 12 },
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
await flushFrame();
|
|
904
|
+
|
|
905
|
+
const frame = testSetup.captureCharFrame();
|
|
906
|
+
expect(frame).toContain("TICKER");
|
|
907
|
+
expect(frame).toContain("AAPL");
|
|
908
|
+
});
|
|
909
|
+
|
|
731
910
|
test("keeps option avg cost on premium scale and multiplies quoted value by contract size", async () => {
|
|
732
911
|
const portfolioId = "broker:ibkr-flex:DU12345";
|
|
733
912
|
const optionTicker = "SPY 260619C00500000";
|
|
@@ -965,6 +1144,100 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
965
1144
|
expect(frame).toContain("22.0");
|
|
966
1145
|
});
|
|
967
1146
|
|
|
1147
|
+
test("force-refreshes old visible quote data even when the pane is not focused", async () => {
|
|
1148
|
+
const config = createPortfolioConfigWithColumns(
|
|
1149
|
+
"broker:ibkr-flex:DU12345",
|
|
1150
|
+
["ticker", "price", "day_pnl"],
|
|
1151
|
+
[createBrokerInstance("flex")],
|
|
1152
|
+
);
|
|
1153
|
+
const oldQuote = makeQuote({
|
|
1154
|
+
price: 120,
|
|
1155
|
+
change: -5,
|
|
1156
|
+
changePercent: -4,
|
|
1157
|
+
currency: "EUR",
|
|
1158
|
+
listingExchangeName: "FWB2",
|
|
1159
|
+
marketState: "REGULAR",
|
|
1160
|
+
lastUpdated: Date.now() - 5 * 60 * 1000,
|
|
1161
|
+
});
|
|
1162
|
+
const batchOptions: Array<{ forceRefresh?: boolean } | undefined> = [];
|
|
1163
|
+
const provider: DataProvider = {
|
|
1164
|
+
id: "test-provider",
|
|
1165
|
+
name: "Test Provider",
|
|
1166
|
+
async getTickerFinancials() {
|
|
1167
|
+
return {
|
|
1168
|
+
annualStatements: [],
|
|
1169
|
+
quarterlyStatements: [],
|
|
1170
|
+
priceHistory: [],
|
|
1171
|
+
quote: oldQuote,
|
|
1172
|
+
};
|
|
1173
|
+
},
|
|
1174
|
+
async getQuotesBatch(targets, options) {
|
|
1175
|
+
batchOptions.push(options);
|
|
1176
|
+
return targets.map((target) => ({
|
|
1177
|
+
target,
|
|
1178
|
+
quote: makeQuote({
|
|
1179
|
+
symbol: target.symbol,
|
|
1180
|
+
price: 126,
|
|
1181
|
+
change: 1,
|
|
1182
|
+
changePercent: 0.8,
|
|
1183
|
+
currency: "EUR",
|
|
1184
|
+
listingExchangeName: "FWB2",
|
|
1185
|
+
marketState: "REGULAR",
|
|
1186
|
+
}),
|
|
1187
|
+
}));
|
|
1188
|
+
},
|
|
1189
|
+
async getQuote(symbol) {
|
|
1190
|
+
return makeQuote({ symbol, price: 126 });
|
|
1191
|
+
},
|
|
1192
|
+
async getExchangeRate() {
|
|
1193
|
+
return 1;
|
|
1194
|
+
},
|
|
1195
|
+
async search() {
|
|
1196
|
+
return [];
|
|
1197
|
+
},
|
|
1198
|
+
async getArticleSummary() {
|
|
1199
|
+
return null;
|
|
1200
|
+
},
|
|
1201
|
+
async getPriceHistory() {
|
|
1202
|
+
return [];
|
|
1203
|
+
},
|
|
1204
|
+
subscribeQuotes() {
|
|
1205
|
+
return () => {};
|
|
1206
|
+
},
|
|
1207
|
+
};
|
|
1208
|
+
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
1209
|
+
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
1210
|
+
|
|
1211
|
+
testSetup = await testRender(
|
|
1212
|
+
<PortfolioHarness
|
|
1213
|
+
config={config}
|
|
1214
|
+
collectionId="broker:ibkr-flex:DU12345"
|
|
1215
|
+
ticker={makeTicker({ exchange: "FWB2", currency: "EUR" })}
|
|
1216
|
+
quote={oldQuote}
|
|
1217
|
+
paneFocused={false}
|
|
1218
|
+
stateMutator={(state) => {
|
|
1219
|
+
state.focusedPaneId = "portfolio-list:other";
|
|
1220
|
+
state.paneState[TEST_PANE_ID] = {
|
|
1221
|
+
...(state.paneState[TEST_PANE_ID] ?? {}),
|
|
1222
|
+
collectionSorts: {
|
|
1223
|
+
"broker:ibkr-flex:DU12345": { columnId: "ticker", direction: "asc" },
|
|
1224
|
+
},
|
|
1225
|
+
};
|
|
1226
|
+
}}
|
|
1227
|
+
/>,
|
|
1228
|
+
{ width: 100, height: 12 },
|
|
1229
|
+
);
|
|
1230
|
+
|
|
1231
|
+
await flushFrame();
|
|
1232
|
+
await act(async () => {
|
|
1233
|
+
await Promise.resolve();
|
|
1234
|
+
});
|
|
1235
|
+
await flushFrame();
|
|
1236
|
+
|
|
1237
|
+
expect(batchOptions.some((options) => options?.forceRefresh === true)).toBe(true);
|
|
1238
|
+
expect(testSetup.captureCharFrame()).toContain("126");
|
|
1239
|
+
});
|
|
1240
|
+
|
|
968
1241
|
test("shows cached market cap on reopen for broker-linked rows", async () => {
|
|
969
1242
|
const dbPath = createTempDbPath("cached-reopen-market-cap");
|
|
970
1243
|
const persistence = new AppPersistence(dbPath);
|
|
@@ -1305,4 +1578,137 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
1305
1578
|
expect(frame).toContain("+5.41%");
|
|
1306
1579
|
});
|
|
1307
1580
|
|
|
1581
|
+
test("streams portfolio rows with the active collection broker contract", async () => {
|
|
1582
|
+
const config = createPortfolioConfigWithColumns(
|
|
1583
|
+
"broker:ibkr-live:DU12345",
|
|
1584
|
+
["ticker", "price", "change_pct", "latency"],
|
|
1585
|
+
[createBrokerInstance("gateway", "ibkr-flex"), createBrokerInstance("gateway", "ibkr-live")],
|
|
1586
|
+
);
|
|
1587
|
+
let subscribedTargets: Array<{ symbol: string; context?: { brokerInstanceId?: string; instrument?: unknown } }> = [];
|
|
1588
|
+
const provider: DataProvider = {
|
|
1589
|
+
id: "test-provider",
|
|
1590
|
+
name: "Test Provider",
|
|
1591
|
+
async getTickerFinancials(symbol) {
|
|
1592
|
+
return {
|
|
1593
|
+
annualStatements: [],
|
|
1594
|
+
quarterlyStatements: [],
|
|
1595
|
+
priceHistory: [],
|
|
1596
|
+
quote: makeQuote({ symbol }),
|
|
1597
|
+
};
|
|
1598
|
+
},
|
|
1599
|
+
async getQuote(symbol) {
|
|
1600
|
+
return makeQuote({ symbol });
|
|
1601
|
+
},
|
|
1602
|
+
async getExchangeRate() {
|
|
1603
|
+
return 1;
|
|
1604
|
+
},
|
|
1605
|
+
async search() {
|
|
1606
|
+
return [];
|
|
1607
|
+
},
|
|
1608
|
+
async getArticleSummary() {
|
|
1609
|
+
return null;
|
|
1610
|
+
},
|
|
1611
|
+
async getPriceHistory() {
|
|
1612
|
+
return [];
|
|
1613
|
+
},
|
|
1614
|
+
subscribeQuotes(targets) {
|
|
1615
|
+
subscribedTargets = targets;
|
|
1616
|
+
return () => {};
|
|
1617
|
+
},
|
|
1618
|
+
};
|
|
1619
|
+
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
1620
|
+
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
1621
|
+
|
|
1622
|
+
const flexContract = {
|
|
1623
|
+
brokerId: "ibkr",
|
|
1624
|
+
brokerInstanceId: "ibkr-flex",
|
|
1625
|
+
symbol: "VICR",
|
|
1626
|
+
localSymbol: "VICR",
|
|
1627
|
+
exchange: "NASDAQ",
|
|
1628
|
+
conId: 275759,
|
|
1629
|
+
};
|
|
1630
|
+
const liveContract = {
|
|
1631
|
+
brokerId: "ibkr",
|
|
1632
|
+
brokerInstanceId: "ibkr-live",
|
|
1633
|
+
symbol: "VICR",
|
|
1634
|
+
localSymbol: "VICR",
|
|
1635
|
+
exchange: "NASDAQ",
|
|
1636
|
+
primaryExchange: "NASDAQ",
|
|
1637
|
+
conId: 275759,
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
testSetup = await testRender(
|
|
1641
|
+
<PortfolioHarness
|
|
1642
|
+
config={config}
|
|
1643
|
+
collectionId="broker:ibkr-live:DU12345"
|
|
1644
|
+
stateMutator={(state) => {
|
|
1645
|
+
state.tickers = new Map([["VICR", makeTicker({
|
|
1646
|
+
ticker: "VICR",
|
|
1647
|
+
name: "Vicor",
|
|
1648
|
+
portfolios: ["broker:ibkr-flex:DU12345", "broker:ibkr-live:DU12345"],
|
|
1649
|
+
positions: [
|
|
1650
|
+
{
|
|
1651
|
+
portfolio: "broker:ibkr-flex:DU12345",
|
|
1652
|
+
shares: 170,
|
|
1653
|
+
avgCost: 198,
|
|
1654
|
+
currency: "USD",
|
|
1655
|
+
broker: "ibkr",
|
|
1656
|
+
brokerInstanceId: "ibkr-flex",
|
|
1657
|
+
brokerAccountId: "DU12345",
|
|
1658
|
+
brokerContractId: 275759,
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
portfolio: "broker:ibkr-live:DU12345",
|
|
1662
|
+
shares: 350,
|
|
1663
|
+
avgCost: 290,
|
|
1664
|
+
currency: "USD",
|
|
1665
|
+
broker: "ibkr",
|
|
1666
|
+
brokerInstanceId: "ibkr-live",
|
|
1667
|
+
brokerAccountId: "DU12345",
|
|
1668
|
+
brokerContractId: 275759,
|
|
1669
|
+
},
|
|
1670
|
+
],
|
|
1671
|
+
broker_contracts: [flexContract, liveContract],
|
|
1672
|
+
})]]);
|
|
1673
|
+
state.financials = new Map();
|
|
1674
|
+
state.paneState[TEST_PANE_ID] = {
|
|
1675
|
+
collectionId: "broker:ibkr-live:DU12345",
|
|
1676
|
+
cursorSymbol: "VICR",
|
|
1677
|
+
cashDrawerExpanded: false,
|
|
1678
|
+
};
|
|
1679
|
+
}}
|
|
1680
|
+
/>,
|
|
1681
|
+
{ width: 100, height: 12 },
|
|
1682
|
+
);
|
|
1683
|
+
|
|
1684
|
+
await flushFrame();
|
|
1685
|
+
await act(async () => {
|
|
1686
|
+
await Promise.resolve();
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
const vicrTarget = subscribedTargets.find((target) => target.symbol === "VICR");
|
|
1690
|
+
expect(vicrTarget?.context?.brokerInstanceId).toBe("ibkr-live");
|
|
1691
|
+
expect(vicrTarget?.context?.instrument).toEqual(liveContract);
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
test("warms hidden quote-missing rows when sorting by change percent", async () => {
|
|
1695
|
+
const { beforeFrame, frame, requestedSnapshots } = await renderHiddenChangePctSortWarmup();
|
|
1696
|
+
expect(beforeFrame).not.toContain("SIVE");
|
|
1697
|
+
expect(requestedSnapshots).toContain("SIVE");
|
|
1698
|
+
expect(frame).toContain("SIVE");
|
|
1699
|
+
expect(frame).toContain("-17.05%");
|
|
1700
|
+
expect(frame).toContain("46.7");
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
test("force-refreshes hidden stale cached snapshots when sorting by change percent", async () => {
|
|
1704
|
+
const { beforeFrame, frame, requestedSnapshots } = await renderHiddenChangePctSortWarmup({
|
|
1705
|
+
staleCachedSiveSnapshot: true,
|
|
1706
|
+
});
|
|
1707
|
+
expect(beforeFrame).not.toContain("SIVE");
|
|
1708
|
+
expect(requestedSnapshots).toContain("SIVE");
|
|
1709
|
+
expect(frame).toContain("SIVE");
|
|
1710
|
+
expect(frame).toContain("-17.05%");
|
|
1711
|
+
expect(frame).toContain("46.7");
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1308
1714
|
});
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
useAppSelector,
|
|
14
14
|
usePaneCollection,
|
|
15
15
|
usePaneInstance,
|
|
16
|
+
usePaneSettingValue,
|
|
16
17
|
usePaneStateValue,
|
|
17
18
|
type CollectionSortPreference,
|
|
18
19
|
} from "../../../../state/app/context";
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
resolveActiveCollectionId,
|
|
34
35
|
resolveScopedCollectionEntries,
|
|
35
36
|
resolveVisibleColumns,
|
|
37
|
+
type PortfolioViewMode,
|
|
36
38
|
} from "../settings";
|
|
37
39
|
import { useQuoteFlashMap } from "../../../../components/quote-flash";
|
|
38
40
|
import { PortfolioTickerTable } from "../table";
|
|
@@ -50,8 +52,6 @@ import {
|
|
|
50
52
|
import { usePortfolioPaneStreaming } from "./streaming";
|
|
51
53
|
import { usePortfolioSupplementalData } from "./supplemental";
|
|
52
54
|
|
|
53
|
-
type PortfolioViewMode = "table" | "grid";
|
|
54
|
-
|
|
55
55
|
export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
56
56
|
const { pinTicker } = usePluginTickerActions();
|
|
57
57
|
const paneInstance = usePaneInstance();
|
|
@@ -68,7 +68,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
68
68
|
const [committedCursorSymbol, setCommittedCursorSymbol] = usePaneStateValue<string | null>("cursorSymbol", null);
|
|
69
69
|
const [collectionSorts, setCollectionSorts] = usePaneStateValue<Record<string, CollectionSortPreference>>("collectionSorts", {});
|
|
70
70
|
const [cashDrawerExpanded, setCashDrawerExpanded] = usePaneStateValue<boolean>("cashDrawerExpanded", false);
|
|
71
|
-
const [
|
|
71
|
+
const [, setViewMode] = usePaneSettingValue<PortfolioViewMode>("viewMode", "table");
|
|
72
72
|
|
|
73
73
|
const [now, setNow] = useState(Date.now());
|
|
74
74
|
const [streamWindow, setStreamWindow] = useState({ start: 0, end: 24 });
|
|
@@ -101,12 +101,16 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
101
101
|
? config.portfolios.find((portfolio) => portfolio.id === activeCollectionId) ?? null
|
|
102
102
|
: null
|
|
103
103
|
), [activeCollectionId, config.portfolios, isPortfolioTab]);
|
|
104
|
+
const viewMode: PortfolioViewMode = isPortfolioTab ? paneSettings.viewMode : "table";
|
|
104
105
|
|
|
105
106
|
const tickers = useMemo(
|
|
106
107
|
() => getCollectionTickersFromConfig(config, tickersBySymbol, activeCollectionId),
|
|
107
108
|
[activeCollectionId, config, tickersBySymbol],
|
|
108
109
|
);
|
|
109
|
-
const
|
|
110
|
+
const financialsInstrumentOptions = useMemo(() => ({
|
|
111
|
+
portfolioId: isPortfolioTab ? activeCollectionId : undefined,
|
|
112
|
+
}), [activeCollectionId, isPortfolioTab]);
|
|
113
|
+
const marketFinancialsMap = useTickerFinancialsMap(tickers, financialsInstrumentOptions);
|
|
110
114
|
const financialsMap = useMemo(() => {
|
|
111
115
|
const merged = new Map(cachedFinancials);
|
|
112
116
|
for (const [symbol, financials] of marketFinancialsMap) {
|
|
@@ -178,8 +182,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
178
182
|
? (cashDrawerExpanded ? Math.min(6, Math.max(3, 2 + accountState.visibleCashBalances.length)) : 1)
|
|
179
183
|
: 0;
|
|
180
184
|
const showCollectionTabs = visibleCollections.length > 1;
|
|
181
|
-
const
|
|
182
|
-
const headerHeight = viewTabsHeight + (showCollectionTabs ? 1 : 0);
|
|
185
|
+
const headerHeight = showCollectionTabs ? 1 : 0;
|
|
183
186
|
const drawerHeight = showCashDrawer
|
|
184
187
|
? Math.min(requestedDrawerHeight, Math.max(1, height - (headerHeight + 2)))
|
|
185
188
|
: 0;
|
|
@@ -220,8 +223,9 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
220
223
|
}, [pinTicker]);
|
|
221
224
|
|
|
222
225
|
const toggleViewMode = useCallback(() => {
|
|
226
|
+
if (!isPortfolioTab) return;
|
|
223
227
|
setViewMode((current) => current === "table" ? "grid" : "table");
|
|
224
|
-
}, [setViewMode]);
|
|
228
|
+
}, [isPortfolioTab, setViewMode]);
|
|
225
229
|
|
|
226
230
|
const handleRowActivate = useCallback((ticker: TickerRecord) => {
|
|
227
231
|
flushCursorSymbol(ticker.metadata.ticker);
|
|
@@ -254,7 +258,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
254
258
|
return true;
|
|
255
259
|
}
|
|
256
260
|
|
|
257
|
-
if (key === "s") {
|
|
261
|
+
if (key === "s" && isPortfolioTab) {
|
|
258
262
|
event.preventDefault?.();
|
|
259
263
|
event.stopPropagation?.();
|
|
260
264
|
toggleViewMode();
|
|
@@ -265,6 +269,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
265
269
|
}, [
|
|
266
270
|
cashDrawerExpanded,
|
|
267
271
|
focused,
|
|
272
|
+
isPortfolioTab,
|
|
268
273
|
openTickerFloating,
|
|
269
274
|
safeSelectedIdx,
|
|
270
275
|
setCashDrawerExpanded,
|
|
@@ -301,16 +306,20 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
301
306
|
const effectiveStreamWindow = useMemo(() => (
|
|
302
307
|
viewMode === "grid"
|
|
303
308
|
? { start: 0, end: Math.min(sortedTickers.length, 96) }
|
|
304
|
-
:
|
|
305
|
-
|
|
309
|
+
: {
|
|
310
|
+
start: streamWindow.start,
|
|
311
|
+
end: Math.min(sortedTickers.length, Math.max(streamWindow.end, height + 4)),
|
|
312
|
+
}
|
|
313
|
+
), [height, sortedTickers.length, streamWindow, viewMode]);
|
|
306
314
|
|
|
307
315
|
usePortfolioPaneStreaming({
|
|
308
316
|
appActive,
|
|
309
|
-
|
|
317
|
+
activeCollectionId: isPortfolioTab ? activeCollectionId : undefined,
|
|
310
318
|
sortedTickers,
|
|
311
319
|
cursorSymbol,
|
|
312
320
|
streamWindow: effectiveStreamWindow,
|
|
313
321
|
isPortfolioTab,
|
|
322
|
+
activeSort,
|
|
314
323
|
financialsMap,
|
|
315
324
|
visibleWarmupRequirements,
|
|
316
325
|
});
|
|
@@ -379,25 +388,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
379
388
|
|
|
380
389
|
return (
|
|
381
390
|
<Box flexDirection="column" width={width} height={height}>
|
|
382
|
-
|
|
383
|
-
<Box flexDirection="
|
|
384
|
-
<Box flexShrink={1} overflow="hidden">
|
|
385
|
-
<Tabs
|
|
386
|
-
tabs={[
|
|
387
|
-
{ label: "Table", value: "table" },
|
|
388
|
-
{ label: "Grid", value: "grid" },
|
|
389
|
-
]}
|
|
390
|
-
activeValue={viewMode}
|
|
391
|
-
onSelect={(value) => setViewMode(value as PortfolioViewMode)}
|
|
392
|
-
compact
|
|
393
|
-
variant="bare"
|
|
394
|
-
focused={focused && !quickAddFocused}
|
|
395
|
-
keyboardNavigation={false}
|
|
396
|
-
/>
|
|
397
|
-
</Box>
|
|
398
|
-
</Box>
|
|
399
|
-
|
|
400
|
-
{showCollectionTabs && (
|
|
391
|
+
{showCollectionTabs && (
|
|
392
|
+
<Box flexDirection="column" height={headerHeight}>
|
|
401
393
|
<Box flexDirection="row" height={1}>
|
|
402
394
|
<Box flexShrink={1} overflow="hidden">
|
|
403
395
|
<Tabs
|
|
@@ -409,8 +401,8 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
409
401
|
/>
|
|
410
402
|
</Box>
|
|
411
403
|
</Box>
|
|
412
|
-
|
|
413
|
-
|
|
404
|
+
</Box>
|
|
405
|
+
)}
|
|
414
406
|
|
|
415
407
|
{viewMode === "table" ? (
|
|
416
408
|
<PortfolioTickerTable
|