Back to Community
Run Summary

A way to print a summary at end of run or status on a specified date, code in the next post.
[Edit: Earlier code removed, added a later better version instead, grab that one]

Example

16 responses

Wow! thanks Gary, I'll be adding this to the quantopian framework I made. https://github.com/Novaleaf/QuantShim

Much improved in part thanks to this.

Clone Algorithm
60
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
'''             ------------------------------------
                           Summary Print
                ------------------------------------

 o  Prints summary (or 'status') on last bar (or date specified)
 o  Can automatically determine last bar
 o  Daily or minutely
 o  'x' return value excludes any cash not used
 o  Details for each security in a set
 o  Easy to implement and tailor

Quick Overview:
    import some stuff
    def initialize(context):
        [ your code ]
        summary(context, 'prep', 'auto')

    def handle_data(context, data):
        [ your code ]
        summary(context, 'accounting', get_datetime(), data)

    def summary(context, action, last_bar = 'auto', dat = None):
        [ Accounting, prep, last bar check and print when the time comes ]

Instructions:
a) Replace the buy/sell logic in handle_data() with your own or
b) Copy/paste:
    i. The imports
   ii. The one summary() line in each of initialize() and handle_data()
  iii. def summary() -- Drop that whole thing into your algo at the end.
  Note: If you have your own 'return' in handle_data()
         you'll probably also need a copy of the summary() call before that.
        Replace 'auto' with your own date if you want to.

A lot of this code will become unnecessary when Quantopian releases
   a run-once-at-the-end wrap_up() function for us, to round out
   initialize() and handle_data().
   (It is on their list I heard, you might want to encourage them ;)

Partial wish list for improvements and streamlining:
    Import fewer modules.
    User list of dates (and/or minutes), print status on all of them.

Send me a message via Quantopian.
    --Gary Hawkins  6-2014
      https://www.quantopian.com/users/52b0e0e0271a27fc88000049

If you improve/streamline this pls consider posting. Thx.
'''

import re
import datetime as dt   # Surprising that datetime doesn't have timezone
import pandas as pd
from pytz import timezone
from zipline.utils import tradingcalendar

def initialize(context):
    context.stocks = [  # a few Buffett stocks and others
        symbol('AAPL'),
        symbol('ALK'),
        symbol('TSLA'),
        symbol('AXP'),
        symbol('BK'),
        symbol('CBI'),
        symbol('COP'),
        symbol('COST'),
        symbol('DE'),
        symbol('DTV'),
        symbol('DVA'),
        symbol('MA'),
        symbol('MCO'),
        symbol('MEG'),
        symbol('MTB'),
        symbol('NOV'),
        symbol('PG'),
        symbol('PSX'),
        symbol('SNY'),
        symbol('TMK'),
        symbol('XOM'),
        symbol('BAC'),
        symbol('CSCO'),
        symbol('AAL'),
        symbol('AA'),
        symbol('AKS'),
        symbol('C'),
        symbol('DOW'),
        symbol('AMD'),
        symbol('APT'),
        symbol('F'),
        symbol('BABA'),
    ]

    # ------------------------
    # Summary preparation
    # ------------------------
    # Optional 3rd arg like 'auto' or '2012-05-22' as latest or in the past.
    summary(context, 'prep', 'auto')

def handle_data(context, data):
    context.stocks = [sid for sid in data]

    for sec in context.stocks:

        # Buy/Sell logic borrowed/adapted
        # from quantopian.com/posts/less-is-more-small-and-simple-mixed-strategies
        # Not important here, relace with your own.

        ma_fast = data[sec].mavg(22)
        ma_slow = data[sec].mavg(44)
        buy_num = int(.91 * context.books[sec.symbol]['cash'] / data[sec].price)

        if ma_fast > ma_slow * .95 and context.books[sec.symbol]['cash'] > 0 \
          and (get_datetime().month <= 6 or get_datetime().month >= 9):
            order(sec, buy_num)             # Buy
        elif 4 < get_datetime().month < 9 \
          and data[sec].price > ma_fast * 1.1:
            order_target_percent(sec, 0)    # Sell

    # ------------------------
    # Summary updating
    # ------------------------
    summary(context, 'accounting', get_datetime(), data)

# ------------------------------------
# Summary processing
# ------------------------------------
def summary(context, action, last_bar = 'auto', dat = None):
    if action == 'accounting':  # Update books if an order occurred
        # ---------------------------------------
        # Update the numbers, manage orders
        # ---------------------------------------
        for sec in context.stocks:    # Need price always up-to-date
            context.books[sec.symbol]['price'] = dat[sec].price
            
        all_open_orders = get_open_orders()
        if all_open_orders:
            for security, oo_for_sid in all_open_orders.iteritems():
                sym   = security.symbol
                # price is the only use for the 4th arg, dat, or data
                price = context.books[sym]['price']
                for order_obj in oo_for_sid:
                    num = order_obj.amount    # Number of shares
                    transaction = num * price #   is negative for sell

                    context.cash -= transaction    # buy or sell
                    if num > 0:     # Buy
                        context.books[sym]['cnt_buy_evnts'] += 1
                        context.books[sym]['count_buy']     += num
                        context.books[sym]['shares']        += num
                        context.books[sym]['cash']          -= transaction
                    elif num < 0:    # Sell
                        context.books[sym]['cnt_sel_evnts'] += 1
                        context.books[sym]['count_sell']    += abs(num)
                        context.books[sym]['shares']        -= abs(num)
                        context.books[sym]['cash']          += abs(transaction)
                    else:
                        continue    # to next in for loop

                    spent = context.books[sym]['init_cash'] - context.books[sym]['cash']
                    if spent > context.books[sym]['max_spent']:
                        context.books[sym]['max_spent'] = spent

                # Just once, set init_price, save for summary
                if not context.books[sym]['init_price']:
                    context.books[sym]['init_price'] = price

            # Overall keep track of lowest cash point
            cash = context.cash
            if cash < context.cash_low:
                if cash < 0:
                    print ".\n\n\n   Negative cash !\n      " + sym + \
                      '   csh ' + str(cash) + '  num ' + str(num) + "\n\n\n\n."
                context.cash_low = cash

        summary(context, 'last_check', last_bar)

    elif action == 'last_check':    # Also sets up daily or minutely
        # ---------------------------------------
        # Mode and last bar trigger
        # ---------------------------------------
        # Set whether daily or minutely one time.
        if context.mode is None:
            str_raw_dt = str(get_datetime())
            # Daily always with time zeros like 2014-05-30 00:00:00+00:00
            if '00:00:00+00:00' in str_raw_dt:
                context.mode              = 'daily'
                context.last_trading_time = '00:00:00+00:00'
                context.prep_prnt        += '  DAILY mode'
            else:
                context.mode              = 'minutely'
                context.last_trading_time = '16:00:00'
                context.prep_prnt        += '  MINUTE mode'
                # Weak due to early close days, holidays...
                # see among others http ...
                #    the-awesome-algorithm-trade-at-the-end-of-the-day

            context.prep_prnt += ('\n    Last trading bar: ' + \
              context.last_trading_date + ' ' + \
              context.last_trading_time + '   ' + context.last_bar + '\n')

            log.info(context.prep_prnt)
            return

        # Note about this last bar stuff (3rd arg when calling summary()):
        # The variable last_bar is being used two ways and can be confusing.
        # From initialize() it means only date to be considered last, end of run
        #    to match the GUI calendar date, or automatically last trading day.
        # From handle_data() it is the date *and time* associated with the
        #    data currently being processed, changing each time thru.

        # Do summary output if last bar now
        # Daily    - like 2014-01-02 00:00:00+00:00
        # Minutely - like 2014-01-02 09:31:00-05:00
        date = str(last_bar.date())
        time = str(last_bar.time())
        if context.mode == 'daily' \
                  and context.last_trading_date == date:
            summary(context, 'print')
        elif context.mode == 'minutely' \
                  and context.last_trading_date == date \
                  and context.last_trading_time in time:
            summary(context, 'print')

    elif action == 'prep':
        # ---------------------------------------
        # Preparation. Initialize one time.
        # ---------------------------------------
        context.cash          = context.portfolio.cash  # Starting value from GUI
        context.init_cash     = context.cash
        context.cash_low      = context.cash
        context.prep_prnt     = 'Summary Prep '
        context.shares        = 0
        context.shares_value  = 0
        context.count_buy     = 0     # Overall buy count, number of shares
        context.count_sell    = 0     # Overall sell count
        context.cnt_buy_evnts = 0     # Overall buy events count
        context.cnt_sel_evnts = 0
        context.books         = {}    # For individual securities
        context.mode          = None  # To become 'daily' or 'minutely'
        context.last_bar      = last_bar if last_bar == 'auto' else ''

        for sec in context.stocks:     # Initialize dictionary
            init_csh = int(context.cash / len(context.stocks))
            context.books[sec.symbol] = {
                'init_price'   : 0,    # Save for summary
                'max_spent'    : 0,
                'price'        : 0,
                'shares'       : 0,
                'count_buy'    : 0,
                'count_sell'   : 0,
                'cnt_buy_evnts': 0,    # Individual buy events count
                'cnt_sel_evnts': 0,
                'init_cash'    : init_csh,
                'cash'         : init_csh,  # For individual 'x' return
                'cash_low'     : init_csh
            }

        # Last date/time
        # https://www.quantopian.com/posts/ ...
        # ... how-to-detect-the-last-bar-of-a-backtest-for-generating-a-summary-report
        a = re.findall('period_end=(.*),', str(dir))
        b = pd.to_datetime(a[0]).tz_localize('UTC')
        c = tradingcalendar.open_and_closes
        d = c.loc[c.index<=b.date().isoformat(),'market_close'].iloc[-1]
        context.last_trading_date = str(d.astimezone(timezone('US/Eastern')).date())
        context.last_trading_time = str(d.astimezone(timezone('US/Eastern')).time())

        if last_bar != 'auto':  # User has set the date manually
            context.prep_prnt += '    Date manual override...\n'
            context.last_trading_date = last_bar

    elif action == 'print':
        # ---------------------------------------
        # Print summary at date specified
        # ---------------------------------------
        # Some overall values by adding individual values
        for sec in context.stocks:
            sym = sec.symbol
            context.count_buy     += context.books[sym]['count_buy']
            context.count_sell    += context.books[sym]['count_sell']
            context.cnt_buy_evnts += context.books[sym]['cnt_buy_evnts']
            context.cnt_sel_evnts += context.books[sym]['cnt_sel_evnts']
            context.shares        += context.books[sym]['shares']
            context.shares_value  += \
                      (context.books[sym]['shares'] * context.books[sym]['price'])

        col_width    = 9
        shares_value = context.shares_value
        my_portfolio = context.cash      + shares_value
        cash_profit  = context.cash      - context.init_cash
        max_spent    = context.init_cash - context.cash_low
        multiplier   = 0
        if max_spent != 0:
            multiplier = my_portfolio / max_spent
        multiplier = 'x' + str("%.3f" % multiplier) # Output over input used.
        cnt_b_evts = ' (' + str(context.cnt_buy_evnts) + ' events)'
        cnt_s_evts = ' (' + str(context.cnt_sel_evnts) + ' events)'
        out  = ' Last trading bar: ' + context.last_trading_date + \
                    ' ' + context.last_trading_time + '\n'
        out += '      QPortfolio:  ' + str(int(context.portfolio.portfolio_value)) + '\n'
        out += '    Initial Cash: $' + str("%.2f" % context.init_cash) + '\n'
        out += '       Max Spent:  ' + str(int(max_spent))     + '\n'
        out += '       Buy Count:  ' + str(context.count_buy)  + cnt_b_evts + '\n'
        out += '      Sell Count:  ' + str(context.count_sell) + cnt_s_evts + '\n'
        out += '      Shares Now:  ' + str(context.shares)     + '\n'
        out += '    Shares Value:  ' + str(int(shares_value))  + '\n'
        out += '        Cash Now:  ' + str(int(context.cash))  + '\n'
        out += '     Cash Profit:  ' + str(int(cash_profit))   + '\n'
        out += '       Portfolio: $' + str(int(my_portfolio))
        if int(context.cash_low) != 0:
            out += ' (untouched ' + str(int(context.cash_low)) + ' dollars omitted)'
        out += '\n'
        out += '        Return:  ' + multiplier + '  (Output/Input, Portfolio/Max Spent)\n'

        # -------------------------------
        # Individual securities detail
        # -------------------------------
        out_content_collections = []
        out_content = '_\n'
        lines_out   = 12
        count_lines = 0

        header1 = [
            '',
            'Return',
            'Buy|',
            'By|Sl',
            'By|Sl',
            'Max',
            'Price',
            'Shrs',
            'Shrs'
        ]
        header2 = [
            'Symbol',
            'Ratio',
            'Hold',
            'Count',
            'Evnts',
            'Spent',
            'Strt|Now',
            'Now',
            'Value'
        ]
        for h in header1:
            out_content += h.center(col_width)
        out_content += '\n'
        count_lines += 1
        for h in header2:
            out_content += h.center(col_width)
        out_content += '\n'
        count_lines += 1

        for sec in context.stocks:
            sym          = sec.symbol
            shares       = context.books[sym]['shares']
            shares_value = shares * context.books[sym]['price']
            xval         = 'x0'
            if context.books[sym]['max_spent']:
                xval = 'x' + str("%.1f" % (
                  (context.books[sym]['cash'] \
                       + shares_value) / context.books[sym]['max_spent']
                ))
            if context.books[sym]['init_price']:
                buy_hold = context.books[sym]['price'] \
                              / context.books[sym]['init_price']
            else:
                buy_hold = 0
            content = [
                sym,
                xval,
                "%.1f" % buy_hold,
                str(context.books[sym]['count_buy']) + '|' \
                    + str(context.books[sym]['count_sell']),
                str(context.books[sym]['cnt_buy_evnts']) + '|' \
                    + str(context.books[sym]['cnt_sel_evnts']),
                int(context.books[sym]['max_spent']),
                str("%.0f" % context.books[sym]['init_price']) + '|' \
                    + str("%.0f" % context.books[sym]['price']),
                shares,
                int(shares_value)
            ]
            for c in content:
                out_content += str(c).center(col_width)
            out_content += '\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 = '_\n'    # restart a group

        if count_lines % lines_out != 0:    # few 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)


There was a runtime error.

Hi Gary, Jason, and all,

Keep in mind that the 'dir' hack is probably not officially supported (I'm guessing). Since it is not part of the user interface, it could change and one day the code above would not work.

Quantopian support team, is there a better approach that could be put on the roadmap for incremental improvements?

Grant

We recently made dir(obj) available in the IDE to see an object's attributes, and we're definitely planning to keep that functionality. But the string representation of the dir function itself, as opposed to the value returned by calling it, could certainly change in the future.

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

Hello Alisa,

Is there any way to get the value of 'last_close' without applying the hack?

Grant

Grant, I think your version is the easiest (though not convenient!) route right now. I imagine we will make this process easier with new tools that we add.

Hello Gary,

One way to tell if you are backtesting is to measure the delay between calls to handle_data in real time (not simulation time). I figure that if you are backtesting, the time difference between calls should be less than 50 seconds. Under paper/live trading, the time difference should be approximately 60 seconds.

Grant

from datetime import datetime

def initialize(context):  
    context.spy = symbol('SPY')  
    context.prior_call = datetime.now()  
    context.first_call = False # set to True after first call to handle_data  
    context.backtest = None # True for backtest, False for paper/live  
def handle_data(context, data):  
    now = datetime.now()  
    dt = (now - context.prior_call).total_seconds()  
    context.prior_call = now  
    if not context.first_call:  
        context.first_call = True  
        return  
    if dt < 50:  
        context.backtest = True  
    else:  
        context.backtest = False  
    print dt  
    print context.backtest  
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
from datetime import datetime

def initialize(context):
    context.spy = symbol('SPY')
    
    context.prior_call = datetime.now()
    
    context.first_call = False # set to True after first call to handle_data
    
    context.backtest = None # True for backtest, False for paper/live
 
def handle_data(context, data):
    
    now = datetime.now()
    dt = (now - context.prior_call).total_seconds()
    context.prior_call = now
    
    if not context.first_call:
        context.first_call = True
        return
    
    if dt < 50:
        context.backtest = True
    else:
        context.backtest = False
        
    print dt
    print context.backtest
There was a runtime error.

Here's an improved version that is now even easier to drop into your algorithm. Check it out, clone and run it quickly now, it's fast.

  • Run Summary now works with set_universe too and also fetcher's universe_func.
  • Accounting on bar after fill for more accuracy.
  • Self-contained, all variables within context.books.
  • Simplified. And fewer modules.
  • Only needs the function and one call to it in handle_data().
  • Detects backtest vs live/paper and daily vs minute.
  • Currently set to log summary at the end of every day when live/paper.

Takes only seconds to add to your algo, note though in handle_data be sure to place the call to summary above any 'return' or could be prevented from printing summary info on the last bar.

Just ignore all of initialize() and everything except the call to summary in handle_data onward (the strategy and graph you see at some 25000% was just sort of for the fun of it).
By the way I'm still not certain on what to do with partial fills, currently the original order is cancelled afterward however it seems another separate order already exists for the remainder.
I have tested this and worked thru some bugs by cloning over 50 of your algo's and dropping this summary into them, and yet it surely has a lot of room for improvement so ...
Anyone is invited to feel free to improve on it.
Currently uses the interim method for environment info and so I plan to replace this when the new environment method arrives, thank you.

Clone Algorithm
49
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
'''             ------------------------------------
                            Run Summary
                ------------------------------------
            https://www.quantopian.com/posts/run-summary

To enable this summary all you need is:
  - the summary function and
  - one call to that function at the end of handle_data
                                (or before any return)
Like this:

def handle_data(context, data):

    summary(context, 'accounting', data) # Copy, say, line 102 onward

def summary(context, action, data):
    [ Accounting, prep, last bar check and print when called for ]

'''

def initialize(context):
    set_symbol_lookup_date('2014-11-21')

    # x11.129 only                2012-09-17 to 2014-11-20
    #context.stocks = symbols('JASO', 'RDNT', 'NVAX')

    # x70.243                     2012-09-17 to 2014-11-20
    #context.stocks = symbols('ACHN', 'JASO', 'NVAX', 'RDNT', 'HGSH')

    # Here, a small amount of negative cash yet high percentage compared to
    #   initial cash causes the output to not be 288__% (GUI) where the
    #   negative cash is ignored, instead, accounting for that negative cash,
    #   the real output in the opinion of this code is 17286%, or x172...
    # x172.858                    2012-09-17 to 2014-11-20
    context.stocks = symbols('ACHN', 'NVAX', 'RDNT', 'EDAP', 'HGSH')

    # x202.520 $10K=>$3.7M sheesh 2012-09-17 to 2014-11-20 some negative cash...
    #context.stocks = symbols('ACHN', 'RDNT', 'EDAP', 'HGSH')

    # x224.559 $10K=>$2.2M        2012-09-24 to 2014-11-20 however some negative cash...
    #context.stocks = symbols('ACHN', 'RDNT', 'HGSH')

    #set_universe(universe.DollarVolumeUniverse(99.90, 100.0))

    #set_slippage(slippage.FixedSlippage(spread=0.00)) # Zero slippage
    # Default slippage if not specified...
    #set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.1))

    # Not certain about this, might apply for some traders
    set_commission(commission.PerShare(cost=0.005, min_trade_cost=1.00))

    context.buys = {} # You won't need this

def handle_data(context, data):
    '''
        Please ignore all of this strategy and replace with your own.
          (Merely here for some buying and selling to work with)

          Some Buy/Sell logic changed/borrowed/adapted from
            http://quantopian.com/posts/less-is-more-small-and-simple-mixed-strategies

        Disclaimer:
          The strategy below is not serious as is, I think it
           serves as a beautiful example of overfitting, tweaked to the hilt,
           and look-ahead / winning stocks, I know that, so no one need mention it,
           the high return is not real-world, it is kind of for the fun of it.
           Again it is mainly here for buy/sell action, plus high numbers are cool.
           The strategy isn't the point, just recreation here.
           On the other hand if you have the patience for seasonal trading, who knows.
    '''
    for sec in data:
        sym   = sec.symbol
        date  = str(get_datetime().date())
        month = get_datetime().month

        # To avoid ordering on top of another order
        if sym in context.buys and context.buys[sym] == date:
            continue

        # Buy/Sell within certain limits
        shares     = context.portfolio.positions[sec].amount
        cost_basis = context.portfolio.positions[sec].cost_basis
        cash_each  = int( context.portfolio.cash / len(data.keys()) )
        buy_num    = int( cash_each / data[sec].price )
        ma_fast    = data[sec].mavg(16)
        ma_slow    = data[sec].mavg(36)

        if ma_fast < ma_slow * .95 and buy_num > 0 \
          and (month <= 5 or month >= 9):
            context.buys[sym] = date    # Record the date

            order(sec, buy_num)             # Buy

        elif data[sec].price > cost_basis * 1.1 and shares \
          and 4 < month < 9:

            order_target_percent(sec, 0)    # Sell
    '''
        End of that overfitting example, now on to the summary...
    '''

    # ------------------------
    # Summary updating
    # ------------------------
    summary(context, 'accounting', data)

    # Try uncommenting this, it can be eye-opening.
    #summary(context, 'print') # Print every bar if you want to, with this,
    # or with other code added once a week etc, or place under accounting
    # within summary to produce the summary after every transaction, 
    # in that case just summary(context, 'print')

def summary(context, action, data = None):
    '''
        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

    # This try/except is narly yet makes to work with set_universe.
    # Is there a better way?  An -| if 'books' in context: |- didn't work.
    try:
        context['books']    # See if this key exists yet.
    except:
        '''
            Preparation. Initialize one time.
        '''
        context.books = {   # Starting cash value from GUI or live restart...
            'cash'          : context.portfolio.cash,
            'init_cash'     : context.portfolio.cash,
            'cash_low'      : context.portfolio.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,
            'costs_total'   : 0,       # Commissions
            'sids_seen'     : [],      # For set_universe since dynamic.
            'arena'         : None,    # To become 'backtest' or 'live'.
            'mode'          : None,    # To become 'daily' or 'minute'.
            'prep_prnt'     : '',
            'init'          : {
                'start_date': 0,
            },
            'orders'        : {        # Keep orders for accounting.
                'open' : {},           # Orders not completely filled yet.
                'syms' : {},           # Dict of symbols with open orders,
            },                         #   can be used to not order over another.
            'first_trading_date' : str(get_datetime().date()),
        }

        b = context.books   # For brevity

        # First/last dates, early closes not handled.
        # Arena: backtest or live. Mode: daily or minute.
        try:
            # Until soon with official environment info, init_cash from
            #   context.portfolio.cash can be wrong if an order happened on first bar.
            #   str(sid) is a hack and will break, is just a stop-gap.
            b['init_cash'] = int(float(re.findall('capital_base=(.*?),', str(sid))[0]))
            b['cash_low']  = b['init_cash']
            b['cash']      = b['init_cash']

            # Prep some environment info
            b['last_trading_date'] = re.findall('period_end=(.*?) ', str(sid))[0]
            arena     = re.findall('Live|Simulation', str(sid))[0]
            b['mode'] = re.findall('data_frequency=(.*?),', str(sid))[0]
            
            if arena == 'Live':
                b['arena'] = 'live'
            elif arena == 'Simulation':
                b['arena'] = 'backtest'

            if b['mode'] == 'daily':
                b['last_trading_time'] = '00:00:00'
            else:
                b['last_trading_time'] = '16:00:00'
        except:
            log.info('Error in str(sid), summary will not print.')
            b['last_trading_date'] = 'unknown'
            b['arena'] = 'Arena unknown'
            b['mode']  = 'Mode unknown'

        # Show environment at the beginning of the run
        msg  = ' {0:s}\n  {1:s}  {2:s} to {3:s}  {4:s}  {5:s}\n'
        b['prep_prnt'] = msg.format(
            b['arena'],
            b['mode'],
            b['first_trading_date'],
            b['last_trading_date'],
            '   $' + "%.0f" % b['cash'],
            '  First bar stocks...',
        )

        # 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'])

        summary(context, 'prep_syms', data)

    if action == 'prep_syms':
        '''
            Prepare individual securities.
            Can run more than once, with dynamic set_universe, fetcher, IPO's appearing etc.
        '''
        if not data:  # Accommodate possible fetcher scenarios that can cause an
            return    #   error when start date before any universe_func sid appears.

        # Initialize individual symbols dictionaries
        b = context.books
        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']:
                b['sids_seen'].append(sec) # Next, scenarios with price missing...
                price = data[sec].price if 'price' in data[sec] else 0
                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,
                }

    elif action == 'accounting': # Update books if an order occurred.
        '''
            Update the numbers, manage orders
        '''
        b = context.books

        # Init if necessary and some filtering
        for sec in data:
            if isinstance(sec, basestring):
                continue   # Skip any injected fetcher string keys.
            sym = sec.symbol
            if sym not in b:
                summary(context, 'prep_syms', data)
            b[sym]['price'] = data[sec].price if 'price' in data[sec] else 0
        '''
        books['orders'] dict lives beyond summary (more than one bar).
        accounting{} is local, used after order filled.
        '''
        #
        # Any time an order 'filled' value changes, account for it.
        #
        accounting  = {}
        open_orders = get_open_orders()  # Read open orders

        for security, oo_for_sid in open_orders.iteritems():
            sym = security.symbol
            for order_obj in oo_for_sid:
                # Convenience option to be able in handle_data to
                #   avoid ordering if an order already exists.
                b['orders']['syms'][sym] = 1

                # If an order not seen before, add for tracking
                if order_obj.id not in b['orders']['open']:
                    b['orders']['open'][order_obj.id] = order_obj.filled

        # Take a look at current orders
        for id in b['orders']['open']:
            o = get_order(id) # Q copy now is the thing to trust, 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:
            sym         = accounting[id]['sid'].symbol
            lkp         = b[sym]['price']           # Last known price.
            filled      = accounting[id]['filled']  # Number filled, sell neg.
            transaction = filled * lkp
            commission  = accounting[id]['commission']

            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
            if sym in b['orders']['syms']:    # There's a scenario in multiple buys
                del b['orders']['syms'][sym]  #   where this key could be gone.
            del b['orders']['open'][id]

            # Overall keep track of lowest cash point
            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']))
                    )

            # And per symbol
            if b[sym]['balance'] < b[sym]['cash_low']:
                b[sym]['cash_low'] = b[sym]['balance']

            # Try uncommenting this for summary after every
            #   transaction accounting, can be interesting.
            #summary(context, 'print', data)

        summary(context, 'last_check', data)

    elif action == 'last_check':
        '''
            Find out if this is the last bar
        '''
        b = context.books

        if context.books['arena'] == 'live':
            # When paper/live print summary every day end of day
            time_of_day = str(get_datetime().astimezone(timezone('US/Eastern')).time())
            if context.books['last_trading_time'] == time_of_day:
                summary(context, 'print')
        elif context.books['arena'] == 'backtest':
            # Do summary output if last bar now
            bar = get_datetime()
            if b['last_trading_date'] == str(bar.date()):
                if b['mode'] == 'daily':
                    summary(context, 'print', data)
                elif b['mode'] == 'minute':
                    now_time = str(bar.astimezone(timezone('US/Eastern')).time())
                    if b['last_trading_time'] == now_time:
                        summary(context, 'print', data)

    elif action == 'print':
        '''
            Summary output to the logging window
        '''
        # Independent copy of context.books 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
            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'])
        out_content = '_       ' + "%.0f" % int(b['init_cash'] / count_sids) \
                + ' average initial cash, ' + str(count_sids) + ' securities\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.
        # Sad 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.

Gary,

F-ing great I would say, really helpful.

To make it even more useful I would add some metrics:
Number of winning trades, number of losing trades in total and per Symbol + ratio, and the average return per trade in total and per symbol. Those are the metrics I use to determine whether a strategy is improving or good enough to improve. I don't mind to help to improve but then again your skills are clearly better than mine and I don't want to stuff up your script

Run Summary new version here can also cope with schedule_function (ironically also requires schedule_function to work).
Well, that's ok, the call to summary just moves out of handle_data to initialize().

I like those suggestions Peter. Meanwhile it seems like a miracle to me that I've been able to even progress this far. Hey, Grant K, you busy? (Or anyone)

Today is the worldwide debut of this great new tagline:
Run Summary stays out of your way.
Yeah I know, back to the drawing board for my marketing team.

Clone Algorithm
49
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
'''             ------------------------------------
                            Run Summary
                ------------------------------------
            https://www.quantopian.com/posts/run-summary

To enable this summary all you need is:
  - the scheduled summary function call in initialize() and
  - the function itself

Like this:

def initialize(context):
    schedule_function(func = summary, date_rule = date_rules.every_day())

def summary(context, data):
    [ Accounting, prep, last bar check and print when called for ]

'''

def initialize(context):
    set_symbol_lookup_date('2014-12-12')
    set_commission(commission.PerShare(cost=0.005, min_trade_cost=1.00))
    context.buys = {} # You won't need this

    # Small amount of negative cash, that's ok.
    # x172.858                    2012-09-17 to 2014-11-20 @10K
    context.stocks = symbols('ACHN', 'NVAX', 'RDNT', 'EDAP', 'HGSH')
    
    
    
    schedule_function(func = summary, date_rule = date_rules.every_day())
    
    
    
def handle_data(context, data):
    '''
        Please ignore all of this strategy and replace with your own.
          (Merely here for some buying and selling to work with)

          Some Buy/Sell logic changed/borrowed/adapted from
            http://quantopian.com/posts/less-is-more-small-and-simple-mixed-strategies

        Disclaimer:
          The strategy below is not serious as is, I think it
           serves as a beautiful example of overfitting, tweaked to the hilt,
           and look-ahead / winning stocks, I know that, so no one need mention it,
           the high return is not real-world, it is kind of for the fun of it.
           Again it is mainly here for buy/sell action, plus high numbers are cool.
           The strategy isn't the point, just recreation here, entertainment purposes.
           On the other hand if you have the patience for seasonal trading, who knows.
    '''
    for sec in data:
        sym   = sec.symbol
        date  = str(get_datetime().date())
        month = get_datetime().month

        # To avoid ordering on top of another order
        if sym in context.buys and context.buys[sym] == date:
            continue

        # Buy/Sell within certain limits
        shares     = context.portfolio.positions[sec].amount
        cost_basis = context.portfolio.positions[sec].cost_basis
        cash_each  = int( context.portfolio.cash / len(data.keys()) )
        buy_num    = int( cash_each / data[sec].price )
        ma_fast    = data[sec].mavg(16)
        ma_slow    = data[sec].mavg(36)

        if ma_fast < ma_slow * .95 and buy_num > 0 \
          and (month <= 5 or month >= 9):
            context.buys[sym] = date    # Record the date

            order(sec, buy_num)     # Buy

        elif data[sec].price > cost_basis * 1.1 and shares \
          and 4 < month < 9:

            order_target(sec, 0)    # Sell
    '''
        End of that overfitting example, now on to the summary...
    '''

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.
            'arena'         : None,    # To become 'backtest' or 'live'.
            'mode'          : None,    # To become 'daily' or 'minute'.
            'prep_prnt'     : '',
            'orders'        : {        # Keep orders for accounting.
                'open' : {},           # Orders not completely filled yet.
                'syms' : {},           # Dict of symbols with open orders,
            },                         #   can be used to not order over another.
            'first_trading_date' : str(get_datetime().date()),
        }

        b = context.books

        # First/last dates and
        #   Arena: backtest or live.  Mode: daily or minute.
        try:
            # Prep some environment info
            b['last_trading_date'] = re.findall('period_end=(.*?) ', str(sid))[0]
            arena     = re.findall('Live|Simulation', str(sid))[0]
            b['mode'] = re.findall('data_frequency=(.*?),', str(sid))[0]

            if arena == 'Live':
                b['arena'] = 'live'
            elif arena == 'Simulation':
                b['arena'] = 'backtest'
        except:
            log.info('Error in str(sid), summary will not print.')
            b['last_trading_date'] = 'unknown'
            b['arena'] = 'Arena unknown'
            b['mode']  = 'Mode unknown'

        # Show environment at the beginning of the run
        msg  = ' {0:s}\n  {1:s}  {2:s} to {3:s}  {4:s}  {5:s}\n'
        b['prep_prnt'] = msg.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:
            # Convenience option to be able in handle_data to
            #   avoid ordering if an order already exists.
            b['orders']['syms'][sym] = 1

            # If an order not seen before, add for tracking
            if order_obj.id not in b['orders']['open']:
                b['orders']['open'][order_obj.id] = order_obj.filled

    # Take a look at current orders
    for id in b['orders']['open']:
        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
        if sym in b['orders']['syms']:    # There's a scenario in multiple buys
            del b['orders']['syms'][sym]  #   where this key could be gone.
        del b['orders']['open'][id]

        # Overall keep track of lowest cash point
        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']))
                )

        # And per symbol
        if b[sym]['balance'] < b[sym]['cash_low']:
            b[sym]['cash_low'] = b[sym]['balance']

    '''
        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':
                    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 (+=)
        #   although the print any time is deprecated, couldn't find a way 
        #   to make work with schedule_function.
        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 groups2
        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.

Hi Gary,

I haven't followed this too carefully, and yes, I'm generally busy. Is there something that is not working as you'd like? Where do you need help?

Grant

One problem you might like: The individual xval seems off in some scenarios. If you clone my backtest here and set so that context.symbols is only 'SPY' and run that, you'll see that the overall return is x0.817 while SPY shows x-0.3, a head-scratcher for me. Something involving this line: drawdown = min( avg_init_cash, abs(b[sym]['cash_low']) ) That's the main thing on my radar.

Then there are wish lists, my own and Peter Bakker's items above.

With the mod for schedule_function, long story short, summary() could no longer take arguments except context and data, so one cannot tell it to print whenever they want, say, after every order while the ordering is sparse. Maybe there is a way for someone more versed in Python. I tried a 'def summary_print()' indented inside summary() which I thought could be called like any other function, nope, uninitialized error in this case.
Sometimes people set an end date on a weekend and the summary won't happen when they do, it would be great to use trading calendar to adjust to the last trading day (usually the Friday before).
Or just anything that happens to catch your interest.
Thanks and I can be emailed with a click on my name if there are any details to discuss, anyone, I'd rather outside of the forum.

Gary,

Regarding being able to control printing within summary(), you might try:

def initialize(context):  
    context.print = False  

Then, from within handle_data, when you want to print, set:

context.print = True  

Within summary(), you would then use:

if context.print:  
    # print to log  

This way, summary() would still be called per the schedule, but output would be suppressed unless you want it.

Grant

Updated with the new additions in Q's get_environment(). No imports now. Couple of improvements. This also alerts you to negative cash. With get_fundamentals there might be 1000's of securities encountered and many not bought nor sold, you can turn off reporting those in the summary, just look for the word 'filter'.

[Removed backtest to replace with an update]

Improved

Clone Algorithm
49
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
'''             ------------------------------------
                            Run Summary
                ------------------------------------
            https://www.quantopian.com/posts/run-summary

To enable this summary all you need is:
  - the scheduled summary function call in initialize() and
  - the function itself

Like this:

def initialize(context):
    schedule_function(summary, date_rules.every_day())

def summary(context, data):
    [ Accounting, prep, last bar check and print when called for ]

To make sure to catch all orders, you could also 
  place summary(context, data) after each order.

Remove my placeholder strategy
'''

def initialize(context):
    set_symbol_lookup_date('2014-12-12')
    set_commission(commission.PerShare(cost=0.005, min_trade_cost=1.00))

    # Some negative cash, that's ok for this example.
    # x208.937                    2012-09-17 to 01/02/2015 @10K
    # x334.574                    2012-09-17 to 01/02/2015 @1K
    context.stocks = symbols('ACHN', 'NVAX', 'RDNT', 'EDAP', 'HGSH')


    schedule_function(summary, date_rules.every_day())


def handle_data(context, data):
    '''
        Please ignore all of this strategy and replace with your own.
          (Merely here for some buying and selling, figures to work with)

          Some Buy/Sell logic changed/borrowed/adapted from
            http://quantopian.com/posts/less-is-more-small-and-simple-mixed-strategies

        Disclaimer:
          The strategy below is not serious as is, I think it
           serves as a beautiful example of overfitting, tweaked to the hilt,
           and look-ahead / winning stocks, I know that, so no one need mention it,
           the high return is not real-world, it is kind of for the fun of it.
           Again it is mainly here for buy/sell action, plus high numbers are cool.
           The strategy isn't the point, just recreation here, entertainment purposes.
           On the other hand if you have the patience for seasonal trading, who knows.
    '''
    for sec in data:
        month   = get_datetime().month
        shares  = context.portfolio.positions[sec].amount
        cost_b  = context.portfolio.positions[sec].cost_basis
        cash_ea = int( context.portfolio.cash / len(data.keys()) )
        buy_num = int( cash_ea / data[sec].price ) # each
        ma_fast = data[sec].mavg(16)
        ma_slow = data[sec].mavg(36)

        # Buy/Sell within certain limits
        if ma_fast < ma_slow * .95 and buy_num > 0 \
          and (month <= 5 or month >= 9):

            # Try partial_drawdown. It won't use all init cash, "Return:" will be reported
            #   higher than Q, based on actual drawdown rather than initial capital.
            partial_drawdown = 0  # 0 or 1
            if partial_drawdown:
                if data[sec].price * buy_num > 1500.:
                    order(sec, buy_num) #  B u y
                    
            # This will use some negative cash and "Return:" will be reported
            #   lower than Q, based on actual drawdown rather than initial capital.
            else:
                order(sec, buy_num)     #  B u y

        elif data[sec].price > cost_b * 1.1 and shares \
          and 4 < month < 9:
            order_target(sec, 0)        #  S e l l

        record(cash = context.portfolio.cash)
    '''
        End of that overfitting example, now on to the summary...
    '''

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

        https://www.quantopian.com/posts/run-summary
    '''
    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'          : 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.
            'prep_prnt'     : '',
            '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.  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.
    '''
    b = context.books   # For brevity.
    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 = {}  # Local, 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
        if sec in data and 'price' in data[sec]:    # Update if available.
            b[sym]['price'] = data[sec].price
        commission         = accounting[id]['commission']
        filled             = accounting[id]['filled']  # Number filled, sell neg.
        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)

        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']

        # 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"
            leverage_alert = 1    # Lowest cash points reached ...
            if leverage_alert and b['cash_low'] < 0:
                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':
                    # This is not ideal. 
                    # How can minute mode be printed only on last bar?
                    log.info('Algo time is ' + 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, all sold, not in current universe, 
            #   and its price is no longer accessible. 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']
        cash_low     = b['cash_low']
        cash_profit  = cash_end - init_cash
        my_portfolio = cash_end + b['shares_value']
        xval         = 'x0'
        b_evts       = ('  (' + str(b['cnt_buy_evnts']) + ' trades)').rjust(17)
        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'
        drawdown     = init_cash - cash_low
        if drawdown != 0:          # Pure profit over input used.
            xval     = '   x' + '%.3f' % ((my_portfolio - drawdown) / 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)+b_evts,
            '  Sell Count: '.rjust(w1) + str(b['count_sell'])        .rjust(w2)+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(drawdown))          .rjust(w2)+neg_cash,
            'Initial Cash: '.rjust(w1) + str(int(init_cash))         .rjust(w2)+untouchd,
            '   Portfolio: '.rjust(w1) + ('$'+str(int(my_portfolio))).rjust(w2),
            '      Return: '.rjust(w1) + xval + '   Profit/Drawdown' .rjust(w2),
        ]
        out  = '_\r\n'
        for o in outs: # Pad and add `'s for replace-all in an editor after copy/paste
            out += (o + ' ' * (82 - 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'
        out_content   = '_      ' + '%.0f' % int(avg_init_cash)
        out_content  += ' average initial cash, ' + str(count_sids) + sec_word
        out_content  += ' ' * (53 - len(out_content)) + '`\r\n'
        lines_out     = 11    # Log in clumps to stay under logging limits.
        count_lines   = 0
        filter_zeros  = 0     # 0 or 1 to filter out those with no buy and no sell.
        if filter_zeros:
            count_lines += 1
            out_content += '.\r\n\tZero buy/sell filtered out `\r\n\r\n.'
        header1 = [
        '',      'Return','Buy|','By|Sl','By|Sl','Price',   'Draw','Cash','Shrs','Shrs ']
        header2 = [
        'Symbol',' Ratio','Hold','Count','Evnts','Strt|Now','Down',' Now',' Now','Value']
        contents_list = [header1, header2] # To be lines per sym as a list of lists.

        for sym in sorted(s.symbol for s in b['sids_seen']):
            if filter_zeros and not b[sym]['count_buy'] and not b[sym]['count_sell']:
                continue
            balance    = avg_init_cash + b[sym]['balance'] # balance started at zero
            cash_low   = b[sym]['cash_low'] + avg_init_cash
            init_price = b[sym]['init_price']
            shares     = b[sym]['shares']
            shares_val = shares * b[sym]['price']
            buy_hold   = 0.0
            xval       = 0
            if b[sym]['count_buy'] or b[sym]['count_sell'] and avg_init_cash:
                portf = balance + shares_val
                xval  = '%.1f' % ((portf - avg_init_cash) / avg_init_cash)
                # I'm against the use of avg_init_cash in the above line however 
                #   I wasn't able to solve the puzzle of actual spent vs profit, can you?
                #   avg_init_cash is *theoretical* for amount per share, not real spent.
                #   The individual Return Ratio is relative to each other, suspect.
                if xval == '-0.0' or xval == '0.0':  # Mainly clearing -0.0
                    xval = '0'    # -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,
                ' ' + str(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' % b[sym]['cash_low'],
                '%.0f' % balance,
                '%.0f' % shares,
                int(shares_val)
            ]
            # 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

        line_count = 0
        for line in contents_list:  # Piece together the output lines formatted.
            cc          = 1  # Column count
            line_count += 1
            out_line    = ''
            for column in line:
                if cc in [3, 4, 5, 6] or line_count in [1, 2]:
                    out_line += str(column).center(col_widths[cc] + 1)
                else:
                    out_line += str(column).rjust(col_widths[cc] + 1)
                cc += 1
            out_line    += ' `\r\n'
            out_content += out_line
            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.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.

Run Summary can haz productivity increase.

In this new version:

Options added, alphabetical list ...

    #  - - - - - - - -  Options  - - - - - - - -  
    cancel_partials  = 1  # Cancel orders after being partially filled.  
    daily_live       = 1  # Log summary at end of each day when live.  
    drawdown_returns = 0  # 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     = 0  # 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  = 0  # Express results like 270% instead of x2.7  

It currently alerts to negative cash, that could be annoying if you're into leverag, can turn that off.

Upper output fewer lines, something like this ...

            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`  

The backticks at the end of lines are for doing replace-all in an editor because after copy/paste everything is all run together at least in Win7/Firefox. [Edit: Seems changed ~Apr 2015, no longer necessary, kudos]

Nitty gritty:
I tried to find a way to calculate the individual securities values based on their maximum spent and did not succeed so those values are analogs (proportional) to the overall calculation (which does work), it is their mean.

This has references along the lines of Drawdown Returns in the code or just "Return" in the output, more here regarding a different way of looking at returns, a "Return" value based on maximum spent.
That "Return" value is constant even if you change initial capital, the Quantopian Returns value (QReturn) is tied to your initial capital setting, change initial capital and your returns will change, sometimes dramatically at least with most code.
My use of the word Drawdown is not the same as the DRAWDOWN you see in the IDE, I should probably change it, next version.

I haven't tested this as thoroughly as some in the past so let me know if you hit a bug.

Clone Algorithm
43
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
'''             ------------------------------------
                            Run Summary
                ------------------------------------
            https://www.quantopian.com/posts/run-summary

To enable this summary all you need is:
  - the scheduled summary function call in initialize() and
  - the function itself

Copy those to your own algorithm like this:

def initialize(context):
    schedule_function(summary, date_rules.every_day())

def summary(context, data):
    [ Accounting, prep, last bar check and print when called for ]


To make sure to catch all orders created, you might need to
  place summary(context, data) so it will run after any order,
  particularly for minute mode.
'''

def initialize(context):
    set_symbol_lookup_date('2015-01-02')
    set_commission(commission.PerShare(cost=0.005, min_trade_cost=1.00))

    # Some negative cash, that's ok for this example.
    # x209                    2012-09-17 to 01/02/2015 @10K
    # x335                    2012-09-17 to 01/02/2015 @1K
    context.stocks = symbols('ACHN', 'NVAX', 'RDNT', 'EDAP', 'HGSH')


    schedule_function(summary, date_rules.every_day(), time_rules.market_close())


def handle_data(context, data):
    '''
        Please ignore all of this strategy.
          (Merely here for some buying and selling, figures to work with)

          Some Buy/Sell logic changed/borrowed/adapted from
            http://quantopian.com/posts/less-is-more-small-and-simple-mixed-strategies

        Disclaimer:
          The strategy below is not serious as is, I think it
           serves as a beautiful example of overfitting, tweaked to the hilt,
           and look-ahead / winning stocks, I know that, so no one need mention it,
           the high return is not real-world, it is kind of for the fun of it.
           Again it is mainly here for buy/sell action, plus high numbers are cool.
           The strategy isn't the point, just recreation here, entertainment purposes.
           On the other hand if you have the patience for seasonal trading, who knows.
    '''
    for sec in data:
        month   = get_datetime().month
        shares  = context.portfolio.positions[sec].amount
        cost_b  = context.portfolio.positions[sec].cost_basis
        cash_ea = int( context.portfolio.cash / len(data.keys()) )
        buy_num = int( cash_ea / data[sec].price ) # each
        ma_fast = data[sec].mavg(16)
        ma_slow = data[sec].mavg(36)

        # Buy/Sell within certain limits
        if ma_fast < ma_slow * .95 and buy_num > 0 \
          and (month <= 5 or month >= 9):

            # Try partial_drawdown. It won't use all init cash, "Return:" will be reported
            #   higher than Q, based on cash drawdown rather than initial capital.
            partial_drawdown = 0  # 0 or 1
            if partial_drawdown:
                if data[sec].price * buy_num > 1500.:
                    order(sec, buy_num) #  B u y

            # This will use some negative cash and "Return:" will be reported
            #   lower than Q, based on cash drawdown rather than initial capital.
            else:
                order(sec, buy_num)     #  B u y

        elif data[sec].price > cost_b * 1.1 and shares \
          and 4 < month < 9:
            order_target(sec, 0)        #  S e l l

        #record(cash = context.portfolio.cash)
    '''
        End of that overfitting example, now on to the summary...
    '''

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

        https://www.quantopian.com/posts/run-summary
    '''
    #  - - - - - - - -  Options  - - - - - - - -
    cancel_partials  = 1  # Cancel orders after being partially filled.
    daily_live       = 1  # Log summary at end of each day when live.
    drawdown_returns = 0  # 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     = 0  # 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  = 0  # 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.  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
        prep_prnt = ' {}\n  {}  {} to {}  {}  {}\n'.format(
            b['arena'],
            b['mode'],
            b['first_trading_date'],
            b['last_trading_date'],
            '   $' + '%.0f' % context.portfolio.starting_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.
            prep_prnt += (sec.symbol + ' ')
        log.info(prep_prnt)

    '''
        Prepare individual securities dictionaries
          with dynamic set_universe, fetcher, IPO's appearing etc.
    '''
    b = context.books   # For brevity.
    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,
                '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 sec in data and 'price' in data[sec]: # Update price if available.
            b[sym]['price'] = data[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 b['last_trading_date'] == str(bar.date()):
                if b['mode'] == 'daily':
                    last_bar_now = 1
                elif b['mode'] == 'minute':
                    # Not ideal.
                    # How to print in minute mode only on last bar simply?
                    log.info('Algo time: ' + 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 sec in data and 'price' in data[sec]:
                b[sec.symbol]['price'] = data[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()'
                minute_note = ',\n    especially when in minute mode, '
                if b['mode'] == 'daily':
                    schedule   += '\n        )'
                    minute_note = '.\n    '
                else:
                    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' + \
                    minute_note + 'like summary(context, data) or\n' + \
                    schedule    + '\n\tAborting summary()'
                )
            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'
            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.