Back to Community
Build Alpha Factors with Cointegrated Pairs

Today, I would like to share a research notebook that includes a couple of examples to build alpha factors with cointegrated pairs. The pairs trading strategy has been around for a long time, and Quantopian has lectures to introduce the idea and how to implement it. Here, we want to introduce a different way to use cointegrated pairs. The basic idea is that if some event happens to one leg of the pair, like an earnings announcement that beats estimates, it is more likely that the market would price a higher chance of the same thing will happen to the other leg too, so in this case, the other leg would also beat estimates.

The attached notebook illustrates this idea by importing a set of cointegrated pairs via the self serve data tool and using the predetermined set of pairs to build two example factors. Use them as a guide and a starting point, but we encourage you to use your creativity to come up with novel ideas and share the tearsheets below in this thread.

About the self serve dataset, first off, we looked through the stocks in the Quantopian tradable Universe (QTU) to see if any of them are cointegrated. We looked for cointegrated pairs by running the cointegration test for the assets in the QTU every month starting from 2012-01-04 to 2019-05-29 (the methodology is mainly based on the lecture, Introduction to Pairs Trading). We then converted the list of pairs into a Q self-serve data format for you to use. To learn how to use self-serve data, please refer to Upload Your Custom Datasets and Signals with Self-Serve Data and Analyzing a Signal and Creating a Contest Algorithm with Self-Serve Data. You could use all of pairs or some of the pairs we provide to generate your alpha factors. Of course, you could generate the pairs with your own method and use them as input data.

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.

9 responses

It is convenient to move the pipeline code from the Q research environment to the algorithm environment. Please use this algo as a reference for you to design your own algo.

Clone Algorithm
45
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 quantopian.algorithm as algo
import quantopian.optimize as opt
from quantopian.pipeline import Pipeline, CustomFactor
from quantopian.pipeline.factors import BusinessDaysSincePreviousEvent, SimpleMovingAverage
 
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.experimental import risk_loading_pipeline

from quantopian.pipeline.data import Fundamentals
import pandas as pd
import quantopian.pipeline.data.factset.estimates as fe  
import numpy as np
# TODO: Un-comment this line to import your dataset here.
#from quantopian.pipeline.data.user_[user_ID] import [dataset name]
from quantopian.pipeline.data.user_57e2b12557e9c947ce001019 import pairs_self_serve_dataset
 
# Constraint Parameters
MAX_GROSS_LEVERAGE = 1.0
MAX_SHORT_POSITION_SIZE = 0.02
MAX_LONG_POSITION_SIZE = 0.02

class Pairs_ES(CustomFactor):
    window_length = 1
    def compute(self, today, asset_ids, out, pair_groups, earnings_surprises):
        pairs = pd.Series(pair_groups[0,:], index=asset_ids)
        pairs = pairs[pairs != 0]
        earnings_surprises = pd.Series(earnings_surprises[0,:], index=asset_ids)
        output = pd.Series(index=asset_ids)
        for _, pair in pairs.groupby(pairs): # find stocks that share the same value  
            if len(pair.index) == 2:           
                (leg_1, leg_2) = pair.index
                # assign the earnings_surprises score of pair leg 2 to pair leg 1
                output[leg_1] = np.sign(earnings_surprises[leg_2])
                # assign the earnings_surprises score of pair leg 1 to pair leg 2
                output[leg_2] = np.sign(earnings_surprises[leg_1])
        out[:] = output.values
        
class MeanFactor(CustomFactor):
    window_length=1
    def compute(self, today, asset_ids, out, *inputs):
        output = pd.DataFrame(index=asset_ids)
        for i in range(0, len(inputs)):
            output['col_' + str(i)] = inputs[i][0,:]
        out[:] = output.mean(axis=1).values
                
 
def initialize(context):
    """
    A core function called automatically once at the beginning of a backtest.
 
    Use this function for initializing state or other bookkeeping.
 
    Parameters
    ----------
    context : AlgorithmContext
        An object that can be used to store state that you want to maintain in 
        your algorithm. context is automatically passed to initialize, 
        before_trading_start, handle_data, and any scheduled functions.
        context provides the portfolio attribute, which can be used to retrieve 
        information about current positions.
    """
    
    algo.attach_pipeline(make_pipeline(), 'my_data_template')
 
    # Attach the pipeline for the risk model factors that we
    # want to neutralize in the optimization step. The 'risk_factors' string is 
    # used to retrieve the output of the pipeline in before_trading_start below.
    algo.attach_pipeline(risk_loading_pipeline(), 'risk_factors')
 
    # Schedule your trade logic via this rebalance function.
    algo.schedule_function(
        func=rebalance,
        date_rule=algo.date_rules.every_day(),
        time_rule=algo.time_rules.market_open(hours=0, minutes=30)
    )
 
 
def make_pipeline():
    """
    A function that creates and returns our pipeline.
 
    We break this piece of logic out into its own function to make it easier to
    test and modify in isolation. In particular, this function can be
    copy/pasted into research and run by itself.
 
    Returns
    -------
    pipe : Pipeline
        Represents computation we would like to perform on the assets that make
        it through the pipeline screen.
    """
    
    # Slice the PeriodicConensus and Actuals DataSetFamilies into DataSets. In this context,  
    # fq0_eps_cons is a DataSet containing consensus estimates data about EPS for the  
    # most recently reported fiscal quarter. fq0_eps_act is a DataSet containing the actual  
    # reported EPS for the most recently reported quarter.  
    fq0_eps_cons = fe.PeriodicConsensus.slice('EPS', 'qf', 0)  
    fq0_eps_act = fe.Actuals.slice('EPS', 'qf', 0)

    # Get the latest mean consensus EPS estimate for the last reported quarter.  
    fq0_eps_cons_mean = fq0_eps_cons.mean.latest

    # Get the EPS value from the last reported quarter.  
    fq0_eps_act_value = fq0_eps_act.actual_value.latest

    # Define a surprise factor to be the relative difference between the estimated and  
    # reported EPS.  
    fq0_surprise = (fq0_eps_act_value - fq0_eps_cons_mean) / fq0_eps_cons_mean
    
    is_pairs_data_fresh = (BusinessDaysSincePreviousEvent(
            inputs=[pairs_self_serve_dataset.asof_date]) <= 1)
    is_es_data_fresh = (BusinessDaysSincePreviousEvent(
            inputs=[fq0_eps_act.asof_date]) <= 30)
    
    
    pipe = Pipeline()  
    earnings_surprises = np.asarray([])
    for col in pairs_self_serve_dataset.columns:
        if 'group' in col.name:  
            earnings_surprises_per_group = Pairs_ES(
                inputs=[col.latest, 
                        fq0_surprise],
                mask=is_pairs_data_fresh
            )
            earnings_surprises = np.append(earnings_surprises,
                                           earnings_surprises_per_group) 
    
    alpha_factor =  MeanFactor(inputs=earnings_surprises).winsorize(.2,.98)  
    pipe.add(alpha_factor, 'es_factor')
    screen_zeros = (alpha_factor != 0.0)
    screen =(pairs_self_serve_dataset.trade_date.latest.notnull()\
             &is_es_data_fresh&alpha_factor.notnull()\
             &screen_zeros\
             &QTradableStocksUS())
    pipe.set_screen(screen)
    return pipe

def before_trading_start(context, data):
    """
    Optional core function called automatically before the open of each market day.
 
    Parameters
    ----------
    context : AlgorithmContext
        See description above.
    data : BarData
        An object that provides methods to get price and volume data, check
        whether a security exists, and check the last time a security traded.
    """
 
    # Store the output of your pipeline in the context variable:
    context.pipeline_data = algo.pipeline_output('my_data_template')
 
    # This dataframe will contain all of our risk loadings
    context.risk_loadings = algo.pipeline_output('risk_factors')
    
 
def rebalance(context, data):
    """
    A function scheduled to run once every Monday at 10AM ET in order to
    rebalance the longs and shorts lists.
 
    Parameters
    ----------
    context : AlgorithmContext
        See description above.
    data : BarData
        See description above.
    """
    # Retrieve pipeline output.
    pipeline_data = context.pipeline_data
 
    # Retrieve risk model factor data.
    risk_loadings = context.risk_loadings
 
    # Here we define our objective for the Optimize API. In this template 
    # we use the MaximizeAlpha objective.

    if not pipeline_data.es_factor.empty:
        objective = opt.MaximizeAlpha(pipeline_data.es_factor)
    # Define the list of constraints.
        constraints = []
    
    # Constrain our maximum gross leverage.
        constraints.append(opt.MaxGrossExposure(MAX_GROSS_LEVERAGE))
 
    # Require our algorithm to remain dollar neutral.
        #constraints.append(opt.DollarNeutral())
 
    # Add the RiskModelExposure constraint to make use of the
    #default risk model constraints.
        neutralize_risk_factors = opt.experimental.RiskModelExposure(
            risk_model_loadings=risk_loadings,
            version=0
        )
        constraints.append(neutralize_risk_factors)
 
    # With this constraint we enforce that no position can make up
    # greater than MAX_SHORT_POSITION_SIZE on the short side and
    # no greater than MAX_LONG_POSITION_SIZE on the long side. This
    # ensures that we do not overly concentrate our portfolio in
    # one security or a small subset of securities.
        constraints.append(
            opt.PositionConcentration.with_equal_bounds(
                min=-MAX_SHORT_POSITION_SIZE,
                max=MAX_LONG_POSITION_SIZE
            ))
 
    # Put together all the pieces we defined above by passing
    # them into the algo.order_optimal_portfolio function. This handles
    # all of our ordering logic, assigning appropriate weights
    # to the securities in our universe to maximize our alpha with
    # respect to the given constraints.
        algo.order_optimal_portfolio(
            objective=objective,
            constraints=constraints
    )
There was a runtime error.

@Rene, thanks for this...interesting!!

In trying to run-it-without-thinking, I get the error:

# from quantopian.pipeline.data.user_[user_ID] import [dataset name]  
from quantopian.pipeline.data.user_57e2b12557e9c947ce001019 import pairs_self_serve_dataset  
ImportErrorTraceback (most recent call last)  
<ipython-input-4-cdb381e4d13e> in <module>()  
      1 # from quantopian.pipeline.data.user_[user_ID] import [dataset name]  
----> 2 from quantopian.pipeline.data.user_57e2b12557e9c947ce001019 import pairs_self_serve_dataset

/build/src/qexec_repo/qexec/algo/safety.py in __call__(self, name, globals, locals, fromlist, level)
    265         # this is a whitelisted import  
    266         return self._import_safety.make_safe(  
--> 267             self._import(name, globals, locals, fromlist, level),  
    268         )  
    269 

ImportError: No module named user_57e2b12557e9c947ce001019  

THis probably has to do with my account not having a copy of the pairs_self_serve_dataset.csv file ?
Any pointers will help.
Thanks!
alan

@Alan To upload the pairs_self_serve_dataset.csv file, please navigate to the Data tab on your account page and click "Add Dataset". You may want to refer the step 1 (Upload the Daily Lists of Pairs to Q Platform) in the notebook for selecting the Primary Date and Primary Asset fields, and declare the data types of the other fields. After the dataset is uploaded, it will have a corresponding information page. You could find your user_id there.

Please feel free to let me know if you have any further questions!

@Rene,
Are you going to supply the file pairs_self_serve_dataset.csv on Google Drive or DropBox, or are we to create it? Thanks,
alan

@Alan,
It looks like it's linked under the line "a set of cointegrated pairs" in the original post.

@Kyle,
Ahh...ok...mystery solved!
Thanks!
alan

Just wondering if anyone played around with this and either ran into problems or came up with something interesting we might want to license!

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.

@Rene,
Thanks for this post!
I believe there is lots of play in cthe cointegrated spce, yet, it's hard to understand, so please bear with my questions.

I instrumented your backtest a bit and changed it so that it goes to "Cash" if there are not enough pairs to play with.

The main question I have is how the long/short allocation of a pair works...they are out of balance(see Activity-ShortLever-LongLever traces).

A secondary question relates to how to create the "Pairs" .csv file. I'm assuming that you are using something like J.Larkin's co-integrated clustering method published previously in this form.

I've included the backtest that shows the best of what I've been able to see over the past two-year period.
alan

Clone Algorithm
8
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 quantopian.algorithm as algo
import quantopian.optimize as opt
from quantopian.pipeline import Pipeline, CustomFactor
from quantopian.pipeline.factors import BusinessDaysSincePreviousEvent, SimpleMovingAverage
 
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.experimental import risk_loading_pipeline
from quantopian.pipeline.classifiers.fundamentals import Sector

from quantopian.pipeline.data import Fundamentals
import pandas as pd
import quantopian.pipeline.data.factset.estimates as fe  
import numpy as np
# TODO: Un-comment this line to import your dataset here.
#from quantopian.pipeline.data.user_[user_ID] import [dataset name]
#Zheng from quantopian.pipeline.data.user_57e2b12557e9c947ce001019 import pairs_self_serve_dataset
# from quantopian.pipeline.data.user_[user_ID] import [dataset name] 
from quantopian.pipeline.data.user_52670e6a2763c5cfb70002c7 import pairs_self_serve_dataset 
# Constraint Parameters
MAX_GROSS_LEVERAGE = 1.5
MAX_SHORT_POSITION_SIZE = 0.025 #0.025 #0.02
MAX_LONG_POSITION_SIZE  = 0.025  #0.025 #0.02
MIN_ASSETS_TRIGGER = 10

class Pairs_ES(CustomFactor):
    window_length = 1
    def compute(self, today, asset_ids, out, pair_groups, earnings_surprises):
        pairs = pd.Series(pair_groups[0,:], index=asset_ids)
        pairs = pairs[pairs != 0]
        earnings_surprises = pd.Series(earnings_surprises[0,:], index=asset_ids)
        output = pd.Series(index=asset_ids)
        for _, pair in pairs.groupby(pairs): # find stocks that share the same value  
            if len(pair.index) == 2:           
                (leg_1, leg_2) = pair.index
                # assign the earnings_surprises score of pair leg 2 to pair leg 1
                output[leg_1] = np.sign(earnings_surprises[leg_2])
                # assign the earnings_surprises score of pair leg 1 to pair leg 2
                output[leg_2] = np.sign(earnings_surprises[leg_1])
        out[:] = output.values
        
class MeanFactor(CustomFactor):
    window_length=1
    def compute(self, today, asset_ids, out, *inputs):
        output = pd.DataFrame(index=asset_ids)
        for i in range(0, len(inputs)):
            output['col_' + str(i)] = inputs[i][0,:]
        out[:] = output.mean(axis=1).values

def recording_statements(context, data):
    print("num_positions={}".format(len(context.portfolio.positions)))
    #record(
    #num_positions=len(context.portfolio.positions))
    record(cash = context.portfolio.cash/1000000.0) 
    record(cap_used = context.portfolio.capital_used) 
    longs = shorts = 0   
    for position in context.portfolio.positions.itervalues():        
        if position.amount > 0:
            longs += 1
        if position.amount < 0:
            shorts += 1          
    record(long_lever=longs, short_lever=shorts)
                
 
def initialize(context):
    """
    A core function called automatically once at the beginning of a backtest.
 
    Use this function for initializing state or other bookkeeping.
 
    Parameters
    ----------
    context : AlgorithmContext
        An object that can be used to store state that you want to maintain in 
        your algorithm. context is automatically passed to initialize, 
        before_trading_start, handle_data, and any scheduled functions.
        context provides the portfolio attribute, which can be used to retrieve 
        information about current positions.
    """
    
    algo.attach_pipeline(make_pipeline(), 'my_data_template')
 
    # Attach the pipeline for the risk model factors that we
    # want to neutralize in the optimization step. The 'risk_factors' string is 
    # used to retrieve the output of the pipeline in before_trading_start below.
    algo.attach_pipeline(risk_loading_pipeline(), 'risk_factors')
 
    # Schedule your trade logic via this rebalance function.
    algo.schedule_function(
        func=rebalance,
        date_rule=algo.date_rules.every_day(),
        time_rule=algo.time_rules.market_open(hours=0, minutes=10) #10
    )
    # record my portfolio variables at the end of day
    algo.schedule_function(func=recording_statements,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(),
                      half_days=True)
 
 
def make_pipeline():
    """
    A function that creates and returns our pipeline.
 
    We break this piece of logic out into its own function to make it easier to
    test and modify in isolation. In particular, this function can be
    copy/pasted into research and run by itself.
 
    Returns
    -------
    pipe : Pipeline
        Represents computation we would like to perform on the assets that make
        it through the pipeline screen.
    """
    
    # Slice the PeriodicConensus and Actuals DataSetFamilies into DataSets. In this context,  
    # fq0_eps_cons is a DataSet containing consensus estimates data about EPS for the  
    # most recently reported fiscal quarter. fq0_eps_act is a DataSet containing the actual  
    # reported EPS for the most recently reported quarter.  
    fq0_eps_cons = fe.PeriodicConsensus.slice('EPS', 'qf', 0)  
    fq0_eps_act = fe.Actuals.slice('EPS', 'qf', 0)

    # Get the latest mean consensus EPS estimate for the last reported quarter.  
    fq0_eps_cons_mean = fq0_eps_cons.mean.latest

    # Get the EPS value from the last reported quarter.  
    fq0_eps_act_value = fq0_eps_act.actual_value.latest

    # Define a surprise factor to be the relative difference between the estimated and  
    # reported EPS.  
    fq0_surprise = (fq0_eps_act_value - fq0_eps_cons_mean) / fq0_eps_cons_mean
    
    is_pairs_data_fresh = (BusinessDaysSincePreviousEvent(
            inputs=[pairs_self_serve_dataset.asof_date]) <= 1)
    is_es_data_fresh = (BusinessDaysSincePreviousEvent(
            inputs=[fq0_eps_act.asof_date]) <= 30 ) #15) #30
    
    sector = Sector()      
    pipe = Pipeline()  
    earnings_surprises = np.asarray([])
    for col in pairs_self_serve_dataset.columns:
        if 'group' in col.name:  
            earnings_surprises_per_group = Pairs_ES(
                inputs=[col.latest, 
                        fq0_surprise],
                mask=is_pairs_data_fresh
            )
            earnings_surprises = np.append(earnings_surprises,
                                           earnings_surprises_per_group) 
    pre_screen =(pairs_self_serve_dataset.trade_date.latest.notnull() & QTradableStocksUS())    
    alpha_factor =  MeanFactor(inputs=earnings_surprises).winsorize(.2,.98)#.zscore()
    #alpha_factor =  MeanFactor(inputs=earnings_surprises).zscore(mask=pre_screen,groupby=Sector())  #(.2,.98)
    pipe.add(alpha_factor, 'es_factor')
    pipe.add(sector, 'sector')
    screen_zeros = (alpha_factor != 0.0)
    
    screen =(pre_screen\
             &is_es_data_fresh&alpha_factor.notnull()\
             &screen_zeros
            )
    pipe.set_screen(screen)
    return pipe

def before_trading_start(context, data):
    """
    Optional core function called automatically before the open of each market day.
 
    Parameters
    ----------
    context : AlgorithmContext
        See description above.
    data : BarData
        An object that provides methods to get price and volume data, check
        whether a security exists, and check the last time a security traded.
    """
 
    # Store the output of your pipeline in the context variable:
    context.pipeline_data = algo.pipeline_output('my_data_template')
 
    # This dataframe will contain all of our risk loadings
    context.risk_loadings = algo.pipeline_output('risk_factors')
    
 
def rebalance(context, data):
    """
    A function scheduled to run once every Monday at 10AM ET in order to
    rebalance the longs and shorts lists.
 
    Parameters
    ----------
    context : AlgorithmContext
        See description above.
    data : BarData
        See description above.
    """
    # Retrieve pipeline output.
    pipeline_data = context.pipeline_data
 
    # Retrieve risk model factor data.
    risk_loadings = context.risk_loadings
 
    # Here we define our objective for the Optimize API. In this template 
    # we use the MaximizeAlpha objective.

    if not pipeline_data.es_factor.empty:
        objective = opt.MaximizeAlpha(pipeline_data.es_factor)
    # Define the list of constraints.
        constraints = []
    
    # Constrain our maximum gross leverage.
        constraints.append(opt.MaxGrossExposure(MAX_GROSS_LEVERAGE))
 
    # Require our algorithm to remain dollar neutral.
       # constraints.append(opt.DollarNeutral(tolerance=0.01))
 
    # Add the RiskModelExposure constraint to make use of the
    #default risk model constraints.
        neutralize_risk_factors = opt.experimental.RiskModelExposure(
            risk_model_loadings=risk_loadings,
            version=0
        )
        constraints.append(neutralize_risk_factors)
 
    # With this constraint we enforce that no position can make up
    # greater than MAX_SHORT_POSITION_SIZE on the short side and
    # no greater than MAX_LONG_POSITION_SIZE on the long side. This
    # ensures that we do not overly concentrate our portfolio in
    # one security or a small subset of securities.
        constraints.append(
            opt.PositionConcentration.with_equal_bounds(
                min=-MAX_SHORT_POSITION_SIZE,
                max=MAX_LONG_POSITION_SIZE
            ))
       # constraints.append(
       #     opt.NetGroupExposure.with_equal_bounds(
        #    labels=pipeline_data.sector,
        #    min=-0.2,
        #    max=0.2,
        # ))
        
    # Put together all the pieces we defined above by passing
    # them into the algo.order_optimal_portfolio function. This handles
    # all of our ordering logic, assigning appropriate weights
    # to the securities in our universe to maximize our alpha with
    # respect to the given constraints.
    if len(pipeline_data) < MIN_ASSETS_TRIGGER:
        #Hold only Cash, not enough Pairs
        print("len(pipeline_data)=", len(pipeline_data), "MIN_ASSETS_TRIGGER=", MIN_ASSETS_TRIGGER)

        algo.order_optimal_portfolio(
            objective=opt.TargetWeights({sid(8554): 0.0}) ,
            constraints=[],
        )
    else:
        print("len(pipeline_data)=", len(pipeline_data), "pipeline_stats=\n",pipeline_data.describe())
        algo.order_optimal_portfolio(
            objective=objective,
            constraints=constraints)
There was a runtime error.

@Alan Thanks for the questions! About question (1), the number of daily long positions may not equal to the number of daily short positions. To simplify the problem, let us just consider one pair (A, B) and set the earnings data to be fresh if it's not more than 1 day old. Assume stock A and stock B have their earnings announcements on different dates. After stock A releasing its earnings, we compute its earnings surprise and assign this value to stock B as stock B’s score (i.e. factor value). So, only B here is assigned with a score. If A’s earnings surprise is positive, B has a positive score and we would like long B. If A’s earnings surprise is negative, B has a negative score and we would like short B. Therefore, the number of daily long positions is not necessarily equal to the number of daily short positions. About Question 2, the way I used to generate the list of pairs is based on this lecture (Introduction to Pairs Trading).