No backtest results yet. Click "Backtest" on a strategy or use "Backtest New Strategy".
| Time | Strategy | Symbol | Action | Price | Sentiment | Result |
|---|
ta.ema/sma/rsi/macd/bollinger/atr/structure/squeeze/regime
ta.crossover/crossunder ta.highest/lowest ·
ctx.close_prices() ctx.window(n) ctx.timeframe(tf)
ctx.state ctx.position ·
Signal.buy()/.sell()/.short()/.cover()
.stop_atr() .target_atr() .trail_atr()
.break_even() .timeout() .with_confidence() ·
strategies.<name>(ctx) to compose other saved scripts —
full DSL reference →
Every backtest is cached by its configuration (symbol, script, sizing, etc.) and resumed forward automatically next time you run it. Click a row to reload its full results and charts on the Dashboard tab; delete a row to force a clean rerun from scratch next time.
| Updated | Symbol | Script | Granularity | Fills | Win Rate | Realized P&L |
|---|
Everything a script can use. Every script is a Python module with a single run(ctx)
entrypoint — this page documents ctx, the indicator library, and the types you build
signals out of.
A script is a Python module. It must define a single function, run(ctx), called
once per bar close. Return a Signal to act, a Score to combine with
other signals, or None to do nothing this bar. ta, stats,
Signal, Score, Bar, and strategies are
already in scope — no imports needed.
ctx.state is a plain dict that persists across calls for this script — use it for
anything you need to remember between bars. Indicator calls inside a single run()
are cached per-arguments, so calling ta.ema(closes, 9) twice in the same bar is free.
Everything run(ctx) gets about the current bar, symbol, and history.
All closes up to and including the current bar, oldest first.
All volumes up to and including the current bar, oldest first.
The last n bars (oldest first). Use when you need OHLC, not just close.
The current (most recently closed) bar.
A resampled view of this same context on a higher timeframe (e.g. "4h",
"1d"). See Multi-timeframe.
Persists across calls to run() for this script. Empty dict on first call.
The symbol currently being evaluated, e.g. "AAPL".
The current open position for this symbol under this script, or None if flat.
A single OHLCV candle.
open | float |
high | float |
low | float |
close | float |
volume | float |
timestamp | datetime |
A read-only snapshot of an open position.
side | "long" | "short" |
qty | float |
entry_price | float |
stop read-only | float | None |
target read-only | float | None |
.stop / .target reflect whatever the entry signal's trade-management
chain set — they're set by the engine, not by your script, once a position is open.
ctx.timeframe("4h") returns a Context resampled to that granularity,
built from the same underlying bars. Use it to check a higher-timeframe trend without a second
data source.
Exponential moving average.
Simple moving average.
Relative strength index, 0-100.
Returns a MACD object with three Series: .macd,
.signal, .histogram.
Returns a BollingerBands object: .middle, .upper,
.lower (all Series), plus .width and
.percent_b / .bandwidth.
Average true range. Adds .rising, .expanding,
.contracting, and .percentile (0-1, where this reading ranks against
recent history) on top of normal Series behavior.
Swing-based market structure: recent swing highs/lows and whether price is making higher highs / higher lows (uptrend structure) or the reverse.
Volatility squeeze detector (Bollinger Bands inside Keltner Channels). Fires when volatility compresses then releases — the classic setup before an expansion move.
Classifies the current market as choppy vs. trending and volatility as high/low, using ATR
percentile and Bollinger bandwidth percentile. Fields: .choppy,
.trending, .volatility, .high_volatility,
.low_volatility, .atr_percentile, .bandwidth_percentile.
Use it to gate a strategy so it only trades the conditions it was designed for.
Equivalent to a.crossed_above(b) — a standalone function form.
Equivalent to a.crossed_below(b).
True on either direction of cross.
Rolling maximum over period bars.
Rolling minimum over period bars.
What every indicator returns. A list of values (oldest first) plus lookback and comparison helpers, so you rarely need to index manually.
.current | last value |
.previous | second-to-last value |
.slope | current − previous |
.acceleration | change in slope |
.crossed_above(other) | bool |
.crossed_below(other) | bool |
.all() / .any() | over the whole series |
Series support elementwise arithmetic (series_a - series_b, series * 2).
Gotcha: a bare Series is deliberately restrictive in a boolean
context (if my_series: raises rather than silently checking "is non-empty" the way
a plain list would) — always compare a specific value, e.g. if series.current > 0,
not if series.
Lightweight statistics helpers for working with raw Series/lists
alongside the indicator library (mean, stdev, percentile-style helpers). Use it when you need a
number the ta module doesn't already compute for you as part of an indicator.
A weighted confluence accumulator — build up evidence from multiple conditions, then convert to
a Signal once a threshold is met. Useful when no single indicator alone should
trigger a trade.
What run(ctx) returns to act on this bar.
Open or close a long position.
Open or close a short position. Backtesting only — live trading currently
only handles buy/sell actions, so a script using
short()/cover() will backtest correctly but silently no-op those
actions when run live. Keep live-bound scripts long-only until that's implemented.
Chain these off a Signal to give the engine (backtest or live) native exit handling,
instead of managing stops/targets yourself in run() on later bars.
Stop-loss at multiple × ATR from entry. Also becomes the risk basis for
expectancy calculations.
Take-profit at multiple × ATR from entry.
Trailing stop, updated as price moves favorably.
Move stop to entry once price has moved trigger_atr × ATR in your favor.
Force-close the position after bars bars if neither stop nor target has hit.
Set by your script (0-1) — how strongly the current setup matches the pattern the strategy is looking for. Subjective, computed from whatever evidence the script has on hand this bar.
Historical win rate for this exact script + symbol, from its own backtest history. Not set by
your script — auto-attached when a signal comes from strategies.<name>(ctx).
Average P&L per trade in R-multiples (units of initial risk), from backtest history. This
is usually the better ranking signal than .probability: a 45% win rate with 3.5R
winners beats an 80% win rate with tiny gains. Risk basis comes from .stop_atr() if
the strategy set one, otherwise the engine's configured stop-loss %.
Number of closed trades behind .probability / .expectancy. Treat
small sample sizes as noise — expectancy_ranked.py (below) requires at least 15
trades before trusting a comparison.
Timestamp of the backtest run these numbers came from.
These four are None until a sub-strategy has actually been backtested for that exact
symbol — "never measured" and "measured and it's bad" are different things, so check for
None before comparing, not just falsiness.
Call one of your other saved scripts as a sub-strategy and combine results. Each call is
memoized per run, gets its own isolated ctx.state and indicator cache, and returns
whatever that script's run(ctx) returned — a Signal, or
None if it didn't fire this bar — with .probability /
.expectancy / .sample_size / .tested_at auto-attached from
that script's own backtest history.
Cycles are detected and raise an error rather than infinite-looping — a script can't (directly or transitively) call itself. You can only compose your own saved scripts, not another user's.
Broker API keys are stored per-user in the database. Fields below are always blank on load — that's not an empty account, it's so this page never shows (or accidentally re-saves) your real secret. Leave a field blank and hit Save to keep its current value; type a new value to replace it; use Clear to remove it.