@backtest-kit/cli 5.11.0 โ 6.0.0
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 +90 -0
- package/build/index.cjs +254 -18
- package/build/index.mjs +256 -20
- package/package.json +13 -13
- package/template/project/CLAUDE.md +158 -0
- package/template/project/README.md +207 -0
- package/template/project/content/feb_2026.strategy.ts +114 -0
- package/template/project/docs/backtest_actions.md +158 -0
- package/template/project/docs/backtest_graph_multiple_outputs.md +137 -0
- package/template/project/docs/backtest_graph_pattern.md +235 -0
- package/template/project/docs/backtest_logging_jsonl.md +246 -0
- package/template/project/docs/backtest_pinets_usage.md +190 -0
- package/template/project/docs/backtest_risk_async.md +153 -0
- package/template/project/docs/backtest_strategy_structure.md +250 -0
- package/template/project/docs/pine_debug.md +174 -0
- package/template/project/docs/pine_indicator_warmup.md +88 -0
- package/template/project/gitignore.mustache +30 -0
- package/template/project/math/feb_2026.pine +120 -0
- package/template/project/modules/dump.module.ts +37 -0
- package/template/project/modules/pine.module.ts +37 -0
- package/template/project/package.mustache +27 -0
- package/template/project/report/feb_2026.md +78 -0
- package/template/project/scripts/fetch_docs.mjs +58 -0
- /package/template/project/{.gitkeep โ docs/lib/.gitkeep} +0 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# ๐งฟ Backtest Kit Project
|
|
2
|
+
|
|
3
|
+
> A TypeScript framework for backtesting and live trading strategies on multi-asset, crypto, forex or [DEX (peer-to-peer marketplace)](https://en.wikipedia.org/wiki/Decentralized_finance#Decentralized_exchanges), spot, futures with crash-safe persistence, signal validation, and AI optimization.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
[](https://deepwiki.com/tripolskypetr/backtest-kit)
|
|
8
|
+
[](https://npmjs.org/package/backtest-kit)
|
|
9
|
+
[]()
|
|
10
|
+
[](https://github.com/tripolskypetr/backtest-kit/actions/workflows/webpack.yml)
|
|
11
|
+
|
|
12
|
+
A minimal project scaffold for [backtest-kit](https://github.com/tripolskypetr/backtest-kit). All infrastructure (exchange registration, candle caching, runner, UI, Telegram) is handled by `@backtest-kit/cli` โ this project contains only your strategy files.
|
|
13
|
+
|
|
14
|
+
## ๐ Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm start # Run the CLI (append flags below)
|
|
18
|
+
npm run sync:lib # Refresh library docs in docs/lib/
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## ๐ Running Modes
|
|
22
|
+
|
|
23
|
+
All modes are invoked via `npm start -- <flags> <entry-point>`.
|
|
24
|
+
|
|
25
|
+
### ๐งช Backtest
|
|
26
|
+
|
|
27
|
+
Runs the strategy against historical candle data defined by a `FrameSchema`.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm start -- --backtest --symbol BTCUSDT --strategy feb_2026_strategy --exchange ccxt-exchange --frame feb_2026_frame ./content/feb_2026.strategy.ts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Flag | Type | Default | Description |
|
|
34
|
+
|------|------|---------|-------------|
|
|
35
|
+
| `--backtest` | boolean | โ | Enable backtest mode |
|
|
36
|
+
| `--symbol` | string | `BTCUSDT` | Trading pair |
|
|
37
|
+
| `--strategy` | string | first registered | Strategy name from `addStrategySchema` |
|
|
38
|
+
| `--exchange` | string | first registered | Exchange name from `addExchangeSchema` |
|
|
39
|
+
| `--frame` | string | first registered | Frame name from `addFrameSchema` |
|
|
40
|
+
| `--cacheInterval` | string | `1m, 15m, 30m, 1h, 4h` | Comma-separated intervals to pre-cache before the run |
|
|
41
|
+
| `--noCache` | boolean | `false` | Skip candle cache warming |
|
|
42
|
+
| `--verbose` | boolean | `false` | Log every candle fetch to stdout |
|
|
43
|
+
| `--ui` | boolean | `false` | Start web dashboard at `http://localhost:60050` |
|
|
44
|
+
| `--telegram` | boolean | `false` | Send trade notifications to Telegram |
|
|
45
|
+
|
|
46
|
+
Before the backtest starts, the CLI warms the candle cache for every interval in `--cacheInterval`. On subsequent runs the cache is used directly โ no extra API calls. Pass `--noCache` to skip this step.
|
|
47
|
+
|
|
48
|
+
Module file `./modules/backtest.module.ts` (or `.mjs`) is loaded automatically if it exists.
|
|
49
|
+
|
|
50
|
+
### ๐ Paper Trading
|
|
51
|
+
|
|
52
|
+
Connects to the live exchange but places no real orders. Identical code path to `--live` โ safe for strategy validation.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm start -- --paper --symbol BTCUSDT --strategy feb_2026_strategy --exchange ccxt-exchange ./content/feb_2026.strategy.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| Flag | Type | Default | Description |
|
|
59
|
+
|------|------|---------|-------------|
|
|
60
|
+
| `--paper` | boolean | โ | Enable paper trading mode |
|
|
61
|
+
| `--symbol` | string | `BTCUSDT` | Trading pair |
|
|
62
|
+
| `--strategy` | string | first registered | Strategy name |
|
|
63
|
+
| `--exchange` | string | first registered | Exchange name |
|
|
64
|
+
| `--verbose` | boolean | `false` | Log every candle fetch to stdout |
|
|
65
|
+
| `--ui` | boolean | `false` | Start web dashboard |
|
|
66
|
+
| `--telegram` | boolean | `false` | Enable Telegram notifications |
|
|
67
|
+
|
|
68
|
+
Module file `./modules/paper.module.ts` is loaded automatically if it exists.
|
|
69
|
+
|
|
70
|
+
### ๐ Live Trading
|
|
71
|
+
|
|
72
|
+
Deploys a real trading bot. Requires exchange API keys in `.env`.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm start -- --live --symbol BTCUSDT --ui --telegram ./content/feb_2026.strategy.ts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Flag | Type | Default | Description |
|
|
79
|
+
|------|------|---------|-------------|
|
|
80
|
+
| `--live` | boolean | โ | Enable live trading mode |
|
|
81
|
+
| `--symbol` | string | `BTCUSDT` | Trading pair |
|
|
82
|
+
| `--strategy` | string | first registered | Strategy name |
|
|
83
|
+
| `--exchange` | string | first registered | Exchange name |
|
|
84
|
+
| `--verbose` | boolean | `false` | Log every candle fetch to stdout |
|
|
85
|
+
| `--ui` | boolean | `false` | Start web dashboard |
|
|
86
|
+
| `--telegram` | boolean | `false` | Enable Telegram notifications |
|
|
87
|
+
|
|
88
|
+
Module file `./modules/live.module.ts` is loaded automatically if it exists. Use it to register a `Broker` adapter that intercepts every trade mutation before internal state changes โ exchange rejection rolls back the operation atomically.
|
|
89
|
+
|
|
90
|
+
## ๐ฒ Running PineScript Indicators (`--pine`)
|
|
91
|
+
|
|
92
|
+
Executes a local `.pine` file against a real exchange and prints the output as a Markdown table or saves it to a file.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm start -- --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --when "2026-02-28T00:00:00.000Z" --jsonl
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Output file is created at `./math/dump/<name>.jsonl` (next to the `.pine` file).
|
|
99
|
+
|
|
100
|
+
| Flag | Type | Default | Description |
|
|
101
|
+
|------|------|---------|-------------|
|
|
102
|
+
| `--pine` | boolean | โ | Enable PineScript execution mode |
|
|
103
|
+
| `--symbol` | string | `BTCUSDT` | Trading pair |
|
|
104
|
+
| `--timeframe` | string | `15m` | Candle interval |
|
|
105
|
+
| `--limit` | string | `250` | Number of candles to fetch |
|
|
106
|
+
| `--when` | string | now | End date โ ISO 8601 or Unix ms |
|
|
107
|
+
| `--exchange` | string | first registered | Exchange name |
|
|
108
|
+
| `--output` | string | `.pine` file name | Output file base name (no extension) |
|
|
109
|
+
| `--json` | boolean | `false` | Save output as JSON array |
|
|
110
|
+
| `--jsonl` | boolean | `false` | Save output as JSONL (one row per line) |
|
|
111
|
+
| `--markdown` | boolean | `false` | Save output as Markdown table |
|
|
112
|
+
|
|
113
|
+
Module file `./modules/pine.module.ts` is loaded automatically. The project includes it pre-configured with CCXT Binance. Override it to use a different exchange.
|
|
114
|
+
|
|
115
|
+
Only `plot()` calls with `display=display.data_window` produce output columns:
|
|
116
|
+
|
|
117
|
+
```pine
|
|
118
|
+
plot(close, "Close", display=display.data_window)
|
|
119
|
+
plot(position, "Position", display=display.data_window)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## ๐พ Dumping Raw Candles (`--dump`)
|
|
123
|
+
|
|
124
|
+
Fetches raw OHLCV candles from an exchange and saves them to a file.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm start -- --dump --timeframe 15m --limit 500 --when "2026-02-28T00:00:00.000Z" --jsonl
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Output file is created at `./dump/<name>.jsonl`.
|
|
131
|
+
|
|
132
|
+
| Flag | Type | Default | Description |
|
|
133
|
+
|------|------|---------|-------------|
|
|
134
|
+
| `--dump` | boolean | โ | Enable candle dump mode |
|
|
135
|
+
| `--symbol` | string | `BTCUSDT` | Trading pair |
|
|
136
|
+
| `--timeframe` | string | `15m` | Candle interval |
|
|
137
|
+
| `--limit` | string | `250` | Number of candles |
|
|
138
|
+
| `--when` | string | now | End date โ ISO 8601 or Unix ms |
|
|
139
|
+
| `--exchange` | string | first registered | Exchange name |
|
|
140
|
+
| `--output` | string | `{SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP}` | Output file base name |
|
|
141
|
+
| `--json` | boolean | `false` | Save as JSON array |
|
|
142
|
+
| `--jsonl` | boolean | `false` | Save as JSONL |
|
|
143
|
+
|
|
144
|
+
Module file `./modules/dump.module.ts` is loaded automatically. The project includes it pre-configured with CCXT Binance.
|
|
145
|
+
|
|
146
|
+
## ๐งฉ Module Hooks
|
|
147
|
+
|
|
148
|
+
| File | Loaded by mode | Purpose |
|
|
149
|
+
|------|----------------|---------|
|
|
150
|
+
| `modules/backtest.module.ts` | `--backtest` | Register a `Broker` adapter for backtest |
|
|
151
|
+
| `modules/paper.module.ts` | `--paper` | Register a `Broker` adapter for paper trading |
|
|
152
|
+
| `modules/live.module.ts` | `--live` | Register a `Broker` adapter for live trading |
|
|
153
|
+
| `modules/pine.module.ts` | `--pine` | Register an exchange schema for PineScript runs |
|
|
154
|
+
| `modules/dump.module.ts` | `--dump` | Register an exchange schema for candle dumps |
|
|
155
|
+
|
|
156
|
+
All files are optional โ a missing module is a soft warning, not an error. Extensions `.ts`, `.mjs`, `.cjs` are tried automatically.
|
|
157
|
+
|
|
158
|
+
## ๐ Environment Variables
|
|
159
|
+
|
|
160
|
+
Create a `.env` file in the project root:
|
|
161
|
+
|
|
162
|
+
```env
|
|
163
|
+
# Telegram notifications (required for --telegram)
|
|
164
|
+
CC_TELEGRAM_TOKEN=your_bot_token_here
|
|
165
|
+
CC_TELEGRAM_CHANNEL=-100123456789
|
|
166
|
+
|
|
167
|
+
# Web UI server (optional, defaults shown)
|
|
168
|
+
CC_WWWROOT_HOST=0.0.0.0
|
|
169
|
+
CC_WWWROOT_PORT=60050
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
| Variable | Default | Description |
|
|
173
|
+
|----------|---------|-------------|
|
|
174
|
+
| `CC_TELEGRAM_TOKEN` | โ | Telegram bot token (from @BotFather) |
|
|
175
|
+
| `CC_TELEGRAM_CHANNEL` | โ | Telegram channel or chat ID |
|
|
176
|
+
| `CC_WWWROOT_HOST` | `0.0.0.0` | UI server bind address |
|
|
177
|
+
| `CC_WWWROOT_PORT` | `60050` | UI server port |
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
## ๐๏ธ Project Structure
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
โโโ content/ # Strategy entry points (.ts)
|
|
184
|
+
โ โโโ feb_2026.strategy.ts
|
|
185
|
+
โโโ docs/ # Documentation
|
|
186
|
+
โ โโโ lib/ # Auto-fetched library READMEs (via sync:lib)
|
|
187
|
+
โ โโโ *.md # Backtest Kit how-to guides
|
|
188
|
+
โโโ math/ # PineScript indicator files (.pine)
|
|
189
|
+
โ โโโ feb_2026.pine
|
|
190
|
+
โโโ modules/ # Side-effect module hooks (loaded automatically)
|
|
191
|
+
โ โโโ dump.module.ts # Exchange schema for --dump mode
|
|
192
|
+
โ โโโ pine.module.ts # Exchange schema for --pine mode
|
|
193
|
+
โโโ report/ # Strategy research reports (.md)
|
|
194
|
+
โ โโโ feb_2026.md
|
|
195
|
+
โโโ scripts/
|
|
196
|
+
โ โโโ fetch_docs.mjs # Downloads library READMEs into docs/lib/
|
|
197
|
+
โโโ CLAUDE.md # AI-agent guide for writing strategies
|
|
198
|
+
โโโ package.json
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## ๐ Updating Library Documentation
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npm run sync:lib
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Downloads the latest README files for all bundled libraries into `docs/lib/`. Run this after updating package versions or when you want fresh documentation available to the AI agent.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addExchangeSchema,
|
|
3
|
+
addFrameSchema,
|
|
4
|
+
addStrategySchema,
|
|
5
|
+
listenError,
|
|
6
|
+
Cache,
|
|
7
|
+
Log,
|
|
8
|
+
} from "backtest-kit";
|
|
9
|
+
import {
|
|
10
|
+
errorData,
|
|
11
|
+
getErrorMessage,
|
|
12
|
+
randomString,
|
|
13
|
+
singleshot,
|
|
14
|
+
} from "functools-kit";
|
|
15
|
+
import ccxt from "ccxt";
|
|
16
|
+
import { run, File, extract } from "@backtest-kit/pinets";
|
|
17
|
+
import { outputNode, resolve, sourceNode } from "@backtest-kit/graph";
|
|
18
|
+
|
|
19
|
+
const getExchange = singleshot(async () => {
|
|
20
|
+
const exchange = new ccxt.binance({
|
|
21
|
+
options: {
|
|
22
|
+
defaultType: "spot",
|
|
23
|
+
adjustForTimeDifference: true,
|
|
24
|
+
recvWindow: 60000,
|
|
25
|
+
},
|
|
26
|
+
enableRateLimit: true,
|
|
27
|
+
});
|
|
28
|
+
await exchange.loadMarkets();
|
|
29
|
+
return exchange;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const pineSource = sourceNode(
|
|
33
|
+
Cache.fn(
|
|
34
|
+
async (symbol) => {
|
|
35
|
+
const plots = await run(File.fromPath("feb_2026.pine", "../math"), {
|
|
36
|
+
symbol,
|
|
37
|
+
timeframe: "15m",
|
|
38
|
+
limit: 2688,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return await extract(plots, {
|
|
42
|
+
position: "Position",
|
|
43
|
+
entryPrice: "EntryPrice",
|
|
44
|
+
tp: "TP",
|
|
45
|
+
sl: "SL",
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
{ interval: "15m", key: ([symbol]) => symbol },
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const signalOutput = outputNode(async ([pineSource]) => {
|
|
53
|
+
const position =
|
|
54
|
+
pineSource.position === -1
|
|
55
|
+
? "short"
|
|
56
|
+
: pineSource.position === 1
|
|
57
|
+
? "long"
|
|
58
|
+
: "wait";
|
|
59
|
+
|
|
60
|
+
if (position === "wait") {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
id: randomString(),
|
|
66
|
+
position,
|
|
67
|
+
priceOpen: pineSource.entryPrice,
|
|
68
|
+
priceTakeProfit: pineSource.tp,
|
|
69
|
+
priceStopLoss: pineSource.sl,
|
|
70
|
+
minuteEstimatedTime: Infinity,
|
|
71
|
+
} as const;
|
|
72
|
+
}, pineSource);
|
|
73
|
+
|
|
74
|
+
addExchangeSchema({
|
|
75
|
+
exchangeName: "ccxt-exchange",
|
|
76
|
+
getCandles: async (symbol, interval, since, limit) => {
|
|
77
|
+
const exchange = await getExchange();
|
|
78
|
+
const candles = await exchange.fetchOHLCV(
|
|
79
|
+
symbol,
|
|
80
|
+
interval,
|
|
81
|
+
since.getTime(),
|
|
82
|
+
limit,
|
|
83
|
+
);
|
|
84
|
+
return candles.map(([timestamp, open, high, low, close, volume]) => ({
|
|
85
|
+
timestamp,
|
|
86
|
+
open,
|
|
87
|
+
high,
|
|
88
|
+
low,
|
|
89
|
+
close,
|
|
90
|
+
volume,
|
|
91
|
+
}));
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
addFrameSchema({
|
|
96
|
+
frameName: "feb_2026_frame",
|
|
97
|
+
interval: "1m",
|
|
98
|
+
startDate: new Date("2026-02-01T00:00:00Z"),
|
|
99
|
+
endDate: new Date("2026-02-28T23:59:59Z"),
|
|
100
|
+
note: "February 2026",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
addStrategySchema({
|
|
104
|
+
strategyName: "feb_2026_strategy",
|
|
105
|
+
interval: "1m",
|
|
106
|
+
getSignal: async () => await resolve(signalOutput),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
listenError((error) => {
|
|
110
|
+
Log.debug("error", {
|
|
111
|
+
error: errorData(error),
|
|
112
|
+
message: getErrorMessage(error),
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Actions (addActionSchema / IPublicAction / pingActive)
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Actions are lifecycle hooks that run while a position is open. They are registered with `addActionSchema` and attached to a strategy via `actions` array. The most common use is `pingActive` โ called on every tick while a pending signal exists.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
addActionSchema,
|
|
10
|
+
commitClosePending,
|
|
11
|
+
getPendingSignal,
|
|
12
|
+
IPublicAction,
|
|
13
|
+
ActivePingContract,
|
|
14
|
+
} from "backtest-kit";
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## `addActionSchema`
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
addActionSchema({
|
|
23
|
+
actionName: "my_action",
|
|
24
|
+
handler: class implements IPublicAction {
|
|
25
|
+
async pingActive(event: ActivePingContract) {
|
|
26
|
+
// called every tick while a position is open
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Attach to strategy:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
addStrategySchema({
|
|
36
|
+
strategyName: "my_strategy",
|
|
37
|
+
interval: "15m",
|
|
38
|
+
getSignal: ...,
|
|
39
|
+
actions: ["my_action"],
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## `ActivePingContract`
|
|
46
|
+
|
|
47
|
+
Fields available inside `pingActive`:
|
|
48
|
+
|
|
49
|
+
| Field | Type | Description |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `symbol` | `string` | Trading pair (e.g. `"BTCUSDT"`) |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## `getPendingSignal(symbol)`
|
|
56
|
+
|
|
57
|
+
Returns the currently active pending signal. Use it to read `position`, `priceOpen`, `priceTakeProfit`, `priceStopLoss`.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const pendingSignal = await getPendingSignal(event.symbol);
|
|
61
|
+
pendingSignal.position // "long" | "short"
|
|
62
|
+
pendingSignal.priceOpen // entry price
|
|
63
|
+
pendingSignal.priceTakeProfit
|
|
64
|
+
pendingSignal.priceStopLoss
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## `commitClosePending(symbol)`
|
|
70
|
+
|
|
71
|
+
Closes the active pending signal immediately (market close).
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
await commitClosePending(event.symbol);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Pattern: Close on trend reversal
|
|
80
|
+
|
|
81
|
+
Close an open position when the trend indicator reverses against it.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
addActionSchema({
|
|
85
|
+
actionName: "trend_reversal_close",
|
|
86
|
+
handler: class implements IPublicAction {
|
|
87
|
+
async pingActive(event: ActivePingContract) {
|
|
88
|
+
const shouldClose = await resolve(exitSignal);
|
|
89
|
+
if (shouldClose) {
|
|
90
|
+
await commitClosePending(event.symbol);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Where `exitSignal` is an output node that returns `boolean`:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const exitSignal = outputNode(
|
|
101
|
+
async ([trend, pendingSignal]) => {
|
|
102
|
+
if (!pendingSignal) return false;
|
|
103
|
+
if (pendingSignal.position === "long" && trend.position === -1) return true;
|
|
104
|
+
if (pendingSignal.position === "short" && trend.position === 1) return true;
|
|
105
|
+
return false;
|
|
106
|
+
},
|
|
107
|
+
masterTrendSource,
|
|
108
|
+
pendingSignalSource,
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
See `backtest_graph_multiple_outputs.md` for the full pattern with `enterSignal` / `exitSignal`.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Other `IPublicAction` lifecycle methods
|
|
117
|
+
|
|
118
|
+
Beyond `pingActive`, actions can implement:
|
|
119
|
+
|
|
120
|
+
| Method | When called |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `pingActive(event)` | Every tick while position is open |
|
|
123
|
+
| `onOpen(event)` | Position just opened |
|
|
124
|
+
| `onClose(event)` | Position closed (TP/SL/manual) |
|
|
125
|
+
| `onIdle(event)` | Every tick when no position is open |
|
|
126
|
+
|
|
127
|
+
All methods are optional โ implement only what you need.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Pattern: DCA (dollar-cost averaging)
|
|
132
|
+
|
|
133
|
+
From `feb_2024.strategy.ts`:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
addActionSchema({
|
|
137
|
+
actionName: "long_dollar_cost_averaging",
|
|
138
|
+
handler: class implements IPublicAction {
|
|
139
|
+
async pingActive(event: ActivePingContract) {
|
|
140
|
+
const pendingSignal = await getPendingSignal(event.symbol);
|
|
141
|
+
if (pendingSignal.position !== "long") return;
|
|
142
|
+
if (pendingSignal.totalEntries > 5) return;
|
|
143
|
+
const currentPrice = await getAveragePrice(event.symbol);
|
|
144
|
+
if (currentPrice > pendingSignal.originalPriceOpen) return;
|
|
145
|
+
if (await getPositionEntryOverlap(event.symbol, currentPrice, {
|
|
146
|
+
lowerPercent: 0.5, upperPercent: 0.5,
|
|
147
|
+
})) return;
|
|
148
|
+
await commitAverageBuy(event.symbol);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Key guards before DCA:
|
|
155
|
+
1. `position !== "long"` โ skip if wrong direction
|
|
156
|
+
2. `totalEntries > 5` โ cap at 5 entries
|
|
157
|
+
3. `currentPrice > originalPriceOpen` โ only average down, not up
|
|
158
|
+
4. `getPositionEntryOverlap` โ avoid averaging too close to an existing entry
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Graph Pattern: Multiple Output Nodes (enterSignal / exitSignal)
|
|
2
|
+
|
|
3
|
+
## Why Two Output Nodes
|
|
4
|
+
|
|
5
|
+
A strategy can have separate output nodes for entry and exit logic. This keeps concerns separated and allows `exitSignal` to be reused in both an action (`pingActive`) and risk validators independently.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
masterTrendSource โโโฌโโโบ enterSignal โ getSignal()
|
|
9
|
+
โโโโบ exitSignal โ pingActive() in action
|
|
10
|
+
fundamentalSource โโโบ enterSignal
|
|
11
|
+
pendingSignalSource โโโบ exitSignal
|
|
12
|
+
garchSource โโโโโโโโโบ risk validator (resolve directly)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Full Pattern
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { sourceNode, outputNode, resolve } from "@backtest-kit/graph";
|
|
21
|
+
import { getPendingSignal, commitClosePending } from "backtest-kit";
|
|
22
|
+
|
|
23
|
+
// --- Source nodes ---
|
|
24
|
+
|
|
25
|
+
const masterTrendSource = sourceNode(
|
|
26
|
+
Cache.fn(
|
|
27
|
+
async (symbol: string) => {
|
|
28
|
+
const plots = await run(
|
|
29
|
+
File.fromPath("master_trend_15m.pine", "../../math"),
|
|
30
|
+
{ symbol, timeframe: "15m", limit: 180 },
|
|
31
|
+
);
|
|
32
|
+
return extract(plots, {
|
|
33
|
+
position: "Position", // -1 | 0 | 1
|
|
34
|
+
close: "Close",
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
{ interval: "15m", key: ([symbol]: [string]) => symbol },
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Raw source node โ no cache, reads live state every tick
|
|
42
|
+
const pendingSignalSource = sourceNode(
|
|
43
|
+
async (symbol) => await getPendingSignal(symbol),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// --- Output node: entry ---
|
|
47
|
+
|
|
48
|
+
const enterSignal = outputNode(
|
|
49
|
+
async ([trend, fundamental]) => {
|
|
50
|
+
if (fundamental.position === "wait") return null;
|
|
51
|
+
if (trend.position === 0) return null;
|
|
52
|
+
const fundamentalDir = fundamental.position === "long" ? 1 : -1;
|
|
53
|
+
if (fundamentalDir !== trend.position) return null;
|
|
54
|
+
|
|
55
|
+
const position = fundamentalDir === 1 ? "long" : "short";
|
|
56
|
+
const price = trend.close;
|
|
57
|
+
return {
|
|
58
|
+
id: randomString(),
|
|
59
|
+
position,
|
|
60
|
+
priceTakeProfit: position === "long" ? price * 1.05 : price * 0.95,
|
|
61
|
+
priceStopLoss: position === "long" ? price * 0.95 : price * 1.05,
|
|
62
|
+
minuteEstimatedTime: 480,
|
|
63
|
+
} as const;
|
|
64
|
+
},
|
|
65
|
+
masterTrendSource,
|
|
66
|
+
fundamentalSource,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// --- Output node: exit ---
|
|
70
|
+
|
|
71
|
+
const exitSignal = outputNode(
|
|
72
|
+
async ([trend, pendingSignal]) => {
|
|
73
|
+
if (!pendingSignal) return false;
|
|
74
|
+
if (pendingSignal.position === "long" && trend.position === -1) return true;
|
|
75
|
+
if (pendingSignal.position === "short" && trend.position === 1) return true;
|
|
76
|
+
return false;
|
|
77
|
+
},
|
|
78
|
+
masterTrendSource, // shared with enterSignal โ resolved once per tick
|
|
79
|
+
pendingSignalSource,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// --- Action uses exitSignal ---
|
|
83
|
+
|
|
84
|
+
addActionSchema({
|
|
85
|
+
actionName: "trend_reversal_close",
|
|
86
|
+
handler: class implements IPublicAction {
|
|
87
|
+
async pingActive(event: ActivePingContract) {
|
|
88
|
+
const shouldClose = await resolve(exitSignal);
|
|
89
|
+
if (shouldClose) {
|
|
90
|
+
await commitClosePending(event.symbol);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// --- Strategy wires enterSignal ---
|
|
97
|
+
|
|
98
|
+
addStrategySchema({
|
|
99
|
+
strategyName: "feb_2026_strategy",
|
|
100
|
+
interval: "15m",
|
|
101
|
+
getSignal: () => resolve(enterSignal),
|
|
102
|
+
actions: ["trend_reversal_close"],
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Shared Node Deduplication
|
|
109
|
+
|
|
110
|
+
`masterTrendSource` is a dependency of both `enterSignal` and `exitSignal`. When both are resolved in the same tick, `@backtest-kit/graph` deduplicates by reference โ the Pine script runs **once**, not twice.
|
|
111
|
+
|
|
112
|
+
This is the main reason to split logic into multiple output nodes rather than one large node: each source is computed once regardless of how many output nodes depend on it.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Raw sourceNode (no Cache.fn)
|
|
117
|
+
|
|
118
|
+
`pendingSignalSource` has no `Cache.fn` wrapper:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const pendingSignalSource = sourceNode(
|
|
122
|
+
async (symbol) => await getPendingSignal(symbol),
|
|
123
|
+
);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Use this pattern when:
|
|
127
|
+
- The data changes every tick and must not be cached (live position state)
|
|
128
|
+
- The source is cheap to fetch (single in-memory lookup)
|
|
129
|
+
- Caching would return stale data within the same candle
|
|
130
|
+
|
|
131
|
+
Contrast with `masterTrendSource` which uses `Cache.fn` with `interval: "15m"` โ Pine runs once per 15m bar and the result is reused for all ticks within that bar.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## `exitSignal` return type
|
|
136
|
+
|
|
137
|
+
`exitSignal` returns `boolean`, not `ISignalDto | null`. Output nodes are not limited to signal shapes โ they can return any value that the consumer needs. The action reads `boolean`, the risk validator could read a number, etc.
|