gloomberb 0.16.6 → 0.17.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/package.json +2 -1
- package/src/api-client/chat-pane-link.ts +47 -0
- package/src/api-client/chat.ts +9 -1
- package/src/api-client/data.ts +37 -1
- package/src/api-client/equity-screener.ts +3 -0
- package/src/api-client/europe-energy.ts +91 -0
- package/src/api-client/index.ts +6 -0
- package/src/api-client/letters.ts +41 -0
- package/src/api-client/normalizers.ts +36 -6
- package/src/api-client/organization-tickers.ts +44 -0
- package/src/api-client/request.ts +3 -1
- package/src/api-client/types.ts +27 -1
- package/src/app/runtime/desktop-deeplink.ts +48 -15
- package/src/architecture/core-builtin-allowlist.json +1 -1
- package/src/architecture/dom-cell-metrics-allowlist.json +28 -0
- package/src/ask-gloom/pane-context.ts +23 -19
- package/src/ask-gloom/pane-request.ts +2 -2
- package/src/cli/commands/fred-series.ts +77 -11
- package/src/cli/commands/market.ts +77 -20
- package/src/cli/commands/overview.ts +29 -20
- package/src/cli/commands/ticker.ts +64 -11
- package/src/cli/desktop-pane-shot-view.ts +114 -0
- package/src/cli/desktop-pane-shot.ts +30 -31
- package/src/cli/entry.ts +7 -0
- package/src/cli/export-source.ts +43 -0
- package/src/cli/freshness.ts +18 -3
- package/src/cli/glossary.ts +17 -0
- package/src/cli/history-rows.ts +174 -13
- package/src/cli/known-gaps.ts +14 -3
- package/src/cli/listing-arg.ts +20 -2
- package/src/cli/listing-suggestion.ts +159 -0
- package/src/cli/native-smoke.ts +13 -0
- package/src/cli/not-a-ticker.ts +13 -1
- package/src/cli/pane-functions/capabilities.ts +16 -8
- package/src/cli/pane-functions/dom.ts +28 -13
- package/src/cli/pane-functions/function-help.ts +51 -6
- package/src/cli/pane-functions/headless.ts +26 -7
- package/src/cli/pane-functions/index.ts +46 -3
- package/src/cli/pane-functions/report-notices.ts +6 -3
- package/src/cli/pane-functions/report.ts +16 -2
- package/src/cli/pane-functions/resolver.ts +7 -4
- package/src/cli/report-tables.ts +31 -7
- package/src/cli/result.ts +54 -11
- package/src/cli/symbol-rows.ts +18 -1
- package/src/components/chart/composite/axis-overlays.tsx +2 -0
- package/src/components/chart/composite/composite-chart.tsx +4 -21
- package/src/components/chart/composite/legend.tsx +10 -6
- package/src/components/chart/composite/price-axis-labels.tsx +1 -0
- package/src/components/command-bar/list/model.ts +2 -0
- package/src/components/command-bar/pane-templates/items.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/actions.ts +1 -0
- package/src/components/command-bar/routes/ticker-search/results.ts +3 -1
- package/src/components/command-bar/workflow/ops.ts +8 -97
- package/src/components/command-bar/workflow/pane-reuse.ts +177 -0
- package/src/components/data-table/view.tsx +1 -0
- package/src/components/feed-data-table/stack-view.tsx +18 -42
- package/src/components/layout/missing-pane.tsx +2 -0
- package/src/components/layout/shell/index.tsx +1 -0
- package/src/components/layout/shell/pane/ask.ts +12 -7
- package/src/components/layout/shell/pane/capture.ts +8 -34
- package/src/components/markdown-text.tsx +37 -6
- package/src/components/reader/article-reader.tsx +91 -266
- package/src/components/reader/detail-reader.tsx +210 -0
- package/src/components/reader/reader-text.tsx +239 -0
- package/src/components/table-view-shared.tsx +8 -4
- package/src/components/ticker/badge/list.tsx +15 -4
- package/src/components/ui/choice-dialog.tsx +3 -0
- package/src/components/ui/data-table/types.ts +7 -0
- package/src/components/ui/detail-scroll-body.tsx +31 -4
- package/src/components/ui/display.tsx +46 -1
- package/src/components/ui/icon.tsx +11 -1
- package/src/components/ui/list-view.tsx +3 -1
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +9 -1
- package/src/components/ui/select-button.tsx +3 -0
- package/src/data/config/store/fold-panes.ts +202 -11
- package/src/data/config/store/migrations.ts +13 -1
- package/src/docs/snapshot.json +29 -0
- package/src/i18n/es.ts +14 -0
- package/src/i18n/ja.ts +14 -0
- package/src/i18n/ko.ts +14 -0
- package/src/i18n/zh-cn.ts +14 -0
- package/src/i18n/zh-tw.ts +14 -0
- package/src/layout/desk-keys.ts +7 -0
- package/src/layout/desks.ts +2 -2
- package/src/market-data/market/status.ts +67 -6
- package/src/market-data/quotes/contributions.ts +15 -0
- package/src/market-data/quotes/regular-session-valuation.ts +131 -0
- package/src/market-data/quotes/resolution.ts +4 -0
- package/src/market-data/snapshot-provider.ts +6 -3
- package/src/notifications/app-badge.ts +30 -0
- package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/account-management/form-components.tsx +53 -1
- package/src/plugins/builtin/account-management/keyboard.ts +5 -0
- package/src/plugins/builtin/account-management/model.ts +27 -0
- package/src/plugins/builtin/account-management/pane.tsx +62 -2
- package/src/plugins/builtin/alerts/cli.ts +6 -4
- package/src/plugins/builtin/analytics/index.tsx +1 -1
- package/src/plugins/builtin/awards/pane.tsx +29 -18
- package/src/plugins/builtin/backtest/pane.tsx +2 -1
- package/src/plugins/builtin/bond-calculator/index.tsx +3 -5
- package/src/plugins/builtin/buildout/detail/company.tsx +7 -4
- package/src/plugins/builtin/builtin-plugin-meta.ts +5 -4
- package/src/plugins/builtin/catalysts/model.ts +92 -10
- package/src/plugins/builtin/catalysts/pane.tsx +6 -4
- package/src/plugins/builtin/cds/index.tsx +21 -30
- package/src/plugins/builtin/changelog/index.tsx +11 -12
- package/src/plugins/builtin/chart-composer/headless-schema.ts +2 -0
- package/src/plugins/builtin/chart-composer/headless.ts +87 -5
- package/src/plugins/builtin/chart-composer/pane.tsx +3 -2
- package/src/plugins/builtin/chart-composer/session-history.ts +20 -5
- package/src/plugins/builtin/chat/command-bar.ts +6 -8
- package/src/plugins/builtin/chat/content/composer-runtime.ts +2 -2
- package/src/plugins/builtin/chat/content/composer.tsx +13 -4
- package/src/plugins/builtin/chat/content/footer.tsx +15 -5
- package/src/plugins/builtin/chat/content/handle-prompt.tsx +326 -0
- package/src/plugins/builtin/chat/content/index.tsx +113 -9
- package/src/plugins/builtin/chat/content/layout-metrics.ts +4 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +20 -4
- package/src/plugins/builtin/chat/content/scroll.ts +6 -1
- package/src/plugins/builtin/chat/content/transcript.tsx +11 -2
- package/src/plugins/builtin/chat/ghost-user.ts +1 -1
- package/src/plugins/builtin/chat/handle-prompt.ts +128 -0
- package/src/plugins/builtin/chat/index.tsx +44 -21
- package/src/plugins/builtin/chat/layout.ts +7 -14
- package/src/plugins/builtin/chat/message/desktop.tsx +59 -11
- package/src/plugins/builtin/chat/message/header.tsx +69 -21
- package/src/plugins/builtin/chat/message/pane-chip.tsx +223 -0
- package/src/plugins/builtin/chat/message/person-icons.tsx +53 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +73 -9
- package/src/plugins/builtin/chat/message/render-state.ts +15 -2
- package/src/plugins/builtin/chat/message/terminal.tsx +33 -8
- package/src/plugins/builtin/chat/message/types.ts +3 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +149 -0
- package/src/plugins/builtin/chat/pane-links/open.ts +29 -0
- package/src/plugins/builtin/chat/pane-links/previews.ts +119 -0
- package/src/plugins/builtin/chat/pane-state.ts +5 -8
- package/src/plugins/builtin/chat/people.ts +202 -0
- package/src/plugins/builtin/chat/profile-popover.ts +18 -2
- package/src/plugins/builtin/chat/status-widget.tsx +3 -8
- package/src/plugins/builtin/chat/unread-badge.ts +65 -0
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +9 -6
- package/src/plugins/builtin/clinical-trials/client.ts +26 -1
- package/src/plugins/builtin/clinical-trials/index.tsx +10 -2
- package/src/plugins/builtin/clinical-trials/model.ts +269 -0
- package/src/plugins/builtin/clinical-trials/pane.tsx +247 -62
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +156 -0
- package/src/plugins/builtin/cloud/askg/attached-pane.tsx +3 -3
- package/src/plugins/builtin/cloud/askg/pane-attachment.ts +3 -3
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -0
- package/src/plugins/builtin/comment-letters/pane.tsx +30 -11
- package/src/plugins/builtin/commodities-energy/index.tsx +179 -0
- package/src/plugins/builtin/commodity-balances/client.ts +208 -0
- package/src/plugins/builtin/commodity-balances/headless.ts +66 -0
- package/src/plugins/builtin/commodity-balances/index.tsx +32 -0
- package/src/plugins/builtin/commodity-balances/model.ts +261 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +227 -0
- package/src/plugins/builtin/composite-plugins.ts +26 -1
- package/src/plugins/builtin/credit-boards/index.tsx +21 -42
- package/src/plugins/builtin/credit-boards/model.ts +0 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +10 -19
- package/src/plugins/builtin/credit-documents/index.tsx +27 -10
- package/src/plugins/builtin/debt-maturities/index.tsx +3 -17
- package/src/plugins/builtin/debug/index.tsx +2 -2
- package/src/plugins/builtin/dividend-yield/client.ts +3 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +3 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +3 -1
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/docs/blocks.tsx +4 -3
- package/src/plugins/builtin/docs/pane.tsx +16 -8
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +2 -12
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +36 -2
- package/src/plugins/builtin/equity-screener/model.ts +121 -68
- package/src/plugins/builtin/executives/pane.tsx +3 -1
- package/src/plugins/builtin/filing-events/index.tsx +20 -0
- package/src/plugins/builtin/fixed-income/index.tsx +46 -0
- package/src/plugins/builtin/fixed-income/model.ts +27 -0
- package/src/plugins/builtin/fixed-income/pane.tsx +162 -0
- package/src/plugins/builtin/fixed-income/shortcut.ts +52 -0
- package/src/plugins/builtin/futures/index.tsx +24 -7
- package/src/plugins/builtin/futures/series.ts +23 -2
- package/src/plugins/builtin/futures/use-front-returns.ts +3 -2
- package/src/plugins/builtin/futures-curve/pane.tsx +6 -4
- package/src/plugins/builtin/help/shortcut-table.tsx +4 -2
- package/src/plugins/builtin/insider/detail.tsx +20 -12
- package/src/plugins/builtin/insider/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/index.tsx +3 -0
- package/src/plugins/builtin/iv-history/screen-pane.tsx +13 -5
- package/src/plugins/builtin/jodi/client.ts +220 -0
- package/src/plugins/builtin/jodi/headless.ts +63 -0
- package/src/plugins/builtin/jodi/index.tsx +32 -0
- package/src/plugins/builtin/jodi/model.ts +75 -0
- package/src/plugins/builtin/jodi/pane.tsx +141 -0
- package/src/plugins/builtin/market-heatmap/evidence.ts +13 -4
- package/src/plugins/builtin/market-heatmap/headless.ts +3 -3
- package/src/plugins/builtin/market-heatmap/index.tsx +41 -23
- package/src/plugins/builtin/market-heatmap/live.ts +2 -1
- package/src/plugins/builtin/market-heatmap/portfolio.ts +67 -21
- package/src/plugins/builtin/market-movers/table.ts +2 -2
- package/src/plugins/builtin/market-valuation/align.ts +10 -9
- package/src/plugins/builtin/mna/distress/model.ts +52 -1
- package/src/plugins/builtin/mna/distress/pane.tsx +17 -12
- package/src/plugins/builtin/mna/index.tsx +2 -0
- package/src/plugins/builtin/news/wire/index.tsx +2 -2
- package/src/plugins/builtin/news/wire/industry-pane.tsx +28 -14
- package/src/plugins/builtin/news/wire/letters/client.ts +108 -0
- package/src/plugins/builtin/news/wire/letters/model.ts +69 -0
- package/src/plugins/builtin/news/wire/letters/pane.tsx +155 -0
- package/src/plugins/builtin/news/wire/mutes.ts +2 -1
- package/src/plugins/builtin/news/wire/news/query-presets.ts +4 -1
- package/src/plugins/builtin/openfda/client.ts +123 -15
- package/src/plugins/builtin/openfda/model.ts +163 -0
- package/src/plugins/builtin/openfda/pane.tsx +109 -65
- package/src/plugins/builtin/openfda/types.ts +25 -0
- package/src/plugins/builtin/options/view.tsx +1 -1
- package/src/plugins/builtin/pe-band/model.ts +3 -2
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
- package/src/plugins/builtin/plugin-module.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +1 -1
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +19 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -2
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +39 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +18 -5
- package/src/plugins/builtin/portfolio-list/live-valuation.ts +25 -13
- package/src/plugins/builtin/power/evidence.ts +32 -3
- package/src/plugins/builtin/power/gas-client.ts +110 -0
- package/src/plugins/builtin/power/gas-model.ts +194 -0
- package/src/plugins/builtin/power/gas.tsx +145 -0
- package/src/plugins/builtin/power/headless.ts +4 -1
- package/src/plugins/builtin/power/index.tsx +19 -9
- package/src/plugins/builtin/power/pane.tsx +2 -3
- package/src/plugins/builtin/power/prices-headless.ts +75 -7
- package/src/plugins/builtin/power/prices.tsx +36 -13
- package/src/plugins/builtin/primary-dealers/client.ts +108 -0
- package/src/plugins/builtin/primary-dealers/headless.ts +53 -0
- package/src/plugins/builtin/primary-dealers/index.tsx +17 -0
- package/src/plugins/builtin/primary-dealers/model.ts +206 -0
- package/src/plugins/builtin/primary-dealers/pane.tsx +149 -0
- package/src/plugins/builtin/relative-rotation/index.tsx +3 -0
- package/src/plugins/builtin/research/analyst-model.ts +9 -5
- package/src/plugins/builtin/research/headless-schema.ts +2 -1
- package/src/plugins/builtin/research/relative-valuation-headless.ts +68 -32
- package/src/plugins/builtin/research/relative-valuation-model.ts +45 -24
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +39 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +8 -1
- package/src/plugins/builtin/sec/filing-display.tsx +3 -36
- package/src/plugins/builtin/sec/index.tsx +131 -43
- package/src/plugins/builtin/sec/model.ts +148 -2
- package/src/plugins/builtin/sec/row-summaries.ts +105 -0
- package/src/plugins/builtin/sectors/index.tsx +2 -0
- package/src/plugins/builtin/sectors/sector-model.ts +2 -2
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
- package/src/plugins/builtin/shared/use-quote-board.ts +179 -43
- package/src/plugins/builtin/shared/wall-teaser.tsx +2 -2
- package/src/plugins/builtin/shared/zip-text.ts +46 -0
- package/src/plugins/builtin/short-interest/watch-model.ts +11 -2
- package/src/plugins/builtin/themes/model.ts +2 -2
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +11 -0
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +81 -19
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/headless.ts +2 -2
- package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +7 -5
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -6
- package/src/plugins/builtin/time-sales/headless.ts +22 -0
- package/src/plugins/builtin/trace-bonds/client.ts +196 -0
- package/src/plugins/builtin/trace-bonds/headless.ts +69 -0
- package/src/plugins/builtin/trace-bonds/index.tsx +33 -0
- package/src/plugins/builtin/trace-bonds/model.ts +115 -0
- package/src/plugins/builtin/trace-bonds/pane.tsx +191 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +33 -47
- package/src/plugins/builtin/treasury-daily/client.ts +96 -0
- package/src/plugins/builtin/treasury-daily/headless.ts +69 -0
- package/src/plugins/builtin/treasury-daily/index.tsx +17 -0
- package/src/plugins/builtin/treasury-daily/model.ts +69 -0
- package/src/plugins/builtin/treasury-daily/pane.tsx +140 -0
- package/src/plugins/builtin/world-venue-map/entity-detail.tsx +15 -2
- package/src/plugins/builtin/world-venue-map/map.tsx +1 -0
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/catalog-browser.ts +25 -2
- package/src/plugins/marketplace/model.ts +57 -21
- package/src/renderers/dom/build-assets.ts +14 -5
- package/src/renderers/dom/data-table/index.tsx +5 -3
- package/src/renderers/dom/desktop/controls.tsx +2 -1
- package/src/renderers/dom/desktop/icons.tsx +25 -0
- package/src/renderers/dom/desktop/list-view.tsx +1 -1
- package/src/renderers/dom/desktop/menu.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +3 -1
- package/src/renderers/dom/host/chart-surface.tsx +7 -2
- package/src/renderers/dom/host/resize-rows.ts +111 -0
- package/src/renderers/dom/host/sticky-scroll.ts +22 -3
- package/src/renderers/dom/host/style.ts +14 -0
- package/src/renderers/dom/host/surface-3d.tsx +2 -1
- package/src/renderers/dom/host/surface-fit.ts +10 -6
- package/src/renderers/dom/host/text.tsx +2 -2
- package/src/renderers/dom/styles.css +63 -6
- package/src/renderers/electrobun/bun/desktop/app-badge.ts +37 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +37 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -40
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +41 -0
- package/src/renderers/electrobun/bun/tui-entry.ts +18 -1
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/app-badge-host.ts +17 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +8 -3
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +5 -1
- package/src/series/validate.ts +1 -1
- package/src/shares/api.ts +5 -1
- package/src/shares/location.ts +31 -0
- package/src/shares/open.ts +26 -0
- package/src/shares/pane-image.ts +18 -6
- package/src/shares/personal-pane.ts +42 -54
- package/src/sources/gloomberb-cloud/index.ts +7 -2
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -0
- package/src/sources/history-coverage.ts +22 -4
- package/src/sources/history-result.ts +3 -1
- package/src/sources/provider-errors.ts +2 -1
- package/src/sources/provider-router/batches.ts +18 -2
- package/src/sources/provider-router/index.ts +2 -1
- package/src/state/app/context/index.tsx +3 -2
- package/src/test-support/builtin-panes.ts +31 -0
- package/src/tickers/listing-venues.ts +22 -0
- package/src/tickers/search/index.ts +7 -2
- package/src/tickers/search/ranking.ts +19 -0
- package/src/time-series/chart-data.ts +2 -0
- package/src/time-series/parsed-history-cache.ts +41 -11
- package/src/time-series/resolve.ts +47 -15
- package/src/types/config.ts +1 -1
- package/src/types/data-provider.ts +13 -1
- package/src/types/financials.ts +13 -0
- package/src/types/headless.ts +22 -0
- package/src/types/plugin.ts +33 -6
- package/src/types/price-history.ts +2 -0
- package/src/ui/host.tsx +6 -0
- package/src/utils/crypto-pair.ts +1 -1
- package/src/utils/currency-pair.ts +66 -0
- package/src/utils/decimal-comma.ts +57 -0
- package/src/utils/guards.ts +25 -0
- package/src/utils/local-utc-offset.ts +37 -0
- package/src/utils/plugin-proxy-hosts.json +8 -1
- package/src/components/chart/composite/crosshair-readout.tsx +0 -150
- package/src/plugins/builtin/chat/message/ai-badge.tsx +0 -18
- package/src/plugins/builtin/credit-boards/pane.tsx +0 -30
|
@@ -34,6 +34,7 @@ import { futuresContractLines, futuresSettleText, isFuturesInstrument, loadFutur
|
|
|
34
34
|
import type { AppConfig } from "../../types/config";
|
|
35
35
|
import type { FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
|
|
36
36
|
import { quoteTradingDay } from "../../market-data/quotes/day-range";
|
|
37
|
+
import { atRegularSessionPrice } from "../../market-data/quotes/regular-session-valuation";
|
|
37
38
|
import { computeTickerPriceReturns } from "../../market-data/ticker-price-returns";
|
|
38
39
|
import { instrumentTypeLabel, isCryptoVenuePerpetual, resolveTickerInstrumentKind } from "../../tickers/instrument-kind";
|
|
39
40
|
import type { SecFilingItem } from "../../types/data-provider";
|
|
@@ -324,6 +325,8 @@ interface FundamentalsMetric {
|
|
|
324
325
|
text: string;
|
|
325
326
|
value?: number | string | null;
|
|
326
327
|
unit?: string;
|
|
328
|
+
/** A figure the text prints inside another line, which CSV gives a row of its own. */
|
|
329
|
+
exportOnly?: boolean;
|
|
327
330
|
}
|
|
328
331
|
|
|
329
332
|
type MetricFigure = Omit<FundamentalsMetric, "label">;
|
|
@@ -395,7 +398,7 @@ function fundamentalsMetrics(
|
|
|
395
398
|
}
|
|
396
399
|
|
|
397
400
|
function metricLines(metrics: readonly FundamentalsMetric[]): Array<[string, string]> {
|
|
398
|
-
return metrics.map((metric) => [metric.label, metric.text]);
|
|
401
|
+
return metrics.filter((metric) => !metric.exportOnly).map((metric) => [metric.label, metric.text]);
|
|
399
402
|
}
|
|
400
403
|
|
|
401
404
|
const TRAILING_MONEY_METRICS = new Set(["EPS (TTM)", "Revenue (TTM)", "Net Income (TTM)", "Operating Cash Flow (TTM)", "Free Cash Flow (TTM)"]);
|
|
@@ -437,8 +440,53 @@ function fundFactsNote(
|
|
|
437
440
|
return !exchange || isUsListingExchange(exchange) ? `${note}; try fn SEC ${symbol}.` : `${note}.`;
|
|
438
441
|
}
|
|
439
442
|
|
|
440
|
-
/**
|
|
441
|
-
|
|
443
|
+
/**
|
|
444
|
+
* The price a valuation is measured at, as `quote` and `ticker` read it: Last
|
|
445
|
+
* and Change are the regular session (after the close, its close and that
|
|
446
|
+
* close's move), and a pre-market, after-hours or overnight print is a line
|
|
447
|
+
* of its own, measured from that close.
|
|
448
|
+
*/
|
|
449
|
+
function valuationPriceMetrics(financials: FundamentalsReportData): FundamentalsMetric[] {
|
|
450
|
+
const quote = financials.quote;
|
|
451
|
+
const session = getRegularSessionDisplay(quote);
|
|
452
|
+
if (!quote || !session) return [];
|
|
453
|
+
const options = withCurrencyMinorDigits(quoteFormatOptions(quote, undefined, financials.quoteMetadata?.instrumentType), quote.currency);
|
|
454
|
+
const price = (value: number) => formatMarketPriceWithCurrency(value, quote.currency, options);
|
|
455
|
+
const extended = getExtendedSessionDisplay(quote);
|
|
456
|
+
const extendedLabel = extended ? EXTENDED_SESSION_LABELS[extended.session] : "";
|
|
457
|
+
return [
|
|
458
|
+
{ label: "Last", text: price(session.price), value: session.price, unit: quote.currency },
|
|
459
|
+
session.changePercent != null && Number.isFinite(session.changePercent)
|
|
460
|
+
? {
|
|
461
|
+
label: "Change",
|
|
462
|
+
text: colorBySign(`${formatMarketChangeWithCurrency(session.change, quote.currency, options, session.price)} (${formatPercentRaw(session.changePercent)})`, session.change),
|
|
463
|
+
value: session.changePercent,
|
|
464
|
+
unit: "%",
|
|
465
|
+
}
|
|
466
|
+
: { label: "Change", text: "—" },
|
|
467
|
+
...extended ? [
|
|
468
|
+
{
|
|
469
|
+
label: extendedLabel,
|
|
470
|
+
// "mid" when the overnight price is the session's bid and ask midpoint, as `quote` marks it.
|
|
471
|
+
text: colorBySign(`${price(extended.price)}${extended.mark ? " mid" : ""}${extended.changePercent != null ? ` (${formatPercentRaw(extended.changePercent)})` : ""}`, extended.change),
|
|
472
|
+
value: extended.price,
|
|
473
|
+
unit: quote.currency,
|
|
474
|
+
},
|
|
475
|
+
extended.changePercent != null
|
|
476
|
+
? { label: `${extendedLabel} Change`, text: formatPercentRaw(extended.changePercent), value: extended.changePercent, unit: "%", exportOnly: true }
|
|
477
|
+
: { label: `${extendedLabel} Change`, text: "—" },
|
|
478
|
+
] : [],
|
|
479
|
+
];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The lines `fundamentals` or `valuation` reports, the ones with a value.
|
|
484
|
+
* Market cap, the P/E multiples and the dividend yield are measured at the
|
|
485
|
+
* regular session price (atRegularSessionPrice), which `valuation` names in
|
|
486
|
+
* its first lines.
|
|
487
|
+
*/
|
|
488
|
+
function fundamentalsReportMetrics(snapshot: FundamentalsReportData, view: "fundamentals" | "valuation"): FundamentalsMetric[] {
|
|
489
|
+
const financials = atRegularSessionPrice(snapshot);
|
|
442
490
|
const quote = financials.quote;
|
|
443
491
|
const fundamentals = financials.fundamentals;
|
|
444
492
|
const capitalization = selectMarketCapitalization(quote, fundamentals);
|
|
@@ -446,9 +494,10 @@ function fundamentalsReportMetrics(financials: FundamentalsReportData, view: "fu
|
|
|
446
494
|
? { text: `${formatCompact(capitalization.value)} ${capitalization.currency}`, value: capitalization.value, unit: capitalization.currency }
|
|
447
495
|
: { text: "—" };
|
|
448
496
|
const metrics = fundamentalsMetrics(quote, fundamentals, financials.profile, marketCap, computeTickerPriceReturns(financials), fundamentalsCurrency(financials));
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
497
|
+
if (view === "fundamentals") return metrics.filter((metric) => metric.text !== "—");
|
|
498
|
+
const valuation = metrics.filter(({ label, text }) => (VALUATION_METRICS.has(label) || label.startsWith("Dividend Yield")) && text !== "—");
|
|
499
|
+
// A listing with no valuation (a fund) says so rather than printing its price alone.
|
|
500
|
+
return valuation.length === 0 ? [] : [...valuationPriceMetrics(financials).filter((metric) => metric.text !== "—"), ...valuation];
|
|
452
501
|
}
|
|
453
502
|
|
|
454
503
|
/** Text for `gloomberb fundamentals` and `gloomberb valuation`: the ticker report's fundamentals without the rest. */
|
|
@@ -616,7 +665,9 @@ export async function buildTickerReport({
|
|
|
616
665
|
lines.push(cliStyles.muted(membershipParts.join(METADATA_SEPARATOR)));
|
|
617
666
|
}
|
|
618
667
|
|
|
619
|
-
|
|
668
|
+
// Market cap and the multiples are measured at the regular session price the Quote section heads with.
|
|
669
|
+
const priced = atRegularSessionPrice(financials);
|
|
670
|
+
const capitalization = selectMarketCapitalization(priced.quote, priced.fundamentals);
|
|
620
671
|
const convertedMarketCap = capitalization ? await toBase(capitalization.value, capitalization.currency) : Number.NaN;
|
|
621
672
|
const marketCapText = capitalization
|
|
622
673
|
? Number.isFinite(convertedMarketCap)
|
|
@@ -624,7 +675,7 @@ export async function buildTickerReport({
|
|
|
624
675
|
: `${formatCompact(capitalization.value)} ${capitalization.currency}`
|
|
625
676
|
: "—";
|
|
626
677
|
// Shown in the same currency as the market cap, so the two can be compared.
|
|
627
|
-
const enterpriseValue = reportedEnterpriseValue(fundamentals);
|
|
678
|
+
const enterpriseValue = reportedEnterpriseValue(priced.fundamentals);
|
|
628
679
|
const evCurrency = enterpriseValueCurrency(quote, fundamentals);
|
|
629
680
|
// A minor unit such as GBp stays as reported: the converter would read it as the major currency.
|
|
630
681
|
const convertedEnterpriseValue = enterpriseValue != null && evCurrency && /^[A-Z]{3}$/.test(evCurrency)
|
|
@@ -672,7 +723,7 @@ export async function buildTickerReport({
|
|
|
672
723
|
]);
|
|
673
724
|
}
|
|
674
725
|
|
|
675
|
-
const metrics = fundamentalsMetrics(quote, fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials), { text: enterpriseValueText });
|
|
726
|
+
const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials), { text: enterpriseValueText });
|
|
676
727
|
const beforeFundamentals = lines.length;
|
|
677
728
|
appendMetricSection(lines, "Fundamentals", metricLines(metrics));
|
|
678
729
|
for (const note of trailingConversionNotes(fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
|
|
@@ -759,6 +810,8 @@ function buildTickerStructuredData({
|
|
|
759
810
|
const quote = financials.quote;
|
|
760
811
|
const priceReturns = computeTickerPriceReturns(financials, tickerFile?.metadata.assetCategory);
|
|
761
812
|
const identity = listingIdentity(listing, quote);
|
|
813
|
+
// Market cap and the multiples at the regular session price, as the text report gives them.
|
|
814
|
+
const priced = atRegularSessionPrice(financials);
|
|
762
815
|
return {
|
|
763
816
|
symbol,
|
|
764
817
|
listing: { symbol: identity.symbol, exchange: identity.exchange || null, name: identity.name },
|
|
@@ -770,7 +823,7 @@ function buildTickerStructuredData({
|
|
|
770
823
|
...getQuoteSessionFields(quote),
|
|
771
824
|
priceBasis: quote.priceBasis ?? null,
|
|
772
825
|
currency: quote.currency,
|
|
773
|
-
marketCap: quote
|
|
826
|
+
marketCap: priced.quote?.marketCap ?? null,
|
|
774
827
|
volume: quote.volume ?? null,
|
|
775
828
|
exchangeName: quote.exchangeName ?? "",
|
|
776
829
|
fullExchangeName: quote.fullExchangeName ?? "",
|
|
@@ -794,7 +847,7 @@ function buildTickerStructuredData({
|
|
|
794
847
|
positions: tickerFile.metadata.positions,
|
|
795
848
|
} : null,
|
|
796
849
|
fundamentals: financials.fundamentals || priceReturns.return1Y != null || priceReturns.return3Y != null ? exportedFundamentals({
|
|
797
|
-
...
|
|
850
|
+
...priced.fundamentals,
|
|
798
851
|
...priceReturns,
|
|
799
852
|
}) : undefined,
|
|
800
853
|
profile: financials.profile,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { buildViewBundle, electrobunViewPath, readViewStylesheet } from "../renderers/dom/build-assets";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The page `shot` and rendered `fn` reports draw a pane in: the view bundle,
|
|
9
|
+
* the chunks it loads later (the world map's land, the docs) and the
|
|
10
|
+
* stylesheet, by path under the page's root. What changes per capture, the
|
|
11
|
+
* pane and its data, goes in the page's HTML instead (desktop-pane-shot.ts).
|
|
12
|
+
*
|
|
13
|
+
* A source checkout builds these on first use. The release binary has no
|
|
14
|
+
* sources to build from, so scripts/build.ts builds them once, packs them
|
|
15
|
+
* with `packShotView` and embeds the archive, which its entry hands to
|
|
16
|
+
* `usePrebuiltShotView` before the CLI starts. The desktop app ships the same
|
|
17
|
+
* archive alongside its CLI entry rather than embedding it in an executable.
|
|
18
|
+
*/
|
|
19
|
+
export type ShotViewFiles = ReadonlyMap<string, string>;
|
|
20
|
+
|
|
21
|
+
const SHOT_VIEW_ENTRY = "cli-pane-shot-entry.tsx";
|
|
22
|
+
/** The bundle the page loads, named after its entry. */
|
|
23
|
+
export const SHOT_VIEW_SCRIPT = "cli-pane-shot-entry.js";
|
|
24
|
+
export const SHOT_VIEW_STYLESHEET = "styles.css";
|
|
25
|
+
export const SHOT_VIEW_ARCHIVE = "shot-view.json.gz";
|
|
26
|
+
|
|
27
|
+
const MISSING_VIEW_MESSAGE = "This copy of Gloomberb cannot draw panes: it was built without the page they are drawn in. "
|
|
28
|
+
+ "Reinstall or update Gloomberb. The standalone gloomberb binary "
|
|
29
|
+
+ "(https://github.com/gloom-sh/gloomberb/releases/latest) also includes it.";
|
|
30
|
+
|
|
31
|
+
let prebuiltArchive: string | null = null;
|
|
32
|
+
let loading: Promise<ShotViewFiles> | null = null;
|
|
33
|
+
|
|
34
|
+
/** Builds the page's files into `outdir`, for a capture or for scripts/build.ts to embed. */
|
|
35
|
+
export async function buildShotView(outdir: string, apiUrl: string): Promise<void> {
|
|
36
|
+
await buildViewBundle({
|
|
37
|
+
entrypoint: electrobunViewPath(SHOT_VIEW_ENTRY),
|
|
38
|
+
outdir,
|
|
39
|
+
sourcemap: "none",
|
|
40
|
+
// The page has no `process`, so the cloud endpoint is baked in.
|
|
41
|
+
define: { __GLOOMBERB_API_URL__: JSON.stringify(apiUrl) },
|
|
42
|
+
aliasRules: [
|
|
43
|
+
["backend-rpc", "native-stubs/backend-rpc.ts"],
|
|
44
|
+
],
|
|
45
|
+
failureMessage: "Failed to build desktop pane screenshot renderer.",
|
|
46
|
+
missingEntryMessage: "Desktop pane screenshot build did not produce a JavaScript entrypoint.",
|
|
47
|
+
});
|
|
48
|
+
await Bun.write(join(outdir, SHOT_VIEW_STYLESHEET), await readViewStylesheet());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function readShotViewDir(dir: string): Promise<Map<string, string>> {
|
|
52
|
+
const files = new Map<string, string>();
|
|
53
|
+
for await (const path of new Bun.Glob("**/*").scan({ cwd: dir })) {
|
|
54
|
+
files.set(path.replaceAll("\\", "/"), await Bun.file(join(dir, path)).text());
|
|
55
|
+
}
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The files `buildShotView` wrote, as one gzipped JSON object of path to text. */
|
|
60
|
+
export async function packShotView(dir: string): Promise<Uint8Array> {
|
|
61
|
+
const json = JSON.stringify(Object.fromEntries(await readShotViewDir(dir)));
|
|
62
|
+
return Bun.gzipSync(new TextEncoder().encode(json), { level: 9 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Serve the page from the archive at `archivePath` rather than building it.
|
|
67
|
+
* @knipignore Called by the entry scripts/build.ts writes for the release binary.
|
|
68
|
+
*/
|
|
69
|
+
export function usePrebuiltShotView(archivePath: string): void {
|
|
70
|
+
prebuiltArchive = archivePath;
|
|
71
|
+
loading = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The page's files, read or built once per process. */
|
|
75
|
+
export function loadShotView(): Promise<ShotViewFiles> {
|
|
76
|
+
loading ??= (prebuiltArchive ? unpackShotView(prebuiltArchive) : buildShotViewFromSource()).catch((error: unknown) => {
|
|
77
|
+
loading = null;
|
|
78
|
+
throw error;
|
|
79
|
+
});
|
|
80
|
+
return loading;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function unpackShotView(archivePath: string): Promise<ShotViewFiles> {
|
|
84
|
+
const file = Bun.file(archivePath);
|
|
85
|
+
if (!(await file.exists())) throw new Error(MISSING_VIEW_MESSAGE);
|
|
86
|
+
const json = new TextDecoder().decode(Bun.gunzipSync(await file.bytes()));
|
|
87
|
+
return completeShotView(new Map(Object.entries(JSON.parse(json) as Record<string, string>)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function buildShotViewFromSource(): Promise<ShotViewFiles> {
|
|
91
|
+
// A bundle of the CLI without its sources, and without the page embedded.
|
|
92
|
+
if (!existsSync(electrobunViewPath(SHOT_VIEW_ENTRY))) throw new Error(MISSING_VIEW_MESSAGE);
|
|
93
|
+
const dir = await mkdtemp(join(tmpdir(), "gloom-pane-shot-view-"));
|
|
94
|
+
try {
|
|
95
|
+
await buildShotView(dir, process.env.GLOOMBERB_API_URL ?? "");
|
|
96
|
+
return completeShotView(await readShotViewDir(dir));
|
|
97
|
+
} finally {
|
|
98
|
+
await rm(dir, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The page's files, once every file it needs is there: the bundle, the
|
|
104
|
+
* stylesheet, and each chunk the bundle loads by path (`import("./x.chunk.js")`,
|
|
105
|
+
* see build-assets.ts), which a pane would otherwise only miss when it draws.
|
|
106
|
+
*/
|
|
107
|
+
function completeShotView(files: Map<string, string>): ShotViewFiles {
|
|
108
|
+
const script = files.get(SHOT_VIEW_SCRIPT);
|
|
109
|
+
if (script === undefined || !files.has(SHOT_VIEW_STYLESHEET)) throw new Error(MISSING_VIEW_MESSAGE);
|
|
110
|
+
for (const [, chunk] of script.matchAll(/["']\.\/([\w./-]+\.chunk\.js)["']/g)) {
|
|
111
|
+
if (!files.has(chunk!)) throw new Error(MISSING_VIEW_MESSAGE);
|
|
112
|
+
}
|
|
113
|
+
return files;
|
|
114
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TapeCapture } from "../plugins/builtin/time-sales/snapshot-client";
|
|
2
2
|
import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
|
-
import { join
|
|
4
|
+
import { join } from "path";
|
|
5
5
|
import { encodeRpcValue } from "../renderers/electrobun/shared/rpc-codec";
|
|
6
6
|
import type { AppConfig } from "../types/config";
|
|
7
7
|
import type { ChartResolutionResult } from "../time-series/types";
|
|
@@ -21,10 +21,8 @@ import { readVisibleKeyValues } from "./visible-key-values";
|
|
|
21
21
|
import { scrollTruncationReasons } from "./scroll-truncation";
|
|
22
22
|
import { SHOT_API_PROXY_PREFIX, SHOT_HTTP_BRIDGE_PATH, SHOT_MARKET_BRIDGE_PATH } from "./desktop-pane-shot-routes";
|
|
23
23
|
import { SESSION_COOKIE_NAMES } from "../api-client/session-cookie";
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
writeElectrobunViewPage,
|
|
27
|
-
} from "../renderers/dom/build-assets";
|
|
24
|
+
import { renderElectrobunViewHtml } from "../renderers/dom/build-assets";
|
|
25
|
+
import { loadShotView, SHOT_VIEW_SCRIPT, SHOT_VIEW_STYLESHEET, type ShotViewFiles } from "./desktop-pane-shot-view";
|
|
28
26
|
import { errorMessage } from "../utils/errors";
|
|
29
27
|
|
|
30
28
|
export interface DesktopPaneShotIntradayHistory {
|
|
@@ -208,14 +206,12 @@ export async function renderDesktopPaneScreenshot(
|
|
|
208
206
|
apiProxy: DesktopPaneShotApiProxy,
|
|
209
207
|
options: { captureImage?: boolean; bridge?: DesktopPaneShotBridge; statusLine?: ShotStatusLine } = {},
|
|
210
208
|
): Promise<DesktopPaneShotRenderResult> {
|
|
209
|
+
const chrome = await findChromeExecutable();
|
|
210
|
+
const view = await loadShotView();
|
|
211
211
|
const tempDir = await mkdtemp(join(tmpdir(), "gloom-pane-shot-"));
|
|
212
212
|
let server: ReturnType<typeof Bun.serve> | null = null;
|
|
213
213
|
try {
|
|
214
|
-
|
|
215
|
-
await mkdir(outdir, { recursive: true });
|
|
216
|
-
await buildShotPage(outdir, payload);
|
|
217
|
-
server = serveShotPage(outdir, apiProxy, options.bridge ?? null);
|
|
218
|
-
const chrome = await findChromeExecutable();
|
|
214
|
+
server = serveShotPage(view, shotPageHtml(view, payload), apiProxy, options.bridge ?? null);
|
|
219
215
|
return await capturePageScreenshot({
|
|
220
216
|
chrome,
|
|
221
217
|
url: new URL("/", server.url).href,
|
|
@@ -234,16 +230,12 @@ export async function renderDesktopPaneScreenshot(
|
|
|
234
230
|
}
|
|
235
231
|
}
|
|
236
232
|
|
|
237
|
-
|
|
233
|
+
/** The page for one capture: the shared view, with this capture's pane and data in its bootstrap script. */
|
|
234
|
+
function shotPageHtml(view: ShotViewFiles, payload: DesktopPaneShotPayload): string {
|
|
238
235
|
const payloadJson = JSON.stringify(encodeRpcValue(payload)).replace(/</g, "\\u003c");
|
|
239
|
-
return
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
aliasRules: [
|
|
243
|
-
["backend-rpc", "native-stubs/backend-rpc.ts"],
|
|
244
|
-
],
|
|
245
|
-
failureMessage: "Failed to build desktop pane screenshot renderer.",
|
|
246
|
-
missingEntryMessage: "Desktop pane screenshot build did not produce a JavaScript entrypoint.",
|
|
236
|
+
return renderElectrobunViewHtml({
|
|
237
|
+
stylesheet: view.get(SHOT_VIEW_STYLESHEET) ?? "",
|
|
238
|
+
entrySrc: `./${SHOT_VIEW_SCRIPT}`,
|
|
247
239
|
title: "Gloomberb Pane Shot",
|
|
248
240
|
loadingText: "Rendering pane...",
|
|
249
241
|
bootstrapScript: `
|
|
@@ -275,11 +267,11 @@ async function buildShotPage(outdir: string, payload: DesktopPaneShotPayload): P
|
|
|
275
267
|
}
|
|
276
268
|
|
|
277
269
|
function serveShotPage(
|
|
278
|
-
|
|
270
|
+
view: ShotViewFiles,
|
|
271
|
+
html: string,
|
|
279
272
|
apiProxy: DesktopPaneShotApiProxy,
|
|
280
273
|
bridge: DesktopPaneShotBridge | null,
|
|
281
274
|
): ReturnType<typeof Bun.serve> {
|
|
282
|
-
const staticRoot = resolve(outdir);
|
|
283
275
|
return Bun.serve({
|
|
284
276
|
hostname: "127.0.0.1",
|
|
285
277
|
port: 0,
|
|
@@ -295,7 +287,7 @@ function serveShotPage(
|
|
|
295
287
|
if (url.pathname === SHOT_HTTP_BRIDGE_PATH) {
|
|
296
288
|
return runShotHttpBridge(request, bridge);
|
|
297
289
|
}
|
|
298
|
-
return serveShotAsset(url,
|
|
290
|
+
return serveShotAsset(url, view, html);
|
|
299
291
|
},
|
|
300
292
|
});
|
|
301
293
|
}
|
|
@@ -330,21 +322,22 @@ async function runShotHttpBridge(
|
|
|
330
322
|
}
|
|
331
323
|
}
|
|
332
324
|
|
|
333
|
-
|
|
325
|
+
function serveShotAsset(url: URL, view: ShotViewFiles, html: string): Response {
|
|
334
326
|
let pathname: string;
|
|
335
327
|
try {
|
|
336
328
|
pathname = decodeURIComponent(url.pathname);
|
|
337
329
|
} catch {
|
|
338
330
|
return new Response("Bad request", { status: 400 });
|
|
339
331
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (filePath !== staticRoot && !filePath.startsWith(`${staticRoot}${sep}`)) {
|
|
343
|
-
return new Response("Not found", { status: 404 });
|
|
332
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
333
|
+
return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
344
334
|
}
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
return new Response(
|
|
335
|
+
const path = pathname.replace(/^\/+/, "");
|
|
336
|
+
const body = view.get(path);
|
|
337
|
+
if (body === undefined) return new Response("Not found", { status: 404 });
|
|
338
|
+
// A module script is only run when served as JavaScript.
|
|
339
|
+
const type = path.endsWith(".js") ? "text/javascript" : path.endsWith(".css") ? "text/css" : "text/plain";
|
|
340
|
+
return new Response(body, { headers: { "Content-Type": `${type}; charset=utf-8` } });
|
|
348
341
|
}
|
|
349
342
|
|
|
350
343
|
async function proxyShotApiRequest(
|
|
@@ -427,15 +420,21 @@ async function findChromeExecutable(): Promise<string> {
|
|
|
427
420
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
428
421
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
429
422
|
Bun.which("google-chrome"),
|
|
423
|
+
Bun.which("google-chrome-stable"),
|
|
430
424
|
Bun.which("chromium"),
|
|
431
425
|
Bun.which("chromium-browser"),
|
|
426
|
+
// Windows installs Chrome outside PATH, per machine or per user.
|
|
427
|
+
...["PROGRAMFILES", "PROGRAMFILES(X86)", "LOCALAPPDATA"].map((name) => {
|
|
428
|
+
const dir = process.env[name];
|
|
429
|
+
return dir ? join(dir, "Google", "Chrome", "Application", "chrome.exe") : null;
|
|
430
|
+
}),
|
|
432
431
|
].filter((value): value is string => !!value);
|
|
433
432
|
|
|
434
433
|
for (const candidate of candidates) {
|
|
435
434
|
const file = Bun.file(candidate);
|
|
436
435
|
if (await file.exists()) return candidate;
|
|
437
436
|
}
|
|
438
|
-
throw new Error("
|
|
437
|
+
throw new Error("Drawing a pane needs Google Chrome or Chromium, and neither was found. Install one, or set CHROME_PATH to its executable.");
|
|
439
438
|
}
|
|
440
439
|
|
|
441
440
|
async function capturePageScreenshot({
|
package/src/cli/entry.ts
CHANGED
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
OPEN_TUI_NATIVE_SMOKE_COMMAND,
|
|
12
12
|
OPEN_TUI_RUNTIME_SMOKE_COMMAND,
|
|
13
13
|
PLUGIN_HOST_SMOKE_COMMAND,
|
|
14
|
+
SHOT_VIEW_SMOKE_COMMAND,
|
|
14
15
|
smokeOpenTuiNative,
|
|
15
16
|
smokeOpenTuiRuntime,
|
|
16
17
|
smokePluginHost,
|
|
18
|
+
smokeShotView,
|
|
17
19
|
} from "./native-smoke";
|
|
18
20
|
|
|
19
21
|
async function launchOpenTuiApp(cliLaunchRequest: CliLaunchRequest | null = null): Promise<void> {
|
|
@@ -51,6 +53,11 @@ export async function runCliEntrypoint(rawArgs = process.argv.slice(2)): Promise
|
|
|
51
53
|
process.exit(0);
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
if (command === SHOT_VIEW_SMOKE_COMMAND) {
|
|
57
|
+
await smokeShotView();
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
if (!command || ((command === "launch-ui" || command === "ui") && !rawArgs.some(isCliHelpFlag))) {
|
|
55
62
|
await launchOpenTuiApp();
|
|
56
63
|
return;
|
package/src/cli/export-source.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Where exported data comes from, said the same way by every command that
|
|
3
3
|
* writes CSV or JSON, so an export can go in an audit file as it is: CSV ends
|
|
4
4
|
* with `#` lines and JSON carries a `source` object with the same fields.
|
|
5
|
+
* CSV and NDJSON rows also end with five fixed columns naming what each row
|
|
6
|
+
* is (`EXPORT_IDENTITY_COLUMNS`).
|
|
5
7
|
*/
|
|
6
8
|
import { formatFreshnessLine, type ReportFreshness } from "./pane-functions/freshness";
|
|
7
9
|
|
|
@@ -38,6 +40,47 @@ export function exportSource(freshness: ReportFreshness): ExportSource {
|
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The columns every row of an export that ends with these lines carries,
|
|
45
|
+
* last and in this order, so a row says what it is after `head`, a filter or
|
|
46
|
+
* concatenating files: what was asked for, the unit of its values, the bar
|
|
47
|
+
* interval or observation frequency served, and the source and as-of date the
|
|
48
|
+
* closing lines give.
|
|
49
|
+
*/
|
|
50
|
+
export const EXPORT_IDENTITY_COLUMNS = ["symbol_or_series", "currency_or_unit", "interval", "source", "as_of"] as const;
|
|
51
|
+
|
|
52
|
+
/** What a command knows about the rows it exports; source and as-of come from its freshness. */
|
|
53
|
+
export interface ExportIdentity {
|
|
54
|
+
/** The symbol or series ID asked for: `USDZAR=X`, `SAN:EPA`, `DEXSFUS`. */
|
|
55
|
+
symbolOrSeries?: string | null;
|
|
56
|
+
/** The currency or unit the values are in: `ZAR`, `USD`, `bp`, `pct`, `index points`. */
|
|
57
|
+
currencyOrUnit?: string | null;
|
|
58
|
+
/** The bar interval or observation frequency actually returned: `1d`, `1mo`, `daily`, `monthly`. */
|
|
59
|
+
interval?: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The five cells of `EXPORT_IDENTITY_COLUMNS` for a row: blank where the
|
|
64
|
+
* command cannot say, and `as_of` the observation date of the closing lines,
|
|
65
|
+
* blank when they read `not published`.
|
|
66
|
+
*/
|
|
67
|
+
export function exportIdentityCells(freshness: ReportFreshness, identity: ExportIdentity = {}): string[] {
|
|
68
|
+
const source = exportSource(freshness);
|
|
69
|
+
return [
|
|
70
|
+
identity.symbolOrSeries?.trim() ?? "",
|
|
71
|
+
identity.currencyOrUnit?.trim() ?? "",
|
|
72
|
+
identity.interval?.trim() ?? "",
|
|
73
|
+
source.name,
|
|
74
|
+
source.observationDate ?? "",
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The same five as fields of an NDJSON row, null where blank. */
|
|
79
|
+
export function exportIdentityFields(freshness: ReportFreshness, identity: ExportIdentity = {}): Record<string, string | null> {
|
|
80
|
+
const cells = exportIdentityCells(freshness, identity);
|
|
81
|
+
return Object.fromEntries(EXPORT_IDENTITY_COLUMNS.map((column, index) => [column, cells[index] || null]));
|
|
82
|
+
}
|
|
83
|
+
|
|
41
84
|
/**
|
|
42
85
|
* The lines an export ends with: the source line the text report ends with,
|
|
43
86
|
* then the publisher, observation date (the newest; the oldest follows when
|
package/src/cli/freshness.ts
CHANGED
|
@@ -60,9 +60,9 @@ function latestReportedPeriod(financials: Pick<TickerFinancials, "annualStatemen
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* Fundamentals
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* Fundamentals: reported figures, not a feed. Dated by when the statistics
|
|
64
|
+
* block was observed, with the newest statement period they run through
|
|
65
|
+
* named in the status; stale only when the block says so.
|
|
66
66
|
*/
|
|
67
67
|
export function fundamentalsFreshness(financials: TickerFinancials, now = Date.now()): ReportFreshness {
|
|
68
68
|
const fundamentals = financials.fundamentals;
|
|
@@ -73,3 +73,18 @@ export function fundamentalsFreshness(financials: TickerFinancials, now = Date.n
|
|
|
73
73
|
asOf: fundamentals?.fetchedAt ?? null,
|
|
74
74
|
}, { stale: fundamentals?.stale === true }, now)!;
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Valuation: market cap, multiples and yields measured at the quote's regular
|
|
79
|
+
* session price, beside the reported figures they divide. Dated by the quote,
|
|
80
|
+
* as `quote` dates it (after the close, its trading day's close), with the
|
|
81
|
+
* statement period the reported figures run through after the status. Stale
|
|
82
|
+
* fundamentals were measured at some older price, so they date it as
|
|
83
|
+
* `fundamentals` does; so does a missing quote.
|
|
84
|
+
*/
|
|
85
|
+
export function valuationFreshness(financials: TickerFinancials, now = Date.now()): ReportFreshness {
|
|
86
|
+
const quote = financials.quote;
|
|
87
|
+
if (!quote || financials.fundamentals?.stale === true) return fundamentalsFreshness(financials, now);
|
|
88
|
+
const period = latestReportedPeriod(financials);
|
|
89
|
+
return rowsFreshness([quoteObservation(quote, now)], period ? { note: `reported through ${period}` } : undefined, undefined, now)!;
|
|
90
|
+
}
|
package/src/cli/glossary.ts
CHANGED
|
@@ -336,6 +336,23 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
|
|
|
336
336
|
shown: ["Drift"],
|
|
337
337
|
definition: "The plain difference between two percentages. A 22% weight against a 20% target is 2pp over it, not 10%.",
|
|
338
338
|
},
|
|
339
|
+
// Financial statements and ratios (FA).
|
|
340
|
+
{
|
|
341
|
+
term: "EBITDA", aliases: ["earnings before interest taxes depreciation and amortization", "ebitda ttm"], functions: ["FA"],
|
|
342
|
+
definition: "Earnings before interest, taxes, depreciation and amortization: operating income with depreciation and amortization "
|
|
343
|
+
+ "added back, a rough measure of the cash a business earns before paying for its capital and its debt.",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
term: "Net debt", aliases: ["net debt/ebitda", "net debt to ebitda", "net leverage"], functions: ["FA"], shown: ["Net Debt"],
|
|
347
|
+
definition: "Total debt less cash and short-term investments. Negative means more cash than debt. "
|
|
348
|
+
+ "Net debt / EBITDA is how many years of EBITDA would repay it; FA divides by a year of EBITDA, the trailing four quarters in a quarterly column.",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
term: "TTM", aliases: ["trailing twelve months", "trailing 12 months", "trailing four quarters", "ltm", "last twelve months"],
|
|
352
|
+
functions: ["FA"],
|
|
353
|
+
definition: "Trailing twelve months: the four latest quarters added up, a year that ends with the latest quarter rather than the fiscal year. "
|
|
354
|
+
+ "FA's quarterly leverage ratios read their flows over it, and show < 4 quarters when fewer are on file.",
|
|
355
|
+
},
|
|
339
356
|
// Funds (ETF). Named in what ETF says it cannot show yet.
|
|
340
357
|
{
|
|
341
358
|
term: "NAV", aliases: ["net asset value", "nav per share", "net asset value per share"], functions: ["ETF"],
|