gloomberb 0.17.2 → 0.17.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/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +18 -8
- package/src/api-client/types.ts +3 -1
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +18 -1
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +32 -4
- package/src/plugins/builtin/futures/model.ts +65 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -9
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/styles.css +6 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +19 -2
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { currentFxClosureStart } from "../../utils/fx-market-hours";
|
|
11
11
|
import { zonedDateKey } from "../../utils/zoned-date-time";
|
|
12
12
|
import type { ReportTime } from "../../utils/utc-time";
|
|
13
|
+
import type { QuoteIndexSession } from "../../types/financials";
|
|
13
14
|
|
|
14
15
|
/** Where the venues behind a quote report stand, as a report says it after the delay. */
|
|
15
16
|
type ReportMarketState = "open" | "pre-market" | "after-hours" | "closed";
|
|
@@ -27,6 +28,8 @@ export interface SessionObservation {
|
|
|
27
28
|
exchange: string;
|
|
28
29
|
marketState?: string;
|
|
29
30
|
time: ReportTime | null;
|
|
31
|
+
/** The regular sessions an index quote carries, for a publisher the app has no calendar for. */
|
|
32
|
+
calendar?: QuoteIndexSession;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
interface VenueSession {
|
|
@@ -35,16 +38,19 @@ interface VenueSession {
|
|
|
35
38
|
/** The latest regular session that has closed: its local date and open. */
|
|
36
39
|
last?: { date: string; open: number | null };
|
|
37
40
|
timeZone?: string;
|
|
41
|
+
/** The sessions are the quote's own, not a calendar of the venue. */
|
|
42
|
+
quoted?: true;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
const CLOSED_STATES = new Set(["CLOSED", "PREPRE", "POSTPOST"]);
|
|
46
|
+
const US_EXCHANGE_TIME_ZONE = "America/New_York";
|
|
41
47
|
|
|
42
48
|
/**
|
|
43
49
|
* A venue's session now, from its calendar (weekends and published holidays),
|
|
44
50
|
* or from the state its source reports when the app has no calendar for it,
|
|
45
51
|
* such as an index publisher or a futures exchange. Null when neither says.
|
|
46
52
|
*/
|
|
47
|
-
function venueSession(exchange: string, marketState: string | undefined, now: number): VenueSession | null {
|
|
53
|
+
function venueSession(exchange: string, marketState: string | undefined, now: number, calendar?: QuoteIndexSession): VenueSession | null {
|
|
48
54
|
if (isAlwaysOpenExchange(exchange)) return { state: "always" };
|
|
49
55
|
const next = nextRegularSessionOpen(exchange, now);
|
|
50
56
|
if (next) {
|
|
@@ -61,10 +67,24 @@ function venueSession(exchange: string, marketState: string | undefined, now: nu
|
|
|
61
67
|
return { state: "closed", next, last, timeZone };
|
|
62
68
|
}
|
|
63
69
|
if (marketState === "REGULAR") return { state: "open" };
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
const stated = marketState === "PRE" ? "pre-market" : marketState === "POST" ? "after-hours" : marketState && CLOSED_STATES.has(marketState) ? "closed" : null;
|
|
71
|
+
if (!stated) return null;
|
|
72
|
+
return calendar ? { state: stated, ...quotedSessions(calendar, now) } : { state: stated };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* An index publisher's sessions (SNP, DJI, NASDAQ GIDS) as its quote dates
|
|
77
|
+
* them, read like a venue's calendar: the open still to come, and the latest
|
|
78
|
+
* regular session that has closed.
|
|
79
|
+
*/
|
|
80
|
+
function quotedSessions({ timeZone, current, regular }: QuoteIndexSession, now: number): Pick<VenueSession, "next" | "last" | "timeZone" | "quoted"> {
|
|
81
|
+
const closed = [...regular, current].filter((window) => window.end <= now).sort((left, right) => right.end - left.end)[0];
|
|
82
|
+
return {
|
|
83
|
+
timeZone,
|
|
84
|
+
quoted: true,
|
|
85
|
+
...(current.start > now ? { next: { date: zonedDateKey(current.start, timeZone), open: current.start } } : {}),
|
|
86
|
+
...(closed ? { last: { date: zonedDateKey(closed.start, timeZone), open: closed.start } } : {}),
|
|
87
|
+
};
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
/** The venue spot FX pairs trade on. It has no session calendar here; its week is in `fx-market-hours`. */
|
|
@@ -101,9 +121,52 @@ function closeDateOf(observation: SessionObservation, session: VenueSession, now
|
|
|
101
121
|
function closeMarketOf(exchange: string, session: VenueSession): string | null {
|
|
102
122
|
if (exchange === FX_VENUE) return "FX";
|
|
103
123
|
if (!session.last || !session.timeZone) return null;
|
|
124
|
+
// An index publisher's name is no market: a session dated in New York is the US's, any other is a bare date.
|
|
125
|
+
if (session.quoted) return session.timeZone === US_EXCHANGE_TIME_ZONE ? "US" : null;
|
|
104
126
|
return isUsListingExchange(exchange) ? "US" : exchange;
|
|
105
127
|
}
|
|
106
128
|
|
|
129
|
+
/** One market's session behind a report that mixes markets: its local date, and whether that session has closed. */
|
|
130
|
+
export interface VenueSessionDate {
|
|
131
|
+
/** `US` for the US listing venues, which share one calendar, the venue's code elsewhere (`XETRA`, `EPA`). */
|
|
132
|
+
market: string;
|
|
133
|
+
/** The session's local date, `YYYY-MM-DD`. */
|
|
134
|
+
date: string;
|
|
135
|
+
/** The newest row on that market is its latest close. */
|
|
136
|
+
close: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Each market's session, for a report whose rows trade on more than one
|
|
141
|
+
* (a US line beside Xetra and Paris ones): the newest row of each, dated in
|
|
142
|
+
* its venue's own day, in the order the markets first appear. Empty when the
|
|
143
|
+
* rows share one market, or fewer than two can be placed.
|
|
144
|
+
*/
|
|
145
|
+
export function reportVenueSessionDates(observations: readonly SessionObservation[], now: number): VenueSessionDate[] {
|
|
146
|
+
const newestByMarket = new Map<string, { observation: SessionObservation & { time: ReportTime }; session: VenueSession }>();
|
|
147
|
+
for (const observation of observations) {
|
|
148
|
+
const exchange = canonicalExchange(observation.exchange);
|
|
149
|
+
const time = observation.time;
|
|
150
|
+
if (!exchange || !time) continue;
|
|
151
|
+
const session = venueSession(exchange, observation.marketState, now);
|
|
152
|
+
if (!session || session.state === "always") continue;
|
|
153
|
+
const market = exchange === FX_VENUE ? "FX" : isUsListingExchange(exchange) ? "US" : exchange;
|
|
154
|
+
const current = newestByMarket.get(market);
|
|
155
|
+
if (!current || time.time > current.observation.time.time) newestByMarket.set(market, { observation: { ...observation, time }, session });
|
|
156
|
+
}
|
|
157
|
+
if (newestByMarket.size < 2) return [];
|
|
158
|
+
return [...newestByMarket].map(([market, { observation, session }]) => {
|
|
159
|
+
// A venue in its session whose newest row is from an earlier day has not printed today: that row is its last close.
|
|
160
|
+
const exchange = canonicalExchange(observation.exchange);
|
|
161
|
+
const latestClose = session.state === "open" && session.timeZone ? latestRegularSessionClose(exchange, now) : null;
|
|
162
|
+
const last = latestClose ? { date: latestClose.date, open: latestRegularSessionOpen(exchange, latestClose.close) } : session.last;
|
|
163
|
+
const closeDate = closeDateOf(observation, { ...session, last }, now);
|
|
164
|
+
const date = closeDate
|
|
165
|
+
?? (session.timeZone ? zonedDateKey(observation.time.time, session.timeZone) : new Date(observation.time.time).toISOString().slice(0, 10));
|
|
166
|
+
return { market, date, close: closeDate != null };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
107
170
|
/**
|
|
108
171
|
* Where the markets behind a quote report stand now, and whether the newest
|
|
109
172
|
* observation is a close, and whose. Null market when the venues disagree
|
|
@@ -120,7 +183,7 @@ export function reportMarketSession(
|
|
|
120
183
|
const exchange = canonicalExchange(observation.exchange);
|
|
121
184
|
if (!exchange) continue;
|
|
122
185
|
const key = `${exchange}:${observation.marketState ?? ""}`;
|
|
123
|
-
const session = sessions.get(key) ?? venueSession(exchange, observation.marketState, now);
|
|
186
|
+
const session = sessions.get(key) ?? venueSession(exchange, observation.marketState, now, observation.calendar);
|
|
124
187
|
if (!session) continue;
|
|
125
188
|
sessions.set(key, session);
|
|
126
189
|
if (observation.time && (!newest || observation.time.time > (newest.observation.time?.time ?? -Infinity))) {
|
|
@@ -12,6 +12,7 @@ import { normalizeTickerInput } from "../../tickers/search";
|
|
|
12
12
|
import { slugifyName } from "../../utils/slugify";
|
|
13
13
|
import { ListingArgError, listingReadNotes, resolveCliListing, usListingsOnlyMessage } from "../listing-arg";
|
|
14
14
|
import { listingAbroadVenue } from "../../utils/sec";
|
|
15
|
+
import { splitSymbolArgs, stripStrayEdges } from "../symbol-args";
|
|
15
16
|
import type { MarketContext } from "../types";
|
|
16
17
|
import {
|
|
17
18
|
buildPaneFunctionLookup,
|
|
@@ -135,10 +136,11 @@ export async function applyListingArgument(
|
|
|
135
136
|
}
|
|
136
137
|
const exchange = typeof args.options.exchange === "string" ? args.options.exchange : undefined;
|
|
137
138
|
const { exchange: _exchange, ...options } = args.options;
|
|
138
|
-
const typed = takesOneTicker ? [args.arg] : args.arg
|
|
139
|
+
const typed = takesOneTicker ? [args.arg] : splitSymbolArgs([args.arg]);
|
|
139
140
|
if (typed.length === 0) return args;
|
|
140
141
|
let listings = await Promise.all(typed.map((raw) => resolveCliListing(raw, exchange, context, { ownExchangeWins: typed.length > 1 })));
|
|
141
|
-
|
|
142
|
+
// A separator stuck to the end of a pasted list (`UPST;`) is not part of a symbol; it is named.
|
|
143
|
+
const notes: string[] = takesOneTicker ? [] : stripStrayEdges(args.arg).notes;
|
|
142
144
|
const usOnly = headless?.usListingsOnly;
|
|
143
145
|
const abroad = usOnly ? listings.filter((listing) => listingAbroadVenue(listing.key) != null) : [];
|
|
144
146
|
if (usOnly && abroad.length > 0) {
|
|
@@ -936,6 +936,8 @@ export async function renderDesktopShot({
|
|
|
936
936
|
watermark,
|
|
937
937
|
statusLine = false,
|
|
938
938
|
fitRows = false,
|
|
939
|
+
fitReader = false,
|
|
940
|
+
maxHeightCells,
|
|
939
941
|
options,
|
|
940
942
|
captureImage = true,
|
|
941
943
|
onStalledReport,
|
|
@@ -953,6 +955,10 @@ export async function renderDesktopShot({
|
|
|
953
955
|
statusLine?: boolean;
|
|
954
956
|
/** No `--height` was asked for: a pane that fits its rows (FUT) grows to show them all. */
|
|
955
957
|
fitRows?: boolean;
|
|
958
|
+
/** A pane whose reader has a page open grows to show all of its text (a report's, which saves no image). */
|
|
959
|
+
fitReader?: boolean;
|
|
960
|
+
/** How tall a pane that fits its rows or its reader may grow, in cells, where the height limit of a saved image does not apply. */
|
|
961
|
+
maxHeightCells?: number;
|
|
956
962
|
options: Record<string, string | true>;
|
|
957
963
|
captureImage?: boolean;
|
|
958
964
|
/**
|
|
@@ -999,7 +1005,14 @@ export async function renderDesktopShot({
|
|
|
999
1005
|
loadModel,
|
|
1000
1006
|
);
|
|
1001
1007
|
if (statusLine && captureImage) payload.statusLine = true;
|
|
1002
|
-
|
|
1008
|
+
const fitRoles = [
|
|
1009
|
+
...(fitRows && resolved.pane.shotFitsRows ? ["data-table-body-scroll"] : []),
|
|
1010
|
+
...(fitReader && resolved.pane.shotFitsReader ? ["detail-scroll-body"] : []),
|
|
1011
|
+
];
|
|
1012
|
+
if (fitRoles.length > 0) {
|
|
1013
|
+
payload.maxHeightCells = maxHeightCells ?? shotMaxHeightCells(scale ?? 1);
|
|
1014
|
+
payload.fitRoles = fitRoles;
|
|
1015
|
+
}
|
|
1003
1016
|
// Loads while the page renders.
|
|
1004
1017
|
const report = payload.statusLine ? reportShotFreshness(resolved, loadModel) : undefined;
|
|
1005
1018
|
// The same load says whether the report is complete, with or without a status line.
|
|
@@ -6,7 +6,11 @@ export interface ScrollBoxMetrics {
|
|
|
6
6
|
scrollLeft: number;
|
|
7
7
|
scrollWidth: number;
|
|
8
8
|
clientWidth: number;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* The box draws a scrollbar on that axis, which says it overflows even when the measures cannot:
|
|
11
|
+
* a table's body, which draws only the rows near the viewport. A plain scroll box draws one by
|
|
12
|
+
* default and holds all its content, so its measures say it.
|
|
13
|
+
*/
|
|
10
14
|
scrollbarY: boolean;
|
|
11
15
|
scrollbarX: boolean;
|
|
12
16
|
}
|
package/src/cli/symbol-args.ts
CHANGED
|
@@ -1,11 +1,49 @@
|
|
|
1
|
+
/** The quote marks a pasted list wraps its entries in: "AAPL", 'MSFT', “NVDA”. */
|
|
2
|
+
const QUOTE_MARKS = "\"'`\u2018\u2019\u201C\u201D";
|
|
3
|
+
const QUOTE_EDGES = new RegExp(`^[${QUOTE_MARKS}]+|[${QUOTE_MARKS}]+$`, "g");
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Symbols given as command arguments, each of which may hold several split by
|
|
3
7
|
* commas, semicolons, spaces, tabs or newlines (a pasted list), with the blanks
|
|
4
|
-
* dropped
|
|
5
|
-
*
|
|
8
|
+
* dropped, the quote marks around each entry removed and the typed case kept.
|
|
9
|
+
* Every command that takes a symbol list splits its arguments here, and
|
|
10
|
+
* `not-a-ticker` reads the typed spelling back from them.
|
|
6
11
|
*/
|
|
7
12
|
export function splitSymbolArgs(args: readonly string[]): string[] {
|
|
8
|
-
return args
|
|
13
|
+
return args
|
|
14
|
+
.flatMap((arg) => arg.replace(/^\uFEFF/, "").split(/[\s,;]+/))
|
|
15
|
+
.map((entry) => entry.replace(QUOTE_EDGES, ""))
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The list separators in `text`, each once: `,;` for a stray `;,;`. */
|
|
20
|
+
function separatorsIn(text: string): string {
|
|
21
|
+
return [...new Set([...text].filter((char) => char === "," || char === ";"))].join("");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A pasted list, or one cell of a file, without the separators and quote marks
|
|
26
|
+
* stuck to its ends (`UPST;`, `"GME",`). The separators it dropped are named
|
|
27
|
+
* (`Ignored the stray ";" after UPST.`) rather than swallowed, and never reach
|
|
28
|
+
* a listing search as part of a symbol, which would ask for `UPST;`.
|
|
29
|
+
*/
|
|
30
|
+
export function stripStrayEdges(text: string): { text: string; notes: string[] } {
|
|
31
|
+
const junk = `[\\s,;${QUOTE_MARKS}]`;
|
|
32
|
+
const leading = text.match(new RegExp(`^${junk}+`))?.[0] ?? "";
|
|
33
|
+
const rest = text.slice(leading.length);
|
|
34
|
+
const trailing = rest.match(new RegExp(`${junk}+$`))?.[0] ?? "";
|
|
35
|
+
const inner = rest.slice(0, rest.length - trailing.length);
|
|
36
|
+
const entries = splitSymbolArgs([inner]);
|
|
37
|
+
const [first, last] = [entries[0], entries.at(-1)];
|
|
38
|
+
const before = separatorsIn(leading);
|
|
39
|
+
const after = separatorsIn(trailing);
|
|
40
|
+
const notes = !first
|
|
41
|
+
? (before || after ? [`Ignored the stray "${separatorsIn(leading + trailing)}".`] : [])
|
|
42
|
+
: [
|
|
43
|
+
...before ? [`Ignored the stray "${before}" before ${first}.`] : [],
|
|
44
|
+
...after ? [`Ignored the stray "${after}" after ${last}.`] : [],
|
|
45
|
+
];
|
|
46
|
+
return { text: inner, notes };
|
|
9
47
|
}
|
|
10
48
|
|
|
11
49
|
/** How a symbol is compared for repeats: AAPL, aapl and $AAPL are one. */
|
package/src/cli/symbol-list.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from "fs/promises";
|
|
2
2
|
import { fstatSync } from "fs";
|
|
3
|
-
import { splitSymbolArgs, uniqueSymbols } from "./symbol-args";
|
|
3
|
+
import { splitSymbolArgs, stripStrayEdges, uniqueSymbols } from "./symbol-args";
|
|
4
4
|
|
|
5
5
|
/** Header cells that name a spreadsheet's symbol column, compared without case, spaces or punctuation. */
|
|
6
6
|
const SYMBOL_HEADERS = new Set(["symbol", "symbols", "ticker", "tickers", "code", "stockcode", "securitycode", "instrument"]);
|
|
@@ -129,13 +129,25 @@ export class SymbolListError extends Error {
|
|
|
129
129
|
/**
|
|
130
130
|
* The symbols a command was given: as arguments, from `--file`, and from
|
|
131
131
|
* standard input when an argument is `-` or nothing else was given and input
|
|
132
|
-
* is piped in. Repeats are dropped.
|
|
132
|
+
* is piped in. Repeats are dropped. A separator stuck to a symbol in a file, or
|
|
133
|
+
* left on the end of what was typed (`UPST;`), is dropped with the quote marks
|
|
134
|
+
* around it, and each is named in `strays` so the command can say so.
|
|
133
135
|
*/
|
|
134
|
-
export async function readSymbolList(source: SymbolListSource): Promise<{ symbols: string[]; repeated: number }> {
|
|
136
|
+
export async function readSymbolList(source: SymbolListSource): Promise<{ symbols: string[]; repeated: number; strays: string[] }> {
|
|
135
137
|
const readStdin = source.readStdin ?? (() => Bun.stdin.text());
|
|
136
138
|
const wantsStdin = source.args.includes("-") || source.file === "-";
|
|
137
|
-
const
|
|
139
|
+
const typedArgs = source.args.filter((arg) => arg !== "-");
|
|
140
|
+
const typed = splitSymbolArgs(typedArgs);
|
|
138
141
|
const collected = [...typed];
|
|
142
|
+
// A separator left on the end of what was typed: `UPST;`, or a list that ends in a comma.
|
|
143
|
+
const strays = new Set<string>(stripStrayEdges(typedArgs.join(" ")).notes);
|
|
144
|
+
const addCells = (cells: readonly string[]) => {
|
|
145
|
+
for (const cell of cells) {
|
|
146
|
+
const cleaned = stripStrayEdges(cell);
|
|
147
|
+
cleaned.notes.forEach((note) => strays.add(note));
|
|
148
|
+
collected.push(cleaned.text);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
139
151
|
if (source.file != null && source.file !== "-") {
|
|
140
152
|
if (!source.file.trim() || source.file.startsWith("--")) throw new SymbolListError("--file needs a path, such as --file symbols.csv.");
|
|
141
153
|
let text: string;
|
|
@@ -145,10 +157,10 @@ export async function readSymbolList(source: SymbolListSource): Promise<{ symbol
|
|
|
145
157
|
const code = (error as NodeJS.ErrnoException).code;
|
|
146
158
|
throw new SymbolListError(code === "ENOENT" ? `No file at ${source.file}.` : `Could not read ${source.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
147
159
|
}
|
|
148
|
-
|
|
160
|
+
addCells(parseSymbolText(text));
|
|
149
161
|
}
|
|
150
162
|
if (wantsStdin || (typed.length === 0 && source.file == null && (source.stdinPiped ?? stdinIsPiped()))) {
|
|
151
|
-
|
|
163
|
+
addCells(parseSymbolText(await readStdin()));
|
|
152
164
|
}
|
|
153
|
-
return uniqueSymbols(collected);
|
|
165
|
+
return { ...uniqueSymbols(collected), strays: [...strays] };
|
|
154
166
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** A function the catalog finds for a word typed as a command (`comps`, `peers`). */
|
|
2
|
+
export interface CatalogCommandHit {
|
|
3
|
+
code: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** What the function takes after its code: `ticker`, `tickers`, `free-text`, `none`... */
|
|
6
|
+
argKind: string | null;
|
|
7
|
+
argPlaceholder: string | null;
|
|
8
|
+
/** It has a `--peers` switch that adds a symbol's competitors (RV). */
|
|
9
|
+
peers: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TICKER_ARGUMENTS = new Set(["ticker", "tickers", "symbol-list", "ticker-list"]);
|
|
13
|
+
|
|
14
|
+
/** A company name typed where a ticker goes (`duolingo`, `"coca cola"`): lowercase and longer than a ticker, or several words. */
|
|
15
|
+
function nameLike(arg: string): boolean {
|
|
16
|
+
return /\s/.test(arg.trim()) || /^[a-z]{6,}$/.test(arg.trim());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function shellWord(arg: string): string {
|
|
20
|
+
return /^[\w.:,=%+@/$^-]+$/.test(arg) ? arg : JSON.stringify(arg);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The command that runs `hit` with what the reader typed after the unknown
|
|
25
|
+
* word: their tickers (`gloomberb fn RV DUOL --peers`, the switch added when
|
|
26
|
+
* one symbol meets a function that picks its peers), or the function's
|
|
27
|
+
* placeholder and a search when they typed a company's name.
|
|
28
|
+
*/
|
|
29
|
+
function catalogHitCommand(hit: CatalogCommandHit, args: readonly string[]): { command: string; search: string | null } {
|
|
30
|
+
const typed = args.filter((arg) => arg.trim() && !arg.startsWith("-"));
|
|
31
|
+
const base = `gloomberb fn ${hit.code}`;
|
|
32
|
+
if (hit.argKind === "none") return { command: base, search: null };
|
|
33
|
+
const placeholder = hit.argPlaceholder ?? hit.argKind;
|
|
34
|
+
if (!hit.argKind || !TICKER_ARGUMENTS.has(hit.argKind)) {
|
|
35
|
+
const argument = typed.length > 0 ? typed.map(shellWord).join(" ") : placeholder ? `<${placeholder}>` : "";
|
|
36
|
+
return { command: [base, argument].filter(Boolean).join(" "), search: null };
|
|
37
|
+
}
|
|
38
|
+
const names = typed.filter(nameLike);
|
|
39
|
+
const single = typed.length === 1;
|
|
40
|
+
const peers = hit.peers && single ? " --peers" : "";
|
|
41
|
+
if (names.length > 0) {
|
|
42
|
+
return { command: `${base} <${single ? "ticker" : placeholder ?? "tickers"}>${peers}`, search: `gloomberb search ${names.map(shellWord).join(" ")}` };
|
|
43
|
+
}
|
|
44
|
+
return { command: `${base} ${typed.length > 0 ? typed.map(shellWord).join(" ") : `<${placeholder ?? "ticker"}>`}${peers}`, search: null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What an unknown command word is answered with when it names something the
|
|
49
|
+
* catalog has (`peers`, `comps`): one function as a command to run, several
|
|
50
|
+
* as a list, with the catalog search for more. Null when the catalog has
|
|
51
|
+
* nothing for it.
|
|
52
|
+
*/
|
|
53
|
+
export function catalogPointer(token: string, hits: readonly CatalogCommandHit[], args: readonly string[]): string | null {
|
|
54
|
+
if (hits.length === 0) return null;
|
|
55
|
+
const word = /^[\w.:-]+$/.test(token) ? token : JSON.stringify(token);
|
|
56
|
+
const more = `gloomberb catalog ${word}`;
|
|
57
|
+
if (hits.length === 1) {
|
|
58
|
+
const { command, search } = catalogHitCommand(hits[0]!, args);
|
|
59
|
+
return `Try \`${command}\` (${hits[0]!.label})${search ? `; \`${search}\` finds the ticker` : ""}. \`${more}\` lists more.`;
|
|
60
|
+
}
|
|
61
|
+
const commands = hits.map((hit) => catalogHitCommand(hit, args));
|
|
62
|
+
const width = Math.max(...commands.map(({ command }) => command.length));
|
|
63
|
+
const searches = [...new Set(commands.flatMap(({ search }) => search ?? []))];
|
|
64
|
+
return [
|
|
65
|
+
"Closest functions:",
|
|
66
|
+
...commands.map(({ command }, index) => ` ${command.padEnd(width)} ${hits[index]!.label}`),
|
|
67
|
+
...searches.map((search) => `Find the ticker: ${search}`),
|
|
68
|
+
`Search the catalog: ${more}`,
|
|
69
|
+
].join("\n");
|
|
70
|
+
}
|
|
@@ -29,27 +29,57 @@ export function resolveNativeSurfaceVisibleRect(
|
|
|
29
29
|
): CellRect | null {
|
|
30
30
|
if (!renderable) return null;
|
|
31
31
|
|
|
32
|
+
const cached = visibleRectCache.get(renderable);
|
|
33
|
+
if (cached && cached.terminalWidth === terminalWidth && cached.terminalHeight === terminalHeight && ancestorChainUnchanged(renderable, cached.chain)) {
|
|
34
|
+
return cached.visible;
|
|
35
|
+
}
|
|
36
|
+
const chain: Array<CellRect & { visible: boolean }> = [];
|
|
37
|
+
let current: NativeSurfaceRenderableNode | null = renderable;
|
|
38
|
+
const seen = new Set<NativeSurfaceRenderableNode>();
|
|
39
|
+
let depth = 0;
|
|
32
40
|
let visible: CellRect = {
|
|
33
41
|
x: 0,
|
|
34
42
|
y: 0,
|
|
35
43
|
width: terminalWidth,
|
|
36
44
|
height: terminalHeight,
|
|
37
45
|
};
|
|
38
|
-
let current: NativeSurfaceRenderableNode | null = renderable;
|
|
39
|
-
const seen = new Set<NativeSurfaceRenderableNode>();
|
|
40
|
-
let depth = 0;
|
|
41
|
-
|
|
42
46
|
while (current && !seen.has(current)) {
|
|
43
47
|
if (depth >= MAX_RENDERABLE_ANCESTOR_DEPTH) return null;
|
|
44
48
|
seen.add(current);
|
|
45
49
|
depth += 1;
|
|
46
50
|
if (current.visible === false) return null;
|
|
47
|
-
|
|
48
|
-
const nextVisible = intersectCellRects(visible,
|
|
51
|
+
const rect = getRenderableCellRect(current);
|
|
52
|
+
const nextVisible = intersectCellRects(visible, rect);
|
|
49
53
|
if (!nextVisible) return null;
|
|
54
|
+
chain.push({ ...rect, visible: true });
|
|
50
55
|
visible = nextVisible;
|
|
51
56
|
current = current.parent;
|
|
52
57
|
}
|
|
53
|
-
|
|
58
|
+
visibleRectCache.set(renderable, { terminalWidth, terminalHeight, chain, visible });
|
|
54
59
|
return visible;
|
|
55
60
|
}
|
|
61
|
+
|
|
62
|
+
function ancestorChainUnchanged(
|
|
63
|
+
renderable: NativeSurfaceRenderableNode,
|
|
64
|
+
chain: ReadonlyArray<CellRect & { visible: boolean }>,
|
|
65
|
+
): boolean {
|
|
66
|
+
let current: NativeSurfaceRenderableNode | null = renderable;
|
|
67
|
+
let index = 0;
|
|
68
|
+
const seen = new Set<NativeSurfaceRenderableNode>();
|
|
69
|
+
while (current && !seen.has(current)) {
|
|
70
|
+
if (index >= chain.length || index >= MAX_RENDERABLE_ANCESTOR_DEPTH) return false;
|
|
71
|
+
seen.add(current);
|
|
72
|
+
const cached = chain[index]!;
|
|
73
|
+
if (current.visible === false || current.x !== cached.x || current.y !== cached.y || current.width !== cached.width || current.height !== cached.height) return false;
|
|
74
|
+
current = current.parent;
|
|
75
|
+
index += 1;
|
|
76
|
+
}
|
|
77
|
+
return index === chain.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const visibleRectCache = new WeakMap<NativeSurfaceRenderableNode, {
|
|
81
|
+
terminalWidth: number;
|
|
82
|
+
terminalHeight: number;
|
|
83
|
+
chain: Array<CellRect & { visible: boolean }>;
|
|
84
|
+
visible: CellRect;
|
|
85
|
+
}>();
|