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
|
@@ -27,7 +27,9 @@ export const optionsScenarioModule: PluginModule = {
|
|
|
27
27
|
paneTemplates: [createTickerSurfacePaneTemplate({
|
|
28
28
|
id: "options-scenario-pane", paneId: "options-scenario", label: "Options Scenario",
|
|
29
29
|
description: "Build multi-leg positions and compare scenario P&L, payoff and aggregate Greeks.",
|
|
30
|
-
|
|
30
|
+
// OSA owns the phrases; OMON and VCA carry the same words without them, so a put-selling search finds all three, OSA first.
|
|
31
|
+
keywords: ["osa", "options", "scenario", "payoff", "strategy", "greeks", "hedge", "hedging", "collar", "protective put", "put spread",
|
|
32
|
+
"cash secured put", "csp", "put selling", "sell put", "short put", "wheel"],
|
|
31
33
|
shortcut: "OSA", publicShare: true,
|
|
32
34
|
settings: (symbol, _context, options) => ({ symbol, ...Object.fromEntries(scenarioSettingKeys
|
|
33
35
|
.filter((key) => options?.values?.[key] != null).map((key) => [key, options!.values![key]])) }),
|
|
@@ -21,7 +21,10 @@ import { buildOptionQuoteKey, freshOptionQuote, OPTIONS_QUOTE_EXCHANGE } from ".
|
|
|
21
21
|
import { liveScenarioPosition, scenarioLegContractSymbol } from "./live";
|
|
22
22
|
import { ScenarioPayoffChart } from "./charts";
|
|
23
23
|
import { ScenarioLegEditor, ScenarioSaveForm, ScenarioInputsForm } from "./editor";
|
|
24
|
-
import { loadScenarioMarket, scenarioControlsFromSettings, scenarioPositionFromSettings, type ScenarioMarketSnapshot } from "./client";
|
|
24
|
+
import { loadScenarioMarket, scenarioControlsFromSettings, scenarioPositionFromSettings, scenarioSpotBasis, type ScenarioMarketSnapshot } from "./client";
|
|
25
|
+
import { cashSecuredPut, cspStatItems, loadNextEarningsDate } from "./csp";
|
|
26
|
+
import { useAssetData } from "../../runtime";
|
|
27
|
+
import { formatSpotTime } from "../options/model-spot";
|
|
25
28
|
import { buildScenario, currencyLabel, scenarioCurrency, scenarioValueUnit, UNKNOWN_CURRENCY, type ScenarioLeg, type ScenarioPosition,
|
|
26
29
|
type ScenarioControls, validatePosition } from "./model";
|
|
27
30
|
import { EMPTY_SAVED_STRATEGIES, restoreSavedStrategies, type SavedScenarioStrategy } from "./state";
|
|
@@ -141,6 +144,19 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
141
144
|
}, [frozen, position, controls, seeded.error, controlState, baseControls.error]);
|
|
142
145
|
const scenario = result.scenario;
|
|
143
146
|
const error = localError ?? result.error;
|
|
147
|
+
// A cash-secured put reads the earnings calendar for whether a report falls before it expires.
|
|
148
|
+
const assetData = useAssetData();
|
|
149
|
+
const cspStrategy = settings.strategy === "csp";
|
|
150
|
+
const earningsLoader = useCallback(() => loadNextEarningsDate(assetData, underlying), [assetData, underlying]);
|
|
151
|
+
// A screenshot keeps the date its report read.
|
|
152
|
+
const frozenEarnings = Object.hasOwn(settings, "scenarioEarningsDate");
|
|
153
|
+
const earnings = useAsyncResource(cspStrategy && underlying && !frozenEarnings ? earningsLoader : null);
|
|
154
|
+
const earningsDate = frozenEarnings ? settings.scenarioEarningsDate as string | null : earnings.data;
|
|
155
|
+
// A position following the market is at the live print, which the footer names with its feed.
|
|
156
|
+
const spotBasis = scenario && !live ? scenarioSpotBasis(settings, scenario.position, market) : null;
|
|
157
|
+
const csp = useMemo(() => cspStrategy && scenario && !earnings.loading
|
|
158
|
+
? cashSecuredPut(scenario.position, live?.basis ?? spotBasis ?? "last price", earningsDate) : null,
|
|
159
|
+
[cspStrategy, scenario, earnings.loading, earningsDate, live?.basis, spotBasis]);
|
|
144
160
|
// The report's Hedge budget, from the same position and quotes; a live position sizes at its live quotes and spot.
|
|
145
161
|
const hedge = useMemo(() => {
|
|
146
162
|
if (!scenario) return { value: null, error: null };
|
|
@@ -230,13 +246,19 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
230
246
|
&& money(market.spot) !== money(position!.spot) ? ` · last ${money(market.spot)}` : "";
|
|
231
247
|
const midVolatility = !midLegs ? "" : (midLegs === position!.legs.length ? " · IV from quote mid"
|
|
232
248
|
: ` · IV from quote mid on ${midLegs} of ${position!.legs.length} legs`) + lastPrint;
|
|
249
|
+
// The spot every figure is valued at, and when: the live print, the spot the quote mids imply, a typed what-if or the last price.
|
|
250
|
+
const spotText = position ? `spot ${money(position.spot)}${spotBasis && spotBasis !== "last price" ? ` ${spotBasis}` : ""}` : "";
|
|
251
|
+
// A narrow row keeps the spot and its time before the rest.
|
|
252
|
+
const shortSpotText = position ? `spot ${money(position.spot)} · ${formatSpotTime(live?.observedAt ?? position.asOf)}` : "";
|
|
233
253
|
usePaneFooter("osa", () => ({ info: [
|
|
234
254
|
...(resource.loading ? [{ id: "loading", parts: [{ text: "loading chain", tone: "muted" as const }] }] : []),
|
|
235
255
|
...(error ? [{ id: "error", parts: [{ text: error, tone: "warning" as const }] }] : []),
|
|
236
256
|
...(hedge.error ? [{ id: "hedge-error", parts: [{ text: hedge.error, tone: "warning" as const }] }] : []),
|
|
237
|
-
...(live ? [{ id: "asof", parts: [{ text: `${live.basis} ·
|
|
238
|
-
:
|
|
239
|
-
|
|
257
|
+
...(live ? [{ id: "asof", parts: [{ text: `${live.basis} · ${spotText} · ${new Date(live.observedAt).toISOString().slice(11, 19)} UTC`, tone: "muted" as const }],
|
|
258
|
+
variants: [[{ text: shortSpotText, tone: "muted" as const }]] }]
|
|
259
|
+
: position ? [{ id: "asof", parts: [{ text: `${spotText} · ${formatSpotTime(position.asOf)} · ${market?.source ? "market" : "input assumptions"}${midVolatility}`, tone: "muted" as const }],
|
|
260
|
+
variants: [[{ text: shortSpotText, tone: "muted" as const }]] }] : []),
|
|
261
|
+
], hints }), [hints, position, market?.source, resource.loading, error, hedge.error, live?.basis, live?.observedAt, midVolatility, spotText, shortSpotText]);
|
|
240
262
|
useScenarioEvidence({ scenario, view: tab, loading: !!resource.loading && !scenario, error: error ?? (snapshotErrors.join("; ") || null), notices });
|
|
241
263
|
// A choice dialog (scenario date, saved strategies) owns the keys while open.
|
|
242
264
|
const dialogOpen = useDialogState((state) => state.isOpen);
|
|
@@ -262,7 +284,9 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
262
284
|
const stats: StatItem[] = scenario ? [
|
|
263
285
|
{ id: "pnl", label: "P&L", value: money(scenario.valuation.pnl), detail: valueUnit,
|
|
264
286
|
tone: cents(scenario.valuation.pnl) >= 0 ? "positive" : "negative" },
|
|
265
|
-
|
|
287
|
+
// A spot other than the last price says what it is: the one the quote mids imply, or a typed what-if.
|
|
288
|
+
{ id: "spot", label: "Spot", value: money(scenario.position.spot),
|
|
289
|
+
detail: spotBasis === "implied by option quotes" ? "per share, implied" : spotBasis === "typed" ? "per share, typed" : "per share" },
|
|
266
290
|
{ id: "value", label: "Value", value: money(scenario.valuation.price), detail: valueUnit },
|
|
267
291
|
{ id: "max-profit", label: "Max profit", value: risk?.unlimitedProfit ? "Unlimited" : money(risk?.maxProfit ?? null) },
|
|
268
292
|
{ id: "max-loss", label: "Max loss", value: risk?.unlimitedLoss ? "Unlimited" : money(risk?.maxLoss ?? null) },
|
|
@@ -274,9 +298,10 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
274
298
|
{ id: "rho", label: "Rho", value: money(scenario.valuation.rhoPerPoint), detail: "per pt" },
|
|
275
299
|
] : [];
|
|
276
300
|
const hedgeItems = hedge.value ? hedgeStatItems(hedge.value) : [];
|
|
301
|
+
const cspItems = csp ? cspStatItems(csp) : [];
|
|
277
302
|
// The query bar and the stat band sit above the content only once a scenario prices.
|
|
278
303
|
const bodyHeight = Math.max(3, height - tabRows - (scenario ? 1 + statGridRows(stats, width) : 0)
|
|
279
|
-
- (hedgeItems.length ? statGridRows(hedgeItems, width) : 0));
|
|
304
|
+
- (hedgeItems.length ? statGridRows(hedgeItems, width) : 0) - (cspItems.length ? statGridRows(cspItems, width) : 0));
|
|
280
305
|
const legColumns: DataTableColumn[] = [{ id: "quantity", label: "Contracts", width: 10, align: "right" },
|
|
281
306
|
{ id: "side", label: "Option", width: 7, align: "left" }, { id: "strike", label: "Strike", width: 11, align: "right" },
|
|
282
307
|
{ id: "expiration", label: "Expiry", width: 12, align: "left" }, { id: "price", label: "Entry / share", width: 14, align: "right" },
|
|
@@ -332,6 +357,7 @@ export function OptionsScenarioPane({ width, height, focused }: PaneProps) {
|
|
|
332
357
|
/>
|
|
333
358
|
<StatGrid items={stats} width={width} />
|
|
334
359
|
{hedgeItems.length > 0 && <StatGrid items={hedgeItems} width={width} />}
|
|
360
|
+
{cspItems.length > 0 && <StatGrid items={cspItems} width={width} />}
|
|
335
361
|
</>}
|
|
336
362
|
{!position?.legs.length && !scenario ? <PaneStatusBody loading={!!resource.loading && !market} error={seeded.error} subject="scenario inputs">
|
|
337
363
|
<Box paddingX={1}><EmptyState title="Build an options position." actions={<Button label="Add leg" onPress={addTyped} />} /></Box>
|
|
@@ -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,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
|
+
import { proLockNote } from "../shared/pro-lock-note";
|
|
2
3
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
3
4
|
import {
|
|
4
5
|
attachRiskFactorsPersistence,
|
|
@@ -13,6 +14,21 @@ import { isKnownNonUsListing } from "../../../utils/sec";
|
|
|
13
14
|
const description =
|
|
14
15
|
"The company's 10-K risk factors, and what was added, dropped, or rewritten since the prior year.";
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* RISK is Pro whole (`useProFeatureWall`). The 10-Ks it reads are in the SEC
|
|
19
|
+
* filing list, which needs no account: `filings` lists them with a link to
|
|
20
|
+
* each, where Item 1A is the risk factors as written, without RISK's grouping
|
|
21
|
+
* or its changes since the year before.
|
|
22
|
+
*/
|
|
23
|
+
function riskFactorsLockNote(lock: { gate: "sign-in" | "pro"; symbol: string | null }): string {
|
|
24
|
+
return proLockNote(lock, {
|
|
25
|
+
token: "RISK",
|
|
26
|
+
does: "it groups a 10-K's risk factors by theme and shows what was added, dropped or reworded since the year before",
|
|
27
|
+
shows: "it",
|
|
28
|
+
free: (ticker, company) => `gloomberb filings ${ticker} --form 10-K lists ${company}'s annual reports, each with its filing date and a link to the filing, whose Item 1A holds the risk factors as written.`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
export const riskFactorsModule: PluginModule = {
|
|
17
33
|
setup(ctx) {
|
|
18
34
|
attachRiskFactorsPersistence(ctx.persistence);
|
|
@@ -33,6 +49,8 @@ export const riskFactorsModule: PluginModule = {
|
|
|
33
49
|
panes: [
|
|
34
50
|
{
|
|
35
51
|
id: RISK_FACTORS_PANE_ID,
|
|
52
|
+
reportLockNote: riskFactorsLockNote,
|
|
53
|
+
reportRequirements: ["Gloom Cloud Pro"],
|
|
36
54
|
name: "Risk Factors",
|
|
37
55
|
icon: "R",
|
|
38
56
|
component: RiskFactorsPane,
|
|
@@ -283,7 +283,8 @@ function RiskFactorsReader({
|
|
|
283
283
|
</Box>
|
|
284
284
|
) : null}
|
|
285
285
|
<Box flexDirection="column">
|
|
286
|
-
|
|
286
|
+
{/* The title names the pane; the heading only marks where the list starts after another section. */}
|
|
287
|
+
{report.overview || diff || report.notes.top.length > 0 ? <SectionHeading marginTop={1} title="All Risk Factors" /> : null}
|
|
287
288
|
{report.risks.map((risk, index) => (
|
|
288
289
|
<Prose
|
|
289
290
|
key={`${index}-${risk.heading}`}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ApiRequestError } from "../../../api-client/errors";
|
|
1
2
|
import type {
|
|
2
3
|
ScannerFeedEvent,
|
|
3
4
|
ScannerFlowPayload,
|
|
@@ -18,6 +19,8 @@ import {
|
|
|
18
19
|
filterFlowEvents,
|
|
19
20
|
type FlowFilters,
|
|
20
21
|
} from "./flow-model";
|
|
22
|
+
import { explainRefusal } from "../shared/cloud-denied";
|
|
23
|
+
import { flowLockNote } from "./lock-note";
|
|
21
24
|
import { buildHiloBarRows, filterHiloRows, formatHiloPrice, type HiloMinPrice, type HiloSort } from "./hilo-model";
|
|
22
25
|
|
|
23
26
|
type ScannerName = "hilo" | "flow";
|
|
@@ -66,7 +69,10 @@ function loadScannerSnapshot<T extends ScannerPayload>(
|
|
|
66
69
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
67
70
|
unsubscribe = client.subscribeScanner(scanner, (event: ScannerFeedEvent) => {
|
|
68
71
|
if (event.type === "denied") {
|
|
69
|
-
|
|
72
|
+
// The refusal a read gets from the server: 402 for a plan without access, 401 for no session.
|
|
73
|
+
finish(() => reject(event.reason === "pro_required"
|
|
74
|
+
? new ApiRequestError("Pro access required", 402)
|
|
75
|
+
: new ApiRequestError("Sign in required", 401)));
|
|
70
76
|
return;
|
|
71
77
|
}
|
|
72
78
|
const payload = event.payload as T;
|
|
@@ -277,7 +283,8 @@ function createFlowHeadless(
|
|
|
277
283
|
],
|
|
278
284
|
describe: "Options Flow",
|
|
279
285
|
async load(args, ctx) {
|
|
280
|
-
const payload = await dependencies.load(args, ctx.apiClient, ctx.signal)
|
|
286
|
+
const payload = await dependencies.load(args, ctx.apiClient, ctx.signal)
|
|
287
|
+
.catch((error: unknown) => { throw explainRefusal(error, flowLockNote, { symbol: null }); });
|
|
281
288
|
return projectFlowSnapshot(payload, {
|
|
282
289
|
...DEFAULT_FLOW_FILTERS,
|
|
283
290
|
...(args.options as Partial<FlowFilters>),
|
|
@@ -4,6 +4,7 @@ import { DEFAULT_FLOW_FILTERS, FLOW_FILTER_OPTIONS } from "./flow-model";
|
|
|
4
4
|
import FlowPane from "./flow-pane";
|
|
5
5
|
import HiloPane from "./hilo-pane";
|
|
6
6
|
import { flowHeadless, hiloHeadless } from "./headless";
|
|
7
|
+
import { flowLockNote } from "./lock-note";
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
const HILO_PANE_ID = "scanner-hilo";
|
|
@@ -80,6 +81,8 @@ export const scannerModule: PluginModule = {
|
|
|
80
81
|
},
|
|
81
82
|
{
|
|
82
83
|
id: FLOW_PANE_ID,
|
|
84
|
+
reportLockNote: flowLockNote,
|
|
85
|
+
reportRequirements: ["Gloom Cloud Pro"],
|
|
83
86
|
name: "Options Flow",
|
|
84
87
|
icon: "F",
|
|
85
88
|
component: FlowPane,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PaneLock } from "../../../types/plugin";
|
|
2
|
+
import { proLockNote } from "../shared/pro-lock-note";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FLOW is Pro whole: the one scanner with no delayed tier to fall back to.
|
|
6
|
+
* The options chain needs no account and lists each contract's day volume and
|
|
7
|
+
* open interest, the raw counts behind a sweep or a block, without the prints.
|
|
8
|
+
*/
|
|
9
|
+
export function flowLockNote(lock: PaneLock): string {
|
|
10
|
+
return proLockNote(lock, {
|
|
11
|
+
token: "FLOW",
|
|
12
|
+
argument: "none",
|
|
13
|
+
does: "it streams real-time sweeps, blocks and large premium prints across US options exchanges",
|
|
14
|
+
shows: "it",
|
|
15
|
+
free: (ticker, company) => `gloomberb options ${ticker} lists ${company}'s option chain, each contract with its volume and open interest.`,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -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*"] },
|
|
@@ -54,7 +55,7 @@ export const SERIES_INVENTORY: readonly SeriesInventoryEntry[] = [
|
|
|
54
55
|
{ module: "credit-documents", chart: "Principal due by year", notInCatalog: "not-a-time-series", reason: "A maturity schedule, not a history." },
|
|
55
56
|
{ module: "debt-maturities", chart: "A company's principal and the share due soon, across annual filings", series: ["debt-maturities:*"] },
|
|
56
57
|
{ module: "debt-maturities", chart: "A company's maturity wall", notInCatalog: "not-a-time-series", reason: "A schedule by maturity year from one filing; its total and near-term shares across filings are in the catalog." },
|
|
57
|
-
{ module: "doe", chart: "Weekly oil and gas inventories against their five-year range", series: ["doe:*"] },
|
|
58
|
+
{ module: "doe", chart: "Weekly oil and gas inventories against their five-year range, and US retail gasoline and diesel prices", series: ["doe:*"] },
|
|
58
59
|
{ module: "cot", chart: "Net positioning by trader class, over the front-month price", series: ["cot:*"] },
|
|
59
60
|
{ module: "futures", chart: "Front-month futures board, charted from the board", series: ["fut:*"] },
|
|
60
61
|
{ module: "futures-curve", chart: "Futures curve by contract month", notInCatalog: "not-a-time-series", reason: "Plotted by delivery month; each front month is in the catalog as FUT:code." },
|
|
@@ -1,15 +1,46 @@
|
|
|
1
1
|
import { ApiRequestError } from "../../../api-client/errors";
|
|
2
|
+
import type { PaneLock } from "../../../types/plugin";
|
|
3
|
+
import { lockNoteReason } from "./pro-lock-note";
|
|
2
4
|
|
|
3
5
|
/** A Gloom Cloud refusal the reader can act on: signed out, an unverified email, or no Pro plan. */
|
|
4
|
-
|
|
6
|
+
type CloudDeniedStatus = 401 | 402 | 403;
|
|
5
7
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The account refusal behind a failed request, or null when it failed for
|
|
10
|
+
* another reason. Looks through the errors that wrap it, so a message written
|
|
11
|
+
* for the reader still says which wall it stands for.
|
|
12
|
+
*/
|
|
13
|
+
function cloudDeniedStatus(error: unknown): CloudDeniedStatus | null {
|
|
14
|
+
for (let current = error, depth = 0; current && depth < 4; depth += 1) {
|
|
15
|
+
const status = current instanceof ApiRequestError ? current.status : undefined;
|
|
16
|
+
if (status === 401 || status === 402 || status === 403) return status;
|
|
17
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
10
20
|
}
|
|
11
21
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The wall a refused read stands for, as the pane's own wall puts it up: no
|
|
24
|
+
* session is the sign-in wall, an unverified email the same wall asking for
|
|
25
|
+
* the email, a plan without access the Pro wall.
|
|
26
|
+
*/
|
|
27
|
+
export function refusedLock(error: unknown): Pick<PaneLock, "gate" | "verifyEmail"> | null {
|
|
28
|
+
const status = cloudDeniedStatus(error);
|
|
29
|
+
if (status === 402) return { gate: "pro" };
|
|
30
|
+
return status ? { gate: "sign-in", ...status === 403 ? { verifyEmail: true } : {} } : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What a headless read throws when the server refused it behind a wall: the
|
|
35
|
+
* pane's lock note on one line, so every consumer of the read says what is
|
|
36
|
+
* locked and how to unlock it, with the refusal as its cause for `fn`, which
|
|
37
|
+
* prints the whole note. Any other failure is returned as it is.
|
|
38
|
+
*/
|
|
39
|
+
export function explainRefusal(
|
|
40
|
+
error: unknown,
|
|
41
|
+
note: (lock: PaneLock) => string,
|
|
42
|
+
lock: Pick<PaneLock, "symbol" | "token">,
|
|
43
|
+
): unknown {
|
|
44
|
+
const refused = refusedLock(error);
|
|
45
|
+
return refused ? new Error(lockNoteReason(note({ ...refused, ...lock })), { cause: error }) : error;
|
|
15
46
|
}
|
|
@@ -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" : "?";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PaneLock } from "../../../types/plugin";
|
|
2
|
+
|
|
3
|
+
/** A lock note on one line, as JSON and CSV say why a locked report is empty and as an error carries it. */
|
|
4
|
+
export function lockNoteReason(note: string): string {
|
|
5
|
+
return note.replace(/\s*\n\s*/g, " ");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The `reportLockNote` of a pane its wall covers whole (`useProFeatureWall`, a
|
|
10
|
+
* scanner's denied state): what the function is and that a wall shows none of
|
|
11
|
+
* it, what a free reader can still get and the command that reads it, and how
|
|
12
|
+
* to unlock it. One shape for every lock a reader meets in `fn`, so a refused
|
|
13
|
+
* read never ends in a bare "Sign in required".
|
|
14
|
+
*/
|
|
15
|
+
export function proLockNote(
|
|
16
|
+
{ gate, symbol, token: typed, verifyEmail }: PaneLock,
|
|
17
|
+
pane: {
|
|
18
|
+
/** The function's shortcut, such as `EK`; a pane that serves several names the one run in the lock. */
|
|
19
|
+
token: string;
|
|
20
|
+
/** What the function does, finishing "EK is part of Gloom Cloud Pro: it ...". */
|
|
21
|
+
does: string;
|
|
22
|
+
/** What the wall hides, as the object of "none of" and "to read": "it" or "them". */
|
|
23
|
+
shows: "it" | "them";
|
|
24
|
+
/**
|
|
25
|
+
* The free route and what it lists, finishing "Free, with no sign-in: ...".
|
|
26
|
+
* Name one only after running it signed out; leave it out when none answers.
|
|
27
|
+
*/
|
|
28
|
+
free?(ticker: string, company: string): string;
|
|
29
|
+
/** What the wall asks for: Gloom Cloud Pro (default), or only a free Gloom account. */
|
|
30
|
+
needs?: "pro" | "sign-in";
|
|
31
|
+
/**
|
|
32
|
+
* Whether the function takes a ticker: `none` for a board such as FLOW,
|
|
33
|
+
* `optional` for one that also lists every company (CALLS, JOBS), where the
|
|
34
|
+
* command repeats the ticker only when the reader gave one. Default `ticker`.
|
|
35
|
+
*/
|
|
36
|
+
argument?: "ticker" | "optional" | "none";
|
|
37
|
+
},
|
|
38
|
+
): string {
|
|
39
|
+
const token = typed ?? pane.token;
|
|
40
|
+
const ticker = symbol ?? "<ticker>";
|
|
41
|
+
// A plan refused at the server is Pro whatever the function usually asks for.
|
|
42
|
+
const pro = pane.needs !== "sign-in" || gate === "pro";
|
|
43
|
+
const without = gate === "pro" ? "On a free account" : verifyEmail ? "Until the email is verified" : "Signed out";
|
|
44
|
+
const what = pro ? "is part of Gloom Cloud Pro" : "needs a free Gloom sign-in";
|
|
45
|
+
const account = verifyEmail
|
|
46
|
+
? "verify your email (Ctrl+P, Resend Verification Email)"
|
|
47
|
+
: `log in or sign up${pro ? "" : " free"} from the command bar (Ctrl+P, Log In or Sign Up)`;
|
|
48
|
+
const unlock = gate === "pro"
|
|
49
|
+
? `To read ${pane.shows} in ${token}, upgrade in the app (gloomberb, then Ctrl+P, UPGRADE)`
|
|
50
|
+
: `To read ${pane.shows} in ${token}, open the app (gloomberb), ${account}${pro ? ", then upgrade (Ctrl+P, UPGRADE)" : ""}`;
|
|
51
|
+
const bare = pane.argument === "none" || (pane.argument === "optional" && !symbol);
|
|
52
|
+
const command = bare ? `gloomberb fn ${token}` : `gloomberb fn ${token} ${ticker}`;
|
|
53
|
+
return [
|
|
54
|
+
`${token} ${what}: ${pane.does}. ${without} it shows none of ${pane.shows}.`,
|
|
55
|
+
...pane.free ? [`Free, with no sign-in: ${pane.free(ticker, symbol ?? "the company")}`] : [],
|
|
56
|
+
`${unlock}; ${command} then reads with that session.`,
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../../../components";
|
|
11
11
|
import type { DataTableViewProps } from "../../../components/data-table/view";
|
|
12
12
|
import { handleRefreshKey } from "../../../components/data-table/table-pane";
|
|
13
|
+
import { FILL_REST } from "../../../components/ui/fill";
|
|
13
14
|
import { useShortcut } from "../../../react/input";
|
|
14
15
|
import { Box, ScrollBox, useUiCapabilities } from "../../../ui";
|
|
15
16
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
@@ -171,13 +172,17 @@ export function SeriesListDetail<Row, Column extends DataTableColumn, R extends
|
|
|
171
172
|
// pane height leaves its lower content clipped outside the scroll viewport.
|
|
172
173
|
const detailHeight = split ? bodyHeight : Math.max(1, bodyHeight - tableHeight);
|
|
173
174
|
const bodyFocused = focused && !list.searchFocused;
|
|
175
|
+
// On the desktop and the web the query bar is taller than a row, so the
|
|
176
|
+
// split table and the detail take what is left under it by flex and end at
|
|
177
|
+
// the pane footer; the row counts above stay the terminal's.
|
|
178
|
+
const fill = nativePaneChrome ? FILL_REST : null;
|
|
174
179
|
|
|
175
180
|
return (
|
|
176
|
-
<Box flexDirection="column" width={width}
|
|
181
|
+
<Box flexDirection="column" width={width} {...(fill ?? { height })}>
|
|
177
182
|
<QueryBar width={width} search={{ ...list.search, placeholder: searchPlaceholder }} filters={filters} view={list.range} />
|
|
178
|
-
<Box flexDirection={split ? "row" : "column"} flexGrow={1} overflow="hidden">
|
|
183
|
+
<Box flexDirection={split ? "row" : "column"} flexGrow={1} overflow="hidden" {...(fill ? { flexBasis: 0, minHeight: 0 } : {})}>
|
|
179
184
|
<Box flexDirection="column" width={listWidth} flexShrink={0}>
|
|
180
|
-
<Box flexDirection="column" width={listWidth}
|
|
185
|
+
<Box flexDirection="column" width={listWidth} overflow="hidden" {...(fill && split ? fill : { height: tableHeight, flexShrink: 0 })}>
|
|
181
186
|
<DataTableView<Row, Column>
|
|
182
187
|
focused={bodyFocused}
|
|
183
188
|
rootWidth={listWidth}
|
|
@@ -201,8 +206,8 @@ export function SeriesListDetail<Row, Column extends DataTableColumn, R extends
|
|
|
201
206
|
</Box>
|
|
202
207
|
</Box>
|
|
203
208
|
|
|
204
|
-
<Box flexDirection="column"
|
|
205
|
-
<ScrollBox
|
|
209
|
+
<Box flexDirection="column" width={detailWidth} overflow="hidden" {...(fill ? split ? { flexGrow: 1, minHeight: 0 } : fill : { flexGrow: 1, height: detailHeight })}>
|
|
210
|
+
<ScrollBox scrollY focusable={false} {...(fill ? fill : { height: detailHeight })}>
|
|
206
211
|
{renderDetail({ width: detailContentWidth, height: detailHeight, focused: bodyFocused })}
|
|
207
212
|
</ScrollBox>
|
|
208
213
|
</Box>
|
|
@@ -47,7 +47,7 @@ export function validateSocialMentions(payload: SocialMentionsPayload, symbol: s
|
|
|
47
47
|
|| payload.wikipedia !== undefined && !validWikipedia(payload.wikipedia)
|
|
48
48
|
|| payload.reddit !== undefined && !validReddit(payload.reddit)
|
|
49
49
|
|| !Array.isArray(payload.pending) || !Array.isArray(payload.warnings) || payload.warnings.some((warning) => typeof warning !== "string"))
|
|
50
|
-
throw new Error("
|
|
50
|
+
throw new Error("Social mention history is invalid");
|
|
51
51
|
return payload;
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -55,7 +55,7 @@ export async function fetchSocialMentions(symbol: string, range: SocialMentionsR
|
|
|
55
55
|
client: Pick<typeof apiClient, "getCloudSocialMentions"> = apiClient): Promise<SocialMentionsPayload> {
|
|
56
56
|
try { return validateSocialMentions(await client.getCloudSocialMentions(symbol, range), symbol, range); }
|
|
57
57
|
catch (error) {
|
|
58
|
-
if (error instanceof ApiRequestError && error.status === 404) throw new Error("Social mentions are not available
|
|
58
|
+
if (error instanceof ApiRequestError && error.status === 404) throw new Error("Social mentions are not available yet");
|
|
59
59
|
throw error;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -77,7 +77,7 @@ export async function loadSocialMentionPosts(symbol: string, date: string): Prom
|
|
|
77
77
|
const result = await socialMentionPostsCache.load(`${symbol}:${date}`, async () => {
|
|
78
78
|
const data = await apiClient.getCloudSocialMentionPosts(symbol, date);
|
|
79
79
|
if (data?.symbol !== symbol || data.day !== date || !Array.isArray(data.posts) || !data.posts.every(validPost))
|
|
80
|
-
throw new Error("
|
|
80
|
+
throw new Error("Social posts are invalid");
|
|
81
81
|
return data;
|
|
82
82
|
});
|
|
83
83
|
if (result.error && !result.data) throw result.error;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { SocialMentionsRange } from "../../../api-client/social-mentions";
|
|
2
2
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
3
|
+
import { explainRefusal } from "../shared/cloud-denied";
|
|
3
4
|
import { fetchSocialMentions } from "./client";
|
|
5
|
+
import { socialMentionsLockNote } from "./lock-note";
|
|
4
6
|
import { socialCount, socialDayRows, socialRatio, socialStance, topPostCell } from "./model";
|
|
5
7
|
|
|
6
8
|
export const socialMentionsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
7
9
|
shape: "bundle", argument: { kind: "ticker", description: "Ticker with a searchable cashtag (up to six letters and digits).", placeholder: "ticker" },
|
|
8
10
|
freshness: { source: "X posts", status: "not-a-feed", basis: "daily post counts" },
|
|
9
|
-
discovery: { aliases: ["BUZZ"], dataRequirements: ["Gloom Cloud X mention history"],
|
|
11
|
+
discovery: { aliases: ["BUZZ"], dataRequirements: ["Free Gloom sign-in", "Gloom Cloud X mention history"],
|
|
10
12
|
limitations: ["Daily $cashtag posts on X in UTC days, spam included.", "Top posts and stance cover days that were looked up.", "Views exist from 2022-12-22."] },
|
|
11
13
|
options: [
|
|
12
14
|
{ key: "range", type: "enum", description: "History window.", values: [{ value: "1y" }, { value: "5y" }, { value: "max" }], defaultValue: "1y", settingKey: "socialRange" },
|
|
@@ -14,7 +16,9 @@ export const socialMentionsHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
14
16
|
describe: (args) => `Social mentions | ${args.symbols[0]}`,
|
|
15
17
|
async load(args, ctx) {
|
|
16
18
|
const range = (["1y", "5y", "max"].includes(String(args.options.range)) ? args.options.range : "1y") as SocialMentionsRange;
|
|
17
|
-
const
|
|
19
|
+
const symbol = String(args.symbols[0]).toUpperCase();
|
|
20
|
+
const data = await fetchSocialMentions(symbol, range, ctx.apiClient)
|
|
21
|
+
.catch((error: unknown) => { throw explainRefusal(error, socialMentionsLockNote, { symbol }); });
|
|
18
22
|
return { complete: !data.pending.length, errors: data.warnings,
|
|
19
23
|
sections: [{ title: "Daily posts on X", columns: [
|
|
20
24
|
{ key: "day", header: "Date" },
|
|
@@ -3,6 +3,7 @@ import { SOCIAL_MENTION_SERIES } from "./series";
|
|
|
3
3
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
4
4
|
import { socialMentionPostsCache, socialMentionsCache } from "./client";
|
|
5
5
|
import { socialMentionsHeadless } from "./headless";
|
|
6
|
+
import { socialMentionsLockNote } from "./lock-note";
|
|
6
7
|
import { SocialMentionsPane } from "./pane";
|
|
7
8
|
|
|
8
9
|
const socialMentionsSettings = [
|
|
@@ -11,7 +12,7 @@ const socialMentionsSettings = [
|
|
|
11
12
|
];
|
|
12
13
|
export const socialMentionsModule: PluginModule = {
|
|
13
14
|
series: SOCIAL_MENTION_SERIES,
|
|
14
|
-
panes: [{ id: "social-mentions", name: "Social Mentions", icon: "B", component: SocialMentionsPane,
|
|
15
|
+
panes: [{ id: "social-mentions", reportLockNote: socialMentionsLockNote, name: "Social Mentions", icon: "B", component: SocialMentionsPane,
|
|
15
16
|
defaultPosition: "right", tickerFollower: true, defaultMode: "floating", defaultFloatingSize: { width: 96, height: 30 },
|
|
16
17
|
tableExport: true, headless: socialMentionsHeadless, settings: { title: "Social Mentions", fields: socialMentionsSettings } }],
|
|
17
18
|
paneTemplates: [{ ...createTickerSurfacePaneTemplate({ id: "social-mentions-pane", paneId: "social-mentions", label: "Social Mentions",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PaneLock } from "../../../types/plugin";
|
|
2
|
+
import { proLockNote } from "../shared/pro-lock-note";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BUZZ needs a signed-in account, not a plan. No command reads the posts it
|
|
6
|
+
* counts without one, so the note names no free route.
|
|
7
|
+
*/
|
|
8
|
+
export function socialMentionsLockNote(lock: PaneLock): string {
|
|
9
|
+
return proLockNote(lock, {
|
|
10
|
+
token: "BUZZ",
|
|
11
|
+
needs: "sign-in",
|
|
12
|
+
does: "it counts the daily posts on X that name a company's cashtag, with the top posts and their stance",
|
|
13
|
+
shows: "it",
|
|
14
|
+
});
|
|
15
|
+
}
|