mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-19 11:22:06 +08:00
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.
339 lines
9.5 KiB
Python
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)
|