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)