gloomberb 0.18.4 → 0.19.0
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +169 -5
- package/src/account/auth-model.ts +7 -3
- package/src/api-client/auth.ts +9 -4
- package/src/api-client/catalysts.ts +2 -1
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +31 -6
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/index.ts +171 -11
- package/src/api-client/live-scan.ts +91 -0
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/members.ts +13 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/request.ts +222 -41
- package/src/api-client/research-activity.ts +6 -12
- package/src/api-client/types.ts +74 -0
- package/src/app/keybindings/index.ts +3 -0
- package/src/app/keybindings/labels.ts +18 -1
- package/src/app/keybindings/resolve.ts +14 -0
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +42 -27
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/core-builtin-allowlist.json +1 -1
- package/src/architecture/pane-tabs-allowlist.json +3 -0
- package/src/architecture/ttl-caches-allowlist.json +19 -0
- package/src/brokers/sync-broker-instance.ts +2 -8
- package/src/capabilities/chart-series.ts +1 -1
- package/src/cli/commands/crypto-hints.ts +40 -24
- package/src/cli/commands/fred-series.ts +5 -0
- package/src/cli/commands/keybindings.ts +23 -0
- package/src/cli/commands/market.ts +138 -46
- package/src/cli/commands/options-chain.ts +107 -10
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +176 -39
- package/src/cli/commands/search.ts +39 -3
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +57 -35
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +10 -2
- package/src/cli/export-source.ts +29 -1
- package/src/cli/glossary.ts +73 -1
- package/src/cli/history-rows.ts +87 -16
- package/src/cli/index.ts +2 -0
- package/src/cli/known-gaps.ts +5 -2
- package/src/cli/listing-arg.ts +195 -56
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +3 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +111 -16
- package/src/cli/pane-functions/function-help.ts +23 -13
- package/src/cli/pane-functions/headless.ts +56 -12
- package/src/cli/pane-functions/index.ts +58 -16
- package/src/cli/pane-functions/market-session.ts +81 -9
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/report.ts +5 -1
- package/src/cli/pane-functions/resolver.ts +35 -8
- package/src/cli/pane-functions/screenshot-assessment.ts +6 -4
- package/src/cli/pane-functions/screenshot-types.ts +5 -0
- package/src/cli/quote-answer.ts +30 -13
- package/src/cli/report-tables.ts +18 -7
- package/src/cli/restore-plugins.ts +13 -1
- package/src/cli/result.ts +57 -5
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +26 -4
- package/src/components/chart/composite/format.ts +27 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/levels.ts +2 -0
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +61 -5
- package/src/components/chart/composite/range-statistics.ts +62 -0
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +46 -7
- package/src/components/chart/composite/series-axis-tags.tsx +16 -9
- package/src/components/chart/composite/series-cues.ts +14 -0
- package/src/components/chart/composite/text-renderer.ts +9 -2
- package/src/components/chart/composite/tool-catalog.ts +36 -0
- package/src/components/chart/composite/tools.ts +203 -44
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +11 -4
- package/src/components/command-bar/assist/inventory.ts +2 -1
- package/src/components/command-bar/commands/direct/index.ts +28 -13
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/panel/prompt-binding.ts +0 -8
- package/src/components/command-bar/workflow/alert.ts +1 -1
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/layout/header.tsx +26 -40
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +27 -0
- package/src/components/onboarding/onboarding-frame.tsx +1 -1
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -9
- package/src/components/onboarding/wizard-broker-sync.ts +0 -1
- package/src/components/onboarding/wizard-positions.ts +3 -3
- package/src/components/ticker/list-table-view.tsx +4 -1
- package/src/components/ui/choice-dialog.tsx +23 -18
- package/src/components/ui/choice-groups.ts +18 -0
- package/src/components/ui/data-table/opentui/index.tsx +9 -7
- package/src/components/ui/data-table/opentui/model.ts +26 -1
- package/src/components/ui/data-table/types.ts +2 -0
- package/src/components/ui/display.tsx +5 -1
- package/src/components/ui/list-view.tsx +8 -4
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +1 -0
- package/src/components/ui/select-button.tsx +5 -1
- package/src/components/ui/select-field.tsx +1 -0
- package/src/components/ui/table-layout.ts +8 -5
- package/src/core/app-runtime.ts +2 -0
- package/src/core/app-services.ts +2 -2
- package/src/core/state/app/layout.ts +2 -0
- package/src/core/state/app/state.ts +9 -0
- package/src/core/state/app/types.ts +3 -0
- package/src/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/saved-copies.ts +55 -16
- package/src/data/ticker-repository.ts +16 -9
- package/src/data/ticker-writes.ts +81 -0
- package/src/data/ttl-cache.ts +165 -0
- package/src/docs/snapshot.json +427 -216
- package/src/i18n/es.ts +19 -3
- package/src/i18n/ja.ts +19 -3
- package/src/i18n/ko.ts +19 -3
- package/src/i18n/zh-cn.ts +19 -3
- package/src/i18n/zh-tw.ts +19 -3
- package/src/market-data/coordinator/financials.ts +10 -2
- package/src/market-data/coordinator/index.ts +3 -1
- package/src/market-data/coordinator/quotes.ts +4 -2
- package/src/market-data/depositary-pair.ts +2 -2
- package/src/market-data/fund-facts.ts +50 -0
- package/src/market-data/market/change-basis.ts +48 -7
- package/src/market-data/market/dated-previous-close.ts +203 -0
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/market/futures-week.ts +68 -0
- package/src/market-data/market/status.ts +16 -4
- package/src/market-data/options-alternatives.ts +141 -4
- package/src/market-data/quotes/contributions.ts +20 -2
- package/src/market-data/quotes/resolution.ts +3 -0
- package/src/market-data/selectors.ts +3 -0
- package/src/news/aggregator.ts +113 -9
- package/src/news/cloud-session.ts +21 -0
- package/src/news/hooks.ts +1 -0
- package/src/news/news-model.ts +1 -0
- package/src/news/types.ts +11 -0
- package/src/notifications/notification-log.ts +6 -4
- package/src/plugins/builtin/alerts/alert-engine.ts +25 -2
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +380 -82
- package/src/plugins/builtin/alerts/command.ts +31 -0
- package/src/plugins/builtin/alerts/constants.ts +1 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +19 -4
- package/src/plugins/builtin/alerts/fired-log.ts +113 -8
- package/src/plugins/builtin/alerts/format.ts +80 -4
- package/src/plugins/builtin/alerts/headless.ts +30 -7
- package/src/plugins/builtin/alerts/history-rows.ts +7 -3
- package/src/plugins/builtin/alerts/history.ts +2 -0
- package/src/plugins/builtin/alerts/index.tsx +135 -22
- package/src/plugins/builtin/alerts/live.ts +3 -3
- package/src/plugins/builtin/alerts/metric-command.ts +134 -0
- package/src/plugins/builtin/alerts/metric-data.ts +111 -0
- package/src/plugins/builtin/alerts/metric-model.ts +324 -0
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
- package/src/plugins/builtin/alerts/pane.tsx +90 -39
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +37 -10
- package/src/plugins/builtin/alerts/types.ts +7 -0
- package/src/plugins/builtin/analytics/index.tsx +2 -1
- package/src/plugins/builtin/analytics/risk-client.ts +16 -5
- package/src/plugins/builtin/analytics/risk-fx.ts +70 -15
- package/src/plugins/builtin/analytics/risk-headless.ts +27 -0
- package/src/plugins/builtin/analytics/risk-model.ts +55 -9
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/attention/index.tsx +2 -1
- package/src/plugins/builtin/attention/listing.ts +14 -0
- package/src/plugins/builtin/backtest/headless.ts +10 -5
- package/src/plugins/builtin/backtest/pane.tsx +9 -4
- package/src/plugins/builtin/backtest/presets.ts +29 -3
- package/src/plugins/builtin/broker-manager/index.tsx +10 -5
- package/src/plugins/builtin/broker-manager/model.ts +6 -1
- package/src/plugins/builtin/broker-trading/controller.ts +22 -12
- package/src/plugins/builtin/broker-trading/index.tsx +13 -1
- package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
- package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +12 -4
- package/src/plugins/builtin/buildout/format.ts +38 -0
- package/src/plugins/builtin/buildout/model/normalizers.ts +36 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +4 -1
- package/src/plugins/builtin/buildout/table-model.ts +23 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +7 -3
- package/src/plugins/builtin/catalysts/model.ts +2 -1
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +6 -1
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
- package/src/plugins/builtin/chart-composer/cli-options.ts +9 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +12 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +4 -2
- package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
- package/src/plugins/builtin/chart-composer/formula.ts +71 -18
- package/src/plugins/builtin/chart-composer/headless-schema.ts +21 -4
- package/src/plugins/builtin/chart-composer/headless.ts +19 -3
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/pane.tsx +53 -9
- package/src/plugins/builtin/chart-composer/presets.ts +50 -15
- package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
- package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
- package/src/plugins/builtin/chat/content/command-suggestions.ts +93 -0
- package/src/plugins/builtin/chat/content/composer.tsx +13 -6
- package/src/plugins/builtin/chat/content/footer.tsx +6 -3
- package/src/plugins/builtin/chat/content/index.tsx +50 -21
- package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
- package/src/plugins/builtin/chat/index.tsx +23 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
- package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
- package/src/plugins/builtin/chat/pane-links/commands.ts +271 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
- package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
- package/src/plugins/builtin/clinical-trials/model.ts +0 -9
- package/src/plugins/builtin/clinical-trials/pane.tsx +15 -22
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +8 -6
- package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +1 -1
- package/src/plugins/builtin/company-attention/building.tsx +5 -1
- package/src/plugins/builtin/company-attention/client.ts +8 -9
- package/src/plugins/builtin/company-attention/evidence.ts +4 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +9 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/correlation/headless.ts +13 -5
- package/src/plugins/builtin/correlation/history.ts +22 -0
- package/src/plugins/builtin/cot/client.ts +22 -18
- package/src/plugins/builtin/cot/headless.ts +15 -10
- package/src/plugins/builtin/cot/model.ts +14 -1
- package/src/plugins/builtin/cot/pane.tsx +22 -14
- package/src/plugins/builtin/credit-boards/client.ts +6 -10
- package/src/plugins/builtin/credit-conditions/headless.ts +8 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +5 -1
- package/src/plugins/builtin/credit-conditions/model.ts +4 -0
- package/src/plugins/builtin/credit-conditions/series.ts +3 -1
- package/src/plugins/builtin/credit-documents/detail.tsx +2 -2
- package/src/plugins/builtin/credit-documents/export.ts +173 -0
- package/src/plugins/builtin/credit-documents/headless.ts +7 -7
- package/src/plugins/builtin/credit-documents/model.ts +104 -14
- package/src/plugins/builtin/credit-documents/pane.tsx +12 -5
- package/src/plugins/builtin/custom-view/sources.ts +2 -0
- package/src/plugins/builtin/dividend-yield/client.ts +131 -23
- package/src/plugins/builtin/dividend-yield/headless.ts +44 -16
- package/src/plugins/builtin/dividend-yield/model.ts +8 -5
- package/src/plugins/builtin/dividend-yield/pane.tsx +38 -13
- package/src/plugins/builtin/dividend-yield/ranking.ts +37 -14
- package/src/plugins/builtin/dividend-yield/types.ts +20 -2
- package/src/plugins/builtin/dividend-yield/view.ts +30 -2
- package/src/plugins/builtin/docs/targets.ts +5 -1
- package/src/plugins/builtin/earnings/board-model.ts +16 -15
- package/src/plugins/builtin/earnings/board.tsx +62 -71
- package/src/plugins/builtin/earnings/headless.ts +40 -4
- package/src/plugins/builtin/earnings/scope.ts +31 -43
- package/src/plugins/builtin/earnings-calls/calendar.tsx +2 -2
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -2
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +5 -4
- package/src/plugins/builtin/econ/calendar-model.ts +14 -9
- package/src/plugins/builtin/econ/figures.ts +72 -0
- package/src/plugins/builtin/econ/index.tsx +33 -3
- package/src/plugins/builtin/econ/types.ts +4 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +85 -35
- package/src/plugins/builtin/econ-statistics/stats.ts +37 -9
- package/src/plugins/builtin/econ-statistics/view.ts +3 -0
- package/src/plugins/builtin/equity-screener/categories.ts +179 -0
- package/src/plugins/builtin/equity-screener/client.ts +4 -2
- package/src/plugins/builtin/equity-screener/headless.ts +144 -13
- package/src/plugins/builtin/equity-screener/ideas.ts +49 -0
- package/src/plugins/builtin/equity-screener/model.ts +99 -10
- package/src/plugins/builtin/equity-screener/pane.tsx +70 -15
- package/src/plugins/builtin/exposure/charts.tsx +2 -2
- package/src/plugins/builtin/exposure/model.ts +13 -1
- package/src/plugins/builtin/exposure/pane.tsx +4 -4
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/futures/model.ts +2 -1
- package/src/plugins/builtin/futures/table.tsx +2 -2
- package/src/plugins/builtin/futures/use-board-terms.ts +42 -28
- package/src/plugins/builtin/futures/use-front-returns.ts +45 -28
- package/src/plugins/builtin/fx-matrix/client.ts +11 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +6 -3
- package/src/plugins/builtin/fx-matrix/index.tsx +16 -4
- package/src/plugins/builtin/fx-matrix/live-legs.ts +54 -3
- package/src/plugins/builtin/gpu/headless.ts +8 -2
- package/src/plugins/builtin/help/function-index.ts +4 -1
- package/src/plugins/builtin/help/index.tsx +1 -0
- package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +18 -1
- package/src/plugins/builtin/iv-history/client.ts +52 -11
- package/src/plugins/builtin/iv-history/format.ts +36 -23
- package/src/plugins/builtin/iv-history/headless.ts +36 -18
- package/src/plugins/builtin/iv-history/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/model.ts +163 -27
- package/src/plugins/builtin/iv-history/pane.tsx +10 -6
- package/src/plugins/builtin/iv-history/screen-pane.tsx +22 -15
- package/src/plugins/builtin/iv-history/universe.ts +5 -1
- package/src/plugins/builtin/jobs/client.ts +25 -29
- package/src/plugins/builtin/kelly-sizer/model.ts +12 -8
- package/src/plugins/builtin/kelly-sizer/pane.tsx +9 -9
- package/src/plugins/builtin/kelly-sizer/sections.tsx +19 -10
- package/src/plugins/builtin/kelly-sizer/types.ts +10 -4
- package/src/plugins/builtin/macro-day/pane.tsx +7 -5
- package/src/plugins/builtin/macro-day/release-table.ts +2 -2
- package/src/plugins/builtin/market-heatmap/data.ts +8 -33
- package/src/plugins/builtin/market-movers/client.ts +25 -7
- package/src/plugins/builtin/market-movers/footer.ts +2 -2
- package/src/plugins/builtin/market-movers/headless.ts +32 -12
- package/src/plugins/builtin/market-movers/index.tsx +26 -14
- package/src/plugins/builtin/market-movers/min-float.ts +74 -0
- package/src/plugins/builtin/market-movers/model.ts +4 -1
- package/src/plugins/builtin/market-movers/screener.ts +14 -0
- package/src/plugins/builtin/market-movers/session-body.tsx +13 -8
- package/src/plugins/builtin/market-movers/session-table.ts +4 -3
- package/src/plugins/builtin/market-movers/table.ts +17 -5
- package/src/plugins/builtin/members/breadth-pane.tsx +82 -0
- package/src/plugins/builtin/members/client.ts +19 -4
- package/src/plugins/builtin/members/headless.ts +20 -7
- package/src/plugins/builtin/members/index.tsx +9 -9
- package/src/plugins/builtin/members/model.ts +23 -4
- package/src/plugins/builtin/members/pane.tsx +25 -15
- package/src/plugins/builtin/news/index.tsx +1 -0
- package/src/plugins/builtin/news/wire/categories.ts +19 -3
- package/src/plugins/builtin/news/wire/industry-pane.tsx +1 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +31 -5
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +6 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -2
- package/src/plugins/builtin/openfda/client.ts +14 -2
- package/src/plugins/builtin/openfda/index.tsx +3 -2
- package/src/plugins/builtin/openfda/model.ts +9 -0
- package/src/plugins/builtin/openfda/pane.tsx +42 -6
- package/src/plugins/builtin/options/analytics.ts +54 -6
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/model-spot.ts +5 -2
- package/src/plugins/builtin/options/strike-window.ts +2 -1
- package/src/plugins/builtin/options/table.ts +6 -9
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +42 -12
- package/src/plugins/builtin/options-positioning/client.ts +30 -0
- package/src/plugins/builtin/options-positioning/headless.ts +74 -21
- package/src/plugins/builtin/options-positioning/index.tsx +4 -4
- package/src/plugins/builtin/options-positioning/model.ts +122 -14
- package/src/plugins/builtin/options-positioning/pane.tsx +87 -42
- package/src/plugins/builtin/options-scenario/client.ts +116 -47
- package/src/plugins/builtin/options-scenario/csp-screen-headless.ts +142 -10
- package/src/plugins/builtin/options-scenario/csp-screen-pane.tsx +206 -55
- package/src/plugins/builtin/options-scenario/csp-screen.ts +63 -23
- package/src/plugins/builtin/options-scenario/csp.ts +8 -2
- package/src/plugins/builtin/options-scenario/index.tsx +9 -7
- package/src/plugins/builtin/options-scenario/strategy-screen.ts +532 -0
- package/src/plugins/builtin/pe-band/model.ts +12 -49
- package/src/plugins/builtin/perps/cascade-tabs.tsx +161 -0
- package/src/plugins/builtin/perps/cascade.ts +236 -0
- package/src/plugins/builtin/perps/client.ts +56 -2
- package/src/plugins/builtin/perps/freshness.ts +12 -10
- package/src/plugins/builtin/perps/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/live-rows.ts +10 -3
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +32 -31
- package/src/plugins/builtin/perps/status-line.ts +25 -0
- package/src/plugins/builtin/plugin-module.ts +5 -1
- package/src/plugins/builtin/portfolio-list/cached-account.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +2 -8
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +12 -7
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +4 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +5 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +3 -2
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +3 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -3
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/rate-path/pane.tsx +2 -2
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/client.ts +18 -4
- package/src/plugins/builtin/relative-rotation/headless.ts +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +2 -1
- package/src/plugins/builtin/relative-rotation/model.ts +16 -40
- package/src/plugins/builtin/relative-rotation/pane.tsx +3 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -2
- package/src/plugins/builtin/research/event-model.ts +10 -8
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/relative-valuation-headless.ts +5 -1
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +11 -9
- package/src/plugins/builtin/reverse-dcf/headless.ts +5 -3
- package/src/plugins/builtin/reverse-dcf/model.ts +61 -6
- package/src/plugins/builtin/reverse-dcf/pane.tsx +7 -4
- package/src/plugins/builtin/scanner/feed.ts +2 -2
- package/src/plugins/builtin/scanner/flow-model.ts +50 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +367 -65
- package/src/plugins/builtin/scanner/flow-session.ts +146 -0
- package/src/plugins/builtin/scanner/headless.ts +204 -35
- package/src/plugins/builtin/scanner/index.ts +40 -6
- package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
- package/src/plugins/builtin/scanner/live-feed.ts +168 -0
- package/src/plugins/builtin/scanner/live-headless.ts +138 -0
- package/src/plugins/builtin/scanner/live-model.ts +362 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
- package/src/plugins/builtin/scanner/live-track.ts +186 -0
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/seasonality/headless.ts +10 -1
- package/src/plugins/builtin/sec/fund-facts-view.tsx +5 -2
- package/src/plugins/builtin/sec/fund-facts.ts +116 -34
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/series-inventory.ts +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +3 -2
- package/src/plugins/builtin/shared/cloud-upgrade.ts +4 -3
- package/src/plugins/builtin/shared/earnings-calendar.ts +6 -2
- package/src/plugins/builtin/shared/earnings-report.ts +2 -2
- package/src/plugins/builtin/shared/job-posting.ts +1 -0
- package/src/plugins/builtin/shared/macro-releases.ts +65 -0
- package/src/plugins/builtin/shared/members-menu.tsx +3 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +12 -12
- package/src/plugins/builtin/shared/report-freshness.ts +3 -2
- package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
- package/src/plugins/builtin/shared/ticker-choice.tsx +2 -1
- package/src/plugins/builtin/shared/ticker-company.ts +77 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +10 -3
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +26 -1
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/headless.ts +6 -4
- package/src/plugins/builtin/themes/model.ts +17 -8
- package/src/plugins/builtin/themes/pane.tsx +7 -7
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +43 -18
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +3 -2
- package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +62 -0
- package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +83 -46
- package/src/plugins/builtin/ticker-detail/headless.ts +31 -16
- package/src/plugins/builtin/ticker-detail/overview/model.ts +1 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/settings.ts +1 -0
- package/src/plugins/builtin/time-sales/pane.tsx +31 -19
- package/src/plugins/builtin/treasury-daily/client.ts +41 -5
- package/src/plugins/builtin/treasury-daily/headless.ts +2 -2
- package/src/plugins/builtin/treasury-daily/index.tsx +1 -1
- package/src/plugins/builtin/treasury-daily/model.ts +50 -3
- package/src/plugins/builtin/treasury-daily/pane.tsx +81 -2
- package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
- package/src/plugins/builtin/vol-surface/client.ts +9 -17
- package/src/plugins/builtin/vol-surface/headless.ts +63 -14
- package/src/plugins/builtin/vol-surface/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/model.ts +22 -7
- package/src/plugins/builtin/vol-surface/pane.tsx +48 -9
- package/src/plugins/builtin/vol-surface/tables.ts +31 -3
- package/src/plugins/builtin/volatility/dvol.ts +46 -0
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +4 -4
- package/src/plugins/builtin/volatility/series.ts +26 -0
- package/src/plugins/builtin/world-indices/footer.ts +1 -1
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/catalog-browser.ts +13 -8
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/column-values.ts +5 -5
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
- package/src/remote/resources.ts +2 -1
- package/src/renderers/browser/app-services.ts +1 -7
- package/src/renderers/browser/config-host.ts +2 -55
- package/src/renderers/browser/main.tsx +7 -57
- package/src/renderers/dom/data-table/row.tsx +26 -0
- package/src/renderers/dom/desktop/list-view.tsx +13 -10
- package/src/renderers/dom/desktop/query-bar.tsx +4 -2
- package/src/renderers/dom/desktop/select-field.tsx +4 -2
- package/src/renderers/dom/host/chart-surface.tsx +21 -0
- package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +29 -7
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
- package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +173 -5
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
- package/src/renderers/electrobun/bun/index.ts +25 -1
- package/src/renderers/electrobun/shared/app-name.ts +30 -0
- package/src/renderers/electrobun/shared/protocol.ts +10 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
- package/src/renderers/electrobun/view/main.tsx +24 -1
- package/src/renderers/electrobun/view/news-source.ts +3 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/view-ids.ts +85 -0
- package/src/renderers/opentui/start.tsx +5 -0
- package/src/renderers/opentui/toast-host.tsx +71 -43
- package/src/series/registry.ts +1 -0
- package/src/series/types.ts +6 -0
- package/src/series/validate.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +1 -0
- package/src/sources/gloomberb-cloud/news.ts +47 -7
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/batches.ts +10 -2
- package/src/sources/provider-router/financial-routes.ts +3 -3
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/history.ts +28 -7
- package/src/sources/provider-router/index.ts +149 -10
- package/src/sources/provider-router/search.ts +13 -2
- package/src/state/app/context/index.tsx +3 -0
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/auth-transition.ts +19 -9
- package/src/sync/baseline.ts +10 -4
- package/src/sync/controller.ts +70 -19
- package/src/sync/core-contributors.ts +90 -31
- package/src/sync/react.ts +4 -1
- package/src/sync/types.ts +3 -0
- package/src/telemetry/stall-reports-dom.ts +38 -0
- package/src/telemetry/stall-reports-node.ts +75 -0
- package/src/telemetry/stall-reports.ts +214 -0
- package/src/telemetry/usage-counts.ts +0 -18
- package/src/test-support/preload.ts +14 -0
- package/src/theme/colors.ts +8 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name.ts +103 -3
- package/src/tickers/listing-venues.ts +38 -3
- package/src/tickers/search/bare-symbol.ts +36 -0
- package/src/tickers/search/index.ts +36 -7
- package/src/tickers/search/ranking.ts +10 -1
- package/src/tickers/search/types.ts +5 -0
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/base-series.ts +13 -5
- package/src/time-series/daily-valuation.ts +62 -0
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +2 -4
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/forward-valuation.ts +32 -0
- package/src/time-series/fundamentals.ts +198 -7
- package/src/time-series/hooks.ts +1 -0
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/price-units.ts +72 -2
- package/src/time-series/resolve-context.ts +7 -1
- package/src/time-series/resolve.ts +116 -15
- package/src/time-series/series-presentation.ts +40 -6
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +5 -3
- package/src/time-series/studies.ts +278 -74
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +3 -3
- package/src/time-series/types.ts +27 -1
- package/src/time-series/valuation-releases.ts +39 -0
- package/src/types/data-provider.ts +9 -1
- package/src/types/desktop-window.ts +2 -0
- package/src/types/financials.ts +73 -2
- package/src/types/headless.ts +61 -5
- package/src/types/instrument.ts +3 -0
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +10 -1
- package/src/ui/host.tsx +3 -0
- package/src/updater/command-names.ts +12 -7
- package/src/updater/failure.ts +4 -1
- package/src/utils/cli-output.ts +42 -1
- package/src/utils/crypto-pair.ts +44 -0
- package/src/utils/financial-statements.ts +6 -1
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +24 -3
- package/src/utils/fx-market-hours.ts +26 -3
- package/src/utils/main-thread-stall.ts +35 -2
- package/src/utils/market-capitalization.ts +24 -1
- package/src/utils/network-failure.ts +9 -6
- package/src/utils/option-expiry.ts +48 -1
- package/src/utils/options.ts +10 -0
- package/src/utils/plugin-proxy-hosts.json +1 -0
- package/src/utils/price-history.ts +12 -1
- package/src/utils/shell-word.ts +8 -0
- package/src/utils/trailing-dividend.ts +53 -4
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
- package/src/renderers/browser/first-screen.ts +0 -193
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useCallback, useMemo, type ReactNode } from "react";
|
|
2
|
+
import { isAccessDenied } from "../../../api-client/errors";
|
|
3
|
+
import type { PerpDepthRow, PerpLiquidationMarket, PerpLiquidationPrint } from "../../../api-client/perps";
|
|
4
|
+
import { ChartTableHeader, DataTableView, PaneStatusBody, QueryBar, useChartTableSelection, usePaneNoticeFooter, usePaneStatusFooter, useQueryBarSearch } from "../../../components";
|
|
5
|
+
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
6
|
+
import { useAsyncResource, useAutoRefresh, usePluginPaneState } from "../../../public/react";
|
|
7
|
+
import { useThemeColors } from "../../../theme/theme-context";
|
|
8
|
+
import { Box } from "../../../ui";
|
|
9
|
+
import { SignInWall } from "../cloud/auth-actions";
|
|
10
|
+
import { useCloudUpgradeAction } from "../shared/cloud-upgrade";
|
|
11
|
+
import { UpgradeLabel } from "../shared/locked-rows";
|
|
12
|
+
import { isCloudSessionRequired, useResearchCloudSession } from "../shared/research-cloud-session";
|
|
13
|
+
import { depthCell, depthColumns, depthFigures, depthFreshness, depthHistoryCount, depthSeries, depthStaleNote, LIQUIDATION_VIEWS, liquidationCell, liquidationColumns, liquidationFigures,
|
|
14
|
+
liquidationFreshness, liquidationSeries, printCell, printColumns, printHour, printKey, type DepthColumnId, type LiquidationColumnId, type LiquidationView, type PrintColumnId } from "./cascade";
|
|
15
|
+
import { cachedPerpsDepth, cachedPerpsLiquidations, loadPerpsDepth, loadPerpsLiquidations } from "./client";
|
|
16
|
+
import { compact } from "./model";
|
|
17
|
+
import { useFreshnessStatusLine } from "./status-line";
|
|
18
|
+
|
|
19
|
+
export interface CascadeTabProps {
|
|
20
|
+
/** The base asset the tab shows, shared with Compare. */
|
|
21
|
+
asset: string;
|
|
22
|
+
onAssetChange: (asset: string) => void;
|
|
23
|
+
accessKey: string;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
focused: boolean;
|
|
27
|
+
/** Enter on a contract: its history. */
|
|
28
|
+
onOpenMarket: (marketId: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const stamp = (s: string | null | undefined) => s ? `${s.slice(5, 10)} ${s.slice(11, 16)} UTC` : "";
|
|
32
|
+
const magnitude = (n: number) => compact(Math.abs(n));
|
|
33
|
+
const HOUR = 3_600_000;
|
|
34
|
+
|
|
35
|
+
/** The preview's lock under the rows: what Pro adds to the tab. */
|
|
36
|
+
function Lock({ text, role, onPress }: { text: string; role: string; onPress: () => void }) {
|
|
37
|
+
return <Box paddingX={1}><UpgradeLabel text={text} onPress={onPress} role={role} /></Box>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One base asset's liquidations across the venues' public streams: the 24-hour and 7-day totals, the hourly columns
|
|
42
|
+
* (Pro), and the contracts or the largest prints below them. What each stream leaves out is in the data warnings.
|
|
43
|
+
*/
|
|
44
|
+
export function Liquidations({ asset, onAssetChange, accessKey, width, height, focused, onOpenMarket }: CascadeTabProps) {
|
|
45
|
+
const colors = useThemeColors();
|
|
46
|
+
const upgrade = useCloudUpgradeAction("perp");
|
|
47
|
+
const session = useResearchCloudSession();
|
|
48
|
+
const querySearch = useQueryBarSearch();
|
|
49
|
+
const [viewValue, setView] = usePluginPaneState<string>("perps:liquidation-view", "contracts");
|
|
50
|
+
const view: LiquidationView = viewValue === "prints" ? "prints" : "contracts";
|
|
51
|
+
const [selectedMarket, setSelectedMarket] = usePluginPaneState<string | null>("perps:liquidation-market", null);
|
|
52
|
+
const [selectedPrint, setSelectedPrint] = usePluginPaneState<string | null>("perps:liquidation-print", null);
|
|
53
|
+
const base = asset.trim().toUpperCase() || "BTC";
|
|
54
|
+
const loader = useCallback((force: boolean) => loadPerpsLiquidations(base, accessKey, force), [base, accessKey]);
|
|
55
|
+
const resource = useAsyncResource(loader, { initialData: () => cachedPerpsLiquidations(base, accessKey), clearOnError: isAccessDenied, keepPreviousData: true });
|
|
56
|
+
const data = resource.data?.payload;
|
|
57
|
+
useAutoRefresh(resource.updatedAt, resource.load, { intervalMs: 60_000 });
|
|
58
|
+
usePaneRefreshKey(() => { void resource.reload(); }, { focused: focused && !querySearch.active });
|
|
59
|
+
const declare = useCallback(() => data ? liquidationFreshness(data) : null, [data]);
|
|
60
|
+
const line = useFreshnessStatusLine(declare, resource.updatedAt);
|
|
61
|
+
usePaneStatusFooter({ registrationId: "perps:liquidations", loading: resource.loading, error: data ? resource.error : null, stale: resource.data?.stale, info: line });
|
|
62
|
+
usePaneNoticeFooter({ registrationId: "perps:liquidations-notice", focused, title: "Liquidation streams",
|
|
63
|
+
notices: [...resource.data?.refreshError ? [resource.data.refreshError] : [], ...data?.notes ?? []] });
|
|
64
|
+
const markets = data?.markets ?? [];
|
|
65
|
+
const prints = data?.largest ?? [];
|
|
66
|
+
const market = markets.find((row) => row.marketId === selectedMarket) ?? markets[0] ?? null;
|
|
67
|
+
const print = prints.find((row) => printKey(row) === selectedPrint) ?? prints[0] ?? null;
|
|
68
|
+
// A print stands in its hour's column, so the chart's cursor follows the selected print.
|
|
69
|
+
const link = useChartTableSelection({ rows: prints, getId: printKey, getDate: printHour, selectedId: print ? printKey(print) : null, onSelect: setSelectedPrint,
|
|
70
|
+
focused: focused && !querySearch.active, enabled: view === "prints" });
|
|
71
|
+
const series = useMemo(() => liquidationSeries(data, colors), [data, colors]);
|
|
72
|
+
if (!data && isCloudSessionRequired(resource.error)) return <SignInWall placement="perps-signin" action="view perpetual liquidations" needsVerification={session.needsVerification} />;
|
|
73
|
+
const pro = data?.access === "pro";
|
|
74
|
+
const since = data?.collectingSince ? Date.parse(data.collectingSince) : null;
|
|
75
|
+
const asOf = data ? Date.parse(data.asOf) : Date.now();
|
|
76
|
+
// The chart starts where collection did, when that is inside the week: earlier hours are missing, not quiet. A venue
|
|
77
|
+
// that sends its recent liquidations on connecting (OKX) can add earlier hours, which the chart then reaches back to.
|
|
78
|
+
const firstHour = data?.hourly[0] ? Date.parse(data.hourly[0].time) : Infinity;
|
|
79
|
+
const start = data ? Math.max(Date.parse(data.from), Math.min(firstHour, since === null ? -Infinity : Math.floor(since / HOUR) * HOUR - HOUR)) : 0;
|
|
80
|
+
const quiet = data?.collectingSince && since! > Date.parse(data.from) ? `No liquidations since collection started ${stamp(data.collectingSince)}` : "No liquidations in the last 7 days";
|
|
81
|
+
const chart = data && pro ? { series: data.hourly.length ? series : [], formatValue: magnitude, formatAxisValue: magnitude, remoteKind: "perpetual-liquidations",
|
|
82
|
+
...(view === "prints" ? link : {}), viewport: { start: new Date(start), end: new Date(Math.ceil(asOf / HOUR) * HOUR) },
|
|
83
|
+
loading: !data && resource.loading, empty: data.hourly.length ? undefined : data.status === "unavailable" ? "Liquidations are not collected yet" : quiet } : null;
|
|
84
|
+
const figures = data ? liquidationFigures(data, colors) : [];
|
|
85
|
+
const contractColumns = liquidationColumns(width);
|
|
86
|
+
const shownPrintColumns = printColumns(width, data?.baseAsset ?? base);
|
|
87
|
+
const columns = view === "prints" ? shownPrintColumns : contractColumns;
|
|
88
|
+
const rows = view === "prints" ? prints.length : markets.length;
|
|
89
|
+
const before: ReactNode = <>
|
|
90
|
+
<QueryBar width={width} search={{ focused, value: asset, onChange: (value) => { onAssetChange(value); setSelectedMarket(null); setSelectedPrint(null); }, placeholder: "Base asset", debounceMs: 300, ...querySearch.searchProps }}
|
|
91
|
+
view={{ value: view, options: LIQUIDATION_VIEWS, onChange: setView }} />
|
|
92
|
+
<ChartTableHeader width={width} height={height - 1} tableRows={rows} tableColumns={columns} figures={figures} chart={chart} />
|
|
93
|
+
</>;
|
|
94
|
+
const lock = data && !pro ? <Lock text="Upgrade for hourly liquidations and the 20 largest prints" onPress={upgrade} role="perps-liquidations-upgrade" /> : null;
|
|
95
|
+
const metadata = () => [["As of", data?.asOf ?? ""], ["Base asset", data?.baseAsset ?? base], ["Collecting since", data?.collectingSince ?? ""],
|
|
96
|
+
["Units", "notional in USD, stablecoins counted as USD; longs are long positions liquidated (forced sells), shorts short positions (forced buys)"],
|
|
97
|
+
...(data?.notes ?? []).map((note) => ["Note", note])];
|
|
98
|
+
const empty = data?.status === "unavailable" ? "Liquidations are not collected yet." : `No perpetual contracts for ${base} on a liquidation stream.`;
|
|
99
|
+
return <PaneStatusBody loading={!data && resource.loading} error={!data ? resource.error : null} subject="perpetual liquidations">
|
|
100
|
+
{view === "prints"
|
|
101
|
+
? <DataTableView<PerpLiquidationPrint> columns={shownPrintColumns} items={prints} focused={focused && !querySearch.active} rootWidth={width} rootHeight={height} rootBefore={before}
|
|
102
|
+
getItemKey={printKey} sortColumnId={null} sortDirection="desc" resetScrollKey={`${base}:prints`}
|
|
103
|
+
selection={{ kind: "id", selectedId: print ? printKey(print) : null, getId: printKey, onChange: setSelectedPrint }} onActivate={(row) => onOpenMarket(row.marketId)}
|
|
104
|
+
renderCell={(row, column) => printCell(row, column.id as PrintColumnId, colors)} selectedTextOverridesCellColor
|
|
105
|
+
emptyStateTitle={data?.status === "unavailable" ? empty : "No liquidations in the last 7 days."} bodyAfter={lock} getExportMetadata={metadata} />
|
|
106
|
+
: <DataTableView<PerpLiquidationMarket> columns={contractColumns} items={markets} focused={focused && !querySearch.active} rootWidth={width} rootHeight={height} rootBefore={before}
|
|
107
|
+
getItemKey={(row) => row.marketId} sortColumnId={null} sortDirection="desc" resetScrollKey={`${base}:contracts`}
|
|
108
|
+
selection={{ kind: "id", selectedId: market?.marketId ?? null, getId: (row) => row.marketId, onChange: setSelectedMarket }} onActivate={(row) => onOpenMarket(row.marketId)}
|
|
109
|
+
renderCell={(row, column) => liquidationCell(row, column.id as LiquidationColumnId, colors)} selectedTextOverridesCellColor
|
|
110
|
+
emptyStateTitle={empty} bodyAfter={lock} getExportMetadata={metadata} />}
|
|
111
|
+
</PaneStatusBody>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* One base asset's order books: the bids and asks resting within 0.5%, 1% and 2% of mid on each venue, read hourly.
|
|
116
|
+
* Pro charts the selected book's 1% depth over the week. A band wider than the book a venue returns reads `≥`.
|
|
117
|
+
*/
|
|
118
|
+
export function Depth({ asset, onAssetChange, accessKey, width, height, focused, onOpenMarket }: CascadeTabProps) {
|
|
119
|
+
const colors = useThemeColors();
|
|
120
|
+
const upgrade = useCloudUpgradeAction("perp");
|
|
121
|
+
const session = useResearchCloudSession();
|
|
122
|
+
const querySearch = useQueryBarSearch();
|
|
123
|
+
const [selectedId, setSelected] = usePluginPaneState<string | null>("perps:depth-selected", null);
|
|
124
|
+
const base = asset.trim().toUpperCase() || "BTC";
|
|
125
|
+
const loader = useCallback((force: boolean) => loadPerpsDepth(base, accessKey, force), [base, accessKey]);
|
|
126
|
+
const resource = useAsyncResource(loader, { initialData: () => cachedPerpsDepth(base, accessKey), clearOnError: isAccessDenied, keepPreviousData: true });
|
|
127
|
+
const data = resource.data?.payload;
|
|
128
|
+
useAutoRefresh(resource.updatedAt, resource.load, { intervalMs: 300_000 });
|
|
129
|
+
usePaneRefreshKey(() => { void resource.reload(); }, { focused: focused && !querySearch.active });
|
|
130
|
+
const declare = useCallback(() => data?.rows.length ? depthFreshness(data) : null, [data]);
|
|
131
|
+
const line = useFreshnessStatusLine(declare, resource.updatedAt);
|
|
132
|
+
usePaneStatusFooter({ registrationId: "perps:depth", loading: resource.loading, error: data ? resource.error : null, stale: resource.data?.stale, info: line });
|
|
133
|
+
const rows = data?.rows ?? [];
|
|
134
|
+
const staleNote = depthStaleNote(rows);
|
|
135
|
+
usePaneNoticeFooter({ registrationId: "perps:depth-notice", focused, title: "Order book depth",
|
|
136
|
+
notices: [...resource.data?.refreshError ? [resource.data.refreshError] : [], ...staleNote ? [staleNote] : [], ...data?.notes ?? []] });
|
|
137
|
+
const selected = rows.find((row) => row.marketId === selectedId) ?? rows[0] ?? null;
|
|
138
|
+
const series = useMemo(() => depthSeries(data?.history ?? [], selected, colors), [data?.history, selected, colors]);
|
|
139
|
+
if (!data && isCloudSessionRequired(resource.error)) return <SignInWall placement="perps-signin" action="view perpetual order book depth" needsVerification={session.needsVerification} />;
|
|
140
|
+
const pro = data?.access === "pro";
|
|
141
|
+
const columns = depthColumns(width);
|
|
142
|
+
const points = depthHistoryCount(data?.history ?? [], selected?.marketId);
|
|
143
|
+
// A chart that follows the selected book keeps its band on a book without history, and says so.
|
|
144
|
+
const chart = data && pro && rows.length ? { series: points >= 3 ? series : [], formatValue: compact, formatAxisValue: compact, remoteKind: "perpetual-depth",
|
|
145
|
+
empty: points >= 3 ? undefined : "Collecting hourly depth for this book" } : null;
|
|
146
|
+
const before: ReactNode = <>
|
|
147
|
+
<QueryBar width={width} search={{ focused, value: asset, onChange: (value) => { onAssetChange(value); setSelected(null); }, placeholder: "Base asset", debounceMs: 300, ...querySearch.searchProps }} />
|
|
148
|
+
<ChartTableHeader width={width} height={height - 1} tableRows={rows.length} tableColumns={columns} figures={depthFigures(rows)} chart={chart} />
|
|
149
|
+
</>;
|
|
150
|
+
const lock = data && !pro ? <Lock text="Upgrade for hourly depth history" onPress={upgrade} role="perps-depth-upgrade" /> : null;
|
|
151
|
+
return <PaneStatusBody loading={!data && resource.loading} error={!data ? resource.error : null} subject="order book depth">
|
|
152
|
+
<DataTableView<PerpDepthRow> columns={columns} items={rows} focused={focused && !querySearch.active} rootWidth={width} rootHeight={height} rootBefore={before}
|
|
153
|
+
getItemKey={(row) => row.marketId} sortColumnId={null} sortDirection="desc" resetScrollKey={base}
|
|
154
|
+
selection={{ kind: "id", selectedId: selected?.marketId ?? null, getId: (row) => row.marketId, onChange: setSelected }} onActivate={(row) => onOpenMarket(row.marketId)}
|
|
155
|
+
renderCell={(row, column) => depthCell(row, column.id as DepthColumnId, colors)} selectedTextOverridesCellColor
|
|
156
|
+
emptyStateTitle={data?.status === "unavailable" ? "Order book depth is not collected yet." : `No order book depth for ${base} yet.`} bodyAfter={lock}
|
|
157
|
+
getExportMetadata={() => [["As of", data?.asOf ?? ""], ["Base asset", data?.baseAsset ?? base],
|
|
158
|
+
["Units", "quote notional in USD resting within the band of mid on each side; a value marked ≥ is a floor, the book returned ended inside the band; book reach in percent of mid"],
|
|
159
|
+
...[...staleNote ? [staleNote] : [], ...data?.notes ?? []].map((note) => ["Note", note])]} />
|
|
160
|
+
</PaneStatusBody>;
|
|
161
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { PerpDepthPayload, PerpDepthRow, PerpDepthSnapshot, PerpLiquidationMarket, PerpLiquidationPrint, PerpLiquidationsPayload } from "../../../api-client/perps";
|
|
2
|
+
import type { DataTableCell, DataTableColumn, StatItem } from "../../../components";
|
|
3
|
+
import { scalarPoint, staticSeries } from "../../../components/chart/static/series";
|
|
4
|
+
import { getTableWidth } from "../../../components/ui/table-layout";
|
|
5
|
+
import type { HeadlessPaneFreshness } from "../../../types/headless";
|
|
6
|
+
import type { ThemeColors } from "../../../theme/colors";
|
|
7
|
+
import { compact, price, venueName } from "./model";
|
|
8
|
+
|
|
9
|
+
// Liquidations and order book depth of one base asset across the venues: the Liquidations and Depth tabs, their
|
|
10
|
+
// text report and their charts read these, so the pane and `fn PERP` always agree.
|
|
11
|
+
|
|
12
|
+
type Colors = Pick<ThemeColors, "positive" | "negative" | "text" | "textBright" | "textMuted" | "textDim">;
|
|
13
|
+
/** Month to second, or to minute: the header names UTC. */
|
|
14
|
+
const stamp = (s: string | null | undefined, seconds = false) => s ? `${s.slice(5, 10)} ${s.slice(11, seconds ? 19 : 16)}` : "--";
|
|
15
|
+
const venueOrder = (venue: string) => { const at = ["binance", "bybit", "okx", "hyperliquid"].indexOf(venue); return at < 0 ? 9 : at; };
|
|
16
|
+
/** The venues behind some rows, in the order the tabs list them. */
|
|
17
|
+
const venuesIn = (rows: readonly { venue: string }[]) => [...new Set(rows.map((row) => row.venue))].sort((a, b) => venueOrder(a) - venueOrder(b));
|
|
18
|
+
const usd = (n: number | null | undefined) => n == null ? "--" : compact(n);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The columns that fit the pane, the least read going first. A pair (a band's bid and ask, a window's longs and
|
|
22
|
+
* shorts) goes together; the venue and the contract stay, since only both tell one venue's contracts apart.
|
|
23
|
+
*/
|
|
24
|
+
function fit<Id extends string>(columns: Record<Id, DataTableColumn>, ids: readonly Id[], drops: readonly (readonly Id[])[], width: number): DataTableColumn[] {
|
|
25
|
+
let kept = ids.map((id) => columns[id]);
|
|
26
|
+
for (const drop of drops) {
|
|
27
|
+
if (getTableWidth(kept) <= width) break;
|
|
28
|
+
kept = kept.filter((column) => !(drop as readonly string[]).includes(column.id));
|
|
29
|
+
}
|
|
30
|
+
return kept;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---- Liquidations ----
|
|
34
|
+
|
|
35
|
+
export type LiquidationView = "contracts" | "prints";
|
|
36
|
+
export const LIQUIDATION_VIEWS: Array<{ value: LiquidationView; label: string }> = [{ value: "contracts", label: "By contract" }, { value: "prints", label: "Largest" }];
|
|
37
|
+
|
|
38
|
+
export type LiquidationColumnId = "venue" | "symbol" | "long24h" | "short24h" | "long7d" | "short7d" | "count24h" | "lastEventAt";
|
|
39
|
+
const LIQUIDATION_COLUMNS: Record<LiquidationColumnId, DataTableColumn> = {
|
|
40
|
+
venue: { id: "venue", label: "Venue", width: 11, align: "left" },
|
|
41
|
+
symbol: { id: "symbol", label: "Contract", width: 14, align: "left" },
|
|
42
|
+
long24h: { id: "long24h", label: "Longs 24h USD", width: 13, align: "right" },
|
|
43
|
+
short24h: { id: "short24h", label: "Shorts 24h USD", width: 14, align: "right" },
|
|
44
|
+
long7d: { id: "long7d", label: "Longs 7d USD", width: 12, align: "right" },
|
|
45
|
+
short7d: { id: "short7d", label: "Shorts 7d USD", width: 13, align: "right" },
|
|
46
|
+
count24h: { id: "count24h", label: "Events 24h", width: 10, align: "right" },
|
|
47
|
+
lastEventAt: { id: "lastEventAt", label: "Last UTC", width: 11, align: "left" },
|
|
48
|
+
};
|
|
49
|
+
export const LIQUIDATION_COLUMN_IDS: LiquidationColumnId[] = ["venue", "symbol", "long24h", "short24h", "long7d", "short7d", "count24h", "lastEventAt"];
|
|
50
|
+
export const liquidationColumns = (width: number) => fit(LIQUIDATION_COLUMNS, LIQUIDATION_COLUMN_IDS, [["lastEventAt"], ["count24h"], ["long7d", "short7d"]], width);
|
|
51
|
+
export const liquidationColumnLabel = (id: LiquidationColumnId) => LIQUIDATION_COLUMNS[id].label;
|
|
52
|
+
|
|
53
|
+
/** A contract's liquidations: longs liquidated are forced sells (negative), shorts forced buys (positive). */
|
|
54
|
+
export function liquidationCellText(row: PerpLiquidationMarket, id: LiquidationColumnId): string {
|
|
55
|
+
if (id === "venue") return venueName(row.venue);
|
|
56
|
+
if (id === "symbol") return row.symbol;
|
|
57
|
+
if (id === "count24h") return row.count24h.toLocaleString("en-US");
|
|
58
|
+
if (id === "lastEventAt") return stamp(row.lastEventAt);
|
|
59
|
+
return usd(row[id]);
|
|
60
|
+
}
|
|
61
|
+
export function liquidationCell(row: PerpLiquidationMarket, id: LiquidationColumnId, colors: Colors): DataTableCell {
|
|
62
|
+
const text = liquidationCellText(row, id);
|
|
63
|
+
if (id === "venue") return { text, color: colors.textBright };
|
|
64
|
+
if (id === "symbol") return { text, color: colors.textMuted };
|
|
65
|
+
if (id === "lastEventAt") return { text, value: row.lastEventAt, color: colors.textDim };
|
|
66
|
+
if (id === "count24h") return { text, value: row.count24h, color: row.count24h ? colors.text : colors.textMuted };
|
|
67
|
+
const value = row[id];
|
|
68
|
+
return { text, value, color: !value ? colors.textMuted : id.startsWith("long") ? colors.negative : colors.positive };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type PrintColumnId = "eventAt" | "venue" | "symbol" | "side" | "price" | "priceKind" | "quantity" | "notionalUsd" | "receivedAt";
|
|
72
|
+
const printColumnsFor = (base: string): Record<PrintColumnId, DataTableColumn> => ({
|
|
73
|
+
eventAt: { id: "eventAt", label: "Time UTC", width: 14, align: "left" },
|
|
74
|
+
venue: { id: "venue", label: "Venue", width: 11, align: "left" },
|
|
75
|
+
symbol: { id: "symbol", label: "Contract", width: 14, align: "left" },
|
|
76
|
+
side: { id: "side", label: "Side", width: 5, align: "left" },
|
|
77
|
+
price: { id: "price", label: "Price", width: 12, align: "right" },
|
|
78
|
+
priceKind: { id: "priceKind", label: "Price basis", width: 11, align: "left" },
|
|
79
|
+
quantity: { id: "quantity", label: `Size ${base}`.slice(0, 14), width: Math.max(8, Math.min(14, base.length + 5)), align: "right" },
|
|
80
|
+
notionalUsd: { id: "notionalUsd", label: "Notional USD", width: 12, align: "right" },
|
|
81
|
+
receivedAt: { id: "receivedAt", label: "Received UTC", width: 12, align: "left" },
|
|
82
|
+
});
|
|
83
|
+
export const PRINT_COLUMN_IDS: PrintColumnId[] = ["eventAt", "venue", "symbol", "side", "notionalUsd", "price", "priceKind", "quantity", "receivedAt"];
|
|
84
|
+
export const printColumns = (width: number, base: string) => fit(printColumnsFor(base), PRINT_COLUMN_IDS, [["receivedAt"], ["quantity"], ["priceKind"], ["price"]], width);
|
|
85
|
+
export const printColumnLabel = (id: PrintColumnId, base: string) => printColumnsFor(base)[id].label;
|
|
86
|
+
export const printKey = (row: PerpLiquidationPrint) => `${row.marketId}:${row.eventAt}:${row.side}:${row.price}:${row.quantity}`;
|
|
87
|
+
|
|
88
|
+
export function printCellText(row: PerpLiquidationPrint, id: PrintColumnId): string {
|
|
89
|
+
if (id === "eventAt") return stamp(row.eventAt, true);
|
|
90
|
+
if (id === "receivedAt") return row.receivedAt.slice(11, 19);
|
|
91
|
+
if (id === "venue") return venueName(row.venue);
|
|
92
|
+
if (id === "symbol") return row.symbol;
|
|
93
|
+
if (id === "side") return row.side === "long" ? "Long" : "Short";
|
|
94
|
+
if (id === "price") return price(row.price);
|
|
95
|
+
if (id === "priceKind") return row.priceKind === "fill" ? "Fill" : "Bankruptcy";
|
|
96
|
+
if (id === "quantity") return compact(row.quantity);
|
|
97
|
+
return usd(row.notionalUsd);
|
|
98
|
+
}
|
|
99
|
+
export function printCell(row: PerpLiquidationPrint, id: PrintColumnId, colors: Colors): DataTableCell {
|
|
100
|
+
const text = printCellText(row, id);
|
|
101
|
+
const tone = row.side === "long" ? colors.negative : colors.positive;
|
|
102
|
+
if (id === "eventAt") return { text, value: row.eventAt };
|
|
103
|
+
if (id === "receivedAt") return { text, value: row.receivedAt, color: colors.textDim };
|
|
104
|
+
if (id === "venue") return { text, color: colors.textBright };
|
|
105
|
+
if (id === "symbol" || id === "priceKind") return { text, color: colors.textMuted };
|
|
106
|
+
if (id === "side") return { text, color: tone };
|
|
107
|
+
if (id === "price") return { text, value: row.price };
|
|
108
|
+
if (id === "quantity") return { text, value: row.quantity };
|
|
109
|
+
return { text, value: row.notionalUsd, color: tone };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isRecent = (since: string | null, from: string) => !!since && Date.parse(since) > Date.parse(from);
|
|
113
|
+
/** The four totals; a window that reaches back before collection started says when it starts. */
|
|
114
|
+
export function liquidationFigures(data: PerpLiquidationsPayload, colors: Colors): StatItem[] {
|
|
115
|
+
// The date only when collection started on another UTC day, so the detail fits beside every figure.
|
|
116
|
+
const started = data.collectingSince?.slice(0, 10) === data.asOf.slice(0, 10) ? data.collectingSince.slice(11, 16) : stamp(data.collectingSince);
|
|
117
|
+
const since = isRecent(data.collectingSince, data.from) ? `since ${started} UTC` : undefined;
|
|
118
|
+
const day = isRecent(data.collectingSince, new Date(Date.parse(data.asOf) - 86_400_000).toISOString()) ? since : undefined;
|
|
119
|
+
// The tab and the chart's legend say these are liquidated positions; the label names the side and the window.
|
|
120
|
+
const figure = (id: "long24h" | "short24h" | "long7d" | "short7d", label: string, detail?: string): StatItem =>
|
|
121
|
+
({ id, label, value: usd(data.totals[id]), color: id.startsWith("long") ? colors.negative : colors.positive, ...(detail ? { detail, detailVariants: [""] } : {}) });
|
|
122
|
+
return [figure("long24h", "Longs 24h", day), figure("short24h", "Shorts 24h", day), figure("long7d", "Longs 7d", since), figure("short7d", "Shorts 7d", since)];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Hourly liquidations as columns: shorts above zero, longs hanging below it, so the two sides of an hour never cover each other. */
|
|
126
|
+
export function liquidationSeries(data: PerpLiquidationsPayload | null | undefined, colors: Pick<ThemeColors, "positive" | "negative">) {
|
|
127
|
+
const hours = data?.hourly ?? [];
|
|
128
|
+
return [
|
|
129
|
+
staticSeries(hours.map((hour) => scalarPoint(new Date(hour.time), hour.short)), { id: "liquidated-shorts", label: "Shorts liquidated", color: colors.positive, style: "columns", calendarSpaced: true }),
|
|
130
|
+
staticSeries(hours.map((hour) => scalarPoint(new Date(hour.time), -hour.long)), { id: "liquidated-longs", label: "Longs liquidated", color: colors.negative, style: "columns", calendarSpaced: true }),
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
/** The hour a print falls in, where the chart's column for it stands. */
|
|
134
|
+
export const printHour = (row: PerpLiquidationPrint) => new Date(Math.floor(Date.parse(row.eventAt) / 3_600_000) * 3_600_000);
|
|
135
|
+
|
|
136
|
+
/** The streams that are down, with when each last reported: `Bybit stream down since 10-26 06:52 UTC`. Empty while every one is live. */
|
|
137
|
+
export function downStreams(data: Pick<PerpLiquidationsPayload, "streams">): string[] {
|
|
138
|
+
return [...data.streams].sort((a, b) => venueOrder(a.venue) - venueOrder(b.venue)).filter((stream) => stream.state !== "live")
|
|
139
|
+
.map((stream) => `${venueName(stream.venue)} stream down${stream.lastOkAt ? ` since ${stamp(stream.lastOkAt)} UTC` : ""}`);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Who the figures come from and when they were counted: the venues' streams, tallied up to the server's time. Not a
|
|
143
|
+
* feed: the tab counts again each minute. A stream that is down is said, since its venue's figures stop growing.
|
|
144
|
+
*/
|
|
145
|
+
export function liquidationFreshness(data: PerpLiquidationsPayload): HeadlessPaneFreshness {
|
|
146
|
+
const venues = venuesIn(data.markets).map(venueName);
|
|
147
|
+
const down = downStreams(data);
|
|
148
|
+
return { ...(venues.length ? { source: venues.join(", ") } : {}), asOf: data.asOf, maxAgeMinutes: 10, ...(down.length ? { note: down.join("; ") } : {}) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---- Depth ----
|
|
152
|
+
|
|
153
|
+
const DEPTH_BANDS = [{ key: "05", label: "0.5%", fraction: 0.005 }, { key: "1", label: "1%", fraction: 0.01 }, { key: "2", label: "2%", fraction: 0.02 }] as const;
|
|
154
|
+
type BandKey = typeof DEPTH_BANDS[number]["key"];
|
|
155
|
+
export type DepthColumnId = "venue" | "symbol" | "mid" | `bid${BandKey}` | `ask${BandKey}` | "reach" | "observedAt";
|
|
156
|
+
const DEPTH_COLUMNS: Record<DepthColumnId, DataTableColumn> = {
|
|
157
|
+
venue: { id: "venue", label: "Venue", width: 11, align: "left" },
|
|
158
|
+
symbol: { id: "symbol", label: "Contract", width: 14, align: "left" },
|
|
159
|
+
mid: { id: "mid", label: "Mid", width: 12, align: "right" },
|
|
160
|
+
bid05: { id: "bid05", label: "Bid 0.5% USD", width: 12, align: "right" },
|
|
161
|
+
ask05: { id: "ask05", label: "Ask 0.5% USD", width: 12, align: "right" },
|
|
162
|
+
bid1: { id: "bid1", label: "Bid 1% USD", width: 10, align: "right" },
|
|
163
|
+
ask1: { id: "ask1", label: "Ask 1% USD", width: 10, align: "right" },
|
|
164
|
+
bid2: { id: "bid2", label: "Bid 2% USD", width: 10, align: "right" },
|
|
165
|
+
ask2: { id: "ask2", label: "Ask 2% USD", width: 10, align: "right" },
|
|
166
|
+
reach: { id: "reach", label: "Book reach %", width: 12, align: "right" },
|
|
167
|
+
observedAt: { id: "observedAt", label: "Observed UTC", width: 17, align: "left" },
|
|
168
|
+
};
|
|
169
|
+
export const DEPTH_COLUMN_IDS: DepthColumnId[] = ["venue", "symbol", "mid", "bid05", "ask05", "bid1", "ask1", "bid2", "ask2", "reach", "observedAt"];
|
|
170
|
+
export const depthColumns = (width: number) => fit(DEPTH_COLUMNS, DEPTH_COLUMN_IDS, [["reach"], ["mid"], ["observedAt"], ["bid2", "ask2"], ["bid05", "ask05"]], width);
|
|
171
|
+
export const depthColumnLabel = (id: DepthColumnId) => DEPTH_COLUMNS[id].label;
|
|
172
|
+
|
|
173
|
+
/** A band wider than the book the venue returned holds at least what the book shows: `≥57.58M`. */
|
|
174
|
+
const bandFraction = (id: string) => DEPTH_BANDS.find((band) => id.endsWith(band.key))!.fraction;
|
|
175
|
+
const isFloor = (row: Pick<PerpDepthSnapshot, "bidReach" | "askReach">, id: `${"bid" | "ask"}${BandKey}`) =>
|
|
176
|
+
bandFraction(id) > (id.startsWith("bid") ? row.bidReach : row.askReach) + 1e-9;
|
|
177
|
+
/** How far the shorter side of the returned book reaches from mid. */
|
|
178
|
+
const bookReach = (row: Pick<PerpDepthSnapshot, "bidReach" | "askReach">) => Math.min(row.bidReach, row.askReach);
|
|
179
|
+
export function depthCellText(row: PerpDepthRow, id: DepthColumnId): string {
|
|
180
|
+
if (id === "venue") return venueName(row.venue);
|
|
181
|
+
if (id === "symbol") return row.symbol;
|
|
182
|
+
if (id === "mid") return price(row.mid);
|
|
183
|
+
if (id === "reach") return `${(bookReach(row) * 100).toFixed(2)}%`;
|
|
184
|
+
// A book not read for 2.5 hours says so, since the band totals leave it out.
|
|
185
|
+
if (id === "observedAt") return `${stamp(row.observedAt)}${row.stale ? " stale" : ""}`;
|
|
186
|
+
return `${isFloor(row, id) ? "≥" : ""}${usd(row[id])}`;
|
|
187
|
+
}
|
|
188
|
+
export function depthCell(row: PerpDepthRow, id: DepthColumnId, colors: Colors): DataTableCell {
|
|
189
|
+
const text = depthCellText(row, id);
|
|
190
|
+
if (id === "venue") return { text, color: row.stale ? colors.textDim : colors.textBright };
|
|
191
|
+
if (id === "symbol") return { text, color: colors.textMuted };
|
|
192
|
+
if (id === "mid") return { text, value: row.mid };
|
|
193
|
+
if (id === "reach") return { text, value: bookReach(row) * 100, color: colors.textMuted };
|
|
194
|
+
if (id === "observedAt") return { text, value: row.observedAt, color: row.stale ? colors.textMuted : colors.textDim };
|
|
195
|
+
return { text, value: row[id], color: row.stale ? colors.textMuted : id.startsWith("bid") ? colors.positive : colors.negative };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Each band's resting bids and asks summed over the current books; `floor` when any of them ends inside the band. A stale
|
|
200
|
+
* book (it left its venue's 50 largest, or the venue has been failing) is listed, marked, and not added to books read now.
|
|
201
|
+
*/
|
|
202
|
+
export function depthBandTotals(all: readonly PerpDepthRow[]) {
|
|
203
|
+
const rows = all.filter((row) => !row.stale);
|
|
204
|
+
if (!rows.length) return [];
|
|
205
|
+
return DEPTH_BANDS.map((band) => ({ key: band.key, label: `±${band.label} of mid`, books: rows.length,
|
|
206
|
+
bids: rows.reduce((sum, row) => sum + row[`bid${band.key}`], 0), asks: rows.reduce((sum, row) => sum + row[`ask${band.key}`], 0),
|
|
207
|
+
floor: rows.some((row) => isFloor(row, `bid${band.key}`) || isFloor(row, `ask${band.key}`)) }));
|
|
208
|
+
}
|
|
209
|
+
export const depthFigures = (rows: readonly PerpDepthRow[]): StatItem[] => depthBandTotals(rows).map(({ key, label, bids, asks, floor }) => ({
|
|
210
|
+
id: `band-${key}`, label, value: `${floor ? "≥" : ""}${usd(bids + asks)}`, detail: `bids ${usd(bids)} · asks ${usd(asks)}`, detailVariants: [`${usd(bids)} / ${usd(asks)}`, ""] }));
|
|
211
|
+
|
|
212
|
+
/** Said under the depth figures when a stale book is left out of them. */
|
|
213
|
+
export const depthStaleNote = (rows: readonly PerpDepthRow[]) => {
|
|
214
|
+
const stale = rows.filter((row) => row.stale).length;
|
|
215
|
+
return stale ? `${stale === 1 ? "One book" : `${stale} books`} not read for 2.5 hours ${stale === 1 ? "is" : "are"} marked stale and left out of the band totals.` : null;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/** One book's hourly bids and asks within 1% of mid, for the chart over the table. */
|
|
219
|
+
export function depthSeries(history: readonly PerpDepthSnapshot[], row: PerpDepthRow | null, colors: Pick<ThemeColors, "positive" | "negative">) {
|
|
220
|
+
if (!row) return [];
|
|
221
|
+
const points = history.filter((point) => point.marketId === row.marketId).sort((a, b) => a.bucket.localeCompare(b.bucket));
|
|
222
|
+
const name = `${venueName(row.venue)} ${row.symbol}`;
|
|
223
|
+
return [
|
|
224
|
+
staticSeries(points.map((point) => scalarPoint(new Date(point.bucket), point.bid1)), { id: "depth-bids", label: `${name} bids 1%`, color: colors.positive, calendarSpaced: true }),
|
|
225
|
+
staticSeries(points.map((point) => scalarPoint(new Date(point.bucket), point.ask1)), { id: "depth-asks", label: "asks 1%", color: colors.negative, calendarSpaced: true }),
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
export const depthHistoryCount = (history: readonly PerpDepthSnapshot[], marketId: string | undefined) => history.filter((point) => point.marketId === marketId).length;
|
|
229
|
+
|
|
230
|
+
/** Each venue's newest book: what the footer, the report and a capture are dated by. Stale after 2.5 hours, as the server marks a row. */
|
|
231
|
+
export function depthFreshness(data: Pick<PerpDepthPayload, "rows">): HeadlessPaneFreshness {
|
|
232
|
+
const venues = venuesIn(data.rows);
|
|
233
|
+
const sources = venues.map((venue) => ({ source: venueName(venue), asOf: data.rows.filter((row) => row.venue === venue).map((row) => row.observedAt).sort().at(-1)! }));
|
|
234
|
+
return { ...(sources.length ? { source: sources.map((entry) => entry.source).join(", ") } : {}), sources, ageOnly: true, maxAgeMinutes: 150,
|
|
235
|
+
observations: data.rows.map((row) => ({ observedAt: row.observedAt, stale: row.stale })), note: "order books read hourly" };
|
|
236
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
|
-
import type { PerpBoardPayload, PerpBoardQuery, PerpBoardRow, PerpEquitiesPayload, PerpEquityListing, PerpHistoryPayload, PerpHistoryQuery,
|
|
3
|
-
PerpLongShortRatio, PerpMarketPayload, PerpRankingsPayload } from "../../../api-client/perps";
|
|
2
|
+
import type { PerpBoardPayload, PerpBoardQuery, PerpBoardRow, PerpDepthPayload, PerpDepthSnapshot, PerpEquitiesPayload, PerpEquityListing, PerpHistoryPayload, PerpHistoryQuery,
|
|
3
|
+
PerpLiquidationsPayload, PerpLongShortPoint, PerpLongShortRatio, PerpMarketPayload, PerpRankingsPayload } from "../../../api-client/perps";
|
|
4
4
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
5
5
|
import { cachedCloudResource, loadCloudResource } from "../shared/cloud-resource";
|
|
6
6
|
import { baseOfLabel, marketLabel, perpBaseCandidates, separatedPairBase } from "./model";
|
|
@@ -97,6 +97,60 @@ export const loadPerpsRankings = (access: string, force = false) => loadCloudRes
|
|
|
97
97
|
const compareKey = (baseAsset: string, access: string) => `compare:v1:${baseAsset.toUpperCase()}:${access}`;
|
|
98
98
|
export const cachedPerpsCompare = (baseAsset: string, access: string) => cachedCloudResource(perpsCache, compareKey(baseAsset, access), (data) => validatePerpsCompare(data, baseAsset));
|
|
99
99
|
export const loadPerpsCompare = (baseAsset: string, access: string, force = false) => loadCloudResource(perpsCache, compareKey(baseAsset, access), () => fetchPerpsCompare(baseAsset), { force, validate: (data) => validatePerpsCompare(data, baseAsset) });
|
|
100
|
+
// Liquidations and order book depth of one base asset. Older servers answer 404, which the tab shows as its error.
|
|
101
|
+
const finite = (n: unknown) => typeof n === "number" && Number.isFinite(n);
|
|
102
|
+
const nonNegative = (n: unknown) => finite(n) && (n as number) >= 0;
|
|
103
|
+
const strings = (value: unknown) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
104
|
+
const cascadeEnvelope = (data: { access: string; locked: boolean; asOf: string; baseAsset: string; notes: unknown }) =>
|
|
105
|
+
!!data && ["pro", "preview"].includes(data.access) && typeof data.locked === "boolean" && date(data.asOf) && typeof data.baseAsset === "string" && strings(data.notes);
|
|
106
|
+
const named = (row: { marketId: string; venue: string; symbol: string }) => !!row && typeof row.marketId === "string" && typeof row.venue === "string" && typeof row.symbol === "string";
|
|
107
|
+
const TOTALS = ["long24h", "short24h", "long7d", "short7d", "count24h", "count7d"] as const;
|
|
108
|
+
export function validatePerpsLiquidations(data: PerpLiquidationsPayload): PerpLiquidationsPayload {
|
|
109
|
+
if (!cascadeEnvelope(data) || !date(data.from) || !(data.collectingSince === null || date(data.collectingSince)) || !data.totals || !TOTALS.every((key) => nonNegative(data.totals[key]))
|
|
110
|
+
|| !Array.isArray(data.markets) || !data.markets.every((row) => named(row) && TOTALS.every((key) => nonNegative(row[key])) && (row.lastEventAt === null || date(row.lastEventAt)))
|
|
111
|
+
|| new Set(data.markets.map((row) => row.marketId)).size !== data.markets.length
|
|
112
|
+
|| !Array.isArray(data.hourly) || !data.hourly.every((hour) => !!hour && date(hour.time) && nonNegative(hour.long) && nonNegative(hour.short) && nonNegative(hour.count))
|
|
113
|
+
|| !Array.isArray(data.largest) || !data.largest.every((print) => named(print) && (print.side === "long" || print.side === "short") && (print.priceKind === "fill" || print.priceKind === "bankruptcy")
|
|
114
|
+
&& [print.price, print.quantity, print.notionalUsd].every(nonNegative) && date(print.eventAt) && date(print.receivedAt))
|
|
115
|
+
|| !Array.isArray(data.streams) || !data.streams.every((stream) => !!stream && typeof stream.venue === "string" && (stream.state === "live" || stream.state === "down")))
|
|
116
|
+
throw new Error("The server returned unreadable perpetual liquidations");
|
|
117
|
+
return data;
|
|
118
|
+
}
|
|
119
|
+
const BANDS = ["bid05", "ask05", "bid1", "ask1", "bid2", "ask2"] as const;
|
|
120
|
+
const validSnapshot = (row: PerpDepthSnapshot) => !!row && typeof row.marketId === "string" && date(row.bucket) && date(row.observedAt) && (row.sourceAsOf === null || date(row.sourceAsOf))
|
|
121
|
+
&& finite(row.mid) && row.mid > 0 && BANDS.every((band) => nonNegative(row[band])) && nonNegative(row.bidReach) && nonNegative(row.askReach) && finite(row.levels) && (row.grouping === null || finite(row.grouping));
|
|
122
|
+
export function validatePerpsDepth(data: PerpDepthPayload): PerpDepthPayload {
|
|
123
|
+
if (!cascadeEnvelope(data) || !Array.isArray(data.rows) || !data.rows.every((row) => validSnapshot(row) && named(row) && typeof row.stale === "boolean")
|
|
124
|
+
|| new Set(data.rows.map((row) => row.marketId)).size !== data.rows.length
|
|
125
|
+
|| !Array.isArray(data.history) || !data.history.every(validSnapshot) || !Array.isArray(data.sources)) throw new Error("The server returned unreadable order book depth");
|
|
126
|
+
return data;
|
|
127
|
+
}
|
|
128
|
+
/** A name the venues do not list as typed (ETH-USD, bitcoin) is read as the base asset it stands for, as Compare reads it. */
|
|
129
|
+
async function byBaseAsset<T extends { markets?: unknown[]; rows?: unknown[] }>(baseAsset: string, fetch: (base: string) => Promise<T>): Promise<T> {
|
|
130
|
+
const listed = (data: T) => (data.markets ?? data.rows ?? []).length > 0;
|
|
131
|
+
const direct = await fetch(baseAsset);
|
|
132
|
+
if (listed(direct)) return direct;
|
|
133
|
+
for (const base of perpBaseCandidates(baseAsset).map(baseOfLabel)) {
|
|
134
|
+
const found = await fetch(base);
|
|
135
|
+
if (listed(found)) return found;
|
|
136
|
+
}
|
|
137
|
+
return direct;
|
|
138
|
+
}
|
|
139
|
+
export const fetchPerpsLiquidations = (baseAsset: string, client: Pick<typeof apiClient, "getCloudPerpsLiquidations"> = apiClient) =>
|
|
140
|
+
byBaseAsset(baseAsset, async (base) => validatePerpsLiquidations(await client.getCloudPerpsLiquidations(base)));
|
|
141
|
+
export const fetchPerpsDepth = (baseAsset: string, client: Pick<typeof apiClient, "getCloudPerpsDepth"> = apiClient) =>
|
|
142
|
+
byBaseAsset(baseAsset, async (base) => validatePerpsDepth(await client.getCloudPerpsDepth(base)));
|
|
143
|
+
export const perpsLiquidationsCache = createPluginCache<PerpLiquidationsPayload>({ kind: "perps-liquidations", source: "gloom-cloud", schemaVersion: 1,
|
|
144
|
+
policy: { staleMs: 60_000, expireMs: 86_400_000 } });
|
|
145
|
+
export const perpsDepthCache = createPluginCache<PerpDepthPayload>({ kind: "perps-depth", source: "gloom-cloud", schemaVersion: 1,
|
|
146
|
+
policy: { staleMs: 300_000, expireMs: 86_400_000 } });
|
|
147
|
+
const cascadeKey = (kind: string, baseAsset: string, access: string) => `${kind}:v1:${baseAsset.toUpperCase()}:${access}`;
|
|
148
|
+
export const cachedPerpsLiquidations = (baseAsset: string, access: string) => cachedCloudResource(perpsLiquidationsCache, cascadeKey("liquidations", baseAsset, access), validatePerpsLiquidations);
|
|
149
|
+
export const loadPerpsLiquidations = (baseAsset: string, access: string, force = false) => loadCloudResource(perpsLiquidationsCache, cascadeKey("liquidations", baseAsset, access),
|
|
150
|
+
() => fetchPerpsLiquidations(baseAsset), { force, validate: validatePerpsLiquidations });
|
|
151
|
+
export const cachedPerpsDepth = (baseAsset: string, access: string) => cachedCloudResource(perpsDepthCache, cascadeKey("depth", baseAsset, access), validatePerpsDepth);
|
|
152
|
+
export const loadPerpsDepth = (baseAsset: string, access: string, force = false) => loadCloudResource(perpsDepthCache, cascadeKey("depth", baseAsset, access),
|
|
153
|
+
() => fetchPerpsDepth(baseAsset), { force, validate: validatePerpsDepth });
|
|
100
154
|
const sameListing = (value: PerpEquityListing | null | undefined, listing: PerpEquityListing) =>
|
|
101
155
|
value?.symbol === listing.symbol && value.exchange === listing.exchange;
|
|
102
156
|
/** The server's proof that a stock contract belongs to the listing asked about. */
|
|
@@ -2,7 +2,7 @@ import type { PerpBoardRow } from "../../../api-client/perps";
|
|
|
2
2
|
import type { HeadlessPaneFreshness } from "../../../types/headless";
|
|
3
3
|
import { venueName, venuesOf } from "./model";
|
|
4
4
|
|
|
5
|
-
type Dated = Pick<PerpBoardRow, "venue" | "observedAt">;
|
|
5
|
+
type Dated = Pick<PerpBoardRow, "venue" | "observedAt" | "stale">;
|
|
6
6
|
type Funded = Pick<PerpBoardRow, "venue" | "fundingRate" | "fundingRate8h" | "fundingKind" | "nextFundingAt">;
|
|
7
7
|
|
|
8
8
|
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
|
|
@@ -47,23 +47,25 @@ export function fundingNote(rows: readonly Funded[], now = Date.now()): string |
|
|
|
47
47
|
}).join("; ")}`;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** What a capture reads of a row: when it was observed and whether the venue has stopped answering for it. */
|
|
51
|
+
const perpObservation = (row: Pick<PerpBoardRow, "observedAt" | "stale">) => ({ observedAt: row.observedAt, stale: row.stale });
|
|
52
|
+
|
|
50
53
|
/**
|
|
51
|
-
* How current a view of perpetual markets is: each venue's own newest observation,
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
54
|
+
* How current a view of perpetual markets is: each venue's own newest observation, how old the oldest price shown is,
|
|
55
|
+
* and what the funding figures it shows (`funding`) are. `live` only while every row followed streams from its venue's
|
|
56
|
+
* socket; otherwise no feed word, since a row off the stream is the venue's own reading from the last pass (about once
|
|
57
|
+
* a minute), neither streamed nor held back. Stale after an hour without a read, or for a row whose venue stopped
|
|
58
|
+
* answering. The report, the capture and the pane footer all read this, rows and all, so they end with the same line.
|
|
55
59
|
*/
|
|
56
60
|
export function perpFreshness(rows: readonly Dated[], { funding = [], venue = "all", now = Date.now(), live = false }: {
|
|
57
61
|
funding?: readonly Funded[]; venue?: string; now?: number;
|
|
58
|
-
/** Every row followed moves with its venue's socket: the view is a live feed
|
|
62
|
+
/** Every row followed moves with its venue's socket: the view is a live feed. */
|
|
59
63
|
live?: boolean;
|
|
60
64
|
} = {}): HeadlessPaneFreshness {
|
|
61
65
|
const sources = venuesOf(rows).map((id) => ({ source: venueName(id), asOf: new Date(Math.max(...rows.filter((row) => row.venue === id)
|
|
62
66
|
.map((row) => Date.parse(row.observedAt)))).toISOString() }));
|
|
63
67
|
const source = sources.length ? sources.map((entry) => entry.source).join(", ") : venue !== "all" ? venueName(venue) : null;
|
|
64
68
|
const note = fundingNote(funding, now);
|
|
65
|
-
return { ...(source ? { source } : {}), sources,
|
|
69
|
+
return { ...(source ? { source } : {}), sources, ...(live ? { status: "live" as const } : {}), ageOnly: true, maxAgeMinutes: 60,
|
|
70
|
+
observations: rows.map(perpObservation), ...(note ? { note } : {}) };
|
|
66
71
|
}
|
|
67
|
-
|
|
68
|
-
/** What a capture reads of a row: when it was observed and whether the venue has stopped answering for it. */
|
|
69
|
-
export const perpObservation = (row: Pick<PerpBoardRow, "observedAt" | "stale">) => ({ observedAt: row.observedAt, stale: row.stale });
|