gloomberb 0.18.1 → 0.18.2
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/commodity-balances.ts +6 -0
- package/src/api-client/company-kpis.ts +9 -0
- package/src/api-client/crypto-markets.ts +9 -0
- package/src/api-client/data.ts +6 -5
- package/src/api-client/equity-screener.ts +2 -0
- package/src/api-client/types.ts +5 -0
- package/src/app/pane-runtime/plugin-bindings.ts +3 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
- package/src/app/runtime/plugin-bindings.ts +6 -2
- package/src/brokers/logos.ts +66 -0
- package/src/brokers/profile-form.ts +3 -0
- package/src/brokers/remote-broker-adapter.ts +13 -3
- package/src/capabilities/index.ts +1 -1
- package/src/capabilities/service-capabilities.ts +7 -1
- package/src/cli/base-converter.ts +60 -18
- package/src/cli/commands/market.ts +281 -154
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +100 -50
- package/src/cli/currency-hints.ts +27 -0
- package/src/cli/fx-pair.ts +41 -4
- package/src/cli/glossary.ts +57 -4
- package/src/cli/helpers.ts +36 -3
- package/src/cli/index.ts +4 -1
- package/src/cli/known-gaps.ts +37 -1
- package/src/cli/listing-arg.ts +37 -2
- package/src/cli/pane-functions/catalog.ts +3 -1
- package/src/cli/pane-functions/discovery.ts +15 -1
- package/src/cli/pane-functions/dom.ts +9 -4
- package/src/cli/pane-functions/freshness.ts +4 -4
- package/src/cli/pane-functions/function-help.ts +27 -3
- package/src/cli/pane-functions/headless.ts +6 -5
- package/src/cli/pane-functions/index.ts +3 -2
- package/src/cli/pane-functions/lock-note.ts +7 -4
- package/src/cli/pane-functions/resolver.ts +2 -1
- package/src/cli/quote-answer.ts +179 -0
- package/src/cli/series-search.ts +2 -1
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +11 -2
- package/src/components/chart/composite/format.ts +13 -1
- package/src/components/chart/composite/level-labels.ts +30 -0
- package/src/components/chart/composite/levels.ts +17 -6
- package/src/components/chart/composite/panel-surface.tsx +58 -8
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/index.ts +2 -0
- package/src/components/layout/pane/content.tsx +16 -14
- package/src/components/layout/pane/footer/index.tsx +22 -3
- package/src/components/layout/pane/footer/model.ts +99 -4
- package/src/components/layout/pane/header-tabs.tsx +15 -1
- package/src/components/onboarding/onboarding-frame.tsx +9 -11
- package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
- package/src/components/onboarding/wizard-model.ts +10 -3
- package/src/components/ui/button.tsx +11 -4
- package/src/components/ui/choice-dialog.tsx +8 -5
- package/src/components/ui/display.tsx +11 -6
- package/src/components/ui/fields.tsx +100 -20
- package/src/components/ui/icon.tsx +6 -1
- package/src/components/ui/list-row-lines.tsx +87 -0
- package/src/components/ui/list-view.tsx +14 -0
- package/src/components/ui/logo-mark.tsx +64 -0
- package/src/components/ui/number-step.ts +54 -0
- package/src/components/ui/select-button.tsx +5 -4
- package/src/components/ui/select-field.tsx +3 -3
- package/src/components/ui/status.tsx +17 -3
- package/src/components/ui/tabs.tsx +9 -1
- package/src/components/ui/toggle.tsx +17 -3
- package/src/core/state/app/instrument.ts +8 -3
- package/src/data/config/store/normalize.ts +4 -1
- package/src/data/plugin-cache.ts +32 -6
- package/src/docs/snapshot.json +5 -5
- package/src/i18n/es.ts +9 -3
- package/src/i18n/ja.ts +9 -3
- package/src/i18n/ko.ts +9 -3
- package/src/i18n/zh-cn.ts +10 -4
- package/src/i18n/zh-tw.ts +10 -4
- package/src/market-data/commodity-names.ts +80 -0
- package/src/market-data/coordinator/auxiliary.ts +3 -1
- package/src/market-data/coordinator/chart.ts +3 -1
- package/src/market-data/futures-board.ts +38 -1
- package/src/market-data/futures-contract.ts +91 -0
- package/src/market-data/fx-closes.ts +78 -1
- package/src/market-data/market/change-basis.ts +219 -0
- package/src/market-data/market/format.ts +28 -12
- package/src/market-data/market/freshness.ts +38 -0
- package/src/market-data/market/status.ts +55 -9
- package/src/market-data/market/year-to-date.ts +46 -0
- package/src/market-data/quotes/contributions.ts +12 -0
- package/src/market-data/quotes/delisting.ts +34 -0
- package/src/market-data/quotes/live-valuation.ts +23 -0
- package/src/market-data/quotes/resolution.ts +8 -2
- package/src/market-data/tax-equivalent.ts +10 -2
- package/src/notifications/app-notifier.ts +2 -2
- package/src/notifications/notification-log.ts +8 -6
- package/src/plugins/builtin/alerts/cli.ts +26 -18
- package/src/plugins/builtin/alerts/headless.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +13 -4
- package/src/plugins/builtin/attention/trending.tsx +8 -6
- package/src/plugins/builtin/backtest/client.ts +5 -2
- package/src/plugins/builtin/backtest/window.ts +16 -3
- package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
- package/src/plugins/builtin/broker-manager/index.tsx +20 -9
- package/src/plugins/builtin/broker-manager/model.ts +5 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
- package/src/plugins/builtin/broker-manager/table.ts +5 -1
- package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
- package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
- package/src/plugins/builtin/broker-trading/command.ts +103 -0
- package/src/plugins/builtin/broker-trading/controller.ts +688 -0
- package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
- package/src/plugins/builtin/broker-trading/headless.ts +76 -0
- package/src/plugins/builtin/broker-trading/index.tsx +77 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
- package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
- package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
- package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
- package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
- package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
- package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
- package/src/plugins/builtin/buildout/format.ts +1 -0
- package/src/plugins/builtin/buildout/model/index.ts +16 -7
- package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
- package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
- package/src/plugins/builtin/chart-composer/presets.ts +2 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
- package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
- package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
- package/src/plugins/builtin/commodity-balances/client.ts +6 -4
- package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
- package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
- package/src/plugins/builtin/commodity-balances/model.ts +22 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
- package/src/plugins/builtin/company-kpis/client.ts +3 -2
- package/src/plugins/builtin/company-kpis/headless.ts +5 -7
- package/src/plugins/builtin/company-kpis/model.ts +85 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
- package/src/plugins/builtin/composite-plugins.ts +3 -2
- package/src/plugins/builtin/cot/headless.ts +1 -1
- package/src/plugins/builtin/cot/index.tsx +1 -1
- package/src/plugins/builtin/cot/model.ts +35 -12
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
- package/src/plugins/builtin/crypto-board/client.ts +33 -2
- package/src/plugins/builtin/crypto-board/headless.ts +5 -2
- package/src/plugins/builtin/crypto-board/model.ts +28 -8
- package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
- package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
- package/src/plugins/builtin/econ/calendar-model.ts +35 -0
- package/src/plugins/builtin/econ/index.tsx +31 -4
- package/src/plugins/builtin/econ/print-watch.ts +279 -0
- package/src/plugins/builtin/equity-screener/headless.ts +20 -5
- package/src/plugins/builtin/equity-screener/model.ts +40 -2
- package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
- package/src/plugins/builtin/exposure/client.ts +4 -1
- package/src/plugins/builtin/exposure/editor.tsx +3 -3
- package/src/plugins/builtin/exposure/evidence.ts +4 -1
- package/src/plugins/builtin/exposure/holdings.ts +3 -1
- package/src/plugins/builtin/exposure/pane.tsx +37 -16
- package/src/plugins/builtin/filing-events/index.tsx +9 -3
- package/src/plugins/builtin/futures/table.tsx +6 -3
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/insider/base-values.ts +72 -0
- package/src/plugins/builtin/insider/headless.ts +29 -4
- package/src/plugins/builtin/jobs/headless.ts +2 -2
- package/src/plugins/builtin/jobs/lock-note.ts +14 -3
- package/src/plugins/builtin/market-halts/model.ts +31 -6
- package/src/plugins/builtin/market-halts/pane.tsx +3 -0
- package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
- package/src/plugins/builtin/members/pane.tsx +8 -3
- package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/options/footer.ts +25 -20
- package/src/plugins/builtin/options/live-quotes.ts +33 -18
- package/src/plugins/builtin/options/view.tsx +77 -22
- package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
- package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
- package/src/plugins/builtin/primary-dealers/client.ts +137 -62
- package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
- package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
- package/src/plugins/builtin/primary-dealers/model.ts +179 -129
- package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
- package/src/plugins/builtin/research/analyst-model.ts +5 -1
- package/src/plugins/builtin/risk-factors/index.tsx +12 -3
- package/src/plugins/builtin/scanner/feed.ts +6 -1
- package/src/plugins/builtin/scanner/headless.ts +1 -1
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
- package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
- package/src/plugins/builtin/shared/free-routes.ts +24 -0
- package/src/plugins/builtin/shared/headless-options.ts +11 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
- package/src/plugins/builtin/shared/research-trading.tsx +62 -0
- package/src/plugins/builtin/social-mentions/headless.ts +1 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -2
- package/src/plugins/builtin/themes/model.ts +43 -10
- package/src/plugins/builtin/themes/pane.tsx +8 -6
- package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
- package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
- package/src/plugins/builtin/time-sales/pane.tsx +2 -2
- package/src/plugins/builtin/trace-bonds/client.ts +14 -3
- package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/compat.ts +3 -0
- package/src/plugins/external-runtime.ts +5 -0
- package/src/plugins/loader.ts +6 -0
- package/src/plugins/plugin-icon.ts +61 -0
- package/src/plugins/registry/host-actions.ts +2 -1
- package/src/plugins/registry/index.ts +9 -1
- package/src/plugins/runtime/context.tsx +5 -0
- package/src/plugins/runtime/index.tsx +3 -0
- package/src/portfolio/column-values.ts +13 -4
- package/src/portfolio/cost-fx.ts +5 -28
- package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
- package/src/public/broker.ts +8 -0
- package/src/public/market-data.ts +5 -0
- package/src/renderers/dom/desktop/control-styles.ts +7 -1
- package/src/renderers/dom/desktop/controls.tsx +39 -7
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/list-view.tsx +51 -19
- package/src/renderers/dom/desktop/menu.tsx +10 -1
- package/src/renderers/dom/desktop/select-field.tsx +8 -6
- package/src/renderers/dom/host/style.ts +6 -1
- package/src/renderers/dom/host/tabs.tsx +6 -0
- package/src/renderers/dom/styles.css +12 -5
- package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
- package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
- package/src/renderers/electrobun/view/external-plugins.ts +1 -0
- package/src/series/validate.ts +2 -2
- package/src/sources/gloomberb-cloud/index.ts +50 -17
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
- package/src/sources/history-result.ts +3 -0
- package/src/sources/provider-errors.ts +3 -1
- package/src/sources/provider-router/history.ts +49 -6
- package/src/sources/provider-router/index.ts +2 -1
- package/src/state/app/activity.ts +13 -0
- package/src/sync/controller.ts +4 -0
- package/src/sync/core-contributors.ts +62 -11
- package/src/sync/local-only.ts +121 -0
- package/src/sync/react.ts +6 -0
- package/src/sync/types.ts +3 -0
- package/src/tickers/listing-fate.ts +27 -8
- package/src/tickers/search/index.ts +24 -3
- package/src/time-series/price-comparison.ts +35 -0
- package/src/time-series/resolve.ts +52 -11
- package/src/time-series/spec.ts +1 -1
- package/src/time-series/types.ts +5 -0
- package/src/types/broker.ts +56 -1
- package/src/types/config.ts +6 -0
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +23 -0
- package/src/types/headless.ts +12 -2
- package/src/types/instrument.ts +4 -0
- package/src/types/plugin.ts +24 -6
- package/src/types/price-history.ts +6 -1
- package/src/types/trading.ts +67 -0
- package/src/ui/host.tsx +4 -0
- package/src/utils/base-currency.ts +108 -0
- package/src/utils/currency-names.ts +128 -0
- package/src/utils/currency-pair.ts +2 -1
- package/src/utils/currency-units.ts +32 -1
- package/src/utils/dom-screenshot.ts +22 -9
- package/src/utils/exchange-notation.generated.ts +360 -0
- package/src/utils/exchanges.ts +15 -322
- package/src/utils/format.ts +31 -1
- package/src/utils/instrument-identity.ts +38 -1
- package/src/utils/options.ts +46 -1
- package/src/utils/price-history.ts +4 -0
- package/src/utils/reported-money.ts +8 -4
|
@@ -2,6 +2,9 @@ import { apiClient } from "../../api-client";
|
|
|
2
2
|
import type { NewsArticle } from "../../news/types";
|
|
3
3
|
import { mapCloudNewsArticle } from "../../sources/gloomberb-cloud/news";
|
|
4
4
|
import { cliStyles } from "../../utils/cli-output";
|
|
5
|
+
import { logFailure } from "../../utils/log-failure";
|
|
6
|
+
import { commodityNamedIn, commoditySearchName, isCommodityName } from "../../market-data/commodity-names";
|
|
7
|
+
import type { FuturesContract } from "../../market-data/futures-board";
|
|
5
8
|
|
|
6
9
|
/** What the service accepts of a search: two characters to two hundred, with a letter or a number in them. */
|
|
7
10
|
const MIN_TEXT_LENGTH = 2;
|
|
@@ -60,8 +63,8 @@ export function newsSearchTextProblem(text: string): string | null {
|
|
|
60
63
|
|
|
61
64
|
/**
|
|
62
65
|
* The word a symbol lookup found no stories for, as text to search stories for.
|
|
63
|
-
* Null under three characters, where the
|
|
64
|
-
*
|
|
66
|
+
* Null under three characters, where the word is mostly a piece of other
|
|
67
|
+
* names (ZN in Telomir-Zn) and the lookup's own answer is the better one.
|
|
65
68
|
*/
|
|
66
69
|
export function newsFallbackText(symbol: string): string | null {
|
|
67
70
|
const text = symbol.trim().replace(/^\$/, "");
|
|
@@ -69,15 +72,39 @@ export function newsFallbackText(symbol: string): string | null {
|
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
/**
|
|
72
|
-
* The stories matching the text,
|
|
73
|
-
* first
|
|
74
|
-
*
|
|
75
|
+
* The stories matching the text, in the service's order: stories with every
|
|
76
|
+
* word in the headline first, then the ones with some only in the summary,
|
|
77
|
+
* newest first within each. The service holds stories back from an account
|
|
78
|
+
* without real-time access, as every other news request.
|
|
75
79
|
*/
|
|
76
80
|
export async function searchNewsStories(text: string, limit: number): Promise<NewsArticle[]> {
|
|
77
81
|
const response = await apiClient.searchCloudNews(text, limit);
|
|
78
|
-
return response.items
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
return response.items.map((item) => mapCloudNewsArticle(item));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The futures news route a search offers when no story matches it: `gloomberb news KC=F` for "arabica". */
|
|
86
|
+
export interface CommodityNewsRoute {
|
|
87
|
+
contract: FuturesContract;
|
|
88
|
+
command: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The future of the commodity a search names, offered when no story matched:
|
|
93
|
+
* `news KC=F` searches the stories of coffee, so it is offered only when
|
|
94
|
+
* those are not what just found nothing and they have a story to show.
|
|
95
|
+
*/
|
|
96
|
+
export async function commodityNewsRoute(text: string): Promise<CommodityNewsRoute | null> {
|
|
97
|
+
const contract = commodityNamedIn(text);
|
|
98
|
+
const name = contract ? commoditySearchName(contract) : null;
|
|
99
|
+
if (!contract || !name || isCommodityName(text, name)) return null;
|
|
100
|
+
const stories = await searchNewsStories(name, 1).catch(logFailure("cli.news-search", "Search a commodity's stories failed", []));
|
|
101
|
+
return stories.length > 0 ? { contract, command: `gloomberb news ${contract.symbol}` } : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** What a search that matched no story says: never a near miss, and the commodity's futures news where it has some. */
|
|
105
|
+
export function noStoriesMessage(text: string, route: CommodityNewsRoute | null): string {
|
|
106
|
+
const none = `No stories match "${text}".`;
|
|
107
|
+
return route ? `${none} For the ${route.contract.name} future, try: ${route.command}` : none;
|
|
81
108
|
}
|
|
82
109
|
|
|
83
110
|
/** The line above the stories: "Stories matching "european gas"", with why a word typed as a symbol is searched. */
|
|
@@ -5,6 +5,7 @@ import { saveConfig } from "../../data/config/store";
|
|
|
5
5
|
import { builtinPluginGroupMembers } from "../../plugins/ownership";
|
|
6
6
|
import type { AppConfig, TelemetryConfig } from "../../types/config";
|
|
7
7
|
import { canonicalTimeZone } from "../../utils/utc-time";
|
|
8
|
+
import { readBaseCurrency } from "../../utils/base-currency";
|
|
8
9
|
import { NIGHTLY_CHANNEL_SUMMARY, parseUpdateChannel, withUpdateChannel } from "../../updater/channel";
|
|
9
10
|
import type { CliCommandDef } from "../../types/plugin";
|
|
10
11
|
import { resolveCliDataDir, withCliServices, withConfigData } from "../context";
|
|
@@ -65,6 +66,32 @@ function parseTimezoneSetting(value: string): string | null {
|
|
|
65
66
|
);
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The code `config set baseCurrency` stores (`cad` is CAD); fails on a name
|
|
71
|
+
* or sign, naming the code it stands for, and on anything that is no code.
|
|
72
|
+
*/
|
|
73
|
+
function parseBaseCurrencySetting(value: string): string {
|
|
74
|
+
const reading = readBaseCurrency(value);
|
|
75
|
+
if ("code" in reading) return reading.code;
|
|
76
|
+
const [first] = reading.suggestions;
|
|
77
|
+
return fail(reading.error, first
|
|
78
|
+
? `baseCurrency takes an ISO 4217 code: gloomberb config set baseCurrency ${first}`
|
|
79
|
+
: "baseCurrency takes an ISO 4217 code such as USD, EUR or CAD.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A saved base currency that is no code (an older version stored names), with what to set instead; null for a code. */
|
|
83
|
+
function baseCurrencyProblem(value: unknown): string | null {
|
|
84
|
+
const reading = readBaseCurrency(typeof value === "string" ? value : "");
|
|
85
|
+
if ("code" in reading) return null;
|
|
86
|
+
const [first] = reading.suggestions;
|
|
87
|
+
return `${reading.error} Set one: gloomberb config set baseCurrency ${first ?? "<code>"}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function describeBaseCurrency(value: unknown): string {
|
|
91
|
+
const problem = baseCurrencyProblem(value);
|
|
92
|
+
return problem ? `${String(value)} ${cliStyles.warning(`(${problem})`)}` : String(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
68
95
|
function describeTimezone(value: unknown): string {
|
|
69
96
|
return typeof value === "string" && value ? value : "not set, times print in UTC alone";
|
|
70
97
|
}
|
|
@@ -189,6 +216,9 @@ export function createSystemCliCommands(): CliCommandDef[] {
|
|
|
189
216
|
checks.push({ check: "database", status: "ok", detail: join(services.dataDir, ".gloomberb-cache.db") });
|
|
190
217
|
checks.push({ check: "plugins", status: "ok", detail: String(services.services.pluginRegistry.allPlugins.size) });
|
|
191
218
|
checks.push({ check: "capabilities", status: "ok", detail: String(services.services.pluginRegistry.capabilities.manifests().length) });
|
|
219
|
+
// Only a saved value no conversion can price is worth a line.
|
|
220
|
+
const baseCurrencyIssue = baseCurrencyProblem(services.config.baseCurrency);
|
|
221
|
+
if (baseCurrencyIssue) checks.push({ check: "baseCurrency", status: "warn", detail: baseCurrencyIssue });
|
|
192
222
|
const theme = services.config.theme;
|
|
193
223
|
checks.push({
|
|
194
224
|
check: "theme",
|
|
@@ -311,6 +341,7 @@ export function createSystemCliCommands(): CliCommandDef[] {
|
|
|
311
341
|
...(key === KEYBINDINGS_CONFIG_KEY ? { format: summarizeKeybindings } : {}),
|
|
312
342
|
...(key === "theme" ? { format: (value: unknown) => describeThemeId(String(value)) } : {}),
|
|
313
343
|
...(key === TIMEZONE_CONFIG_KEY ? { format: describeTimezone } : {}),
|
|
344
|
+
...(key === "baseCurrency" ? { format: describeBaseCurrency } : {}),
|
|
314
345
|
})),
|
|
315
346
|
});
|
|
316
347
|
return;
|
|
@@ -329,7 +360,8 @@ export function createSystemCliCommands(): CliCommandDef[] {
|
|
|
329
360
|
}
|
|
330
361
|
ctx.printResult({ data: { key, value: safeConfig[key] } }, {
|
|
331
362
|
text: (data) => key === KEYBINDINGS_CONFIG_KEY ? renderKeybindings(data.value)
|
|
332
|
-
: key === TIMEZONE_CONFIG_KEY ? describeTimezone(data.value)
|
|
363
|
+
: key === TIMEZONE_CONFIG_KEY ? describeTimezone(data.value)
|
|
364
|
+
: key === "baseCurrency" ? describeBaseCurrency(data.value) : describeConfigValue(data.value),
|
|
333
365
|
});
|
|
334
366
|
return;
|
|
335
367
|
}
|
|
@@ -367,6 +399,8 @@ export function createSystemCliCommands(): CliCommandDef[] {
|
|
|
367
399
|
// A display name may come unquoted: config set theme White Phosphor.
|
|
368
400
|
const parsedValue = key === "theme"
|
|
369
401
|
? requireThemeId(args.slice(2).join(" "))
|
|
402
|
+
: key === "baseCurrency"
|
|
403
|
+
? parseBaseCurrencySetting(args.slice(2).join(" "))
|
|
370
404
|
: key === TIMEZONE_CONFIG_KEY
|
|
371
405
|
? parseTimezoneSetting(value)
|
|
372
406
|
: key === UPDATE_CHANNEL_CONFIG_KEY
|
|
@@ -8,11 +8,12 @@ import { describeFundamentalMarketCap, marketCapShareBasisNote, selectMarketCapi
|
|
|
8
8
|
import {
|
|
9
9
|
formatCompact,
|
|
10
10
|
formatCurrency,
|
|
11
|
+
formatCurrencyCode,
|
|
11
12
|
formatNumber,
|
|
12
13
|
formatPercent,
|
|
13
14
|
formatPercentRaw,
|
|
14
15
|
} from "../../utils/format";
|
|
15
|
-
import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity,
|
|
16
|
+
import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity, quoteFormatOptions, withCurrencyMinorDigits, type MarketFormatOptions } from "../../market-data/market/format";
|
|
16
17
|
import {
|
|
17
18
|
cliStyles,
|
|
18
19
|
cliTerminalWidth,
|
|
@@ -28,14 +29,16 @@ import {
|
|
|
28
29
|
getExtendedSessionDisplay,
|
|
29
30
|
getQuoteSessionFields,
|
|
30
31
|
getRegularSessionDisplay,
|
|
32
|
+
quoteBookSessionLabel,
|
|
31
33
|
quoteHeadlineTime,
|
|
32
34
|
quoteMarketStateLabel,
|
|
33
35
|
} from "../../market-data/market/status";
|
|
34
36
|
import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
|
|
35
|
-
import {
|
|
37
|
+
import { changeBasisJson, quoteChangeBasis, quotePreviousCloseDate } from "../../market-data/market/change-basis";
|
|
38
|
+
import { futuresContractLines, futuresSettleText, isFuturesInstrument, loadFuturesContractTerms, minorUnitPricePer } from "../../market-data/futures-contract";
|
|
39
|
+
import { currencyCodeLabel } from "../../utils/currency-units";
|
|
36
40
|
import type { AppConfig } from "../../types/config";
|
|
37
41
|
import type { DeclaredDividendAction, FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
|
|
38
|
-
import { quoteTradingDay } from "../../market-data/quotes/day-range";
|
|
39
42
|
import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
|
|
40
43
|
import { computeTickerPriceReturns } from "../../market-data/ticker-price-returns";
|
|
41
44
|
import { instrumentTypeLabel, resolveTickerInstrumentKind } from "../../tickers/instrument-kind";
|
|
@@ -46,7 +49,8 @@ import type { TickerRecord } from "../../types/ticker";
|
|
|
46
49
|
import type { CliCommandContext } from "../../types/plugin";
|
|
47
50
|
import { getPortfolioPositionMetrics, getPortfolioQuoteDisplay, lotCostFxRate, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../portfolio/position-metrics";
|
|
48
51
|
import { isFuturesTicker } from "../../portfolio/futures-positions";
|
|
49
|
-
import { createBaseConverter } from "../base-converter";
|
|
52
|
+
import { createBaseConverter, type BaseConversion } from "../base-converter";
|
|
53
|
+
import { formatFxRate } from "../fx-pair";
|
|
50
54
|
import { initMarketData, withMarketData } from "../context";
|
|
51
55
|
import { fail } from "../errors";
|
|
52
56
|
import type { MarketContext } from "../types";
|
|
@@ -58,6 +62,7 @@ import {
|
|
|
58
62
|
formatSignedCurrency,
|
|
59
63
|
formatTimestamp,
|
|
60
64
|
formatWatchlistNames,
|
|
65
|
+
quoteChangeText,
|
|
61
66
|
} from "../helpers";
|
|
62
67
|
import { NotesFiles } from "../../plugins/builtin/notes/files";
|
|
63
68
|
import { isKnownNonUsListing, secFilingItemCodes } from "../../utils/sec";
|
|
@@ -106,10 +111,6 @@ async function loadRecentSecFilings(
|
|
|
106
111
|
// Prose such as a company description stays readable on wide terminals.
|
|
107
112
|
const MAX_PROSE_WIDTH = 100;
|
|
108
113
|
const METADATA_SEPARATOR = " · ";
|
|
109
|
-
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
|
|
110
|
-
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
111
|
-
/** The states the header reads as CLOSED: the venue is between sessions, so nobody is quoting. */
|
|
112
|
-
const CLOSED_MARKET_STATES = new Set<string>(["CLOSED", "PREPRE", "POSTPOST"]);
|
|
113
114
|
|
|
114
115
|
interface TickerCommandDependencies {
|
|
115
116
|
/** `--exchange`: the listing, for a symbol that trades in several places. */
|
|
@@ -149,25 +150,22 @@ function appendMetricSection(lines: string[], title: string, metrics: Array<[str
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
153
|
+
* Out of the regular session the bid and ask say whose book they are: the pre-market or after-hours book beside
|
|
154
|
+
* Last and the extended print, or a closed market's last quote with its day (ITA's $202.00 against $208.74
|
|
155
|
+
* around a $206.65 close is no open book).
|
|
154
156
|
*/
|
|
155
157
|
function bidAskText(quote: Quote, options: MarketFormatOptions): string {
|
|
156
158
|
const text = formatBidAsk(quote.bid, quote.ask, quote.bidSize, quote.askSize, quote.currency, options);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const date = day ? new Date(`${day}T00:00:00Z`) : null;
|
|
160
|
-
const label = date && Number.isFinite(date.getTime())
|
|
161
|
-
? `, ${WEEKDAYS[date.getUTCDay()]} ${date.getUTCDate()} ${MONTHS[date.getUTCMonth()]}` : "";
|
|
162
|
-
return `${text} ${cliStyles.muted(`(last quote${label})`)}`;
|
|
159
|
+
const label = text === "—" ? null : quoteBookSessionLabel(quote);
|
|
160
|
+
return label ? `${text} ${cliStyles.muted(`(${label})`)}` : text;
|
|
163
161
|
}
|
|
164
162
|
|
|
165
163
|
function wrapProse(text: string): string {
|
|
166
164
|
return wrapText(text, Math.min(cliTerminalWidth() ?? MAX_PROSE_WIDTH, MAX_PROSE_WIDTH)).join("\n");
|
|
167
165
|
}
|
|
168
166
|
|
|
169
|
-
function buildStatementMetrics(statement: FinancialStatement, currency?: string): Array<[string, string]> {
|
|
170
|
-
const money = (value: number | undefined, perShare = false) => formatReportedMoney(value, currency, perShare);
|
|
167
|
+
function buildStatementMetrics(statement: FinancialStatement, currency?: string, codes = false): Array<[string, string]> {
|
|
168
|
+
const money = (value: number | undefined, perShare = false) => formatReportedMoney(value, currency, perShare, codes);
|
|
171
169
|
return [
|
|
172
170
|
["Revenue", money(statement.totalRevenue)],
|
|
173
171
|
["Gross Profit", money(statement.grossProfit)],
|
|
@@ -303,8 +301,11 @@ function shouldFetchSecFilings(
|
|
|
303
301
|
}
|
|
304
302
|
|
|
305
303
|
async function appendTickerPositions(lines: string[], tickerFile: TickerRecord | null, quote: TickerFinancials["quote"],
|
|
306
|
-
config: AppConfig, toBase:
|
|
304
|
+
config: AppConfig, toBase: ReportBaseConverter): Promise<void> {
|
|
307
305
|
const quoteCurrency = quote?.currency?.trim() || tickerFile?.metadata.currency?.trim() || "";
|
|
306
|
+
const codes = writesCurrencyCodes(config);
|
|
307
|
+
const money = codes ? formatCurrencyCode : formatCurrency;
|
|
308
|
+
const converted = new Set<string>();
|
|
308
309
|
if (tickerFile && tickerFile.metadata.positions.length > 0) {
|
|
309
310
|
lines.push("");
|
|
310
311
|
lines.push(renderSection("Positions"));
|
|
@@ -319,6 +320,8 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
|
|
|
319
320
|
const currentPrice = getPortfolioQuoteDisplay(metrics, quote)?.price ?? null;
|
|
320
321
|
const currentPriceBase = currentPrice != null && quoteCurrency ? await toBase(currentPrice, quoteCurrency) : null;
|
|
321
322
|
const positionRate = positionCurrency ? await toBase(1, positionCurrency) : Number.NaN;
|
|
323
|
+
if (positionCurrency) converted.add(positionCurrency.toUpperCase());
|
|
324
|
+
if (quoteCurrency && currentPrice != null) converted.add(quoteCurrency.toUpperCase());
|
|
322
325
|
const baseMetrics = getPortfolioPositionMetrics({ ...tickerFile, metadata: { ...tickerFile.metadata, positions: [position] } }, undefined, quoteCurrency,
|
|
323
326
|
{ currency: config.baseCurrency, convert: value => value * positionRate }, quote);
|
|
324
327
|
// A cost bought at an entered rate into the base currency converts at it, as its P&L does; any other at today's.
|
|
@@ -334,26 +337,51 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
|
|
|
334
337
|
const stats: CliStatEntry[] = [
|
|
335
338
|
[
|
|
336
339
|
"Position",
|
|
337
|
-
`${formatMarketQuantity(metrics.totalShares, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency })} ${metrics.priceBasis === "percent-of-par" ? "@" : `${tickerFile.metadata.assetCategory === "BOND" ? "units" : contracts ? "contracts" : "shares"} @`} ${positionCurrency ? formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis }) : "—"}`,
|
|
340
|
+
`${formatMarketQuantity(metrics.totalShares, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency })} ${metrics.priceBasis === "percent-of-par" ? "@" : `${tickerFile.metadata.assetCategory === "BOND" ? "units" : contracts ? "contracts" : "shares"} @`} ${positionCurrency ? formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, currencyCode: codes }) : "—"}`,
|
|
338
341
|
],
|
|
339
|
-
["Cost Basis",
|
|
340
|
-
["Market Value",
|
|
342
|
+
["Cost Basis", money(costBasisBase, config.baseCurrency)],
|
|
343
|
+
["Market Value", money(marketValueBase, config.baseCurrency)],
|
|
341
344
|
[
|
|
342
345
|
selectedPnl.basis === "broker-snapshot" ? "Broker P&L" : "P&L",
|
|
343
|
-
pnl === null ? "—" : colorBySign(formatSignedCurrency(pnl, config.baseCurrency), pnl),
|
|
346
|
+
pnl === null ? "—" : colorBySign(formatSignedCurrency(pnl, config.baseCurrency, codes), pnl),
|
|
344
347
|
],
|
|
345
348
|
];
|
|
346
349
|
if (position.markPrice != null) {
|
|
347
|
-
stats.push(["Broker Mark", positionCurrency ? formatMarketPriceWithCurrency(position.markPrice, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis }) : "—"]);
|
|
350
|
+
stats.push(["Broker Mark", positionCurrency ? formatMarketPriceWithCurrency(position.markPrice, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, currencyCode: codes }) : "—"]);
|
|
348
351
|
}
|
|
349
352
|
lines.push(renderStats(stats));
|
|
350
353
|
if (index < positions.length - 1) {
|
|
351
354
|
lines.push("");
|
|
352
355
|
}
|
|
353
356
|
}
|
|
357
|
+
for (const note of conversionNotes(toBase, converted)) lines.push(cliStyles.muted(note));
|
|
354
358
|
}
|
|
355
359
|
}
|
|
356
360
|
|
|
361
|
+
/** The ticker report's converter: what `createBaseConverter` makes, whose rates a report states; a plain function in tests. */
|
|
362
|
+
type ReportBaseConverter = ((value: number, fromCurrency: string) => Promise<number>) & { conversions?(): BaseConversion[] };
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* A base currency other than USD reads every amount with its code (259.36
|
|
366
|
+
* USD, 878.15B CAD): $ alone is also the Canadian, Australian and other
|
|
367
|
+
* dollars. A USD base keeps the signs it always had.
|
|
368
|
+
*/
|
|
369
|
+
export function writesCurrencyCodes(config: Pick<AppConfig, "baseCurrency"> | undefined): boolean {
|
|
370
|
+
const base = config?.baseCurrency?.trim().toUpperCase();
|
|
371
|
+
return !!base && base !== "USD";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* "CAD at USD/CAD 1.40623, as of 2026-10-21 04:46 UTC.": the rate and its
|
|
376
|
+
* date for each currency in `currencies` the report converted into the base
|
|
377
|
+
* currency, so a converted figure is never without what it was converted at.
|
|
378
|
+
*/
|
|
379
|
+
function conversionNotes(toBase: ReportBaseConverter, currencies: ReadonlySet<string>): string[] {
|
|
380
|
+
return (toBase.conversions?.() ?? [])
|
|
381
|
+
.filter((conversion) => currencies.has(conversion.currency) && conversion.currency !== conversion.baseCurrency)
|
|
382
|
+
.map(({ currency, baseCurrency, rate, asOf }) => `${baseCurrency} figures at ${currency}/${baseCurrency} ${formatFxRate(rate)}${asOf != null ? `, as of ${formatTimestamp(asOf)}` : ""}.`);
|
|
383
|
+
}
|
|
384
|
+
|
|
357
385
|
/** A source's enterprise value is in the units of the capitalization it reports beside it,
|
|
358
386
|
* which without a declared unit is the listing's quote currency (as the overview labels it). */
|
|
359
387
|
function enterpriseValueCurrency(
|
|
@@ -378,9 +406,9 @@ interface FundamentalsMetric {
|
|
|
378
406
|
|
|
379
407
|
type MetricFigure = Omit<FundamentalsMetric, "label">;
|
|
380
408
|
|
|
381
|
-
/** An amount in the currency the source reported it in; `ccy?` when it named none, as the text says. */
|
|
382
|
-
function reportedMoney(value: number | undefined, currency: string | undefined, perShare = false): MetricFigure {
|
|
383
|
-
return { text: formatReportedMoney(value, currency, perShare), value, unit: currency?.trim() || "ccy?" };
|
|
409
|
+
/** An amount in the currency the source reported it in; `ccy?` when it named none, as the text says. `codes`: per share too (6.54 USD). */
|
|
410
|
+
function reportedMoney(value: number | undefined, currency: string | undefined, perShare = false, codes = false): MetricFigure {
|
|
411
|
+
return { text: formatReportedMoney(value, currency, perShare, codes), value, unit: currency?.trim() || "ccy?" };
|
|
384
412
|
}
|
|
385
413
|
|
|
386
414
|
/** The EV line: labelled "(provider)" when it is more than 5% from market cap + debt - cash, "unavailable" when withdrawn. */
|
|
@@ -570,6 +598,7 @@ function fundamentalsMetrics(
|
|
|
570
598
|
enterpriseValue = reportedMoney(reportedEnterpriseValue(fundamentals), enterpriseValueCurrency(quote, fundamentals)),
|
|
571
599
|
distributions: DividendDistributions | null = null,
|
|
572
600
|
bond: FundBondFacts | null = null,
|
|
601
|
+
codes = false,
|
|
573
602
|
): FundamentalsMetric[] {
|
|
574
603
|
const signed = (value: number) => colorBySign(formatPercent(value), value);
|
|
575
604
|
const receipt = fundamentals?.sharesOutstanding != null && sharesOutstandingInReceipts(quote, fundamentals, profile?.description);
|
|
@@ -581,7 +610,7 @@ function fundamentalsMetrics(
|
|
|
581
610
|
{ label: "Forward P/E", ...multipleFigure(priceEarningsOnEarnings(fundamentals?.forwardPE, fundamentals?.forwardEps)) },
|
|
582
611
|
{ label: "PEG", ...multipleFigure(fundamentals?.pegRatio) },
|
|
583
612
|
// The flows and EPS are one trailing-twelve-month block, the same twelve months for each line.
|
|
584
|
-
{ label: "EPS (TTM)", ...reportedMoney(fundamentals?.eps, fundamentals?.epsCurrency?.trim() || reportingCurrency, true) },
|
|
613
|
+
{ label: "EPS (TTM)", ...reportedMoney(fundamentals?.eps, fundamentals?.epsCurrency?.trim() || reportingCurrency, true, codes) },
|
|
585
614
|
{
|
|
586
615
|
label: dividendYieldLabel(fundamentals?.dividendYieldBasis, bond),
|
|
587
616
|
...forwardYieldFigure(fundamentals, distributions),
|
|
@@ -684,6 +713,8 @@ type FundamentalsReportData = TickerFinancials & {
|
|
|
684
713
|
bank?: BankFiguresPayload | null;
|
|
685
714
|
/** In place of `bank`, for a bank whose figures could not be read. */
|
|
686
715
|
bankNote?: string | null;
|
|
716
|
+
/** A base currency other than USD: every amount reads with its code (`writesCurrencyCodes`). */
|
|
717
|
+
currencyCodes?: boolean;
|
|
687
718
|
};
|
|
688
719
|
|
|
689
720
|
/** "Latest quarter 2026-06-30, 10-Q filed 2026-08-06." and the newer quarter's results release, for the lines under a report's figures. */
|
|
@@ -723,7 +754,10 @@ function valuationPriceMetrics(financials: FundamentalsReportData): Fundamentals
|
|
|
723
754
|
const quote = financials.quote;
|
|
724
755
|
const session = getRegularSessionDisplay(quote);
|
|
725
756
|
if (!quote || !session) return [];
|
|
726
|
-
const options = withCurrencyMinorDigits(
|
|
757
|
+
const options = withCurrencyMinorDigits({
|
|
758
|
+
...quoteFormatOptions(quote, undefined, financials.quoteMetadata?.instrumentType),
|
|
759
|
+
...financials.currencyCodes ? { currencyCode: true } : {},
|
|
760
|
+
}, quote.currency);
|
|
727
761
|
const price = (value: number) => formatMarketPriceWithCurrency(value, quote.currency, options);
|
|
728
762
|
const extended = getExtendedSessionDisplay(quote);
|
|
729
763
|
const extendedLabel = extended ? EXTENDED_SESSION_LABELS[extended.session] : "";
|
|
@@ -732,7 +766,8 @@ function valuationPriceMetrics(financials: FundamentalsReportData): Fundamentals
|
|
|
732
766
|
session.changePercent != null && Number.isFinite(session.changePercent)
|
|
733
767
|
? {
|
|
734
768
|
label: "Change",
|
|
735
|
-
|
|
769
|
+
// With what the move is measured from, as `ticker` and `quote` say it: "vs Fri 16 Oct close".
|
|
770
|
+
text: quoteChangeText(quote, session, options),
|
|
736
771
|
value: session.changePercent,
|
|
737
772
|
unit: "%",
|
|
738
773
|
}
|
|
@@ -767,7 +802,7 @@ function fundamentalsReportMetrics(snapshot: FundamentalsReportData, view: "fund
|
|
|
767
802
|
? { text: `${formatCompact(capitalization.value)} ${capitalization.currency}`, value: capitalization.value, unit: capitalization.currency }
|
|
768
803
|
: { text: "—" };
|
|
769
804
|
const metrics = fundamentalsMetrics(quote, fundamentals, financials.profile, marketCap, computeTickerPriceReturns(financials), fundamentalsCurrency(financials),
|
|
770
|
-
undefined, snapshot.distributions ?? null, snapshot.bondFund ?? null);
|
|
805
|
+
undefined, snapshot.distributions ?? null, snapshot.bondFund ?? null, snapshot.currencyCodes === true);
|
|
771
806
|
if (view === "fundamentals") {
|
|
772
807
|
return [...metrics, ...floatAndShortInterestMetrics(quote, fundamentals, financials.profile)]
|
|
773
808
|
.filter((metric) => metric.text !== "—");
|
|
@@ -927,7 +962,7 @@ export async function buildTickerReport({
|
|
|
927
962
|
tickerFile: TickerRecord | null;
|
|
928
963
|
financials: TickerFinancials;
|
|
929
964
|
config: AppConfig;
|
|
930
|
-
toBase:
|
|
965
|
+
toBase: ReportBaseConverter;
|
|
931
966
|
notes?: string;
|
|
932
967
|
/** Why there is no quote, when a source said; "Quote unavailable." otherwise. */
|
|
933
968
|
quoteNote?: string;
|
|
@@ -944,8 +979,10 @@ export async function buildTickerReport({
|
|
|
944
979
|
const profile = financials.profile;
|
|
945
980
|
const name = quote?.name || tickerFile?.metadata.name || symbol;
|
|
946
981
|
const baseQuoteOptions = quoteFormatOptions(quote, tickerFile?.metadata.assetCategory, financials.quoteMetadata?.instrumentType);
|
|
982
|
+
const baseCurrency = config.baseCurrency.trim().toUpperCase();
|
|
983
|
+
const codes = writesCurrencyCodes(config);
|
|
947
984
|
// Pad money prices to the currency's minor unit so a range reads £35.10 - £35.485, never past it (JPY has none).
|
|
948
|
-
const quoteOptions = withCurrencyMinorDigits(baseQuoteOptions, quote?.currency);
|
|
985
|
+
const quoteOptions = withCurrencyMinorDigits(codes ? { ...baseQuoteOptions, currencyCode: true } : baseQuoteOptions, quote?.currency);
|
|
949
986
|
const lines: string[] = [];
|
|
950
987
|
|
|
951
988
|
lines.push(`${cliStyles.accent(quote?.symbol ?? symbol)} ${cliStyles.bold(name)}`);
|
|
@@ -956,9 +993,10 @@ export async function buildTickerReport({
|
|
|
956
993
|
const indexLevel = isIndexLevel(quote?.instrumentType?.trim() || financials.quoteMetadata?.instrumentType?.trim() || tickerFile?.metadata.assetCategory);
|
|
957
994
|
const summaryParts = [
|
|
958
995
|
listingVenueLabel(listingExchange, quote, financials, tickerFile) || undefined,
|
|
959
|
-
indexLevel ? INDEX_POINTS_LABEL : quoteCurrency ? `Currency ${quoteCurrency}` : undefined,
|
|
996
|
+
indexLevel ? INDEX_POINTS_LABEL : quoteCurrency ? `Currency ${currencyCodeLabel(quoteCurrency)}` : undefined,
|
|
960
997
|
quoteMarketStateLabel(quote) ?? undefined,
|
|
961
|
-
|
|
998
|
+
// A listing that no longer trades has no delay to speak of: its quote is its last session's close.
|
|
999
|
+
quote?.dataSource && !quote.delisting ? `Source ${quote.dataSource.toUpperCase()}` : undefined,
|
|
962
1000
|
].filter((part): part is string => !!part);
|
|
963
1001
|
if (summaryParts.length > 0) {
|
|
964
1002
|
lines.push(cliStyles.muted(summaryParts.join(METADATA_SEPARATOR)));
|
|
@@ -994,21 +1032,25 @@ export async function buildTickerReport({
|
|
|
994
1032
|
// Market cap and the multiples are measured at the regular session price the Quote section heads with.
|
|
995
1033
|
const priced = atRegularSessionPrice(financials);
|
|
996
1034
|
const capitalization = selectMarketCapitalization(priced.quote, priced.fundamentals);
|
|
997
|
-
|
|
1035
|
+
// Market cap and EV read in the currency they are reported in, as the revenue and income beside them do,
|
|
1036
|
+
// with the base currency's figure after them when it differs: 627.2B USD (878.15B CAD).
|
|
1037
|
+
const convertedCurrencies = new Set<string>();
|
|
1038
|
+
const inBase = async (value: number, currency: string): Promise<string> => {
|
|
1039
|
+
if (currency.trim().toUpperCase() === baseCurrency) return "";
|
|
1040
|
+
const converted = await toBase(value, currency);
|
|
1041
|
+
if (!Number.isFinite(converted)) return "";
|
|
1042
|
+
convertedCurrencies.add(currency.trim().toUpperCase());
|
|
1043
|
+
return ` (${formatCompact(converted)} ${baseCurrency})`;
|
|
1044
|
+
};
|
|
998
1045
|
const marketCapText = capitalization
|
|
999
|
-
?
|
|
1000
|
-
? `${formatCompact(convertedMarketCap)} ${config.baseCurrency}`
|
|
1001
|
-
: `${formatCompact(capitalization.value)} ${capitalization.currency}`
|
|
1046
|
+
? `${formatCompact(capitalization.value)} ${capitalization.currency}${await inBase(capitalization.value, capitalization.currency)}`
|
|
1002
1047
|
: "—";
|
|
1003
1048
|
// Shown in the same currency as the market cap, so the two can be compared.
|
|
1004
1049
|
const enterpriseValue = reportedEnterpriseValue(priced.fundamentals);
|
|
1005
1050
|
const evCurrency = enterpriseValueCurrency(quote, fundamentals);
|
|
1006
1051
|
// A minor unit such as GBp stays as reported: the converter would read it as the major currency.
|
|
1007
|
-
const
|
|
1008
|
-
? await
|
|
1009
|
-
const enterpriseValueText = Number.isFinite(convertedEnterpriseValue)
|
|
1010
|
-
? `${formatCompact(convertedEnterpriseValue)} ${config.baseCurrency}`
|
|
1011
|
-
: formatReportedMoney(enterpriseValue, evCurrency);
|
|
1052
|
+
const enterpriseValueText = `${formatReportedMoney(enterpriseValue, evCurrency)}${enterpriseValue != null && evCurrency && /^[A-Z]{3}$/.test(evCurrency)
|
|
1053
|
+
? await inBase(enterpriseValue, evCurrency) : ""}`;
|
|
1012
1054
|
|
|
1013
1055
|
if (quote) {
|
|
1014
1056
|
// As the Overview reads it: the regular session, then the extended-hours print against its close.
|
|
@@ -1023,13 +1065,16 @@ export async function buildTickerReport({
|
|
|
1023
1065
|
const volume = reportedQuoteVolume(quote);
|
|
1024
1066
|
// A futures price is its latest trade; the exchange settlement is its own row, dated by its session.
|
|
1025
1067
|
appendMetricSection(lines, "Quote", [
|
|
1026
|
-
|
|
1068
|
+
// A price in cents says what it is per: 314.95¢/lb.
|
|
1069
|
+
["Last", colorBySign(`${formatMarketPriceWithCurrency(session.price, quote.currency, quoteOptions)}${minorUnitPricePer(futuresContract, quote.currency)}`, session.change)],
|
|
1027
1070
|
...futuresContract
|
|
1028
1071
|
? [["Settle", futuresSettleText(futuresContract, quote)] as [string, string]]
|
|
1029
1072
|
: [],
|
|
1030
|
-
|
|
1073
|
+
// The move says what it is measured from: "vs Fri 16 Oct close", a future's "vs Fri 16 Oct settle".
|
|
1074
|
+
["Change", quoteChangeText(quote, session, quoteOptions, futuresContract)],
|
|
1031
1075
|
["Open", quote.open != null ? formatMarketPriceWithCurrency(quote.open, quote.currency, quoteOptions) : "—"],
|
|
1032
|
-
|
|
1076
|
+
// A listing that no longer trades: its last session's range, never today's.
|
|
1077
|
+
[quote.delisting ? "Last Session" : "Day Range", quote.low != null || quote.high != null
|
|
1033
1078
|
? formatPriceRange(quote.low, quote.high, quote.currency, quoteOptions)
|
|
1034
1079
|
: "—"],
|
|
1035
1080
|
["52W Range", quote.low52w != null || quote.high52w != null
|
|
@@ -1050,10 +1095,11 @@ export async function buildTickerReport({
|
|
|
1050
1095
|
}
|
|
1051
1096
|
|
|
1052
1097
|
const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials),
|
|
1053
|
-
{ text: enterpriseValueText }, distributions, bondFund);
|
|
1098
|
+
{ text: enterpriseValueText }, distributions, bondFund, codes);
|
|
1054
1099
|
const beforeFundamentals = lines.length;
|
|
1055
1100
|
appendMetricSection(lines, "Fundamentals", metricLines(metrics));
|
|
1056
1101
|
for (const note of fundamentalsNotes(priced, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
|
|
1102
|
+
if (lines.length > beforeFundamentals) for (const note of conversionNotes(toBase, convertedCurrencies)) lines.push(cliStyles.muted(note));
|
|
1057
1103
|
// Where a fund's facts would go: under its fundamentals, or in their place.
|
|
1058
1104
|
const fund = fundFactsNote(quote?.symbol ?? symbol,
|
|
1059
1105
|
quote?.tradingExchangeName || listingExchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange, financials, tickerFile);
|
|
@@ -1071,13 +1117,13 @@ export async function buildTickerReport({
|
|
|
1071
1117
|
const latestAnnual = latestFinancialPeriod(financials.annualStatements, row => row.date);
|
|
1072
1118
|
if (latestAnnual) {
|
|
1073
1119
|
appendMetricSection(lines, `Latest Annual (${statementPeriodText(latestAnnual.date, statementFilings?.latestAnnual ?? null)})`,
|
|
1074
|
-
buildStatementMetrics(latestAnnual, statementCurrency(latestAnnual)));
|
|
1120
|
+
buildStatementMetrics(latestAnnual, statementCurrency(latestAnnual), codes));
|
|
1075
1121
|
}
|
|
1076
1122
|
|
|
1077
1123
|
const latestQuarter = latestFinancialPeriod(financials.quarterlyStatements, row => row.date);
|
|
1078
1124
|
if (latestQuarter) {
|
|
1079
1125
|
appendMetricSection(lines, `Latest Quarter (${statementPeriodText(latestQuarter.date, statementFilings?.latestQuarter ?? null)})`,
|
|
1080
|
-
buildStatementMetrics(latestQuarter, statementCurrency(latestQuarter)));
|
|
1126
|
+
buildStatementMetrics(latestQuarter, statementCurrency(latestQuarter), codes));
|
|
1081
1127
|
}
|
|
1082
1128
|
// The statements stop a quarter short of the results the company has released.
|
|
1083
1129
|
if (statementFilings?.resultsRelease && (latestAnnual || latestQuarter)) lines.push(cliStyles.muted(resultsReleaseText(statementFilings.resultsRelease)));
|
|
@@ -1161,6 +1207,10 @@ function buildTickerStructuredData({
|
|
|
1161
1207
|
name: quote.name,
|
|
1162
1208
|
// Last and Change as the text report reads them, then the extended print from that close.
|
|
1163
1209
|
...getQuoteSessionFields(quote),
|
|
1210
|
+
// What Change is measured from ("vs Fri 16 Oct close"), and the quote's previous close with its exchange-local date.
|
|
1211
|
+
changeBasis: changeBasisJson(quoteChangeBasis(quote, { terms: futuresContract })),
|
|
1212
|
+
previousClose: quote.previousClose ?? null,
|
|
1213
|
+
previousCloseDate: quotePreviousCloseDate(quote),
|
|
1164
1214
|
priceBasis: quote.priceBasis ?? null,
|
|
1165
1215
|
currency: quote.currency,
|
|
1166
1216
|
marketCap: priced.quote?.marketCap ?? null,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isVenueAbroad } from "../tickers/listing-fate";
|
|
2
|
+
import type { Quote } from "../types/financials";
|
|
3
|
+
import { isCurrencyCode } from "../utils/currency-pair";
|
|
4
|
+
import { canonicalExchange } from "../utils/exchanges";
|
|
5
|
+
import { UNLISTED_TYPES, type CliListing } from "./listing-arg";
|
|
6
|
+
|
|
7
|
+
// Quoted as dollars per unit (EUR/USD); every other currency as units per dollar (USD/MXN).
|
|
8
|
+
const DOLLARS_PER_UNIT = new Set(["EUR", "GBP", "AUD", "NZD"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* "MXN is also a currency: fx MXN, quote USD/MXN" for a bare currency code
|
|
12
|
+
* that read as a listing abroad (MXN is mixi Inc on Stuttgart's exchange): the
|
|
13
|
+
* quote is that company's, and a currency was likely what was typed. Null for
|
|
14
|
+
* a code that is no currency, for the dollar (a US fund trades as USD, and
|
|
15
|
+
* there is no USD/USD), for a listing that named or saved its venue, and for
|
|
16
|
+
* one in the US.
|
|
17
|
+
*/
|
|
18
|
+
export function currencyCodeNote(
|
|
19
|
+
listing: Pick<CliListing, "symbol" | "exchange" | "saved">,
|
|
20
|
+
quote: Pick<Quote, "listingExchangeName" | "exchangeName" | "instrumentType"> | null | undefined,
|
|
21
|
+
): string | null {
|
|
22
|
+
const code = listing.symbol;
|
|
23
|
+
if (!quote || listing.exchange || listing.saved || code === "USD" || !/^[A-Z]{3}$/.test(code) || !isCurrencyCode(code)) return null;
|
|
24
|
+
if (UNLISTED_TYPES.has(quote.instrumentType?.trim().toUpperCase() ?? "")) return null;
|
|
25
|
+
if (!isVenueAbroad(canonicalExchange(quote.listingExchangeName || quote.exchangeName))) return null;
|
|
26
|
+
return `${code} is also a currency: fx ${code}, quote ${DOLLARS_PER_UNIT.has(code) ? `${code}/USD` : `USD/${code}`}`;
|
|
27
|
+
}
|
package/src/cli/fx-pair.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { currencyMinorDigits } from "../market-data/market/format";
|
|
2
2
|
import { parseTypedNumber, readCurrencyAmount } from "../utils/decimal-comma";
|
|
3
|
+
import { currenciesNamed } from "../utils/currency-names";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* What `gloomberb fx` is asked for. A bare code is one unit of it in the base
|
|
@@ -12,6 +13,8 @@ export interface FxRequest {
|
|
|
12
13
|
currency: string;
|
|
13
14
|
/** The currency the price is in: the base currency for a bare code, the quote of a pair. */
|
|
14
15
|
baseCurrency: string;
|
|
16
|
+
/** Each currency typed as its name (`yen`) and the code it was read as, said beside the answer. */
|
|
17
|
+
readAs?: string[];
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
const CURRENCY_CODE = /^[A-Z]{3}$/;
|
|
@@ -30,19 +33,45 @@ function isKnownCurrency(code: string): boolean {
|
|
|
30
33
|
return knownCurrencies.has(code);
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
/**
|
|
37
|
+
* A currency typed as its name: the code a name that is one currency's reads
|
|
38
|
+
* as, or the sentence listing the currencies a name several share (`peso`),
|
|
39
|
+
* with `retry` the argument that asks for the first of them. Null for text
|
|
40
|
+
* that names none.
|
|
41
|
+
*/
|
|
42
|
+
function readCurrencyName(typed: string, retry: (code: string) => string): { code: string } | { error: string } | null {
|
|
43
|
+
const named = currenciesNamed(typed);
|
|
44
|
+
if (named.length === 0) return null;
|
|
45
|
+
if (named.length === 1) return { code: named[0]!.code };
|
|
46
|
+
const choices = named.map(({ code, qualifier }) => (qualifier ? `${code} (${qualifier})` : code)).join(", ");
|
|
47
|
+
return { error: `${typed} names several currencies: ${choices}. Write one as its code, such as fx ${retry(named[0]!.code)}.` };
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
/** The request, or the sentence saying what is wrong with the argument. */
|
|
34
51
|
export function parseFxRequest(raw: string, baseCurrency: string): FxRequest | { error: string } {
|
|
52
|
+
const typed = raw.trim().split("/").map((part) => part.trim());
|
|
35
53
|
const text = raw.trim().toUpperCase();
|
|
36
|
-
const parts =
|
|
54
|
+
const parts = typed.map((part) => part.toUpperCase());
|
|
37
55
|
if (parts.length > 2 || parts.some((part) => !part)) return { error: `"${raw}" is not a currency or a pair such as USD/NGN.` };
|
|
38
56
|
// EURUSD is the pair typed without its slash: say how to write it.
|
|
39
57
|
const joined = parts.length === 1 && /^[A-Z]{6}$/.test(text) && isKnownCurrency(text.slice(0, 3)) && isKnownCurrency(text.slice(3));
|
|
40
58
|
if (joined) return { error: `${text} is not a currency code; write the pair as ${text.slice(0, 3)}/${text.slice(3)}.` };
|
|
59
|
+
// A name (peso, yen, Swiss franc) is a currency's code. Three letters that are a code stay one: only a name that is no code reads as one.
|
|
60
|
+
const readAs: string[] = [];
|
|
61
|
+
for (const [index, part] of parts.entries()) {
|
|
62
|
+
if (isKnownCurrency(part)) continue;
|
|
63
|
+
const named = readCurrencyName(typed[index]!, (code) => parts.map((other, at) => at === index ? code : other).join("/"));
|
|
64
|
+
if (!named) continue;
|
|
65
|
+
if ("error" in named) return named;
|
|
66
|
+
readAs.push(`Read ${typed[index]} as ${named.code}.`);
|
|
67
|
+
parts[index] = named.code;
|
|
68
|
+
}
|
|
41
69
|
const bad = parts.filter((part) => !CURRENCY_CODE.test(part));
|
|
42
70
|
if (bad.length > 0) return { error: `${bad.join(" and ")} ${bad.length > 1 ? "are not currency codes" : "is not a currency code"}; use three letters such as NGN.` };
|
|
43
|
-
|
|
71
|
+
const request = parts.length === 2
|
|
44
72
|
? { currency: parts[0]!, baseCurrency: parts[1]! }
|
|
45
73
|
: { currency: parts[0]!, baseCurrency: baseCurrency.trim().toUpperCase() };
|
|
74
|
+
return readAs.length > 0 ? { ...request, readAs } : request;
|
|
46
75
|
}
|
|
47
76
|
|
|
48
77
|
/** What `gloomberb fx` is asked for, with the amount to convert when one comes first. */
|
|
@@ -55,7 +84,14 @@ export interface FxArgs {
|
|
|
55
84
|
}
|
|
56
85
|
|
|
57
86
|
/** Every form `fx` takes, for its usage line. */
|
|
58
|
-
export const FX_USAGE = "fx <currency> | fx <base>/<quote> | fx <amount> <from>/<to> | fx <amount> <from> <to>";
|
|
87
|
+
export const FX_USAGE = "fx <currency> | fx <base>/<quote> | fx <amount> <from>/<to> | fx <amount> <from> <to> [--date <day>]";
|
|
88
|
+
|
|
89
|
+
/** A currency's name of several words (`hong kong dollar`, `100 swiss franc`) as the one argument it is. */
|
|
90
|
+
function joinNameWords(args: readonly string[]): readonly string[] {
|
|
91
|
+
const amount = args.length > 2 && parseTypedNumber(args[0]).kind !== "invalid" ? 1 : 0;
|
|
92
|
+
const name = args.slice(amount).join(" ");
|
|
93
|
+
return args.length - amount > 1 && currenciesNamed(name).length > 0 ? [...args.slice(0, amount), name] : args;
|
|
94
|
+
}
|
|
59
95
|
|
|
60
96
|
/**
|
|
61
97
|
* `fx` arguments: a currency or a pair (`EUR`, `USD/NGN`), or an amount and
|
|
@@ -66,7 +102,8 @@ export const FX_USAGE = "fx <currency> | fx <base>/<quote> | fx <amount> <from>/
|
|
|
66
102
|
* refused naming both readings. An amount and one currency converts to USD
|
|
67
103
|
* (`toUsdByDefault`). The error sentence otherwise.
|
|
68
104
|
*/
|
|
69
|
-
export function parseFxArgs(
|
|
105
|
+
export function parseFxArgs(words: readonly string[], baseCurrency: string): FxArgs | { error: string } {
|
|
106
|
+
const args = joinNameWords(words);
|
|
70
107
|
if (args.length === 1) {
|
|
71
108
|
const request = parseFxRequest(args[0]!, baseCurrency);
|
|
72
109
|
if (!("error" in request)) return { request };
|