Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-ollama-crontab/llms.txt

Use this file to discover all available pages before exploring further.

The backtest harness replays a full month of Telegram signals against Binance historical OHLCV data, applying the complete LLM pipeline — crawler, parser, RiskOutline verdict, and getSignal entry logic — to every candle in the frame. Under the hood, backtest-kit drives the simulation: it advances time minute by minute, calls getSignal for each tracked symbol, and simulates position open and close against real Binance price history fetched via ccxt. This means the backtest exercises the exact same code paths as live mode; only the time source and data transport differ.

Frame Configuration

The backtest frame is registered in content/jan_2026.strategy/modules/backtest.module.ts using addFrameSchema:
addFrameSchema({
  frameName: "jan_2026_frame",
  interval: "1m",
  startDate: new Date("2026-01-01T00:00:00Z"),
  endDate: new Date("2026-01-31T23:59:59Z"),
  note: "January 2026",
});
FieldValueDescription
frameName"jan_2026_frame"Identifier used to reference this frame in backtest-kit
interval"1m"Candle granularity — the simulation ticks forward one minute at a time
startDate2026-01-01T00:00:00ZInclusive start of the replay window
endDate2026-01-31T23:59:59ZInclusive end of the replay window — covers all of January 2026
note"January 2026"Human-readable label shown in the backtest-kit UI

Exchange Schema

The exchange is registered with addExchangeSchema using ccxt.binance configured for spot markets. The same module also sets two global backtest parameters via setConfig:
setConfig({
  CC_MAX_STOPLOSS_DISTANCE_PERCENT: 100,
  CC_BREAKEVEN_THRESHOLD: 0,
});

const getExchange = singleshot(async () => {
  const exchange = new ccxt.binance({
    options: {
      defaultType: "spot",
      adjustForTimeDifference: true,
      recvWindow: 60000,
    },
    enableRateLimit: true,
  });
  await exchange.loadMarkets();
  return exchange;
});
The exchange instance is created once (singleshot) and reused for every candle fetch. Key points:
  • defaultType: "spot" — all symbols are traded on the Binance spot market.
  • enableRateLimit: true — ccxt’s built-in rate limiter is active; the backtest will pause between requests automatically to avoid hitting Binance API limits.
  • CC_MAX_STOPLOSS_DISTANCE_PERCENT: 100 — allows stop-losses up to 100% away from entry, effectively disabling the maximum stop-loss distance guard.
  • CC_BREAKEVEN_THRESHOLD: 0 — breakeven move-to-cost is disabled; the stop-loss is never automatically adjusted to entry.

Candle Fetching

getCandles: async (symbol, interval, since, limit) => {
  const exchange = await getExchange();
  const candles = await exchange.fetchOHLCV(
    symbol,
    interval,
    since.getTime(),
    limit,
  );
  return candles.map(([timestamp, open, high, low, close, volume]) => ({
    timestamp, open, high, low, close, volume,
  }));
},
getCandles calls fetchOHLCV on the Binance public API. since is passed as a Unix millisecond timestamp. The returned array is mapped from the ccxt tuple format to the named-field shape expected by backtest-kit.

Price and Quantity Formatting

formatPrice: async (symbol, price) => {
  const exchange = await getExchange();
  const market = exchange.market(symbol);
  const tickSize = market.limits?.price?.min || market.precision?.price;
  if (tickSize !== undefined) {
    return roundTicks(price, tickSize);
  }
  return exchange.priceToPrecision(symbol, price);
},
Both formatPrice and formatQuantity read tick size and step size directly from the Binance market info (loaded by loadMarkets()), then apply roundTicks — a backtest-kit utility that rounds a number to the nearest tick — to produce exchange-compliant values. If market info is unavailable, they fall back to ccxt’s built-in priceToPrecision / amountToPrecision.

Running the Backtest

From the repository root, launch the backtest with the backtest-kit UI enabled:
npm start -- --backtest --ui --entry ./content/jan_2026.strategy/jan_2026.strategy.ts
FlagDescription
--backtestRuns in historical simulation mode; time advances through the configured frame instead of the system clock
--uiLaunches the backtest-kit web interface so you can inspect trade charts, equity curves, and per-trade details in real time
--entryPath to the strategy file — backtest-kit loads this module at startup, executing all addStrategySchema, addExchangeSchema, addFrameSchema, and Cron.register calls

What Happens During a Backtest

1

Strategy module loaded

The CLI loads jan_2026.strategy.ts and its imported modules (backtest.module.ts). All schema registration calls — addStrategySchema, addExchangeSchema, addFrameSchema — execute synchronously, wiring up the exchange, frame window, and getSignal handler.
2

One-shot cron fires

The "backtest-prepare-data" cron handler fires immediately (no interval means one-shot). It calls core.crawlerMainService.crawlBacktestFrame(when), which fetches all Telegram messages from the configured channel for the January 1–31 window and upserts them into the parser-items MongoDB collection.
3

Signal job processes parser items

signalJobSubject.next() fires, triggering SignalJobService.run(). For every unvisited parser-items row, SignalLogicService invokes the RiskOutline: the LLM receives the parsed signal fields plus 1m/15m candle metrics and returns a riskAction: "skip" | "follow" verdict. Results are written to screen-items.
4

getSignal called per symbol per candle

backtest-kit advances the simulation clock minute by minute through the frame. At each tick, getSignal(symbol, when, currentPrice) is called for every symbol in CC_SYMBOL_LIST. It queries screen-items for the most recent LLM-enriched signal in the last four hours; if riskAction === "follow" and price is inside the entry zone, a position is opened.
5

Position lifecycle simulated

backtest-kit simulates execution against the Binance OHLCV data: the position opens when the candle’s price range first intersects the entry zone, and closes when price hits targets[2] (TP3) or signal.stoploss (SL). With minuteEstimatedTime: Infinity, there is no time-based forced exit.

Dump Directory

During the RiskOutline step, each LLM response is saved to disk by the onValidDocument callback. Outline results land in ./dump/outline/risk/, one JSON file per processed signal. Each file contains the full LLM response — riskAction, riskSureLevel, riskConfidence, riskDescription, and riskReasoning — alongside the input metrics packet that was sent to the model. These files are useful for auditing which signals the LLM vetoed and why, without querying the database.

January 2026 Results

Running this backtest with the Ollama LLM risk filter active produced the following outcome over 17 trades in January 2026, compared to 22 trades without the filter:
MetricWithout OllamaWith OllamaΔ
Total trades2217−5 trades skipped
Total PNL+52.22%+68.90%+16.68 pp
Winrate68%82%+14 pp
Wins / Losses15 / 714 / 3−4 losing trades
Sharpe Ratio+0.309+0.512+0.203
Profit factor2.736.37+3.64
Expectancy per trade+$2.37+$4.05+$1.68
The LLM vetoed 6 signals in total, of which 4 were losing trades (TRX SHORT Jan 06 −4.24%, NEAR LONG Jan 07 −4.26%, TRX SHORT Jan 12 −4.45%, SOL LONG Jan 28 −4.34%), avoiding a combined −17.29% in losses.
The backtest requires a live internet connection. It fetches Binance historical OHLCV candles via the public ccxt API and pulls Telegram messages from the configured channel via MTProto — both happen at runtime, not from a local cache. Ensure session.txt is in place and the CC_OLLAMA_URL endpoint is reachable before starting.

Build docs developers (and LLMs) love