Trading Dashboard

Sign in to manage strategies
Trading Dashboard
connecting...
Strategies
0
Running
0
Trades
0
Realized P&L
$0
Tiingo (hr)
Tiingo (mo)

New Strategy

Setup

Position

Backtest New Strategy

Setup

Position

Sentiment & Cache

Backtest Results

No backtest results yet. Click "Backtest" on a strategy or use "Backtest New Strategy".

Trade Log

TimeStrategySymbolAction PriceSentimentResult

Script Editor

Quick referenceta.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 →

Backtest with this script

Setup

Backtest History

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.

UpdatedSymbolScriptGranularity FillsWin RateRealized P&L

DSL Reference

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.

The script contract

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.

# minimal example: EMA crossover def run(ctx): closes = ctx.close_prices() fast = ta.ema(closes, 9) slow = ta.ema(closes, 21) if fast.crossed_above(slow): return Signal.buy().stop_atr(1.5).target_atr(3) if fast.crossed_below(slow): return Signal.sell() return None

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.

Context

Everything run(ctx) gets about the current bar, symbol, and history.

ctx.close_prices() → Series[float]

All closes up to and including the current bar, oldest first.

ctx.volumes() → Series[float]

All volumes up to and including the current bar, oldest first.

ctx.window(n) → list[Bar]

The last n bars (oldest first). Use when you need OHLC, not just close.

ctx.last() → Bar

The current (most recently closed) bar.

ctx.timeframe(tf) → Context

A resampled view of this same context on a higher timeframe (e.g. "4h", "1d"). See Multi-timeframe.

ctx.state → dict

Persists across calls to run() for this script. Empty dict on first call.

ctx.symbol → str

The symbol currently being evaluated, e.g. "AAPL".

ctx.position → Position | None

The current open position for this symbol under this script, or None if flat.

Bar

A single OHLCV candle.

openfloat
highfloat
lowfloat
closefloat
volumefloat
timestampdatetime

Position

A read-only snapshot of an open position.

side"long" | "short"
qtyfloat
entry_pricefloat
stop read-onlyfloat | None
target read-onlyfloat | 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.

Multi-timeframe

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.

def run(ctx): htf = ctx.timeframe("4h") htf_trend = ta.ema(htf.close_prices(), 21).slope > 0 fast = ta.ema(ctx.close_prices(), 9) slow = ta.ema(ctx.close_prices(), 21) if fast.crossed_above(slow) and htf_trend: return Signal.buy()

ta.ema / ta.sma

ta.ema(series, period) → Series[float]

Exponential moving average.

ta.sma(series, period) → Series[float]

Simple moving average.

fast = ta.ema(ctx.close_prices(), 9) if fast.crossed_above(slow): ...

ta.rsi

ta.rsi(series, period=14) → Series[float]

Relative strength index, 0-100.

rsi = ta.rsi(ctx.close_prices()) if rsi.current < 30: return Signal.buy()

ta.macd

ta.macd(series, fast=12, slow=26, signal=9) → MACD

Returns a MACD object with three Series: .macd, .signal, .histogram.

m = ta.macd(ctx.close_prices()) if m.macd.crossed_above(m.signal): return Signal.buy()

ta.bollinger

ta.bollinger(series, period=20, std=2.0) → BollingerBands

Returns a BollingerBands object: .middle, .upper, .lower (all Series), plus .width and .percent_b / .bandwidth.

bb = ta.bollinger(ctx.close_prices()) if ctx.close_prices().current <= bb.lower.current: return Signal.buy()

ta.atr

ta.atr(bars, period=14) → ATR (Series subclass)

Average true range. Adds .rising, .expanding, .contracting, and .percentile (0-1, where this reading ranks against recent history) on top of normal Series behavior.

atr = ta.atr(ctx.window(50)) if atr.expanding: return Signal.buy().stop_atr(1.5).target_atr(3)

ta.structure

ta.structure(bars, lookback=10) → Structure

Swing-based market structure: recent swing highs/lows and whether price is making higher highs / higher lows (uptrend structure) or the reverse.

s = ta.structure(ctx.window(50)) if s.uptrend and ctx.last().close > s.last_swing_high: return Signal.buy()

ta.squeeze

ta.squeeze(bars, period=20) → Squeeze

Volatility squeeze detector (Bollinger Bands inside Keltner Channels). Fires when volatility compresses then releases — the classic setup before an expansion move.

sq = ta.squeeze(ctx.window(50)) if sq.fired and sq.direction == "up": return Signal.buy()

ta.regime

ta.regime(bars, period=20) → Regime

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.

r = ta.regime(ctx.window(50)) if r.choppy and rsi.current < 30: return Signal.buy() # mean-reversion only makes sense when choppy

ta.crossover / ta.crossunder / ta.cross

ta.crossover(a, b) → bool

Equivalent to a.crossed_above(b) — a standalone function form.

ta.crossunder(a, b) → bool

Equivalent to a.crossed_below(b).

ta.cross(a, b) → bool

True on either direction of cross.

ta.highest / ta.lowest

ta.highest(series, period) → Series[float]

Rolling maximum over period bars.

ta.lowest(series, period) → Series[float]

Rolling minimum over period bars.

breakout_level = ta.highest(ctx.close_prices(), 20) if ctx.last().close > breakout_level.previous: return Signal.buy()

Series

What every indicator returns. A list of values (oldest first) plus lookback and comparison helpers, so you rarely need to index manually.

.currentlast value
.previoussecond-to-last value
.slopecurrent − previous
.accelerationchange 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.

stats

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.

Score

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.

score = Score() score.add(sq.fired, weight=2) score.add(s.uptrend, weight=1) score.add(vol_confirmed, weight=1) if score.total >= 3: return Signal.buy().with_confidence(score.total / score.max_possible)

Signal

What run(ctx) returns to act on this bar.

Signal.buy() / Signal.sell()

Open or close a long position.

Signal.short() / Signal.cover()

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.

Trade management chain

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_atr(multiple) → Signal

Stop-loss at multiple × ATR from entry. Also becomes the risk basis for expectancy calculations.

.target_atr(multiple) → Signal

Take-profit at multiple × ATR from entry.

.trail_atr(multiple) → Signal

Trailing stop, updated as price moves favorably.

.break_even(trigger_atr) → Signal

Move stop to entry once price has moved trigger_atr × ATR in your favor.

.timeout(bars) → Signal

Force-close the position after bars bars if neither stop nor target has hit.

Signal.buy() \ .stop_atr(1.5) \ .target_atr(3) \ .break_even(1) \ .timeout(20)

Confidence, probability, expectancy

.with_confidence(value) → Signal

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.

.probability framework

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).

.expectancy framework

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 %.

.sample_size framework

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.

.tested_at framework

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.

strategies.<name>(ctx)

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.

m = strategies.regime_trend(ctx)

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.

# composed_confluence.py — first candidate that fires wins def run(ctx): for name in ("regime_trend", "structure_breakout", "squeeze_breakout"): sig = getattr(strategies, name)(ctx) if sig is not None: return sig return None
# expectancy_ranked.py — pick by measured expectancy, not order MIN_SAMPLE_SIZE = 15 def run(ctx): candidates = [c for c in [ strategies.regime_trend(ctx), strategies.structure_breakout(ctx), strategies.squeeze_breakout(ctx), ] if c is not None] if not candidates: return None trusted = [c for c in candidates if c.expectancy is not None and c.sample_size >= MIN_SAMPLE_SIZE] if trusted: return max(trusted, key=lambda s: s.expectancy) return candidates[0] # cold start: nothing trusted yet, take whatever fired

Settings

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.

Robinhood  

Alpaca  

Running backtest...