Machine Learning in Trading Strategy Design › Sentiment Analysis for Trading Signals

Michael BrenndoerferJanuary 3, 202612 min read
Quantitative FinanceAll chapters
Part 13 of 15Machine Learning and Alternative Data
Chapter 70Machine Learning in Trading Strategy Design › Sentiment Analysis for Trading Signals
Reading section 4 of 14 · Sentiment Analysis for Trading Signals · 12 minSection 4 of 14 · 12 min

Book contents

Section 4 of 14 · 12 min read

Machine Learning in Trading Strategy Design › Sentiment Analysis for Trading SignalsOne section · Sections in this chapter4 / 14 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Financial markets are driven by fundamentals and by collective psychology. Market participants' expectations and risk perceptions manifest in price movements, but they also leave traces in the vast corpus of financial text produced daily. News articles, earnings call transcripts, social media posts, and analyst reports contain information about market sentiment that can predict price movements. Natural language processing (NLP) techniques transform this unstructured text data into quantitative trading signals.

The intuition behind sentiment analysis for trading is straightforward: if we can systematically measure whether market commentary is becoming more optimistic or pessimistic, we might anticipate the buying or selling pressure that follows. When analysts upgrade their tone, when news coverage shifts from concerns to opportunities, when executives speak more confidently on earnings calls, these verbal signals often precede tangible market moves. The challenge lies in converting messy human language into clean numerical signals that trading algorithms can consume.

Text Preprocessing for Financial Data

Financial text requires domain-specific preprocessing. Standard NLP techniques must be adapted to handle ticker symbols, numerical expressions, financial jargon, and the particular ways sentiment is expressed in market contexts. A preprocessing pipeline that works well for movie reviews will struggle with the peculiarities of financial language.

Consider the differences: financial text contains dense numerical information (revenue figures, growth percentages, price targets) that carries meaning. It uses abbreviations and acronyms specific to the industry (EPS, EBITDA, YoY). Ticker symbols appear frequently and must be recognized and associated with their respective companies. Temporal references require careful interpretation, since a statement about "last quarter" means something very different from one about "next fiscal year." These domain-specific features require careful handling:

In[17]:
Code
import re


def preprocess_financial_text(text):
    """
    Preprocess financial text for sentiment analysis.
    """
    # Convert to lowercase
    text = text.lower()

    # Preserve ticker symbols (typically $AAPL or just AAPL)
    ticker_pattern = r"\$?[A-Z]{1,5}(?=\s|$|,|\.)"
    tickers = re.findall(ticker_pattern, text, re.IGNORECASE)

    # Handle common financial abbreviations
    abbreviations = {
        "q1": "first quarter",
        "q2": "second quarter",
        "q3": "third quarter",
        "q4": "fourth quarter",
        "yoy": "year over year",
        "qoq": "quarter over quarter",
        "eps": "earnings per share",
        "p/e": "price to earnings",
        "roi": "return on investment",
        "ebitda": "ebitda",
    }
    for abbr, expansion in abbreviations.items():
        text = re.sub(r"\b" + abbr + r"\b", expansion, text)

    # Remove URLs
    text = re.sub(r"http\S+|www\.\S+", "", text)

    # Remove special characters but keep sentence structure
    text = re.sub(r"[^\w\s\.\,\!\?]", "", text)

    # Normalize whitespace
    text = " ".join(text.split())

    return text, tickers


# Example financial headlines
headlines = [
    "Apple beats Q3 earnings estimates, stock surges 5% in after-hours trading",
    "Fed signals rate cuts ahead, markets rally on dovish commentary",
    "Tesla misses delivery targets, shares plunge amid production concerns",
    "Goldman Sachs upgrades MSFT to buy, cites cloud growth momentum",
    "Oil prices crash as OPEC+ fails to reach production agreement",
]

processed_headlines = [preprocess_financial_text(h) for h in headlines]
Out[18]:
Console
Preprocessed Headlines:
------------------------------------------------------------
Original: Apple beats Q3 earnings estimates, stock surges 5%...
Processed: apple beats third quarter earnings estimates, stoc...
Tickers: ['apple', 'beats', 'nings', 'mates', 'stock', 'urges', 'in', 'hours', 'ading']

Original: Fed signals rate cuts ahead, markets rally on dovi...
Processed: fed signals rate cuts ahead, markets rally on dovi...
Tickers: ['fed', 'gnals', 'rate', 'cuts', 'ahead', 'rkets', 'rally', 'on', 'ovish', 'ntary']

Original: Tesla misses delivery targets, shares plunge amid ...
Processed: tesla misses delivery targets, shares plunge amid ...
Tickers: ['tesla', 'isses', 'ivery', 'rgets', 'hares', 'lunge', 'amid', 'ction', 'cerns']

Original: Goldman Sachs upgrades MSFT to buy, cites cloud gr...
Processed: goldman sachs upgrades msft to buy, cites cloud gr...
Tickers: ['ldman', 'sachs', 'rades', 'msft', 'to', 'buy', 'cites', 'cloud', 'rowth', 'entum']

Original: Oil prices crash as OPEC+ fails to reach productio...
Processed: oil prices crash as opec fails to reach production...
Tickers: ['oil', 'rices', 'crash', 'as', 'fails', 'to', 'reach', 'ction', 'ement']

Preprocessing standardizes the text structure, separating ticker symbols for entity linking and expanding abbreviations to improve consistency across sources. This normalization ensures that the sentiment model focuses on meaningful content rather than formatting variations. Without this step, the same underlying sentiment could appear in many different surface forms, making it harder for the model to learn consistent relationships between wording and sentiment.

Dictionary-Based Sentiment Analysis

The simplest approach to sentiment analysis uses predefined dictionaries of positive and negative words. When encountering text, the algorithm counts occurrences of words from each category and computes a sentiment score based on the balance. Financial-specific dictionaries like Loughran-McDonald are particularly effective because they account for how words carry different connotations in financial versus general contexts.

Domain context changes how individual words should be scored. Consider the word "liability": in everyday speech, it's neutral or only mildly negative ("he's a liability to the team"), but in financial contexts, it refers to debts and obligations, carrying a more clearly negative connotation. Similarly, "tax" might be neutral in general text but signals a drag on earnings in financial analysis. Words like "leverage" and "risk" require careful handling, as they can be positive or negative depending on context. General-purpose sentiment dictionaries, trained on movie reviews or product feedback, will misclassify these terms and produce unreliable signals.

In[19]:
Code
# Financial sentiment lexicon (simplified Loughran-McDonald style)
positive_words = {
    "beat",
    "beats",
    "exceeded",
    "surged",
    "surge",
    "rally",
    "rallied",
    "upgrade",
    "upgrades",
    "growth",
    "profit",
    "profitable",
    "gain",
    "gains",
    "positive",
    "strong",
    "strength",
    "outperform",
    "buy",
    "bullish",
    "momentum",
    "opportunity",
    "success",
    "successful",
    "improve",
    "improved",
}

negative_words = {
    "miss",
    "misses",
    "missed",
    "plunge",
    "plunged",
    "crash",
    "crashed",
    "downgrade",
    "downgrades",
    "loss",
    "losses",
    "decline",
    "declined",
    "negative",
    "weak",
    "weakness",
    "underperform",
    "sell",
    "bearish",
    "concern",
    "concerns",
    "risk",
    "risks",
    "fail",
    "failed",
    "warning",
}


def dictionary_sentiment(text, positive_dict, negative_dict):
    """
    Calculate sentiment score using dictionary approach.
    Returns score between -1 (very negative) and +1 (very positive).
    """
    words = text.lower().split()

    pos_count = sum(1 for w in words if w in positive_dict)
    neg_count = sum(1 for w in words if w in negative_dict)

    total = pos_count + neg_count
    if total == 0:
        return 0.0

    # Normalized sentiment score
    score = (pos_count - neg_count) / total

    return score, pos_count, neg_count


# Analyze headlines
sentiment_results = []
for original, (processed, tickers) in zip(headlines, processed_headlines):
    score, pos, neg = dictionary_sentiment(
        processed, positive_words, negative_words
    )
    sentiment_results.append(
        {
            "headline": original[:40] + "...",
            "sentiment": score,
            "positive_words": pos,
            "negative_words": neg,
        }
    )
Out[20]:
Console
Dictionary-Based Sentiment Analysis:
----------------------------------------------------------------------
Apple beats Q3 earnings estimates, stock...
  Score: +1.00 (Positive)
  Positive words: 1, Negative words: 0

Fed signals rate cuts ahead, markets ral...
  Score: +1.00 (Positive)
  Positive words: 1, Negative words: 0

Tesla misses delivery targets, shares pl...
  Score: -1.00 (Negative)
  Positive words: 0, Negative words: 3

Goldman Sachs upgrades MSFT to buy, cite...
  Score: +1.00 (Positive)
  Positive words: 3, Negative words: 0

Oil prices crash as OPEC+ fails to reach...
  Score: -1.00 (Negative)
  Positive words: 0, Negative words: 1
Out[21]:
Visualization
Horizontal bar chart of sentiment scores from -1 to 1 for five financial headlines, with green bars for positive sentiment and red bars for negative sentiment.
Sentiment scores for five financial headlines using a dictionary-based approach. Headings related to earnings beats and analyst upgrades show strong positive scores, while production misses and price crashes generate large negative values, showing the model's ability to capture directional tone.

Dictionary-based methods offer important advantages: they are transparent, fast, and require no training data. You can inspect the dictionary and understand exactly how the model reaches its conclusions. This interpretability is valuable for regulatory compliance and internal governance. However, these methods have significant limitations. They miss context entirely. The phrase "not profitable" contains a positive word ("profitable") but expresses negative sentiment through negation. Phrases like "exceeded expectations but guidance disappointed" contain both positive and negative elements that require understanding of sentence structure to interpret correctly. More sophisticated approaches are needed to capture these nuances.

Machine Learning Sentiment Models

Modern sentiment analysis uses machine learning models trained on labeled financial text. These models learn contextual patterns that dictionary methods miss, including negation handling, comparative constructions, and the subtle ways that sentiment is expressed in professional financial communication:

In[22]:
Code
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline

# Create synthetic training data for demonstration
# In practice, you would use labeled datasets like Financial PhraseBank
training_texts = [
    "Company reports record profits and raises dividend",
    "Strong quarterly results exceed analyst expectations",
    "Revenue growth accelerates amid market expansion",
    "Management announces stock buyback program",
    "Earnings beat consensus estimates significantly",
    "Company misses revenue targets by wide margin",
    "Profit warning issued ahead of quarterly report",
    "Sales decline accelerates as competition intensifies",
    "Management cuts full year guidance substantially",
    "Restructuring charges lead to quarterly loss",
    "Market conditions remain challenging for growth",
    "Regulatory concerns weigh on stock performance",
    "Supply chain disruptions impact production capacity",
    "Debt levels raise concerns among analysts",
    "Customer losses accelerate amid service issues",
]
training_labels = [
    1,
    1,
    1,
    1,
    1,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
]  # 1=positive, 0=negative

# Build sentiment classification pipeline
sentiment_pipeline = Pipeline(
    [
        ("tfidf", TfidfVectorizer(ngram_range=(1, 2), max_features=500)),
        ("classifier", MultinomialNB(alpha=0.1)),
    ]
)

sentiment_pipeline.fit(training_texts, training_labels)

# Analyze new headlines
ml_predictions = []
for original, (processed, tickers) in zip(headlines, processed_headlines):
    prob = sentiment_pipeline.predict_proba([processed])[0]
    ml_predictions.append(
        {
            "headline": original[:40] + "...",
            "negative_prob": prob[0],
            "positive_prob": prob[1],
            "prediction": "Positive" if prob[1] > 0.5 else "Negative",
        }
    )
Out[23]:
Console
ML-Based Sentiment Analysis:
----------------------------------------------------------------------
Apple beats Q3 earnings estimates, stock...
  Prediction: Positive
  Confidence - Positive: 86.7%, Negative: 13.3%

Fed signals rate cuts ahead, markets ral...
  Prediction: Negative
  Confidence - Positive: 8.9%, Negative: 91.1%

Tesla misses delivery targets, shares pl...
  Prediction: Negative
  Confidence - Positive: 8.5%, Negative: 91.5%

Goldman Sachs upgrades MSFT to buy, cite...
  Prediction: Negative
  Confidence - Positive: 24.6%, Negative: 75.4%

Oil prices crash as OPEC+ fails to reach...
  Prediction: Negative
  Confidence - Positive: 8.7%, Negative: 91.3%

The machine learning model captures nuances that simple keyword matching would miss. By learning from examples of financial text labeled with sentiment, the model develops internal representations that go beyond individual words. It learns that "misses targets" signals negative sentiment even without the word "bad," and that "raises dividend" implies confidence in future cash flows. The TF-IDF vectorization with bigrams helps here, because phrases like "misses targets" or "raises dividend" are captured as single features rather than independent words.

Production sentiment systems typically use more sophisticated architectures, including deep learning models like BERT and its financial variants (FinBERT), which understand context through attention mechanisms that can weigh the importance of different parts of the text. These models can handle complex sentences with multiple sentiment-bearing clauses, correctly parsing constructions like "despite strong revenue growth, margins contracted significantly" as mixed or negative overall.

Aggregating Sentiment Signals

Individual sentiment scores must be aggregated into tradable signals. A single news article provides limited information, but the collective sentiment across all coverage of a company tells a more complete story. Common approaches include time-weighted averages (more recent information matters more), volume-weighted sentiment (high-profile sources like the Wall Street Journal get more weight than obscure blogs), and sentiment momentum (changes in sentiment level):

In[24]:
Code
def aggregate_sentiment_signals(sentiment_scores, timestamps, decay_halflife=3):
    """
    Aggregate multiple sentiment observations into a single signal.
    Uses exponential decay weighting: more recent observations matter more.

    Parameters:

    - sentiment_scores: Array of individual sentiment scores
    - timestamps: Array of observation times (in days relative to now)
    - decay_halflife: Half-life for exponential decay in days
    """
    # Calculate decay weights
    decay_rate = np.log(2) / decay_halflife
    weights = np.exp(-decay_rate * timestamps)
    weights = weights / weights.sum()  # Normalize

    # Weighted average sentiment
    weighted_sentiment = np.sum(sentiment_scores * weights)

    # Sentiment dispersion (disagreement)
    sentiment_std = np.sqrt(
        np.sum(weights * (sentiment_scores - weighted_sentiment) ** 2)
    )

    # Sentiment momentum: compare recent vs older
    recent_mask = timestamps <= decay_halflife
    if recent_mask.sum() > 0 and (~recent_mask).sum() > 0:
        recent_sentiment = sentiment_scores[recent_mask].mean()
        older_sentiment = sentiment_scores[~recent_mask].mean()
        sentiment_momentum = recent_sentiment - older_sentiment
    else:
        sentiment_momentum = 0.0

    return {
        "aggregate_sentiment": weighted_sentiment,
        "sentiment_dispersion": sentiment_std,
        "sentiment_momentum": sentiment_momentum,
    }


# Example: aggregate sentiment for a stock over past week
np.random.seed(42)
n_observations = 20
sentiment_scores = np.random.normal(
    0.1, 0.3, n_observations
)  # Slightly positive on average
timestamps = np.random.uniform(0, 7, n_observations)  # Days ago

aggregated = aggregate_sentiment_signals(sentiment_scores, timestamps)
Out[25]:
Console
Aggregated Sentiment Signal:
  Weighted Sentiment: +0.029
  Sentiment Dispersion: 0.276
  Sentiment Momentum: -0.094

  - Sentiment is neutral
  - High disagreement among sources (lower conviction)
Out[26]:
Visualization
Scatter plot with days ago on x-axis and sentiment score on y-axis; point size encodes decay weight and color encodes positive (green) or negative (red) sentiment.
Individual sentiment observations overlaid with their decay weights. Recent news items (larger points) exert greater influence on the aggregate signal than older information.
Out[27]:
Visualization
Line chart of exponential decay weight from 1 to near 0 over 7 days, with a vertical dashed line at the 3-day half-life.
Exponential decay weighting function for sentiment aggregation. The half-life of 3 days ensures the signal remains responsive to new information while smoothing out transient noise.

Aggregating signals across multiple sources and time periods, we turn noisy individual data points into a coherent market view. The exponential decay weighting ensures that yesterday's news matters more than last week's, which reflects how information gets incorporated into prices over time. The sentiment dispersion metric captures an often-overlooked dimension: when sources disagree widely, the signal is less reliable than when there's consensus. A stock with uniformly positive coverage gives a stronger buy signal than one with mixed reviews, even if the average sentiment is similar. The momentum component adds yet another layer, capturing whether sentiment is improving or deteriorating. A company with moderately positive sentiment that's improving may be more attractive than one with strongly positive sentiment that's declining.