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.
This commit is contained in:
Scott Sanderson
2014-04-25 11:52:28 -04:00
parent 17df628171
commit 5373b6797c
2 changed files with 55 additions and 27 deletions
+35 -15
View File
@@ -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))
+20 -12
View File
@@ -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: [<X-1>.0095, X.0195) -> round to X.01,
sell: (<X-1>.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: [<X-1>.0095, X.0195) -> round to X.01.
If not prefer_round_down: (<X-1>.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)