@backtest-kit/graph 14.0.0 → 15.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 +254 -254
- package/build/index.cjs +117 -31
- package/build/index.mjs +113 -27
- package/package.json +83 -86
- package/types.d.ts +58 -5
package/README.md
CHANGED
|
@@ -1,254 +1,254 @@
|
|
|
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](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
|
-
|
|
7
|
-

|
|
8
|
-
|
|
9
|
-
[](https://deepwiki.com/tripolskypetr/backtest-kit)
|
|
10
|
-
[](https://npmjs.org/package/@backtest-kit/graph)
|
|
11
|
-
[]()
|
|
12
|
-
|
|
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
|
-
|
|
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.
|
|
24
|
-
|
|
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.
|
|
26
|
-
|
|
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>
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
import { extract, run, toSignalDto, File } from '@backtest-kit/pinets';
|
|
38
|
-
import { addStrategySchema, Cache } from 'backtest-kit';
|
|
39
|
-
import { randomString } from 'functools-kit';
|
|
40
|
-
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
41
|
-
|
|
42
|
-
// SourceNode — 4h trend filter, cached per candle interval
|
|
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 }));
|
|
47
|
-
|
|
48
|
-
// SourceNode — 15m entry signal, cached per candle interval
|
|
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);
|
|
62
|
-
|
|
63
|
-
addStrategySchema({
|
|
64
|
-
strategyName: 'mtf_graph_strategy', interval: '5m',
|
|
65
|
-
getSignal: (symbol) => resolve(mtfSignal),
|
|
66
|
-
actions: ['partial_profit_action', 'breakeven_action'],
|
|
67
|
-
});
|
|
68
|
-
```
|
|
69
|
-
|
|
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.
|
|
71
|
-
|
|
72
|
-
</details>
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
## The model
|
|
77
|
-
|
|
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.
|
|
79
|
-
|
|
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.
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## Authoring API
|
|
88
|
-
|
|
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:
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
96
|
-
|
|
97
|
-
const closePrice = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
98
|
-
const candles = await getCandles(symbol, '1h', 1, exchangeName);
|
|
99
|
-
return candles[0].close; // SourceNode<number>
|
|
100
|
-
});
|
|
101
|
-
const volume = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
102
|
-
const candles = await getCandles(symbol, '1h', 1, exchangeName);
|
|
103
|
-
return candles[0].volume; // SourceNode<number>
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const vwap = outputNode(([price, vol]) => price * vol, closePrice, volume); // price, vol: number
|
|
107
|
-
const result = await resolve(vwap); // Promise<number>, inside a strategy
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
</details>
|
|
111
|
-
|
|
112
|
-
<details>
|
|
113
|
-
<summary>Mixed types — heterogeneous inference by position</summary>
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
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>
|
|
119
|
-
|
|
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>
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
</details>
|
|
127
|
-
|
|
128
|
-
<details>
|
|
129
|
-
<summary>Inline anonymous composition — a single object literal</summary>
|
|
130
|
-
|
|
131
|
-
```typescript
|
|
132
|
-
import { NodeType, TypedNode, resolve } from '@backtest-kit/graph';
|
|
133
|
-
|
|
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);
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
</details>
|
|
151
|
-
|
|
152
|
-
<details>
|
|
153
|
-
<summary>Inside a backtest-kit strategy</summary>
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
import { addStrategy } from 'backtest-kit';
|
|
157
|
-
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
158
|
-
|
|
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);
|
|
161
|
-
|
|
162
|
-
addStrategy({
|
|
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
|
-
},
|
|
168
|
-
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
</details>
|
|
172
|
-
|
|
173
|
-
---
|
|
174
|
-
|
|
175
|
-
## Runtime interface & storage
|
|
176
|
-
|
|
177
|
-
<details>
|
|
178
|
-
<summary>Low-level INode — manual construction (post-deserialize / DI)</summary>
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
import { INode, Value } from '@backtest-kit/graph';
|
|
182
|
-
import NodeType from '@backtest-kit/graph/enum/NodeType';
|
|
183
|
-
|
|
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 };
|
|
186
|
-
```
|
|
187
|
-
|
|
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>
|
|
191
|
-
|
|
192
|
-
<details>
|
|
193
|
-
<summary>DB serialization — serialize / deserialize</summary>
|
|
194
|
-
|
|
195
|
-
`serialize` flattens the graph into `IFlatNode[]`, replacing object references in `nodes` with `nodeIds`; `deserialize` rebuilds the tree:
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
import { serialize, deserialize, IFlatNode } from '@backtest-kit/graph';
|
|
199
|
-
|
|
200
|
-
const flat: IFlatNode[] = serialize([vwap]);
|
|
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
|
|
204
|
-
await db.collection('nodes').insertMany(flat);
|
|
205
|
-
|
|
206
|
-
const stored = await db.collection('nodes').find().toArray();
|
|
207
|
-
const roots: INode[] = deserialize(stored); // nodes[] re-wired from nodeIds
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
`fetch` and `compute` are **not** stored — restore them on the application side after `deserialize`.
|
|
211
|
-
|
|
212
|
-
</details>
|
|
213
|
-
|
|
214
|
-
<details>
|
|
215
|
-
<summary>deepFlat — topological traversal</summary>
|
|
216
|
-
|
|
217
|
-
Returns all nodes in topological order (dependencies before parents), deduplicated by reference:
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
import { deepFlat } from '@backtest-kit/graph';
|
|
221
|
-
const all = deepFlat([vwap]); // [closePrice, volume, vwap]
|
|
222
|
-
all.forEach(node => console.log(node.description));
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
</details>
|
|
226
|
-
|
|
227
|
-
---
|
|
228
|
-
|
|
229
|
-
## API reference
|
|
230
|
-
|
|
231
|
-
| Export | Description |
|
|
232
|
-
|--------|-------------|
|
|
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.
|
|
247
|
-
|
|
248
|
-
## 🤝 Contribute
|
|
249
|
-
|
|
250
|
-
Fork / PR on [GitHub](https://github.com/tripolskypetr/backtest-kit).
|
|
251
|
-
|
|
252
|
-
## 📜 License
|
|
253
|
-
|
|
254
|
-
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](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
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
[](https://deepwiki.com/tripolskypetr/backtest-kit)
|
|
10
|
+
[](https://npmjs.org/package/@backtest-kit/graph)
|
|
11
|
+
[]()
|
|
12
|
+
|
|
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
|
+
|
|
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.
|
|
24
|
+
|
|
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.
|
|
26
|
+
|
|
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>
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { extract, run, toSignalDto, File } from '@backtest-kit/pinets';
|
|
38
|
+
import { addStrategySchema, Cache } from 'backtest-kit';
|
|
39
|
+
import { randomString } from 'functools-kit';
|
|
40
|
+
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
41
|
+
|
|
42
|
+
// SourceNode — 4h trend filter, cached per candle interval
|
|
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 }));
|
|
47
|
+
|
|
48
|
+
// SourceNode — 15m entry signal, cached per candle interval
|
|
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);
|
|
62
|
+
|
|
63
|
+
addStrategySchema({
|
|
64
|
+
strategyName: 'mtf_graph_strategy', interval: '5m',
|
|
65
|
+
getSignal: (symbol) => resolve(mtfSignal),
|
|
66
|
+
actions: ['partial_profit_action', 'breakeven_action'],
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
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.
|
|
71
|
+
|
|
72
|
+
</details>
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## The model
|
|
77
|
+
|
|
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.
|
|
79
|
+
|
|
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.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Authoring API
|
|
88
|
+
|
|
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:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
96
|
+
|
|
97
|
+
const closePrice = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
98
|
+
const candles = await getCandles(symbol, '1h', 1, exchangeName);
|
|
99
|
+
return candles[0].close; // SourceNode<number>
|
|
100
|
+
});
|
|
101
|
+
const volume = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
|
|
102
|
+
const candles = await getCandles(symbol, '1h', 1, exchangeName);
|
|
103
|
+
return candles[0].volume; // SourceNode<number>
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const vwap = outputNode(([price, vol]) => price * vol, closePrice, volume); // price, vol: number
|
|
107
|
+
const result = await resolve(vwap); // Promise<number>, inside a strategy
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
</details>
|
|
111
|
+
|
|
112
|
+
<details>
|
|
113
|
+
<summary>Mixed types — heterogeneous inference by position</summary>
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
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>
|
|
119
|
+
|
|
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>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
</details>
|
|
127
|
+
|
|
128
|
+
<details>
|
|
129
|
+
<summary>Inline anonymous composition — a single object literal</summary>
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { NodeType, TypedNode, resolve } from '@backtest-kit/graph';
|
|
133
|
+
|
|
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);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
</details>
|
|
151
|
+
|
|
152
|
+
<details>
|
|
153
|
+
<summary>Inside a backtest-kit strategy</summary>
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { addStrategy } from 'backtest-kit';
|
|
157
|
+
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
|
|
158
|
+
|
|
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);
|
|
161
|
+
|
|
162
|
+
addStrategy({
|
|
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
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
</details>
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Runtime interface & storage
|
|
176
|
+
|
|
177
|
+
<details>
|
|
178
|
+
<summary>Low-level INode — manual construction (post-deserialize / DI)</summary>
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { INode, Value } from '@backtest-kit/graph';
|
|
182
|
+
import NodeType from '@backtest-kit/graph/enum/NodeType';
|
|
183
|
+
|
|
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 };
|
|
186
|
+
```
|
|
187
|
+
|
|
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>
|
|
191
|
+
|
|
192
|
+
<details>
|
|
193
|
+
<summary>DB serialization — serialize / deserialize</summary>
|
|
194
|
+
|
|
195
|
+
`serialize` flattens the graph into `IFlatNode[]`, replacing object references in `nodes` with `nodeIds`; `deserialize` rebuilds the tree:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { serialize, deserialize, IFlatNode } from '@backtest-kit/graph';
|
|
199
|
+
|
|
200
|
+
const flat: IFlatNode[] = serialize([vwap]);
|
|
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
|
|
204
|
+
await db.collection('nodes').insertMany(flat);
|
|
205
|
+
|
|
206
|
+
const stored = await db.collection('nodes').find().toArray();
|
|
207
|
+
const roots: INode[] = deserialize(stored); // nodes[] re-wired from nodeIds
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`fetch` and `compute` are **not** stored — restore them on the application side after `deserialize`.
|
|
211
|
+
|
|
212
|
+
</details>
|
|
213
|
+
|
|
214
|
+
<details>
|
|
215
|
+
<summary>deepFlat — topological traversal</summary>
|
|
216
|
+
|
|
217
|
+
Returns all nodes in topological order (dependencies before parents), deduplicated by reference:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { deepFlat } from '@backtest-kit/graph';
|
|
221
|
+
const all = deepFlat([vwap]); // [closePrice, volume, vwap]
|
|
222
|
+
all.forEach(node => console.log(node.description));
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
</details>
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## API reference
|
|
230
|
+
|
|
231
|
+
| Export | Description |
|
|
232
|
+
|--------|-------------|
|
|
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.
|
|
247
|
+
|
|
248
|
+
## 🤝 Contribute
|
|
249
|
+
|
|
250
|
+
Fork / PR on [GitHub](https://github.com/tripolskypetr/backtest-kit).
|
|
251
|
+
|
|
252
|
+
## 📜 License
|
|
253
|
+
|
|
254
|
+
MIT © [tripolskypetr](https://github.com/tripolskypetr)
|
package/build/index.cjs
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var backtestKit = require('backtest-kit');
|
|
4
3
|
var functoolsKit = require('functools-kit');
|
|
4
|
+
var backtestKit = require('backtest-kit');
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
exports.NodeType = void 0;
|
|
7
7
|
(function (NodeType) {
|
|
8
8
|
NodeType["SourceNode"] = "source_node";
|
|
9
9
|
NodeType["OutputNode"] = "output_node";
|
|
10
|
-
})(NodeType || (NodeType = {}));
|
|
11
|
-
var NodeType
|
|
10
|
+
})(exports.NodeType || (exports.NodeType = {}));
|
|
11
|
+
var NodeType = exports.NodeType;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Создаёт SourceNode с проставленным идентификатором.
|
|
15
|
+
* Для стабильности между перезапусками процесса (JSON round-trip)
|
|
16
|
+
* перезапишите id своим значением после создания.
|
|
17
|
+
*/
|
|
13
18
|
const sourceNode = (fetch) => ({
|
|
14
|
-
type: NodeType
|
|
19
|
+
type: NodeType.SourceNode,
|
|
20
|
+
id: functoolsKit.randomString(),
|
|
15
21
|
fetch,
|
|
16
22
|
});
|
|
23
|
+
/**
|
|
24
|
+
* Создаёт OutputNode с проставленным идентификатором.
|
|
25
|
+
* Для стабильности между перезапусками процесса (JSON round-trip)
|
|
26
|
+
* перезапишите id своим значением после создания.
|
|
27
|
+
*/
|
|
17
28
|
const outputNode = (compute, ...nodes) => ({
|
|
18
|
-
type: NodeType
|
|
29
|
+
type: NodeType.OutputNode,
|
|
30
|
+
id: functoolsKit.randomString(),
|
|
19
31
|
nodes,
|
|
20
32
|
compute,
|
|
21
33
|
});
|
|
@@ -40,6 +52,32 @@ const deepFlat = (arr = []) => {
|
|
|
40
52
|
return result;
|
|
41
53
|
};
|
|
42
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Проверяет граф на циклы (DFS с раскраской: path — серые, done — чёрные).
|
|
57
|
+
* Линейная сложность за счёт done-набора; общие зависимости (ромбы) не
|
|
58
|
+
* обходятся повторно. Циклы возможны только у графов, собранных вручную
|
|
59
|
+
* или восстановленных через deserialize — хелперы sourceNode/outputNode
|
|
60
|
+
* создать цикл не позволяют.
|
|
61
|
+
*/
|
|
62
|
+
const CHECK_CYCLES_FN = (root) => {
|
|
63
|
+
const path = new Set();
|
|
64
|
+
const done = new Set();
|
|
65
|
+
const walk = (node) => {
|
|
66
|
+
if (done.has(node)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (path.has(node)) {
|
|
70
|
+
throw new Error("graph resolve: cycle detected in node graph");
|
|
71
|
+
}
|
|
72
|
+
path.add(node);
|
|
73
|
+
if (node.type === NodeType.OutputNode) {
|
|
74
|
+
(node.nodes ?? []).forEach(walk);
|
|
75
|
+
}
|
|
76
|
+
path.delete(node);
|
|
77
|
+
done.add(node);
|
|
78
|
+
};
|
|
79
|
+
walk(root);
|
|
80
|
+
};
|
|
43
81
|
async function resolve(node) {
|
|
44
82
|
if (!backtestKit.ExecutionContextService.hasContext()) {
|
|
45
83
|
throw new Error("Execution context is required to resolve graph nodes. Please ensure that resolve() is called within a valid execution context.");
|
|
@@ -47,52 +85,95 @@ async function resolve(node) {
|
|
|
47
85
|
if (!backtestKit.MethodContextService.hasContext()) {
|
|
48
86
|
throw new Error("Method context is required to resolve graph nodes. Please ensure that resolve() is called within a valid method context.");
|
|
49
87
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
88
|
+
CHECK_CYCLES_FN(node);
|
|
89
|
+
const { symbol, when } = backtestKit.lib.executionContextService.context;
|
|
90
|
+
const { exchangeName } = backtestKit.lib.methodContextService.context;
|
|
91
|
+
// Одна цена на весь проход: без повторных запросов на каждый SourceNode
|
|
92
|
+
// и без рассинхрона значений между источниками одного вычисления.
|
|
93
|
+
let currentPricePromise = null;
|
|
94
|
+
const GET_PRICE_FN = () => {
|
|
95
|
+
if (!currentPricePromise) {
|
|
96
|
+
currentPricePromise = backtestKit.getAveragePrice(symbol);
|
|
97
|
+
}
|
|
98
|
+
return currentPricePromise;
|
|
99
|
+
};
|
|
100
|
+
const memo = new Map();
|
|
101
|
+
const resolveNode = (target) => {
|
|
102
|
+
const cached = memo.get(target);
|
|
103
|
+
if (cached) {
|
|
104
|
+
return cached;
|
|
105
|
+
}
|
|
106
|
+
const promise = (async () => {
|
|
107
|
+
if (target.type === NodeType.SourceNode) {
|
|
108
|
+
const currentPrice = await GET_PRICE_FN();
|
|
109
|
+
return await target.fetch(symbol, when, currentPrice, exchangeName);
|
|
110
|
+
}
|
|
111
|
+
const values = await Promise.all((target.nodes ?? []).map(resolveNode));
|
|
112
|
+
return await target.compute(values);
|
|
113
|
+
})();
|
|
114
|
+
memo.set(target, promise);
|
|
115
|
+
return promise;
|
|
116
|
+
};
|
|
117
|
+
return await resolveNode(node);
|
|
58
118
|
}
|
|
59
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Нормализация на входе пайплайна: гарантирует каждому узлу идентификатор.
|
|
122
|
+
* Узлы из sourceNode/outputNode приходят уже с id; рукописным INode id
|
|
123
|
+
* доштамповывается прямо на объект — повторный serialize того же графа
|
|
124
|
+
* даёт те же идентификаторы.
|
|
125
|
+
*/
|
|
126
|
+
const ENSURE_ID_FN = (node) => {
|
|
127
|
+
if (!node.id) {
|
|
128
|
+
node.id = functoolsKit.randomString();
|
|
129
|
+
}
|
|
130
|
+
return node;
|
|
131
|
+
};
|
|
60
132
|
/**
|
|
61
133
|
* Преобразует древовидный граф в плоский массив IFlatNode для хранения в БД.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
134
|
+
* Объектные ссылки nodes заменяются на массив nodeIds.
|
|
135
|
+
*
|
|
136
|
+
* Задавайте узлам стабильные id, если планируете восстанавливать fetch/compute
|
|
137
|
+
* после JSON-сериализации: функции в JSON не переживают round-trip, и найти
|
|
138
|
+
* узел для повторной привязки можно только по известному id (случайный id
|
|
139
|
+
* не переживает перезапуск процесса).
|
|
64
140
|
*/
|
|
65
141
|
const serialize = (roots) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
142
|
+
// Вход пайплайна: все узлы получают гарантированный id
|
|
143
|
+
const flat = deepFlat(roots).map(ENSURE_ID_FN);
|
|
144
|
+
const usedIds = new Set();
|
|
69
145
|
flat.forEach((node) => {
|
|
70
|
-
|
|
71
|
-
|
|
146
|
+
if (usedIds.has(node.id)) {
|
|
147
|
+
throw new Error(`graph serialize: duplicate node id "${node.id}"`);
|
|
148
|
+
}
|
|
149
|
+
usedIds.add(node.id);
|
|
72
150
|
});
|
|
73
|
-
// Второй проход: строим IFlatNode с nodeIds вместо nodes
|
|
74
151
|
return flat.map((node) => {
|
|
75
152
|
const flatNode = {
|
|
76
|
-
id:
|
|
153
|
+
id: node.id,
|
|
77
154
|
type: node.type,
|
|
78
155
|
description: node.description,
|
|
79
156
|
fetch: node.fetch,
|
|
80
157
|
compute: node.compute,
|
|
81
|
-
nodeIds: node.nodes?.map((child) =>
|
|
158
|
+
nodeIds: node.nodes?.map((child) => ENSURE_ID_FN(child).id),
|
|
82
159
|
};
|
|
83
160
|
return flatNode;
|
|
84
161
|
});
|
|
85
162
|
};
|
|
86
163
|
/**
|
|
87
164
|
* Восстанавливает древовидный граф из плоского массива IFlatNode.
|
|
88
|
-
* nodes каждого узла заполняется по nodeIds
|
|
165
|
+
* nodes каждого узла заполняется по nodeIds; id сохраняется на узле,
|
|
166
|
+
* так что повторный serialize даёт те же идентификаторы.
|
|
167
|
+
* Ссылка на неизвестный nodeId — ошибка: contract compute(values)
|
|
168
|
+
* позиционный, тихое выпадение элемента сдвинуло бы чужие значения.
|
|
89
169
|
* Возвращает корневые узлы (те, на которые никто не ссылается).
|
|
90
170
|
*/
|
|
91
171
|
const deserialize = (flat) => {
|
|
92
|
-
// Первый проход: создаём
|
|
172
|
+
// Первый проход: создаём узлы, индексируем по id
|
|
93
173
|
const byId = new Map();
|
|
94
174
|
flat.forEach((flatNode) => {
|
|
95
175
|
const node = {
|
|
176
|
+
id: flatNode.id,
|
|
96
177
|
type: flatNode.type,
|
|
97
178
|
description: flatNode.description,
|
|
98
179
|
fetch: flatNode.fetch,
|
|
@@ -100,13 +181,18 @@ const deserialize = (flat) => {
|
|
|
100
181
|
};
|
|
101
182
|
byId.set(flatNode.id, node);
|
|
102
183
|
});
|
|
103
|
-
// Второй проход: проставляем nodes[] по nodeIds
|
|
184
|
+
// Второй проход: проставляем nodes[] по nodeIds (включая пустой массив —
|
|
185
|
+
// OutputNode без зависимостей легален и должен получить nodes: [])
|
|
104
186
|
flat.forEach((flatNode) => {
|
|
105
|
-
if (flatNode.nodeIds
|
|
187
|
+
if (flatNode.nodeIds) {
|
|
106
188
|
const node = byId.get(flatNode.id);
|
|
107
|
-
node.nodes = flatNode.nodeIds
|
|
108
|
-
|
|
109
|
-
|
|
189
|
+
node.nodes = flatNode.nodeIds.map((id) => {
|
|
190
|
+
const child = byId.get(id);
|
|
191
|
+
if (!child) {
|
|
192
|
+
throw new Error(`graph deserialize: node "${flatNode.id}" references unknown nodeId "${id}"`);
|
|
193
|
+
}
|
|
194
|
+
return child;
|
|
195
|
+
});
|
|
110
196
|
}
|
|
111
197
|
});
|
|
112
198
|
// Корневые узлы — те, на которые не ссылается никто другой
|
package/build/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ExecutionContextService, MethodContextService, lib, getAveragePrice } from 'backtest-kit';
|
|
2
1
|
import { randomString } from 'functools-kit';
|
|
2
|
+
import { ExecutionContextService, MethodContextService, lib, getAveragePrice } from 'backtest-kit';
|
|
3
3
|
|
|
4
4
|
var NodeType;
|
|
5
5
|
(function (NodeType) {
|
|
@@ -8,12 +8,24 @@ var NodeType;
|
|
|
8
8
|
})(NodeType || (NodeType = {}));
|
|
9
9
|
var NodeType$1 = NodeType;
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Создаёт SourceNode с проставленным идентификатором.
|
|
13
|
+
* Для стабильности между перезапусками процесса (JSON round-trip)
|
|
14
|
+
* перезапишите id своим значением после создания.
|
|
15
|
+
*/
|
|
11
16
|
const sourceNode = (fetch) => ({
|
|
12
17
|
type: NodeType$1.SourceNode,
|
|
18
|
+
id: randomString(),
|
|
13
19
|
fetch,
|
|
14
20
|
});
|
|
21
|
+
/**
|
|
22
|
+
* Создаёт OutputNode с проставленным идентификатором.
|
|
23
|
+
* Для стабильности между перезапусками процесса (JSON round-trip)
|
|
24
|
+
* перезапишите id своим значением после создания.
|
|
25
|
+
*/
|
|
15
26
|
const outputNode = (compute, ...nodes) => ({
|
|
16
27
|
type: NodeType$1.OutputNode,
|
|
28
|
+
id: randomString(),
|
|
17
29
|
nodes,
|
|
18
30
|
compute,
|
|
19
31
|
});
|
|
@@ -38,6 +50,32 @@ const deepFlat = (arr = []) => {
|
|
|
38
50
|
return result;
|
|
39
51
|
};
|
|
40
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Проверяет граф на циклы (DFS с раскраской: path — серые, done — чёрные).
|
|
55
|
+
* Линейная сложность за счёт done-набора; общие зависимости (ромбы) не
|
|
56
|
+
* обходятся повторно. Циклы возможны только у графов, собранных вручную
|
|
57
|
+
* или восстановленных через deserialize — хелперы sourceNode/outputNode
|
|
58
|
+
* создать цикл не позволяют.
|
|
59
|
+
*/
|
|
60
|
+
const CHECK_CYCLES_FN = (root) => {
|
|
61
|
+
const path = new Set();
|
|
62
|
+
const done = new Set();
|
|
63
|
+
const walk = (node) => {
|
|
64
|
+
if (done.has(node)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (path.has(node)) {
|
|
68
|
+
throw new Error("graph resolve: cycle detected in node graph");
|
|
69
|
+
}
|
|
70
|
+
path.add(node);
|
|
71
|
+
if (node.type === NodeType$1.OutputNode) {
|
|
72
|
+
(node.nodes ?? []).forEach(walk);
|
|
73
|
+
}
|
|
74
|
+
path.delete(node);
|
|
75
|
+
done.add(node);
|
|
76
|
+
};
|
|
77
|
+
walk(root);
|
|
78
|
+
};
|
|
41
79
|
async function resolve(node) {
|
|
42
80
|
if (!ExecutionContextService.hasContext()) {
|
|
43
81
|
throw new Error("Execution context is required to resolve graph nodes. Please ensure that resolve() is called within a valid execution context.");
|
|
@@ -45,52 +83,95 @@ async function resolve(node) {
|
|
|
45
83
|
if (!MethodContextService.hasContext()) {
|
|
46
84
|
throw new Error("Method context is required to resolve graph nodes. Please ensure that resolve() is called within a valid method context.");
|
|
47
85
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
86
|
+
CHECK_CYCLES_FN(node);
|
|
87
|
+
const { symbol, when } = lib.executionContextService.context;
|
|
88
|
+
const { exchangeName } = lib.methodContextService.context;
|
|
89
|
+
// Одна цена на весь проход: без повторных запросов на каждый SourceNode
|
|
90
|
+
// и без рассинхрона значений между источниками одного вычисления.
|
|
91
|
+
let currentPricePromise = null;
|
|
92
|
+
const GET_PRICE_FN = () => {
|
|
93
|
+
if (!currentPricePromise) {
|
|
94
|
+
currentPricePromise = getAveragePrice(symbol);
|
|
95
|
+
}
|
|
96
|
+
return currentPricePromise;
|
|
97
|
+
};
|
|
98
|
+
const memo = new Map();
|
|
99
|
+
const resolveNode = (target) => {
|
|
100
|
+
const cached = memo.get(target);
|
|
101
|
+
if (cached) {
|
|
102
|
+
return cached;
|
|
103
|
+
}
|
|
104
|
+
const promise = (async () => {
|
|
105
|
+
if (target.type === NodeType$1.SourceNode) {
|
|
106
|
+
const currentPrice = await GET_PRICE_FN();
|
|
107
|
+
return await target.fetch(symbol, when, currentPrice, exchangeName);
|
|
108
|
+
}
|
|
109
|
+
const values = await Promise.all((target.nodes ?? []).map(resolveNode));
|
|
110
|
+
return await target.compute(values);
|
|
111
|
+
})();
|
|
112
|
+
memo.set(target, promise);
|
|
113
|
+
return promise;
|
|
114
|
+
};
|
|
115
|
+
return await resolveNode(node);
|
|
56
116
|
}
|
|
57
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Нормализация на входе пайплайна: гарантирует каждому узлу идентификатор.
|
|
120
|
+
* Узлы из sourceNode/outputNode приходят уже с id; рукописным INode id
|
|
121
|
+
* доштамповывается прямо на объект — повторный serialize того же графа
|
|
122
|
+
* даёт те же идентификаторы.
|
|
123
|
+
*/
|
|
124
|
+
const ENSURE_ID_FN = (node) => {
|
|
125
|
+
if (!node.id) {
|
|
126
|
+
node.id = randomString();
|
|
127
|
+
}
|
|
128
|
+
return node;
|
|
129
|
+
};
|
|
58
130
|
/**
|
|
59
131
|
* Преобразует древовидный граф в плоский массив IFlatNode для хранения в БД.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
132
|
+
* Объектные ссылки nodes заменяются на массив nodeIds.
|
|
133
|
+
*
|
|
134
|
+
* Задавайте узлам стабильные id, если планируете восстанавливать fetch/compute
|
|
135
|
+
* после JSON-сериализации: функции в JSON не переживают round-trip, и найти
|
|
136
|
+
* узел для повторной привязки можно только по известному id (случайный id
|
|
137
|
+
* не переживает перезапуск процесса).
|
|
62
138
|
*/
|
|
63
139
|
const serialize = (roots) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
140
|
+
// Вход пайплайна: все узлы получают гарантированный id
|
|
141
|
+
const flat = deepFlat(roots).map(ENSURE_ID_FN);
|
|
142
|
+
const usedIds = new Set();
|
|
67
143
|
flat.forEach((node) => {
|
|
68
|
-
|
|
69
|
-
|
|
144
|
+
if (usedIds.has(node.id)) {
|
|
145
|
+
throw new Error(`graph serialize: duplicate node id "${node.id}"`);
|
|
146
|
+
}
|
|
147
|
+
usedIds.add(node.id);
|
|
70
148
|
});
|
|
71
|
-
// Второй проход: строим IFlatNode с nodeIds вместо nodes
|
|
72
149
|
return flat.map((node) => {
|
|
73
150
|
const flatNode = {
|
|
74
|
-
id:
|
|
151
|
+
id: node.id,
|
|
75
152
|
type: node.type,
|
|
76
153
|
description: node.description,
|
|
77
154
|
fetch: node.fetch,
|
|
78
155
|
compute: node.compute,
|
|
79
|
-
nodeIds: node.nodes?.map((child) =>
|
|
156
|
+
nodeIds: node.nodes?.map((child) => ENSURE_ID_FN(child).id),
|
|
80
157
|
};
|
|
81
158
|
return flatNode;
|
|
82
159
|
});
|
|
83
160
|
};
|
|
84
161
|
/**
|
|
85
162
|
* Восстанавливает древовидный граф из плоского массива IFlatNode.
|
|
86
|
-
* nodes каждого узла заполняется по nodeIds
|
|
163
|
+
* nodes каждого узла заполняется по nodeIds; id сохраняется на узле,
|
|
164
|
+
* так что повторный serialize даёт те же идентификаторы.
|
|
165
|
+
* Ссылка на неизвестный nodeId — ошибка: contract compute(values)
|
|
166
|
+
* позиционный, тихое выпадение элемента сдвинуло бы чужие значения.
|
|
87
167
|
* Возвращает корневые узлы (те, на которые никто не ссылается).
|
|
88
168
|
*/
|
|
89
169
|
const deserialize = (flat) => {
|
|
90
|
-
// Первый проход: создаём
|
|
170
|
+
// Первый проход: создаём узлы, индексируем по id
|
|
91
171
|
const byId = new Map();
|
|
92
172
|
flat.forEach((flatNode) => {
|
|
93
173
|
const node = {
|
|
174
|
+
id: flatNode.id,
|
|
94
175
|
type: flatNode.type,
|
|
95
176
|
description: flatNode.description,
|
|
96
177
|
fetch: flatNode.fetch,
|
|
@@ -98,13 +179,18 @@ const deserialize = (flat) => {
|
|
|
98
179
|
};
|
|
99
180
|
byId.set(flatNode.id, node);
|
|
100
181
|
});
|
|
101
|
-
// Второй проход: проставляем nodes[] по nodeIds
|
|
182
|
+
// Второй проход: проставляем nodes[] по nodeIds (включая пустой массив —
|
|
183
|
+
// OutputNode без зависимостей легален и должен получить nodes: [])
|
|
102
184
|
flat.forEach((flatNode) => {
|
|
103
|
-
if (flatNode.nodeIds
|
|
185
|
+
if (flatNode.nodeIds) {
|
|
104
186
|
const node = byId.get(flatNode.id);
|
|
105
|
-
node.nodes = flatNode.nodeIds
|
|
106
|
-
|
|
107
|
-
|
|
187
|
+
node.nodes = flatNode.nodeIds.map((id) => {
|
|
188
|
+
const child = byId.get(id);
|
|
189
|
+
if (!child) {
|
|
190
|
+
throw new Error(`graph deserialize: node "${flatNode.id}" references unknown nodeId "${id}"`);
|
|
191
|
+
}
|
|
192
|
+
return child;
|
|
193
|
+
});
|
|
108
194
|
}
|
|
109
195
|
});
|
|
110
196
|
// Корневые узлы — те, на которые не ссылается никто другой
|
|
@@ -114,4 +200,4 @@ const deserialize = (flat) => {
|
|
|
114
200
|
.map(([, node]) => node);
|
|
115
201
|
};
|
|
116
202
|
|
|
117
|
-
export { deepFlat, deserialize, outputNode, resolve, serialize, sourceNode };
|
|
203
|
+
export { NodeType, deepFlat, deserialize, outputNode, resolve, serialize, sourceNode };
|
package/package.json
CHANGED
|
@@ -1,86 +1,83 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@backtest-kit/graph",
|
|
3
|
-
"version": "
|
|
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": "
|
|
71
|
-
"worker-testbed": "
|
|
72
|
-
},
|
|
73
|
-
"peerDependencies": {
|
|
74
|
-
"backtest-kit": "^
|
|
75
|
-
"typescript": "^5.0.0"
|
|
76
|
-
},
|
|
77
|
-
"dependencies": {
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
"access": "public"
|
|
85
|
-
}
|
|
86
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@backtest-kit/graph",
|
|
3
|
+
"version": "15.0.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": "15.0.0",
|
|
71
|
+
"worker-testbed": "3.0.0"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"backtest-kit": "^15.0.0",
|
|
75
|
+
"typescript": "^5.0.0"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"functools-kit": "^4.0.0"
|
|
79
|
+
},
|
|
80
|
+
"publishConfig": {
|
|
81
|
+
"access": "public"
|
|
82
|
+
}
|
|
83
|
+
}
|
package/types.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ interface INode {
|
|
|
19
19
|
* Тип узла для логического ветвления при исполнении графа
|
|
20
20
|
*/
|
|
21
21
|
type: NodeType;
|
|
22
|
+
/**
|
|
23
|
+
* Стабильный идентификатор узла. Хелперы sourceNode/outputNode
|
|
24
|
+
* проставляют его при создании; для рукописных INode он будет
|
|
25
|
+
* доштампован при первом serialize. Задавайте свой id, если после
|
|
26
|
+
* JSON round-trip нужно повторно привязать fetch/compute к узлам
|
|
27
|
+
* (случайный id не переживает перезапуск процесса).
|
|
28
|
+
*/
|
|
29
|
+
id?: string;
|
|
22
30
|
/**
|
|
23
31
|
* Человеко-читаемое описание узла, не влияет на исполнение графа.
|
|
24
32
|
*/
|
|
@@ -41,6 +49,15 @@ interface INode {
|
|
|
41
49
|
nodes?: INode[];
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Внутренний тип: узел с гарантированно проставленным идентификатором.
|
|
54
|
+
* В таком виде узлы существуют после входа в пайплайн (хелперы
|
|
55
|
+
* sourceNode/outputNode, serialize, deserialize).
|
|
56
|
+
*/
|
|
57
|
+
interface INodeInternal extends INode {
|
|
58
|
+
id: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
44
61
|
/**
|
|
45
62
|
* Маппинг tuple нод в tuple их resolved-значений.
|
|
46
63
|
* Сохраняет позиционную структуру: [SourceNode<number>, SourceNode<string>] → [number, string].
|
|
@@ -59,6 +76,12 @@ type InferNodeValue<T extends TypedNode> = T extends SourceNode<infer V> ? V : T
|
|
|
59
76
|
*/
|
|
60
77
|
type SourceNode<T extends Value = Value> = {
|
|
61
78
|
type: NodeType.SourceNode;
|
|
79
|
+
/**
|
|
80
|
+
* Идентификатор узла. Хелпер sourceNode проставляет случайный id при
|
|
81
|
+
* создании; перезапишите своим стабильным значением, если узел должен
|
|
82
|
+
* переживать JSON round-trip (повторная привязка fetch по id).
|
|
83
|
+
*/
|
|
84
|
+
id: string;
|
|
62
85
|
description?: string;
|
|
63
86
|
fetch: (symbol: string, when: Date, currentPrice: number, exchangeName: ExchangeName) => Promise<T> | T;
|
|
64
87
|
};
|
|
@@ -69,6 +92,12 @@ type SourceNode<T extends Value = Value> = {
|
|
|
69
92
|
*/
|
|
70
93
|
type OutputNode<TNodes extends TypedNode[] = TypedNode[], TResult extends Value = Value> = {
|
|
71
94
|
type: NodeType.OutputNode;
|
|
95
|
+
/**
|
|
96
|
+
* Идентификатор узла. Хелпер outputNode проставляет случайный id при
|
|
97
|
+
* создании; перезапишите своим стабильным значением, если узел должен
|
|
98
|
+
* переживать JSON round-trip (повторная привязка compute по id).
|
|
99
|
+
*/
|
|
100
|
+
id: string;
|
|
72
101
|
description?: string;
|
|
73
102
|
nodes: TNodes;
|
|
74
103
|
compute: (values: InferValues<TNodes>) => Promise<TResult> | TResult;
|
|
@@ -79,7 +108,17 @@ type OutputNode<TNodes extends TypedNode[] = TypedNode[], TResult extends Value
|
|
|
79
108
|
*/
|
|
80
109
|
type TypedNode = SourceNode<Value> | OutputNode<TypedNode[], Value>;
|
|
81
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Создаёт SourceNode с проставленным идентификатором.
|
|
113
|
+
* Для стабильности между перезапусками процесса (JSON round-trip)
|
|
114
|
+
* перезапишите id своим значением после создания.
|
|
115
|
+
*/
|
|
82
116
|
declare const sourceNode: <T extends Value>(fetch: (symbol: string, when: Date, currentPrice: number, exchangeName: ExchangeName) => Promise<T> | T) => SourceNode<T>;
|
|
117
|
+
/**
|
|
118
|
+
* Создаёт OutputNode с проставленным идентификатором.
|
|
119
|
+
* Для стабильности между перезапусками процесса (JSON round-trip)
|
|
120
|
+
* перезапишите id своим значением после создания.
|
|
121
|
+
*/
|
|
83
122
|
declare const outputNode: <TNodes extends TypedNode[], TResult extends Value = Value>(compute: (values: InferValues<TNodes>) => Promise<TResult> | TResult, ...nodes: TNodes) => OutputNode<TNodes, TResult>;
|
|
84
123
|
|
|
85
124
|
/**
|
|
@@ -94,6 +133,13 @@ declare const deepFlat: (arr?: INode[]) => INode[];
|
|
|
94
133
|
* Для SourceNode вызывает fetch().
|
|
95
134
|
* Для OutputNode сначала резолвит все дочерние nodes параллельно,
|
|
96
135
|
* затем передаёт их типизированные значения в compute().
|
|
136
|
+
*
|
|
137
|
+
* Гарантии одного прохода:
|
|
138
|
+
* - каждый узел вычисляется ровно один раз (мемоизация по ссылке):
|
|
139
|
+
* общая зависимость в «ромбе» даёт один fetch и одно согласованное
|
|
140
|
+
* значение для всех потребителей;
|
|
141
|
+
* - currentPrice запрашивается один раз и передаётся всем SourceNode;
|
|
142
|
+
* - цикл в графе даёт понятную ошибку, а не переполнение стека.
|
|
97
143
|
*/
|
|
98
144
|
declare function resolve<V extends Value>(node: SourceNode<V>): Promise<V>;
|
|
99
145
|
declare function resolve<TNodes extends TypedNode[], V extends Value>(node: OutputNode<TNodes, V>): Promise<V>;
|
|
@@ -134,15 +180,22 @@ interface IFlatNode {
|
|
|
134
180
|
|
|
135
181
|
/**
|
|
136
182
|
* Преобразует древовидный граф в плоский массив IFlatNode для хранения в БД.
|
|
137
|
-
*
|
|
138
|
-
*
|
|
183
|
+
* Объектные ссылки nodes заменяются на массив nodeIds.
|
|
184
|
+
*
|
|
185
|
+
* Задавайте узлам стабильные id, если планируете восстанавливать fetch/compute
|
|
186
|
+
* после JSON-сериализации: функции в JSON не переживают round-trip, и найти
|
|
187
|
+
* узел для повторной привязки можно только по известному id (случайный id
|
|
188
|
+
* не переживает перезапуск процесса).
|
|
139
189
|
*/
|
|
140
190
|
declare const serialize: (roots: INode[]) => IFlatNode[];
|
|
141
191
|
/**
|
|
142
192
|
* Восстанавливает древовидный граф из плоского массива IFlatNode.
|
|
143
|
-
* nodes каждого узла заполняется по nodeIds
|
|
193
|
+
* nodes каждого узла заполняется по nodeIds; id сохраняется на узле,
|
|
194
|
+
* так что повторный serialize даёт те же идентификаторы.
|
|
195
|
+
* Ссылка на неизвестный nodeId — ошибка: contract compute(values)
|
|
196
|
+
* позиционный, тихое выпадение элемента сдвинуло бы чужие значения.
|
|
144
197
|
* Возвращает корневые узлы (те, на которые никто не ссылается).
|
|
145
198
|
*/
|
|
146
|
-
declare const deserialize: (flat: IFlatNode[]) =>
|
|
199
|
+
declare const deserialize: (flat: IFlatNode[]) => INodeInternal[];
|
|
147
200
|
|
|
148
|
-
export { type IFlatNode, type INode, type TypedNode, type Value, deepFlat, deserialize, outputNode, resolve, serialize, sourceNode };
|
|
201
|
+
export { type IFlatNode, type INode, type INodeInternal, NodeType, type TypedNode, type Value, deepFlat, deserialize, outputNode, resolve, serialize, sourceNode };
|