From 5373b6797c321d5873608e774495cc5894d2dd42 Mon Sep 17 00:00:00 2001 From: Scott Sanderson Date: Fri, 25 Apr 2014 11:52:28 -0400 Subject: [PATCH] ENH: Treat stop and limit prices differently when rounding. Stop and limit prices both trigger when a price crosses some threshold, but they trigger in "opposite directions". For example, on a buy, a limit price is triggered when a price falls below a specified value, whereas a stop price triggers when the price exceeds a specified value. Our current stop/limit price rounding logic is asymmetric, preferring to "round to improve" the specified price. This change makes it so that we interpret "improvement" in opposite directions for stop vs limit prices. --- tests/test_execution_styles.py | 50 ++++++++++++++++++++++++---------- zipline/finance/execution.py | 32 ++++++++++++++-------- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/tests/test_execution_styles.py b/tests/test_execution_styles.py index 8aa5de14..dc1b44c5 100644 --- a/tests/test_execution_styles.py +++ b/tests/test_execution_styles.py @@ -39,7 +39,7 @@ class ExecutionStyleTestCase(TestCase): epsilon = .000001 - # Input, expected for buy, expected for sell. + # Input, expected on limit buy/stop sell, expected on limit sell/stop buy. EXPECTED_PRICE_ROUNDING = [ (0.00, 0.00, 0.00), (0.0005, 0.00, 0.00), @@ -95,41 +95,61 @@ class ExecutionStyleTestCase(TestCase): self.assertEqual(style.get_stop_price(False), None) @parameterized.expand(EXPECTED_PRICE_ROUNDING) - def test_limit_order_prices(self, price, expected_buy, expected_sell): + def test_limit_order_prices(self, + price, + expected_limit_buy_or_stop_sell, + expected_limit_sell_or_stop_buy): """ Test price getters for the LimitOrder class. """ style = LimitOrder(price) - self.assertEqual(expected_buy, style.get_limit_price(True)) - self.assertEqual(expected_sell, style.get_limit_price(False)) + self.assertEqual(expected_limit_buy_or_stop_sell, + style.get_limit_price(True)) + self.assertEqual(expected_limit_sell_or_stop_buy, + style.get_limit_price(False)) self.assertEqual(None, style.get_stop_price(True)) self.assertEqual(None, style.get_stop_price(False)) @parameterized.expand(EXPECTED_PRICE_ROUNDING) - def test_stop_order_prices(self, price, expected_buy, expected_sell): + def test_stop_order_prices(self, + price, + expected_limit_buy_or_stop_sell, + expected_limit_sell_or_stop_buy): """ - Test price getters for StopOrder class. + Test price getters for StopOrder class. Note that the expected rounding + direction for stop prices is the reverse of that for limit prices. """ style = StopOrder(price) - self.assertEqual(None, style.get_limit_price(True)) self.assertEqual(None, style.get_limit_price(False)) + self.assertEqual(None, style.get_limit_price(True)) - self.assertEqual(expected_buy, style.get_stop_price(True)) - self.assertEqual(expected_sell, style.get_stop_price(False)) + self.assertEqual(expected_limit_buy_or_stop_sell, + style.get_stop_price(False)) + self.assertEqual(expected_limit_sell_or_stop_buy, + style.get_stop_price(True)) @parameterized.expand(EXPECTED_PRICE_ROUNDING) - def test_stop_limit_order_prices(self, price, expected_buy, expected_sell): + def test_stop_limit_order_prices(self, + price, + expected_limit_buy_or_stop_sell, + expected_limit_sell_or_stop_buy): """ - Test price getters for StopLimitOrder class. + Test price getters for StopLimitOrder class. Note that the expected + rounding direction for stop prices is the reverse of that for limit + prices. """ style = StopLimitOrder(price, price + 1) - self.assertEqual(expected_buy, style.get_limit_price(True)) - self.assertEqual(expected_sell, style.get_limit_price(False)) + self.assertEqual(expected_limit_buy_or_stop_sell, + style.get_limit_price(True)) + self.assertEqual(expected_limit_sell_or_stop_buy, + style.get_limit_price(False)) - self.assertEqual(expected_buy + 1, style.get_stop_price(True)) - self.assertEqual(expected_sell + 1, style.get_stop_price(False)) + self.assertEqual(expected_limit_buy_or_stop_sell + 1, + style.get_stop_price(False)) + self.assertEqual(expected_limit_sell_or_stop_buy + 1, + style.get_stop_price(True)) diff --git a/zipline/finance/execution.py b/zipline/finance/execution.py index 3d9c0516..04226e7b 100644 --- a/zipline/finance/execution.py +++ b/zipline/finance/execution.py @@ -22,14 +22,22 @@ from six import with_metaclass import zipline.utils.math_utils as zp_math -def round_for_minimum_price_variation(x, is_buy, - diff=(0.0095 - .005)): +def asymmetric_round_price_to_penny(price, prefer_round_down, + diff=(0.0095 - .005)): """ - On an order to buy, between .05 below to .95 above a penny, use that penny. - On an order to sell, between .95 below to .05 above a penny, use that - penny. - buy: [.0095, X.0195) -> round to X.01, - sell: (.0005, X.0105] -> round to X.01 + Asymmetric rounding function for adjusting prices to two places in a way + that "improves" the price. For limit prices, this means preferring to + round down on buys and preferring to round up on sells. For stop prices, + it means the reverse. + + If prefer_round_down == True: + When .05 below to .95 above a penny, use that penny. + If prefer_round_down == False: + When .95 below to .05 above a penny, use that penny. + + In math-speak: + If prefer_round_down: [.0095, X.0195) -> round to X.01. + If not prefer_round_down: (.0005, X.0105] -> round to X.01. """ # Subtracting an epsilon from diff to enforce the open-ness of the upper # bound on buys and the lower bound on sells. Using the actual system @@ -38,7 +46,7 @@ def round_for_minimum_price_variation(x, is_buy, diff = diff - epsilon # relies on rounding half away from zero, unlike numpy's bankers' rounding - rounded = round(x - (diff if is_buy else -diff), 2) + rounded = round(price - (diff if prefer_round_down else -diff), 2) if zp_math.tolerant_equals(rounded, 0.0): return 0.0 return rounded @@ -95,7 +103,7 @@ class LimitOrder(ExecutionStyle): self.limit_price = limit_price def get_limit_price(self, is_buy): - return round_for_minimum_price_variation(self.limit_price, is_buy) + return asymmetric_round_price_to_penny(self.limit_price, is_buy) def get_stop_price(self, _is_buy): return None @@ -120,7 +128,7 @@ class StopOrder(ExecutionStyle): return None def get_stop_price(self, is_buy): - return round_for_minimum_price_variation(self.stop_price, is_buy) + return asymmetric_round_price_to_penny(self.stop_price, not is_buy) class StopLimitOrder(ExecutionStyle): @@ -145,7 +153,7 @@ class StopLimitOrder(ExecutionStyle): self.stop_price = stop_price def get_limit_price(self, is_buy): - return round_for_minimum_price_variation(self.limit_price, is_buy) + return asymmetric_round_price_to_penny(self.limit_price, is_buy) def get_stop_price(self, is_buy): - return round_for_minimum_price_variation(self.stop_price, is_buy) + return asymmetric_round_price_to_penny(self.stop_price, not is_buy)