Notebook
In [ ]:
# From: https://www.quantopian.com/posts/long-only-non-day-trading-algorithm-for-live

# PYLIVETRADER
# from quantopian.algorithm import attach_pipeline, pipeline_output

# Quantopian
from zipline.api import attach_pipeline, pipeline_output

from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.factors import SimpleMovingAverage, AverageDollarVolume, RSI, CustomFactor
from quantopian.pipeline.filters.morningstar import IsPrimaryShare
    
from quantopian.pipeline.domain import US_EQUITIES


from quantopian.pipeline import Pipeline  
from quantopian.pipeline.data.builtin import USEquityPricing  
from quantopian.pipeline.factors import AverageDollarVolume  

import pandas as pd  
import numpy as np  # needed for NaN handling
import math  # ceil and floor are useful for rounding

from itertools import cycle
In [ ]:
class ADX(CustomFactor):
    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close]
    params = {'adx_len' : 14}
    window_length = 140            # use window_length = 10 * adx_len
    
    def compute(self, today, assets, out, highs, lows, closes, adx_len):
        THs = np.maximum(highs[1:], closes[:-1])  # max of current high or previous close
        TLs = np.minimum(lows[1:], closes[:-1])   # min of current low or previous close
        TRs = THs - TLs
        
        # This comented out code below is an efficient way to calculate ATR without a for loop
        # However it produces a single ATR value and not the array of ATR values needed later on
        #count = len(TRs)
        #decay_rate = (1.0 - (1.0 / (adx_len)))
        #weights = np.full(count, decay_rate, float) ** np.arange(count + 1, 1, -1)
        #ATR = np.average(TRs, axis=0, weights=weights)
        
        ATRs = np.empty(TRs.shape)
        ATRs.fill(np.nan)
        
        for j in range(0, TRs.shape[1]):
            for i in range(adx_len,  TRs.shape[0]):
                if i == adx_len:
                    ATRs[i,j] = np.average(TRs[1:adx_len,j],axis=0)
                else:
                    ATRs[i,j] = (ATRs[i-1,j]*(adx_len-1)+TRs[i,j])/adx_len
        
        high_diffs = highs[1:]-highs[:-1]  # current high - previous high
        low_diffs = lows[:-1]-lows[1:]     # previous low  - current low
 
        pDIs = np.where(((high_diffs > low_diffs) & (high_diffs > 0)), high_diffs, 0.)
        nDIs = np.where(((low_diffs > high_diffs) & (low_diffs > 0)), low_diffs, 0.)
        
        # This comented out code below is an efficient way to calculate ApDI and AnDI without a for loop
        # However it produces single values and not the array of values needed later on
        #ApDI = np.average(pDIs, axis=0, weights=weights)
        #AnDI = np.average(nDIs, axis=0, weights=weights)
        
        ApDIs = np.empty(pDIs.shape)
        ApDIs.fill(np.nan)
        
        for j in range(0, pDIs.shape[1]):
            for i in range(adx_len, pDIs.shape[0]):
                if i == adx_len:
                    ApDIs[i,j] = np.average(pDIs[1:adx_len,j],axis=0)
                else:
                    ApDIs[i,j] = (ApDIs[i-1,j]*(adx_len-1)+pDIs[i,j])/adx_len
                
        AnDIs = np.empty(nDIs.shape)
        AnDIs.fill(np.nan)
        
        for j in range(0, nDIs.shape[1]):
            for i in range(adx_len, nDIs.shape[0]):
                if i == adx_len:
                    AnDIs[i,j] = np.average(nDIs[1:adx_len,j],axis=0)
                else:
                    AnDIs[i,j] = (AnDIs[i-1,j]*(adx_len-1)+nDIs[i,j])/adx_len
        
        pDMs = 100 * (ApDIs / ATRs)
        nDMs = 100 * (AnDIs / ATRs)
        
 
        DXs = 100 * np.abs(pDMs - nDMs) / (pDMs + nDMs)
                
        # This commented out code below seems like it should have worked
        # However it just produce NaN values. 
        #count = len(DXs)
        #decay_rate = (1.0 - (1.0 / (adx_len)))
        #weights = np.full(count, decay_rate, float) ** np.arange(count + 1, 1, -1)
        #ADX = np.average(DXs, axis=0, weights=weights)
        #out[:] = ADX
    
        ADXs = np.empty(DXs.shape)
        ADXs.fill(np.nan)
        
        for j in range(0, DXs.shape[1]):
            for i in range(2*adx_len, DXs.shape[0]):
                if i == 2*adx_len:
                    ADXs[i,j] = np.average(DXs[adx_len+1:2*adx_len,j],axis=0)
                else:
                    ADXs[i,j] = (ADXs[i-1,j]*(adx_len-1)+DXs[i,j])/adx_len
                
        out[:] = ADXs[-1:]
In [ ]:
MaxCandidates = 15
MaxBuyOrdersAtOnce = 5
MyLeastPrice = 3.0
MyFireSalePrice = MyLeastPrice
MyFireSaleAge = 4
In [67]:
def make_pipeline():
    """
    Create our pipeline.
    """

 
    # Filter for primary share equities. IsPrimaryShare is a built-in filter.
    primary_share = IsPrimaryShare()
 
    # Not when-issued equities.
    not_wi = ~morningstar.share_class_reference.symbol.latest.endswith('.WI')
 
    # Equities without LP in their name, .matches does a match using a regular
    # expression
    not_lp_name = ~morningstar.company_reference.standard_name.latest.matches(
        '.* L[. ]?P.?$')
 
    # Equities whose most recent Morningstar market cap is not null have
    # fundamental data and therefore are not ETFs.
    #have_market_cap = morningstar.valuation.market_cap.latest.notnull()
    have_market_cap = morningstar.valuation.market_cap.latest >= 1
 
    # At least a certain price
    price = USEquityPricing.close.latest
    AtLeastPrice = (price >= MyLeastPrice)
    
    filter_volume = SimpleMovingAverage(inputs=[USEquityPricing.volume],
        window_length=30) > 500000
    filter_dollar_volume = AverageDollarVolume(window_length=30) > 2500000
    
    rsi = RSI(inputs=[USEquityPricing.close], window_length=3)
    filter_overbought = rsi < 30
    
    
    # Filter for stocks that pass all of our previous filters.
    tradeable_stocks = (
        primary_share
        & not_wi
        & not_lp_name
        & have_market_cap
        & AtLeastPrice
        & filter_volume
        & filter_dollar_volume
        & filter_overbought
    )
 
    adx = ADX(mask=tradeable_stocks,window_length=70,adx_len=7)

    tradable_with_adx_stocks = adx > 20
    
    # Short close price average.
    ShortAvg = SimpleMovingAverage(
        inputs=[USEquityPricing.close],
        window_length=3,
        mask=tradable_with_adx_stocks
    )
 
    # Long close price average.
    LongAvg = SimpleMovingAverage(
        inputs=[USEquityPricing.close],
        window_length=45,
        mask=tradable_with_adx_stocks
    )
 
    percent_difference = (ShortAvg - LongAvg) / LongAvg
     
    # Filter to select securities to long.
    stocks_worst = percent_difference.bottom(MaxCandidates)
    # securities_to_trade = (stocks_worst)
 
    return Pipeline(
        columns={
            'percent_difference': -percent_difference,
        },
        screen=tradable_with_adx_stocks,
    )
In [68]:
pipeline = make_pipeline()
In [69]:
# Run the pipeline from 2016 to 2019 and display the first few rows of output.
from quantopian.research import run_pipeline
factor_data = run_pipeline(pipeline, '2016-05-01', '2020-05-08')
print("Result contains {} rows of output.".format(len(factor_data)))
factor_data.head()

Pipeline Execution Time: 3 Minutes, 29.50 Seconds
Result contains 399334 rows of output.
Out[69]:
percent_difference
2016-05-02 00:00:00+00:00 Equity(2 [HWM]) -0.142073
Equity(24 [AAPL]) 0.091581
Equity(62 [ABT]) 0.004561
Equity(67 [ADSK]) -0.059296
Equity(114 [ADBE]) -0.029733
In [70]:
pricing_data = get_pricing(
  symbols=factor_data.index.levels[1], # Finds all assets that appear at least once in "factor_data"  
  start_date='2016-05-01',
  end_date='2020-06-08', # must be after run_pipeline()'s end date. Explained more in lesson 4
  fields='open_price' # Generally, you should use open pricing. Explained more in lesson 4
)
In [71]:
# Show the first 5 rows of pricing_data
pricing_data.head()
Out[71]:
Equity(2 [HWM]) Equity(24 [AAPL]) Equity(52 [ABM]) Equity(53 [ABMD]) Equity(62 [ABT]) Equity(67 [ADSK]) Equity(69 [ACAT]) Equity(76 [TAP]) Equity(84 [ACET]) Equity(110 [RAMP]) ... Equity(53535 [PTON]) Equity(53578 [BNTX]) Equity(53586 [VIR]) Equity(53625 [PGNY]) Equity(53703 [BXRX]) Equity(53710 [BRMK]) Equity(53724 [CAN]) Equity(53749 [GRWG]) Equity(53770 [XP]) Equity(53775 [BILL])
2016-05-02 00:00:00+00:00 32.340 87.838 29.783 97.61 35.918 60.00 16.62 86.213 21.543 22.00 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2016-05-03 00:00:00+00:00 30.839 88.057 29.884 101.06 35.872 60.87 16.70 87.305 21.572 21.70 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2016-05-04 00:00:00+00:00 29.770 88.992 29.783 98.92 35.456 59.93 15.72 88.207 21.266 22.56 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2016-05-05 00:00:00+00:00 29.886 88.405 30.300 94.88 35.391 59.07 15.40 88.766 21.333 22.35 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2016-05-06 00:00:00+00:00 28.930 87.813 30.106 93.82 35.336 58.44 14.75 88.784 21.295 22.33 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

5 rows × 2884 columns

In [72]:
factor_data.head()
Out[72]:
percent_difference
2016-05-02 00:00:00+00:00 Equity(2 [HWM]) -0.142073
Equity(24 [AAPL]) 0.091581
Equity(62 [ABT]) 0.004561
Equity(67 [ADSK]) -0.059296
Equity(114 [ADBE]) -0.029733
In [73]:
from alphalens.utils import get_clean_factor_and_forward_returns

merged_data = get_clean_factor_and_forward_returns(
  factor=factor_data, 
  prices=pricing_data
)

# Show the first 5 rows of merged_data
merged_data.head(5)
Dropped 0.3% entries from factor data: 0.3% in forward returns computation and 0.0% in binning phase (set max_loss=0 to see potentially suppressed Exceptions).
max_loss is 35.0%, not exceeded: OK!
Out[73]:
1D 5D 10D factor factor_quantile
date asset
2016-05-02 00:00:00+00:00 Equity(2 [HWM]) -0.046413 -0.117069 -0.167223 -0.142073 1
Equity(24 [AAPL]) 0.002493 -0.004246 -0.010781 0.091581 5
Equity(62 [ABT]) -0.001281 -0.019043 -0.032157 0.004561 4
Equity(67 [ADSK]) 0.014500 -0.022167 -0.034333 -0.059296 2
Equity(114 [ADBE]) -0.002333 -0.003076 0.017181 -0.029733 3
In [74]:
from alphalens.tears import create_full_tear_sheet

create_full_tear_sheet(merged_data)
Quantiles Statistics
min max mean std count count %
factor_quantile
1 -8.103838 0.288928 -0.128419 0.166522 80067 20.102840
2 -0.255859 0.378703 -0.024493 0.053940 79460 19.950438
3 -0.157109 0.447230 0.011667 0.059124 79460 19.950438
4 -0.098131 0.537497 0.051299 0.067767 79460 19.950438
5 -0.053009 0.910343 0.145576 0.108530 79840 20.045846
Returns Analysis
1D 5D 10D
Ann. alpha 0.188 0.136 0.069
beta 0.286 0.337 0.226
Mean Period Wise Return Top Quantile (bps) 3.735 1.857 -0.292
Mean Period Wise Return Bottom Quantile (bps) -1.992 -2.695 -1.202
Mean Period Wise Spread (bps) 5.727 4.410 0.858
<matplotlib.figure.Figure at 0x7f9af8364358>
Information Analysis
1D 5D 10D
IC Mean 0.008 0.015 0.013
IC Std. 0.155 0.162 0.152
Risk-Adjusted IC 0.050 0.093 0.084
t-stat(IC) 1.576 2.964 2.688
p-value(IC) 0.115 0.003 0.007
IC Skew 0.160 0.564 0.250
IC Kurtosis 1.116 1.607 0.813
Turnover Analysis
1D 5D 10D
Quantile 1 Mean Turnover 0.576 0.816 0.855
Quantile 2 Mean Turnover 0.669 0.891 0.914
Quantile 3 Mean Turnover 0.672 0.891 0.914
Quantile 4 Mean Turnover 0.623 0.853 0.887
Quantile 5 Mean Turnover 0.475 0.712 0.768
1D 5D 10D
Mean Factor Rank Autocorrelation 0.98 0.84 0.685
In [ ]:
 
In [ ]: