Overall Statistics
Total Trades
1854
Average Win
0.17%
Average Loss
-0.12%
Compounding Annual Return
7.670%
Drawdown
19.400%
Expectancy
0.506
Net Profit
44.723%
Sharpe Ratio
0.849
Probabilistic Sharpe Ratio
34.452%
Loss Rate
36%
Win Rate
64%
Profit-Loss Ratio
1.36
Alpha
0.036
Beta
0.368
Annual Standard Deviation
0.096
Annual Variance
0.009
Information Ratio
-0.372
Tracking Error
0.119
Treynor Ratio
0.223
Total Fees
$1888.09
from math import ceil,floor
from datetime import datetime
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression


class TrendFollowingAlgorithm(QCAlgorithm):
    

    def Initialize(self):
        self.SetStartDate(2015, 1, 1)  
        self.SetEndDate(2020, 1, 1)
        self.SetCash(100000)            
        self.lookback = int(252/2)
        self.profittake = 1.96 # 95% bollinger band
        self.maxlever = 0.9 # always hold 10% Cash
        self.AddEquity("SPY", Resolution.Minute)
        etfs = [
            # Equity
            'DIA',    # Dow
            'SPY',    # S&P 500
            # Fixed income
            'IEF',    # Treasury Bond
            'HYG',    # High yield bond
            # Alternatives
            'USO',    # Oil
            'GLD',    # Gold
            'VNQ',    # US Real Estate
            'RWX',    # Dow Jones Global ex-U.S. Select Real Estate Securities Index
            'UNG',    # Natual gas
            'DBA',    # Agriculture
        ]
        
        for etf in etfs:
            self.AddEquity(etf, Resolution.Minute)
        
        self.symbol_data = {} # stores the Rolling open and close data, as well as weights and stop prices
        
        self.PctDailyVolatilityTarget = 0.025 # target daily vol target in %

        # trailing stop
        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.AfterMarketOpen("SPY", 10), self.trail_stop)
        
        # perform calculations for asset weightings
        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.AfterMarketOpen("SPY", 28), self.compute_regression_asset_weightings)
        
        # rebalance the portfolio according to calculations
        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.AfterMarketOpen("SPY", 30), self.rebalance)

        self.curr_day = -1
        
        # update closing price data
        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.BeforeMarketClose("SPY", 1), self.update_closes)

    
    def update_closes(self):
        # updates closing price data
        
        for symbol, sd in self.symbol_data.items():
            if self.CurrentSlice.Bars.ContainsKey(symbol):
                sd.UpdateClose(self.CurrentSlice.Bars[symbol].Close)
    
    def OnData(self, data):
        # updates the SymbolDatas with daily opening prices
        
        if self.curr_day == self.Time.day:
            return
        
        self.curr_day = self.Time.day
        
        for symbol, sd in self.symbol_data.items():
            if data.Bars.ContainsKey(symbol):
                sd.UpdateOpen(data.Bars[symbol].Open)
    
    def OnSecuritiesChanged(self, changed):
        # add and remove SymbolDatas to our dict
        for security in changed.AddedSecurities:
            self.symbol_data[security.Symbol] = SymbolData(self, security.Symbol, self.lookback)
        
        for security in changed.RemovedSecurities:
            self.symbol_data.pop(security.Symbol, None)
    
    def calc_vol_scalar(self):
        # calculate the volatility scale factors for each ticker
        
        processed_sd = {symbol.Value: list(sd.open_data)[::-1] for symbol, sd in self.symbol_data.items()}
        
        df_price = pd.DataFrame(processed_sd) 
        rets = np.log(df_price).diff().dropna()
        lock_value = df_price.iloc[-1]
        
        # Exponentially-weighted moving std
        price_vol = rets.ewm(halflife=20,ignore_na=True, min_periods=0, adjust=True).std(bias=False).dropna() 
        
        volatility_scalar = self.PctDailyVolatilityTarget / price_vol.iloc[-1]

        return volatility_scalar
  
    def compute_regression_asset_weightings(self):
        # compute asset weightings
        
        A = range( self.lookback + 1 )
        for symbol, sd in self.symbol_data.items():
            if not sd.IsReady:
                continue
            
            prices = list(sd.open_data)[::-1]  # undo reverse order of RWs
            
            # volatility
            std = np.std(prices)
            # Price points to run regression
            Y = prices
            # Add column of ones so we get intercept
            X = np.column_stack([np.ones(len(A)), A])
            if len(X) != len(Y):
                length = min(len(X), len(Y))
                X = X[-length:]
                Y = Y[-length:]
                A = A[-length:]
            # Creating Model
            reg = LinearRegression()
            # Fitting training data
            
            reg = reg.fit(X, Y)
            # run linear regression y = ax + b
            b = reg.intercept_
            a = reg.coef_[1]
            
            # Normalized slope
            slope = a / b *252.0
            # Currently how far away from regression line
            delta = Y - (np.dot(a, A) + b)
            # Don't trade if the slope is near flat (at least %7 growth per year to trade)
            slope_min = 0.252
            
            # Long but slope turns down, then exit
            if sd.weight > 0 and slope < 0:
                sd.weight = 0
                
            # short but slope turns upward, then exit
            if sd.weight < 0 and slope > 0:
                sd.weight = 0
                
            # Trend is up
            if slope > slope_min:
                
                # price crosses the regression line
                if delta[-1] > 0 and delta[-2] < 0 and sd.weight == 0:
                    sd.stopprice = None
                    sd.weight = slope
                # Profit take, reaches the top of 95% bollinger band
                if delta[-1] > self.profittake * std and sd.weight > 0:
                    sd.weight = 0
            
            # Trend is down
            if slope < -slope_min:
          
                # price crosses the regression line
                if delta[-1] < 0 and delta[-2] > 0 and sd.weight == 0:
                    sd.stopprice = None
                    sd.weight = slope
                # profit take, reaches the top of 95% bollinger band
                if delta[-1] < self.profittake * std and sd.weight < 0:
                    sd.weight = 0
                    
    
    def rebalance(self):
        # rebalance portfolio
        
        vol_mult = self.calc_vol_scalar()
        no_positions = len([1 for _, sd in self.symbol_data.items() if sd.weight != 0])

        for symbol, sd in self.symbol_data.items():
            if not sd.IsReady:
                continue
            if sd.weight == 0:
                self.Liquidate(symbol)
            elif sd.weight > 0:
                self.SetHoldings(symbol, (min(sd.weight, self.maxlever)/no_positions)*vol_mult[symbol.Value])
            elif sd.weight < 0:
                self.SetHoldings(symbol, (max(sd.weight, -self.maxlever)/no_positions)*vol_mult[symbol.Value])

    def trail_stop(self):
        for symbol, sd in self.symbol_data.items():
            if not sd.IsReady:
                continue
            mean_price = np.mean(list(sd.close_data))
            # Stop loss percentage is the return over the lookback period
            stoploss = abs(sd.weight * self.lookback / 252.0) + 1    # percent change per period
            if sd.weight > 0 and sd.stopprice is not None:
                if sd.stopprice is not None and sd.stopprice < 0:
                    sd.stopprice = mean_price / stoploss
                else:
                    sd.stopprice = max(mean_price / stoploss, sd.stopprice)
                    if mean_price < sd.stopprice:
                        sd.weight = 0
                        self.Liquidate(symbol)
            
            elif sd.weight < 0 and sd.stopprice is not None: 
                if sd.stopprice is not None and sd.stopprice < 0:
                    sd.stopprice = mean_price * stoploss
                else:
                    sd.stopprice = min(mean_price * stoploss, sd.stopprice)
                    if mean_price > sd.stopprice:
                       sd.weight = 0
                       self.Liquidate(symbol)
            
            else:
                sd.stopprice = None
                
class SymbolData:
    def __init__(self, algorithm, symbol, lookback):
        self.open_data = RollingWindow[float](lookback)
        self.close_data = RollingWindow[float](3)
        
        hist = algorithm.History(symbol, lookback, Resolution.Daily).loc[symbol]
        for _, row in hist.iterrows():
            self.open_data.Add(row.open)
            self.close_data.Add(row.close)
            
        self.stopprice = 0
        self.weight = 0
    
    def UpdateOpen(self, value):
        self.open_data.Add(value)
    
    def UpdateClose(self, value):
        self.close_data.Add(value)
    
    @property
    def IsReady(self):
        return self.open_data.IsReady and self.close_data.IsReady