Completed
Pull Request — master (#858)
by Eddie
02:50 queued 01:15
created

zipline.finance.Blotter.get_transactions()   D

Complexity

Conditions 9

Size

Total Lines 72

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 9
dl 0
loc 72
rs 4.1527

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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, bar_data):
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
            trade_bar = bar_data[asset]
234
235
            for order, txn in self.slippage_func(trade_bar, asset_orders):
236
                direction = math.copysign(1, txn.amount)
237
                per_share, total_commission = self.commission.calculate(txn)
238
                txn.price += per_share * direction
239
                txn.commission = total_commission
240
                order.filled += txn.amount
241
242
                if txn.commission is not None:
243
                    order.commission = (order.commission or 0.0) + \
244
                        txn.commission
245
246
                txn.dt = pd.Timestamp(txn.dt, tz='UTC')
247
                order.dt = txn.dt
248
249
                transactions.append(txn)
250
251
                if not order.open:
252
                    closed_orders.append(order)
253
254
        # remove all closed orders from our open_orders dict
255
        for order in closed_orders:
256
            sid = order.sid
257
            try:
258
                sid_orders = self.open_orders[sid]
259
                sid_orders.remove(order)
260
            except KeyError:
261
                continue
262
263
        # now clear out the sids from our open_orders dict that have
264
        # zero open orders
265
        for sid in list(self.open_orders.keys()):
266
            if len(self.open_orders[sid]) == 0:
267
                del self.open_orders[sid]
268
269
        # FIXME this API doesn't feel right (returning two things here)
270
        return transactions, None
271
272
    def __getstate__(self):
273
274
        state_to_save = ['new_orders', 'orders', '_status', 'data_frequency']
275
276
        state_dict = {k: self.__dict__[k] for k in state_to_save
277
                      if k in self.__dict__}
278
279
        # Have to handle defaultdicts specially
280
        state_dict['open_orders'] = dict(self.open_orders)
281
282
        STATE_VERSION = 1
283
        state_dict[VERSION_LABEL] = STATE_VERSION
284
285
        return state_dict
286
287
    def __setstate__(self, state):
288
289
        self.__init__(state.pop('data_frequency'))
290
291
        OLDEST_SUPPORTED_STATE = 1
292
        version = state.pop(VERSION_LABEL)
293
294
        if version < OLDEST_SUPPORTED_STATE:
295
            raise BaseException("Blotter saved is state too old.")
296
297
        open_orders = defaultdict(list)
298
        open_orders.update(state.pop('open_orders'))
299
        self.open_orders = open_orders
300
301
        self.__dict__.update(state)
302