DocsBest Practices

Debugging Strategies

When a backtest produces unexpected results — no trades, wrong signals, or poor metrics — use these patterns to diagnose the issue.

No Trades Generated

The most common problem. Your strategy runs but places zero orders.

Check 1: Indicators returning NaN

If your warm-up is too short, indicators may be NaN for the entire run:

function onBar(ctx, i) { // Add temporary logging if (i < 5) { console.log('Bar', i, 'EMA:', ctx.ind.ema[i], 'RSI:', ctx.ind.rsi[i]); } }

Fix: Increase warmupBars to at least max(all indicator periods) + 1.

Check 2: Conditions never met simultaneously

Multiple filters may be individually reasonable but never true at the same time:

// These might never all be true at once if (rsi < 30 && close > ema200 && volume > avgVol * 3 && atr < threshold) { // Too many conditions }

Fix: Test each condition independently. Remove filters one at a time to see which one blocks all entries.

Check 3: Position check prevents re-entry

// If you close and re-enter on the same bar, the position // might not be 0 when you expect it to be if (pos.qty === 0 && buySignal) { ctx.order.market('ASSET', 1, { signal: 'buy' }); }

Fix: Remember that orders fill on the next bar's open. Position state reflects fills, not pending orders.

Unexpected Entry/Exit Timing

Order Fill Timing

All market orders fill at the next bar's open, not the current bar's close:

Bar 100: Signal detected (close = $50,000)
Bar 101: Order fills (open = $50,150)  <-- actual entry price

This is by design — it prevents look-ahead bias. If you see entries at "wrong" prices, check that you're comparing against the correct bar.

Crossover Fires Once

q.crossOver() returns true only on the bar where the crossing happens, not while one series remains above the other:

// This fires once (correct) if (q.crossOver(fastEma, slowEma, i)) { ... } // This fires every bar while fast > slow (different behavior) if (fastEma[i] > slowEma[i]) { ... }

Excessive Trading

If your strategy generates too many trades with high turnover:

Choppy Signal Filter

Add a minimum bars-between-trades cooldown:

function onBar(ctx, i) { const pos = ctx.position('ASSET'); // Simple cooldown: don't re-enter within 5 bars of an exit if (pos.qty === 0 && pos.lastExitBar && i - pos.lastExitBar < 5) { return; } }

Trend Filter

Only trade in the direction of the larger trend:

function init(ctx) { ctx.addIndicator('fastEma', 'EMA', { period: 10 }); ctx.addIndicator('slowEma', 'EMA', { period: 50 }); ctx.addIndicator('trendEma', 'EMA', { period: 200 }); } function onBar(ctx, i) { const inUptrend = ctx.series.close[i] > ctx.ind.trendEma[i]; // Only take long entries in an uptrend if (inUptrend && q.crossOver(ctx.ind.fastEma, ctx.ind.slowEma, i)) { ctx.order.market('ASSET', 1, { signal: 'buy' }); } }

Poor Performance Metrics

High Drawdown

  • Position sizing too aggressive — reduce quantity from 1 to 0.5 or less
  • No stop loss — add ATR-based or percentage-based stops
  • Trading against the trend — add a trend filter

Low Win Rate

  • Entry signals may be noisy — add confirmation filters
  • Stops too tight — widen stop distance (e.g. 2x ATR instead of 1x)
  • Wrong timeframe — some strategies work better on higher timeframes

High Fee Drag

  • Check total fees in the performance metrics
  • Default fee is 0.1% per side (0.2% round trip)
  • Tight stop-losses in choppy markets generate many small losing trades where fees dominate

Strategy Checklist

Before running a backtest, verify:

  • All indicators declared in init() with correct parameters
  • warmupBars >= max indicator period + 1
  • q.isNaN() guards on indicator values
  • Position check (pos.qty === 0) before entries
  • Both entry AND exit logic defined
  • Signal names are descriptive
  • Quantity is reasonable (0.1 to 1.0)

Related

debuggingtroubleshootingerrorsno tradesNaNcommon issues