Files
catalyst/zipline/finance/slippage.py
T
Brian Fink 82d2ddfa90 BUG: Fix limit orders
Only fill limit order if impacted fill price is better than the limit price.
If a limit order is partially filled, only fill the remaining shares if the
impacted fill price is better than the limit price.
2015-04-16 17:09:14 -04:00

339 lines
9.5 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.
from __future__ import division
import abc
import math
from copy import copy
from functools import partial
from six import with_metaclass
from zipline.protocol import DATASOURCE_TYPE
from zipline.utils.serialization_utils import (
VERSION_LABEL
)
SELL = 1 << 0
BUY = 1 << 1
STOP = 1 << 2
LIMIT = 1 << 3
def check_order_triggers(order, event):
"""
Given an order and a trade event, return a tuple of
(stop_reached, limit_reached).
For market orders, will return (False, False).
For stop orders, limit_reached will always be False.
For limit orders, stop_reached will always be False.
For stop limit orders a Boolean is returned to flag
that the stop has been reached.
Orders that have been triggered already (price targets reached),
the order's current values are returned.
"""
if order.triggered:
return (order.stop_reached, order.limit_reached, False)
stop_reached = False
limit_reached = False
sl_stop_reached = False
order_type = 0
if order.amount > 0:
order_type |= BUY
else:
order_type |= SELL
if order.stop is not None:
order_type |= STOP
if order.limit is not None:
order_type |= LIMIT
if order_type == BUY | STOP | LIMIT:
if event.price >= order.stop:
sl_stop_reached = True
if event.price <= order.limit:
limit_reached = True
elif order_type == SELL | STOP | LIMIT:
if event.price <= order.stop:
sl_stop_reached = True
if event.price >= order.limit:
limit_reached = True
elif order_type == BUY | STOP:
if event.price >= order.stop:
stop_reached = True
elif order_type == SELL | STOP:
if event.price <= order.stop:
stop_reached = True
elif order_type == BUY | LIMIT:
if event.price <= order.limit:
limit_reached = True
elif order_type == SELL | LIMIT:
# This is a SELL LIMIT order
if event.price >= order.limit:
limit_reached = True
return (stop_reached, limit_reached, sl_stop_reached)
def transact_stub(slippage, commission, event, open_orders):
"""
This is intended to be wrapped in a partial, so that the
slippage and commission models can be enclosed.
"""
for order, transaction in slippage(event, open_orders):
if transaction and transaction.amount != 0:
direction = math.copysign(1, transaction.amount)
per_share, total_commission = commission.calculate(transaction)
transaction.price += per_share * direction
transaction.commission = total_commission
yield order, transaction
def transact_partial(slippage, commission):
return partial(transact_stub, slippage, commission)
class Transaction(object):
def __init__(self, sid, amount, dt, price, order_id, commission=None):
self.sid = sid
self.amount = amount
self.dt = dt
self.price = price
self.order_id = order_id
self.commission = commission
self.type = DATASOURCE_TYPE.TRANSACTION
def __getitem__(self, name):
return self.__dict__[name]
def to_dict(self):
py = copy(self.__dict__)
del py['type']
return py
def __getstate__(self):
state_dict = copy(self.__dict__)
STATE_VERSION = 1
state_dict[VERSION_LABEL] = STATE_VERSION
return state_dict
def __setstate__(self, state):
OLDEST_SUPPORTED_STATE = 1
version = state.pop(VERSION_LABEL)
if version < OLDEST_SUPPORTED_STATE:
raise BaseException("Transaction saved state is too old.")
self.__dict__.update(state)
def create_transaction(event, order, price, amount):
# floor the amount to protect against non-whole number orders
# TODO: Investigate whether we can add a robust check in blotter
# and/or tradesimulation, as well.
amount_magnitude = int(abs(amount))
if amount_magnitude < 1:
raise Exception("Transaction magnitude must be at least 1.")
transaction = Transaction(
sid=event.sid,
amount=int(amount),
dt=event.dt,
price=price,
order_id=order.id
)
return transaction
class LiquidityExceeded(Exception):
pass
class SlippageModel(with_metaclass(abc.ABCMeta)):
@property
def volume_for_bar(self):
return self._volume_for_bar
@abc.abstractproperty
def process_order(self, event, order):
pass
def simulate(self, event, current_orders):
self._volume_for_bar = 0
for order in current_orders:
if order.open_amount == 0:
continue
order.check_triggers(event)
if not order.triggered:
continue
try:
txn = self.process_order(event, order)
except LiquidityExceeded:
break
if txn:
self._volume_for_bar += abs(txn.amount)
yield order, txn
def __call__(self, event, current_orders, **kwargs):
return self.simulate(event, current_orders, **kwargs)
class VolumeShareSlippage(SlippageModel):
def __init__(self,
volume_limit=.25,
price_impact=0.1):
self.volume_limit = volume_limit
self.price_impact = price_impact
def __repr__(self):
return """
{class_name}(
volume_limit={volume_limit},
price_impact={price_impact})
""".strip().format(class_name=self.__class__.__name__,
volume_limit=self.volume_limit,
price_impact=self.price_impact)
def process_order(self, event, order):
max_volume = self.volume_limit * event.volume
# price impact accounts for the total volume of transactions
# created against the current minute bar
remaining_volume = max_volume - self.volume_for_bar
if remaining_volume < 1:
# we can't fill any more transactions
raise LiquidityExceeded()
# the current order amount will be the min of the
# volume available in the bar or the open amount.
cur_volume = int(min(remaining_volume, abs(order.open_amount)))
if cur_volume < 1:
return
# tally the current amount into our total amount ordered.
# total amount will be used to calculate price impact
total_volume = self.volume_for_bar + cur_volume
volume_share = min(total_volume / event.volume,
self.volume_limit)
simulated_impact = volume_share ** 2 \
* math.copysign(self.price_impact, order.direction) \
* event.price
impacted_price = event.price + simulated_impact
if order.limit:
# this is tricky! if an order with a limit price has reached
# the limit price, we will try to fill the order. do not fill
# these shares if the impacted price is worse than the limit
# price. return early to avoid creating the transaction.
# buy order is worse if the impacted price is greater than
# the limit price. sell order is worse if the impacted price
# is less than the limit price
if (order.direction > 0 and impacted_price > order.limit) or \
(order.direction < 0 and impacted_price < order.limit):
return
return create_transaction(
event,
order,
impacted_price,
math.copysign(cur_volume, order.direction)
)
def __getstate__(self):
state_dict = copy(self.__dict__)
STATE_VERSION = 1
state_dict[VERSION_LABEL] = STATE_VERSION
return state_dict
def __setstate__(self, state):
OLDEST_SUPPORTED_STATE = 1
version = state.pop(VERSION_LABEL)
if version < OLDEST_SUPPORTED_STATE:
raise BaseException("VolumeShareSlippage saved state is too old.")
self.__dict__.update(state)
class FixedSlippage(SlippageModel):
def __init__(self, spread=0.0):
"""
Use the fixed slippage model, which will just add/subtract
a specified spread spread/2 will be added on buys and subtracted
on sells per share
"""
self.spread = spread
def process_order(self, event, order):
return create_transaction(
event,
order,
event.price + (self.spread / 2.0 * order.direction),
order.amount,
)
def __getstate__(self):
state_dict = copy(self.__dict__)
STATE_VERSION = 1
state_dict[VERSION_LABEL] = STATE_VERSION
return state_dict
def __setstate__(self, state):
OLDEST_SUPPORTED_STATE = 1
version = state.pop(VERSION_LABEL)
if version < OLDEST_SUPPORTED_STATE:
raise BaseException("FixedSlippage saved state is too old.")
self.__dict__.update(state)