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
|
@@ -10,8 +10,16 @@ import {
|
|
|
10
10
|
type ScreenerCategory,
|
|
11
11
|
} from "../../plugins/builtin/market-movers/screener";
|
|
12
12
|
import { formatMoverPrice, moverReferencePrice } from "../../plugins/builtin/market-movers/model";
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
COUNTRY_CYCLE,
|
|
15
|
+
FILTER_CYCLE,
|
|
16
|
+
loadCalendar,
|
|
17
|
+
matchesCountry,
|
|
18
|
+
matchesImpact,
|
|
19
|
+
type CountryFilter,
|
|
20
|
+
type ImpactFilter,
|
|
21
|
+
} from "../../plugins/builtin/econ/calendar-model";
|
|
22
|
+
import { isoDate, rejectExtraArgs, requireArg, requireOneArg, takeOption } from "./command-utils";
|
|
15
23
|
import { buildCorrelationSeries } from "../../plugins/builtin/correlation/matrix/model";
|
|
16
24
|
import { alignDailyCloses, correlateDailyCloses } from "../../plugins/builtin/correlation/compute";
|
|
17
25
|
import { mixedSessionCloseNote } from "../../market-data/market/session-close-note";
|
|
@@ -48,11 +56,26 @@ const MOVER_COLUMNS = [
|
|
|
48
56
|
];
|
|
49
57
|
const START_OPTION = { flags: "--start <yyyy-mm-dd>", description: "First observation date (default 2021-01-01)" };
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
const MOVER_LISTS: Record<string, ScreenerCategory | "trending"> = {
|
|
60
|
+
gainers: "day_gainers",
|
|
61
|
+
losers: "day_losers",
|
|
62
|
+
active: "most_actives",
|
|
63
|
+
"most-active": "most_actives",
|
|
64
|
+
trending: "trending",
|
|
65
|
+
};
|
|
66
|
+
const MOVERS_USAGE = "movers [gainers|losers|active|trending]";
|
|
67
|
+
|
|
68
|
+
function screenerCategory(args: readonly string[], ctx: Parameters<CliCommandDef["execute"]>[1]): ScreenerCategory | "trending" {
|
|
69
|
+
rejectExtraArgs(args, 1, { usage: MOVERS_USAGE, takes: "one list" }, ctx);
|
|
70
|
+
const list = args[0]?.toLowerCase() ?? "gainers";
|
|
71
|
+
const category = MOVER_LISTS[list];
|
|
72
|
+
if (!category) ctx.fail(`Unknown list "${args[0]}".`, "Use gainers, losers, active or trending.");
|
|
73
|
+
return category;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A command that takes no arguments fails on one, rather than answering as if it were not there. */
|
|
77
|
+
function rejectArgs(args: readonly string[], usage: string, ctx: Parameters<CliCommandDef["execute"]>[1], advice?: string): void {
|
|
78
|
+
rejectExtraArgs(args, 0, { usage, takes: "no arguments", advice }, ctx);
|
|
56
79
|
}
|
|
57
80
|
|
|
58
81
|
function quoteRows(results: Awaited<ReturnType<NonNullable<import("../../types/data-provider").AssetDataProvider["getQuotesBatch"]>>>) {
|
|
@@ -72,7 +95,7 @@ function quoteRows(results: Awaited<ReturnType<NonNullable<import("../../types/d
|
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
async function runMoverCommand(args: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
75
|
-
const category = screenerCategory(args
|
|
98
|
+
const category = screenerCategory(args, ctx);
|
|
76
99
|
const limit = ctx.cliOptions.limit ?? 25;
|
|
77
100
|
if (category === "trending") {
|
|
78
101
|
await withCliServices(ctx, async (services) => {
|
|
@@ -126,10 +149,21 @@ function utcDateTimePart(value: unknown, part: "date" | "time"): string {
|
|
|
126
149
|
return part === "date" ? iso.slice(0, 10) : `${iso.slice(11, 16)} UTC`;
|
|
127
150
|
}
|
|
128
151
|
|
|
152
|
+
const ECON_USAGE = "econ [--country <region>] [--impact <level>]";
|
|
153
|
+
|
|
154
|
+
/** One of a filter's values, matched without regard to case; anything else fails with the list. */
|
|
155
|
+
function parseFilter<T extends string>(value: string | undefined, values: readonly T[], label: string, ctx: Parameters<CliCommandDef["execute"]>[1]): T {
|
|
156
|
+
if (value == null) return values[0]!;
|
|
157
|
+
const match = values.find((candidate) => candidate.toLowerCase() === value.trim().toLowerCase());
|
|
158
|
+
if (!match) ctx.fail(`Unknown ${label} "${value}".`, `Use ${values.join(", ")}.`);
|
|
159
|
+
return match;
|
|
160
|
+
}
|
|
161
|
+
|
|
129
162
|
async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
130
163
|
const rawArgs = [...args];
|
|
131
|
-
const country = (takeOption(rawArgs, "--country")
|
|
132
|
-
const impact = (takeOption(rawArgs, "--impact")
|
|
164
|
+
const country: CountryFilter = parseFilter(takeOption(rawArgs, "--country"), COUNTRY_CYCLE, "country", ctx);
|
|
165
|
+
const impact: ImpactFilter = parseFilter(takeOption(rawArgs, "--impact"), FILTER_CYCLE, "impact", ctx);
|
|
166
|
+
rejectArgs(rawArgs, ECON_USAGE, ctx, "Filter with --country and --impact.");
|
|
133
167
|
await withCliServices(ctx, async (services) => {
|
|
134
168
|
const { data: events } = await loadCalendar(ctx.cliOptions.refresh);
|
|
135
169
|
const rows = events
|
|
@@ -170,7 +204,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
170
204
|
const args = [...rawArgs];
|
|
171
205
|
const startDate = takeOption(args, "--start") ?? "2021-01-01";
|
|
172
206
|
const sortOrder = (takeOption(args, "--sort") ?? "desc") as "asc" | "desc";
|
|
173
|
-
const seriesId =
|
|
207
|
+
const seriesId = requireOneArg(args, "fred <series-id> [--start <yyyy-mm-dd>]", "series", ctx).toUpperCase();
|
|
174
208
|
const data = await apiClient.getCloudFredSeries(seriesId, { startDate, sortOrder });
|
|
175
209
|
const rows = data.observations.slice(0, ctx.cliOptions.limit ?? data.observations.length);
|
|
176
210
|
ctx.printResult({
|
|
@@ -187,8 +221,10 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
187
221
|
|
|
188
222
|
const YIELD_TENORS: Record<string, string> = { DGS3MO: "3M", DGS2: "2Y", DGS10: "10Y", DGS30: "30Y" };
|
|
189
223
|
|
|
190
|
-
async function runYieldCurve(
|
|
224
|
+
async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
225
|
+
const args = [...rawArgs];
|
|
191
226
|
const startDate = takeOption(args, "--start") ?? "2021-01-01";
|
|
227
|
+
rejectArgs(args, "yield-curve [--start <yyyy-mm-dd>]", ctx);
|
|
192
228
|
const series = Object.keys(YIELD_TENORS);
|
|
193
229
|
const results = await Promise.all(series.map(async (seriesId) => {
|
|
194
230
|
const data = await apiClient.getCloudFredSeries(seriesId, { startDate, sortOrder: "desc" });
|
|
@@ -217,9 +253,10 @@ async function runYieldCurve(args: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
217
253
|
async function runCorrelation(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
218
254
|
const args = [...rawArgs];
|
|
219
255
|
const exchange = takeOption(args, "--exchange");
|
|
220
|
-
const usage = "
|
|
221
|
-
requireArg(args[0], usage
|
|
222
|
-
requireArg(args[1], usage
|
|
256
|
+
const usage = "correlation <symbol-a> <symbol-b>";
|
|
257
|
+
requireArg(args[0], `Usage: gloomberb ${usage}`, ctx);
|
|
258
|
+
requireArg(args[1], `Usage: gloomberb ${usage}`, ctx);
|
|
259
|
+
rejectExtraArgs(args, 2, { usage, takes: "two symbols", advice: "Run it once per pair." }, ctx);
|
|
223
260
|
await withCliServices(ctx, async (services) => {
|
|
224
261
|
// --exchange is for a symbol that names no exchange of its own.
|
|
225
262
|
const [leftListing, rightListing] = await Promise.all([args[0]!, args[1]!].map((raw) => (
|
|
@@ -274,7 +311,7 @@ export const overviewCliCommands: CliCommandDef[] = [
|
|
|
274
311
|
description: "Show gainers, losers, most active, or trending stocks",
|
|
275
312
|
help: {
|
|
276
313
|
group: CLI_COMMAND_GROUPS.markets,
|
|
277
|
-
usage: [
|
|
314
|
+
usage: [MOVERS_USAGE],
|
|
278
315
|
examples: ["movers", "movers losers --limit 10", "movers trending"],
|
|
279
316
|
},
|
|
280
317
|
execute: runMoverCommand,
|
|
@@ -283,20 +320,26 @@ export const overviewCliCommands: CliCommandDef[] = [
|
|
|
283
320
|
name: "indices",
|
|
284
321
|
description: "Show the major US stock indices",
|
|
285
322
|
help: { group: CLI_COMMAND_GROUPS.markets, usage: ["indices"] },
|
|
286
|
-
execute: (
|
|
323
|
+
execute: (args, ctx) => {
|
|
324
|
+
rejectArgs(args, "indices", ctx);
|
|
325
|
+
return runQuoteBasket([...MARKET_SUMMARY_SYMBOLS], ctx, { group: "indices" });
|
|
326
|
+
},
|
|
287
327
|
},
|
|
288
328
|
{
|
|
289
329
|
name: "sectors",
|
|
290
330
|
description: "Show the SPDR sector ETFs",
|
|
291
331
|
help: { group: CLI_COMMAND_GROUPS.markets, usage: ["sectors"] },
|
|
292
|
-
execute: (
|
|
332
|
+
execute: (args, ctx) => {
|
|
333
|
+
rejectArgs(args, "sectors", ctx);
|
|
334
|
+
return runQuoteBasket(getSectorCollection("sectors").items.map((item) => item.etf), ctx, { group: "sectors" });
|
|
335
|
+
},
|
|
293
336
|
},
|
|
294
337
|
{
|
|
295
338
|
name: "econ",
|
|
296
339
|
description: "List upcoming economic calendar events",
|
|
297
340
|
help: {
|
|
298
341
|
group: CLI_COMMAND_GROUPS.markets,
|
|
299
|
-
usage: [
|
|
342
|
+
usage: [ECON_USAGE],
|
|
300
343
|
options: [
|
|
301
344
|
{ flags: "--country <region>", description: "US, G7, EU, or all (default all)" },
|
|
302
345
|
{ flags: "--impact <level>", description: "high, medium, low, or all (default all)" },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatShortDate, parseDisplayDate } from "../../utils/datetime-format";
|
|
2
2
|
import { formatReportedMoney } from "../../utils/reported-money";
|
|
3
|
+
import { fundamentalsCurrency, reportedEnterpriseValue } from "../../utils/fundamentals";
|
|
3
4
|
import { latestFinancialPeriod } from "../../utils/latest-financial-period";
|
|
4
5
|
import { exportedFundamentals, formatPriceEarnings, priceEarningsOnEarnings } from "../../utils/price-earnings";
|
|
5
6
|
import { describeFundamentalMarketCap, selectMarketCapitalization } from "../../utils/market-capitalization";
|
|
@@ -51,8 +52,10 @@ import { NotesFiles } from "../../plugins/builtin/notes/files";
|
|
|
51
52
|
import { isUsEquityTicker } from "../../utils/sec";
|
|
52
53
|
import { canonicalExchange, exchangeLabel, isKnownExchangeCode } from "../../utils/exchanges";
|
|
53
54
|
import { failIfNotTraded, ListingArgError, listingIdentity, resolveCliListing, type CliListing } from "../listing-arg";
|
|
54
|
-
import {
|
|
55
|
+
import { sharesOutstandingInReceipts } from "../../utils/depositary-receipt";
|
|
55
56
|
import { cliFreshnessFooter } from "../result";
|
|
57
|
+
import { exportEntriesTable, reportFooterLines, type CliReportTables } from "../report-tables";
|
|
58
|
+
import type { ReportFreshness } from "../pane-functions/freshness";
|
|
56
59
|
import { fundamentalsFreshness, quotesFreshness } from "../freshness";
|
|
57
60
|
|
|
58
61
|
const NEWS_ITEM_LIMIT = 5;
|
|
@@ -99,10 +102,26 @@ function buildStatementMetrics(statement: FinancialStatement, currency?: string)
|
|
|
99
102
|
["Total Debt", money(statement.totalDebt)],
|
|
100
103
|
["Equity", money(statement.totalEquity)],
|
|
101
104
|
["Diluted EPS", money(statement.eps, true)],
|
|
102
|
-
|
|
105
|
+
// Labelled only when the service says the row counts receipts.
|
|
106
|
+
["Diluted Shares", statement.dilutedShares != null && statement.shareBasis === "depositary_receipt"
|
|
107
|
+
? `${formatCompact(statement.dilutedShares)} (ADR equivalent)`
|
|
108
|
+
: formatCompact(statement.dilutedShares)],
|
|
103
109
|
];
|
|
104
110
|
}
|
|
105
111
|
|
|
112
|
+
/**
|
|
113
|
+
* A receipt's count is its ordinary shares expressed in receipts, as its
|
|
114
|
+
* market cap is; the ordinary count follows when the service gives it.
|
|
115
|
+
*/
|
|
116
|
+
function sharesOutstandingText(fundamentals: TickerFinancials["fundamentals"], inReceipts: boolean): string {
|
|
117
|
+
const shares = fundamentals?.sharesOutstanding;
|
|
118
|
+
if (shares == null || !inReceipts) return formatCompact(shares);
|
|
119
|
+
const ordinary = fundamentals?.shareBasis === "depositary_receipt" ? fundamentals.underlyingOrdinaryShares : undefined;
|
|
120
|
+
return ordinary != null && Number.isFinite(ordinary) && ordinary > 0
|
|
121
|
+
? `${formatCompact(shares)} (ADR equivalent = ${formatCompact(ordinary)} ordinary shares)`
|
|
122
|
+
: `${formatCompact(shares)} (ADR equivalent)`;
|
|
123
|
+
}
|
|
124
|
+
|
|
106
125
|
/** Provider prose is wrapped; `verbatim` keeps the user's own spacing, such as a table in a note. */
|
|
107
126
|
function appendTextSection(lines: string[], title: string, content: string | undefined, verbatim = false) {
|
|
108
127
|
const text = content?.trim();
|
|
@@ -241,60 +260,103 @@ function enterpriseValueCurrency(
|
|
|
241
260
|
|| undefined;
|
|
242
261
|
}
|
|
243
262
|
|
|
263
|
+
/** A fundamentals line: the text the report prints, and for CSV the figure behind it in `unit`. */
|
|
264
|
+
interface FundamentalsMetric {
|
|
265
|
+
label: string;
|
|
266
|
+
text: string;
|
|
267
|
+
value?: number | string | null;
|
|
268
|
+
unit?: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
type MetricFigure = Omit<FundamentalsMetric, "label">;
|
|
272
|
+
|
|
273
|
+
/** An amount in the currency the source reported it in; `ccy?` when it named none, as the text says. */
|
|
274
|
+
function reportedMoney(value: number | undefined, currency: string | undefined, perShare = false): MetricFigure {
|
|
275
|
+
return { text: formatReportedMoney(value, currency, perShare), value, unit: currency?.trim() || "ccy?" };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function percentFigure(fraction: number | null | undefined, format: (value: number) => string): MetricFigure {
|
|
279
|
+
return fraction != null ? { text: format(fraction), value: fraction * 100, unit: "%" } : { text: "—" };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function multipleFigure(value: ReturnType<typeof priceEarningsOnEarnings> | number | undefined): MetricFigure {
|
|
283
|
+
const text = formatPriceEarnings(value, 2);
|
|
284
|
+
return { text, value: typeof value === "number" && Number.isFinite(value) && value > 0 ? value : text === "N/M" ? text : null };
|
|
285
|
+
}
|
|
286
|
+
|
|
244
287
|
function fundamentalsMetrics(
|
|
245
288
|
quote: TickerFinancials["quote"],
|
|
246
289
|
fundamentals: TickerFinancials["fundamentals"],
|
|
247
290
|
profile: TickerFinancials["profile"],
|
|
248
|
-
|
|
291
|
+
marketCap: MetricFigure,
|
|
249
292
|
priceReturns: { return1Y?: number | null; return3Y?: number | null },
|
|
250
|
-
|
|
251
|
-
)
|
|
293
|
+
reportingCurrency: string | undefined,
|
|
294
|
+
enterpriseValue = reportedMoney(reportedEnterpriseValue(fundamentals), enterpriseValueCurrency(quote, fundamentals)),
|
|
295
|
+
): FundamentalsMetric[] {
|
|
296
|
+
const signed = (value: number) => colorBySign(formatPercent(value), value);
|
|
297
|
+
const receipt = fundamentals?.sharesOutstanding != null && sharesOutstandingInReceipts(quote, fundamentals, profile?.description);
|
|
252
298
|
return [
|
|
253
|
-
|
|
254
|
-
|
|
299
|
+
{ label: "Market Cap", ...marketCap },
|
|
300
|
+
{ label: "Enterprise Value", ...enterpriseValue },
|
|
255
301
|
// A multiple over a loss is N/M, even beside a positive one the source served from an older period.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
302
|
+
{ label: "P/E (TTM)", ...multipleFigure(priceEarningsOnEarnings(fundamentals?.trailingPE, fundamentals?.eps)) },
|
|
303
|
+
{ label: "Forward P/E", ...multipleFigure(priceEarningsOnEarnings(fundamentals?.forwardPE, fundamentals?.forwardEps)) },
|
|
304
|
+
{ label: "PEG", ...multipleFigure(fundamentals?.pegRatio) },
|
|
259
305
|
// The flows and EPS are one trailing-twelve-month block, the same twelve months for each line.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
306
|
+
{ label: "EPS (TTM)", ...reportedMoney(fundamentals?.eps, reportingCurrency, true) },
|
|
307
|
+
{
|
|
308
|
+
label: `Dividend Yield${fundamentals?.dividendYieldBasis ? ` (${fundamentals.dividendYieldBasis})` : ""}`,
|
|
309
|
+
...percentFigure(fundamentals?.dividendYield, formatFractionPercentCell),
|
|
310
|
+
},
|
|
311
|
+
{ label: "Revenue (TTM)", ...reportedMoney(fundamentals?.revenue, reportingCurrency) },
|
|
312
|
+
{ label: "Net Income (TTM)", ...reportedMoney(fundamentals?.netIncome, reportingCurrency) },
|
|
313
|
+
{ label: "Operating Cash Flow (TTM)", ...reportedMoney(fundamentals?.operatingCashFlow, reportingCurrency) },
|
|
314
|
+
{ label: "Free Cash Flow (TTM)", ...reportedMoney(fundamentals?.freeCashFlow, reportingCurrency) },
|
|
266
315
|
// Levels, not changes, so they carry no sign.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
fundamentals?.sharesOutstanding
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
],
|
|
316
|
+
{ label: "Operating Margin", ...percentFigure(fundamentals?.operatingMargin, formatFractionPercentCell) },
|
|
317
|
+
{ label: "Profit Margin", ...percentFigure(fundamentals?.profitMargin, formatFractionPercentCell) },
|
|
318
|
+
{ label: "Revenue Growth", ...percentFigure(fundamentals?.revenueGrowth, signed) },
|
|
319
|
+
{ label: "Last Quarter Growth", ...percentFigure(fundamentals?.lastQuarterGrowth, signed) },
|
|
320
|
+
{ label: "1Y Return", ...percentFigure(priceReturns.return1Y, signed) },
|
|
321
|
+
{ label: "3Y Return", ...percentFigure(priceReturns.return3Y, signed) },
|
|
322
|
+
{
|
|
323
|
+
label: "Shares Outstanding",
|
|
324
|
+
text: sharesOutstandingText(fundamentals, receipt),
|
|
325
|
+
value: fundamentals?.sharesOutstanding,
|
|
326
|
+
...(receipt ? { unit: "ADR equivalent" } : {}),
|
|
327
|
+
},
|
|
280
328
|
];
|
|
281
329
|
}
|
|
282
330
|
|
|
331
|
+
function metricLines(metrics: readonly FundamentalsMetric[]): Array<[string, string]> {
|
|
332
|
+
return metrics.map((metric) => [metric.label, metric.text]);
|
|
333
|
+
}
|
|
334
|
+
|
|
283
335
|
const VALUATION_METRICS = new Set(["Market Cap", "Enterprise Value", "P/E (TTM)", "Forward P/E", "PEG", "EPS (TTM)"]);
|
|
284
336
|
|
|
337
|
+
type FundamentalsReportData = TickerFinancials & { symbol: string; exchange?: string };
|
|
338
|
+
|
|
339
|
+
/** The lines `fundamentals` or `valuation` reports, the ones with a value. */
|
|
340
|
+
function fundamentalsReportMetrics(financials: FundamentalsReportData, view: "fundamentals" | "valuation"): FundamentalsMetric[] {
|
|
341
|
+
const quote = financials.quote;
|
|
342
|
+
const fundamentals = financials.fundamentals;
|
|
343
|
+
const capitalization = selectMarketCapitalization(quote, fundamentals);
|
|
344
|
+
const marketCap: MetricFigure = capitalization
|
|
345
|
+
? { text: `${formatCompact(capitalization.value)} ${capitalization.currency}`, value: capitalization.value, unit: capitalization.currency }
|
|
346
|
+
: { text: "—" };
|
|
347
|
+
const metrics = fundamentalsMetrics(quote, fundamentals, financials.profile, marketCap, computeTickerPriceReturns(financials), fundamentalsCurrency(financials));
|
|
348
|
+
return (view === "valuation"
|
|
349
|
+
? metrics.filter(({ label }) => VALUATION_METRICS.has(label) || label.startsWith("Dividend Yield"))
|
|
350
|
+
: metrics).filter((metric) => metric.text !== "—");
|
|
351
|
+
}
|
|
352
|
+
|
|
285
353
|
/** Text for `gloomberb fundamentals` and `gloomberb valuation`: the ticker report's fundamentals without the rest. */
|
|
286
354
|
export function renderFundamentalsReport(
|
|
287
|
-
financials:
|
|
355
|
+
financials: FundamentalsReportData,
|
|
288
356
|
view: "fundamentals" | "valuation",
|
|
289
357
|
): string {
|
|
290
358
|
const quote = financials.quote;
|
|
291
|
-
const fundamentals = financials.fundamentals;
|
|
292
359
|
const profile = financials.profile;
|
|
293
|
-
const capitalization = selectMarketCapitalization(quote, fundamentals);
|
|
294
|
-
const marketCapText = capitalization
|
|
295
|
-
? `${formatCompact(capitalization.value)} ${capitalization.currency}`
|
|
296
|
-
: "—";
|
|
297
|
-
const metrics = fundamentalsMetrics(quote, fundamentals, profile, marketCapText, computeTickerPriceReturns(financials));
|
|
298
360
|
const symbol = quote?.symbol ?? financials.symbol;
|
|
299
361
|
const name = quote?.name && quote.name !== symbol ? ` ${cliStyles.bold(quote.name)}` : "";
|
|
300
362
|
const lines = [`${cliStyles.accent(symbol)}${name}`];
|
|
@@ -305,16 +367,49 @@ export function renderFundamentalsReport(
|
|
|
305
367
|
].filter((part): part is string => !!part);
|
|
306
368
|
if (profileParts.length > 0) lines.push(cliStyles.muted(profileParts.join(METADATA_SEPARATOR)));
|
|
307
369
|
|
|
308
|
-
const shown = view === "valuation"
|
|
309
|
-
? metrics.filter(([label]) => VALUATION_METRICS.has(label) || label.startsWith("Dividend Yield"))
|
|
310
|
-
: metrics;
|
|
311
370
|
const before = lines.length;
|
|
312
|
-
appendMetricSection(lines, view === "valuation" ? "Valuation" : "Fundamentals",
|
|
371
|
+
appendMetricSection(lines, view === "valuation" ? "Valuation" : "Fundamentals", metricLines(fundamentalsReportMetrics(financials, view)));
|
|
313
372
|
if (lines.length === before) lines.push("", cliStyles.muted(`No ${view} reported for ${financials.symbol}.`));
|
|
314
373
|
if (view === "fundamentals") appendTextSection(lines, "Description", profile?.description);
|
|
315
374
|
return lines.join("\n");
|
|
316
375
|
}
|
|
317
376
|
|
|
377
|
+
/**
|
|
378
|
+
* `fundamentals` and `valuation` for `--csv` and `--ndjson`: the metrics as
|
|
379
|
+
* `Metric,Value` with each unit in its label, and for `fundamentals` the
|
|
380
|
+
* company profile the text prints around them.
|
|
381
|
+
*/
|
|
382
|
+
export function fundamentalsReportTables(
|
|
383
|
+
financials: FundamentalsReportData,
|
|
384
|
+
view: "fundamentals" | "valuation",
|
|
385
|
+
freshness: ReportFreshness,
|
|
386
|
+
): CliReportTables {
|
|
387
|
+
const metrics = fundamentalsReportMetrics(financials, view);
|
|
388
|
+
const tables = [exportEntriesTable(
|
|
389
|
+
view === "valuation" ? "Valuation" : "Fundamentals",
|
|
390
|
+
metrics.map((metric) => ({ label: metric.label, value: metric.value, formatted: metric.text, unit: metric.unit })),
|
|
391
|
+
)];
|
|
392
|
+
if (view === "fundamentals") {
|
|
393
|
+
const quote = financials.quote;
|
|
394
|
+
const profile = financials.profile;
|
|
395
|
+
tables.push(exportEntriesTable("Profile", [
|
|
396
|
+
{ label: "Symbol", value: quote?.symbol ?? financials.symbol },
|
|
397
|
+
{ label: "Name", value: quote?.name },
|
|
398
|
+
{ label: "Exchange", value: financials.exchange ? exchangeLabel(financials.exchange) : undefined },
|
|
399
|
+
{ label: "Sector", value: profile?.sector },
|
|
400
|
+
{ label: "Industry", value: profile?.industry },
|
|
401
|
+
{ label: "Description", value: profile?.description?.trim() },
|
|
402
|
+
].filter((entry) => typeof entry.value === "string" && entry.value.trim().length > 0)));
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
tables,
|
|
406
|
+
footer: reportFooterLines({
|
|
407
|
+
freshness,
|
|
408
|
+
notes: metrics.length === 0 ? [`No ${view} reported for ${financials.symbol}.`] : [],
|
|
409
|
+
}),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
318
413
|
/** "Euronext Paris (EPA)": the venue the report is for, named in full when the app knows it. */
|
|
319
414
|
function listingVenueLabel(
|
|
320
415
|
listingExchange: string | undefined,
|
|
@@ -407,10 +502,10 @@ export async function buildTickerReport({
|
|
|
407
502
|
: `${formatCompact(capitalization.value)} ${capitalization.currency}`
|
|
408
503
|
: "—";
|
|
409
504
|
// Shown in the same currency as the market cap, so the two can be compared.
|
|
410
|
-
const enterpriseValue = fundamentals
|
|
505
|
+
const enterpriseValue = reportedEnterpriseValue(fundamentals);
|
|
411
506
|
const evCurrency = enterpriseValueCurrency(quote, fundamentals);
|
|
412
507
|
// A minor unit such as GBp stays as reported: the converter would read it as the major currency.
|
|
413
|
-
const convertedEnterpriseValue = enterpriseValue != null &&
|
|
508
|
+
const convertedEnterpriseValue = enterpriseValue != null && evCurrency && /^[A-Z]{3}$/.test(evCurrency)
|
|
414
509
|
? await toBase(enterpriseValue, evCurrency) : Number.NaN;
|
|
415
510
|
const enterpriseValueText = Number.isFinite(convertedEnterpriseValue)
|
|
416
511
|
? `${formatCompact(convertedEnterpriseValue)} ${config.baseCurrency}`
|
|
@@ -447,7 +542,7 @@ export async function buildTickerReport({
|
|
|
447
542
|
]);
|
|
448
543
|
}
|
|
449
544
|
|
|
450
|
-
appendMetricSection(lines, "Fundamentals", fundamentalsMetrics(quote, fundamentals, profile, marketCapText, priceReturns, enterpriseValueText));
|
|
545
|
+
appendMetricSection(lines, "Fundamentals", metricLines(fundamentalsMetrics(quote, fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials), { text: enterpriseValueText })));
|
|
451
546
|
|
|
452
547
|
if (capitalization?.provenance.kind === "fundamentals") {
|
|
453
548
|
lines.push(cliStyles.muted(`Market cap: ${describeFundamentalMarketCap(capitalization.provenance)}.`));
|
|
@@ -10,6 +10,7 @@ import type { ManualChartResolution } from "../time-series/resolution";
|
|
|
10
10
|
import type { SnapshotMarketData } from "../market-data/snapshot-provider";
|
|
11
11
|
import type { InstrumentRef } from "../market-data/request-types";
|
|
12
12
|
import type { TickerRecord } from "../types/ticker";
|
|
13
|
+
import type { BrokerAccount } from "../types/trading";
|
|
13
14
|
import type { PaneRuntimeState } from "../core/state/app/state";
|
|
14
15
|
import type { RemoteUiNodeSnapshot } from "../remote/types";
|
|
15
16
|
import type { DatedObservation } from "../plugins/builtin/market-valuation/series";
|
|
@@ -54,6 +55,8 @@ export interface DesktopPaneShotPayload {
|
|
|
54
55
|
/** Label drawn at the right edge of the pane title bar; null draws nothing. */
|
|
55
56
|
watermark?: string | null;
|
|
56
57
|
tickers: TickerRecord[];
|
|
58
|
+
/** Broker accounts as last synced, by profile, so a broker portfolio shows its equity and cash. */
|
|
59
|
+
brokerAccounts?: Record<string, BrokerAccount[]>;
|
|
57
60
|
financials: Array<[string, TickerFinancials]>;
|
|
58
61
|
instrumentFinancials?: SnapshotMarketData["instrumentFinancials"];
|
|
59
62
|
historyVariants?: SnapshotMarketData["historyVariants"];
|
package/src/cli/help.ts
CHANGED
|
@@ -10,6 +10,12 @@ import {
|
|
|
10
10
|
wrapText,
|
|
11
11
|
} from "../utils/cli-output";
|
|
12
12
|
|
|
13
|
+
/** `--section` of a command whose report has several tables. */
|
|
14
|
+
export const TABLE_SECTION_OPTION = {
|
|
15
|
+
flags: "--section <title|n>",
|
|
16
|
+
description: "With --csv or --ndjson, write only that table, by its title or number",
|
|
17
|
+
};
|
|
18
|
+
|
|
13
19
|
/** Headings of `gloomberb help`, in display order. */
|
|
14
20
|
export const CLI_COMMAND_GROUPS = {
|
|
15
21
|
research: "Research",
|
|
@@ -29,7 +35,9 @@ const MAX_HELP_WIDTH = 100;
|
|
|
29
35
|
const PIPED_HELP_WIDTH = 80;
|
|
30
36
|
|
|
31
37
|
const GLOBAL_OPTIONS: Array<[string, string]> = [
|
|
32
|
-
["--json
|
|
38
|
+
["--json", "Print the full result as JSON"],
|
|
39
|
+
["--csv", "Print rows as CSV under the columns the text shows; fn reports and fundamentals put each table under # section: and end with # source lines"],
|
|
40
|
+
["--ndjson", "Print one JSON object per table row"],
|
|
33
41
|
["--limit <n>", "Show at most n rows"],
|
|
34
42
|
["--refresh", "Fetch fresh data instead of reading the cache"],
|
|
35
43
|
["--dry-run", "Preview config, cache, notes, alerts, plugin on/off, and remote changes without saving"],
|
|
@@ -178,14 +186,10 @@ function editDistance(left: string, right: string): number {
|
|
|
178
186
|
return previous[right.length]!;
|
|
179
187
|
}
|
|
180
188
|
|
|
181
|
-
|
|
182
|
-
export function suggestCliCommand(token: string, candidates: Iterable<string>): string | null {
|
|
183
|
-
const needle = token.trim().toLowerCase();
|
|
184
|
-
if (!needle || needle.startsWith("-")) return null;
|
|
189
|
+
function closestName(needle: string, candidates: Iterable<string>): string | null {
|
|
185
190
|
const allowed = needle.length <= 4 ? 1 : 2;
|
|
186
191
|
let best: { name: string; score: number } | null = null;
|
|
187
192
|
for (const candidate of candidates) {
|
|
188
|
-
if (candidate.startsWith("-")) continue;
|
|
189
193
|
// A clear prefix ("tick" for "ticker") beats an edit of the same size.
|
|
190
194
|
const score = needle.length >= 3 && candidate.startsWith(needle)
|
|
191
195
|
? 0.5
|
|
@@ -195,3 +199,19 @@ export function suggestCliCommand(token: string, candidates: Iterable<string>):
|
|
|
195
199
|
}
|
|
196
200
|
return best?.name ?? null;
|
|
197
201
|
}
|
|
202
|
+
|
|
203
|
+
/** The closest command token to a mistyped one, or null when nothing is plausibly meant. */
|
|
204
|
+
export function suggestCliCommand(token: string, candidates: Iterable<string>): string | null {
|
|
205
|
+
const needle = token.trim().toLowerCase();
|
|
206
|
+
if (!needle || needle.startsWith("-")) return null;
|
|
207
|
+
return closestName(needle, [...candidates].filter((candidate) => !candidate.startsWith("-")));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The closest of a command's options to a mistyped one (`--rnage` for `--range`), or null. */
|
|
211
|
+
export function suggestCliOption(flag: string, options: Iterable<string>): string | null {
|
|
212
|
+
const needle = flag.trim().toLowerCase().replace(/^-+/, "");
|
|
213
|
+
if (!needle) return null;
|
|
214
|
+
const byName = new Map([...options].map((option) => [option.replace(/^-+/, ""), option] as const));
|
|
215
|
+
const name = closestName(needle, byName.keys());
|
|
216
|
+
return name == null ? null : byName.get(name)!;
|
|
217
|
+
}
|