Skip to content

Commit

Permalink
ENH: Adds auto-closing feature and implements for Futures
Browse files Browse the repository at this point in the history
  • Loading branch information
jfkirk committed Jul 31, 2015
1 parent f13e9fd commit c546255
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 25 deletions.
29 changes: 27 additions & 2 deletions tests/test_algorithm.py
Expand Up @@ -1392,6 +1392,13 @@ def setUp(self):
DATASOURCE_TYPE.CLOSE_POSITION]},
index=self.index)
})
self.no_close_panel = pd.Panel({1: pd.DataFrame({
'price': [1, 2, 4], 'volume': [1e9, 0, 0],
'type': [DATASOURCE_TYPE.TRADE,
DATASOURCE_TYPE.TRADE,
DATASOURCE_TYPE.TRADE]},
index=self.index)
})

def test_close_position_equity(self):
metadata = {1: {'symbol': 'TEST',
Expand All @@ -1412,8 +1419,7 @@ def test_close_position_equity(self):
def test_close_position_future(self):
metadata = {1: {'symbol': 'TEST',
'asset_type': 'future',
'notice_date': self.days[2],
'expiration_date': self.days[3]}}
}}
self.algo = TestAlgorithm(sid=1, amount=1, order_count=1,
instant_fill=True, commission=PerShare(0),
asset_metadata=metadata)
Expand All @@ -1426,6 +1432,25 @@ def test_close_position_future(self):
self.check_algo_pnl(results, expected_pnl)
self.check_algo_positions(results, expected_positions)

def test_auto_close_future(self):
metadata = {1: {'symbol': 'TEST',
'asset_type': 'future',
'notice_date': self.days[3],
'expiration_date': self.days[4]}}
self.algo = TestAlgorithm(sid=1, amount=1, order_count=1,
instant_fill=True, commission=PerShare(0),
asset_metadata=metadata)
self.data = DataPanelSource(self.no_close_panel)

# Check results
results = self.run_algo()

expected_pnl = [0, 1, 2]
self.check_algo_pnl(results, expected_pnl)

expected_positions = [1, 1, 0]
self.check_algo_positions(results, expected_positions)

def run_algo(self):
results = self.algo.run(self.data)
return results
Expand Down
2 changes: 1 addition & 1 deletion tests/test_perf_tracking.py
Expand Up @@ -2009,7 +2009,7 @@ def test_close_position_event(self, env=None):

source = DataPanelSource(pan)
for i, event in enumerate(source):
txn = pt.create_close_position_transaction(event)
txn = pt.maybe_create_close_position_transaction(event)
if event.sid == 1:
# Test owned long
self.assertEqual(-120, txn.amount)
Expand Down
97 changes: 89 additions & 8 deletions zipline/finance/performance/position_tracker.py
Expand Up @@ -10,7 +10,9 @@
except ImportError:
from collections import OrderedDict
from six import iteritems, itervalues
import bisect

from zipline.protocol import Event, DATASOURCE_TYPE
from zipline.finance.slippage import Transaction
from zipline.utils.serialization_utils import (
VERSION_LABEL
Expand Down Expand Up @@ -42,11 +44,16 @@ def __init__(self):
)
self._positions_store = zp.Positions()

# Dict, keyed on dates, that contains lists of close position events
# for any Assets in this tracker's positions
self._auto_close_position_dates = []
self._auto_close_position_sids = {}

@with_environment()
def _retrieve_asset(self, sid, env=None):
return env.asset_finder.retrieve_asset(sid)

def _update_multipliers(self, sid):
def _update_asset(self, sid):
try:
self._position_value_multipliers[sid]
self._position_exposure_multipliers[sid]
Expand All @@ -64,6 +71,74 @@ def _update_multipliers(self, sid):
asset.contract_multiplier
self._position_payout_multipliers[sid] = \
asset.contract_multiplier
# Futures are closed on their notice_date
if asset.notice_date:
self._insert_auto_close_position_date(
dt=asset.notice_date,
sid=sid
)
# If the Future does not have a notice_date, it will be closed
# on its expiration_date
elif asset.expiration_date:
self._insert_auto_close_position_date(
dt=asset.expiration_date,
sid=sid
)

def _insert_auto_close_position_date(self, dt, sid):
"""
Inserts the given SID in to the list of positions to be auto-closed by
the given dt.
Parameters
----------
dt : pandas.Timestamp
The date before-which the given SID will be auto-closed
sid : int
The SID of the Asset to be auto-closed
"""
if dt not in self._auto_close_position_sids:
bisect.insort(self._auto_close_position_dates, dt)
self._auto_close_position_sids.setdefault(dt, set()).add(sid)

def auto_close_position_events(self, next_trading_day):
"""
Generates CLOSE_POSITION events for any SIDs whose auto-close date is
before or equal to the given date.
Parameters
----------
next_trading_day : pandas.Timestamp
The time before-which certain Assets need to be closed
Yields
------
Event
A close position event for any sids that should be closed before
the next_trading_day parameter
"""
past_asset_end_dates = set()

# Check the auto_close_position_dates dict for SIDs to close
for date in self._auto_close_position_dates:
if date > next_trading_day:
break
past_asset_end_dates.add(date)
sids = self._auto_close_position_sids[date]
for sid in sids:
# Yield a CLOSE_POSITION event
event = Event({
'dt': date,
'type': DATASOURCE_TYPE.CLOSE_POSITION,
'sid': sid,
})
yield event

# Clear out past dates
while past_asset_end_dates:
to_pop = past_asset_end_dates.pop()
self._auto_close_position_sids.pop(to_pop)
self._auto_close_position_dates.remove(to_pop)

def update_last_sale(self, event):
# NOTE, PerformanceTracker already vetted as TRADE type
Expand Down Expand Up @@ -92,7 +167,7 @@ def update_positions(self, 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_multipliers(sid)
self._update_asset(sid)

def update_position(self, sid, amount=None, last_sale_price=None,
last_sale_date=None, cost_basis=None):
Expand All @@ -102,7 +177,7 @@ def update_position(self, sid, amount=None, last_sale_price=None,
pos.amount = amount
self._position_amounts[sid] = amount
self._position_values = None # invalidate cache
self._update_multipliers(sid=sid)
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
Expand All @@ -120,7 +195,7 @@ def execute_transaction(self, txn):
position.update(txn)
self._position_amounts[sid] = position.amount
self._position_last_sale_prices[sid] = position.last_sale_price
self._update_multipliers(sid)
self._update_asset(sid)

def handle_commission(self, commission):
# Adjust the cost basis of the stock if we own it
Expand Down Expand Up @@ -203,7 +278,7 @@ def handle_split(self, split):
self._position_amounts[split.sid] = position.amount
self._position_last_sale_prices[split.sid] = \
position.last_sale_price
self._update_multipliers(split.sid)
self._update_asset(split.sid)
return leftover_cash

def _maybe_earn_dividend(self, dividend):
Expand Down Expand Up @@ -270,7 +345,7 @@ def pay_dividends(self, dividend_frame):
position.amount += share_count
self._position_amounts[stock] = position.amount
self._position_last_sale_prices[stock] = position.last_sale_price
self._update_multipliers(stock)
self._update_asset(stock)

# Add cash equal to the net cash payed from all dividends. Note that
# "negative cash" is effectively paid if we're short an asset,
Expand All @@ -279,14 +354,18 @@ def pay_dividends(self, dividend_frame):
net_cash_payment = payments['cash_amount'].fillna(0).sum()
return net_cash_payment

def create_close_position_transaction(self, event):
def maybe_create_close_position_transaction(self, event):
if not self._position_amounts.get(event.sid):
return None
if 'price' in event:
price = event.price
else:
price = self._position_last_sale_prices[event.sid]
txn = Transaction(
sid=event.sid,
amount=(-1 * self._position_amounts[event.sid]),
dt=event.dt,
price=event.price,
price=price,
commission=0,
order_id=0
)
Expand Down Expand Up @@ -354,5 +433,7 @@ def __setstate__(self, state):
self._position_value_multipliers = OrderedDict()
self._position_exposure_multipliers = OrderedDict()
self._position_payout_multipliers = OrderedDict()
self._auto_close_position_dates = []
self._auto_close_position_sids = {}

self.update_positions(state['positions'])
53 changes: 39 additions & 14 deletions zipline/finance/performance/tracker.py
Expand Up @@ -358,15 +358,17 @@ def process_benchmark(self, event):

def process_close_position(self, event):

# CLOSE_POSITION events contain prices that must be handled as a final
# trade event
self.process_trade(event)
# CLOSE_POSITION events that contain prices that must be handled as
# a final trade event
if 'price' in event:
self.process_trade(event)

txn = self.position_tracker.create_close_position_transaction(event)
txn = self.position_tracker.\
maybe_create_close_position_transaction(event)
if txn:
self.process_transaction(txn)

def check_upcoming_dividends(self, completed_date):
def check_upcoming_dividends(self, next_trading_day):
"""
Check if we currently own any stocks with dividends whose ex_date is
the next trading day. Track how much we should be payed on those
Expand All @@ -381,13 +383,6 @@ def check_upcoming_dividends(self, completed_date):
# period, so bail.
return

# Get the next trading day and, if it is outside the bounds of the
# simulation, bail.
next_trading_day = TradingEnvironment.instance().\
next_trading_day(completed_date)
if (next_trading_day is None) or (next_trading_day >= self.last_close):
return

# Dividends whose ex_date is the next trading day. We need to check if
# we own any of these stocks so we know to pay them out when the pay
# date comes.
Expand All @@ -413,6 +408,22 @@ def check_upcoming_dividends(self, completed_date):
# notify periods to update their stats
period.handle_dividends_paid(net_cash_payment)

def check_asset_auto_closes(self, next_trading_day):
"""
Check if the position tracker currently owns any Assets with an
auto-close date that is the next trading day. Close those positions.
Parameters
----------
next_trading_day : pandas.Timestamp
The next trading day of the simulation
"""
auto_close_events = self.position_tracker.auto_close_position_events(
next_trading_day=next_trading_day
)
for event in auto_close_events:
self.process_close_position(event)

def handle_minute_close(self, dt):
"""
Handles the close of the given minute. This includes handling
Expand Down Expand Up @@ -477,6 +488,16 @@ def _handle_market_close(self, completed_date):
# increment the day counter before we move markers forward.
self.day_count += 1.0

# Get the next trading day and, if it is past the bounds of this
# simulation, return the daily perf packet
next_trading_day = TradingEnvironment.instance().\
next_trading_day(completed_date)

# Check if any assets need to be auto-closed before generating today's
# perf period
if next_trading_day:
self.check_asset_auto_closes(next_trading_day=next_trading_day)

# Take a snapshot of our current performance to return to the
# browser.
daily_update = self.to_dict(emission_type='daily')
Expand All @@ -498,9 +519,13 @@ def _handle_market_close(self, completed_date):
self.todays_performance.period_open = self.market_open
self.todays_performance.period_close = self.market_close

# Check for any dividends
self.check_upcoming_dividends(completed_date)
# If the next trading day is irrelevant, then return the daily packet
if (next_trading_day is None) or (next_trading_day >= self.last_close):
return daily_update

# Check for any dividends and auto-closes, then return the daily perf
# packet
self.check_upcoming_dividends(next_trading_day=next_trading_day)
return daily_update

def handle_simulation_end(self):
Expand Down

0 comments on commit c546255

Please sign in to comment.