Back to Community
SPY, SH & TLT w/ constrained optimizer

Here's something I sorta stumbled upon by tinkering. Perhaps it is of interest to others. I'll post updates here, if I make improvements. Seems like an algo that would work with Robinhood, since it is long-only and trades weekly. I tagged it "market neutral" since the beta is -0.11. --Grant

Clone Algorithm
607
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
import numpy as np
from pytz import timezone
import scipy
import pandas as pd

def initialize(context):
    
    context.stocks = [ sid(8554),   # SPY
                       sid(32268),  # SH
                       sid(23921) ] # TLT
    
    context.x0 = np.zeros_like(context.stocks)
    
    schedule_function(trade,date_rules.week_start(days_offset=1),time_rules.market_open(minutes=30))
    # schedule_function(trade,date_rules.week_end(),time_rules.market_open(minutes=30))
    # schedule_function(close,date_rules.every_day(),time_rules.market_close(minutes=30))
    
    set_commission(commission.PerTrade(cost=0))
    
    set_long_only()
    
    context.eps = 0.01
    
def handle_data(context,data):
    
    leverage = context.account.leverage
    
    record(leverage = leverage)
    
    if leverage >= 3.0:
        print "Leverage >= 3.0"

def trade(context, data):
    
    prices = history(20*390,'1m', 'price')
    ret = prices.pct_change()[1:].as_matrix(context.stocks)
    ret_sum = np.sum(ret,axis=0)
        
    bnds = []
    limits = [0,1]
    
    for stock in context.stocks:
        bnds.append(limits)
        
    bnds = tuple(tuple(x) for x in bnds)

    cons = ({'type': 'eq', 'fun': lambda x:  np.sum(x)-1.0},
            {'type': 'ineq', 'fun': lambda x:  np.dot(x,ret_sum) - context.eps})
    
    res= scipy.optimize.minimize(variance, context.x0, args=ret,method='SLSQP',constraints=cons,bounds=bnds)
    
    if res.success:
        allocation = res.x
        allocation[allocation<0]=0
        denom = np.sum(allocation)
        if denom != 0:
            allocation = allocation/denom
    else:
        allocation = [0,0,1]
           
    if get_open_orders():
           return
    
    for i,stock in enumerate(context.stocks):
        order_target_percent(stock,allocation[i])
        
    record(cons = np.dot(allocation,ret_sum))
    print np.dot(allocation,ret_sum)
        
    # record(pct_sso = allocation[0])
    # record(pct_sds = allocation[1])
    # record(pct_tlt = allocation[2])
        
def close(context, data):
    
    for i,stock in enumerate(context.stocks):
        order_target_percent(stock,0)
        
def variance(x,*args):
    
    p = np.squeeze(np.asarray(args))
    Acov = np.cov(p.T)
    
    return np.dot(x,np.dot(Acov,x))
We have migrated this algorithm to work with a new version of the Quantopian API. The code is different than the original version, but the investment rationale of the algorithm has not changed. We've put everything you need to know here on one page.
There was a runtime error.
16 responses

Here's an incremental improvement. Leverage fixed at 1.0 (no more dips to 0).

Clone Algorithm
607
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
import numpy as np
from pytz import timezone
import scipy
import pandas as pd

def initialize(context):
    
    context.stocks = [ sid(8554),   # SPY
                       sid(32268),  # SH
                       sid(23921) ] # TLT
    
    context.x0 = np.ones_like(context.stocks)
    
    schedule_function(trade,date_rules.week_start(days_offset=1),time_rules.market_open(minutes=30))
    
    set_commission(commission.PerTrade(cost=0))
    
    set_long_only()
    
    context.eps = 0.01
    
def handle_data(context,data):
    
    leverage = context.account.leverage
    
    record(leverage = leverage)
    
    if leverage >= 3.0:
        print "Leverage >= 3.0"

def trade(context, data):
    
    prices = history(20*390,'1m', 'price')
    ret = prices.pct_change()[1:].as_matrix(context.stocks)
    ret_sum = np.sum(ret,axis=0)
        
    bnds = []
    limits = [0,1]
    
    for stock in context.stocks:
        bnds.append(limits)
        
    bnds = tuple(tuple(x) for x in bnds)

    cons = ({'type': 'eq', 'fun': lambda x:  np.sum(x)-1.0},
            {'type': 'ineq', 'fun': lambda x:  np.dot(x,ret_sum) - context.eps})
    
    res= scipy.optimize.minimize(variance, context.x0, args=ret,method='SLSQP',constraints=cons,bounds=bnds)
    
    if res.success:
        allocation = res.x
        allocation[allocation<0]=0
        denom = np.sum(allocation)
        if denom != 0:
            allocation = allocation/denom
    else:
        allocation = np.array([0,0,1.0])
           
    if get_open_orders():
           return
    
    for i,stock in enumerate(context.stocks):
        order_target_percent(stock,allocation[i])
        
    record(cons = np.dot(allocation,ret_sum))
        
def close(context, data):
    
    for i,stock in enumerate(context.stocks):
        order_target_percent(stock,0)
        
def variance(x,*args):
    
    p = np.squeeze(np.asarray(args))
    Acov = np.cov(p.T)
    
    return np.dot(x,np.dot(Acov,x))
We have migrated this algorithm to work with a new version of the Quantopian API. The code is different than the original version, but the investment rationale of the algorithm has not changed. We've put everything you need to know here on one page.
There was a runtime error.

I ran it for past 2 months (since they have been not too good for regular traders) with a 1k capital and it doesn't do that well. I noticed how most algorithms require a very large capital base for good performance. That defeats the purpose of robinhood+Quantopian IMHO. Also, it might be useful if you could comment your code for didactic purposes.

In the real world, my belief has grown over the last 15 years that you cast your bread widely over the waters, limit risk and accept the returns the market provides. Negative or positive. Without looking at the details in depth, this seems to be a mean variance optimisation system. Go read Markowitz as an initial primer.

ETFs do NOT require " a large capital base for good performance".

Personally I don't like short ETFs or shorting but there you go.

Otherwise, in my view these sort of systems are along the right lines. I query the portfolio choice. In a recent article I have written for the Investors Chronicle I point out the futility of prediction. And who knows where the US is heading? Not I for sure. So why bet your entire investment future on the US stock and bond market.

Would you have been right in betting 100% on Germany in 1920? Japan in 1980? Russia in 1900?

Sam $.,

You might run the algo as a backtest at varying levels of capital to see the effect. It's a pretty simple algo, so just let me know if you have specific questions. The overall idea is to minimize the variance in minutely returns subject to a constraint that should apply more weight to better-performing ETFs in the portfolio.

Also, when you say "it doesn't do that well" you might consider if the performance was degraded due to the live-trading environment, by comparing it to a backtest over the same period. Also, the performance may well have been within the bounds of what one would expect over such a short time frame. You could try the "tear sheet" analysis tool provided by Quantopian to sort this out.

Grant

Hi Anthony,

Very good points. If it is so hard to predict the future and beat the market, I wonder what's the point of sophisticated statistical algorithms. Let's just put all the money into SPY. Again, I am coming from the perspective of a small investor; maybe for someone with a 100mil net worth, a 5% improvement in returns is worth it (even with a 2% broker fee, and 50% tax). Anyhow, I am sure that there are people who rake in the $$$ using algorithms. Probably the money-making code is 100x longer than the simple examples I have noticed on Quantopian, and perhaps is a basket of algos that are switched dynamically based on market conditions. What are your thoughts on the larger quant industry?

Sam

If there are 5% returns achievable, and you shoot for 50% returns, you will likely end up with 0% returns, or worse. Undercapitalized investors taking too much risk on long-shot bets are (likely) the patsies that provide the excess returns of several well-known anomalies that professionals exploit.

Whether we here are patsies or pros is the million dollar question.

Fair point, Simon. Capital is the key, it seems.

Well, being undercapitalized is a problem in and of itself, in that commissions are relatively more punitive, but the greater problem is over-reaching in terms of percentage returns in order to hit some ad hoc target in terms of absolute returns.

If you have, say, $10k, but are still only shooting for reasonable returns given commission overhead, then I don't think that is a problem. $1000 seems too little to do anything, really, and I would think $50k would be a reasonable minimum if you are trading more than once every few days.

The real problem is when you say, "I need to make $1000 a month, but I only have $10k in capital", and then you start trading FX or futures to get the leverage necessary to have even a shot of those returns, but instead you completely wipe out.

(all in my humble opinion)

I agree on all the points. My question, however, is whether statistical techniques can perform better than curated 'value investing' (see rubicoin's Invest app). In my experience, value investing seems to have an edge over the market, even with limited capital (or capital accumulation in small but regular amounts). Given quantopian's slogan "Leveling Wall Street's Playing Field", and their partnership with Robinhood (another enterprise geared towards the 'little guy'), I wonder if the two combined can upend the traditional wisdom on investing championed by Peter Lynch and Warren Buffett. Note that I am not a finance professional and have no proverbial dog in this fight.

I agree wholeheartedly with Simon. I have seen time and again people (understandably) wanting to shoot the lights out. Usually using leverage. The desire is entirely understandable and is fostered by those who make their living selling dreams. This includes hedge fund managers - a class which includes some extremely disreputable characters. Unfortunately, high returns inevitably lead to high draw downs and even if you don't lose the whole value of your account (or more) you may give up out of sheer fear. I know I have done that before now.

Those who have recorded huge returns for prolonged periods of time without undue mishap will almost inevitably meet their "black swan" in due course. It has been amusing these past twenty years to watch the rise (and now quite possibly the fall) of hedge funds and speculative trading desks. It may well all be about survivor-ship bias rather than skill.

Or inside trading or trading with some sort of house advantage.

I am now deeply cynical. I believe that one can modestly beat the market (certainly in risk adjusted terms, possibly in absolute terms) but you need to consider your definition of "market". I don't consider any single index as the "market". a single index is almost inevitably too narrowly based if by "market" one means the general price of enterprise in an economy.

No, I don't believe the money making code for fund managers is particularly complex. The money making code for the likes of the HFT industry no doubt is, but that is not investment. It seems to be a type of market making based on inside advantage and knowledge and you certainly won't make that sort of money by mere investment nor indeed leveraged "probability" trading.

Look at JW Henry, Bill Dunn and the many other in the US who eventually crashed and burnt after many successful high return years. The assumption is that they met their black swan.

Excuse the cynicism but finance is largely a bullshit industry populated by ....odd characters

I don't think there's any question or conflict here, to me, Quantopian and Robinhood are each orthogonal to the existence of the value premium and various statistical edges. They have made it easier and lowered the bar to systematically exploit such edges without paying additional fees to intermediaries, but they I don't think they have any impact on whether or not certain edges exist. Perhaps they will accelerate the submersion of such anomalies into the sea of EMH, but only if they attract substantial new capital to those strategies. I would probably argue that most of the people putting capital into Quantopian would otherwise have put it into the same strategies a different way; in my case, by using my own backtesting and execution code, others, perhaps through active ETFs, RIA SMAs or hedge funds, in which case the net impact of Quantopian and Robinhood on the market is merely to reduce overhead/duplicative effort and fees/rents.

As to "levelling wall street" you have no need to create a wizard algorithm to do that. This algorithm right here would do very nicely and Quantopian is a hell of a place investigate these sort of algos because of the fantastic libraries available, vectorisation and so on.

What you really need to do to "level Wall Street" is to put huge energy into building a track record and raising AUM. The latter being a most unenviable task. There is a good market out there for steady returns at lower draw-down and volatility than conventional stock indices. But have you got the raw energy to go and chase it?

I reckon in the long term most high octane traders blow up. Not so the dull fund managers giving their investors 8% a year at low vol and DD. You only have to look at some of the vast asset allocation funds out there. Huge AUM and unspectacular "safe" returns are a better bet than gunslinging if you want to become very rich.

And back testing is a terribly dangerous tool. So many are fooled by it. Including myself. Witness that other thread where an apparently successful test run of the Russell 3000 on daily data faltered because of illiquidity and lousy prices on minute data.

Thanks for the great insights and candor, Anthony. I suspect that a lot of robo-advisers (e.g. Wealthfront) are basically doing what you suggested in your post. In fact, I had a chance to attend a talk by one of the guys from Betterment a couple of years ago. He was candid that they are not really trying to outwit the market, rather their focus is on inculcating good investment habits with the help of visualizations.

I seem to be getting decent results with Logical Invest's UIS. I agree with your backtesting analysis as well; it seems that in addition to Capital, the start date of backtesting has a significant influence on the outcome. The element of luck is almost impossible to ignore.

Said Anthony G: "And back testing is a terribly dangerous tool. So many are fooled by it".

The second algo appears to have a consistent leverage of 1 and return of 48%, both wrong.
It hits an intraday leverage of 1.74 and returns are really only 27% profit vs amount put into play.

Clone Algorithm
11
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
import numpy as np
from pytz import timezone
import scipy
import pandas as pd

def initialize(context):
    context.stocks = [ sid(8554),   # SPY
                       sid(32268),  # SH
                       sid(23921) ] # TLT
    
    context.x0 = np.ones_like(context.stocks)
    
    schedule_function(trade,date_rules.week_start(days_offset=1),time_rules.market_open(minutes=30))
    
    set_commission(commission.PerTrade(cost=0))
    
    set_long_only()
    
    context.eps = 0.01
    
def handle_data(context, data):
    pvr(context, data)
    return

    leverage = context.account.leverage
    
    record(leverage = leverage)
    
    if leverage >= 3.0:
        print "Leverage >= 3.0"

def trade(context, data):
    prices = history(20*390,'1m', 'price')
    ret = prices.pct_change()[1:].as_matrix(context.stocks)
    ret_sum = np.sum(ret,axis=0)
        
    bnds = []
    limits = [0,1]
    
    for stock in context.stocks:
        bnds.append(limits)
        
    bnds = tuple(tuple(x) for x in bnds)

    cons = ({'type': 'eq', 'fun': lambda x:  np.sum(x)-1.0},
            {'type': 'ineq', 'fun': lambda x:  np.dot(x,ret_sum) - context.eps})
    
    res= scipy.optimize.minimize(variance, context.x0, args=ret,method='SLSQP',constraints=cons,bounds=bnds)
    
    if res.success:
        allocation = res.x
        allocation[allocation<0]=0
        denom = np.sum(allocation)
        if denom != 0:
            allocation = allocation/denom
    else:
        allocation = np.array([0,0,1.0])
           
    if get_open_orders():
           return
    
    for i,stock in enumerate(context.stocks):
        order_target_percent(stock,allocation[i])
        
    #record(cons = np.dot(allocation,ret_sum))
        
def close(context, data):
    for i,stock in enumerate(context.stocks):
        order_target_percent(stock,0)
        
def variance(x,*args):
    p = np.squeeze(np.asarray(args))
    Acov = np.cov(p.T)
    
    return np.dot(x,np.dot(Acov,x))

def pvr(context, data):
    ''' Custom chart and/or log of profit_vs_risk returns and related information
    '''
    # # # # # # # # # #  Options  # # # # # # # # # #
    record_max_lvrg = 1         # Maximum leverage encountered
    record_leverage = 0         # Leverage (context.account.leverage)
    record_q_return = 1         # Quantopian returns (percentage)
    record_pvr      = 1         # Profit vs Risk returns (percentage)
    record_pnl      = 0         # Profit-n-Loss
    record_shorting = 0         # Total value of any shorts
    record_overshrt = 0         # Shorts beyond longs+cash
    record_risk     = 0         # Risked, max cash spent or shorts beyond longs+cash
    record_risk_hi  = 1         # Highest risk overall
    record_cash     = 0         # Cash available
    record_cash_low = 1         # Any new lowest cash level
    logging         = 1         # Also to logging window conditionally (1) or not (0)
    log_method      = 'risk_hi' # 'daily' or 'risk_hi'

    from pytz import timezone   # Python will only do once, makes this portable.
                                #   Move to top of algo for better efficiency.
    c = context  # Brevity is the soul of wit -- Shakespeare [for efficiency, readability]
    if 'pvr' not in c:
        date_strt = get_environment('start').date()
        date_end  = get_environment('end').date()
        cash_low  = c.portfolio.starting_cash
        mode      = get_environment('data_frequency')
        c.pvr = {
            'max_lvrg': 0,
            'risk_hi' : 0,
            'date_prv': '',
            'cash_low': cash_low,
            'date_end': date_end,
            'mode'    : mode,
            'run_str' : '{} to {}  {}  {}'.format(date_strt,date_end,int(cash_low),mode)
        }
        log.info(c.pvr['run_str'])
    pvr_rtrn     = 0            # Profit vs Risk returns based on maximum spent
    profit_loss  = 0            # Profit-n-loss
    shorts       = 0            # Shorts value
    longs        = 0            # Longs  value
    overshorts   = 0            # Shorts value beyond longs plus cash
    new_risk_hi  = 0
    new_cash_low = 0                           # To trigger logging in cash_low case
    lvrg         = c.account.leverage          # Standard leverage, in-house
    date         = get_datetime().date()       # To trigger logging in daily case
    cash         = c.portfolio.cash
    start        = c.portfolio.starting_cash
    cash_dip     = int(max(0, start - cash))
    q_rtrn       = 100 * (c.portfolio.portfolio_value - start) / start

    if int(cash) < c.pvr['cash_low']:                # New cash low
        new_cash_low = 1
        c.pvr['cash_low']   = int(cash)
        if record_cash_low:
            record(CashLow = int(c.pvr['cash_low'])) # Lowest cash level hit

    if record_max_lvrg:
        if c.account.leverage > c.pvr['max_lvrg']:
            c.pvr['max_lvrg'] = c.account.leverage
            record(MaxLv = c.pvr['max_lvrg'])        # Maximum leverage

    if record_pnl:
        profit_loss = c.portfolio.pnl
        record(PnL = profit_loss)                    # "Profit and Loss" in dollars

    for p in c.portfolio.positions:
        shrs = c.portfolio.positions[p].amount
        if shrs < 0:
            shorts += int(abs(shrs * data[p].price))
        if shrs > 0:
            longs  += int(shrs * data[p].price)

    if shorts > longs + cash: overshorts = shorts             # Shorts when too high
    if record_shorting: record(Shorts = shorts)               # Shorts value as a positve
    if record_overshrt: record(OvrShrt = overshorts)          # Shorts value as a positve
    if record_cash:     record(Cash = int(c.portfolio.cash))  # Cash
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage

    risk = int(max(cash_dip, shorts, overshorts))
    if record_risk: record(Risk = risk)       # Amount in play, maximum of shorts or cash used

    if risk > c.pvr['risk_hi']:
        c.pvr['risk_hi'] = risk
        new_risk_hi = 1

        if record_risk_hi:
            record(RiskHi = c.pvr['risk_hi']) # Highest risk overall

    if record_pvr:      # Profit_vs_Risk returns based on max amount actually spent (risk high)
        if c.pvr['risk_hi'] != 0:     # Avoid zero-divide
            pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.pvr['risk_hi']
            record(PvR = pvr_rtrn)            # Profit_vs_Risk returns

    if record_q_return:
        record(QRet = q_rtrn)                 # Quantopian returns to compare to pvr returns curve

    def _minute():   # To preface each line with the minute of the day.
        if get_environment('data_frequency') == 'minute':
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)
            return str(minute).rjust(3)
        return ''    # Daily mode, just leave it out.

    if logging:
        if log_method == 'risk_hi' and new_risk_hi \
          or log_method == 'daily' and c.date_prv != date \
          or new_cash_low:
            qret    = ' QRet '   + '%.1f' % q_rtrn
            lv      = ' Lv '     + '%.1f' % lvrg              if record_leverage else ''
            pvr     = ' PvR '    + '%.1f' % pvr_rtrn          if record_pvr      else ''
            pnl     = ' PnL '    + '%.0f' % profit_loss       if record_pnl      else ''
            csh     = ' Cash '   + '%.0f' % cash              if record_cash     else ''
            shrt    = ' Shrt '   + '%.0f' % shorts            if record_shorting else ''
            ovrshrt = ' Shrt '   + '%.0f' % overshorts        if record_overshrt else ''
            risk    = ' Risk '   + '%.0f' % risk              if record_risk     else ''
            mxlv    = ' MaxLv '  + '%.1f' % c.pvr['max_lvrg'] if record_max_lvrg else ''
            csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low'] if record_cash_low else ''
            rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']  if record_risk_hi  else ''
            log.info('{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minute(),
               lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, ovrshrt, risk, rsk_hi))
    c.date_prv = date
    if c.pvr['date_end'] == date:
        # Summary on last minute of last day.
        # If using schedule_function(), backtest last day/time may need to match for this to execute.
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0
        log_summary = 0
        if c.pvr['mode'] == 'daily' and get_datetime().date() == c.pvr['date_end']:
            log_summary = 1
        elif c.pvr['mode'] == 'minute' and get_datetime() == get_environment('end'):
            log_summary = 1
        if log_summary and not c.pvr_summary_done:
            log.info('PvR {} ... {}'.format('%.1f' % pvr_rtrn, c.pvr['run_str']))
            log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format(
                '%.0f' % (c.portfolio.portfolio_value - start), '%.0f' % c.pvr['risk_hi'], '%.1f' % pvr_rtrn))
            log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {}'.format(
                '%.2f' % q_rtrn, '%.2f' % pvr_rtrn, '%.2f' % c.pvr['cash_low'],
                '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi']))
            c.pvr_summary_done = 1

There was a runtime error.