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
|
@@ -14,24 +14,26 @@ import {
|
|
|
14
14
|
validateBrokerProfileValues,
|
|
15
15
|
type BrokerProfileDraft,
|
|
16
16
|
} from "../../../brokers/profile-form";
|
|
17
|
+
import { brokerDirectoryLogo } from "../../../brokers/logos";
|
|
17
18
|
import { getSignedInBrokers, refreshSignedInBrokers, subscribeSignedInBrokers } from "../../../brokers/signed-in/catalog";
|
|
18
19
|
import type { SignedInBroker } from "../../../brokers/signed-in/client";
|
|
19
20
|
import { connectSignedInBrokerProfile } from "../../../brokers/signed-in/connect";
|
|
20
21
|
import { SIGNED_IN_BROKER_TYPE } from "../../../brokers/signed-in/profile";
|
|
21
22
|
import { runBrokerSignIn, type BrokerSignInOutcome } from "../../../brokers/signed-in/sign-in";
|
|
22
23
|
import { BrokerConnectView, promptGloomSignIn, useBrokerSignInAttempt } from "../../../brokers/signed-in/sign-in-dialog";
|
|
23
|
-
import { Button, ListView, useFieldRing } from "../../../components";
|
|
24
|
+
import { Button, confirmDialog, ListView, useFieldRing } from "../../../components";
|
|
24
25
|
import { showCollectionInPortfolioPane } from "../../../components/command-bar/pane-actions";
|
|
25
26
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
26
27
|
|
|
27
28
|
import { listCursorMove, type ListViewItem } from "../../../components/ui/list-view";
|
|
29
|
+
import { LogoMark } from "../../../components/ui/logo-mark";
|
|
28
30
|
import { t, tf } from "../../../i18n";
|
|
29
31
|
import { useShortcut, type KeyEventLike } from "../../../react/input";
|
|
30
32
|
import { useAppDispatch, useAppGetState } from "../../../state/app/context";
|
|
31
33
|
import { colors } from "../../../theme/colors";
|
|
32
34
|
import type { BrokerAdapter } from "../../../types/broker";
|
|
33
35
|
import type { BrokerInstanceConfig } from "../../../types/config";
|
|
34
|
-
import { Box, ScrollBox, Text, type ScrollBoxRenderable } from "../../../ui";
|
|
36
|
+
import { Box, ScrollBox, Text, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
|
|
35
37
|
import { useDialog } from "../../../ui/dialog";
|
|
36
38
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
37
39
|
import { usePluginAppActions, usePluginBrokerActions } from "../../runtime";
|
|
@@ -191,7 +193,13 @@ export function useBrokerAddFlow({
|
|
|
191
193
|
setBusy(t("Connecting broker…"));
|
|
192
194
|
let instanceId: string;
|
|
193
195
|
try {
|
|
194
|
-
const
|
|
196
|
+
const config = buildBrokerProfileConfig(adapter, draft.values);
|
|
197
|
+
const proposed = { id: "new-profile", brokerType: adapter.id, label, enabled: true, config };
|
|
198
|
+
if (adapter.getTradingCapabilities?.(proposed).enabled && !await confirmDialog(dialog, {
|
|
199
|
+
title: "Enable trading?", body: "Orders can commit real money. Start with a simulation account. Every order needs review and confirmation; LIVE orders also need a typed confirmation. You can turn trading off in Brokers at any time.",
|
|
200
|
+
confirmLabel: "Enable trading", confirmVariant: "danger",
|
|
201
|
+
})) { setBusy(null); return; }
|
|
202
|
+
const instance = await createBrokerInstance(adapter.id, label, config, { localOnly: draft.localOnly });
|
|
195
203
|
instanceId = instance.id;
|
|
196
204
|
} catch (error) {
|
|
197
205
|
setBusy(null);
|
|
@@ -202,7 +210,7 @@ export function useBrokerAddFlow({
|
|
|
202
210
|
} finally {
|
|
203
211
|
committingRef.current = false;
|
|
204
212
|
}
|
|
205
|
-
}, [createBrokerInstance, setBusy, setMessage, syncNewProfile]);
|
|
213
|
+
}, [createBrokerInstance, dialog, setBusy, setMessage, syncNewProfile]);
|
|
206
214
|
|
|
207
215
|
/** Shows one connect attempt in the pane until it ends, then says how. */
|
|
208
216
|
const showAttempt = useCallback((flowId: number, write: boolean) => new Promise<BrokerSignInOutcome>((resolve) => {
|
|
@@ -360,6 +368,8 @@ export function BrokerAddFlowContent({
|
|
|
360
368
|
scope: string;
|
|
361
369
|
}) {
|
|
362
370
|
const { flow } = addFlow;
|
|
371
|
+
const desktop = useUiCapabilities().nativePaneChrome === true;
|
|
372
|
+
const { getBrokerLogo } = usePluginBrokerActions();
|
|
363
373
|
if (!flow) return <Box flexGrow={1} />;
|
|
364
374
|
const { step } = flow;
|
|
365
375
|
|
|
@@ -369,7 +379,15 @@ export function BrokerAddFlowContent({
|
|
|
369
379
|
focused={focused}
|
|
370
380
|
label={addFlow.title}
|
|
371
381
|
cursor={flow.cursor}
|
|
372
|
-
items={flow.directory.map((entry) => ({
|
|
382
|
+
items={flow.directory.map((entry) => ({
|
|
383
|
+
id: entry.key,
|
|
384
|
+
label: entry.name,
|
|
385
|
+
// The desktop says how it connects under the name, beside its logo.
|
|
386
|
+
...(desktop
|
|
387
|
+
? { description: brokerMethodSummary(entry), leading: <LogoMark src={brokerDirectoryLogo(entry, getBrokerLogo)} name={entry.name} size="lead" /> }
|
|
388
|
+
: { detail: brokerMethodSummary(entry) }),
|
|
389
|
+
}))}
|
|
390
|
+
rowDescriptions={desktop}
|
|
373
391
|
emptyMessage="No connectable brokers are installed."
|
|
374
392
|
onCursorChange={addFlow.setCursor}
|
|
375
393
|
onChoose={(index) => {
|
|
@@ -446,6 +464,7 @@ function ChoiceList({
|
|
|
446
464
|
label,
|
|
447
465
|
cursor,
|
|
448
466
|
items,
|
|
467
|
+
rowDescriptions,
|
|
449
468
|
emptyMessage,
|
|
450
469
|
onCursorChange,
|
|
451
470
|
onChoose,
|
|
@@ -454,10 +473,12 @@ function ChoiceList({
|
|
|
454
473
|
label: string;
|
|
455
474
|
cursor: number;
|
|
456
475
|
items: ListViewItem[];
|
|
476
|
+
rowDescriptions?: boolean;
|
|
457
477
|
emptyMessage?: string;
|
|
458
478
|
onCursorChange: (index: number) => void;
|
|
459
479
|
onChoose: (index: number) => void;
|
|
460
480
|
}) {
|
|
481
|
+
const desktop = useUiCapabilities().nativePaneChrome === true;
|
|
461
482
|
const selectedIndex = Math.min(Math.max(0, cursor), Math.max(0, items.length - 1));
|
|
462
483
|
useShortcut((event: KeyEventLike) => {
|
|
463
484
|
const move = listCursorMove(event, 10);
|
|
@@ -475,7 +496,8 @@ function ChoiceList({
|
|
|
475
496
|
}, { enabled: focused });
|
|
476
497
|
|
|
477
498
|
return (
|
|
478
|
-
|
|
499
|
+
// The desktop keeps the pane's inset above the first row, as on its sides.
|
|
500
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1} style={desktop ? { paddingTop: "var(--gloom-inset)" } : undefined}>
|
|
479
501
|
<ListView
|
|
480
502
|
items={items}
|
|
481
503
|
selectedIndex={selectedIndex}
|
|
@@ -485,6 +507,11 @@ function ChoiceList({
|
|
|
485
507
|
scrollable
|
|
486
508
|
flexGrow={1}
|
|
487
509
|
remoteLabel={label}
|
|
510
|
+
rowDescriptions={rowDescriptions}
|
|
511
|
+
// The desktop sets the rows on the pane at its inset, and the pointer
|
|
512
|
+
// moves the one highlight the keys move. A two-line row has its own
|
|
513
|
+
// room above and below, so those rows sit one under the next.
|
|
514
|
+
{...(desktop ? { surface: "plain" as const, bgColor: "transparent", selectOnHover: true, ...(rowDescriptions ? { rowGap: 0 } : {}) } : {})}
|
|
488
515
|
/>
|
|
489
516
|
</Box>
|
|
490
517
|
);
|
|
@@ -508,9 +535,9 @@ function DeviceProfileForm({
|
|
|
508
535
|
}) {
|
|
509
536
|
const { adapter, draft, activeKey } = step;
|
|
510
537
|
const fields = getVisibleBrokerConfigFields(adapter, draft.values);
|
|
511
|
-
const keys = useMemo<BrokerEditKey[]>(() => ["label", ...fields.map((field) => field.key)], [fields]);
|
|
538
|
+
const keys = useMemo<BrokerEditKey[]>(() => ["label", ...fields.map((field) => field.key), "localOnly"], [fields]);
|
|
512
539
|
const selectKeys = useMemo<ReadonlySet<BrokerEditKey>>(
|
|
513
|
-
() => new Set(fields.filter((field) => field.type === "select").map((field) => field.key)),
|
|
540
|
+
() => new Set(["localOnly", ...fields.filter((field) => field.type === "select").map((field) => field.key)]),
|
|
514
541
|
[fields],
|
|
515
542
|
);
|
|
516
543
|
const { updateDraft, connectDevice, leave } = addFlow;
|
|
@@ -533,6 +560,10 @@ function DeviceProfileForm({
|
|
|
533
560
|
}, [activeKey, keys, updateDraft]);
|
|
534
561
|
|
|
535
562
|
const cycleSelect = useCallback((key: BrokerEditKey, direction: -1 | 1) => {
|
|
563
|
+
if (key === "localOnly") {
|
|
564
|
+
updateDraft((current) => ({ draft: { ...current.draft, localOnly: !current.draft.localOnly } }));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
536
567
|
const options = fields.find((field) => field.key === key)?.options ?? [];
|
|
537
568
|
if (options.length === 0) return;
|
|
538
569
|
updateDraft((current) => {
|
|
@@ -570,6 +601,7 @@ function DeviceProfileForm({
|
|
|
570
601
|
rowRef={rowRef}
|
|
571
602
|
onActiveKeyChange={setActiveKey}
|
|
572
603
|
onLabelChange={(label) => updateDraft((current) => ({ draft: { ...current.draft, label } }))}
|
|
604
|
+
onLocalOnlyChange={(localOnly) => updateDraft((current) => ({ draft: { ...current.draft, localOnly } }))}
|
|
573
605
|
onValueChange={(key, value) => updateDraft((current) => ({
|
|
574
606
|
draft: { ...current.draft, values: { ...current.draft.values, [key]: value } },
|
|
575
607
|
}))}
|
|
@@ -17,7 +17,7 @@ import type { BrokerManagerMessage } from "./pane-actions";
|
|
|
17
17
|
import { signInEndNote } from "./sign-in-end";
|
|
18
18
|
import { stateColor } from "./table";
|
|
19
19
|
|
|
20
|
-
export type BrokerEditKey = "label" | "enabled" | string;
|
|
20
|
+
export type BrokerEditKey = "label" | "enabled" | "localOnly" | string;
|
|
21
21
|
|
|
22
22
|
type RowRef = (node: BoxRenderable | null) => void;
|
|
23
23
|
|
|
@@ -116,6 +116,7 @@ export function BrokerProfileForm({
|
|
|
116
116
|
onActiveKeyChange,
|
|
117
117
|
onLabelChange,
|
|
118
118
|
onEnabledChange,
|
|
119
|
+
onLocalOnlyChange,
|
|
119
120
|
onValueChange,
|
|
120
121
|
onSubmit,
|
|
121
122
|
onCancel,
|
|
@@ -139,6 +140,7 @@ export function BrokerProfileForm({
|
|
|
139
140
|
onLabelChange: (label: string) => void;
|
|
140
141
|
/** Editing only: a new profile starts enabled. */
|
|
141
142
|
onEnabledChange?: (enabled: boolean) => void;
|
|
143
|
+
onLocalOnlyChange: (localOnly: boolean) => void;
|
|
142
144
|
onValueChange: (key: string, value: string) => void;
|
|
143
145
|
onSubmit: () => void;
|
|
144
146
|
onCancel: () => void;
|
|
@@ -188,6 +190,25 @@ export function BrokerProfileForm({
|
|
|
188
190
|
onSubmit={onSubmit}
|
|
189
191
|
/>
|
|
190
192
|
))}
|
|
193
|
+
<Box ref={rowRef("localOnly")} flexDirection="column" onMouseDown={() => onActiveKeyChange("localOnly")}>
|
|
194
|
+
<FieldLabel label={t("Account data")} active={activeKey === "localOnly"} />
|
|
195
|
+
<SegmentedControl
|
|
196
|
+
value={draft.localOnly ? "local" : "sync"}
|
|
197
|
+
focused={paneFocused && activeKey === "localOnly"}
|
|
198
|
+
options={[
|
|
199
|
+
{ label: t("Sync with Gloom Cloud"), value: "sync" },
|
|
200
|
+
{ label: t("Keep on this device"), value: "local" },
|
|
201
|
+
]}
|
|
202
|
+
onChange={(value) => onLocalOnlyChange(value === "local")}
|
|
203
|
+
allowEditable
|
|
204
|
+
shortcutScope={scope}
|
|
205
|
+
/>
|
|
206
|
+
{draft.localOnly ? (
|
|
207
|
+
<Text fg={colors.textMuted} wrapText>
|
|
208
|
+
{t("Daily roundups, the weekly brief, position alerts, the mobile app and Ask Gloom will go without this account.")}
|
|
209
|
+
</Text>
|
|
210
|
+
) : null}
|
|
211
|
+
</Box>
|
|
191
212
|
<Box flexDirection="row" gap={1}>
|
|
192
213
|
<Button label={submitLabel} shortcut="Enter" variant="primary" onPress={onSubmit} disabled={!!busy} />
|
|
193
214
|
<Button label={t("Cancel")} shortcut="Esc" variant="secondary" onPress={onCancel} disabled={!!busy} />
|
|
@@ -216,6 +237,7 @@ export function BrokerDetailContent({
|
|
|
216
237
|
onActiveEditKeyChange,
|
|
217
238
|
onDraftLabelChange,
|
|
218
239
|
onDraftEnabledChange,
|
|
240
|
+
onDraftLocalOnlyChange,
|
|
219
241
|
onDraftValueChange,
|
|
220
242
|
onSaveEdit,
|
|
221
243
|
onCancelEdit,
|
|
@@ -237,6 +259,7 @@ export function BrokerDetailContent({
|
|
|
237
259
|
onActiveEditKeyChange: (key: BrokerEditKey) => void;
|
|
238
260
|
onDraftLabelChange: (label: string) => void;
|
|
239
261
|
onDraftEnabledChange: (enabled: boolean) => void;
|
|
262
|
+
onDraftLocalOnlyChange: (localOnly: boolean) => void;
|
|
240
263
|
onDraftValueChange: (key: string, value: string) => void;
|
|
241
264
|
onSaveEdit: () => void;
|
|
242
265
|
onCancelEdit: () => void;
|
|
@@ -282,6 +305,7 @@ export function BrokerDetailContent({
|
|
|
282
305
|
<Text fg={colors.warning} width={width} wrapText>{signInEndNote(row.signInEndsAt)}</Text>
|
|
283
306
|
)}
|
|
284
307
|
<Text fg={colors.textMuted}>{tf("Last sync {time}", { time: formatRelativeAge(row.lastSyncedAt) })}</Text>
|
|
308
|
+
{row.instance.localOnly ? <Text fg={colors.textMuted}>{t("Account data stays on this device, out of Gloom Cloud")}</Text> : null}
|
|
285
309
|
<Text fg={colors.textMuted}>{tf("Status updated {time}", { time: formatRelativeAge(row.updatedAt) })}</Text>
|
|
286
310
|
<Box height={1} />
|
|
287
311
|
|
|
@@ -303,6 +327,7 @@ export function BrokerDetailContent({
|
|
|
303
327
|
onActiveKeyChange={onActiveEditKeyChange}
|
|
304
328
|
onLabelChange={onDraftLabelChange}
|
|
305
329
|
onEnabledChange={onDraftEnabledChange}
|
|
330
|
+
onLocalOnlyChange={onDraftLocalOnlyChange}
|
|
306
331
|
onValueChange={onDraftValueChange}
|
|
307
332
|
onSubmit={onSaveEdit}
|
|
308
333
|
onCancel={onCancelEdit}
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "../../../state/app/context";
|
|
24
24
|
import type { BrokerAdapter } from "../../../types/broker";
|
|
25
25
|
import type { PaneProps } from "../../../types/plugin";
|
|
26
|
-
import { Box } from "../../../ui";
|
|
26
|
+
import { Box, useUiCapabilities } from "../../../ui";
|
|
27
27
|
import { getCurrentPluginTarget } from "../../current-target";
|
|
28
28
|
import { usePluginBrokerActions, usePluginPaneState } from "../../runtime";
|
|
29
29
|
import type { PluginModule } from "../plugin-module";
|
|
@@ -50,7 +50,8 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
|
|
|
50
50
|
const config = usePaneAppConfig();
|
|
51
51
|
const paneId = usePaneInstanceId();
|
|
52
52
|
const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
|
|
53
|
-
const { getBrokerAdapter } = usePluginBrokerActions();
|
|
53
|
+
const { getBrokerAdapter, getBrokerLogo } = usePluginBrokerActions();
|
|
54
|
+
const desktop = useUiCapabilities().nativePaneChrome === true;
|
|
54
55
|
// The profile is remembered by id so a reload lands on the same row.
|
|
55
56
|
const [selectedId, setSelectedId] = usePluginPaneState<string | null>("selectedId", null);
|
|
56
57
|
const [detailOpen, setDetailOpen] = usePluginPaneState<boolean>("detailOpen", false);
|
|
@@ -81,8 +82,8 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
|
|
|
81
82
|
const signedInBrokers = useSyncExternalStore(subscribeSignedInBrokers, getSignedInBrokers, getSignedInBrokers);
|
|
82
83
|
const { ends: signInEnds, reload: reloadSignInEnds } = useSignInEnds(config.brokerInstances);
|
|
83
84
|
const rows = useMemo(
|
|
84
|
-
() => buildBrokerProfileRows(config, adapters, brokerAccounts, signInEnds),
|
|
85
|
-
[adapters, brokerAccounts, config, language, signInEnds, signedInBrokers, statusVersion],
|
|
85
|
+
() => buildBrokerProfileRows(config, adapters, brokerAccounts, signInEnds, getBrokerLogo),
|
|
86
|
+
[adapters, brokerAccounts, config, getBrokerLogo, language, signInEnds, signedInBrokers, statusVersion],
|
|
86
87
|
);
|
|
87
88
|
const selectedIndex = Math.max(0, rows.findIndex((row) => row.id === selectedId));
|
|
88
89
|
const selectedRow = rows[selectedIndex] ?? null;
|
|
@@ -91,11 +92,11 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
|
|
|
91
92
|
? getVisibleBrokerConfigFields(selectedRow.adapter, editDraft.values)
|
|
92
93
|
: [];
|
|
93
94
|
const editKeys = useMemo<BrokerEditKey[]>(
|
|
94
|
-
() => ["label", "enabled", ...editFields.map((field) => field.key)],
|
|
95
|
+
() => ["label", "enabled", ...editFields.map((field) => field.key), "localOnly"],
|
|
95
96
|
[editFields],
|
|
96
97
|
);
|
|
97
98
|
const editSelectKeys = useMemo<ReadonlySet<BrokerEditKey>>(
|
|
98
|
-
() => new Set(["enabled", ...editFields.filter((field) => field.type === "select").map((field) => field.key)]),
|
|
99
|
+
() => new Set(["enabled", "localOnly", ...editFields.filter((field) => field.type === "select").map((field) => field.key)]),
|
|
99
100
|
[editFields],
|
|
100
101
|
);
|
|
101
102
|
const editScope = `broker-edit:${paneId}`;
|
|
@@ -200,8 +201,8 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
|
|
|
200
201
|
});
|
|
201
202
|
|
|
202
203
|
const cycleEditSelect = useCallback((key: BrokerEditKey, direction: -1 | 1) => {
|
|
203
|
-
if (key === "enabled") {
|
|
204
|
-
setEditDraft((current) => current ? { ...current,
|
|
204
|
+
if (key === "enabled" || key === "localOnly") {
|
|
205
|
+
setEditDraft((current) => current ? { ...current, [key]: !current[key] } : current);
|
|
205
206
|
return;
|
|
206
207
|
}
|
|
207
208
|
const options = editFields.find((field) => field.key === key)?.options ?? [];
|
|
@@ -229,6 +230,11 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
|
|
|
229
230
|
const tableWidth = Math.max(24, width);
|
|
230
231
|
const columns = useMemo(() => buildBrokerColumns(tableWidth), [language, tableWidth]);
|
|
231
232
|
|
|
233
|
+
const renderCell = useCallback(
|
|
234
|
+
(row: BrokerProfileRow, column: BrokerColumn) => renderBrokerCell(row, column, desktop),
|
|
235
|
+
[desktop],
|
|
236
|
+
);
|
|
237
|
+
|
|
232
238
|
const openSelectedDetail = useCallback((_index: number, row: BrokerProfileRow) => {
|
|
233
239
|
setSelectedId(row.id);
|
|
234
240
|
setEditDraft(null);
|
|
@@ -256,6 +262,10 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
|
|
|
256
262
|
setEditDraft((current) => current ? { ...current, enabled } : current);
|
|
257
263
|
}, []);
|
|
258
264
|
|
|
265
|
+
const updateDraftLocalOnly = useCallback((localOnly: boolean) => {
|
|
266
|
+
setEditDraft((current) => current ? { ...current, localOnly } : current);
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
259
269
|
const saveCurrentEdit = useCallback(() => {
|
|
260
270
|
saveEdit().catch(() => {});
|
|
261
271
|
}, [saveEdit]);
|
|
@@ -288,6 +298,7 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
|
|
|
288
298
|
onActiveEditKeyChange={setActiveEditKey}
|
|
289
299
|
onDraftLabelChange={updateDraftLabel}
|
|
290
300
|
onDraftEnabledChange={updateDraftEnabled}
|
|
301
|
+
onDraftLocalOnlyChange={updateDraftLocalOnly}
|
|
291
302
|
onDraftValueChange={updateDraftValue}
|
|
292
303
|
onSaveEdit={saveCurrentEdit}
|
|
293
304
|
onCancelEdit={cancelEdit}
|
|
@@ -319,7 +330,7 @@ export function BrokersPane({ focused, width, height }: PaneProps) {
|
|
|
319
330
|
sortColumnId={null}
|
|
320
331
|
sortDirection="asc"
|
|
321
332
|
getItemKey={(row) => row.id}
|
|
322
|
-
renderCell={
|
|
333
|
+
renderCell={renderCell}
|
|
323
334
|
// The footer's a adds one.
|
|
324
335
|
emptyContent={(
|
|
325
336
|
<Box width="100%" paddingX={1} paddingY={1}>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { brokerProfileLogo, type BrokerPluginLogo } from "../../../brokers/logos";
|
|
1
2
|
import type { BrokerAdapter, BrokerConnectionStatus } from "../../../types/broker";
|
|
2
3
|
import type { AppConfig, BrokerInstanceConfig } from "../../../types/config";
|
|
3
4
|
import type { BrokerAccount } from "../../../types/trading";
|
|
@@ -27,6 +28,8 @@ export interface BrokerProfileRow {
|
|
|
27
28
|
accountIds: string[];
|
|
28
29
|
/** When the broker ends this profile's sign-in, once Gloom says it is time to connect again. */
|
|
29
30
|
signInEndsAt: number | null;
|
|
31
|
+
/** The broker's logo for the desktop and the web (brokers/logos.ts); null draws its initial. */
|
|
32
|
+
logo: string | null;
|
|
30
33
|
instance: BrokerInstanceConfig;
|
|
31
34
|
adapter: BrokerAdapter | null;
|
|
32
35
|
}
|
|
@@ -111,6 +114,7 @@ export function buildBrokerProfileRows(
|
|
|
111
114
|
adapters: ReadonlyMap<string, BrokerAdapter | null>,
|
|
112
115
|
brokerAccounts: Record<string, BrokerAccount[]>,
|
|
113
116
|
signInEnds: SignInEnds = new Map(),
|
|
117
|
+
pluginLogo: BrokerPluginLogo = () => null,
|
|
114
118
|
): BrokerProfileRow[] {
|
|
115
119
|
return config.brokerInstances.map((instance) => {
|
|
116
120
|
const adapter = adapters.get(instance.brokerType) ?? null;
|
|
@@ -147,6 +151,7 @@ export function buildBrokerProfileRows(
|
|
|
147
151
|
signInEndsAt: state.state === "error" || state.state === "disabled" || state.state === "unavailable"
|
|
148
152
|
? null
|
|
149
153
|
: signInEnds.get(instance.id) ?? null,
|
|
154
|
+
logo: brokerProfileLogo(instance, adapter, pluginLogo),
|
|
150
155
|
instance,
|
|
151
156
|
adapter,
|
|
152
157
|
};
|
|
@@ -93,9 +93,16 @@ export function useBrokerManagerActions({
|
|
|
93
93
|
try {
|
|
94
94
|
setBusy(t("Saving…"));
|
|
95
95
|
const nextConfig = buildBrokerProfileConfig(selectedRow.adapter, editDraft.values, selectedRow.instance);
|
|
96
|
+
const wasEnabled = selectedRow.adapter.getTradingCapabilities?.(selectedRow.instance).enabled === true;
|
|
97
|
+
const willEnable = selectedRow.adapter.getTradingCapabilities?.({ ...selectedRow.instance, config: nextConfig }).enabled === true;
|
|
98
|
+
if (!wasEnabled && willEnable && !await confirmDialog(dialog, {
|
|
99
|
+
title: "Enable trading?", body: "Orders can commit real money. Start with a simulation account. Every order needs review and confirmation; LIVE orders also need a typed confirmation. You can turn trading off in this profile at any time.",
|
|
100
|
+
confirmLabel: "Enable trading", confirmVariant: "danger",
|
|
101
|
+
})) return;
|
|
96
102
|
await updateBrokerInstance(selectedRow.id, nextConfig, {
|
|
97
103
|
label,
|
|
98
104
|
enabled: editDraft.enabled,
|
|
105
|
+
localOnly: editDraft.localOnly,
|
|
99
106
|
replaceConfig: true,
|
|
100
107
|
});
|
|
101
108
|
setEditDraft(null);
|
|
@@ -105,7 +112,7 @@ export function useBrokerManagerActions({
|
|
|
105
112
|
} finally {
|
|
106
113
|
setBusy(null);
|
|
107
114
|
}
|
|
108
|
-
}, [editDraft, selectedRow, setEditDraft, updateBrokerInstance]);
|
|
115
|
+
}, [dialog, editDraft, selectedRow, setEditDraft, updateBrokerInstance]);
|
|
109
116
|
|
|
110
117
|
const connectSelected = useCallback(async () => {
|
|
111
118
|
if (!selectedRow) return;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
1
2
|
import { TextAttributes } from "../../../ui";
|
|
3
|
+
import { LogoMark } from "../../../components/ui/logo-mark";
|
|
2
4
|
import type { DataTableCell, DataTableColumn } from "../../../components";
|
|
3
5
|
import { colors } from "../../../theme/colors";
|
|
4
6
|
import { formatRelativeAge } from "../../../utils/datetime-format";
|
|
@@ -37,13 +39,15 @@ export function buildBrokerColumns(width: number): BrokerColumn[] {
|
|
|
37
39
|
];
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
/** `desktop`: the profile leads with its broker's logo, which the terminal does not draw. */
|
|
43
|
+
export function renderBrokerCell(row: BrokerProfileRow, column: BrokerColumn, desktop = false): DataTableCell {
|
|
41
44
|
switch (column.id) {
|
|
42
45
|
case "profile":
|
|
43
46
|
return {
|
|
44
47
|
text: row.label,
|
|
45
48
|
color: colors.text,
|
|
46
49
|
attributes: TextAttributes.BOLD,
|
|
50
|
+
leading: desktop ? createElement(LogoMark, { src: row.logo, name: row.brokerName }) : undefined,
|
|
47
51
|
};
|
|
48
52
|
case "status":
|
|
49
53
|
// A sign-in the broker is about to end replaces "Connected" until it is renewed.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useCallback, useImperativeHandle, useMemo, useRef, useState, type Ref } from "react";
|
|
2
|
+
import { Badge, Button, DialogFrame, DisclosureMarker, ListView, type SelectControl } from "../../../components";
|
|
3
|
+
import { listCursorMove } from "../../../components/ui/list-view";
|
|
4
|
+
import { useThemeColors } from "../../../theme/theme-context";
|
|
5
|
+
import { blendForContrast, higherContrast } from "../../../theme/color-utils";
|
|
6
|
+
import type { BrokerTradingMode } from "../../../types/trading";
|
|
7
|
+
import { Box, Text, TextAttributes, useUiCapabilities } from "../../../ui";
|
|
8
|
+
import { useDialog, useDialogKeyboard, type PromptContext } from "../../../ui/dialog";
|
|
9
|
+
|
|
10
|
+
export interface BrokerAccountChoice {
|
|
11
|
+
value: string;
|
|
12
|
+
label: string;
|
|
13
|
+
profileLabel?: string;
|
|
14
|
+
tradingMode?: BrokerTradingMode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BrokerAccountPickerProps {
|
|
18
|
+
accounts: readonly BrokerAccountChoice[];
|
|
19
|
+
value?: string;
|
|
20
|
+
onChange(value: string): void;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
controlRef?: Ref<SelectControl>;
|
|
23
|
+
active?: boolean;
|
|
24
|
+
width?: number;
|
|
25
|
+
showMode?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function accountModeLabel(mode: BrokerTradingMode | undefined): string {
|
|
29
|
+
return mode === "simulation" ? "SIMULATION" : mode === "live" ? "LIVE" : "MODE UNKNOWN";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function BrokerAccountChoiceDialog({ accounts, value, resolve }: PromptContext<string> & Pick<BrokerAccountPickerProps, "accounts" | "value">) {
|
|
33
|
+
const colors = useThemeColors();
|
|
34
|
+
const [index, setIndex] = useState(() => accounts.findIndex((account) => account.value === value));
|
|
35
|
+
// Read the latest key movement, not the last rendered account, on Enter.
|
|
36
|
+
const cursor = useRef(index);
|
|
37
|
+
const selectIndex = (next: number) => { cursor.current = next; setIndex(next); };
|
|
38
|
+
useDialogKeyboard((event) => {
|
|
39
|
+
event.stopPropagation();
|
|
40
|
+
const move = listCursorMove(event, 5);
|
|
41
|
+
if (move) { event.preventDefault(); selectIndex(move(accounts, cursor.current)); }
|
|
42
|
+
else if (event.name === "return" || event.name === "enter") { event.preventDefault(); const selected = accounts[cursor.current]; if (selected) resolve(selected.value); }
|
|
43
|
+
else if (event.name === "escape") { event.preventDefault(); resolve(""); }
|
|
44
|
+
}, { allowEditable: true });
|
|
45
|
+
return <DialogFrame title="Choose an account" subtitle="Simulation accounts first. Choose before trading." footer="↑↓ choose · Enter select · Esc back" onClose={() => resolve("")}>
|
|
46
|
+
<Box width={64} flexDirection="column">
|
|
47
|
+
<ListView items={accounts.map((account) => ({ id: account.value, label: account.label, detail: accountModeLabel(account.tradingMode) }))}
|
|
48
|
+
selectedIndex={index} onSelect={selectIndex} onActivate={(item) => resolve(item.id)} selectOnHover
|
|
49
|
+
height={Math.min(Math.max(accounts.length, 1), 6) * 2} rowHeight={2} rowGap={0} scrollable={accounts.length > 6}
|
|
50
|
+
surface="framed" emptyMessage="No accounts available." remoteLabel="Broker accounts"
|
|
51
|
+
renderRow={(_item, state, rowIndex) => {
|
|
52
|
+
const account = accounts[rowIndex]!;
|
|
53
|
+
return <Box flexDirection="row" alignItems="flex-start" gap={1} width="100%" minWidth={0}>
|
|
54
|
+
<Box width={1}>{state.selected ? <DisclosureMarker expanded={false} color={colors.textBright} /> : null}</Box>
|
|
55
|
+
<Box flexDirection="column" flexGrow={1} minWidth={0}>
|
|
56
|
+
<Text fg={colors.text} attributes={state.selected ? TextAttributes.BOLD : undefined} truncate wrapMode="none">{account.label}</Text>
|
|
57
|
+
<Text fg={colors.textDim} truncate wrapMode="none">{account.profileLabel ?? "Broker account"}</Text>
|
|
58
|
+
</Box>
|
|
59
|
+
<Box flexShrink={0}><Badge label={accountModeLabel(account.tradingMode)} tone={account.tradingMode === "simulation" ? "accent" : "negative"} /></Box>
|
|
60
|
+
</Box>;
|
|
61
|
+
}} />
|
|
62
|
+
</Box>
|
|
63
|
+
</DialogFrame>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Keeps native and terminal choices visually identical, including account mode badges. */
|
|
67
|
+
export function BrokerAccountPicker({ accounts, value, onChange, disabled = false, controlRef, active, width, showMode = false }: BrokerAccountPickerProps) {
|
|
68
|
+
const native = useUiCapabilities().nativePaneChrome;
|
|
69
|
+
const dialog = useDialog();
|
|
70
|
+
const colors = useThemeColors();
|
|
71
|
+
const choices = useMemo(() => [...accounts].sort((a, b) => Number(b.tradingMode === "simulation") - Number(a.tradingMode === "simulation")), [accounts]);
|
|
72
|
+
const current = accounts.find((account) => account.value === value);
|
|
73
|
+
const modeColor = blendForContrast(current?.tradingMode === "simulation" ? colors.positive : colors.negative, colors.bg, higherContrast("#000000", "#ffffff", colors.bg), 4.5);
|
|
74
|
+
const choose = useRef<(next: string | undefined) => void>(() => {});
|
|
75
|
+
choose.current = (next) => { if (!disabled && next && next !== value && accounts.some((account) => account.value === next)) onChange(next); };
|
|
76
|
+
const opened = useRef(false);
|
|
77
|
+
const open = useCallback(() => {
|
|
78
|
+
if (disabled || opened.current || !choices.length) return;
|
|
79
|
+
opened.current = true;
|
|
80
|
+
void dialog.prompt<string>({ closeOnClickOutside: true, style: { width: 72 }, content: (context) => <BrokerAccountChoiceDialog {...context} accounts={choices} value={value} /> })
|
|
81
|
+
.then((next) => choose.current(next)).catch(() => {}).finally(() => { opened.current = false; });
|
|
82
|
+
}, [choices, dialog, disabled, value]);
|
|
83
|
+
useImperativeHandle(controlRef, () => ({ open }), [open]);
|
|
84
|
+
return <Button label="Choose account" variant="quiet" flush active={active} disabled={disabled || !choices.length} width={width} onPress={open} stopPropagation>
|
|
85
|
+
<Box flexDirection="row" gap={1} flexGrow={1} minWidth={0} alignItems="center">
|
|
86
|
+
<Text fg={disabled ? colors.textMuted : colors.text} truncate wrapMode="none" flexShrink={1} minWidth={0}>{`${active && !native ? "> " : ""}${current?.label ?? "Choose an account"}`}</Text>
|
|
87
|
+
{showMode && current ? <Text fg={modeColor} attributes={TextAttributes.BOLD}>{current.tradingMode === "simulation" ? "SIM" : accountModeLabel(current.tradingMode)}</Text> : null}
|
|
88
|
+
<DisclosureMarker expanded color={colors.textDim} />
|
|
89
|
+
</Box>
|
|
90
|
+
</Button>;
|
|
91
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CompositeChartLevel } from "../../../components/chart/composite/levels";
|
|
2
|
+
import type { ThemeColors } from "../../../theme/colors";
|
|
3
|
+
import type { BrokerContractRef } from "../../../types/instrument";
|
|
4
|
+
import type { BrokerOrder, BrokerOrderRequest } from "../../../types/trading";
|
|
5
|
+
import { brokerOrderKey, brokerOrderStatus, canChangeBrokerOrder, ticketEditingPriceStep, withLatestOrder } from "./orders-data";
|
|
6
|
+
import { sameBrokerContract } from "../../../utils/instrument-identity";
|
|
7
|
+
import { isStrategyOrder } from "./strategy";
|
|
8
|
+
|
|
9
|
+
export function ticketOrderMatches(order: Pick<BrokerOrder, "accountId" | "brokerInstanceId" | "contract" | "legs">, contract: BrokerContractRef, profileId: string, accountId?: string): boolean {
|
|
10
|
+
if (!accountId || order.accountId !== accountId || order.brokerInstanceId && order.brokerInstanceId !== profileId
|
|
11
|
+
|| order.contract.brokerInstanceId && order.contract.brokerInstanceId !== profileId || order.contract.brokerId !== contract.brokerId) return false;
|
|
12
|
+
const held = order.contract;
|
|
13
|
+
// A spread is its underlying's order: the stock's ticket and a spread ticket on it both list it.
|
|
14
|
+
const spreadTicket = contract.secType?.toUpperCase() === "BAG";
|
|
15
|
+
if (isStrategyOrder(order) || spreadTicket) {
|
|
16
|
+
return isStrategyOrder(order) && held.symbol.trim().toUpperCase() === contract.symbol.trim().toUpperCase()
|
|
17
|
+
&& (spreadTicket || !contract.secType || ["STK", "ETF"].includes(contract.secType.toUpperCase()));
|
|
18
|
+
}
|
|
19
|
+
return sameBrokerContract(held, contract);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A read-only projection. No broker operation is attached to a chart level. */
|
|
23
|
+
export function ticketChartLevels({ draft, editing, orders, latestOrder, avgCost, position, colors, priceStep, priceDecimals }: {
|
|
24
|
+
draft?: BrokerOrderRequest; editing: boolean; orders: readonly BrokerOrder[]; latestOrder?: BrokerOrder;
|
|
25
|
+
avgCost?: number; position?: number; colors: ThemeColors; priceStep?: number; priceDecimals?: number;
|
|
26
|
+
}): CompositeChartLevel[] {
|
|
27
|
+
const levels: CompositeChartLevel[] = [];
|
|
28
|
+
// The broker's tick, or the ticket's editing step: a cent, a hundredth of a cent below a dollar. Accepted
|
|
29
|
+
// decimals are a limit, not a tick: five of them do not make $719.60745 an average anyone reads.
|
|
30
|
+
const tick = (value: number) => priceStep !== undefined && Number.isFinite(priceStep) && priceStep > 0 ? priceStep : ticketEditingPriceStep(value, priceDecimals);
|
|
31
|
+
// An order price shows every digit it was entered with; an average is arithmetic, so it rounds to the tick.
|
|
32
|
+
const add = (id: string, value: number | undefined, label: string, color: string, dashed = false, averaged = false) => {
|
|
33
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) return;
|
|
34
|
+
const minimumFractionDigits = Math.min(18, tick(value).toLocaleString("en-US", { useGrouping: false, maximumFractionDigits: 18 }).split(".")[1]?.length ?? 0);
|
|
35
|
+
const maximumFractionDigits = averaged ? minimumFractionDigits : Math.max(8, minimumFractionDigits);
|
|
36
|
+
const axisLabel = value.toLocaleString("en-US", { minimumFractionDigits, maximumFractionDigits });
|
|
37
|
+
levels.push({ id, value, axisLabel, label, color, editable: false, actionable: false, dashed });
|
|
38
|
+
};
|
|
39
|
+
if (draft && editing) {
|
|
40
|
+
const color = draft.action === "SELL" ? colors.negative : colors.positive;
|
|
41
|
+
if (["LMT", "STP LMT"].includes(draft.orderType)) add("ticket:limit", draft.limitPrice, "Draft limit", color, true);
|
|
42
|
+
if (["STP", "STP LMT"].includes(draft.orderType)) add("ticket:stop", draft.stopPrice, "Draft stop", color, true);
|
|
43
|
+
}
|
|
44
|
+
const projected = latestOrder ? withLatestOrder(orders, latestOrder) : orders;
|
|
45
|
+
for (const order of projected) {
|
|
46
|
+
if (!canChangeBrokerOrder(order)) continue;
|
|
47
|
+
const label = `${order.action === "BUY" ? "Buy" : "Sell"} ${order.remaining} ${({ "Partially filled": "partial", Replacing: "replacing" } as Record<string, string>)[brokerOrderStatus(order.status)] ?? "working"}`;
|
|
48
|
+
const color = order.action === "SELL" ? colors.negative : colors.positive;
|
|
49
|
+
add(`order:${brokerOrderKey(order)}`, order.limitPrice ?? order.stopPrice, label, color);
|
|
50
|
+
if (order.orderType === "STP LMT") add(`stop:${brokerOrderKey(order)}`, order.stopPrice, `${label} stop`, color);
|
|
51
|
+
}
|
|
52
|
+
if (position && avgCost !== undefined) add("position:average", avgCost, "Average cost", colors.textDim, false, true);
|
|
53
|
+
return levels;
|
|
54
|
+
}
|