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,6 +1,7 @@
|
|
|
1
1
|
import { aggregateLoadedCongress } from "./aggregates";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Box, ScrollBox, Text, useRendererHost, type ScrollBoxRenderable } from "../../../ui";
|
|
3
|
+
import { Box, ScrollBox, Text, useRendererHost, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
|
|
4
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
4
5
|
import {
|
|
5
6
|
DataTableView,
|
|
6
7
|
KeyValueRow,
|
|
@@ -51,6 +52,9 @@ export function TradeDetail({
|
|
|
51
52
|
width: number;
|
|
52
53
|
}) {
|
|
53
54
|
const lineWidth = Math.max(1, width - 2);
|
|
55
|
+
// The desktop's rows cut long values where they end, by CSS; the terminal at its columns.
|
|
56
|
+
const desktop = !!useUiCapabilities().nativePaneChrome;
|
|
57
|
+
const fit = (value: string) => desktop ? value : truncateWithEllipsis(value, Math.max(10, lineWidth - 16));
|
|
54
58
|
return (
|
|
55
59
|
<ScrollBox scrollY focusable={false} flexGrow={1} paddingX={1}>
|
|
56
60
|
<Box flexDirection="column" width={lineWidth}>
|
|
@@ -58,7 +62,7 @@ export function TradeDetail({
|
|
|
58
62
|
<KeyValueRow {...DETAIL_ROW} label="chamber" value={`${trade.chamber === "senate" ? "Senate" : "House"}${trade.stateDistrict ? ` · ${trade.stateDistrict}` : ""}`} tone="muted" />
|
|
59
63
|
<KeyValueRow {...DETAIL_ROW} label="party" value={trade.party ?? "--"} />
|
|
60
64
|
<KeyValueRow {...DETAIL_ROW} label="side" value={trade.transactionType} tone={trade.side === "BUY" ? "positive" : trade.side === "SELL" ? "negative" : trade.side === "OTHER" ? "muted" : undefined} />
|
|
61
|
-
<KeyValueRow {...DETAIL_ROW} label="asset" value={
|
|
65
|
+
<KeyValueRow {...DETAIL_ROW} label="asset" value={fit(trade.assetName)} />
|
|
62
66
|
<KeyValueRow {...DETAIL_ROW} label="amount" value={trade.amount} color={colors.textBright} />
|
|
63
67
|
<KeyValueRow {...DETAIL_ROW} label="owner" value={trade.owner} />
|
|
64
68
|
<KeyValueRow {...DETAIL_ROW} label="tx return" value={formatCongressReturn(trade.returnSinceTx)} />
|
|
@@ -69,12 +73,12 @@ export function TradeDetail({
|
|
|
69
73
|
<KeyValueRow {...DETAIL_ROW} label="filed" value={trade.filingDate} />
|
|
70
74
|
<KeyValueRow {...DETAIL_ROW} label="lag" value={`${formatLag(trade.lagDays)}${(trade.lagDays ?? 0) > 45 ? " (past 45 days)" : ""}`} tone={(trade.lagDays ?? 0) > 45 ? "warning" : undefined} />
|
|
71
75
|
{trade.filingStatus ? <KeyValueRow {...DETAIL_ROW} label="status" value={trade.filingStatus} /> : null}
|
|
72
|
-
{trade.subholdingOf ? <KeyValueRow {...DETAIL_ROW} label="subholding" value={
|
|
76
|
+
{trade.subholdingOf ? <KeyValueRow {...DETAIL_ROW} label="subholding" value={fit(trade.subholdingOf)} /> : null}
|
|
73
77
|
{trade.description ? (
|
|
74
78
|
<>
|
|
75
79
|
<Text>{" "}</Text>
|
|
76
80
|
<Text fg={colors.textDim}>description</Text>
|
|
77
|
-
<
|
|
81
|
+
<ClippedText fg={colors.text} text={trade.description} width={lineWidth} cut={truncateWithEllipsis} wrap />
|
|
78
82
|
</>
|
|
79
83
|
) : null}
|
|
80
84
|
</Box>
|
|
@@ -9,7 +9,8 @@ import type {
|
|
|
9
9
|
} from "../../../core/connection-health";
|
|
10
10
|
import { colors } from "../../../theme/colors";
|
|
11
11
|
import type { PaneProps } from "../../../types/plugin";
|
|
12
|
-
import { Box, ScrollBox, Text } from "../../../ui";
|
|
12
|
+
import { Box, ScrollBox, Text, useUiCapabilities } from "../../../ui";
|
|
13
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
13
14
|
import { truncateToDisplayWidth } from "../../../utils/format";
|
|
14
15
|
import { formatRelativeAge } from "../../../utils/datetime-format";
|
|
15
16
|
import { nextHeaderSort } from "../../../utils/sort-values";
|
|
@@ -29,6 +30,15 @@ const STATUS_ORDER: Record<ConnectionHealthStatus, number> = {
|
|
|
29
30
|
idle: 4,
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
function connectionKindLabel(kind: string): string {
|
|
34
|
+
if (kind === "asset-data") return "Market data";
|
|
35
|
+
if (kind === "news") return "News";
|
|
36
|
+
if (kind === "api") return "Request";
|
|
37
|
+
if (kind === "websocket") return "Live feed";
|
|
38
|
+
if (kind === "capability") return "Plugin";
|
|
39
|
+
return "Connection";
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
function statusLabel(status: ConnectionHealthStatus): string {
|
|
33
43
|
if (status === "connected") return "Connected";
|
|
34
44
|
if (status === "connecting") return "Connecting";
|
|
@@ -64,12 +74,13 @@ function columnsForWidth(width: number): ConnectionColumn[] {
|
|
|
64
74
|
|
|
65
75
|
function ConnectionDetail({ source, width, now }: { source: ConnectionHealthState; width: number; now: number }) {
|
|
66
76
|
const lineWidth = Math.max(24, width - 2);
|
|
77
|
+
// The desktop's detail row cuts a long value where it ends, by CSS.
|
|
78
|
+
const desktop = !!useUiCapabilities().nativePaneChrome;
|
|
67
79
|
return (
|
|
68
80
|
<ScrollBox scrollY focusable={false} flexGrow={1} paddingX={1}>
|
|
69
81
|
<Box flexDirection="column" width={lineWidth}>
|
|
70
82
|
<KeyValueRow labelWidth={16} emphasis={false} label="Status" value={statusLabel(source.status)} color={statusColor(source.status)} />
|
|
71
|
-
<KeyValueRow labelWidth={16} emphasis={false} label="Type" value={source.kind} color={colors.textDim} />
|
|
72
|
-
{source.ownerId ? <KeyValueRow labelWidth={16} emphasis={false} label="Owner" value={source.ownerId} color={colors.textDim} /> : null}
|
|
83
|
+
<KeyValueRow labelWidth={16} emphasis={false} label="Type" value={connectionKindLabel(source.kind)} color={colors.textDim} />
|
|
73
84
|
{source.socketState ? <KeyValueRow labelWidth={16} emphasis={false} label="Socket" value={source.socketState} color={statusColor(source.status)} /> : null}
|
|
74
85
|
{source.lastTransitionAt ? (
|
|
75
86
|
<KeyValueRow labelWidth={16} emphasis={false} label="Transition" value={formatRelativeAge(source.lastTransitionAt, now)} color={colors.textMuted} />
|
|
@@ -78,7 +89,7 @@ function ConnectionDetail({ source, width, now }: { source: ConnectionHealthStat
|
|
|
78
89
|
<KeyValueRow labelWidth={16} emphasis={false} label="Operation" value={source.lastOperation ?? "-"} color={colors.textDim} />
|
|
79
90
|
<KeyValueRow labelWidth={16} emphasis={false} label="Latency" value={formatLatency(source.lastLatencyMs)} color={colors.textMuted} />
|
|
80
91
|
{source.currentDetail ? (
|
|
81
|
-
<KeyValueRow labelWidth={16} emphasis={false} label="Detail" value={truncateToDisplayWidth(source.currentDetail, Math.max(8, lineWidth - 16))} color={source.status === "error" ? colors.negative : colors.textDim} />
|
|
92
|
+
<KeyValueRow labelWidth={16} emphasis={false} label="Detail" value={desktop ? source.currentDetail : truncateToDisplayWidth(source.currentDetail, Math.max(8, lineWidth - 16))} color={source.status === "error" ? colors.negative : colors.textDim} />
|
|
82
93
|
) : null}
|
|
83
94
|
{source.lastSuccess ? (
|
|
84
95
|
<KeyValueRow labelWidth={16} emphasis={false}
|
|
@@ -103,7 +114,7 @@ function ConnectionDetail({ source, width, now }: { source: ConnectionHealthStat
|
|
|
103
114
|
<Box width={7}><Text fg={request.success ? colors.positive : colors.negative}>{request.success ? "ok" : "error"}</Text></Box>
|
|
104
115
|
<Box width={9}><Text fg={colors.textMuted}>{formatLatency(request.latencyMs)}</Text></Box>
|
|
105
116
|
<Box width={10}><Text fg={colors.textMuted}>{formatRelativeAge(request.at, now)}</Text></Box>
|
|
106
|
-
<
|
|
117
|
+
<ClippedText fg={colors.textDim} text={request.error ?? request.operation} width={Math.max(8, lineWidth - 26)} />
|
|
107
118
|
</Box>
|
|
108
119
|
))}
|
|
109
120
|
</>
|
|
@@ -19,8 +19,8 @@ import { priceColor } from "../../../theme/colors";
|
|
|
19
19
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
20
20
|
import type { ResolvedSeries } from "../../../time-series/types";
|
|
21
21
|
import type { PaneProps } from "../../../types/plugin";
|
|
22
|
-
import { Box,
|
|
23
|
-
import {
|
|
22
|
+
import { Box, TextAttributes } from "../../../ui";
|
|
23
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
24
24
|
import { nextHeaderSort } from "../../../utils/sort-values";
|
|
25
25
|
import { CPI_NOT_AVAILABLE, getCachedCpiBoard, loadCpiBoard } from "./client";
|
|
26
26
|
import {
|
|
@@ -67,14 +67,15 @@ function CpiChart({ row, series, width, height }: { row: CpiRow; series: Resolve
|
|
|
67
67
|
const colors = useThemeColors();
|
|
68
68
|
const latest = row.history.at(-1)?.[0] ?? null;
|
|
69
69
|
const axis = useMemo(() => cpiTimeAxis(row.history), [row.history]);
|
|
70
|
-
|
|
70
|
+
// The plain read under the axis; the desktop cuts it where its line ends.
|
|
71
|
+
const read = row.read && width > 2 ? row.read : "";
|
|
71
72
|
return (
|
|
72
73
|
<Box width={width} height={height} flexDirection="column">
|
|
73
74
|
<CompositeChart series={series} panels={PANELS} width={width} height={Math.max(3, height - (read ? 1 : 0))}
|
|
74
75
|
focused={false} navigable={false} showLegend showTimeAxis cursorDate={latest ? cpiMonthDate(latest) : null}
|
|
75
76
|
viewport={axis?.viewport} xAxis={axis?.xAxis} clipToViewport={!!axis}
|
|
76
77
|
formatValue={legendValue} formatAxisValue={formatPercentAxis} remoteKind="cpi-component-history" />
|
|
77
|
-
{read ? <Box height={1} paddingX={1}><
|
|
78
|
+
{read ? <Box height={1} paddingX={1}><ClippedText fg={colors.textMuted} text={read} width={width - 2} /></Box> : null}
|
|
78
79
|
</Box>
|
|
79
80
|
);
|
|
80
81
|
}
|
|
@@ -4,8 +4,8 @@ import { DataTableView } from "../../../components/data-table/view";
|
|
|
4
4
|
import { ChoiceDialog } from "../../../components/ui/choice-dialog";
|
|
5
5
|
import { useDialog, type PromptContext } from "../../../ui/dialog";
|
|
6
6
|
import { getSharedRegistry } from "../../registry";
|
|
7
|
-
import { teamPrefix } from "
|
|
8
|
-
import { teamStore } from "
|
|
7
|
+
import { teamPrefix } from "../../../team/model";
|
|
8
|
+
import { teamStore } from "../../../team/store";
|
|
9
9
|
import { teamViewsStore } from "../cloud/team/views";
|
|
10
10
|
import { setPaneSettings } from "../../../pane-settings";
|
|
11
11
|
import { customViewInstanceSettings } from "./index";
|
|
@@ -55,6 +55,12 @@ function dateEntry(label: string, date: Date | null) {
|
|
|
55
55
|
return { label, value: date, formatted: date ? date.toISOString().slice(0, 10) : "—" };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** The next pay date, when it is the one stated beside the ex-date the forward rate is dated by. */
|
|
59
|
+
function forwardPayDate(metrics: DividendData["metrics"]): Date | null {
|
|
60
|
+
const exDate = metrics.nextExDividendDate?.toISOString().slice(0, 10);
|
|
61
|
+
return exDate != null && exDate === metrics.forwardRateAsOf ? metrics.nextPayDate : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
58
64
|
function matchesType(payment: DividendPayment, type: string): boolean {
|
|
59
65
|
return type === "all" || payment.type === type;
|
|
60
66
|
}
|
|
@@ -95,7 +101,7 @@ export function projectDividendYieldHeadless(
|
|
|
95
101
|
: "Reference price time unavailable; cash yield may be out of date." }] : []),
|
|
96
102
|
{ label: "Trailing yield", value: metrics.trailingYield, formatted: formatDividendYield(metrics.trailingYield) },
|
|
97
103
|
{ label: "Forward yield", value: metrics.forwardYield, formatted: metrics.forwardYield == null ? formatDividendYield(null)
|
|
98
|
-
: `${formatDividendYield(metrics.forwardYield)} (${forwardDividendBasis(metrics.forwardRateAsOf)})` },
|
|
104
|
+
: `${formatDividendYield(metrics.forwardYield)} (${forwardDividendBasis(metrics.forwardRateAsOf, { payDate: forwardPayDate(metrics) })})` },
|
|
99
105
|
{ label: "Trailing rate", value: metrics.trailingRate, formatted: currency ? formatDistributionAmount(metrics.trailingRate ?? undefined, currency) : "—" },
|
|
100
106
|
{ label: "Forward rate", value: metrics.forwardRate, formatted: currency ? formatDistributionAmount(metrics.forwardRate ?? undefined, currency) : "—" },
|
|
101
107
|
{ label: "Earnings Payout", value: metrics.payoutRatio, formatted: formatDividendYield(metrics.payoutRatio) },
|
|
@@ -132,7 +138,7 @@ export function projectDividendYieldHeadless(
|
|
|
132
138
|
limitations: data.notes ?? [],
|
|
133
139
|
yieldMethod: "Cash distributions with ex-dates in the preceding 12 months, one payment per period for a regular cadence, divided by the reference share price; excludes reinvestment and is not SEC yield or total return.",
|
|
134
140
|
forwardRateAsOf: metrics.forwardRateAsOf,
|
|
135
|
-
forwardRateMethod: "Indicated annual cash rate, the one fundamentals use: the latest regular distribution annualized, dated by its ex-date (forwardRateAsOf). Only when its currency units are comparable with the share price.",
|
|
141
|
+
forwardRateMethod: "Indicated annual cash rate, the one fundamentals use: the latest regular distribution annualized, or once the next one is declared that one, dated by its ex-date (forwardRateAsOf; a date still ahead is the declared distribution's). Only when its currency units are comparable with the share price.",
|
|
136
142
|
paymentFrequencyMethod: "Cadence inferred from ex-dates in the last two years, unavailable when no cash was reported in the last year; not an announced payment schedule.",
|
|
137
143
|
},
|
|
138
144
|
};
|
|
@@ -22,6 +22,7 @@ import { colors, priceColor } from "../../../theme/colors";
|
|
|
22
22
|
import { TextAttributes, useUiCapabilities } from "../../../ui";
|
|
23
23
|
import { displayWidth, formatCurrency, formatDistributionAmount, formatPercentRaw } from "../../../utils/format";
|
|
24
24
|
import { resolveCurrencyUnit } from "../../../utils/currency-units";
|
|
25
|
+
import { forwardDividendNote } from "../../../utils/forward-dividend";
|
|
25
26
|
import { nextHeaderSort } from "../../../utils/sort-values";
|
|
26
27
|
import { handleRefreshKey, loadingErrorFooterInfo } from "../../../components/data-table/table-pane";
|
|
27
28
|
import { SignInWall } from "../cloud/auth-actions";
|
|
@@ -76,11 +77,12 @@ function buildMetricItems(metrics: DividendMetrics, currency: string, hasHistory
|
|
|
76
77
|
const exDate: StatItem[] = metrics.nextExDividendDate
|
|
77
78
|
? [{ id: "next-ex", label: "Next ex", value: formatDate(metrics.nextExDividendDate) }]
|
|
78
79
|
: hasHistory ? [] : [{ id: "last-ex", label: "Last ex", value: formatDate(metrics.lastExDividendDate) }];
|
|
80
|
+
const forwardNote = metrics.forwardYield != null ? forwardDividendNote(metrics.forwardRateAsOf) : null;
|
|
79
81
|
return [
|
|
80
82
|
{ id: "ttm-yield", label: short ? "TTM yld" : "TTM yield", value: formatDividendYield(metrics.trailingYield), color: priceColor(metrics.trailingYield ?? 0) },
|
|
81
|
-
// Dated by the distribution it annualizes; docs/research-data.md says how it is built.
|
|
83
|
+
// Dated by the distribution it annualizes, or marked declared beside the next ex-date; docs/research-data.md says how it is built.
|
|
82
84
|
{ id: "forward-yield", label: short ? "Fwd yld" : "Fwd yield", value: formatDividendYield(metrics.forwardYield), color: priceColor(metrics.forwardYield ?? 0),
|
|
83
|
-
...(
|
|
85
|
+
...(forwardNote ? { detail: forwardNote } : {}) },
|
|
84
86
|
{ id: "ttm-rate", label: short ? "TTM/sh" : "TTM/share", value: formatRate(metrics.trailingRate, currency) },
|
|
85
87
|
{ id: "forward-rate", label: short ? "Fwd/sh" : "Fwd/share", value: formatRate(metrics.forwardRate, currency) },
|
|
86
88
|
{ id: "growth-1y", label: short ? "1Y chg" : "1Y growth", value: formatGrowth(metrics.growth1Y), color: priceColor(metrics.growth1Y ?? 0) },
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { useState } from "react";
|
|
8
8
|
import { ActionRow, MarkdownText, RemoteImage, usePaneLinkMenuEntry } from "../../../components";
|
|
9
|
+
import { READER_HEADING_STYLE } from "../../../components/reader/reader-text";
|
|
9
10
|
import type { DocsBlock } from "../../../docs/content";
|
|
10
11
|
import { tf } from "../../../i18n";
|
|
11
12
|
import { isDarkTheme } from "../../../theme/themes";
|
|
@@ -25,6 +26,9 @@ const CALLOUT_LABELS: Record<string, string> = {
|
|
|
25
26
|
example: "Example",
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
/** The reader's section heading size; the space around it is the blocks' own. */
|
|
30
|
+
const SECTION_HEADING_STYLE = { fontSize: READER_HEADING_STYLE.fontSize, lineHeight: READER_HEADING_STYLE.lineHeight } as const;
|
|
31
|
+
|
|
28
32
|
const LINK_PATTERN = /\[([^\]]+)\]\(([^)\s]+)\)/g;
|
|
29
33
|
|
|
30
34
|
/** Inline links of a block, for the pane menu: the terminal cannot press a word. */
|
|
@@ -122,9 +126,16 @@ function Block({ block, width, onOpenLink }: { block: DocsBlock; width: number;
|
|
|
122
126
|
case "paragraph":
|
|
123
127
|
return <MarkdownText text={block.text.replace(/\n/g, " ")} lineWidth={width} onOpenLink={onOpenLink} />;
|
|
124
128
|
case "heading":
|
|
125
|
-
// A section reads bright; a step inside it, in the text colour.
|
|
129
|
+
// A section reads bright and, on the desktop and the web, a step larger, as in every reader; a step inside it, in the text colour.
|
|
126
130
|
return (
|
|
127
|
-
<Text
|
|
131
|
+
<Text
|
|
132
|
+
fg={block.level === 2 ? colors.textBright : colors.text}
|
|
133
|
+
attributes={TextAttributes.BOLD}
|
|
134
|
+
wrapText
|
|
135
|
+
selectable
|
|
136
|
+
width={nativePaneChrome ? "100%" : width}
|
|
137
|
+
style={nativePaneChrome && block.level === 2 ? SECTION_HEADING_STYLE : undefined}
|
|
138
|
+
>
|
|
128
139
|
{block.text.replace(/[*`]/g, "")}
|
|
129
140
|
</Text>
|
|
130
141
|
);
|
|
@@ -54,6 +54,7 @@ import type { PaneProps } from "../../../types/plugin";
|
|
|
54
54
|
import { Box, Text, TextAttributes, useRendererHost, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
|
|
55
55
|
import { usePluginAppActions, usePluginPaneState } from "../../runtime";
|
|
56
56
|
import { badgeCell } from "../help/table-cells";
|
|
57
|
+
import { READER_COLUMN_FONT } from "../../../components/reader/reader-text";
|
|
57
58
|
import { DocsBlocks } from "./blocks";
|
|
58
59
|
import { useDocsFeed, useDocsLesson } from "./use-docs";
|
|
59
60
|
|
|
@@ -73,12 +74,12 @@ const STACK_LIMIT = 30;
|
|
|
73
74
|
/** Prose reads best at about seventy columns, however wide the pane. */
|
|
74
75
|
const MEASURE = 72;
|
|
75
76
|
/**
|
|
76
|
-
* On the desktop and the web the text reads
|
|
77
|
-
* with more air between lines, at most 69 characters
|
|
78
|
-
* wide as a screenshot of the measure): the column fills
|
|
79
|
-
* stops there in a wider one.
|
|
77
|
+
* On the desktop and the web the text reads at the reader's body size, a step
|
|
78
|
+
* up from the table text, with more air between lines, at most 69 characters
|
|
79
|
+
* of it to the line (as wide as a screenshot of the measure): the column fills
|
|
80
|
+
* a narrower pane and stops there in a wider one.
|
|
80
81
|
*/
|
|
81
|
-
const READING_STYLE = { fontSize:
|
|
82
|
+
const READING_STYLE = { fontSize: READER_COLUMN_FONT, "--cell-h": "1.7em", maxWidth: "69ch" } as const;
|
|
82
83
|
/** The terminal wraps at `width` columns; the desktop and the web at the reading column's own width. */
|
|
83
84
|
function useTextWidth(width: number): number | "100%" {
|
|
84
85
|
return useUiCapabilities().nativePaneChrome ? "100%" : width;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { DoeTab } from "../../../api-client/doe";
|
|
2
2
|
import type { PaneTemplateCreateOptions, PaneTemplateInstanceConfig } from "../../../types/plugin";
|
|
3
3
|
import type { PluginModule } from "../plugin-module";
|
|
4
|
-
import {
|
|
4
|
+
import { DOE_CATALOG_SERIES } from "./series";
|
|
5
5
|
import { doeBoardCache } from "./client";
|
|
6
|
+
import { doePriceCache } from "./prices";
|
|
6
7
|
import { doeHeadless } from "./headless";
|
|
7
8
|
import { DOE_SERIES_OPTIONS, doeSeriesOption } from "./model";
|
|
8
9
|
import { DoePane } from "./pane";
|
|
@@ -21,7 +22,7 @@ const shortcut = (prefix: string, tab: DoeTab | null) => ({
|
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
export const doeModule: PluginModule = {
|
|
24
|
-
series:
|
|
25
|
+
series: DOE_CATALOG_SERIES,
|
|
25
26
|
panes: [{ id: "doe", name: "Oil and Gas Inventories", icon: "D", component: DoePane,
|
|
26
27
|
defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 96, height: 34 },
|
|
27
28
|
tableExport: true, headless: doeHeadless("all") }],
|
|
@@ -38,6 +39,12 @@ export const doeModule: PluginModule = {
|
|
|
38
39
|
shortcut: shortcut("NGS", "gas"), headless: doeHeadless("gas"),
|
|
39
40
|
createInstance: (_context, options) => doeInstance("gas", options),
|
|
40
41
|
}],
|
|
41
|
-
setup(ctx) {
|
|
42
|
-
|
|
42
|
+
setup(ctx) {
|
|
43
|
+
doeBoardCache.attach(ctx.persistence);
|
|
44
|
+
doePriceCache.attach(ctx.persistence);
|
|
45
|
+
},
|
|
46
|
+
dispose() {
|
|
47
|
+
doeBoardCache.reset();
|
|
48
|
+
doePriceCache.reset();
|
|
49
|
+
},
|
|
43
50
|
};
|
|
@@ -18,8 +18,8 @@ import { usePaneInstance } from "../../../state/app/context";
|
|
|
18
18
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
19
19
|
import type { ResolvedSeries } from "../../../time-series/types";
|
|
20
20
|
import type { PaneProps } from "../../../types/plugin";
|
|
21
|
-
import { Box
|
|
22
|
-
import {
|
|
21
|
+
import { Box } from "../../../ui";
|
|
22
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
23
23
|
import { DOE_NOT_AVAILABLE, getCachedDoeBoard, loadDoeBoard } from "./client";
|
|
24
24
|
import {
|
|
25
25
|
DOE_TABS,
|
|
@@ -71,14 +71,15 @@ function SeasonalChart({ row, series, width, height }: { row: DoeSeriesRow; seri
|
|
|
71
71
|
const latestWeek = row.seasonal?.current.at(-1)?.[0] ?? null;
|
|
72
72
|
const viewport = useMemo(() => ({ start: doeWeekDate(1), end: doeWeekDate(weeks) }), [weeks]);
|
|
73
73
|
const xAxis = useMemo(() => ({ ticks: doeWeekTicks(weeks), formatCursor: (ratio: number) => `W${Math.round(ratio * (weeks - 1)) + 1}` }), [weeks]);
|
|
74
|
-
|
|
74
|
+
// The plain read under the axis; the desktop cuts it where its line ends.
|
|
75
|
+
const read = row.read && width > 2 ? row.read : "";
|
|
75
76
|
return (
|
|
76
77
|
<Box width={width} height={height} flexDirection="column">
|
|
77
78
|
<CompositeChart series={series} panels={PANELS} width={width} height={Math.max(3, height - (read ? 1 : 0))}
|
|
78
79
|
focused={false} navigable={false} showLegend showTimeAxis viewport={viewport} clipToViewport xAxis={xAxis}
|
|
79
80
|
cursorDate={latestWeek ? doeWeekDate(latestWeek) : null} formatValue={legendValue(row)} formatAxisValue={doeAxisFormatter(row.unit)}
|
|
80
81
|
remoteKind="doe-seasonal" />
|
|
81
|
-
{read ? <Box height={1} paddingX={1}><
|
|
82
|
+
{read ? <Box height={1} paddingX={1}><ClippedText fg={colors.textMuted} text={read} width={width - 2} /></Box> : null}
|
|
82
83
|
</Box>
|
|
83
84
|
);
|
|
84
85
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { apiClient } from "../../../api-client";
|
|
2
|
+
import type { DoePricesPayload } from "../../../api-client/doe";
|
|
3
|
+
import { createPluginCache } from "../../../data/plugin-cache";
|
|
4
|
+
import type { SeriesDefinition } from "../../../series/types";
|
|
5
|
+
import { cachedSeriesWarning } from "../shared/series-warning";
|
|
6
|
+
import { loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* EIA weekly retail prices the petroleum board does not carry, served by Gloom
|
|
10
|
+
* Cloud from the EIA's history workbooks. `key` is the EIA series id a report
|
|
11
|
+
* of the series cites.
|
|
12
|
+
*/
|
|
13
|
+
const PRICE_SERIES = [
|
|
14
|
+
{
|
|
15
|
+
id: "gasoline-price",
|
|
16
|
+
name: "US regular gasoline retail price",
|
|
17
|
+
key: "EMM_EPMR_PTE_NUS_DPG",
|
|
18
|
+
url: "https://www.eia.gov/dnav/pet/hist/LeafHandler.ashx?n=PET&s=EMM_EPMR_PTE_NUS_DPG&f=W",
|
|
19
|
+
tags: ["gasoline", "retail", "pump price"],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "diesel-price",
|
|
23
|
+
name: "US No. 2 diesel retail price",
|
|
24
|
+
key: "EMD_EPD2D_PTE_NUS_DPG",
|
|
25
|
+
url: "https://www.eia.gov/dnav/pet/hist/LeafHandler.ashx?n=PET&s=EMD_EPD2D_PTE_NUS_DPG&f=W",
|
|
26
|
+
tags: ["diesel", "retail", "pump price"],
|
|
27
|
+
},
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export const doePriceCache = createPluginCache<DoePricesPayload>({
|
|
31
|
+
kind: "doe-retail-prices", source: "gloom-cloud", schemaVersion: 1,
|
|
32
|
+
// Weekly data: a quarter hour is soon enough to catch a Monday release.
|
|
33
|
+
policy: { staleMs: 15 * 60_000, expireMs: 14 * 24 * 60 * 60_000 },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const isDate = (value: unknown) => typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)
|
|
37
|
+
&& new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
|
|
38
|
+
|
|
39
|
+
/** A payload that breaks the contract is refused whole rather than charted with holes in it. */
|
|
40
|
+
function validatePrices(payload: DoePricesPayload): DoePricesPayload {
|
|
41
|
+
if (!payload || payload.source !== "EIA" || !Number.isFinite(Date.parse(payload.generatedAt))
|
|
42
|
+
|| !["available", "partial", "unavailable"].includes(payload.status)
|
|
43
|
+
|| !Array.isArray(payload.gaps) || !payload.gaps.every((gap) => typeof gap === "string")
|
|
44
|
+
|| !Array.isArray(payload.series) || !payload.series.every((series) => typeof series?.id === "string"
|
|
45
|
+
&& typeof series.key === "string" && series.unit === "usd-per-gallon" && Array.isArray(series.points)
|
|
46
|
+
&& series.points.every((point) => Array.isArray(point) && isDate(point[0])
|
|
47
|
+
&& typeof point[1] === "number" && Number.isFinite(point[1])))) {
|
|
48
|
+
throw new Error("The server returned invalid EIA retail prices");
|
|
49
|
+
}
|
|
50
|
+
return payload;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A server without the route (404), or without its tables yet (503), has no prices to give. */
|
|
54
|
+
async function fetchDoePrices(): Promise<DoePricesPayload> {
|
|
55
|
+
try {
|
|
56
|
+
return validatePrices(await apiClient.getCloudDoePrices());
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw unavailableOnServer(error, "EIA retail prices are not available yet.", [404, 503]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const DOE_PRICE_SERIES: readonly SeriesDefinition[] = PRICE_SERIES.map((series) => ({
|
|
63
|
+
id: `doe:${series.id}`,
|
|
64
|
+
name: series.name,
|
|
65
|
+
unit: "USD/gallon",
|
|
66
|
+
frequency: "weekly",
|
|
67
|
+
group: "Energy",
|
|
68
|
+
publisher: "EIA",
|
|
69
|
+
url: series.url,
|
|
70
|
+
tags: ["oil", "petroleum", "EIA", "weekly", ...series.tags],
|
|
71
|
+
description: "EIA weekly US retail price. Not gasoline stocks, and not the AAA daily average.",
|
|
72
|
+
load: async () => {
|
|
73
|
+
const resource = await loadCloudResource(doePriceCache, "prices", () => fetchDoePrices(), { validate: validatePrices });
|
|
74
|
+
const row = resource.payload.series.find((entry) => entry.id === series.id);
|
|
75
|
+
if (!row?.points.length) throw new Error(`${series.name} is not available yet.`);
|
|
76
|
+
const warning = cachedSeriesWarning(resource);
|
|
77
|
+
return {
|
|
78
|
+
points: row.points.map(([date, value]) => ({ date, value })),
|
|
79
|
+
...(warning ? { warning } : {}),
|
|
80
|
+
// Cited as the EIA series it is, not as the route that served it.
|
|
81
|
+
attribution: { source: `EIA series ${series.key}`, publisher: "EIA" },
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
}));
|
|
@@ -3,6 +3,7 @@ import type { SeriesDefinition, SeriesPoint } from "../../../series/types";
|
|
|
3
3
|
import { cachedSeriesWarning } from "../shared/series-warning";
|
|
4
4
|
import { loadDoeBoard } from "./client";
|
|
5
5
|
import { DOE_SERIES_OPTIONS } from "./model";
|
|
6
|
+
import { DOE_PRICE_SERIES } from "./prices";
|
|
6
7
|
|
|
7
8
|
/** Stocks in million barrels, as the pane shows them; flows, rates and gas as published. */
|
|
8
9
|
const UNITS: Record<DoeUnit, { unit: string; scale: number }> = {
|
|
@@ -40,7 +41,7 @@ function doeRowPoints(row: DoeSeriesRow): SeriesPoint[] {
|
|
|
40
41
|
return [...dated(seasonal.year - 1, seasonal.previous), ...dated(seasonal.year, seasonal.current)];
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
const DOE_SERIES: readonly SeriesDefinition[] = DOE_SERIES_OPTIONS.map((option): SeriesDefinition => {
|
|
44
45
|
const unit = ROW_UNITS[option.value] ?? (option.tab === "gas" ? "bcf" : "kb");
|
|
45
46
|
const gas = option.tab === "gas";
|
|
46
47
|
return {
|
|
@@ -60,3 +61,5 @@ export const DOE_SERIES: readonly SeriesDefinition[] = DOE_SERIES_OPTIONS.map((o
|
|
|
60
61
|
},
|
|
61
62
|
};
|
|
62
63
|
});
|
|
64
|
+
|
|
65
|
+
export const DOE_CATALOG_SERIES = [...DOE_SERIES, ...DOE_PRICE_SERIES];
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { EarningsCalendarReport, EarningsTiming } from "../../../api-client/earnings";
|
|
2
2
|
import { getPublishedUsEquityCalendarDay } from "../../../market-data/published-us-sessions";
|
|
3
3
|
import type { EarningsEvent } from "../../../types/data-provider";
|
|
4
|
+
import type { EarningsDateStatus, EarningsEpsBasis } from "../../../types/financials";
|
|
4
5
|
import type { TickerRecord } from "../../../types/ticker";
|
|
6
|
+
import { dateStatusFromEstimateFlag, earningsDateStatus, earningsEpsBasis } from "../shared/earnings-report";
|
|
5
7
|
import { coherentEarningsValue } from "./estimate-basis";
|
|
6
8
|
import { shortDate, weekday } from "./format";
|
|
7
9
|
|
|
@@ -16,9 +18,15 @@ export interface BoardReport {
|
|
|
16
18
|
timing: EarningsTiming | null;
|
|
17
19
|
/** Timing taken from the company's last reports, not announced yet. */
|
|
18
20
|
expectedTiming: boolean;
|
|
21
|
+
/** Whether the date is out, announced or projected; null when the source does not say. */
|
|
22
|
+
dateStatus: EarningsDateStatus | null;
|
|
19
23
|
marketCap: number | null;
|
|
20
24
|
epsEstimate: number | null;
|
|
25
|
+
/** Analysts behind `epsEstimate`. */
|
|
26
|
+
epsAnalysts: number | null;
|
|
21
27
|
epsActual: number | null;
|
|
28
|
+
/** Null when the reported figure's basis cannot be determined. */
|
|
29
|
+
epsBasis: EarningsEpsBasis | null;
|
|
22
30
|
revenueEstimate: number | null;
|
|
23
31
|
impliedMove: number | null;
|
|
24
32
|
averageMove: number | null;
|
|
@@ -120,9 +128,12 @@ export function boardReport(report: EarningsCalendarReport, owned: ReadonlyMap<s
|
|
|
120
128
|
date: report.date,
|
|
121
129
|
timing: report.timing,
|
|
122
130
|
expectedTiming: report.timingSource === "history",
|
|
131
|
+
dateStatus: earningsDateStatus(report.dateStatus),
|
|
123
132
|
marketCap: report.marketCap,
|
|
124
133
|
epsEstimate: report.epsEstimate,
|
|
134
|
+
epsAnalysts: report.epsAnalysts,
|
|
125
135
|
epsActual: report.epsActual,
|
|
136
|
+
epsBasis: earningsEpsBasis(report.epsBasis),
|
|
126
137
|
revenueEstimate: report.revenueEstimate,
|
|
127
138
|
impliedMove: report.implied?.move ?? null,
|
|
128
139
|
averageMove: report.averageReports >= MIN_AVERAGE_REPORTS ? report.averageMove : null,
|
|
@@ -139,6 +150,7 @@ export function fallbackReport(event: EarningsEvent, owned: ReadonlyMap<string,
|
|
|
139
150
|
const currency = event.estimateBasis?.[field]?.currency;
|
|
140
151
|
return currency && currency !== "USD" ? null : coherentEarningsValue(event, field);
|
|
141
152
|
};
|
|
153
|
+
const epsEstimate = dollars("epsEstimate");
|
|
142
154
|
return {
|
|
143
155
|
key: `${event.symbol}:${date}`,
|
|
144
156
|
symbol: event.symbol,
|
|
@@ -146,9 +158,14 @@ export function fallbackReport(event: EarningsEvent, owned: ReadonlyMap<string,
|
|
|
146
158
|
date,
|
|
147
159
|
timing,
|
|
148
160
|
expectedTiming: false,
|
|
161
|
+
dateStatus: dateStatusFromEstimateFlag(event.isDateEstimate),
|
|
149
162
|
marketCap: null,
|
|
150
|
-
epsEstimate
|
|
163
|
+
epsEstimate,
|
|
164
|
+
// The count belongs to the estimate beside it.
|
|
165
|
+
epsAnalysts: epsEstimate == null ? null : coherentEarningsValue(event, "epsAnalysts"),
|
|
151
166
|
epsActual: null,
|
|
167
|
+
// The estimate's basis goes with it.
|
|
168
|
+
epsBasis: epsEstimate == null ? null : earningsEpsBasis(event.epsBasis),
|
|
152
169
|
revenueEstimate: dollars("revenueEstimate"),
|
|
153
170
|
impliedMove: null,
|
|
154
171
|
averageMove: null,
|
|
@@ -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,
|