Skip to content
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

Merged
merged 19 commits into from
Jul 12, 2018
Merged

Conversation

jnazaren
Copy link
Contributor

Solves rounding bug which caused rounding of all asset stop/limit prices to the nearest penny regardless of their tick size.

@coveralls
Copy link

Coverage Status

Coverage increased (+0.06%) to 86.391% when pulling 9707d26 on rounding_bugfix into c20a588 on master.

@coveralls
Copy link

coveralls commented Jun 13, 2018

Coverage Status

Coverage decreased (-0.6%) to 85.443% when pulling d433521 on rounding_bugfix into f61d3df on master.

@@ -158,19 +189,31 @@ def asymmetric_round_price_to_penny(price, prefer_round_down,
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.
"""
precision = get_precision(tick_size)
diff *= (10 ** -precision)
print(price, precision, diff)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While useful for debugging, we probably don't want to merge with the print.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yep! Forgot to take that out, sorry



def asymmetric_round_price_to_penny(price, prefer_round_down,
diff=(0.0095 - .005)):
def asymmetric_round_price(price, prefer_round_down,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there tests for this method? How does it handle tick sizes like .0005?

Copy link
Contributor Author

@jnazaren jnazaren Jun 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prsutherland I actually noticed that all of the tests that used this functionality were fine with the normal precision of this function (at least 0.01), which is why I left 0.01 as a default parameter, but I can now add some tests for smaller precision

return 0.0
rounded = round(price - (diff if prefer_round_down else -diff), precision)
# if zp_math.tolerant_equals(rounded, 0.0):
# return 0.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this dead code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@prsutherland It could be, but I'm not sure if we may need it - it seems unnecessary now that precision could be as small as desired

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it is needed to avoid near zero floating point errors. For example, I believe without it, the code will return

>>> asymmetric_round_price(.004, True, tick_size=0.005)
-0.0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll leave it in that case


def get_limit_price(self, is_buy):
return asymmetric_round_price_to_penny(self.limit_price, is_buy)
if hasattr(self.asset, 'tick_size'):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like roughly the same block is repeated ~4 times. Can the pattern be abstracted?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would make sense to put tick_size on all asset types, with a default value? Any performance or domain modeling concerns?

@@ -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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is tick_size a string?

Copy link
Contributor

Choose a reason for hiding this comment

The 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 Future below. Still not sure I understand why though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 object rather than a float in the cython definitions.


def _get_stop_price(asset, price, is_buy):
if hasattr(asset, 'tick_size'):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this if statement can be removed now that all assets have a tick_size.

@@ -160,8 +160,8 @@ class Order(Event):
)


def asset_multiplier(asset):
return asset.multiplier if isinstance(asset, Future) else 1
def asset_price_multiplier(asset):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably replace invocations of this method with asset.price_multiplier.

return asymmetric_round_price(
price,
not is_buy,
tick_size=float(asset.tick_size)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this PR, we just cast tick_size to a float. Are there other uses that we want it to be a string?

style = LimitOrder(price, asset=self.FUTURE)

assert_equal(expected_limit_buy_or_stop_sell,
style.get_limit_price(True))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make the calls to style.get_limit_price and style.get_stop_price in these tests pass in constants/flags as kwargs? style.get_limit_price(is_buy=True) is easier to understand what the flag means.

return pd.DataFrame.from_dict({
1: {
'multiplier': 100,
'tick_size': '0.0001',
Copy link
Contributor

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 want asymmetric_round_price to return a multiple of the tick_size, but for non-powers of 10, one gets:

>>> asymmetric_round_price(.0049, prefer_round_down=True, tick_size=0.005)
0.004

Copy link
Contributor

@prsutherland prsutherland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, looks good, just two small questions before merging.

@@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank's for cleaning up the stringly-typed tick_size field! Can this be float tick_size now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can change that

@@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Equities, do we want a different default size? The get_stop_price and get_limit_price default to 0.01 if there is no asset.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll change it to 0.01

Copy link
Contributor

@prsutherland prsutherland Jul 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you updated Equity to the 0.01 tick size. Why the default value of 0.001 for futures?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A while back I had asked @ehebert for a default tick size and that's the value he gave, but I believe the default was 0.01 previously

Copy link
Contributor

@prsutherland prsutherland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LGTM once we figure out what the defaults should be for futures and equities.

@jnazaren jnazaren merged commit 7e642c8 into master Jul 12, 2018
@jnazaren jnazaren deleted the rounding_bugfix branch July 12, 2018 19:38
taoxuau pushed a commit to taoxuau/zipline that referenced this pull request Aug 1, 2019
Includes a reorganization of the `Asset` class, as well as the revision in the logic of rounding in the `StopOrder`, `LimitOrder`, and `StopLimitOrder` classes. Some fields such as `price_multiplier` and `tick_size` have been moved up to the `Asset` class, and are now shared by futures and equities. Finally, all rounding done in the order classes will be based on the `tick_size` of assets.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants