@backtest-kit/graph 8.4.0 → 8.5.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.
Files changed (2) hide show
  1. package/README.md +306 -306
  2. 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
- ![screenshot](https://raw.githubusercontent.com/tripolskypetr/backtest-kit/HEAD/assets/screenshots/screenshot16.png)
8
-
9
- [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/tripolskypetr/backtest-kit)
10
- [![npm](https://img.shields.io/npm/v/@backtest-kit/graph.svg?style=flat-square)](https://npmjs.org/package/@backtest-kit/graph)
11
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)]()
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
+ ![screenshot](https://raw.githubusercontent.com/tripolskypetr/backtest-kit/HEAD/assets/screenshots/screenshot16.png)
8
+
9
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/tripolskypetr/backtest-kit)
10
+ [![npm](https://img.shields.io/npm/v/@backtest-kit/graph.svg?style=flat-square)](https://npmjs.org/package/@backtest-kit/graph)
11
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue)]()
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": "8.4.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": "8.4.0",
71
- "worker-testbed": "2.0.0"
72
- },
73
- "peerDependencies": {
74
- "backtest-kit": "^8.4.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
- }
1
+ {
2
+ "name": "@backtest-kit/graph",
3
+ "version": "8.5.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": "8.5.0",
71
+ "worker-testbed": "2.0.0"
72
+ },
73
+ "peerDependencies": {
74
+ "backtest-kit": "^8.5.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
+ }