-
Notifications
You must be signed in to change notification settings - Fork 52
Description
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