Back to Community
Help with Custom Factor

Hi all,
I know there are quite a bit of these threads but after looking over several, I became no wiser.

I want to write a custom factor that fetches returns over the last year, resamples them to monthly and ultimately returns the cumulative product of these monthly returns (for the purpose of making a filter). How can I go about this?

Thank you all in advance.

11 responses

You may not need a custom factor for this. However, not completely sure what you intend by 'cumulative product of monthly returns'. Are you wanting to get the total return from the monthly returns? Something like this?

month_1_return : 1%  
month_2_return : 2%  
month_3_return : -1%  
total_returns : ?

In the above case the total return would be

1.01 x 1.02 x .99 = 1.019898  

this equals 1.9898%

Is that the goal?

Hi Dan, thanks for answering!
No, the goal would be

0.01 x 0.02 x -0.01  

I don't care about the actual product in the end, only the sign!

Gotcha. Another question then... Do the monthly returns need to be 'true' 1st of the month - to 1st of the month returns, or, could an average previous 21 trading days returns be good enough (there are about 21 trading days per month on the average).

I know this isn't an exact solution to what you described above, but hopefully it will help guide in the right direction.

When you initialize your algo, you can add the below Custom Factor:

Monthly Return Factor = Price of current day / Price of 21 days ago

class MonthlyReturns(CustomFactor):  

# predeclare your inputs and window length  
inputs = [USEquityPricing.close]  
window_length=21

# return out the montly return value  
def compute(self, today, assets, out, close):  
    out[:] = close[-1]/close[0]

You could set your pipeline to update once per month, that way once per month you are adding 21 day returns to your pipeline (i.e. one month of trading days).

Once you have added this factor to the pipeline with something like:

monthly_returns = MonthlyReturns()
pipe.add(monthly_returns, 'monthly_returns')

In your order function, you can maybe implement an if statement on the return column for your algo to go long on stocks that are > 0 in this regard and short stock that are < 0 monthly returns. In the attached, I just went long on the top 100 returning stocks each month.

Clone Algorithm
11
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 LIBRARIES
#=========================================================================================
# IMPORT PIPELINE
from quantopian.pipeline import Pipeline
from quantopian.pipeline import CustomFactor

# IMPORT TWO PIPELINE FUNCTIONS NECESSARY FOR ALGORITHM
from quantopian.algorithm import attach_pipeline, pipeline_output    

# IMPORT DATASETS  
from quantopian.pipeline.data.builtin import USEquityPricing  
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.data import Fundamentals  

# IMPORT FILTERS, FACTORS, AND CLASSIFIERS  
from quantopian.pipeline.filters import QTradableStocksUS  


# DEFINE CUSTOM FACTOR                      
#=========================================================================================
        
# Monthly Return Factor = Price of current day / Price of 21 days ago
class MonthlyReturn(CustomFactor):
    
    # predeclare your inputs and window length
    inputs = [USEquityPricing.close]
    window_length=21
    
    # return out the return value  
    def compute(self, today, assets, out, close):
        out[:] = close[-1]/close[0]
        
                
# INITIALIZE ALGORITHM                      
#=========================================================================================                    
def initialize(context):
    
    # ATTACH PIPELINE NAME
    pipe = Pipeline()
    attach_pipeline(pipe, 'my pipe')
   
    # UNIVERSE DECLARATION
    universe = QTradableStocksUS()
    
    # ADD SECTOR TO THE PIPELINE
    sector = Fundamentals.morningstar_sector_code.latest
    pipe.add(sector, 'sector')  
    
    # ADD MONTHLY RETURN FACTOR
    return_factor = MonthlyReturn()
    pipe.add(return_factor, 'return_factor')        
    return_rank = return_factor.rank(mask=universe)
    pipe.add(return_rank, 'return_rank')                   
    
    
# SETTINGS
#=========================================================================================
    
    
    # PIPELINE SCREEN SETTINGS
    pipe.set_screen(universe)
    
    # BENCHMARK SETTINGS (SPY DEFAULT)
    #set_benchmark(symbol('SPY'))
    
    # SCHEDULE SETTINGS FOR THE 'REBALANCE' FUNCTION
    schedule_function(func=rebalance, 
                      date_rule=date_rules.month_start(days_offset=0), 
                      time_rule=time_rules.market_open(hours=0,minutes=30), 
                      half_days=True)
    
    # SCHEDULE SETTINGS FOR THE 'RECORD VARS' FUNCTION
    schedule_function(func=record_vars,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close(),
                      half_days=True)

    
    # LEVERAGE SETTINGS (CAN BE ADJUSTED)
    context.long_leverage = 1.0
    context.short_leverage = 0.0 

       
# EVERYDAY BEFORE TRADING START: 
#=========================================================================================                      
def before_trading_start(context, data):
    
    # CALL PIPELINE BEFORE TRADING START (FILL N/A IS DEFAULT TO NaN)
    context.output = pipeline_output('my pipe').fillna(1000)
      
    # DEFINE NUMBER OF SECURITIES TO LONG AND SHORT BASED ON INDEX LOCATION 
    context.long_list = context.output.sort_values(['return_rank'], ascending=False).iloc[:100]
    context.short_list = context.output.sort_values(['return_rank'], ascending=False).iloc[-100:]   

                
                
# RECORD AND RETURN VARIABLES: RETURN THE TOP TEN RETURN RANKING STOCKS FROM THE LONG AND SHORT LISTS       
#=========================================================================================                           
def record_vars(context, data):  
    
    # RECORDED METRICS DURING BACKTEST -- LEVERAGE 
    record(leverage = context.account.leverage)
        
    # PRINT TOP 10 DAILY LONG AND SHORT POSITIONS
    print "Long List"
    log.info("\n" + str(context.long_list.sort_values(['return_rank'], ascending=True).head(10)))
    
    print "Short List" 
    log.info("\n" + str(context.short_list.sort_values(['return_rank'], ascending=True).head(10)))      
                
                
               
# REBALANCE
#=========================================================================================  
def rebalance(context,data):
    
    # DEFINE THE TARGET WEIGHT OF EACH STOCK IN THE PORTFOLIO
    long_weight = context.long_leverage / float(len(context.long_list))
    short_weight = context.short_leverage / float(len(context.short_list))

    # FOR EACH STOCK THAT WE HAVE CLASSIFIED IN OUR LONG AND SHORT LISTS, 
    # WE WANT TO PLACE A MARKET ORDER BASED ON THE DEFINED TARGET PERCENT. 
    # THE ORDER WILL BE EXECUTED WHEN THE FUNCTION IS SCHEDULED TO BE CALLED 
    # (SEE SETTINGS)
    
    for long_stock in context.long_list.index:
        log.info("ordering longs")
        log.info("weight is %s" % (long_weight))
        order_target_percent(long_stock, long_weight)
        
    for short_stock in context.short_list.index:
        log.info("ordering shorts")
        log.info("weight is %s" % (short_weight))
        order_target_percent(short_stock, short_weight)
        
    # EXIT ANY POSITIONS THAT ARE NO LONGER ON OUR LONG OR SHORT LIST    
    for stock in context.portfolio.positions.iterkeys():
        if stock not in context.long_list.index and stock not in context.short_list.index:
            order_target(stock, 0)
            
                        
#=========================================================================================================================
There was a runtime error.

Dan: I think median or mean could be a suitable substitute yes!
Brooks: Thank you for taking the time to write that out, but it is unfortunately not what I need. I am essentially trying to do a double-sort as a part of a momentum strategy. I essentially want to classify stocks to either have positive or negative time-series momentum (the sign of the cumulative product of monthly returns) and then calculate cross-sectional momentum (like your factor does) for both the positive and negative time-series partitions of the data.

Ultimately I want to long stocks with high cross-sectional momentum and positive time-series momentum and short the losers in the negative time-series partition.

Nikolas,

I made a few adjustments after having a better idea of what you were looking for. I simplified the factors using the built-in Returns class. I also added a column to the pipeline that calculates the running product of the monthly returns. If the cumulative product is positive it will display 'True' in this column. Cumulative returns that are <= 0 will show as 'False' in this column. To accomplish the double-sort, these are used as filters for the long and short lists before they are then ranked for cross-sectional momentum. We go long the top 50 monthly returning stocks that have a positive cumulative return product. We go short the bottom 50 monthly returning stocks that have a negative cumulative return product. I have not implemented the Optimize API or any other constraints other than leverage. Hopefully this is a little more helpful than before --I try to make the comments very descriptive.

Clone Algorithm
11
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 LIBRARIES
#=========================================================================================
import numpy as np
import pandas as pd

# IMPORT PIPELINE
from quantopian.pipeline import Pipeline
from quantopian.pipeline import CustomFactor


# IMPORT TWO PIPELINE FUNCTIONS NECESSARY FOR ALGORITHM
from quantopian.algorithm import attach_pipeline, pipeline_output    

# IMPORT DATASETS  
from quantopian.pipeline.data.builtin import USEquityPricing  
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.data import Fundamentals  

# IMPORT FILTERS, FACTORS, AND CLASSIFIERS  
from quantopian.pipeline.filters import QTradableStocksUS  
from quantopian.pipeline.factors import Returns

                
# INITIALIZE ALGORITHM                      
#=========================================================================================                    
def initialize(context):
    
    # UNIVERSE DECLARATION
    universe = QTradableStocksUS()
    
    # ATTACH PIPELINE NAME
    pipe = Pipeline(screen=universe)
    attach_pipeline(pipe, 'my pipe')
    
    # ADD SECTOR TO THE PIPELINE
    sector = Fundamentals.morningstar_sector_code.latest
    pipe.add(sector, 'sector')  
    
    # ADD MONTHLY RETURN FACTOR AND RANKED RETURN FACTOR
    monthly_return = Returns(window_length=21)
    monthly_rank = monthly_return.rank(mask=universe)
    pipe.add(monthly_return, 'Mreturn')
    pipe.add(monthly_rank, 'Mrank')
    
    # ADD CUMULATIVE MONTHLY RETURNS TO THE PIPELINE
    sum_return = np.prod(monthly_return)
    pipe.add(sum_return, 'sumReturn')
    
    # DEFINE OUR PRE-FILTERED LONG AND SHORT LISTS
    longs = (sum_return > 0) 
    pipe.add(longs, 'longs')    
    
        
# SETTINGS
#=========================================================================================    
    
    # SCHEDULE SETTINGS FOR THE 'REBALANCE' FUNCTION
    schedule_function(func=rebalance, 
                      date_rule=date_rules.month_start(), 
                      time_rule=time_rules.market_open(), 
                      half_days=True)
    
    # SCHEDULE SETTINGS FOR THE 'RECORD VARS' FUNCTION
    schedule_function(func=record_vars,
                      date_rule=date_rules.month_start(),
                      time_rule=time_rules.market_open(),
                      half_days=True)

    
    # LEVERAGE SETTINGS (CAN BE ADJUSTED)
    context.long_leverage = 0.5
    context.short_leverage = -0.5 

       
# EVERYDAY BEFORE TRADING START: 
#=========================================================================================                      
def before_trading_start(context, data):
    
    # CALL PIPELINE BEFORE TRADING START
    context.output = pipeline_output('my pipe')
    
    # IF A STOCK HAS ALREADY BEEN CATEGORIZED AS A LONG, TAKE THE TOP OF THE MONTHLY RETURNS
    for stock in context.output['longs'] == 0:     
        context.long_list = context.output.sort_values(['Mrank'], ascending=False).iloc[:50]
        
    # IF A STOCK HAS ALREADY BEEN CATEGORIZED AS NOT A LONG (SHORT), TAKE THE BOTTOM OF THE MONTHLY RETURNS
    for stock in context.output['longs'] != 0:
        context.short_list = context.output.sort_values(['Mrank'], ascending=False).iloc[-50:]  
                
                
# RECORD AND RETURN VARIABLES: RETURN THE TOP TEN RETURN RANKING STOCKS FROM THE LONG AND SHORT LISTS       
#======================================================================================                           
def record_vars(context, data):  
    
    # RECORDED METRICS DURING BACKTEST -- LEVERAGE 
    record(leverage = context.account.leverage)
        
    # PRINT TOP 10 DAILY LONG AND SHORT POSITIONS
    print "Long List"
    log.info("\n" + str(context.long_list.sort_values(['Mrank'], ascending=True).head(10)))
    
    print "Short List" 
    log.info("\n" + str(context.short_list.sort_values(['Mrank'], ascending=True).head(10)))      
    
               
# REBALANCE
#=========================================================================================  
def rebalance(context,data):
    
    # DEFINE THE TARGET WEIGHT OF EACH STOCK IN THE PORTFOLIO
    long_weight = context.long_leverage / float(len(context.long_list))
    short_weight = context.short_leverage / float(len(context.short_list))

    # FOR EACH STOCK THAT WE HAVE CLASSIFIED IN OUR LONG AND SHORT LISTS, 
    # WE WANT TO PLACE A MARKET ORDER BASED ON THE DEFINED TARGET PERCENT. 
    # THE ORDER WILL BE EXECUTED WHEN THE FUNCTION IS SCHEDULED TO BE CALLED 
    # (SEE SETTINGS)
     
    for long_stock in context.long_list.index:
        log.info("ordering longs")
        log.info("weight is %s" % (long_weight))
        order_target_percent(long_stock, long_weight)
       
        
    for short_stock in context.short_list.index:
        log.info("ordering shorts")
        log.info("weight is %s" % (short_weight))
        order_target_percent(short_stock, short_weight)
              
        
    # EXIT ANY POSITIONS THAT ARE NO LONGER ON OUR LONG OR SHORT LIST    
    for stock in context.portfolio.positions.iterkeys():
        if stock not in context.long_list.index and stock not in context.short_list.index:
            order_target(stock, 0)
There was a runtime error.

Thank you so much Brooks! I will try to re-create this logic in the Research Environment as well, just so I understand fully what is happening and I'll work on implementing the Optimize API.

One question regarding line 46 though: Doesn't the Returns factor return a single value as opposed to a return series? Would it not be more appropriate to use the DailyReturns factor?

For anyone interested, I ended up doing this to achieve my goal:

class TSMomentum(CustomFactor):  
    inputs = [USEquityPricing.close]  
    window_length=252  
    def compute(self, today, assets, out, close):  
        monthly_price = close[::21,  :]  
        monthly_returns = np.diff(monthly_price, axis=0) / monthly_price[:-1,:]  
        out[:] = np.prod(monthly_returns, axis=0)


does that factor have alpha?

I plan to use it in conjunction with other factors, this one is really just for filtering purposes, but probably not, no. That doesn't really matter too much to me because I am doing this as part of a research paper. If it sucks, that is a completely valid result in the end.