Quantopian's community platform is shutting down. Please read this post for more information and download your code.
Back to Community
Tracking Win Rate

I'd like to track the win rate of my algorithm. I'm not the most familiar with quantopian/python so maybe this isn't possible...

I'm currently tracking number of longs, and then recording longs+leverage like this:

    for position in context.portfolio.positions.itervalues():  
        if position.amount > 0:  
            longcount += 1

    record(numLongs = longcount,  
           leverage = context.account.leverage)  

I'd like to do something similar for win rate. So I would need to know the price I bought a security, and the price I sold it for but not sure how to access those. Any help is appreciated.

3 responses

Right, the only route I know of is to save cost_basis each minute for all positions, then calc pnl when trades happen, not easy, and partial fills make things more difficult.

Would ballpark PnL per day do?

Or, notice the PnL and Return %/day here. I had always hoped Quantopian would have wanted to reward me for that comprehensive code (since I put so much time into it) and integrate it here but they have other priorities.

Trying to figure out whether I might be able to squeeze in some time and inspiration to offer some of it, as part of a better version of track_orders, not sure, it's summertime, life is swamped besides trying to survive and that work is my hermit-mode-rainy-days mindset.

I think cost_basis would actually work! Here's a blueprint of what I think I should do, let me know if you think I should do it differently.

  1. Save cost_basis for each security in a series
  2. Upon selling, save the sale price (if I sell 100% of my holdings in a security, how do I access this? will last_sale_price still work?)
  3. Compare the two to see which are positive
  4. if (positive), then wins += 1
  5. record wins/total positions

Pretty much.
Usually data.current(stock, 'price') will do, and then in the rare case of a delist, as backup, the positions object last_sale_price, which is the same as current, rather than one's own last trade price (that would be nice to have).

I decided to work on this a little, although not thoroughly vetted ...
So on (2) where a position has closed, the code below uses the last known cost basis with current price. It could also be saving price.

And then you can add win rate to it.

If the bit that says ... and not amt ... is in use, positions that just closed (exclusively) are logged and look like this. ADBE went from 70 shares to 0.

2018-05-08 07:31 transactions:53 INFO Rows: 32  
2018-05-08 07:31 log_clean:77 INFO .  
  ADBE  amt     70 ->      0  cb 230.94  pnl  10.8  
    BA  amt     47 ->      0  cb 339.80  pnl -45.4  
   CAG  amt    431 ->      0  cb  37.06  pnl  98.2  
    CI  amt     94 ->      0  cb 168.98  pnl 196.4  
   CMI  amt    109 ->      0  cb 146.47  pnl  -1.2  
   DIS  amt    158 ->      0  cb 102.24  pnl -103.0  
   LHX  amt    107 ->      0  cb 148.94  pnl 290.8  
........

Otherwise the output can be large, 309 rows here:

2018-05-04 07:31 transactions:53 INFO Rows: 309  
2018-05-04 07:31 log_clean:77 INFO .  
  AAPL  amt     94 ->     -5  cb 182.92  pnl  -0.6  
   ABT  amt    277 ->      2  cb  57.43  pnl   0.9  
  ADBE  amt     71 ->      0  cb 220.91  pnl 375.6  
   ADI  amt    185 ->     -2  cb  88.56  pnl  -0.3  
   AEP  amt    230 ->      2  cb  68.73  pnl   1.3  
   AES  amt   1307 ->   1139  cb  12.22  pnl 156.8  
   HES  amt    282 ->     -4  cb  58.26  pnl  -0.4  
   AIG  amt    313 ->     -6  cb  52.58  pnl  -0.5  
  AMAT  amt    320 ->     -4  cb  51.19  pnl  -0.4  
  AMGN  amt     94 ->      3  cb 167.69  pnl  -5.1  
    AN  amt    347 ->    297  cb  47.67  pnl  -8.9  
   AON  amt    112 ->     10  cb 138.28  pnl   7.7  
   APC  amt    238 ->      6  cb  65.43  pnl   3.6  
   APD  amt     99 ->     21  cb 159.93  pnl  12.7  
   APH  amt    192 ->      0  cb  83.08  pnl 272.2  
   AZO  amt     25 ->      0  cb 633.11  pnl 456.3  
    BA  amt     49 ->      0  cb 321.15  pnl 509.7  
   BAX  amt    227 ->      6  cb  69.77  pnl  -2.2  
...........

Or if you set log_type = 'high_low', notice these 5 highs happen to now be closed but their PnL's were stored.

2018-05-07 06:31 transactions:22 INFO High:  
                      cb  amt    pnl  
Equity(40430 [GM])   0.0  0.0  395.1  
Equity(6683 [SBUX])  0.0  0.0  398.6  
Equity(6119 [PPL])   0.0  0.0  407.3  
Equity(693 [AZO])    0.0  0.0  456.3  
Equity(698 [BA])     0.0  0.0  509.7  
2018-05-07 06:31 transactions:23 INFO  Low:  
                            cb    amt    pnl  
Equity(22954 [ABC])   0.000000    0.0 -528.6  
Equity(34661 [TDC])  40.379109   38.0 -108.6  
Equity(7797 [UNM])   40.368725   66.0  -69.2  
Equity(51649 [ADT])   8.664511  128.0  -44.1  
Equity(4799 [CVS])   64.465724   21.0  -38.6  

Here's the code:

import pandas as pd

def transactions(context, data):  
    # Storing current cost basis and amount (shares held)  
    #   and logging pnl when amount just changed

    c   = context  
    pos = context.portfolio.positions  
    to_log   = []  
    c.date   = str(get_datetime().date())

    # I added this part at the last minute as an afterthought since highs and lows can be interesting  
    log_type = 'all'                  # 'high_low' or 'all'

    try:   # will log high & low from end of day yesterday at the start of each day  
        if log_type == 'high_low' and c.date_prv != c.date:  
            log.info('High:\n{}'.format( c.states.sort_values(by='pnl').tail(5) ))  
            log.info(' Low:\n{}'.format( c.states.sort_values(by='pnl').head(5) ))  
    except:  # the try:except is because I didn't feel like initializing c.date_prv in initialize()  
        pass

    c.date_prv = c.date

    for s in context.output.index:  # Initializing any in output from pipeline if not there already  
        if s not in c.states.index:  
            c.states.loc[s] = { 'cb': 0, 'amt': 0, 'pnl': 0}

    for s in c.states.index:  
        cb  = pos[s].cost_basis or 0  
        amt = pos[s].amount     or 0

        # When there was a trade ...  
        if amt != c.states.loc[s].amt:  # amount changed from last time  
            # Log pnl  
            cb_to_use  = cb  or c.states.loc[s].cb        # falling back to last known if cb is now 0 (was closed)  
            amt_to_use = amt or c.states.loc[s].amt  
            prc        = data.current(s, 'price') or pos[s].last_sale_price  
            pnl        = amt_to_use * (prc - cb_to_use)   # if closed now, this is based on last known cb and number of shares.

            if log_type == 'all':  #                 and not amt:  # add this to see just those closed  
                # Save lines to log them later  
                to_log.append('{}  amt {} -> {}  cb {}  pnl {}'.format(  
                    s.symbol                     .rjust(6),    # right aligned for vertical alignment  
                    str(int(c.states.loc[s].amt)).rjust(6),    # previous amount  
                    str(                     amt).rjust(6),    # current  amount  
                    ('%.2f' % cb_to_use)         .rjust(6),    # two digits only  
                    ('%.1f' % pnl)               .rjust(5),    # PnL  
                ))

            # Update latest  
            c.states.loc[s].cb  = cb  
            c.states.loc[s].amt = amt  
            c.states.loc[s].pnl = '%.1f' % pnl

    if to_log:  
            log.info('Rows: {}'.format(len(to_log)))  
            log_clean(to_log)

def log_clean(lines):  # Log lines of output more efficiently  
    # https://www.quantopian.com/posts/logging-more-content-while-remaining-under-the-logging-limit  
    if not lines:  
        return

    if 'list' not in str(type(lines)):  
        lines = [lines]  # single line accomodation

    buffer_len = 1024   # each group  
    chunk = '.'

    for line in lines:  
        if line is None or not len(line):  
            continue          # skip if empty string for example  
        if len(chunk) + len(line) < buffer_len:  
            # Add to chunk if will still be under buffer_len  
            chunk += '\n{}'.format(line)  
        else:  # Or log chunk and start over with new line.  
            log.info(chunk)  
            chunk = ':\n{}'.format(line)

    if len(chunk) > 2:        # if anything remaining  
        log.info(chunk)

def initialize(context):  
    # For https://www.quantopian.com/posts/tracking-win-rate  
    context.states = pd.DataFrame(columns=['cb', 'amt', 'pnl'])  # could also add prc

    for i in range(1, 391, 1):  # start, until (non-inclusive), every n minutes  
        schedule_function(transactions, date_rules.every_day(), time_rules.market_open(minutes=i))

Other pages that might be helpful:
https://www.google.com/search?q=pnl+site:quantopian.com