mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-06 05:14:38 +08:00
3811df78b9
To fix the grouping of events so that (dt, events) ordering is preserved, the tracking of order states needs to change in the following way. Change how order keeps track of dates: - Change order's dt field to reflect modified date. - Add a created field. Change how performance keeps track of orders by: - Map dt to transactions - Map dt to orders - Map order ids to keep track of updated orders.
229 lines
7.0 KiB
Python
229 lines
7.0 KiB
Python
#
|
|
# Copyright 2013 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 pytz
|
|
import math
|
|
|
|
from copy import copy
|
|
from functools import partial
|
|
from zipline.protocol import DATASOURCE_TYPE
|
|
import zipline.utils.math_utils as zp_math
|
|
|
|
from logbook import Processor
|
|
|
|
|
|
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.
|
|
|
|
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)
|
|
|
|
stop_reached = False
|
|
limit_reached = False
|
|
# if the stop price is reached, simply set stop_reached
|
|
if order.stop is not None:
|
|
if (order.direction * (event.price - order.stop) <= 0):
|
|
# convert stop -> limit or market
|
|
stop_reached = True
|
|
|
|
# if the limit price is reached, we execute this order at
|
|
# (event.price + simulated_impact)
|
|
# we skip this order with a continue when the limit is not reached
|
|
if order.limit is not None:
|
|
# if limit conditions not met, then continue
|
|
if (order.direction * (event.price - order.limit) <= 0):
|
|
limit_reached = True
|
|
|
|
return (stop_reached, limit_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.
|
|
"""
|
|
def inject_algo_dt(record):
|
|
if not 'algo_dt' in record.extra:
|
|
record.extra['algo_dt'] = event['dt']
|
|
|
|
with Processor(inject_algo_dt).threadbound():
|
|
|
|
transactions = slippage.simulate(event, open_orders)
|
|
|
|
for transaction in transactions:
|
|
if (
|
|
transaction
|
|
and not
|
|
zp_math.tolerant_equals(transaction.amount, 0)
|
|
):
|
|
direction = math.copysign(1, transaction.amount)
|
|
per_share, total_commission = commission.calculate(transaction)
|
|
transaction.price = transaction.price + (per_share * direction)
|
|
transaction.commission = total_commission
|
|
return transactions
|
|
|
|
|
|
def transact_partial(slippage, commission):
|
|
return partial(transact_stub, slippage, commission)
|
|
|
|
|
|
class Transaction(object):
|
|
|
|
def __init__(self, sid, amount, dt, price, order_id=None, 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 create_transaction(sid, amount, price, dt, order_id):
|
|
|
|
txn = {
|
|
'sid': sid,
|
|
'amount': int(amount),
|
|
'dt': dt,
|
|
'price': price,
|
|
'order_id': order_id
|
|
}
|
|
|
|
transaction = Transaction(**txn)
|
|
return transaction
|
|
|
|
|
|
class VolumeShareSlippage(object):
|
|
|
|
def __init__(self,
|
|
volume_limit=.25,
|
|
price_impact=0.1):
|
|
|
|
self.volume_limit = volume_limit
|
|
self.price_impact = price_impact
|
|
|
|
def simulate(self, event, current_orders):
|
|
|
|
dt = event.dt
|
|
simulated_impact = 0.0
|
|
max_volume = self.volume_limit * event.volume
|
|
total_volume = 0
|
|
|
|
txns = []
|
|
for order in current_orders:
|
|
|
|
open_amount = order.amount - order.filled
|
|
|
|
if zp_math.tolerant_equals(open_amount, 0):
|
|
continue
|
|
|
|
order.check_triggers(event)
|
|
if not order.triggered:
|
|
continue
|
|
|
|
# price impact accounts for the total volume of transactions
|
|
# created against the current minute bar
|
|
remaining_volume = max_volume - total_volume
|
|
if (
|
|
remaining_volume <= 0
|
|
or
|
|
zp_math.tolerant_equals(remaining_volume, 0)
|
|
):
|
|
# we can't fill any more transactions
|
|
return txns
|
|
|
|
# the current order amount will be the min of the
|
|
# volume available in the bar or the open amount.
|
|
cur_amount = min(remaining_volume, abs(open_amount))
|
|
cur_amount = cur_amount * order.direction
|
|
# tally the current amount into our total amount ordered.
|
|
# total amount will be used to calculate price impact
|
|
total_volume = total_volume + order.direction * cur_amount
|
|
|
|
volume_share = min(order.direction * (total_volume) / event.volume,
|
|
self.volume_limit)
|
|
|
|
simulated_impact = (volume_share) ** 2 \
|
|
* self.price_impact * order.direction * event.price
|
|
|
|
if order.direction * cur_amount > 0:
|
|
txn = create_transaction(
|
|
event.sid,
|
|
cur_amount,
|
|
# In the future, we may want to change the next line
|
|
# for limit pricing
|
|
event.price + simulated_impact,
|
|
dt.replace(tzinfo=pytz.utc),
|
|
order.id
|
|
)
|
|
|
|
txns.append(txn)
|
|
|
|
return txns
|
|
|
|
|
|
class FixedSlippage(object):
|
|
|
|
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 simulate(self, event, orders):
|
|
|
|
txns = []
|
|
for order in orders:
|
|
# TODO: what if we have 2 orders, one for 100 shares long,
|
|
# and one for 100 shares short
|
|
# such as in a hedging scenario?
|
|
|
|
order.check_triggers(event)
|
|
if not order.triggered:
|
|
continue
|
|
|
|
if zp_math.tolerant_equals(order.amount, 0):
|
|
return txns
|
|
|
|
txn = create_transaction(
|
|
event.sid,
|
|
order.amount,
|
|
event.price + (self.spread / 2.0 * order.direction),
|
|
event.dt.replace(tzinfo=pytz.utc),
|
|
order.id
|
|
)
|
|
|
|
# mark the date of the order to match the transaction
|
|
order.dt = event.dt
|
|
txns.append(txn)
|
|
return txns
|