Back to Community
Momentum with kelly strategy

The backtest tried to be as realistic as possible. Shorting and margin was eliminated. The code is a bit dense but it should serve as a base because of the many options.

Clone Algorithm
223
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
# Import the libraries we will use here
import math
import numpy as np
import pandas
from scipy.optimize import minimize
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline import CustomFactor
from quantopian.pipeline.data import morningstar

class OrderHandler:
    queue = []
    safety_factor = 0.95
    limit_order = True # Limit orders don't work for daily mode
    # limit_order = False
    iceberg_order = True
    # iceberg_order = False
    
    def push(self,security,leverage):
        self.queue.insert(0,{'security':security, 'leverage':leverage})
        
    def clearQueue(self):
        self.queue = []
        
    def sort(self,context,data):
        deltas = {}
    
        for i,item in enumerate(self.queue):
            security = item['security']
            leverage = item['leverage']
            
            if security not in data:
                deltas[security] = 0
                continue
            
            price = data[security].price
            target = int( (context.curr_risk * leverage * self.safety_factor ) / price )
            shares = context.portfolio.positions[security].amount
            delta_shares = target - shares
            delta_cash = delta_shares*price
            deltas[security] = delta_cash
            
        self.queue.sort(key=lambda x: deltas[x['security']], reverse=True)
        
    def run(self,context,data):
        if context._day_count % context.trade_period_days != 0: return
        purchasing_power = max(context.curr_risk*context.risk_leverage - context.portfolio.portfolio_value*context.account.leverage, context.portfolio.cash, 0)
        purchasing_power = int( purchasing_power )
        
        if get_open_orders(): return
        
        if not context.hold_cash and context.riskless in data:
            security = context.riskless
            reserved = np.sum(np.abs(context.leverage.values())) * context.curr_risk * self.safety_factor
            available = max(context.portfolio.portfolio_value - reserved, 0) * self.safety_factor
            
            if available >= context.portfolio.portfolio_value*self.safety_factor:
                target = int( available / data[security].price )
            else:
                target = 0
                
            shares = context.portfolio.positions[security].amount
            delta_shares = target - shares
            if self.iceberg_order:
                sign = np.sign(delta_shares)
                delta_shares = sign*min(data[security].volume*context.max_pct_liquidity,np.abs(delta_shares))
            delta_cash = delta_shares*data[security].price
            
            if delta_cash <= purchasing_power:
                if self.limit_order and context.minute_mode_only:
                    mid_price = int( (data[security].high + data[security].low) / 2 * 100 )
                    mid_price = mid_price / 100
                    
                    order(security,delta_shares,style=LimitOrder(mid_price))
                else:
                    order(security,delta_shares)
                    
                purchasing_power -= max(delta_cash,0)
                purchasing_power = max(purchasing_power,0)
        
        for i,item in reversed(list(enumerate(self.queue))):
            security = item['security']
            leverage = item['leverage']
            delisted = security.end_date < get_datetime()
            
            if security not in data: continue
            
            price = data[security].price
            target = int( (context.curr_risk * leverage * self.safety_factor ) / price )
            shares = context.portfolio.positions[security].amount
            delta_shares = target - shares
            if self.iceberg_order:
                sign = np.sign(delta_shares)
                delta_shares = sign*min(data[security].volume*context.max_pct_liquidity,np.abs(delta_shares))
            delta_cash = delta_shares*price
            
            if (target - shares) == 0 or delisted:
                self.queue.pop(i)
                
            elif delta_cash <= purchasing_power:
                if self.limit_order and context.minute_mode_only:
                    mid_price = int( (data[security].high + data[security].low) / 2 * 100 )
                    mid_price = mid_price / 100
                    
                    order(security,delta_shares,style=LimitOrder(mid_price))
                else:
                    order(security,delta_shares)
                    
                purchasing_power -= max(delta_cash,0)
                purchasing_power = max(purchasing_power,0)
                
        context._first_traded = True
        
orderHandler = OrderHandler()

class Mean(CustomFactor):
    inputs = [USEquityPricing.close]

    def compute(self, today, asset_ids, out, close):
        r = np.diff(close, axis=0) / close[0:-1]
        mean = np.mean(r,axis=0)
        out[:] = mean
        
class Variance(CustomFactor):
    inputs = [USEquityPricing.close]

    def compute(self, today, asset_ids, out, close):
        r = np.diff(close, axis=0) / close[0:-1]
        var = np.var(r,axis=0)
        out[:] = var
        
class DollarVolume(CustomFactor):
    inputs = [USEquityPricing.close, USEquityPricing.volume]
    def compute(self, today, assets, out, close_price, volume):
        dollar_volume = close_price * volume
        avg_dollar_volume = np.mean(dollar_volume, axis=0)
        out[:] = avg_dollar_volume

# The initialize function is the place to set your tradable universe and define any parameters. 
def initialize(context):
    # set_slippage(slippage.VolumeShareSlippage(volume_limit=1.0, price_impact=0.0))
    
    # UNIVERSE ###################################
    # context.random_universe = False
    context.random_universe = True
    
    if context.random_universe == False:
        set_symbol_lookup_date('2016-01-01')
    
        context.universe = symbols(
            # 'EDV',
            # 'VTI',
            # 'VNQ',
            # 'VNQI',
            # 'VPL',
            # 'VGK',
            # 'VWO',
            # 'VDE',
            # 'BIV',
            
            'VTI', # USA
            'IYR', # REIT
            'EZU', # Europe
            'EWJ', # Japan
            'EEM', # Emerging
            'IYE', # US Energy
            'TLT', # 20+ Treasury 
            'AGG', # Aggregate bonds
            'GLD',
        )
        context.universe_yield = dict.fromkeys(context.universe,1.00)
        custom_yield = {
            # 'EDV' :1.025,
            # 'VTI' :1.02,
            # 'VNQ' :1.025,
            # 'VNQI':1.025,
            # 'VPL' :1.02,
            # 'VGK' :1.02,
            # 'VWO' :1.02,
            # 'VDE' :1.02,
            # 'BIV' :1.02,
            
            'VTI':1.02,
            'IYR':1.03,
            'EZU':1.02,
            'EWJ':1.01,
            'EEM':1.02,
            'IYE':1.03,
            'TLT':1.02,
            'AGG':1.02,
        }
        
        for sec in context.universe:
            if sec.symbol in custom_yield:
                context.universe_yield[sec] = custom_yield[sec.symbol]
        
    # OPTIONS ###################################
    context.securities = []
    context.benchmark = symbol('SPY')
    context.riskless = symbol('SHY')
    context.riskless_apr = 1.005 # Assumed rate of riskless investment
    # context.riskless = symbol('BSV')
    # context.riskless_apr = 1.01 # Assumed rate of riskless investment
    context.ignore = [ # Stocks to ignore when picking
        context.riskless,   
        context.benchmark,
    ]
    context.max_drawdown = 1.0 # Portion of portfolio to risk
    context.risk_leverage = 1.0 # Portion leverage = max_drawdown * risk_leverage
    context.curr_risk = context.portfolio.portfolio_value*context.max_drawdown
    context.max_risk = context.curr_risk
    context.port = context.portfolio.portfolio_value
    context.recalculate_period_days = 5
    context.trade_period_days = 1
    context.lookback = 21*9 # Number of previous days to sample
    context.minute_mode_only = True
    # context.minute_mode_only = False
    context.hold_cash = False # Use or don't use excess cash to invest in riskless
    # context.hold_cash = True # Use or don't use excess cash to invest in riskless
    context.shortable = False
    # context.shortable = True
    
    context.hedge = True
    # context.hedge = False
    context.hedge_max_beta = 0.00 # Max absolute beta at risky times
    context.hedge_min_bench_kelly = 0
    
    context._day_count = -1   
    context._first_traded = False
    
    context.borrowing_rate = 1.02
    context.accrued_interest = 0
    
    context.pick_period = context.recalculate_period_days
    context.pick_size = 10
    context.universe_size = 500
    context.max_variance = 9.99
    
    context.alpha = 1.0
    context.drawdown_trigger = 0.10
    context.trigger_fraction = 1.00
    
    context.epsilon = 1.00
    context.max_pct_liquidity = 0.10
    # END #######################################
    context.securities = []
    context.leverage = dict.fromkeys(context.securities,0)
    
    if context.random_universe == True:
        pipe = Pipeline()
        attach_pipeline(pipe, name='my_pipeline')
        mean = Mean(window_length = context.lookback)
        var = Variance(window_length = context.lookback)
        f = context.max_drawdown*context.risk_leverage
        growth = mean*f - (f**2.0 * var)/2.0
        dollar_volume = DollarVolume(window_length = context.lookback)
        pipe.add(var,'var')
        pipe.add(growth,'growth')
        pipe.set_screen((var <= context.max_variance/252.0) & dollar_volume.top(context.universe_size))
    
    schedule_function(repick, 
        date_rules.every_day(),
        time_rules.market_open(hours=0,minutes=1))
    
    schedule_function(recalculate, 
        date_rules.every_day(),
        time_rules.market_open(hours = 0, minutes = 5))
        
    schedule_function(logging,
        date_rules.month_start(days_offset=0),
        # date_rules.week_start(days_offset=0),
        time_rules.market_close(hours = 0, minutes = 5))
        
    schedule_function(day_end,
        date_rules.every_day(),
        time_rules.market_close(hours = 0, minutes = 1))
        
def simulate_borrowing(context,data):
    shorts = 0
    longs = 0
    cash = context.portfolio.cash
    for sec,v in context.portfolio.positions.iteritems():
        delisted = 0
        if sec.end_date < get_datetime():
            delisted += np.abs(v.amount*v.last_sale_price)
            
        shares = v.amount
        if shares < 0:
            shorts += np.abs(shares)*v.last_sale_price - delisted
        else:
            longs += shares*v.last_sale_price - delisted
            
    daily_interest = (context.borrowing_rate-1.0)/252.0
    borrowed = shorts + max(0-(cash-1.5*shorts),0) # 1.5 = 150% short requirement
    context.accrued_interest += (borrowed + context.accrued_interest) * daily_interest
    
def before_trading_start(context,data):
    if not context._first_traded:
        context._day_count = -1

    context._day_count += 1
    
    simulate_borrowing(context,data)

    if context.random_universe and context._day_count % context.pick_period == 0:
        context.universe = pipeline_output('my_pipeline')
        context.universe = context.universe[~context.universe.index.isin(context.ignore)]
        context.universe = context.universe[~context.universe.index.isin(security_lists.leveraged_etf_list)]
        context.universe = context.universe.sort('growth', ascending=False).head(context.pick_size)
        update_universe(context.universe.index)
            
def repick(context,data):
    prices = history(bar_count=context.lookback, frequency='1d', field='price').dropna(axis=1)
    
    force_repick = False
    
    for sec in context.securities[:]:
        if sec not in prices:
            force_repick = True
            break

    if force_repick or context._day_count % context.pick_period == 0:
        
        picked = None
        if context.random_universe == True:
            picked = context.universe
        else:
            r = prices.pct_change().dropna()
            mean = r.mean()
            var = r.var()
            var = var[var <= context.max_variance/252.0]
            mean = mean[var.index]
            
            # growth rate with assumed bet size = 1.0 and risk free r = 0
            # g = r + f(m-r) - s^2*f^2/2
            f = context.max_drawdown*context.risk_leverage
            g = mean*f - (var*f**2.0)/2
            g = g[g.index.isin(context.universe)]
            g.sort()
            picked = g.tail(context.pick_size)
        
        
        count = 0
        context.securities = []
        for security in picked.index:
            count += 1
            if count > context.pick_size: break
            context.securities.append(security)
            
        context.leverage = dict.fromkeys(context.securities,0)
        
def optimize(x,*args):
    corr = args[0]
    return np.dot(np.dot(x.T,corr),x)
    
def jacobian(x,*args):
    corr = args[0]
    return 2*np.dot(x.T,corr)
            
def recalculate(context,data):
    if context._day_count % context.recalculate_period_days != 0: return
    
    prices = history(bar_count=context.lookback, frequency='1d', field='price').dropna(axis=1)
    v = history(bar_count=context.lookback, frequency='1d', field='volume').dropna(axis=1)
    dvol = (v * prices).mean()
    r = prices.pct_change().dropna()
    r_all = r
    k = None
    
    if context.hold_cash:
        riskless_r = 0
    else:
        riskless_r = np.exp(np.log(context.riskless_apr)/252.0)-1.0
    
    # Long
    while True:
        securities = context.securities
    
        r = r[r.columns[r.columns.isin(securities)]]
        securities = r.columns
        
        cov = r.cov().values
        mean = r.mean().values
        
        if not context.random_universe:
            yld = np.array([context.universe_yield[sec] for sec in securities])
            yld = np.exp(np.log(yld)/252.0)-1.0
            mean += yld
            
        try:
            cov_inv = np.linalg.inv(cov)
        except:
            cov_inv = 1.0/cov
        
        k = np.dot(cov_inv,mean-riskless_r)
        k = np.nan_to_num(k)
        k = pandas.Series(k, index=securities)
        # if True in (k < 0).values and k.size > 2:
        if True in (k < 0).values:
            k = k[k >= 0]
            context.securities = k.index
            context.leverage = dict.fromkeys(context.securities,0)
        else:        
            if not context.shortable:
                k[k < 0] = 0    
            
            # Maximize diversification
            if k.abs().sum() > context.risk_leverage:
                
                corr = r.corr().values
                                
                min_bnd = 0
                max_bnd = 1.0
                
                bounds = tuple((min_bnd,max_bnd) for x in securities)
                constr = (
                    {'type': 'eq', 'fun': lambda x:  1.0-np.sum(x)},
                )
                x0 = k.values
                x0 = x0*0 + 1.0/k.size
                
                result = minimize(optimize, x0, jac=jacobian, args=(corr), constraints=constr, bounds=bounds)
            
                x0 = result.x
                
                k = pandas.Series(x0,index=k.index)
                volatility = r.std()
                k = k/volatility
                k = k / k.sum() * context.risk_leverage
                
            break    
            
    # Hedge against systematic risk
    securities = np.concatenate((k.index,[context.benchmark]))
    k[context.benchmark] = 0
    r = r_all
    r = r[r.columns[r.columns.isin(securities)]]
    cov = r.cov()
    mean = r.mean()
    var = r.var()
    beta = cov[context.benchmark] / var[context.benchmark]
    beta = beta[~beta.index.isin([context.benchmark])]
    port_beta = (beta*k).sum()
    # port_var = np.dot(k.values,np.dot(cov,k.values))
    # port_mean = (k * mean).sum()
    
    kbench = (mean[context.benchmark]-riskless_r) / var[context.benchmark]
    # kmean = context.risk_leverage*mean[context.benchmark]
    # kvar = context.risk_leverage**2.0 * var[context.benchmark]
        
    if context.hedge and kbench < context.hedge_min_bench_kelly and port_beta != 0:
        if context.shortable:
            k[context.benchmark] = -port_beta
        else:
            k = k * min(context.hedge_max_beta/np.abs(port_beta),1)
            
    if k.abs().sum() > context.risk_leverage:
        k = k / k.abs().sum() * context.risk_leverage
        k = k.fillna(value=0)
    
    # Trading volume control
    dvol = dvol[securities]
    
    dollars = context.portfolio.portfolio_value*k
    d_pct = (dollars / (dvol*max(context.recalculate_period_days/context.trade_period_days,1))).abs()
    context.epsilon = (context.max_pct_liquidity/d_pct).min()
    context.epsilon = min(1.0, context.epsilon)
        
    # context.securities = k.index
    context.securities = securities
    context.leverage = dict.fromkeys(context.securities,0)
    
    # QUEUE ORDERS #########################
    orderHandler.clearQueue()
    my_secs = np.concatenate((context.securities,context.ignore),axis=0)
    
    for sec in context.portfolio.positions:
        if sec.end_date < get_datetime():
            if sec not in context.ignore:
                context.ignore.append(sec)
        elif (sec not in my_secs) and sec in data:
            orderHandler.push(sec,0)
    
    for i,sec in enumerate(context.securities):
        context.leverage[sec] = k[sec]
        orderHandler.push(sec,k[sec])
        
    orderHandler.sort(context,data)
        
# The handle_data function is run every bar.    
def handle_data(context,data):
    risk_record(context,data)
    
    # UPDATE RISK SIZE ##########################
    
    new_port = context.portfolio.portfolio_value*context.max_drawdown
    diff = context.portfolio.portfolio_value - context.port
    if diff > 0:
        context.curr_risk = new_port
        context.max_risk = new_port
        context.port = context.portfolio.portfolio_value
    else:
        context.curr_risk = context.max_risk + diff
        context.curr_risk = max(0,context.curr_risk)
        
    # UPDATE RISK CONTROL ######################
    ceiling = max(context.portfolio.portfolio_value, context.max_risk)
    alpha = (ceiling - context.curr_risk)/ceiling
    alpha = math.floor( alpha/context.drawdown_trigger )
    alpha = context.trigger_fraction**alpha
    context.alpha = alpha
    
    context.curr_risk = min(context.curr_risk*context.alpha,context.epsilon*context.portfolio.portfolio_value)
    
    orderHandler.run(context,data)

def day_end(context,data):
    long = 0
    short = 0
    total = 0

    if context.minute_mode_only:
        for sec,orders in get_open_orders().iteritems():
            for order in orders:
                cancel_order(order)
            
    for security in context.portfolio.positions:
        asset = context.portfolio.positions[security].amount * context.portfolio.positions[security].last_sale_price
        if asset > 0:
            long += asset
        else:
            short += np.abs(asset)
            
    long = long/context.portfolio.portfolio_value
    short = short/context.portfolio.portfolio_value
    total += long + short
    
    record(long = long)
    record(short = short)
    
def logging(context,data):
    log.info('---')
    log.info('Risk size: {}'.format(context.curr_risk))
    log.info('Max risk: {}'.format(context.max_risk))
    log.info('Portfolio value: {}'.format(context.portfolio.portfolio_value))
    log.info('Accrued interest: {}'.format(context.accrued_interest))
    
    pos_sum = 0
    delisted_sum = 0
    positions = 0
    delisted = 0
    
    for sec,v in context.portfolio.positions.iteritems():
        if sec in context.securities:
            pos_sum += np.abs(v.amount*v.last_sale_price)
            positions += 1
        elif sec.end_date < get_datetime():
            delisted_sum += np.abs(v.amount*v.last_sale_price)
            delisted += 1
            
    log.info('Real lev: {}'.format(pos_sum/context.portfolio.portfolio_value))
    log.info('Delisted lev: {}'.format(delisted_sum/context.portfolio.portfolio_value))
    log.info('Positions: {}'.format(positions))
    log.info('Delisted: {}'.format(delisted))
    
def risk_record(context,data):
    if not '_init_rpr' in context:
        context._init_rpr = True
        context._max_shorts = 0
        context._min_cash = context.portfolio.cash
        context._peak_leverage = 0
        
    shorts = 0
    cash = context.portfolio.cash
    for sec,v in context.portfolio.positions.iteritems():
        delisted = 0
        if sec.end_date < get_datetime():
            delisted += np.abs(v.amount*v.last_sale_price)
            
        shares = v.amount
        if shares < 0:
            shorts += np.abs(shares*v.last_sale_price) - delisted
    
    context._max_shorts = max(context._max_shorts,shorts)
    context._min_cash = min(context._min_cash,cash)
    context._peak_leverage = max(context._peak_leverage,context.account.leverage)
    
    record(min_cash = context._min_cash)
    record(max_shorts = context._max_shorts)
    record(peak_leverage = context._peak_leverage)
There was a runtime error.
2 responses

Here's the tearsheet.

Loading notebook preview...

how do you add the Kelly Criterion in the code?