gloomberb 0.18.0 → 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/market-movers.ts +7 -0
- package/src/api-client/types.ts +14 -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/fred-series.ts +15 -5
- package/src/cli/commands/market.ts +290 -159
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/overview.ts +31 -6
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +105 -52
- 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 +31 -7
- 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/symbol-list.ts +51 -16
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +22 -7
- 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 +64 -16
- package/src/components/chart/composite/price-axis-labels.tsx +2 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/command-bar/workflow/collection-actions.ts +4 -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/onboarding/wizard-positions.ts +8 -0
- 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 +57 -22
- 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/analytics/index.tsx +1 -1
- package/src/plugins/builtin/analytics/risk-client.ts +44 -11
- package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
- package/src/plugins/builtin/analytics/risk-model.ts +79 -16
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
- 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/cloud/team/pane-sections.tsx +10 -3
- package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
- 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/custom-view/loader.ts +3 -2
- package/src/plugins/builtin/custom-view/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
- 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 +38 -2
- package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
- package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
- 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/executives/data.ts +24 -8
- package/src/plugins/builtin/executives/index.tsx +2 -18
- package/src/plugins/builtin/executives/pane.tsx +34 -10
- 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-movers/client.ts +2 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -3
- package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
- package/src/plugins/builtin/market-movers/session.ts +25 -10
- 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 +33 -13
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
- package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
- package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
- package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/notes/cli.ts +33 -8
- package/src/plugins/builtin/notes/export.ts +49 -3
- package/src/plugins/builtin/notes/files.ts +14 -3
- package/src/plugins/builtin/notes/index.tsx +3 -3
- package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
- package/src/plugins/builtin/notes/store.ts +13 -3
- 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/bulk-symbols.ts +8 -7
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
- package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
- 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/broker-reconnect.ts +105 -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-feature-wall.tsx +9 -4
- 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/thirteenf/api.ts +4 -1
- package/src/plugins/builtin/thirteenf/data.ts +5 -1
- package/src/plugins/builtin/thirteenf/headless.ts +3 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
- 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 +61 -0
- package/src/portfolio/mutations.ts +16 -2
- package/src/portfolio/position-metrics.ts +45 -4
- package/src/portfolio/position-quantity.ts +43 -0
- 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/fred-series.ts +27 -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 +63 -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/team/collections.ts +2 -0
- package/src/tickers/listing-fate.ts +27 -8
- package/src/tickers/metadata.ts +10 -0
- 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/ticker.ts +9 -0
- 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 +41 -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
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { wholeNumberStep } from "../../../components/ui/number-step";
|
|
2
|
+
import { FigureText } from "../../../components/ui/figure";
|
|
3
|
+
import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
|
|
4
|
+
import { Badge, Button, Checkbox, FieldLabel, Icon, IconButton, Notice, NumberField, SegmentedControl, SelectButton, TextField, useFieldRing, usePaneFooter, usePaneMenuItems, type PaneHint, type SelectControl } from "../../../components";
|
|
5
|
+
import { useShortcut } from "../../../react/input";
|
|
6
|
+
import { afterLayout, revealInScrollBox } from "../../../components/ui/reveal-in-scroll-box";
|
|
7
|
+
import { useThemeColors } from "../../../theme/theme-context";
|
|
8
|
+
import type { Quote } from "../../../types/financials";
|
|
9
|
+
import { formatPercentRaw } from "../../../utils/format";
|
|
10
|
+
import type { BrokerAccount, BrokerOrder, BrokerOrderPreview, BrokerOrderRequest, BrokerTradingCapabilities } from "../../../types/trading";
|
|
11
|
+
import { Box, ScrollBox, Text, TextAttributes, useUiCapabilities, type BoxRenderable, type ScrollBoxRenderable } from "../../../ui";
|
|
12
|
+
import { useDialogState } from "../../../ui/dialog";
|
|
13
|
+
import { BrokerAccountPicker, type BrokerAccountChoice } from "./account-picker";
|
|
14
|
+
import { TicketLegs, TicketSection, TicketOrderHeadline, TicketOrderSummary, TicketReceiptRow, TicketStatus, ticketMoney as money, ticketNumber as number, ticketTifName, ticketTypeName as typeName } from "./ticket-parts";
|
|
15
|
+
import { brokerOrderDone, orderEstimatePrice, ticketEditingPriceStep } from "./orders-data";
|
|
16
|
+
|
|
17
|
+
export type TicketAction = "enable" | "review" | "submit" | "back" | "confirm" | "refresh" | "orders" | "new" | "cancel" | "all" | "quantity25" | "quantity50" | "priceBid" | "priceMid" | "priceAsk" | "priceLast";
|
|
18
|
+
export type TicketField = "action" | "orderType" | "quantity" | "limitPrice" | "stopPrice" | "tif" | "outsideRth" | "typedConfirmation" | "confirmOrders";
|
|
19
|
+
export interface TicketViewModel {
|
|
20
|
+
brokerName: string; symbol: string; accounts: BrokerAccount[]; accountId?: string;
|
|
21
|
+
accountChoices?: readonly BrokerAccountChoice[]; accountChoiceValue?: string;
|
|
22
|
+
quote?: Quote; quoteData?: "realtime" | "delayed";
|
|
23
|
+
/** A spread's legs' quotes, in leg order. `quote` is then the spread's own. */
|
|
24
|
+
legQuotes?: Quote[];
|
|
25
|
+
phase: "enable" | "editing" | "previewing" | "review" | "submitting" | "result" | "cancel-review" | "cancelling";
|
|
26
|
+
draft?: BrokerOrderRequest; preview?: BrokerOrderPreview; warnings: string[]; result?: BrokerOrder; error?: string;
|
|
27
|
+
resultQuantities?: "confirmed" | "unconfirmed" | "last-known";
|
|
28
|
+
position?: number; avgCost?: number; positionPnl?: number; accountType?: string;
|
|
29
|
+
tradingEnabled: boolean; typedConfirmation?: string; synthetic?: boolean; connected: boolean;
|
|
30
|
+
/** The profile is reconnecting: it keeps its accounts and says so instead of "disconnected". */
|
|
31
|
+
reconnecting?: boolean;
|
|
32
|
+
capabilities?: BrokerTradingCapabilities; modifying?: boolean; defaultPriceLabel?: string; now?: number;
|
|
33
|
+
/** The working order being replaced, so its review says what changes. */
|
|
34
|
+
original?: BrokerOrder;
|
|
35
|
+
initialFocus?: string; focusRevision?: number;
|
|
36
|
+
confirmOrders?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface BrokerTicketViewProps {
|
|
39
|
+
model: TicketViewModel; width: number; height: number; focused: boolean;
|
|
40
|
+
onEdit(field: TicketField, value: string | boolean): void;
|
|
41
|
+
onAction(action: TicketAction): void;
|
|
42
|
+
onAccountChange(accountId: string): void;
|
|
43
|
+
onCapture?(capturing: boolean): void;
|
|
44
|
+
docked?: boolean;
|
|
45
|
+
/** The open orders are busy: the ticket waits. */
|
|
46
|
+
blocked?: boolean;
|
|
47
|
+
/** They are asking a question (cancel this order?), so nothing is pending yet and the ticket says nothing. */
|
|
48
|
+
asking?: boolean;
|
|
49
|
+
onClose?(): void;
|
|
50
|
+
/** Terminal content measurement; the DOM uses intrinsic flex layout. */
|
|
51
|
+
onContentHeight?(height: number): void;
|
|
52
|
+
}
|
|
53
|
+
/** "Limit at $759.50 · Day", "Stop $750.00 · Day": an order's terms, each named once. */
|
|
54
|
+
function orderTerms(order: Pick<BrokerOrder, "orderType" | "limitPrice" | "stopPrice" | "tif"> & { outsideRth?: boolean }, limitWord: string, money: (value: number) => string): string {
|
|
55
|
+
const limit = order.limitPrice !== undefined ? `${limitWord} ${money(order.limitPrice)}` : "";
|
|
56
|
+
const stop = order.stopPrice !== undefined ? `Stop ${money(order.stopPrice)}` : "";
|
|
57
|
+
const price = order.orderType === "STP" && stop ? [stop] : order.orderType === "STP LMT" && stop ? [stop, ...(order.limitPrice !== undefined ? [`limit ${money(order.limitPrice)}`] : [])]
|
|
58
|
+
: [`${typeName[order.orderType] ?? order.orderType}${limit ? ` ${limit}` : ""}`, ...(stop ? [stop] : [])];
|
|
59
|
+
return [...price, ...(order.tif ? [ticketTifName[order.tif] ?? order.tif] : []), ...(order.outsideRth ? ["Outside RTH"] : [])].join(" · ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function ticketQuoteAge(quote: Quote | undefined, now = Date.now()): string {
|
|
63
|
+
const at = quote?.lastTradeTime ?? quote?.lastUpdated;
|
|
64
|
+
if (!at || at > now) return "Age unknown";
|
|
65
|
+
const seconds = Math.max(0, Math.floor((now - at) / 1000));
|
|
66
|
+
return seconds < 60 ? `${seconds}s old` : seconds < 3600 ? `${Math.floor(seconds / 60)}m old` : `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m old`;
|
|
67
|
+
}
|
|
68
|
+
/** The decimals a price step needs: 0.01 and 0.05 take two, 0.0001 four. Never fewer than two. */
|
|
69
|
+
function stepDecimals(step: number): number {
|
|
70
|
+
return Number.isFinite(step) && step > 0 ? Math.max(2, Math.min(8, Math.ceil(-Math.log10(step) - 1e-9))) : 2;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* "+1.20 (+0.36%)", or nothing for a quote without a change of its own, such as a spread's. The move takes the
|
|
74
|
+
* price's tick decimals, and its percent two.
|
|
75
|
+
*/
|
|
76
|
+
function quoteChange(quote: Quote | undefined, decimals: number): string {
|
|
77
|
+
if (!quote || !Number.isFinite(quote.change)) return "";
|
|
78
|
+
const move = quote.change.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: decimals });
|
|
79
|
+
return `${quote.change > 0 && /[1-9]/.test(move) ? "+" : ""}${move} (${formatPercentRaw(quote.changePercent)})`;
|
|
80
|
+
}
|
|
81
|
+
/** "Limit $760.00 → $759.50", what a replacement changes; "No change" when it changes nothing. */
|
|
82
|
+
function replacementChanges(original: BrokerOrder, draft: BrokerOrderRequest, price: (value: number | undefined) => string): string {
|
|
83
|
+
const changes = [
|
|
84
|
+
original.quantity !== draft.quantity ? `Quantity ${number(original.quantity)} → ${number(draft.quantity)}` : "",
|
|
85
|
+
original.limitPrice !== draft.limitPrice && draft.limitPrice !== undefined ? `Limit ${price(original.limitPrice)} → ${price(draft.limitPrice)}` : "",
|
|
86
|
+
original.stopPrice !== draft.stopPrice && draft.stopPrice !== undefined ? `Stop ${price(original.stopPrice)} → ${price(draft.stopPrice)}` : "",
|
|
87
|
+
].filter(Boolean);
|
|
88
|
+
return changes.length ? changes.join(" · ") : "No change from the working order";
|
|
89
|
+
}
|
|
90
|
+
const stateName: Record<string, string> = {
|
|
91
|
+
NEW: "Working", WORKING: "Working", SUBMITTED: "Submitted", PENDING_NEW: "Submitted", PARTIALLY_FILLED: "Partially filled", FILLED: "Filled",
|
|
92
|
+
REJECTED: "Rejected", PENDING_CANCEL: "Pending cancel", PENDING_REPLACE: "Replacing", REPLACED: "Working", CANCELED: "Cancelled", CANCELLED: "Cancelled", UNKNOWN: "Outcome unknown",
|
|
93
|
+
};
|
|
94
|
+
/** Beside the chart, the TIF button shows these. */
|
|
95
|
+
const tifShortName: Record<string, string> = { DAY: "Day", GTC: "GTC", IOC: "IOC", FOK: "FOK", EXTENDED_HOURS: "Extended", AT_THE_OPENING: "At open", AT_THE_CLOSE: "At close" };
|
|
96
|
+
|
|
97
|
+
/** Preserve intermediate edits such as an empty field or trailing decimal point. */
|
|
98
|
+
function TicketNumberInput({ value, focused, onChange, ...props }: {
|
|
99
|
+
value: number | undefined; focused: boolean; active: boolean; width: number; accessibleLabel: string;
|
|
100
|
+
onChange(value: string): void; onSubmit(): void;
|
|
101
|
+
disabled: boolean; step: number; min: number; max?: number;
|
|
102
|
+
}) {
|
|
103
|
+
const format = (next: number | undefined) => next === undefined || !Number.isFinite(next) ? "" : String(next);
|
|
104
|
+
const [raw, setRaw] = useState(() => format(value));
|
|
105
|
+
const rawRef = useRef(raw);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const equivalent = rawRef.current === "" ? value === undefined || Number.isNaN(value) : Object.is(Number(rawRef.current), value);
|
|
108
|
+
if (!focused || !equivalent) {
|
|
109
|
+
rawRef.current = format(value);
|
|
110
|
+
setRaw(rawRef.current);
|
|
111
|
+
}
|
|
112
|
+
}, [value, focused]);
|
|
113
|
+
return <NumberField {...props} variant="well" value={raw} focused={focused} onChange={(next) => {
|
|
114
|
+
if (next === rawRef.current) return;
|
|
115
|
+
rawRef.current = next; setRaw(next); onChange(next);
|
|
116
|
+
}} />;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The docked quote: last price and change first, then bid in green and ask in
|
|
121
|
+
* red with their sizes, as a broker's quote panel reads. A delayed quote shows
|
|
122
|
+
* a clock whose tooltip names the source and its age.
|
|
123
|
+
*/
|
|
124
|
+
function DockedQuote({ quote, delayed, source, updated, price, change, width }: {
|
|
125
|
+
quote?: Quote; delayed: boolean; source: string; updated: string; price(value: number | undefined): string; change: string; width: number;
|
|
126
|
+
}) {
|
|
127
|
+
const c = useThemeColors();
|
|
128
|
+
const native = useUiCapabilities().nativePaneChrome;
|
|
129
|
+
const sized = (size: number | undefined) => size !== undefined && Number.isFinite(size) ? `× ${number(size)}` : "";
|
|
130
|
+
// Sizes drop before bid and ask leave one line.
|
|
131
|
+
const showSizes = `Bid ${price(quote?.bid)} ${sized(quote?.bidSize)} Ask ${price(quote?.ask)} ${sized(quote?.askSize)}`.length <= width;
|
|
132
|
+
const side = (label: string, value: number | undefined, size: number | undefined, color: string) => <Box flexDirection="row" gap={1} alignItems="baseline" flexShrink={0}>
|
|
133
|
+
<Text fg={c.textDim}>{label}</Text>
|
|
134
|
+
<Text fg={color} attributes={TextAttributes.BOLD} selectable>{price(value)}</Text>
|
|
135
|
+
{showSizes && sized(size) ? <Text fg={c.textDim} selectable>{sized(size)}</Text> : null}
|
|
136
|
+
</Box>;
|
|
137
|
+
return <Box flexDirection="column" gap={native ? 0.2 : 0} data-gloom-selectable="true">
|
|
138
|
+
<Box flexDirection="row" gap={1} alignItems={native ? "baseline" : "center"}>
|
|
139
|
+
<FigureText>{quote?.price === undefined ? "--" : price(quote.price)}</FigureText>
|
|
140
|
+
<Box flexGrow={1} minWidth={0}><FigureText part="sub" change={quote?.change}>{change}</FigureText></Box>
|
|
141
|
+
{delayed ? <Box flexShrink={0} title={`${source} · ${updated}`}><Icon name="clock" color={c.warning} /></Box> : null}
|
|
142
|
+
</Box>
|
|
143
|
+
<Box flexDirection="row" gap={2}>
|
|
144
|
+
{side("Bid", quote?.bid, quote?.bidSize, c.positive)}
|
|
145
|
+
{side("Ask", quote?.ask, quote?.askSize, c.negative)}
|
|
146
|
+
</Box>
|
|
147
|
+
</Box>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** One ticket presentation, with the same review and focus gates on every renderer. */
|
|
151
|
+
export function BrokerTicketView({ model: m, width, height, focused, onEdit, onAction, onAccountChange, onCapture, docked = false, blocked = false, asking = false, onClose, onContentHeight }: BrokerTicketViewProps) {
|
|
152
|
+
const c = useThemeColors();
|
|
153
|
+
const native = useUiCapabilities().nativePaneChrome;
|
|
154
|
+
const dialogOpen = useDialogState((state) => state.isOpen);
|
|
155
|
+
const scope = `broker-ticket:${useId()}`;
|
|
156
|
+
const wide = width >= 110;
|
|
157
|
+
const narrow = native && width < 70;
|
|
158
|
+
const account = m.accounts.find((item) => item.accountId === m.accountId);
|
|
159
|
+
const mode = account?.tradingMode ?? "unknown";
|
|
160
|
+
const live = mode !== "simulation";
|
|
161
|
+
const draft = m.draft;
|
|
162
|
+
const sell = draft?.action === "SELL";
|
|
163
|
+
const sideTone = sell ? "negative" : "positive";
|
|
164
|
+
// A ticker opened by its symbol alone can carry an empty currency, which no formatter takes.
|
|
165
|
+
const currency = draft?.contract.currency || account?.currency || "USD";
|
|
166
|
+
const strategy = !!draft?.legs?.length;
|
|
167
|
+
// A credit spread's preview reports what it receives as a negative cost.
|
|
168
|
+
const previewCredit = (m.preview?.estimatedCost ?? 0) < 0;
|
|
169
|
+
const previewCost = m.preview?.estimatedCost === undefined ? undefined : Math.abs(m.preview.estimatedCost);
|
|
170
|
+
const unit = strategy ? "spreads" : draft?.contract.secType === "OPT" ? "contracts" : "shares";
|
|
171
|
+
const busy = blocked || ["previewing", "submitting", "cancelling"].includes(m.phase);
|
|
172
|
+
const fast = mode === "simulation" && m.confirmOrders === false && !m.modifying;
|
|
173
|
+
const fastSubmitting = m.phase === "submitting" && fast && !m.warnings.length && !m.preview?.errors?.length;
|
|
174
|
+
const review = m.phase === "review" || m.phase === "submitting" && !fastSubmitting;
|
|
175
|
+
const editing = m.phase === "editing" || m.phase === "previewing" || fastSubmitting;
|
|
176
|
+
const showLimit = draft?.orderType === "LMT" || draft?.orderType === "STP LMT";
|
|
177
|
+
const showStop = draft?.orderType === "STP" || draft?.orderType === "STP LMT";
|
|
178
|
+
const extendedHours = m.capabilities?.extendedHours && (!m.capabilities.extendedHoursTif || m.capabilities.extendedHoursTif.includes(draft?.tif ?? "DAY"));
|
|
179
|
+
const inner = Math.max(28, width - (docked ? 2 : native ? 6 : 2));
|
|
180
|
+
const formWidth = wide ? Math.min(74, Math.floor((inner - 3) * 0.58)) : inner;
|
|
181
|
+
const summaryWidth = inner - formWidth - 3;
|
|
182
|
+
const rowGap = native ? 0.25 : 0;
|
|
183
|
+
const labelWidth = docked ? 10 : native ? 11 : 14;
|
|
184
|
+
const selectWidth = docked ? Math.max(14, formWidth - 11) : 22;
|
|
185
|
+
const [active, setActive] = useState("account");
|
|
186
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
187
|
+
const headerRef = useRef<BoxRenderable | null>(null);
|
|
188
|
+
const bodyRef = useRef<BoxRenderable | null>(null);
|
|
189
|
+
const reportContentHeight = () => {
|
|
190
|
+
if (native || !docked || !headerRef.current?.height || !bodyRef.current?.height) return;
|
|
191
|
+
onContentHeight?.(headerRef.current.height + bodyRef.current.height);
|
|
192
|
+
};
|
|
193
|
+
const reviewControlRef = useRef<BoxRenderable | null>(null);
|
|
194
|
+
const confirmControlRef = useRef<BoxRenderable | null>(null);
|
|
195
|
+
const accountControl = useRef<SelectControl>(null);
|
|
196
|
+
const tifControl = useRef<SelectControl>(null);
|
|
197
|
+
const orderTypeControl = useRef<SelectControl>(null);
|
|
198
|
+
const resultStatus = m.result?.status.toUpperCase() ?? "UNKNOWN";
|
|
199
|
+
// Filled, cancelled, rejected or expired: nothing is left to refresh, and the next step leads.
|
|
200
|
+
const finished = brokerOrderDone(resultStatus);
|
|
201
|
+
// Tab order of the focusable controls in each phase.
|
|
202
|
+
const keys = useMemo((): string[] => {
|
|
203
|
+
if (m.phase === "enable") return ["enable", "back"];
|
|
204
|
+
if (editing) return [
|
|
205
|
+
"account", "action", "orderType", "quantity", "quantity25", "quantity50", "all",
|
|
206
|
+
...(showLimit ? ["limitPrice", "priceBid", "priceMid", "priceAsk", "priceLast"] : []),
|
|
207
|
+
...(showStop ? ["stopPrice"] : []),
|
|
208
|
+
"tif",
|
|
209
|
+
...(extendedHours ? ["outsideRth"] : []),
|
|
210
|
+
...(mode === "simulation" && !m.modifying ? ["confirmOrders"] : []),
|
|
211
|
+
m.tradingEnabled ? "review" : "enable",
|
|
212
|
+
];
|
|
213
|
+
if (review) return ["back", ...(live ? ["typedConfirmation"] : []), "confirm"];
|
|
214
|
+
if (m.phase === "cancel-review") return ["back", "cancel"];
|
|
215
|
+
if (resultStatus === "REJECTED") return ["back", "orders"];
|
|
216
|
+
if (finished) return ["new", "orders"];
|
|
217
|
+
if (resultStatus === "UNKNOWN") return ["refresh", "orders"];
|
|
218
|
+
return ["refresh", "orders", "new"];
|
|
219
|
+
}, [m.phase, editing, review, live, showLimit, showStop, extendedHours, resultStatus, finished, m.tradingEnabled, mode, m.modifying]);
|
|
220
|
+
const previousFocus = useRef<{ phase: TicketViewModel["phase"]; revision?: number } | undefined>(undefined);
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (previousFocus.current?.phase !== m.phase || previousFocus.current?.revision !== m.focusRevision) {
|
|
223
|
+
previousFocus.current = { phase: m.phase, revision: m.focusRevision };
|
|
224
|
+
setActive(review || m.phase === "cancel-review" ? "back" : editing && m.initialFocus && keys.includes(m.initialFocus) ? m.initialFocus : keys[0]!);
|
|
225
|
+
} else if (!keys.includes(active)) setActive(keys[0]!);
|
|
226
|
+
}, [m.phase, m.focusRevision, keys, active]);
|
|
227
|
+
const { nodeRef } = useFieldRing({ ids: keys, activeId: active, onActivate: setActive, enabled: focused && !busy && !dialogOpen, scope, scrollRef });
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (active === "review" && editing) return afterLayout(() => revealInScrollBox(scrollRef.current, reviewControlRef.current));
|
|
230
|
+
}, [active, editing, m.focusRevision]);
|
|
231
|
+
// A review taller than the panel still shows its place button on arrival;
|
|
232
|
+
// focus stays on Edit order, so Enter alone never places it.
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (review) return afterLayout(() => revealInScrollBox(scrollRef.current, confirmControlRef.current));
|
|
235
|
+
}, [review]);
|
|
236
|
+
const confirmationSymbol = (draft?.contract.symbol ?? m.symbol).toUpperCase();
|
|
237
|
+
const typedMatches = !live || m.typedConfirmation?.trim().toUpperCase() === confirmationSymbol;
|
|
238
|
+
const holdingBlocked = sell && !strategy && !m.capabilities?.shortSelling && (m.position === undefined || (draft?.quantity ?? 0) > Math.max(0, m.position));
|
|
239
|
+
const validPrice = (value: number | undefined) => value !== undefined && Number.isFinite(value) && value > 0;
|
|
240
|
+
const canReview = m.connected && m.tradingEnabled && !!account && !!draft && validPrice(draft.quantity) && !holdingBlocked && !busy
|
|
241
|
+
&& (!showLimit || validPrice(draft.limitPrice)) && (!showStop || validPrice(draft.stopPrice));
|
|
242
|
+
const blockingPreview = !!m.preview?.errors?.length;
|
|
243
|
+
const canConfirm = review && typedMatches && !blockingPreview && m.connected && m.tradingEnabled && !busy;
|
|
244
|
+
const textActive = ["quantity", "limitPrice", "stopPrice", "typedConfirmation"].includes(active);
|
|
245
|
+
// Leaving a field must return h/l to the research strip, including after a command.
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
onCapture?.(focused && textActive);
|
|
248
|
+
return () => onCapture?.(false);
|
|
249
|
+
}, [focused, textActive, onCapture]);
|
|
250
|
+
// A spread sizes by hand: its cost and holdings are per leg.
|
|
251
|
+
const quickDisabled = (key: TicketAction) => strategy && ["all", "quantity25", "quantity50", "priceLast"].includes(key) ? true : key === "priceBid" ? !validPrice(m.quote?.bid) : key === "priceAsk" ? !validPrice(m.quote?.ask)
|
|
252
|
+
: key === "priceMid" ? !validPrice(m.quote?.bid) || !validPrice(m.quote?.ask) : key === "priceLast" ? !validPrice(m.quote?.price)
|
|
253
|
+
: sell ? m.position === undefined || m.position <= 0 : !account || !validPrice(account.buyingPower) || !validPrice(m.quote?.ask ?? m.quote?.price);
|
|
254
|
+
const quickActions: TicketAction[] = ["all", "quantity25", "quantity50", "priceBid", "priceMid", "priceAsk", "priceLast"];
|
|
255
|
+
const activate = () => {
|
|
256
|
+
if (busy) return;
|
|
257
|
+
if (active === "account") { if (!m.modifying) accountControl.current?.open(); return; }
|
|
258
|
+
if (active === "orderType") { if (!m.modifying) orderTypeControl.current?.open(); return; }
|
|
259
|
+
if (active === "confirmOrders") { if (mode === "simulation" && !m.modifying) onEdit("confirmOrders", m.confirmOrders === false); return; }
|
|
260
|
+
if (active === "tif") { if (!m.modifying) tifControl.current?.open(); return; }
|
|
261
|
+
if (active === "outsideRth") { if (!m.modifying && extendedHours) onEdit("outsideRth", !draft?.outsideRth); return; }
|
|
262
|
+
if (active === "review") { if (canReview) onAction("submit"); return; }
|
|
263
|
+
if (active === "confirm") { if (canConfirm) onAction("confirm"); return; }
|
|
264
|
+
if (quickActions.includes(active as TicketAction)) { if (!quickDisabled(active as TicketAction)) onAction(active as TicketAction); return; }
|
|
265
|
+
if (active === "enable" && !m.connected) return;
|
|
266
|
+
if (["back", "refresh", "orders", "new", "enable", "cancel"].includes(active)) onAction(active as TicketAction);
|
|
267
|
+
else setActive(editing ? m.tradingEnabled ? "review" : "enable" : "confirm");
|
|
268
|
+
};
|
|
269
|
+
const keyAction = (key: string, label: string, run: () => void, disabled = false): PaneHint => ({ id: `ticket-${key}`, key, label, onPress: run, disabled: busy || disabled });
|
|
270
|
+
const editShortcut = (key: string, label: string, field: TicketField, value: string | boolean, disabled = false) => keyAction(key, label, () => { setActive(field); onEdit(field, value); }, disabled);
|
|
271
|
+
const actionShortcut = (key: string, label: string, action: TicketAction, disabled = false) => keyAction(key, label, () => { setActive(action); onAction(action); }, disabled);
|
|
272
|
+
const shortcuts: PaneHint[] = editing ? [
|
|
273
|
+
editShortcut("b", "uy", "action", "BUY", !!m.modifying), editShortcut("s", "ell", "action", "SELL", !!m.modifying),
|
|
274
|
+
m.tradingEnabled ? actionShortcut("r", "eview", "review", !canReview) : actionShortcut("e", "nable trading", "enable", !m.connected), actionShortcut("o", "rders", "orders"),
|
|
275
|
+
keyAction("a", "ccount", () => { setActive("account"); accountControl.current?.open(); }, !!m.modifying),
|
|
276
|
+
keyAction("t", "ime in force", () => { setActive("tif"); tifControl.current?.open(); }, !!m.modifying),
|
|
277
|
+
...(["MKT", "LMT", "STP", "STP LMT"] as const).map((type, index) => editShortcut(String(index + 1), typeName[type]!, "orderType", type, !!m.modifying || !m.capabilities?.orderTypes.includes(type))),
|
|
278
|
+
...([["5", "25%", "quantity25"], ["6", "50%", "quantity50"], ["7", "Max", "all"]] as const).map(([key, label, action]) => actionShortcut(key, label, action, quickDisabled(action))),
|
|
279
|
+
...(showLimit || showStop ? ([["8", "Bid", "priceBid"], ["9", "Mid", "priceMid"], ["0", "Ask", "priceAsk"], ...(strategy ? [] : [["p", "Last", "priceLast"]] as const)] as const)
|
|
280
|
+
.map(([key, label, action]) => actionShortcut(key, label, action, quickDisabled(action))) : []),
|
|
281
|
+
...(extendedHours ? [editShortcut("x", "Extended", "outsideRth", !draft?.outsideRth, !!m.modifying)] : []),
|
|
282
|
+
] : review ? [actionShortcut("e", "dit", "back"), actionShortcut("o", "rders", "orders")]
|
|
283
|
+
: m.phase === "enable" ? [actionShortcut("e", "nable", "enable", !m.connected)]
|
|
284
|
+
: m.phase === "cancel-review" ? [actionShortcut("e", "dit", "back")]
|
|
285
|
+
: m.phase === "result" ? [
|
|
286
|
+
...(resultStatus === "REJECTED" ? [actionShortcut("e", "dit", "back")] : []),
|
|
287
|
+
...(finished ? [] : [actionShortcut("r", "efresh", "refresh")]), actionShortcut("o", "rders", "orders"),
|
|
288
|
+
...(!["UNKNOWN", "REJECTED"].includes(resultStatus) ? [actionShortcut("n", "ew", "new")] : []),
|
|
289
|
+
] : [];
|
|
290
|
+
useShortcut((event) => {
|
|
291
|
+
if (event.defaultPrevented || event.propagationStopped) return;
|
|
292
|
+
if (event.ctrl || event.meta || event.alt || event.super) return;
|
|
293
|
+
if (event.name === "escape" && !busy && (review || m.phase === "cancel-review" || m.phase === "enable" || textActive)) {
|
|
294
|
+
event.preventDefault(); event.stopPropagation();
|
|
295
|
+
if (editing) setActive("account"); else onAction("back");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (event.name === "escape" && onClose) { event.preventDefault(); event.stopPropagation(); onClose(); return; }
|
|
299
|
+
if (["return", "enter"].includes(event.name ?? "") || event.name === "space" && !textActive && !event.targetEditable) {
|
|
300
|
+
event.preventDefault(); event.stopPropagation();
|
|
301
|
+
if (!event.repeat) activate();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (event.shift || event.targetEditable || textActive) return;
|
|
305
|
+
const shortcut = shortcuts.find((item) => item.key === event.name);
|
|
306
|
+
if (!shortcut) return;
|
|
307
|
+
event.preventDefault(); event.stopPropagation();
|
|
308
|
+
// The shortcut registry lets plain letters repeat, for typing and stepping. A ticket action acts once per
|
|
309
|
+
// press, so a held key never steps past a consent, a confirmation or a result.
|
|
310
|
+
if (!shortcut.disabled && !event.repeat) shortcut.onPress?.();
|
|
311
|
+
}, { enabled: focused && !dialogOpen, phase: "before", scope, allowEditable: true });
|
|
312
|
+
// The full keyboard map remains available without filling the status bar.
|
|
313
|
+
const footerKeys = editing ? ["r", "o"] : m.phase === "result" ? ["r", "o", resultStatus === "REJECTED" ? "e" : "n"] : ["e"];
|
|
314
|
+
usePaneFooter(scope, () => ({
|
|
315
|
+
info: busy && !(blocked && asking) ? [{ id: "progress", parts: [{ text: blocked ? "Order action pending..." : m.phase === "previewing" ? "Requesting broker preview..." : m.phase === "cancelling" ? "Cancelling..." : "Submitting..." }] }] : undefined,
|
|
316
|
+
hints: busy || textActive ? [] : shortcuts.filter((hint) => footerKeys.includes(hint.key)),
|
|
317
|
+
}), [m, busy, blocked, asking, textActive]);
|
|
318
|
+
usePaneMenuItems(scope + ":keys", () => [
|
|
319
|
+
...shortcuts.filter((hint) => !footerKeys.includes(hint.key)).map((hint) => ({
|
|
320
|
+
id: hint.id, label: hint.key.length === 1 && /^[a-z]/.test(hint.label) ? hint.key + hint.label : hint.label,
|
|
321
|
+
accelerator: hint.key, enabled: !hint.disabled, onSelect: hint.onPress,
|
|
322
|
+
})),
|
|
323
|
+
{ id: "ticket-next", label: "Next field", accelerator: "Tab" },
|
|
324
|
+
{ id: "ticket-previous", label: "Previous field", accelerator: "Shift+Tab" },
|
|
325
|
+
{ id: "ticket-activate", label: "Activate focused control", accelerator: "Enter", onSelect: activate, enabled: !busy },
|
|
326
|
+
{ id: "ticket-back", label: textActive ? "Leave field" : review || m.phase === "cancel-review" ? "Edit order" : "Close trade panel", accelerator: "Esc",
|
|
327
|
+
onSelect: () => { if (busy) return; if (editing && textActive) setActive("account"); else if (review || m.phase === "cancel-review" || m.phase === "enable") onAction("back"); else onClose?.(); } },
|
|
328
|
+
], [m, busy, active, textActive, canConfirm, canReview, onClose]);
|
|
329
|
+
// The terminal marks the focused control with ">"; inline result links use their short names.
|
|
330
|
+
const marker = (key: string) => focused && active === key ? "> " : "";
|
|
331
|
+
const action = (key: TicketAction, label: string, disabled = false, primary = false, tone: "positive" | "negative" | "neutral" | "warning" = sideTone, inline = false) => {
|
|
332
|
+
const shortLabel = ({ refresh: "Refresh", orders: "Orders", new: "New" } as Partial<Record<TicketAction, string>>)[key] ?? label;
|
|
333
|
+
const displayLabel = native ? undefined : inline ? `${marker(key)}${shortLabel}` : marker(key) ? `> ${label}` : undefined;
|
|
334
|
+
const ref = (node: BoxRenderable | null) => {
|
|
335
|
+
nodeRef(key)(node);
|
|
336
|
+
if (key === "review") reviewControlRef.current = node;
|
|
337
|
+
if (key === "confirm") confirmControlRef.current = node;
|
|
338
|
+
};
|
|
339
|
+
return <Box ref={ref} onMouseDown={() => setActive(key)} flexShrink={0}>
|
|
340
|
+
<Button label={label} displayLabel={displayLabel} variant={inline ? "plain" : primary ? "primary" : "quiet"} flush={inline} compact={inline}
|
|
341
|
+
tone={primary ? tone : undefined} height={native && primary ? 2 : 1} active={focused && active === key} disabled={disabled || busy}
|
|
342
|
+
width={primary ? Math.max(16, formWidth) : undefined} onPress={() => { setActive(key); onAction(key === "review" ? "submit" : key); }} />
|
|
343
|
+
</Box>;
|
|
344
|
+
};
|
|
345
|
+
const quick = (key: TicketAction, label: string, disabled = false) => <Box ref={nodeRef(key)} key={key}>
|
|
346
|
+
<Button label={label} displayLabel={!native && marker(key) ? `> ${label}` : undefined} variant="quiet" compact active={active === key && focused}
|
|
347
|
+
disabled={disabled || busy} onPress={() => { setActive(key); onAction(key); }} />
|
|
348
|
+
</Box>;
|
|
349
|
+
const numeric = (key: TicketField, label: string, value: number | undefined, suffix: string, picks?: ReactNode) => {
|
|
350
|
+
const inlinePicks = !!picks && formWidth >= 60;
|
|
351
|
+
const quantity = key === "quantity";
|
|
352
|
+
const reportedStep = m.capabilities?.quantityStep ?? 1;
|
|
353
|
+
const quantityStep = m.capabilities?.fractionalQuantity ? reportedStep : wholeNumberStep(reportedStep) ?? NaN;
|
|
354
|
+
// Decimal precision is not a declared exchange tick. The fallback is an editing convenience only.
|
|
355
|
+
const priceStep = m.capabilities?.priceStep ?? ticketEditingPriceStep(value ?? m.quote?.price, m.capabilities?.priceDecimals);
|
|
356
|
+
const step = quantity ? quantityStep : priceStep;
|
|
357
|
+
return <Box flexDirection="column" gap={0}>
|
|
358
|
+
<Box flexDirection="row" alignItems="center" gap={1}>
|
|
359
|
+
<Box ref={nodeRef(key)} onMouseDown={() => { if (!busy) setActive(key); }} flexDirection="row" alignItems="center" gap={1} flexGrow={1}>
|
|
360
|
+
<FieldLabel label={label} width={labelWidth} active={active === key} />
|
|
361
|
+
<Box flexGrow={1}><TicketNumberInput accessibleLabel={key === "limitPrice" ? "Limit price" : key === "stopPrice" ? "Stop price" : label} value={value}
|
|
362
|
+
width={Math.max(14, formWidth - (docked ? 11 : native ? 21 : 24) - (inlinePicks ? 20 : 0))} focused={focused && active === key && !busy} active={active === key} disabled={busy}
|
|
363
|
+
step={step} min={quantity ? Math.max(step, m.capabilities?.minQuantity ?? step) : step} max={quantity && sell && !m.capabilities?.shortSelling ? Math.max(0, m.position ?? 0) : undefined}
|
|
364
|
+
onChange={(text) => { if (!busy) onEdit(key, text); }} onSubmit={() => setActive(m.tradingEnabled ? "review" : "enable")} /></Box>
|
|
365
|
+
{!docked ? <Text fg={c.textDim}>{suffix}</Text> : null}
|
|
366
|
+
</Box>
|
|
367
|
+
{inlinePicks ? <Box flexDirection="row" gap={1}>{picks}</Box> : null}
|
|
368
|
+
</Box>
|
|
369
|
+
{picks && !inlinePicks ? <Box flexDirection="row" justifyContent="flex-end" gap={1} paddingRight={docked ? 0 : 9}>{picks}</Box> : null}
|
|
370
|
+
</Box>;
|
|
371
|
+
};
|
|
372
|
+
// Quotes read to the instrument's tick: a sub-penny print of a stock above a dollar shows as cents.
|
|
373
|
+
const tickDecimals = (value: number | undefined) => stepDecimals(m.capabilities?.priceStep ?? ticketEditingPriceStep(value, m.capabilities?.priceDecimals));
|
|
374
|
+
const quotePrice = (value: number | undefined) => value === undefined ? "Unavailable" : value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: tickDecimals(value) });
|
|
375
|
+
const change = quoteChange(m.quote, tickDecimals(m.quote?.price));
|
|
376
|
+
const sourceAt = m.quote?.lastTradeTime ?? m.quote?.lastUpdated;
|
|
377
|
+
const updated = sourceAt && Number.isFinite(sourceAt) ? `${new Date(sourceAt).toISOString().slice(11, 19)} UTC` : "unknown";
|
|
378
|
+
const delayed = m.quoteData !== "realtime" || m.quote?.dataSource !== "live";
|
|
379
|
+
const age = ticketQuoteAge(m.quote, m.now).replace(" old", "");
|
|
380
|
+
const source = `${m.brokerName}, ${delayed ? "delayed" : "real-time"} ${age}`;
|
|
381
|
+
const accountPicker = (pickerWidth: number) => <BrokerAccountPicker
|
|
382
|
+
accounts={m.accountChoices ?? m.accounts.map((a) => ({ value: a.accountId, label: a.name, tradingMode: a.tradingMode }))}
|
|
383
|
+
value={m.accountChoiceValue ?? m.accountId} onChange={onAccountChange} disabled={!editing || busy || m.modifying}
|
|
384
|
+
active={active === "account" && focused} controlRef={accountControl} width={pickerWidth} showMode />;
|
|
385
|
+
const bookSide = (label: string, size: number | undefined, value: number | undefined, color: string) => <Box flexDirection="column" paddingX={1}
|
|
386
|
+
border={["bottom"]} borderColor={color} style={native ? { borderBottom: `2px solid ${color}` } : undefined}>
|
|
387
|
+
<Text fg={c.textDim}>{`${label}${size !== undefined ? ` × ${number(size)}` : ""}`}</Text>
|
|
388
|
+
<Text fg={color} attributes={TextAttributes.BOLD}>{quotePrice(value)}</Text>
|
|
389
|
+
</Box>;
|
|
390
|
+
const resultQuantity = (value: number | undefined) => m.resultQuantities === "unconfirmed" || !Number.isFinite(value) ? "Not confirmed" : number(value);
|
|
391
|
+
const title = stateName[resultStatus] ?? (m.result?.status ? `${m.result.status.charAt(0)}${m.result.status.slice(1).toLowerCase().replace(/_/g, " ")}` : "Outcome unknown");
|
|
392
|
+
// A spread's limit is its net debit or credit.
|
|
393
|
+
const limitLabel = strategy ? docked ? sell ? "Credit" : "Debit" : sell ? "Net credit" : "Net debit" : docked ? "Limit" : "Limit price";
|
|
394
|
+
const warnings = [...new Set(m.warnings)];
|
|
395
|
+
if (warnings.includes("The broker quote is delayed.") && warnings.includes("The broker quote is stale. Prices may have changed.")) {
|
|
396
|
+
warnings.splice(warnings.indexOf("The broker quote is delayed."), 1);
|
|
397
|
+
warnings[ warnings.indexOf("The broker quote is stale. Prices may have changed.") ] = "Delayed, stale broker quote. Execution prices may differ.";
|
|
398
|
+
}
|
|
399
|
+
return <Box flexDirection="column" flexGrow={docked ? 0 : 1} flexShrink={1} minHeight={0} height={docked ? undefined : height} maxHeight={height}>
|
|
400
|
+
{docked ? <Box ref={headerRef} onSizeChange={native ? undefined : reportContentHeight} flexDirection="column" paddingX={1} paddingTop={native ? 0.4 : 0} gap={native ? 0.4 : 0} flexShrink={0}>
|
|
401
|
+
<Box flexDirection="row" gap={1} alignItems="center" justifyContent="space-between">
|
|
402
|
+
<Box ref={nodeRef("account")} flexShrink={1} minWidth={0}>{accountPicker(width - (onClose ? 6 : 2))}</Box>
|
|
403
|
+
{onClose ? <IconButton icon="close" label="Close trade panel" shortcut="Esc" onPress={onClose} /> : null}
|
|
404
|
+
</Box>
|
|
405
|
+
<DockedQuote quote={m.quote} delayed={delayed} source={source} updated={updated} price={quotePrice} change={change} width={width - 2} />
|
|
406
|
+
</Box> : <Box flexShrink={0}>
|
|
407
|
+
<Box flexDirection="column" data-gloom-selectable="true" paddingX={native ? 3 : 1} paddingY={native ? wide ? 1 : 0.5 : 0} gap={native ? wide ? 0.65 : 0.2 : 0} >
|
|
408
|
+
<Box flexDirection={narrow ? "column" : "row"} justifyContent="space-between" alignItems={narrow ? "flex-start" : "center"} gap={narrow ? 0.4 : 1}>
|
|
409
|
+
<Box flexDirection="row" alignItems="center" gap={1} flexGrow={1} minWidth={0}>
|
|
410
|
+
<Text fg={c.textBright} attributes={TextAttributes.BOLD}>{m.symbol}</Text>
|
|
411
|
+
<Text fg={c.textDim} truncate>{m.quote?.name && m.quote.name !== m.symbol ? m.quote.name : ""}</Text>
|
|
412
|
+
{m.synthetic && narrow ? <Badge label="SYNTHETIC" /> : null}
|
|
413
|
+
</Box>
|
|
414
|
+
<Box flexDirection="row" gap={1} alignItems="center">
|
|
415
|
+
<Box ref={nodeRef("account")} onMouseDown={() => { if (editing) setActive("account"); }} flexShrink={0}>{accountPicker(Math.min(42, width - 6))}</Box>
|
|
416
|
+
{m.synthetic && wide ? <Badge label="SYNTHETIC" /> : null}
|
|
417
|
+
</Box>
|
|
418
|
+
</Box>
|
|
419
|
+
<Box flexDirection={narrow ? "column" : "row"} alignItems={narrow ? "flex-start" : "center"} gap={narrow ? 0.5 : 2}>
|
|
420
|
+
<Box flexDirection="row" alignItems="center" gap={2}>
|
|
421
|
+
<FigureText>{money(m.quote?.price, currency)}</FigureText>
|
|
422
|
+
<FigureText part="sub" change={m.quote?.change}>{change}</FigureText>
|
|
423
|
+
</Box>
|
|
424
|
+
<Box flexDirection="row" gap={1}>
|
|
425
|
+
{bookSide("Bid", m.quote?.bidSize, m.quote?.bid, c.positive)}
|
|
426
|
+
{bookSide("Ask", m.quote?.askSize, m.quote?.ask, c.negative)}
|
|
427
|
+
</Box>
|
|
428
|
+
</Box>
|
|
429
|
+
<Box flexDirection="row" flexWrap={native ? "wrap" : "nowrap"} alignItems="center" gap={1}>
|
|
430
|
+
{/* A delayed quote is the yellow clock, as in pane footers, with the detail on hover. */}
|
|
431
|
+
{delayed ? <Box flexShrink={0} title={source}><Icon name="clock" color={c.warning} /></Box> : null}
|
|
432
|
+
<Text fg={c.textDim}>{delayed ? `${m.brokerName} ${age}` : source}</Text>
|
|
433
|
+
<Text fg={c.textDim}>{`${updated}${m.synthetic && !wide && !narrow ? " · SYNTHETIC" : ""}`}</Text>
|
|
434
|
+
</Box>
|
|
435
|
+
</Box>
|
|
436
|
+
</Box>}
|
|
437
|
+
<ScrollBox ref={scrollRef} flexGrow={docked ? 0 : 1} flexShrink={1} scrollY>
|
|
438
|
+
<Box ref={bodyRef} onSizeChange={native ? undefined : reportContentHeight} flexShrink={0} flexDirection="column" paddingX={docked ? 1 : native ? 3 : 1} paddingY={native ? wide ? 1 : 0.4 : 0} gap={native ? wide ? 1 : 0.3 : 0}>
|
|
439
|
+
{!docked ? <Box flexDirection="row" gap={1} flexWrap={native ? "wrap" : "nowrap"}>
|
|
440
|
+
{/* A spread's positions are its legs', which the form lists. */}
|
|
441
|
+
{strategy ? null : <Text fg={c.textDim}>{`Position ${m.position === undefined ? "unknown" : `${number(m.position)} ${unit === "shares" ? "sh" : "ct"}`}${m.avgCost === undefined ? "" : ` @ ${money(m.avgCost, currency)}`}`}</Text>}
|
|
442
|
+
{m.positionPnl !== undefined ? <Text fg={m.positionPnl >= 0 ? c.positive : c.negative}>{`P&L ${money(m.positionPnl, currency)}`}</Text> : null}
|
|
443
|
+
<Text fg={c.textDim}>{`Buying power ${money(account?.buyingPower, currency)}`}</Text>
|
|
444
|
+
{m.accountType ? <Text fg={c.textDim}>{m.accountType}</Text> : null}
|
|
445
|
+
</Box> : null}
|
|
446
|
+
{editing && draft ? <Box flexDirection={wide ? "row" : "column"} gap={wide ? 3 : 0} alignItems="flex-start">
|
|
447
|
+
<TicketSection width={formWidth} title={m.modifying ? "Modify order" : docked ? undefined : "Order details"}>
|
|
448
|
+
<Box flexDirection="column" gap={rowGap}>
|
|
449
|
+
{!m.connected ? m.reconnecting ? <Notice tone="muted">{`Reconnecting to ${m.brokerName}...`}</Notice> : <Notice variant="callout">Broker disconnected. Connect in Brokers.</Notice>
|
|
450
|
+
: !m.tradingEnabled ? <Notice variant="callout">Trading is off for this profile.</Notice> : null}
|
|
451
|
+
<Box ref={nodeRef("action")} flexDirection="row" onMouseDown={() => setActive("action")}>
|
|
452
|
+
{!native ? <FieldLabel label="" width={2} active={focused && active === "action"} /> : null}
|
|
453
|
+
<Box flexGrow={1}><SegmentedControl variant="flat" accessibleLabel="Order side" value={draft.action} size="large" width="100%"
|
|
454
|
+
options={[{ value: "BUY", label: "Buy", tone: "positive", disabled: busy || m.modifying }, { value: "SELL", label: "Sell", tone: "negative", disabled: busy || m.modifying }]}
|
|
455
|
+
focused={focused && active === "action"} onChange={(v) => onEdit("action", v)} shortcutScope={scope} /></Box>
|
|
456
|
+
</Box>
|
|
457
|
+
<Box ref={nodeRef("orderType")} flexDirection="row" gap={1} alignItems="center" onMouseDown={() => { if (!busy) setActive("orderType"); }}>
|
|
458
|
+
<FieldLabel label={docked ? "Type" : "Order type"} width={labelWidth} active={active === "orderType"} />
|
|
459
|
+
<SelectButton label="Order type" showLabel={false} variant="well" width={selectWidth} value={draft.orderType}
|
|
460
|
+
options={(m.capabilities?.orderTypes ?? ["LMT"]).map((t) => ({ value: t, label: typeName[t] ?? t }))} disabled={busy || m.modifying}
|
|
461
|
+
emphasized={focused && active === "orderType"} controlRef={orderTypeControl} onFocus={() => setActive("orderType")} onChange={(v) => onEdit("orderType", v)} />
|
|
462
|
+
</Box>
|
|
463
|
+
{strategy ? <TicketLegs model={m} width={formWidth} inset /> : null}
|
|
464
|
+
{numeric("quantity", strategy && docked ? "Spreads" : "Quantity", draft.quantity, unit, strategy ? undefined : <>
|
|
465
|
+
{quick("quantity25", "25%", sell && m.position === undefined)}
|
|
466
|
+
{quick("quantity50", "50%", sell && m.position === undefined)}
|
|
467
|
+
{quick("all", sell ? `Max ${number(m.position)}` : "Max", sell && !(m.position && m.position > 0))}
|
|
468
|
+
</>)}
|
|
469
|
+
{showLimit ? numeric("limitPrice", limitLabel, draft.limitPrice, currency, <>
|
|
470
|
+
{quick("priceBid", "Bid", !(m.quote?.bid! > 0))}
|
|
471
|
+
{quick("priceMid", "Mid", !(m.quote?.bid! > 0) || !(m.quote?.ask! > 0))}
|
|
472
|
+
{quick("priceAsk", "Ask", !(m.quote?.ask! > 0))}
|
|
473
|
+
{strategy ? null : quick("priceLast", "Last", !m.quote?.price)}
|
|
474
|
+
</>) : null}
|
|
475
|
+
{showStop ? numeric("stopPrice", docked ? "Stop" : "Stop price", draft.stopPrice, currency) : null}
|
|
476
|
+
<Box flexDirection="row" flexWrap="wrap" alignItems="center" gap={docked ? rowGap : 1}>
|
|
477
|
+
<Box ref={nodeRef("tif")} flexDirection="row" alignItems="center" gap={1} onMouseDown={() => setActive("tif")}>
|
|
478
|
+
<FieldLabel label="TIF" width={labelWidth} active={active === "tif"} />
|
|
479
|
+
<SelectButton label="Time in force" title="Time in force" showLabel={false} variant="well" width={selectWidth} value={draft.tif ?? "DAY"}
|
|
480
|
+
options={(m.capabilities?.tif ?? ["DAY"]).map((v) => ({ value: v, label: ticketTifName[v] ?? v, short: docked ? tifShortName[v] : undefined }))} disabled={busy || m.modifying}
|
|
481
|
+
emphasized={focused && active === "tif"} controlRef={tifControl} onFocus={() => setActive("tif")} onChange={(v) => onEdit("tif", v)} />
|
|
482
|
+
</Box>
|
|
483
|
+
{extendedHours ? <Box ref={nodeRef("outsideRth")}><Checkbox label="Outside RTH" checked={draft.outsideRth ?? false} disabled={busy || m.modifying}
|
|
484
|
+
active={focused && active === "outsideRth"} onChange={(v) => { setActive("outsideRth"); onEdit("outsideRth", v); }} /></Box> : null}
|
|
485
|
+
</Box>
|
|
486
|
+
{holdingBlocked ? <Notice tone="negative">{m.position === undefined ? "Refresh holdings before selling." : `You hold ${number(m.position)} ${unit}. Short selling is unavailable.`}</Notice> : null}
|
|
487
|
+
{m.error ? <Notice variant="callout" tone="negative">{m.error}</Notice> : null}
|
|
488
|
+
{mode === "simulation" && !m.modifying ? <Box ref={nodeRef("confirmOrders")} flexDirection="column" gap={native ? 0.25 : 0}>
|
|
489
|
+
<Checkbox label="Confirm orders" checked={!fast} disabled={busy} active={focused && active === "confirmOrders"} onChange={(value) => { setActive("confirmOrders"); onEdit("confirmOrders", value); }} />
|
|
490
|
+
{fast ? <Text fg={c.warning}>SIM one-click · preview required</Text> : null}
|
|
491
|
+
</Box> : null}
|
|
492
|
+
{docked ? <Box flexDirection="column">
|
|
493
|
+
{strategy ? null : <TicketReceiptRow label="Position" width={formWidth} value={m.position === undefined ? "Not reported" : m.position === 0 ? "None"
|
|
494
|
+
: `${number(m.position)} ${unit === "shares" ? "sh" : "ct"}${m.avgCost === undefined ? "" : ` @ ${money(m.avgCost, currency)}`}`} />}
|
|
495
|
+
<TicketReceiptRow label={sell ? strategy ? "Est. credit" : "Est. proceeds" : "Est. cost"} width={formWidth}
|
|
496
|
+
value={money((orderEstimatePrice(draft, m.quote) ?? NaN) * draft.quantity * Number(draft.contract.multiplier ?? 1), currency)} />
|
|
497
|
+
<TicketReceiptRow label="Buying power" value={money(account?.buyingPower, currency)} width={formWidth} />
|
|
498
|
+
</Box> : null}
|
|
499
|
+
<Box marginTop={native && !docked ? 0.5 : 0}>{m.tradingEnabled
|
|
500
|
+
? action("review", busy && !(blocked && asking) ? blocked ? "Order action pending..." : m.phase === "submitting" ? "Submitting..." : "Requesting preview..." : m.modifying ? "Review change" : `${sell ? "Sell" : "Buy"} ${number(draft.quantity)} ${m.symbol}`, !canReview, true)
|
|
501
|
+
: action("enable", "Enable trading", !m.connected, true, "neutral")}</Box>
|
|
502
|
+
</Box>
|
|
503
|
+
</TicketSection>
|
|
504
|
+
</Box> : null}
|
|
505
|
+
{m.phase === "enable" ? <Box flexDirection={wide ? "row" : "column"} gap={3}><TicketSection width={formWidth} title="Enable trading">
|
|
506
|
+
<FigureText>Review first. Confirm once.</FigureText>
|
|
507
|
+
<Notice variant="callout" tone="warning">Orders can commit real money.</Notice>
|
|
508
|
+
<Text fg={c.text} wrapText>Start with a simulation account. Every order gets its own broker preview and review.</Text>
|
|
509
|
+
<Text fg={c.textDim} wrapText>LIVE orders also require a typed confirmation. Turn trading off at any time in Brokers.</Text>
|
|
510
|
+
<Box marginTop={1} gap={1} flexDirection="column">{action("enable", "Enable trading", false, true, "neutral")}{action("back", "Keep read only")}</Box>
|
|
511
|
+
</TicketSection>{wide ? <TicketOrderSummary model={m} width={summaryWidth} /> : null}</Box> : null}
|
|
512
|
+
{review ? <Box flexDirection={wide ? "row" : "column"} gap={wide ? 3 : 0} alignItems="flex-start"><TicketSection width={formWidth} title={m.modifying ? "Review replacement" : "Review order"} selectable>
|
|
513
|
+
<TicketOrderHeadline model={m} large />
|
|
514
|
+
{strategy ? <TicketLegs model={m} width={formWidth} /> : null}
|
|
515
|
+
<Text fg={c.textDim}>{orderTerms({ orderType: draft?.orderType ?? "LMT", limitPrice: showLimit ? draft?.limitPrice : undefined,
|
|
516
|
+
stopPrice: showStop ? draft?.stopPrice : undefined, tif: draft?.tif ?? "DAY", outsideRth: draft?.outsideRth }, strategy ? sell ? "net credit" : "net debit" : "at", (value) => money(value, currency))}</Text>
|
|
517
|
+
{m.original && draft ? <Text fg={c.warning}>{replacementChanges(m.original, draft, (value) => money(value, currency))}</Text> : null}
|
|
518
|
+
<Box marginTop={native && wide ? 1 : 0} flexDirection="column" gap={native && wide ? 0.4 : 0}>
|
|
519
|
+
<TicketReceiptRow label="Fees" value={money(m.preview?.fees, currency)} width={formWidth} />
|
|
520
|
+
<TicketReceiptRow label="Commission" value={money(m.preview?.commission, m.preview?.commissionCurrency ?? currency)} width={formWidth} />
|
|
521
|
+
{!wide ? <TicketOrderSummary model={m} width={formWidth} compact receipt /> : null}
|
|
522
|
+
</Box>
|
|
523
|
+
<Box flexDirection="row" justifyContent="space-between" alignItems={native ? "baseline" : "center"} paddingTop={native && wide ? 0.7 : 0}>
|
|
524
|
+
<Text fg={c.textDim}>{previewCredit ? "Estimated credit" : "Estimated cost"}</Text>
|
|
525
|
+
<FigureText>{money(previewCost, currency)}</FigureText>
|
|
526
|
+
</Box>
|
|
527
|
+
{/* Beside the chart, warnings are lines, so the place button fits without scrolling. */}
|
|
528
|
+
{warnings.map((warning, index) => <Notice key={index} variant={docked ? undefined : "callout"} tone="warning">{warning}</Notice>)}
|
|
529
|
+
{m.preview?.errors?.map((error, index) => <Notice key={index} variant={docked ? undefined : "callout"} tone="negative">{error}</Notice>)}
|
|
530
|
+
{live ? <TicketSection width={formWidth}><Box ref={nodeRef("typedConfirmation")} onMouseDown={() => setActive("typedConfirmation")}>
|
|
531
|
+
<FieldLabel label={`Type ${confirmationSymbol} to confirm a LIVE order`} active={active === "typedConfirmation"} />
|
|
532
|
+
<TextField variant="well" accessibleLabel={`Type ${confirmationSymbol} to confirm a LIVE order`} value={m.typedConfirmation ?? ""} onChange={(v) => onEdit("typedConfirmation", v)}
|
|
533
|
+
focused={focused && active === "typedConfirmation"} active={active === "typedConfirmation"} width={formWidth} onSubmit={() => setActive("confirm")} />
|
|
534
|
+
</Box></TicketSection> : null}
|
|
535
|
+
{m.error ? <Notice tone="negative">{m.error}</Notice> : null}
|
|
536
|
+
<Box flexDirection="column" gap={native ? 0.5 : 0}>
|
|
537
|
+
{action("back", "Edit order")}
|
|
538
|
+
{action("confirm", busy ? "Submitting..." : m.modifying ? "Confirm replacement" : live ? "Place LIVE order" : "Place simulation order",
|
|
539
|
+
!typedMatches || blockingPreview || !m.connected || !m.tradingEnabled, true, live ? "negative" : sideTone)}
|
|
540
|
+
</Box>
|
|
541
|
+
</TicketSection>{wide ? <TicketOrderSummary model={m} width={summaryWidth} receipt /> : null}</Box> : null}
|
|
542
|
+
{m.phase === "cancel-review" || m.phase === "cancelling" ? <TicketSection width={formWidth} title="Cancel this order?">
|
|
543
|
+
<TicketOrderHeadline model={m} large /><Notice variant="callout">The order can still fill before the broker confirms cancellation.</Notice>
|
|
544
|
+
<Box flexDirection="column" gap={native ? 0.5 : 0}>{action("back", "Keep order")}{action("cancel", "Confirm cancellation", false, true, "negative")}</Box>
|
|
545
|
+
</TicketSection> : null}
|
|
546
|
+
{m.phase === "result" ? <Box flexDirection={wide ? "row" : "column"} gap={3}><TicketSection width={formWidth} title={title} selectable>
|
|
547
|
+
<TicketOrderHeadline model={m} large />{strategy ? <TicketLegs model={m} width={formWidth} /> : null}
|
|
548
|
+
{m.result ? <Text fg={c.textDim}>{orderTerms(m.result, strategy ? "net" : "at", (value) => money(value, currency))}</Text> : null}
|
|
549
|
+
<TicketStatus model={m} width={formWidth} />
|
|
550
|
+
{m.resultQuantities === "last-known" ? <Text fg={c.warning}>Last known quantities</Text> : null}
|
|
551
|
+
{!["PARTIALLY_FILLED", "FILLED"].includes(resultStatus) ? <TicketReceiptRow label="Filled" value={resultQuantity(m.result?.filled)} width={formWidth} /> : null}
|
|
552
|
+
<TicketReceiptRow label="Remaining" value={resultQuantity(m.result?.remaining)} width={formWidth} />
|
|
553
|
+
<Text fg={c.textDim}>Estimates from order preview</Text>
|
|
554
|
+
<TicketReceiptRow label={previewCredit ? "Preview credit" : "Preview cost"} value={money(previewCost, currency)} width={formWidth} />
|
|
555
|
+
<TicketReceiptRow label="Fees" value={money(m.preview?.fees, currency)} width={formWidth} />
|
|
556
|
+
<TicketReceiptRow label="Commission" value={money(m.preview?.commission, m.preview?.commissionCurrency ?? currency)} width={formWidth} />
|
|
557
|
+
{m.preview?.buyingPowerAfter !== undefined ? <TicketReceiptRow label="Buying power after" value={money(m.preview.buyingPowerAfter, currency)} width={formWidth} /> : null}
|
|
558
|
+
{m.error && !["UNKNOWN", "REJECTED"].includes(resultStatus) ? <Notice variant="callout" tone="negative">{m.error}</Notice> : null}
|
|
559
|
+
{!finished && resultStatus !== "UNKNOWN" ? <Box flexDirection="row" alignItems="center" gap={1}>
|
|
560
|
+
{action("refresh", "Refresh status", false, false, "neutral", true)}<Text fg={c.textDim}>·</Text>
|
|
561
|
+
{action("orders", "Open orders", false, false, "neutral", true)}<Text fg={c.textDim}>·</Text>
|
|
562
|
+
{action("new", "New order", false, false, "neutral", true)}
|
|
563
|
+
</Box> : <Box flexDirection="column" gap={native ? 0.5 : 0}>
|
|
564
|
+
{resultStatus === "REJECTED" ? action("back", "Edit order", false, true, "neutral")
|
|
565
|
+
: finished ? action("new", "New order", false, true)
|
|
566
|
+
: action("refresh", "Refresh status", false, true, "warning")}
|
|
567
|
+
<Box flexDirection="row" flexWrap="wrap" gap={1}>{action("orders", "Open orders")}</Box>
|
|
568
|
+
</Box>}
|
|
569
|
+
</TicketSection>{wide ? <TicketOrderSummary model={m} width={summaryWidth} /> : null}</Box> : null}
|
|
570
|
+
</Box>
|
|
571
|
+
</ScrollBox>
|
|
572
|
+
</Box>;
|
|
573
|
+
}
|
|
@@ -48,7 +48,6 @@ export function useBuildoutDataRuntime({
|
|
|
48
48
|
|
|
49
49
|
async function load() {
|
|
50
50
|
loadingPageKeysRef.current.clear();
|
|
51
|
-
onBeforeLoad();
|
|
52
51
|
// A refresh keeps what is loaded; only the first load replaces the body.
|
|
53
52
|
setState((current) => current.status === "ready"
|
|
54
53
|
? { ...current, refreshing: true, refreshError: null }
|
|
@@ -57,6 +56,7 @@ export function useBuildoutDataRuntime({
|
|
|
57
56
|
const token = await getBuildoutProToken();
|
|
58
57
|
const data = await loadBuildoutData(token);
|
|
59
58
|
if (!cancelled) {
|
|
59
|
+
onBeforeLoad();
|
|
60
60
|
setState({
|
|
61
61
|
status: "ready",
|
|
62
62
|
...data,
|
|
@@ -130,6 +130,7 @@ function domainFromUrl(value: string | null | undefined) {
|
|
|
130
130
|
const hostname = new URL(value).hostname.replace(/^www\./, "");
|
|
131
131
|
return hostname || null;
|
|
132
132
|
} catch {
|
|
133
|
+
// Intentional ignore: malformed source URLs have no displayable domain.
|
|
133
134
|
return null;
|
|
134
135
|
}
|
|
135
136
|
}
|