Overall Statistics
Total Trades
2814
Average Win
0.08%
Average Loss
-0.07%
Compounding Annual Return
0.761%
Drawdown
9.300%
Expectancy
0.014
Net Profit
1.206%
Sharpe Ratio
0.116
Probabilistic Sharpe Ratio
9.069%
Loss Rate
53%
Win Rate
47%
Profit-Loss Ratio
1.17
Alpha
-0.003
Beta
0.062
Annual Standard Deviation
0.062
Annual Variance
0.004
Information Ratio
-0.706
Tracking Error
0.223
Treynor Ratio
0.116
Total Fees
$4483.81
from QuantConnect.Data.Custom.Tiingo import *
from SentimentByPhrase import SentimentByPhrase
from nltk.util import ngrams

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.ContainsKey(tiingo_symbol) and data[tiingo_symbol] is not None:
                    article = data[tiingo_symbol]
                    symbol_data.cumulative_sentiment += self.CalculateSentiment(article)
        
            if data.ContainsKey(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.GetNextMarketClose(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.DateTimeIsOpen(data.Time):
                    symbol_data.cumulative_sentiment = 0
                    symbol_data.bars_seen_today = 0
        
        return insights
        
        
    def OnSecuritiesChanged(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.AddedSecurities:
            self.symbol_data_by_symbol[security.Symbol] = SymbolData(security, algorithm)

        for security in changes.RemovedSecurities:
            symbol_data = self.symbol_data_by_symbol.pop(security.Symbol, None)
            if symbol_data:
                algorithm.RemoveSecurity(symbol_data.tiingo_symbol)
            
    
    def CalculateSentiment(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.AddData(TiingoNews, security.Symbol).Symbol
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import *
from QuantConnect.Algorithm import *

from DrugManufacturerUniverseSelection import DrugManufacturerUniverseSelection
from DrugNewsSentimentAlphaModel import DrugNewsSentimentAlphaModel

class NewsSentimentDrugManufacturerAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2019, 1, 1)
        self.SetCash(100000)
        
        self.SetUniverseSelection(DrugManufacturerUniverseSelection())
        self.UniverseSettings.Resolution = Resolution.Minute
        
        self.SetAlpha(DrugNewsSentimentAlphaModel())
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        
        self.SetExecution(ImmediateExecutionModel())
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel

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 SelectCoarse(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.HasFundamentalData]
        sorted_by_dollar_volume = sorted(has_fundamentals, key=lambda c: c.DollarVolume, reverse=True)
        return [ x.Symbol for x in sorted_by_dollar_volume[:self.coarse_size] ]
    
        
    def SelectFine(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.AssetClassification.MorningstarIndustryGroupCode == MorningstarIndustryGroupCode.DrugManufacturers]
        sorted_by_pe = sorted(drug_manufacturers, key=lambda f: f.ValuationRatios.PERatio, reverse=True)
        return [ x.Symbol for x in sorted_by_pe[:self.fine_size] ]
"""
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
    }