diff --git a/tests/finance/test_slippage.py b/tests/finance/test_slippage.py index ed6d480f..c4feaaec 100644 --- a/tests/finance/test_slippage.py +++ b/tests/finance/test_slippage.py @@ -77,3 +77,439 @@ class SlippageTestCase(TestCase): # TODO: Make expected_txn an Transaction object and ensure there # is a __eq__ for that class. self.assertEquals(expected_txn, txn.__dict__) + + def test_orders_limit(self): + + events = self.gen_trades() + + slippage_model = VolumeShareSlippage() + + # long, does not trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': 100, + 'filled': 0, + 'sid': 133, + 'limit': 3.5}) + ]} + + txn = slippage_model.simulate( + events[2], + open_orders + ) + + expected_txn = {} + + self.assertIsNone(txn) + + # long, does trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': 100, + 'filled': 0, + 'sid': 133, + 'limit': 3.5}) + ]} + + txn = slippage_model.simulate( + events[3], + open_orders + ) + + expected_txn = { + 'price': float(3.500875), + 'dt': datetime.datetime( + 2006, 1, 5, 14, 34, tzinfo=pytz.utc), + 'amount': int(100), + 'sid': int(133) + } + + self.assertIsNotNone(txn) + + for key, value in expected_txn.items(): + self.assertEquals(value, txn[key]) + + # short, does not trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': -100, + 'filled': 0, + 'sid': 133, + 'limit': 3.5}) + ]} + + txn = slippage_model.simulate( + events[0], + open_orders + ) + + expected_txn = {} + + self.assertIsNone(txn) + + # short, does trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': -100, + 'filled': 0, + 'sid': 133, + 'limit': 3.5}) + ]} + + txn = slippage_model.simulate( + events[1], + open_orders + ) + + expected_txn = { + 'price': float(3.499125), + 'dt': datetime.datetime( + 2006, 1, 5, 14, 32, tzinfo=pytz.utc), + 'amount': int(-100), + 'sid': int(133) + } + + self.assertIsNotNone(txn) + + for key, value in expected_txn.items(): + self.assertEquals(value, txn[key]) + + def test_orders_stop(self): + events = self.gen_trades() + + slippage_model = VolumeShareSlippage() + + # long, does not trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': 100, + 'filled': 0, + 'sid': 133, + 'stop': 3.5}) + ]} + + txn = slippage_model.simulate( + events[2], + open_orders + ) + + expected_txn = {} + + self.assertIsNone(txn) + + # long, does trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': 100, + 'filled': 0, + 'sid': 133, + 'stop': 3.6}) + ]} + + txn = slippage_model.simulate( + events[3], + open_orders + ) + + expected_txn = { + 'price': float(3.500875), + 'dt': datetime.datetime( + 2006, 1, 5, 14, 34, tzinfo=pytz.utc), + 'amount': int(100), + 'sid': int(133) + } + + self.assertIsNotNone(txn) + + for key, value in expected_txn.items(): + self.assertEquals(value, txn[key]) + + # short, does not trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': -100, + 'filled': 0, + 'sid': 133, + 'stop': 3.5}) + ]} + + txn = slippage_model.simulate( + events[0], + open_orders + ) + + expected_txn = {} + + self.assertIsNone(txn) + + # short, does trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': -100, + 'filled': 0, + 'sid': 133, + 'stop': 3.4}) + ]} + + txn = slippage_model.simulate( + events[1], + open_orders + ) + + expected_txn = { + 'price': float(3.499125), + 'dt': datetime.datetime( + 2006, 1, 5, 14, 32, tzinfo=pytz.utc), + 'amount': int(-100), + 'sid': int(133) + } + + self.assertIsNotNone(txn) + + for key, value in expected_txn.items(): + self.assertEquals(value, txn[key]) + + def test_orders_stop_limit(self): + + events = self.gen_trades() + slippage_model = VolumeShareSlippage() + + # long, does not trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': 100, + 'filled': 0, + 'sid': 133, + 'stop': 4.0, + 'limit': 3.0}) + ]} + + txn = slippage_model.simulate( + events[2], + open_orders + ) + + expected_txn = {} + + self.assertIsNone(txn) + + txn = slippage_model.simulate( + events[3], + open_orders + ) + + expected_txn = {} + + self.assertIsNone(txn) + + # long, does trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': 100, + 'filled': 0, + 'sid': 133, + 'stop': 4.0, + 'limit': 3.5}) + ]} + + txn = slippage_model.simulate( + events[2], + open_orders + ) + + expected_txn = {} + + self.assertIsNone(txn) + + txn = slippage_model.simulate( + events[3], + open_orders + ) + + expected_txn = { + 'price': float(3.500875), + 'dt': datetime.datetime( + 2006, 1, 5, 14, 34, tzinfo=pytz.utc), + 'amount': int(100), + 'sid': int(133) + } + + self.assertIsNotNone(txn) + + for key, value in expected_txn.items(): + self.assertEquals(value, txn[key]) + + # short, does not trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': -100, + 'filled': 0, + 'sid': 133, + 'stop': 3.0, + 'limit': 4.0}) + ]} + + txn = slippage_model.simulate( + events[0], + open_orders + ) + + expected_txn = {} + + self.assertIsNone(txn) + + txn = slippage_model.simulate( + events[1], + open_orders + ) + + expected_txn = {} + + self.assertIsNone(txn) + + # short, does trade + + open_orders = {133: [ + Order(**{ + 'dt': datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc), + 'amount': -100, + 'filled': 0, + 'sid': 133, + 'stop': 3.0, + 'limit': 3.5}) + ]} + + txn = slippage_model.simulate( + events[0], + open_orders + ) + + expected_txn = {} + + self.assertIsNone(txn) + + txn = slippage_model.simulate( + events[1], + open_orders + ) + + expected_txn = { + 'price': float(3.499125), + 'dt': datetime.datetime( + 2006, 1, 5, 14, 32, tzinfo=pytz.utc), + 'amount': int(-100), + 'sid': int(133) + } + + self.assertIsNotNone(txn) + + for key, value in expected_txn.items(): + self.assertEquals(value, txn[key]) + + def gen_trades(self): + # create a sequence of trades + events = [ + Event({ + 'volume': 2000, + 'TRANSACTION': None, + 'type': 4, + 'price': 3.0, + 'datetime': datetime.datetime( + 2006, 1, 5, 14, 31, tzinfo=pytz.utc), + 'high': 3.15, + 'low': 2.85, + 'sid': 133, + 'source_id': 'test_source', + 'close': 3.0, + 'dt': + datetime.datetime(2006, 1, 5, 14, 31, tzinfo=pytz.utc), + 'open': 3.0 + }), + Event({ + 'volume': 2000, + 'TRANSACTION': None, + 'type': 4, + 'price': 3.5, + 'datetime': datetime.datetime( + 2006, 1, 5, 14, 32, tzinfo=pytz.utc), + 'high': 3.15, + 'low': 2.85, + 'sid': 133, + 'source_id': 'test_source', + 'close': 3.5, + 'dt': + datetime.datetime(2006, 1, 5, 14, 32, tzinfo=pytz.utc), + 'open': 3.0 + }), + Event({ + 'volume': 2000, + 'TRANSACTION': None, + 'type': 4, + 'price': 4.0, + 'datetime': datetime.datetime( + 2006, 1, 5, 14, 33, tzinfo=pytz.utc), + 'high': 3.15, + 'low': 2.85, + 'sid': 133, + 'source_id': 'test_source', + 'close': 4.0, + 'dt': + datetime.datetime(2006, 1, 5, 14, 33, tzinfo=pytz.utc), + 'open': 3.5 + }), + Event({ + 'volume': 2000, + 'TRANSACTION': None, + 'type': 4, + 'price': 3.5, + 'datetime': datetime.datetime( + 2006, 1, 5, 14, 34, tzinfo=pytz.utc), + 'high': 3.15, + 'low': 2.85, + 'sid': 133, + 'source_id': 'test_source', + 'close': 3.5, + 'dt': + datetime.datetime(2006, 1, 5, 14, 34, tzinfo=pytz.utc), + 'open': 4.0 + }), + Event({ + 'volume': 2000, + 'TRANSACTION': None, + 'type': 4, + 'price': 3.0, + 'datetime': datetime.datetime( + 2006, 1, 5, 14, 35, tzinfo=pytz.utc), + 'high': 3.15, + 'low': 2.85, + 'sid': 133, + 'source_id': 'test_source', + 'close': 3.0, + 'dt': + datetime.datetime(2006, 1, 5, 14, 35, tzinfo=pytz.utc), + 'open': 3.5 + }) + ] + return events diff --git a/tests/test_finance.py b/tests/test_finance.py index bb0c3f52..08269f5a 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -28,11 +28,12 @@ from nose.tools import timed import zipline.utils.factory as factory import zipline.utils.simfactory as simfactory +from zipline.gens.tradesimulation import Order + import zipline.finance.trading as trading from zipline.finance.trading import SimulationParameters from zipline.finance.performance import PerformanceTracker -from zipline.utils.protocol_utils import ndict from zipline.finance.trading import TransactionSimulator from zipline.utils.test_utils import( setup_logger, @@ -302,7 +303,7 @@ class FinanceTestCase(TestCase): order_date = start_date for i in xrange(order_count): - order = ndict({ + order = Order(**{ 'sid': sid, 'amount': order_amount * alternator ** i, 'dt': order_date diff --git a/zipline/finance/slippage.py b/zipline/finance/slippage.py index e13380e5..20653aaa 100644 --- a/zipline/finance/slippage.py +++ b/zipline/finance/slippage.py @@ -108,10 +108,27 @@ class VolumeShareSlippage(object): open_amount = order.amount - order.filled - if not np.allclose(open_amount, 0): - direction = math.copysign(1, open_amount) - else: - direction = 1 + if np.allclose(open_amount, 0): + continue + + direction = math.copysign(1, open_amount) + + # if the stop price is reached, simply set stop to None + # othrewise we skip this order with a continue + if order.stop is not None: + if (direction * (event.price - order.stop) < 0): + # convert stop -> limit or market + order.stop = None + else: + continue + + # if the limit price is reached, we execute this order at + # (event.price + simulated_impact) + # we skip this order with a continue when the limit is not reached + if order.limit is not None: + # if limit conditions not met, then continue + if (direction * (event.price - order.limit) > 0): + continue desired_order = total_order + open_amount @@ -146,6 +163,8 @@ class VolumeShareSlippage(object): return create_transaction( event.sid, simulated_amount, + # In the future, we may want to change the next line + # for limit pricing event.price + simulated_impact, dt.replace(tzinfo=pytz.utc), ) @@ -170,13 +189,32 @@ class FixedSlippage(object): amount = 0 for order in orders: + # what if we have 2 orders, one for 100 shares long, + # and one for 100 shares short + # such as in a hedging scenario? amount += order.amount + direction = math.copysign(1, amount) + + # if the stop price is reached, simply set stop to None + # othrewise we skip this order with a continue + if order.stop is not None: + if (direction * (event.price - order.stop) < 0): + # convert stop -> limit or market + order.stop = None + else: + continue + + # if the limit price is reached, we execute this order at + # (event.price + simulated_impact) + # we skip this order with a continue when the limit is not reached + if order.limit is not None: + # if limit conditions not met, then continue + if (direction * (event.price - order.limit) > 0): + continue if np.allclose(amount, 0): return - direction = math.copysign(1, amount) - txn = create_transaction( event.sid, amount, diff --git a/zipline/gens/tradesimulation.py b/zipline/gens/tradesimulation.py index f3d54352..91adef27 100644 --- a/zipline/gens/tradesimulation.py +++ b/zipline/gens/tradesimulation.py @@ -28,7 +28,7 @@ log = Logger('Trade Simulation') class Order(object): - def __init__(self, dt, sid, amount, filled=0): + def __init__(self, dt, sid, amount, stop=None, limit=None, filled=0): """ @dt - datetime.datetime that the order was placed @sid - stock sid of the order @@ -41,6 +41,11 @@ class Order(object): self.sid = sid self.amount = amount self.filled = filled + self.stop = stop + self.limit = limit + + def __getitem__(self, name): + return self.__dict__[name] class TradeSimulationClient(object): @@ -181,33 +186,49 @@ class AlgorithmSimulator(object): record.extra['algo_dt'] = self.snapshot_dt self.processor = Processor(inject_algo_dt) - def order(self, sid, amount): - """ - Closure to pass into the user's algo to allow placing orders - into the transaction simulator's dict of open orders. - """ - order = Order(**{ - 'dt': self.simulation_dt, - 'sid': sid, - 'amount': int(amount), - 'filled': 0 - }) + def order(self, sid, amount, limit_price=None, stop_price=None): + # something could be done with amount to further divide + # between buy by share count OR buy shares up to a dollar amount + # numeric == share count AND "$dollar.cents" == cost amount + + """ + amount > 0 :: Buy/Cover + amount < 0 :: Sell/Short + Market order: order(sid,amount) + Limit order: order(sid,amount, limit_price) + Stop order: order(sid,amount, None, stop_price) + StopLimit order: order(sid,amount, limit_price, stop_price) + """ + + # just validates amount and passes rest on to TransactionSimulator # Tell the user if they try to buy 0 shares of something. - if order.amount == 0: - zero_message = "Requested to trade zero shares of {sid}".format( - sid=order.sid + if amount == 0: + zero_message = "Requested to trade zero shares of {psid}".format( + psid=sid ) log.debug(zero_message) # Don't bother placing orders for 0 shares. return + order = Order(**{ + 'dt': self.simulation_dt, + 'sid': sid, + 'amount': int(amount), + 'filled': 0, + 'stop': stop_price, + 'limit': limit_price + }) + # Add non-zero orders to the order book. # !!!IMPORTANT SIDE-EFFECT!!! # This modifies the internal state of the transaction # simulator so that it can fill the placed order when it # receives its next message. - self.order_book.place_order(order) + err_str = self.order_book.place_order(order) + if err_str is not None and len(err_str) > 0: + # error, trade was not placed, log it out + log.debug(err_str) def transform(self, stream_in): """