Back to Community
[deleted]
12 responses

I added a pipeline and a few filters. It was running really slowly so I only backtested it for a month. There were lots of unfilled orders but it seemed to be generating a profit. Looking back at the results, it seems like the pipeline was mostly returning small caps and even a few mid caps instead of just penny stocks. This wouldn't work very well with commissions.

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
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian import pipeline as p
from quantopian.pipeline.data import morningstar
from quantopian.pipeline import CustomFactor
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters.morningstar import Q1500US

class Previous(CustomFactor):  
    # Returns value of input x trading days ago where x is the window_length  
    # Both the inputs and window_length must be specified as there are no defaults

        def compute(self, today, assets, out, inputs):  
            out[:] = inputs[0]

def initialize(context):
    # Start buying 60 minutes after open
    schedule_function(my_open, date_rules.every_day(), time_rules.market_open(minutes=60))
    # Stop buying 120 minutes before close
    schedule_function(my_close, date_rules.every_day(), time_rules.market_close(minutes=120))
    # Flag tells us whether we're allowed to buy or not.
    context.trading_allowed=False
     
    # Record tracking variables at the end of each day.
    schedule_function(my_record_vars, date_rules.every_day(), time_rules.market_close())

    ##context.penny_stocks = [sid(34536), #NEPT Neptune Omega-3]

    ##set_benchmark(context.penny_stocks[0])
    
    # Robinhood!
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    
    attach_pipeline(make_pipeline(context), 'pipe')
    
def make_pipeline(context):
    ##stock screen with market cap, price change, and volume
    val = morningstar.valuation.market_cap
    val_n = val.latest
    val_p = Previous(inputs = [val], window_length = 30)
    volume = USEquityPricing.volume
    
    priceb_filter = val_n > 1e8
    pricet_filter = val_n < 2e9
    movet_filter = (val_n / val_p) < 2.0
    moveb_filter = (val_n / val_p) > 0.5
    vol_filter = (val_n / volume) < 1e4
    
    return p.Pipeline(
    columns={
        'name' : morningstar.company_reference.legal_name.latest
        
    },
    screen=priceb_filter & pricet_filter & moveb_filter & movet_filter & vol_filter & p.filters.Q1500US()
  )

def before_trading_start(context, data):
    context.intraday_leverage = 0
    context.output = pipeline_output('pipe')
    context.penny_stocks = context.output.index
    context.weight = 1.0 / len(context.penny_stocks)

def my_open(context,data):
    context.trading_allowed = True
    
def my_close(context,data):
    context.trading_allowed = False
 
def my_record_vars(context, data):
    record(peak_leverage = context.intraday_leverage)
 
def handle_data(context,data):
    
    for stock in context.penny_stocks:
        ps_support_resistance(context,data,stock)
        
    # Track peak intraday leverage
    if context.account.leverage > context.intraday_leverage:
        context.intraday_leverage = context.account.leverage
    
def ps_support_resistance(context,data,stock):
    if get_open_orders(stock):
        return
    
    # Get our data
    price = data.current(stock,'price')
    hist = data.history(stock,'price',90,'1m')
    low = min(hist)
    low_threshold = low * 1.003 #relaxed by 0.3%
    high = max(hist)
    high_threshold = high * 0.997 #relaxed by 0.3%
    cost_basis = context.portfolio.positions[stock].cost_basis

    if context.trading_allowed \
    and price <= low_threshold \
    and not cost_basis:
        

        # Calculate how often support is retested
        support_retests = 0
        at_low = False
        for data_frame in hist:
            if not at_low and data_frame <= low_threshold:
                at_low = True
                support_retests += 1
            elif at_low and data_frame > low_threshold:
                at_low = False
        # Calculate how often resistance is retested
        resistance_retests = 0
        at_high = False
        for data_frame in hist:
            if not at_high and data_frame >= high_threshold:
                at_high = True
                resistance_retests += 1
            elif at_high and data_frame < high_threshold:
                at_high = False
                   
        if support_retests > 2 \
        and resistance_retests > 1 \
        and high_threshold > low_threshold * 1.005:
            order_target_percent(stock,context.weight,style=LimitOrder(price))

    elif cost_basis:      
        
        # stop loss
        if price < cost_basis * 0.998:
            order_target_percent(stock,0,style=LimitOrder(price))
        # hitting resistance
        elif price >= high_threshold:
            order_target_percent(stock,0,style=LimitOrder(price))
        # take profit
        elif price > cost_basis * 1.05:
            order_target_percent(stock,0,style=LimitOrder(price))
            
There was a runtime error.

I worry on backtests like these that the penny stocks could be so thinly traded the backtest won't match actual results

Think the original algorithm was using penny stocks and the pipeline version is mostly returning small caps. The pipeline version used Q1500 as one of the filters and when I looked at the stocks it was trading, many of them were over $1B market cap and were worth $10-$50 per share. Alpha might be higher with actual penny stocks, but capacity could be a lot higher with small caps. Looks like multiple people are interested in trading this so I want to see if capacity can be increased further.

I am not sure it's faster, but it is nicer:

price_hist_shifted = hist.shift(1)  
support_retests = sum( (hist <= low_threshold) & (price_hist_shifted > low_threshold) )  
resistance_retests = sum( (hist >= high_threshold) & (price_hist_shifted < high_threshold) )  

Also, pay attention to this zipline "behaviour": https://www.quantopian.com/posts/simulation-of-non-marketable-limit-orders
When you use limit orders you get better execution price on zipline than reality, especially on low volume securities.

I encountered that problem with two algorithms that I traded via IB. Fantastic results on backtests, horrible in live trading. Am I sure the bad performance was due to that bug mentioned above? No, I cannot be sure 100% but I spent quite some time trying to figure out the problem and that's the most plausible explanation.

I think it might be a combination of Q Slippage and real world trading.

Say for example your thinly traded penny stock is bought at .50 and hits a high of day of .75, which just happens to be where you were looking to sell. The issue is that in Q backtesting it just sees a price of .75, but there might have only been 1-2 shares traded at this level when it hit .75 and not the 1,000s of shares you might have in your backtest.

I believe Charles Witt had a "bottom fishing" algo where this type of scenario played out and the backtest vs live trading was drastically different.

I think setting up the filters with get fundamentals would be faster than using the pipeline, although one of the filters compares the current market cap to the market cap a month ago and I don't think that's possible with get fundamentals. Comparing the current stock price to the stock price a month ago should work, though. I haven't tried live algorithm trading yet so don't know how closely it would match the backtest results.

Is there a typo on the last line? The original value was 1.04, wondering if it's supposed to be 1.004.

I looked at the chart for PLCE, Children's Place, since it was one of the stocks returned by the Q1500 screen. After considering the chart, it looks like small caps and mid caps can rise 4% sometimes, but it usually happens the next day. So I think holding the position overnight might work better for the bigger stocks. Seems like this was happening anyway since many of the orders entered late in the day were not going through. If I'm looking at the algorithm correctly it's risking $1 to make $20 so it doesn't have to be correct very often. Tests with values other than 4% didn't work as well, although it's hard to test a lot of different values because the algorithm is slow.

I did another test with a higher starting balance. Seems like it still works pretty well, although alpha did go down a bit.

Clone Algorithm
27
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 import pipeline as p
from quantopian.pipeline.data import morningstar
from quantopian.pipeline import CustomFactor
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters.morningstar import Q1500US

class Previous(CustomFactor):  
    # Returns value of input x trading days ago where x is the window_length  
    # Both the inputs and window_length must be specified as there are no defaults
        def compute(self, today, assets, out, inputs):  
            out[:] = inputs[0]

def initialize(context):
    # Start buying 60 minutes after open in order to avoid potential big drops during morning volatility
    schedule_function(my_open, date_rules.every_day(), time_rules.market_open(minutes=60))
    # Stop buying 120 minutes before close so we can hopefully close out our positions before close
    schedule_function(my_close, date_rules.every_day(), time_rules.market_close(minutes=120))
    # Flag tells us whether or not we're allowed to buy.
    context.trading_allowed=False
    # Record tracking variables at the end of each day.
    schedule_function(my_record_vars, date_rules.every_day(), time_rules.market_close())

    # Current iteration is likely only profitable on Robinhood
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    
    attach_pipeline(make_pipeline(context), 'pipe')
    
def make_pipeline(context):
    ##stock screen with market cap, price change, and volume
    val = morningstar.valuation.market_cap
    val_n = val.latest
    val_p = Previous(inputs = [val], window_length = 30)
    volume = USEquityPricing.volume
    
    priceb_filter = val_n > 1e8
    pricet_filter = val_n < 2e9
    movet_filter = (val_n / val_p) < 2.0
    moveb_filter = (val_n / val_p) > 0.5
    vol_filter = (val_n / volume) < 1e4
    
    return p.Pipeline(
    columns={
        'name' : morningstar.company_reference.legal_name.latest
    },
    screen=priceb_filter & pricet_filter & moveb_filter & movet_filter & vol_filter & p.filters.Q1500US()
  )

def before_trading_start(context, data):
    context.intraday_leverage = 0 # Track the maximum leverage used throughout the day
    context.xactions = 0 # Track how many buy orders we execute each day
    context.output = pipeline_output('pipe')
    context.penny_stocks = context.output.index
    context.weight = 2.9 / len(context.penny_stocks)

def my_open(context,data):
    context.trading_allowed = True
    # Continue to close out any partially filled sell orders that 
    # might still be hanging around.
    for stock in context.portfolio.positions:
        if stock not in context.penny_stocks:
            order_target_percent(stock,0)            

def my_close(context,data):
    context.trading_allowed = False
 
def my_record_vars(context, data):
    record(peak_leverage = context.intraday_leverage)
    record(buy_orders = context.xactions)
    record(pipelen = len(context.penny_stocks))
 
def handle_data(context,data):
        
    # Loop through stocks
    for stock in context.penny_stocks:
        ps_support_resistance(context,data,stock)
        
    # Track peak intraday leverage
    if context.account.leverage > context.intraday_leverage:
        context.intraday_leverage = context.account.leverage
    
def ps_support_resistance(context,data,stock):
    
    # TODO: Cancel open orders after 90 minutes
    if get_open_orders(stock):
        return
    
    # Get our data
    price = data.current(stock,'price')
    hist = data.history(stock,'price',90,'1m')
    low = min(hist)
    low_threshold = low * 1.003 #relaxed by 0.3%
    high = max(hist)
    high_threshold = high * 0.997 #relaxed by 0.3%
    cost_basis = context.portfolio.positions[stock].cost_basis

    if context.trading_allowed \
    and not cost_basis \
    and price <= low_threshold \
    and high_threshold > low_threshold * 1.005:
        
        # Calculate how often support and resistance are retested
        support_retests = 0
        at_low = False
        resistance_retests = 0
        at_high = False
        for data_frame in hist:
            if not at_low and data_frame <= low_threshold:
                at_low = True
                support_retests += 1
            elif at_low and data_frame > low_threshold:
                at_low = False
            if not at_high and data_frame >= high_threshold:
                at_high = True
                resistance_retests += 1
            elif at_high and data_frame < high_threshold:
                at_high = False
        
        if support_retests > 4 \
        and resistance_retests > 1:
            order_target_percent(stock,context.weight,style=LimitOrder(price))
            context.xactions += 1

    elif cost_basis:      
        
        # stop loss
        if price < cost_basis * 0.998:
            order_target_percent(stock,0,style=LimitOrder(price))
        # hitting resistance
        elif price >= high_threshold:
            order_target_percent(stock,0,style=LimitOrder(price))
        # take profit
        elif price > cost_basis * 1.04:
            order_target_percent(stock,0,style=LimitOrder(price))
            
There was a runtime error.

Think it might run faster outside of the cloud platform, the code and inputs would need significant modifications and market cap might have to be replaced with share price depending on the data that's available.

Interesting. I was going to backtest it myself, but I was too impatient. I should probably let it run overnight. I tried something similar a week or two ago, but used the highs and lows of a moving average to determine support and resistance. I'll have to track down the code (I'm not good at labeling them or remembering which backtest is which) and post it. For trading penny stocks, I'd probably set high relative volume as a condition for entering or exiting a position to avoid liquidity issues.