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
|
@@ -45,35 +45,37 @@ export async function loadBrowserRows(
|
|
|
45
45
|
tab: ThirteenFBrowserTab,
|
|
46
46
|
query: string,
|
|
47
47
|
signal?: AbortSignal,
|
|
48
|
-
options: { forceRefresh?: boolean; offset?: number } = {},
|
|
48
|
+
options: { forceRefresh?: boolean; offset?: number; limit?: number } = {},
|
|
49
49
|
): Promise<BrowserLoadResult> {
|
|
50
50
|
const now = new Date();
|
|
51
51
|
const from = dateYearsAgo(2, now);
|
|
52
52
|
const to = todayIso(now);
|
|
53
53
|
const quarter = latestLikely13FQuarter(now);
|
|
54
54
|
const offset = Math.max(0, options.offset ?? 0);
|
|
55
|
+
const browserLimit = Math.max(1, options.limit ?? BROWSER_PAGE_LIMIT);
|
|
56
|
+
const latestLimit = Math.max(1, options.limit ?? LATEST_FILINGS_PAGE_LIMIT);
|
|
55
57
|
const apiOptions = { forceRefresh: options.forceRefresh };
|
|
56
58
|
const pageApiOptions = { forceRefresh: options.forceRefresh, offset };
|
|
57
59
|
|
|
58
60
|
if (tab === "performance") {
|
|
59
|
-
const topFunds = await listTopThirteenFFunds(quarter,
|
|
61
|
+
const topFunds = await listTopThirteenFFunds(quarter, browserLimit, signal, pageApiOptions);
|
|
60
62
|
const funds: ThirteenFFund[] = topFunds.map((fund) => ({ cik: fund.cik, name: fund.name }));
|
|
61
|
-
const forms = await loadLatestFormsForFunds(funds.slice(0, FORM_ENRICHMENT_LIMIT), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh });
|
|
63
|
+
const forms = await loadLatestFormsForFunds(funds.slice(0, Math.min(browserLimit, FORM_ENRICHMENT_LIMIT)), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh });
|
|
62
64
|
return {
|
|
63
65
|
rows: buildBrowserRows({ topFunds, forms, source: "performance" }),
|
|
64
66
|
quarter,
|
|
65
67
|
period: topFunds[0]?.periodOfReport,
|
|
66
|
-
hasMore: topFunds.length >=
|
|
68
|
+
hasMore: topFunds.length >= browserLimit,
|
|
67
69
|
nextOffset: offset + topFunds.length,
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
if (tab === "latest") {
|
|
72
|
-
const filings = await listThirteenFFilings(recentIso(21, now), to,
|
|
74
|
+
const filings = await listThirteenFFilings(recentIso(21, now), to, latestLimit, signal, pageApiOptions);
|
|
73
75
|
return {
|
|
74
76
|
rows: buildBrowserRows({ latestFilings: filings, source: "latest" }),
|
|
75
77
|
period: filings[0]?.periodOfReport,
|
|
76
|
-
hasMore: filings.length >=
|
|
78
|
+
hasMore: filings.length >= latestLimit,
|
|
77
79
|
nextOffset: offset + filings.length,
|
|
78
80
|
};
|
|
79
81
|
}
|
|
@@ -97,7 +99,7 @@ export async function loadBrowserRows(
|
|
|
97
99
|
try {
|
|
98
100
|
const holders = await lookupThirteenFHoldersByCusip(cusip, periodOfReport, signal, apiOptions);
|
|
99
101
|
const forms = new Map<string, ThirteenFFormSummary>();
|
|
100
|
-
const pageCiks = holders.ciks.slice(offset, offset +
|
|
102
|
+
const pageCiks = holders.ciks.slice(offset, offset + browserLimit);
|
|
101
103
|
const funds = await Promise.all(pageCiks.map(async (cik): Promise<ThirteenFFund> => {
|
|
102
104
|
const fundForms = await listThirteenFForms(cik, from, to, 1, signal, apiOptions);
|
|
103
105
|
if (fundForms[0]) forms.set(cik, fundForms[0]);
|
|
@@ -136,10 +138,10 @@ export async function loadBrowserRows(
|
|
|
136
138
|
nextOffset: offset + 1,
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
|
-
const funds = await searchThirteenFFunds(trimmed,
|
|
141
|
+
const funds = await searchThirteenFFunds(trimmed, browserLimit, signal, pageApiOptions);
|
|
140
142
|
const [forms, topFunds] = await Promise.all([
|
|
141
|
-
loadLatestFormsForFunds(funds.slice(0, FORM_ENRICHMENT_LIMIT), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh }),
|
|
142
|
-
listTopThirteenFFunds(quarter,
|
|
143
|
+
loadLatestFormsForFunds(funds.slice(0, Math.min(browserLimit, FORM_ENRICHMENT_LIMIT)), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh }),
|
|
144
|
+
listTopThirteenFFunds(quarter, browserLimit, signal, { forceRefresh: options.forceRefresh }).catch(() => []),
|
|
143
145
|
]);
|
|
144
146
|
const topByCik = new Map(topFunds.map((fund) => [fund.cik, fund]));
|
|
145
147
|
const matchedTopFunds = funds
|
|
@@ -153,7 +155,7 @@ export async function loadBrowserRows(
|
|
|
153
155
|
source: "funds",
|
|
154
156
|
}),
|
|
155
157
|
quarter,
|
|
156
|
-
hasMore: funds.length >=
|
|
158
|
+
hasMore: funds.length >= browserLimit,
|
|
157
159
|
nextOffset: offset + funds.length,
|
|
158
160
|
};
|
|
159
161
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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 { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
5
|
+
import { createThirteenFHeadless } from "./headless";
|
|
6
|
+
import type { FundDetailData } from "./types";
|
|
7
|
+
|
|
8
|
+
const detail: FundDetailData = {
|
|
9
|
+
cik: "0001067983",
|
|
10
|
+
name: "Berkshire Hathaway",
|
|
11
|
+
forms: [
|
|
12
|
+
{
|
|
13
|
+
url: "https://www.sec.gov/latest",
|
|
14
|
+
accessionNumber: "latest",
|
|
15
|
+
submissionType: "13F-HR",
|
|
16
|
+
periodOfReport: "2026-06-30",
|
|
17
|
+
filedAsOfDate: "2026-08-14",
|
|
18
|
+
cik: "0001067983",
|
|
19
|
+
companyName: "Berkshire Hathaway",
|
|
20
|
+
tableValueTotal: 300,
|
|
21
|
+
tableEntryTotal: 2,
|
|
22
|
+
isAmendment: false,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
url: "https://www.sec.gov/previous",
|
|
26
|
+
accessionNumber: "previous",
|
|
27
|
+
submissionType: "13F-HR",
|
|
28
|
+
periodOfReport: "2026-03-31",
|
|
29
|
+
filedAsOfDate: "2026-05-15",
|
|
30
|
+
cik: "0001067983",
|
|
31
|
+
companyName: "Berkshire Hathaway",
|
|
32
|
+
tableValueTotal: 200,
|
|
33
|
+
tableEntryTotal: 1,
|
|
34
|
+
isAmendment: false,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
latestForm: null,
|
|
38
|
+
previousForm: null,
|
|
39
|
+
latestHoldings: [],
|
|
40
|
+
previousHoldings: [],
|
|
41
|
+
};
|
|
42
|
+
detail.latestForm = detail.forms[0]!;
|
|
43
|
+
detail.previousForm = detail.forms[1]!;
|
|
44
|
+
detail.latestHoldings = [
|
|
45
|
+
{
|
|
46
|
+
accessionNumber: "latest",
|
|
47
|
+
cik: detail.cik,
|
|
48
|
+
issuer: "Apple Inc",
|
|
49
|
+
titleOfClass: "COM",
|
|
50
|
+
cusip: "037833100",
|
|
51
|
+
ticker: "AAPL",
|
|
52
|
+
value: 300,
|
|
53
|
+
shares: 3,
|
|
54
|
+
shareType: "SH",
|
|
55
|
+
investmentDiscretion: "DFND",
|
|
56
|
+
votingAuthoritySole: 3,
|
|
57
|
+
votingAuthorityShared: 0,
|
|
58
|
+
votingAuthorityNone: 0,
|
|
59
|
+
putCall: "",
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
detail.previousHoldings = [{ ...detail.latestHoldings[0]!, accessionNumber: "previous", value: 200, shares: 2 }];
|
|
63
|
+
|
|
64
|
+
function context(): HeadlessPaneContext {
|
|
65
|
+
return {
|
|
66
|
+
marketData: createTestDataProvider(),
|
|
67
|
+
apiClient: {} as HeadlessPaneContext["apiClient"],
|
|
68
|
+
config: createDefaultConfig("/tmp/gloomberb-headless-thirteenf"),
|
|
69
|
+
signal: new AbortController().signal,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function args(view: string, limit = 50): HeadlessPaneLoadArgs {
|
|
74
|
+
return {
|
|
75
|
+
rawArgument: "1067983",
|
|
76
|
+
argument: "1067983",
|
|
77
|
+
symbols: [],
|
|
78
|
+
options: { view, limit },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("13F headless model", () => {
|
|
83
|
+
test("projects browser rows and switches to fund holdings", async () => {
|
|
84
|
+
const headless = createThirteenFHeadless({
|
|
85
|
+
loadBrowser: async () => ({
|
|
86
|
+
rows: [
|
|
87
|
+
{ id: "one", cik: detail.cik, name: detail.name, estQuarterReturn: 8.2, source: "performance" },
|
|
88
|
+
{ id: "two", cik: "0000000002", name: "Second Fund", estQuarterReturn: 4.1, source: "performance" },
|
|
89
|
+
],
|
|
90
|
+
quarter: "2026Q2",
|
|
91
|
+
}),
|
|
92
|
+
loadDetail: async () => detail,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const browser = await headless.load(args("performance", 1), context());
|
|
96
|
+
expect(browser.rows).toEqual([expect.objectContaining({
|
|
97
|
+
cik: detail.cik,
|
|
98
|
+
name: detail.name,
|
|
99
|
+
estQuarterReturn: 8.2,
|
|
100
|
+
})]);
|
|
101
|
+
|
|
102
|
+
const holdings = await headless.load(args("holdings", 5), context());
|
|
103
|
+
expect(holdings.rows).toEqual([expect.objectContaining({
|
|
104
|
+
ticker: "AAPL",
|
|
105
|
+
value: 300,
|
|
106
|
+
sharesChange: 1,
|
|
107
|
+
action: "add",
|
|
108
|
+
actionLabel: "Add",
|
|
109
|
+
})]);
|
|
110
|
+
expect(holdings.metadata).toMatchObject({ view: "holdings", fund: detail.name });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HeadlessPaneColumn,
|
|
3
|
+
HeadlessPaneContext,
|
|
4
|
+
HeadlessPaneDefinition,
|
|
5
|
+
HeadlessPaneLoadArgs,
|
|
6
|
+
} from "../../../types/plugin";
|
|
7
|
+
import { normalizeCik } from "./api";
|
|
8
|
+
import {
|
|
9
|
+
loadBrowserRows,
|
|
10
|
+
loadFundDetail,
|
|
11
|
+
type BrowserLoadResult,
|
|
12
|
+
} from "./data";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_BROWSER_SORT,
|
|
15
|
+
DEFAULT_HOLDING_SORT,
|
|
16
|
+
DEFAULT_TIMELINE_SORT,
|
|
17
|
+
buildFundHoldingRows,
|
|
18
|
+
buildTimelineRows,
|
|
19
|
+
inferBrowserTabFromQuery,
|
|
20
|
+
isCikQuery,
|
|
21
|
+
positionType,
|
|
22
|
+
sortBrowserRows,
|
|
23
|
+
sortHoldingRows,
|
|
24
|
+
sortTimelineRows,
|
|
25
|
+
} from "./model";
|
|
26
|
+
import {
|
|
27
|
+
actionLabel,
|
|
28
|
+
formatChangeShares,
|
|
29
|
+
formatMoneyCompact,
|
|
30
|
+
formatPercentMaybe,
|
|
31
|
+
formatRawPercentMaybe,
|
|
32
|
+
formatShares,
|
|
33
|
+
formatShortDate,
|
|
34
|
+
} from "./format";
|
|
35
|
+
import type {
|
|
36
|
+
FundDetailData,
|
|
37
|
+
ThirteenFBrowserTab,
|
|
38
|
+
} from "./types";
|
|
39
|
+
|
|
40
|
+
const BROWSER_COLUMNS: HeadlessPaneColumn[] = [
|
|
41
|
+
{ key: "name", header: "Fund" },
|
|
42
|
+
{ key: "cik", header: "CIK" },
|
|
43
|
+
{ key: "periodOfReport", header: "Period" },
|
|
44
|
+
{
|
|
45
|
+
key: "estQuarterReturn",
|
|
46
|
+
header: "Est 13F",
|
|
47
|
+
align: "right",
|
|
48
|
+
format: (value) => formatRawPercentMaybe(value == null ? null : Number(value)),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: "tableValueTotal",
|
|
52
|
+
header: "Value",
|
|
53
|
+
align: "right",
|
|
54
|
+
format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
|
|
55
|
+
},
|
|
56
|
+
{ key: "tableEntryTotal", header: "Rows", align: "right" },
|
|
57
|
+
{
|
|
58
|
+
key: "filedAsOfDate",
|
|
59
|
+
header: "Filed",
|
|
60
|
+
format: (value) => formatShortDate(typeof value === "string" ? value : null),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const HOLDING_COLUMNS: HeadlessPaneColumn[] = [
|
|
65
|
+
{ key: "ticker", header: "Ticker" },
|
|
66
|
+
{ key: "type", header: "Type" },
|
|
67
|
+
{ key: "issuer", header: "Issuer" },
|
|
68
|
+
{
|
|
69
|
+
key: "value",
|
|
70
|
+
header: "Value",
|
|
71
|
+
align: "right",
|
|
72
|
+
format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: "estimatedPnl",
|
|
76
|
+
header: "Est P&L",
|
|
77
|
+
align: "right",
|
|
78
|
+
format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: "weight",
|
|
82
|
+
header: "Weight",
|
|
83
|
+
align: "right",
|
|
84
|
+
format: (value) => formatPercentMaybe(value == null ? null : Number(value)),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "shares",
|
|
88
|
+
header: "Shares",
|
|
89
|
+
align: "right",
|
|
90
|
+
format: (value) => formatShares(value == null ? null : Number(value)),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: "sharesChange",
|
|
94
|
+
header: "QoQ",
|
|
95
|
+
align: "right",
|
|
96
|
+
format: (value) => formatChangeShares(value == null ? null : Number(value)),
|
|
97
|
+
},
|
|
98
|
+
{ key: "actionLabel", header: "Action" },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const FILING_COLUMNS: HeadlessPaneColumn[] = [
|
|
102
|
+
{ key: "periodOfReport", header: "Period" },
|
|
103
|
+
{
|
|
104
|
+
key: "filedAsOfDate",
|
|
105
|
+
header: "Filed",
|
|
106
|
+
format: (value) => formatShortDate(typeof value === "string" ? value : null),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: "tableValueTotal",
|
|
110
|
+
header: "Value",
|
|
111
|
+
align: "right",
|
|
112
|
+
format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
|
|
113
|
+
},
|
|
114
|
+
{ key: "tableEntryTotal", header: "Rows", align: "right" },
|
|
115
|
+
{
|
|
116
|
+
key: "valueChangePercent",
|
|
117
|
+
header: "Value%",
|
|
118
|
+
align: "right",
|
|
119
|
+
format: (value) => formatPercentMaybe(value == null ? null : Number(value)),
|
|
120
|
+
},
|
|
121
|
+
{ key: "submissionType", header: "Form" },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
type HeadlessThirteenFView =
|
|
125
|
+
| "auto"
|
|
126
|
+
| "performance"
|
|
127
|
+
| "funds"
|
|
128
|
+
| "by-ticker"
|
|
129
|
+
| "latest"
|
|
130
|
+
| "holdings"
|
|
131
|
+
| "filings";
|
|
132
|
+
|
|
133
|
+
export interface ThirteenFHeadlessDependencies {
|
|
134
|
+
loadBrowser(
|
|
135
|
+
tab: ThirteenFBrowserTab,
|
|
136
|
+
query: string,
|
|
137
|
+
limit: number,
|
|
138
|
+
args: HeadlessPaneLoadArgs,
|
|
139
|
+
ctx: HeadlessPaneContext,
|
|
140
|
+
): Promise<BrowserLoadResult>;
|
|
141
|
+
loadDetail(
|
|
142
|
+
cik: string,
|
|
143
|
+
name: string,
|
|
144
|
+
args: HeadlessPaneLoadArgs,
|
|
145
|
+
ctx: HeadlessPaneContext,
|
|
146
|
+
): Promise<FundDetailData>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const defaultDependencies: ThirteenFHeadlessDependencies = {
|
|
150
|
+
loadBrowser: (tab, query, limit, _args, ctx) => loadBrowserRows(
|
|
151
|
+
tab,
|
|
152
|
+
query,
|
|
153
|
+
ctx.signal,
|
|
154
|
+
{ limit },
|
|
155
|
+
),
|
|
156
|
+
loadDetail: (cik, name, _args, ctx) => loadFundDetail(cik, name, ctx.signal),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
function browserTab(view: HeadlessThirteenFView, query: string): ThirteenFBrowserTab {
|
|
160
|
+
if (view === "performance" || view === "funds" || view === "latest") return view;
|
|
161
|
+
if (view === "by-ticker") return "byTicker";
|
|
162
|
+
return inferBrowserTabFromQuery(query);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function resolveFund(
|
|
166
|
+
query: string,
|
|
167
|
+
limit: number,
|
|
168
|
+
args: HeadlessPaneLoadArgs,
|
|
169
|
+
ctx: HeadlessPaneContext,
|
|
170
|
+
dependencies: ThirteenFHeadlessDependencies,
|
|
171
|
+
): Promise<{ cik: string; name: string }> {
|
|
172
|
+
if (!query) throw new Error("13F holdings and filings require a fund name or CIK.");
|
|
173
|
+
if (isCikQuery(query)) return { cik: normalizeCik(query), name: query };
|
|
174
|
+
const result = await dependencies.loadBrowser("funds", query, Math.min(limit, 25), args, ctx);
|
|
175
|
+
const exact = result.rows.find((row) => row.name.toLocaleLowerCase() === query.toLocaleLowerCase());
|
|
176
|
+
const fund = exact ?? result.rows[0];
|
|
177
|
+
if (!fund) throw new Error(`No 13F fund found for "${query}".`);
|
|
178
|
+
return { cik: fund.cik, name: fund.name };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createThirteenFHeadless(
|
|
182
|
+
dependencies: ThirteenFHeadlessDependencies = defaultDependencies,
|
|
183
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
184
|
+
return {
|
|
185
|
+
shape: "rows",
|
|
186
|
+
argument: {
|
|
187
|
+
kind: "free-text",
|
|
188
|
+
placeholder: "fund, ticker, or CIK",
|
|
189
|
+
description: "Optional fund name, ticker, CIK, or latest keyword.",
|
|
190
|
+
optional: true,
|
|
191
|
+
},
|
|
192
|
+
options: [
|
|
193
|
+
{
|
|
194
|
+
key: "view",
|
|
195
|
+
description: "Fund browser mode or detail tab.",
|
|
196
|
+
type: "enum",
|
|
197
|
+
values: [
|
|
198
|
+
{ value: "auto" },
|
|
199
|
+
{ value: "performance" },
|
|
200
|
+
{ value: "funds" },
|
|
201
|
+
{ value: "by-ticker", aliases: ["ticker", "byTicker"] },
|
|
202
|
+
{ value: "latest" },
|
|
203
|
+
{ value: "holdings" },
|
|
204
|
+
{ value: "filings" },
|
|
205
|
+
],
|
|
206
|
+
defaultValue: "auto",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
key: "limit",
|
|
210
|
+
aliases: ["count", "rows"],
|
|
211
|
+
description: "Maximum rows.",
|
|
212
|
+
type: "integer",
|
|
213
|
+
defaultValue: 50,
|
|
214
|
+
minimum: 1,
|
|
215
|
+
maximum: 200,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
describe: (args) => `13F Funds | ${String(args.options.view)}`,
|
|
219
|
+
async load(args, ctx) {
|
|
220
|
+
const query = typeof args.argument === "string" ? args.argument.trim() : "";
|
|
221
|
+
const requestedView = String(args.options.view) as HeadlessThirteenFView;
|
|
222
|
+
const view = requestedView === "auto" && isCikQuery(query) ? "holdings" : requestedView;
|
|
223
|
+
const limit = Number(args.options.limit);
|
|
224
|
+
|
|
225
|
+
if (view === "holdings" || view === "filings") {
|
|
226
|
+
const fund = await resolveFund(query, limit, args, ctx, dependencies);
|
|
227
|
+
const detail = await dependencies.loadDetail(fund.cik, fund.name, args, ctx);
|
|
228
|
+
if (view === "filings") {
|
|
229
|
+
const rows = sortTimelineRows(buildTimelineRows(detail.forms), DEFAULT_TIMELINE_SORT)
|
|
230
|
+
.slice(0, limit)
|
|
231
|
+
.map((row) => ({ ...row }));
|
|
232
|
+
return {
|
|
233
|
+
columns: FILING_COLUMNS,
|
|
234
|
+
rows,
|
|
235
|
+
metadata: {
|
|
236
|
+
view,
|
|
237
|
+
cik: detail.cik,
|
|
238
|
+
fund: detail.name,
|
|
239
|
+
latestPeriod: detail.latestForm?.periodOfReport ?? null,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const rows = sortHoldingRows(
|
|
244
|
+
buildFundHoldingRows(detail).filter((row) => row.value != null),
|
|
245
|
+
DEFAULT_HOLDING_SORT,
|
|
246
|
+
)
|
|
247
|
+
.slice(0, limit)
|
|
248
|
+
.map((row) => ({
|
|
249
|
+
...row,
|
|
250
|
+
type: positionType(row),
|
|
251
|
+
actionLabel: actionLabel(row.action),
|
|
252
|
+
}));
|
|
253
|
+
return {
|
|
254
|
+
columns: HOLDING_COLUMNS,
|
|
255
|
+
rows,
|
|
256
|
+
metadata: {
|
|
257
|
+
view,
|
|
258
|
+
cik: detail.cik,
|
|
259
|
+
fund: detail.name,
|
|
260
|
+
latestPeriod: detail.latestForm?.periodOfReport ?? null,
|
|
261
|
+
previousPeriod: detail.previousForm?.periodOfReport ?? null,
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const tab = browserTab(view, query);
|
|
267
|
+
const result = await dependencies.loadBrowser(tab, query, limit, args, ctx);
|
|
268
|
+
return {
|
|
269
|
+
columns: BROWSER_COLUMNS,
|
|
270
|
+
rows: sortBrowserRows(result.rows, DEFAULT_BROWSER_SORT)
|
|
271
|
+
.slice(0, limit)
|
|
272
|
+
.map((row) => ({ ...row })),
|
|
273
|
+
...(result.warning ? { errors: [result.warning] } : {}),
|
|
274
|
+
metadata: {
|
|
275
|
+
view: tab,
|
|
276
|
+
query: query || null,
|
|
277
|
+
period: result.period ?? null,
|
|
278
|
+
quarter: result.quarter ?? null,
|
|
279
|
+
hasMore: result.hasMore ?? false,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const thirteenFHeadless = createThirteenFHeadless();
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
attachThirteenFApiPersistence,
|
|
12
12
|
resetThirteenFApiPersistence,
|
|
13
13
|
} from "./api";
|
|
14
|
+
import { thirteenFHeadless } from "./headless";
|
|
15
|
+
|
|
16
|
+
export { thirteenFHeadless } from "./headless";
|
|
14
17
|
|
|
15
18
|
function queryFromOptions(options?: PaneTemplateCreateOptions): string {
|
|
16
19
|
return (options?.arg ?? options?.values?.query ?? "").trim();
|
|
@@ -55,6 +58,7 @@ export const thirteenFModule: PluginModule = {
|
|
|
55
58
|
argKind: "text",
|
|
56
59
|
argOptional: true,
|
|
57
60
|
},
|
|
61
|
+
headless: thirteenFHeadless,
|
|
58
62
|
createInstance(_context: PaneTemplateContext, options?: PaneTemplateCreateOptions) {
|
|
59
63
|
const query = queryFromOptions(options);
|
|
60
64
|
const browserMode = inferBrowserTabFromQuery(query);
|
|
@@ -615,6 +615,19 @@ export function selectedIndexById<T extends { id: string }>(rows: T[], selectedI
|
|
|
615
615
|
return index >= 0 ? index : rows.length > 0 ? 0 : -1;
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
+
export function positionType(row: {
|
|
619
|
+
putCall: string;
|
|
620
|
+
titleOfClass: string;
|
|
621
|
+
shareType: string;
|
|
622
|
+
}): string {
|
|
623
|
+
const putCall = row.putCall.trim().toUpperCase();
|
|
624
|
+
if (putCall === "PUT" || putCall === "CALL") return putCall;
|
|
625
|
+
const title = row.titleOfClass.trim().toUpperCase();
|
|
626
|
+
if (title) return title;
|
|
627
|
+
const shareType = row.shareType.trim().toUpperCase();
|
|
628
|
+
return shareType || "--";
|
|
629
|
+
}
|
|
630
|
+
|
|
618
631
|
export function isCikQuery(query: string): boolean {
|
|
619
632
|
return CIK_RE.test(query.trim());
|
|
620
633
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
formatShares,
|
|
13
13
|
formatShortDate,
|
|
14
14
|
} from "./format";
|
|
15
|
+
import { positionType } from "./model";
|
|
15
16
|
import type {
|
|
16
17
|
FilingPositionColumn,
|
|
17
18
|
FilingPositionRow,
|
|
@@ -82,7 +83,7 @@ export function renderHoldingCell(
|
|
|
82
83
|
attributes: TextAttributes.BOLD,
|
|
83
84
|
};
|
|
84
85
|
case "type":
|
|
85
|
-
return { text:
|
|
86
|
+
return { text: positionType(row), color: selectedColor ?? colors.textDim };
|
|
86
87
|
case "issuer":
|
|
87
88
|
return { text: row.issuer, color: selectedColor ?? colors.text };
|
|
88
89
|
case "value":
|
|
@@ -136,7 +137,7 @@ export function renderFilingPositionCell(
|
|
|
136
137
|
attributes: TextAttributes.BOLD,
|
|
137
138
|
};
|
|
138
139
|
case "type":
|
|
139
|
-
return { text:
|
|
140
|
+
return { text: positionType(row), color: selectedColor ?? colors.textDim };
|
|
140
141
|
case "issuer":
|
|
141
142
|
return { text: row.issuer, color: selectedColor ?? colors.text };
|
|
142
143
|
case "value":
|
|
@@ -185,19 +186,6 @@ export function renderTimelineCell(
|
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
function formatPositionType(row: {
|
|
189
|
-
putCall: string;
|
|
190
|
-
titleOfClass: string;
|
|
191
|
-
shareType: string;
|
|
192
|
-
}): string {
|
|
193
|
-
const putCall = row.putCall.trim().toUpperCase();
|
|
194
|
-
if (putCall === "PUT" || putCall === "CALL") return putCall;
|
|
195
|
-
const title = row.titleOfClass.trim().toUpperCase();
|
|
196
|
-
if (title) return title;
|
|
197
|
-
const shareType = row.shareType.trim().toUpperCase();
|
|
198
|
-
return shareType || "--";
|
|
199
|
-
}
|
|
200
|
-
|
|
201
189
|
function actionColor(action: FundHoldingRow["action"]): string {
|
|
202
190
|
switch (action) {
|
|
203
191
|
case "new":
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../types/plugin";
|
|
3
|
+
import {
|
|
4
|
+
createPredictionMarketsHeadless,
|
|
5
|
+
type PredictionMarketsHeadlessDependencies,
|
|
6
|
+
} from "./headless";
|
|
7
|
+
import type { PredictionMarketSummary, PredictionVenue } from "./types";
|
|
8
|
+
|
|
9
|
+
function market(
|
|
10
|
+
key: string,
|
|
11
|
+
venue: PredictionVenue,
|
|
12
|
+
title: string,
|
|
13
|
+
volume24h: number,
|
|
14
|
+
endsAt: string,
|
|
15
|
+
): PredictionMarketSummary {
|
|
16
|
+
return {
|
|
17
|
+
key,
|
|
18
|
+
venue,
|
|
19
|
+
marketId: `${key}-id`,
|
|
20
|
+
title,
|
|
21
|
+
marketLabel: "Yes",
|
|
22
|
+
eventLabel: title,
|
|
23
|
+
category: title.includes("Fed") ? "Economics" : "Politics",
|
|
24
|
+
tags: [],
|
|
25
|
+
status: "open",
|
|
26
|
+
url: `https://example.com/${key}`,
|
|
27
|
+
description: `${title} market`,
|
|
28
|
+
endsAt,
|
|
29
|
+
updatedAt: "2026-09-04T20:00:00.000Z",
|
|
30
|
+
yesPrice: 0.62,
|
|
31
|
+
noPrice: 0.38,
|
|
32
|
+
yesBid: 0.61,
|
|
33
|
+
yesAsk: 0.63,
|
|
34
|
+
noBid: 0.37,
|
|
35
|
+
noAsk: 0.39,
|
|
36
|
+
spread: 0.02,
|
|
37
|
+
lastTradePrice: 0.62,
|
|
38
|
+
volume24h,
|
|
39
|
+
volume24hUnit: "usd",
|
|
40
|
+
totalVolume: volume24h * 10,
|
|
41
|
+
totalVolumeUnit: "usd",
|
|
42
|
+
openInterest: volume24h / 2,
|
|
43
|
+
openInterestUnit: venue === "kalshi" ? "contracts" : "usd",
|
|
44
|
+
liquidity: volume24h / 4,
|
|
45
|
+
liquidityUnit: "usd",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const markets = [
|
|
50
|
+
market("poly-fed", "polymarket", "Will the Fed cut rates?", 2_000_000, "2026-10-01T00:00:00.000Z"),
|
|
51
|
+
market("kalshi-election", "kalshi", "Will the incumbent win?", 500_000, "2026-11-01T00:00:00.000Z"),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function args(
|
|
55
|
+
argument: string | null,
|
|
56
|
+
options: Record<string, string | number | boolean>,
|
|
57
|
+
): HeadlessPaneLoadArgs {
|
|
58
|
+
return {
|
|
59
|
+
rawArgument: argument ?? "",
|
|
60
|
+
argument,
|
|
61
|
+
symbols: [],
|
|
62
|
+
options,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const context = {} as HeadlessPaneContext;
|
|
67
|
+
|
|
68
|
+
function dependencies(seen: string[]): PredictionMarketsHeadlessDependencies {
|
|
69
|
+
return {
|
|
70
|
+
loadPolymarket: async (query) => {
|
|
71
|
+
seen.push(`polymarket:${query}`);
|
|
72
|
+
return markets.filter((entry) => entry.venue === "polymarket");
|
|
73
|
+
},
|
|
74
|
+
loadKalshi: async (query) => {
|
|
75
|
+
seen.push(`kalshi:${query}`);
|
|
76
|
+
return markets.filter((entry) => entry.venue === "kalshi");
|
|
77
|
+
},
|
|
78
|
+
now: () => new Date("2026-09-04T21:00:00.000Z"),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("prediction markets headless", () => {
|
|
83
|
+
test("projects venue data through the pane's shared row ordering", async () => {
|
|
84
|
+
const definition = createPredictionMarketsHeadless(dependencies([]));
|
|
85
|
+
|
|
86
|
+
const result = await definition.load(args(null, {
|
|
87
|
+
venue: "all",
|
|
88
|
+
category: "all",
|
|
89
|
+
tab: "top",
|
|
90
|
+
limit: 1,
|
|
91
|
+
}), context);
|
|
92
|
+
|
|
93
|
+
expect(result.rows).toEqual([
|
|
94
|
+
expect.objectContaining({
|
|
95
|
+
key: "poly-fed",
|
|
96
|
+
venue: "polymarket",
|
|
97
|
+
yesProbability: 0.62,
|
|
98
|
+
volume24h: 2_000_000,
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
expect(result.metadata).toMatchObject({ total: 2, returned: 1, truncated: true });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("uses the shortcut venue prefix without querying the other venue", async () => {
|
|
105
|
+
const seen: string[] = [];
|
|
106
|
+
const definition = createPredictionMarketsHeadless(dependencies(seen));
|
|
107
|
+
|
|
108
|
+
const result = await definition.load(args("polymarket:fed", {
|
|
109
|
+
venue: "all",
|
|
110
|
+
category: "macro",
|
|
111
|
+
tab: "ending",
|
|
112
|
+
limit: 20,
|
|
113
|
+
}), context);
|
|
114
|
+
|
|
115
|
+
expect(seen).toEqual(["polymarket:fed"]);
|
|
116
|
+
expect(result.rows.map((row) => row.key)).toEqual(["poly-fed"]);
|
|
117
|
+
expect(result.metadata).toMatchObject({ venue: "polymarket", query: "fed" });
|
|
118
|
+
});
|
|
119
|
+
});
|