Navigation

    Backtrader Community

    • Register
    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    1. Home
    2. soulmachine
    For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/
    S
    • Profile
    • Following 2
    • Followers 0
    • Topics 12
    • Posts 37
    • Best 14
    • Groups 0

    soulmachine

    @soulmachine

    24
    Reputation
    27
    Profile views
    37
    Posts
    0
    Followers
    2
    Following
    Joined Last Online
    Location Mountain View, CA, USA

    soulmachine Unfollow Follow

    Best posts made by soulmachine

    • Share my customized commission scheme for cryptocurrencies

      I wrote two customized commission scheme for cryptocurrencies, one for spot market and the other for contracts:

      class CryptoSpotCommissionInfo(bt.CommissionInfo):
          '''Commission scheme for cryptocurrency spot market.
          
              Required Args:
                  commission: commission fee in percentage, between 0.0 and 1.0
          '''
          params = (
              ('stocklike', True),
              ('commtype', bt.CommInfoBase.COMM_PERC), # apply % commission
          )
      
          def __init__(self):
              assert abs(self.p.commission) < 1.0 # commission is a percentage
              assert self.p.mult == 1.0
              assert self.p.margin is None
              assert self.p.commtype == bt.CommInfoBase.COMM_PERC
              assert self.p.stocklike
              assert self.p.percabs
              assert self.p.leverage == 1.0
              assert self.p.automargin == False
      
              super().__init__()
      
          def getsize(self, price, cash):
              '''Support fractional size.
              
                  More details at https://www.backtrader.com/blog/posts/2019-08-29-fractional-sizes/fractional-sizes/.
              '''
              return self.p.leverage * (cash / price)
      
      class CryptoContractCommissionInfo(bt.CommissionInfo):
          '''Commission scheme for cryptocurrency contracts.
          
              Including futures contracts and perpetual swap contracts.
              
              Required Args:
                  commission: commission fee in percentage, between 0.0 and 1.0
                  mult: leverage, for example 10 means 10x leverage
          '''
          params = (
              ('stocklike', False),
              ('commtype', bt.CommInfoBase.COMM_PERC), # apply % commission
          )
          
          def __init__(self):
              assert abs(self.p.commission) < 1.0 # commission is a percentage
              assert self.p.mult > 1.0
              assert self.p.margin is None
              assert self.p.commtype == bt.CommInfoBase.COMM_PERC
              assert not self.p.stocklike
              assert self.p.percabs
              assert self.p.leverage == 1.0
              self.p.automargin = 1 / self.p.mult
      
              super().__init__()
      
          def getsize(self, price, cash):
              '''Support fractional size.
              
                  More details at https://www.backtrader.com/blog/posts/2019-08-29-fractional-sizes/fractional-sizes/.
              '''
              return self.p.leverage * (cash / price)
      
          def _getcommission(self, size, price, pseudoexec):
              '''Percentage based commission fee.
              
                  More details at https://www.backtrader.com/docu/user-defined-commissions/commission-schemes-subclassing/.
              '''
              return abs(size) * self.p.commission * price * self.p.mult
      

      Hope it helps!

      posted in General Discussion
      S
      soulmachine
    • RE: How to initialize bt.analyzers.SharpeRatio?

      After reading the source code I think I've figured out.

      timeframe and compression are used at analyzers/sharpe.py#L153:

      returns = list(itervalues(self.timereturn.get_analysis()))
      
      • timeframe=bt.TimeFrame.Days, compression=1, means TimeReturn will take a snapshot on the equity curve per day
      • timeframe=bt.TimeFrame.Minutes, compression=24*60, means TimeReturn will take a snapshot on the equity curve per 24*60 minutes, which is the same as a day

      returns is a list of (timestamp, percentage), sharpe ratio is calculated on top of this list.

      timeframe, compression --> TimeReturn -->returns --> sharpe_ratio
      

      The factor is used to convert riskfreerate to rate, which used at analyzers/sharpe.py#L186:

      ret_free = [r - rate for r in returns]
      

      Since I snapshot on the equity curve per day, and one year contains 365 trading days(crypto exchanges never stop on weekends, which is different with stocks).

      Recommended configurations:

      • Prerequisites: make sure you have over 2 days data and make over 2 trades during backtesting, otherwise you'll get None
      • Always set annualize =True, because sharpe ratio is usually in annual form.
      • Set riskfreerate=0.01 and convertrate=True, Backtrader already sets them default
      • Set timeframe and compression to make TimeReturn take a snapshot on equity curve per day, if the timeframe of your data feed is equal or less than 1 day, set timeframe=bt.TimeFrame.Days, compression=1, otherwise set timeframe=bt.TimeFrame.Days, compression=data_feed
      • Set factor to 252 for stocks and 365 for cryptocurrencies.

      Examples:

      • 1-day OHLCV bars for US stocks, timeframe=bt.TimeFrame.Days, compression=1, factor=252,annualize =True
      • 3-day OHLCV bars for US stocks, timeframe=bt.TimeFrame.Days, compression=3, factor=252,annualize =True
      • 1-day OHLCV bars for cryptocurrency, timeframe=bt.TimeFrame.Days, compression=1, factor=365,annualize =True
      • 3-day OHLCV bars for cryptocurrency, timeframe=bt.TimeFrame.Days, compression=3, factor=365,annualize =True
      • 15-minutes OHLCV bars for cryptocurrency, timeframe=bt.TimeFrame.Days, compression=1, factor=365,annualize =True
      posted in General Code/Help
      S
      soulmachine
    • Share my implementation of the trend following strategy in the book "Following the trend" by Andreas Clenow

      I implemented the trend following strategy in the book "Following the trend" by Andreas Clenow. The following is the core logic:

      class ClenowTrendFollowingStrategy(bt.Strategy):
          """The trend following strategy from the book "Following the trend" by Andreas Clenow."""
          alias = ('MeanReversion',)
      
          params = (
              ('trend_filter_fast_period', 50),
              ('trend_filter_slow_period', 100),
              ('fast_donchian_channel_period', 25),
              ('slow_donchian_channel_period', 50),
              ('trailing_stop_atr_period', 100),
              ('trailing_stop_atr_count', 3),
              ('risk_factor', 0.002)
          )
      
          def __init__(self):
              self.trend_filter_fast = bt.indicators.EMA(period=self.params.trend_filter_fast_period)
              self.trend_filter_slow = bt.indicators.EMA(period=self.params.trend_filter_slow_period)
              self.dc_fast = DonchianChannelsIndicator(period=self.params.fast_donchian_channel_period)
              self.dc_slow = DonchianChannelsIndicator(period=self.params.slow_donchian_channel_period)
              self.atr = bt.indicators.ATR(period=self.params.trailing_stop_atr_period)
              # For trailing stop
              self.max_price = self.data.close[0] # track the highest price after opening long positions
              self.min_price = self.data.close[0] # track the lowest price after opening short positions
      
          def next(self):
              is_long = self.trend_filter_fast > self.trend_filter_slow # trend filter
              
              # Position size rule
              max_loss = self.broker.getvalue() * self.p.risk_factor # cash you afford to loss
              position_size = max_loss / self.atr
      
              # self.dc_slow.low <= self.dc_fast.low <= self.dc_fast.high <= self.dc_slow.high
              assert self.dc_slow.low <= self.dc_fast.low
              assert self.dc_fast.low <= self.dc_fast.high
              assert self.dc_fast.high <= self.dc_slow.high
      
              if self.data.close > self.dc_slow.high:
                  if is_long and self.position.size == 0:
                      self.long_order = self.buy(size=position_size) # Entry rule 1
                      print(f'Long {position_size}')
                      self.max_price = self.data.close[0]
                      return
              elif self.data.close > self.dc_fast.high:
                  if self.position.size < 0:
                      print(f'Close {self.position.size} by exit rule 2')
                      self.close() # Exit rule 2
                      return
              elif self.data.close > self.dc_fast.low:
                  pass
              elif self.data.close > self.dc_slow.low:
                  if self.position.size > 0:
                      print(f'Close {self.position.size} by exit rule 1')
                      self.close() # Exit rule 1
                      return
              else:
                  if (not is_long) and self.position.size == 0:
                      self.short_order = self.sell(size=position_size) # Entry rule 2
                      print(f'Short {position_size}')
                      self.min_price = self.data.close[0]
                      return
      
              # Trailing stop
              if self.position.size > 0:
                  self.max_price = max(self.max_price, self.data.close[0])
                  if self.data.close[0] < (self.max_price-self.atr[0]*3):
                      print(f'Close {self.position.size}  by trailing stop rule')
                      self.close()
                      return
              if self.position.size < 0:
                  self.min_price = min(self.max_price, self.data.close[0])
                  if self.data.close[0] > (self.min_price+self.atr[0]*3):
                      print(f'Close {self.position.size} by trailing stop rule')
                      self.close()
                      return
      

      Jupyter notebook: https://github.com/soulmachine/crypto-notebooks/blob/master/backtest/Clenow-trend-following.ipynb

      Any improvement suggestions? Thanks.

      posted in Indicators/Strategies/Analyzers
      S
      soulmachine
    • RE: Throwing an exception when run in multiple processes

      It seems Backtrader is not compatible with Dask.

      Dask version not working:

      with ProgressBar(): # Backtrader is not compatible with Dask
          stats = db.from_sequence(params_list).map(run_strategy).compute()
      

      Plain multiprocessing version works:

      with multiprocessing.Pool(os.cpu_count()) as p:
          stats = p.map(run_strategy, params_list)
      
      posted in General Discussion
      S
      soulmachine
    • RE: How to initialize bt.analyzers.SharpeRatio?

      @run-out The link you posted above explains compression clearly, thanks a lot!

      posted in General Code/Help
      S
      soulmachine
    • RE: How to use two CommissionInfo inside a strategy?

      @run-out You're right, the strategy retrieves the CommissionInfo instance at this line https://github.com/mementum/backtrader/blob/master/backtrader/strategy.py#L1315 :

      comminfo = self.broker.getcommissioninfo(data)
      

      So it uses the data name.

      Since data has only one name, so it's not possible to apply multiple commission fees?

      posted in Indicators/Strategies/Analyzers
      S
      soulmachine
    • RE: Incorrect `_timeframe` after loading Pandas dataframe to a data feed

      @dasch What if my bars are volume bars(from Chapter 2 "Financial Data Structures" of the book "Advances in Financial Machine Learning" by Marcos Prado), which don't have a fixed timeframe, use timeframe=bt.TimeFrame.Ticks?

      posted in General Discussion
      S
      soulmachine
    • RE: Share my implementation of the trend following strategy in the book "Following the trend" by Andreas Clenow

      Typo: alias = ('ClenowTrendFollowing',)

      posted in Indicators/Strategies/Analyzers
      S
      soulmachine
    • RE: How to write an indicator equivalent to pandas.expanding().mean()?

      @ab_trader It works, thanks!

      posted in General Discussion
      S
      soulmachine
    • RE: Why are there so many ways to access lines data?

      @ab_trader https://en.wikipedia.org/wiki/Zen_of_Python

      posted in General Discussion
      S
      soulmachine

    Latest posts made by soulmachine

    • RE: Throwing an exception when run in multiple processes

      It seems Backtrader is not compatible with Dask.

      Dask version not working:

      with ProgressBar(): # Backtrader is not compatible with Dask
          stats = db.from_sequence(params_list).map(run_strategy).compute()
      

      Plain multiprocessing version works:

      with multiprocessing.Pool(os.cpu_count()) as p:
          stats = p.map(run_strategy, params_list)
      
      posted in General Discussion
      S
      soulmachine
    • RE: Throwing an exception when run in multiple processes

      I guess the issue is in the Donchian channel indicator:

      class DonchianChannelsIndicator(bt.Indicator):
          '''Donchian channel.'''
      
          alias = ('DCH', 'DonchianChannel',)
      
          lines = ('dcm', 'dch', 'dcl',)  # dc middle, dc high, dc low
      
          params = (
              ('period', 20), # lookback period
          )
      
          plotinfo = dict(subplot=False)  # plot along with data
          plotlines = dict(
              dcm=dict(ls='--'),  # dashed line
              dch=dict(_samecolor=True),  # use same color as prev line (dcm)
              dcl=dict(_samecolor=True),  # use same color as prev line (dch)
          )
      
          def __init__(self):
              super().__init__()
              self.addminperiod(self.params.period + 1)
              self.lines.dch = bt.indicators.Highest(self.data.high(-1), period=self.params.period)
              self.lines.dcl = bt.indicators.Lowest(self.data.low(-1), period=self.params.period)
              self.lines.dcm = (self.lines.dch + self.lines.dcl) / 2.0  # avg of the above
      

      In sequential mode the indicator has 3 lines, while in parallel mode the indicator has 0 lines, why? This is so weird.

      Probably Backtrader is not compatible with Dask?

      posted in General Discussion
      S
      soulmachine
    • RE: How to pass a list of dicts to optstrategy?

      @ab_trader Good idea, I'll try it, thanks!

      posted in Indicators/Strategies/Analyzers
      S
      soulmachine
    • Throwing an exception when run in multiple processes

      I write a strategy in this Jupyter notebook: Clenow trend following strategy - Github

      I try to run a grid search to find out the optimal parameters combination. If I run sequentially as the following, no problem at all:

      stats = []
          for params in tqdm(params_list):
              result = run_strategy(params)
              stats.append(result)
      

      If I run in parallel as the following:

      stats = db.from_sequence(params_list).map(run_strategy).compute()
      

      It always throw out an exception:

      ~/anaconda3/lib/python3.7/site-packages/backtrader/lineiterator.py in dopostinit()
          133 
          134         # my minperiod is as large as the minperiod of my lines
      --> 135         _obj._minperiod = max([x._minperiod for x in _obj.lines])
          136 
          137         # Recalc the period
      
      ValueError: max() arg is an empty sequence
      

      Full stack trace is in the Jupyter notebook.

      Any ideas? Thanks!

      posted in General Discussion
      S
      soulmachine
    • How to pass a list of dicts to optstrategy?

      I want to optimize my strategy with customized parameters, for example:

      cerebro.optstrategy(bt.strategies.MA_CrossOver, fast=range(2,8), slow=range(4, 16))
      

      The above configuration generates a lot of parameters that I don't want.

      Instead, I want to specify parameters manually as the following:

      cerebro.optstrategy(bt.strategies.MA_CrossOver, myparams=[
        {'fast': 2, 'slow': 4},
        {'fast': 4, 'slow': 8},
      {'fast': 3, 'slow': 7},
      ])
      

      How can I achieve this? Thanks

      posted in Indicators/Strategies/Analyzers
      S
      soulmachine
    • RE: Share my implementation of the trend following strategy in the book "Following the trend" by Andreas Clenow

      Typo: alias = ('ClenowTrendFollowing',)

      posted in Indicators/Strategies/Analyzers
      S
      soulmachine
    • Share my implementation of the trend following strategy in the book "Following the trend" by Andreas Clenow

      I implemented the trend following strategy in the book "Following the trend" by Andreas Clenow. The following is the core logic:

      class ClenowTrendFollowingStrategy(bt.Strategy):
          """The trend following strategy from the book "Following the trend" by Andreas Clenow."""
          alias = ('MeanReversion',)
      
          params = (
              ('trend_filter_fast_period', 50),
              ('trend_filter_slow_period', 100),
              ('fast_donchian_channel_period', 25),
              ('slow_donchian_channel_period', 50),
              ('trailing_stop_atr_period', 100),
              ('trailing_stop_atr_count', 3),
              ('risk_factor', 0.002)
          )
      
          def __init__(self):
              self.trend_filter_fast = bt.indicators.EMA(period=self.params.trend_filter_fast_period)
              self.trend_filter_slow = bt.indicators.EMA(period=self.params.trend_filter_slow_period)
              self.dc_fast = DonchianChannelsIndicator(period=self.params.fast_donchian_channel_period)
              self.dc_slow = DonchianChannelsIndicator(period=self.params.slow_donchian_channel_period)
              self.atr = bt.indicators.ATR(period=self.params.trailing_stop_atr_period)
              # For trailing stop
              self.max_price = self.data.close[0] # track the highest price after opening long positions
              self.min_price = self.data.close[0] # track the lowest price after opening short positions
      
          def next(self):
              is_long = self.trend_filter_fast > self.trend_filter_slow # trend filter
              
              # Position size rule
              max_loss = self.broker.getvalue() * self.p.risk_factor # cash you afford to loss
              position_size = max_loss / self.atr
      
              # self.dc_slow.low <= self.dc_fast.low <= self.dc_fast.high <= self.dc_slow.high
              assert self.dc_slow.low <= self.dc_fast.low
              assert self.dc_fast.low <= self.dc_fast.high
              assert self.dc_fast.high <= self.dc_slow.high
      
              if self.data.close > self.dc_slow.high:
                  if is_long and self.position.size == 0:
                      self.long_order = self.buy(size=position_size) # Entry rule 1
                      print(f'Long {position_size}')
                      self.max_price = self.data.close[0]
                      return
              elif self.data.close > self.dc_fast.high:
                  if self.position.size < 0:
                      print(f'Close {self.position.size} by exit rule 2')
                      self.close() # Exit rule 2
                      return
              elif self.data.close > self.dc_fast.low:
                  pass
              elif self.data.close > self.dc_slow.low:
                  if self.position.size > 0:
                      print(f'Close {self.position.size} by exit rule 1')
                      self.close() # Exit rule 1
                      return
              else:
                  if (not is_long) and self.position.size == 0:
                      self.short_order = self.sell(size=position_size) # Entry rule 2
                      print(f'Short {position_size}')
                      self.min_price = self.data.close[0]
                      return
      
              # Trailing stop
              if self.position.size > 0:
                  self.max_price = max(self.max_price, self.data.close[0])
                  if self.data.close[0] < (self.max_price-self.atr[0]*3):
                      print(f'Close {self.position.size}  by trailing stop rule')
                      self.close()
                      return
              if self.position.size < 0:
                  self.min_price = min(self.max_price, self.data.close[0])
                  if self.data.close[0] > (self.min_price+self.atr[0]*3):
                      print(f'Close {self.position.size} by trailing stop rule')
                      self.close()
                      return
      

      Jupyter notebook: https://github.com/soulmachine/crypto-notebooks/blob/master/backtest/Clenow-trend-following.ipynb

      Any improvement suggestions? Thanks.

      posted in Indicators/Strategies/Analyzers
      S
      soulmachine
    • RE: How to initialize bt.analyzers.SharpeRatio?

      After reading the source code I think I've figured out.

      timeframe and compression are used at analyzers/sharpe.py#L153:

      returns = list(itervalues(self.timereturn.get_analysis()))
      
      • timeframe=bt.TimeFrame.Days, compression=1, means TimeReturn will take a snapshot on the equity curve per day
      • timeframe=bt.TimeFrame.Minutes, compression=24*60, means TimeReturn will take a snapshot on the equity curve per 24*60 minutes, which is the same as a day

      returns is a list of (timestamp, percentage), sharpe ratio is calculated on top of this list.

      timeframe, compression --> TimeReturn -->returns --> sharpe_ratio
      

      The factor is used to convert riskfreerate to rate, which used at analyzers/sharpe.py#L186:

      ret_free = [r - rate for r in returns]
      

      Since I snapshot on the equity curve per day, and one year contains 365 trading days(crypto exchanges never stop on weekends, which is different with stocks).

      Recommended configurations:

      • Prerequisites: make sure you have over 2 days data and make over 2 trades during backtesting, otherwise you'll get None
      • Always set annualize =True, because sharpe ratio is usually in annual form.
      • Set riskfreerate=0.01 and convertrate=True, Backtrader already sets them default
      • Set timeframe and compression to make TimeReturn take a snapshot on equity curve per day, if the timeframe of your data feed is equal or less than 1 day, set timeframe=bt.TimeFrame.Days, compression=1, otherwise set timeframe=bt.TimeFrame.Days, compression=data_feed
      • Set factor to 252 for stocks and 365 for cryptocurrencies.

      Examples:

      • 1-day OHLCV bars for US stocks, timeframe=bt.TimeFrame.Days, compression=1, factor=252,annualize =True
      • 3-day OHLCV bars for US stocks, timeframe=bt.TimeFrame.Days, compression=3, factor=252,annualize =True
      • 1-day OHLCV bars for cryptocurrency, timeframe=bt.TimeFrame.Days, compression=1, factor=365,annualize =True
      • 3-day OHLCV bars for cryptocurrency, timeframe=bt.TimeFrame.Days, compression=3, factor=365,annualize =True
      • 15-minutes OHLCV bars for cryptocurrency, timeframe=bt.TimeFrame.Days, compression=1, factor=365,annualize =True
      posted in General Code/Help
      S
      soulmachine
    • RE: How to initialize bt.analyzers.SharpeRatio?

      After reading some source code and several experiments, my guess is that the timeframe in SharpeRatio has no relation to the timeframe of the data feed. The timeframe in SharpeRatio means how frequent you want to snapshot the equity curve.

      posted in General Code/Help
      S
      soulmachine
    • RE: Sharpe ratio with seconds-resolution data

      @benmercerdev See my another post https://community.backtrader.com/topic/2747/how-to-initialize-bt-analyzers-sharperatio/

      posted in Indicators/Strategies/Analyzers
      S
      soulmachine