Back to Community
"volatility strategies" - what are they?

I've seen lots of posts on so-called "volatility strategies" - what are they? Why are they so popular? What is the underlying principle/economic condition that makes them profitable?

Perhaps someone can shed some high-level light on this thing? The various threads are bogged down in various tweaks to this-or-that particular strategy. I'm interested in a more elevated view.

Thanks,

Grant

15 responses

Thanks Tim - looks to be quite an extensive paper! --Grant

Hi Grant,

Here's a very simple volatility trading algo that I cobbled together. It's based on the fact that XIV and the market are highly correlated.

If you decide to delve into this sort of strategies, I would appreciate receiving your comments and suggestions for improvement.

Regards,

Tim

Clone Algorithm
8
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 pandas as pd
import numpy as np


def initialize(context):    
    
    set_asset_restrictions(security_lists.restrict_leveraged_etfs)
    
    context.m = sid(24744) # RSP
    context.x = sid(40516) # XIV
    context.b = sid(22887) # EDV
    
    context.secs = [context.m, context.x, context.b]
    
    context.start = True
    
    schedule_function(func = rebalance,
                      date_rule = date_rules.week_start(),
                      time_rule = time_rules.market_close(minutes = 20),
                      half_days = True)
    
    schedule_function(buy,
        date_rule=date_rules.every_day(),
        time_rule=time_rules.market_open(minutes = 30))
    
    schedule_function(display,
        date_rule=date_rules.every_day(),
        time_rule=time_rules.market_close())    
    
    
def rebalance(context, data):
    
    P = data.history(context.secs,'price', 252, '1d')
    
    x = P.tail(10).median() / P.tail(100).median() - 1
    y = P.tail(2).median() / P.tail(200).median() - 1

    if (x[context.m] > 0) and (x[context.x] > 0) and (y[context.m] > 0):
        context.longs = [context.m, context.x]
    elif (x[context.m] > 0) and (y[context.m] > 0):
        context.longs = [context.m]
    else:
        context.longs = [context.b]


def buy(context,data):
    
    for s in context.portfolio.positions:
        if s in context.longs:
            continue
        if not data.can_trade(s):
            continue
        order_target(s, 0)    

    if get_open_orders():
        return  
    
    for s in context.longs:
        if not data.can_trade(s):
            continue
        order_target_percent(s, 1.0 / len(context.longs))
        
        
def display(context,data):                             
    
    record(leverage = context.account.leverage,
           exposure = context.account.net_leverage)
    
    
def handle_data(context, data):
    
    if context.start:
        rebalance(context, data)
        buy(context, data)
        display(context, data)
        context.start = False

There was a runtime error.

And here's a more docile version.

Clone Algorithm
12
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 pandas as pd
import numpy as np


def initialize(context):    
    
    set_asset_restrictions(security_lists.restrict_leveraged_etfs)
    
    context.m = sid(24744) # RSP
    context.x = sid(40516) # XIV
    context.b = sid(22887) # EDV
    
    context.secs = [context.m, context.x, context.b]
    
    context.start = True

    context.w = None
    
    
    schedule_function(func = rebalance,
                      date_rule = date_rules.week_start(),
                      time_rule = time_rules.market_close(minutes = 20),
                      half_days = True)
    
    schedule_function(buy,
        date_rule=date_rules.every_day(),
        time_rule=time_rules.market_open(minutes = 30))
    
    schedule_function(display,
        date_rule=date_rules.every_day(),
        time_rule=time_rules.market_close())    
    
    
def rebalance(context, data):
    
    P = data.history(context.secs,'price', 252, '1d')
    
    R = P.pct_change().dropna()
    s = R.std()
    
    x = P.tail(10).median() / P.tail(100).median() - 1
    y = P.tail(2).median() / P.tail(200).median() - 1

    if (x[context.m] > 0) and (x[context.x] > 0) and (y[context.m] > 0):
        longs = [context.m, context.x]
    elif (x[context.m] > 0) and (y[context.m] > 0):
        longs = [context.m]
    else:
        longs = [context.b]

    w = 1 / s
    w = w[longs]
    
    q = np.nan_to_num(w)
    w = pd.Series(q, index = longs)
    u = w.abs().sum()
    if u > 0: w  = w / u
    context.w = w
    
    
def buy(context,data):
    
    for s in context.portfolio.positions:
        if s in context.w.index:
            continue
        if not data.can_trade(s):
            continue
        order_target(s, 0)    

    if get_open_orders():
        return  
    
    for s in context.w.index:
        if not data.can_trade(s):
            continue
        order_target_percent(s, context.w[s])

        
def display(context,data):                             
    
    record(leverage = context.account.leverage,
           exposure = context.account.net_leverage)
    
    
def handle_data(context, data):
    
    if context.start:
        rebalance(context, data)
        buy(context, data)
        display(context, data)
        context.start = False

There was a runtime error.

Strange, now I notice that they have virtually exactly the same shape -- what's going on here? ...

Pardon my interruption from the volatility topic here, I just want you guys to be doing your best.
I don't know the answer to your question however run this an see if you figure out what happened in Feb 2014 with the sudden margin in your first algo. track_orders is set to begin 2014-02-21. Maybe RSP hit a point where everybody was selling?

2013-12-02 08:10 _pvr:122 INFO PvR 0.3963 %/day   cagr 0.6   Portfolio value 82284   PnL 62284  
2013-12-02 08:10 _pvr:123 INFO   Profited 62284 on 20787 activated/transacted for PvR of 299.6%  
2013-12-02 08:10 _pvr:124 INFO   QRet 311.42 PvR 299.63 CshLw -787 MxLv 1.01 RskHi 20787 MxShrt 0  
2014-02-25 07:00 _orders:276 INFO   30   Sell -571 RSP at 72.11   cash 12  
2014-02-25 07:00 _orders:276 INFO   30   Buy 1303 XIV at 31.62   cash 12  
2014-02-25 07:01 pvr:205 INFO  31 Lv 1.5 MxLv 1.47 QRet 311.9 PvR 106.7 PnL 62377 Cash -38468 CshLw -38468 Shrt 0 MxShrt 0 oShrt 0 Risk 58468 RskHi 58468  
2014-02-25 07:02 _orders:276 INFO   32      Bot 1303 XIV at 31.57   cash -37603  
2014-02-25 07:07 _orders:276 INFO   37      Sold -128/-571 RSP at 72.02   cash -31915  
2014-02-25 07:15 _orders:276 INFO   45      Sold -176/-571 RSP at 72.12   cash -19221  
2014-02-25 07:24 _orders:276 INFO   54      Sold -571 RSP at 72.28   cash 51  
2014-02-26 07:00 _orders:276 INFO   30   Buy 1 XIV at 31.65   cash 51  
2014-02-26 07:02 _orders:276 INFO   32      Bot 1 XIV at 31.85   cash 19  

Remember, margin is discarded in returns. The profit on amount invested plunged from 300% to 107% due to the negative cash. (PvR looks at profit on the amount actually invested no matter how much capital at the beginning). So a little bit later in the log you can see that it had profited 71,020 however it took 58,468 (38k invisible) to achieve that (sudden jump) for PvR of a mere 122% (Q return is 355% at that point). You can see the three times it happened (in a big way) in the custom chart here. One approach could be to schedule closing of positions before opens.

Clone Algorithm
4
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
'''
https://www.quantopian.com/posts/volatility-strategies-what-are-they
'''

import pandas as pd
import numpy as np


def initialize(context):    
    
    set_asset_restrictions(security_lists.restrict_leveraged_etfs)
    
    context.m = sid(24744) # RSP
    context.x = sid(40516) # XIV
    context.b = sid(22887) # EDV
    
    context.secs = [context.m, context.x, context.b]
    
    context.start = True
    
    schedule_function(func = rebalance,
                      date_rule = date_rules.week_start(),
                      time_rule = time_rules.market_close(minutes = 20),
                      half_days = True)
    
    schedule_function(buy,
        date_rule=date_rules.every_day(),
        time_rule=time_rules.market_open(minutes = 30))
    
    
def rebalance(context, data):
    
    P = data.history(context.secs,'price', 252, '1d')
    
    x = P.tail(10).median() / P.tail(100).median() - 1
    y = P.tail(2).median() / P.tail(200).median() - 1

    if (x[context.m] > 0) and (x[context.x] > 0) and (y[context.m] > 0):
        context.longs = [context.m, context.x]
    elif (x[context.m] > 0) and (y[context.m] > 0):
        context.longs = [context.m]
    else:
        context.longs = [context.b]


def buy(context,data):
    
    for s in context.portfolio.positions:
        if s in context.longs:
            continue
        if not data.can_trade(s):
            continue
        order_target(s, 0)    

    if get_open_orders():
        track_orders(context, data)
        return  
    
    for s in context.longs:
        if not data.can_trade(s):
            continue
        order_target_percent(s, 1.0 / len(context.longs))
    track_orders(context, data)
        
        
def display(context,data):                             
    
    record(leverage = context.account.leverage,
           exposure = context.account.net_leverage)
    
    
def pvr(context, data):
    ''' Custom chart and/or logging of profit_vs_risk returns and related information
    '''
    # # # # # # # # # #  Options  # # # # # # # # # #
    logging         = 1            # Info to logging window with some new maximums

    record_pvr      = 1            # Profit vs Risk returns (percentage)
    record_pvrp     = 0            # PvR (p)roportional neg cash vs portfolio value
    record_cash     = 1            # Cash available
    record_max_lvrg = 1            # Maximum leverage encountered
    record_risk_hi  = 0            # Highest risk overall
    record_shorting = 0            # Total value of any shorts
    record_max_shrt = 0            # Max value of shorting total
    record_cash_low = 1            # Any new lowest cash level
    record_q_return = 0            # Quantopian returns (percentage)
    record_pnl      = 1            # Profit-n-Loss
    record_risk     = 0            # Risked, max cash spent or shorts beyond longs+cash
    record_leverage = 0            # Leverage (context.account.leverage)
    record_overshrt = 0            # Shorts beyond longs+cash
    if record_pvrp: record_pvr = 0 # if pvrp is active, straight pvr is off

    import time
    from datetime import datetime
    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 readability]
    if 'pvr' not in c:
        date_strt = get_environment('start').date()
        date_end  = get_environment('end').date()
        cash_low  = c.portfolio.starting_cash
        c.cagr    = 0.0
        c.pvr     = {
            'pvr'        : 0,      # Profit vs Risk returns based on maximum spent
            'max_lvrg'   : 0,
            'max_shrt'   : 0,
            'risk_hi'    : 0,
            'days'       : 0.0,
            'date_prv'   : '',
            'date_end'   : date_end,
            'cash_low'   : cash_low,
            'cash'       : cash_low,
            'start'      : cash_low,
            'pstart'     : c.portfolio.portfolio_value, # Used if restart
            'begin'      : time.time(),                 # For run time
            'log_summary': 126,                         # Summary every x days
            'run_str'    : '{} to {}  ${}  {} US/Eastern'.format(date_strt, date_end, int(cash_low), datetime.now(timezone('US/Eastern')).strftime("%Y-%m-%d %H:%M"))
        }
        log.info(c.pvr['run_str'])

    def _pvr(c):
        c.cagr = ((c.portfolio.portfolio_value / c.pvr['start']) ** (1 / (c.pvr['days'] / 252.))) - 1
        ptype = 'PvR' if record_pvr else 'PvRp'
        log.info('{} {} %/day   cagr {}   Portfolio value {}   PnL {}'.format(ptype, '%.4f' % (c.pvr['pvr'] / c.pvr['days']), '%.1f' % c.cagr, '%.0f' % c.portfolio.portfolio_value, '%.0f' % (c.portfolio.portfolio_value - c.pvr['start'])))
        log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format('%.0f' % (c.portfolio.portfolio_value - c.pvr['start']), '%.0f' % c.pvr['risk_hi'], '%.1f' % c.pvr['pvr']))
        log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {} MxShrt {}'.format('%.2f' % q_rtrn, '%.2f' % c.pvr['pvr'], '%.0f' % c.pvr['cash_low'], '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi'], '%.0f' % c.pvr['max_shrt']))

    def _minut():
        dt = get_datetime().astimezone(timezone('US/Eastern'))
        return str((dt.hour * 60) + dt.minute - 570).rjust(3)  # (-570 = 9:31a)

    date = get_datetime().date()
    if c.pvr['date_prv'] != date:
        c.pvr['date_prv'] = date
        c.pvr['days'] += 1.0
    do_summary = 0
    if c.pvr['log_summary'] and c.pvr['days'] % c.pvr['log_summary'] == 0 and _minut() == '100':
        do_summary = 1              # Log summary every x days
    if do_summary or date == c.pvr['date_end']:
        c.pvr['cash'] = c.portfolio.cash
    elif c.pvr['cash'] == c.portfolio.cash and not logging: return  # for speed

    longs  = sum([p.amount * p.last_sale_price for s, p in c.portfolio.positions.items() if p.amount > 0])
    shorts = sum([p.amount * p.last_sale_price for s, p in c.portfolio.positions.items() if p.amount < 0])
    q_rtrn       = 100 * (c.portfolio.portfolio_value - c.pvr['start']) / c.pvr['start']
    cash         = c.portfolio.cash
    new_risk_hi  = 0
    new_max_lv   = 0
    new_max_shrt = 0
    new_cash_low = 0               # To trigger logging in cash_low case
    overshorts   = 0               # Shorts value beyond longs plus cash
    cash_dip     = int(max(0, c.pvr['pstart'] - cash))
    risk         = int(max(cash_dip, shorts))

    if record_pvrp and cash < 0:   # Let negative cash ding less when portfolio is up.
        cash_dip = int(max(0, c.pvr['start'] - cash * c.pvr['start'] / c.portfolio.portfolio_value))
        # Imagine: Start with 10, grows to 1000, goes negative to -10, should not be 200% risk.

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

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

    if shorts < c.pvr['max_shrt']:
        new_max_shrt = 1
        c.pvr['max_shrt'] = shorts                # Maximum shorts value
        if record_max_shrt: record(MxShrts = c.pvr['max_shrt'])

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

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

    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 beyond payable
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage
    if record_cash:     record(Cash = cash)                   # Cash
    if record_risk:     record(Risk = risk)   # Amount in play, maximum of shorts or cash used
    if record_q_return: record(QRet = q_rtrn) # Quantopian returns to compare to pvr returns curve
    if record_pnl:      record(PnL  = c.portfolio.portfolio_value - c.pvr['start']) # Profit|Loss

    if logging and (new_risk_hi or new_cash_low or new_max_lv or new_max_shrt):
        csh     = ' Cash '   + '%.0f' % cash
        risk    = ' Risk '   + '%.0f' % risk
        qret    = ' QRet '   + '%.1f' % q_rtrn
        shrt    = ' Shrt '   + '%.0f' % shorts
        ovrshrt = ' oShrt '  + '%.0f' % overshorts
        lv      = ' Lv '     + '%.1f' % c.account.leverage
        pvr     = ' PvR '    + '%.1f' % c.pvr['pvr']
        rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']
        csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low']
        mxlv    = ' MxLv '   + '%.2f' % c.pvr['max_lvrg']
        mxshrt  = ' MxShrt ' + '%.0f' % c.pvr['max_shrt']
        pnl     = ' PnL '    + '%.0f' % (c.portfolio.portfolio_value - c.pvr['start'])
        log.info('{}{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minut(), lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, mxshrt, ovrshrt, risk, rsk_hi))
    if do_summary: _pvr(c)
    if get_datetime() == get_environment('end'):    # Summary at end of run
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0
        if not c.pvr_summary_done:
            _pvr(c)
            elapsed = (time.time() - c.pvr['begin']) / 60  # minutes
            log.info( '{}\nRuntime {} hr {} min'.format(c.pvr['run_str'], int(elapsed / 60), '%.1f' % (elapsed % 60)))
            c.pvr_summary_done = 1

def handle_data(context, data):
    pvr(context, data)
    track_orders(context, data)
    
    if context.start:
        rebalance(context, data)
        buy(context, data)
        #display(context, data)
        context.start = False
        
def track_orders(context, data):  # Log orders created, filled, unfilled or canceled.
    '''      https://www.quantopian.com/posts/track-orders
    Status:
       0 - Unfilled
       1 - Filled (can be partial)
       2 - Canceled
    '''
    c = context
    log_cash = 1    # Show cash values in logging window or not.
    log_ids  = 0    # Include order id's in logging window or not.
    log_unfilled = 1

    ''' Start and stop date options ...
    To not overwhelm the logging window, start/stop dates can be entered
      either below or in initialize() if you move to there for better efficiency.
    Example:
        c.dates  = {
            'active': 0,
            'start' : ['2007-05-07', '2010-04-26'],
            'stop'  : ['2008-02-13', '2010-11-15']
        }
    '''
    if 'orders' not in c:
        c.orders = {}               # Move these to initialize() for better efficiency.
        c.dates  = {
            'active': 0,
            'start' : ['2014-02-21'],           # Start dates, option
            'stop'  : []            # Stop  dates, option
        }
    from pytz import timezone       # Python only does once, makes this portable.
                                    #   Move to top of algo for better efficiency.

    # If the dates 'start' or 'stop' lists have something in them, sets them.
    if c.dates['start'] or c.dates['stop']:
        date = str(get_datetime().date())
        if   date in c.dates['start']:    # See if there's a match to start
            c.dates['active'] = 1
        elif date in c.dates['stop']:     #   ... or to stop
            c.dates['active'] = 0
    else:
        c.dates['active'] = 1  # Set to active b/c no conditions

    if c.dates['active'] == 0:
        return                 # Skip if off

    def _minute():   # To preface each line with the minute of the day.
        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)

    def _orders(to_log):    # So all logging comes from the same line number,
        log.info(to_log)    #   for vertical alignment in the logging window.

    ordrs = c.orders.copy()    # Independent copy to allow deletes
    for id in ordrs:
        o = get_order(id)
        if o.dt == get_datetime(): continue  # Same minute as order, no chance of fill yet.
        sec  = o.sid ; sym = sec.symbol
        oid  = o.id if log_ids else ''
        cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''
        prc  = '%.2f' % data.current(sec, 'price') if data.can_trade(sec) else 'unknwn'
        if o.filled:        # Filled at least some
            trade  = 'Bot' if o.amount > 0 else 'Sold'
            filled = '{}'.format(o.amount)
            filled_this = ''
            if o.filled == o.amount:    # complete
                if 0 < c.orders[o.id] < o.amount:
                    filled  = 'all/{}'.format(o.amount)
                else:
                    filled  = '{}'.format(o.amount)
                filled_this = 1
                del c.orders[o.id]
            else:
                done_prv       = c.orders[o.id]       # previously filled ttl
                filled_this    = o.filled - done_prv  # filled this time, can be 0
                c.orders[o.id] = o.filled             # save for increments math
                filled         = '{}/{}'.format(filled_this, o.amount)
            if filled_this:
                _orders(' {}      {} {} {} at {}   {} {}'.format(_minute(),
                    trade, filled, sym, prc, cash, oid))
        elif log_unfilled:
            canceled = 'canceled' if o.status == 2 else ''
            _orders(' {}         {} {} unfilled {} {}'.format(_minute(),
                    o.sid.symbol, o.amount, canceled, oid))
            if canceled: del c.orders[o.id]

    for oo_list in get_open_orders().values(): # Open orders list
        for o in oo_list:
            sec  = o.sid ; sym = sec.symbol
            oid  = o.id if log_ids else ''
            cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''
            prc  = '%.2f' % data.current(sec, 'price') if data.can_trade(sec) else 'unknwn'
            if o.status == 2:                  # Canceled
                _orders(' {}    Canceled {} {} order   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, cash, oid))
                del c.orders[o.id]
            elif o.id not in c.orders:         # New
                c.orders[o.id] = 0
                trade = 'Buy' if o.amount > 0 else 'Sell'
                if o.limit:                    # Limit order
                    _orders(' {}   {} {} {} now {} limit {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, o.limit, cash, oid))
                elif o.stop:                   # Stop order
                    _orders(' {}   {} {} {} now {} stop {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, o.stop, cash, oid))
                else:                          # Market order
                    _orders(' {}   {} {} {} at {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, cash, oid))

        
'''

2010-12-01 06:31 pvr:115 INFO 2010-12-01 to 2017-02-09  $20000  2017-02-24 03:32 US/Eastern
2010-12-01 06:32 pvr:203 INFO   2 Lv 0.1 MxLv 0.11 QRet -0.0 PvR -0.1 PnL -1 Cash 17841 CshLw 17840 Shrt 0 MxShrt 0 oShrt 0 Risk 2159 RskHi 2159
2010-12-01 06:33 pvr:203 INFO   3 Lv 0.7 MxLv 0.67 QRet -0.0 PvR -0.0 PnL -3 Cash 6686 CshLw 6686 Shrt 0 MxShrt 0 oShrt 0 Risk 13313 RskHi 13313
2010-12-01 06:34 pvr:203 INFO   4 Lv 1.0 MxLv 1.00 QRet 0.0 PvR 0.0 PnL 4 Cash -20 CshLw -19 Shrt 0 MxShrt 0 oShrt 0 Risk 20019 RskHi 20019
2010-12-01 06:35 pvr:203 INFO   5 Lv 1.0 MxLv 1.00 QRet 0.0 PvR 0.0 PnL 1 Cash -20 CshLw -19 Shrt 0 MxShrt 0 oShrt 0 Risk 20019 RskHi 20019
2010-12-01 06:43 pvr:203 INFO  13 Lv 1.0 MxLv 1.00 QRet -0.0 PvR -0.0 PnL -3 Cash -20 CshLw -19 Shrt 0 MxShrt 0 oShrt 0 Risk 20019 RskHi 20019
2011-01-03 07:01 pvr:203 INFO  31 Lv 1.0 MxLv 1.00 QRet 5.9 PvR 5.9 PnL 1190 Cash -92 CshLw -92 Shrt 0 MxShrt 0 oShrt 0 Risk 20092 RskHi 20092
2011-01-03 07:04 pvr:203 INFO  34 Lv 1.0 MxLv 1.00 QRet 5.9 PvR 5.9 PnL 1188 Cash -92 CshLw -92 Shrt 0 MxShrt 0 oShrt 0 Risk 20092 RskHi 20092
2011-01-06 07:01 pvr:203 INFO  31 Lv 1.0 MxLv 1.01 QRet 7.1 PvR 7.1 PnL 1423 Cash -115 CshLw -114 Shrt 0 MxShrt 0 oShrt 0 Risk 20114 RskHi 20114
2011-01-06 07:11 pvr:203 INFO  41 Lv 1.0 MxLv 1.01 QRet 7.1 PvR 7.1 PnL 1423 Cash -115 CshLw -114 Shrt 0 MxShrt 0 oShrt 0 Risk 20114 RskHi 20114
2011-01-06 07:13 pvr:203 INFO  43 Lv 1.0 MxLv 1.01 QRet 7.1 PvR 7.1 PnL 1421 Cash -115 CshLw -114 Shrt 0 MxShrt 0 oShrt 0 Risk 20114 RskHi 20114
2011-01-06 07:15 pvr:203 INFO  45 Lv 1.0 MxLv 1.01 QRet 7.1 PvR 7.0 PnL 1417 Cash -115 CshLw -114 Shrt 0 MxShrt 0 oShrt 0 Risk 20114 RskHi 20114
2011-01-06 08:05 pvr:203 INFO  95 Lv 1.0 MxLv 1.01 QRet 7.0 PvR 7.0 PnL 1401 Cash -115 CshLw -114 Shrt 0 MxShrt 0 oShrt 0 Risk 20114 RskHi 20114
2011-01-06 08:06 pvr:203 INFO  96 Lv 1.0 MxLv 1.01 QRet 7.0 PvR 7.0 PnL 1401 Cash -115 CshLw -114 Shrt 0 MxShrt 0 oShrt 0 Risk 20114 RskHi 20114
2011-01-06 08:07 pvr:203 INFO  97 Lv 1.0 MxLv 1.01 QRet 7.0 PvR 7.0 PnL 1399 Cash -115 CshLw -114 Shrt 0 MxShrt 0 oShrt 0 Risk 20114 RskHi 20114
2011-01-06 08:08 pvr:203 INFO  98 Lv 1.0 MxLv 1.01 QRet 6.7 PvR 6.7 PnL 1342 Cash -115 CshLw -114 Shrt 0 MxShrt 0 oShrt 0 Risk 20114 RskHi 20114
2011-01-06 08:09 pvr:203 INFO  99 Lv 1.0 MxLv 1.01 QRet 6.7 PvR 6.6 PnL 1337 Cash -115 CshLw -114 Shrt 0 MxShrt 0 oShrt 0 Risk 20114 RskHi 20114
2011-01-06 08:11 pvr:203 INFO 101 Lv 1.0 MxLv 1.01 QRet 6.3 PvR 6.3 PnL 1265 Cash -115 CshLw -114 Shrt 0 MxShrt 0 oShrt 0 Risk 20114 RskHi 20114
2011-01-07 07:01 pvr:203 INFO  31 Lv 1.0 MxLv 1.01 QRet 7.3 PvR 7.2 PnL 1462 Cash -164 CshLw -163 Shrt 0 MxShrt 0 oShrt 0 Risk 20163 RskHi 20163
2011-01-12 07:01 pvr:203 INFO  31 Lv 1.0 MxLv 1.01 QRet 9.8 PvR 9.7 PnL 1965 Cash -185 CshLw -185 Shrt 0 MxShrt 0 oShrt 0 Risk 20185 RskHi 20185
2011-02-25 07:01 pvr:203 INFO  31 Lv 1.0 MxLv 1.01 QRet 13.2 PvR 13.1 PnL 2647 Cash -228 CshLw -227 Shrt 0 MxShrt 0 oShrt 0 Risk 20227 RskHi 20227
2011-04-25 07:01 pvr:203 INFO  31 Lv 1.0 MxLv 1.01 QRet 22.5 PvR 22.2 PnL 4491 Cash -230 CshLw -230 Shrt 0 MxShrt 0 oShrt 0 Risk 20230 RskHi 20230
2011-06-01 08:10 _pvr:120 INFO PvR 0.2337 %/day   cagr 0.7   Portfolio value 25958   PnL 5958
2011-06-01 08:10 _pvr:121 INFO   Profited 5958 on 20230 activated/transacted for PvR of 29.4%
2011-06-01 08:10 _pvr:122 INFO   QRet 29.79 PvR 29.45 CshLw -230 MxLv 1.01 RskHi 20230 MxShrt 0
2011-11-29 08:10 _pvr:120 INFO PvR 0.3371 %/day   cagr 0.9   Portfolio value 37187   PnL 17187
2011-11-29 08:10 _pvr:121 INFO   Profited 17187 on 20230 activated/transacted for PvR of 85.0%
2011-11-29 08:10 _pvr:122 INFO   QRet 85.93 PvR 84.96 CshLw -230 MxLv 1.01 RskHi 20230 MxShrt 0
2012-05-31 08:10 _pvr:120 INFO PvR 0.3725 %/day   cagr 0.8   Portfolio value 48483   PnL 28483
2012-05-31 08:10 _pvr:121 INFO   Profited 28483 on 20230 activated/transacted for PvR of 140.8%
2012-05-31 08:10 _pvr:122 INFO   QRet 142.41 PvR 140.80 CshLw -230 MxLv 1.01 RskHi 20230 MxShrt 0
2012-08-22 07:01 pvr:203 INFO  31 Lv 1.0 MxLv 1.01 QRet 131.0 PvR 128.6 PnL 26198 Cash -373 CshLw -372 Shrt 0 MxShrt 0 oShrt 0 Risk 20372 RskHi 20372
2012-09-17 07:01 pvr:203 INFO  31 Lv 1.0 MxLv 1.01 QRet 161.3 PvR 156.2 PnL 32263 Cash -658 CshLw -658 Shrt 0 MxShrt 0 oShrt 0 Risk 20658 RskHi 20658
2012-11-21 13:00 WARN Your order for 422 shares of EDV has been partially filled. 196 shares were successfully purchased. 226 shares were not filled by the end of day and were canceled.
2012-11-23 10:00 WARN Your order for 226 shares of EDV has been partially filled. 176 shares were successfully purchased. 50 shares were not filled by the end of day and were canceled.
2012-11-30 08:10 _pvr:120 INFO PvR 0.3252 %/day   cagr 0.6   Portfolio value 53857   PnL 33857
2012-11-30 08:10 _pvr:121 INFO   Profited 33857 on 20658 activated/transacted for PvR of 163.9%
2012-11-30 08:10 _pvr:122 INFO   QRet 169.29 PvR 163.89 CshLw -658 MxLv 1.01 RskHi 20658 MxShrt 0
2013-06-04 08:10 _pvr:120 INFO PvR 0.3467 %/day   cagr 0.6   Portfolio value 65126   PnL 45126
2013-06-04 08:10 _pvr:121 INFO   Profited 45126 on 20658 activated/transacted for PvR of 218.4%
2013-06-04 08:10 _pvr:122 INFO   QRet 225.63 PvR 218.45 CshLw -658 MxLv 1.01 RskHi 20658 MxShrt 0
2013-10-14 07:01 pvr:203 INFO  31 Lv 1.0 MxLv 1.01 QRet 251.2 PvR 241.7 PnL 50239 Cash -787 CshLw -787 Shrt 0 MxShrt 0 oShrt 0 Risk 20787 RskHi 20787
2013-12-02 08:10 _pvr:120 INFO PvR 0.3963 %/day   cagr 0.6   Portfolio value 82284   PnL 62284
2013-12-02 08:10 _pvr:121 INFO   Profited 62284 on 20787 activated/transacted for PvR of 299.6%
2013-12-02 08:10 _pvr:122 INFO   QRet 311.42 PvR 299.63 CshLw -787 MxLv 1.01 RskHi 20787 MxShrt 0
2014-02-25 07:01 pvr:203 INFO  31 Lv 1.5 MxLv 1.47 QRet 311.9 PvR 106.7 PnL 62377 Cash -38468 CshLw -38468 Shrt 0 MxShrt 0 oShrt 0 Risk 58468 RskHi 58468
2014-06-04 08:10 _pvr:120 INFO PvR 0.1377 %/day   cagr 0.5   Portfolio value 91020   PnL 71020
2014-06-04 08:10 _pvr:121 INFO   Profited 71020 on 58468 activated/transacted for PvR of 121.5%
2014-06-04 08:10 _pvr:122 INFO   QRet 355.10 PvR 121.47 CshLw -38468 MxLv 1.47 RskHi 58468 MxShrt 0
2014-08-26 07:01 pvr:203 INFO  31 Lv 1.5 MxLv 1.49 QRet 399.0 PvR 115.2 PnL 79793 Cash -49254 CshLw -49253 Shrt 0 MxShrt 0 oShrt 0 Risk 69253 RskHi 69253
2014-11-11 13:00 WARN Your order for -795 shares of EDV has been partially filled. 383 shares were successfully sold. 412 shares were not filled by the end of day and were canceled.
2014-11-12 13:00 WARN Your order for -412 shares of EDV has been partially filled. 388 shares were successfully sold. 24 shares were not filled by the end of day and were canceled.
2014-12-02 08:10 _pvr:120 INFO PvR 0.1027 %/day   cagr 0.5   Portfolio value 91702   PnL 71702
2014-12-02 08:10 _pvr:121 INFO   Profited 71702 on 69253 activated/transacted for PvR of 103.5%
2014-12-02 08:10 _pvr:122 INFO   QRet 358.51 PvR 103.54 CshLw -49253 MxLv 1.49 RskHi 69253 MxShrt 0
2015-06-04 08:10 _pvr:120 INFO PvR 0.1161 %/day   cagr 0.5   Portfolio value 111190   PnL 91190
2015-06-04 08:10 _pvr:121 INFO   Profited 91190 on 69253 activated/transacted for PvR of 131.7%
2015-06-04 08:10 _pvr:122 INFO   QRet 455.95 PvR 131.68 CshLw -49253 MxLv 1.49 RskHi 69253 MxShrt 0
2015-06-17 13:00 WARN Your order for 989 shares of EDV has been partially filled. 764 shares were successfully purchased. 225 shares were not filled by the end of day and were canceled.
2015-06-23 13:00 WARN Your order for -999 shares of EDV has been partially filled. 535 shares were successfully sold. 464 shares were not filled by the end of day and were canceled.
2015-06-24 13:00 WARN Your order for -464 shares of EDV has been partially filled. 246 shares were successfully sold. 218 shares were not filled by the end of day and were canceled.
2015-07-01 13:00 WARN Your order for 923 shares of EDV has been partially filled. 906 shares were successfully purchased. 17 shares were not filled by the end of day and were canceled.
2015-12-02 08:10 _pvr:120 INFO PvR 0.1031 %/day   cagr 0.4   Portfolio value 109953   PnL 89953
2015-12-02 08:10 _pvr:121 INFO   Profited 89953 on 69253 activated/transacted for PvR of 129.9%
2015-12-02 08:10 _pvr:122 INFO   QRet 449.77 PvR 129.89 CshLw -49253 MxLv 1.49 RskHi 69253 MxShrt 0
2016-03-22 13:00 WARN Your order for -935 shares of EDV has been partially filled. 472 shares were successfully sold. 463 shares were not filled by the end of day and were canceled.
2016-03-29 07:01 pvr:203 INFO  31 Lv 1.4 MxLv 1.49 QRet 488.1 PvR 140.2 PnL 97628 Cash -49635 CshLw -49634 Shrt 0 MxShrt 0 oShrt 0 Risk 69634 RskHi 69634
2016-06-03 08:10 _pvr:120 INFO PvR 0.1230 %/day   cagr 0.4   Portfolio value 138683   PnL 118683
2016-06-03 08:10 _pvr:121 INFO   Profited 118683 on 69634 activated/transacted for PvR of 170.4%
2016-06-03 08:10 _pvr:122 INFO   QRet 593.42 PvR 170.44 CshLw -49634 MxLv 1.49 RskHi 69634 MxShrt 0
2016-07-19 07:01 pvr:203 INFO  31 Lv 1.4 MxLv 1.49 QRet 541.5 PvR 148.7 PnL 108291 Cash -52814 CshLw -52814 Shrt 0 MxShrt 0 oShrt 0 Risk 72814 RskHi 72814
2016-07-19 07:02 pvr:203 INFO  32 Lv 1.5 MxLv 1.49 QRet 541.4 PvR 130.2 PnL 108272 Cash -63186 CshLw -63185 Shrt 0 MxShrt 0 oShrt 0 Risk 83185 RskHi 83185
2016-10-19 13:00 WARN Your order for 1102 shares of EDV has been partially filled. 209 shares were successfully purchased. 893 shares were not filled by the end of day and were canceled.
2016-12-01 08:10 _pvr:120 INFO PvR 0.0846 %/day   cagr 0.4   Portfolio value 126456   PnL 106456
2016-12-01 08:10 _pvr:121 INFO   Profited 106456 on 83185 activated/transacted for PvR of 128.0%
2016-12-01 08:10 _pvr:122 INFO   QRet 532.28 PvR 127.98 CshLw -63185 MxLv 1.49 RskHi 83185 MxShrt 0
2017-02-09 13:00 _pvr:120 INFO PvR 0.1060 %/day   cagr 0.4   Portfolio value 157506   PnL 137506
2017-02-09 13:00 _pvr:121 INFO   Profited 137506 on 83185 activated/transacted for PvR of 165.3%
2017-02-09 13:00 _pvr:122 INFO   QRet 687.53 PvR 165.30 CshLw -63185 MxLv 1.49 RskHi 83185 MxShrt 0
2017-02-09 13:00 pvr:210 INFO 2010-12-01 to 2017-02-09  $20000  2017-02-24 03:32 US/Eastern
Runtime 0 hr 7.5 min
End of logs.

'''

There was a runtime error.

Thank you so much, Blue, your PvR tool is really invaluable. I should remember to always use it.

I believe I have now somehow brought the leverage and the negative cash under control (although not 100%), but the returns are of course much less.

Clone Algorithm
3
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
'''
https://www.quantopian.com/posts/volatility-strategies-what-are-they
'''

import pandas as pd
import numpy as np


def initialize(context):    
    
    set_asset_restrictions(security_lists.restrict_leveraged_etfs)
    
    context.m = sid(24744) # RSP
    context.x = sid(40516) # XIV
    context.b = sid(22887) # EDV
    
    context.secs = [context.m, context.x, context.b]
    
    context.start = True
    
    schedule_function(func = rebalance,
                      date_rule = date_rules.week_start(),
                      time_rule = time_rules.market_close(minutes = 20),
                      half_days = True)
    
    schedule_function(buy,
        date_rule=date_rules.every_day(),
        time_rule=time_rules.market_open(minutes = 30))
    
    
def rebalance(context, data):
    
    P = data.history(context.secs,'price', 252, '1d')
    
    x = P.tail(10).median() / P.tail(100).median() - 1
    y = P.tail(2).median() / P.tail(200).median() - 1

    if (x[context.m] > 0) and (x[context.x] > 0) and (y[context.m] > 0):
        context.longs = [context.m, context.x]
    elif (x[context.m] > 0) and (y[context.m] > 0):
        context.longs = [context.m]
    else:
        context.longs = [context.b]


def buy(context,data):
    
    for s in context.portfolio.positions:
        if s in context.longs:
            continue
        if not data.can_trade(s):
            continue
        order_target(s, 0)    

    if get_open_orders():
        track_orders(context, data)
        return  
    
    for s in context.longs:
        if not data.can_trade(s):
            continue
        order_value(s, context.portfolio.cash / len(context.longs))
    
    track_orders(context, data)
        
        
def display(context,data):                             
    
    record(leverage = context.account.leverage,
           exposure = context.account.net_leverage)
    
    
def pvr(context, data):
    ''' Custom chart and/or logging of profit_vs_risk returns and related information
    '''
    # # # # # # # # # #  Options  # # # # # # # # # #
    logging         = 1            # Info to logging window with some new maximums

    record_pvr      = 1            # Profit vs Risk returns (percentage)
    record_pvrp     = 0            # PvR (p)roportional neg cash vs portfolio value
    record_cash     = 1            # Cash available
    record_max_lvrg = 1            # Maximum leverage encountered
    record_risk_hi  = 0            # Highest risk overall
    record_shorting = 0            # Total value of any shorts
    record_max_shrt = 0            # Max value of shorting total
    record_cash_low = 1            # Any new lowest cash level
    record_q_return = 0            # Quantopian returns (percentage)
    record_pnl      = 1            # Profit-n-Loss
    record_risk     = 0            # Risked, max cash spent or shorts beyond longs+cash
    record_leverage = 0            # Leverage (context.account.leverage)
    record_overshrt = 0            # Shorts beyond longs+cash
    if record_pvrp: record_pvr = 0 # if pvrp is active, straight pvr is off

    import time
    from datetime import datetime
    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 readability]
    if 'pvr' not in c:
        date_strt = get_environment('start').date()
        date_end  = get_environment('end').date()
        cash_low  = c.portfolio.starting_cash
        c.cagr    = 0.0
        c.pvr     = {
            'pvr'        : 0,      # Profit vs Risk returns based on maximum spent
            'max_lvrg'   : 0,
            'max_shrt'   : 0,
            'risk_hi'    : 0,
            'days'       : 0.0,
            'date_prv'   : '',
            'date_end'   : date_end,
            'cash_low'   : cash_low,
            'cash'       : cash_low,
            'start'      : cash_low,
            'pstart'     : c.portfolio.portfolio_value, # Used if restart
            'begin'      : time.time(),                 # For run time
            'log_summary': 126,                         # Summary every x days
            'run_str'    : '{} to {}  ${}  {} US/Eastern'.format(date_strt, date_end, int(cash_low), datetime.now(timezone('US/Eastern')).strftime("%Y-%m-%d %H:%M"))
        }
        log.info(c.pvr['run_str'])

    def _pvr(c):
        c.cagr = ((c.portfolio.portfolio_value / c.pvr['start']) ** (1 / (c.pvr['days'] / 252.))) - 1
        ptype = 'PvR' if record_pvr else 'PvRp'
        log.info('{} {} %/day   cagr {}   Portfolio value {}   PnL {}'.format(ptype, '%.4f' % (c.pvr['pvr'] / c.pvr['days']), '%.1f' % c.cagr, '%.0f' % c.portfolio.portfolio_value, '%.0f' % (c.portfolio.portfolio_value - c.pvr['start'])))
        log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format('%.0f' % (c.portfolio.portfolio_value - c.pvr['start']), '%.0f' % c.pvr['risk_hi'], '%.1f' % c.pvr['pvr']))
        log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {} MxShrt {}'.format('%.2f' % q_rtrn, '%.2f' % c.pvr['pvr'], '%.0f' % c.pvr['cash_low'], '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi'], '%.0f' % c.pvr['max_shrt']))

    def _minut():
        dt = get_datetime().astimezone(timezone('US/Eastern'))
        return str((dt.hour * 60) + dt.minute - 570).rjust(3)  # (-570 = 9:31a)

    date = get_datetime().date()
    if c.pvr['date_prv'] != date:
        c.pvr['date_prv'] = date
        c.pvr['days'] += 1.0
    do_summary = 0
    if c.pvr['log_summary'] and c.pvr['days'] % c.pvr['log_summary'] == 0 and _minut() == '100':
        do_summary = 1              # Log summary every x days
    if do_summary or date == c.pvr['date_end']:
        c.pvr['cash'] = c.portfolio.cash
    elif c.pvr['cash'] == c.portfolio.cash and not logging: return  # for speed

    longs  = sum([p.amount * p.last_sale_price for s, p in c.portfolio.positions.items() if p.amount > 0])
    shorts = sum([p.amount * p.last_sale_price for s, p in c.portfolio.positions.items() if p.amount < 0])
    q_rtrn       = 100 * (c.portfolio.portfolio_value - c.pvr['start']) / c.pvr['start']
    cash         = c.portfolio.cash
    new_risk_hi  = 0
    new_max_lv   = 0
    new_max_shrt = 0
    new_cash_low = 0               # To trigger logging in cash_low case
    overshorts   = 0               # Shorts value beyond longs plus cash
    cash_dip     = int(max(0, c.pvr['pstart'] - cash))
    risk         = int(max(cash_dip, shorts))

    if record_pvrp and cash < 0:   # Let negative cash ding less when portfolio is up.
        cash_dip = int(max(0, c.pvr['start'] - cash * c.pvr['start'] / c.portfolio.portfolio_value))
        # Imagine: Start with 10, grows to 1000, goes negative to -10, should not be 200% risk.

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

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

    if shorts < c.pvr['max_shrt']:
        new_max_shrt = 1
        c.pvr['max_shrt'] = shorts                # Maximum shorts value
        if record_max_shrt: record(MxShrts = c.pvr['max_shrt'])

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

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

    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 beyond payable
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage
    if record_cash:     record(Cash = cash)                   # Cash
    if record_risk:     record(Risk = risk)   # Amount in play, maximum of shorts or cash used
    if record_q_return: record(QRet = q_rtrn) # Quantopian returns to compare to pvr returns curve
    if record_pnl:      record(PnL  = c.portfolio.portfolio_value - c.pvr['start']) # Profit|Loss

    if logging and (new_risk_hi or new_cash_low or new_max_lv or new_max_shrt):
        csh     = ' Cash '   + '%.0f' % cash
        risk    = ' Risk '   + '%.0f' % risk
        qret    = ' QRet '   + '%.1f' % q_rtrn
        shrt    = ' Shrt '   + '%.0f' % shorts
        ovrshrt = ' oShrt '  + '%.0f' % overshorts
        lv      = ' Lv '     + '%.1f' % c.account.leverage
        pvr     = ' PvR '    + '%.1f' % c.pvr['pvr']
        rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']
        csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low']
        mxlv    = ' MxLv '   + '%.2f' % c.pvr['max_lvrg']
        mxshrt  = ' MxShrt ' + '%.0f' % c.pvr['max_shrt']
        pnl     = ' PnL '    + '%.0f' % (c.portfolio.portfolio_value - c.pvr['start'])
        log.info('{}{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minut(), lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, mxshrt, ovrshrt, risk, rsk_hi))
    if do_summary: _pvr(c)
    if get_datetime() == get_environment('end'):    # Summary at end of run
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0
        if not c.pvr_summary_done:
            _pvr(c)
            elapsed = (time.time() - c.pvr['begin']) / 60  # minutes
            log.info( '{}\nRuntime {} hr {} min'.format(c.pvr['run_str'], int(elapsed / 60), '%.1f' % (elapsed % 60)))
            c.pvr_summary_done = 1

def handle_data(context, data):
    pvr(context, data)
    track_orders(context, data)
    
    if context.start:
        rebalance(context, data)
        buy(context, data)
        #display(context, data)
        context.start = False
        
def track_orders(context, data):  # Log orders created, filled, unfilled or canceled.
    '''      https://www.quantopian.com/posts/track-orders
    Status:
       0 - Unfilled
       1 - Filled (can be partial)
       2 - Canceled
    '''
    c = context
    log_cash = 1    # Show cash values in logging window or not.
    log_ids  = 0    # Include order id's in logging window or not.
    log_unfilled = 1

    ''' Start and stop date options ...
    To not overwhelm the logging window, start/stop dates can be entered
      either below or in initialize() if you move to there for better efficiency.
    Example:
        c.dates  = {
            'active': 0,
            'start' : ['2007-05-07', '2010-04-26'],
            'stop'  : ['2008-02-13', '2010-11-15']
        }
    '''
    if 'orders' not in c:
        c.orders = {}               # Move these to initialize() for better efficiency.
        c.dates  = {
            'active': 0,
            'start' : ['2014-02-21'],           # Start dates, option
            'stop'  : []            # Stop  dates, option
        }
    from pytz import timezone       # Python only does once, makes this portable.
                                    #   Move to top of algo for better efficiency.

    # If the dates 'start' or 'stop' lists have something in them, sets them.
    if c.dates['start'] or c.dates['stop']:
        date = str(get_datetime().date())
        if   date in c.dates['start']:    # See if there's a match to start
            c.dates['active'] = 1
        elif date in c.dates['stop']:     #   ... or to stop
            c.dates['active'] = 0
    else:
        c.dates['active'] = 1  # Set to active b/c no conditions

    if c.dates['active'] == 0:
        return                 # Skip if off

    def _minute():   # To preface each line with the minute of the day.
        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)

    def _orders(to_log):    # So all logging comes from the same line number,
        log.info(to_log)    #   for vertical alignment in the logging window.

    ordrs = c.orders.copy()    # Independent copy to allow deletes
    for id in ordrs:
        o = get_order(id)
        if o.dt == get_datetime(): continue  # Same minute as order, no chance of fill yet.
        sec  = o.sid ; sym = sec.symbol
        oid  = o.id if log_ids else ''
        cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''
        prc  = '%.2f' % data.current(sec, 'price') if data.can_trade(sec) else 'unknwn'
        if o.filled:        # Filled at least some
            trade  = 'Bot' if o.amount > 0 else 'Sold'
            filled = '{}'.format(o.amount)
            filled_this = ''
            if o.filled == o.amount:    # complete
                if 0 < c.orders[o.id] < o.amount:
                    filled  = 'all/{}'.format(o.amount)
                else:
                    filled  = '{}'.format(o.amount)
                filled_this = 1
                del c.orders[o.id]
            else:
                done_prv       = c.orders[o.id]       # previously filled ttl
                filled_this    = o.filled - done_prv  # filled this time, can be 0
                c.orders[o.id] = o.filled             # save for increments math
                filled         = '{}/{}'.format(filled_this, o.amount)
            if filled_this:
                _orders(' {}      {} {} {} at {}   {} {}'.format(_minute(),
                    trade, filled, sym, prc, cash, oid))
        elif log_unfilled:
            canceled = 'canceled' if o.status == 2 else ''
            _orders(' {}         {} {} unfilled {} {}'.format(_minute(),
                    o.sid.symbol, o.amount, canceled, oid))
            if canceled: del c.orders[o.id]

    for oo_list in get_open_orders().values(): # Open orders list
        for o in oo_list:
            sec  = o.sid ; sym = sec.symbol
            oid  = o.id if log_ids else ''
            cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''
            prc  = '%.2f' % data.current(sec, 'price') if data.can_trade(sec) else 'unknwn'
            if o.status == 2:                  # Canceled
                _orders(' {}    Canceled {} {} order   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, cash, oid))
                del c.orders[o.id]
            elif o.id not in c.orders:         # New
                c.orders[o.id] = 0
                trade = 'Buy' if o.amount > 0 else 'Sell'
                if o.limit:                    # Limit order
                    _orders(' {}   {} {} {} now {} limit {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, o.limit, cash, oid))
                elif o.stop:                   # Stop order
                    _orders(' {}   {} {} {} now {} stop {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, o.stop, cash, oid))
                else:                          # Market order
                    _orders(' {}   {} {} {} at {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, cash, oid))

        
There was a runtime error.

Here is the second algo with the PvR controls implemented. Thanks again, Blue!

Clone Algorithm
12
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 pandas as pd
import numpy as np


def initialize(context):    
    
    set_asset_restrictions(security_lists.restrict_leveraged_etfs)
    
    context.m = sid(24744) # RSP
    context.x = sid(40516) # XIV
    context.b = sid(22887) # EDV
    
    context.secs = [context.m, context.x, context.b]
    
    context.start = True

    context.w = None
    
    
    schedule_function(func = rebalance,
                      date_rule = date_rules.week_start(),
                      time_rule = time_rules.market_close(minutes = 20),
                      half_days = True)
    
    schedule_function(buy,
        date_rule=date_rules.every_day(),
        time_rule=time_rules.market_open(minutes = 30))

    
def rebalance(context, data):
    
    P = data.history(context.secs,'price', 252, '1d')
    
    R = P.pct_change().dropna()
    s = R.std()
    
    x = P.tail(10).median() / P.tail(100).median() - 1
    y = P.tail(2).median() / P.tail(200).median() - 1

    if (x[context.m] > 0) and (x[context.x] > 0) and (y[context.m] > 0):
        longs = [context.m, context.x]
    elif (x[context.m] > 0) and (y[context.m] > 0):
        longs = [context.m]
    else:
        longs = [context.b]

    w = 1 / s
    w = w[longs]
    
    q = np.nan_to_num(w)
    w = pd.Series(q, index = longs)
    u = w.abs().sum()
    if u > 0: w  = w / u
    context.w = w
    
    
def buy(context,data):
    
    for s in context.portfolio.positions:
        if s in context.w.index:
            continue
        if not data.can_trade(s):
            continue
        order_target(s, 0)    

    if get_open_orders():
        track_orders(context, data)
        return  
    
    for s in context.w.index:
        if not data.can_trade(s):
            continue
        order_value(s, context.portfolio.cash * context.w[s])

    track_orders(context, data)


def pvr(context, data):
    ''' Custom chart and/or logging of profit_vs_risk returns and related information
    '''
    # # # # # # # # # #  Options  # # # # # # # # # #
    logging         = 1            # Info to logging window with some new maximums

    record_pvr      = 1            # Profit vs Risk returns (percentage)
    record_pvrp     = 0            # PvR (p)roportional neg cash vs portfolio value
    record_cash     = 1            # Cash available
    record_max_lvrg = 1            # Maximum leverage encountered
    record_risk_hi  = 0            # Highest risk overall
    record_shorting = 0            # Total value of any shorts
    record_max_shrt = 0            # Max value of shorting total
    record_cash_low = 1            # Any new lowest cash level
    record_q_return = 0            # Quantopian returns (percentage)
    record_pnl      = 1            # Profit-n-Loss
    record_risk     = 0            # Risked, max cash spent or shorts beyond longs+cash
    record_leverage = 0            # Leverage (context.account.leverage)
    record_overshrt = 0            # Shorts beyond longs+cash
    if record_pvrp: record_pvr = 0 # if pvrp is active, straight pvr is off

    import time
    from datetime import datetime
    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 readability]
    if 'pvr' not in c:
        date_strt = get_environment('start').date()
        date_end  = get_environment('end').date()
        cash_low  = c.portfolio.starting_cash
        c.cagr    = 0.0
        c.pvr     = {
            'pvr'        : 0,      # Profit vs Risk returns based on maximum spent
            'max_lvrg'   : 0,
            'max_shrt'   : 0,
            'risk_hi'    : 0,
            'days'       : 0.0,
            'date_prv'   : '',
            'date_end'   : date_end,
            'cash_low'   : cash_low,
            'cash'       : cash_low,
            'start'      : cash_low,
            'pstart'     : c.portfolio.portfolio_value, # Used if restart
            'begin'      : time.time(),                 # For run time
            'log_summary': 126,                         # Summary every x days
            'run_str'    : '{} to {}  ${}  {} US/Eastern'.format(date_strt, date_end, int(cash_low), datetime.now(timezone('US/Eastern')).strftime("%Y-%m-%d %H:%M"))
        }
        log.info(c.pvr['run_str'])

    def _pvr(c):
        c.cagr = ((c.portfolio.portfolio_value / c.pvr['start']) ** (1 / (c.pvr['days'] / 252.))) - 1
        ptype = 'PvR' if record_pvr else 'PvRp'
        log.info('{} {} %/day   cagr {}   Portfolio value {}   PnL {}'.format(ptype, '%.4f' % (c.pvr['pvr'] / c.pvr['days']), '%.1f' % c.cagr, '%.0f' % c.portfolio.portfolio_value, '%.0f' % (c.portfolio.portfolio_value - c.pvr['start'])))
        log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format('%.0f' % (c.portfolio.portfolio_value - c.pvr['start']), '%.0f' % c.pvr['risk_hi'], '%.1f' % c.pvr['pvr']))
        log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {} MxShrt {}'.format('%.2f' % q_rtrn, '%.2f' % c.pvr['pvr'], '%.0f' % c.pvr['cash_low'], '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi'], '%.0f' % c.pvr['max_shrt']))

    def _minut():
        dt = get_datetime().astimezone(timezone('US/Eastern'))
        return str((dt.hour * 60) + dt.minute - 570).rjust(3)  # (-570 = 9:31a)

    date = get_datetime().date()
    if c.pvr['date_prv'] != date:
        c.pvr['date_prv'] = date
        c.pvr['days'] += 1.0
    do_summary = 0
    if c.pvr['log_summary'] and c.pvr['days'] % c.pvr['log_summary'] == 0 and _minut() == '100':
        do_summary = 1              # Log summary every x days
    if do_summary or date == c.pvr['date_end']:
        c.pvr['cash'] = c.portfolio.cash
    elif c.pvr['cash'] == c.portfolio.cash and not logging: return  # for speed

    longs  = sum([p.amount * p.last_sale_price for s, p in c.portfolio.positions.items() if p.amount > 0])
    shorts = sum([p.amount * p.last_sale_price for s, p in c.portfolio.positions.items() if p.amount < 0])
    q_rtrn       = 100 * (c.portfolio.portfolio_value - c.pvr['start']) / c.pvr['start']
    cash         = c.portfolio.cash
    new_risk_hi  = 0
    new_max_lv   = 0
    new_max_shrt = 0
    new_cash_low = 0               # To trigger logging in cash_low case
    overshorts   = 0               # Shorts value beyond longs plus cash
    cash_dip     = int(max(0, c.pvr['pstart'] - cash))
    risk         = int(max(cash_dip, shorts))

    if record_pvrp and cash < 0:   # Let negative cash ding less when portfolio is up.
        cash_dip = int(max(0, c.pvr['start'] - cash * c.pvr['start'] / c.portfolio.portfolio_value))
        # Imagine: Start with 10, grows to 1000, goes negative to -10, should not be 200% risk.

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

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

    if shorts < c.pvr['max_shrt']:
        new_max_shrt = 1
        c.pvr['max_shrt'] = shorts                # Maximum shorts value
        if record_max_shrt: record(MxShrts = c.pvr['max_shrt'])

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

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

    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 beyond payable
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage
    if record_cash:     record(Cash = cash)                   # Cash
    if record_risk:     record(Risk = risk)   # Amount in play, maximum of shorts or cash used
    if record_q_return: record(QRet = q_rtrn) # Quantopian returns to compare to pvr returns curve
    if record_pnl:      record(PnL  = c.portfolio.portfolio_value - c.pvr['start']) # Profit|Loss

    if logging and (new_risk_hi or new_cash_low or new_max_lv or new_max_shrt):
        csh     = ' Cash '   + '%.0f' % cash
        risk    = ' Risk '   + '%.0f' % risk
        qret    = ' QRet '   + '%.1f' % q_rtrn
        shrt    = ' Shrt '   + '%.0f' % shorts
        ovrshrt = ' oShrt '  + '%.0f' % overshorts
        lv      = ' Lv '     + '%.1f' % c.account.leverage
        pvr     = ' PvR '    + '%.1f' % c.pvr['pvr']
        rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']
        csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low']
        mxlv    = ' MxLv '   + '%.2f' % c.pvr['max_lvrg']
        mxshrt  = ' MxShrt ' + '%.0f' % c.pvr['max_shrt']
        pnl     = ' PnL '    + '%.0f' % (c.portfolio.portfolio_value - c.pvr['start'])
        log.info('{}{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minut(), lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, mxshrt, ovrshrt, risk, rsk_hi))
    if do_summary: _pvr(c)
    if get_datetime() == get_environment('end'):    # Summary at end of run
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0
        if not c.pvr_summary_done:
            _pvr(c)
            elapsed = (time.time() - c.pvr['begin']) / 60  # minutes
            log.info( '{}\nRuntime {} hr {} min'.format(c.pvr['run_str'], int(elapsed / 60), '%.1f' % (elapsed % 60)))
            c.pvr_summary_done = 1

            
def handle_data(context, data):
    pvr(context, data)
    track_orders(context, data)
    
    if context.start:
        rebalance(context, data)
        buy(context, data)
        context.start = False

        
def track_orders(context, data):  # Log orders created, filled, unfilled or canceled.
    '''      https://www.quantopian.com/posts/track-orders
    Status:
       0 - Unfilled
       1 - Filled (can be partial)
       2 - Canceled
    '''
    c = context
    log_cash = 1    # Show cash values in logging window or not.
    log_ids  = 0    # Include order id's in logging window or not.
    log_unfilled = 1

    ''' Start and stop date options ...
    To not overwhelm the logging window, start/stop dates can be entered
      either below or in initialize() if you move to there for better efficiency.
    Example:
        c.dates  = {
            'active': 0,
            'start' : ['2007-05-07', '2010-04-26'],
            'stop'  : ['2008-02-13', '2010-11-15']
        }
    '''
    if 'orders' not in c:
        c.orders = {}               # Move these to initialize() for better efficiency.
        c.dates  = {
            'active': 0,
            'start' : ['2014-02-21'],           # Start dates, option
            'stop'  : []            # Stop  dates, option
        }
    from pytz import timezone       # Python only does once, makes this portable.
                                    #   Move to top of algo for better efficiency.

    # If the dates 'start' or 'stop' lists have something in them, sets them.
    if c.dates['start'] or c.dates['stop']:
        date = str(get_datetime().date())
        if   date in c.dates['start']:    # See if there's a match to start
            c.dates['active'] = 1
        elif date in c.dates['stop']:     #   ... or to stop
            c.dates['active'] = 0
    else:
        c.dates['active'] = 1  # Set to active b/c no conditions

    if c.dates['active'] == 0:
        return                 # Skip if off

    def _minute():   # To preface each line with the minute of the day.
        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)

    def _orders(to_log):    # So all logging comes from the same line number,
        log.info(to_log)    #   for vertical alignment in the logging window.

    ordrs = c.orders.copy()    # Independent copy to allow deletes
    for id in ordrs:
        o = get_order(id)
        if o.dt == get_datetime(): continue  # Same minute as order, no chance of fill yet.
        sec  = o.sid ; sym = sec.symbol
        oid  = o.id if log_ids else ''
        cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''
        prc  = '%.2f' % data.current(sec, 'price') if data.can_trade(sec) else 'unknwn'
        if o.filled:        # Filled at least some
            trade  = 'Bot' if o.amount > 0 else 'Sold'
            filled = '{}'.format(o.amount)
            filled_this = ''
            if o.filled == o.amount:    # complete
                if 0 < c.orders[o.id] < o.amount:
                    filled  = 'all/{}'.format(o.amount)
                else:
                    filled  = '{}'.format(o.amount)
                filled_this = 1
                del c.orders[o.id]
            else:
                done_prv       = c.orders[o.id]       # previously filled ttl
                filled_this    = o.filled - done_prv  # filled this time, can be 0
                c.orders[o.id] = o.filled             # save for increments math
                filled         = '{}/{}'.format(filled_this, o.amount)
            if filled_this:
                _orders(' {}      {} {} {} at {}   {} {}'.format(_minute(),
                    trade, filled, sym, prc, cash, oid))
        elif log_unfilled:
            canceled = 'canceled' if o.status == 2 else ''
            _orders(' {}         {} {} unfilled {} {}'.format(_minute(),
                    o.sid.symbol, o.amount, canceled, oid))
            if canceled: del c.orders[o.id]

    for oo_list in get_open_orders().values(): # Open orders list
        for o in oo_list:
            sec  = o.sid ; sym = sec.symbol
            oid  = o.id if log_ids else ''
            cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''
            prc  = '%.2f' % data.current(sec, 'price') if data.can_trade(sec) else 'unknwn'
            if o.status == 2:                  # Canceled
                _orders(' {}    Canceled {} {} order   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, cash, oid))
                del c.orders[o.id]
            elif o.id not in c.orders:         # New
                c.orders[o.id] = 0
                trade = 'Buy' if o.amount > 0 else 'Sell'
                if o.limit:                    # Limit order
                    _orders(' {}   {} {} {} now {} limit {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, o.limit, cash, oid))
                elif o.stop:                   # Stop order
                    _orders(' {}   {} {} {} now {} stop {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, o.stop, cash, oid))
                else:                          # Market order
                    _orders(' {}   {} {} {} at {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, cash, oid))

        
There was a runtime error.

If someone would like to summarize that paper on volatility and what's happening in these backtests, like, so a five-year-old can understand, my gratitude.

The latest first algo is nearly 200 percentage points higher in PvR than the original. Since this is not the easiest thing in the world to understand, lemme try to clarify for folks this way: Imagine you start out with $10 and buy a share of SPY each day for several years. The backtester will show you astronomical returns because it treats the negative cash as if it never happened, it says, wow, you spent just $10 and made a fortune so that's a great return.

The first algo appears to be 680%. However it spent 83,185 to make 137,506. So, whoever the reader might be, think to yourself, are the returns 680%? No that's 165%. Tim's revision is 350%, great job. Anyway ...

PnL per stock added to your latest first algo here in the custom chart so you can see what each one is up to. track_orders turned off for speed this time.

Second algo: If I'm not mistaken, with dropna(), any time one stock has a NaN, the day's data is dropped for all stocks, entire row. One possibility might be to use back fill and forward fill instead. Try replacing .dropna() with .bfill().ffill()

Clone Algorithm
8
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
'''
https://www.quantopian.com/posts/volatility-strategies-what-are-they
'''

import pandas as pd
import numpy as np

def initialize(context):

    set_asset_restrictions(security_lists.restrict_leveraged_etfs)

    context.m = sid(24744) # RSP
    context.x = sid(40516) # XIV
    context.b = sid(22887) # EDV

    context.secs = [context.m, context.x, context.b]

    context.start = True

    schedule_function(func = rebalance,
                      date_rule = date_rules.week_start(),
                      time_rule = time_rules.market_close(minutes = 20),
                      half_days = True)

    schedule_function(buy,
        date_rule=date_rules.every_day(),
        time_rule=time_rules.market_open(minutes = 30))

    schedule_function(record_pnl, date_rules.every_day())
    context.pnl_sids  = []
    context.day_count = 0


def record_pnl(context, data):
    def _pnl_value(sec, context, data):
        pos = context.portfolio.positions[sec]
        pnl = pos.amount * (data.current(sec, 'price') - pos.cost_basis)

        #if pnl < -500:
        #    order_target(sec, 0)

        return pnl

    context.day_count += 1

    for s in context.portfolio.positions:
        if not data.can_trade(s): continue

        # periodically log all
        if context.day_count % 126 == 0:
            log.info('{} {}'.format(s.symbol, int(_pnl_value(s, context, data))))

        # add up to 5 securities for record
        if len(context.pnl_sids) < 5 and s not in context.pnl_sids:
            context.pnl_sids.append(s)
        if s not in context.pnl_sids: continue     # limit to only them

        # record their profit and loss
        who  = s.symbol
        what = _pnl_value(s, context, data)
        record( **{ who: what } )


def rebalance(context, data):

    P = data.history(context.secs,'price', 252, '1d')

    x = P.tail(10).median() / P.tail(100).median() - 1
    y = P.tail(2).median() / P.tail(200).median() - 1

    if (x[context.m] > 0) and (x[context.x] > 0) and (y[context.m] > 0):
        context.longs = [context.m, context.x]
    elif (x[context.m] > 0) and (y[context.m] > 0):
        context.longs = [context.m]
    else:
        context.longs = [context.b]


def buy(context,data):

    for s in context.portfolio.positions:
        if s in context.longs:
            continue
        if not data.can_trade(s):
            continue
        order_target(s, 0)

    if get_open_orders():
        #track_orders(context, data)
        return

    for s in context.longs:
        if not data.can_trade(s):
            continue
        order_value(s, context.portfolio.cash / len(context.longs))

    #track_orders(context, data)


def display(context,data):
    record(leverage = context.account.leverage,
           exposure = context.account.net_leverage)

def handle_data(context, data):
    pvr(context, data)
    #track_orders(context, data)

    if context.start:
        rebalance(context, data)
        buy(context, data)
        #display(context, data)
        context.start = False

def pvr(context, data):
    ''' Custom chart and/or logging of profit_vs_risk returns and related information
    '''
    # # # # # # # # # #  Options  # # # # # # # # # #
    logging         = 1            # Info to logging window with some new maximums

    record_pvr      = 1            # Profit vs Risk returns (percentage)
    record_pvrp     = 0            # PvR (p)roportional neg cash vs portfolio value
    record_cash     = 0            # Cash available
    record_max_lvrg = 0            # Maximum leverage encountered
    record_risk_hi  = 0            # Highest risk overall
    record_shorting = 0            # Total value of any shorts
    record_max_shrt = 0            # Max value of shorting total
    record_cash_low = 0            # Any new lowest cash level
    record_q_return = 0            # Quantopian returns (percentage)
    record_pnl      = 0            # Profit-n-Loss
    record_risk     = 0            # Risked, max cash spent or shorts beyond longs+cash
    record_leverage = 0            # Leverage (context.account.leverage)
    record_overshrt = 0            # Shorts beyond longs+cash
    if record_pvrp: record_pvr = 0 # if pvrp is active, straight pvr is off

    import time
    from datetime import datetime
    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 readability]
    if 'pvr' not in c:
        date_strt = get_environment('start').date()
        date_end  = get_environment('end').date()
        cash_low  = c.portfolio.starting_cash
        c.cagr    = 0.0
        c.pvr     = {
            'pvr'        : 0,      # Profit vs Risk returns based on maximum spent
            'max_lvrg'   : 0,
            'max_shrt'   : 0,
            'risk_hi'    : 0,
            'days'       : 0.0,
            'date_prv'   : '',
            'date_end'   : date_end,
            'cash_low'   : cash_low,
            'cash'       : cash_low,
            'start'      : cash_low,
            'pstart'     : c.portfolio.portfolio_value, # Used if restart
            'begin'      : time.time(),                 # For run time
            'log_summary': 126,                         # Summary every x days
            'run_str'    : '{} to {}  ${}  {} US/Eastern'.format(date_strt, date_end, int(cash_low), datetime.now(timezone('US/Eastern')).strftime("%Y-%m-%d %H:%M"))
        }
        log.info(c.pvr['run_str'])

    def _pvr(c):
        c.cagr = ((c.portfolio.portfolio_value / c.pvr['start']) ** (1 / (c.pvr['days'] / 252.))) - 1
        ptype = 'PvR' if record_pvr else 'PvRp'
        log.info('{} {} %/day   cagr {}   Portfolio value {}   PnL {}'.format(ptype, '%.4f' % (c.pvr['pvr'] / c.pvr['days']), '%.1f' % c.cagr, '%.0f' % c.portfolio.portfolio_value, '%.0f' % (c.portfolio.portfolio_value - c.pvr['start'])))
        log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format('%.0f' % (c.portfolio.portfolio_value - c.pvr['start']), '%.0f' % c.pvr['risk_hi'], '%.1f' % c.pvr['pvr']))
        log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {} MxShrt {}'.format('%.2f' % q_rtrn, '%.2f' % c.pvr['pvr'], '%.0f' % c.pvr['cash_low'], '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi'], '%.0f' % c.pvr['max_shrt']))

    def _minut():
        dt = get_datetime().astimezone(timezone('US/Eastern'))
        return str((dt.hour * 60) + dt.minute - 570).rjust(3)  # (-570 = 9:31a)

    date = get_datetime().date()
    if c.pvr['date_prv'] != date:
        c.pvr['date_prv'] = date
        c.pvr['days'] += 1.0
    do_summary = 0
    if c.pvr['log_summary'] and c.pvr['days'] % c.pvr['log_summary'] == 0 and _minut() == '100':
        do_summary = 1              # Log summary every x days
    if do_summary or date == c.pvr['date_end']:
        c.pvr['cash'] = c.portfolio.cash
    elif c.pvr['cash'] == c.portfolio.cash and not logging: return  # for speed

    longs  = sum([p.amount * p.last_sale_price for s, p in c.portfolio.positions.items() if p.amount > 0])
    shorts = sum([p.amount * p.last_sale_price for s, p in c.portfolio.positions.items() if p.amount < 0])
    q_rtrn       = 100 * (c.portfolio.portfolio_value - c.pvr['start']) / c.pvr['start']
    cash         = c.portfolio.cash
    new_risk_hi  = 0
    new_max_lv   = 0
    new_max_shrt = 0
    new_cash_low = 0               # To trigger logging in cash_low case
    overshorts   = 0               # Shorts value beyond longs plus cash
    cash_dip     = int(max(0, c.pvr['pstart'] - cash))
    risk         = int(max(cash_dip, shorts))

    if record_pvrp and cash < 0:   # Let negative cash ding less when portfolio is up.
        cash_dip = int(max(0, c.pvr['start'] - cash * c.pvr['start'] / c.portfolio.portfolio_value))
        # Imagine: Start with 10, grows to 1000, goes negative to -10, should not be 200% risk.

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

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

    if shorts < c.pvr['max_shrt']:
        new_max_shrt = 1
        c.pvr['max_shrt'] = shorts                # Maximum shorts value
        if record_max_shrt: record(MxShrts = c.pvr['max_shrt'])

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

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

    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 beyond payable
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage
    if record_cash:     record(Cash = cash)                   # Cash
    if record_risk:     record(Risk = risk)   # Amount in play, maximum of shorts or cash used
    if record_q_return: record(QRet = q_rtrn) # Quantopian returns to compare to pvr returns curve
    if record_pnl:      record(PnL  = c.portfolio.portfolio_value - c.pvr['start']) # Profit|Loss

    if logging and (new_risk_hi or new_cash_low or new_max_lv or new_max_shrt):
        csh     = ' Cash '   + '%.0f' % cash
        risk    = ' Risk '   + '%.0f' % risk
        qret    = ' QRet '   + '%.1f' % q_rtrn
        shrt    = ' Shrt '   + '%.0f' % shorts
        ovrshrt = ' oShrt '  + '%.0f' % overshorts
        lv      = ' Lv '     + '%.1f' % c.account.leverage
        pvr     = ' PvR '    + '%.1f' % c.pvr['pvr']
        rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']
        csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low']
        mxlv    = ' MxLv '   + '%.2f' % c.pvr['max_lvrg']
        mxshrt  = ' MxShrt ' + '%.0f' % c.pvr['max_shrt']
        pnl     = ' PnL '    + '%.0f' % (c.portfolio.portfolio_value - c.pvr['start'])
        log.info('{}{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minut(), lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, mxshrt, ovrshrt, risk, rsk_hi))
    if do_summary: _pvr(c)
    if get_datetime() == get_environment('end'):    # Summary at end of run
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0
        if not c.pvr_summary_done:
            _pvr(c)
            elapsed = (time.time() - c.pvr['begin']) / 60  # minutes
            log.info( '{}\nRuntime {} hr {} min'.format(c.pvr['run_str'], int(elapsed / 60), '%.1f' % (elapsed % 60)))
            c.pvr_summary_done = 1

def track_orders(context, data):  # Log orders created, filled, unfilled or canceled.
    '''      https://www.quantopian.com/posts/track-orders
    Status:
       0 - Unfilled
       1 - Filled (can be partial)
       2 - Canceled
    '''
    c = context
    log_cash = 1    # Show cash values in logging window or not.
    log_ids  = 0    # Include order id's in logging window or not.
    log_unfilled = 1

    ''' Start and stop date options ...
    To not overwhelm the logging window, start/stop dates can be entered
      either below or in initialize() if you move to there for better efficiency.
    Example:
        c.dates  = {
            'active': 0,
            'start' : ['2007-05-07', '2010-04-26'],
            'stop'  : ['2008-02-13', '2010-11-15']
        }
    '''
    if 'orders' not in c:
        c.orders = {}               # Move these to initialize() for better efficiency.
        c.dates  = {
            'active': 0,
            'start' : ['2014-02-21'],           # Start dates, option
            'stop'  : []            # Stop  dates, option
        }
    from pytz import timezone       # Python only does once, makes this portable.
                                    #   Move to top of algo for better efficiency.

    # If the dates 'start' or 'stop' lists have something in them, sets them.
    if c.dates['start'] or c.dates['stop']:
        date = str(get_datetime().date())
        if   date in c.dates['start']:    # See if there's a match to start
            c.dates['active'] = 1
        elif date in c.dates['stop']:     #   ... or to stop
            c.dates['active'] = 0
    else:
        c.dates['active'] = 1  # Set to active b/c no conditions

    if c.dates['active'] == 0:
        return                 # Skip if off

    def _minute():   # To preface each line with the minute of the day.
        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)

    def _orders(to_log):    # So all logging comes from the same line number,
        log.info(to_log)    #   for vertical alignment in the logging window.

    ordrs = c.orders.copy()    # Independent copy to allow deletes
    for id in ordrs:
        o = get_order(id)
        if o.dt == get_datetime(): continue  # Same minute as order, no chance of fill yet.
        sec  = o.sid ; sym = sec.symbol
        oid  = o.id if log_ids else ''
        cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''
        prc  = '%.2f' % data.current(sec, 'price') if data.can_trade(sec) else 'unknwn'
        if o.filled:        # Filled at least some
            trade  = 'Bot' if o.amount > 0 else 'Sold'
            filled = '{}'.format(o.amount)
            filled_this = ''
            if o.filled == o.amount:    # complete
                if 0 < c.orders[o.id] < o.amount:
                    filled  = 'all/{}'.format(o.amount)
                else:
                    filled  = '{}'.format(o.amount)
                filled_this = 1
                del c.orders[o.id]
            else:
                done_prv       = c.orders[o.id]       # previously filled ttl
                filled_this    = o.filled - done_prv  # filled this time, can be 0
                c.orders[o.id] = o.filled             # save for increments math
                filled         = '{}/{}'.format(filled_this, o.amount)
            if filled_this:
                _orders(' {}      {} {} {} at {}   {} {}'.format(_minute(),
                    trade, filled, sym, prc, cash, oid))
        elif log_unfilled:
            canceled = 'canceled' if o.status == 2 else ''
            _orders(' {}         {} {} unfilled {} {}'.format(_minute(),
                    o.sid.symbol, o.amount, canceled, oid))
            if canceled: del c.orders[o.id]

    for oo_list in get_open_orders().values(): # Open orders list
        for o in oo_list:
            sec  = o.sid ; sym = sec.symbol
            oid  = o.id if log_ids else ''
            cash = 'cash {}'.format(int(c.portfolio.cash)) if log_cash else ''
            prc  = '%.2f' % data.current(sec, 'price') if data.can_trade(sec) else 'unknwn'
            if o.status == 2:                  # Canceled
                _orders(' {}    Canceled {} {} order   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, cash, oid))
                del c.orders[o.id]
            elif o.id not in c.orders:         # New
                c.orders[o.id] = 0
                trade = 'Buy' if o.amount > 0 else 'Sell'
                if o.limit:                    # Limit order
                    _orders(' {}   {} {} {} now {} limit {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, o.limit, cash, oid))
                elif o.stop:                   # Stop order
                    _orders(' {}   {} {} {} now {} stop {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, o.stop, cash, oid))
                else:                          # Market order
                    _orders(' {}   {} {} {} at {}   {} {}'.format(_minute(),
                        trade, o.amount, sym, prc, cash, oid))


There was a runtime error.

Blue, would you provide some guidance for using your PvR tool? In particular, which values would be considered good, excellent, bad, or terrible? And are there any other things to watch out for?

Answered here.

If someone would like to summarize that paper on volatility and what's happening in these backtests, like, so a five-year-old can understand, my gratitude.

I'm not so interested in particular backtests, but rather a bit more background. What's this VIX thingy, anyway? And why should I care? I see that it is "a popular measure of the implied volatility of S&P 500 index options" (reference), which sounds like a bunch of financial voodoo (no offense to any finance witch doctors out there). And then we have XIV and VXX, which are ETFs that are based on the VIX. Even more voodoo. If I buy a share in XIV or VXX, what do I have? If I buy a share of SPY, then I think I could explain it to a five-year-old. I'd kinda-sorta "own" little pieces of lots of companies, and one could imagine various analogies that would be acceptable to a five-year-old (keeping in mind that one has 30 sec. to a minute max. of attention span).

Grant,

I just provided the backtests as a basic example of a "volatility" algo, that is all. I am not particularly proud of them, but I cannot do much better then this at the moment, I am afraid.

I must admit that I have only read the article that I gave a link to superficially, although I do hope to find the time to study it in some more detail. I take a very pragmatic view towards the VIX and the relatd ETFs: they offer a retail trader the possibility of trading volatility. By this I mean that using them one can bet on the S&P 500 volatility rising or falling, much like betting on the rise or the fall of the index itself, except that volatility posesses different properties from those of the stocks themselves, i.e., it is said to be (more) predictable and to exhibit mean reversion characteristics. (I am sure you know much more about it then I do)

It is my understanding that option traders do the same for most part of their trading (trying to forecast volatilty, that is), but if you do not have access to options, the VIX and its ETFs are your best alternative, I suppose ...

Blue,

Great explanation by (a straightforard) example why leverage control is so important!

They asked us nicely to write our bug reports so a five-year-old could understand, at Microsoft. For the developers.
Thank you Tim, you understand. At the risk of belaboring the point, to try to help make that crystal clear for others, this backtest appears to make a 100x profit from $500, just that the margin leverage was discarded in the returns calculation and it doesn't work that way in the real world. PvR on the other hand reports this as a 26% return because it takes the negative cash into account.

def initialize(context):  
    schedule_function(buy, date_rules.every_day(), time_rules.market_open())

def buy(context,data):  
    order(symbol('SPY'), 1)  
    record(PnL = context.portfolio.pnl)  

And back on point, whew, this volatility trading is tough to wrap one's head around in my opinion ...

The trader will try to achieve a higher profit by finding an instrument in which the implied (market) volatility is higher or lower than the trader’s forecast for the actual realised profit. source

By the way I wonder if pandas variance [.var()] can be used on history() output for volatility.

Clone Algorithm
3
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
def initialize(context):
    schedule_function(buy, date_rules.every_day(), time_rules.market_open())

def buy(context,data):
    order(symbol('SPY'), 1)
    record(PnL = context.portfolio.pnl)
There was a runtime error.