Back to Community
Picking Liquid Stocks With Over 1000 % Return Since 2005

Introduction

Can one pick stocks and hope to beat the SPY index?

The following strategy and backtest answers this question affirmatively, at least for the period from 2005 until 2019.

Warning: 2005 until 2019 was a bull market. As we see later, this is a precondition for the strategy to work
Note: The title refer's to the backtest/strategy presented here picking stocks rather than anyone trading this strategy since 2005 and getting this return.

Strategy

  1. Filter top k stocks by AverageDollarVolume.
  2. Additionally compute (mean stock return - mean index return)/(eps + mean stock return std)
  3. If score from point 2 is higher than a threshold hold the stock.
  4. Every day make adjustments to the portfolio.

Caveats

  • When in bear market, the score from 2 would lead to false positives which will lead to losses.
  • May buy/sell too frequently which will lead to high costs. Currently instead of entry/exit conditions, a hold condition is used. Using different entry/exit conditions may be beneficial.
  • There are periods (days) when we are not holding anything. Holding SPY index in those period might be beneficial.
  • Instead of allocating the portfolio equally among stocks one can try to use weights related to past stock performance.
  • order_percent_change is used to buy stocks, which is not optimal. This is because it may lead to minor corrections (e.g. buying/selling 1-2 shares only) which are inconsequential but still incur costs.

Previous work

The strategy improves upon a previous post in two ways:
- additional selection criterion based on comparison of long term returns with SPY returns make sure we won't select complete lemons. For example AverageDollarVolume could be associated with higher selling than buying
- we use more than one stock, which provides extra protection against individual stocks
- the strategy tested uses less than 5 stocks because the criterion with high AverageDollarVolume and out performing SPY index is very restrictive.

Disclaimer

This presentation is for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation for any security; nor does it constitute an offer to provide investment advisory or other services by the author or anyone else. Nothing contained herein constitutes investment advice or offers any opinion with respect to the suitability of any security, and any views expressed herein should not be taken as advice to buy, sell, or hold any security or as an endorsement of any security or company. This disclaimer was adapted from Quantopian's own disclaimer.

Clone Algorithm
29
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
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.factors import AverageDollarVolume, SimpleMovingAverage
from quantopian.pipeline.factors import CustomFactor
from quantopian.pipeline.data import USEquityPricing
from quantopian.pipeline.filters import QTradableStocksUS, StaticAssets

import numpy as np
import pandas as pd

from quantopian.pipeline import Pipeline

from quantopian.pipeline.filters import Q500US

from quantopian.pipeline.data import Fundamentals
from quantopian.pipeline.data.builtin import USEquityPricing

def initialize(context):
    algo.schedule_function(
        rebalance,
        algo.date_rules.every_day(),
        algo.time_rules.market_open(hours=1),
    )

    algo.schedule_function(
        record_vars,
        algo.date_rules.every_day(),
        algo.time_rules.market_close(),
    )

    algo.attach_pipeline(make_pipeline(), 'pipeline')


def f(data, asset, base_asset, formation_period=270, threshold=0.1):
    def s(inp):
        return pd.Series(inp, index=data.index)
    h = int(formation_period/2)
    prices_asset = (data[asset + ':Return']).rolling(formation_period, min_periods=h).mean()
    prices_base = (data[base_asset + ':Return']).rolling(formation_period, min_periods=h).mean()
    std = prices_asset.rolling(formation_period, min_periods=h).std()
    w = (prices_asset - prices_base)/(0.01 + std)

    w = s(np.where(w > threshold, w, 0.0))
    
    ret = (data[asset + ':Return'])
    
    return w

def prepare(g_prices, g_assets):
    num_days = g_prices.shape[0]
    num_assets = len(list(g_assets))
    t_prices = g_prices.T
    idx = range(0, num_days)
    g_asset_lst = list(g_assets.values)
    out = []
    for i in range(0, len(g_asset_lst)):
        name = str(i)
        fr = pd.Series(t_prices[i], index=idx, name= name + ":Prices").to_frame()
        fr[name + ':LogEndPrice'] = np.log(fr[name + ":Prices"].ffill())
        fr[name + ':Return'] = (fr[name +  ':LogEndPrice'] - fr[name +  ':LogEndPrice'].shift(1)).fillna(0.0)

        out.append(fr[[name + ':Return']])
    
    data = pd.concat(out, axis=1)

    spx_name, _ = filter(lambda (i, x): x == 8554, enumerate(list(g_assets.values)))[0]
    spx_name=str(spx_name)
    out = []
    g_asset_lst = list(g_assets.values)
    def s(inp):
        return pd.Series(inp, index=data.index)
    for i in range(0, len(g_asset_lst)):
        name = str(i)
        w = f(data, name, spx_name, formation_period=270, threshold=0.1)
        out.append(w.rename(name + ":W"))
    ws = pd.concat(out, axis=1)
    return ws.tail(1).values

class KeyllyFactor(CustomFactor):  
    inputs = [USEquityPricing.close]  
    window_length = 270
    def compute(self, today, assets, out, prices):
        out[:] = prepare(prices, assets)
        
        
def make_pipeline():
    q500us = Q500US()
    dollar_volume = AverageDollarVolume(mask=q500us, window_length=270)
   
    my_etfs = StaticAssets([sid(8554)]) # SPY

    screen = (dollar_volume.top(5) & q500us) | my_etfs
    
    kf = KeyllyFactor()
    kf.mask = screen

    pipe = Pipeline(
        columns = {'kf': kf})
    pipe.set_screen(screen)
    return pipe


def before_trading_start(context, data):
    """
    Called every day before market open.
    """
    context.output = algo.pipeline_output('pipeline')['kf']
    context.security_list = context.output.index

def rebalance(context, data):
    selected_stocks = []
    for stock in context.output.index:  
        v = context.output.at[stock] 
        if v > 0:
            selected_stocks.append(stock)
    for stock in list(context.portfolio.positions.iterkeys()):
        if stock not in selected_stocks:
            print("closing position for " + str(stock))
            order_target_percent(stock, 0)
    
    old_stocks = list(context.portfolio.positions.iterkeys())
    if set(old_stocks) != set(selected_stocks):
        for stock in selected_stocks:
            v = 1.0/len(selected_stocks)
            print("open position for " + str(stock) + " " + str(v))
            order_target_percent(stock, v)

def record_vars(context, data):
    """
    Plot variables at the end of each day.
    """
    record(leverage=context.account.leverage)



def handle_data(context, data):
    """
    Called every minute.
    """
    pass
There was a runtime error.