Completed
Push — master ( 7d146f...c413e2 )
by Rafael S.
01:21
created

Operation.update_positions()   B

Complexity

Conditions 3

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 3
dl 0
loc 30
rs 8.8571
c 5
b 0
f 0
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 fix_price_for_zero_quantity(self, accumulator):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
157
        if not accumulator.state['quantity']:
158
            accumulator.state['price'] = 0
159
160
    def update_position_different_sign(self, accumulator, new_quantity):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
161
        # check if we are trading more than what
162
            # we have on our portfolio; if yes,
163
            # the result will be calculated based
164
            # only on what was traded (the rest create
165
            # a new position)
166
            if abs(self.quantity) > abs(accumulator.state['quantity']):
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 8 spaces were expected, but 12 were found.
Loading history...
167
                result_quantity = accumulator.state['quantity'] * -1
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 12 spaces were expected, but 16 were found.
Loading history...
168
169
            # If we're not trading more than what we have,
170
            # then use the operation quantity to calculate
171
            # the result
172
            else:
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 8 spaces were expected, but 12 were found.
Loading history...
173
                result_quantity = self.quantity
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 12 spaces were expected, but 16 were found.
Loading history...
174
175
            # calculates the results and costs
176
            results = \
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 8 spaces were expected, but 12 were found.
Loading history...
177
                result_quantity * accumulator.state['price'] - \
178
                result_quantity * self.real_price
179
            if results:
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 8 spaces were expected, but 12 were found.
Loading history...
180
                self.results['trades'] = results
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 12 spaces were expected, but 16 were found.
Loading history...
181
            if not same_sign(accumulator.state['quantity'], new_quantity):
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 8 spaces were expected, but 12 were found.
Loading history...
182
                accumulator.state['price'] = self.real_price
0 ignored issues
show
Coding Style introduced by
The indentation here looks off. 12 spaces were expected, but 16 were found.
Loading history...
183
184
    def update_positions(self, accumulator):
185
        """Updates the state of the asset with the operation data."""
186
        
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
187
        new_quantity = accumulator.state['quantity'] + self.quantity
188
189
        # If the original quantity and the operation
190
        # have the same sign, udpate the cost
191
        if same_sign(accumulator.state['quantity'], self.quantity):
192
193
            # Update the cost
194
            accumulator.state['price'] = average_price(
195
                accumulator.state['quantity'],
196
                accumulator.state['price'],
197
                self.quantity,
198
                self.real_price
199
            )
200
201
        # If they have different signs, and the
202
        # original quantity was not zero, update the results
203
        elif accumulator.state['quantity'] != 0:
204
            self.update_position_different_sign(
205
                accumulator, new_quantity)
206
207
        # if none of these conditions are met, the new cost
208
        # is the operation price
209
        else:
210
            accumulator.state['price'] = self.real_price
211
212
        accumulator.state['quantity'] = new_quantity
213
        self.fix_price_for_zero_quantity(accumulator)
214
215
216
class Daytrade(Operation):
217
    """A daytrade operation.
218
219
    Daytrades are operations of purchase and sale of an asset on
220
    the same date.
221
222
    Attributes:
223
        asset: An asset instance, the asset that is being traded.
224
        quantity: The traded quantity of the asset.
225
        purchase: A Operation object representing the purchase of the
226
            asset.
227
        sale: A Operation object representing the sale of the asset.
228
        update_position: Set to False, as daytrades don't change the
229
            portfolio position; they just create results.
230
    """
231
232
    update_position = False
233
234
    def __init__(self, operation_a, operation_b):
235
        """Creates the daytrade object.
236
237
        Based on the informed values this method creates 2 operations:
238
        - a purchase operation
239
        - a sale operation
240
241
        Both operations can be treated like any other operation when it
242
        comes to taxes and the prorate of commissions.
243
        """
244
        super(Daytrade, self).__init__(
245
            date=operation_a.date,
246
            subject=operation_a.subject,
247
        )
248
        purchase, sale = find_purchase_and_sale(operation_a, operation_b)
249
        self.extract_daytrade(purchase, sale)
250
251
        # Purchase is 0, Sale is 1
252
        self.operations = [
253
            Operation(
254
                date=purchase.date,
255
                subject=purchase.subject,
256
                quantity=self.quantity,
257
                price=purchase.price
258
            ),
259
            Operation(
260
                date=sale.date,
261
                subject=sale.subject,
262
                quantity=self.quantity*-1,
263
                price=sale.price
264
            )
265
        ]
266
267
    @property
268
    def results(self):
269
        """Returns the profit or the loss generated by the day trade."""
270
        return {
271
            'daytrades': abs(self.operations[1].real_value) - \
272
                                        abs(self.operations[0].real_value)
273
        }
274
275
    def update_accumulator(self, accumulator):
276
        """Updates the accumulator state with the day trade result."""
277
        self.update_accumulator_results(accumulator)
278
279
    def extract_daytrade(self, purchase, sale):
280
        """Extracts the daytraded quantity from 2 operations."""
281
        self.quantity = min([purchase.quantity, abs(sale.quantity)])
282
        purchase.quantity -= self.quantity
283
        sale.quantity += self.quantity
284
285
    def append_to_positions(self, container):
286
        """Saves a Daytrade object in the container.
287
288
        If there is already a day trade with the same asset on the
289
        container, then the day trades are merged.
290
        """
291
        if 'positions' not in container.context:
292
            container.context['positions'] = {}
293
294
        if 'daytrades' not in container.context['positions']:
295
            container.context['positions']['daytrades'] = {}
296
297
        if self.subject.symbol in container.context['positions']['daytrades']:
298
            self.merge_underlying(container, 0)
299
            self.merge_underlying(container, 1)
300
            container.context['positions']['daytrades'][self.subject.symbol].quantity +=\
301
                self.quantity
302
        else:
303
            container.context['positions']['daytrades'][self.subject.symbol] = self
304
305
    def merge_underlying(self, container, operation_index):
306
        """Merges one day trade underlying operation."""
307
        merge_operations(
308
            container.context['positions']['daytrades'][self.subject.symbol]\
309
                .operations[operation_index],
310
            self.operations[operation_index]
311
        )
312