Back to Community
Picking stocks based on the Kelly Criterion

I'm sure many know of calculating the kelly sizing for an optimum bet. This algo does things a bit differently. It attempts to pick stocks based on some other formulas surrounding the kelly criterion. This algo will ask for your desired bet size and you pick stocks based on a criterion such as minimum variance or maximum growth with that bet size. It doesn't do any fancy entry, exit, or risk control. It's also long only. It might be useful for real trading in your personal account if you managed to figure out a strategy around it. The algo comes with different options and parameters that you can fiddle with. Let me know if you figure out the parameters that'll make money :). By the way, you can read all about these formulas in "THE KELLY CRITERION IN BLACKJACK SPORTS BETTING, AND THE STOCK MARKET" by Thorp.

I'm going to post 3 different results based on variance, growth, and time. The first will be stock picking based on minimal variance.

Clone Algorithm
41
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
# Picking stocks based on the Kelly criterion
import math
import numpy as np
import pandas

# Pick stocks with the shortest time for Vn > V0 with ~98% probability
def time(context,data,g,mean,var):
    k = 2.0 # Standard deviation
    t = (k**2.0*var*context.f**2.0/g**2.0)
    t.sort()
    return t.head(context.securities)
    
# Pick stocks with the least variance
def var(context,data,g,mean,var):
    var.sort()
    return var.head(context.securities)
    
# Pick stocks with the highest estimated growth rate
def growth(context,data,g,mean,var):
    return g.tail(context.securities)
    
# The initialize function is the place to set your tradable universe and define any parameters. 
def initialize(context):
    set_do_not_order_list(security_lists.leveraged_etf_list)
    set_universe(universe.DollarVolumeUniverse(floor_percentile=95.0,ceiling_percentile=100.0))
    
    context.securities = 10
    context.delisted = []
    context.lookback = 252
    context.apr_min_filter = 0.01
    context.apr_max_filter = 1.00
    context.picks = pandas.Series()
    # bet size of each security for estimated growth
    # multiplied by 2 to underbet (half kelly)
    context.f = 1.0/context.securities*2.0 
    context.pick_option = 2
    context.pick_method = {
        1 : time,
        2 : var,
        3 : growth,
    }
    
    schedule_function(repick, 
        date_rules.month_start(),
        time_rules.market_open(hours = 0, minutes = 15))
    
    schedule_function(rebalance, 
        date_rules.week_start(),
        time_rules.market_open(hours = 1, minutes = 0))
     
def handle_data(context,data):
    if not '_peak_leverage' in context:
        context._peak_leverage = 0
    if not '_min_cash' in context:
        context._min_cash = context.portfolio.cash
        
    context._peak_leverage = max(context._peak_leverage,context.account.leverage)
    context._min_cash = min(context._min_cash,context.portfolio.cash)
    
    # record(peak_lev = context._peak_leverage)
    record(leverage = context.account.leverage)
    # record(min_cash = context._min_cash)
    
    pos_sum = 0
    delisted_sum = 0

    for sec,v in context.portfolio.positions.iteritems():
        if sec in context.picks.index:
            pos_sum += np.abs(v.amount*v.last_sale_price)
        elif sec in context.delisted:
            delisted_sum += np.abs(v.amount*v.last_sale_price)
            
    record(correct_lev = pos_sum/context.portfolio.portfolio_value)
    record(delisted_lev = delisted_sum/context.portfolio.portfolio_value)
            
def repick(context,data):
    prices = history(bar_count=context.lookback, frequency='1d', field='price').dropna(axis=1)
    r = prices.pct_change().dropna()
    mean = r.mean()
    var = r.var()
    
    # g = r + f(m-r) - s^2*f^2/2
    # instantaneous growth rate with assumed bet size and risk free r
    f = context.f
    g = mean*f - (var*f**2)/2
    
    gyr = g.apply(lambda x: np.exp(x*252.0)-1.0)
    g = (g[gyr <= context.apr_max_filter*f])[gyr >= context.apr_min_filter*f]
    g = g[~g.index.isin(context.delisted)]
    g = g[~g.index.isin(security_lists.leveraged_etf_list)]
    mean = mean[g.index]
    var = var[g.index]
    
    context.picks = context.pick_method[context.pick_option](context,data,g,mean,var)
            
    record(delisted = len(context.delisted))
    
    log.info('Picks: {}'.format(context.picks))
    
def rebalance(context,data):
    positions = 0
    
    for security,w in context.picks.iteritems():
        if security not in data:
            repick(context,data)
            break
    
    for sec,v in context.portfolio.positions.iteritems():
        if sec not in context.picks.index:
            order_target_value(sec,0)
        if data[sec].sid.end_date < get_datetime():
            if not (sec in context.delisted):
                context.delisted.append(sec)
    
    for security,w in context.picks.iteritems():
        if get_open_orders(security): continue
        if security in data:
            order_target_percent(security,1.0/context.securities)
            positions += 1
            
    # record(picked_size = context.picks.size)
    record(positions = positions)
There was a runtime error.
3 responses

This one picks stocks based on the highest estimated growth rate (g)

Clone Algorithm
41
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
# Picking stocks based on the Kelly criterion
import math
import numpy as np
import pandas

# Pick stocks with the shortest time for Vn > V0 with ~98% probability
def time(context,data,g,mean,var):
    k = 2.0 # Standard deviation
    t = (k**2.0*var*context.f**2.0/g**2.0)
    t.sort()
    return t.head(context.securities)
    
# Pick stocks with the least variance
def var(context,data,g,mean,var):
    var.sort()
    return var.head(context.securities)
    
# Pick stocks with the highest estimated growth rate
def growth(context,data,g,mean,var):
    return g.tail(context.securities)
    
# The initialize function is the place to set your tradable universe and define any parameters. 
def initialize(context):
    set_do_not_order_list(security_lists.leveraged_etf_list)
    set_universe(universe.DollarVolumeUniverse(floor_percentile=95.0,ceiling_percentile=100.0))
    
    context.securities = 10
    context.delisted = []
    context.lookback = 252
    context.apr_min_filter = 0.00
    context.apr_max_filter = 1.00
    context.picks = pandas.Series()
    # bet size of each security for estimated growth
    # multiplied by 2 to underbet (half kelly)
    context.f = 1.0/context.securities*2.0 
    context.pick_option = 3
    context.pick_method = {
        1 : time,
        2 : var,
        3 : growth,
    }
    
    schedule_function(repick, 
        date_rules.month_start(),
        time_rules.market_open(hours = 0, minutes = 15))
    
    schedule_function(rebalance, 
        date_rules.week_start(),
        time_rules.market_open(hours = 1, minutes = 0))
     
def handle_data(context,data):
    if not '_peak_leverage' in context:
        context._peak_leverage = 0
    if not '_min_cash' in context:
        context._min_cash = context.portfolio.cash
        
    context._peak_leverage = max(context._peak_leverage,context.account.leverage)
    context._min_cash = min(context._min_cash,context.portfolio.cash)
    
    # record(peak_lev = context._peak_leverage)
    record(leverage = context.account.leverage)
    # record(min_cash = context._min_cash)
    
    pos_sum = 0
    delisted_sum = 0

    for sec,v in context.portfolio.positions.iteritems():
        if sec in context.picks.index:
            pos_sum += np.abs(v.amount*v.last_sale_price)
        elif sec in context.delisted:
            delisted_sum += np.abs(v.amount*v.last_sale_price)
            
    record(correct_lev = pos_sum/context.portfolio.portfolio_value)
    record(delisted_lev = delisted_sum/context.portfolio.portfolio_value)
            
def repick(context,data):
    prices = history(bar_count=context.lookback, frequency='1d', field='price').dropna(axis=1)
    r = prices.pct_change().dropna()
    mean = r.mean()
    var = r.var()
    
    # g = r + f(m-r) - s^2*f^2/2
    # instantaneous growth rate with assumed bet size and risk free r
    f = context.f
    g = mean*f - (var*f**2)/2
    
    gyr = g.apply(lambda x: np.exp(x*252.0)-1.0)
    g = (g[gyr <= context.apr_max_filter*f])[gyr >= context.apr_min_filter*f]
    g = g[~g.index.isin(context.delisted)]
    g = g[~g.index.isin(security_lists.leveraged_etf_list)]
    mean = mean[g.index]
    var = var[g.index]
    
    context.picks = context.pick_method[context.pick_option](context,data,g,mean,var)
            
    record(delisted = len(context.delisted))
    
    log.info('Picks: {}'.format(context.picks))
    
def rebalance(context,data):
    positions = 0
    
    for security,w in context.picks.iteritems():
        if security not in data:
            repick(context,data)
            break
    
    for sec,v in context.portfolio.positions.iteritems():
        if sec not in context.picks.index:
            order_target_value(sec,0)
        if data[sec].sid.end_date < get_datetime():
            if not (sec in context.delisted):
                context.delisted.append(sec)
    
    for security,w in context.picks.iteritems():
        if get_open_orders(security): continue
        if security in data:
            order_target_percent(security,1.0/context.securities)
            positions += 1
            
    # record(picked_size = context.picks.size)
    record(positions = positions)
There was a runtime error.

This one picks stocks with the shortest time for Vn > V0 with ~98% probability, where V is the value of the portfolio.

Clone Algorithm
41
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
# Picking stocks based on the Kelly criterion
import math
import numpy as np
import pandas

# Pick stocks with the shortest time for Vn > V0 with ~98% probability
def time(context,data,g,mean,var):
    k = 2.0 # Standard deviation
    t = (k**2.0*var*context.f**2.0/g**2.0)
    t.sort()
    return t.head(context.securities)
    
# Pick stocks with the least variance
def var(context,data,g,mean,var):
    var.sort()
    return var.head(context.securities)
    
# Pick stocks with the highest estimated growth rate
def growth(context,data,g,mean,var):
    return g.tail(context.securities)
    
# The initialize function is the place to set your tradable universe and define any parameters. 
def initialize(context):
    set_do_not_order_list(security_lists.leveraged_etf_list)
    set_universe(universe.DollarVolumeUniverse(floor_percentile=95.0,ceiling_percentile=100.0))
    
    context.securities = 10
    context.delisted = []
    context.lookback = 252
    context.apr_min_filter = 0.00
    context.apr_max_filter = 1.00
    context.picks = pandas.Series()
    # bet size of each security for estimated growth
    # multiplied by 2 to underbet (half kelly)
    context.f = 1.0/context.securities*2.0 
    context.pick_option = 1
    context.pick_method = {
        1 : time,
        2 : var,
        3 : growth,
    }
    
    schedule_function(repick, 
        date_rules.month_start(),
        time_rules.market_open(hours = 0, minutes = 15))
    
    schedule_function(rebalance, 
        date_rules.week_start(),
        time_rules.market_open(hours = 1, minutes = 0))
     
def handle_data(context,data):
    if not '_peak_leverage' in context:
        context._peak_leverage = 0
    if not '_min_cash' in context:
        context._min_cash = context.portfolio.cash
        
    context._peak_leverage = max(context._peak_leverage,context.account.leverage)
    context._min_cash = min(context._min_cash,context.portfolio.cash)
    
    # record(peak_lev = context._peak_leverage)
    record(leverage = context.account.leverage)
    # record(min_cash = context._min_cash)
    
    pos_sum = 0
    delisted_sum = 0

    for sec,v in context.portfolio.positions.iteritems():
        if sec in context.picks.index:
            pos_sum += np.abs(v.amount*v.last_sale_price)
        elif sec in context.delisted:
            delisted_sum += np.abs(v.amount*v.last_sale_price)
            
    record(correct_lev = pos_sum/context.portfolio.portfolio_value)
    record(delisted_lev = delisted_sum/context.portfolio.portfolio_value)
            
def repick(context,data):
    prices = history(bar_count=context.lookback, frequency='1d', field='price').dropna(axis=1)
    r = prices.pct_change().dropna()
    mean = r.mean()
    var = r.var()
    
    # g = r + f(m-r) - s^2*f^2/2
    # instantaneous growth rate with assumed bet size and risk free r
    f = context.f
    g = mean*f - (var*f**2)/2
    
    gyr = g.apply(lambda x: np.exp(x*252.0)-1.0)
    g = (g[gyr <= context.apr_max_filter*f])[gyr >= context.apr_min_filter*f]
    g = g[~g.index.isin(context.delisted)]
    g = g[~g.index.isin(security_lists.leveraged_etf_list)]
    mean = mean[g.index]
    var = var[g.index]
    
    context.picks = context.pick_method[context.pick_option](context,data,g,mean,var)
            
    record(delisted = len(context.delisted))
    
    log.info('Picks: {}'.format(context.picks))
    
def rebalance(context,data):
    positions = 0
    
    for security,w in context.picks.iteritems():
        if security not in data:
            repick(context,data)
            break
    
    for sec,v in context.portfolio.positions.iteritems():
        if sec not in context.picks.index:
            order_target_value(sec,0)
        if data[sec].sid.end_date < get_datetime():
            if not (sec in context.delisted):
                context.delisted.append(sec)
    
    for security,w in context.picks.iteritems():
        if get_open_orders(security): continue
        if security in data:
            order_target_percent(security,1.0/context.securities)
            positions += 1
            
    # record(picked_size = context.picks.size)
    record(positions = positions)
There was a runtime error.

Hi Minh,

Thanks for sharing these! Have you tried running different iterations through pyfolio for a more detailed performance/risk analysis?

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.