gloomberb 0.17.2 → 0.17.4
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 +2 -1
- package/src/api-client/chat.ts +28 -0
- package/src/api-client/errors.ts +11 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +22 -9
- package/src/api-client/normalizers.ts +21 -0
- package/src/api-client/socket.ts +27 -0
- package/src/api-client/types.ts +3 -1
- package/src/app/runtime/startup.ts +4 -0
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/index.ts +1 -1
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/metric-treemap/heat-surface.tsx +76 -14
- package/src/components/metric-treemap/heat-tiles.ts +5 -3
- package/src/components/metric-treemap/index.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/button.tsx +24 -9
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/status.tsx +148 -5
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/layout-marketplace/gallery-desktop.tsx +2 -2
- package/src/market-data/futures-board.ts +44 -1
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/alerts/cli.ts +72 -11
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/awards/headless.ts +5 -1
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/catalysts/headless.ts +5 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/controller/connections.ts +18 -0
- package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +49 -21
- package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
- package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/model.ts +20 -0
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +19 -2
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +33 -6
- package/src/plugins/builtin/futures/model.ts +69 -5
- package/src/plugins/builtin/futures/series.ts +3 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/jobs/pane.tsx +12 -9
- package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/news/headless.ts +4 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +4 -12
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/build-assets.ts +17 -4
- package/src/renderers/dom/desktop/controls.tsx +2 -0
- package/src/renderers/dom/styles.css +6 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
- package/src/sources/gloomberb-cloud/normalizers.ts +13 -4
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +24 -5
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/ui/resize-settle.ts +128 -0
- package/src/utils/calendar-date.ts +56 -0
- package/src/utils/decimal-comma.ts +31 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/futures-generic.ts +25 -4
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
- package/src/utils/plugin-proxy-hosts.json +5 -0
- package/src/utils/utc-time.ts +14 -3
|
@@ -53,6 +53,64 @@ function formatPercent(value: number | null): string {
|
|
|
53
53
|
return value == null ? "-" : `${formatNumber(value, 2)}%`;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const finiteNumber = (value: number | null | undefined): value is number => typeof value === "number" && Number.isFinite(value);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Shares short over the float, in percent. Short interest above the float is
|
|
60
|
+
* possible (shares lent twice), twice it is not: such a float is another
|
|
61
|
+
* class's (BRK.B's is Berkshire's Class A), so it is not used. The bound
|
|
62
|
+
* Gloom Cloud's statistics use for their short % of float.
|
|
63
|
+
*/
|
|
64
|
+
function percentOfFloat(sharesShort: number, floatShares: number | null | undefined): number | null {
|
|
65
|
+
return finiteNumber(floatShares) && floatShares > 0 && sharesShort <= floatShares * 2 ? (sharesShort / floatShares) * 100 : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The statistics' short % of float (a fraction, null when not served) and the
|
|
70
|
+
* settlement it is of: Gloom Cloud serves it on the float it serves, from the
|
|
71
|
+
* settlement SI and SIW show, as the overview and `fundamentals` read it.
|
|
72
|
+
*/
|
|
73
|
+
export interface SettlementFloatStatistics {
|
|
74
|
+
settlementDate: string;
|
|
75
|
+
shortPercentOfFloat: number | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A settlement's percent of float: the statistics' own when they are of the
|
|
80
|
+
* same settlement, then a percentage the source states, then the shares short
|
|
81
|
+
* over the current float.
|
|
82
|
+
*/
|
|
83
|
+
export function settlementPercentFloat(
|
|
84
|
+
record: ShortInterestRecord,
|
|
85
|
+
floatShares: number | null | undefined,
|
|
86
|
+
statistics?: SettlementFloatStatistics | null,
|
|
87
|
+
): number | null {
|
|
88
|
+
if (statistics && statistics.settlementDate === record.settlementDate.toISOString().slice(0, 10)) {
|
|
89
|
+
return finiteNumber(statistics.shortPercentOfFloat) ? statistics.shortPercentOfFloat * 100 : null;
|
|
90
|
+
}
|
|
91
|
+
return record.shortPercentFloat ?? percentOfFloat(record.sharesShort, floatShares);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The newest settlement's percent of float where the source gives none, read
|
|
96
|
+
* against the current float as SIW reads it (settlementPercentFloat). The
|
|
97
|
+
* float is undated, so the settlements before it stay blank rather than take
|
|
98
|
+
* today's float.
|
|
99
|
+
*/
|
|
100
|
+
export function withLatestPercentFloat(
|
|
101
|
+
records: readonly ShortInterestRecord[],
|
|
102
|
+
floatShares: number | null | undefined,
|
|
103
|
+
statistics?: SettlementFloatStatistics | null,
|
|
104
|
+
): ShortInterestRecord[] {
|
|
105
|
+
const latest = records.reduce<ShortInterestRecord | null>(
|
|
106
|
+
(best, record) => (!best || record.settlementDate.getTime() > best.settlementDate.getTime() ? record : best),
|
|
107
|
+
null,
|
|
108
|
+
);
|
|
109
|
+
return records.map((record) => (record === latest
|
|
110
|
+
? { ...record, shortPercentFloat: settlementPercentFloat(record, floatShares, statistics) }
|
|
111
|
+
: record));
|
|
112
|
+
}
|
|
113
|
+
|
|
56
114
|
export function buildRows(records: ShortInterestRecord[]): ShortInterestRow[] {
|
|
57
115
|
return records.map((record, index) => ({
|
|
58
116
|
key: `${record.settlementDate.toISOString()}:${index}`,
|
|
@@ -89,9 +147,10 @@ export function sortRows(rows: ShortInterestRow[], preference: SortPreference):
|
|
|
89
147
|
/**
|
|
90
148
|
* The columns sit together rather than spread across the pane, so the figures
|
|
91
149
|
* of one settlement read as a row, and fit a 60-column pane without a
|
|
92
|
-
* horizontal scrollbar. FINRA settlement history carries no float, so percent of float
|
|
93
|
-
*
|
|
94
|
-
*
|
|
150
|
+
* horizontal scrollbar. FINRA settlement history carries no float, so percent of float
|
|
151
|
+
* is null for every row unless the latest one is read against the current float
|
|
152
|
+
* (`withLatestPercentFloat`). Keeping the column with nothing in it drew a header over a
|
|
153
|
+
* column of dashes, which reads as missing data rather than data the source never had.
|
|
95
154
|
*/
|
|
96
155
|
export function buildColumns(
|
|
97
156
|
records: readonly ShortInterestRecord[] = [],
|
|
@@ -19,7 +19,6 @@ import { Box, TextAttributes } from "../../../ui";
|
|
|
19
19
|
import { isKnownNonUsEquityTicker } from "../../../utils/sec";
|
|
20
20
|
import { nextHeaderSort } from "../../../utils/sort-values";
|
|
21
21
|
import { usePluginPaneState } from "../../runtime";
|
|
22
|
-
import { loadShortInterest } from "./client";
|
|
23
22
|
import {
|
|
24
23
|
DEFAULT_SORT,
|
|
25
24
|
buildColumns,
|
|
@@ -34,6 +33,7 @@ import {
|
|
|
34
33
|
type SortPreference,
|
|
35
34
|
} from "./model";
|
|
36
35
|
import type { ShortInterestRecord } from "./types";
|
|
36
|
+
import { loadShortInterestWithFloat } from "./watch-client";
|
|
37
37
|
import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
|
|
38
38
|
|
|
39
39
|
const EMPTY_RECORDS: ShortInterestRecord[] = [];
|
|
@@ -45,7 +45,7 @@ function ShortInterestView({ width, height, focused }: { width: number; height:
|
|
|
45
45
|
const symbol = ticker?.metadata.ticker ?? null;
|
|
46
46
|
|
|
47
47
|
const skipNonUs = isKnownNonUsEquityTicker(ticker);
|
|
48
|
-
const request = useCallback(() =>
|
|
48
|
+
const request = useCallback(() => loadShortInterestWithFloat(symbol!), [symbol]);
|
|
49
49
|
const resource = useAsyncResource(symbol && !skipNonUs ? request : null, { clearOnError: true });
|
|
50
50
|
const { error, updatedAt, reload: refresh } = resource;
|
|
51
51
|
const records = resource.data?.records ?? EMPTY_RECORDS;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { getPublishedUsEquityCalendarDay } from "../../../market-data/published-us-sessions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* When a short interest settlement reaches readers. FINRA's calendar follows a
|
|
5
|
+
* fixed rule (Rule 4560): positions are struck on the 15th of the month, or the
|
|
6
|
+
* business day before when it is not one, and on the month's last business day;
|
|
7
|
+
* firms report two business days later and FINRA publishes seven business days
|
|
8
|
+
* after the settlement. The dataset carries only the settlement date, so the
|
|
9
|
+
* rest is read from that rule, against the US exchange holidays.
|
|
10
|
+
*/
|
|
11
|
+
const PUBLICATION_LAG_BUSINESS_DAYS = 7;
|
|
12
|
+
const MID_MONTH_DAY = 15;
|
|
13
|
+
const DAY_MS = 86_400_000;
|
|
14
|
+
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
|
|
15
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
16
|
+
|
|
17
|
+
export interface ShortInterestSchedule {
|
|
18
|
+
/** The settlement the schedule is read from, `YYYY-MM-DD`. */
|
|
19
|
+
settlement: string;
|
|
20
|
+
/** When FINRA published that settlement. */
|
|
21
|
+
published: string;
|
|
22
|
+
/** The first settlement after it. */
|
|
23
|
+
nextSettlement: string;
|
|
24
|
+
/** When FINRA publishes that one. */
|
|
25
|
+
nextPublished: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isoDay = (ms: number) => new Date(ms).toISOString().slice(0, 10);
|
|
29
|
+
const parseDay = (date: string) => Date.parse(`${date}T00:00:00Z`);
|
|
30
|
+
|
|
31
|
+
/** A US business day: a weekday the exchanges are open. Years the calendar does not cover read as weekdays only. */
|
|
32
|
+
function isBusinessDay(ms: number): boolean {
|
|
33
|
+
const weekday = new Date(ms).getUTCDay();
|
|
34
|
+
if (weekday === 0 || weekday === 6) return false;
|
|
35
|
+
return getPublishedUsEquityCalendarDay("NYSE", isoDay(ms)) !== "closed";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function addBusinessDays(ms: number, count: number): number {
|
|
39
|
+
let day = ms;
|
|
40
|
+
for (let left = count; left > 0;) {
|
|
41
|
+
day += DAY_MS;
|
|
42
|
+
if (isBusinessDay(day)) left -= 1;
|
|
43
|
+
}
|
|
44
|
+
return day;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The business day on or before `ms`. */
|
|
48
|
+
function businessDayOnOrBefore(ms: number): number {
|
|
49
|
+
let day = ms;
|
|
50
|
+
while (!isBusinessDay(day)) day -= DAY_MS;
|
|
51
|
+
return day;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The two settlements of a month, in order: the 15th and the last business day. */
|
|
55
|
+
function monthSettlements(year: number, month: number): [number, number] {
|
|
56
|
+
return [
|
|
57
|
+
businessDayOnOrBefore(Date.UTC(year, month, MID_MONTH_DAY)),
|
|
58
|
+
// Day 0 of the next month is this month's last day.
|
|
59
|
+
businessDayOnOrBefore(Date.UTC(year, month + 1, 0)),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The first settlement strictly after `ms`. */
|
|
64
|
+
function settlementAfter(ms: number): number {
|
|
65
|
+
const from = new Date(ms);
|
|
66
|
+
for (let step = 0; step < 3; step += 1) {
|
|
67
|
+
const found = monthSettlements(from.getUTCFullYear(), from.getUTCMonth() + step).find((day) => day > ms);
|
|
68
|
+
if (found != null) return found;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`No short interest settlement follows ${isoDay(ms)}.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The publication dates of a settlement and of the one after it; null for a date that is not `YYYY-MM-DD`. */
|
|
74
|
+
export function shortInterestSchedule(settlement: string): ShortInterestSchedule | null {
|
|
75
|
+
const ms = parseDay(settlement);
|
|
76
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(settlement) || !Number.isFinite(ms) || isoDay(ms) !== settlement) return null;
|
|
77
|
+
const next = settlementAfter(ms);
|
|
78
|
+
return {
|
|
79
|
+
settlement,
|
|
80
|
+
published: isoDay(addBusinessDays(ms, PUBLICATION_LAG_BUSINESS_DAYS)),
|
|
81
|
+
nextSettlement: isoDay(next),
|
|
82
|
+
nextPublished: isoDay(addBusinessDays(next, PUBLICATION_LAG_BUSINESS_DAYS)),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** `Fri 9 Oct`, with its year when it is not the settlement's: a reader sees the year once, in the settlement's own date. */
|
|
87
|
+
function dayText(date: string, settlement: string): string {
|
|
88
|
+
const ms = parseDay(date);
|
|
89
|
+
const day = new Date(ms);
|
|
90
|
+
const year = date.slice(0, 4) === settlement.slice(0, 4) ? "" : ` ${date.slice(0, 4)}`;
|
|
91
|
+
return `${WEEKDAYS[day.getUTCDay()]} ${day.getUTCDate()} ${MONTHS[day.getUTCMonth()]}${year}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `published Fri 9 Oct, next settlement Thu 15 Oct, published Mon 26 Oct`: what a
|
|
96
|
+
* trader could see on a given day, beside the settlement date the report cites.
|
|
97
|
+
*/
|
|
98
|
+
export function shortInterestScheduleText(schedule: ShortInterestSchedule): string {
|
|
99
|
+
const day = (date: string) => dayText(date, schedule.settlement);
|
|
100
|
+
return `published ${day(schedule.published)}, next settlement ${day(schedule.nextSettlement)}, published ${day(schedule.nextPublished)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The report's freshness for the newest settlement it shows; nothing when no settlement is dated. */
|
|
104
|
+
export function shortInterestFreshness(latestSettlement: string | null | undefined): { schedule: string; publishedAt: string } | undefined {
|
|
105
|
+
const schedule = latestSettlement ? shortInterestSchedule(latestSettlement) : null;
|
|
106
|
+
return schedule ? { schedule: shortInterestScheduleText(schedule), publishedAt: schedule.published } : undefined;
|
|
107
|
+
}
|
|
@@ -5,7 +5,8 @@ import type { QueryEntry } from "../../../market-data/result-types";
|
|
|
5
5
|
import type { DataProvider } from "../../../types/data-provider";
|
|
6
6
|
import type { PricePoint, TickerFinancials } from "../../../types/financials";
|
|
7
7
|
import { errorMessage } from "../../../utils/errors";
|
|
8
|
-
import { loadShortInterest } from "./client";
|
|
8
|
+
import { loadShortInterest, type ShortInterestResult } from "./client";
|
|
9
|
+
import { type SettlementFloatStatistics, withLatestPercentFloat } from "./model";
|
|
9
10
|
import { buildShortWatchRow, type ShortWatchRow } from "./watch-model";
|
|
10
11
|
import type { ShortInterestRecord } from "./types";
|
|
11
12
|
|
|
@@ -30,10 +31,55 @@ export function createShortWatchDependencies(
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/** A symbol's current float and the statistics' short % of float with its settlement, from its snapshot. */
|
|
35
|
+
interface FloatSnapshot {
|
|
36
|
+
floatShares: number | null;
|
|
37
|
+
statistics: SettlementFloatStatistics | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Each symbol's float and statistics, in order: empty where its snapshot is missing or carries none. */
|
|
41
|
+
async function loadFloats(
|
|
42
|
+
symbols: readonly string[],
|
|
43
|
+
loadSnapshots: ShortWatchDependencies["loadSnapshots"],
|
|
44
|
+
options: { forceRefresh?: boolean } = {},
|
|
45
|
+
): Promise<FloatSnapshot[]> {
|
|
46
|
+
const entries = await Promise.resolve()
|
|
47
|
+
.then(() => loadSnapshots(symbols.map((symbol) => ({ symbol, exchange: "" })), options))
|
|
48
|
+
.catch(() => []);
|
|
49
|
+
return symbols.map((_symbol, index) => {
|
|
50
|
+
const entry = entries[index];
|
|
51
|
+
const fundamentals = (entry ? resolveEntryValue(entry) : null)?.fundamentals;
|
|
52
|
+
return {
|
|
53
|
+
floatShares: fundamentals?.floatShares ?? null,
|
|
54
|
+
statistics: fundamentals?.shortInterestDate
|
|
55
|
+
? { settlementDate: fundamentals.shortInterestDate, shortPercentOfFloat: fundamentals.shortPercentOfFloat ?? null }
|
|
56
|
+
: null,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A ticker's settlements with the newest one's percent of float, as SIW reads
|
|
63
|
+
* it (settlementPercentFloat). A snapshot that fails leaves the settlements as
|
|
64
|
+
* they are.
|
|
65
|
+
*/
|
|
66
|
+
export async function loadShortInterestWithFloat(
|
|
67
|
+
symbol: string,
|
|
68
|
+
{ marketData, client = apiClient, loadSnapshots = createShortWatchDependencies(marketData, client).loadSnapshots }: {
|
|
69
|
+
marketData?: DataProvider;
|
|
70
|
+
client?: Pick<typeof apiClient, "getCloudShortInterest">;
|
|
71
|
+
loadSnapshots?: ShortWatchDependencies["loadSnapshots"];
|
|
72
|
+
} = {},
|
|
73
|
+
): Promise<ShortInterestResult> {
|
|
74
|
+
const [result, [snapshot]] = await Promise.all([loadShortInterest(symbol, client), loadFloats([symbol], loadSnapshots)]);
|
|
75
|
+
return { ...result, records: withLatestPercentFloat(result.records, snapshot?.floatShares, snapshot?.statistics) };
|
|
76
|
+
}
|
|
77
|
+
|
|
33
78
|
/**
|
|
34
|
-
* Every name's settlements, float and daily closes. The floats
|
|
35
|
-
*
|
|
36
|
-
*
|
|
79
|
+
* Every name's settlements, float and daily closes. The floats, with the
|
|
80
|
+
* statistics' short % of float and its settlement, come in one batched
|
|
81
|
+
* snapshot; settlements and closes are per symbol, six at a time. A name
|
|
82
|
+
* whose closes fail keeps its short interest without a price move.
|
|
37
83
|
*/
|
|
38
84
|
export async function loadShortWatch(
|
|
39
85
|
symbols: readonly string[],
|
|
@@ -41,7 +87,7 @@ export async function loadShortWatch(
|
|
|
41
87
|
dependencies: ShortWatchDependencies = createShortWatchDependencies(),
|
|
42
88
|
): Promise<ShortWatchRow[]> {
|
|
43
89
|
const instruments = symbols.map((symbol) => ({ symbol, exchange: "" }));
|
|
44
|
-
const
|
|
90
|
+
const floats = loadFloats(symbols, dependencies.loadSnapshots, { forceRefresh: options.forceRefresh });
|
|
45
91
|
const rows = new Array<ShortWatchRow>(symbols.length);
|
|
46
92
|
let next = 0;
|
|
47
93
|
const worker = async () => {
|
|
@@ -53,12 +99,12 @@ export async function loadShortWatch(
|
|
|
53
99
|
dependencies.loadChart({ instrument, bufferRange: "3M", granularity: "resolution", resolution: "1d" },
|
|
54
100
|
{ forceRefresh: options.forceRefresh }).then((entry) => resolveEntryValue(entry) ?? []),
|
|
55
101
|
]);
|
|
56
|
-
const
|
|
57
|
-
const floatShares = (entry ? resolveEntryValue(entry) : null)?.fundamentals?.floatShares ?? null;
|
|
102
|
+
const snapshot = (await floats)[index];
|
|
58
103
|
rows[index] = buildShortWatchRow({
|
|
59
104
|
symbol: instrument.symbol,
|
|
60
105
|
records: records.status === "fulfilled" ? records.value : null,
|
|
61
|
-
floatShares,
|
|
106
|
+
floatShares: snapshot?.floatShares ?? null,
|
|
107
|
+
statistics: snapshot?.statistics ?? null,
|
|
62
108
|
history: history.status === "fulfilled" ? history.value : null,
|
|
63
109
|
error: records.status === "rejected" ? errorMessage(records.reason) : null,
|
|
64
110
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
2
2
|
import { formatCompact, formatNumber } from "../../../utils/format";
|
|
3
|
+
import { shortInterestFreshness } from "./schedule";
|
|
3
4
|
import { createShortWatchDependencies, loadShortWatch } from "./watch-client";
|
|
4
5
|
import { customUniverse, type ShortWatchSetup, setupLabel, SHORT_WATCH_LIMIT, sortShortWatchRows } from "./watch-model";
|
|
5
6
|
|
|
@@ -13,33 +14,37 @@ export const shortWatchHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
13
14
|
freshness: { source: "FINRA", status: "not-a-feed", basis: "twice-monthly settlement", observedKey: "settlementDate", maxAgeMinutes: 30 * 24 * 60 },
|
|
14
15
|
argument: { kind: "symbol-list", maximum: SHORT_WATCH_LIMIT, description: "US tickers; the pane defaults to your portfolios and watchlists." },
|
|
15
16
|
usListingsOnly: "FINRA short interest",
|
|
17
|
+
failsWithoutData: true,
|
|
16
18
|
options: [],
|
|
17
19
|
describe: "Short squeeze watch",
|
|
18
20
|
discovery: {
|
|
19
21
|
aliases: ["SIW"],
|
|
20
22
|
screenshotReadiness: "live-dom",
|
|
21
23
|
dataRequirements: ["FINRA short interest settlements", "Float", "Daily closes"],
|
|
22
|
-
limitations: ["
|
|
24
|
+
limitations: ["Borrow fee, utilization and locate availability are not available: FINRA short interest counts shares short, not what they cost to borrow.",
|
|
25
|
+
"Percent of float uses the current float against the settlement's shares short.",
|
|
23
26
|
"Crowded is a fixed threshold, not a squeeze forecast."],
|
|
24
27
|
},
|
|
25
28
|
columns: [
|
|
26
29
|
{ key: "symbol", header: "Symbol" },
|
|
27
30
|
{ key: "setup", header: "Setup", format: (value) => setupLabel(value as ShortWatchSetup | null) || "--" },
|
|
28
31
|
{ key: "percentFloat", header: "% Float", align: "right", format: number(2, "%") },
|
|
29
|
-
{ key: "daysToCover", header: "Days to
|
|
32
|
+
{ key: "daysToCover", header: "Days to cover", align: "right", format: number(2) },
|
|
30
33
|
{ key: "changePercent", header: "SI Chg %", align: "right", format: signedPercent },
|
|
31
34
|
{ key: "return1M", header: "1M %", align: "right", format: signedPercent },
|
|
32
35
|
{ key: "sharesShort", header: "Shares Short", align: "right", format: (value) => typeof value === "number" ? formatCompact(value) : "--" },
|
|
33
36
|
{ key: "settlementDate", header: "Settled" },
|
|
34
|
-
{ key: "closeDate", header: "Close" },
|
|
37
|
+
{ key: "closeDate", header: "Close date" },
|
|
35
38
|
],
|
|
36
39
|
async load(args, ctx) {
|
|
37
40
|
const universe = customUniverse(args.symbols.join(","));
|
|
38
41
|
const rows = sortShortWatchRows(await loadShortWatch(universe.symbols, { signal: ctx.signal, forceRefresh: ctx.refresh },
|
|
39
42
|
createShortWatchDependencies(ctx.marketData, ctx.apiClient)), "setup", "desc");
|
|
40
43
|
const missing = rows.filter((row) => !row.settlementDate);
|
|
44
|
+
const freshness = shortInterestFreshness(rows.map((row) => row.settlementDate).filter((date): date is string => !!date).sort().at(-1));
|
|
41
45
|
return {
|
|
42
46
|
rows: rows.map(({ error: _error, ...row }) => row),
|
|
47
|
+
...(freshness ? { freshness } : {}),
|
|
43
48
|
complete: !missing.length && !universe.notice && rows.every((row) => row.return1M != null),
|
|
44
49
|
symbols: universe.symbols,
|
|
45
50
|
unavailableSymbols: missing.map((row) => row.symbol),
|
|
@@ -3,7 +3,9 @@ import type { PricePoint } from "../../../types/financials";
|
|
|
3
3
|
import type { TickerRecord } from "../../../types/ticker";
|
|
4
4
|
import { isKnownNonUsListing, listingAbroadVenue } from "../../../utils/sec";
|
|
5
5
|
import { parsePublicTickerKey } from "../../../utils/exchanges";
|
|
6
|
+
import { splitSymbolArgs } from "../../../cli/symbol-args";
|
|
6
7
|
import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
|
|
8
|
+
import { type SettlementFloatStatistics, settlementPercentFloat } from "./model";
|
|
7
9
|
import type { ShortInterestRecord } from "./types";
|
|
8
10
|
|
|
9
11
|
/** Short interest is published per settlement for each US listing, so a list fetches one symbol at a time. */
|
|
@@ -51,6 +53,8 @@ export interface ShortWatchInputs {
|
|
|
51
53
|
symbol: string;
|
|
52
54
|
records: readonly ShortInterestRecord[] | null;
|
|
53
55
|
floatShares: number | null;
|
|
56
|
+
/** The statistics' short % of float and its settlement (settlementPercentFloat). */
|
|
57
|
+
statistics?: SettlementFloatStatistics | null;
|
|
54
58
|
history: readonly PricePoint[] | null;
|
|
55
59
|
error?: string | null;
|
|
56
60
|
}
|
|
@@ -101,19 +105,16 @@ function shortWatchSetup(row: Pick<ShortWatchRow, "percentFloat" | "daysToCover"
|
|
|
101
105
|
|
|
102
106
|
/**
|
|
103
107
|
* One name's latest settlement, its change from the settlement before, its
|
|
104
|
-
* shares short over the float, and its month's price move.
|
|
105
|
-
*
|
|
106
|
-
*
|
|
108
|
+
* shares short over the float, and its month's price move. The percent of
|
|
109
|
+
* float is the statistics' own when they are of the same settlement, so it
|
|
110
|
+
* reads as the overview and `fundamentals` do; otherwise the settlement's
|
|
111
|
+
* shares short over the snapshot's float.
|
|
107
112
|
*/
|
|
108
113
|
export function buildShortWatchRow(inputs: ShortWatchInputs): ShortWatchRow {
|
|
109
114
|
const records = [...(inputs.records ?? [])].sort((left, right) => left.settlementDate.getTime() - right.settlementDate.getTime());
|
|
110
115
|
const latest = records.at(-1);
|
|
111
116
|
const prior = records.at(-2);
|
|
112
|
-
|
|
113
|
-
const percentFloat = latest
|
|
114
|
-
? latest.shortPercentFloat ?? (finite(inputs.floatShares) && inputs.floatShares >= latest.sharesShort && inputs.floatShares > 0
|
|
115
|
-
? latest.sharesShort / inputs.floatShares * 100 : null)
|
|
116
|
-
: null;
|
|
117
|
+
const percentFloat = latest ? settlementPercentFloat(latest, inputs.floatShares, inputs.statistics) : null;
|
|
117
118
|
const move = inputs.history ? oneMonthReturn(inputs.history) : null;
|
|
118
119
|
const row = {
|
|
119
120
|
symbol: inputs.symbol,
|
|
@@ -163,10 +164,20 @@ export function sharedDate(rows: readonly ShortWatchRow[], key: "settlementDate"
|
|
|
163
164
|
|
|
164
165
|
export interface ShortWatchUniverse { symbols: string[]; notice: string | null }
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
const NOT_US_LISTED = "not US-listed";
|
|
168
|
+
const NOT_A_US_TICKER = "not a US ticker symbol";
|
|
169
|
+
const NOT_A_STOCK_OR_FUND = "not a US-listed stock or fund";
|
|
170
|
+
|
|
171
|
+
/** A US-listed stock or fund; otherwise why this name has no short interest. */
|
|
172
|
+
function listedReason(ticker: Pick<TickerRecord, "metadata">): string | null {
|
|
168
173
|
const category = (ticker.metadata.assetCategory ?? "").toUpperCase().replace(/[\s_-]/g, "");
|
|
169
|
-
|
|
174
|
+
if (!LISTED_TYPES.has(category)) return NOT_A_STOCK_OR_FUND;
|
|
175
|
+
return isKnownNonUsListing(ticker as TickerRecord) ? NOT_US_LISTED : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** "Left out: ABC.L: not US-listed; XYZ: not a US ticker symbol.", or null when every name was read. */
|
|
179
|
+
function leftOut(skipped: readonly { name: string; reason: string }[]): string | null {
|
|
180
|
+
return skipped.length ? `Left out: ${skipped.map(({ name, reason }) => `${name}: ${reason}`).join("; ")}.` : null;
|
|
170
181
|
}
|
|
171
182
|
|
|
172
183
|
function cap(symbols: string[], skipped: string | null): ShortWatchUniverse {
|
|
@@ -177,22 +188,40 @@ function cap(symbols: string[], skipped: string | null): ShortWatchUniverse {
|
|
|
177
188
|
};
|
|
178
189
|
}
|
|
179
190
|
|
|
180
|
-
const plural = (count: number) => `${count} name${count === 1 ? "" : "s"}`;
|
|
181
|
-
|
|
182
191
|
/** Every name in the user's portfolios and watchlists. */
|
|
183
192
|
export function ownNamesUniverse(tickers: readonly Pick<TickerRecord, "metadata">[]): ShortWatchUniverse {
|
|
184
193
|
const members = tickers.filter((ticker) => ticker.metadata.portfolios.length > 0 || ticker.metadata.watchlists.length > 0);
|
|
185
|
-
const symbols =
|
|
186
|
-
const skipped
|
|
187
|
-
|
|
194
|
+
const symbols: string[] = [];
|
|
195
|
+
const skipped: { name: string; reason: string }[] = [];
|
|
196
|
+
for (const ticker of members) {
|
|
197
|
+
const symbol = ticker.metadata.ticker.toUpperCase();
|
|
198
|
+
const reason = listedReason(ticker) ?? (SYMBOL.test(symbol) ? null : NOT_A_US_TICKER);
|
|
199
|
+
if (reason) skipped.push({ name: symbol, reason });
|
|
200
|
+
else symbols.push(symbol);
|
|
201
|
+
}
|
|
202
|
+
return cap(symbols.sort(), leftOut(skipped.sort((left, right) => left.name.localeCompare(right.name))));
|
|
188
203
|
}
|
|
189
204
|
|
|
205
|
+
/** A share class written with a dash (BRK-B, BF-B) is the dotted symbol (BRK.B, BF.B) short interest is filed under. */
|
|
206
|
+
const DASHED_CLASS_SHARE = /^([A-Z]{1,5})-([A-Z])$/;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The names typed or pasted: split by commas, semicolons or blanks, each
|
|
210
|
+
* without the quote marks or the `$` around it. A name this cannot read as a US
|
|
211
|
+
* symbol is left out and named with its reason.
|
|
212
|
+
*/
|
|
190
213
|
export function customUniverse(text: string): ShortWatchUniverse {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
const symbols: string[] = [];
|
|
215
|
+
const skipped: { name: string; reason: string }[] = [];
|
|
216
|
+
for (const entry of splitSymbolArgs([text]).map((value) => value.replace(/^\$+/, "").toUpperCase())) {
|
|
217
|
+
// A listing abroad (SAN:EPA) is not its symbol's US line, which can be another company's.
|
|
218
|
+
if (listingAbroadVenue(entry) != null) {
|
|
219
|
+
skipped.push({ name: entry, reason: NOT_US_LISTED });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const symbol = parsePublicTickerKey(entry).symbol.replace(DASHED_CLASS_SHARE, "$1.$2");
|
|
223
|
+
if (SYMBOL.test(symbol)) symbols.push(symbol);
|
|
224
|
+
else skipped.push({ name: entry, reason: NOT_A_US_TICKER });
|
|
225
|
+
}
|
|
226
|
+
return cap(symbols, leftOut(skipped));
|
|
198
227
|
}
|
|
@@ -50,7 +50,7 @@ const COLUMNS: WatchColumn[] = [
|
|
|
50
50
|
{ id: "return1M", label: "1M%", width: 8, align: "right" },
|
|
51
51
|
{ id: "sharesShort", label: "Short", width: 8, align: "right" },
|
|
52
52
|
{ id: "settlementDate", label: "Settled", width: 8, align: "right" },
|
|
53
|
-
{ id: "closeDate", label: "Close", width:
|
|
53
|
+
{ id: "closeDate", label: "Close date", width: 10, align: "right" },
|
|
54
54
|
];
|
|
55
55
|
const DEFAULT_SORT = { columnId: "setup" as ShortWatchSortId, direction: "desc" as SortDirection };
|
|
56
56
|
const rowKey = (row: ShortWatchRow) => row.symbol;
|
|
@@ -183,9 +183,10 @@ export function SupplyFlow({ rows, symbol, focusName, focusId, width, height, fo
|
|
|
183
183
|
const hasBand = (band: "suppliers" | "customers") => nodes.some((node) => node.band === band);
|
|
184
184
|
return <Box width={width} height={height} flexGrow={1} flexBasis={0} minHeight={0} position="relative" overflow="hidden">
|
|
185
185
|
<ChartSurface ref={surfaceRef} width={width} height={height} position="absolute" left={0} top={0}
|
|
186
|
-
vectors={desktop ? vectors : undefined} bitmap={bitmap} flexDirection="column"
|
|
186
|
+
vectors={desktop ? vectors : undefined} bitmap={bitmap} flexDirection="column"
|
|
187
187
|
onMouseMove={hoverAt} onMouseOut={() => setHovered(null)}
|
|
188
|
-
onMouseDown={() => { const node = hovered ? nodes.find((entry) => entry.id === hovered) : null; if (node) selectNode(node); }}
|
|
188
|
+
onMouseDown={() => { const node = hovered ? nodes.find((entry) => entry.id === hovered) : null; if (node) selectNode(node); }}
|
|
189
|
+
aria-label={hovered ? flowNodeName(nodes.find((entry) => entry.id === hovered)!) : "Supply chain flow"}>
|
|
189
190
|
{fallback ? fallback.map((spans, y) => <Text key={y}>{spans.map((span, i) => <Span key={i} fg={span.color}>{span.text}</Span>)}</Text>) : null}
|
|
190
191
|
</ChartSurface>
|
|
191
192
|
{header("suppliers", 1)}
|
|
@@ -217,6 +218,13 @@ export function SupplyFlow({ rows, symbol, focusName, focusId, width, height, fo
|
|
|
217
218
|
</Box>;
|
|
218
219
|
}
|
|
219
220
|
|
|
221
|
+
function flowNodeName(node: PositionedNode): string {
|
|
222
|
+
if (node.more) return `Show ${node.more} more ${node.role ? ROLE_PLURAL[node.role].toLowerCase() : node.band}`;
|
|
223
|
+
const name = node.row ? counterpartyLabel(node.row) : node.label;
|
|
224
|
+
const role = node.role ? roleLabel(node.role) : node.band;
|
|
225
|
+
return `Open ${name}, ${role}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
220
228
|
function tooltipAnchor(node: PositionedNode, ribbons: readonly FlowRibbon[], width: number) {
|
|
221
229
|
const ribbon = ribbons.find((entry) => entry.id === node.id);
|
|
222
230
|
const x = ribbon ? (ribbon.x0 + ribbon.x1) / 2 : node.x + node.width / 2;
|
|
@@ -275,7 +283,8 @@ function FlowCard({ node, focusId, desktop, active, onPress, onHover }: {
|
|
|
275
283
|
backgroundColor={node.more ? undefined : active ? desktop ? blendHex(colors.bg, accent, 0.2) : colors.selected : blendHex(colors.bg, accent, desktop ? 0.09 : 0.12)}
|
|
276
284
|
style={desktop ? { borderLeft: `3px ${node.more ? "dotted" : "solid"} ${node.more ? blendHex(colors.bg, accent, 0.5) : accent}`, borderRadius: 3, paddingLeft: 6, cursor: "pointer",
|
|
277
285
|
boxShadow: active ? `inset 0 0 0 1px ${blendHex(colors.bg, accent, 0.6)}` : undefined } : undefined}
|
|
278
|
-
onMouseDown={onPress} onMouseOver={() => onHover(true)} onMouseOut={() => onHover(false)} data-gloom-role="supply-flow-node"
|
|
286
|
+
onMouseDown={onPress} onMouseOver={() => onHover(true)} onMouseOut={() => onHover(false)} data-gloom-role="supply-flow-node"
|
|
287
|
+
role="button" aria-label={flowNodeName(node)}>
|
|
279
288
|
{/* The ticker shows only beside the whole name. On the desktop that is CSS: the row wraps, one line shows, and a ticker that does not fit goes to the hidden line. */}
|
|
280
289
|
<Box flexDirection="row" height={1} overflow="hidden" style={desktop ? { flexWrap: "wrap" } : undefined}>
|
|
281
290
|
{!desktop ? <Text fg={node.more ? colors.textMuted : accent}>▍</Text> : null}
|
|
@@ -295,7 +304,8 @@ function FlowChip({ node, desktop, active, onPress, onHover }: { node: Positione
|
|
|
295
304
|
const background = active ? blendHex(colors.bg, accent, desktop ? 0.32 : 0.45) : node.more ? undefined : blendHex(colors.bg, accent, 0.14);
|
|
296
305
|
const text = <ClippedText fg={node.more ? colors.textDim : active ? colors.textBright : accent} text={flowChipLabel(node)} width={node.width - 2} />;
|
|
297
306
|
return <Box position="absolute" left={node.x} top={node.y} width={node.width} height={1} justifyContent={desktop ? "center" : undefined}
|
|
298
|
-
onMouseDown={onPress} onMouseOver={() => onHover(true)} onMouseOut={() => onHover(false)} data-gloom-role="supply-flow-chip"
|
|
307
|
+
onMouseDown={onPress} onMouseOver={() => onHover(true)} onMouseOut={() => onHover(false)} data-gloom-role="supply-flow-chip"
|
|
308
|
+
role="button" aria-label={flowNodeName(node)}>
|
|
299
309
|
{/* The desktop chip leaves a hairline of row above and below, like ticker chips. */}
|
|
300
310
|
{desktop ? <Box paddingX={1} minWidth={0} backgroundColor={background} justifyContent="center" style={{ height: "calc(100% - 4px)", borderRadius: 3, cursor: "pointer" }}>{text}</Box>
|
|
301
311
|
: <Box paddingX={1} height={1} backgroundColor={background}>{text}</Box>}
|
|
@@ -72,7 +72,7 @@ export const themesHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
72
72
|
const argument = Array.isArray(args.argument) ? args.argument.join(" ") : args.argument;
|
|
73
73
|
if (argument) {
|
|
74
74
|
const theme = matchTheme(board.themes, argument);
|
|
75
|
-
if (!theme) throw new Error(`No theme matches ${argument}
|
|
75
|
+
if (!theme) throw new Error(`No theme matches ${argument}. Themes: ${board.themes.map((row) => row.name).join(", ")}.`);
|
|
76
76
|
const data = await fetchThemeMembers(theme.id, context.apiClient);
|
|
77
77
|
const observation = boardObservation(data);
|
|
78
78
|
// Price and 1D are the regular session's; an extended print gets a column only when a member has one.
|