diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index 39fb80f1..c1dc1c93 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -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 @@ -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 @@ -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 @@ -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}") @@ -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.""" @@ -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 @@ -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 @@ -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