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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/market-movers.ts +7 -0
  3. package/src/api-client/types.ts +9 -0
  4. package/src/cli/commands/fred-series.ts +15 -5
  5. package/src/cli/commands/market.ts +9 -5
  6. package/src/cli/commands/overview.ts +31 -6
  7. package/src/cli/commands/ticker.ts +5 -2
  8. package/src/cli/pane-functions/function-help.ts +4 -4
  9. package/src/cli/symbol-list.ts +51 -16
  10. package/src/components/chart/composite/composite-chart.tsx +11 -5
  11. package/src/components/chart/composite/panel-surface.tsx +6 -8
  12. package/src/components/chart/composite/price-axis-labels.tsx +2 -1
  13. package/src/components/chart/composite/types.ts +4 -0
  14. package/src/components/command-bar/workflow/collection-actions.ts +4 -0
  15. package/src/components/onboarding/wizard-positions.ts +8 -0
  16. package/src/docs/snapshot.json +52 -17
  17. package/src/plugins/builtin/analytics/index.tsx +1 -1
  18. package/src/plugins/builtin/analytics/risk-client.ts +44 -11
  19. package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
  20. package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
  21. package/src/plugins/builtin/analytics/risk-model.ts +79 -16
  22. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  23. package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
  24. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
  25. package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
  26. package/src/plugins/builtin/custom-view/loader.ts +3 -2
  27. package/src/plugins/builtin/custom-view/pane.tsx +5 -4
  28. package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
  29. package/src/plugins/builtin/econ/calendar-model.ts +3 -2
  30. package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
  31. package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
  32. package/src/plugins/builtin/executives/data.ts +24 -8
  33. package/src/plugins/builtin/executives/index.tsx +2 -18
  34. package/src/plugins/builtin/executives/pane.tsx +34 -10
  35. package/src/plugins/builtin/market-movers/client.ts +2 -2
  36. package/src/plugins/builtin/market-movers/index.tsx +0 -3
  37. package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
  38. package/src/plugins/builtin/market-movers/session.ts +25 -10
  39. package/src/plugins/builtin/news/wire/feed-config.ts +4 -1
  40. package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
  41. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
  42. package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
  43. package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
  44. package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
  45. package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
  46. package/src/plugins/builtin/notes/cli.ts +33 -8
  47. package/src/plugins/builtin/notes/export.ts +49 -3
  48. package/src/plugins/builtin/notes/files.ts +14 -3
  49. package/src/plugins/builtin/notes/index.tsx +3 -3
  50. package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
  51. package/src/plugins/builtin/notes/store.ts +13 -3
  52. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
  53. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
  54. package/src/plugins/builtin/portfolio-list/cli/render.ts +35 -5
  55. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
  56. package/src/plugins/builtin/portfolio-list/headless.ts +2 -0
  57. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -3
  58. package/src/plugins/builtin/portfolio-list/summary/index.tsx +10 -1
  59. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
  60. package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
  61. package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
  62. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  63. package/src/plugins/builtin/thirteenf/data.ts +5 -1
  64. package/src/plugins/builtin/thirteenf/headless.ts +3 -1
  65. package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
  66. package/src/portfolio/cost-fx.ts +84 -0
  67. package/src/portfolio/mutations.ts +16 -2
  68. package/src/portfolio/position-metrics.ts +45 -4
  69. package/src/portfolio/position-quantity.ts +43 -0
  70. package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
  71. package/src/sync/core-contributors.ts +1 -0
  72. package/src/team/collections.ts +2 -0
  73. package/src/tickers/metadata.ts +10 -0
  74. package/src/types/ticker.ts +9 -0
  75. package/src/utils/currency-units.ts +9 -0
@@ -39,6 +39,13 @@ export interface SetManualPortfolioPositionInput {
39
39
  * read does not unsize it.
40
40
  */
41
41
  multiplier?: number;
42
+ /** The purchase date, YYYY-MM-DD. Left out, a single manual lot keeps the one it had. */
43
+ dateAcquired?: string;
44
+ /**
45
+ * The rate the cost was bought at: one unit of `currency` in `costFx.currency`.
46
+ * Null clears it; left out, a single manual lot in the same currency keeps the one it had.
47
+ */
48
+ costFx?: { rate: number; currency: string } | null;
42
49
  }
43
50
 
44
51
  /** What a position's size counts, as its field and messages name it. */
@@ -406,8 +413,14 @@ export function setManualPortfolioPosition(
406
413
  const replacedPositionCount = replacedPositions.length;
407
414
  // Editing a single snapshot (for example after a split) does not change its
408
415
  // acquisition date. A consolidated replacement of several lots has no one date.
409
- const dateAcquired = replacedPositions.length === 1 && replacedPositions[0]?.broker === "manual"
410
- ? replacedPositions[0].dateAcquired : undefined;
416
+ const single = replacedPositions.length === 1 && replacedPositions[0]?.broker === "manual" ? replacedPositions[0] : null;
417
+ const dateAcquired = input.dateAcquired ?? single?.dateAcquired;
418
+ // The rate the cost was bought at goes with its cost: a lot kept in its currency keeps it unless it is replaced or cleared.
419
+ const keptCostFx = single && single.currency?.trim().toUpperCase() === nextCurrency
420
+ && typeof single.costFxRate === "number" && single.costFxCurrency
421
+ ? { rate: single.costFxRate, currency: single.costFxCurrency } : null;
422
+ const costFx = input.costFx === undefined ? keptCostFx : input.costFx;
423
+ if (costFx && !(Number.isFinite(costFx.rate) && costFx.rate > 0)) throw new Error("The cost FX rate must be a positive number.");
411
424
  // A contract's size does not change with the count held; one the lots agree on carries over.
412
425
  const keptMultipliers = new Set(replacedPositions.map((position) => position.multiplier));
413
426
  const keptMultiplier = keptMultipliers.size === 1 ? [...keptMultipliers][0] : undefined;
@@ -422,6 +435,7 @@ export function setManualPortfolioPosition(
422
435
  currency: nextCurrency,
423
436
  broker: "manual",
424
437
  ...(dateAcquired ? { dateAcquired } : {}),
438
+ ...(costFx ? { costFxRate: costFx.rate, costFxCurrency: costFx.currency.trim().toUpperCase() } : {}),
425
439
  ...(multiplier !== undefined ? { multiplier } : {}),
426
440
  });
427
441
 
@@ -3,7 +3,7 @@ import type { Quote } from "../types/financials";
3
3
  import type { PriceBasis } from "../types/instrument";
4
4
  import { resolvePriceBasis } from "../market-data/market/price-basis";
5
5
  import { getActiveQuoteDisplay, type ActiveQuoteDisplay } from "../market-data/market/status";
6
- import { resolveCurrencyUnit } from "../utils/currency-units";
6
+ import { heldCurrencyUnit, resolveCurrencyUnit } from "../utils/currency-units";
7
7
  import { isFuturesTicker } from "./futures-positions";
8
8
 
9
9
  export interface PortfolioPositionMetrics {
@@ -103,6 +103,42 @@ function resolvePositionCostMultiplier(position: TickerRecord["metadata"]["posit
103
103
  return withoutMultiplierError < withMultiplierError ? 1 : priceMultiplier;
104
104
  }
105
105
 
106
+ /**
107
+ * The rate a lot's cost converts at when valued in `valuationCurrency`: the
108
+ * one it was bought at (`costFxRate`), when that rate converts into this very
109
+ * currency and the lot is held in another. Null otherwise, and the cost
110
+ * converts at today's rate as its value does.
111
+ */
112
+ export function lotCostFxRate(
113
+ position: Pick<TickerPosition, "costFxRate" | "costFxCurrency">,
114
+ positionCurrency: string,
115
+ valuationCurrency: string,
116
+ ): number | null {
117
+ const rate = position.costFxRate;
118
+ const into = position.costFxCurrency?.trim();
119
+ if (typeof rate !== "number" || !Number.isFinite(rate) || rate <= 0 || !into) return null;
120
+ return into === valuationCurrency.trim() && !sameCurrency(positionCurrency, into) ? rate : null;
121
+ }
122
+
123
+ /** Two currency codes in one currency, its minor units included: GBp is GBP, USX is USD. */
124
+ export function sameCurrency(left: string, right: string): boolean {
125
+ return heldCurrencyUnit(left).currency === heldCurrencyUnit(right).currency;
126
+ }
127
+
128
+ /**
129
+ * Whether a lot's cost, valued in `valuationCurrency`, converts at today's
130
+ * rate: it is held in another currency and no rate from when it was bought
131
+ * applies, so its P&L leaves out the currency's move since.
132
+ */
133
+ export function lotCostAtTodaysRate(
134
+ position: Pick<TickerPosition, "costFxRate" | "costFxCurrency">,
135
+ positionCurrency: string,
136
+ valuationCurrency: string,
137
+ ): boolean {
138
+ return !!positionCurrency.trim() && !sameCurrency(positionCurrency, valuationCurrency)
139
+ && lotCostFxRate(position, positionCurrency, valuationCurrency) === null;
140
+ }
141
+
106
142
  export function getPortfolioPositionMetrics(
107
143
  ticker: TickerRecord,
108
144
  activeTab: string | undefined,
@@ -128,6 +164,11 @@ export function getPortfolioPositionMetrics(
128
164
  if (!currency) return Number.NaN;
129
165
  return valuation ? valuation.convert(value, currency) : currencies.size > 1 ? Number.NaN : value;
130
166
  };
167
+ // A cost converts at the rate the lot was bought at, when it has one into the valuation's currency.
168
+ const convertCost = (value: number, position: typeof positions[number]) => {
169
+ const bought = valuation ? lotCostFxRate(position, currencyFor(position), valuation.currency) : null;
170
+ return bought !== null ? value * bought : convert(value, position);
171
+ };
131
172
  const metrics: PortfolioPositionMetrics = {
132
173
  positionCurrency, positionCount: positions.length, hasShorts: false,
133
174
  totalShares: 0, totalCost: 0, hasCostBasis: positions.length > 0, priceBasis, signedCost: 0, totalCostUnits: 0,
@@ -169,8 +210,8 @@ export function getPortfolioPositionMetrics(
169
210
  metrics.unknownContractSize ||= unknownSize && !Number.isFinite(position.marketValue);
170
211
  if (Number.isFinite(contractMultiplier)) metrics.multiplierHint = Math.max(metrics.multiplierHint, contractMultiplier);
171
212
  metrics.totalShares += magnitude * direction;
172
- metrics.totalCost += convert(cost, position);
173
- metrics.signedCost += direction * convert(cost, position);
213
+ metrics.totalCost += convertCost(cost, position);
214
+ metrics.signedCost += direction * convertCost(cost, position);
174
215
  metrics.totalCostUnits += magnitude * costMultiplier;
175
216
  metrics.totalPriceUnits += magnitude * quoteMultiplier * direction;
176
217
  metrics.grossPriceUnits += magnitude * quoteMultiplier;
@@ -191,7 +232,7 @@ export function getPortfolioPositionMetrics(
191
232
  if (pnl == null || !Number.isFinite(pnl)) metrics.hasBrokerPnl = false;
192
233
  else metrics.brokerPnl += convert(pnl, position);
193
234
  metrics.pnlLots.push({
194
- signedCost: direction * convert(cost, position),
235
+ signedCost: direction * convertCost(cost, position),
195
236
  priceUnits: magnitude * quoteMultiplier * direction,
196
237
  direction,
197
238
  brokerMarketValue: marketValue != null && Number.isFinite(marketValue) ? convert(marketValue, position) : null,
@@ -0,0 +1,43 @@
1
+ import { resolveAssetDisplayKind } from "../market-data/market/format";
2
+ import type { TickerRecord } from "../types/ticker";
3
+ import { ambiguousNumberMessage, thousandsDotReadings } from "../utils/decimal-comma";
4
+ import { isKnownNonUsListing } from "../utils/sec";
5
+ import { isFuturesTicker } from "./futures-positions";
6
+ import { positionQuantityLabel, type PositionQuantityLabel } from "./mutations";
7
+
8
+ /** Fund units are issued in fractions wherever the fund is listed. */
9
+ const FRACTIONAL_FUND_TYPES = new Set(["FUND", "MUTUALFUND"]);
10
+
11
+ /**
12
+ * Whether a listing is held in whole units: a future's or an option's
13
+ * contracts, and shares listed outside the US, which trade in whole lots
14
+ * (THYAO.IS, SAP:XETRA). US shares, which brokers split into fractions,
15
+ * mutual funds, coins, currencies and bonds may be held in fractions.
16
+ */
17
+ function tradesInWholeUnits(ticker: TickerRecord): boolean {
18
+ const kind = resolveAssetDisplayKind({ assetCategory: ticker.metadata.assetCategory });
19
+ if (isFuturesTicker(ticker) || kind === "contract") return true;
20
+ const category = ticker.metadata.assetCategory?.trim().toUpperCase().replace(/[\s_-]/g, "") ?? "";
21
+ const shares = kind === "equity" ? !FRACTIONAL_FUND_TYPES.has(category) : kind === "other" && !category;
22
+ return shares && isKnownNonUsListing(ticker);
23
+ }
24
+
25
+ /**
26
+ * Why a typed size cannot be stored as read: a dot and three digits (`1.250`)
27
+ * on a listing held in whole units, where it is far likelier the thousands
28
+ * grouping many locales write (1,250 shares) than a quarter of a share, so the
29
+ * command asks which, as it does for `1,250`. Null for any other size, and
30
+ * for a listing that may be held in fractions, where `1.250` is 1.25.
31
+ */
32
+ export function wholeUnitQuantityProblem(
33
+ typed: string,
34
+ ticker: TickerRecord,
35
+ options: { label?: PositionQuantityLabel; listing?: string } = {},
36
+ ): string | null {
37
+ const reading = thousandsDotReadings(typed);
38
+ if (!reading || !tradesInWholeUnits(ticker)) return null;
39
+ const label = options.label ?? positionQuantityLabel(ticker);
40
+ const listing = options.listing ?? ticker.metadata.ticker;
41
+ const because = `${listing} trades in whole ${label === "Contracts" ? "contracts" : "shares"}`;
42
+ return `${label} ${ambiguousNumberMessage(typed, reading, { because })}.`;
43
+ }
@@ -53,9 +53,34 @@ export function resetFredSeriesPersistence(): void {
53
53
  cache.reset();
54
54
  }
55
55
 
56
- /** The series' page on the FRED site, where its notes and release schedule live. */
56
+ /** Who publishes a series Gloom Cloud serves in FRED's shape that FRED does not carry, and its page. */
57
+ interface SeriesNotOnFred {
58
+ /** Who compiles the figures (TurkStat). */
59
+ publisher: string;
60
+ /** Whose publication they are read from (the CBRT). */
61
+ via: string;
62
+ url: string;
63
+ }
64
+
65
+ const TURKISH_CPI: SeriesNotOnFred = {
66
+ publisher: "TurkStat",
67
+ via: "the CBRT",
68
+ url: "https://www.tcmb.gov.tr/wps/wcm/connect/EN/TCMB+EN/Main+Menu/Statistics/Inflation+Data/Consumer+Prices",
69
+ };
70
+
71
+ const NOT_ON_FRED: Readonly<Record<string, SeriesNotOnFred>> = {
72
+ TURCPIYOY: TURKISH_CPI,
73
+ TURCPIMOM: TURKISH_CPI,
74
+ };
75
+
76
+ /** The publisher of a served series FRED does not carry; null for FRED's own. */
77
+ export function seriesNotOnFred(seriesId: string): SeriesNotOnFred | null {
78
+ return NOT_ON_FRED[seriesId.trim().toUpperCase()] ?? null;
79
+ }
80
+
81
+ /** The series' page on the FRED site, where its notes and release schedule live, or its publisher's page when FRED does not carry it. */
57
82
  export function fredSeriesUrl(seriesId: string): string {
58
- return `https://fred.stlouisfed.org/series/${seriesId}`;
83
+ return seriesNotOnFred(seriesId)?.url ?? `https://fred.stlouisfed.org/series/${seriesId}`;
59
84
  }
60
85
 
61
86
  function cacheKey(request: FredSeriesRequest): string {
@@ -264,6 +264,7 @@ function sanitizePosition(position: TickerPosition): TickerPosition {
264
264
  avgCost: position.avgCost,
265
265
  currency: position.currency,
266
266
  dateAcquired: position.dateAcquired,
267
+ ...(position.costFxRate != null && position.costFxCurrency ? { costFxRate: position.costFxRate, costFxCurrency: position.costFxCurrency } : {}),
267
268
  broker: position.broker,
268
269
  side: position.side,
269
270
  marketValue: position.marketValue,
@@ -174,6 +174,8 @@ export function applyMembership(
174
174
  currency: position?.currency ?? currency,
175
175
  broker: "manual",
176
176
  ...(!flipped && position?.dateAcquired ? { dateAcquired: position.dateAcquired } : {}),
177
+ // So does the rate its cost was bought at, with its cost.
178
+ ...(!flipped && position?.costFxRate != null && position.costFxCurrency ? { costFxRate: position.costFxRate, costFxCurrency: position.costFxCurrency } : {}),
177
179
  // A future's contract size stays with the holding when the team changes its count.
178
180
  ...(position?.multiplier ? { multiplier: position.multiplier } : {}),
179
181
  });
@@ -9,6 +9,15 @@ const DEFAULT_TICKER_METADATA: Omit<TickerMetadata, "ticker" | "exchange" | "cur
9
9
  tags: [],
10
10
  };
11
11
 
12
+ /** A lot's cost FX rate, kept only whole: a positive rate and the currency it converts into. */
13
+ function hydrateCostFx(raw: Record<string, unknown>): Pick<TickerPosition, "costFxRate" | "costFxCurrency"> {
14
+ const rate = raw.costFxRate ?? raw.cost_fx_rate;
15
+ const currency = raw.costFxCurrency ?? raw.cost_fx_currency;
16
+ return typeof rate === "number" && Number.isFinite(rate) && rate > 0 && typeof currency === "string" && currency.trim()
17
+ ? { costFxRate: rate, costFxCurrency: currency.trim() }
18
+ : {};
19
+ }
20
+
12
21
  function hydratePosition(raw: Record<string, unknown>): TickerPosition {
13
22
  const avgCost = Object.hasOwn(raw, "avgCost") ? raw.avgCost : raw.avg_cost;
14
23
  return {
@@ -18,6 +27,7 @@ function hydratePosition(raw: Record<string, unknown>): TickerPosition {
18
27
  priceBasis: raw.priceBasis === "per-unit" || raw.priceBasis === "percent-of-par" ? raw.priceBasis : undefined,
19
28
  currency: (raw.currency as string) ?? undefined,
20
29
  dateAcquired: (raw.dateAcquired ?? raw.date_acquired) as string | undefined,
30
+ ...hydrateCostFx(raw),
21
31
  broker: (raw.broker as string) ?? "manual",
22
32
  side: (raw.side as "long" | "short") ?? undefined,
23
33
  marketValue: (raw.marketValue ?? raw.market_value) as number | undefined,
@@ -8,6 +8,15 @@ export interface TickerPosition {
8
8
  priceBasis?: PriceBasis;
9
9
  currency?: string;
10
10
  dateAcquired?: string;
11
+ /**
12
+ * What one unit of the lot's `currency` cost in `costFxCurrency` when it was
13
+ * bought: entered with the position, or the FX close of its purchase date.
14
+ * Valued in that currency, the lot's cost converts at this rate rather than
15
+ * today's, so its P&L carries the currency's move since.
16
+ */
17
+ costFxRate?: number;
18
+ /** The currency `costFxRate` converts the cost into: the portfolio's when it was entered. */
19
+ costFxCurrency?: string;
11
20
  broker: string; // "manual" | future broker plugin IDs
12
21
  side?: "long" | "short";
13
22
  marketValue?: number;
@@ -64,6 +64,15 @@ export interface CurrencyUnitInfo {
64
64
  divisor: number;
65
65
  }
66
66
 
67
+ /**
68
+ * The currency money held in a code is counted in, and how many of the code
69
+ * make one of it: resolveCurrencyUnit's, and US cents as a hundredth of a
70
+ * dollar, which a lot's cost in USX is.
71
+ */
72
+ export function heldCurrencyUnit(currency?: string | null): CurrencyUnitInfo {
73
+ return currency?.trim().toUpperCase() === US_CENTS ? { currency: "USD", divisor: 100 } : resolveCurrencyUnit(currency);
74
+ }
75
+
67
76
  interface ExchangeSubUnitOptions {
68
77
  allowDefaultCurrency?: boolean;
69
78
  allowMissingCurrency?: boolean;