Overall Statistics
Total Trades
88
Average Win
0.35%
Average Loss
-0.27%
Compounding Annual Return
9.387%
Drawdown
2.800%
Expectancy
0.429
Net Profit
6.494%
Sharpe Ratio
1.736
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
1.29
Alpha
0.047
Beta
0.183
Annual Standard Deviation
0.05
Annual Variance
0.003
Information Ratio
-0.999
Tracking Error
0.132
Treynor Ratio
0.478
Total Fees
$88.00
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import *
from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *
from QuantConnect.Python import PythonQuandl
from QuantConnect.Securities.Equity import EquityExchange

from datetime import datetime, timedelta
import numpy as np
import pandas as pd
from scipy.stats import norm

#from QuantConnect.Data.Custom.Tiingo import *
from QuantConnect.Python import PythonQuandl # quandl data not CLOSE
from QuantConnect.Python import PythonData # custom data
from QuantConnect.Data import SubscriptionDataSource

# Risk Premia RW algorithm
class RPRWAlgorithm(QCAlgorithm):

    def Initialize(self):

        # Initial settings
        self.SetStartDate(2018, 12, 18)
        self.SetEndDate(2019, 8, 30)
        self.SetCash(10000)
        self.MarketAsset = "SPY"
        self.WarmupTime = 310
        self.Window = 300
        
        #parameters
        self.vol_lookback = 90
        self.corr_lookback = 120
        self.formation_periods = np.array([3, 6, 9, 12])*22
        self.z_score_cutoff = 0
        self.momo_multiplier = 0.1        
        
        # these are the growth symbols we'll rotate through
        self.GrowthSymbols = ["VTI",  # Vanguard Total Stock Market ETF
                              "VEA",  # VEA - Vanguard FTSE Developed Markets 
                              "PUTW", # WisdomTree CBOE S&P 500 PutWrite Strategy Fund
                              "TLT",  # iShares 20+ Year Treasury Bond ETF
                              "UST",  # ProShares Ultra 7-10 Year Treasury
                              "VWO",  # iShares MSCI Emerging Markets Indx
                              "VNQI", # VANGUARD INTL E/GLB EX-US RL EST IX
                              "GBTC", #BTC
                              "EMB"]  # iShares J.P. Morgan USD Emerging Markets Bond ETF"
        # these are the safety symbols we go to when things are looking bad for growth
        # this part is not supposed to work
        # I don't know how to open these assets
        #self.SafetySymbols =  "PUTW", # WisdomTree CBOE S&P 500 PutWrite Strategy Fund
        #                      "EMB"]  # iShares J.P. Morgan USD Emerging Markets Bond ETF"
        
        
        #Tiingo.SetAuthCode("49e4ce130ce49c90d9e45193f70723b16c5e4ff7")
        #self.AddData(TiingoDailyData, "PUTW", Resolution.Daily)
        #self.AddData(TiingoDailyData, "EMB", Resolution.Daily)
        
        #self.ticker = "PUTW"
        #self.symbol = self.AddData(TiingoDailyData, self.ticker, Resolution.Daily).Symbol
        self.AddEquity("SPY", Resolution.Daily)
        
        if self.LiveMode:
            self.Debug("Trading Live!")
        
        self.SafetySymbols = []

        
        # all symbols set
        self.AllSymbols = list(set(self.GrowthSymbols) | set(self.SafetySymbols))
        
        # open equity symbols
        for symbol in self.GrowthSymbols:
            self.AddEquity(symbol, Resolution.Daily)

        # this doesn't do anything at the moment. We need to work out how to properly handles these assets
        for symbol in self.SafetySymbols:
            self.AddOption(symbol, Resolution.Daily)

        # wait for warming up
        self.SetWarmUp(self.WarmupTime)
        # schedule the trading function
        #self.Schedule.On(self.DateRules.MonthStart(self.MarketAsset), self.TimeRules.AfterMarketOpen(self.MarketAsset, 10), Action(self.RebalanceAndTrade))
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen(self.MarketAsset, 10), Action(self.RebalanceAndTrade))
        

    def OnData(self, slice):
        if self.LiveMode: self.Debug("Running algorithm!!")
        
        # Make sure all the data we need is in place
        if self.IsWarmingUp: return 

        if not slice.ContainsKey("PUTW"): 
            self.Debug("PUTW not found!!")
            return
        if not slice.ContainsKey("EMB"): 
            self.Debug("EMB not found!!")
            return       
        
        if self.LiveMode: self.Debug("Warm Up Complete Deciding..")

    # trading function
    def RebalanceAndTrade(self):
        
        self.Notify.Email("danielqing@yahoo.com.au", "Current Time" + str(self.Time) , "Portfolio" + str(self.Portfolio))
        # skipping if it is warming up
        if self.IsWarmingUp: return 
        if self.Time.day != 5: return
        self.Liquidate("AUDUSD")

        # creating the pandas DataFrame
        slices = self.History(self.AllSymbols, self.Window, Resolution.Daily)
        slices_df = pd.pivot_table(slices, values = 'close', index='time', columns = 'symbol').reset_index()
        slices_df = slices_df.drop(columns=['time'])
        returns = slices_df.pct_change()
        
        # for debugging
        #self.Debug(self.Time)
        #self.Debug(returns.shape)
        
        # weights calculation
        vol_weights = self.get_srp_weights(returns, self.vol_lookback)
        cor_adjust = self.get_cor_adjustments(returns, self.corr_lookback)
        cor_adjust_weights = self.adjust_weights(vol_weights, cor_adjust, shrinkage=1)
        momo_adjusted_weights = self.get_momo_adjusted_weights(returns, cor_adjust_weights, self.formation_periods, self.z_score_cutoff, self.momo_multiplier)
        # the following should contain asset EMB instead of EEM
        capped_weights = self.cap_allocation_and_rescale(momo_adjusted_weights, ticker="EMB", cap=0.15)
        # the following should VTI and PUTW but I don't know how to handle yet
        final_weights = self.split_allocation(capped_weights, "VTI", "PUTW", ratio=0.5)
        self.Debug(final_weights.shape)
        self.Debug(self.Time)
        self.Debug(final_weights)
        
        # allocating assets
        for i in range(len(final_weights)):
            self.Log("{} : asset {}, allocating {}".format(self.Time, slices_df.columns[i], final_weights[i]))
            self.SetHoldings(slices_df.columns[i], final_weights[i])
        self.Notify.Email("danielqing@yahoo.com.au", "Current Time" + str(self.Time) , "Weight" + str(final_weights))
                    

    def get_srp_weights(self, returns, vol_lookback):
        """
        returns current srp werights given a pandas DataFrame of returns and a vol_lookback period
        """
        n_assets = len(returns.columns)
        vols = returns.iloc[-vol_lookback:, :].apply(lambda x: np.std(x)*np.sqrt(252), axis=0)
        raw_weights = 1/vols
        weights = raw_weights/np.sum(raw_weights)
    
        return weights

    def get_cor_adjustments(self, returns, corr_lookback):
        """
        returns current correlation adjustments given a pandas DataFrame of returns and a corr_lookback period
        """
        cor = returns.iloc[-corr_lookback:, :].corr()
        pairwise_ave_cor = cor.mean(axis=1)
        zscore_pairwise_ave_cor = (pairwise_ave_cor - pairwise_ave_cor.mean())/pairwise_ave_cor.std()
        gauss_scale = 1 - norm.cdf(zscore_pairwise_ave_cor, 0, 1)
        raw_adjustments = gauss_scale/gauss_scale.sum()
        norm_adjustments = raw_adjustments - 1./len(returns.columns)
    
        return norm_adjustments
        
    def adjust_weights(self, vol_weights, corr_adjustments, shrinkage):
        raw_weights = vol_weights * (1 +corr_adjustments * shrinkage)
        adj_weights = raw_weights/raw_weights.sum()
    
        return adj_weights
        

    def get_momo_adjustments(self, returns, formation_period):
        """
        returns current cross-sectional zscore of total return momentum 
        given a pandas DataFrame of returns and formation_period
        """
        synth_prices = (returns+1).cumprod()
        roc = (synth_prices.iloc[-1,:]/synth_prices.iloc[-formation_period-1,:]-1)
        momo_adjustments = (roc - roc.mean())/roc.std()
    
        return momo_adjustments

    def get_sma_slope_adjustments(self, returns, formation_period):
        """
        returns current cross-sectional zscore of slope of moving average 
        given a pandes DataFrame of returns and a formation_period
        """
        synth_prices = (returns+1).cumprod()
        sma = synth_prices.iloc[-formation_period-1:,:].rolling(formation_period).mean()
        sma_slope = (sma.iloc[-1,:]/sma.iloc[-2,:])-1
        momo_adjustments = (sma_slope - sma_slope.mean())/sma_slope.std()
    
        return momo_adjustments

    def adjust_momo_weights(self, base_weights, momo_adjustments, z_score_cutoff, multiplier):
        raw_weights = base_weights * (1 + ((momo_adjustments >= z_score_cutoff) * multiplier))
        adj_weights = raw_weights/raw_weights.sum()
    
        return adj_weights

    def get_momo_adjusted_weights(self, returns, base_weights, formation_periods, z_score_cutoff, multiplier):
        """
        returns current momentum-adjusted weights given a pandes DataFrame of returns and a formation_period
        """
        momo_weights = base_weights

        for period in formation_periods :
            momo_adjustments = self.get_momo_adjustments(returns, period)
            momo_weights = self.adjust_momo_weights(momo_weights, momo_adjustments, z_score_cutoff, multiplier)

        for period in formation_periods :
            momo_adjustments = self.get_sma_slope_adjustments(returns, period)
            momo_weights = self.adjust_momo_weights(momo_weights, momo_adjustments, z_score_cutoff, multiplier)        
        
        return momo_weights
        
    def cap_allocation_and_rescale(self, weights, ticker, cap=0.15):
        """
        cap the allocation into ticker and rescale remaining weights
        """
        if weights[ticker] > cap:
            weights = (1-cap)*weights.drop(ticker)/weights.drop(ticker).sum()
            weights[ticker] = cap
    
        return weights

    def split_allocation(self, weights, ticker, split_ticker, ratio=0.5):
        """
        split the allocation into ticker into ticker and split_ticker according to ratio
        """
        weights[split_ticker] = (1-ratio)*weights[ticker]
        weights[ticker] = ratio*weights[ticker]
    
        #global tradeable_universe
        #if split_ticker not in tradeable_universe:
        #    tradeable_universe.append(split_ticker)
    
        return weights
'''
# Duplicate code?
from System import *
from QuantConnect import *
from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *
from QuantConnect.Python import PythonQuandl
from QuantConnect.Securities.Equity import EquityExchange

from datetime import datetime, timedelta
import numpy as np
import pandas as pd
from scipy.stats import norm

#from QuantConnect.Data.Custom.Tiingo import *
from QuantConnect.Python import PythonQuandl # quandl data not CLOSE
from QuantConnect.Python import PythonData # custom data
from QuantConnect.Data import SubscriptionDataSource

# Risk Premia RW algorithm
class RPRWAlgorithm(QCAlgorithm):

    def Initialize(self):

        # Initial settings
        self.SetStartDate(2019, 1, 1)
        self.SetEndDate(2019, 8, 1)
        self.SetCash(10000)
        self.MarketAsset = "SPY"
        self.WarmupTime = 310
        self.Window = 300
        
        #parameters
        self.vol_lookback = 90
        self.corr_lookback = 120
        self.formation_periods = np.array([3, 6, 9, 12])*22
        self.z_score_cutoff = 0
        self.momo_multiplier = 0.1        
        
        # these are the growth symbols we'll rotate through
        self.GrowthSymbols = ["VTI",  # Vanguard Total Stock Market ETF
                              "SPY",  # SPDR S&P 500 ETF Trust
                              "TLT",  # iShares 20+ Year Treasury Bond ETF
                              "EFA",  # iShares MSCI EAFE ETF
                              "EEM",  # iShares J.P. Morgan USD Emerging Markets Bond ETF 
                              "PUTW", # WisdomTree CBOE S&P 500 PutWrite Strategy Fund
                              "EMB"]  # iShares J.P. Morgan USD Emerging Markets Bond ETF"
        # these are the safety symbols we go to when things are looking bad for growth
        # this part is not supposed to work
        # I don't know how to open these assets
        #self.SafetySymbols =  "PUTW", # WisdomTree CBOE S&P 500 PutWrite Strategy Fund
        #                      "EMB"]  # iShares J.P. Morgan USD Emerging Markets Bond ETF"
        
        
        #Tiingo.SetAuthCode("49e4ce130ce49c90d9e45193f70723b16c5e4ff7")
        #self.AddData(TiingoDailyData, "PUTW", Resolution.Daily)
        #self.AddData(TiingoDailyData, "EMB", Resolution.Daily)
        
        #self.ticker = "PUTW"
        #self.symbol = self.AddData(TiingoDailyData, self.ticker, Resolution.Daily).Symbol
        #self.AddEquity("EMB", Resolution.Daily)
        
        
        
        self.SafetySymbols = []

        
        # all symbols set
        self.AllSymbols = list(set(self.GrowthSymbols) | set(self.SafetySymbols))
        
        # open equity symbols
        for symbol in self.GrowthSymbols:
            self.AddEquity(symbol, Resolution.Daily)

        # this doesn't do anything at the moment. We need to work out how to properly handles these assets
        for symbol in self.SafetySymbols:
            self.AddOption(symbol, Resolution.Daily)

        # wait for warming up
        self.SetWarmUp(self.WarmupTime)
        # schedule the trading function
        self.Schedule.On(self.DateRules.MonthStart(self.MarketAsset), self.TimeRules.AfterMarketOpen(self.MarketAsset, 10), Action(self.RebalanceAndTrade))
        

    def OnData(self, slice):
        if self.LiveMode: self.Debug("Running algorithm!!")
        
        # Make sure all the data we need is in place
        if self.IsWarmingUp: return 

        if not slice.ContainsKey("PUTW"): 
            self.Debug("PUTW not found!!")
            return
        if not slice.ContainsKey("EMB"): 
            self.Debug("EMB not found!!")
            return       
        
        if self.LiveMode: self.Debug("Warm Up Complete Deciding..")

    # trading function
    def RebalanceAndTrade(self):

        # skipping if it is warming up
        if self.IsWarmingUp: return 
    

        # creating the pandas DataFrame
        slices = self.History(self.AllSymbols, self.Window, Resolution.Daily)
        slices_df = pd.pivot_table(slices, values = 'close', index='time', columns = 'symbol').reset_index()
        slices_df = slices_df.drop(columns=['time'])
        returns = slices_df.pct_change()
        
        # for debugging
        #self.Debug(self.Time)
        #self.Debug(returns.shape)
        
        # weights calculation
        vol_weights = self.get_srp_weights(returns, self.vol_lookback)
        cor_adjust = self.get_cor_adjustments(returns, self.corr_lookback)
        cor_adjust_weights = self.adjust_weights(vol_weights, cor_adjust, shrinkage=1)
        momo_adjusted_weights = self.get_momo_adjusted_weights(returns, cor_adjust_weights, self.formation_periods, self.z_score_cutoff, self.momo_multiplier)
        # the following should contain asset EMB instead of EEM
        capped_weights = self.cap_allocation_and_rescale(momo_adjusted_weights, ticker="EMB", cap=0.15)
        # the following should VTI and PUTW but I don't know how to handle yet
        final_weights = self.split_allocation(capped_weights, "VTI", "PUTW", ratio=0.5)
        self.Debug(final_weights.shape)

        # allocating assets
        for i in range(len(final_weights)):
            self.Log("{} : asset {}, allocating {}".format(self.Time, slices_df.columns[i], final_weights[i]))
            self.SetHoldings(slices_df.columns[i], final_weights[i])
                    

    def get_srp_weights(self, returns, vol_lookback):
        """
        returns current srp werights given a pandas DataFrame of returns and a vol_lookback period
        """
        n_assets = len(returns.columns)
        vols = returns.iloc[-vol_lookback:, :].apply(lambda x: np.std(x)*np.sqrt(252), axis=0)
        raw_weights = 1/vols
        weights = raw_weights/np.sum(raw_weights)
    
        return weights

    def get_cor_adjustments(self, returns, corr_lookback):
        """
        returns current correlation adjustments given a pandas DataFrame of returns and a corr_lookback period
        """
        cor = returns.iloc[-corr_lookback:, :].corr()
        pairwise_ave_cor = cor.mean(axis=1)
        zscore_pairwise_ave_cor = (pairwise_ave_cor - pairwise_ave_cor.mean())/pairwise_ave_cor.std()
        gauss_scale = 1 - norm.cdf(zscore_pairwise_ave_cor, 0, 1)
        raw_adjustments = gauss_scale/gauss_scale.sum()
        norm_adjustments = raw_adjustments - 1./len(returns.columns)
    
        return norm_adjustments
        
    def adjust_weights(self, vol_weights, corr_adjustments, shrinkage):
        raw_weights = vol_weights * (1 +corr_adjustments * shrinkage)
        adj_weights = raw_weights/raw_weights.sum()
    
        return adj_weights
        

    def get_momo_adjustments(self, returns, formation_period):
        """
        returns current cross-sectional zscore of total return momentum 
        given a pandas DataFrame of returns and formation_period
        """
        synth_prices = (returns+1).cumprod()
        roc = (synth_prices.iloc[-1,:]/synth_prices.iloc[-formation_period-1,:]-1)
        momo_adjustments = (roc - roc.mean())/roc.std()
    
        return momo_adjustments

    def get_sma_slope_adjustments(self, returns, formation_period):
        """
        returns current cross-sectional zscore of slope of moving average 
        given a pandes DataFrame of returns and a formation_period
        """
        synth_prices = (returns+1).cumprod()
        sma = synth_prices.iloc[-formation_period-1:,:].rolling(formation_period).mean()
        sma_slope = (sma.iloc[-1,:]/sma.iloc[-2,:])-1
        momo_adjustments = (sma_slope - sma_slope.mean())/sma_slope.std()
    
        return momo_adjustments

    def adjust_momo_weights(self, base_weights, momo_adjustments, z_score_cutoff, multiplier):
        raw_weights = base_weights * (1 + ((momo_adjustments >= z_score_cutoff) * multiplier))
        adj_weights = raw_weights/raw_weights.sum()
    
        return adj_weights

    def get_momo_adjusted_weights(self, returns, base_weights, formation_periods, z_score_cutoff, multiplier):
        """
        returns current momentum-adjusted weights given a pandes DataFrame of returns and a formation_period
        """
        momo_weights = base_weights

        for period in formation_periods :
            momo_adjustments = self.get_momo_adjustments(returns, period)
            momo_weights = self.adjust_momo_weights(momo_weights, momo_adjustments, z_score_cutoff, multiplier)

        for period in formation_periods :
            momo_adjustments = self.get_sma_slope_adjustments(returns, period)
            momo_weights = self.adjust_momo_weights(momo_weights, momo_adjustments, z_score_cutoff, multiplier)        
        
        return momo_weights
        
    def cap_allocation_and_rescale(self, weights, ticker, cap=0.15):
        """
        cap the allocation into ticker and rescale remaining weights
        """
        if weights[ticker] > cap:
            weights = (1-cap)*weights.drop(ticker)/weights.drop(ticker).sum()
            weights[ticker] = cap
    
        return weights

    def split_allocation(self, weights, ticker, split_ticker, ratio=0.5):
        """
        split the allocation into ticker into ticker and split_ticker according to ratio
        """
        weights[split_ticker] = (1-ratio)*weights[ticker]
        weights[ticker] = ratio*weights[ticker]
    
        #global tradeable_universe
        #if split_ticker not in tradeable_universe:
        #    tradeable_universe.append(split_ticker)
    
        return weights
        
        
'''