@backtest-kit/graph 9.5.0 → 9.7.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 +306 -306
- package/package.json +86 -86
package/README.md
CHANGED
|
@@ -1,306 +1,306 @@
|
|
|
1
|
-
<img src="https://github.com/tripolskypetr/backtest-kit/raw/refs/heads/master/assets/assignation.svg" height="45px" align="right">
|
|
2
|
-
|
|
3
|
-
# 📊 @backtest-kit/graph
|
|
4
|
-
|
|
5
|
-
> Compose backtest-kit computations as a typed directed acyclic graph. Define source nodes that fetch market data and output nodes that compute derived values — then resolve the whole graph in topological order.
|
|
6
|
-
|
|
7
|
-

|
|
8
|
-
|
|
9
|
-
[](https://deepwiki.com/tripolskypetr/backtest-kit)
|
|
10
|
-
[](https://npmjs.org/package/@backtest-kit/graph)
|
|
11
|
-
[]()
|
|
12
|
-
|
|
13
|
-
📚 **[Backtest Kit Docs](https://backtest-kit.github.io/documents/article_07_ai_news_trading_signals.html)** | 🌟 **[GitHub](https://github.com/tripolskypetr/backtest-kit)**
|
|
14
|
-
|
|
15
|
-
> **New to backtest-kit?** The fastest way to get a real, production-ready setup is to clone the [reference implementation](https://github.com/tripolskypetr/backtest-kit/tree/master/example) — a fully working news-sentiment AI trading system with LLM forecasting, multi-timeframe data, and a documented February 2026 backtest. Start there instead of from scratch.
|
|
16
|
-
|
|
17
|
-
## 🔥 Multi-timeframe Pine Script strategy
|
|
18
|
-
|
|
19
|
-
The graph below replicates a two-timeframe strategy: a 4h Pine Script acts as a trend filter, a 15m Pine Script generates the entry signal. `outputNode` combines them and returns `null` when the trend disagrees.
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
import { extract, run, toSignalDto, File } from '@backtest-kit/pinets';
|
|
23
|
-
import { addStrategySchema, Cache } from 'backtest-kit';
|
|
24
|
-
import { randomString } from 'functools-kit';
|
|
25
|
-
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
26
|
-
|
|
27
|
-
// SourceNode — 4h trend filter, cached per candle interval
|
|
28
|
-
const higherTimeframe = sourceNode(
|
|
29
|
-
Cache.fn(
|
|
30
|
-
async (symbol) => {
|
|
31
|
-
const plots = await run(File.fromPath('timeframe_4h.pine'), {
|
|
32
|
-
symbol,
|
|
33
|
-
timeframe: '4h',
|
|
34
|
-
limit: 100,
|
|
35
|
-
});
|
|
36
|
-
return extract(plots, {
|
|
37
|
-
allowLong: 'AllowLong',
|
|
38
|
-
allowShort: 'AllowShort',
|
|
39
|
-
noTrades: 'NoTrades',
|
|
40
|
-
});
|
|
41
|
-
},
|
|
42
|
-
{ interval: '4h', key: ([symbol]) => symbol },
|
|
43
|
-
),
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
// SourceNode — 15m entry signal, cached per candle interval
|
|
47
|
-
const lowerTimeframe = sourceNode(
|
|
48
|
-
Cache.fn(
|
|
49
|
-
async (symbol) => {
|
|
50
|
-
const plots = await run(File.fromPath('timeframe_15m.pine'), {
|
|
51
|
-
symbol,
|
|
52
|
-
timeframe: '15m',
|
|
53
|
-
limit: 100,
|
|
54
|
-
});
|
|
55
|
-
return extract(plots, {
|
|
56
|
-
position: 'Signal',
|
|
57
|
-
priceOpen: 'Close',
|
|
58
|
-
priceTakeProfit: 'TakeProfit',
|
|
59
|
-
priceStopLoss: 'StopLoss',
|
|
60
|
-
minuteEstimatedTime: 'EstimatedTime',
|
|
61
|
-
});
|
|
62
|
-
},
|
|
63
|
-
{ interval: '15m', key: ([symbol]) => symbol },
|
|
64
|
-
),
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
// OutputNode — applies MTF filter, returns ISignalDto or null
|
|
68
|
-
const mtfSignal = outputNode(
|
|
69
|
-
async ([higher, lower]) => {
|
|
70
|
-
if (higher.noTrades) return null;
|
|
71
|
-
if (lower.position === 0) return null;
|
|
72
|
-
if (higher.allowShort && lower.position === 1) return null;
|
|
73
|
-
if (higher.allowLong && lower.position === -1) return null;
|
|
74
|
-
|
|
75
|
-
return toSignalDto(randomString(), lower, null);
|
|
76
|
-
},
|
|
77
|
-
higherTimeframe,
|
|
78
|
-
lowerTimeframe,
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
addStrategySchema({
|
|
82
|
-
strategyName: 'mtf_graph_strategy',
|
|
83
|
-
interval: '5m',
|
|
84
|
-
getSignal: (symbol) => resolve(mtfSignal),
|
|
85
|
-
actions: ['partial_profit_action', 'breakeven_action'],
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
The graph resolves both Pine Script nodes **in parallel** via `Promise.all`, then passes their typed results to `compute`. Replacing either timeframe script or adding a third filter node requires no changes to the strategy wiring.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
## 🚀 Installation
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
npm install @backtest-kit/graph backtest-kit
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## ✨ Features
|
|
99
|
-
|
|
100
|
-
- 📊 **DAG execution**: Nodes are resolved bottom-up in topological order with `Promise.all` parallelism
|
|
101
|
-
- 🔒 **Type-safe values**: TypeScript infers the return type of every node through the graph via generics
|
|
102
|
-
- 🧱 **Two APIs**: Low-level `INode` for runtime/storage, high-level `TypedNode` + builders for authoring
|
|
103
|
-
- 💾 **DB-ready serialization**: `serialize` / `deserialize` convert the graph to a flat `IFlatNode[]` list with `id` / `nodeIds`
|
|
104
|
-
- 🔌 **Context-aware fetch**: `SourceNode.fetch` receives `(symbol, when, currentPrice, exchangeName)` from the execution context automatically
|
|
105
|
-
|
|
106
|
-
## 📖 Usage
|
|
107
|
-
|
|
108
|
-
### Quick Start — builder API
|
|
109
|
-
|
|
110
|
-
Use `sourceNode` and `outputNode` to define a typed computation graph. TypeScript infers the type of `values` in `compute` from the `nodes` passed to `outputNode`:
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
114
|
-
|
|
115
|
-
// SourceNode<number> — fetch receives symbol, when, currentPrice, exchangeName from context
|
|
116
|
-
const closePrice = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
117
|
-
const candles = await getCandles(symbol, '1h', 1, exchangeName);
|
|
118
|
-
return candles[0].close; // number
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// SourceNode<number>
|
|
122
|
-
const volume = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
123
|
-
const candles = await getCandles(symbol, '1h', 1, exchangeName);
|
|
124
|
-
return candles[0].volume; // number
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// OutputNode<[SourceNode<number>, SourceNode<number>], number>
|
|
128
|
-
// price and vol are automatically number
|
|
129
|
-
const vwap = outputNode(
|
|
130
|
-
([price, vol]) => price * vol,
|
|
131
|
-
closePrice,
|
|
132
|
-
volume,
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// Resolve inside a backtest-kit strategy
|
|
136
|
-
const result = await resolve(vwap); // Promise<number>
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Inline anonymous composition
|
|
140
|
-
|
|
141
|
-
The entire graph can be defined as a single object literal.
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
import { NodeType } from '@backtest-kit/graph';
|
|
145
|
-
import { TypedNode, resolve } from '@backtest-kit/graph';
|
|
146
|
-
|
|
147
|
-
const signal: TypedNode = {
|
|
148
|
-
type: NodeType.OutputNode,
|
|
149
|
-
nodes: [
|
|
150
|
-
{
|
|
151
|
-
type: NodeType.SourceNode,
|
|
152
|
-
fetch: async (symbol, when, currentPrice, exchangeName) => {
|
|
153
|
-
const plots = await run(File.fromPath('timeframe_4h.pine'), { symbol, timeframe: '4h', limit: 100 });
|
|
154
|
-
return extract(plots, { allowLong: 'AllowLong', allowShort: 'AllowShort', noTrades: 'NoTrades' });
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
type: NodeType.SourceNode,
|
|
159
|
-
fetch: async (symbol, when, currentPrice, exchangeName) => {
|
|
160
|
-
const plots = await run(File.fromPath('timeframe_15m.pine'), { symbol, timeframe: '15m', limit: 100 });
|
|
161
|
-
return extract(plots, { position: 'Signal', priceOpen: 'Close', priceTakeProfit: 'TakeProfit', priceStopLoss: 'StopLoss' });
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
],
|
|
165
|
-
compute: ([higher, lower]) => {
|
|
166
|
-
if (higher.noTrades || lower.position === 0) return null;
|
|
167
|
-
if (higher.allowShort && lower.position === 1) return null;
|
|
168
|
-
if (higher.allowLong && lower.position === -1) return null;
|
|
169
|
-
return lower.position;
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const result = await resolve(signal);
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### Mixed types
|
|
177
|
-
|
|
178
|
-
TypeScript correctly infers heterogeneous types by position in `nodes`:
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
const price = sourceNode(async (symbol) => 42); // SourceNode<number>
|
|
182
|
-
const name = sourceNode(async (symbol) => 'BTCUSDT'); // SourceNode<string>
|
|
183
|
-
const flag = sourceNode(async (symbol) => true); // SourceNode<boolean>
|
|
184
|
-
|
|
185
|
-
const result = outputNode(
|
|
186
|
-
([p, n, f]) => `${n}: ${p} (active: ${f})`, // p: number, n: string, f: boolean
|
|
187
|
-
price,
|
|
188
|
-
name,
|
|
189
|
-
flag,
|
|
190
|
-
);
|
|
191
|
-
// OutputNode<[SourceNode<number>, SourceNode<string>, SourceNode<boolean>], string>
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### Using inside a backtest-kit strategy
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
import { addStrategy } from 'backtest-kit';
|
|
198
|
-
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
199
|
-
|
|
200
|
-
const rsi = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
201
|
-
// ... compute RSI
|
|
202
|
-
return 55.2;
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const signal = outputNode(
|
|
206
|
-
([rsiValue]) => rsiValue < 30 ? 1 : rsiValue > 70 ? -1 : 0,
|
|
207
|
-
rsi,
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
addStrategy({
|
|
211
|
-
strategyName: 'graph-rsi',
|
|
212
|
-
interval: '1h',
|
|
213
|
-
riskName: 'demo',
|
|
214
|
-
getSignal: async (symbol) => {
|
|
215
|
-
const direction = await resolve(signal); // 1 | -1 | 0
|
|
216
|
-
return direction === 1
|
|
217
|
-
? { position: 'long', ... }
|
|
218
|
-
: null;
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
### Low-level INode
|
|
224
|
-
|
|
225
|
-
For manual graph construction without builders (e.g. after deserialization or in a DI container):
|
|
226
|
-
|
|
227
|
-
```typescript
|
|
228
|
-
import { INode, Value } from '@backtest-kit/graph';
|
|
229
|
-
import NodeType from '@backtest-kit/graph/enum/NodeType';
|
|
230
|
-
|
|
231
|
-
const priceNode: INode = {
|
|
232
|
-
type: NodeType.SourceNode,
|
|
233
|
-
description: 'Close price',
|
|
234
|
-
fetch: async (symbol, when, currentPrice, exchangeName) => 42,
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const outputNode: INode = {
|
|
238
|
-
type: NodeType.OutputNode,
|
|
239
|
-
description: 'Doubled price',
|
|
240
|
-
nodes: [priceNode],
|
|
241
|
-
compute: ([price]) => (price as number) * 2,
|
|
242
|
-
};
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
> `INode` has no generic parameters — `values` in `compute` is typed as `Value[]`. Use `TypedNode` and builders for full IntelliSense.
|
|
246
|
-
|
|
247
|
-
### DB serialization
|
|
248
|
-
|
|
249
|
-
`serialize` flattens the graph into an `IFlatNode[]` array, replacing object references in `nodes` with `nodeIds`. `deserialize` reconstructs the tree:
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
import { serialize, deserialize, IFlatNode } from '@backtest-kit/graph';
|
|
253
|
-
|
|
254
|
-
// Graph → flat array for DB
|
|
255
|
-
const flat: IFlatNode[] = serialize([vwap]);
|
|
256
|
-
// [
|
|
257
|
-
// { id: 'abc', type: 'source_node', nodeIds: [] }, // closePrice
|
|
258
|
-
// { id: 'def', type: 'source_node', nodeIds: [] }, // volume
|
|
259
|
-
// { id: 'ghi', type: 'output_node', nodeIds: ['abc', 'def'] }, // vwap
|
|
260
|
-
// ]
|
|
261
|
-
|
|
262
|
-
// Save to DB
|
|
263
|
-
await db.collection('nodes').insertMany(flat);
|
|
264
|
-
|
|
265
|
-
// Load from DB and reconstruct the graph
|
|
266
|
-
const stored: IFlatNode[] = await db.collection('nodes').find().toArray();
|
|
267
|
-
const roots: INode[] = deserialize(stored); // nodes[] is wired up from nodeIds
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
> `fetch` and `compute` are not stored in the DB — they must be restored on the application side after `deserialize`.
|
|
271
|
-
|
|
272
|
-
### deepFlat — traversal utility
|
|
273
|
-
|
|
274
|
-
`deepFlat` returns all nodes in topological order (dependencies before parents), deduplicated by reference:
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
import { deepFlat } from '@backtest-kit/graph';
|
|
278
|
-
|
|
279
|
-
const all = deepFlat([vwap]);
|
|
280
|
-
// [closePrice, volume, vwap] — dependencies first
|
|
281
|
-
|
|
282
|
-
all.forEach(node => console.log(node.description));
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
## 📋 API Reference
|
|
286
|
-
|
|
287
|
-
| Export | Description |
|
|
288
|
-
|--------|-------------|
|
|
289
|
-
| **`sourceNode(fetch)`** | Builder — creates a typed source node |
|
|
290
|
-
| **`outputNode(compute, ...nodes)`** | Builder — creates a typed output node, infers `values` types from `nodes` |
|
|
291
|
-
| **`resolve(node)`** | Recursively resolves a node graph within backtest-kit execution context |
|
|
292
|
-
| **`serialize(roots)`** | Flattens a node tree into `IFlatNode[]` for DB storage |
|
|
293
|
-
| **`deserialize(flat)`** | Reconstructs a node tree from `IFlatNode[]`, returns root nodes |
|
|
294
|
-
| **`deepFlat(nodes)`** | Utility — returns all nodes in topological order (dependencies first) |
|
|
295
|
-
| **`INode`** | Base runtime interface (untyped, used internally and for serialization) |
|
|
296
|
-
| **`TypedNode`** | Discriminated union for authoring with full IntelliSense |
|
|
297
|
-
| **`IFlatNode`** | Serialized node shape for DB storage |
|
|
298
|
-
| **`Value`** | `string \| number \| boolean \| null` |
|
|
299
|
-
|
|
300
|
-
## 🤝 Contribute
|
|
301
|
-
|
|
302
|
-
Fork/PR on [GitHub](https://github.com/tripolskypetr/backtest-kit).
|
|
303
|
-
|
|
304
|
-
## 📜 License
|
|
305
|
-
|
|
306
|
-
MIT © [tripolskypetr](https://github.com/tripolskypetr)
|
|
1
|
+
<img src="https://github.com/tripolskypetr/backtest-kit/raw/refs/heads/master/assets/assignation.svg" height="45px" align="right">
|
|
2
|
+
|
|
3
|
+
# 📊 @backtest-kit/graph
|
|
4
|
+
|
|
5
|
+
> Compose backtest-kit computations as a typed directed acyclic graph. Define source nodes that fetch market data and output nodes that compute derived values — then resolve the whole graph in topological order.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
[](https://deepwiki.com/tripolskypetr/backtest-kit)
|
|
10
|
+
[](https://npmjs.org/package/@backtest-kit/graph)
|
|
11
|
+
[]()
|
|
12
|
+
|
|
13
|
+
📚 **[Backtest Kit Docs](https://backtest-kit.github.io/documents/article_07_ai_news_trading_signals.html)** | 🌟 **[GitHub](https://github.com/tripolskypetr/backtest-kit)**
|
|
14
|
+
|
|
15
|
+
> **New to backtest-kit?** The fastest way to get a real, production-ready setup is to clone the [reference implementation](https://github.com/tripolskypetr/backtest-kit/tree/master/example) — a fully working news-sentiment AI trading system with LLM forecasting, multi-timeframe data, and a documented February 2026 backtest. Start there instead of from scratch.
|
|
16
|
+
|
|
17
|
+
## 🔥 Multi-timeframe Pine Script strategy
|
|
18
|
+
|
|
19
|
+
The graph below replicates a two-timeframe strategy: a 4h Pine Script acts as a trend filter, a 15m Pine Script generates the entry signal. `outputNode` combines them and returns `null` when the trend disagrees.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { extract, run, toSignalDto, File } from '@backtest-kit/pinets';
|
|
23
|
+
import { addStrategySchema, Cache } from 'backtest-kit';
|
|
24
|
+
import { randomString } from 'functools-kit';
|
|
25
|
+
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
26
|
+
|
|
27
|
+
// SourceNode — 4h trend filter, cached per candle interval
|
|
28
|
+
const higherTimeframe = sourceNode(
|
|
29
|
+
Cache.fn(
|
|
30
|
+
async (symbol) => {
|
|
31
|
+
const plots = await run(File.fromPath('timeframe_4h.pine'), {
|
|
32
|
+
symbol,
|
|
33
|
+
timeframe: '4h',
|
|
34
|
+
limit: 100,
|
|
35
|
+
});
|
|
36
|
+
return extract(plots, {
|
|
37
|
+
allowLong: 'AllowLong',
|
|
38
|
+
allowShort: 'AllowShort',
|
|
39
|
+
noTrades: 'NoTrades',
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
{ interval: '4h', key: ([symbol]) => symbol },
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// SourceNode — 15m entry signal, cached per candle interval
|
|
47
|
+
const lowerTimeframe = sourceNode(
|
|
48
|
+
Cache.fn(
|
|
49
|
+
async (symbol) => {
|
|
50
|
+
const plots = await run(File.fromPath('timeframe_15m.pine'), {
|
|
51
|
+
symbol,
|
|
52
|
+
timeframe: '15m',
|
|
53
|
+
limit: 100,
|
|
54
|
+
});
|
|
55
|
+
return extract(plots, {
|
|
56
|
+
position: 'Signal',
|
|
57
|
+
priceOpen: 'Close',
|
|
58
|
+
priceTakeProfit: 'TakeProfit',
|
|
59
|
+
priceStopLoss: 'StopLoss',
|
|
60
|
+
minuteEstimatedTime: 'EstimatedTime',
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
{ interval: '15m', key: ([symbol]) => symbol },
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// OutputNode — applies MTF filter, returns ISignalDto or null
|
|
68
|
+
const mtfSignal = outputNode(
|
|
69
|
+
async ([higher, lower]) => {
|
|
70
|
+
if (higher.noTrades) return null;
|
|
71
|
+
if (lower.position === 0) return null;
|
|
72
|
+
if (higher.allowShort && lower.position === 1) return null;
|
|
73
|
+
if (higher.allowLong && lower.position === -1) return null;
|
|
74
|
+
|
|
75
|
+
return toSignalDto(randomString(), lower, null);
|
|
76
|
+
},
|
|
77
|
+
higherTimeframe,
|
|
78
|
+
lowerTimeframe,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
addStrategySchema({
|
|
82
|
+
strategyName: 'mtf_graph_strategy',
|
|
83
|
+
interval: '5m',
|
|
84
|
+
getSignal: (symbol) => resolve(mtfSignal),
|
|
85
|
+
actions: ['partial_profit_action', 'breakeven_action'],
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The graph resolves both Pine Script nodes **in parallel** via `Promise.all`, then passes their typed results to `compute`. Replacing either timeframe script or adding a third filter node requires no changes to the strategy wiring.
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
## 🚀 Installation
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm install @backtest-kit/graph backtest-kit
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## ✨ Features
|
|
99
|
+
|
|
100
|
+
- 📊 **DAG execution**: Nodes are resolved bottom-up in topological order with `Promise.all` parallelism
|
|
101
|
+
- 🔒 **Type-safe values**: TypeScript infers the return type of every node through the graph via generics
|
|
102
|
+
- 🧱 **Two APIs**: Low-level `INode` for runtime/storage, high-level `TypedNode` + builders for authoring
|
|
103
|
+
- 💾 **DB-ready serialization**: `serialize` / `deserialize` convert the graph to a flat `IFlatNode[]` list with `id` / `nodeIds`
|
|
104
|
+
- 🔌 **Context-aware fetch**: `SourceNode.fetch` receives `(symbol, when, currentPrice, exchangeName)` from the execution context automatically
|
|
105
|
+
|
|
106
|
+
## 📖 Usage
|
|
107
|
+
|
|
108
|
+
### Quick Start — builder API
|
|
109
|
+
|
|
110
|
+
Use `sourceNode` and `outputNode` to define a typed computation graph. TypeScript infers the type of `values` in `compute` from the `nodes` passed to `outputNode`:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
114
|
+
|
|
115
|
+
// SourceNode<number> — fetch receives symbol, when, currentPrice, exchangeName from context
|
|
116
|
+
const closePrice = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
117
|
+
const candles = await getCandles(symbol, '1h', 1, exchangeName);
|
|
118
|
+
return candles[0].close; // number
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// SourceNode<number>
|
|
122
|
+
const volume = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
123
|
+
const candles = await getCandles(symbol, '1h', 1, exchangeName);
|
|
124
|
+
return candles[0].volume; // number
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// OutputNode<[SourceNode<number>, SourceNode<number>], number>
|
|
128
|
+
// price and vol are automatically number
|
|
129
|
+
const vwap = outputNode(
|
|
130
|
+
([price, vol]) => price * vol,
|
|
131
|
+
closePrice,
|
|
132
|
+
volume,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Resolve inside a backtest-kit strategy
|
|
136
|
+
const result = await resolve(vwap); // Promise<number>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Inline anonymous composition
|
|
140
|
+
|
|
141
|
+
The entire graph can be defined as a single object literal.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { NodeType } from '@backtest-kit/graph';
|
|
145
|
+
import { TypedNode, resolve } from '@backtest-kit/graph';
|
|
146
|
+
|
|
147
|
+
const signal: TypedNode = {
|
|
148
|
+
type: NodeType.OutputNode,
|
|
149
|
+
nodes: [
|
|
150
|
+
{
|
|
151
|
+
type: NodeType.SourceNode,
|
|
152
|
+
fetch: async (symbol, when, currentPrice, exchangeName) => {
|
|
153
|
+
const plots = await run(File.fromPath('timeframe_4h.pine'), { symbol, timeframe: '4h', limit: 100 });
|
|
154
|
+
return extract(plots, { allowLong: 'AllowLong', allowShort: 'AllowShort', noTrades: 'NoTrades' });
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: NodeType.SourceNode,
|
|
159
|
+
fetch: async (symbol, when, currentPrice, exchangeName) => {
|
|
160
|
+
const plots = await run(File.fromPath('timeframe_15m.pine'), { symbol, timeframe: '15m', limit: 100 });
|
|
161
|
+
return extract(plots, { position: 'Signal', priceOpen: 'Close', priceTakeProfit: 'TakeProfit', priceStopLoss: 'StopLoss' });
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
compute: ([higher, lower]) => {
|
|
166
|
+
if (higher.noTrades || lower.position === 0) return null;
|
|
167
|
+
if (higher.allowShort && lower.position === 1) return null;
|
|
168
|
+
if (higher.allowLong && lower.position === -1) return null;
|
|
169
|
+
return lower.position;
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const result = await resolve(signal);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Mixed types
|
|
177
|
+
|
|
178
|
+
TypeScript correctly infers heterogeneous types by position in `nodes`:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const price = sourceNode(async (symbol) => 42); // SourceNode<number>
|
|
182
|
+
const name = sourceNode(async (symbol) => 'BTCUSDT'); // SourceNode<string>
|
|
183
|
+
const flag = sourceNode(async (symbol) => true); // SourceNode<boolean>
|
|
184
|
+
|
|
185
|
+
const result = outputNode(
|
|
186
|
+
([p, n, f]) => `${n}: ${p} (active: ${f})`, // p: number, n: string, f: boolean
|
|
187
|
+
price,
|
|
188
|
+
name,
|
|
189
|
+
flag,
|
|
190
|
+
);
|
|
191
|
+
// OutputNode<[SourceNode<number>, SourceNode<string>, SourceNode<boolean>], string>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Using inside a backtest-kit strategy
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { addStrategy } from 'backtest-kit';
|
|
198
|
+
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
199
|
+
|
|
200
|
+
const rsi = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
201
|
+
// ... compute RSI
|
|
202
|
+
return 55.2;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const signal = outputNode(
|
|
206
|
+
([rsiValue]) => rsiValue < 30 ? 1 : rsiValue > 70 ? -1 : 0,
|
|
207
|
+
rsi,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
addStrategy({
|
|
211
|
+
strategyName: 'graph-rsi',
|
|
212
|
+
interval: '1h',
|
|
213
|
+
riskName: 'demo',
|
|
214
|
+
getSignal: async (symbol) => {
|
|
215
|
+
const direction = await resolve(signal); // 1 | -1 | 0
|
|
216
|
+
return direction === 1
|
|
217
|
+
? { position: 'long', ... }
|
|
218
|
+
: null;
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Low-level INode
|
|
224
|
+
|
|
225
|
+
For manual graph construction without builders (e.g. after deserialization or in a DI container):
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { INode, Value } from '@backtest-kit/graph';
|
|
229
|
+
import NodeType from '@backtest-kit/graph/enum/NodeType';
|
|
230
|
+
|
|
231
|
+
const priceNode: INode = {
|
|
232
|
+
type: NodeType.SourceNode,
|
|
233
|
+
description: 'Close price',
|
|
234
|
+
fetch: async (symbol, when, currentPrice, exchangeName) => 42,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const outputNode: INode = {
|
|
238
|
+
type: NodeType.OutputNode,
|
|
239
|
+
description: 'Doubled price',
|
|
240
|
+
nodes: [priceNode],
|
|
241
|
+
compute: ([price]) => (price as number) * 2,
|
|
242
|
+
};
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
> `INode` has no generic parameters — `values` in `compute` is typed as `Value[]`. Use `TypedNode` and builders for full IntelliSense.
|
|
246
|
+
|
|
247
|
+
### DB serialization
|
|
248
|
+
|
|
249
|
+
`serialize` flattens the graph into an `IFlatNode[]` array, replacing object references in `nodes` with `nodeIds`. `deserialize` reconstructs the tree:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { serialize, deserialize, IFlatNode } from '@backtest-kit/graph';
|
|
253
|
+
|
|
254
|
+
// Graph → flat array for DB
|
|
255
|
+
const flat: IFlatNode[] = serialize([vwap]);
|
|
256
|
+
// [
|
|
257
|
+
// { id: 'abc', type: 'source_node', nodeIds: [] }, // closePrice
|
|
258
|
+
// { id: 'def', type: 'source_node', nodeIds: [] }, // volume
|
|
259
|
+
// { id: 'ghi', type: 'output_node', nodeIds: ['abc', 'def'] }, // vwap
|
|
260
|
+
// ]
|
|
261
|
+
|
|
262
|
+
// Save to DB
|
|
263
|
+
await db.collection('nodes').insertMany(flat);
|
|
264
|
+
|
|
265
|
+
// Load from DB and reconstruct the graph
|
|
266
|
+
const stored: IFlatNode[] = await db.collection('nodes').find().toArray();
|
|
267
|
+
const roots: INode[] = deserialize(stored); // nodes[] is wired up from nodeIds
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
> `fetch` and `compute` are not stored in the DB — they must be restored on the application side after `deserialize`.
|
|
271
|
+
|
|
272
|
+
### deepFlat — traversal utility
|
|
273
|
+
|
|
274
|
+
`deepFlat` returns all nodes in topological order (dependencies before parents), deduplicated by reference:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { deepFlat } from '@backtest-kit/graph';
|
|
278
|
+
|
|
279
|
+
const all = deepFlat([vwap]);
|
|
280
|
+
// [closePrice, volume, vwap] — dependencies first
|
|
281
|
+
|
|
282
|
+
all.forEach(node => console.log(node.description));
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## 📋 API Reference
|
|
286
|
+
|
|
287
|
+
| Export | Description |
|
|
288
|
+
|--------|-------------|
|
|
289
|
+
| **`sourceNode(fetch)`** | Builder — creates a typed source node |
|
|
290
|
+
| **`outputNode(compute, ...nodes)`** | Builder — creates a typed output node, infers `values` types from `nodes` |
|
|
291
|
+
| **`resolve(node)`** | Recursively resolves a node graph within backtest-kit execution context |
|
|
292
|
+
| **`serialize(roots)`** | Flattens a node tree into `IFlatNode[]` for DB storage |
|
|
293
|
+
| **`deserialize(flat)`** | Reconstructs a node tree from `IFlatNode[]`, returns root nodes |
|
|
294
|
+
| **`deepFlat(nodes)`** | Utility — returns all nodes in topological order (dependencies first) |
|
|
295
|
+
| **`INode`** | Base runtime interface (untyped, used internally and for serialization) |
|
|
296
|
+
| **`TypedNode`** | Discriminated union for authoring with full IntelliSense |
|
|
297
|
+
| **`IFlatNode`** | Serialized node shape for DB storage |
|
|
298
|
+
| **`Value`** | `string \| number \| boolean \| null` |
|
|
299
|
+
|
|
300
|
+
## 🤝 Contribute
|
|
301
|
+
|
|
302
|
+
Fork/PR on [GitHub](https://github.com/tripolskypetr/backtest-kit).
|
|
303
|
+
|
|
304
|
+
## 📜 License
|
|
305
|
+
|
|
306
|
+
MIT © [tripolskypetr](https://github.com/tripolskypetr)
|
package/package.json
CHANGED
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@backtest-kit/graph",
|
|
3
|
-
"version": "9.
|
|
4
|
-
"description": "Compose backtest-kit computations as a typed directed acyclic graph. Define source nodes that fetch market data and output nodes that compute derived values — then resolve the whole graph in topological order.",
|
|
5
|
-
"author": {
|
|
6
|
-
"name": "Petr Tripolsky",
|
|
7
|
-
"email": "tripolskypetr@gmail.com",
|
|
8
|
-
"url": "https://github.com/tripolskypetr"
|
|
9
|
-
},
|
|
10
|
-
"funding": {
|
|
11
|
-
"type": "individual",
|
|
12
|
-
"url": "http://paypal.me/tripolskypetr"
|
|
13
|
-
},
|
|
14
|
-
"license": "MIT",
|
|
15
|
-
"homepage": "https://backtest-kit.github.io/documents/article_07_ai_news_trading_signals.html",
|
|
16
|
-
"keywords": [
|
|
17
|
-
"graph",
|
|
18
|
-
"dag",
|
|
19
|
-
"directed-acyclic-graph",
|
|
20
|
-
"computation-graph",
|
|
21
|
-
"dataflow",
|
|
22
|
-
"trading-bot",
|
|
23
|
-
"algorithmic-trading",
|
|
24
|
-
"backtest",
|
|
25
|
-
"backtesting",
|
|
26
|
-
"cryptocurrency",
|
|
27
|
-
"forex",
|
|
28
|
-
"strategy",
|
|
29
|
-
"typescript",
|
|
30
|
-
"type-safe",
|
|
31
|
-
"serialization"
|
|
32
|
-
],
|
|
33
|
-
"files": [
|
|
34
|
-
"build",
|
|
35
|
-
"types.d.ts",
|
|
36
|
-
"README.md"
|
|
37
|
-
],
|
|
38
|
-
"repository": {
|
|
39
|
-
"type": "git",
|
|
40
|
-
"url": "https://github.com/tripolskypetr/backtest-kit",
|
|
41
|
-
"documentation": "https://github.com/tripolskypetr/backtest-kit/tree/master/docs"
|
|
42
|
-
},
|
|
43
|
-
"bugs": {
|
|
44
|
-
"url": "https://github.com/tripolskypetr/backtest-kit/issues"
|
|
45
|
-
},
|
|
46
|
-
"scripts": {
|
|
47
|
-
"build": "rollup -c"
|
|
48
|
-
},
|
|
49
|
-
"main": "build/index.cjs",
|
|
50
|
-
"module": "build/index.mjs",
|
|
51
|
-
"source": "src/index.ts",
|
|
52
|
-
"types": "./types.d.ts",
|
|
53
|
-
"exports": {
|
|
54
|
-
"require": "./build/index.cjs",
|
|
55
|
-
"types": "./types.d.ts",
|
|
56
|
-
"import": "./build/index.mjs",
|
|
57
|
-
"default": "./build/index.cjs"
|
|
58
|
-
},
|
|
59
|
-
"devDependencies": {
|
|
60
|
-
"@rollup/plugin-typescript": "11.1.6",
|
|
61
|
-
"@types/node": "22.9.0",
|
|
62
|
-
"glob": "11.0.1",
|
|
63
|
-
"rimraf": "6.0.1",
|
|
64
|
-
"rollup": "3.29.5",
|
|
65
|
-
"rollup-plugin-dts": "6.1.1",
|
|
66
|
-
"rollup-plugin-peer-deps-external": "2.2.4",
|
|
67
|
-
"ts-morph": "27.0.2",
|
|
68
|
-
"tslib": "2.7.0",
|
|
69
|
-
"typedoc": "0.27.9",
|
|
70
|
-
"backtest-kit": "9.
|
|
71
|
-
"worker-testbed": "2.0.0"
|
|
72
|
-
},
|
|
73
|
-
"peerDependencies": {
|
|
74
|
-
"backtest-kit": "^9.
|
|
75
|
-
"typescript": "^5.0.0"
|
|
76
|
-
},
|
|
77
|
-
"dependencies": {
|
|
78
|
-
"di-kit": "^1.1.1",
|
|
79
|
-
"di-scoped": "^1.0.21",
|
|
80
|
-
"functools-kit": "^2.3.0",
|
|
81
|
-
"get-moment-stamp": "^1.1.2"
|
|
82
|
-
},
|
|
83
|
-
"publishConfig": {
|
|
84
|
-
"access": "public"
|
|
85
|
-
}
|
|
86
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@backtest-kit/graph",
|
|
3
|
+
"version": "9.7.0",
|
|
4
|
+
"description": "Compose backtest-kit computations as a typed directed acyclic graph. Define source nodes that fetch market data and output nodes that compute derived values — then resolve the whole graph in topological order.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Petr Tripolsky",
|
|
7
|
+
"email": "tripolskypetr@gmail.com",
|
|
8
|
+
"url": "https://github.com/tripolskypetr"
|
|
9
|
+
},
|
|
10
|
+
"funding": {
|
|
11
|
+
"type": "individual",
|
|
12
|
+
"url": "http://paypal.me/tripolskypetr"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"homepage": "https://backtest-kit.github.io/documents/article_07_ai_news_trading_signals.html",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"graph",
|
|
18
|
+
"dag",
|
|
19
|
+
"directed-acyclic-graph",
|
|
20
|
+
"computation-graph",
|
|
21
|
+
"dataflow",
|
|
22
|
+
"trading-bot",
|
|
23
|
+
"algorithmic-trading",
|
|
24
|
+
"backtest",
|
|
25
|
+
"backtesting",
|
|
26
|
+
"cryptocurrency",
|
|
27
|
+
"forex",
|
|
28
|
+
"strategy",
|
|
29
|
+
"typescript",
|
|
30
|
+
"type-safe",
|
|
31
|
+
"serialization"
|
|
32
|
+
],
|
|
33
|
+
"files": [
|
|
34
|
+
"build",
|
|
35
|
+
"types.d.ts",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/tripolskypetr/backtest-kit",
|
|
41
|
+
"documentation": "https://github.com/tripolskypetr/backtest-kit/tree/master/docs"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/tripolskypetr/backtest-kit/issues"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "rollup -c"
|
|
48
|
+
},
|
|
49
|
+
"main": "build/index.cjs",
|
|
50
|
+
"module": "build/index.mjs",
|
|
51
|
+
"source": "src/index.ts",
|
|
52
|
+
"types": "./types.d.ts",
|
|
53
|
+
"exports": {
|
|
54
|
+
"require": "./build/index.cjs",
|
|
55
|
+
"types": "./types.d.ts",
|
|
56
|
+
"import": "./build/index.mjs",
|
|
57
|
+
"default": "./build/index.cjs"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@rollup/plugin-typescript": "11.1.6",
|
|
61
|
+
"@types/node": "22.9.0",
|
|
62
|
+
"glob": "11.0.1",
|
|
63
|
+
"rimraf": "6.0.1",
|
|
64
|
+
"rollup": "3.29.5",
|
|
65
|
+
"rollup-plugin-dts": "6.1.1",
|
|
66
|
+
"rollup-plugin-peer-deps-external": "2.2.4",
|
|
67
|
+
"ts-morph": "27.0.2",
|
|
68
|
+
"tslib": "2.7.0",
|
|
69
|
+
"typedoc": "0.27.9",
|
|
70
|
+
"backtest-kit": "9.7.0",
|
|
71
|
+
"worker-testbed": "2.0.0"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"backtest-kit": "^9.7.0",
|
|
75
|
+
"typescript": "^5.0.0"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"di-kit": "^1.1.1",
|
|
79
|
+
"di-scoped": "^1.0.21",
|
|
80
|
+
"functools-kit": "^2.3.0",
|
|
81
|
+
"get-moment-stamp": "^1.1.2"
|
|
82
|
+
},
|
|
83
|
+
"publishConfig": {
|
|
84
|
+
"access": "public"
|
|
85
|
+
}
|
|
86
|
+
}
|