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.

import pandas as pd

import qrt as q

spy = q.data.datasets.load("spy")
benchmark = spy["close"].pct_change().dropna().rename("SPY")
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()
Back to top