| Overall Statistics |
|
Total Orders 10 Average Win 1.39% Average Loss -2.22% Compounding Annual Return 14.667% Drawdown 33.400% Expectancy -0.389 Start Equity 100000 End Equity 144289.31 Net Profit 44.289% Sharpe Ratio 0.32 Sortino Ratio 0.376 Probabilistic Sharpe Ratio 17.794% Loss Rate 62% Win Rate 38% Profit-Loss Ratio 0.63 Alpha -0.015 Beta 0.987 Annual Standard Deviation 0.219 Annual Variance 0.048 Information Ratio -0.584 Tracking Error 0.027 Treynor Ratio 0.071 Total Fees $34.65 Estimated Strategy Capacity $15000000.00 Lowest Capacity Asset AAPL R735QTJ8XC9X Portfolio Turnover 1.33% Drawdown Recovery 297 |
#region imports
from AlgorithmImports import *
#endregion
from QuantConnect.DataSource import *
from collections import deque
"""
TIINGO NEWS SENTIMENT MODEL FOR AAPL
This strategy trades AAPL using TiingoNews as a news-sentiment input.
The model reads Tiingo news descriptions for AAPL and assigns a simple dictionary-
based sentiment score. Positive words add to the score. Negative words subtract
from the score.
The strategy does not trade immediately on every article. Instead, it aggregates
news sentiment during the day and makes one portfolio decision near the market
close.
Sentiment logic:
- Positive rolling sentiment:
long AAPL
- Negative rolling sentiment:
short AAPL
- Neutral sentiment:
cash
This is a simple pedagogical sentiment model. It does not use a machine-learning
language model, article relevance weighting, source credibility scoring, or
earnings/event classification. The goal is to show how news data can be converted
into a trading signal in QuantConnect.
Parameters:
sentiment_window_days
Number of daily sentiment observations used in the rolling sentiment
score.
long_weight
Target AAPL allocation when sentiment is positive.
short_weight
Target AAPL allocation when sentiment is negative.
sentiment_threshold
Minimum absolute rolling sentiment required before taking a position.
rebalance_threshold
Minimum target-weight change required before sending a new order.
Benchmark:
The benchmark is buy-and-hold AAPL.
"""
class TiingoNewsDataAlgorithm(QCAlgorithm):
def Initialize(self):
# ------------------------------------------------------------
# 1. BACKTEST SETTINGS
# ------------------------------------------------------------
self.SetStartDate(2023, 9, 1)
self.SetEndDate(2026, 5, 5)
self.initial_cash = 100000
self.SetCash(self.initial_cash)
# ------------------------------------------------------------
# 2. TRADED ASSET
# ------------------------------------------------------------
self.aapl = self.AddEquity(
"AAPL",
Resolution.Minute
).Symbol
self.SetBenchmark(self.aapl)
# ------------------------------------------------------------
# 3. TIINGO NEWS DATA
# ------------------------------------------------------------
self.tiingo_symbol = self.AddData(
TiingoNews,
self.aapl
).Symbol
# ------------------------------------------------------------
# 4. PARAMETERS
# ------------------------------------------------------------
self.sentiment_window_days = self.GetIntParameter(
"sentiment_window_days",
5
)
self.long_weight = self.GetFloatParameter(
"long_weight",
1.00
)
self.short_weight = self.GetFloatParameter(
"short_weight",
-0.50
)
self.sentiment_threshold = self.GetFloatParameter(
"sentiment_threshold",
1.00
)
self.rebalance_threshold = self.GetFloatParameter(
"rebalance_threshold",
0.05
)
# Safety checks.
self.sentiment_window_days = max(1, self.sentiment_window_days)
self.long_weight = max(-1.0, min(1.5, self.long_weight))
self.short_weight = max(-1.5, min(1.0, self.short_weight))
self.sentiment_threshold = max(0.0, self.sentiment_threshold)
self.rebalance_threshold = max(0.0, self.rebalance_threshold)
# ------------------------------------------------------------
# 5. SENTIMENT DICTIONARY
# ------------------------------------------------------------
self.word_scores = {
# Positive words
"good": 1,
"great": 1,
"best": 1,
"growth": 1,
"profit": 1,
"profits": 1,
"beat": 1,
"beats": 1,
"upgrade": 1,
"upgraded": 1,
"strong": 1,
"record": 1,
"surge": 1,
"bullish": 1,
"positive": 1,
# Negative words
"bad": -1,
"terrible": -1,
"worst": -1,
"loss": -1,
"losses": -1,
"miss": -1,
"misses": -1,
"downgrade": -1,
"downgraded": -1,
"weak": -1,
"decline": -1,
"falls": -1,
"fall": -1,
"bearish": -1,
"negative": -1,
"lawsuit": -1,
"probe": -1,
"investigation": -1
}
# ------------------------------------------------------------
# 6. STATE
# ------------------------------------------------------------
self.daily_sentiment_score = 0
self.daily_article_count = 0
self.sentiment_history = deque(
maxlen=self.sentiment_window_days
)
self.current_target_weight = 0.0
self.initial_aapl_price = None
# ------------------------------------------------------------
# 7. HISTORICAL NEWS DIAGNOSTIC
# ------------------------------------------------------------
history = self.History(
self.tiingo_symbol,
14,
Resolution.Daily
)
try:
self.Debug(
"TiingoNews history request returned "
+ str(len(history))
+ " rows."
)
except:
self.Debug("TiingoNews history request completed.")
# ------------------------------------------------------------
# 8. DAILY REBALANCE
# ------------------------------------------------------------
self.Schedule.On(
self.DateRules.EveryDay(self.aapl),
self.TimeRules.BeforeMarketClose(self.aapl, 10),
self.Rebalance
)
def OnData(self, data):
# ------------------------------------------------------------
# 1. READ NEWS EVENTS
# ------------------------------------------------------------
if data.ContainsKey(self.tiingo_symbol):
news_item = data[self.tiingo_symbol]
text = ""
if hasattr(news_item, "Description") and news_item.Description is not None:
text += " " + str(news_item.Description)
if hasattr(news_item, "Title") and news_item.Title is not None:
text += " " + str(news_item.Title)
text = text.lower()
article_score = self.ScoreText(text)
self.daily_sentiment_score += article_score
self.daily_article_count += 1
if article_score != 0:
self.Debug(
str(self.Time)
+ " | News score="
+ str(article_score)
+ " | Daily score="
+ str(self.daily_sentiment_score)
)
def Rebalance(self):
# ------------------------------------------------------------
# 1. UPDATE ROLLING SENTIMENT
# ------------------------------------------------------------
self.sentiment_history.append(
self.daily_sentiment_score
)
rolling_sentiment = sum(self.sentiment_history)
# Reset daily counters after storing the score.
article_count = self.daily_article_count
self.daily_sentiment_score = 0
self.daily_article_count = 0
# ------------------------------------------------------------
# 2. CONVERT SENTIMENT TO TARGET WEIGHT
# ------------------------------------------------------------
if rolling_sentiment >= self.sentiment_threshold:
target_weight = self.long_weight
signal = 1
elif rolling_sentiment <= -self.sentiment_threshold:
target_weight = self.short_weight
signal = -1
else:
target_weight = 0.0
signal = 0
# ------------------------------------------------------------
# 3. EXECUTE ONLY IF TARGET CHANGES MEANINGFULLY
# ------------------------------------------------------------
current_weight = self.GetCurrentWeight(self.aapl)
if abs(target_weight - current_weight) >= self.rebalance_threshold:
self.SetHoldings(
self.aapl,
target_weight
)
self.current_target_weight = target_weight
self.Debug(
str(self.Time.date())
+ " | Rolling sentiment="
+ str(rolling_sentiment)
+ " | Articles today="
+ str(article_count)
+ " | Target AAPL weight="
+ str(round(target_weight, 2))
)
# ------------------------------------------------------------
# 4. PLOTS
# ------------------------------------------------------------
self.PlotDiagnostics(
rolling_sentiment,
article_count,
signal,
target_weight
)
def ScoreText(self, text):
score = 0
for word, word_score in self.word_scores.items():
if word in text:
score += word_score
return score
def PlotDiagnostics(
self,
rolling_sentiment,
article_count,
signal,
target_weight
):
aapl_price = self.Securities[self.aapl].Price
if aapl_price > 0:
if self.initial_aapl_price is None:
self.initial_aapl_price = aapl_price
buy_hold_aapl = (
self.initial_cash
* aapl_price
/ self.initial_aapl_price
)
self.Plot(
"Strategy Equity",
"Portfolio Value",
self.Portfolio.TotalPortfolioValue
)
self.Plot(
"Strategy Equity",
"Buy Hold AAPL",
buy_hold_aapl
)
self.Plot(
"Sentiment",
"Rolling Sentiment",
rolling_sentiment
)
self.Plot(
"Sentiment",
"Signal",
signal
)
self.Plot(
"Sentiment",
"Articles Today",
article_count
)
self.Plot(
"Portfolio State",
"Target AAPL Weight",
target_weight
)
def GetCurrentWeight(self, symbol):
if self.Portfolio.TotalPortfolioValue <= 0:
return 0.0
return (
self.Portfolio[symbol].HoldingsValue
/ self.Portfolio.TotalPortfolioValue
)
def GetIntParameter(self, name, default_value):
value = self.GetParameter(name)
if value is None or value == "":
return default_value
return int(value)
def GetFloatParameter(self, name, default_value):
value = self.GetParameter(name)
if value is None or value == "":
return default_value
return float(value)