Overall Statistics |
Total Orders 219 Average Win 0.54% Average Loss -0.39% Compounding Annual Return 19.353% Drawdown 15.600% Expectancy 0.502 Start Equity 1000000 End Equity 1382226.13 Net Profit 38.223% Sharpe Ratio 0.724 Sortino Ratio 1.071 Probabilistic Sharpe Ratio 37.727% Loss Rate 37% Win Rate 63% Profit-Loss Ratio 1.40 Alpha 0 Beta 0 Annual Standard Deviation 0.168 Annual Variance 0.028 Information Ratio 0.868 Tracking Error 0.168 Treynor Ratio 0 Total Fees $948.05 Estimated Strategy Capacity $120000000.00 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 9.73% |
# 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.add_data(RegalyticsRegulatoryArticles, "REG").symbol # Schedule model training sessions algorithm.train(algorithm.date_rules.month_end(), algorithm.time_rules.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, data_normalization_mode=DataNormalizationMode.SCALED_RAW) 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.contains_key(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.is_ready or base_weight * algorithm.portfolio.total_portfolio_value > 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 on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None: for security in changes.added_securities: 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, data_normalization_mode=DataNormalizationMode.SCALED_RAW) for bar in bars: security.avg_liquidity.update(bar.end_time, bar.close*bar.volume) # Create a consolidator to update to the indicator consolidator = TradeBarConsolidator(timedelta(days=1)) consolidator.data_consolidated += lambda _, consolidated_bar: algorithm.securities[consolidated_bar.symbol].avg_liquidity.update(consolidated_bar.end_time, consolidated_bar.close*consolidated_bar.volume) algorithm.subscription_manager.add_consolidator(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.set_start_date(2021, 9, 1) self.set_end_date(2023, 7, 1) self.set_cash(1_000_000) # Configure algorithm settings self.settings.minimum_order_margin_portfolio_percentage = 0 self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN) self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW self.add_universe_selection(CountryEtfUniverse()) self.add_alpha(CountryRotationAlphaModel(self, self.get_parameter("lookback_days", 90))) self.settings.rebalance_portfolio_on_security_changes = False self.settings.rebalance_portfolio_on_insight_changes = False self.set_portfolio_construction(InsightWeightingPortfolioConstructionModel(self.rebalance_func)) self.add_risk_management(NullRiskManagementModel()) self.set_execution(ImmediateExecutionModel()) self.set_warm_up(timedelta(31)) def rebalance_func(self, time): if self.is_warming_up: return None # Rebalance when a Regalytics article references one of the countries in the universe for kvp in self.current_slice.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 on_data(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.is_warming_up 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.has_active_insights(symbol, self.utc_time): 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.is_market_open(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)