| 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)