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:
- 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.
context.indexmismatch: If a custom strategy usedinput.history[input.context.index](absolute indexing), thecontext.indexwas 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
lookbackCandlescandles),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— Addedsandbox?: 'quickjs'toNativeStrategyRuntimeservices/backend/src/lib/custom-strategy-runtime.ts— Setsandbox: 'quickjs'on compiled custom strategiesservices/backend/src/lib/backtest-engine/runner.ts— UpdatedbuildStrategyExecutionInputto use per-type history/index logic
Deployment
Pushed to both origin/main (production) and origin/develop (dev).