gloomberb 0.5.1 → 0.6.1
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/README.md +91 -11
- package/package.json +13 -5
- package/src/app.tsx +414 -191
- package/src/architecture/import-boundaries.test.ts +118 -0
- package/src/assets/gloomberb-logo.png +0 -0
- package/src/assets/gloomberb-logo.svg +73 -0
- package/src/brokers/account-cache.ts +157 -0
- package/src/brokers/profile-form.test.ts +73 -0
- package/src/brokers/profile-form.ts +109 -0
- package/src/brokers/remote-broker-adapter.ts +91 -0
- package/src/brokers/sync-broker-instance.test.ts +124 -1
- package/src/brokers/sync-broker-instance.ts +113 -17
- package/src/capabilities/factories.ts +100 -0
- package/src/capabilities/index.ts +4 -0
- package/src/capabilities/registry.test.ts +137 -0
- package/src/capabilities/registry.ts +131 -0
- package/src/capabilities/service-capabilities.ts +27 -0
- package/src/capabilities/types.ts +84 -0
- package/src/cli/commands/ticker.ts +11 -3
- package/src/cli/context.ts +19 -13
- package/src/cli/helpers.ts +1 -0
- package/src/cli/types.ts +2 -2
- package/src/components/chart/chart-controller.ts +4 -24
- package/src/components/chart/chart-data.ts +3 -14
- package/src/components/chart/chart-indicator-selector.tsx +28 -6
- package/src/components/chart/chart-pane-settings.ts +15 -28
- package/src/components/chart/chart-renderer.ts +74 -99
- package/src/components/chart/chart-resolution.ts +10 -10
- package/src/components/chart/chart-scroll.test.ts +6 -5
- package/src/components/chart/chart-scroll.ts +2 -2
- package/src/components/chart/chart-types.ts +0 -1
- package/src/components/chart/chart-viewport.ts +2 -1
- package/src/components/chart/chart.test.ts +37 -0
- package/src/components/chart/comparison-chart-data.ts +3 -17
- package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
- package/src/components/chart/comparison-chart-renderer.ts +1 -1
- package/src/components/chart/comparison-stock-chart.tsx +318 -158
- package/src/components/chart/cursor-motion.ts +3 -2
- package/src/components/chart/indicators/bands.test.ts +0 -14
- package/src/components/chart/indicators/moving-averages.test.ts +0 -15
- package/src/components/chart/indicators/options.ts +3 -3
- package/src/components/chart/indicators/oscillators.test.ts +0 -21
- package/src/components/chart/indicators/types.ts +1 -2
- package/src/components/chart/native/chart-rasterizer.ts +37 -44
- package/src/components/chart/native/kitty-adapter.ts +2 -5
- package/src/components/chart/native/kitty-manager.ts +1 -1
- package/src/components/chart/native/kitty-protocol.ts +0 -1
- package/src/components/chart/native/kitty-support.ts +1 -1
- package/src/components/chart/native/kitty.test.ts +53 -13
- package/src/components/chart/native/renderer-selection.ts +1 -1
- package/src/components/chart/native/surface-manager.ts +1 -1
- package/src/components/chart/native/surface-visibility.ts +48 -0
- package/src/components/chart/static-chart-surface.test.tsx +51 -0
- package/src/components/chart/static-chart-surface.tsx +191 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
- package/src/components/chart/stock-chart.tsx +452 -191
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +269 -20
- package/src/components/command-bar/command-bar.tsx +830 -353
- package/src/components/command-bar/command-registry.ts +10 -20
- package/src/components/command-bar/helpers.test.ts +1 -1
- package/src/components/command-bar/helpers.ts +9 -11
- package/src/components/command-bar/pane-template-display.ts +4 -8
- package/src/components/command-bar/root-shortcuts.ts +12 -11
- package/src/components/command-bar/view-model.test.ts +8 -42
- package/src/components/command-bar/view-model.ts +8 -36
- package/src/components/command-bar/workflow-ops.test.ts +76 -2
- package/src/components/command-bar/workflow-ops.ts +40 -9
- package/src/components/command-bar/workflow-types.ts +2 -1
- package/src/components/data-table-stack-view.test.tsx +206 -0
- package/src/components/data-table-stack-view.tsx +152 -0
- package/src/components/data-table-view.test.tsx +188 -0
- package/src/components/data-table-view.tsx +279 -0
- package/src/components/feed-data-table-stack-view.test.tsx +225 -0
- package/src/components/feed-data-table-stack-view.tsx +365 -0
- package/src/components/index.ts +26 -31
- package/src/components/layout/detached-pane-shell.tsx +185 -0
- package/src/components/layout/floating-pane.tsx +87 -14
- package/src/components/layout/header.tsx +139 -49
- package/src/components/layout/pane-content.tsx +42 -0
- package/src/components/layout/pane-footer.test.tsx +195 -0
- package/src/components/layout/pane-footer.tsx +425 -0
- package/src/components/layout/pane-header.test.tsx +39 -0
- package/src/components/layout/pane-header.tsx +192 -14
- package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
- package/src/components/layout/pane-sizing.ts +9 -10
- package/src/components/layout/pane-title.ts +35 -0
- package/src/components/layout/pane.tsx +52 -11
- package/src/components/layout/shell.test.tsx +315 -107
- package/src/components/layout/shell.tsx +1010 -368
- package/src/components/layout/status-bar.test.tsx +17 -10
- package/src/components/layout/status-bar.tsx +367 -100
- package/src/components/layout/titlebar-overlay.ts +2 -0
- package/src/components/markdown-editor.tsx +29 -14
- package/src/components/markdown-text.tsx +25 -24
- package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
- package/src/components/onboarding/onboarding-wizard.tsx +347 -473
- package/src/components/pane-settings-dialog.tsx +521 -89
- package/src/components/pane-template-wizard.test.tsx +1 -1
- package/src/components/pane-template-wizard.tsx +38 -37
- package/src/components/price-selector-dialog.tsx +22 -21
- package/src/components/ticker-badge-list.tsx +53 -0
- package/src/components/ticker-badge-text.test.tsx +44 -1
- package/src/components/ticker-badge-text.tsx +78 -38
- package/src/components/ticker-badge.tsx +28 -7
- package/src/components/ticker-list-table-view.tsx +248 -0
- package/src/components/ticker-list-table.test.tsx +47 -1
- package/src/components/ticker-list-table.tsx +212 -41
- package/src/components/toggle-list.tsx +114 -8
- package/src/components/ui/button.tsx +24 -16
- package/src/components/ui/choice-dialog.tsx +89 -0
- package/src/components/ui/data-table.tsx +313 -114
- package/src/components/ui/external-link.tsx +52 -13
- package/src/components/ui/fields.tsx +44 -30
- package/src/components/ui/frame.tsx +24 -57
- package/src/components/ui/index.ts +15 -50
- package/src/components/ui/list-view.tsx +65 -25
- package/src/components/ui/loading.tsx +5 -60
- package/src/components/ui/message-composer.test.tsx +81 -0
- package/src/components/ui/message-composer.tsx +139 -0
- package/src/components/ui/multi-select-dialog.tsx +100 -32
- package/src/components/ui/multi-select.ts +112 -0
- package/src/components/ui/native-select.tsx +120 -0
- package/src/components/ui/page-stack-view.tsx +49 -19
- package/src/components/ui/remote-image.tsx +50 -0
- package/src/components/ui/row-value-cache.test.ts +17 -0
- package/src/components/ui/row-value-cache.ts +41 -0
- package/src/components/ui/shortcut-hint.tsx +55 -0
- package/src/components/ui/status.tsx +12 -67
- package/src/components/ui/table-layout.ts +59 -0
- package/src/components/ui/tabs.tsx +249 -27
- package/src/components/ui/toggle.tsx +19 -127
- package/src/components/ui/ui.test.tsx +266 -53
- package/src/components/use-double-click-activation.ts +7 -2
- package/src/components/use-external-link-footer.ts +79 -0
- package/src/core/app-services.ts +95 -0
- package/src/core/state/app-state.ts +886 -0
- package/src/{state → core/state}/session-persistence.ts +7 -6
- package/src/data/config-layout.ts +225 -0
- package/src/data/config-store-node.ts +390 -0
- package/src/data/config-store.test.ts +72 -0
- package/src/data/config-store.ts +35 -478
- package/src/data/sqlite-json.ts +2 -5
- package/src/data/sqlite-retry.ts +1 -0
- package/src/index.tsx +2 -68
- package/src/market-data/coordinator-subscriptions.test.ts +125 -0
- package/src/market-data/coordinator.test.ts +73 -4
- package/src/market-data/coordinator.ts +190 -24
- package/src/market-data/hooks.test.tsx +1 -1
- package/src/market-data/hooks.tsx +109 -87
- package/src/market-data/query-store.ts +3 -3
- package/src/market-data/request-types.test.ts +0 -14
- package/src/market-data/result-types.ts +1 -0
- package/src/news/aggregator.test.ts +267 -0
- package/src/news/aggregator.ts +452 -0
- package/src/news/hooks.ts +39 -0
- package/src/news/ticker-symbols.ts +21 -0
- package/src/news/types.ts +77 -0
- package/src/notifications/app-notifier.ts +21 -10
- package/src/pane-settings.test.ts +0 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
- package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +41 -5
- package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
- package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
- package/src/plugins/builtin/ai/settings.ts +0 -1
- package/src/plugins/builtin/ai/utils.ts +1 -71
- package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
- package/src/plugins/builtin/alerts/index.test.tsx +61 -82
- package/src/plugins/builtin/alerts/index.tsx +263 -155
- package/src/plugins/builtin/alerts/types.ts +7 -0
- package/src/plugins/builtin/analytics/index.test.tsx +13 -8
- package/src/plugins/builtin/analytics/index.tsx +78 -120
- package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
- package/src/plugins/builtin/ask-ai.test.tsx +55 -54
- package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
- package/src/plugins/builtin/broker-manager/index.tsx +759 -0
- package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
- package/src/plugins/builtin/broker-manager/model.ts +142 -0
- package/src/plugins/builtin/chat-controller.test.ts +129 -0
- package/src/plugins/builtin/chat-controller.ts +85 -14
- package/src/plugins/builtin/chat.test.tsx +283 -36
- package/src/plugins/builtin/chat.tsx +363 -170
- package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
- package/src/plugins/builtin/cloud-tweets.tsx +896 -0
- package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
- package/src/plugins/builtin/comparison-chart.tsx +11 -11
- package/src/plugins/builtin/correlation/compute.test.ts +0 -17
- package/src/plugins/builtin/correlation/index.tsx +51 -46
- package/src/plugins/builtin/correlation/settings.ts +1 -2
- package/src/plugins/builtin/debug.tsx +99 -70
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
- package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
- package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
- package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
- package/src/plugins/builtin/earnings/index.tsx +187 -94
- package/src/plugins/builtin/econ/calendar-source.ts +9 -116
- package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
- package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
- package/src/plugins/builtin/econ/index.tsx +236 -421
- package/src/plugins/builtin/econ/types.ts +1 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
- package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
- package/src/plugins/builtin/help.test.tsx +120 -60
- package/src/plugins/builtin/help.tsx +266 -82
- package/src/plugins/builtin/holders/index.test.ts +45 -0
- package/src/plugins/builtin/holders/index.tsx +949 -0
- package/src/plugins/builtin/insider/index.tsx +207 -253
- package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
- package/src/plugins/builtin/insider/insider-data.ts +1 -0
- package/src/plugins/builtin/layout-manager.tsx +12 -40
- package/src/plugins/builtin/market-movers/index.tsx +70 -144
- package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
- package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
- package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
- package/src/plugins/builtin/news-wire/categories.ts +114 -0
- package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
- package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
- package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
- package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
- package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
- package/src/plugins/builtin/news-wire/hash.ts +9 -0
- package/src/plugins/builtin/news-wire/index.tsx +92 -0
- package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
- package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
- package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
- package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
- package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
- package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
- package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
- package/src/plugins/builtin/news-wire/read-state.ts +76 -0
- package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
- package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
- package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
- package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
- package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
- package/src/plugins/builtin/news.tsx +98 -25
- package/src/plugins/builtin/notes-files.ts +51 -10
- package/src/plugins/builtin/notes.tsx +332 -131
- package/src/plugins/builtin/options.test.tsx +185 -0
- package/src/plugins/builtin/options.tsx +346 -206
- package/src/plugins/builtin/plugin-groups.ts +92 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
- package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
- package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
- package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
- package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
- package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
- package/src/plugins/builtin/portfolio-list.test.ts +60 -1
- package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
- package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
- package/src/plugins/builtin/research/index.tsx +724 -0
- package/src/plugins/builtin/sec.tsx +67 -19
- package/src/plugins/builtin/sectors/index.tsx +42 -80
- package/src/plugins/builtin/sectors/sector-data.ts +1 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
- package/src/plugins/builtin/ticker-detail/pane.tsx +143 -49
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
- package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +82 -10
- package/src/plugins/builtin/ticker-detail.test.tsx +258 -33
- package/src/plugins/builtin/ticker-surface.ts +68 -0
- package/src/plugins/builtin/world-indices/index.tsx +44 -82
- package/src/plugins/builtin/world-indices/indices.ts +1 -0
- package/src/plugins/builtin/yahoo.ts +55 -1
- package/src/plugins/builtin/yield-curve/index.tsx +87 -145
- package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
- package/src/plugins/catalog-ui.test.ts +24 -0
- package/src/plugins/catalog-ui.ts +44 -0
- package/src/plugins/catalog.ts +2 -50
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/account-cache.test.ts +15 -14
- package/src/plugins/ibkr/account-cache.ts +15 -120
- package/src/plugins/ibkr/broker-adapter.ts +67 -0
- package/src/plugins/ibkr/config.test.ts +0 -18
- package/src/plugins/ibkr/dialogs.tsx +14 -56
- package/src/plugins/ibkr/flex.test.ts +57 -2
- package/src/plugins/ibkr/flex.ts +116 -12
- package/src/plugins/ibkr/gateway-helpers.ts +8 -3
- package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
- package/src/plugins/ibkr/gateway-service.test.ts +47 -9
- package/src/plugins/ibkr/gateway-service.ts +264 -2235
- package/src/plugins/ibkr/gateway-types.ts +27 -0
- package/src/plugins/ibkr/index.test.tsx +3 -11
- package/src/plugins/ibkr/index.tsx +1 -0
- package/src/plugins/ibkr/price-normalization.test.ts +1 -6
- package/src/plugins/ibkr/price-normalization.ts +1 -0
- package/src/plugins/ibkr/trade-badge.tsx +6 -5
- package/src/plugins/ibkr/trade-tab.test.tsx +55 -3
- package/src/plugins/ibkr/trade-tab.tsx +166 -117
- package/src/plugins/ibkr/trading-pane.tsx +117 -83
- package/src/plugins/pane-manager.test.ts +3 -1
- package/src/plugins/pane-manager.ts +93 -19
- package/src/plugins/plugin-runtime.test.tsx +176 -74
- package/src/plugins/plugin-runtime.tsx +228 -10
- package/src/plugins/prediction-markets/cache.ts +49 -0
- package/src/plugins/prediction-markets/categories.ts +0 -10
- package/src/plugins/prediction-markets/chart.test.tsx +1 -1
- package/src/plugins/prediction-markets/chart.tsx +21 -20
- package/src/plugins/prediction-markets/cli.ts +1 -0
- package/src/plugins/prediction-markets/controller-data.ts +135 -89
- package/src/plugins/prediction-markets/controller.ts +37 -108
- package/src/plugins/prediction-markets/detail/book.tsx +23 -22
- package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
- package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
- package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
- package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
- package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
- package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
- package/src/plugins/prediction-markets/detail.test.tsx +1 -1
- package/src/plugins/prediction-markets/keyboard.ts +1 -0
- package/src/plugins/prediction-markets/launch.test.ts +1 -1
- package/src/plugins/prediction-markets/launch.ts +1 -1
- package/src/plugins/prediction-markets/metrics.ts +25 -12
- package/src/plugins/prediction-markets/pane.test.tsx +16 -7
- package/src/plugins/prediction-markets/pane.tsx +180 -82
- package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
- package/src/plugins/prediction-markets/services/fetch.ts +27 -10
- package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/settings.ts +0 -5
- package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
- package/src/plugins/registry.test.ts +307 -0
- package/src/plugins/registry.ts +293 -82
- package/src/react/input.ts +61 -0
- package/src/react/plugins/plugin-slot.tsx +21 -0
- package/src/react/use-raf-callback.ts +44 -0
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
- package/src/renderers/electrobun/bun/application-menu.ts +118 -0
- package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
- package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
- package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
- package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
- package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
- package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
- package/src/renderers/electrobun/bun/index.ts +1149 -0
- package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
- package/src/renderers/electrobun/bun/window-frame.ts +46 -0
- package/src/renderers/electrobun/shared/protocol.ts +72 -0
- package/src/renderers/electrobun/view/ai-host.ts +93 -0
- package/src/renderers/electrobun/view/app-services.ts +348 -0
- package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
- package/src/renderers/electrobun/view/config-host.ts +35 -0
- package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
- package/src/renderers/electrobun/view/context-menu.ts +164 -0
- package/src/renderers/electrobun/view/data-table.tsx +773 -0
- package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
- package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
- package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
- package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
- package/src/renderers/electrobun/view/http-fetch.ts +113 -0
- package/src/renderers/electrobun/view/input-host.tsx +112 -0
- package/src/renderers/electrobun/view/key-event.test.ts +34 -0
- package/src/renderers/electrobun/view/key-event.ts +134 -0
- package/src/renderers/electrobun/view/main.tsx +104 -0
- package/src/renderers/electrobun/view/native-renderer.ts +110 -0
- package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
- package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
- package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
- package/src/renderers/electrobun/view/notes-files.ts +56 -0
- package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
- package/src/renderers/electrobun/view/resource-store.ts +106 -0
- package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
- package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
- package/src/renderers/electrobun/view/toast-host.tsx +65 -0
- package/src/renderers/electrobun/view/ui-host.tsx +1509 -0
- package/src/renderers/electrobun/view/update-host.ts +38 -0
- package/src/renderers/opentui/dialog-host.test.tsx +44 -0
- package/src/renderers/opentui/dialog-host.tsx +56 -0
- package/src/renderers/opentui/host.tsx +140 -0
- package/src/renderers/opentui/image-loader.test.ts +25 -0
- package/src/renderers/opentui/image-loader.ts +105 -0
- package/src/renderers/opentui/image-surface.tsx +266 -0
- package/src/renderers/opentui/input-host.tsx +63 -0
- package/src/renderers/opentui/start.tsx +71 -0
- package/src/renderers/opentui/test-utils.tsx +76 -0
- package/src/renderers/opentui/toast-host.tsx +10 -0
- package/src/renderers/opentui/ui-host.tsx +98 -0
- package/src/sources/gloomberb-cloud.test.ts +288 -17
- package/src/sources/gloomberb-cloud.ts +176 -9
- package/src/sources/provider-errors.ts +1 -0
- package/src/sources/provider-router.test.ts +220 -81
- package/src/sources/provider-router.ts +294 -111
- package/src/sources/sec-edgar.ts +19 -6
- package/src/sources/yahoo-finance.test.ts +0 -5
- package/src/sources/yahoo-finance.ts +420 -11
- package/src/state/app-activity.ts +7 -7
- package/src/state/app-bootstrap.ts +114 -40
- package/src/state/app-context-selector.test.tsx +70 -1
- package/src/state/app-context.test.ts +71 -3
- package/src/state/app-context.test.tsx +14 -7
- package/src/state/app-context.tsx +209 -869
- package/src/state/config-save-scheduler.ts +35 -0
- package/src/state/pane-scroll-registry.tsx +254 -0
- package/src/state/persist-scheduler.test.ts +67 -0
- package/src/state/persist-scheduler.ts +69 -0
- package/src/state/selectors-ui.ts +55 -0
- package/src/state/selectors.ts +15 -71
- package/src/state/session-persistence.test.ts +1 -1
- package/src/state/ticker-refresh-queue.ts +52 -3
- package/src/state/use-inline-tickers.ts +18 -16
- package/src/state/use-quote-streaming.test.ts +0 -6
- package/src/state/use-quote-streaming.test.tsx +1 -1
- package/src/state/use-quote-streaming.ts +3 -2
- package/src/state/use-session-snapshot.ts +32 -17
- package/src/test-support/plugin-runtime.ts +149 -0
- package/src/theme/color-utils.ts +1 -0
- package/src/theme/colors.ts +16 -0
- package/src/types/broker.ts +16 -1
- package/src/types/capability-route-source.ts +23 -0
- package/src/types/config.test.ts +23 -0
- package/src/types/config.ts +145 -10
- package/src/types/context-menu.ts +95 -0
- package/src/types/data-provider.ts +34 -3
- package/src/types/desktop-menu.ts +13 -0
- package/src/types/desktop-window.ts +29 -0
- package/src/types/financials.ts +123 -2
- package/src/types/instrument.ts +1 -0
- package/src/types/news-source.ts +9 -0
- package/src/types/persistence.ts +1 -0
- package/src/types/plugin.ts +32 -8
- package/src/ui/ascii-font.ts +85 -0
- package/src/ui/context-menu.test.ts +94 -0
- package/src/ui/context-menu.tsx +433 -0
- package/src/ui/dialog.tsx +55 -0
- package/src/ui/host.tsx +364 -0
- package/src/ui/index.tsx +134 -0
- package/src/ui/toast.tsx +40 -0
- package/src/updater.test.ts +69 -0
- package/src/updater.ts +61 -18
- package/src/utils/api-client.test.ts +99 -9
- package/src/utils/api-client.ts +378 -18
- package/src/utils/back-navigation.ts +6 -0
- package/src/utils/cli-output.ts +1 -0
- package/src/utils/currency-units.test.ts +1 -6
- package/src/utils/currency-units.ts +1 -0
- package/src/utils/debug-log.ts +68 -0
- package/src/utils/dom-screenshot.ts +326 -0
- package/src/utils/exchange-rate-map.ts +1 -0
- package/src/utils/exchanges.ts +214 -0
- package/src/utils/format.test.ts +7 -0
- package/src/utils/format.ts +1 -0
- package/src/utils/fuzzy-search.ts +1 -0
- package/src/utils/html-entities.test.ts +8 -0
- package/src/utils/html-entities.ts +32 -0
- package/src/utils/http-transport.ts +11 -0
- package/src/utils/main-thread-monitor.ts +127 -0
- package/src/utils/perf-marks.ts +54 -0
- package/src/utils/quote-freshness.ts +27 -50
- package/src/utils/quote-resolution.test.ts +5 -5
- package/src/utils/quote-resolution.ts +0 -1
- package/src/utils/quote-time.ts +1 -5
- package/src/utils/sec.test.ts +0 -12
- package/src/utils/selection-clipboard.ts +14 -6
- package/src/utils/shortcut-labels.test.ts +20 -0
- package/src/utils/shortcut-labels.ts +67 -0
- package/src/utils/slugify.ts +1 -0
- package/src/utils/terminal-input-reset.ts +1 -0
- package/src/utils/text-wrap.ts +82 -0
- package/src/utils/throttled-fetch.test.ts +19 -0
- package/src/utils/throttled-fetch.ts +11 -9
- package/src/utils/ticker-list.test.ts +1 -7
- package/src/utils/ticker-list.ts +1 -0
- package/src/utils/timestamp.ts +29 -0
- package/src/utils/tweet-text.test.ts +13 -0
- package/src/utils/tweet-text.ts +18 -0
- package/src/utils/url.ts +11 -0
- package/src/version.ts +1 -1
- package/src/components/chart/chart-control-hint.tsx +0 -39
- package/src/components/detail-feed-view.test.tsx +0 -180
- package/src/components/detail-feed-view.tsx +0 -531
- package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
- package/src/components/layout/pixel-mouse.test.ts +0 -40
- package/src/components/spinner.tsx +0 -2
- package/src/components/tab-bar.tsx +0 -17
- package/src/components/ui/multi-select-chips.tsx +0 -69
- package/src/plugins/builtin/ai/index.test.tsx +0 -139
- package/src/plugins/builtin/ask-ai.tsx +0 -9
- package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
- package/src/plugins/builtin/correlation/settings.test.ts +0 -34
- package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
- package/src/plugins/builtin/econ/fred-client.ts +0 -79
- package/src/plugins/builtin/fred-settings.ts +0 -10
- package/src/plugins/builtin/options-availability.test.tsx +0 -179
- package/src/plugins/builtin/options-availability.ts +0 -158
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
- package/src/plugins/prediction-markets/status.ts +0 -24
- package/src/plugins/prediction-markets/table.tsx +0 -87
- package/src/utils/quote-time.test.ts +0 -22
- package/src/utils/selection-clipboard.test.ts +0 -43
- package/src/utils/terminal-input-reset.test.ts +0 -50
|
@@ -1,110 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BarSizeSetting,
|
|
3
|
-
ConnectionState,
|
|
4
|
-
EventName,
|
|
5
|
-
IBApiTickType,
|
|
6
|
-
IBApiNext,
|
|
7
|
-
MarketDataType,
|
|
8
|
-
OptionType,
|
|
9
|
-
OrderAction,
|
|
10
|
-
OrderType,
|
|
11
|
-
SecType,
|
|
12
|
-
TimeInForce,
|
|
13
|
-
WhatToShow,
|
|
14
|
-
type Contract,
|
|
15
|
-
type ContractDescription,
|
|
16
|
-
type ContractDetails,
|
|
17
|
-
type MarketDataUpdate,
|
|
18
|
-
type Order,
|
|
19
|
-
type OrderState,
|
|
20
|
-
type TickByTickAllLast,
|
|
21
|
-
} from "@stoqey/ib";
|
|
22
|
-
import { mkdir, open, readFile, rm } from "fs/promises";
|
|
23
|
-
import { createConnection } from "net";
|
|
24
|
-
import { tmpdir } from "os";
|
|
25
|
-
import { join } from "path";
|
|
26
|
-
import { firstValueFrom, filter, take, timeout } from "rxjs";
|
|
27
1
|
import type { TimeRange } from "../../components/chart/chart-types";
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
type ChartResolutionSupport,
|
|
31
|
-
type ManualChartResolution,
|
|
32
|
-
} from "../../components/chart/chart-resolution";
|
|
2
|
+
import type { ChartResolutionSupport, ManualChartResolution } from "../../components/chart/chart-resolution";
|
|
3
|
+
import { getBrokerRemoteClient } from "../../brokers/remote-broker-adapter";
|
|
33
4
|
import type { BrokerConnectionStatus, BrokerPosition } from "../../types/broker";
|
|
34
5
|
import type { QuoteSubscriptionTarget } from "../../types/data-provider";
|
|
35
|
-
import type {
|
|
6
|
+
import type { Quote, PricePoint, TickerFinancials } from "../../types/financials";
|
|
36
7
|
import type { BrokerContractRef, InstrumentSearchResult } from "../../types/instrument";
|
|
37
8
|
import type {
|
|
38
9
|
BrokerAccount,
|
|
39
|
-
BrokerCashBalance,
|
|
40
10
|
BrokerExecution,
|
|
41
11
|
BrokerOrder,
|
|
42
12
|
BrokerOrderPreview,
|
|
43
13
|
BrokerOrderRequest,
|
|
44
14
|
} from "../../types/trading";
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
accounts: BrokerAccount[];
|
|
69
|
-
openOrders: BrokerOrder[];
|
|
70
|
-
executions: BrokerExecution[];
|
|
71
|
-
lastError?: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface ClientLockMetadata {
|
|
75
|
-
pid: number;
|
|
76
|
-
ownerToken: string;
|
|
77
|
-
requestedClientId: number;
|
|
78
|
-
clientId: number;
|
|
79
|
-
host: string;
|
|
80
|
-
port: number;
|
|
81
|
-
instanceId?: string;
|
|
82
|
-
cwd: string;
|
|
83
|
-
timestamp: number;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
interface ClaimedClientLock {
|
|
87
|
-
clientId: number;
|
|
88
|
-
requestedClientId: number;
|
|
89
|
-
path: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
type QuoteStreamListener = (target: QuoteSubscriptionTarget, quote: Quote) => void;
|
|
93
|
-
|
|
94
|
-
interface ActiveQuoteStream {
|
|
95
|
-
target: QuoteSubscriptionTarget;
|
|
96
|
-
listeners: Map<QuoteStreamListener, QuoteSubscriptionTarget>;
|
|
97
|
-
stop: () => void;
|
|
98
|
-
lastQuote?: Quote;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
interface AccountPortfolioSnapshotPosition {
|
|
102
|
-
contract: Contract;
|
|
103
|
-
avgCost?: number;
|
|
104
|
-
marketPrice?: number;
|
|
105
|
-
marketValue?: number;
|
|
106
|
-
unrealizedPNL?: number;
|
|
107
|
-
}
|
|
15
|
+
import type {
|
|
16
|
+
IbkrGatewayConfig,
|
|
17
|
+
IbkrSnapshot,
|
|
18
|
+
ResolvedIbkrGatewayConnection,
|
|
19
|
+
} from "./gateway-types";
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
IbkrGatewayConfig,
|
|
23
|
+
IbkrSnapshot,
|
|
24
|
+
ResolvedIbkrGatewayConnection,
|
|
25
|
+
} from "./gateway-types";
|
|
26
|
+
|
|
27
|
+
type NativeGatewayService = any;
|
|
28
|
+
type NativeGatewayManager = any;
|
|
29
|
+
type NativeGatewayModule = {
|
|
30
|
+
ibkrGatewayManager: NativeGatewayManager;
|
|
31
|
+
setResolvedIbkrGatewayListener(listener: ResolvedGatewayListener | null): void;
|
|
32
|
+
};
|
|
33
|
+
type Listener = () => void;
|
|
34
|
+
type ResolvedGatewayListener = (
|
|
35
|
+
instanceId: string | undefined,
|
|
36
|
+
connection: ResolvedIbkrGatewayConnection,
|
|
37
|
+
) => void | Promise<void>;
|
|
108
38
|
|
|
109
39
|
const DEFAULT_SNAPSHOT: IbkrSnapshot = {
|
|
110
40
|
status: { state: "disconnected", updatedAt: Date.now() },
|
|
@@ -113,928 +43,218 @@ const DEFAULT_SNAPSHOT: IbkrSnapshot = {
|
|
|
113
43
|
executions: [],
|
|
114
44
|
};
|
|
115
45
|
|
|
116
|
-
const gatewayLog = debugLog.createLogger("ibkr-gateway");
|
|
117
|
-
const IBKR_CLIENT_LOCK_DIR = join(tmpdir(), "gloomberb-ibkr-client-locks");
|
|
118
|
-
const IBKR_CLIENT_ID_SEARCH_SPAN = 64;
|
|
119
|
-
const LOCAL_IBKR_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
120
|
-
const COMMON_LOCAL_IBKR_PORTS = [4001, 4002, 7496, 7497] as const;
|
|
121
|
-
const LOCAL_TCP_PROBE_TIMEOUT_MS = 250;
|
|
122
|
-
const DEFAULT_AUTO_CLIENT_ID = 1;
|
|
123
|
-
|
|
124
|
-
type ResolvedGatewayListener = (
|
|
125
|
-
instanceId: string | undefined,
|
|
126
|
-
connection: ResolvedIbkrGatewayConnection,
|
|
127
|
-
) => void | Promise<void>;
|
|
128
|
-
|
|
129
46
|
let resolvedGatewayListener: ResolvedGatewayListener | null = null;
|
|
47
|
+
let loadedNativeGatewayModule: NativeGatewayModule | null = null;
|
|
48
|
+
let nativeGatewayModulePromise: Promise<NativeGatewayModule> | null = null;
|
|
49
|
+
let nativeGatewayManager: NativeGatewayManager | null = null;
|
|
50
|
+
let nativeGatewayModuleLoader: () => Promise<NativeGatewayModule>;
|
|
51
|
+
const importNativeGatewayModule = new Function("specifier", "return import(specifier)") as (
|
|
52
|
+
specifier: string,
|
|
53
|
+
) => Promise<NativeGatewayModule>;
|
|
130
54
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
type AccountSummaryValueMap = ReadonlyMap<string, { value: string }>;
|
|
136
|
-
type AccountSummaryTags = ReadonlyMap<string, AccountSummaryValueMap>;
|
|
137
|
-
|
|
138
|
-
function getAccountSummaryEntry(
|
|
139
|
-
values: AccountSummaryValueMap | undefined,
|
|
140
|
-
preferredCurrency?: string,
|
|
141
|
-
): { currency: string; value: number } | null {
|
|
142
|
-
if (!values) return null;
|
|
143
|
-
|
|
144
|
-
if (preferredCurrency) {
|
|
145
|
-
const preferred = values.get(preferredCurrency);
|
|
146
|
-
if (preferred?.value) {
|
|
147
|
-
const numeric = parseFloat(preferred.value);
|
|
148
|
-
if (Number.isFinite(numeric)) {
|
|
149
|
-
return { currency: preferredCurrency, value: numeric };
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
for (const [currency, entry] of values.entries()) {
|
|
155
|
-
const numeric = parseFloat(entry?.value ?? "");
|
|
156
|
-
if (Number.isFinite(numeric)) {
|
|
157
|
-
return { currency, value: numeric };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function getAccountSummaryNumber(
|
|
164
|
-
tags: AccountSummaryTags | undefined,
|
|
165
|
-
tagName: string,
|
|
166
|
-
preferredCurrency?: string,
|
|
167
|
-
): number | undefined {
|
|
168
|
-
return getAccountSummaryEntry(tags?.get(tagName), preferredCurrency)?.value;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function getAccountSummaryNumberWithAggregateFallback(
|
|
172
|
-
tags: AccountSummaryTags | undefined,
|
|
173
|
-
aggregateTags: AccountSummaryTags | undefined,
|
|
174
|
-
tagName: string,
|
|
175
|
-
preferredCurrency: string | undefined,
|
|
176
|
-
allowAggregateFallback: boolean,
|
|
177
|
-
): number | undefined {
|
|
178
|
-
const direct = getAccountSummaryNumber(tags, tagName, preferredCurrency);
|
|
179
|
-
if (direct != null) return direct;
|
|
180
|
-
if (!allowAggregateFallback) return undefined;
|
|
181
|
-
return getAccountSummaryNumber(aggregateTags, tagName, preferredCurrency);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function inferAccountCurrency(tags: AccountSummaryTags | undefined): string | undefined {
|
|
185
|
-
if (!tags) return undefined;
|
|
186
|
-
for (const tagName of ["NetLiquidation", "TotalCashValue", "SettledCash", "AvailableFunds"]) {
|
|
187
|
-
const entry = getAccountSummaryEntry(tags.get(tagName));
|
|
188
|
-
if (entry?.currency) return entry.currency;
|
|
189
|
-
}
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function getSummaryMap(
|
|
194
|
-
tags: AccountSummaryTags | undefined,
|
|
195
|
-
tagName: string,
|
|
196
|
-
): AccountSummaryValueMap | undefined {
|
|
197
|
-
const values = tags?.get(tagName);
|
|
198
|
-
return values && values.size > 0 ? values : undefined;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function buildCashBalancesFromSummary(
|
|
202
|
-
cashBalanceMap: AccountSummaryValueMap | undefined,
|
|
203
|
-
exchangeRateMap: AccountSummaryValueMap | undefined,
|
|
204
|
-
baseCurrency?: string,
|
|
205
|
-
): BrokerCashBalance[] | undefined {
|
|
206
|
-
const balances: BrokerCashBalance[] = [];
|
|
207
|
-
for (const [currency, entry] of cashBalanceMap?.entries() ?? []) {
|
|
208
|
-
if (!currency || currency === "BASE") continue;
|
|
209
|
-
const numeric = parseFloat(entry?.value ?? "");
|
|
210
|
-
if (!Number.isFinite(numeric)) continue;
|
|
211
|
-
|
|
212
|
-
const exchangeRate = currency === baseCurrency
|
|
213
|
-
? 1
|
|
214
|
-
: parseFloat(exchangeRateMap?.get(currency)?.value ?? "");
|
|
215
|
-
const baseValue = Number.isFinite(exchangeRate) ? numeric * exchangeRate : undefined;
|
|
216
|
-
|
|
217
|
-
balances.push({
|
|
218
|
-
currency,
|
|
219
|
-
quantity: numeric,
|
|
220
|
-
baseValue,
|
|
221
|
-
baseCurrency,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return balances.length > 0 ? balances : undefined;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function buildCashBalances(
|
|
229
|
-
tags: AccountSummaryTags | undefined,
|
|
230
|
-
aggregateTags: AccountSummaryTags | undefined,
|
|
231
|
-
baseCurrency: string | undefined,
|
|
232
|
-
allowAggregateFallback: boolean,
|
|
233
|
-
): BrokerCashBalance[] | undefined {
|
|
234
|
-
const directLedger = buildCashBalancesFromSummary(
|
|
235
|
-
getSummaryMap(tags, "$LEDGER:ALL"),
|
|
236
|
-
undefined,
|
|
237
|
-
baseCurrency,
|
|
238
|
-
);
|
|
239
|
-
if (directLedger) return directLedger;
|
|
240
|
-
|
|
241
|
-
const directSummary = buildCashBalancesFromSummary(
|
|
242
|
-
getSummaryMap(tags, "CashBalance") ?? getSummaryMap(tags, "TotalCashBalance"),
|
|
243
|
-
getSummaryMap(tags, "ExchangeRate"),
|
|
244
|
-
baseCurrency,
|
|
245
|
-
);
|
|
246
|
-
if (directSummary) return directSummary;
|
|
247
|
-
|
|
248
|
-
if (!allowAggregateFallback) return undefined;
|
|
249
|
-
|
|
250
|
-
return buildCashBalancesFromSummary(
|
|
251
|
-
getSummaryMap(aggregateTags, "CashBalance") ?? getSummaryMap(aggregateTags, "TotalCashBalance"),
|
|
252
|
-
getSummaryMap(aggregateTags, "ExchangeRate"),
|
|
253
|
-
baseCurrency,
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function summarizeBrokerAccount(
|
|
258
|
-
accountId: string,
|
|
259
|
-
tags: AccountSummaryTags | undefined,
|
|
260
|
-
updatedAt: number,
|
|
261
|
-
aggregateTags?: AccountSummaryTags,
|
|
262
|
-
allowAggregateCashBalances = false,
|
|
263
|
-
): BrokerAccount {
|
|
264
|
-
const currency = inferAccountCurrency(tags) ?? (allowAggregateCashBalances ? inferAccountCurrency(aggregateTags) : undefined);
|
|
265
|
-
return {
|
|
266
|
-
accountId,
|
|
267
|
-
name: accountId,
|
|
268
|
-
currency,
|
|
269
|
-
source: "gateway",
|
|
270
|
-
updatedAt,
|
|
271
|
-
netLiquidation: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "NetLiquidation", currency, allowAggregateCashBalances),
|
|
272
|
-
totalCashValue: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "TotalCashValue", currency, allowAggregateCashBalances),
|
|
273
|
-
settledCash: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "SettledCash", currency, allowAggregateCashBalances),
|
|
274
|
-
availableFunds: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "AvailableFunds", currency, allowAggregateCashBalances),
|
|
275
|
-
buyingPower: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "BuyingPower", currency, allowAggregateCashBalances),
|
|
276
|
-
excessLiquidity: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "ExcessLiquidity", currency, allowAggregateCashBalances),
|
|
277
|
-
initMarginReq: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "InitMarginReq", currency, allowAggregateCashBalances),
|
|
278
|
-
maintMarginReq: getAccountSummaryNumberWithAggregateFallback(tags, aggregateTags, "MaintMarginReq", currency, allowAggregateCashBalances),
|
|
279
|
-
cashBalances: buildCashBalances(tags, aggregateTags, currency, allowAggregateCashBalances),
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** Wrap a promise with a timeout so that IBKR calls don't hang indefinitely. */
|
|
284
|
-
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
285
|
-
return new Promise<T>((resolve, reject) => {
|
|
286
|
-
const timer = setTimeout(() => reject(new Error(`IBKR ${label} timed out after ${ms}ms`)), ms);
|
|
287
|
-
promise.then(
|
|
288
|
-
(value) => { clearTimeout(timer); resolve(value); },
|
|
289
|
-
(error) => { clearTimeout(timer); reject(error); },
|
|
290
|
-
);
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const IBKR_DATA_TIMEOUT = 8_000;
|
|
295
|
-
const IBKR_QUOTE_STREAM_TICKS = "165,221,233";
|
|
296
|
-
|
|
297
|
-
const HISTORY_PARAMS: Record<TimeRange, { duration: string; size: BarSizeSetting }> = {
|
|
298
|
-
"1D": { duration: "1 D", size: BarSizeSetting.MINUTES_FIVE },
|
|
299
|
-
"1W": { duration: "7 D", size: BarSizeSetting.HOURS_ONE },
|
|
300
|
-
"1M": { duration: "1 M", size: BarSizeSetting.HOURS_ONE },
|
|
301
|
-
"3M": { duration: "3 M", size: BarSizeSetting.DAYS_ONE },
|
|
302
|
-
"6M": { duration: "6 M", size: BarSizeSetting.DAYS_ONE },
|
|
303
|
-
"1Y": { duration: "1 Y", size: BarSizeSetting.DAYS_ONE },
|
|
304
|
-
"5Y": { duration: "5 Y", size: BarSizeSetting.WEEKS_ONE },
|
|
305
|
-
"ALL": { duration: "10 Y", size: BarSizeSetting.MONTHS_ONE },
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const IBKR_RESOLUTION_SUPPORT = normalizeChartResolutionSupport([
|
|
309
|
-
{ resolution: "1m", maxRange: "1W" },
|
|
310
|
-
{ resolution: "5m", maxRange: "1M" },
|
|
311
|
-
{ resolution: "15m", maxRange: "3M" },
|
|
312
|
-
{ resolution: "30m", maxRange: "6M" },
|
|
313
|
-
{ resolution: "1h", maxRange: "1Y" },
|
|
314
|
-
{ resolution: "1d", maxRange: "ALL" },
|
|
315
|
-
{ resolution: "1wk", maxRange: "ALL" },
|
|
316
|
-
{ resolution: "1mo", maxRange: "ALL" },
|
|
317
|
-
]);
|
|
318
|
-
|
|
319
|
-
/** Map generic bar size labels to IBKR BarSizeSetting values. */
|
|
320
|
-
const GENERIC_BAR_SIZE_MAP: Record<string, BarSizeSetting> = {
|
|
321
|
-
"1m": BarSizeSetting.MINUTES_ONE,
|
|
322
|
-
"5m": BarSizeSetting.MINUTES_FIVE,
|
|
323
|
-
"15m": BarSizeSetting.MINUTES_FIFTEEN,
|
|
324
|
-
"30m": BarSizeSetting.MINUTES_THIRTY,
|
|
325
|
-
"1h": BarSizeSetting.HOURS_ONE,
|
|
326
|
-
"1d": BarSizeSetting.DAYS_ONE,
|
|
327
|
-
"1wk": BarSizeSetting.WEEKS_ONE,
|
|
328
|
-
"1mo": BarSizeSetting.MONTHS_ONE,
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
export function parseIbkrHistoricalBarTime(value: string | number): Date {
|
|
332
|
-
if (typeof value === "number") {
|
|
333
|
-
if (!Number.isFinite(value)) return new Date(Number.NaN);
|
|
334
|
-
|
|
335
|
-
const compactDate = String(Math.trunc(value));
|
|
336
|
-
if (/^\d{8}$/.test(compactDate)) {
|
|
337
|
-
return parseIbkrHistoricalBarTime(compactDate);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return new Date(value > 10_000_000_000 ? value : value * 1000);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const trimmed = value.trim();
|
|
344
|
-
if (!trimmed) return new Date(Number.NaN);
|
|
345
|
-
|
|
346
|
-
const parsed = Date.parse(trimmed);
|
|
347
|
-
if (Number.isFinite(parsed)) {
|
|
348
|
-
return new Date(parsed);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const compactMatch = trimmed.match(/^(\d{4})(\d{2})(\d{2})(?:\D+(\d{2}):(\d{2}):(\d{2}))?/);
|
|
352
|
-
if (!compactMatch) {
|
|
353
|
-
return new Date(Number.NaN);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const [, yearText, monthText, dayText, hourText, minuteText, secondText] = compactMatch;
|
|
357
|
-
const year = Number(yearText);
|
|
358
|
-
const month = Number(monthText) - 1;
|
|
359
|
-
const day = Number(dayText);
|
|
360
|
-
const hour = Number(hourText ?? "0");
|
|
361
|
-
const minute = Number(minuteText ?? "0");
|
|
362
|
-
const second = Number(secondText ?? "0");
|
|
363
|
-
|
|
364
|
-
return new Date(year, month, day, hour, minute, second);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function normalizeQuoteStreamTarget(target: QuoteSubscriptionTarget): QuoteSubscriptionTarget | null {
|
|
368
|
-
const symbol = target.symbol.trim().toUpperCase();
|
|
369
|
-
if (!symbol) return null;
|
|
370
|
-
return {
|
|
371
|
-
...target,
|
|
372
|
-
symbol,
|
|
373
|
-
exchange: (target.exchange ?? "").trim().toUpperCase(),
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function buildQuoteStreamKey(target: QuoteSubscriptionTarget): string {
|
|
378
|
-
const contractKey = target.context?.instrument?.conId
|
|
379
|
-
?? target.context?.instrument?.localSymbol
|
|
380
|
-
?? target.context?.instrument?.symbol
|
|
381
|
-
?? "";
|
|
382
|
-
return [target.symbol, target.exchange ?? "", contractKey].join("|");
|
|
383
|
-
}
|
|
55
|
+
const snapshots = new Map<string, IbkrSnapshot>();
|
|
56
|
+
const listeners = new Map<string, Set<Listener>>();
|
|
384
57
|
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
)
|
|
388
|
-
return [
|
|
389
|
-
IBApiTickType.DELAYED_BID,
|
|
390
|
-
IBApiTickType.DELAYED_ASK,
|
|
391
|
-
IBApiTickType.DELAYED_LAST,
|
|
392
|
-
IBApiTickType.DELAYED_BID_SIZE,
|
|
393
|
-
IBApiTickType.DELAYED_ASK_SIZE,
|
|
394
|
-
IBApiTickType.DELAYED_LAST_SIZE,
|
|
395
|
-
IBApiTickType.DELAYED_HIGH,
|
|
396
|
-
IBApiTickType.DELAYED_LOW,
|
|
397
|
-
IBApiTickType.DELAYED_VOLUME,
|
|
398
|
-
IBApiTickType.DELAYED_CLOSE,
|
|
399
|
-
IBApiTickType.DELAYED_OPEN,
|
|
400
|
-
IBApiTickType.DELAYED_LAST_TIMESTAMP,
|
|
401
|
-
].some((tickType) => marketData.has(tickType));
|
|
58
|
+
function getSnapshotEntry(instanceId?: string): IbkrSnapshot {
|
|
59
|
+
if (!instanceId) return DEFAULT_SNAPSHOT;
|
|
60
|
+
return snapshots.get(instanceId) ?? DEFAULT_SNAPSHOT;
|
|
402
61
|
}
|
|
403
62
|
|
|
404
|
-
function
|
|
405
|
-
return
|
|
406
|
-
?.split(",")
|
|
407
|
-
.map((entry) => entry.trim())
|
|
408
|
-
.find((entry) => entry.length > 0 && entry !== "SMART");
|
|
63
|
+
function defaultNativeGatewayModuleLoader(): Promise<NativeGatewayModule> {
|
|
64
|
+
return importNativeGatewayModule("./gateway-service-native");
|
|
409
65
|
}
|
|
410
66
|
|
|
411
|
-
|
|
412
|
-
current: Quote | undefined,
|
|
413
|
-
contract: Contract,
|
|
414
|
-
details: ContractDetails,
|
|
415
|
-
): string | undefined {
|
|
416
|
-
return current?.listingExchangeName
|
|
417
|
-
?? contract.primaryExch
|
|
418
|
-
?? firstNonSmartExchange(details.validExchanges)
|
|
419
|
-
?? (current?.exchangeName !== "SMART" ? current?.exchangeName : undefined)
|
|
420
|
-
?? (contract.exchange !== "SMART" ? contract.exchange : undefined);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function resolveIbkrRoutingExchange(current: Quote | undefined, contract: Contract): string | undefined {
|
|
424
|
-
return current?.routingExchangeName
|
|
425
|
-
?? contract.exchange
|
|
426
|
-
?? "SMART";
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function ibkrVenueFields(
|
|
430
|
-
current: Quote | undefined,
|
|
431
|
-
contract: Contract,
|
|
432
|
-
details: ContractDetails,
|
|
433
|
-
): Pick<
|
|
434
|
-
Quote,
|
|
435
|
-
| "exchangeName"
|
|
436
|
-
| "fullExchangeName"
|
|
437
|
-
| "listingExchangeName"
|
|
438
|
-
| "listingExchangeFullName"
|
|
439
|
-
| "routingExchangeName"
|
|
440
|
-
| "routingExchangeFullName"
|
|
441
|
-
| "marketState"
|
|
442
|
-
| "sessionConfidence"
|
|
443
|
-
> {
|
|
444
|
-
const listingExchangeName = resolveIbkrListingExchange(current, contract, details);
|
|
445
|
-
const listingExchangeFullName = current?.listingExchangeFullName
|
|
446
|
-
?? current?.fullExchangeName
|
|
447
|
-
?? listingExchangeName;
|
|
448
|
-
const routingExchangeName = resolveIbkrRoutingExchange(current, contract);
|
|
449
|
-
const routingExchangeFullName = current?.routingExchangeFullName ?? routingExchangeName;
|
|
67
|
+
nativeGatewayModuleLoader = defaultNativeGatewayModuleLoader;
|
|
450
68
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
fullExchangeName: listingExchangeFullName,
|
|
454
|
-
listingExchangeName,
|
|
455
|
-
listingExchangeFullName,
|
|
456
|
-
routingExchangeName,
|
|
457
|
-
routingExchangeFullName,
|
|
458
|
-
marketState: current?.marketState,
|
|
459
|
-
sessionConfidence: current?.sessionConfidence ?? "unknown",
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
interface TickByTickBidAskUpdate {
|
|
464
|
-
time: number;
|
|
465
|
-
bidPrice: number;
|
|
466
|
-
askPrice: number;
|
|
467
|
-
bidSize: number;
|
|
468
|
-
askSize: number;
|
|
69
|
+
export function setNativeIbkrGatewayModuleLoader(loader: (() => Promise<NativeGatewayModule>) | null): void {
|
|
70
|
+
nativeGatewayModuleLoader = loader ?? defaultNativeGatewayModuleLoader;
|
|
469
71
|
}
|
|
470
72
|
|
|
471
|
-
|
|
472
|
-
current
|
|
473
|
-
|
|
474
|
-
details: ContractDetails,
|
|
475
|
-
tick: TickByTickAllLast,
|
|
476
|
-
priceDivisor: number,
|
|
477
|
-
dataSource: Quote["dataSource"],
|
|
478
|
-
): Quote | null {
|
|
479
|
-
const nextPrice = normalizeIbkrPriceValue(tick.price, priceDivisor);
|
|
480
|
-
if (nextPrice == null || nextPrice <= 0) return null;
|
|
481
|
-
|
|
482
|
-
const previousClose = current?.previousClose ?? current?.price ?? nextPrice;
|
|
483
|
-
const change = nextPrice - previousClose;
|
|
484
|
-
return {
|
|
485
|
-
symbol: current?.symbol ?? contract.localSymbol ?? contract.symbol ?? "",
|
|
486
|
-
providerId: "ibkr",
|
|
487
|
-
price: nextPrice,
|
|
488
|
-
currency: current?.currency ?? contract.currency ?? "USD",
|
|
489
|
-
change,
|
|
490
|
-
changePercent: previousClose ? (change / previousClose) * 100 : 0,
|
|
491
|
-
previousClose,
|
|
492
|
-
high52w: current?.high52w,
|
|
493
|
-
low52w: current?.low52w,
|
|
494
|
-
volume: current?.volume,
|
|
495
|
-
name: current?.name ?? details.longName ?? details.marketName ?? contract.symbol,
|
|
496
|
-
lastUpdated: tick.time ? tick.time * 1000 : Date.now(),
|
|
497
|
-
...ibkrVenueFields(current, contract, details),
|
|
498
|
-
preMarketPrice: current?.preMarketPrice,
|
|
499
|
-
preMarketChange: current?.preMarketChange,
|
|
500
|
-
preMarketChangePercent: current?.preMarketChangePercent,
|
|
501
|
-
postMarketPrice: current?.postMarketPrice,
|
|
502
|
-
postMarketChange: current?.postMarketChange,
|
|
503
|
-
postMarketChangePercent: current?.postMarketChangePercent,
|
|
504
|
-
bid: current?.bid,
|
|
505
|
-
ask: current?.ask,
|
|
506
|
-
bidSize: current?.bidSize,
|
|
507
|
-
askSize: current?.askSize,
|
|
508
|
-
open: current?.open,
|
|
509
|
-
high: current?.high,
|
|
510
|
-
low: current?.low,
|
|
511
|
-
mark: current?.mark,
|
|
512
|
-
dataSource,
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
export function applyTickByTickBidAskToQuote(
|
|
517
|
-
current: Quote | undefined,
|
|
518
|
-
update: TickByTickBidAskUpdate,
|
|
519
|
-
priceDivisor: number,
|
|
520
|
-
): Quote | null {
|
|
521
|
-
if (!current) return null;
|
|
522
|
-
const bid = normalizeIbkrPriceValue(update.bidPrice, priceDivisor);
|
|
523
|
-
const ask = normalizeIbkrPriceValue(update.askPrice, priceDivisor);
|
|
524
|
-
return {
|
|
73
|
+
function applySnapshot(instanceId: string, patch: Partial<IbkrSnapshot>, shouldNotify: boolean): IbkrSnapshot {
|
|
74
|
+
const current = getSnapshotEntry(instanceId);
|
|
75
|
+
const next = {
|
|
525
76
|
...current,
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
77
|
+
...patch,
|
|
78
|
+
status: patch.status ?? current.status,
|
|
79
|
+
accounts: patch.accounts ?? current.accounts,
|
|
80
|
+
openOrders: patch.openOrders ?? current.openOrders,
|
|
81
|
+
executions: patch.executions ?? current.executions,
|
|
531
82
|
};
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
return MarketDataType.DELAYED;
|
|
540
|
-
case "delayed-frozen":
|
|
541
|
-
return MarketDataType.DELAYED_FROZEN;
|
|
542
|
-
case "live":
|
|
543
|
-
return MarketDataType.REALTIME;
|
|
544
|
-
case "auto":
|
|
545
|
-
default:
|
|
546
|
-
// Start with realtime; withMarketDataFallback will downgrade to delayed
|
|
547
|
-
// if the account lacks live market data subscriptions.
|
|
548
|
-
return MarketDataType.REALTIME;
|
|
83
|
+
if (
|
|
84
|
+
next.status === current.status
|
|
85
|
+
&& next.accounts === current.accounts
|
|
86
|
+
&& next.openOrders === current.openOrders
|
|
87
|
+
&& next.executions === current.executions
|
|
88
|
+
) {
|
|
89
|
+
return current;
|
|
549
90
|
}
|
|
91
|
+
snapshots.set(instanceId, next);
|
|
92
|
+
if (shouldNotify) notify(instanceId);
|
|
93
|
+
return next;
|
|
550
94
|
}
|
|
551
95
|
|
|
552
|
-
function
|
|
553
|
-
|
|
554
|
-
const text = (message || "").toLowerCase();
|
|
555
|
-
return text.includes("displaying delayed market data")
|
|
556
|
-
|| text.includes("delayed market data is available")
|
|
557
|
-
|| text.includes("market data connections")
|
|
558
|
-
|| text.includes("requested market data is not subscribed")
|
|
559
|
-
|| text.includes("requested market data requires additional subscription")
|
|
560
|
-
|| text.includes("market data subscription");
|
|
96
|
+
function updateSnapshot(instanceId: string, patch: Partial<IbkrSnapshot>): void {
|
|
97
|
+
applySnapshot(instanceId, patch, true);
|
|
561
98
|
}
|
|
562
99
|
|
|
563
|
-
function
|
|
564
|
-
|
|
565
|
-
return true;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const text = (message || "").toLowerCase();
|
|
569
|
-
return text.includes("connection is ok")
|
|
570
|
-
|| text.includes("connectivity between ib and trader workstation has been restored")
|
|
571
|
-
|| text.includes("inactive but should be available upon demand")
|
|
572
|
-
|| text.includes("sec-def data farm connection is ok")
|
|
573
|
-
|| text.includes("market data farm connection is ok")
|
|
574
|
-
|| text.includes("hmds data farm connection is ok");
|
|
100
|
+
function cacheSnapshot(instanceId: string, patch: Partial<IbkrSnapshot>): IbkrSnapshot {
|
|
101
|
+
return applySnapshot(instanceId, patch, false);
|
|
575
102
|
}
|
|
576
103
|
|
|
577
|
-
function
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
error?.code,
|
|
581
|
-
error?.error?.errorCode,
|
|
582
|
-
error?.error?.code,
|
|
583
|
-
error?.id,
|
|
584
|
-
];
|
|
585
|
-
for (const candidate of candidates) {
|
|
586
|
-
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
587
|
-
return candidate;
|
|
588
|
-
}
|
|
104
|
+
function notify(instanceId: string): void {
|
|
105
|
+
for (const listener of listeners.get(instanceId) ?? []) {
|
|
106
|
+
listener();
|
|
589
107
|
}
|
|
590
|
-
return undefined;
|
|
591
108
|
}
|
|
592
109
|
|
|
593
|
-
function
|
|
594
|
-
|
|
595
|
-
error
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
for (const candidate of candidates) {
|
|
600
|
-
if (typeof candidate === "string" && candidate.trim()) {
|
|
601
|
-
return candidate;
|
|
602
|
-
}
|
|
110
|
+
async function loadNativeGatewayModule(): Promise<NativeGatewayModule> {
|
|
111
|
+
if (!nativeGatewayModulePromise) {
|
|
112
|
+
nativeGatewayModulePromise = nativeGatewayModuleLoader().catch((error) => {
|
|
113
|
+
nativeGatewayModulePromise = null;
|
|
114
|
+
throw error;
|
|
115
|
+
});
|
|
603
116
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const text = (message || "").toLowerCase();
|
|
610
|
-
return text.includes("client id is already in use")
|
|
611
|
-
|| text.includes("clientid is already in use")
|
|
612
|
-
|| text.includes("client id already in use");
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function buildClientLockPath(config: IbkrGatewayConfig, clientId: number): string {
|
|
616
|
-
const host = (config.host || "localhost").replace(/[^a-z0-9_.-]+/gi, "_");
|
|
617
|
-
return join(IBKR_CLIENT_LOCK_DIR, `${host}-${config.port}-${clientId}.lock`);
|
|
117
|
+
const module = await nativeGatewayModulePromise;
|
|
118
|
+
loadedNativeGatewayModule = module;
|
|
119
|
+
nativeGatewayManager = module.ibkrGatewayManager;
|
|
120
|
+
module.setResolvedIbkrGatewayListener(resolvedGatewayListener);
|
|
121
|
+
return module;
|
|
618
122
|
}
|
|
619
123
|
|
|
620
|
-
function
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
contract.conId ?? "",
|
|
624
|
-
contract.localSymbol ?? "",
|
|
625
|
-
contract.symbol ?? "",
|
|
626
|
-
contract.secType ?? "",
|
|
627
|
-
].join("|");
|
|
124
|
+
async function getNativeService(instanceId: string): Promise<NativeGatewayService> {
|
|
125
|
+
const module = await loadNativeGatewayModule();
|
|
126
|
+
return module.ibkrGatewayManager.getService(instanceId);
|
|
628
127
|
}
|
|
629
128
|
|
|
630
|
-
function
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
return false;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
async function readClientLock(path: string): Promise<ClientLockMetadata | null> {
|
|
641
|
-
try {
|
|
642
|
-
const raw = await readFile(path, "utf-8");
|
|
643
|
-
const parsed = JSON.parse(raw) as Partial<ClientLockMetadata>;
|
|
644
|
-
if (
|
|
645
|
-
typeof parsed.pid !== "number"
|
|
646
|
-
|| typeof parsed.ownerToken !== "string"
|
|
647
|
-
|| typeof parsed.clientId !== "number"
|
|
648
|
-
|| typeof parsed.requestedClientId !== "number"
|
|
649
|
-
) {
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
return {
|
|
653
|
-
pid: parsed.pid,
|
|
654
|
-
ownerToken: parsed.ownerToken,
|
|
655
|
-
clientId: parsed.clientId,
|
|
656
|
-
requestedClientId: parsed.requestedClientId,
|
|
657
|
-
host: typeof parsed.host === "string" ? parsed.host : "",
|
|
658
|
-
port: typeof parsed.port === "number" ? parsed.port : 0,
|
|
659
|
-
instanceId: typeof parsed.instanceId === "string" ? parsed.instanceId : undefined,
|
|
660
|
-
cwd: typeof parsed.cwd === "string" ? parsed.cwd : "",
|
|
661
|
-
timestamp: typeof parsed.timestamp === "number" ? parsed.timestamp : 0,
|
|
662
|
-
};
|
|
663
|
-
} catch {
|
|
664
|
-
return null;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function buildConnectionNote(
|
|
669
|
-
requestedClientId: number,
|
|
670
|
-
actualClientId: number,
|
|
671
|
-
requestedPort: number | undefined,
|
|
672
|
-
actualPort: number,
|
|
673
|
-
): string | undefined {
|
|
674
|
-
const notes: string[] = [];
|
|
675
|
-
if (requestedPort == null || requestedPort !== actualPort) {
|
|
676
|
-
notes.push(`Detected IBKR API on port ${actualPort}.`);
|
|
129
|
+
export function setResolvedIbkrGatewayListener(listener: ResolvedGatewayListener | null): void {
|
|
130
|
+
resolvedGatewayListener = listener;
|
|
131
|
+
if (getBrokerRemoteClient()) return;
|
|
132
|
+
if (loadedNativeGatewayModule) {
|
|
133
|
+
loadedNativeGatewayModule.setResolvedIbkrGatewayListener(listener);
|
|
134
|
+
return;
|
|
677
135
|
}
|
|
678
|
-
if (
|
|
679
|
-
|
|
136
|
+
if (!nativeGatewayModulePromise) {
|
|
137
|
+
return;
|
|
680
138
|
}
|
|
681
|
-
|
|
139
|
+
void nativeGatewayModulePromise.then((module) => {
|
|
140
|
+
module.setResolvedIbkrGatewayListener(listener);
|
|
141
|
+
}).catch(() => {});
|
|
682
142
|
}
|
|
683
143
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function isLoopbackHost(host: string): boolean {
|
|
689
|
-
return LOCAL_IBKR_HOSTS.has(host.trim().toLowerCase());
|
|
690
|
-
}
|
|
144
|
+
class IbkrGatewayServiceFacade {
|
|
145
|
+
constructor(private readonly instanceId: string) {}
|
|
691
146
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (settled) return;
|
|
697
|
-
settled = true;
|
|
698
|
-
socket.removeAllListeners();
|
|
699
|
-
socket.destroy();
|
|
700
|
-
resolve(result);
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
const socket = createConnection({ host, port });
|
|
704
|
-
socket.once("connect", () => finish(true));
|
|
705
|
-
socket.once("error", () => finish(false));
|
|
706
|
-
socket.setTimeout(timeoutMs, () => finish(false));
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function uniquePorts(...values: Array<number | undefined | readonly number[]>): number[] {
|
|
711
|
-
const unique = new Set<number>();
|
|
712
|
-
for (const value of values) {
|
|
713
|
-
if (Array.isArray(value)) {
|
|
714
|
-
for (const candidate of value) {
|
|
715
|
-
if (Number.isFinite(candidate)) unique.add(candidate);
|
|
716
|
-
}
|
|
717
|
-
continue;
|
|
147
|
+
getSnapshot(): IbkrSnapshot {
|
|
148
|
+
const remoteStatus = getBrokerRemoteClient()?.getStatus(this.instanceId);
|
|
149
|
+
if (remoteStatus) {
|
|
150
|
+
return cacheSnapshot(this.instanceId, { status: remoteStatus });
|
|
718
151
|
}
|
|
719
|
-
if (
|
|
720
|
-
|
|
152
|
+
if (nativeGatewayManager) {
|
|
153
|
+
return nativeGatewayManager.getService(this.instanceId).getSnapshot();
|
|
721
154
|
}
|
|
722
|
-
|
|
723
|
-
return [...unique];
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
interface IbkrPortDiagnosticOptions {
|
|
727
|
-
candidatePorts?: readonly number[];
|
|
728
|
-
probePort?: (host: string, port: number) => Promise<boolean>;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
export async function diagnoseLocalIbkrPortIssue(
|
|
732
|
-
config: IbkrGatewayConfig,
|
|
733
|
-
options: IbkrPortDiagnosticOptions = {},
|
|
734
|
-
): Promise<string | null> {
|
|
735
|
-
if (typeof config.port !== "number" || !Number.isFinite(config.port)) return null;
|
|
736
|
-
if (!isLoopbackHost(config.host)) return null;
|
|
737
|
-
|
|
738
|
-
const candidatePorts = options.candidatePorts ?? COMMON_LOCAL_IBKR_PORTS;
|
|
739
|
-
const probePort = options.probePort ?? ((host: string, port: number) => probeTcpPort(host, port));
|
|
740
|
-
if (await probePort(config.host, config.port)) return null;
|
|
741
|
-
|
|
742
|
-
const openAlternatives: number[] = [];
|
|
743
|
-
for (const port of candidatePorts) {
|
|
744
|
-
if (port === config.port) continue;
|
|
745
|
-
if (await probePort(config.host, port)) openAlternatives.push(port);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
if (openAlternatives.length === 0) {
|
|
749
|
-
return `IBKR is not listening on ${config.host}:${config.port}. Start Gateway/TWS and confirm the API socket port in the IBKR API settings.`;
|
|
155
|
+
return getSnapshotEntry(this.instanceId);
|
|
750
156
|
}
|
|
751
157
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
export async function resolveGatewayConnection(
|
|
757
|
-
config: IbkrGatewayConfig,
|
|
758
|
-
options: IbkrPortDiagnosticOptions = {},
|
|
759
|
-
): Promise<ResolvedIbkrGatewayConnection> {
|
|
760
|
-
const host = (config.host || "127.0.0.1").trim() || "127.0.0.1";
|
|
761
|
-
const requestedPort = typeof config.port === "number" && Number.isFinite(config.port) ? config.port : undefined;
|
|
762
|
-
const requestedClientId = typeof config.clientId === "number" && Number.isFinite(config.clientId)
|
|
763
|
-
? config.clientId
|
|
764
|
-
: typeof config.lastSuccessfulClientId === "number" && Number.isFinite(config.lastSuccessfulClientId)
|
|
765
|
-
? config.lastSuccessfulClientId
|
|
766
|
-
: DEFAULT_AUTO_CLIENT_ID;
|
|
767
|
-
const probePort = options.probePort ?? ((candidateHost: string, candidatePort: number) => probeTcpPort(candidateHost, candidatePort));
|
|
768
|
-
|
|
769
|
-
if (requestedPort != null) {
|
|
770
|
-
const localPortIssue = await diagnoseLocalIbkrPortIssue({
|
|
771
|
-
host,
|
|
772
|
-
port: requestedPort,
|
|
773
|
-
clientId: requestedClientId,
|
|
774
|
-
marketDataType: config.marketDataType,
|
|
775
|
-
}, options);
|
|
776
|
-
if (localPortIssue) throw new Error(localPortIssue);
|
|
777
|
-
return {
|
|
778
|
-
host,
|
|
779
|
-
port: requestedPort,
|
|
780
|
-
clientId: requestedClientId,
|
|
781
|
-
requestedPort,
|
|
782
|
-
requestedClientId,
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (isLoopbackHost(host)) {
|
|
787
|
-
const candidatePorts = uniquePorts(config.lastSuccessfulPort, options.candidatePorts ?? COMMON_LOCAL_IBKR_PORTS);
|
|
788
|
-
for (const port of candidatePorts) {
|
|
789
|
-
if (await probePort(host, port)) {
|
|
790
|
-
return {
|
|
791
|
-
host,
|
|
792
|
-
port,
|
|
793
|
-
clientId: requestedClientId,
|
|
794
|
-
requestedPort: typeof config.lastSuccessfulPort === "number" ? config.lastSuccessfulPort : undefined,
|
|
795
|
-
requestedClientId,
|
|
796
|
-
};
|
|
797
|
-
}
|
|
158
|
+
getResolvedConnection(): ResolvedIbkrGatewayConnection | null {
|
|
159
|
+
if (nativeGatewayManager) {
|
|
160
|
+
return nativeGatewayManager.getService(this.instanceId).getResolvedConnection();
|
|
798
161
|
}
|
|
799
|
-
|
|
800
|
-
throw new Error(
|
|
801
|
-
`No local IBKR API listeners were detected on ${host}. Checked ports ${candidatePorts.join(", ")}. Start Gateway/TWS and enable socket clients in the IBKR API settings.`,
|
|
802
|
-
);
|
|
162
|
+
return null;
|
|
803
163
|
}
|
|
804
164
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
return {
|
|
809
|
-
host,
|
|
810
|
-
port: fallbackPort,
|
|
811
|
-
clientId: requestedClientId,
|
|
812
|
-
requestedPort: typeof config.lastSuccessfulPort === "number" ? config.lastSuccessfulPort : undefined,
|
|
813
|
-
requestedClientId,
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
export class IbkrGatewayService {
|
|
818
|
-
private api: IBApiNext | null = null;
|
|
819
|
-
private configKey: string | null = null;
|
|
820
|
-
private connecting: Promise<void> | null = null;
|
|
821
|
-
/** Account IDs captured from the managedAccounts event on initial connection. */
|
|
822
|
-
private cachedAccountIds: string[] = [];
|
|
823
|
-
private snapshot: IbkrSnapshot = DEFAULT_SNAPSHOT;
|
|
824
|
-
private listeners = new Set<() => void>();
|
|
825
|
-
private activeMarketDataType: MarketDataType = MarketDataType.REALTIME;
|
|
826
|
-
private autoMarketData = true;
|
|
827
|
-
/** Dedup in-flight requests: reuse pending promises for the same key. */
|
|
828
|
-
private pendingRequests = new Map<string, Promise<any>>();
|
|
829
|
-
private readonly clientLockOwner = `${process.pid}:${Math.random().toString(36).slice(2, 10)}`;
|
|
830
|
-
private claimedClientLock: ClaimedClientLock | null = null;
|
|
831
|
-
private connectionNote?: string;
|
|
832
|
-
private resolvedConnection: ResolvedIbkrGatewayConnection | null = null;
|
|
833
|
-
private readonly quoteStreams = new Map<string, ActiveQuoteStream>();
|
|
834
|
-
private readonly quoteStreamStarts = new Map<string, Promise<void>>();
|
|
835
|
-
|
|
836
|
-
constructor(private readonly instanceId?: string) {}
|
|
837
|
-
|
|
838
|
-
/** Dedup concurrent calls with the same key — returns cached promise if one is in flight. */
|
|
839
|
-
private dedup<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
840
|
-
const existing = this.pendingRequests.get(key);
|
|
841
|
-
if (existing) return existing as Promise<T>;
|
|
842
|
-
const promise = fn().finally(() => this.pendingRequests.delete(key));
|
|
843
|
-
this.pendingRequests.set(key, promise);
|
|
844
|
-
return promise;
|
|
845
|
-
}
|
|
165
|
+
subscribe(listener: Listener): () => void {
|
|
166
|
+
if (!listeners.has(this.instanceId)) listeners.set(this.instanceId, new Set());
|
|
167
|
+
listeners.get(this.instanceId)!.add(listener);
|
|
846
168
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
169
|
+
const remoteDispose = getBrokerRemoteClient()?.subscribeStatus(this.instanceId, () => {
|
|
170
|
+
const status = getBrokerRemoteClient()?.getStatus(this.instanceId);
|
|
171
|
+
if (status) {
|
|
172
|
+
updateSnapshot(this.instanceId, { status });
|
|
173
|
+
} else {
|
|
174
|
+
listener();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
851
177
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
178
|
+
if (remoteDispose) {
|
|
179
|
+
return () => {
|
|
180
|
+
remoteDispose();
|
|
181
|
+
listeners.get(this.instanceId)?.delete(listener);
|
|
182
|
+
};
|
|
183
|
+
}
|
|
855
184
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
185
|
+
let nativeDispose: (() => void) | null = null;
|
|
186
|
+
if (nativeGatewayManager) {
|
|
187
|
+
const service = nativeGatewayManager.getService(this.instanceId);
|
|
188
|
+
nativeDispose = service.subscribe(() => {
|
|
189
|
+
updateSnapshot(this.instanceId, service.getSnapshot());
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
listener();
|
|
859
193
|
|
|
860
|
-
|
|
861
|
-
|
|
194
|
+
return () => {
|
|
195
|
+
nativeDispose?.();
|
|
196
|
+
listeners.get(this.instanceId)?.delete(listener);
|
|
197
|
+
};
|
|
862
198
|
}
|
|
863
199
|
|
|
864
200
|
async connect(config: IbkrGatewayConfig): Promise<void> {
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
clientId: config.clientId ?? null,
|
|
869
|
-
marketDataType: config.marketDataType ?? "auto",
|
|
870
|
-
});
|
|
871
|
-
if (this.api?.isConnected && this.configKey === nextKey) {
|
|
201
|
+
const remote = getBrokerRemoteClient();
|
|
202
|
+
if (remote) {
|
|
203
|
+
await remote.invoke<void>(this.instanceId, "connect");
|
|
872
204
|
return;
|
|
873
205
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
this.connecting = this.connectWithClientFallback(config, nextKey);
|
|
879
|
-
try {
|
|
880
|
-
await this.connecting;
|
|
881
|
-
} finally {
|
|
882
|
-
this.connecting = null;
|
|
883
|
-
}
|
|
206
|
+
const service = await getNativeService(this.instanceId);
|
|
207
|
+
await service.connect(config);
|
|
208
|
+
updateSnapshot(this.instanceId, service.getSnapshot());
|
|
884
209
|
}
|
|
885
210
|
|
|
886
211
|
async disconnect(): Promise<void> {
|
|
887
|
-
|
|
888
|
-
this.
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
this.
|
|
892
|
-
this.activeMarketDataType = MarketDataType.REALTIME;
|
|
893
|
-
this.connectionNote = undefined;
|
|
894
|
-
this.resolvedConnection = null;
|
|
895
|
-
await this.releaseClientLock();
|
|
896
|
-
this.updateSnapshot({
|
|
897
|
-
...this.snapshot,
|
|
898
|
-
status: { state: "disconnected", updatedAt: Date.now() },
|
|
899
|
-
accounts: [],
|
|
900
|
-
openOrders: [],
|
|
901
|
-
executions: [],
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
async searchInstruments(query: string, config: IbkrGatewayConfig): Promise<InstrumentSearchResult[]> {
|
|
906
|
-
await this.connect(config);
|
|
907
|
-
const [descriptions, directMatches] = await Promise.all([
|
|
908
|
-
withTimeout(this.api!.getMatchingSymbols(query), IBKR_DATA_TIMEOUT, "getMatchingSymbols").catch(() => [] as ContractDescription[]),
|
|
909
|
-
withTimeout(this.getDirectContractMatches(query), IBKR_DATA_TIMEOUT, "getDirectContractMatches").catch(() => [] as InstrumentSearchResult[]),
|
|
910
|
-
]);
|
|
911
|
-
const merged = new Map<string, InstrumentSearchResult>();
|
|
912
|
-
|
|
913
|
-
for (const result of descriptions
|
|
914
|
-
.map((description) => this.contractDescriptionToSearchResult(description))
|
|
915
|
-
.filter((value): value is InstrumentSearchResult => value != null)) {
|
|
916
|
-
const key = `${result.symbol}|${result.exchange}|${result.type}`;
|
|
917
|
-
merged.set(key, result);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
for (const result of directMatches) {
|
|
921
|
-
const key = `${result.symbol}|${result.exchange}|${result.type}`;
|
|
922
|
-
merged.set(key, result);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
return [...merged.values()];
|
|
212
|
+
const remote = getBrokerRemoteClient();
|
|
213
|
+
if (remote) return remote.invoke<void>(this.instanceId, "disconnect");
|
|
214
|
+
const service = await getNativeService(this.instanceId);
|
|
215
|
+
await service.disconnect();
|
|
216
|
+
updateSnapshot(this.instanceId, service.getSnapshot());
|
|
926
217
|
}
|
|
927
218
|
|
|
928
219
|
async getAccounts(config: IbkrGatewayConfig): Promise<BrokerAccount[]> {
|
|
929
|
-
|
|
930
|
-
|
|
220
|
+
const remote = getBrokerRemoteClient();
|
|
221
|
+
const accounts = remote
|
|
222
|
+
? await remote.invoke<BrokerAccount[]>(this.instanceId, "listAccounts")
|
|
223
|
+
: await (await getNativeService(this.instanceId)).getAccounts(config);
|
|
224
|
+
updateSnapshot(this.instanceId, { accounts });
|
|
225
|
+
return accounts;
|
|
931
226
|
}
|
|
932
227
|
|
|
933
228
|
async getPositions(config: IbkrGatewayConfig): Promise<BrokerPosition[]> {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
for (const [accountId, accountPositions] of update.all) {
|
|
939
|
-
for (const position of accountPositions) {
|
|
940
|
-
if (!position.contract.symbol) continue;
|
|
941
|
-
const portfolioSnapshot = portfolioSnapshots.get(buildPortfolioPositionKey(accountId, position.contract));
|
|
942
|
-
const priceDivisor = getIbkrPriceDivisor(position.contract);
|
|
943
|
-
positions.push({
|
|
944
|
-
ticker: position.contract.localSymbol || position.contract.symbol,
|
|
945
|
-
exchange: position.contract.primaryExch || position.contract.exchange || "",
|
|
946
|
-
shares: Math.abs(position.pos),
|
|
947
|
-
avgCost: normalizeIbkrPriceValue(portfolioSnapshot?.avgCost ?? position.avgCost, priceDivisor),
|
|
948
|
-
currency: position.contract.currency || "USD",
|
|
949
|
-
accountId,
|
|
950
|
-
name: position.contract.description || position.contract.localSymbol || position.contract.symbol,
|
|
951
|
-
assetCategory: position.contract.secType,
|
|
952
|
-
markPrice: normalizeIbkrPriceValue(portfolioSnapshot?.marketPrice ?? position.marketPrice, priceDivisor),
|
|
953
|
-
marketValue: portfolioSnapshot?.marketValue ?? position.marketValue,
|
|
954
|
-
unrealizedPnl: portfolioSnapshot?.unrealizedPNL ?? position.unrealizedPNL,
|
|
955
|
-
side: position.pos < 0 ? "short" : "long",
|
|
956
|
-
multiplier: position.contract.multiplier,
|
|
957
|
-
brokerContract: this.contractToRef(position.contract),
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
return positions;
|
|
229
|
+
const remote = getBrokerRemoteClient();
|
|
230
|
+
return remote
|
|
231
|
+
? remote.invoke(this.instanceId, "importPositions")
|
|
232
|
+
: (await getNativeService(this.instanceId)).getPositions(config);
|
|
962
233
|
}
|
|
963
234
|
|
|
964
|
-
async
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
return
|
|
971
|
-
await this.connect(config);
|
|
972
|
-
const contract = await withTimeout(this.resolveContract(ticker, exchange, instrument ?? null), IBKR_DATA_TIMEOUT, "resolveContract");
|
|
973
|
-
const details = await withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails");
|
|
974
|
-
const marketData = await this.withMarketDataFallback(
|
|
975
|
-
config,
|
|
976
|
-
() => withTimeout(this.api!.getMarketDataSnapshot(contract, "", false), IBKR_DATA_TIMEOUT, "getMarketDataSnapshot"),
|
|
977
|
-
);
|
|
978
|
-
const quote = this.marketDataToQuote(contract, details, marketData);
|
|
979
|
-
quote.dataSource = hasDelayedMarketData(marketData) || this.activeMarketDataType !== MarketDataType.REALTIME
|
|
980
|
-
? "delayed"
|
|
981
|
-
: "live";
|
|
982
|
-
return quote;
|
|
983
|
-
});
|
|
235
|
+
async listOpenOrders(config: IbkrGatewayConfig): Promise<BrokerOrder[]> {
|
|
236
|
+
const remote = getBrokerRemoteClient();
|
|
237
|
+
const openOrders = remote
|
|
238
|
+
? await remote.invoke<BrokerOrder[]>(this.instanceId, "listOpenOrders")
|
|
239
|
+
: await (await getNativeService(this.instanceId)).listOpenOrders(config);
|
|
240
|
+
updateSnapshot(this.instanceId, { openOrders });
|
|
241
|
+
return openOrders;
|
|
984
242
|
}
|
|
985
243
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
.filter((target): target is QuoteSubscriptionTarget => target != null)
|
|
995
|
-
.map((target) => [buildQuoteStreamKey(target), target] as const),
|
|
996
|
-
).values()];
|
|
997
|
-
|
|
998
|
-
for (const target of uniqueTargets) {
|
|
999
|
-
const key = buildQuoteStreamKey(target);
|
|
1000
|
-
const stream = this.quoteStreams.get(key) ?? {
|
|
1001
|
-
target,
|
|
1002
|
-
listeners: new Map<QuoteStreamListener, QuoteSubscriptionTarget>(),
|
|
1003
|
-
stop: () => {},
|
|
1004
|
-
};
|
|
1005
|
-
stream.target = target;
|
|
1006
|
-
stream.listeners.set(onQuote, target);
|
|
1007
|
-
this.quoteStreams.set(key, stream);
|
|
1008
|
-
|
|
1009
|
-
if (!this.quoteStreamStarts.has(key) && stream.listeners.size === 1) {
|
|
1010
|
-
const startPromise = this.ensureQuoteStream(key, target, config)
|
|
1011
|
-
.catch((error) => {
|
|
1012
|
-
gatewayLog.warn("Quote stream setup failed", {
|
|
1013
|
-
instanceId: this.instanceId,
|
|
1014
|
-
symbol: target.symbol,
|
|
1015
|
-
exchange: target.exchange ?? "",
|
|
1016
|
-
error: error instanceof Error ? error.message : String(error ?? ""),
|
|
1017
|
-
});
|
|
1018
|
-
})
|
|
1019
|
-
.finally(() => {
|
|
1020
|
-
this.quoteStreamStarts.delete(key);
|
|
1021
|
-
});
|
|
1022
|
-
this.quoteStreamStarts.set(key, startPromise);
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
244
|
+
async listExecutions(config: IbkrGatewayConfig): Promise<BrokerExecution[]> {
|
|
245
|
+
const remote = getBrokerRemoteClient();
|
|
246
|
+
const executions = remote
|
|
247
|
+
? await remote.invoke<BrokerExecution[]>(this.instanceId, "listExecutions")
|
|
248
|
+
: await (await getNativeService(this.instanceId)).listExecutions(config);
|
|
249
|
+
updateSnapshot(this.instanceId, { executions });
|
|
250
|
+
return executions;
|
|
251
|
+
}
|
|
1025
252
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
stream.listeners.delete(onQuote);
|
|
1032
|
-
if (stream.listeners.size === 0) {
|
|
1033
|
-
stream.stop();
|
|
1034
|
-
this.quoteStreams.delete(key);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
};
|
|
253
|
+
async searchInstruments(query: string, config: IbkrGatewayConfig): Promise<InstrumentSearchResult[]> {
|
|
254
|
+
const remote = getBrokerRemoteClient();
|
|
255
|
+
return remote
|
|
256
|
+
? remote.invoke(this.instanceId, "searchInstruments", [query])
|
|
257
|
+
: (await getNativeService(this.instanceId)).searchInstruments(query, config);
|
|
1038
258
|
}
|
|
1039
259
|
|
|
1040
260
|
async getTickerFinancials(
|
|
@@ -1043,49 +263,22 @@ export class IbkrGatewayService {
|
|
|
1043
263
|
exchange = "",
|
|
1044
264
|
instrument?: BrokerContractRef | null,
|
|
1045
265
|
): Promise<TickerFinancials> {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const [quote, priceHistory, snapshotXml, statementsXml] = await Promise.all([
|
|
1051
|
-
this.getQuote(ticker, config, exchange, instrument),
|
|
1052
|
-
this.getPriceHistory(ticker, config, exchange, "1Y", instrument),
|
|
1053
|
-
this.fetchFundamentalData(contract, "ReportSnapshot"),
|
|
1054
|
-
this.fetchFundamentalData(contract, "ReportsFinStatements"),
|
|
1055
|
-
]);
|
|
1056
|
-
|
|
1057
|
-
const fundamentals = snapshotXml ? parseReportSnapshot(snapshotXml) : {};
|
|
1058
|
-
const statements = statementsXml ? parseFinStatements(statementsXml) : { annual: [], quarterly: [] };
|
|
1059
|
-
|
|
1060
|
-
// Compute 1Y return from price history
|
|
1061
|
-
if (priceHistory.length >= 2) {
|
|
1062
|
-
const oldest = priceHistory[0]!.close;
|
|
1063
|
-
const newest = priceHistory[priceHistory.length - 1]!.close;
|
|
1064
|
-
if (oldest > 0) {
|
|
1065
|
-
fundamentals.return1Y = (newest - oldest) / oldest;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
return {
|
|
1070
|
-
quote,
|
|
1071
|
-
fundamentals,
|
|
1072
|
-
annualStatements: statements.annual,
|
|
1073
|
-
quarterlyStatements: statements.quarterly,
|
|
1074
|
-
priceHistory,
|
|
1075
|
-
};
|
|
1076
|
-
});
|
|
266
|
+
const remote = getBrokerRemoteClient();
|
|
267
|
+
return remote
|
|
268
|
+
? remote.invoke(this.instanceId, "getTickerFinancials", [ticker, exchange, instrument])
|
|
269
|
+
: (await getNativeService(this.instanceId)).getTickerFinancials(ticker, config, exchange, instrument);
|
|
1077
270
|
}
|
|
1078
271
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
272
|
+
async getQuote(
|
|
273
|
+
ticker: string,
|
|
274
|
+
config: IbkrGatewayConfig,
|
|
275
|
+
exchange = "",
|
|
276
|
+
instrument?: BrokerContractRef | null,
|
|
277
|
+
): Promise<Quote> {
|
|
278
|
+
const remote = getBrokerRemoteClient();
|
|
279
|
+
return remote
|
|
280
|
+
? remote.invoke(this.instanceId, "getQuote", [ticker, exchange, instrument])
|
|
281
|
+
: (await getNativeService(this.instanceId)).getQuote(ticker, config, exchange, instrument);
|
|
1089
282
|
}
|
|
1090
283
|
|
|
1091
284
|
async getPriceHistory(
|
|
@@ -1095,54 +288,22 @@ export class IbkrGatewayService {
|
|
|
1095
288
|
range: TimeRange,
|
|
1096
289
|
instrument?: BrokerContractRef | null,
|
|
1097
290
|
): Promise<PricePoint[]> {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const detailsPromise = withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails")
|
|
1103
|
-
.catch(() => undefined);
|
|
1104
|
-
const bars = await this.withMarketDataFallback(
|
|
1105
|
-
config,
|
|
1106
|
-
() => withTimeout(this.api!.getHistoricalData(
|
|
1107
|
-
contract,
|
|
1108
|
-
"",
|
|
1109
|
-
params.duration,
|
|
1110
|
-
params.size,
|
|
1111
|
-
WhatToShow.TRADES,
|
|
1112
|
-
1,
|
|
1113
|
-
1,
|
|
1114
|
-
), IBKR_DATA_TIMEOUT, "getHistoricalData"),
|
|
1115
|
-
);
|
|
1116
|
-
const details = await detailsPromise;
|
|
1117
|
-
const priceDivisor = getIbkrPriceDivisor(contract, details);
|
|
1118
|
-
|
|
1119
|
-
return bars.map((bar) => ({
|
|
1120
|
-
date: parseIbkrHistoricalBarTime(bar.time),
|
|
1121
|
-
open: normalizeIbkrPriceValue(bar.open, priceDivisor),
|
|
1122
|
-
high: normalizeIbkrPriceValue(bar.high, priceDivisor),
|
|
1123
|
-
low: normalizeIbkrPriceValue(bar.low, priceDivisor),
|
|
1124
|
-
close: normalizeIbkrPriceValue(bar.close ?? bar.open ?? bar.high ?? bar.low ?? 0, priceDivisor) ?? 0,
|
|
1125
|
-
volume: bar.volume,
|
|
1126
|
-
}));
|
|
1127
|
-
});
|
|
291
|
+
const remote = getBrokerRemoteClient();
|
|
292
|
+
return remote
|
|
293
|
+
? remote.invoke(this.instanceId, "getPriceHistory", [ticker, exchange, range, instrument])
|
|
294
|
+
: (await getNativeService(this.instanceId)).getPriceHistory(ticker, config, exchange, range, instrument);
|
|
1128
295
|
}
|
|
1129
296
|
|
|
1130
297
|
getChartResolutionSupport(
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
): ChartResolutionSupport[] {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
_ticker: string,
|
|
1141
|
-
_config: IbkrGatewayConfig,
|
|
1142
|
-
_exchange?: string,
|
|
1143
|
-
_instrument?: BrokerContractRef | null,
|
|
1144
|
-
): ManualChartResolution[] {
|
|
1145
|
-
return IBKR_RESOLUTION_SUPPORT.map((entry) => entry.resolution);
|
|
298
|
+
ticker: string,
|
|
299
|
+
config: IbkrGatewayConfig,
|
|
300
|
+
exchange = "",
|
|
301
|
+
instrument?: BrokerContractRef | null,
|
|
302
|
+
): Promise<ChartResolutionSupport[]> | ChartResolutionSupport[] {
|
|
303
|
+
const remote = getBrokerRemoteClient();
|
|
304
|
+
return remote
|
|
305
|
+
? remote.invoke(this.instanceId, "getChartResolutionSupport", [ticker, exchange, instrument])
|
|
306
|
+
: getNativeService(this.instanceId).then((service) => service.getChartResolutionSupport(ticker, config, exchange, instrument));
|
|
1146
307
|
}
|
|
1147
308
|
|
|
1148
309
|
async getPriceHistoryForResolution(
|
|
@@ -1153,39 +314,10 @@ export class IbkrGatewayService {
|
|
|
1153
314
|
resolution: ManualChartResolution,
|
|
1154
315
|
instrument?: BrokerContractRef | null,
|
|
1155
316
|
): Promise<PricePoint[]> {
|
|
1156
|
-
const
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
await this.connect(config);
|
|
1161
|
-
const contract = await withTimeout(this.resolveContract(ticker, exchange, instrument ?? null), IBKR_DATA_TIMEOUT, "resolveContract");
|
|
1162
|
-
const params = HISTORY_PARAMS[bufferRange];
|
|
1163
|
-
const detailsPromise = withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails")
|
|
1164
|
-
.catch(() => undefined);
|
|
1165
|
-
const bars = await this.withMarketDataFallback(
|
|
1166
|
-
config,
|
|
1167
|
-
() => withTimeout(this.api!.getHistoricalData(
|
|
1168
|
-
contract,
|
|
1169
|
-
"",
|
|
1170
|
-
params.duration,
|
|
1171
|
-
ibkrBarSize,
|
|
1172
|
-
WhatToShow.TRADES,
|
|
1173
|
-
1,
|
|
1174
|
-
1,
|
|
1175
|
-
), IBKR_DATA_TIMEOUT, "getHistoricalData"),
|
|
1176
|
-
);
|
|
1177
|
-
const details = await detailsPromise;
|
|
1178
|
-
const priceDivisor = getIbkrPriceDivisor(contract, details);
|
|
1179
|
-
|
|
1180
|
-
return bars.map((bar) => ({
|
|
1181
|
-
date: parseIbkrHistoricalBarTime(bar.time),
|
|
1182
|
-
open: normalizeIbkrPriceValue(bar.open, priceDivisor),
|
|
1183
|
-
high: normalizeIbkrPriceValue(bar.high, priceDivisor),
|
|
1184
|
-
low: normalizeIbkrPriceValue(bar.low, priceDivisor),
|
|
1185
|
-
close: normalizeIbkrPriceValue(bar.close ?? bar.open ?? bar.high ?? bar.low ?? 0, priceDivisor) ?? 0,
|
|
1186
|
-
volume: bar.volume,
|
|
1187
|
-
}));
|
|
1188
|
-
});
|
|
317
|
+
const remote = getBrokerRemoteClient();
|
|
318
|
+
return remote
|
|
319
|
+
? remote.invoke(this.instanceId, "getPriceHistoryForResolution", [ticker, exchange, bufferRange, resolution, instrument])
|
|
320
|
+
: (await getNativeService(this.instanceId)).getPriceHistoryForResolution(ticker, config, exchange, bufferRange, resolution, instrument);
|
|
1189
321
|
}
|
|
1190
322
|
|
|
1191
323
|
async getDetailedPriceHistory(
|
|
@@ -1197,1181 +329,68 @@ export class IbkrGatewayService {
|
|
|
1197
329
|
barSize: string,
|
|
1198
330
|
instrument?: BrokerContractRef | null,
|
|
1199
331
|
): Promise<PricePoint[]> {
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
return this.dedup(key, async () => {
|
|
1205
|
-
await this.connect(config);
|
|
1206
|
-
const contract = await withTimeout(this.resolveContract(ticker, exchange, instrument ?? null), IBKR_DATA_TIMEOUT, "resolveContract");
|
|
1207
|
-
const detailsPromise = withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails")
|
|
1208
|
-
.catch(() => undefined);
|
|
1209
|
-
|
|
1210
|
-
// Format endDateTime as "yyyyMMdd HH:mm:ss" for IBKR
|
|
1211
|
-
const pad = (n: number) => String(n).padStart(2, "0");
|
|
1212
|
-
const endDateTime = `${endDate.getFullYear()}${pad(endDate.getMonth() + 1)}${pad(endDate.getDate())} ${pad(endDate.getHours())}:${pad(endDate.getMinutes())}:${pad(endDate.getSeconds())}`;
|
|
1213
|
-
|
|
1214
|
-
// Compute duration from date range
|
|
1215
|
-
const spanMs = endDate.getTime() - startDate.getTime();
|
|
1216
|
-
const spanDays = Math.ceil(spanMs / (1000 * 60 * 60 * 24));
|
|
1217
|
-
let durationStr: string;
|
|
1218
|
-
if (spanDays <= 1) durationStr = `${Math.max(Math.ceil(spanMs / (1000 * 60 * 60)), 1)} hours`;
|
|
1219
|
-
else if (spanDays <= 30) durationStr = `${spanDays} D`;
|
|
1220
|
-
else if (spanDays <= 365) durationStr = `${Math.ceil(spanDays / 30)} M`;
|
|
1221
|
-
else durationStr = `${Math.ceil(spanDays / 365)} Y`;
|
|
1222
|
-
|
|
1223
|
-
const bars = await this.withMarketDataFallback(
|
|
1224
|
-
config,
|
|
1225
|
-
() => withTimeout(this.api!.getHistoricalData(
|
|
1226
|
-
contract,
|
|
1227
|
-
endDateTime,
|
|
1228
|
-
durationStr,
|
|
1229
|
-
ibkrBarSize,
|
|
1230
|
-
WhatToShow.TRADES,
|
|
1231
|
-
1,
|
|
1232
|
-
1,
|
|
1233
|
-
), IBKR_DATA_TIMEOUT, "getDetailedHistoricalData"),
|
|
1234
|
-
);
|
|
1235
|
-
const details = await detailsPromise;
|
|
1236
|
-
const priceDivisor = getIbkrPriceDivisor(contract, details);
|
|
1237
|
-
|
|
1238
|
-
return bars.map((bar) => ({
|
|
1239
|
-
date: parseIbkrHistoricalBarTime(bar.time),
|
|
1240
|
-
open: normalizeIbkrPriceValue(bar.open, priceDivisor),
|
|
1241
|
-
high: normalizeIbkrPriceValue(bar.high, priceDivisor),
|
|
1242
|
-
low: normalizeIbkrPriceValue(bar.low, priceDivisor),
|
|
1243
|
-
close: normalizeIbkrPriceValue(bar.close ?? bar.open ?? bar.high ?? bar.low ?? 0, priceDivisor) ?? 0,
|
|
1244
|
-
volume: bar.volume,
|
|
1245
|
-
}));
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
async listOpenOrders(config: IbkrGatewayConfig): Promise<BrokerOrder[]> {
|
|
1250
|
-
await this.connect(config);
|
|
1251
|
-
return this.loadOpenOrders();
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
async listExecutions(config: IbkrGatewayConfig): Promise<BrokerExecution[]> {
|
|
1255
|
-
await this.connect(config);
|
|
1256
|
-
return this.loadExecutions();
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
async previewOrder(config: IbkrGatewayConfig, request: BrokerOrderRequest): Promise<BrokerOrderPreview> {
|
|
1260
|
-
await this.connect(config);
|
|
1261
|
-
const contract = await this.resolveContract(request.contract.symbol, request.contract.exchange || "", request.contract);
|
|
1262
|
-
const rawApi = this.getRawApi();
|
|
1263
|
-
const orderId = await this.api!.getNextValidOrderId();
|
|
1264
|
-
const order = this.buildOrder(request, true);
|
|
1265
|
-
|
|
1266
|
-
return new Promise<BrokerOrderPreview>((resolve, reject) => {
|
|
1267
|
-
const timeoutId = setTimeout(() => {
|
|
1268
|
-
cleanup();
|
|
1269
|
-
reject(new Error("Timed out while previewing order"));
|
|
1270
|
-
}, 10_000);
|
|
1271
|
-
|
|
1272
|
-
const cleanup = () => {
|
|
1273
|
-
clearTimeout(timeoutId);
|
|
1274
|
-
rawApi.off(EventName.openOrder, onOpenOrder);
|
|
1275
|
-
rawApi.off(EventName.error, onError);
|
|
1276
|
-
};
|
|
1277
|
-
|
|
1278
|
-
const onOpenOrder = (incomingOrderId: number, _contract: Contract, _order: Order, orderState: OrderState) => {
|
|
1279
|
-
if (incomingOrderId !== orderId) return;
|
|
1280
|
-
cleanup();
|
|
1281
|
-
resolve({
|
|
1282
|
-
initMarginBefore: orderState.initMarginBefore,
|
|
1283
|
-
initMarginAfter: orderState.initMarginAfter,
|
|
1284
|
-
maintMarginBefore: orderState.maintMarginBefore,
|
|
1285
|
-
maintMarginAfter: orderState.maintMarginAfter,
|
|
1286
|
-
equityWithLoanBefore: orderState.equityWithLoanBefore,
|
|
1287
|
-
equityWithLoanAfter: orderState.equityWithLoanAfter,
|
|
1288
|
-
commission: orderState.commission,
|
|
1289
|
-
commissionCurrency: orderState.commissionCurrency,
|
|
1290
|
-
warningText: orderState.warningText,
|
|
1291
|
-
});
|
|
1292
|
-
};
|
|
1293
|
-
|
|
1294
|
-
const onError = (error: Error, code: number, reqId: number) => {
|
|
1295
|
-
if (reqId !== orderId) return;
|
|
1296
|
-
cleanup();
|
|
1297
|
-
reject(new Error(error.message || `IBKR error ${code}`));
|
|
1298
|
-
};
|
|
1299
|
-
|
|
1300
|
-
rawApi.on(EventName.openOrder, onOpenOrder);
|
|
1301
|
-
rawApi.on(EventName.error, onError);
|
|
1302
|
-
rawApi.placeOrder(orderId, contract, order);
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
async placeOrder(config: IbkrGatewayConfig, request: BrokerOrderRequest): Promise<BrokerOrder> {
|
|
1307
|
-
await this.connect(config);
|
|
1308
|
-
const contract = await this.resolveContract(request.contract.symbol, request.contract.exchange || "", request.contract);
|
|
1309
|
-
const order = this.buildOrder(request, false);
|
|
1310
|
-
const rawApi = this.getRawApi();
|
|
1311
|
-
const orderId = await this.api!.getNextValidOrderId();
|
|
1312
|
-
|
|
1313
|
-
await new Promise<void>((resolve, reject) => {
|
|
1314
|
-
const timeoutId = setTimeout(() => {
|
|
1315
|
-
cleanup();
|
|
1316
|
-
reject(new Error("Timed out waiting for order acknowledgement"));
|
|
1317
|
-
}, 10_000);
|
|
1318
|
-
|
|
1319
|
-
const cleanup = () => {
|
|
1320
|
-
clearTimeout(timeoutId);
|
|
1321
|
-
rawApi.off(EventName.openOrder, onOpenOrder);
|
|
1322
|
-
rawApi.off(EventName.orderStatus, onOrderStatus);
|
|
1323
|
-
rawApi.off(EventName.error, onError);
|
|
1324
|
-
};
|
|
1325
|
-
|
|
1326
|
-
const onOpenOrder = (incomingOrderId: number) => {
|
|
1327
|
-
if (incomingOrderId !== orderId) return;
|
|
1328
|
-
cleanup();
|
|
1329
|
-
resolve();
|
|
1330
|
-
};
|
|
1331
|
-
|
|
1332
|
-
const onOrderStatus = (incomingOrderId: number, status: string) => {
|
|
1333
|
-
if (incomingOrderId !== orderId) return;
|
|
1334
|
-
cleanup();
|
|
1335
|
-
resolve();
|
|
1336
|
-
};
|
|
1337
|
-
|
|
1338
|
-
const onError = (error: Error, code: number, reqId: number) => {
|
|
1339
|
-
if (reqId !== orderId) return;
|
|
1340
|
-
cleanup();
|
|
1341
|
-
reject(new Error(error.message || `IBKR error ${code}`));
|
|
1342
|
-
};
|
|
1343
|
-
|
|
1344
|
-
rawApi.on(EventName.openOrder, onOpenOrder);
|
|
1345
|
-
rawApi.on(EventName.orderStatus, onOrderStatus);
|
|
1346
|
-
rawApi.on(EventName.error, onError);
|
|
1347
|
-
rawApi.placeOrder(orderId, contract, order);
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
|
-
const openOrders = await this.listOpenOrders(config);
|
|
1351
|
-
return openOrders.find((openOrder) => openOrder.orderId === orderId) ?? {
|
|
1352
|
-
orderId,
|
|
1353
|
-
brokerInstanceId: this.instanceId,
|
|
1354
|
-
accountId: request.accountId,
|
|
1355
|
-
status: "Submitted",
|
|
1356
|
-
action: request.action,
|
|
1357
|
-
orderType: request.orderType,
|
|
1358
|
-
quantity: request.quantity,
|
|
1359
|
-
filled: 0,
|
|
1360
|
-
remaining: request.quantity,
|
|
1361
|
-
limitPrice: request.limitPrice,
|
|
1362
|
-
stopPrice: request.stopPrice,
|
|
1363
|
-
tif: request.tif,
|
|
1364
|
-
updatedAt: Date.now(),
|
|
1365
|
-
contract: request.contract,
|
|
1366
|
-
};
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
async modifyOrder(config: IbkrGatewayConfig, orderId: number, request: BrokerOrderRequest): Promise<BrokerOrder> {
|
|
1370
|
-
await this.connect(config);
|
|
1371
|
-
const contract = await this.resolveContract(request.contract.symbol, request.contract.exchange || "", request.contract);
|
|
1372
|
-
const rawApi = this.getRawApi();
|
|
1373
|
-
const order = this.buildOrder({ ...request }, false);
|
|
1374
|
-
|
|
1375
|
-
await new Promise<void>((resolve, reject) => {
|
|
1376
|
-
const timeoutId = setTimeout(() => {
|
|
1377
|
-
cleanup();
|
|
1378
|
-
reject(new Error("Timed out waiting for order modification acknowledgement"));
|
|
1379
|
-
}, 10_000);
|
|
1380
|
-
|
|
1381
|
-
const cleanup = () => {
|
|
1382
|
-
clearTimeout(timeoutId);
|
|
1383
|
-
rawApi.off(EventName.openOrder, onOpenOrder);
|
|
1384
|
-
rawApi.off(EventName.orderStatus, onOrderStatus);
|
|
1385
|
-
rawApi.off(EventName.error, onError);
|
|
1386
|
-
};
|
|
1387
|
-
|
|
1388
|
-
const onOpenOrder = (incomingOrderId: number) => {
|
|
1389
|
-
if (incomingOrderId !== orderId) return;
|
|
1390
|
-
cleanup();
|
|
1391
|
-
resolve();
|
|
1392
|
-
};
|
|
1393
|
-
|
|
1394
|
-
const onOrderStatus = (incomingOrderId: number, status: string) => {
|
|
1395
|
-
if (incomingOrderId !== orderId) return;
|
|
1396
|
-
cleanup();
|
|
1397
|
-
resolve();
|
|
1398
|
-
};
|
|
1399
|
-
|
|
1400
|
-
const onError = (error: Error, code: number, reqId: number) => {
|
|
1401
|
-
if (reqId !== orderId) return;
|
|
1402
|
-
cleanup();
|
|
1403
|
-
reject(new Error(error.message || `IBKR error ${code}`));
|
|
1404
|
-
};
|
|
1405
|
-
|
|
1406
|
-
rawApi.on(EventName.openOrder, onOpenOrder);
|
|
1407
|
-
rawApi.on(EventName.orderStatus, onOrderStatus);
|
|
1408
|
-
rawApi.on(EventName.error, onError);
|
|
1409
|
-
rawApi.placeOrder(orderId, contract, order);
|
|
1410
|
-
});
|
|
1411
|
-
|
|
1412
|
-
const openOrders = await this.listOpenOrders(config);
|
|
1413
|
-
return openOrders.find((openOrder) => openOrder.orderId === orderId) ?? {
|
|
1414
|
-
orderId,
|
|
1415
|
-
brokerInstanceId: this.instanceId,
|
|
1416
|
-
accountId: request.accountId,
|
|
1417
|
-
status: "Submitted",
|
|
1418
|
-
action: request.action,
|
|
1419
|
-
orderType: request.orderType,
|
|
1420
|
-
quantity: request.quantity,
|
|
1421
|
-
filled: 0,
|
|
1422
|
-
remaining: request.quantity,
|
|
1423
|
-
limitPrice: request.limitPrice,
|
|
1424
|
-
stopPrice: request.stopPrice,
|
|
1425
|
-
tif: request.tif,
|
|
1426
|
-
updatedAt: Date.now(),
|
|
1427
|
-
contract: request.contract,
|
|
1428
|
-
};
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
async cancelOrder(config: IbkrGatewayConfig, orderId: number): Promise<void> {
|
|
1432
|
-
await this.connect(config);
|
|
1433
|
-
this.api!.cancelOrder(orderId);
|
|
1434
|
-
await this.listOpenOrders(config);
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
private async connectWithClientFallback(config: IbkrGatewayConfig, configKey: string): Promise<void> {
|
|
1438
|
-
let resolvedConfig: ResolvedIbkrGatewayConnection;
|
|
1439
|
-
try {
|
|
1440
|
-
resolvedConfig = await resolveGatewayConnection(config);
|
|
1441
|
-
} catch (error: any) {
|
|
1442
|
-
const message = error?.message || String(error || "");
|
|
1443
|
-
this.updateSnapshot({
|
|
1444
|
-
...this.snapshot,
|
|
1445
|
-
status: {
|
|
1446
|
-
state: "error",
|
|
1447
|
-
updatedAt: Date.now(),
|
|
1448
|
-
mode: "gateway",
|
|
1449
|
-
message,
|
|
1450
|
-
},
|
|
1451
|
-
lastError: message,
|
|
1452
|
-
});
|
|
1453
|
-
throw error;
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
const candidates = this.getClientIdCandidates(resolvedConfig.requestedClientId);
|
|
1457
|
-
let lastConflict: Error | null = null;
|
|
1458
|
-
|
|
1459
|
-
for (const clientId of candidates) {
|
|
1460
|
-
const claimed = await this.tryClaimClientLock(resolvedConfig, clientId, resolvedConfig.requestedClientId);
|
|
1461
|
-
if (!claimed) continue;
|
|
1462
|
-
|
|
1463
|
-
try {
|
|
1464
|
-
await this.connectInternal(
|
|
1465
|
-
{ ...config, host: resolvedConfig.host, port: resolvedConfig.port, clientId },
|
|
1466
|
-
configKey,
|
|
1467
|
-
buildConnectionNote(resolvedConfig.requestedClientId, clientId, resolvedConfig.requestedPort, resolvedConfig.port),
|
|
1468
|
-
);
|
|
1469
|
-
this.resolvedConnection = {
|
|
1470
|
-
...resolvedConfig,
|
|
1471
|
-
clientId,
|
|
1472
|
-
};
|
|
1473
|
-
void resolvedGatewayListener?.(this.instanceId, this.resolvedConnection);
|
|
1474
|
-
return;
|
|
1475
|
-
} catch (error: any) {
|
|
1476
|
-
const code = getIbErrorCode(error);
|
|
1477
|
-
const message = getIbErrorMessage(error) || error?.message || String(error || "");
|
|
1478
|
-
await this.disconnect();
|
|
1479
|
-
if (!isClientIdInUseError(code, message)) {
|
|
1480
|
-
throw error;
|
|
1481
|
-
}
|
|
1482
|
-
lastConflict = new Error(message || `IBKR client ID ${clientId} is already in use.`);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
throw lastConflict ?? new Error(
|
|
1487
|
-
`No IBKR client IDs are available near ${resolvedConfig.requestedClientId}. Close other sessions or choose a different client ID.`,
|
|
1488
|
-
);
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
private async connectInternal(
|
|
1492
|
-
config: IbkrGatewayConfig,
|
|
1493
|
-
configKey: string,
|
|
1494
|
-
connectionNote?: string,
|
|
1495
|
-
): Promise<void> {
|
|
1496
|
-
if (this.api && this.configKey !== configKey) {
|
|
1497
|
-
await this.disconnect();
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
this.connectionNote = connectionNote;
|
|
1501
|
-
this.updateSnapshot({
|
|
1502
|
-
...this.snapshot,
|
|
1503
|
-
status: {
|
|
1504
|
-
state: "connecting",
|
|
1505
|
-
updatedAt: Date.now(),
|
|
1506
|
-
mode: "gateway",
|
|
1507
|
-
message: this.connectionNote,
|
|
1508
|
-
},
|
|
1509
|
-
lastError: undefined,
|
|
1510
|
-
});
|
|
1511
|
-
|
|
1512
|
-
if (!this.api) {
|
|
1513
|
-
this.api = new IBApiNext({
|
|
1514
|
-
host: config.host,
|
|
1515
|
-
port: config.port,
|
|
1516
|
-
reconnectInterval: 2_000,
|
|
1517
|
-
});
|
|
1518
|
-
this.bindConnectionEvents(this.api);
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
let unsubscribeConflict = () => {};
|
|
1522
|
-
const conflictPromise = new Promise<never>((_, reject) => {
|
|
1523
|
-
const subscription = this.api!.error.subscribe((err) => {
|
|
1524
|
-
const code = getIbErrorCode(err);
|
|
1525
|
-
const message = getIbErrorMessage(err);
|
|
1526
|
-
if (!isClientIdInUseError(code, message)) return;
|
|
1527
|
-
subscription.unsubscribe();
|
|
1528
|
-
reject(new Error(message || `IBKR client ID ${config.clientId} is already in use.`));
|
|
1529
|
-
});
|
|
1530
|
-
unsubscribeConflict = () => subscription.unsubscribe();
|
|
1531
|
-
});
|
|
1532
|
-
|
|
1533
|
-
this.api.connect(config.clientId);
|
|
1534
|
-
try {
|
|
1535
|
-
await Promise.race([
|
|
1536
|
-
firstValueFrom(this.api.connectionState.pipe(
|
|
1537
|
-
filter((state) => state === ConnectionState.Connected),
|
|
1538
|
-
take(1),
|
|
1539
|
-
timeout(10_000),
|
|
1540
|
-
)),
|
|
1541
|
-
conflictPromise,
|
|
1542
|
-
]);
|
|
1543
|
-
} finally {
|
|
1544
|
-
unsubscribeConflict();
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
gatewayLog.info("Connected to IBKR gateway", {
|
|
1548
|
-
instanceId: this.instanceId,
|
|
1549
|
-
requestedClientId: this.claimedClientLock?.requestedClientId ?? config.clientId,
|
|
1550
|
-
actualClientId: config.clientId,
|
|
1551
|
-
host: config.host,
|
|
1552
|
-
port: config.port,
|
|
1553
|
-
});
|
|
1554
|
-
this.autoMarketData = (config.marketDataType ?? "auto") === "auto";
|
|
1555
|
-
this.activeMarketDataType = marketDataTypeFromConfig(config);
|
|
1556
|
-
this.api.setMarketDataType(this.activeMarketDataType);
|
|
1557
|
-
this.configKey = configKey;
|
|
1558
|
-
await Promise.allSettled([
|
|
1559
|
-
this.loadAccountsAndUpdateSnapshot(),
|
|
1560
|
-
this.loadOpenOrders(),
|
|
1561
|
-
this.loadExecutions(),
|
|
1562
|
-
]);
|
|
1563
|
-
this.updateSnapshot({
|
|
1564
|
-
...this.snapshot,
|
|
1565
|
-
status: {
|
|
1566
|
-
state: "connected",
|
|
1567
|
-
updatedAt: Date.now(),
|
|
1568
|
-
mode: "gateway",
|
|
1569
|
-
message: this.connectionNote,
|
|
1570
|
-
},
|
|
1571
|
-
});
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
private bindConnectionEvents(api: IBApiNext): void {
|
|
1575
|
-
api.connectionState.subscribe((state) => {
|
|
1576
|
-
if (state === ConnectionState.Connected) {
|
|
1577
|
-
this.updateSnapshot({
|
|
1578
|
-
...this.snapshot,
|
|
1579
|
-
status: {
|
|
1580
|
-
state: "connected",
|
|
1581
|
-
updatedAt: Date.now(),
|
|
1582
|
-
mode: "gateway",
|
|
1583
|
-
message: this.connectionNote,
|
|
1584
|
-
},
|
|
1585
|
-
});
|
|
1586
|
-
} else if (state === ConnectionState.Connecting) {
|
|
1587
|
-
this.updateSnapshot({
|
|
1588
|
-
...this.snapshot,
|
|
1589
|
-
status: {
|
|
1590
|
-
state: "connecting",
|
|
1591
|
-
updatedAt: Date.now(),
|
|
1592
|
-
mode: "gateway",
|
|
1593
|
-
message: this.connectionNote,
|
|
1594
|
-
},
|
|
1595
|
-
});
|
|
1596
|
-
} else {
|
|
1597
|
-
this.updateSnapshot({
|
|
1598
|
-
...this.snapshot,
|
|
1599
|
-
status: { state: "disconnected", updatedAt: Date.now(), mode: "gateway" },
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
});
|
|
1603
|
-
|
|
1604
|
-
// Capture account IDs as they arrive from the managedAccounts event on connection.
|
|
1605
|
-
const rawApi = this.getRawApi();
|
|
1606
|
-
rawApi.on(EventName.managedAccounts, (accountsList: string) => {
|
|
1607
|
-
this.cachedAccountIds = accountsList.split(",").map((s: string) => s.trim()).filter(Boolean);
|
|
1608
|
-
});
|
|
1609
|
-
|
|
1610
|
-
api.error.subscribe((err) => {
|
|
1611
|
-
const code = getIbErrorCode(err);
|
|
1612
|
-
const message = getIbErrorMessage(err);
|
|
1613
|
-
if (isMarketDataPermissionError(code, message)) {
|
|
1614
|
-
this.updateSnapshot({
|
|
1615
|
-
...this.snapshot,
|
|
1616
|
-
status: {
|
|
1617
|
-
state: "connected",
|
|
1618
|
-
updatedAt: Date.now(),
|
|
1619
|
-
mode: "gateway",
|
|
1620
|
-
message: this.autoMarketData
|
|
1621
|
-
? "Live API market data unavailable; delayed quotes will be used when IBKR allows them."
|
|
1622
|
-
: message,
|
|
1623
|
-
},
|
|
1624
|
-
lastError: message,
|
|
1625
|
-
});
|
|
1626
|
-
return;
|
|
1627
|
-
}
|
|
1628
|
-
if (isIbInformationalWarning(code, message)) {
|
|
1629
|
-
this.updateSnapshot({
|
|
1630
|
-
...this.snapshot,
|
|
1631
|
-
status: {
|
|
1632
|
-
...this.snapshot.status,
|
|
1633
|
-
updatedAt: Date.now(),
|
|
1634
|
-
mode: "gateway",
|
|
1635
|
-
message,
|
|
1636
|
-
},
|
|
1637
|
-
});
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
const keepConnectionState = this.snapshot.status.state === "connected" || this.snapshot.status.state === "connecting";
|
|
1642
|
-
this.updateSnapshot({
|
|
1643
|
-
...this.snapshot,
|
|
1644
|
-
status: keepConnectionState
|
|
1645
|
-
? {
|
|
1646
|
-
...this.snapshot.status,
|
|
1647
|
-
updatedAt: Date.now(),
|
|
1648
|
-
mode: "gateway",
|
|
1649
|
-
message,
|
|
1650
|
-
}
|
|
1651
|
-
: { state: "error", updatedAt: Date.now(), mode: "gateway", message },
|
|
1652
|
-
lastError: message,
|
|
1653
|
-
});
|
|
1654
|
-
});
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
private getClientIdCandidates(requestedClientId: number): number[] {
|
|
1658
|
-
const preferred = this.claimedClientLock?.clientId;
|
|
1659
|
-
const candidates = new Set<number>();
|
|
1660
|
-
if (preferred && Number.isFinite(preferred) && preferred > 0) {
|
|
1661
|
-
candidates.add(preferred);
|
|
1662
|
-
}
|
|
1663
|
-
for (let offset = 0; offset < IBKR_CLIENT_ID_SEARCH_SPAN; offset += 1) {
|
|
1664
|
-
candidates.add(requestedClientId + offset);
|
|
1665
|
-
}
|
|
1666
|
-
return [...candidates];
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
private async tryClaimClientLock(
|
|
1670
|
-
config: IbkrGatewayConfig,
|
|
1671
|
-
clientId: number,
|
|
1672
|
-
requestedClientId: number,
|
|
1673
|
-
): Promise<boolean> {
|
|
1674
|
-
const path = buildClientLockPath(config, clientId);
|
|
1675
|
-
const existing = this.claimedClientLock;
|
|
1676
|
-
if (
|
|
1677
|
-
existing
|
|
1678
|
-
&& existing.clientId === clientId
|
|
1679
|
-
&& existing.requestedClientId === requestedClientId
|
|
1680
|
-
) {
|
|
1681
|
-
return true;
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
await mkdir(IBKR_CLIENT_LOCK_DIR, { recursive: true });
|
|
1685
|
-
const metadata: ClientLockMetadata = {
|
|
1686
|
-
pid: process.pid,
|
|
1687
|
-
ownerToken: this.clientLockOwner,
|
|
1688
|
-
requestedClientId,
|
|
1689
|
-
clientId,
|
|
1690
|
-
host: config.host,
|
|
1691
|
-
port: config.port,
|
|
1692
|
-
instanceId: this.instanceId,
|
|
1693
|
-
cwd: process.cwd(),
|
|
1694
|
-
timestamp: Date.now(),
|
|
1695
|
-
};
|
|
1696
|
-
|
|
1697
|
-
try {
|
|
1698
|
-
const handle = await open(path, "wx");
|
|
1699
|
-
await handle.writeFile(JSON.stringify(metadata), "utf-8");
|
|
1700
|
-
await handle.close();
|
|
1701
|
-
await this.releaseClientLock();
|
|
1702
|
-
this.claimedClientLock = { clientId, requestedClientId, path };
|
|
1703
|
-
return true;
|
|
1704
|
-
} catch (error: any) {
|
|
1705
|
-
if (error?.code !== "EEXIST") throw error;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
const lock = await readClientLock(path);
|
|
1709
|
-
if (!lock || !isProcessAlive(lock.pid)) {
|
|
1710
|
-
await rm(path, { force: true }).catch(() => {});
|
|
1711
|
-
return this.tryClaimClientLock(config, clientId, requestedClientId);
|
|
1712
|
-
}
|
|
1713
|
-
if (lock.ownerToken === this.clientLockOwner) {
|
|
1714
|
-
await this.releaseClientLock();
|
|
1715
|
-
this.claimedClientLock = { clientId, requestedClientId, path };
|
|
1716
|
-
return true;
|
|
1717
|
-
}
|
|
1718
|
-
return false;
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
private async releaseClientLock(): Promise<void> {
|
|
1722
|
-
const lock = this.claimedClientLock;
|
|
1723
|
-
this.claimedClientLock = null;
|
|
1724
|
-
if (!lock) return;
|
|
1725
|
-
await rm(lock.path, { force: true }).catch(() => {});
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
private updateSnapshot(snapshot: IbkrSnapshot): void {
|
|
1729
|
-
this.snapshot = snapshot;
|
|
1730
|
-
for (const listener of this.listeners) listener();
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
private teardownQuoteStreams(): void {
|
|
1734
|
-
for (const stream of this.quoteStreams.values()) {
|
|
1735
|
-
stream.stop();
|
|
1736
|
-
}
|
|
1737
|
-
this.quoteStreams.clear();
|
|
1738
|
-
this.quoteStreamStarts.clear();
|
|
332
|
+
const remote = getBrokerRemoteClient();
|
|
333
|
+
return remote
|
|
334
|
+
? remote.invoke(this.instanceId, "getDetailedPriceHistory", [ticker, exchange, startDate, endDate, barSize, instrument])
|
|
335
|
+
: (await getNativeService(this.instanceId)).getDetailedPriceHistory(ticker, config, exchange, startDate, endDate, barSize, instrument);
|
|
1739
336
|
}
|
|
1740
337
|
|
|
1741
|
-
|
|
1742
|
-
key: string,
|
|
1743
|
-
target: QuoteSubscriptionTarget,
|
|
1744
|
-
config: IbkrGatewayConfig,
|
|
1745
|
-
): Promise<void> {
|
|
1746
|
-
await this.connect(config);
|
|
1747
|
-
const contract = await withTimeout(
|
|
1748
|
-
this.resolveContract(target.symbol, target.exchange ?? "", target.context?.instrument ?? null),
|
|
1749
|
-
IBKR_DATA_TIMEOUT,
|
|
1750
|
-
"resolveContract",
|
|
1751
|
-
);
|
|
1752
|
-
const details = await withTimeout(this.getPrimaryContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails");
|
|
1753
|
-
const priceDivisor = getIbkrPriceDivisor(contract, details);
|
|
1754
|
-
const stream = this.quoteStreams.get(key);
|
|
1755
|
-
if (!stream || stream.listeners.size === 0) {
|
|
1756
|
-
this.quoteStreams.delete(key);
|
|
1757
|
-
return;
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
const seededQuote = await this.loadSeedQuote(contract, details, config).catch(() => null);
|
|
1761
|
-
if (seededQuote) {
|
|
1762
|
-
stream.lastQuote = seededQuote;
|
|
1763
|
-
for (const [listener, listenerTarget] of stream.listeners.entries()) {
|
|
1764
|
-
listener(listenerTarget, seededQuote);
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
const subscription = this.api!.getMarketData(contract, IBKR_QUOTE_STREAM_TICKS, false, false).subscribe({
|
|
1769
|
-
next: (update) => this.emitQuoteStreamUpdate(key, contract, details, update),
|
|
1770
|
-
error: (error) => {
|
|
1771
|
-
gatewayLog.warn("Quote stream error", {
|
|
1772
|
-
instanceId: this.instanceId,
|
|
1773
|
-
symbol: target.symbol,
|
|
1774
|
-
exchange: target.exchange ?? "",
|
|
1775
|
-
error: error instanceof Error ? error.message : String(error ?? ""),
|
|
1776
|
-
});
|
|
1777
|
-
},
|
|
1778
|
-
});
|
|
1779
|
-
const tradeSubscription = this.api!.getTickByTickAllLastDataUpdates(contract, 0, false).subscribe({
|
|
1780
|
-
next: (tick) => this.emitTickByTickAllLastUpdate(key, contract, details, tick, priceDivisor),
|
|
1781
|
-
error: (error) => {
|
|
1782
|
-
gatewayLog.warn("Tick-by-tick trade stream error", {
|
|
1783
|
-
instanceId: this.instanceId,
|
|
1784
|
-
symbol: target.symbol,
|
|
1785
|
-
exchange: target.exchange ?? "",
|
|
1786
|
-
error: error instanceof Error ? error.message : String(error ?? ""),
|
|
1787
|
-
});
|
|
1788
|
-
},
|
|
1789
|
-
});
|
|
1790
|
-
const bidAskStop = this.startTickByTickBidAskStream(
|
|
1791
|
-
contract,
|
|
1792
|
-
(update) => this.emitTickByTickBidAskUpdate(key, priceDivisor, update),
|
|
1793
|
-
(error) => {
|
|
1794
|
-
gatewayLog.warn("Tick-by-tick bid/ask stream error", {
|
|
1795
|
-
instanceId: this.instanceId,
|
|
1796
|
-
symbol: target.symbol,
|
|
1797
|
-
exchange: target.exchange ?? "",
|
|
1798
|
-
error,
|
|
1799
|
-
});
|
|
1800
|
-
},
|
|
1801
|
-
);
|
|
1802
|
-
|
|
1803
|
-
const liveStream = this.quoteStreams.get(key);
|
|
1804
|
-
if (!liveStream || liveStream.listeners.size === 0) {
|
|
1805
|
-
subscription.unsubscribe();
|
|
1806
|
-
tradeSubscription.unsubscribe();
|
|
1807
|
-
bidAskStop();
|
|
1808
|
-
this.quoteStreams.delete(key);
|
|
1809
|
-
return;
|
|
1810
|
-
}
|
|
1811
|
-
liveStream.stop = () => {
|
|
1812
|
-
subscription.unsubscribe();
|
|
1813
|
-
tradeSubscription.unsubscribe();
|
|
1814
|
-
bidAskStop();
|
|
1815
|
-
};
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
private emitQuoteStreamUpdate(
|
|
1819
|
-
key: string,
|
|
1820
|
-
contract: Contract,
|
|
1821
|
-
details: ContractDetails,
|
|
1822
|
-
update: MarketDataUpdate,
|
|
1823
|
-
): void {
|
|
1824
|
-
const stream = this.quoteStreams.get(key);
|
|
1825
|
-
if (!stream || stream.listeners.size === 0) return;
|
|
1826
|
-
|
|
1827
|
-
try {
|
|
1828
|
-
const quote = this.marketDataToQuote(contract, details, update.all);
|
|
1829
|
-
quote.providerId = "ibkr";
|
|
1830
|
-
quote.dataSource = hasDelayedMarketData(update.all) || this.activeMarketDataType !== MarketDataType.REALTIME
|
|
1831
|
-
? "delayed"
|
|
1832
|
-
: "live";
|
|
1833
|
-
stream.lastQuote = quote;
|
|
1834
|
-
for (const [listener, listenerTarget] of stream.listeners.entries()) {
|
|
1835
|
-
listener(listenerTarget, quote);
|
|
1836
|
-
}
|
|
1837
|
-
} catch {
|
|
1838
|
-
// Ignore partial tick snapshots until IBKR has sent a usable quote.
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
private emitTickByTickAllLastUpdate(
|
|
1843
|
-
key: string,
|
|
1844
|
-
contract: Contract,
|
|
1845
|
-
details: ContractDetails,
|
|
1846
|
-
tick: TickByTickAllLast,
|
|
1847
|
-
priceDivisor: number,
|
|
1848
|
-
): void {
|
|
1849
|
-
const stream = this.quoteStreams.get(key);
|
|
1850
|
-
if (!stream || stream.listeners.size === 0) return;
|
|
1851
|
-
|
|
1852
|
-
const nextQuote = applyTickByTickAllLastToQuote(
|
|
1853
|
-
stream.lastQuote,
|
|
1854
|
-
contract,
|
|
1855
|
-
details,
|
|
1856
|
-
tick,
|
|
1857
|
-
priceDivisor,
|
|
1858
|
-
stream.lastQuote?.dataSource ?? (this.activeMarketDataType === MarketDataType.REALTIME ? "live" : "delayed"),
|
|
1859
|
-
);
|
|
1860
|
-
if (!nextQuote) return;
|
|
1861
|
-
stream.lastQuote = nextQuote;
|
|
1862
|
-
for (const [listener, listenerTarget] of stream.listeners.entries()) {
|
|
1863
|
-
listener(listenerTarget, nextQuote);
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
private emitTickByTickBidAskUpdate(
|
|
1868
|
-
key: string,
|
|
1869
|
-
priceDivisor: number,
|
|
1870
|
-
update: TickByTickBidAskUpdate,
|
|
1871
|
-
): void {
|
|
1872
|
-
const stream = this.quoteStreams.get(key);
|
|
1873
|
-
if (!stream || stream.listeners.size === 0) return;
|
|
1874
|
-
const nextQuote = applyTickByTickBidAskToQuote(stream.lastQuote, update, priceDivisor);
|
|
1875
|
-
if (!nextQuote) return;
|
|
1876
|
-
stream.lastQuote = nextQuote;
|
|
1877
|
-
for (const [listener, listenerTarget] of stream.listeners.entries()) {
|
|
1878
|
-
listener(listenerTarget, nextQuote);
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
private async loadSeedQuote(
|
|
1883
|
-
contract: Contract,
|
|
1884
|
-
details: ContractDetails,
|
|
338
|
+
subscribeQuotes(
|
|
1885
339
|
config: IbkrGatewayConfig,
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
const marketData = await this.withMarketDataFallback(
|
|
1889
|
-
config,
|
|
1890
|
-
() => withTimeout(this.api!.getMarketDataSnapshot(contract, "", false), IBKR_DATA_TIMEOUT, "getMarketDataSnapshot"),
|
|
1891
|
-
);
|
|
1892
|
-
const quote = this.marketDataToQuote(contract, details, marketData);
|
|
1893
|
-
quote.providerId = "ibkr";
|
|
1894
|
-
quote.dataSource = hasDelayedMarketData(marketData) || this.activeMarketDataType !== MarketDataType.REALTIME
|
|
1895
|
-
? "delayed"
|
|
1896
|
-
: "live";
|
|
1897
|
-
return quote;
|
|
1898
|
-
} catch {
|
|
1899
|
-
return null;
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
private startTickByTickBidAskStream(
|
|
1904
|
-
contract: Contract,
|
|
1905
|
-
onUpdate: (update: TickByTickBidAskUpdate) => void,
|
|
1906
|
-
onError: (message: string) => void,
|
|
340
|
+
targets: QuoteSubscriptionTarget[],
|
|
341
|
+
onQuote: (target: QuoteSubscriptionTarget, quote: Quote) => void,
|
|
1907
342
|
): () => void {
|
|
1908
|
-
const
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
time: number,
|
|
1918
|
-
bidPrice: number,
|
|
1919
|
-
askPrice: number,
|
|
1920
|
-
bidSize: number,
|
|
1921
|
-
askSize: number,
|
|
1922
|
-
) => {
|
|
1923
|
-
if (incomingReqId !== reqId) return;
|
|
1924
|
-
onUpdate({ time, bidPrice, askPrice, bidSize, askSize });
|
|
1925
|
-
};
|
|
1926
|
-
const handleError = (error: Error, code: number, incomingReqId: number) => {
|
|
1927
|
-
if (incomingReqId !== reqId) return;
|
|
1928
|
-
onError(error?.message || `IBKR error ${code}`);
|
|
1929
|
-
};
|
|
1930
|
-
|
|
1931
|
-
rawApi.on(EventName.tickByTickBidAsk, handleBidAsk);
|
|
1932
|
-
rawApi.on(EventName.error, handleError);
|
|
1933
|
-
rawApi.reqTickByTickData(reqId, contract, "BidAsk", 0, false);
|
|
1934
|
-
|
|
343
|
+
const remote = getBrokerRemoteClient();
|
|
344
|
+
if (remote) return remote.subscribeQuotes(this.instanceId, targets, onQuote);
|
|
345
|
+
|
|
346
|
+
let nativeDispose: (() => void) | null = null;
|
|
347
|
+
let disposed = false;
|
|
348
|
+
void getNativeService(this.instanceId).then((service) => {
|
|
349
|
+
if (disposed) return;
|
|
350
|
+
nativeDispose = service.subscribeQuotes(config, targets, onQuote);
|
|
351
|
+
}).catch(() => {});
|
|
1935
352
|
return () => {
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
try {
|
|
1939
|
-
rawApi.cancelTickByTickData(reqId);
|
|
1940
|
-
} catch {
|
|
1941
|
-
// ignore shutdown races
|
|
1942
|
-
}
|
|
353
|
+
disposed = true;
|
|
354
|
+
nativeDispose?.();
|
|
1943
355
|
};
|
|
1944
356
|
}
|
|
1945
357
|
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
return await operation();
|
|
1952
|
-
} catch (error: any) {
|
|
1953
|
-
const message = error?.message || String(error || "");
|
|
1954
|
-
const isPermission = isMarketDataPermissionError(undefined, message);
|
|
1955
|
-
const isTimeout = message.includes("timed out");
|
|
1956
|
-
const isNoData = message.includes("No valid market data");
|
|
1957
|
-
if (
|
|
1958
|
-
!this.autoMarketData
|
|
1959
|
-
|| this.activeMarketDataType === MarketDataType.DELAYED
|
|
1960
|
-
|| !(isPermission || isTimeout || isNoData)
|
|
1961
|
-
) {
|
|
1962
|
-
throw error;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
this.api?.setMarketDataType(MarketDataType.DELAYED);
|
|
1966
|
-
this.activeMarketDataType = MarketDataType.DELAYED;
|
|
1967
|
-
this.updateSnapshot({
|
|
1968
|
-
...this.snapshot,
|
|
1969
|
-
status: {
|
|
1970
|
-
state: "connected",
|
|
1971
|
-
updatedAt: Date.now(),
|
|
1972
|
-
mode: "gateway",
|
|
1973
|
-
message: "Using delayed IBKR market data because live API market data is not enabled for this account.",
|
|
1974
|
-
},
|
|
1975
|
-
lastError: message,
|
|
1976
|
-
});
|
|
1977
|
-
return operation();
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
private async loadAccounts(): Promise<BrokerAccount[]> {
|
|
1982
|
-
let managedAccounts: string[];
|
|
1983
|
-
try {
|
|
1984
|
-
managedAccounts = await withTimeout(this.api!.getManagedAccounts(), IBKR_DATA_TIMEOUT, "getManagedAccounts");
|
|
1985
|
-
} catch {
|
|
1986
|
-
// getManagedAccounts() can hang after reconnects or when another client with the same
|
|
1987
|
-
// clientId is connected. Fall back to requesting via the raw event API, or use cached IDs.
|
|
1988
|
-
if (this.cachedAccountIds.length > 0) {
|
|
1989
|
-
managedAccounts = this.cachedAccountIds;
|
|
1990
|
-
} else {
|
|
1991
|
-
managedAccounts = await withTimeout(
|
|
1992
|
-
new Promise<string[]>((resolve) => {
|
|
1993
|
-
const rawApi = this.getRawApi();
|
|
1994
|
-
const handler = (accountsList: string) => {
|
|
1995
|
-
rawApi.off(EventName.managedAccounts, handler);
|
|
1996
|
-
resolve(accountsList.split(",").map((s: string) => s.trim()).filter(Boolean));
|
|
1997
|
-
};
|
|
1998
|
-
rawApi.on(EventName.managedAccounts, handler);
|
|
1999
|
-
rawApi.reqManagedAccts();
|
|
2000
|
-
}),
|
|
2001
|
-
IBKR_DATA_TIMEOUT,
|
|
2002
|
-
"getManagedAccounts-fallback",
|
|
2003
|
-
);
|
|
2004
|
-
this.cachedAccountIds = managedAccounts;
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
if (!managedAccounts.length) return [];
|
|
2008
|
-
let summary: ReadonlyMap<string, ReadonlyMap<string, ReadonlyMap<string, { value: string }>>> | undefined;
|
|
2009
|
-
try {
|
|
2010
|
-
const result = await firstValueFrom(
|
|
2011
|
-
this.api!.getAccountSummary(
|
|
2012
|
-
"All",
|
|
2013
|
-
"NetLiquidation,TotalCashValue,SettledCash,AvailableFunds,BuyingPower,ExcessLiquidity,InitMarginReq,MaintMarginReq,$LEDGER:ALL",
|
|
2014
|
-
)
|
|
2015
|
-
.pipe(take(1), timeout(10_000)),
|
|
2016
|
-
);
|
|
2017
|
-
summary = result.all;
|
|
2018
|
-
} catch {
|
|
2019
|
-
// Account summary may fail; return accounts with basic info only.
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
const updatedAt = Date.now();
|
|
2023
|
-
const aggregateTags = summary?.get("All");
|
|
2024
|
-
const allowAggregateCashBalances = managedAccounts.length === 1;
|
|
2025
|
-
return managedAccounts.map((accountId) => summarizeBrokerAccount(
|
|
2026
|
-
accountId,
|
|
2027
|
-
summary?.get(accountId),
|
|
2028
|
-
updatedAt,
|
|
2029
|
-
aggregateTags,
|
|
2030
|
-
allowAggregateCashBalances,
|
|
2031
|
-
));
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
private async loadAccountsAndUpdateSnapshot(): Promise<BrokerAccount[]> {
|
|
2035
|
-
const accounts = await this.loadAccounts();
|
|
2036
|
-
this.updateSnapshot({ ...this.snapshot, accounts });
|
|
2037
|
-
return accounts;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
private async loadPortfolioSnapshotsForAccounts(
|
|
2041
|
-
accountIds: string[],
|
|
2042
|
-
): Promise<Map<string, AccountPortfolioSnapshotPosition>> {
|
|
2043
|
-
const snapshots = new Map<string, AccountPortfolioSnapshotPosition>();
|
|
2044
|
-
for (const accountId of accountIds) {
|
|
2045
|
-
const positions = await this.loadPortfolioSnapshotForAccount(accountId);
|
|
2046
|
-
for (const position of positions) {
|
|
2047
|
-
snapshots.set(buildPortfolioPositionKey(accountId, position.contract), position);
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
return snapshots;
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
private async loadPortfolioSnapshotForAccount(accountId: string): Promise<AccountPortfolioSnapshotPosition[]> {
|
|
2054
|
-
if (!this.api || !accountId) return [];
|
|
2055
|
-
|
|
2056
|
-
try {
|
|
2057
|
-
const update = await firstValueFrom(
|
|
2058
|
-
this.api.getAccountUpdates(accountId).pipe(
|
|
2059
|
-
filter((value) => value.changed == null && value.added == null && value.removed == null),
|
|
2060
|
-
take(1),
|
|
2061
|
-
timeout(IBKR_DATA_TIMEOUT),
|
|
2062
|
-
),
|
|
2063
|
-
);
|
|
2064
|
-
return (update.all.portfolio?.get(accountId) ?? []) as AccountPortfolioSnapshotPosition[];
|
|
2065
|
-
} catch (error: any) {
|
|
2066
|
-
gatewayLog.warn("Failed to load IBKR portfolio snapshot", {
|
|
2067
|
-
instanceId: this.instanceId,
|
|
2068
|
-
accountId,
|
|
2069
|
-
error: error?.message || String(error || ""),
|
|
2070
|
-
});
|
|
2071
|
-
return [];
|
|
2072
|
-
}
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
private async loadOpenOrders(): Promise<BrokerOrder[]> {
|
|
2076
|
-
const orders = await withTimeout(this.api!.getAllOpenOrders(), IBKR_DATA_TIMEOUT, "getAllOpenOrders");
|
|
2077
|
-
const mapped = orders.map((order) => this.openOrderToBrokerOrder(order));
|
|
2078
|
-
this.updateSnapshot({ ...this.snapshot, openOrders: mapped });
|
|
2079
|
-
return mapped;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
private async loadExecutions(): Promise<BrokerExecution[]> {
|
|
2083
|
-
const executions = await withTimeout(this.api!.getExecutionDetails({}), IBKR_DATA_TIMEOUT, "getExecutionDetails");
|
|
2084
|
-
const mapped = executions.map((detail) => ({
|
|
2085
|
-
execId: detail.execution.execId || `${detail.execution.orderId ?? "exec"}-${detail.execution.time ?? Date.now()}`,
|
|
2086
|
-
brokerInstanceId: this.instanceId,
|
|
2087
|
-
orderId: detail.execution.orderId,
|
|
2088
|
-
accountId: detail.execution.acctNumber,
|
|
2089
|
-
side: detail.execution.side || "",
|
|
2090
|
-
shares: detail.execution.shares ?? 0,
|
|
2091
|
-
price: detail.execution.price ?? 0,
|
|
2092
|
-
time: detail.execution.time ? Date.parse(detail.execution.time) : Date.now(),
|
|
2093
|
-
exchange: detail.execution.exchange,
|
|
2094
|
-
contract: this.contractToRef(detail.contract),
|
|
2095
|
-
}));
|
|
2096
|
-
this.updateSnapshot({ ...this.snapshot, executions: mapped });
|
|
2097
|
-
return mapped;
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
private contractDescriptionToSearchResult(description: ContractDescription): InstrumentSearchResult | null {
|
|
2101
|
-
const contract = description.contract;
|
|
2102
|
-
if (!contract?.symbol) return null;
|
|
2103
|
-
return {
|
|
2104
|
-
providerId: "ibkr",
|
|
2105
|
-
brokerInstanceId: this.instanceId,
|
|
2106
|
-
symbol: contract.localSymbol || contract.symbol,
|
|
2107
|
-
name: contract.description || contract.symbol,
|
|
2108
|
-
exchange: contract.primaryExch || contract.exchange || "",
|
|
2109
|
-
type: contract.secType || "",
|
|
2110
|
-
currency: contract.currency,
|
|
2111
|
-
primaryExchange: contract.primaryExch,
|
|
2112
|
-
brokerContract: this.contractToRef(contract),
|
|
2113
|
-
};
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
private contractToRef(contract: Contract): BrokerContractRef {
|
|
2117
|
-
return {
|
|
2118
|
-
brokerId: "ibkr",
|
|
2119
|
-
brokerInstanceId: this.instanceId,
|
|
2120
|
-
conId: contract.conId,
|
|
2121
|
-
symbol: contract.symbol || "",
|
|
2122
|
-
localSymbol: contract.localSymbol,
|
|
2123
|
-
secType: contract.secType,
|
|
2124
|
-
exchange: contract.exchange,
|
|
2125
|
-
primaryExchange: contract.primaryExch,
|
|
2126
|
-
currency: contract.currency,
|
|
2127
|
-
lastTradeDateOrContractMonth: contract.lastTradeDateOrContractMonth,
|
|
2128
|
-
right: contract.right === OptionType.Call ? "C" : contract.right === OptionType.Put ? "P" : undefined,
|
|
2129
|
-
strike: contract.strike,
|
|
2130
|
-
multiplier: contract.multiplier != null ? String(contract.multiplier) : undefined,
|
|
2131
|
-
tradingClass: contract.tradingClass,
|
|
2132
|
-
};
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
private refToContract(ref: BrokerContractRef): Contract {
|
|
2136
|
-
return {
|
|
2137
|
-
conId: ref.conId,
|
|
2138
|
-
symbol: ref.symbol,
|
|
2139
|
-
localSymbol: ref.localSymbol,
|
|
2140
|
-
secType: (ref.secType as SecType | undefined) ?? SecType.STK,
|
|
2141
|
-
exchange: "SMART",
|
|
2142
|
-
primaryExch: ref.primaryExchange || ref.exchange || undefined,
|
|
2143
|
-
currency: ref.currency || "USD",
|
|
2144
|
-
lastTradeDateOrContractMonth: ref.lastTradeDateOrContractMonth,
|
|
2145
|
-
right: ref.right === "C" ? OptionType.Call : ref.right === "P" ? OptionType.Put : undefined,
|
|
2146
|
-
strike: ref.strike,
|
|
2147
|
-
multiplier: ref.multiplier ? parseFloat(ref.multiplier) : undefined,
|
|
2148
|
-
tradingClass: ref.tradingClass,
|
|
2149
|
-
};
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
private async resolveContract(ticker: string, exchange: string, instrument: BrokerContractRef | null): Promise<Contract> {
|
|
2153
|
-
if (instrument) {
|
|
2154
|
-
if (instrument.conId) {
|
|
2155
|
-
return this.refToContract(instrument);
|
|
2156
|
-
}
|
|
2157
|
-
return this.getPrimaryContractDetails(this.refToContract(instrument)).then((detail) => detail.contract);
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
const directMatches = await withTimeout(this.getDirectContractMatches(ticker), IBKR_DATA_TIMEOUT, "getDirectContractMatches");
|
|
2161
|
-
const direct = directMatches.find((result) => {
|
|
2162
|
-
const symbol = result.symbol.toUpperCase();
|
|
2163
|
-
const upperTicker = ticker.toUpperCase();
|
|
2164
|
-
return symbol === upperTicker || symbol === ticker;
|
|
2165
|
-
});
|
|
2166
|
-
if (direct?.brokerContract) {
|
|
2167
|
-
return this.refToContract(direct.brokerContract);
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
const symbolResults = (await withTimeout(this.api!.getMatchingSymbols(ticker), IBKR_DATA_TIMEOUT, "getMatchingSymbols"))
|
|
2171
|
-
.map((description) => this.contractDescriptionToSearchResult(description))
|
|
2172
|
-
.filter((value): value is InstrumentSearchResult => value != null);
|
|
2173
|
-
const matched = symbolResults.find((result) => {
|
|
2174
|
-
const symbol = result.symbol.toUpperCase();
|
|
2175
|
-
const upperTicker = ticker.toUpperCase();
|
|
2176
|
-
return symbol === upperTicker || symbol === ticker;
|
|
2177
|
-
});
|
|
2178
|
-
if (matched?.brokerContract) {
|
|
2179
|
-
return this.refToContract(matched.brokerContract);
|
|
2180
|
-
}
|
|
2181
|
-
|
|
2182
|
-
const fallbackContract: Contract = {
|
|
2183
|
-
symbol: ticker,
|
|
2184
|
-
exchange: exchange || "SMART",
|
|
2185
|
-
currency: "USD",
|
|
2186
|
-
secType: SecType.STK,
|
|
2187
|
-
};
|
|
2188
|
-
const details = await this.getPrimaryContractDetails(fallbackContract);
|
|
2189
|
-
return details.contract;
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
private async getPrimaryContractDetails(contract: Contract): Promise<ContractDetails> {
|
|
2193
|
-
const details = await withTimeout(this.api!.getContractDetails(contract), IBKR_DATA_TIMEOUT, "getContractDetails");
|
|
2194
|
-
if (!details.length) {
|
|
2195
|
-
throw new Error(`Unable to resolve contract ${contract.symbol || contract.localSymbol || contract.conId}`);
|
|
2196
|
-
}
|
|
2197
|
-
return details[0]!;
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
private async getDirectContractMatches(query: string): Promise<InstrumentSearchResult[]> {
|
|
2201
|
-
const trimmed = query.trim();
|
|
2202
|
-
if (!trimmed) return [];
|
|
2203
|
-
|
|
2204
|
-
const candidates: Contract[] = [];
|
|
2205
|
-
const optionLike = /^\S+\s+\d{6}[CP]\d{8}$/i.test(trimmed);
|
|
2206
|
-
|
|
2207
|
-
if (optionLike) {
|
|
2208
|
-
candidates.push({
|
|
2209
|
-
localSymbol: trimmed,
|
|
2210
|
-
secType: SecType.OPT,
|
|
2211
|
-
exchange: "SMART",
|
|
2212
|
-
});
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
candidates.push({
|
|
2216
|
-
localSymbol: trimmed,
|
|
2217
|
-
exchange: "SMART",
|
|
2218
|
-
});
|
|
2219
|
-
|
|
2220
|
-
const seen = new Set<string>();
|
|
2221
|
-
const results: InstrumentSearchResult[] = [];
|
|
2222
|
-
|
|
2223
|
-
for (const candidate of candidates) {
|
|
2224
|
-
try {
|
|
2225
|
-
const details = await withTimeout(this.api!.getContractDetails(candidate), IBKR_DATA_TIMEOUT, "getDirectContractDetails");
|
|
2226
|
-
for (const detail of details) {
|
|
2227
|
-
const contract = detail.contract;
|
|
2228
|
-
const result: InstrumentSearchResult = {
|
|
2229
|
-
providerId: "ibkr",
|
|
2230
|
-
brokerInstanceId: this.instanceId,
|
|
2231
|
-
symbol: contract.localSymbol || contract.symbol || trimmed,
|
|
2232
|
-
name: detail.longName || detail.marketName || contract.description || contract.symbol || trimmed,
|
|
2233
|
-
exchange: contract.primaryExch || contract.exchange || "",
|
|
2234
|
-
type: contract.secType || "",
|
|
2235
|
-
currency: contract.currency,
|
|
2236
|
-
primaryExchange: contract.primaryExch,
|
|
2237
|
-
brokerContract: this.contractToRef(contract),
|
|
2238
|
-
};
|
|
2239
|
-
const key = `${result.symbol}|${result.exchange}|${result.type}`;
|
|
2240
|
-
if (seen.has(key)) continue;
|
|
2241
|
-
seen.add(key);
|
|
2242
|
-
results.push(result);
|
|
2243
|
-
}
|
|
2244
|
-
} catch {
|
|
2245
|
-
// Ignore exact-match lookup failures and fall back to symbol search.
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
return results;
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
private marketDataToQuote(contract: Contract, details: ContractDetails, marketData: ReadonlyMap<number, { value?: number; ingressTm: number }>): Quote {
|
|
2253
|
-
const priceDivisor = getIbkrPriceDivisor(contract, details);
|
|
2254
|
-
const mark = normalizeIbkrPriceValue(marketData.get(IBApiTickType.MARK_PRICE)?.value, priceDivisor);
|
|
2255
|
-
const last = normalizeIbkrPriceValue(
|
|
2256
|
-
marketData.get(IBApiTickType.LAST)?.value
|
|
2257
|
-
?? marketData.get(IBApiTickType.DELAYED_LAST)?.value
|
|
2258
|
-
?? marketData.get(IBApiTickType.MARK_PRICE)?.value
|
|
2259
|
-
?? marketData.get(IBApiTickType.CLOSE)?.value,
|
|
2260
|
-
priceDivisor,
|
|
2261
|
-
);
|
|
2262
|
-
if (last == null || last <= 0) {
|
|
2263
|
-
throw new Error(`No valid market data for ${contract.symbol || contract.localSymbol || contract.conId}`);
|
|
2264
|
-
}
|
|
2265
|
-
const close = normalizeIbkrPriceValue(
|
|
2266
|
-
marketData.get(IBApiTickType.CLOSE)?.value
|
|
2267
|
-
?? marketData.get(IBApiTickType.DELAYED_CLOSE)?.value
|
|
2268
|
-
?? last,
|
|
2269
|
-
priceDivisor,
|
|
2270
|
-
) ?? last;
|
|
2271
|
-
const change = last - close;
|
|
2272
|
-
const ingressTm = marketData.get(IBApiTickType.LAST)?.ingressTm
|
|
2273
|
-
?? marketData.get(IBApiTickType.DELAYED_LAST)?.ingressTm
|
|
2274
|
-
?? marketData.get(IBApiTickType.MARK_PRICE)?.ingressTm
|
|
2275
|
-
?? Date.now();
|
|
2276
|
-
|
|
2277
|
-
return {
|
|
2278
|
-
symbol: contract.localSymbol || contract.symbol || "",
|
|
2279
|
-
price: last,
|
|
2280
|
-
currency: contract.currency || "USD",
|
|
2281
|
-
change,
|
|
2282
|
-
changePercent: close ? (change / close) * 100 : 0,
|
|
2283
|
-
previousClose: close,
|
|
2284
|
-
high52w: normalizeIbkrPriceValue(marketData.get(IBApiTickType.HIGH_52_WEEK)?.value, priceDivisor),
|
|
2285
|
-
low52w: normalizeIbkrPriceValue(marketData.get(IBApiTickType.LOW_52_WEEK)?.value, priceDivisor),
|
|
2286
|
-
volume: marketData.get(IBApiTickType.VOLUME)?.value ?? marketData.get(IBApiTickType.DELAYED_VOLUME)?.value,
|
|
2287
|
-
name: details.longName || details.marketName || contract.symbol,
|
|
2288
|
-
lastUpdated: ingressTm,
|
|
2289
|
-
...ibkrVenueFields(undefined, contract, details),
|
|
2290
|
-
bid: normalizeIbkrPriceValue(
|
|
2291
|
-
marketData.get(IBApiTickType.BID)?.value ?? marketData.get(IBApiTickType.DELAYED_BID)?.value,
|
|
2292
|
-
priceDivisor,
|
|
2293
|
-
),
|
|
2294
|
-
ask: normalizeIbkrPriceValue(
|
|
2295
|
-
marketData.get(IBApiTickType.ASK)?.value ?? marketData.get(IBApiTickType.DELAYED_ASK)?.value,
|
|
2296
|
-
priceDivisor,
|
|
2297
|
-
),
|
|
2298
|
-
bidSize: marketData.get(IBApiTickType.BID_SIZE)?.value ?? marketData.get(IBApiTickType.DELAYED_BID_SIZE)?.value,
|
|
2299
|
-
askSize: marketData.get(IBApiTickType.ASK_SIZE)?.value ?? marketData.get(IBApiTickType.DELAYED_ASK_SIZE)?.value,
|
|
2300
|
-
open: normalizeIbkrPriceValue(
|
|
2301
|
-
marketData.get(IBApiTickType.OPEN)?.value ?? marketData.get(IBApiTickType.DELAYED_OPEN)?.value,
|
|
2302
|
-
priceDivisor,
|
|
2303
|
-
),
|
|
2304
|
-
high: normalizeIbkrPriceValue(
|
|
2305
|
-
marketData.get(IBApiTickType.HIGH)?.value ?? marketData.get(IBApiTickType.DELAYED_HIGH)?.value,
|
|
2306
|
-
priceDivisor,
|
|
2307
|
-
),
|
|
2308
|
-
low: normalizeIbkrPriceValue(
|
|
2309
|
-
marketData.get(IBApiTickType.LOW)?.value ?? marketData.get(IBApiTickType.DELAYED_LOW)?.value,
|
|
2310
|
-
priceDivisor,
|
|
2311
|
-
),
|
|
2312
|
-
mark,
|
|
2313
|
-
};
|
|
358
|
+
async previewOrder(config: IbkrGatewayConfig, request: BrokerOrderRequest): Promise<BrokerOrderPreview> {
|
|
359
|
+
const remote = getBrokerRemoteClient();
|
|
360
|
+
return remote
|
|
361
|
+
? remote.invoke(this.instanceId, "previewOrder", [request])
|
|
362
|
+
: (await getNativeService(this.instanceId)).previewOrder(config, request);
|
|
2314
363
|
}
|
|
2315
364
|
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
orderStatus?: {
|
|
2322
|
-
status: string;
|
|
2323
|
-
filled: number;
|
|
2324
|
-
remaining: number;
|
|
2325
|
-
avgFillPrice: number;
|
|
2326
|
-
};
|
|
2327
|
-
}): BrokerOrder {
|
|
2328
|
-
return {
|
|
2329
|
-
orderId: openOrder.orderId,
|
|
2330
|
-
brokerInstanceId: this.instanceId,
|
|
2331
|
-
accountId: openOrder.order.account,
|
|
2332
|
-
status: openOrder.orderStatus?.status || openOrder.orderState.status || "Unknown",
|
|
2333
|
-
action: (openOrder.order.action || "BUY") as BrokerOrder["action"],
|
|
2334
|
-
orderType: openOrder.order.orderType || "",
|
|
2335
|
-
quantity: openOrder.order.totalQuantity ?? 0,
|
|
2336
|
-
filled: openOrder.orderStatus?.filled ?? 0,
|
|
2337
|
-
remaining: openOrder.orderStatus?.remaining ?? openOrder.order.totalQuantity ?? 0,
|
|
2338
|
-
avgFillPrice: openOrder.orderStatus?.avgFillPrice,
|
|
2339
|
-
limitPrice: openOrder.order.lmtPrice,
|
|
2340
|
-
stopPrice: openOrder.order.auxPrice,
|
|
2341
|
-
tif: openOrder.order.tif,
|
|
2342
|
-
warningText: openOrder.orderState.warningText,
|
|
2343
|
-
updatedAt: Date.now(),
|
|
2344
|
-
contract: this.contractToRef(openOrder.contract),
|
|
2345
|
-
};
|
|
365
|
+
async placeOrder(config: IbkrGatewayConfig, request: BrokerOrderRequest): Promise<BrokerOrder> {
|
|
366
|
+
const remote = getBrokerRemoteClient();
|
|
367
|
+
return remote
|
|
368
|
+
? remote.invoke(this.instanceId, "placeOrder", [request])
|
|
369
|
+
: (await getNativeService(this.instanceId)).placeOrder(config, request);
|
|
2346
370
|
}
|
|
2347
371
|
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
orderType: request.orderType as OrderType,
|
|
2354
|
-
lmtPrice: request.limitPrice,
|
|
2355
|
-
auxPrice: request.stopPrice,
|
|
2356
|
-
tif: (request.tif ?? TimeInForce.DAY) as typeof TimeInForce[keyof typeof TimeInForce],
|
|
2357
|
-
outsideRth: request.outsideRth ?? false,
|
|
2358
|
-
whatIf,
|
|
2359
|
-
transmit: true,
|
|
2360
|
-
};
|
|
372
|
+
async modifyOrder(config: IbkrGatewayConfig, orderId: number, request: BrokerOrderRequest): Promise<BrokerOrder> {
|
|
373
|
+
const remote = getBrokerRemoteClient();
|
|
374
|
+
return remote
|
|
375
|
+
? remote.invoke(this.instanceId, "modifyOrder", [orderId, request])
|
|
376
|
+
: (await getNativeService(this.instanceId)).modifyOrder(config, orderId, request);
|
|
2361
377
|
}
|
|
2362
378
|
|
|
2363
|
-
|
|
2364
|
-
|
|
379
|
+
async cancelOrder(config: IbkrGatewayConfig, orderId: number): Promise<void> {
|
|
380
|
+
const remote = getBrokerRemoteClient();
|
|
381
|
+
return remote
|
|
382
|
+
? remote.invoke(this.instanceId, "cancelOrder", [orderId])
|
|
383
|
+
: (await getNativeService(this.instanceId)).cancelOrder(config, orderId);
|
|
2365
384
|
}
|
|
2366
385
|
}
|
|
2367
386
|
|
|
2368
387
|
export class IbkrGatewayServiceManager {
|
|
2369
|
-
private services = new Map<string,
|
|
388
|
+
private services = new Map<string, IbkrGatewayServiceFacade>();
|
|
2370
389
|
|
|
2371
|
-
getService(instanceId: string):
|
|
390
|
+
getService(instanceId: string): IbkrGatewayServiceFacade {
|
|
2372
391
|
let service = this.services.get(instanceId);
|
|
2373
392
|
if (!service) {
|
|
2374
|
-
service = new
|
|
393
|
+
service = new IbkrGatewayServiceFacade(instanceId);
|
|
2375
394
|
this.services.set(instanceId, service);
|
|
2376
395
|
}
|
|
2377
396
|
return service;
|
|
@@ -2382,22 +401,32 @@ export class IbkrGatewayServiceManager {
|
|
|
2382
401
|
return this.getService(instanceId).getSnapshot();
|
|
2383
402
|
}
|
|
2384
403
|
|
|
2385
|
-
subscribe(instanceId: string | undefined, listener:
|
|
404
|
+
subscribe(instanceId: string | undefined, listener: Listener): () => void {
|
|
2386
405
|
if (!instanceId) return () => {};
|
|
2387
406
|
return this.getService(instanceId).subscribe(listener);
|
|
2388
407
|
}
|
|
2389
408
|
|
|
2390
409
|
async removeInstance(instanceId: string): Promise<void> {
|
|
2391
|
-
const
|
|
2392
|
-
if (
|
|
2393
|
-
|
|
410
|
+
const remote = getBrokerRemoteClient();
|
|
411
|
+
if (remote) {
|
|
412
|
+
await remote.removeInstance(instanceId);
|
|
413
|
+
} else if (nativeGatewayManager) {
|
|
414
|
+
await nativeGatewayManager.removeInstance(instanceId);
|
|
415
|
+
}
|
|
2394
416
|
this.services.delete(instanceId);
|
|
417
|
+
snapshots.delete(instanceId);
|
|
418
|
+
notify(instanceId);
|
|
2395
419
|
}
|
|
2396
420
|
|
|
2397
421
|
async destroyAll(): Promise<void> {
|
|
2398
|
-
const
|
|
422
|
+
const remote = getBrokerRemoteClient();
|
|
423
|
+
if (remote) {
|
|
424
|
+
await remote.destroyAll();
|
|
425
|
+
} else if (nativeGatewayManager) {
|
|
426
|
+
await nativeGatewayManager.destroyAll();
|
|
427
|
+
}
|
|
2399
428
|
this.services.clear();
|
|
2400
|
-
|
|
429
|
+
snapshots.clear();
|
|
2401
430
|
}
|
|
2402
431
|
}
|
|
2403
432
|
|