Historical backtest
You write the strategy. We run the backtest.
Paste your Python strategy in the dashboard. We execute it inside an isolated Docker container with no network egress, capped CPU/RAM, and a 2-minute wall clock. Results stream to your dashboard tagged mode=backtest — apples-to-apples with your live trading numbers.
Why this design
- You don't host the data. We license and serve 15 months of CME/CBOT/NYMEX/COMEX 1-min bars. You can't download or redistribute them — just point your strategy at them.
- You don't run the infrastructure. No Docker, no AWS, no Postgres on your side. Submit code, get results. We absorb the ops cost.
- Code submitted, not extracted. Your strategy runs in a hardened container (no network, read-only filesystem, capped 256 MB / 1 CPU, dropped capabilities). After the run, the strategy file is deleted. We keep only the resulting events, not your source.
- Same dashboard as live. Backtest results show up next to your live runs with a
backtestbadge. ElevateMind Score, Sharpe, Calmar, drawdown — same math, comparable numbers.
Strategy contract
Implement one function, on_bar(bar, state). The sandbox calls it once per 1-minute bar in chronological order. Return a tuple of (orders, new_state).
def on_bar(bar, state):
"""
bar: dict with keys ts, open, high, low, close, volume, sym
state: arbitrary dict — your strategy carries it across calls
Returns: (orders, new_state)
orders is a list of order intents:
{"type": "market_buy", "qty": 1, "reason": "breakout"}
{"type": "market_sell", "qty": 1, "reason": "fade"}
{"type": "exit", "reason": "target"|"stop"|"time"|"signal"}
"""
closes = state.setdefault("closes", [])
closes.append(bar["close"])
if len(closes) < 22:
return [], state
# 9 / 21 EMA crossover
def ema(prev, x, span):
a = 2 / (span + 1)
return x * a + prev * (1 - a) if prev is not None else x
state["ema9"] = ema(state.get("ema9"), bar["close"], 9)
state["ema21"] = ema(state.get("ema21"), bar["close"], 21)
prev9, prev21 = state.get("prev9"), state.get("prev21")
state["prev9"], state["prev21"] = state["ema9"], state["ema21"]
if prev9 is None or prev21 is None:
return [], state
orders, pos = [], state.get("position")
if pos is None and prev9 < prev21 and state["ema9"] > state["ema21"]:
orders.append(dict(type="market_buy", qty=1, reason="ema9x21_up"))
state["position"] = "long"
elif pos == "long" and prev9 > prev21 and state["ema9"] < state["ema21"]:
orders.append(dict(type="exit", reason="ema9x21_down"))
state["position"] = None
return orders, state
How a run flows
- 1 · Open the dashboard. app.elevatemindstudio.net → sign in → click Backtest in the topbar.
- 2 · Pick a contract. Dropdown shows every cached front-month parquet — ES, NQ, GC, CL, etc., with date ranges.
- 3 · Paste your strategy. The default loaded is a 9/21 EMA crossover. Replace with your own; the only contract is
on_bar(bar, state). - 4 · Run. We spin up
elevatemind/sandbox:py3.12with:--network none,--read-only,--memory 256m,--cpus 1,--pids-limit 64,--cap-drop ALL,--security-opt no-new-privileges,--user 1000. AST checks rejectsubprocess/socket/urllib/eval/exec/openbefore execution. - 5 · Watch results. Job status, trades, realized P&L, and event count update live in the modal. When done, switch the dashboard's Mode filter to backtest only — the full Universal Ranking (ElevateMind Score, Sharpe, Calmar, Kelly %) is computed from your events.
Safety boundary — what runs where
- Strategy file is bind-mounted into the container read-only at
/code/strategy.py. Bars at/data/bars.parquet, also read-only. - Container has no network. Cannot reach any external host. Cannot reach our own backend, DB, or any other tenant.
- Writable filesystem is a 64 MB tmpfs at
/tmp— wiped when the container exits. - Wall-clock deadline: 2 minutes in the sandbox + 150 s in the orchestrator. After that the container is killed.
- Memory cap: 256 MB. CPU cap: 1 core. Process count cap: 64.
- After the run, we delete the strategy file. We retain only the events the strategy emitted (signals, fills, exits) — never your source.
Current limits
- Code file ≤ 200 KB
- 9 symbols (ES, NQ, GC, CL, ZB, YM, RTY, 6E, NG); 1-minute bars; ~15 months cached
- 200,000 bars max per symbol (≈ 5 trading months at 1-min resolution)
- Free: 1 symbol × 1-month window per backtest, 1 backtest per day
- Pro: up to 3 symbols × 3-month window, 10 backtests per day
- Studio: up to 6 symbols × 12-month window, 50 backtests per day
- Whitelisted libraries inside the sandbox:
pandas,numpy,scipy,pyarrow. Standard library too — minussubprocess,socket,urllib,http,os.system.
Roadmap
- Walk-forward + Monte Carlo helpers
- Per-bot leaderboard — rank versions of your strategy by ElevateMind Score
- Stitched multi-quarter contracts (12-month windows from quarterly futures rolls)
- Tick-level data (currently 1-min bars)
- Strategy DSL / visual builder for non-Python traders
Data attribution: bars sourced from Massive (CME/CBOT/NYMEX/COMEX). Data is licensed for our service; users do not download or redistribute it.