gloomberb 0.18.4 → 0.18.5
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 +32 -1
- package/src/account/auth-model.ts +7 -3
- package/src/api-client/auth.ts +9 -4
- package/src/api-client/catalysts.ts +2 -1
- package/src/api-client/data.ts +14 -0
- package/src/api-client/index.ts +160 -11
- package/src/api-client/live-scan.ts +91 -0
- package/src/api-client/members.ts +12 -0
- package/src/api-client/request.ts +222 -41
- package/src/api-client/research-activity.ts +3 -10
- package/src/api-client/types.ts +6 -0
- package/src/app/keybindings/index.ts +3 -0
- package/src/app/keybindings/labels.ts +18 -1
- package/src/app/keybindings/resolve.ts +14 -0
- package/src/app/runtime/broker-import.ts +43 -26
- package/src/architecture/core-builtin-allowlist.json +1 -1
- package/src/cli/commands/fred-series.ts +5 -0
- package/src/cli/commands/keybindings.ts +23 -0
- package/src/cli/commands/market.ts +68 -22
- package/src/cli/commands/options-chain.ts +59 -3
- package/src/cli/commands/overview.ts +119 -31
- package/src/cli/commands/search.ts +18 -2
- package/src/cli/commands/ticker.ts +2 -1
- package/src/cli/errors.ts +2 -2
- package/src/cli/export-source.ts +29 -1
- package/src/cli/glossary.ts +49 -0
- package/src/cli/history-rows.ts +30 -7
- package/src/cli/known-gaps.ts +5 -2
- package/src/cli/listing-arg.ts +16 -5
- package/src/cli/pane-functions/freshness.ts +25 -13
- package/src/cli/pane-functions/function-help.ts +10 -3
- package/src/cli/pane-functions/headless.ts +39 -5
- package/src/cli/pane-functions/index.ts +10 -3
- package/src/cli/pane-functions/market-session.ts +15 -5
- package/src/cli/pane-functions/report.ts +5 -1
- package/src/cli/pane-functions/resolver.ts +18 -2
- package/src/cli/quote-answer.ts +10 -4
- package/src/cli/report-tables.ts +18 -7
- package/src/cli/restore-plugins.ts +13 -1
- package/src/cli/result.ts +57 -5
- package/src/components/chart/composite/composite-chart.tsx +22 -4
- package/src/components/chart/composite/format.ts +27 -0
- package/src/components/chart/composite/levels.ts +2 -0
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +9 -2
- package/src/components/chart/composite/range-statistics.ts +62 -0
- package/src/components/chart/composite/scene.ts +7 -3
- package/src/components/chart/composite/series-axis-tags.tsx +16 -9
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/panel/prompt-binding.ts +0 -8
- package/src/components/command-bar/workflow/alert.ts +1 -1
- package/src/components/layout/header.tsx +26 -40
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +27 -0
- package/src/components/onboarding/onboarding-frame.tsx +1 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +2 -2
- package/src/components/ui/display.tsx +5 -1
- package/src/core/state/app/layout.ts +2 -0
- package/src/core/state/app/state.ts +9 -0
- package/src/core/state/app/types.ts +3 -0
- package/src/data/saved-copies.ts +55 -16
- package/src/docs/snapshot.json +361 -84
- package/src/i18n/es.ts +1 -1
- package/src/i18n/ja.ts +1 -1
- package/src/i18n/ko.ts +1 -1
- package/src/i18n/zh-cn.ts +1 -1
- package/src/i18n/zh-tw.ts +1 -1
- package/src/market-data/coordinator/financials.ts +10 -2
- package/src/market-data/coordinator/index.ts +3 -1
- package/src/market-data/coordinator/quotes.ts +4 -2
- package/src/market-data/market/change-basis.ts +33 -5
- package/src/market-data/market/dated-previous-close.ts +203 -0
- package/src/market-data/market/futures-week.ts +68 -0
- package/src/market-data/market/status.ts +16 -4
- package/src/market-data/options-alternatives.ts +60 -0
- package/src/market-data/quotes/contributions.ts +20 -2
- package/src/market-data/quotes/resolution.ts +3 -0
- package/src/market-data/selectors.ts +3 -0
- package/src/notifications/notification-log.ts +6 -4
- package/src/plugins/builtin/alerts/alert-engine.ts +5 -1
- package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
- package/src/plugins/builtin/alerts/cli.ts +59 -20
- package/src/plugins/builtin/alerts/command.ts +7 -0
- package/src/plugins/builtin/alerts/constants.ts +1 -1
- package/src/plugins/builtin/alerts/fired-log.ts +45 -4
- package/src/plugins/builtin/alerts/format.ts +8 -2
- package/src/plugins/builtin/alerts/headless.ts +10 -5
- package/src/plugins/builtin/alerts/history-rows.ts +5 -2
- package/src/plugins/builtin/alerts/history.ts +2 -0
- package/src/plugins/builtin/alerts/index.tsx +66 -16
- package/src/plugins/builtin/alerts/live.ts +3 -3
- package/src/plugins/builtin/alerts/metric-command.ts +61 -0
- package/src/plugins/builtin/alerts/metric-data.ts +70 -0
- package/src/plugins/builtin/alerts/metric-model.ts +175 -0
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
- package/src/plugins/builtin/alerts/pane.tsx +42 -29
- package/src/plugins/builtin/alerts/storage.ts +33 -10
- package/src/plugins/builtin/alerts/types.ts +3 -0
- package/src/plugins/builtin/backtest/headless.ts +10 -5
- package/src/plugins/builtin/backtest/pane.tsx +9 -4
- package/src/plugins/builtin/backtest/presets.ts +29 -3
- package/src/plugins/builtin/broker-manager/index.tsx +3 -2
- package/src/plugins/builtin/broker-manager/model.ts +6 -1
- package/src/plugins/builtin/broker-trading/controller.ts +22 -12
- package/src/plugins/builtin/broker-trading/index.tsx +13 -1
- package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
- package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
- package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
- package/src/plugins/builtin/catalysts/model.ts +2 -1
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -1
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
- package/src/plugins/builtin/chart-composer/cli-options.ts +5 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +5 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
- package/src/plugins/builtin/chart-composer/formula.ts +71 -18
- package/src/plugins/builtin/chart-composer/headless-schema.ts +14 -3
- package/src/plugins/builtin/chart-composer/headless.ts +13 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +17 -2
- package/src/plugins/builtin/chart-composer/presets.ts +50 -15
- package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
- package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
- package/src/plugins/builtin/chat/content/command-suggestions.ts +92 -0
- package/src/plugins/builtin/chat/content/composer.tsx +13 -6
- package/src/plugins/builtin/chat/content/footer.tsx +6 -3
- package/src/plugins/builtin/chat/content/index.tsx +50 -21
- package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
- package/src/plugins/builtin/chat/index.tsx +23 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
- package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
- package/src/plugins/builtin/chat/pane-links/commands.ts +265 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
- package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
- package/src/plugins/builtin/cloud/plugin.tsx +1 -1
- package/src/plugins/builtin/econ/calendar-model.ts +14 -9
- package/src/plugins/builtin/econ/figures.ts +72 -0
- package/src/plugins/builtin/econ/index.tsx +33 -3
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +79 -31
- package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
- package/src/plugins/builtin/econ-statistics/view.ts +3 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +2 -2
- package/src/plugins/builtin/exposure/charts.tsx +2 -2
- package/src/plugins/builtin/exposure/model.ts +13 -1
- package/src/plugins/builtin/exposure/pane.tsx +4 -4
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/futures/model.ts +2 -1
- package/src/plugins/builtin/futures/table.tsx +2 -2
- package/src/plugins/builtin/fx-matrix/client.ts +6 -1
- package/src/plugins/builtin/fx-matrix/headless.ts +5 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +13 -3
- package/src/plugins/builtin/fx-matrix/live-legs.ts +12 -0
- package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
- package/src/plugins/builtin/iv-history/headless.ts +3 -0
- package/src/plugins/builtin/macro-day/pane.tsx +7 -5
- package/src/plugins/builtin/market-movers/footer.ts +2 -2
- package/src/plugins/builtin/members/breadth-pane.tsx +81 -0
- package/src/plugins/builtin/members/client.ts +19 -4
- package/src/plugins/builtin/members/headless.ts +20 -7
- package/src/plugins/builtin/members/index.tsx +9 -9
- package/src/plugins/builtin/members/model.ts +23 -4
- package/src/plugins/builtin/members/pane.tsx +25 -15
- package/src/plugins/builtin/options/analytics.ts +26 -2
- package/src/plugins/builtin/options/model-spot.ts +5 -2
- package/src/plugins/builtin/options/strike-window.ts +2 -1
- package/src/plugins/builtin/options/table.ts +1 -7
- package/src/plugins/builtin/options/view.tsx +25 -8
- package/src/plugins/builtin/options-positioning/client.ts +19 -0
- package/src/plugins/builtin/options-positioning/headless.ts +54 -14
- package/src/plugins/builtin/options-positioning/index.tsx +3 -3
- package/src/plugins/builtin/options-positioning/model.ts +47 -8
- package/src/plugins/builtin/options-positioning/pane.tsx +68 -38
- package/src/plugins/builtin/perps/freshness.ts +12 -10
- package/src/plugins/builtin/perps/live-rows.ts +10 -3
- package/src/plugins/builtin/perps/pane.tsx +15 -11
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
- package/src/plugins/builtin/rate-path/pane.tsx +2 -2
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +4 -4
- package/src/plugins/builtin/scanner/feed.ts +2 -2
- package/src/plugins/builtin/scanner/index.ts +24 -1
- package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
- package/src/plugins/builtin/scanner/live-feed.ts +168 -0
- package/src/plugins/builtin/scanner/live-headless.ts +114 -0
- package/src/plugins/builtin/scanner/live-model.ts +351 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
- package/src/plugins/builtin/scanner/live-track.ts +186 -0
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/macro-releases.ts +65 -0
- package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +8 -2
- package/src/plugins/builtin/shared/volatility/pricing.ts +7 -1
- package/src/plugins/builtin/themes/headless.ts +6 -4
- package/src/plugins/builtin/themes/model.ts +1 -1
- package/src/plugins/builtin/themes/pane.tsx +3 -3
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
- package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
- package/src/plugins/builtin/vol-surface/client.ts +6 -4
- package/src/plugins/builtin/vol-surface/headless.ts +62 -13
- package/src/plugins/builtin/vol-surface/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/model.ts +22 -7
- package/src/plugins/builtin/vol-surface/pane.tsx +44 -7
- package/src/plugins/builtin/vol-surface/tables.ts +31 -3
- package/src/plugins/builtin/volatility/dvol.ts +46 -0
- package/src/plugins/builtin/volatility/pane.tsx +2 -2
- package/src/plugins/builtin/volatility/series.ts +26 -0
- package/src/plugins/builtin/world-indices/footer.ts +1 -1
- package/src/plugins/catalog-browser.ts +13 -8
- package/src/portfolio/column-values.ts +5 -5
- package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
- package/src/renderers/browser/app-services.ts +1 -7
- package/src/renderers/browser/config-host.ts +2 -55
- package/src/renderers/browser/main.tsx +7 -57
- package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +17 -7
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
- package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +70 -3
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
- package/src/renderers/electrobun/bun/index.ts +25 -1
- package/src/renderers/electrobun/shared/app-name.ts +17 -0
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
- package/src/renderers/opentui/toast-host.tsx +71 -43
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/batches.ts +10 -2
- package/src/sources/provider-router/financial-routes.ts +3 -3
- package/src/sources/provider-router/history.ts +28 -7
- package/src/sources/provider-router/index.ts +146 -10
- package/src/sources/provider-router/search.ts +13 -2
- package/src/state/app/context/index.tsx +3 -0
- package/src/sync/auth-transition.ts +19 -9
- package/src/sync/baseline.ts +10 -4
- package/src/sync/controller.ts +66 -16
- package/src/sync/react.ts +4 -1
- package/src/sync/types.ts +3 -0
- package/src/telemetry/usage-counts.ts +0 -18
- package/src/theme/colors.ts +8 -0
- package/src/tickers/company-name.ts +59 -0
- package/src/tickers/listing-venues.ts +23 -0
- package/src/tickers/search/bare-symbol.ts +36 -0
- package/src/tickers/search/index.ts +36 -7
- package/src/tickers/search/ranking.ts +10 -1
- package/src/tickers/search/types.ts +5 -0
- package/src/time-series/base-series.ts +13 -5
- package/src/time-series/daily-valuation.ts +62 -0
- package/src/time-series/field-catalog.ts +1 -3
- package/src/time-series/forward-valuation.ts +32 -0
- package/src/time-series/fundamentals.ts +155 -7
- package/src/time-series/hooks.ts +1 -0
- package/src/time-series/price-units.ts +72 -2
- package/src/time-series/resolve-context.ts +5 -1
- package/src/time-series/resolve.ts +61 -12
- package/src/time-series/series-presentation.ts +40 -6
- package/src/time-series/spec.ts +2 -1
- package/src/time-series/studies.ts +270 -72
- package/src/time-series/transforms.ts +1 -0
- package/src/time-series/types.ts +5 -0
- package/src/time-series/valuation-releases.ts +39 -0
- package/src/types/data-provider.ts +9 -1
- package/src/types/desktop-window.ts +2 -0
- package/src/types/financials.ts +41 -1
- package/src/types/headless.ts +42 -4
- package/src/types/instrument.ts +3 -0
- package/src/types/plugin.ts +1 -1
- package/src/updater/command-names.ts +12 -7
- package/src/utils/financial-statements.ts +6 -1
- package/src/utils/network-failure.ts +9 -6
- package/src/utils/option-expiry.ts +48 -1
- package/src/utils/options.ts +10 -0
- package/src/utils/plugin-proxy-hosts.json +1 -0
- package/src/utils/price-history.ts +5 -0
- package/src/renderers/browser/first-screen.ts +0 -193
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
latestRegularSessionOpen,
|
|
8
8
|
nextRegularSessionOpen,
|
|
9
9
|
} from "../../market-data/market/freshness";
|
|
10
|
+
import { futuresWeekendClosure } from "../../market-data/market/futures-week";
|
|
10
11
|
import { currentFxClosureStart } from "../../utils/fx-market-hours";
|
|
11
12
|
import { zonedDateKey } from "../../utils/zoned-date-time";
|
|
12
13
|
import type { ReportTime } from "../../utils/utc-time";
|
|
@@ -21,6 +22,11 @@ export interface ReportMarket {
|
|
|
21
22
|
reopensAt?: string;
|
|
22
23
|
/** That open's date at its venue, so a Monday open in Sydney reads Monday. */
|
|
23
24
|
reopensOn?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The reopen is the week's first trade (a futures venue's Sunday evening),
|
|
27
|
+
* not a regular session's open: `reopens 22:00 UTC`, never a session's name.
|
|
28
|
+
*/
|
|
29
|
+
firstTrade?: true;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/** One dated row of a quote report: the venue it trades on and the session its source reports. */
|
|
@@ -34,7 +40,7 @@ export interface SessionObservation {
|
|
|
34
40
|
|
|
35
41
|
interface VenueSession {
|
|
36
42
|
state: ReportMarketState | "always";
|
|
37
|
-
next?: { date: string; open: number };
|
|
43
|
+
next?: { date: string; open: number; firstTrade?: true };
|
|
38
44
|
/** The latest regular session that has closed: its local date and open. */
|
|
39
45
|
last?: { date: string; open: number | null };
|
|
40
46
|
timeZone?: string;
|
|
@@ -48,8 +54,9 @@ const US_EXCHANGE_TIME_ZONE = "America/New_York";
|
|
|
48
54
|
/**
|
|
49
55
|
* A venue's session now, from its calendar (weekends and published holidays),
|
|
50
56
|
* or from the state its source reports when the app has no calendar for it,
|
|
51
|
-
* such as an index publisher or a futures exchange
|
|
52
|
-
* none is placed on the sessions it
|
|
57
|
+
* such as an index publisher or a futures exchange (whose weekend gap it
|
|
58
|
+
* knows); an index quote that states none is placed on the sessions it
|
|
59
|
+
* carries. Null when none of them says.
|
|
53
60
|
*/
|
|
54
61
|
function venueSession(exchange: string, marketState: string | undefined, now: number, calendar?: QuoteIndexSession): VenueSession | null {
|
|
55
62
|
if (isAlwaysOpenExchange(exchange)) return { state: "always" };
|
|
@@ -67,6 +74,9 @@ function venueSession(exchange: string, marketState: string | undefined, now: nu
|
|
|
67
74
|
}
|
|
68
75
|
return { state: "closed", next, last, timeZone };
|
|
69
76
|
}
|
|
77
|
+
// A futures venue's weekend gap closes every contract, so it outranks the state a quote stamped before the Friday close.
|
|
78
|
+
const weekend = futuresWeekendClosure(exchange, now);
|
|
79
|
+
if (weekend) return { state: "closed", ...(weekend.reopensAt != null && weekend.reopensOn ? { next: { date: weekend.reopensOn, open: weekend.reopensAt, firstTrade: true as const } } : {}) };
|
|
70
80
|
if (marketState === "REGULAR") return { state: "open" };
|
|
71
81
|
const stated = marketState === "PRE" ? "pre-market" : marketState === "POST" ? "after-hours" : marketState && CLOSED_STATES.has(marketState) ? "closed" : null;
|
|
72
82
|
if (!calendar) return stated ? { state: stated } : null;
|
|
@@ -209,7 +219,7 @@ export function reportMarketSession(
|
|
|
209
219
|
if (states.size !== 1 || states.has("always")) return { market: null, closeDate: null, closeMarket: null };
|
|
210
220
|
const state = [...states][0] as ReportMarketState;
|
|
211
221
|
const reopen = state === "closed" || state === "pre-market"
|
|
212
|
-
? [...sessions.values()].reduce<
|
|
222
|
+
? [...sessions.values()].reduce<NonNullable<VenueSession["next"]> | null>((first, session) => (
|
|
213
223
|
session.next && (!first || session.next.open < first.open) ? session.next : first
|
|
214
224
|
), null)
|
|
215
225
|
: null;
|
|
@@ -217,7 +227,7 @@ export function reportMarketSession(
|
|
|
217
227
|
return {
|
|
218
228
|
market: {
|
|
219
229
|
state,
|
|
220
|
-
...(reopen ? { reopensAt: new Date(reopen.open).toISOString(), reopensOn: reopen.date } : {}),
|
|
230
|
+
...(reopen ? { reopensAt: new Date(reopen.open).toISOString(), reopensOn: reopen.date, ...(reopen.firstTrade ? { firstTrade: true as const } : {}) } : {}),
|
|
221
231
|
},
|
|
222
232
|
closeDate,
|
|
223
233
|
closeMarket: closeDate && newest ? closeMarketOf(newest.exchange, newest.session) : null,
|
|
@@ -5,6 +5,7 @@ import { buildHeadlessFunctionReport } from "./headless";
|
|
|
5
5
|
import { appendDomReportFooter, buildDomFunctionReport, domReportNote, domReportView } from "./dom";
|
|
6
6
|
import type { ReportFreshness } from "./freshness";
|
|
7
7
|
import type { CliReportTables } from "../report-tables";
|
|
8
|
+
import { knownGapMessage } from "../known-gaps";
|
|
8
9
|
|
|
9
10
|
export type PaneFunctionReportSource = "headless" | "dom";
|
|
10
11
|
|
|
@@ -72,7 +73,10 @@ export async function buildFunctionReport(
|
|
|
72
73
|
// A pane `shot` can draw is still one command away.
|
|
73
74
|
const screenshot = resolved.capability.screenshotReadiness;
|
|
74
75
|
if (screenshot != null && screenshot !== "unsupported") {
|
|
75
|
-
|
|
76
|
+
const error = new Error(`${resolved.token} is an interactive pane with no data report; run \`${shotCommand(resolved.token, rawArg)}\` to see it.`);
|
|
77
|
+
// What the query names that no series is (`fn CAT PMI`): where it is, as `catalog` says it.
|
|
78
|
+
const gap = rawArg.trim() ? knownGapMessage(rawArg) : null;
|
|
79
|
+
throw gap ? Object.assign(error, { details: gap }) : error;
|
|
76
80
|
}
|
|
77
81
|
throw new Error(`${resolved.token} is an interactive pane and does not expose a data report.`);
|
|
78
82
|
}
|
|
@@ -176,12 +176,16 @@ export async function applyListingArgument(
|
|
|
176
176
|
const exchange = typeof args.options.exchange === "string" ? args.options.exchange : undefined;
|
|
177
177
|
const { exchange: _exchange, ...options } = args.options;
|
|
178
178
|
// A list entry with spaces, or an argument of words, is a company's name search finds, or the symbols it holds.
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
179
|
+
const entered = takesOneTicker ? [args.arg] : await readNameEntries(splitSymbolArgs([args.arg], { phrases: true }), context.dataProvider);
|
|
180
|
+
if (entered.length === 0) return args;
|
|
181
|
+
// A symbol the function reads as another (OPX's BTC is the coin, BTC-USD) is named in a note; --exchange keeps it as typed.
|
|
182
|
+
const aliases = exchange ? [] : entered.map((raw) => headless?.symbolAlias?.(raw) ?? null);
|
|
183
|
+
const typed = entered.map((raw, index) => aliases[index]?.symbol ?? raw);
|
|
181
184
|
// RY.TO is the listing its quote names, RY:TSX, as `quote` reads it: a function's data is read by venue.
|
|
182
185
|
let listings = await Promise.all(typed.map((raw) => resolveCliListing(raw, exchange, context, { ownExchangeWins: typed.length > 1, venueSuffix: true })));
|
|
183
186
|
// A separator stuck to the end of a pasted list (`UPST;`) is not part of a symbol; it is named.
|
|
184
187
|
const notes: string[] = takesOneTicker ? [] : stripStrayEdges(args.arg).notes;
|
|
188
|
+
notes.push(...aliases.flatMap((alias) => alias ? [alias.note] : []));
|
|
185
189
|
const token = found.template?.shortcut?.prefix ?? args.target.toUpperCase();
|
|
186
190
|
// Words search finds no company for, and that are not symbols either, name nothing a function can read: tickers do.
|
|
187
191
|
const unnamed = takesList ? listings.filter((listing) => /\s/.test(listing.symbol)) : [];
|
|
@@ -213,6 +217,18 @@ export async function applyListingArgument(
|
|
|
213
217
|
notes.push(...answers.map((answer) => `${answer.message} ${answer.details}`));
|
|
214
218
|
}
|
|
215
219
|
}
|
|
220
|
+
// A listing the function has nothing for (OPX's SOL-USD) says what exists instead; a list leaves it out.
|
|
221
|
+
const refusals = headless?.refuseSymbol ? listings.flatMap((listing) => {
|
|
222
|
+
const refusal = headless.refuseSymbol!(listing.key);
|
|
223
|
+
return refusal ? [{ listing, refusal }] : [];
|
|
224
|
+
}) : [];
|
|
225
|
+
if (refusals.length > 0) {
|
|
226
|
+
listings = listings.filter((listing) => !refusals.some((entry) => entry.listing === listing));
|
|
227
|
+
if (listings.length === 0) {
|
|
228
|
+
throw new ListingArgError(refusals.map(({ refusal }) => refusal.message).join("\n"), refusals.map(({ refusal }) => refusal.details).join("\n"));
|
|
229
|
+
}
|
|
230
|
+
notes.push(...refusals.map(({ refusal }) => `${refusal.message} ${refusal.details}`));
|
|
231
|
+
}
|
|
216
232
|
const usOnly = headless?.usListingsOnly;
|
|
217
233
|
const abroad = usOnly ? listings.filter((listing) => listingAbroadVenue(listing.key) != null) : [];
|
|
218
234
|
if (usOnly && abroad.length > 0) {
|
package/src/cli/quote-answer.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type ListingVenue,
|
|
30
30
|
} from "./listing-arg";
|
|
31
31
|
import { withListingSuggestion } from "./not-a-ticker";
|
|
32
|
+
import { isNetworkFailure } from "../utils/network-failure";
|
|
32
33
|
import type { MarketContext } from "./types";
|
|
33
34
|
|
|
34
35
|
/** The text of an error, or null for none. */
|
|
@@ -76,6 +77,8 @@ export async function answerQuote(
|
|
|
76
77
|
request: ListingDataRequest,
|
|
77
78
|
refresh: boolean,
|
|
78
79
|
venues: Promise<ListingVenue[]> | null,
|
|
80
|
+
/** The row prints what its move is from: a quote asked for again is dated too. */
|
|
81
|
+
{ datesChangeBasis = false }: { datesChangeBasis?: boolean } = {},
|
|
79
82
|
): Promise<QuoteAnswer> {
|
|
80
83
|
const root = listing.exchange ? null : indexRootFor(listing.symbol);
|
|
81
84
|
// A bare root that quoted something else, such as MOVE (Corvex), points at the index too.
|
|
@@ -89,10 +92,12 @@ export async function answerQuote(
|
|
|
89
92
|
const board = tradingBoardNote(listing, result.quote);
|
|
90
93
|
return { result, listing, notes: [...choice, venue ?? [], board ?? [], also(result.quote), currencyCodeNote(listing, result.quote) ?? []].flat() };
|
|
91
94
|
}
|
|
95
|
+
// No answer from the data service: no venue lookup or other listing would get one either.
|
|
96
|
+
if (isNetworkFailure(result.error)) return { result, listing, notes: [] };
|
|
92
97
|
const notTraded = await notTradedMessage(listing, market);
|
|
93
98
|
if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
|
|
94
99
|
if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
|
|
95
|
-
const other = await quoteBareListing(listing, market, request, { refresh });
|
|
100
|
+
const other = await quoteBareListing(listing, market, request, { refresh, datesChangeBasis });
|
|
96
101
|
if (other?.kind === "quoted") {
|
|
97
102
|
return {
|
|
98
103
|
result: { target: other.listing.request, quote: other.quote },
|
|
@@ -110,7 +115,7 @@ export async function answerQuote(
|
|
|
110
115
|
listing.exchange ? null : knownGapForTicker(listing.symbol),
|
|
111
116
|
].filter((line): line is string => !!line);
|
|
112
117
|
if (!other && tryLines.length === 0) {
|
|
113
|
-
return await answerCompanyName(result, listing, market, request, refresh)
|
|
118
|
+
return await answerCompanyName(result, listing, market, request, refresh, datesChangeBasis)
|
|
114
119
|
?? { result: await withCurrencyPairHint(result, market), listing, notes: [] };
|
|
115
120
|
}
|
|
116
121
|
const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
|
|
@@ -136,6 +141,7 @@ async function answerCompanyName(
|
|
|
136
141
|
market: MarketContext,
|
|
137
142
|
request: ListingDataRequest,
|
|
138
143
|
refresh: boolean,
|
|
144
|
+
datesChangeBasis: boolean,
|
|
139
145
|
): Promise<QuoteAnswer | null> {
|
|
140
146
|
if (!listing.typed || listing.exchange || !notATickerSymbol(errorMessage(result.error) ?? "")) return null;
|
|
141
147
|
const named = await oneWordCompanyName(listing.typed, market);
|
|
@@ -147,8 +153,8 @@ async function answerCompanyName(
|
|
|
147
153
|
});
|
|
148
154
|
if (!resolved) return null;
|
|
149
155
|
const found = { ...resolved, readAs: named.readAs, ...named.meant ? { meant: named.meant } : {} };
|
|
150
|
-
const [quoted] = await market.dataProvider.getQuotesBatch([found.request], { forceRefresh: refresh, revalidate: true });
|
|
151
|
-
return quoted?.quote ? answerQuote(quoted, found, market, request, refresh, null) : null;
|
|
156
|
+
const [quoted] = await market.dataProvider.getQuotesBatch([found.request], { forceRefresh: refresh, revalidate: true, datesChangeBasis });
|
|
157
|
+
return quoted?.quote ? answerQuote(quoted, found, market, request, refresh, null, { datesChangeBasis }) : null;
|
|
152
158
|
}
|
|
153
159
|
|
|
154
160
|
/** A listing a command found a quote for, with what `quote` says beside it. */
|
package/src/cli/report-tables.ts
CHANGED
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
import { isEpochMilliseconds, parseReportTime } from "../utils/utc-time";
|
|
19
19
|
import type { ReportFreshness } from "./pane-functions/freshness";
|
|
20
20
|
import {
|
|
21
|
+
EXPORT_DATING_COLUMNS,
|
|
21
22
|
EXPORT_IDENTITY_COLUMNS,
|
|
23
|
+
exportDatingCells,
|
|
22
24
|
exportIdentityCells,
|
|
23
25
|
exportSourceLines,
|
|
24
26
|
type ExportIdentity,
|
|
@@ -46,6 +48,12 @@ export interface CliReportTables {
|
|
|
46
48
|
footer: string[];
|
|
47
49
|
/** What the rows are of, for the fixed columns every row ends with (`EXPORT_IDENTITY_COLUMNS`). */
|
|
48
50
|
identity?: ExportIdentity;
|
|
51
|
+
/**
|
|
52
|
+
* The rows are statistics dated by a period and a release: every table also
|
|
53
|
+
* ends with `period`, `released` and `retrieved` (`EXPORT_DATING_COLUMNS`),
|
|
54
|
+
* less any it has by name, with `as_of` the release.
|
|
55
|
+
*/
|
|
56
|
+
dated?: boolean;
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
/** A column as a report declares it: the text view draws `format(value, row)`, or the value itself. */
|
|
@@ -623,19 +631,22 @@ function csvLine(row: readonly unknown[]): string {
|
|
|
623
631
|
function identifiedTable(table: CliReportTable, report: CliReportTables, freshness: ReportFreshness | undefined): CliReportTable {
|
|
624
632
|
if (!freshness || table.columns.length === 0) return table;
|
|
625
633
|
const identity = { ...report.identity, ...table.identity };
|
|
626
|
-
const
|
|
627
|
-
const rowCells = (index: number) => {
|
|
634
|
+
const identityFor = (index: number) => {
|
|
628
635
|
const own = table.rowIdentities?.[index];
|
|
629
|
-
return own ?
|
|
636
|
+
return own ? { ...identity, ...own } : identity;
|
|
630
637
|
};
|
|
631
638
|
const added = EXPORT_IDENTITY_COLUMNS.flatMap((column, index) => table.columns.includes(column) ? [] : [index]);
|
|
632
|
-
|
|
639
|
+
const dating = report.dated
|
|
640
|
+
? EXPORT_DATING_COLUMNS.flatMap((column, index) => table.columns.includes(column) ? [] : [index]) : [];
|
|
641
|
+
if (added.length === 0 && dating.length === 0) return table;
|
|
633
642
|
return {
|
|
634
643
|
...table,
|
|
635
|
-
columns: [...table.columns, ...added.map((index) => EXPORT_IDENTITY_COLUMNS[index]!)],
|
|
644
|
+
columns: [...table.columns, ...added.map((index) => EXPORT_IDENTITY_COLUMNS[index]!), ...dating.map((index) => EXPORT_DATING_COLUMNS[index]!)],
|
|
636
645
|
rows: table.rows.map((row, index) => {
|
|
637
|
-
const
|
|
638
|
-
|
|
646
|
+
const rowIdentity = identityFor(index);
|
|
647
|
+
const identityCells = exportIdentityCells(freshness, rowIdentity);
|
|
648
|
+
const datingCells = exportDatingCells(freshness, rowIdentity);
|
|
649
|
+
return [...row, ...added.map((column) => identityCells[column]!), ...dating.map((column) => datingCells[column]!)];
|
|
639
650
|
}),
|
|
640
651
|
};
|
|
641
652
|
}
|
|
@@ -5,9 +5,18 @@ import { pluginSeedFailure, pluginSeedNotice, samePluginSeedFailure, seedExtract
|
|
|
5
5
|
import { setPluginSeedNotice } from "../plugins/seed-notice";
|
|
6
6
|
import { debugLog } from "../utils/debug-log";
|
|
7
7
|
import type { AppConfig } from "../types/config";
|
|
8
|
+
import { withDeadline } from "../utils/async-deadline";
|
|
8
9
|
|
|
9
10
|
const log = debugLog.createLogger("plugin-seed");
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* How long a launch waits for the plugin registry before it starts without
|
|
14
|
+
* the plugins left to install. A connection that hangs would otherwise hold
|
|
15
|
+
* the screen blank: the registry is read first, and git would hang the same
|
|
16
|
+
* way after it. They are retried at the next launch.
|
|
17
|
+
*/
|
|
18
|
+
const REGISTRY_WAIT_MS = 8_000;
|
|
19
|
+
|
|
11
20
|
/**
|
|
12
21
|
* Reinstalls plugins that moved out of this repository into their own.
|
|
13
22
|
*
|
|
@@ -40,7 +49,10 @@ export async function restoreExtractedPlugins(): Promise<Pick<AppConfig, "seeded
|
|
|
40
49
|
// Without git every clone fails the same way: no registry read, no attempt.
|
|
41
50
|
gitMissing ??= !gitInstalled();
|
|
42
51
|
if (gitMissing) throw new Error("git is not installed");
|
|
43
|
-
|
|
52
|
+
// Only no answer at all stops here: the network hangs, and a clone would
|
|
53
|
+
// too, so nothing is tried until the next launch. A registry that fails
|
|
54
|
+
// is not reported as silent: the plugins install from their repositories.
|
|
55
|
+
listings ??= withDeadline(loadRegistryListings(), REGISTRY_WAIT_MS, "the plugin registry did not answer");
|
|
44
56
|
return installListedPlugin(repo, { quiet: true }, await listings);
|
|
45
57
|
});
|
|
46
58
|
// Recorded, so doctor knows a launch tried, and a failure the next launch
|
package/src/cli/result.ts
CHANGED
|
@@ -18,7 +18,10 @@ import { formatFreshnessLine, withSavedCopy, type ReportFreshness } from "./pane
|
|
|
18
18
|
import { withSearchHints, type FredSeriesCheck } from "./not-a-ticker";
|
|
19
19
|
import { renderReportCsv, renderReportNdjson, withCsvFooter, type CliReportTables } from "./report-tables";
|
|
20
20
|
import {
|
|
21
|
+
EXPORT_DATING_COLUMNS,
|
|
21
22
|
EXPORT_IDENTITY_COLUMNS,
|
|
23
|
+
exportDatingCells,
|
|
24
|
+
exportDatingFields,
|
|
22
25
|
exportIdentityCells,
|
|
23
26
|
exportIdentityFields,
|
|
24
27
|
exportSource,
|
|
@@ -120,6 +123,15 @@ export interface CliResultRenderOptions<T = unknown, Row = Record<string, unknow
|
|
|
120
123
|
exportIdentity?: ExportIdentity | ((row: Row) => ExportIdentity);
|
|
121
124
|
/** What CSV says after the source lines as `# note:` lines: what the rows do not show, such as where a series begins. */
|
|
122
125
|
exportNotes?: readonly string[];
|
|
126
|
+
/**
|
|
127
|
+
* The rows are statistics dated by a period and a release (`econ`, `fred`):
|
|
128
|
+
* CSV and NDJSON rows also end with `period`, `released` and `retrieved`
|
|
129
|
+
* (`EXPORT_DATING_COLUMNS`) after the five fixed columns, from the `period`
|
|
130
|
+
* and `released` of each row's `exportIdentity`, and `as_of` is the release.
|
|
131
|
+
* A column the rows already have by name keeps its own values. Needs a
|
|
132
|
+
* cited source, as the fixed columns do.
|
|
133
|
+
*/
|
|
134
|
+
exportDated?: boolean;
|
|
123
135
|
/**
|
|
124
136
|
* Why a row's figures are empty (`Not a ticker: ZZZZQQ.`, `Chg%: no previous
|
|
125
137
|
* close`), for the `status` column CSV writes after the row's own columns and
|
|
@@ -348,20 +360,31 @@ function renderCsv<Row extends Record<string, unknown>>(
|
|
|
348
360
|
columns?: CliResultColumn<Row>[],
|
|
349
361
|
identity?: (row: Row) => string[],
|
|
350
362
|
status?: (row: Row) => string | null | undefined,
|
|
363
|
+
dating?: (row: Row) => string[],
|
|
351
364
|
): string {
|
|
352
365
|
const resolvedColumns = columns && columns.length > 0
|
|
353
366
|
? columns
|
|
354
367
|
: inferColumns(rows as Record<string, unknown>[]) as CliResultColumn<Row>[];
|
|
355
368
|
const headers = resolvedColumns.map((column) => column.header);
|
|
369
|
+
// A dated export's own `released` column keeps its values; the others close the row.
|
|
370
|
+
const datingAdded = dating
|
|
371
|
+
? EXPORT_DATING_COLUMNS.flatMap((column, index) => headers.includes(column) ? [] : [index]) : [];
|
|
356
372
|
return serializeCsv([
|
|
357
|
-
[
|
|
373
|
+
[
|
|
374
|
+
...headers,
|
|
375
|
+
...status ? [statusHeader(headers)] : [],
|
|
376
|
+
...identity ? EXPORT_IDENTITY_COLUMNS : [],
|
|
377
|
+
...datingAdded.map((index) => EXPORT_DATING_COLUMNS[index]!),
|
|
378
|
+
],
|
|
358
379
|
...rows.map((row) => {
|
|
359
380
|
const values = resolvedColumns.map((column) => (column.value ? column.value(row) : row[column.key]));
|
|
360
381
|
const blanked = resolvedColumns.flatMap((column, index) => isNonFiniteNumber(values[index]) ? [column.header] : []);
|
|
382
|
+
const datingCells = dating?.(row) ?? [];
|
|
361
383
|
return [
|
|
362
384
|
...values.map((value) => isNonFiniteNumber(value) ? "" : value),
|
|
363
385
|
...status ? [exportStatusText(status(row), blanked)] : [],
|
|
364
386
|
...identity?.(row) ?? [],
|
|
387
|
+
...datingAdded.map((index) => datingCells[index] ?? ""),
|
|
365
388
|
];
|
|
366
389
|
}),
|
|
367
390
|
]);
|
|
@@ -534,13 +557,26 @@ function serializeCliRows<T, Row extends Record<string, unknown>>(
|
|
|
534
557
|
return rows.map((row) => {
|
|
535
558
|
if (!isPlainObject(row)) return JSON.stringify(row);
|
|
536
559
|
const fields = ndjsonRow(row as Row, status, statusKey);
|
|
537
|
-
return JSON.stringify(
|
|
560
|
+
if (!cited) return JSON.stringify(fields);
|
|
561
|
+
const identity = rowIdentity(row as Row, renderOptions.exportIdentity);
|
|
562
|
+
// A dated export's own fields (a row's `released`) keep their values.
|
|
563
|
+
const dating = renderOptions.exportDated
|
|
564
|
+
? Object.fromEntries(Object.entries(exportDatingFields(cited, identity)).filter(([key]) => !(key in fields))) : {};
|
|
565
|
+
return JSON.stringify({ ...fields, ...exportIdentityFields(cited, identity), ...dating });
|
|
538
566
|
}).join("\n");
|
|
539
567
|
}
|
|
540
568
|
if (options.format === "csv") {
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
return withCsvFooter(
|
|
569
|
+
// The rows a window cut say so after them, as text and JSON do: a script has no other way to know.
|
|
570
|
+
const cutNote = window.note ? [`note: ${window.note}`] : [];
|
|
571
|
+
if (!cited) return withCsvFooter(renderCsv(rows as Row[], renderOptions.columns, undefined, status), cutNote);
|
|
572
|
+
const csv = renderCsv(
|
|
573
|
+
rows as Row[],
|
|
574
|
+
renderOptions.columns,
|
|
575
|
+
(row) => exportIdentityCells(cited, rowIdentity(row, renderOptions.exportIdentity)),
|
|
576
|
+
status,
|
|
577
|
+
renderOptions.exportDated ? (row) => exportDatingCells(cited, rowIdentity(row, renderOptions.exportIdentity)) : undefined,
|
|
578
|
+
);
|
|
579
|
+
return withCsvFooter(csv, [...exportSourceLines(cited), ...cutNote, ...(renderOptions.exportNotes ?? []).map((note) => `note: ${note}`)]);
|
|
544
580
|
}
|
|
545
581
|
if (renderOptions.text) {
|
|
546
582
|
return renderOptions.text(result.data);
|
|
@@ -608,6 +644,22 @@ export function printCliResult<T, Row extends Record<string, unknown> = Record<s
|
|
|
608
644
|
console.error(`${cliStyles.warning("warning:")} ${hinted(warning)}`);
|
|
609
645
|
}
|
|
610
646
|
}
|
|
647
|
+
// CSV says what its rows do not in closing `# note:` lines; NDJSON has no place for one among its rows,
|
|
648
|
+
// so the same notes go to stderr, where the warnings are.
|
|
649
|
+
if (options.format === "ndjson" && !options.quiet) {
|
|
650
|
+
for (const note of exportNdjsonNotes(result, options, renderOptions)) console.error(`note: ${note}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** What a `--limit`, or the default a command caps its rows at, left out of a list, then the command's own notes: what CSV closes with as `# note:` lines. */
|
|
655
|
+
function exportNdjsonNotes<T, Row extends Record<string, unknown>>(
|
|
656
|
+
result: CliResult<T>,
|
|
657
|
+
options: CliGlobalOptions,
|
|
658
|
+
renderOptions: CliResultRenderOptions<T, Row>,
|
|
659
|
+
): string[] {
|
|
660
|
+
if (renderOptions.tables || renderOptions.text) return [];
|
|
661
|
+
const cut = windowed(asRows(result.data, renderOptions.rows), options, renderOptions).note;
|
|
662
|
+
return [...cut ? [cut] : [], ...renderOptions.exportNotes ?? []];
|
|
611
663
|
}
|
|
612
664
|
|
|
613
665
|
export function serializeCliError(error: CliErrorObject, options: CliGlobalOptions): string {
|
|
@@ -7,6 +7,7 @@ import { AXIS_DOUBLE_PRESS_MS, isVerticalWheelDirection } from "./panel-gestures
|
|
|
7
7
|
import { type ChartToolSpan, CHART_DRAWING_COLORS, isDrawingTool, type ChartDrawing } from "./tools";
|
|
8
8
|
import { ChartDrawingStore, NO_DRAWINGS } from "./drawing-store";
|
|
9
9
|
import { CompositeLegend } from "./legend";
|
|
10
|
+
import { compositeRangeStatisticsLevels } from "./range-statistics";
|
|
10
11
|
import { ChartToolbar, CHART_TOOLBAR_WIDTH } from "./toolbar";
|
|
11
12
|
import { AsciiText, Box, ChartSurface, Text, useUiCapabilities, type BoxRenderable } from "../../../ui";
|
|
12
13
|
import { Button } from "../../ui/button";
|
|
@@ -204,6 +205,7 @@ function ThemedCompositeChart({
|
|
|
204
205
|
timePick,
|
|
205
206
|
pricePick,
|
|
206
207
|
levels,
|
|
208
|
+
rangeStatisticsSeriesId,
|
|
207
209
|
chartLayout,
|
|
208
210
|
onChartLayoutChange,
|
|
209
211
|
}: CompositeChartProps & { chartLayout: ChartLayoutPrefs; onChartLayoutChange: ChartLayoutUpdate }) {
|
|
@@ -337,6 +339,20 @@ function ThemedCompositeChart({
|
|
|
337
339
|
textDim: colors?.textDim ?? activeThemeColors.textDim,
|
|
338
340
|
negative: colors?.negative ?? activeThemeColors.negative,
|
|
339
341
|
}), [activeThemeColors, colors, paneSurface]);
|
|
342
|
+
const rangeStatisticsLevels = useMemo(() => {
|
|
343
|
+
void lastTickKey;
|
|
344
|
+
return compositeRangeStatisticsLevels(
|
|
345
|
+
panelSeries.find((entry) => entry.id === rangeStatisticsSeriesId),
|
|
346
|
+
effectiveViewport,
|
|
347
|
+
resolvedColors.textDim,
|
|
348
|
+
formatValue,
|
|
349
|
+
);
|
|
350
|
+
}, [effectiveViewport, formatValue, lastTickKey, panelSeries, rangeStatisticsSeriesId, resolvedColors.textDim]);
|
|
351
|
+
const displayedLevels = useMemo(() => (
|
|
352
|
+
levels && rangeStatisticsLevels && levels.seriesId === rangeStatisticsLevels.seriesId
|
|
353
|
+
? { ...levels, items: [...levels.items, ...rangeStatisticsLevels.items] }
|
|
354
|
+
: levels ?? rangeStatisticsLevels
|
|
355
|
+
), [levels, rangeStatisticsLevels]);
|
|
340
356
|
// A custom x axis means x is not time: a tenor, a fraction, a return. Its
|
|
341
357
|
// last observation belongs at the right edge, under its own label.
|
|
342
358
|
const rightOffsetRatio = xAxis ? 0 : COMPOSITE_RIGHT_OFFSET_RATIO;
|
|
@@ -351,6 +367,7 @@ function ThemedCompositeChart({
|
|
|
351
367
|
timelineSeries: marketTimelineSeries,
|
|
352
368
|
rightOffsetRatio,
|
|
353
369
|
valueScales,
|
|
370
|
+
referenceLevels: rangeStatisticsLevels,
|
|
354
371
|
});
|
|
355
372
|
}, [
|
|
356
373
|
clipToViewport,
|
|
@@ -361,6 +378,7 @@ function ThemedCompositeChart({
|
|
|
361
378
|
panelSeries,
|
|
362
379
|
panels,
|
|
363
380
|
rightOffsetRatio,
|
|
381
|
+
rangeStatisticsLevels,
|
|
364
382
|
valueScales,
|
|
365
383
|
]);
|
|
366
384
|
// Gutters follow the axes the scene actually built. Reading the series list
|
|
@@ -426,7 +444,7 @@ function ThemedCompositeChart({
|
|
|
426
444
|
availableAxisWidth,
|
|
427
445
|
Math.max(
|
|
428
446
|
Math.min(MINIMUM_AXIS_LABEL_WIDTH, maximumAxisWidth),
|
|
429
|
-
...(
|
|
447
|
+
...(displayedLevels?.items ?? []).map((level) => level.axisLabel?.length ?? 0),
|
|
430
448
|
...(layoutPanels ?? []).flatMap((panel) => (["left", "right"] as const).map((side) => {
|
|
431
449
|
const width = compositeAxisLabelWidth(panel.axes[side], formatAxisValue, includeCursorLabels);
|
|
432
450
|
if (!includeSeriesTags || !width) return includeCursorLabels ? width : Math.min(width, maximumAxisWidth);
|
|
@@ -437,7 +455,7 @@ function ThemedCompositeChart({
|
|
|
437
455
|
return (width + 1) * lanes;
|
|
438
456
|
})),
|
|
439
457
|
),
|
|
440
|
-
), [availableAxisWidth, formatAxisValue, includeCursorLabels, includeSeriesTags, layoutPanels, maximumAxisWidth
|
|
458
|
+
), [availableAxisWidth, displayedLevels?.items, formatAxisValue, includeCursorLabels, includeSeriesTags, layoutPanels, maximumAxisWidth]);
|
|
441
459
|
const leftAxisWidth = hasLeftAxis ? resolvedAxisWidth : 0;
|
|
442
460
|
const rightAxisWidth = hasRightAxis ? resolvedAxisWidth : 0;
|
|
443
461
|
const axisGap = resolvedAxisWidth > 0 ? 1 : 0;
|
|
@@ -689,7 +707,7 @@ function ThemedCompositeChart({
|
|
|
689
707
|
onActivate,
|
|
690
708
|
timePick,
|
|
691
709
|
pricePick,
|
|
692
|
-
levels,
|
|
710
|
+
levels: displayedLevels,
|
|
693
711
|
visibleLegendSeries,
|
|
694
712
|
visibleSeriesIds,
|
|
695
713
|
onToggleSeries,
|
|
@@ -1136,7 +1154,7 @@ function ThemedCompositeChart({
|
|
|
1136
1154
|
showTextFallback={showTextFallback}
|
|
1137
1155
|
onPickTime={timePick?.onPick}
|
|
1138
1156
|
onPickPrice={pricePick?.onPick}
|
|
1139
|
-
levels={levelHost?.panel.id === panel.id ?
|
|
1157
|
+
levels={levelHost?.panel.id === panel.id ? displayedLevels ?? null : null}
|
|
1140
1158
|
selectedLevelId={selectedLevelId}
|
|
1141
1159
|
onSelectLevel={setSelectedLevelId}
|
|
1142
1160
|
valueScalable={interactive}
|
|
@@ -2,6 +2,7 @@ import { formatMarketPrice, formatMarketPriceWithCurrency, stablePriceFractionDi
|
|
|
2
2
|
import { INDEX_POINTS_UNIT } from "../../../market-data/market/index-level";
|
|
3
3
|
import type { ResolvedSeries, TimeSeriesPoint } from "../../../time-series/types";
|
|
4
4
|
import type { CompositeAxisDomain, CompositePanelScene } from "./types";
|
|
5
|
+
import { formatNumber, formatPercentRaw } from "../../../utils/format";
|
|
5
6
|
import { timeZoneLabel, zonedWallClockMs } from "../../../utils/zoned-date-time";
|
|
6
7
|
|
|
7
8
|
const CURRENCY_SYMBOLS: Record<string, string> = {
|
|
@@ -237,6 +238,28 @@ function compactResolution(value: number): number {
|
|
|
237
238
|
return absolute > 0 ? 10 ** (Math.floor(Math.log10(absolute)) - 2) : 0;
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
/** A series of changes over a baseline: returns since the range began, year-on-year or quarter-on-quarter growth. */
|
|
242
|
+
function isChangeSeries(series: ResolvedSeries): boolean {
|
|
243
|
+
return series.unit.trim() === "%" && (series.transform === "percent" || series.transform === "yoy" || series.transform === "qoq");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* A value worked out from prices (a formula, a spread, a price over an FX
|
|
248
|
+
* rate) at its legs' decimals: bare beside a stated unit (92.6164 USD /
|
|
249
|
+
* barrel), with its derived unit, or with its currency's sign ($12.34).
|
|
250
|
+
*/
|
|
251
|
+
function formatDerivedValue(value: number, series: ResolvedSeries, decimals: number): string {
|
|
252
|
+
const number = formatNumber(Math.abs(value), decimals);
|
|
253
|
+
const sign = value < 0 && /[1-9]/.test(number) ? "-" : "";
|
|
254
|
+
if (series.statedUnit) return `${sign}${number}`;
|
|
255
|
+
const unit = series.unit.trim();
|
|
256
|
+
if (series.unitGroup.toLowerCase().startsWith("derived-unit:")) return `${sign}${number} ${unit}`;
|
|
257
|
+
const currency = unitCurrencyCode(unit);
|
|
258
|
+
if (!currency) return `${sign}${number}`;
|
|
259
|
+
const symbol = CURRENCY_SYMBOLS[currency];
|
|
260
|
+
return symbol ? `${sign}${symbol}${number}` : `${sign}${number} ${currency}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
240
263
|
/** `referencePrice` defaults to the series' own; a study on a price axis
|
|
241
264
|
* passes the axis's reference so it shares the price's decimals. */
|
|
242
265
|
export function formatCompositeSeriesValue(
|
|
@@ -244,6 +267,10 @@ export function formatCompositeSeriesValue(
|
|
|
244
267
|
series: ResolvedSeries,
|
|
245
268
|
referencePrice = seriesPriceReference(series),
|
|
246
269
|
): string {
|
|
270
|
+
// A return reads as every other return in the app, signed with two decimals, so a legend of them lines up
|
|
271
|
+
// (+34.02%, +0.76%, +15.80%) instead of one precision per magnitude.
|
|
272
|
+
if (isChangeSeries(series)) return formatPercentRaw(value);
|
|
273
|
+
if (series.valueDecimals !== undefined) return formatDerivedValue(value, series, series.valueDecimals);
|
|
247
274
|
if (series.statedUnit) {
|
|
248
275
|
// The number alone: the legend prints the unit beside it, as FUT prints
|
|
249
276
|
// a price beside its unit (4.6242 USD / gallon, not $4.6242).
|
|
@@ -23,6 +23,8 @@ export interface CompositeChartLevel {
|
|
|
23
23
|
label?: string;
|
|
24
24
|
/** Exact instrument price, independent of the grid's compact tick formatter. */
|
|
25
25
|
axisLabel?: string;
|
|
26
|
+
/** A named reference guide keeps its axis label ahead of ordinary grid ticks. */
|
|
27
|
+
prioritizeAxisLabel?: boolean;
|
|
26
28
|
dashed?: boolean;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -30,6 +30,7 @@ function sameSeriesMeta(left: ResolvedSeries, right: ResolvedSeries): boolean {
|
|
|
30
30
|
&& left.unitGroup === right.unitGroup
|
|
31
31
|
&& left.statedUnit === right.statedUnit
|
|
32
32
|
&& left.priceAssetCategory === right.priceAssetCategory
|
|
33
|
+
&& left.valueDecimals === right.valueDecimals
|
|
33
34
|
&& left.nativeFrequency === right.nativeFrequency
|
|
34
35
|
&& left.timestampMode === right.timestampMode
|
|
35
36
|
&& left.dataShape === right.dataShape
|
|
@@ -542,6 +542,7 @@ export function CompositePanelSurface({
|
|
|
542
542
|
yRatio,
|
|
543
543
|
label: level.axisLabel ?? (formatAxisValue ? formatAxisValue(level.value, levelDomain) : formatCompositeCursorValue(level.value, levelDomain)),
|
|
544
544
|
color: level.color,
|
|
545
|
+
prioritizeAxisLabel: level.prioritizeAxisLabel,
|
|
545
546
|
})) : [], [formatAxisValue, levelDomain, projectedLevels, selectedLevelId]);
|
|
546
547
|
const buildAxisMarkers = (side: "left" | "right") => {
|
|
547
548
|
if (!panel.axes[side]) return undefined;
|
|
@@ -557,7 +558,8 @@ export function CompositePanelSurface({
|
|
|
557
558
|
);
|
|
558
559
|
const fixed = [lastPriceMarker, axisMarkers].filter(onSide).map(toAxisMarker);
|
|
559
560
|
// The prior close's label yields to every level's.
|
|
560
|
-
const
|
|
561
|
+
const referenceLabels = levelMarkers.filter((marker) => marker.prioritizeAxisLabel && onSide(marker)).map(toAxisMarker);
|
|
562
|
+
const levelLabels = [...levelMarkers.filter((marker) => !marker.prioritizeAxisLabel), priorCloseMarker].filter(onSide).map(toAxisMarker);
|
|
561
563
|
// A label is a row tall, so close levels would stack into an unreadable
|
|
562
564
|
// pile and hide the last price: each keeps its line, and only the labels
|
|
563
565
|
// with a row to themselves show. Desktop badges sit between rows.
|
|
@@ -567,8 +569,13 @@ export function CompositePanelSurface({
|
|
|
567
569
|
pixelY: tick.topPercent / 100 * Math.max(panel.height * cellHeightPx - 1, 1),
|
|
568
570
|
}))
|
|
569
571
|
: [...(side === "left" ? leftAxisLabels : rightAxisLabels)].map(([row, label]) => ({ id: undefined, row, pixelY: row * cellHeightPx, label, color: colors.textDim }));
|
|
572
|
+
const rowOf = (marker: ReturnType<typeof toAxisMarker>) => fractionalViewport ? marker.pixelY / cellHeightPx : marker.row;
|
|
573
|
+
// Named guides are part of the chart's readout: ordinary ticks yield to
|
|
574
|
+
// them, while a live price or cursor still keeps its place.
|
|
575
|
+
const references = fitAxisLabels(referenceLabels, fixed, rowOf);
|
|
570
576
|
const markers = [
|
|
571
|
-
...
|
|
577
|
+
...references,
|
|
578
|
+
...fitAxisLabels(levelLabels, [...references, ...fixed, ...grid], rowOf),
|
|
572
579
|
...fixed,
|
|
573
580
|
];
|
|
574
581
|
return markers.length > 0 ? markers : undefined;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ResolvedSeries, TimeSeriesPoint } from "../../../time-series/types";
|
|
2
|
+
import { formatCompositeSeriesValue } from "./format";
|
|
3
|
+
import type { CompositeViewportRange } from "./interactions";
|
|
4
|
+
import type { CompositeChartLevel, CompositeChartLevels } from "./levels";
|
|
5
|
+
|
|
6
|
+
/** Population statistics of the finite observations actually in the window. */
|
|
7
|
+
export function compositeRangeStatistics(points: readonly TimeSeriesPoint[], viewport: CompositeViewportRange) {
|
|
8
|
+
const start = viewport.start.getTime();
|
|
9
|
+
const end = viewport.end.getTime();
|
|
10
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || start > end) return null;
|
|
11
|
+
let count = 0;
|
|
12
|
+
let mean = 0;
|
|
13
|
+
let squaredDeviations = 0;
|
|
14
|
+
for (const point of points) {
|
|
15
|
+
const time = point.date.getTime();
|
|
16
|
+
const value = Number.isFinite(point.value) ? point.value : point.close;
|
|
17
|
+
if (!Number.isFinite(time) || time < start || time > end
|
|
18
|
+
|| typeof value !== "number" || !Number.isFinite(value)) continue;
|
|
19
|
+
count += 1;
|
|
20
|
+
// Welford's update avoids subtracting nearly equal squared sums when
|
|
21
|
+
// valuations are tightly clustered around a large level.
|
|
22
|
+
const delta = value - mean;
|
|
23
|
+
mean += delta / count;
|
|
24
|
+
squaredDeviations += delta * (value - mean);
|
|
25
|
+
}
|
|
26
|
+
if (!count || !Number.isFinite(mean) || !Number.isFinite(squaredDeviations)) return null;
|
|
27
|
+
return { count, mean, standardDeviation: Math.sqrt(Math.max(0, squaredDeviations / count)) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Read-only levels share the chart's axis labels, vector, Kitty and text paths. */
|
|
31
|
+
export function compositeRangeStatisticsLevels(
|
|
32
|
+
series: ResolvedSeries | undefined,
|
|
33
|
+
viewport: CompositeViewportRange | null,
|
|
34
|
+
color: string,
|
|
35
|
+
formatValue: (value: number, series: ResolvedSeries) => string = formatCompositeSeriesValue,
|
|
36
|
+
): CompositeChartLevels | null {
|
|
37
|
+
if (!series || series.hidden || !viewport) return null;
|
|
38
|
+
const statistics = compositeRangeStatistics(series.points, viewport);
|
|
39
|
+
if (!statistics || statistics.count < 20) return null;
|
|
40
|
+
const { mean, standardDeviation } = statistics;
|
|
41
|
+
const guides = [
|
|
42
|
+
{ id: "mean", label: "Avg", value: mean, dashed: false },
|
|
43
|
+
{ id: "upper", label: "+1σ", value: mean + standardDeviation, dashed: true },
|
|
44
|
+
{ id: "lower", label: "−1σ", value: mean - standardDeviation, dashed: true },
|
|
45
|
+
];
|
|
46
|
+
const items: CompositeChartLevel[] = [];
|
|
47
|
+
for (const guide of guides) {
|
|
48
|
+
// A constant window (including one observation) has only its mean.
|
|
49
|
+
if (!Number.isFinite(guide.value) || items.some((item) => item.value === guide.value)) continue;
|
|
50
|
+
items.push({
|
|
51
|
+
id: `range-statistics:${series.id}:${guide.id}`,
|
|
52
|
+
value: guide.value,
|
|
53
|
+
color,
|
|
54
|
+
dashed: guide.dashed,
|
|
55
|
+
axisLabel: `${guide.label} ${formatValue(guide.value, series)}`,
|
|
56
|
+
prioritizeAxisLabel: true,
|
|
57
|
+
editable: false,
|
|
58
|
+
actionable: false,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return { seriesId: series.id, items };
|
|
62
|
+
}
|