New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BUG: Fix rounding error in StopOrder/LimitOrder #2211
Changes from 11 commits
9707d26
44236fe
c46a563
523aee3
aff3270
aaa6ec2
362e83f
99f1525
90190b6
4d0f510
091606e
712c3e8
c1cba61
a4d3c00
cfb67ea
9f5cf20
d433521
783e926
a59aed4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
# limitations under the License. | ||
from nose_parameterized import parameterized | ||
from six.moves import range | ||
import pandas as pd | ||
|
||
from zipline.errors import BadOrderParameters | ||
from zipline.finance.execution import ( | ||
|
@@ -25,10 +26,15 @@ | |
from zipline.testing.fixtures import ( | ||
WithLogger, | ||
ZiplineTestCase, | ||
WithConstantFutureMinuteBarData | ||
) | ||
|
||
from zipline.testing.predicates import assert_equal | ||
|
||
class ExecutionStyleTestCase(WithLogger, ZiplineTestCase): | ||
|
||
class ExecutionStyleTestCase(WithConstantFutureMinuteBarData, | ||
WithLogger, | ||
ZiplineTestCase): | ||
""" | ||
Tests for zipline ExecutionStyle classes. | ||
""" | ||
|
@@ -46,6 +52,21 @@ class ExecutionStyleTestCase(WithLogger, ZiplineTestCase): | |
(0.01, 0.01, 0.01) | ||
] | ||
|
||
# Testing for an asset with a tick_size of 0.0001 | ||
smaller_epsilon = 0.00000001 | ||
|
||
EXPECTED_PRECISION_ROUNDING = [ | ||
(0.00, 0.00, 0.00), | ||
(0.0005, 0.0005, 0.0005), | ||
(0.00005, 0.00, 0.0001), | ||
(0.000005, 0.00, 0.00), | ||
(1.000005, 1.00, 1.00), # Lowest value to round down on sell. | ||
(1.000005 + smaller_epsilon, 1.00, 1.0001), | ||
(1.000095 - smaller_epsilon, 1.0, 1.0001), | ||
(1.000095, 1.0001, 1.0001), # Highest value to round up on buy. | ||
(0.01, 0.01, 0.01) | ||
] | ||
|
||
# Test that the same rounding behavior is maintained if we add between 1 | ||
# and 10 to all values, because floating point math is made of lies. | ||
EXPECTED_PRICE_ROUNDING += [ | ||
|
@@ -68,6 +89,22 @@ def __str__(self): | |
(ArbitraryObject(),), | ||
] | ||
|
||
@classmethod | ||
def make_futures_info(cls): | ||
return pd.DataFrame.from_dict({ | ||
1: { | ||
'multiplier': 100, | ||
'tick_size': '0.0001', | ||
'symbol': 'F', | ||
'exchange': 'TEST' | ||
} | ||
}, orient='index') | ||
|
||
@classmethod | ||
def init_class_fixtures(cls): | ||
super(ExecutionStyleTestCase, cls).init_class_fixtures() | ||
cls.FUTURE = cls.asset_finder.retrieve_asset(1) | ||
|
||
@parameterized.expand(INVALID_PRICES) | ||
def test_invalid_prices(self, price): | ||
""" | ||
|
@@ -155,3 +192,65 @@ def test_stop_limit_order_prices(self, | |
style.get_stop_price(False)) | ||
self.assertEqual(expected_limit_sell_or_stop_buy + 1, | ||
style.get_stop_price(True)) | ||
|
||
@parameterized.expand(EXPECTED_PRECISION_ROUNDING) | ||
def test_limit_order_precision(self, | ||
price, | ||
expected_limit_buy_or_stop_sell, | ||
expected_limit_sell_or_stop_buy): | ||
""" | ||
Test price getters for the LimitOrder class with an asset that | ||
has a tick_size of 0.0001. | ||
""" | ||
style = LimitOrder(price, asset=self.FUTURE) | ||
|
||
assert_equal(expected_limit_buy_or_stop_sell, | ||
style.get_limit_price(True)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you make the calls to |
||
assert_equal(expected_limit_sell_or_stop_buy, | ||
style.get_limit_price(False)) | ||
|
||
assert_equal(None, style.get_stop_price(True)) | ||
assert_equal(None, style.get_stop_price(False)) | ||
|
||
@parameterized.expand(EXPECTED_PRECISION_ROUNDING) | ||
def test_stop_order_precision(self, | ||
price, | ||
expected_limit_buy_or_stop_sell, | ||
expected_limit_sell_or_stop_buy): | ||
""" | ||
Test price getters for StopOrder class with an asset that | ||
has a tick_size of 0.0001. Note that the expected rounding | ||
direction for stop prices is the reverse of that for limit prices. | ||
""" | ||
style = StopOrder(price, asset=self.FUTURE) | ||
|
||
assert_equal(None, style.get_limit_price(False)) | ||
assert_equal(None, style.get_limit_price(True)) | ||
|
||
assert_equal(expected_limit_buy_or_stop_sell, | ||
style.get_stop_price(False)) | ||
assert_equal(expected_limit_sell_or_stop_buy, | ||
style.get_stop_price(True)) | ||
|
||
@parameterized.expand(EXPECTED_PRECISION_ROUNDING) | ||
def test_stop_limit_order_precision(self, | ||
price, | ||
expected_limit_buy_or_stop_sell, | ||
expected_limit_sell_or_stop_buy): | ||
""" | ||
Test price getters for StopLimitOrder class with an asset that | ||
has a tick_size of 0.0001. Note that the expected rounding direction | ||
for stop prices is the reverse of that for limit prices. | ||
""" | ||
|
||
style = StopLimitOrder(price, price + 1.00, asset=self.FUTURE) | ||
|
||
assert_equal(expected_limit_buy_or_stop_sell, | ||
style.get_limit_price(True)) | ||
assert_equal(expected_limit_sell_or_stop_buy, | ||
style.get_limit_price(False)) | ||
|
||
assert_equal(expected_limit_buy_or_stop_sell + 1, | ||
style.get_stop_price(False)) | ||
assert_equal(expected_limit_sell_or_stop_buy + 1, | ||
style.get_stop_price(True)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,6 +54,8 @@ cdef class Asset: | |
'auto_close_date', | ||
'exchange', | ||
'exchange_full', | ||
'tick_size', | ||
'multiplier', | ||
}) | ||
|
||
def __init__(self, | ||
|
@@ -65,7 +67,9 @@ cdef class Asset: | |
object end_date=None, | ||
object first_traded=None, | ||
object auto_close_date=None, | ||
object exchange_full=None): | ||
object exchange_full=None, | ||
object tick_size="0.001", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I see that it was previously defaulting to an empty string in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I left it as a string primarily because I noticed its default value was an empty string. Similarly, it was defined as an |
||
float multiplier=1.0): | ||
|
||
self.sid = sid | ||
self.symbol = symbol | ||
|
@@ -77,6 +81,8 @@ cdef class Asset: | |
self.end_date = end_date | ||
self.first_traded = first_traded | ||
self.auto_close_date = auto_close_date | ||
self.tick_size = tick_size | ||
self.price_multiplier = multiplier | ||
|
||
def __int__(self): | ||
return self.sid | ||
|
@@ -144,7 +150,9 @@ cdef class Asset: | |
self.end_date, | ||
self.first_traded, | ||
self.auto_close_date, | ||
self.exchange_full)) | ||
self.exchange_full, | ||
self.tick_size, | ||
self.price_multiplier)) | ||
|
||
cpdef to_dict(self): | ||
""" | ||
|
@@ -160,6 +168,8 @@ cdef class Asset: | |
'auto_close_date': self.auto_close_date, | ||
'exchange': self.exchange, | ||
'exchange_full': self.exchange_full, | ||
'tick_size': self.tick_size, | ||
'multiplier': self.price_multiplier, | ||
} | ||
|
||
@classmethod | ||
|
@@ -254,9 +264,9 @@ cdef class Future(Asset): | |
'auto_close_date', | ||
'first_traded', | ||
'exchange', | ||
'exchange_full', | ||
'tick_size', | ||
'multiplier', | ||
'exchange_full', | ||
}) | ||
|
||
def __init__(self, | ||
|
@@ -271,7 +281,7 @@ cdef class Future(Asset): | |
object expiration_date=None, | ||
object auto_close_date=None, | ||
object first_traded=None, | ||
object tick_size="", | ||
object tick_size="0.001", | ||
float multiplier=1.0, | ||
object exchange_full=None): | ||
|
||
|
@@ -285,12 +295,12 @@ cdef class Future(Asset): | |
first_traded=first_traded, | ||
auto_close_date=auto_close_date, | ||
exchange_full=exchange_full, | ||
tick_size=tick_size, | ||
multiplier=multiplier | ||
) | ||
self.root_symbol = root_symbol | ||
self.notice_date = notice_date | ||
self.expiration_date = expiration_date | ||
self.tick_size = tick_size | ||
self.multiplier = multiplier | ||
|
||
if auto_close_date is None: | ||
if notice_date is None: | ||
|
@@ -300,6 +310,17 @@ cdef class Future(Asset): | |
else: | ||
self.auto_close_date = min(notice_date, expiration_date) | ||
|
||
property multiplier: | ||
""" | ||
DEPRECATION: This property should be deprecated and is only present for | ||
backwards compatibility | ||
""" | ||
def __get__(self): | ||
warnings.warn("The multiplier property will soon be " | ||
"retired. Please use the price_multiplier property instead.", | ||
DeprecationWarning) | ||
return self.price_multiplier | ||
|
||
cpdef __reduce__(self): | ||
""" | ||
Function used by pickle to determine how to serialize/deserialize this | ||
|
@@ -319,7 +340,7 @@ cdef class Future(Asset): | |
self.auto_close_date, | ||
self.first_traded, | ||
self.tick_size, | ||
self.multiplier, | ||
self.price_multiplier, | ||
self.exchange_full)) | ||
|
||
cpdef to_dict(self): | ||
|
@@ -331,7 +352,7 @@ cdef class Future(Asset): | |
super_dict['notice_date'] = self.notice_date | ||
super_dict['expiration_date'] = self.expiration_date | ||
super_dict['tick_size'] = self.tick_size | ||
super_dict['multiplier'] = self.multiplier | ||
super_dict['multiplier'] = self.price_multiplier | ||
return super_dict | ||
|
||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add tests for
tick_sizes
that aren't powers of 10? I think we always wantasymmetric_round_price
to return a multiple of thetick_size
, but for non-powers of 10, one gets: