Passed
Push — master ( cbe225...905fd3 )
by Rafael S.
01:17
created

Operation.update_position_different_sign()   A

Complexity

Conditions 4

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
c 0
b 0
f 0
dl 0
loc 16
rs 9.2
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
        # if we are trading more than the amount in the portfolio
159
        # the result will be calculated based only on what was traded
160
        # (the rest creates a new position)
161
        if abs(self.quantity) > abs(accumulator.state['quantity']):
162
            result_quantity = accumulator.state['quantity'] * -1
163
        else:
164
            result_quantity = self.quantity
165
        results = \
166
            result_quantity * accumulator.state['price'] - \
167
            result_quantity * self.real_price
168
        if results:
169
            self.results['trades'] = results
170
        if not same_sign(accumulator.state['quantity'], new_quantity):
171
            accumulator.state['price'] = self.real_price
172
173
    def update_positions(self, accumulator):
174
        """Updates the state of the asset with the operation data."""
175
        new_quantity = accumulator.state['quantity'] + self.quantity
176
        # If the original quantity and the operation
177
        # have the same sign, udpate the cost
178
        if same_sign(accumulator.state['quantity'], self.quantity):
179
            accumulator.state['price'] = average_price(
180
                accumulator.state['quantity'],
181
                accumulator.state['price'],
182
                self.quantity,
183
                self.real_price
184
            )
185
        # If they have different signs and the original quantity
186
        # was not zero, update the results
187
        elif accumulator.state['quantity'] != 0:
188
            self.update_position_different_sign(
189
                accumulator, new_quantity)
190
        else:
191
            accumulator.state['price'] = self.real_price
192
193
        accumulator.state['quantity'] = new_quantity
194
        if not accumulator.state['quantity']:
195
            accumulator.state['price'] = 0
196
197
198
class Daytrade(Operation):
199
    """A daytrade operation.
200
201
    Daytrades are operations of purchase and sale of an asset on
202
    the same date.
203
204
    Attributes:
205
        asset: An asset instance, the asset that is being traded.
206
        quantity: The traded quantity of the asset.
207
        purchase: A Operation object representing the purchase of the
208
            asset.
209
        sale: A Operation object representing the sale of the asset.
210
        update_position: Set to False, as daytrades don't change the
211
            portfolio position; they just create results.
212
    """
213
214
    update_position = False
215
216
    def __init__(self, operation_a, operation_b):
217
        """Create the daytrade object. A daytrade have 2 operations.
218
219
        - a purchase operation
220
        - a sale operation.
221
        """
222
        super(Daytrade, self).__init__(
223
            date=operation_a.date, subject=operation_a.subject,
224
        )
225
        purchase, sale = find_purchase_and_sale(operation_a, operation_b)
226
        self.extract_daytrade(purchase, sale)
227
        self.operations = [
228
            Operation(
229
                date=purchase.date,
230
                subject=purchase.subject,
231
                quantity=self.quantity,
232
                price=purchase.price
233
            ),
234
            Operation(
235
                date=sale.date,
236
                subject=sale.subject,
237
                quantity=self.quantity*-1,
238
                price=sale.price
239
            )]
240
241
    @property
242
    def results(self):
243
        """Returns the profit or the loss generated by the day trade."""
244
        return {
245
            'daytrades': abs(self.operations[1].real_value) - \
246
                                        abs(self.operations[0].real_value)
247
        }
248
249
    def update_accumulator(self, accumulator):
250
        """Updates the accumulator state with the day trade result."""
251
        self.update_accumulator_results(accumulator)
252
253
    def extract_daytrade(self, purchase, sale):
254
        """Extracts the daytraded quantity from 2 operations."""
255
        self.quantity = min([purchase.quantity, abs(sale.quantity)])
256
        purchase.quantity -= self.quantity
257
        sale.quantity += self.quantity
258
259
    def append_to_positions(self, container):
260
        """Saves a Daytrade object in the container.
261
262
        If there is already a day trade with the same asset on the
263
        container, then the day trades are merged.
264
        """
265
        if 'positions' not in container.context:
266
            container.context['positions'] = {}
267
268
        if 'daytrades' not in container.context['positions']:
269
            container.context['positions']['daytrades'] = {}
270
271
        if self.subject.symbol in container.context['positions']['daytrades']:
272
            self.merge_underlying(container, 0)
273
            self.merge_underlying(container, 1)
274
            container.context['positions']['daytrades'][self.subject.symbol].quantity +=\
275
                self.quantity
276
        else:
277
            container.context['positions']['daytrades'][self.subject.symbol] = self
278
279
    def merge_underlying(self, container, operation_index):
280
        """Merges one day trade underlying operation."""
281
        merge_operations(
282
            container.context['positions']['daytrades'][self.subject.symbol]\
283
                .operations[operation_index],
284
            self.operations[operation_index]
285
        )
286