Trade analytics

The qtrade.analytics module slices closed trades by time, duration, and outcome — turning the raw trade history into questions you actually want answered after a backtest:

  • How long do my winners hold vs my losers?

  • Are my entries clustered on Mondays? In specific months?

  • What's different about the trades I lose money on — bigger size, longer hold, lower entry price?

Every function takes either a Broker (so you can pass bt.broker directly) or the trade-history DataFrame from Broker.get_trade_history(). They return plain pandas objects so you can .plot() them inline, hand them to seaborn, or render them yourself.

Hold duration distribution

from qtrade.analytics import hold_duration_distribution

print(hold_duration_distribution(bt.broker))
#           count   mean  median    std   min   25%   50%   75%   max
# all        42.0  18.50    12.0  14.20   1.0   8.0  12.0  24.0  72.0
# winners    25.0  22.10    18.0  13.80   2.0  12.0  18.0  30.0  72.0
# losers     17.0  13.20     8.0  12.50   1.0   4.0   8.0  16.0  48.0

All durations are reported in hours so intraday and daily strategies share one scale. Pass by_outcome=False to get just the "all" row.

A common pattern: do my winners hold longer than my losers? If so, your strategy is probably riding trends well — and you might be cutting some losers too late. If winners are shorter, the opposite — you're scalping small wins and letting losers run.

Calendar bias

from qtrade.analytics import entries_by_weekday, entries_by_month

# How many entries fire each weekday?
entries_by_weekday(bt.broker, metric="count")
# Monday       12
# Tuesday       8
# ...

# Are some weekdays consistently negative?
entries_by_weekday(bt.broker, metric="profit_sum")

# Win rate by weekday — useful for a "skip Friday" filter check
entries_by_weekday(bt.broker, metric="win_rate")

# Same shape, monthly
entries_by_month(bt.broker, metric="profit_mean")

Available metric values:

  • "count" — number of entries per bucket.

  • "profit_sum" — total profit per bucket.

  • "profit_mean" — average profit per bucket.

  • "win_rate" — fraction of trades closing positive (0.0–1.0).

Empty buckets are filled with 0 (count / profit_sum) or NaN (profit_mean / win_rate). The result is always indexed in calendar order (Monday → Sunday, January → December).

Winning vs losing trade comparison

from qtrade.analytics import win_loss_feature_comparison

win_loss_feature_comparison(bt.broker)
#                  winners_mean  losers_mean   diff  winners_median  losers_median
# Size                    85.20        92.10  -6.90           80.00          90.00
# Entry Price            172.40       175.80  -3.40          170.50         174.00
# Exit Price             184.30       170.20  14.10          181.00         169.50
# Duration (h)            22.10        13.20   8.90           18.00           8.00

Size is compared as abs(size) so longs and shorts pool by magnitude. diff is winners_mean losers_mean — quick visual scan to see which features separate good from bad trades.

To compare extra columns from your trade history (e.g. a custom tag):

trades = bt.get_trade_history()
trades["RSI at Entry"] = ...   # populate however you like
win_loss_feature_comparison(trades, extra_features=["RSI at Entry"])

What's not modeled

  • Time-of-day distribution. For intraday strategies you'd want entries-by-hour. Easy to roll yourself: pd.to_datetime(trades['Entry Time']).dt.hour.value_counts().

  • Equity-curve drawdown analysis. That's portfolio-level, not trade-level — see calculate_stats for the equity-curve metrics.

  • Significance testing. The win/loss comparison shows magnitudes, not p-values. Consider sample size before drawing conclusions — with 10 winners and 10 losers you're in noise territory.