Completed
Pull Request — master (#858)
by Eddie
02:21
created

zipline.finance.Blotter   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 272
Duplicated Lines 0 %
Metric Value
dl 0
loc 272
rs 8.6
wmc 37

11 Methods

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