Back to Community
Simple Flexible calendar-spread trade with VIX & gas contracts

(credits to Jeremy Muhia for the first version)

I took the algo from Jeremy and made it a bit more flexible (add more futures) and configurable and I added the optimizer. I have not tried all combo's but somehow Vix and Gas seem to work well together except for the terrible drawdown half way, but in general the algo is quite reliable and can be traded from 50K onwards... when futures are allowed for trading.

I also added the scalping in the last 15 minutes somebody found, but that is more a remnant of a trial

just add a future and a reprentattive etf to the list of futures in the list and it will be picked up:

continuous_future("HG", offset=0, roll="calendar", adjustment=None):symbol('JJC'),

Needs some dollar neutral and hedging and expansion of contracts to be suitable for the strict criteria of the contest.

Would love to see what you guys can add/expand

Clone Algorithm
103
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 quantopian.experimental.optimize as opt
import quantopian.algorithm as algo
import numpy as np
import pandas as pd

MAX_GROSS_LEVERAGE = 1.0
MAX_BETA_EXPOSURE = 0.99
 
def initialize(context):
    c = context
    c.hedging = False
    c.spy           = sid(8554)  # For beta calc
    c.SP            = continuous_future("SP", offset=0, roll="calendar", adjustment=None)
    c.beta          = 1.0        # Assumed starting beta
    c.beta_target   = MAX_BETA_EXPOSURE        # Target any Beta you wish # was 0.0
    c.beta_limit    =  .16       # Pos/neg threshold, set_ratio only outside of this either side of target
    c.long_limit_hi =  .98       # Max ratio of long to portfolio
    c.long_limit_lo = 1.0 - c.long_limit_hi
    c.beta_df       = pd.DataFrame([], columns=['pf', 'spy'])
    c.stops         = {}
    c.stoploss      = 0.25
    c.stoppedout    = []

    context.long_shrt_ratio = 2.0  # to 1
    context.long_ratio = 2.0 / (context.long_shrt_ratio + 1)
    context.shrt_ratio = 1.0 - context.long_ratio

    context.max_long_sec   = 150
    context.max_short_sec  = int(context.max_long_sec / context.long_shrt_ratio)
    context.long_short_total = context.max_long_sec + context.max_short_sec

    context.days= 15 #how many days left on the contract 30>x>2
    # Save the futures contracts we'll be trading and the corresponding proxies for the underlying's spot price.
    # context.securities = continuous_future("NG", offset=0, roll="calendar", adjustment=None)
    # context.proxy = sid(33837)
    context.securities = {
                          continuous_future("VX", offset=0, roll="calendar", adjustment=None):symbol('VXX'),
                          continuous_future("NG", offset=0, roll="calendar", adjustment=None):symbol('FCG'),
                          continuous_future("HG", offset=0, roll="calendar", adjustment=None):symbol('JJC'),                   
                         }
    #                      continuous_future("EC", offset=0, roll="calendar", adjustment=None):symbol('FXE')}
    etfs= dict.values(context.securities)
    
    # Create empty keys that will later contain our window of cost of carry data.
    context.cost_of_carry_data = dict.fromkeys(etfs, [])
    context.cost_of_carry_quantiles = dict.fromkeys(etfs, [])

    context.DJF =  continuous_future('SP') #YM=DOW , SP=S&P, RM = Russel
    context.DJ = sid(2174) #better results with DJ 2174
    context.scalp_entered = False
    context.scalp_enter_cash = 0
    
    # Rebalance every day, 1 hour after market open.
    schedule_function(set_ratio, date_rules.every_day(), time_rules.market_open())
    schedule_function(train_algorithm, date_rules.every_day(), time_rules.market_open(hours=1))
    schedule_function(daily_rebalance, date_rules.every_day(), time_rules.market_open(hours=1))
    schedule_function(record_vars, date_rules.every_day(), time_rules.market_open())
     # open position 15 minutes before market close
    schedule_function(definecontracts, date_rules.every_day(), time_rules.market_open(hours=6,minutes=14) )
    schedule_function(scalp_enter, date_rules.every_day(), time_rules.market_open(hours=6,minutes=15) )
    schedule_function(scalp_exit,date_rules.every_day(),time_rules.market_close())

def definecontracts(context, data):
    cl_chain = data.current_chain(context.DJF)
    context.DJcontract = cl_chain[0]


def scalp_enter(context, data):
    
  if not context.scalp_entered :    
    # Position 50% of our portfolio to be short in SPY
    order_target_percent(context.DJcontract, -1.0) 
    context.scalp_entered = True
    record_vars(context, data)
    log.info("++DJ Scalp Entered")
    

def scalp_exit(context, data):

  if context.scalp_entered :    
    order_target_percent(context.DJcontract, 0.0)
    context.scalp_entered = False
    record_vars(context, data)
    log.info("--DJ Scalp Exited")
    
    
def train_algorithm(context, data):
    """
    Before executing any trades, we must collect at least 30 days of data. After this, keep sliding the 30 day window
    to remove the oldest data point while adding the newest point.
    """
    
    for contracts,etf in context.securities.iteritems():
        contract = data.current(contracts, "contract")
        etfprice = data.current(etf, "price")
        cost_of_carry = calc_cost_of_carry(context, data, contract, etfprice, etf)
        context.cost_of_carry_data[etf].append(cost_of_carry)
        
        if len(context.cost_of_carry_data[etf]) > 30:
            #HARDCODE~!
            # After collecting 30 days worth of data, group the data points into 5 quantiles.
            context.cost_of_carry_quantiles[etf] = pd.qcut(context.cost_of_carry_data[etf], 5, labels=False) + 1
            context.cost_of_carry_data[etf].pop(0)

def daily_rebalance(context, data):
    """
    Execute orders according to our schedule_function() timing. 
    """
    
    weights = {}
    
    # After collecting 30 days worth of data, execute our ordering logic by buying low cost of carry contracts.
    constraints=[]
    contracts = data.current(context.securities.keys(), "contract")
    for root,contract in contracts.iteritems():
        if contract.symbol in context.stoppedout:
            log.info(' ! '+contract.symbol +' was stopped out before')
        chain  = data.current_chain(root)
        etf = context.securities.get(root)
        cp = data.current(chain,'price')
        backwardation = (cp[-3]-cp[-1]) > -0.01
        #PRE-FILL
        if len(context.cost_of_carry_data[etf]) >= 30:
            if len(context.cost_of_carry_quantiles[etf]) >= 2:
                if context.cost_of_carry_quantiles[etf][-1] == 5 and (contract.expiration_date - get_datetime()).days > context.days:
                    weights[contract] = -1
                    constraints.append(opt.ShortOnly(contract))
                elif (context.cost_of_carry_quantiles[etf][-1] == 1 ) and (contract.expiration_date - get_datetime()).days > context.days:
                    weights[contract] = 1
                    constraints.append(opt.LongOnly(contract))
                else:
                    weights[contract] = 0
                    constraints.append(opt.ReduceOnly(contract))
    for contract in context.portfolio.positions:
        if (contract.expiration_date - get_datetime()).days <= context.days:
            weights[contract] = 0
            
    sumweight = sum(weights.itervalues(), 0.0)
    if abs(sumweight) > MAX_GROSS_LEVERAGE:
        weights = {k: v / total for total in (sum(weights.itervalues(), 0.0),) for k, v in weights.iteritems()}
        
    #Get SP contract in to hedge
    if context.hedging:
        SPcontract = data.current(context.SP, "contract")
        weights[SPcontract] = -context.beta         
        hedge = opt.ShortOnly(SPcontract)# FixedWeight(asset, weight)
        constraints.append(hedge)

    betas={}    
    for contract in weights:
        prices = data.history(contract,'price', 30,'1d')        
        betas[contract] = estimateBeta(prices,context.beta_df.spy)
        
        
    market_neutral = opt.WeightedExposure(
        loadings=pd.DataFrame({'market_beta': betas}),
        min_exposures={'market_beta': -MAX_BETA_EXPOSURE},
        max_exposures={'market_beta': MAX_BETA_EXPOSURE},
    )
    constraints.append(market_neutral)
   
    leverage_constraint = opt.MaxGrossLeverage(MAX_GROSS_LEVERAGE)
    constraints.append(leverage_constraint)
    
    if len(weights) > 0:
        orderids = order_optimal_portfolio(objective=opt.TargetPortfolioWeights(weights),constraints=constraints)
        record_vars(context, data)
        for oo in orderids:
            order = get_order(oo)
            log.info('Ordered '+str(order.amount)+' of asset '+str(order.sid) )
            

    

    
def calc_cost_of_carry(context, data, contract, spot_price, etf):
    """
    Calculate cost of carry using the following formula:
        F(t, T) = S(t) * e^c(T - t)
    where F(t, T) is the futures price at time t for maturity date T, S(t) is the spot price at time t, and c is
    the cost of carry.
    """
    current_date = get_datetime()
    current_price = data.current(contract, "price")
    maturity_date = contract.expiration_date
    cost_of_carry = np.log(current_price / spot_price) / (maturity_date - current_date).days
    return cost_of_carry


def set_ratio(context, data):
    c = context
    beta = calc_beta(c,data)
    bzat = beta - c.beta_target     # bzat is beta-zero adjusted for target
    if (c.beta_target - c.beta_limit) < beta < (c.beta_target + c.beta_limit):  # Skip if inside boundaries
        log.info('Beta in boundaries')

    # -------- Adjust ratios to move toward target Beta --------
    longs_count = len([s for s, pos in context.portfolio.positions.items() if pos.amount > 0])
    shrts_count = len([s for s, pos in context.portfolio.positions.items() if pos.amount < 0])
    long_ratio  = 1.0 * longs_count / (longs_count + shrts_count) if shrts_count > 0 else 0

    # Reduce long & increase short or visa-versa
    # The further away from target Beta, the stronger the adjustment.
    # https://www.quantopian.com/posts/scaling for explanation of next line ...
    temperance = scale(abs(bzat), 0, .30, .35, .80) # Not straight Beta, a portion of it.
    adjust     = max(c.long_limit_lo, long_ratio - (bzat * temperance))
    adjust     = min(c.long_limit_hi, adjust)  # long ratio no higher than long_limit_hi
    #log.info('b{} long {} to {}'.format('%.2f' % beta, '%.2f' % long_ratio, '%.2f' % adjust))
    context.adjust = adjust
    # Adjust the current to new numbers
    record(adjust = adjust) 
    record(Leverage = c.account.leverage) 

def estimateBeta( priceY,priceX):  
    algorithm_returns = (priceY/priceY.shift(1)-1).dropna().values  
    benchmark_returns = (priceX/priceX.shift(1)-1).dropna().values  
    if len(algorithm_returns) <> len(benchmark_returns):  
        minlen = min(len(algorithm_returns), len(benchmark_returns))  
        if minlen > 2:  
            algorithm_returns = algorithm_returns[-minlen:]  
            benchmark_returns = benchmark_returns[-minlen:]  
        else:  
            return 1.00  
    returns_matrix = np.vstack([algorithm_returns, benchmark_returns])  
    C = np.cov(returns_matrix, ddof=1)  
    algorithm_covariance = C[0][1]  
    benchmark_variance = C[1][1]  
    beta = algorithm_covariance / benchmark_variance
    return beta


def calc_beta(c,data):   # Calculate current Beta value
    if len(c.beta_df.pf)>10:
        c.beta = estimateBeta(c.beta_df.pf,c.beta_df.spy)    
    #record(beta_calc = c.beta)
    return c.beta

def scale(wild, a_lo, a_hi, b_lo, b_hi):
    ''' Based on wild value relative to a_lo_hi range,
          return its analog within b_hi_lo, with min b_lo and max b_hi
    '''
    return min(b_hi, max(b_lo, (b_hi * (wild - a_lo)) / (a_hi - a_lo)))

def before_trading_start(context, data):    # Rank for long and short baskets.
    c = context
    c.beta_df = c.beta_df.append({    # Beta calc prep
            'pf' : c.portfolio.portfolio_value,
            'spy': data.current(c.spy, 'price')}, ignore_index=True)
    c.beta_df            = c.beta_df.ix[-30:]    

def handle_data(context, data):
    c=context
    for position in c.portfolio.positions.itervalues():
        if position.amount == 0:
            if position.asset.symbol in c.stops: del c.stops[position.asset.symbol]
            continue
        elif position.asset.symbol not in c.stops:
            stoploss= c.stoploss if position.amount > 0 else -c.stoploss
            c.stops[position.asset.symbol]=position.last_sale_price*(1-stoploss)
            #log.info(' ! I have added '+str(position.asset.symbol)+' to Stops @ '+str((position.last_sale_price)*(1-stoploss)))
        elif c.stops[position.asset.symbol] > position.last_sale_price and position.amount > 0:
            #sell
            log.info(' ! '+str(position.asset.symbol)+'- (Long) has hit stoploss @ '+str(position.last_sale_price))
            if not get_open_orders(position.sid):
                order_target_value(position.sid,0.0)
                record_vars(context, data)
                c.stoppedout.append(position.asset.symbol)
                del c.stops[position.asset.symbol]
        elif c.stops[position.asset.symbol] < position.last_sale_price and position.amount < 0:
            #sell
            log.info(' ! '+str(position.asset.symbol)+'- (Short) has hit stoploss @ '+str(position.last_sale_price))
            if not get_open_orders(position.sid): 
                order_target_value(position.sid,0.0)
                record_vars(context, data)
                c.stoppedout.append(position.asset.symbol)
                del c.stops[position.asset.symbol]
        elif c.stops[position.asset.symbol] < position.last_sale_price*(1- c.stoploss) and position.amount > 0:
            c.stops[position.asset.symbol]=position.last_sale_price*(1- c.stoploss)
            #log.info(' ! I have updated '+str(position.asset.symbol)+'- (Long) to stop @ '+str((position.last_sale_price)*(1- c.stoploss)))
        elif c.stops[position.asset.symbol] > position.last_sale_price*(1+ c.stoploss) and position.amount < 0:
            c.stops[position.asset.symbol]=position.last_sale_price*(1+ c.stoploss)
           # log.info(' ! I have updated '+str(position.asset.symbol)+'- (Short) to stop @ '+str((position.last_sale_price)*(1+ c.stoploss)))

            
def record_vars(context, data):
    """
    This function is called at the end of each day and plots
    the number of long and short positions we are holding.
    """
    # Check how many long and short positions we have.
    longs = shorts = 0
    for position in context.portfolio.positions.itervalues():
        if position.amount > 0:
            longs += 1
        elif position.amount < 0:
            shorts += 1

    # Record our variables.
    record(long_count=longs, short_count=-shorts)            
    ''' Summary processing
        https://www.quantopian.com/posts/run-summary
    '''
    #  - - - - - - - -  Options  - - - - - - - -
    cancel_partials  = 0  # Cancel orders after being partially filled.
    daily_live       = 1  # Log summary at end of each day when live.
    drawdown_returns = 1  # Custom chart returns based on profit/drawdown.
                          #  At 1/2015 Q returns are profit/init_cash.
                          #  Custom chart only accepts 5 items so watch for that.
    filter_zeros     = 1  # 0 or 1 to filter out those with no buy and no sell.
                          # In get_fundamentals for example there can be over
                          #   a thousand stocks processed with many not traded.
    leverage_alert   = 1  # Log new lowest cash points reached.
    percent_results  = 1  # Express results like 270.1% instead of x2.701

    if 'books' not in context:           # Preparation. Initialize one time.
        cash = context.portfolio.starting_cash
        context.books = {   # Starting cash value from GUI or live restart...
            'cash_low'      : cash,
            'shares'        : 0,       # Overall number of shares owned.
            'count_buy'     : 0,       # Overall buy count, number of shares.
            'count_sell'    : 0,       # Overall sell count.
            'cnt_buy_evnts' : 0,       # Overall buy events count.
            'cnt_sel_evnts' : 0,
            'summary_print' : 0,       # Use to force print when you like.
            'commissions'   : 0,       # Commissions.
            'sids_seen'     : [],      # For set_universe since dynamic.
            'orders'        : {},      # Keep orders for accounting,
        }                              #   orders not completely filled yet.
        b = context.books

        # Environment   First/last dates and
        #   Arena: backtest or live. 
        env = get_environment('*')
        b['first_trading_date'] = str(env['start'].date())
        b['last_trading_date']  = str(env['end']  .date())
        b['last_trading_time']  = str(env['end']  .time())
        b['arena'] = env['arena']

        if b['arena'] == 'live':
            b['arena'] = 'paper'
        elif b['arena'] != 'backtest': # ie like 'IB'
            b['arena'] = 'live'

        # Environment at the beginning of the run
        log.info(' {}\n  {} to {}  {}\n'.format(
            b['arena'],
            b['first_trading_date'],
            b['last_trading_date'],
            '   $' + '%.0f' % context.portfolio.starting_cash,
        ))

    '''
        Prepare individual securities dictionaries
          with dynamic set_universe, fetcher, IPO's appearing etc.
    '''
    b = context.books   # For brevity.
    for sec in context.portfolio.positions.keys() + b['sids_seen']:
        sym = sec.symbol
        if sym in b:
            continue
        if sec not in b['sids_seen']:
            # Scenarios with price missing ...
            price = data.current(sec, 'price') if data.can_trade(sec) else 0
            b['sids_seen'].append(sec)
            b[sym] = {
                'init_price'    : price,  # Save for summary.
                'price'         : price,  # Most recent price.
                'cash_low'      : 0,      # Lowest level of cash.
                'balance'       : 0,      # For individual 'x' return.
                'shares'        : 0,
                'count_buy'     : 0,      # Individual buy number of shares.
                'count_sell'    : 0,
                'cnt_buy_evnts' : 0,      # Individual buy events count.
                'cnt_sel_evnts' : 0,
                'return'        : 0,      # Return calculated.
                'analog'        : 0,      # Analog relative return ratio.
            }
    cash_now = context.portfolio.cash
    if cash_now < b['cash_low']:
        b['cash_low'] = cash_now
        # An alert for negative cash unless you like "leverage"
        if leverage_alert and cash_now < 0.:
            log.info('cash low ' + str(b['cash_low']))
    '''
        Custom chart of drawdown returns, profit/drawdown
    '''
    if drawdown_returns:
        cash_now  = context.portfolio.cash
        cash_strt = context.portfolio.starting_cash
        portfolio = context.portfolio.portfolio_value
        drawdown  = cash_strt - b['cash_low']
        profit    = portfolio - cash_strt
        dreturns  = 0 if not drawdown else profit / drawdown
        qreturns  = profit / cash_strt    # Can be useful for easy visual compare,
        #record(QReturn = 100 * qreturns)  #   overlay, same as standard chart.
        record( Return = 100 * dreturns)
    '''
        Accounting. Update the numbers, manage orders if any.
    '''
    accounting = {}  # Local, any orders ready to be counted.

    # Read open orders
    for security, oo_for_sid in get_open_orders().iteritems():
        for order_obj in oo_for_sid:
            # If an order not seen before, add for tracking
            if order_obj.id not in b['orders']:
                b['orders'][order_obj.id] = order_obj.filled

    for id in b['orders']:  # Take a look at current orders saved.
        o = get_order(id)   # Current order, might have been updated.

        # If filled is not zero, account for it
        if o.filled != 0:
            accounting[id] = o    # Set to account for filled.

            # On partial fills, a new order is automatically
            #   generated for the remainder.
            # Bugbug: The only way I could make sense of things so far ...
            # If filled is not amount (shares), that's a partial fill,
            #   cancelling remainder to simplify life. Unsure.
            if o.filled != o.amount and cancel_partials:
                cancel_order(id)

    for id in accounting:    # Do any accounting, into books{}.
        sec = accounting[id]['sid']
        sym = sec.symbol
        if data.can_trade(sec): # Update price if available.
            b[sym]['price'] = data.current(sec, 'price')
        commission          = accounting[id]['commission']
        filled              = accounting[id]['filled']  # Number filled, sell neg.
        # ToDo: Don't know the official actual fill prices.
        transaction         = filled * b[sym]['price']  # Last known price.
        b[sym]['shares']   += filled      # The transaction on sell is negative
        b[sym]['balance']  -= transaction #   so this line adds to balance then.
        b[sym]['balance']  -= commission
        b['commissions']   += commission

        if filled > 0:                          # Buy
            b[sym]['cnt_buy_evnts'] += 1
            b[sym]['count_buy']     += filled
        elif filled < 0:                        # Sell
            b[sym]['cnt_sel_evnts'] += 1
            b[sym]['count_sell']    += abs(filled)

        del b['orders'][id]    # Remove from the list, accounting done.

        # Keep track of lowest cash per symbol
        if b[sym]['balance'] < b[sym]['cash_low']:
            b[sym]['cash_low'] = b[sym]['balance']
    '''
        Show summary if last bar
    '''
    last_bar_now = 0
    if not b['summary_print']:
        if context.books['arena'] in ['paper', 'live'] and daily_live:
            # When paper or live log summary every day end of day.
            # Assumes schedule is set to every_day().
            last_bar_now = 1
        elif context.books['arena'] == 'backtest':
            # Flag for summary output if last bar now
            bar = get_datetime()
            if get_datetime().date() == get_environment('end').date():    # Summary at end of run
                # Not ideal.
                log.info(str(bar.time()))
                last_bar_now = 1
    '''
        Summary output to the logging window
    '''
    if last_bar_now or b['summary_print']:
        # Independent copy of context.books using dict() in case summary print
        #   is set to happen more than once in a run, due to concats below (+=)
        b    = dict(context.books)
        done = {}   # Protect against any listed twice.

        # Some overall values by adding individual values
        for sec in b['sids_seen']:
            if sec in done:
                continue

            # There's a problem with a dynamic run where a security can have
            #   dropped out of the picture, all sold, not in current universe,
            #   and its price is no longer accessible. Need help from Q.
            if data.can_trade(sec):
                b[sec.symbol]['price'] = data.current(sec, 'price')
            sym = sec.symbol
            b['count_buy']     += b[sym]['count_buy']
            b['count_sell']    += b[sym]['count_sell']
            b['cnt_buy_evnts'] += b[sym]['cnt_buy_evnts']
            b['cnt_sel_evnts'] += b[sym]['cnt_sel_evnts']
            b['shares']        += b[sym]['shares']
            done[sec] = 1

        portfolio    = context.portfolio.portfolio_value
        init_cash    = context.portfolio.starting_cash
        cash_now     = context.portfolio.cash
        cash_low     = b['cash_low']
        cash_profit  = cash_now - init_cash
        shares_value = portfolio - cash_now
        spent        = 0
        if init_cash > cash_low:
            spent    = init_cash - cash_low
        else:
            spent    = cash_now - init_cash  # ??  case of short-selling
        spent_prcnt  = ' ({}%)'.format(int(100 * (spent / init_cash)))
        tot_profit   = cash_profit + shares_value
        qntp_return  = (portfolio - init_cash) / init_cash
        draw_return  = 0.
        if b['count_buy'] or b['count_sell']:   # If there were trades
            draw_return = tot_profit / spent
        draw_float   = draw_return
        rel_word     = ' Prcnt' if percent_results else ' Ratio'
        if percent_results:
            if qntp_return < 10.:
                qntp_return = '{}%'.format(float('%.2f' % (100 * qntp_return)))
                draw_return = '{}%'.format(float('%.2f' % (100 * draw_return)))
                draw_float  = float('%.3f' % (100 * draw_float))
            elif qntp_return < 100.:
                qntp_return = '{}%'.format(float('%.1f' % (100 * qntp_return)))
                draw_return = '{}%'.format(float('%.1f' % (100 * draw_return)))
                draw_float  = float('%.1f' % (100 * draw_float))
            else:
                qntp_return = '{}%'.format(int(100 * qntp_return))
                draw_return = '{}%'.format(int(100 * draw_return))
                draw_float  = int(100 * draw_float)
        else:
            if qntp_return < 10.:
                qntp_return = 'x' + '%.2f' % qntp_return
                draw_return = 'x' + '%.2f' % draw_return
            elif qntp_return < 100.:
                qntp_return = 'x' + '%.1f' % qntp_return
                draw_return = 'x' + '%.1f' % draw_return
            else:
                qntp_return = 'x' + '%.0f' % qntp_return
                draw_return = 'x' + '%.0f' % draw_return
        if qntp_return == '0.00%' or qntp_return == 'x0.00':
            qntp_return = '0'
        if draw_return == '0.00%' or draw_return == 'x0.00':
            draw_return = '0'

        v1 = {  # values
            'pflo': '%.0f' % portfolio,
            'icsh': str(int(init_cash)),
            'untd': '0' if int(cash_low) <= 0 else str(int(cash_low)),
            'ncsh': '0' if int(cash_low) >= 0 else str(int(cash_low)),
            'down': str(int(spent)),
            'cshp': str(int(cash_profit)),
            'totp': '%.0f' % tot_profit,
            'qret': qntp_return,
            'dret': draw_return,
        }
        v2 = {
            'cbuy': str(b['count_buy']),
            'csel': str(b['count_sell']),
            'shnw': str(b['shares']),
            'shvl': '%.0f' % shares_value,
            'cmsn': '%.0f' % b['commissions'],
            'cshn': '%.0f' % cash_now,
        }
        # Widths of the longest for columns
        w1 = 0; w2 = 0
        for v in v1:
            len_v_str = len(str(v1[v]))
            if len_v_str > w1:
                w1 = len_v_str
        for v in v2:
            len_v_str = len(str(v2[v]))
            if len_v_str > w2:
                w2 = len_v_str
        for v in v1:  # Padding
            v1[v] = v1[v].rjust(w1)
        for v in v2:
            v2[v] = v2[v].rjust(w2)
        '''
            Portfolio: 342690                                                              
         Initial Cash:   1000                          Buys: 217225 (147 trades)           
          Unused Cash:      0                         Sells: 77559 (11 trades)             
             Neg Cash:    -21                   Commissions: 1523                          
             Drawdown:   1021 (102%)             Shares Now: 139666                        
          Cash Profit:   -991                  Shares Value: 342681                        
         Total Profit: 341690  w/ shares               Cash: 9                             
              QReturn:   x342  Profit/InitCash                                             
               Return:   x335  Profit/Drawdown                                             
        2015-01-02 summary:616 INFO             200 average initial cash, 5 securities     
               Relativ   Buy|    By|Sl     By|Sl    Price    Draw    Cash    Shrs   Shrs   
         Symbol  Ratio   Hold    Count     Evnts   Strt|Now  Down     Now     Now   Value  
           RDNT   x32.0   2.2 16849|16849   20|2     3|9     -39502   39768       0       0
           NVAX   x9.71   2.0 23042|23009   18|3     2|6     -34756   10439      33     190
           EDAP   x21.2   0.5 143855|4419   34|1     2|2    -205336 -205336  139436  341618
           ACHN   x61.8   0.6 22133|22133   38|2     8|13    -49470   96132       0       0
           HGSH   x1548  10.8 11346|11149   37|3     0|4      -1019   58273     197     872
        '''
        pflo   = '{m1:>15} {m2}'.format(
            m1 = 'Portfolio:',     m2 = v1['pflo'] )
        icsh   = '{m1:>15} {m2:<34}{m3}{m4}'.format(
            m1 = 'Initial Cash:',  m2 = v1['icsh'],
            m3 = 'Buys: ' + v2['cbuy'],
            m4 = ' (' + str(b['cnt_buy_evnts']) + ' trades)' )
        ucsh   = '{m1:>15} {m2:<33}{m3}{m4}'.format(
            m1 = 'Unused Cash:',   m2 = v1['untd'],
            m3 = 'Sells: ' + v2['csel'],
            m4 = ' (' + str(b['cnt_sel_evnts']) + ' trades)' )
        ncsh   = '{m1:>15} {m2:<27}{m3}'.format(
            m1 = 'Neg Cash:',      m2 = v1['ncsh'],
            m3 = 'Commissions: ' + v2['cmsn'] )
        dcsh   = '{m1:>15} {m2:<28}{m3}'.format(
            m1 = 'Drawdown:',      m2 = v1['down'] + spent_prcnt,
            m3 = 'Shares Now: ' + v2['shnw'] )
        pcsh   = '{m1:>15} {m2:<26}{m3}'.format(
            m1 = 'Cash Profit:',   m2 = v1['cshp'],
            m3 = 'Shares Value: ' + v2['shvl'] )
        ttlp   = '{m1:>15} {m2:<34}{m3}'.format(
            m1 = 'Total Profit:',  m2 = v1['totp'] + '   w/ shares',
            m3 = 'Cash: ' + v2['cshn'] )
        qret   = '{m1:>15} {m2}'.format(
            m1 = 'QReturn:',
            m2 = v1['qret'] + '   Profit/InitCash' )
        dret   = '{m1:>15} {m2}'.format(            # drawdown return
            m1 = 'Return:',
            m2 = v1['dret'] + '   Profit/Drawdown')
        outs        = [pflo, icsh, ucsh, ncsh, dcsh, pcsh, ttlp, qret, dret]
        out_summary = '_\r\n'
        line_len    = 80      # Length
        for o in outs:
            out_summary += (o + ' ' * (line_len - len(o)) + '\r\n')

        # -------------------------------
        # Individual securities detail
        # -------------------------------
        out_content_collections = []
        count_sids    = len(b['sids_seen'])
        avg_init_cash = init_cash / len(b['sids_seen'])
        sec_word      = ' security' if count_sids == 1 else ' securities'
        sec_strng     = '  ' + '%.0f' % int(avg_init_cash) \
                             + ' average initial cash, ' + str(count_sids) + sec_word
        out_content   = (sec_strng + '    \r\n').rjust(line_len - 26)
        lines_out     = 11    # Log in clumps to stay under logging limits.
        count_lines   = 0
        if filter_zeros:
            count_lines += 1
            out_content += '.\r\n\tZero buy/sell filtered out \r\n\r\n.'
        header1 = [
        '',     'Relativ','Buy|','By|Sl','By|Sl','Price',   'Draw','Cash','Shrs','Shrs ']
        header2 = [
        'Symbol',rel_word,'Hold','Count','Evnts','Strt|Now','Down',' Now',' Now','Value']
        contents_list = [header1, header2]    # To be lines per sym as a list of lists.

        # The list to process
        sids_to_process = []
        for sec in sorted(b['sids_seen']):
            sym = sec.symbol
            if filter_zeros and not b[sym]['count_buy'] and not b[sym]['count_sell']:
                continue
            sids_to_process.append(sec)

        # Individual return
        return_list = []
        for sec in sids_to_process:
            sym = sec.symbol
            # There's a problem with balance, it is tracked based on
            #   last known price, and when filled, no current way to obtain
            #   the actual fill price.
            # For that reason, there can be discrepancies, sometimes major.
            # To Q, request made to provide us with fill_price in the object id.
            cash_now   = b[sym]['balance']      # Balance started at zero
            cash_low   = b[sym]['cash_low']     # Maximum expended
            shares_val = b[sym]['shares'] * b[sym]['price']
            outputs    = shares_val + cash_now
            cash_pnl   = 0   # Cash profit and loss
            if avg_init_cash > cash_low:             # Typical trading
                cash_pnl = avg_init_cash - cash_low
            else:
                cash_pnl = cash_now - avg_init_cash  # ?? Case of short-selling, unsure
            if (b[sym]['count_buy'] or b[sym]['count_sell']) and avg_init_cash:
                b[sym]['return'] = outputs / cash_pnl
                return_list.append(b[sym]['return'])

        if not return_list:
            if not avg_init_cash:
                log.info('Odd, no avg_init_cash, aborting summary')
            else:
                schedule = '        ' + \
                'schedule_function( \n\t\tsummary, date_rules.every_day()'
                schedule += ', time_rules.market_close()\n        )'
                log.info(
                    '.\n  No buys and no sells. If unexpected, check placement' + \
                    ' of calls to summary,\n    after any orders and before any' + \
                    ' returns and/or the scheduling for it ' + \
                    'like summary(context, data) or\n' + \
                    schedule
                )
            return

        # Multiplication factor
        mult_factor  = 0
        shift        = 0    # Up/dn to move value to line up with overall.
        avg_of_list  = 0    # Taking avg as an analog of draw_float.
        return_list  = sorted(return_list)
        lowest       = return_list[0]
        list_shifted = []
        if lowest < 0:
            # Shift upward to avoid zero-division,
            #   in case some are negative, then each back down.
            shift = 0 - lowest
            for r in return_list:
                list_shifted.append(r + shift)
            avg_of_list = sum(list_shifted) / len(list_shifted)
            mult_factor = draw_float / avg_of_list
        else:
            avg_of_list = sum(return_list) / len(return_list)
            mult_factor = draw_float / avg_of_list

        # Normalize x values proportionally compared to overall x value
        for sec in sids_to_process:
            sym    = sec.symbol
            analog = 0.
            value  = (b[sym]['return'] * mult_factor) - shift
            if value == 0:   # like 0.00
                analog = '0'
                continue
            if percent_results:
                if value < 10.:
                    analog = '{}%'.format(float('%.2f' % value))
                elif value < 100.:
                    analog = '{}%'.format(float('%.1f' % value))
                else:
                    analog = '{}%'.format(int(value))
            else:
                if value < 10.:
                    analog = 'x' + '%.2f' % value
                elif value < 100.:
                    analog = 'x' + '%.1f' % value
                else:
                    analog = 'x' + '%.0f' % value

            b[sym]['analog'] = analog

        # Set values
        for sec in sids_to_process:
            sym = sec.symbol
            init_price = b[sym]['init_price']
            if init_price:
                buy_hold = '%.1f' % ((b[sym]['price'] - init_price) / init_price)
                if buy_hold == '-0.0' or buy_hold == '0.0':
                    buy_hold = '0'
            else:
                buy_hold = '0'
            content = [
                sym,
                ' ' + str(b[sym]['analog']),
                buy_hold,
                str(b[sym]['count_buy']) + '|' \
                    + str(b[sym]['count_sell']),
                str(b[sym]['cnt_buy_evnts']) + '|' \
                    + str(b[sym]['cnt_sel_evnts']),
                '%.0f' % init_price + '|' + '%.0f' % b[sym]['price'],
                int(b[sym]['cash_low']),
                int(b[sym]['balance']),
                b[sym]['shares'],
                int(b[sym]['shares'] * b[sym]['price'])
            ]
            # Collect lines per sym as a list of lists
            contents_list.append(content)

        # Set widths
        col_widths = {}
        for i in range(len(contents_list[0])):
            col_widths[i + 1] = 7       # Defaults
        col_widths[1] = 6               # Symbol
        col_widths[3] = 6               # Buy|Hold
        for line_list in contents_list:
            ec = 1  # element count
            for element in line_list:
                if len(str(element)) > col_widths[ec]:
                    col_widths[ec] = len(str(element)) # Set width to largest seen.
                ec += 1

        # Piece together the output lines formatted.
        line_c = 0
        for line in contents_list:
            out_line = ''
            line_c  += 1  # Line count
            cc       = 1  # Column count
            for column in line:
                if cc in [4, 5, 6] or line_c in [1, 2]:
                    out_line += str(column).center(col_widths[cc] + 1)
                else:
                    column = str(column) + ' ' if cc == 3 else column
                    out_line += str(column).rjust(col_widths[cc] + 1)
                cc += 1

            out_content += (out_line + ' ' * (line_len- len(out_line)) + '\r\n')
            count_lines += 1

            # Backticks at the end of line are for replace-all in an editor
            #   later after copy/paste, since new lines are gone at least on Windows.
            #   Unfortunate to not be able to copy and paste results easily.

            # Decide when to tuck a group away for later and start a new group,
            #   due to logging limits, using modulus (remainder).
            if count_lines % lines_out == 0:
                out_content_collections.append(out_content)
                out_content = '_\r\n'       # Restart a group.

        if count_lines % lines_out != 0:    # A few remaining lines.
            out_content_collections.append(out_content)

        # Log output
        log.info(out_summary)   # The top, general overall output first

        # Log stored groups
        for occ in out_content_collections:
            log.info(occ)

        out_content = '(symbol ratios adjusted proportionally to overall)'.rjust(20)

        # Add any other content you want ---------------------------
        #out_content += '_\n' # Underscore to a new line for left alignment,
                              #   '\n' by itself would be ignored/dropped.
        # Some variables or whatever you might want to add ...
        out_content += ''

        log.info(out_content)
    
    
    
There was a runtime error.