Completed
Pull Request — master (#858)
by Eddie
01:43
created

zipline.finance.Order.check_order_triggers()   F

Complexity

Conditions 19

Size

Total Lines 58

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 19
dl 0
loc 58
rs 3.2647

How to fix   Long Method    Complexity   

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:

Complexity

Complex classes like zipline.finance.Order.check_order_triggers() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
from copy import copy
2
import uuid
3
import math
4
from six import text_type, iteritems
5
6
import zipline.protocol as zp
7
from zipline.utils.protocol_utils import Enum
8
from zipline.utils.serialization_utils import VERSION_LABEL
9
10
ORDER_STATUS = Enum(
11
    'OPEN',
12
    'FILLED',
13
    'CANCELLED',
14
    'REJECTED',
15
    'HELD',
16
)
17
18
19
SELL = 1 << 0
20
BUY = 1 << 1
21
STOP = 1 << 2
22
LIMIT = 1 << 3
23
24
25
class Order(object):
26
    def __init__(self, dt, sid, amount, stop=None, limit=None, filled=0,
27
                 commission=None, id=None):
28
        """
29
        @dt - datetime.datetime that the order was placed
30
        @sid - stock sid of the order
31
        @amount - the number of shares to buy/sell
32
                  a positive sign indicates a buy
33
                  a negative sign indicates a sell
34
        @filled - how many shares of the order have been filled so far
35
        """
36
        # get a string representation of the uuid.
37
        self.id = id or self.make_id()
38
        self.dt = dt
39
        self.reason = None
40
        self.created = dt
41
        self.sid = sid
42
        self.amount = amount
43
        self.filled = filled
44
        self.commission = commission
45
        self._status = ORDER_STATUS.OPEN
46
        self.stop = stop
47
        self.limit = limit
48
        self.stop_reached = False
49
        self.limit_reached = False
50
        self.direction = math.copysign(1, self.amount)
51
        self.type = zp.DATASOURCE_TYPE.ORDER
52
53
    def make_id(self):
54
        return uuid.uuid4().hex
55
56
    def to_dict(self):
57
        py = copy(self.__dict__)
58
        for field in ['type', 'direction', '_status']:
59
            del py[field]
60
        py['status'] = self.status
61
        return py
62
63
    def to_api_obj(self):
64
        pydict = self.to_dict()
65
        obj = zp.Order(initial_values=pydict)
66
        return obj
67
68
    def check_triggers(self, price, dt):
69
        """
70
        Update internal state based on price triggers and the
71
        trade event's price.
72
        """
73
        stop_reached, limit_reached, sl_stop_reached = \
74
            self.check_order_triggers(price)
75
        if (stop_reached, limit_reached) \
76
                != (self.stop_reached, self.limit_reached):
77
            self.dt = dt
78
        self.stop_reached = stop_reached
79
        self.limit_reached = limit_reached
80
        if sl_stop_reached:
81
            # Change the STOP LIMIT order into a LIMIT order
82
            self.stop = None
83
84
    def check_order_triggers(self, current_price):
85
        """
86
        Given an order and a trade event, return a tuple of
87
        (stop_reached, limit_reached).
88
        For market orders, will return (False, False).
89
        For stop orders, limit_reached will always be False.
90
        For limit orders, stop_reached will always be False.
91
        For stop limit orders a Boolean is returned to flag
92
        that the stop has been reached.
93
94
        Orders that have been triggered already (price targets reached),
95
        the order's current values are returned.
96
        """
97
        if self.triggered:
98
            return (self.stop_reached, self.limit_reached, False)
99
100
        stop_reached = False
101
        limit_reached = False
102
        sl_stop_reached = False
103
104
        order_type = 0
105
106
        if self.amount > 0:
107
            order_type |= BUY
108
        else:
109
            order_type |= SELL
110
111
        if self.stop is not None:
112
            order_type |= STOP
113
114
        if self.limit is not None:
115
            order_type |= LIMIT
116
117
        if order_type == BUY | STOP | LIMIT:
118
            if current_price >= self.stop:
119
                sl_stop_reached = True
120
                if current_price <= self.limit:
121
                    limit_reached = True
122
        elif order_type == SELL | STOP | LIMIT:
123
            if current_price <= self.stop:
124
                sl_stop_reached = True
125
                if current_price >= self.limit:
126
                    limit_reached = True
127
        elif order_type == BUY | STOP:
128
            if current_price >= self.stop:
129
                stop_reached = True
130
        elif order_type == SELL | STOP:
131
            if current_price <= self.stop:
132
                stop_reached = True
133
        elif order_type == BUY | LIMIT:
134
            if current_price <= self.limit:
135
                limit_reached = True
136
        elif order_type == SELL | LIMIT:
137
            # This is a SELL LIMIT order
138
            if current_price >= self.limit:
139
                limit_reached = True
140
141
        return (stop_reached, limit_reached, sl_stop_reached)
142
143
    def handle_split(self, ratio):
144
        # update the amount, limit_price, and stop_price
145
        # by the split's ratio
146
147
        # info here: http://finra.complinet.com/en/display/display_plain.html?
148
        # rbid=2403&element_id=8950&record_id=12208&print=1
149
150
        # new_share_amount = old_share_amount / ratio
151
        # new_price = old_price * ratio
152
        self.amount = int(self.amount / ratio)
153
154
        if self.limit is not None:
155
            self.limit = round(self.limit * ratio, 2)
156
157
        if self.stop is not None:
158
            self.stop = round(self.stop * ratio, 2)
159
160
    @property
161
    def status(self):
162
        if not self.open_amount:
163
            return ORDER_STATUS.FILLED
164
        elif self._status == ORDER_STATUS.HELD and self.filled:
165
            return ORDER_STATUS.OPEN
166
        else:
167
            return self._status
168
169
    @status.setter
170
    def status(self, status):
171
        self._status = status
172
173
    def cancel(self):
174
        self.status = ORDER_STATUS.CANCELLED
175
176
    def reject(self, reason=''):
177
        self.status = ORDER_STATUS.REJECTED
178
        self.reason = reason
179
180
    def hold(self, reason=''):
181
        self.status = ORDER_STATUS.HELD
182
        self.reason = reason
183
184
    @property
185
    def open(self):
186
        return self.status in [ORDER_STATUS.OPEN, ORDER_STATUS.HELD]
187
188
    @property
189
    def triggered(self):
190
        """
191
        For a market order, True.
192
        For a stop order, True IFF stop_reached.
193
        For a limit order, True IFF limit_reached.
194
        """
195
        if self.stop is not None and not self.stop_reached:
196
            return False
197
198
        if self.limit is not None and not self.limit_reached:
199
            return False
200
201
        return True
202
203
    @property
204
    def open_amount(self):
205
        return self.amount - self.filled
206
207
    def __repr__(self):
208
        """
209
        String representation for this object.
210
        """
211
        return "Order(%s)" % self.to_dict().__repr__()
212
213
    def __unicode__(self):
214
        """
215
        Unicode representation for this object.
216
        """
217
        return text_type(repr(self))
218
219
    def __getstate__(self):
220
221
        state_dict = \
222
            {k: v for k, v in iteritems(self.__dict__)
223
                if not k.startswith('_')}
224
225
        state_dict['_status'] = self._status
226
227
        STATE_VERSION = 1
228
        state_dict[VERSION_LABEL] = STATE_VERSION
229
230
        return state_dict
231
232
    def __setstate__(self, state):
233
        OLDEST_SUPPORTED_STATE = 1
234
        version = state.pop(VERSION_LABEL)
235
236
        if version < OLDEST_SUPPORTED_STATE:
237
            raise BaseException("Order saved state is too old.")
238
239
        self.__dict__.update(state)
240