opencandle 0.10.0 → 0.11.1
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/README.md +12 -9
- package/dist/analysts/orchestrator.d.ts +3 -4
- package/dist/analysts/orchestrator.js +34 -44
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/cli-main.js +242 -9
- package/dist/cli-main.js.map +1 -1
- package/dist/config.d.ts +4 -9
- package/dist/config.js +7 -10
- package/dist/config.js.map +1 -1
- package/dist/doctor/report.js +1 -1
- package/dist/doctor/report.js.map +1 -1
- package/dist/infra/cache.d.ts +6 -0
- package/dist/infra/cache.js +16 -6
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/freshness.d.ts +21 -0
- package/dist/infra/freshness.js +119 -0
- package/dist/infra/freshness.js.map +1 -0
- package/dist/infra/market-calendar.d.ts +14 -0
- package/dist/infra/market-calendar.js +137 -0
- package/dist/infra/market-calendar.js.map +1 -0
- package/dist/infra/rate-limiter.js +1 -0
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/market-state/daily-report.d.ts +1 -1
- package/dist/market-state/daily-report.js +5 -16
- package/dist/market-state/daily-report.js.map +1 -1
- package/dist/market-state/service.d.ts +0 -35
- package/dist/market-state/service.js +0 -63
- package/dist/market-state/service.js.map +1 -1
- package/dist/market-state/summaries.d.ts +8 -0
- package/dist/market-state/summaries.js +70 -0
- package/dist/market-state/summaries.js.map +1 -0
- package/dist/memory/sqlite.js +20 -19
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/onboarding/providers.d.ts +25 -1
- package/dist/onboarding/providers.js +26 -0
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/pi/opencandle-extension.js +57 -226
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-action-dedupe.d.ts +6 -0
- package/dist/pi/session-action-dedupe.js +126 -0
- package/dist/pi/session-action-dedupe.js.map +1 -0
- package/dist/pi/session-writer-lock.d.ts +26 -2
- package/dist/pi/session-writer-lock.js +230 -18
- package/dist/pi/session-writer-lock.js.map +1 -1
- package/dist/pi/setup.js +5 -5
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tui-session-coordinator.d.ts +15 -0
- package/dist/pi/tui-session-coordinator.js +283 -0
- package/dist/pi/tui-session-coordinator.js.map +1 -0
- package/dist/prompts/context-builder.js +1 -1
- package/dist/prompts/policy-cards.js +1 -1
- package/dist/prompts/policy-cards.js.map +1 -1
- package/dist/providers/alpha-vantage.js +6 -0
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/coingecko.js +1 -0
- package/dist/providers/coingecko.js.map +1 -1
- package/dist/providers/polymarket.d.ts +2 -0
- package/dist/providers/polymarket.js +163 -0
- package/dist/providers/polymarket.js.map +1 -0
- package/dist/providers/wrap-provider.js +5 -3
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +3 -0
- package/dist/providers/yahoo-finance.js +206 -10
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.js +4 -5
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/entity-extractor.js +16 -0
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/route-manifest.js +1 -2
- package/dist/routing/route-manifest.js.map +1 -1
- package/dist/routing/router-llm-client.js +18 -3
- package/dist/routing/router-llm-client.js.map +1 -1
- package/dist/routing/router-prompt.js +1 -1
- package/dist/routing/router.d.ts +1 -1
- package/dist/routing/router.js +309 -9
- package/dist/routing/router.js.map +1 -1
- package/dist/routing/slot-resolver.d.ts +1 -0
- package/dist/routing/slot-resolver.js +1 -1
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/runtime/evidence.d.ts +1 -0
- package/dist/runtime/evidence.js.map +1 -1
- package/dist/runtime/numeric-claims.d.ts +23 -0
- package/dist/runtime/numeric-claims.js +100 -0
- package/dist/runtime/numeric-claims.js.map +1 -0
- package/dist/runtime/planning-evidence.js +1 -76
- package/dist/runtime/planning-evidence.js.map +1 -1
- package/dist/runtime/prompt-step.d.ts +10 -1
- package/dist/runtime/prompt-step.js +116 -2
- package/dist/runtime/prompt-step.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +11 -1
- package/dist/runtime/session-coordinator.js +354 -62
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/validation.d.ts +3 -1
- package/dist/runtime/workflow-events.d.ts +1 -1
- package/dist/runtime/workflow-events.js.map +1 -1
- package/dist/runtime/workflow-types.d.ts +3 -0
- package/dist/runtime/workflow-types.js.map +1 -1
- package/dist/system-prompt.js +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -0
- package/dist/tools/fundamentals/comps.js +20 -9
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +190 -76
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/macro/event-probabilities.d.ts +8 -0
- package/dist/tools/macro/event-probabilities.js +121 -0
- package/dist/tools/macro/event-probabilities.js.map +1 -0
- package/dist/tools/market/crypto-price.d.ts +4 -1
- package/dist/tools/market/crypto-price.js +10 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/screen-stocks.js +14 -3
- package/dist/tools/market/screen-stocks.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +4 -1
- package/dist/tools/market/stock-quote.js +9 -4
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +4 -1
- package/dist/tools/options/option-chain.js +12 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/daily-report.js +10 -4
- package/dist/tools/portfolio/daily-report.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +23 -5
- package/dist/tools/technical/backtest.js +131 -94
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.js +1 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/fundamentals.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/market.d.ts +2 -0
- package/dist/types/options.d.ts +1 -0
- package/dist/types/prediction-markets.d.ts +13 -0
- package/dist/types/prediction-markets.js +2 -0
- package/dist/types/prediction-markets.js.map +1 -0
- package/gui/server/chat-event-adapter.ts +89 -5
- package/gui/server/http-routes.ts +661 -37
- package/gui/server/invoke-tool.ts +165 -16
- package/gui/server/live-chat-event-adapter.ts +37 -2
- package/gui/server/local-session-coordinator.ts +97 -0
- package/gui/server/market-state-api.ts +64 -48
- package/gui/server/projector.ts +132 -3
- package/gui/server/server.ts +51 -6
- package/gui/server/session-actions.ts +146 -45
- package/gui/server/session-entry-wait.ts +22 -3
- package/gui/server/tool-metadata.ts +3 -1
- package/gui/server/ws-hub.ts +77 -10
- package/gui/shared/chat-events.ts +10 -1
- package/gui/shared/event-reducer.ts +1 -0
- package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
- package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
- package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +13 -15
- package/src/analysts/orchestrator.ts +35 -56
- package/src/cli-main.ts +253 -13
- package/src/config.ts +12 -20
- package/src/doctor/report.ts +1 -1
- package/src/infra/cache.ts +24 -11
- package/src/infra/freshness.ts +165 -0
- package/src/infra/market-calendar.ts +193 -0
- package/src/infra/rate-limiter.ts +1 -0
- package/src/market-state/daily-report.ts +6 -16
- package/src/market-state/service.ts +0 -136
- package/src/market-state/summaries.ts +75 -0
- package/src/memory/sqlite.ts +23 -19
- package/src/onboarding/providers.ts +29 -1
- package/src/pi/opencandle-extension.ts +67 -270
- package/src/pi/session-action-dedupe.ts +155 -0
- package/src/pi/session-writer-lock.ts +290 -20
- package/src/pi/setup.ts +6 -6
- package/src/pi/tui-session-coordinator.ts +351 -0
- package/src/prompts/context-builder.ts +1 -1
- package/src/prompts/policy-cards.ts +1 -1
- package/src/providers/alpha-vantage.ts +6 -0
- package/src/providers/coingecko.ts +2 -0
- package/src/providers/polymarket.ts +214 -0
- package/src/providers/wrap-provider.ts +5 -3
- package/src/providers/yahoo-finance.ts +339 -27
- package/src/routing/classify-intent.ts +4 -5
- package/src/routing/entity-extractor.ts +17 -0
- package/src/routing/route-manifest.ts +1 -2
- package/src/routing/router-llm-client.ts +31 -19
- package/src/routing/router-prompt.ts +1 -1
- package/src/routing/router.ts +379 -8
- package/src/routing/slot-resolver.ts +1 -1
- package/src/runtime/evidence.ts +1 -0
- package/src/runtime/numeric-claims.ts +108 -0
- package/src/runtime/planning-evidence.ts +6 -91
- package/src/runtime/prompt-step.ts +127 -2
- package/src/runtime/session-coordinator.ts +464 -74
- package/src/runtime/validation.ts +1 -1
- package/src/runtime/workflow-events.ts +2 -0
- package/src/runtime/workflow-types.ts +3 -0
- package/src/system-prompt.ts +1 -1
- package/src/tools/AGENTS.md +1 -1
- package/src/tools/fundamentals/comps.ts +25 -14
- package/src/tools/fundamentals/dcf.ts +217 -87
- package/src/tools/index.ts +3 -3
- package/src/tools/macro/event-probabilities.ts +141 -0
- package/src/tools/market/crypto-price.ts +14 -2
- package/src/tools/market/screen-stocks.ts +14 -3
- package/src/tools/market/stock-quote.ts +49 -43
- package/src/tools/options/option-chain.ts +16 -2
- package/src/tools/portfolio/daily-report.ts +10 -4
- package/src/tools/portfolio/risk-analysis.ts +1 -1
- package/src/tools/technical/backtest.ts +167 -108
- package/src/tools/technical/indicators.ts +1 -1
- package/src/types/fundamentals.ts +1 -0
- package/src/types/index.ts +1 -0
- package/src/types/market.ts +2 -0
- package/src/types/options.ts +1 -0
- package/src/types/prediction-markets.ts +13 -0
- package/dist/tools/portfolio/predictions.d.ts +0 -55
- package/dist/tools/portfolio/predictions.js +0 -422
- package/dist/tools/portfolio/predictions.js.map +0 -1
- package/gui/server/chat-run-session.ts +0 -16
- package/gui/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
- package/gui/web/dist/assets/index-B7QAjY5g.js +0 -65
- package/gui/web/dist/assets/index-D5dbWPfM.css +0 -2
- package/src/tools/portfolio/predictions.ts +0 -553
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { buildFreshnessStamp, type FreshnessStamp, formatAsOfLine } from "../../infra/freshness.js";
|
|
3
4
|
import { getCryptoPrice } from "../../providers/coingecko.js";
|
|
4
5
|
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
5
6
|
import type { CryptoPrice } from "../../types/market.js";
|
|
@@ -10,7 +11,10 @@ const params = Type.Object({
|
|
|
10
11
|
}),
|
|
11
12
|
});
|
|
12
13
|
|
|
13
|
-
export const cryptoPriceTool: AgentTool<
|
|
14
|
+
export const cryptoPriceTool: AgentTool<
|
|
15
|
+
typeof params,
|
|
16
|
+
CryptoPrice & { freshness: FreshnessStamp }
|
|
17
|
+
> = {
|
|
14
18
|
name: "get_crypto_price",
|
|
15
19
|
label: "Crypto Price",
|
|
16
20
|
description: "Get current crypto price, 24h change, market cap, volume, ATH, and supply data",
|
|
@@ -26,6 +30,13 @@ export const cryptoPriceTool: AgentTool<typeof params, CryptoPrice> = {
|
|
|
26
30
|
};
|
|
27
31
|
}
|
|
28
32
|
const crypto = result.data;
|
|
33
|
+
const freshness = buildFreshnessStamp({
|
|
34
|
+
asOf: crypto.asOf,
|
|
35
|
+
cached: result.cached,
|
|
36
|
+
stale: result.stale,
|
|
37
|
+
cachedAt: result.cached || result.stale ? result.timestamp : undefined,
|
|
38
|
+
assetClass: "crypto",
|
|
39
|
+
});
|
|
29
40
|
const sign = crypto.changePercent24h >= 0 ? "+" : "";
|
|
30
41
|
const text = [
|
|
31
42
|
`${crypto.name} (${crypto.symbol.toUpperCase()}): $${formatPrice(crypto.price)} (${sign}${crypto.changePercent24h.toFixed(2)}%)`,
|
|
@@ -33,9 +44,10 @@ export const cryptoPriceTool: AgentTool<typeof params, CryptoPrice> = {
|
|
|
33
44
|
`Market Cap: $${formatLargeNumber(crypto.marketCap)} | 24h Volume: $${formatLargeNumber(crypto.volume24h)}`,
|
|
34
45
|
`ATH: $${formatPrice(crypto.ath)} (${crypto.athDate.split("T")[0]})`,
|
|
35
46
|
`Circulating: ${formatLargeNumber(crypto.circulatingSupply)} ${crypto.symbol.toUpperCase()}`,
|
|
47
|
+
formatAsOfLine(freshness),
|
|
36
48
|
].join("\n");
|
|
37
49
|
|
|
38
|
-
return { content: [{ type: "text", text }], details: crypto };
|
|
50
|
+
return { content: [{ type: "text", text }], details: { ...crypto, freshness } };
|
|
39
51
|
},
|
|
40
52
|
};
|
|
41
53
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { type Static, Type } from "@sinclair/typebox";
|
|
3
|
+
import { buildFreshnessStamp, formatAsOfLine } from "../../infra/freshness.js";
|
|
3
4
|
import type { ScreenerRow, ScreenFilterOp } from "../../providers/tradingview.js";
|
|
4
5
|
import { screenStocks } from "../../providers/tradingview.js";
|
|
5
6
|
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
@@ -136,12 +137,21 @@ export const screenStocksTool: AgentTool<typeof params, ScreenerRow[]> = {
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
const rows = result.data;
|
|
140
|
+
const freshness = buildFreshnessStamp({
|
|
141
|
+
cached: result.cached,
|
|
142
|
+
stale: result.stale,
|
|
143
|
+
cachedAt: result.cached || result.stale ? result.timestamp : undefined,
|
|
144
|
+
dataDelayMs: 15 * 60_000,
|
|
145
|
+
});
|
|
139
146
|
if (rows.length === 0) {
|
|
140
147
|
return {
|
|
141
148
|
content: [
|
|
142
149
|
{
|
|
143
150
|
type: "text",
|
|
144
|
-
text:
|
|
151
|
+
text: [
|
|
152
|
+
"No stocks matched the screen. TradingView scanner data may be delayed about 15 minutes and comes from an unofficial endpoint.",
|
|
153
|
+
formatAsOfLine(freshness),
|
|
154
|
+
].join("\n"),
|
|
145
155
|
},
|
|
146
156
|
],
|
|
147
157
|
details: rows,
|
|
@@ -150,11 +160,12 @@ export const screenStocksTool: AgentTool<typeof params, ScreenerRow[]> = {
|
|
|
150
160
|
|
|
151
161
|
const lines = [
|
|
152
162
|
`**Stock screen** — ${rows.length} TradingView result${rows.length === 1 ? "" : "s"}`,
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
rows[0]?.dataCaveat ??
|
|
164
|
+
"TradingView scanner data may be delayed about 15 minutes and comes from an unofficial endpoint.",
|
|
155
165
|
formatInterpretationNote(normalized),
|
|
156
166
|
"",
|
|
157
167
|
...rows.map(formatRow),
|
|
168
|
+
formatAsOfLine(freshness),
|
|
158
169
|
];
|
|
159
170
|
|
|
160
171
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import { getConfig } from "../../config.js";
|
|
4
|
+
import { buildFreshnessStamp, type FreshnessStamp, formatAsOfLine } from "../../infra/freshness.js";
|
|
4
5
|
import { getGlobalQuote } from "../../providers/alpha-vantage.js";
|
|
5
6
|
import { withFallback } from "../../providers/with-fallback.js";
|
|
6
7
|
import { getQuote } from "../../providers/yahoo-finance.js";
|
|
@@ -10,54 +11,59 @@ const params = Type.Object({
|
|
|
10
11
|
symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT, TSLA)" }),
|
|
11
12
|
});
|
|
12
13
|
|
|
13
|
-
export const stockQuoteTool: AgentTool<typeof params, StockQuote> =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
export const stockQuoteTool: AgentTool<typeof params, StockQuote & { freshness: FreshnessStamp }> =
|
|
15
|
+
{
|
|
16
|
+
name: "get_stock_quote",
|
|
17
|
+
label: "Stock Quote",
|
|
18
|
+
description:
|
|
19
|
+
"Get real-time stock price, volume, market cap, and 52-week range for a ticker symbol",
|
|
20
|
+
parameters: params,
|
|
21
|
+
async execute(_toolCallId, args) {
|
|
22
|
+
const symbol = args.symbol.toUpperCase();
|
|
23
|
+
const apiKey = getConfig().alphaVantageApiKey;
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const entries = [
|
|
26
|
+
{ provider: "yahoo" as const, fn: () => getQuote(symbol) },
|
|
27
|
+
...(apiKey
|
|
28
|
+
? [{ provider: "alphavantage" as const, fn: () => getGlobalQuote(symbol, apiKey) }]
|
|
29
|
+
: []),
|
|
30
|
+
];
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
const result = await withFallback(entries);
|
|
33
|
+
if (result.status === "unavailable") {
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{ type: "text", text: `⚠ Stock quote unavailable for ${symbol} (${result.reason}).` },
|
|
37
|
+
],
|
|
38
|
+
details: null as any,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const quote = result.data;
|
|
42
|
+
const freshness = buildFreshnessStamp({
|
|
43
|
+
asOf: quote.asOf,
|
|
44
|
+
cached: result.cached,
|
|
45
|
+
stale: result.stale,
|
|
46
|
+
cachedAt: result.cached || result.stale ? result.timestamp : undefined,
|
|
47
|
+
});
|
|
48
|
+
const sign = quote.changePercent >= 0 ? "+" : "";
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
const week52 =
|
|
51
|
+
quote.week52High > 0 && quote.week52Low > 0
|
|
52
|
+
? `$${quote.week52Low.toFixed(2)} - $${quote.week52High.toFixed(2)}`
|
|
53
|
+
: "N/A";
|
|
54
|
+
const marketCapStr = quote.marketCap > 0 ? `$${formatLargeNumber(quote.marketCap)}` : "N/A";
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
const text = [
|
|
57
|
+
`${quote.symbol}: $${quote.price.toFixed(2)} (${sign}${quote.changePercent.toFixed(2)}%)`,
|
|
58
|
+
`Open: $${quote.open.toFixed(2)} | High: $${quote.high.toFixed(2)} | Low: $${quote.low.toFixed(2)}`,
|
|
59
|
+
`Volume: ${quote.volume.toLocaleString()} | Market Cap: ${marketCapStr}`,
|
|
60
|
+
`52W Range: ${week52}`,
|
|
61
|
+
formatAsOfLine(freshness),
|
|
62
|
+
].join("\n");
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return { content: [{ type: "text", text: prefix + text }], details: quote };
|
|
59
|
-
},
|
|
60
|
-
};
|
|
64
|
+
return { content: [{ type: "text", text }], details: { ...quote, freshness } };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
61
67
|
|
|
62
68
|
function formatLargeNumber(n: number): string {
|
|
63
69
|
if (n >= 1e12) return `${(n / 1e12).toFixed(2)}T`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { buildFreshnessStamp, type FreshnessStamp, formatAsOfLine } from "../../infra/freshness.js";
|
|
3
4
|
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
4
5
|
import { getOptionsChain } from "../../providers/yahoo-finance.js";
|
|
5
6
|
import type { OptionContract, OptionsChain } from "../../types/options.js";
|
|
@@ -22,7 +23,10 @@ const params = Type.Object({
|
|
|
22
23
|
),
|
|
23
24
|
});
|
|
24
25
|
|
|
25
|
-
export const optionChainTool: AgentTool<
|
|
26
|
+
export const optionChainTool: AgentTool<
|
|
27
|
+
typeof params,
|
|
28
|
+
OptionsChain & { freshness: FreshnessStamp }
|
|
29
|
+
> = {
|
|
26
30
|
name: "get_option_chain",
|
|
27
31
|
label: "Options Chain",
|
|
28
32
|
description:
|
|
@@ -43,6 +47,12 @@ export const optionChainTool: AgentTool<typeof params, OptionsChain> = {
|
|
|
43
47
|
};
|
|
44
48
|
}
|
|
45
49
|
const chain = result.data;
|
|
50
|
+
const freshness = buildFreshnessStamp({
|
|
51
|
+
asOf: chain.asOf,
|
|
52
|
+
cached: result.cached,
|
|
53
|
+
stale: result.stale,
|
|
54
|
+
cachedAt: result.cached || result.stale ? result.timestamp : undefined,
|
|
55
|
+
});
|
|
46
56
|
|
|
47
57
|
const lines: string[] = [
|
|
48
58
|
`**${chain.symbol} Options Chain** — Expiry: ${chain.expirationDate}`,
|
|
@@ -90,8 +100,12 @@ export const optionChainTool: AgentTool<typeof params, OptionsChain> = {
|
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
lines.push(`Put/Call Ratio: ${chain.putCallRatio.toFixed(2)}`);
|
|
103
|
+
lines.push(formatAsOfLine(freshness));
|
|
93
104
|
|
|
94
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
107
|
+
details: { ...chain, freshness },
|
|
108
|
+
};
|
|
95
109
|
},
|
|
96
110
|
};
|
|
97
111
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
findDefaultWatchlistReportTemplate,
|
|
5
5
|
nextDailyReportRunAt,
|
|
6
6
|
recordDailyWatchlistReportRun,
|
|
7
7
|
targetsDefaultWatchlist,
|
|
@@ -84,14 +84,20 @@ export const dailyReportTool: AgentTool<typeof params> = {
|
|
|
84
84
|
return { content: [{ type: "text", text: lines.join("\n") }], details: runs };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
// Manual runs link to the configured schedule when one exists but never
|
|
88
|
+
// create a template as a side effect — generation must not imply a
|
|
89
|
+
// schedule the user did not set up.
|
|
90
|
+
const template = findDefaultWatchlistReportTemplate(service);
|
|
88
91
|
const { report, run } = await recordDailyWatchlistReportRun(service, {
|
|
89
|
-
templateId: template
|
|
92
|
+
templateId: template?.id ?? null,
|
|
90
93
|
triggerType: "manual",
|
|
91
94
|
});
|
|
92
95
|
await deliverPendingNotifications(service);
|
|
96
|
+
const text = template
|
|
97
|
+
? report.text
|
|
98
|
+
: `${report.text}\n\n(Unscheduled manual run — use the configure action to set up a morning report that runs while OpenCandle is open.)`;
|
|
93
99
|
return {
|
|
94
|
-
content: [{ type: "text", text
|
|
100
|
+
content: [{ type: "text", text }],
|
|
95
101
|
details: run,
|
|
96
102
|
};
|
|
97
103
|
} finally {
|
|
@@ -169,5 +169,5 @@ function riskSummary(m: RiskMetrics): string {
|
|
|
169
169
|
if (m.sharpeRatio < 0) signals.push("Negative risk-adjusted returns");
|
|
170
170
|
if (m.sharpeRatio >= 1.5) signals.push("Strong risk-adjusted performance");
|
|
171
171
|
if (m.var95 > 0.03) signals.push("High daily VaR (>3%)");
|
|
172
|
-
return signals.length > 0 ?
|
|
172
|
+
return signals.length > 0 ? `Flags: ${signals.join(" | ")}` : "Risk profile appears moderate";
|
|
173
173
|
}
|
|
@@ -7,141 +7,165 @@ import { computeRSI, computeSMA } from "./indicators.js";
|
|
|
7
7
|
|
|
8
8
|
export type Strategy = "sma_crossover" | "sma_50_200_crossover" | "rsi_mean_reversion";
|
|
9
9
|
|
|
10
|
+
/** Flat per-side transaction cost assumption, in basis points. */
|
|
11
|
+
export const DEFAULT_COST_BPS = 5;
|
|
12
|
+
|
|
13
|
+
/** What this simulator deliberately does not model. Rendered in tool output. */
|
|
14
|
+
export const BACKTEST_LIMITATIONS = [
|
|
15
|
+
"Fills are simulated at the next bar's open after a signal; intrabar moves and gaps beyond that open are not modeled",
|
|
16
|
+
"Only a flat per-side cost is applied — no slippage model, spreads, or market impact",
|
|
17
|
+
"Dividends, taxes, and financing costs are excluded",
|
|
18
|
+
"Liquidity is assumed unlimited at the fill price",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export interface BacktestOptions {
|
|
22
|
+
/** Flat per-side cost in basis points applied to every fill. */
|
|
23
|
+
costBps?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BacktestTrade {
|
|
27
|
+
type: "buy" | "sell";
|
|
28
|
+
date: string;
|
|
29
|
+
price: number;
|
|
30
|
+
pnl?: number;
|
|
31
|
+
forced?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
10
34
|
export interface BacktestResult {
|
|
11
35
|
strategy: string;
|
|
12
36
|
totalReturn: number;
|
|
13
37
|
buyAndHoldReturn: number;
|
|
14
38
|
trades: number;
|
|
39
|
+
forcedClosures: number;
|
|
15
40
|
wins: number;
|
|
16
41
|
winRate: number;
|
|
17
42
|
maxDrawdown: number;
|
|
18
|
-
|
|
43
|
+
costBpsPerSide: number;
|
|
44
|
+
/** Signal generated on the final bar that has no next open to fill at. */
|
|
45
|
+
pendingSignal: { type: "buy" | "sell"; date: string } | null;
|
|
46
|
+
tradeLog: BacktestTrade[];
|
|
19
47
|
}
|
|
20
48
|
|
|
21
|
-
|
|
22
|
-
const closes = bars.map((b) => b.close);
|
|
49
|
+
type Signal = "enter" | "exit" | null;
|
|
23
50
|
|
|
24
|
-
|
|
25
|
-
|
|
51
|
+
export function runBacktest(
|
|
52
|
+
bars: OHLCV[],
|
|
53
|
+
strategy: Strategy,
|
|
54
|
+
options?: BacktestOptions,
|
|
55
|
+
): BacktestResult {
|
|
56
|
+
const closes = bars.map((b) => b.close);
|
|
57
|
+
const costBps = options?.costBps ?? DEFAULT_COST_BPS;
|
|
58
|
+
|
|
59
|
+
if (strategy === "sma_crossover" || strategy === "sma_50_200_crossover") {
|
|
60
|
+
const [shortWindow, longWindow] = strategy === "sma_crossover" ? [20, 50] : [50, 200];
|
|
61
|
+
const shortSma = computeSMA(closes, shortWindow);
|
|
62
|
+
const longSma = computeSMA(closes, longWindow);
|
|
63
|
+
if (longSma.length === 0) {
|
|
64
|
+
return emptyResult(strategy, closes, costBps);
|
|
65
|
+
}
|
|
66
|
+
const signalAt = (barIdx: number): Signal => {
|
|
67
|
+
const sShort = shortSma[barIdx - (shortWindow - 1)];
|
|
68
|
+
const sLong = longSma[barIdx - (longWindow - 1)];
|
|
69
|
+
if (sShort > sLong) return "enter";
|
|
70
|
+
if (sShort < sLong) return "exit";
|
|
71
|
+
return null;
|
|
72
|
+
};
|
|
73
|
+
return simulate(bars, closes, signalAt, longWindow - 1, strategy, costBps);
|
|
26
74
|
}
|
|
27
|
-
|
|
28
|
-
|
|
75
|
+
|
|
76
|
+
const rsi = computeRSI(closes, 14);
|
|
77
|
+
if (rsi.length === 0) {
|
|
78
|
+
return emptyResult("rsi_mean_reversion", closes, costBps);
|
|
29
79
|
}
|
|
30
|
-
|
|
80
|
+
const rsiOffset = 14;
|
|
81
|
+
const signalAt = (barIdx: number): Signal => {
|
|
82
|
+
const r = rsi[barIdx - rsiOffset];
|
|
83
|
+
if (r < 30) return "enter";
|
|
84
|
+
if (r > 70) return "exit";
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
return simulate(bars, closes, signalAt, rsiOffset, "rsi_mean_reversion", costBps);
|
|
31
88
|
}
|
|
32
89
|
|
|
33
|
-
|
|
90
|
+
// Shared fill engine: a signal computed from bar N's close fills at bar N+1's
|
|
91
|
+
// open (no same-bar lookahead), with a flat per-side cost on every fill. A
|
|
92
|
+
// signal on the final bar is reported as pending instead of a phantom trade.
|
|
93
|
+
function simulate(
|
|
34
94
|
bars: OHLCV[],
|
|
35
95
|
closes: number[],
|
|
36
|
-
|
|
37
|
-
|
|
96
|
+
signalAt: (barIdx: number) => Signal,
|
|
97
|
+
startIdx: number,
|
|
38
98
|
strategyName: Strategy,
|
|
99
|
+
costBps: number,
|
|
39
100
|
): BacktestResult {
|
|
40
|
-
const
|
|
41
|
-
const longSma = computeSMA(closes, longWindow);
|
|
42
|
-
|
|
43
|
-
if (longSma.length === 0) {
|
|
44
|
-
return emptyResult(strategyName, closes);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const shortOffset = shortWindow - 1;
|
|
48
|
-
const longOffset = longWindow - 1;
|
|
49
|
-
|
|
101
|
+
const cost = costBps / 10_000;
|
|
50
102
|
let position = false;
|
|
51
|
-
let
|
|
103
|
+
let entryFill = 0;
|
|
104
|
+
let pendingAction: Signal = null;
|
|
105
|
+
let pendingSignal: BacktestResult["pendingSignal"] = null;
|
|
52
106
|
const tradeLog: BacktestResult["tradeLog"] = [];
|
|
53
107
|
let equity = 1.0;
|
|
54
108
|
let peak = 1.0;
|
|
55
109
|
let maxDd = 0;
|
|
56
110
|
|
|
57
|
-
for (let i =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
111
|
+
for (let i = startIdx; i < bars.length; i++) {
|
|
112
|
+
// Execute the previous bar's signal at this bar's open.
|
|
113
|
+
if (pendingAction === "enter" && !position) {
|
|
114
|
+
const fillPrice = bars[i].open;
|
|
115
|
+
if (fillPrice > 0) {
|
|
116
|
+
position = true;
|
|
117
|
+
entryFill = fillPrice * (1 + cost);
|
|
118
|
+
tradeLog.push({ type: "buy", date: bars[i].date, price: fillPrice });
|
|
119
|
+
}
|
|
120
|
+
} else if (pendingAction === "exit" && position) {
|
|
121
|
+
const fillPrice = bars[i].open;
|
|
122
|
+
if (fillPrice > 0 && entryFill > 0) {
|
|
123
|
+
const pnl = (fillPrice * (1 - cost) - entryFill) / entryFill;
|
|
124
|
+
equity *= 1 + pnl;
|
|
125
|
+
tradeLog.push({ type: "sell", date: bars[i].date, price: fillPrice, pnl });
|
|
126
|
+
position = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
pendingAction = null;
|
|
130
|
+
|
|
131
|
+
// Evaluate the signal at this bar's close.
|
|
132
|
+
const signal = signalAt(i);
|
|
133
|
+
if ((signal === "enter" && !position) || (signal === "exit" && position)) {
|
|
134
|
+
if (i + 1 >= bars.length) {
|
|
135
|
+
pendingSignal = { type: signal === "enter" ? "buy" : "sell", date: bars[i].date };
|
|
136
|
+
} else {
|
|
137
|
+
pendingAction = signal;
|
|
138
|
+
}
|
|
75
139
|
}
|
|
76
140
|
|
|
77
|
-
// Track mark-to-market equity for accurate drawdown
|
|
78
|
-
const
|
|
141
|
+
// Track mark-to-market equity for accurate drawdown.
|
|
142
|
+
const price = closes[i];
|
|
143
|
+
const currentEquity =
|
|
144
|
+
position && entryFill > 0 && price > 0
|
|
145
|
+
? equity * (1 + (price - entryFill) / entryFill)
|
|
146
|
+
: equity;
|
|
79
147
|
if (currentEquity > peak) peak = currentEquity;
|
|
80
148
|
const dd = (peak - currentEquity) / peak;
|
|
81
149
|
if (dd > maxDd) maxDd = dd;
|
|
82
150
|
}
|
|
83
151
|
|
|
84
|
-
// Close open position at
|
|
85
|
-
if (position) {
|
|
152
|
+
// Close any open position at the final close for reporting.
|
|
153
|
+
if (position && entryFill > 0) {
|
|
86
154
|
const lastPrice = closes[closes.length - 1];
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
tradeLog.push({ type: "sell", date: bars[bars.length - 1].date, price: lastPrice, pnl });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return buildResult(strategyName, equity - 1, closes, tradeLog, maxDd);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function backtestRSIMeanReversion(bars: OHLCV[], closes: number[]): BacktestResult {
|
|
96
|
-
const rsi = computeRSI(closes, 14);
|
|
97
|
-
|
|
98
|
-
if (rsi.length === 0) {
|
|
99
|
-
return emptyResult("rsi_mean_reversion", closes);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// RSI starts at index 14 (after 14 periods of data)
|
|
103
|
-
const rsiOffset = 14;
|
|
104
|
-
let position = false;
|
|
105
|
-
let entryPrice = 0;
|
|
106
|
-
const tradeLog: BacktestResult["tradeLog"] = [];
|
|
107
|
-
let equity = 1.0;
|
|
108
|
-
let peak = 1.0;
|
|
109
|
-
let maxDd = 0;
|
|
110
|
-
|
|
111
|
-
for (let i = 0; i < rsi.length; i++) {
|
|
112
|
-
const barIdx = i + rsiOffset;
|
|
113
|
-
const r = rsi[i];
|
|
114
|
-
const price = closes[barIdx];
|
|
115
|
-
|
|
116
|
-
if (!position && r < 30 && price > 0) {
|
|
117
|
-
// RSI oversold → buy
|
|
118
|
-
position = true;
|
|
119
|
-
entryPrice = price;
|
|
120
|
-
tradeLog.push({ type: "buy", date: bars[barIdx].date, price });
|
|
121
|
-
} else if (position && r > 70) {
|
|
122
|
-
// RSI overbought → sell
|
|
123
|
-
const pnl = (price - entryPrice) / entryPrice;
|
|
155
|
+
if (lastPrice > 0) {
|
|
156
|
+
const pnl = (lastPrice * (1 - cost) - entryFill) / entryFill;
|
|
124
157
|
equity *= 1 + pnl;
|
|
125
|
-
tradeLog.push({
|
|
126
|
-
|
|
158
|
+
tradeLog.push({
|
|
159
|
+
type: "sell",
|
|
160
|
+
date: bars[bars.length - 1].date,
|
|
161
|
+
price: lastPrice,
|
|
162
|
+
pnl,
|
|
163
|
+
forced: true,
|
|
164
|
+
});
|
|
127
165
|
}
|
|
128
|
-
|
|
129
|
-
// Track mark-to-market equity for accurate drawdown
|
|
130
|
-
const currentEquity = position ? equity * (1 + (price - entryPrice) / entryPrice) : equity;
|
|
131
|
-
if (currentEquity > peak) peak = currentEquity;
|
|
132
|
-
const dd = (peak - currentEquity) / peak;
|
|
133
|
-
if (dd > maxDd) maxDd = dd;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Close open position at end
|
|
137
|
-
if (position) {
|
|
138
|
-
const lastPrice = closes[closes.length - 1];
|
|
139
|
-
const pnl = (lastPrice - entryPrice) / entryPrice;
|
|
140
|
-
equity *= 1 + pnl;
|
|
141
|
-
tradeLog.push({ type: "sell", date: bars[bars.length - 1].date, price: lastPrice, pnl });
|
|
142
166
|
}
|
|
143
167
|
|
|
144
|
-
return buildResult(
|
|
168
|
+
return buildResult(strategyName, equity - 1, closes, tradeLog, maxDd, costBps, pendingSignal);
|
|
145
169
|
}
|
|
146
170
|
|
|
147
171
|
function buildResult(
|
|
@@ -150,9 +174,15 @@ function buildResult(
|
|
|
150
174
|
closes: number[],
|
|
151
175
|
tradeLog: BacktestResult["tradeLog"],
|
|
152
176
|
maxDrawdown: number,
|
|
177
|
+
costBpsPerSide: number,
|
|
178
|
+
pendingSignal: BacktestResult["pendingSignal"],
|
|
153
179
|
): BacktestResult {
|
|
154
|
-
const sellTrades = tradeLog.filter(
|
|
155
|
-
|
|
180
|
+
const sellTrades = tradeLog.filter(
|
|
181
|
+
(t): t is BacktestTrade & { type: "sell"; pnl: number } =>
|
|
182
|
+
t.type === "sell" && t.pnl != null && !t.forced,
|
|
183
|
+
);
|
|
184
|
+
const forcedClosures = tradeLog.filter((t) => t.type === "sell" && t.forced).length;
|
|
185
|
+
const wins = sellTrades.filter((t) => t.pnl > 0).length;
|
|
156
186
|
const buyAndHoldReturn =
|
|
157
187
|
closes.length > 1 && closes[0] > 0 ? (closes[closes.length - 1] - closes[0]) / closes[0] : 0;
|
|
158
188
|
|
|
@@ -161,23 +191,29 @@ function buildResult(
|
|
|
161
191
|
totalReturn,
|
|
162
192
|
buyAndHoldReturn,
|
|
163
193
|
trades: sellTrades.length,
|
|
194
|
+
forcedClosures,
|
|
164
195
|
wins,
|
|
165
196
|
winRate: sellTrades.length > 0 ? wins / sellTrades.length : 0,
|
|
166
197
|
maxDrawdown,
|
|
198
|
+
costBpsPerSide,
|
|
199
|
+
pendingSignal,
|
|
167
200
|
tradeLog,
|
|
168
201
|
};
|
|
169
202
|
}
|
|
170
203
|
|
|
171
|
-
function emptyResult(strategy: string, closes: number[]): BacktestResult {
|
|
204
|
+
function emptyResult(strategy: string, closes: number[], costBpsPerSide: number): BacktestResult {
|
|
172
205
|
return {
|
|
173
206
|
strategy,
|
|
174
207
|
totalReturn: 0,
|
|
175
208
|
buyAndHoldReturn:
|
|
176
209
|
closes.length > 1 && closes[0] > 0 ? (closes[closes.length - 1] - closes[0]) / closes[0] : 0,
|
|
177
210
|
trades: 0,
|
|
211
|
+
forcedClosures: 0,
|
|
178
212
|
wins: 0,
|
|
179
213
|
winRate: 0,
|
|
180
214
|
maxDrawdown: 0,
|
|
215
|
+
costBpsPerSide,
|
|
216
|
+
pendingSignal: null,
|
|
181
217
|
tradeLog: [],
|
|
182
218
|
};
|
|
183
219
|
}
|
|
@@ -200,13 +236,20 @@ const params = Type.Object({
|
|
|
200
236
|
description: "Historical period to backtest: 1y, 2y, 5y. Default: 2y",
|
|
201
237
|
}),
|
|
202
238
|
),
|
|
239
|
+
cost_bps: Type.Optional(
|
|
240
|
+
Type.Number({
|
|
241
|
+
minimum: 0,
|
|
242
|
+
maximum: 100,
|
|
243
|
+
description: `Flat per-side transaction cost in basis points applied to every fill. Default: ${DEFAULT_COST_BPS}`,
|
|
244
|
+
}),
|
|
245
|
+
),
|
|
203
246
|
});
|
|
204
247
|
|
|
205
248
|
export const backtestTool: AgentTool<typeof params> = {
|
|
206
249
|
name: "backtest_strategy",
|
|
207
250
|
label: "Backtest Strategy",
|
|
208
251
|
description:
|
|
209
|
-
"
|
|
252
|
+
"Replay a simple trading strategy against historical daily bars. Signals fill at the next bar's open with a flat per-side cost assumption; dividends, taxes, slippage beyond that cost, and liquidity are not modeled. Supported strategies: SMA crossover (SMA20/SMA50), standard long-term SMA crossover (SMA50/SMA200), and RSI mean-reversion (buy <30, sell >70). Returns total return, win rate, max drawdown, and comparison to buy-and-hold.",
|
|
210
253
|
parameters: params,
|
|
211
254
|
async execute(_toolCallId, args) {
|
|
212
255
|
const symbol = args.symbol.toUpperCase();
|
|
@@ -235,22 +278,38 @@ export const backtestTool: AgentTool<typeof params> = {
|
|
|
235
278
|
};
|
|
236
279
|
}
|
|
237
280
|
|
|
238
|
-
const
|
|
281
|
+
const costBps = args.cost_bps ?? DEFAULT_COST_BPS;
|
|
282
|
+
const result = runBacktest(bars, args.strategy, { costBps });
|
|
239
283
|
|
|
240
284
|
const outperformance = result.totalReturn - result.buyAndHoldReturn;
|
|
241
285
|
const lines = [
|
|
242
286
|
`**${symbol} Backtest: ${strategyLabel(args.strategy)}** (${bars[0].date} to ${bars[bars.length - 1].date}, ${bars.length} days)`,
|
|
243
287
|
``,
|
|
244
|
-
`Strategy Return: ${(result.totalReturn * 100).toFixed(2)}%`,
|
|
245
|
-
`Buy & Hold Return: ${(result.buyAndHoldReturn * 100).toFixed(2)}%`,
|
|
288
|
+
`Strategy Return (net of costs): ${(result.totalReturn * 100).toFixed(2)}%`,
|
|
289
|
+
`Buy & Hold Return (gross): ${(result.buyAndHoldReturn * 100).toFixed(2)}%`,
|
|
246
290
|
`Outperformance: ${outperformance >= 0 ? "+" : ""}${(outperformance * 100).toFixed(2)}%`,
|
|
247
291
|
``,
|
|
248
|
-
`Trades: ${result.trades} | Wins: ${result.wins} | Win Rate: ${(result.winRate * 100).toFixed(0)}%`,
|
|
292
|
+
`Signal Trades: ${result.trades} | Forced Closures: ${result.forcedClosures} | Wins: ${result.wins} | Win Rate: ${(result.winRate * 100).toFixed(0)}%`,
|
|
249
293
|
`Max Drawdown: ${(result.maxDrawdown * 100).toFixed(2)}%`,
|
|
250
294
|
``,
|
|
295
|
+
`Execution assumptions: signals fill at the next bar's open with a flat cost of ${costBps} bps per side.`,
|
|
296
|
+
...(result.pendingSignal
|
|
297
|
+
? [
|
|
298
|
+
`Note: a ${result.pendingSignal.type} signal fired on the final bar (${result.pendingSignal.date}) and is pending — it has no next open to fill at and is not counted as a signal trade.`,
|
|
299
|
+
]
|
|
300
|
+
: []),
|
|
301
|
+
...(result.forcedClosures > 0
|
|
302
|
+
? [
|
|
303
|
+
`Note: ${result.forcedClosures} open position${result.forcedClosures === 1 ? " was" : "s were"} liquidated at the final close for return reporting; forced closures are not counted as signal trades.`,
|
|
304
|
+
]
|
|
305
|
+
: []),
|
|
306
|
+
``,
|
|
251
307
|
result.totalReturn > result.buyAndHoldReturn
|
|
252
|
-
? `
|
|
253
|
-
: `
|
|
308
|
+
? `In this simulation the strategy outperformed buy-and-hold by ${(outperformance * 100).toFixed(2)}%.`
|
|
309
|
+
: `In this simulation buy-and-hold outperformed the strategy by ${(-outperformance * 100).toFixed(2)}%.`,
|
|
310
|
+
``,
|
|
311
|
+
`Limitations — this is a strategy replay, not an execution simulation:`,
|
|
312
|
+
...BACKTEST_LIMITATIONS.map((limit) => `- ${limit}`),
|
|
254
313
|
];
|
|
255
314
|
|
|
256
315
|
return {
|