Overall Statistics |
Total Orders 2480 Average Win 0.13% Average Loss -0.14% Compounding Annual Return -12.329% Drawdown 30.500% Expectancy -0.171 Start Equity 100000 End Equity 73529.89 Net Profit -26.470% Sharpe Ratio -1.882 Sortino Ratio -1.075 Probabilistic Sharpe Ratio 0.001% Loss Rate 56% Win Rate 44% Profit-Loss Ratio 0.90 Alpha -0.123 Beta 0.077 Annual Standard Deviation 0.065 Annual Variance 0.004 Information Ratio -0.807 Tracking Error 0.156 Treynor Ratio -1.588 Total Fees $5069.43 Estimated Strategy Capacity $880000.00 Lowest Capacity Asset FLGC XOE50BT1KVAD Portfolio Turnover 28.39% |
#region imports from AlgorithmImports import * from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel #endregion class DrugManufacturerUniverseSelection(FundamentalUniverseSelectionModel): """ This universe selection model contain securities in the drug manufacturing industry group. """ def __init__(self, coarse_size=2500, fine_size=50): self._coarse_size = coarse_size self._fine_size = fine_size super().__init__(True) def select_coarse(self, algorithm, coarse): """ Coarse universe selection is called each day at midnight. Input: - algorithm Algorithm instance running the backtest - coarse List of CoarseFundamental objects Returns the symbols that have fundamental data and the largest dollar volume. """ has_fundamentals = [c for c in coarse if c.has_fundamental_data] sorted_by_dollar_volume = sorted(has_fundamentals, key=lambda c: c.dollar_volume, reverse=True) return [ x.symbol for x in sorted_by_dollar_volume[:self._coarse_size] ] def select_fine(self, algorithm, fine): """ Fine universe selection is performed each day at midnight after `SelectCoarse`. Input: - algorithm Algorithm instance running the backtest - fine List of FineFundamental objects that result from `SelectCoarse` processing Returns a list of symbols that are in the drug manufacturing industry and have the greatest PE ratios. """ drug_manufacturers = [f for f in fine if f.asset_classification.morningstar_industry_group_code == MorningstarIndustryGroupCode.DRUG_MANUFACTURERS] sorted_by_pe = sorted(drug_manufacturers, key=lambda f: f.valuation_ratios.pe_ratio, reverse=True) return [ x.symbol for x in sorted_by_pe[:self._fine_size] ]
#region imports from AlgorithmImports import * from SentimentByPhrase import SentimentByPhrase from nltk.util import ngrams #endregion class DrugNewsSentimentAlphaModel(AlphaModel): """ This class emits insights to take long intraday positions for securities that have positive news sentiment. """ _symbol_data_by_symbol = {} _sentiment_by_phrase = SentimentByPhrase.dictionary _max_phrase_words = max([len(phrase.split()) for phrase in _sentiment_by_phrase.keys()]) _sign = lambda _, x: int(x and (1, -1)[x < 0]) def __init__(self, bars_before_insight=30): """ Input: - bars_before_insight The number of bars to wait each morning before looking to emit insights """ self._bars_before_insight = bars_before_insight def update(self, algorithm, data): """ Called each time our alpha model receives a new data slice. Input: - algorithm Algorithm instance running the backtest - data A data structure for all of an algorithm's data at a single time step Returns a list of Insights to the portfolio construction model """ insights = [] for symbol, symbol_data in self._symbol_data_by_symbol.items(): # If it's after-hours or within 30-minutes of the open, update # cumulative sentiment for each symbol if symbol_data.bars_seen_today < self._bars_before_insight: tiingo_symbol = symbol_data.tiingo_symbol if data.contains_key(tiingo_symbol) and data[tiingo_symbol] is not None: article = data[tiingo_symbol] symbol_data.cumulative_sentiment += self._calculate_sentiment(article) if data.contains_key(symbol) and data[symbol] is not None: symbol_data.bars_seen_today += 1 # 30-mintes after the open, emit insights in the direction of the cumulative sentiment. # Only emit insights on Wednesdays to capture the analomaly documented by Berument and # Kiymaz (2001). if symbol_data.bars_seen_today == self._bars_before_insight and data.time.weekday() == 2: next_close_time = symbol_data.exchange.hours.get_next_market_close(data.time, False) direction = self._sign(symbol_data.cumulative_sentiment) if direction == 0: continue insight = Insight.price(symbol, next_close_time - timedelta(minutes=2), direction) insights.append(insight) # At the close, reset the cumulative sentiment if not symbol_data.exchange.date_time_is_open(data.time): symbol_data.cumulative_sentiment = 0 symbol_data.bars_seen_today = 0 return insights def on_securities_changed(self, algorithm, changes): """ Called each time our universe has changed. Input: - algorithm Algorithm instance running the backtest - changes The additions and subtractions to the algorithm's security subscriptions """ for security in changes.added_securities: self._symbol_data_by_symbol[security.symbol] = SymbolData(security, algorithm) for security in changes.removed_securities: symbol_data = self._symbol_data_by_symbol.pop(security.symbol, None) if symbol_data: algorithm.remove_security(symbol_data.tiingo_symbol) def _calculate_sentiment(self, article): """ Calculates the sentiment of a Tiingo news article by analyzing the article's title and description. We utilize a dictionary of sentiment values composed by experts in the domain who reviewed news articles over several years. Input: - article Tiingo news article object Returns the sentiment value of the article. """ sentiment = 0 for content in (article.title, article.description): words = content.lower().split() for num_words in range(1, self._max_phrase_words + 1): for gram in ngrams(words, num_words): phrase = ' '.join(gram) if phrase in self._sentiment_by_phrase.keys(): sentiment += self._sentiment_by_phrase[phrase] return sentiment class SymbolData: """ This class is used to store information on each security in the universe and initilize the Tiingo news feeds for the security. """ cumulative_sentiment = 0 bars_seen_today = 0 def __init__(self, security, algorithm): self.exchange = security.exchange self.tiingo_symbol = algorithm.add_data(TiingoNews, security.symbol).symbol
#region imports from AlgorithmImports import * #endregion """ Sentiment dicationary retrieved from: https://github.com/queensbamlab/NewsSentiments/blob/master/dict.csv "The dictionary was created by leveraging author's domain expertise and thorough analysis of news articles over the years." (Isah, Shah, & Zulkernine, , Merchant, & Sargeant, 2018, p. 3) The dictionary has been adjusted to lowercase. """ class SentimentByPhrase: dictionary = { 'okay from fda' : 1, 'fda approval' : 1, 'usfda approval' : 1, 'weaker rupee' : 1, 'positive step' : 1, 'resolution' : 1, 'successful' : 1, 'stellar' : 1, 'better' : 1, 'much better' : 1, 'better margins' : 1, 'favourable' : 1, 'approval' : 1, 'tough' : -1, 'reported lower than expected sales' : -1, 'lower than expected sales' : -1, 'affecting sales growth' : -1, 'difficult one' : -1, 'pricing pressure' : -1, 'sales declined' : -1, 'dull' : -1, 'significant violations' : -1, 'warning letter' : -1, 'issued warning letter' : -1, 'adulterate' : -1, 'potentially contaminate' : -1, 'contaminate' : -1, 'fail' : -1, 'warn' : -1, 'violation' : -1, 'legal action' : -1, 'drag' : -1, 'sales decline' : -1, 'margins decline' : -1, 'weak' : -1, 'offset price erosion' : 1, 'price erosion' : -1, 'slowdown' : -1, 'sanction' : -1, 'concern' : -1, 'drag on sale' : -1, 'drop' : -1, 'challenge' : -1, 'toll' : -1, 'uncertain' : -1, 'recall' : -1, 'health' : 1, 'stability' : 1, 'mixed set' : -1, 'shares declined' : 0, 'major breakthrough' : 1, 'good quarter' : 1, 'appreciating rupee' : -1, 'depreciating rupee' : 1, 'heightened competition' : -1, 'incorrect instructions' : -1, 'shares decline' : 0, 'zero observations' : 1, 'strong us pipeline' : 1, 'upgrade' : 1, 'downgrade' : -1, 'mixed bag' : -1, 'disappointing year' : -1, 'domestic challenges' : -1, 'benefit' : 1, 'percent growth' : 1, 'flat revenue' : -1, 'flat' : -1, 'beat' : 1, 'achieve' : 1, 'steady margins' : 1, 'rise' : 1, 'expand' : 1, 'ramp up' : 1, 'launch' : 1, 'not issued' : 1, 'clear' : 1, 'address' : 0, 'observation' : 0, 'procedural' : 0, 'eir' : 1, 'monetise' : 1, 'outperform' : 1, 'enhance' : 1, 'form 483' : -1, 'clarify' : 1, 'facility' : 0, 'starts' : 1, 'stable' : 1, 'initiative' : 1, 'sold rights' : 1, 'terminate' : -1, 'strengthen' : 1, 'sahpra approval' : 1, 'nod' : 1, 'acquire' : 1, 'raise target' : 1, 'scaling up' : 1, 'raise' : 1, 'subject to clearance' : 0 }
#region imports from AlgorithmImports import * from DrugManufacturerUniverseSelection import DrugManufacturerUniverseSelection from DrugNewsSentimentAlphaModel import DrugNewsSentimentAlphaModel #endregion class NewsSentimentDrugManufacturerAlgorithm(QCAlgorithm): def initialize(self): self.set_start_date(2019, 1, 1) self.set_start_date(2022, 1, 1) self.set_cash(100000) self.set_universe_selection(DrugManufacturerUniverseSelection()) self.universe_settings.resolution = Resolution.MINUTE self.set_alpha(DrugNewsSentimentAlphaModel()) self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel()) self.set_execution(ImmediateExecutionModel())