Merge pull request #726 from quantopian/explicit-position-value-calcs

MAINT: Only calc position values once per packet.
This commit is contained in:
Eddie Hebert
2015-09-25 17:33:53 -04:00
3 changed files with 215 additions and 145 deletions
+25 -23
View File
@@ -35,6 +35,7 @@ from six.moves import range, zip
import zipline.utils.factory as factory
import zipline.finance.performance as perf
from zipline.finance.performance import position_tracker
from zipline.finance.slippage import Transaction, create_transaction
import zipline.utils.math_utils as zp_math
@@ -2181,22 +2182,22 @@ class TestPositionTracker(unittest.TestCase):
np.bool_(False)
"""
pt = perf.PositionTracker(self.env.asset_finder)
pos_stats = position_tracker.calc_position_stats(pt)
stats = [
'calculate_positions_value',
'_net_exposure',
'_gross_value',
'_gross_exposure',
'_short_value',
'_short_exposure',
'_shorts_count',
'_long_value',
'_long_exposure',
'_longs_count',
'net_value',
'net_exposure',
'gross_value',
'gross_exposure',
'short_value',
'short_exposure',
'shorts_count',
'long_value',
'long_exposure',
'longs_count',
]
for name in stats:
meth = getattr(pt, name)
val = meth()
val = getattr(pos_stats, name)
self.assertEquals(val, 0)
self.assertNotIsInstance(val, (bool, np.bool_))
@@ -2234,20 +2235,24 @@ class TestPositionTracker(unittest.TestCase):
pt.update_positions({1: pos1, 2: pos2, 3: pos3, 4: pos4})
# Test long-only methods
self.assertEqual(100, pt._long_value())
self.assertEqual(100 + 300000, pt._long_exposure())
pos_stats = position_tracker.calc_position_stats(pt)
self.assertEqual(100, pos_stats.long_value)
self.assertEqual(100 + 300000, pos_stats.long_exposure)
self.assertEqual(2, pos_stats.longs_count)
# Test short-only methods
self.assertEqual(-200, pt._short_value())
self.assertEqual(-200 - 400000, pt._short_exposure())
self.assertEqual(-200, pos_stats.short_value)
self.assertEqual(-200 - 400000, pos_stats.short_exposure)
self.assertEqual(2, pos_stats.shorts_count)
# Test gross and net values
self.assertEqual(100 + 200, pt._gross_value())
self.assertEqual(100 - 200, pt._net_value())
self.assertEqual(100 + 200, pos_stats.gross_value)
self.assertEqual(100 - 200, pos_stats.net_value)
# Test gross and net exposures
self.assertEqual(100 + 200 + 300000 + 400000, pt._gross_exposure())
self.assertEqual(100 - 200 + 300000 - 400000, pt._net_exposure())
self.assertEqual(100 + 200 + 300000 + 400000, pos_stats.gross_exposure)
self.assertEqual(100 - 200 + 300000 - 400000, pos_stats.net_exposure)
def test_serialization(self):
pt = perf.PositionTracker(self.env.asset_finder)
@@ -2260,9 +2265,6 @@ class TestPositionTracker(unittest.TestCase):
pt.update_positions({1: pos1, 3: pos3})
p_string = dumps_with_persistent_ids(pt)
test = loads_with_persistent_ids(p_string, env=self.env)
nt.assert_dict_equal(test._position_amounts, pt._position_amounts)
nt.assert_dict_equal(test._position_last_sale_prices,
pt._position_last_sale_prices)
nt.assert_count_equal(test.positions.keys(), pt.positions.keys())
for sid in pt.positions:
nt.assert_dict_equal(test.positions[sid].__dict__,
+56 -35
View File
@@ -75,6 +75,7 @@ import logbook
import numpy as np
from collections import namedtuple
from zipline.assets import Future
try:
@@ -90,11 +91,42 @@ import zipline.protocol as zp
from zipline.utils.serialization_utils import (
VERSION_LABEL
)
from zipline.finance.performance.position_tracker import calc_position_stats
log = logbook.Logger('Performance')
TRADE_TYPE = zp.DATASOURCE_TYPE.TRADE
PeriodStats = namedtuple('PeriodStats',
['net_liquidation',
'gross_leverage',
'net_leverage'])
def calc_net_liquidation(ending_cash, long_value, short_value):
return ending_cash + long_value + short_value
def calc_leverage(exposure, net_liq):
if net_liq != 0:
return exposure / net_liq
return np.inf
def calc_period_stats(pos_stats, ending_cash):
net_liq = calc_net_liquidation(ending_cash,
pos_stats.long_value,
pos_stats.short_value)
gross_leverage = calc_leverage(pos_stats.gross_exposure, net_liq)
net_leverage = calc_leverage(pos_stats.net_exposure, net_liq)
return PeriodStats(
net_liquidation=net_liq,
gross_leverage=gross_leverage,
net_leverage=net_leverage)
class PerformancePeriod(object):
def __init__(
@@ -178,8 +210,9 @@ class PerformancePeriod(object):
def calculate_performance(self):
pt = self.position_tracker
self.ending_value = pt.calculate_positions_value()
self.ending_exposure = pt.calculate_positions_exposure()
pos_stats = calc_position_stats(pt)
self.ending_value = pos_stats.net_value
self.ending_exposure = pos_stats.net_exposure
total_at_start = self.starting_cash + self.starting_value
self.ending_cash = self.starting_cash + self.period_cash_flow
@@ -245,27 +278,10 @@ class PerformancePeriod(object):
def position_amounts(self):
return self.position_tracker.position_amounts
@property
def _net_liquidation_value(self):
pt = self.position_tracker
return self.ending_cash + pt._long_value() + pt._short_value()
def _gross_leverage(self):
net_liq = self._net_liquidation_value
if net_liq != 0:
return self.position_tracker._gross_exposure() / net_liq
return np.inf
def _net_leverage(self):
net_liq = self._net_liquidation_value
if net_liq != 0:
return self.position_tracker._net_exposure() / net_liq
return np.inf
def __core_dict(self):
pt = self.position_tracker
pos_stats = calc_position_stats(self.position_tracker)
period_stats = calc_period_stats(pos_stats, self.ending_cash)
rval = {
'ending_value': self.ending_value,
'ending_exposure': self.ending_exposure,
@@ -281,14 +297,14 @@ class PerformancePeriod(object):
'returns': self.returns,
'period_open': self.period_open,
'period_close': self.period_close,
'gross_leverage': self._gross_leverage(),
'net_leverage': self._net_leverage(),
'short_exposure': pt._short_exposure(),
'long_exposure': pt._long_exposure(),
'short_value': pt._short_value(),
'long_value': pt._long_value(),
'longs_count': pt._longs_count(),
'shorts_count': pt._shorts_count()
'gross_leverage': period_stats.gross_leverage,
'net_leverage': period_stats.net_leverage,
'short_exposure': pos_stats.short_exposure,
'long_exposure': pos_stats.long_exposure,
'short_value': pos_stats.short_value,
'long_value': pos_stats.long_value,
'longs_count': pos_stats.longs_count,
'shorts_count': pos_stats.shorts_count,
}
return rval
@@ -367,6 +383,10 @@ class PerformancePeriod(object):
def as_account(self):
account = self._account_store
pt = self.position_tracker
pos_stats = calc_position_stats(pt)
period_stats = calc_period_stats(pos_stats, self.ending_cash)
# If no attribute is found on the PerformancePeriod resort to the
# following default values. If an attribute is found use the existing
# value. For instance, a broker may provide updates to these
@@ -402,11 +422,12 @@ class PerformancePeriod(object):
self.ending_cash / (self.ending_cash + self.ending_value))
account.day_trades_remaining = \
getattr(self, 'day_trades_remaining', float('inf'))
account.leverage = \
getattr(self, 'leverage', self._gross_leverage())
account.net_leverage = self._net_leverage()
account.net_liquidation = \
getattr(self, 'net_liquidation', self._net_liquidation_value)
account.leverage = getattr(self, 'leverage',
period_stats.gross_leverage)
account.net_leverage = period_stats.net_leverage
account.net_liquidation = getattr(self, 'net_liquidation',
period_stats.net_liquidation)
return account
def __getstate__(self):
+134 -87
View File
@@ -4,6 +4,7 @@ import logbook
import numpy as np
import pandas as pd
from pandas.lib import checknull
from collections import namedtuple
try:
# optional cython based OrderedDict
from cyordereddict import OrderedDict
@@ -27,6 +28,131 @@ from . position import positiondict
log = logbook.Logger('Performance')
PositionStats = namedtuple('PositionStats',
['net_exposure',
'gross_value',
'gross_exposure',
'short_value',
'short_exposure',
'shorts_count',
'long_value',
'long_exposure',
'longs_count',
'net_value'])
def calc_position_values(amounts,
last_sale_prices,
value_multipliers):
iter_amount_price_multiplier = zip(
amounts,
last_sale_prices,
itervalues(value_multipliers),
)
return [
price * amount * multiplier for
price, amount, multiplier in iter_amount_price_multiplier
]
def calc_net(values):
# Returns 0.0 if there are no values.
return sum(values, np.float64())
def calc_position_exposures(amounts,
last_sale_prices,
exposure_multipliers):
iter_amount_price_multiplier = zip(
amounts,
last_sale_prices,
itervalues(exposure_multipliers),
)
return [
price * amount * multiplier for
price, amount, multiplier in iter_amount_price_multiplier
]
def calc_long_value(position_values):
return sum(i for i in position_values if i > 0)
def calc_short_value(position_values):
return sum(i for i in position_values if i < 0)
def calc_long_exposure(position_exposures):
return sum(i for i in position_exposures if i > 0)
def calc_short_exposure(position_exposures):
return sum(i for i in position_exposures if i < 0)
def calc_longs_count(position_exposures):
return sum(1 for i in position_exposures if i > 0)
def calc_shorts_count(position_exposures):
return sum(1 for i in position_exposures if i < 0)
def calc_gross_exposure(long_exposure, short_exposure):
return long_exposure + abs(short_exposure)
def calc_gross_value(long_value, short_value):
return long_value + abs(short_value)
def calc_position_stats(pt):
amounts = []
last_sale_prices = []
for pos in itervalues(pt.positions):
amounts.append(pos.amount)
last_sale_prices.append(pos.last_sale_price)
position_value_multipliers = pt._position_value_multipliers
position_exposure_multipliers = pt._position_exposure_multipliers
position_values = calc_position_values(
amounts,
last_sale_prices,
position_value_multipliers
)
position_exposures = calc_position_exposures(
amounts,
last_sale_prices,
position_exposure_multipliers
)
long_value = calc_long_value(position_values)
short_value = calc_short_value(position_values)
gross_value = calc_gross_value(long_value, short_value)
long_exposure = calc_long_exposure(position_exposures)
short_exposure = calc_short_exposure(position_exposures)
gross_exposure = calc_gross_exposure(long_exposure, short_exposure)
net_exposure = calc_net(position_exposures)
longs_count = calc_longs_count(position_exposures)
shorts_count = calc_shorts_count(position_exposures)
net_value = calc_net(position_values)
return PositionStats(
long_value=long_value,
gross_value=gross_value,
short_value=short_value,
long_exposure=long_exposure,
short_exposure=short_exposure,
gross_exposure=gross_exposure,
net_exposure=net_exposure,
longs_count=longs_count,
shorts_count=shorts_count,
net_value=net_value
)
class PositionTracker(object):
def __init__(self, asset_finder):
@@ -35,8 +161,6 @@ class PositionTracker(object):
# sid => position object
self.positions = positiondict()
# Arrays for quick calculations of positions value
self._position_amounts = OrderedDict()
self._position_last_sale_prices = OrderedDict()
self._position_value_multipliers = OrderedDict()
self._position_exposure_multipliers = OrderedDict()
self._position_payout_multipliers = OrderedDict()
@@ -145,7 +269,6 @@ class PositionTracker(object):
old_price = pos.last_sale_price
pos.last_sale_date = event.dt
pos.last_sale_price = price
self._position_last_sale_prices[sid] = price
# Calculate cash adjustment on assets with multipliers
return ((price - old_price) * self._position_payout_multipliers[sid]
@@ -155,8 +278,6 @@ class PositionTracker(object):
# update positions in batch
self.positions.update(positions)
for sid, pos in iteritems(positions):
self._position_amounts[sid] = pos.amount
self._position_last_sale_prices[sid] = pos.last_sale_price
self._update_asset(sid)
def update_position(self, sid, amount=None, last_sale_price=None,
@@ -165,13 +286,9 @@ class PositionTracker(object):
if amount is not None:
pos.amount = amount
self._position_amounts[sid] = amount
self._position_values = None # invalidate cache
self._update_asset(sid=sid)
if last_sale_price is not None:
pos.last_sale_price = last_sale_price
self._position_last_sale_prices[sid] = last_sale_price
self._position_values = None # invalidate cache
if last_sale_date is not None:
pos.last_sale_date = last_sale_date
if cost_basis is not None:
@@ -183,8 +300,6 @@ class PositionTracker(object):
sid = txn.sid
position = self.positions[sid]
position.update(txn)
self._position_amounts[sid] = position.amount
self._position_last_sale_prices[sid] = position.last_sale_price
self._update_asset(sid)
def handle_commission(self, commission):
@@ -193,81 +308,12 @@ class PositionTracker(object):
self.positions[commission.sid].\
adjust_commission_cost_basis(commission)
@property
def position_values(self):
iter_amount_price_multiplier = zip(
itervalues(self._position_amounts),
itervalues(self._position_last_sale_prices),
itervalues(self._position_value_multipliers),
)
return [
price * amount * multiplier for
price, amount, multiplier in iter_amount_price_multiplier
]
@property
def position_exposures(self):
iter_amount_price_multiplier = zip(
itervalues(self._position_amounts),
itervalues(self._position_last_sale_prices),
itervalues(self._position_exposure_multipliers),
)
return [
price * amount * multiplier for
price, amount, multiplier in iter_amount_price_multiplier
]
def calculate_positions_value(self):
if len(self.position_values) == 0:
return np.float64(0)
return sum(self.position_values)
def calculate_positions_exposure(self):
if len(self.position_exposures) == 0:
return np.float64(0)
return sum(self.position_exposures)
def _longs_count(self):
return sum(1 for i in self.position_exposures if i > 0)
def _long_exposure(self):
return sum(i for i in self.position_exposures if i > 0)
def _long_value(self):
return sum(i for i in self.position_values if i > 0)
def _shorts_count(self):
return sum(1 for i in self.position_exposures if i < 0)
def _short_exposure(self):
return sum(i for i in self.position_exposures if i < 0)
def _short_value(self):
return sum(i for i in self.position_values if i < 0)
def _gross_exposure(self):
return self._long_exposure() + abs(self._short_exposure())
def _gross_value(self):
return self._long_value() + abs(self._short_value())
def _net_exposure(self):
return self.calculate_positions_exposure()
def _net_value(self):
return self.calculate_positions_value()
def handle_split(self, split):
if split.sid in self.positions:
# Make the position object handle the split. It returns the
# leftover cash from a fractional share, if there is any.
position = self.positions[split.sid]
leftover_cash = position.handle_split(split)
self._position_amounts[split.sid] = position.amount
self._position_last_sale_prices[split.sid] = \
position.last_sale_price
self._update_asset(split.sid)
return leftover_cash
@@ -333,8 +379,6 @@ class PositionTracker(object):
position = self.positions[stock]
position.amount += share_count
self._position_amounts[stock] = position.amount
self._position_last_sale_prices[stock] = position.last_sale_price
self._update_asset(stock)
# Add cash equal to the net cash payed from all dividends. Note that
@@ -345,15 +389,20 @@ class PositionTracker(object):
return net_cash_payment
def maybe_create_close_position_transaction(self, event):
if not self._position_amounts.get(event.sid):
try:
pos = self.positions[event.sid]
amount = pos.amount
if amount == 0:
return None
except KeyError:
return None
if 'price' in event:
price = event.price
else:
price = self._position_last_sale_prices[event.sid]
price = pos.last_sale_price
txn = Transaction(
sid=event.sid,
amount=(-1 * self._position_amounts[event.sid]),
amount=(-1 * pos.amount),
dt=event.dt,
price=price,
commission=0,
@@ -422,8 +471,6 @@ class PositionTracker(object):
self._auto_close_position_sids = state['auto_close_position_sids']
# Arrays for quick calculations of positions value
self._position_amounts = OrderedDict()
self._position_last_sale_prices = OrderedDict()
self._position_value_multipliers = OrderedDict()
self._position_exposure_multipliers = OrderedDict()
self._position_payout_multipliers = OrderedDict()