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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { Box, Text, useUiCapabilities } from "../../../ui";
|
|
3
|
-
import { usePaneSettingValue, usePaneTicker, useUpdatePaneSettings } from "../../../state/app/context";
|
|
3
|
+
import { usePaneAppConfig, usePaneSettingValue, usePaneTicker, useUpdatePaneSettings } from "../../../state/app/context";
|
|
4
4
|
import { colors, priceColor } from "../../../theme/colors";
|
|
5
5
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
6
6
|
import { formatCompact, formatPercentRaw } from "../../../utils/format";
|
|
7
7
|
import { formatMarketPrice, formatSignedMarketPrice, liveQuoteFormatOptions } from "../../../market-data/market/format";
|
|
8
|
-
import { formatExpDate, resolveOptionsTarget } from "../../../utils/options";
|
|
8
|
+
import { chainOptionContract, formatExpDate, optionContractName, resolveOptionsTarget } from "../../../utils/options";
|
|
9
9
|
import {
|
|
10
10
|
expiryIsoDate, findListedExpiry, formatDaysToExpiry, daysToExpiry, missingExpiryMessage, missingExpiryText, readOptionExpiration,
|
|
11
11
|
} from "../../../utils/option-expiry";
|
|
@@ -34,7 +34,9 @@ import { useLiveQuoteEntries, useQuoteUpdates } from "../../../state/hooks/quote
|
|
|
34
34
|
import { buildChartKey } from "../../../market-data/selectors";
|
|
35
35
|
import type { ChartRequest } from "../../../market-data/request-types";
|
|
36
36
|
import { quoteSubscriptionTargetFromTicker } from "../../../market-data/request-types";
|
|
37
|
-
import { usePluginAppActions } from "../../runtime";
|
|
37
|
+
import { usePluginAppActions, usePluginBrokerActions, usePluginTickerActions } from "../../runtime";
|
|
38
|
+
import { brokerCanTrade, openTradeTicket } from "../broker-trading/command";
|
|
39
|
+
import type { OptionContract } from "../../../types/financials";
|
|
38
40
|
import { OPTIONS_CALCULATOR_TEMPLATE_ID } from "../options-calculator/model";
|
|
39
41
|
import { buildChainCalcParams, resolveCalcSide } from "./calc-seed";
|
|
40
42
|
import { useOptionsCatalogue } from "./expiry-catalogue";
|
|
@@ -66,7 +68,7 @@ import { signedPositionDirection } from "../../../portfolio/position-metrics";
|
|
|
66
68
|
import { optionMarketReference } from "./market-reference";
|
|
67
69
|
import { useOptionsEnrichment } from "./enrichment";
|
|
68
70
|
import type { OptionsEnrichmentSnapshot } from "./enrichment-model";
|
|
69
|
-
import { optionMid, type OptionSide } from "../shared/volatility";
|
|
71
|
+
import { daysToExpiryFrom, optionMid, type OptionSide } from "../shared/volatility";
|
|
70
72
|
import type { TickerRecord } from "../../../types/ticker";
|
|
71
73
|
import type { TickerFinancials } from "../../../types/financials";
|
|
72
74
|
import type { IvStats } from "../iv-history/client";
|
|
@@ -176,6 +178,9 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
176
178
|
const ticker = useMemo(() => savedTicker ?? (boundSymbol ? transientTicker(boundSymbol, fallbackExchange, fallbackCurrency) : null),
|
|
177
179
|
[savedTicker, boundSymbol, fallbackExchange, fallbackCurrency]);
|
|
178
180
|
const { createPaneFromTemplate } = usePluginAppActions();
|
|
181
|
+
const { pinTicker } = usePluginTickerActions();
|
|
182
|
+
const { getBrokerAdapter } = usePluginBrokerActions();
|
|
183
|
+
const appConfig = usePaneAppConfig();
|
|
179
184
|
const liveStreaming = useLiveStreamingSetting();
|
|
180
185
|
// A number from the app, or the date or seconds `fn OMON --expiration` took as text.
|
|
181
186
|
const [seededSetting] = usePaneSettingValue<unknown>("expiration", undefined);
|
|
@@ -238,13 +243,20 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
238
243
|
const strikeWindowText = strikeWindowValue(strikeWindow);
|
|
239
244
|
const initialChainEntry = useOptionsQuery(baseRequest);
|
|
240
245
|
const initialChain = useResolvedEntryValue(initialChainEntry);
|
|
241
|
-
|
|
246
|
+
// A listing can still carry a date whose 16:00 ET close has passed. The chain
|
|
247
|
+
// never opens there, nor on a saved date that has since passed: it takes the nearest live one.
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
const liveExpiration = (expiration: number) => daysToExpiryFrom(expiration, now) > 0;
|
|
250
|
+
const listedExpirations = initialChain?.expirationDates ?? [];
|
|
251
|
+
const openingExpirations = listedExpirations.some(liveExpiration) ? listedExpirations.filter(liveExpiration) : listedExpirations;
|
|
252
|
+
const initialExpiration = openingExpirations.length === 0 ? undefined : openingExpirations.reduce((best, expiration) => (
|
|
242
253
|
parsed && Math.abs(expiration - parsed.expTs) < Math.abs(best - parsed.expTs) ? expiration : best
|
|
243
|
-
),
|
|
254
|
+
), openingExpirations[0]!);
|
|
244
255
|
// A typed date names a day; the catalogue's stamp for that day is the expiry.
|
|
245
256
|
const seededDate = readOptionExpiration(seededSetting);
|
|
246
|
-
const
|
|
247
|
-
|
|
257
|
+
const seededListed = seededDate == null ? undefined : findListedExpiry(seededDate, listedExpirations) ?? seededDate;
|
|
258
|
+
const seededExpiration = seededListed != null && (liveExpiration(seededListed) || !openingExpirations.some(liveExpiration))
|
|
259
|
+
? seededListed : undefined;
|
|
248
260
|
const selectedExpiration = expirationTargetKey == null || expirationTargetKey === selectionTargetKey
|
|
249
261
|
? seededExpiration ?? initialExpiration : initialExpiration;
|
|
250
262
|
const viewportKey = `${effectiveTicker}:${selectedExpiration ?? "initial"}`;
|
|
@@ -281,8 +293,11 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
281
293
|
?? (matchesSelectedExpiration(initialChain) ? initialChain : null);
|
|
282
294
|
const strikesLoading = strikeChain === null && !expirationUnavailable;
|
|
283
295
|
const expirationDates = useMemo(() => {
|
|
284
|
-
|
|
296
|
+
const listed = selectedExpirationMissing
|
|
285
297
|
? [...availableExpirations, selectedExpiration!].sort((left, right) => left - right) : availableExpirations;
|
|
298
|
+
// A date that has ended has no contracts left to show; the strip leaves it out, unless every date has.
|
|
299
|
+
const current = (expiration: number) => daysToExpiry(expiration, Date.now()) >= 0;
|
|
300
|
+
return listed.some(current) ? listed.filter((expiration) => expiration === selectedExpiration || current(expiration)) : listed;
|
|
286
301
|
}, [availableExpirations, selectedExpiration, selectedExpirationMissing]);
|
|
287
302
|
// Keep the date strip stable while a mouse press focuses this pane. A new
|
|
288
303
|
// tab list asks the web host to reveal the active tab and can move the date
|
|
@@ -468,13 +483,17 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
468
483
|
: { key: viewportKey, range }
|
|
469
484
|
));
|
|
470
485
|
}, [viewportKey]);
|
|
486
|
+
// A chain read from a broker profile has its contracts quoted by that profile too.
|
|
487
|
+
const quoteBrokerId = instrument?.brokerId, quoteBrokerInstanceId = instrument?.brokerInstanceId;
|
|
488
|
+
const optionQuoteBroker = useMemo(() => quoteBrokerId || quoteBrokerInstanceId
|
|
489
|
+
? { brokerId: quoteBrokerId, brokerInstanceId: quoteBrokerInstanceId } : undefined, [quoteBrokerId, quoteBrokerInstanceId]);
|
|
471
490
|
const optionQuoteTargets = useMemo(
|
|
472
491
|
() => buildOptionQuoteTargets(quoteRows, {
|
|
473
492
|
fallbackHeight: height,
|
|
474
493
|
selectedIndex: selectedStrikeIdx,
|
|
475
494
|
visibleRange: visibleStrikeRange,
|
|
476
|
-
}),
|
|
477
|
-
[height, quoteRows, selectedStrikeIdx, visibleStrikeRange],
|
|
495
|
+
}, optionQuoteBroker),
|
|
496
|
+
[height, quoteRows, selectedStrikeIdx, visibleStrikeRange, optionQuoteBroker],
|
|
478
497
|
);
|
|
479
498
|
const {
|
|
480
499
|
entries: optionQuoteEntries,
|
|
@@ -497,8 +516,8 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
497
516
|
// The snapshot object itself comes back while nothing streamed, so the
|
|
498
517
|
// memos below only recompute when a quote actually landed.
|
|
499
518
|
const liveChain = useMemo(
|
|
500
|
-
() => strikeChain ? overlayOptionChainQuotes(strikeChain, optionQuoteEntries, optionQuoteFreshness).chain : null,
|
|
501
|
-
[optionQuoteEntries, optionQuoteFreshness, strikeChain],
|
|
519
|
+
() => strikeChain ? overlayOptionChainQuotes(strikeChain, optionQuoteEntries, optionQuoteFreshness, optionQuoteBroker).chain : null,
|
|
520
|
+
[optionQuoteEntries, optionQuoteFreshness, strikeChain, optionQuoteBroker],
|
|
502
521
|
);
|
|
503
522
|
const callsByStrike = useMemo(
|
|
504
523
|
() => new Map(liveChain?.calls.map((c) => [c.strike, c]) ?? []),
|
|
@@ -672,6 +691,28 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
672
691
|
}, [scenarioContract, scenarioAvailable, scenarioMid, scenarioVolatility, createPaneFromTemplate, dividendYield, effectiveTicker,
|
|
673
692
|
effectiveExchange, selectedSide, spot, underlying?.quote?.lastUpdated]);
|
|
674
693
|
|
|
694
|
+
// A contract opens as its own listing: quote, chart and Trade. It stays on
|
|
695
|
+
// the chain's broker while that profile is on, else it opens by its symbol.
|
|
696
|
+
const contractListing = useCallback((contract: OptionContract) => {
|
|
697
|
+
const symbol = contract.contractSymbol.trim().toUpperCase();
|
|
698
|
+
const currency = contract.currency || underlying?.quote?.currency || "USD";
|
|
699
|
+
const brokerOn = !!instrument && appConfig.brokerInstances.some((profile) => profile.id === instrument.brokerInstanceId && profile.enabled !== false);
|
|
700
|
+
return { symbol, selection: { instrument: brokerOn ? chainOptionContract(symbol, instrument, currency) : null,
|
|
701
|
+
listing: { name: optionContractName(symbol) ?? symbol, exchange: "", currency, type: "OPT" } } };
|
|
702
|
+
}, [appConfig.brokerInstances, instrument, underlying?.quote?.currency]);
|
|
703
|
+
const openContract = useCallback((contract: OptionContract | null | undefined) => {
|
|
704
|
+
if (!contract) return;
|
|
705
|
+
const { symbol, selection } = contractListing(contract);
|
|
706
|
+
pinTicker(symbol, selection);
|
|
707
|
+
}, [contractListing, pinTicker]);
|
|
708
|
+
const tradable = !!scenarioContract && brokerCanTrade(appConfig.brokerInstances, getBrokerAdapter, scenarioContract.contractSymbol.trim().toUpperCase(), "OPT");
|
|
709
|
+
const tradeContract = useCallback(() => {
|
|
710
|
+
if (!scenarioContract || !tradable) return;
|
|
711
|
+
const { symbol, selection } = contractListing(scenarioContract);
|
|
712
|
+
// A new contract's ticket starts on Buy; b and s choose the side in it.
|
|
713
|
+
openTradeTicket({ pinTicker }, symbol, { action: "BUY" }, selection);
|
|
714
|
+
}, [contractListing, pinTicker, scenarioContract, tradable]);
|
|
715
|
+
|
|
675
716
|
const openSurface = useCallback(() => {
|
|
676
717
|
if (!ticker || selectedExpiration == null) return;
|
|
677
718
|
createPaneFromTemplate("vol-surface-pane", { symbol: ticker.metadata.ticker, ticker, instrument,
|
|
@@ -703,12 +744,14 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
703
744
|
setCalcSide(sideTarget);
|
|
704
745
|
}, [selectContract, selectedRow, selectedStrikeIdx, sideTarget]);
|
|
705
746
|
|
|
747
|
+
// With a broker that trades the contract, [t]rade opens its ticket on Buy, and Sell is chosen there.
|
|
706
748
|
const footerHints = useMemo(() => [
|
|
707
749
|
...(sideTarget ? [{ id: "side", key: SIDE_KEY, label: ` ${sideTarget}`, title: sideTarget === "put" ? "Select Put" : "Select Call", onPress: switchSide }] : []),
|
|
750
|
+
...(tradable ? [{ id: "trade-contract", key: "t", label: "rade", title: "Trade Contract", onPress: tradeContract }] : []),
|
|
708
751
|
...(calcParams ? [{ id: "calc", key: "c", label: "alc", onPress: openCalculator }] : []),
|
|
709
752
|
...(scenarioAvailable ? [{ id: "scenario", key: "a", label: "dd to OSA", onPress: openScenario }] : []),
|
|
710
753
|
...(ticker && selectedExpiration != null ? [{ id: "surface", key: "s", label: "urface", onPress: openSurface }] : []),
|
|
711
|
-
], [sideTarget, switchSide, calcParams, openCalculator, scenarioAvailable, openScenario, ticker, selectedExpiration, openSurface]);
|
|
754
|
+
], [sideTarget, switchSide, tradable, tradeContract, calcParams, openCalculator, scenarioAvailable, openScenario, ticker, selectedExpiration, openSurface]);
|
|
712
755
|
|
|
713
756
|
// Back to where the chain opens: the held contract's strike, else the money.
|
|
714
757
|
const defaultStrikeIndex = strikes.length === 0 ? -1 : (() => {
|
|
@@ -732,8 +775,10 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
732
775
|
{ id: "default-strike", label: parsed ? "Go to Held Strike" : "Go to ATM Strike",
|
|
733
776
|
enabled: defaultStrikeIndex >= 0, onSelect: goToDefaultStrike },
|
|
734
777
|
...(enrichment?.asOf && dialog ? [{ id: "analytics-as-of", label: "Analytics As Of…", onSelect: showAnalyticsAsOf }] : []),
|
|
778
|
+
{ id: "open-contract", label: "Open Contract", accelerator: "Enter", enabled: !!scenarioContract, onSelect: () => openContract(scenarioContract) },
|
|
779
|
+
...(tradable ? [{ id: "trade-contract", label: "Trade Contract", accelerator: "t", onSelect: tradeContract }] : []),
|
|
735
780
|
], [defaultStrikeIndex, dialog, enrichment?.asOf, expirationDates.length, goToDefaultStrike, parsed,
|
|
736
|
-
selectAdjacentExpiration, selectedExpirationIndex, showAnalyticsAsOf]);
|
|
781
|
+
selectAdjacentExpiration, selectedExpirationIndex, showAnalyticsAsOf, scenarioContract, openContract, tradable, tradeContract]);
|
|
737
782
|
|
|
738
783
|
const renderCell = useCallback((
|
|
739
784
|
row: OptionTableRow,
|
|
@@ -756,6 +801,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
756
801
|
}, [selectContract, selectedSide]);
|
|
757
802
|
|
|
758
803
|
useOptionsAccessFooter({
|
|
804
|
+
broker: !!optionQuoteBroker,
|
|
759
805
|
chain,
|
|
760
806
|
error: [error, underlyingStale ? "Underlying quote stale: Greeks and calculator unavailable" : null,
|
|
761
807
|
selectedContract && strikeChain && !selectedContractAvailable
|
|
@@ -789,7 +835,21 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
789
835
|
// the arrows for it; in a research tab those stay with the tab strip.
|
|
790
836
|
useShortcut((event) => {
|
|
791
837
|
if (event.defaultPrevented || event.propagationStopped || event.targetEditable) return;
|
|
792
|
-
if (event.ctrl || event.meta || event.alt
|
|
838
|
+
if (event.ctrl || event.meta || event.alt) return;
|
|
839
|
+
const surface = isPlainKey(event, "s");
|
|
840
|
+
if (surface && ticker && selectedExpiration != null) {
|
|
841
|
+
event.preventDefault();
|
|
842
|
+
event.stopPropagation();
|
|
843
|
+
openSurface();
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (event.shift) return;
|
|
847
|
+
if (tradable && isPlainKey(event, "t") && !event.repeat) {
|
|
848
|
+
event.preventDefault();
|
|
849
|
+
event.stopPropagation();
|
|
850
|
+
tradeContract();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
793
853
|
|
|
794
854
|
const expiryStep = isPlainKey(event, EXPIRY_PREVIOUS_KEY) || (!nestedInTabs && isPlainKey(event, "h", "left")) ? -1
|
|
795
855
|
: isPlainKey(event, EXPIRY_NEXT_KEY) || (!nestedInTabs && isPlainKey(event, "l", "right")) ? 1 : 0;
|
|
@@ -799,12 +859,6 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
799
859
|
selectAdjacentExpiration(expiryStep);
|
|
800
860
|
return;
|
|
801
861
|
}
|
|
802
|
-
if (isPlainKey(event, "s") && ticker && selectedExpiration != null) {
|
|
803
|
-
event.preventDefault();
|
|
804
|
-
event.stopPropagation();
|
|
805
|
-
openSurface();
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
862
|
if (isPlainKey(event, "c") && calcParams) {
|
|
809
863
|
event.preventDefault();
|
|
810
864
|
event.stopPropagation();
|
|
@@ -925,6 +979,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
925
979
|
onVisibleRangeChange={handleVisibleStrikeRangeChange}
|
|
926
980
|
getItemKey={(row) => String(row.strike)}
|
|
927
981
|
renderCell={renderCell}
|
|
982
|
+
onActivate={(row) => openContract(selectedSide === "put" ? row.put : selectedSide === "call" ? row.call : null)}
|
|
928
983
|
selectedTextOverridesCellColor
|
|
929
984
|
emptyStateTitle={unavailableExpiry ?? (error && !strikeChain ? "Selected expiration unavailable." : strikesLoading ? "Loading strikes..."
|
|
930
985
|
: strikeWindowResult.total > 0 ? "No strikes in this window." : "No strikes available.")}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
|
|
2
|
+
import { delayedFooterSegment, FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
|
|
3
3
|
import { CompositeChart } from "../../../components/chart/composite/composite-chart";
|
|
4
4
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
5
5
|
import { useAsyncResource, useInputCapture } from "../../../public/react";
|
|
@@ -279,7 +279,9 @@ export function OptionsCalculatorPane({ focused, width, height }: PaneProps) {
|
|
|
279
279
|
...(surfaceSource && surfaceResource.loading ? [{ id: "loading", parts: [{ text: "loading surface", tone: "muted" as const }] }] : []),
|
|
280
280
|
...(surfaceSource && surface?.asOf ? [{ id: "surface-asof", parts: [{ text: `surface · ${surface.asOf}`, tone: "muted" as const }] }] : []),
|
|
281
281
|
...(effectiveSteps && effectiveSteps !== (draft.steps ?? 400) ? [{ id: "refined", parts: [{ text: `tree refined to ${effectiveSteps} steps`, tone: "muted" as const }] }] : []),
|
|
282
|
-
...(liveInputs ? [
|
|
282
|
+
...(liveInputs ? [liveInputs.delayed
|
|
283
|
+
? delayedFooterSegment({ id: "market", detail: "Delayed market inputs" })
|
|
284
|
+
: { id: "market", parts: [{ text: "real-time market", tone: "muted" as const }] }] : []),
|
|
283
285
|
],
|
|
284
286
|
hints: activeFieldId ? [] : [
|
|
285
287
|
// Names the cell Enter or `e` edits, which the grid does not mark.
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Button, ChoiceDialog, confirmDialog, DataTableView, EmptyState, PageStackView, PaneStatusBody, QueryBar, StatGrid, statGridRows,
|
|
2
|
+
import { splitDelayedStatus, Button, ChoiceDialog, confirmDialog, DataTableView, EmptyState, PageStackView, PaneStatusBody, QueryBar, StatGrid, statGridRows,
|
|
3
3
|
usePaneFooter, usePaneNoticeFooter, usePaneTabs, type DataTableColumn, type SelectControl, type StatItem } from "../../../components";
|
|
4
4
|
import { useAsyncResource, useInputCapture, usePaneInstance, usePaneSettingValue, usePaneTicker,
|
|
5
5
|
usePluginAppActions, usePluginPaneState, usePluginState, useShortcut } from "../../../public/react";
|
|
6
|
+
import { usePaneAppConfig } from "../../../state/app/context";
|
|
7
|
+
import { usePluginBrokerActions, usePluginTickerActions } from "../../runtime";
|
|
8
|
+
import { brokerCanTrade, brokerCanTradeStrategy, openTradeTicket } from "../broker-trading/command";
|
|
9
|
+
import { MAX_STRATEGY_LEGS, strategyFromLegs } from "../broker-trading/strategy";
|
|
10
|
+
import { optionContractName } from "../../../utils/options";
|
|
6
11
|
import { Box } from "../../../ui";
|
|
7
12
|
import { useDialogState } from "../../../ui/dialog";
|
|
8
13
|
import { useDialog, type PromptContext } from "../../../ui/dialog";
|
|
@@ -72,13 +77,16 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
72
77
|
const [consumedSeed, setConsumedSeed] = usePluginPaneState<string | null>("consumedSeed", null);
|
|
73
78
|
const [localError, setLocalError] = useState<string | null>(null);
|
|
74
79
|
const controller = useRef<AbortController | null>(null);
|
|
80
|
+
const brokerInstrument = target?.instrument ?? null;
|
|
75
81
|
const explicitSpot = Number(settings.spot);
|
|
76
82
|
const loadMarket = useCallback(async (force: boolean) => {
|
|
77
83
|
controller.current?.abort();
|
|
78
84
|
const abort = new AbortController(); controller.current = abort;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
// The chain OMON reads for this ticker, from its broker when it is linked to one: one chain, one source, one cache entry.
|
|
86
|
+
return loadScenarioMarket({ instrument: { symbol: underlying, exchange, brokerId: brokerInstrument?.brokerId,
|
|
87
|
+
brokerInstanceId: brokerInstrument?.brokerInstanceId, instrument: brokerInstrument },
|
|
88
|
+
expiration: expiration ?? undefined, forceRefresh: force, signal: abort.signal });
|
|
89
|
+
}, [underlying, exchange, expiration, brokerInstrument]);
|
|
82
90
|
// Fully specified calculations and screenshot snapshots never need a quote refresh.
|
|
83
91
|
const needsMarket = !frozen && (!["spot", "rate", "dividendYield"].every((key) => settings[key] != null && settings[key] !== "") || detail === "chain");
|
|
84
92
|
const resource = useAsyncResource(underlying && needsMarket ? loadMarket : null);
|
|
@@ -240,6 +248,32 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
240
248
|
};
|
|
241
249
|
// Freezing keeps the marks shown at this moment as the scenario's inputs.
|
|
242
250
|
const freeze = () => { if (live) setPosition(live.position); setFollow(false); };
|
|
251
|
+
// With a broker that takes them, the position trades as one order: a
|
|
252
|
+
// spread on the underlying's Trade tab, or one contract's own ticket. The
|
|
253
|
+
// live mids, else the entry prices, say whether it opens for a debit or a credit.
|
|
254
|
+
const { pinTicker } = usePluginTickerActions();
|
|
255
|
+
const { getBrokerAdapter } = usePluginBrokerActions();
|
|
256
|
+
const { brokerInstances } = usePaneAppConfig();
|
|
257
|
+
const tradeIntent = useMemo(() => {
|
|
258
|
+
const legs = position?.legs ?? [];
|
|
259
|
+
if (frozen || !underlying || !legs.length || legs.length > MAX_STRATEGY_LEGS || legs.some((leg) => leg.multiplier !== 100 || !legSymbols.get(leg.id))) return null;
|
|
260
|
+
const intent = strategyFromLegs(legs.map((leg) => {
|
|
261
|
+
const quote = live?.quotes.get(leg.id);
|
|
262
|
+
return { contractSymbol: legSymbols.get(leg.id)!, quantity: leg.quantity, price: quote ? optionMid(quote) ?? leg.price : leg.price };
|
|
263
|
+
}));
|
|
264
|
+
if (!intent) return null;
|
|
265
|
+
const tradable = intent.legs.length === 1 ? brokerCanTrade(brokerInstances, getBrokerAdapter, intent.legs[0]!.contractSymbol, "OPT")
|
|
266
|
+
: brokerCanTradeStrategy(brokerInstances, getBrokerAdapter, underlying, intent.legs.length);
|
|
267
|
+
return tradable ? intent : null;
|
|
268
|
+
}, [frozen, underlying, position?.legs, legSymbols, live?.quotes, brokerInstances, getBrokerAdapter]);
|
|
269
|
+
const trade = () => {
|
|
270
|
+
if (!tradeIntent) return;
|
|
271
|
+
const [only] = tradeIntent.legs;
|
|
272
|
+
if (tradeIntent.legs.length === 1 && only) {
|
|
273
|
+
openTradeTicket({ pinTicker }, only.contractSymbol, { action: only.action, quantity: tradeIntent.quantity * only.ratio },
|
|
274
|
+
{ instrument: null, listing: { name: optionContractName(only.contractSymbol) ?? only.contractSymbol, exchange: "", currency: position?.currency ?? "USD", type: "OPT" } });
|
|
275
|
+
} else openTradeTicket({ pinTicker }, symbol ?? underlying, tradeIntent);
|
|
276
|
+
};
|
|
243
277
|
const hints = detail === "chain" ? [{ id: "expiry", key: "d", label: "ate", onPress: () => chainExpiryControl.current?.open() }] : detail || volActive ? [] : [
|
|
244
278
|
{ id: "add", key: "a", label: "dd leg", onPress: addTyped },
|
|
245
279
|
{ id: "chain", key: "c", label: "hain", onPress: () => setDetail("chain") },
|
|
@@ -250,6 +284,7 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
250
284
|
{ id: "vol", key: "v", label: "ol shift", onPress: () => setVolActive(true) },
|
|
251
285
|
{ id: "save", key: "s", label: "ave", onPress: () => setDetail("save") }] : []),
|
|
252
286
|
{ id: "load", key: "b", label: "rowse saved", onPress: () => void loadSaved() },
|
|
287
|
+
...(tradeIntent ? [{ id: "trade", key: "t", label: "rade", onPress: trade }] : []),
|
|
253
288
|
...(live ? [{ id: "follow", key: "f", label: "reeze", onPress: freeze }]
|
|
254
289
|
: follow || frozen || frozenMarket || !storedPosition ? [] : [{ id: "follow", key: "f", label: "ollow market", onPress: () => setFollow(true) }]),
|
|
255
290
|
];
|
|
@@ -270,8 +305,8 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
270
305
|
...(resource.loading ? [{ id: "loading", parts: [{ text: "loading chain", tone: "muted" as const }] }] : []),
|
|
271
306
|
...(error ? [{ id: "error", parts: [{ text: error, tone: "warning" as const }] }] : []),
|
|
272
307
|
...(hedge.error ? [{ id: "hedge-error", parts: [{ text: hedge.error, tone: "warning" as const }] }] : []),
|
|
273
|
-
...(live ?
|
|
274
|
-
variants: [[{ text: shortSpotText, tone: "muted"
|
|
308
|
+
...(live ? splitDelayedStatus({ id: "asof", parts: [{ text: `${live.basis} · ${spotText} · ${new Date(live.observedAt).toISOString().slice(11, 19)} UTC`, tone: "muted" }],
|
|
309
|
+
variants: [[{ text: shortSpotText, tone: "muted" }]] })
|
|
275
310
|
: position ? [{ id: "asof", parts: [{ text: `${spotText} · ${formatSpotTime(position.asOf)} · ${market?.source ? "market" : "input assumptions"}${midVolatility}`, tone: "muted" as const }],
|
|
276
311
|
variants: [[{ text: shortSpotText, tone: "muted" as const }]] }] : []),
|
|
277
312
|
], hints }), [hints, position, market?.source, resource.loading, error, hedge.error, live?.basis, live?.observedAt, midVolatility, spotText, shortSpotText]);
|
|
@@ -7,6 +7,8 @@ import { isFiniteNumber } from "../../../utils/guards";
|
|
|
7
7
|
export interface PortfolioAccountMetrics {
|
|
8
8
|
dailyPnl: number;
|
|
9
9
|
dailyPnlPct: number;
|
|
10
|
+
/** The day is the broker's own, measured from its account's prior close rather than each quote's. */
|
|
11
|
+
brokerDay?: true;
|
|
10
12
|
unrealizedPnl: number;
|
|
11
13
|
unrealizedPnlPct: number;
|
|
12
14
|
realizedPnl?: number;
|
|
@@ -196,6 +198,7 @@ export function resolvePortfolioAccountMetrics(
|
|
|
196
198
|
return {
|
|
197
199
|
dailyPnl,
|
|
198
200
|
dailyPnlPct,
|
|
201
|
+
...(brokerDailyPnl != null ? { brokerDay: true as const } : {}),
|
|
199
202
|
unrealizedPnl,
|
|
200
203
|
unrealizedPnlPct,
|
|
201
204
|
realizedPnl: isFiniteNumber(account?.realizedPnl) ? convertAccountValue(account.realizedPnl) : undefined,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { formatCompact, formatPercentRaw } from "../../../../utils/format";
|
|
13
13
|
import { currencyPriceMinDigits, formatMarketCostWithCurrency, formatMarketPrice, formatMarketPriceWithCurrency, formatMarketQuantity, quoteFormatOptions } from "../../../../market-data/market/format";
|
|
14
14
|
import { resolvePriceBasis } from "../../../../market-data/market/price-basis";
|
|
15
|
+
import { combinedChangeBasisLabel, quoteChangeBasis, type QuoteChangeBasis } from "../../../../market-data/market/change-basis";
|
|
15
16
|
import { getPortfolioPositionMetrics, lotCostAtTodaysRate, lotCostFxRate, resolvePortfolioMarketValue, resolvePortfolioPositionPnl } from "../../../../portfolio/position-metrics";
|
|
16
17
|
import {
|
|
17
18
|
getPortfolioValuationDisplay,
|
|
@@ -56,6 +57,7 @@ import { loadPerpetualMarket } from "../../../../sources/gloomberb-cloud/perpetu
|
|
|
56
57
|
import { listedMarketCapitalization, selectMarketCapitalization } from "../../../../utils/market-capitalization";
|
|
57
58
|
import { chunked, mapWithConcurrency } from "../../../../utils/concurrency";
|
|
58
59
|
import { liveMarketCapitalization } from "../../../../market-data/quotes/live-valuation";
|
|
60
|
+
import { delistingLabel } from "../../../../market-data/quotes/delisting";
|
|
59
61
|
import { instrumentFromTicker } from "../../../../market-data/request-types";
|
|
60
62
|
import { toMarketDataContext } from "../../../../market-data/selectors";
|
|
61
63
|
import { cliFreshnessFooter } from "../../../../cli/result";
|
|
@@ -485,29 +487,46 @@ export async function valuePortfolioPositions({
|
|
|
485
487
|
/**
|
|
486
488
|
* The holdings' move over the day of the prices they are valued at, in the
|
|
487
489
|
* portfolio's currency, and that move against their value at the close
|
|
488
|
-
* before
|
|
490
|
+
* before, with what it is measured from as the pane's Day says it ("vs Fri 16
|
|
491
|
+
* Oct close", "vs mixed closes"). Null when any priced holding's move is
|
|
492
|
+
* unknown, as the pane's Day is.
|
|
489
493
|
*/
|
|
490
|
-
export function portfolioDayMove(
|
|
494
|
+
export function portfolioDayMove(
|
|
495
|
+
valuation: Pick<PortfolioValuation, "positions">,
|
|
496
|
+
now = Date.now(),
|
|
497
|
+
): { change: number; changePercent: number | null; basis: string | null } | null {
|
|
491
498
|
let change = 0;
|
|
492
499
|
let previousValue = 0;
|
|
500
|
+
const bases: Array<QuoteChangeBasis | null> = [];
|
|
493
501
|
for (const entry of valuation.positions) {
|
|
494
502
|
if (!entry.position) continue;
|
|
495
503
|
if (!entry.dayMove) return null;
|
|
496
504
|
change += entry.dayMove.change;
|
|
497
505
|
previousValue += entry.dayMove.previousValue;
|
|
506
|
+
bases.push(quoteChangeBasis(entry.quote, { terms: entry.futures }));
|
|
498
507
|
}
|
|
499
|
-
return {
|
|
508
|
+
return {
|
|
509
|
+
change,
|
|
510
|
+
changePercent: previousValue !== 0 ? (change / previousValue) * 100 : null,
|
|
511
|
+
basis: combinedChangeBasisLabel(bases, now),
|
|
512
|
+
};
|
|
500
513
|
}
|
|
501
514
|
|
|
502
|
-
/**
|
|
515
|
+
/**
|
|
516
|
+
* `Day's move: +$1,165.49 (+0.51%) vs Fri 16 Oct close`, the line a book
|
|
517
|
+
* valued at the close carries; `color` tints the figures by sign, `muted` the
|
|
518
|
+
* basis.
|
|
519
|
+
*/
|
|
503
520
|
export function dayMoveLine(
|
|
504
521
|
move: ReturnType<typeof portfolioDayMove>,
|
|
505
522
|
currency: string,
|
|
506
523
|
color: (text: string, value: number) => string = (text) => text,
|
|
524
|
+
muted: (text: string) => string = (text) => text,
|
|
507
525
|
): string {
|
|
508
526
|
if (!move) return "Day's move: unavailable";
|
|
509
527
|
const percent = move.changePercent != null ? ` (${formatPercentRaw(move.changePercent)})` : "";
|
|
510
|
-
|
|
528
|
+
const basis = move.basis ? ` ${muted(move.basis)}` : "";
|
|
529
|
+
return `Day's move: ${color(`${formatAllocationMoney(move.change, currency, { signed: true })}${percent}`, move.change)}${basis}`;
|
|
511
530
|
}
|
|
512
531
|
|
|
513
532
|
/**
|
|
@@ -898,7 +917,7 @@ async function showCollectionWithMarketData(
|
|
|
898
917
|
total: allocation.total, cash: cashExport, targetSum: allocation.targetSum,
|
|
899
918
|
// Only on a book that holds a short: its weights are shares of `weightBase`, its gross exposure, not of the total.
|
|
900
919
|
...(allocation.weightBasis === "gross" ? { weightBasis: allocation.weightBasis, weightBase: allocation.weightBase } : {}),
|
|
901
|
-
...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null } : {}),
|
|
920
|
+
...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null, dayChangeBasis: dayMove?.basis ?? null } : {}),
|
|
902
921
|
unpricedCount: allocation.unpriced.length, unpricedSymbols: allocation.unpriced,
|
|
903
922
|
totalUnrealizedPnl: totalPnlValue,
|
|
904
923
|
annualIncome: totalIncome, knownAnnualIncome: income.knownIncome, portfolioYield,
|
|
@@ -1043,7 +1062,7 @@ async function showCollectionWithMarketData(
|
|
|
1043
1062
|
rows,
|
|
1044
1063
|
));
|
|
1045
1064
|
console.log("");
|
|
1046
|
-
if (basis === "regular-close") console.log(dayMoveLine(dayMove, currency, colorBySign));
|
|
1065
|
+
if (basis === "regular-close") console.log(dayMoveLine(dayMove, currency, colorBySign, cliStyles.muted));
|
|
1047
1066
|
if (valued.some((entry) => entry.position)) console.log(exposureLine(exposure, currency));
|
|
1048
1067
|
const notes: string[] = [];
|
|
1049
1068
|
const weightNote = weightBasisLine(allocation, currency);
|
|
@@ -1136,13 +1155,15 @@ async function showCollectionWithMarketData(
|
|
|
1136
1155
|
const priceText = quote && headline
|
|
1137
1156
|
? colorBySign(formatListPrice(headline.price, quote, ticker.metadata.assetCategory), headline.change)
|
|
1138
1157
|
: "—";
|
|
1139
|
-
|
|
1158
|
+
// A listing that no longer trades has no day's move: its last session's is years old.
|
|
1159
|
+
const changeText = quote?.delisting ? "—" : headline ? colorBySign(formatPercentRaw(headline.changePercent), headline.change) : "—";
|
|
1160
|
+
const staleText = quote?.delisting ? delistingLabel(quote.delisting) : lastTrade ? `STALE ${lastTrade}` : "STALE";
|
|
1140
1161
|
return [
|
|
1141
1162
|
listingLabel(ticker),
|
|
1142
1163
|
venue ?? "—",
|
|
1143
1164
|
priceText,
|
|
1144
1165
|
changeText,
|
|
1145
|
-
...stale.length > 0 ? [rowStale ? cliStyles.warning(
|
|
1166
|
+
...stale.length > 0 ? [rowStale ? cliStyles.warning(staleText) : ""] : [],
|
|
1146
1167
|
...extendedCells(extendedSessions, extended, quote, ticker.metadata.assetCategory),
|
|
1147
1168
|
...showFunding ? [funding == null ? "—" : colorBySign(formatPerpetualRate(funding, 4), funding)] : [],
|
|
1148
1169
|
marketCap == null ? "—" : `${formatCompact(marketCap)} ${currency}`,
|
|
@@ -1168,7 +1189,8 @@ async function showCollectionWithMarketData(
|
|
|
1168
1189
|
if (notes.length > 0) console.log(["", ...notes.map((note) => cliStyles.muted(note))].join("\n"));
|
|
1169
1190
|
// The closing line counts the stale rows; the line under it names them.
|
|
1170
1191
|
if (stale.length > 0) {
|
|
1171
|
-
staleNote = `Stale: ${namedList(stale.map(({ ticker, lastTrade }) => `${listingLabel(ticker)}${
|
|
1192
|
+
staleNote = `Stale: ${namedList(stale.map(({ ticker, quote, lastTrade }) => `${listingLabel(ticker)}${quote?.delisting ? ` (${delistingLabel(quote.delisting)})`
|
|
1193
|
+
: lastTrade ? ` (last trade ${lastTrade})` : ""}`))}.`;
|
|
1172
1194
|
}
|
|
1173
1195
|
}
|
|
1174
1196
|
if (freshness) console.log(`\n${cliFreshnessFooter(freshness)}${staleNote ? `\n${cliStyles.muted(staleNote)}` : ""}`);
|
|
@@ -1304,6 +1326,8 @@ async function valueWatchlistRows({
|
|
|
1304
1326
|
// A stale quote (as the source line counts it) and the exchange-local date it last traded.
|
|
1305
1327
|
stale,
|
|
1306
1328
|
lastTradeDate: lastTrade,
|
|
1329
|
+
// A listing that no longer trades: its status and last session.
|
|
1330
|
+
...quote?.delisting ? { delisting: quote.delisting } : {},
|
|
1307
1331
|
},
|
|
1308
1332
|
});
|
|
1309
1333
|
}
|
|
@@ -444,7 +444,7 @@ async function portfolioHoldings(
|
|
|
444
444
|
cash: allocation.cash ? finite(allocation.cash.value) : null,
|
|
445
445
|
total: allocation.total,
|
|
446
446
|
// At the regular close, the holdings' move from the close before.
|
|
447
|
-
...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null } : {}),
|
|
447
|
+
...(basis === "regular-close" ? { dayChange: dayMove?.change ?? null, dayChangePercent: dayMove?.changePercent ?? null, dayChangeBasis: dayMove?.basis ?? null } : {}),
|
|
448
448
|
},
|
|
449
449
|
// Only on a book that holds a short: its weights are shares of `weightBase`, its gross exposure, not of the total.
|
|
450
450
|
...(allocation.weightBasis === "gross" ? { weightBasis: allocation.weightBasis, weightBase: allocation.weightBase } : {}),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isStablecoinCurrency } from "../../../../utils/currency-units";
|
|
1
|
+
import { heldCurrencyUnit, isStablecoinCurrency } from "../../../../utils/currency-units";
|
|
2
|
+
import { isIndexLevel } from "../../../../market-data/market/index-level";
|
|
2
3
|
import { createRowValueCache } from "../../../../components/ui/row-value-cache";
|
|
3
4
|
import { PRICE_SPARKLINE_COLUMN_ID } from "../../../../components/price-sparkline/view";
|
|
4
5
|
import type { AppConfig, ColumnConfig } from "../../../../types/config";
|
|
@@ -196,6 +197,27 @@ export function buildTrackedCurrencies(
|
|
|
196
197
|
return [...currencies].filter((currency) => !isStablecoinCurrency(currency));
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Whether a row is priced in another currency than the collection's totals
|
|
202
|
+
* (RY in CAD beside JNJ in USD, or a US stock in a CAD portfolio), so its
|
|
203
|
+
* prices need their currency beside them. Pence and cents count as their
|
|
204
|
+
* currency, whose sign the price already carries; an index has none.
|
|
205
|
+
*/
|
|
206
|
+
export function holdsOtherCurrencies(
|
|
207
|
+
tickers: readonly TickerRecord[],
|
|
208
|
+
financialsMap: ReadonlyMap<string, TickerFinancials>,
|
|
209
|
+
totalsCurrency: string,
|
|
210
|
+
): boolean {
|
|
211
|
+
const totals = heldCurrencyUnit(totalsCurrency).currency;
|
|
212
|
+
return tickers.some((ticker) => {
|
|
213
|
+
const quote = financialsMap.get(ticker.metadata.ticker)?.quote;
|
|
214
|
+
if (isIndexLevel(quote?.instrumentType || ticker.metadata.assetCategory)) return false;
|
|
215
|
+
const currency = heldCurrencyUnit(ticker.metadata.positions.find((position) => position.currency)?.currency
|
|
216
|
+
|| quote?.currency || ticker.metadata.currency).currency;
|
|
217
|
+
return !!currency && currency !== totals;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
199
221
|
export function getCollectionTypeFromConfig(config: AppConfig, collectionId: string | null): "portfolio" | "watchlist" | null {
|
|
200
222
|
if (!collectionId) return null;
|
|
201
223
|
if (config.portfolios.some((portfolio) => portfolio.id === collectionId)) return "portfolio";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Box } from "../../../../ui";
|
|
1
|
+
import { Box, useUiCapabilities } from "../../../../ui";
|
|
2
|
+
import { brokerPortfolioLogo } from "../../../../brokers/logos";
|
|
3
|
+
import { LogoMark } from "../../../../components/ui/logo-mark";
|
|
2
4
|
import { describeFundamentalMarketCap } from "../../../../utils/market-capitalization";
|
|
3
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
6
|
import {
|
|
@@ -69,7 +71,7 @@ import { PortfolioGrid } from "../grid";
|
|
|
69
71
|
import { useThrottledCursorSymbol } from "../use-throttled-cursor-symbol";
|
|
70
72
|
import { useCursorNeighborPrefetch } from "../use-cursor-neighbor-prefetch";
|
|
71
73
|
import { getSharedRegistry } from "../../../registry";
|
|
72
|
-
import { usePluginAppActions } from "../../../runtime";
|
|
74
|
+
import { usePluginAppActions, usePluginBrokerActions } from "../../../runtime";
|
|
73
75
|
import { isManualPortfolio } from "../../../../portfolio/mutations";
|
|
74
76
|
import {
|
|
75
77
|
cursorAfterRemoval,
|
|
@@ -78,8 +80,10 @@ import {
|
|
|
78
80
|
tickerRemovalSummary,
|
|
79
81
|
} from "./remove-ticker";
|
|
80
82
|
import { QuickAddTickerInput, type QuickAddCollectionKind } from "../quick-add";
|
|
83
|
+
import { delistingLabel } from "../../../../market-data/quotes/delisting";
|
|
81
84
|
import {
|
|
82
85
|
buildTrackedCurrencies,
|
|
86
|
+
holdsOtherCurrencies,
|
|
83
87
|
getCollectionTickersFromConfig,
|
|
84
88
|
getCollectionTypeFromConfig,
|
|
85
89
|
resolveVisibleWarmupRequirements,
|
|
@@ -196,9 +200,16 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
196
200
|
// The positions stay as last synced while the broker is away, dated, with one key to bring it back.
|
|
197
201
|
const offlineBrokerFooter = useOfflineBrokerFooter(brokerPortfolio ? offline : null);
|
|
198
202
|
const portfolioTargets = currentPortfolio?.targetWeights;
|
|
203
|
+
// Totals, money columns and account values are in the portfolio's currency.
|
|
204
|
+
const totalsCurrency = useMemo(
|
|
205
|
+
() => resolvePortfolioTotalsCurrency(currentPortfolio, config.baseCurrency),
|
|
206
|
+
[config.baseCurrency, currentPortfolio],
|
|
207
|
+
);
|
|
208
|
+
// A boolean, so a quote tick does not rebuild the columns.
|
|
209
|
+
const otherCurrencies = holdsOtherCurrencies(tickers, financialsMap, totalsCurrency);
|
|
199
210
|
const columns = useMemo(
|
|
200
|
-
() => resolveVisibleColumns(paneSettings.columnIds, isPortfolioTab, { targetWeights: hasTargetWeights(currentPortfolio) }),
|
|
201
|
-
[currentPortfolio, isPortfolioTab, paneSettings.columnIds],
|
|
211
|
+
() => resolveVisibleColumns(paneSettings.columnIds, isPortfolioTab, { targetWeights: hasTargetWeights(currentPortfolio), otherCurrencies }),
|
|
212
|
+
[currentPortfolio, isPortfolioTab, otherCurrencies, paneSettings.columnIds],
|
|
202
213
|
);
|
|
203
214
|
const supplementalData = usePortfolioSupplementalData(tickers, columns, appActive, financialsMap);
|
|
204
215
|
// One lookup for the whole list, only while a perpetual column is on screen.
|
|
@@ -210,11 +221,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
210
221
|
[columns],
|
|
211
222
|
);
|
|
212
223
|
|
|
213
|
-
// Totals, money columns and account values are in the portfolio's currency.
|
|
214
|
-
const totalsCurrency = useMemo(
|
|
215
|
-
() => resolvePortfolioTotalsCurrency(currentPortfolio, config.baseCurrency),
|
|
216
|
-
[config.baseCurrency, currentPortfolio],
|
|
217
|
-
);
|
|
218
224
|
// The broker account's cash when it reports one, else the cash entered by hand.
|
|
219
225
|
const cashLine = useMemo(
|
|
220
226
|
() => isPortfolioTab ? resolvePortfolioCash(currentPortfolio, accountState?.account) : null,
|
|
@@ -420,10 +426,20 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
420
426
|
cancelPendingCursorSymbol();
|
|
421
427
|
setCurrentCollectionId(collectionId);
|
|
422
428
|
}, [cancelPendingCursorSymbol, setCurrentCollectionId]);
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
429
|
+
const { getBrokerAdapter, getBrokerLogo } = usePluginBrokerActions();
|
|
430
|
+
const desktop = useUiCapabilities().nativePaneChrome === true;
|
|
431
|
+
const collectionTabs = useMemo(() => visibleCollections.map((collection) => {
|
|
432
|
+
const portfolio = desktop && collection.kind === "portfolio"
|
|
433
|
+
? config.portfolios.find((entry) => entry.id === collection.id)
|
|
434
|
+
: undefined;
|
|
435
|
+
// A broker account's tab leads with its broker's logo on the desktop and the web.
|
|
436
|
+
const logo = portfolio ? brokerPortfolioLogo(portfolio, config.brokerInstances, getBrokerAdapter, getBrokerLogo) : null;
|
|
437
|
+
return {
|
|
438
|
+
label: collection.name,
|
|
439
|
+
value: collection.id,
|
|
440
|
+
...(logo ? { leading: <LogoMark src={logo.src} name={logo.name} /> } : {}),
|
|
441
|
+
};
|
|
442
|
+
}), [config.brokerInstances, config.portfolios, desktop, getBrokerAdapter, getBrokerLogo, visibleCollections]);
|
|
427
443
|
const { strip: tabStrip, rows: headerHeight } = usePaneTabs(showCollectionTabs
|
|
428
444
|
? {
|
|
429
445
|
tabs: collectionTabs,
|
|
@@ -716,9 +732,12 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
716
732
|
? `${cursorSymbol} market cap: ${describeFundamentalMarketCap(selectedCap.provenance)}.` : undefined;
|
|
717
733
|
const multiplierNotice = isPortfolioTab && selectedTicker && hasUnknownOptionMultiplier(selectedTicker, activeCollectionId)
|
|
718
734
|
? `${cursorSymbol} has no contract multiplier; its value assumes 1x.` : undefined;
|
|
735
|
+
// The row's change cells say DELISTED; the notice gives its last session.
|
|
736
|
+
const selectedDelisting = selectedFinancials?.quote?.delisting;
|
|
737
|
+
const delistedNotice = selectedDelisting ? `${cursorSymbol}: ${delistingLabel(selectedDelisting)}.` : undefined;
|
|
719
738
|
usePaneNoticeFooter({
|
|
720
739
|
registrationId: "portfolio-list-notices",
|
|
721
|
-
notices: [...summaryNotices, capNotice, multiplierNotice].filter((notice): notice is string => !!notice),
|
|
740
|
+
notices: [...summaryNotices, delistedNotice, capNotice, multiplierNotice].filter((notice): notice is string => !!notice),
|
|
722
741
|
focused: focused && !quickAddFocused,
|
|
723
742
|
});
|
|
724
743
|
const contentHeight = Math.max(1, height - headerHeight - summaryHeight - drawerHeight - quickAddHeight);
|