import pandas as pd
import qrt as q
spy = q.data.datasets.load("spy")
benchmark = spy["close"].pct_change().dropna().rename("SPY")Trade logs
Beyond return streams, q.plot understands qrt’s canonical trades format: one row per round-trip trade with reserved columns (entry_time, exit_time, direction (1 = long, -1 = short), entry_reason/exit_reason, prices, the direction-adjusted decimal return, mae/mfe excursions, …; see q.stats.TRADE_COLUMNS). Any extra columns are entry-time feature snapshots — the indicator values that triggered the entry. This is the shape a backtest or execution log should produce.
qrt bundles four demo trade logs generated from the SPY sample data (q.data.datasets.TRADE_LOGS): a slow EMA 50/200 golden/death cross, Connors’ RSI-2 mean reversion, a 252-day-high breakout with a chandelier trailing stop, and a seeded random baseline:
See Data schemas for the full column-by-column schema.
trade_logs = {name: q.data.datasets.load(name) for name in q.data.datasets.TRADE_LOGS}
ema_trades = trade_logs["spy_ema_cross"]
ema_trades.tail()| symbol | entry_time | exit_time | direction | entry_reason | exit_reason | entry_price | exit_price | return | mae | mfe | size | fees | ema_spread | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 6 | SPY | 2019-02-27 | 2020-03-20 | 1 | golden_cross | death_cross | 249.158389 | 222.370731 | -0.107513 | -0.165813 | 0.240488 | NaN | NaN | 0.001155 |
| 7 | SPY | 2020-06-09 | 2022-05-06 | 1 | golden_cross | death_cross | 293.676443 | 388.013508 | 0.321228 | -0.073556 | 0.537823 | NaN | NaN | 0.000072 |
| 8 | SPY | 2023-02-10 | 2023-03-24 | 1 | golden_cross | death_cross | 388.076813 | 376.101052 | -0.030859 | -0.062115 | 0.022643 | NaN | NaN | 0.000437 |
| 9 | SPY | 2023-03-30 | 2025-04-17 | 1 | golden_cross | death_cross | 387.859061 | 520.319482 | 0.341517 | -0.005766 | 0.554452 | NaN | NaN | 0.000354 |
| 10 | SPY | 2025-05-16 | 2026-07-17 | 1 | golden_cross | end_of_data | 583.046910 | 743.289978 | 0.274837 | -0.026469 | 0.300832 | NaN | NaN | 0.000492 |
Trade markers on price: q.plot.trades
Renders the price series with one marker per entry (triangle up for longs, down for shorts) and exit (x, green for winners, red for losers), shading each holding period by outcome. Hovering a marker shows the trade’s reason, return, holding period, and any feature-snapshot columns. Per-bar indicators are not stored in the trade log; recompute them with q.indicator and pass them as features overlays (TA-Lib’s EMA seeds slightly differently than the generator’s, but they converge quickly):
features = pd.DataFrame(
{
"EMA 50": q.indicator.talib.EMA(spy["close"], timeperiod=50),
"EMA 200": q.indicator.talib.EMA(spy["close"], timeperiod=200),
}
)
fig = q.plot.trades(ema_trades, spy, features=features, title="SPY EMA 50/200 cross trades")
fig.show()Excursion analysis: q.plot.mae_mfe
Each trade’s maximum adverse excursion (how far it went against you while it was open — informs stop placement) and maximum favorable excursion (how much open profit existed — informs profit taking) plotted against its final return, on shared axes. The RSI-2 log’s 188 short-duration mean-reversion trades make for a richer scatter than the EMA cross’s 11:
fig = q.plot.mae_mfe(trade_logs["spy_rsi2"], title="SPY RSI-2 trade excursions")
fig.show()Return distributions: q.plot.trade_distribution
Box plot of per-trade returns grouped by any trades column — exit_reason, direction, or a feature-snapshot column. Since the format is just a DataFrame, tagging and concatenating the four demo logs compares the strategies directly:
all_trades = pd.concat(
[log.assign(strategy=name.removeprefix("spy_")) for name, log in trade_logs.items()],
ignore_index=True,
)
fig = q.plot.trade_distribution(all_trades, by="strategy", title="Per-trade returns by demo strategy")
fig.show()From trades to a tearsheet
q.stats.trades_to_returns bridges the two worlds: with prices it marks each open trade to market daily, producing a return stream that every figure on this page accepts. Here the EMA-cross log becomes a full performance report against SPY buy & hold (q.stats.trade_stats is the tabular counterpart):
ema_returns = q.stats.trades_to_returns(ema_trades, prices=spy["close"]).rename("EMA cross")
fig = q.plot.plot(ema_returns, benchmark=benchmark, title="SPY EMA cross vs. SPY buy & hold")
fig.show()