Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 52 additions & 7 deletions backtesting/_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,16 @@ def _plot_ohlc_trades():
legend_label=f'Trades ({len(trades)})',
line_width=8, line_alpha=1, line_dash='dotted')

MARKER_FUNCTIONS = {
'circle': lambda fig, **kwargs: fig.scatter(marker='circle', **kwargs),
'square': lambda fig, **kwargs: fig.scatter(marker='square', **kwargs),
'triangle': lambda fig, **kwargs: fig.scatter(marker='triangle', **kwargs),
'diamond': lambda fig, **kwargs: fig.scatter(marker='diamond', **kwargs),
'cross': lambda fig, **kwargs: fig.scatter(marker='cross', **kwargs),
'x': lambda fig, **kwargs: fig.scatter(marker='x', **kwargs),
'star': lambda fig, **kwargs: fig.scatter(marker='star', **kwargs),
}

def _plot_indicators():
"""Strategy indicators"""

Expand Down Expand Up @@ -563,7 +573,36 @@ def __eq__(self, other):
tooltips = []
colors = value._opts['color']
colors = colors and cycle(_as_list(colors)) or (
cycle([next(ohlc_colors)]) if is_overlay else colorgen())
cycle([next(ohlc_colors)]) if is_overlay else colorgen()
)

marker = value._opts.get('marker', 'circle')
marker_list = _as_list(marker)

# Check for invalid markers and replace them with 'circle'
if any(m not in MARKER_FUNCTIONS for m in marker_list):
if len(marker_list) == 1:
# If it's a single invalid marker, replace it
warnings.warn(f"Unknown marker type '{marker}', falling back to 'circle'")
marker = 'circle'
value._opts['marker'] = marker
marker_list = ['circle']
else:
# If it's an array with some invalid markers, replace only the invalid ones
warnings.warn(f"Unknown marker type(s) in '{marker}', replacing invalid markers with 'circle'")
marker_list = [m if m in MARKER_FUNCTIONS else 'circle' for m in marker_list]
value._opts['marker'] = marker_list

markers = cycle(marker_list)

marker_size = value._opts.get('marker_size')
# Handle marker_size as either a single value or an array
if marker_size is not None:
marker_size_list = _as_list(marker_size)
marker_sizes = cycle(marker_size_list)
else:
default_size = BAR_WIDTH / 2 * (.9 if is_overlay else .6)
marker_sizes = cycle([default_size])

if isinstance(value.name, str):
tooltip_label = value.name
Expand All @@ -574,6 +613,8 @@ def __eq__(self, other):

for j, arr in enumerate(value):
color = next(colors)
marker = next(markers)
marker_size = next(marker_sizes)
source_name = f'{legend_labels[j]}_{i}_{j}'
if arr.dtype == bool:
arr = arr.astype(int)
Expand All @@ -582,22 +623,26 @@ def __eq__(self, other):
if is_overlay:
ohlc_extreme_values[source_name] = arr
if is_scatter:
fig.circle(
'index', source_name, source=source,
marker_func = MARKER_FUNCTIONS[marker]
marker_func(
fig,
x='index', y=source_name, source=source,
legend_label=legend_labels[j], color=color,
line_color='black', fill_alpha=.8,
radius=BAR_WIDTH / 2 * .9)
size=marker_size)
else:
fig.line(
'index', source_name, source=source,
legend_label=legend_labels[j], line_color=color,
line_width=1.3)
else:
if is_scatter:
r = fig.circle(
'index', source_name, source=source,
marker_func = MARKER_FUNCTIONS[marker]
r = marker_func(
fig,
x='index', y=source_name, source=source,
legend_label=legend_labels[j], color=color,
radius=BAR_WIDTH / 2 * .6)
size=marker_size)
else:
r = fig.line(
'index', source_name, source=source,
Expand Down
21 changes: 18 additions & 3 deletions backtesting/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def _check_params(self, params):
def I(self, # noqa: E743
func: Callable, *args,
name=None, plot=True, overlay=None, color=None, scatter=False,
marker='circle', marker_size=None,
**kwargs) -> np.ndarray:
"""
Declare an indicator. An indicator is just an array of values
Expand Down Expand Up @@ -106,6 +107,13 @@ def I(self, # noqa: E743
If `scatter` is `True`, the plotted indicator marker will be a
circle instead of a connected line segment (default).

`marker` sets the marker shape for scatter plots. Available options:
'circle', 'square', 'triangle', 'diamond', 'cross', 'x', 'star'.
Default is 'circle'.

`marker_size` sets the size of scatter plot markers. If None,
defaults to a size relative to the bar width.

Additional `*args` and `**kwargs` are passed to `func` and can
be used for parameters.

Expand Down Expand Up @@ -173,6 +181,7 @@ def _format_name(name: str) -> str:

value = _Indicator(value, name=name, plot=plot, overlay=overlay,
color=color, scatter=scatter,
marker=marker, marker_size=marker_size,
# _Indicator.s Series accessor uses this:
index=self.data.index)
self._indicators.append(value)
Expand Down Expand Up @@ -1240,10 +1249,13 @@ def __init__(self,
self._results: Optional[pd.Series] = None
self._finalize_trades = bool(finalize_trades)

def run(self, **kwargs) -> pd.Series:
def run(self, show_progress: bool = True, **kwargs) -> pd.Series:
"""
Run the backtest. Returns `pd.Series` with results and statistics.

'show_progress' : bool, default True
Whether to show the progress bar during backtest execution.

Keyword arguments are interpreted as strategy parameters.

>>> Backtest(GOOG, SmaCross).run()
Expand Down Expand Up @@ -1289,6 +1301,7 @@ def run(self, **kwargs) -> pd.Series:
begin on bar 201. The actual length of delay is equal to the lookback
period of the `Strategy.I` indicator which lags the most.
Obviously, this can affect results.

"""
data = _Data(self._data.copy(deep=False))
broker: _Broker = self._broker(data=data)
Expand All @@ -1308,8 +1321,10 @@ def run(self, **kwargs) -> pd.Series:
# np.nan >= 3 is not invalid; it's False.
with np.errstate(invalid='ignore'):

for i in _tqdm(range(start, len(self._data)), desc=self.run.__qualname__,
unit='bar', mininterval=2, miniters=100):
seq = range(start, len(self._data))
if show_progress:
seq = _tqdm(seq, desc=self.run.__qualname__, unit='bar', mininterval=2, miniters=100)
for i in seq:
# Prepare data and indicators for `next` call
data._set_length(i + 1)
for attr, indicator in indicator_attrs:
Expand Down
Loading