Overall Statistics
Total Trades
39
Average Win
14.49%
Average Loss
-1.17%
Compounding Annual Return
23.894%
Drawdown
18.000%
Expectancy
10.248
Net Profit
744.521%
Sharpe Ratio
1.302
Probabilistic Sharpe Ratio
77.679%
Loss Rate
16%
Win Rate
84%
Profit-Loss Ratio
12.36
Alpha
0.148
Beta
0.18
Annual Standard Deviation
0.13
Annual Variance
0.017
Information Ratio
0.297
Tracking Error
0.169
Treynor Ratio
0.938
Total Fees
$599.80
Estimated Strategy Capacity
$260000000.00
Lowest Capacity Asset
QQQ RIWIV7K5Z9LX
#https://lean-api-docs.netlify.app/index.html
#https://www.quantconnect.com/forum/discussion/11566/kei-based-strategy/p1
#https://seekingalpha.com/article/4434713-sector-rotation-strategy-using-the-high-yield-spread
#https://seekingalpha.com/article/4394646-this-sector-rotation-strategy-made-17-percent-year-since-1991
#self.AddData(Fred, Fred.ICEBofAML.USHighYieldMasterIIOptionAdjustedSpread)
## SIMON LesFlex June 2021 ##
## Modified by Vladimir

from QuantConnect.Python import PythonQuandl

### Simon LesFlex June 2021 ###
### Key Short—Term Economic Indicators. The Key Economic Indicators (KEI) database contains monthly and quarterly statistics 
### (and associated statistical methodological information) for the 33 OECD member and for a selection of non—member countries 
### on a wide variety of economic indicators, namely: quarterly national accounts, industrial production, composite leading indicators, 
### business tendency and consumer opinion surveys, retail trade, consumer and producer prices, hourly earnings, employment/unemployment,
### interest rates, monetary aggregates, exchange rates, international trade and balance of payments. Indicators have been prepared by national statistical 
### agencies primarily to meet the requirements of users within their own country. In most instances, the indicators are compiled in accordance with 
### international statistical guidelines and recommendations. However, national practices may depart from these guidelines, and these departures may 
### impact on comparability between countries. There is an on—going process of review and revision of the contents of the database in order to maximise 
### the relevance of the database for short—term economic analysis.
### For more information see: http://stats.oecd.org/OECDStat_Metadata/ShowMetadata.ashx?Dataset=KEI&Lang=en
### Reference Data Set: https://www.quandl.com/data/OECD/KEI_LOLITOAA_OECDE_ST_M-Leading-indicator-amplitude-adjusted-OECD-Europe-Level-ratio-or-index-Monthly
import numpy as np
import datetime as dt

class QuandlImporterAlgorithm(QCAlgorithm):

    def Initialize(self):
        #self.quandlCode = "OECD/KEI_LOLITOAA_OECDE_ST_M" #Euro
        self.quandlCode = "OECD/KEI_LOLITOAA_OECD_ST_M"  #Total
        ## Optional argument - personal token necessary for restricted dataset
        #Quandl.SetAuthCode("MLNarxdsMU92vk-ZJDvg")
        Quandl.SetAuthCode("RXk7Mxue6oH1TM-U8b7c")
        
        self.SetStartDate(2012,1,1)                                 #Set Start Date
        self.SetEndDate(datetime.today() - timedelta(1))            #Set End Date
        self.SetCash(100000)                                         #Set Strategy Cash
        self.SetWarmup(100)
        self.SetBenchmark("SPY")
        self.init = True
        self.stage = 0
        self.kei = self.AddData(QuandlCustomColumns, self.quandlCode, Resolution.Daily, TimeZones.NewYork).Symbol
        self.sma = self.SMA(self.kei, 1)
        self.mom = self.MOMP(self.kei, 2)
        self.MKT = self.AddEquity('SPY', Resolution.Hour).Symbol
        self.bond = self.AddEquity('TLT', Resolution.Hour).Symbol
        self.previous_SPY = 0
        
        #This dictionary with hold the symbols and the prices if needed
        self.symbols_dict = dict([
            ('SPY',['SPY',0]),('XMMO',['XMMO',0]),('MTUM',['MTUM',0]),('FVAL',['FVAL',0]),
            ('DXD',['DXD',0]),('GLD',['GLD',0]),('DIA',['DIA',0]),('TLT',['TLT',0]),('QQQ',['QQQ',0])
            ])
        
        #use this dataframe to hold the stats about the stage
        self.columns = ['stage_date', 'stage_num','stage','stage_days','symbol','price']
        self.stage_log_df = pd.DataFrame(columns=self.columns)
        
        for key in self.symbols_dict:
            self.AddEquity(self.symbols_dict[key][0], Resolution.Hour).Symbol
            
        self.Schedule.On(self.DateRules.WeekStart(self.MKT), self.TimeRules.AfterMarketOpen(self.MKT, 31), 
            self.Rebalance)
        

    def Rebalance(self):
        if self.IsWarmingUp or not self.mom.IsReady or not self.sma.IsReady: return
        initial_asset = self.symbols_dict['QQQ'][0] if self.mom.Current.Value > 0 else self.symbols_dict['TLT'][0]
        
        
        if self.init:
            self.SetHoldings(initial_asset, 1)
            self.init = False
            

        keihist = self.History([self.kei], 1400)
        #returns the historical data for custom 90 day period.
        #keihist = self.History([self.kei],self.StartDate-timedelta(100),self.StartDate-timedelta(10))
        
        #keihist = keihist['Value'].unstack(level=0).dropna()
        keihistlowt = np.nanpercentile(keihist, 15)
        keihistmidt = np.nanpercentile(keihist, 50)
        keihisthight = np.nanpercentile(keihist, 90)
        kei = self.sma.Current.Value
        keimom = self.mom.Current.Value
            
        #if (keimom < 0 and kei < keihistmidt and  kei > keihistlowt) and not (self.Securities[self.bond].Invested):
        if (keimom < 0 and kei < keihistmidt and  kei > keihistlowt) and not (self.stage == 5):
            # DECLINE
            self.Liquidate()
            self.stage = 5
            self.SetHoldings(self.symbols_dict['GLD'][0], 1)
            self.add_stage_log("DECLINE", self.stage)
        
        
        elif (keimom > 0 and kei < keihistlowt) and not (self.stage == 1):
            # RECOVERY
            self.Liquidate()
            self.stage = 1
            self.SetHoldings(self.symbols_dict['MTUM'][0], 1)
            self.add_stage_log("RECOVERY", self.stage)
            
        elif (keimom > 0 and kei > keihistlowt and kei < keihistmidt) and not (self.stage == 2):
            # EARLY
            self.Liquidate()
            self.stage = 2
            self.SetHoldings(self.symbols_dict['XMMO'][0], 1)
            self.add_stage_log("EARLY", self.stage)
            
        
        elif (keimom > 0 and kei > keihistmidt and kei < keihisthight) and not (self.stage == 3):    
            # REBOUND
            self.Liquidate()
            self.stage = 3
            self.SetHoldings(self.symbols_dict['MTUM'][0], 1)
            self.add_stage_log("REBOUND", self.stage)
            
        
        elif (keimom < 0 and kei < keihisthight and kei > keihistmidt) and not (self.stage == 4):
            # LATE
            self.Liquidate()
            self.stage = 4
            self.SetHoldings(self.symbols_dict['QQQ'][0], 1)
            self.add_stage_log("LATE", self.stage)
            
        
        elif (keimom < 0 and kei < 100 and not self.Securities[self.bond].Invested):
            self.Liquidate()
            self.stage = 5
            self.SetHoldings(self.symbols_dict['TLT'][0], 1)
            self.add_stage_log("BONDS", self.stage)
            
        
        self.Plot("LeadInd", "SMA(LeadInd)", self.sma.Current.Value)
        self.Plot("LeadInd", "THRESHOLD", 100)
        self.Plot("MOMP", "MOMP(LeadInd)", self.mom.Current.Value)
        self.Plot("MOMP", "THRESHOLD", 0)
        self.Plot("Stage","recovery = 1 early = 2 rebound = 3 late = 4 decline = 5",self.stage)

    def OnEndOfAlgorithm(self):
        self.portfolio_holdings()
        self.write_stage_log()
    
    def portfolio_holdings(self): 
        self.Log("-- Portfolio --")
        for kvp in self.Portfolio:
            if kvp.Value.Invested:
                symbol = kvp.Key  #full security key
                holding = kvp.Value
                ticker = holding.Symbol.Value 
                quantity = holding.Quantity
                avgprice = holding.AveragePrice
                cost = quantity * avgprice
                price =self.Portfolio[symbol].Price
                unrealized_profit = self.Portfolio[symbol].UnrealizedProfit
                net = (unrealized_profit / cost) * 100
                s1 = '{:3.2f}'.format(avgprice) + ',' + '{:3.2f}'.format(price) + ',' + '{:3.2f}'.format(net) + ',' + '{:3.2f}'.format(unrealized_profit)   
                self.Log(ticker + "," + str(quantity) + "," + s1)
        return()
        
    def add_stage_log(self, stage, stage_num):
        d1 = str(self.Time)
        entry_date = d1[0:10]
        for key in self.symbols_dict:
            self.stage_log_df = self.stage_log_df.append({
            'stage_date': entry_date,
            'stage_num': stage_num,
            'stage': stage,
            'stage_days': 0,
            'symbol': self.symbols_dict[key][0],
            'price' : self.Securities[self.symbols_dict[key][0]].Price
            }, ignore_index = True)
       
        self.Log(d1 + "," + stage + "," + str(stage_num) )
        
        
    def write_stage_log(self):
        #Convert each stage_date and stage_num group to a single row
        df_out = self.stage_log_df.set_index(['stage_date','stage_num','stage',self.stage_log_df.groupby(['stage_date','stage_num','stage']).cumcount()+1]).unstack().sort_index(level=1, axis=1)
        df_out.columns = df_out.columns.map('{0[0]}_{0[1]}'.format)
        df_out.reset_index()
        
        #calculate the gain and net change for each symbol in the row
        for x in range(1, (len(self.symbols_dict)+1)):
            gain_col = 'gain_' + str(x)
            price_col = 'price_' + str(x)
            net_col = "net_" + str(x)
            df_out[gain_col] =  (df_out[price_col].shift(-1)- df_out[price_col]) #calculate the gain during the stage
            df_out[net_col]  = (df_out[gain_col] / df_out[price_col]) * 100 #calculate the net change during the stage
        
        df_out.fillna(0,inplace=True)
        """
        #self.stage_log_df['start_date'] = pd.to_datetime(self.stage_log_df['entry_date'], format="%Y-%m-%d")
        
        """
        
        #for each row build output log string
        print_header = True
        for index, row in df_out.iterrows():
            s1 = str(index[0])  + "," + str(index[1]) + "," + str(index[2]) + ","
            x1 = str(index[0])  + "," + str(index[1]) + "," + str(index[2]) + ","

            for x in range(1, (len(self.symbols_dict)+1)):
                symbol_col = 'symbol_' + str(x)
                gain_col = 'gain_' + str(x)
                price_col = 'price_' + str(x)
                net_col = "net_" + str(x)
                s2 = symbol_col + "," + price_col + "," + gain_col + "," + net_col + ","
                s1 = s1 + s2

                symbol_val = row[symbol_col]
                gain_val = '{:3.2f}'.format(row[gain_col])
                price_val = '{:3.2f}'.format(row[price_col])
                net_val = '{:3.2f}'.format(row[net_col])
                x2 = symbol_val + "," + price_val + "," + gain_val + "," + net_val + ","
                x1 = x1 + x2

            if print_header :
                self.Log (s1)
                print_header = False
            self.Log (x1)
         
# Quandl often doesn't use close columns so need to tell LEAN which is the "value" column.
class QuandlCustomColumns(PythonQuandl):
    def __init__(self):
        # Define ValueColumnName: cannot be None, Empty or non-existant column name
        self.ValueColumnName = "Value"
        
"""
        kei mo < 0          kei percentile   kei mo > 0
        
                                High
                                |
        Late (tech,hc, ind      |           Rebound (tech,utilities)
                                |
                                Medium
                                |
        Decline (bonds)         |           Early (energy,financials,industrials) Quad1
                                |
                                Low
                                |           Recovery (bonds)
                                0

recovery = 1
early = 2
rebound = 3
late = 4
decline = 5
"""