From 28f86bc14e55aaae86323f1abde539af005ac094 Mon Sep 17 00:00:00 2001 From: Peter Cawthron Date: Wed, 16 Oct 2013 15:14:58 +0100 Subject: [PATCH 1/3] BUG: Fix handling of STOP, LIMIT and STOP LIMIT Orders Includes specific handling of Buy Stop, Sell Stop, Buy Limit, Sell Limit, Buy Stop Limit and Sell Stop Limit orders. --- zipline/finance/blotter.py | 6 +++-- zipline/finance/slippage.py | 50 ++++++++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/zipline/finance/blotter.py b/zipline/finance/blotter.py index 3af95fd5..cab04045 100644 --- a/zipline/finance/blotter.py +++ b/zipline/finance/blotter.py @@ -282,13 +282,16 @@ class Order(object): Update internal state based on price triggers and the trade event's price. """ - stop_reached, limit_reached = \ + stop_reached, limit_reached, sl_stop_reached = \ check_order_triggers(self, event) if (stop_reached, limit_reached) \ != (self.stop_reached, self.limit_reached): self.dt = event.dt self.stop_reached = stop_reached self.limit_reached = limit_reached + if sl_stop_reached: + # Change the STOP LIMIT order into a LIMIT order + self.stop = None def handle_split(self, split_event): ratio = split_event.ratio @@ -329,7 +332,6 @@ class Order(object): For a market order, True. For a stop order, True IFF stop_reached. For a limit order, True IFF limit_reached. - For a stop-limit order, True IFF (stop_reached AND limit_reached) """ if self.stop and not self.stop_reached: return False diff --git a/zipline/finance/slippage.py b/zipline/finance/slippage.py index b4c2895a..4ff0226f 100644 --- a/zipline/finance/slippage.py +++ b/zipline/finance/slippage.py @@ -31,30 +31,52 @@ def check_order_triggers(order, event): For market orders, will return (False, False). For stop orders, limit_reached will always be False. For limit orders, stop_reached will always be False. + For stop limit orders a Boolean is returned to flag + that the stop has been reached. Orders that have been triggered already (price targets reached), the order's current values are returned. """ if order.triggered: - return (order.stop_reached, order.limit_reached) + return (order.stop_reached, order.limit_reached, False) stop_reached = False limit_reached = False - # if the stop price is reached, simply set stop_reached + sl_stop_reached = False if order.stop is not None: - if (order.direction * (event.price - order.stop) <= 0): - # convert stop -> limit or market - stop_reached = True + if order.limit is not None: + if order.amount > 0: + # This is a BUY STOP LIMIT order + if event.price >= order.stop: + sl_stop_reached = True + if event.price <= order.limit: + limit_reached = True + else: + # This is a SELL STOP LIMIT order + if event.price <= order.stop: + sl_stop_reached = True + if event.price >= order.limit: + limit_reached = True + else: + if order.amount > 0: + # This is a BUY STOP order + if event.price >= order.stop: + stop_reached = True + else: + # This is a SELL STOP order + if event.price <= order.stop: + stop_reached = True + else: + if order.amount > 0: + # This is BUY LIMIT order + if event.price <= order.limit: + limit_reached = True + else: + # This is a SELL LIMIT order + if event.price >= order.limit: + limit_reached = True - # 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 (order.direction * (event.price - order.limit) <= 0): - limit_reached = True - - return (stop_reached, limit_reached) + return (stop_reached, limit_reached, sl_stop_reached) def transact_stub(slippage, commission, event, open_orders): From 32c1f935726542fe52e794e4c65b05d948281a2f Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Fri, 25 Oct 2013 15:29:06 -0400 Subject: [PATCH 2/3] TST: Updates tests to fit fixed stop behavior. Change the answers to the stop order tests to match the corrected behavior, where results are opposite from previous behavior. --- tests/finance/test_slippage.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/finance/test_slippage.py b/tests/finance/test_slippage.py index 3a5f41ee..d935a4e7 100644 --- a/tests/finance/test_slippage.py +++ b/tests/finance/test_slippage.py @@ -238,7 +238,12 @@ class SlippageTestCase(TestCase): 'open': 3.5 }, 'expected': { - 'transaction': None + 'transaction': { + 'price': 4.001, + 'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'), + 'amount': 100, + 'sid': 133, + } } }, 'long | price lt stop': { @@ -260,13 +265,8 @@ class SlippageTestCase(TestCase): 'open': 4.0 }, 'expected': { - 'transaction': { - 'price': 3.500875, - 'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'), - 'amount': 100, - 'sid': 133, - } - }, + 'transaction': None + } }, 'short | price gt stop': { 'order': { @@ -287,12 +287,7 @@ class SlippageTestCase(TestCase): 'open': 3.0 }, 'expected': { - 'transaction': { - 'price': 3.499125, - 'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'), - 'amount': -100, - 'sid': 133, - } + 'transaction': None } }, 'short | price lt stop': { @@ -314,7 +309,12 @@ class SlippageTestCase(TestCase): 'open': 3.0 }, 'expected': { - 'transaction': None + 'transaction': { + 'price': 2.99925, + 'dt': pd.Timestamp('2006-01-05 14:31', tz='UTC'), + 'amount': -100, + 'sid': 133, + } } }, } From 7412cc97a0a7e05d238bc3d99a77804b9bab6c8b Mon Sep 17 00:00:00 2001 From: Eddie Hebert Date: Fri, 25 Oct 2013 16:14:11 -0400 Subject: [PATCH 3/3] MAINT: Use bitwise flags to help order cases easier to follow. Instead of nesting order direction and related stop and limit logic, derive a bitwise mask from the combination of order configurations and use the mask as a 'switch'. --- zipline/finance/slippage.py | 72 +++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/zipline/finance/slippage.py b/zipline/finance/slippage.py index 4ff0226f..0224a5ce 100644 --- a/zipline/finance/slippage.py +++ b/zipline/finance/slippage.py @@ -23,6 +23,11 @@ from functools import partial from zipline.protocol import DATASOURCE_TYPE import zipline.utils.math_utils as zp_math +SELL = 0 +BUY = 1 +STOP = 1 << 1 +LIMIT = 1 << 2 + def check_order_triggers(order, event): """ @@ -43,38 +48,43 @@ def check_order_triggers(order, event): stop_reached = False limit_reached = False sl_stop_reached = False - if order.stop is not None: - if order.limit is not None: - if order.amount > 0: - # This is a BUY STOP LIMIT order - if event.price >= order.stop: - sl_stop_reached = True - if event.price <= order.limit: - limit_reached = True - else: - # This is a SELL STOP LIMIT order - if event.price <= order.stop: - sl_stop_reached = True - if event.price >= order.limit: - limit_reached = True - else: - if order.amount > 0: - # This is a BUY STOP order - if event.price >= order.stop: - stop_reached = True - else: - # This is a SELL STOP order - if event.price <= order.stop: - stop_reached = True + + order_type = 0 + + if order.amount > 0: + order_type |= BUY else: - if order.amount > 0: - # This is BUY LIMIT order - if event.price <= order.limit: - limit_reached = True - else: - # This is a SELL LIMIT order - if event.price >= order.limit: - limit_reached = True + order_type |= SELL + + if order.stop is not None: + order_type |= STOP + + if order.limit is not None: + order_type |= LIMIT + + if order_type == BUY | STOP | LIMIT: + if event.price >= order.stop: + sl_stop_reached = True + if event.price <= order.limit: + limit_reached = True + elif order_type == SELL | STOP | LIMIT: + if event.price <= order.stop: + sl_stop_reached = True + if event.price >= order.limit: + limit_reached = True + elif order_type == BUY | STOP: + if event.price >= order.stop: + stop_reached = True + elif order_type == SELL | STOP: + if event.price <= order.stop: + stop_reached = True + elif order_type == BUY | LIMIT: + if event.price <= order.limit: + limit_reached = True + elif order_type == SELL | LIMIT: + # This is a SELL LIMIT order + if event.price >= order.limit: + limit_reached = True return (stop_reached, limit_reached, sl_stop_reached)