gloomberb 0.13.1 → 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/package.json +1 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/request.ts +4 -1
- 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/index.ts +5 -1
- package/src/cli/pane-functions/screenshot.ts +2 -19
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +43 -0
- package/src/news/types.ts +1 -0
- 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 +3 -2
- package/src/plugins/builtin/cloud/askg/protocol.ts +16 -4
- 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/plugin.tsx +2 -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/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/transcript-view.tsx +8 -9
- 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/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/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +28 -18
- 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/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/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/sources/gloomberb-cloud/news.ts +1 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createTestDataProvider } from "../../../test-support/data-provider";
|
|
3
|
+
import { createDefaultConfig } from "../../../types/config";
|
|
4
|
+
import type { SecFilingItem } from "../../../types/data-provider";
|
|
5
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
6
|
+
import { createSecHeadless } from "./headless";
|
|
7
|
+
|
|
8
|
+
const filings: SecFilingItem[] = [
|
|
9
|
+
{
|
|
10
|
+
accessionNumber: "0000320193-26-000100",
|
|
11
|
+
form: "8-K",
|
|
12
|
+
filingDate: new Date("2026-08-25T00:00:00.000Z"),
|
|
13
|
+
primaryDocDescription: "8-K Results of Operations",
|
|
14
|
+
items: "2.02,9.01",
|
|
15
|
+
cik: "0000320193",
|
|
16
|
+
filingUrl: "https://www.sec.gov/filing/one",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
accessionNumber: "0000320193-26-000099",
|
|
20
|
+
form: "10-Q",
|
|
21
|
+
filingDate: new Date("2026-08-20T00:00:00.000Z"),
|
|
22
|
+
cik: "0000320193",
|
|
23
|
+
filingUrl: "https://www.sec.gov/filing/two",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function context(): HeadlessPaneContext {
|
|
28
|
+
return {
|
|
29
|
+
marketData: createTestDataProvider(),
|
|
30
|
+
apiClient: {} as HeadlessPaneContext["apiClient"],
|
|
31
|
+
config: createDefaultConfig("/tmp/gloomberb-headless-sec"),
|
|
32
|
+
signal: new AbortController().signal,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function args(limit: number): HeadlessPaneLoadArgs {
|
|
37
|
+
return {
|
|
38
|
+
rawArgument: "AAPL",
|
|
39
|
+
argument: "AAPL",
|
|
40
|
+
symbols: ["AAPL"],
|
|
41
|
+
options: { limit },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("SEC headless model", () => {
|
|
46
|
+
test("projects filing rows and honors the page limit", async () => {
|
|
47
|
+
const requestedLimits: number[] = [];
|
|
48
|
+
const headless = createSecHeadless({
|
|
49
|
+
loadFilings: async (_symbol, limit) => {
|
|
50
|
+
requestedLimits.push(limit);
|
|
51
|
+
return filings;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const first = await headless.load(args(1), context());
|
|
56
|
+
expect(first.rows).toEqual([{
|
|
57
|
+
filedAt: "2026-08-25T00:00:00.000Z",
|
|
58
|
+
acceptedAt: null,
|
|
59
|
+
form: "8-K",
|
|
60
|
+
filing: "8-K | Results of Operations | Current Report",
|
|
61
|
+
items: "2.02,9.01",
|
|
62
|
+
accessionNumber: "0000320193-26-000100",
|
|
63
|
+
primaryDocument: null,
|
|
64
|
+
cik: "0000320193",
|
|
65
|
+
url: "https://www.sec.gov/filing/one",
|
|
66
|
+
}]);
|
|
67
|
+
|
|
68
|
+
const both = await headless.load(args(2), context());
|
|
69
|
+
expect(both.rows).toHaveLength(2);
|
|
70
|
+
expect(requestedLimits).toEqual([1, 2]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HeadlessPaneColumn,
|
|
3
|
+
HeadlessPaneContext,
|
|
4
|
+
HeadlessPaneDefinition,
|
|
5
|
+
HeadlessPaneLoadArgs,
|
|
6
|
+
} from "../../../types/plugin";
|
|
7
|
+
import { formatCompact, formatNumber } from "../../../utils/format";
|
|
8
|
+
import { fetchShortInterest } from "./client";
|
|
9
|
+
import {
|
|
10
|
+
buildRows,
|
|
11
|
+
sortRows,
|
|
12
|
+
type SortPreference,
|
|
13
|
+
} from "./model";
|
|
14
|
+
import type { ShortInterestRecord } from "./types";
|
|
15
|
+
|
|
16
|
+
const SHORT_INTEREST_COLUMNS: HeadlessPaneColumn[] = [
|
|
17
|
+
{ key: "settlementDate", header: "Date" },
|
|
18
|
+
{
|
|
19
|
+
key: "sharesShort",
|
|
20
|
+
header: "Shares Short",
|
|
21
|
+
align: "right",
|
|
22
|
+
format: (value) => formatCompact(Number(value)),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: "daysToCover",
|
|
26
|
+
header: "Days to Cover",
|
|
27
|
+
align: "right",
|
|
28
|
+
format: (value) => value == null ? "-" : formatNumber(Number(value), 2),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: "averageDailyVolume",
|
|
32
|
+
header: "Avg Daily Vol",
|
|
33
|
+
align: "right",
|
|
34
|
+
format: (value) => value == null ? "-" : formatCompact(Number(value)),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "shortPercentFloat",
|
|
38
|
+
header: "% Float",
|
|
39
|
+
align: "right",
|
|
40
|
+
format: (value) => value == null ? "-" : `${formatNumber(Number(value), 2)}%`,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export interface ShortInterestHeadlessDependencies {
|
|
45
|
+
loadRecords(
|
|
46
|
+
symbol: string,
|
|
47
|
+
args: HeadlessPaneLoadArgs,
|
|
48
|
+
ctx: HeadlessPaneContext,
|
|
49
|
+
): Promise<ShortInterestRecord[]>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const defaultDependencies: ShortInterestHeadlessDependencies = {
|
|
53
|
+
loadRecords: (symbol, _args, ctx) => fetchShortInterest(symbol, ctx.apiClient),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function createShortInterestHeadless(
|
|
57
|
+
dependencies: ShortInterestHeadlessDependencies = defaultDependencies,
|
|
58
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
59
|
+
return {
|
|
60
|
+
shape: "rows",
|
|
61
|
+
argument: {
|
|
62
|
+
kind: "ticker",
|
|
63
|
+
placeholder: "ticker",
|
|
64
|
+
description: "US equity ticker.",
|
|
65
|
+
},
|
|
66
|
+
options: [
|
|
67
|
+
{
|
|
68
|
+
key: "order",
|
|
69
|
+
description: "Settlement date order.",
|
|
70
|
+
type: "enum",
|
|
71
|
+
values: [{ value: "newest" }, { value: "oldest" }],
|
|
72
|
+
defaultValue: "newest",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "limit",
|
|
76
|
+
aliases: ["count", "rows"],
|
|
77
|
+
description: "Maximum settlement rows.",
|
|
78
|
+
type: "integer",
|
|
79
|
+
defaultValue: 24,
|
|
80
|
+
minimum: 1,
|
|
81
|
+
maximum: 100,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
columns: SHORT_INTEREST_COLUMNS,
|
|
85
|
+
describe: (args) => `Short Interest | ${args.symbols[0]}`,
|
|
86
|
+
async load(args, ctx) {
|
|
87
|
+
const symbol = args.symbols[0]!;
|
|
88
|
+
const records = await dependencies.loadRecords(symbol, args, ctx);
|
|
89
|
+
const preference: SortPreference = {
|
|
90
|
+
columnId: "settlementDate",
|
|
91
|
+
direction: args.options.order === "oldest" ? "asc" : "desc",
|
|
92
|
+
};
|
|
93
|
+
const limit = Number(args.options.limit);
|
|
94
|
+
const rows = sortRows(buildRows(records), preference)
|
|
95
|
+
.slice(0, limit)
|
|
96
|
+
.map((row) => ({
|
|
97
|
+
settlementDate: row.settlementDate,
|
|
98
|
+
sharesShort: row.record.sharesShort,
|
|
99
|
+
daysToCover: row.record.shortRatio,
|
|
100
|
+
averageDailyVolume: row.record.averageDailyVolume,
|
|
101
|
+
shortPercentFloat: row.record.shortPercentFloat,
|
|
102
|
+
}));
|
|
103
|
+
return {
|
|
104
|
+
rows,
|
|
105
|
+
metadata: { symbol, order: args.options.order },
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const shortInterestHeadless = createShortInterestHeadless();
|
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
resetShortInterestHealth,
|
|
6
6
|
YAHOO_SHORT_INTEREST_CONNECTION_ID,
|
|
7
7
|
} from "./client";
|
|
8
|
+
import { shortInterestHeadless } from "./headless";
|
|
8
9
|
import { ShortInterestView } from "./pane";
|
|
9
10
|
|
|
11
|
+
export { shortInterestHeadless } from "./headless";
|
|
12
|
+
|
|
10
13
|
let disposeConnection: (() => void) | null = null;
|
|
11
14
|
|
|
12
15
|
export const shortInterestModule: PluginModule = {
|
|
@@ -50,14 +53,17 @@ export const shortInterestModule: PluginModule = {
|
|
|
50
53
|
],
|
|
51
54
|
|
|
52
55
|
paneTemplates: [
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
{
|
|
57
|
+
...createTickerSurfacePaneTemplate({
|
|
58
|
+
id: "short-interest-pane",
|
|
59
|
+
paneId: "short-interest",
|
|
60
|
+
label: "Short Interest",
|
|
61
|
+
// Yahoo's key-statistics module only carries the current and prior settlement dates.
|
|
62
|
+
description: "Bi-monthly short interest settlements from FINRA with days to cover and average daily volume.",
|
|
63
|
+
keywords: ["short", "interest", "si", "shorts", "borrow", "days", "cover"],
|
|
64
|
+
shortcut: "SI",
|
|
65
|
+
}),
|
|
66
|
+
headless: shortInterestHeadless,
|
|
67
|
+
},
|
|
62
68
|
],
|
|
63
69
|
};
|