mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-03 16:21:24 +08:00
ENH: Adds auto-closing feature and implements for Futures
This commit is contained in:
+27
-2
@@ -1392,6 +1392,13 @@ class TestClosePosAlgo(TestCase):
|
||||
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',
|
||||
@@ -1412,8 +1419,7 @@ class TestClosePosAlgo(TestCase):
|
||||
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)
|
||||
@@ -1426,6 +1432,25 @@ class TestClosePosAlgo(TestCase):
|
||||
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
|
||||
|
||||
@@ -2009,7 +2009,7 @@ class TestPerformanceTracker(unittest.TestCase):
|
||||
|
||||
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)
|
||||
|
||||
@@ -11,6 +11,7 @@ except ImportError:
|
||||
from collections import OrderedDict
|
||||
from six import iteritems, itervalues
|
||||
|
||||
from zipline.protocol import Event, DATASOURCE_TYPE
|
||||
from zipline.finance.slippage import Transaction
|
||||
from zipline.utils.serialization_utils import (
|
||||
VERSION_LABEL
|
||||
@@ -42,11 +43,15 @@ class PositionTracker(object):
|
||||
)
|
||||
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_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]
|
||||
@@ -64,6 +69,70 @@ class PositionTracker(object):
|
||||
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
|
||||
"""
|
||||
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, sids in self._auto_close_position_sids.items():
|
||||
if date > next_trading_day:
|
||||
continue
|
||||
past_asset_end_dates.add(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:
|
||||
self._auto_close_position_sids.pop(past_asset_end_dates.pop())
|
||||
|
||||
def update_last_sale(self, event):
|
||||
# NOTE, PerformanceTracker already vetted as TRADE type
|
||||
@@ -92,7 +161,7 @@ class PositionTracker(object):
|
||||
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):
|
||||
@@ -102,7 +171,7 @@ class PositionTracker(object):
|
||||
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
|
||||
@@ -120,7 +189,7 @@ class PositionTracker(object):
|
||||
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
|
||||
@@ -203,7 +272,7 @@ class PositionTracker(object):
|
||||
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):
|
||||
@@ -270,7 +339,7 @@ class PositionTracker(object):
|
||||
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,
|
||||
@@ -279,14 +348,18 @@ class PositionTracker(object):
|
||||
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
|
||||
)
|
||||
@@ -354,5 +427,6 @@ class PositionTracker(object):
|
||||
self._position_value_multipliers = OrderedDict()
|
||||
self._position_exposure_multipliers = OrderedDict()
|
||||
self._position_payout_multipliers = OrderedDict()
|
||||
self._auto_close_position_sids = {}
|
||||
|
||||
self.update_positions(state['positions'])
|
||||
|
||||
@@ -358,15 +358,17 @@ class PerformanceTracker(object):
|
||||
|
||||
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
|
||||
@@ -381,13 +383,6 @@ class PerformanceTracker(object):
|
||||
# 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.
|
||||
@@ -413,6 +408,22 @@ class PerformanceTracker(object):
|
||||
# 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
|
||||
@@ -477,6 +488,16 @@ class PerformanceTracker(object):
|
||||
# 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')
|
||||
@@ -498,9 +519,13 @@ class PerformanceTracker(object):
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user