Skip to main content

Fix: Custom Strategy Windowed History & Relative context.index

Date: 05-05-2026
Commit: 92b2dc2

Problem​

Custom strategies (user-authored JavaScript running inside a QuickJS WASM sandbox) were failing backtests with "Cannot read properties of undefined (reading 'close')".

The previous fix (4c573ec) set historyStart = 0 for ALL native strategies, giving them the full accumulated candle history. This was correct for built-in strategies (which use candles[context.index] with an absolute index) but had two problems for custom QuickJS strategies:

  1. Performance: At tick 5,000 the input JSON becomes ~500 KB+. At tick 50,000+ it can exceed the 8 MB QuickJS memory limit, causing OOM/timeout errors mid-backtest.
  2. context.index mismatch: If a custom strategy used input.history[input.context.index] (absolute indexing), the context.index was the absolute position in the full series. After historyStart=0 this worked — but the root contract was still fragile.

Root Cause​

Built-in strategies (Strategy helper in strategies/helpers.ts) use calculateSignal(candles, context.index) where context.index is the absolute index. They require history.length >= context.index + 1.

Custom strategies can be authored either way:

  • Relative: history[history.length - 1] (the current candle)
  • Absolute: history[context.index] (same intent, different code)

The windowed history (before fix) broke the absolute pattern. The full history (after first fix) broke performance for QuickJS.

Solution​

Added sandbox?: 'quickjs' to NativeStrategyRuntime in types.ts.

compileCustomStrategy now sets runtime: { type: 'native', language: 'typescript', sandbox: 'quickjs' }.

In buildStrategyExecutionInput:

const isQuickJs = input.strategy.runtime.type === 'native'
&& (input.strategy.runtime as { sandbox?: string }).sandbox === 'quickjs';

const historyStart = (!isQuickJs && input.strategy.runtime.type === 'native')
? 0 // built-in: full history, absolute context.index
: Math.max(0, input.index - lookbackCandles + 1); // QuickJS/remote/wasm: windowed

// context.index is relative to the window start for QuickJS strategies
const contextIndex = isQuickJs ? input.index - historyStart : input.index;

Effect:

  • Built-in native strategies: full history (historyStart=0), absolute context.index. Unchanged.
  • Custom QuickJS strategies: windowed history (max lookbackCandles candles), context.index = last index in window. history[context.index] always resolves to the current candle.
  • Remote/WASM strategies: windowed history (unchanged behavior).

Files Changed​

  • services/backend/src/lib/backtest-engine/types.ts — Added sandbox?: 'quickjs' to NativeStrategyRuntime
  • services/backend/src/lib/custom-strategy-runtime.ts — Set sandbox: 'quickjs' on compiled custom strategies
  • services/backend/src/lib/backtest-engine/runner.ts — Updated buildStrategyExecutionInput to use per-type history/index logic

Deployment​

Pushed to both origin/main (production) and origin/develop (dev).