gloomberb 0.17.1 → 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/earnings.ts +4 -2
- 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.tsx +1 -1
- package/src/architecture/core-builtin-allowlist.json +4 -9
- package/src/cli/commands/market.ts +34 -16
- package/src/cli/commands/options-chain.ts +29 -33
- package/src/cli/commands/options-leaps.ts +462 -63
- package/src/cli/commands/search.ts +10 -7
- package/src/cli/commands/ticker.ts +1 -1
- package/src/cli/known-gaps.ts +22 -0
- package/src/cli/pane-functions/catalog.ts +6 -3
- package/src/cli/pane-functions/dom.ts +17 -2
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/index.ts +5 -3
- package/src/cli/pane-functions/report-notices.ts +15 -0
- package/src/cli/symbol-args.ts +19 -0
- 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/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/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/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/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 +1 -1
- 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/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/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/help/feedback-tab.tsx +1 -1
- 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/open-interest-as-of.ts +16 -5
- package/src/plugins/builtin/options/view.tsx +19 -7
- 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/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/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/earnings-report.ts +10 -4
- package/src/renderers/dom/styles.css +4 -2
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -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 +6 -0
- package/src/utils/decimal-comma.ts +18 -0
- package/src/utils/exchanges.ts +7 -0
- package/src/utils/forward-dividend.ts +39 -9
- package/src/utils/option-expiry.ts +16 -0
- package/src/utils/sec.ts +2 -1
|
@@ -2,10 +2,13 @@ import type { DataTableCell, DataTableColumn } from "../../../components";
|
|
|
2
2
|
import { TABLE_COLUMN_GAP, tableColumnWidth } from "../../../components/ui/table-layout";
|
|
3
3
|
import { colors } from "../../../theme/colors";
|
|
4
4
|
import { TextAttributes } from "../../../ui";
|
|
5
|
+
import { epsBasisShort, showsEpsBasis } from "../shared/earnings-report";
|
|
5
6
|
import type { BoardRow, BoardSort } from "./board-model";
|
|
6
7
|
import { epsText, impliedText, moneyText, moveSize, TIMING_LABEL, weekday } from "./format";
|
|
7
8
|
|
|
8
|
-
type BoardColumnId =
|
|
9
|
+
type BoardColumnId =
|
|
10
|
+
| "owned" | "symbol" | "name" | "when" | "status" | "cap" | "epsEstimate" | "analysts" | "epsActual" | "basis"
|
|
11
|
+
| "revenueEstimate" | "implied" | "average";
|
|
9
12
|
|
|
10
13
|
export type BoardColumn = DataTableColumn & { id: BoardColumnId };
|
|
11
14
|
|
|
@@ -16,16 +19,19 @@ const ALL_COLUMNS: BoardColumn[] = [
|
|
|
16
19
|
{ id: "symbol", label: "TICKER", width: 6, align: "left" },
|
|
17
20
|
{ id: "name", label: "NAME", width: NAME_MIN_WIDTH, align: "left" },
|
|
18
21
|
{ id: "when", label: "WHEN", width: 7, align: "left" },
|
|
22
|
+
{ id: "status", label: "STATUS", width: 9, align: "left" },
|
|
19
23
|
{ id: "implied", label: "IMPLIED", width: 7, align: "right" },
|
|
20
24
|
{ id: "average", label: "AVG MOVE", width: 8, align: "right" },
|
|
21
25
|
{ id: "epsEstimate", label: "EPS EST", width: 7, align: "right" },
|
|
26
|
+
{ id: "analysts", label: "ESTS", width: 4, align: "right" },
|
|
22
27
|
{ id: "epsActual", label: "EPS ACT", width: 7, align: "right" },
|
|
28
|
+
{ id: "basis", label: "BASIS", width: 5, align: "left" },
|
|
23
29
|
{ id: "revenueEstimate", label: "SALES EST", width: 9, align: "right" },
|
|
24
30
|
{ id: "cap", label: "MKT CAP", width: 8, align: "right" },
|
|
25
31
|
];
|
|
26
32
|
|
|
27
|
-
/** Dropped first when narrow; the implied move and its average stay beside the name. */
|
|
28
|
-
const DROP_ORDER: BoardColumnId[] = ["epsActual", "cap", "revenueEstimate", "epsEstimate", "owned"];
|
|
33
|
+
/** Dropped first when narrow, the report's labels before its figures; the implied move and its average stay beside the name. */
|
|
34
|
+
const DROP_ORDER: BoardColumnId[] = ["basis", "analysts", "status", "epsActual", "cap", "revenueEstimate", "epsEstimate", "owned"];
|
|
29
35
|
|
|
30
36
|
/** Header clicks sort these; the rest keep the ranking. */
|
|
31
37
|
export const SORTABLE: Partial<Record<BoardColumnId, BoardSort["column"]>> = {
|
|
@@ -66,14 +72,23 @@ export function renderBoardCell(row: BoardRow, column: BoardColumn): DataTableCe
|
|
|
66
72
|
// An expected time, from the company's last reports, reads quieter than an announced one.
|
|
67
73
|
return { text: row.showDay ? `${weekday(report.date)} ${timing}` : timing, color: report.expectedTiming ? colors.textMuted : dim };
|
|
68
74
|
}
|
|
75
|
+
case "status":
|
|
76
|
+
// A projected date reads quieter than an announced one, as an expected time does.
|
|
77
|
+
return { text: report.dateStatus ?? "--", color: report.dateStatus === "projected" ? colors.textMuted : dim };
|
|
69
78
|
case "cap":
|
|
70
79
|
return { text: moneyText(report.marketCap), color: dim };
|
|
71
80
|
case "epsEstimate":
|
|
72
81
|
return { text: epsText(report.epsEstimate), color: dim };
|
|
82
|
+
case "analysts":
|
|
83
|
+
return { text: report.epsAnalysts == null ? "--" : String(report.epsAnalysts), color: dim };
|
|
73
84
|
case "epsActual": {
|
|
74
85
|
const beat = report.epsActual != null && report.epsEstimate != null ? report.epsActual - report.epsEstimate : null;
|
|
75
86
|
return { text: report.epsActual == null ? "" : epsText(report.epsActual), color: beat == null || beat === 0 ? colors.text : beat > 0 ? colors.positive : colors.negative };
|
|
76
87
|
}
|
|
88
|
+
case "basis":
|
|
89
|
+
return !showsEpsBasis(report)
|
|
90
|
+
? { text: "" }
|
|
91
|
+
: { text: epsBasisShort(report.epsBasis), color: report.epsBasis ? colors.textMuted : dim };
|
|
77
92
|
case "revenueEstimate":
|
|
78
93
|
return { text: moneyText(report.revenueEstimate), color: dim };
|
|
79
94
|
case "implied":
|
|
@@ -13,7 +13,7 @@ import { fetchEarningsCalendar, fetchEarningsHistory } from "./client";
|
|
|
13
13
|
import { loadEarningsCalendar } from "./data/cache";
|
|
14
14
|
import { epsText, impliedText, moneyText, moveSize, signedPercent, TIMING_LABEL } from "./format";
|
|
15
15
|
import { historyRows } from "./history-model";
|
|
16
|
-
import { EPS_BASIS_NOTE, epsBasisName } from "../shared/earnings-report";
|
|
16
|
+
import { EPS_BASIS_NOTE, epsBasisName, showsEpsBasis } from "../shared/earnings-report";
|
|
17
17
|
import { isUsListingExchange } from "../../../utils/exchanges";
|
|
18
18
|
import { listingAbroadVenue } from "../../../utils/sec";
|
|
19
19
|
import { findUsListingOfCompany } from "../../../tickers/us-listing";
|
|
@@ -24,17 +24,23 @@ const num = (value: unknown) => (typeof value === "number" && Number.isFinite(va
|
|
|
24
24
|
|
|
25
25
|
const BOARD_COLUMNS: HeadlessPaneColumn[] = [
|
|
26
26
|
{ key: "date", header: "Date" },
|
|
27
|
+
{ key: "dateStatus", header: "Status" },
|
|
27
28
|
{ key: "timing", header: "When" },
|
|
28
29
|
{ key: "symbol", header: "Ticker" },
|
|
29
30
|
{ key: "name", header: "Name" },
|
|
30
31
|
{ key: "marketCap", header: "Mkt cap", align: right, format: (value) => moneyText(num(value)) },
|
|
31
32
|
{ key: "epsEstimate", header: "EPS est", align: right, format: (value) => epsText(num(value)) },
|
|
33
|
+
{ key: "epsAnalysts", header: "Ests", align: right },
|
|
32
34
|
{ key: "epsActual", header: "EPS act", align: right, format: (value) => epsText(num(value)) },
|
|
35
|
+
{ key: "epsBasis", header: "Basis" },
|
|
33
36
|
{ key: "revenueEstimate", header: "Sales est", align: right, format: (value) => moneyText(num(value)) },
|
|
34
37
|
{ key: "impliedMove", header: "Implied", align: right, format: (value) => impliedText(num(value)) },
|
|
35
38
|
{ key: "averageMove", header: "Avg move", align: right, format: (value) => moveSize(num(value)) },
|
|
36
39
|
];
|
|
37
40
|
|
|
41
|
+
/** Where the board's caps come from and how old they can be, in its metadata. */
|
|
42
|
+
const MARKET_CAP_NOTE = "Each company's market cap as RV shows it: its latest financials snapshot at the regular session's price (the close outside the session), from the newest quote Gloom Cloud holds for it. A company with no snapshot shows the earnings calendar's figure.";
|
|
43
|
+
|
|
38
44
|
const HISTORY_COLUMNS: HeadlessPaneColumn[] = [
|
|
39
45
|
{ key: "date", header: "Date" },
|
|
40
46
|
{ key: "dateStatus", header: "Status" },
|
|
@@ -105,13 +111,16 @@ async function boardRows(args: HeadlessPaneLoadArgs, context: HeadlessPaneContex
|
|
|
105
111
|
const all = [...reports, ...extra].sort((left, right) => left.date.localeCompare(right.date) || (right.marketCap ?? 0) - (left.marketCap ?? 0));
|
|
106
112
|
const rows = all.slice(0, limit).map((report) => ({
|
|
107
113
|
date: report.date,
|
|
114
|
+
dateStatus: report.dateStatus,
|
|
108
115
|
timing: report.timing ? TIMING_LABEL[report.timing] : null,
|
|
109
116
|
timingExpected: report.expectedTiming,
|
|
110
117
|
symbol: report.symbol,
|
|
111
118
|
name: report.name,
|
|
112
119
|
marketCap: report.marketCap,
|
|
113
120
|
epsEstimate: report.epsEstimate,
|
|
121
|
+
epsAnalysts: report.epsAnalysts,
|
|
114
122
|
epsActual: report.epsActual,
|
|
123
|
+
epsBasis: showsEpsBasis(report) ? epsBasisName(report.epsBasis) : null,
|
|
115
124
|
revenueEstimate: report.revenueEstimate,
|
|
116
125
|
impliedMove: report.impliedMove,
|
|
117
126
|
averageMove: report.averageMove,
|
|
@@ -127,6 +136,8 @@ async function boardRows(args: HeadlessPaneLoadArgs, context: HeadlessPaneContex
|
|
|
127
136
|
from: query.from,
|
|
128
137
|
to: query.to,
|
|
129
138
|
averageMoveOver: `last 8 reports, at least ${MIN_AVERAGE_REPORTS}`,
|
|
139
|
+
epsBasis: EPS_BASIS_NOTE,
|
|
140
|
+
marketCap: MARKET_CAP_NOTE,
|
|
130
141
|
total: all.length,
|
|
131
142
|
returned: rows.length,
|
|
132
143
|
truncated: rows.length < all.length,
|
|
@@ -168,7 +179,7 @@ async function historyReport(symbol: string, context: HeadlessPaneContext, deps:
|
|
|
168
179
|
epsEstimate: row.epsEstimate,
|
|
169
180
|
epsAnalysts: row.epsAnalysts,
|
|
170
181
|
epsActual: row.epsActual,
|
|
171
|
-
epsBasis: row
|
|
182
|
+
epsBasis: showsEpsBasis(row) ? epsBasisName(row.epsBasis) : null,
|
|
172
183
|
epsSurprise: row.epsSurprise,
|
|
173
184
|
revenueEstimate: row.revenueEstimate,
|
|
174
185
|
revenueActual: row.revenueActual,
|
|
@@ -87,7 +87,7 @@ export function historyRows(payload: EarningsHistoryPayload | null, today: strin
|
|
|
87
87
|
epsEstimate: coherentEarningsValue(upcoming, "epsEstimate"),
|
|
88
88
|
epsAnalysts: coherentEarningsValue(upcoming, "epsAnalysts"),
|
|
89
89
|
epsActual: null,
|
|
90
|
-
epsBasis:
|
|
90
|
+
epsBasis: earningsEpsBasis(upcoming.epsBasis),
|
|
91
91
|
epsSurprise: null,
|
|
92
92
|
revenueEstimate: coherentEarningsValue(upcoming, "revenueEstimate"),
|
|
93
93
|
revenueActual: null,
|
|
@@ -3,7 +3,7 @@ import { TABLE_COLUMN_GAP, tableColumnWidth } from "../../../components/ui/table
|
|
|
3
3
|
import { colors } from "../../../theme/colors";
|
|
4
4
|
import { TextAttributes } from "../../../ui";
|
|
5
5
|
import { datedYear, epsText, fiscalMonth, impliedText, moneyText, signedPercent, TIMING_LABEL } from "./format";
|
|
6
|
-
import { epsBasisShort } from "../shared/earnings-report";
|
|
6
|
+
import { epsBasisShort, showsEpsBasis } from "../shared/earnings-report";
|
|
7
7
|
import type { HistoryRow } from "./history-model";
|
|
8
8
|
|
|
9
9
|
type HistoryColumnId =
|
|
@@ -74,7 +74,7 @@ export function renderHistoryCell(row: HistoryRow, column: HistoryColumn): DataT
|
|
|
74
74
|
case "epsActual":
|
|
75
75
|
return { text: epsText(row.epsActual), color: colors.text };
|
|
76
76
|
case "basis":
|
|
77
|
-
return row
|
|
77
|
+
return !showsEpsBasis(row)
|
|
78
78
|
? { text: "--", color: dim }
|
|
79
79
|
: { text: epsBasisShort(row.epsBasis), color: row.epsBasis ? colors.textMuted : dim };
|
|
80
80
|
case "epsSurprise":
|
|
@@ -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" },
|
|
@@ -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);
|
|
@@ -4,11 +4,11 @@ import { loadOpenInterest, type OpenInterestPayload, type OptionsPositioningApi
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The session a chain's open interest is counted at
|
|
7
|
-
* (docs/research-data.md#greeks-spot-and-time).
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* OPX's: every contract with a count in both agrees.
|
|
11
|
-
*
|
|
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
12
|
*/
|
|
13
13
|
export interface OpenInterestAsOf {
|
|
14
14
|
/** The session the chain's counts are as of, YYYY-MM-DD; null when it cannot be established. */
|
|
@@ -21,6 +21,17 @@ export interface OpenInterestAsOf {
|
|
|
21
21
|
matched: number;
|
|
22
22
|
}
|
|
23
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
|
+
|
|
24
35
|
export function chainOpenInterestAsOf(
|
|
25
36
|
chain: Pick<OptionsChain, "calls" | "puts">,
|
|
26
37
|
listed: Pick<OpenInterestPayload, "oiDate" | "strikes">,
|
|
@@ -6,7 +6,9 @@ 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
8
|
import { formatExpDate, resolveOptionsTarget } from "../../../utils/options";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
expiryIsoDate, findListedExpiry, formatDaysToExpiry, daysToExpiry, missingExpiryMessage, missingExpiryText, readOptionExpiration,
|
|
11
|
+
} from "../../../utils/option-expiry";
|
|
10
12
|
import { canonicalTickerKey } from "../../../utils/exchanges";
|
|
11
13
|
import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../../market-data/options-alternatives";
|
|
12
14
|
import { useChartQueries, useOptionsQuery, useResolvedEntryValue, useTickerFinancials } from "../../../market-data/hooks";
|
|
@@ -73,7 +75,7 @@ import { useOptionsSessionOpen, useThrottledValue } from "../shared/volatility/l
|
|
|
73
75
|
import { useRemoteUiNode } from "../../../remote/semantic-tree";
|
|
74
76
|
import { optionCarry, optionCostOfSpot } from "./carry";
|
|
75
77
|
import { formatModelSpotPrice, modelSpotLabel, optionModelSpot } from "./model-spot";
|
|
76
|
-
import { chainOpenInterestAsOf, loadListedOpenInterest, openInterestAsOfLabel } from "./open-interest-as-of";
|
|
78
|
+
import { chainOpenInterestAsOf, loadListedOpenInterest, openInterestAsOfLabel, statedOpenInterestAsOf } from "./open-interest-as-of";
|
|
77
79
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
78
80
|
import {
|
|
79
81
|
ALL_STRIKES,
|
|
@@ -402,7 +404,7 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
402
404
|
const expiryAsOfMs = Number.isFinite(chainAsOfMs) ? chainAsOfMs : Date.now();
|
|
403
405
|
const expiryDate = selectedExpiration == null ? "" : expiryIsoDate(selectedExpiration);
|
|
404
406
|
const expiryDays = selectedExpiration == null ? "" : formatDaysToExpiry(selectedExpiration, expiryAsOfMs);
|
|
405
|
-
useRemoteUiNode(expiryDate && chain ? {
|
|
407
|
+
useRemoteUiNode(expiryDate && chain && !expirationUnavailable ? {
|
|
406
408
|
role: "report-notice",
|
|
407
409
|
label: "Expiry",
|
|
408
410
|
getMetadata: () => ({
|
|
@@ -411,6 +413,12 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
411
413
|
value: { date: expiryDate, daysToExpiry: daysToExpiry(selectedExpiration!, expiryAsOfMs) },
|
|
412
414
|
}),
|
|
413
415
|
} : null);
|
|
416
|
+
// `fn OMON --expiration` on a date the chain cannot show fails as `options --expiration` does.
|
|
417
|
+
useRemoteUiNode(expirationUnavailable && selectedExpiration != null && effectiveTicker ? {
|
|
418
|
+
role: "report-failure",
|
|
419
|
+
label: "Expiry unavailable",
|
|
420
|
+
getMetadata: () => missingExpiryMessage(effectiveTicker, selectedExpiration, availableExpirations),
|
|
421
|
+
} : null);
|
|
414
422
|
// A refresh that failed keeps the last chain, which is then stale.
|
|
415
423
|
const asOfEntry = strikeChain == null ? null : strikeChain === expirationChain ? expirationChainEntry : initialChainEntry;
|
|
416
424
|
const chainDelay = asOfChain?.delayMinutes ?? 0;
|
|
@@ -529,12 +537,16 @@ export function OptionsView({ width, height, focused, nestedInTabs = false, ivRa
|
|
|
529
537
|
isPositionStrike: !!parsed && strike === parsed.strike,
|
|
530
538
|
};
|
|
531
539
|
}), [callsByStrike, dividendYield, greeksSpot, parsed, putsByStrike, spot, strikes, volatilities]);
|
|
532
|
-
// The open interest's session
|
|
533
|
-
|
|
540
|
+
// The open interest's session: the chain's own, or for a chain that does not say, OPX's listing of the
|
|
541
|
+
// same expiry, stated only when the counts are OPX's.
|
|
542
|
+
const statedOpenInterest = useMemo(() => strikeChain ? statedOpenInterestAsOf(strikeChain) : undefined, [strikeChain]);
|
|
543
|
+
const listedExpiry = selectedExpiration == null || !effectiveTicker || !strikeChain || statedOpenInterest
|
|
544
|
+
? null : expiryIsoDate(selectedExpiration);
|
|
534
545
|
const listedLoader = useCallback(() => loadListedOpenInterest(effectiveTicker, listedExpiry!), [effectiveTicker, listedExpiry]);
|
|
535
546
|
const listedOpenInterest = useAsyncResource(listedExpiry ? listedLoader : null);
|
|
536
|
-
const openInterestAsOf = useMemo(() =>
|
|
537
|
-
|
|
547
|
+
const openInterestAsOf = useMemo(() => statedOpenInterest
|
|
548
|
+
?? (strikeChain && listedOpenInterest.data ? chainOpenInterestAsOf(strikeChain, listedOpenInterest.data) : null),
|
|
549
|
+
[statedOpenInterest, strikeChain, listedOpenInterest.data]);
|
|
538
550
|
// A report of the pane (`gloomberb fn OMON`) names them too.
|
|
539
551
|
useRemoteUiNode(modelSpot ? {
|
|
540
552
|
role: "report-notice",
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { FieldGrid, KeyValueRow, QueryBar, usePaneFooter, usePaneNoticeFooter, type GridField } from "../../../components";
|
|
3
|
+
import { CompositeChart } from "../../../components/chart/composite/composite-chart";
|
|
3
4
|
import { useAsyncResource, useInputCapture } from "../../../public/react";
|
|
4
5
|
import { useShortcut } from "../../../react/input";
|
|
5
6
|
import { useAppSelector, usePaneInstance, usePaneStateValue } from "../../../state/app/context";
|
|
6
7
|
import { colors } from "../../../theme/colors";
|
|
7
8
|
import type { PaneProps } from "../../../types/plugin";
|
|
8
|
-
import { Box, ScrollBox } from "../../../ui";
|
|
9
|
+
import { Box, ScrollBox, Text } from "../../../ui";
|
|
9
10
|
import { formatNumber } from "../../../utils/format";
|
|
10
11
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
11
12
|
import { OPTIONS_CALCULATOR_PANE_ID, describeDraftProblem, draftFromParams, reconcileOptionCalcDraft, updateOptionCalcDraft, type OptionCalcDraft } from "./model";
|
|
@@ -20,6 +21,8 @@ import { useThrottledValue } from "../shared/volatility/live-session";
|
|
|
20
21
|
import { valueBinomialOption, solveBinomialImpliedVolatility, effectiveBinomialSteps } from "./binomial";
|
|
21
22
|
import { draftFromCalculatorInputs, parseCashDividends } from "./inputs";
|
|
22
23
|
import { loadCalculatorSurfaceVol } from "./surface";
|
|
24
|
+
import { loadContractPremiumHistory, premiumSeries } from "./premium-history";
|
|
25
|
+
import type { ResolvedSeries } from "../../../time-series/types";
|
|
23
26
|
import { OptionQuoteContext, optionQuoteContextHeight } from "../options/quote-context";
|
|
24
27
|
import { useCalculatorEvidence, CALCULATOR_IGNORED_DIVIDENDS_NOTICE, type CalculatorScreenshotSnapshot } from "./evidence";
|
|
25
28
|
|
|
@@ -89,6 +92,12 @@ export function OptionsCalculatorPane({ focused, width, height }: PaneProps) {
|
|
|
89
92
|
// any edit is theirs and ends the link for that input.
|
|
90
93
|
const liveStreaming = useLiveStreamingSetting();
|
|
91
94
|
const reference = draft.marketReference;
|
|
95
|
+
const contractSymbol = screenshotSnapshot ? null : reference?.contractSymbol ?? null;
|
|
96
|
+
const loadPremium = useCallback(async () => {
|
|
97
|
+
if (!contractSymbol) return [];
|
|
98
|
+
return loadContractPremiumHistory(contractSymbol);
|
|
99
|
+
}, [contractSymbol]);
|
|
100
|
+
const premiumResource = useAsyncResource(contractSymbol ? loadPremium : null);
|
|
92
101
|
const priceLinked = !screenshotSnapshot && !!draft.marketPriceSource && !!reference && !!draft.symbol;
|
|
93
102
|
const spotLinked = priceLinked && draft.spot === seed.spot;
|
|
94
103
|
// One stable subscription while the price is linked: dropping the
|
|
@@ -331,6 +340,15 @@ export function OptionsCalculatorPane({ focused, width, height }: PaneProps) {
|
|
|
331
340
|
{linkedDraft.marketReference && <Box paddingX={1} height={referenceHeight} flexShrink={0}>
|
|
332
341
|
<OptionQuoteContext reference={linkedDraft.marketReference} width={resultWidth} height={referenceHeight} snapshot={!liveInputs} scrollable={false} />
|
|
333
342
|
</Box>}
|
|
343
|
+
{contractSymbol ? (
|
|
344
|
+
<ContractPremiumChart
|
|
345
|
+
series={premiumSeries(premiumResource.data ?? [], colors.positive)}
|
|
346
|
+
loading={premiumResource.loading && !premiumResource.data}
|
|
347
|
+
unavailable={premiumResource.error != null && !premiumResource.data}
|
|
348
|
+
width={resultWidth}
|
|
349
|
+
focused={focused}
|
|
350
|
+
/>
|
|
351
|
+
) : null}
|
|
334
352
|
|
|
335
353
|
<Box flexDirection={pairMetrics ? "row" : "column"} paddingX={1}>
|
|
336
354
|
<KeyValueRow
|
|
@@ -366,6 +384,43 @@ export function OptionsCalculatorPane({ focused, width, height }: PaneProps) {
|
|
|
366
384
|
);
|
|
367
385
|
}
|
|
368
386
|
|
|
387
|
+
const PREMIUM_CHART_ROWS = 8;
|
|
388
|
+
|
|
389
|
+
function ContractPremiumChart({
|
|
390
|
+
series,
|
|
391
|
+
loading,
|
|
392
|
+
unavailable,
|
|
393
|
+
width,
|
|
394
|
+
focused,
|
|
395
|
+
}: {
|
|
396
|
+
series: ResolvedSeries[];
|
|
397
|
+
loading: boolean;
|
|
398
|
+
unavailable: boolean;
|
|
399
|
+
width: number;
|
|
400
|
+
focused: boolean;
|
|
401
|
+
}) {
|
|
402
|
+
const empty = unavailable ? "Premium history unavailable." : series.length < 1 ? "No premium history." : undefined;
|
|
403
|
+
return (
|
|
404
|
+
<Box flexDirection="column" paddingX={1} height={PREMIUM_CHART_ROWS} flexShrink={0}>
|
|
405
|
+
{empty && !loading ? (
|
|
406
|
+
<Text fg={colors.textDim}>{empty}</Text>
|
|
407
|
+
) : (
|
|
408
|
+
<CompositeChart
|
|
409
|
+
series={series}
|
|
410
|
+
panels={[{ id: "main" }]}
|
|
411
|
+
width={width}
|
|
412
|
+
height={PREMIUM_CHART_ROWS}
|
|
413
|
+
focused={focused}
|
|
414
|
+
navigable={false}
|
|
415
|
+
remoteKind="option-premium-history"
|
|
416
|
+
formatValue={(value) => formatNumber(value, 2)}
|
|
417
|
+
emptyMessage={loading ? "Loading..." : "No premium history."}
|
|
418
|
+
/>
|
|
419
|
+
)}
|
|
420
|
+
</Box>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
369
424
|
function SurfaceNotices({ notices, focused }: { notices: string[]; focused: boolean }) {
|
|
370
425
|
usePaneNoticeFooter({ registrationId: "ovme-surface-notices", notices, focused });
|
|
371
426
|
return null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { apiClient } from "../../../api-client";
|
|
2
|
+
import { scalarPoint, staticSeries } from "../../../components/chart/static/series";
|
|
3
|
+
import type { ResolvedSeries } from "../../../time-series/types";
|
|
4
|
+
import { OPTIONS_QUOTE_EXCHANGE } from "../options/live-quotes";
|
|
5
|
+
|
|
6
|
+
/** Daily closes for one listed contract. A typed premium has no contract to chart. */
|
|
7
|
+
export async function loadContractPremiumHistory(
|
|
8
|
+
contractSymbol: string,
|
|
9
|
+
signal?: AbortSignal,
|
|
10
|
+
): Promise<{ date: string; close: number }[]> {
|
|
11
|
+
const response = await apiClient.getCloudHistory(contractSymbol, OPTIONS_QUOTE_EXCHANGE, {
|
|
12
|
+
interval: "1day",
|
|
13
|
+
outputsize: 400,
|
|
14
|
+
}, signal ? { signal } : {});
|
|
15
|
+
const rows = response.data ?? [];
|
|
16
|
+
return rows.flatMap((row) => {
|
|
17
|
+
const close = row.close;
|
|
18
|
+
if (!row.date || !Number.isFinite(close) || close < 0) return [];
|
|
19
|
+
const time = Date.parse(row.date);
|
|
20
|
+
if (!Number.isFinite(time)) return [];
|
|
21
|
+
return [{ date: row.date, close }];
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function premiumSeries(
|
|
26
|
+
rows: readonly { date: string; close: number }[],
|
|
27
|
+
color: string,
|
|
28
|
+
): ResolvedSeries[] {
|
|
29
|
+
if (rows.length < 2) return [];
|
|
30
|
+
return [staticSeries(
|
|
31
|
+
rows.map((row) => scalarPoint(new Date(row.date), row.close)),
|
|
32
|
+
{ id: "premium", label: "Premium", color, calendarSpaced: true },
|
|
33
|
+
)];
|
|
34
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { AppConfig } from "../../../types/config";
|
|
|
2
2
|
import type { Portfolio, PortfolioCash, TickerPosition, TickerRecord, Watchlist } from "../../../types/ticker";
|
|
3
3
|
import { resolveCurrencyUnit } from "../../../utils/currency-units";
|
|
4
4
|
import { slugifyName } from "../../../utils/slugify";
|
|
5
|
-
import { parseCollectionRef } from "
|
|
5
|
+
import { parseCollectionRef } from "../../../team/collections";
|
|
6
6
|
import { findNamedCollection } from "../../../portfolio/collection-name";
|
|
7
7
|
|
|
8
8
|
export interface DeleteWatchlistResult {
|
|
@@ -2,7 +2,7 @@ import { t, tf } from "../../../../i18n";
|
|
|
2
2
|
import { formatMarketCost, formatMarketQuantity } from "../../../../market-data/market/format";
|
|
3
3
|
import type { AppConfig } from "../../../../types/config";
|
|
4
4
|
import type { TickerRecord } from "../../../../types/ticker";
|
|
5
|
-
import { parseCollectionRef } from "
|
|
5
|
+
import { parseCollectionRef } from "../../../../team/collections";
|
|
6
6
|
import { isManualPortfolio, removeTickerFromPortfolio, removeTickerFromWatchlist } from "../mutations";
|
|
7
7
|
|
|
8
8
|
export interface RemovableCollection {
|
|
@@ -285,9 +285,11 @@ export function buildEventDetail({
|
|
|
285
285
|
const comparison: EventDetailBlock[] = [
|
|
286
286
|
...(row.dateStatus && row.dateStatus !== "reported" ? [detailRow("Report date", row.date, row.dateStatus)] : []),
|
|
287
287
|
detailRow("Actual", earningsInput(row.epsActual, row.epsCurrency),
|
|
288
|
-
row.epsBasis === "unknown" ? "basis unknown" : row.epsBasis),
|
|
288
|
+
row.epsActual == null ? undefined : row.epsBasis === "unknown" ? "basis unknown" : row.epsBasis),
|
|
289
|
+
// An upcoming estimate names its basis when it is adjusted (a REIT's FFO consensus).
|
|
289
290
|
detailRow("Consensus", earningsInput(row.epsEstimate, row.epsCurrency),
|
|
290
|
-
row.epsAnalysts != null ? `${row.epsAnalysts} analysts` :
|
|
291
|
+
[row.epsAnalysts != null ? `${row.epsAnalysts} analysts` : null,
|
|
292
|
+
row.epsActual == null && row.epsBasis === "adjusted" ? "adjusted" : null].filter(Boolean).join(", ") || undefined),
|
|
291
293
|
];
|
|
292
294
|
if (row.epsDifference != null) comparison.push(detailRow("Difference", earningsInput(row.epsDifference, row.epsCurrency)));
|
|
293
295
|
if (row.surprisePercent != null) {
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
epsBasisName,
|
|
21
21
|
epsSurprise,
|
|
22
22
|
inFiscalMonth,
|
|
23
|
+
showsEpsBasis,
|
|
23
24
|
type EpsBasisName,
|
|
24
25
|
} from "../shared/earnings-report";
|
|
25
26
|
|
|
@@ -46,7 +47,7 @@ export interface EventRow {
|
|
|
46
47
|
epsEstimate?: number;
|
|
47
48
|
epsDifference?: number;
|
|
48
49
|
surprisePercent?: number;
|
|
49
|
-
/** A reported figure's basis; the same quarter is named the same way in ERN. */
|
|
50
|
+
/** A reported figure's basis, or an upcoming estimate's when it is adjusted; the same quarter is named the same way in ERN. */
|
|
50
51
|
epsBasis?: EpsBasisName;
|
|
51
52
|
/** Analysts behind `epsEstimate`, when known. */
|
|
52
53
|
epsAnalysts?: number;
|
|
@@ -224,11 +225,12 @@ function ttmRow(
|
|
|
224
225
|
};
|
|
225
226
|
}
|
|
226
227
|
|
|
227
|
-
/** A reported row names its basis and beat; a pending one whether its date is announced and
|
|
228
|
+
/** A reported row names its basis and beat; a pending one whether its date is announced, its analysts and an adjusted estimate's basis. */
|
|
228
229
|
function earningsDetail(earning: CorporateActionsData["earnings"][number]): string {
|
|
229
230
|
if (earning.epsActual == null) {
|
|
230
231
|
const status = earningsDateStatus(earning.dateStatus);
|
|
231
|
-
const parts = [status === "reported" ? null : status, earning.epsAnalysts != null ? `${earning.epsAnalysts} analysts` : null
|
|
232
|
+
const parts = [status === "reported" ? null : status, earning.epsAnalysts != null ? `${earning.epsAnalysts} analysts` : null,
|
|
233
|
+
earningsEpsBasis(earning.epsBasis) === "adjusted" ? BASIS_TEXT.adjusted : null];
|
|
232
234
|
return parts.filter(Boolean).join(", ") || "Pending";
|
|
233
235
|
}
|
|
234
236
|
const basis = BASIS_TEXT[epsBasisName(earningsEpsBasis(earning.epsBasis))];
|
|
@@ -341,7 +343,8 @@ export function buildEventRows(
|
|
|
341
343
|
epsEstimate: earning.epsEstimate,
|
|
342
344
|
epsDifference: earning.epsActual == null ? undefined : earning.difference,
|
|
343
345
|
surprisePercent,
|
|
344
|
-
epsBasis: earning.epsActual
|
|
346
|
+
epsBasis: showsEpsBasis({ epsActual: earning.epsActual, epsBasis: earningsEpsBasis(earning.epsBasis) })
|
|
347
|
+
? epsBasisName(earningsEpsBasis(earning.epsBasis)) : undefined,
|
|
345
348
|
epsAnalysts: earning.epsAnalysts,
|
|
346
349
|
dateStatus: earningsDateStatus(earning.dateStatus) ?? undefined,
|
|
347
350
|
providerId: data?.providerId,
|
|
@@ -37,6 +37,7 @@ export const SERIES_INVENTORY: readonly SeriesInventoryEntry[] = [
|
|
|
37
37
|
{ module: "realized-vol", chart: "Realized volatility cone and rolling windows", notInCatalog: "derived", reason: "The composer's realized volatility study draws it from any price series." },
|
|
38
38
|
{ module: "vol-surface", chart: "Implied volatility surface by strike and expiry", notInCatalog: "not-a-time-series", reason: "A surface over strike and tenor." },
|
|
39
39
|
{ module: "options-positioning", chart: "Open interest and gamma by strike and expiry", notInCatalog: "not-a-time-series", reason: "Plotted by strike and expiry, not by date." },
|
|
40
|
+
{ module: "options-calculator", chart: "A linked option contract's daily premium", series: ["option:*"] },
|
|
40
41
|
{ module: "options-scenario", chart: "Option payoff by underlying price", notInCatalog: "not-a-time-series", reason: "A payoff curve over prices." },
|
|
41
42
|
{ module: "kelly-sizer", chart: "Position size against win rate", notInCatalog: "account", reason: "Drawn from the inputs typed into the sizer." },
|
|
42
43
|
{ module: "yield-curve", chart: "Treasury curve and each maturity's history", series: ["ust:*", "fred:T10Y2Y", "fred:T10Y3M", "fred:DFII*"] },
|
|
@@ -2,9 +2,10 @@ import type { EarningsDateStatus, EarningsEpsBasis } from "../../../types/financ
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* How every earnings surface (ERN, EE, the corporate-actions events and
|
|
5
|
-
* their headless rows) reads and names a reported quarter's EPS basis
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* their headless rows) reads and names a reported quarter's EPS basis, an
|
|
6
|
+
* upcoming estimate's when Gloom Cloud names one, and a report date's
|
|
7
|
+
* status. Gloom Cloud resolves one figure per quarter; this keeps the panes
|
|
8
|
+
* from naming it differently.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
const BASES: ReadonlySet<string> = new Set<EarningsEpsBasis>(["gaap", "adjusted"]);
|
|
@@ -27,13 +28,18 @@ export function dateStatusFromEstimateFlag(isDateEstimate: boolean | null | unde
|
|
|
27
28
|
export type EpsBasisName = "GAAP" | "adjusted" | "unknown";
|
|
28
29
|
|
|
29
30
|
/** What the basis names mean, in every report's metadata. */
|
|
30
|
-
export const EPS_BASIS_NOTE = "One reported EPS per quarter. GAAP: the quarter's diluted statement EPS. adjusted: the consensus-basis figure the estimate is measured against, which leaves out items the statements include. unknown: no statement on a comparable share basis to tell.";
|
|
31
|
+
export const EPS_BASIS_NOTE = "One reported EPS per quarter. GAAP: the quarter's diluted statement EPS. adjusted: the consensus-basis figure the estimate is measured against, which leaves out items the statements include. unknown: no statement on a comparable share basis to tell. An upcoming report's estimate is labelled adjusted when it is the consensus on the adjusted basis the company's reports are on (a REIT's FFO), not a GAAP forecast.";
|
|
31
32
|
|
|
32
33
|
/** The basis as rows, exports and tools name it. */
|
|
33
34
|
export function epsBasisName(basis: EarningsEpsBasis | null): EpsBasisName {
|
|
34
35
|
return basis === "gaap" ? "GAAP" : basis === "adjusted" ? "adjusted" : "unknown";
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/** Whether a row names a basis: a reported figure always does, an upcoming estimate only when Gloom Cloud labels it. */
|
|
39
|
+
export function showsEpsBasis(row: { epsActual: number | null | undefined; epsBasis: EarningsEpsBasis | null | undefined }): boolean {
|
|
40
|
+
return row.epsActual != null || row.epsBasis != null;
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
/** The basis in a table cell or a detail line. */
|
|
38
44
|
export function epsBasisShort(basis: EarningsEpsBasis | null): string {
|
|
39
45
|
return basis === "gaap" ? "GAAP" : basis === "adjusted" ? "adj" : "?";
|
|
@@ -1280,14 +1280,16 @@ html[data-gloom-screenshot="true"] [data-gloom-role="chart-watermark"] {
|
|
|
1280
1280
|
}
|
|
1281
1281
|
|
|
1282
1282
|
/* Reading columns (src/components/reader) set their type from their own
|
|
1283
|
-
width, not from a count of cells:
|
|
1283
|
+
width, not from a count of cells: body text at 1.1667em of the app's size
|
|
1284
|
+
(14px), or 1.1em (13.2px) under 480px. The rest of the reader's scale is in
|
|
1285
|
+
em of it (reader-text.tsx). */
|
|
1284
1286
|
[data-gloom-ui="article-reader"],
|
|
1285
1287
|
[data-gloom-ui="detail-reader"] {
|
|
1286
1288
|
container-type: inline-size;
|
|
1287
1289
|
}
|
|
1288
1290
|
@container (max-width: 480px) {
|
|
1289
1291
|
[data-gloom-ui="reader-column"] {
|
|
1290
|
-
--reader-em: 1.
|
|
1292
|
+
--reader-em: 1.1;
|
|
1291
1293
|
}
|
|
1292
1294
|
}
|
|
1293
1295
|
/* A detail's facts: label and value pairs, two pairs to a row when the
|
|
@@ -311,6 +311,8 @@ export function mapOptionsChain(
|
|
|
311
311
|
delayMinutes: chain.delayMinutes,
|
|
312
312
|
realtimeEligible: chain.realtimeEligible,
|
|
313
313
|
asOf: chain.asOf,
|
|
314
|
+
// Left absent when the server does not send it, so the date is then established against OPX.
|
|
315
|
+
...(chain.openInterestAsOf !== undefined ? { openInterestAsOf: chain.openInterestAsOf } : {}),
|
|
314
316
|
};
|
|
315
317
|
}
|
|
316
318
|
|