Back to Community
Avoiding the Twitter Leak with Accern Sentiment Indicators

For those of you who haven’t followed the news – Twitter earnings report was mistakenly made public on Shareholders.com an hour before the market had closed. The earnings report was only publicly accessible for 45 seconds when scraper bots captured the information and broadcast it on Twitter.com around 3:07 PM. Twitter earnings fall short from expectation. This caused a panic among shareholders which led to a 20% drop in Twitter’s stock price - shaving $5 billion right off Twitter’s market cap.

We ran a backtest on over 100,000 articles related to Twitter over a span of 6 months from November 1st, 2014 to April 29th, 2015. Each article has around 25+ metrics, however, we only used the following 3 metrics in our backtest.

  • Article Sentiment (-1 to 1): This metric calculated the sentiment
    score of an article which was relevant to a company. This can be used
    as a directional trigger.
  • Event Impact Score on Entity (1 to 100): This metric calculated if
    the article would have a greater-than-1% impact on the stock on the
    same day.
  • Overall Source Rank (1 to 10): This metric calculated the timeliness
    and reposting of a source information. This can be used as a
    trust/credibility or viral factor.

Check out the results in the backtest. Over a course of 6 months, our algorithm returned 71.6% verse Twitter's benchmark return of -5.1%.

Read full story here: https://www.linkedin.com/pulse/avoiding-twitter-leak-using-accern-sentiment-kumesh-aroomoogan

Credits on building the algo: Seong Lee and Derek Tishler

Clone Algorithm
70
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
import numpy as np
import pandas as pd

def initialize(context):
    #Use file from dropbox to assign sid's and sentiment values to trade on.
    set_symbol_lookup_date('2015-04-29')
    # Universe is set daily by inputs from the cvs fetch. But we will set a benchmark for comparison.
    context.stocks = symbols('TWTR')
    
    set_benchmark(symbol('TWTR'))
    
    # set a more realistic commission for IB, remove both this and slippage when live trading in IB
    set_commission(commission.PerShare(cost=0.014, min_trade_cost=1.4))
    
    # Default slippage values, but here to mess with for fun.
  #  set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.1))
    
    #Only needed in testing/debugging to ensure orders are closed like in IB
    schedule_function(end_of_day, date_rules.every_day(), time_rules.market_close(minutes=1))
    #https://dl.dropboxusercontent.com/u/70792051/Accern%20Backtest/Twitterbacktest_small.csv
    #https://dl.dropboxusercontent.com/u/46283113/Twitterbacktest_small.csv
    fetch_csv("https://dl.dropboxusercontent.com/u/70792051/Accern%20Backtest/TWITTER_backtest.csv",
              date_column ='start_date',
              pre_func=preview,
              post_func = post_view,
              date_format = '%-m/%-d/%Y %-H:%M')
    
    #Article Sentiment
    context.sentiment_positive = 0.16
    context.sentiment_negative = -0.16
    
    #Impact Score On Entity
    context.EIE = 77 #80

    #Overall Source Rank
    context.OSR = 7  #8
    
# see a snapshot of your CSV for debugging
def preview(df):
    df['unchanged_symbol'] = df.symbol
    log.info(' %s ' % df.head())
    return df

def post_view(df):
    def change_symbol(row):
        if row['unchanged_symbol'] == 'TWTR':
            row['sid'] = symbol('TWTR')
        return row
    df = df.apply(lambda row: change_symbol(row), axis=1)
    return df
    
def my_universe(context, fetcher_data):
    my_stocks = set(fetcher_data['sid'])
    print my_stocks
    return my_stocks
    
def handle_data(context, data):
    #Get EST Time
    context.exchange_time = pd.Timestamp(get_datetime()).tz_convert('US/Eastern')
    
    #Check that our portfolio does not  contain any invalid/external positions/securities
    check_invalid_positions(context, data)
    
    for stock in data:
        
        if ('article_sentiment' and 'event_impact_score_overall' and 'overall_source_rank') in data[stock]:
            
            #record(Accern_Article_Sentiment = data[stock]['article_sentiment'], upperBound = context.sentiment_positive, lowerBound = context.sentiment_negative)
            #record(Event_Impact_Entity = data[stock]['event_impact_score_entity_1'], upperBound = context.EIE, lowerBound = context.EIE)
            #record(Overall_Source_Rank = data[stock]['overall_source_rank'], upperBound = context.OSR, lowerBound = context.OSR)
        
            record(Article_Sentiment = data[stock]['article_sentiment'],Event_Impact = data[stock]['event_impact_score_overall'], Source_Rank = data[stock]['overall_source_rank'])
           
                        
            # We will not place orders if a stock is already in the process of handeling an order(fill time)
            if check_if_no_conflicting_orders(stock):
                # order_target_percent(stock, 1)
                #try:
                    
               # Go Long(buy), or exit and then buy(since minute mode so this condition will be valid all day
                    
                if data[stock]['article_sentiment'] > context.sentiment_positive and data[stock]['event_impact_score_entity_1'] > context.EIE and data[stock]['overall_source_rank'] > context.OSR:


                    # If we hav no positions, then we are good to buy
                    log.info("buy signal")
                    if context.portfolio.positions[stock.sid].amount == 0:
                        buy_position(context, data, stock)

                    # We have some positions, if they are short, then exit that position so we can go long.
                    else:
                        if context.portfolio.positions[stock.sid].amount < 0:
                            exit_position(context, data, stock)

         # Go short(sell), or exit and then short(since minute mode so this condition will be valid all day

                elif data[stock]['article_sentiment'] < context.sentiment_negative and data[stock]['event_impact_score_entity_1'] > context.EIE and data[stock]['overall_source_rank'] > context.OSR:


                    # If we have no positions, then we are good to buy
                    log.info("Sell Signal")
                    if context.portfolio.positions[stock.sid].amount == 0:
                        short_position(context, data, stock)

                    # We have some positions, if they are long, then exit that position so we can go short.                        
                    else:
                        if context.portfolio.positions[stock.sid].amount > 0:
                            exit_position(context, data, stock)
                #except:
                #    pass
                
     
def buy_position(context, data, stock):

    # Place an order, and store the ID to fetch order info
    orderId    = order_target_percent(stock, 1.0)
    # How many shares did we just order, since we used target percent of availible cash to place order not share count.
    shareCount = get_order(orderId).amount

    # We need to calculate our own inter cycle portfolio snapshot as its not updated till next cycle.
    value_of_open_orders(context, data)
    availibleCash = context.portfolio.cash-context.cashCommitedToBuy-context.cashCommitedToSell

    log.info("+ BUY {0:,d} of {1:s} at ${2:,.2f} for ${3:,.2f} / ${4:,.2f} @ {5:d}:{6:d}"\
             .format(shareCount,
                     stock.symbol,data[stock]['price'],
                     data[stock]['price']*shareCount, 
                     availibleCash,
                     context.exchange_time.hour,
                     context.exchange_time.minute))
    

def short_position(context, data, stock):
    
    #orderId    = order_target_percent(stock, -1.0/len(data))
    orderId    = order_target_percent(stock, -1.0)
    # How many shares did we just order, since we used target percent of availible cash to place order not share count.
    shareCount = get_order(orderId).amount

    # We need to calculate our own inter cycle portfolio snapshot as its not updated till next cycle.
    value_of_open_orders(context, data)
    availibleCash = context.portfolio.cash-context.cashCommitedToBuy+context.cashCommitedToSell

    log.info("- SHORT {0:,d} of {1:s} at ${2:,.2f} for ${3:,.2f} / ${4:,.2f} @ {5:d}:{6:d}"\
             .format(shareCount,
                     stock.symbol,data[stock]['price'],
                     data[stock]['price']*shareCount, 
                     availibleCash,
                     context.exchange_time.hour,
                     context.exchange_time.minute))

def exit_position(context, data, stock):
    order_target(stock, 0.0)
    value_of_open_orders(context, data)
    availibleCash = context.portfolio.cash-context.cashCommitedToBuy-context.cashCommitedToSell
    log.info("- Exit {0:,d} of {1:s} at ${2:,.2f} for ${3:,.2f} / ${4:,.2f} @ {5:d}:{6:d}"\
                 .format(int(context.portfolio.positions[stock.sid].amount),
                         stock.symbol,
                         data[stock]['price'],
                         data[stock]['price']*context.portfolio.positions[stock.sid].amount,
                         availibleCash,
                         context.exchange_time.hour,
                         context.exchange_time.minute))    
    
################################################################################

def check_if_no_conflicting_orders(stock):
    # Check that we are not already trying to move this stock
    open_orders = get_open_orders()
    safeToMove  = True
    if open_orders:
        for security, orders in open_orders.iteritems():
            for oo in orders:
                if oo.sid == stock.sid:
                    if oo.amount != 0:
                        safeToMove = False
    return safeToMove
    #

def check_invalid_positions(context, securities):
    # Check that the portfolio does not contain any broken positions
    # or external securities
    for sid, position in context.portfolio.positions.iteritems():
        if sid not in securities and position.amount != 0:
            errmsg = \
                "Invalid position found: {sid} amount = {amt} on {date}"\
                .format(sid=position.sid,
                        amt=position.amount,
                        date=get_datetime())
            raise Exception(errmsg)
            
def end_of_day(context, data):
    # cancle any order at the end of day. Do it ourselves so we can see slow moving stocks.
    open_orders = get_open_orders()
    
    if open_orders:# or context.portfolio.positions_value > 0.:
        #log.info("")
        log.info("*** EOD: Stoping Orders & Printing Held ***")

    # Print what positions we are holding overnight
    for stock in data:
        if context.portfolio.positions[stock.sid].amount != 0:
            log.info("{0:s} has remaining {1:,d} Positions worth ${2:,.2f}"\
                     .format(stock.symbol,
                             context.portfolio.positions[stock.sid].amount,
                             context.portfolio.positions[stock.sid].cost_basis\
                             *context.portfolio.positions[stock.sid].amount))
    # Cancle any open orders ourselves(In live trading this would be done for us, soon in backtest too)
    if open_orders:  
        # Cancle any open orders ourselves(In live trading this would be done for us, soon in backtest too)
        for security, orders in open_orders.iteritems():
            for oo in orders:
                log.info("X CANCLED {0:s} with {1:,d} / {2:,d} filled"\
                                     .format(security.symbol,
                                             oo.filled,
                                             oo.amount))
                cancel_order(oo)
    #
        log.info('') 
            
def value_of_open_orders(context, data):
    # Current cash commited to open orders, bit of an estimation for logging only
    context.currentCash = context.portfolio.cash
    open_orders = get_open_orders()
    context.cashCommitedToBuy  = 0.0
    context.cashCommitedToSell = 0.0
    if open_orders:
        for security, orders in open_orders.iteritems():
            for oo in orders:
                # Estimate value of existing order with current price, best to use order conditons?
                if(oo.amount>0):
                    context.cashCommitedToBuy  += oo.amount * data[oo.sid]['price']
                elif(oo.amount<0):
                    context.cashCommitedToSell += oo.amount * data[oo.sid]['price']
    #

    
    
    
    
    
There was a runtime error.
3 responses

I can't look into this now, but this looks suspicious; are you sure you've correctly handled the times of the data pulled in with fetch_csv? I notice the timestamps are in US/Eastern, but you haven't set the timezone to US/Eastern, so possibly the algo has five hours of lookahead bias?

Again, not 100% sure, just asking.

Timestamp is our dataset are in UTC.

Thanks!