Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: kernc/backtesting.py
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.6.2
Choose a base ref
...
head repository: kernc/backtesting.py
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 0.6.3
Choose a head ref

Commits on Feb 20, 2025

  1. REF: Add tqdm to Optimize.run

    Closes #1176
    kernc committed Feb 20, 2025
    Copy the full SHA
    6cbdd99 View commit details
  2. Copy the full SHA
    5e72e7b View commit details
  3. Copy the full SHA
    9286232 View commit details
  4. Copy the full SHA
    0853e96 View commit details
  5. Copy the full SHA
    9938d08 View commit details
  6. ENH: lib.MultiBacktest multi-dataset backtesting wrapper

    Fixes #508
    
    Thanks!
    
    Co-Authored-By: Mike Judge <mikelovesrobots@gmail.com>
    kernc and mikelovesrobots committed Feb 20, 2025
    Copy the full SHA
    5503b9d View commit details
  7. CI: Test on Python '3.*'

    kernc committed Feb 20, 2025
    Copy the full SHA
    e25a1c9 View commit details
  8. DOC: Fix YouTube link

    kernc committed Feb 20, 2025
    Copy the full SHA
    ad6d6e0 View commit details

Commits on Mar 4, 2025

  1. Copy the full SHA
    cd4bc6c View commit details

Commits on Mar 11, 2025

  1. Copy the full SHA
    e1a86ef View commit details
  2. Copy the full SHA
    31cff13 View commit details
  3. BUG: Only plot trades when some trades are present

    Avoids "Trades (0)" legend item.
    kernc committed Mar 11, 2025
    Copy the full SHA
    533c711 View commit details
  4. Copy the full SHA
    3b9a294 View commit details
  5. Copy the full SHA
    a78edf5 View commit details
  6. Copy the full SHA
    886fc1d View commit details
  7. Copy the full SHA
    b35bced View commit details
  8. Copy the full SHA
    acef307 View commit details
  9. Copy the full SHA
    233cd03 View commit details
  10. Copy the full SHA
    7431063 View commit details
  11. Copy the full SHA
    3b74729 View commit details
  12. Copy the full SHA
    86f5f39 View commit details
  13. Copy the full SHA
    3938cf7 View commit details
  14. BUG: Single legend item for indicators with singular/default names

    Refs: 01551af
    
    Populating legends with numerous `λ[i]` items was never the intention
    of #382
    kernc committed Mar 11, 2025
    Copy the full SHA
    d673d0a View commit details
  15. Copy the full SHA
    5928524 View commit details
  16. Copy the full SHA
    bf28ddd View commit details
  17. Copy the full SHA
    5e5dfdf View commit details
  18. Copy the full SHA
    5e68bba View commit details
  19. ENH: Add Alpha & Beta stats (#1221)

    * added beta & alpha / resolved merge conflict
    
    * simplified beta calculation
    
    * remove DS_store
    
    * move beta & alpha / use log return
    
    * Update backtesting/_stats.py
    * Update backtesting/_stats.py
    
    * alpha & beta test
    
    * #noqa: E501
    
    * add space
    
    * update docs
    
    * Revert unrelated change
    * Add comment
    jensnesten authored Mar 11, 2025
    Copy the full SHA
    70a2c33 View commit details
  20. Copy the full SHA
    fa3ea11 View commit details
  21. BUG: Plot: Increase subplots heights after 3b74729

    Refs: 3b74729 "ENH: Reduce height of indicator charts, introduce an overridable global"
    kernc committed Mar 11, 2025
    Copy the full SHA
    2c6fcad View commit details
  22. Copy the full SHA
    b1a869c View commit details
1 change: 0 additions & 1 deletion .github/ISSUE_TEMPLATE/2-enh.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: Enhancement proposal
description: Describe the enhancement you'd like to see
title: "Short title loaded with keywords"
labels: ["enhancement"]
body:
- type: markdown
attributes:
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -34,7 +34,12 @@ jobs:
timeout-minutes: 3
strategy:
matrix:
python-version: [3.11, 3.12, 3.13]
python-version: [3.12, 3.13]
experimental: [false]
include:
- python-version: '3.*'
experimental: true
continue-on-error: ${{ matrix.experimental }}
steps:
- uses: actions/setup-python@v5
with:
32 changes: 31 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -5,12 +5,42 @@ These were the major changes contributing to each release:

### 0.x.x

### 0.6.3
(2025-03-11)

* Enhancements:
* `backtesting.lib.TrailingStrategy` supports setting trailing stop-loss by percentage.
* [`backtesting.lib.MultiBacktest`](https://kernc.github.io/backtesting.py/doc/backtesting/lib.html#backtesting.lib.MultiBacktest)
multi-dataset backtesting wrapper.
* `Backtest.run()` wrapped in `tqdm()`
* Rename parameter `lib.FractionalBacktest(fractional_unit=)`.
* Add market alpha & market beta stats (#1221)
* Plot improvements:
* Plot trade duration lines in the P&L plot section.
* Simplify PL section, use circular markers.
* Only plot trades when some trades are present.
* Set `fig.yaxis.ticker.desired_num_ticks=3` for indicator subplots.
* Single legend item for indicators with singular/default names.
* Make "OHLC" itself a togglable legend item.
* Add xwheel_pan tool, conditioned on activation for now
(upvote [Bokeh issue](https://github.com/bokeh/bokeh/issues/14363)).
* Reduce height of indicator charts, introduce an overridable private
global `backtesting._plotting._INDICATOR_HEIGHT`.
* Bug fixes:
* Fixed `Position.pl` occasionally not matching `Position.pl_pct` in sign.
* SL _always_ executes before TP when hit in the same bar.
* Fix `functools.partial` objects do not always have a `__module__` attr in Python 3.9 (#1233)
* Fix stop-market and TP hit within the same bar.
* Documentation improvements (warnings, links, ...)


### 0.6.2
(2025-02-19)

* Enhancements:
* Grid optimization with mp.Pool & mp.shm.SharedMemory (#1222)
* `backtesting.lib.FractionalBacktest` that supports fractional trading
* [`backtesting.lib.FractionalBacktest`](https://kernc.github.io/backtesting.py/doc/backtesting/lib.html#backtesting.lib.FractionalBacktest)
that supports fractional trading
* `backtesting.__all__` for better `from backtesting import *` and suggestions
* Bugs fixed:
* Fix remaining issues with `trade_on_close=True`
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -3,9 +3,10 @@
Backtesting.py
==============
[![Build Status](https://img.shields.io/github/actions/workflow/status/kernc/backtesting.py/ci.yml?branch=master&style=for-the-badge)](https://github.com/kernc/backtesting.py/actions)
[![Code Coverage](https://img.shields.io/codecov/c/gh/kernc/backtesting.py.svg?style=for-the-badge)](https://codecov.io/gh/kernc/backtesting.py)
[![Code Coverage](https://img.shields.io/codecov/c/gh/kernc/backtesting.py.svg?style=for-the-badge&label=Covr)](https://codecov.io/gh/kernc/backtesting.py)
[![Source lines of code](https://img.shields.io/endpoint?url=https%3A%2F%2Fghloc.vercel.app%2Fapi%2Fkernc%2Fbacktesting.py%2Fbadge?filter=.py%26format=human&style=for-the-badge&label=SLOC&color=green)](https://ghloc.vercel.app/kernc/backtesting.py)
[![Backtesting on PyPI](https://img.shields.io/pypi/v/backtesting.svg?color=blue&style=for-the-badge)](https://pypi.org/project/backtesting)
[![PyPI downloads](https://img.shields.io/pypi/dd/backtesting.svg?color=skyblue&style=for-the-badge)](https://pypistats.org/packages/backtesting)
[![PyPI downloads](https://img.shields.io/pypi/dd/backtesting.svg?style=for-the-badge&label=D/L&color=skyblue)](https://pypistats.org/packages/backtesting)
[![Total downloads](https://img.shields.io/pepy/dt/backtesting?style=for-the-badge&label=%E2%88%91&color=skyblue)](https://pypistats.org/packages/backtesting)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/kernc?color=pink&style=for-the-badge&label=%E2%99%A5)](https://github.com/sponsors/kernc)

@@ -14,7 +15,7 @@ Backtest trading strategies with Python.
[**Project website**](https://kernc.github.io/backtesting.py) + [Documentation] &nbsp;&nbsp;|&nbsp; [YouTube]

[Documentation]: https://kernc.github.io/backtesting.py/doc/backtesting/
[YouTube]: https://www.youtube.com/?q=%22backtesting.py%22
[YouTube]: https://www.youtube.com/results?q=%22backtesting.py%22

Installation
------------
@@ -67,6 +68,8 @@ CAGR [%] 16.80
Sharpe Ratio 0.66
Sortino Ratio 1.30
Calmar Ratio 0.77
Alpha [%] 450.62
Beta 0.02
Max. Drawdown [%] -33.08
Avg. Drawdown [%] -5.58
Max. Drawdown Duration 688 days 00:00:00
2 changes: 1 addition & 1 deletion backtesting/__init__.py
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@
## Video Tutorials
* Some [**coverage on YouTube**](https://github.com/kernc/backtesting.py/discussions/677).
* [YouTube search](https://www.youtube.com/?q=%22backtesting.py%22)
* [YouTube search](https://www.youtube.com/results?q=%22backtesting.py%22)
## Example Strategies
56 changes: 26 additions & 30 deletions backtesting/_plotting.py
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@
CrosshairTool,
CustomJS,
ColumnDataSource,
CustomJSTransform,
Label, NumeralTickFormatter,
Span,
HoverTool,
@@ -39,7 +40,7 @@
from bokeh.io.state import curstate
from bokeh.layouts import gridplot
from bokeh.palettes import Category10
from bokeh.transform import factor_cmap
from bokeh.transform import factor_cmap, transform

from backtesting._util import _data_period, _as_list, _Indicator, try_

@@ -54,7 +55,7 @@
warnings.warn('Jupyter Notebook detected. '
'Setting Bokeh output to notebook. '
'This may not work in Jupyter clients without JavaScript '
'support (e.g. PyCharm, Spyder IDE). '
'support, such as old IDEs. '
'Reset with `backtesting.set_bokeh_output(notebook=False)`.')
output_notebook()

@@ -110,6 +111,7 @@ def lightness(color, lightness=.94):


_MAX_CANDLES = 10_000
_INDICATOR_HEIGHT = 50


def _maybe_resample_data(resample_rule, df, indicators, equity_data, trades):
@@ -216,6 +218,7 @@ def plot(*, results: pd.Series,
plot_equity = plot_equity and not trades.empty
plot_return = plot_return and not trades.empty
plot_pl = plot_pl and not trades.empty
plot_trades = plot_trades and not trades.empty
is_datetime_index = isinstance(df.index, pd.DatetimeIndex)

from .lib import OHLCV_AGG
@@ -238,7 +241,8 @@ def plot(*, results: pd.Series,
x_axis_type='linear',
width=plot_width,
height=400,
tools="xpan,xwheel_zoom,box_zoom,undo,redo,reset,save",
# TODO: xwheel_pan on horizontal after https://github.com/bokeh/bokeh/issues/14363
tools="xpan,xwheel_zoom,xwheel_pan,box_zoom,undo,redo,reset,save",
active_drag='xpan',
active_scroll='xwheel_zoom')

@@ -257,7 +261,6 @@ def plot(*, results: pd.Series,
trade_source = ColumnDataSource(dict(
index=trades['ExitBar'],
datetime=trades['ExitTime'],
exit_price=trades['ExitPrice'],
size=trades['Size'],
returns_positive=(trades['ReturnPct'] > 0).astype(int).astype(str),
))
@@ -293,13 +296,14 @@ def plot(*, results: pd.Series,
('Volume', '@Volume{0,0}')]

def new_indicator_figure(**kwargs):
kwargs.setdefault('height', 90)
kwargs.setdefault('height', _INDICATOR_HEIGHT)
fig = new_bokeh_figure(x_range=fig_ohlc.x_range,
active_scroll='xwheel_zoom',
active_drag='xpan',
**kwargs)
fig.xaxis.visible = False
fig.yaxis.minor_tick_line_color = None
fig.yaxis.ticker.desired_num_ticks = 3
return fig

def set_tooltips(fig, tooltips=(), vline=True, renderers=()):
@@ -358,7 +362,7 @@ def _plot_equity_section(is_return=False):
source.add(equity, source_key)
fig = new_indicator_figure(
y_axis_label=yaxis_label,
**({} if plot_drawdown else dict(height=110)))
**(dict(height=80) if plot_drawdown else dict(height=100)))

# High-watermark drawdown dents
fig.patch('index', 'equity_dd',
@@ -409,7 +413,7 @@ def _plot_equity_section(is_return=False):

def _plot_drawdown_section():
"""Drawdown section"""
fig = new_indicator_figure(y_axis_label="Drawdown")
fig = new_indicator_figure(y_axis_label="Drawdown", height=80)
drawdown = equity_data['DrawdownPct']
argmax = drawdown.idxmax()
source.add(drawdown, 'drawdown')
@@ -423,29 +427,26 @@ def _plot_drawdown_section():

def _plot_pl_section():
"""Profit/Loss markers section"""
fig = new_indicator_figure(y_axis_label="Profit / Loss")
fig = new_indicator_figure(y_axis_label="Profit / Loss", height=80)
fig.add_layout(Span(location=0, dimension='width', line_color='#666666',
line_dash='dashed', line_width=1))
returns_long = np.where(trades['Size'] > 0, trades['ReturnPct'], np.nan)
returns_short = np.where(trades['Size'] < 0, trades['ReturnPct'], np.nan)
line_dash='dashed', level='underlay', line_width=1))
trade_source.add(trades['ReturnPct'], 'returns')
size = trades['Size'].abs()
size = np.interp(size, (size.min(), size.max()), (8, 20))
trade_source.add(returns_long, 'returns_long')
trade_source.add(returns_short, 'returns_short')
trade_source.add(size, 'marker_size')
if 'count' in trades:
trade_source.add(trades['count'], 'count')
r1 = fig.scatter('index', 'returns_long', source=trade_source, fill_color=cmap,
marker='triangle', line_color='black', size='marker_size')
r2 = fig.scatter('index', 'returns_short', source=trade_source, fill_color=cmap,
marker='inverted_triangle', line_color='black', size='marker_size')
trade_source.add(trades[['EntryBar', 'ExitBar']].values.tolist(), 'lines')
fig.multi_line(xs='lines',
ys=transform('returns', CustomJSTransform(v_func='return [...xs].map(i => [0, i]);')),
source=trade_source, color='#999', line_width=1)
r1 = fig.scatter('index', 'returns', source=trade_source, fill_color=cmap,
marker='circle', line_color='black', size='marker_size')
tooltips = [("Size", "@size{0,0}")]
if 'count' in trades:
tooltips.append(("Count", "@count{0,0}"))
set_tooltips(fig, tooltips + [("P/L", "@returns_long{+0.[000]%}")],
set_tooltips(fig, tooltips + [("P/L", "@returns{+0.[000]%}")],
vline=False, renderers=[r1])
set_tooltips(fig, tooltips + [("P/L", "@returns_short{+0.[000]%}")],
vline=False, renderers=[r2])
fig.yaxis.formatter = NumeralTickFormatter(format="0.[00]%")
return fig

@@ -506,9 +507,10 @@ def _plot_superimposed_ohlc():

def _plot_ohlc():
"""Main OHLC bars"""
fig_ohlc.segment('index', 'High', 'index', 'Low', source=source, color="black")
fig_ohlc.segment('index', 'High', 'index', 'Low', source=source, color="black",
legend_label='OHLC')
r = fig_ohlc.vbar('index', BAR_WIDTH, 'Open', 'Close', source=source,
line_color="black", fill_color=inc_cmap)
line_color="black", fill_color=inc_cmap, legend_label='OHLC')
return r

def _plot_ohlc_trades():
@@ -565,13 +567,7 @@ def __eq__(self, other):

if isinstance(value.name, str):
tooltip_label = value.name
if len(value) == 1:
legend_labels = [LegendStr(value.name)]
else:
legend_labels = [
LegendStr(f"{value.name}[{i}]")
for i in range(len(value))
]
legend_labels = [LegendStr(value.name)] * len(value)
else:
tooltip_label = ", ".join(value.name)
legend_labels = [LegendStr(item) for item in value.name]
@@ -614,7 +610,7 @@ def __eq__(self, other):
round(abs(mean), -1) in (50, 100, 200)):
fig.add_layout(Span(location=float(mean), dimension='width',
line_color='#666666', line_dash='dashed',
line_width=.5))
level='underlay', line_width=.5))
if is_overlay:
ohlc_tooltips.append((tooltip_label, NBSP.join(tooltips)))
else:
7 changes: 7 additions & 0 deletions backtesting/_stats.py
Original file line number Diff line number Diff line change
@@ -152,6 +152,13 @@ def _round_timedelta(value, _period=_data_period(index)):
s.loc['Sortino Ratio'] = (annualized_return - risk_free_rate) / (np.sqrt(np.mean(day_returns.clip(-np.inf, 0)**2)) * np.sqrt(annual_trading_days)) # noqa: E501
max_dd = -np.nan_to_num(dd.max())
s.loc['Calmar Ratio'] = annualized_return / (-max_dd or np.nan)
equity_log_returns = np.log(equity[1:] / equity[:-1])
market_log_returns = np.log(c[1:] / c[:-1])
cov_matrix = np.cov(equity_log_returns, market_log_returns)
beta = cov_matrix[0, 1] / cov_matrix[1, 1]
# Jensen CAPM Alpha: can be strongly positive when beta is negative and B&H Return is large
s.loc['Alpha [%]'] = s.loc['Return [%]'] - risk_free_rate * 100 - beta * (s.loc['Buy & Hold Return [%]'] - risk_free_rate * 100) # noqa: E501
s.loc['Beta'] = beta
s.loc['Max. Drawdown [%]'] = max_dd * 100
s.loc['Avg. Drawdown [%]'] = -dd_peaks.mean() * 100
s.loc['Max. Drawdown Duration'] = _round_timedelta(dd_dur.max())
Loading