backtrader-next 2.1.1__py3-none-any.whl → 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,15 +7,13 @@ import numpy as np
7
7
  import pandas as pd
8
8
  from pandas import DataFrame as df
9
9
  from numbers import Number
10
- from typing import Dict, List, Optional, Sequence, Union, cast
10
+ from typing import Union
11
11
  import math
12
- # from math import copysign
13
12
 
14
13
 
15
14
  class Eq(bt.Analyzer):
16
- '''This analyzer calculates trading system drawdowns stats such as drawdown
17
- values in %s and in dollars, max drawdown in %s and in dollars, drawdown
18
- length and drawdown max length
15
+ '''This analyzer calculates comprehensive trading system performance statistics
16
+ including equity curves, drawdown analysis, returns, risk metrics, and trade statistics.
19
17
 
20
18
  Params:
21
19
 
@@ -28,20 +26,42 @@ class Eq(bt.Analyzer):
28
26
 
29
27
  Set it to ``True`` or ``False`` for a specific behavior
30
28
 
29
+ - ``data`` (default: ``None``)
30
+
31
+ Specific data feed to analyze. If ``None``, all data is analyzed.
32
+
33
+ - ``cash`` (default: ``True``)
34
+
35
+ Whether to include cash information in equity tracking.
36
+
31
37
  Methods:
32
38
 
33
- - ``get_analysis``
39
+ - ``compute_stats(risk_free_rate=0.0)``
40
+
41
+ Returns a pandas Series with comprehensive trading statistics including:
42
+
43
+ - Equity metrics: Start, End, Duration, Peak equity
44
+ - Returns: Cumulative return %, Annualized return, CAGR
45
+ - Risk metrics: Volatility (Ann.), Sharpe Ratio, Sortino Ratio, Smart Sharpe Ratio
46
+ - Drawdown stats: Max Drawdown %, Avg Drawdown %, Drawdown Duration
47
+ - Trade statistics: Win Rate, Best/Worst/Avg Trade, # of Trades
48
+ - Performance ratios: Calmar Ratio, VWR Ratio, Profit Factor, SQN, Kelly Criterion
49
+
50
+ - ``gen_eq()``
51
+
52
+ Returns equity curve as a pandas DataFrame with datetime index.
53
+
54
+ - ``gen_eq_dd()``
55
+
56
+ Returns equity and drawdown data as a pandas DataFrame.
34
57
 
35
- Returns a dictionary (with . notation support and subdctionaries) with
36
- drawdown stats as values, the following keys/attributes are available:
58
+ - ``gen_trades(data_name=None, pct=False)``
37
59
 
38
- - ``drawdown`` - drawdown value in 0.xx %
39
- - ``moneydown`` - drawdown value in monetary units
40
- - ``len`` - drawdown length
60
+ Returns trade-level statistics as a pandas DataFrame.
41
61
 
42
- - ``max.drawdown`` - max drawdown value in 0.xx %
43
- - ``max.moneydown`` - max drawdown value in monetary units
44
- - ``max.len`` - max drawdown length
62
+ - ``gen_orders(data_name=None)``
63
+
64
+ Returns order history as a pandas DataFrame.
45
65
  '''
46
66
 
47
67
  params = (
@@ -136,7 +156,6 @@ class Eq(bt.Analyzer):
136
156
  def gen_trades(self, data_name=None, pct=False) -> 'DataFrame':
137
157
  if self.trades_df is None:
138
158
  self.trades_df = df.from_records(self.trades, columns=self.trades_header)
139
- # self.trades_df = df.from_records(self.trades, index=self.trades_header[0], columns=self.trades_header)
140
159
  if data_name is None:
141
160
  rdf = self.trades_df.copy()
142
161
  else:
@@ -211,7 +230,6 @@ class Eq(bt.Analyzer):
211
230
  # our risk doesn't; they use the simpler approach below.
212
231
  annualized_return = (1 + gmean_day_return) ** annual_trading_days - 1
213
232
  s.loc['Return (Ann.) [%]'] = round(annualized_return * 100, 4)
214
- # s.loc['Risk (Ann.) [%]'] = day_returns.std(ddof=1) * np.sqrt(annual_trading_days) * 100
215
233
  s.loc['Volatility (Ann.) [%]'] = volatility = round(day_returns.std(ddof=1) * np.sqrt(annual_trading_days) * 100, 4)
216
234
 
217
235
  # CAGR from quantstats
@@ -286,10 +304,6 @@ class Eq(bt.Analyzer):
286
304
  else:
287
305
  s.loc['Kelly Criterion [%]'] = np.nan
288
306
 
289
- # s.loc['_strategy'] = strategy_instance
290
- # s.loc['_equity_curve'] = equity_df
291
- # s.loc['_trades'] = trades_df
292
- #
293
307
  return s
294
308
 
295
309
 
@@ -322,39 +336,6 @@ def geometric_mean(returns: pd.Series) -> float:
322
336
  return np.exp(np.log(returns).sum() / (len(returns) or np.nan)) - 1
323
337
 
324
338
 
325
- def calc_vwr0(eq_days: np.array, sdev_max=2.0, tau=0.20) -> float:
326
- eq = eq_days #.to_numpy()
327
- eq_0 = eq_days.shift().to_numpy()
328
-
329
- try:
330
- nlrtot = eq[-1] / eq[0]
331
- except ZeroDivisionError:
332
- rtot = float('-inf')
333
- else:
334
- if nlrtot <= 0.0:
335
- rtot = float('-inf')
336
- else:
337
- rtot = math.log(nlrtot)
338
-
339
- ravg = rtot / len(eq)
340
- rnorm = math.expm1(ravg * 252)
341
- rnorm100 = rnorm * 100.0
342
-
343
- dts = []
344
- for n, zip_data in enumerate(zip(eq_0, eq), 0):
345
- eq0, eq1 = zip_data
346
- if (n > 0):
347
- _v = (eq0 * math.exp(ravg * n))
348
- if _v != 0:
349
- dt = eq1 / (eq0 * math.exp(ravg * n)) - 1.0
350
- dts.append(dt)
351
- else:
352
- dts.append(0.0)
353
-
354
- sdev_p = np.array(dts).std(ddof=True)
355
- vwr = rnorm100 * (1.0 - pow(sdev_p / sdev_max, tau))
356
- return vwr
357
-
358
339
  # calc VariabilityWeightedReturn
359
340
  # See:
360
341
  # - https://www.crystalbull.com/sharpe-ratio-better-with-log-returns/
backtrader_next/order.py CHANGED
@@ -283,6 +283,21 @@ class OrderBase(with_metaclass(MetaParams, object)):
283
283
  self._plimit = val
284
284
 
285
285
  plimit = property(_getplimit, _setplimit)
286
+
287
+ @property
288
+ def owner(self):
289
+ if self._owner_classname and not self.p.owner:
290
+ class_name = self._owner_classname
291
+ for s in self.broker.cerebro.runningstrategies:
292
+ if s.__class__.__name__ == class_name:
293
+ self.p.owner = s
294
+ self._owner_classname = None
295
+ break
296
+ return self.p.owner
297
+
298
+ @owner.setter
299
+ def owner(self, value):
300
+ self.p.owner = value
286
301
 
287
302
  def __getattr__(self, name):
288
303
  # Return attr from params if not found in order
@@ -323,6 +338,9 @@ class OrderBase(with_metaclass(MetaParams, object)):
323
338
  self.info = AutoOrderedDict()
324
339
  self.comminfo = None
325
340
  self.triggered = False
341
+ self.parent_ref = None
342
+ self._owner_classname = None
343
+ self._restored = False
326
344
 
327
345
  self._active = self.parent is None
328
346
  self.status = Order.Created
@@ -337,10 +355,10 @@ class OrderBase(with_metaclass(MetaParams, object)):
337
355
 
338
356
  # Set a reference price if price is not set using
339
357
  # the close price
340
- pclose = self.data.close[0] if not self.p.simulated else self.price
358
+ pclose = self.data.close[0] if not self.p.simulated and not self._restored else self.price
341
359
  price = pclose if not self.price and not self.pricelimit else self.price
342
360
 
343
- dcreated = self.data.datetime[0] if not self.p.simulated else 0.0
361
+ dcreated = self.data.datetime[0] if not self.p.simulated and not self._restored else 0.0
344
362
  self.created = OrderData(dt=dcreated,
345
363
  size=self.size,
346
364
  price=price,
@@ -382,7 +400,7 @@ class OrderBase(with_metaclass(MetaParams, object)):
382
400
  else: # assume float
383
401
  valid = self.data.datetime[0] + self.valid
384
402
 
385
- if not self.p.simulated:
403
+ if not self.p.simulated and not self._restored:
386
404
  # provisional end-of-session
387
405
  # get next session end
388
406
  dtime = self.data.datetime.datetime(0)
@@ -655,52 +673,80 @@ class Order(OrderBase):
655
673
  'price': v.price,
656
674
  'pricelimit': v.pricelimit,
657
675
  'remsize': v.remsize,
676
+ 'pclose': v.pclose,
658
677
  'trailamount': v.trailamount,
659
678
  'trailpercent': v.trailpercent,
679
+ 'value': v.value,
680
+ 'comm': v.comm,
681
+ 'margin': v.margin,
682
+ 'pnl': v.pnl,
683
+ 'psize': v.psize,
684
+ 'pprice': v.pprice,
660
685
  }
661
- elif k in ('broker', 'data'):
686
+ elif k in ('broker', 'data','params','comminfo','info',):
662
687
  # handled below
663
688
  pass
664
- elif k in ('parent',):
665
- odict[k] = v.ref if v else None
689
+ elif k == 'parent':
690
+ odict['parent'] = None
691
+ if v is not None:
692
+ odict['parent_ref'] = v.ref
666
693
  # handled below
667
- pass
694
+ elif k in ('p',):
695
+ odict['params'] = vars(v).copy()
696
+ odict['params']['_owner_classname'] = v.owner.__class__.__qualname__ if v.owner else None
697
+ odict['params']['owner'] = None
698
+ odict['params']['data'] = v.data._name if v.data else None
668
699
  else:
669
700
  odict[k] = v
701
+ odict['ordtype'] = self.ordtype
670
702
  return odict
671
703
 
672
704
  @classmethod
673
- def from_dict(cls, odict):
705
+ def from_dict(cls, params, odict):
674
706
  '''Loads the order data from a dictionary representation'''
707
+ odict['_restored'] = True
675
708
  ordtype = odict.get('ordtype', None)
676
709
  ord = None
677
710
  if ordtype is None:
678
- return None
711
+ ord = Order(**params)
679
712
  if ordtype == cls.Buy:
680
- ord = BuyOrder()
713
+ ord = BuyOrder(**params)
681
714
  elif ordtype == cls.Sell:
682
- ord = SellOrder()
715
+ ord = SellOrder(**params)
683
716
  else:
684
- ord = Order()
717
+ ord = Order(**params)
685
718
 
686
719
  for key, val in odict.items():
687
- if key == 'created' or key == 'executed':
688
- ord.created = OrderData(
720
+ if key in ('created', 'executed'):
721
+ odata = OrderData(
689
722
  dt=val.get('dt', None),
690
723
  size=val.get('size', 0),
691
724
  price=val.get('price', 0.0),
692
725
  pricelimit=val.get('pricelimit', 0.0),
726
+ remsize=val.get('remsize', 0),
727
+ pclose=val.get('pclose', 0.0),
693
728
  trailamount=val.get('trailamount', 0.0),
694
729
  trailpercent=val.get('trailpercent', 0.0),
695
730
  )
731
+ odata.value = val.get('value', 0.0)
732
+ odata.comm = val.get('comm', 0.0)
733
+ odata.margin = val.get('margin', None)
734
+ odata.pnl = val.get('pnl', 0.0)
735
+ odata.psize = val.get('psize', 0)
736
+ odata.pprice = val.get('pprice', 0.0)
737
+ if key == 'created':
738
+ ord.created = odata
739
+ else:
740
+ ord.executed = odata
741
+ elif key in ('ordtype', 'params'):
742
+ pass # already handled
696
743
  elif key == 'info':
697
744
  for ik, iv in val.items():
698
745
  ord.info[ik] = iv
699
746
  elif key == 'comminfo':
700
- ord.comminfo = val #?? string representation only
747
+ ord.comminfo = None
701
748
  elif key == 'parent':
702
- ref = val
703
- ord.parent = None #??TODO
749
+ ord.parent = None
704
750
  elif hasattr(ord, key):
705
751
  setattr(ord, key, val)
706
752
  return ord
@@ -22,6 +22,6 @@ from __future__ import (absolute_import, division, print_function,
22
22
  unicode_literals)
23
23
 
24
24
 
25
- __version__ = '2.1.0'
25
+ __version__ = '2.2.0'
26
26
 
27
27
  __btversion__ = tuple(int(x) for x in __version__.split('.'))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backtrader_next
3
- Version: 2.1.1
3
+ Version: 2.2.0
4
4
  Summary: Live Trading and backtesting platform in Python
5
5
  Project-URL: Homepage, https://github.com/smalinin/backtrader_next
6
6
  Project-URL: Source, https://github.com/smalinin/backtrader_next
@@ -37,6 +37,7 @@ Live Trading and backtesting platform written in Python.
37
37
  ```
38
38
  pip install backtrader-next
39
39
  ```
40
+
40
41
  ## History
41
42
  Package is based on [backtrader](https://github.com/mementum/backtrader)
42
43
 
@@ -51,6 +52,21 @@ Changes:
51
52
  - Detailed results
52
53
  - Interactive visualizations
53
54
 
55
+ ## Performance
56
+
57
+ Performance comparison using the [perf_compare](https://github.com/smalinin/backtrader_next/tree/master/examples/3_perf_compare) benchmark.
58
+
59
+ - **Backtrader-next** using an optimized **PandasData** feed
60
+ - [Backtrader](https://github.com/mementum/backtrader) with **PandasData** feed
61
+ - [Backtesting.py](https://github.com/kernc/backtesting.py)
62
+
63
+ | Framework | Execution Time | Relative Speed |
64
+ |---|---|---|
65
+ | Backtesting | 2.95 sec | 14.3x faster than Backtrader |
66
+ | Backtrader-next | 12.33 sec | 3.4x faster than Backtrader |
67
+ | Backtrader | 42.25 sec | Baseline |
68
+
69
+
54
70
  ## Here a snippet of a Simple Moving Average CrossOver.
55
71
  ```python
56
72
  import pandas as pd
@@ -17,7 +17,7 @@ backtrader_next/lineseries.py,sha256=-aNlK6FiOm47-cfyH3oIUDI3awsyEmbyAyAg_o-rETA
17
17
  backtrader_next/mathsupport.py,sha256=jc2fTjaf3dWzQ4ZV-ypvVPrHnKnoJbrWLHd-pIhdQ2w,1923
18
18
  backtrader_next/metabase.py,sha256=1g18zfwPOQQlL0MVEYI2pQ-W-FQvfJpG071a_Xbn-N4,11136
19
19
  backtrader_next/observer.py,sha256=xlR7JCYgEcZuwpgLbORPVccRD6cA5RhlkXAfKjVQjdY,2255
20
- backtrader_next/order.py,sha256=IYyHAZzia11jZ40D6o2ZoJdU_vzEnUL2ErgOoLOrS2E,24896
20
+ backtrader_next/order.py,sha256=H2pfRnxDwD9eOq0HdirDBKkzaQ3D_VW8axA5YcSMazk,26866
21
21
  backtrader_next/position.py,sha256=odBPNNkWVRHZnNY03SqP0TSlhP1I8xoCMHJCuS2rH7E,7445
22
22
  backtrader_next/resamplerfilter.py,sha256=z0hqTqbj4nj7gWQPR9qtBk9Iqz8wcqgF3iqL04cMJdA,26141
23
23
  backtrader_next/signal.py,sha256=WwhQUu9L35209K3NEv343dvd57-gBGow1yN9VbmvqPM,1899
@@ -28,13 +28,13 @@ backtrader_next/talib.py,sha256=Q9t2V-jgp-7I9vvPg2BuCJi73GOa7B2LfMrAygPi2nc,9100
28
28
  backtrader_next/timer.py,sha256=WrY5dpyXpGUOEeOtr_3pjiOldQyiUcXQTBpl8Jd2QCY,7578
29
29
  backtrader_next/trade.py,sha256=3AABrbLCxsMubZfenHyONKDCY6j3e-d7bk0ejQPvD8g,11633
30
30
  backtrader_next/tradingcal.py,sha256=X4VQ0dojoKgWoOVTd8aYTyvZYiFZV1TALrQ9sWzb9Rg,9903
31
- backtrader_next/version.py,sha256=1UOF0BNyEf9cJp15SoIPSkFlFw6Q2AeTdv6SbRb2SmQ,1105
31
+ backtrader_next/version.py,sha256=7T7z-LB2VTNCmy6PwRuT-O8G4FwDO4Wti4A4VS9p7PU,1105
32
32
  backtrader_next/writer.py,sha256=eDyz3xRsDp1i8WIZrXou6MZ80QHpYBzkf_DAqPGOcAw,7712
33
33
  backtrader_next/analyzers/__init__.py,sha256=krHAQhnqyHj0MKGyTIDNqXqqVUTX7dnQeAB1eRwcD9I,1568
34
34
  backtrader_next/analyzers/annualreturn.py,sha256=iIuQAE0nacVEPMyc1JUqIsfkWOG_8kwW104FlXvYd0s,2784
35
35
  backtrader_next/analyzers/calmar.py,sha256=eyfIxodGigJ_BgAhDKC8R9SctGsdUMK9K0WagFVviTM,3826
36
36
  backtrader_next/analyzers/drawdown.py,sha256=cHE5_SMuxYQN7WXDQ9SOyEWZcXRWUV_1QJuqHUcKV1w,6523
37
- backtrader_next/analyzers/eq.py,sha256=vnbvqYHIAzMqca7UhLy8TY10ze9lvt98yDUxqZ2qUE4,15877
37
+ backtrader_next/analyzers/eq.py,sha256=SKd-0jNiBXTnelocjewaH-bGoe5WfG-OWYrH4fiSiho,15245
38
38
  backtrader_next/analyzers/leverage.py,sha256=XjEp6AvSJuALYyIFCLX9U1nZohJ9KmOQmHTxIT4myLY,2402
39
39
  backtrader_next/analyzers/logreturnsrolling.py,sha256=DJimm1ywT2moXVgu27zB-hOrtyA5s1hkQ_et5PYlCwU,5025
40
40
  backtrader_next/analyzers/periodstats.py,sha256=-i1_wTtsLn-pBNDBpYQSdyx-2zxFmsGhaczTSgW9N0g,3570
@@ -201,7 +201,7 @@ backtrader_next/utils/dateintern.py,sha256=VcFWhMw_NazuVMHSvSueNgMxRqiEgu5YmRnz5
201
201
  backtrader_next/utils/flushfile.py,sha256=ekrNdFCs0j4HwRRxho-Fnl0XZBCrpd0uHyq02kV2wjE,1588
202
202
  backtrader_next/utils/ordereddefaultdict.py,sha256=8y7FDnqI1mTcr1NwVfgqsaK-42XwC76Mu_iYRVvLmLM,2091
203
203
  backtrader_next/utils/py3.py,sha256=hSoodWo9-eUXzUCcLoQWPtTR51QxD-m01QzZNcuK9A8,2416
204
- backtrader_next-2.1.1.dist-info/METADATA,sha256=8E6A61lvfRKsmpXjTGpX0LBGT17zw8PQC-Xub-aLCXQ,10774
205
- backtrader_next-2.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
206
- backtrader_next-2.1.1.dist-info/licenses/LICENSE,sha256=4cCtcomD2KVzNeUs8QZPGv_R1FQXPYzr0-2LSnK0hwQ,35121
207
- backtrader_next-2.1.1.dist-info/RECORD,,
204
+ backtrader_next-2.2.0.dist-info/METADATA,sha256=YIWlou26cB6I2rGP2jmH9AsF7EJM4cE9gBhKkhceORE,11359
205
+ backtrader_next-2.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
206
+ backtrader_next-2.2.0.dist-info/licenses/LICENSE,sha256=4cCtcomD2KVzNeUs8QZPGv_R1FQXPYzr0-2LSnK0hwQ,35121
207
+ backtrader_next-2.2.0.dist-info/RECORD,,