Skip to content
Open
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
25 changes: 18 additions & 7 deletions backtesting/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from itertools import chain, product, repeat
from math import copysign
from numbers import Number
from difflib import get_close_matches
from typing import Callable, List, Optional, Sequence, Tuple, Type, Union

import numpy as np
Expand Down Expand Up @@ -64,10 +65,12 @@ def __str__(self):
def _check_params(self, params):
for k, v in params.items():
if not hasattr(self, k):
suggestions = get_close_matches(k, [a for a in dir(self) if not a.startswith('_')], n=3)
hint = f" Did you mean: {', '.join(suggestions)}?" if suggestions else ""
raise AttributeError(
f"Strategy '{self.__class__.__name__}' is missing parameter '{k}'."
f"Strategy '{self.__class__.__name__}' is missing parameter '{k}'. "
"Strategy class should define parameters as class variables before they "
"can be optimized or run with.")
"can be optimized or run with." + hint)
setattr(self, k, v)
return params

Expand Down Expand Up @@ -139,6 +142,8 @@ def _format_name(name: str) -> str:

try:
value = func(*args, **kwargs)
if isinstance(value, pd.Series):
value = value.to_numpy()
except Exception as e:
raise RuntimeError(f'Indicator "{name}" error. See traceback above.') from e

Expand Down Expand Up @@ -337,7 +342,7 @@ def __getattr__(self, item):
removed_attrs = ('entry', 'set_entry', 'is_long', 'is_short',
'sl', 'tp', 'set_sl', 'set_tp')
if item in removed_attrs:
raise AttributeError(f'Strategy.orders.{"/.".join(removed_attrs)} were removed in'
raise AttributeError(f'Strategy.orders.{"/.".join(removed_attrs)} were removed in '
'Backtesting 0.2.0. '
'Use `Order` API instead. See docs.')
raise AttributeError(f"'tuple' object has no attribute {item!r}")
Expand Down Expand Up @@ -448,7 +453,7 @@ def __repr__(self):
('tp', self.__tp_price),
('contingent', self.is_contingent),
('tag', self.__tag),
) if value is not None)) # noqa: E126
) if (value is not None and (not isinstance(value, bool) or value)))) # noqa: E126

def cancel(self):
"""Cancel the order."""
Expand Down Expand Up @@ -964,8 +969,10 @@ def _process_orders(self):
# Not enough cash/margin even for a single unit
if not size:
warnings.warn(
f'time={self._i}: Broker canceled the relative-sized '
f'order due to insufficient margin.', category=UserWarning)
f'({data.index[self._i]}) broker canceled the relative-sized order '
f'{order} due to insufficient margin '
f'(equity={self.equity:.2f}, margin_available={self.margin_available:.2f}).',
category=UserWarning)
# XXX: The order is canceled by the broker?
self.orders.remove(order)
continue
Expand Down Expand Up @@ -998,6 +1005,10 @@ def _process_orders(self):
# If we don't have enough liquidity to cover for the order, the broker CANCELS it
if abs(need_size) * adjusted_price_plus_commission > \
self.margin_available * self._leverage:
warnings.warn(
f'({data.index[self._i]}) broker canceled order {order} due to insufficient margin '
f'(equity={self.equity:.2f}, margin_available={self.margin_available:.2f}).',
category=UserWarning)
self.orders.remove(order)
continue

Expand Down Expand Up @@ -1151,7 +1162,7 @@ class Backtest:

`margin` is the required margin (ratio) of a leveraged account.
No difference is made between initial and maintenance margins.
To run the backtest using e.g. 50:1 leverge that your broker allows,
To run the backtest using e.g. 50:1 leverage that your broker allows,
set margin to `0.02` (1 / leverage).

If `trade_on_close` is `True`, market orders will be filled
Expand Down