gloomberb 0.18.0 → 0.18.1
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/market-movers.ts +7 -0
- package/src/api-client/types.ts +9 -0
- package/src/cli/commands/fred-series.ts +15 -5
- package/src/cli/commands/market.ts +9 -5
- package/src/cli/commands/overview.ts +31 -6
- package/src/cli/commands/ticker.ts +5 -2
- package/src/cli/pane-functions/function-help.ts +4 -4
- package/src/cli/symbol-list.ts +51 -16
- package/src/components/chart/composite/composite-chart.tsx +11 -5
- package/src/components/chart/composite/panel-surface.tsx +6 -8
- package/src/components/chart/composite/price-axis-labels.tsx +2 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/workflow/collection-actions.ts +4 -0
- package/src/components/onboarding/wizard-positions.ts +8 -0
- package/src/docs/snapshot.json +52 -17
- package/src/plugins/builtin/analytics/index.tsx +1 -1
- package/src/plugins/builtin/analytics/risk-client.ts +44 -11
- package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
- package/src/plugins/builtin/analytics/risk-model.ts +79 -16
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
- package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
- package/src/plugins/builtin/custom-view/loader.ts +3 -2
- package/src/plugins/builtin/custom-view/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
- package/src/plugins/builtin/econ/calendar-model.ts +3 -2
- package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
- package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
- package/src/plugins/builtin/executives/data.ts +24 -8
- package/src/plugins/builtin/executives/index.tsx +2 -18
- package/src/plugins/builtin/executives/pane.tsx +34 -10
- package/src/plugins/builtin/market-movers/client.ts +2 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -3
- package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
- package/src/plugins/builtin/market-movers/session.ts +25 -10
- package/src/plugins/builtin/news/wire/feed-config.ts +4 -1
- package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
- package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
- package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
- package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
- package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
- package/src/plugins/builtin/notes/cli.ts +33 -8
- package/src/plugins/builtin/notes/export.ts +49 -3
- package/src/plugins/builtin/notes/files.ts +14 -3
- package/src/plugins/builtin/notes/index.tsx +3 -3
- package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
- package/src/plugins/builtin/notes/store.ts +13 -3
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
- package/src/plugins/builtin/portfolio-list/cli/render.ts +35 -5
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +2 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -3
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +10 -1
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
- package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
- package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
- package/src/plugins/builtin/thirteenf/api.ts +4 -1
- package/src/plugins/builtin/thirteenf/data.ts +5 -1
- package/src/plugins/builtin/thirteenf/headless.ts +3 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
- package/src/portfolio/cost-fx.ts +84 -0
- package/src/portfolio/mutations.ts +16 -2
- package/src/portfolio/position-metrics.ts +45 -4
- package/src/portfolio/position-quantity.ts +43 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
- package/src/sync/core-contributors.ts +1 -0
- package/src/team/collections.ts +2 -0
- package/src/tickers/metadata.ts +10 -0
- package/src/types/ticker.ts +9 -0
- package/src/utils/currency-units.ts +9 -0
package/package.json
CHANGED
|
@@ -50,6 +50,13 @@ export interface CloudSessionMoversPayload {
|
|
|
50
50
|
phase: CloudSessionPhase;
|
|
51
51
|
asOf: string;
|
|
52
52
|
stale?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* `delayed` for an account without Pro: the list as it was built at least
|
|
55
|
+
* `delayMinutes` before it was served, as old as that account's quotes,
|
|
56
|
+
* and `asOf` that time. Absent on Pro's live list.
|
|
57
|
+
*/
|
|
58
|
+
access?: "delayed";
|
|
59
|
+
delayMinutes?: number;
|
|
53
60
|
items: CloudSessionMoverItem[];
|
|
54
61
|
}
|
|
55
62
|
|
package/src/api-client/types.ts
CHANGED
|
@@ -1005,6 +1005,13 @@ interface CloudProxyStatementSummaryPayload {
|
|
|
1005
1005
|
}
|
|
1006
1006
|
|
|
1007
1007
|
export interface CloudProxyStatementPayload extends CloudProxyStatementSummaryPayload {
|
|
1008
|
+
/**
|
|
1009
|
+
* `preview` for an account without Pro: each executive's total pay from
|
|
1010
|
+
* the latest proxy, with the breakdown, the year before, the pay ratio,
|
|
1011
|
+
* say-on-pay, highlights, key figures and other years null or empty.
|
|
1012
|
+
* Absent on Pro's full statement.
|
|
1013
|
+
*/
|
|
1014
|
+
access?: "preview";
|
|
1008
1015
|
docUrl: string;
|
|
1009
1016
|
ceo: (CloudExecutiveRowPayload & { priorYearTotal: number | null }) | null;
|
|
1010
1017
|
namedExecutives: CloudExecutiveRowPayload[];
|
|
@@ -1126,6 +1133,8 @@ export interface CloudProxyStatementListPayload {
|
|
|
1126
1133
|
shortName: string;
|
|
1127
1134
|
};
|
|
1128
1135
|
proxies: CloudProxyStatementSummaryPayload[];
|
|
1136
|
+
/** `preview` for an account without Pro: the latest proxy alone, without the year before or the pay ratio. */
|
|
1137
|
+
access?: "preview";
|
|
1129
1138
|
}
|
|
1130
1139
|
|
|
1131
1140
|
export interface CloudTranscriptTurnPayload {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { seriesMoves, type MoveHorizon } from "../../plugins/builtin/shared/move-rank";
|
|
2
|
+
import { seriesNotOnFred } from "../../sources/gloomberb-cloud/fred-series";
|
|
2
3
|
import { ICE_SPREAD_SERIES } from "../../time-series/ice-credit";
|
|
3
4
|
import { toBasisPoints } from "../../utils/basis-points";
|
|
4
5
|
import { parseReportTime } from "../../utils/utc-time";
|
|
@@ -138,7 +139,10 @@ export interface FredProvenance {
|
|
|
138
139
|
* looking for the figure in FRED's own data knows it is not there yet:
|
|
139
140
|
* `U.S. Treasury via FRED, published 2026-10-09`. Null for FRED's own.
|
|
140
141
|
*/
|
|
141
|
-
export function fredProvenance(
|
|
142
|
+
export function fredProvenance(
|
|
143
|
+
observations: ReadonlyArray<{ date: string; publisher?: string; publishedAt?: string }>,
|
|
144
|
+
seriesId?: string,
|
|
145
|
+
): FredProvenance | null {
|
|
142
146
|
const ahead = observations.filter((observation) => observation.publisher?.trim());
|
|
143
147
|
if (ahead.length === 0) return null;
|
|
144
148
|
const publishers = [...new Set(ahead.map((observation) => observation.publisher!.trim()))];
|
|
@@ -149,7 +153,8 @@ export function fredProvenance(observations: ReadonlyArray<{ date: string; publi
|
|
|
149
153
|
const publishedAt = times.at(-1);
|
|
150
154
|
const publisher = publishers.join(" and ");
|
|
151
155
|
return {
|
|
152
|
-
|
|
156
|
+
// A series FRED does not carry names whose publication it is read from instead (TurkStat via the CBRT).
|
|
157
|
+
source: `${publisher} via ${(seriesId ? seriesNotOnFred(seriesId)?.via : null) ?? "FRED"}${publishedAt ? `, published ${publishedAt.slice(0, 10)}` : ""}`,
|
|
153
158
|
publisher,
|
|
154
159
|
...(publishedAt ? { publishedAt } : {}),
|
|
155
160
|
};
|
|
@@ -275,6 +280,11 @@ export interface FredListRow {
|
|
|
275
280
|
group: string;
|
|
276
281
|
}
|
|
277
282
|
|
|
283
|
+
/** Lower case without accents, so `turk` and `turkiye` find Türkiye. */
|
|
284
|
+
function foldAccents(text: string): string {
|
|
285
|
+
return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
286
|
+
}
|
|
287
|
+
|
|
278
288
|
/** How well a word of the filter matches a series, best first: its id or group name, a word of its group or title, the start of its id, then any text. */
|
|
279
289
|
function matchTier(term: string, row: FredListRow, words: readonly string[]): number {
|
|
280
290
|
if (row.id.toLowerCase() === term || row.group.toLowerCase() === term) return 0;
|
|
@@ -290,11 +300,11 @@ function matchTier(term: string, row: FredListRow, words: readonly string[]): nu
|
|
|
290
300
|
* Catalog order breaks ties; an empty filter keeps the catalog as it is.
|
|
291
301
|
*/
|
|
292
302
|
export function filterFredSeries(rows: readonly FredListRow[], filter: string): FredListRow[] {
|
|
293
|
-
const terms = filter
|
|
303
|
+
const terms = foldAccents(filter).split(/\s+/).filter(Boolean);
|
|
294
304
|
const ranked = rows.flatMap((row, index) => {
|
|
295
|
-
const text = `${row.id} ${row.title} ${row.group}
|
|
305
|
+
const text = foldAccents(`${row.id} ${row.title} ${row.group}`);
|
|
296
306
|
if (!terms.every((term) => text.includes(term))) return [];
|
|
297
|
-
const words = `${row.title} ${row.group}
|
|
307
|
+
const words = foldAccents(`${row.title} ${row.group}`).split(/[^a-z0-9]+/).filter(Boolean);
|
|
298
308
|
const tiers = terms.map((term) => matchTier(term, row, words));
|
|
299
309
|
return [{ row, index, worst: Math.max(0, ...tiers), total: tiers.reduce((sum, tier) => sum + tier, 0) }];
|
|
300
310
|
});
|
|
@@ -297,8 +297,9 @@ async function readSymbols(symbols: readonly string[], dataProvider: Parameters<
|
|
|
297
297
|
|
|
298
298
|
/**
|
|
299
299
|
* The symbols of `--file <path>` or standard input (`-`), with any typed
|
|
300
|
-
* beside them, read by the parser `watchlist add` uses
|
|
301
|
-
* only when asked for, so a script that
|
|
300
|
+
* beside them, read by the parser `watchlist add` uses, and its notes on what
|
|
301
|
+
* it left out. Standard input is read only when asked for, so a script that
|
|
302
|
+
* pipes in nothing never waits on it.
|
|
302
303
|
*/
|
|
303
304
|
async function readListedSymbols(
|
|
304
305
|
args: readonly string[],
|
|
@@ -306,13 +307,13 @@ async function readListedSymbols(
|
|
|
306
307
|
market: MarketContext,
|
|
307
308
|
ctx: Parameters<CliCommandDef["execute"]>[1],
|
|
308
309
|
usage: string,
|
|
309
|
-
): Promise<string[]> {
|
|
310
|
+
): Promise<{ symbols: string[]; notes: string[] }> {
|
|
310
311
|
try {
|
|
311
312
|
const list = await readSymbolList({
|
|
312
313
|
args, file, stdinPiped: false, readEntries: (entries) => readNameEntries(entries, market.dataProvider),
|
|
313
314
|
});
|
|
314
315
|
if (list.symbols.length === 0) ctx.fail(`No symbols in ${file && file !== "-" ? file : "standard input"}.`, usage);
|
|
315
|
-
return uniqueEntries(list.symbols);
|
|
316
|
+
return { symbols: uniqueEntries(list.symbols), notes: list.notes };
|
|
316
317
|
} catch (error) {
|
|
317
318
|
if (error instanceof SymbolListError) ctx.fail(error.message, usage);
|
|
318
319
|
throw error;
|
|
@@ -718,9 +719,11 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
718
719
|
await withMarketData(ctx, async (market) => {
|
|
719
720
|
const read = phrase ? await readPhraseArgument(phrase, market.dataProvider) : null;
|
|
720
721
|
if (read?.kind === "phrase") return answerPhrase(read.phrase, market, ctx);
|
|
722
|
+
// A list read from a file names what it left out: a stray separator, the cells beside a table's symbols.
|
|
723
|
+
const list = listed && !read ? await readListedSymbols(args, file, market, ctx, usage) : null;
|
|
721
724
|
const symbols = read?.kind === "name" ? [read.name]
|
|
722
725
|
: read?.kind === "symbols" ? uniqueEntries(read.symbols)
|
|
723
|
-
:
|
|
726
|
+
: list ? list.symbols
|
|
724
727
|
: await readSymbols(entries, market.dataProvider);
|
|
725
728
|
// With several symbols, --exchange is for the ones that name no exchange of their own.
|
|
726
729
|
const listings = await Promise.all(symbols.map((symbol) => requireCliListing(
|
|
@@ -791,6 +794,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
791
794
|
...formerIssuers[index] ? { formerIssuer: formerIssuers[index] } : {},
|
|
792
795
|
}));
|
|
793
796
|
const notes = [
|
|
797
|
+
...list?.notes ?? [],
|
|
794
798
|
// FB now lists another security; the company that was FB trades as META. A symbol a
|
|
795
799
|
// company renamed into (META) is what was typed: its change is in the row, not here.
|
|
796
800
|
// GOGL was Golden Ocean, which merged into CMB.Tech; a fund lists there now.
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../../plugins/builtin/market-movers/model";
|
|
23
23
|
import { MOVER_CANDIDATES } from "../../plugins/builtin/market-movers/client";
|
|
24
24
|
import { fetchEconCalendarFrom } from "../../plugins/builtin/econ/calendar-source";
|
|
25
|
+
import { seriesNotOnFred } from "../../sources/gloomberb-cloud/fred-series";
|
|
25
26
|
import { EXTENDED_SESSION_LABELS, getQuoteSessionFields, type ExtendedSession } from "../../market-data/market/status";
|
|
26
27
|
import {
|
|
27
28
|
COUNTRY_CYCLE,
|
|
@@ -311,9 +312,27 @@ function parseFilter<T extends string>(value: string | undefined, values: readon
|
|
|
311
312
|
return match;
|
|
312
313
|
}
|
|
313
314
|
|
|
315
|
+
/** One of the pane's regions, or any country by its two-letter code (TR, DE), upper-cased. */
|
|
316
|
+
function parseCountry(value: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]): CountryFilter {
|
|
317
|
+
if (value == null) return COUNTRY_CYCLE[0]!;
|
|
318
|
+
const text = value.trim();
|
|
319
|
+
const region = COUNTRY_CYCLE.find((candidate) => candidate.toLowerCase() === text.toLowerCase());
|
|
320
|
+
if (region) return region;
|
|
321
|
+
if (/^[a-z]{2}$/i.test(text)) return text.toUpperCase();
|
|
322
|
+
return ctx.fail(`Unknown country "${value}".`, `Use ${COUNTRY_CYCLE.join(", ")}, or a two-letter country code such as TR.`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** A country the calendar lists nothing for says which ones it does list, so a typo (UK for GB) is not read as a quiet week. */
|
|
326
|
+
function unlistedCountryMessage(country: CountryFilter, events: ReadonlyArray<{ country: string }>): string | null {
|
|
327
|
+
if ((COUNTRY_CYCLE as readonly string[]).includes(country)) return null;
|
|
328
|
+
const listed = [...new Set(events.map((event) => event.country).filter((code) => /^[A-Z]{2}$/.test(code)))].sort();
|
|
329
|
+
if (listed.length === 0 || listed.includes(country)) return null;
|
|
330
|
+
return `The calendar lists no ${country} events; it lists ${listed.join(", ")}.`;
|
|
331
|
+
}
|
|
332
|
+
|
|
314
333
|
async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
315
334
|
const rawArgs = [...args];
|
|
316
|
-
const country
|
|
335
|
+
const country = parseCountry(takeOption(rawArgs, "--country"), ctx);
|
|
317
336
|
const impact: ImpactFilter = parseFilter(takeOption(rawArgs, "--impact"), FILTER_CYCLE, "impact", ctx);
|
|
318
337
|
const fromOption = parseDateOption(takeOption(rawArgs, "--from"), "--from", ctx);
|
|
319
338
|
rejectArgs(rawArgs, ECON_USAGE, ctx, "Filter with --country and --impact.");
|
|
@@ -363,7 +382,7 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
|
|
|
363
382
|
}, {
|
|
364
383
|
dateKey: "date",
|
|
365
384
|
defaultLimit: 50,
|
|
366
|
-
empty: econEmptyMessage(from, listedThrough, listedAfter != null),
|
|
385
|
+
empty: unlistedCountryMessage(country, events) ?? econEmptyMessage(from, listedThrough, listedAfter != null),
|
|
367
386
|
...(footnote ? { footnote } : {}),
|
|
368
387
|
columns: [
|
|
369
388
|
// Text shows both halves of the event timestamp in UTC, never the host's zone; exports keep the source values.
|
|
@@ -485,7 +504,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
485
504
|
const periodicity = periodicityOf(data.info?.frequency);
|
|
486
505
|
const newestDate = rows.reduce<string | null>((newest, row) => (!newest || row.date > newest ? row.date : newest), null);
|
|
487
506
|
// A newest value FRED does not list yet names its publisher, Treasury for a yield; FRED's own stay "FRED".
|
|
488
|
-
const ahead = fredProvenance(data.observations.filter((observation) => observation.date === newestDate));
|
|
507
|
+
const ahead = fredProvenance(data.observations.filter((observation) => observation.date === newestDate), seriesId);
|
|
489
508
|
// FRED names the series' publisher when Gloom Cloud passes it on, and when it last updated the series.
|
|
490
509
|
const publisher = ahead?.publisher ?? data.info?.source?.trim();
|
|
491
510
|
const publishedAt = ahead ? ahead.publishedAt : fredPublicationTime(data.info, newestDate);
|
|
@@ -498,7 +517,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
498
517
|
...(latest ? { latestObservation: exportRow(latest) } : {}),
|
|
499
518
|
},
|
|
500
519
|
freshness: rowsFreshness(rows, {
|
|
501
|
-
source: ahead?.source ?? "FRED", status: "not-a-feed", basis: "published statistics", observedKey: "date", oldest: null,
|
|
520
|
+
source: ahead?.source ?? notOnFredSource(seriesId) ?? "FRED", status: "not-a-feed", basis: "published statistics", observedKey: "date", oldest: null,
|
|
502
521
|
// A monthly or quarterly observation is dated the first of its period, not a day it was seen.
|
|
503
522
|
...(periodicity ? { periodicity } : {}),
|
|
504
523
|
...(publisher ? { publisher } : {}),
|
|
@@ -522,6 +541,12 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
522
541
|
});
|
|
523
542
|
}
|
|
524
543
|
|
|
544
|
+
/** Who a series FRED does not carry comes from, for a source line whose rows do not name it: `TurkStat via the CBRT`. */
|
|
545
|
+
function notOnFredSource(seriesId: string): string | null {
|
|
546
|
+
const elsewhere = seriesNotOnFred(seriesId);
|
|
547
|
+
return elsewhere ? `${elsewhere.publisher} via ${elsewhere.via}` : null;
|
|
548
|
+
}
|
|
549
|
+
|
|
525
550
|
const YIELD_TENORS: Record<string, string> = { DGS3MO: "3M", DGS2: "2Y", DGS10: "10Y", DGS30: "30Y" };
|
|
526
551
|
|
|
527
552
|
async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
@@ -672,11 +697,11 @@ export const overviewCliCommands: CliCommandDef[] = [
|
|
|
672
697
|
group: CLI_COMMAND_GROUPS.markets,
|
|
673
698
|
usage: [ECON_USAGE],
|
|
674
699
|
options: [
|
|
675
|
-
{ flags: "--country <region>", description: "US, G7, EU,
|
|
700
|
+
{ flags: "--country <region>", description: "US, G7, EU, all (default), or one country by its two-letter code, such as TR" },
|
|
676
701
|
{ flags: "--impact <level>", description: "high, medium, low, or all (default all)" },
|
|
677
702
|
{ flags: "--from <date>", description: `First day listed, as ${DATE_OPTION_EXAMPLES} (default today, UTC); an earlier day lists past events from the stored calendar and says where it starts` },
|
|
678
703
|
],
|
|
679
|
-
examples: ["econ", "econ --country US --impact high", "econ --from 2026-10-12 --country US", "econ --tail 20"],
|
|
704
|
+
examples: ["econ", "econ --country US --impact high", "econ --country TR", "econ --from 2026-10-12 --country US", "econ --tail 20"],
|
|
680
705
|
},
|
|
681
706
|
execute: runEcon,
|
|
682
707
|
},
|
|
@@ -44,7 +44,7 @@ import type { SecFilingItem } from "../../types/data-provider";
|
|
|
44
44
|
import type { NewsArticle } from "../../news/types";
|
|
45
45
|
import type { TickerRecord } from "../../types/ticker";
|
|
46
46
|
import type { CliCommandContext } from "../../types/plugin";
|
|
47
|
-
import { getPortfolioPositionMetrics, getPortfolioQuoteDisplay, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../portfolio/position-metrics";
|
|
47
|
+
import { getPortfolioPositionMetrics, getPortfolioQuoteDisplay, lotCostFxRate, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../portfolio/position-metrics";
|
|
48
48
|
import { isFuturesTicker } from "../../portfolio/futures-positions";
|
|
49
49
|
import { createBaseConverter } from "../base-converter";
|
|
50
50
|
import { initMarketData, withMarketData } from "../context";
|
|
@@ -318,10 +318,13 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
|
|
|
318
318
|
const positionCurrency = metrics.positionCurrency;
|
|
319
319
|
const currentPrice = getPortfolioQuoteDisplay(metrics, quote)?.price ?? null;
|
|
320
320
|
const currentPriceBase = currentPrice != null && quoteCurrency ? await toBase(currentPrice, quoteCurrency) : null;
|
|
321
|
-
const costBasisBase = positionCurrency ? await toBase(metrics.signedCost, positionCurrency) : Number.NaN;
|
|
322
321
|
const positionRate = positionCurrency ? await toBase(1, positionCurrency) : Number.NaN;
|
|
323
322
|
const baseMetrics = getPortfolioPositionMetrics({ ...tickerFile, metadata: { ...tickerFile.metadata, positions: [position] } }, undefined, quoteCurrency,
|
|
324
323
|
{ currency: config.baseCurrency, convert: value => value * positionRate }, quote);
|
|
324
|
+
// A cost bought at an entered rate into the base currency converts at it, as its P&L does; any other at today's.
|
|
325
|
+
const costBasisBase = !positionCurrency ? Number.NaN
|
|
326
|
+
: lotCostFxRate(position, positionCurrency, config.baseCurrency) !== null ? baseMetrics.signedCost
|
|
327
|
+
: await toBase(metrics.signedCost, positionCurrency);
|
|
325
328
|
const marketValueBase = resolvePortfolioMarketValue(baseMetrics, currentPriceBase)?.net ?? Number.NaN;
|
|
326
329
|
const selectedPnl = resolvePortfolioPositionPnl(baseMetrics, currentPriceBase);
|
|
327
330
|
const pnl = selectedPnl.value;
|
|
@@ -445,8 +445,8 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
445
445
|
summary: "Named executive officers and what they were paid, read from the proxy statement and checked against the filing.",
|
|
446
446
|
usage: ["EXEC NVDA"],
|
|
447
447
|
keys: [key("y", "ear"), key("o", "pen filing")],
|
|
448
|
-
data: pro
|
|
449
|
-
access: "
|
|
448
|
+
data: { free: "As filed; each executive's total pay from the latest proxy", pro: "As filed; the pay breakdown and every year" },
|
|
449
|
+
access: "preview",
|
|
450
450
|
bloomberg: ["MGMT"],
|
|
451
451
|
},
|
|
452
452
|
EK: {
|
|
@@ -681,10 +681,10 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
681
681
|
|
|
682
682
|
// Watch markets
|
|
683
683
|
MOST: {
|
|
684
|
-
summary: "Today's top gainers, losers, most active and trending tickers with price, change, volume against average and market cap
|
|
684
|
+
summary: "Today's top gainers, losers, most active and trending tickers with price, change, volume against average and market cap, and the pre-market and after-hours movers. On Pro, the gaps at the open.",
|
|
685
685
|
usage: ["MOST"],
|
|
686
686
|
keys: [TABS, OPEN],
|
|
687
|
-
data: { free: DELAYED, pro:
|
|
687
|
+
data: { free: DELAYED, pro: REAL_TIME },
|
|
688
688
|
bloomberg: ["MOST"],
|
|
689
689
|
},
|
|
690
690
|
WEI: {
|
package/src/cli/symbol-list.ts
CHANGED
|
@@ -74,28 +74,59 @@ function looksLikeSymbol(cell: string): boolean {
|
|
|
74
74
|
return SYMBOL_TOKEN.test(cell) && !(NUMBER_CELL.test(cell) && /[.,%]/.test(cell));
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** How many cells a table's ignored-cells note names before it counts the rest. */
|
|
78
|
+
const IGNORED_PREVIEW = 8;
|
|
79
|
+
|
|
80
|
+
/** What a file or piped text holds: its symbols in order, and the cells of a table read by one column that were left out. */
|
|
81
|
+
interface SymbolText {
|
|
82
|
+
symbols: string[];
|
|
83
|
+
/** Cells beside the symbol column of a table with no header, in reading order; empty for a list or a CSV with a header. */
|
|
84
|
+
ignored: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
/**
|
|
78
88
|
* The symbols in a file or piped text, in order. A spreadsheet export reads
|
|
79
|
-
* from the column its header names (Symbol, Ticker, Code)
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* are ignored.
|
|
89
|
+
* from the column its header names (Symbol, Ticker, Code). Without a header,
|
|
90
|
+
* every cell is a symbol unless some column after the first holds a name, a
|
|
91
|
+
* price or a weight: that table is read by its first column, and the cells
|
|
92
|
+
* beside it are returned as `ignored` so the command can name them. A list
|
|
93
|
+
* split by commas, spaces or newlines gives every entry, even when a line of
|
|
94
|
+
* it is ragged or carries a stray separator (`"BIMAS ,"`). A byte order mark,
|
|
95
|
+
* Windows line ends, blank lines and lines starting with # or // are ignored.
|
|
83
96
|
*/
|
|
84
|
-
export function parseSymbolText(text: string):
|
|
97
|
+
export function parseSymbolText(text: string): SymbolText {
|
|
85
98
|
const normalized = text.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
|
|
86
99
|
const lines = normalized.split("\n");
|
|
87
100
|
const delimiter = detectDelimiter(lines);
|
|
88
101
|
const records = (delimiter ? parseDelimited(normalized, delimiter) : lines.map((line) => line.trim().split(/\s+/)))
|
|
89
102
|
.filter((cells) => cells.some(Boolean) && !isComment(cells[0] ?? ""));
|
|
90
|
-
if (records.length === 0) return [];
|
|
103
|
+
if (records.length === 0) return { symbols: [], ignored: [] };
|
|
91
104
|
const header = records[0]!.map(headerKey);
|
|
92
105
|
const exact = header.findIndex((cell) => SYMBOL_HEADERS.has(cell));
|
|
93
106
|
const column = exact >= 0 ? exact : header.findIndex((cell) => cell.includes("symbol") || cell.includes("ticker"));
|
|
94
|
-
if (column >= 0) return records.slice(1).map((cells) => cells[column] ?? "").filter(Boolean);
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
const grid =
|
|
98
|
-
|
|
107
|
+
if (column >= 0) return { symbols: records.slice(1).map((cells) => cells[column] ?? "").filter(Boolean), ignored: [] };
|
|
108
|
+
// A separator or quote mark stuck to a cell is dropped (and named) when it is read, so it does not make the list a table.
|
|
109
|
+
const cleaned = records.map((cells) => cells.map((cell) => stripStrayEdges(cell).text));
|
|
110
|
+
const grid = cleaned.every((cells) => cells.every((cell, index) => !cell || (looksLikeSymbol(cell) && (index === 0 || !NUMBER_CELL.test(cell)))));
|
|
111
|
+
if (grid) return { symbols: records.flat().filter(Boolean), ignored: [] };
|
|
112
|
+
return {
|
|
113
|
+
symbols: records.map((cells) => cells[0] ?? "").filter(Boolean),
|
|
114
|
+
ignored: records.flatMap((cells) => cells.slice(1)).filter(Boolean),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The note a table read by its first column gets, naming what was left out:
|
|
120
|
+
* `Read the first column as symbols; ignored the 4 cells beside it: Apple
|
|
121
|
+
* Inc., 336.64, Microsoft, 535.07.` Long tables name the first few and count
|
|
122
|
+
* the rest. Null when nothing was ignored.
|
|
123
|
+
*/
|
|
124
|
+
function ignoredCellsNote(ignored: readonly string[]): string | null {
|
|
125
|
+
if (ignored.length === 0) return null;
|
|
126
|
+
const named = ignored.slice(0, IGNORED_PREVIEW).join(", ");
|
|
127
|
+
const more = ignored.length - IGNORED_PREVIEW;
|
|
128
|
+
const cells = ignored.length === 1 ? "the cell" : `the ${ignored.length} cells`;
|
|
129
|
+
return `Read the first column as symbols; ignored ${cells} beside it: ${named}${more > 0 ? ` and ${more} more` : ""}.`;
|
|
99
130
|
}
|
|
100
131
|
|
|
101
132
|
/** Whether standard input is a pipe or a file rather than a terminal, so a command may read its list from it. */
|
|
@@ -138,9 +169,10 @@ export class SymbolListError extends Error {
|
|
|
138
169
|
* standard input when an argument is `-` or nothing else was given and input
|
|
139
170
|
* is piped in. Repeats are dropped. A separator stuck to a symbol in a file, or
|
|
140
171
|
* left on the end of what was typed (`UPST;`), is dropped with the quote marks
|
|
141
|
-
* around it, and
|
|
172
|
+
* around it, and the cells beside the symbols of a table read by its first
|
|
173
|
+
* column are left out; `notes` names each so the command can say so.
|
|
142
174
|
*/
|
|
143
|
-
export async function readSymbolList(source: SymbolListSource): Promise<{ symbols: string[]; repeated: number;
|
|
175
|
+
export async function readSymbolList(source: SymbolListSource): Promise<{ symbols: string[]; repeated: number; notes: string[] }> {
|
|
144
176
|
const readStdin = source.readStdin ?? (() => Bun.stdin.text());
|
|
145
177
|
const wantsStdin = source.args.includes("-") || source.file === "-";
|
|
146
178
|
const typedArgs = source.args.filter((arg) => arg !== "-");
|
|
@@ -148,12 +180,14 @@ export async function readSymbolList(source: SymbolListSource): Promise<{ symbol
|
|
|
148
180
|
const collected = [...typed];
|
|
149
181
|
// A separator left on the end of what was typed: `UPST;`, or a list that ends in a comma.
|
|
150
182
|
const strays = new Set<string>(stripStrayEdges(typedArgs.join(" ")).notes);
|
|
151
|
-
const
|
|
152
|
-
|
|
183
|
+
const ignored: string[] = [];
|
|
184
|
+
const addCells = ({ symbols, ignored: beside }: SymbolText) => {
|
|
185
|
+
for (const cell of symbols) {
|
|
153
186
|
const cleaned = stripStrayEdges(cell);
|
|
154
187
|
cleaned.notes.forEach((note) => strays.add(note));
|
|
155
188
|
collected.push(cleaned.text);
|
|
156
189
|
}
|
|
190
|
+
ignored.push(...beside);
|
|
157
191
|
};
|
|
158
192
|
if (source.file != null && source.file !== "-") {
|
|
159
193
|
if (!source.file.trim() || source.file.startsWith("--")) throw new SymbolListError("--file needs a path, such as --file symbols.csv.");
|
|
@@ -169,5 +203,6 @@ export async function readSymbolList(source: SymbolListSource): Promise<{ symbol
|
|
|
169
203
|
if (wantsStdin || (typed.length === 0 && source.file == null && (source.stdinPiped ?? stdinIsPiped()))) {
|
|
170
204
|
addCells(parseSymbolText(await readStdin()));
|
|
171
205
|
}
|
|
172
|
-
|
|
206
|
+
const ignoredNote = ignoredCellsNote(ignored);
|
|
207
|
+
return { ...uniqueSymbols(collected), notes: [...strays, ...ignoredNote ? [ignoredNote] : []] };
|
|
173
208
|
}
|
|
@@ -186,7 +186,13 @@ function ThemedCompositeChart({
|
|
|
186
186
|
(drawing) => drawing.id === selectedDrawingId ? { ...drawing, color } : drawing,
|
|
187
187
|
));
|
|
188
188
|
}, [selectedDrawingId]);
|
|
189
|
-
|
|
189
|
+
// A cursor the owner rests on without following its moves (a seasonal
|
|
190
|
+
// chart's latest week) is where the chart's own cursor goes back to: the
|
|
191
|
+
// pointer moves it as on any chart, and the pointer leaving returns it.
|
|
192
|
+
const restingCursor = cursorDate !== undefined && !onCursorDateChange;
|
|
193
|
+
const ownsCursor = cursorDate === undefined || restingCursor;
|
|
194
|
+
const resolvedCursorDate = cursorDate === undefined ? internalCursorDate
|
|
195
|
+
: restingCursor ? internalCursorDate ?? cursorDate : cursorDate;
|
|
190
196
|
const totalWidth = Math.max(1, Math.floor(width));
|
|
191
197
|
const totalHeight = Math.max(1, Math.floor(height));
|
|
192
198
|
const seriesIdentityRef = useRef<ResolvedSeries[]>([]);
|
|
@@ -417,18 +423,18 @@ function ThemedCompositeChart({
|
|
|
417
423
|
const nextTimestamp = timestamp !== null && Number.isFinite(timestamp) ? timestamp : null;
|
|
418
424
|
if (lastCursorTimestampRef.current === nextTimestamp) return;
|
|
419
425
|
lastCursorTimestampRef.current = nextTimestamp;
|
|
420
|
-
if (
|
|
426
|
+
if (ownsCursor) setInternalCursorDate(date);
|
|
421
427
|
onCursorDateChange?.(date);
|
|
422
428
|
publishCursor?.(date && baseScene && nextTimestamp !== null ? compositeCursorAsOf(baseScene, date) : null);
|
|
423
|
-
}, [baseScene,
|
|
429
|
+
}, [baseScene, onCursorDateChange, ownsCursor, publishCursor]);
|
|
424
430
|
// Another chart taking the cursor, or clearing it, takes it from this one,
|
|
425
431
|
// so it does not come back here later. Read live: a move since the render
|
|
426
432
|
// makes this chart's cursor the shared one again.
|
|
427
433
|
useEffect(() => {
|
|
428
434
|
void holdsSharedCursor;
|
|
429
|
-
if (!link ||
|
|
435
|
+
if (!link || !ownsCursor || lastCursorTimestampRef.current === null || link.held()) return;
|
|
430
436
|
updateCursor(null);
|
|
431
|
-
}, [
|
|
437
|
+
}, [holdsSharedCursor, link, ownsCursor, updateCursor]);
|
|
432
438
|
// Esc on a chart showing another chart's cursor clears it, as on that chart.
|
|
433
439
|
// While the tab's cursor is pinned the arrows move the pin and Esc unpins it.
|
|
434
440
|
const updateKeyboardCursor = useCallback((date: Date | null) => {
|
|
@@ -207,14 +207,12 @@ export function CompositePanelSurface({
|
|
|
207
207
|
const plotSpanFactor = 1 + compositeRightOffsetRatio(scene.timeScale);
|
|
208
208
|
const bitmapSize = useStaticChartBitmapSize(plotSize.width, plotSize.height);
|
|
209
209
|
const bitmap = useCompositePanelBitmap({ panel, bitmapSize, colors, isDesktopWeb, revealed: plotSize.revealed === true });
|
|
210
|
-
// The
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
const readsPointerLevel = !isDesktopWeb || armedTool !== null || activeDrag !== null;
|
|
217
|
-
const pointerCursorYRatio = scene.cursorXRatio === null || !readsPointerLevel ? null : heldCursorYRatio;
|
|
210
|
+
// The chart under the pointer draws both lines: the level line follows the
|
|
211
|
+
// pointer, and its value sits on the axes, beside each series' own tag on
|
|
212
|
+
// the desktop and the web. A keyboard or shared cursor knows its column
|
|
213
|
+
// only, so a chart following another chart's cursor draws the time line
|
|
214
|
+
// alone. A tool placed from the keyboard is the exception: its end is a level.
|
|
215
|
+
const pointerCursorYRatio = scene.cursorXRatio === null ? null : heldCursorYRatio;
|
|
218
216
|
const crosshair = useMemo(
|
|
219
217
|
() => resolvePanelCrosshair(panel, bitmap, scene, pointerCursorYRatio, colors.crosshair),
|
|
220
218
|
[bitmap, colors.crosshair, panel, pointerCursorYRatio, scene],
|
|
@@ -281,7 +281,8 @@ export function PriceAxisLabels({
|
|
|
281
281
|
transform: "translateY(-50%)",
|
|
282
282
|
whiteSpace: "pre",
|
|
283
283
|
pointerEvents: "none",
|
|
284
|
-
|
|
284
|
+
// The value under the pointer reads whole, over a series' tag at that height.
|
|
285
|
+
zIndex: 3,
|
|
285
286
|
}}
|
|
286
287
|
>
|
|
287
288
|
<Box flexDirection="row" width={axisWidth} justifyContent={axisLabelJustify} overflow="hidden">
|
|
@@ -274,6 +274,10 @@ export interface CompositeChartProps {
|
|
|
274
274
|
remoteKind?: string;
|
|
275
275
|
/** Permit panning into an older unloaded window so the owner can backfill it. */
|
|
276
276
|
allowHistoricalBackfill?: boolean;
|
|
277
|
+
/**
|
|
278
|
+
* The owner's cursor. With `onCursorDateChange` the owner moves it; without,
|
|
279
|
+
* the cursor rests there while no pointer is on the plot.
|
|
280
|
+
*/
|
|
277
281
|
cursorDate?: Date | null;
|
|
278
282
|
/**
|
|
279
283
|
* A crosshair shared with other charts. By default an interactive chart
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
validatePositionShares,
|
|
28
28
|
} from "../../../portfolio/mutations";
|
|
29
29
|
import { loadFuturesEntrySize } from "../../../portfolio/futures-terms";
|
|
30
|
+
import { wholeUnitQuantityProblem } from "../../../portfolio/position-quantity";
|
|
30
31
|
import type { CommandBarFieldValue } from "./types";
|
|
31
32
|
import type { WorkflowStringValues } from "./broker";
|
|
32
33
|
import { coerceFieldString } from "../helpers";
|
|
@@ -248,6 +249,9 @@ export function createCommandBarCollectionWorkflowActions(options: {
|
|
|
248
249
|
// Negative for a short; zero is no position. A future counts contracts, and the messages say so.
|
|
249
250
|
const quantityLabel = positionQuantityLabel(ticker);
|
|
250
251
|
const shares = readTypedNumber(coerceFieldString(values.shares), quantityLabel);
|
|
252
|
+
// 1.250 on a listing held in whole lots could be 1,250 as well, and asks the same way.
|
|
253
|
+
const lots = wholeUnitQuantityProblem(coerceFieldString(values.shares).trim(), ticker, { label: quantityLabel });
|
|
254
|
+
if (lots) throw new Error(lots);
|
|
251
255
|
const sharesError = validatePositionShares(shares, quantityLabel);
|
|
252
256
|
if (sharesError) {
|
|
253
257
|
throw new Error(t(sharesError));
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "../../portfolio/mutations";
|
|
13
13
|
import { isFuturesTicker } from "../../portfolio/futures-positions";
|
|
14
14
|
import { loadFuturesEntrySize } from "../../portfolio/futures-terms";
|
|
15
|
+
import { wholeUnitQuantityProblem } from "../../portfolio/position-quantity";
|
|
15
16
|
import { useAppDispatch, useAppSelector, useAppStateRef } from "../../state/app/context";
|
|
16
17
|
import { saveConfigImmediately } from "../../state/config-save-scheduler";
|
|
17
18
|
import { resolveTickerSearch, upsertTickerFromSearchResult, type ResolvedTickerSearch } from "../../tickers/search";
|
|
@@ -291,6 +292,13 @@ export function useOnboardingPositions({
|
|
|
291
292
|
if (shares === null) {
|
|
292
293
|
nextTicker = addTickerToPortfolio(ticker, portfolioId).ticker;
|
|
293
294
|
} else {
|
|
295
|
+
// 1.250 on a listing held in whole lots could be 1,250 as well, and asks as 1,250 does.
|
|
296
|
+
const lots = wholeUnitQuantityProblem(current.shares.trim(), ticker, { label: quantityLabel });
|
|
297
|
+
if (lots) {
|
|
298
|
+
setFieldIdx(1);
|
|
299
|
+
setError(lots);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
294
302
|
const costBasis = avgCost ?? result.quote?.price;
|
|
295
303
|
if (costBasis === undefined || costBasis === null || !Number.isFinite(costBasis)) {
|
|
296
304
|
setFieldIdx(2);
|