Typo: alias = ('ClenowTrendFollowing',)
soulmachine
@soulmachine
Posts made by soulmachine

RE: Share my implementation of the trend following strategy in the book "Following the trend" by Andreas Clenow

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_priceself.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/cryptonotebooks/blob/master/backtest/Clenowtrendfollowing.ipynb
Any improvement suggestions? Thanks.

RE: How to initialize bt.analyzers.SharpeRatio?
After reading the source code I think I've figured out.
timeframe
andcompression
are used at analyzers/sharpe.py#L153:returns = list(itervalues(self.timereturn.get_analysis()))
timeframe=bt.TimeFrame.Days, compression=1
, meansTimeReturn
will take a snapshot on the equity curve per daytimeframe=bt.TimeFrame.Minutes, compression=24*60
, meansTimeReturn
will take a snapshot on the equity curve per24*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 convertriskfreerate
torate
, 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
andconvertrate=True
, Backtrader already sets them default  Set
timeframe
andcompression
to makeTimeReturn
take a snapshot on equity curve per day, if the timeframe of your data feed is equal or less than 1 day, settimeframe=bt.TimeFrame.Days, compression=1
, otherwise settimeframe=bt.TimeFrame.Days, compression=data_feed
 Set
factor
to252
for stocks and365
for cryptocurrencies.
Examples:
 1day OHLCV bars for US stocks,
timeframe=bt.TimeFrame.Days, compression=1, factor=252,annualize =True
 3day OHLCV bars for US stocks,
timeframe=bt.TimeFrame.Days, compression=3, factor=252,annualize =True
 1day OHLCV bars for cryptocurrency,
timeframe=bt.TimeFrame.Days, compression=1, factor=365,annualize =True
 3day OHLCV bars for cryptocurrency,
timeframe=bt.TimeFrame.Days, compression=3, factor=365,annualize =True
 15minutes OHLCV bars for cryptocurrency,
timeframe=bt.TimeFrame.Days, compression=1, factor=365,annualize =True

RE: How to initialize bt.analyzers.SharpeRatio?
After reading some source code and several experiments, my guess is that the
timeframe
inSharpeRatio
has no relation to the timeframe of the data feed. The timeframe inSharpeRatio
means how frequent you want to snapshot the equity curve. 
RE: Sharpe ratio with secondsresolution data
@benmercerdev Your
compression
is incorrect, you should use the following:cerebro.addanalyzer(btanalyzers.SharpeRatio, _name='mysharpe', timeframe=bt.TimeFrame.Seconds, factor=252, compression=60*60*24)

RE: How to initialize bt.analyzers.SharpeRatio?
Anyone knows what's the difference between
timeframe=bt.TimeFrame.Days, compression=1, factor=365
andtimeframe=bt.TimeFrame.Minutes, compression=24*60, factor=365
? My data are 1minute OHLCV bars from 20200509 to 20200515. 
RE: How to write __init__() in customized Commission schema?
@vladisld Thanks, it works! I shared my complete code in another post https://community.backtrader.com/topic/2806/sharemycustomizedcommissionschemeforcryptocurrencies

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/20190829fractionalsizes/fractionalsizes/. ''' 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/20190829fractionalsizes/fractionalsizes/. ''' return self.p.leverage * (cash / price) def _getcommission(self, size, price, pseudoexec): '''Percentage based commission fee. More details at https://www.backtrader.com/docu/userdefinedcommissions/commissionschemessubclassing/. ''' return abs(size) * self.p.commission * price * self.p.mult
Hope it helps!

RE: Is Backtrader eventdriven or vectorized?
@vladisld @runout Thanks for your clarification !
Now I'm sure that Backtrader is a pure eventdriven framework.
vectorized
means it must take advantage of SMID operations like numpy and Pandas(Pandas actually uses numpy under the hood).