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