Passed
Push — master ( c413e2...cbe225 )
by Rafael S.
01:18
created

Operation.update_position_different_sign()   B

Complexity

Conditions 4

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
c 0
b 0
f 0
dl 0
loc 25
rs 8.5806
1
"""Occurrences.
2
3
trade: Financial Application Framework
4
http://trade.readthedocs.org/
5
https://github.com/rochars/trade
6
License: MIT
7
8
Copyright (c) 2015-2017 Rafael da Silva Rocha
9
10
Permission is hereby granted, free of charge, to any person obtaining a copy
11
of this software and associated documentation files (the "Software"), to deal
12
in the Software without restriction, including without limitation the rights
13
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
copies of the Software, and to permit persons to whom the Software is
15
furnished to do so, subject to the following conditions:
16
17
The above copyright notice and this permission notice shall be included in
18
all copies or substantial portions of the Software.
19
20
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
THE SOFTWARE.
27
"""
28
29
from __future__ import absolute_import
30
from __future__ import division
31
32
import math
33
from . accumulator import Occurrence, Subject
34
35
from . utils import (
36
    average_price,
37
    same_sign,
38
    merge_operations,
39
    find_purchase_and_sale
40
)
41
42
43
class Asset(Subject):
44
    """An asset represents anything that can be traded."""
45
46
    default_state = {
47
        'quantity': 0,
48
        'price': 0,
49
        'results': {}
50
    }
51
52
    def __init__(self, symbol=None, name=None, expiration_date=None, **kwargs):
53
        super(Asset, self).__init__(symbol, name, expiration_date)
54
        self.underlying_assets = kwargs.get('underlying_assets', {})
55
56
57
class Operation(Occurrence):
58
    """An Operation represents an occurrence with an Asset.
59
60
    Class Attributes:
61
        update_position: A boolean indication if the operation should
62
            update the position of the accumulator or not.
63
        update_results: A boolean indication if the operation should
64
            update the results of the accumulator or not.
65
        update_container: A boolean indication if the operation should
66
            update the context in a OperationContainer or not.
67
68
    Attributes:
69
        date: A string 'YYYY-mm-dd', the date the operation occurred.
70
        subject: An Asset instance, the asset that is being traded.
71
        quantity: A number representing the quantity being traded.
72
            Positive quantities represent a purchase.
73
            Negative quantities represent a sale.
74
        price: The raw unitary price of the asset being traded.
75
        commissions: A dict of discounts. String keys and float values
76
            representing the name of the discounts and the values
77
            to be deducted added to the the operation value.
78
        operations: A list of underlying occurrences that the
79
            might may have.
80
81
    """
82
83
    # By default every operation
84
    # updates the accumulator position
85
    update_position = True
86
87
    # By default every operation
88
    # updates the accumulator results
89
    update_results = True
90
91
    # By default every operation updates
92
    # the OperationContainer positions
93
    update_container = True
94
95
    def __init__(self, subject=None, date=None, **kwargs):
96
        super(Operation, self).__init__(subject, date)
97
        self.quantity = kwargs.get('quantity', 0)
98
        self.price = kwargs.get('price', 0)
99
        self.commissions = kwargs.get('commissions', {})
100
        self.raw_results = kwargs.get('raw_results', {})
101
        self.operations = kwargs.get('operations', [])
102
103
    @property
104
    def results(self):
105
        """Returns the results associated with the operation."""
106
        return self.raw_results
107
108
    @property
109
    def real_value(self):
110
        """Returns the quantity * the real price of the operation."""
111
        return self.quantity * self.real_price
112
113
    @property
114
    def real_price(self):
115
        """Returns the real price of the operation.
116
117
        The real price is the price with all commission and costs
118
        already deducted or added.
119
        """
120
        return self.price + math.copysign(
121
            self.total_commissions / self.quantity,
122
            self.quantity
123
        )
124
125
    @property
126
    def total_commissions(self):
127
        """Returns the sum of all commissions of this operation."""
128
        return sum(self.commissions.values())
129
130
    @property
131
    def volume(self):
132
        """Returns the quantity of the operation * its raw price."""
133
        return abs(self.quantity) * self.price
134
135
    def update_accumulator(self, accumulator):
136
        """Updates the accumulator with the operation data."""
137
        if self.need_position_update(accumulator):
138
            self.update_positions(accumulator)
139
        if self.update_results:
140
            self.update_accumulator_results(accumulator)
141
142
    def update_accumulator_results(self, accumulator):
143
        """Updates the results stored in the accumulator."""
144
        for key, value in self.results.items():
145
            if key not in accumulator.state['results']:
146
                accumulator.state['results'][key] = 0
147
            accumulator.state['results'][key] += value
148
149
    def need_position_update(self, accumulator):
150
        """Check if there is a need to update the position."""
151
        return (
152
            self.subject.symbol == accumulator.subject.symbol and
153
            self.quantity
154
        )
155
156
    def update_position_different_sign(self, accumulator, new_quantity):
157
        """Update when the operation and position have opposing signs."""
158
159
        # check if we are trading more than what
160
        # we have on our portfolio; if yes,
161
        # the result will be calculated based
162
        # only on what was traded (the rest create
163
        # a new position)
164
        if abs(self.quantity) > abs(accumulator.state['quantity']):
165
            result_quantity = accumulator.state['quantity'] * -1
166
167
        # If we're not trading more than what we have,
168
        # then use the operation quantity to calculate
169
        # the result
170
        else:
171
            result_quantity = self.quantity
172
173
        # calculates the results and costs
174
        results = \
175
            result_quantity * accumulator.state['price'] - \
176
            result_quantity * self.real_price
177
        if results:
178
            self.results['trades'] = results
179
        if not same_sign(accumulator.state['quantity'], new_quantity):
180
            accumulator.state['price'] = self.real_price
181
182
    def update_positions(self, accumulator):
183
        """Updates the state of the asset with the operation data."""
184
185
        new_quantity = accumulator.state['quantity'] + self.quantity
186
187
        # If the original quantity and the operation
188
        # have the same sign, udpate the cost
189
        if same_sign(accumulator.state['quantity'], self.quantity):
190
191
            # Update the cost
192
            accumulator.state['price'] = average_price(
193
                accumulator.state['quantity'],
194
                accumulator.state['price'],
195
                self.quantity,
196
                self.real_price
197
            )
198
199
        # If they have different signs, and the
200
        # original quantity was not zero, update the results
201
        elif accumulator.state['quantity'] != 0:
202
            self.update_position_different_sign(
203
                accumulator, new_quantity)
204
205
        # if none of these conditions are met, the new cost
206
        # is the operation price
207
        else:
208
            accumulator.state['price'] = self.real_price
209
210
        accumulator.state['quantity'] = new_quantity
211
        if not accumulator.state['quantity']:
212
            accumulator.state['price'] = 0
213
214
215
class Daytrade(Operation):
216
    """A daytrade operation.
217
218
    Daytrades are operations of purchase and sale of an asset on
219
    the same date.
220
221
    Attributes:
222
        asset: An asset instance, the asset that is being traded.
223
        quantity: The traded quantity of the asset.
224
        purchase: A Operation object representing the purchase of the
225
            asset.
226
        sale: A Operation object representing the sale of the asset.
227
        update_position: Set to False, as daytrades don't change the
228
            portfolio position; they just create results.
229
    """
230
231
    update_position = False
232
233
    def __init__(self, operation_a, operation_b):
234
        """Creates the daytrade object.
235
236
        Based on the informed values this method creates 2 operations:
237
        - a purchase operation
238
        - a sale operation
239
240
        Both operations can be treated like any other operation when it
241
        comes to taxes and the prorate of commissions.
242
        """
243
        super(Daytrade, self).__init__(
244
            date=operation_a.date,
245
            subject=operation_a.subject,
246
        )
247
        purchase, sale = find_purchase_and_sale(operation_a, operation_b)
248
        self.extract_daytrade(purchase, sale)
249
250
        # Purchase is 0, Sale is 1
251
        self.operations = [
252
            Operation(
253
                date=purchase.date,
254
                subject=purchase.subject,
255
                quantity=self.quantity,
256
                price=purchase.price
257
            ),
258
            Operation(
259
                date=sale.date,
260
                subject=sale.subject,
261
                quantity=self.quantity*-1,
262
                price=sale.price
263
            )
264
        ]
265
266
    @property
267
    def results(self):
268
        """Returns the profit or the loss generated by the day trade."""
269
        return {
270
            'daytrades': abs(self.operations[1].real_value) - \
271
                                        abs(self.operations[0].real_value)
272
        }
273
274
    def update_accumulator(self, accumulator):
275
        """Updates the accumulator state with the day trade result."""
276
        self.update_accumulator_results(accumulator)
277
278
    def extract_daytrade(self, purchase, sale):
279
        """Extracts the daytraded quantity from 2 operations."""
280
        self.quantity = min([purchase.quantity, abs(sale.quantity)])
281
        purchase.quantity -= self.quantity
282
        sale.quantity += self.quantity
283
284
    def append_to_positions(self, container):
285
        """Saves a Daytrade object in the container.
286
287
        If there is already a day trade with the same asset on the
288
        container, then the day trades are merged.
289
        """
290
        if 'positions' not in container.context:
291
            container.context['positions'] = {}
292
293
        if 'daytrades' not in container.context['positions']:
294
            container.context['positions']['daytrades'] = {}
295
296
        if self.subject.symbol in container.context['positions']['daytrades']:
297
            self.merge_underlying(container, 0)
298
            self.merge_underlying(container, 1)
299
            container.context['positions']['daytrades'][self.subject.symbol].quantity +=\
300
                self.quantity
301
        else:
302
            container.context['positions']['daytrades'][self.subject.symbol] = self
303
304
    def merge_underlying(self, container, operation_index):
305
        """Merges one day trade underlying operation."""
306
        merge_operations(
307
            container.context['positions']['daytrades'][self.subject.symbol]\
308
                .operations[operation_index],
309
            self.operations[operation_index]
310
        )
311