@backtest-kit/graph 3.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 ADDED
@@ -0,0 +1,304 @@
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/screenshot8.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/example_02_first_backtest.html)** | 🌟 **[GitHub](https://github.com/tripolskypetr/backtest-kit)**
14
+
15
+ ## 🔥 Multi-timeframe Pine Script strategy
16
+
17
+ 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.
18
+
19
+ ```typescript
20
+ import { extract, run, toSignalDto, File } from '@backtest-kit/pinets';
21
+ import { addStrategySchema, Cache } from 'backtest-kit';
22
+ import { randomString } from 'functools-kit';
23
+ import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
24
+
25
+ // SourceNode — 4h trend filter, cached per candle interval
26
+ const higherTimeframe = sourceNode(
27
+ Cache.fn(
28
+ async (symbol) => {
29
+ const plots = await run(File.fromPath('timeframe_4h.pine'), {
30
+ symbol,
31
+ timeframe: '4h',
32
+ limit: 100,
33
+ });
34
+ return extract(plots, {
35
+ allowLong: 'AllowLong',
36
+ allowShort: 'AllowShort',
37
+ noTrades: 'NoTrades',
38
+ });
39
+ },
40
+ { interval: '4h', key: ([symbol]) => symbol },
41
+ ),
42
+ );
43
+
44
+ // SourceNode — 15m entry signal, cached per candle interval
45
+ const lowerTimeframe = sourceNode(
46
+ Cache.fn(
47
+ async (symbol) => {
48
+ const plots = await run(File.fromPath('timeframe_15m.pine'), {
49
+ symbol,
50
+ timeframe: '15m',
51
+ limit: 100,
52
+ });
53
+ return extract(plots, {
54
+ position: 'Signal',
55
+ priceOpen: 'Close',
56
+ priceTakeProfit: 'TakeProfit',
57
+ priceStopLoss: 'StopLoss',
58
+ minuteEstimatedTime: 'EstimatedTime',
59
+ });
60
+ },
61
+ { interval: '15m', key: ([symbol]) => symbol },
62
+ ),
63
+ );
64
+
65
+ // OutputNode — applies MTF filter, returns ISignalDto or null
66
+ const mtfSignal = outputNode(
67
+ async ([higher, lower]) => {
68
+ if (higher.noTrades) return null;
69
+ if (lower.position === 0) return null;
70
+ if (higher.allowShort && lower.position === 1) return null;
71
+ if (higher.allowLong && lower.position === -1) return null;
72
+
73
+ return toSignalDto(randomString(), lower, null);
74
+ },
75
+ higherTimeframe,
76
+ lowerTimeframe,
77
+ );
78
+
79
+ addStrategySchema({
80
+ strategyName: 'mtf_graph_strategy',
81
+ interval: '5m',
82
+ getSignal: (symbol) => resolve(mtfSignal),
83
+ actions: ['partial_profit_action', 'breakeven_action'],
84
+ });
85
+ ```
86
+
87
+ 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.
88
+
89
+
90
+ ## 🚀 Installation
91
+
92
+ ```bash
93
+ npm install @backtest-kit/graph backtest-kit
94
+ ```
95
+
96
+ ## ✨ Features
97
+
98
+ - 📊 **DAG execution**: Nodes are resolved bottom-up in topological order with `Promise.all` parallelism
99
+ - 🔒 **Type-safe values**: TypeScript infers the return type of every node through the graph via generics
100
+ - 🧱 **Two APIs**: Low-level `INode` for runtime/storage, high-level `TypedNode` + builders for authoring
101
+ - 💾 **DB-ready serialization**: `serialize` / `deserialize` convert the graph to a flat `IFlatNode[]` list with `id` / `nodeIds`
102
+ - 🔌 **Context-aware fetch**: `SourceNode.fetch` receives `(symbol, when, exchangeName)` from the execution context automatically
103
+
104
+ ## 📖 Usage
105
+
106
+ ### Quick Start — builder API
107
+
108
+ Use `sourceNode` and `outputNode` to define a typed computation graph. TypeScript infers the type of `values` in `compute` from the `nodes` passed to `outputNode`:
109
+
110
+ ```typescript
111
+ import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
112
+
113
+ // SourceNode<number> — fetch receives symbol, when, exchangeName from context
114
+ const closePrice = sourceNode(async (symbol, when, exchangeName) => {
115
+ const candles = await getCandles(symbol, '1h', 1, exchangeName);
116
+ return candles[0].close; // number
117
+ });
118
+
119
+ // SourceNode<number>
120
+ const volume = sourceNode(async (symbol, when, exchangeName) => {
121
+ const candles = await getCandles(symbol, '1h', 1, exchangeName);
122
+ return candles[0].volume; // number
123
+ });
124
+
125
+ // OutputNode<[SourceNode<number>, SourceNode<number>], number>
126
+ // price and vol are automatically number
127
+ const vwap = outputNode(
128
+ ([price, vol]) => price * vol,
129
+ closePrice,
130
+ volume,
131
+ );
132
+
133
+ // Resolve inside a backtest-kit strategy
134
+ const result = await resolve(vwap); // Promise<number>
135
+ ```
136
+
137
+ ### Inline anonymous composition
138
+
139
+ The entire graph can be defined as a single object literal.
140
+
141
+ ```typescript
142
+ import { NodeType } from '@backtest-kit/graph';
143
+ import { TypedNode, resolve } from '@backtest-kit/graph';
144
+
145
+ const signal: TypedNode = {
146
+ type: NodeType.OutputNode,
147
+ nodes: [
148
+ {
149
+ type: NodeType.SourceNode,
150
+ fetch: async (symbol, when, exchangeName) => {
151
+ const plots = await run(File.fromPath('timeframe_4h.pine'), { symbol, timeframe: '4h', limit: 100 });
152
+ return extract(plots, { allowLong: 'AllowLong', allowShort: 'AllowShort', noTrades: 'NoTrades' });
153
+ },
154
+ },
155
+ {
156
+ type: NodeType.SourceNode,
157
+ fetch: async (symbol, when, exchangeName) => {
158
+ const plots = await run(File.fromPath('timeframe_15m.pine'), { symbol, timeframe: '15m', limit: 100 });
159
+ return extract(plots, { position: 'Signal', priceOpen: 'Close', priceTakeProfit: 'TakeProfit', priceStopLoss: 'StopLoss' });
160
+ },
161
+ },
162
+ ],
163
+ compute: ([higher, lower]) => {
164
+ if (higher.noTrades || lower.position === 0) return null;
165
+ if (higher.allowShort && lower.position === 1) return null;
166
+ if (higher.allowLong && lower.position === -1) return null;
167
+ return lower.position;
168
+ },
169
+ };
170
+
171
+ const result = await resolve(signal);
172
+ ```
173
+
174
+ ### Mixed types
175
+
176
+ TypeScript correctly infers heterogeneous types by position in `nodes`:
177
+
178
+ ```typescript
179
+ const price = sourceNode(async (symbol) => 42); // SourceNode<number>
180
+ const name = sourceNode(async (symbol) => 'BTCUSDT'); // SourceNode<string>
181
+ const flag = sourceNode(async (symbol) => true); // SourceNode<boolean>
182
+
183
+ const result = outputNode(
184
+ ([p, n, f]) => `${n}: ${p} (active: ${f})`, // p: number, n: string, f: boolean
185
+ price,
186
+ name,
187
+ flag,
188
+ );
189
+ // OutputNode<[SourceNode<number>, SourceNode<string>, SourceNode<boolean>], string>
190
+ ```
191
+
192
+ ### Using inside a backtest-kit strategy
193
+
194
+ ```typescript
195
+ import { addStrategy } from 'backtest-kit';
196
+ import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
197
+
198
+ const rsi = sourceNode(async (symbol, when, exchangeName) => {
199
+ // ... compute RSI
200
+ return 55.2;
201
+ });
202
+
203
+ const signal = outputNode(
204
+ ([rsiValue]) => rsiValue < 30 ? 1 : rsiValue > 70 ? -1 : 0,
205
+ rsi,
206
+ );
207
+
208
+ addStrategy({
209
+ strategyName: 'graph-rsi',
210
+ interval: '1h',
211
+ riskName: 'demo',
212
+ getSignal: async (symbol) => {
213
+ const direction = await resolve(signal); // 1 | -1 | 0
214
+ return direction === 1
215
+ ? { position: 'long', ... }
216
+ : null;
217
+ },
218
+ });
219
+ ```
220
+
221
+ ### Low-level INode
222
+
223
+ For manual graph construction without builders (e.g. after deserialization or in a DI container):
224
+
225
+ ```typescript
226
+ import { INode, Value } from '@backtest-kit/graph';
227
+ import NodeType from '@backtest-kit/graph/enum/NodeType';
228
+
229
+ const priceNode: INode = {
230
+ type: NodeType.SourceNode,
231
+ description: 'Close price',
232
+ fetch: async (symbol, when, exchangeName) => 42,
233
+ };
234
+
235
+ const outputNode: INode = {
236
+ type: NodeType.OutputNode,
237
+ description: 'Doubled price',
238
+ nodes: [priceNode],
239
+ compute: ([price]) => (price as number) * 2,
240
+ };
241
+ ```
242
+
243
+ > `INode` has no generic parameters — `values` in `compute` is typed as `Value[]`. Use `TypedNode` and builders for full IntelliSense.
244
+
245
+ ### DB serialization
246
+
247
+ `serialize` flattens the graph into an `IFlatNode[]` array, replacing object references in `nodes` with `nodeIds`. `deserialize` reconstructs the tree:
248
+
249
+ ```typescript
250
+ import { serialize, deserialize, IFlatNode } from '@backtest-kit/graph';
251
+
252
+ // Graph → flat array for DB
253
+ const flat: IFlatNode[] = serialize([vwap]);
254
+ // [
255
+ // { id: 'abc', type: 'source_node', nodeIds: [] }, // closePrice
256
+ // { id: 'def', type: 'source_node', nodeIds: [] }, // volume
257
+ // { id: 'ghi', type: 'output_node', nodeIds: ['abc', 'def'] }, // vwap
258
+ // ]
259
+
260
+ // Save to DB
261
+ await db.collection('nodes').insertMany(flat);
262
+
263
+ // Load from DB and reconstruct the graph
264
+ const stored: IFlatNode[] = await db.collection('nodes').find().toArray();
265
+ const roots: INode[] = deserialize(stored); // nodes[] is wired up from nodeIds
266
+ ```
267
+
268
+ > `fetch` and `compute` are not stored in the DB — they must be restored on the application side after `deserialize`.
269
+
270
+ ### deepFlat — traversal utility
271
+
272
+ `deepFlat` returns all nodes in topological order (dependencies before parents), deduplicated by reference:
273
+
274
+ ```typescript
275
+ import { deepFlat } from '@backtest-kit/graph';
276
+
277
+ const all = deepFlat([vwap]);
278
+ // [closePrice, volume, vwap] — dependencies first
279
+
280
+ all.forEach(node => console.log(node.description));
281
+ ```
282
+
283
+ ## 📋 API Reference
284
+
285
+ | Export | Description |
286
+ |--------|-------------|
287
+ | **`sourceNode(fetch)`** | Builder — creates a typed source node |
288
+ | **`outputNode(compute, ...nodes)`** | Builder — creates a typed output node, infers `values` types from `nodes` |
289
+ | **`resolve(node)`** | Recursively resolves a node graph within backtest-kit execution context |
290
+ | **`serialize(roots)`** | Flattens a node tree into `IFlatNode[]` for DB storage |
291
+ | **`deserialize(flat)`** | Reconstructs a node tree from `IFlatNode[]`, returns root nodes |
292
+ | **`deepFlat(nodes)`** | Utility — returns all nodes in topological order (dependencies first) |
293
+ | **`INode`** | Base runtime interface (untyped, used internally and for serialization) |
294
+ | **`TypedNode`** | Discriminated union for authoring with full IntelliSense |
295
+ | **`IFlatNode`** | Serialized node shape for DB storage |
296
+ | **`Value`** | `string \| number \| boolean \| null` |
297
+
298
+ ## 🤝 Contribute
299
+
300
+ Fork/PR on [GitHub](https://github.com/tripolskypetr/backtest-kit).
301
+
302
+ ## 📜 License
303
+
304
+ MIT © [tripolskypetr](https://github.com/tripolskypetr)
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ var backtestKit = require('backtest-kit');
4
+ var functoolsKit = require('functools-kit');
5
+
6
+ var NodeType;
7
+ (function (NodeType) {
8
+ NodeType["SourceNode"] = "source_node";
9
+ NodeType["OutputNode"] = "output_node";
10
+ })(NodeType || (NodeType = {}));
11
+ var NodeType$1 = NodeType;
12
+
13
+ const sourceNode = (fetch) => ({
14
+ type: NodeType$1.SourceNode,
15
+ fetch,
16
+ });
17
+ const outputNode = (compute, ...nodes) => ({
18
+ type: NodeType$1.OutputNode,
19
+ nodes,
20
+ compute,
21
+ });
22
+
23
+ /**
24
+ * Рекурсивно разворачивает граф узлов в плоский массив.
25
+ * Порядок: сначала зависимости (children), затем родитель — топологический порядок.
26
+ * Дубликаты (один узел может быть зависимостью нескольких) исключаются по ссылке.
27
+ */
28
+ const deepFlat = (arr = []) => {
29
+ const result = [];
30
+ const seen = new Set();
31
+ const process = (entries = []) => entries.forEach((entry) => {
32
+ if (seen.has(entry)) {
33
+ return;
34
+ }
35
+ seen.add(entry);
36
+ process(entry.nodes ?? []);
37
+ result.push(entry);
38
+ });
39
+ process(arr);
40
+ return result;
41
+ };
42
+
43
+ /**
44
+ * Рекурсивно вычисляет значение узла графа.
45
+ * Для SourceNode вызывает fetch().
46
+ * Для OutputNode сначала резолвит все дочерние nodes параллельно,
47
+ * затем передаёт их типизированные значения в compute().
48
+ */
49
+ const resolve = async (node) => {
50
+ if (!backtestKit.ExecutionContextService.hasContext()) {
51
+ throw new Error("Execution context is required to resolve graph nodes. Please ensure that resolve() is called within a valid execution context.");
52
+ }
53
+ if (!backtestKit.MethodContextService.hasContext()) {
54
+ throw new Error("Method context is required to resolve graph nodes. Please ensure that resolve() is called within a valid method context.");
55
+ }
56
+ if (node.type === NodeType$1.SourceNode) {
57
+ const { symbol, when } = backtestKit.lib.executionContextService.context;
58
+ const { exchangeName } = backtestKit.lib.methodContextService.context;
59
+ return node.fetch(symbol, when, exchangeName);
60
+ }
61
+ const values = await Promise.all(node.nodes.map(resolve));
62
+ return node.compute(values);
63
+ };
64
+
65
+ /**
66
+ * Преобразует древовидный граф в плоский массив IFlatNode для хранения в БД.
67
+ * Каждому узлу присваивается уникальный id (если не задан),
68
+ * объектные ссылки nodes заменяются на массив nodeIds.
69
+ */
70
+ const serialize = (roots) => {
71
+ const flat = deepFlat(roots);
72
+ // Первый проход: назначаем id каждому уникальному узлу
73
+ const idMap = new Map();
74
+ flat.forEach((node) => {
75
+ const id = functoolsKit.randomString();
76
+ idMap.set(node, id);
77
+ });
78
+ // Второй проход: строим IFlatNode с nodeIds вместо nodes
79
+ return flat.map((node) => {
80
+ const flatNode = {
81
+ id: idMap.get(node),
82
+ type: node.type,
83
+ description: node.description,
84
+ fetch: node.fetch,
85
+ compute: node.compute,
86
+ nodeIds: node.nodes?.map((child) => idMap.get(child)),
87
+ };
88
+ return flatNode;
89
+ });
90
+ };
91
+ /**
92
+ * Восстанавливает древовидный граф из плоского массива IFlatNode.
93
+ * nodes каждого узла заполняется по nodeIds.
94
+ * Возвращает корневые узлы (те, на которые никто не ссылается).
95
+ */
96
+ const deserialize = (flat) => {
97
+ // Первый проход: создаём INode-объекты, индексируем по id
98
+ const byId = new Map();
99
+ flat.forEach((flatNode) => {
100
+ const node = {
101
+ type: flatNode.type,
102
+ description: flatNode.description,
103
+ fetch: flatNode.fetch,
104
+ compute: flatNode.compute,
105
+ };
106
+ byId.set(flatNode.id, node);
107
+ });
108
+ // Второй проход: проставляем nodes[] по nodeIds
109
+ flat.forEach((flatNode) => {
110
+ if (flatNode.nodeIds?.length) {
111
+ const node = byId.get(flatNode.id);
112
+ node.nodes = flatNode.nodeIds
113
+ .map((id) => byId.get(id))
114
+ .filter((n) => n !== undefined);
115
+ }
116
+ });
117
+ // Корневые узлы — те, на которые не ссылается никто другой
118
+ const referenced = new Set(flat.flatMap((n) => n.nodeIds ?? []));
119
+ return [...byId.entries()]
120
+ .filter(([id]) => !referenced.has(id))
121
+ .map(([, node]) => node);
122
+ };
123
+
124
+ exports.deepFlat = deepFlat;
125
+ exports.deserialize = deserialize;
126
+ exports.outputNode = outputNode;
127
+ exports.resolve = resolve;
128
+ exports.serialize = serialize;
129
+ exports.sourceNode = sourceNode;
@@ -0,0 +1,122 @@
1
+ import { ExecutionContextService, MethodContextService, lib } from 'backtest-kit';
2
+ import { randomString } from 'functools-kit';
3
+
4
+ var NodeType;
5
+ (function (NodeType) {
6
+ NodeType["SourceNode"] = "source_node";
7
+ NodeType["OutputNode"] = "output_node";
8
+ })(NodeType || (NodeType = {}));
9
+ var NodeType$1 = NodeType;
10
+
11
+ const sourceNode = (fetch) => ({
12
+ type: NodeType$1.SourceNode,
13
+ fetch,
14
+ });
15
+ const outputNode = (compute, ...nodes) => ({
16
+ type: NodeType$1.OutputNode,
17
+ nodes,
18
+ compute,
19
+ });
20
+
21
+ /**
22
+ * Рекурсивно разворачивает граф узлов в плоский массив.
23
+ * Порядок: сначала зависимости (children), затем родитель — топологический порядок.
24
+ * Дубликаты (один узел может быть зависимостью нескольких) исключаются по ссылке.
25
+ */
26
+ const deepFlat = (arr = []) => {
27
+ const result = [];
28
+ const seen = new Set();
29
+ const process = (entries = []) => entries.forEach((entry) => {
30
+ if (seen.has(entry)) {
31
+ return;
32
+ }
33
+ seen.add(entry);
34
+ process(entry.nodes ?? []);
35
+ result.push(entry);
36
+ });
37
+ process(arr);
38
+ return result;
39
+ };
40
+
41
+ /**
42
+ * Рекурсивно вычисляет значение узла графа.
43
+ * Для SourceNode вызывает fetch().
44
+ * Для OutputNode сначала резолвит все дочерние nodes параллельно,
45
+ * затем передаёт их типизированные значения в compute().
46
+ */
47
+ const resolve = async (node) => {
48
+ if (!ExecutionContextService.hasContext()) {
49
+ throw new Error("Execution context is required to resolve graph nodes. Please ensure that resolve() is called within a valid execution context.");
50
+ }
51
+ if (!MethodContextService.hasContext()) {
52
+ throw new Error("Method context is required to resolve graph nodes. Please ensure that resolve() is called within a valid method context.");
53
+ }
54
+ if (node.type === NodeType$1.SourceNode) {
55
+ const { symbol, when } = lib.executionContextService.context;
56
+ const { exchangeName } = lib.methodContextService.context;
57
+ return node.fetch(symbol, when, exchangeName);
58
+ }
59
+ const values = await Promise.all(node.nodes.map(resolve));
60
+ return node.compute(values);
61
+ };
62
+
63
+ /**
64
+ * Преобразует древовидный граф в плоский массив IFlatNode для хранения в БД.
65
+ * Каждому узлу присваивается уникальный id (если не задан),
66
+ * объектные ссылки nodes заменяются на массив nodeIds.
67
+ */
68
+ const serialize = (roots) => {
69
+ const flat = deepFlat(roots);
70
+ // Первый проход: назначаем id каждому уникальному узлу
71
+ const idMap = new Map();
72
+ flat.forEach((node) => {
73
+ const id = randomString();
74
+ idMap.set(node, id);
75
+ });
76
+ // Второй проход: строим IFlatNode с nodeIds вместо nodes
77
+ return flat.map((node) => {
78
+ const flatNode = {
79
+ id: idMap.get(node),
80
+ type: node.type,
81
+ description: node.description,
82
+ fetch: node.fetch,
83
+ compute: node.compute,
84
+ nodeIds: node.nodes?.map((child) => idMap.get(child)),
85
+ };
86
+ return flatNode;
87
+ });
88
+ };
89
+ /**
90
+ * Восстанавливает древовидный граф из плоского массива IFlatNode.
91
+ * nodes каждого узла заполняется по nodeIds.
92
+ * Возвращает корневые узлы (те, на которые никто не ссылается).
93
+ */
94
+ const deserialize = (flat) => {
95
+ // Первый проход: создаём INode-объекты, индексируем по id
96
+ const byId = new Map();
97
+ flat.forEach((flatNode) => {
98
+ const node = {
99
+ type: flatNode.type,
100
+ description: flatNode.description,
101
+ fetch: flatNode.fetch,
102
+ compute: flatNode.compute,
103
+ };
104
+ byId.set(flatNode.id, node);
105
+ });
106
+ // Второй проход: проставляем nodes[] по nodeIds
107
+ flat.forEach((flatNode) => {
108
+ if (flatNode.nodeIds?.length) {
109
+ const node = byId.get(flatNode.id);
110
+ node.nodes = flatNode.nodeIds
111
+ .map((id) => byId.get(id))
112
+ .filter((n) => n !== undefined);
113
+ }
114
+ });
115
+ // Корневые узлы — те, на которые не ссылается никто другой
116
+ const referenced = new Set(flat.flatMap((n) => n.nodeIds ?? []));
117
+ return [...byId.entries()]
118
+ .filter(([id]) => !referenced.has(id))
119
+ .map(([, node]) => node);
120
+ };
121
+
122
+ export { deepFlat, deserialize, outputNode, resolve, serialize, sourceNode };
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@backtest-kit/graph",
3
+ "version": "3.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/example_02_first_backtest.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
+ "worker-testbed": "1.0.12"
71
+ },
72
+ "peerDependencies": {
73
+ "backtest-kit": "^3.0.18",
74
+ "typescript": "^5.0.0"
75
+ },
76
+ "dependencies": {
77
+ "di-kit": "^1.0.18",
78
+ "di-scoped": "^1.0.21",
79
+ "functools-kit": "^1.0.95",
80
+ "get-moment-stamp": "^1.1.1"
81
+ },
82
+ "publishConfig": {
83
+ "access": "public"
84
+ }
85
+ }
package/types.d.ts ADDED
@@ -0,0 +1,147 @@
1
+ declare enum NodeType {
2
+ SourceNode = "source_node",
3
+ OutputNode = "output_node"
4
+ }
5
+
6
+ type ExchangeName = string;
7
+
8
+ /**
9
+ * Любое возможное вычисленное значение узла графа.
10
+ */
11
+ type Value = string | number | boolean | null;
12
+ /**
13
+ * Плоский базовый интерфейс узла графа.
14
+ * Следует тому же паттерну, что IField в react-declarative:
15
+ * все свойства опциональны, type — обязателен.
16
+ */
17
+ interface INode {
18
+ /**
19
+ * Тип узла для логического ветвления при исполнении графа
20
+ */
21
+ type: NodeType;
22
+ /**
23
+ * Человеко-читаемое описание узла, не влияет на исполнение графа.
24
+ */
25
+ description?: string;
26
+ /**
27
+ * Источник данных для SourceNode.
28
+ * Вызывается при вычислении узла без входящих зависимостей.
29
+ */
30
+ fetch?: (symbol: string, when: Date, exchangeName: ExchangeName) => Promise<Value> | Value;
31
+ /**
32
+ * Функция вычисления для OutputNode.
33
+ * Получает на вход массив значений, возвращённых fetch/compute
34
+ * из узлов массива nodes, в том же порядке.
35
+ */
36
+ compute?: (values: Value[]) => Promise<Value> | Value;
37
+ /**
38
+ * Входящие зависимости для OutputNode.
39
+ * Значения этих узлов передаются в compute.
40
+ */
41
+ nodes?: INode[];
42
+ }
43
+
44
+ /**
45
+ * Маппинг tuple нод в tuple их resolved-значений.
46
+ * Сохраняет позиционную структуру: [SourceNode<number>, SourceNode<string>] → [number, string].
47
+ */
48
+ type InferValues<TNodes extends TypedNode[]> = {
49
+ [K in keyof TNodes]: TNodes[K] extends TypedNode ? InferNodeValue<TNodes[K]> : never;
50
+ };
51
+ /**
52
+ * Извлекает тип возвращаемого значения из TypedNode.
53
+ * Используется InferValues и run() для типобезопасного резолвинга.
54
+ */
55
+ type InferNodeValue<T extends TypedNode> = T extends SourceNode<infer V> ? V : T extends OutputNode<any, infer V> ? V : never;
56
+ /**
57
+ * Узел-источник данных. Не имеет входящих зависимостей.
58
+ * T — тип значения, возвращаемого fetch().
59
+ */
60
+ type SourceNode<T extends Value = Value> = {
61
+ type: NodeType.SourceNode;
62
+ description?: string;
63
+ fetch: (symbol: string, when: Date, exchangeName: ExchangeName) => Promise<T> | T;
64
+ };
65
+ /**
66
+ * Узел вычисления. TNodes — tuple входящих зависимостей,
67
+ * TResult — тип возвращаемого значения compute().
68
+ * values в compute автоматически выводится из типов TNodes.
69
+ */
70
+ type OutputNode<TNodes extends TypedNode[] = TypedNode[], TResult extends Value = Value> = {
71
+ type: NodeType.OutputNode;
72
+ description?: string;
73
+ nodes: TNodes;
74
+ compute: (values: InferValues<TNodes>) => Promise<TResult> | TResult;
75
+ };
76
+ /**
77
+ * Типизированный узел графа для прикладного программиста.
78
+ * Подставляется вместо INode для строгой проверки типов и IntelliSense.
79
+ */
80
+ type TypedNode = SourceNode<Value> | OutputNode<TypedNode[], Value>;
81
+
82
+ declare const sourceNode: <T extends Value>(fetch: (symbol: string, when: Date, exchangeName: ExchangeName) => Promise<T> | T) => SourceNode<T>;
83
+ declare const outputNode: <TNodes extends TypedNode[], TResult extends Value = Value>(compute: (values: InferValues<TNodes>) => Promise<TResult> | TResult, ...nodes: TNodes) => OutputNode<TNodes, TResult>;
84
+
85
+ /**
86
+ * Рекурсивно разворачивает граф узлов в плоский массив.
87
+ * Порядок: сначала зависимости (children), затем родитель — топологический порядок.
88
+ * Дубликаты (один узел может быть зависимостью нескольких) исключаются по ссылке.
89
+ */
90
+ declare const deepFlat: (arr?: INode[]) => INode[];
91
+
92
+ /**
93
+ * Рекурсивно вычисляет значение узла графа.
94
+ * Для SourceNode вызывает fetch().
95
+ * Для OutputNode сначала резолвит все дочерние nodes параллельно,
96
+ * затем передаёт их типизированные значения в compute().
97
+ */
98
+ declare const resolve: <T extends TypedNode>(node: T) => Promise<InferNodeValue<T>>;
99
+
100
+ /**
101
+ * Сериализованная (плоская) форма узла графа для хранения в БД.
102
+ * Объектные ссылки nodes заменены на массив идентификаторов nodeIds.
103
+ */
104
+ interface IFlatNode {
105
+ /**
106
+ * Уникальный идентификатор узла.
107
+ */
108
+ id: string;
109
+ /**
110
+ * Тип узла.
111
+ */
112
+ type: NodeType;
113
+ /**
114
+ * Человеко-читаемое описание узла.
115
+ */
116
+ description?: string;
117
+ /**
118
+ * Идентификаторы входящих зависимостей.
119
+ * Порядок соответствует порядку значений в compute(values).
120
+ */
121
+ nodeIds?: string[];
122
+ /**
123
+ * Источник данных для SourceNode — не сериализуется в БД,
124
+ * восстанавливается на стороне приложения.
125
+ */
126
+ fetch?: (symbol: string, when: Date, exchangeName: ExchangeName) => Promise<Value> | Value;
127
+ /**
128
+ * Функция вычисления для OutputNode — не сериализуется в БД,
129
+ * восстанавливается на стороне приложения.
130
+ */
131
+ compute?: (values: Value[]) => Promise<Value> | Value;
132
+ }
133
+
134
+ /**
135
+ * Преобразует древовидный граф в плоский массив IFlatNode для хранения в БД.
136
+ * Каждому узлу присваивается уникальный id (если не задан),
137
+ * объектные ссылки nodes заменяются на массив nodeIds.
138
+ */
139
+ declare const serialize: (roots: INode[]) => IFlatNode[];
140
+ /**
141
+ * Восстанавливает древовидный граф из плоского массива IFlatNode.
142
+ * nodes каждого узла заполняется по nodeIds.
143
+ * Возвращает корневые узлы (те, на которые никто не ссылается).
144
+ */
145
+ declare const deserialize: (flat: IFlatNode[]) => INode[];
146
+
147
+ export { type IFlatNode, type INode, type TypedNode, type Value, deepFlat, deserialize, outputNode, resolve, serialize, sourceNode };