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
44
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
"""
"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.
1 response

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
44
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
"""
"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.