gloomberb 0.8.1 → 0.8.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 +102 -52
- package/package.json +1 -1
- package/src/api-client/chat.ts +8 -0
- package/src/api-client/index.test.ts +30 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/normalizers.ts +1 -0
- package/src/api-client/types.ts +1 -0
- package/src/capabilities/factories.ts +4 -0
- package/src/components/chart/comparison/data.test.ts +54 -0
- package/src/components/chart/comparison/data.ts +47 -24
- package/src/components/chart/core/projection.ts +51 -27
- package/src/components/data-table/stack-view.test.tsx +14 -11
- package/src/components/data-table/stack-view.tsx +1 -2
- package/src/components/data-table/view.test.tsx +24 -18
- package/src/components/data-table/view.tsx +313 -83
- package/src/components/feed-data-table/stack-view.tsx +6 -8
- package/src/components/layout/pane/footer/index.test.tsx +6 -0
- package/src/components/layout/pane/footer/index.tsx +14 -2
- package/src/components/layout/pane/title.ts +5 -1
- package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
- package/src/components/pane-settings-dialog/tui.tsx +1 -1
- package/src/components/ticker/list-table-view.tsx +11 -25
- package/src/components/ui/choice-dialog.tsx +12 -2
- package/src/components/ui/multi-select/dialog.tsx +17 -5
- package/src/components/ui/shortcut-hint.tsx +14 -2
- package/src/components/ui/table-layout.test.ts +2 -2
- package/src/components/ui/table-layout.ts +8 -2
- package/src/components/ui/tabs.tsx +20 -6
- package/src/components/ui/ui.test.tsx +30 -5
- package/src/market-data/coordinator/auxiliary.ts +29 -1
- package/src/market-data/coordinator/index.ts +17 -1
- package/src/market-data/hooks.tsx +17 -1
- package/src/market-data/selectors.ts +4 -0
- package/src/plugins/builtin/account-management/form-components.tsx +0 -4
- package/src/plugins/builtin/account-management/model.test.ts +54 -0
- package/src/plugins/builtin/account-management/model.ts +13 -3
- package/src/plugins/builtin/account-management/pane.tsx +5 -11
- package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +5 -3
- package/src/plugins/builtin/analytics/index.tsx +0 -3
- package/src/plugins/builtin/analytics/view.tsx +6 -5
- package/src/plugins/builtin/broker-manager/index.tsx +6 -6
- package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
- package/src/plugins/builtin/changelog/index.tsx +40 -27
- package/src/plugins/builtin/changelog/model.test.ts +77 -0
- package/src/plugins/builtin/changelog/model.ts +98 -0
- package/src/plugins/builtin/chat/channel-labels.ts +53 -0
- package/src/plugins/builtin/chat/channels.test.ts +28 -0
- package/src/plugins/builtin/chat/channels.ts +36 -19
- package/src/plugins/builtin/chat/chat.test.tsx +264 -5
- package/src/plugins/builtin/chat/content/channel-navigation.ts +6 -0
- package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
- package/src/plugins/builtin/chat/content/composer.tsx +23 -2
- package/src/plugins/builtin/chat/content/index.tsx +61 -0
- package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +20 -1
- package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
- package/src/plugins/builtin/chat/content/types.ts +1 -0
- package/src/plugins/builtin/chat/controller/index.ts +41 -0
- package/src/plugins/builtin/chat/controller/messages.ts +18 -0
- package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
- package/src/plugins/builtin/chat/controller/utils.ts +6 -3
- package/src/plugins/builtin/chat/controller/view.ts +1 -1
- package/src/plugins/builtin/chat/controller.test.ts +189 -0
- package/src/plugins/builtin/chat/edit-window.ts +24 -0
- package/src/plugins/builtin/chat/layout.ts +94 -31
- package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
- package/src/plugins/builtin/chat/message/render-state.ts +5 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
- package/src/plugins/builtin/chat/message/types.ts +2 -0
- package/src/plugins/builtin/chat/pane.tsx +35 -7
- package/src/plugins/builtin/chat/sidebar.test.tsx +1 -0
- package/src/plugins/builtin/chat/status-widget.tsx +29 -11
- package/src/plugins/builtin/chat/test-harness.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
- package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
- package/src/plugins/builtin/earnings/index.tsx +8 -7
- package/src/plugins/builtin/econ/index.tsx +6 -8
- package/src/plugins/builtin/help/index.tsx +6 -0
- package/src/plugins/builtin/holders/pane.tsx +7 -6
- package/src/plugins/builtin/market-movers/index.tsx +6 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
- package/src/plugins/builtin/options/view.tsx +16 -8
- package/src/plugins/builtin/research/analyst-pane.tsx +1 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
- package/src/plugins/builtin/research/index.test.ts +30 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
- package/src/plugins/builtin/sec/filing-documents.ts +66 -0
- package/src/plugins/builtin/sec/index.tsx +132 -20
- package/src/plugins/builtin/sectors/index.tsx +13 -23
- package/src/plugins/builtin/shared/ticker-request.ts +1 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +26 -25
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
- package/src/plugins/builtin/world-indices/index.tsx +9 -6
- package/src/plugins/prediction-markets/pane.tsx +10 -9
- package/src/plugins/registry/index.ts +3 -0
- package/src/plugins/runtime/context.tsx +1 -0
- package/src/plugins/runtime/index.tsx +4 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
- package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
- package/src/renderers/electrobun/view/host/input.tsx +23 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -0
- package/src/sources/provider-router/cache.ts +1 -0
- package/src/sources/provider-router/document-routes.ts +45 -0
- package/src/sources/provider-router/index.ts +4 -0
- package/src/sources/sec-edgar/content.ts +9 -3
- package/src/sources/sec-edgar.test.ts +108 -1
- package/src/sources/sec-edgar.ts +105 -1
- package/src/sources/yahoo-finance.ts +4 -0
- package/src/test-support/plugin-runtime.ts +1 -0
- package/src/types/data-provider.ts +11 -0
- package/src/ui/host.tsx +1 -0
- package/src/version.ts +1 -1
|
@@ -328,12 +328,12 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
328
328
|
rootWidth={width}
|
|
329
329
|
rootHeight={height}
|
|
330
330
|
onRootKeyDown={handleRootKeyDown}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
331
|
+
selection={{
|
|
332
|
+
kind: "index",
|
|
333
|
+
selectedIndex: eventIdxToRowIdx.get(selectedIdx) ?? selectedIdx,
|
|
334
|
+
onChange: (_index, row) => {
|
|
335
|
+
if (row.kind === "event") setSelectedIdx(row.eventIdx);
|
|
336
|
+
},
|
|
337
337
|
}}
|
|
338
338
|
columns={columns}
|
|
339
339
|
items={rows}
|
|
@@ -344,8 +344,6 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
344
344
|
headerScrollRef={headerScrollRef}
|
|
345
345
|
scrollRef={scrollRef}
|
|
346
346
|
getItemKey={(row) => row.key}
|
|
347
|
-
isSelected={(row) => row.kind === "event" && row.eventIdx === selectedIdx}
|
|
348
|
-
onSelect={selectDisplayRow}
|
|
349
347
|
onActivate={openDisplayRow}
|
|
350
348
|
renderSectionHeader={renderSectionHeader}
|
|
351
349
|
renderCell={renderCell}
|
|
@@ -200,6 +200,12 @@ export function HelpPane({ width, height }: PaneProps) {
|
|
|
200
200
|
description="Pop the focused pane out to a desktop window."
|
|
201
201
|
/>
|
|
202
202
|
)}
|
|
203
|
+
{isDesktopWeb && (
|
|
204
|
+
<ShortcutRow
|
|
205
|
+
badges={[platformShortcut(["Shift", "C"])]}
|
|
206
|
+
description="Copy a screenshot of the focused pane."
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
203
209
|
<ShortcutRow
|
|
204
210
|
badges={[platformShortcut(["Shift", "L"])]}
|
|
205
211
|
description="Open layout actions."
|
|
@@ -296,9 +296,13 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
|
|
|
296
296
|
{viewMode === "table" ? (
|
|
297
297
|
<DataTableView<HolderRow, HolderColumn>
|
|
298
298
|
focused={focused}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
299
|
+
selection={{
|
|
300
|
+
kind: "id",
|
|
301
|
+
selectedId,
|
|
302
|
+
getId: (row) => row.id,
|
|
303
|
+
onChange: (id) => setSelectedId(id),
|
|
304
|
+
}}
|
|
305
|
+
onActivate={openFundDetail}
|
|
302
306
|
onRootKeyDown={handleKeyDown}
|
|
303
307
|
resetScrollKey={data?.symbol}
|
|
304
308
|
rootWidth={width}
|
|
@@ -308,9 +312,6 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
|
|
|
308
312
|
sortDirection={sortPreference.direction}
|
|
309
313
|
onHeaderClick={handleHeaderClick}
|
|
310
314
|
getItemKey={(row) => row.id}
|
|
311
|
-
isSelected={(row) => row.id === selectedId}
|
|
312
|
-
onSelect={(row) => setSelectedId(row.id)}
|
|
313
|
-
onActivate={(row) => openFundDetail(row)}
|
|
314
315
|
renderCell={renderCell}
|
|
315
316
|
emptyStateTitle={emptyTitle}
|
|
316
317
|
/>
|
|
@@ -227,7 +227,12 @@ function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
|
227
227
|
|
|
228
228
|
<DataTableView<MarketMoverRow, MarketMoverColumn>
|
|
229
229
|
focused={focused}
|
|
230
|
-
|
|
230
|
+
selection={{
|
|
231
|
+
kind: "id",
|
|
232
|
+
selectedId: selectedSymbol,
|
|
233
|
+
getId: (row) => row.symbol,
|
|
234
|
+
onChange: (symbol) => setSelectedSymbol(symbol),
|
|
235
|
+
}}
|
|
231
236
|
onRootKeyDown={handleTableKeyDown}
|
|
232
237
|
resetScrollKey={activeTab}
|
|
233
238
|
columns={columns}
|
|
@@ -236,8 +241,6 @@ function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
|
236
241
|
sortDirection={sortPreference.direction}
|
|
237
242
|
onHeaderClick={handleHeaderClick}
|
|
238
243
|
getItemKey={(row) => `${row.symbol}-${row.rank}`}
|
|
239
|
-
isSelected={(row) => row.symbol === selectedSymbol}
|
|
240
|
-
onSelect={(row) => setSelectedSymbol(row.symbol)}
|
|
241
244
|
onActivate={(row) => openSymbol(row.symbol)}
|
|
242
245
|
renderCell={renderMarketMoverCell}
|
|
243
246
|
emptyStateTitle={loading ? "Loading movers..." : loadError ?? "No data"}
|
|
@@ -245,9 +245,13 @@ export function NewsArticleStackView({
|
|
|
245
245
|
onBack={onBack}
|
|
246
246
|
detailContent={detailContent}
|
|
247
247
|
detailTitle={detailTitle}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
selection={{
|
|
249
|
+
kind: "id",
|
|
250
|
+
selectedId: selectedArticleId,
|
|
251
|
+
getId: (article) => article.id,
|
|
252
|
+
onChange: (id) => setSelectedArticleId(id),
|
|
253
|
+
}}
|
|
254
|
+
onActivate={openArticle}
|
|
251
255
|
rootBefore={rootBefore}
|
|
252
256
|
rootHeight={rootHeight}
|
|
253
257
|
onRootKeyDown={onRootKeyDown}
|
|
@@ -257,9 +261,6 @@ export function NewsArticleStackView({
|
|
|
257
261
|
sortDirection={sortPreference.direction}
|
|
258
262
|
onHeaderClick={(columnId) => setSortPreference(nextSortPreference(sortPreference, columnId as NewsColumnId))}
|
|
259
263
|
getItemKey={(item) => item.id}
|
|
260
|
-
isSelected={(item, index) => item.id === selectedArticleId || (selectedArticleId === null && index === 0)}
|
|
261
|
-
onSelect={(article) => setSelectedArticleId(article.id)}
|
|
262
|
-
onActivate={openArticle}
|
|
263
264
|
renderCell={renderCell}
|
|
264
265
|
emptyContent={emptyContent}
|
|
265
266
|
emptyStateTitle={emptyStateTitle}
|
|
@@ -238,7 +238,22 @@ export function OptionsView({ width, focused, onCapture = () => {} }: OptionsVie
|
|
|
238
238
|
|
|
239
239
|
<DataTableView<OptionTableRow, OptionColumn>
|
|
240
240
|
focused={focused}
|
|
241
|
-
|
|
241
|
+
selection={{
|
|
242
|
+
kind: "index",
|
|
243
|
+
selectedIndex: strikeIdx,
|
|
244
|
+
onChange: (index) => {
|
|
245
|
+
userSelectedStrikeRef.current = true;
|
|
246
|
+
setScrollToIndexAlign("nearest");
|
|
247
|
+
enterInteractive();
|
|
248
|
+
setStrikeIdx(index);
|
|
249
|
+
},
|
|
250
|
+
}}
|
|
251
|
+
onCursorChange={(_row, index) => {
|
|
252
|
+
userSelectedStrikeRef.current = true;
|
|
253
|
+
setScrollToIndexAlign("nearest");
|
|
254
|
+
enterInteractive();
|
|
255
|
+
setStrikeIdx(index);
|
|
256
|
+
}}
|
|
242
257
|
onRootKeyDown={handleTableKeyDown}
|
|
243
258
|
columns={optionColumns}
|
|
244
259
|
items={rows}
|
|
@@ -246,13 +261,6 @@ export function OptionsView({ width, focused, onCapture = () => {} }: OptionsVie
|
|
|
246
261
|
sortDirection="asc"
|
|
247
262
|
onHeaderClick={() => {}}
|
|
248
263
|
getItemKey={(row) => String(row.strike)}
|
|
249
|
-
isSelected={(_row, index) => index === strikeIdx}
|
|
250
|
-
onSelect={(_row, index) => {
|
|
251
|
-
userSelectedStrikeRef.current = true;
|
|
252
|
-
setScrollToIndexAlign("nearest");
|
|
253
|
-
enterInteractive();
|
|
254
|
-
setStrikeIdx(index);
|
|
255
|
-
}}
|
|
256
264
|
renderCell={renderOptionCell}
|
|
257
265
|
emptyStateTitle="No strikes available."
|
|
258
266
|
scrollToIndex={strikeIdx}
|
|
@@ -364,6 +364,7 @@ export function AnalystResearchView({ focused, width, height }: { focused: boole
|
|
|
364
364
|
return (
|
|
365
365
|
<DataTableView<AnalystResearchData["ratings"][number], RatingColumn>
|
|
366
366
|
focused={focused}
|
|
367
|
+
selection={{ kind: "none" }}
|
|
367
368
|
rootWidth={width}
|
|
368
369
|
rootHeight={height}
|
|
369
370
|
rootBefore={<AnalystSummary data={data} />}
|
|
@@ -374,8 +375,6 @@ export function AnalystResearchView({ focused, width, height }: { focused: boole
|
|
|
374
375
|
sortDirection={sortPreference.direction}
|
|
375
376
|
onHeaderClick={handleHeaderClick}
|
|
376
377
|
getItemKey={(row, index) => `${row.date}:${row.firm}:${index}`}
|
|
377
|
-
isSelected={() => false}
|
|
378
|
-
onSelect={() => {}}
|
|
379
378
|
renderCell={renderCell}
|
|
380
379
|
emptyStateTitle={loading ? "Loading analyst data..." : error ?? "No analyst data"}
|
|
381
380
|
/>
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { useCallback, useMemo } from "react";
|
|
2
|
-
import { TextAttributes } from "../../../ui";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Box, ScrollBox, Text, TextAttributes, type ScrollBoxRenderable } from "../../../ui";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
DataTableStackView,
|
|
5
5
|
usePaneFooter,
|
|
6
6
|
type DataTableCell,
|
|
7
7
|
type DataTableColumn,
|
|
8
8
|
type DataTableKeyEvent,
|
|
9
9
|
} from "../../../components";
|
|
10
|
+
import type { SecFilingDocument, SecFilingItem } from "../../../types/data-provider";
|
|
10
11
|
import type {
|
|
11
12
|
AnalystEstimateRecord,
|
|
12
13
|
AnalystResearchData,
|
|
@@ -16,9 +17,23 @@ import type {
|
|
|
16
17
|
} from "../../../types/financials";
|
|
17
18
|
import { blendHex, colors } from "../../../theme/colors";
|
|
18
19
|
import { formatCompact, formatCurrency, formatNumber, formatPercent, formatPercentRaw } from "../../../utils/format";
|
|
20
|
+
import { isPlainKey } from "../../../utils/keyboard";
|
|
21
|
+
import { wrapTextLines } from "../../../utils/text-wrap";
|
|
22
|
+
import { useResolvedEntryValue, useSecFilingContent, useSecFilingDocuments, useSecFilingsQuery } from "../../../market-data/hooks";
|
|
23
|
+
import { getSharedMarketDataCoordinator } from "../../../market-data/coordinator";
|
|
24
|
+
import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
25
|
+
import { isUsEquityTicker } from "../../../utils/sec";
|
|
19
26
|
import { useAssetData } from "../../runtime";
|
|
20
27
|
import { handleRefreshKey, loadingErrorFooterInfo, refreshFooterHint } from "../shared/table-pane";
|
|
21
28
|
import { useBoundTicker as useSymbolBinding, useTickerRequest } from "../shared/ticker-request";
|
|
29
|
+
import {
|
|
30
|
+
documentContentKey,
|
|
31
|
+
documentContentTarget,
|
|
32
|
+
documentHeading,
|
|
33
|
+
formatCompactDocumentLabel,
|
|
34
|
+
isDefaultVisibleFilingDocument,
|
|
35
|
+
isInlineExhibitDocument,
|
|
36
|
+
} from "../sec/filing-documents";
|
|
22
37
|
|
|
23
38
|
type EventStatus = "Earnings" | "Q Est" | "FY Est" | "TTM" | "Dividend" | "Split";
|
|
24
39
|
|
|
@@ -46,6 +61,9 @@ type EstimatePair = {
|
|
|
46
61
|
revenue?: AnalystEstimateRecord;
|
|
47
62
|
};
|
|
48
63
|
|
|
64
|
+
const SEC_EVENT_FILING_LIMIT = 50;
|
|
65
|
+
const SEC_EVENT_MATCH_WINDOW_DAYS = 7;
|
|
66
|
+
|
|
49
67
|
function todayDateKey(): string {
|
|
50
68
|
const now = new Date();
|
|
51
69
|
const year = now.getFullYear();
|
|
@@ -84,6 +102,75 @@ function daysBetween(leftDate: string, rightDate: string): number {
|
|
|
84
102
|
return Math.abs(left - right) / 86_400_000;
|
|
85
103
|
}
|
|
86
104
|
|
|
105
|
+
function dateKeyToEpochDay(dateKey: string): number | null {
|
|
106
|
+
const timestamp = new Date(`${dateKey}T00:00:00Z`).getTime();
|
|
107
|
+
if (!Number.isFinite(timestamp)) return null;
|
|
108
|
+
return Math.floor(timestamp / 86_400_000);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function signedDaysBetween(leftDate: string, rightDate: string): number {
|
|
112
|
+
const left = dateKeyToEpochDay(leftDate);
|
|
113
|
+
const right = dateKeyToEpochDay(rightDate);
|
|
114
|
+
if (left == null || right == null) return Number.POSITIVE_INFINITY;
|
|
115
|
+
return right - left;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function filingDateKey(filing: SecFilingItem): string {
|
|
119
|
+
const value = filing.filingDate as Date | string | number;
|
|
120
|
+
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
121
|
+
const date = new Date(value);
|
|
122
|
+
if (!Number.isNaN(date.getTime())) return date.toISOString().slice(0, 10);
|
|
123
|
+
return String(value).slice(0, 10);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isSecEarningsFilingCandidate(filing: SecFilingItem): boolean {
|
|
127
|
+
const form = filing.form.trim().toUpperCase();
|
|
128
|
+
return form === "8-K"
|
|
129
|
+
|| form === "8-K/A"
|
|
130
|
+
|| form === "10-Q"
|
|
131
|
+
|| form === "10-Q/A"
|
|
132
|
+
|| form === "10-K"
|
|
133
|
+
|| form === "10-K/A";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function filingSearchText(filing: SecFilingItem): string {
|
|
137
|
+
return [
|
|
138
|
+
filing.form,
|
|
139
|
+
filing.items,
|
|
140
|
+
filing.primaryDocument,
|
|
141
|
+
filing.primaryDocDescription,
|
|
142
|
+
].filter(Boolean).join(" ").toUpperCase();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function scoreFilingForEarnings(filing: SecFilingItem, earningsDate: string): number | null {
|
|
146
|
+
if (!isSecEarningsFilingCandidate(filing)) return null;
|
|
147
|
+
const delta = signedDaysBetween(earningsDate, filingDateKey(filing));
|
|
148
|
+
if (!Number.isFinite(delta) || delta < -1 || delta > SEC_EVENT_MATCH_WINDOW_DAYS) return null;
|
|
149
|
+
|
|
150
|
+
const form = filing.form.trim().toUpperCase();
|
|
151
|
+
const text = filingSearchText(filing);
|
|
152
|
+
let score = Math.abs(delta) * 10;
|
|
153
|
+
if (delta < 0) score += 12;
|
|
154
|
+
if (form.startsWith("10-")) score += 30;
|
|
155
|
+
if (text.includes("2.02")) score -= 18;
|
|
156
|
+
if (text.includes("9.01")) score -= 5;
|
|
157
|
+
if (/RESULTS OF OPERATIONS|FINANCIAL CONDITION|EARNINGS/i.test(text)) score -= 6;
|
|
158
|
+
return score;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function matchEarningsSecFiling(row: { status: string; date: string } | null | undefined, filings: readonly SecFilingItem[]): SecFilingItem | null {
|
|
162
|
+
if (!row || row.status !== "Earnings") return null;
|
|
163
|
+
let best: { filing: SecFilingItem; score: number } | null = null;
|
|
164
|
+
for (const filing of filings) {
|
|
165
|
+
const score = scoreFilingForEarnings(filing, row.date);
|
|
166
|
+
if (score == null) continue;
|
|
167
|
+
if (!best || score < best.score) {
|
|
168
|
+
best = { filing, score };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return best?.filing ?? null;
|
|
172
|
+
}
|
|
173
|
+
|
|
87
174
|
function statementForEarningsDate(
|
|
88
175
|
quarterlyStatements: readonly FinancialStatement[],
|
|
89
176
|
earningsDate: string,
|
|
@@ -293,6 +380,98 @@ function toneColor(tone: EventRow["tone"]): string {
|
|
|
293
380
|
return colors.text;
|
|
294
381
|
}
|
|
295
382
|
|
|
383
|
+
function eventDetailTitle(row: EventRow): string {
|
|
384
|
+
return `${row.status} | ${row.date}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function eventSummaryLine(row: EventRow): string {
|
|
388
|
+
return [
|
|
389
|
+
row.date,
|
|
390
|
+
row.period,
|
|
391
|
+
row.qEps != null ? `EPS ${formatNumber(row.qEps, 2)}` : null,
|
|
392
|
+
row.qRevenue != null ? `Rev ${formatCompact(row.qRevenue)}` : null,
|
|
393
|
+
row.annualEps != null ? `Ann EPS ${formatNumber(row.annualEps, 2)}` : null,
|
|
394
|
+
row.annualRevenue != null ? `Ann Rev ${formatCompact(row.annualRevenue)}` : null,
|
|
395
|
+
row.value !== "-" ? row.value : null,
|
|
396
|
+
row.detail || null,
|
|
397
|
+
].filter((line): line is string => !!line).join(" | ");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function buildEventDetailBody({
|
|
401
|
+
row,
|
|
402
|
+
secFilingsLoading,
|
|
403
|
+
filing,
|
|
404
|
+
documents,
|
|
405
|
+
documentsLoading,
|
|
406
|
+
inlineContent,
|
|
407
|
+
primaryContent,
|
|
408
|
+
primaryContentLoading,
|
|
409
|
+
}: {
|
|
410
|
+
row: EventRow;
|
|
411
|
+
secFilingsLoading: boolean;
|
|
412
|
+
filing: SecFilingItem | null;
|
|
413
|
+
documents: SecFilingDocument[];
|
|
414
|
+
documentsLoading: boolean;
|
|
415
|
+
inlineContent: Map<string, string | null>;
|
|
416
|
+
primaryContent: string | null | undefined;
|
|
417
|
+
primaryContentLoading: boolean;
|
|
418
|
+
}): string {
|
|
419
|
+
const lines: string[] = ["Summary", eventSummaryLine(row)];
|
|
420
|
+
if (row.status !== "Earnings") {
|
|
421
|
+
return lines.join("\n");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
lines.push("", "SEC Filing");
|
|
425
|
+
if (secFilingsLoading && !filing) {
|
|
426
|
+
lines.push("Loading recent SEC filings...");
|
|
427
|
+
return lines.join("\n");
|
|
428
|
+
}
|
|
429
|
+
if (!filing) {
|
|
430
|
+
lines.push("No related SEC filing found in recent filings.");
|
|
431
|
+
return lines.join("\n");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
lines.push([
|
|
435
|
+
`${filing.form} filed ${filingDateKey(filing)}`,
|
|
436
|
+
filing.items ? `Items ${filing.items}` : null,
|
|
437
|
+
`Accession ${filing.accessionNumber}`,
|
|
438
|
+
].filter(Boolean).join(" | "));
|
|
439
|
+
|
|
440
|
+
lines.push("", "Documents");
|
|
441
|
+
if (documentsLoading && documents.length === 0) {
|
|
442
|
+
lines.push("Loading filing documents...");
|
|
443
|
+
} else if (documents.length === 0) {
|
|
444
|
+
lines.push("No filing documents were listed for this filing.");
|
|
445
|
+
} else {
|
|
446
|
+
const visibleDocuments = documents.filter(isDefaultVisibleFilingDocument);
|
|
447
|
+
lines.push(...visibleDocuments.map(formatCompactDocumentLabel));
|
|
448
|
+
const hiddenCount = documents.length - visibleDocuments.length;
|
|
449
|
+
if (hiddenCount > 0) lines.push(`+ ${hiddenCount} support documents hidden`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const exhibits = documents.filter(isInlineExhibitDocument);
|
|
453
|
+
if (exhibits.length > 0) {
|
|
454
|
+
lines.push("", "Inline Exhibits");
|
|
455
|
+
for (const document of exhibits) {
|
|
456
|
+
const key = documentContentKey(filing, document);
|
|
457
|
+
const hasContent = inlineContent.has(key);
|
|
458
|
+
const content = inlineContent.get(key);
|
|
459
|
+
lines.push("", documentHeading(document));
|
|
460
|
+
lines.push(hasContent
|
|
461
|
+
? content || "Readable document content was not available for this exhibit."
|
|
462
|
+
: "Loading exhibit content...");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!documentsLoading && exhibits.length === 0) {
|
|
467
|
+
lines.push("", "Primary Filing Content");
|
|
468
|
+
lines.push(primaryContentLoading
|
|
469
|
+
? "Loading filing content..."
|
|
470
|
+
: primaryContent || "Readable filing content was not available.");
|
|
471
|
+
}
|
|
472
|
+
return lines.join("\n");
|
|
473
|
+
}
|
|
474
|
+
|
|
296
475
|
export function CorporateActionsView({
|
|
297
476
|
focused,
|
|
298
477
|
width,
|
|
@@ -305,7 +484,7 @@ export function CorporateActionsView({
|
|
|
305
484
|
footerPaneId?: string;
|
|
306
485
|
}) {
|
|
307
486
|
const dataProvider = useAssetData();
|
|
308
|
-
const { symbol, exchange, currency } = useSymbolBinding();
|
|
487
|
+
const { symbol, ticker, exchange, currency } = useSymbolBinding();
|
|
309
488
|
const actionsLoader = useCallback((nextSymbol: string, nextExchange: string, forceRefresh: boolean) => {
|
|
310
489
|
if (!dataProvider?.getCorporateActions) throw new Error("Corporate actions source unavailable");
|
|
311
490
|
return dataProvider.getCorporateActions(nextSymbol, nextExchange, forceRefresh ? { cacheMode: "refresh" } : undefined);
|
|
@@ -341,6 +520,12 @@ export function CorporateActionsView({
|
|
|
341
520
|
buildEventRows(actionsData, analystData, financialsData, displayCurrency)
|
|
342
521
|
), [actionsData, analystData, displayCurrency, financialsData]);
|
|
343
522
|
const columns = useMemo(() => buildEventColumns(), []);
|
|
523
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
524
|
+
const [openRowId, setOpenRowId] = useState<string | null>(null);
|
|
525
|
+
const detailScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
526
|
+
const [inlineContent, setInlineContent] = useState<Map<string, string | null>>(new Map());
|
|
527
|
+
const documentFetchStartedRef = useRef(new Set<string>());
|
|
528
|
+
const documentFetchGenRef = useRef(0);
|
|
344
529
|
const todayKey = todayDateKey();
|
|
345
530
|
const futureRowBackground = blendHex(colors.bg, colors.positive, 0.16);
|
|
346
531
|
const loading = actionsLoading || analystLoading || financialsLoading;
|
|
@@ -350,6 +535,163 @@ export function CorporateActionsView({
|
|
|
350
535
|
reloadAnalyst();
|
|
351
536
|
reloadFinancials();
|
|
352
537
|
}, [reloadActions, reloadAnalyst, reloadFinancials]);
|
|
538
|
+
const openRow = openRowId
|
|
539
|
+
? rows.find((row) => row.id === openRowId) ?? null
|
|
540
|
+
: null;
|
|
541
|
+
const instrument = useMemo(() => instrumentFromTicker(ticker, symbol), [symbol, ticker]);
|
|
542
|
+
const secFilingsEntry = useSecFilingsQuery(
|
|
543
|
+
openRow?.status === "Earnings" && instrument && isUsEquityTicker(ticker)
|
|
544
|
+
? { instrument, count: SEC_EVENT_FILING_LIMIT }
|
|
545
|
+
: null,
|
|
546
|
+
);
|
|
547
|
+
const secFilings = useResolvedEntryValue(secFilingsEntry) ?? [];
|
|
548
|
+
const secFilingsLoading = openRow?.status === "Earnings" && (
|
|
549
|
+
secFilingsEntry?.phase === "idle"
|
|
550
|
+
|| secFilingsEntry?.phase === "loading"
|
|
551
|
+
|| secFilingsEntry?.phase === "refreshing"
|
|
552
|
+
);
|
|
553
|
+
const matchedFiling = useMemo(
|
|
554
|
+
() => matchEarningsSecFiling(openRow, secFilings),
|
|
555
|
+
[openRow, secFilings],
|
|
556
|
+
);
|
|
557
|
+
const documentsEntry = useSecFilingDocuments(matchedFiling);
|
|
558
|
+
const documents = useResolvedEntryValue(documentsEntry) ?? [];
|
|
559
|
+
const documentsLoading = !!matchedFiling && (
|
|
560
|
+
documentsEntry?.phase === "idle"
|
|
561
|
+
|| documentsEntry?.phase === "loading"
|
|
562
|
+
|| documentsEntry?.phase === "refreshing"
|
|
563
|
+
);
|
|
564
|
+
const hasInlineExhibits = documents.some(isInlineExhibitDocument);
|
|
565
|
+
const filingContentEntry = useSecFilingContent(
|
|
566
|
+
matchedFiling && !documentsLoading && !hasInlineExhibits ? matchedFiling : null,
|
|
567
|
+
);
|
|
568
|
+
const primaryContent = useResolvedEntryValue(filingContentEntry);
|
|
569
|
+
const primaryContentLoading = !!matchedFiling && (
|
|
570
|
+
filingContentEntry?.phase === "idle"
|
|
571
|
+
|| filingContentEntry?.phase === "loading"
|
|
572
|
+
|| filingContentEntry?.phase === "refreshing"
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
useEffect(() => {
|
|
576
|
+
if (rows.length > 0 && selectedIdx >= rows.length) {
|
|
577
|
+
setSelectedIdx(Math.max(0, rows.length - 1));
|
|
578
|
+
}
|
|
579
|
+
}, [rows.length, selectedIdx]);
|
|
580
|
+
|
|
581
|
+
useEffect(() => {
|
|
582
|
+
if (openRowId && !rows.some((row) => row.id === openRowId)) {
|
|
583
|
+
setOpenRowId(null);
|
|
584
|
+
}
|
|
585
|
+
}, [openRowId, rows]);
|
|
586
|
+
|
|
587
|
+
useEffect(() => {
|
|
588
|
+
if (!openRowId) return;
|
|
589
|
+
const scrollBox = detailScrollRef.current;
|
|
590
|
+
if (scrollBox) scrollBox.scrollTop = 0;
|
|
591
|
+
}, [openRowId]);
|
|
592
|
+
|
|
593
|
+
useEffect(() => {
|
|
594
|
+
setInlineContent(new Map());
|
|
595
|
+
documentFetchStartedRef.current.clear();
|
|
596
|
+
documentFetchGenRef.current += 1;
|
|
597
|
+
}, [exchange, symbol]);
|
|
598
|
+
|
|
599
|
+
useEffect(() => {
|
|
600
|
+
if (!matchedFiling || documents.length === 0) return;
|
|
601
|
+
const coordinator = getSharedMarketDataCoordinator();
|
|
602
|
+
if (!coordinator) return;
|
|
603
|
+
const gen = documentFetchGenRef.current;
|
|
604
|
+
const documentsToFetch = documents.filter((document) => {
|
|
605
|
+
if (!isInlineExhibitDocument(document)) return false;
|
|
606
|
+
const key = documentContentKey(matchedFiling, document);
|
|
607
|
+
return !inlineContent.has(key) && !documentFetchStartedRef.current.has(key);
|
|
608
|
+
});
|
|
609
|
+
if (documentsToFetch.length === 0) return;
|
|
610
|
+
|
|
611
|
+
for (const document of documentsToFetch) {
|
|
612
|
+
documentFetchStartedRef.current.add(documentContentKey(matchedFiling, document));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
(async () => {
|
|
616
|
+
for (const document of documentsToFetch) {
|
|
617
|
+
if (documentFetchGenRef.current !== gen) return;
|
|
618
|
+
const key = documentContentKey(matchedFiling, document);
|
|
619
|
+
try {
|
|
620
|
+
const entry = await coordinator.loadSecFilingContent(documentContentTarget(matchedFiling, document));
|
|
621
|
+
if (documentFetchGenRef.current !== gen) return;
|
|
622
|
+
setInlineContent((prev) => new Map(prev).set(key, entry.data ?? entry.lastGoodData ?? null));
|
|
623
|
+
} catch {
|
|
624
|
+
if (documentFetchGenRef.current !== gen) return;
|
|
625
|
+
setInlineContent((prev) => new Map(prev).set(key, null));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
})();
|
|
629
|
+
}, [documents, inlineContent, matchedFiling]);
|
|
630
|
+
const detailBody = openRow
|
|
631
|
+
? buildEventDetailBody({
|
|
632
|
+
row: openRow,
|
|
633
|
+
secFilingsLoading,
|
|
634
|
+
filing: matchedFiling,
|
|
635
|
+
documents,
|
|
636
|
+
documentsLoading,
|
|
637
|
+
inlineContent,
|
|
638
|
+
primaryContent,
|
|
639
|
+
primaryContentLoading,
|
|
640
|
+
})
|
|
641
|
+
: "";
|
|
642
|
+
const detailTextWidth = Math.max(width - 2, 12);
|
|
643
|
+
const scrollDetailBy = useCallback((delta: number) => {
|
|
644
|
+
const scrollBox = detailScrollRef.current;
|
|
645
|
+
if (!scrollBox?.viewport) return;
|
|
646
|
+
const maxScrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height);
|
|
647
|
+
scrollBox.scrollTop = Math.max(0, Math.min(maxScrollTop, scrollBox.scrollTop + delta));
|
|
648
|
+
}, []);
|
|
649
|
+
|
|
650
|
+
const handleDetailKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
651
|
+
if (isPlainKey(event, "j", "down")) {
|
|
652
|
+
event.stopPropagation?.();
|
|
653
|
+
event.preventDefault?.();
|
|
654
|
+
scrollDetailBy(1);
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
if (isPlainKey(event, "k", "up")) {
|
|
658
|
+
event.stopPropagation?.();
|
|
659
|
+
event.preventDefault?.();
|
|
660
|
+
scrollDetailBy(-1);
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
}, [scrollDetailBy]);
|
|
665
|
+
const detailContent = openRow ? (
|
|
666
|
+
<Box
|
|
667
|
+
flexDirection="column"
|
|
668
|
+
flexGrow={1}
|
|
669
|
+
flexBasis={0}
|
|
670
|
+
minHeight={0}
|
|
671
|
+
overflow="hidden"
|
|
672
|
+
paddingX={1}
|
|
673
|
+
paddingY={1}
|
|
674
|
+
>
|
|
675
|
+
<ScrollBox
|
|
676
|
+
ref={detailScrollRef}
|
|
677
|
+
flexGrow={1}
|
|
678
|
+
flexBasis={0}
|
|
679
|
+
minHeight={0}
|
|
680
|
+
scrollY
|
|
681
|
+
focusable={false}
|
|
682
|
+
>
|
|
683
|
+
<Box flexDirection="column">
|
|
684
|
+
{wrapTextLines(detailBody, detailTextWidth).map((line, index) => (
|
|
685
|
+
<Box key={`event-detail-${index}`} height={1}>
|
|
686
|
+
<Text fg={colors.text}>{line}</Text>
|
|
687
|
+
</Box>
|
|
688
|
+
))}
|
|
689
|
+
</Box>
|
|
690
|
+
</ScrollBox>
|
|
691
|
+
</Box>
|
|
692
|
+
) : (
|
|
693
|
+
<Box flexGrow={1} />
|
|
694
|
+
);
|
|
353
695
|
|
|
354
696
|
const renderCell = useCallback((
|
|
355
697
|
row: EventRow,
|
|
@@ -390,8 +732,19 @@ export function CorporateActionsView({
|
|
|
390
732
|
}), [error, footerPaneId, loading, reload]);
|
|
391
733
|
|
|
392
734
|
return (
|
|
393
|
-
<
|
|
735
|
+
<DataTableStackView<EventRow, EventColumn>
|
|
394
736
|
focused={focused}
|
|
737
|
+
detailOpen={!!openRow}
|
|
738
|
+
onBack={() => setOpenRowId(null)}
|
|
739
|
+
detailContent={detailContent}
|
|
740
|
+
detailTitle={openRow ? eventDetailTitle(openRow) : undefined}
|
|
741
|
+
selection={{
|
|
742
|
+
kind: "index",
|
|
743
|
+
selectedIndex: selectedIdx,
|
|
744
|
+
onChange: (index) => setSelectedIdx(index),
|
|
745
|
+
}}
|
|
746
|
+
onActivate={(row) => setOpenRowId(row.id)}
|
|
747
|
+
onDetailKeyDown={handleDetailKeyDown}
|
|
395
748
|
rootWidth={width}
|
|
396
749
|
rootHeight={height}
|
|
397
750
|
onRootKeyDown={handleKeyDown}
|
|
@@ -401,8 +754,6 @@ export function CorporateActionsView({
|
|
|
401
754
|
sortDirection="desc"
|
|
402
755
|
onHeaderClick={() => {}}
|
|
403
756
|
getItemKey={(row) => row.id}
|
|
404
|
-
isSelected={() => false}
|
|
405
|
-
onSelect={() => {}}
|
|
406
757
|
renderCell={renderCell}
|
|
407
758
|
getRowBackgroundColor={(row) => (
|
|
408
759
|
row.date > todayKey ? futureRowBackground : undefined
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
sortRatingRows,
|
|
9
9
|
type RatingSortPreference,
|
|
10
10
|
} from "./analyst-pane";
|
|
11
|
-
import { buildEventRows } from "./corporate-actions-pane";
|
|
11
|
+
import { buildEventRows, matchEarningsSecFiling } from "./corporate-actions-pane";
|
|
12
12
|
|
|
13
13
|
const ratings: AnalystRatingRecord[] = [
|
|
14
14
|
{
|
|
@@ -337,4 +337,33 @@ describe("event rows", () => {
|
|
|
337
337
|
expect(rows[2]?.qEps).toBeUndefined();
|
|
338
338
|
expect(rows[2]?.annualEps).toBeUndefined();
|
|
339
339
|
});
|
|
340
|
+
|
|
341
|
+
test("matches reported earnings to nearby SEC earnings-release filings", () => {
|
|
342
|
+
const row = buildEventRows({
|
|
343
|
+
symbol: "AAPL",
|
|
344
|
+
dividends: [],
|
|
345
|
+
splits: [],
|
|
346
|
+
earnings: [{ date: "2026-01-30", epsActual: 2.4 }],
|
|
347
|
+
}, null, null, "USD").find((candidate) => candidate.status === "Earnings");
|
|
348
|
+
const filings = [
|
|
349
|
+
{
|
|
350
|
+
accessionNumber: "0000320193-26-000010",
|
|
351
|
+
form: "10-Q",
|
|
352
|
+
filingDate: new Date("2026-02-04T00:00:00Z"),
|
|
353
|
+
cik: "0000320193",
|
|
354
|
+
filingUrl: "https://www.sec.gov/Archives/edgar/data/320193/0000320193-26-000010-index.htm",
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
accessionNumber: "0000320193-26-000009",
|
|
358
|
+
form: "8-K",
|
|
359
|
+
filingDate: "2026-01-31T00:00:00.000Z" as unknown as Date,
|
|
360
|
+
items: "2.02,9.01",
|
|
361
|
+
primaryDocDescription: "Results of Operations and Financial Condition",
|
|
362
|
+
cik: "0000320193",
|
|
363
|
+
filingUrl: "https://www.sec.gov/Archives/edgar/data/320193/0000320193-26-000009-index.htm",
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
expect(matchEarningsSecFiling(row, filings)?.accessionNumber).toBe("0000320193-26-000009");
|
|
368
|
+
});
|
|
340
369
|
});
|
|
@@ -165,9 +165,12 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
165
165
|
return (
|
|
166
166
|
<DataTableView<RelativeRow, RelativeColumn>
|
|
167
167
|
focused={focused}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
selection={{
|
|
169
|
+
kind: "index",
|
|
170
|
+
selectedIndex: rows.length > 0 ? selectedIdx : -1,
|
|
171
|
+
onChange: (index) => setSelectedIdx(index),
|
|
172
|
+
}}
|
|
173
|
+
onActivate={(row) => navigateTicker(row.symbol)}
|
|
171
174
|
onRootKeyDown={handleKeyDown}
|
|
172
175
|
rootWidth={width}
|
|
173
176
|
rootHeight={height}
|
|
@@ -177,11 +180,6 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
177
180
|
sortDirection="asc"
|
|
178
181
|
onHeaderClick={() => {}}
|
|
179
182
|
getItemKey={(row) => row.symbol}
|
|
180
|
-
isSelected={(row) => rows[selectedIdx]?.symbol === row.symbol}
|
|
181
|
-
onSelect={(row, index) => {
|
|
182
|
-
setSelectedIdx(index);
|
|
183
|
-
navigateTicker(row.symbol);
|
|
184
|
-
}}
|
|
185
183
|
renderCell={renderCell}
|
|
186
184
|
emptyStateTitle={loading ? "Loading peers..." : error ?? "No peers"}
|
|
187
185
|
/>
|