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,7 +2,9 @@ import { formatPriceEarnings } from "../../../../utils/price-earnings";
|
|
|
2
2
|
import { convertMarketCapitalization } from "../../../../utils/market-capitalization";
|
|
3
3
|
import { priceColor } from "../../../../theme/colors";
|
|
4
4
|
import type { CompanyProfile, NextEarnings, Quote, TickerFinancials } from "../../../../types/financials";
|
|
5
|
-
import type { TickerPosition, TickerRecord } from "../../../../types/ticker";
|
|
5
|
+
import type { Portfolio, TickerPosition, TickerRecord } from "../../../../types/ticker";
|
|
6
|
+
import type { BrokerInstanceConfig } from "../../../../types/config";
|
|
7
|
+
import type { BrokerAccount } from "../../../../types/trading";
|
|
6
8
|
import {
|
|
7
9
|
formatCompact,
|
|
8
10
|
formatCompactCurrency,
|
|
@@ -65,15 +67,45 @@ function daysAhead(days: number, prefix?: string): string[] {
|
|
|
65
67
|
return days === 0 ? [`${lead}today`] : [`${lead}in ${days}d`, `${lead}${days}d`];
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
/** What the Account column reads names from: the portfolios, the brokers' accounts and their profiles. */
|
|
71
|
+
export interface PositionAccountNames {
|
|
72
|
+
portfolios: readonly Pick<Portfolio, "id" | "name" | "brokerAccountId" | "brokerInstanceId">[];
|
|
73
|
+
/** Accounts each broker profile reported, by profile id. */
|
|
74
|
+
brokerAccounts?: Readonly<Record<string, readonly Pick<BrokerAccount, "accountId" | "name">[]>>;
|
|
75
|
+
brokerInstances?: readonly Pick<BrokerInstanceConfig, "id" | "label">[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const NO_ACCOUNT_NAMES: PositionAccountNames = { portfolios: [] };
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The account a position is held in, by the name the Portfolio pane gives its
|
|
82
|
+
* portfolio ("Aries individual (simulation)"), never the broker's raw account
|
|
83
|
+
* id: a portfolio still named by its id takes the name the broker reports for
|
|
84
|
+
* the account, then the broker profile's.
|
|
85
|
+
*/
|
|
86
|
+
function positionAccountName(position: TickerPosition, names: PositionAccountNames): string {
|
|
87
|
+
const portfolio = names.portfolios.find((entry) => entry.id === position.portfolio);
|
|
88
|
+
const brokerPortfolio = position.portfolio.startsWith("broker:");
|
|
89
|
+
const accountId = position.brokerAccountId || portfolio?.brokerAccountId
|
|
90
|
+
|| (brokerPortfolio ? position.portfolio.split(":").slice(2).join(":") : "");
|
|
91
|
+
const named = (name: string | undefined) => {
|
|
92
|
+
const text = name?.trim();
|
|
93
|
+
return text && text !== accountId && text !== position.portfolio ? text : null;
|
|
94
|
+
};
|
|
95
|
+
const portfolioName = named(portfolio?.name);
|
|
96
|
+
if (portfolioName) return portfolioName;
|
|
97
|
+
const instanceId = position.brokerInstanceId || portfolio?.brokerInstanceId;
|
|
98
|
+
if (!accountId && !brokerPortfolio && !instanceId) return portfolio?.name.trim() || position.portfolio;
|
|
99
|
+
const accounts = instanceId ? names.brokerAccounts?.[instanceId] ?? [] : [];
|
|
100
|
+
const accountName = named(accounts.find((account) => account.accountId === accountId)?.name);
|
|
101
|
+
if (accountName) return accountName;
|
|
102
|
+
const profileName = named(names.brokerInstances?.find((instance) => instance.id === instanceId)?.label);
|
|
103
|
+
return profileName ?? position.broker;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function positionAccountLabel(position: TickerPosition, names: PositionAccountNames): string {
|
|
75
107
|
const suffix = signedPositionDirection(position) < 0 ? " SHORT" : "";
|
|
76
|
-
return `${
|
|
108
|
+
return `${positionAccountName(position, names)}${suffix}`;
|
|
77
109
|
}
|
|
78
110
|
|
|
79
111
|
export function buildOverviewStats({
|
|
@@ -294,12 +326,14 @@ export function buildPositionRows({
|
|
|
294
326
|
quoteCurrency,
|
|
295
327
|
baseCurrency,
|
|
296
328
|
toBase,
|
|
329
|
+
accountNames = NO_ACCOUNT_NAMES,
|
|
297
330
|
}: {
|
|
298
331
|
ticker: TickerRecord;
|
|
299
332
|
quote: Quote | undefined;
|
|
300
333
|
quoteCurrency: string;
|
|
301
334
|
baseCurrency: string;
|
|
302
335
|
toBase: CurrencyConverter;
|
|
336
|
+
accountNames?: PositionAccountNames;
|
|
303
337
|
}): PositionTableRow[] {
|
|
304
338
|
return ticker.metadata.positions.filter((position) => position.shares !== 0).map((position) => {
|
|
305
339
|
const positionCurrency = getPortfolioPositionMetrics(
|
|
@@ -328,7 +362,7 @@ export function buildPositionRows({
|
|
|
328
362
|
const unit = metrics.priceBasis === "percent-of-par" ? "" : ticker.metadata.assetCategory === "BOND" ? " units" : metrics.multiplierHint > 1 ? " ct" : " sh";
|
|
329
363
|
|
|
330
364
|
return {
|
|
331
|
-
account:
|
|
365
|
+
account: positionAccountLabel(position, accountNames),
|
|
332
366
|
qty: `${formatMarketQuantity(metrics.totalShares, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency, maxWidth: metrics.priceBasis === "percent-of-par" ? 11 : undefined })}${unit}`,
|
|
333
367
|
quantityUnit: metrics.priceBasis === "percent-of-par" ? "face" : undefined,
|
|
334
368
|
// Avg and Mark sit side by side, so both keep the currency's minor unit ($118.40 beside $224.36).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useOptionsQuery, useResolvedEntryValue } from "../../../../market-data/hooks";
|
|
3
|
+
import { formatMarketPriceWithCurrency } from "../../../../market-data/market/format";
|
|
4
|
+
import type { OptionContract } from "../../../../types/financials";
|
|
5
|
+
import type { TickerRecord } from "../../../../types/ticker";
|
|
6
|
+
import { formatCompact, formatNumber } from "../../../../utils/format";
|
|
7
|
+
import { expiryIsoDate, formatDaysToExpiry } from "../../../../utils/option-expiry";
|
|
8
|
+
import { resolveOptionsTarget, type ResolvedOptionsTarget } from "../../../../utils/options";
|
|
9
|
+
import type { StatField } from "./types";
|
|
10
|
+
|
|
11
|
+
const contractKey = (symbol: string) => symbol.replace(/\s+/g, "").toUpperCase();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An option contract's own row of its chain, the same query OMON reads: it
|
|
15
|
+
* carries the open interest and implied volatility a contract quote leaves out.
|
|
16
|
+
* Null for anything that is not an option, and until the chain answers.
|
|
17
|
+
*/
|
|
18
|
+
export function useOptionContractRow(ticker: TickerRecord | null | undefined): OptionContract | null {
|
|
19
|
+
const target = resolveOptionsTarget(ticker);
|
|
20
|
+
const option = target?.isOptionTicker && target.parsedOption ? target : null;
|
|
21
|
+
const entry = useOptionsQuery(option ? {
|
|
22
|
+
instrument: {
|
|
23
|
+
symbol: option.effectiveTicker, exchange: option.effectiveExchange,
|
|
24
|
+
brokerId: option.instrument?.brokerId, brokerInstanceId: option.instrument?.brokerInstanceId, instrument: option.instrument,
|
|
25
|
+
},
|
|
26
|
+
expirationDate: option.parsedOption!.expTs,
|
|
27
|
+
} : null);
|
|
28
|
+
const chain = useResolvedEntryValue(entry);
|
|
29
|
+
const symbol = ticker?.metadata.ticker;
|
|
30
|
+
return useMemo(() => {
|
|
31
|
+
if (!chain || !symbol || !option) return null;
|
|
32
|
+
const key = contractKey(symbol);
|
|
33
|
+
return [...chain.calls, ...chain.puts].find((contract) => contractKey(contract.contractSymbol) === key) ?? null;
|
|
34
|
+
}, [chain, option !== null, symbol]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What a contract is, then what its market says about it: underlying, side,
|
|
39
|
+
* strike, expiry and deliverable, with the open interest, implied volatility
|
|
40
|
+
* and volume the broker's chain gives. A figure the broker leaves out is left out.
|
|
41
|
+
*/
|
|
42
|
+
export function optionContractFields(
|
|
43
|
+
target: Pick<ResolvedOptionsTarget, "parsedOption" | "effectiveTicker" | "instrument"> | null,
|
|
44
|
+
row: OptionContract | null,
|
|
45
|
+
currency: string,
|
|
46
|
+
now = Date.now(),
|
|
47
|
+
): StatField[] {
|
|
48
|
+
const parsed = target?.parsedOption;
|
|
49
|
+
if (!target || !parsed) return [];
|
|
50
|
+
const fields: StatField[] = [
|
|
51
|
+
{ label: "Underlying", value: target.effectiveTicker },
|
|
52
|
+
{ label: "Type", value: parsed.side === "C" ? "Call" : "Put" },
|
|
53
|
+
{ label: "Strike", value: formatMarketPriceWithCurrency(parsed.strike, currency) },
|
|
54
|
+
{ label: "Expiry", value: expiryIsoDate(parsed.expTs), detail: formatDaysToExpiry(parsed.expTs, now) },
|
|
55
|
+
];
|
|
56
|
+
const multiplier = Number(target.instrument?.multiplier);
|
|
57
|
+
if (Number.isFinite(multiplier) && multiplier > 0) fields.push({ label: "Multiplier", value: formatNumber(multiplier, 0) });
|
|
58
|
+
if (row?.openInterest != null && Number.isFinite(row.openInterest)) fields.push({ label: "Open Int", value: formatCompact(row.openInterest) });
|
|
59
|
+
if (Number.isFinite(row?.impliedVolatility) && row!.impliedVolatility > 0) {
|
|
60
|
+
fields.push({ label: "Implied Vol", value: `${(row!.impliedVolatility * 100).toFixed(1)}%` });
|
|
61
|
+
}
|
|
62
|
+
if (row?.volume != null && Number.isFinite(row.volume)) fields.push({ label: "Volume", value: formatCompact(row.volume) });
|
|
63
|
+
return fields;
|
|
64
|
+
}
|
|
@@ -3,7 +3,9 @@ import { fundFactOverviewFields } from "../sec/fund-facts";
|
|
|
3
3
|
import { useFundFacts } from "../sec/fund-facts-view";
|
|
4
4
|
import { resolveTickerInstrumentKind } from "../../../tickers/instrument-kind";
|
|
5
5
|
import { mayBeUsEquityOrFundTicker } from "../../../utils/sec";
|
|
6
|
-
import
|
|
6
|
+
import { ResearchTradeActions, researchTradeMatches, useResearchTrading } from "../shared/research-trading";
|
|
7
|
+
import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
8
|
+
import { useMemo, type ReactNode } from "react";
|
|
7
9
|
import { EmptyState, PaneLinkMenu, SectionHeading, usePaneNoticeFooter } from "../../../components";
|
|
8
10
|
import { CompositeChart, pricePointsToResolvedSeries } from "../../../components/chart/composite";
|
|
9
11
|
import { CompanyLogo, resolveCompanyLogoSrc } from "../../../components/company-logo";
|
|
@@ -22,7 +24,11 @@ import {
|
|
|
22
24
|
quoteSessionLabel,
|
|
23
25
|
} from "../../../market-data/market/status";
|
|
24
26
|
import { appendQuoteToPriceReturnHistory, buildPriceReturnFields } from "../../../market-data/performance";
|
|
25
|
-
import {
|
|
27
|
+
import { changeBasisLabel, changeBasisNote, quoteChangeBasis } from "../../../market-data/market/change-basis";
|
|
28
|
+
import {
|
|
29
|
+
fitFuturesContractLines, futuresContractLines, futuresSettleText, isFuturesInstrument, minorUnitPricePer,
|
|
30
|
+
type FuturesContractLine,
|
|
31
|
+
} from "../../../market-data/futures-contract";
|
|
26
32
|
import { useViewport } from "../../../react/input";
|
|
27
33
|
import { useAppSelector } from "../../../state/app/context";
|
|
28
34
|
import { colors, priceColor } from "../../../theme/colors";
|
|
@@ -47,6 +53,8 @@ import {
|
|
|
47
53
|
} from "./overview/components";
|
|
48
54
|
import { useFuturesContractTerms } from "./overview/futures-contract";
|
|
49
55
|
import type { OverviewDividendState } from "./overview/dividend";
|
|
56
|
+
import { optionContractFields, useOptionContractRow } from "./overview/option-contract";
|
|
57
|
+
import { resolveOptionsTarget } from "../../../utils/options";
|
|
50
58
|
import { buildOverviewStats, buildPositionRows, buildProfileFields } from "./overview/model";
|
|
51
59
|
import type { OverviewFunctionLink, StatField } from "./overview/types";
|
|
52
60
|
import { describeFundamentalMarketCap } from "../../../utils/market-capitalization";
|
|
@@ -99,7 +107,15 @@ export function OverviewTab(props: OverviewTabProps) {
|
|
|
99
107
|
const FUND_FACTS_LINK: OverviewFunctionLink = { name: "Fund Facts", templateId: "sec-etf-pane" };
|
|
100
108
|
|
|
101
109
|
function ResolvedOverviewTab({ width, height, focused = false, ticker, financials, onOpenChart, onOpenFunction, perpetuals, dividend }: OverviewTabProps & { ticker: TickerRecord }) {
|
|
110
|
+
const trade = useResearchTrading();
|
|
102
111
|
const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
|
|
112
|
+
const portfolios = useAppSelector((state) => state.config.portfolios);
|
|
113
|
+
const brokerInstances = useAppSelector((state) => state.config.brokerInstances);
|
|
114
|
+
const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
|
|
115
|
+
const accountNames = useMemo(
|
|
116
|
+
() => ({ portfolios, brokerInstances, brokerAccounts }),
|
|
117
|
+
[brokerAccounts, brokerInstances, portfolios],
|
|
118
|
+
);
|
|
103
119
|
const { width: termWidth } = useViewport();
|
|
104
120
|
const { fractionalViewport = false, nativePaneChrome, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
|
|
105
121
|
const figureCells = useFigureCells();
|
|
@@ -114,6 +130,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
114
130
|
// A futures symbol's settlement and contract terms load beside the quote, never in its way.
|
|
115
131
|
const futuresSymbol = quote?.symbol || ticker.metadata.ticker;
|
|
116
132
|
const contractTerms = useFuturesContractTerms(isFuturesInstrument(futuresSymbol, instrumentType) ? futuresSymbol : null);
|
|
133
|
+
const optionRow = useOptionContractRow(ticker);
|
|
117
134
|
const exchangeRates = useFxRatesMap([
|
|
118
135
|
baseCurrency,
|
|
119
136
|
ticker.metadata.currency,
|
|
@@ -179,7 +196,9 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
179
196
|
],
|
|
180
197
|
focused,
|
|
181
198
|
});
|
|
182
|
-
|
|
199
|
+
// A listing that no longer trades has no book: its quote is its last session's close.
|
|
200
|
+
const delisted = !!quote?.delisting;
|
|
201
|
+
const hasBidAsk = !delisted && (quote?.bid != null || quote?.ask != null);
|
|
183
202
|
const quoteBookInline = hasBidAsk && contentWidth >= 68;
|
|
184
203
|
const quoteBookWidth = quoteBookInline ? Math.min(32, Math.max(24, Math.floor(contentWidth * 0.3))) : Math.min(contentWidth, 32);
|
|
185
204
|
const quoteSummaryWidth = quoteBookInline ? Math.max(20, contentWidth - quoteBookWidth - 2) : contentWidth;
|
|
@@ -188,9 +207,16 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
188
207
|
// The headline is the regular session, which stops at the close; extended trading is the line below it.
|
|
189
208
|
const sessionQuote = getRegularSessionDisplay(quote);
|
|
190
209
|
const extendedQuote = getExtendedSessionDisplay(quote);
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
210
|
+
// A price in cents says what it is per (314.95¢/lb), and the move what it is measured from (vs Fri 16 Oct close).
|
|
211
|
+
const quotePriceText = sessionQuote
|
|
212
|
+
? `${formatMarketPriceWithCurrency(sessionQuote.price, quote?.currency, moneyOptions)}${minorUnitPricePer(contractTerms, quote?.currency)}` : "";
|
|
213
|
+
// Nor a move: the close it ended on is where it stays.
|
|
214
|
+
const quoteChangeText = sessionQuote && !delisted ? formatSignedMarketPrice(sessionQuote.change, moneyOptions) : "";
|
|
215
|
+
const quotePercentText = sessionQuote && !delisted ? `(${formatPercentRaw(sessionQuote.changePercent)})` : "";
|
|
216
|
+
const changeBasis = delisted ? null : quoteChangeBasis(quote, { terms: contractTerms });
|
|
217
|
+
const changeBasisText = changeBasis ? changeBasisLabel(changeBasis) : "";
|
|
218
|
+
// The days the venue was closed since that close, under the move: "HKEX closed Mon 19 Oct, holiday".
|
|
219
|
+
const changeBasisNoteText = changeBasisNote(changeBasis);
|
|
194
220
|
// The pane title already names the ticker, so the line leads with the company and drops a name that only repeats it.
|
|
195
221
|
const companyName = [ticker.metadata.name, quote?.name].find((name) => name && name !== ticker.metadata.ticker) ?? "";
|
|
196
222
|
const venueText = listingVenue ? (companyName ? ` (${listingVenue})` : listingVenue) : "";
|
|
@@ -217,15 +243,25 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
217
243
|
? truncateToDisplayWidth(futuresSettleText(contractTerms, quote),
|
|
218
244
|
Math.max(1, quoteTextWidth - displayWidth(settleLabel) - 1))
|
|
219
245
|
: "";
|
|
220
|
-
const quoteChangeCells = figureCells(quoteChangeText, "sub") + 1 + figureCells(quotePercentText, "sub");
|
|
246
|
+
const quoteChangeCells = delisted ? 0 : figureCells(quoteChangeText, "sub") + 1 + figureCells(quotePercentText, "sub");
|
|
221
247
|
const stackQuoteChange = quoteChangeCells > quoteTextWidth;
|
|
222
248
|
const stackQuoteSummary = figureCells(quotePriceText) + 2 + quoteChangeCells > quoteTextWidth;
|
|
249
|
+
// What the move is from follows it on its line when there is room; otherwise it takes the line
|
|
250
|
+
// under the headline, so a narrow pane never splits the move itself to fit the words.
|
|
251
|
+
const changeBasisInline = !!changeBasisText && (stackQuoteSummary ? 0 : figureCells(quotePriceText) + 2)
|
|
252
|
+
+ quoteChangeCells + 1 + displayWidth(changeBasisText) <= quoteTextWidth;
|
|
223
253
|
const companyNameWidth = Math.max(8, quoteTextWidth
|
|
224
254
|
- displayWidth(venueText)
|
|
225
255
|
- (marketStateText ? displayWidth(marketStateText) + 1 : 0));
|
|
256
|
+
// A listing that no longer trades keeps its delisting date whole on a narrow pane: the venue gives way, then the name.
|
|
257
|
+
const delistedRoom = quote?.delisting && marketStateText ? quoteTextWidth - displayWidth(marketStateText) - 1 : null;
|
|
258
|
+
const identityVenue = delistedRoom !== null && delistedRoom - displayWidth(venueText) < 8 ? "" : venueText;
|
|
259
|
+
const identityNameWidth = delistedRoom !== null ? delistedRoom - displayWidth(identityVenue) : companyNameWidth;
|
|
260
|
+
const identityName = identityNameWidth >= 4 ? companyName : "";
|
|
226
261
|
const hasDayRange = quote?.low != null && quote?.high != null && quote.high > quote.low;
|
|
227
262
|
const yearRange = liveFiftyTwoWeekRange(quote);
|
|
228
|
-
|
|
263
|
+
// A listing that no longer trades: its range is its last session's, years ago perhaps, never today's.
|
|
264
|
+
const dayRangeLabel = delisted ? t("Last Session") : t("Day Range");
|
|
229
265
|
const yearRangeLabel = t("52W Range");
|
|
230
266
|
const rangeLabelWidth = Math.max(
|
|
231
267
|
hasDayRange ? displayWidth(dayRangeLabel) : 0,
|
|
@@ -243,7 +279,12 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
243
279
|
const contractLines = contractTerms ? futuresContractLines(contractTerms, futuresSymbol) : [];
|
|
244
280
|
// Columns that keep every term whole; a narrow pane stacks them in one.
|
|
245
281
|
const contractColumns = textGridColumns(contractLines.map(contractField), contentWidth);
|
|
246
|
-
const
|
|
282
|
+
const futuresFields = fitFuturesContractLines(contractLines, contractColumns * contractRowBudget(height)).map(contractField);
|
|
283
|
+
// An option contract states its own terms, and the chain's open interest and IV, where a stock has a profile.
|
|
284
|
+
const optionTarget = resolveOptionsTarget(ticker);
|
|
285
|
+
const optionFields = optionTarget?.isOptionTicker ? optionContractFields(optionTarget, optionRow, quoteCurrency) : [];
|
|
286
|
+
const contractFields = futuresFields.length > 0 ? futuresFields : optionFields;
|
|
287
|
+
const contractGridColumns = futuresFields.length > 0 ? contractColumns : textGridColumns(optionFields, contentWidth);
|
|
247
288
|
const stats = buildOverviewStats({
|
|
248
289
|
quote,
|
|
249
290
|
fundamentals,
|
|
@@ -260,7 +301,8 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
260
301
|
bond: fundFacts?.bond ?? null,
|
|
261
302
|
});
|
|
262
303
|
const profileFields = buildProfileFields({
|
|
263
|
-
|
|
304
|
+
// The contract section names an option's type.
|
|
305
|
+
instrumentType: optionFields.length > 0 ? undefined : instrumentTypeLabel(instrumentType, quote?.name || ticker.metadata.name),
|
|
264
306
|
sector,
|
|
265
307
|
industry,
|
|
266
308
|
profile,
|
|
@@ -276,6 +318,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
276
318
|
quoteCurrency,
|
|
277
319
|
baseCurrency,
|
|
278
320
|
toBase,
|
|
321
|
+
accountNames,
|
|
279
322
|
});
|
|
280
323
|
|
|
281
324
|
return (
|
|
@@ -315,13 +358,13 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
315
358
|
<Box flexDirection="column" flexGrow={1} flexShrink={1} minWidth={0}>
|
|
316
359
|
{hasIdentityLine && (
|
|
317
360
|
<Box flexDirection="row" minWidth={0} overflow="hidden">
|
|
318
|
-
{
|
|
319
|
-
<ClippedText fg={colors.textDim} text={
|
|
361
|
+
{identityName && (
|
|
362
|
+
<ClippedText fg={colors.textDim} text={identityName} width={identityNameWidth} />
|
|
320
363
|
)}
|
|
321
|
-
{
|
|
322
|
-
{
|
|
364
|
+
{identityVenue && <Text flexShrink={0} fg={colors.textDim}>{identityVenue}</Text>}
|
|
365
|
+
{marketStateText && (
|
|
323
366
|
<Text flexShrink={0} fg={quoteSessionColor(quote)}>
|
|
324
|
-
{
|
|
367
|
+
{identityName || identityVenue ? " " : ""}{marketStateText}
|
|
325
368
|
</Text>
|
|
326
369
|
)}
|
|
327
370
|
</Box>
|
|
@@ -334,12 +377,26 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
334
377
|
alignItems={stackQuoteSummary ? undefined : "baseline"}
|
|
335
378
|
>
|
|
336
379
|
<FigureText>{quotePriceText}</FigureText>
|
|
337
|
-
|
|
338
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
380
|
+
{!delisted && (
|
|
381
|
+
<Box flexDirection={stackQuoteChange ? "column" : "row"} gap={stackQuoteChange ? 0 : 1}
|
|
382
|
+
alignItems={changeBasisInline ? "baseline" : undefined}>
|
|
383
|
+
<FigureText part="sub" change={sessionQuote.change}>{quoteChangeText}</FigureText>
|
|
384
|
+
<FigureText part="sub" change={sessionQuote.change}>{quotePercentText}</FigureText>
|
|
385
|
+
{changeBasisInline ? <Text flexShrink={0} fg={colors.textDim}>{changeBasisText}</Text> : null}
|
|
386
|
+
</Box>
|
|
387
|
+
)}
|
|
341
388
|
</Box>
|
|
342
389
|
)}
|
|
390
|
+
{sessionQuote && changeBasisText && !changeBasisInline ? (
|
|
391
|
+
<Box minWidth={0} overflow="hidden">
|
|
392
|
+
<ClippedText fg={colors.textDim} text={changeBasisText} width={quoteTextWidth} />
|
|
393
|
+
</Box>
|
|
394
|
+
) : null}
|
|
395
|
+
{sessionQuote && changeBasisNoteText ? (
|
|
396
|
+
<Box minWidth={0} overflow="hidden">
|
|
397
|
+
<ClippedText fg={colors.textDim} text={changeBasisNoteText} width={quoteTextWidth} />
|
|
398
|
+
</Box>
|
|
399
|
+
) : null}
|
|
343
400
|
{extendedQuote && (
|
|
344
401
|
<Box flexDirection="row" gap={2}>
|
|
345
402
|
<Text fg={colors.textDim}>{extendedSessionLineLabel(extendedQuote, t)}:</Text>
|
|
@@ -384,7 +441,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
384
441
|
low={quote.low!}
|
|
385
442
|
high={quote.high!}
|
|
386
443
|
label={dayRangeLabel}
|
|
387
|
-
shortLabel={t("Day")}
|
|
444
|
+
shortLabel={delisted ? t("Last") : t("Day")}
|
|
388
445
|
labelWidth={rangeLabelWidth}
|
|
389
446
|
endpointWidth={rangeEndpointCells}
|
|
390
447
|
width={rangeWidth}
|
|
@@ -414,10 +471,11 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
414
471
|
{contractFields.length > 0 && (
|
|
415
472
|
<Box flexDirection="column">
|
|
416
473
|
<SectionHeading title={t("Contract")} />
|
|
417
|
-
<FundamentalsGrid fields={contractFields} width={contentWidth} columns={
|
|
474
|
+
<FundamentalsGrid fields={contractFields} width={contentWidth} columns={contractGridColumns} />
|
|
418
475
|
</Box>
|
|
419
476
|
)}
|
|
420
477
|
|
|
478
|
+
<ResearchTradeActions focused={focused} />
|
|
421
479
|
{hasHistory && (
|
|
422
480
|
<Box
|
|
423
481
|
onMouseDown={onOpenChart
|
|
@@ -437,6 +495,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
437
495
|
interactive={false}
|
|
438
496
|
series={[priceSeries]}
|
|
439
497
|
panels={[{ id: "price" }]}
|
|
498
|
+
levels={trade && researchTradeMatches(trade, instrumentFromTicker(ticker)) ? { seriesId: priceSeries.id, items: trade.levels } : undefined}
|
|
440
499
|
axisWidth={8}
|
|
441
500
|
showLegend={false}
|
|
442
501
|
/>
|
|
@@ -475,6 +534,8 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
475
534
|
</Box>
|
|
476
535
|
)}
|
|
477
536
|
|
|
537
|
+
{trade?.orders?.(contentWidth)}
|
|
538
|
+
|
|
478
539
|
{profileFields.length > 0 && (
|
|
479
540
|
<Box flexDirection="column">
|
|
480
541
|
<SectionHeading title={t("Profile")} />
|
|
@@ -39,6 +39,7 @@ import { ResearchTabKeysProvider, useResearchTabKeysHost } from "./research-tab-
|
|
|
39
39
|
import { ResearchTabNavigationProvider } from "./research-tab-navigation";
|
|
40
40
|
import { usePluginAppActions } from "../../../public/react";
|
|
41
41
|
import { resolveTickerInstrumentKind } from "../../../tickers/instrument-kind";
|
|
42
|
+
import { ResearchTradeWorkspace } from "../broker-trading/dock";
|
|
42
43
|
|
|
43
44
|
const TICKER_RESEARCH_TAB_COMMIT_DELAY_MS = 120;
|
|
44
45
|
/** A tab counts as viewed once it stays open this long, not when h/l passes over it. */
|
|
@@ -207,7 +208,9 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
207
208
|
const quoteFooterActive = resolvedTabId === "overview" || resolvedTabId === "chart";
|
|
208
209
|
const cloudAccess = useCloudAccessFooter({
|
|
209
210
|
delayLabel: tf("{count}m", { count: CLOUD_QUOTE_DELAY_MINUTES }),
|
|
210
|
-
|
|
211
|
+
// A listing that no longer trades has no delay to speak of: its quote is its last session's close.
|
|
212
|
+
degraded: !financials?.quote?.delisting && financials?.quote?.priceObservation !== "nav"
|
|
213
|
+
&& financials?.quote?.dataSource !== "live",
|
|
211
214
|
focused: focused && quoteFooterActive,
|
|
212
215
|
segmentId: "ticker-research-access",
|
|
213
216
|
shortcutScope: "ticker-research:upgrade",
|
|
@@ -359,16 +362,14 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
359
362
|
);
|
|
360
363
|
}
|
|
361
364
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
<Box height={contentHeight} flexGrow={1} flexBasis={0} overflow="hidden">
|
|
367
|
-
<ResearchTabKeysProvider value={showTabs ? researchTabKeys.value : null}>
|
|
365
|
+
const tradeAvailable = visibleTabIds.has("broker-trade");
|
|
366
|
+
// Trade shows Chart beside its ticket: the workspace names the tab it shows.
|
|
367
|
+
const renderTabs = (contentWidth: number, contentFocused: boolean, shownTabId = resolvedTabId) => (
|
|
368
|
+
<ResearchTabKeysProvider value={showTabs ? researchTabKeys.value : null}>
|
|
368
369
|
{tickerResearchTabs.map((tab) => {
|
|
369
|
-
if (!renderedTabIds.has(tab.id) || !visibleTabIds.has(tab.id)) return null;
|
|
370
|
+
if (tradeAvailable && tab.id === "broker-trade" || !(renderedTabIds.has(tab.id) || tab.id === shownTabId) || !visibleTabIds.has(tab.id)) return null;
|
|
370
371
|
const TickerResearchTab = tab.component;
|
|
371
|
-
const isActive =
|
|
372
|
+
const isActive = shownTabId === tab.id;
|
|
372
373
|
return (
|
|
373
374
|
<Box
|
|
374
375
|
key={tab.id}
|
|
@@ -383,9 +384,9 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
383
384
|
<NestedPaneTabs>
|
|
384
385
|
<ResearchTabNavigationProvider value={showTabs ? openResearchTab : null}>
|
|
385
386
|
<TickerResearchTab
|
|
386
|
-
width={
|
|
387
|
+
width={contentWidth}
|
|
387
388
|
height={contentHeight}
|
|
388
|
-
focused={
|
|
389
|
+
focused={contentFocused && isActive}
|
|
389
390
|
onCapture={isActive ? handlePluginCapture : ignorePluginCapture}
|
|
390
391
|
/>
|
|
391
392
|
</ResearchTabNavigationProvider>
|
|
@@ -394,8 +395,14 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
394
395
|
</Box>
|
|
395
396
|
);
|
|
396
397
|
})}
|
|
397
|
-
|
|
398
|
-
</Box>
|
|
399
|
-
</Box>
|
|
398
|
+
</ResearchTabKeysProvider>
|
|
400
399
|
);
|
|
400
|
+
return <Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
|
|
401
|
+
{tabStrip}
|
|
402
|
+
<Box height={contentHeight} flexGrow={1} flexBasis={0} overflow="hidden">
|
|
403
|
+
{tradeAvailable ? <ResearchTradeWorkspace width={width} height={contentHeight} focused={focused} symbol={ticker.metadata.ticker}
|
|
404
|
+
tabId={resolvedTabId} selectTab={selectResearchTab} onCapture={handlePluginCapture} locked={!!paneSettings.hideTabs}>{renderTabs}</ResearchTradeWorkspace>
|
|
405
|
+
: renderTabs(width, focused)}
|
|
406
|
+
</Box>
|
|
407
|
+
</Box>;
|
|
401
408
|
}
|
|
@@ -2,6 +2,7 @@ import type { PaneFooterSegment } from "../../../components/layout/pane/footer";
|
|
|
2
2
|
import { t } from "../../../i18n";
|
|
3
3
|
import { isQuoteStaleForCurrentSession } from "../../../market-data/quotes/freshness";
|
|
4
4
|
import { formatQuoteNavAsOf } from "../../../market-data/quotes/time";
|
|
5
|
+
import { delistingLabel } from "../../../market-data/quotes/delisting";
|
|
5
6
|
import type { Quote } from "../../../types/financials";
|
|
6
7
|
import { displayWidth } from "../../../utils/format";
|
|
7
8
|
|
|
@@ -12,7 +13,10 @@ export function tickerQuoteFooterInfo(
|
|
|
12
13
|
): PaneFooterSegment[] {
|
|
13
14
|
const info: PaneFooterSegment[] = [];
|
|
14
15
|
const navAsOf = formatQuoteNavAsOf(quote);
|
|
15
|
-
if (
|
|
16
|
+
if (quote?.delisting) {
|
|
17
|
+
// A listing that no longer trades: its quote is its last trade, said once with its date.
|
|
18
|
+
info.push({ id: "ticker-research-stale", parts: [{ text: delistingLabel(quote.delisting), tone: "warning" }] });
|
|
19
|
+
} else if (isQuoteStaleForCurrentSession(quote)) {
|
|
16
20
|
const status: PaneFooterSegment = { id: "ticker-research-stale", parts: [{ text: t("Stale quote"), tone: "warning" }] };
|
|
17
21
|
const timestamp = new Date(quote!.lastUpdated);
|
|
18
22
|
if (navAsOf) {
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "../../../../market-data/market/status";
|
|
21
21
|
import { t } from "../../../../i18n";
|
|
22
22
|
import { isQuoteStaleForCurrentSession } from "../../../../market-data/quotes/freshness";
|
|
23
|
+
import { delistingLabel, delistingStateLabel } from "../../../../market-data/quotes/delisting";
|
|
23
24
|
import { formatQuoteNavAsOf } from "../../../../market-data/quotes/time";
|
|
24
25
|
import { useQuoteFlashDirection } from "../../../../components/quote-flash";
|
|
25
26
|
import { followLiveSparklinePrice } from "../../../../components/price-sparkline/model";
|
|
@@ -89,6 +90,8 @@ interface QuoteStatus {
|
|
|
89
90
|
failed: boolean;
|
|
90
91
|
text: string;
|
|
91
92
|
stale?: boolean;
|
|
93
|
+
/** DELISTED or INACTIVE: the listing no longer trades, and `text` says since when. */
|
|
94
|
+
ended?: string;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
function resolveQuoteStatus(entry: QueryEntry<Quote> | null, symbol: string, quote: Quote | null | undefined): QuoteStatus {
|
|
@@ -98,6 +101,7 @@ function resolveQuoteStatus(entry: QueryEntry<Quote> | null, symbol: string, quo
|
|
|
98
101
|
? { failed: true, text: `${symbol} not recognized` }
|
|
99
102
|
: { failed: true, text: error.message || "Quote unavailable" };
|
|
100
103
|
}
|
|
104
|
+
if (quote?.delisting) return { failed: true, text: delistingLabel(quote.delisting), stale: true, ended: delistingStateLabel(quote.delisting) };
|
|
101
105
|
if (isQuoteStaleForCurrentSession(quote)) return { failed: true, text: "Stale quote", stale: true };
|
|
102
106
|
if (entry?.phase === "ready") return { failed: false, text: "No quote data" };
|
|
103
107
|
return { failed: false, text: "Loading quote..." };
|
|
@@ -175,8 +179,9 @@ export function QuoteMonitorCard({
|
|
|
175
179
|
// One decimal count per instrument, so streamed ticks never narrow or widen the price column.
|
|
176
180
|
const priceOptions = liveQuoteFormatOptions(quote, currency, assetCategory, cachedFinancials?.quoteMetadata?.instrumentType);
|
|
177
181
|
const priceText = display ? formatMarketPriceWithCurrency(display.price, currency, priceOptions) : "";
|
|
178
|
-
|
|
179
|
-
const
|
|
182
|
+
// A listing that no longer trades shows its last close alone: no move, not even a dash for one.
|
|
183
|
+
const changePercentText = display && !quote?.delisting ? formatPercentRaw(display.changePercent) : "";
|
|
184
|
+
const changeValueText = display && !quote?.delisting ? formatSignedMarketPrice(display.change, priceOptions) : "";
|
|
180
185
|
const priceColumnWidth = Math.max(priceText.length, changePercentText.length + changeValueText.length + 1);
|
|
181
186
|
const nameMaxWidth = Math.max(10, width - priceColumnWidth - (nativePaneChrome ? 5 : 3));
|
|
182
187
|
// The terminal sets the line beside the change, under the symbol (or after it in a
|
|
@@ -187,6 +192,8 @@ export function QuoteMonitorCard({
|
|
|
187
192
|
const extendedParts = display && extended
|
|
188
193
|
? extendedLineParts(extended, formatMarketPriceWithCurrency(extended.price, currency, priceOptions), (text) => displayWidth(text) <= extendedRoom)
|
|
189
194
|
: null;
|
|
195
|
+
// A desktop card says a listing no longer trades on a row of its own.
|
|
196
|
+
const endedRow = nativePaneChrome && quoteFailed && !!quoteStatus.ended && !extendedParts;
|
|
190
197
|
const extendedColor = quoteFailed ? colors.textDim : priceColor(extended?.change ?? 0);
|
|
191
198
|
const extendedDim = !!flashDirection && headlineHolds;
|
|
192
199
|
const sparklineRange = resolvePriceSparklineRange(priceHistory, chartPeriod);
|
|
@@ -254,7 +261,7 @@ export function QuoteMonitorCard({
|
|
|
254
261
|
<Text attributes={TextAttributes.BOLD} fg={symbolFg} bg={symbolBg} style={desktopSymbolStyle}>
|
|
255
262
|
{symbol}
|
|
256
263
|
</Text>
|
|
257
|
-
<Text fg={quoteStatus.failed ? colors.negative : colors.textDim}>{quoteStatus.text}</Text>
|
|
264
|
+
<Text fg={quoteStatus.ended ? colors.warning : quoteStatus.failed ? colors.negative : colors.textDim}>{quoteStatus.text}</Text>
|
|
258
265
|
</Box>
|
|
259
266
|
) : nativePaneChrome ? (
|
|
260
267
|
<Box
|
|
@@ -276,7 +283,7 @@ export function QuoteMonitorCard({
|
|
|
276
283
|
minWidth={0}
|
|
277
284
|
style={{
|
|
278
285
|
gridColumn: "1",
|
|
279
|
-
gridRow: extendedParts ? "1" : "1 / span 2",
|
|
286
|
+
gridRow: extendedParts || endedRow ? "1" : "1 / span 2",
|
|
280
287
|
minWidth: 0,
|
|
281
288
|
overflow: "hidden",
|
|
282
289
|
}}
|
|
@@ -284,8 +291,8 @@ export function QuoteMonitorCard({
|
|
|
284
291
|
<Text attributes={TextAttributes.BOLD} fg={symbolFg} bg={symbolBg} style={desktopSymbolStyle}>
|
|
285
292
|
{symbol}
|
|
286
293
|
</Text>
|
|
287
|
-
{quoteFailed && (
|
|
288
|
-
<Text fg={colors.negative} style={{ fontSize: "var(--gloom-type-ui)", lineHeight: "14px" }}>
|
|
294
|
+
{quoteFailed && !endedRow && (
|
|
295
|
+
<Text fg={quoteStatus.ended ? colors.warning : colors.negative} style={{ fontSize: "var(--gloom-type-ui)", lineHeight: "14px" }}>
|
|
289
296
|
{quoteStatus.text}
|
|
290
297
|
</Text>
|
|
291
298
|
)}
|
|
@@ -332,6 +339,28 @@ export function QuoteMonitorCard({
|
|
|
332
339
|
</Box>
|
|
333
340
|
</Box>
|
|
334
341
|
|
|
342
|
+
{endedRow && (
|
|
343
|
+
// Under the price, across the card, so the date is never cut on a narrow card;
|
|
344
|
+
// backed like the price so the sparkline does not run through it.
|
|
345
|
+
<Text
|
|
346
|
+
fg={colors.warning}
|
|
347
|
+
style={{
|
|
348
|
+
gridColumn: "1 / -1",
|
|
349
|
+
gridRow: "2",
|
|
350
|
+
justifySelf: "start",
|
|
351
|
+
alignSelf: "start",
|
|
352
|
+
fontSize: "var(--gloom-type-ui)",
|
|
353
|
+
lineHeight: "14px",
|
|
354
|
+
whiteSpace: "nowrap",
|
|
355
|
+
backgroundColor: colors.bg,
|
|
356
|
+
borderRadius: "var(--gloom-radius-sm)",
|
|
357
|
+
boxShadow: `0 0 0 2px ${colors.bg}`,
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
{quoteStatus.text}
|
|
361
|
+
</Text>
|
|
362
|
+
)}
|
|
363
|
+
|
|
335
364
|
{extendedParts && (
|
|
336
365
|
<ExtendedSessionLine
|
|
337
366
|
parts={extendedParts}
|
|
@@ -385,7 +414,7 @@ export function QuoteMonitorCard({
|
|
|
385
414
|
</Box>
|
|
386
415
|
<Box flexDirection="column">
|
|
387
416
|
<FigureText fg={changeColor} dim={flashing} style={figureHalo}>
|
|
388
|
-
{compactQuoteFailure ? `${priceText} · ${quoteStatus.stale ? "STALE" : "ERROR"}` : priceText}
|
|
417
|
+
{compactQuoteFailure ? `${priceText} · ${quoteStatus.ended ?? (quoteStatus.stale ? "STALE" : "ERROR")}` : priceText}
|
|
389
418
|
</FigureText>
|
|
390
419
|
<Box flexDirection="row" gap={1}>
|
|
391
420
|
<FigureText part="sub" fg={changeColor} dim={flashing} style={figureHalo}>{changePercentText}</FigureText>
|
|
@@ -432,7 +461,7 @@ export function QuoteMonitorCard({
|
|
|
432
461
|
)}
|
|
433
462
|
|
|
434
463
|
{quoteFailed && !compactQuoteFailure && (
|
|
435
|
-
<Box height={1} overflow="hidden"><Text fg={colors.negative}>{quoteStatus.text}</Text></Box>
|
|
464
|
+
<Box height={1} overflow="hidden"><Text fg={quoteStatus.ended ? colors.warning : colors.negative}>{quoteStatus.text}</Text></Box>
|
|
436
465
|
)}
|
|
437
466
|
{navAsOf && !compactNavAsOf && (
|
|
438
467
|
<Box height={1} overflow="hidden"><Text fg={colors.textDim}>{navAsOf}</Text></Box>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { Box } from "../../../ui";
|
|
3
3
|
import { usePaneSettingValue, useShortcut } from "../../../public/react";
|
|
4
|
-
import { DataTableStackView, KeyValueRow, PaneStatusBody, StatGrid, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, usePaneTicker, type DataTableColumn } from "../../../components";
|
|
4
|
+
import { splitDelayedStatus, DataTableStackView, KeyValueRow, PaneStatusBody, StatGrid, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, usePaneTicker, type DataTableColumn } from "../../../components";
|
|
5
5
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
6
6
|
import type { PaneProps } from "../../../types/plugin";
|
|
7
7
|
import type { TapeQuote, TapeSnapshot, TapeTrade } from "../../../api-client/tape";
|
|
@@ -109,7 +109,7 @@ function TimeSalesView({ width, height, focused, symbol, exchange }: PaneProps &
|
|
|
109
109
|
usePaneNoticeFooter({ registrationId: "time-sales:notices", focused, notices: [...(data?.gaps ?? []), ...(resource.transport ? [resource.transport] : []),
|
|
110
110
|
...(pinnedBeforeTape ? [`The pinned moment is before this tape's first ${tab === "quotes" ? "quote" : "print"}, ${tapeStampNewYork(rowTime(rows.at(-1)!))}.`] : [])] });
|
|
111
111
|
usePaneStatusFooter({ registrationId: "time-sales:status", loading: resource.loading, error: resource.error,
|
|
112
|
-
info: data ? [{ id: "feed", quoteStatus: true, parts: [{ text: `${data.feed === "sip" ? "real-time" : "15m delayed"} · ${tapeStampNewYork(data.asOf)}`, tone: "muted" }] },
|
|
112
|
+
info: data ? [...splitDelayedStatus({ id: "feed", quoteStatus: true, parts: [{ text: `${data.feed === "sip" ? "real-time" : "15m delayed"} · ${tapeStampNewYork(data.asOf)}`, tone: "muted" }] }),
|
|
113
113
|
...(frozen ? [{ id: "paused", parts: [{ text: "paused", tone: "warning" as const }] }] : []),
|
|
114
114
|
...(!data.connected || resource.snapshotOnly ? [{ id: "snapshot", parts: [{ text: "snapshot", tone: "warning" as const }] }] : [])] : [] });
|
|
115
115
|
const selectTab = (value: string) => { setTab(value); setDetail(null); setSelected(null); };
|