Skip to content

Indication warmup validation missing #91

@tripolskypetr

Description

@tripolskypetr

Indicator warmup

I am trying to run this indicator with position entry signals

//@version=5
indicator("Signal Strategy 15m v1", overlay=true)

plotcandle(open, high, low, close, 
     color=close > open ? color.new(color.green, 70) : color.new(color.red, 70),
     wickcolor=color.new(color.gray, 70),
     bordercolor=color.new(color.gray, 70))


// === INPUTS ===
rsi_len = input.int(7, "RSI Length", minval=2)
ema_fast_len = input.int(5, "EMA Fast", minval=1)
ema_slow_len = input.int(13, "EMA Slow", minval=1)
ema_trend_len = input.int(50, "EMA Trend", minval=1)
atr_len = input.int(14, "ATR Length", minval=1)
vol_ma_len = input.int(20, "Volume MA Length", minval=1)

sl_mult = input.float(1.5, "SL ATR Multiplier", minval=0.5, step=0.1)
tp_mult = input.float(2.5, "TP ATR Multiplier", minval=0.5, step=0.1)
signal_valid_bars = input.int(5, "Signal Valid Bars", minval=1)

// === INDICATORS ===
rsi = ta.rsi(close, rsi_len)
ema_fast = ta.ema(close, ema_fast_len)
ema_slow = ta.ema(close, ema_slow_len)
ema_trend = ta.ema(close, ema_trend_len)
atr = ta.atr(atr_len)

// Volume filter - выше среднего
vol_ma = ta.sma(volume, vol_ma_len)
vol_spike = volume > vol_ma * 2.0

// Momentum confirmation
mom = ta.mom(close, 3)
mom_up = mom > 0
mom_down = mom < 0

// === TREND FILTER ===
trend_up = close > ema_trend and ema_fast > ema_trend
trend_down = close < ema_trend and ema_fast < ema_trend

// === ENTRY CONDITIONS ===
// Long: пересечение + RSI не перекуплен + тренд вверх + объём + momentum
long_cond = ta.crossover(ema_fast, ema_slow) and rsi > 40 and rsi < 65 and trend_up and vol_spike and mom_up

// Short: пересечение + RSI не перепродан + тренд вниз + объём + momentum  
short_cond = ta.crossunder(ema_fast, ema_slow) and rsi < 60 and rsi > 35 and trend_down and vol_spike and mom_down

// === SIGNAL MANAGEMENT ===
var int bars_since_signal = 0
var int last_signal = 0
var float entry_price = na
var float signal_atr = na

if long_cond
    last_signal := 1
    bars_since_signal := 0
    entry_price := close
    signal_atr := atr
else if short_cond
    last_signal := -1
    bars_since_signal := 0
    entry_price := close
    signal_atr := atr
else
    bars_since_signal += 1

// Signal expires faster on 15m
active_signal = bars_since_signal <= signal_valid_bars ? last_signal : 0

// === DYNAMIC SL/TP based on ATR ===
// sl = last_signal == 1 ? entry_price - signal_atr * sl_mult : last_signal == -1 ? entry_price + signal_atr * sl_mult : na
// tp = last_signal == 1 ? entry_price + signal_atr * tp_mult : last_signal == -1 ? entry_price - signal_atr * tp_mult : na

// === STATIC SL/TP for watch strategy ==
sl = last_signal == -1 ? close * 1.02 : close * 0.98
tp = last_signal == -1 ? close * 0.97 : close * 1.03

// === VISUALIZATION ===
line_color = active_signal == 1 ? color.green : active_signal == -1 ? color.red : color.gray

plot(ema_fast, "EMA Fast", color=color.new(color.blue, 50), linewidth=1)
plot(ema_slow, "EMA Slow", color=color.new(color.orange, 50), linewidth=1)
plot(ema_trend, "EMA Trend", color=color.new(color.white, 70), linewidth=2)

plotshape(long_cond, "Long", shape.triangleup, location.belowbar, color.green, size=size.small)
plotshape(short_cond, "Short", shape.triangledown, location.abovebar, color.red, size=size.small)

plot(close, "Active Signal", color=line_color, linewidth=6)

// === OUTPUTS FOR BOT ===
plot(close, "Close", display=display.data_window)
plot(active_signal, "Signal", display=display.data_window)
plot(sl, "StopLoss", display=display.data_window)
plot(tp, "TakeProfit", display=display.data_window)
plot(1440, "EstimatedTime", display=display.data_window)  // 24 hour for 15m TF

All entry conditions (line 44, line 47) must be satisfied simultaneously:

long_cond = ta.crossover(ema_fast, ema_slow) and rsi > 40 and rsi < 65 and trend_up and vol_spike and mom_up

This means every indicator must return a valid (non-na) value at the same bar. With ema_trend_len = 50 (line 14), the trend filter conditions at (line 39–40) close > ema_trend and ema_fast > ema_trend will produce misleading results before the EMA has stabilized.

As a result: 50 bars minimum for reliable signal generation.

Current behaviour

When I run the strategy with 60 candles limit

import { addExchangeSchema } from "backtest-kit";
import { singleshot, randomString } from "functools-kit";
import { run, File, toMarkdown } from "@backtest-kit/pinets";
import ccxt from "ccxt";

const SIGNAL_SCHEMA = {
  position: "Signal",
  priceOpen: "Close",
  priceTakeProfit: "TakeProfit",
  priceStopLoss: "StopLoss",
  minuteEstimatedTime: "EstimatedTime",
};

const SIGNAL_ID = randomString();

const getExchange = singleshot(async () => {
  const exchange = new ccxt.binance({
    options: {
      defaultType: "spot",
      adjustForTimeDifference: true,
      recvWindow: 60000,
    },
    enableRateLimit: true,
  });
  await exchange.loadMarkets();
  return exchange;
});

addExchangeSchema({
  exchangeName: "ccxt-exchange",
  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,
    }));
  },
});

const plots = await run(
  File.fromPath("timeframe_15m.pine"),
  {
    symbol: "BTCUSDT",
    timeframe: "15m",
    limit: 60,
  },
  "ccxt-exchange",
  new Date("2025-09-23T16:00:00.000Z"),
);

console.log(await toMarkdown(SIGNAL_ID, plots, SIGNAL_SCHEMA));

It produce valid signals

| 1.0000 | 113053.6700 | 116445.2801 | 110792.5966 | 1440.0000 | 2025-09-23T14:00:00.000Z |
| 1.0000 | 112814.8700 | 116199.3161 | 110558.5726 | 1440.0000 | 2025-09-23T14:15:00.000Z |
| 1.0000 | 112814.2300 | 116198.6569 | 110557.9454 | 1440.0000 | 2025-09-23T14:30:00.000Z |
| 1.0000 | 112652.9100 | 116032.4973 | 110399.8518 | 1440.0000 | 2025-09-23T14:45:00.000Z |
| 1.0000 | 112949.2000 | 116337.6760 | 110690.2160 | 1440.0000 | 2025-09-23T15:00:00.000Z |

But if I change the limit to 40

const plots = await run(
  File.fromPath("timeframe_15m.pine"),
  {
    symbol: "BTCUSDT",
    timeframe: "15m",
    limit: 40,
//       ^^^^^^
  },

Signals missing without an exception to be thrown

| 0.0000 | 112952.3100 | 116340.8793 | 110693.2638 | 1440.0000 | 2025-09-23T13:45:00.000Z |
| 0.0000 | 113053.6700 | 116445.2801 | 110792.5966 | 1440.0000 | 2025-09-23T14:00:00.000Z |
| 0.0000 | 112814.8700 | 116199.3161 | 110558.5726 | 1440.0000 | 2025-09-23T14:15:00.000Z |
| 0.0000 | 112814.2300 | 116198.6569 | 110557.9454 | 1440.0000 | 2025-09-23T14:30:00.000Z |
| 0.0000 | 112652.9100 | 116032.4973 | 110399.8518 | 1440.0000 | 2025-09-23T14:45:00.000Z |
| 0.0000 | 112949.2000 | 116337.6760 | 110690.2160 | 1440.0000 | 2025-09-23T15:00:00.000Z |

Enviroment

package.json

{
  "name": "my-backtest-project",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "description": "Backtest Kit trading bot project",
  "scripts": {
    "start": "node ./index.mjs"
  },
  "dependencies": {
    "@backtest-kit/ollama": "^3.0.3",
    "@backtest-kit/pinets": "^3.0.7",
    "@backtest-kit/ui": "^3.0.5",
    "@huggingface/inference": "^4.7.1",
    "@langchain/core": "^0.3.57",
    "@langchain/xai": "^0.0.2",
    "agent-swarm-kit": "^1.2.3",
    "backtest-kit": "^3.0.9",
    "ccxt": "^4.4.41",
    "dotenv": "^16.4.7",
    "functools-kit": "^1.0.95",
    "ollama": "^0.6.0",
    "openai": "^4.97.0",
    "pinolog": "^1.0.5",
    "pinets": "^0.8.6",
    "uuid": "^11.0.3"
  },
  "devDependencies": {
    "dotenv-cli": "^7.4.2"
  },
  "keywords": [
    "backtest",
    "trading",
    "crypto",
    "bot"
  ]
}

I think pinets should skip the candles until all indicators warmup or throw an exception if there are less then required. At least console.warn

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions