Back to Community
Quantitative Micro-cap Portfolio

This post showed up in my news feed yesterday: https://seekingalpha.com/article/4253717-misled-ishares-micro-cap-etf

In it the author claims that a simple quantitative micro-cap portfolio would have significantly outperformed the IWC microcap ETF. I followed his criteria (except the bit about short interest, since I still don't know how to access that information in Quantopian) and lo and behold -- Looks impressive.

Clone Algorithm
21
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
"""
"Microcap Fundamental Portfolio"
By Viridian Hawk
Based on: https://seekingalpha.com/article/4253717-misled-ishares-micro-cap-etf
"""

import quantopian.algorithm  as algo
import quantopian.optimize   as opt
from quantopian.pipeline               import Pipeline, CustomFactor
from quantopian.pipeline.data.builtin  import USEquityPricing
from quantopian.pipeline.data          import Fundamentals
from quantopian.pipeline.filters       import QTradableStocksUS
from quantopian.pipeline.factors       import AverageDollarVolume


def initialize(context):
    # Rebalance every day, 1 hour after market open.
    algo.schedule_function(
        rebalance,
        algo.date_rules.month_start(),
        algo.time_rules.market_open(hours=1),
    )

    # Create our dynamic stock selector.
    algo.attach_pipeline(make_pipeline(), 'pipeline')
    
    set_benchmark(symbol('IWC'))
    
    
class Previous(CustomFactor):  
    def compute(self, today, assets, out, inputs):  
        out[:] = inputs[0]
        

def make_pipeline():
    # Market cap between $6m and $3.5b
    mktcap = Fundamentals.market_cap.latest
    m   =   mktcap > 6e6
    m  &=   mktcap < 3.5e9

    # Price above $1.00
    yesterday_close = USEquityPricing.close.latest
    m  &=   yesterday_close > 1.0
    
    # adv > $100k
    m  &=   AverageDollarVolume(window_length=20) > 100e3
    
    # 1-yr of positive FCF
    fcf = Fundamentals.fcf_per_share
    m  &=   fcf.latest.notnull()
    m  &=   fcf.latest > 0
    m  &=   Previous(inputs = [fcf], window_length =  63, mask=m) > 0
    m  &=   Previous(inputs = [fcf], window_length = 126, mask=m) > 0
    m  &=   Previous(inputs = [fcf], window_length = 189, mask=m) > 0
    m  &=   Previous(inputs = [fcf], window_length = 252, mask=m) > 0
    
    # stocks w/most free cash flow
    m  &=   (fcf.latest / yesterday_close).percentile_between(50,90, mask=m)
    
    # remove highest price-to-sales ratio stocks per sector
    price_to_sales = Fundamentals.ps_ratio.latest
    industry_group = Fundamentals.morningstar_industry_group_code.latest
    m  &=   price_to_sales.zscore(groupby=industry_group, mask=m).percentile_between(10,25)

    pipe = Pipeline(
        columns={
        },
        screen=m
    )
    return pipe


def before_trading_start(context, data):
    context.longs  = algo.pipeline_output('pipeline').index
    
    longs = shorts = 0
    for stock in context.portfolio.positions:
        if context.portfolio.positions[stock].amount > 0:
            longs += 1
        elif  context.portfolio.positions[stock].amount < 0:
            shorts += 1
    record(longs = longs)
    record(shorts = shorts)
    record(l = context.account.leverage)
    
    
def rebalance(context, data):
    for s in context.portfolio.positions:
        if s not in context.longs:
            order_target(s, 0)
            
    for s in context.longs:
        if data.can_trade(s):
            order_target_percent(s, 1.0/len(context.longs))
There was a runtime error.
2 responses

In practice microcaps are likely too expensive to short as a hedge for a market-neutral version, so here's a version that uses the IWC ETF as the short hedge.

Clone Algorithm
21
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
"""
The market-cap range is currently from $3.5 billion down to less than $6 million.
There are 600 stocks which have a market-cap that is greater than $300 million.
Roughly 180 stocks have a market-cap less than $50 million.


1. Look for stocks which have positive trailing free cash flow over the past 12 months. This means that they are cash flow positive after accounting for capital expenditures. Forget about earnings in small companies and look for free cash flow.

2. Remove overvalued stocks. As many small companies do not have positive earnings, you should not use the P/E ratio. A simple filter is to remove stocks which have a high price to sales ratio when compared to the sector average.

3. Remove high short interest stocks. Shorting micro-caps is no easy endeavour. When you see high short interest in a micro-cap stock, it usually means that informed investors are shorting because there is very high risk present. My recommendation is to just stay away.

4. Liquidity. I usually don’t recommend trading super illiquid stocks. Share price above $1 and average turnover of more than $100,000 per day should be sufficient for smaller investors.
"""
import quantopian.algorithm as algo
from quantopian.pipeline               import Pipeline, CustomFactor
from quantopian.pipeline.data.builtin  import USEquityPricing
from quantopian.pipeline.data          import Fundamentals
from quantopian.pipeline.filters       import QTradableStocksUS
from quantopian.pipeline.factors       import AverageDollarVolume


def initialize(context):
    """
    Called once at the start of the algorithm.
    """
    # Rebalance every day, 1 hour after market open.
    algo.schedule_function(
        rebalance,
        algo.date_rules.month_start(),
        algo.time_rules.market_open(hours=1),
    )

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

    # Create our dynamic stock selector.
    algo.attach_pipeline(make_pipeline(), 'pipeline')
    
    set_benchmark(symbol('IWC'))

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

def make_pipeline():
    # Market cap between $6m and $3.5b
    mktcap = Fundamentals.market_cap.latest
    m   =   mktcap > 6e6
    m  &=   mktcap < 3.5e9

    # Price above $1.00
    yesterday_close = USEquityPricing.close.latest
    m  &=   yesterday_close > 1.0
    
    # adv > $100k
    m  &=   AverageDollarVolume(window_length=20) > 100e3
    
    # 1-yr of positive FCF
    fcf = Fundamentals.fcf_per_share
    m  &=   fcf.latest.notnull()
    m  &=   fcf.latest > 0
    m  &=   Previous(inputs = [fcf], window_length =  63) > 0
    m  &=   Previous(inputs = [fcf], window_length = 126) > 0
    m  &=   Previous(inputs = [fcf], window_length = 189) > 0
    m  &=   Previous(inputs = [fcf], window_length = 252) > 0
    
    # remove highest price-to-sales ratio stocks per sector
    price_to_sales = Fundamentals.ps_ratio.latest
    industry_group = Fundamentals.morningstar_industry_group_code.latest
    m  &=   price_to_sales.zscore(groupby = industry_group).percentile_between(0,50)

    pipe = Pipeline(
        columns={
            #'close': yesterday_close,
        },
        screen=m
    )
    return pipe


def before_trading_start(context, data):
    context.output = algo.pipeline_output('pipeline')


def rebalance(context, data):
    
    HEDGE = symbol('IWC')
    
    order_target_percent(HEDGE, -0.49)
    
    for s in context.output.index:
        if data.can_trade(s):
            order_target_percent(s, 0.5/len(context.output.index))
        
    for s in context.portfolio.positions:
        if s not in context.output.index \
        and s != HEDGE:
            order_target(s, 0)
 


def record_vars(context, data):
    record(pos = len(context.portfolio.positions))
    record(l = context.account.leverage)
There was a runtime error.

I also gave it a go hedging with stocks (though in practice this would likely be too costly). Surprisingly consistent returns until 2017.

1.52 sharpe over a 15-year backtest is not bad!

Clone Algorithm
21
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
"""
"Hedged Microcap Fundamental Portfolio"
By Viridian Hawk
Based on: https://seekingalpha.com/article/4253717-misled-ishares-micro-cap-etf
"""
import quantopian.algorithm  as algo
import quantopian.optimize   as opt
from quantopian.pipeline               import Pipeline, CustomFactor
from quantopian.pipeline.data.builtin  import USEquityPricing
from quantopian.pipeline.data          import Fundamentals
from quantopian.pipeline.filters       import QTradableStocksUS
from quantopian.pipeline.factors       import AverageDollarVolume
from quantopian.pipeline.experimental  import risk_loading_pipeline


def initialize(context):
    # Rebalance every day, 1 hour after market open.
    algo.schedule_function(
        rebalance,
        algo.date_rules.month_start(),
        algo.time_rules.market_open(hours=1),
    )
    # Rebalance every day, 1 hour after market open.
    algo.schedule_function(
        check_leverage,
        algo.date_rules.every_day(),
        algo.time_rules.market_close(hours=1),
    )

    # Create our dynamic stock selector.
    algo.attach_pipeline(make_pipeline(), 'pipeline')
    algo.attach_pipeline(make_pipeline_universe(), 'pipeline_universe')
    algo.attach_pipeline(risk_loading_pipeline(), 'risk_loading_pipeline')
    
    
class Previous(CustomFactor):  
    def compute(self, today, assets, out, inputs):  
        out[:] = inputs[0]
        

def make_pipeline():
    # Market cap between $6m and $3.5b
    mktcap = Fundamentals.market_cap.latest
    m   =   mktcap > 6e6
    m  &=   mktcap < 3.5e9

    # Price above $1.00
    yesterday_close = USEquityPricing.close.latest
    m  &=   yesterday_close > 1.0
    
    # adv > $100k
    m  &=   AverageDollarVolume(window_length=20) > 100e3
    
    # 1-yr of positive FCF
    fcf = Fundamentals.fcf_per_share
    m  &=   fcf.latest.notnull()
    m  &=   fcf.latest > 0
    m  &=   Previous(inputs = [fcf], window_length =  63, mask=m) > 0
    m  &=   Previous(inputs = [fcf], window_length = 126, mask=m) > 0
    m  &=   Previous(inputs = [fcf], window_length = 189, mask=m) > 0
    m  &=   Previous(inputs = [fcf], window_length = 252, mask=m) > 0
    
    # remove highest price-to-sales ratio stocks per sector
    price_to_sales = Fundamentals.ps_ratio.latest
    industry_group = Fundamentals.morningstar_industry_group_code.latest
    m  &=   price_to_sales.notnull()
    m  &=   industry_group.notnull()
    m  &=   price_to_sales.zscore(groupby=industry_group, mask=m).percentile_between(1,50)

    pipe = Pipeline(
        columns={
        },
        screen=m
    )
    return pipe


def make_pipeline_universe():
    # Market cap between $6m and $3.5b
    mktcap = Fundamentals.market_cap.latest
    m   =   mktcap > 6e6
    m  &=   mktcap < 3.5e9
    
    # Price above $1.00
    yesterday_close = USEquityPricing.close.latest
    m  &=   yesterday_close > 1.0
    
    # adv > $100k
    m  &=   AverageDollarVolume(window_length=20) > 100e3
    
    industry_group = Fundamentals.morningstar_industry_group_code.latest
    
    pipe = Pipeline(
        columns={
            'industry_group': industry_group,
        },
        screen=m
    )
    return pipe
    
    
def before_trading_start(context, data):
    context.risk_loading_pipeline = algo.pipeline_output('risk_loading_pipeline')
    context.output = algo.pipeline_output('pipeline_universe')
    context.longs  = algo.pipeline_output('pipeline').index
    
    longs = shorts = 0
    for stock in context.portfolio.positions:
        if context.portfolio.positions[stock].amount > 0:
            longs += 1
        elif  context.portfolio.positions[stock].amount < 0:
            shorts += 1
    record(longs = longs)
    record(shorts = shorts)
    
    
def check_leverage(context, data):
    leverage = context.account.leverage
    record(l = leverage)
    if leverage > 1.05 \
    or leverage < 0.9:
        rebalance(context, data)    
    
    
def rebalance(context, data):
    weight = {}
    for stock in context.output.index:
        if data.can_trade(stock):
            if stock in context.longs:
                weight[stock] = 1
            else:
                weight[stock] = -1
    
    order_optimal_portfolio(  
        objective=opt.TargetWeights(weight),  
        constraints=[
            opt.MaxGrossExposure(1.0),
            opt.DollarNeutral(),
            #opt.PositionConcentration.with_equal_bounds(-0.01, 0.01),
            opt.NetGroupExposure.with_equal_bounds(labels=context.output.industry_group,
                min=-0.01,
                max=0.01),
            #opt.FactorExposure(context.output[['beta']],
            #    min_exposures={'beta': -0.05},
            #    max_exposures={'beta': 0.05}),
            opt.experimental.RiskModelExposure(
                risk_model_loadings=context.risk_loading_pipeline,
                version=opt.Newest,
                min_size       = -0.00,
                max_size       =  0.00,
                min_value      = -0.02,
                max_value      =  0.02,
                min_volatility = -0.01,
                max_volatility =  0.01,
                min_momentum   = -0.01,
                max_momentum   =  0.01,
                min_short_term_reversal = -0.01,
                max_short_term_reversal =  0.01,
                min_consumer_cyclical   = -0.01, 
                max_consumer_cyclical   =  0.01, 
                min_energy              = -0.02, 
                max_energy              =  0.02, 
                min_industrials         = -0.01,
                max_industrials         =  0.01, 
            ),
        ],  
    )
There was a runtime error.