Overall Statistics |
Total Trades 4773 Average Win 0.12% Average Loss -0.10% Compounding Annual Return 7.811% Drawdown 12.400% Expectancy 0.025 Net Profit 14.754% Sharpe Ratio 0.536 Probabilistic Sharpe Ratio 21.585% Loss Rate 54% Win Rate 46% Profit-Loss Ratio 1.24 Alpha 0 Beta 0 Annual Standard Deviation 0.112 Annual Variance 0.012 Information Ratio 0.536 Tracking Error 0.112 Treynor Ratio 0 Total Fees $69137.42 Estimated Strategy Capacity $1100000.00 Lowest Capacity Asset EWH R735QTJ8XC9X Portfolio Turnover 60.38% |
# region imports from AlgorithmImports import * from etf_by_country import etf_by_country from country_etf import CountryETF # endregion class CountryRotationAlphaModel(AlphaModel): security_by_country = {} def __init__(self, algorithm, lookback_days): self.algorithm = algorithm self.LOOKBACK_PERIOD = timedelta(lookback_days) # Subscribe to Reg Alerts dataset self.dataset_symbol = algorithm.AddData(RegalyticsRegulatoryArticles, "REG").Symbol # Schedule model training sessions algorithm.Train(algorithm.DateRules.MonthEnd(), algorithm.TimeRules.At(23,0), self.train_model) def train_model(self): reg_history = self.algorithm.History[RegalyticsRegulatoryArticles](self.dataset_symbol, self.LOOKBACK_PERIOD) for security in self.security_by_country.values(): # Get daily returns of country ETF etf_history = self.algorithm.History(security.Symbol, self.LOOKBACK_PERIOD, Resolution.Daily, dataNormalizationMode=DataNormalizationMode.ScaledRaw) if etf_history.empty: continue security.etf.update_word_sentiments(reg_history, etf_history) def Update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]: # Only emit insights when we get a new Regalytics alert if not data.ContainsKey(self.dataset_symbol): return [] # Calculate sentiment for each country in the universe sentiments_by_symbol = {} for article in data[self.dataset_symbol]: countries = [kvp.Key for kvp in article.States] for country in countries: if country not in self.security_by_country: continue security = self.security_by_country[country] if security.Symbol not in sentiments_by_symbol: sentiments_by_symbol[security.Symbol] = [] article_sentiment = security.etf.get_sentiment(article) sentiments_by_symbol[security.Symbol].append(article_sentiment) if not sentiments_by_symbol: return [] # Aggregate the sentiment of each country across all of the articles that reference the country sentiment_by_symbol = { symbol: sum(sentiments)/len(sentiments) for symbol, sentiments in sentiments_by_symbol.items() } # Calculate portfolio weight of each country ETF base_weight = 1 / len(sentiment_by_symbol) weight_by_symbol = {} for security in self.security_by_country.values(): symbol = security.Symbol # Check if there is material news if symbol not in sentiment_by_symbol or sentiment_by_symbol[symbol] == 0: weight_by_symbol[symbol] = 0 continue # Check if the security is liquid enough to trade if not security.avg_liquidity.IsReady or base_weight * algorithm.Portfolio.TotalPortfolioValue > 0.01 * security.avg_liquidity.Current.Value: weight_by_symbol[symbol] = 0 continue long_short_bias = 1 if sentiment_by_symbol[symbol] > 0 else -1 weight_by_symbol[symbol] = long_short_bias * base_weight # Calculate a factor to scale portfolio weights so the algorithm uses 1x leverage positive_weight_sum = sum([weight for weight in weight_by_symbol.values() if weight > 0]) negative_weight_sum = sum([weight for weight in weight_by_symbol.values() if weight < 0]) scale_factor = positive_weight_sum + abs(negative_weight_sum) if scale_factor == 0: return [] # Place orders to rebalance portfolio insights = [] for symbol, weight in weight_by_symbol.items(): if weight == 0: algorithm.Insights.Cancel([symbol]) continue direction = InsightDirection.Up if weight > 0 else InsightDirection.Down insights.append(Insight.Price(symbol, timedelta(7), direction, weight=weight/scale_factor)) return insights def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None: for security in changes.AddedSecurities: countries = [country for country, ticker in etf_by_country.items() if ticker == security.Symbol.Value] if countries: country = countries[0] security.etf = CountryETF(country, security.Symbol) # Create indicator to track average liquidity security.avg_liquidity = SimpleMovingAverage(21*3) bars = algorithm.History[TradeBar](security.Symbol, security.avg_liquidity.Period, Resolution.Daily, dataNormalizationMode=DataNormalizationMode.ScaledRaw) for bar in bars: security.avg_liquidity.Update(bar.EndTime, bar.Close*bar.Volume) # Create a consolidator to update to the indicator consolidator = TradeBarConsolidator(timedelta(days=1)) consolidator.DataConsolidated += lambda _, consolidated_bar: algorithm.Securities[consolidated_bar.Symbol].avg_liquidity.Update(consolidated_bar.EndTime, consolidated_bar.Close*consolidated_bar.Volume) algorithm.SubscriptionManager.AddConsolidator(security.Symbol, consolidator) self.security_by_country[country] = security self.train_model()
#region imports from AlgorithmImports import * #endregion from nltk.corpus import stopwords from nltk.tokenize import word_tokenize class CountryETF: STOP_WORDS = set(stopwords.words('english')) def __init__(self, country, symbol): self.country = country self.symbol = symbol self.future_returns_by_word = {} def update_word_sentiments(self, reg_history, etf_history): daily_returns = etf_history.loc[self.symbol]['open'].pct_change()[1:] # Get articles that are tagged with this country future_returns_by_word = {} for articles in reg_history: future_returns = daily_returns.loc[daily_returns.index > articles.Time] if len(future_returns) < 2: continue future_return = future_returns.iloc[1] for article in articles: if self.country not in [kvp.Key for kvp in article.States]: continue filtered_words = self.filter_words(article) for word in filtered_words: if word not in future_returns_by_word: future_returns_by_word[word] = [] future_returns_by_word[word].append(future_return) self.future_returns_by_word = { word: sum(future_returns)/len(future_returns) for word, future_returns in future_returns_by_word.items() } def filter_words(self, article): word_tokens = word_tokenize(article.Title) return list(set([w for w in word_tokens if not w.lower() in self.STOP_WORDS and w not in [",", ".", "-"]])) def get_sentiment(self, article): if len(self.future_returns_by_word) == 0: return 0 filtered_words = self.filter_words(article) future_returns = [] for word in filtered_words: if word not in self.future_returns_by_word: continue future_returns.append(self.future_returns_by_word[word]) if len(future_returns) == 0: return 0 return sum(future_returns)/len(future_returns)
#region imports from AlgorithmImports import * #endregion etf_by_country = { "Croatia" : "FM", "Singapore" : "EWS", "Hungary" : "CRAK", "Denmark" : "EDEN", "Norway" : "NORW", "Saudi Arabia" : "FLSA", "United Kingdom" : "EWUS", "Republic of the Philippines" : "EPHE", "Romania" : "FM", "Sweden" : "EWD", "France" : "FLFR", "Brazil" : "EWZS", "Luxembourg" : "SLX", "Japan" : "DFJ", "China" : "CHIU", "Libya" : "BRF", "Pakistan" : "PAK", "Germany" : "FLGR", "Sri Lanka" : "FM", "Greece" : "GREK", "Finland" : "EFNL", "Ireland" : "EIRL", "United Arab Emirates" : "UAE", "Qatar" : "QAT", "The Bahamas" : "RNSC", "Vietnam" : "VNAM", "Czech Republic" : "NLR", "Portugal" : "PGAL", "Austria" : "EWO", #"Russia" : "FLRU", "Turkey" : "TUR", "South Korea" : "FLKR", "Hong Kong" : "EWH", "Belgium" : "EWK", "Cyprus" : "SIL", "Switzerland" : "EWL", "Thailand" : "THD", "United States": "SPY", "Colombia" : "GXG", "Malta" : "BETZ", "Canada" : "FLCA", "Israel" : "EIS", "Indonesia" : "EIDO", "Spain" : "EWP", "Malaysia" : "EWM", "Ukraine" : "TLTE", "Poland" : "EPOL", "Argentina" : "ARGT", "Estonia" : "FM", "Taiwan" : "FLTW", "Netherlands" : "EWN", "Mexico" : "FLMX", "Australia" : "EWA", "Italy" : "FLIY", "Egypt" : "EGPT" } # Croatia: https://etfdb.com/country/croatia/ (FM 0.37%) # Singapore: https://etfdb.com/country/singapore/ (EWS 91.83%) # Afghanistan : None # Hungary: https://etfdb.com/country/hungary/ (CRAK 4.55%) # Denmark: https://etfdb.com/country/denmark/ (EDEN 97.84%) # Norway: https://etfdb.com/country/norway/ (NORW 90.11%) # Saudi Arabia: https://etfdb.com/country/saudi-arabia/ (FLSA 100%) # United Kingdom: https://etfdb.com/country/united-kingdom/ (EWUS 95.65%) # Republic of the Philippines: https://etfdb.com/country/philippines/ (EPHE 99.59%) # Romania: https://etfdb.com/country/romania/ (FM 5.78%) # Sweden: https://etfdb.com/country/sweden/ (EWD 91.48%) # France: https://etfdb.com/country/france/ (FLFR 94.29%) # Brazil: https://etfdb.com/country/brazil/ (EWZS 96.91%) # Luxembourg: https://etfdb.com/country/luxembourg/ (SLX 5.65%) # Japan: https://etfdb.com/country/japan/ (DFJ 99.93%) # China: https://etfdb.com/country/china/ (CHIU 100%) # Libya: https://etfdb.com/country/libya/ (BRF 0.4%) # Pakistan: https://etfdb.com/country/pakistan/ (PAK 85.82%) # Germany: https://etfdb.com/country/germany/ (FLGR 97.7%) # Sri Lanka: https://etfdb.com/country/sri-lanka/ (FM 1.11%) # Greece: https://etfdb.com/country/greece/ (GREK 94.75%) # Tajikistan: None # Finland: https://etfdb.com/country/finland/ (EFNL 99.42%) # Ireland: https://etfdb.com/country/ireland/ (EIRL 72.84%) # United Arab Emirates: https://etfdb.com/country/united-arab-emirates/ (UAE 96.32%) # Qatar : https://etfdb.com/country/qatar/ (QAT 99.96%) # The Bahamas : https://etfdb.com/country/bahamas/ (RNSC 0.35%) # Iceland : None # Vietnam : https://etfdb.com/country/vietnam/ (VNAM 100%; VNM has more history but 65.55% weight) # Republic of Kosovo : None # Latvia : None # Czech Republic : https://etfdb.com/country/czech-republic/ (NLR 4.29%) # Slovakia : None # Portugal : https://etfdb.com/country/portugal/ (PGAL 95.59%) # Austria : https://etfdb.com/country/austria/ (EWO 92.18%) # Russia : https://etfdb.com/country/russia/ (FLRU 93.65%; Missing data for last year) # Turkey : https://etfdb.com/country/turkey/ (TUR 92.79%) # South Korea : https://etfdb.com/country/south-korea/ (FLKR 99.44%) # Slovenia : None # Hong Kong : https://etfdb.com/country/hong-kong/ (EWH 85.66%) # Belgium : https://etfdb.com/country/belgium/ (EWK 83.52%) # Cyprus : https://etfdb.com/country/cyprus/ (SIL 12.88%) # Switzerland : https://etfdb.com/country/switzerland/ (EWL 99.27%) # Bulgaria : None # Thailand : https://etfdb.com/country/thailand/ (THD 98.14%) # United States : https://etfdb.com/country/united-states/ (SPY???) # Colombia : https://etfdb.com/country/colombia/ (GXG 92.79%) # Botswana : None # Malta : https://etfdb.com/country/malta/ (BETZ 10.29%) # Canada : https://etfdb.com/country/canada/ (FLCA 89.45%; There is also EWC iShares MSCI Canada) # Barbados : None # Israel : https://etfdb.com/country/israel/ (EIS 79.68%) # Indonesia : https://etfdb.com/country/indonesia/ (EIDO 73.15%) # Albania : None # Spain : https://etfdb.com/country/spain/ (EWP 93.33%) # Malaysia : https://etfdb.com/country/malaysia/ (EWM 95.62%) # Ukraine : https://etfdb.com/country/ukraine/ (TLTE 0.11%) # Poland : https://etfdb.com/country/poland/ (EPOL 87.85%) # Argentina : https://etfdb.com/country/argentina/ (ARGT 52.07%) # Estonia : https://etfdb.com/country/estonia/ (FM 0.65%) # Taiwan : https://etfdb.com/country/taiwan/ (FLTW 98.04%; There is also EWT iShares MSCI Taiwan) # Solomon Islands : None # Netherlands : https://etfdb.com/country/netherlands/ (EWN 90.03%) # Mexico : https://etfdb.com/country/mexico/ (FLMX 87.86%; There is also EWW iShares MSCI Mexico) # Australia : https://etfdb.com/country/australia/ (EWA 94.71%) # Italy : https://etfdb.com/country/italy/ (FLIY 88.50%; There is also EWI iShares MSCI Italy) # Lithuania : None # Egypt : https://etfdb.com/country/egypt/ (EGPT 93.56%)
# region imports from AlgorithmImports import * from universe import CountryEtfUniverse from alpha import CountryRotationAlphaModel from etf_by_country import etf_by_country # endregion class CountryRotationAlgorithm(QCAlgorithm): undesired_symbols_from_previous_deployment = [] checked_symbols_from_previous_deployment = False def Initialize(self): self.SetStartDate(2021, 9, 1) self.SetEndDate(2023, 7, 1) self.SetCash(1_000_000) # Configure algorithm settings self.Settings.MinimumOrderMarginPortfolioPercentage = 0 self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw self.AddUniverseSelection(CountryEtfUniverse()) self.AddAlpha(CountryRotationAlphaModel(self, self.GetParameter("lookback_days", 90))) self.Settings.RebalancePortfolioOnSecurityChanges = False self.Settings.RebalancePortfolioOnInsightChanges = False self.SetPortfolioConstruction(InsightWeightingPortfolioConstructionModel(self.rebalance_func)) self.AddRiskManagement(NullRiskManagementModel()) self.SetExecution(ImmediateExecutionModel()) self.SetWarmUp(timedelta(31)) def rebalance_func(self, time): if self.IsWarmingUp: return None # Rebalance when a Regalytics article references one of the countries in the universe for kvp in self.CurrentSlice.Get[RegalyticsRegulatoryArticles](): articles = kvp.Value for article in articles: countries = [kvp.Key for kvp in article.States] for country in countries: if country in etf_by_country: return time return None def OnData(self, data): # Exit positions that aren't backed by existing insights. # If you don't want this behavior, delete this method definition. if not self.IsWarmingUp and not self.checked_symbols_from_previous_deployment: for security_holding in self.Portfolio.Values: if not security_holding.Invested: continue symbol = security_holding.Symbol if not self.Insights.HasActiveInsights(symbol, self.UtcTime): self.undesired_symbols_from_previous_deployment.append(symbol) self.checked_symbols_from_previous_deployment = True for symbol in self.undesired_symbols_from_previous_deployment[:]: if self.IsMarketOpen(symbol): self.Liquidate(symbol, tag="Holding from previous deployment that's no longer desired") self.undesired_symbols_from_previous_deployment.remove(symbol)
# region imports from AlgorithmImports import * from etf_by_country import etf_by_country # endregion class CountryEtfUniverse(ManualUniverseSelectionModel): def __init__(self): symbols = [Symbol.Create(ticker, SecurityType.Equity, Market.USA) for ticker in etf_by_country.values()] super().__init__(symbols)