gloomberb 0.17.2 → 0.17.4
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/chat.ts +28 -0
- package/src/api-client/errors.ts +11 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +22 -9
- package/src/api-client/normalizers.ts +21 -0
- package/src/api-client/socket.ts +27 -0
- package/src/api-client/types.ts +3 -1
- package/src/app/runtime/startup.ts +4 -0
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/index.ts +1 -1
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/metric-treemap/heat-surface.tsx +76 -14
- package/src/components/metric-treemap/heat-tiles.ts +5 -3
- package/src/components/metric-treemap/index.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/button.tsx +24 -9
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/status.tsx +148 -5
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/layout-marketplace/gallery-desktop.tsx +2 -2
- package/src/market-data/futures-board.ts +44 -1
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/alerts/cli.ts +72 -11
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/awards/headless.ts +5 -1
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/catalysts/headless.ts +5 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/controller/connections.ts +18 -0
- package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +49 -21
- package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
- package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/model.ts +20 -0
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +19 -2
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +33 -6
- package/src/plugins/builtin/futures/model.ts +69 -5
- package/src/plugins/builtin/futures/series.ts +3 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/jobs/pane.tsx +12 -9
- package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/news/headless.ts +4 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +4 -12
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/build-assets.ts +17 -4
- package/src/renderers/dom/desktop/controls.tsx +2 -0
- package/src/renderers/dom/styles.css +6 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
- package/src/sources/gloomberb-cloud/normalizers.ts +13 -4
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +24 -5
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/ui/resize-settle.ts +128 -0
- package/src/utils/calendar-date.ts +56 -0
- package/src/utils/decimal-comma.ts +31 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/futures-generic.ts +25 -4
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
- package/src/utils/plugin-proxy-hosts.json +5 -0
- package/src/utils/utc-time.ts +14 -3
package/src/sync/types.ts
CHANGED
|
@@ -19,8 +19,29 @@ export interface SyncSnapshot {
|
|
|
19
19
|
contributors: Record<string, SyncContributorPayload>;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* What this device remembers about its own copy of one contributor's data
|
|
24
|
+
* between syncs, kept across launches: when it last edited it, or which cloud
|
|
25
|
+
* copy it took. Never sent anywhere.
|
|
26
|
+
*/
|
|
27
|
+
interface SyncContributorMemo {
|
|
28
|
+
get(): unknown;
|
|
29
|
+
set(value: unknown): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
interface SyncCollectContext {
|
|
23
33
|
state: AppState;
|
|
34
|
+
/** The payload this device last pushed for this contributor, or null when it never synced. */
|
|
35
|
+
baselinePayload?: unknown;
|
|
36
|
+
memo?: SyncContributorMemo;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SyncObserveContext {
|
|
40
|
+
state: AppState;
|
|
41
|
+
baselinePayload: unknown;
|
|
42
|
+
memo: SyncContributorMemo;
|
|
43
|
+
/** True for the first look after the app started: a change seen then was made while it was closed. */
|
|
44
|
+
launch: boolean;
|
|
24
45
|
}
|
|
25
46
|
|
|
26
47
|
interface SyncApplyContext {
|
|
@@ -37,6 +58,7 @@ interface SyncApplyContext {
|
|
|
37
58
|
isCurrent: () => boolean;
|
|
38
59
|
dispatch: Dispatch<AppAction>;
|
|
39
60
|
tickerRepository: AppTickerRepositoryPort;
|
|
61
|
+
memo?: SyncContributorMemo;
|
|
40
62
|
}
|
|
41
63
|
|
|
42
64
|
export interface SyncContributor {
|
|
@@ -44,6 +66,12 @@ export interface SyncContributor {
|
|
|
44
66
|
schemaVersion: number;
|
|
45
67
|
collect(context: SyncCollectContext): unknown | Promise<unknown>;
|
|
46
68
|
apply?(payload: unknown, context: SyncApplyContext): void | Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Called before every sync attempt and shortly after local changes, online
|
|
71
|
+
* or not, so an edit can be dated when it happens rather than when the
|
|
72
|
+
* network is next reachable.
|
|
73
|
+
*/
|
|
74
|
+
observe?(context: SyncObserveContext): void;
|
|
47
75
|
}
|
|
48
76
|
|
|
49
77
|
export interface SyncSnapshotResponse {
|
|
@@ -63,9 +91,22 @@ export interface SyncTransport {
|
|
|
63
91
|
id: string;
|
|
64
92
|
isAvailable(): boolean;
|
|
65
93
|
pullSnapshot(): Promise<SyncSnapshotResponse>;
|
|
94
|
+
/**
|
|
95
|
+
* `baseRevision` is the stored revision the snapshot was merged with (0 when
|
|
96
|
+
* there was none). A transport that can refuse a write made against an older
|
|
97
|
+
* revision throws `SyncRevisionConflictError`, and the controller pulls again.
|
|
98
|
+
*/
|
|
66
99
|
pushSnapshot(snapshot: SyncSnapshot, options?: { baseRevision?: number | null }): Promise<SyncPushResult>;
|
|
67
100
|
}
|
|
68
101
|
|
|
102
|
+
/** Another device wrote since this one pulled; its push would have replaced that write. */
|
|
103
|
+
export class SyncRevisionConflictError extends Error {
|
|
104
|
+
constructor(message = "The synced copy changed since it was pulled.") {
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = "SyncRevisionConflictError";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
69
110
|
export interface SyncSettings {
|
|
70
111
|
syncEnabled: boolean;
|
|
71
112
|
weeklyRoundupEnabled: boolean;
|
|
@@ -79,6 +120,9 @@ export interface SyncSettings {
|
|
|
79
120
|
export interface SyncBaselineStore {
|
|
80
121
|
load(): Record<string, unknown> | null;
|
|
81
122
|
save(payloads: Record<string, unknown>): void;
|
|
123
|
+
/** Contributor memos by contributor id; see `SyncContributorMemo`. */
|
|
124
|
+
loadMemos?(): Record<string, unknown> | null;
|
|
125
|
+
saveMemos?(memos: Record<string, unknown>): void;
|
|
82
126
|
}
|
|
83
127
|
|
|
84
128
|
export interface RegisteredSyncContributor {
|
|
@@ -83,7 +83,7 @@ import { getPricePointTimestamp, isPriceHistoryStaleForCurrentWindow } from "../
|
|
|
83
83
|
import { timeZoneLabel, zonedDateKey, zonedWallClockMs } from "../utils/zoned-date-time";
|
|
84
84
|
import { formatShortDate } from "../utils/datetime-format";
|
|
85
85
|
import { latestSessionStart, latestTradingSessionOpen, listingTimeZone, priorSessionClose } from "../market-data/market/trading-sessions";
|
|
86
|
-
import { futuresGenericCaption, futuresGenericListing,
|
|
86
|
+
import { futuresGenericCaption, futuresGenericListing, futuresListingPriceBasis } from "../utils/futures-generic";
|
|
87
87
|
import { isOhlcSeriesStyle } from "./spec";
|
|
88
88
|
import { SERIES_COLORS } from "../theme/series-colors";
|
|
89
89
|
import type {
|
|
@@ -1166,8 +1166,10 @@ function baseSecuritySeries(
|
|
|
1166
1166
|
: field.unit;
|
|
1167
1167
|
const generic = field.unitGroup === "price"
|
|
1168
1168
|
? futuresGenericListing(spec.source.instrument.symbol, spec.source.instrument.exchange) : null;
|
|
1169
|
-
// A generic future on an index, VIX or Treasuries reads in points or 32nds,
|
|
1170
|
-
|
|
1169
|
+
// A generic future on an index, VIX or Treasuries reads in points or 32nds, and an index
|
|
1170
|
+
// future's continuous symbol or contract in points, not dollars, as its quote does.
|
|
1171
|
+
const genericBasis = field.unitGroup === "price" && isMarketFieldId(field.id)
|
|
1172
|
+
? futuresListingPriceBasis(spec.source.instrument.symbol, spec.source.instrument.exchange) : null;
|
|
1171
1173
|
const genericUnit = genericBasis === "points" ? "points" : genericBasis === "thirty-seconds" ? "32nds" : null;
|
|
1172
1174
|
const unit = field.id === "market.volume" ? volumeUnit ?? ""
|
|
1173
1175
|
: genericUnit ?? (unitTemplate.startsWith("currency") && currency ? unitTemplate.replace("currency", currency) : unitTemplate);
|
|
@@ -1235,6 +1237,21 @@ function baseSecuritySeries(
|
|
|
1235
1237
|
};
|
|
1236
1238
|
}
|
|
1237
1239
|
|
|
1240
|
+
const FRED_CATALOG_UNITS: Readonly<Record<string, string>> = {
|
|
1241
|
+
"percent": "%",
|
|
1242
|
+
"millions of u.s. dollars": "million USD",
|
|
1243
|
+
"millions of dollars": "million USD",
|
|
1244
|
+
"billions of dollars": "billion USD",
|
|
1245
|
+
"billions of u.s. dollars": "billion USD",
|
|
1246
|
+
"billions of us dollars": "billion USD",
|
|
1247
|
+
"dollars per barrel": "USD/barrel",
|
|
1248
|
+
"thousands of persons": "thousand people",
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
function fredCatalogUnit(units: string): string {
|
|
1252
|
+
return FRED_CATALOG_UNITS[units.toLowerCase()] ?? units;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1238
1255
|
function baseEconomicSeries(
|
|
1239
1256
|
spec: ChartSeriesSpec,
|
|
1240
1257
|
loaded: FredSeriesLoadResult,
|
|
@@ -1243,13 +1260,15 @@ function baseEconomicSeries(
|
|
|
1243
1260
|
if (spec.source.kind !== "economic") return null;
|
|
1244
1261
|
const { data } = loaded;
|
|
1245
1262
|
const units = data.info?.units?.trim() || "value";
|
|
1263
|
+
const catalogUnit = fredCatalogUnit(units);
|
|
1264
|
+
// "Percent Change from Year Ago" and "Percent per Annum" are percent too.
|
|
1246
1265
|
const isPercent = units.toLowerCase().includes("percent");
|
|
1247
1266
|
return {
|
|
1248
1267
|
id: spec.id,
|
|
1249
1268
|
label: spec.label?.trim() || data.info?.title?.trim() || spec.source.seriesId,
|
|
1250
1269
|
color: spec.color ?? SERIES_COLORS[index % SERIES_COLORS.length]!,
|
|
1251
|
-
unit: isPercent ? "%" :
|
|
1252
|
-
unitGroup: isPercent ? "percent" : `economic:${
|
|
1270
|
+
unit: isPercent ? "%" : catalogUnit,
|
|
1271
|
+
unitGroup: isPercent ? "percent" : `economic:${catalogUnit.toLowerCase()}`,
|
|
1253
1272
|
nativeFrequency: "auto",
|
|
1254
1273
|
timestampMode: "period-end",
|
|
1255
1274
|
dataShape: "scalar",
|
|
@@ -180,6 +180,14 @@ export interface QuoteBatchOptions {
|
|
|
180
180
|
* provider with no cache answers every target with no quote.
|
|
181
181
|
*/
|
|
182
182
|
cacheOnly?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Ask the sources even for a target whose cached quote is still fresh; that
|
|
185
|
+
* copy answers only when they cannot. A command that prints quotes once
|
|
186
|
+
* shows the service's quote of the moment, so every form of one listing
|
|
187
|
+
* (BRK.B saved on NYSE, BRK-B, BRK.B:NYSE) prints the same quote, never
|
|
188
|
+
* each form's own copy from whenever it was last asked for.
|
|
189
|
+
*/
|
|
190
|
+
revalidate?: boolean;
|
|
183
191
|
/** The caller stopped waiting; a transport that can abandon the request does. */
|
|
184
192
|
signal?: AbortSignal;
|
|
185
193
|
}
|
package/src/types/financials.ts
CHANGED
|
@@ -5,6 +5,20 @@ export type SessionConfidence = "explicit" | "derived" | "unknown";
|
|
|
5
5
|
export type QuoteDataSource = "live" | "delayed" | "snapshot";
|
|
6
6
|
export type QuoteDelivery = "stream" | "poll";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The regular sessions an index quote carries, dated by its source: the
|
|
10
|
+
* calendar its publisher's name does not give (SNP, DJI, NASDAQ GIDS). Times
|
|
11
|
+
* are ms.
|
|
12
|
+
*/
|
|
13
|
+
export interface QuoteIndexSession {
|
|
14
|
+
/** The zone the sessions' dates are read in. */
|
|
15
|
+
timeZone: string;
|
|
16
|
+
/** The regular session the quote's day follows: today's, or the next one while it has not opened. */
|
|
17
|
+
current: { start: number; end: number };
|
|
18
|
+
/** The latest regular sessions, oldest first. */
|
|
19
|
+
regular: Array<{ start: number; end: number }>;
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
export interface QuoteFieldProvenance {
|
|
9
23
|
providerId: string;
|
|
10
24
|
dataSource?: QuoteDataSource;
|
|
@@ -100,6 +114,8 @@ export interface Quote extends DepositaryReceiptFacts {
|
|
|
100
114
|
routingExchangeFullName?: string;
|
|
101
115
|
marketState?: MarketState;
|
|
102
116
|
sessionConfidence?: SessionConfidence;
|
|
117
|
+
/** An index's own regular sessions, for the open its pre-market waits for and the close its last print was. */
|
|
118
|
+
indexSession?: QuoteIndexSession;
|
|
103
119
|
preMarketPrice?: number;
|
|
104
120
|
preMarketChange?: number;
|
|
105
121
|
preMarketChangePercent?: number;
|
|
@@ -245,15 +261,17 @@ export interface Fundamentals extends DepositaryReceiptFacts {
|
|
|
245
261
|
return3Y?: number;
|
|
246
262
|
lastQuarterGrowth?: number;
|
|
247
263
|
sharesOutstanding?: number;
|
|
248
|
-
/** Free float: shares outstanding less insider and strategic holdings
|
|
264
|
+
/** Free float: shares outstanding less insider and strategic holdings, never above `sharesOutstanding`. */
|
|
249
265
|
floatShares?: number;
|
|
250
266
|
beta?: number;
|
|
251
|
-
/** Shares
|
|
267
|
+
/** Shares short at the latest FINRA settlement, the one `SI` and `SIW` show; dated by `shortInterestDate`. */
|
|
252
268
|
sharesShort?: number;
|
|
253
|
-
/**
|
|
269
|
+
/** `sharesShort` over `floatShares`, as a fraction (0.0127 for 1.27%). */
|
|
254
270
|
shortPercentOfFloat?: number;
|
|
255
|
-
/** Days to cover: shares short over average daily volume. */
|
|
271
|
+
/** Days to cover as FINRA states it for the settlement: shares short over the period's average daily volume. */
|
|
256
272
|
shortRatio?: number;
|
|
273
|
+
/** The settlement date (YYYY-MM-DD) of `sharesShort`, `shortPercentOfFloat` and `shortRatio`. */
|
|
274
|
+
shortInterestDate?: string;
|
|
257
275
|
/** Fractions of shares outstanding. */
|
|
258
276
|
insiderPercentHeld?: number;
|
|
259
277
|
institutionPercentHeld?: number;
|
|
@@ -366,6 +384,20 @@ export interface DividendAction {
|
|
|
366
384
|
amount: number;
|
|
367
385
|
}
|
|
368
386
|
|
|
387
|
+
/**
|
|
388
|
+
* A distribution the issuer has declared and not yet paid, as the source
|
|
389
|
+
* states it. Kept apart from the paid `dividends`: no history, yield or growth
|
|
390
|
+
* figure takes it.
|
|
391
|
+
*/
|
|
392
|
+
export interface DeclaredDividendAction {
|
|
393
|
+
exDate: string;
|
|
394
|
+
/** Per share, in the listing's dividend currency (`currency`). */
|
|
395
|
+
amount: number;
|
|
396
|
+
declarationDate?: string;
|
|
397
|
+
recordDate?: string;
|
|
398
|
+
payDate?: string;
|
|
399
|
+
}
|
|
400
|
+
|
|
369
401
|
export interface SplitAction {
|
|
370
402
|
date: string;
|
|
371
403
|
description?: string;
|
|
@@ -417,6 +449,8 @@ export interface CorporateActionsData {
|
|
|
417
449
|
micCode?: string;
|
|
418
450
|
exchangeTimezone?: string;
|
|
419
451
|
dividends: DividendAction[];
|
|
452
|
+
/** Declared and unpaid, for the listings Gloom Cloud has them for; absent elsewhere and from older servers. */
|
|
453
|
+
declaredDividends?: DeclaredDividendAction[];
|
|
420
454
|
splits: SplitAction[];
|
|
421
455
|
earnings: EarningsAction[];
|
|
422
456
|
}
|
|
@@ -757,6 +791,8 @@ export interface PricePoint {
|
|
|
757
791
|
partial?: boolean;
|
|
758
792
|
/** The bar's prices were rebuilt from shorter bars where the source left it empty or too coarse. */
|
|
759
793
|
filled?: boolean;
|
|
794
|
+
/** A future's session that has closed but whose settlement is not confirmed yet: its close is the last trade. */
|
|
795
|
+
unsettled?: boolean;
|
|
760
796
|
}
|
|
761
797
|
|
|
762
798
|
export interface StatementHistoryAttempt {
|
package/src/types/headless.ts
CHANGED
|
@@ -257,6 +257,12 @@ export interface HeadlessPaneFreshness {
|
|
|
257
257
|
sources?: Array<{ source: string; asOf: string | number | Date }>;
|
|
258
258
|
/** Say how old the oldest observation is (`oldest 4 min`): rows polled apart from one source. */
|
|
259
259
|
oldestAge?: boolean;
|
|
260
|
+
/**
|
|
261
|
+
* Rows quoted on several markets (a US line beside Xetra and Paris ones)
|
|
262
|
+
* date each market's session in the status line, `US, XETRA and EPA Tue 13
|
|
263
|
+
* Oct 2026 close`, instead of only the newest row's.
|
|
264
|
+
*/
|
|
265
|
+
datesEachVenue?: boolean;
|
|
260
266
|
/**
|
|
261
267
|
* Exports also cite the oldest observation whenever the rows are dated
|
|
262
268
|
* apart, not only when it is a day older: a portfolio valued at one
|
|
@@ -265,6 +271,12 @@ export interface HeadlessPaneFreshness {
|
|
|
265
271
|
citeOldest?: boolean;
|
|
266
272
|
/** What the figures are, said after the status: `funding = current estimate, pays 08:00 UTC`. */
|
|
267
273
|
note?: string;
|
|
274
|
+
/**
|
|
275
|
+
* When the figures reach readers and when the next ones are due, said right
|
|
276
|
+
* after the as-of date: `published Fri 9 Oct, next settlement Thu 15 Oct,
|
|
277
|
+
* published Mon 26 Oct` for a settlement a regulator publishes days later.
|
|
278
|
+
*/
|
|
279
|
+
schedule?: string;
|
|
268
280
|
}
|
|
269
281
|
|
|
270
282
|
interface HeadlessPaneResultBase {
|
|
@@ -295,6 +307,12 @@ interface HeadlessPaneResultBase {
|
|
|
295
307
|
|
|
296
308
|
export interface HeadlessRowsResult extends HeadlessPaneResultBase {
|
|
297
309
|
columns?: HeadlessPaneColumn[];
|
|
310
|
+
/**
|
|
311
|
+
* The columns the text report prints in place of `columns`, when the
|
|
312
|
+
* exports carry more than a terminal table should (each cell's source and
|
|
313
|
+
* as-of beside it). CSV, NDJSON and JSON keep `columns`.
|
|
314
|
+
*/
|
|
315
|
+
textColumns?: HeadlessPaneColumn[];
|
|
298
316
|
rows: HeadlessPaneRow[];
|
|
299
317
|
/** What the report says when `rows` is empty, in place of "No data.": why there are none. */
|
|
300
318
|
empty?: string;
|
|
@@ -401,7 +419,22 @@ export interface HeadlessPaneDefinition<Shape extends HeadlessPaneShape = Headle
|
|
|
401
419
|
* points at GIP's 1-minute bars). `key` is the listing as read, RMS:ASX.
|
|
402
420
|
*/
|
|
403
421
|
usListingsOnlyHint?(key: string): string;
|
|
422
|
+
/**
|
|
423
|
+
* `fn` exits 1 when the report has no name with data: no rows, or every row
|
|
424
|
+
* is one of `unavailableSymbols`. A list that gives data for some names
|
|
425
|
+
* exits 0 and names the others in its errors.
|
|
426
|
+
*/
|
|
427
|
+
failsWithoutData?: boolean;
|
|
404
428
|
options: HeadlessPaneOptionDef[];
|
|
429
|
+
/**
|
|
430
|
+
* Widens the argument before the function runs, so its report, its capture
|
|
431
|
+
* and its pane all read the same symbols: RV's `--peers` adds a symbol's
|
|
432
|
+
* competitors. Null runs the argument as typed.
|
|
433
|
+
*/
|
|
434
|
+
expandArgument?(
|
|
435
|
+
args: HeadlessPaneLoadArgs,
|
|
436
|
+
ctx: HeadlessPaneContext,
|
|
437
|
+
): Promise<HeadlessArgumentExpansion | null>;
|
|
405
438
|
/** What is true of every load: its source, and whether it is a feed. */
|
|
406
439
|
freshness?: HeadlessPaneFreshness;
|
|
407
440
|
columns?: HeadlessPaneColumn[];
|
|
@@ -426,3 +459,13 @@ export interface HeadlessPaneDefinition<Shape extends HeadlessPaneShape = Headle
|
|
|
426
459
|
}
|
|
427
460
|
|
|
428
461
|
export type HeadlessPaneResult = HeadlessPaneResultByShape[HeadlessPaneShape];
|
|
462
|
+
|
|
463
|
+
/** What a function runs with once it has widened its argument (`expandArgument`). */
|
|
464
|
+
export interface HeadlessArgumentExpansion {
|
|
465
|
+
/** The argument as it would be typed: `DUOL, COUR, CHGG`. */
|
|
466
|
+
argument: string;
|
|
467
|
+
/** Merged into the pane's settings, which the load reads as `ctx.settings` and the pane as its own. */
|
|
468
|
+
settings?: Record<string, unknown>;
|
|
469
|
+
/** The pane's title when the widened argument needs saying (`RV DUOL + auto peers by industry: Airlines`). */
|
|
470
|
+
title?: string;
|
|
471
|
+
}
|
package/src/types/plugin.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AppTickerRepositoryPort } from "../core/app-service-ports";
|
|
|
3
3
|
import type { HeadlessPaneDefinition, HeadlessPaneFreshness, HeadlessPaneOptionDef } from "./headless";
|
|
4
4
|
|
|
5
5
|
export type {
|
|
6
|
+
HeadlessArgumentExpansion,
|
|
6
7
|
HeadlessBundleResult,
|
|
7
8
|
HeadlessBundleSection,
|
|
8
9
|
HeadlessFreshnessStatus,
|
|
@@ -189,6 +190,12 @@ export interface PaneDef {
|
|
|
189
190
|
* the last ones. Leave it off for long or paged lists.
|
|
190
191
|
*/
|
|
191
192
|
shotFitsRows?: true;
|
|
193
|
+
/**
|
|
194
|
+
* A report of the pane grows to the whole text of the page its reader has
|
|
195
|
+
* open (a docs page), so it is not cut at the first screen. A list the reader
|
|
196
|
+
* opens from keeps its height.
|
|
197
|
+
*/
|
|
198
|
+
shotFitsReader?: true;
|
|
192
199
|
/** Add an Excel-compatible CSV action for the pane's single active DataTable. */
|
|
193
200
|
tableExport?: true;
|
|
194
201
|
settings?: PaneSettingsDef | ((context: PaneSettingsContext) => PaneSettingsDef | null);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { isLiveResizing, onLiveResizeEnd } from "./live-resize";
|
|
3
|
+
import { LivePaneFrameContext, type CellSize } from "./live-stretch";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* How long a box's size has to hold still before what is laid out in it
|
|
7
|
+
* settles at that size. A resize's content catches up every 100 ms
|
|
8
|
+
* (live-resize.ts), so a pane under the pointer stays resizing between its
|
|
9
|
+
* catch-ups.
|
|
10
|
+
*/
|
|
11
|
+
export const RESIZE_SETTLE_MS = 120;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How far a box may stretch from the size its layout was made for, on either
|
|
15
|
+
* axis, before the layout is made again mid-resize: past this, tiles and
|
|
16
|
+
* blocks drawn in percentages of the old size would look squashed or pulled.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_HELD_STRETCH = 1.5;
|
|
19
|
+
|
|
20
|
+
export interface ResizeHold {
|
|
21
|
+
/** The box's size as it last rendered. */
|
|
22
|
+
size: CellSize;
|
|
23
|
+
/** While the box is resizing, the size its layout holds to; null at rest. */
|
|
24
|
+
held: CellSize | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stretch(from: number, to: number): number {
|
|
28
|
+
return from > 0 && to > 0 ? Math.max(from / to, to / from) : Infinity;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The box rendered at `size`: the first change of a resize holds the layout at
|
|
33
|
+
* the size it had, later ones keep it there, and a stretch past
|
|
34
|
+
* MAX_HELD_STRETCH lays it out again at the new size (still resizing).
|
|
35
|
+
*/
|
|
36
|
+
export function nextResizeHold(state: ResizeHold, size: CellSize, maxStretch = MAX_HELD_STRETCH): ResizeHold {
|
|
37
|
+
if (state.size.width === size.width && state.size.height === size.height) return state;
|
|
38
|
+
const held = state.held ?? state.size;
|
|
39
|
+
const far = Math.max(stretch(held.width, size.width), stretch(held.height, size.height)) > maxStretch;
|
|
40
|
+
return { size, held: far ? size : held };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** At rest: the layout takes the box's size. */
|
|
44
|
+
export function settleResizeHold(state: ResizeHold): ResizeHold {
|
|
45
|
+
return state.held ? { size: state.size, held: null } : state;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SettledLayoutSize extends CellSize {
|
|
49
|
+
/**
|
|
50
|
+
* The box is being resized: from the first move of a resize that has its
|
|
51
|
+
* pane's frame, or its first change of size, until it has held still for
|
|
52
|
+
* RESIZE_SETTLE_MS with the pointer let go. Nothing laid out in it should
|
|
53
|
+
* animate meanwhile.
|
|
54
|
+
*/
|
|
55
|
+
resizing: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Desktop: the size to lay out a surface at whose content is placed in
|
|
60
|
+
* percentages of its box (a heat map's tiles), and whether that box is being
|
|
61
|
+
* resized. A pane resize, a divider, a maximise or restore and a window
|
|
62
|
+
* resize all change the box's size, often once a frame; laying the content
|
|
63
|
+
* out again at every one of them costs a full restyle and moves every tile
|
|
64
|
+
* each time. Instead, while the size changes the layout holds to the size it
|
|
65
|
+
* had and the box stretches it by CSS, and once the size has held still it is
|
|
66
|
+
* laid out once, at the size the box settled on. A stretch too far from the
|
|
67
|
+
* held size lays it out again on the way.
|
|
68
|
+
*
|
|
69
|
+
* `resizing` is the shared signal for anything that animates when its layout
|
|
70
|
+
* changes (a glide, a fade, a pulse): none of it while the size is changing.
|
|
71
|
+
* Off (`enabled: false`, the terminal), the size is the box's at once.
|
|
72
|
+
*/
|
|
73
|
+
export function useSettledLayoutSize(
|
|
74
|
+
width: number,
|
|
75
|
+
height: number,
|
|
76
|
+
{ enabled = true }: { enabled?: boolean } = {},
|
|
77
|
+
): SettledLayoutSize {
|
|
78
|
+
const [state, setState] = useState<ResizeHold>(() => ({ size: { width, height }, held: null }));
|
|
79
|
+
let current = state;
|
|
80
|
+
if (state.size.width !== width || state.size.height !== height) {
|
|
81
|
+
// Stored while rendering, so the render that has the new size already lays out at the held one.
|
|
82
|
+
current = enabled ? nextResizeHold(state, { width, height }) : { size: { width, height }, held: null };
|
|
83
|
+
setState(current);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A resize that has this pane's frame: it holds still from the first move, before its content catches up.
|
|
87
|
+
const frame = useContext(LivePaneFrameContext);
|
|
88
|
+
const [framed, setFramed] = useState(false);
|
|
89
|
+
const framedRef = useRef(false);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
// Called on every move: only a resize starting or ending is news.
|
|
92
|
+
const follow = () => {
|
|
93
|
+
const next = enabled && frame !== null && frame.live() !== null;
|
|
94
|
+
if (next === framedRef.current) return;
|
|
95
|
+
framedRef.current = next;
|
|
96
|
+
setFramed(next);
|
|
97
|
+
};
|
|
98
|
+
follow();
|
|
99
|
+
return enabled ? frame?.subscribe(follow) : undefined;
|
|
100
|
+
}, [enabled, frame]);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!state.held || framed) return;
|
|
104
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
105
|
+
let stopWaiting: (() => void) | null = null;
|
|
106
|
+
const settle = () => {
|
|
107
|
+
timer = null;
|
|
108
|
+
// A box still under the pointer (a sidebar, a split) settles when it is let go, not when the pointer pauses.
|
|
109
|
+
if (isLiveResizing()) {
|
|
110
|
+
stopWaiting = onLiveResizeEnd(() => {
|
|
111
|
+
stopWaiting?.();
|
|
112
|
+
stopWaiting = null;
|
|
113
|
+
timer = setTimeout(settle, RESIZE_SETTLE_MS);
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
setState(settleResizeHold);
|
|
118
|
+
};
|
|
119
|
+
timer = setTimeout(settle, RESIZE_SETTLE_MS);
|
|
120
|
+
return () => {
|
|
121
|
+
if (timer !== null) clearTimeout(timer);
|
|
122
|
+
stopWaiting?.();
|
|
123
|
+
};
|
|
124
|
+
}, [framed, state]);
|
|
125
|
+
|
|
126
|
+
const layout = current.held ?? current.size;
|
|
127
|
+
return { width: layout.width, height: layout.height, resizing: enabled && (current.held !== null || framed) };
|
|
128
|
+
}
|
|
@@ -29,3 +29,59 @@ export function isoDateDaysAgo(days: number, now = new Date()): string {
|
|
|
29
29
|
export function isoDateYearsAgo(years: number, now = new Date()): string {
|
|
30
30
|
return isoDateBefore(now, years, 0);
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/** YYYY-MM-DD for a day the calendar has; null for a month or day out of range. */
|
|
34
|
+
function calendarDay(year: number, month: number, day: number): string | null {
|
|
35
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
36
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
|
|
37
|
+
? date.toISOString().slice(0, 10)
|
|
38
|
+
: null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
42
|
+
/** A day and a month in either order, then a four-digit year, with one separator throughout: 14/10/2026, 14.10.2026, 14-10-2026. */
|
|
43
|
+
const YEAR_LAST = /^(\d{1,2})([/.-])(\d{1,2})\2(\d{4})$/;
|
|
44
|
+
/** A four-digit year first, written another way than YYYY-MM-DD: 2026/10/14, 2026.10.14, 2026-10-4. */
|
|
45
|
+
const YEAR_FIRST = /^(\d{4})([/.-])(\d{1,2})\2(\d{1,2})$/;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The YYYY-MM-DD date that a date written another way names: 14/10/2026 and
|
|
49
|
+
* 10/14/2026 are 2026-10-14, since a part over 12 can only be the day. When
|
|
50
|
+
* both parts could be the month (03/04/2026), only dots are read, day first,
|
|
51
|
+
* as dotted dates are written (`assumedDayFirst`); a slash or a dash is
|
|
52
|
+
* written both ways round, so that is null. Null as well for text that is not
|
|
53
|
+
* a date of the calendar.
|
|
54
|
+
*/
|
|
55
|
+
function isoDateReading(value: string): { date: string; assumedDayFirst: boolean } | null {
|
|
56
|
+
const text = value.trim();
|
|
57
|
+
const yearFirst = YEAR_FIRST.exec(text);
|
|
58
|
+
if (yearFirst) {
|
|
59
|
+
const date = calendarDay(Number(yearFirst[1]), Number(yearFirst[3]), Number(yearFirst[4]));
|
|
60
|
+
return date && date !== text ? { date, assumedDayFirst: false } : null;
|
|
61
|
+
}
|
|
62
|
+
const yearLast = YEAR_LAST.exec(text);
|
|
63
|
+
if (!yearLast) return null;
|
|
64
|
+
const [first, second, year] = [Number(yearLast[1]), Number(yearLast[3]), Number(yearLast[4])];
|
|
65
|
+
const monthFirst = first <= 12 && second > 12;
|
|
66
|
+
const either = first <= 12 && second <= 12 && first !== second;
|
|
67
|
+
if (either && yearLast[2] !== ".") return null;
|
|
68
|
+
const date = monthFirst ? calendarDay(year, first, second) : calendarDay(year, second, first);
|
|
69
|
+
return date ? { date, assumedDayFirst: either } : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A date option's value, checked before any request is made: YYYY-MM-DD as
|
|
74
|
+
* typed, or an error that names the format. A date written another way gets
|
|
75
|
+
* the option to type instead (`--from 2026-10-14` for 14/10/2026); one whose
|
|
76
|
+
* day and month could be either way round gets an example of the format. An
|
|
77
|
+
* empty value is no date.
|
|
78
|
+
*/
|
|
79
|
+
export function isoDateOption(value: string, flag: string, now = new Date()): string {
|
|
80
|
+
const text = value.trim();
|
|
81
|
+
const iso = ISO_DATE.exec(text);
|
|
82
|
+
if (!text || (iso && calendarDay(Number(iso[1]), Number(iso[2]), Number(iso[3])))) return text;
|
|
83
|
+
const format = `Use a --${flag} date in YYYY-MM-DD format`;
|
|
84
|
+
const reading = isoDateReading(text);
|
|
85
|
+
if (reading) throw new Error(`${format}: type --${flag} ${reading.date} for ${text}${reading.assumedDayFirst ? " (day first)" : ""}.`);
|
|
86
|
+
throw new Error(`${format}, such as --${flag} ${isoDateToday(now)}.`);
|
|
87
|
+
}
|
|
@@ -92,3 +92,34 @@ export function parsePastedNumber(value: unknown): PastedNumber {
|
|
|
92
92
|
export function ambiguousNumberMessage(text: string, reading: { thousands: number; decimal: number }): string {
|
|
93
93
|
return `${text.trim()} could be ${reading.thousands} (thousands) or ${reading.decimal} (decimal comma); type ${reading.thousands} or ${reading.decimal}`;
|
|
94
94
|
}
|
|
95
|
+
|
|
96
|
+
/** What a dot and exactly three digits after one to three (`81.000`, `81.250`) could mean. */
|
|
97
|
+
export interface ThousandsDotReadings {
|
|
98
|
+
/** The number as written, the dot a decimal point: 81. */
|
|
99
|
+
decimal: number;
|
|
100
|
+
/** The dot as many locales write it, between thousands: 81000. */
|
|
101
|
+
thousands: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const THOUSANDS_DOT = /^([+-]?)([1-9]\d{0,2})\.(\d{3})$/;
|
|
105
|
+
|
|
106
|
+
/** Both readings of `81.000`; null for any other text, which has one. */
|
|
107
|
+
export function thousandsDotReadings(value: unknown): ThousandsDotReadings | null {
|
|
108
|
+
const match = THOUSANDS_DOT.exec(typeof value === "string" ? value.trim() : "");
|
|
109
|
+
if (!match) return null;
|
|
110
|
+
const sign = match[1] === "-" ? -1 : 1;
|
|
111
|
+
return { decimal: sign * Number(`${match[2]}.${match[3]}`), thousands: sign * Number(`${match[2]}${match[3]}`) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether a dot and three digits was meant as a thousands separator, beside a
|
|
116
|
+
* price it must be near (the last price): the number as written is more than
|
|
117
|
+
* 90% away from it, while the thousands reading lands within 10%. A plain
|
|
118
|
+
* dotted number otherwise keeps its meaning, and without a reference it is
|
|
119
|
+
* read as written.
|
|
120
|
+
*/
|
|
121
|
+
export function meantThousandsDot(readings: ThousandsDotReadings, reference: number | null | undefined): boolean {
|
|
122
|
+
if (reference == null || !Number.isFinite(reference) || reference === 0) return false;
|
|
123
|
+
const scale = Math.abs(reference);
|
|
124
|
+
return Math.abs(readings.decimal - reference) * 10 > scale * 9 && Math.abs(readings.thousands - reference) * 10 <= scale;
|
|
125
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CorporateActionsData, DeclaredDividendAction } from "../types/financials";
|
|
2
|
+
|
|
3
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4
|
+
/** A paid row this close to a declared one is that distribution, dated a day apart. */
|
|
5
|
+
const SAME_DISTRIBUTION_DAYS = 3;
|
|
6
|
+
const DAY_MS = 86_400_000;
|
|
7
|
+
|
|
8
|
+
function isoDate(value: unknown): string | undefined {
|
|
9
|
+
return typeof value === "string" && ISO_DATE.test(value) && Number.isFinite(Date.parse(`${value}T00:00:00Z`)) ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The declared, unpaid distributions a corporate actions payload carries,
|
|
14
|
+
* oldest first: rows the source states with a real ex-date and amount, still
|
|
15
|
+
* owed today (not yet paid, or with no pay date, not yet ex), and not in the
|
|
16
|
+
* paid history yet. Kept apart from `dividends`, which history, yields and
|
|
17
|
+
* growth read. A server that does not send them, or a listing it has none
|
|
18
|
+
* for, gives none.
|
|
19
|
+
*/
|
|
20
|
+
export function declaredDividends(
|
|
21
|
+
actions: Pick<CorporateActionsData, "dividends" | "declaredDividends"> | null | undefined,
|
|
22
|
+
now = new Date(),
|
|
23
|
+
): DeclaredDividendAction[] {
|
|
24
|
+
if (!Array.isArray(actions?.declaredDividends)) return [];
|
|
25
|
+
const today = now.toISOString().slice(0, 10);
|
|
26
|
+
const paid = (actions.dividends ?? []).map((payment) => Date.parse(`${payment.exDate}T00:00:00Z`));
|
|
27
|
+
return actions.declaredDividends.flatMap((row): DeclaredDividendAction[] => {
|
|
28
|
+
const exDate = isoDate(row?.exDate);
|
|
29
|
+
if (!exDate || typeof row.amount !== "number" || !Number.isFinite(row.amount) || row.amount <= 0) return [];
|
|
30
|
+
const payDate = isoDate(row.payDate);
|
|
31
|
+
const declarationDate = isoDate(row.declarationDate);
|
|
32
|
+
const recordDate = isoDate(row.recordDate);
|
|
33
|
+
if ((payDate ?? exDate) < today) return [];
|
|
34
|
+
const at = Date.parse(`${exDate}T00:00:00Z`);
|
|
35
|
+
if (paid.some((time) => Math.abs(time - at) <= SAME_DISTRIBUTION_DAYS * DAY_MS)) return [];
|
|
36
|
+
return [{ exDate, amount: row.amount, ...declarationDate ? { declarationDate } : {},
|
|
37
|
+
...recordDate ? { recordDate } : {}, ...payDate ? { payDate } : {} }];
|
|
38
|
+
}).sort((left, right) => left.exDate.localeCompare(right.exDate));
|
|
39
|
+
}
|