Backtest price-action and quant strategies in Python — simple API, serious simulation.
pip install stolgo · Python 3.10+ · MIT License · bandl data & brokers
stolgo is a lightweight backtesting framework built for traders who think in price action — breakouts, consolidation, support/resistance, candle behaviour — not only indicator crossovers.
Most libraries optimise for MACD/RSI grids. stolgo optimises for clear rules on OHLCV, a deterministic event loop (fills, cash, positions), and reporting you can share (equity curve, drawdown, trade log, Plotly tearsheet).
Write your logic once. Point it at market data or a CSV. Get metrics and charts.
| Strength | What you get |
|---|---|
| Price-action first | Write pa.crosses_above(pa.resistance(21)) — readable rules, not indicator soup |
| Composable | Combine levels, relations, candles, and streaks with & | ~ .then() |
| Trade in one line | trade.long(ctx, rr=(1, 2), stop="candle_low") — stops, targets, sizing handled |
| Presets | pa.preset.consolidation_breakout(7) and friends — proven setups, zero wiring |
| Honest simulation | Event loop with configurable fill timing (next_open or close), commission, slippage |
| No look-ahead | ctx.data only exposes history up to the current bar; MTF levels align safely |
| Data your way | bandl for crypto/equity OHLCV, or load() for CSV/Parquet |
| Built-in analytics | Sharpe, drawdown, hit rate, profit factor, HTML tearsheet |
pip install stolgoFor live market data via bandl (recommended):
pip install stolgo bandlOptional extras:
pip install "stolgo[numba]" # faster sweeps
pip install "stolgo[ui]" # local read-only backtest browserSee docs/UI.md for the local backtest UI, its read-only API, and security notes.
This is the heart of stolgo. Describe a setup the way you'd say it out loud, attach a
risk/reward bracket, and backtest it. One import: import stolgo.pa as pa.
from datetime import datetime, timedelta, timezone
import stolgo.pa as pa
from stolgo import Backtest, Bandl, Context, Strategy, trade
# 1. Describe the edge in plain price-action language
entry = pa.crosses_above(pa.resistance(21)) # close breaks the 21-bar high
# 2. Trade it with a 1:2 risk/reward bracket, stop under the signal candle
class Breakout(Strategy):
bracket = None
def on_bar(self, ctx: Context) -> None:
if not ctx.position.flat: # in a trade → manage exit
if trade.bracket_hit(ctx, self.bracket): # stop or target touched
trade.close(ctx)
self.bracket = None
elif entry(ctx): # flat + breakout → enter
self.bracket = trade.long(ctx, rr=(1, 2), stop="candle_low", qty=0.05)
# 3. Run it on real data
end = datetime.now(timezone.utc)
df = Bandl().history("BTCUSDT", "1h", end - timedelta(days=365), end)
result = Backtest(Breakout(), df, fill_on="close").run()
print(result.summary())
result.report.to_html("tearsheet.html")That's a complete, look-ahead-safe breakout backtest with stops, targets, fees, and a shareable tearsheet — no indicators required.
Everything below is a flat attribute on pa. Mix and match with & (and), | (or),
~ (not), and .then() (sequence) to build any setup.
Levels — a price line per bar
pa.resistance(20) pa.support(20) # rolling highs / lows
pa.range_high(7) pa.range_low(7) # consolidation box edges
pa.donchian_high(20) pa.donchian_low(20)
pa.vwap() pa.prev_day_high() pa.prev_day_low()
pa.swing_high(5) pa.swing_low(5) pa.pivot_point()
pa.level(42_000) # a fixed price
pa.resistance(21, tf="1d") # daily level on intraday bars (MTF)Relations — turn a level into a true/false rule
pa.above(lvl) pa.below(lvl)
pa.crosses_above(lvl) pa.crosses_below(lvl) # breakouts / breakdowns
pa.rejected_at(lvl) pa.recovered_at(lvl) # failed break / reclaim
pa.near(lvl, pct=0.005) pa.touched(lvl)Patterns — candles, streaks, structure, momentum
pa.bullish_engulfing() pa.bearish_engulfing() pa.hammer() pa.doji()
pa.streak.green(3) pa.streak.red(3) pa.first_red_day()
pa.consolidation(days=7) pa.breakout_up(7) pa.breakout_down(7)
pa.run_up(min_pct=2.0) pa.parabolic_up() pa.giant_uptrend()Compose them into the setup you actually trade
# 7-day box, then close breaks the box high
entry = pa.consolidation(days=7) & pa.crosses_above(pa.range_high(7))
# three green candles, then a bearish engulfing → fade it
fade = pa.streak.green(3).then(pa.bearish_engulfing())
# break above 21-day daily resistance, but only near VWAP
intraday = pa.crosses_above(pa.resistance(21, tf="1d")) & pa.near(pa.vwap())stolgo.trade turns a signal into an order with a stop and target — no manual SL/TP math.
from stolgo import trade
trade.long(ctx, rr=(1, 2), stop="candle_low", qty=0.05) # target = 2× risk
trade.short(ctx, rr=(1, 3), stop="candle_high", qty=0.05)
trade.bracket_hit(ctx, bracket) # -> "stop", "target", or None on this bar
trade.close(ctx) # flatten (longs sell, shorts cover)Size by fixed qty, or risk a fixed fraction of equity per trade with
size_risk_pct=0.01.
Don't want to wire rules yourself? pa.preset.* returns ready-made rules.
import stolgo.pa as pa
pa.preset.scalp_green_fade(min_green=3) # fade a green run on a reversal candle
pa.preset.consolidation_breakout(days=7) # classic box breakout
pa.preset.breakout_intraday(days=7) # daily S/R, intraday trigger → (long, short)
pa.preset.failed_break_intraday(days=7) # fade failed breaks of daily S/R
pa.preset.parabolic_short(min_pct=2.0) # short the first red day after a parabolaSee docs/PA.md for the full grammar, multi-timeframe levels, and every
rule. Runnable bots live under examples/pa/.
Prefer indicators or vector signals? The same engine runs those too.
from stolgo import Backtest, Context, Strategy, load
from stolgo.signals import sma
class Trend(Strategy):
def on_start(self, ctx: Context) -> None:
self._sma = sma(ctx.data.close, 50)
def on_bar(self, ctx: Context) -> None:
if ctx.position.flat and ctx.data.close[-1] > self._sma[ctx.i]:
ctx.buy(size_pct=0.25)
elif not ctx.position.flat and ctx.data.close[-1] < self._sma[ctx.i]:
ctx.close()
df = load("ohlcv.csv", symbol="BTCUSDT")
print(Backtest(Trend(), df, cash=100_000, commission=0.001).run().summary())stolgo does not lock you into one vendor. Use whichever fits your workflow.
bandl — Bandl
bandl aggregates OHLCV from crypto exchanges, equity feeds, and other providers. stolgo wraps it as Bandl: fetch, normalize columns, optional parquet cache, then backtest.
from stolgo import Bandl
df = Bandl(provider="crypto").history("BTCUSDT", "1d", start, end)Use this for crypto (e.g. BTCUSDT), Indian/US equity, and any symbol bandl supports. Configure credentials per bandl’s docs when required.
For research archives, exports, or offline work:
from stolgo import load
df = load("data/btc_daily.parquet", symbol="BTCUSDT")Supports .csv, .parquet, .pq. Timestamps are parsed to UTC; columns are normalized to open, high, low, close, volume.
Already have pandas OHLCV? Pass it directly to Backtest as long as it has a UTC datetime index (or a timestamp column).
| Import | Purpose |
|---|---|
stolgo.pa |
Price-action toolkit: levels, relations, patterns, presets |
trade |
Risk/reward brackets: trade.long, trade.short, trade.bracket_hit, trade.close |
Backtest |
High-level runner → RunResult |
Strategy |
Base class: on_start, on_bar, on_fill, on_end |
Context |
Per-bar API: ctx.data, ctx.position, ctx.buy / ctx.close |
Bandl |
Market data via bandl (BandlDataSource alias) |
load |
Load CSV / Parquet |
RunResult |
metrics, trades, equity, report |
parameter_sweep |
Grid search over strategy parameters |
stolgo.signals |
sma, ema, atr, rsi, donchian, … |
Advanced: Engine, RunConfig, Pipeline (cross-sectional), stolgo.report.export_all.
Price action (stolgo.pa + trade) — start here:
| Script | Setup |
|---|---|
examples/pa/btc_21d_sr_intraday.py |
Break of 21-day daily support/resistance on intraday bars (long + short) |
examples/pa/btc_consolidation_pa.py |
7-day consolidation breakout with a 1:4 bracket |
examples/pa/scalp_green_fade.py |
Fade a green streak on a reversal candle |
examples/pa/intraday_breakout.py |
pa.preset.breakout_intraday long/short |
examples/pa/failed_break.py |
Fade failed breaks of daily S/R |
examples/pa/parabolic_short.py |
Short the first red day after a parabolic run |
Indicators & vector signals:
| Script | Demonstrates |
|---|---|
examples/trend_breakout_backtest.py |
SMA trend, signals, tearsheet |
examples/vector_momentum_backtest.py |
Vector entries / exits (no on_bar body) |
examples/parameter_sweep.py |
100-combo parameter sweep |
CLI:
stolgo my_strategy.py --class MyStrategy --data ohlcv.csv --output ./resultsOHLCV (Bandl / load / DataFrame)
↓
Backtest(strategy, data).run()
↓
Engine: for each bar → match fills → on_bar → risk → submit orders
↓
RunResult: equity, trades, metrics, Plotly tearsheet
Default fill model: signal on bar t → fill at bar t+1 open (fill_on="next_open"). Use fill_on="close" when you want same-bar close fills (e.g. breakout-on-close setups).
The original stolgo modules (candlestick, breakout, trend, …) remain available for pattern checks on raw DataFrames. New projects should prefer the Strategy + Backtest API above.
from stolgo.candlestick import CandleStick
from stolgo.breakout import Breakout
cs = CandleStick()
is_engulfing = cs.is_bullish_engulfing(df)git clone https://github.com/stockalgo/stolgo.git
cd stolgo
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest tests/ -qArchitecture notes: docs/HLD.md.
Pull requests are welcome. For larger changes, open an issue first. Follow PEP 8, add tests for new behaviour, and keep the public API intuitive (prefer from stolgo import … over deep internal paths).