gloomberb 0.17.0 → 0.17.2
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/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
- package/src/api-client/data.ts +5 -1
- package/src/api-client/doe.ts +19 -0
- package/src/api-client/earnings.ts +4 -2
- package/src/api-client/index.ts +1 -0
- package/src/app/global-shortcuts.ts +10 -0
- package/src/app/keybindings/actions.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +1 -1
- package/src/app/runtime/update.ts +16 -0
- package/src/app.tsx +1 -1
- package/src/architecture/core-builtin-allowlist.json +4 -9
- package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
- package/src/cli/commands/market.ts +87 -17
- package/src/cli/commands/options-chain.ts +93 -23
- package/src/cli/commands/options-leaps.ts +462 -63
- package/src/cli/commands/search.ts +10 -7
- package/src/cli/commands/ticker.ts +19 -10
- package/src/cli/glossary.ts +1 -1
- package/src/cli/index.ts +14 -6
- package/src/cli/known-gaps.ts +22 -0
- package/src/cli/pane-functions/catalog.ts +45 -10
- package/src/cli/pane-functions/dom.ts +36 -10
- package/src/cli/pane-functions/function-help.ts +4 -2
- package/src/cli/pane-functions/headless.ts +12 -1
- package/src/cli/pane-functions/index.ts +44 -3
- package/src/cli/pane-functions/lock-note.ts +56 -0
- package/src/cli/pane-functions/report-notices.ts +15 -0
- package/src/cli/symbol-args.ts +19 -0
- package/src/components/command-bar/list/view.tsx +2 -1
- package/src/components/command-bar/workflow/builtin.ts +2 -2
- package/src/components/command-bar/workflow/collection-actions.ts +1 -1
- package/src/components/feedback-dialog.tsx +1 -1
- package/src/components/index.ts +3 -2
- package/src/components/layout/header.tsx +58 -14
- package/src/components/layout/shell/chrome.ts +10 -4
- package/src/components/layout/shell/index.tsx +1 -1
- package/src/components/layout/status-bar-groups.ts +1 -1
- package/src/components/layout/status-bar.tsx +2 -2
- package/src/components/markdown-text.tsx +14 -2
- package/src/components/reader/article-reader.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +10 -7
- package/src/components/reader/reader-text.tsx +32 -17
- package/src/components/ui/column-grid.tsx +206 -0
- package/src/components/ui/display.tsx +40 -3
- package/src/components/ui/fill.ts +8 -0
- package/src/components/ui/index.ts +2 -0
- package/src/layout-marketplace/gallery.tsx +2 -2
- package/src/layout-marketplace/linked-sync.ts +1 -1
- package/src/market-data/market/freshness.ts +3 -0
- package/src/market-data/published-apac-sessions.ts +12 -2
- package/src/market-data/quotes/screener-live-quotes.ts +31 -7
- package/src/plugins/builtin/account-management/form-components.tsx +4 -7
- package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
- package/src/plugins/builtin/backtest/pane.tsx +9 -3
- package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
- package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/presets.ts +33 -1
- package/src/plugins/builtin/chat/channel-labels.ts +2 -2
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
- package/src/plugins/builtin/chat/content/index.tsx +2 -2
- package/src/plugins/builtin/chat/conversations.ts +1 -1
- package/src/plugins/builtin/chat/index.tsx +2 -2
- package/src/plugins/builtin/chat/share/flow.tsx +1 -1
- package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
- package/src/plugins/builtin/chat/sidebar.tsx +2 -2
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
- package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
- package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
- package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
- package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
- package/src/plugins/builtin/cloud/team/capability.ts +2 -2
- package/src/plugins/builtin/cloud/team/command.ts +4 -4
- package/src/plugins/builtin/cloud/team/module.tsx +3 -3
- package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
- package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
- package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
- package/src/plugins/builtin/cloud/team/views.ts +2 -2
- package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
- package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
- package/src/plugins/builtin/connections/pane.tsx +16 -5
- package/src/plugins/builtin/cpi/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
- package/src/plugins/builtin/docs/blocks.tsx +13 -2
- package/src/plugins/builtin/docs/pane.tsx +6 -5
- package/src/plugins/builtin/doe/index.tsx +11 -4
- package/src/plugins/builtin/doe/pane.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +84 -0
- package/src/plugins/builtin/doe/series.ts +4 -1
- package/src/plugins/builtin/earnings/board-model.ts +18 -1
- package/src/plugins/builtin/earnings/board-table.ts +18 -3
- package/src/plugins/builtin/earnings/headless.ts +13 -2
- package/src/plugins/builtin/earnings/history-model.ts +1 -1
- package/src/plugins/builtin/earnings/history-table.ts +2 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
- package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
- package/src/plugins/builtin/equity-screener/headless.ts +59 -1
- package/src/plugins/builtin/equity-screener/live.ts +82 -31
- package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
- package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
- package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
- package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
- package/src/plugins/builtin/executives/index.tsx +18 -0
- package/src/plugins/builtin/exposure/charts.tsx +4 -3
- package/src/plugins/builtin/filing-events/index.tsx +8 -10
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/gpu/equities.tsx +9 -6
- package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +3 -1
- package/src/plugins/builtin/iv-history/index.tsx +3 -1
- package/src/plugins/builtin/iv-history/model.ts +3 -1
- package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
- package/src/plugins/builtin/iv-history/universe.ts +1 -1
- package/src/plugins/builtin/jobs/headless.ts +6 -20
- package/src/plugins/builtin/jobs/index.tsx +2 -0
- package/src/plugins/builtin/jobs/lock-note.ts +16 -0
- package/src/plugins/builtin/notes/index.tsx +1 -1
- package/src/plugins/builtin/notes/owner.tsx +2 -2
- package/src/plugins/builtin/options/carry.ts +36 -0
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/options/model-spot.ts +94 -0
- package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
- package/src/plugins/builtin/options/view.tsx +69 -11
- package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
- package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
- package/src/plugins/builtin/options-scenario/client.ts +33 -9
- package/src/plugins/builtin/options-scenario/csp.ts +168 -0
- package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
- package/src/plugins/builtin/options-scenario/headless.ts +24 -8
- package/src/plugins/builtin/options-scenario/index.tsx +3 -1
- package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
- package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
- package/src/plugins/builtin/research/event-model.ts +7 -4
- package/src/plugins/builtin/risk-factors/index.tsx +18 -0
- package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
- package/src/plugins/builtin/scanner/headless.ts +9 -2
- package/src/plugins/builtin/scanner/index.ts +3 -0
- package/src/plugins/builtin/scanner/lock-note.ts +17 -0
- package/src/plugins/builtin/series-inventory.ts +2 -1
- package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
- package/src/plugins/builtin/shared/earnings-report.ts +10 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
- package/src/plugins/builtin/social-mentions/client.ts +3 -3
- package/src/plugins/builtin/social-mentions/headless.ts +6 -2
- package/src/plugins/builtin/social-mentions/index.tsx +2 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
- package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
- package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
- package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
- package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
- package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/data-table/index.tsx +48 -15
- package/src/renderers/dom/data-table/row.tsx +1 -1
- package/src/renderers/dom/desktop/popover.tsx +6 -15
- package/src/renderers/dom/host/input.tsx +27 -3
- package/src/renderers/dom/host/surface-3d.tsx +6 -4
- package/src/renderers/dom/styles.css +26 -6
- package/src/renderers/electrobun/view/app-services.ts +2 -22
- package/src/renderers/electrobun/view/news-source.ts +42 -0
- package/src/series/validate.ts +5 -0
- package/src/shares/dialog.tsx +2 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/financials.ts +5 -0
- package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
- package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
- package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
- package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
- package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
- package/src/types/data-provider.ts +3 -0
- package/src/types/financials.ts +28 -0
- package/src/types/plugin.ts +17 -4
- package/src/utils/decimal-comma.ts +18 -0
- package/src/utils/exchanges.ts +7 -0
- package/src/utils/financial-statements.ts +25 -0
- package/src/utils/forward-dividend.ts +39 -9
- package/src/utils/fundamentals.ts +41 -1
- package/src/utils/option-expiry.ts +16 -0
- package/src/utils/sec.ts +2 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
|
+
import { proLockNote } from "../shared/pro-lock-note";
|
|
2
3
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
3
4
|
import {
|
|
4
5
|
attachExecutivesPersistence,
|
|
@@ -10,6 +11,21 @@ import { isKnownNonUsListing } from "../../../utils/sec";
|
|
|
10
11
|
const description =
|
|
11
12
|
"Named executive officers and what they were paid, read from the company's proxy statement and checked against the filing.";
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* EXEC is Pro whole (`useProFeatureWall`). The proxy statements it reads are
|
|
16
|
+
* in the SEC filing list, which needs no account: `filings` lists them with a
|
|
17
|
+
* link to each, where the Summary Compensation Table is the pay as filed,
|
|
18
|
+
* without EXEC's parsed figures.
|
|
19
|
+
*/
|
|
20
|
+
function executivesLockNote(lock: { gate: "sign-in" | "pro"; symbol: string | null }): string {
|
|
21
|
+
return proLockNote(lock, {
|
|
22
|
+
token: "EXEC",
|
|
23
|
+
does: "it reads each proxy statement into the named executive officers and what they were paid",
|
|
24
|
+
shows: "it",
|
|
25
|
+
free: (ticker, company) => `gloomberb filings ${ticker} --form DEF14A lists ${company}'s proxy statements, each with its filing date and a link to the filing, whose Summary Compensation Table holds the pay as filed.`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
export const executivesModule: PluginModule = {
|
|
14
30
|
setup(ctx) {
|
|
15
31
|
attachExecutivesPersistence(ctx.persistence);
|
|
@@ -31,6 +47,8 @@ export const executivesModule: PluginModule = {
|
|
|
31
47
|
{
|
|
32
48
|
id: EXECUTIVES_PANE_ID,
|
|
33
49
|
reportFreshness: { source: "SEC EDGAR", status: "not-a-feed", basis: "proxy statements" },
|
|
50
|
+
reportLockNote: executivesLockNote,
|
|
51
|
+
reportRequirements: ["Gloom Cloud Pro"],
|
|
34
52
|
name: "Executives",
|
|
35
53
|
icon: "X",
|
|
36
54
|
component: ExecutivesPane,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ActionRow } from "../../../components";
|
|
2
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
2
3
|
import { Box, ChartSurface, Text, useUiCapabilities } from "../../../ui";
|
|
3
4
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
4
5
|
import type { ChartVectorShape } from "../../../ui/host";
|
|
@@ -53,14 +54,14 @@ export function ExposurePathDiagram({ component, width, height }: { component: E
|
|
|
53
54
|
const vectors: ChartVectorShape[] = hops.map((hop, i) => ({ id: `${i}`, points: [{ x: (i + .5) / nodes.length, y: .5 }, { x: (i + 1.5) / nodes.length, y: .5 }], color: hop.pct === null ? colors.textDim : colors.warning, strokeWidth: 2, handles: true }));
|
|
54
55
|
return <Box flexDirection="column" height={height} overflow="hidden">
|
|
55
56
|
<Box paddingX={1} height={1} flexShrink={0}><Text fg={colors.textDim}>{hops.length} hop{hops.length === 1 ? "" : "s"} · {component.classification}{component.exposurePct ? ` · ${rangeText(component.exposurePct)}` : ""}</Text></Box>
|
|
56
|
-
<Box flexDirection="row" height={1} flexShrink={0} paddingX={1}>{nodes.map((node, i) => <Box key={`${node.id}:${i}`} width={nodeWidth} alignItems="center"><
|
|
57
|
+
<Box flexDirection="row" height={1} flexShrink={0} paddingX={1}>{nodes.map((node, i) => <Box key={`${node.id}:${i}`} width={nodeWidth} alignItems="center"><ClippedText fg={colors.textBright} text={node.symbol ?? node.name} width={nodeWidth - 2} /></Box>)}</Box>
|
|
57
58
|
<Box height={1} flexShrink={0} paddingX={1}>
|
|
58
59
|
{desktop ? <ChartSurface width={width - 2} height={1} vectors={vectors} /> : <Text fg={colors.textDim}>{nodes.map((_, i) => i < hops.length ? "─".repeat(Math.max(1, nodeWidth - 2)) + "→ " : "").join("")}</Text>}
|
|
59
60
|
</Box>
|
|
60
|
-
{height >= 4 ? <Box flexDirection="row" height={1} flexShrink={0} paddingX={1}>{nodes.map((_, i) => <Box key={i} width={nodeWidth} alignItems="center"><
|
|
61
|
+
{height >= 4 ? <Box flexDirection="row" height={1} flexShrink={0} paddingX={1}>{nodes.map((_, i) => <Box key={i} width={nodeWidth} alignItems="center"><ClippedText fg={colors.warning} text={i === 0 ? "" : hops[i - 1]!.pct === null ? "Undisclosed share" : `${hops[i - 1]!.pct}% ${basisLabel(hops[i - 1]!.basis)}`} width={nodeWidth - 1} /></Box>)}</Box> : null}
|
|
61
62
|
{height >= 5 ? <Box flexDirection="row" height={1} flexShrink={0} paddingX={1}>{nodes.map((_, i) => {
|
|
62
63
|
const evidence = i > 0 ? hops[i - 1]!.evidence[0] : null;
|
|
63
|
-
return <Box key={i} width={nodeWidth} alignItems="center"><
|
|
64
|
+
return <Box key={i} width={nodeWidth} alignItems="center"><ClippedText fg={colors.textDim} text={evidence ? `${evidence.tier} · ${evidence.asOf ?? "undated"}` : ""} width={nodeWidth - 1} /></Box>;
|
|
64
65
|
})}</Box> : null}
|
|
65
66
|
</Box>;
|
|
66
67
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
|
+
import { proLockNote } from "../shared/pro-lock-note";
|
|
2
3
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
3
4
|
import { FILING_EVENTS_PANE_ID, FilingEventsPane } from "./pane";
|
|
4
5
|
import { isKnownNonUsListing } from "../../../utils/sec";
|
|
@@ -12,16 +13,13 @@ const description =
|
|
|
12
13
|
* are in SEC, which needs no account: its rows carry each 8-K's filing date,
|
|
13
14
|
* acceptance time and items, by number and in words.
|
|
14
15
|
*/
|
|
15
|
-
function filingEventsLockNote(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
: "
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`Free, with no sign-in: gloomberb fn SEC ${ticker} lists ${symbol ?? "the company"}'s recent filings, 8-Ks among them, each with its filing date and its items by number and name.`,
|
|
23
|
-
`${unlock}; gloomberb fn EK ${ticker} then reads with that session.`,
|
|
24
|
-
].join("\n");
|
|
16
|
+
function filingEventsLockNote(lock: { gate: "sign-in" | "pro"; symbol: string | null }): string {
|
|
17
|
+
return proLockNote(lock, {
|
|
18
|
+
token: "EK",
|
|
19
|
+
does: "it reads each 8-K and sums up what it said and who it names",
|
|
20
|
+
shows: "them",
|
|
21
|
+
free: (ticker, company) => `gloomberb fn SEC ${ticker} lists ${company}'s recent filings, 8-Ks among them, each with its filing date and its items by number and name.`,
|
|
22
|
+
});
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
export const filingEventsModule: PluginModule = {
|
|
@@ -108,6 +108,7 @@ function FixedIncomeSectionPane({ section, ...props }: PaneProps & { section: Fi
|
|
|
108
108
|
flexGrow={1}
|
|
109
109
|
flexBasis={0}
|
|
110
110
|
overflow="hidden"
|
|
111
|
+
data-gloom-role="nested-pane-content"
|
|
111
112
|
>
|
|
112
113
|
<PaneFooterScope active={value === view}>
|
|
113
114
|
<View {...props} paneType={paneType} height={contentHeight} focused={props.focused && value === view} />
|
|
@@ -147,7 +148,7 @@ export function FixedIncomePane(props: PaneProps) {
|
|
|
147
148
|
<Box height={contentHeight} flexGrow={1} flexBasis={0} overflow="hidden">
|
|
148
149
|
{FIXED_INCOME_SECTIONS.map(({ value }) => (
|
|
149
150
|
mounted.has(value) || value === section ? (
|
|
150
|
-
<Box key={value} visible={value === section} height={contentHeight} flexGrow={1} flexBasis={0} overflow="hidden">
|
|
151
|
+
<Box key={value} visible={value === section} height={contentHeight} flexGrow={1} flexBasis={0} overflow="hidden" data-gloom-role="nested-pane-content">
|
|
151
152
|
<PaneFooterScope active={value === section}>
|
|
152
153
|
<NestedPaneTabs>
|
|
153
154
|
<FixedIncomeSectionPane {...props} section={value} height={contentHeight} focused={props.focused && value === section} />
|
|
@@ -10,9 +10,10 @@ import { useAssetData, useAsyncResource, useAutoRefresh, usePluginPaneState, use
|
|
|
10
10
|
import { priceColor } from "../../../theme/colors";
|
|
11
11
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
12
12
|
import { TICKER_RESEARCH_PANE_ID } from "../../../types/config";
|
|
13
|
-
import { Box,
|
|
13
|
+
import { Box, TextAttributes, useUiCapabilities } from "../../../ui";
|
|
14
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
14
15
|
import { publicTickerKey } from "../../../utils/exchanges";
|
|
15
|
-
import { displayWidth
|
|
16
|
+
import { displayWidth } from "../../../utils/format";
|
|
16
17
|
import { quoteBoardFooterInfo, quoteBoardStatus, renderQuoteBoardCell, useQuoteBoard } from "../shared/use-quote-board";
|
|
17
18
|
import { GpuModelQuery } from "./board";
|
|
18
19
|
import { loadGpuEquityHistory } from "./client";
|
|
@@ -66,6 +67,7 @@ export function GpuEquities({ board, models, locked, model, setModel, reloadBoar
|
|
|
66
67
|
const dataRef = useRef({ quotes, historyMap });
|
|
67
68
|
dataRef.current = { quotes, historyMap };
|
|
68
69
|
const medium = width >= 100;
|
|
70
|
+
const desktop = !!useUiCapabilities().nativePaneChrome;
|
|
69
71
|
const renderCell = useCallback((item: Row, column: DataTableColumn, _index: number, state: { selected: boolean }): DataTableCell => {
|
|
70
72
|
if (!isSectionedItemRow(item)) return EMPTY_TABLE_CELL;
|
|
71
73
|
const row = item.item;
|
|
@@ -81,9 +83,10 @@ export function GpuEquities({ board, models, locked, model, setModel, reloadBoar
|
|
|
81
83
|
const variant = [row.gpuModel, ...gpuVariant(reference)].join(" ");
|
|
82
84
|
// The name keeps its room; the variant gives way first.
|
|
83
85
|
const rest = Math.max(0, column.width - displayWidth(name) - 2);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
// On the desktop the variant gives way first at the column's edge, by CSS.
|
|
87
|
+
return { text: `${name} ${variant}`, content: <Box flexDirection="row" height={1} overflow="hidden" {...(desktop ? { flexShrink: 1, minWidth: 0 } : {})}>
|
|
88
|
+
<ClippedText fg={state.selected ? colors.selectedText : colors.text} text={name} width={column.width} />
|
|
89
|
+
{desktop || rest >= 4 ? <ClippedText fg={state.selected ? colors.selectedText : colors.textDim} lead=" " text={variant} width={rest} flex="0 1000 auto" /> : null}
|
|
87
90
|
</Box> };
|
|
88
91
|
}
|
|
89
92
|
case "gpuPrice": return reference ? { text: gpuPrice(reference.pricePerGpuHr), value: reference.pricePerGpuHr, color: colors.textBright } : gapCell;
|
|
@@ -104,7 +107,7 @@ export function GpuEquities({ board, models, locked, model, setModel, reloadBoar
|
|
|
104
107
|
default: return renderQuoteBoardCell(column.id as "price" | "changePercent", dataRef.current.quotes.get(row.symbol),
|
|
105
108
|
{ formatPrice: (quote) => gpuPrice(quote.price), formatChange: (quote) => gpuChange(quote.change) });
|
|
106
109
|
}
|
|
107
|
-
}, [colors]);
|
|
110
|
+
}, [colors, desktop]);
|
|
108
111
|
return <DataTableView<Row> columns={equityColumns(width)} items={items} rootWidth={width} rootHeight={height} focused={focused}
|
|
109
112
|
rootBefore={<GpuModelQuery models={models} model={model || "H100"} setModel={setModel} width={width} allowAll={false} />}
|
|
110
113
|
selection={{ kind: "id", selectedId: selected, getId: rowKey, onChange: (id) => setSelected(String(id)) }} getItemKey={rowKey}
|
|
@@ -7,7 +7,7 @@ import { t } from "../../../i18n";
|
|
|
7
7
|
import { colors } from "../../../theme/colors";
|
|
8
8
|
import { Box, Text } from "../../../ui";
|
|
9
9
|
import { formatRelativeTime } from "../../../utils/datetime-format";
|
|
10
|
-
import { requestAuthDialog } from "
|
|
10
|
+
import { requestAuthDialog } from "../../../account/auth-dialog";
|
|
11
11
|
|
|
12
12
|
const STATUS_BADGES: Record<FeedbackStatus, { label: string; tone: "neutral" | "accent" | "positive" }> = {
|
|
13
13
|
received: { label: "Received", tone: "neutral" },
|
|
@@ -21,6 +21,9 @@ export const VCA_DEFINITIONS = [
|
|
|
21
21
|
*/
|
|
22
22
|
export const skewHeader = (stem: string, sign: string) => `${stem} ${sign} (pts)`;
|
|
23
23
|
|
|
24
|
+
/** The underlying price a reading was taken at, to the cent. */
|
|
25
|
+
export const formatSpot = (value: number | null | undefined) => value == null || !Number.isFinite(value) ? "--"
|
|
26
|
+
: value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
24
27
|
export const formatVol = (value: number | null | undefined) => value == null || !Number.isFinite(value) ? "--" : `${(value * 100).toFixed(1)}%`;
|
|
25
28
|
export const formatPoints = (value: number | null | undefined) =>
|
|
26
29
|
value == null || !Number.isFinite(value) ? "--" : `${value >= 0 ? "+" : ""}${(value * 100).toFixed(1)}`;
|
|
@@ -2,7 +2,7 @@ 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, loadRealizedVolatilities } from "./client";
|
|
5
|
-
import { formatPoints, formatRank, formatStat, formatVol, readingLabel, sharedDates, skewHeader, VCA_DEFINITIONS, verdictLabel } from "./format";
|
|
5
|
+
import { formatPoints, formatRank, formatSpot, formatStat, formatVol, 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
8
|
import { listingFateNotes, loadListingFate } from "../../../cli/listing-arg";
|
|
@@ -120,6 +120,8 @@ export const ivScreenHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
120
120
|
{ key: "symbol", header: "Symbol" }, { key: "iv30", header: "IV30", format: percent },
|
|
121
121
|
...(dates.reading ? [] : [{ key: "date", header: "As of", format: (value: unknown, row?: unknown) =>
|
|
122
122
|
typeof value === "string" ? readingLabel(value, (row as RichCheapRow).method) : "--" }]),
|
|
123
|
+
// The underlying price each reading was taken at, on the date the title or the As of column gives.
|
|
124
|
+
{ key: "spot", header: "Spot", format: (value: unknown) => typeof value === "number" ? formatSpot(value) : "--" },
|
|
123
125
|
...(dates.rankApart ? [{ key: "rankDate", header: "Rank as of", format: (value: unknown) => typeof value === "string" ? `${value} close` : "--" }] : []),
|
|
124
126
|
{ key: "rank", header: "IVR", format: (value: unknown) => formatRank(value as number | null) },
|
|
125
127
|
{ key: "percentile", header: "IVP", format: (value: unknown) => formatRank(value as number | null) },
|
|
@@ -37,7 +37,9 @@ export const ivHistoryModule: PluginModule = {
|
|
|
37
37
|
{
|
|
38
38
|
id: "iv-screen-pane", paneId: "iv-screen", label: "Volatility Rich/Cheap",
|
|
39
39
|
description: "IV rank, percentile, one-year IV, term slope, skew and IV/HV across a list, ranked rich to cheap.",
|
|
40
|
-
|
|
40
|
+
// Where a put seller finds rich premium; OSA owns the exact put-selling phrases and ranks first on them.
|
|
41
|
+
keywords: ["vca", "rich", "cheap", "iv rank", "implied volatility", "screen", "cash-secured puts", "selling puts", "short-put candidates",
|
|
42
|
+
"csp candidates", "wheel candidates"],
|
|
41
43
|
shortcut: { prefix: "VCA", argKind: "ticker-list" as const, argOptional: true }, headless: ivScreenHeadless,
|
|
42
44
|
createInstance: (_context, options) => ({
|
|
43
45
|
title: "VCA", placement: "floating" as const,
|
|
@@ -241,6 +241,8 @@ export interface RichCheapRow {
|
|
|
241
241
|
iv30: number | null;
|
|
242
242
|
date: string | null;
|
|
243
243
|
method: IvMethod | null;
|
|
244
|
+
/** The underlying price the reading was taken at: the session's close, or the spot at the quote capture. */
|
|
245
|
+
spot: number | null;
|
|
244
246
|
rank: number | null;
|
|
245
247
|
percentile: number | null;
|
|
246
248
|
/** Trade close the rank is measured on: the newest trade-close session, which a newer quote reading can be ahead of. */
|
|
@@ -304,7 +306,7 @@ export function projectRichCheap(rows: readonly IvScreenRow[], hv: ReadonlyMap<s
|
|
|
304
306
|
return {
|
|
305
307
|
symbol: row.symbol, status: row.coverageStatus ?? row.status, queuedAt: row.coverage?.addedAt ?? null,
|
|
306
308
|
backfilledThrough: row.coverage?.backfilledThrough ?? null, warning: row.warnings?.[0] ?? null,
|
|
307
|
-
iv30, date: latest?.date ?? null, method: latest?.method ?? null,
|
|
309
|
+
iv30, date: latest?.date ?? null, method: latest?.method ?? null, spot: latest?.spot ?? null,
|
|
308
310
|
rank: row.iv30?.rank ?? null, percentile, rankDate: row.iv30?.date ?? null,
|
|
309
311
|
termSlope: iv30 != null && latest?.iv90 != null ? iv30 - latest.iv90 : null,
|
|
310
312
|
iv1y: latest?.iv365 ?? null,
|
|
@@ -9,7 +9,7 @@ import { wrapTextLines } from "../../../utils/text-wrap";
|
|
|
9
9
|
import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
10
10
|
import { compareSortValues, nextHeaderSort, type SortDirection } from "../../../utils/sort-values";
|
|
11
11
|
import { loadIvScreen, loadRealizedVolatilities } from "./client";
|
|
12
|
-
import { formatPoints, formatRank, formatVol, readingLabel, sharedDates, shortDate, VCA_DEFINITIONS, verdictLabel } from "./format";
|
|
12
|
+
import { formatPoints, formatRank, formatSpot, formatVol, readingLabel, sharedDates, shortDate, VCA_DEFINITIONS, verdictLabel } from "./format";
|
|
13
13
|
import { projectRichCheap, type RichCheapRow, richCheapDates, screenPaneNotices, type VcaPreset } from "./model";
|
|
14
14
|
import { vcaUniverse } from "./universe";
|
|
15
15
|
|
|
@@ -21,6 +21,7 @@ const COLUMNS: DataTableColumn[] = [
|
|
|
21
21
|
{ id: "symbol", label: "Symbol", width: 8, align: "left" },
|
|
22
22
|
{ id: "iv30", label: "IV30", width: 7, align: "right" },
|
|
23
23
|
{ id: "date", label: "As of", width: 11, align: "right" },
|
|
24
|
+
{ id: "spot", label: "Spot", width: 9, align: "right" },
|
|
24
25
|
{ id: "rankDate", label: "Rank as of", width: 11, align: "right" },
|
|
25
26
|
{ id: "rank", label: "IVR", width: 5, align: "right" },
|
|
26
27
|
{ id: "percentile", label: "IVP", width: 5, align: "right" },
|
|
@@ -99,6 +100,7 @@ export function IvScreenPane({ width, height, focused }: PaneProps) {
|
|
|
99
100
|
switch (id) {
|
|
100
101
|
case "symbol": return { text: row.symbol, color: colors.textBright };
|
|
101
102
|
case "iv30": return { text: formatVol(row.iv30), color: colors.warning };
|
|
103
|
+
case "spot": return { text: formatSpot(row.spot), color: colors.textDim };
|
|
102
104
|
case "date": return { text: row.date ? readingLabel(row.date, row.method, true)
|
|
103
105
|
: row.status === "queued" || row.status === "backfilling" ? row.status : "--", color: colors.textDim };
|
|
104
106
|
case "rankDate": return { text: row.rankDate ? `${shortDate(row.rankDate)} close` : "--", color: colors.textDim };
|
|
@@ -34,7 +34,7 @@ export function vcaUniverse(scope: string, symbolsText: string, collectionId: st
|
|
|
34
34
|
}
|
|
35
35
|
const valid = [...new Map(instruments.filter((instrument) => SYMBOL.test(instrument.symbol)).map((instrument) => [instrument.symbol, instrument])).values()];
|
|
36
36
|
const error = !valid.length ? scope === "collection" ? "Link a watchlist or portfolio with US-listed symbols in pane settings." : "Add symbols in pane settings."
|
|
37
|
-
: valid.length > VCA_LIMIT ? `
|
|
37
|
+
: valid.length > VCA_LIMIT ? `Only the first ${VCA_LIMIT} symbols are screened.`
|
|
38
38
|
: skipped ? `${skipped} non-US listing${skipped === 1 ? "" : "s"} left out: stored implied volatility covers US option underlyings.` : null;
|
|
39
39
|
return { label, instruments: valid.slice(0, VCA_LIMIT), error };
|
|
40
40
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { ApiRequestError } from "../../../api-client/errors";
|
|
1
2
|
import type {
|
|
2
3
|
HeadlessBundleResult,
|
|
3
|
-
HeadlessPaneContext,
|
|
4
4
|
HeadlessPaneDefinition,
|
|
5
5
|
} from "../../../types/plugin";
|
|
6
|
-
import {
|
|
6
|
+
import { explainRefusal } from "../shared/cloud-denied";
|
|
7
7
|
import { formatCompact, formatNumber } from "../../../utils/format";
|
|
8
8
|
import { fetchJobs, fetchJobsMovers, type JobsCloudClient, type JobsCompanyState } from "./client";
|
|
9
|
+
import { jobsLockNote } from "./lock-note";
|
|
9
10
|
import {
|
|
10
11
|
buildMoverRows,
|
|
11
12
|
buildPostingRows,
|
|
@@ -15,18 +16,6 @@ import {
|
|
|
15
16
|
functionLabel,
|
|
16
17
|
} from "./model";
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
* What a refused account needs, and the free preview to try instead: HIRE
|
|
20
|
-
* reads the same careers systems, and is named only when it answers for this
|
|
21
|
-
* company.
|
|
22
|
-
*/
|
|
23
|
-
async function jobsDeniedMessage(symbol: string | undefined, status: CloudDeniedStatus, client: Pick<HeadlessPaneContext["apiClient"], "getCloudHiring">): Promise<string> {
|
|
24
|
-
const need = cloudAccountNeed(status);
|
|
25
|
-
const preview = await client.getCloudHiring(symbol, { limit: 1 })
|
|
26
|
-
.then((payload) => !!payload && (symbol ? "symbol" in payload : "companies" in payload), () => false);
|
|
27
|
-
return `JOBS needs ${need}${preview ? `; HIRE${symbol ? ` ${symbol}` : ""} is free to preview` : ""}.`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
19
|
function companyBundle(symbol: string, state: Exclude<JobsCompanyState, { kind: "denied" }>, roleLimit: number): HeadlessBundleResult {
|
|
31
20
|
if (state.kind === "pending") {
|
|
32
21
|
return {
|
|
@@ -193,14 +182,11 @@ export const jobsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
193
182
|
const symbol = args.symbols[0];
|
|
194
183
|
if (symbol) {
|
|
195
184
|
const state = await fetchJobs(symbol, { force: true, client: ctx.apiClient as JobsCloudClient });
|
|
196
|
-
if (state.kind === "denied") throw new
|
|
185
|
+
if (state.kind === "denied") throw explainRefusal(new ApiRequestError(state.message, state.status), jobsLockNote, { symbol });
|
|
197
186
|
return companyBundle(symbol, state, limit);
|
|
198
187
|
}
|
|
199
|
-
const payload = await fetchJobsMovers(ctx.apiClient as JobsCloudClient)
|
|
200
|
-
|
|
201
|
-
if (status) throw new Error(await jobsDeniedMessage(undefined, status, ctx.apiClient));
|
|
202
|
-
throw error;
|
|
203
|
-
});
|
|
188
|
+
const payload = await fetchJobsMovers(ctx.apiClient as JobsCloudClient)
|
|
189
|
+
.catch((error: unknown) => { throw explainRefusal(error, jobsLockNote, { symbol: null }); });
|
|
204
190
|
const rows = buildMoverRows(payload.movers).slice(0, Math.max(limit, 15)).map((row) => ({
|
|
205
191
|
ticker: row.ticker,
|
|
206
192
|
company: row.company,
|
|
@@ -3,6 +3,7 @@ import type { PluginModule } from "../plugin-module";
|
|
|
3
3
|
import { JOBS_SERIES } from "./series";
|
|
4
4
|
import { resetJobsCache } from "./client";
|
|
5
5
|
import { jobsHeadless } from "./headless";
|
|
6
|
+
import { jobsLockNote } from "./lock-note";
|
|
6
7
|
import { JOBS_PANE_ID, JobsPane, JobsResearchTab } from "./pane";
|
|
7
8
|
|
|
8
9
|
|
|
@@ -33,6 +34,7 @@ export const jobsModule: PluginModule = {
|
|
|
33
34
|
panes: [
|
|
34
35
|
{
|
|
35
36
|
id: JOBS_PANE_ID,
|
|
37
|
+
reportLockNote: jobsLockNote,
|
|
36
38
|
name: "Hiring",
|
|
37
39
|
icon: "J",
|
|
38
40
|
component: JobsPane,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PaneLock } from "../../../types/plugin";
|
|
2
|
+
import { proLockNote } from "../shared/pro-lock-note";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JOBS is Pro whole. HIRE reads the same careers systems and needs no
|
|
6
|
+
* account: a company's latest roles, without the full history.
|
|
7
|
+
*/
|
|
8
|
+
export function jobsLockNote(lock: PaneLock): string {
|
|
9
|
+
return proLockNote(lock, {
|
|
10
|
+
token: "JOBS",
|
|
11
|
+
argument: "optional",
|
|
12
|
+
does: "it reads each company's careers system into open roles, hiring pace and the mix by function and country",
|
|
13
|
+
shows: "it",
|
|
14
|
+
free: (ticker, company) => `gloomberb fn HIRE ${ticker} previews ${company}'s hiring with its latest roles, without the full history.`,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
2
|
import type { GloomPlugin } from "../../../types/plugin";
|
|
3
|
-
import { teamStore } from "
|
|
3
|
+
import { teamStore } from "../../../team/store";
|
|
4
4
|
import { notesCliCommand } from "./cli";
|
|
5
5
|
import { exportNotesToDirectory, exportSourceLabel } from "./export";
|
|
6
6
|
import { NotesFiles } from "./files";
|
|
@@ -5,8 +5,8 @@ import { QueryBar } from "../../../components/ui/query-bar";
|
|
|
5
5
|
import { Box, Text } from "../../../ui";
|
|
6
6
|
import type { DialogApi, PromptContext } from "../../../ui/dialog";
|
|
7
7
|
import { colors } from "../../../theme/colors";
|
|
8
|
-
import { teamAccentHex, teamPrefix } from "
|
|
9
|
-
import { teamStore } from "
|
|
8
|
+
import { teamAccentHex, teamPrefix } from "../../../team/model";
|
|
9
|
+
import { teamStore } from "../../../team/store";
|
|
10
10
|
import { NoteConflictError, type NoteOwner, noteOwnerKey } from "./store";
|
|
11
11
|
|
|
12
12
|
export function useNoteTeams(): TeamSummary[] {
|
|
@@ -48,6 +48,42 @@ export function optionCarry(
|
|
|
48
48
|
return { extrinsic, extrinsicPerYear: days >= MIN_CARRY_DAYS ? extrinsic / spot / (days / 365) : null };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Extrinsic per year net of financing and dividends, a fraction of spot per
|
|
53
|
+
* year (`gloomberb options --leaps --columns carry-net`):
|
|
54
|
+
*
|
|
55
|
+
* call: (extrinsic - strike x rate x years + spot x dividend yield x years) / spot / years
|
|
56
|
+
* put: (extrinsic + strike x rate x years - spot x dividend yield x years) / spot / years
|
|
57
|
+
*
|
|
58
|
+
* A call bought instead of the stock leaves the strike in cash earning the rate
|
|
59
|
+
* and forgoes the dividends; a put's midpoint already has both in it the other
|
|
60
|
+
* way round (put-call parity), so the signs turn. Simple interest over the same
|
|
61
|
+
* years as `optionCarry`. Null when the carry has no yearly figure.
|
|
62
|
+
*/
|
|
63
|
+
export function optionCarryNet(
|
|
64
|
+
carry: OptionCarry,
|
|
65
|
+
side: OptionSide,
|
|
66
|
+
strike: number,
|
|
67
|
+
spot: number,
|
|
68
|
+
rate: number,
|
|
69
|
+
dividendYield: number,
|
|
70
|
+
): number | null {
|
|
71
|
+
if (carry.extrinsicPerYear == null || !positive(spot) || !positive(strike)) return null;
|
|
72
|
+
const financing = rate * strike / spot - dividendYield;
|
|
73
|
+
return side === "call" ? carry.extrinsicPerYear - financing : carry.extrinsicPerYear + financing;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Where the underlying has to finish for a contract bought at its midpoint to
|
|
78
|
+
* pay back at expiry: strike plus the midpoint for a call, strike less it for a
|
|
79
|
+
* put. Null without a two-sided quote.
|
|
80
|
+
*/
|
|
81
|
+
export function optionBreakeven(contract: Pick<OptionContract, "bid" | "ask" | "strike">, side: OptionSide): number | null {
|
|
82
|
+
const mid = optionMid(contract);
|
|
83
|
+
if (mid == null || !positive(contract.strike)) return null;
|
|
84
|
+
return side === "call" ? contract.strike + mid : contract.strike - mid;
|
|
85
|
+
}
|
|
86
|
+
|
|
51
87
|
/** Bid/ask width as a percent of the midpoint; null without a two-sided quote. */
|
|
52
88
|
export function optionSpreadPercent(contract: Pick<OptionContract, "bid" | "ask">): number | null {
|
|
53
89
|
const spread = optionSpread(contract);
|
|
@@ -135,7 +135,8 @@ export const optionsModule: PluginModule = {
|
|
|
135
135
|
paneId: "options",
|
|
136
136
|
label: "Options",
|
|
137
137
|
description: "Options chain for the selected ticker.",
|
|
138
|
-
|
|
138
|
+
// Where a put seller picks the strike; OSA owns the exact put-selling phrases and ranks first on them.
|
|
139
|
+
keywords: ["options", "chain", "calls", "puts", "omon", "cash-secured puts", "selling puts", "short-put strikes", "csp strikes", "wheel strikes"],
|
|
139
140
|
shortcut: "OMON",
|
|
140
141
|
settings: (_symbol, _context, options) => {
|
|
141
142
|
const expiration = readOptionExpiration(options?.values?.expiration);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { getPublishedUsEquitySession } from "../../../market-data/published-us-sessions";
|
|
2
|
+
import type { OptionsChain, Quote } from "../../../types/financials";
|
|
3
|
+
import { zonedWallClockToUtcMs } from "../../../utils/zoned-date-time";
|
|
4
|
+
import { optionQuoteValuationTime } from "../vol-surface/model";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The underlying price and instant a chain's IV and Greeks are valued at
|
|
8
|
+
* (docs/research-data.md#greeks-spot-and-time). The chain's quotes and the
|
|
9
|
+
* spot belong to one moment:
|
|
10
|
+
*
|
|
11
|
+
* - when the quote carries the official close of the regular session the
|
|
12
|
+
* chain's quotes were taken in (a chain from the close, read before the next
|
|
13
|
+
* open or early in it), that close, at the chain's time;
|
|
14
|
+
* - otherwise the underlying's latest price, at the chain's time once its
|
|
15
|
+
* quotes are over an hour old (outside the session) and now while they are
|
|
16
|
+
* current.
|
|
17
|
+
*
|
|
18
|
+
* So a pre-market print never moves the delta of a chain still quoting the
|
|
19
|
+
* last close.
|
|
20
|
+
*/
|
|
21
|
+
export interface OptionModelSpot {
|
|
22
|
+
price: number;
|
|
23
|
+
/** When the price was observed: the session's close, or the quote's own time. */
|
|
24
|
+
observedAt: number;
|
|
25
|
+
/** The instant time to expiry is measured from. */
|
|
26
|
+
valuationTime: number;
|
|
27
|
+
/** `close`: the regular session's official close; `last`: the latest price. */
|
|
28
|
+
basis: "close" | "last";
|
|
29
|
+
/** The session the latest price is from, when it is not the regular one. */
|
|
30
|
+
session: "pre-market" | "after hours" | "overnight" | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const newYorkDate = new Intl.DateTimeFormat("en-CA", { timeZone: "America/New_York", year: "numeric", month: "2-digit", day: "2-digit" });
|
|
34
|
+
const positive = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
35
|
+
|
|
36
|
+
function regularCloseTime(exchange: string | undefined, date: string): number {
|
|
37
|
+
const session = getPublishedUsEquitySession(exchange || "NYSE", date);
|
|
38
|
+
if (session?.kind === "session" && Number.isFinite(session.close)) return session.close;
|
|
39
|
+
const [year, month, day] = date.split("-").map(Number) as [number, number, number];
|
|
40
|
+
return zonedWallClockToUtcMs("America/New_York", year, month, day, 16, 0, 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The session a quote's price is from when it is not the regular one; null in the regular session or when nothing traded outside it. */
|
|
44
|
+
export function extendedSession(quote: Quote): OptionModelSpot["session"] {
|
|
45
|
+
if (quote.overnightPrice != null && quote.price === quote.overnightPrice) return "overnight";
|
|
46
|
+
if (quote.extendedSessionPrint === false) return null;
|
|
47
|
+
if (quote.marketState === "PRE" || quote.marketState === "PREPRE") return "pre-market";
|
|
48
|
+
if (quote.marketState === "POST" || quote.marketState === "POSTPOST") return "after hours";
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The spot the chain is valued at; null without a current underlying price. */
|
|
53
|
+
export function optionModelSpot(
|
|
54
|
+
quote: Quote | null | undefined,
|
|
55
|
+
chain: Pick<OptionsChain, "asOf"> | null | undefined,
|
|
56
|
+
now: number,
|
|
57
|
+
exchange?: string,
|
|
58
|
+
): OptionModelSpot | null {
|
|
59
|
+
if (!quote || quote.stale === true || !positive(quote.price)) return null;
|
|
60
|
+
const chainTime = chain?.asOf ? Date.parse(chain.asOf) : Number.NaN;
|
|
61
|
+
const chainDate = Number.isFinite(chainTime) ? newYorkDate.format(chainTime) : null;
|
|
62
|
+
if (chainDate && positive(quote.regularClose) && quote.regularCloseSessionDate === chainDate && chainTime <= now) {
|
|
63
|
+
return { price: quote.regularClose, observedAt: regularCloseTime(exchange, chainDate), valuationTime: chainTime,
|
|
64
|
+
basis: "close", session: null };
|
|
65
|
+
}
|
|
66
|
+
return { price: quote.price, observedAt: positive(quote.lastUpdated) ? quote.lastUpdated : now,
|
|
67
|
+
valuationTime: optionQuoteValuationTime(chain ?? {}, now), basis: "last", session: extendedSession(quote) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
71
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
72
|
+
|
|
73
|
+
/** `Fri 9 Oct` for a calendar date, YYYY-MM-DD. */
|
|
74
|
+
export function formatCalendarDay(date: string): string {
|
|
75
|
+
const day = new Date(`${date}T00:00:00Z`);
|
|
76
|
+
return `${WEEKDAYS[day.getUTCDay()]} ${day.getUTCDate()} ${MONTHS[day.getUTCMonth()]}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `Mon 12 Oct 20:00 UTC`. */
|
|
80
|
+
export function formatSpotTime(time: number): string {
|
|
81
|
+
const date = new Date(time);
|
|
82
|
+
return `${WEEKDAYS[date.getUTCDay()]} ${date.getUTCDate()} ${MONTHS[date.getUTCMonth()]} ${date.toISOString().slice(11, 16)} UTC`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** How a footer names the spot: `close Mon 12 Oct 20:00 UTC`, `pre-market Tue 13 Oct 12:21 UTC`, `last Tue 13 Oct 14:05 UTC`. */
|
|
86
|
+
export function modelSpotLabel(spot: Pick<OptionModelSpot, "basis" | "session" | "observedAt">): string {
|
|
87
|
+
const word = spot.basis === "close" ? "close" : spot.session ?? "last";
|
|
88
|
+
return `${word} ${formatSpotTime(spot.observedAt)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A price to the cent, or to the tick it was quoted at below a dollar. */
|
|
92
|
+
export function formatModelSpotPrice(price: number): string {
|
|
93
|
+
return price.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: price < 1 ? 4 : 2, useGrouping: false });
|
|
94
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { OptionsChain } from "../../../types/financials";
|
|
2
|
+
import { formatCalendarDay } from "./model-spot";
|
|
3
|
+
import { loadOpenInterest, type OpenInterestPayload, type OptionsPositioningApi } from "../options-positioning/client";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The session a chain's open interest is counted at
|
|
7
|
+
* (docs/research-data.md#greeks-spot-and-time). Gloom Cloud states it on the
|
|
8
|
+
* chain (`openInterestAsOf`). A chain that does not say, from an older server,
|
|
9
|
+
* borrows OPX's listed date, the session whose close its counts are, only when
|
|
10
|
+
* the chain's counts are OPX's: every contract with a count in both agrees.
|
|
11
|
+
* Otherwise the date is not stated.
|
|
12
|
+
*/
|
|
13
|
+
export interface OpenInterestAsOf {
|
|
14
|
+
/** The session the chain's counts are as of, YYYY-MM-DD; null when it cannot be established. */
|
|
15
|
+
date: string | null;
|
|
16
|
+
/** The session OPX's counts are as of. */
|
|
17
|
+
listedDate: string | null;
|
|
18
|
+
/** Contracts with a count in both the chain and OPX's listing. */
|
|
19
|
+
compared: number;
|
|
20
|
+
/** Of those, the ones whose counts agree. */
|
|
21
|
+
matched: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The date the chain states for its counts: a session, or null when none of
|
|
26
|
+
* its counts is dated. Undefined when the chain does not say, and only then is
|
|
27
|
+
* OPX's listing compared.
|
|
28
|
+
*/
|
|
29
|
+
export function statedOpenInterestAsOf(chain: Pick<OptionsChain, "openInterestAsOf">): OpenInterestAsOf | undefined {
|
|
30
|
+
const stated = chain.openInterestAsOf;
|
|
31
|
+
if (stated !== null && (typeof stated !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(stated))) return undefined;
|
|
32
|
+
return { date: stated, listedDate: null, compared: 0, matched: 0 };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function chainOpenInterestAsOf(
|
|
36
|
+
chain: Pick<OptionsChain, "calls" | "puts">,
|
|
37
|
+
listed: Pick<OpenInterestPayload, "oiDate" | "strikes">,
|
|
38
|
+
): OpenInterestAsOf {
|
|
39
|
+
const byStrike = new Map(listed.strikes.map((row) => [row.strike, row]));
|
|
40
|
+
let compared = 0;
|
|
41
|
+
let matched = 0;
|
|
42
|
+
for (const [side, contracts] of [["callOI", chain.calls], ["putOI", chain.puts]] as const) {
|
|
43
|
+
for (const contract of contracts) {
|
|
44
|
+
const count = byStrike.get(contract.strike)?.[side];
|
|
45
|
+
if (contract.openInterest == null || count == null) continue;
|
|
46
|
+
compared += 1;
|
|
47
|
+
if (count === contract.openInterest) matched += 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { date: listed.oiDate && compared > 0 && matched === compared ? listed.oiDate : null,
|
|
51
|
+
listedDate: listed.oiDate, compared, matched };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** How long a chain waits for OPX's listing before it leaves the open-interest date unstated. */
|
|
55
|
+
const LISTED_TIMEOUT_MS = 8_000;
|
|
56
|
+
|
|
57
|
+
/** OPX's listed open interest for one expiry; null when it cannot be read in time or names another expiry. */
|
|
58
|
+
export async function loadListedOpenInterest(
|
|
59
|
+
symbol: string,
|
|
60
|
+
expiry: string,
|
|
61
|
+
options: { signal?: AbortSignal; api?: OptionsPositioningApi } = {},
|
|
62
|
+
): Promise<OpenInterestPayload | null> {
|
|
63
|
+
try {
|
|
64
|
+
const listed = await loadOpenInterest(symbol, expiry, { signal: options.signal ?? AbortSignal.timeout(LISTED_TIMEOUT_MS) }, options.api);
|
|
65
|
+
return listed.expiry === expiry ? listed : null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `OI as of Fri 9 Oct`, or `OI date not published` when the date cannot be established. */
|
|
72
|
+
export function openInterestAsOfLabel(asOf: Pick<OpenInterestAsOf, "date"> | null): string {
|
|
73
|
+
return asOf?.date ? `OI as of ${formatCalendarDay(asOf.date)}` : "OI date not published";
|
|
74
|
+
}
|