Concepts¶
QTrade is a small library, but understanding how its components fit together makes both the API and the source code easier to navigate. This page is the mental model.
The pieces¶
┌─────────────────────────────────────────────────┐
│ Backtest │
│ (orchestrates a full run; iterates the bars) │
└────────────────────┬────────────────────────────┘
│ owns
▼
┌─────────────────────────────────────────────────┐
│ Strategy │
│ user code: prepare(), on_bar_close() │
│ reads market data, places Orders via the Broker│
└────────────────────┬────────────────────────────┘
│ places orders on
▼
┌─────────────────────────────────────────────────┐
│ Broker │
│ fills Orders into Trades, tracks cash, equity, │
│ margin, SL/TP, equity history │
└──────────┬──────────────────────────┬───────────┘
│ holds │ records
▼ ▼
┌──────────────────┐ ┌─────────────────────┐
│ Position │ │ Order / Trade │
│ active + closed │ │ filled / closed │
│ Trade objects │ │ queues │
└──────────────────┘ └─────────────────────┘
For RL workflows, replace Backtest with TradingEnv — same Broker
underneath, just driven by Gymnasium’s step() instead of a Python
for loop.
What each component does¶
Broker¶
The simulation core. You don’t usually instantiate it directly — both
Backtest and TradingEnv create one for you.
The Broker:
Holds the OHLCV data (single asset or
dict[str, DataFrame]for portfolios)Tracks
cash, computesequity,unrealized_pnl,available_marginMaintains queues of pending / executing / filled / closed orders
Triggers SL / TP exits per bar
Owns one
Positionper assetRecords the portfolio-level
equity_historySeries
Order¶
A trade intention — what you’d like to do. Created with a signed size
(positive = buy, negative = sell), optional limit / stop / sl /
tp / tag, and an asset symbol. Orders go through the broker’s
queues and either fill or get rejected (typically for insufficient margin).
Trade¶
A filled position. Each Order that fills opens (or partially closes)
a Trade. While open, the Trade has entry_price, entry_date, size,
optional sl / tp (modifiable mid-trade — see Stops),
and optional trailing-stop state. When the Trade is closed (by an
opposite-side order, an SL/TP trigger, or end-of-backtest), it gets
exit_price, exit_date, profit, and an exit_reason.
Position¶
A holder for trades on one asset. active_trades are open; closed_trades
are closed. position.size is the net signed size across all active
trades on that asset.
For multi-asset, the Broker holds positions: dict[str, Position] —
one per asset. broker.position (singular) is a convenience accessor
that only works in single-asset mode.
Strategy¶
The piece you write. Subclass qtrade.backtest.Strategy and implement:
prepare()— runs once before the bar loop. Add indicators here.on_bar_close()— called on every bar. Place orders, manage positions.
Inside on_bar_close() you have:
self.data(single-asset) /self.data_by_asset[asset](multi-asset) — the OHLCV slice up to the current bar.self.position/self.positions[asset]— current holdings.self.equity,self.unrealized_pnl— portfolio-level snapshots.self.buy(...),self.sell(...),self.close(...)— order helpers.
Backtest¶
The driver. Given data, a Strategy class, cash, commission, and
margin settings, it:
Constructs a
Brokerand aStrategyinstance.Calls
strategy.prepare().Loops over bars, calling
broker.process_bar(ts)thenstrategy.on_bar_close().Closes open positions at the end.
Plus utilities: optimize, walk_forward_optimize, show_stats,
get_trade_history, plot.
TradingEnv (RL)¶
A Gymnasium environment wrapping the same Broker. Composes three
pluggable schemes:
Scheme |
Decides |
|---|---|
|
Action space + how an action becomes Orders |
|
Observation space + what the agent sees |
|
The scalar reward each step |
See Customizing Trading Environment for the full story.
Lifecycle of a backtest¶
For a typical bar, in this order:
Backtest.runadvances to bar i.Broker.process_bar(ts)fires:Drop cancelled / rejected orders from the pending queue.
Process executing orders (queued for fill at the next bar’s open).
Check SL / TP on every active trade — close any that triggered.
Process pending stop / limit orders.
Update
equity_historywith the new bar’s equity.
Strategy.on_bar_close()runs — you can readself.data(truncated to i) and place new orders. New orders fill immediately iftrade_on_close=True, otherwise queue for next bar’s open.
After the final bar, Broker.close_all_positions() flushes everything
at the last bar’s close.
Key design choices (and why)¶
Event-loop, not vectorized. Strategy code reads like real trading logic (
if x then buy). Easier to write, easier to debug. Slower than vectorbt for huge sweeps — see COMPARISON.md.No look-ahead by construction.
self.datais sliced to[:current_time]. You literally cannot read future bars from insideon_bar_close().One cash pool, per-asset positions. Multi-asset backtests get realistic portfolio behavior (margin shared across assets) without having to manage N independent brokers.
Strategy + RL share one Broker. Same accounting code, same SL/TP logic, same fill semantics. An RL agent in
TradingEnvis governed by exactly the same rules as aStrategyinBacktest.
Where to read next¶
Getting Started — the canonical Strategy / Backtest workflow on a single asset.
Multi-asset / portfolio backtests — passing dict data and using the multi-asset Strategy API.
Walk-forward optimization — sample-out parameter selection.
Stats glossary — what every metric means.
Trading environment — the RL workflow.
FAQ — common pitfalls.