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
|
@@ -2,10 +2,19 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
|
2
2
|
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
3
3
|
import { createRealizedVolatilityDependencies, loadRealizedVolatilityHistory } from "../realized-vol/client";
|
|
4
4
|
import { createHvDependencies, loadIvHistory, loadIvScreen, loadScreenVolatilities } from "./client";
|
|
5
|
-
import { formatPoints, formatRank, formatSpot, formatStat, formatVol, readingLabel, sharedDates, skewHeader, VCA_DEFINITIONS, verdictLabel } from "./format";
|
|
5
|
+
import { formatPoints, formatRank, formatSpot, formatStat, formatVol, IMPLIED_VOL_WORDS, readingLabel, sharedDates, skewHeader, VCA_DEFINITIONS, verdictLabel } from "./format";
|
|
6
6
|
import { HV_WINDOWS, type HvWindow, type IvLookback, type IvStatRow, projectIvHistory, projectRichCheap, type RichCheapRow, richCheapDates, screenCoverage, VCA_LIMIT, VCA_PRESETS } from "./model";
|
|
7
7
|
import { vcaUniverse } from "./universe";
|
|
8
|
-
import {
|
|
8
|
+
import { offsetText, releaseOffsetRows } from "./events";
|
|
9
|
+
import { loadMacroReleases } from "../macro-day/client";
|
|
10
|
+
import { MACRO_EVENT_KINDS, MACRO_EVENT_LABELS, MACRO_RELEASE_SOURCES, staleMacroReleasesNotice, type MacroEventKind, type MacroRelease,
|
|
11
|
+
type MacroReleaseList } from "../macro-day/releases";
|
|
12
|
+
import { lookupScheduledDates } from "../alerts/scheduled";
|
|
13
|
+
import { fetchCpiBoard } from "../cpi/client";
|
|
14
|
+
import { fetchRatePath } from "../rate-path/client";
|
|
15
|
+
import type { HeadlessPaneContext } from "../../../types/headless";
|
|
16
|
+
import { zonedDateKey } from "../../../utils/zoned-date-time";
|
|
17
|
+
import { listingFateNotes, listingFateReport, loadListingFate, MovedSymbolError } from "../../../tickers/listing-fate";
|
|
9
18
|
import { startTickerChangeLookup } from "../../../tickers/ticker-changes";
|
|
10
19
|
|
|
11
20
|
const METHODOLOGY = "docs/research-data.md#implied-volatility-history";
|
|
@@ -17,23 +26,33 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
17
26
|
usListingsOnly: "Implied volatility history",
|
|
18
27
|
freshness: { status: "not-a-feed", basis: "daily implied volatility", cadence: "daily", observedKey: "date", oldest: null },
|
|
19
28
|
describe: (args) => `HIVG ${args.symbols[0] ?? ""}`,
|
|
20
|
-
discovery: { aliases: ["HIVG"], screenshotReadiness: "live-dom", dataRequirements: ["Cloud stored implied volatility", "Daily price history"],
|
|
29
|
+
discovery: { aliases: ["HIVG"], suggestFor: IMPLIED_VOL_WORDS, screenshotReadiness: "live-dom", dataRequirements: ["Cloud stored implied volatility", "Daily price history"],
|
|
21
30
|
limitations: ["History starts February 2024 (daily option trade closes).", "Rank and percentile use trade-close readings only.",
|
|
22
31
|
"IV1Y needs long-dated trades: it can start later, has gaps, and ranks from 60 prior sessions.",
|
|
23
32
|
"skew25d (put-call) and rr25d (call-put) come from the daily quote captures and are blank before the first one."] },
|
|
24
33
|
options: [
|
|
25
34
|
{ key: "lookback", type: "enum", values: [{ value: "1Y" }, { value: "2Y" }, { value: "ALL" }], defaultValue: "1Y", description: "Visible history" },
|
|
26
35
|
{ key: "hvWindow", type: "enum", values: HV_WINDOWS.map((window) => ({ value: String(window) })), defaultValue: "20", description: "Realized volatility window in sessions" },
|
|
36
|
+
{ key: "event", type: "enum", aliases: ["release"], values: MACRO_EVENT_KINDS.map((value) => ({ value, ...value === "jobs" ? { aliases: ["nfp", "payrolls"] } : {} })),
|
|
37
|
+
description: "One row per release of this kind whose session at --offset has closed (the next scheduled FOMC or CPI included), in place of the daily history",
|
|
38
|
+
example: "--event fomc --offset -6", shot: { reportOnly: "lists the readings around past release days" } },
|
|
39
|
+
{ key: "offset", type: "integer", minimum: -60, maximum: 60, defaultValue: 0,
|
|
40
|
+
description: "Sessions from each release with --event: 0 is the first session on or after the release day, -6 the sixth session before that",
|
|
41
|
+
example: "--event cpi --offset -1", shot: { reportOnly: "picks the session read around each release" } },
|
|
27
42
|
],
|
|
28
43
|
async load(args, ctx) {
|
|
44
|
+
const event = typeof args.options.event === "string" && args.options.event ? args.options.event as MacroEventKind : null;
|
|
45
|
+
const offset = Number(args.options.offset) || 0;
|
|
46
|
+
if (!event && offset !== 0) throw new Error("--offset counts sessions from a release day: add --event fomc, cpi or jobs.");
|
|
29
47
|
const instrument = await resolveHeadlessInstrument(ctx, args.symbols[0]!);
|
|
30
48
|
// What became of the symbol, as `quote` reads it, looked up beside the figures: FB's are the fund listed
|
|
31
49
|
// there now, TWTR stopped trading, and HFC, which became DINO, has none of its own.
|
|
32
50
|
const changes = startTickerChangeLookup(ctx.marketData, instrument.symbol, instrument.exchange);
|
|
33
51
|
const quote = ctx.marketData.getQuote(instrument.symbol, instrument.exchange ?? "").catch(() => null);
|
|
34
|
-
const [payload, prices] = await Promise.all([
|
|
52
|
+
const [payload, prices, releases] = await Promise.all([
|
|
35
53
|
loadIvHistory(instrument.symbol, { signal: ctx.signal }, ctx.apiClient),
|
|
36
54
|
loadRealizedVolatilityHistory({ instrument, signal: ctx.signal }, createRealizedVolatilityDependencies(ctx.marketData)),
|
|
55
|
+
event ? eventReleases(event, ctx) : null,
|
|
37
56
|
]);
|
|
38
57
|
const fate = await loadListingFate(instrument, ctx.marketData, { quote, changes });
|
|
39
58
|
if (fate.moved) throw new MovedSymbolError(fate.moved);
|
|
@@ -57,11 +76,47 @@ export const ivHistoryHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
57
76
|
const skewColumns = [{ key: "skew25d", header: skewHeader("skew25d", "put-call"), format: points },
|
|
58
77
|
{ key: "rr25d", header: skewHeader("rr25d", "call-put"), format: points }];
|
|
59
78
|
const served = model.iv30.length > 0 || prices.history.length > 0;
|
|
60
|
-
const fateNotes = listingFateNotes(fate, { served });
|
|
61
79
|
// With nothing served, the fate is why: no "not a ticker" or missing-history errors beside it.
|
|
62
|
-
const
|
|
63
|
-
|
|
80
|
+
const { notes: fateNotes, errors } = listingFateReport(fate, { served },
|
|
81
|
+
[prices.error, ...model.warnings].filter((value): value is string => !!value));
|
|
64
82
|
const notes = [...fateNotes, ...[model.oneYearNote, model.skewNote].filter((note): note is string => !!note)];
|
|
83
|
+
if (event && releases) {
|
|
84
|
+
const lookback = String(args.options.lookback) as IvLookback;
|
|
85
|
+
const label = MACRO_EVENT_LABELS[event];
|
|
86
|
+
const offsets = releaseOffsetRows(model, releases.list.releases, event, offset, {
|
|
87
|
+
lookback, readThrough: payload.coverage?.backfilledThrough ?? null });
|
|
88
|
+
const plural = (count: number, one: string, many: string) => count === 1 ? one : many;
|
|
89
|
+
const eventNotes = [
|
|
90
|
+
offsets.beforeLookback > 0
|
|
91
|
+
? `${offsets.beforeLookback} earlier ${label} ${plural(offsets.beforeLookback, "release is", "releases are")} outside the ${lookback} lookback; --lookback ALL adds ${plural(offsets.beforeLookback, "it", "them")}.` : null,
|
|
92
|
+
// Said once the lookback says nothing: the history's own start is then where the rows stop.
|
|
93
|
+
offsets.beforeHistory > 0 && offsets.beforeLookback === 0
|
|
94
|
+
? `${offsets.beforeHistory} earlier ${label} ${plural(offsets.beforeHistory, "release predates", "releases predate")} the IV history, which starts ${offsets.historyStart}.` : null,
|
|
95
|
+
offsets.pending.length > 0
|
|
96
|
+
? `Left out ${label} ${offsets.pending.join(", ")}: no closing reading yet for ${plural(offsets.pending.length, "its session", "their sessions")} at --offset ${offset}.` : null,
|
|
97
|
+
releases.upcomingNote,
|
|
98
|
+
staleMacroReleasesNotice(releases.list),
|
|
99
|
+
].filter((note): note is string => !!note);
|
|
100
|
+
const eventColumns = [{ key: "releaseDate", header: "Release date" }, { key: "session", header: "Session" },
|
|
101
|
+
...historyColumns.slice(1), ...skewColumns];
|
|
102
|
+
return {
|
|
103
|
+
sections: [{
|
|
104
|
+
title: `${label} releases, ${offsetText(offset)}`,
|
|
105
|
+
// The export names the release and the offset on every row, so files of several offsets or releases join.
|
|
106
|
+
columns: [{ key: "release", header: "Release" }, eventColumns[0]!, { key: "offset", header: "Offset (sessions)" }, ...eventColumns.slice(1)],
|
|
107
|
+
rows: offsets.rows.map(({ date, ...row }) => ({ ...row, session: date, release: label })),
|
|
108
|
+
textColumns: offsets.rows.some((row) => row.skew25d != null) ? eventColumns : eventColumns.slice(0, -skewColumns.length),
|
|
109
|
+
}],
|
|
110
|
+
complete: payload.status === "ready" && errors.length === 0,
|
|
111
|
+
unavailableSymbols: offsets.rows.some((row) => row.iv30 != null) ? [] : [instrument.symbol], errors,
|
|
112
|
+
...(notes.length + eventNotes.length > 0 ? { notes: [...notes, ...eventNotes] } : {}),
|
|
113
|
+
// Dated by the history read, not by the newest release's session: an old release is no stale reading.
|
|
114
|
+
...(model.asOf ? { freshness: { asOf: model.asOf } } : {}),
|
|
115
|
+
metadata: { status: payload.status, coverage: payload.coverage, event, offset, releasesThrough: releases.list.coveredThrough,
|
|
116
|
+
nextRelease: releases.upcoming,
|
|
117
|
+
releaseSource: MACRO_RELEASE_SOURCES[event], unit: "decimal annualized volatility", methodology: METHODOLOGY },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
65
120
|
return {
|
|
66
121
|
sections: [
|
|
67
122
|
{ title: "Statistics", columns: [
|
|
@@ -153,3 +208,32 @@ export const ivScreenHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
153
208
|
};
|
|
154
209
|
},
|
|
155
210
|
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The releases of `event` HIVG reads around: the days MDAY studies (the list
|
|
214
|
+
* the app ships with, extended by Gloom Cloud's), and the next one scheduled,
|
|
215
|
+
* from the CPI board's release calendar or the FOMC meetings WIRP prices, as
|
|
216
|
+
* scheduled alerts read them. Payrolls have no schedule to read.
|
|
217
|
+
*/
|
|
218
|
+
async function eventReleases(event: MacroEventKind, ctx: HeadlessPaneContext): Promise<{
|
|
219
|
+
list: MacroReleaseList; upcoming: string | null; upcomingNote: string | null;
|
|
220
|
+
}> {
|
|
221
|
+
const label = MACRO_EVENT_LABELS[event];
|
|
222
|
+
const [published, next] = await Promise.all([
|
|
223
|
+
loadMacroReleases({}, () => ctx.apiClient.getCloudMacroReleaseDays()),
|
|
224
|
+
event === "jobs" ? null : lookupScheduledDates([{ kind: "macro", event }], {
|
|
225
|
+
earnings: async () => [],
|
|
226
|
+
cpi: () => fetchCpiBoard(ctx.apiClient),
|
|
227
|
+
fomc: async () => (await fetchRatePath(ctx.apiClient)).meetings,
|
|
228
|
+
}, zonedDateKey(Date.now(), "America/New_York")).then((dates) => dates.get(`macro:${event}`) ?? { date: null }),
|
|
229
|
+
]);
|
|
230
|
+
const upcoming = next && "date" in next ? next.date : null;
|
|
231
|
+
const listed = upcoming != null && published.releases.some((release) => release.kind === event && release.date === upcoming);
|
|
232
|
+
const releases: MacroRelease[] = upcoming && !listed ? [...published.releases, { kind: event, date: upcoming }] : published.releases;
|
|
233
|
+
return {
|
|
234
|
+
list: { ...published, releases },
|
|
235
|
+
upcoming,
|
|
236
|
+
upcomingNote: next == null ? `The next ${label} release is not scheduled here, so it has no row yet.`
|
|
237
|
+
: "error" in next ? `The next ${label} date could not be loaded (${next.error}), so it has no row.` : null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
@@ -39,7 +39,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
|
|
|
39
39
|
const balances = resource.data;
|
|
40
40
|
const gas = balances?.gas ?? null;
|
|
41
41
|
const [storedTab, setTab] = usePluginPaneState<string>("commodity", "oil");
|
|
42
|
-
const tab: BalanceCommodity =
|
|
42
|
+
const tab: BalanceCommodity = storedTab === "gas" ? "gas" : "oil";
|
|
43
43
|
const board = tab === "gas" ? gas : balances?.oil ?? null;
|
|
44
44
|
const rows = board?.rows ?? EMPTY_ROWS;
|
|
45
45
|
const [selectedId, setSelectedId] = usePluginPaneState<string | null>("selected", null);
|
|
@@ -54,7 +54,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
|
|
|
54
54
|
const selectTab = useCallback((value: string) => {
|
|
55
55
|
setTab(value === "gas" ? "gas" : "oil");
|
|
56
56
|
}, [setTab]);
|
|
57
|
-
const { strip: tabs, rows: tabRows } = usePaneTabs(
|
|
57
|
+
const { strip: tabs, rows: tabRows } = usePaneTabs({
|
|
58
58
|
tabs: TABS,
|
|
59
59
|
activeValue: tab,
|
|
60
60
|
onSelect: selectTab,
|
|
@@ -62,7 +62,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
|
|
|
62
62
|
compact: true,
|
|
63
63
|
variant: "underline",
|
|
64
64
|
queryBarWidth: width,
|
|
65
|
-
}
|
|
65
|
+
});
|
|
66
66
|
const info = useMemo(
|
|
67
67
|
() => (resource.error || !board ? [] : [{ id: "month", parts: [{ text: formatBalanceMonth(board.month) }] }]),
|
|
68
68
|
[board, resource.error],
|
|
@@ -139,7 +139,7 @@ export function JodiPane({ focused, width, height }: PaneProps) {
|
|
|
139
139
|
onRootKeyDown={onRootKeyDown}
|
|
140
140
|
renderCell={renderCell}
|
|
141
141
|
selectedTextOverridesCellColor
|
|
142
|
-
emptyStateTitle="No balances for this month."
|
|
142
|
+
emptyStateTitle={board ? "No balances for this month." : tab === "gas" ? "Gas balances unavailable." : "Oil balances unavailable."}
|
|
143
143
|
/>
|
|
144
144
|
</Box>
|
|
145
145
|
);
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
CloudSessionMoversSide,
|
|
6
6
|
} from "../../../api-client/market-movers";
|
|
7
7
|
import type { DataProvider } from "../../../types/data-provider";
|
|
8
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
8
9
|
import {
|
|
9
10
|
CATEGORY_MAP,
|
|
10
11
|
moverRegularSession,
|
|
@@ -63,7 +64,7 @@ async function hydrateTrending(
|
|
|
63
64
|
if (provider.getQuotesBatch) {
|
|
64
65
|
const results = await provider.getQuotesBatch(
|
|
65
66
|
symbols.map((symbol) => ({ symbol, exchange: "" })),
|
|
66
|
-
).catch((
|
|
67
|
+
).catch(logFailure("market-movers", "Trending quote batch failed", []));
|
|
67
68
|
for (const result of results) {
|
|
68
69
|
if (result.quote) bySymbol.set(result.target.symbol, screenerQuoteFromQuote(result.target.symbol, result.quote));
|
|
69
70
|
}
|
|
@@ -72,8 +73,8 @@ async function hydrateTrending(
|
|
|
72
73
|
try {
|
|
73
74
|
const quote = await provider.getQuote(symbol, "");
|
|
74
75
|
bySymbol.set(symbol, screenerQuoteFromQuote(symbol, quote));
|
|
75
|
-
} catch {
|
|
76
|
-
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logFailure("market-movers", "Trending quote failed")(error);
|
|
77
78
|
}
|
|
78
79
|
}));
|
|
79
80
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ScreenerQuote } from "../../../api-client/market-discovery";
|
|
2
2
|
export type { ScreenerQuote } from "../../../api-client/market-discovery";
|
|
3
3
|
import { resolveCurrencyUnit } from "../../../utils/currency-units";
|
|
4
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
4
5
|
import type { PluginPersistence } from "../../../types/plugin";
|
|
5
6
|
import { apiClient } from "../../../api-client";
|
|
6
7
|
import type {
|
|
@@ -324,7 +325,7 @@ async function bestEffortMarketMetadata(
|
|
|
324
325
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
325
326
|
try {
|
|
326
327
|
return await Promise.race([
|
|
327
|
-
request.then((result) => result.data).catch((
|
|
328
|
+
request.then((result) => result.data).catch(logFailure("market-movers", "Market metadata failed", [])),
|
|
328
329
|
new Promise<ScreenerQuote[]>((resolve) => {
|
|
329
330
|
timeout = setTimeout(() => resolve([]), waitMs);
|
|
330
331
|
}),
|
|
@@ -392,8 +393,8 @@ export async function fetchPreferredMarketMovers(
|
|
|
392
393
|
stale: response.stale === true || response.data.stale === true,
|
|
393
394
|
};
|
|
394
395
|
}
|
|
395
|
-
} catch {
|
|
396
|
-
|
|
396
|
+
} catch (error) {
|
|
397
|
+
logFailure("market-movers", "Live rankings failed")(error);
|
|
397
398
|
}
|
|
398
399
|
|
|
399
400
|
const fallback = await marketMetadata;
|
|
@@ -80,7 +80,7 @@ export const notesCliCommand: CliCommandDef = {
|
|
|
80
80
|
});
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
|
-
warnings.push(...result.skipped.map(describeExportSkip));
|
|
83
|
+
warnings.push(...(result.warnings ?? []), ...result.skipped.map(describeExportSkip));
|
|
84
84
|
ctx.printResult({
|
|
85
85
|
data: {
|
|
86
86
|
dir: result.dir,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { errorMessage } from "../../../utils/errors";
|
|
2
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
2
3
|
import type { NoteFileEntry, QuickNoteEntry } from "./files";
|
|
3
4
|
import type { NotesStore } from "./store";
|
|
4
5
|
import { noteOwnerKey, splitNoteKey } from "./store";
|
|
@@ -25,6 +26,8 @@ export interface NotesExportResult {
|
|
|
25
26
|
/** The sources whose notes were written, in the order given. */
|
|
26
27
|
exported: string[];
|
|
27
28
|
skipped: NotesExportSkip[];
|
|
29
|
+
/** Notes were written, but some descriptive metadata could not be read. */
|
|
30
|
+
warnings?: string[];
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function safeFileName(value: string): string {
|
|
@@ -46,15 +49,21 @@ export async function exportNotesToDirectory(
|
|
|
46
49
|
let files = 0;
|
|
47
50
|
const exported: string[] = [];
|
|
48
51
|
const skipped: NotesExportResult["skipped"] = [];
|
|
52
|
+
const warnings: string[] = [];
|
|
49
53
|
for (const source of sources) {
|
|
50
54
|
const folder = sources.length > 1 ? `${dir}/${safeFileName(source.label)}` : dir;
|
|
51
55
|
let entries: NoteFileEntry[];
|
|
52
56
|
let quick: QuickNoteEntry[];
|
|
57
|
+
let titlesWarning: string | null = null;
|
|
53
58
|
const unreadable: Array<{ key: string; error: unknown }> = [];
|
|
54
59
|
try {
|
|
55
60
|
[entries, quick] = await Promise.all([
|
|
56
61
|
source.store.list({ strict: true, skipped: (key, error) => unreadable.push({ key, error }) }),
|
|
57
|
-
source.store.loadQuickNotesIndex()
|
|
62
|
+
source.store.loadQuickNotesIndex({ strict: true }).catch((error) => {
|
|
63
|
+
logFailure("notes.export", "Read quick note titles for export failed")(error);
|
|
64
|
+
titlesWarning = `Quick notes from ${source.label} were exported using ids because their titles could not be read (${errorMessage(error)}).`;
|
|
65
|
+
return [];
|
|
66
|
+
}),
|
|
58
67
|
]);
|
|
59
68
|
} catch (error) {
|
|
60
69
|
skipped.push({ label: source.label, reason: errorMessage(error) });
|
|
@@ -74,9 +83,10 @@ export async function exportNotesToDirectory(
|
|
|
74
83
|
await writeFile(path, entry.text, "utf-8");
|
|
75
84
|
files += 1;
|
|
76
85
|
}
|
|
86
|
+
if (titlesWarning && entries.some((entry) => splitNoteKey(entry.key).kind === "quick")) warnings.push(titlesWarning);
|
|
77
87
|
exported.push(source.label);
|
|
78
88
|
}
|
|
79
|
-
return { dir, files, exported, skipped };
|
|
89
|
+
return { dir, files, exported, skipped, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
/** One line per skip, as `notes export` warns and the Export Notes command reports. */
|
|
@@ -86,12 +96,13 @@ export function describeExportSkip(skip: NotesExportSkip): string {
|
|
|
86
96
|
: `Skipped the ${skip.label} note ${skip.note}: it could not be read (${skip.reason}).`;
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
/** The Export Notes command's notice: the count, then what was skipped. */
|
|
99
|
+
/** The Export Notes command's notice: the count, then warnings and what was skipped. */
|
|
90
100
|
export function exportNotice(result: NotesExportResult): { body: string; type: "success" | "error" } {
|
|
91
101
|
const exported = `Exported ${result.files} ${result.files === 1 ? "note" : "notes"} to ${result.dir}`;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
102
|
+
const issues = [...(result.warnings ?? []), ...result.skipped.map(describeExportSkip)];
|
|
103
|
+
if (issues.length === 0) return { body: exported, type: "success" };
|
|
104
|
+
const shown = issues.slice(0, 3);
|
|
105
|
+
const more = issues.length - shown.length;
|
|
95
106
|
return {
|
|
96
107
|
body: [`${exported}.`, ...shown, ...(more > 0 ? [`${more} more could not be read.`] : [])].join(" "),
|
|
97
108
|
type: "error",
|
|
@@ -29,9 +29,10 @@ export interface NotesFilesIO {
|
|
|
29
29
|
* Every note's key (its `.md` file name without the extension) and when it
|
|
30
30
|
* was last written. A folder that does not exist yet, or a note gone before
|
|
31
31
|
* it was looked at, lists nothing. Any other failure is logged and that part
|
|
32
|
-
* left out, or, with `strict`, rejects.
|
|
32
|
+
* left out, or, with `strict`, rejects. A `skipped` callback receives an
|
|
33
|
+
* unreadable note instead of rejecting the whole listing.
|
|
33
34
|
*/
|
|
34
|
-
listKeys(options?:
|
|
35
|
+
listKeys(options?: NotesListOptions): Promise<Array<[string, number]>>;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const QUICK_NOTES_INDEX_FILE = "__quick-notes-index__.json";
|
|
@@ -108,8 +109,9 @@ export function diskNotesFilesIO(dataDir: string): NotesFilesIO {
|
|
|
108
109
|
} catch (error) {
|
|
109
110
|
// A note deleted since the folder was listed is simply gone.
|
|
110
111
|
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") continue;
|
|
111
|
-
if (options?.strict) throw error;
|
|
112
|
+
if (options?.strict && !options.skipped) throw error;
|
|
112
113
|
logFailure("notes.files", "Read when a note was last written failed")(error);
|
|
114
|
+
options?.skipped?.(name.slice(0, -3), error);
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
return keys;
|
|
@@ -200,7 +202,16 @@ export function remoteNotesFilesIO(
|
|
|
200
202
|
read: async (file) => await invoke("read", { dataDir, file }) as string | null,
|
|
201
203
|
write: async (file, text) => { await invoke("write", { dataDir, file, text }); },
|
|
202
204
|
delete: async (file) => { await invoke("delete", { dataDir, file }); },
|
|
203
|
-
|
|
205
|
+
async listKeys(options) {
|
|
206
|
+
const result = await invoke("listKeys", {
|
|
207
|
+
dataDir, ...(options?.strict ? { strict: true } : {}),
|
|
208
|
+
...(options?.skipped ? { reportSkipped: true } : {}),
|
|
209
|
+
});
|
|
210
|
+
if (!options?.skipped) return result as Array<[string, number]>;
|
|
211
|
+
const { keys, skipped } = result as { keys: Array<[string, number]>; skipped: Array<{ key: string; reason: string }> };
|
|
212
|
+
for (const { key, reason } of skipped) options.skipped(key, new Error(reason));
|
|
213
|
+
return keys;
|
|
214
|
+
},
|
|
204
215
|
};
|
|
205
216
|
}
|
|
206
217
|
|
|
@@ -245,7 +256,7 @@ export class NotesFiles implements NotesStore {
|
|
|
245
256
|
|
|
246
257
|
/** Every stored note, ticker notes and quick notes alike, newest first. */
|
|
247
258
|
async list(options: NotesListOptions = {}): Promise<NoteFileEntry[]> {
|
|
248
|
-
const { entries } = await this.readEach(await this.io.listKeys(
|
|
259
|
+
const { entries } = await this.readEach(await this.io.listKeys(options), options.skipped);
|
|
249
260
|
return entries.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
250
261
|
}
|
|
251
262
|
|
|
@@ -4,7 +4,7 @@ import { buildOptionsKey, resolveEntryData } from "../../../market-data/selector
|
|
|
4
4
|
import type { OptionsChain } from "../../../types/financials";
|
|
5
5
|
import { normalizeSymbol, parsePublicTickerKey } from "../../../utils/exchanges";
|
|
6
6
|
import { daysToExpiryFrom, volatilityTermSlope, type ExpectedMove } from "../shared/volatility";
|
|
7
|
-
import { buildSurfaceExpiry,
|
|
7
|
+
import { buildSurfaceExpiry, type SurfaceExpiry } from "../vol-surface/model";
|
|
8
8
|
import type { YieldPoint } from "../yield-curve/treasury-data";
|
|
9
9
|
|
|
10
10
|
export interface OptionsEnrichmentSelection {
|
|
@@ -97,8 +97,8 @@ export function optionsEnrichmentSelectionIssue(input: OptionsEnrichmentSelectio
|
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* Models kept between projections of one selection. The adjacent expiry does
|
|
100
|
-
* not stream, so a live re-projection refits only the selected smile and
|
|
101
|
-
* the adjacent ATM level
|
|
100
|
+
* not stream, so a live re-projection refits only the selected smile and keeps
|
|
101
|
+
* the adjacent fit, whose ATM-forward level does not move with the spot.
|
|
102
102
|
*/
|
|
103
103
|
export interface OptionsEnrichmentCache {
|
|
104
104
|
neighbour?: { chain: OptionsChain; curve: readonly YieldPoint[] | undefined; model: SurfaceExpiry };
|
|
@@ -134,17 +134,18 @@ export function projectOptionsEnrichment(input: OptionsEnrichmentProjection, cac
|
|
|
134
134
|
else if (neighbourChain) {
|
|
135
135
|
const cached = cache?.neighbour?.chain === neighbourChain && cache.neighbour.curve === input.curve
|
|
136
136
|
? cache.neighbour.model : null;
|
|
137
|
-
const neighbour = cached
|
|
138
|
-
:
|
|
139
|
-
curve: input.curve ?? [], now: input.now, source: input.neighbourEntry?.source });
|
|
137
|
+
const neighbour = cached ?? buildSurfaceExpiry({ chain: neighbourChain, expiration: neighbourExpiration, spot: input.spot,
|
|
138
|
+
curve: input.curve ?? [], now: input.now, source: input.neighbourEntry?.source });
|
|
140
139
|
if (cache && !cached) cache.neighbour = { chain: neighbourChain, curve: input.curve, model: neighbour };
|
|
141
140
|
result.rateAsOf = [...new Set([...result.rateAsOf, ...neighbour.rateAsOf])];
|
|
142
141
|
result.warnings.push(...neighbour.warnings
|
|
143
142
|
.filter((warning) => !(input.curveLoading && warning === "Treasury rate unavailable"))
|
|
144
143
|
.map((warning) => `Adjacent expiry: ${warning}`));
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
// The slope runs between the two expiries' IVs at their forwards, as OVDV's does, so a print
|
|
145
|
+
// against a closed session's chain does not tilt it.
|
|
146
|
+
if (selected.skew.atm != null && neighbour.skew.atm != null) {
|
|
147
|
+
Object.assign(result, optionsEnrichmentTermSlope({ years: selected.years, volatility: selected.skew.atm },
|
|
148
|
+
{ years: neighbour.years, volatility: neighbour.skew.atm }));
|
|
148
149
|
if (new Date(selected.asOf!).toISOString().slice(0, 10) !== new Date(neighbour.asOf!).toISOString().slice(0, 10)) {
|
|
149
150
|
result.warnings.push("Term slope uses option observations from different dates");
|
|
150
151
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getExtendedSessionDisplay, type ExtendedSession } from "../../../market-data/market/status";
|
|
1
2
|
import { getPublishedUsEquitySession } from "../../../market-data/published-us-sessions";
|
|
2
3
|
import type { OptionsChain, Quote } from "../../../types/financials";
|
|
3
4
|
import { zonedWallClockToUtcMs } from "../../../utils/zoned-date-time";
|
|
@@ -82,10 +83,75 @@ export function formatSpotTime(time: number): string {
|
|
|
82
83
|
return `${WEEKDAYS[date.getUTCDay()]} ${date.getUTCDate()} ${MONTHS[date.getUTCMonth()]} ${date.toISOString().slice(11, 16)} UTC`;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
type SpotStampInput = Pick<OptionModelSpot, "basis" | "session" | "observedAt">;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The stamp every options pane puts beside the spot it priced a chain with
|
|
90
|
+
* (docs/research-data.md#greeks-spot-and-time): `close Tue 20 Oct` for the
|
|
91
|
+
* official close of the session the chain was quoted in, `pre-market Wed 21
|
|
92
|
+
* Oct 09:35 UTC` (`after hours`, `overnight`) for an extended-session print.
|
|
93
|
+
* Null for the regular session's latest price, which the pane's live or
|
|
94
|
+
* delayed status already dates.
|
|
95
|
+
*/
|
|
96
|
+
export function modelSpotStamp(spot: SpotStampInput): string | null {
|
|
97
|
+
const dated = Number.isFinite(spot.observedAt);
|
|
98
|
+
if (spot.basis === "close") return dated ? `close ${formatCalendarDay(newYorkDate.format(spot.observedAt))}` : "close";
|
|
99
|
+
return spot.session && dated ? `${spot.session} ${formatSpotTime(spot.observedAt)}` : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The stamp's shorter forms, longest first, for a figure with less room
|
|
104
|
+
* (`StatItem.detailVariants`): `pre-market 09:35 UTC`, then `pre-market`;
|
|
105
|
+
* `close`. Empty when there is no stamp.
|
|
106
|
+
*/
|
|
107
|
+
export function modelSpotStampVariants(spot: SpotStampInput): string[] {
|
|
108
|
+
if (!modelSpotStamp(spot)) return [];
|
|
109
|
+
if (spot.basis === "close") return ["close"];
|
|
110
|
+
return [`${spot.session} ${new Date(spot.observedAt).toISOString().slice(11, 16)} UTC`, spot.session!];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The stamp where a report or footer always dates the spot: `last Wed 21 Oct 14:05 UTC` for the regular session's price. */
|
|
114
|
+
export function modelSpotLabel(spot: SpotStampInput): string {
|
|
115
|
+
return modelSpotStamp(spot) ?? `last ${formatSpotTime(spot.observedAt)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const HOUR_MS = 60 * 60_000;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A spot known only by its price and when it was observed, such as the one
|
|
122
|
+
* Gloom Cloud prices dealer gamma at: the session that time falls in on the
|
|
123
|
+
* listing's calendar. Pre-market from 04:00 New York to the open, after hours
|
|
124
|
+
* for four hours from the close, overnight between; a day the calendar does
|
|
125
|
+
* not know reads as the latest price.
|
|
126
|
+
*/
|
|
127
|
+
export function observedModelSpot(price: number, observedAt: number, exchange?: string): OptionModelSpot {
|
|
128
|
+
const day = Number.isFinite(observedAt) ? getPublishedUsEquitySession(exchange || "NYSE", newYorkDate.format(observedAt)) : null;
|
|
129
|
+
const session: OptionModelSpot["session"] = day?.kind !== "session" ? null
|
|
130
|
+
: observedAt < day.open ? (observedAt >= day.open - 5.5 * HOUR_MS ? "pre-market" : "overnight")
|
|
131
|
+
: observedAt < day.close ? null
|
|
132
|
+
: observedAt < day.close + 4 * HOUR_MS ? "after hours" : "overnight";
|
|
133
|
+
return { price, observedAt, valuationTime: observedAt, basis: "last", session };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const EXTENDED_SESSION_WORDS: Record<ExtendedSession, NonNullable<OptionModelSpot["session"]>> = {
|
|
137
|
+
PRE: "pre-market", POST: "after hours", OVERNIGHT: "overnight",
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The underlying's extended-session print beside a spot priced at the close,
|
|
142
|
+
* named by its session as the stamp names it: `pre-market 770.65`, `after
|
|
143
|
+
* hours mid 771.10`. Null when the spot is not the close, nothing traded
|
|
144
|
+
* outside the session, or the print is the spot.
|
|
145
|
+
*/
|
|
146
|
+
export function modelSpotExtendedPrint(
|
|
147
|
+
quote: Quote | null | undefined,
|
|
148
|
+
spot: Pick<OptionModelSpot, "basis" | "price">,
|
|
149
|
+
format: (price: number) => string = formatModelSpotPrice,
|
|
150
|
+
): string | null {
|
|
151
|
+
if (spot.basis !== "close") return null;
|
|
152
|
+
const print = getExtendedSessionDisplay(quote);
|
|
153
|
+
if (!print || !positive(print.price) || format(print.price) === format(spot.price)) return null;
|
|
154
|
+
return `${EXTENDED_SESSION_WORDS[print.session]}${print.mark ? " mid" : ""} ${format(print.price)}`;
|
|
89
155
|
}
|
|
90
156
|
|
|
91
157
|
/** A price to the cent, or to the tick it was quoted at below a dollar. */
|
|
@@ -5,6 +5,7 @@ import { colors, priceColor } from "../../../theme/colors";
|
|
|
5
5
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
6
6
|
import { formatCompact, formatPercentRaw } from "../../../utils/format";
|
|
7
7
|
import { formatMarketPrice, formatSignedMarketPrice, liveQuoteFormatOptions } from "../../../market-data/market/format";
|
|
8
|
+
import { getRegularSessionDisplay } from "../../../market-data/market/status";
|
|
8
9
|
import { chainOptionContract, formatExpDate, optionContractName, resolveOptionsTarget } from "../../../utils/options";
|
|
9
10
|
import {
|
|
10
11
|
expiryIsoDate, findListedExpiry, formatDaysToExpiry, daysToExpiry, missingExpiryMessage, missingExpiryText, readOptionExpiration,
|
|
@@ -76,7 +77,10 @@ import { useIvRank } from "../iv-history/rank";
|
|
|
76
77
|
import { useOptionsSessionOpen, useThrottledValue } from "../shared/volatility/live-session";
|
|
77
78
|
import { useRemoteUiNode } from "../../../remote/semantic-tree";
|
|
78
79
|
import { optionCarry, optionCostOfSpot } from "./carry";
|
|
79
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
formatModelSpotPrice, modelSpotExtendedPrint, modelSpotLabel, modelSpotStamp, modelSpotStampVariants, optionModelSpot,
|
|
82
|
+
type OptionModelSpot,
|
|
83
|
+
} from "./model-spot";
|
|
80
84
|
import { chainOpenInterestAsOf, loadListedOpenInterest, openInterestAsOfLabel, statedOpenInterestAsOf } from "./open-interest-as-of";
|
|
81
85
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
82
86
|
import {
|
|
@@ -113,13 +117,35 @@ function transientTicker(symbol: string, exchange: string, currency: string): Ti
|
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
/**
|
|
116
|
-
* The underlying's price
|
|
117
|
-
*
|
|
120
|
+
* The underlying's price: the level every strike is read against. A chain
|
|
121
|
+
* priced at a session's close or an extended-session print shows that spot
|
|
122
|
+
* with its stamp (`modelSpotStamp`), and any extended print after it named by
|
|
123
|
+
* its session; the regular session's live price shows its day change. A stale
|
|
124
|
+
* quote stays on screen, muted, while the footer says why.
|
|
118
125
|
*/
|
|
119
|
-
function spotItem(
|
|
126
|
+
function spotItem(
|
|
127
|
+
underlying: TickerFinancials | null | undefined,
|
|
128
|
+
assetCategory: string | undefined,
|
|
129
|
+
modelSpot: OptionModelSpot | null,
|
|
130
|
+
): StatItem {
|
|
120
131
|
const quote = underlying?.quote;
|
|
121
132
|
if (!quote || !Number.isFinite(quote.price)) return { id: "spot", label: "Spot", value: "--" };
|
|
122
133
|
const options = liveQuoteFormatOptions(quote, quote.currency, assetCategory, underlying?.quoteMetadata?.instrumentType);
|
|
134
|
+
const stamp = modelSpot ? modelSpotStamp(modelSpot) : null;
|
|
135
|
+
if (modelSpot && stamp) {
|
|
136
|
+
const move = modelSpot.basis === "close" ? getRegularSessionDisplay(quote)?.change : quote.change;
|
|
137
|
+
const extended = modelSpotExtendedPrint(quote, modelSpot, (price) => formatMarketPrice(price, options));
|
|
138
|
+
// The band keeps its columns: the extended print, then the stamp's day, give way first.
|
|
139
|
+
const shorter = modelSpotStampVariants(modelSpot);
|
|
140
|
+
return {
|
|
141
|
+
id: "spot",
|
|
142
|
+
label: "Spot",
|
|
143
|
+
value: formatMarketPrice(modelSpot.price, options),
|
|
144
|
+
detail: extended ? `${stamp} · ${extended}` : stamp,
|
|
145
|
+
detailVariants: extended ? [stamp, ...shorter] : shorter,
|
|
146
|
+
color: quote.stale ? colors.textDim : priceColor(Number.isFinite(move) ? move! : 0),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
123
149
|
const change = Number.isFinite(quote.change) ? quote.change : 0;
|
|
124
150
|
return {
|
|
125
151
|
id: "spot",
|
|
@@ -552,10 +578,10 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
552
578
|
putGreeks: volatilities ? calculateOptionGreeks(put, "put", greeksSpot, dividendYield, volatilities) : undefined,
|
|
553
579
|
callExtrinsicPerYear: call && volatilities ? optionCarry(call, "call", greeksSpot, volatilities.valuationTime)?.extrinsicPerYear ?? null : null,
|
|
554
580
|
putExtrinsicPerYear: put && volatilities ? optionCarry(put, "put", greeksSpot, volatilities.valuationTime)?.extrinsicPerYear ?? null : null,
|
|
555
|
-
putCostOfSpot: put ? optionCostOfSpot(put,
|
|
581
|
+
putCostOfSpot: put ? optionCostOfSpot(put, greeksSpot) : null,
|
|
556
582
|
isPositionStrike: !!parsed && strike === parsed.strike,
|
|
557
583
|
};
|
|
558
|
-
}), [callsByStrike, dividendYield, greeksSpot, parsed, putsByStrike,
|
|
584
|
+
}), [callsByStrike, dividendYield, greeksSpot, parsed, putsByStrike, strikes, volatilities]);
|
|
559
585
|
// The open interest's session: the chain's own, or for a chain that does not say, OPX's listing of the
|
|
560
586
|
// same expiry, stated only when the counts are OPX's.
|
|
561
587
|
const statedOpenInterest = useMemo(() => strikeChain ? statedOpenInterestAsOf(strikeChain) : undefined, [strikeChain]);
|
|
@@ -584,10 +610,12 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
584
610
|
} : null);
|
|
585
611
|
// What IV and the Greeks are valued at, and the open interest's date. In a
|
|
586
612
|
// crowded footer it shortens, then gives way to the contract under the cursor.
|
|
587
|
-
|
|
613
|
+
// A stamped spot (a close, an extended print) is already the Spot figure's.
|
|
614
|
+
const footerSpot = modelSpot && !modelSpotStamp(modelSpot) ? modelSpot : null;
|
|
615
|
+
const basisText = [footerSpot ? `spot ${formatModelSpotPrice(footerSpot.price)} ${modelSpotLabel(footerSpot)}` : null,
|
|
588
616
|
strikeChain && !listedOpenInterest.loading ? openInterestAsOfLabel(openInterestAsOf) : null].filter(Boolean).join(" · ");
|
|
589
|
-
const shortBasisText = [
|
|
590
|
-
+ `${new Date(
|
|
617
|
+
const shortBasisText = [footerSpot ? `${formatModelSpotPrice(footerSpot.price)} last `
|
|
618
|
+
+ `${new Date(footerSpot.observedAt).toISOString().slice(11, 16)} UTC` : null,
|
|
591
619
|
strikeChain && !listedOpenInterest.loading ? openInterestAsOf?.date ? `OI ${openInterestAsOf.date.slice(5)}` : "OI undated" : null]
|
|
592
620
|
.filter(Boolean).join(" · ");
|
|
593
621
|
usePaneFooter("options-basis", () => ({ info: basisText ? [{ id: "greeks-basis",
|
|
@@ -597,9 +625,9 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
597
625
|
// busy stream reads as a steady strip rather than flicker.
|
|
598
626
|
// A new expiry, a refreshed snapshot or the underlying price arriving or
|
|
599
627
|
// going stale shows at once; only the stream itself is paced.
|
|
600
|
-
const summaryResetKey = useMemo(() => ({}), [strikeSelectionKey, strikeChain,
|
|
628
|
+
const summaryResetKey = useMemo(() => ({}), [strikeSelectionKey, strikeChain, greeksSpot == null]);
|
|
601
629
|
const summaryInput = useThrottledValue(
|
|
602
|
-
useMemo(() => ({ chain: liveChain, spot, volatilities }), [liveChain,
|
|
630
|
+
useMemo(() => ({ chain: liveChain, spot: greeksSpot, volatilities }), [liveChain, greeksSpot, volatilities]),
|
|
603
631
|
OPTIONS_SUMMARY_THROTTLE_MS,
|
|
604
632
|
summaryResetKey,
|
|
605
633
|
);
|
|
@@ -613,7 +641,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
613
641
|
instrument: baseRequest?.instrument ?? null, expiration: selectedExpiration,
|
|
614
642
|
selectedEntry: strikeChain === expirationChain ? expirationChainEntry
|
|
615
643
|
: strikeChain === initialChain ? initialChainEntry : null,
|
|
616
|
-
catalogue: availableExpirations, spot, spotAsOf:
|
|
644
|
+
catalogue: availableExpirations, spot: greeksSpot, spotAsOf: modelSpot?.observedAt,
|
|
617
645
|
liveChain: liveChain !== strikeChain ? liveChain : null,
|
|
618
646
|
});
|
|
619
647
|
const enrichment = expirationUnavailable ? null : enrichmentState.snapshot;
|
|
@@ -901,7 +929,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
901
929
|
// The root insets the terminal body, so the band gets the width inside it.
|
|
902
930
|
const statWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
|
|
903
931
|
const figures = optionsSummaryItems({
|
|
904
|
-
spot: spotItem(underlying, isOpt ? undefined : ticker.metadata.assetCategory),
|
|
932
|
+
spot: spotItem(underlying, isOpt ? undefined : ticker.metadata.assetCategory, modelSpot),
|
|
905
933
|
summary, enrichment, currency: underlying?.quote?.currency || ticker.metadata.currency || "",
|
|
906
934
|
ivRank: showIvRank ? { stats: ivRank } : null,
|
|
907
935
|
});
|