gloomberb 0.15.0 → 0.15.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/bin/gloomberb +9 -1
- package/package.json +1 -1
- package/src/api-client/central-bank-rates.ts +3 -0
- package/src/api-client/types.ts +1 -1
- package/src/app/pane-runtime/index.tsx +7 -5
- package/src/app/runtime/broker-import.test.tsx +89 -0
- package/src/app/runtime/broker-import.ts +29 -8
- package/src/brokers/sync-broker-instance.ts +67 -0
- package/src/cli/commands/overview.ts +6 -2
- package/src/cli/native-smoke.ts +2 -0
- package/src/components/chart/composite/composite-chart.tsx +21 -1
- package/src/components/chart/composite/format.ts +71 -8
- package/src/components/chart/composite/fx-precision.test.ts +21 -3
- package/src/components/chart/composite/scene.ts +13 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/layout/market-summary.ts +4 -2
- package/src/components/ticker/badge/format.test.ts +3 -3
- package/src/components/ticker/badge/format.ts +11 -9
- package/src/components/ticker/badge/text.test.tsx +6 -6
- package/src/components/ui/index.ts +2 -1
- package/src/components/ui/multi-select/dialog.tsx +46 -4
- package/src/components/ui/number-prompt-dialog.tsx +71 -0
- package/src/core/state/app/state.ts +8 -2
- package/src/market-data/coordinator/events.ts +5 -0
- package/src/market-data/coordinator/index.ts +44 -10
- package/src/market-data/market/format.test.ts +54 -0
- package/src/market-data/market/format.ts +164 -5
- package/src/market-data/query-store.test.ts +87 -0
- package/src/market-data/query-store.ts +66 -9
- package/src/plugins/builtin/alerts/format.ts +4 -3
- package/src/plugins/builtin/analytics/display.ts +3 -3
- package/src/plugins/builtin/analytics/index.test.tsx +22 -22
- package/src/plugins/builtin/analytics/pane-model.test.ts +10 -10
- package/src/plugins/builtin/analytics/pane-model.ts +8 -8
- package/src/plugins/builtin/analytics/portfolio-identity.test.tsx +6 -6
- package/src/plugins/builtin/analytics/risk-client.ts +6 -8
- package/src/plugins/builtin/analytics/risk-model.test.ts +21 -2
- package/src/plugins/builtin/analytics/risk-model.ts +8 -0
- package/src/plugins/builtin/analytics/view.tsx +2 -2
- package/src/plugins/builtin/backtest/engine.test.ts +0 -1
- package/src/plugins/builtin/backtest/engine.ts +1 -4
- package/src/plugins/builtin/backtest/pane.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/company.tsx +3 -3
- package/src/plugins/builtin/central-bank-rates/client.ts +2 -1
- package/src/plugins/builtin/central-bank-rates/headless.ts +2 -2
- package/src/plugins/builtin/central-bank-rates/model.test.ts +16 -1
- package/src/plugins/builtin/central-bank-rates/model.ts +4 -4
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +50 -3
- package/src/plugins/builtin/chart-composer/presets.test.ts +17 -0
- package/src/plugins/builtin/chart-composer/presets.ts +48 -0
- package/src/plugins/builtin/chart-composer/settings.ts +42 -11
- package/src/plugins/builtin/chat/chat.test.tsx +2 -2
- package/src/plugins/builtin/cloud/askg/pane.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/board-pane.test.tsx +25 -0
- package/src/plugins/builtin/cloud/thesis/board-pane.tsx +3 -2
- package/src/plugins/builtin/cloud/thesis/store.ts +2 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +1 -1
- package/src/plugins/builtin/congress-trades/table.tsx +1 -1
- package/src/plugins/builtin/cot/client.ts +2 -1
- package/src/plugins/builtin/cot/model.test.ts +9 -0
- package/src/plugins/builtin/cot/model.ts +30 -1
- package/src/plugins/builtin/cot/pane.tsx +2 -4
- package/src/plugins/builtin/credit-conditions/client.ts +2 -1
- package/src/plugins/builtin/crypto-board/client.ts +2 -1
- package/src/plugins/builtin/crypto-board/model.test.ts +9 -0
- package/src/plugins/builtin/crypto-board/model.ts +11 -3
- package/src/plugins/builtin/custom-view/live-quotes.test.ts +6 -1
- package/src/plugins/builtin/custom-view/live-quotes.ts +26 -0
- package/src/plugins/builtin/custom-view/pane.tsx +18 -7
- package/src/plugins/builtin/custom-view/view-spec.test.ts +24 -6
- package/src/plugins/builtin/custom-view/view-spec.ts +53 -21
- package/src/plugins/builtin/debt-maturities/client.ts +16 -3
- package/src/plugins/builtin/debt-maturities/debt-maturities.test.ts +8 -0
- package/src/plugins/builtin/debt-maturities/headless.ts +3 -2
- package/src/plugins/builtin/debt-maturities/model.ts +2 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +0 -8
- package/src/plugins/builtin/earnings-calls/pane.test.tsx +14 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +3 -1
- package/src/plugins/builtin/econ/fred-client.test.ts +2 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +0 -1
- package/src/plugins/builtin/econ/index.tsx +4 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +4 -2
- package/src/plugins/builtin/econ-statistics/stats.ts +29 -24
- package/src/plugins/builtin/econ-statistics/view.test.ts +16 -0
- package/src/plugins/builtin/equity-screener/headless.test.ts +22 -0
- package/src/plugins/builtin/equity-screener/headless.ts +18 -10
- package/src/plugins/builtin/estimate-revisions/client.ts +3 -1
- package/src/plugins/builtin/estimate-revisions/model.test.ts +7 -0
- package/src/plugins/builtin/estimate-revisions/model.ts +0 -5
- package/src/plugins/builtin/futures/table.tsx +4 -3
- package/src/plugins/builtin/futures-curve/client.ts +2 -1
- package/src/plugins/builtin/futures-curve/model.test.ts +6 -4
- package/src/plugins/builtin/futures-curve/model.ts +18 -14
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +21 -4
- package/src/plugins/builtin/fx-matrix/live-legs.ts +7 -0
- package/src/plugins/builtin/fx-matrix/pairs.test.ts +5 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +9 -3
- package/src/plugins/builtin/insider/index.tsx +15 -12
- package/src/plugins/builtin/insider/pane.test.tsx +24 -0
- package/src/plugins/builtin/market-movers/headless.ts +2 -2
- package/src/plugins/builtin/market-movers/model.ts +23 -10
- package/src/plugins/builtin/market-movers/screener.test.ts +5 -1
- package/src/plugins/builtin/market-movers/screener.ts +3 -0
- package/src/plugins/builtin/market-movers/table.ts +2 -2
- package/src/plugins/builtin/market-valuation/client.test.ts +3 -0
- package/src/plugins/builtin/market-valuation/client.ts +6 -1
- package/src/plugins/builtin/money-markets/client.ts +2 -1
- package/src/plugins/builtin/money-markets/pane.tsx +0 -1
- package/src/plugins/builtin/options/expiry-selection.test.tsx +1 -1
- package/src/plugins/builtin/options/footer.test.ts +2 -2
- package/src/plugins/builtin/options/footer.ts +2 -2
- package/src/plugins/builtin/options/market-reference.test.ts +2 -2
- package/src/plugins/builtin/options/market-reference.ts +14 -2
- package/src/plugins/builtin/options/table.ts +5 -8
- package/src/plugins/builtin/options/view.test.tsx +2 -2
- package/src/plugins/builtin/options/view.tsx +1 -1
- package/src/plugins/builtin/options-calculator/pane.test.tsx +6 -6
- package/src/plugins/builtin/options-calculator/pane.tsx +23 -8
- package/src/plugins/builtin/options-calculator/surface.test.ts +16 -1
- package/src/plugins/builtin/options-calculator/surface.ts +13 -6
- package/src/plugins/builtin/portfolio-list/column-values.ts +17 -9
- package/src/plugins/builtin/portfolio-list/index.test.ts +5 -5
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +8 -8
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +12 -12
- package/src/plugins/builtin/portfolio-list/position-cost-integrity.test.ts +2 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +7 -6
- package/src/plugins/builtin/rate-path/client.ts +2 -1
- package/src/plugins/builtin/rate-path/headless.ts +1 -1
- package/src/plugins/builtin/rate-path/pane.tsx +0 -1
- package/src/plugins/builtin/realized-vol/client.test.ts +7 -0
- package/src/plugins/builtin/realized-vol/headless.ts +1 -1
- package/src/plugins/builtin/realized-vol/model.test.ts +16 -4
- package/src/plugins/builtin/realized-vol/model.ts +24 -9
- package/src/plugins/builtin/relative-rotation/client.ts +2 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -10
- package/src/plugins/builtin/risk-factors/data.test.ts +7 -0
- package/src/plugins/builtin/risk-factors/data.ts +8 -1
- package/src/plugins/builtin/risk-factors/pane.test.tsx +3 -0
- package/src/plugins/builtin/shared/screener-live-quotes.test.ts +3 -3
- package/src/plugins/builtin/shared/screener-live-quotes.ts +16 -1
- package/src/plugins/builtin/short-volume/client.ts +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +0 -1
- package/src/plugins/builtin/thirteenf/headless.ts +3 -3
- package/src/plugins/builtin/thirteenf/model.ts +14 -4
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +12 -5
- package/src/plugins/builtin/thirteenf/overlap.test.ts +13 -1
- package/src/plugins/builtin/thirteenf/overlap.ts +31 -3
- package/src/plugins/builtin/thirteenf/pane.test.tsx +2 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -9
- package/src/plugins/builtin/thirteenf/table.ts +9 -3
- package/src/plugins/builtin/ticker-detail/financials/model.ts +0 -12
- package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -1
- package/src/plugins/builtin/ticker-detail/index.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +9 -10
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +6 -9
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +3 -2
- package/src/plugins/builtin/time-sales/model.test.ts +17 -1
- package/src/plugins/builtin/time-sales/model.ts +14 -3
- package/src/plugins/builtin/time-sales/pane.tsx +8 -4
- package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
- package/src/plugins/builtin/vol-surface/client.ts +4 -1
- package/src/plugins/builtin/vol-surface/model.test.ts +4 -2
- package/src/plugins/builtin/vol-surface/model.ts +14 -2
- package/src/plugins/builtin/vol-surface/pane.tsx +9 -2
- package/src/plugins/builtin/volatility/client.test.ts +13 -8
- package/src/plugins/builtin/volatility/client.ts +3 -2
- package/src/plugins/builtin/volatility/headless.test.ts +2 -2
- package/src/plugins/builtin/volatility/model.test.ts +4 -0
- package/src/plugins/builtin/volatility/model.ts +6 -7
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -2
- package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +33 -1
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +34 -1
- package/src/renderers/electrobun/bun/index.ts +2 -1
- package/src/renderers/opentui/render-timing-smoke.ts +20 -0
- package/src/sources/provider-router/profile-enrichment.test.ts +0 -3
- package/src/sources/yahoo-finance.test.ts +5 -1
- package/src/sources/yahoo-finance.ts +2 -1
- package/src/time-series/forward-valuation.ts +6 -0
- package/src/time-series/resolve.ts +5 -1
- package/src/time-series/valuation-currency.test.ts +2 -0
- package/src/time-series/valuation-currency.ts +3 -1
- package/src/utils/exchange-rate-snapshot.test.ts +12 -1
- package/src/utils/exchange-rate-snapshot.ts +3 -2
- package/src/utils/format.ts +55 -20
- package/src/utils/fx-market-hours.test.ts +28 -0
- package/src/utils/fx-market-hours.ts +42 -0
|
@@ -147,9 +147,9 @@ test("actual analytics account switching changes quote, sector denominator and h
|
|
|
147
147
|
await act(async () => { setup!.mockInput.pressArrow("right"); await setup!.renderOnce(); }); await settleFrame(setup!, 20);
|
|
148
148
|
expect(latestState.paneState[paneId]?.portfolioId).toBe("b");
|
|
149
149
|
const frame = setup!.captureCharFrame();
|
|
150
|
-
expect(frame).toContain("Val
|
|
151
|
-
expect(frame).toContain("P&L +800");
|
|
152
|
-
expect(frame).toContain("Technology 50.0%
|
|
150
|
+
expect(frame).toContain("Val 4.0k");
|
|
151
|
+
expect(frame).toContain("P&L +800.00");
|
|
152
|
+
expect(frame).toContain("Technology 50.0% 2.0k");
|
|
153
153
|
expect(frame).not.toContain("Weights unavailable");
|
|
154
154
|
const requested = readChart.mock.calls.filter(([request]) => request.instrument.symbol === "ACME").map(([request]) => request.instrument.instrument?.conId);
|
|
155
155
|
expect(requested).toContain(101); expect(requested).toContain(202);
|
|
@@ -163,7 +163,7 @@ for (const portfolioId of ["a", "b"]) test(`actual analytics ${portfolioId} reje
|
|
|
163
163
|
await act(async () => f.coordinator.primeCachedFinancials([{ instrument: instrumentFromTicker(ticker, "ACME", { portfolioId })!, financials: financials("ACME", 300) }]));
|
|
164
164
|
await settleFrame(setup!, 20);
|
|
165
165
|
expect(setup!.captureCharFrame()).toContain("60.0%");
|
|
166
|
-
expect(setup!.captureCharFrame()).toContain("Val
|
|
166
|
+
expect(setup!.captureCharFrame()).toContain("Val 5.0k");
|
|
167
167
|
});
|
|
168
168
|
|
|
169
169
|
test("actual shared-portfolio preview uses its selected quote and history contracts", async () => {
|
|
@@ -195,7 +195,7 @@ test("unresolved selected contract skips market requests, keeps its own mark and
|
|
|
195
195
|
restore.push(() => quotes.mockRestore(), () => charts.mockRestore());
|
|
196
196
|
await render(f, "b", new Map([["ACME", financials("ACME", 999)]]));
|
|
197
197
|
const frame = setup!.captureCharFrame();
|
|
198
|
-
expect(frame).toContain("Technology 50.0%
|
|
198
|
+
expect(frame).toContain("Technology 50.0% 2.0k");
|
|
199
199
|
expect(frame).toContain("Broker contract unavailable for ACME");
|
|
200
200
|
expect(quotes.mock.calls.filter(([request]) => request.symbol === "ACME")).toEqual([]);
|
|
201
201
|
expect(charts.mock.calls.filter(([request]) => request.instrument.symbol === "ACME")).toEqual([]);
|
|
@@ -249,7 +249,7 @@ test("actual PF rejects another account's symbol-only quote on a scoped cache mi
|
|
|
249
249
|
const f = fixture(false, "b");
|
|
250
250
|
await render(f, "b", new Map([["ACME", financials("ACME", 999)]]), false, "portfolio-list");
|
|
251
251
|
const frame = setup!.captureCharFrame();
|
|
252
|
-
expect(frame).toContain("
|
|
252
|
+
expect(frame).toContain("2.0k"); expect(frame).toContain("50.00%"); expect(frame).not.toContain("10.0k");
|
|
253
253
|
});
|
|
254
254
|
|
|
255
255
|
test("actual Kelly uses scoped bankroll, current holding and price without research quote overriding them", async () => {
|
|
@@ -57,7 +57,7 @@ const sessionError = (error: unknown) =>
|
|
|
57
57
|
export const portfolioRiskCache = createPluginCache<RiskMarketSnapshot>({
|
|
58
58
|
kind: "portfolio-risk",
|
|
59
59
|
source: "gloom-cloud",
|
|
60
|
-
schemaVersion:
|
|
60
|
+
schemaVersion: 2,
|
|
61
61
|
policy: { staleMs: 2 * 60_000, expireMs: 24 * 60 * 60_000 },
|
|
62
62
|
});
|
|
63
63
|
type RiskCloudClient = Pick<
|
|
@@ -288,8 +288,9 @@ export async function fetchPortfolioRiskMarket(
|
|
|
288
288
|
const start = new Date(now);
|
|
289
289
|
start.setUTCMonth(start.getUTCMonth() - 18);
|
|
290
290
|
start.setUTCDate(1);
|
|
291
|
+
const today = now.toISOString().slice(0, 10);
|
|
291
292
|
const startDate = start.toISOString().slice(0, 10),
|
|
292
|
-
endDate = new Date(Date.parse(
|
|
293
|
+
endDate = new Date(Date.parse(today) - 86_400_000)
|
|
293
294
|
.toISOString()
|
|
294
295
|
.slice(0, 10);
|
|
295
296
|
const histories: RiskMarketHistory[] = new Array(unique.length);
|
|
@@ -307,7 +308,9 @@ export async function fetchPortfolioRiskMarket(
|
|
|
307
308
|
{
|
|
308
309
|
interval: "1day",
|
|
309
310
|
startDate,
|
|
310
|
-
|
|
311
|
+
// validateRiskHistory drops today's bar; ending on yesterday
|
|
312
|
+
// loses yesterday's close for most listings.
|
|
313
|
+
endDate: today,
|
|
311
314
|
outputsize: 1000,
|
|
312
315
|
rangeKey: "2Y",
|
|
313
316
|
},
|
|
@@ -364,11 +367,6 @@ export async function fetchPortfolioRiskMarket(
|
|
|
364
367
|
);
|
|
365
368
|
}
|
|
366
369
|
}
|
|
367
|
-
const closeMarked = histories.filter((row) => row?.closeMark).length;
|
|
368
|
-
if (closeMarked)
|
|
369
|
-
warnings.push(
|
|
370
|
-
`${closeMarked} instrument${closeMarked === 1 ? "" : "s"} had no current quote; holdings are weighted at the latest completed close.`,
|
|
371
|
-
);
|
|
372
370
|
return {
|
|
373
371
|
histories,
|
|
374
372
|
yields: fred[0]!.status === "fulfilled" ? fred[0]!.value : null,
|
|
@@ -29,13 +29,16 @@ function holding(symbol: string, quantity = 1): TickerRecord {
|
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
-
function market(): RiskMarketSnapshot {
|
|
32
|
+
function market(staleQuotes: readonly string[] = []): RiskMarketSnapshot {
|
|
33
33
|
return {
|
|
34
34
|
histories: RISK_FACTOR_INSTRUMENTS.map((instrument) => {
|
|
35
35
|
const history = riskHistory();
|
|
36
36
|
history.providerMeta!.normalizedSymbol = instrument.symbol;
|
|
37
37
|
history.providerMeta!.normalizedExchange = instrument.exchange;
|
|
38
|
-
const quote =
|
|
38
|
+
const quote = {
|
|
39
|
+
...riskQuote(instrument.symbol, instrument.exchange),
|
|
40
|
+
...(staleQuotes.includes(instrument.symbol) ? { stale: true } : {}),
|
|
41
|
+
},
|
|
39
42
|
valid = validateRiskHistory(history, instrument, quote, now);
|
|
40
43
|
return { instrument, currency: "USD", quote, ...valid, error: null };
|
|
41
44
|
}),
|
|
@@ -96,6 +99,22 @@ test("factor proxies resolve on their own listing venue", () => {
|
|
|
96
99
|
expect(factor.samples).toBe(60);
|
|
97
100
|
expect(factor.value).not.toBeNull();
|
|
98
101
|
});
|
|
102
|
+
test("only held positions marked at a close raise the close-mark warning", () => {
|
|
103
|
+
// Factor proxies with no current quote never weight a holding.
|
|
104
|
+
const snapshot = market(["IWM", "IWD", "IWF", "MTUM", "IEF", "HYG"]);
|
|
105
|
+
const warning = (model: ReturnType<typeof buildPortfolioRisk>) =>
|
|
106
|
+
model.warnings.filter((row) => row.includes("no current quote"));
|
|
107
|
+
expect(
|
|
108
|
+
warning(buildPortfolioRisk(portfolio, [holding("SPY")], snapshot)),
|
|
109
|
+
).toEqual([]);
|
|
110
|
+
expect(
|
|
111
|
+
warning(
|
|
112
|
+
buildPortfolioRisk(portfolio, [holding("SPY"), holding("IWM")], snapshot),
|
|
113
|
+
),
|
|
114
|
+
).toEqual([
|
|
115
|
+
"1 holding had no current quote; weighted at the latest completed close.",
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
99
118
|
test("signed equity exposure remains in concentration while shorts block the unfinanced basket", () => {
|
|
100
119
|
const model = buildPortfolioRisk(
|
|
101
120
|
portfolio,
|
|
@@ -215,8 +215,16 @@ export function buildPortfolioRisk(
|
|
|
215
215
|
for (const holding of holdings)
|
|
216
216
|
holding.weight =
|
|
217
217
|
book?.rows.find((row) => row.id === holding.id)?.weight ?? null;
|
|
218
|
+
const closeMarked = holdings.filter(
|
|
219
|
+
(row) => row.markSource === "close" && row.value != null,
|
|
220
|
+
).length;
|
|
218
221
|
const warnings = [
|
|
219
222
|
...market.warnings,
|
|
223
|
+
...(closeMarked
|
|
224
|
+
? [
|
|
225
|
+
`${closeMarked} holding${closeMarked === 1 ? "" : "s"} had no current quote; weighted at the latest completed close.`,
|
|
226
|
+
]
|
|
227
|
+
: []),
|
|
220
228
|
...market.histories.flatMap((row) =>
|
|
221
229
|
row.error ? [`${row.instrument.symbol}: ${row.error}`] : [],
|
|
222
230
|
),
|
|
@@ -8,7 +8,7 @@ import type { StaticChartSurfaceProps } from "../../../components/chart/static";
|
|
|
8
8
|
import type { DataTableCell } from "../../../components/ui/data-table/types";
|
|
9
9
|
import { colors, priceColor } from "../../../theme/colors";
|
|
10
10
|
import { Box, Text } from "../../../ui";
|
|
11
|
-
import {
|
|
11
|
+
import { formatCompactAmount, formatPercentRaw } from "../../../utils/format";
|
|
12
12
|
import { formatSignedCompact, formatWeight, renderBar } from "./display";
|
|
13
13
|
import type {
|
|
14
14
|
SectorSortPreference,
|
|
@@ -143,7 +143,7 @@ function renderSectorCell(row: SectorTableRow, column: SectorTableColumn): DataT
|
|
|
143
143
|
case "weight":
|
|
144
144
|
return { text: formatWeight(row.weight) };
|
|
145
145
|
case "value":
|
|
146
|
-
return { text: row.value
|
|
146
|
+
return { text: formatCompactAmount(row.value ?? undefined) };
|
|
147
147
|
case "pnl":
|
|
148
148
|
return {
|
|
149
149
|
text: formatSignedCompact(row.pnl),
|
|
@@ -50,7 +50,6 @@ test("signals fill at the next open with costs, and a final open trade is marked
|
|
|
50
50
|
expect(result.strategy.hitRatePct).toBe(0);
|
|
51
51
|
// Buy-and-hold buys at the first fill after the start with the same cost.
|
|
52
52
|
expect(result.equity.at(-1)!.benchmark).toBeCloseTo(15 / (9 * 1.01), 10);
|
|
53
|
-
expect(result.warnings[0]).toContain("too few");
|
|
54
53
|
});
|
|
55
54
|
|
|
56
55
|
test("a signal on the final session does not trade", () => {
|
|
@@ -153,10 +153,7 @@ export function runBacktest(
|
|
|
153
153
|
windows += 1;
|
|
154
154
|
if (strategy[index]! / strategy[index - TRADING_DAYS]! > benchmark[index]! / benchmark[index - TRADING_DAYS]!) beat += 1;
|
|
155
155
|
}
|
|
156
|
-
const warnings = [
|
|
157
|
-
...(closed.length < 10 ? [`${closed.length} closed trade${closed.length === 1 ? "" : "s"}: too few to judge a hit rate.`] : []),
|
|
158
|
-
...(clean.length < bars.length ? [`${bars.length - clean.length} sessions without a positive close were skipped.`] : []),
|
|
159
|
-
];
|
|
156
|
+
const warnings = clean.length < bars.length ? [`${bars.length - clean.length} sessions without a positive close were skipped.`] : [];
|
|
160
157
|
return {
|
|
161
158
|
start: startDate, end: endDate, sessions: dates.length, equity, trades,
|
|
162
159
|
strategy: {
|
|
@@ -235,7 +235,7 @@ export function BacktestPane({ width, height, focused }: PaneProps) {
|
|
|
235
235
|
: column.id === "returnPct" ? (Math.abs(trade.returnPct) < 0.05 ? "0.0%" : `${trade.returnPct > 0 ? "+" : ""}${trade.returnPct.toFixed(1)}%`)
|
|
236
236
|
: String(trade.sessions),
|
|
237
237
|
color: column.id === "returnPct" ? (Math.abs(trade.returnPct) < 0.05 ? undefined : trade.returnPct > 0 ? colors.positive : colors.negative)
|
|
238
|
-
: trade.open && column.id === "exitDate" ? colors.
|
|
238
|
+
: trade.open && column.id === "exitDate" ? colors.textMuted : undefined,
|
|
239
239
|
})}
|
|
240
240
|
emptyStateTitle="No trades in this window."
|
|
241
241
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Section } from "../../../../components";
|
|
2
|
-
import { formatMarketPriceWithCurrency } from "../../../../market-data/market/format";
|
|
2
|
+
import { formatMarketPriceWithCurrency, liveQuoteFormatOptions } from "../../../../market-data/market/format";
|
|
3
3
|
import { useInlineTickerQuote } from "../../../../state/hooks/inline-tickers";
|
|
4
4
|
import { useTickerQuoteStream } from "../../../../state/hooks/live-ticker-financials";
|
|
5
5
|
import { colors } from "../../../../theme/colors";
|
|
@@ -32,8 +32,8 @@ export function liveCompanyValues(company: BuildoutCompany, quote: Quote | null)
|
|
|
32
32
|
? datasetCap * (quote.price / datasetPrice)
|
|
33
33
|
: null;
|
|
34
34
|
return {
|
|
35
|
-
price: formatMarketPriceWithCurrency(quote.price, quote.currency,
|
|
36
|
-
marketCap: liveCap != null ? `$${formatCompact(liveCap)}` : null,
|
|
35
|
+
price: formatMarketPriceWithCurrency(quote.price, quote.currency, liveQuoteFormatOptions(quote, quote.currency, "STK")),
|
|
36
|
+
marketCap: liveCap != null ? `$${formatCompact(liveCap, { fixedDecimals: true })}` : null,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -59,7 +59,8 @@ export async function fetchCentralBankRates(client: Pick<typeof apiClient, "getC
|
|
|
59
59
|
export interface CentralBankRatesResource { payload: CentralBankRatesPayload; stale: boolean; refreshError: string | null }
|
|
60
60
|
export function getCachedCentralBankRates(): CentralBankRatesResource | null {
|
|
61
61
|
const cached = centralBankRatesCache.get("g20", { allowExpired: true });
|
|
62
|
-
|
|
62
|
+
// The pane revalidates this copy on mount; only a failed refresh makes it stale.
|
|
63
|
+
return cached ? { payload: cached.data, stale: false, refreshError: null } : null;
|
|
63
64
|
}
|
|
64
65
|
export async function loadCentralBankRates(force = false): Promise<CentralBankRatesResource> {
|
|
65
66
|
const result = await centralBankRatesCache.load("g20", () => fetchCentralBankRates(), { force });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
2
2
|
import { fetchCentralBankRates } from "./client";
|
|
3
|
-
import { policyBoardRow, policyNotices } from "./model";
|
|
3
|
+
import { hasNoPolicyRate, policyBoardRow, policyNotices } from "./model";
|
|
4
4
|
|
|
5
5
|
export const centralBankRatesHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
6
6
|
discovery: { aliases: ["CBR", "ECFC", "CBRT"], dataRequirements: ["Gloom Cloud central-bank-rates endpoint"],
|
|
@@ -18,7 +18,7 @@ export const centralBankRatesHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
18
18
|
], rows: data.rows.map(({ history: _history, ...row }) => {
|
|
19
19
|
const board = policyBoardRow({ ...row, history: [] });
|
|
20
20
|
return { ...row, valueText: board.valueText, changeText: board.changeText, percentileValue: row.percentile.value };
|
|
21
|
-
}) }], errors, unavailableSymbols: data.rows.filter((row) => row.status === "unavailable").map((row) => row.id),
|
|
21
|
+
}) }], errors, unavailableSymbols: data.rows.filter((row) => row.status === "unavailable" && !hasNoPolicyRate(row)).map((row) => row.id),
|
|
22
22
|
metadata: { ...data, complete } };
|
|
23
23
|
},
|
|
24
24
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type { CentralBankRatesPayload, CentralBankRow } from "../../../api-client/central-bank-rates";
|
|
3
3
|
import { ApiRequestError } from "../../../api-client/errors";
|
|
4
|
+
import type { HeadlessPaneContext } from "../../../types/plugin";
|
|
4
5
|
import { fetchCentralBankRates, validateCentralBankRates } from "./client";
|
|
6
|
+
import { centralBankRatesHeadless } from "./headless";
|
|
5
7
|
import { policyBoardRow, policyHistory, policyNotices } from "./model";
|
|
6
8
|
|
|
7
9
|
function row(overrides: Partial<CentralBankRow> = {}): CentralBankRow {
|
|
@@ -36,7 +38,20 @@ describe("central bank policy boundary", () => {
|
|
|
36
38
|
previousValue: null, previousAsOf: null, direction: "unavailable", history: [], status: "unavailable", unavailableReason: "no-policy-rate" }));
|
|
37
39
|
expect(validateCentralBankRates(data).rows[1]!.value).toBeNull();
|
|
38
40
|
expect(policyBoardRow(data.rows[1]!).history).toEqual([]);
|
|
39
|
-
expect(policyNotices(data)).
|
|
41
|
+
expect(policyNotices(data)).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
test("members without a policy rate stay out of notices and report completeness", async () => {
|
|
44
|
+
const unavailable = { value: null, range: null, asOf: null, lagDays: null, changeBps: null, lastChangeDate: null,
|
|
45
|
+
previousValue: null, previousAsOf: null, direction: "unavailable" as const, history: [], status: "unavailable" as const };
|
|
46
|
+
const data = payload();
|
|
47
|
+
data.rows.push(row({ id: "AR", label: "Argentina", ...unavailable, unavailableReason: "no-policy-rate" }),
|
|
48
|
+
row({ id: "african-union", label: "African Union", ...unavailable, unavailableReason: "no-unified-rate" }));
|
|
49
|
+
const load = (body: CentralBankRatesPayload) => centralBankRatesHeadless.load({ rawArgument: "", argument: null, symbols: [], options: {} },
|
|
50
|
+
{ apiClient: { getCloudCentralBankRates: async () => body } } as unknown as HeadlessPaneContext);
|
|
51
|
+
expect(await load(data)).toMatchObject({ complete: true, errors: [], unavailableSymbols: [] });
|
|
52
|
+
data.status = "partial";
|
|
53
|
+
data.rows.push(row({ id: "IN", label: "India", ...unavailable, unavailableReason: "source-unavailable" }));
|
|
54
|
+
expect(await load(data)).toMatchObject({ complete: false, errors: ["India: source unavailable."], unavailableSymbols: ["IN"] });
|
|
40
55
|
});
|
|
41
56
|
test("rejects impossible dates, wrong units, contradictory ranges and undated changes", () => {
|
|
42
57
|
for (const overrides of [
|
|
@@ -7,17 +7,17 @@ export const policyRate = (value: number | null) => value == null ? "--" : `${pe
|
|
|
7
7
|
export const policyLevel = (row: CentralBankRow) => row.range ? `${percentText(row.range.lower)}-${percentText(row.range.upper)}%` : policyRate(row.value);
|
|
8
8
|
export const policyChange = (row: CentralBankRow) => row.changeBps == null ? "--"
|
|
9
9
|
: `${row.changeBps > 0 ? "+" : ""}${Number(row.changeBps.toFixed(1))}bp ${row.direction === "hike" ? "↑" : row.direction === "cut" ? "↓" : ""}`.trim();
|
|
10
|
+
/** Members with no policy rate to publish, kept out of the board and the notices; docs/research-data.md explains them. */
|
|
11
|
+
export const hasNoPolicyRate = (row: CentralBankRow) => row.status === "unavailable"
|
|
12
|
+
&& (row.unavailableReason === "no-policy-rate" || row.unavailableReason === "no-unified-rate");
|
|
10
13
|
export function policyNotices(data: CentralBankRatesPayload): string[] {
|
|
11
|
-
return data.rows.flatMap((row) => row.status === "stale" ? [`${row.label}: stale, latest observation ${row.asOf ?? "unknown"}${row.lagDays == null ? "" : ` (${row.lagDays} days old)`}.`]
|
|
14
|
+
return data.rows.filter((row) => !hasNoPolicyRate(row)).flatMap((row) => row.status === "stale" ? [`${row.label}: stale, latest observation ${row.asOf ?? "unknown"}${row.lagDays == null ? "" : ` (${row.lagDays} days old)`}.`]
|
|
12
15
|
: row.status === "unavailable" ? [`${row.label}: ${(row.unavailableReason ?? "unavailable").replaceAll("-", " ")}.`] : []);
|
|
13
16
|
}
|
|
14
17
|
export function policyHistory(row: CentralBankRow) {
|
|
15
18
|
return row.history.filter((point) => (!row.percentile.windowStart || point.date >= row.percentile.windowStart)
|
|
16
19
|
&& (!row.percentile.windowEnd || point.date <= row.percentile.windowEnd));
|
|
17
20
|
}
|
|
18
|
-
/** Members with no policy rate to publish, kept out of the board and listed in the notices. */
|
|
19
|
-
export const hasNoPolicyRate = (row: CentralBankRow) => row.status === "unavailable"
|
|
20
|
-
&& (row.unavailableReason === "no-policy-rate" || row.unavailableReason === "no-unified-rate");
|
|
21
21
|
export interface PolicyBoardRow extends MarketBoardRow { observation: CentralBankRow }
|
|
22
22
|
export function policyBoardRow(row: CentralBankRow): PolicyBoardRow {
|
|
23
23
|
return { id: row.id, label: row.label, labelDetail: row.instrument, value: row.value, valueText: policyLevel(row),
|
|
@@ -29,7 +29,8 @@ function PolicyDetail({ row, width, height, focused }: { row: CentralBankRow; wi
|
|
|
29
29
|
// A target range ranks and charts its midpoint, which the band's range would not say.
|
|
30
30
|
{ id: "range", label: "1Y range", value: `${policyRate(p.min)} to ${policyRate(p.max)}`, detail: row.range ? "midpoint" : undefined },
|
|
31
31
|
{ id: "asOf", label: "As of", value: row.asOf ?? "--", tone: row.status === "stale" ? "warning" : undefined,
|
|
32
|
-
detail: [row.publicationFrequency, row.lagDays == null ? null : `${row.lagDays}d lag`,
|
|
32
|
+
detail: [row.publicationFrequency, row.lagDays == null ? null : `${row.lagDays}d lag`,
|
|
33
|
+
row.confirmedAt ? `confirmed ${row.confirmedAt.slice(0, 10)}` : null, row.status === "stale" ? "stale" : null]
|
|
33
34
|
.filter(Boolean).join(" · ") || undefined },
|
|
34
35
|
{ id: "meeting", label: "Next meeting", value: row.nextMeeting?.date ?? "--" },
|
|
35
36
|
{ id: "source", label: "Source", value: row.source?.toUpperCase() ?? "--", detail: row.sourceSeriesIds.join(", ") || undefined },
|
|
@@ -64,7 +65,6 @@ export function CentralBankRatesPane({ width, height, focused }: PaneProps) {
|
|
|
64
65
|
info: data ? [
|
|
65
66
|
...(updatedAgo ? [{ id: "updated", parts: [{ text: updatedAgo, tone: "muted" as const }] }] : []),
|
|
66
67
|
...(resource.data?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "warning" as const }] }] : []),
|
|
67
|
-
...(data.status !== "available" ? [{ id: "partial", parts: [{ text: data.status, tone: "warning" as const }] }] : []),
|
|
68
68
|
] : [],
|
|
69
69
|
});
|
|
70
70
|
return <Box width={width} height={height} flexDirection="column">
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
} from "../../../components";
|
|
12
12
|
import {
|
|
13
13
|
MultiSelectDialogButton,
|
|
14
|
+
NumberPromptDialog,
|
|
14
15
|
type MultiSelectDialogButtonHandle,
|
|
16
|
+
type MultiSelectRowAction,
|
|
15
17
|
} from "../../../components/ui";
|
|
16
18
|
import { CompositeChart } from "../../../components/chart/composite";
|
|
17
19
|
import type { PaneProps, TickerResearchTabProps } from "../../../types/plugin";
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
} from "../../../time-series/resolution";
|
|
24
26
|
import { useResolvedChartSpec } from "../../../time-series/hooks";
|
|
25
27
|
import { LIVE_CHART_TERMINAL_FRAME_MS } from "../../../time-series/live-quotes";
|
|
28
|
+
import { FORWARD_VALUATION_BASIS_NOTICES } from "../../../time-series/forward-valuation";
|
|
26
29
|
import { chartSeriesSourceKey } from "../../../capabilities";
|
|
27
30
|
import { useShortcut } from "../../../react/input";
|
|
28
31
|
import { useDialog, useDialogState, type PromptContext } from "../../../ui/dialog";
|
|
@@ -57,11 +60,16 @@ import {
|
|
|
57
60
|
buildPriceChartPreset,
|
|
58
61
|
chartSeriesLabel,
|
|
59
62
|
defaultFinancialTimestampMode,
|
|
63
|
+
builtinStudyPeriod,
|
|
60
64
|
getSelectedBuiltinStudies,
|
|
61
65
|
getSelectedPairStudies,
|
|
66
|
+
isPeriodStudy,
|
|
62
67
|
setBuiltinStudies,
|
|
68
|
+
setBuiltinStudyPeriod,
|
|
63
69
|
setPairStudies,
|
|
64
70
|
rebindResearchChartSpec,
|
|
71
|
+
STUDY_PERIOD_MAX,
|
|
72
|
+
STUDY_PERIOD_MIN,
|
|
65
73
|
type BuiltinStudySelection,
|
|
66
74
|
type PairStudySelection,
|
|
67
75
|
} from "./presets";
|
|
@@ -70,7 +78,9 @@ import {
|
|
|
70
78
|
CHART_FORMULA_OPTIONS,
|
|
71
79
|
CHART_RANGES as RANGES,
|
|
72
80
|
CHART_RESOLUTIONS as RESOLUTIONS,
|
|
73
|
-
|
|
81
|
+
chartStudyLabel,
|
|
82
|
+
chartStudyOptionsFor,
|
|
83
|
+
chartStudyPeriodTitle,
|
|
74
84
|
} from "./settings";
|
|
75
85
|
import { resolveChartComposerShortcut } from "./shortcuts";
|
|
76
86
|
import { describeChartResolution, formatChartDateWindow, formatChartResolution } from "./viewport-labels";
|
|
@@ -309,6 +319,37 @@ function ChartComposerSurface({
|
|
|
309
319
|
const interactionCaptureRef = useRef(false);
|
|
310
320
|
const interactionCaptureSourcesRef = useRef(new Set<string>());
|
|
311
321
|
const indicatorsDialogRef = useRef<MultiSelectDialogButtonHandle | null>(null);
|
|
322
|
+
// The Indicators dialog keeps the callbacks it opened with; reading the spec
|
|
323
|
+
// through this ref keeps a toggle from undoing a period edit made meanwhile.
|
|
324
|
+
const specRef = useRef(spec);
|
|
325
|
+
specRef.current = spec;
|
|
326
|
+
const studyOptions = useMemo(() => chartStudyOptionsFor(spec), [spec]);
|
|
327
|
+
const studyPeriodAction = useMemo<MultiSelectRowAction>(() => ({
|
|
328
|
+
label: "Period",
|
|
329
|
+
shortcut: "p",
|
|
330
|
+
appliesTo: (value, selected) => selected && isPeriodStudy(value),
|
|
331
|
+
run: async (value) => {
|
|
332
|
+
if (!isPeriodStudy(value)) return;
|
|
333
|
+
const current = builtinStudyPeriod(specRef.current, value) ?? STUDY_PERIOD_MIN;
|
|
334
|
+
const next = await dialog.prompt<number>({
|
|
335
|
+
content: (ctx: PromptContext<number>) => (
|
|
336
|
+
<NumberPromptDialog
|
|
337
|
+
{...ctx}
|
|
338
|
+
title={chartStudyPeriodTitle(value)}
|
|
339
|
+
initialValue={current}
|
|
340
|
+
min={STUDY_PERIOD_MIN}
|
|
341
|
+
max={STUDY_PERIOD_MAX}
|
|
342
|
+
invalidMessage={`Whole number from ${STUDY_PERIOD_MIN} to ${STUDY_PERIOD_MAX}`}
|
|
343
|
+
/>
|
|
344
|
+
),
|
|
345
|
+
});
|
|
346
|
+
if (next === undefined || next === current) return;
|
|
347
|
+
const nextSpec = setBuiltinStudyPeriod(specRef.current, value, next);
|
|
348
|
+
specRef.current = nextSpec;
|
|
349
|
+
setSpec(nextSpec);
|
|
350
|
+
return chartStudyLabel(value, next);
|
|
351
|
+
},
|
|
352
|
+
}), [dialog, setSpec]);
|
|
312
353
|
const formulasDialogRef = useRef<MultiSelectDialogButtonHandle | null>(null);
|
|
313
354
|
const indicatorsDisabled = !isPriceStudyTarget(spec);
|
|
314
355
|
const formulasDisabled = spec.series.filter((series) => series.visible !== false).length < 2;
|
|
@@ -560,6 +601,7 @@ function ChartComposerSurface({
|
|
|
560
601
|
: []);
|
|
561
602
|
const statusWarnings = resolution.warnings.filter((warning) => (
|
|
562
603
|
warning !== FINANCIAL_VINTAGE_NOTICE && warning !== SEC_EPS_BASIS_NOTICE && warning !== comparisonNotice
|
|
604
|
+
&& !FORWARD_VALUATION_BASIS_NOTICES.has(warning)
|
|
563
605
|
&& !duplicateErrorNotices.has(warning) && !comparisonEmptyNotices.has(warning)
|
|
564
606
|
&& !spec.studies.some((study) => (
|
|
565
607
|
(study.kind === "ratio" && warning.startsWith(`${study.id}: ratio inputs use different currencies (`)
|
|
@@ -640,9 +682,14 @@ function ChartComposerSurface({
|
|
|
640
682
|
ref={indicatorsDialogRef}
|
|
641
683
|
label="Indicators"
|
|
642
684
|
title="Chart Indicators"
|
|
643
|
-
options={
|
|
685
|
+
options={studyOptions}
|
|
644
686
|
selectedValues={selectedStudies}
|
|
645
|
-
onChange={(values) =>
|
|
687
|
+
onChange={(values) => {
|
|
688
|
+
const nextSpec = setBuiltinStudies(specRef.current, values as BuiltinStudySelection[]);
|
|
689
|
+
specRef.current = nextSpec;
|
|
690
|
+
setSpec(nextSpec);
|
|
691
|
+
}}
|
|
692
|
+
rowAction={studyPeriodAction}
|
|
646
693
|
disabled={indicatorsDisabled}
|
|
647
694
|
idPrefix={`${footerId}:indicators`}
|
|
648
695
|
shortcutKey="i"
|
|
@@ -653,3 +653,20 @@ describe("chart composer CLI options", () => {
|
|
|
653
653
|
});
|
|
654
654
|
});
|
|
655
655
|
});
|
|
656
|
+
|
|
657
|
+
describe("indicator periods", () => {
|
|
658
|
+
test("an edited period survives toggling other indicators and names itself in the picker", async () => {
|
|
659
|
+
const { builtinStudyPeriod, setBuiltinStudyPeriod } = await import("./presets");
|
|
660
|
+
const { chartStudyLabel } = await import("./settings");
|
|
661
|
+
const withSma = setBuiltinStudies(buildPriceChartPreset("AAPL"), ["sma50"]);
|
|
662
|
+
const edited = setBuiltinStudyPeriod(withSma, "sma50", 30);
|
|
663
|
+
expect(builtinStudyPeriod(edited, "sma50")).toBe(30);
|
|
664
|
+
// Turning another indicator on and off rebuilds the list from the selection.
|
|
665
|
+
const toggled = setBuiltinStudies(setBuiltinStudies(edited, ["sma50", "rsi14"]), ["sma50"]);
|
|
666
|
+
expect(builtinStudyPeriod(toggled, "sma50")).toBe(30);
|
|
667
|
+
expect(chartStudyLabel("sma50", builtinStudyPeriod(toggled, "sma50"))).toBe("Simple moving average (SMA 30)");
|
|
668
|
+
// An indicator that is not on keeps its default and is left alone.
|
|
669
|
+
expect(setBuiltinStudyPeriod(toggled, "ema20", 9)).toBe(toggled);
|
|
670
|
+
expect(builtinStudyPeriod(toggled, "ema20")).toBe(20);
|
|
671
|
+
});
|
|
672
|
+
});
|
|
@@ -794,6 +794,54 @@ export function setBuiltinStudies(spec: ChartSpec, selected: readonly BuiltinStu
|
|
|
794
794
|
return { ...spec, studies, panels: reconcilePanels(spec.panels, spec.series, studies) };
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
+
/** Builtin studies whose lookback the Indicators dialog lets you change. */
|
|
798
|
+
const PERIOD_STUDIES = new Set<BuiltinStudySelection>(["sma20", "sma50", "sma200", "ema20", "bollinger20", "rsi14"]);
|
|
799
|
+
|
|
800
|
+
export const STUDY_PERIOD_MIN = 2;
|
|
801
|
+
export const STUDY_PERIOD_MAX = 500;
|
|
802
|
+
|
|
803
|
+
export function isPeriodStudy(selection: string): selection is BuiltinStudySelection {
|
|
804
|
+
return PERIOD_STUDIES.has(selection as BuiltinStudySelection);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function builtinStudy(spec: ChartSpec, selection: BuiltinStudySelection): ChartStudySpec | undefined {
|
|
808
|
+
const prefix = `${BUILTIN_STUDY_ID_PREFIX}${selection}:`;
|
|
809
|
+
return spec.studies.find((study) => study.id.startsWith(prefix));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export function defaultStudyPeriod(selection: BuiltinStudySelection): number | null {
|
|
813
|
+
const defaults = STUDY_DEFAULTS[selection].parameters as Record<string, unknown>;
|
|
814
|
+
return typeof defaults.period === "number" ? defaults.period : null;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** The period a builtin study runs with: its own when selected, the default otherwise. */
|
|
818
|
+
export function builtinStudyPeriod(spec: ChartSpec, selection: BuiltinStudySelection): number | null {
|
|
819
|
+
const period = builtinStudy(spec, selection)?.parameters.period;
|
|
820
|
+
return typeof period === "number" && Number.isFinite(period) ? period : defaultStudyPeriod(selection);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Changes the period of a selected builtin study. The study keeps its id, so
|
|
825
|
+
* `setBuiltinStudies` carries the new period through later toggles; an
|
|
826
|
+
* unselected study is left alone.
|
|
827
|
+
*/
|
|
828
|
+
export function setBuiltinStudyPeriod(
|
|
829
|
+
spec: ChartSpec,
|
|
830
|
+
selection: BuiltinStudySelection,
|
|
831
|
+
period: number,
|
|
832
|
+
): ChartSpec {
|
|
833
|
+
if (!isPeriodStudy(selection)) return spec;
|
|
834
|
+
const bounded = Math.round(Math.min(STUDY_PERIOD_MAX, Math.max(STUDY_PERIOD_MIN, period)));
|
|
835
|
+
const target = builtinStudy(spec, selection);
|
|
836
|
+
if (!target) return spec;
|
|
837
|
+
return {
|
|
838
|
+
...spec,
|
|
839
|
+
studies: spec.studies.map((study) => (
|
|
840
|
+
study.id === target.id ? { ...study, parameters: { ...study.parameters, period: bounded } } : study
|
|
841
|
+
)),
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
797
845
|
export type PairStudySelection = "ratio" | "spread" | "correlation";
|
|
798
846
|
|
|
799
847
|
const PAIR_STUDY_ID_PREFIX = "pair:";
|
|
@@ -24,7 +24,10 @@ import {
|
|
|
24
24
|
chartSeriesLabel,
|
|
25
25
|
formatSeriesExpression,
|
|
26
26
|
getCompatibleSeriesStyles,
|
|
27
|
+
builtinStudyPeriod,
|
|
28
|
+
defaultStudyPeriod,
|
|
27
29
|
getSelectedBuiltinStudies,
|
|
30
|
+
isPeriodStudy,
|
|
28
31
|
getSelectedPairStudies,
|
|
29
32
|
setBuiltinStudies,
|
|
30
33
|
setPairStudies,
|
|
@@ -38,17 +41,45 @@ import {
|
|
|
38
41
|
|
|
39
42
|
export { CHART_RANGES, CHART_RESOLUTIONS };
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
{
|
|
45
|
-
{
|
|
46
|
-
{
|
|
47
|
-
{
|
|
48
|
-
{
|
|
49
|
-
{
|
|
50
|
-
{
|
|
51
|
-
|
|
44
|
+
/** Full name and short form for each builtin indicator; the label adds its period. */
|
|
45
|
+
const CHART_STUDY_NAMES: Record<BuiltinStudySelection, { name: string; short?: string; description: string }> = {
|
|
46
|
+
volume: { name: "Volume", description: "Volume columns in a lower panel." },
|
|
47
|
+
sma20: { name: "Simple moving average", short: "SMA", description: "Simple moving average on the primary price series." },
|
|
48
|
+
sma50: { name: "Simple moving average", short: "SMA", description: "Simple moving average on the primary price series." },
|
|
49
|
+
sma200: { name: "Simple moving average", short: "SMA", description: "Simple moving average on the primary price series." },
|
|
50
|
+
ema20: { name: "Exponential moving average", short: "EMA", description: "Exponential moving average on the primary price series." },
|
|
51
|
+
bollinger20: { name: "Bollinger Bands", short: "BB", description: "Bollinger Bands at two standard deviations." },
|
|
52
|
+
rsi14: { name: "Relative strength index", short: "RSI", description: "Relative Strength Index in a lower panel." },
|
|
53
|
+
macd: { name: "MACD", short: "12, 26, 9", description: "12/26/9 MACD in a lower panel." },
|
|
54
|
+
"realized-vol": { name: "Realized volatility", description: "Annualized daily volatility in a lower panel." },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Title of the prompt that edits a study's period: `SMA period`. */
|
|
58
|
+
export function chartStudyPeriodTitle(selection: BuiltinStudySelection): string {
|
|
59
|
+
return `${CHART_STUDY_NAMES[selection].short ?? CHART_STUDY_NAMES[selection].name} period`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `Simple moving average (SMA 50)`: the name, the acronym and the period it runs with. */
|
|
63
|
+
export function chartStudyLabel(selection: BuiltinStudySelection, period: number | null): string {
|
|
64
|
+
const { name, short } = CHART_STUDY_NAMES[selection];
|
|
65
|
+
if (!short) return name;
|
|
66
|
+
return isPeriodStudy(selection) && period != null ? `${name} (${short} ${period})` : `${name} (${short})`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function chartStudyOptions(periodOf: (selection: BuiltinStudySelection) => number | null) {
|
|
70
|
+
return (Object.keys(CHART_STUDY_NAMES) as BuiltinStudySelection[]).map((value) => ({
|
|
71
|
+
value,
|
|
72
|
+
label: chartStudyLabel(value, periodOf(value)),
|
|
73
|
+
description: CHART_STUDY_NAMES[value].description,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const CHART_STUDY_OPTIONS: Array<PaneSettingOption & { value: BuiltinStudySelection }> = chartStudyOptions(defaultStudyPeriod);
|
|
78
|
+
|
|
79
|
+
/** The indicator options with the periods this chart actually uses. */
|
|
80
|
+
export function chartStudyOptionsFor(spec: ChartSpec): Array<PaneSettingOption & { value: BuiltinStudySelection }> {
|
|
81
|
+
return chartStudyOptions((selection) => builtinStudyPeriod(spec, selection));
|
|
82
|
+
}
|
|
52
83
|
|
|
53
84
|
export const CHART_FORMULA_OPTIONS: Array<PaneSettingOption & { value: PairStudySelection }> = [
|
|
54
85
|
{ value: "ratio", label: "Ratio", description: "First series divided by the second series." },
|
|
@@ -1076,8 +1076,8 @@ describe("ChatContent", () => {
|
|
|
1076
1076
|
await flushFrame();
|
|
1077
1077
|
|
|
1078
1078
|
const lines = setup().captureCharFrame().split("\n");
|
|
1079
|
-
const row = lines.findIndex((line) => line.includes("TSLA -5%"));
|
|
1080
|
-
const col = lines[row]?.indexOf("TSLA -5%") ?? -1;
|
|
1079
|
+
const row = lines.findIndex((line) => line.includes("TSLA -5.0%"));
|
|
1080
|
+
const col = lines[row]?.indexOf("TSLA -5.0%") ?? -1;
|
|
1081
1081
|
|
|
1082
1082
|
expect(row).toBeGreaterThanOrEqual(0);
|
|
1083
1083
|
expect(col).toBeGreaterThanOrEqual(0);
|
|
@@ -190,7 +190,7 @@ function ToolTimelineRow({
|
|
|
190
190
|
<Text fg={colors.textDim}>{` ${truncateWithEllipsis(row.argumentSummary, summaryWidth)}`}</Text>
|
|
191
191
|
) : null}
|
|
192
192
|
<Box flexGrow={1} />
|
|
193
|
-
{tier ? <Text fg={colors.warning}>{`${tier} `}</Text> : null}
|
|
193
|
+
{tier ? <Text fg={row.writeTier === "ui-write" ? colors.textMuted : colors.warning}>{`${tier} `}</Text> : null}
|
|
194
194
|
{row.status === "running" || row.status === "pending" ? (
|
|
195
195
|
<Spinner label={status} />
|
|
196
196
|
) : (
|