gloomberb 0.18.2 → 0.18.3
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 +1 -1
- package/patches/electrobun@1.18.1.patch +106 -5
- package/src/api-client/data.ts +15 -0
- package/src/api-client/index.ts +2 -0
- package/src/api-client/request.ts +5 -3
- package/src/api-client/telemetry.ts +17 -0
- package/src/app/global-shortcuts.ts +1 -16
- package/src/app/runtime/update.ts +28 -8
- package/src/architecture/pane-tabs-allowlist.json +65 -0
- package/src/cli/commands/market.ts +73 -33
- package/src/cli/commands/ticker.ts +82 -25
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +8 -0
- package/src/cli/issuer-membership.ts +103 -0
- package/src/cli/listing-arg.ts +15 -2
- package/src/cli/name-entries.ts +51 -12
- package/src/cli/pane-functions/capabilities.ts +2 -0
- package/src/cli/pane-functions/catalog.ts +16 -0
- package/src/cli/pane-functions/dom.ts +2 -1
- package/src/cli/pane-functions/headless.ts +25 -6
- package/src/cli/pane-functions/index.ts +64 -5
- package/src/cli/pane-functions/resolver.ts +19 -5
- package/src/cli/report-tables.ts +136 -18
- package/src/cli/result.ts +12 -4
- package/src/cli/symbol-list.ts +47 -11
- package/src/components/chart/composite/composite-chart.tsx +14 -5
- package/src/components/chart/composite/drawing-store.ts +26 -6
- package/src/components/chart/composite/interactions.ts +2 -1
- package/src/components/chart/composite/panel-surface.tsx +11 -0
- package/src/components/chart/composite/types.ts +6 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
- package/src/components/command-bar/routes/root/command-items.ts +32 -6
- package/src/components/data-table/view.tsx +23 -3
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +1 -0
- package/src/components/layout/floating-pane.tsx +1 -0
- package/src/components/layout/header.tsx +228 -55
- package/src/components/layout/pane/header.tsx +10 -3
- package/src/components/layout/pane/index.tsx +1 -0
- package/src/components/layout/pane/pane-tabs.tsx +9 -1
- package/src/components/ui/stat-grid.tsx +28 -3
- package/src/components/ui/status.tsx +3 -1
- package/src/core/app-services.ts +4 -0
- package/src/i18n/es.ts +22 -0
- package/src/i18n/ja.ts +22 -0
- package/src/i18n/ko.ts +22 -0
- package/src/i18n/zh-cn.ts +22 -0
- package/src/i18n/zh-tw.ts +22 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
- package/src/plugins/builtin/alerts/events.ts +1 -0
- package/src/plugins/builtin/alerts/research-rules.ts +1 -0
- package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
- package/src/plugins/builtin/attention/pane.tsx +16 -14
- package/src/plugins/builtin/awards/pane.tsx +6 -6
- package/src/plugins/builtin/backtest/export.ts +1 -1
- package/src/plugins/builtin/broker-trading/dock.tsx +4 -2
- package/src/plugins/builtin/broker-trading/pane.tsx +6 -4
- package/src/plugins/builtin/broker-trading/ticket.tsx +10 -5
- package/src/plugins/builtin/buildout/pane/index.tsx +9 -6
- package/src/plugins/builtin/catalysts/client.ts +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +3 -4
- package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
- package/src/plugins/builtin/company-attention/pane.tsx +13 -11
- package/src/plugins/builtin/company-kpis/pane.tsx +4 -3
- package/src/plugins/builtin/correlation/headless.ts +2 -1
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
- package/src/plugins/builtin/correlation/settings.ts +1 -0
- package/src/plugins/builtin/cot/pane.tsx +4 -3
- package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
- package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
- package/src/plugins/builtin/doe/pane.tsx +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +10 -0
- package/src/plugins/builtin/equity-screener/model.ts +28 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +105 -98
- package/src/plugins/builtin/exposure/pane.tsx +10 -9
- package/src/plugins/builtin/gpu/board.tsx +4 -1
- package/src/plugins/builtin/gpu/changes.tsx +10 -10
- package/src/plugins/builtin/gpu/equities.tsx +4 -1
- package/src/plugins/builtin/gpu/history.tsx +4 -2
- package/src/plugins/builtin/gpu/pane.tsx +5 -3
- package/src/plugins/builtin/holders/client.ts +2 -1
- package/src/plugins/builtin/holders/headless.ts +2 -1
- package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
- package/src/plugins/builtin/insider/base-values.ts +11 -7
- package/src/plugins/builtin/insider/client.ts +3 -0
- package/src/plugins/builtin/insider/headless.ts +19 -6
- package/src/plugins/builtin/insider/index.tsx +17 -8
- package/src/plugins/builtin/insider/issuer.ts +34 -4
- package/src/plugins/builtin/insider/signals-client.ts +2 -2
- package/src/plugins/builtin/insider/table-model.ts +35 -13
- package/src/plugins/builtin/insider/table.tsx +5 -2
- package/src/plugins/builtin/iv-history/events.ts +145 -0
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +91 -7
- package/src/plugins/builtin/jodi/pane.tsx +4 -4
- package/src/plugins/builtin/market-movers/client.ts +4 -3
- package/src/plugins/builtin/market-movers/screener.ts +4 -3
- package/src/plugins/builtin/notes/cli.ts +1 -1
- package/src/plugins/builtin/notes/export.ts +17 -6
- package/src/plugins/builtin/notes/files.ts +16 -5
- package/src/plugins/builtin/options/enrichment-model.ts +10 -9
- package/src/plugins/builtin/options/model-spot.ts +70 -4
- package/src/plugins/builtin/options/view.tsx +41 -13
- package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
- package/src/plugins/builtin/options-positioning/headless.ts +11 -3
- package/src/plugins/builtin/options-positioning/model.ts +24 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
- package/src/plugins/builtin/options-scenario/client.ts +19 -1
- package/src/plugins/builtin/options-scenario/headless.ts +13 -6
- package/src/plugins/builtin/options-scenario/pane.tsx +13 -5
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/power/client.ts +2 -2
- package/src/plugins/builtin/power/pane.tsx +36 -31
- package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
- package/src/plugins/builtin/seasonality/client.ts +2 -1
- package/src/plugins/builtin/sectors/client.ts +4 -3
- package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
- package/src/plugins/builtin/shared/research-trading.tsx +2 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
- package/src/plugins/builtin/shared/volatility/options.ts +2 -1
- package/src/plugins/builtin/social-mentions/client.ts +2 -1
- package/src/plugins/builtin/vol-surface/client.ts +23 -3
- package/src/plugins/builtin/vol-surface/headless.ts +33 -14
- package/src/plugins/builtin/vol-surface/model.ts +30 -10
- package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
- package/src/plugins/builtin/vol-surface/stored.ts +34 -3
- package/src/plugins/builtin/vol-surface/tables.ts +14 -0
- package/src/renderers/dom/desktop/controls.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +2 -0
- package/src/renderers/dom/host/tabs.tsx +16 -5
- package/src/renderers/dom/host/top-surface.ts +127 -0
- package/src/renderers/dom/styles.css +39 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
- package/src/renderers/electrobun/view/news-source.ts +4 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
- package/src/renderers/opentui/start.tsx +7 -0
- package/src/sources/gloomberb-cloud/index.ts +7 -0
- package/src/sources/provider-router/index.ts +16 -0
- package/src/telemetry/crash-reports.ts +4 -0
- package/src/telemetry/update-failures-node.ts +59 -0
- package/src/telemetry/update-failures.ts +307 -0
- package/src/tickers/company-name-listing.ts +9 -7
- package/src/tickers/company-name.ts +43 -0
- package/src/tickers/listing-fate.ts +30 -0
- package/src/types/data-provider.ts +6 -0
- package/src/types/headless.ts +7 -0
- package/src/types/issuer-listings.ts +28 -0
- package/src/updater/failure.ts +163 -0
- package/src/updater/index.ts +87 -23
- package/src/utils/currency-pair.ts +26 -0
- package/src/utils/sec.ts +68 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unavailableText } from "../../../components/ui/status";
|
|
1
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
3
|
import { delayedFooterSegment, FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
|
|
3
4
|
import { CompositeChart } from "../../../components/chart/composite/composite-chart";
|
|
@@ -194,7 +195,7 @@ export function OptionsCalculatorPane({ focused, width, height }: PaneProps) {
|
|
|
194
195
|
const calculation = useMemo(() => {
|
|
195
196
|
const unavailable = seedError ?? (american ? dividendInput.error : null)
|
|
196
197
|
?? (surfaceSource && !draft.symbol ? "Choose an underlying ticker for surface IV." : null)
|
|
197
|
-
?? (surfaceSource && !surfaceResource.loading && surface?.volatility == null ? surface?.error ?? surfaceResource.error ?? "Surface IV
|
|
198
|
+
?? (surfaceSource && !surfaceResource.loading && surface?.volatility == null ? surface?.error ?? surfaceResource.error ?? unavailableText("Surface IV") : null);
|
|
198
199
|
if (unavailable || surfaceSource && surface?.volatility == null) {
|
|
199
200
|
return { valuation: null, implied: { volatility: null, note: null }, problem: unavailable };
|
|
200
201
|
}
|
|
@@ -400,7 +401,7 @@ function ContractPremiumChart({
|
|
|
400
401
|
width: number;
|
|
401
402
|
focused: boolean;
|
|
402
403
|
}) {
|
|
403
|
-
const empty = unavailable ? "Premium history
|
|
404
|
+
const empty = unavailable ? unavailableText("Premium history") : series.length < 1 ? "No premium history." : undefined;
|
|
404
405
|
return (
|
|
405
406
|
<Box flexDirection="column" paddingX={1} height={PREMIUM_CHART_ROWS} flexShrink={0}>
|
|
406
407
|
{empty && !loading ? (
|
|
@@ -2,6 +2,8 @@ import type { HeadlessPaneDefinition, HeadlessPaneEntry } from "../../../types/p
|
|
|
2
2
|
import { expirationOptionSeconds, expiryIsoDate, OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER } from "../../../utils/option-expiry";
|
|
3
3
|
import { formatUtcTime } from "../../../utils/utc-time";
|
|
4
4
|
import { loadGamma, loadOpenInterest, positioningSymbol, type GammaPayload, type OpenInterestPayload } from "./client";
|
|
5
|
+
import { listingFateReport, loadListingFate } from "../../../tickers/listing-fate";
|
|
6
|
+
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
5
7
|
import {
|
|
6
8
|
ALL_EXPIRIES,
|
|
7
9
|
expiryLabel,
|
|
@@ -10,6 +12,7 @@ import {
|
|
|
10
12
|
formatDistance,
|
|
11
13
|
formatGamma,
|
|
12
14
|
formatLevel,
|
|
15
|
+
formatPositioningSpot,
|
|
13
16
|
formatPayout,
|
|
14
17
|
formatRatio,
|
|
15
18
|
formatStrike,
|
|
@@ -49,7 +52,7 @@ function summary(openInterest: OpenInterestPayload, dealer: GammaPayload | null)
|
|
|
49
52
|
{ key: "oiDate", label: "Open interest as of", value: openInterest.oiDate },
|
|
50
53
|
];
|
|
51
54
|
const textEntries: HeadlessPaneEntry[] = [
|
|
52
|
-
{ label: "Spot", value: spot, formatted:
|
|
55
|
+
{ label: "Spot", value: spot, formatted: formatPositioningSpot(spot, spotAsOf) },
|
|
53
56
|
...(dealer ? [{ label: "Quotes as of", value: dealer.quotesAsOf, formatted: time(dealer.quotesAsOf) }] : []),
|
|
54
57
|
{ label: "Open interest as of", value: openInterest.oiDate, formatted: openInterest.oiDate ?? "--" },
|
|
55
58
|
];
|
|
@@ -118,7 +121,7 @@ export const optionsPositioningHeadless = (initialTab: PositioningTab): Headless
|
|
|
118
121
|
// VIX has no dealer gamma, so its expiries have no column for it.
|
|
119
122
|
const expiryGamma = !(allExpiries instanceof Error) && allExpiries.total ? netGammaByExpiry(allExpiries) : null;
|
|
120
123
|
const gammaColumn = allExpiries instanceof Error || !!allExpiries.total;
|
|
121
|
-
const
|
|
124
|
+
const loadErrors = [
|
|
122
125
|
...openInterest.warnings,
|
|
123
126
|
...(dealer instanceof Error ? [`Dealer gamma: ${dealer.message}`] : [
|
|
124
127
|
...dealer.warnings,
|
|
@@ -126,6 +129,11 @@ export const optionsPositioningHeadless = (initialTab: PositioningTab): Headless
|
|
|
126
129
|
]),
|
|
127
130
|
...(everyExpiry instanceof Error ? [`Net GEX by expiry: ${everyExpiry.message}`] : []),
|
|
128
131
|
];
|
|
132
|
+
// A symbol with no listed open interest says what became of it when something did (TWTR stopped
|
|
133
|
+
// trading, FB is a fund now) in place of the load's errors.
|
|
134
|
+
const fate = openInterest.expiries.length || expiry ? null
|
|
135
|
+
: listingFateReport(await loadListingFate(await resolveHeadlessInstrument(ctx, symbol), ctx.marketData), { served: false }, loadErrors);
|
|
136
|
+
const errors = fate?.errors ?? loadErrors;
|
|
129
137
|
const changes = openInterest.previousOiDate != null;
|
|
130
138
|
const gammaShown = dealer instanceof Error || !dealer.total ? null : dealer;
|
|
131
139
|
const scope = dealer instanceof Error ? null : dealer.expiry ? expiryLabel(dealer.expiry) : "all expiries";
|
|
@@ -173,7 +181,7 @@ export const optionsPositioningHeadless = (initialTab: PositioningTab): Headless
|
|
|
173
181
|
],
|
|
174
182
|
complete: !!openInterest.expiries.length && !(dealer instanceof Error) && errors.length === 0,
|
|
175
183
|
unavailableSymbols: openInterest.expiries.length ? [] : [symbol],
|
|
176
|
-
errors,
|
|
184
|
+
errors, ...(fate?.notes.length ? { notes: fate.notes } : {}),
|
|
177
185
|
metadata: {
|
|
178
186
|
underlying: openInterest.underlying,
|
|
179
187
|
oiDate: openInterest.oiDate,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatCompact, formatCompactAmount } from "../../../utils/format";
|
|
2
2
|
import { formatExpDate } from "../../../utils/options";
|
|
3
|
+
import { modelSpotLabel, modelSpotStamp, modelSpotStampVariants, observedModelSpot } from "../options/model-spot";
|
|
3
4
|
|
|
4
5
|
export type PositioningTab = "strikes" | "expiries" | "gex";
|
|
5
6
|
export const POSITIONING_TABS: { value: PositioningTab; label: string }[] = [
|
|
@@ -66,6 +67,29 @@ export function formatRatio(value: number | null | undefined): string {
|
|
|
66
67
|
export function formatLevel(value: number | null | undefined): string {
|
|
67
68
|
return value == null || !Number.isFinite(value) ? "--" : value.toFixed(2);
|
|
68
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* The spot Gloom Cloud read the distances and dealer gamma at, stamped as
|
|
72
|
+
* every options pane stamps its spot (`modelSpotStamp`), as a stat cell
|
|
73
|
+
* carries it: `pre-market Wed 21 Oct 09:35 UTC`, its shorter forms, then
|
|
74
|
+
* none, so the band keeps the columns it has in regular hours. Nothing for
|
|
75
|
+
* the regular session's price.
|
|
76
|
+
*/
|
|
77
|
+
export function positioningSpotDetail(spot: number | null | undefined, spotAsOf: string | null | undefined):
|
|
78
|
+
{ detail?: string; detailVariants?: string[] } {
|
|
79
|
+
const at = spotAsOf ? Date.parse(spotAsOf) : Number.NaN;
|
|
80
|
+
if (spot == null || !Number.isFinite(at)) return {};
|
|
81
|
+
const observed = observedModelSpot(spot, at);
|
|
82
|
+
const stamp = modelSpotStamp(observed);
|
|
83
|
+
return stamp ? { detail: stamp, detailVariants: [...modelSpotStampVariants(observed), ""] } : {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The spot as a report says it: `770.88 pre-market Wed 21 Oct 09:35 UTC`, `770.10 last Wed 21 Oct 14:05 UTC`. */
|
|
87
|
+
export function formatPositioningSpot(spot: number | null | undefined, spotAsOf: string | null | undefined): string {
|
|
88
|
+
const at = spotAsOf ? Date.parse(spotAsOf) : Number.NaN;
|
|
89
|
+
if (spot == null || !Number.isFinite(spot)) return "--";
|
|
90
|
+
return Number.isFinite(at) ? `${formatLevel(spot)} ${modelSpotLabel(observedModelSpot(spot, at))}` : formatLevel(spot);
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
/** Where a level sits against spot: +0.6%. */
|
|
70
94
|
export function formatDistance(level: number | null | undefined, spot: number | null | undefined): string {
|
|
71
95
|
if (level == null || spot == null || !(spot > 0)) return "--";
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
formatDistance,
|
|
58
58
|
formatGamma,
|
|
59
59
|
formatLevel,
|
|
60
|
+
positioningSpotDetail,
|
|
60
61
|
formatPayout,
|
|
61
62
|
formatRatio,
|
|
62
63
|
formatStrike,
|
|
@@ -204,6 +205,7 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
|
|
|
204
205
|
const openInterest = useAsyncResource<OpenInterestPayload>(openInterestLoader, { keepPreviousData: true });
|
|
205
206
|
const data = openInterest.data;
|
|
206
207
|
const spot = data?.spot ?? null;
|
|
208
|
+
const spotStamp = useMemo(() => positioningSpotDetail(spot, data?.spotAsOf), [spot, data?.spotAsOf]);
|
|
207
209
|
const expiries = data?.expiries ?? [];
|
|
208
210
|
const shownExpiry = data?.expiry ?? requestedExpiry;
|
|
209
211
|
const changes = data?.previousOiDate != null;
|
|
@@ -264,9 +266,7 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
|
|
|
264
266
|
enabled: expiryIndex < expiryDates.length - 1, onSelect: () => stepExpiry(1) },
|
|
265
267
|
], [expiryDates.length, expiryIndex, stepExpiry, tab]);
|
|
266
268
|
|
|
267
|
-
const { strip: tabStrip, rows: tabRows } = usePaneTabs(
|
|
268
|
-
? { tabs, activeValue: tab, onSelect: (value: string) => setTab(value), focused, dense: true }
|
|
269
|
-
: null);
|
|
269
|
+
const { strip: tabStrip, rows: tabRows } = usePaneTabs({ tabs, activeValue: tab, onSelect: (value: string) => setTab(value), focused, dense: true });
|
|
270
270
|
const bodyHeight = Math.max(3, height - tabRows);
|
|
271
271
|
|
|
272
272
|
// Footer: what the data is (delayed or real time, the OI session), and its gaps.
|
|
@@ -335,13 +335,13 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
|
|
|
335
335
|
const strikeFigures = useMemo<StatItem[]>(() => !callTotal ? [] : [
|
|
336
336
|
{ id: "max-pain", label: "Max pain", value: maxPain == null ? "--" : formatStrike(maxPain), color: colors.warning,
|
|
337
337
|
detail: maxPain == null ? undefined : `${formatDistance(maxPain, spot)} vs spot` },
|
|
338
|
-
{ id: "spot", label: "Spot", value: formatLevel(spot) },
|
|
338
|
+
{ id: "spot", label: "Spot", value: formatLevel(spot), ...spotStamp },
|
|
339
339
|
{ id: "pc", label: "P/C OI", value: formatRatio(callTotal.putCallRatio) },
|
|
340
340
|
{ id: "calls", label: "Call OI", value: formatCompact(callTotal.callOI), color: colors.positive,
|
|
341
341
|
detail: callTotal.callChange == null ? undefined : formatCountChange(callTotal.callChange) },
|
|
342
342
|
{ id: "puts", label: "Put OI", value: formatCompact(callTotal.putOI), color: colors.negative,
|
|
343
343
|
detail: callTotal.putChange == null ? undefined : formatCountChange(callTotal.putChange) },
|
|
344
|
-
], [callTotal, colors.negative, colors.positive, colors.warning, maxPain, spot]);
|
|
344
|
+
], [callTotal, colors.negative, colors.positive, colors.warning, maxPain, spot, spotStamp]);
|
|
345
345
|
const selectedStrikeRow = strikes.find((row) => strikeKey(row) === strikeSelectedId) ?? null;
|
|
346
346
|
const strikeStrip = useMemo<ChartStripSpec | null>(() => strikeWindow.length < 3 ? null : {
|
|
347
347
|
label: "Open interest by strike",
|
|
@@ -404,9 +404,9 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
|
|
|
404
404
|
split: [{ id: "calls", value: calls, color: colors.positive }, { id: "puts", value: puts, color: colors.negative }] },
|
|
405
405
|
{ id: "pc", label: "P/C OI", value: formatRatio(calls > 0 ? puts / calls : null) },
|
|
406
406
|
{ id: "largest", label: "Largest expiry", value: expiryLabel(largest.date), detail: formatCompact(expirySize(largest)) },
|
|
407
|
-
{ id: "spot", label: "Spot", value: formatLevel(spot) },
|
|
407
|
+
{ id: "spot", label: "Spot", value: formatLevel(spot), ...spotStamp },
|
|
408
408
|
];
|
|
409
|
-
}, [changes, colors.negative, colors.positive, expiries, spot]);
|
|
409
|
+
}, [changes, colors.negative, colors.positive, expiries, spot, spotStamp]);
|
|
410
410
|
const selectedExpiryRow = expiries.find((row) => row.date === expirySelectedId) ?? null;
|
|
411
411
|
const expiryStrip = useMemo<ChartStripSpec | null>(() => expiries.length < 3 ? null : {
|
|
412
412
|
label: "Open interest by expiry",
|
|
@@ -479,7 +479,7 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
|
|
|
479
479
|
})),
|
|
480
480
|
...(band ? [{ id: "band", label: "Dealer range", value: `${formatGamma(band.low)} to ${formatGamma(band.high)}`,
|
|
481
481
|
detail: `${shares} with dealers${gammaData.boxes?.length ? ", boxes out" : ""}` }] : []),
|
|
482
|
-
{ id: "spot", label: "Spot", value: formatLevel(gammaSpot) },
|
|
482
|
+
{ id: "spot", label: "Spot", value: formatLevel(gammaSpot), ...positioningSpotDetail(gammaSpot, gammaData.spotAsOf) },
|
|
483
483
|
], [band, colors.negative, colors.positive, gammaData, gammaSpot, shares, walls]);
|
|
484
484
|
// The assumption rides in the legend, where the numbers it changes are read; the
|
|
485
485
|
// dealer range figure names its own share. It gives way before the series name does.
|
|
@@ -610,13 +610,11 @@ function OptionsPositioningView({ width, height, focused, symbol }: PaneProps &
|
|
|
610
610
|
}
|
|
611
611
|
|
|
612
612
|
return <Box width={width} height={height} flexDirection="column">
|
|
613
|
+
{tabStrip}
|
|
613
614
|
<PaneStatusBody loading={openInterest.loading && !data} error={!data ? openInterest.error : null}
|
|
614
615
|
empty={!!data && !data.expiries.length} subject="open interest"
|
|
615
616
|
emptyTitle={`No listed options for ${parsePublicTickerKey(symbol).symbol.replace(/^\^/, "")}.`}>
|
|
616
|
-
{data?.expiries.length ?
|
|
617
|
-
{tabStrip}
|
|
618
|
-
{content}
|
|
619
|
-
</> : null}
|
|
617
|
+
{data?.expiries.length ? content : null}
|
|
620
618
|
</PaneStatusBody>
|
|
621
619
|
</Box>;
|
|
622
620
|
}
|
|
@@ -13,7 +13,7 @@ import type { YieldPoint } from "../yield-curve/treasury-data";
|
|
|
13
13
|
import { parseLegs, scenarioCurrency, validatePosition, type ScenarioControls, type ScenarioLeg, type ScenarioPosition } from "./model";
|
|
14
14
|
import { loadModelDividendYield, type ModelDividendYield } from "../../../market-data/model-dividend-yield";
|
|
15
15
|
import { abortable, abortError } from "../../../utils/async-deadline";
|
|
16
|
-
import { extendedSession } from "../options/model-spot";
|
|
16
|
+
import { extendedSession, modelSpotStamp, modelSpotStampVariants, type OptionModelSpot } from "../options/model-spot";
|
|
17
17
|
import { errorMessage } from "../../../utils/errors";
|
|
18
18
|
|
|
19
19
|
export interface ScenarioMarketSnapshot {
|
|
@@ -191,6 +191,24 @@ export function scenarioSpotBasis(
|
|
|
191
191
|
return (market?.underlyingQuote ? extendedSession(market.underlyingQuote) : null) ?? "last price";
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
const EXTENDED_BASES: readonly string[] = ["pre-market", "after hours", "overnight"];
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A spot basis as the pane and report name it: an extended-session print with
|
|
198
|
+
* its time, stamped as every options pane stamps its spot (`modelSpotStamp`),
|
|
199
|
+
* `pre-market Wed 21 Oct 09:42 UTC`; any other basis as it is.
|
|
200
|
+
*/
|
|
201
|
+
export function scenarioSpotStamp(basis: string, observedAt: number): string {
|
|
202
|
+
if (!EXTENDED_BASES.includes(basis)) return basis;
|
|
203
|
+
return modelSpotStamp({ basis: "last", session: basis as OptionModelSpot["session"], observedAt }) ?? basis;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** That stamp's shorter forms, longest first (`modelSpotStampVariants`): `pre-market 09:42 UTC`, `pre-market`. */
|
|
207
|
+
export function scenarioSpotStampVariants(basis: string, observedAt: number): string[] {
|
|
208
|
+
if (!EXTENDED_BASES.includes(basis)) return [];
|
|
209
|
+
return modelSpotStampVariants({ basis: "last", session: basis as OptionModelSpot["session"], observedAt });
|
|
210
|
+
}
|
|
211
|
+
|
|
194
212
|
function supplied(value: unknown): boolean { return value != null && value !== ""; }
|
|
195
213
|
|
|
196
214
|
/** A position typed whole: its legs and the spot, rate and dividend yield they are valued at, so no market data is read. */
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
7
7
|
import {
|
|
8
8
|
createScenarioDependencies, loadScenarioMarket, SCENARIO_STRATEGIES, scenarioControlsFromSettings, scenarioPositionFromSettings,
|
|
9
|
-
scenarioSpotBasis, suppliedScenarioInputs, type ScenarioMarketSnapshot,
|
|
9
|
+
scenarioSpotBasis, scenarioSpotStamp, suppliedScenarioInputs, type ScenarioMarketSnapshot,
|
|
10
10
|
} from "./client";
|
|
11
11
|
import { scenarioFreshness } from "./freshness";
|
|
12
12
|
import { cashSecuredPut, cspReportEntries, loadNextEarningsDate } from "./csp";
|
|
@@ -15,6 +15,7 @@ import { buildScenario, currencyLabel, parseLegs, scenarioValueUnit } from "./mo
|
|
|
15
15
|
import { hedgeInputsFromSettings, hedgeReportEntries, scenarioHedgeBudget } from "./hedge";
|
|
16
16
|
import { collarReportEntries, scenarioCollar } from "./collar";
|
|
17
17
|
import { modelDividendYieldNote } from "../../../market-data/model-dividend-yield";
|
|
18
|
+
import { listingFateEmpty, loadListingFate } from "../../../tickers/listing-fate";
|
|
18
19
|
|
|
19
20
|
// Float noise at a flat origin or a breakeven prints as 0.00, not -0.00.
|
|
20
21
|
const money = (value: unknown) => typeof value === "number" ? (Math.abs(value) < 0.005 ? 0 : value).toFixed(2) : "--";
|
|
@@ -82,12 +83,13 @@ export const optionsScenarioHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
82
83
|
hedgeInputsFromSettings(settings);
|
|
83
84
|
const suppliedInputs = suppliedScenarioInputs(settings);
|
|
84
85
|
let market: ScenarioMarketSnapshot;
|
|
86
|
+
let instrument: Awaited<ReturnType<typeof resolveHeadlessInstrument>> | null = null;
|
|
85
87
|
if (suppliedInputs) {
|
|
86
88
|
// An explicit position can be valued offline and must never depend on unrelated market requests.
|
|
87
89
|
market = { ...parsePublicTickerKey(symbol), spot: null, currency: "", asOf: Date.now(), chain: null, expirationDates: [], rate: null,
|
|
88
90
|
dividendYield: null, source: null, underlyingQuote: null, rateAsOf: [], warnings: [] };
|
|
89
91
|
} else {
|
|
90
|
-
|
|
92
|
+
instrument = await resolveHeadlessInstrument(ctx, symbol);
|
|
91
93
|
const typedExpiries = typeof settings.legs === "string" && settings.legs.trim() ? parseLegs(settings.legs).map((leg) => leg.expiration) : [];
|
|
92
94
|
const expiration = readOptionExpiration(args.options.expiration);
|
|
93
95
|
market = await loadScenarioMarket({ instrument, expiration: expiration ?? undefined,
|
|
@@ -104,8 +106,13 @@ export const optionsScenarioHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
104
106
|
// A typed rate or yield stands in for the source that could not supply one.
|
|
105
107
|
const marketWarnings = market.warnings.filter((warning) => !(supplied(settings.dividendYield) && warning.startsWith("Dividend yield unavailable"))
|
|
106
108
|
&& !(supplied(settings.rate) && warning.startsWith("Treasury")));
|
|
107
|
-
const
|
|
109
|
+
const loadErrors = scenario ? [...new Set([...marketWarnings, ...scenario.warnings])]
|
|
108
110
|
: [...marketWarnings, "No position supplied; add --legs or select --strategy"];
|
|
111
|
+
// A symbol with no option chain at all says what became of it when something did (TWTR stopped
|
|
112
|
+
// trading, FB is a fund now) in place of the chain's errors.
|
|
113
|
+
const fate = instrument && !market.chain && !scenario
|
|
114
|
+
? listingFateEmpty(await loadListingFate(instrument, ctx.marketData, market.underlyingQuote ? { quote: market.underlyingQuote } : {}), loadErrors) : null;
|
|
115
|
+
const errors = fate?.errors ?? loadErrors;
|
|
109
116
|
const midVolatility = !!position?.legs.some((leg) => leg.volatilitySource === "mid");
|
|
110
117
|
// A seeded strategy starts at the spot its quote mids imply; the last print is reported beside it.
|
|
111
118
|
const lastPrint = midVolatility && !supplied(settings.spot) && market.spot != null && money(market.spot) !== money(position!.spot)
|
|
@@ -135,9 +142,9 @@ export const optionsScenarioHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
135
142
|
...(midVolatility ? { volatilityFrom: leg.volatilitySource === "mid" ? "quote mid" : "provider" } : {}) })) },
|
|
136
143
|
{ title: "Inputs", entries: [
|
|
137
144
|
{ key: "spot", label: lastPrint != null ? "Spot implied by option quotes" : "Spot", value: position!.spot,
|
|
138
|
-
formatted: `${money(position!.spot)} ${PRICE_UNIT}${lastPrint == null && spotBasis !== "last price" ? ` (${spotBasis})` : ""}` },
|
|
145
|
+
formatted: `${money(position!.spot)} ${PRICE_UNIT}${lastPrint == null && spotBasis !== "last price" ? ` (${scenarioSpotStamp(spotBasis!, position!.asOf)})` : ""}` },
|
|
139
146
|
...(lastPrint != null ? [{ key: "last", label: "Last price", value: lastPrint,
|
|
140
|
-
formatted: `${money(lastPrint)} ${PRICE_UNIT}${lastSession && lastSession !== "last price" ? ` (${lastSession})` : ""}` }] : []),
|
|
147
|
+
formatted: `${money(lastPrint)} ${PRICE_UNIT}${lastSession && lastSession !== "last price" ? ` (${scenarioSpotStamp(lastSession, market.asOf)})` : ""}` }] : []),
|
|
141
148
|
...(position!.underlying ? [{ key: "shares", label: "Shares held", value: position!.underlying.quantity,
|
|
142
149
|
formatted: `${position!.underlying.quantity.toLocaleString("en-US")} at ${money(position!.underlying.price)} ${PRICE_UNIT}` }] : []),
|
|
143
150
|
{ key: "asOf", label: "Valued at", value: new Date(position!.asOf).toISOString(), formatted: formatSpotTime(position!.asOf) },
|
|
@@ -167,7 +174,7 @@ export const optionsScenarioHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
167
174
|
...Object.fromEntries(row.values.map((value, index) => [`date${index}`, value])) })) },
|
|
168
175
|
] : [],
|
|
169
176
|
complete: scenario != null && errors.length === 0,
|
|
170
|
-
unavailableSymbols: scenario ? [] : [symbol], errors,
|
|
177
|
+
unavailableSymbols: scenario ? [] : [symbol], errors, ...(fate?.empty ? { empty: fate.empty } : {}),
|
|
171
178
|
metadata: { scenario, position, controls, market, ...(hedge ? { hedge } : {}), ...(csp ? { csp } : {}), ...(collar ? { collar } : {}),
|
|
172
179
|
...(position ? { spot: { price: position.spot, basis: spotBasis, valuedAt: new Date(position.asOf).toISOString() } } : {}),
|
|
173
180
|
inputSource: suppliedInputs ? "user" : "market",
|
|
@@ -26,7 +26,7 @@ import { buildOptionQuoteKey, freshOptionQuote, OPTIONS_QUOTE_EXCHANGE } from ".
|
|
|
26
26
|
import { liveScenarioPosition, scenarioLegContractSymbol } from "./live";
|
|
27
27
|
import { ScenarioPayoffChart } from "./charts";
|
|
28
28
|
import { ScenarioLegEditor, ScenarioSaveForm, ScenarioInputsForm } from "./editor";
|
|
29
|
-
import { loadScenarioMarket, scenarioControlsFromSettings, scenarioPositionFromSettings, scenarioSpotBasis, suppliedScenarioInputs,
|
|
29
|
+
import { loadScenarioMarket, scenarioControlsFromSettings, scenarioPositionFromSettings, scenarioSpotBasis, scenarioSpotStamp, scenarioSpotStampVariants, suppliedScenarioInputs,
|
|
30
30
|
type ScenarioMarketSnapshot } from "./client";
|
|
31
31
|
import { scenarioFreshness } from "./freshness";
|
|
32
32
|
import { cashSecuredPut, cspStatItems, loadNextEarningsDate } from "./csp";
|
|
@@ -294,11 +294,15 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
294
294
|
const midLegs = live || !position ? 0 : position.legs.filter((leg) => leg.volatilitySource === "mid").length;
|
|
295
295
|
// A seed starts at the spot its quote mids imply, so name the last print when it differs.
|
|
296
296
|
const lastPrint = midLegs && (settings.spot == null || settings.spot === "") && market?.spot != null
|
|
297
|
-
&& money(market.spot) !== money(position!.spot)
|
|
297
|
+
&& money(market.spot) !== money(position!.spot)
|
|
298
|
+
? ` · ${(market.underlyingQuote ? extendedSession(market.underlyingQuote) : null) ?? "last"} ${money(market.spot)}` : "";
|
|
298
299
|
const midVolatility = !midLegs ? "" : (midLegs === position!.legs.length ? " · IV from quote mid"
|
|
299
300
|
: ` · IV from quote mid on ${midLegs} of ${position!.legs.length} legs`) + lastPrint;
|
|
300
301
|
// The spot every figure is valued at, and when: the live print, the spot the quote mids imply, a typed what-if or the last price.
|
|
301
|
-
|
|
302
|
+
// An extended-session print carries its own time in the stamp.
|
|
303
|
+
const spotStamp = position && spotBasis && spotBasis !== "last price" ? scenarioSpotStamp(spotBasis, position.asOf) : null;
|
|
304
|
+
const spotText = position ? `spot ${money(position.spot)}${spotStamp ? ` ${spotStamp}` : ""}` : "";
|
|
305
|
+
const spotTimed = !!spotStamp && spotStamp !== spotBasis;
|
|
302
306
|
// A narrow row keeps the spot and its time before the rest.
|
|
303
307
|
const shortSpotText = position ? `spot ${money(position.spot)} · ${formatSpotTime(live?.observedAt ?? position.asOf)}` : "";
|
|
304
308
|
usePaneFooter("osa", () => ({ info: [
|
|
@@ -307,7 +311,7 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
307
311
|
...(hedge.error ? [{ id: "hedge-error", parts: [{ text: hedge.error, tone: "warning" as const }] }] : []),
|
|
308
312
|
...(live ? splitDelayedStatus({ id: "asof", parts: [{ text: `${live.basis} · ${spotText} · ${new Date(live.observedAt).toISOString().slice(11, 19)} UTC`, tone: "muted" }],
|
|
309
313
|
variants: [[{ text: shortSpotText, tone: "muted" }]] })
|
|
310
|
-
: position ? [{ id: "asof", parts: [{ text: `${spotText} · ${formatSpotTime(position.asOf)} · ${market?.source ? "market" : "input assumptions"}${midVolatility}`, tone: "muted" as const }],
|
|
314
|
+
: position ? [{ id: "asof", parts: [{ text: `${spotText}${spotTimed ? "" : ` · ${formatSpotTime(position.asOf)}`} · ${market?.source ? "market" : "input assumptions"}${midVolatility}`, tone: "muted" as const }],
|
|
311
315
|
variants: [[{ text: shortSpotText, tone: "muted" as const }]] }] : []),
|
|
312
316
|
], hints }), [hints, position, market?.source, resource.loading, error, hedge.error, live?.basis, live?.observedAt, midVolatility, spotText, shortSpotText]);
|
|
313
317
|
useScenarioEvidence({ scenario, view: tab, loading: !!resource.loading && !scenario, error: error ?? (snapshotErrors.join("; ") || null), notices });
|
|
@@ -337,7 +341,11 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
337
341
|
tone: cents(scenario.valuation.pnl) >= 0 ? "positive" : "negative" },
|
|
338
342
|
// A spot other than the last price says what it is: the one the quote mids imply, or a typed what-if.
|
|
339
343
|
{ id: "spot", label: "Spot", value: money(scenario.position.spot),
|
|
340
|
-
detail: spotBasis === "implied by option quotes" ? "per share, implied" : spotBasis === "typed" ? "per share, typed"
|
|
344
|
+
detail: spotBasis === "implied by option quotes" ? "per share, implied" : spotBasis === "typed" ? "per share, typed"
|
|
345
|
+
: spotTimed ? `per share, ${spotStamp}` : "per share",
|
|
346
|
+
// The band keeps its columns: the stamp's day, its time, then the stamp give way first.
|
|
347
|
+
...(spotTimed ? { detailVariants: [...scenarioSpotStampVariants(spotBasis!, scenario.position.asOf)
|
|
348
|
+
.map((form) => `per share, ${form}`), "per share"] } : {}) },
|
|
341
349
|
{ id: "value", label: "Value", value: money(scenario.valuation.price), detail: valueUnit },
|
|
342
350
|
{ id: "max-profit", label: "Max profit", value: risk?.unlimitedProfit ? "Unlimited" : money(risk?.maxProfit ?? null) },
|
|
343
351
|
{ id: "max-loss", label: "Max loss", value: risk?.unlimitedLoss ? "Unlimited" : money(risk?.maxLoss ?? null) },
|
|
@@ -31,6 +31,7 @@ import { isPlainKeyboardEvent } from "../../../utils/keyboard";
|
|
|
31
31
|
import { formatRelativeAge } from "../../../utils/datetime-format";
|
|
32
32
|
import { displayWidth } from "../../../utils/format";
|
|
33
33
|
import { requiredGloomberb } from "../../../utils/semver";
|
|
34
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
34
35
|
import { VERSION } from "../../../version";
|
|
35
36
|
import { getCurrentPluginTarget, runsExternalPlugins } from "../../current-target";
|
|
36
37
|
import { pluginSetupCommandId } from "../../registry/setup-command";
|
|
@@ -483,7 +484,7 @@ export function PluginMarketplacePane({ focused, width, height }: PaneProps) {
|
|
|
483
484
|
void check(unlistedKey.split(","))
|
|
484
485
|
// Offline, or a private repository this machine has no credentials for:
|
|
485
486
|
// the row keeps whatever it knew, without an error in the way.
|
|
486
|
-
.catch((
|
|
487
|
+
.catch(logFailure("plugin-marketplace.pane", "Check installed plugin remote heads failed", {}))
|
|
487
488
|
.then((heads) => {
|
|
488
489
|
if (cancelled) return;
|
|
489
490
|
setRemoteHeads(heads);
|
|
@@ -638,8 +639,8 @@ export function PluginMarketplacePane({ focused, width, height }: PaneProps) {
|
|
|
638
639
|
if (!confirmed) return;
|
|
639
640
|
setBusy({ id: entry.id, verb: "removing" });
|
|
640
641
|
setLastError(null);
|
|
641
|
-
await host.deactivate(entry.id, entry.directory).catch((
|
|
642
|
-
await manager.deactivate?.(entry.id).catch((
|
|
642
|
+
await host.deactivate(entry.id, entry.directory).catch(logFailure("plugin-marketplace.pane", "Deactivate plugin view before removal failed"));
|
|
643
|
+
await manager.deactivate?.(entry.id).catch(logFailure("plugin-marketplace.pane", "Deactivate plugin host before removal failed"));
|
|
643
644
|
const result = await manager.remove(entry.directory!);
|
|
644
645
|
setBusy(null);
|
|
645
646
|
bump();
|
|
@@ -735,7 +736,7 @@ export function PluginMarketplacePane({ focused, width, height }: PaneProps) {
|
|
|
735
736
|
}))}
|
|
736
737
|
/>
|
|
737
738
|
),
|
|
738
|
-
}).catch((
|
|
739
|
+
}).catch(logFailure("plugin-marketplace.pane", "Choose starter pack failed", null));
|
|
739
740
|
const pack = STARTER_PACKS.find((candidate) => candidate.id === id);
|
|
740
741
|
if (pack) await applyPack(pack);
|
|
741
742
|
}, [applyPack, currentPack, dialog, disabledPlugins, host]);
|
|
@@ -37,7 +37,7 @@ export const FILE_OPTION = {
|
|
|
37
37
|
export const SYMBOL_LIST_HELP = {
|
|
38
38
|
title: "Several symbols",
|
|
39
39
|
lines: [
|
|
40
|
-
"add and remove take any number of symbols split by spaces, commas or newlines, a --file, or piped input (or -). An entry of a list with commas or newlines that has spaces in it is a company's name when search finds one, read as its US listing first with a line under it saying which,
|
|
40
|
+
"add and remove take any number of symbols split by spaces, commas or newlines, a --file, or piped input (or -). An entry of a list with commas, semicolons or newlines that has spaces in it is a company's name when search finds one (philip morris intl is PM), read as its US listing first with a line under it saying which, else the symbols it holds when each is written as a symbol or has a quote (AAPL MSFT), and otherwise a name not found. A file or piped list reads each line as such an entry. A CSV is read from its Symbol, Ticker or Code column, else its Name or Company column as names. Without that header every cell is a symbol, unless names, prices or weights sit beside them: then the first column is read and the cells left out are named. Quote a list name with spaces.",
|
|
41
41
|
"Each symbol gets a line, then a summary. Exit status 0 when at least one symbol was added (or removed) or already was, 1 when none resolved or the usage is wrong.",
|
|
42
42
|
"A bare US-style symbol is not added when the listing it matched stopped trading, is abroad (DFS as DFS:LSE) or was taken by another company in the last two years (BRKL), or when nothing lists under it now and its company trades as another. Those are listed under Needs a choice, with the add for what their company trades as now and the listing that matched, which naming keeps; one symbol alone fails with the same. A symbol another company took longer ago says so under its line.",
|
|
43
43
|
"add --dry-run looks every symbol up and prints the same lines without saving.",
|
|
@@ -218,15 +218,18 @@ export async function requireSymbolList(
|
|
|
218
218
|
readEntries?: (entries: string[]) => Promise<string[]>,
|
|
219
219
|
): Promise<{ symbols: string[]; repeated: number; notes: string[]; single: string | null }> {
|
|
220
220
|
try {
|
|
221
|
-
|
|
221
|
+
// The first entries read are what was typed; a file's and standard input's come after.
|
|
222
|
+
let typed: string[] | null = null;
|
|
222
223
|
const list = await readSymbolList({ args, file, readEntries: async (entries) => {
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
const read = readEntries ? await readEntries(entries) : splitSymbolWords(entries);
|
|
225
|
+
typed ??= read;
|
|
226
|
+
return read;
|
|
225
227
|
} });
|
|
226
228
|
if (list.symbols.length === 0) ctx.fail(usage);
|
|
227
229
|
// One symbol typed alone keeps the single-symbol output and errors. It goes as typed, so a separator
|
|
228
230
|
// left on it (`UPST;`) is named by the listing it resolves to.
|
|
229
|
-
const
|
|
231
|
+
const entries: string[] = typed ?? [];
|
|
232
|
+
const single = file == null && !args.includes("-") && entries.length === 1 ? (args.length === 1 ? args[0]!.trim() : entries[0]!) : null;
|
|
230
233
|
return { ...list, single };
|
|
231
234
|
} catch (error) {
|
|
232
235
|
if (error instanceof SymbolListError) ctx.fail(error.message, usage);
|
|
@@ -106,11 +106,11 @@ export async function fetchPowerTickerScopes(symbol: string, tab: PowerTab, cont
|
|
|
106
106
|
}
|
|
107
107
|
const key = (query: unknown, scope: string) => `${scope}:${JSON.stringify(query)}`;
|
|
108
108
|
export const loadPowerBoard = (filter: PowerFilter, scope: string, force = false, signal?: AbortSignal) =>
|
|
109
|
-
loadCloudResource(powerBoardCache, key(filter, scope), () => fetchPowerBoard(filter, apiClient, signal), { force, validate: validatePowerBoard });
|
|
109
|
+
loadCloudResource(powerBoardCache, key(filter, scope), () => fetchPowerBoard(filter, apiClient, signal), { force, validate: validatePowerBoard, signal });
|
|
110
110
|
export const loadPowerHistory = (filter: PowerFilter, scope: string, force = false) =>
|
|
111
111
|
loadCloudResource(powerHistoryCache, key(filter, scope), () => fetchPowerHistory(filter), { force, validate: validatePowerHistory });
|
|
112
112
|
export const loadPowerDetail = (id: string, scope: string, force = false, offset = 0, signal?: AbortSignal) => loadCloudResource(powerDetailCache, key({ id, offset }, scope),
|
|
113
113
|
async () => { const value = validateDetail(await apiClient.getCloudPowerProject(id, { offset, limit: 100 }, signal));
|
|
114
114
|
if (value.hasMore && value.nextOffset! <= offset) throw new Error("Project revision pagination did not advance.");
|
|
115
115
|
if (value.project && value.project.id !== id || value.revisions.some((r) => r.id !== id)) throw new Error("Project evidence belongs to a different project.");
|
|
116
|
-
return value; }, { force, validate: validateDetail });
|
|
116
|
+
return value; }, { force, validate: validateDetail, signal });
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
|
2
2
|
import type { PowerBoard, PowerFilter, PowerHistory } from "../../../api-client/power";
|
|
3
3
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
4
4
|
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
5
|
-
import { Button, ChartTableHeader, DataTableStackView, EmptyState,
|
|
5
|
+
import { Button, ChartTableHeader, DataTableStackView, EmptyState, QueryBar, useChartTableSelection, usePagedRows, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, useQueryBarSearch, useTableLoadMore, type DataTableCell, type PaneHint, type QueryBarFilter } from "../../../components";
|
|
6
6
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
7
7
|
import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
|
|
8
8
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
@@ -97,6 +97,10 @@ function PowerView({ width, height, focused, scope, needsVerification }: PanePro
|
|
|
97
97
|
}, [filter, scope, projectTab, snapshot]);
|
|
98
98
|
const pages = usePagedRows(loader, { getId: (row) => row.id });
|
|
99
99
|
const board = isAccessDenied(pages.error) || isAccessDenied(pages.moreError) ? undefined : pages.pages[0]?.payload;
|
|
100
|
+
// A tab or filter change starts its list over; the filter menus keep the last board's choices until the new one answers.
|
|
101
|
+
const lastFilters = useRef(board?.filters);
|
|
102
|
+
if (board) lastFilters.current = board.filters;
|
|
103
|
+
const filterChoices = board?.filters ?? lastFilters.current;
|
|
100
104
|
const historyLoader = useCallback(async ({ offset, force }: { offset: number; force: boolean }) => {
|
|
101
105
|
const result = snapshot?.history ? { payload: snapshot.history, stale: false, refreshError: null }
|
|
102
106
|
: await loadPowerHistory({ ...filter, offset, limit: 500 }, scope, force);
|
|
@@ -157,7 +161,8 @@ function PowerView({ width, height, focused, scope, needsVerification }: PanePro
|
|
|
157
161
|
const notices = [pages.moreError?.message, historyPages.moreError?.message, ...pages.pages.map((p) => p.refreshError), historyData?.refreshError,
|
|
158
162
|
board && tab !== "capacity" ? powerCoverageNote(board.coverage) : null].filter((v): v is string => !!v);
|
|
159
163
|
usePaneNoticeFooter({ registrationId: "power:notices", focused, notices: [...new Set(notices)], enabled: !openRow });
|
|
160
|
-
const
|
|
164
|
+
const signInWall = !board && isCloudSessionRequired(error);
|
|
165
|
+
const { strip, rows: tabRows } = usePaneTabs(!signInWall ? { tabs: [...POWER_TABS], activeValue: tab, onSelect: setTab, focused, dense: true } : null);
|
|
161
166
|
const baseColumns = tab === "capacity" && context === "generation" ? GENERATION_COLUMNS : tab === "capacity" && context === "utility" ? UTILITY_COLUMNS : projectTab ? PROJECT_COLUMNS : tab === "history" ? HISTORY_COLUMNS : tab === "outcomes" ? RATE_COLUMNS : tab === "utilities" ? EXPOSURE_COLUMNS : COVERAGE_COLUMNS;
|
|
162
167
|
const records = useMemo(() => rows.filter(isRecord), [rows]);
|
|
163
168
|
const columns = useMemo(() => withoutQuietColumns(baseColumns, records, (r, id) => r.cells[id] ?? "", QUIET_COLUMNS), [baseColumns, records]);
|
|
@@ -167,10 +172,10 @@ function PowerView({ width, height, focused, scope, needsVerification }: PanePro
|
|
|
167
172
|
const filters: QueryBarFilter[] = [
|
|
168
173
|
...(historical && ["queue", "history", "outcomes"].includes(tab) ? [{ id: "source", label: "History", value: sourceId, defaultValue: "", options: [{ value: "", label: "All benchmarks" }, { value: "us-lbnl-annual", label: "Annual benchmark" }, { value: "us-lbnl", label: "Project benchmark" }], onChange: setSourceId }] : []),
|
|
169
174
|
...(["loads", "utilities"].includes(tab) ? [{ id: "loadClass", label: "Loads", value: loadClass, defaultValue: "", options: [{ value: "", label: "All loads" }, { value: "datacenter", label: "Datacenters" }], onChange: setLoadClass }] : []),
|
|
170
|
-
{ id: "country", label: "Country", value: country, defaultValue: "", options: choices(
|
|
171
|
-
{ id: "region", label: "Region", value: region, defaultValue: "", options: choices(
|
|
172
|
-
...(!["loads", "utilities", "coverage"].includes(tab) ? [{ id: "fuel", label: "Fuel", value: fuel, defaultValue: "", options: choices(
|
|
173
|
-
...(tab !== "coverage" ? [{ id: "status", label: "Status", value: status, defaultValue: "", options: choices(
|
|
175
|
+
{ id: "country", label: "Country", value: country, defaultValue: "", options: choices(filterChoices?.countries), onChange: setCountry },
|
|
176
|
+
{ id: "region", label: "Region", value: region, defaultValue: "", options: choices(filterChoices?.regions), onChange: setRegion },
|
|
177
|
+
...(!["loads", "utilities", "coverage"].includes(tab) ? [{ id: "fuel", label: "Fuel", value: fuel, defaultValue: "", options: choices(filterChoices?.fuels), onChange: setFuel }] : []),
|
|
178
|
+
...(tab !== "coverage" ? [{ id: "status", label: "Status", value: status, defaultValue: "", options: choices(filterChoices?.statuses), onChange: setStatus }] : []),
|
|
174
179
|
...(["queue", "history", "outcomes"].includes(tab) ? [{ id: "historical", label: "Benchmark", kind: "toggle" as const, value: historical, defaultValue: false, onChange: setHistorical }] : []),
|
|
175
180
|
...(tab === "capacity" ? [{ id: "context", label: "Context", value: context, options: [{ value: "capacity", label: "Capacity" }, { value: "generation", label: "Generation" }, { value: "utility", label: "Utilities" }], onChange: setContext }] : []),
|
|
176
181
|
];
|
|
@@ -206,31 +211,31 @@ function PowerView({ width, height, focused, scope, needsVerification }: PanePro
|
|
|
206
211
|
color: column.id === "name" || column.id === "utility" || column.id === "capacityMw" || column.id === "requestedMw" ? colors.textBright
|
|
207
212
|
: ["location", "asOf", "observedAt", "scope", "proposedDate", "region", "country", "kind", "role", "period"].includes(column.id) ? colors.textDim : colors.text };
|
|
208
213
|
};
|
|
209
|
-
if (
|
|
214
|
+
if (signInWall) return <SignInWall placement="power-signin" action="view power and interconnection data" needsVerification={needsVerification} />;
|
|
215
|
+
// The strip and the query bar stay through every load: a tab or filter change waits in the table's body.
|
|
210
216
|
return <Box width={width} height={height} flexDirection="column">{strip}
|
|
211
|
-
<
|
|
212
|
-
{board ?
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
</PaneStatusBody>
|
|
217
|
+
<DataTableStackView<PowerRow> columns={columns} items={board ? items : []} getItemKey={rowId} focused={focused && !searchFocus.active}
|
|
218
|
+
status={board ? undefined : { loading: pages.loading, error, subject: "power and interconnection data" }}
|
|
219
|
+
rootWidth={width} rootHeight={bodyHeight} scrollRef={scrollRef} onBodyScrollActivity={loadMore} resetScrollKey={`${tab}:${queryKey}`}
|
|
220
|
+
selection={{ kind: "id", selectedId: selected?.id ?? null, getId: rowId, onChange: setSelectedId }}
|
|
221
|
+
onActivate={(row) => row.locked ? upgrade() : setOpenId(row.id)} detailOpen={!!openRow} onBack={() => setOpenId(null)} detailTitle={openRow?.label}
|
|
222
|
+
detailScrollRef={detailScrollRef} detailContent={openRow ? <PowerDetailView row={openRow} scope={scope} scrollRef={detailScrollRef} snapshot={!!snapshot} focused={focused} width={width} height={bodyHeight - 1} /> : null}
|
|
223
|
+
renderCell={cell} freezeFirstColumn showHorizontalScrollbar selectedTextOverridesCellColor
|
|
224
|
+
isNavigable={isRecord} renderSectionHeader={(row) => row.section ? { text: `${row.label} (${row.section.count})` } : null}
|
|
225
|
+
sortColumnId={projectTab ? sort.column ?? null : null} sortDirection={sort.direction}
|
|
226
|
+
isColumnSortable={(column) => projectTab && POWER_SORT_COLUMNS.includes(column.id)}
|
|
227
|
+
onHeaderClick={projectTab ? (column) => { if (POWER_SORT_COLUMNS.includes(column)) setSort((old) => ({ column: column as PowerFilter["sort"], direction: old.column === column && old.direction === "desc" ? "asc" : "desc" })); } : undefined}
|
|
228
|
+
rootBefore={<ChartTableHeader width={width} height={bodyHeight} tableRows={items.length} tableColumns={columns} query={query}
|
|
229
|
+
figures={projectTab && tab === "queue" && board?.access === "full" ? queueFigures : undefined}
|
|
230
|
+
chart={board && tab === "history" ? { series, ...chartLink, formatValue: (n) => `${powerNumber(n)} MW`, loading: historyPages.loading || historyPages.hasMore && !historyPages.moreError,
|
|
231
|
+
empty: series.length ? undefined : historyPages.moreError ? "Full history could not load. Refresh to retry." : historyData?.payload.access === "preview" ? "Full history is available with Pro." : "History is accumulating from recorded snapshots.", remoteKind: "power-history" } : null} />}
|
|
232
|
+
emptyStateTitle={tab === "history" ? historyPages.error?.message ?? "No recorded history for these filters." : "No reported records match these filters."}
|
|
233
|
+
emptyContent={tickerMessage ? <Box paddingX={1} paddingY={1}>
|
|
234
|
+
<EmptyState title={tickerMessage.title} hint={tickerMessage.hint || undefined}
|
|
235
|
+
actions={tickerMessage.found.length ? tickerMessage.found.map((scope) => <Button key={`${scope.tab}:${scope.context}`} label={`Open ${scope.view}`}
|
|
236
|
+
onPress={() => { if (scope.tab === "capacity") setContext(scope.context); setTab(scope.tab); }} />) : undefined} />
|
|
237
|
+
</Box> : undefined}
|
|
238
|
+
bodyAfter={locked && desktop ? <LockedOverlay rows={Math.min(3, locked)} text="Unlock full power data with Pro" onPress={upgrade} role="power-upgrade" /> : undefined}
|
|
239
|
+
/>
|
|
235
240
|
</Box>;
|
|
236
241
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unavailableText } from "../../../components/ui/status";
|
|
1
2
|
import { getSharedMarketDataCoordinator, MarketDataCoordinator, resolveEntryValue } from "../../../market-data/coordinator";
|
|
2
3
|
import type { InstrumentRef } from "../../../market-data/request-types";
|
|
3
4
|
import type { DataProvider } from "../../../types/data-provider";
|
|
@@ -27,7 +28,7 @@ export async function loadReverseDcfInputs(
|
|
|
27
28
|
const entry = await abortable(coordinator.loadSnapshot(request.instrument, { forceRefresh: request.forceRefresh }), request.signal, CANCELLED);
|
|
28
29
|
const financials = resolveEntryValue(entry);
|
|
29
30
|
return { financials, stale: !!entry.error || !!financials?.fundamentals?.stale || (entry.staleAt != null && entry.staleAt <= now),
|
|
30
|
-
error: entry.error?.message ?? (financials ? null : "Fundamentals
|
|
31
|
+
error: entry.error?.message ?? (financials ? null : unavailableText("Fundamentals")), fetchedAt: entry.fetchedAt ?? now };
|
|
31
32
|
} catch (error) {
|
|
32
33
|
if (request.signal?.aborted || (error instanceof Error && error.name === "AbortError")) throw abortError(CANCELLED);
|
|
33
34
|
return { financials: null, stale: false, error: errorMessage(error), fetchedAt: now };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unavailableText } from "../../../components/ui/status";
|
|
1
2
|
import { atRegularSessionPrice } from "../../../market-data/quotes/regular-session-valuation";
|
|
2
3
|
import type { Fundamentals, TickerFinancials } from "../../../types/financials";
|
|
3
4
|
import { enterpriseValueCheck, enterpriseValueLabel, enterpriseValueReconciliationNote, enterpriseValueWithdrawn,
|
|
@@ -152,10 +153,10 @@ export function projectReverseDcf(
|
|
|
152
153
|
}
|
|
153
154
|
// A withdrawn EV is unavailable here as in `valuation`, which says why.
|
|
154
155
|
if (enterpriseValueWithdrawn(fundamentals)) {
|
|
155
|
-
return { ...empty, error: enterpriseValueReconciliationNote(fundamentals) ?? "Enterprise value
|
|
156
|
+
return { ...empty, error: enterpriseValueReconciliationNote(fundamentals) ?? unavailableText("Enterprise value") };
|
|
156
157
|
}
|
|
157
|
-
if (enterpriseValue == null || !(enterpriseValue > 0)) return { ...empty, error: "Enterprise value
|
|
158
|
-
if (freeCashFlow == null) return { ...empty, error: "Free cash flow
|
|
158
|
+
if (enterpriseValue == null || !(enterpriseValue > 0)) return { ...empty, error: unavailableText("Enterprise value") };
|
|
159
|
+
if (freeCashFlow == null) return { ...empty, error: unavailableText("Free cash flow") };
|
|
159
160
|
if (!(freeCashFlow > 0)) return { ...empty, error: "Free cash flow over the last twelve months is negative, so no growth rate prices it." };
|
|
160
161
|
const implied = solveImpliedGrowth(enterpriseValue, freeCashFlow, options.discountRate, TERMINAL_GROWTH);
|
|
161
162
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unavailableText } from "../../../components/ui/status";
|
|
1
2
|
import { getSharedMarketDataCoordinator, MarketDataCoordinator, resolveEntryValue } from "../../../market-data/coordinator";
|
|
2
3
|
import type { InstrumentRef } from "../../../market-data/request-types";
|
|
3
4
|
import type { ManualChartResolution } from "../../../time-series/resolution";
|
|
@@ -66,7 +67,7 @@ export async function loadSeasonalityHistory(
|
|
|
66
67
|
granularity: "resolution", resolution: SEASONALITY_HISTORY_RESOLUTION[cadence] }, { forceRefresh: request.forceRefresh }), request.signal, CANCELLED);
|
|
67
68
|
const history = resolveEntryValue(entry) ?? [];
|
|
68
69
|
return { history, stale: !!entry.error || (entry.staleAt != null && entry.staleAt <= now),
|
|
69
|
-
error: entry.error?.message ?? (history.length ? null :
|
|
70
|
+
error: entry.error?.message ?? (history.length ? null : unavailableText(cadence === "daily" ? "Daily price history" : "Monthly price history")),
|
|
70
71
|
fetchedAt: entry.fetchedAt ?? now };
|
|
71
72
|
} catch (error) {
|
|
72
73
|
if (request.signal?.aborted || (error instanceof Error && error.name === "AbortError")) throw abortError(CANCELLED);
|