Back to Community
Error in fundamental data?

I tried to get the EPS data a year ago and the current EPS data, then calculate the percent increase. However, the percent increase seems to be wrong for the first couple of symbols.

The result of the run is as follows:

                     eps  eps_change  
Equity(40531 [FXCM])  0.0500  277.200000  
Equity(4415 [LEG])    0.0034  197.058824  
Equity(12200 [LGND])  0.0600  174.333333  
Equity(23096 [UNTD])  0.0100  169.000000  
Equity(3766 [IBM])    0.0200  150.500000

For symbol FXCM, the increase is 277.2X. But from NYSE, it seems the value should really be 27.72X:
https://www.nyse.com/quote/XNYS:FXCM

A bit further digging of the data seems to show the EPS a year ago is wrong. It shows up as 0.05 from the algorithm, but from NYSE, it is actually 0.50.

For symbol LEG, the increase is 197X, but from NYSE, it seems the increase should be 1.97X:
https://www.nyse.com/quote/XNYS:LEG
Seems to be the same issue, the EPS a year ago shows up as 0.0034, but from NYSE, it should be 0.34.

I don't know whether other values are correct or not.

I'm not sure whether the incorrect data is from morning star or not. NYSE also use morning star data. Or is it just my algorithm gets the wrong data point?

Thanks.

Clone Algorithm
1
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
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline import CustomFactor
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar
import numpy as np

class EPSQoQ(CustomFactor) :
    inputs = [morningstar.earnings_report.diluted_eps]
    window_length = 270
    
    # Compute EPS change since end of last quarter
    def compute(self, today, assets, out, eps):
        out_list = []
        
        # For each security in the universe
        for i in range(eps.shape[1]):
            
            # Store the column of each day's eps for the security in eps_col
            eps_col = eps[:,i]
            #file_date_col = file_date[:,i]
            
            # Since the basic_eps only changes at the end of each quarter, np.unique
            # will get us the unique EPS values in the 150-day window. return_index
            # is set to True so that the most recent unique values can be referenced later.
            _, idx = np.unique(eps_col, return_index=True)
            
            # Get the 2 most recent unique values in the 150-day value.
            prev_and_curr_eps = eps_col[np.sort(idx)[-5:]]
            
            length = len(prev_and_curr_eps)
            
            # Store the 2 most recent unique EPS values for the stock in out_list
            eps_one_year_ago = prev_and_curr_eps[-length]
            eps_current = prev_and_curr_eps[-1]
            if (eps_one_year_ago <= 0) or (eps_current <= 0) :
                eps_one_year_ago = -1000
                eps_current = 1
                
            eps_list = []
            eps_list.append(eps_one_year_ago)
            eps_list.append(eps_current)
            out_list.append(eps_list)
            
        # Convert out_list to a 2*num_stocks numpy array
        all_prev_and_curr_eps = np.transpose(np.array(out_list))
         
        # Return the most recent EPS minus the previous EPS
        out[:] = all_prev_and_curr_eps[-1] / all_prev_and_curr_eps[-2]

class EPS(CustomFactor) :
    inputs = [morningstar.earnings_report.diluted_eps]
    window_length = 270
    
    # Compute EPS change since end of last quarter
    def compute(self, today, assets, out, eps):
        out_list = []
        
        # For each security in the universe
        for i in range(eps.shape[1]):
            
            # Store the column of each day's eps for the security in eps_col
            eps_col = eps[:,i]
            #file_date_col = file_date[:,i]
            
            # Since the basic_eps only changes at the end of each quarter, np.unique
            # will get us the unique EPS values in the 150-day window. return_index
            # is set to True so that the most recent unique values can be referenced later.
            _, idx = np.unique(eps_col, return_index=True)
            
            # Get the 2 most recent unique values in the 150-day value.
            prev_and_curr_eps = eps_col[np.sort(idx)[-5:]]
            
            length = len(prev_and_curr_eps)
            
            # Store the 2 most recent unique EPS values for the stock in out_list
            eps_one_year_ago = prev_and_curr_eps[-length]
            eps_current = prev_and_curr_eps[-1]
            if (eps_one_year_ago <= 0) or (eps_current <= 0) :
                eps_one_year_ago = -1000
                eps_current = 1
                
            eps_list = []
            eps_list.append(eps_one_year_ago)
            eps_list.append(eps_current)
            out_list.append(eps_list)            
        # Convert out_list to a 2*num_stocks numpy array
        all_prev_and_curr_eps = np.transpose(np.array(out_list))
         
        # Return the most recent EPS minus the previous EPS
        out[:] = all_prev_and_curr_eps[-2]   
        
def initialize(context):
    # Create the pipe
    pipe = Pipeline()
    attach_pipeline(pipe, 'eps-change-example')

    # Construct the custom factor
    eps_change = EPSQoQ()
    eps = EPS()
    # Add the EPS change factor to the pipe
    pipe.add(eps_change, 'eps_change')
    pipe.add(eps, 'eps')
    
    # Create and apply a filter representing all stocks where the current EPS is
    # greater than the previous one
    eps_better_top_100 = eps_change.top(10)
    pipe.set_screen(eps_better_top_100)
    context.stocks = [symbol('FXCM'), symbol('LEG')]

def before_trading_start(context, data):
    context.output = pipeline_output('eps-change-example').sort(['eps_change'], ascending=False)
    update_universe(context.output.index)    
    
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    print "SECURITY LIST"
    log.info("\n" + str(context.output))
There was a runtime error.
8 responses

it seems like the values are not split adjusted

Thanks. It seems that FXCM does have a 1:10 reverse split. However, I cannot find any splits for LEG. What is going on with this stock?

A second question. If quantopian doesn't adjust EPS for splits, how to adjust it in the algorithm? Where to get the split data? Thanks.

Hi Fei,

You are correct. We do not yet cover this use case well with our fundamental data. This is something that is in our backlog since it is much more likely that you will run into this problem when using the Pipeline API as compared to previously with get_fundamentals. One way around it would be to avoid EPS and other per share metrics over timeframe and use the raw earnings metrics from Morningstar.

Sorry for the inconvenience.

Thanks
Josh

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.

Also the ROA and the Asset Turnover seem to be wrong in the Pipeline API.

For example I get this values for AAPL on 2016-03-01
AssetsTurnover Latest: 0.25994
ROALatest: 0.06291

but according to the Morningstar web site (http://financials.morningstar.com/ratios/r.html?t=AAPL&region=USA&culture=en_US), the correct values are:
Asset Turnover (Average) 0.85
Return on Assets % 19.36

I implemented the factors as follows:

latest = -1

class ROALatest(CustomFactor):  
    window_length = 1  
    inputs = [morningstar.operation_ratios.roa]  
    def compute(self, today, assets, out, roa):  
        out[:] = roa[latest]

class AssetsTurnoverLatest(CustomFactor):  
    window_length = 1  
    inputs = [morningstar.operation_ratios.assets_turnover]  
    def compute(self, today, assets, out, assets_turnover):  
        out[:] = assets_turnover[latest]  

I did something wrong in the factors or there is a problem with these data?
Thanks

One potential reason for the difference is that Quantopian displays the most recent quarterly value for the metric. I'm not seeing the values you cite on the webpage, but those might be annual values or TTM (as compared to a quarterly value).

Hi Josh,

thanks for the reply. You can find the values on the Morningstar page, if you go to "Key Ratio" and then "Profitability".

You're right, the problem is the timing: ROA and Asset Turnover are computed using the quartely Net Income and Revenues.
To check it, I've used the data from the Morningstar page:
Asset Turnover: 75,872/293,284 = 0.25869
ROA: 18,361/293,284 = 0.06260

I was aware than the most fundamentals are quarterly but tought that the ratios were TTM, as you said in this post:
https://www.quantopian.com/posts/dont-trade-earnings-reports-a-simple-risk-avoidance-trading-strategy#56b75e9b00abaf131d00080c

I think, Quantopian should better document the timeframe of the data... or - much better - offer annual and TTM data! :-)
Actually it's quite confusing and I'm sure, I'm not the first to be a victim of this ambiguity. For example the algo in this post:
https://www.quantopian.com/posts/piotroski-score-plus-aroon-indicator#564f7f7e308ca9d46e0000ab
uses also a quarterly ROA and Assert Turnover to compute the Piotrosky score, while annual ratios would be required.

To overcome this limitation of the platform, I've implemented the workaround described in this post:
https://www.quantopian.com/posts/beneish-score-accounting-manipulation-as-pipeline-factor-memoryerror#56f2bc685fe2be6837000355
but unfortunately loading 255 days of data or more often lead to an out-of-memory error.

Quantopian is great and I hope you and your team will soon address this limitation that makes implementing algoriths based on historical fundamental data quite hard... it's not the first time I "invoked" this feature...sorry to seem insistent, nut in my opinion this a very important and necessary improvement.

I've now completed a Pipeline version of the Piotroski score.
It's the only accurate version on Quantopian I'm aware of. As said in the long post above, the other ones use a wrong timeframe.

I've run the algorithm factor by factor and compared the results of some stocks with that ones provided by Gurufocus.com and they are the same!
But there still a major problem: when you want to compute the whole score, a Memory or Timeout Error occurs

I hope somebody at Quantopian will be help to solve this issue...
The algo is attached... to reproduce the error, just comment/uncomment the follwing lines (starting at #214)

profitability = ROA() + ROAChange() + CashFlow() + EarningsQuality()  
#profitability = ROA() + CashFlow() + EarningsQuality() 

# Efficiency  
efficiency = GrossMarginChange() + AssetsTurnoverChange()  
#efficiency = GrossMarginChange()  

ROAChange() and AssetsTurnoverChange() cause the Memory-Error because - conform to the Piotrosky original paper - they requires two years of data (window_length=456).

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
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import CustomFactor, SimpleMovingAverage
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar
import pandas as pd
import numpy as np

quarter_lenght = 65
latest = -1
one_year_ago = -4*quarter_lenght
ttm    = [               -1,   -quarter_lenght, -2*quarter_lenght, -3*quarter_lenght]
ttm_py = [-4*quarter_lenght, -5*quarter_lenght, -6*quarter_lenght, -7*quarter_lenght]

class ROA(CustomFactor):
    """
    Profitability - Question 1. Return on Assets (ROA)
    ROA = Net Income / Total Assets
    Score 1 if the most recent ROA positive, 0 if negative.
    """
    window_length = 3*quarter_lenght + 1
    inputs = [morningstar.income_statement.net_income]
    
    def compute(self, today, assets, out, net_income):
        net_income_ttm = np.sum(net_income[ttm], axis=0)        
        out[:] = (net_income_ttm > 0).astype(int)
       
class ROAChange(CustomFactor):
    """
    Profitability - Question 3. Change in Return on Assets
    Score 1 if this year’s ROA is higher than last year’s ROA, 0 if it’s lower.
    """
    window_length = 7*quarter_lenght + 1
    inputs = [morningstar.income_statement.net_income,
              morningstar.balance_sheet.total_assets]
    
    def compute(self, today, assets, out, net_income, total_assets):
        net_income_ttm = np.sum(net_income[ttm], axis=0)
        net_income_ttm_py = np.sum(net_income[ttm_py], axis=0)
        
        roa = net_income_ttm / total_assets[latest]
        roa_py = net_income_ttm_py / total_assets[one_year_ago]
        out[:] = (roa > roa_py).astype(int)

class CashFlow(CustomFactor):
    """
    Profitability - Question 2. Cash Flow Return on Assets (CFROA)
    Score 1 if the most recent Operating Cash Flow positive, otherwise 0.
    """
    window_length = 3*quarter_lenght + 1
    inputs = [morningstar.cash_flow_statement.operating_cash_flow]
    
    def compute(self, today, assets, out, cash_flow):
        cash_flow_ttm = np.sum(cash_flow[ttm], axis=0)  
        out[:] = (cash_flow_ttm > 0).astype(int)
 
class EarningsQuality(CustomFactor):
    """
    Profitability - Question 4. Quality of Earnings (Accrual)
    Score 1 if Operating Cash Flow (TTM) > Net Income (TTM), otherwise 0.
    """
    window_length = 3*quarter_lenght + 1        
    inputs = [morningstar.cash_flow_statement.operating_cash_flow,
             morningstar.income_statement.net_income]
    
    def compute(self, today, assets, out, operating_cash_flow, net_income):
        operating_cash_flow_ttm = np.sum(operating_cash_flow[ttm], axis=0)
        net_income_ttm = np.sum(net_income[ttm], axis=0)
        out[:] = (operating_cash_flow_ttm > net_income_ttm).astype(int)
        
class LongTermDebtRatioChange(CustomFactor):
    """
    Leverage - Question 5. Change in Gearing or Leverage
    Score 1 if this year's this year’s gearing (long-term debt to total assets) is lower or equal to last year’s gearing, 1 otherwise.
    """
    window_length = 4*quarter_lenght + 1
    inputs = [morningstar.operation_ratios.long_term_debt_equity_ratio,
              morningstar.operation_ratios.financial_leverage]
    
    def compute(self, today, assets, out, long_term_debt_equity_ratio, financial_leverage):
        long_term_debt_assets_ratio = long_term_debt_equity_ratio / financial_leverage
        out[:] = (long_term_debt_assets_ratio[latest] <= long_term_debt_assets_ratio[one_year_ago]).astype(int)
        
class CurrentDebtRatioChange(CustomFactor):
    """
    Leverage - Question 6. Change in Working Capital (Liquidity)
    Score 1 if this year’s current ratio is higher than last year’s, 0 if it’s lower
    """
    window_length = 4*quarter_lenght + 1
    inputs = [morningstar.operation_ratios.current_ratio]
    
    def compute(self, today, assets, out, current_ratio):
        out[:] = (current_ratio[latest] > current_ratio[one_year_ago]).astype(int)

class SharesOutstandingChange(CustomFactor):
    """
    Leverage - Question 7. Change in Shares in Issue
    Score 1 if the net number of shares in issue this year is lower or equal to last year
    """
    window_length = 3*quarter_lenght + 1
    inputs = [morningstar.cash_flow_statement.net_common_stock_issuance]
    
    def compute(self, today, assets, out, issuance):
        issuance_ttm = np.sum(issuance[ttm], axis=0)
        out[:] = (issuance_ttm <= 0).astype(int)
        
class GrossMarginChange(CustomFactor):
    """
    Efficiency - Question 8. Change in Gross Margin
    Score 1 if this year’s gross margin is higher than last year, 0 if it’s lower.
    """    
    window_length = 4*quarter_lenght + 1
    inputs = [morningstar.operation_ratios.gross_margin]
    
    def compute(self, today, assets, out, gross_margin):
        out[:] = (gross_margin[latest] > gross_margin[one_year_ago]).astype(int)
        
class AssetsTurnoverChange(CustomFactor):
    """
    Question 9. Change in asset turnover
    Assets Turnover = Revenue / Total Assets
    Score 1 if this year’s asset turnover ratio is higher compared to last year’.
    """
    window_length = 7*quarter_lenght + 1
    inputs = [morningstar.income_statement.total_revenue,
              morningstar.balance_sheet.total_assets]
    
    def compute(self, today, assets, out, revenue, total_assets):
        revenue_ttm = np.sum(revenue[ttm], axis=0)
        revenue_ttm_py = np.sum(revenue[ttm_py], axis=0)
        
        assets_turnover  = revenue_ttm / total_assets[latest]
        assets_turnover_py = revenue_ttm_py / total_assets[one_year_ago]
        out[:] = (assets_turnover > assets_turnover_py).astype(int)
        
        
class UniverseFilter(CustomFactor):
    """
    Return 1.0 for the following class of assets, otherwise 0.0:
      * No Financials (103), Real Estate (104), Basic Materials (101) and ADR
        (Basic Materials are too much sensitive to exogenous macroeconomical shocks.)
      * Only primary common stocks
      * Exclude When Distributed(WD), When Issued(WI) and VJ - usuallly companies in bankruptcy
      * Exclude Halted stocks (_V, _H)
      * Only NYSE, AMEX and Nasdaq
      * mkt cap > 5,000,000
      * invested_capital > 0 (sanity check)
      * total_assets > 0 (sanity check)
      * Avoid illiquid stock (dollar trading volume average in the last 10 days less than 100,000)
    """
    window_length = 10
    inputs = [USEquityPricing.close, USEquityPricing.volume,
              morningstar.valuation.market_cap,
              morningstar.share_class_reference.is_primary_share, 
              morningstar.share_class_reference.is_depositary_receipt, 
              morningstar.asset_classification.morningstar_sector_code,
              morningstar.balance_sheet.invested_capital,
              morningstar.balance_sheet.total_assets
              ]      
    
    def compute(self, today, assets, out, close_price, volume, mkt_cap, is_primary_share, \
                is_depositary_receipt, sector_code, invested_capital, total_assets):
        dollar_volume_10d_avg = np.mean(close_price * volume, axis=0)  
        criteria = dollar_volume_10d_avg > 1e5 # Avoid illiquid stock (dollar trading volume average in the last 10 days less than 100,000)
        criteria = criteria & (mkt_cap[-1] > 5e6) # Mkt cap > 5,000,000        
        criteria = criteria & (is_primary_share[-1]) # Only primary Common Stock
        criteria = criteria & (~is_depositary_receipt[-1]) # No ADR
        criteria = criteria & (sector_code[-1] != 101) # No Basic Materials
        criteria = criteria & (sector_code[-1] != 103) # No Financials
        criteria = criteria & (sector_code[-1] != 104) # No Real Estate
        criteria = criteria & (invested_capital[-1] > 0) # Sanity check
        criteria = criteria & (total_assets[-1] > 0) # Sanity check
        
        def accept_symbol(equity):
            symbol = equity.symbol
            if symbol.endswith("_PR") or symbol.endswith("_WI") or symbol.endswith("_WD") or \
               symbol.endswith("_VJ") or symbol.endswith("_V") or symbol.endswith("_H"):
                return False
            else:
                return True
                
        def accept_exchange(equity):
            exchange = equity.exchange
            if exchange == "NEW YORK STOCK EXCHANGE":
                return True
            elif exchange == "AMERICAN STOCK EXCHANGE":
                return True
            elif exchange.startswith("NASDAQ"):
                return True
            else:
                return False
            
        vsid = np.vectorize(sid)
        equities = vsid(assets)
        
        # Exclude When Distributed(WD), When Issued(WI) and VJ (usuallly companies in bankruptcy) and Halted stocks (V, H)
        vaccept_symbol = np.vectorize(accept_symbol)
        accept_symbol = vaccept_symbol(equities)
        criteria = criteria & (accept_symbol)
        
        # Only NYSE, AMEX and Nasdaq
        vaccept_exchange = np.vectorize(accept_exchange)
        accept_exchange = vaccept_exchange(equities)
        criteria = criteria & (accept_exchange)
                
        out[:] = criteria.astype(float)   
        
def initialize(context):
    context.count = 0
    pipe = Pipeline()
    attach_pipeline(pipe, 'my_pipeline')
        
    # Profitability   
    #profitability = ROA() + ROAChange() + CashFlow() + EarningsQuality()
    profitability = ROA() + CashFlow() + EarningsQuality() 
           
    # Funding
    funding = LongTermDebtRatioChange() + CurrentDebtRatioChange() + SharesOutstandingChange()  
    
    # Efficiency
    #efficiency = GrossMarginChange() + AssetsTurnoverChange()
    efficiency = GrossMarginChange()
        
    piotroski = profitability + funding + efficiency
    pipe.add(piotroski, 'piotroski')
    
    #Filters
    universe_filter = UniverseFilter()
    sma_200 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=200)
    
    # Screen out criteria
    pipe.set_screen((universe_filter >= 1.0) & (sma_200 > 5) & (piotroski.isfinite()))
        
def before_trading_start(context, data):
    output = pipeline_output('my_pipeline')
    #context.my_universe = output.sort('piotroski', ascending=False).iloc[:500]
    context.my_universe = output.iloc[:500]
    update_universe(context.my_universe.index)


def handle_data(context, data):
    hist = context.my_universe
    ncols = hist.shape[1]
    step = 13
    start = context.count * step
    end = start + step
    sample = hist.iloc[start:end]
    log.info("%d:%d" % (start, end))
    #log.info(hist.iloc[0:1].to_string(formatters=['{:,.0f}'.format] * ncols))
    log.info(sample.to_string(formatters=['{:,.5f}'.format] * ncols))
    context.count = context.count + 1
There was a runtime error.