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