@backtest-kit/graph 13.5.0 β 14.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 +148 -200
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<img src="https://github.com/tripolskypetr/backtest-kit/raw/refs/heads/master/assets/assignation.svg" height="45px" align="right">
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# πΈοΈ @backtest-kit/graph
|
|
4
4
|
|
|
5
|
-
> Compose backtest-kit computations as a typed directed acyclic graph.
|
|
5
|
+
> Compose [backtest-kit](https://www.npmjs.com/package/backtest-kit) computations as a typed directed acyclic graph. Declare **source nodes** that fetch market data and **output nodes** that derive values from them β then resolve the whole graph in topological order, in parallel, fully type-inferred.
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -10,13 +10,28 @@
|
|
|
10
10
|
[](https://npmjs.org/package/@backtest-kit/graph)
|
|
11
11
|
[]()
|
|
12
12
|
|
|
13
|
-
π **[
|
|
13
|
+
π **[Docs](https://backtest-kit.github.io/documents/article_07_ai_news_trading_signals.html)** Β· π **[Reference implementation](https://github.com/tripolskypetr/backtest-kit/tree/master/example)** Β· π **[GitHub](https://github.com/tripolskypetr/backtest-kit)**
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npm install @backtest-kit/graph backtest-kit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What it's for
|
|
22
|
+
|
|
23
|
+
A trading signal is rarely one number β it's a small computation: pull 4h trend, pull 15m entry, combine them, maybe gate on RSI. Written inline, that becomes a tangle of `await`s with hand-managed ordering. This package lets you declare it as a **graph of typed nodes**: leaves (`sourceNode`) fetch data, branches (`outputNode`) compute from their children, and `resolve()` walks the tree bottom-up β resolving every node's dependencies **in parallel** (`Promise.all`) before computing the node itself. Swapping a timeframe or adding a filter node needs no change to the strategy wiring, and TypeScript infers the value type through the entire graph.
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
It plugs straight into a `getSignal` and runs inside backtest-kit's execution context, so a `sourceNode`'s `fetch` automatically receives `(symbol, when, currentPrice, exchangeName)` β the same look-ahead-safe "now" the rest of the engine sees.
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Multi-timeframe Pine strategy (the canonical example)
|
|
30
|
+
|
|
31
|
+
A two-timeframe strategy: a **4h Pine Script** acts as a trend filter, a **15m Pine Script** generates the entry. The output node combines them and returns `null` whenever the trend disagrees with the entry β so a long signal is suppressed in a downtrend and vice-versa.
|
|
32
|
+
|
|
33
|
+
<details>
|
|
34
|
+
<summary>The Code</summary>
|
|
20
35
|
|
|
21
36
|
```typescript
|
|
22
37
|
import { extract, run, toSignalDto, File } from '@backtest-kit/pinets';
|
|
@@ -25,281 +40,214 @@ import { randomString } from 'functools-kit';
|
|
|
25
40
|
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
26
41
|
|
|
27
42
|
// SourceNode β 4h trend filter, cached per candle interval
|
|
28
|
-
const higherTimeframe = sourceNode(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
);
|
|
43
|
+
const higherTimeframe = sourceNode(Cache.fn(async (symbol) => {
|
|
44
|
+
const plots = await run(File.fromPath('timeframe_4h.pine'), { symbol, timeframe: '4h', limit: 100 });
|
|
45
|
+
return extract(plots, { allowLong: 'AllowLong', allowShort: 'AllowShort', noTrades: 'NoTrades' });
|
|
46
|
+
}, { interval: '4h', key: ([symbol]) => symbol }));
|
|
45
47
|
|
|
46
48
|
// SourceNode β 15m entry signal, cached per candle interval
|
|
47
|
-
const lowerTimeframe = sourceNode(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
);
|
|
49
|
+
const lowerTimeframe = sourceNode(Cache.fn(async (symbol) => {
|
|
50
|
+
const plots = await run(File.fromPath('timeframe_15m.pine'), { symbol, timeframe: '15m', limit: 100 });
|
|
51
|
+
return extract(plots, { position: 'Signal', priceOpen: 'Close', priceTakeProfit: 'TakeProfit', priceStopLoss: 'StopLoss', minuteEstimatedTime: 'EstimatedTime' });
|
|
52
|
+
}, { interval: '15m', key: ([symbol]) => symbol }));
|
|
53
|
+
|
|
54
|
+
// OutputNode β applies the MTF filter, returns ISignalDto or null
|
|
55
|
+
const mtfSignal = outputNode(async ([higher, lower]) => {
|
|
56
|
+
if (higher.noTrades) return null;
|
|
57
|
+
if (lower.position === 0) return null;
|
|
58
|
+
if (higher.allowShort && lower.position === 1) return null; // long blocked in downtrend
|
|
59
|
+
if (higher.allowLong && lower.position === -1) return null; // short blocked in uptrend
|
|
60
|
+
return toSignalDto(randomString(), lower, null);
|
|
61
|
+
}, higherTimeframe, lowerTimeframe);
|
|
80
62
|
|
|
81
63
|
addStrategySchema({
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
actions: ['partial_profit_action', 'breakeven_action'],
|
|
64
|
+
strategyName: 'mtf_graph_strategy', interval: '5m',
|
|
65
|
+
getSignal: (symbol) => resolve(mtfSignal),
|
|
66
|
+
actions: ['partial_profit_action', 'breakeven_action'],
|
|
86
67
|
});
|
|
87
68
|
```
|
|
88
69
|
|
|
89
|
-
|
|
70
|
+
Both Pine nodes resolve **in parallel**; their typed results flow into `compute`. Replacing either script β or adding a third filter node β requires no change to the strategy registration.
|
|
90
71
|
|
|
72
|
+
</details>
|
|
91
73
|
|
|
92
|
-
|
|
74
|
+
---
|
|
93
75
|
|
|
94
|
-
|
|
95
|
-
npm install @backtest-kit/graph backtest-kit
|
|
96
|
-
```
|
|
76
|
+
## The model
|
|
97
77
|
|
|
98
|
-
|
|
78
|
+
Two layers, by design: a **low-level runtime interface** (`INode`) that serializes to a DB and reconstructs without builders, and a **high-level authoring API** (`TypedNode` + `sourceNode`/`outputNode`) that gives full type inference. You write with the builders; the runtime and storage use the interface.
|
|
99
79
|
|
|
100
|
-
- π **DAG execution
|
|
101
|
-
- π **Type-safe values
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
- π **Context-aware fetch**: `SourceNode.fetch` receives `(symbol, when, currentPrice, exchangeName)` from the execution context automatically
|
|
80
|
+
- π **DAG execution** β nodes resolve bottom-up in topological order with `Promise.all` parallelism.
|
|
81
|
+
- π **Type-safe values** β TypeScript infers each node's return type through the graph via generics (`OutputNode<[SourceNode<number>, SourceNode<string>], β¦>`).
|
|
82
|
+
- πΎ **DB-ready** β `serialize`/`deserialize` convert the graph to a flat `IFlatNode[]` list with `id`/`nodeIds`.
|
|
83
|
+
- π **Context-aware fetch** β `SourceNode.fetch` receives `(symbol, when, currentPrice, exchangeName)` from the execution context automatically.
|
|
105
84
|
|
|
106
|
-
|
|
85
|
+
---
|
|
107
86
|
|
|
108
|
-
|
|
87
|
+
## Authoring API
|
|
109
88
|
|
|
110
|
-
|
|
89
|
+
<details>
|
|
90
|
+
<summary>Builder API β sourceNode / outputNode / resolve</summary>
|
|
91
|
+
|
|
92
|
+
`outputNode` infers the type of `values` in `compute` from the nodes you pass:
|
|
111
93
|
|
|
112
94
|
```typescript
|
|
113
95
|
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
114
96
|
|
|
115
|
-
// SourceNode<number> β fetch receives symbol, when, currentPrice, exchangeName from context
|
|
116
97
|
const closePrice = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
117
|
-
|
|
118
|
-
|
|
98
|
+
const candles = await getCandles(symbol, '1h', 1, exchangeName);
|
|
99
|
+
return candles[0].close; // SourceNode<number>
|
|
119
100
|
});
|
|
120
|
-
|
|
121
|
-
// SourceNode<number>
|
|
122
101
|
const volume = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
123
|
-
|
|
124
|
-
|
|
102
|
+
const candles = await getCandles(symbol, '1h', 1, exchangeName);
|
|
103
|
+
return candles[0].volume; // SourceNode<number>
|
|
125
104
|
});
|
|
126
105
|
|
|
127
|
-
|
|
128
|
-
|
|
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>
|
|
106
|
+
const vwap = outputNode(([price, vol]) => price * vol, closePrice, volume); // price, vol: number
|
|
107
|
+
const result = await resolve(vwap); // Promise<number>, inside a strategy
|
|
137
108
|
```
|
|
138
109
|
|
|
139
|
-
|
|
110
|
+
</details>
|
|
140
111
|
|
|
141
|
-
|
|
112
|
+
<details>
|
|
113
|
+
<summary>Mixed types β heterogeneous inference by position</summary>
|
|
142
114
|
|
|
143
115
|
```typescript
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
};
|
|
116
|
+
const price = sourceNode(async (symbol) => 42); // SourceNode<number>
|
|
117
|
+
const name = sourceNode(async (symbol) => 'BTCUSDT'); // SourceNode<string>
|
|
118
|
+
const flag = sourceNode(async (symbol) => true); // SourceNode<boolean>
|
|
172
119
|
|
|
173
|
-
const result =
|
|
120
|
+
const result = outputNode(
|
|
121
|
+
([p, n, f]) => `${n}: ${p} (active: ${f})`, // p: number, n: string, f: boolean
|
|
122
|
+
price, name, flag,
|
|
123
|
+
); // OutputNode<[SourceNode<number>, SourceNode<string>, SourceNode<boolean>], string>
|
|
174
124
|
```
|
|
175
125
|
|
|
176
|
-
|
|
126
|
+
</details>
|
|
177
127
|
|
|
178
|
-
|
|
128
|
+
<details>
|
|
129
|
+
<summary>Inline anonymous composition β a single object literal</summary>
|
|
179
130
|
|
|
180
131
|
```typescript
|
|
181
|
-
|
|
182
|
-
const name = sourceNode(async (symbol) => 'BTCUSDT'); // SourceNode<string>
|
|
183
|
-
const flag = sourceNode(async (symbol) => true); // SourceNode<boolean>
|
|
132
|
+
import { NodeType, TypedNode, resolve } from '@backtest-kit/graph';
|
|
184
133
|
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
134
|
+
const signal: TypedNode = {
|
|
135
|
+
type: NodeType.OutputNode,
|
|
136
|
+
nodes: [
|
|
137
|
+
{ type: NodeType.SourceNode, fetch: async (symbol) => extract(await run(File.fromPath('timeframe_4h.pine'), { symbol, timeframe: '4h', limit: 100 }), { allowLong: 'AllowLong', allowShort: 'AllowShort', noTrades: 'NoTrades' }) },
|
|
138
|
+
{ type: NodeType.SourceNode, fetch: async (symbol) => extract(await run(File.fromPath('timeframe_15m.pine'), { symbol, timeframe: '15m', limit: 100 }), { position: 'Signal', priceOpen: 'Close', priceTakeProfit: 'TakeProfit', priceStopLoss: 'StopLoss' }) },
|
|
139
|
+
],
|
|
140
|
+
compute: ([higher, lower]) => {
|
|
141
|
+
if (higher.noTrades || lower.position === 0) return null;
|
|
142
|
+
if (higher.allowShort && lower.position === 1) return null;
|
|
143
|
+
if (higher.allowLong && lower.position === -1) return null;
|
|
144
|
+
return lower.position;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const result = await resolve(signal);
|
|
192
148
|
```
|
|
193
149
|
|
|
194
|
-
|
|
150
|
+
</details>
|
|
151
|
+
|
|
152
|
+
<details>
|
|
153
|
+
<summary>Inside a backtest-kit strategy</summary>
|
|
195
154
|
|
|
196
155
|
```typescript
|
|
197
156
|
import { addStrategy } from 'backtest-kit';
|
|
198
157
|
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
199
158
|
|
|
200
|
-
const rsi = sourceNode(async (symbol, when, currentPrice, exchangeName) =>
|
|
201
|
-
|
|
202
|
-
return 55.2;
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const signal = outputNode(
|
|
206
|
-
([rsiValue]) => rsiValue < 30 ? 1 : rsiValue > 70 ? -1 : 0,
|
|
207
|
-
rsi,
|
|
208
|
-
);
|
|
159
|
+
const rsi = sourceNode(async (symbol, when, currentPrice, exchangeName) => 55.2 /* compute RSI */);
|
|
160
|
+
const signal = outputNode(([rsiValue]) => rsiValue < 30 ? 1 : rsiValue > 70 ? -1 : 0, rsi);
|
|
209
161
|
|
|
210
162
|
addStrategy({
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return direction === 1
|
|
217
|
-
? { position: 'long', ... }
|
|
218
|
-
: null;
|
|
219
|
-
},
|
|
163
|
+
strategyName: 'graph-rsi', interval: '1h', riskName: 'demo',
|
|
164
|
+
getSignal: async (symbol) => {
|
|
165
|
+
const direction = await resolve(signal); // 1 | -1 | 0
|
|
166
|
+
return direction === 1 ? { position: 'long', /* β¦ */ } : null;
|
|
167
|
+
},
|
|
220
168
|
});
|
|
221
169
|
```
|
|
222
170
|
|
|
223
|
-
|
|
171
|
+
</details>
|
|
172
|
+
|
|
173
|
+
---
|
|
224
174
|
|
|
225
|
-
|
|
175
|
+
## Runtime interface & storage
|
|
176
|
+
|
|
177
|
+
<details>
|
|
178
|
+
<summary>Low-level INode β manual construction (post-deserialize / DI)</summary>
|
|
226
179
|
|
|
227
180
|
```typescript
|
|
228
181
|
import { INode, Value } from '@backtest-kit/graph';
|
|
229
182
|
import NodeType from '@backtest-kit/graph/enum/NodeType';
|
|
230
183
|
|
|
231
|
-
const priceNode: INode = {
|
|
232
|
-
|
|
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
|
-
};
|
|
184
|
+
const priceNode: INode = { type: NodeType.SourceNode, description: 'Close price', fetch: async (symbol, when, currentPrice, exchangeName) => 42 };
|
|
185
|
+
const doubled: INode = { type: NodeType.OutputNode, description: 'Doubled price', nodes: [priceNode], compute: ([price]) => (price as number) * 2 };
|
|
243
186
|
```
|
|
244
187
|
|
|
245
|
-
|
|
188
|
+
`INode` has no generic parameters β `values` in `compute` is typed as `Value[]` (`string | number | boolean | null`). Use `TypedNode` + builders for full IntelliSense.
|
|
189
|
+
|
|
190
|
+
</details>
|
|
246
191
|
|
|
247
|
-
|
|
192
|
+
<details>
|
|
193
|
+
<summary>DB serialization β serialize / deserialize</summary>
|
|
248
194
|
|
|
249
|
-
`serialize` flattens the graph into
|
|
195
|
+
`serialize` flattens the graph into `IFlatNode[]`, replacing object references in `nodes` with `nodeIds`; `deserialize` rebuilds the tree:
|
|
250
196
|
|
|
251
197
|
```typescript
|
|
252
198
|
import { serialize, deserialize, IFlatNode } from '@backtest-kit/graph';
|
|
253
199
|
|
|
254
|
-
// Graph β flat array for DB
|
|
255
200
|
const flat: IFlatNode[] = serialize([vwap]);
|
|
256
|
-
// [
|
|
257
|
-
// { id:
|
|
258
|
-
// { id:
|
|
259
|
-
// { id: 'ghi', type: 'output_node', nodeIds: ['abc', 'def'] }, // vwap
|
|
260
|
-
// ]
|
|
261
|
-
|
|
262
|
-
// Save to DB
|
|
201
|
+
// [ { id:'abc', type:'source_node', nodeIds:[] }, // closePrice
|
|
202
|
+
// { id:'def', type:'source_node', nodeIds:[] }, // volume
|
|
203
|
+
// { id:'ghi', type:'output_node', nodeIds:['abc','def'] } ] // vwap
|
|
263
204
|
await db.collection('nodes').insertMany(flat);
|
|
264
205
|
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
const roots: INode[] = deserialize(stored); // nodes[] is wired up from nodeIds
|
|
206
|
+
const stored = await db.collection('nodes').find().toArray();
|
|
207
|
+
const roots: INode[] = deserialize(stored); // nodes[] re-wired from nodeIds
|
|
268
208
|
```
|
|
269
209
|
|
|
270
|
-
|
|
210
|
+
`fetch` and `compute` are **not** stored β restore them on the application side after `deserialize`.
|
|
271
211
|
|
|
272
|
-
|
|
212
|
+
</details>
|
|
273
213
|
|
|
274
|
-
|
|
214
|
+
<details>
|
|
215
|
+
<summary>deepFlat β topological traversal</summary>
|
|
216
|
+
|
|
217
|
+
Returns all nodes in topological order (dependencies before parents), deduplicated by reference:
|
|
275
218
|
|
|
276
219
|
```typescript
|
|
277
220
|
import { deepFlat } from '@backtest-kit/graph';
|
|
278
|
-
|
|
279
|
-
const all = deepFlat([vwap]);
|
|
280
|
-
// [closePrice, volume, vwap] β dependencies first
|
|
281
|
-
|
|
221
|
+
const all = deepFlat([vwap]); // [closePrice, volume, vwap]
|
|
282
222
|
all.forEach(node => console.log(node.description));
|
|
283
223
|
```
|
|
284
224
|
|
|
285
|
-
|
|
225
|
+
</details>
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## API reference
|
|
286
230
|
|
|
287
231
|
| Export | Description |
|
|
288
232
|
|--------|-------------|
|
|
289
|
-
|
|
|
290
|
-
|
|
|
291
|
-
|
|
|
292
|
-
|
|
|
293
|
-
|
|
|
294
|
-
|
|
|
295
|
-
|
|
|
296
|
-
|
|
|
297
|
-
|
|
|
298
|
-
|
|
|
233
|
+
| `sourceNode(fetch)` | Builder β typed source (leaf) node; `fetch(symbol, when, currentPrice, exchangeName)` |
|
|
234
|
+
| `outputNode(compute, ...nodes)` | Builder β typed output node; infers `values` types from `nodes` |
|
|
235
|
+
| `resolve(node)` | Recursively resolves a graph within backtest-kit execution context |
|
|
236
|
+
| `serialize(roots)` | Flattens a node tree into `IFlatNode[]` for DB storage |
|
|
237
|
+
| `deserialize(flat)` | Reconstructs a node tree from `IFlatNode[]`, returns root nodes |
|
|
238
|
+
| `deepFlat(nodes)` | Returns all nodes in topological order (dependencies first) |
|
|
239
|
+
| `INode` | Base runtime interface (untyped; used internally and for serialization) |
|
|
240
|
+
| `TypedNode` | Discriminated union for authoring with full IntelliSense |
|
|
241
|
+
| `IFlatNode` | Serialized node shape for DB storage (`id`, `type`, `nodeIds`) |
|
|
242
|
+
| `NodeType` | Enum β `SourceNode` (`source_node`) / `OutputNode` (`output_node`) |
|
|
243
|
+
| `Value` | `string \| number \| boolean \| null` |
|
|
244
|
+
| `ExchangeName` | Exchange-name type alias passed to `fetch` |
|
|
245
|
+
|
|
246
|
+
> **Complete source map.** `enum/NodeType.ts` Β· `helpers/{node,resolve,serialize,deepFlat}.ts` Β· `interfaces/{Node,TypedNode,FlatNode}.interface.ts` Β· `model/ExchangeName.model.ts` Β· `index.ts` β every export above is one of these files; nothing in `src/` is undocumented.
|
|
299
247
|
|
|
300
248
|
## π€ Contribute
|
|
301
249
|
|
|
302
|
-
Fork/PR on [GitHub](https://github.com/tripolskypetr/backtest-kit).
|
|
250
|
+
Fork / PR on [GitHub](https://github.com/tripolskypetr/backtest-kit).
|
|
303
251
|
|
|
304
252
|
## π License
|
|
305
253
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backtest-kit/graph",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.0",
|
|
4
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
5
|
"author": {
|
|
6
6
|
"name": "Petr Tripolsky",
|
|
@@ -67,17 +67,17 @@
|
|
|
67
67
|
"ts-morph": "27.0.2",
|
|
68
68
|
"tslib": "2.7.0",
|
|
69
69
|
"typedoc": "0.27.9",
|
|
70
|
-
"backtest-kit": "
|
|
71
|
-
"worker-testbed": "2.
|
|
70
|
+
"backtest-kit": "14.0.0",
|
|
71
|
+
"worker-testbed": "2.1.0"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
|
-
"backtest-kit": "^
|
|
74
|
+
"backtest-kit": "^14.0.0",
|
|
75
75
|
"typescript": "^5.0.0"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"di-kit": "^1.1.1",
|
|
79
79
|
"di-scoped": "^1.0.21",
|
|
80
|
-
"functools-kit": "^
|
|
80
|
+
"functools-kit": "^3.0.0",
|
|
81
81
|
"get-moment-stamp": "^2.0.0"
|
|
82
82
|
},
|
|
83
83
|
"publishConfig": {
|