gloomberb 0.19.1 → 0.19.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 +2 -1
- package/src/api-client/errors.ts +38 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/capabilities/chart-series.ts +2 -1
- package/src/cli/commands/market.ts +9 -4
- package/src/cli/commands/overview.ts +5 -6
- package/src/cli/commands/search.ts +13 -13
- package/src/cli/context.ts +1 -0
- package/src/cli/glossary.ts +2 -1
- package/src/cli/history-rows.ts +35 -7
- package/src/cli/known-gaps.ts +11 -0
- package/src/cli/listing-arg.ts +17 -1
- package/src/cli/quote-answer.ts +29 -6
- package/src/components/command-bar/routes/root/runtime.ts +27 -5
- package/src/components/command-bar/selection-runtime.ts +12 -29
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +2 -2
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +33 -1
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +2 -1
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-wizard.tsx +6 -3
- package/src/components/ui/display.tsx +16 -5
- package/src/components/ui/icon.tsx +8 -2
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/plugin-cache.ts +15 -0
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/resource-store.ts +130 -58
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +313 -0
- package/src/data/sqlite/checkpointer.ts +198 -0
- package/src/data/sqlite/database.ts +47 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +4 -2
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/series.ts +4 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +19 -11
- package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
- package/src/plugins/builtin/econ/detail-view.tsx +16 -2
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/gpu/client.ts +4 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/market-movers/client.ts +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +10 -5
- package/src/plugins/builtin/market-movers/screener.ts +23 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/risk-factors/data.ts +8 -4
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/shared/cloud-resource.ts +13 -8
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/thirteenf/api.ts +11 -16
- package/src/plugins/builtin/thirteenf/data.ts +34 -6
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
- package/src/plugins/builtin/thirteenf/types.ts +2 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +6 -2
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/yield-curve/client.ts +4 -6
- package/src/plugins/installer.ts +50 -39
- package/src/renderers/dom/desktop/icons.tsx +3 -2
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +2 -0
- package/src/renderers/electrobun/launcher/preload.ts +13 -0
- package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
- package/src/renderers/electrobun/view/ui-host.tsx +9 -2
- package/src/series/resolve.ts +2 -1
- package/src/series/types.ts +5 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
- package/src/telemetry/stall-reports-node.ts +13 -2
- package/src/telemetry/stall-reports.ts +189 -13
- package/src/test-support/outage.ts +8 -0
- package/src/tickers/company-name-listing.ts +59 -29
- package/src/tickers/company-name.ts +65 -1
- package/src/tickers/search/home-listing.ts +93 -8
- package/src/tickers/search/index.ts +1 -1
- package/src/time-series/market.ts +5 -3
- package/src/time-series/price-comparison.ts +20 -2
- package/src/time-series/resolve.ts +20 -1
- package/src/time-series/types.ts +7 -0
- package/src/types/financials.ts +18 -3
- package/src/ui/host.tsx +6 -1
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +17 -5
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +47 -2
- package/src/utils/perf-marks.ts +23 -11
- package/src/utils/price-history-integrity.ts +47 -1
package/src/data/ticker-store.ts
CHANGED
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { createSqliteAccess, type SqliteAccess } from "./sqlite/access";
|
|
2
3
|
import { serializeJson } from "./sqlite/json";
|
|
3
|
-
import { withSqliteBusyRetry } from "./sqlite/retry";
|
|
4
4
|
|
|
5
5
|
export interface StoredTickerRecord {
|
|
6
6
|
symbol: string;
|
|
7
7
|
metadata: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/** A queued write's row (null metadata for a delete), one object per write. */
|
|
11
|
+
interface PendingTicker {
|
|
12
|
+
metadata: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export class TickerStore {
|
|
11
|
-
|
|
16
|
+
/** Writes queued behind another program's lock, by symbol, read back until they land. */
|
|
17
|
+
private readonly pending = new Map<string, PendingTicker>();
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly db: Database,
|
|
21
|
+
private readonly access: SqliteAccess = createSqliteAccess(db),
|
|
22
|
+
) {}
|
|
12
23
|
|
|
13
24
|
getAll(): StoredTickerRecord[] {
|
|
14
|
-
|
|
25
|
+
const rows = this.access.run("load tickers", () => (
|
|
15
26
|
this.db
|
|
16
27
|
.query<StoredTickerRecord, []>(
|
|
17
28
|
"SELECT symbol, metadata FROM tickers ORDER BY symbol",
|
|
18
29
|
)
|
|
19
30
|
.all()
|
|
20
31
|
));
|
|
32
|
+
if (this.pending.size === 0) return rows;
|
|
33
|
+
const bySymbol = new Map(rows.map((row) => [row.symbol, row]));
|
|
34
|
+
for (const [symbol, { metadata }] of this.pending) {
|
|
35
|
+
if (metadata == null) bySymbol.delete(symbol);
|
|
36
|
+
else bySymbol.set(symbol, { symbol, metadata });
|
|
37
|
+
}
|
|
38
|
+
return [...bySymbol.values()].sort((a, b) => (a.symbol < b.symbol ? -1 : a.symbol > b.symbol ? 1 : 0));
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
get(symbol: string): string | null {
|
|
24
|
-
const
|
|
42
|
+
const pending = this.pending.get(symbol);
|
|
43
|
+
if (pending) return pending.metadata;
|
|
44
|
+
const row = this.access.run("load ticker", () => (
|
|
25
45
|
this.db
|
|
26
46
|
.query<{ metadata: string }, [string]>(
|
|
27
47
|
"SELECT metadata FROM tickers WHERE symbol = ?",
|
|
@@ -32,34 +52,48 @@ export class TickerStore {
|
|
|
32
52
|
}
|
|
33
53
|
|
|
34
54
|
save(symbol: string, metadata: unknown): void {
|
|
35
|
-
|
|
36
|
-
this.db
|
|
37
|
-
.query(
|
|
38
|
-
`INSERT OR REPLACE INTO tickers (symbol, metadata, updated_at)
|
|
39
|
-
VALUES (?, ?, ?)`,
|
|
40
|
-
)
|
|
41
|
-
.run(symbol, serializeJson(metadata), Date.now());
|
|
42
|
-
});
|
|
55
|
+
this.saveMany([{ symbol, metadata }]);
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
/** Saves every record in one transaction: one write for a whole import, not one per ticker. */
|
|
46
59
|
saveMany(records: ReadonlyArray<{ symbol: string; metadata: unknown }>): void {
|
|
47
60
|
if (records.length === 0) return;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
const rows = records.map((record) => ({ symbol: record.symbol, metadata: serializeJson(record.metadata) }));
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
this.write(
|
|
64
|
+
rows.length === 1 ? "save ticker" : "save tickers",
|
|
65
|
+
rows.map((row) => [row.symbol, { metadata: row.metadata }]),
|
|
66
|
+
() => {
|
|
67
|
+
const statement = this.db.query(
|
|
68
|
+
`INSERT OR REPLACE INTO tickers (symbol, metadata, updated_at)
|
|
69
|
+
VALUES (?, ?, ?)`,
|
|
70
|
+
);
|
|
71
|
+
this.db.transaction(() => {
|
|
72
|
+
for (const row of rows) statement.run(row.symbol, row.metadata, now);
|
|
73
|
+
})();
|
|
74
|
+
},
|
|
75
|
+
);
|
|
58
76
|
}
|
|
59
77
|
|
|
60
78
|
delete(symbol: string): void {
|
|
61
|
-
|
|
79
|
+
this.write("delete ticker", [[symbol, { metadata: null }]], () => {
|
|
62
80
|
this.db.query("DELETE FROM tickers WHERE symbol = ?").run(symbol);
|
|
63
81
|
});
|
|
64
82
|
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Runs `statement` through the write queue, reading `entries` back until it
|
|
86
|
+
* lands. A write of one row replaces a queued write of the same row.
|
|
87
|
+
*/
|
|
88
|
+
private write(operationName: string, entries: Array<[string, PendingTicker]>, statement: () => void): void {
|
|
89
|
+
for (const [symbol, entry] of entries) this.pending.set(symbol, entry);
|
|
90
|
+
this.access.write(operationName, statement, {
|
|
91
|
+
...(entries.length === 1 ? { key: `tickers\u0000${entries[0]![0]}` } : {}),
|
|
92
|
+
settled: () => {
|
|
93
|
+
for (const [symbol, entry] of entries) {
|
|
94
|
+
if (this.pending.get(symbol) === entry) this.pending.delete(symbol);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
65
99
|
}
|
|
@@ -3,6 +3,7 @@ import { barsPerYear } from "./bars-per-year";
|
|
|
3
3
|
import {
|
|
4
4
|
mergePriceHistoryIntegrity,
|
|
5
5
|
pricePointIntegrity,
|
|
6
|
+
pricePointWicks,
|
|
6
7
|
type PriceHistoryIntegrity,
|
|
7
8
|
} from "../utils/price-history-integrity";
|
|
8
9
|
|
|
@@ -115,8 +116,9 @@ function observations(points: readonly PricePoint[]): Observation[] | null {
|
|
|
115
116
|
time,
|
|
116
117
|
close: positiveLog(point.close),
|
|
117
118
|
open: positiveLog(point.open),
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
// A wick too far from the bar's body to have traded is missing, not a range.
|
|
120
|
+
high: positiveLog(pricePointWicks(point).high),
|
|
121
|
+
low: positiveLog(pricePointWicks(point).low),
|
|
120
122
|
integrity: pricePointIntegrity(point),
|
|
121
123
|
}));
|
|
122
124
|
}
|
|
@@ -37,24 +37,28 @@ function readJson(path: string): Record<string, unknown> | null {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
async function readCheckouts(): Promise<PluginCheckout[]> {
|
|
40
|
-
const { installedPluginDirectories,
|
|
40
|
+
const { installedPluginDirectories, readPluginRemoteAsync } = await installer();
|
|
41
41
|
const pluginsDir = getPluginsDir();
|
|
42
|
-
|
|
42
|
+
const checkouts: PluginCheckout[] = [];
|
|
43
|
+
// One git at a time: starting a process holds the thread for a moment, and
|
|
44
|
+
// starting them all at once adds those moments up into one pause.
|
|
45
|
+
for (const directory of installedPluginDirectories()) {
|
|
43
46
|
const dir = join(pluginsDir, directory);
|
|
44
47
|
const pkg = readJson(join(dir, "package.json"));
|
|
45
48
|
const dependencies = pkg?.dependencies;
|
|
46
|
-
|
|
49
|
+
checkouts.push({
|
|
47
50
|
directory,
|
|
48
51
|
id: readPluginManifest(dir).id,
|
|
49
|
-
repo: existsSync(join(dir, ".git")) ?
|
|
52
|
+
repo: existsSync(join(dir, ".git")) ? await readPluginRemoteAsync(dir) : null,
|
|
50
53
|
commit: readPluginCommit(dir),
|
|
51
54
|
version: typeof pkg?.version === "string" ? pkg.version : null,
|
|
52
55
|
linked: lstatSync(dir, { throwIfNoEntry: false })?.isSymbolicLink() === true,
|
|
53
56
|
needsGloomberb: checkPluginCompatibility(dir)?.needsGloomberb ?? null,
|
|
54
57
|
peers: installedPeerPlugins(dir, pluginsDir).map((peer) => peer.directory),
|
|
55
58
|
hasDependencies: !!dependencies && typeof dependencies === "object" && Object.keys(dependencies).length > 0,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return checkouts;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
function createNodeAutoUpdateDeps(manager: PluginManager): PluginAutoUpdateDeps {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SeriesDefinition } from "../../../series/types";
|
|
2
2
|
import { CENTRAL_BANKS, centralBankNames } from "../../../utils/central-banks";
|
|
3
|
-
import {
|
|
3
|
+
import { cachedSeriesNote } from "../shared/series-warning";
|
|
4
4
|
import { loadCentralBankRates } from "./client";
|
|
5
5
|
import { policySeriesAttribution } from "./model";
|
|
6
6
|
|
|
@@ -24,12 +24,11 @@ export const CENTRAL_BANK_SERIES: readonly SeriesDefinition[] = CENTRAL_BANKS.ma
|
|
|
24
24
|
const resource = await loadCentralBankRates();
|
|
25
25
|
const row = resource.payload.rows.find((entry) => entry.id === bank.id);
|
|
26
26
|
if (!row) throw new Error(`${bank.place} is not on the board right now.`);
|
|
27
|
-
const warning = cachedSeriesWarning(resource);
|
|
28
27
|
// Cited as CBR cites the row: the BIS or FRED series and the bank that sets the rate.
|
|
29
28
|
const attribution = policySeriesAttribution(row);
|
|
30
29
|
return {
|
|
31
30
|
points: row.history.map((point) => ({ date: point.date, value: point.value })),
|
|
32
|
-
...(
|
|
31
|
+
...cachedSeriesNote(resource),
|
|
33
32
|
...(attribution ? { attribution } : {}),
|
|
34
33
|
};
|
|
35
34
|
},
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../../../time-series/spec";
|
|
12
12
|
import type { ChartSeriesSpec, ChartSpec, SeriesPeriod } from "../../../time-series/types";
|
|
13
13
|
import { yearToDateBaseDay } from "../../../market-data/market/year-to-date";
|
|
14
|
+
import { readPriceWindow } from "../../../utils/price-window";
|
|
14
15
|
|
|
15
16
|
const PRICE_CAPABILITIES = new Set(["price-chart", "intraday-price-chart", "price-comparison"]);
|
|
16
17
|
|
|
@@ -35,6 +36,13 @@ export function yearToDateWindow(spec: ChartSpec, now = Date.now()): { start: st
|
|
|
35
36
|
return { start: starts.sort()[0] ?? `${Number(today.slice(0, 4)) - 1}-12-31`, end: today };
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/** The shortest range whose history reaches back to `from`: 5Y for a window from 2022, ALL for one from 2019. */
|
|
40
|
+
function rangeCovering(from: string, now = Date.now()): TimeRange {
|
|
41
|
+
const days = (now - Date.parse(`${from}T00:00:00Z`)) / 86_400_000;
|
|
42
|
+
const covering: Array<[TimeRange, number]> = [["1M", 31], ["3M", 92], ["6M", 183], ["1Y", 366], ["5Y", 5 * 366]];
|
|
43
|
+
return covering.find(([, span]) => days <= span)?.[0] ?? "ALL";
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
function chartResolution(options: HeadlessPaneOptionValues): ChartResolution | null {
|
|
39
47
|
return parseChartResolution(options.chartResolution ?? options.resolution);
|
|
40
48
|
}
|
|
@@ -77,6 +85,14 @@ export function applyChartComposerCapabilityOptions(
|
|
|
77
85
|
};
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
// --from and --to: those dates, each leg rebased at its first close in them, as YTD is from the prior year's last.
|
|
89
|
+
const window = capabilityId === "price-comparison"
|
|
90
|
+
? readPriceWindow(typeof options.from === "string" ? options.from : undefined, typeof options.to === "string" ? options.to : undefined)
|
|
91
|
+
: null;
|
|
92
|
+
if (window) {
|
|
93
|
+
next = { ...next, viewport: { ...next.viewport, range: rangeCovering(window.from), dateWindow: { start: window.from, end: window.to } } };
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
if (capabilityId === "price-chart" && options.extendedHours === true) {
|
|
81
97
|
next = { ...next, viewport: { ...next.viewport, extendedHours: true } };
|
|
82
98
|
}
|
|
@@ -3,6 +3,7 @@ import { isSubMinuteInterval, subMinuteDataMessage } from "../../../cli/known-ga
|
|
|
3
3
|
import { CHART_BAR_SIZE_PRESETS, parseChartBarSize } from "../../../time-series/resolution";
|
|
4
4
|
import { FINANCIAL_PERIOD_OPTION, FINANCIAL_PERIOD_COUNT_OPTION, HISTORY_RANGE_VALUES, RETURN_RANGE_VALUES } from "../shared/headless-options";
|
|
5
5
|
import { DATE_OPTION_EXAMPLES } from "../../../utils/calendar-date";
|
|
6
|
+
import { PRICE_WINDOW_FROM_HELP, PRICE_WINDOW_TO_HELP } from "../../../utils/price-window";
|
|
6
7
|
|
|
7
8
|
type PaneSchema = Pick<HeadlessPaneDefinition, "argument" | "options" | "discovery">;
|
|
8
9
|
|
|
@@ -138,6 +139,10 @@ export const paneSchemas = {
|
|
|
138
139
|
],
|
|
139
140
|
defaultValue: "percent",
|
|
140
141
|
},
|
|
142
|
+
// A window of dates, each leg rebased at its first close in it: --from 2019-12-31 compares since then.
|
|
143
|
+
{ key: "from", type: "string", placeholder: "year|date", description: `${PRICE_WINDOW_FROM_HELP}; the returns are measured from its first shared close`,
|
|
144
|
+
example: "--from 2019-12-31", replaces: ["rangePreset"] },
|
|
145
|
+
{ key: "to", type: "string", placeholder: "year|date", description: PRICE_WINDOW_TO_HELP },
|
|
141
146
|
{
|
|
142
147
|
key: "totalReturn",
|
|
143
148
|
description: "Reinvest each cash distribution at its ex-date close, so the returns are total returns, not price returns.",
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type MultiSelectRowAction,
|
|
21
21
|
} from "../../../components/ui";
|
|
22
22
|
import { CompositeChart } from "../../../components/chart/composite";
|
|
23
|
+
import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
23
24
|
import { useChartViewReset, useChartViewResetListener } from "../../../components/chart/core/view-reset";
|
|
24
25
|
import { formatCompositeDatasetDate } from "../../../components/chart/composite/format";
|
|
25
26
|
import type { CompositeChartLevels, CompositeLevelEdit } from "../../../components/chart/composite/levels";
|
|
@@ -1116,9 +1117,12 @@ function ChartComposerSurface({
|
|
|
1116
1117
|
}
|
|
1117
1118
|
return newest > end ? formatCompositeDatasetDate(new Date(newest), end) : null;
|
|
1118
1119
|
}, [activeRuntimeViewport, plottedSeries, spec, viewport]);
|
|
1120
|
+
// A series a failed refresh left on a saved copy says when that copy was saved, as every pane does.
|
|
1121
|
+
const savedAt = resolution.savedAt;
|
|
1119
1122
|
usePaneFooter(footerId, () => ({
|
|
1120
1123
|
info: [
|
|
1121
1124
|
...(delistingKey ? [{ id: "delisted", parts: [{ text: delistingKey, tone: "warning" as const }] }] : []),
|
|
1125
|
+
...savedCopyFooterInfo([savedAt]),
|
|
1122
1126
|
...(loading
|
|
1123
1127
|
? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }]
|
|
1124
1128
|
: pickingAnchor
|
|
@@ -1136,7 +1140,7 @@ function ChartComposerSurface({
|
|
|
1136
1140
|
{ id: "resolution", key: "b", label: "ars", title: "Bar Size", onPress: footerResolution },
|
|
1137
1141
|
...(anchorable ? [{ id: "vwap-anchor", key: "v", label: "wap anchor", title: "Anchor VWAP", onPress: footerAnchor }] : []),
|
|
1138
1142
|
],
|
|
1139
|
-
}), [delistingKey, loading, pickingAnchor, newestBeyondView, footerSeries, openIndicators, indicatorsDisabled, openFormulas, formulasDisabled, footerResolution, anchorable, footerAnchor, trade]);
|
|
1143
|
+
}), [delistingKey, savedAt, loading, pickingAnchor, newestBeyondView, footerSeries, openIndicators, indicatorsDisabled, openFormulas, formulasDisabled, footerResolution, anchorable, footerAnchor, trade]);
|
|
1140
1144
|
|
|
1141
1145
|
// A fixed window (a GIP session) highlights no range, so the bar names it.
|
|
1142
1146
|
const dateWindowLabel = useMemo(() => {
|
|
@@ -4,6 +4,7 @@ import { cleanFloat32Price } from "../../../cli/history-rows";
|
|
|
4
4
|
import { exportNumber } from "../../../cli/report-tables";
|
|
5
5
|
import { publicTickerKey } from "../../../utils/exchanges";
|
|
6
6
|
import { formatUtcTime } from "../../../utils/utc-time";
|
|
7
|
+
import { pricePointWicks } from "../../../utils/price-history-integrity";
|
|
7
8
|
|
|
8
9
|
interface SessionExtreme {
|
|
9
10
|
price: number;
|
|
@@ -26,8 +27,10 @@ function sessionRange(points: readonly PricePoint[]): SessionRange | null {
|
|
|
26
27
|
for (const point of points) {
|
|
27
28
|
const at = point.date;
|
|
28
29
|
if (!(at instanceof Date) || !Number.isFinite(at.getTime())) continue;
|
|
29
|
-
|
|
30
|
-
const
|
|
30
|
+
// A wick too far from the bar's body to have traded is not a price the bars traded.
|
|
31
|
+
const wicks = pricePointWicks(point);
|
|
32
|
+
const pointLow = finitePositive(wicks.low) ?? finitePositive(point.close);
|
|
33
|
+
const pointHigh = finitePositive(wicks.high) ?? finitePositive(point.close);
|
|
31
34
|
if (pointLow != null && (!low || pointLow < low.price)) low = { price: pointLow, at };
|
|
32
35
|
if (pointHigh != null && (!high || pointHigh > high.price)) high = { price: pointHigh, at };
|
|
33
36
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
|
+
import { sourceDidNotAnswer } from "../../../api-client/errors";
|
|
2
3
|
import type { PsdBoardPayload } from "../../../api-client/commodity-balances";
|
|
3
4
|
import { unavailableOnServer } from "../shared/cloud-resource";
|
|
4
5
|
import type { BalanceRow, PriceForecast } from "./model";
|
|
@@ -37,7 +38,9 @@ function readBoard(payload: PsdBoardPayload): CommodityBalances {
|
|
|
37
38
|
|| (payload.marketYear != null && !text(payload.marketYear))) {
|
|
38
39
|
throw new Error("The server returned an invalid crop balance board");
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
// The server's source did not answer this time, which may pass; a board with no rows has none yet.
|
|
42
|
+
if (payload.status === "unavailable") throw sourceDidNotAnswer(CROPS_NOT_AVAILABLE);
|
|
43
|
+
if (payload.rows.length === 0) throw new Error(CROPS_NOT_AVAILABLE);
|
|
41
44
|
return {
|
|
42
45
|
rows: payload.rows,
|
|
43
46
|
prices: payload.prices,
|
|
@@ -3,7 +3,10 @@ import type { CotBoardPayload, CotClass, CotClassSummary, CotContractPayload, Co
|
|
|
3
3
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
4
4
|
import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
5
5
|
import type { PricePoint } from "../../../types/financials";
|
|
6
|
-
import { COT_CLASSES, cotPriceMapping } from "./model";
|
|
6
|
+
import { COT_CLASSES, cotFrontPriceUnit, cotPriceMapping } from "./model";
|
|
7
|
+
import { futuresPriceUnit, loadFuturesContractTerms, loadFuturesPriceUnit } from "../../../market-data/futures-contract";
|
|
8
|
+
import { futuresBoardContract } from "../../../market-data/futures-board";
|
|
9
|
+
import { settleWithin } from "../../../utils/async-deadline";
|
|
7
10
|
|
|
8
11
|
export const cotBoardCache = createPluginCache<CotBoardPayload>({ kind: "cot-board", source: "gloom-cloud", schemaVersion: 1,
|
|
9
12
|
policy: { staleMs: 15 * 60_000, expireMs: 8 * 24 * 60 * 60_000 } });
|
|
@@ -88,7 +91,12 @@ export async function fetchCotContract(contractCode: string, family: CotFamily,
|
|
|
88
91
|
catch (error) { throw unavailable(error); }
|
|
89
92
|
}
|
|
90
93
|
export interface CotDetailData { payload: CotContractPayload; price: PricePoint[]; priceSymbol: string | null; priceAsOf: string | null; priceWarning: string | null }
|
|
91
|
-
|
|
94
|
+
/**
|
|
95
|
+
* A market's report and its front price. The price's unit is not waited for
|
|
96
|
+
* here: it joins the legend when it is known (loadCotPriceUnit).
|
|
97
|
+
*/
|
|
98
|
+
export async function loadCotDetail(contractCode: string, family: CotFamily,
|
|
99
|
+
client: Pick<typeof apiClient, "getCloudCotContract" | "getCloudHistory"> = apiClient,
|
|
92
100
|
positions: CotPositions = "futures-only"): Promise<CotDetailData> {
|
|
93
101
|
const payload = await fetchCotContract(contractCode, family, client, positions);
|
|
94
102
|
const mapping = cotPriceMapping(contractCode);
|
|
@@ -109,3 +117,21 @@ export async function loadCotDetail(contractCode: string, family: CotFamily, cli
|
|
|
109
117
|
priceWarning: result.stale ? "Front-price history is stale." : price.length ? null : "Front-price history unavailable." };
|
|
110
118
|
} catch { return { payload, price: [], priceSymbol: mapping.priceSymbol, priceAsOf: null, priceWarning: "Front-price history unavailable. Positioning remains available." }; }
|
|
111
119
|
}
|
|
120
|
+
|
|
121
|
+
/** How long the front price's legend waits for the contract terms before it writes the catalog's unit. */
|
|
122
|
+
const COT_PRICE_UNIT_WAIT_MS = 1_500;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The unit a market's front price is written in, as its legend writes it
|
|
126
|
+
* after the price (cotFrontPriceUnit): `USD/gal` for heating oil. It is the
|
|
127
|
+
* contract terms' unit, else the futures catalog's where the terms fail or
|
|
128
|
+
* have not answered within `waitMs`; null for a market with no front price.
|
|
129
|
+
* The chart draws without it, so a slow answer never holds the detail up.
|
|
130
|
+
*/
|
|
131
|
+
export async function loadCotPriceUnit(contractCode: string, client: Pick<typeof apiClient, "getCloudFuturesContract"> = apiClient,
|
|
132
|
+
waitMs = COT_PRICE_UNIT_WAIT_MS): Promise<string | null> {
|
|
133
|
+
const symbol = cotPriceMapping(contractCode)?.priceSymbol;
|
|
134
|
+
if (!symbol) return null;
|
|
135
|
+
const terms = await settleWithin(loadFuturesPriceUnit(symbol, (key) => loadFuturesContractTerms(key, client)), waitMs);
|
|
136
|
+
return cotFrontPriceUnit(contractCode, terms ?? futuresPriceUnit(futuresBoardContract(symbol), null));
|
|
137
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CotClass, CotClassSummary, CotContractPayload, CotHistoryPoint } from "../../../api-client/cot";
|
|
2
2
|
import type { HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/plugin";
|
|
3
3
|
import { fetchCotBoard, fetchCotContract } from "./client";
|
|
4
|
-
import { COT_CLASSES, COT_FAMILIES, COT_MAJOR_CODES, COT_POSITIONS, COT_SCOPES, cotClass, cotContractCode, cotContractUnitText, cotFamily, cotInteger, cotMarketName, cotPositions, cotPositionsText, cotScope, cotUnknownMarketMessage } from "./model";
|
|
4
|
+
import { COT_CLASSES, COT_FAMILIES, COT_MAJOR_CODES, COT_POSITIONS, COT_SCOPES, cotClass, cotContractCode, cotContractUnitText, cotFamily, cotInteger, cotMarketName, cotPositions, cotPositionsText, cotProxyFor, cotProxyNote, cotScope, cotUnknownMarketMessage } from "./model";
|
|
5
5
|
import { COMMODITIES_VIEW_STATE } from "../commodities-energy/view-state";
|
|
6
6
|
import { REPORT_ROWS } from "../shared/headless-options";
|
|
7
7
|
|
|
@@ -67,6 +67,7 @@ export const cotHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
67
67
|
if (args.argument) {
|
|
68
68
|
const code = cotContractCode(args.argument);
|
|
69
69
|
if (!code) throw new Error(cotUnknownMarketMessage(args.argument));
|
|
70
|
+
const proxy = cotProxyFor(args.argument);
|
|
70
71
|
const data = await fetchCotContract(code, family, ctx.apiClient, positions);
|
|
71
72
|
const classes = data.positions.map((row) => row.id);
|
|
72
73
|
const limit = args.options.limit == null ? null : Number(args.options.limit);
|
|
@@ -80,6 +81,8 @@ export const cotHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
80
81
|
...classes.map((id) => ({ key: `${camelClass(id)}Net`, header: `${data.positions.find((row) => row.id === id)?.label ?? id} net`, align: "right" as const, format: signed }))],
|
|
81
82
|
rows: weeks.map((row) => ({ reportDate: row.reportDate, openInterest: row.openInterest,
|
|
82
83
|
...Object.fromEntries(row.positions.map((position) => [`${camelClass(position.id)}Net`, position.net])) })) }],
|
|
84
|
+
// "jet fuel" reads heating oil's positions, and says so.
|
|
85
|
+
...(proxy ? { notes: [cotProxyNote(proxy)] } : {}),
|
|
83
86
|
errors: data.gaps, metadata: { ...data, complete: data.status === "available" } };
|
|
84
87
|
}
|
|
85
88
|
const traderClass = cotClass(family, args.options.traderClass);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
2
|
import { cotBoardCache } from "./client";
|
|
3
3
|
import { cotHeadless } from "./headless";
|
|
4
|
-
import { COT_ROOT_OPTIONS, cotContractCode, cotContractCodeForTicker, cotRoot, cotUnknownMarketMessage } from "./model";
|
|
4
|
+
import { COT_ROOT_OPTIONS, cotContractCode, cotContractCodeForTicker, cotProxyFor, cotRoot, cotUnknownMarketMessage } from "./model";
|
|
5
5
|
import { CotPane } from "./pane";
|
|
6
6
|
import { COT_SERIES } from "./series";
|
|
7
7
|
|
|
@@ -19,7 +19,9 @@ export const cotModule: PluginModule = {
|
|
|
19
19
|
const input = options?.arg?.trim();
|
|
20
20
|
const code = input ? cotContractCode(input) : cotContractCodeForTicker(context.activeTicker);
|
|
21
21
|
if (input && !code) throw new Error(cotUnknownMarketMessage(input));
|
|
22
|
-
|
|
22
|
+
// "jet fuel" opens heating oil, and the title says which market stands in.
|
|
23
|
+
const proxy = input ? cotProxyFor(input) : null;
|
|
24
|
+
return { title: code ? `COT ${cotRoot(code) ?? code}${proxy ? ` (${proxy.subject} proxy)` : ""}` : "CFTC Positioning", params: code ? { code } : undefined,
|
|
23
25
|
settings: { report: options?.values?.report ?? "legacy" }, placement: "floating" as const };
|
|
24
26
|
},
|
|
25
27
|
}],
|
|
@@ -2,10 +2,14 @@ import type { CotClass, CotContractPayload, CotFamily, CotHistoryPoint, CotPosit
|
|
|
2
2
|
import { staticSeries } from "../../../components/chart/static/series";
|
|
3
3
|
import type { ResolvedSeries } from "../../../time-series/types";
|
|
4
4
|
import type { PricePoint } from "../../../types/financials";
|
|
5
|
-
import { formatPriceObservation } from "../../../market-data/market/format";
|
|
5
|
+
import { formatMarketPriceWithCurrency, formatPriceObservation } from "../../../market-data/market/format";
|
|
6
|
+
import { futuresPricePer } from "../../../market-data/futures-contract";
|
|
7
|
+
import { unitCurrencyToken } from "../../../time-series/price-units";
|
|
6
8
|
import { cleanFloat32Price } from "../../../cli/history-rows";
|
|
7
9
|
import { cryptoPairCoin, isFuturesSymbol } from "../shared/crypto-pair";
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
bestNameMatch, FUTURES_CONTRACTS, futuresBoardContract, futuresBoardContractForTicker, futuresContractByCode, futuresContractByName, type FuturesContract,
|
|
12
|
+
} from "../../../market-data/futures-board";
|
|
9
13
|
|
|
10
14
|
export const COT_CLASSES: Record<CotFamily, Array<{ value: CotClass; label: string }>> = {
|
|
11
15
|
legacy: [{ value: "noncommercial", label: "Noncommercial" }, { value: "commercial", label: "Commercial" }, { value: "nonreportable", label: "Nonreportable" }],
|
|
@@ -162,7 +166,35 @@ export function cotContractCode(value: unknown): string | null {
|
|
|
162
166
|
if (alias) return alias;
|
|
163
167
|
const market = typedMarket(value);
|
|
164
168
|
if (market && "root" in market) return ROOTS[market.root]!.code;
|
|
165
|
-
|
|
169
|
+
if (market) return boardMarketCode(market.board);
|
|
170
|
+
const proxy = proxyMarket(value);
|
|
171
|
+
const proxied = proxy ? futuresContractByCode(proxy.root) : null;
|
|
172
|
+
return (proxied ? boardMarketCode(proxied) : null) ?? marketCode(input);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Markets the CFTC does not report, opened as the one traders price and hedge
|
|
176
|
+
* them by: jet fuel (kerosene) as NY Harbor ULSD, heating oil (HO), which
|
|
177
|
+
* airlines' fuel hedges and analysts follow it with. Opened this way, the
|
|
178
|
+
* pane's title and the report say which market stands in.
|
|
179
|
+
*/
|
|
180
|
+
const PROXY_MARKETS: ReadonlyArray<{ names: readonly string[]; root: string; subject: string }> = [
|
|
181
|
+
{ names: ["jet fuel", "jet fuels", "kerosene", "kerosine", "jet kerosene", "aviation fuel", "avtur"], root: "HO", subject: "jet fuel" },
|
|
182
|
+
];
|
|
183
|
+
function proxyMarket(value: string) {
|
|
184
|
+
const typed = value.trim().toLowerCase().replace(/[\s_-]+/g, " ");
|
|
185
|
+
return PROXY_MARKETS.find((proxy) => proxy.names.includes(typed)) ?? null;
|
|
186
|
+
}
|
|
187
|
+
/** What a typed name opens in place of a market the CFTC does not report ("jet fuel" opens HO), or null. */
|
|
188
|
+
export function cotProxyFor(value: unknown): { subject: string; root: string } | null {
|
|
189
|
+
if (typeof value !== "string" || typedMarket(value) || rootAlias(value.trim().toUpperCase())) return null;
|
|
190
|
+
const proxy = proxyMarket(value);
|
|
191
|
+
return proxy ? { subject: proxy.subject, root: proxy.root } : null;
|
|
192
|
+
}
|
|
193
|
+
/** The line a report opened through a proxy says: "Jet fuel has no CFTC market: heating oil (NY Harbor ULSD, HO) is the usual proxy." */
|
|
194
|
+
export function cotProxyNote(proxy: { subject: string; root: string }): string {
|
|
195
|
+
const subject = `${proxy.subject[0]!.toUpperCase()}${proxy.subject.slice(1)}`;
|
|
196
|
+
return `${subject} has no CFTC market: heating oil (NY Harbor ULSD, ${proxy.root}) is the usual proxy. Its price: gloom quote ${proxy.root}=F; `
|
|
197
|
+
+ "US jet fuel stocks (EIA, weekly): gloom fn G CAP:doe:jet";
|
|
166
198
|
}
|
|
167
199
|
/** The markets the unknown-market hint names, by the code and the word a newcomer knows them by. */
|
|
168
200
|
const SUGGESTED_MARKETS: ReadonlyArray<readonly [string, string]> = [
|
|
@@ -234,10 +266,27 @@ export function cotInteger(value: number | null, signed = false): string {
|
|
|
234
266
|
return value == null ? "--" : `${signed && value > 0 ? "+" : ""}${value.toLocaleString("en-US", { maximumFractionDigits: 0 })}`;
|
|
235
267
|
}
|
|
236
268
|
|
|
269
|
+
/**
|
|
270
|
+
* The front price's unit as a quote of the future writes it after the price:
|
|
271
|
+
* `USD/gal` for heating oil ($4.6218/gal), `USD` for gold, from the
|
|
272
|
+
* contract's unit ("USD / gallon"). Null where the unit names no currency
|
|
273
|
+
* (index points, Treasury points) or is unknown: the price reads bare.
|
|
274
|
+
*/
|
|
275
|
+
export function cotFrontPriceUnit(code: string, contractUnit: string | null | undefined): string | null {
|
|
276
|
+
const currency = contractUnit ? unitCurrencyToken(contractUnit.split("/")[0] ?? "") : null;
|
|
277
|
+
if (!currency) return null;
|
|
278
|
+
const root = cotRoot(code) ?? undefined;
|
|
279
|
+
return `${currency}${futuresPricePer({ root, spec: { unit: contractUnit } } as Parameters<typeof futuresPricePer>[0], currency)}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
237
282
|
/** Legend values stay exact: a compact 7.8K hides the futures price and -100K the net position. */
|
|
238
|
-
export function cotLegendValue(value: number, series: Pick<ResolvedSeries, "id">): string {
|
|
283
|
+
export function cotLegendValue(value: number, series: Pick<ResolvedSeries, "id"> & Partial<Pick<ResolvedSeries, "unit">>): string {
|
|
284
|
+
if (series.id !== "price") return cotInteger(value, true);
|
|
239
285
|
// Drop the float32 tail of provider bars without rounding away a 1/128 Treasury tick.
|
|
240
|
-
|
|
286
|
+
const price = cleanFloat32Price(value);
|
|
287
|
+
// In its currency and per its quantity, as a quote of the future reads: $4.6218/gal.
|
|
288
|
+
const [currency, per] = [series.unit?.split("/")[0]?.trim(), series.unit?.includes("/") ? `/${series.unit.split("/")[1]}` : ""];
|
|
289
|
+
return currency ? `${formatMarketPriceWithCurrency(price, currency, { assetCategory: "FUT" })}${per}` : formatPriceObservation(price);
|
|
241
290
|
}
|
|
242
291
|
|
|
243
292
|
/**
|
|
@@ -263,7 +312,9 @@ export function cotNetPoints(history: readonly CotHistoryPoint[], traderClass: C
|
|
|
263
312
|
}
|
|
264
313
|
return points;
|
|
265
314
|
}
|
|
266
|
-
|
|
315
|
+
/** `priceUnit` is the front price's (cotFrontPriceUnit), which its legend value is written in. */
|
|
316
|
+
export function cotChartSeries(data: CotContractPayload, traderClass: CotClass, price: readonly PricePoint[], colors: { positive: string; warning: string },
|
|
317
|
+
priceUnit: string | null = null): ResolvedSeries[] {
|
|
267
318
|
const series: ResolvedSeries[] = [{
|
|
268
319
|
...staticSeries(cotNetPoints(data.history, traderClass), { id: "net", label: `${COT_CLASSES[data.reportFamily].find((row) => row.value === traderClass)?.label ?? traderClass} net`, color: colors.warning, calendarSpaced: true }),
|
|
269
320
|
panelId: "net", unit: "contracts", unitGroup: "positions",
|
|
@@ -271,7 +322,7 @@ export function cotChartSeries(data: CotContractPayload, traderClass: CotClass,
|
|
|
271
322
|
// Net positioning is the subject, so it leads the legend and the strip; the front price follows.
|
|
272
323
|
if (price.length) series.push({
|
|
273
324
|
...staticSeries(price.map((row) => ({ date: row.date, observedAt: row.date, value: row.close })), { id: "price", label: "Front price", color: colors.positive, calendarSpaced: true }),
|
|
274
|
-
panelId: "price", unit: "", unitGroup: "price",
|
|
325
|
+
panelId: "price", unit: priceUnit ?? "", unitGroup: "price",
|
|
275
326
|
});
|
|
276
327
|
return series;
|
|
277
328
|
}
|
|
@@ -12,7 +12,7 @@ import { Box } from "../../../ui";
|
|
|
12
12
|
import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
13
13
|
import { compareSortValues, nextHeaderSort, type SortPreference } from "../../../utils/sort-values";
|
|
14
14
|
import { useResearchCloudSession } from "../shared/research-cloud-session";
|
|
15
|
-
import { cachedCotBoard, loadCotBoard, loadCotDetail } from "./client";
|
|
15
|
+
import { cachedCotBoard, loadCotBoard, loadCotDetail, loadCotPriceUnit } from "./client";
|
|
16
16
|
import { COT_CLASSES, COT_FAMILIES, COT_MAJOR_CODES, COT_POSITIONS, COT_SCOPES, cotChartSeries, cotClass, cotContractUnitText, cotFamily, cotInteger, cotLegendValue, cotMarketName, cotPositions, cotPositionsText, cotScope, type CotScope } from "./model";
|
|
17
17
|
|
|
18
18
|
const BOARD_COLUMNS: DataTableColumn[] = [
|
|
@@ -159,10 +159,14 @@ function CotDetail({ width, height, focused, code, family, positions, traderClas
|
|
|
159
159
|
const session = useResearchCloudSession();
|
|
160
160
|
const loader = useCallback(() => loadCotDetail(code, family, undefined, positions), [code, family, positions, session.requestKey]);
|
|
161
161
|
const resource = useAsyncResource(loader, { clearOnError: isAccessDenied });
|
|
162
|
+
// The front price's unit joins the legend once the contract terms answer, or
|
|
163
|
+
// the catalog's after a short wait: the chart draws without waiting for it.
|
|
164
|
+
const unitLoader = useCallback(() => loadCotPriceUnit(code), [code]);
|
|
165
|
+
const priceUnit = useAsyncResource(unitLoader).data ?? null;
|
|
162
166
|
const data = resource.data;
|
|
163
167
|
const payload = data?.payload;
|
|
164
168
|
const current = payload?.positions.find((row) => row.id === traderClass);
|
|
165
|
-
const series = useMemo(() => data ? cotChartSeries(data.payload, traderClass, data.price, colors) : [], [data, traderClass, colors]);
|
|
169
|
+
const series = useMemo(() => data ? cotChartSeries(data.payload, traderClass, data.price, colors, priceUnit) : [], [data, traderClass, colors, priceUnit]);
|
|
166
170
|
// The selected class row already carries the net and both ranks, and the
|
|
167
171
|
// legend names the class it charts, so the detail opens on the chart. A rank
|
|
168
172
|
// over a short history is a limitation, so it sits behind the warning.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
|
+
import { sourceDidNotAnswer } from "../../../api-client/errors";
|
|
2
3
|
import type { CpiBoardPayload, CpiRow } from "../../../api-client/cpi";
|
|
3
|
-
import { ApiRequestError } from "../../../api-client/errors";
|
|
4
4
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
5
|
-
import { cachedCloudResource, loadCloudResource } from "../shared/cloud-resource";
|
|
5
|
+
import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
6
6
|
|
|
7
7
|
/** What the pane says while the server has no months to serve: before the backend ships, or before its first read. */
|
|
8
8
|
export const CPI_NOT_AVAILABLE = "US consumer prices are not available yet.";
|
|
@@ -54,11 +54,12 @@ export async function fetchCpiBoard(client: Pick<typeof apiClient, "getCloudCpiB
|
|
|
54
54
|
try {
|
|
55
55
|
payload = await client.getCloudCpiBoard();
|
|
56
56
|
} catch (error) {
|
|
57
|
-
|
|
58
|
-
throw error;
|
|
57
|
+
throw unavailableOnServer(error, CPI_NOT_AVAILABLE, [404, 503]);
|
|
59
58
|
}
|
|
60
59
|
const board = validateCpiBoard(payload);
|
|
61
|
-
|
|
60
|
+
// The server's source did not answer this time: a saved copy answers, as it does a 5xx.
|
|
61
|
+
if (board.status === "unavailable") throw sourceDidNotAnswer(CPI_NOT_AVAILABLE);
|
|
62
|
+
if (!board.release.period || !board.rows.length) throw new Error(CPI_NOT_AVAILABLE);
|
|
62
63
|
return board;
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CreditBoardPoint } from "../../../api-client/credit-boards";
|
|
2
2
|
import type { SeriesDefinition, SeriesLoadResult } from "../../../series/types";
|
|
3
|
-
import {
|
|
3
|
+
import { cachedSeriesNote } from "../shared/series-warning";
|
|
4
4
|
import { loadCdxBoard, loadSovrBoard } from "./client";
|
|
5
5
|
|
|
6
6
|
/** The five on-the-run 5Y index contracts the CDX tab lists; IG and iTraxx quote spread, HY and EM price. */
|
|
@@ -27,14 +27,13 @@ function boardPoints(
|
|
|
27
27
|
rows: ReadonlyArray<{ id: string; points: readonly CreditBoardPoint[] }>,
|
|
28
28
|
id: string,
|
|
29
29
|
label: string,
|
|
30
|
-
resource: { stale?: boolean; refreshError?: string | null },
|
|
30
|
+
resource: { stale?: boolean; refreshError?: string | null; savedAt?: number },
|
|
31
31
|
): SeriesLoadResult {
|
|
32
32
|
const row = rows.find((entry) => entry.id === id);
|
|
33
33
|
if (!row) throw new Error(`${label} is not on the board right now.`);
|
|
34
|
-
const warning = cachedSeriesWarning(resource);
|
|
35
34
|
return {
|
|
36
35
|
points: row.points.map((point) => ({ date: point.date, value: point.level })),
|
|
37
|
-
...(
|
|
36
|
+
...cachedSeriesNote(resource),
|
|
38
37
|
};
|
|
39
38
|
}
|
|
40
39
|
|