Back to Community
Forecasting Equity Performance Using Trader Mood Data

PsychSignal's StockTwits Trader Mood analyzes trader's messages posted on StockTwits, and provides a measure of bull/bear intensity for securities based on aggregated message sentiment. This dataset includes factors such as number of bullish and bearish messages, bull-to-bear intensity and bull-to-bear message ratio.

A simple strategy could be to go long in securities with a high bull-to-bear intensity, and short securities with a low bull-to-bear intensity. Using this dataset, I constructed the following factors:
- Bull-to-Bear intensity average over the past 3 days.
- Average # of messages over the past 30 days

The attached notebook contains the analysis I conducted using Alphalens to determine how well these factors were able to predict returns. This analysis also helped me determine a trading frequency/ holding period for my algorithm. For more details on how to use Alphalens for factor analysis, check out Lecture 41: Factor Analysis from our Lecture Series.

Then, using the results from the analysis, I integrated both factors into the Long/Short Equity template algorithm from the Lecture Series to construct an algorithm with the following characteristics:

  • Q1500US base universe
  • Long securities with high intensity value, and short securities with a low intensity value
  • Only consider top 1000 securities based on average number of messages
  • Maintain dollar neutrality, or equal long/short exposure
  • Maintain market neutrality, or low beta-to-market risk
  • Maintain low sector exposure

The Optimize API allows our algorithms to easily manage Sector and Beta-to-Market risk, and maintain Dollar Neutrality on our target portfolio every time our algorithm rebalances.

Things to consider trying next:
- Try different window lengths for both factors and use Alphalens to determine if there is any improvement
- Try different algorithm parameters, like trading frequencies or number of securities allowed based on average # of messages
- Build a multi-factor model using more alternative datasets, or combine these factors with your own alpha factors
- Conduct further analysis on the StockTwits Trader Mood dataset and develop your own factors

This post was inspired by Seong Lee's Social Media Trader Mood Series.

Our allocation process attaches high value to algorithms that use alternative datasets. We evaluate all algorithms that use alternative data, including strategies that use either free datasets or premium datasets. And, this dataset in particular does not require a subscription, which means you can research and backtest using the full dataset, as well as paper and live trade algorithms that use it.

Loading notebook preview...
Notebook previews are currently unavailable.
Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

1 response

This is the corresponding backtest.

Clone Algorithm
75
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
"""
Psychsignal's StockTwits Trader Mood analyzes trader's messages posted on StockTwits, 
and provides a measure of bull/bear intensity for securities based on aggregated message 
sentiment.

Psychsignal's factors used in this algorithm:

bull_minus_bear      - subtracts the bearish intesity from the 
                       bullish intensity [BULL - BEAR] to provide 
                       an immediate net score.
bull_scored_messages - total count of bullish sentiment messages
                       scored by PsychSignal's algorithm
bear_scored_messages - total count of bearish sentiment messages
                       scored by PsychSignal's algorithm
                       
"""
from quantopian.pipeline import Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.factors import CustomFactor, RollingLinearRegressionOfReturns
from quantopian.pipeline.filters.morningstar import Q1500US
from quantopian.pipeline.classifiers.morningstar import Sector
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar
import pandas as pd
import numpy as np

# Optimize API
# https://www.quantopian.com/help#optimize-api
from quantopian.algorithm import order_optimal_portfolio
import quantopian.experimental.optimize as opt

# Psychsignal's StockTwits Trader Mood
# https://www.quantopian.com/data/psychsignal/stocktwits
from quantopian.pipeline.data.psychsignal import stocktwits

# Constraint Parameters
MAX_GROSS_LEVERAGE = 1.0
MAX_SHORT_POSITION_SIZE = 0.005  # 0.5%
MAX_LONG_POSITION_SIZE = 0.005  # 0.5%

# Risk Exposures
MAX_SECTOR_EXPOSURE = 0.10
MAX_BETA_EXPOSURE = 0.20

# This custom factor calculates the average [BULL - BEAR] intensity
# over the past 3 days. 
class BullBearIntensity(CustomFactor):
    """
    Baseline PsychSignal Factor
    """
    inputs = [stocktwits.bull_minus_bear]
    window_length = 3
    
    def compute(self, today, assets, out, bull_minus_bear):
        np.nanmean(bull_minus_bear, axis = 0, out = out)
        
# This custom factor calculates the average amount of total messages
# during the previous 30 days.
class PsychSignalMessages(CustomFactor):
    """
    Created to rank each security by message coverage
    """
    inputs = [stocktwits.bull_scored_messages, stocktwits.bear_scored_messages]
    window_length = 30
    
    def compute(self, today, assets, out, bull_msgs, bear_msgs):
        np.nanmean(bull_msgs + bear_msgs, axis = 0, out = out)
        
    
# Use the following function to create and attach your pipeline. 
# Scheduling logic and variable initialization also go in here. 
def initialize(context):
    
    # Quantopian Open Commissions model
    set_commission(commission.PerShare(cost = 0.001, min_trade_cost = 0))
    
    # Pipeline Definition
    attach_pipeline(make_pipeline(), name = 'PsychSignal')
    
    # Schedule functions
    schedule_function(rebalance, 
                      date_rules.week_start(),
                      time_rules.market_open())
    
    schedule_function(record_vars, 
                      date_rules.every_day(),
                      time_rules.market_close())
    

def make_pipeline():
    """
    Our pipeline selects securities based on their bull-to-bear net score.
    Additionally, it includes beta-to-spy and Morningstar's Sector for use
    with the Optimize API. 
    """
    # Rank securities by their average number of messages over the past
    # 30 days
    message_rank = PsychSignalMessages().rank(ascending = False)
    
    # Define a trading universe:
    # - Q1500US. Learn more here: https://www.quantopian.com/posts/the-q500us-and-q1500us
    # - Only top 1000 securities based on average number of messages
    # - Price filter to exclude penny stocks
    # - Set a minimum $500MM market cap
    message_filter = message_rank < 1000
    price_filter = USEquityPricing.close.latest > 5
    mkt_cap_filter = morningstar.valuation.market_cap.latest >= 500000000
    universe = Q1500US() & message_filter & price_filter & mkt_cap_filter
    
    # Bull-to-Bear intensity factor
    sentiment = BullBearIntensity().rank(mask = universe).zscore()
    
    # We will take market beta into consideration when constructing our portfolio.
    # Here we use Bloomberg's computation for beta.
    # Ref: https://www.lib.uwo.ca/business/betasbydatabasebloombergdefinitionofbeta.html
    beta = 0.66 * RollingLinearRegressionOfReturns(
                    target = sid(8554),
                    returns_length = 5,
                    regression_length = 260,
                    mask = universe
                    ).beta + 0.33 * 1.0
    
    # We will use Morningstar's Sector classifier to enforce sector neutrality
    sector = Sector()

    return Pipeline(
        columns={
            'sentiment': sentiment,
            'beta': beta,
            'sector': sector
        },
        screen = universe
    )

# Store pipeline output in context. 
def before_trading_start(context, data):
    # Pipeline output
    results = pipeline_output('PsychSignal')
    
    # Sentiment values for today's trading universe
    context.sentiment = results['sentiment']
    
    # We assume any securities missing a beta 
    # value have full exposure to the market,
    # so we fill in any missing values with 1.0.
    context.market_beta = results['beta'].fillna(1.0)
    
    # Sector mapping
    context.sector_map = results['sector']

    
def rebalance(context, data):
    
    # Our objective is to try to maximize alpha based on
    # sentiment values
    objective = opt.MaximizeAlpha(context.sentiment)
    
    # This constraint ensures that our portfolio is not overly 
    # concentrated in a single security, or a small subset of securities.
    constrain_pos_size = opt.PositionConcentration.with_equal_bounds(
        -MAX_SHORT_POSITION_SIZE,
        MAX_LONG_POSITION_SIZE
    )
    
    # Constraint allocations to be equally distributed
    # between long and short positions 
    dollar_neutral = opt.DollarNeutral(tolerance = 0.05)
    
    # We will cap leverage at 1x
    max_leverage = opt.MaxGrossLeverage(MAX_GROSS_LEVERAGE)
    
    # Market neutrality constraint. Our portfolio should not 
    # be overly exposed to the market (beta-to-spy).
    market_neutral = opt.WeightedExposure(
        loadings = pd.DataFrame({'market_beta': context.market_beta}),
        min_exposures = {'market_beta': -MAX_BETA_EXPOSURE},
        max_exposures = {'market_beta': MAX_BETA_EXPOSURE},
    )
    
    # Sector neutrality constraint. Our portfolio should not be over-
    # exposed to any particular sector.
    sector_neutral = opt.NetPartitionExposure.with_equal_bounds(
        labels = context.sector_map,
        min = -MAX_SECTOR_EXPOSURE,
        max = MAX_SECTOR_EXPOSURE,
    )

    # Rebalance portfolio
    order_optimal_portfolio(
        objective = objective,
        constraints = [
        max_leverage,
        dollar_neutral,
        market_neutral,
        sector_neutral,
        constrain_pos_size
        ]
    )

def record_vars(context, data):
    """
    Record variables at the end of each day.
    """
    longs = shorts = 0
    for position in context.portfolio.positions.itervalues():
        if position.amount > 0:
            longs += 1
        elif position.amount < 0:
            shorts += 1

    # Record Long and Short count, as well as Leverage.
    record(leverage = context.account.leverage, 
           long_count = longs, 
           short_count = shorts)
There was a runtime error.