@gloom-sh/terminal 0.19.1 → 0.19.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 +2 -1
- package/src/api-client/errors.ts +38 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/capabilities/chart-series.ts +2 -1
- package/src/cli/commands/market.ts +9 -4
- package/src/cli/commands/overview.ts +5 -6
- package/src/cli/commands/search.ts +13 -13
- package/src/cli/context.ts +1 -0
- package/src/cli/glossary.ts +2 -1
- package/src/cli/history-rows.ts +35 -7
- package/src/cli/known-gaps.ts +11 -0
- package/src/cli/listing-arg.ts +17 -1
- package/src/cli/quote-answer.ts +29 -6
- package/src/components/command-bar/routes/root/runtime.ts +27 -5
- package/src/components/command-bar/selection-runtime.ts +12 -29
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +2 -2
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +33 -1
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +2 -1
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-wizard.tsx +6 -3
- package/src/components/ui/display.tsx +16 -5
- package/src/components/ui/icon.tsx +8 -2
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/plugin-cache.ts +15 -0
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/resource-store.ts +130 -58
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +313 -0
- package/src/data/sqlite/checkpointer.ts +198 -0
- package/src/data/sqlite/database.ts +47 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +4 -2
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/series.ts +4 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +19 -11
- package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
- package/src/plugins/builtin/econ/detail-view.tsx +16 -2
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/gpu/client.ts +4 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/market-movers/client.ts +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +10 -5
- package/src/plugins/builtin/market-movers/screener.ts +23 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/risk-factors/data.ts +8 -4
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/shared/cloud-resource.ts +13 -8
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/thirteenf/api.ts +11 -16
- package/src/plugins/builtin/thirteenf/data.ts +34 -6
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
- package/src/plugins/builtin/thirteenf/types.ts +2 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +6 -2
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/yield-curve/client.ts +4 -6
- package/src/plugins/installer.ts +50 -39
- package/src/renderers/dom/desktop/icons.tsx +3 -2
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +2 -0
- package/src/renderers/electrobun/launcher/preload.ts +13 -0
- package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
- package/src/renderers/electrobun/view/ui-host.tsx +9 -2
- package/src/series/resolve.ts +2 -1
- package/src/series/types.ts +5 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
- package/src/telemetry/stall-reports-node.ts +13 -2
- package/src/telemetry/stall-reports.ts +189 -13
- package/src/test-support/outage.ts +8 -0
- package/src/tickers/company-name-listing.ts +59 -29
- package/src/tickers/company-name.ts +65 -1
- package/src/tickers/search/home-listing.ts +93 -8
- package/src/tickers/search/index.ts +1 -1
- package/src/time-series/market.ts +5 -3
- package/src/time-series/price-comparison.ts +20 -2
- package/src/time-series/resolve.ts +20 -1
- package/src/time-series/types.ts +7 -0
- package/src/types/financials.ts +18 -3
- package/src/ui/host.tsx +6 -1
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +17 -5
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +47 -2
- package/src/utils/perf-marks.ts +23 -11
- package/src/utils/price-history-integrity.ts +47 -1
|
@@ -70,19 +70,23 @@ async function routeAnswers(route: FreeRoute, check: NonNullable<FreeRoute["answ
|
|
|
70
70
|
* symbol the routes read `<ticker>` and the first is named; without a way to
|
|
71
71
|
* read data, only a route that answers for every symbol is.
|
|
72
72
|
*/
|
|
73
|
-
async function answeringFreeRoute(lock: PaneLock, routes: readonly FreeRoute[]): Promise<FreeRoute | null> {
|
|
73
|
+
async function answeringFreeRoute(lock: PaneLock, routes: readonly FreeRoute[]): Promise<{ route: FreeRoute | null; noneHasData: boolean }> {
|
|
74
74
|
const { symbol, context } = lock;
|
|
75
|
-
if (!symbol) return routes[0] ?? null;
|
|
75
|
+
if (!symbol) return { route: routes[0] ?? null, noneHasData: false };
|
|
76
76
|
// Routes past the first that needs no check are never named, so they are not checked.
|
|
77
77
|
const unchecked = routes.findIndex((route) => !route.answers);
|
|
78
78
|
const candidates = unchecked < 0 ? routes : routes.slice(0, unchecked + 1);
|
|
79
79
|
const deadline = Date.now() + ROUTE_CHECK_WAIT_MS;
|
|
80
80
|
const checks = candidates.map((route) => !route.answers ? Promise.resolve(true)
|
|
81
|
-
: context ? routeAnswers(route, route.answers, symbol, context, deadline) : Promise.resolve(
|
|
81
|
+
: context ? routeAnswers(route, route.answers, symbol, context, deadline) : Promise.resolve(null));
|
|
82
|
+
const answered: Array<boolean | null> = [];
|
|
82
83
|
for (const [index, check] of checks.entries()) {
|
|
83
|
-
|
|
84
|
+
const answer = await check;
|
|
85
|
+
if (answer) return { route: candidates[index]!, noneHasData: false };
|
|
86
|
+
answered.push(answer);
|
|
84
87
|
}
|
|
85
|
-
|
|
88
|
+
// Every check answered no: a failed or stalled one is not known to.
|
|
89
|
+
return { route: null, noneHasData: candidates.length > 0 && answered.every((answer) => answer === false) };
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
/**
|
|
@@ -114,6 +118,14 @@ export async function proLockNote(
|
|
|
114
118
|
* command repeats the ticker only when the reader gave one. Default `ticker`.
|
|
115
119
|
*/
|
|
116
120
|
argument?: "ticker" | "optional" | "none";
|
|
121
|
+
/**
|
|
122
|
+
* For a function that reads what its free routes read (RISK the SEC's
|
|
123
|
+
* 10-Ks and 20-Fs, FLOW the listing's options): the sentence the note
|
|
124
|
+
* says when every route answered that the symbol has none. The note then
|
|
125
|
+
* names no command for the symbol, which would read nothing once unlocked
|
|
126
|
+
* either: IAG:LSE files neither form, and LHA:XETRA has no options served.
|
|
127
|
+
*/
|
|
128
|
+
noneFor?: (ticker: string) => string;
|
|
117
129
|
},
|
|
118
130
|
): Promise<string> {
|
|
119
131
|
const { gate, symbol, token: typed, verifyEmail } = lock;
|
|
@@ -129,12 +141,15 @@ export async function proLockNote(
|
|
|
129
141
|
const unlock = gate === "pro"
|
|
130
142
|
? `To read ${pane.shows} in ${token}, upgrade in the app (gloom, then Ctrl+P, UPGRADE)`
|
|
131
143
|
: `To read ${pane.shows} in ${token}, open the app (gloom), ${account}${pro ? ", then upgrade (Ctrl+P, UPGRADE)" : ""}`;
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const
|
|
144
|
+
const { route: free, noneHasData } = pane.free ? await answeringFreeRoute(lock, pane.free) : { route: null, noneHasData: false };
|
|
145
|
+
// Unlocking reads nothing for a symbol none of the function's own data covers: it is named only where it has some.
|
|
146
|
+
const uncovered = noneHasData && !!pane.noneFor;
|
|
147
|
+
const bare = pane.argument === "none" || (pane.argument === "optional" && (!symbol || uncovered));
|
|
148
|
+
const command = bare ? `gloom fn ${token}` : `gloom fn ${token} ${uncovered ? "<ticker>" : ticker}`;
|
|
135
149
|
return [
|
|
136
150
|
`${token} ${what}: ${pane.does}. ${without} it shows none of ${pane.shows}.`,
|
|
137
151
|
...free ? [`Free, with no sign-in: ${free.text(ticker, symbol ?? "the company")}`] : [],
|
|
152
|
+
...uncovered ? [pane.noneFor!(ticker)] : [],
|
|
138
153
|
`${unlock}; ${command} then reads with that session.`,
|
|
139
154
|
].join("\n");
|
|
140
155
|
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* What a loaded catalog series says of an old copy. A copy that answered
|
|
3
|
+
* because its refresh failed carries when it was saved (`savedAt`), which the
|
|
4
|
+
* chart's footer shows as "saved copy from …", the note every pane gives it.
|
|
5
|
+
* A cached copy the source has not updated since is a note on the chart.
|
|
6
|
+
*/
|
|
7
|
+
export function cachedSeriesNote(resource: { stale?: boolean; refreshError?: string | null; savedAt?: number | null }): {
|
|
8
|
+
warning?: string;
|
|
9
|
+
savedAt?: number;
|
|
10
|
+
} {
|
|
11
|
+
if (resource.savedAt != null && Number.isFinite(resource.savedAt)) return { savedAt: resource.savedAt };
|
|
12
|
+
if (resource.refreshError) return { warning: `Refresh failed (${resource.refreshError}); showing cached data.` };
|
|
13
|
+
return resource.stale ? { warning: "Showing cached data; the source has not updated since." } : {};
|
|
5
14
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SeriesDefinition } from "../../../series/types";
|
|
2
|
-
import {
|
|
2
|
+
import { cachedSeriesNote } from "../shared/series-warning";
|
|
3
3
|
import { loadShortVolume } from "./client";
|
|
4
4
|
|
|
5
5
|
/** The share of a ticker's daily on-exchange volume reported as short sales. */
|
|
@@ -14,10 +14,9 @@ export const SHORT_VOLUME_SERIES: readonly SeriesDefinition[] = [{
|
|
|
14
14
|
input: "ticker",
|
|
15
15
|
load: async ({ ticker }) => {
|
|
16
16
|
const resource = await loadShortVolume(ticker!, "nms");
|
|
17
|
-
const warning = cachedSeriesWarning(resource);
|
|
18
17
|
return {
|
|
19
18
|
points: resource.payload.history.map((point) => ({ date: point.date, value: point.ratioPercent })),
|
|
20
|
-
...(
|
|
19
|
+
...cachedSeriesNote(resource),
|
|
21
20
|
};
|
|
22
21
|
},
|
|
23
22
|
}];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { unavailableText } from "../../../components/ui/status";
|
|
2
2
|
import { apiClient } from "../../../api-client";
|
|
3
|
-
import {
|
|
3
|
+
import { isAccessDenied } from "../../../api-client/errors";
|
|
4
4
|
import type { SocialMentionDayPosts, SocialMentionPost, SocialMentionsPayload, SocialMentionsRange } from "../../../api-client/social-mentions";
|
|
5
|
-
import { createPluginCache } from "../../../data/plugin-cache";
|
|
5
|
+
import { createPluginCache, savedCopyForTransientFailure } from "../../../data/plugin-cache";
|
|
6
6
|
import { CLOUD_SESSION_REQUIRED } from "../shared/research-cloud-session";
|
|
7
|
+
import { unavailableOnServer } from "../shared/cloud-resource";
|
|
7
8
|
|
|
8
9
|
export const socialMentionsCache = createPluginCache<SocialMentionsPayload>({
|
|
9
10
|
kind: "social-mentions", source: "gloom-cloud", schemaVersion: 1,
|
|
@@ -56,12 +57,15 @@ export function validateSocialMentions(payload: SocialMentionsPayload, symbol: s
|
|
|
56
57
|
export async function fetchSocialMentions(symbol: string, range: SocialMentionsRange = "1y",
|
|
57
58
|
client: Pick<typeof apiClient, "getCloudSocialMentions"> = apiClient): Promise<SocialMentionsPayload> {
|
|
58
59
|
try { return validateSocialMentions(await client.getCloudSocialMentions(symbol, range), symbol, range); }
|
|
59
|
-
catch (error) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
catch (error) { throw unavailableOnServer(error, unavailableText("social mentions")); }
|
|
61
|
+
}
|
|
62
|
+
export interface SocialMentionsResource {
|
|
63
|
+
payload: SocialMentionsPayload;
|
|
64
|
+
stale: boolean;
|
|
65
|
+
refreshError: string | null;
|
|
66
|
+
/** When the copy shown was saved, when it is one because its refresh failed. */
|
|
67
|
+
savedAt?: number;
|
|
63
68
|
}
|
|
64
|
-
export interface SocialMentionsResource { payload: SocialMentionsPayload; stale: boolean; refreshError: string | null }
|
|
65
69
|
const cacheKey = (symbol: string, range: SocialMentionsRange) => `${range}:${symbol}`;
|
|
66
70
|
export function cachedSocialMentions(symbol: string, range: SocialMentionsRange): SocialMentionsResource | null {
|
|
67
71
|
const cached = socialMentionsCache.get(cacheKey(symbol, range), { allowExpired: true });
|
|
@@ -70,9 +74,10 @@ export function cachedSocialMentions(symbol: string, range: SocialMentionsRange)
|
|
|
70
74
|
catch { return null; }
|
|
71
75
|
}
|
|
72
76
|
export async function loadSocialMentions(symbol: string, range: SocialMentionsRange, force = false): Promise<SocialMentionsResource> {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return { payload: validateSocialMentions(result.data, symbol, range), stale: result.stale, refreshError: result.refreshError ?? null
|
|
77
|
+
// A refusal (a signed-out session, any other 4xx but 408 and 429) or a reply that fails validation shows as itself.
|
|
78
|
+
const result = savedCopyForTransientFailure(await socialMentionsCache.load(cacheKey(symbol, range), () => fetchSocialMentions(symbol, range), { force }));
|
|
79
|
+
return { payload: validateSocialMentions(result.data, symbol, range), stale: result.stale, refreshError: result.refreshError ?? null,
|
|
80
|
+
...(result.source === "stale-fallback" ? { savedAt: result.fetchedAt } : {}) };
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SeriesDefinition } from "../../../series/types";
|
|
2
|
-
import {
|
|
2
|
+
import { cachedSeriesNote } from "../shared/series-warning";
|
|
3
3
|
import { loadSocialMentions } from "./client";
|
|
4
4
|
|
|
5
5
|
/** How often a ticker is posted about each day, over the last year. */
|
|
@@ -14,10 +14,9 @@ export const SOCIAL_MENTION_SERIES: readonly SeriesDefinition[] = [{
|
|
|
14
14
|
style: "columns",
|
|
15
15
|
load: async ({ ticker }) => {
|
|
16
16
|
const resource = await loadSocialMentions(ticker!, "1y");
|
|
17
|
-
const warning = cachedSeriesWarning(resource);
|
|
18
17
|
return {
|
|
19
18
|
points: resource.payload.x.days.map((row) => ({ date: row.day, value: row.mentions })),
|
|
20
|
-
...(
|
|
19
|
+
...cachedSeriesNote(resource),
|
|
21
20
|
};
|
|
22
21
|
},
|
|
23
22
|
}];
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
} from "./types";
|
|
11
11
|
import type { PluginPersistence } from "../../../types/plugin";
|
|
12
12
|
import { apiClient } from "../../../api-client";
|
|
13
|
-
import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
|
|
13
|
+
import { ApiRequestError, isPermanentClientError, isTransientFailure } from "../../../api-client/errors";
|
|
14
14
|
import { httpFetch } from "../../../utils/http-transport";
|
|
15
15
|
import { asRecord, finiteOrNull } from "../../../utils/guards";
|
|
16
16
|
import { logFailure } from "../../../utils/log-failure";
|
|
@@ -30,11 +30,12 @@ const FORMS_13F_CACHE_POLICY = {
|
|
|
30
30
|
} as const;
|
|
31
31
|
|
|
32
32
|
export interface Forms13FReadOptions {
|
|
33
|
+
/** A part of the answer that could not be read, said beside what was. */
|
|
33
34
|
onWarning?: (warning: string) => void;
|
|
34
35
|
/**
|
|
35
36
|
* A read answered from the copy saved at `savedAt` (ms) because its refresh
|
|
36
|
-
* failed
|
|
37
|
-
*
|
|
37
|
+
* failed. Only a caller that says when its copy was saved (the footer's
|
|
38
|
+
* "saved copy from", a command's closing line) is answered from one.
|
|
38
39
|
*/
|
|
39
40
|
onSavedCopy?: (savedAt: number) => void;
|
|
40
41
|
forceRefresh?: boolean;
|
|
@@ -156,23 +157,17 @@ async function fetchForms13F<T>(
|
|
|
156
157
|
} catch (error) {
|
|
157
158
|
options.signal?.throwIfAborted();
|
|
158
159
|
if (current()) failedRefreshes.add(key);
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
// A saved copy answers only for a failure asking again may get past; a refusal or a reply that
|
|
161
|
+
// fails validation is the error, and a missing or denied resource also drops its copy.
|
|
162
|
+
if (!isTransientFailure(error)) {
|
|
163
|
+
if (isPermanentClientError(error) && current()) store?.deleteResource(CACHE_KIND, key, { sourceKey: CACHE_SOURCE });
|
|
161
164
|
throw error;
|
|
162
165
|
}
|
|
163
166
|
const retained = options.cache !== false
|
|
164
167
|
? store?.getResource<T>(CACHE_KIND, key, { ...cacheOptions, allowExpired: true }) : null;
|
|
165
|
-
// A caller may retain old research only when it can
|
|
166
|
-
if (!retained) throw error;
|
|
167
|
-
|
|
168
|
-
options.onSavedCopy(retained.fetchedAt);
|
|
169
|
-
return retained.value;
|
|
170
|
-
}
|
|
171
|
-
if (!options.onWarning) throw error;
|
|
172
|
-
const retrieved = new Date(retained.fetchedAt);
|
|
173
|
-
const age = Number.isFinite(retrieved.getTime()) ? `retrieved ${retrieved.toISOString()}` : "with unavailable retrieval time";
|
|
174
|
-
const reason = (error instanceof Error ? error.message : String(error)).trim() || "Request failed";
|
|
175
|
-
options.onWarning(`${key}: refresh failed: ${reason}. Retained data ${age}.`);
|
|
168
|
+
// A caller may retain old research only when it can say when it was saved.
|
|
169
|
+
if (!retained || !options.onSavedCopy || !Number.isFinite(retained.fetchedAt)) throw error;
|
|
170
|
+
options.onSavedCopy(retained.fetchedAt);
|
|
176
171
|
return retained.value;
|
|
177
172
|
} finally {
|
|
178
173
|
if (activeRequests.get(key) === slot.promise) activeRequests.delete(key);
|
|
@@ -106,6 +106,8 @@ export interface BrowserLoadResult {
|
|
|
106
106
|
period?: string;
|
|
107
107
|
quarter?: string;
|
|
108
108
|
warning?: string;
|
|
109
|
+
/** A refresh failed and the oldest copy that answered instead was saved at this time (ms). */
|
|
110
|
+
savedAt?: number;
|
|
109
111
|
hasMore?: boolean;
|
|
110
112
|
nextOffset?: number;
|
|
111
113
|
reportsLoaded?: Promise<void>;
|
|
@@ -118,11 +120,31 @@ export async function loadBrowserRows(
|
|
|
118
120
|
options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & { offset?: number; limit?: number } = {},
|
|
119
121
|
): Promise<BrowserLoadResult> {
|
|
120
122
|
const warnings: string[] = [];
|
|
121
|
-
const result
|
|
123
|
+
const page: { result?: BrowserLoadResult; savedAt?: number } = {};
|
|
124
|
+
const onSavedCopy = (savedAt: number) => {
|
|
125
|
+
page.savedAt = Math.min(page.savedAt ?? savedAt, savedAt);
|
|
126
|
+
// The rows' reports answer after the page does, and their copies date it too.
|
|
127
|
+
if (page.result) page.result.savedAt = page.savedAt;
|
|
128
|
+
options.onSavedCopy?.(savedAt);
|
|
129
|
+
};
|
|
130
|
+
const result = await loadBrowserPage(tab, query, signal, { ...options, onSavedCopy, onWarning: warning => warnings.push(warning) });
|
|
131
|
+
page.result = result;
|
|
132
|
+
if (page.savedAt != null) result.savedAt = page.savedAt;
|
|
122
133
|
if (warnings.length) result.warning = [...new Set([result.warning, ...warnings].filter(Boolean))].join(" ") || undefined;
|
|
123
134
|
return result;
|
|
124
135
|
}
|
|
125
136
|
|
|
137
|
+
/** The oldest saved copy a load answered from (`savedAt`), each also passed on to `onSavedCopy`. */
|
|
138
|
+
function savedCopies(onSavedCopy?: (savedAt: number) => void) {
|
|
139
|
+
const copies: { savedAt?: number; note: (savedAt: number) => void } = {
|
|
140
|
+
note: (savedAt) => {
|
|
141
|
+
copies.savedAt = Math.min(copies.savedAt ?? savedAt, savedAt);
|
|
142
|
+
onSavedCopy?.(savedAt);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
return copies;
|
|
146
|
+
}
|
|
147
|
+
|
|
126
148
|
async function loadBrowserPage(
|
|
127
149
|
tab: ThirteenFBrowserTab,
|
|
128
150
|
query: string,
|
|
@@ -440,7 +462,8 @@ export async function loadFundPerformance(
|
|
|
440
462
|
options: Pick<Forms13FReadOptions, "forceRefresh" | "onSavedCopy"> & { splitFactor?: SplitFactorLookup } = {},
|
|
441
463
|
): Promise<FundPerformanceResult> {
|
|
442
464
|
const warnings: string[] = [];
|
|
443
|
-
const
|
|
465
|
+
const saved = savedCopies(options.onSavedCopy);
|
|
466
|
+
const apiOptions = { forceRefresh: options.forceRefresh, onSavedCopy: saved.note, onWarning: (warning: string) => warnings.push(warning) };
|
|
444
467
|
const detail = await loadFundDetail(cik, fallbackName, signal, options);
|
|
445
468
|
const period = detail.latestForm?.periodOfReport;
|
|
446
469
|
const quarter = period ? periodToQuarter(period) : null;
|
|
@@ -482,7 +505,9 @@ export async function loadFundPerformance(
|
|
|
482
505
|
: filed?.skipped.length ? `No share position held through the quarter to ${period} that could be priced across it.`
|
|
483
506
|
: `No share position held through the quarter to ${period}.`;
|
|
484
507
|
const warning = [...new Set([...(detail.warnings ?? []), ...warnings])].join(" ") || undefined;
|
|
508
|
+
const savedAt = Math.min(detail.savedAt ?? Infinity, saved.savedAt ?? Infinity);
|
|
485
509
|
return {
|
|
510
|
+
...(Number.isFinite(savedAt) ? { savedAt } : {}),
|
|
486
511
|
rows: [row], ...(quarter ? { quarter } : {}), ...(period ? { period } : {}), hasMore: false, basis, ranking,
|
|
487
512
|
...(filed?.splits.length ? { splits: filed.splits } : {}),
|
|
488
513
|
...(filed?.skipped.length ? { skipped: filed.skipped } : {}),
|
|
@@ -500,7 +525,8 @@ export async function loadFundDetail(
|
|
|
500
525
|
): Promise<FundDetailData> {
|
|
501
526
|
const now = new Date();
|
|
502
527
|
const warnings: string[] = [];
|
|
503
|
-
const
|
|
528
|
+
const saved = savedCopies(options.onSavedCopy);
|
|
529
|
+
const apiOptions = { forceRefresh: options.forceRefresh, onSavedCopy: saved.note, onWarning: (warning: string) => warnings.push(warning) };
|
|
504
530
|
|
|
505
531
|
const forms = await listThirteenFForms(
|
|
506
532
|
cik,
|
|
@@ -554,6 +580,7 @@ export async function loadFundDetail(
|
|
|
554
580
|
previousReport,
|
|
555
581
|
warnings,
|
|
556
582
|
filingGap,
|
|
583
|
+
...(saved.savedAt != null ? { savedAt: saved.savedAt } : {}),
|
|
557
584
|
};
|
|
558
585
|
}
|
|
559
586
|
|
|
@@ -562,12 +589,13 @@ export async function loadFilingPositions(
|
|
|
562
589
|
accessionNumber: string,
|
|
563
590
|
signal?: AbortSignal,
|
|
564
591
|
options: { forceRefresh?: boolean; offset?: number; limit?: number } = {},
|
|
565
|
-
): Promise<{ rows: ThirteenFHoldingRecord[]; hasMore: boolean; warnings: string[] }> {
|
|
592
|
+
): Promise<{ rows: ThirteenFHoldingRecord[]; hasMore: boolean; warnings: string[]; savedAt?: number }> {
|
|
566
593
|
const warnings: string[] = [];
|
|
594
|
+
const saved = savedCopies();
|
|
567
595
|
const result = await listThirteenFFormHoldingsPage(cik, accessionNumber, signal, {
|
|
568
|
-
...options, onWarning: warning => warnings.push(warning),
|
|
596
|
+
...options, onSavedCopy: saved.note, onWarning: warning => warnings.push(warning),
|
|
569
597
|
});
|
|
570
|
-
return { ...result, warnings };
|
|
598
|
+
return { ...result, warnings, ...(saved.savedAt != null ? { savedAt: saved.savedAt } : {}) };
|
|
571
599
|
}
|
|
572
600
|
|
|
573
601
|
/** `2026Q2` for a quarter end such as 2026-06-30. */
|
|
@@ -10,6 +10,7 @@ import { usePluginPaneState, usePluginTickerActions } from "../../runtime";
|
|
|
10
10
|
import { useMineTickers } from "../shared/mine-tickers";
|
|
11
11
|
import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
|
|
12
12
|
import { handleRefreshKey } from "../../../components/data-table/table-pane";
|
|
13
|
+
import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
13
14
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
14
15
|
import { cachedFunds, cachedMembers, loadFunds, loadMembers } from "../members/client";
|
|
15
16
|
import { coveredFund } from "../members/model";
|
|
@@ -115,7 +116,10 @@ export function FundOverlapView({ data, focused, width }: { data: FundDetailData
|
|
|
115
116
|
const period = peer ? overlapPeriod(data, peer) : null;
|
|
116
117
|
const mismatch = peer && !period;
|
|
117
118
|
const listError = etf ? etfFile.error : error ?? paged.error?.message ?? paged.moreError?.message ?? null;
|
|
118
|
-
|
|
119
|
+
// Either side answered from a copy a failed refresh left says when it was saved.
|
|
120
|
+
const etfSavedAt = etf ? etfFile.data?.savedAt : null;
|
|
121
|
+
const savedInfo = useMemo(() => savedCopyFooterInfo([data.savedAt, peer?.savedAt, etfSavedAt]), [data.savedAt, peer?.savedAt, etfSavedAt]);
|
|
122
|
+
usePaneStatusFooter({ registrationId: "13f-overlap", loading: loading || loadingMore || paged.loading || (!!etf && etfFile.loading), error: listError, info: savedInfo, hints: open ? [{ id: "mine", key: "m", label: mineOnly ? "all tickers" : "mine", title: mineOnly ? "All Tickers" : "Mine Only", onPress: () => setMineOnly(value => !value) }] : [{ id: "search", key: "/", label: "search", onPress: focusSearch }] });
|
|
119
123
|
usePaneNoticeFooter({ registrationId: "13f-overlap-notice", focused, notices: [
|
|
120
124
|
...(peer?.warnings ?? []),
|
|
121
125
|
...(mismatch ? ["No reporting quarter is loaded for both funds; overlap is unavailable."] : []),
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
DataTableStackView,
|
|
4
4
|
DataTableView,
|
|
5
|
-
EmptyState, PaneStatusBody, QueryBar, StatGrid, usePagedRows, usePaneNoticeFooter, usePaneTabs, useQueryBarSearch, useTableLoadMore, type DataTableKeyEvent,
|
|
5
|
+
EmptyState, PaneStatusBody, QueryBar, StatGrid, usePagedRows, usePaneFooter, usePaneNoticeFooter, usePaneTabs, useQueryBarSearch, useTableLoadMore, type DataTableKeyEvent,
|
|
6
6
|
type DataTableRootKeyContext, type PageRequest, type PaneFooterSegment, type PaneHint, type StatItem
|
|
7
7
|
} from "../../../components";
|
|
8
8
|
import { useShortcut } from "../../../react/input";
|
|
@@ -26,7 +26,7 @@ import { usePaneStatusFooter, usePaneStatusLinkFooter } from "../../../component
|
|
|
26
26
|
import { corporateSplitFactor, loadBrowserRows, loadFilingPositions, loadFundDetail, loadSplitFactors } from "./data";
|
|
27
27
|
import { FundOverlapView } from "./overlap-pane";
|
|
28
28
|
import { ThirteenFCrowdingPane, ThirteenFTickerHoldingsView } from "./signals-pane";
|
|
29
|
-
import { PaneFooterScope } from "../../../components/layout/pane/footer";
|
|
29
|
+
import { PaneFooterScope, savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
30
30
|
import { handleRefreshKey } from "../../../components/data-table/table-pane";
|
|
31
31
|
import { useClaimResearchTabKeys } from "../ticker-detail/research-tab-keys";
|
|
32
32
|
import {
|
|
@@ -129,6 +129,9 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
|
|
|
129
129
|
const notes = [...(pagesSettled ? browser.pages.map((page) => page.warning) : []), browser.moreError?.message];
|
|
130
130
|
return [...new Set(notes.filter(Boolean))].join(" ") || null;
|
|
131
131
|
}, [browser.moreError, browser.pages, pagesSettled, reportPaint]);
|
|
132
|
+
// A page a failed refresh answered from a saved copy says when it was saved, as every pane does.
|
|
133
|
+
const savedInfo = useMemo(() => savedCopyFooterInfo(pagesSettled ? browser.pages.map((page) => page.savedAt) : []),
|
|
134
|
+
[browser.pages, pagesSettled, reportPaint]);
|
|
132
135
|
const [detailSeed, setDetailSeed] = useState<FundSeed | null>(() => (
|
|
133
136
|
initialCik ? { cik: String(initialCik), name: normalizedQuery || String(initialCik) } : null
|
|
134
137
|
));
|
|
@@ -166,7 +169,9 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
|
|
|
166
169
|
}, { allowEditable: true });
|
|
167
170
|
|
|
168
171
|
const browserSort = useMemo(() => browserSortFor(sortPreference, browserMode), [sortPreference, browserMode]);
|
|
169
|
-
|
|
172
|
+
// The rows' reports fill the rows in place after their page answers (from the network or a saved copy);
|
|
173
|
+
// a row the table drew before then is drawn again only as a new object.
|
|
174
|
+
const sortedRows = useMemo(() => sortBrowserRows(rows.map((row) => ({ ...row })), browserSort), [rows, browserSort, reportPaint]);
|
|
170
175
|
const columns = useMemo(() => {
|
|
171
176
|
if (browserMode !== "performance") return buildBrowserColumns(false);
|
|
172
177
|
const history = rows[0]?.priorReturns ?? [];
|
|
@@ -202,7 +207,8 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
|
|
|
202
207
|
const browserStatusInfo = useMemo<PaneFooterSegment[]>(() => [
|
|
203
208
|
...(loadingMore ? [{ id: "loading-more", parts: [{ text: "loading more", tone: "muted" as const }] }] : []),
|
|
204
209
|
...(warning ? [{ id: "warning", parts: [{ text: warning, tone: "warning" as const }] }] : []),
|
|
205
|
-
|
|
210
|
+
...savedInfo,
|
|
211
|
+
], [loadingMore, warning, savedInfo]);
|
|
206
212
|
const searchHints = useMemo(() => [{ id: "search", key: "/", label: "search", onPress: focusSearch }], [focusSearch]);
|
|
207
213
|
usePaneStatusFooter({
|
|
208
214
|
registrationId: THIRTEENF_PANE_ID,
|
|
@@ -544,6 +550,9 @@ export function FundDetailView({
|
|
|
544
550
|
const detailHints = useMemo<PaneHint[]>(() => hasHoldingFiling
|
|
545
551
|
? [{ id: "filing", key: "f", label: "iling", title: "Open Filing", onPress: openSelectedFilingInPane }]
|
|
546
552
|
: [], [hasHoldingFiling, openSelectedFilingInPane]);
|
|
553
|
+
// An open filing and the overlap tab say when their own saved copies are from.
|
|
554
|
+
const detailSavedInfo = useMemo(() => savedCopyFooterInfo(openFiling || activeTab === "overlap" ? [] : [data?.savedAt]),
|
|
555
|
+
[activeTab, data?.savedAt, openFiling]);
|
|
547
556
|
usePaneStatusLinkFooter({
|
|
548
557
|
registrationId: "thirteenf-detail",
|
|
549
558
|
focused,
|
|
@@ -551,6 +560,7 @@ export function FundDetailView({
|
|
|
551
560
|
source: openFiling ? "SEC 13F" : null,
|
|
552
561
|
loading: activeTab !== "overlap" && status === "loading",
|
|
553
562
|
error: activeTab === "overlap" ? null : error,
|
|
563
|
+
info: detailSavedInfo,
|
|
554
564
|
showOpenHint: true,
|
|
555
565
|
hints: detailHints,
|
|
556
566
|
});
|
|
@@ -590,7 +600,7 @@ export function FundDetailView({
|
|
|
590
600
|
onBack={closeOpenFiling}
|
|
591
601
|
detailTitle={openFiling ? `${openFiling.periodOfReport} filing` : "13F filing"}
|
|
592
602
|
detailContent={openFiling ? (
|
|
593
|
-
<FilingDetailView focused={focused} filing={openFiling} width={width} sourceWarnings={data?.warnings ?? []} />
|
|
603
|
+
<FilingDetailView focused={focused} filing={openFiling} width={width} sourceWarnings={data?.warnings ?? []} sourceSavedAt={data?.savedAt} />
|
|
594
604
|
) : (
|
|
595
605
|
<Box flexGrow={1} />
|
|
596
606
|
)}
|
|
@@ -659,11 +669,14 @@ function FilingDetailView({
|
|
|
659
669
|
filing,
|
|
660
670
|
width,
|
|
661
671
|
sourceWarnings,
|
|
672
|
+
sourceSavedAt,
|
|
662
673
|
}: {
|
|
663
674
|
focused: boolean;
|
|
664
675
|
filing: FundTimelineRow;
|
|
665
676
|
width: number;
|
|
666
677
|
sourceWarnings: string[];
|
|
678
|
+
/** When the fund's saved copy was saved, when a failed refresh left it on screen. */
|
|
679
|
+
sourceSavedAt?: number;
|
|
667
680
|
}) {
|
|
668
681
|
const { pinTicker } = usePluginTickerActions();
|
|
669
682
|
const [sortPreference, setSortPreference] = usePluginPaneState<FundSortPreference<FilingPositionColumnId>>(
|
|
@@ -711,6 +724,9 @@ function FilingDetailView({
|
|
|
711
724
|
);
|
|
712
725
|
|
|
713
726
|
usePaneNoticeFooter({ registrationId: "thirteenf-filing-notices", notices: [...new Set([...sourceWarnings, ...warnings])], focused });
|
|
727
|
+
const savedInfo = useMemo(() => savedCopyFooterInfo([sourceSavedAt, ...(pagesSettled ? positions.pages.map((page) => page.savedAt) : [])]),
|
|
728
|
+
[pagesSettled, positions.pages, sourceSavedAt]);
|
|
729
|
+
usePaneFooter("thirteenf-filing-saved", () => ({ info: savedInfo }), [savedInfo]);
|
|
714
730
|
|
|
715
731
|
// An untyped amendment may replace or add to the report, so only it is a caveat.
|
|
716
732
|
const kind = amendmentKind(filing);
|
|
@@ -99,6 +99,8 @@ export interface FundDetailData {
|
|
|
99
99
|
warnings?: string[];
|
|
100
100
|
/** Set when the latest report predates the newest quarter past its deadline. */
|
|
101
101
|
filingGap?: ThirteenFFilingGap | null;
|
|
102
|
+
/** A refresh failed and the oldest copy that answered instead was saved at this time (ms). */
|
|
103
|
+
savedAt?: number;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
/** A manager a 13F-NT names as reporting its holdings. */
|
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
barPriceDecimals, formatBarPrice, historyAllStart, historyBarsAfterHole, historyChangeBaseNote, historyMissingPeriodsNote, quotedPriceDivisor,
|
|
25
25
|
} from "../../../../cli/history-rows";
|
|
26
26
|
import {
|
|
27
|
-
MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, priceHistoryIntegrityNotice,
|
|
27
|
+
MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, pricePointWickFlags, priceHistoryIntegrityNotice,
|
|
28
|
+
priceHistoryWickNotice,
|
|
28
29
|
} from "../../../../utils/price-history-integrity";
|
|
29
30
|
import {
|
|
30
31
|
useAssetData,
|
|
@@ -257,6 +258,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
|
|
|
257
258
|
[points, venue],
|
|
258
259
|
);
|
|
259
260
|
const integrityNotice = priceHistoryIntegrityNotice(rows.filter((row) => row.point.integrity).length);
|
|
261
|
+
const wickNotice = useMemo(() => priceHistoryWickNotice(points.filter((point) => pricePointWickFlags(point).length > 0).length), [points]);
|
|
260
262
|
const volumeMissing = useMemo(() => missingVolumeCount(rows.map((row) => ({
|
|
261
263
|
...row.point, date: row.time, partial: row.partial, filled: row.filled,
|
|
262
264
|
}))), [rows]);
|
|
@@ -356,7 +358,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
|
|
|
356
358
|
|
|
357
359
|
usePaneNoticeFooter({
|
|
358
360
|
registrationId: "historical-prices:integrity",
|
|
359
|
-
notices: [integrityNotice, volumeNotice, ...gapNotices].filter((notice): notice is string => !!notice),
|
|
361
|
+
notices: [integrityNotice, wickNotice, volumeNotice, ...gapNotices].filter((notice): notice is string => !!notice),
|
|
360
362
|
focused,
|
|
361
363
|
title: "Price history data",
|
|
362
364
|
});
|
|
@@ -404,6 +406,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
|
|
|
404
406
|
: loading ? (rows.length ? "Refreshing" : "Loading") : rows.length ? "Available" : "Empty"],
|
|
405
407
|
...(allStart ? [["Source starts", allStart]] : []),
|
|
406
408
|
...(integrityNotice ? [["Warning", integrityNotice]] : []),
|
|
409
|
+
...(wickNotice ? [["Warning", wickNotice]] : []),
|
|
407
410
|
...gapNotices.map((notice) => ["Gap note", notice]),
|
|
408
411
|
...(volumeNotice ? [["Volume note", volumeNotice]] : []),
|
|
409
412
|
...(error ? [["Error", error]] : []),
|
|
@@ -505,13 +505,46 @@ function coversLatestYear(ttm: FinancialTableStatement, latestYear: FinancialSta
|
|
|
505
505
|
return days <= 7;
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
/** Counts of the shares a company has issued or has outstanding, which are never zero; treasury shares can be. */
|
|
509
|
+
const OUTSTANDING_COUNT_FIELDS = ["basicShares", "dilutedShares", "shareIssued", "ordinarySharesNumber"] as const;
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Share counts that cannot be the company's: zero, or under a thousandth of its
|
|
513
|
+
* typical count (IAG's 4,642 for Q3 2025, millions read as shares, which shows
|
|
514
|
+
* as 0.00 bn and a fall of 100%). They are missing, not a value to grow from.
|
|
515
|
+
*/
|
|
516
|
+
function withoutImpossibleShareCounts(
|
|
517
|
+
annualStatements: FinancialStatement[],
|
|
518
|
+
quarterlyStatements: FinancialStatement[],
|
|
519
|
+
): { annualStatements: FinancialStatement[]; quarterlyStatements: FinancialStatement[] } {
|
|
520
|
+
const all = [...annualStatements, ...quarterlyStatements];
|
|
521
|
+
const floors = new Map<string, number>();
|
|
522
|
+
for (const field of OUTSTANDING_COUNT_FIELDS) {
|
|
523
|
+
const counts = all.map((row) => row[field]).filter((value): value is number => typeof value === "number" && value > 0).sort((a, b) => a - b);
|
|
524
|
+
floors.set(field, counts.length ? counts[Math.floor(counts.length / 2)]! / 1000 : 0);
|
|
525
|
+
}
|
|
526
|
+
const impossible = (row: FinancialStatement, field: typeof OUTSTANDING_COUNT_FIELDS[number]) => {
|
|
527
|
+
const value = row[field];
|
|
528
|
+
return typeof value === "number" && (value <= 0 || value < floors.get(field)!);
|
|
529
|
+
};
|
|
530
|
+
const clean = (rows: FinancialStatement[]) => rows.map((row) => {
|
|
531
|
+
const drop = OUTSTANDING_COUNT_FIELDS.filter((field) => impossible(row, field));
|
|
532
|
+
if (!drop.length) return row;
|
|
533
|
+
const next = { ...row };
|
|
534
|
+
for (const field of drop) delete next[field];
|
|
535
|
+
return next;
|
|
536
|
+
});
|
|
537
|
+
return { annualStatements: clean(annualStatements), quarterlyStatements: clean(quarterlyStatements) };
|
|
538
|
+
}
|
|
539
|
+
|
|
508
540
|
export function selectFinancialStatements(
|
|
509
541
|
period: FinancialPeriod,
|
|
510
542
|
statement: string,
|
|
511
|
-
|
|
512
|
-
|
|
543
|
+
servedAnnual: FinancialStatement[],
|
|
544
|
+
servedQuarterly: FinancialStatement[],
|
|
513
545
|
limit = period === "annual" ? 5 : 6,
|
|
514
546
|
) {
|
|
547
|
+
const { annualStatements, quarterlyStatements } = withoutImpossibleShareCounts(servedAnnual, servedQuarterly);
|
|
515
548
|
// A period with nothing on this statement (a fiscal year-end balance point on
|
|
516
549
|
// the income statement) would render as an all-dash column.
|
|
517
550
|
const tabRows = FINANCIAL_SUB_TABS.find((tab) => tab.key === statement)?.rows;
|