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 strategy layer is the final consumer of the pipeline. Rather than evaluating raw Telegram messages directly, jan_2026.strategy.ts reads screen-items rows that have already been enriched with LLM risk assessments produced by the RiskOutline, and opens positions only when the LLM has voted follow and the live price is sitting within the signal’s prescribed entry zone. This separation of concerns keeps the decision logic simple: empirical rules about market conditions live in the Ollama prompt; the strategy code only acts on the verdict.

Strategy Decision Logic

The strategy is registered via addStrategySchema. The getSignal function runs on every 1-minute candle for every tracked symbol. It first fetches the most recent LLM-assessed signal from screen-items, enforces the riskAction verdict, validates the current price against the entry zone, and then returns the position parameters or null.
addStrategySchema({
  strategyName: "jan_2026_strategy",
  getSignal: async (symbol, when, currentPrice) => {
    const signal = await core.signalMainService.getLast4HourSignal(symbol, when);

    if (!signal) {
      return null;
    }

    // LLM verdict: skip this signal
    if (signal.riskAction === "skip") {
      return null;
    }

    // Price must be within the signal's entry zone
    const closePrice = await getClosePrice(symbol, "1m");
    if (closePrice < signal.entryFrom || closePrice > signal.entryTo) {
      return null;
    }

    return {
      id: signal.id,
      position: signal.direction,
      priceStopLoss: signal.stoploss,
      priceTakeProfit: signal.targets[2],  // Third target (TP3)
      minuteEstimatedTime: Infinity,
      note: JSON.stringify(info, null, 2),
    };
  },
});

Position Parameters

When getSignal returns a non-null value, backtest-kit uses the following fields to open and manage the position:
FieldSourceDescription
positionsignal.direction"long" or "short" — the trade direction as parsed from the Telegram signal
priceStopLosssignal.stoplossThe stop-loss level extracted from the signal text and stored in screen-items
priceTakeProfitsignal.targets[2]The third take-profit target (TP3) from the signal’s target array — deliberately the most ambitious exit
minuteEstimatedTimeInfinityNo time-based exit; the position remains open until either TP3 or the stop-loss is hit
noteJSON.stringify(info, null, 2)Full JSON payload containing signal metadata and LLM risk fields — visible in the backtest-kit UI
Using targets[2] (TP3) as the take-profit means the strategy holds for the full projected move. Combined with minuteEstimatedTime: Infinity, there is no forced time-exit — the trade either succeeds or fails on its own merits.

Signal Lookup

const signal = await core.signalMainService.getLast4HourSignal(symbol, when);
getLast4HourSignal(symbol, when) delegates to ScreenDbService.findLast4HourRow, which queries the screen-items MongoDB collection for the most recent document matching the given symbol whose publishedAt falls within the four-hour window [when − 4h, when], sorted by publishedAt descending.
public findLast4HourRow = async (symbol: string, when: Date): Promise<IScreenRow | null> => {
  const from = new Date(when.getTime() - 4 * 60 * 60 * 1000);
  return await this.findByFilter(
    { symbol, publishedAt: { $gte: from, $lte: when } },
    { publishedAt: -1 },
  ) as unknown as IScreenRow | null;
};
The four-hour window prevents stale signals from triggering new entries. If the channel posted a signal more than four hours ago and no newer one exists, getSignal returns null and no position is opened.

Entry Zone Check

const closePrice = await getClosePrice(symbol, "1m");
if (closePrice < signal.entryFrom || closePrice > signal.entryTo) {
  return null;
}
Every signal from the Telegram channel specifies an entry zone — a price range within which the trade is considered valid (e.g., “Open LONG in zone 135.00135.00 – 137.50”). The strategy fetches the current 1-minute close price and rejects entry if it has moved outside [entryFrom, entryTo]. Entering outside the zone would invalidate the signal’s risk-to-reward assumptions. If the price has already broken through TP1 before entry, the remaining upside to TP3 is compressed while the stop-loss distance stays the same, worsening the R:R ratio. The zone check enforces that the trade is only taken when the original setup is still intact.

Active Position Monitoring

For every active position, listenActivePing fires on each candle tick and logs four key metrics:
listenActivePing(async ({ symbol, data, currentPrice }) => {
  const peakProfitDistance = await getPositionHighestProfitDistancePnlCost(symbol);
  const peakMaxDrawdown = await getPositionHighestMaxDrawdownPnlCost(symbol);
  const currentPnl = await getPositionPnlCost(symbol);
  Log.info("position active", {
    symbol,
    signalId: data.id,
    priceOpen: data.priceOpen,
    takeProfit: data.priceTakeProfit,
    stopLoss: data.priceStopLoss,
    currentPrice,
    peakProfitDistance,
    peakMaxDrawdown,
    currentPnl,
  });
});
The four runtime metrics tracked per active position are:
  • peakProfitDistance — the highest favourable PnL cost reached since position open; shows how far in profit the trade went at its best.
  • peakMaxDrawdown — the largest adverse PnL cost recorded since open; measures how deep the trade went against you before recovering (or hitting the stop).
  • currentPnl — the unrealised PnL cost at the current candle close.
  • currentPrice — the raw market price at tick time, logged alongside open price, TP, and SL for full context.
These values are written to the backtest-kit log system and are accessible through the UI during both backtest replay and live monitoring.

Note Payload

When a position is opened, the full info object is JSON-stringified into the note field. This payload is stored in the position record and is visible in the backtest-kit UI’s trade detail view:
const info = {
  publishedAt: signal.publishedAt,
  data: {
    direction: signal.direction,
    entryFrom: signal.entryFrom,
    entryTo: signal.entryTo,
    targets: signal.targets,
    stoploss: signal.stoploss,
  },
  risk: {
    action: signal.riskAction,
    sureLevel: signal.riskSureLevel,
    confidence: signal.riskConfidence,
    description: signal.riskDescription,
    reasoning: signal.riskReasoning,
  },
  parsed: signal.note,
};
The risk block captures all five LLM-generated fields from the RiskOutline:
FieldTypeDescription
action"skip" | "follow"The LLM’s binary verdict — only "follow" reaches getSignal return
sureLevel"low" | "low_medium" | "medium" | "medium_high" | "high"Confidence in the signal setup quality
confidence"reliable" | "not_reliable"Whether the LLM considers its own assessment trustworthy
descriptionstringShort human-readable summary of the LLM’s assessment
reasoningstringFull chain-of-thought reasoning from the LLM
The parsed field contains the original signal.note — typically the raw text extracted from the Telegram message by the parser — providing a complete audit trail from source message to open position.
See the Backtest guide for January 2026 outcome statistics and a full trade-by-trade comparison of results with and without the Ollama risk filter.

Build docs developers (and LLMs) love