Back to Community
Revisiting Penny Stocks

I recently came to take a look at penny stocks having trawled through Tim Syke's somewhat embarrassing website. I attach a back test which seems to show vast profit.

I think this code originated with Charles Witt here on Quantopian but in any event I nicked it to see if Syke's chart reading could be duplicated algorithmically.

The point here is that nobody has or can take account of the apparently vast bid offer spreads in a penny stock back test. To do that you need fine resolution bid/offer data not minute data. Without bid offer data too often your limit buy order will be filled at the bid and your limit sell order will be filled at the offer. If the spread is anywhere between 50 and 100% then clearly any profit shown would be a nonsense.

I believe Charles and others discovered that when they tried live trading.

That does not mean you can not benefit from penny stocks but my suspicion would be you need to ride on the coat tails of those who are organizing "promotions". Rather than trading algorithmically. But without bid offer data it is difficult to tell. Of course I am not suggesting doing anything illegal or that anyone such as Sykes is doing anything illegal. Provided you are not an insider, there is no illegality in following somebody else's promotions.

Having said all that, it appears that volume expands exponentially and spreads narrow likewise during pumps and dumps - legal or otherwise.
I have seen reports of live trades conducted at tiny percentage profits and losses.

It seems to me therefor that the key to testing more realistically using minute data has to be to trade on high volume only. Potentially by back testing on this basis and using appropriate volume based slippage it may be that more realism can be introduced.

Clone Algorithm
7
Loading...
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.algorithm 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
from quantopian.pipeline.filters.morningstar import IsPrimaryShare

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

from itertools import cycle

def initialize(context):
    #set_commission(commission.PerShare(cost=0.01, min_trade_cost=1.50))
    set_slippage(slippage.VolumeShareSlippage(volume_limit=.20, price_impact=0.0))
    #set_slippage(slippage.FixedSlippage(spread=0.00))
    set_commission(commission.PerTrade(cost=0.00))
    #set_slippage(slippage.FixedSlippage(spread=0.00))
    set_long_only()

    context.MaxCandidates=30
    context.MaxBuyOrdersAtOnce=10
    context.MyLeastPrice=0.40
    context.MyMostPrice=1.40
    context.MyFireSalePrice=context.MyLeastPrice
    context.MyFireSaleAge=3

    # over simplistic tracking of position age
    context.age={}
    print(len(context.portfolio.positions))
    # Rebalance
    EveryThisManyMinutes=105
    TradingDayHours=6.5
    TradingDayMinutes=int(TradingDayHours*60)
    for minutez in range(
        1, 
        TradingDayMinutes, 
        EveryThisManyMinutes
    ):
        schedule_function(my_rebalance, date_rules.every_day(), time_rules.market_open(minutes=minutez))

    # Prevent excessive logging of canceled orders at market close.
    schedule_function(cancel_open_orders, date_rules.every_day(), time_rules.market_close(hours=0, minutes=1))

    # Record variables at the end of each day.
    schedule_function(my_record_vars, date_rules.every_day(), time_rules.market_close())

    # Create our pipeline and attach it to our algorithm.
    my_pipe = make_pipeline(context)
    attach_pipeline(my_pipe, 'my_pipeline')

def make_pipeline(context):
    """
    Create our pipeline.
    """

    # Filter for primary share equities. IsPrimaryShare is a built-in filter.
    primary_share = IsPrimaryShare()

    # Equities listed as common stock (as opposed to, say, preferred stock).
    # 'ST00000001' indicates common stock.
    common_stock = morningstar.share_class_reference.security_type.latest.eq('ST00000001')

    # Non-depositary receipts. Recall that the ~ operator inverts filters,
    # turning Trues into Falses and vice versa
    not_depositary = ~morningstar.share_class_reference.is_depositary_receipt.latest

    # Equities not trading over-the-counter.
    not_otc = ~morningstar.share_class_reference.exchange_id.latest.startswith('OTC')

    # 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 with a null value in the limited_partnership Morningstar
    # fundamental field.
    not_lp_balance_sheet = morningstar.balance_sheet.limited_partnership.latest.isnull()

    # 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()

    # At least a certain price
    price = USEquityPricing.close.latest
    AtLeastPrice   = (price >= context.MyLeastPrice)
    AtMostPrice    = (price <= context.MyMostPrice)

    # Filter for stocks that pass all of our previous filters.
    tradeable_stocks = (
        primary_share
        & common_stock
        & not_depositary
        & not_otc
        & not_wi
        & not_lp_name
        & not_lp_balance_sheet
        & have_market_cap
        & AtLeastPrice
        & AtMostPrice
    )

    LowVar=6
    HighVar=40

    log.info('\nAlgorithm initialized variables:\n context.MaxCandidates %s \n LowVar %s \n HighVar %s'
        % (context.MaxCandidates, LowVar, HighVar)
    )

    # High dollar volume filter.
    base_universe = AverageDollarVolume(
        window_length=20,
        mask=tradeable_stocks
    ).percentile_between(LowVar, HighVar)

    # Short close price average.
    ShortAvg = SimpleMovingAverage(
        inputs=[USEquityPricing.close],
        window_length=3,
        mask=base_universe
    )

    # Long close price average.
    LongAvg = SimpleMovingAverage(
        inputs=[USEquityPricing.close],
        window_length=45,
        mask=base_universe
    )

    percent_difference = (ShortAvg - LongAvg) / LongAvg

    # Filter to select securities to long.
    stocks_worst = percent_difference.bottom(context.MaxCandidates)
    securities_to_trade = (stocks_worst)

    return Pipeline(
        columns={
            'stocks_worst': stocks_worst
        },
        screen=(securities_to_trade),
    )

def my_compute_weights(context):
    """
    Compute ordering weights.
    """
    # Compute even target weights for our long positions and short positions.
    stocks_worst_weight = 1.00/len(context.stocks_worst)

    return stocks_worst_weight

def before_trading_start(context, data):
    # Gets our pipeline output every day.
    context.output = pipeline_output('my_pipeline')

    context.stocks_worst = context.output[context.output['stocks_worst']].index.tolist()

    print(context.stocks_worst)
    context.stocks_worst_weight = my_compute_weights(context)

    context.MyCandidate = cycle(context.stocks_worst)
    
    context.LowestPrice=context.MyLeastPrice #reset beginning of day
    print(len(context.portfolio.positions))
    for stock in context.portfolio.positions:
        CurrPrice = float(data.current([stock], 'price'))
        if CurrPrice<context.LowestPrice:
            context.LowestPrice = CurrPrice
        if stock in context.age:
            context.age[stock] += 1
        else:
            context.age[stock] = 1
    for stock in context.age:
        if stock not in context.portfolio.positions:
            context.age[stock] = 0
        message = 'stock.symbol: {symbol}  :  age: {age}'
        log.info(message.format(symbol=stock.symbol, age=context.age[stock]))

    pass

def my_rebalance(context, data):
    BuyFactor=.99
    SellFactor=1.01
    cash=context.portfolio.cash

    cancel_open_buy_orders(context, data)

    # Order sell at profit target in hope that somebody actually buys it
    for stock in context.portfolio.positions:
        if not get_open_orders(stock):
            StockShares = context.portfolio.positions[stock].amount
            CurrPrice = float(data.current([stock], 'price'))
            CostBasis = float(context.portfolio.positions[stock].cost_basis)
            SellPrice = float(make_div_by_05(CostBasis*SellFactor, buy=False))
            if np.isnan(SellPrice):
                pass # probably best to wait until nan goes away
            elif (
                stock in context.age 
                and context.MyFireSaleAge<=context.age[stock] 
                and (
                    context.MyFireSalePrice>CurrPrice
                    or CostBasis>CurrPrice
                )
            ):
                SellPrice = float(make_div_by_05(.95*CurrPrice, buy=False))
                order(stock, -StockShares,
                    style=LimitOrder(SellPrice)
                )
            else:
                order(stock, -StockShares,
                    style=LimitOrder(SellPrice)
                )

    WeightThisBuyOrder=float(1.00/context.MaxBuyOrdersAtOnce)
    for ThisBuyOrder in range(context.MaxBuyOrdersAtOnce):
        stock = next(context.MyCandidate)
        PH = data.history([stock], 'price', 20, '1d')
        PH_Avg = float(PH.mean())
        CurrPrice = float(data.current([stock], 'price'))
        if np.isnan(CurrPrice):
            pass # probably best to wait until nan goes away
        else:
            if CurrPrice > float(1.25*PH_Avg):
                BuyPrice=float(CurrPrice)
            else:
                BuyPrice=float(CurrPrice*BuyFactor)
            BuyPrice=float(make_div_by_05(BuyPrice, buy=True))
            StockShares = int(WeightThisBuyOrder*cash/BuyPrice)
            order(stock, StockShares,
                style=LimitOrder(BuyPrice)
            )

#if cents not divisible by .05, round down if buy, round up if sell
def make_div_by_05(s, buy=False):
    s *= 20.00
    s =  math.floor(s) if buy else math.ceil(s)
    s /= 20.00
    return s

def my_record_vars(context, data):
    """
    Record variables at the end of each day.
    """

    # Record our variables.
    record(leverage=context.account.leverage)
    record(positions=len(context.portfolio.positions))
    if 0<len(context.age):
        MaxAge=context.age[max(list(context.age.keys()), key=(lambda k: context.age[k]))]
        print(MaxAge)
        record(MaxAge=MaxAge)
    record(LowestPrice=context.LowestPrice)

def log_open_order(StockToLog):
    oo = get_open_orders()
    if len(oo) == 0:
        return
    for stock, orders in oo.items():
        if stock == StockToLog:
            for order in orders:
                message = 'Found open order for {amount} shares in {stock}'
                log.info(message.format(amount=order.amount, stock=stock))

def log_open_orders():
    oo = get_open_orders()
    if len(oo) == 0:
        return
    for stock, orders in oo.items():
        for order in orders:
            message = 'Found open order for {amount} shares in {stock}'
            log.info(message.format(amount=order.amount, stock=stock))

def cancel_open_buy_orders(context, data):
    oo = get_open_orders()
    if len(oo) == 0:
        return
    for stock, orders in oo.items():
        for order in orders:
            #message = 'Canceling order of {amount} shares in {stock}'
            #log.info(message.format(amount=order.amount, stock=stock))
            if 0<order.amount: #it is a buy order
                cancel_order(order)

def cancel_open_orders(context, data):
    oo = get_open_orders()
    if len(oo) == 0:
        return
    for stock, orders in oo.items():
        for order in orders:
            #message = 'Canceling order of {amount} shares in {stock}'
            #log.info(message.format(amount=order.amount, stock=stock))
            cancel_order(order)

# This is the every minute stuff
def handle_data(context, data):
    pass
There was a runtime error.