gloomberb 0.12.0 → 0.13.2
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 +2 -0
- package/package.json +1 -2
- package/src/api-client/auth.ts +2 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/index.ts +10 -2
- package/src/api-client/request.ts +4 -1
- package/src/api-client/research-activity.ts +61 -0
- package/src/api-client/types.ts +2 -1
- package/src/cli/commands/plugins.ts +4 -3
- package/src/cli/desktop-pane-shot.ts +79 -10
- package/src/cli/pane-functions/capabilities.ts +76 -23
- package/src/cli/pane-functions/cloud-session.test.ts +54 -0
- package/src/cli/pane-functions/cloud-session.ts +38 -0
- package/src/cli/pane-functions/dom.test.ts +41 -0
- package/src/cli/pane-functions/dom.ts +199 -0
- package/src/cli/pane-functions/index.test.ts +13 -2
- package/src/cli/pane-functions/index.ts +9 -5
- package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
- package/src/cli/pane-functions/intraday-shot.ts +334 -0
- package/src/cli/pane-functions/report.test.ts +29 -1
- package/src/cli/pane-functions/report.ts +68 -7
- package/src/cli/pane-functions/screenshot.ts +195 -42
- package/src/cli/restore-plugins.ts +10 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -1
- package/src/components/chart/composite/composite-chart.tsx +87 -21
- package/src/components/chart/composite/rasterizer.ts +33 -0
- package/src/components/chart/composite/renderers.test.ts +51 -4
- package/src/components/chart/composite/scene.test.ts +128 -2
- package/src/components/chart/composite/scene.ts +92 -16
- package/src/components/chart/composite/text-renderer.ts +13 -0
- package/src/components/chart/composite/time-scale.ts +30 -10
- package/src/components/chart/composite/types.ts +26 -0
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +63 -9
- package/src/components/command-bar/surface/test-harness.tsx +23 -4
- package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
- package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
- package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
- package/src/components/onboarding/onboarding-wizard.tsx +49 -14
- package/src/data/config/store/index.test.ts +23 -0
- package/src/data/config/store/node.ts +7 -1
- package/src/data/config/store/normalize.ts +4 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/cds/client.test.ts +20 -0
- package/src/plugins/builtin/cds/client.ts +13 -3
- package/src/plugins/builtin/cds/headless.test.ts +30 -0
- package/src/plugins/builtin/cds/headless.ts +113 -0
- package/src/plugins/builtin/cds/index.tsx +4 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chat/controller/channels.ts +2 -1
- package/src/plugins/builtin/chat/controller/index.ts +13 -5
- package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
- package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
- package/src/plugins/builtin/chat/controller/state.ts +1 -0
- package/src/plugins/builtin/chat/controller.test.ts +260 -4
- package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
- package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
- package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
- package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
- package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
- package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
- package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
- package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
- package/src/plugins/builtin/congress-trades/client.ts +11 -0
- package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
- package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
- package/src/plugins/builtin/congress-trades/headless.ts +145 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
- package/src/plugins/builtin/credit-conditions/client.ts +1 -1
- package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
- package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
- package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
- package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
- package/src/plugins/builtin/dividend-yield/model.ts +4 -22
- package/src/plugins/builtin/dividend-yield/view.ts +17 -0
- package/src/plugins/builtin/earnings/headless.test.ts +82 -0
- package/src/plugins/builtin/earnings/headless.ts +162 -0
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/data.ts +29 -8
- package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
- package/src/plugins/builtin/earnings-calls/model.ts +238 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
- package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
- package/src/plugins/builtin/fear-greed/headless.ts +84 -0
- package/src/plugins/builtin/fear-greed/index.tsx +4 -0
- package/src/plugins/builtin/holders/client.ts +29 -0
- package/src/plugins/builtin/holders/headless.test.ts +58 -0
- package/src/plugins/builtin/holders/headless.ts +162 -0
- package/src/plugins/builtin/holders/index.tsx +14 -8
- package/src/plugins/builtin/holders/pane.tsx +7 -1
- package/src/plugins/builtin/insider/client.ts +50 -0
- package/src/plugins/builtin/insider/headless.test.ts +122 -0
- package/src/plugins/builtin/insider/headless.ts +117 -0
- package/src/plugins/builtin/insider/index.tsx +21 -50
- package/src/plugins/builtin/insider/insider-data.ts +1 -1
- package/src/plugins/builtin/insider/model.ts +72 -0
- package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
- package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
- package/src/plugins/builtin/market-movers/client.ts +76 -0
- package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
- package/src/plugins/builtin/market-movers/headless.ts +73 -0
- package/src/plugins/builtin/market-movers/index.tsx +11 -51
- package/src/plugins/builtin/news/headless.test.ts +138 -0
- package/src/plugins/builtin/news/headless.ts +190 -0
- package/src/plugins/builtin/news/index.tsx +13 -9
- package/src/plugins/builtin/news/wire/index.tsx +2 -1
- package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
- package/src/plugins/builtin/research/analyst-headless.ts +172 -0
- package/src/plugins/builtin/research/analyst-model.ts +219 -0
- package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
- package/src/plugins/builtin/research/client.ts +12 -0
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
- package/src/plugins/builtin/research/event-model.ts +245 -0
- package/src/plugins/builtin/research/events-headless.test.ts +32 -0
- package/src/plugins/builtin/research/events-headless.ts +87 -0
- package/src/plugins/builtin/research/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +32 -18
- package/src/plugins/builtin/research-search/pane.tsx +4 -0
- package/src/plugins/builtin/scanner/headless.test.ts +59 -0
- package/src/plugins/builtin/scanner/headless.ts +125 -0
- package/src/plugins/builtin/scanner/index.ts +4 -0
- package/src/plugins/builtin/sec/client.ts +16 -0
- package/src/plugins/builtin/sec/headless.test.ts +72 -0
- package/src/plugins/builtin/sec/headless.ts +69 -0
- package/src/plugins/builtin/sec/index.tsx +20 -75
- package/src/plugins/builtin/sec/model.ts +89 -0
- package/src/plugins/builtin/sectors/client.ts +54 -0
- package/src/plugins/builtin/sectors/headless.test.ts +28 -0
- package/src/plugins/builtin/sectors/headless.ts +80 -0
- package/src/plugins/builtin/sectors/index.tsx +6 -51
- package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
- package/src/plugins/builtin/short-interest/client.ts +5 -2
- package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
- package/src/plugins/builtin/short-interest/headless.ts +111 -0
- package/src/plugins/builtin/short-interest/index.tsx +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +13 -11
- package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
- package/src/plugins/builtin/thirteenf/headless.ts +286 -0
- package/src/plugins/builtin/thirteenf/index.tsx +4 -0
- package/src/plugins/builtin/thirteenf/model.ts +13 -0
- package/src/plugins/builtin/thirteenf/table.ts +3 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
- package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
- package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
- package/src/plugins/builtin/volatility/client.ts +16 -3
- package/src/plugins/builtin/volatility/headless.test.ts +35 -0
- package/src/plugins/builtin/volatility/headless.ts +75 -0
- package/src/plugins/builtin/volatility/index.tsx +4 -0
- package/src/plugins/builtin/world-indices/client.ts +43 -0
- package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
- package/src/plugins/builtin/world-indices/headless.ts +109 -0
- package/src/plugins/builtin/world-indices/index.tsx +4 -0
- package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
- package/src/plugins/builtin/yield-curve/headless.ts +67 -0
- package/src/plugins/builtin/yield-curve/index.tsx +6 -8
- package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
- package/src/plugins/bundle.test.ts +58 -1
- package/src/plugins/bundle.ts +4 -2
- package/src/plugins/catalog-backend.test.ts +26 -0
- package/src/plugins/catalog-backend.ts +9 -2
- package/src/plugins/catalog-browser.test.ts +0 -13
- package/src/plugins/catalog-browser.ts +4 -0
- package/src/plugins/catalog-manifest.test.ts +15 -0
- package/src/plugins/catalog-manifest.ts +12 -5
- package/src/plugins/catalog-ui.ts +0 -6
- package/src/plugins/host-contract.ts +9 -0
- package/src/plugins/host-modules.ts +12 -0
- package/src/plugins/loader.ts +63 -15
- package/src/plugins/prediction-markets/headless.test.ts +119 -0
- package/src/plugins/prediction-markets/headless.ts +255 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/seed.ts +3 -0
- package/src/public/broker.ts +6 -0
- package/src/remote/schema.test.ts +65 -0
- package/src/remote/schema.ts +322 -39
- package/src/remote/types.ts +49 -2
- package/src/renderers/browser/app-services.ts +3 -0
- package/src/renderers/browser/config-host.ts +24 -8
- package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
- package/src/renderers/browser/deeplink-bridge.ts +15 -3
- package/src/renderers/browser/main.tsx +3 -1
- package/src/renderers/browser/research-entry.test.ts +8 -0
- package/src/renderers/browser/research-entry.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
- package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
- package/src/renderers/electrobun/view/build-assets.ts +0 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
- package/src/sources/gloomberb-cloud/news.ts +1 -0
- package/src/sync/core-contributors.test.ts +111 -0
- package/src/sync/core-contributors.ts +155 -1
- package/src/time-series/resolve.test.ts +118 -1
- package/src/time-series/resolve.ts +14 -16
- package/src/types/config.ts +2 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
- package/src/plugins/broker-sync/native-loader.ts +0 -10
- package/src/plugins/broker-sync/normalize.ts +0 -308
- package/src/plugins/broker-sync/public.ts +0 -122
- package/src/plugins/broker-sync/robinhood-native.ts +0 -367
- package/src/plugins/broker-sync/robinhood.ts +0 -114
- package/src/plugins/broker-sync/simplefin-native.ts +0 -201
- package/src/plugins/broker-sync/simplefin.ts +0 -70
- package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { SecFilingItem } from "../../../types/data-provider";
|
|
2
|
+
import { formatCompact, formatCurrency } from "../../../utils/format";
|
|
3
|
+
import {
|
|
4
|
+
transactionTypeLabel,
|
|
5
|
+
type InsiderTransaction,
|
|
6
|
+
} from "./insider-data";
|
|
7
|
+
|
|
8
|
+
const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
export interface ParsedInsiderFiling {
|
|
11
|
+
filing: SecFilingItem;
|
|
12
|
+
transaction: InsiderTransaction | null;
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Null while filings are still parsing, so the pane never claims no activity early. */
|
|
17
|
+
export function buildInsiderSummary(
|
|
18
|
+
parsed: ParsedInsiderFiling[],
|
|
19
|
+
now = Date.now(),
|
|
20
|
+
): string | null {
|
|
21
|
+
if (parsed.some(({ isLoading }) => isLoading)) return null;
|
|
22
|
+
const cutoff = new Date(now - NINETY_DAYS_MS);
|
|
23
|
+
let buyShares = 0;
|
|
24
|
+
let sellShares = 0;
|
|
25
|
+
let buyValue = 0;
|
|
26
|
+
let sellValue = 0;
|
|
27
|
+
|
|
28
|
+
for (const { transaction } of parsed) {
|
|
29
|
+
if (!transaction) continue;
|
|
30
|
+
const txDate = transaction.filingDate instanceof Date
|
|
31
|
+
? transaction.filingDate
|
|
32
|
+
: new Date(transaction.filingDate);
|
|
33
|
+
if (Number.isNaN(txDate.getTime()) || txDate < cutoff) continue;
|
|
34
|
+
if (transaction.transactionType === "P") {
|
|
35
|
+
buyShares += transaction.shares;
|
|
36
|
+
buyValue += transaction.totalValue ?? 0;
|
|
37
|
+
} else if (transaction.transactionType === "S") {
|
|
38
|
+
sellShares += transaction.shares;
|
|
39
|
+
sellValue += transaction.totalValue ?? 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (buyShares === 0 && sellShares === 0) return "No buy/sell activity in last 90 days.";
|
|
44
|
+
|
|
45
|
+
const parts: string[] = [];
|
|
46
|
+
if (buyShares > 0) parts.push(`Bought ${formatCompact(buyShares)} shares (${formatCurrency(buyValue)})`);
|
|
47
|
+
if (sellShares > 0) parts.push(`Sold ${formatCompact(sellShares)} shares (${formatCurrency(sellValue)})`);
|
|
48
|
+
return parts.join(" | ");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildInsiderRows(parsed: readonly ParsedInsiderFiling[]) {
|
|
52
|
+
return parsed.map(({ filing, transaction, isLoading }) => ({
|
|
53
|
+
filingDate: filing.filingDate instanceof Date
|
|
54
|
+
? filing.filingDate.toISOString()
|
|
55
|
+
: String(filing.filingDate),
|
|
56
|
+
transactionDate: transaction?.filingDate instanceof Date
|
|
57
|
+
? transaction.filingDate.toISOString()
|
|
58
|
+
: transaction?.filingDate ? String(transaction.filingDate) : null,
|
|
59
|
+
insider: transaction?.reportedName ?? null,
|
|
60
|
+
title: transaction?.title ?? null,
|
|
61
|
+
side: transaction ? transactionTypeLabel(transaction.transactionType) : null,
|
|
62
|
+
transactionCode: transaction?.transactionType ?? null,
|
|
63
|
+
shares: transaction?.shares ?? null,
|
|
64
|
+
pricePerShare: transaction?.pricePerShare ?? null,
|
|
65
|
+
totalValue: transaction?.totalValue ?? null,
|
|
66
|
+
sharesOwnedAfter: transaction?.sharesOwned ?? null,
|
|
67
|
+
form: filing.form,
|
|
68
|
+
accessionNumber: filing.accessionNumber,
|
|
69
|
+
url: filing.filingUrl,
|
|
70
|
+
status: isLoading ? "loading" : transaction ? "parsed" : "unavailable",
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
@@ -256,6 +256,21 @@ async function fetchUpcomingIpos(): Promise<IPORecord[]> {
|
|
|
256
256
|
return parseCalendarRecords(flat);
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
export function formatIpoDate(date: Date): string {
|
|
260
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
261
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
262
|
+
return `${date.getUTCFullYear()}-${month}-${day}`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function matchesIpoRecord(record: IPORecord, query: string): boolean {
|
|
266
|
+
if (!query) return true;
|
|
267
|
+
const normalized = query.toLowerCase();
|
|
268
|
+
return record.ticker.toLowerCase().includes(normalized)
|
|
269
|
+
|| record.companyName.toLowerCase().includes(normalized)
|
|
270
|
+
|| (record.exchange?.toLowerCase().includes(normalized) ?? false)
|
|
271
|
+
|| record.status.includes(normalized);
|
|
272
|
+
}
|
|
273
|
+
|
|
259
274
|
function dedupeAndSort(records: IPORecord[]): IPORecord[] {
|
|
260
275
|
const byTicker = new Map<string, IPORecord>();
|
|
261
276
|
const statusOrder: Record<IPOStatus, number> = { upcoming: 0, priced: 1, trading: 2 };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
3
|
+
import { createIpoCalendarHeadless } from "./headless";
|
|
4
|
+
import type { IPORecord } from "./types";
|
|
5
|
+
|
|
6
|
+
const records: IPORecord[] = [
|
|
7
|
+
{
|
|
8
|
+
ticker: "ALFA",
|
|
9
|
+
companyName: "Alpha Labs",
|
|
10
|
+
date: new Date("2026-09-10T00:00:00.000Z"),
|
|
11
|
+
status: "upcoming",
|
|
12
|
+
exchange: "NASDAQ",
|
|
13
|
+
offerSize: 120_000_000,
|
|
14
|
+
priceRange: [18, 20],
|
|
15
|
+
pricedPrice: null,
|
|
16
|
+
shares: 6_000_000,
|
|
17
|
+
closePrice: null,
|
|
18
|
+
change1D: null,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
ticker: "BETA",
|
|
22
|
+
companyName: "Beta Systems",
|
|
23
|
+
date: new Date("2026-09-03T00:00:00.000Z"),
|
|
24
|
+
status: "trading",
|
|
25
|
+
exchange: "NYSE",
|
|
26
|
+
offerSize: null,
|
|
27
|
+
priceRange: null,
|
|
28
|
+
pricedPrice: 24,
|
|
29
|
+
shares: null,
|
|
30
|
+
closePrice: 30,
|
|
31
|
+
change1D: 25,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function args(
|
|
36
|
+
argument: string | null = null,
|
|
37
|
+
options: Record<string, string | number | boolean> = { status: "all", limit: 50 },
|
|
38
|
+
): HeadlessPaneLoadArgs {
|
|
39
|
+
return {
|
|
40
|
+
rawArgument: argument ?? "",
|
|
41
|
+
argument,
|
|
42
|
+
symbols: [],
|
|
43
|
+
options,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const context = {} as HeadlessPaneContext;
|
|
48
|
+
|
|
49
|
+
describe("IPO calendar headless", () => {
|
|
50
|
+
test("maps IPO records into structured rows", async () => {
|
|
51
|
+
const definition = createIpoCalendarHeadless({
|
|
52
|
+
loadCalendar: async () => ({
|
|
53
|
+
records,
|
|
54
|
+
fetchedAt: 123,
|
|
55
|
+
stale: false,
|
|
56
|
+
errors: [],
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const result = await definition.load(args("alpha"), context);
|
|
61
|
+
|
|
62
|
+
expect(result.rows).toEqual([{
|
|
63
|
+
ticker: "ALFA",
|
|
64
|
+
company: "Alpha Labs",
|
|
65
|
+
date: "2026-09-10",
|
|
66
|
+
status: "upcoming",
|
|
67
|
+
exchange: "NASDAQ",
|
|
68
|
+
offerSize: 120_000_000,
|
|
69
|
+
priceLow: 18,
|
|
70
|
+
priceHigh: 20,
|
|
71
|
+
pricedPrice: null,
|
|
72
|
+
shares: 6_000_000,
|
|
73
|
+
closePrice: null,
|
|
74
|
+
change1D: null,
|
|
75
|
+
}]);
|
|
76
|
+
expect(result.metadata).toMatchObject({ total: 1, returned: 1, truncated: false });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("applies status and limit options", async () => {
|
|
80
|
+
const definition = createIpoCalendarHeadless({
|
|
81
|
+
loadCalendar: async () => ({ records, fetchedAt: 123, stale: false, errors: [] }),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const result = await definition.load(args(null, { status: "trading", limit: 1 }), context);
|
|
85
|
+
|
|
86
|
+
expect(result.rows.map((row) => row.ticker)).toEqual(["BETA"]);
|
|
87
|
+
expect(result.metadata).toMatchObject({ status: "trading", returned: 1 });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HeadlessPaneDefinition,
|
|
3
|
+
HeadlessPaneLoadArgs,
|
|
4
|
+
HeadlessRowsResult,
|
|
5
|
+
} from "../../../types/plugin";
|
|
6
|
+
import { formatIpoDate, matchesIpoRecord } from "./client";
|
|
7
|
+
import { loadIpoCalendar, type IpoCalendarResult } from "./cache";
|
|
8
|
+
import type { IPORecord, IPOStatus } from "./types";
|
|
9
|
+
|
|
10
|
+
const IPO_COLUMNS = [
|
|
11
|
+
{ key: "ticker", header: "Ticker" },
|
|
12
|
+
{ key: "company", header: "Company" },
|
|
13
|
+
{ key: "date", header: "Date" },
|
|
14
|
+
{ key: "status", header: "Status" },
|
|
15
|
+
{ key: "exchange", header: "Exchange" },
|
|
16
|
+
{ key: "offerSize", header: "Offer", align: "right" as const },
|
|
17
|
+
{ key: "priceLow", header: "Price low", align: "right" as const },
|
|
18
|
+
{ key: "priceHigh", header: "Price high", align: "right" as const },
|
|
19
|
+
{ key: "pricedPrice", header: "Priced", align: "right" as const },
|
|
20
|
+
{ key: "shares", header: "Shares", align: "right" as const },
|
|
21
|
+
{ key: "closePrice", header: "Close", align: "right" as const },
|
|
22
|
+
{ key: "change1D", header: "1D %", align: "right" as const },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export interface IpoCalendarHeadlessDependencies {
|
|
26
|
+
loadCalendar(): Promise<IpoCalendarResult>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const defaultDependencies: IpoCalendarHeadlessDependencies = {
|
|
30
|
+
loadCalendar: () => loadIpoCalendar(false),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function statusOption(args: HeadlessPaneLoadArgs): IPOStatus | "all" {
|
|
34
|
+
return String(args.options.status ?? "all") as IPOStatus | "all";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toHeadlessRow(record: IPORecord) {
|
|
38
|
+
return {
|
|
39
|
+
ticker: record.ticker,
|
|
40
|
+
company: record.companyName,
|
|
41
|
+
date: formatIpoDate(record.date),
|
|
42
|
+
status: record.status,
|
|
43
|
+
exchange: record.exchange,
|
|
44
|
+
offerSize: record.offerSize,
|
|
45
|
+
priceLow: record.priceRange?.[0] ?? null,
|
|
46
|
+
priceHigh: record.priceRange?.[1] ?? null,
|
|
47
|
+
pricedPrice: record.pricedPrice,
|
|
48
|
+
shares: record.shares,
|
|
49
|
+
closePrice: record.closePrice,
|
|
50
|
+
change1D: record.change1D,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function projectIpoCalendarHeadless(
|
|
55
|
+
result: IpoCalendarResult,
|
|
56
|
+
args: HeadlessPaneLoadArgs,
|
|
57
|
+
): HeadlessRowsResult {
|
|
58
|
+
const query = typeof args.argument === "string" ? args.argument.trim() : "";
|
|
59
|
+
const status = statusOption(args);
|
|
60
|
+
const limit = Number(args.options.limit ?? 50);
|
|
61
|
+
const matching = result.records.filter((record) => (
|
|
62
|
+
matchesIpoRecord(record, query)
|
|
63
|
+
&& (status === "all" || record.status === status)
|
|
64
|
+
));
|
|
65
|
+
const rows = matching.slice(0, limit).map(toHeadlessRow);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
columns: IPO_COLUMNS,
|
|
69
|
+
rows,
|
|
70
|
+
errors: result.errors.length > 0 ? result.errors : undefined,
|
|
71
|
+
metadata: {
|
|
72
|
+
fetchedAt: result.fetchedAt,
|
|
73
|
+
stale: result.stale,
|
|
74
|
+
total: matching.length,
|
|
75
|
+
returned: rows.length,
|
|
76
|
+
truncated: rows.length < matching.length,
|
|
77
|
+
query: query || null,
|
|
78
|
+
status,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createIpoCalendarHeadless(
|
|
84
|
+
dependencies: IpoCalendarHeadlessDependencies = defaultDependencies,
|
|
85
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
86
|
+
return {
|
|
87
|
+
shape: "rows",
|
|
88
|
+
argument: {
|
|
89
|
+
kind: "free-text",
|
|
90
|
+
placeholder: "company or ticker",
|
|
91
|
+
description: "Optional company, ticker, exchange, or status filter.",
|
|
92
|
+
optional: true,
|
|
93
|
+
},
|
|
94
|
+
options: [
|
|
95
|
+
{
|
|
96
|
+
key: "status",
|
|
97
|
+
description: "IPO stage to include.",
|
|
98
|
+
type: "enum",
|
|
99
|
+
values: [
|
|
100
|
+
{ value: "all" },
|
|
101
|
+
{ value: "upcoming" },
|
|
102
|
+
{ value: "priced" },
|
|
103
|
+
{ value: "trading" },
|
|
104
|
+
],
|
|
105
|
+
defaultValue: "all",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: "limit",
|
|
109
|
+
description: "Maximum IPO rows to return.",
|
|
110
|
+
type: "integer",
|
|
111
|
+
defaultValue: 50,
|
|
112
|
+
minimum: 1,
|
|
113
|
+
maximum: 200,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
columns: IPO_COLUMNS,
|
|
117
|
+
describe: (args) => {
|
|
118
|
+
const status = statusOption(args);
|
|
119
|
+
return status === "all" ? "IPO Calendar" : `IPO Calendar | ${status}`;
|
|
120
|
+
},
|
|
121
|
+
async load(args) {
|
|
122
|
+
return projectIpoCalendarHeadless(await dependencies.loadCalendar(), args);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const ipoCalendarHeadless = createIpoCalendarHeadless();
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "./client";
|
|
7
7
|
import { attachIpoCalendarPersistence, resetIpoCalendarPersistence } from "./cache";
|
|
8
8
|
import { IPOCalendarPane } from "./pane";
|
|
9
|
+
import { ipoCalendarHeadless } from "./headless";
|
|
9
10
|
import { IPO_CALENDAR_PANE_ID } from "./types";
|
|
10
11
|
|
|
11
12
|
let disposeConnection: (() => void) | null = null;
|
|
@@ -60,6 +61,7 @@ export const ipoCalendarModule: PluginModule = {
|
|
|
60
61
|
"debut",
|
|
61
62
|
],
|
|
62
63
|
shortcut: { prefix: "IPO" },
|
|
64
|
+
headless: ipoCalendarHeadless,
|
|
63
65
|
createInstance: () => ({ placement: "floating" }),
|
|
64
66
|
},
|
|
65
67
|
],
|
|
@@ -2,6 +2,7 @@ import type { DataTableColumn } from "../../../components";
|
|
|
2
2
|
import { colors } from "../../../theme/colors";
|
|
3
3
|
import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
|
|
4
4
|
import type { IPORecord, IPOStatus } from "./types";
|
|
5
|
+
import { formatIpoDate, matchesIpoRecord } from "./client";
|
|
5
6
|
|
|
6
7
|
type IPOColumnId =
|
|
7
8
|
| "ticker"
|
|
@@ -37,11 +38,7 @@ export function statusColor(status: IPOStatus): string {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export
|
|
41
|
-
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
42
|
-
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
43
|
-
return `${date.getUTCFullYear()}-${month}-${day}`;
|
|
44
|
-
}
|
|
41
|
+
export const formatDate = formatIpoDate;
|
|
45
42
|
|
|
46
43
|
export function formatOfferSize(value: number | null): string {
|
|
47
44
|
if (value == null) return "—";
|
|
@@ -133,13 +130,4 @@ export function nextSortPreference(current: IPOSortPreference, columnId: string)
|
|
|
133
130
|
return DEFAULT_SORT_PREFERENCE;
|
|
134
131
|
}
|
|
135
132
|
|
|
136
|
-
export
|
|
137
|
-
if (!query) return true;
|
|
138
|
-
const q = query.toLowerCase();
|
|
139
|
-
return (
|
|
140
|
-
record.ticker.toLowerCase().includes(q)
|
|
141
|
-
|| record.companyName.toLowerCase().includes(q)
|
|
142
|
-
|| (record.exchange?.toLowerCase().includes(q) ?? false)
|
|
143
|
-
|| record.status.includes(q)
|
|
144
|
-
);
|
|
145
|
-
}
|
|
133
|
+
export const matchesSearch = matchesIpoRecord;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { DataProvider } from "../../../types/data-provider";
|
|
2
|
+
import { CATEGORY_MAP, screenerQuoteFromQuote, type TabId } from "./model";
|
|
3
|
+
import {
|
|
4
|
+
fetchPreferredMarketMovers,
|
|
5
|
+
fetchTrending,
|
|
6
|
+
type MarketMoversResult,
|
|
7
|
+
type ScreenerQuote,
|
|
8
|
+
type TrendingSymbol,
|
|
9
|
+
} from "./screener";
|
|
10
|
+
|
|
11
|
+
export interface MarketMoverTabResult extends MarketMoversResult {
|
|
12
|
+
tab: TabId;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MarketMoverTabDependencies {
|
|
16
|
+
fetchPreferred(
|
|
17
|
+
category: "day_gainers" | "day_losers" | "most_actives",
|
|
18
|
+
count: number,
|
|
19
|
+
options?: { forceRefresh?: boolean },
|
|
20
|
+
): Promise<MarketMoversResult>;
|
|
21
|
+
fetchTrending(count: number, options?: { forceRefresh?: boolean }): Promise<TrendingSymbol[]>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const defaultDependencies: MarketMoverTabDependencies = {
|
|
25
|
+
fetchPreferred: (category, count, options) => fetchPreferredMarketMovers(category, count, options),
|
|
26
|
+
fetchTrending: (count, options) => fetchTrending(count, undefined, options),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
async function hydrateTrending(
|
|
30
|
+
trending: readonly TrendingSymbol[],
|
|
31
|
+
provider: DataProvider | null,
|
|
32
|
+
): Promise<ScreenerQuote[]> {
|
|
33
|
+
const symbols = trending.slice(0, 25).map(({ symbol }) => symbol);
|
|
34
|
+
const bySymbol = new Map<string, ScreenerQuote>();
|
|
35
|
+
if (!provider) return [];
|
|
36
|
+
if (provider.getQuotesBatch) {
|
|
37
|
+
const results = await provider.getQuotesBatch(
|
|
38
|
+
symbols.map((symbol) => ({ symbol, exchange: "" })),
|
|
39
|
+
).catch(() => []);
|
|
40
|
+
for (const result of results) {
|
|
41
|
+
if (result.quote) bySymbol.set(result.target.symbol, screenerQuoteFromQuote(result.target.symbol, result.quote));
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
await Promise.all(symbols.map(async (symbol) => {
|
|
45
|
+
try {
|
|
46
|
+
const quote = await provider.getQuote(symbol, "");
|
|
47
|
+
bySymbol.set(symbol, screenerQuoteFromQuote(symbol, quote));
|
|
48
|
+
} catch {
|
|
49
|
+
// Missing quotes are omitted from a ranked list.
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
return symbols.flatMap((symbol) => {
|
|
54
|
+
const quote = bySymbol.get(symbol);
|
|
55
|
+
return quote ? [quote] : [];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function loadMarketMoverTab(
|
|
60
|
+
tab: TabId,
|
|
61
|
+
provider: DataProvider | null,
|
|
62
|
+
options?: { forceRefresh?: boolean },
|
|
63
|
+
dependencies: MarketMoverTabDependencies = defaultDependencies,
|
|
64
|
+
): Promise<MarketMoverTabResult> {
|
|
65
|
+
if (tab === "trending") {
|
|
66
|
+
const trending = await dependencies.fetchTrending(25, options);
|
|
67
|
+
return {
|
|
68
|
+
tab,
|
|
69
|
+
quotes: await hydrateTrending(trending, provider),
|
|
70
|
+
source: "yahoo",
|
|
71
|
+
stale: false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const result = await dependencies.fetchPreferred(CATEGORY_MAP[tab], 25, options);
|
|
75
|
+
return { ...result, tab };
|
|
76
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
3
|
+
import { createMarketMoversHeadless } from "./headless";
|
|
4
|
+
|
|
5
|
+
function args(list: string): HeadlessPaneLoadArgs {
|
|
6
|
+
return { rawArgument: "", argument: null, symbols: [], options: { list } };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("market movers headless model", () => {
|
|
10
|
+
test("loads the selected list and computes each quote's 52-week position", async () => {
|
|
11
|
+
const loaded: string[] = [];
|
|
12
|
+
const headless = createMarketMoversHeadless({
|
|
13
|
+
load: async (_args, tab) => {
|
|
14
|
+
loaded.push(tab);
|
|
15
|
+
return {
|
|
16
|
+
tab,
|
|
17
|
+
source: "cloud",
|
|
18
|
+
stale: false,
|
|
19
|
+
quotes: [{
|
|
20
|
+
symbol: "NVDA", name: "NVIDIA", price: 150, change: 5, changePercent: 3.4,
|
|
21
|
+
volume: 100, avgVolume: 50, volumeRatio: 2, marketCap: 1_000,
|
|
22
|
+
currency: "USD", fiftyTwoWeekHigh: 200, fiftyTwoWeekLow: 100,
|
|
23
|
+
dayHigh: 151, dayLow: 142, exchange: "NASDAQ",
|
|
24
|
+
}],
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const gainers = await headless.load(args("gainers"), {} as HeadlessPaneContext);
|
|
30
|
+
await headless.load(args("actives"), {} as HeadlessPaneContext);
|
|
31
|
+
expect(loaded).toEqual(["gainers", "actives"]);
|
|
32
|
+
expect(gainers.rows).toEqual([expect.objectContaining({ rank: 1, rangePositionPercent: 50 })]);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { DataProvider } from "../../../types/data-provider";
|
|
2
|
+
import type {
|
|
3
|
+
HeadlessPaneDefinition,
|
|
4
|
+
HeadlessPaneLoadArgs,
|
|
5
|
+
} from "../../../types/plugin";
|
|
6
|
+
import { formatCompact, formatCurrency, formatNumber, formatPercentRaw } from "../../../utils/format";
|
|
7
|
+
import { loadMarketMoverTab, type MarketMoverTabResult } from "./client";
|
|
8
|
+
import { createRows, fiftyTwoWeekPositionPercent, type TabId } from "./model";
|
|
9
|
+
|
|
10
|
+
const COLUMNS = [
|
|
11
|
+
{ key: "rank", header: "Rank", align: "right" as const },
|
|
12
|
+
{ key: "symbol", header: "Symbol" },
|
|
13
|
+
{ key: "name", header: "Name" },
|
|
14
|
+
{ key: "price", header: "Last", align: "right" as const, format: (value: unknown, row: Record<string, unknown>) => formatCurrency(Number(value), String(row.currency)) },
|
|
15
|
+
{ key: "changePercent", header: "Change %", align: "right" as const, format: (value: unknown) => formatPercentRaw(Number(value)) },
|
|
16
|
+
{ key: "volume", header: "Volume", align: "right" as const, format: (value: unknown) => formatCompact(Number(value)) },
|
|
17
|
+
{ key: "volumeRatio", header: "Vol / Avg", align: "right" as const, format: (value: unknown) => formatNumber(Number(value), 1) },
|
|
18
|
+
{ key: "rangePositionPercent", header: "52W pos", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
|
|
19
|
+
{ key: "marketCap", header: "Market cap", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatCompact(Number(value)) },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export interface MarketMoversHeadlessDependencies {
|
|
23
|
+
load(
|
|
24
|
+
args: HeadlessPaneLoadArgs,
|
|
25
|
+
tab: TabId,
|
|
26
|
+
provider: DataProvider,
|
|
27
|
+
): Promise<MarketMoverTabResult>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultDependencies: MarketMoversHeadlessDependencies = {
|
|
31
|
+
load: (_args, tab, provider) => loadMarketMoverTab(tab, provider),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function createMarketMoversHeadless(
|
|
35
|
+
dependencies: MarketMoversHeadlessDependencies = defaultDependencies,
|
|
36
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
37
|
+
return {
|
|
38
|
+
shape: "rows",
|
|
39
|
+
argument: { kind: "none" },
|
|
40
|
+
options: [{
|
|
41
|
+
key: "list",
|
|
42
|
+
description: "Market movers list.",
|
|
43
|
+
type: "enum",
|
|
44
|
+
values: [
|
|
45
|
+
{ value: "gainers" },
|
|
46
|
+
{ value: "losers" },
|
|
47
|
+
{ value: "actives", aliases: ["active", "most-active"] },
|
|
48
|
+
{ value: "trending" },
|
|
49
|
+
],
|
|
50
|
+
defaultValue: "actives",
|
|
51
|
+
}],
|
|
52
|
+
columns: COLUMNS,
|
|
53
|
+
describe: (args) => `Market Movers | ${String(args.options.list)}`,
|
|
54
|
+
async load(args, ctx) {
|
|
55
|
+
const tab = args.options.list as TabId;
|
|
56
|
+
const result = await dependencies.load(args, tab, ctx.marketData);
|
|
57
|
+
const rows = createRows(result.quotes).map((row) => ({
|
|
58
|
+
...row,
|
|
59
|
+
rangePositionPercent: fiftyTwoWeekPositionPercent(
|
|
60
|
+
row.price,
|
|
61
|
+
row.fiftyTwoWeekLow,
|
|
62
|
+
row.fiftyTwoWeekHigh,
|
|
63
|
+
),
|
|
64
|
+
}));
|
|
65
|
+
return {
|
|
66
|
+
rows,
|
|
67
|
+
metadata: { list: tab, source: result.source, stale: result.stale },
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const marketMoversHeadless = createMarketMoversHeadless();
|
|
@@ -13,15 +13,12 @@ import { useAutoRefresh } from "../shared/auto-refresh";
|
|
|
13
13
|
import { useQuoteBoard } from "../shared/use-quote-board";
|
|
14
14
|
import {
|
|
15
15
|
attachMarketMoversPersistence,
|
|
16
|
-
fetchPreferredMarketMovers,
|
|
17
|
-
fetchTrending,
|
|
18
16
|
MARKET_SUMMARY_SYMBOLS,
|
|
19
17
|
resetMarketMoversPersistence,
|
|
20
18
|
type ScreenerQuote,
|
|
21
19
|
type MarketSummaryQuote,
|
|
22
20
|
} from "./screener";
|
|
23
21
|
import {
|
|
24
|
-
CATEGORY_MAP,
|
|
25
22
|
DEFAULT_SORT_PREFERENCE,
|
|
26
23
|
INDEX_SHORT,
|
|
27
24
|
TABS,
|
|
@@ -29,7 +26,6 @@ import {
|
|
|
29
26
|
nextSortPreference,
|
|
30
27
|
resolveSummarySymbols,
|
|
31
28
|
resolveTabs,
|
|
32
|
-
screenerQuoteFromQuote,
|
|
33
29
|
sortRows,
|
|
34
30
|
summaryQuoteFromQuote,
|
|
35
31
|
type MarketMoverColumn,
|
|
@@ -37,6 +33,8 @@ import {
|
|
|
37
33
|
type MarketMoverSortPreference,
|
|
38
34
|
type TabId,
|
|
39
35
|
} from "./model";
|
|
36
|
+
import { loadMarketMoverTab } from "./client";
|
|
37
|
+
import { marketMoversHeadless } from "./headless";
|
|
40
38
|
import { buildMarketMoverColumns, renderMarketMoverCell } from "./table";
|
|
41
39
|
import {
|
|
42
40
|
LIVE_STREAMING_QUICK_SETTING,
|
|
@@ -49,6 +47,8 @@ import {
|
|
|
49
47
|
resolveScreenerQuoteFeedStatus,
|
|
50
48
|
} from "../shared/screener-live-quotes";
|
|
51
49
|
|
|
50
|
+
export { marketMoversHeadless } from "./headless";
|
|
51
|
+
|
|
52
52
|
/** Stable identity: a fresh literal here would reload the board every render. */
|
|
53
53
|
const NO_SAVED_SELECTION: string[] = [];
|
|
54
54
|
|
|
@@ -138,54 +138,13 @@ function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
forceRefresh: options?.forceRefresh,
|
|
146
|
-
});
|
|
147
|
-
if (fetchGenRef.current !== gen) return;
|
|
148
|
-
|
|
149
|
-
const resolved: ScreenerQuote[] = [];
|
|
150
|
-
|
|
151
|
-
if (dataProvider) {
|
|
152
|
-
const targets = trending.slice(0, 25).map(({ symbol }) => ({ symbol, exchange: "" }));
|
|
153
|
-
if (dataProvider.getQuotesBatch) {
|
|
154
|
-
const batchResults = await dataProvider.getQuotesBatch(targets).catch(() => []);
|
|
155
|
-
for (const result of batchResults) {
|
|
156
|
-
if (fetchGenRef.current !== gen) return;
|
|
157
|
-
if (result.quote) {
|
|
158
|
-
resolved.push(screenerQuoteFromQuote(result.target.symbol, result.quote));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
await Promise.allSettled(targets.map(async ({ symbol }) => {
|
|
163
|
-
try {
|
|
164
|
-
const q = await dataProvider.getQuote(symbol, "");
|
|
165
|
-
if (fetchGenRef.current !== gen) return;
|
|
166
|
-
if (q) {
|
|
167
|
-
resolved.push(screenerQuoteFromQuote(symbol, q));
|
|
168
|
-
}
|
|
169
|
-
} catch { /* skip */ }
|
|
170
|
-
}));
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (fetchGenRef.current !== gen) return;
|
|
175
|
-
data = trending
|
|
176
|
-
.map((t) => resolved.find((r) => r.symbol === t.symbol))
|
|
177
|
-
.filter((r): r is ScreenerQuote => r !== undefined);
|
|
178
|
-
setMoversStale(false);
|
|
179
|
-
} else {
|
|
180
|
-
const result = await fetchPreferredMarketMovers(CATEGORY_MAP[tab], 25, {
|
|
181
|
-
forceRefresh: options?.forceRefresh,
|
|
182
|
-
});
|
|
183
|
-
if (fetchGenRef.current !== gen) return;
|
|
184
|
-
data = result.quotes;
|
|
185
|
-
setMoversStale(result.stale);
|
|
186
|
-
}
|
|
141
|
+
const result = await loadMarketMoverTab(tab, dataProvider, {
|
|
142
|
+
forceRefresh: options?.forceRefresh,
|
|
143
|
+
});
|
|
144
|
+
if (fetchGenRef.current !== gen) return;
|
|
187
145
|
|
|
188
|
-
setQuotes(
|
|
146
|
+
setQuotes(result.quotes);
|
|
147
|
+
setMoversStale(result.stale);
|
|
189
148
|
if (!options?.background) setSelectedSymbol(null);
|
|
190
149
|
setLoadError(null);
|
|
191
150
|
setLastLoadedAt(Date.now());
|
|
@@ -361,6 +320,7 @@ export const marketMoversModule: PluginModule = {
|
|
|
361
320
|
description: "Top gainers, losers, most active, and trending tickers.",
|
|
362
321
|
keywords: ["movers", "gainers", "losers", "active", "trending", "screener", "top"],
|
|
363
322
|
shortcut: { prefix: "MOST" },
|
|
323
|
+
headless: marketMoversHeadless,
|
|
364
324
|
},
|
|
365
325
|
],
|
|
366
326
|
};
|