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 has first-class support for LLM-powered trading strategies. Every provider integration, structured output schema, and context injection pattern is designed to slot directly into a getSignal function — meaning your LLM runs in the same backtest/live duality as any other strategy, with automatic look-ahead bias protection, the same VWAP exits, and the same crash-safe persistence. The result is a reproducible audit trail: you can replay a backtest and watch the exact conversation that produced every signal.

Installation

npm install @backtest-kit/ollama agent-swarm-kit backtest-kit
agent-swarm-kit provides the addOutline, json, and commitPrompt primitives used for structured output enforcement. @backtest-kit/ollama wraps 11 LLM providers into a unified HOF API.

Supported Providers

OpenAI

GPT-5, GPT-4o via gpt5()

Anthropic

Claude 3.5/3.7 via claude()

DeepSeek

DeepSeek-V3 via deepseek()

Grok (xAI)

Grok-3 via grok()

Mistral

Mistral Large via mistral()

Perplexity

Sonar Pro via perplexity()

Cohere

Command R+ via cohere()

Alibaba (Qwen)

Qwen-Max via alibaba()

Hugging Face

Any HF model via hf()

Ollama (local)

Any local model via ollama()

Zhipu AI

GLM-4 via glm4()
All providers share the same HOF signature: provider(fn, model, apiKey?) => fn. This means switching providers is a one-line change.

Basic Usage: LLM Signal in a Strategy

The minimal pattern is to call getCandles for multiple timeframes, format the data into messages, call the LLM, and return the structured signal.
import { v4 as uuid } from 'uuid';
import { addStrategySchema, getCandles, dumpAgentAnswer, dumpRecord } from 'backtest-kit';

addStrategySchema({
  strategyName: 'llm-strategy',
  interval: '5m',
  riskName: 'demo',
  getSignal: async (symbol) => {
    const candles1h  = await getCandles(symbol, '1h',  24);
    const candles15m = await getCandles(symbol, '15m', 48);
    const candles5m  = await getCandles(symbol, '5m',  60);
    const candles1m  = await getCandles(symbol, '1m',  60);

    // Build messages array with multi-timeframe context
    const messages = await getMessages(symbol, {
      candles1h, candles15m, candles5m, candles1m,
    });

    const resultId = uuid();
    const signal   = await json(messages);  // LLM generates structured signal

    // Persist the full conversation for debugging and audit
    await dumpAgentAnswer({
      dumpId:      'position-context',
      bucketName:  'multi-timeframe-strategy',
      messages,
      description: 'agent reasoning for this signal',
    });

    await dumpRecord({
      dumpId:      'position-entry',
      bucketName:  'multi-timeframe-strategy',
      record:      signal,
      description: 'signal entry parameters',
    });

    return { ...signal, id: resultId };
  },
});
getCandles automatically respects the current backtest or live temporal context via Node.js AsyncLocalStorage — there is no risk of look-ahead bias regardless of which timeframe you request.

Structured Output Enforcement

@backtest-kit/ollama uses agent-swarm-kit’s outline system to enforce a typed JSON schema on every LLM response. Malformed responses are automatically retried.

Defining a Signal Schema with Zod

import { z } from 'zod';
import { str } from 'functools-kit';

export const SignalSchema = z.object({
  position: z.enum(['long', 'short', 'wait']).describe(
    str.newline(
      'Position direction:',
      'long: bullish signals, uptrend potential',
      'short: bearish signals, downtrend potential',
      'wait: conflicting signals or unfavorable conditions',
    )
  ),
  price_open: z.number().describe('Entry price in USD'),
  price_stop_loss: z.number().describe(
    str.newline('Stop-loss price', 'LONG: below price_open', 'SHORT: above price_open')
  ),
  price_take_profit: z.number().describe(
    str.newline('Take-profit price', 'LONG: above price_open', 'SHORT: below price_open')
  ),
  minute_estimated_time: z.number().describe('Estimated minutes to reach TP'),
  risk_note: z.string().describe('Risk assessment with specific numbers'),
});

Registering the Outline

import { addOutline } from 'agent-swarm-kit';
import { zodResponseFormat } from 'openai/helpers/zod';
import { SignalSchema, TSignalSchema } from '../schema/Signal.schema';
import { CompletionName } from '@backtest-kit/ollama';

addOutline<TSignalSchema>({
  outlineName: 'SignalOutline',
  completion:  CompletionName.RunnerOutlineCompletion,
  format:      zodResponseFormat(SignalSchema, 'position_decision'),
  getOutlineHistory: async ({ history, param: messages = [] }) => {
    await history.push(messages);
  },
  validations: [
    {
      validate: ({ data }) => {
        if (data.position === 'long' && data.price_stop_loss >= data.price_open) {
          throw new Error('For LONG, stop_loss must be below price_open');
        }
        if (data.position === 'short' && data.price_stop_loss <= data.price_open) {
          throw new Error('For SHORT, stop_loss must be above price_open');
        }
      },
    },
  ],
});
Validations run after parsing. If a validation throws, the LLM is called again with the error message appended as a correction prompt.

Wrapping a Function with a Provider

The HOF pattern wraps your signal function with a provider context. Swapping providers is a single token change:
import { deepseek, gpt5, claude, ollama } from '@backtest-kit/ollama';

// DeepSeek (cloud)
const getSignal = deepseek(mySignalFn, 'deepseek-chat', process.env.DEEPSEEK_API_KEY);

// GPT-5 (OpenAI)
const getSignal = gpt5(mySignalFn, 'gpt-4o', process.env.OPENAI_API_KEY);

// Claude
const getSignal = claude(mySignalFn, 'claude-3-5-sonnet-20241022', process.env.ANTHROPIC_API_KEY);

// Local Ollama (no API key required)
const getSignal = ollama(mySignalFn, 'deepseek-r1:7b');

Token Rotation

For Ollama and other providers that support multiple API keys, pass an array to enable automatic rotation. When the active key hits a rate limit, the wrapper rotates to the next key in the array:
const getSignal = ollama(mySignalFn, 'llama3.3:70b', [
  process.env.OLLAMA_KEY_1,
  process.env.OLLAMA_KEY_2,
  process.env.OLLAMA_KEY_3,
]);

Fallback Chains

Chain providers so that if the primary fails, execution falls through to the next:
import { deepseek, claude, ollama } from '@backtest-kit/ollama';

// Try DeepSeek first, then Claude, then local Ollama
const getSignal = deepseek(
  claude(
    ollama(mySignalFn, 'deepseek-r1:7b'),
    'claude-3-5-sonnet-20241022',
    process.env.ANTHROPIC_API_KEY,
  ),
  'deepseek-chat',
  process.env.DEEPSEEK_API_KEY,
);

Memory Adapters for Persistent LLM Context

The Memory persistence adapter stores conversation history across ticks. This is useful for strategies that want the LLM to remember previous reasoning or market summaries.
import { getMemoryData, setMemoryData } from 'backtest-kit';

// Load previous context
const history = await getMemoryData('market-context', signal.id) ?? [];

// Append new messages and pass to LLM
const messages = [...history, ...newMessages];
const result   = await json(messages);

// Save updated context for the next tick
await setMemoryData('market-context', signal.id, messages.slice(-20));
The Memory adapter supports soft delete — history is logically removed without physical deletion, preserving the audit trail.

Session Caching for Indicator Calculations

The Session adapter caches expensive indicator calculations between ticks. This prevents recomputing the same 50-indicator bundle on every 1-minute tick when your strategy only runs every 5 minutes.
import { getSessionData, setSessionData } from 'backtest-kit';

// Check cache first
const cached = await getSessionData('indicators', symbol);
if (cached) return cached;

// Compute indicators (expensive)
const indicators = await computeMultiTimeframeIndicators(symbol);

// Cache for the duration of the current session
await setSessionData('indicators', symbol, indicators);
return indicators;

Debugging LLM Decisions

dumpAgentAnswer and dumpRecord write structured debug files alongside the report, one directory per signal UUID:
./dump/strategy/{uuid}/
├── 00_system_prompt.md
├── 01_user_message.md        ← 1h candle analysis
├── 02_assistant_message.md
├── 03_user_message.md        ← 15m candle analysis
├── 04_assistant_message.md
├── 05_user_message.md        ← 5m candle analysis
├── 06_assistant_message.md
├── 07_user_message.md        ← 1m candle analysis
├── 08_assistant_message.md
├── 09_user_message.md        ← signal generation request
└── 10_llm_output.md          ← final JSON signal
This gives you a complete audit trail for every trade decision, which is invaluable for prompt engineering and debugging unexpected signal directions.

Pre-Computed Indicator Markdown

Use @backtest-kit/signals to automatically compute 50+ technical indicators across four timeframes (1m, 15m, 30m, 1h) and inject the results as formatted Markdown into your LLM message context. This eliminates manual indicator calculation in getSignal and produces context that LLMs parse more reliably than raw OHLCV numbers.
npm install @backtest-kit/signals backtest-kit
The package handles RSI, MACD, Bollinger Bands, Stochastic, ADX, ATR, CCI, Fibonacci levels, support/resistance, and order book analysis — all with intelligent per-timeframe TTL caching.

Periodic Data Fetching with Cron

LLM strategies often need to fetch external data — news, funding rates, on-chain signals — on a schedule rather than on every tick. Backtest Kit’s built-in Cron scheduler runs in virtual time, so the same Cron.register call works in both backtest and live modes.
import { Cron, Backtest } from 'backtest-kit';

// Global hourly fetch — fires once per virtual hour across all parallel backtests.
// Use this to fetch news, macro data, or Telegram signals into MongoDB before
// the strategy reads them on the next tick.
Cron.register({
  name: 'fetch-news',
  interval: '1h',
  handler: async ({ symbol, when, backtest }) => {
    await fetchNewsToMongo(when);
  },
});

// Fire-once warm-up — runs exactly once on the first tick, then never again
// (until Cron.clear() or re-registration). Use to pre-warm LLM context or
// pull the full signal history for a backtest frame.
Cron.register({
  name: 'warm-cache',
  // No interval → fire-once
  handler: async ({ symbol, when, backtest }) => {
    await warmupLLMContext();
  },
});

// Wire Cron to the engine once at startup — must be called before Backtest.background().
Cron.enable();

for (const symbol of ['BTCUSDT', 'ETHUSDT', 'SOLUSDT']) {
  Backtest.background(symbol, { strategyName, exchangeName, frameName });
}
A global job (symbols omitted) fires once per virtual boundary across all parallel backtests — no double-fires on expensive fetches. A fan-out job (symbols: ['BTC', 'ETH']) fires once per boundary per whitelisted symbol. See the Multi-Symbol Parallel guide for coordination details.

Provider Reference

ProviderFunctionModel Examples
OpenAIgpt5()gpt-4o, o1-mini
Anthropicclaude()claude-3-5-sonnet-20241022
DeepSeekdeepseek()deepseek-chat, deepseek-reasoner
Grok (xAI)grok()grok-3
Mistralmistral()mistral-large-latest
Perplexityperplexity()llama-3.1-sonar-large-128k-online
Coherecohere()command-r-plus
Alibabaalibaba()qwen-max
Hugging Facehf()Any hosted model ID
Ollamaollama()llama3.3:70b, deepseek-r1:7b
Zhipu AIglm4()glm-4-plus

Build docs developers (and LLMs) love