Skip to main content

Documentation Index

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

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

The backtest mode replays every Telegram signal published inside a configured date range, passes each one through the local Ollama LLM risk filter, and simulates position entries and exits against real historical OHLCV candles. Because the LLM is called with market context anchored at the signal’s original publication time, there is no look-ahead bias — the verdict is identical to what a live run would have produced on that date.

Prerequisites

Before running a backtest, make sure all of the following are in place.

Packages built

Run npm run build:x (macOS/Linux) or npm run build:win (Windows) from the repo root. Each workspace package emits build/index.cjs and a rolled-up types.d.ts that the strategy file imports at runtime.

Telegram session authenticated

Run cd ./packages/main && npm run auth, scan the QR code with the Telegram app, then copy the resulting session.txt into your strategy folder:
cp packages/main/session.txt content/jan_2026.strategy/session.txt

MongoDB + Redis running

The crawler writes raw messages to the parser-items MongoDB collection and the LLM job service reads from it. Redis is used for caching and distributed locking. Both must be reachable at their default ports before npm start is invoked.

Ollama running with gpt-oss

The risk outline calls the local Ollama HTTP API at CC_OLLAMA_URL (default http://localhost:11434). Pull the model before your first run:
ollama pull gpt-oss

Backtest Command

npm start -- --backtest --ui --entry ./content/jan_2026.strategy/jan_2026.strategy.ts --cache
FlagTypeDescription
--backtestbooleanActivates backtest mode. packages/main/src/main/backtest.ts checks this flag and exits early if it is absent.
--entryboolean (presence flag)Tells @backtest-kit/cli to load the file path that follows it as the strategy entry point. The file is never bundled — it is loaded at runtime by the CLI.
--uibooleanOpens the backtest-kit web UI so you can inspect the trade log, equity curve, and per-trade details in a browser.
--cachebooleanPre-caches OHLCV candles for every symbol in CC_SYMBOL_LIST before the simulation starts. Uses the exchange schema’s getCandles implementation and the frame schema’s startDate / endDate to determine the download window.
The --entry value must be the path to a strategy file that calls addStrategySchema(...), registers Cron handlers, and (optionally) imports an exchange + frame schema module.

What Happens Step by Step

1

backtest.ts bootstraps and waits for readiness

@backtest-kit/cli resolves packages/main/src/main/backtest.ts and calls main(). The function first checks that both --entry and --backtest flags are present, then calls:
await waitForReady(true);
Passing true puts the framework into backtest mode. The function waits for MongoDB, Redis, and the Ollama connection to all report healthy before returning.
2

Strategy, exchange, and frame schemas are read

After waitForReady resolves, the runner reads the schemas that were registered by the strategy entry file and the module it imports:
const [strategySchema] = await listStrategySchema();
const [exchangeSchema] = await listExchangeSchema();
const [frameSchema]    = await listFrameSchema();
For jan_2026.strategy.ts the frame schema is registered by content/jan_2026.strategy/modules/backtest.module.ts:
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",
});
If any schema is missing the runner throws immediately with a descriptive error.
3

Candles are pre-cached (if --cache is set)

When --cache is present, cacheCandles() is called once for every symbol in CC_SYMBOL_LIST using the exchange schema’s getCandles implementation and the frame date range:
for (const symbol of CC_SYMBOL_LIST) {
  await cacheCandles({
    exchangeName: exchangeSchema.exchangeName,
    from: frameSchema.startDate,
    to:   frameSchema.endDate,
    interval: "1m",
    symbol,
  });
}
Candles are stored locally so that subsequent runs and the per-tick price feed never make live exchange requests during the simulation.
4

Backtest.background is called for each symbol

With schemas resolved and candles cached, the runner launches a simulation worker for each symbol:
for (const symbol of CC_SYMBOL_LIST) {
  Backtest.background(symbol, {
    exchangeName: exchangeSchema.exchangeName,
    strategyName: strategySchema.strategyName,
    frameName:    frameSchema.frameName,
  });
}
Each worker replays 1-minute candles from startDate to endDate, calling getSignal(symbol, when, currentPrice) on every tick.
5

backtest-prepare-data cron fires on the first tick

The strategy file registers a one-shot Cron handler named "backtest-prepare-data":
Cron.register({
  name: "backtest-prepare-data",
  handler: async (symbol, when, backtest) => {
    if (!backtest) {
      return;
    }
    await core.crawlerMainService.crawlBacktestFrame(when);
  },
});
Because no interval is set, this handler fires once at the start of the simulation. crawlBacktestFrame(when) reads the frame’s startDate and endDate from the schema, calls crawlerService.crawlRange(fromStamp, toStamp) to pull all Telegram messages for the full month, and upserts them into the parser-items MongoDB collection.
6

signalJobSubject triggers LLM screening

After the crawl completes, crawlBacktestFrame calls:
await signalJobSubject.next();
This wakes SignalJobService, which iterates every unvisited parser-items row whose publishedAt falls within the frame date range. For each row it runs the LLM risk outline inside an execution context aligned to the signal’s publication minute:
const when = alignToInterval(row.publishedAt, "1m");
await ExecutionContextService.runInContext(
  async () => self.signalLogicService.execute(row),
  { symbol: row.symbol, when, backtest: true },
);
The outline fetches 1m/15m candles at that historical timestamp, computes avgRangePct and momentum24hPct, sends them to gpt-oss via Ollama, and stores the structured verdict (riskAction, riskSureLevel, riskConfidence, riskDescription, riskReasoning) in the screen-items collection. Rows are marked as visited after screening.
7

getSignal reads screen-items and returns positions

On every subsequent price tick, backtest-kit calls the strategy’s getSignal function. The function queries the most recent LLM-screened record from screen-items for that symbol using a 4-hour look-back window:
const signal = await core.signalMainService.getLast4HourSignal(symbol, when);
Signals with riskAction === "skip" are rejected immediately. For surviving signals the function additionally confirms that the current 1m close price lies within the channel’s published entry zone before returning a position object:
return {
  id:                  signal.id,
  position:            signal.direction,        // "long" | "short"
  priceStopLoss:       signal.stoploss,
  priceTakeProfit:     signal.targets[2],       // third target (T3)
  minuteEstimatedTime: Infinity,                // no time-based exit
  note:                JSON.stringify(info, null, 2),
};
The backtest replays at historical candle prices — Ollama is called with the 1m/15m candle context that existed at the signal’s original publication time. Computed metrics (avgRangePct, momentum24hPct) reflect conditions 24 hours before publication, so there is no forward-looking information in the LLM prompt.

Controlling Which Symbols Are Processed

The CC_SYMBOL_LIST environment variable controls every symbol processed by the crawler, the LLM screener, and the simulation workers. Set it in your .env file as a comma-separated list of exchange pair identifiers:
CC_SYMBOL_LIST=BTCUSDT,SOLUSDT,HYPEUSDT,NEARUSDT
The default value (defined in packages/main/src/config/params.ts) is:
BTCUSDT,POLUSDT,ZECUSDT,HYPEUSDT,DOGEUSDT,SOLUSDT,PENGUUSDT,TRXUSDT,HBARUSDT,NEARUSDT,FARTCOINUSDT,ETHUSDT,PUMPUSDT
Reducing the list speeds up both the candle-cache step and the LLM screening pass, which can be significant when running gpt-oss:120b locally.

January 2026 Results

The table below shows the improvement the LLM gate produced on the same parsed-signal set:
MetricWithout OllamaWith OllamaΔ
Total trades2217−5 skipped
Total PNL+52.22%+68.90%+16.68 pp
Winrate68%82%+14 pp
Sharpe Ratio+0.309+0.512+0.203
Profit factor2.736.37+3.64
Expectancy / trade+$2.37+$4.05+$1.68
The LLM correctly vetoed 6 signals, of which 4 were losers totalling −17.29% avoided. The two skipped winners cost −2.47%, for a net filtering gain of +14.82%.

Build docs developers (and LLMs) love