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,69 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HeadlessPaneColumn,
|
|
3
|
+
HeadlessPaneContext,
|
|
4
|
+
HeadlessPaneDefinition,
|
|
5
|
+
HeadlessPaneLoadArgs,
|
|
6
|
+
} from "../../../types/plugin";
|
|
7
|
+
import type { SecFilingItem } from "../../../types/data-provider";
|
|
8
|
+
import { loadSecFilings } from "./client";
|
|
9
|
+
import { buildSecFilingRows } from "./model";
|
|
10
|
+
|
|
11
|
+
const SEC_COLUMNS: HeadlessPaneColumn[] = [
|
|
12
|
+
{
|
|
13
|
+
key: "filedAt",
|
|
14
|
+
header: "Filed",
|
|
15
|
+
format: (value) => typeof value === "string" ? value.slice(0, 10) : "-",
|
|
16
|
+
},
|
|
17
|
+
{ key: "form", header: "Form" },
|
|
18
|
+
{ key: "filing", header: "Filing" },
|
|
19
|
+
{ key: "items", header: "Items" },
|
|
20
|
+
{ key: "accessionNumber", header: "Accession" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export interface SecHeadlessDependencies {
|
|
24
|
+
loadFilings(
|
|
25
|
+
symbol: string,
|
|
26
|
+
limit: number,
|
|
27
|
+
args: HeadlessPaneLoadArgs,
|
|
28
|
+
ctx: HeadlessPaneContext,
|
|
29
|
+
): Promise<SecFilingItem[]>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const defaultDependencies: SecHeadlessDependencies = {
|
|
33
|
+
loadFilings: (symbol, limit, _args, ctx) => loadSecFilings(ctx.marketData, symbol, limit),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function createSecHeadless(
|
|
37
|
+
dependencies: SecHeadlessDependencies = defaultDependencies,
|
|
38
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
39
|
+
return {
|
|
40
|
+
shape: "rows",
|
|
41
|
+
argument: {
|
|
42
|
+
kind: "ticker",
|
|
43
|
+
placeholder: "ticker",
|
|
44
|
+
description: "US equity ticker.",
|
|
45
|
+
},
|
|
46
|
+
options: [{
|
|
47
|
+
key: "limit",
|
|
48
|
+
aliases: ["count", "rows"],
|
|
49
|
+
description: "Maximum recent filings.",
|
|
50
|
+
type: "integer",
|
|
51
|
+
defaultValue: 50,
|
|
52
|
+
minimum: 1,
|
|
53
|
+
maximum: 200,
|
|
54
|
+
}],
|
|
55
|
+
columns: SEC_COLUMNS,
|
|
56
|
+
describe: (args) => `SEC Filings | ${args.symbols[0]}`,
|
|
57
|
+
async load(args, ctx) {
|
|
58
|
+
const symbol = args.symbols[0]!;
|
|
59
|
+
const limit = Number(args.options.limit);
|
|
60
|
+
const filings = await dependencies.loadFilings(symbol, limit, args, ctx);
|
|
61
|
+
return {
|
|
62
|
+
rows: buildSecFilingRows(filings).slice(0, limit),
|
|
63
|
+
metadata: { symbol, returned: Math.min(filings.length, limit) },
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const secHeadless = createSecHeadless();
|
|
@@ -27,54 +27,19 @@ import {
|
|
|
27
27
|
useSecFilingContentCache,
|
|
28
28
|
} from "./filing-content";
|
|
29
29
|
import { usePaneStatusLinkFooter } from "../shared/pane-footer";
|
|
30
|
+
import { secHeadless } from "./headless";
|
|
31
|
+
import {
|
|
32
|
+
getFilingDisplayTitle,
|
|
33
|
+
getFormDescription,
|
|
34
|
+
getMeaningfulPrimaryDescription,
|
|
35
|
+
} from "./model";
|
|
36
|
+
|
|
37
|
+
export { secHeadless } from "./headless";
|
|
30
38
|
|
|
31
39
|
const SEC_FILING_FETCH_LIMIT = 20_000;
|
|
32
40
|
const SEC_FILING_PAGE_SIZE = 50;
|
|
33
41
|
const OWNERSHIP_FORMS = new Set(["3", "4", "5"]);
|
|
34
42
|
|
|
35
|
-
function getDisplayFormLabel(form: string): string {
|
|
36
|
-
const trimmed = form.trim();
|
|
37
|
-
return /^\d+(?:\/[A-Z])?$/i.test(trimmed)
|
|
38
|
-
? `FORM ${trimmed}`
|
|
39
|
-
: trimmed;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function normalizeComparableText(value: string): string {
|
|
43
|
-
return value
|
|
44
|
-
.toUpperCase()
|
|
45
|
-
.replace(/\bFORM\b/g, "")
|
|
46
|
-
.replace(/[^A-Z0-9]+/g, "");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function escapeRegExp(value: string): string {
|
|
50
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function stripRedundantFormPrefix(form: string, description: string): string {
|
|
54
|
-
const pattern = escapeRegExp(form.trim()).replace(/\s+/g, "\\s+");
|
|
55
|
-
return description
|
|
56
|
-
.trim()
|
|
57
|
-
.replace(new RegExp(`^(?:FORM\\s+)?${pattern}(?:\\s*[:|-]\\s*|\\s+)`, "i"), "")
|
|
58
|
-
.trim();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function getMeaningfulPrimaryDescription(filing: SecFilingItem): string | undefined {
|
|
62
|
-
const description = filing.primaryDocDescription?.trim();
|
|
63
|
-
if (!description) return undefined;
|
|
64
|
-
if (normalizeComparableText(description) === normalizeComparableText(filing.form)) return undefined;
|
|
65
|
-
|
|
66
|
-
const stripped = stripRedundantFormPrefix(filing.form, description);
|
|
67
|
-
if (!stripped) return undefined;
|
|
68
|
-
if (normalizeComparableText(stripped) === normalizeComparableText(filing.form)) return undefined;
|
|
69
|
-
return stripped;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function getFilingDisplayTitle(filing: SecFilingItem): string {
|
|
73
|
-
const description = getMeaningfulPrimaryDescription(filing);
|
|
74
|
-
const formLabel = getDisplayFormLabel(filing.form);
|
|
75
|
-
return description ? `${formLabel} | ${description}` : formLabel;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
43
|
function formatFiledAt(filing: SecFilingItem): string {
|
|
79
44
|
return formatFilingMetaDate(filing.filingDate);
|
|
80
45
|
}
|
|
@@ -161,29 +126,6 @@ function buildForm4Detail(content: string | null, filing: SecFilingItem): string
|
|
|
161
126
|
return lines.join("\n");
|
|
162
127
|
}
|
|
163
128
|
|
|
164
|
-
function getFormDescription(form: string): string {
|
|
165
|
-
const f = form.trim().toUpperCase();
|
|
166
|
-
switch (f) {
|
|
167
|
-
case "10-K": return "Annual Report";
|
|
168
|
-
case "10-K/A": return "Annual Report (Amended)";
|
|
169
|
-
case "10-Q": return "Quarterly Report";
|
|
170
|
-
case "10-Q/A": return "Quarterly Report (Amended)";
|
|
171
|
-
case "8-K": return "Current Report";
|
|
172
|
-
case "8-K/A": return "Current Report (Amended)";
|
|
173
|
-
case "4": return "Insider Transaction";
|
|
174
|
-
case "3": return "Initial Insider Ownership";
|
|
175
|
-
case "5": return "Annual Insider Ownership";
|
|
176
|
-
case "SC 13G": return "Beneficial Ownership (Passive)";
|
|
177
|
-
case "SC 13G/A": return "Beneficial Ownership (Amended)";
|
|
178
|
-
case "SC 13D": return "Beneficial Ownership (Active)";
|
|
179
|
-
case "SC 13D/A": return "Beneficial Ownership (Amended)";
|
|
180
|
-
case "DEF 14A": return "Proxy Statement";
|
|
181
|
-
case "S-1": return "Registration Statement";
|
|
182
|
-
case "20-F": return "Annual Report (Foreign)";
|
|
183
|
-
default: return "";
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
129
|
function toFeedItems(
|
|
188
130
|
filings: SecFilingItem[],
|
|
189
131
|
selectedAccessionNumber: string | undefined,
|
|
@@ -360,15 +302,18 @@ export const secModule: PluginModule = {
|
|
|
360
302
|
],
|
|
361
303
|
|
|
362
304
|
paneTemplates: [
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
305
|
+
{
|
|
306
|
+
...createTickerSurfacePaneTemplate({
|
|
307
|
+
id: "sec-pane",
|
|
308
|
+
paneId: "sec",
|
|
309
|
+
label: "SEC",
|
|
310
|
+
description: "Recent SEC filings for the selected ticker.",
|
|
311
|
+
keywords: ["sec", "filings", "10-k", "10-q", "8-k"],
|
|
312
|
+
shortcut: "SEC",
|
|
313
|
+
canCreate: (_context, options) => !options?.ticker || isUsEquityTicker(options.ticker),
|
|
314
|
+
}),
|
|
315
|
+
headless: secHeadless,
|
|
316
|
+
},
|
|
372
317
|
],
|
|
373
318
|
|
|
374
319
|
setup(ctx) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { SecFilingItem } from "../../../types/data-provider";
|
|
2
|
+
|
|
3
|
+
export function getDisplayFormLabel(form: string): string {
|
|
4
|
+
const trimmed = form.trim();
|
|
5
|
+
return /^\d+(?:\/[A-Z])?$/i.test(trimmed)
|
|
6
|
+
? `FORM ${trimmed}`
|
|
7
|
+
: trimmed;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeComparableText(value: string): string {
|
|
11
|
+
return value
|
|
12
|
+
.toUpperCase()
|
|
13
|
+
.replace(/\bFORM\b/g, "")
|
|
14
|
+
.replace(/[^A-Z0-9]+/g, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function escapeRegExp(value: string): string {
|
|
18
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stripRedundantFormPrefix(form: string, description: string): string {
|
|
22
|
+
const pattern = escapeRegExp(form.trim()).replace(/\s+/g, "\\s+");
|
|
23
|
+
return description
|
|
24
|
+
.trim()
|
|
25
|
+
.replace(new RegExp(`^(?:FORM\\s+)?${pattern}(?:\\s*[:|-]\\s*|\\s+)`, "i"), "")
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getMeaningfulPrimaryDescription(filing: SecFilingItem): string | undefined {
|
|
30
|
+
const description = filing.primaryDocDescription?.trim();
|
|
31
|
+
if (!description) return undefined;
|
|
32
|
+
if (normalizeComparableText(description) === normalizeComparableText(filing.form)) return undefined;
|
|
33
|
+
|
|
34
|
+
const stripped = stripRedundantFormPrefix(filing.form, description);
|
|
35
|
+
if (!stripped) return undefined;
|
|
36
|
+
if (normalizeComparableText(stripped) === normalizeComparableText(filing.form)) return undefined;
|
|
37
|
+
return stripped;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getFilingDisplayTitle(filing: SecFilingItem): string {
|
|
41
|
+
const description = getMeaningfulPrimaryDescription(filing);
|
|
42
|
+
const formLabel = getDisplayFormLabel(filing.form);
|
|
43
|
+
return description ? `${formLabel} | ${description}` : formLabel;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getFormDescription(form: string): string {
|
|
47
|
+
const normalized = form.trim().toUpperCase();
|
|
48
|
+
switch (normalized) {
|
|
49
|
+
case "10-K": return "Annual Report";
|
|
50
|
+
case "10-K/A": return "Annual Report (Amended)";
|
|
51
|
+
case "10-Q": return "Quarterly Report";
|
|
52
|
+
case "10-Q/A": return "Quarterly Report (Amended)";
|
|
53
|
+
case "8-K": return "Current Report";
|
|
54
|
+
case "8-K/A": return "Current Report (Amended)";
|
|
55
|
+
case "4": return "Insider Transaction";
|
|
56
|
+
case "3": return "Initial Insider Ownership";
|
|
57
|
+
case "5": return "Annual Insider Ownership";
|
|
58
|
+
case "SC 13G": return "Beneficial Ownership (Passive)";
|
|
59
|
+
case "SC 13G/A": return "Beneficial Ownership (Amended)";
|
|
60
|
+
case "SC 13D": return "Beneficial Ownership (Active)";
|
|
61
|
+
case "SC 13D/A": return "Beneficial Ownership (Amended)";
|
|
62
|
+
case "DEF 14A": return "Proxy Statement";
|
|
63
|
+
case "S-1": return "Registration Statement";
|
|
64
|
+
case "20-F": return "Annual Report (Foreign)";
|
|
65
|
+
default: return "";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildSecFilingRows(filings: readonly SecFilingItem[]) {
|
|
70
|
+
return filings.map((filing) => {
|
|
71
|
+
const displayTitle = getFilingDisplayTitle(filing);
|
|
72
|
+
const formDescription = getFormDescription(filing.form);
|
|
73
|
+
return {
|
|
74
|
+
filedAt: filing.filingDate instanceof Date
|
|
75
|
+
? filing.filingDate.toISOString()
|
|
76
|
+
: String(filing.filingDate),
|
|
77
|
+
acceptedAt: filing.acceptedAt instanceof Date
|
|
78
|
+
? filing.acceptedAt.toISOString()
|
|
79
|
+
: filing.acceptedAt ? String(filing.acceptedAt) : null,
|
|
80
|
+
form: filing.form,
|
|
81
|
+
filing: formDescription ? `${displayTitle} | ${formDescription}` : displayTitle,
|
|
82
|
+
items: filing.items ?? null,
|
|
83
|
+
accessionNumber: filing.accessionNumber,
|
|
84
|
+
primaryDocument: filing.primaryDocument ?? null,
|
|
85
|
+
cik: filing.cik,
|
|
86
|
+
url: filing.filingUrl,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { DataProvider } from "../../../types/data-provider";
|
|
2
|
+
import type { PricePoint, Quote } from "../../../types/financials";
|
|
3
|
+
import type { SectorDef } from "./sector-data";
|
|
4
|
+
import {
|
|
5
|
+
computeTrailingReturn,
|
|
6
|
+
latestHistoryClose,
|
|
7
|
+
ONE_MONTH_DAYS,
|
|
8
|
+
ONE_YEAR_DAYS,
|
|
9
|
+
type SectorRow,
|
|
10
|
+
} from "./sector-model";
|
|
11
|
+
|
|
12
|
+
export interface SectorRowOutcome {
|
|
13
|
+
etf: string;
|
|
14
|
+
row: Partial<SectorRow> | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadSectorRows(
|
|
18
|
+
sectors: readonly SectorDef[],
|
|
19
|
+
provider: DataProvider,
|
|
20
|
+
): Promise<SectorRowOutcome[]> {
|
|
21
|
+
const quotes = new Map<string, Quote | null>();
|
|
22
|
+
if (provider.getQuotesBatch) {
|
|
23
|
+
const results = await provider.getQuotesBatch(
|
|
24
|
+
sectors.map((sector) => ({ symbol: sector.etf, exchange: "" })),
|
|
25
|
+
).catch(() => []);
|
|
26
|
+
for (const result of results) quotes.set(result.target.symbol, result.quote ?? null);
|
|
27
|
+
} else {
|
|
28
|
+
await Promise.all(sectors.map(async (sector) => {
|
|
29
|
+
try {
|
|
30
|
+
quotes.set(sector.etf, await provider.getQuote(sector.etf, ""));
|
|
31
|
+
} catch {
|
|
32
|
+
quotes.set(sector.etf, null);
|
|
33
|
+
}
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Promise.all(sectors.map(async (sector) => {
|
|
38
|
+
const history: PricePoint[] = await provider.getPriceHistory(sector.etf, "", "1Y")
|
|
39
|
+
.catch(() => []);
|
|
40
|
+
const quote = quotes.get(sector.etf) ?? null;
|
|
41
|
+
if (!quote && history.length === 0) return { etf: sector.etf, row: null };
|
|
42
|
+
const price = quote?.price ?? latestHistoryClose(history);
|
|
43
|
+
return {
|
|
44
|
+
etf: sector.etf,
|
|
45
|
+
row: {
|
|
46
|
+
price,
|
|
47
|
+
changePercent: quote?.changePercent ?? null,
|
|
48
|
+
return1M: computeTrailingReturn(history, ONE_MONTH_DAYS, price),
|
|
49
|
+
return1Y: computeTrailingReturn(history, ONE_YEAR_DAYS, price),
|
|
50
|
+
currency: quote?.currency ?? "USD",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
3
|
+
import { createSectorsHeadless } from "./headless";
|
|
4
|
+
|
|
5
|
+
function args(collection: string): HeadlessPaneLoadArgs {
|
|
6
|
+
return { rawArgument: "", argument: null, symbols: [], options: { collection } };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("sectors headless model", () => {
|
|
10
|
+
test("changes the loaded ETF universe with the collection option", async () => {
|
|
11
|
+
const requested: string[][] = [];
|
|
12
|
+
const headless = createSectorsHeadless({
|
|
13
|
+
load: async (_args, definitions) => {
|
|
14
|
+
requested.push(definitions.map(({ etf }) => etf));
|
|
15
|
+
return definitions.map((definition, index) => ({
|
|
16
|
+
etf: definition.etf,
|
|
17
|
+
row: { price: 100, changePercent: index, return1M: index + 1, return1Y: index + 2, currency: "USD" },
|
|
18
|
+
}));
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const sectors = await headless.load(args("sectors"), {} as HeadlessPaneContext);
|
|
23
|
+
const industries = await headless.load(args("industries"), {} as HeadlessPaneContext);
|
|
24
|
+
expect(sectors.rows.some((row) => row.etf === "XLK")).toBe(true);
|
|
25
|
+
expect(industries.rows.some((row) => row.etf === "SMH")).toBe(true);
|
|
26
|
+
expect(requested[0]).not.toEqual(requested[1]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { DataProvider } from "../../../types/data-provider";
|
|
2
|
+
import type {
|
|
3
|
+
HeadlessPaneDefinition,
|
|
4
|
+
HeadlessPaneLoadArgs,
|
|
5
|
+
} from "../../../types/plugin";
|
|
6
|
+
import { formatCurrency, formatPercentRaw } from "../../../utils/format";
|
|
7
|
+
import { loadSectorRows, type SectorRowOutcome } from "./client";
|
|
8
|
+
import { getSectorCollection, type SectorCollectionId, type SectorDef } from "./sector-data";
|
|
9
|
+
import { DEFAULT_SORT_PREFERENCE, sortRows, type SectorRow } from "./sector-model";
|
|
10
|
+
|
|
11
|
+
const COLUMNS = [
|
|
12
|
+
{ key: "name", header: "Sector" },
|
|
13
|
+
{ key: "etf", header: "ETF" },
|
|
14
|
+
{ key: "price", header: "Last", align: "right" as const, format: (value: unknown, row: Record<string, unknown>) => value == null ? "-" : formatCurrency(Number(value), String(row.currency)) },
|
|
15
|
+
{ key: "changePercent", header: "1D", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
|
|
16
|
+
{ key: "return1M", header: "1M", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
|
|
17
|
+
{ key: "return1Y", header: "1Y", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)) },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function projectSectorRows(
|
|
21
|
+
definitions: readonly SectorDef[],
|
|
22
|
+
outcomes: readonly SectorRowOutcome[],
|
|
23
|
+
): SectorRow[] {
|
|
24
|
+
const byEtf = new Map(outcomes.map((outcome) => [outcome.etf, outcome.row]));
|
|
25
|
+
return sortRows(definitions.map((definition) => ({
|
|
26
|
+
...definition,
|
|
27
|
+
price: byEtf.get(definition.etf)?.price ?? null,
|
|
28
|
+
changePercent: byEtf.get(definition.etf)?.changePercent ?? null,
|
|
29
|
+
return1M: byEtf.get(definition.etf)?.return1M ?? null,
|
|
30
|
+
return1Y: byEtf.get(definition.etf)?.return1Y ?? null,
|
|
31
|
+
currency: byEtf.get(definition.etf)?.currency ?? "USD",
|
|
32
|
+
loading: false,
|
|
33
|
+
})), DEFAULT_SORT_PREFERENCE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SectorsHeadlessDependencies {
|
|
37
|
+
load(
|
|
38
|
+
args: HeadlessPaneLoadArgs,
|
|
39
|
+
definitions: readonly SectorDef[],
|
|
40
|
+
provider: DataProvider,
|
|
41
|
+
): Promise<SectorRowOutcome[]>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const defaultDependencies: SectorsHeadlessDependencies = {
|
|
45
|
+
load: (_args, definitions, provider) => loadSectorRows(definitions, provider),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function createSectorsHeadless(
|
|
49
|
+
dependencies: SectorsHeadlessDependencies = defaultDependencies,
|
|
50
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
51
|
+
return {
|
|
52
|
+
shape: "rows",
|
|
53
|
+
argument: { kind: "none" },
|
|
54
|
+
options: [{
|
|
55
|
+
key: "collection",
|
|
56
|
+
description: "Sector or industry ETF collection.",
|
|
57
|
+
type: "enum",
|
|
58
|
+
values: [{ value: "sectors" }, { value: "industries" }],
|
|
59
|
+
defaultValue: "sectors",
|
|
60
|
+
}],
|
|
61
|
+
columns: COLUMNS,
|
|
62
|
+
describe: (args) => `Sector Performance | ${String(args.options.collection)}`,
|
|
63
|
+
async load(args, ctx) {
|
|
64
|
+
const collectionId = args.options.collection as SectorCollectionId;
|
|
65
|
+
const definitions = getSectorCollection(collectionId).items;
|
|
66
|
+
const outcomes = await dependencies.load(args, definitions, ctx.marketData);
|
|
67
|
+
const rows = projectSectorRows(definitions, outcomes);
|
|
68
|
+
return {
|
|
69
|
+
rows: rows.map((row) => ({ ...row })),
|
|
70
|
+
metadata: {
|
|
71
|
+
collection: collectionId,
|
|
72
|
+
available: outcomes.filter((outcome) => outcome.row).length,
|
|
73
|
+
requested: definitions.length,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const sectorsHeadless = createSectorsHeadless();
|
|
@@ -3,7 +3,6 @@ import { Box } from "../../../ui";
|
|
|
3
3
|
import { DataTableView, Tabs, usePaneFooter, type DataTableCell, type DataTableKeyEvent, type PaneFooterSegment } from "../../../components";
|
|
4
4
|
import type { PaneProps } from "../../../types/plugin";
|
|
5
5
|
import type { PluginModule } from "../plugin-module";
|
|
6
|
-
import type { PricePoint, Quote } from "../../../types/financials";
|
|
7
6
|
import { usePaneSettingValue } from "../../../state/app/context";
|
|
8
7
|
import { colors, priceColor } from "../../../theme/colors";
|
|
9
8
|
import { formatCurrency, formatPercentRaw } from "../../../utils/format";
|
|
@@ -22,11 +21,7 @@ import {
|
|
|
22
21
|
DEFAULT_SORT_PREFERENCE,
|
|
23
22
|
INITIAL_REFRESH_BY_COLLECTION,
|
|
24
23
|
INITIAL_ROWS_BY_COLLECTION,
|
|
25
|
-
ONE_MONTH_DAYS,
|
|
26
|
-
ONE_YEAR_DAYS,
|
|
27
24
|
buildSectorColumns,
|
|
28
|
-
computeTrailingReturn,
|
|
29
|
-
latestHistoryClose,
|
|
30
25
|
nextSortPreference,
|
|
31
26
|
normalizeRowsForCollection,
|
|
32
27
|
sortRows,
|
|
@@ -37,6 +32,10 @@ import {
|
|
|
37
32
|
type SectorRowsByCollection,
|
|
38
33
|
type SectorSortPreference,
|
|
39
34
|
} from "./sector-model";
|
|
35
|
+
import { loadSectorRows } from "./client";
|
|
36
|
+
import { sectorsHeadless } from "./headless";
|
|
37
|
+
|
|
38
|
+
export { sectorsHeadless } from "./headless";
|
|
40
39
|
|
|
41
40
|
/** Stable identity: a fresh literal here would refetch the board every render. */
|
|
42
41
|
const NO_SAVED_ETFS: string[] = [];
|
|
@@ -105,52 +104,7 @@ function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
|
105
104
|
rows.map((row) => ({ ...row, loading: true }))
|
|
106
105
|
)));
|
|
107
106
|
|
|
108
|
-
|
|
109
|
-
const quotes = new Map<string, Quote | null>();
|
|
110
|
-
if (dataProvider.getQuotesBatch) {
|
|
111
|
-
// A failed batch leaves every quote missing, which the row outcome below
|
|
112
|
-
// reports as unavailable instead of stamping the pane as fresh.
|
|
113
|
-
const results = await dataProvider.getQuotesBatch(
|
|
114
|
-
sectorDefs.map((sector) => ({ symbol: sector.etf, exchange: "" })),
|
|
115
|
-
).catch(() => []);
|
|
116
|
-
for (const result of results) {
|
|
117
|
-
quotes.set(result.target.symbol, result.quote ?? null);
|
|
118
|
-
}
|
|
119
|
-
return quotes;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
await Promise.all(sectorDefs.map(async (sector) => {
|
|
123
|
-
try {
|
|
124
|
-
quotes.set(sector.etf, await dataProvider.getQuote(sector.etf, ""));
|
|
125
|
-
} catch {
|
|
126
|
-
quotes.set(sector.etf, null);
|
|
127
|
-
}
|
|
128
|
-
}));
|
|
129
|
-
return quotes;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const loadRows = async (): Promise<Array<{ etf: string; row: Partial<SectorRow> | null }>> => {
|
|
133
|
-
const quotesByEtf = await loadQuotes();
|
|
134
|
-
return Promise.all(sectorDefs.map(async (sector) => {
|
|
135
|
-
const history: PricePoint[] = await dataProvider.getPriceHistory(sector.etf, "", "1Y")
|
|
136
|
-
.catch(() => []);
|
|
137
|
-
const quote = quotesByEtf.get(sector.etf) ?? null;
|
|
138
|
-
if (!quote && history.length === 0) return { etf: sector.etf, row: null };
|
|
139
|
-
const price = quote?.price ?? latestHistoryClose(history);
|
|
140
|
-
return {
|
|
141
|
-
etf: sector.etf,
|
|
142
|
-
row: {
|
|
143
|
-
price,
|
|
144
|
-
changePercent: quote?.changePercent ?? null,
|
|
145
|
-
return1M: computeTrailingReturn(history, ONE_MONTH_DAYS, price),
|
|
146
|
-
return1Y: computeTrailingReturn(history, ONE_YEAR_DAYS, price),
|
|
147
|
-
currency: quote?.currency ?? "USD",
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
}));
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
loadRows().then((outcomes) => {
|
|
107
|
+
loadSectorRows(sectorDefs, dataProvider).then((outcomes) => {
|
|
154
108
|
if (fetchGenRef.current !== gen) return;
|
|
155
109
|
const loadedByEtf = new Map(outcomes.map((outcome) => [outcome.etf, outcome.row]));
|
|
156
110
|
setRowsByCollection((prev) => updateRowsForCollection(prev, collectionId, sectorDefs, (rows) => (
|
|
@@ -365,6 +319,7 @@ export const sectorsModule: PluginModule = {
|
|
|
365
319
|
description: "S&P 500 sector and industry performance sorted by daily change.",
|
|
366
320
|
keywords: ["sector", "sectors", "industry", "semis", "defense", "food", "leisure", "etf", "xlk", "xlv", "xlf", "performance", "spdr"],
|
|
367
321
|
shortcut: { prefix: "BI" },
|
|
322
|
+
headless: sectorsHeadless,
|
|
368
323
|
},
|
|
369
324
|
],
|
|
370
325
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo } from "react";
|
|
2
2
|
import { apiClient } from "../../../api-client";
|
|
3
|
+
import { recordResearchActivity, researchUpgradeUrl } from "../../../api-client/research-activity";
|
|
4
|
+
import { getCurrentPluginTarget } from "../../current-target";
|
|
3
5
|
import type { PaneFooterSegment } from "../../../components";
|
|
4
6
|
import { tf } from "../../../i18n";
|
|
5
7
|
import { useAppLanguage } from "../../../i18n/react";
|
|
@@ -25,10 +27,12 @@ let cloudUpgradeOpener: (() => void) | null = null;
|
|
|
25
27
|
* already hold. Both URLs are account-bound, so no session handoff is needed.
|
|
26
28
|
*/
|
|
27
29
|
export async function resolveCloudUpgradeUrl(): Promise<string> {
|
|
28
|
-
|
|
30
|
+
recordResearchActivity("upgrade_intent");
|
|
31
|
+
const returnTo = getCurrentPluginTarget() === "web" ? window.location.href : undefined;
|
|
32
|
+
if (!apiClient.isSignedIn() || !apiClient.getCurrentUser()?.emailVerified) return researchUpgradeUrl(returnTo);
|
|
29
33
|
const { url } = resolvePlanAccess(apiClient.getCurrentUser()).hasProAccess
|
|
30
34
|
? await apiClient.createBillingPortal()
|
|
31
|
-
: await apiClient.createCloudCheckout();
|
|
35
|
+
: await apiClient.createCloudCheckout(returnTo);
|
|
32
36
|
return url;
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -142,8 +146,8 @@ export function useCloudAccessFooter({
|
|
|
142
146
|
onPress: openUpgrade,
|
|
143
147
|
parts: [{
|
|
144
148
|
text: shortcutScope
|
|
145
|
-
? tf("{delay} delayed · u
|
|
146
|
-
: tf("{delay} delayed ·
|
|
149
|
+
? tf("{delay} delayed · u try Pro live", { delay: delayLabel })
|
|
150
|
+
: tf("{delay} delayed · try Pro live", { delay: delayLabel }),
|
|
147
151
|
tone: "warning",
|
|
148
152
|
}],
|
|
149
153
|
};
|
|
@@ -110,9 +110,12 @@ function normalizeCloudRecords(payload: CloudShortInterestPayload): ShortInteres
|
|
|
110
110
|
* Yahoo carries the current and prior settlement plus percent of float, so it
|
|
111
111
|
* stays as the fallback when the cloud route is unavailable.
|
|
112
112
|
*/
|
|
113
|
-
export async function fetchShortInterest(
|
|
113
|
+
export async function fetchShortInterest(
|
|
114
|
+
symbol: string,
|
|
115
|
+
cloudClient: Pick<typeof apiClient, "getCloudShortInterest"> = apiClient,
|
|
116
|
+
): Promise<ShortInterestRecord[]> {
|
|
114
117
|
try {
|
|
115
|
-
const response = await
|
|
118
|
+
const response = await cloudClient.getCloudShortInterest(symbol);
|
|
116
119
|
const records = response.status === "success" && response.data
|
|
117
120
|
? normalizeCloudRecords(response.data)
|
|
118
121
|
: [];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { CloudDataApi } from "../../../api-client/data";
|
|
3
|
+
import type { CloudMarketResponse, CloudShortInterestPayload } from "../../../api-client/types";
|
|
4
|
+
import { createTestDataProvider } from "../../../test-support/data-provider";
|
|
5
|
+
import { createDefaultConfig } from "../../../types/config";
|
|
6
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
7
|
+
import { fetchShortInterest } from "./client";
|
|
8
|
+
import { createShortInterestHeadless } from "./headless";
|
|
9
|
+
|
|
10
|
+
const records = [
|
|
11
|
+
{
|
|
12
|
+
settlementDate: new Date("2026-08-15T00:00:00.000Z"),
|
|
13
|
+
sharesShort: 12_000_000,
|
|
14
|
+
shortRatio: 2.5,
|
|
15
|
+
averageDailyVolume: 4_800_000,
|
|
16
|
+
shortPercentFloat: 3.2,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
settlementDate: new Date("2026-08-31T00:00:00.000Z"),
|
|
20
|
+
sharesShort: 15_000_000,
|
|
21
|
+
shortRatio: 3,
|
|
22
|
+
averageDailyVolume: 5_000_000,
|
|
23
|
+
shortPercentFloat: 4.1,
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function context(): HeadlessPaneContext {
|
|
28
|
+
return {
|
|
29
|
+
marketData: createTestDataProvider(),
|
|
30
|
+
apiClient: {} as HeadlessPaneContext["apiClient"],
|
|
31
|
+
config: createDefaultConfig("/tmp/gloomberb-headless-short-interest"),
|
|
32
|
+
signal: new AbortController().signal,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function args(overrides: Partial<HeadlessPaneLoadArgs["options"]> = {}): HeadlessPaneLoadArgs {
|
|
37
|
+
return {
|
|
38
|
+
rawArgument: "AMD",
|
|
39
|
+
argument: "AMD",
|
|
40
|
+
symbols: ["AMD"],
|
|
41
|
+
options: { order: "newest", limit: 24, ...overrides },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("short interest client", () => {
|
|
46
|
+
test("uses the served market route for FINRA history", async () => {
|
|
47
|
+
const paths: string[] = [];
|
|
48
|
+
const dataApi = new CloudDataApi(async <T>(path: string): Promise<T> => {
|
|
49
|
+
paths.push(path);
|
|
50
|
+
return {
|
|
51
|
+
status: "success",
|
|
52
|
+
data: {
|
|
53
|
+
symbol: "AAPL",
|
|
54
|
+
issueName: "Apple Inc.",
|
|
55
|
+
points: [{
|
|
56
|
+
settlementDate: "2026-08-31",
|
|
57
|
+
sharesShort: 15_000_000,
|
|
58
|
+
previousSharesShort: 12_000_000,
|
|
59
|
+
averageDailyVolume: 5_000_000,
|
|
60
|
+
daysToCover: 3,
|
|
61
|
+
changePercent: 25,
|
|
62
|
+
revised: false,
|
|
63
|
+
}],
|
|
64
|
+
},
|
|
65
|
+
} as CloudMarketResponse<CloudShortInterestPayload> as T;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const result = await fetchShortInterest("aapl", dataApi);
|
|
69
|
+
|
|
70
|
+
expect(paths).toEqual(["/market/short-interest?symbol=AAPL"]);
|
|
71
|
+
expect(result[0]).toMatchObject({ sharesShort: 15_000_000, shortRatio: 3 });
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("short interest headless model", () => {
|
|
76
|
+
test("projects raw settlement values and applies order and limit", async () => {
|
|
77
|
+
const headless = createShortInterestHeadless({
|
|
78
|
+
loadRecords: async () => records,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const newest = await headless.load(args({ limit: 1 }), context());
|
|
82
|
+
expect(newest.rows).toEqual([{
|
|
83
|
+
settlementDate: "2026-08-31",
|
|
84
|
+
sharesShort: 15_000_000,
|
|
85
|
+
daysToCover: 3,
|
|
86
|
+
averageDailyVolume: 5_000_000,
|
|
87
|
+
shortPercentFloat: 4.1,
|
|
88
|
+
}]);
|
|
89
|
+
|
|
90
|
+
const oldest = await headless.load(args({ order: "oldest", limit: 1 }), context());
|
|
91
|
+
expect(oldest.rows[0]).toMatchObject({
|
|
92
|
+
settlementDate: "2026-08-15",
|
|
93
|
+
sharesShort: 12_000_000,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|