Documentation Index
Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-docs/llms.txt
Use this file to discover all available pages before exploring further.
@backtest-kit/graph brings directed acyclic graph (DAG) execution to Backtest Kit strategies. Instead of manually chaining async indicator calls, you define each computation as a node and let the graph resolve dependencies in topological order — independent source nodes run in parallel via Promise.all, and output nodes receive their upstream values as typed arrays. Adding a new timeframe or indicator filter requires no changes to the existing wiring.
Install
npm install @backtest-kit/graph backtest-kit
Core Concepts
A graph consists of two node types:
- Source nodes fetch market data. They receive
(symbol, when, currentPrice, exchangeName) from the active execution context automatically — no manual plumbing required.
- Output nodes combine the results of one or more upstream nodes. TypeScript infers the type of each upstream value by position in the
nodes array.
The graph is resolved with resolve(), which traverses nodes bottom-up, runs all independent nodes in parallel, and passes typed results downstream.
Usage Example
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
import { getCandles } from 'backtest-kit';
import { addStrategySchema } from 'backtest-kit';
// Source nodes fetch market data in parallel
const candles1h = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
return getCandles(symbol, '1h', 50);
});
const candles15m = sourceNode(async (symbol, when, currentPrice, exchangeName) => {
return getCandles(symbol, '15m', 100);
});
// Output node combines sources — h1 and m15 are typed automatically
const signal = outputNode(
([h1, m15]) => {
// combine indicators from both timeframes
return computeSignal(h1, m15);
},
candles1h,
candles15m,
);
addStrategySchema({
strategyName: 'mtf-strategy',
interval: '5m',
getSignal: async (symbol) => resolve(signal),
});
Multi-Timeframe Pine Script Example
The graph is especially powerful when combining Pine Script indicators across timeframes. Both Pine Script nodes run in parallel:
import { extract, run, File } from '@backtest-kit/pinets';
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';
import { addStrategySchema, Cache } from 'backtest-kit';
// 4h trend filter — cached per candle interval
const higherTimeframe = sourceNode(
Cache.fn(
async (symbol) => {
const plots = await run(File.fromPath('timeframe_4h.pine'), {
symbol, timeframe: '4h', limit: 100,
});
return extract(plots, {
allowLong: 'AllowLong',
allowShort: 'AllowShort',
noTrades: 'NoTrades',
});
},
{ interval: '4h', key: ([symbol]) => symbol },
),
);
// 15m entry signal — cached per candle interval
const lowerTimeframe = sourceNode(
Cache.fn(
async (symbol) => {
const plots = await run(File.fromPath('timeframe_15m.pine'), {
symbol, timeframe: '15m', limit: 100,
});
return extract(plots, {
position: 'Signal',
priceOpen: 'Close',
priceTakeProfit: 'TakeProfit',
priceStopLoss: 'StopLoss',
minuteEstimatedTime: 'EstimatedTime',
});
},
{ interval: '15m', key: ([symbol]) => symbol },
),
);
// Output node applies the MTF filter
const mtfSignal = outputNode(
async ([higher, lower]) => {
if (higher.noTrades) return null;
if (lower.position === 0) return null;
if (higher.allowShort && lower.position === 1) return null;
if (higher.allowLong && lower.position === -1) return null;
return lower;
},
higherTimeframe,
lowerTimeframe,
);
addStrategySchema({
strategyName: 'mtf_graph_strategy',
interval: '5m',
getSignal: (symbol) => resolve(mtfSignal),
});
Type-Safe Node Values
TypeScript infers the return type of every node through the graph via generics. Mixed upstream types are handled correctly by position:
const price = sourceNode(async (symbol) => 42); // SourceNode<number>
const name = sourceNode(async (symbol) => 'BTCUSDT'); // SourceNode<string>
const active = sourceNode(async (symbol) => true); // SourceNode<boolean>
const result = outputNode(
([p, n, f]) => `${n}: ${p} (active: ${f})`, // p: number, n: string, f: boolean
price,
name,
active,
);
// OutputNode<[SourceNode<number>, SourceNode<string>, SourceNode<boolean>], string>
DB-Ready Serialization
serialize flattens the graph into an IFlatNode[] array, replacing object references with string IDs. deserialize reconstructs the tree from that array:
import { serialize, deserialize, IFlatNode } from '@backtest-kit/graph';
// Graph → flat array for DB storage
const flat: IFlatNode[] = serialize([mtfSignal]);
// Save to MongoDB
await db.collection('nodes').insertMany(flat);
// Load from DB and reconstruct the graph
const stored: IFlatNode[] = await db.collection('nodes').find().toArray();
const roots = deserialize(stored);
fetch and compute functions are not stored in the flat representation — they must be restored on the application side after deserialization.
API Reference
| Export | Description |
|---|
sourceNode(fetch) | Builder — creates a typed source node |
outputNode(compute, ...nodes) | Builder — creates a typed output node, infers values types from nodes |
resolve(node) | Recursively resolves a node graph within backtest-kit execution context |
serialize(roots) | Flattens a node tree into IFlatNode[] for DB storage |
deserialize(flat) | Reconstructs a node tree from IFlatNode[], returns root nodes |
deepFlat(nodes) | Utility — returns all nodes in topological order (dependencies first) |
INode | Base runtime interface (untyped, used internally and for serialization) |
TypedNode | Discriminated union for authoring with full IntelliSense |
IFlatNode | Serialized node shape for DB storage |
The graph is the natural composition layer for pipelines that mix Pine Script indicators with custom TypeScript logic. Define each Pine Script timeframe as a sourceNode, apply your filter logic in an outputNode, and add new timeframes without changing the downstream wiring. Combine with Cache.fn() from backtest-kit to avoid redundant Pine Script executions on the same candle interval.