Skip to main content

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

ExportDescription
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)
INodeBase runtime interface (untyped, used internally and for serialization)
TypedNodeDiscriminated union for authoring with full IntelliSense
IFlatNodeSerialized 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.

Build docs developers (and LLMs) love