Overall Statistics
Total Orders
58
Average Win
1.19%
Average Loss
-0.87%
Compounding Annual Return
22.657%
Drawdown
11.100%
Expectancy
0.581
Start Equity
100000
End Equity
120615.14
Net Profit
20.615%
Sharpe Ratio
1.154
Sortino Ratio
1.484
Probabilistic Sharpe Ratio
76.101%
Loss Rate
33%
Win Rate
67%
Profit-Loss Ratio
1.37
Alpha
0.031
Beta
0.657
Annual Standard Deviation
0.091
Annual Variance
0.008
Information Ratio
-0.118
Tracking Error
0.067
Treynor Ratio
0.16
Total Fees
$72.50
Estimated Strategy Capacity
$480000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
12.20%
from AlgorithmImports import *


class SentimentAnalysis(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2023, 1, 1)
        self.set_end_date(2023, 12, 1)  
        
        self.spy = self.add_equity("SPY", Resolution.DAILY).symbol
        self.set_benchmark(self.spy)

        #TODO:
        self.set_universe_selection(ManualUniverseSelectionModel(self.spy))
        self.set_alpha(NewsSentimentAlphaModel())
        self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel()) 
        self.set_execution(ImmediateExecutionModel()) 
        self.set_risk_management(NullRiskManagementModel())

class NewsData():
    def __init__(self, symbol):
        self.symbol = symbol
        self.window = RollingWindow[float](100)  
        
class NewsSentimentAlphaModel(AlphaModel):
    def __init__(self): 
        self.news_data = {}

        self.word_scores = {
            "risk": -0.5, "opportunity": 0.5, "plunge": -0.5,
            "recovery": 0.5, "volatile": -0.5, "steady": 0.5,
            "decline": -0.5, "rise": 0.5, "loss": -0.5,
            "gain": 0.5, "cut": -0.5, "increase": 0.5,
            "reduce": -0.5, "expand": 0.5, "collapse": -0.5,
            "revenue": 0.5, "decrease": -0.5, "improve": 0.5,
            "strong": 0.5, "weak": -0.5, "boost": 0.5,
            "fall": -0.5, "recover": 0.5, "underperform": -0.5,
            "outperform": 0.5, "struggle": -0.5, "thrive": 0.5,
            "trouble": -0.5, "prosper": 0.5, "drop": -0.5,
            "stability": 0.5, "tumble": -0.5, "surge": 0.5,
            "pressure": -0.5, "optimistic": 0.5, "bearish": -0.5,
            "bullish": 0.5, "crash": -0.5, "slump": -0.5, 
            "bad": -0.5, "good": 0.5, "negative": -0.5, 
            "great": 0.5, "growth": 0.5, "fail": -0.5, 
            "failed": -0.5, "success": 0.5, "nailed": 0.5,
            "beat": 0.5, "missed": -0.5, "profitable": 0.5,
            "beneficial": 0.5, "right": 0.5, "positive": 0.5, 
            "large":0.5, "attractive": 0.5, "sound": 0.5, 
            "excellent": 0.5, "wrong": -0.5, "unproductive": -0.5, 
            "lose": -0.5, "missing": -0.5, "mishandled": -0.5, 
            "unlucrative": -0.5, "up": 0.5, "down": -0.5,
            "unproductive": -0.5, "poor": -0.5, "wrong": -0.5,
            "worthwhile": 0.5, "lucrative": 0.5, "solid": 0.5
        } 
                
    def update(self, algorithm, data):
        insights = []
        news = data.get(TiingoNews) 

        for article in news.values():
            words = article.description.lower().split(" ")
            score = 0
            for word in words:
                if word in self.word_scores:
                    score += self.word_scores[word]
            
            symbol = article.symbol.underlying
            self.news_data[symbol].window.add(score)
            sentiment = sum(self.news_data[symbol].window)
            
            if sentiment > 5:
                insights.append(Insight.price(symbol, timedelta(1), InsightDirection.Up, None, None))
           
        return insights
    
    def on_securities_changed(self, algorithm, changes):
        for security in changes.added_securities:
            symbol = security.symbol
            news_asset = algorithm.add_data(TiingoNews, symbol)
            self.news_data[symbol] = NewsData(news_asset.symbol)

        for security in changes.removed_securities:
            news_data = self.news_data.pop(security.symbol, None)
            if news_data is not None:
                algorithm.remove_security(news_data.symbol)