Back to Community
Custom Factor - Persistent Net Accruals Growth

Here is an interesting approach to a long only strategy using a custom factor. I provide a little more detail (yet still brief) on the strategy in the beginning of the algorithm code. Thought I would share this one with the community. Open to any suggestions, comments, criticisms.

Clone Algorithm
48
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
"""

                             Investing in Companies With Persistent Net Accruals Growth
                                                    By Frank Vigilante
                                                    
Abstract:

Earnings quality is a very important topic to understand when analyzing the financial statements of publically traded companies. At its core, it highlights how the difference between cash and accrual basis accounting can impact reported earnings for better or worse. The custom factor I developed below adheres to the notion that short term accounting differences (absent of fraud), will always normalize over the long term. As such, a company with growth in net accruals over many consecutive quarters is likely working overtime to grow their business relative to their peers. Basically this strategy is simply seeking the "go-getters" of the market, and comparing their performance against the performance of the overall S&P500. 

Strategy Mechanics:

Custom Class - The factor below calculates net accruals growth for all companies using data from earnings reports stretching 150,120,90,30, and 1 day in the past. It then sums net accruals for all the included time periods to obtain a "net accrual growth" figure. This figure serves as the basis for stocks that are to be purchased. It should be noted that the utilization of redundant data (i.e. the 1 and 30 day data will likely be from the same earnings report) is intentional. The idea behind this was to generate a means of weighting more recent data against older earnings data.  

Filters - In addition to only purchasing the stocks selected from the persistent accruals growth factor, the algorithm also requires the following: 1) Stocks in Top 500 by Market Cap 2) Average trading volume greater than 100K shares over prior 20 days 

Ordering Logic - All stocks in the system are equally weighted. When a stock no longer qualifies as a persistent accruals growth stock, it is sold from the portfolio. Rebalancing is done every day.

Leverage - This algorithm attempts to ensure 100% of cash is utilized without borrowing funds.

Charts - The algorithm is set to display the quantity of stocks within the portfolio. Leverage is also plotted.


                                        

"""



from quantopian.pipeline.data.quandl import rateinf_inflation_usa
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing 
from quantopian.pipeline import CustomFactor 
from quantopian.pipeline.data import morningstar 
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import AverageDollarVolume
import numpy as np

class Earnings_Quality_150(CustomFactor):
    inputs = [morningstar.cash_flow_statement.end_cash_position, morningstar.cash_flow_statement.beginning_cash_position, morningstar.income_statement.net_income, morningstar.cash_flow_statement.cash_dividends_paid, morningstar.cash_flow_statement.common_stock_issuance, morningstar.cash_flow_statement.repurchase_of_capital_stock]
    window_length = 150
    def compute(self, today, assets, out, cash_end, cash_begin, net_income, dividend, stock_issuance, repurchase):
        out[:] = (net_income[0] - (cash_begin[0] - cash_end[0]) + dividend[0] + repurchase[0] + stock_issuance[0]) + (net_income[-120] - (cash_begin[-120] - cash_end[-120]) + dividend[-120] + repurchase[-120] + stock_issuance[-120]) + (net_income[-90] - (cash_begin[-90] - cash_end[-90]) + dividend[-90] + repurchase[-90] + stock_issuance[-90]) + (net_income[-30] - (cash_begin[-30] - cash_end[-30]) + dividend[-30] + repurchase[-30] + stock_issuance[-30]) + (net_income[-1] - (cash_begin[-1] - cash_end[-1]) + dividend[-1] + repurchase[-1] + stock_issuance[-1])

class MarketCap(CustomFactor):
    inputs = [USEquityPricing.close, morningstar.valuation.shares_outstanding]
    window_length = 1
    def compute(self, today, assets, out, close, shares):
        out[:] = close[-1] * shares[-1]


def initialize(context):
    attach_pipeline(my_pipeline(context), 'my_pipeline')
    set_slippage(slippage.FixedSlippage(spread=0.00))
    set_commission(commission.PerShare(cost=0.0, min_trade_cost=0.0))
    schedule_function(execute_trade,
                      date_rules.every_day(),
                      time_rules.market_open(hours=0, minutes=1))
def my_pipeline(context):
    pipe = Pipeline()
    dollar_volume = AverageDollarVolume(window_length=20)
    minimum_volume = dollar_volume > 100000
    mkt_cap = MarketCap()
    mkt_cap_top_500 = mkt_cap.top(500)
    Earning = Earnings_Quality_150()
    Quality = Earning > 0
    pipe.set_screen(minimum_volume & mkt_cap_top_500 & Quality)
    Earning_Quality_150 = Earnings_Quality_150()
    pipe.add(Earning_Quality_150, 'Earnings_Quality_150')
    return pipe

def before_trading_start(context, data):
    context.Buy_List = []
    context.output = pipeline_output('my_pipeline')
    for stock in context.output.index:
        context.Buy_List.append(stock)
    
def execute_trade(context, data):
    Buy_List = context.Buy_List
    Weight = 1.0 / len(Buy_List)
    for stock in context.portfolio.positions:
        if stock not in Buy_List:
            order_target_percent(stock, 0)
    for stock in Buy_List:
        if data.can_trade(stock):
            order_target_percent(stock, Weight)
    leverage = context.account.leverage
    record(leverage=leverage)
    Quantity = len(Buy_List)
    record(Quantity=Quantity)
    
            
            
There was a runtime error.
5 responses

Attached is the tear sheet.

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

What amazed me about the superior risk adjusted returns of this strategy is that the draw down was actually less than the S&P500 during the 08-09 recession...52% v 54% (S&P500 strategy below). I would have thought that buying companies with persistent net accruals growth would have exposed the portfolio to significantly more counter party risk heading into the recession, forcing large write offs. I guess the combination of diversification and persistence outweighs the incremental credit risk.

Open to any ideas that critique this factor?

Clone Algorithm
2
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
def initialize(context):
    context.SPY = symbol('SPY')
def handle_data(context, data):
    order_target_percent(context.SPY, 1.0)
There was a runtime error.

I had a few thoughts:

  • The best benchmark to use will be your universe (market cap + volume filters). Since you have no trading costs, I would expect this to perform better than SPY. I would suspect the volume filter also might effect the performance slightly.

  • It would be good to run this long short, to remove the beta. You could rank and use top 10% and bottom 10% to choose your longs and shorts, rather than comparing to the fixed Quality factor score of zero. You could also beta hedge. I have some code for this if you like.

  • AlphaLens would tell you how predictive your ranking is for future returns.

Burrito Dan,

This shows why it is good to share with the community. 99% of the time that I run a back test on a factor model, I do it with Q default slippage and commission. Somehow I must have popped zero trade costs into this algo, and then forgot about it. I'm going run a back test with transaction costs, but I am not going to expect much....

Thanks again for pointing this out.

Burrito Dan,

Here are the results with default slippage/commission. Looks like this dog can still hunt.

Moving on to your other thoughts:

"It would be good to run this long short, to remove the beta. You could rank and use top 10% and bottom 10% to choose your longs and shorts, rather than comparing to the fixed Quality factor score of zero. You could also beta hedge. I have some code for this if you like."

  • I am not sure that I would want to short any of the stocks in this portfolio because the purpose was to seek stocks that will outperform using the factor. Also do not want to reduce the quantity in the portfolio for fear of losing the diversification. Maybe it would be a better idea to create an independent short factor to make this a long/short?
Clone Algorithm
48
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
"""

                             Investing in Companies With Persistent Net Accruals Growth
                                                    By Frank Vigilante
                                                    
Abstract:

Earnings quality is a very important topic to understand when analyzing the financial statements of publically traded companies. At its core, it highlights how the difference between cash and accrual basis accounting can impact reported earnings for better or worse. The custom factor I developed below adheres to the notion that short term accounting differences (absent of fraud), will always normalize over the long term. As such, a company with growth in net accruals over many consecutive quarters is likely working overtime to grow their business relative to their peers. Basically this strategy is simply seeking the "go-getters" of the market, and comparing their performance against the performance of the overall S&P500. 

Strategy Mechanics:

Custom Class - The factor below calculates net accruals growth for all companies using data from earnings reports stretching 150,120,90,30, and 1 day in the past. It then sums net accruals for all the included time periods to obtain a "net accrual growth" figure. This figure serves as the basis for stocks that are to be purchased. It should be noted that the utilization of redundant data (i.e. the 1 and 30 day data will likely be from the same earnings report) is intentional. The idea behind this was to generate a means of weighting more recent data against older earnings data.  

Filters - In addition to only purchasing the stocks selected from the persistent accruals growth factor, the algorithm also requires the following: 1) Stocks in Top 500 by Market Cap 2) Average trading volume greater than 100K shares over prior 20 days 

Ordering Logic - All stocks in the system are equally weighted. When a stock no longer qualifies as a persistent accruals growth stock, it is sold from the portfolio. Rebalancing is done every day.

Leverage - This algorithm attempts to ensure 100% of cash is utilized without borrowing funds.

Charts - The algorithm is set to display the quantity of stocks within the portfolio. Leverage is also plotted.


                                        

"""



from quantopian.pipeline.data.quandl import rateinf_inflation_usa
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing 
from quantopian.pipeline import CustomFactor 
from quantopian.pipeline.data import morningstar 
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import AverageDollarVolume
import numpy as np

class Earnings_Quality_150(CustomFactor):
    inputs = [morningstar.cash_flow_statement.end_cash_position, morningstar.cash_flow_statement.beginning_cash_position, morningstar.income_statement.net_income, morningstar.cash_flow_statement.cash_dividends_paid, morningstar.cash_flow_statement.common_stock_issuance, morningstar.cash_flow_statement.repurchase_of_capital_stock]
    window_length = 150
    def compute(self, today, assets, out, cash_end, cash_begin, net_income, dividend, stock_issuance, repurchase):
        out[:] = (net_income[0] - (cash_begin[0] - cash_end[0]) + dividend[0] + repurchase[0] + stock_issuance[0]) + (net_income[-120] - (cash_begin[-120] - cash_end[-120]) + dividend[-120] + repurchase[-120] + stock_issuance[-120]) + (net_income[-90] - (cash_begin[-90] - cash_end[-90]) + dividend[-90] + repurchase[-90] + stock_issuance[-90]) + (net_income[-30] - (cash_begin[-30] - cash_end[-30]) + dividend[-30] + repurchase[-30] + stock_issuance[-30]) + (net_income[-1] - (cash_begin[-1] - cash_end[-1]) + dividend[-1] + repurchase[-1] + stock_issuance[-1])

class MarketCap(CustomFactor):
    inputs = [USEquityPricing.close, morningstar.valuation.shares_outstanding]
    window_length = 1
    def compute(self, today, assets, out, close, shares):
        out[:] = close[-1] * shares[-1]


def initialize(context):
    attach_pipeline(my_pipeline(context), 'my_pipeline')
    #set_slippage(slippage.FixedSlippage(spread=0.00))
    #set_commission(commission.PerShare(cost=0.0, min_trade_cost=0.0))
    schedule_function(execute_trade,
                      date_rules.every_day(),
                      time_rules.market_open(hours=0, minutes=1))
def my_pipeline(context):
    pipe = Pipeline()
    dollar_volume = AverageDollarVolume(window_length=20)
    minimum_volume = dollar_volume > 100000
    mkt_cap = MarketCap()
    mkt_cap_top_500 = mkt_cap.top(500)
    Earning = Earnings_Quality_150()
    Quality = Earning > 0
    pipe.set_screen(minimum_volume & mkt_cap_top_500 & Quality)
    Earning_Quality_150 = Earnings_Quality_150()
    pipe.add(Earning_Quality_150, 'Earnings_Quality_150')
    return pipe

def before_trading_start(context, data):
    context.Buy_List = []
    context.output = pipeline_output('my_pipeline')
    for stock in context.output.index:
        context.Buy_List.append(stock)
    
def execute_trade(context, data):
    Buy_List = context.Buy_List
    Weight = 1.0 / len(Buy_List)
    for stock in context.portfolio.positions:
        if stock not in Buy_List:
            order_target_percent(stock, 0)
    for stock in Buy_List:
        if data.can_trade(stock):
            order_target_percent(stock, Weight)
    leverage = context.account.leverage
    record(leverage=leverage)
    Quantity = len(Buy_List)
    record(Quantity=Quantity)
    
            
            
There was a runtime error.