npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@backtest-kit/graph

v3.8.0

Published

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.

Downloads

1,423

Readme

📊 @backtest-kit/graph

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.

screenshot

Ask DeepWiki npm TypeScript

📚 Backtest Kit Docs | 🌟 GitHub

🔥 Multi-timeframe Pine Script strategy

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.

import { extract, run, toSignalDto, File } from '@backtest-kit/pinets';
import { addStrategySchema, Cache } from 'backtest-kit';
import { randomString } from 'functools-kit';
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';

// SourceNode — 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 },
    ),
);

// SourceNode — 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 },
    ),
);

// OutputNode — applies MTF filter, returns ISignalDto or null
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 toSignalDto(randomString(), lower, null);
    },
    higherTimeframe,
    lowerTimeframe,
);

addStrategySchema({
    strategyName: 'mtf_graph_strategy',
    interval: '5m',
    getSignal: (symbol) => resolve(mtfSignal),
    actions: ['partial_profit_action', 'breakeven_action'],
});

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.

🚀 Installation

npm install @backtest-kit/graph backtest-kit

✨ Features

  • 📊 DAG execution: Nodes are resolved bottom-up in topological order with Promise.all parallelism
  • 🔒 Type-safe values: TypeScript infers the return type of every node through the graph via generics
  • 🧱 Two APIs: Low-level INode for runtime/storage, high-level TypedNode + builders for authoring
  • 💾 DB-ready serialization: serialize / deserialize convert the graph to a flat IFlatNode[] list with id / nodeIds
  • 🔌 Context-aware fetch: SourceNode.fetch receives (symbol, when, exchangeName) from the execution context automatically

📖 Usage

Quick Start — builder API

Use sourceNode and outputNode to define a typed computation graph. TypeScript infers the type of values in compute from the nodes passed to outputNode:

import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';

// SourceNode<number> — fetch receives symbol, when, exchangeName from context
const closePrice = sourceNode(async (symbol, when, exchangeName) => {
    const candles = await getCandles(symbol, '1h', 1, exchangeName);
    return candles[0].close; // number
});

// SourceNode<number>
const volume = sourceNode(async (symbol, when, exchangeName) => {
    const candles = await getCandles(symbol, '1h', 1, exchangeName);
    return candles[0].volume; // number
});

// OutputNode<[SourceNode<number>, SourceNode<number>], number>
// price and vol are automatically number
const vwap = outputNode(
    ([price, vol]) => price * vol,
    closePrice,
    volume,
);

// Resolve inside a backtest-kit strategy
const result = await resolve(vwap); // Promise<number>

Inline anonymous composition

The entire graph can be defined as a single object literal.

import { NodeType } from '@backtest-kit/graph';
import { TypedNode, resolve } from '@backtest-kit/graph';

const signal: TypedNode = {
    type: NodeType.OutputNode,
    nodes: [
        {
            type: NodeType.SourceNode,
            fetch: async (symbol, when, exchangeName) => {
                const plots = await run(File.fromPath('timeframe_4h.pine'), { symbol, timeframe: '4h', limit: 100 });
                return extract(plots, { allowLong: 'AllowLong', allowShort: 'AllowShort', noTrades: 'NoTrades' });
            },
        },
        {
            type: NodeType.SourceNode,
            fetch: async (symbol, when, exchangeName) => {
                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' });
            },
        },
    ],
    compute: ([higher, lower]) => {
        if (higher.noTrades || lower.position === 0) return null;
        if (higher.allowShort && lower.position === 1) return null;
        if (higher.allowLong && lower.position === -1) return null;
        return lower.position;
    },
};

const result = await resolve(signal);

Mixed types

TypeScript correctly infers heterogeneous types by position in nodes:

const price = sourceNode(async (symbol) => 42);        // SourceNode<number>
const name  = sourceNode(async (symbol) => 'BTCUSDT'); // SourceNode<string>
const flag  = sourceNode(async (symbol) => true);      // SourceNode<boolean>

const result = outputNode(
    ([p, n, f]) => `${n}: ${p} (active: ${f})`, // p: number, n: string, f: boolean
    price,
    name,
    flag,
);
// OutputNode<[SourceNode<number>, SourceNode<string>, SourceNode<boolean>], string>

Using inside a backtest-kit strategy

import { addStrategy } from 'backtest-kit';
import { sourceNode, outputNode, resolve } from '@backtest-kit/graph';

const rsi = sourceNode(async (symbol, when, exchangeName) => {
    // ... compute RSI
    return 55.2;
});

const signal = outputNode(
    ([rsiValue]) => rsiValue < 30 ? 1 : rsiValue > 70 ? -1 : 0,
    rsi,
);

addStrategy({
    strategyName: 'graph-rsi',
    interval: '1h',
    riskName: 'demo',
    getSignal: async (symbol) => {
        const direction = await resolve(signal); // 1 | -1 | 0
        return direction === 1
            ? { position: 'long', ... }
            : null;
    },
});

Low-level INode

For manual graph construction without builders (e.g. after deserialization or in a DI container):

import { INode, Value } from '@backtest-kit/graph';
import NodeType from '@backtest-kit/graph/enum/NodeType';

const priceNode: INode = {
    type: NodeType.SourceNode,
    description: 'Close price',
    fetch: async (symbol, when, exchangeName) => 42,
};

const outputNode: INode = {
    type: NodeType.OutputNode,
    description: 'Doubled price',
    nodes: [priceNode],
    compute: ([price]) => (price as number) * 2,
};

INode has no generic parameters — values in compute is typed as Value[]. Use TypedNode and builders for full IntelliSense.

DB serialization

serialize flattens the graph into an IFlatNode[] array, replacing object references in nodes with nodeIds. deserialize reconstructs the tree:

import { serialize, deserialize, IFlatNode } from '@backtest-kit/graph';

// Graph → flat array for DB
const flat: IFlatNode[] = serialize([vwap]);
// [
//   { id: 'abc', type: 'source_node', nodeIds: [] },            // closePrice
//   { id: 'def', type: 'source_node', nodeIds: [] },            // volume
//   { id: 'ghi', type: 'output_node', nodeIds: ['abc', 'def'] }, // vwap
// ]

// Save to DB
await db.collection('nodes').insertMany(flat);

// Load from DB and reconstruct the graph
const stored: IFlatNode[] = await db.collection('nodes').find().toArray();
const roots: INode[] = deserialize(stored); // nodes[] is wired up from nodeIds

fetch and compute are not stored in the DB — they must be restored on the application side after deserialize.

deepFlat — traversal utility

deepFlat returns all nodes in topological order (dependencies before parents), deduplicated by reference:

import { deepFlat } from '@backtest-kit/graph';

const all = deepFlat([vwap]);
// [closePrice, volume, vwap] — dependencies first

all.forEach(node => console.log(node.description));

📋 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 | | Value | string \| number \| boolean \| null |

🤝 Contribute

Fork/PR on GitHub.

📜 License

MIT © tripolskypetr