backtesting.py icon indicating copy to clipboard operation
backtesting.py copied to clipboard

Include the number of long and short trades in the output report

Open bravegag opened this issue 2 months ago • 1 comments

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

bravegag avatar Oct 05 '25 10:10 bravegag

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!

kernc avatar Oct 30 '25 21:10 kernc