Include the number of long and short trades in the output report
Fixes #1309 providing more context on explaining the PnL in terms of number of long and short positions.
Just to give a bit of context why this is so useful, I have a backtesting model iterator that runs each backtest and concatenates all the reports together into a final dataframe which I then export to excel for slicing and dicing the best model. So each of these stats report outputs is a row.
The output report now is the following, for example, in this example I can see that there is a big imbalance or bias towards bulls. It also reveals the hit rate per side and a quick long to shorts ratio:
Start 2025-09-20 00:00...
End 2025-10-04 10:00...
Duration 14 days 10:00:00
Exposure Time [%] 21.61383
Equity Final [$] 112.95491
Equity Peak [$] 113.15776
Commissions [$] 17.94649
Return [%] 12.95491
Buy & Hold Return [%] -0.16376
Return (Ann.) [%] 4059.93722
Volatility (Ann.) [%] 693.21658
CAGR [%] 2084.97859
Sharpe Ratio 5.85666
Sortino Ratio 4496.70859
Calmar Ratio 1244.74942
Alpha [%] 13.0004
Beta 0.2778
Max. Drawdown [%] -3.26165
Avg. Drawdown [%] -0.43875
Max. Drawdown Duration 3 days 23:00:00
Avg. Drawdown Duration 0 days 17:00:00
# Trades 49
Win Rate [%] 65.30612
# Long Trades 48
Win Rate Longs [%] 64.58333
# Short Trades 1
Win Rate Shorts [%] 100.0
Long/Short Ratio 48.0
Best Trade [%] 1.95605
Worst Trade [%] -0.66365
Avg. Trade [%] 0.25247
Max. Trade Duration 0 days 01:00:00
Avg. Trade Duration 0 days 01:00:00
Profit Factor 2.96564
Expectancy [%] 0.25432
SQN 2.96001
Kelly Criterion 0.43913
_strategy KSpotTradingStra...
_equity_curve ...
_trades Size IsLong...
dtype: object
We maintain it reasonably simple and convenient to compute many custom stats on your own, e.g.:
def long_trades(stats):
return (stats._trades.Size > 0).sum()
def long_short_ratio(stats):
is_long = stats._trades.Size > 0
return is_long.sum() / (~is_long).sum()
def win_rate_long(stats):
is_long = stats._trades.Size > 0
return (stats._trades[is_long].PnL > 0).mean()
Your PR introduces 5 new items into the stats series, increasing its viewable/scroll length for everyone. I wonder if we shouldn't rather add an _extended_stats key with all possible trade stats (and with stats.__getitem__ like:
try:
return super().__getitem__(key)
except KeyError:
# Fallback to extended stats lookup
return self['_extended_stats'][key]
🤔 I'd be open to that PR for sure!