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
package/src/ui/index.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createElement, forwardRef, useCallback, useRef, type ComponentProps } from "react";
|
|
2
2
|
import { useForwardedScrollBoxRef, useRegisterPaneScrollBox } from "../state/pane-scroll-registry";
|
|
3
3
|
import { useUiHost, type UiHost } from "./host";
|
|
4
|
+
import { scrollBoxScrollsVertically } from "./scroll-box-axis";
|
|
4
5
|
import { useRemoteUiNode } from "../remote/semantic-tree";
|
|
5
6
|
import { assignRef } from "../react/assign-ref";
|
|
6
7
|
import {
|
|
@@ -48,6 +49,7 @@ export type {
|
|
|
48
49
|
MediaSurfaceHandle,
|
|
49
50
|
MediaSurfaceProps,
|
|
50
51
|
InputRenderable,
|
|
52
|
+
LiveBoxFrame,
|
|
51
53
|
NativeCursorState,
|
|
52
54
|
NativeRendererHost,
|
|
53
55
|
NativePostProcessFn,
|
|
@@ -128,8 +130,11 @@ export const ScrollBox = forwardRef<any, ComponentProps<UiHost["ScrollBox"]>>((p
|
|
|
128
130
|
const { ScrollBox: HostScrollBox } = useUiHost();
|
|
129
131
|
const localRef = useForwardedScrollBoxRef<any>(ref);
|
|
130
132
|
const rawProps = props as Record<string, unknown>;
|
|
133
|
+
// The same rule the hosts scroll by: a ScrollBox that omits scrollY scrolls,
|
|
134
|
+
// so it is a scrollbox to the remote tree and to the pane scroll keys.
|
|
135
|
+
const scrollsVertically = scrollBoxScrollsVertically(props);
|
|
131
136
|
const remoteNodeId = useRemoteUiNode(
|
|
132
|
-
|
|
137
|
+
scrollsVertically
|
|
133
138
|
? {
|
|
134
139
|
role: remotePropRole(rawProps, "scrollbox"),
|
|
135
140
|
label: remotePropLabel(rawProps),
|
|
@@ -150,7 +155,7 @@ export const ScrollBox = forwardRef<any, ComponentProps<UiHost["ScrollBox"]>>((p
|
|
|
150
155
|
...remoteMetadataFromProps(rawProps),
|
|
151
156
|
width: rawProps.width,
|
|
152
157
|
height: rawProps.height,
|
|
153
|
-
scrollY:
|
|
158
|
+
scrollY: true,
|
|
154
159
|
scrollTop: localRef.current?.scrollTop,
|
|
155
160
|
viewportHeight: localRef.current?.viewport?.height,
|
|
156
161
|
},
|
|
@@ -158,7 +163,7 @@ export const ScrollBox = forwardRef<any, ComponentProps<UiHost["ScrollBox"]>>((p
|
|
|
158
163
|
: null,
|
|
159
164
|
);
|
|
160
165
|
useRegisterPaneScrollBox(localRef, {
|
|
161
|
-
enabled:
|
|
166
|
+
enabled: scrollsVertically,
|
|
162
167
|
onScrollActivity: typeof props.onMouseScroll === "function"
|
|
163
168
|
? props.onMouseScroll as (event: { scroll: { direction: "up" | "down"; delta: number } }) => void
|
|
164
169
|
: undefined,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which axes a ScrollBox scrolls on, decided from its props. The terminal and
|
|
3
|
+
* DOM hosts and the pane keyboard scroll registration all read it here, so a
|
|
4
|
+
* ScrollBox that omits `scrollY` behaves the same everywhere: it scrolls
|
|
5
|
+
* vertically, with the wheel and with the arrows, PageUp, PageDown, Home and
|
|
6
|
+
* End. Pass `scrollY={false}` to turn that off.
|
|
7
|
+
*/
|
|
8
|
+
interface ScrollBoxAxisProps {
|
|
9
|
+
scrollX?: unknown;
|
|
10
|
+
scrollY?: unknown;
|
|
11
|
+
height?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A one-row strip that only scrolls sideways: tab bars, table headers and
|
|
16
|
+
* chart legends. It is not a vertical scroller even though `scrollY` is
|
|
17
|
+
* omitted.
|
|
18
|
+
*/
|
|
19
|
+
export function isHeaderStripScrollBox(props: ScrollBoxAxisProps): boolean {
|
|
20
|
+
return props.scrollX === true && props.scrollY !== true && props.height === 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Whether the ScrollBox scrolls vertically: unless `scrollY` is false, and never a header strip. */
|
|
24
|
+
export function scrollBoxScrollsVertically(props: ScrollBoxAxisProps): boolean {
|
|
25
|
+
return props.scrollY !== false && !isHeaderStripScrollBox(props);
|
|
26
|
+
}
|
|
@@ -13,11 +13,16 @@ const SUB_UNIT_NAMES: Record<string, string> = {
|
|
|
13
13
|
ZAc: "cents",
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* A currency code as a reader should see it; a sub-unit says which one,
|
|
18
|
+
* "GBp (pence)", and with `ofMajor` what it divides: "ILA (agorot, 1/100 ILS)".
|
|
19
|
+
*/
|
|
20
|
+
export function currencyUnitLabel(code?: string | null, { ofMajor = false }: { ofMajor?: boolean } = {}): string {
|
|
18
21
|
const raw = (code ?? "").trim();
|
|
19
22
|
const unit = SUB_UNIT_NAMES[raw];
|
|
20
|
-
|
|
23
|
+
const major = SUB_UNIT_CURRENCIES[raw];
|
|
24
|
+
if (!unit) return raw;
|
|
25
|
+
return ofMajor && major ? `${raw} (${unit}, 1/${major.divisor} ${major.currency})` : `${raw} (${unit})`;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
const LIKELY_UNIT_MISMATCH_RATIOS: Record<string, number[]> = {
|
|
@@ -1,26 +1,70 @@
|
|
|
1
|
+
import type { Fundamentals, Quote, ShareBasis } from "../types/financials";
|
|
2
|
+
|
|
1
3
|
// A word of its own, so a company called "ADS-TEC" is not one.
|
|
2
4
|
const DEPOSITARY_NAME = /(?:^|\s)(?:ADRs?|ADSs?|American depositary (?:receipts?|shares?))(?=$|[\s,)])/i;
|
|
3
5
|
const DEPOSITARY_TYPES = new Set(["ADR", "ADS", "DEPOSITARYRECEIPT"]);
|
|
4
6
|
/** A profile that opens by naming the security: "Toyota Motor Corporation Sponsored ADR is ...". */
|
|
5
7
|
const DEPOSITARY_SUBJECT = /^.{0,160}?\s(?:ADRs?|ADSs?|American depositary (?:receipts?|shares?)) is\s/i;
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* name, and the security name a profile description opens with. Its share
|
|
11
|
-
* count and market cap are then counted in receipts, not ordinary shares. A
|
|
12
|
-
* listing in another currency is the ordinary line even when its profile
|
|
13
|
-
* mentions the receipts.
|
|
14
|
-
*/
|
|
15
|
-
export function isDepositaryReceipt(input: {
|
|
9
|
+
interface ListingEvidence {
|
|
10
|
+
/** Gloom Cloud's verdict; absent when it has no evidence either way. */
|
|
11
|
+
isDepositaryReceipt?: boolean | null;
|
|
16
12
|
instrumentType?: string | null;
|
|
17
13
|
name?: string | null;
|
|
18
14
|
currency?: string | null;
|
|
19
15
|
description?: string | null;
|
|
20
|
-
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether a listing is a depositary receipt. Gloom Cloud's flag decides when
|
|
20
|
+
* it is set. Without it (an older service, or a listing it has no evidence
|
|
21
|
+
* for) this reads the instrument type, the name, and the security name a
|
|
22
|
+
* profile description opens with. A listing in another currency is the
|
|
23
|
+
* ordinary line even when its profile mentions the receipts.
|
|
24
|
+
*/
|
|
25
|
+
export function isDepositaryReceipt(input: ListingEvidence): boolean {
|
|
26
|
+
if (typeof input.isDepositaryReceipt === "boolean") return input.isDepositaryReceipt;
|
|
21
27
|
const currency = input.currency?.trim().toUpperCase();
|
|
22
28
|
if (currency && currency !== "USD") return false;
|
|
23
29
|
if (DEPOSITARY_TYPES.has((input.instrumentType ?? "").replace(/[\s_-]/g, "").toUpperCase())) return true;
|
|
24
30
|
if (input.name && DEPOSITARY_NAME.test(input.name)) return true;
|
|
25
31
|
return DEPOSITARY_SUBJECT.test(input.description?.trim() ?? "");
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a record's share counts are in receipts. Gloom Cloud says so with
|
|
36
|
+
* `shareBasis`; without it, a receipt's counts are read as receipts, as its
|
|
37
|
+
* market cap is.
|
|
38
|
+
*/
|
|
39
|
+
function sharesInReceipts(input: ListingEvidence & { shareBasis?: ShareBasis | null }): boolean {
|
|
40
|
+
if (input.shareBasis === "depositary_receipt") return true;
|
|
41
|
+
if (input.shareBasis === "ordinary") return false;
|
|
42
|
+
return isDepositaryReceipt(input);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether a listing's shares outstanding are counted in receipts: the
|
|
47
|
+
* fundamentals' basis, else the listing's flag (the fundamentals' or the
|
|
48
|
+
* quote's), else what the quote and profile say.
|
|
49
|
+
*/
|
|
50
|
+
export function sharesOutstandingInReceipts(
|
|
51
|
+
quote: Pick<Quote, "isDepositaryReceipt" | "instrumentType" | "name" | "currency"> | null | undefined,
|
|
52
|
+
fundamentals: Pick<Fundamentals, "isDepositaryReceipt" | "shareBasis"> | null | undefined,
|
|
53
|
+
description?: string | null,
|
|
54
|
+
): boolean {
|
|
55
|
+
return sharesInReceipts({
|
|
56
|
+
instrumentType: quote?.instrumentType,
|
|
57
|
+
name: quote?.name,
|
|
58
|
+
currency: quote?.currency,
|
|
59
|
+
description,
|
|
60
|
+
isDepositaryReceipt: fundamentals?.isDepositaryReceipt ?? quote?.isDepositaryReceipt,
|
|
61
|
+
shareBasis: fundamentals?.shareBasis,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** "1 ADR = 2 ordinary shares", or null without a usable ratio. */
|
|
66
|
+
export function adrRatioText(ratio: number | null | undefined): string | null {
|
|
67
|
+
if (typeof ratio !== "number" || !Number.isFinite(ratio) || ratio <= 0) return null;
|
|
68
|
+
const count = Number.isInteger(ratio) ? String(ratio) : String(Number(ratio.toPrecision(6)));
|
|
69
|
+
return `1 ADR = ${count} ordinary share${ratio === 1 ? "" : "s"}`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading back a number a table or report drew ("+1.25%", "1.20B", "$1,234.50"),
|
|
3
|
+
* so a CSV export can write the number a spreadsheet uses and name the unit
|
|
4
|
+
* in its header instead of in every cell.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** What a cell draws for a missing value: a dash, a run of dashes, or the loading ellipsis. */
|
|
8
|
+
export const DISPLAY_PLACEHOLDER = /^(?:[-\u2013\u2014]{1,3}|\u2026|\.\.\.)$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A number as a table draws it: sign, currency symbol, thousands commas,
|
|
12
|
+
* decimals, a compact scale (12.3k, 1.20B) and a unit (%, x, bp, USD, or c
|
|
13
|
+
* for the cents grain futures quote in).
|
|
14
|
+
*/
|
|
15
|
+
const DISPLAY_NUMBER = /^([+-]?)([A-Z]{0,3}[$\u20ac\u00a3\u00a5\u20b9\u20a9\u20bd\u20ba\u20aa\u20ab\u0e3f])?(?=\.?\d)(\d{1,3}(?:,\d{3})+|\d*)(?:\.(\d+))?([kKMBT])?(%|x|c|\u00a2| ?bps?| [A-Z]{3})?$/;
|
|
16
|
+
|
|
17
|
+
const SCALE_DIGITS: Record<string, number> = { k: 3, K: 3, M: 6, B: 9, T: 12 };
|
|
18
|
+
|
|
19
|
+
export interface DisplayNumber {
|
|
20
|
+
/** Plain decimal text: no sign for positives, no commas, no scale letter. */
|
|
21
|
+
number: string;
|
|
22
|
+
/** What the column header has to name once the cells are bare numbers. */
|
|
23
|
+
unit: string;
|
|
24
|
+
/** The place value of the last digit drawn: 0.01 for "1.25%", 10,000,000 for "1.20B". */
|
|
25
|
+
step: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const BLANK_DISPLAY = "blank";
|
|
29
|
+
|
|
30
|
+
/** Moves the decimal point right without going through a float, so 1.23B is exactly 1230000000. */
|
|
31
|
+
function scaleDecimal(integer: string, fraction: string, digits: number): string {
|
|
32
|
+
const whole = `${integer}${fraction.padEnd(digits, "0").slice(0, digits)}`.replace(/^0+(?=\d)/, "") || "0";
|
|
33
|
+
const rest = fraction.slice(digits);
|
|
34
|
+
return rest ? `${whole}.${rest}` : whole;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseDisplayNumber(text: string): DisplayNumber | typeof BLANK_DISPLAY | null {
|
|
38
|
+
// A direction arrow beside a signed move repeats the sign.
|
|
39
|
+
const trimmed = text.trim().replace(/\u2212/g, "-").replace(/\s*[\u2191\u2193]$/, "");
|
|
40
|
+
if (!trimmed || DISPLAY_PLACEHOLDER.test(trimmed)) return BLANK_DISPLAY;
|
|
41
|
+
const match = DISPLAY_NUMBER.exec(trimmed);
|
|
42
|
+
if (!match) return null;
|
|
43
|
+
const [, sign, currency = "", grouped, fraction = "", scale, unitText = ""] = match;
|
|
44
|
+
const integer = grouped!.replace(/,/g, "") || "0";
|
|
45
|
+
const digits = scale ? SCALE_DIGITS[scale]! : 0;
|
|
46
|
+
const magnitude = digits ? scaleDecimal(integer, fraction, digits) : fraction ? `${integer}.${fraction}` : integer;
|
|
47
|
+
const unit = unitText.trim().replace(/^bps$/, "bp").replace(/^\u00a2$/, "c");
|
|
48
|
+
return {
|
|
49
|
+
number: `${sign === "-" ? "-" : ""}${magnitude}`,
|
|
50
|
+
unit: [currency, unit].filter(Boolean).join(" "),
|
|
51
|
+
step: 10 ** (digits - fraction.length),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Adds the unit the cells dropped, unless the label already names it. */
|
|
56
|
+
export function labelWithUnit(label: string, unit: string): string {
|
|
57
|
+
if (!unit) return label;
|
|
58
|
+
// A word unit has to stand alone: "Max" does not name x.
|
|
59
|
+
const named = unit === "bp" ? /\bbps?\b/i.test(label)
|
|
60
|
+
: /^[A-Za-z]+$/.test(unit) ? new RegExp(`\\b${unit}\\b`, "i").test(label)
|
|
61
|
+
: label.includes(unit);
|
|
62
|
+
return named ? label : `${label} (${unit})`;
|
|
63
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Fundamentals } from "../types/financials";
|
|
1
|
+
import type { Fundamentals, TickerFinancials } from "../types/financials";
|
|
2
2
|
|
|
3
3
|
export const RETRACTABLE_VALUATION_FIELDS = ["enterpriseValue", "enterpriseToRevenue"] as const;
|
|
4
4
|
|
|
@@ -13,3 +13,19 @@ export function redactUnavailableFundamentals(value: Fundamentals | undefined):
|
|
|
13
13
|
for (const field of unavailableFields) delete result[field];
|
|
14
14
|
return result;
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
/** A source's enterprise value, or undefined when it has none: banks and insurers come back as 0, which is a gap, not a value. */
|
|
18
|
+
export function reportedEnterpriseValue(fundamentals: Pick<Fundamentals, "enterpriseValue"> | undefined): number | undefined {
|
|
19
|
+
const value = fundamentals?.enterpriseValue;
|
|
20
|
+
return typeof value === "number" && Number.isFinite(value) && value !== 0 ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The currency of the trailing EPS, revenue, income and cash flows: the one the fundamentals declare, else the
|
|
25
|
+
* one the statements report in. Never the quote's, which a depositary receipt can differ from (USD quote, TWD books).
|
|
26
|
+
*/
|
|
27
|
+
export function fundamentalsCurrency(
|
|
28
|
+
financials: Pick<TickerFinancials, "fundamentals" | "financialCurrency"> | null | undefined,
|
|
29
|
+
): string | undefined {
|
|
30
|
+
return financials?.fundamentals?.financialCurrency?.trim() || financials?.financialCurrency?.trim() || undefined;
|
|
31
|
+
}
|
|
@@ -27,13 +27,26 @@ function above(left: number, right: number): boolean {
|
|
|
27
27
|
return left - right > 8 * Number.EPSILON * Math.max(Math.abs(left), Math.abs(right));
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Each way a bar's prices contradict each other, as short codes such as
|
|
32
|
+
* `high<open` or `low>close`; empty when they agree. Missing OHLC fields are
|
|
33
|
+
* not contradictory: close-only histories remain usable.
|
|
34
|
+
*/
|
|
35
|
+
export function pricePointIntegrityFlags(point: PricePoint): string[] {
|
|
32
36
|
const { open, high, low, close } = point;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
const flags: string[] = [];
|
|
38
|
+
for (const [name, value] of [["open", open], ["close", close]] as const) {
|
|
39
|
+
if (isFiniteNumber(value) && isFiniteNumber(high) && above(value, high)) flags.push(`high<${name}`);
|
|
40
|
+
}
|
|
41
|
+
for (const [name, value] of [["open", open], ["close", close]] as const) {
|
|
42
|
+
if (isFiniteNumber(value) && isFiniteNumber(low) && above(low, value)) flags.push(`low>${name}`);
|
|
43
|
+
}
|
|
44
|
+
if (isFiniteNumber(high) && isFiniteNumber(low) && above(low, high)) flags.push("low>high");
|
|
45
|
+
return flags;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function pricePointIntegrity(point: PricePoint): PriceHistoryIntegrity | undefined {
|
|
49
|
+
if (pricePointIntegrityFlags(point).length === 0) return undefined;
|
|
37
50
|
const date = new Date(point.date);
|
|
38
51
|
return Object.freeze({
|
|
39
52
|
reason: "inconsistent-ohlc",
|