Back to Community
Trading on 13-D Filings

We continue to add pipeline factors, making use of data from EventVestor. Our latest release uses 13-D filing data.

13-D filings are required by the SEC from anyone who acquires more than 5% of a publicly traded company.

One study in particular documents the information leakage that happens prior to 13D filings suggesting that:

Over 90% of the effect of the 13D filing on the target’s stock
price is realized prior to the filing. This implies little information
is revealed to the market when the 13D filing is made public, as most
of the information has already been leaked to the market.

And yet, there is still a small amount of drift that tends to follow the filing date. You can see a small positive average price drift for days after a 13D filing for every year starting in 2012. The strategy here attempts to capture that drift.

Note, 13D filings are different from 13F filings which occur on a quarterly basis from institutional investment managers with over $100 million in qualifying assets.

Strategy Details:

  • Data set: 13-D Filings by EventVestor
  • Weights: The weight for each security is determined by the total number of longs we have in that current day. So if we have 2 longs, the weight for each long will be 50% (1.0/number of securities). This is a rolling rebalance at the beginning of each day according to the number of securities currently held and to order.
  • Capital base: $1,000,000
  • Days held: Positions are currently held for 10 days but are easily changeable by modifying 'context.days_to_hold'
  • Trade dates: All trades are made 1 business day AFTER a 13-D Filing
  • Slippage and commissions in this backtest are set to 0.

For more examples using data, visit the data factor library.

Clone Algorithm
216
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
"""
This is a strategy based off a white paper that documents the effects
of information leakage surrounding a 13D filing. The full whitepaper can
be found here:
https://www.bradley.edu/dotAsset/dd6c62c8-fb5e-4684-8151-6053b6c45600.pdf

The strategy buys and holds for 10 days starting from 1 business day
after a 13D filing. The dataset used here is EventVestor's 13-D Filings Date
data feed found here:

https://www.quantopian.com/data/eventvestor/_13d_filings
"""

import numpy as np

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume

from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysSince13DFilingsDate,
)

def make_pipeline(context):
    # Create our pipeline  
    pipe = Pipeline()  

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Set our pipeline screens
    top_universe = is_liquid

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(BusinessDaysSince13DFilingsDate(), 'pbd_13')
    pipe.set_screen(is_liquid)
    
    return pipe  
        
def initialize(context):
    #: Set commissions and slippage to 0 to determine pure alpha
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    set_slippage(slippage.FixedSlippage(spread=0))

    #: Declaring the days to hold, change this to what you want)))
    context.days_to_hold = 10
    #: Declares which stocks we currently held and how many days we've held them dict[stock:days_held]
    context.stocks_held = {}
    
    # Make our pipeline
    attach_pipeline(make_pipeline(context), '13_d')

    
    # Log our positions at 10:00AM
    schedule_function(func=log_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())

def before_trading_start(context, data):
    # Screen for securities that only have an earnings release
    # 1 business day previous and separate out the earnings surprises into
    # positive and negative 
    results = pipeline_output('13_d')
    results = results[results['pbd_13'] == 1]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        all_positions = "Current positions for %s : " % (str(get_datetime()))  
        for pos in context.portfolio.positions:  
            if context.portfolio.positions[pos].amount != 0:  
                all_positions += "%s at %s shares, " % (pos.symbol, context.portfolio.positions[pos].amount)  
        log.info(all_positions)  
        
def order_positions(context, data):
    """
    Main ordering conditions to always order an equal percentage in each position
    so it does a rolling rebalance by looking at the stocks to order today and the stocks
    we currently hold in our portfolio.
    """
    port = context.portfolio.positions
    record(leverage=context.account.leverage)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current positions
    current_positive_pos = [pos for pos in port if (port[pos].amount > 0 and pos in context.stocks_held)]
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos

    # Rebalance our positive surprise securities (existing + new)                
    for security in positive_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            order_target_percent(security, 1.0 / len(positive_stocks))
            if context.stocks_held.get(security) is None:
                context.stocks_held[security] = 0
There was a runtime error.
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.

25 responses

Just for informational purposes, here is the backtest curve with default commission/slippage including downtrending period as well.

Clone Algorithm
10
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
"""
This is a strategy based off a white paper that documents the effects
of information leakage surrounding a 13D filing. The full whitepaper can
be found here:
https://www.bradley.edu/dotAsset/dd6c62c8-fb5e-4684-8151-6053b6c45600.pdf

The strategy buys and holds for 10 days starting from 1 business day
after a 13D filing. The dataset used here is EventVestor's 13-D Filings Date
data feed found here:

https://www.quantopian.com/data/eventvestor/_13d_filings
"""

import numpy as np

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume

from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysSince13DFilingsDate,
)

def make_pipeline(context):
    # Create our pipeline  
    pipe = Pipeline()  

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Set our pipeline screens
    top_universe = is_liquid

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(BusinessDaysSince13DFilingsDate(), 'pbd_13')
    pipe.set_screen(is_liquid)
    
    return pipe  
        
def initialize(context):
    #: Set commissions and slippage to 0 to determine pure alpha
    #set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    #set_slippage(slippage.FixedSlippage(spread=0))

    #: Declaring the days to hold, change this to what you want)))
    context.days_to_hold = 10
    #: Declares which stocks we currently held and how many days we've held them dict[stock:days_held]
    context.stocks_held = {}
    
    # Make our pipeline
    attach_pipeline(make_pipeline(context), '13_d')

    
    # Log our positions at 10:00AM
    schedule_function(func=log_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())

def before_trading_start(context, data):
    # Screen for securities that only have an earnings release
    # 1 business day previous and separate out the earnings surprises into
    # positive and negative 
    results = pipeline_output('13_d')
    results = results[results['pbd_13'] == 1]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        all_positions = "Current positions for %s : " % (str(get_datetime()))  
        for pos in context.portfolio.positions:  
            if context.portfolio.positions[pos].amount != 0:  
                all_positions += "%s at %s shares, " % (pos.symbol, context.portfolio.positions[pos].amount)  
        log.info(all_positions)  
        
def order_positions(context, data):
    """
    Main ordering conditions to always order an equal percentage in each position
    so it does a rolling rebalance by looking at the stocks to order today and the stocks
    we currently hold in our portfolio.
    """
    port = context.portfolio.positions
    record(leverage=context.account.leverage)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current positions
    current_positive_pos = [pos for pos in port if (port[pos].amount > 0 and pos in context.stocks_held)]
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos

    # Rebalance our positive surprise securities (existing + new)                
    for security in positive_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            order_target_percent(security, 1.0 / len(positive_stocks))
            if context.stocks_held.get(security) is None:
                context.stocks_held[security] = 0
There was a runtime error.

@Mikko - Good find bro! Hypocrisy at its finest! haha

Here is a theory....

The post filing drift is sensitive to changes in overall market volatility from the date(s) in which the filing entity was acquiring their position relative to the day they filed. When risk (via VXX in this instance) is higher, the drift gets cut short. When risk is lower at time of the filing, the drift will have a more lasting effect and investors should stay in the stock longer.

Clone Algorithm
52
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
"""
This is a strategy based off a white paper that documents the effects
of information leakage surrounding a 13D filing. The full whitepaper can
be found here:
https://www.bradley.edu/dotAsset/dd6c62c8-fb5e-4684-8151-6053b6c45600.pdf

The strategy buys and holds for 10 days starting from 1 business day
after a 13D filing. The dataset used here is EventVestor's 13-D Filings Date
data feed found here:

https://www.quantopian.com/data/eventvestor/_13d_filings
"""

import numpy as np

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume

from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysSince13DFilingsDate,
)

def make_pipeline(context):
    # Create our pipeline  
    pipe = Pipeline()  

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Set our pipeline screens
    top_universe = is_liquid

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(BusinessDaysSince13DFilingsDate(), 'pbd_13')
    pipe.set_screen(is_liquid)
    
    return pipe  
        
def initialize(context):
    #: Set commissions and slippage to 0 to determine pure alpha
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    set_slippage(slippage.FixedSlippage(spread=0))

    #: Declaring the days to hold, change this to what you want)))
    context.days_to_hold = 10
    # Establish the VIX ETF for adjusting days to hold
    context.risk = symbol('VXX')
    
    #: Declares which stocks we currently held and how many days we've held them dict[stock:days_held]
    context.stocks_held = {}
    
    # Make our pipeline
    attach_pipeline(make_pipeline(context), '13_d')

    
    # Log our positions at 10:00AM
    schedule_function(func=log_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())

def before_trading_start(context, data):
    # Screen for securities that only have an earnings release
    # 1 business day previous and separate out the earnings surprises into
    # positive and negative 
    results = pipeline_output('13_d')
    results = results[results['pbd_13'] == 1]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    
    # My adjustments
    context.days_to_hold = context.days_to_hold
    context.risk = context.risk
    Market_Risk = data.history(context.risk, 'price', 10, '1d')
    if Market_Risk[-1] > Market_Risk[0]:
        context.days_to_hold = 10
    else:
        context.days_to_hold = 90

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        all_positions = "Current positions for %s : " % (str(get_datetime()))  
        for pos in context.portfolio.positions:  
            if context.portfolio.positions[pos].amount != 0:  
                all_positions += "%s at %s shares, " % (pos.symbol, context.portfolio.positions[pos].amount)  
        log.info(all_positions)  
        
def order_positions(context, data):
    """
    Main ordering conditions to always order an equal percentage in each position
    so it does a rolling rebalance by looking at the stocks to order today and the stocks
    we currently hold in our portfolio.
    """
    port = context.portfolio.positions
    record(leverage=context.account.leverage)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current positions
    current_positive_pos = [pos for pos in port if (port[pos].amount > 0 and pos in context.stocks_held)]
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos

    # Rebalance our positive surprise securities (existing + new)                
    for security in positive_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            order_target_percent(security, 1.0 / len(positive_stocks))
            if context.stocks_held.get(security) is None:
                context.stocks_held[security] = 0
There was a runtime error.

That's very interesting Frank, so you look at the movement of VXX prior to trading in order to determine your holding period?

On another note, here's an event study based around 13D Filings.

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.

Seong,

Thanks for the notebook. I will try to play around with it.

To answer your question, yes...Basically the idea was to measure VIX 10 days before the market filing, which I thought would generally characterize market risk during the time in which the firm was acquiring their position. If the company ultimately files their 13-D in a higher risk environment, then I would think less investors would follow suit (regardless of the idiosyncratic dynamics of the company), creating a shorter post filing drift.

I am not even sure if the code I added to the algo implements this logic properly, just thought I would provide a starting point for testing the theory.

Frank -

The volatility seems like a good idea. I think also what might be interesting is researching what the change in volume is and if it is higher than the average of other 13 D filings then maybe it would indicate that others are trading the news. I would code it, but don't know how. Go Eagles!

Raf,

Eagles? Heck yea buddy!!! If you wanna shoot me your email, I will give your thesis the ole college try (pun intended), and send you the algo via the collaboration feature on Quantopian.

That's very interesting Frank, so you look at the movement of VXX
prior to trading in order to determine your holding period?

On another note, here's an event study based around 13D Filings.

Shouldn't that case study be normalized? I mean the period is uptrending so it's not clear what the study is trying to present.

@ Mikko,

I am a little confused...which data should be normalized? I am playing with this study as we speak so I am looking for any way to mess around with it.

Thanks

@ Seong,

I am working through the "Icahn" example notebook provided by Q for the 13-D data, and hit an error on in the attached notebook that I just cant seem to fix. Any thoughts? (FYI - the only change I made was switching from the paid set to the free set). Do you have any thoughts on why the notebook printed on Q's end, but is throwing this error for me? I am not finding any clues in stack overflow or elsewhere. Seems like the .like thing is not happening on my end.

Thanks

Frank

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

I am a little confused...which data should be normalized? I am playing with this study as we speak so I am looking for any way to mess around with it.

Either the percentage change should be normalized to be relative to some index (sp500 for example) or the field that is displayed should be changed to show rank or percentile instead of raw percentage change. At the moment charts show only the raw percentage change but it's unclear what part of percentage change is actually related to filing and what part is due to correlation to overall market.

It would also be very important to have downtrending periods in the study as the impact could be very different for these periods (or the same, I have no idea). It would also be very interesting to see results clustered for differert kinds of companies, I would assume the impact will be different (or of different magnitude) for small caps and large caps for example.

Mikko,

So a chart for Beta to the Benchmark? And then a Fama-French 3 Factor Study?

What I would do is just replace the percentage change to percentile rank (no beta needed). And yes, Fama-French factors would be interesting but I would also check other factors (profitability and momentum for example).

Frank,

It seems like I am getting the same error as you are. I would suggest trying out this block of code:

# Since Carl Icahn is represented in several ways, we use `like` to capture all those ways  
names = _13d_filings[_13d_filings.acquiring_entity.like("*Icahn*")]  
# now let's filter down to the percentage of shares, timestamp, and sid  
names = names[['timestamp', 'percent_shares', 'sid', 'acquiring_entity']]  
# # When displaying a Blaze Data Object, the printout is automatically truncated to ten rows.  
names.sort('timestamp')  
names  

Seong

Seong,

Thanks, but that was the code block that was causing the error on my end. I just found a Blaze manual dated 5/2/16 (recent), and I think it led me to the solution. I will post the new code side by side with the old code in the next post. Below is the Blaze manual I am referencing. The solution was on page 12.

https://media.readthedocs.org/pdf/blaze/latest/blaze.pdf

Notebook is attached

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

Frank,

I've taken your algorithm and replaced the VIX ETF with the actual price of VIX and am comparing yesterday's VIX close price to the 30 day moving average to determine holding periods. That seems to improve the performance of the algorithm.

I've also left commissions & slippage to default settings.

Clone Algorithm
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
"""
This is a strategy based off a white paper that documents the effects
of information leakage surrounding a 13D filing. The full whitepaper can
be found here:
https://www.bradley.edu/dotAsset/dd6c62c8-fb5e-4684-8151-6053b6c45600.pdf

The strategy buys and holds for 10 days starting from 1 business day
after a 13D filing. The dataset used here is EventVestor's 13-D Filings Date
data feed found here:

https://www.quantopian.com/data/eventvestor/_13d_filings
"""

import numpy as np

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data.quandl import cboe_vix
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume, SimpleMovingAverage

from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysSince13DFilingsDate,
)

class HistoricalVix(CustomFactor):
    inputs = [cboe_vix.vix_close]
    window_length = 10
    
    def compute(self, today, assets, out, vix):
        out[:] = vix[0]

def make_pipeline(context):
    # Create our pipeline  
    pipe = Pipeline()  

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Set our pipeline screens
    top_universe = is_liquid

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(BusinessDaysSince13DFilingsDate(), 'pbd_13')
    pipe.add(SimpleMovingAverage(inputs=[cboe_vix.vix_close], window_length=30),
             "historical_vix")
    pipe.add(cboe_vix.vix_close.latest, "yesterday_vix_close")
    pipe.set_screen(is_liquid)
    
    return pipe  
        
def initialize(context):
    #: Set commissions and slippage to 0 to determine pure alpha
    # set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    # set_slippage(slippage.FixedSlippage(spread=0))

    #: Declaring the days to hold, change this to what you want)))
    context.days_to_hold = 10
    
    #: Declares which stocks we currently held and how many days we've held them dict[stock:days_held]
    context.stocks_held = {}
    
    # Make our pipeline
    attach_pipeline(make_pipeline(context), '13_d')

    
    # Log our positions at 10:00AM
    schedule_function(func=log_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())

def before_trading_start(context, data):
    # Screen for securities that only have an earnings release
    # 1 business day previous and separate out the earnings surprises into
    # positive and negative 
    results = pipeline_output('13_d')

    yesterday_vix = results['yesterday_vix_close'].iloc[0]
    historical_vix = results['historical_vix'].iloc[0]
    if yesterday_vix > historical_vix:
        context.days_to_hold = 10
    else:
        context.days_to_hold = 90

    results = results[results['pbd_13'] == 1]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    
    # My adjustments
    context.days_to_hold = context.days_to_hold

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        all_positions = "Current positions for %s : " % (str(get_datetime()))  
        for pos in context.portfolio.positions:  
            if context.portfolio.positions[pos].amount != 0:  
                all_positions += "%s at %s shares, " % (pos.symbol, context.portfolio.positions[pos].amount)  
        log.info(all_positions)  
        
def order_positions(context, data):
    """
    Main ordering conditions to always order an equal percentage in each position
    so it does a rolling rebalance by looking at the stocks to order today and the stocks
    we currently hold in our portfolio.
    """
    port = context.portfolio.positions
    record(leverage=context.account.leverage)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current positions
    current_positive_pos = [pos for pos in port if (port[pos].amount > 0 and pos in context.stocks_held)]
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos

    # Rebalance our positive surprise securities (existing + new)                
    for security in positive_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            order_target_percent(security, 1.0 / len(positive_stocks))
            if context.stocks_held.get(security) is None:
                context.stocks_held[security] = 0
There was a runtime error.

OOS results. My opinion is that this event is quite volatile to trade on and that there is quite a bit of room to use it as a risk factor as opposed to a trading signal (e.g. avoid trading a few days after a 13D filing as it will navigate the possible price overreaction during that time)

Clone Algorithm
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
"""
This is a strategy based off a white paper that documents the effects
of information leakage surrounding a 13D filing. The full whitepaper can
be found here:
https://www.bradley.edu/dotAsset/dd6c62c8-fb5e-4684-8151-6053b6c45600.pdf

The strategy buys and holds for 10 days starting from 1 business day
after a 13D filing. The dataset used here is EventVestor's 13-D Filings Date
data feed found here:

https://www.quantopian.com/data/eventvestor/_13d_filings
"""

import numpy as np

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data.quandl import cboe_vix
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume, SimpleMovingAverage

from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysSince13DFilingsDate,
)

class HistoricalVix(CustomFactor):
    inputs = [cboe_vix.vix_close]
    window_length = 10
    
    def compute(self, today, assets, out, vix):
        out[:] = vix[0]

def make_pipeline(context):
    # Create our pipeline  
    pipe = Pipeline()  

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Set our pipeline screens
    top_universe = is_liquid

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(BusinessDaysSince13DFilingsDate(), 'pbd_13')
    pipe.add(SimpleMovingAverage(inputs=[cboe_vix.vix_close], window_length=30),
             "historical_vix")
    pipe.add(cboe_vix.vix_close.latest, "yesterday_vix_close")
    pipe.set_screen(is_liquid)
    
    return pipe  
        
def initialize(context):
    #: Set commissions and slippage to 0 to determine pure alpha
    # set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    # set_slippage(slippage.FixedSlippage(spread=0))

    #: Declaring the days to hold, change this to what you want)))
    context.days_to_hold = 10
    
    #: Declares which stocks we currently held and how many days we've held them dict[stock:days_held]
    context.stocks_held = {}
    
    # Make our pipeline
    attach_pipeline(make_pipeline(context), '13_d')

    
    # Log our positions at 10:00AM
    schedule_function(func=log_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())

def before_trading_start(context, data):
    # Screen for securities that only have an earnings release
    # 1 business day previous and separate out the earnings surprises into
    # positive and negative 
    results = pipeline_output('13_d')

    yesterday_vix = results['yesterday_vix_close'].iloc[0]
    historical_vix = results['historical_vix'].iloc[0]
    if yesterday_vix > historical_vix:
        context.days_to_hold = 10
    else:
        context.days_to_hold = 90

    results = results[results['pbd_13'] == 1]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    
    # My adjustments
    context.days_to_hold = context.days_to_hold

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        all_positions = "Current positions for %s : " % (str(get_datetime()))  
        for pos in context.portfolio.positions:  
            if context.portfolio.positions[pos].amount != 0:  
                all_positions += "%s at %s shares, " % (pos.symbol, context.portfolio.positions[pos].amount)  
        log.info(all_positions)  
        
def order_positions(context, data):
    """
    Main ordering conditions to always order an equal percentage in each position
    so it does a rolling rebalance by looking at the stocks to order today and the stocks
    we currently hold in our portfolio.
    """
    port = context.portfolio.positions
    record(leverage=context.account.leverage)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current positions
    current_positive_pos = [pos for pos in port if (port[pos].amount > 0 and pos in context.stocks_held)]
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos

    # Rebalance our positive surprise securities (existing + new)                
    for security in positive_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            order_target_percent(security, 1.0 / len(positive_stocks))
            if context.stocks_held.get(security) is None:
                context.stocks_held[security] = 0
There was a runtime error.

Seong,

I agree. Thanks again for this work. It provides a lot of avenues for future study.

p.s. I have checked this thread a few times now and have not been able to view the embedded algo chart/metrics/code...

Frank

Frank, sorry about that. Here's a second try at uploading the new backtest. This contains the results up till 2016 so you'll need to adjust the dates for 2014 if you're using the sample data.

Clone Algorithm
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
"""
This is a strategy based off a white paper that documents the effects
of information leakage surrounding a 13D filing. The full whitepaper can
be found here:
https://www.bradley.edu/dotAsset/dd6c62c8-fb5e-4684-8151-6053b6c45600.pdf

The strategy buys and holds for 10 days starting from 1 business day
after a 13D filing. The dataset used here is EventVestor's 13-D Filings Date
data feed found here:

https://www.quantopian.com/data/eventvestor/_13d_filings
"""

import numpy as np

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data.quandl import cboe_vix
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume, SimpleMovingAverage

from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysSince13DFilingsDate,
)

class HistoricalVix(CustomFactor):
    inputs = [cboe_vix.vix_close]
    window_length = 10
    
    def compute(self, today, assets, out, vix):
        out[:] = vix[0]

def make_pipeline(context):
    # Create our pipeline  
    pipe = Pipeline()  

    # Screen out penny stocks and low liquidity securities.  
    dollar_volume = AverageDollarVolume(window_length=20)  
    is_liquid = dollar_volume > 10**7

    # Set our pipeline screens
    top_universe = is_liquid

    # Add long/shorts to the pipeline  
    pipe.add(top_universe, "longs")
    pipe.add(BusinessDaysSince13DFilingsDate(), 'pbd_13')
    pipe.add(SimpleMovingAverage(inputs=[cboe_vix.vix_close], window_length=30),
             "historical_vix")
    pipe.add(cboe_vix.vix_close.latest, "yesterday_vix_close")
    pipe.set_screen(is_liquid)
    
    return pipe  
        
def initialize(context):
    #: Set commissions and slippage to 0 to determine pure alpha
    # set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    # set_slippage(slippage.FixedSlippage(spread=0))

    #: Declaring the days to hold, change this to what you want)))
    context.days_to_hold = 10
    
    #: Declares which stocks we currently held and how many days we've held them dict[stock:days_held]
    context.stocks_held = {}
    
    # Make our pipeline
    attach_pipeline(make_pipeline(context), '13_d')

    
    # Log our positions at 10:00AM
    schedule_function(func=log_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(minutes=30))
    # Order our positions
    schedule_function(func=order_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())

def before_trading_start(context, data):
    # Screen for securities that only have an earnings release
    # 1 business day previous and separate out the earnings surprises into
    # positive and negative 
    results = pipeline_output('13_d')

    yesterday_vix = results['yesterday_vix_close'].iloc[0]
    historical_vix = results['historical_vix'].iloc[0]
    if yesterday_vix > historical_vix:
        context.days_to_hold = 10
    else:
        context.days_to_hold = 90

    results = results[results['pbd_13'] == 1]
    assets_in_universe = results.index
    context.positive_surprise = assets_in_universe[results.longs]
    
    # My adjustments
    context.days_to_hold = context.days_to_hold

def log_positions(context, data):
    #: Get all positions  
    if len(context.portfolio.positions) > 0:  
        all_positions = "Current positions for %s : " % (str(get_datetime()))  
        for pos in context.portfolio.positions:  
            if context.portfolio.positions[pos].amount != 0:  
                all_positions += "%s at %s shares, " % (pos.symbol, context.portfolio.positions[pos].amount)  
        log.info(all_positions)  
        
def order_positions(context, data):
    """
    Main ordering conditions to always order an equal percentage in each position
    so it does a rolling rebalance by looking at the stocks to order today and the stocks
    we currently hold in our portfolio.
    """
    port = context.portfolio.positions
    record(leverage=context.account.leverage)
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:  
        if data.can_trade(security):  
            if context.stocks_held.get(security) is not None:  
                context.stocks_held[security] += 1  
                if context.stocks_held[security] >= context.days_to_hold:  
                    order_target_percent(security, 0)  
                    del context.stocks_held[security]  
            # If we've deleted it but it still hasn't been exited. Try exiting again  
            else:  
                log.info("Haven't yet exited %s, ordering again" % security.symbol)  
                order_target_percent(security, 0)  

    # Check our current positions
    current_positive_pos = [pos for pos in port if (port[pos].amount > 0 and pos in context.stocks_held)]
    positive_stocks = context.positive_surprise.tolist() + current_positive_pos

    # Rebalance our positive surprise securities (existing + new)                
    for security in positive_stocks:
        can_trade = context.stocks_held.get(security) <= context.days_to_hold or \
                    context.stocks_held.get(security) is None
        if data.can_trade(security) and can_trade:
            order_target_percent(security, 1.0 / len(positive_stocks))
            if context.stocks_held.get(security) is None:
                context.stocks_held[security] = 0
There was a runtime error.

Hi,

I am new here. This seem might like a very interesting strategy. But can you think of any reason why it has such a high beta? From what I am reading here, the essence of the strategy is that since 5 percent and above holding change obviously have a market impact on the prices (likely upwards), by following suit, one tries to time a good entry point into the security. If anything, I would imagine the return to be not so correlated with the general market and more alpha based. Maybe I am missing some there here.

Here's a version that strictly bets on mean reversion following a 13D filing. Due to the relative infrequency of 13D filings, it doesn't hold many positions at a time, and the long and short sides have much different position concentration due to the fact that both sides are forced to 50% total, but it might be interesting to try combining this with another strategy.

Default commissions and slippage are on.

Clone Algorithm
46
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
"""
This strategy trades companies 1 day after a 13D filing. The algorithm goes long in securities
that had negative returns leading up to the filing, and short those that had positive returns
leading up to the filing. The assumption is that the 13D triggers a mean reversion behavior.
The strategy makes equal-weighted bets in the long and short book with the sum of the longs
adding up to 50% of the portfolio value and the shorts adding up to the other 50%.

The hold period and returns lookback window can be adjusted with DAYS_TO_HOLD and 
RETURNS_LOOKBACK_DAYS respectively.

https://www.quantopian.com/data/eventvestor/_13d_filings
"""

import numpy as np

from quantopian.algorithm import attach_pipeline, pipeline_output, order_optimal_portfolio

import quantopian.experimental.optimize as opt

from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume, Returns
from quantopian.pipeline.filters import Q1500US

from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysSince13DFilingsDate,
)

# Number of days holding the position which we are looking at the price change prior to the filing.
DAYS_TO_HOLD = 30

# Number of days over which we are looking at the price change prior to the filing.
RETURNS_LOOKBACK_DAYS = 30

LONG_WEIGHT = 0.5
SHORT_WEIGHT = -0.5

def make_pipeline(context):
    # Create our pipeline  
    pipe = Pipeline()  

    # Set our universe to top liquid stocks.
    universe = Q1500US()
    
    recent_filing = (BusinessDaysSince13DFilingsDate(mask=universe) <= 1)
    
    recent_returns = Returns(window_length=30, mask=universe)
    longs = recent_returns < 0
    shorts = recent_returns > 0
    
    pipe = Pipeline(
        columns={
            'longs': longs,
            'shorts': shorts
        },
        screen=recent_filing
    )
    
    return pipe  
        
def initialize(context):
    
    # Make our pipeline
    attach_pipeline(make_pipeline(context), '13_d')

    # Rebalance our portfolio at market open.
    schedule_function(func=rebalance,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    
    # Log our positions at the end of the day.
    schedule_function(func=log_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close())
    
    context.days_held = {}
    context.held_longs = set()
    context.held_shorts = set()
    

def before_trading_start(context, data):
    # Screen for securities that only have an earnings release
    # 1 business day previous and separate out the earnings surprises into
    # positive and negative 
    results = pipeline_output('13_d')
    
    context.longs = results[results.longs].index
    context.shorts = results[results.shorts].index

        
def rebalance(context, data):
    """
    Main ordering conditions to always order an equal percentage in each position
    so it does a rolling rebalance by looking at the stocks to order today and the stocks
    we currently hold in our portfolio.
    """
    port = context.portfolio.positions
    
    weights = {}
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:
        if security not in context.longs and security not in context.shorts and data.can_trade(security):
            if context.days_held[security] >= DAYS_TO_HOLD:
                weights[security] = 0
            elif port[security].amount > 0:
                weights[security] = LONG_WEIGHT/(len(context.longs) + len(context.held_longs))
            elif port[security].amount < 0:
                weights[security] = SHORT_WEIGHT/(len(context.shorts) + len(context.held_shorts))

    for security in context.longs:
        if data.can_trade(security) and security not in port:
            weights[security] = LONG_WEIGHT/(len(context.longs) + len(context.held_longs))
            context.days_held[security] = 0
    
    for security in context.shorts:
        if data.can_trade(security) and security not in port:
            weights[security] = SHORT_WEIGHT/(len(context.shorts) + len(context.held_shorts))
            context.days_held[security] = 0
            
    order_optimal_portfolio(
        objective=opt.TargetPortfolioWeights(weights),
        constraints=[],
        universe=weights.keys()
    )
            

def log_positions(context, data):
    
    # Reset these every day so we don't track positions that have been closed.
    context.held_longs = set()
    context.held_shorts = set()
    
    port = context.portfolio.positions
    
    #: Get all positions  
    if len(port) > 0:  
        all_positions = "Current positions for %s : " % (str(get_datetime()))  
        for pos in port:  
            if port[pos].amount != 0:  
                all_positions += "%s at %s shares, " % (pos.symbol, port[pos].amount)
                context.days_held[pos] += 1
                if port[pos].amount > 0:
                    context.held_longs.add(pos)
                else:
                    context.held_shorts.add(pos)
             
        log.info(all_positions) 
        
    # Clear out positions from our days_held dictionary after the position has been closed out.
    for security in context.days_held:
        if context.days_held[security] >= DAYS_TO_HOLD and security not in port:
            context.days_held[security] = 0
        
    record(leverage=context.account.leverage, num_longs=len(context.held_longs), num_shorts=len(context.held_shorts))
There was a runtime error.
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.

Great find Jamie!

Not much of an improvement, but here is the same strategy with a 10% position constraint. Results in a bit lower vol and drawdown.

Clone Algorithm
22
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
"""
This strategy trades companies 1 day after a 13D filing. The algorithm goes long in securities
that had negative returns leading up to the filing, and short those that had positive returns
leading up to the filing. The assumption is that the 13D triggers a mean reversion behavior.
The strategy makes equal-weighted bets in the long and short book with the sum of the longs
adding up to 50% of the portfolio value and the shorts adding up to the other 50%.

The hold period and returns lookback window can be adjusted with DAYS_TO_HOLD and 
RETURNS_LOOKBACK_DAYS respectively.

https://www.quantopian.com/data/eventvestor/_13d_filings
"""

import numpy as np

from quantopian.algorithm import attach_pipeline, pipeline_output, order_optimal_portfolio

import quantopian.experimental.optimize as opt

from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor, AverageDollarVolume, Returns
from quantopian.pipeline.filters import Q1500US

from quantopian.pipeline.factors.eventvestor import (
    BusinessDaysSince13DFilingsDate,
)

# Number of days holding the position which we are looking at the price change prior to the filing.
DAYS_TO_HOLD = 30

# Number of days over which we are looking at the price change prior to the filing.
RETURNS_LOOKBACK_DAYS = 30

LONG_WEIGHT = 0.5
SHORT_WEIGHT = -0.5

def make_pipeline(context):
    # Create our pipeline  
    pipe = Pipeline()  

    # Set our universe to top liquid stocks.
    universe = Q1500US()
    
    recent_filing = (BusinessDaysSince13DFilingsDate(mask=universe) <= 1)
    
    recent_returns = Returns(window_length=30, mask=universe)
    longs = recent_returns < 0
    shorts = recent_returns > 0
    
    pipe = Pipeline(
        columns={
            'longs': longs,
            'shorts': shorts
        },
        screen=recent_filing
    )
    
    return pipe  
        
def initialize(context):
    
    # Make our pipeline
    attach_pipeline(make_pipeline(context), '13_d')

    # Rebalance our portfolio at market open.
    schedule_function(func=rebalance,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_open())
    
    # Log our positions at the end of the day.
    schedule_function(func=log_positions,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close())
    
    context.days_held = {}
    context.held_longs = set()
    context.held_shorts = set()
    

def before_trading_start(context, data):
    # Screen for securities that only have an earnings release
    # 1 business day previous and separate out the earnings surprises into
    # positive and negative 
    results = pipeline_output('13_d')
    
    context.longs = results[results.longs].index
    context.shorts = results[results.shorts].index

        
def rebalance(context, data):
    """
    Main ordering conditions to always order an equal percentage in each position
    so it does a rolling rebalance by looking at the stocks to order today and the stocks
    we currently hold in our portfolio.
    """
    port = context.portfolio.positions
    
    weights = {}
    
    # Check if we've exited our positions and if we haven't, exit the remaining securities
    # that we have left
    for security in port:
        if security not in context.longs and security not in context.shorts and data.can_trade(security):
            if context.days_held[security] >= DAYS_TO_HOLD:
                weights[security] = 0
            elif port[security].amount > 0:
                weights[security] = LONG_WEIGHT/(len(context.longs) + len(context.held_longs))
            elif port[security].amount < 0:
                weights[security] = SHORT_WEIGHT/(len(context.shorts) + len(context.held_shorts))

    for security in context.longs:
        if data.can_trade(security) and security not in port:
            weights[security] = LONG_WEIGHT/(len(context.longs) + len(context.held_longs))
            context.days_held[security] = 0
    
    for security in context.shorts:
        if data.can_trade(security) and security not in port:
            weights[security] = SHORT_WEIGHT/(len(context.shorts) + len(context.held_shorts))
            context.days_held[security] = 0
            
    # Constrain individual position size to no more than a fixed percentage 
    # of our portfolio. Because our alphas are so widely distributed, we 
    # should expect to end up hitting this max for every stock in our universe.
    constrain_pos_size = opt.PositionConcentration.with_equal_bounds(
        -0.1,
        0.1,
    )

    # Constrain ourselves to allocate the same amount of capital to 
    # long and short positions.
    # market_neutral = opt.DollarNeutral()
    
    order_optimal_portfolio(
        objective=opt.TargetPortfolioWeights(weights),
        # constraints=[],
        constraints=[constrain_pos_size],
        universe=weights.keys()
    )
            

def log_positions(context, data):
    
    # Reset these every day so we don't track positions that have been closed.
    context.held_longs = set()
    context.held_shorts = set()
    
    port = context.portfolio.positions
    
    #: Get all positions  
    if len(port) > 0:  
        all_positions = "Current positions for %s : " % (str(get_datetime()))  
        for pos in port:  
            if port[pos].amount != 0:  
                all_positions += "%s at %s shares, " % (pos.symbol, port[pos].amount)
                context.days_held[pos] += 1
                if port[pos].amount > 0:
                    context.held_longs.add(pos)
                else:
                    context.held_shorts.add(pos)
             
        log.info(all_positions) 
        
    # Clear out positions from our days_held dictionary after the position has been closed out.
    for security in context.days_held:
        if context.days_held[security] >= DAYS_TO_HOLD and security not in port:
            context.days_held[security] = 0
        
    record(leverage=context.account.leverage, num_longs=len(context.held_longs), num_shorts=len(context.held_shorts))
There was a runtime error.

Thanks Cheng. The idea actually came from Mark Gaffney at ValueWalk who is putting together a content series on activist investing.

Is there any reason why we can't access a field such as .like(*icahn) in the backtester? I'd like to review some research in action, but it looks like you only allow a few factors via the pipeline unrelated to the fund buyer/seller.