Overall Statistics |
Total Trades 1008 Average Win 0.43% Average Loss -0.34% Compounding Annual Return 1.468% Drawdown 15.400% Expectancy 0.173 Net Profit 32.579% Sharpe Ratio 0.263 Probabilistic Sharpe Ratio 0.015% Loss Rate 48% Win Rate 52% Profit-Loss Ratio 1.26 Alpha 0 Beta 0.134 Annual Standard Deviation 0.049 Annual Variance 0.002 Information Ratio -0.519 Tracking Error 0.155 Treynor Ratio 0.097 Total Fees $0.00 Estimated Strategy Capacity $1800000.00 Lowest Capacity Asset AUDUSD 8G |
""" Useful References: -QC.Securities Namespace link https://www.quantconnect.com/lean/documentation/topic26198.html -QC SecurityExchange Class Members https://www.quantconnect.com/lean/documentation/topic26905.html -QC SecurityExchangeHours Class Members https://www.quantconnect.com/lean/documentation/topic26922.html -Data Consolidation Example Algo https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/DataConsolidationAlgorithm.py -Link to list of built-in indicators for QC (from Github) https://github.com/QuantConnect/Lean/tree/master/Indicators """ ################################################################################ # BACKTEST DETAILS CASH = 1e6 START_DATE = '05-01-2002' #'05-01-1971' # MM-DD-YYYY format END_DATE = None #'12-31-1971' # MM-DD-YYYY format (or None for to date) TIMEZONE = 'US/Central' # e.g. 'US/Eastern', 'US/Central', 'US/Pacific' # Lookback period in months for fundamental indices trend scores TREND_PERIOD = 12 # Turn on/off using the simplified version basing the desired country weights on # blended scores only and not the rankings against the other countries # When using this logic, it is essentially looking at absolute momentum rather than cross-sectional momentum SIMPLIFIED_WEIGHTS = False # Set the currency exchange to use CURRENCY_EXCHANGE = 'Oanda' # either 'Oanda' or 'FXCM' # Set the benchmark - must be an equity/etf BENCHMARK = "SPY" # Set your quandl authorization code QUANDL_AUTH_CODE = 'ZYmz4yBbyvxrejZ44hKo' # DEFINE COUNTRIES TO TRACK COUNTRIES = [ 'Australia', 'Canada', # 'France', # 'Germany', # 'Italy', 'Europe', 'Japan', 'UK', # 'USA' ] # DEFINE THE WEIGHTS FOR PORTFOLIO CONSTRUCTION WEIGHT_EA = 0.5 # economic activity weight WEIGHT_IN = 0.5 # inflation weight # DEFINE CURRENCIES TO TRADE TRADE_CURRENCIES = True # turn on/off using this data CURRENCIES = {} CURRENCIES['Australia'] = 'AUDUSD' # QC oanda data starts 2002-05-05, fxcm starts 2007-03-30 CURRENCIES['Canada'] = 'USDCAD' # QC oanda data starts 2002-05-06, fxcm starts 2007-03-30 CURRENCIES['Europe'] = 'EURUSD' # QC oanda data starts 2002-05-05, fxcm starts 2007-03-30 CURRENCIES['Japan'] = 'USDJPY' # QC oanda data starts 2002-05-05, fxcm starts 2007-03-30 CURRENCIES['UK'] = 'GBPUSD' # QC oanda data starts 2002-05-05, fxcm starts 2007-03-30 # Instead trade based on FRED exchange rates TRADE_FRED_RATES = False # turn on/off using this data FRED = {} # Enter FRED rate codes below FRED['Australia'] = 'DEXUSAL' FRED['Canada'] = 'DEXCAUS' FRED['Europe'] = 'DEXUSEU' FRED['Japan'] = 'DEXJPUS' FRED['UK'] = 'DEXUSUK' # Enter CSV download links below # The countries need to be ALL CAPS for this FRED_LINKS = {} FRED_LINKS['AUSTRALIA'] = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSaLzwcbVI_2PIQwha5IaIPk8oCymJq5fZtAw-VS20mOty-iEY53MANo1GliCos8TaiLr48RoD_WwQ5/pub?gid=367664124&single=true&output=csv' FRED_LINKS['CANADA'] = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSaLzwcbVI_2PIQwha5IaIPk8oCymJq5fZtAw-VS20mOty-iEY53MANo1GliCos8TaiLr48RoD_WwQ5/pub?gid=0&single=true&output=csv' FRED_LINKS['EUROPE'] = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSaLzwcbVI_2PIQwha5IaIPk8oCymJq5fZtAw-VS20mOty-iEY53MANo1GliCos8TaiLr48RoD_WwQ5/pub?gid=748699328&single=true&output=csv' FRED_LINKS['JAPAN'] = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSaLzwcbVI_2PIQwha5IaIPk8oCymJq5fZtAw-VS20mOty-iEY53MANo1GliCos8TaiLr48RoD_WwQ5/pub?gid=1378346043&single=true&output=csv' FRED_LINKS['UK'] = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSaLzwcbVI_2PIQwha5IaIPk8oCymJq5fZtAw-VS20mOty-iEY53MANo1GliCos8TaiLr48RoD_WwQ5/pub?gid=1282896252&single=true&output=csv' # QUANDL DATASET CODES # Country codes for datasets CODE = {} CODE['Australia'] = 'AUS' CODE['Canada'] = 'CAN' CODE['Europe'] = 'OECDE' # CODE['France'] = 'FRA' # CODE['Germany'] = 'DEU' # CODE['Italy'] = 'ITA' CODE['Japan'] = 'JPN' CODE['UK'] = 'GBR' # CODE['USA'] = 'USA' # Monthly Industrial Production IP = {} for country in COUNTRIES: IP[country] = 'OECD/KEI_PRINTO01_' + CODE[country] + '_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_PRINTO01_CAN_ST_M-Industrial-production-s-a-Canada-Level-ratio-or-index-Monthly # IP['Canada'] = 'OECD/KEI_PRINTO01_CAN_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_PRINTO01_FRA_ST_M-Industrial-production-s-a-France-Level-ratio-or-index-Monthly # IP['France'] = 'OECD/KEI_PRINTO01_FRA_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_PRINTO01_DEU_ST_M-Industrial-production-s-a-Germany-Level-ratio-or-index-Monthly # IP['Germany'] = 'OECD/KEI_PRINTO01_DEU_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_PRINTO01_ITA_ST_M-Industrial-production-s-a-Italy-Level-ratio-or-index-Monthly # IP['Italy'] = 'OECD/KEI_PRINTO01_ITA_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_PRINTO01_JPN_ST_M-Industrial-production-s-a-Japan-Level-ratio-or-index-Monthly # IP['Japan'] = 'OECD/KEI_PRINTO01_JPN_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_PRINTO01_GBR_ST_M-Industrial-production-s-a-United-Kingdom-Level-ratio-or-index-Monthly # IP['UK'] = 'OECD/KEI_PRINTO01_GBR_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_PRINTO01_USA_ST_M-Industrial-production-s-a-United-States-Level-ratio-or-index-Monthly # IP['USA'] = 'OECD/KEI_PRINTO01_USA_ST_M' # Monthly Retail Sales RS = {} for country in COUNTRIES: RS[country] = 'OECD/KEI_SLRTTO01_' + CODE[country] + '_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_SLRTTO01_CAN_ST_M-Retail-trade-Volume-s-a-Canada-Level-ratio-or-index-Monthly # RS['Canada'] = 'OECD/KEI_SLRTTO01_CAN_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_SLRTTO01_USA_ST_M-Retail-trade-Volume-s-a-United-States-Level-ratio-or-index-Monthly # RS['USA'] = 'OECD/KEI_SLRTTO01_USA_ST_M' # Monthly Unemployment UE = {} for country in COUNTRIES: UE[country] = 'OECD/STLABOUR_' + CODE[country] + '_LFHUTTTT_ST_M' # # REF: https://www.quandl.com/data/OECD/STLABOUR_CAN_LFHUTTTT_ST_M-Canada-Unemployment-Monthly-Total-All-Persons-Level-Rate-Or-Quantity-Series # UE['Canada'] = 'OECD/STLABOUR_CAN_LFHUTTTT_ST_M' # # REF: https://www.quandl.com/data/OECD/STLABOUR_USA_LFHUTTTT_ST_M-United-States-Harmonised-Unemployment-Monthly-Total-All-Persons-Level-Rate-Or-Quantity-Series # UE['USA'] = 'OECD/STLABOUR_USA_LFHUTTTT_ST_M' # Monthly Consumer Prices CP = {} for country in COUNTRIES: CP[country] = 'OECD/KEI_CPALTT01_' + CODE[country] + '_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_CPALTT01_CAN_ST_M-Consumer-prices-all-items-Canada-Level-ratio-or-index-Monthly # CP['Canada'] = 'OECD/KEI_CPALTT01_CAN_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_CPALTT01_USA_ST_M-Consumer-prices-all-items-United-States-Level-ratio-or-index-Monthly # CP['USA'] = 'OECD/KEI_CPALTT01_USA_ST_M' # Monthly Producer Prices PP = {} for country in COUNTRIES: PP[country] = 'OECD/KEI_PIEAMP01_' + CODE[country] + '_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_PIEAMP01_CAN_ST_M-Producer-prices-Manufacturing-Canada-Level-ratio-or-index-Monthly # PP['Canada'] = 'OECD/KEI_PIEAMP01_CAN_ST_M' # # REF: https://www.quandl.com/data/OECD/KEI_PIEAMP01_USA_ST_M-Producer-prices-Manufacturing-United-States-Level-ratio-or-index-Monthly # PP['USA'] = 'OECD/KEI_PIEAMP01_USA_ST_M' # Set the number of warmup days CALENDAR_WARMUP_DAYS = 50 # Turn on/off specific logs PRINT_WARMUP = False # print logs during warmup for debugging # PRINT_BARS = False # print desired OHLC bars for debugging PRINT_SIGNALS = True # print desired trading signals ################################################################################ ############################ END OF ALL USER INPUTS ############################ ################################################################################ # VALIDATE USER INPUTS - DO NOT CHANGE BELOW #------------------------------------------------------------------------------- import datetime as DT # Verify start date try: START_DATE_DT = DT.datetime.strptime(START_DATE, '%m-%d-%Y') except: raise ValueError("Invalid START_DATE format ({}). Must be in MM-DD-YYYY " "format.".format(START_DATE)) # Verify end date try: if END_DATE: END_DATE_DT = DT.datetime.strptime(END_DATE, '%m-%d-%Y') except: raise ValueError("Invalid END_DATE format ({}). Must be in MM-DD-YYYY " "format or set to None to run to date.".format(END_DATE)) # Verify CURRENCY_EXCHANGE if CURRENCY_EXCHANGE not in ['Oanda', 'FXCM']: raise ValueError("Invalid CURRENCY_EXCHANGE ({}). Must be 'Oanda' or " "'FXCM'.".format(CURRENCY_EXCHANGE))
############################################################################### # Standard library imports import datetime as DT # from datetime import timedelta # from datetime import date from dateutil.parser import parse import decimal import math # import numpy as np # import pandas as pd import pytz # QuantConnect specific imports import QuantConnect as qc from QuantConnect.Python import PythonQuandl # Import from files from notes_and_inputs import * ################################################################################ class CustomTradingStrategy(QCAlgorithm): def Initialize(self): """Initialize algorithm.""" # Set backtest details self.SetStartDate( START_DATE_DT.year, START_DATE_DT.month, START_DATE_DT.day) if END_DATE: self.SetEndDate( END_DATE_DT.year, END_DATE_DT.month, END_DATE_DT.day) self.SetCash(CASH) self.SetTimeZone(TIMEZONE) # Set your personal token necessary for restricted dataset Quandl.SetAuthCode(QUANDL_AUTH_CODE) # Set up dictionaries to hold all Quandl custom datasets self.industrial_production = {} self.retail_sales = {} self.unemployment = {} self.consumer_prices = {} self.producer_prices = {} self.symbols = {} # Loop through all desired countries to track for country in COUNTRIES: # Get all industrial production datasets self.industrial_production[country] = self.AddData( QuandlCustomColumns, IP[country], Resolution.Daily, TimeZones.NewYork ).Symbol # Get all retail sales datasets self.retail_sales[country] = self.AddData( QuandlCustomColumns, RS[country], Resolution.Daily, TimeZones.NewYork ).Symbol # Get all unemployment datasets self.unemployment[country] = self.AddData( QuandlCustomColumns, UE[country], Resolution.Daily, TimeZones.NewYork ).Symbol # Get all consumer prices datasets self.consumer_prices[country] = self.AddData( QuandlCustomColumns, CP[country], Resolution.Daily, TimeZones.NewYork ).Symbol # Get all producer prices datasets self.producer_prices[country] = self.AddData( QuandlCustomColumns, PP[country], Resolution.Daily, TimeZones.NewYork ).Symbol # Add data to trade if TRADE_CURRENCIES: # Add built-in currency data from the desired exchange if CURRENCY_EXCHANGE == 'Oanda': # either 'Oanda' or 'FXCM' exchange = Market.Oanda else: exchange = Market.FXCM self.symbols[country] = \ self.AddForex(CURRENCIES[country], Resolution.Hour, exchange).Symbol elif TRADE_FRED_RATES: # Add FRED rate data self.symbols[country] =\ self.AddData(MyCustomData, country, Resolution.Daily).Symbol # Set benchmark for scheduling purposes self.bm = self.AddEquity(BENCHMARK, Resolution.Hour).Symbol # Rebalance the portfolio at the end of every month at market close self.Schedule.On( self.DateRules.MonthEnd(self.bm), self.TimeRules.BeforeMarketClose(self.bm, 0), self.Rebalance) # Initialize required variables self.portfolio_targets = [] try: self.TREND_PERIOD = int(self.GetParameter("TREND_PERIOD")) except: self.TREND_PERIOD = TREND_PERIOD # Warm up the indicators prior to the start date self.SetWarmUp(timedelta(CALENDAR_WARMUP_DAYS)) #------------------------------------------------------------------------------- def OnData(self, data): """Built-in event handler for new data.""" # Check for new portfolio targets if len(self.portfolio_targets) > 0: # Update portfolio self.SetHoldings(self.portfolio_targets) # Empty portfolio targets list self.portfolio_targets = [] #------------------------------------------------------------------------------- def Rebalance(self): """Rebalance the portfolio.""" # First calculate the economic momentum scores and get the desired # country weights self.CalculateScores() # Loop through each country and get the portfolio targets for each self.portfolio_targets = [] for country in COUNTRIES: # Get the desired country weight if country in self.weights: target_weight = self.weights[country] else: target_weight = 0 # Get the instrument to trade for the country symbol = self.symbols[country] # # Verify that data is available for the symbol # if not self.Securities[symbol].HasData: # continue # Check if USD is the base or quote currency # 'USD' is the base currency if at beginning of symbol usd_base = False if TRADE_CURRENCIES and 'USD' == str(symbol)[:3]: usd_base = True elif TRADE_FRED_RATES: # Get the FRED rate code fred_code = str(FRED[country])[3:] # remove 'DEX' at beginning if 'US' == fred_code[:2]: usd_base = True if usd_base: # Want to sell to go long the country currency # So inverse the target weight target_weight = -target_weight # else: # 'USD' is the quote currency # Set the target allocation for the currency self.portfolio_targets.append( PortfolioTarget(symbol, target_weight) ) #------------------------------------------------------------------------------- def CalculateScores(self): """Calculate the economic momentum scores for all countries.""" # Create dictionaries to store country specific index scores ea_index = {} # economic activity index in_index = {} # inflation index weight = {} # overall desired weight for each country # Loop through each country for country in COUNTRIES: # Get history for all 5 of the fundamental factors length = (TREND_PERIOD+1+6)*31 # get an extra 5-6 months of data hist_ip = self.History([self.industrial_production[country]], length) hist_rs = self.History([self.retail_sales[country]], length) hist_ue = self.History([self.unemployment[country]], length) hist_cp = self.History([self.consumer_prices[country]], length) hist_pp = self.History([self.producer_prices[country]], length) # we can test different lookback periods in QC's optimization feature, but it takes some extra compute power (discuss with Jon later - need to just get it working for now) # Get the economic activity index if min([len(hist_ip), len(hist_rs), len(hist_ue)]) > TREND_PERIOD+1: # Get the industrial production log growth rate ip = math.log(hist_ip.iloc[-1].value) \ -math.log(hist_ip.iloc[-TREND_PERIOD-1].value) # Get the retail sales log growth rate rs = math.log(hist_rs.iloc[-1].value) \ -math.log(hist_rs.iloc[-TREND_PERIOD-1].value) # Get the unemployment log growth rate ue = math.log(hist_ue.iloc[-1].value) \ -math.log(hist_ue.iloc[-TREND_PERIOD-1].value) # Calculate the economic activity index # equal weighted average of factors above ea_index[country] = (ip+rs+ue)/3.0 else: ea_index[country] = 0 # Get the inflation index if min([len(hist_cp), len(hist_pp)]) > TREND_PERIOD+1: # Get the consumer prices log growth rate cp = math.log(hist_cp.iloc[-1].value) \ -math.log(hist_cp.iloc[-TREND_PERIOD-1].value) pp = math.log(hist_pp.iloc[-1].value) \ -math.log(hist_pp.iloc[-TREND_PERIOD-1].value) # Calculate the inflation index # equal weighted average of factors above in_index[country] = (cp+pp)/2.0 else: in_index[country] = 0 # Check if SIMPLIFIED_WEIGHTS is used if SIMPLIFIED_WEIGHTS: # Loop through each country for country in COUNTRIES: # Get the overall desired weight based on raw blended score blended_score = WEIGHT_EA*ea_index[country] + WEIGHT_IN*in_index[country] if blended_score > 0: weight[country] = 1 elif blended_score < 0: weight[country] = -1 else: weight[country] = 0 else: # Now get rankings for each index # reverse=True for those we want to sort in descending order sorted_ea = sorted(ea_index.items(), key=lambda x: x[1], reverse=True) sorted_in = sorted(in_index.items(), key=lambda x: x[1], reverse=True) ea_ranks = list(range(1,len(sorted_ea)+1)) ea_avg_rank = sum(ea_ranks)/len(ea_ranks) in_ranks = list(range(1,len(sorted_in)+1)) in_avg_rank = sum(in_ranks)/len(in_ranks) # Loop through each country for country in COUNTRIES: # paper mentions a 3month lag on using this data for the portfolio # Get the economic activity score if country in ea_index: rank_ea = [x[0] for x in sorted_ea].index(country)+1 inverse_rank_ea = ea_ranks[-rank_ea] # this formula doesn't make sense to me - discuss with Jon score_ea = inverse_rank_ea - ea_avg_rank # sum([x[0] for x in sorted_ea])/len(sorted_ea) else: score_ea = 0 # Get the inflation score if country in in_index: rank_in = [x[0] for x in sorted_in].index(country)+1 inverse_rank_in = in_ranks[-rank_in] # this formula doesn't make sense to me - discuss with Jon score_in = inverse_rank_in - in_avg_rank # sum([x[0] for x in sorted_in])/len(sorted_in) else: score_in = 0 # Get the overall desired weight weight[country] = WEIGHT_EA*score_ea + WEIGHT_IN*score_in # NOTE: weight adds up to 0 / longs + shorts # Make sure absolute values of long and short weights add to 1 if sum([abs(x[1]) for x in weight.items()]) != 1: # Rescale so absolute value of long weights and short weights add to 1 weights_sum = sum([abs(x[1]) for x in weight.items()]) if weights_sum > 0: factor = 1.0/weights_sum else: factor = 0 for country in COUNTRIES: weight[country] = factor*weight[country] # Save weights and return self.weights = weight return #------------------------------------------------------------------------------- def CustomSecurityInitializer(self, security): """ Define models to be used for securities as they are added to the algorithm's universe. """ # Define the data normalization mode security.SetDataNormalizationMode(DataNormalizationMode.Adjusted) # Define the fee model to use for the security # security.SetFeeModel() # Define the slippage model to use for the security # security.SetSlippageModel() # Define the fill model to use for the security # security.SetFillModel() # Define the buying power model to use for the security # security.SetBuyingPowerModel() ################################################################################ class QuandlCustomColumns(PythonQuandl): """ Quandl often doesn't use close columns so need to tell LEAN which is the "value" column. REF: https://www.quantconnect.com/forum/discussion/11566/kei-based-strategy/p1 """ def __init__(self): # Define ValueColumnName: cannot be None, Empty or non-existant column name self.ValueColumnName = "Value" ################################################################################ class MyCustomData(PythonData): """ Custom Data Class REF: https://www.quantconnect.com/forum/discussion/4079/python-best-practise-for-using-consolidator-on-custom-data/p1 """ def GetSource(self, config, date, isLiveMode): # Get file specific to the asset symbol country = config.Symbol.Value # Get the custom url link for the FRED data for the desired country file = FRED_LINKS[country] return SubscriptionDataSource( file, SubscriptionTransportMedium.RemoteFile) def Reader(self, config, line, date, isLiveMode): # Create new object asset = MyCustomData() asset.Symbol = config.Symbol # If first character is not a digit, return if not (line.strip() and line[0].isdigit()): return None # try: # Example File Format: # Date Price # 2017-01-02 09:02:00+01:00 64.88 # Comma separated file, so split data row by comma data = line.split(',') # If price is invalid, return None try: value = float(data[1]) except: return None # Return None if value is 0 if value == 0: return None # Set time of data at close 16:00 / 57600s = 16hr*60min/hr*60s/min # Set time of data at close 16:01 / 57660s = 16hr*60min/hr*60s/min # asset.Time = parse(data[0]) #+ timedelta(seconds=57660) asset.Time = DT.datetime.strptime(data[0], "%m/%d/%Y") # Set the value used for filling positions asset.Value = value #decimal.Decimal(data[1]) asset["Close"] = value return asset # except ValueError: # # Do nothing # return None