Skip to content

Commit b2ff83d

Browse files
committed
use variable annotation issue #161
1 parent 028f02d commit b2ff83d

File tree

2 files changed

+40
-30
lines changed

2 files changed

+40
-30
lines changed

backtesting/_util.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class _Array(np.ndarray):
4545
ndarray extended to supply .name and other arbitrary properties
4646
in ._opts dict.
4747
"""
48+
4849
def __new__(cls, array, *, name=None, **kwargs):
4950
obj = np.asarray(array).view(cls)
5051
obj.name = name or array.name
@@ -96,12 +97,13 @@ class _Data:
9697
and the returned "series" are _not_ `pd.Series` but `np.ndarray`
9798
for performance reasons.
9899
"""
100+
99101
def __init__(self, df: pd.DataFrame):
100102
self.__df = df
101103
self.__i = len(df)
102-
self.__pip = None # type: Optional[float]
103-
self.__cache = {} # type: Dict[str, _Array]
104-
self.__arrays = {} # type: Dict[str, _Array]
104+
self.__pip: Optional[float] = None
105+
self.__cache: Dict[str, _Array] = {}
106+
self.__arrays: Dict[str, _Array] = {}
105107
self._update()
106108

107109
def __getitem__(self, item):
@@ -141,8 +143,8 @@ def df(self) -> pd.DataFrame:
141143
@property
142144
def pip(self) -> float:
143145
if self.__pip is None:
144-
self.__pip = 10**-np.median([len(s.partition('.')[-1])
145-
for s in self.__arrays['Close'].astype(str)])
146+
self.__pip = 10 ** -np.median([len(s.partition('.')[-1])
147+
for s in self.__arrays['Close'].astype(str)])
146148
return self.__pip
147149

148150
def __get_array(self, key) -> _Array:

backtesting/backtesting.py

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
try:
3030
from tqdm.auto import tqdm as _tqdm
31+
3132
_tqdm = partial(_tqdm, leave=False)
3233
except ImportError:
3334
def _tqdm(seq, **_):
@@ -52,10 +53,11 @@ class Strategy(metaclass=ABCMeta):
5253
`backtesting.backtesting.Strategy.next` to define
5354
your own strategy.
5455
"""
56+
5557
def __init__(self, broker, data, params):
5658
self._indicators = []
57-
self._broker = broker # type: _Broker
58-
self._data = data # type: _Data
59+
self._broker: _Broker = broker
60+
self._data: _Data = data
5961
self._params = self._check_params(params)
6062

6163
def __repr__(self):
@@ -146,7 +148,7 @@ def init():
146148
raise ValueError(
147149
'Indicators must return (optionally a tuple of) numpy.arrays of same '
148150
'length as `data` (data shape: {}; indicator "{}" shape: {}, returned value: {})'
149-
.format(self._data.Close.shape, name, getattr(value, 'shape', ''), value))
151+
.format(self._data.Close.shape, name, getattr(value, 'shape', ''), value))
150152

151153
if plot and overlay is None and np.issubdtype(value.dtype, np.number):
152154
x = value / self._data.Close
@@ -282,6 +284,7 @@ class _Orders(tuple):
282284
"""
283285
TODO: remove this class. Only for deprecation.
284286
"""
287+
285288
def cancel(self):
286289
"""Cancel all non-contingent (i.e. SL/TP) orders."""
287290
for order in self:
@@ -309,6 +312,7 @@ class Position:
309312
if self.position:
310313
... # we have a position, either long or short
311314
"""
315+
312316
def __init__(self, broker: '_Broker'):
313317
self.__broker = broker
314318

@@ -373,6 +377,7 @@ class Order:
373377
[filled]: https://www.investopedia.com/terms/f/fill.asp
374378
[Good 'Til Canceled]: https://www.investopedia.com/terms/g/gtc.asp
375379
"""
380+
376381
def __init__(self, broker: '_Broker',
377382
size: float,
378383
limit_price: float = None,
@@ -507,15 +512,16 @@ class Trade:
507512
When an `Order` is filled, it results in an active `Trade`.
508513
Find active trades in `Strategy.trades` and closed, settled trades in `Strategy.closed_trades`.
509514
"""
515+
510516
def __init__(self, broker: '_Broker', size: int, entry_price: float, entry_bar):
511517
self.__broker = broker
512518
self.__size = size
513519
self.__entry_price = entry_price
514-
self.__exit_price = None # type: Optional[float]
515-
self.__entry_bar = entry_bar # type: int
516-
self.__exit_bar = None # type: Optional[int]
517-
self.__sl_order = None # type: Optional[Order]
518-
self.__tp_order = None # type: Optional[Order]
520+
self.__exit_price: Optional[float] = None
521+
self.__entry_bar: int = entry_bar
522+
self.__exit_bar: Optional[int] = None
523+
self.__sl_order: Optional[Order] = None
524+
self.__tp_order: Optional[Order] = None
519525

520526
def __repr__(self):
521527
return '<Trade size={} time={}-{} price={}-{} pl={:.0f}>'.format(
@@ -653,7 +659,7 @@ def __set_contingent(self, type, price):
653659
assert type in ('sl', 'tp')
654660
assert price is None or 0 < price < np.inf
655661
attr = '_{}__{}_order'.format(self.__class__.__qualname__, type)
656-
order = getattr(self, attr) # type: Order
662+
order: Order = getattr(self, attr)
657663
if order:
658664
order.cancel()
659665
if price:
@@ -668,7 +674,7 @@ def __init__(self, *, data, cash, commission, margin,
668674
assert 0 < cash, "cash shosuld be >0, is {}".format(cash)
669675
assert 0 <= commission < .1, "commission should be between 0-10%, is {}".format(commission)
670676
assert 0 < margin <= 1, "margin should be between 0 and 1, is {}".format(margin)
671-
self._data = data # type: _Data
677+
self._data: _Data = data
672678
self._cash = cash
673679
self._commission = commission
674680
self._leverage = 1 / margin
@@ -677,10 +683,10 @@ def __init__(self, *, data, cash, commission, margin,
677683
self._exclusive_orders = exclusive_orders
678684

679685
self._equity = np.tile(np.nan, len(index))
680-
self.orders = [] # type: List[Order]
681-
self.trades = [] # type: List[Trade]
686+
self.orders: List[Order] = []
687+
self.trades: List[Trade] = []
682688
self.position = Position(self)
683-
self.closed_trades = [] # type: List[Trade]
689+
self.closed_trades: List[Trade] = []
684690

685691
def __repr__(self):
686692
return '<Broker: {:.0f}{:+.1f} ({} trades)>'.format(
@@ -774,7 +780,8 @@ def _process_orders(self):
774780
reprocess_orders = False
775781

776782
# Process orders
777-
for order in list(self.orders): # type: Order
783+
order: Order
784+
for order in list(self.orders):
778785

779786
# Related SL/TP order was already removed
780787
if order not in self.orders:
@@ -967,6 +974,7 @@ class Backtest:
967974
instance, or `backtesting.backtesting.Backtest.optimize` to
968975
optimize it.
969976
"""
977+
970978
def __init__(self,
971979
data: pd.DataFrame,
972980
strategy: Type[Strategy],
@@ -1036,10 +1044,10 @@ def __init__(self,
10361044

10371045
# Convert index to datetime index
10381046
if (not data.index.is_all_dates and
1039-
not isinstance(data.index, pd.RangeIndex) and
1040-
# Numeric index with most large numbers
1041-
(data.index.is_numeric() and
1042-
(data.index > pd.Timestamp('1975').timestamp()).mean() > .8)):
1047+
not isinstance(data.index, pd.RangeIndex) and
1048+
# Numeric index with most large numbers
1049+
(data.index.is_numeric() and
1050+
(data.index > pd.Timestamp('1975').timestamp()).mean() > .8)):
10431051
try:
10441052
data.index = pd.to_datetime(data.index, infer_datetime_format=True)
10451053
except ValueError:
@@ -1071,7 +1079,7 @@ def __init__(self,
10711079
'but `pd.DateTimeIndex` is advised.',
10721080
stacklevel=2)
10731081

1074-
self._data = data # type: pd.DataFrame
1082+
self._data = data # type: pd.DataFrame
10751083
self._broker = partial(
10761084
_Broker, cash=cash, commission=commission, margin=margin,
10771085
trade_on_close=trade_on_close, hedging=hedging,
@@ -1118,8 +1126,8 @@ def run(self, **kwargs) -> pd.Series:
11181126
dtype: object
11191127
"""
11201128
data = _Data(self._data.copy(deep=False))
1121-
broker = self._broker(data=data) # type: _Broker
1122-
strategy = self._strategy(broker, data, kwargs) # type: Strategy
1129+
broker: _Broker = self._broker(data=data)
1130+
strategy: Strategy = self._strategy(broker, data, kwargs)
11231131

11241132
strategy.init()
11251133
data._update() # Strategy.init might have changed/added to data.df
@@ -1400,24 +1408,24 @@ def geometric_mean(x):
14001408
day_returns = equity_df['Equity'].resample('D').last().dropna().pct_change()
14011409
gmean_day_return = geometric_mean(day_returns)
14021410
annual_trading_days = (
1403-
365 if index.dayofweek.to_series().between(5, 6).mean() > 2/7 * .6 else
1411+
365 if index.dayofweek.to_series().between(5, 6).mean() > 2 / 7 * .6 else
14041412
252)
14051413

14061414
# Annualized return and risk metrics are computed based on the (mostly correct)
14071415
# assumption that the returns are compounded. See: https://dx.doi.org/10.2139/ssrn.3054517
14081416
# Our annualized return matches `empyrical.annual_return(day_returns)` whereas
14091417
# our risk doesn't; they use the simpler approach below.
1410-
annualized_return = (1 + gmean_day_return)**annual_trading_days - 1
1418+
annualized_return = (1 + gmean_day_return) ** annual_trading_days - 1
14111419
s.loc['Return (Ann.) [%]'] = annualized_return * 100
1412-
s.loc['Volatility (Ann.) [%]'] = np.sqrt((day_returns.var(ddof=1) + (1 + gmean_day_return)**2)**annual_trading_days - (1 + gmean_day_return)**(2*annual_trading_days)) * 100 # noqa: E501
1420+
s.loc['Volatility (Ann.) [%]'] = np.sqrt((day_returns.var(ddof=1) + (1 + gmean_day_return) ** 2) ** annual_trading_days - (1 + gmean_day_return) ** (2 * annual_trading_days)) * 100 # noqa: E501
14131421
# s.loc['Return (Ann.) [%]'] = gmean_day_return * annual_trading_days * 100
14141422
# s.loc['Risk (Ann.) [%]'] = day_returns.std(ddof=1) * np.sqrt(annual_trading_days) * 100
14151423

14161424
# Our Sharpe mismatches `empyrical.sharpe_ratio()` because they use arithmetic mean return
14171425
# and simple standard deviation
14181426
s.loc['Sharpe Ratio'] = np.clip(s.loc['Return (Ann.) [%]'] / (s.loc['Volatility (Ann.) [%]'] or np.nan), 0, np.inf) # noqa: E501
14191427
# Our Sortino mismatches `empyrical.sortino_ratio()` because they use arithmetic mean return
1420-
s.loc['Sortino Ratio'] = np.clip(annualized_return / (np.sqrt(np.mean(day_returns.clip(-np.inf, 0)**2)) * np.sqrt(annual_trading_days)), 0, np.inf) # noqa: E501
1428+
s.loc['Sortino Ratio'] = np.clip(annualized_return / (np.sqrt(np.mean(day_returns.clip(-np.inf, 0) ** 2)) * np.sqrt(annual_trading_days)), 0, np.inf) # noqa: E501
14211429
max_dd = -np.nan_to_num(dd.max())
14221430
s.loc['Calmar Ratio'] = np.clip(annualized_return / (-max_dd or np.nan), 0, np.inf)
14231431
s.loc['Max. Drawdown [%]'] = max_dd * 100

0 commit comments

Comments
 (0)