Files
catalyst/zipline/finance/execution.py
T
Scott Sanderson 5373b6797c 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.
2014-04-25 12:02:23 -04:00

160 lines
4.8 KiB
Python

#
# Copyright 2014 Quantopian, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
from sys import float_info
from six import with_metaclass
import zipline.utils.math_utils as zp_math
def asymmetric_round_price_to_penny(price, prefer_round_down,
diff=(0.0095 - .005)):
"""
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
# epsilon doesn't quite get there, so use a slightly less epsilon-ey value.
epsilon = float_info.epsilon * 10
diff = diff - epsilon
# relies on rounding half away from zero, unlike numpy's bankers' rounding
rounded = round(price - (diff if prefer_round_down else -diff), 2)
if zp_math.tolerant_equals(rounded, 0.0):
return 0.0
return rounded
class ExecutionStyle(with_metaclass(abc.ABCMeta)):
"""
Abstract base class representing a modification to a standard order.
"""
@abc.abstractmethod
def get_limit_price(self, is_buy):
"""
Get the limit price for this order.
Returns either None or a numerical value >= 0.
"""
raise NotImplemented
@abc.abstractmethod
def get_stop_price(self, is_buy):
"""
Get the stop price for this order.
Returns either None or a numerical value >= 0.
"""
raise NotImplemented
class MarketOrder(ExecutionStyle):
"""
Class encapsulating an order to be placed at the current market price.
"""
def __init__(self):
pass
def get_limit_price(self, _is_buy):
return None
def get_stop_price(self, _is_buy):
return None
class LimitOrder(ExecutionStyle):
"""
Execution style representing an order to be executed at a price equal to or
better than a specified limit price.
"""
def __init__(self, limit_price):
"""
Store the given price.
"""
if limit_price < 0:
raise ValueError("Can't place a limit with a negative price.")
self.limit_price = limit_price
def get_limit_price(self, is_buy):
return asymmetric_round_price_to_penny(self.limit_price, is_buy)
def get_stop_price(self, _is_buy):
return None
class StopOrder(ExecutionStyle):
"""
Execution style representing an order to be placed once the market price
reaches a specified stop price.
"""
def __init__(self, stop_price):
"""
Store the given price.
"""
if stop_price < 0:
raise ValueError(
"Can't place a stop order with a negative price."
)
self.stop_price = stop_price
def get_limit_price(self, _is_buy):
return None
def get_stop_price(self, is_buy):
return asymmetric_round_price_to_penny(self.stop_price, not is_buy)
class StopLimitOrder(ExecutionStyle):
"""
Execution style representing a limit order to be placed with a specified
limit price once the market reaches a specified stop price.
"""
def __init__(self, limit_price, stop_price):
"""
Store the given prices
"""
if limit_price < 0:
raise ValueError(
"Can't place a limit with a negative price."
)
if stop_price < 0:
raise ValueError(
"Can't place a stop order with a negative price."
)
self.limit_price = limit_price
self.stop_price = stop_price
def get_limit_price(self, is_buy):
return asymmetric_round_price_to_penny(self.limit_price, is_buy)
def get_stop_price(self, is_buy):
return asymmetric_round_price_to_penny(self.stop_price, not is_buy)