28
28
29
29
try :
30
30
from tqdm .auto import tqdm as _tqdm
31
+
31
32
_tqdm = partial (_tqdm , leave = False )
32
33
except ImportError :
33
34
def _tqdm (seq , ** _ ):
@@ -52,10 +53,11 @@ class Strategy(metaclass=ABCMeta):
52
53
`backtesting.backtesting.Strategy.next` to define
53
54
your own strategy.
54
55
"""
56
+
55
57
def __init__ (self , broker , data , params ):
56
58
self ._indicators = []
57
- self ._broker = broker # type: _Broker
58
- self ._data = data # type: _Data
59
+ self ._broker : _Broker = broker
60
+ self ._data : _Data = data
59
61
self ._params = self ._check_params (params )
60
62
61
63
def __repr__ (self ):
@@ -146,7 +148,7 @@ def init():
146
148
raise ValueError (
147
149
'Indicators must return (optionally a tuple of) numpy.arrays of same '
148
150
'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 ))
150
152
151
153
if plot and overlay is None and np .issubdtype (value .dtype , np .number ):
152
154
x = value / self ._data .Close
@@ -282,6 +284,7 @@ class _Orders(tuple):
282
284
"""
283
285
TODO: remove this class. Only for deprecation.
284
286
"""
287
+
285
288
def cancel (self ):
286
289
"""Cancel all non-contingent (i.e. SL/TP) orders."""
287
290
for order in self :
@@ -309,6 +312,7 @@ class Position:
309
312
if self.position:
310
313
... # we have a position, either long or short
311
314
"""
315
+
312
316
def __init__ (self , broker : '_Broker' ):
313
317
self .__broker = broker
314
318
@@ -373,6 +377,7 @@ class Order:
373
377
[filled]: https://www.investopedia.com/terms/f/fill.asp
374
378
[Good 'Til Canceled]: https://www.investopedia.com/terms/g/gtc.asp
375
379
"""
380
+
376
381
def __init__ (self , broker : '_Broker' ,
377
382
size : float ,
378
383
limit_price : float = None ,
@@ -507,15 +512,16 @@ class Trade:
507
512
When an `Order` is filled, it results in an active `Trade`.
508
513
Find active trades in `Strategy.trades` and closed, settled trades in `Strategy.closed_trades`.
509
514
"""
515
+
510
516
def __init__ (self , broker : '_Broker' , size : int , entry_price : float , entry_bar ):
511
517
self .__broker = broker
512
518
self .__size = size
513
519
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
519
525
520
526
def __repr__ (self ):
521
527
return '<Trade size={} time={}-{} price={}-{} pl={:.0f}>' .format (
@@ -653,7 +659,7 @@ def __set_contingent(self, type, price):
653
659
assert type in ('sl' , 'tp' )
654
660
assert price is None or 0 < price < np .inf
655
661
attr = '_{}__{}_order' .format (self .__class__ .__qualname__ , type )
656
- order = getattr (self , attr ) # type: Order
662
+ order : Order = getattr (self , attr )
657
663
if order :
658
664
order .cancel ()
659
665
if price :
@@ -668,7 +674,7 @@ def __init__(self, *, data, cash, commission, margin,
668
674
assert 0 < cash , "cash shosuld be >0, is {}" .format (cash )
669
675
assert 0 <= commission < .1 , "commission should be between 0-10%, is {}" .format (commission )
670
676
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
672
678
self ._cash = cash
673
679
self ._commission = commission
674
680
self ._leverage = 1 / margin
@@ -677,10 +683,10 @@ def __init__(self, *, data, cash, commission, margin,
677
683
self ._exclusive_orders = exclusive_orders
678
684
679
685
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 ] = [ ]
682
688
self .position = Position (self )
683
- self .closed_trades = [] # type: List[Trade ]
689
+ self .closed_trades : List [ Trade ] = [ ]
684
690
685
691
def __repr__ (self ):
686
692
return '<Broker: {:.0f}{:+.1f} ({} trades)>' .format (
@@ -774,7 +780,8 @@ def _process_orders(self):
774
780
reprocess_orders = False
775
781
776
782
# Process orders
777
- for order in list (self .orders ): # type: Order
783
+ order : Order
784
+ for order in list (self .orders ):
778
785
779
786
# Related SL/TP order was already removed
780
787
if order not in self .orders :
@@ -967,6 +974,7 @@ class Backtest:
967
974
instance, or `backtesting.backtesting.Backtest.optimize` to
968
975
optimize it.
969
976
"""
977
+
970
978
def __init__ (self ,
971
979
data : pd .DataFrame ,
972
980
strategy : Type [Strategy ],
@@ -1036,10 +1044,10 @@ def __init__(self,
1036
1044
1037
1045
# Convert index to datetime index
1038
1046
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 )):
1043
1051
try :
1044
1052
data .index = pd .to_datetime (data .index , infer_datetime_format = True )
1045
1053
except ValueError :
@@ -1071,7 +1079,7 @@ def __init__(self,
1071
1079
'but `pd.DateTimeIndex` is advised.' ,
1072
1080
stacklevel = 2 )
1073
1081
1074
- self ._data = data # type: pd.DataFrame
1082
+ self ._data = data # type: pd.DataFrame
1075
1083
self ._broker = partial (
1076
1084
_Broker , cash = cash , commission = commission , margin = margin ,
1077
1085
trade_on_close = trade_on_close , hedging = hedging ,
@@ -1118,8 +1126,8 @@ def run(self, **kwargs) -> pd.Series:
1118
1126
dtype: object
1119
1127
"""
1120
1128
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 )
1123
1131
1124
1132
strategy .init ()
1125
1133
data ._update () # Strategy.init might have changed/added to data.df
@@ -1400,24 +1408,24 @@ def geometric_mean(x):
1400
1408
day_returns = equity_df ['Equity' ].resample ('D' ).last ().dropna ().pct_change ()
1401
1409
gmean_day_return = geometric_mean (day_returns )
1402
1410
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
1404
1412
252 )
1405
1413
1406
1414
# Annualized return and risk metrics are computed based on the (mostly correct)
1407
1415
# assumption that the returns are compounded. See: https://dx.doi.org/10.2139/ssrn.3054517
1408
1416
# Our annualized return matches `empyrical.annual_return(day_returns)` whereas
1409
1417
# 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
1411
1419
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
1413
1421
# s.loc['Return (Ann.) [%]'] = gmean_day_return * annual_trading_days * 100
1414
1422
# s.loc['Risk (Ann.) [%]'] = day_returns.std(ddof=1) * np.sqrt(annual_trading_days) * 100
1415
1423
1416
1424
# Our Sharpe mismatches `empyrical.sharpe_ratio()` because they use arithmetic mean return
1417
1425
# and simple standard deviation
1418
1426
s .loc ['Sharpe Ratio' ] = np .clip (s .loc ['Return (Ann.) [%]' ] / (s .loc ['Volatility (Ann.) [%]' ] or np .nan ), 0 , np .inf ) # noqa: E501
1419
1427
# 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
1421
1429
max_dd = - np .nan_to_num (dd .max ())
1422
1430
s .loc ['Calmar Ratio' ] = np .clip (annualized_return / (- max_dd or np .nan ), 0 , np .inf )
1423
1431
s .loc ['Max. Drawdown [%]' ] = max_dd * 100
0 commit comments