@gloom-sh/terminal 0.18.5 → 0.19.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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +163 -8
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/cot.ts +6 -2
- package/src/api-client/credit-documents.ts +6 -0
- package/src/api-client/data.ts +19 -7
- package/src/api-client/equity-screener.ts +24 -0
- package/src/api-client/fund-facts.ts +45 -4
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +15 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/market-discovery.ts +10 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/members.ts +1 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/perps.ts +43 -0
- package/src/api-client/research-activity.ts +3 -2
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +82 -2
- package/src/app/pane-runtime/plugin-bindings.ts +9 -5
- package/src/app/runtime/broker-import.ts +0 -2
- package/src/app/runtime/plugin-bindings.ts +73 -56
- package/src/architecture/core-builtin-allowlist.json +2 -2
- 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/market.ts +107 -38
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/options-chain.ts +48 -7
- package/src/cli/commands/options-leaps.ts +9 -3
- package/src/cli/commands/overview.ts +147 -32
- package/src/cli/commands/search.ts +21 -1
- package/src/cli/commands/themes.ts +11 -2
- package/src/cli/commands/ticker.ts +87 -36
- package/src/cli/company-name-hint.ts +41 -0
- package/src/cli/earnings-calendar.ts +1 -1
- package/src/cli/errors.ts +25 -2
- package/src/cli/glossary.ts +31 -2
- package/src/cli/history-rows.ts +57 -9
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/index.ts +2 -0
- package/src/cli/listing-arg.ts +235 -60
- package/src/cli/output-file.ts +68 -0
- package/src/cli/pane-functions/capabilities.ts +4 -0
- package/src/cli/pane-functions/catalog.ts +80 -7
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/dom.ts +13 -3
- package/src/cli/pane-functions/freshness.ts +93 -4
- package/src/cli/pane-functions/function-help.ts +20 -10
- package/src/cli/pane-functions/headless.ts +17 -7
- package/src/cli/pane-functions/index.ts +54 -16
- package/src/cli/pane-functions/market-session.ts +66 -4
- package/src/cli/pane-functions/options.ts +19 -1
- package/src/cli/pane-functions/resolver.ts +19 -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 +22 -11
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/cli/ticker-resolution.ts +3 -2
- package/src/components/chart/composite/composite-chart.tsx +4 -0
- package/src/components/chart/composite/interactions.ts +6 -0
- package/src/components/chart/composite/legend.tsx +45 -7
- package/src/components/chart/composite/panel-gestures.ts +33 -0
- package/src/components/chart/composite/panel-surface.tsx +52 -3
- package/src/components/chart/composite/rasterizer.ts +24 -13
- package/src/components/chart/composite/scene.ts +39 -4
- 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/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/list/model.ts +5 -0
- package/src/components/command-bar/pane-templates/items.ts +3 -2
- package/src/components/command-bar/routes/root/runtime.ts +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/command-bar/workflow/collection-actions.ts +3 -3
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-wizard.tsx +4 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +12 -7
- 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 +8 -21
- 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 +18 -3
- package/src/components/ui/icon.tsx +4 -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/data/config/chart-settings.ts +1 -1
- package/src/data/json-ticker-repository.ts +17 -8
- package/src/data/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- 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 +103 -142
- package/src/i18n/es.ts +20 -6
- package/src/i18n/ja.ts +20 -6
- package/src/i18n/ko.ts +20 -6
- package/src/i18n/zh-cn.ts +20 -6
- package/src/i18n/zh-tw.ts +20 -6
- package/src/market-data/depositary-pair.ts +27 -3
- package/src/market-data/fund-facts.ts +50 -0
- package/src/market-data/market/change-basis.ts +16 -3
- package/src/market-data/market/format.ts +25 -0
- package/src/market-data/market/freshness.ts +2 -1
- package/src/market-data/options-alternatives.ts +81 -4
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/news/aggregator.ts +119 -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 +17 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +20 -1
- package/src/plugins/builtin/alerts/alert-log.ts +33 -0
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli-forms.ts +216 -0
- package/src/plugins/builtin/alerts/cli.ts +362 -80
- package/src/plugins/builtin/alerts/command.ts +25 -1
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +34 -10
- package/src/plugins/builtin/alerts/fired-log.ts +73 -9
- package/src/plugins/builtin/alerts/format.ts +75 -5
- package/src/plugins/builtin/alerts/headless.ts +34 -12
- package/src/plugins/builtin/alerts/history-rows.ts +3 -2
- package/src/plugins/builtin/alerts/history.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +79 -16
- package/src/plugins/builtin/alerts/metric-command.ts +77 -4
- package/src/plugins/builtin/alerts/metric-data.ts +54 -13
- package/src/plugins/builtin/alerts/metric-model.ts +163 -14
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +61 -23
- package/src/plugins/builtin/alerts/quotes.ts +6 -0
- package/src/plugins/builtin/alerts/storage.ts +12 -8
- package/src/plugins/builtin/alerts/types.ts +4 -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/broker-manager/index.tsx +7 -3
- 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 +6 -2
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/changelog/index.tsx +9 -6
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +7 -0
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -2
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -1
- package/src/plugins/builtin/chart-composer/headless.ts +6 -1
- package/src/plugins/builtin/chart-composer/index.tsx +5 -6
- package/src/plugins/builtin/chart-composer/pane.tsx +37 -8
- package/src/plugins/builtin/chart-composer/settings.ts +27 -0
- package/src/plugins/builtin/chat/content/command-suggestions.ts +1 -0
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/pane-links/commands.ts +7 -1
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -31
- package/src/plugins/builtin/clinical-trials/pane.tsx +61 -49
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +95 -15
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +2 -0
- 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/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/moves.tsx +5 -1
- package/src/plugins/builtin/company-attention/pane.tsx +12 -2
- package/src/plugins/builtin/company-attention/signals.tsx +5 -1
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -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 +10 -10
- 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/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- 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/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/format.ts +31 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +20 -5
- package/src/plugins/builtin/earnings-calls/model.ts +6 -1
- package/src/plugins/builtin/earnings-calls/pane.tsx +12 -4
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +6 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +16 -6
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +39 -9
- 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 +68 -13
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- 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 +6 -3
- package/src/plugins/builtin/fx-matrix/headless.ts +1 -1
- package/src/plugins/builtin/fx-matrix/index.tsx +4 -2
- package/src/plugins/builtin/fx-matrix/live-legs.ts +42 -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 +62 -59
- package/src/plugins/builtin/help/shortcut-model.ts +1 -1
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
- package/src/plugins/builtin/holders/fund-holders.ts +51 -0
- package/src/plugins/builtin/holders/headless.ts +22 -3
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- 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 +33 -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/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- 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 +27 -7
- package/src/plugins/builtin/market-movers/headless.ts +34 -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 +22 -4
- 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 +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +4 -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 +29 -5
- package/src/plugins/builtin/options/enrichment.ts +17 -5
- package/src/plugins/builtin/options/table.ts +5 -2
- package/src/plugins/builtin/options/types.ts +2 -0
- package/src/plugins/builtin/options/view.tsx +17 -4
- package/src/plugins/builtin/options-positioning/client.ts +11 -0
- package/src/plugins/builtin/options-positioning/headless.ts +21 -8
- package/src/plugins/builtin/options-positioning/index.tsx +1 -1
- package/src/plugins/builtin/options-positioning/model.ts +78 -9
- package/src/plugins/builtin/options-positioning/pane.tsx +21 -6
- 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/headless.ts +65 -9
- package/src/plugins/builtin/perps/index.tsx +7 -5
- package/src/plugins/builtin/perps/model.ts +18 -4
- package/src/plugins/builtin/perps/pane.tsx +19 -22
- 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 +7 -5
- package/src/plugins/builtin/portfolio-list/pane/futures-front.ts +40 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -7
- 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/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +5 -2
- package/src/plugins/builtin/portfolio-list/table.tsx +15 -9
- package/src/plugins/builtin/primary-dealers/client.ts +13 -38
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/index.tsx +2 -2
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +11 -9
- 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 +40 -12
- package/src/plugins/builtin/research/headless.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +13 -26
- package/src/plugins/builtin/research/relative-valuation-model.ts +31 -3
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +59 -20
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- 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/risk-factors/data.ts +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- 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 +16 -5
- package/src/plugins/builtin/scanner/live-headless.ts +48 -5
- package/src/plugins/builtin/scanner/live-model.ts +11 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +3 -3
- 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 +4 -2
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +16 -7
- 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/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/job-posting.ts +1 -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 +11 -2
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- 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 +2 -1
- package/src/plugins/builtin/shared/volatility/live-session.ts +34 -8
- package/src/plugins/builtin/shared/volatility/pricing.ts +19 -0
- package/src/plugins/builtin/shared/wall-view-lifecycle.ts +3 -1
- package/src/plugins/builtin/themes/model.ts +16 -7
- package/src/plugins/builtin/themes/pane.tsx +5 -5
- package/src/plugins/builtin/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- 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 +11 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +14 -8
- 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-auctions/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- 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/client.ts +3 -13
- package/src/plugins/builtin/vol-surface/headless.ts +3 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +7 -5
- package/src/plugins/builtin/volatility/index.tsx +2 -2
- package/src/plugins/builtin/volatility/pane.tsx +7 -4
- package/src/plugins/builtin/world-venue-map/headless.ts +3 -3
- package/src/plugins/builtin/yield-curve/client.ts +61 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/catalog-manifest.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/marketplace/model.ts +18 -5
- package/src/portfolio/column-values.ts +11 -7
- package/src/portfolio/futures-positions.ts +6 -3
- package/src/portfolio/position-quantity.ts +15 -9
- package/src/remote/resources.ts +2 -1
- package/src/renderers/dom/data-table/row.tsx +26 -0
- package/src/renderers/dom/desktop/icons.tsx +7 -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 +3 -3
- package/src/renderers/electrobun/bun/desktop/backend.ts +21 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +15 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/update.ts +115 -14
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +52 -0
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/create.ts +43 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/shared/app-name.ts +13 -0
- package/src/renderers/electrobun/shared/protocol.ts +12 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +6 -0
- package/src/renderers/electrobun/view/main.tsx +24 -1
- package/src/renderers/electrobun/view/news-source.ts +5 -1
- package/src/renderers/electrobun/view/remote/ticker-repository.ts +14 -7
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- package/src/renderers/electrobun/view/view-ids.ts +85 -0
- package/src/renderers/opentui/start.tsx +5 -0
- 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/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +43 -8
- package/src/sources/gloomberb-cloud/news.ts +47 -7
- package/src/sources/provider-router/financials.ts +15 -3
- package/src/sources/provider-router/index.ts +8 -5
- package/src/state/ticker-cleanup.ts +50 -0
- package/src/sync/controller.ts +4 -3
- package/src/sync/core-contributors.ts +90 -31
- 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/test-support/preload.ts +14 -0
- package/src/theme/themes.ts +10 -0
- package/src/tickers/company-name-listing.ts +53 -13
- package/src/tickers/company-name.ts +50 -4
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/listing-venues.ts +15 -3
- package/src/tickers/search/index.ts +6 -2
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/terminal-codes.ts +19 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/time-series/alignment.ts +1 -1
- package/src/time-series/economic.ts +18 -4
- package/src/time-series/field-catalog.ts +1 -1
- package/src/time-series/filed-trailing-eps.ts +197 -0
- package/src/time-series/fundamentals.ts +44 -1
- package/src/time-series/ice-credit.ts +3 -0
- package/src/time-series/price-comparison.ts +30 -2
- package/src/time-series/resolve-context.ts +3 -1
- package/src/time-series/resolve.ts +56 -4
- package/src/time-series/session-bar.ts +11 -0
- package/src/time-series/spec.ts +3 -2
- package/src/time-series/studies.ts +8 -2
- package/src/time-series/total-return.ts +160 -0
- package/src/time-series/transforms.ts +2 -3
- package/src/time-series/types.ts +22 -1
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +59 -1
- package/src/types/headless.ts +27 -3
- package/src/types/news-source.ts +1 -0
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +3 -0
- package/src/updater/failure.ts +4 -1
- package/src/utils/cli-output.ts +42 -1
- package/src/utils/crypto-pair.ts +165 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/forward-dividend.ts +2 -0
- package/src/utils/fundamentals.ts +60 -8
- 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/price-history.ts +7 -1
- package/src/utils/shell-word.ts +8 -0
- package/src/utils/trailing-dividend.ts +53 -4
- package/src/version.ts +2 -2
- package/src/plugins/builtin/clinical-trials/ticker-sponsor.ts +0 -17
|
@@ -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. */
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { PerpBoardPayload, PerpBoardRow } from "../../../api-client/perps";
|
|
1
|
+
import type { PerpBoardPayload, PerpBoardRow, PerpDepthRow, PerpLiquidationMarket, PerpLiquidationPrint } from "../../../api-client/perps";
|
|
2
2
|
import type { HeadlessBundleSection, HeadlessPaneColumn, HeadlessPaneLoadArgs, HeadlessPaneRow } from "../../../types/headless";
|
|
3
3
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
4
|
-
import {
|
|
4
|
+
import { DEPTH_COLUMN_IDS, depthBandTotals, depthCellText, depthColumnLabel, depthFreshness, depthStaleNote, downStreams, LIQUIDATION_COLUMN_IDS, liquidationCellText, liquidationColumnLabel, liquidationFreshness,
|
|
5
|
+
PRINT_COLUMN_IDS, printCellText, printColumnLabel, type DepthColumnId, type LiquidationColumnId, type PrintColumnId } from "./cascade";
|
|
6
|
+
import { boardSearchText, fetchPerpSelection, fetchPerpsBoard, fetchPerpsCompare, fetchPerpsDepth, fetchPerpsHistory, fetchPerpsLiquidations, fetchPerpsRankings, marketQuery, nearestPerpMarkets } from "./client";
|
|
5
7
|
import { perpFreshness } from "./freshness";
|
|
6
8
|
import { aprFootnote, ASSET_FILTERS, BOARD_COLUMNS, BOARD_SORTS, collectedVenueCount, COMPARE_COLUMNS, compact, fundingSpread, HISTORY_METRICS, label, marketLabel, openingTab, PERP_TABS, perpCellText,
|
|
7
|
-
perpColumnLabel, percent, perpHistoryProText, positioningFields, price, RANKING_COLUMNS, rankingRows, RANKINGS, ratioText, share, time, VENUES, venueName, type PerpColumnId } from "./model";
|
|
9
|
+
perpColumnLabel, percent, perpHistoryProText, positioningFields, price, RANKING_COLUMNS, rankingRows, RANKINGS, ratioText, share, time, uncoveredVenueNote, VENUES, venueName, IDENTITY_VENUES, type PerpColumnId } from "./model";
|
|
8
10
|
import { freePreviewNote } from "../shared/free-preview";
|
|
9
11
|
import { basisSpotSymbol } from "../futures-curve/model";
|
|
10
12
|
|
|
@@ -71,6 +73,18 @@ const REVISION_COLUMNS: HeadlessPaneColumn[] = [
|
|
|
71
73
|
{ key: "fingerprint", header: "Fingerprint", format: (value) => text(value)?.slice(0, 8) || "--" },
|
|
72
74
|
];
|
|
73
75
|
|
|
76
|
+
// Liquidations and depth print what their tabs' cells show, through the same formatters.
|
|
77
|
+
const LIQUIDATION_RIGHT = new Set<string>(["long24h", "short24h", "long7d", "short7d", "count24h", "notionalUsd", "price", "quantity", "mid", "bid05", "ask05", "bid1", "ask1", "bid2", "ask2", "reach"]);
|
|
78
|
+
const cascadeColumn = <Id extends string>(id: Id, header: string, format: (row: HeadlessPaneRow) => string): HeadlessPaneColumn =>
|
|
79
|
+
({ key: id === "reach" ? "bidReach" : id, header, ...(LIQUIDATION_RIGHT.has(id) ? { align: "right" as const } : {}), format: (_, row) => format(row) });
|
|
80
|
+
const LIQUIDATION_TEXT_COLUMNS = LIQUIDATION_COLUMN_IDS.map((id: LiquidationColumnId) => cascadeColumn(id, liquidationColumnLabel(id), (row) => liquidationCellText(row as unknown as PerpLiquidationMarket, id)));
|
|
81
|
+
const printTextColumns = (base: string) => PRINT_COLUMN_IDS.map((id: PrintColumnId) => cascadeColumn(id, printColumnLabel(id, base), (row) => printCellText(row as unknown as PerpLiquidationPrint, id)));
|
|
82
|
+
const DEPTH_TEXT_COLUMNS = DEPTH_COLUMN_IDS.map((id: DepthColumnId) => cascadeColumn(id, depthColumnLabel(id), (row) => depthCellText(row as unknown as PerpDepthRow, id)));
|
|
83
|
+
const HOURLY_COLUMNS: HeadlessPaneColumn[] = [timeColumn("time", "Hour"), right("long", "Longs USD", (value) => compact(num(value))), right("short", "Shorts USD", (value) => compact(num(value))),
|
|
84
|
+
right("count", "Events", (value) => num(value)?.toLocaleString("en-US") ?? "--")];
|
|
85
|
+
const DEPTH_HISTORY_COLUMNS: HeadlessPaneColumn[] = [{ key: "marketId", header: "Market", format: (value) => text(value) ?? "--" }, timeColumn("bucket", "Hour"),
|
|
86
|
+
...(["bid05", "ask05", "bid1", "ask1", "bid2", "ask2"] as const).map((id) => right(id, depthColumnLabel(id), (value) => compact(num(value))))];
|
|
87
|
+
|
|
74
88
|
// With no market the report shows the first; a market nothing matches names ones that work, a builder dex's among them.
|
|
75
89
|
const EXAMPLE_MARKETS = ["BTC", "ETH", "SOL"] as const;
|
|
76
90
|
const DEFAULT_MARKET = EXAMPLE_MARKETS[0];
|
|
@@ -92,7 +106,7 @@ const tabOf = (args: HeadlessPaneLoadArgs) => PERP_TABS.find((tab) => tab === ar
|
|
|
92
106
|
// A free account's market opens its latest values under the title, so the title names the function, not the Pro history: a table the free view
|
|
93
107
|
// locks is left out and named on the report's one Pro line (`lockedTable`), never marked on the title over the free ones.
|
|
94
108
|
const TITLES: Record<typeof PERP_TABS[number], string> = { board: "Perpetual markets", rankings: "Perpetual rankings", compare: "Perpetual contracts across venues",
|
|
95
|
-
history: "Perpetuals", evidence: "Perpetual evidence" };
|
|
109
|
+
liquidations: "Perpetual liquidations across venues", depth: "Perpetual order book depth across venues", history: "Perpetuals", evidence: "Perpetual evidence" };
|
|
96
110
|
const copy = (rows: readonly PerpBoardRow[]) => rows.map((row) => ({ ...row }));
|
|
97
111
|
const more = (count: number, noun: string) => `${count.toLocaleString("en-US")} more ${noun} need Gloom Pro`;
|
|
98
112
|
// A table the free view leaves empty is locked: the report leaves it out and its one Pro line says what Pro adds.
|
|
@@ -108,7 +122,7 @@ const historyLock = (selected: PerpBoardRow, venues: number | null) => {
|
|
|
108
122
|
const venueNotice = (venue: string, selected: PerpBoardRow, requested: string, access: string) => {
|
|
109
123
|
if (venue === "all" || selected.venue === venue) return [];
|
|
110
124
|
const [wanted, shown] = [venueName(venue), venueName(selected.venue)];
|
|
111
|
-
return [
|
|
125
|
+
return [IDENTITY_VENUES.some((id) => requested.startsWith(`${id}:`)) ? `${requested} is a ${shown} market, so the ${wanted} filter does not apply.`
|
|
112
126
|
: `${wanted} has no ${selected.baseAsset} contract${access === "pro" ? "" : " in the free preview"}; showing ${shown}.`];
|
|
113
127
|
};
|
|
114
128
|
const venueUnused = (venue: string, tab: string) => venue === "all" ? [] : [`The ${venueName(venue)} filter applies to the board, not ${tab}; showing every venue.`];
|
|
@@ -116,17 +130,20 @@ const listMetadata = (data: Pick<PerpBoardPayload, "asOf" | "access" | "status">
|
|
|
116
130
|
|
|
117
131
|
export const perpsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
118
132
|
shape: "bundle", argument: { kind: "free-text", optional: true, placeholder: "market",
|
|
119
|
-
description: "A base asset, underlying, canonical market identity or Market column label, such as BTC, hyperliquid:xyz:TSLA or SP500 · XYZ. Opens its history; with --tab board it searches the board, with --tab compare it is the base asset. Without one, the board." },
|
|
133
|
+
description: "A base asset, underlying, canonical market identity or Market column label, such as BTC, hyperliquid:xyz:TSLA or SP500 · XYZ. Opens its history; with --tab board it searches the board, with --tab compare, liquidations or depth it is the base asset. Without one, the board." },
|
|
120
134
|
options: [
|
|
121
|
-
{ key: "tab", type: "enum", settingKey: "tab", values: PERP_TABS.map((value) => ({ value })), description: "Board, rankings, compare, history or evidence. Board without a market, history with one." },
|
|
135
|
+
{ key: "tab", type: "enum", settingKey: "tab", values: PERP_TABS.map((value) => ({ value })), description: "Board, rankings, compare, liquidations, depth, history or evidence. Board without a market, history with one." },
|
|
122
136
|
{ key: "asset-class", type: "enum", settingKey: "assetClass", values: ASSET_FILTERS.map((value) => ({ value })), defaultValue: "all", description: "Board asset class." },
|
|
123
|
-
{ key: "venue", type: "enum", settingKey: "venue", values: ["all", ...VENUES].map((value) => ({ value })), defaultValue: "all",
|
|
137
|
+
{ key: "venue", type: "enum", settingKey: "venue", values: ["all", ...VENUES].map((value) => ({ value })), defaultValue: "all", invalidValueHint: uncoveredVenueNote,
|
|
138
|
+
description: "Board venue; with a market, opens that venue's contract when the board lists it." },
|
|
124
139
|
{ key: "sort", type: "enum", settingKey: "sort", values: BOARD_SORTS.map((value) => ({ value })), defaultValue: "oi", description: "Board order: open interest, funding, open-interest change, premium or long share of accounts." },
|
|
125
140
|
{ key: "days", type: "enum", settingKey: "days", values: ["1", "7", "30", "90", "365"].map((value) => ({ value })), defaultValue: "7", description: "History lookback in days." },
|
|
126
141
|
{ key: "metric", type: "enum", settingKey: "metric", values: HISTORY_METRICS.map((value) => ({ value })), defaultValue: "funding", description: "Displayed history chart; JSON retains every series." },
|
|
127
142
|
],
|
|
128
143
|
discovery: { aliases: ["PERP"], dataRequirements: ["Perpetual market observations"], limitations: ["Free accounts see a fixed preview of the board, rankings and comparisons, and latest values for one market.", "Pro unlocks every market, full rankings and history.", "Rankings list up to ten markets each.", "Rates, premiums and changes are fractions in JSON.", "Open-interest history begins at first collection.", "Funding APR is a simple annualization, not a forecast.",
|
|
129
|
-
"Long/short account shares: venue definitions differ; history begins at first collection."
|
|
144
|
+
"Long/short account shares: venue definitions differ; history begins at first collection.",
|
|
145
|
+
"Liquidations come from each venue's public stream and begin at first collection: Binance and OKX send at most one a second per contract, Hyperliquid only its vault's backstop liquidations. Free shows totals and the three largest; Pro the hourly series and twenty.",
|
|
146
|
+
"Order book depth is read hourly for each venue's 50 largest markets; a band wider than the book a venue returns is a floor (≥). Free shows the latest; Pro the week's history."] },
|
|
130
147
|
describe: (args) => TITLES[tabOf(args)],
|
|
131
148
|
async load(args, ctx) {
|
|
132
149
|
const requested = requestedOf(args);
|
|
@@ -173,6 +190,45 @@ export const perpsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
173
190
|
...listMetadata(compare), total: compare.total, baseAsset: base,
|
|
174
191
|
...(spread ? { fundingSpread8h: spread.value, fundingSpreadHigh: spread.high.marketId, fundingSpreadLow: spread.low.marketId } : {}) } };
|
|
175
192
|
}
|
|
193
|
+
if (tab === "liquidations" || tab === "depth") {
|
|
194
|
+
// A canonical identity names one contract; its base asset is what the venues share. A pair or plain name reads as its base asset.
|
|
195
|
+
const identity = marketQuery(requested);
|
|
196
|
+
const asked = identity.includes(":") ? (await fetchPerpSelection(identity, ctx.apiClient)).rows[0]?.baseAsset ?? identity : (requested || DEFAULT_MARKET).toUpperCase();
|
|
197
|
+
const notices = (base: string) => [...(requested ? [] : [`Showing ${DEFAULT_MARKET}. Try fn PERP ${EXAMPLE_MARKETS[1]} --tab ${tab}.`]),
|
|
198
|
+
...requested && !identity.includes(":") && base.toUpperCase() !== asked ? readAs(base, requested) : [], ...venueUnused(String(args.options.venue ?? "all"), tab)];
|
|
199
|
+
if (tab === "liquidations") {
|
|
200
|
+
const data = await fetchPerpsLiquidations(asked, ctx.apiClient);
|
|
201
|
+
if (!data.markets.length && data.status !== "unavailable") throw noMarket(requested || asked, await nearestPerpMarkets(requested || asked, ctx.apiClient));
|
|
202
|
+
const told = notices(data.baseAsset);
|
|
203
|
+
const locked = lockedTable(data.locked);
|
|
204
|
+
const entries = (["long24h", "short24h", "long7d", "short7d"] as const).map((key) => ({ key, label: `${key.startsWith("long") ? "Longs" : "Shorts"} liquidated ${key.endsWith("24h") ? "24h" : "7d"}`,
|
|
205
|
+
value: data.totals[key], formatted: `${compact(data.totals[key])} USD` }));
|
|
206
|
+
return { sections: [{ title: "Totals", entries: [...entries, { key: "collectingSince", label: "Collecting since", value: data.collectingSince, formatted: time(data.collectingSince) }] },
|
|
207
|
+
{ title: "By contract", columns: LIQUIDATION_TEXT_COLUMNS, rows: data.markets.map((row) => ({ ...row })) },
|
|
208
|
+
{ title: "Hourly", columns: HOURLY_COLUMNS, rows: data.hourly.map((row) => ({ ...row })), ...locked },
|
|
209
|
+
{ title: "Largest prints", columns: printTextColumns(data.baseAsset), rows: data.largest.map((row) => ({ ...row })) }],
|
|
210
|
+
complete: !data.locked && data.status === "ok" && !downStreams(data).length, notes: data.notes,
|
|
211
|
+
errors: data.locked ? [freePreviewNote({ shows: "liquidation totals and the three largest prints", adds: "the hourly series and the twenty largest prints" })] : [],
|
|
212
|
+
freshness: liquidationFreshness(data),
|
|
213
|
+
metadata: { ...(requested ? {} : { defaultArgument: DEFAULT_MARKET }), ...(told.length ? { notices: told } : {}), baseAsset: data.baseAsset, asOf: data.asOf, from: data.from,
|
|
214
|
+
access: data.access, locked: data.locked, status: data.status, collectingSince: data.collectingSince, streams: data.streams, notionalCurrency: "USD" } };
|
|
215
|
+
}
|
|
216
|
+
const data = await fetchPerpsDepth(asked, ctx.apiClient);
|
|
217
|
+
if (!data.rows.length && data.status !== "unavailable") throw noMarket(requested || asked, await nearestPerpMarkets(requested || asked, ctx.apiClient));
|
|
218
|
+
const told = notices(data.baseAsset);
|
|
219
|
+
const staleNote = depthStaleNote(data.rows);
|
|
220
|
+
// The band totals over the current books, as the tab's figures show them; a floor (≥) when a book ends inside the band.
|
|
221
|
+
const bands = depthBandTotals(data.rows).map((band) => ({ key: `band${band.key}`, label: band.label, value: band.bids + band.asks,
|
|
222
|
+
formatted: `${band.floor ? "≥" : ""}${compact(band.bids + band.asks)} USD (bids ${compact(band.bids)} · asks ${compact(band.asks)}, ${band.books} ${band.books === 1 ? "book" : "books"})` }));
|
|
223
|
+
return { sections: [...bands.length ? [{ title: "Band totals", entries: bands }] : [],
|
|
224
|
+
{ title: "Order books", columns: DEPTH_TEXT_COLUMNS, rows: data.rows.map((row) => ({ ...row })) },
|
|
225
|
+
{ title: "Hourly depth", columns: DEPTH_HISTORY_COLUMNS, rows: data.history.map((row) => ({ ...row })), ...lockedTable(data.locked) }],
|
|
226
|
+
complete: !data.locked && data.status === "ok" && !staleNote, notes: [...staleNote ? [staleNote] : [], ...data.notes],
|
|
227
|
+
errors: data.locked ? [freePreviewNote({ shows: "each book's latest depth", adds: "the week's hourly depth" })] : [],
|
|
228
|
+
freshness: depthFreshness(data),
|
|
229
|
+
metadata: { ...(requested ? {} : { defaultArgument: DEFAULT_MARKET }), ...(told.length ? { notices: told } : {}), baseAsset: data.baseAsset, asOf: data.asOf,
|
|
230
|
+
access: data.access, locked: data.locked, status: data.status, sources: data.sources, notionalCurrency: "USD", reach: "fraction of mid" } };
|
|
231
|
+
}
|
|
176
232
|
const search = requested || DEFAULT_MARKET, venue = String(args.options.venue ?? "all");
|
|
177
233
|
const market = await fetchPerpSelection(search, ctx.apiClient, venue !== "all" ? venue : undefined);
|
|
178
234
|
const selected = market.rows[0];
|