-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Conversation
zipline/finance/execution.py
Outdated
@@ -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) |
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.
While useful for debugging, we probably don't want to merge with the print
.
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.
Oh yep! Forgot to take that out, sorry
zipline/finance/execution.py
Outdated
|
||
|
||
def asymmetric_round_price_to_penny(price, prefer_round_down, | ||
diff=(0.0095 - .005)): | ||
def asymmetric_round_price(price, prefer_round_down, |
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.
Are there tests for this method? How does it handle tick sizes like .0005
?
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.
@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
zipline/finance/execution.py
Outdated
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 |
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.
Is this dead code?
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.
@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
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.
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
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.
Ok, I'll leave it in that case
zipline/finance/execution.py
Outdated
|
||
def get_limit_price(self, is_buy): | ||
return asymmetric_round_price_to_penny(self.limit_price, is_buy) | ||
if hasattr(self.asset, 'tick_size'): |
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.
It looks like roughly the same block is repeated ~4 times. Can the pattern be abstracted?
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.
Would make sense to put tick_size
on all asset types, with a default value? Any performance or domain modeling concerns?
zipline/assets/_assets.pyx
Outdated
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
Why is tick_size
a string?
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.
Oh, I see that it was previously defaulting to an empty string in Future
below. Still not sure I understand why though.
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.
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.
zipline/finance/execution.py
Outdated
|
||
def _get_stop_price(asset, price, is_buy): | ||
if hasattr(asset, 'tick_size'): |
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.
I think this if statement can be removed now that all assets have a tick_size
.
zipline/protocol.py
Outdated
@@ -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): |
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.
We can probably replace invocations of this method with asset.price_multiplier
.
zipline/finance/execution.py
Outdated
return asymmetric_round_price( | ||
price, | ||
not is_buy, | ||
tick_size=float(asset.tick_size) |
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.
For this PR, we just cast tick_size to a float. Are there other uses that we want it to be a string?
tests/test_execution_styles.py
Outdated
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 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.
tests/test_execution_styles.py
Outdated
return pd.DataFrame.from_dict({ | ||
1: { | ||
'multiplier': 100, | ||
'tick_size': '0.0001', |
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 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
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.
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, |
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.
Thank's for cleaning up the stringly-typed tick_size
field! Can this be float tick_size
now?
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.
Sure, I can change that
zipline/assets/_assets.pyx
Outdated
@@ -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 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.
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.
Sure, I'll change it to 0.01
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.
I see you updated Equity
to the 0.01 tick size. Why the default value of 0.001 for futures?
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.
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
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.
This LGTM once we figure out what the defaults should be for futures and equities.
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.
Solves rounding bug which caused rounding of all asset stop/limit prices to the nearest penny regardless of their tick size.