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/cli/history-rows.ts
CHANGED
|
@@ -1,19 +1,52 @@
|
|
|
1
1
|
import type { PricePoint } from "../types/financials";
|
|
2
2
|
import { marketPriceFractionDigitCeiling, resolveAssetDisplayKind } from "../market-data/market/format";
|
|
3
|
-
import {
|
|
3
|
+
import { TIME_RANGES, type TimeRange } from "../time-series/range";
|
|
4
|
+
import { subtractTimeRange } from "../time-series/date-window";
|
|
5
|
+
import {
|
|
6
|
+
CHART_RESOLUTION_STEP_MS,
|
|
7
|
+
isIntradayResolution,
|
|
8
|
+
RANGE_HISTORY_RESOLUTION,
|
|
9
|
+
type ManualChartResolution,
|
|
10
|
+
} from "../time-series/resolution";
|
|
11
|
+
import { currencyUnitLabel } from "../utils/currency-units";
|
|
4
12
|
import { getPricePointTimestamp } from "../utils/price-history";
|
|
13
|
+
import { pricePointIntegrityFlags, pricePointValues } from "../utils/price-history-integrity";
|
|
14
|
+
import { formatUtcTime } from "../utils/utc-time";
|
|
5
15
|
|
|
6
16
|
const PRICE_KEYS = ["open", "high", "low", "close"] as const;
|
|
17
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
7
18
|
/** Daily bars are a day apart even when stamped at the session open. */
|
|
8
19
|
const INTRADAY_MAX_GAP_MS = 20 * 60 * 60 * 1000;
|
|
20
|
+
const INTRADAY_RESOLUTIONS = ["1m", "5m", "15m", "30m", "45m", "1h"] as const;
|
|
21
|
+
/** A first bar later than this after the range's start is a late start, not a weekend or holiday. */
|
|
22
|
+
const LATE_START_MIN_MS = 10 * DAY_MS;
|
|
23
|
+
const LATE_START_MIN_BARS = 3;
|
|
9
24
|
|
|
10
|
-
|
|
25
|
+
const BAR_INTERVAL_LABELS: Record<ManualChartResolution, string> = {
|
|
26
|
+
"1m": "1-minute",
|
|
27
|
+
"5m": "5-minute",
|
|
28
|
+
"15m": "15-minute",
|
|
29
|
+
"30m": "30-minute",
|
|
30
|
+
"45m": "45-minute",
|
|
31
|
+
"1h": "hourly",
|
|
32
|
+
"1d": "daily",
|
|
33
|
+
"1wk": "weekly",
|
|
34
|
+
"1mo": "monthly",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface HistoryRow {
|
|
11
38
|
date: string;
|
|
12
39
|
open: number | null;
|
|
13
40
|
high: number | null;
|
|
14
41
|
low: number | null;
|
|
15
|
-
close: number;
|
|
42
|
+
close: number | null;
|
|
16
43
|
volume: number | null;
|
|
44
|
+
/** Currency code of the prices; null for index points or when no source says. */
|
|
45
|
+
currency: string | null;
|
|
46
|
+
/** Bar size, in the codes of `resolution`: 5m, 1h, 1d, 1wk, 1mo. */
|
|
47
|
+
interval: ManualChartResolution | null;
|
|
48
|
+
/** Each way the bar's prices contradict each other, such as `high<open`; null when they agree. */
|
|
49
|
+
flag: string | null;
|
|
17
50
|
}
|
|
18
51
|
|
|
19
52
|
function decimalPlaces(value: number): number {
|
|
@@ -43,29 +76,63 @@ function isUtcMidnight(time: number): boolean {
|
|
|
43
76
|
return new Date(time).toISOString().endsWith("T00:00:00.000Z");
|
|
44
77
|
}
|
|
45
78
|
|
|
79
|
+
function medianGap(times: readonly number[]): number | null {
|
|
80
|
+
const finite = times.filter(Number.isFinite).sort((left, right) => left - right);
|
|
81
|
+
const gaps = finite.slice(1).map((time, index) => time - finite[index]!).filter((gap) => gap > 0).sort((left, right) => left - right);
|
|
82
|
+
return gaps.length > 0 ? gaps[Math.floor((gaps.length - 1) / 2)]! : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
46
85
|
/** Without a declared cadence, bar spacing decides: daily bars may be stamped at the session open. */
|
|
47
86
|
function looksIntraday(times: readonly number[]): boolean {
|
|
48
|
-
const finite = times.filter(Number.isFinite)
|
|
87
|
+
const finite = times.filter(Number.isFinite);
|
|
49
88
|
if (finite.length < 2) return finite.some((time) => !isUtcMidnight(time));
|
|
50
|
-
const
|
|
51
|
-
return
|
|
89
|
+
const gap = medianGap(finite);
|
|
90
|
+
return gap != null && gap < INTRADAY_MAX_GAP_MS;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The bar size served: as the source declared it, otherwise read from the typical spacing of the bars. */
|
|
94
|
+
function historyInterval(times: readonly number[], resolution: ManualChartResolution | null): ManualChartResolution | null {
|
|
95
|
+
if (resolution) return resolution;
|
|
96
|
+
const gap = medianGap(times);
|
|
97
|
+
if (gap == null) return null;
|
|
98
|
+
if (gap < INTRADAY_MAX_GAP_MS) {
|
|
99
|
+
return INTRADAY_RESOLUTIONS.reduce((best, candidate) => (
|
|
100
|
+
Math.abs(CHART_RESOLUTION_STEP_MS[candidate] - gap) < Math.abs(CHART_RESOLUTION_STEP_MS[best] - gap) ? candidate : best
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
const days = gap / DAY_MS;
|
|
104
|
+
return days >= 25 ? "1mo" : days >= 6 ? "1wk" : "1d";
|
|
52
105
|
}
|
|
53
106
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Intraday bars keep their UTC time; daily and longer bars are trading dates.
|
|
109
|
+
* A bar whose prices contradict each other is blanked exactly as HP blanks it,
|
|
110
|
+
* and says why in `flag`.
|
|
111
|
+
*/
|
|
112
|
+
export function historyRows(
|
|
113
|
+
points: readonly PricePoint[],
|
|
114
|
+
resolution: ManualChartResolution | null,
|
|
115
|
+
currency: string | null = null,
|
|
116
|
+
): HistoryRow[] {
|
|
56
117
|
const times = points.map(getPricePointTimestamp);
|
|
57
118
|
const intraday = resolution ? isIntradayResolution(resolution) : looksIntraday(times);
|
|
119
|
+
const interval = historyInterval(times, resolution);
|
|
58
120
|
return points.map((point, index) => {
|
|
59
121
|
const time = times[index]!;
|
|
60
122
|
const iso = Number.isFinite(time) ? new Date(time).toISOString() : "";
|
|
61
|
-
const
|
|
123
|
+
const values = pricePointValues(point);
|
|
124
|
+
const flags = pricePointIntegrityFlags(point);
|
|
125
|
+
const price = (value: number | null) => value == null ? null : cleanFloat32Price(value);
|
|
62
126
|
return {
|
|
63
127
|
date: intraday ? iso.replace(".000Z", "Z") : iso.slice(0, 10),
|
|
64
|
-
open: price(
|
|
65
|
-
high: price(
|
|
66
|
-
low: price(
|
|
67
|
-
close:
|
|
68
|
-
volume:
|
|
128
|
+
open: price(values.open),
|
|
129
|
+
high: price(values.high),
|
|
130
|
+
low: price(values.low),
|
|
131
|
+
close: price(values.close),
|
|
132
|
+
volume: values.volume,
|
|
133
|
+
currency,
|
|
134
|
+
interval,
|
|
135
|
+
flag: flags.length > 0 ? flags.join(",") : null,
|
|
69
136
|
};
|
|
70
137
|
});
|
|
71
138
|
}
|
|
@@ -84,7 +151,7 @@ function neededDecimals(value: number, cap: number): number {
|
|
|
84
151
|
* one, the largest price sets a seven significant digit ceiling (BTC keeps
|
|
85
152
|
* cents, FX keeps pips, sub-cent coins keep their digits). Cents at least above 1.
|
|
86
153
|
*/
|
|
87
|
-
export function historyPriceDecimals(rows: readonly HistoryRow[], assetCategory?: string): number {
|
|
154
|
+
export function historyPriceDecimals(rows: readonly Pick<HistoryRow, typeof PRICE_KEYS[number]>[], assetCategory?: string): number {
|
|
88
155
|
const values = rows.flatMap((row) => PRICE_KEYS.flatMap((key) => {
|
|
89
156
|
const value = row[key];
|
|
90
157
|
return value != null && Number.isFinite(value) && value !== 0 ? [Math.abs(value)] : [];
|
|
@@ -100,3 +167,108 @@ export function historyPriceDecimals(rows: readonly HistoryRow[], assetCategory?
|
|
|
100
167
|
const decimals = needed[Math.floor(0.9 * (needed.length - 1))]!;
|
|
101
168
|
return largest >= 1 ? Math.max(2, decimals) : decimals;
|
|
102
169
|
}
|
|
170
|
+
|
|
171
|
+
/** How far back ALL reaches, in whole years, from the window a history request for it asks for. */
|
|
172
|
+
export function historyAllRangeYears(now = new Date()): number {
|
|
173
|
+
return Math.round((now.getTime() - subtractTimeRange(now, "ALL").getTime()) / (365.25 * DAY_MS));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** "1D 5-minute, 1W hourly, 1M to 1Y daily, 5Y weekly, ALL monthly", from the table history requests use. */
|
|
177
|
+
export function historyIntervalsByRange(): string {
|
|
178
|
+
const groups: Array<{ ranges: TimeRange[]; resolution: ManualChartResolution }> = [];
|
|
179
|
+
for (const range of TIME_RANGES) {
|
|
180
|
+
const resolution = RANGE_HISTORY_RESOLUTION[range];
|
|
181
|
+
const last = groups.at(-1);
|
|
182
|
+
if (last?.resolution === resolution) last.ranges.push(range);
|
|
183
|
+
else groups.push({ ranges: [range], resolution });
|
|
184
|
+
}
|
|
185
|
+
return groups.map(({ ranges, resolution }) => (
|
|
186
|
+
`${ranges.length > 1 ? `${ranges[0]} to ${ranges.at(-1)}` : ranges[0]} ${BAR_INTERVAL_LABELS[resolution]}`
|
|
187
|
+
)).join(", ");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface HistoryUnit {
|
|
191
|
+
kind: "currency" | "pair" | "points" | "unknown";
|
|
192
|
+
/** Currency code of the prices; null for index points or when no source says. */
|
|
193
|
+
currency: string | null;
|
|
194
|
+
/** What one price is: "USD", "ILA (agorot, 1/100 ILS)", "ZAR per USD", "index points"; null when unknown. */
|
|
195
|
+
unit: string | null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* What the prices are in. An FX pair is named by its symbol (ZAR=X is ZAR per
|
|
200
|
+
* USD, EURUSD=X is USD per EUR), an index is in points, and anything else is
|
|
201
|
+
* in the currency its quote states, a sub-unit named as such. Without one the
|
|
202
|
+
* unit is unknown rather than guessed from the exchange.
|
|
203
|
+
*/
|
|
204
|
+
export function historyUnit(symbol: string, quote: { currency?: string; instrumentType?: string } | null | undefined): HistoryUnit {
|
|
205
|
+
const pair = /^([A-Z]{3})([A-Z]{3})?=X$/.exec(symbol.trim().toUpperCase());
|
|
206
|
+
if (pair) {
|
|
207
|
+
const [base, counter] = pair[2] ? [pair[1]!, pair[2]] : ["USD", pair[1]!];
|
|
208
|
+
return { kind: "pair", currency: counter, unit: `${counter} per ${base}` };
|
|
209
|
+
}
|
|
210
|
+
if (quote?.instrumentType?.trim().toUpperCase() === "INDEX") return { kind: "points", currency: null, unit: "index points" };
|
|
211
|
+
const code = quote?.currency?.trim();
|
|
212
|
+
return code
|
|
213
|
+
? { kind: "currency", currency: code, unit: currencyUnitLabel(code, { ofMajor: true }) }
|
|
214
|
+
: { kind: "unknown", currency: null, unit: null };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function historyDate(value: string): string {
|
|
218
|
+
return value.length > 10 ? formatUtcTime(value) : value;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The facts line under a history heading: "Currency USD", "daily bars", "2025-10-09 to 2026-10-08", "251 bars". */
|
|
222
|
+
export function historyFacts(unit: HistoryUnit, rows: readonly Pick<HistoryRow, "date" | "interval">[]): string[] {
|
|
223
|
+
const first = rows[0]?.date;
|
|
224
|
+
const last = rows.at(-1)?.date;
|
|
225
|
+
const interval = rows[0]?.interval;
|
|
226
|
+
return [
|
|
227
|
+
unit.kind === "pair" ? unit.unit! : unit.kind === "points" ? "Index points" : `Currency ${unit.unit ?? "unknown"}`,
|
|
228
|
+
interval ? `${BAR_INTERVAL_LABELS[interval]} bars` : "",
|
|
229
|
+
first && last ? (first === last ? historyDate(first) : `${historyDate(first)} to ${historyDate(last)}`) : "",
|
|
230
|
+
`${rows.length} ${rows.length === 1 ? "bar" : "bars"}`,
|
|
231
|
+
].filter(Boolean);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* What an export should know that its rows do not show: bars coarser than the
|
|
236
|
+
* range is served in, or a first bar well after the start asked for, as for a
|
|
237
|
+
* listing younger than the range. Weekends and holidays are within tolerance.
|
|
238
|
+
*/
|
|
239
|
+
export function historyNotes(
|
|
240
|
+
range: TimeRange,
|
|
241
|
+
interval: ManualChartResolution | null,
|
|
242
|
+
firstDate: string | undefined,
|
|
243
|
+
now = Date.now(),
|
|
244
|
+
): string[] {
|
|
245
|
+
if (!firstDate) return [];
|
|
246
|
+
const notes: string[] = [];
|
|
247
|
+
const expected = RANGE_HISTORY_RESOLUTION[range];
|
|
248
|
+
if (interval && CHART_RESOLUTION_STEP_MS[interval] > CHART_RESOLUTION_STEP_MS[expected]) {
|
|
249
|
+
const from = interval === "1mo" ? firstDate.slice(0, 7) : firstDate.slice(0, 10);
|
|
250
|
+
notes.push(`Asked ${range}, got ${BAR_INTERVAL_LABELS[interval]} bars from ${from}`);
|
|
251
|
+
}
|
|
252
|
+
const first = Date.parse(firstDate);
|
|
253
|
+
if (range !== "ALL" && Number.isFinite(first)) {
|
|
254
|
+
const tolerance = Math.max(LATE_START_MIN_MS, LATE_START_MIN_BARS * CHART_RESOLUTION_STEP_MS[interval ?? expected]);
|
|
255
|
+
if (first - subtractTimeRange(new Date(now), range).getTime() > tolerance) {
|
|
256
|
+
notes.push(`Asked ${range}, data starts ${firstDate.slice(0, 10)} (first available bar)`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return notes;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** "37 of 251 bars have high below open or close in the source data; ...", or null when every bar agrees. */
|
|
263
|
+
export function historyFlagNote(rows: readonly Pick<HistoryRow, "flag">[]): string | null {
|
|
264
|
+
const flagged = rows.filter((row) => row.flag);
|
|
265
|
+
if (flagged.length === 0) return null;
|
|
266
|
+
const codes = new Set(flagged.flatMap((row) => row.flag!.split(",")));
|
|
267
|
+
const kinds = [
|
|
268
|
+
codes.has("high<open") || codes.has("high<close") ? "high below open or close" : "",
|
|
269
|
+
codes.has("low>open") || codes.has("low>close") ? "low above open or close" : "",
|
|
270
|
+
codes.has("low>high") ? "low above high" : "",
|
|
271
|
+
].filter(Boolean);
|
|
272
|
+
const count = `${flagged.length} of ${rows.length} ${flagged.length === 1 && rows.length === 1 ? "bar" : "bars"}`;
|
|
273
|
+
return `${count} ${flagged.length === 1 ? "has" : "have"} ${kinds.join(", or ")} in the source data; their prices are left blank`;
|
|
274
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -16,14 +16,16 @@ import {
|
|
|
16
16
|
renderCliHelp,
|
|
17
17
|
renderCommandHelp,
|
|
18
18
|
suggestCliCommand,
|
|
19
|
+
TABLE_SECTION_OPTION,
|
|
19
20
|
type CliHelpEntry,
|
|
20
21
|
} from "./help";
|
|
21
22
|
import { parsePaneFunctionArgs } from "./pane-functions/options";
|
|
23
|
+
import { checkCliCommandOptions } from "./command-options";
|
|
22
24
|
import { fail, inferCliErrorOptions, printCliError } from "./errors";
|
|
23
25
|
import { setCliColorEnabledOverride } from "../utils/cli-output";
|
|
24
26
|
import { search, searchCandidatesForCli, buildSearchReport } from "./commands/search";
|
|
25
27
|
import { ticker } from "./commands/ticker";
|
|
26
|
-
import { takeOption } from "./commands/command-utils";
|
|
28
|
+
import { requireOneArg, takeOption } from "./commands/command-utils";
|
|
27
29
|
import { EXCHANGE_OPTION } from "./listing-arg";
|
|
28
30
|
import { apiCliCommand } from "./commands/api";
|
|
29
31
|
import { marketDataCliCommands } from "./commands/market";
|
|
@@ -107,11 +109,8 @@ function createCoreCliCommands(
|
|
|
107
109
|
execute: async (rawArgs, ctx) => {
|
|
108
110
|
const args = [...rawArgs];
|
|
109
111
|
const exchange = takeOption(args, "--exchange");
|
|
110
|
-
const symbol = args
|
|
111
|
-
|
|
112
|
-
ctx.fail("Usage: gloomberb ticker <symbol>");
|
|
113
|
-
}
|
|
114
|
-
await ticker(symbol!, {
|
|
112
|
+
const symbol = requireOneArg(args, "ticker <symbol>", "symbol", ctx);
|
|
113
|
+
await ticker(symbol, {
|
|
115
114
|
exchange,
|
|
116
115
|
initMarketData: ctx.initMarketData,
|
|
117
116
|
fail: ctx.fail,
|
|
@@ -128,7 +127,7 @@ function createCoreCliCommands(
|
|
|
128
127
|
usage: ["catalog [query] [--all] [--bot-safe]"],
|
|
129
128
|
options: [
|
|
130
129
|
{ flags: "--all", description: "List every match instead of the first 25" },
|
|
131
|
-
{ flags: "--bot-safe", description: "Only functions with a verified unattended report" },
|
|
130
|
+
{ flags: "--bot-safe, --botsafe", description: "Only functions with a verified unattended report" },
|
|
132
131
|
],
|
|
133
132
|
examples: ["catalog", "catalog options", "catalog HP"],
|
|
134
133
|
},
|
|
@@ -146,12 +145,17 @@ function createCoreCliCommands(
|
|
|
146
145
|
options: [
|
|
147
146
|
{ flags: "--<option> <value>", description: "A function setting; gloomberb catalog <function> lists them" },
|
|
148
147
|
{ flags: "--require-bot-safe", description: "Fail unless the function has a verified, complete report" },
|
|
148
|
+
{
|
|
149
|
+
...TABLE_SECTION_OPTION,
|
|
150
|
+
description: `${TABLE_SECTION_OPTION.description} (CALLS keeps --section for the transcript part)`,
|
|
151
|
+
},
|
|
149
152
|
EXCHANGE_OPTION,
|
|
150
153
|
],
|
|
151
154
|
examples: [
|
|
152
155
|
"fn HP AAPL",
|
|
153
156
|
"fn ANR SAN:EPA",
|
|
154
157
|
"fn CBR --json",
|
|
158
|
+
"fn WEI --csv --section europe > europe.csv",
|
|
155
159
|
"fn 13F AAPL --view=ticker-holdings",
|
|
156
160
|
"fn OVME --spot 100 --strike 100 --days 30 --volatility 25",
|
|
157
161
|
],
|
|
@@ -354,6 +358,7 @@ export async function dispatchCli(args: string[], options: DispatchCliOptions =
|
|
|
354
358
|
const target = helpOnly ? registry.lookup.get("help")! : resolved;
|
|
355
359
|
const commandArgs = helpOnly ? [resolved.command.name] : parsed.args.slice(1);
|
|
356
360
|
try {
|
|
361
|
+
if (!helpOnly && resolved.builtin) checkCliCommandOptions(resolved.command, commandArgs, parsed.literalStart - 1);
|
|
357
362
|
const result = await target.command.execute(
|
|
358
363
|
commandArgs,
|
|
359
364
|
createCliCommandContext(target.ownerId, registry, parsed.options),
|
package/src/cli/options.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface ParsedCliArgs {
|
|
|
15
15
|
options: CliGlobalOptions;
|
|
16
16
|
/** `--help` or `-h` appeared before `--`. The flag is removed from args. */
|
|
17
17
|
help: boolean;
|
|
18
|
+
/** Index in `args` where the arguments after a bare `--` begin; they are never options. */
|
|
19
|
+
literalStart: number;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function isCliHelpFlag(arg: string): boolean {
|
|
@@ -43,10 +45,12 @@ export function parseCliGlobalArgs(rawArgs: string[]): ParsedCliArgs {
|
|
|
43
45
|
const options: CliGlobalOptions = { ...DEFAULT_CLI_OPTIONS };
|
|
44
46
|
const args: string[] = [];
|
|
45
47
|
let help = false;
|
|
48
|
+
let literalStart: number | null = null;
|
|
46
49
|
|
|
47
50
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
48
51
|
const arg = rawArgs[index]!;
|
|
49
52
|
if (arg === "--") {
|
|
53
|
+
literalStart = args.length;
|
|
50
54
|
args.push(...rawArgs.slice(index + 1));
|
|
51
55
|
break;
|
|
52
56
|
}
|
|
@@ -102,5 +106,5 @@ export function parseCliGlobalArgs(rawArgs: string[]): ParsedCliArgs {
|
|
|
102
106
|
args.push(arg);
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
return { args, options, help };
|
|
109
|
+
return { args, options, help, literalStart: literalStart ?? args.length };
|
|
106
110
|
}
|
|
@@ -134,6 +134,11 @@ function resolveOptionDef(
|
|
|
134
134
|
));
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/** Whether the function has an option of its own under this name or an alias. */
|
|
138
|
+
export function capabilityHasOption(capability: PaneFunctionCapability, key: string): boolean {
|
|
139
|
+
return resolveOptionDef(capability, key) !== undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
137
142
|
function normalizeEnumValue(option: PaneFunctionOptionDef, rawValue: string): string {
|
|
138
143
|
const token = optionToken(rawValue);
|
|
139
144
|
const match = option.values?.find((candidate) => (
|
|
@@ -7,6 +7,7 @@ import type { ResolvedPaneFunction } from "./resolver";
|
|
|
7
7
|
import { collectShotSymbols } from "./data";
|
|
8
8
|
import { renderDesktopShot, type PaneScreenshotResult } from "./screenshot";
|
|
9
9
|
import { deriveRenderedFreshness, formatFreshnessLine, type ReportFreshness } from "./freshness";
|
|
10
|
+
import { exportTextTable, reportFooterLines, type CliReportTables } from "../report-tables";
|
|
10
11
|
|
|
11
12
|
const DOM_REPORT_WIDTH = 1280;
|
|
12
13
|
const DOM_REPORT_HEIGHT = 720;
|
|
@@ -42,7 +43,13 @@ function cellText(cell: DesktopPaneShotRenderedRow["cells"][number]): string {
|
|
|
42
43
|
return cell.instant ? formatUtcTime(cell.instant) || cell.text : cell.text;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
interface DomTable {
|
|
47
|
+
tableIndex: number;
|
|
48
|
+
columns: Array<{ key: string; header: string }>;
|
|
49
|
+
rows: Array<Map<string, DesktopPaneShotRenderedRow["cells"][number]>>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function domTables(rows: DesktopPaneShotRenderedRow[]): DomTable[] {
|
|
46
53
|
const byTable = new Map<number, DesktopPaneShotRenderedRow[]>();
|
|
47
54
|
for (const row of rows) {
|
|
48
55
|
const tableRows = byTable.get(row.tableIndex) ?? [];
|
|
@@ -50,7 +57,7 @@ function renderDomTables(rows: DesktopPaneShotRenderedRow[]): string[] {
|
|
|
50
57
|
byTable.set(row.tableIndex, tableRows);
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
return [...byTable.entries()].
|
|
60
|
+
return [...byTable.entries()].map(([tableIndex, tableRows]) => {
|
|
54
61
|
// Captured rows omit blank cells, so a cell's position is not its column.
|
|
55
62
|
const columns: Array<{ key: string; header: string }> = [];
|
|
56
63
|
for (const row of tableRows) {
|
|
@@ -66,22 +73,54 @@ function renderDomTables(rows: DesktopPaneShotRenderedRow[]): string[] {
|
|
|
66
73
|
previous = position;
|
|
67
74
|
}
|
|
68
75
|
}
|
|
69
|
-
|
|
76
|
+
return {
|
|
77
|
+
tableIndex,
|
|
70
78
|
columns,
|
|
71
|
-
tableRows.map((row) => {
|
|
79
|
+
rows: tableRows.map((row) => {
|
|
72
80
|
const keys = domCellKeys(row);
|
|
73
|
-
|
|
74
|
-
return columns.map((column) => textByKey.get(column.key) ?? "");
|
|
81
|
+
return new Map(row.cells.map((cell, cellIndex) => [keys[cellIndex]!, cell]));
|
|
75
82
|
}),
|
|
76
|
-
|
|
77
|
-
return [
|
|
78
|
-
...(index > 0 ? [""] : []),
|
|
79
|
-
...(byTable.size > 1 ? [renderSection(`Rendered table ${tableIndex + 1}`)] : []),
|
|
80
|
-
output,
|
|
81
|
-
];
|
|
83
|
+
};
|
|
82
84
|
});
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
function domTableTitle(table: DomTable): string {
|
|
88
|
+
return `Rendered table ${table.tableIndex + 1}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderDomTables(rows: DesktopPaneShotRenderedRow[]): string[] {
|
|
92
|
+
const tables = domTables(rows);
|
|
93
|
+
return tables.flatMap((table, index) => [
|
|
94
|
+
...(index > 0 ? [""] : []),
|
|
95
|
+
...(tables.length > 1 ? [renderSection(domTableTitle(table))] : []),
|
|
96
|
+
renderTable(
|
|
97
|
+
table.columns,
|
|
98
|
+
table.rows.map((cells) => table.columns.map((column) => {
|
|
99
|
+
const cell = cells.get(column.key);
|
|
100
|
+
return cell ? cellText(cell) : "";
|
|
101
|
+
})),
|
|
102
|
+
),
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The rendered tables as `--csv` writes them, with the instant behind a shortened time. */
|
|
107
|
+
function domReportTables(
|
|
108
|
+
rows: DesktopPaneShotRenderedRow[],
|
|
109
|
+
title: string,
|
|
110
|
+
freshness: ReportFreshness,
|
|
111
|
+
incomplete: boolean | string,
|
|
112
|
+
): CliReportTables {
|
|
113
|
+
const tables = domTables(rows);
|
|
114
|
+
return {
|
|
115
|
+
tables: tables.map((table) => exportTextTable(
|
|
116
|
+
tables.length > 1 ? domTableTitle(table) : title,
|
|
117
|
+
table.columns.map((column) => column.header),
|
|
118
|
+
table.rows.map((cells) => table.columns.map((column) => cells.get(column.key))),
|
|
119
|
+
)),
|
|
120
|
+
footer: reportFooterLines({ freshness, incomplete }),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
85
124
|
function renderedFailureReason(
|
|
86
125
|
result: PaneScreenshotResult,
|
|
87
126
|
rows: DesktopPaneShotRenderedRow[],
|
|
@@ -144,6 +183,7 @@ export function buildDomPaneReportFromRender(
|
|
|
144
183
|
cellTimes: rows.flatMap((row) => row.cells.flatMap((cell) => cell.instant ?? [])),
|
|
145
184
|
});
|
|
146
185
|
|
|
186
|
+
const complete = failureReason === null && !truncated;
|
|
147
187
|
return {
|
|
148
188
|
data: {
|
|
149
189
|
kind: "rendered-view",
|
|
@@ -153,7 +193,7 @@ export function buildDomPaneReportFromRender(
|
|
|
153
193
|
options: resolved.options,
|
|
154
194
|
rowCount: rows.length,
|
|
155
195
|
empty: rows.length === 0,
|
|
156
|
-
complete
|
|
196
|
+
complete,
|
|
157
197
|
unavailableSymbols,
|
|
158
198
|
rows,
|
|
159
199
|
visibleText: result.render.visibleText,
|
|
@@ -164,6 +204,12 @@ export function buildDomPaneReportFromRender(
|
|
|
164
204
|
freshness,
|
|
165
205
|
},
|
|
166
206
|
text: textLines.join("\n").trimEnd(),
|
|
207
|
+
tables: domReportTables(
|
|
208
|
+
rows,
|
|
209
|
+
resolved.label,
|
|
210
|
+
freshness,
|
|
211
|
+
!complete && (failureReason ?? (truncationReasons.length > 0 ? truncationReasons.join("; ") : true)),
|
|
212
|
+
),
|
|
167
213
|
};
|
|
168
214
|
}
|
|
169
215
|
|
|
@@ -173,15 +219,20 @@ export function appendDomReportFooter(
|
|
|
173
219
|
truncated: boolean,
|
|
174
220
|
freshness: ReportFreshness,
|
|
175
221
|
): string {
|
|
176
|
-
const clipping = truncated ? "The rendered view is clipped." : "The rendered view may be clipped.";
|
|
177
222
|
return [
|
|
178
223
|
text,
|
|
179
224
|
"",
|
|
180
|
-
|
|
225
|
+
domReportNote(elapsedMs, truncated),
|
|
181
226
|
cliStyles.muted(formatFreshnessLine(freshness)),
|
|
182
227
|
].join("\n");
|
|
183
228
|
}
|
|
184
229
|
|
|
230
|
+
/** What a rendered-view report says about where its values come from. */
|
|
231
|
+
export function domReportNote(elapsedMs: number, truncated: boolean): string {
|
|
232
|
+
const clipping = truncated ? "The rendered view is clipped." : "The rendered view may be clipped.";
|
|
233
|
+
return `Rendered view: values come from the visible pane. ${clipping} Render time: ${elapsedMs} ms.`;
|
|
234
|
+
}
|
|
235
|
+
|
|
185
236
|
function failedDomReport(
|
|
186
237
|
resolved: ResolvedPaneFunction,
|
|
187
238
|
rawArg: string,
|
|
@@ -193,6 +244,7 @@ function failedDomReport(
|
|
|
193
244
|
.trim();
|
|
194
245
|
const reason = `The rendered view could not be read: ${message || "unknown renderer error"}`;
|
|
195
246
|
const symbols = collectShotSymbols(resolved, rawArg);
|
|
247
|
+
const freshness = deriveRenderedFreshness(resolved.pane.reportFreshness, { footerText: "", cellTimes: [] });
|
|
196
248
|
return {
|
|
197
249
|
data: {
|
|
198
250
|
kind: "rendered-view",
|
|
@@ -210,9 +262,10 @@ function failedDomReport(
|
|
|
210
262
|
truncationReasons: [],
|
|
211
263
|
limitation: DOM_LIMITATION,
|
|
212
264
|
reason,
|
|
213
|
-
freshness
|
|
265
|
+
freshness,
|
|
214
266
|
},
|
|
215
267
|
text: [resolved.label, "", reason].join("\n"),
|
|
268
|
+
tables: { tables: [], footer: reportFooterLines({ freshness, incomplete: reason }) },
|
|
216
269
|
};
|
|
217
270
|
}
|
|
218
271
|
|
|
@@ -81,6 +81,8 @@ const SEARCH = key("/", "search");
|
|
|
81
81
|
const OPEN_SOURCE = key("o", "pen source");
|
|
82
82
|
const POP_OUT = key("p", "op out");
|
|
83
83
|
const STEP = key("←/→", " step");
|
|
84
|
+
/** THEM and MEMB members: open them in RRG, CORR, SIW or RIPL, or save them as a watchlist. */
|
|
85
|
+
const MEMBERS_MENU = key(".", " open members in, save as watchlist");
|
|
84
86
|
const CHART_KEYS = [key("s", "eries"), key("i", "ndicators"), key("t", "imeframe"), key("f", "ormulas")];
|
|
85
87
|
|
|
86
88
|
export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
@@ -663,14 +665,14 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
663
665
|
MEMB: {
|
|
664
666
|
summary: "ETF holdings with weights, shares, member returns, daily contributions and index changes. SPX and SPY use IVV holdings.",
|
|
665
667
|
usage: ["MEMB", "MEMB SPY", "MEMB IWM"],
|
|
666
|
-
keys: [TABS, OPEN],
|
|
668
|
+
keys: [TABS, OPEN, MEMBERS_MENU],
|
|
667
669
|
data: same("Dated fund holdings and partial delayed member returns. Nasdaq-100 is not covered."),
|
|
668
670
|
bloomberg: ["MEMB", "MRR", "IMOV"],
|
|
669
671
|
},
|
|
670
672
|
THEM: {
|
|
671
673
|
summary: "Curated thematic baskets with equal-weight returns and breadth. Open a theme to see its members, leaders and laggards. The Themes tab of BI.",
|
|
672
674
|
usage: ["THEM", "THEM nuclear"],
|
|
673
|
-
keys: [TABS, OPEN, key("Esc", "back")],
|
|
675
|
+
keys: [TABS, OPEN, key("Esc", "back"), MEMBERS_MENU],
|
|
674
676
|
data: same("Updated every 15 minutes"),
|
|
675
677
|
bloomberg: ["IMAP", "custom baskets"],
|
|
676
678
|
},
|