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.
Files changed (200) hide show
  1. package/package.json +2 -1
  2. package/src/api-client/chat.ts +28 -0
  3. package/src/api-client/errors.ts +11 -1
  4. package/src/api-client/futures-curve.ts +16 -0
  5. package/src/api-client/index.ts +22 -9
  6. package/src/api-client/normalizers.ts +21 -0
  7. package/src/api-client/socket.ts +27 -0
  8. package/src/api-client/types.ts +3 -1
  9. package/src/app/runtime/startup.ts +4 -0
  10. package/src/cli/commands/market.ts +60 -9
  11. package/src/cli/commands/search.ts +43 -2
  12. package/src/cli/commands/ticker.ts +100 -21
  13. package/src/cli/desktop-pane-shot.ts +34 -9
  14. package/src/cli/fx-pair.ts +17 -0
  15. package/src/cli/glossary.ts +134 -6
  16. package/src/cli/history-rows.ts +16 -4
  17. package/src/cli/index.ts +44 -12
  18. package/src/cli/known-gaps.ts +21 -2
  19. package/src/cli/listing-arg.ts +19 -4
  20. package/src/cli/listing-suggestion.ts +33 -2
  21. package/src/cli/pane-functions/dom.ts +39 -6
  22. package/src/cli/pane-functions/freshness.ts +48 -5
  23. package/src/cli/pane-functions/function-help.ts +1 -1
  24. package/src/cli/pane-functions/headless.ts +85 -19
  25. package/src/cli/pane-functions/index.ts +60 -4
  26. package/src/cli/pane-functions/market-session.ts +69 -6
  27. package/src/cli/pane-functions/resolver.ts +4 -2
  28. package/src/cli/pane-functions/screenshot.ts +14 -1
  29. package/src/cli/scroll-truncation.ts +5 -1
  30. package/src/cli/symbol-args.ts +41 -3
  31. package/src/cli/symbol-list.ts +19 -7
  32. package/src/cli/unknown-command.ts +70 -0
  33. package/src/components/chart/native/surface/visibility.ts +37 -7
  34. package/src/components/index.ts +1 -1
  35. package/src/components/layout/header.tsx +348 -102
  36. package/src/components/markdown-text.tsx +40 -66
  37. package/src/components/market-board/index.tsx +12 -6
  38. package/src/components/metric-treemap/heat-surface.tsx +76 -14
  39. package/src/components/metric-treemap/heat-tiles.ts +5 -3
  40. package/src/components/metric-treemap/index.tsx +1 -1
  41. package/src/components/reader/detail-reader.tsx +28 -6
  42. package/src/components/ticker/badge/index.tsx +2 -0
  43. package/src/components/ui/button.tsx +24 -9
  44. package/src/components/ui/display.tsx +6 -1
  45. package/src/components/ui/external-link.tsx +10 -2
  46. package/src/components/ui/index.ts +1 -1
  47. package/src/components/ui/status.tsx +148 -5
  48. package/src/data/memory-resource-store.ts +1 -1
  49. package/src/data/resource-store.ts +13 -1
  50. package/src/docs/snapshot.json +13 -13
  51. package/src/i18n/es.ts +1 -0
  52. package/src/i18n/ja.ts +1 -0
  53. package/src/i18n/ko.ts +1 -0
  54. package/src/i18n/zh-cn.ts +1 -0
  55. package/src/i18n/zh-tw.ts +1 -0
  56. package/src/layout-marketplace/gallery-desktop.tsx +2 -2
  57. package/src/market-data/futures-board.ts +44 -1
  58. package/src/market-data/futures-contract.ts +10 -0
  59. package/src/market-data/market/format.ts +7 -2
  60. package/src/market-data/market/freshness.ts +25 -1
  61. package/src/market-data/quotes/regular-session-valuation.ts +32 -1
  62. package/src/market-data/quotes/resolution.ts +2 -0
  63. package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
  64. package/src/plugins/builtin/account-management/pane.tsx +17 -13
  65. package/src/plugins/builtin/alerts/cli.ts +72 -11
  66. package/src/plugins/builtin/attention/headless.ts +19 -10
  67. package/src/plugins/builtin/attention/model.ts +22 -0
  68. package/src/plugins/builtin/awards/headless.ts +5 -1
  69. package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
  70. package/src/plugins/builtin/catalysts/headless.ts +5 -1
  71. package/src/plugins/builtin/changelog/index.tsx +11 -8
  72. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
  73. package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
  74. package/src/plugins/builtin/chat/attachments/model.ts +5 -3
  75. package/src/plugins/builtin/chat/content/footer.tsx +6 -0
  76. package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
  77. package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
  78. package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
  79. package/src/plugins/builtin/chat/controller/connections.ts +18 -0
  80. package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
  81. package/src/plugins/builtin/chat/controller/index.ts +49 -21
  82. package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
  83. package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
  84. package/src/plugins/builtin/chat/layout.ts +91 -31
  85. package/src/plugins/builtin/chat/markdown.ts +94 -0
  86. package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
  87. package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
  88. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  89. package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
  90. package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
  91. package/src/plugins/builtin/company-attention/headless.ts +8 -5
  92. package/src/plugins/builtin/company-attention/model.ts +53 -2
  93. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  94. package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
  95. package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
  96. package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
  97. package/src/plugins/builtin/cot/client.ts +12 -4
  98. package/src/plugins/builtin/cot/model.ts +20 -0
  99. package/src/plugins/builtin/cot/pane.tsx +2 -2
  100. package/src/plugins/builtin/crypto-board/model.ts +7 -3
  101. package/src/plugins/builtin/custom-view/pane.tsx +2 -2
  102. package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
  103. package/src/plugins/builtin/debug/index.tsx +15 -15
  104. package/src/plugins/builtin/dividend-yield/client.ts +8 -1
  105. package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
  106. package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
  107. package/src/plugins/builtin/docs/index.tsx +5 -3
  108. package/src/plugins/builtin/docs/pane.tsx +19 -2
  109. package/src/plugins/builtin/docs/targets.ts +5 -3
  110. package/src/plugins/builtin/earnings-calls/data.ts +22 -0
  111. package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
  112. package/src/plugins/builtin/equity-screener/headless.ts +4 -3
  113. package/src/plugins/builtin/equity-screener/live.ts +27 -1
  114. package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
  115. package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
  116. package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
  117. package/src/plugins/builtin/futures/index.tsx +33 -6
  118. package/src/plugins/builtin/futures/model.ts +69 -5
  119. package/src/plugins/builtin/futures/series.ts +3 -3
  120. package/src/plugins/builtin/futures/table.tsx +31 -11
  121. package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
  122. package/src/plugins/builtin/holders/pane.tsx +6 -1
  123. package/src/plugins/builtin/jobs/pane.tsx +12 -9
  124. package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
  125. package/src/plugins/builtin/mna/client.ts +12 -2
  126. package/src/plugins/builtin/mna/detail.tsx +2 -2
  127. package/src/plugins/builtin/mna/pane.tsx +2 -2
  128. package/src/plugins/builtin/news/headless.ts +4 -2
  129. package/src/plugins/builtin/notes/owner.tsx +5 -4
  130. package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
  131. package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
  132. package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
  133. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  134. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
  135. package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
  136. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
  137. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +4 -12
  138. package/src/plugins/builtin/research/event-model.ts +14 -0
  139. package/src/plugins/builtin/research/headless-schema.ts +31 -1
  140. package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
  141. package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
  142. package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
  143. package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
  144. package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
  145. package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
  146. package/src/plugins/builtin/research-search/data.ts +11 -0
  147. package/src/plugins/builtin/research-search/pane.tsx +13 -2
  148. package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
  149. package/src/plugins/builtin/seasonality/client.ts +23 -0
  150. package/src/plugins/builtin/seasonality/pane.tsx +4 -2
  151. package/src/plugins/builtin/sectors/index.tsx +7 -3
  152. package/src/plugins/builtin/shared/report-freshness.ts +6 -2
  153. package/src/plugins/builtin/short-interest/headless.ts +13 -3
  154. package/src/plugins/builtin/short-interest/index.tsx +1 -1
  155. package/src/plugins/builtin/short-interest/model.ts +62 -3
  156. package/src/plugins/builtin/short-interest/pane.tsx +2 -2
  157. package/src/plugins/builtin/short-interest/schedule.ts +107 -0
  158. package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
  159. package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
  160. package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
  161. package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
  162. package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
  163. package/src/plugins/builtin/themes/headless.ts +1 -1
  164. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
  165. package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
  166. package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
  167. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
  168. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
  169. package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
  170. package/src/plugins/builtin/volatility/chart-model.ts +1 -1
  171. package/src/plugins/builtin/volatility/charts.tsx +1 -1
  172. package/src/renderers/dom/build-assets.ts +17 -4
  173. package/src/renderers/dom/desktop/controls.tsx +2 -0
  174. package/src/renderers/dom/styles.css +6 -0
  175. package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
  176. package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
  177. package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
  178. package/src/sources/gloomberb-cloud/normalizers.ts +13 -4
  179. package/src/sources/provider-router/batches.ts +2 -1
  180. package/src/sources/provider-router/cache.ts +3 -0
  181. package/src/sync/baseline.ts +19 -14
  182. package/src/sync/controller.ts +140 -40
  183. package/src/sync/core-contributors.ts +256 -32
  184. package/src/sync/types.ts +44 -0
  185. package/src/time-series/resolve.ts +24 -5
  186. package/src/types/data-provider.ts +8 -0
  187. package/src/types/financials.ts +40 -4
  188. package/src/types/headless.ts +43 -0
  189. package/src/types/plugin.ts +7 -0
  190. package/src/ui/resize-settle.ts +128 -0
  191. package/src/utils/calendar-date.ts +56 -0
  192. package/src/utils/decimal-comma.ts +31 -0
  193. package/src/utils/declared-dividends.ts +39 -0
  194. package/src/utils/forward-dividend.ts +34 -11
  195. package/src/utils/fundamentals.ts +13 -2
  196. package/src/utils/futures-generic.ts +25 -4
  197. package/src/utils/markdown.ts +180 -0
  198. package/src/utils/market-capitalization.ts +22 -0
  199. package/src/utils/plugin-proxy-hosts.json +5 -0
  200. 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, futuresGenericPriceBasis } from "../utils/futures-generic";
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, not dollars.
1170
- const genericBasis = generic && isMarketFieldId(field.id) ? futuresGenericPriceBasis(generic) : null;
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 ? "%" : units,
1252
- unitGroup: isPercent ? "percent" : `economic:${units.toLowerCase()}`,
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
  }
@@ -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 sold short at the source's latest settlement. */
267
+ /** Shares short at the latest FINRA settlement, the one `SI` and `SIW` show; dated by `shortInterestDate`. */
252
268
  sharesShort?: number;
253
- /** Shares short over float from the same observation, as a fraction (0.0127 for 1.27%). */
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 {
@@ -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
+ }
@@ -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
+ }