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
|
@@ -148,7 +148,8 @@ export async function applyListingArgument(
|
|
|
148
148
|
// A list entry with spaces is a company's name search finds, or the symbols it holds.
|
|
149
149
|
const typed = takesOneTicker ? [args.arg] : await readNameEntries(splitSymbolArgs([args.arg]), context.dataProvider);
|
|
150
150
|
if (typed.length === 0) return args;
|
|
151
|
-
|
|
151
|
+
// RY.TO is the listing its quote names, RY:TSX, as `quote` reads it: a function's data is read by venue.
|
|
152
|
+
let listings = await Promise.all(typed.map((raw) => resolveCliListing(raw, exchange, context, { ownExchangeWins: typed.length > 1, venueSuffix: true })));
|
|
152
153
|
// A separator stuck to the end of a pasted list (`UPST;`) is not part of a symbol; it is named.
|
|
153
154
|
const notes: string[] = takesOneTicker ? [] : stripStrayEdges(args.arg).notes;
|
|
154
155
|
const token = found.template?.shortcut?.prefix ?? args.target.toUpperCase();
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { QuoteBatchResult } from "../types/data-provider";
|
|
2
|
+
import type { Quote } from "../types/financials";
|
|
3
|
+
import { bareListingNote } from "../tickers/listing-fate";
|
|
4
|
+
import { notATickerSymbol } from "../sources/provider-errors";
|
|
5
|
+
import { looksLikeCurrencyPair } from "../utils/currency-pair";
|
|
6
|
+
import { canonicalExchange } from "../utils/exchanges";
|
|
7
|
+
import type { CliCommandDef } from "../types/plugin";
|
|
8
|
+
import { currencyCodeNote } from "./currency-hints";
|
|
9
|
+
import { describeSuggestion, suggestListing, suggestUsTicker, type ListingSuggestion } from "./listing-suggestion";
|
|
10
|
+
import { indexRootAlsoLine, indexRootFor, indexRootTryLine } from "./index-roots";
|
|
11
|
+
import { treasuryYieldLine, treasuryYieldTenor } from "./treasury-yields";
|
|
12
|
+
import { knownGapForTicker } from "./known-gaps";
|
|
13
|
+
import {
|
|
14
|
+
futuresTryLine,
|
|
15
|
+
isNoProviderError,
|
|
16
|
+
listingChoiceNotes,
|
|
17
|
+
listingReadNotes,
|
|
18
|
+
loadBareVenues,
|
|
19
|
+
notTradedMessage,
|
|
20
|
+
quoteBareListing,
|
|
21
|
+
requireCliListing,
|
|
22
|
+
tradingBoardNote,
|
|
23
|
+
type CliListing,
|
|
24
|
+
type ListingDataRequest,
|
|
25
|
+
type ListingVenue,
|
|
26
|
+
} from "./listing-arg";
|
|
27
|
+
import { withListingSuggestion } from "./not-a-ticker";
|
|
28
|
+
import type { MarketContext } from "./types";
|
|
29
|
+
|
|
30
|
+
/** The text of an error, or null for none. */
|
|
31
|
+
export function errorMessage(error: unknown): string | null {
|
|
32
|
+
if (error == null) return null;
|
|
33
|
+
return error instanceof Error ? error.message : String(error);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* "APPL is not a US ticker: it is Aptus Pharma Limited on BSE (INR). Did you
|
|
38
|
+
* mean AAPL (Apple Inc.)?", for a symbol read on a listing abroad that is one
|
|
39
|
+
* slip from a US ticker, worded as the not-a-ticker answers word it.
|
|
40
|
+
*/
|
|
41
|
+
export function notUsTickerSentence({ target, quote, listing, usSuggestion }: { target: { symbol: string }; quote: Quote | null; listing?: { name: string | null }; usSuggestion?: ListingSuggestion }): string | null {
|
|
42
|
+
if (!quote || !usSuggestion) return null;
|
|
43
|
+
const venue = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
|
|
44
|
+
const name = quote.name || listing?.name;
|
|
45
|
+
const what = `${name ? `${name} on ` : ""}${venue}${quote.currency ? ` (${quote.currency})` : ""}`;
|
|
46
|
+
return `${target.symbol} is not a US ticker: it is ${what}. Did you mean ${describeSuggestion(usSuggestion)}?`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface QuoteAnswer {
|
|
50
|
+
result: QuoteBatchResult;
|
|
51
|
+
/** The listing the row is: the one asked for, or the venue a bare symbol was quoted on. */
|
|
52
|
+
listing: CliListing;
|
|
53
|
+
/** Why there is no quote, when there is more to say than the source's own error. */
|
|
54
|
+
failure?: { message: string; details?: string; oneLine: string };
|
|
55
|
+
notes: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
|
|
60
|
+
* is not listed on says so. A bare symbol the data service read on its home
|
|
61
|
+
* listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
|
|
62
|
+
* quoted on its listings (`quoteBareListing`), else names them, or asks for an
|
|
63
|
+
* exchange when several companies list it (2222); a bare index root (VIX) also
|
|
64
|
+
* names its index (^VIX), and a futures board code a security keeps (GC) its
|
|
65
|
+
* future (GC=F). Venues are only looked up for a
|
|
66
|
+
* symbol with no quote, so a request that succeeds costs no search.
|
|
67
|
+
*/
|
|
68
|
+
export async function answerQuote(
|
|
69
|
+
result: QuoteBatchResult,
|
|
70
|
+
listing: CliListing,
|
|
71
|
+
market: MarketContext,
|
|
72
|
+
request: ListingDataRequest,
|
|
73
|
+
refresh: boolean,
|
|
74
|
+
venues: Promise<ListingVenue[]> | null,
|
|
75
|
+
): Promise<QuoteAnswer> {
|
|
76
|
+
const root = listing.exchange ? null : indexRootFor(listing.symbol);
|
|
77
|
+
// A bare root that quoted something else, such as MOVE (Corvex), points at the index too.
|
|
78
|
+
const also = (quote: Quote) => root && quote.instrumentType?.trim().toUpperCase() !== "INDEX"
|
|
79
|
+
? [indexRootAlsoLine(root, request.command)] : [];
|
|
80
|
+
if (result.quote) {
|
|
81
|
+
// "CAR = Avis Budget Group, NASDAQ (also XETRA, ASX)" names the venue too, so it replaces "SXR8 -> XETRA".
|
|
82
|
+
const choice = venues ? await listingChoiceNotes(listing, market, result.quote, venues) : listingReadNotes(listing);
|
|
83
|
+
const venue = choice.length > 0 ? null : bareListingNote(listing, result.quote);
|
|
84
|
+
// A row saved as 247540:KRX is quoted from KOSDAQ, the board it trades on.
|
|
85
|
+
const board = tradingBoardNote(listing, result.quote);
|
|
86
|
+
return { result, listing, notes: [...choice, venue ?? [], board ?? [], also(result.quote), currencyCodeNote(listing, result.quote) ?? []].flat() };
|
|
87
|
+
}
|
|
88
|
+
const notTraded = await notTradedMessage(listing, market);
|
|
89
|
+
if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
|
|
90
|
+
if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
|
|
91
|
+
const other = await quoteBareListing(listing, market, request, { refresh });
|
|
92
|
+
if (other?.kind === "quoted") {
|
|
93
|
+
return {
|
|
94
|
+
result: { target: other.listing.request, quote: other.quote },
|
|
95
|
+
listing: other.listing,
|
|
96
|
+
notes: [other.note, ...also(other.quote), currencyCodeNote(listing, other.quote) ?? []].flat(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Other listings already name a futures board code's future (GC=F); with none, it is named here.
|
|
100
|
+
// A Treasury yield's root (TNX) names its FRED series and the curve beside its index.
|
|
101
|
+
const yieldTenor = root ? treasuryYieldTenor(listing.symbol) : null;
|
|
102
|
+
const tryLines = [
|
|
103
|
+
yieldTenor ? treasuryYieldLine(yieldTenor) : root ? indexRootTryLine(root, request.command) : null,
|
|
104
|
+
other || listing.exchange ? null : futuresTryLine(listing.symbol, request),
|
|
105
|
+
// BDI asks which exchange (Black Diamond Group) and says nowhere that the Baltic Dry Index is not here.
|
|
106
|
+
listing.exchange ? null : knownGapForTicker(listing.symbol),
|
|
107
|
+
].filter((line): line is string => !!line);
|
|
108
|
+
if (!other && tryLines.length === 0) return { result: await withCurrencyPairHint(result, market), listing, notes: [] };
|
|
109
|
+
const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
|
|
110
|
+
const details = [other?.details, ...tryLines].filter(Boolean).join("\n");
|
|
111
|
+
const oneLine = [
|
|
112
|
+
message,
|
|
113
|
+
other ? `${other.heading}: ${other.listings.join(", ")}.` : "",
|
|
114
|
+
...tryLines,
|
|
115
|
+
].filter(Boolean).join(" ");
|
|
116
|
+
return { result, listing, failure: { message, details, oneLine }, notes: [] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A listing a command found a quote for, with what `quote` says beside it. */
|
|
120
|
+
interface QuotedListing {
|
|
121
|
+
/** The listing the quote is of: the one typed, or the venue a bare symbol was quoted on. */
|
|
122
|
+
listing: CliListing;
|
|
123
|
+
quote: Quote;
|
|
124
|
+
/** The venue a bare symbol was read on, the currency it may have meant, the US ticker it is one slip from. */
|
|
125
|
+
notes: string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The listing a command that keeps a symbol (`alerts add`) was given, read as
|
|
130
|
+
* `quote` reads it: the same resolver and the same quote request, so what is
|
|
131
|
+
* kept is the listing `quote` prints (USDMXN and USD/MXN are USDMXN=X, MXN is
|
|
132
|
+
* MXN:FWB2) and `quote`'s own notes come back with it. A symbol nothing
|
|
133
|
+
* quotes fails the command with why, and the nearest listing search finds when
|
|
134
|
+
* it is near ("Not a ticker: ZZZQQ. Did you mean ZZZQ?"), so nothing is kept
|
|
135
|
+
* for it.
|
|
136
|
+
*/
|
|
137
|
+
export async function requireQuotedListing(
|
|
138
|
+
raw: string,
|
|
139
|
+
exchange: string | null | undefined,
|
|
140
|
+
market: MarketContext,
|
|
141
|
+
ctx: Pick<Parameters<CliCommandDef["execute"]>[1], "fail">,
|
|
142
|
+
command: string,
|
|
143
|
+
): Promise<QuotedListing> {
|
|
144
|
+
const listing = await requireCliListing(raw, exchange, market, ctx);
|
|
145
|
+
const venues = loadBareVenues(listing, market);
|
|
146
|
+
const [result] = await market.dataProvider.getQuotesBatch([listing.request], { revalidate: true });
|
|
147
|
+
const answer = await answerQuote(result ?? { target: listing.request, quote: null }, listing, market, { command, noun: "quote" }, false, venues);
|
|
148
|
+
const quote = answer.result.quote;
|
|
149
|
+
if (!quote) {
|
|
150
|
+
if (answer.failure) ctx.fail(answer.failure.message, answer.failure.details);
|
|
151
|
+
const message = errorMessage(answer.result.error) ?? `No quote found for ${listing.key}.`;
|
|
152
|
+
// The listing search names, else the US ticker it is one slip from (AAPLL for AAPL), which search does not list for a typo.
|
|
153
|
+
const suggestion = notATickerSymbol(message) && !message.includes(" Did you mean ")
|
|
154
|
+
? await suggestListing(listing.symbol, market) ?? await suggestUsTicker(listing.symbol, market)
|
|
155
|
+
: null;
|
|
156
|
+
ctx.fail(suggestion ? withListingSuggestion(message, describeSuggestion(suggestion)) : message);
|
|
157
|
+
}
|
|
158
|
+
// A bare symbol read on a listing abroad that is one slip from a US ticker says so, as `quote` does.
|
|
159
|
+
const venue = answer.listing.saved ? null : bareListingNote(answer.listing, quote);
|
|
160
|
+
const usSuggestion = venue && answer.notes.includes(venue) ? await suggestUsTicker(answer.listing.symbol, market) : null;
|
|
161
|
+
const notUs = notUsTickerSentence({ target: answer.result.target, quote, listing: { name: quote.name ?? null }, usSuggestion: usSuggestion ?? undefined });
|
|
162
|
+
return { listing: answer.listing, quote, notes: [...answer.notes, ...notUs ? [notUs] : []] };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A symbol written like a currency pair that is not a ticker (USD_NGN, as some
|
|
167
|
+
* FX feeds write a pair) names the listing `gloomberb search` finds nearest to
|
|
168
|
+
* it, as `watchlist add` does: "Did you mean USDNGN=X (US Dollar/Nigerian
|
|
169
|
+
* Naira)?". When search finds nothing near, the row keeps its own error and its
|
|
170
|
+
* search hint.
|
|
171
|
+
*/
|
|
172
|
+
async function withCurrencyPairHint(result: QuoteBatchResult, market: MarketContext): Promise<QuoteBatchResult> {
|
|
173
|
+
const message = errorMessage(result.error);
|
|
174
|
+
const symbol = message ? notATickerSymbol(message) : null;
|
|
175
|
+
if (!message || !symbol || !looksLikeCurrencyPair(symbol)) return result;
|
|
176
|
+
const suggestion = await suggestListing(symbol, market);
|
|
177
|
+
return suggestion ? { ...result, error: withListingSuggestion(message, describeSuggestion(suggestion)) } : result;
|
|
178
|
+
}
|
|
179
|
+
|
package/src/cli/series-search.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { seriesExpression } from "../series/registry";
|
|
2
2
|
import type { SeriesDefinition } from "../series/types";
|
|
3
3
|
import { cliStyles, renderSection, renderTable, wrapText } from "../utils/cli-output";
|
|
4
|
+
import { currencyCodeLabel } from "../utils/currency-units";
|
|
4
5
|
|
|
5
6
|
/** A data catalog series a query names, as the CLI lists it. */
|
|
6
7
|
export interface SeriesMatch {
|
|
@@ -108,7 +109,7 @@ export function renderSeriesMatches(
|
|
|
108
109
|
renderTable(
|
|
109
110
|
[{ header: "Series" }, { header: "Unit" }, { header: "Frequency", optional: true }, { header: "Publisher", optional: true },
|
|
110
111
|
{ header: "Chart", shrink: false }],
|
|
111
|
-
shown.map((match) => [match.name, match.unit, match.frequency, match.publisher ?? "", cliStyles.command(shellArgument(match.expression))]),
|
|
112
|
+
shown.map((match) => [match.name, currencyCodeLabel(match.unit), match.frequency, match.publisher ?? "", cliStyles.command(shellArgument(match.expression))]),
|
|
112
113
|
),
|
|
113
114
|
"",
|
|
114
115
|
...wrapText(hint, width).map((line) => cliStyles.muted(line)),
|
package/src/cli/types.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { AssetDataRouter } from "../sources/provider-router";
|
|
|
5
5
|
import type { AppServices } from "../core/app-services";
|
|
6
6
|
import type { CapabilityInvoker } from "../capabilities/types";
|
|
7
7
|
import type { StatementReportSources } from "./statement-filings";
|
|
8
|
+
import type { FxHistoryClient } from "../market-data/fx-closes";
|
|
8
9
|
|
|
9
10
|
export type ConfigContext = {
|
|
10
11
|
config: AppConfig;
|
|
@@ -15,12 +16,15 @@ export type ConfigContext = {
|
|
|
15
16
|
|
|
16
17
|
export type MarketContext = ConfigContext & {
|
|
17
18
|
dataProvider: AssetDataRouter;
|
|
19
|
+
resolveBroker?: (brokerType: string) => import("../types/broker").BrokerAdapter | null;
|
|
18
20
|
/** Charts catalog series in a report run without the plugin runtime. */
|
|
19
21
|
seriesCapabilities?: CapabilityInvoker;
|
|
20
22
|
/** `--refresh`: prefetched data bypasses the local cache. */
|
|
21
23
|
refresh?: boolean;
|
|
22
24
|
/** Filings behind the statements and a bank's figures, read from Gloom Cloud; absent where the data service is a stand-in. */
|
|
23
25
|
statementSources?: StatementReportSources;
|
|
26
|
+
/** Daily FX closes (`fx --date`, converted INS values); Gloom Cloud unless a test stands in for it. */
|
|
27
|
+
fxHistory?: FxHistoryClient;
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
export type CliServicesContext = {
|
|
@@ -11,6 +11,7 @@ import { ChartToolbar, CHART_TOOLBAR_WIDTH } from "./toolbar";
|
|
|
11
11
|
import { AsciiText, Box, ChartSurface, Text, useUiCapabilities, type BoxRenderable } from "../../../ui";
|
|
12
12
|
import { useLiveStretch } from "../../../ui/live-stretch";
|
|
13
13
|
import { useOptionalPaneInstanceId } from "../../../state/app/context";
|
|
14
|
+
import { usePaneFocused } from "../../../state/app/activity";
|
|
14
15
|
import { useTabCursorLink } from "../../../state/app/tab-cursor";
|
|
15
16
|
import { useThemeColors, useThemeId } from "../../../theme/theme-context";
|
|
16
17
|
import { themedSeriesColor, themeSeriesColors } from "../../../theme/series-colors";
|
|
@@ -155,6 +156,8 @@ function ThemedCompositeChart({
|
|
|
155
156
|
const showTextFallback = useShowChartTextFallback();
|
|
156
157
|
const [internalCursorDate, setInternalCursorDate] = useState<Date | null>(null);
|
|
157
158
|
const [toolSpan, setToolSpan] = useState<ChartToolSpan | null>(null);
|
|
159
|
+
const [pointerDragging, setPointerDragging] = useState(false);
|
|
160
|
+
const paneFocused = usePaneFocused();
|
|
158
161
|
const paneInstanceId = useOptionalPaneInstanceId();
|
|
159
162
|
const [drawings, setDrawings] = useState<readonly ChartDrawing[]>(NO_DRAWINGS);
|
|
160
163
|
const [selectedDrawingId, setSelectedDrawingId] = useState<string | null>(null);
|
|
@@ -333,6 +336,7 @@ function ThemedCompositeChart({
|
|
|
333
336
|
availableAxisWidth,
|
|
334
337
|
Math.max(
|
|
335
338
|
Math.min(MINIMUM_AXIS_LABEL_WIDTH, maximumAxisWidth),
|
|
339
|
+
...(levels?.items ?? []).map((level) => level.axisLabel?.length ?? 0),
|
|
336
340
|
...(layoutPanels ?? []).flatMap((panel) => (["left", "right"] as const).map((side) => {
|
|
337
341
|
const width = compositeAxisLabelWidth(panel.axes[side], formatAxisValue, includeCursorLabels);
|
|
338
342
|
if (!includeSeriesTags || !width) return includeCursorLabels ? width : Math.min(width, maximumAxisWidth);
|
|
@@ -343,7 +347,7 @@ function ThemedCompositeChart({
|
|
|
343
347
|
return (width + 1) * lanes;
|
|
344
348
|
})),
|
|
345
349
|
),
|
|
346
|
-
), [availableAxisWidth, formatAxisValue, includeCursorLabels, includeSeriesTags, layoutPanels, maximumAxisWidth]);
|
|
350
|
+
), [availableAxisWidth, formatAxisValue, includeCursorLabels, includeSeriesTags, layoutPanels, maximumAxisWidth, levels?.items]);
|
|
347
351
|
const leftAxisWidth = hasLeftAxis ? resolvedAxisWidth : 0;
|
|
348
352
|
const rightAxisWidth = hasRightAxis ? resolvedAxisWidth : 0;
|
|
349
353
|
const axisGap = resolvedAxisWidth > 0 ? 1 : 0;
|
|
@@ -650,7 +654,11 @@ function ThemedCompositeChart({
|
|
|
650
654
|
);
|
|
651
655
|
}
|
|
652
656
|
|
|
653
|
-
|
|
657
|
+
// The tools belong to the focused pane, and stay while one is in hand or a
|
|
658
|
+
// press drags the plot, so focus moving elsewhere never hides what is
|
|
659
|
+
// drawing. A chart outside a pane (a share page) keeps them.
|
|
660
|
+
const toolbarWanted = paneFocused !== false || armedTool !== null || pointerDragging || toolSpan !== null;
|
|
661
|
+
const showToolbar = interactive && navigable && toolbarWanted && plotWidth > CHART_TOOLBAR_WIDTH + 4;
|
|
654
662
|
// Bars shorter than a day carry a time of day the cursor reads out.
|
|
655
663
|
const intradayBars = scene.timeZone !== undefined || visibleSeries.some((entry) => (
|
|
656
664
|
!!entry.historyResolution && isIntradayResolution(entry.historyResolution)
|
|
@@ -772,6 +780,7 @@ function ThemedCompositeChart({
|
|
|
772
780
|
onZoomViewport={zoomViewport}
|
|
773
781
|
onSetViewport={setViewportRange}
|
|
774
782
|
onToolSpanChange={setToolSpan}
|
|
783
|
+
onDragActiveChange={setPointerDragging}
|
|
775
784
|
keyboardToolDrag={keyboardToolDrag?.panelId === panel.id ? keyboardToolDrag : null}
|
|
776
785
|
onPointerPress={cancelKeyboardPlacement}
|
|
777
786
|
showTextFallback={showTextFallback}
|
|
@@ -298,6 +298,14 @@ export function formatChartLegendValue(
|
|
|
298
298
|
return trimmed && trimmed.length <= 6 ? `${compact}${trimmed.startsWith("/") ? "" : " "}${trimmed}` : compact;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/** compactNumber's scale, with the decimals a tick gap needs at that scale. */
|
|
302
|
+
function preciseCompactNumber(value: number, gap: number): string {
|
|
303
|
+
const absolute = Math.abs(value);
|
|
304
|
+
const [divisor, suffix] = ([[1e12, "T"], [1e9, "B"], [1e6, "M"], [1e3, "K"]] as const).find(([scale]) => absolute >= scale) ?? [1, ""];
|
|
305
|
+
const digits = Math.min(12, Math.max(0, Math.ceil(-Math.log10(gap / divisor) - 1e-9)));
|
|
306
|
+
return `${(value / divisor).toFixed(digits)}${suffix}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
301
309
|
const COMPACT_LABEL = /^(-?\d+)(?:\.(\d+))?([KMBT]?)$/;
|
|
302
310
|
|
|
303
311
|
function trimmedDecimals(label: string): number {
|
|
@@ -332,13 +340,17 @@ export function formatCompositeAxisValue(value: number, domain: CompositeAxisDom
|
|
|
332
340
|
// Where that happens the axis spends the digits needed to tell them apart,
|
|
333
341
|
// shared across every tick so the column reads as one scale.
|
|
334
342
|
const gap = narrowestAxisTickGap(domain);
|
|
343
|
+
const prefix = currencyPrefix(domain.unit);
|
|
335
344
|
if (gap !== null && gap < compactResolution(value)) {
|
|
336
345
|
const resolved = formatAxisPriceValue(value, domain, {
|
|
337
346
|
fixedFractionDigits: Math.max(0, Math.ceil(-Math.log10(gap))),
|
|
338
347
|
});
|
|
339
348
|
if (resolved) return resolved;
|
|
349
|
+
// No currency to format with, such as a price whose quote has not
|
|
350
|
+
// arrived: the same digits still keep the ticks apart.
|
|
351
|
+
const precise = preciseCompactNumber(value, gap);
|
|
352
|
+
return prefix && precise.startsWith("-") ? `-${prefix}${precise.slice(1)}` : `${prefix}${precise}`;
|
|
340
353
|
}
|
|
341
|
-
const prefix = currencyPrefix(domain.unit);
|
|
342
354
|
return prefix && compact.startsWith("-") ? `-${prefix}${compact.slice(1)}` : `${prefix}${compact}`;
|
|
343
355
|
}
|
|
344
356
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { blendForContrast, higherContrast } from "../../../theme/color-utils";
|
|
3
|
+
import { fitAxisLabels, type CompositeChartLevel, type ProjectedLevel } from "./levels";
|
|
4
|
+
|
|
5
|
+
/** Keep a precise price in the tag only when it has no readable axis marker. */
|
|
6
|
+
export function levelLabelText(level: CompositeChartLevel, axisVisible: boolean): string {
|
|
7
|
+
return `${level.label ?? ""}${!axisVisible && level.axisLabel ? ` ${level.axisLabel}` : ""}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Compact DOM tags straddle their line; crowded or clipped tags yield. */
|
|
11
|
+
export function layoutLevelLabels(levels: readonly ProjectedLevel[], plotHeight: number, textHeight: number) {
|
|
12
|
+
const candidates = levels.filter(({ level }) => level.label).map((entry) => ({
|
|
13
|
+
...entry, top: entry.yRatio * plotHeight - textHeight / 2,
|
|
14
|
+
})).filter(({ top }) => top >= 0 && top + textHeight <= plotHeight);
|
|
15
|
+
return fitAxisLabels(candidates, [], ({ top }) => top / textHeight);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function levelLabelStyle({ yRatio, level }: ProjectedLevel, background: string): CSSProperties {
|
|
19
|
+
return {
|
|
20
|
+
top: `${yRatio * 100}%`,
|
|
21
|
+
transform: "translateY(-50%)",
|
|
22
|
+
pointerEvents: "none",
|
|
23
|
+
paddingInline: 3,
|
|
24
|
+
height: "auto",
|
|
25
|
+
width: "max-content",
|
|
26
|
+
border: `1px solid ${level.color}`,
|
|
27
|
+
backgroundColor: background,
|
|
28
|
+
color: blendForContrast(level.color, background, higherContrast("#000000", "#ffffff", background), 4.5),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -14,6 +14,11 @@ export interface CompositeChartLevel {
|
|
|
14
14
|
editable: boolean;
|
|
15
15
|
/** Whether the chart's level action applies to it. */
|
|
16
16
|
actionable: boolean;
|
|
17
|
+
/** Optional read-only annotation, independent of drawing-tool selection. */
|
|
18
|
+
label?: string;
|
|
19
|
+
/** Exact instrument price, independent of the grid's compact tick formatter. */
|
|
20
|
+
axisLabel?: string;
|
|
21
|
+
dashed?: boolean;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export type CompositeLevelEdit =
|
|
@@ -111,13 +116,17 @@ export function fitAxisLabels<T>(
|
|
|
111
116
|
});
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
function levelSegments(dashed?: boolean): Array<[number, number]> {
|
|
120
|
+
return dashed ? Array.from({ length: 48 }, (_, i) => [i / 48, (i + 0.55) / 48]) : [[0, 1]];
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
export function levelVectors(levels: readonly ProjectedLevel[], selectedId: string | null): ChartVectorShape[] {
|
|
115
|
-
return levels.
|
|
116
|
-
id: `level:${level.id}`,
|
|
117
|
-
points: [{ x:
|
|
124
|
+
return levels.flatMap(({ level, yRatio }) => levelSegments(level.dashed).map(([start, end], index) => ({
|
|
125
|
+
id: `level:${level.id}:${index}`,
|
|
126
|
+
points: [{ x: start, y: yRatio }, { x: end, y: yRatio }],
|
|
118
127
|
color: level.color,
|
|
119
128
|
strokeWidth: level.id === selectedId ? 2.2 : 1.2,
|
|
120
|
-
}));
|
|
129
|
+
})));
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
/** Levels painted on a copy of the panel raster, the one layer the terminal uploads. */
|
|
@@ -131,7 +140,9 @@ export function paintLevels(
|
|
|
131
140
|
const maxY = Math.max(base.height - 1, 0);
|
|
132
141
|
for (const { level, yRatio } of levels) {
|
|
133
142
|
const y = Math.round(yRatio * maxY);
|
|
134
|
-
|
|
143
|
+
for (const [start, end] of levelSegments(level.dashed)) {
|
|
144
|
+
drawLine(data, base.width, base.height, start * maxX, y, end * maxX, y, parseHex(level.color), level.id === selectedId ? 2.2 : 1.2);
|
|
145
|
+
}
|
|
135
146
|
}
|
|
136
147
|
return { width: base.width, height: base.height, pixels: data };
|
|
137
148
|
}
|
|
@@ -145,7 +156,7 @@ export function writeLevelText(lines: readonly string[], levels: readonly Projec
|
|
|
145
156
|
const rows = lines.map((line) => Array.from(line));
|
|
146
157
|
for (const { level, yRatio } of levels) {
|
|
147
158
|
const row = rows[clamp(Math.round(yRatio * (rows.length - 1)), 0, rows.length - 1)]!;
|
|
148
|
-
const glyph = level.id === selectedId ? "━" : level.editable ? "
|
|
159
|
+
const glyph = level.id === selectedId ? "━" : level.dashed || !level.editable ? "┄" : "─";
|
|
149
160
|
for (let x = 0; x < row.length; x += 1) {
|
|
150
161
|
if (row[x] === " " || row[x] === "·" || row[x] === "╌") row[x] = glyph;
|
|
151
162
|
}
|
|
@@ -12,7 +12,8 @@ import { colors as themeColors } from "../../../theme/colors";
|
|
|
12
12
|
import { consumeChartMouseEvent, getGlobalMouseX, getLocalPlotPointer, type ChartMouseEvent } from "../core/pointer";
|
|
13
13
|
import { useStaticChartBitmapSize } from "./bitmap";
|
|
14
14
|
import { useMeasuredCells } from "../../../ui/measured-cells";
|
|
15
|
-
import { PriceAxisLabels } from "./price-axis-labels";
|
|
15
|
+
import { PriceAxisLabels, placeAxisTicks } from "./price-axis-labels";
|
|
16
|
+
import { layoutLevelLabels, levelLabelStyle, levelLabelText } from "./level-labels";
|
|
16
17
|
import { formatCompositeCursorValue, type CompositeAxisValueFormatter } from "./format";
|
|
17
18
|
import {
|
|
18
19
|
compositeViewportPositions,
|
|
@@ -111,6 +112,8 @@ interface CompositePanelSurfaceProps {
|
|
|
111
112
|
onZoomViewport: (zoomFactor: number, anchorRatio: number) => void;
|
|
112
113
|
onSetViewport: (range: CompositeViewportRange) => void;
|
|
113
114
|
onToolSpanChange: (span: ChartToolSpan | null) => void;
|
|
115
|
+
/** A press on the plot started (true) or stopped (false) dragging: a pan, a tool, a drawing or a level. */
|
|
116
|
+
onDragActiveChange?: (dragging: boolean) => void;
|
|
114
117
|
/** The tool being placed from the keyboard, when it lives in this panel. */
|
|
115
118
|
keyboardToolDrag: KeyboardToolDrag | null;
|
|
116
119
|
/** A press on the plot: the pointer takes over from a keyboard placement. */
|
|
@@ -154,6 +157,7 @@ export function CompositePanelSurface({
|
|
|
154
157
|
onZoomViewport,
|
|
155
158
|
onSetViewport,
|
|
156
159
|
onToolSpanChange,
|
|
160
|
+
onDragActiveChange,
|
|
157
161
|
keyboardToolDrag,
|
|
158
162
|
onPointerPress,
|
|
159
163
|
showTextFallback,
|
|
@@ -477,14 +481,16 @@ export function CompositePanelSurface({
|
|
|
477
481
|
.sort((left, right) => Number(right.level.id === selectedLevelId) - Number(left.level.id === selectedLevelId)
|
|
478
482
|
|| Number(left.level.editable) - Number(right.level.editable))
|
|
479
483
|
.map(({ level, yRatio }) => ({
|
|
484
|
+
id: level.id,
|
|
480
485
|
side: levelDomain.side,
|
|
481
486
|
yRatio,
|
|
482
|
-
label: formatAxisValue ? formatAxisValue(level.value, levelDomain) : formatCompositeCursorValue(level.value, levelDomain),
|
|
487
|
+
label: level.axisLabel ?? (formatAxisValue ? formatAxisValue(level.value, levelDomain) : formatCompositeCursorValue(level.value, levelDomain)),
|
|
483
488
|
color: level.color,
|
|
484
489
|
})) : [], [formatAxisValue, levelDomain, projectedLevels, selectedLevelId]);
|
|
485
490
|
const buildAxisMarkers = (side: "left" | "right") => {
|
|
486
491
|
if (!panel.axes[side]) return undefined;
|
|
487
|
-
const toAxisMarker = (marker: { yRatio: number; label: string; color: string }) => ({
|
|
492
|
+
const toAxisMarker = (marker: { id?: string; yRatio: number; label: string; color: string }) => ({
|
|
493
|
+
id: marker.id,
|
|
488
494
|
row: Math.round(marker.yRatio * Math.max(panel.height - 1, 0)),
|
|
489
495
|
pixelY: marker.yRatio * Math.max(panel.height * cellHeightPx - 1, 0),
|
|
490
496
|
label: marker.label,
|
|
@@ -499,8 +505,14 @@ export function CompositePanelSurface({
|
|
|
499
505
|
// A label is a row tall, so close levels would stack into an unreadable
|
|
500
506
|
// pile and hide the last price: each keeps its line, and only the labels
|
|
501
507
|
// with a row to themselves show. Desktop badges sit between rows.
|
|
508
|
+
const grid = fractionalViewport
|
|
509
|
+
? placeAxisTicks({ ticks: side === "left" ? leftAxisTicks : rightAxisTicks, height: panel.height, cellHeightPx }).map((tick) => ({
|
|
510
|
+
...toAxisMarker({ yRatio: tick.ratio, label: tick.label, color: colors.textDim }),
|
|
511
|
+
pixelY: tick.topPercent / 100 * Math.max(panel.height * cellHeightPx - 1, 1),
|
|
512
|
+
}))
|
|
513
|
+
: [...(side === "left" ? leftAxisLabels : rightAxisLabels)].map(([row, label]) => ({ id: undefined, row, pixelY: row * cellHeightPx, label, color: colors.textDim }));
|
|
502
514
|
const markers = [
|
|
503
|
-
...fitAxisLabels(levelLabels, fixed, (marker) => fractionalViewport ? marker.pixelY / cellHeightPx : marker.row),
|
|
515
|
+
...fitAxisLabels(levelLabels, [...fixed, ...grid], (marker) => fractionalViewport ? marker.pixelY / cellHeightPx : marker.row),
|
|
504
516
|
...fixed,
|
|
505
517
|
];
|
|
506
518
|
return markers.length > 0 ? markers : undefined;
|
|
@@ -736,12 +748,29 @@ export function CompositePanelSurface({
|
|
|
736
748
|
}
|
|
737
749
|
finishToolDrag();
|
|
738
750
|
}, [finishToolDrag]);
|
|
751
|
+
// Said once per change, so a pointer move costs the chart nothing.
|
|
752
|
+
const reportedDragRef = useRef(false);
|
|
753
|
+
const reportDrag = useCallback((dragging: boolean) => {
|
|
754
|
+
if (reportedDragRef.current === dragging) return;
|
|
755
|
+
reportedDragRef.current = dragging;
|
|
756
|
+
onDragActiveChange?.(dragging);
|
|
757
|
+
}, [onDragActiveChange]);
|
|
758
|
+
const pressPlot = useCallback((event: ChartMouseEvent) => {
|
|
759
|
+
const before = dragRef.current;
|
|
760
|
+
startDrag(event);
|
|
761
|
+
if (dragRef.current !== null && dragRef.current !== before) reportDrag(true);
|
|
762
|
+
}, [reportDrag, startDrag]);
|
|
763
|
+
const releasePlot = useCallback(() => {
|
|
764
|
+
resetDrag();
|
|
765
|
+
reportDrag(false);
|
|
766
|
+
}, [reportDrag, resetDrag]);
|
|
739
767
|
const handleMouseMove = useCallback((event: ChartMouseEvent) => {
|
|
740
768
|
// A release over a neighbouring pane never reaches this surface, so treat a
|
|
741
769
|
// plain move, which only fires with no button held, as the missing release.
|
|
742
770
|
finishToolDrag();
|
|
771
|
+
reportDrag(false);
|
|
743
772
|
updateCursor(event);
|
|
744
|
-
}, [finishToolDrag, updateCursor]);
|
|
773
|
+
}, [finishToolDrag, reportDrag, updateCursor]);
|
|
745
774
|
// Trackpads fire wheel events faster than the plot can paint, so one frame
|
|
746
775
|
// absorbs every event that arrived since the last one.
|
|
747
776
|
const pendingWheelRef = useRef<PendingWheel | null>(null);
|
|
@@ -876,10 +905,10 @@ export function CompositePanelSurface({
|
|
|
876
905
|
crosshair={crosshair}
|
|
877
906
|
vectors={vectors}
|
|
878
907
|
onMouseMove={interactive ? handleMouseMove : undefined}
|
|
879
|
-
onMouseDown={interactive ? navigable ?
|
|
908
|
+
onMouseDown={interactive ? navigable ? pressPlot : pressCursor : undefined}
|
|
880
909
|
onMouseDrag={interactive && navigable ? dragViewport : undefined}
|
|
881
|
-
onMouseUp={interactive && navigable ?
|
|
882
|
-
onMouseDragEnd={interactive && navigable ?
|
|
910
|
+
onMouseUp={interactive && navigable ? releasePlot : undefined}
|
|
911
|
+
onMouseDragEnd={interactive && navigable ? releasePlot : undefined}
|
|
883
912
|
onMouseScroll={interactive && navigable ? panFromWheel : undefined}
|
|
884
913
|
onMouseOut={interactive ? clearCursor : undefined}
|
|
885
914
|
cursor={interactive ? toolDrag || !navigable || onPickTime ? "crosshair" : "grab" : undefined}
|
|
@@ -890,6 +919,27 @@ export function CompositePanelSurface({
|
|
|
890
919
|
>
|
|
891
920
|
{textLines.map((line, index) => <Text key={index} fg={colors.text}>{line}</Text>)}
|
|
892
921
|
</ChartSurface>
|
|
922
|
+
{(isDesktopWeb
|
|
923
|
+
? layoutLevelLabels(projectedLevels, plotSize.height * cellHeightPx, cellHeightPx)
|
|
924
|
+
: fitAxisLabels(projectedLevels.filter((entry) => entry.level.label), [], (entry) => entry.yRatio * Math.max(1, panel.height - 1))
|
|
925
|
+
).map((entry) => {
|
|
926
|
+
const { level, yRatio } = entry;
|
|
927
|
+
const axisWidth = levelDomain?.side === "left" ? leftAxisWidth : rightAxisWidth;
|
|
928
|
+
const axisMarkers = levelDomain?.side === "left" ? leftAxisMarkers : rightAxisMarkers;
|
|
929
|
+
const cursorCovers = activeCursorYRatio !== null && Math.abs(activeCursorYRatio - yRatio) * panel.height < 1;
|
|
930
|
+
const axisVisible = !cursorCovers && (level.axisLabel?.length ?? 0) <= axisWidth && !!axisMarkers?.some((marker) => marker.id === level.id);
|
|
931
|
+
const label = levelLabelText(level, axisVisible);
|
|
932
|
+
const labelStyle = isDesktopWeb ? {
|
|
933
|
+
...levelLabelStyle(entry, colors.background), left: "auto", right: (rightAxisWidth + (rightAxisWidth ? axisGap : 0) + 1) * cellWidthPx,
|
|
934
|
+
maxWidth: Math.max(1, plotWidth - 2) * cellWidthPx,
|
|
935
|
+
} : undefined;
|
|
936
|
+
const labelWidth = Math.min(plotWidth - 2, label.length + 2);
|
|
937
|
+
return <Box key={`annotation:${level.id}`} position="absolute" left={leftAxisWidth + (leftAxisWidth ? axisGap : 0) + plotWidth - labelWidth - 1} top={yRatio * Math.max(0, panel.height - 1)} height={1}
|
|
938
|
+
width={labelWidth} flexDirection="row" justifyContent="flex-end" backgroundColor={colors.background} zIndex={20}
|
|
939
|
+
style={labelStyle} data-gloom-role="chart-level-label">
|
|
940
|
+
<Text fg={labelStyle?.color ?? level.color} wrapMode="none" style={isDesktopWeb ? { lineHeight: 1 } : undefined}>{label}</Text>
|
|
941
|
+
</Box>;
|
|
942
|
+
})}
|
|
893
943
|
{rightAxisWidth > 0 ? (
|
|
894
944
|
<>
|
|
895
945
|
<Box width={axisGap} />
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type TickerSearchInstrumentClass,
|
|
18
18
|
} from "../../../../tickers/search";
|
|
19
19
|
import type { ResultItem } from "../../list/model";
|
|
20
|
+
import { delistingLabel, parseListingDelisting } from "../../../../market-data/quotes/delisting";
|
|
20
21
|
import {
|
|
21
22
|
buildTickerSearchCacheKey,
|
|
22
23
|
createQuickLookTickerCandidates,
|
|
@@ -74,6 +75,9 @@ export function useCommandBarTickerSearchActions({
|
|
|
74
75
|
// The candidate's detail leads with the name; the rest repeats the badge or venue.
|
|
75
76
|
const leadingName = candidate.detail.split(" | ")[0]?.trim();
|
|
76
77
|
const name = leadingName && leadingName.toUpperCase() !== candidate.label.toUpperCase() ? leadingName : undefined;
|
|
78
|
+
// A listing that no longer trades says so under its row, with its last session.
|
|
79
|
+
const delisting = parseListingDelisting(candidate.result?.delisting);
|
|
80
|
+
const lines = delisting ? [{ segments: [{ text: delistingLabel(delisting), emphasis: "muted" as const }] }] : undefined;
|
|
77
81
|
|
|
78
82
|
if (candidate.kind === "ticker" && candidate.ticker) {
|
|
79
83
|
return {
|
|
@@ -86,6 +90,7 @@ export function useCommandBarTickerSearchActions({
|
|
|
86
90
|
badge,
|
|
87
91
|
name,
|
|
88
92
|
right,
|
|
93
|
+
...(lines ? { lines } : {}),
|
|
89
94
|
category: candidate.category,
|
|
90
95
|
kind: "ticker",
|
|
91
96
|
resolveTicker: async () => candidate.ticker!,
|
|
@@ -112,6 +117,7 @@ export function useCommandBarTickerSearchActions({
|
|
|
112
117
|
badge,
|
|
113
118
|
name,
|
|
114
119
|
right,
|
|
120
|
+
...(lines ? { lines } : {}),
|
|
115
121
|
category: candidate.category,
|
|
116
122
|
kind: "search",
|
|
117
123
|
resolveTicker: () => resolveSearchTicker(candidate.result!, publicTickerKey(
|
package/src/components/index.ts
CHANGED
|
@@ -101,6 +101,8 @@ export { usePaneStatusFooter, usePaneStatusLinkFooter } from "./layout/pane/stat
|
|
|
101
101
|
export { usePaneNoticeFooter, type UsePaneNoticeFooterOptions } from "./use-pane-notice-footer";
|
|
102
102
|
export { loadingErrorFooterInfo } from "./data-table/table-pane";
|
|
103
103
|
export type { PaneFooterPressEvent, PaneFooterSegment, PaneHint } from "./layout/pane/footer";
|
|
104
|
+
// Data a source delivers late: the footer's yellow clock, its detail on hover.
|
|
105
|
+
export { DELAYED_SEGMENT, delayedFooterSegment, splitDelayedStatus } from "./layout/pane/footer";
|
|
104
106
|
export {
|
|
105
107
|
getPaneSidebarWidth,
|
|
106
108
|
getPaneSidebarWidthRange,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { memo, Profiler, useCallback, useMemo, type ReactNode } from "react";
|
|
2
2
|
import { useAppLanguage } from "../../../i18n/react";
|
|
3
3
|
import { isPerfTraceEnabled, recordPerfSample } from "../../../utils/perf-marks";
|
|
4
|
-
import { PaneInViewProvider } from "../../../state/app/activity";
|
|
4
|
+
import { PaneFocusedProvider, PaneInViewProvider } from "../../../state/app/activity";
|
|
5
5
|
import { PaneInstanceProvider } from "../../../state/app/context";
|
|
6
6
|
import { RemoteUiPaneScope } from "../../../remote/semantic-tree";
|
|
7
7
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
@@ -91,19 +91,21 @@ export const PaneContent = memo(function PaneContent({
|
|
|
91
91
|
<PaneInstanceProvider paneId={paneId}>
|
|
92
92
|
<RemoteUiPaneScope paneId={paneId}>
|
|
93
93
|
<PaneInViewProvider value={inView}>
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
94
|
+
<PaneFocusedProvider value={focused}>
|
|
95
|
+
<PaneKeyboardScrollController paneId={paneId} focused={focused} />
|
|
96
|
+
<Box
|
|
97
|
+
flexDirection="column"
|
|
98
|
+
flexGrow={1}
|
|
99
|
+
flexShrink={1}
|
|
100
|
+
flexBasis={0}
|
|
101
|
+
minWidth={0}
|
|
102
|
+
minHeight={0}
|
|
103
|
+
overflow="hidden"
|
|
104
|
+
data-gloom-role="pane-content"
|
|
105
|
+
>
|
|
106
|
+
{body}
|
|
107
|
+
</Box>
|
|
108
|
+
</PaneFocusedProvider>
|
|
107
109
|
</PaneInViewProvider>
|
|
108
110
|
</RemoteUiPaneScope>
|
|
109
111
|
</PaneInstanceProvider>
|