Completed
Pull Request — master (#858)
by Eddie
10:07 queued 01:13
created

zipline.finance.Blotter.hold()   A

Complexity

Conditions 4

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 4
dl 0
loc 18
rs 9.2
1
#
2
# Copyright 2015 Quantopian, Inc.
3
#
4
# Licensed under the Apache License, Version 2.0 (the "License");
5
# you may not use this file except in compliance with the License.
6
# You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
import math
16
17
from logbook import Logger
18
from collections import defaultdict
19
20
import pandas as pd
21
from six import iteritems
22
23
from zipline.finance.order import Order
24
25
from zipline.finance.slippage import VolumeShareSlippage
26
from zipline.finance.commission import PerShare
27
28
from zipline.utils.serialization_utils import (
29
    VERSION_LABEL
30
)
31
32
log = Logger('Blotter')
33
34
35
class Blotter(object):
36
    def __init__(self, data_frequency,
37
                 slippage_func=None, commission=None):
38
        # these orders are aggregated by sid
39
        self.open_orders = defaultdict(list)
40
41
        # keep a dict of orders by their own id
42
        self.orders = {}
43
44
        # holding orders that have come in since the last event.
45
        self.new_orders = []
46
        self.current_dt = None
47
48
        self.max_shares = int(1e+11)
49
50
        self.slippage_func = slippage_func or VolumeShareSlippage()
51
        self.commission = commission or PerShare()
52
53
        self.data_frequency = data_frequency
54
55
    def __repr__(self):
56
        return """
57
{class_name}(
58
    slippage={slippage_func},
59
    commission={commission},
60
    open_orders={open_orders},
61
    orders={orders},
62
    new_orders={new_orders},
63
    current_dt={current_dt})
64
""".strip().format(class_name=self.__class__.__name__,
65
                   slippage_func=self.slippage_func,
66
                   commission=self.commission,
67
                   open_orders=self.open_orders,
68
                   orders=self.orders,
69
                   new_orders=self.new_orders,
70
                   current_dt=self.current_dt)
71
72
    def set_date(self, dt):
73
        self.current_dt = dt
74
75
    def order(self, sid, amount, style, order_id=None):
76
77
        # something could be done with amount to further divide
78
        # between buy by share count OR buy shares up to a dollar amount
79
        # numeric == share count  AND  "$dollar.cents" == cost amount
80
81
        """
82
        amount > 0 :: Buy/Cover
83
        amount < 0 :: Sell/Short
84
        Market order:    order(sid, amount)
85
        Limit order:     order(sid, amount, style=LimitOrder(limit_price))
86
        Stop order:      order(sid, amount, style=StopOrder(stop_price))
87
        StopLimit order: order(sid, amount, style=StopLimitOrder(limit_price,
88
                               stop_price))
89
        """
90
        if amount == 0:
91
            # Don't bother placing orders for 0 shares.
92
            return
93
        elif amount > self.max_shares:
94
            # Arbitrary limit of 100 billion (US) shares will never be
95
            # exceeded except by a buggy algorithm.
96
            raise OverflowError("Can't order more than %d shares" %
97
                                self.max_shares)
98
99
        is_buy = (amount > 0)
100
        order = Order(
101
            dt=self.current_dt,
102
            sid=sid,
103
            amount=amount,
104
            stop=style.get_stop_price(is_buy),
105
            limit=style.get_limit_price(is_buy),
106
            id=order_id
107
        )
108
109
        self.open_orders[order.sid].append(order)
110
        self.orders[order.id] = order
111
        self.new_orders.append(order)
112
113
        return order.id
114
115
    def cancel(self, order_id):
116
        if order_id not in self.orders:
117
            return
118
119
        cur_order = self.orders[order_id]
120
121
        if cur_order.open:
122
            order_list = self.open_orders[cur_order.sid]
123
            if cur_order in order_list:
124
                order_list.remove(cur_order)
125
126
            if cur_order in self.new_orders:
127
                self.new_orders.remove(cur_order)
128
            cur_order.cancel()
129
            cur_order.dt = self.current_dt
130
            # we want this order's new status to be relayed out
131
            # along with newly placed orders.
132
            self.new_orders.append(cur_order)
133
134
    def reject(self, order_id, reason=''):
135
        """
136
        Mark the given order as 'rejected', which is functionally similar to
137
        cancelled. The distinction is that rejections are involuntary (and
138
        usually include a message from a broker indicating why the order was
139
        rejected) while cancels are typically user-driven.
140
        """
141
        if order_id not in self.orders:
142
            return
143
144
        cur_order = self.orders[order_id]
145
146
        order_list = self.open_orders[cur_order.sid]
147
        if cur_order in order_list:
148
            order_list.remove(cur_order)
149
150
        if cur_order in self.new_orders:
151
            self.new_orders.remove(cur_order)
152
        cur_order.reject(reason=reason)
153
        cur_order.dt = self.current_dt
154
        # we want this order's new status to be relayed out
155
        # along with newly placed orders.
156
        self.new_orders.append(cur_order)
157
158
    def hold(self, order_id, reason=''):
159
        """
160
        Mark the order with order_id as 'held'. Held is functionally similar
161
        to 'open'. When a fill (full or partial) arrives, the status
162
        will automatically change back to open/filled as necessary.
163
        """
164
        if order_id not in self.orders:
165
            return
166
167
        cur_order = self.orders[order_id]
168
        if cur_order.open:
169
            if cur_order in self.new_orders:
170
                self.new_orders.remove(cur_order)
171
            cur_order.hold(reason=reason)
172
            cur_order.dt = self.current_dt
173
            # we want this order's new status to be relayed out
174
            # along with newly placed orders.
175
            self.new_orders.append(cur_order)
176
177
    def process_splits(self, splits):
178
        """
179
        Processes a list of splits by modifying any open orders as needed.
180
181
        Parameters
182
        ----------
183
        splits: list
184
            A list of splits.  Each split is a tuple of (sid, ratio).
185
186
        Returns
187
        -------
188
        None
189
        """
190
        for split in splits:
191
            sid = split[0]
192
            if sid not in self.open_orders:
193
                return
194
195
            orders_to_modify = self.open_orders[sid]
196
            for order in orders_to_modify:
197
                order.handle_split(split[1])
198
199
    def get_transactions(self, data_portal):
200
        """
201
        Creates a list of transactions based on the current open orders,
202
        slippage model, and commission model.
203
204
        Parameters
205
        ----------
206
        data_portal: zipline.data.DataPortal
207
            The data portal to use for getting price and volume information
208
            when calculating slippage.
209
210
        Notes
211
        -----
212
        This method book-keeps the blotter's open_orders dictionary, so that
213
         it is accurate by the time we're done processing open orders.
214
215
        Returns
216
        -------
217
        transactions_list: List
218
            transactions_list: list of transactions resulting from the current
219
            open orders.  If there were no open orders, an empty list is
220
            returned.
221
222
        commissions_list: List
223
            commissions_list: list of commissions resulting from filling the
224
            open orders.  A commission is an object with "sid" and "cost"
225
            parameters.  If there are no commission events (because, for
226
            example, Zipline models the commission cost into the fill price
227
            of the transaction), then this is None.
228
        """
229
        closed_orders = []
230
        transactions = []
231
232
        for asset, asset_orders in iteritems(self.open_orders):
233
            price = data_portal.get_spot_value(
234
                asset, 'close', self.current_dt, self.data_frequency)
235
236
            volume = data_portal.get_spot_value(
237
                asset, 'volume', self.current_dt, self.data_frequency)
238
239
            for order, txn in self.slippage_func(asset_orders, self.current_dt,
240
                                                 price, volume):
241
                direction = math.copysign(1, txn.amount)
242
                per_share, total_commission = self.commission.calculate(txn)
243
                txn.price += per_share * direction
244
                txn.commission = total_commission
245
                order.filled += txn.amount
246
247
                if txn.commission is not None:
248
                    order.commission = (order.commission or 0.0) + \
249
                        txn.commission
250
251
                txn.dt = pd.Timestamp(txn.dt, tz='UTC')
252
                order.dt = txn.dt
253
254
                transactions.append(txn)
255
256
                if not order.open:
257
                    closed_orders.append(order)
258
259
        # remove all closed orders from our open_orders dict
260
        for order in closed_orders:
261
            sid = order.sid
262
            try:
263
                sid_orders = self.open_orders[sid]
264
                sid_orders.remove(order)
265
            except KeyError:
266
                continue
267
268
        # now clear out the sids from our open_orders dict that have
269
        # zero open orders
270
        for sid in list(self.open_orders.keys()):
271
            if len(self.open_orders[sid]) == 0:
272
                del self.open_orders[sid]
273
274
        # FIXME this API doesn't feel right (returning two things here)
275
        return transactions, None
276
277
    def __getstate__(self):
278
279
        state_to_save = ['new_orders', 'orders', '_status', 'data_frequency']
280
281
        state_dict = {k: self.__dict__[k] for k in state_to_save
282
                      if k in self.__dict__}
283
284
        # Have to handle defaultdicts specially
285
        state_dict['open_orders'] = dict(self.open_orders)
286
287
        STATE_VERSION = 1
288
        state_dict[VERSION_LABEL] = STATE_VERSION
289
290
        return state_dict
291
292
    def __setstate__(self, state):
293
294
        self.__init__(state.pop('data_frequency'))
295
296
        OLDEST_SUPPORTED_STATE = 1
297
        version = state.pop(VERSION_LABEL)
298
299
        if version < OLDEST_SUPPORTED_STATE:
300
            raise BaseException("Blotter saved is state too old.")
301
302
        open_orders = defaultdict(list)
303
        open_orders.update(state.pop('open_orders'))
304
        self.open_orders = open_orders
305
306
        self.__dict__.update(state)
307