gloomberb 0.16.2 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/brokers/account-cache.ts +45 -0
- package/src/cli/command-options.ts +74 -0
- package/src/cli/commands/command-utils.ts +29 -0
- package/src/cli/commands/market.ts +170 -54
- package/src/cli/commands/overview.ts +62 -19
- package/src/cli/commands/ticker.ts +138 -43
- package/src/cli/desktop-pane-shot.ts +3 -0
- package/src/cli/help.ts +26 -6
- package/src/cli/history-rows.ts +187 -15
- package/src/cli/index.ts +12 -7
- package/src/cli/options.ts +5 -1
- package/src/cli/pane-functions/capabilities.ts +5 -0
- package/src/cli/pane-functions/dom.ts +69 -16
- package/src/cli/pane-functions/function-help.ts +4 -2
- package/src/cli/pane-functions/headless.ts +163 -31
- package/src/cli/pane-functions/index.ts +9 -2
- package/src/cli/pane-functions/report.ts +8 -1
- package/src/cli/pane-functions/resolver.ts +28 -2
- package/src/cli/pane-functions/screenshot.ts +4 -0
- package/src/cli/registry.ts +6 -3
- package/src/cli/report-tables.ts +460 -0
- package/src/cli/result.ts +13 -3
- package/src/components/data-table/export.ts +6 -51
- package/src/components/data-table/view.tsx +3 -1
- package/src/components/layout/shell/active-drag.ts +67 -35
- package/src/components/layout/shell/cursor-occlusion.ts +6 -5
- package/src/components/layout/shell/drag/index.ts +12 -7
- package/src/components/layout/shell/drag/live.ts +122 -0
- package/src/components/layout/shell/drag/overlays.tsx +179 -35
- package/src/components/layout/shell/drag/runtime.ts +62 -50
- package/src/components/layout/shell/drag/slide.ts +10 -0
- package/src/components/layout/shell/index.tsx +37 -51
- package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
- package/src/components/layout/shell/native/surfaces.ts +30 -14
- package/src/components/layout/shell/pane/layers.tsx +419 -179
- package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
- package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
- package/src/data/json-persistence.ts +17 -3
- package/src/data/json-storage.ts +2 -0
- package/src/data/memory-resource-store.ts +46 -0
- package/src/market-data/market/freshness.ts +45 -0
- package/src/market-data/quotes/freshness.ts +5 -0
- package/src/plugins/builtin/analytics/pane-model.ts +10 -6
- package/src/plugins/builtin/attention/pane.tsx +1 -1
- package/src/plugins/builtin/catalysts/detail.tsx +1 -1
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/holders/client.ts +3 -6
- package/src/plugins/builtin/holders/format.ts +31 -1
- package/src/plugins/builtin/holders/headless.ts +26 -5
- package/src/plugins/builtin/holders/pane.tsx +22 -13
- package/src/plugins/builtin/holders/report-header.ts +46 -5
- package/src/plugins/builtin/kelly-sizer/pane.tsx +12 -6
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
- package/src/plugins/builtin/members/pane.tsx +10 -0
- package/src/plugins/builtin/perps/pane.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
- package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
- package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -0
- package/src/plugins/builtin/portfolio-list/headless.ts +2 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
- package/src/plugins/builtin/shared/members-menu.tsx +260 -0
- package/src/plugins/builtin/short-volume/pane.tsx +1 -1
- package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
- package/src/plugins/builtin/themes/pane.tsx +3 -0
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
- package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/headless.ts +20 -4
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
- package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
- package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
- package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
- package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
- package/src/plugins/builtin/world-venue-map/index.ts +5 -5
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
- package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
- package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
- package/src/plugins/builtin/world-venue-map/model.ts +16 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
- package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
- package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
- package/src/renderers/browser/app-services.ts +3 -1
- package/src/renderers/dom/data-table/dom.ts +31 -0
- package/src/renderers/dom/data-table/index.tsx +5 -0
- package/src/renderers/dom/host/box.tsx +113 -17
- package/src/renderers/dom/host/drag-shield.ts +24 -0
- package/src/renderers/dom/host/live-frame.ts +54 -0
- package/src/renderers/dom/host/mouse.ts +8 -1
- package/src/renderers/dom/host/scroll-box.tsx +6 -5
- package/src/renderers/dom/host/tabs.tsx +3 -2
- package/src/renderers/dom/styles.css +13 -5
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +10 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +3 -0
- package/src/renderers/electrobun/view/host/native.ts +0 -10
- package/src/renderers/electrobun/view/host/window-move.ts +28 -0
- package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
- package/src/renderers/electrobun/view/ui-host.tsx +2 -5
- package/src/renderers/opentui/ui-host.tsx +7 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +16 -20
- package/src/sources/provider-router/batches.ts +3 -3
- package/src/sources/provider-router/financial-routes.ts +2 -2
- package/src/sources/provider-router/financials.ts +70 -7
- package/src/sources/provider-router/primary.ts +2 -2
- package/src/state/pane-scroll-registry.tsx +67 -35
- package/src/test-support/margin-account.ts +108 -0
- package/src/time-series/resolution.ts +15 -0
- package/src/types/financials.ts +32 -3
- package/src/ui/host.tsx +12 -0
- package/src/ui/index.tsx +8 -3
- package/src/ui/scroll-box-axis.ts +26 -0
- package/src/utils/currency-units.ts +8 -3
- package/src/utils/depositary-receipt.ts +54 -10
- package/src/utils/display-number.ts +63 -0
- package/src/utils/fundamentals.ts +17 -1
- package/src/utils/price-history-integrity.ts +19 -6
|
@@ -2,6 +2,7 @@ import { RGBA, StyledText as OpenTuiStyledText, SyntaxStyle, TextAttributes as O
|
|
|
2
2
|
import { createElement, forwardRef, useEffect, useRef, useState, type ReactNode } from "react";
|
|
3
3
|
import { TextAttributes, type UiHost, type TextProps } from "../../ui/host";
|
|
4
4
|
import { renderAsciiText } from "../../ui/ascii-font";
|
|
5
|
+
import { scrollBoxScrollsVertically } from "../../ui/scroll-box-axis";
|
|
5
6
|
import { OpenTuiImageSurface } from "./image/surface";
|
|
6
7
|
import { OpenTuiChartSurface } from "./chart-surface";
|
|
7
8
|
|
|
@@ -141,7 +142,12 @@ function mapTextContent(content: unknown): unknown {
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
const OpenTuiBox = createOpenTuiPrimitive("box");
|
|
144
|
-
const
|
|
145
|
+
const OpenTuiScrollBoxPrimitive = createOpenTuiPrimitive("scrollbox");
|
|
146
|
+
// The native box scrolls vertically unless scrollY is false; the shared rule
|
|
147
|
+
// also keeps the one-row horizontal strips from scrolling on that axis.
|
|
148
|
+
const OpenTuiScrollBox = forwardRef<unknown, OpenTuiPrimitiveProps>(function OpenTuiScrollBox(props, ref) {
|
|
149
|
+
return createElement(OpenTuiScrollBoxPrimitive, { ...props, scrollY: scrollBoxScrollsVertically(props), ref });
|
|
150
|
+
});
|
|
145
151
|
const OpenTuiInput = createOpenTuiPrimitive("input");
|
|
146
152
|
const OpenTuiTextarea = createOpenTuiPrimitive("textarea");
|
|
147
153
|
const OpenTuiMediaSurface = createOpenTuiPrimitive("box");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { futuresGenericListing, futuresGenericPriceBasis } from "../../utils/futures-generic";
|
|
2
2
|
import type { TimeRange } from "../../time-series/range";
|
|
3
|
+
import { RANGE_HISTORY_RESOLUTION } from "../../time-series/resolution";
|
|
3
4
|
import type {
|
|
4
5
|
FinancialStatement,
|
|
5
6
|
Fundamentals,
|
|
@@ -370,29 +371,24 @@ export function formatCloudDateTime(
|
|
|
370
371
|
return `${year}-${month}-${day}`;
|
|
371
372
|
}
|
|
372
373
|
|
|
374
|
+
const HISTORY_OUTPUT_SIZE: Record<TimeRange, number> = {
|
|
375
|
+
"1D": 24 * 12,
|
|
376
|
+
"1W": 7 * 24,
|
|
377
|
+
"1M": 31,
|
|
378
|
+
"3M": 93,
|
|
379
|
+
"6M": 186,
|
|
380
|
+
"1Y": 366,
|
|
381
|
+
"5Y": 261,
|
|
382
|
+
"ALL": 600,
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/** The bar size comes from the shared range table, which `gloomberb help history` also lists. */
|
|
373
386
|
export function toHistoryRequest(range: TimeRange): {
|
|
374
387
|
interval: string;
|
|
375
388
|
outputsize: number;
|
|
376
389
|
rangeKey: TimeRange;
|
|
377
390
|
} {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
case "1W":
|
|
382
|
-
return { interval: "1h", outputsize: 7 * 24, rangeKey: range };
|
|
383
|
-
case "1M":
|
|
384
|
-
return { interval: "1day", outputsize: 31, rangeKey: range };
|
|
385
|
-
case "3M":
|
|
386
|
-
return { interval: "1day", outputsize: 93, rangeKey: range };
|
|
387
|
-
case "6M":
|
|
388
|
-
return { interval: "1day", outputsize: 186, rangeKey: range };
|
|
389
|
-
case "1Y":
|
|
390
|
-
return { interval: "1day", outputsize: 366, rangeKey: range };
|
|
391
|
-
case "5Y":
|
|
392
|
-
return { interval: "1week", outputsize: 261, rangeKey: range };
|
|
393
|
-
case "ALL":
|
|
394
|
-
return { interval: "1month", outputsize: 600, rangeKey: range };
|
|
395
|
-
default:
|
|
396
|
-
return { interval: "1day", outputsize: 366, rangeKey: range };
|
|
397
|
-
}
|
|
391
|
+
const resolution = RANGE_HISTORY_RESOLUTION[range];
|
|
392
|
+
if (!resolution) return { interval: "1day", outputsize: 366, rangeKey: range };
|
|
393
|
+
return { interval: toCloudInterval(resolution), outputsize: HISTORY_OUTPUT_SIZE[range], rangeKey: range };
|
|
398
394
|
}
|
|
@@ -66,7 +66,7 @@ export class ProviderRouterBatchRoutes {
|
|
|
66
66
|
const rawCached = selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, false);
|
|
67
67
|
const cached = rawCached && (brokerSourceKeys.includes(rawCached.sourceKey)
|
|
68
68
|
? !isQuoteStaleForCurrentSession(quoteWithFreshnessExchange(rawCached.value, target.exchange))
|
|
69
|
-
: isProviderQuoteUsableForCurrentSession(rawCached.value, target.exchange, target.symbol))
|
|
69
|
+
: isProviderQuoteUsableForCurrentSession(rawCached.value, target.exchange, target.symbol, { recentAnswer: !rawCached.stale }))
|
|
70
70
|
? rawCached
|
|
71
71
|
: null;
|
|
72
72
|
if (cached && !forceRefresh && !cached.stale) {
|
|
@@ -93,7 +93,7 @@ export class ProviderRouterBatchRoutes {
|
|
|
93
93
|
const uniqueTargets = [...providerIndexes.values()].map((bucket) => bucket[0]!.target);
|
|
94
94
|
const batchResults = await batchProvider.getQuotesBatch!(uniqueTargets, options).catch(() => []);
|
|
95
95
|
for (const item of batchResults) {
|
|
96
|
-
if (!isProviderQuoteUsableForCurrentSession(item.quote, item.target.exchange, item.target.symbol)) continue;
|
|
96
|
+
if (!isProviderQuoteUsableForCurrentSession(item.quote, item.target.exchange, item.target.symbol, { recentAnswer: true })) continue;
|
|
97
97
|
const key = this.quoteBatchKey(item.target);
|
|
98
98
|
const sourceKey = this.deps.providerSourceKey(batchProvider);
|
|
99
99
|
for (const entry of providerIndexes.get(key) ?? []) {
|
|
@@ -162,7 +162,7 @@ export class ProviderRouterBatchRoutes {
|
|
|
162
162
|
const key = this.cachedFinancialsBatchKey(item.target);
|
|
163
163
|
let value = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(item.financials));
|
|
164
164
|
if (value && !item.target.instrument && !providerFinancialsMatchTarget(value, item.target.symbol, item.target.exchange)) continue;
|
|
165
|
-
if (value) value = dropUnusableProviderQuote(value, item.target.exchange);
|
|
165
|
+
if (value) value = dropUnusableProviderQuote(value, item.target.exchange, { recentAnswer: true });
|
|
166
166
|
if (!value) continue;
|
|
167
167
|
const sourceKey = this.deps.providerSourceKey(batchProvider);
|
|
168
168
|
for (const entry of providerIndexes.get(key) ?? []) {
|
|
@@ -200,7 +200,7 @@ export class ProviderRouterFinancialRoutes {
|
|
|
200
200
|
const rawCached = selectCachedResource<Quote>(this.deps.resources, "quote", entityKey, variantKeys, sourceKeys, false);
|
|
201
201
|
const cachedIsStale = rawCached && (brokerSourceKeys.includes(rawCached.sourceKey)
|
|
202
202
|
? isQuoteContributionStaleForCurrentSession(quoteWithFreshnessExchange(rawCached.value, exchange))
|
|
203
|
-
: !isProviderQuoteUsableForCurrentSession(rawCached.value, exchange, ticker));
|
|
203
|
+
: !isProviderQuoteUsableForCurrentSession(rawCached.value, exchange, ticker, { recentAnswer: !rawCached.stale }));
|
|
204
204
|
const cached = rawCached && !cachedIsStale
|
|
205
205
|
? rawCached
|
|
206
206
|
: null;
|
|
@@ -259,7 +259,7 @@ export class ProviderRouterFinancialRoutes {
|
|
|
259
259
|
if (
|
|
260
260
|
record
|
|
261
261
|
&& (includeStale || !record.stale)
|
|
262
|
-
&& isProviderQuoteUsableForCurrentSession(record.value, exchange, ticker)
|
|
262
|
+
&& isProviderQuoteUsableForCurrentSession(record.value, exchange, ticker, { recentAnswer: !record.stale })
|
|
263
263
|
) {
|
|
264
264
|
return record.value;
|
|
265
265
|
}
|
|
@@ -7,7 +7,16 @@ import { redactUnavailableFundamentals, RETRACTABLE_VALUATION_FIELDS } from "../
|
|
|
7
7
|
import { isExtendedHoursExchange, isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
|
|
8
8
|
import { mergeQuoteMetadata, quoteMetadataFromQuote, quoteMetadataMatchesTarget } from "../../market-data/quotes/metadata";
|
|
9
9
|
import { parsePublicTickerKey } from "../../utils/exchanges";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
activeUsMarketSession,
|
|
12
|
+
delayedFeedLagMs,
|
|
13
|
+
isMiddayBreakPrint,
|
|
14
|
+
isRegularSessionTime,
|
|
15
|
+
isUsPostSessionPrint,
|
|
16
|
+
isUsPriorSessionPremarketQuote,
|
|
17
|
+
isUsRegularCloseQuoteInPostSession,
|
|
18
|
+
latestRegularSessionOpen,
|
|
19
|
+
} from "../../market-data/market/freshness";
|
|
11
20
|
import {
|
|
12
21
|
mergeQuoteContributionMaps,
|
|
13
22
|
isQuoteContributionStaleForCurrentSession,
|
|
@@ -122,7 +131,42 @@ function isQuoteInActiveSession(quote: Quote, now: number): boolean {
|
|
|
122
131
|
return false;
|
|
123
132
|
}
|
|
124
133
|
|
|
125
|
-
|
|
134
|
+
// An opening auction can print before the continuous open (Hong Kong's at 09:20).
|
|
135
|
+
const OPENING_AUCTION_MS = 15 * 60_000;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether Gloom Cloud vouches that a delayed quote is current, however long
|
|
139
|
+
* ago its last trade was. The service judges more than that trade's age: it
|
|
140
|
+
* knows when it asked for the quote and how often the listing trades, so a
|
|
141
|
+
* thinly traded listing's hour-old trade can be the current price while its
|
|
142
|
+
* venue trades. Its verdict stands in for the age bound when:
|
|
143
|
+
* - the service answered on this request, or a cache entry still inside its
|
|
144
|
+
* TTL holds that answer (`recentAnswer`); a copy past the TTL is not vouched for;
|
|
145
|
+
* - the answer says `stale: false`. One without the flag vouches for nothing,
|
|
146
|
+
* and one flagged stale never gets this far;
|
|
147
|
+
* - the quote is delayed. The service bounds a delayed quote's feed age itself
|
|
148
|
+
* but not a real-time one's, so real-time quotes keep the age bound;
|
|
149
|
+
* - it reports the regular session, the venue's session is in progress as
|
|
150
|
+
* the delayed feed shows it, and the trade belongs to that session; or it
|
|
151
|
+
* reports a US after-hours price, the after-hours session is in progress and
|
|
152
|
+
* the trade belongs to it.
|
|
153
|
+
* The answer says nothing about how often a listing trades, so a liquid
|
|
154
|
+
* listing the service wrongly vouches for is accepted too. The service's own
|
|
155
|
+
* bound for liquid listings, about twenty minutes, is tighter than this one.
|
|
156
|
+
*/
|
|
157
|
+
function isVouchedCurrentByService(quote: Quote, exchange: string | undefined, now: number): boolean {
|
|
158
|
+
if (quote.providerId !== "gloomberb-cloud" || quote.stale !== false || quote.dataSource !== "delayed") return false;
|
|
159
|
+
if (quote.marketState === "POST") {
|
|
160
|
+
return finitePositiveNumber(quote.postMarketPrice) && isUsPostSessionPrint(quote.lastUpdated, exchange, now);
|
|
161
|
+
}
|
|
162
|
+
if (quote.marketState !== "REGULAR") return false;
|
|
163
|
+
const feedTime = now - delayedFeedLagMs(exchange);
|
|
164
|
+
if (isRegularSessionTime(exchange, feedTime) !== true) return false;
|
|
165
|
+
const open = latestRegularSessionOpen(exchange, feedTime);
|
|
166
|
+
return open != null && quote.lastUpdated >= open - OPENING_AUCTION_MS;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isActiveProviderQuoteTooOld(quote: Quote, now = Date.now(), recentAnswer = false): boolean {
|
|
126
170
|
// The shared freshness predicate already validates and dates a daily NAV.
|
|
127
171
|
if (quote.priceObservation === "nav") return false;
|
|
128
172
|
if (!isQuoteInActiveSession(quote, now)) return false;
|
|
@@ -132,12 +176,17 @@ function isActiveProviderQuoteTooOld(quote: Quote, now = Date.now()): boolean {
|
|
|
132
176
|
if (isUsPriorSessionPremarketQuote(quote.lastUpdated, exchange, quote.marketState, now)) {
|
|
133
177
|
return false;
|
|
134
178
|
}
|
|
179
|
+
// Likewise today's regular close before any after-hours trade.
|
|
180
|
+
if (quote.postMarketPrice == null && isUsRegularCloseQuoteInPostSession(quote.lastUpdated, exchange, quote.marketState, now)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
135
183
|
if (!Number.isFinite(quote.lastUpdated)) return false;
|
|
136
184
|
const delayed = quote.dataSource === "delayed";
|
|
137
185
|
const maxAge = delayed ? ACTIVE_DELAYED_PROVIDER_QUOTE_MAX_AGE_MS : ACTIVE_PROVIDER_QUOTE_MAX_AGE_MS;
|
|
138
186
|
if (now - quote.lastUpdated <= maxAge) return false;
|
|
139
187
|
// While the venue pauses at midday, the morning's last print is the current price.
|
|
140
|
-
|
|
188
|
+
if (isMiddayBreakPrint(quote.lastUpdated, exchange, now, delayed ? delayedFeedLagMs(exchange) : 0)) return false;
|
|
189
|
+
return !(recentAnswer && isVouchedCurrentByService(quote, exchange, now));
|
|
141
190
|
}
|
|
142
191
|
|
|
143
192
|
export function providerQuoteMatchesTarget(quote: Quote | null | undefined, symbol?: string, exchange?: string): quote is Quote {
|
|
@@ -157,11 +206,21 @@ export function providerQuoteMatchesTarget(quote: Quote | null | undefined, symb
|
|
|
157
206
|
return true;
|
|
158
207
|
}
|
|
159
208
|
|
|
160
|
-
|
|
209
|
+
/**
|
|
210
|
+
* `recentAnswer`: the source answered with this quote on this request, or a
|
|
211
|
+
* cache entry still inside its TTL holds it. Only then can the source's own
|
|
212
|
+
* verdict on a delayed quote stand in for the age bound.
|
|
213
|
+
*/
|
|
214
|
+
export function isProviderQuoteUsableForCurrentSession(
|
|
215
|
+
quote: Quote | null | undefined,
|
|
216
|
+
exchange?: string,
|
|
217
|
+
symbol?: string,
|
|
218
|
+
options: { recentAnswer?: boolean } = {},
|
|
219
|
+
): quote is Quote {
|
|
161
220
|
if (!providerQuoteMatchesTarget(quote, symbol, exchange)) return false;
|
|
162
221
|
const normalized = quoteWithFreshnessExchange(quote, exchange);
|
|
163
222
|
if (isQuoteStaleForCurrentSession(normalized)) return false;
|
|
164
|
-
if (isActiveProviderQuoteTooOld(normalized)) return false;
|
|
223
|
+
if (isActiveProviderQuoteTooOld(normalized, Date.now(), options.recentAnswer === true)) return false;
|
|
165
224
|
// Futures may trade or settle at zero or negative prices. Require explicit
|
|
166
225
|
// source type metadata; an alias alone cannot establish the price domain.
|
|
167
226
|
const futures = ["FUT", "FUTURE", "FUTURES"].includes(normalized.instrumentType?.trim().toUpperCase() ?? "");
|
|
@@ -188,8 +247,12 @@ export function providerFinancialsMatchTarget(value: TickerFinancials, symbol: s
|
|
|
188
247
|
return quoteMetadataMatchesTarget(metadata, hasListing ? symbol : parsePublicTickerKey(symbol).symbol, hasListing ? exchange : undefined);
|
|
189
248
|
}
|
|
190
249
|
|
|
191
|
-
export function dropUnusableProviderQuote(
|
|
192
|
-
|
|
250
|
+
export function dropUnusableProviderQuote(
|
|
251
|
+
value: TickerFinancials,
|
|
252
|
+
exchange?: string,
|
|
253
|
+
options: { recentAnswer?: boolean } = {},
|
|
254
|
+
): TickerFinancials {
|
|
255
|
+
if (!value.quote || isProviderQuoteUsableForCurrentSession(value.quote, exchange, undefined, options)) {
|
|
193
256
|
return value;
|
|
194
257
|
}
|
|
195
258
|
|
|
@@ -73,7 +73,7 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
73
73
|
if (rawValue && !context?.instrument && !providerFinancialsMatchTarget(rawValue, ticker, exchange)) continue;
|
|
74
74
|
const resolvedValue = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(rawValue));
|
|
75
75
|
if (resolvedValue && !context?.instrument && !providerFinancialsMatchTarget(resolvedValue, ticker, exchange)) continue;
|
|
76
|
-
let value = resolvedValue ? dropUnusableProviderQuote(resolvedValue, exchange) : null;
|
|
76
|
+
let value = resolvedValue ? dropUnusableProviderQuote(resolvedValue, exchange, { recentAnswer: true }) : null;
|
|
77
77
|
if (!value) continue;
|
|
78
78
|
const sourceKey = this.options.providerSourceKey(provider);
|
|
79
79
|
if (context?.statementHistory === "extended" && !hasReusableExtendedHistory(value)) {
|
|
@@ -187,7 +187,7 @@ export class ProviderRouterPrimaryRoutes {
|
|
|
187
187
|
for (const provider of this.options.providersInPriorityOrder()) {
|
|
188
188
|
try {
|
|
189
189
|
const quote = await provider.getQuote(ticker, exchange, context);
|
|
190
|
-
if (!isProviderQuoteUsableForCurrentSession(quote, exchange, ticker)) continue;
|
|
190
|
+
if (!isProviderQuoteUsableForCurrentSession(quote, exchange, ticker, { recentAnswer: true })) continue;
|
|
191
191
|
const sourceKey = this.options.providerSourceKey(provider);
|
|
192
192
|
this.options.cacheResource(
|
|
193
193
|
"quote",
|
|
@@ -5,9 +5,12 @@ import { useAppSelector, useOptionalPaneInstanceId } from "./app/context";
|
|
|
5
5
|
|
|
6
6
|
type ScrollDirection = "up" | "down";
|
|
7
7
|
|
|
8
|
+
/** A scroll position: rows (or cells), or device pixels where the host measures in them. */
|
|
9
|
+
type ScrollTarget = { cells: number } | { pixels: number };
|
|
10
|
+
|
|
8
11
|
interface PaneScrollAction {
|
|
9
12
|
direction: ScrollDirection;
|
|
10
|
-
resolveTarget(scrollBox: ScrollBoxRenderable):
|
|
13
|
+
resolveTarget(scrollBox: ScrollBoxRenderable): ScrollTarget | null;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
interface PaneScrollEntry {
|
|
@@ -57,17 +60,58 @@ function isVisibleScrollBox(scrollBox: ScrollBoxRenderable): boolean {
|
|
|
57
60
|
return !hasHiddenAncestor(scrollBox) && !hasCollapsedBounds(scrollBox);
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
function
|
|
63
|
+
function maxScrollTop(scrollBox: ScrollBoxRenderable): number {
|
|
61
64
|
const viewportHeight = Math.max(0, scrollBox.viewport?.height ?? 0);
|
|
62
|
-
if (viewportHeight <= 0) return
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
if (viewportHeight <= 0) return 0;
|
|
66
|
+
return Math.max(0, scrollBox.scrollHeight - viewportHeight);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The desktop host rounds its viewport and content to whole rows, which can
|
|
71
|
+
* leave up to half a row of content past the last scrollable row. Where it
|
|
72
|
+
* also reports pixels, the ends are measured in them.
|
|
73
|
+
*/
|
|
74
|
+
function pixelRange(scrollBox: ScrollBoxRenderable): { top: number; max: number } | null {
|
|
75
|
+
const { scrollTopPx, scrollHeightPx, viewportPx } = scrollBox;
|
|
76
|
+
if (!scrollBox.scrollToPixels || typeof scrollTopPx !== "number" || typeof scrollHeightPx !== "number" || !viewportPx) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return { top: scrollTopPx, max: Math.max(0, scrollHeightPx - viewportPx.height) };
|
|
66
80
|
}
|
|
67
81
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
function topTarget(scrollBox: ScrollBoxRenderable): ScrollTarget | null {
|
|
83
|
+
if ((scrollBox.viewport?.height ?? 0) <= 0) return null;
|
|
84
|
+
const pixels = pixelRange(scrollBox);
|
|
85
|
+
if (pixels) return pixels.top >= 1 ? { pixels: 0 } : null;
|
|
86
|
+
return scrollBox.scrollTop > 0 ? { cells: 0 } : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function bottomTarget(scrollBox: ScrollBoxRenderable): ScrollTarget | null {
|
|
90
|
+
const max = maxScrollTop(scrollBox);
|
|
91
|
+
// A body that overflows by less than a row is not worth capturing the keys for.
|
|
92
|
+
if (max <= 0) return null;
|
|
93
|
+
const pixels = pixelRange(scrollBox);
|
|
94
|
+
if (pixels) return pixels.max - pixels.top >= 1 ? { pixels: pixels.max } : null;
|
|
95
|
+
return max !== scrollBox.scrollTop ? { cells: max } : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function scrollTargetByDelta(scrollBox: ScrollBoxRenderable, delta: number): ScrollTarget | null {
|
|
99
|
+
if ((scrollBox.viewport?.height ?? 0) <= 0) return null;
|
|
100
|
+
const wanted = scrollBox.scrollTop + delta;
|
|
101
|
+
if (delta < 0 && wanted <= 0) return topTarget(scrollBox);
|
|
102
|
+
if (delta > 0 && wanted >= maxScrollTop(scrollBox)) return bottomTarget(scrollBox);
|
|
103
|
+
return delta === 0 ? null : { cells: wanted };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function applyScrollTarget(scrollBox: ScrollBoxRenderable, target: ScrollTarget): void {
|
|
107
|
+
if ("pixels" in target) {
|
|
108
|
+
scrollBox.scrollToPixels?.(target.pixels);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
scrollBox.scrollTo(target.cells);
|
|
112
|
+
if (scrollBox.scrollTop !== target.cells) {
|
|
113
|
+
scrollBox.scrollTop = target.cells;
|
|
114
|
+
}
|
|
71
115
|
}
|
|
72
116
|
|
|
73
117
|
/**
|
|
@@ -76,7 +120,7 @@ function scrollTargetByDelta(scrollBox: ScrollBoxRenderable, delta: number): num
|
|
|
76
120
|
*/
|
|
77
121
|
export function scrollByLines(scrollBox: ScrollBoxRenderable, delta: number): void {
|
|
78
122
|
const target = scrollTargetByDelta(scrollBox, delta);
|
|
79
|
-
if (target
|
|
123
|
+
if (target) applyScrollTarget(scrollBox, target);
|
|
80
124
|
}
|
|
81
125
|
|
|
82
126
|
function arrowScrollLines(scrollBox: ScrollBoxRenderable): number {
|
|
@@ -117,20 +161,9 @@ function resolveKeyScrollAction(event: PaneScrollKeyEvent): PaneScrollAction | n
|
|
|
117
161
|
),
|
|
118
162
|
};
|
|
119
163
|
case "home":
|
|
120
|
-
return {
|
|
121
|
-
direction: "up",
|
|
122
|
-
resolveTarget: (scrollBox) => (
|
|
123
|
-
scrollBox.scrollTop > 0 ? 0 : null
|
|
124
|
-
),
|
|
125
|
-
};
|
|
164
|
+
return { direction: "up", resolveTarget: topTarget };
|
|
126
165
|
case "end":
|
|
127
|
-
return {
|
|
128
|
-
direction: "down",
|
|
129
|
-
resolveTarget: (scrollBox) => {
|
|
130
|
-
const target = clampScrollTop(scrollBox, Number.MAX_SAFE_INTEGER);
|
|
131
|
-
return target == null || target === scrollBox.scrollTop ? null : target;
|
|
132
|
-
},
|
|
133
|
-
};
|
|
166
|
+
return { direction: "down", resolveTarget: bottomTarget };
|
|
134
167
|
default:
|
|
135
168
|
return null;
|
|
136
169
|
}
|
|
@@ -145,12 +178,12 @@ function getEntryScore(entry: PaneScrollEntry, scrollBox: ScrollBoxRenderable):
|
|
|
145
178
|
function findScrollableEntry(paneId: string, action: PaneScrollAction): {
|
|
146
179
|
entry: PaneScrollEntry;
|
|
147
180
|
scrollBox: ScrollBoxRenderable;
|
|
148
|
-
target:
|
|
181
|
+
target: ScrollTarget;
|
|
149
182
|
} | null {
|
|
150
183
|
const entries = paneScrollBoxes.get(paneId);
|
|
151
184
|
if (!entries) return null;
|
|
152
185
|
|
|
153
|
-
let best: { entry: PaneScrollEntry; scrollBox: ScrollBoxRenderable; target:
|
|
186
|
+
let best: { entry: PaneScrollEntry; scrollBox: ScrollBoxRenderable; target: ScrollTarget; score: number } | null = null;
|
|
154
187
|
for (const entry of entries.values()) {
|
|
155
188
|
const scrollBox = entry.ref.current;
|
|
156
189
|
if (!scrollBox) continue;
|
|
@@ -176,17 +209,16 @@ function scrollPaneByKey(paneId: string, event: PaneScrollKeyEvent): boolean {
|
|
|
176
209
|
const match = findScrollableEntry(paneId, action);
|
|
177
210
|
if (!match) return false;
|
|
178
211
|
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
212
|
+
const { scrollBox } = match;
|
|
213
|
+
const position = () => scrollBox.scrollTopPx ?? scrollBox.scrollTop;
|
|
214
|
+
const previousTop = scrollBox.scrollTop;
|
|
215
|
+
const previousPosition = position();
|
|
216
|
+
applyScrollTarget(scrollBox, match.target);
|
|
184
217
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return delta > 0;
|
|
218
|
+
if (position() === previousPosition) return false;
|
|
219
|
+
const delta = Math.max(1, Math.abs(scrollBox.scrollTop - previousTop));
|
|
220
|
+
match.entry.onScrollActivity?.({ scroll: { direction: action.direction, delta } });
|
|
221
|
+
return true;
|
|
190
222
|
}
|
|
191
223
|
|
|
192
224
|
export function useForwardedScrollBoxRef<T>(
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Quote, TickerFinancials } from "../types/financials";
|
|
2
|
+
import type { Portfolio, TickerRecord } from "../types/ticker";
|
|
3
|
+
import type { BrokerAccount } from "../types/trading";
|
|
4
|
+
import { createTestTicker } from "./ticker";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An invented margin account: 1.0M of equity carrying 1.6M of positions, the
|
|
8
|
+
* difference borrowed in three currencies. It holds a US stock, a listing
|
|
9
|
+
* quoted in euros and an option, which no quote prices, so it stays at its
|
|
10
|
+
* broker mark. Every figure is made up.
|
|
11
|
+
*/
|
|
12
|
+
export const MARGIN_PORTFOLIO: Portfolio = {
|
|
13
|
+
id: "broker:margin-test:M1",
|
|
14
|
+
name: "Margin test",
|
|
15
|
+
currency: "USD",
|
|
16
|
+
brokerId: "test",
|
|
17
|
+
brokerInstanceId: "margin-test",
|
|
18
|
+
brokerAccountId: "M1",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Dollars per unit of each currency. */
|
|
22
|
+
export const MARGIN_EXCHANGE_RATES = new Map([["USD", 1], ["EUR", 1.2], ["JPY", 0.008]]);
|
|
23
|
+
|
|
24
|
+
const OPTION = "CCC 280121C00050000";
|
|
25
|
+
|
|
26
|
+
function holding(symbol: string, details: {
|
|
27
|
+
exchange: string;
|
|
28
|
+
currency: string;
|
|
29
|
+
shares: number;
|
|
30
|
+
mark: number;
|
|
31
|
+
multiplier?: number;
|
|
32
|
+
assetCategory?: string;
|
|
33
|
+
}): TickerRecord {
|
|
34
|
+
return createTestTicker(symbol, symbol, {
|
|
35
|
+
exchange: details.exchange,
|
|
36
|
+
currency: details.currency,
|
|
37
|
+
assetCategory: details.assetCategory,
|
|
38
|
+
portfolios: [MARGIN_PORTFOLIO.id],
|
|
39
|
+
positions: [{
|
|
40
|
+
portfolio: MARGIN_PORTFOLIO.id,
|
|
41
|
+
shares: details.shares,
|
|
42
|
+
avgCost: details.mark,
|
|
43
|
+
markPrice: details.mark,
|
|
44
|
+
marketValue: details.shares * details.mark * (details.multiplier ?? 1),
|
|
45
|
+
unrealizedPnl: 0,
|
|
46
|
+
multiplier: details.multiplier,
|
|
47
|
+
currency: details.currency,
|
|
48
|
+
side: "long",
|
|
49
|
+
broker: "test",
|
|
50
|
+
brokerInstanceId: "margin-test",
|
|
51
|
+
brokerAccountId: "M1",
|
|
52
|
+
}],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const MARGIN_TICKERS: TickerRecord[] = [
|
|
57
|
+
holding("AAA", { exchange: "NASDAQ", currency: "USD", shares: 4_000, mark: 200 }),
|
|
58
|
+
holding("BBB", { exchange: "XETRA", currency: "EUR", shares: 5_000, mark: 100 }),
|
|
59
|
+
holding(OPTION, { exchange: "", currency: "USD", shares: 20, mark: 100, multiplier: 100, assetCategory: "OPT" }),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export function createMarginAccount(overrides: Partial<BrokerAccount> = {}): BrokerAccount {
|
|
63
|
+
return {
|
|
64
|
+
accountId: "M1",
|
|
65
|
+
name: "Margin test",
|
|
66
|
+
currency: "USD",
|
|
67
|
+
source: "cloud",
|
|
68
|
+
updatedAt: Date.now() - 60_000,
|
|
69
|
+
netLiquidation: 1_000_000,
|
|
70
|
+
grossPositionValue: 1_600_000,
|
|
71
|
+
totalCashValue: -600_000,
|
|
72
|
+
cashBalances: [
|
|
73
|
+
{ currency: "EUR", quantity: -250_000, baseValue: -300_000, baseCurrency: "USD" },
|
|
74
|
+
{ currency: "JPY", quantity: -25_000_000, baseValue: -200_000, baseCurrency: "USD" },
|
|
75
|
+
{ currency: "USD", quantity: -100_000, baseValue: -100_000, baseCurrency: "USD" },
|
|
76
|
+
],
|
|
77
|
+
...overrides,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function liveQuote(symbol: string, price: number, previousClose: number, currency: string, exchange: string): TickerFinancials {
|
|
82
|
+
const quote: Quote = {
|
|
83
|
+
symbol,
|
|
84
|
+
price,
|
|
85
|
+
change: price - previousClose,
|
|
86
|
+
changePercent: (price / previousClose - 1) * 100,
|
|
87
|
+
previousClose,
|
|
88
|
+
currency,
|
|
89
|
+
lastUpdated: Date.now() - 1_000,
|
|
90
|
+
listingExchangeName: exchange,
|
|
91
|
+
marketState: "REGULAR",
|
|
92
|
+
dataSource: "live",
|
|
93
|
+
};
|
|
94
|
+
return { annualStatements: [], quarterlyStatements: [], priceHistory: [], quote };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Real-time quotes for the stock and the euro listing, the stock `move`
|
|
99
|
+
* dollars above its mark. With `option`, the option is quoted at its mark too,
|
|
100
|
+
* so every position has a live price.
|
|
101
|
+
*/
|
|
102
|
+
export function marginQuotes({ move = 0, option = false }: { move?: number; option?: boolean } = {}): Map<string, TickerFinancials> {
|
|
103
|
+
return new Map([
|
|
104
|
+
["AAA", liveQuote("AAA", 200 + move, 200, "USD", "NASDAQ")],
|
|
105
|
+
["BBB", liveQuote("BBB", 100, 100, "EUR", "XETRA")],
|
|
106
|
+
...(option ? [[OPTION, liveQuote(OPTION, 100, 100, "USD", "CBOE")] as const] : []),
|
|
107
|
+
]);
|
|
108
|
+
}
|
|
@@ -37,6 +37,21 @@ const RANGE_PRESET_RESOLUTION: Record<TimeRange, ManualChartResolution> = {
|
|
|
37
37
|
"ALL": "1mo",
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The bar size a price history for a range is served in, unlike the chart
|
|
42
|
+
* presets above: what `gloomberb history` and the historical prices pane get.
|
|
43
|
+
*/
|
|
44
|
+
export const RANGE_HISTORY_RESOLUTION: Readonly<Record<TimeRange, ManualChartResolution>> = {
|
|
45
|
+
"1D": "5m",
|
|
46
|
+
"1W": "1h",
|
|
47
|
+
"1M": "1d",
|
|
48
|
+
"3M": "1d",
|
|
49
|
+
"6M": "1d",
|
|
50
|
+
"1Y": "1d",
|
|
51
|
+
"5Y": "1wk",
|
|
52
|
+
"ALL": "1mo",
|
|
53
|
+
};
|
|
54
|
+
|
|
40
55
|
const RANGE_PRELOAD_BUFFER: Record<TimeRange, TimeRange> = {
|
|
41
56
|
"1D": "1W",
|
|
42
57
|
"1W": "1M",
|
package/src/types/financials.ts
CHANGED
|
@@ -38,7 +38,21 @@ export interface QuoteMetadata {
|
|
|
38
38
|
fieldSources?: Partial<Record<"currency" | "instrumentType", QuoteMetadataSource>>;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* What a record's share counts count: depositary receipts or the ordinary
|
|
43
|
+
* shares behind them. Gloom Cloud states it only when it established it.
|
|
44
|
+
*/
|
|
45
|
+
export type ShareBasis = "depositary_receipt" | "ordinary";
|
|
46
|
+
|
|
47
|
+
/** How a depositary receipt relates to the ordinary shares, as Gloom Cloud states it. */
|
|
48
|
+
interface DepositaryReceiptFacts {
|
|
49
|
+
/** Whether the listing is a depositary receipt; absent when unknown. */
|
|
50
|
+
isDepositaryReceipt?: boolean;
|
|
51
|
+
/** Ordinary shares per receipt, on a receipt's own records. */
|
|
52
|
+
adrRatio?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface Quote extends DepositaryReceiptFacts {
|
|
42
56
|
/** Applies to this price observation; a stored position cannot supply it. */
|
|
43
57
|
priceBasis?: PriceBasis;
|
|
44
58
|
/** Daily fund net asset value; independent of the units used to format its price. */
|
|
@@ -109,7 +123,11 @@ export interface QuoteContribution extends Quote {
|
|
|
109
123
|
|
|
110
124
|
export type QuoteContributionMap = Record<string, QuoteContribution>;
|
|
111
125
|
|
|
112
|
-
export interface Fundamentals {
|
|
126
|
+
export interface Fundamentals extends DepositaryReceiptFacts {
|
|
127
|
+
/** Every share count here is on this basis, the basis of the listing's price and market cap; absent when unproven. */
|
|
128
|
+
shareBasis?: ShareBasis;
|
|
129
|
+
/** On a receipt, the ordinary shares behind `sharesOutstanding`. */
|
|
130
|
+
underlyingOrdinaryShares?: number;
|
|
113
131
|
source?: "gloom";
|
|
114
132
|
fetchedAt?: string;
|
|
115
133
|
stale?: boolean;
|
|
@@ -180,6 +198,8 @@ export interface HolderRecord {
|
|
|
180
198
|
percentHeld?: number;
|
|
181
199
|
changeShares?: number;
|
|
182
200
|
changePercent?: number;
|
|
201
|
+
/** Set when the position's filing ties it to the receipts or to the ordinary shares. */
|
|
202
|
+
shareBasis?: ShareBasis;
|
|
183
203
|
}
|
|
184
204
|
|
|
185
205
|
interface HolderSummary {
|
|
@@ -189,11 +209,18 @@ interface HolderSummary {
|
|
|
189
209
|
institutionsCount?: number;
|
|
190
210
|
}
|
|
191
211
|
|
|
192
|
-
export interface HolderData {
|
|
212
|
+
export interface HolderData extends DepositaryReceiptFacts {
|
|
193
213
|
providerId?: string;
|
|
194
214
|
symbol: string;
|
|
195
215
|
name?: string;
|
|
216
|
+
/** The listing's price currency. */
|
|
196
217
|
currency?: string;
|
|
218
|
+
/** The currency of `value`, which can differ from the listing's (USD on a London line priced from 13F filings). */
|
|
219
|
+
valueCurrency?: string;
|
|
220
|
+
/** Reported shares times the latest price, or times the price at the report date. */
|
|
221
|
+
valueBasis?: "latest_price" | "period_end_price";
|
|
222
|
+
/** Set when every row is on one basis. */
|
|
223
|
+
shareBasis?: ShareBasis;
|
|
197
224
|
exchange?: string;
|
|
198
225
|
asOf?: string;
|
|
199
226
|
summary?: HolderSummary;
|
|
@@ -430,6 +457,8 @@ export interface ReportedEarningsCohort {
|
|
|
430
457
|
export interface EarningsResultProvenance { version: 1; reported: ReportedEarningsCohort }
|
|
431
458
|
|
|
432
459
|
export interface FinancialStatement {
|
|
460
|
+
/** The basis of this row's share counts and per-share earnings; absent when unproven. */
|
|
461
|
+
shareBasis?: ShareBasis;
|
|
433
462
|
/** @deprecated No longer populated. */
|
|
434
463
|
earningsResult?: EarningsResultProvenance;
|
|
435
464
|
/** EPS lines unavailable for this period, for example because they disagree with the row's own income and shares. */
|
package/src/ui/host.tsx
CHANGED
|
@@ -108,6 +108,12 @@ export interface SyntaxStyleLike {
|
|
|
108
108
|
}): number;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/** Where a drag has a box this frame: an offset in cells from where it is laid out. */
|
|
112
|
+
export interface LiveBoxFrame {
|
|
113
|
+
dx: number;
|
|
114
|
+
dy: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
export interface BoxRenderable {
|
|
112
118
|
x?: number;
|
|
113
119
|
y?: number;
|
|
@@ -117,6 +123,12 @@ export interface BoxRenderable {
|
|
|
117
123
|
absoluteY?: number;
|
|
118
124
|
absoluteBounds?: { x: number; y: number; width: number; height: number };
|
|
119
125
|
getBoundingClientRect?: () => { x: number; y: number; width: number; height: number };
|
|
126
|
+
/**
|
|
127
|
+
* Desktop only: draws the box at `frame` by restyling its own element, with
|
|
128
|
+
* no render, until it is called with null. The offset rides the compositor,
|
|
129
|
+
* so it costs no layout or repaint.
|
|
130
|
+
*/
|
|
131
|
+
setLiveFrame?: (frame: LiveBoxFrame | null) => void;
|
|
120
132
|
[key: string]: unknown;
|
|
121
133
|
}
|
|
122
134
|
|