From b4836b976e22ef54fc4a924eac4f48f11be9300e Mon Sep 17 00:00:00 2001 From: Richard Frank Date: Fri, 20 Sep 2013 15:55:24 -0400 Subject: [PATCH 1/2] ENH: Restrict limit prices to a penny precision to account for minimum price variation. On an order to buy, between .05 below to .95 above a penny, use that penny. On an order to sell, between .05 above to .95 below a penny, use that penny. --- tests/test_blotter.py | 29 +++++++++++++++++++++++++++++ zipline/finance/blotter.py | 11 +++++++++++ 2 files changed, 40 insertions(+) create mode 100644 tests/test_blotter.py diff --git a/tests/test_blotter.py b/tests/test_blotter.py new file mode 100644 index 00000000..c722f5cc --- /dev/null +++ b/tests/test_blotter.py @@ -0,0 +1,29 @@ +from nose_parameterized import parameterized +from unittest import TestCase + +from zipline.finance.blotter import round_for_minimum_price_variation + + +class BlotterTestCase(TestCase): + + @parameterized.expand([(0.00, 0.00), + (0.01, 0.01), + (0.0005, 0.00), + (1.006, 1.00), + (1.0095, 1.01), + (1.00949, 1.00), + (1.0005, 1.00)]) + def test_round_for_minimum_price_variation_buy(self, price, expected): + result = round_for_minimum_price_variation(price, is_buy=True) + self.assertEqual(result, expected) + + @parameterized.expand([(0.00, 0.00), + (0.01, 0.01), + (0.0005, 0.00), + (1.006, 1.01), + (1.0005, 1.00), + (1.00051, 1.01), + (1.0095, 1.01)]) + def test_round_for_minimum_price_variation_sell(self, price, expected): + result = round_for_minimum_price_variation(price, is_buy=False) + self.assertEqual(result, expected) diff --git a/zipline/finance/blotter.py b/zipline/finance/blotter.py index 9bd1f588..e592c60b 100644 --- a/zipline/finance/blotter.py +++ b/zipline/finance/blotter.py @@ -42,6 +42,14 @@ ORDER_STATUS = Enum( ) +# On an order to buy, between .05 below to .95 above a penny, use that penny. +# On an order to sell, between .05 above to .95 below a penny, use that penny. +# buy: [.0095, .0195) -> round to .01, sell: (.0005, .0105] -> round to .01 +def round_for_minimum_price_variation(x, is_buy, diff=(0.0095 - .005)): + # relies on rounding half away from zero, unlike numpy's bankers' rounding + return round(x - (diff if is_buy else -diff), 2) + + class Blotter(object): def __init__(self): @@ -107,6 +115,9 @@ class Blotter(object): raise OverflowError("Can't order more than %d shares" % self.max_shares) + if limit_price: + limit_price = round_for_minimum_price_variation(limit_price, + amount > 0) order = Order( dt=self.current_dt, sid=sid, From 599ff1ad8a393d739db89e04e8a226f7fee8c901 Mon Sep 17 00:00:00 2001 From: Richard Frank Date: Fri, 20 Sep 2013 16:28:09 -0400 Subject: [PATCH 2/2] MAINT: Ensure the sign of the result is positive --- tests/test_blotter.py | 6 ++++++ zipline/finance/blotter.py | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_blotter.py b/tests/test_blotter.py index c722f5cc..b47a282a 100644 --- a/tests/test_blotter.py +++ b/tests/test_blotter.py @@ -1,3 +1,5 @@ +import math + from nose_parameterized import parameterized from unittest import TestCase @@ -16,6 +18,8 @@ class BlotterTestCase(TestCase): def test_round_for_minimum_price_variation_buy(self, price, expected): result = round_for_minimum_price_variation(price, is_buy=True) self.assertEqual(result, expected) + self.assertEqual(math.copysign(1.0, result), + math.copysign(1.0, expected)) @parameterized.expand([(0.00, 0.00), (0.01, 0.01), @@ -27,3 +31,5 @@ class BlotterTestCase(TestCase): def test_round_for_minimum_price_variation_sell(self, price, expected): result = round_for_minimum_price_variation(price, is_buy=False) self.assertEqual(result, expected) + self.assertEqual(math.copysign(1.0, result), + math.copysign(1.0, expected)) diff --git a/zipline/finance/blotter.py b/zipline/finance/blotter.py index e592c60b..3af95fd5 100644 --- a/zipline/finance/blotter.py +++ b/zipline/finance/blotter.py @@ -47,7 +47,10 @@ ORDER_STATUS = Enum( # buy: [.0095, .0195) -> round to .01, sell: (.0005, .0105] -> round to .01 def round_for_minimum_price_variation(x, is_buy, diff=(0.0095 - .005)): # relies on rounding half away from zero, unlike numpy's bankers' rounding - return round(x - (diff if is_buy else -diff), 2) + rounded = round(x - (diff if is_buy else -diff), 2) + if zp_math.tolerant_equals(rounded, 0.0): + return 0.0 + return rounded class Blotter(object):