Back to Community
Explanation of why results are so good

I am not sure how I have gotten a nearly 1000% yield in a year so I think my code is broken. Help please

Clone Algorithm
16
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 datetime
import pytz
import pandas as pd

def initialize(context):

    context.security = symbol('SPYG')

    set_commission(commission.PerTrade(cost=0.03))
    set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.1))
    
def handle_data(context, data):

    average_price = data[context.security].mavg(5)
    current_price = data[context.security].price
    

    cash = context.portfolio.cash
    
    # Here is the meat of our algorithm.
    # If the current price is 5% below the 5-day average price 
    # AND we have enough cash, then we will order.
    # If the current price is below the average price, 
    # then we want to close our position to 0 shares.
    if current_price > 0.95*average_price and cash > current_price:
        
        # Need to calculate how many shares we can buy
        number_of_shares = int(cash/current_price)
        
        # Place the buy order (positive means buy, negative means sell)
        order(context.security, +number_of_shares)
        log.info("Buying %s" % (context.security.symbol))
        

    elif current_price > average_price:

        
        # Place the buy order (positive means buy, negative means sell)
        order(context.security, 0)
        log.info("Selling %s" % (context.security.symbol))
    
    # You can use the record() method to track any custom signal. 
    # The record graph tracks up to five different variables. 
    # Here we record the Apple stock price.
    record(stock_price=data[context.security].price)
There was a runtime error.
5 responses

It looks like the algo keeps on buying shares even when you don't want it to. In this backtest I plotted the leverage used and it gets unrealistically high. You can prevent this using several different methods. I'll post a backtest that gives an example

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
import datetime
import pytz
import pandas as pd

def initialize(context):

    context.security = symbol('SPYG')

    set_commission(commission.PerTrade(cost=0.03))
    set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.1))
    
def handle_data(context, data):

    average_price = data[context.security].mavg(5)
    current_price = data[context.security].price
    

    cash = context.portfolio.cash
    
    # Here is the meat of our algorithm.
    # If the current price is 5% below the 5-day average price 
    # AND we have enough cash, then we will order.
    # If the current price is below the average price, 
    # then we want to close our position to 0 shares.
    if current_price > 0.95*average_price and cash > current_price:
        
        # Need to calculate how many shares we can buy
        number_of_shares = int(cash/current_price)
        
        # Place the buy order (positive means buy, negative means sell)
        order(context.security, +number_of_shares)
        log.info("Buying %s" % (context.security.symbol))
        

    elif current_price > average_price:

        
        # Place the buy order (positive means buy, negative means sell)
        order(context.security, 0)
        log.info("Selling %s" % (context.security.symbol))
    
    # You can use the record() method to track any custom signal. 
    # The record graph tracks up to five different variables. 
    # Here we record the Apple stock price.
    record(leverage=context.account.leverage)
There was a runtime error.

In this version, your trading logic is only executed once a day. This keeps the leverage at 1.

Clone Algorithm
1
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 datetime
import pytz
import pandas as pd

def initialize(context):

    context.security = symbol('SPYG')

    set_commission(commission.PerTrade(cost=0.03))
    set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.1))
    
def handle_data(context, data):
    exchange_time = get_datetime().astimezone(pytz.timezone('US/Eastern'))
    average_price = data[context.security].mavg(5)
    current_price = data[context.security].price
    

    cash = context.portfolio.cash
    
    # Here is the meat of our algorithm.
    # If the current price is 5% below the 5-day average price 
    # AND we have enough cash, then we will order.
    # If the current price is below the average price, 
    # then we want to close our position to 0 shares.
    if exchange_time.hour == 15 and exchange_time.minute == 55:
        if current_price > 0.95*average_price and cash > current_price:
        
            # Need to calculate how many shares we can buy
            number_of_shares = int(cash/current_price)
        
        # Place the buy order (positive means buy, negative means sell)
            order(context.security, +number_of_shares)
            log.info("Buying %s" % (context.security.symbol))
        

        elif current_price > average_price:

        
        # Place the buy order (positive means buy, negative means sell)
            order(context.security, 0)
            log.info("Selling %s" % (context.security.symbol))
    
    # You can use the record() method to track any custom signal. 
    # The record graph tracks up to five different variables. 
    # Here we record the Apple stock price.
    record(leverage=context.account.leverage)
There was a runtime error.

It's my first time using Quantopian, so I'm not really sure about this, but it looks like your cash is negative. Maybe, transactions takes some minutes to be done, so you keep buying shares, and when all the orders get executed your cash becomes negative. And because the performance is based on your initial capital, you get such a good performance.
When running this daily, your cash get updated before you can place a new order, and you get more reasonable results.

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
import datetime
import pytz
import pandas as pd

def initialize(context):

    context.security = symbol('SPYG')

    set_commission(commission.PerTrade(cost=0.03))
    set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.1))
    
def handle_data(context, data):

    average_price = data[context.security].mavg(1)
    current_price = data[context.security].price
    

    cash = context.portfolio.cash
    
    # Here is the meat of our algorithm.
    # If the current price is 5% below the 5-day average price 
    # AND we have enough cash, then we will order.
    # If the current price is below the average price, 
    # then we want to close our position to 0 shares.
    if current_price > 0.95*average_price and cash > current_price:
        
        # Need to calculate how many shares we can buy
        number_of_shares = int(cash/current_price)
        
        # Place the buy order (positive means buy, negative means sell)
        order(context.security, +number_of_shares)
        log.info("Buying %s" % (context.security.symbol))
        

    elif current_price > average_price:

        
        # Place the buy order (positive means buy, negative means sell)
        order(context.security, 0)
        log.info("Selling %s" % (context.security.symbol))
    
    # You can use the record() method to track any custom signal. 
    # The record graph tracks up to five different variables. 
    # Here we record the Apple stock price.
    record(stock_price=data[context.security].price)
There was a runtime error.

Here are the results by using a auxiliary cash variable.
But the regular cash variable still have a weird step behaviour I don't understand

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
import datetime
import pytz
import pandas as pd

def initialize(context):

    context.security = symbol('SPYG')

    set_commission(commission.PerTrade(cost=0.03))
    set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.1))
    
    context.real_cash = context.portfolio.cash
    
def handle_data(context, data):

    average_price = data[context.security].mavg(1)
    current_price = data[context.security].price
    

    cash = context.portfolio.cash
    record('cash',cash)
    
    # Here is the meat of our algorithm.
    # If the current price is 5% below the 5-day average price 
    # AND we have enough cash, then we will order.
    # If the current price is below the average price, 
    # then we want to close our position to 0 shares.real_cash
    if current_price > 0.95*average_price and cash > current_price:
        
        # Need to calculate how many shares we can buy
        number_of_shares = int(context.real_cash/current_price)
        
        # Place the buy order (positive means buy, negative means sell)
        context.real_cash -= number_of_shares * current_price
        if number_of_shares > 0:
            order(context.security, number_of_shares)
            log.info("Buying %s" % (context.security.symbol))
        

    elif current_price > average_price:

        
        # Place the buy order (positive means buy, negative means sell)
        order(context.security, 0)
        log.info("Selling %s" % (context.security.symbol))
    
There was a runtime error.

One problem is on the level of a typo, order(context.security, 0) where you intended to sell, it would buy or sell 0, oops, use order_target(context.security, 0) or order_target_percent(context.security, 0).
And then it looks like SPYG is thinly traded, lots of partial fills (both buy and sell).

Clone Algorithm
2
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 datetime
import pytz
import pandas as pd

def initialize(context):
    context.security = symbol('SPYG')

    set_commission(commission.PerTrade(cost=0.03))
    set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.1))
    
    # This triggers the summary at the end even if no trade last bar (skip of handle_data)
    schedule_function(func = summary, date_rule = date_rules.every_day())
    
def handle_data(context, data):
    #if get_open_orders():
    #    track_orders(context, data)
    #    summary(context, data) # accounting and last bar summary
    #    return
    
    average_price = data[context.security].mavg(5)
    current_price = data[context.security].price

    cash = context.portfolio.cash
    
    # Here is the meat of the algorithm.
    # If the current price is 5% below the 5-day average price 
    #   AND enough cash, then buy.
    # If the current price is below the average price, 
    #   then close position to 0 shares.
    if current_price > 0.95 * average_price and cash > current_price:
        # Need to calculate how many shares can buy
        number_of_shares = int(cash / current_price)
        
        # Place buy order (positive means buy, negative means sell)
        #log.info("Buying {} {}".format(number_of_shares, context.security.symbol))
        order(context.security, +number_of_shares)

    elif current_price > average_price * 1.02:
        # Place sell order (positive means buy, negative means sell)
        shares = context.portfolio.positions[context.security].amount
        if shares > 0:
            #log.info("Selling {} {}".format(shares, context.security.symbol))
            order_target(context.security, 0)
    
    # record() to track custom signals, up to five different variables. 
    # Here, the stock price and cash.
    #record(stock_price=data[context.security].price)
    # ** = kw or keyword args of dictionary, key: value, like 'SPYG price' = 85.36
    record(**{context.security.symbol + ' price': current_price})
    record(cash_over_100k = context.portfolio.cash / 100000) # bringing cash near price
    
    track_orders(context, data)
    summary(context, data) # accounting and last bar summary
    
def track_orders(context, data):  # Log orders created or filled.
    try:
        context.orders
    except:
        context.orders = {}
    
    to_delete = []
    for id in context.orders:
        o   = get_order(id)
        sec = o.sid
        sym = sec.symbol
        if o.filled:        # Filled at least some
            trade = 'Bot' if o.amount > 0 else 'Sold'
            log.info('    {} {} {} at {}\n'.format(
                    trade, o.filled, sym, data[sec].price))
            to_delete.append(o.id)
            
    for sec, oo_for_sid in get_open_orders().iteritems(): # Open orders
        sym = sec.symbol
        for o in oo_for_sid: # Orders per security
            if o.id in to_delete:
                continue
            context.orders[o.id] = 1
            if o.status == 2:   # Cancelled
                log.info('    Cancelled {} {} order\n'.format(
                        trade, o.amount, sym, data[sec].price))
            else:               # New
                trade = 'Buy' if o.amount > 0 else 'Sell'
                if o.limit:        # Limit order
                    log.info('  {} {} {} now {} limit {}\n'.format(
                            trade, o.amount, sym, data[sec].price, o.limit))
                elif o.stop:       # Stop order
                    log.info('  {} {} {} now {} stop {}\n'.format(
                            trade, o.amount, sym, data[sec].price, o.stop))
                else:              # Market order
                    log.info('  {} {} {} at {}\n'.format(
                            trade, o.amount, sym, data[sec].price))
    for d in to_delete:
        del context.orders[d]

def summary(context, data):
    '''
        Summary processing

        https://www.quantopian.com/posts/run-summary
    '''
    # Need a couple of imports, you might need to comment these out if already imported.
    #   That's pretty much the only change that might be necessary.
    from pytz import timezone
    import re

    # Yes try/except is narly yet makes to work with set_universe etc.
    # Is there a better way?  An -| if 'books' in context: |- didn't work.
    try:
        context['books']    # See if this key exists yet.
        b = context.books   # For brevity.
    except:
        '''
            Preparation. Initialize one time.
        '''
        cash = context.portfolio.starting_cash
        context.books = {   # Starting cash value from GUI or live restart...
            'cash'          : cash,
            'init_cash'     : cash,
            'cash_low'      : cash,
            'shares'        : 0,
            'shares_value'  : 0,
            '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,
            'costs_total'   : 0,       # Commissions.
            'sids_seen'     : [],      # For set_universe since dynamic.
            'prep_prnt'     : '',
            'orders'        : {},      # Keep orders for accounting,
        }                              #   orders not completely filled yet.

        b = context.books

        # Environment   First/last dates and
        #   Arena: backtest or live.  Mode: daily or minute.
        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']
        b['mode']  = env['data_frequency']

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

        # Show environment at the beginning of the run
        b['prep_prnt'] = ' {}\n  {}  {} to {}  {}  {}\n'.format(
            b['arena'],
            b['mode'],
            b['first_trading_date'],
            b['last_trading_date'],
            '   $' + "%.0f" % b['cash'],
            '  First bar stocks ({}) ...'.format(len(data)),
        )

        # Show current universe once
        for sec in data:
            if isinstance(sec, basestring):
                continue   # Skip any injected fetcher string keys.
            b['prep_prnt'] += (sec.symbol + ' ')
        log.info(b['prep_prnt'])

    '''
        Prepare individual securities dictionaries
          with dynamic set_universe, fetcher, IPO's appearing etc.
    '''
    for sec in data:
        if isinstance(sec, basestring):
            continue   # Skip any injected fetcher string keys.
        sym = sec.symbol
        if sym in b:
            continue
        if sec not in b['sids_seen']:
            # Scenarios with price missing ...
            price = data[sec].price if 'price' in data[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,
            }

    '''
        Accounting. Update the numbers, manage orders if any.
    '''
    accounting = {} # Locally, any orders ready to be counted.
    
    # Read open orders
    for security, oo_for_sid in get_open_orders().iteritems():
        sym = security.symbol
        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

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

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

            # 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.
            # ToDo: Not sure of official actual fill prices.
            if o.filled != o.amount:
                cancel_order(id)  # You might want to change/remove this.

    # Do any accounting, into books{}
    for id in accounting:
        sec             = accounting[id]['sid']
        sym             = sec.symbol
        commission      = accounting[id]['commission']
        filled          = accounting[id]['filled']  # Number filled, sell neg.
        if sec in data and 'price' in data[sec]:    # Update if available.
            b[sym]['price'] = data[sec].price
        lkp             = b[sym]['price']           # Last known price.
        transaction     = filled * lkp

        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['costs_total']  += 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)

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

        # Keep track of lowest cash per symbol
        if b[sym]['balance'] < b[sym]['cash_low']:
            b[sym]['cash_low'] = b[sym]['balance']

        # And overall
        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 b['cash_low'] < 0:   # Lowest cash points reached ...
                log.info(str(sym).ljust(5) \
                    + ' order for ' + (('$' + "%.0f" % transaction) \
                    + ',').ljust(8) + ' cash low: ' + str(int(b['cash_low']))
                )
    '''
        Show summary if this is the last bar
    '''
    last_bar_now = 0

    if not b['summary_print']:
        if context.books['arena'] == 'live':
            # When paper/live print summary every day end of day
            last_bar_now = 1
        elif context.books['arena'] == 'backtest':
            # Flag for summary output if last bar now
            bar = get_datetime()
            if b['last_trading_date'] == str(bar.date()):
                if b['mode'] == 'daily':
                    last_bar_now = 1
                elif b['mode'] == 'minute':
                    if b['last_trading_time'] == str(bar.time()):
                        last_bar_now = 1

    if last_bar_now or b['summary_print']:
        '''
            Summary output to the logging window
        '''
        # 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, and its price
            #   is no longer accessible. Bad. Need help from Q.
            if sec in data and 'price' in data[sec]:
                b[sec.symbol]['price'] = data[sec].price

            sym    = sec.symbol
            shares = b[sym]['shares']
            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']        += shares
            b['shares_value']  += (shares * b[sym]['price'])
            done[sec] = 1

        q__portfolio  = str(int(context.portfolio.portfolio_value))
        cash_end      = context.portfolio.cash
        init_cash     = b['init_cash']
        avg_init_cash = init_cash / len(b['sids_seen'])
        cash_low      = b['cash_low']
        my_portfolio  = cash_end + b['shares_value']
        cash_profit   = cash_end - b['init_cash']
        xval          = 'x0'
        max_spent     = init_cash - cash_low
        drawdown      = max(init_cash, init_cash - cash_low)
        cnt_b_evts    = ('  (' + str(b['cnt_buy_evnts']) + ' trades)').rjust(17)
        cnt_s_evts    = ('  (' + str(b['cnt_sel_evnts']) + ' trades)').rjust(17)
        untouchd      = '' if int(cash_low) <= 0 else \
                        '  (' + str(int(cash_low)) + ' unused)'
        neg_cash      = '' if int(cash_low) >= 0 else '                       ' \
                            + "%.0f" % cash_low + ' max negative cash'
        if drawdown  != 0:               # Pure profit over input used.
            xval      = 'x'  + "%.3f" % ((my_portfolio - init_cash) / drawdown)

        w1 = 16; w2 = 8  # Widths of columns
        outs = [
            '  QPortfolio: '.rjust(w1)+('$'+str(q__portfolio)) .rjust(w2),
            '   Buy Count: '.rjust(w1)+str(b['count_buy'])     .rjust(w2)+cnt_b_evts,
            '  Sell Count: '.rjust(w1)+str(b['count_sell'])    .rjust(w2)+cnt_s_evts,
            '  Shares Now: '.rjust(w1) + str(b['shares'])      .rjust(w2),
            'Shares Value: '.rjust(w1) + str(int(b['shares_value'])).rjust(w2),
            '    Cash Now: '.rjust(w1) + str(int(cash_end))         .rjust(w2),
            ' Cash Profit: '.rjust(w1) + str(int(cash_profit))      .rjust(w2),
            ' Commissions: '.rjust(w1) + str(int(b['costs_total'])) .rjust(w2),
            '   Max Spent: '.rjust(w1) + str(int(max_spent))        .rjust(w2)+neg_cash,
            'Initial Cash: '.rjust(w1) + str(int(init_cash))        .rjust(w2)+untouchd,
            '   Portfolio: '.rjust(w1)+('$'+str(int(my_portfolio))) .rjust(w2),
        ]
        out  = '_\r\n'
        for o in outs:
            out += (o + '\r\n')
        out += '        Return:  ' + xval + '   Profit/Drawdown\r\n'

        # -------------------------------
        # Individual securities detail
        # -------------------------------
        out_content_collections = []
        count_sids  = len(b['sids_seen'])
        sec_word    = ' security' if count_sids == 1 else ' securities'
        out_content = '_      ' + "%.0f" % int(b['init_cash'] / count_sids) \
                + ' average initial cash, ' + str(count_sids) + sec_word + '\r\n'
        lines_out   = 11    # Log in clumps to stay under logging limits.
        count_lines = 0
        col_widths  = {1: 8, 2: 7, 3: 7, 4: 12, 5: 8, 6: 8, 7: 9, 8: 9, 9: 8, 10: 9}
        header1 = [
        '',     'Return','Buy|','By|Sl','By|Sl', 'Price',  'Max', 'Cash','Shrs','Shrs'
        ]
        header2 = [
        'Symbol','Ratio','Hold','Count','Evnts','Strt|Now','Spent','Now','Now', 'Value'
        ]

        cc = 1  # Column count
        for h in header1:
            out_content += h.center(col_widths[cc])
            cc += 1
        out_content += '~\r\n' # Tilde at the end of line 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.

        count_lines += 1
        cc = 1
        for h in header2:
            out_content += h.center(col_widths[cc])
            cc += 1
        out_content += '~\r\n'
        count_lines += 1

        for sym in sorted(s.symbol for s in b['sids_seen']):
            balance      = b[sym]['balance']
            init_price   = b[sym]['init_price']
            shares       = b[sym]['shares']
            shares_value = shares * b[sym]['price']
            buy_hold     = 0.0
            xval         = 'x0'
            max_spent    = abs(b[sym]['cash_low'])
            drawdown     = min( avg_init_cash, abs(b[sym]['cash_low']) )
            if drawdown != 0:
                portf = balance + shares_value
                xval  = 'x' + "%.1f" % ((portf - drawdown) / drawdown)
                if xval == 'x-0.0' or xval == 'x0.0':  # Mainly clearing -0.0
                    xval = 'x0'    # -0.0 would have been something like -0.02
            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'
            content = [
                sym,
                xval,
                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'],
                "%.0f" % max_spent,
                "%.0f" % balance,
                shares,
                int(shares_value)
            ]
            cc = 1
            for c in content:
                out_content += str(c).center(col_widths[cc])
                cc += 1
            out_content += '~\r\n'
            count_lines += 1

            # Decide when to tuck a group away for later and
            #    start a new group, 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.info(out)        # The top, general overall output first.

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

        # 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.