No backtest results yet. Click "Backtest" on a strategy or use "Backtest New Strategy".
| Time | Strategy | Symbol | Action | Price | Qty | Sentiment | Result |
|---|
ta.ema/sma/rsi/macd/bollinger/atr/structure/squeeze/manipulation/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 |
|---|
A background worker backtests the bundled default scripts, plus any script a user has submitted, against every symbol/timeframe with stored bar data — DB-only, no live API calls (see strategy_ranker.py). Each combo is measured over a few calendar windows sized to its granularity (5m/15m: 1 week/1 month/3 months; 30m-4h: 1 month/3 months/6 months; 6h+: 3 months/6 months/1 year), so results are comparable across timeframes without one giant backtest dominating the cost. Submit one of your own scripts for ranking from the Script Editor. "Global" shows defaults plus public submissions from anyone; "Mine" shows only your own submitted scripts (public or private), no defaults.
Note: ranking backtests always run with sentiment ignored (there's no correct lookback window to score
it by once bars are windowed by calendar length — see strategy_ranker.py). Any script gated on
ctx.sentiment_score (e.g. the bundled sentiment_gate) will never fire here and
always shows 0 fills — that's expected, not a sign the script or the worker is broken.
| Script | Owner | Symbol | Granularity | Window | Win Rate | Expectancy (R) | Annualized Return | Return % (window) | Realized P&L | Fills | Updated |
|---|
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(ctx.bars, 9) twice in the same bar is free.
Important: almost every ta.* indicator takes bars — a
list[Bar], i.e. ctx.bars or ctx.window(n) — not
ctx.close_prices(). ctx.close_prices()/ctx.volumes() return
a plain list[float] for your own math or for ta.crossover/
stats.* (which do take plain lists) — passing one to ta.ema/
ta.rsi/etc. instead of ctx.bars raises
AttributeError: 'float' object has no attribute 'close'.
Everything run(ctx) gets about the current bar, symbol, and history.
Closes up to and including the current bar, oldest first — all of them by default, or just
the trailing n. A plain list, not a Series — for your own math or
ta.crossover/stats.*. Pass ctx.bars, not this, to
ta.ema/ta.rsi/etc.
Same shape as close_prices(), for volume.
The last n bars (oldest first). Use when you need OHLC, not just close — and
what to pass most ta.* functions instead of a bare price list.
The current (most recently closed) bar.
Resamples this context's bars up to a higher timeframe (e.g. "4h",
"1d"). Not a Context — a small object with just
.bars (list[Bar]) and .label; pass
htf.bars to ta.* functions, there's no
htf.close_prices(). 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 | float (Unix epoch seconds, not a datetime) |
A read-only snapshot of an open position.
| Field | Type | Description |
|---|---|---|
.side | "long" | "short" | direction of the position |
.qty | float | position size |
.entry_price | float | average entry price |
.current_price | float | latest close — what .pnl is computed against |
.pnl | float | unrealized P&L: (current_price − entry_price) × qty for a long, reversed for a short |
.direction | str | alias for .side |
.stop read-only | float | None | current engine-managed stop, if the entry signal set one |
.target read-only | float | None | current engine-managed profit target, if the entry signal set one |
.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") resamples this context's bars up to that granularity and
returns a Timeframe (.bars, .label — not a full
Context). Use it to check a higher-timeframe trend without a second data source.
Exponential moving average. bars is list[Bar] — pass
ctx.bars or ctx.window(n).
Simple moving average.
Relative strength index, 0-100.
Trend-following momentum: the gap between a fast and slow EMA, plus a smoothed trigger line.
| Field | Type | Description |
|---|---|---|
.macd | Series[float] | MACD line — fast EMA minus slow EMA |
.signal | Series[float] | EMA of the MACD line — the trigger line |
.histogram | Series[float] | .macd minus .signal |
A moving average with volatility bands num_std standard deviations above/below it.
| Field | Type | Description |
|---|---|---|
.middle | Series[float] | The SMA basis the bands are built around |
.upper | Series[float] | middle + num_std × rolling std |
.lower | Series[float] | middle − num_std × rolling std |
.width | Series[float] | upper − lower, in price units |
.percent_b | Series[float] | (close − lower) / (upper − lower) — 0 = at the lower band, 1 = at the upper band, 0.5 when the bands are flat |
.bandwidth | Series[float] | width / middle — width normalized by price, comparable across time/instruments |
Average true range, Wilder-smoothed. The ATR value itself IS the Series (use
.current/.previous like any other indicator) — these are extra
fields on top.
| Field | Type | Description |
|---|---|---|
.rising | bool | .current > .previous |
.expanding | bool | current reading above its own trailing average |
.contracting | bool | current reading below its own trailing average |
.percentile | float | None | 0-100 — where the current reading ranks against its own trailing history |
Swing-based market structure: recent confirmed swing highs/lows, and whether price is making higher highs/higher lows (uptrend) or the reverse.
| Field | Type | Description |
|---|---|---|
.trend | str | "uptrend" / "downtrend" / "range" |
.higher_high | bool | latest confirmed swing high > the one before it |
.lower_high | bool | latest confirmed swing high < the one before it |
.higher_low | bool | latest confirmed swing low > the one before it |
.lower_low | bool | latest confirmed swing low < the one before it |
.swing_high | float | None | most recent confirmed swing high |
.swing_low | float | None | most recent confirmed swing low |
.breakout | bool | current bar's close > .swing_high |
.breakdown | bool | current bar's close < .swing_low |
.inside_bar | bool | current bar's range is fully inside the prior bar's |
.outside_bar | bool | current bar's range fully engulfs the prior bar's |
Volatility squeeze detector (Bollinger Bands inside Keltner Channels). Fires when volatility compresses then releases — the classic setup before an expansion move.
| Field | Type | Description |
|---|---|---|
.active | bool | squeeze is on right now (BB inside KC) |
.score | float | 0-100, higher = tighter compression |
.direction | str | "up" / "down" / "neutral" — slope of recent closes |
.release | bool | squeeze was active last bar, inactive this bar — the breakout signal |
.compression | float | None | BB width ÷ KC width; < 1 means squeezed |
.duration | int | consecutive bars the squeeze has been active |
Wyckoff spring/upthrust detector: a bar that pierces a recent swing low (or high) —
sweeping the stops resting just beyond it — and closes back inside the prior range on
the same bar. Reuses ta.structure() for the swing levels, so
.swing_high/.swing_low always reflect a pivot confirmed
strictly before the current bar.
| Field | Type | Description |
|---|---|---|
.spring | bool | bullish: this bar pierced below the recent swing low, then closed back above it |
.upthrust | bool | bearish: this bar pierced above the recent swing high, then closed back below it |
.direction | str | "bullish" / "bearish" / "neutral" |
.strength | float | 0-100 — the classic Accumulation/Distribution money-flow multiplier: how convincingly this bar closed back into range (0 = barely, 100 = closed at the extreme opposite the sweep) |
.volume_ratio | float | None | this bar's volume ÷ its own trailing average; None if not enough bars. Exposed rather than hard-coded into a threshold, since which side of "high volume" actually confirms a spring is genuinely debated even within Wyckoff method |
.swing_high | float | None | the swing high this reading was measured against |
.swing_low | float | None | the swing low this reading was measured against |
Classifies the current market as choppy vs. trending and volatility as high/low, using ATR percentile and Bollinger bandwidth percentile. Use it to gate a strategy so it only trades the conditions it was designed for.
| Field | Type | Description |
|---|---|---|
.choppy | bool | low volatility AND a tight recent range |
.trending | bool | elevated volatility AND a wide recent range |
.volatility | str | "low" / "normal" / "high" |
.high_volatility | bool | .volatility == "high" |
.low_volatility | bool | .volatility == "low" |
.atr_percentile | float | None | 0-100 — ATR's rank against its own trailing history |
.bandwidth_percentile | float | None | 0-100 — Bollinger bandwidth's rank against its own trailing history |
Equivalent to a.crossed_above(b) — a standalone function form.
Equivalent to a.crossed_below(b).
True on either direction of cross.
Highest close over the trailing period bars — a single number, not a
Series.
Lowest close over the trailing period bars.
Session Volume-Weighted Average Price: cumulative (typical price × volume) / cumulative
volume, resetting each UTC calendar day. Only covers the current session's bars
— len(result) is bars-in-today's-session, not len(bars), unlike
every other indicator here. Use .current/.previous, never positional
indexing against 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 list[float]
alongside the indicator library. Use them when you need a number the ta module
doesn't already compute for you as part of an indicator.
stats.mean(values) | float |
stats.std(values) | float (population std, not sample) |
stats.percentile(values, pct) | float — pct is 0-100 |
stats.zscore(values) | float — how many std-devs the last value is from the mean |
A weighted confluence accumulator — build up evidence from multiple named conditions, then
convert to a Signal once a threshold is met. Useful when no single indicator alone
should trigger a trade.
max_score is only the denominator .percent/.normalize()
divide by — it's not enforced, your components can add up to more or less than it.
name is a label string (for .breakdown()/.best_factor),
points is a number — not a condition + weight. Gate the points
yourself: score.add("Squeeze released", 30 if sq.release else 0).
.total | sum of every .add()'s points |
.percent | total / max_score * 100 |
.best_factor | name of the highest-point component, or None |
.normalize(target=100.0) | total / max_score * target |
.breakdown() | {name: points} dict |
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 the trade is r × its initial risk in profit — units of
the stop distance .stop_atr() set, not raw ATR directly (only the same number
when your stop multiple is exactly 1). Requires .stop_atr() to also be set —
that's what defines "initial risk"; without it this is a no-op.
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.
Applied to every strategy running against the in-house paper broker (see paper_broker.py) — no API keys needed for these. Changes only affect fills from now on, not trades already made.