]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blame - tests/test_portfolio.py
Merge branch 'dev'
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / tests / test_portfolio.py
CommitLineData
c682bdf4
IB
1from .helper import *
2import portfolio
3import datetime
4
3080f31d 5@unittest.skipUnless("unit" in limits, "Unit skipped")
c682bdf4
IB
6class ComputationTest(WebMockTestCase):
7 def test_compute_value(self):
8 compute = mock.Mock()
9 portfolio.Computation.compute_value("foo", "buy", compute_value=compute)
10 compute.assert_called_with("foo", "ask")
11
12 compute.reset_mock()
13 portfolio.Computation.compute_value("foo", "sell", compute_value=compute)
14 compute.assert_called_with("foo", "bid")
15
16 compute.reset_mock()
17 portfolio.Computation.compute_value("foo", "ask", compute_value=compute)
18 compute.assert_called_with("foo", "ask")
19
20 compute.reset_mock()
21 portfolio.Computation.compute_value("foo", "bid", compute_value=compute)
22 compute.assert_called_with("foo", "bid")
23
24 compute.reset_mock()
25 portfolio.Computation.computations["test"] = compute
26 portfolio.Computation.compute_value("foo", "bid", compute_value="test")
27 compute.assert_called_with("foo", "bid")
28
3080f31d 29@unittest.skipUnless("unit" in limits, "Unit skipped")
c682bdf4
IB
30class TradeTest(WebMockTestCase):
31
32 def test_values_assertion(self):
33 value_from = portfolio.Amount("BTC", "1.0")
34 value_from.linked_to = portfolio.Amount("ETH", "10.0")
35 value_to = portfolio.Amount("BTC", "1.0")
36 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
37 self.assertEqual("BTC", trade.base_currency)
38 self.assertEqual("ETH", trade.currency)
39 self.assertEqual(self.m, trade.market)
40
41 with self.assertRaises(AssertionError):
42 portfolio.Trade(value_from, -value_to, "ETH", self.m)
43 with self.assertRaises(AssertionError):
44 portfolio.Trade(value_from, value_to, "ETC", self.m)
45 with self.assertRaises(AssertionError):
46 value_from.currency = "ETH"
47 portfolio.Trade(value_from, value_to, "ETH", self.m)
48 value_from.currency = "BTC"
49 with self.assertRaises(AssertionError):
50 value_from2 = portfolio.Amount("BTC", "1.0")
51 portfolio.Trade(value_from2, value_to, "ETH", self.m)
52
53 value_from = portfolio.Amount("BTC", 0)
54 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
55 self.assertEqual(0, trade.value_from.linked_to)
56
57 def test_action(self):
58 value_from = portfolio.Amount("BTC", "1.0")
59 value_from.linked_to = portfolio.Amount("ETH", "10.0")
60 value_to = portfolio.Amount("BTC", "1.0")
61 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
62
63 self.assertIsNone(trade.action)
64
65 value_from = portfolio.Amount("BTC", "1.0")
66 value_from.linked_to = portfolio.Amount("BTC", "1.0")
67 value_to = portfolio.Amount("BTC", "2.0")
68 trade = portfolio.Trade(value_from, value_to, "BTC", self.m)
69
70 self.assertIsNone(trade.action)
71
72 value_from = portfolio.Amount("BTC", "0.5")
73 value_from.linked_to = portfolio.Amount("ETH", "10.0")
74 value_to = portfolio.Amount("BTC", "1.0")
75 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
76
77 self.assertEqual("acquire", trade.action)
78
79 value_from = portfolio.Amount("BTC", "0")
80 value_from.linked_to = portfolio.Amount("ETH", "0")
81 value_to = portfolio.Amount("BTC", "-1.0")
82 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
83
84 self.assertEqual("acquire", trade.action)
85
86 def test_order_action(self):
87 value_from = portfolio.Amount("BTC", "0.5")
88 value_from.linked_to = portfolio.Amount("ETH", "10.0")
89 value_to = portfolio.Amount("BTC", "1.0")
90 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
91
92 trade.inverted = False
93 self.assertEqual("buy", trade.order_action())
94 trade.inverted = True
95 self.assertEqual("sell", trade.order_action())
96
97 value_from = portfolio.Amount("BTC", "0")
98 value_from.linked_to = portfolio.Amount("ETH", "0")
99 value_to = portfolio.Amount("BTC", "-1.0")
100 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
101
102 trade.inverted = False
103 self.assertEqual("sell", trade.order_action())
104 trade.inverted = True
105 self.assertEqual("buy", trade.order_action())
106
107 def test_trade_type(self):
108 value_from = portfolio.Amount("BTC", "0.5")
109 value_from.linked_to = portfolio.Amount("ETH", "10.0")
110 value_to = portfolio.Amount("BTC", "1.0")
111 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
112
113 self.assertEqual("long", trade.trade_type)
114
115 value_from = portfolio.Amount("BTC", "0")
116 value_from.linked_to = portfolio.Amount("ETH", "0")
117 value_to = portfolio.Amount("BTC", "-1.0")
118 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
119
120 self.assertEqual("short", trade.trade_type)
121
122 def test_is_fullfiled(self):
123 with self.subTest(inverted=False):
124 value_from = portfolio.Amount("BTC", "0.5")
125 value_from.linked_to = portfolio.Amount("ETH", "10.0")
126 value_to = portfolio.Amount("BTC", "1.0")
127 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
128
129 order1 = mock.Mock()
130 order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")
131
132 order2 = mock.Mock()
133 order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
134 trade.orders.append(order1)
135 trade.orders.append(order2)
136
137 self.assertFalse(trade.is_fullfiled)
138
139 order3 = mock.Mock()
140 order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
141 trade.orders.append(order3)
142
143 self.assertTrue(trade.is_fullfiled)
144
4ae84fb7
IB
145 order1.filled_amount.assert_called_with(in_base_currency=True, refetch=True)
146 order2.filled_amount.assert_called_with(in_base_currency=True, refetch=True)
147 order3.filled_amount.assert_called_with(in_base_currency=True, refetch=True)
c682bdf4
IB
148
149 with self.subTest(inverted=True):
150 value_from = portfolio.Amount("BTC", "0.5")
151 value_from.linked_to = portfolio.Amount("USDT", "1000.0")
152 value_to = portfolio.Amount("BTC", "1.0")
153 trade = portfolio.Trade(value_from, value_to, "USDT", self.m)
154 trade.inverted = True
155
156 order1 = mock.Mock()
157 order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")
158
159 order2 = mock.Mock()
160 order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
161 trade.orders.append(order1)
162 trade.orders.append(order2)
163
164 self.assertFalse(trade.is_fullfiled)
165
166 order3 = mock.Mock()
167 order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
168 trade.orders.append(order3)
169
170 self.assertTrue(trade.is_fullfiled)
171
4ae84fb7
IB
172 order1.filled_amount.assert_called_with(in_base_currency=False, refetch=True)
173 order2.filled_amount.assert_called_with(in_base_currency=False, refetch=True)
174 order3.filled_amount.assert_called_with(in_base_currency=False, refetch=True)
c682bdf4
IB
175
176
177 def test_filled_amount(self):
178 value_from = portfolio.Amount("BTC", "0.5")
179 value_from.linked_to = portfolio.Amount("ETH", "10.0")
180 value_to = portfolio.Amount("BTC", "1.0")
181 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
182
183 order1 = mock.Mock()
184 order1.filled_amount.return_value = portfolio.Amount("ETH", "0.3")
185
186 order2 = mock.Mock()
187 order2.filled_amount.return_value = portfolio.Amount("ETH", "0.01")
188 trade.orders.append(order1)
189 trade.orders.append(order2)
190
191 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount())
4ae84fb7
IB
192 order1.filled_amount.assert_called_with(in_base_currency=False, refetch=False)
193 order2.filled_amount.assert_called_with(in_base_currency=False, refetch=False)
c682bdf4
IB
194
195 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=False))
4ae84fb7
IB
196 order1.filled_amount.assert_called_with(in_base_currency=False, refetch=False)
197 order2.filled_amount.assert_called_with(in_base_currency=False, refetch=False)
c682bdf4
IB
198
199 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=True))
4ae84fb7
IB
200 order1.filled_amount.assert_called_with(in_base_currency=True, refetch=False)
201 order2.filled_amount.assert_called_with(in_base_currency=True, refetch=False)
c682bdf4
IB
202
203 @mock.patch.object(portfolio.Computation, "compute_value")
204 @mock.patch.object(portfolio.Trade, "filled_amount")
205 @mock.patch.object(portfolio, "Order")
206 def test_prepare_order(self, Order, filled_amount, compute_value):
207 Order.return_value = "Order"
208
209 with self.subTest(desc="Nothing to do"):
210 value_from = portfolio.Amount("BTC", "10")
211 value_from.rate = D("0.1")
212 value_from.linked_to = portfolio.Amount("FOO", "100")
213 value_to = portfolio.Amount("BTC", "10")
214 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
215
216 trade.prepare_order()
217
218 filled_amount.assert_not_called()
219 compute_value.assert_not_called()
220 self.assertEqual(0, len(trade.orders))
221 Order.assert_not_called()
222
18de421e
IB
223 self.m.get_ticker.return_value = None
224 with self.subTest(desc="Unknown ticker"):
225 filled_amount.return_value = portfolio.Amount("BTC", "3")
226 compute_value.return_value = D("0.125")
227
228 value_from = portfolio.Amount("BTC", "1")
229 value_from.rate = D("0.1")
230 value_from.linked_to = portfolio.Amount("FOO", "10")
231 value_to = portfolio.Amount("BTC", "10")
232 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
233
234 trade.prepare_order()
235
236 filled_amount.assert_not_called()
237 compute_value.assert_not_called()
238 self.assertEqual(0, len(trade.orders))
239 Order.assert_not_called()
240 self.m.report.log_error.assert_called_once_with('prepare_order',
241 message='Unknown ticker FOO/BTC')
242
c682bdf4
IB
243 self.m.get_ticker.return_value = { "inverted": False }
244 with self.subTest(desc="Already filled"):
245 filled_amount.return_value = portfolio.Amount("FOO", "100")
246 compute_value.return_value = D("0.125")
247
248 value_from = portfolio.Amount("BTC", "10")
249 value_from.rate = D("0.1")
250 value_from.linked_to = portfolio.Amount("FOO", "100")
251 value_to = portfolio.Amount("BTC", "0")
252 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
253
254 trade.prepare_order()
255
256 filled_amount.assert_called_with(in_base_currency=False)
257 compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
258 self.assertEqual(0, len(trade.orders))
259 self.m.report.log_error.assert_called_with("prepare_order", message=mock.ANY)
260 Order.assert_not_called()
261
262 with self.subTest(action="dispose", inverted=False):
263 filled_amount.return_value = portfolio.Amount("FOO", "60")
264 compute_value.return_value = D("0.125")
265
266 value_from = portfolio.Amount("BTC", "10")
267 value_from.rate = D("0.1")
268 value_from.linked_to = portfolio.Amount("FOO", "100")
269 value_to = portfolio.Amount("BTC", "1")
270 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
271
272 trade.prepare_order()
273
274 filled_amount.assert_called_with(in_base_currency=False)
275 compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
276 self.assertEqual(1, len(trade.orders))
277 Order.assert_called_with("sell", portfolio.Amount("FOO", 30),
278 D("0.125"), "BTC", "long", self.m,
279 trade, close_if_possible=False)
280
281 with self.subTest(action="dispose", inverted=False, close_if_possible=True):
282 filled_amount.return_value = portfolio.Amount("FOO", "60")
283 compute_value.return_value = D("0.125")
284
285 value_from = portfolio.Amount("BTC", "10")
286 value_from.rate = D("0.1")
287 value_from.linked_to = portfolio.Amount("FOO", "100")
288 value_to = portfolio.Amount("BTC", "1")
289 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
290
291 trade.prepare_order(close_if_possible=True)
292
293 filled_amount.assert_called_with(in_base_currency=False)
294 compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
295 self.assertEqual(1, len(trade.orders))
296 Order.assert_called_with("sell", portfolio.Amount("FOO", 30),
297 D("0.125"), "BTC", "long", self.m,
298 trade, close_if_possible=True)
299
300 with self.subTest(action="acquire", inverted=False):
301 filled_amount.return_value = portfolio.Amount("BTC", "3")
302 compute_value.return_value = D("0.125")
303
304 value_from = portfolio.Amount("BTC", "1")
305 value_from.rate = D("0.1")
306 value_from.linked_to = portfolio.Amount("FOO", "10")
307 value_to = portfolio.Amount("BTC", "10")
308 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
309
310 trade.prepare_order()
311
312 filled_amount.assert_called_with(in_base_currency=True)
313 compute_value.assert_called_with(self.m.get_ticker.return_value, "buy", compute_value="default")
314 self.assertEqual(1, len(trade.orders))
315
316 Order.assert_called_with("buy", portfolio.Amount("FOO", 48),
317 D("0.125"), "BTC", "long", self.m,
318 trade, close_if_possible=False)
319
320 with self.subTest(close_if_possible=True):
321 filled_amount.return_value = portfolio.Amount("FOO", "0")
322 compute_value.return_value = D("0.125")
323
324 value_from = portfolio.Amount("BTC", "10")
325 value_from.rate = D("0.1")
326 value_from.linked_to = portfolio.Amount("FOO", "100")
327 value_to = portfolio.Amount("BTC", "0")
328 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
329
330 trade.prepare_order()
331
332 filled_amount.assert_called_with(in_base_currency=False)
333 compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
334 self.assertEqual(1, len(trade.orders))
335 Order.assert_called_with("sell", portfolio.Amount("FOO", 100),
336 D("0.125"), "BTC", "long", self.m,
337 trade, close_if_possible=True)
338
339 self.m.get_ticker.return_value = { "inverted": True, "original": {} }
340 with self.subTest(action="dispose", inverted=True):
341 filled_amount.return_value = portfolio.Amount("FOO", "300")
342 compute_value.return_value = D("125")
343
344 value_from = portfolio.Amount("BTC", "10")
345 value_from.rate = D("0.01")
346 value_from.linked_to = portfolio.Amount("FOO", "1000")
347 value_to = portfolio.Amount("BTC", "1")
348 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
349
350 trade.prepare_order(compute_value="foo")
351
352 filled_amount.assert_called_with(in_base_currency=True)
353 compute_value.assert_called_with(self.m.get_ticker.return_value["original"], "buy", compute_value="foo")
354 self.assertEqual(1, len(trade.orders))
355 Order.assert_called_with("buy", portfolio.Amount("BTC", D("4.8")),
356 D("125"), "FOO", "long", self.m,
357 trade, close_if_possible=False)
358
359 with self.subTest(action="acquire", inverted=True):
360 filled_amount.return_value = portfolio.Amount("BTC", "4")
361 compute_value.return_value = D("125")
362
363 value_from = portfolio.Amount("BTC", "1")
364 value_from.rate = D("0.01")
365 value_from.linked_to = portfolio.Amount("FOO", "100")
366 value_to = portfolio.Amount("BTC", "10")
367 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
368
369 trade.prepare_order(compute_value="foo")
370
371 filled_amount.assert_called_with(in_base_currency=False)
372 compute_value.assert_called_with(self.m.get_ticker.return_value["original"], "sell", compute_value="foo")
373 self.assertEqual(1, len(trade.orders))
374 Order.assert_called_with("sell", portfolio.Amount("BTC", D("5")),
375 D("125"), "FOO", "long", self.m,
376 trade, close_if_possible=False)
377
378 def test_tick_actions_recreate(self):
379 value_from = portfolio.Amount("BTC", "0.5")
380 value_from.linked_to = portfolio.Amount("ETH", "10.0")
381 value_to = portfolio.Amount("BTC", "1.0")
382 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
383
384 self.assertEqual("average", trade.tick_actions_recreate(0))
385 self.assertEqual("foo", trade.tick_actions_recreate(0, default="foo"))
386 self.assertEqual("average", trade.tick_actions_recreate(1))
387 self.assertEqual(trade.tick_actions[2][1], trade.tick_actions_recreate(2))
388 self.assertEqual(trade.tick_actions[2][1], trade.tick_actions_recreate(3))
389 self.assertEqual(trade.tick_actions[5][1], trade.tick_actions_recreate(5))
390 self.assertEqual(trade.tick_actions[5][1], trade.tick_actions_recreate(6))
391 self.assertEqual("default", trade.tick_actions_recreate(7))
392 self.assertEqual("default", trade.tick_actions_recreate(8))
393
394 @mock.patch.object(portfolio.Trade, "prepare_order")
395 def test_update_order(self, prepare_order):
396 order_mock = mock.Mock()
397 new_order_mock = mock.Mock()
398
399 value_from = portfolio.Amount("BTC", "0.5")
400 value_from.linked_to = portfolio.Amount("ETH", "10.0")
401 value_to = portfolio.Amount("BTC", "1.0")
402 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
403 prepare_order.return_value = new_order_mock
404
405 for i in [0, 1, 3, 4, 6]:
406 with self.subTest(tick=i):
407 trade.update_order(order_mock, i)
408 order_mock.cancel.assert_not_called()
409 new_order_mock.run.assert_not_called()
410 self.m.report.log_order.assert_called_once_with(order_mock, i,
411 update="waiting", compute_value=None, new_order=None)
412
413 order_mock.reset_mock()
414 new_order_mock.reset_mock()
415 trade.orders = []
416 self.m.report.log_order.reset_mock()
417
418 trade.update_order(order_mock, 2)
419 order_mock.cancel.assert_called()
420 new_order_mock.run.assert_called()
421 prepare_order.assert_called()
422 self.m.report.log_order.assert_called()
423 self.assertEqual(2, self.m.report.log_order.call_count)
424 calls = [
425 mock.call(order_mock, 2, update="adjusting",
426 compute_value=mock.ANY,
427 new_order=new_order_mock),
428 mock.call(order_mock, 2, new_order=new_order_mock),
429 ]
430 self.m.report.log_order.assert_has_calls(calls)
431
432 order_mock.reset_mock()
433 new_order_mock.reset_mock()
434 trade.orders = []
435 self.m.report.log_order.reset_mock()
436
437 trade.update_order(order_mock, 5)
438 order_mock.cancel.assert_called()
439 new_order_mock.run.assert_called()
440 prepare_order.assert_called()
441 self.assertEqual(2, self.m.report.log_order.call_count)
442 self.m.report.log_order.assert_called()
443 calls = [
444 mock.call(order_mock, 5, update="adjusting",
445 compute_value=mock.ANY,
446 new_order=new_order_mock),
447 mock.call(order_mock, 5, new_order=new_order_mock),
448 ]
449 self.m.report.log_order.assert_has_calls(calls)
450
451 order_mock.reset_mock()
452 new_order_mock.reset_mock()
453 trade.orders = []
454 self.m.report.log_order.reset_mock()
455
456 trade.update_order(order_mock, 7)
457 order_mock.cancel.assert_called()
458 new_order_mock.run.assert_called()
459 prepare_order.assert_called_with(compute_value="default")
460 self.m.report.log_order.assert_called()
461 self.assertEqual(2, self.m.report.log_order.call_count)
462 calls = [
463 mock.call(order_mock, 7, update="market_fallback",
464 compute_value='default',
465 new_order=new_order_mock),
466 mock.call(order_mock, 7, new_order=new_order_mock),
467 ]
468 self.m.report.log_order.assert_has_calls(calls)
469
470 order_mock.reset_mock()
471 new_order_mock.reset_mock()
472 trade.orders = []
473 self.m.report.log_order.reset_mock()
474
475 for i in [10, 13, 16]:
476 with self.subTest(tick=i):
477 trade.update_order(order_mock, i)
478 order_mock.cancel.assert_called()
479 new_order_mock.run.assert_called()
480 prepare_order.assert_called_with(compute_value="default")
481 self.m.report.log_order.assert_called()
482 self.assertEqual(2, self.m.report.log_order.call_count)
483 calls = [
484 mock.call(order_mock, i, update="market_adjust",
485 compute_value='default',
486 new_order=new_order_mock),
487 mock.call(order_mock, i, new_order=new_order_mock),
488 ]
489 self.m.report.log_order.assert_has_calls(calls)
490
491 order_mock.reset_mock()
492 new_order_mock.reset_mock()
493 trade.orders = []
494 self.m.report.log_order.reset_mock()
495
496 for i in [8, 9, 11, 12]:
497 with self.subTest(tick=i):
498 trade.update_order(order_mock, i)
499 order_mock.cancel.assert_not_called()
500 new_order_mock.run.assert_not_called()
501 self.m.report.log_order.assert_called_once_with(order_mock, i, update="waiting",
502 compute_value=None, new_order=None)
503
504 order_mock.reset_mock()
505 new_order_mock.reset_mock()
506 trade.orders = []
507 self.m.report.log_order.reset_mock()
508
509
510 def test_print_with_order(self):
511 value_from = portfolio.Amount("BTC", "0.5")
512 value_from.linked_to = portfolio.Amount("ETH", "10.0")
513 value_to = portfolio.Amount("BTC", "1.0")
514 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
515
516 order_mock1 = mock.Mock()
517 order_mock1.__repr__ = mock.Mock()
518 order_mock1.__repr__.return_value = "Mock 1"
519 order_mock2 = mock.Mock()
520 order_mock2.__repr__ = mock.Mock()
521 order_mock2.__repr__.return_value = "Mock 2"
522 order_mock1.mouvements = []
523 mouvement_mock1 = mock.Mock()
524 mouvement_mock1.__repr__ = mock.Mock()
525 mouvement_mock1.__repr__.return_value = "Mouvement 1"
526 mouvement_mock2 = mock.Mock()
527 mouvement_mock2.__repr__ = mock.Mock()
528 mouvement_mock2.__repr__.return_value = "Mouvement 2"
529 order_mock2.mouvements = [
530 mouvement_mock1, mouvement_mock2
531 ]
532 trade.orders.append(order_mock1)
533 trade.orders.append(order_mock2)
534
535 with mock.patch.object(trade, "filled_amount") as filled:
536 filled.return_value = portfolio.Amount("BTC", "0.1")
537
538 trade.print_with_order()
539
540 self.m.report.print_log.assert_called()
541 calls = self.m.report.print_log.mock_calls
542 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(calls[0][1][0]))
543 self.assertEqual("\tMock 1", str(calls[1][1][0]))
544 self.assertEqual("\tMock 2", str(calls[2][1][0]))
545 self.assertEqual("\t\tMouvement 1", str(calls[3][1][0]))
546 self.assertEqual("\t\tMouvement 2", str(calls[4][1][0]))
547
548 self.m.report.print_log.reset_mock()
549
550 filled.return_value = portfolio.Amount("BTC", "0.5")
551 trade.print_with_order()
552 calls = self.m.report.print_log.mock_calls
553 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire ✔)", str(calls[0][1][0]))
554
555 self.m.report.print_log.reset_mock()
556
557 filled.return_value = portfolio.Amount("BTC", "0.1")
558 trade.closed = True
559 trade.print_with_order()
560 calls = self.m.report.print_log.mock_calls
561 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire ❌)", str(calls[0][1][0]))
562
563 def test_close(self):
564 value_from = portfolio.Amount("BTC", "0.5")
565 value_from.linked_to = portfolio.Amount("ETH", "10.0")
566 value_to = portfolio.Amount("BTC", "1.0")
567 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
568 order1 = mock.Mock()
569 trade.orders.append(order1)
570
571 trade.close()
572
573 self.assertEqual(True, trade.closed)
574 order1.cancel.assert_called_once_with()
575
576 def test_pending(self):
577 value_from = portfolio.Amount("BTC", "0.5")
578 value_from.linked_to = portfolio.Amount("ETH", "10.0")
579 value_to = portfolio.Amount("BTC", "1.0")
580 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
581
582 trade.closed = True
583 self.assertEqual(False, trade.pending)
584
585 trade.closed = False
586 self.assertEqual(True, trade.pending)
587
588 order1 = mock.Mock()
589 order1.filled_amount.return_value = portfolio.Amount("BTC", "0.5")
590 trade.orders.append(order1)
591 self.assertEqual(False, trade.pending)
592
593 def test__repr(self):
594 value_from = portfolio.Amount("BTC", "0.5")
595 value_from.linked_to = portfolio.Amount("ETH", "10.0")
596 value_to = portfolio.Amount("BTC", "1.0")
597 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
598
599 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(trade))
600
601 def test_as_json(self):
602 value_from = portfolio.Amount("BTC", "0.5")
603 value_from.linked_to = portfolio.Amount("ETH", "10.0")
604 value_to = portfolio.Amount("BTC", "1.0")
605 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
606
607 as_json = trade.as_json()
608 self.assertEqual("acquire", as_json["action"])
609 self.assertEqual(D("0.5"), as_json["from"])
610 self.assertEqual(D("1.0"), as_json["to"])
611 self.assertEqual("ETH", as_json["currency"])
612 self.assertEqual("BTC", as_json["base_currency"])
613
3080f31d 614@unittest.skipUnless("unit" in limits, "Unit skipped")
c682bdf4
IB
615class BalanceTest(WebMockTestCase):
616 def test_values(self):
617 balance = portfolio.Balance("BTC", {
618 "exchange_total": "0.65",
619 "exchange_free": "0.35",
620 "exchange_used": "0.30",
621 "margin_total": "-10",
622 "margin_borrowed": "10",
623 "margin_available": "0",
624 "margin_in_position": "0",
625 "margin_position_type": "short",
626 "margin_borrowed_base_currency": "USDT",
627 "margin_liquidation_price": "1.20",
628 "margin_pending_gain": "10",
629 "margin_lending_fees": "0.4",
630 "margin_borrowed_base_price": "0.15",
631 })
632 self.assertEqual(portfolio.D("0.65"), balance.exchange_total.value)
633 self.assertEqual(portfolio.D("0.35"), balance.exchange_free.value)
634 self.assertEqual(portfolio.D("0.30"), balance.exchange_used.value)
635 self.assertEqual("BTC", balance.exchange_total.currency)
636 self.assertEqual("BTC", balance.exchange_free.currency)
637 self.assertEqual("BTC", balance.exchange_total.currency)
638
639 self.assertEqual(portfolio.D("-10"), balance.margin_total.value)
640 self.assertEqual(portfolio.D("10"), balance.margin_borrowed.value)
641 self.assertEqual(portfolio.D("0"), balance.margin_available.value)
642 self.assertEqual("BTC", balance.margin_total.currency)
643 self.assertEqual("BTC", balance.margin_borrowed.currency)
644 self.assertEqual("BTC", balance.margin_available.currency)
645
646 self.assertEqual("BTC", balance.currency)
647
648 self.assertEqual(portfolio.D("0.4"), balance.margin_lending_fees.value)
649 self.assertEqual("USDT", balance.margin_lending_fees.currency)
650
651 def test__repr(self):
652 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX])",
653 repr(portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 })))
654 balance = portfolio.Balance("BTX", { "exchange_total": 3,
655 "exchange_used": 1, "exchange_free": 2 })
656 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance))
657
658 balance = portfolio.Balance("BTX", { "exchange_total": 1, "exchange_used": 1})
659 self.assertEqual("Balance(BTX Exch: [❌1.00000000 BTX])", repr(balance))
660
661 balance = portfolio.Balance("BTX", { "margin_total": 3,
662 "margin_in_position": 1, "margin_available": 2 })
663 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance))
664
665 balance = portfolio.Balance("BTX", { "margin_total": 2, "margin_available": 2 })
666 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX])", repr(balance))
667
668 balance = portfolio.Balance("BTX", { "margin_total": -3,
669 "margin_borrowed_base_price": D("0.1"),
670 "margin_borrowed_base_currency": "BTC",
671 "margin_lending_fees": D("0.002") })
672 self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance))
673
674 balance = portfolio.Balance("BTX", { "margin_total": 1,
675 "margin_in_position": 1, "exchange_free": 2, "exchange_total": 2})
676 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [❌1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance))
677
678 def test_as_json(self):
679 balance = portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 })
680 as_json = balance.as_json()
681 self.assertEqual(set(portfolio.Balance.base_keys), set(as_json.keys()))
682 self.assertEqual(D(0), as_json["total"])
683 self.assertEqual(D(2), as_json["exchange_total"])
684 self.assertEqual(D(2), as_json["exchange_free"])
685 self.assertEqual(D(0), as_json["exchange_used"])
686 self.assertEqual(D(0), as_json["margin_total"])
687 self.assertEqual(D(0), as_json["margin_available"])
688 self.assertEqual(D(0), as_json["margin_borrowed"])
689
3080f31d 690@unittest.skipUnless("unit" in limits, "Unit skipped")
c682bdf4
IB
691class OrderTest(WebMockTestCase):
692 def test_values(self):
693 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
694 D("0.1"), "BTC", "long", "market", "trade")
695 self.assertEqual("buy", order.action)
696 self.assertEqual(10, order.amount.value)
697 self.assertEqual("ETH", order.amount.currency)
698 self.assertEqual(D("0.1"), order.rate)
699 self.assertEqual("BTC", order.base_currency)
700 self.assertEqual("market", order.market)
701 self.assertEqual("long", order.trade_type)
702 self.assertEqual("pending", order.status)
703 self.assertEqual("trade", order.trade)
704 self.assertIsNone(order.id)
705 self.assertFalse(order.close_if_possible)
706
707 def test__repr(self):
708 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
709 D("0.1"), "BTC", "long", "market", "trade")
710 self.assertEqual("Order(buy long 10.00000000 ETH at 0.1 BTC [pending])", repr(order))
711
712 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
713 D("0.1"), "BTC", "long", "market", "trade",
714 close_if_possible=True)
715 self.assertEqual("Order(buy long 10.00000000 ETH at 0.1 BTC [pending] ✂)", repr(order))
716
717 def test_as_json(self):
718 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
719 D("0.1"), "BTC", "long", "market", "trade")
720 mouvement_mock1 = mock.Mock()
721 mouvement_mock1.as_json.return_value = 1
722 mouvement_mock2 = mock.Mock()
723 mouvement_mock2.as_json.return_value = 2
724
725 order.mouvements = [mouvement_mock1, mouvement_mock2]
726 as_json = order.as_json()
727 self.assertEqual("buy", as_json["action"])
728 self.assertEqual("long", as_json["trade_type"])
729 self.assertEqual(10, as_json["amount"])
730 self.assertEqual("ETH", as_json["currency"])
731 self.assertEqual("BTC", as_json["base_currency"])
732 self.assertEqual(D("0.1"), as_json["rate"])
733 self.assertEqual("pending", as_json["status"])
734 self.assertEqual(False, as_json["close_if_possible"])
735 self.assertIsNone(as_json["id"])
736 self.assertEqual([1, 2], as_json["mouvements"])
737
738 def test_account(self):
739 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
740 D("0.1"), "BTC", "long", "market", "trade")
741 self.assertEqual("exchange", order.account)
742
743 order = portfolio.Order("sell", portfolio.Amount("ETH", 10),
744 D("0.1"), "BTC", "short", "market", "trade")
745 self.assertEqual("margin", order.account)
746
747 def test_pending(self):
748 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
749 D("0.1"), "BTC", "long", "market", "trade")
750 self.assertTrue(order.pending)
751 order.status = "open"
752 self.assertFalse(order.pending)
753
754 def test_open(self):
755 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
756 D("0.1"), "BTC", "long", "market", "trade")
757 self.assertFalse(order.open)
758 order.status = "open"
759 self.assertTrue(order.open)
760
761 def test_finished(self):
762 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
763 D("0.1"), "BTC", "long", "market", "trade")
764 self.assertFalse(order.finished)
765 order.status = "closed"
766 self.assertTrue(order.finished)
767 order.status = "canceled"
768 self.assertTrue(order.finished)
769 order.status = "error"
770 self.assertTrue(order.finished)
771
772 @mock.patch.object(portfolio.Order, "fetch")
773 def test_cancel(self, fetch):
774 with self.subTest(debug=True):
775 self.m.debug = True
776 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
777 D("0.1"), "BTC", "long", self.m, "trade")
778 order.status = "open"
779
780 order.cancel()
781 self.m.ccxt.cancel_order.assert_not_called()
782 self.m.report.log_debug_action.assert_called_once()
783 self.m.report.log_debug_action.reset_mock()
784 self.assertEqual("canceled", order.status)
785
786 with self.subTest(desc="Nominal case"):
787 self.m.debug = False
788 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
789 D("0.1"), "BTC", "long", self.m, "trade")
790 order.status = "open"
791 order.id = 42
792
793 order.cancel()
794 self.m.ccxt.cancel_order.assert_called_with(42)
795 fetch.assert_called_once_with()
796 self.m.report.log_debug_action.assert_not_called()
797
798 with self.subTest(exception=True):
799 self.m.ccxt.cancel_order.side_effect = portfolio.OrderNotFound
800 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
801 D("0.1"), "BTC", "long", self.m, "trade")
802 order.status = "open"
803 order.id = 42
804 order.cancel()
805 self.m.ccxt.cancel_order.assert_called_with(42)
806 self.m.report.log_error.assert_called_once()
807
808 self.m.reset_mock()
809 with self.subTest(id=None):
810 self.m.ccxt.cancel_order.side_effect = portfolio.OrderNotFound
811 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
812 D("0.1"), "BTC", "long", self.m, "trade")
813 order.status = "open"
814 order.cancel()
815 self.m.ccxt.cancel_order.assert_not_called()
816
817 self.m.reset_mock()
818 with self.subTest(open=False):
819 self.m.ccxt.cancel_order.side_effect = portfolio.OrderNotFound
820 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
821 D("0.1"), "BTC", "long", self.m, "trade")
822 order.status = "closed"
823 order.cancel()
824 self.m.ccxt.cancel_order.assert_not_called()
825
1902674c 826 def test_mark_dust_amount_remaining(self):
c682bdf4
IB
827 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
828 D("0.1"), "BTC", "long", self.m, "trade")
1902674c
IB
829 self.m.ccxt.is_dust_trade.return_value = False
830 order.mark_dust_amount_remaining_order()
831 self.assertEqual("pending", order.status)
c682bdf4 832
1902674c
IB
833 self.m.ccxt.is_dust_trade.return_value = True
834 order.mark_dust_amount_remaining_order()
e24df7cf
IB
835 self.assertEqual("pending", order.status)
836
837 order.status = "open"
838 order.mark_dust_amount_remaining_order()
1902674c 839 self.assertEqual("closed_dust_remaining", order.status)
c682bdf4
IB
840
841 @mock.patch.object(portfolio.Order, "fetch")
842 @mock.patch.object(portfolio.Order, "filled_amount", return_value=portfolio.Amount("ETH", 1))
843 def test_remaining_amount(self, filled_amount, fetch):
844 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
845 D("0.1"), "BTC", "long", self.m, "trade")
846
847 self.assertEqual(9, order.remaining_amount().value)
848
849 order.status = "open"
850 self.assertEqual(9, order.remaining_amount().value)
851
852 @mock.patch.object(portfolio.Order, "fetch")
853 def test_filled_amount(self, fetch):
854 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
855 D("0.1"), "BTC", "long", self.m, "trade")
856 order.mouvements.append(portfolio.Mouvement("ETH", "BTC", {
857 "tradeID": 42, "type": "buy", "fee": "0.0015",
858 "date": "2017-12-30 12:00:12", "rate": "0.1",
859 "amount": "3", "total": "0.3"
860 }))
861 order.mouvements.append(portfolio.Mouvement("ETH", "BTC", {
862 "tradeID": 43, "type": "buy", "fee": "0.0015",
863 "date": "2017-12-30 13:00:12", "rate": "0.2",
864 "amount": "2", "total": "0.4"
865 }))
866 self.assertEqual(portfolio.Amount("ETH", 5), order.filled_amount())
867 fetch.assert_not_called()
868 order.status = "open"
869 self.assertEqual(portfolio.Amount("ETH", 5), order.filled_amount(in_base_currency=False))
4ae84fb7
IB
870 fetch.assert_not_called()
871 self.assertEqual(portfolio.Amount("ETH", 5), order.filled_amount(in_base_currency=False, refetch=True))
c682bdf4
IB
872 fetch.assert_called_once()
873 self.assertEqual(portfolio.Amount("BTC", "0.7"), order.filled_amount(in_base_currency=True))
874
875 def test_fetch_mouvements(self):
876 self.m.ccxt.privatePostReturnOrderTrades.return_value = [
877 {
878 "tradeID": 42, "type": "buy", "fee": "0.0015",
879 "date": "2017-12-30 13:00:12", "rate": "0.1",
880 "amount": "3", "total": "0.3"
881 },
882 {
883 "tradeID": 43, "type": "buy", "fee": "0.0015",
884 "date": "2017-12-30 12:00:12", "rate": "0.2",
885 "amount": "2", "total": "0.4"
886 }
887 ]
888 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
889 D("0.1"), "BTC", "long", self.m, "trade")
890 order.id = 12
891 order.mouvements = ["Foo", "Bar", "Baz"]
892
893 order.fetch_mouvements()
894
895 self.m.ccxt.privatePostReturnOrderTrades.assert_called_with({"orderNumber": 12})
896 self.assertEqual(2, len(order.mouvements))
897 self.assertEqual(43, order.mouvements[0].id)
898 self.assertEqual(42, order.mouvements[1].id)
899
900 self.m.ccxt.privatePostReturnOrderTrades.side_effect = portfolio.ExchangeError
901 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
902 D("0.1"), "BTC", "long", self.m, "trade")
903 order.fetch_mouvements()
904 self.assertEqual(0, len(order.mouvements))
905
906 def test_mark_finished_order(self):
907 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
908 D("0.1"), "BTC", "short", self.m, "trade",
909 close_if_possible=True)
910 order.status = "closed"
911 self.m.debug = False
912
913 order.mark_finished_order()
914 self.m.ccxt.close_margin_position.assert_called_with("ETH", "BTC")
915 self.m.ccxt.close_margin_position.reset_mock()
916
917 order.status = "open"
918 order.mark_finished_order()
919 self.m.ccxt.close_margin_position.assert_not_called()
920
921 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
922 D("0.1"), "BTC", "short", self.m, "trade",
923 close_if_possible=False)
924 order.status = "closed"
925 order.mark_finished_order()
926 self.m.ccxt.close_margin_position.assert_not_called()
927
928 order = portfolio.Order("sell", portfolio.Amount("ETH", 10),
929 D("0.1"), "BTC", "short", self.m, "trade",
930 close_if_possible=True)
931 order.status = "closed"
932 order.mark_finished_order()
933 self.m.ccxt.close_margin_position.assert_not_called()
934
935 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
936 D("0.1"), "BTC", "long", self.m, "trade",
937 close_if_possible=True)
938 order.status = "closed"
939 order.mark_finished_order()
940 self.m.ccxt.close_margin_position.assert_not_called()
941
942 self.m.debug = True
943
944 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
945 D("0.1"), "BTC", "short", self.m, "trade",
946 close_if_possible=True)
947 order.status = "closed"
948
949 order.mark_finished_order()
950 self.m.ccxt.close_margin_position.assert_not_called()
951 self.m.report.log_debug_action.assert_called_once()
952
953 @mock.patch.object(portfolio.Order, "fetch_mouvements")
954 @mock.patch.object(portfolio.Order, "mark_disappeared_order")
955 @mock.patch.object(portfolio.Order, "mark_finished_order")
956 def test_fetch(self, mark_finished_order, mark_disappeared_order, fetch_mouvements):
957 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
958 D("0.1"), "BTC", "long", self.m, "trade")
959 order.id = 45
960 with self.subTest(debug=True):
961 self.m.debug = True
962 order.fetch()
963 self.m.report.log_debug_action.assert_called_once()
964 self.m.report.log_debug_action.reset_mock()
965 self.m.ccxt.fetch_order.assert_not_called()
966 mark_finished_order.assert_not_called()
967 mark_disappeared_order.assert_not_called()
968 fetch_mouvements.assert_not_called()
969
970 with self.subTest(debug=False):
971 self.m.debug = False
972 self.m.ccxt.fetch_order.return_value = {
973 "status": "foo",
974 "datetime": "timestamp"
975 }
1902674c 976 self.m.ccxt.is_dust_trade.return_value = False
c682bdf4
IB
977 order.fetch()
978
979 self.m.ccxt.fetch_order.assert_called_once_with(45)
980 fetch_mouvements.assert_called_once()
981 self.assertEqual("foo", order.status)
982 self.assertEqual("timestamp", order.timestamp)
983 self.assertEqual(1, len(order.results))
984 self.m.report.log_debug_action.assert_not_called()
985 mark_finished_order.assert_called_once()
986 mark_disappeared_order.assert_called_once()
987
988 mark_finished_order.reset_mock()
989 with self.subTest(missing_order=True):
990 self.m.ccxt.fetch_order.side_effect = [
991 portfolio.OrderNotCached,
992 ]
993 order.fetch()
994 self.assertEqual("closed_unknown", order.status)
995 mark_finished_order.assert_called_once()
996
997 def test_mark_disappeared_order(self):
998 with self.subTest("Open order"):
999 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1000 D("0.1"), "BTC", "long", self.m, "trade")
1001 order.id = 45
1002 order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
1003 "tradeID":21336541,
1004 "currencyPair":"BTC_XRP",
1005 "type":"sell",
1006 "rate":"0.00007013",
1007 "amount":"0.00000222",
1008 "total":"0.00000000",
1009 "fee":"0.00150000",
1010 "date":"2018-04-02 00:09:13"
1011 }))
1012 order.mark_disappeared_order()
1013 self.assertEqual("pending", order.status)
1014
1015 with self.subTest("Non-zero amount"):
1016 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1017 D("0.1"), "BTC", "long", self.m, "trade")
1018 order.id = 45
1019 order.status = "closed"
1020 order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
1021 "tradeID":21336541,
1022 "currencyPair":"BTC_XRP",
1023 "type":"sell",
1024 "rate":"0.00007013",
1025 "amount":"0.00000222",
1026 "total":"0.00000010",
1027 "fee":"0.00150000",
1028 "date":"2018-04-02 00:09:13"
1029 }))
1030 order.mark_disappeared_order()
1031 self.assertEqual("closed", order.status)
1032
1033 with self.subTest("Other mouvements"):
1034 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1035 D("0.1"), "BTC", "long", self.m, "trade")
1036 order.id = 45
1037 order.status = "closed"
1038 order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
1039 "tradeID":21336541,
1040 "currencyPair":"BTC_XRP",
1041 "type":"sell",
1042 "rate":"0.00007013",
1043 "amount":"0.00000222",
1044 "total":"0.00000001",
1045 "fee":"0.00150000",
1046 "date":"2018-04-02 00:09:13"
1047 }))
1048 order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
1049 "tradeID":21336541,
1050 "currencyPair":"BTC_XRP",
1051 "type":"sell",
1052 "rate":"0.00007013",
1053 "amount":"0.00000222",
1054 "total":"0.00000000",
1055 "fee":"0.00150000",
1056 "date":"2018-04-02 00:09:13"
1057 }))
1058 order.mark_disappeared_order()
1059 self.assertEqual("error_disappeared", order.status)
1060
1061 with self.subTest("Order disappeared"):
1062 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1063 D("0.1"), "BTC", "long", self.m, "trade")
1064 order.id = 45
1065 order.status = "closed"
1066 order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
1067 "tradeID":21336541,
1068 "currencyPair":"BTC_XRP",
1069 "type":"sell",
1070 "rate":"0.00007013",
1071 "amount":"0.00000222",
1072 "total":"0.00000000",
1073 "fee":"0.00150000",
1074 "date":"2018-04-02 00:09:13"
1075 }))
1076 order.mark_disappeared_order()
1077 self.assertEqual("error_disappeared", order.status)
1078
1079 @mock.patch.object(portfolio.Order, "fetch")
1080 def test_get_status(self, fetch):
1081 with self.subTest(debug=True):
1082 self.m.debug = True
1083 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1084 D("0.1"), "BTC", "long", self.m, "trade")
1085 self.assertEqual("pending", order.get_status())
1086 fetch.assert_not_called()
1087 self.m.report.log_debug_action.assert_called_once()
1088
1089 with self.subTest(debug=False, finished=False):
1090 self.m.debug = False
1091 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1092 D("0.1"), "BTC", "long", self.m, "trade")
1093 def _fetch(order):
1094 def update_status():
1095 order.status = "open"
1096 return update_status
1097 fetch.side_effect = _fetch(order)
1098 self.assertEqual("open", order.get_status())
1099 fetch.assert_called_once()
1100
1101 fetch.reset_mock()
1102 with self.subTest(debug=False, finished=True):
1103 self.m.debug = False
1104 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1105 D("0.1"), "BTC", "long", self.m, "trade")
1106 def _fetch(order):
1107 def update_status():
1108 order.status = "closed"
1109 return update_status
1110 fetch.side_effect = _fetch(order)
1111 self.assertEqual("closed", order.get_status())
1112 fetch.assert_called_once()
1113
1114 def test_run(self):
1115 self.m.ccxt.order_precision.return_value = 4
1116 with self.subTest(debug=True):
1117 self.m.debug = True
1118 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1119 D("0.1"), "BTC", "long", self.m, "trade")
1120 order.run()
1121 self.m.ccxt.create_order.assert_not_called()
1122 self.m.report.log_debug_action.assert_called_with("market.ccxt.create_order('ETH/BTC', 'limit', 'buy', 10.0000, price=0.1, account=exchange)")
1123 self.assertEqual("open", order.status)
1124 self.assertEqual(1, len(order.results))
1125 self.assertEqual(-1, order.id)
1126
1127 self.m.ccxt.create_order.reset_mock()
1128 with self.subTest(debug=False):
1129 self.m.debug = False
1130 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1131 D("0.1"), "BTC", "long", self.m, "trade")
1132 self.m.ccxt.create_order.return_value = { "id": 123 }
1133 order.run()
1134 self.m.ccxt.create_order.assert_called_once()
1135 self.assertEqual(1, len(order.results))
1136 self.assertEqual("open", order.status)
1137
1138 self.m.ccxt.create_order.reset_mock()
1139 with self.subTest(exception=True):
1140 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1141 D("0.1"), "BTC", "long", self.m, "trade")
1142 self.m.ccxt.create_order.side_effect = Exception("bouh")
1143 order.run()
1144 self.m.ccxt.create_order.assert_called_once()
1145 self.assertEqual(0, len(order.results))
1146 self.assertEqual("error", order.status)
1147 self.m.report.log_error.assert_called_once()
1148
1149 self.m.ccxt.create_order.reset_mock()
1150 with self.subTest(dust_amount_exception=True),\
1151 mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
1152 order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001),
1153 D("0.1"), "BTC", "long", self.m, "trade")
1154 self.m.ccxt.create_order.side_effect = portfolio.InvalidOrder
1155 order.run()
1156 self.m.ccxt.create_order.assert_called_once()
1157 self.assertEqual(0, len(order.results))
1158 self.assertEqual("closed", order.status)
1159 mark_finished_order.assert_called_once()
1160
1161 self.m.ccxt.order_precision.return_value = 8
1162 self.m.ccxt.create_order.reset_mock()
1163 with self.subTest(insufficient_funds=True),\
1164 mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
1165 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
1166 D("0.1"), "BTC", "long", self.m, "trade")
1167 self.m.ccxt.create_order.side_effect = [
1168 portfolio.InsufficientFunds,
1169 portfolio.InsufficientFunds,
1170 portfolio.InsufficientFunds,
1171 { "id": 123 },
1172 ]
1173 order.run()
1174 self.m.ccxt.create_order.assert_has_calls([
1175 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1176 mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')),
1177 mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')),
1178 mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')),
1179 ])
1180 self.assertEqual(4, self.m.ccxt.create_order.call_count)
1181 self.assertEqual(1, len(order.results))
1182 self.assertEqual("open", order.status)
1183 self.assertEqual(4, order.tries)
1184 self.m.report.log_error.assert_called()
1185 self.assertEqual(4, self.m.report.log_error.call_count)
1186
1187 self.m.ccxt.order_precision.return_value = 8
1188 self.m.ccxt.create_order.reset_mock()
1189 self.m.report.log_error.reset_mock()
1190 with self.subTest(insufficient_funds=True),\
1191 mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
1192 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
1193 D("0.1"), "BTC", "long", self.m, "trade")
1194 self.m.ccxt.create_order.side_effect = [
1195 portfolio.InsufficientFunds,
1196 portfolio.InsufficientFunds,
1197 portfolio.InsufficientFunds,
1198 portfolio.InsufficientFunds,
1199 portfolio.InsufficientFunds,
1200 ]
1201 order.run()
1202 self.m.ccxt.create_order.assert_has_calls([
1203 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1204 mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')),
1205 mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')),
1206 mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')),
1207 mock.call('ETH/BTC', 'limit', 'buy', D('0.00096059'), account='exchange', price=D('0.1')),
1208 ])
1209 self.assertEqual(5, self.m.ccxt.create_order.call_count)
1210 self.assertEqual(0, len(order.results))
1211 self.assertEqual("error", order.status)
1212 self.assertEqual(5, order.tries)
1213 self.m.report.log_error.assert_called()
1214 self.assertEqual(5, self.m.report.log_error.call_count)
1215 self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00096060 ETH at 0.1 BTC [pending])", exception=mock.ANY)
1216
1217 self.m.reset_mock()
1218 with self.subTest(invalid_nonce=True):
1219 with self.subTest(retry_success=True):
1220 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
1221 D("0.1"), "BTC", "long", self.m, "trade")
1222 self.m.ccxt.create_order.side_effect = [
1223 portfolio.InvalidNonce,
1224 portfolio.InvalidNonce,
1225 { "id": 123 },
1226 ]
1227 order.run()
1228 self.m.ccxt.create_order.assert_has_calls([
1229 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1230 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1231 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1232 ])
1233 self.assertEqual(3, self.m.ccxt.create_order.call_count)
1234 self.assertEqual(3, order.tries)
1235 self.m.report.log_error.assert_called()
1236 self.assertEqual(2, self.m.report.log_error.call_count)
1237 self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after invalid nonce", exception=mock.ANY)
1238 self.assertEqual(123, order.id)
1239
1240 self.m.reset_mock()
1241 with self.subTest(retry_success=False):
1242 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
1243 D("0.1"), "BTC", "long", self.m, "trade")
1244 self.m.ccxt.create_order.side_effect = [
1245 portfolio.InvalidNonce,
1246 portfolio.InvalidNonce,
1247 portfolio.InvalidNonce,
1248 portfolio.InvalidNonce,
1249 portfolio.InvalidNonce,
1250 ]
1251 order.run()
1252 self.assertEqual(5, self.m.ccxt.create_order.call_count)
1253 self.assertEqual(5, order.tries)
1254 self.m.report.log_error.assert_called()
1255 self.assertEqual(5, self.m.report.log_error.call_count)
1256 self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00100000 ETH at 0.1 BTC [pending]) after invalid nonce", exception=mock.ANY)
1257 self.assertEqual("error", order.status)
1258
1259 self.m.reset_mock()
1260 with self.subTest(request_timeout=True):
1261 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
1262 D("0.1"), "BTC", "long", self.m, "trade")
1263 with self.subTest(retrieved=False), \
1264 mock.patch.object(order, "retrieve_order") as retrieve:
1265 self.m.ccxt.create_order.side_effect = [
1266 portfolio.RequestTimeout,
1267 portfolio.RequestTimeout,
1268 { "id": 123 },
1269 ]
1270 retrieve.return_value = False
1271 order.run()
1272 self.m.ccxt.create_order.assert_has_calls([
1273 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1274 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1275 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1276 ])
1277 self.assertEqual(3, self.m.ccxt.create_order.call_count)
1278 self.assertEqual(3, order.tries)
1279 self.m.report.log_error.assert_called()
1280 self.assertEqual(2, self.m.report.log_error.call_count)
1281 self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after timeout", exception=mock.ANY)
1282 self.assertEqual(123, order.id)
1283
1284 self.m.reset_mock()
1285 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
1286 D("0.1"), "BTC", "long", self.m, "trade")
1287 with self.subTest(retrieved=True), \
1288 mock.patch.object(order, "retrieve_order") as retrieve:
1289 self.m.ccxt.create_order.side_effect = [
1290 portfolio.RequestTimeout,
1291 ]
1292 def _retrieve():
1293 order.results.append({"id": 123})
1294 return True
1295 retrieve.side_effect = _retrieve
1296 order.run()
1297 self.m.ccxt.create_order.assert_has_calls([
1298 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1299 ])
1300 self.assertEqual(1, self.m.ccxt.create_order.call_count)
1301 self.assertEqual(1, order.tries)
1302 self.m.report.log_error.assert_called()
1303 self.assertEqual(1, self.m.report.log_error.call_count)
1304 self.m.report.log_error.assert_called_with(mock.ANY, message="Timeout, found the order")
1305 self.assertEqual(123, order.id)
1306
1307 self.m.reset_mock()
1308 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
1309 D("0.1"), "BTC", "long", self.m, "trade")
1310 with self.subTest(retrieved=False), \
1311 mock.patch.object(order, "retrieve_order") as retrieve:
1312 self.m.ccxt.create_order.side_effect = [
1313 portfolio.RequestTimeout,
1314 portfolio.RequestTimeout,
1315 portfolio.RequestTimeout,
1316 portfolio.RequestTimeout,
1317 portfolio.RequestTimeout,
1318 ]
1319 retrieve.return_value = False
1320 order.run()
1321 self.m.ccxt.create_order.assert_has_calls([
1322 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1323 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1324 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1325 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1326 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
1327 ])
1328 self.assertEqual(5, self.m.ccxt.create_order.call_count)
1329 self.assertEqual(5, order.tries)
1330 self.m.report.log_error.assert_called()
1331 self.assertEqual(5, self.m.report.log_error.call_count)
1332 self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00100000 ETH at 0.1 BTC [pending]) after timeouts", exception=mock.ANY)
1333 self.assertEqual("error", order.status)
1334
1335 def test_retrieve_order(self):
1336 with self.subTest(similar_open_order=True):
1337 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
1338 D("0.1"), "BTC", "long", self.m, "trade")
1339 order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55)
1340
1341 self.m.ccxt.order_precision.return_value = 8
1342 self.m.ccxt.fetch_orders.return_value = [
1343 { # Wrong amount
1344 'amount': 0.002, 'cost': 0.1,
1345 'datetime': '2018-03-25T15:15:51.000Z',
1346 'fee': None, 'filled': 0.0,
1347 'id': '1',
1348 'info': {
1349 'amount': '0.002',
1350 'date': '2018-03-25 15:15:51',
1351 'margin': 0, 'orderNumber': '1',
1352 'price': '0.1', 'rate': '0.1',
1353 'side': 'buy', 'startingAmount': '0.002',
1354 'status': 'open', 'total': '0.0002',
1355 'type': 'limit'
1356 },
1357 'price': 0.1, 'remaining': 0.002, 'side': 'buy',
1358 'status': 'open', 'symbol': 'ETH/BTC',
1359 'timestamp': 1521990951000, 'trades': None,
1360 'type': 'limit'
1361 },
1362 { # Margin
1363 'amount': 0.001, 'cost': 0.1,
1364 'datetime': '2018-03-25T15:15:51.000Z',
1365 'fee': None, 'filled': 0.0,
1366 'id': '2',
1367 'info': {
1368 'amount': '0.001',
1369 'date': '2018-03-25 15:15:51',
1370 'margin': 1, 'orderNumber': '2',
1371 'price': '0.1', 'rate': '0.1',
1372 'side': 'buy', 'startingAmount': '0.001',
1373 'status': 'open', 'total': '0.0001',
1374 'type': 'limit'
1375 },
1376 'price': 0.1, 'remaining': 0.001, 'side': 'buy',
1377 'status': 'open', 'symbol': 'ETH/BTC',
1378 'timestamp': 1521990951000, 'trades': None,
1379 'type': 'limit'
1380 },
1381 { # selling
1382 'amount': 0.001, 'cost': 0.1,
1383 'datetime': '2018-03-25T15:15:51.000Z',
1384 'fee': None, 'filled': 0.0,
1385 'id': '3',
1386 'info': {
1387 'amount': '0.001',
1388 'date': '2018-03-25 15:15:51',
1389 'margin': 0, 'orderNumber': '3',
1390 'price': '0.1', 'rate': '0.1',
1391 'side': 'sell', 'startingAmount': '0.001',
1392 'status': 'open', 'total': '0.0001',
1393 'type': 'limit'
1394 },
1395 'price': 0.1, 'remaining': 0.001, 'side': 'sell',
1396 'status': 'open', 'symbol': 'ETH/BTC',
1397 'timestamp': 1521990951000, 'trades': None,
1398 'type': 'limit'
1399 },
1400 { # Wrong rate
1401 'amount': 0.001, 'cost': 0.15,
1402 'datetime': '2018-03-25T15:15:51.000Z',
1403 'fee': None, 'filled': 0.0,
1404 'id': '4',
1405 'info': {
1406 'amount': '0.001',
1407 'date': '2018-03-25 15:15:51',
1408 'margin': 0, 'orderNumber': '4',
1409 'price': '0.15', 'rate': '0.15',
1410 'side': 'buy', 'startingAmount': '0.001',
1411 'status': 'open', 'total': '0.0001',
1412 'type': 'limit'
1413 },
1414 'price': 0.15, 'remaining': 0.001, 'side': 'buy',
1415 'status': 'open', 'symbol': 'ETH/BTC',
1416 'timestamp': 1521990951000, 'trades': None,
1417 'type': 'limit'
1418 },
1419 { # All good
1420 'amount': 0.001, 'cost': 0.1,
1421 'datetime': '2018-03-25T15:15:51.000Z',
1422 'fee': None, 'filled': 0.0,
1423 'id': '5',
1424 'info': {
1425 'amount': '0.001',
1426 'date': '2018-03-25 15:15:51',
1427 'margin': 0, 'orderNumber': '1',
1428 'price': '0.1', 'rate': '0.1',
1429 'side': 'buy', 'startingAmount': '0.001',
1430 'status': 'open', 'total': '0.0001',
1431 'type': 'limit'
1432 },
1433 'price': 0.1, 'remaining': 0.001, 'side': 'buy',
1434 'status': 'open', 'symbol': 'ETH/BTC',
1435 'timestamp': 1521990951000, 'trades': None,
1436 'type': 'limit'
1437 }
1438 ]
1439 result = order.retrieve_order()
1440 self.assertTrue(result)
1441 self.assertEqual('5', order.results[0]["id"])
1442 self.m.ccxt.fetch_my_trades.assert_not_called()
1443 self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750)
1444
1445 self.m.reset_mock()
1446 with self.subTest(similar_open_order=False, past_trades=True):
1447 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
1448 D("0.1"), "BTC", "long", self.m, "trade")
1449 order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55)
1450
1451 self.m.ccxt.order_precision.return_value = 8
1452 self.m.ccxt.fetch_orders.return_value = []
1453 self.m.ccxt.fetch_my_trades.return_value = [
1454 { # Wrong timestamp 1
1455 'amount': 0.0006,
1456 'cost': 0.00006,
1457 'datetime': '2018-03-25T15:15:14.000Z',
1458 'id': '1-1',
1459 'info': {
1460 'amount': '0.0006',
1461 'category': 'exchange',
1462 'date': '2018-03-25 15:15:14',
1463 'fee': '0.00150000',
1464 'globalTradeID': 1,
1465 'orderNumber': '1',
1466 'rate': '0.1',
1467 'total': '0.00006',
1468 'tradeID': '1-1',
1469 'type': 'buy'
1470 },
1471 'order': '1',
1472 'price': 0.1,
1473 'side': 'buy',
1474 'symbol': 'ETH/BTC',
1475 'timestamp': 1521983714,
1476 'type': 'limit'
1477 },
1478 { # Wrong timestamp 2
1479 'amount': 0.0004,
1480 'cost': 0.00004,
1481 'datetime': '2018-03-25T15:16:54.000Z',
1482 'id': '1-2',
1483 'info': {
1484 'amount': '0.0004',
1485 'category': 'exchange',
1486 'date': '2018-03-25 15:16:54',
1487 'fee': '0.00150000',
1488 'globalTradeID': 2,
1489 'orderNumber': '1',
1490 'rate': '0.1',
1491 'total': '0.00004',
1492 'tradeID': '1-2',
1493 'type': 'buy'
1494 },
1495 'order': '1',
1496 'price': 0.1,
1497 'side': 'buy',
1498 'symbol': 'ETH/BTC',
1499 'timestamp': 1521983814,
1500 'type': 'limit'
1501 },
1502 { # Wrong side 1
1503 'amount': 0.0006,
1504 'cost': 0.00006,
1505 'datetime': '2018-03-25T15:15:54.000Z',
1506 'id': '2-1',
1507 'info': {
1508 'amount': '0.0006',
1509 'category': 'exchange',
1510 'date': '2018-03-25 15:15:54',
1511 'fee': '0.00150000',
1512 'globalTradeID': 1,
1513 'orderNumber': '2',
1514 'rate': '0.1',
1515 'total': '0.00006',
1516 'tradeID': '2-1',
1517 'type': 'sell'
1518 },
1519 'order': '2',
1520 'price': 0.1,
1521 'side': 'sell',
1522 'symbol': 'ETH/BTC',
1523 'timestamp': 1521983754,
1524 'type': 'limit'
1525 },
1526 { # Wrong side 2
1527 'amount': 0.0004,
1528 'cost': 0.00004,
1529 'datetime': '2018-03-25T15:16:54.000Z',
1530 'id': '2-2',
1531 'info': {
1532 'amount': '0.0004',
1533 'category': 'exchange',
1534 'date': '2018-03-25 15:16:54',
1535 'fee': '0.00150000',
1536 'globalTradeID': 2,
1537 'orderNumber': '2',
1538 'rate': '0.1',
1539 'total': '0.00004',
1540 'tradeID': '2-2',
1541 'type': 'buy'
1542 },
1543 'order': '2',
1544 'price': 0.1,
1545 'side': 'buy',
1546 'symbol': 'ETH/BTC',
1547 'timestamp': 1521983814,
1548 'type': 'limit'
1549 },
1550 { # Margin trade 1
1551 'amount': 0.0006,
1552 'cost': 0.00006,
1553 'datetime': '2018-03-25T15:15:54.000Z',
1554 'id': '3-1',
1555 'info': {
1556 'amount': '0.0006',
1557 'category': 'marginTrade',
1558 'date': '2018-03-25 15:15:54',
1559 'fee': '0.00150000',
1560 'globalTradeID': 1,
1561 'orderNumber': '3',
1562 'rate': '0.1',
1563 'total': '0.00006',
1564 'tradeID': '3-1',
1565 'type': 'buy'
1566 },
1567 'order': '3',
1568 'price': 0.1,
1569 'side': 'buy',
1570 'symbol': 'ETH/BTC',
1571 'timestamp': 1521983754,
1572 'type': 'limit'
1573 },
1574 { # Margin trade 2
1575 'amount': 0.0004,
1576 'cost': 0.00004,
1577 'datetime': '2018-03-25T15:16:54.000Z',
1578 'id': '3-2',
1579 'info': {
1580 'amount': '0.0004',
1581 'category': 'marginTrade',
1582 'date': '2018-03-25 15:16:54',
1583 'fee': '0.00150000',
1584 'globalTradeID': 2,
1585 'orderNumber': '3',
1586 'rate': '0.1',
1587 'total': '0.00004',
1588 'tradeID': '3-2',
1589 'type': 'buy'
1590 },
1591 'order': '3',
1592 'price': 0.1,
1593 'side': 'buy',
1594 'symbol': 'ETH/BTC',
1595 'timestamp': 1521983814,
1596 'type': 'limit'
1597 },
1598 { # Wrong amount 1
1599 'amount': 0.0005,
1600 'cost': 0.00005,
1601 'datetime': '2018-03-25T15:15:54.000Z',
1602 'id': '4-1',
1603 'info': {
1604 'amount': '0.0005',
1605 'category': 'exchange',
1606 'date': '2018-03-25 15:15:54',
1607 'fee': '0.00150000',
1608 'globalTradeID': 1,
1609 'orderNumber': '4',
1610 'rate': '0.1',
1611 'total': '0.00005',
1612 'tradeID': '4-1',
1613 'type': 'buy'
1614 },
1615 'order': '4',
1616 'price': 0.1,
1617 'side': 'buy',
1618 'symbol': 'ETH/BTC',
1619 'timestamp': 1521983754,
1620 'type': 'limit'
1621 },
1622 { # Wrong amount 2
1623 'amount': 0.0004,
1624 'cost': 0.00004,
1625 'datetime': '2018-03-25T15:16:54.000Z',
1626 'id': '4-2',
1627 'info': {
1628 'amount': '0.0004',
1629 'category': 'exchange',
1630 'date': '2018-03-25 15:16:54',
1631 'fee': '0.00150000',
1632 'globalTradeID': 2,
1633 'orderNumber': '4',
1634 'rate': '0.1',
1635 'total': '0.00004',
1636 'tradeID': '4-2',
1637 'type': 'buy'
1638 },
1639 'order': '4',
1640 'price': 0.1,
1641 'side': 'buy',
1642 'symbol': 'ETH/BTC',
1643 'timestamp': 1521983814,
1644 'type': 'limit'
1645 },
1646 { # Wrong price 1
1647 'amount': 0.0006,
1648 'cost': 0.000066,
1649 'datetime': '2018-03-25T15:15:54.000Z',
1650 'id': '5-1',
1651 'info': {
1652 'amount': '0.0006',
1653 'category': 'exchange',
1654 'date': '2018-03-25 15:15:54',
1655 'fee': '0.00150000',
1656 'globalTradeID': 1,
1657 'orderNumber': '5',
1658 'rate': '0.11',
1659 'total': '0.000066',
1660 'tradeID': '5-1',
1661 'type': 'buy'
1662 },
1663 'order': '5',
1664 'price': 0.11,
1665 'side': 'buy',
1666 'symbol': 'ETH/BTC',
1667 'timestamp': 1521983754,
1668 'type': 'limit'
1669 },
1670 { # Wrong price 2
1671 'amount': 0.0004,
1672 'cost': 0.00004,
1673 'datetime': '2018-03-25T15:16:54.000Z',
1674 'id': '5-2',
1675 'info': {
1676 'amount': '0.0004',
1677 'category': 'exchange',
1678 'date': '2018-03-25 15:16:54',
1679 'fee': '0.00150000',
1680 'globalTradeID': 2,
1681 'orderNumber': '5',
1682 'rate': '0.1',
1683 'total': '0.00004',
1684 'tradeID': '5-2',
1685 'type': 'buy'
1686 },
1687 'order': '5',
1688 'price': 0.1,
1689 'side': 'buy',
1690 'symbol': 'ETH/BTC',
1691 'timestamp': 1521983814,
1692 'type': 'limit'
1693 },
1694 { # All good 1
1695 'amount': 0.0006,
1696 'cost': 0.00006,
1697 'datetime': '2018-03-25T15:15:54.000Z',
1698 'id': '7-1',
1699 'info': {
1700 'amount': '0.0006',
1701 'category': 'exchange',
1702 'date': '2018-03-25 15:15:54',
1703 'fee': '0.00150000',
1704 'globalTradeID': 1,
1705 'orderNumber': '7',
1706 'rate': '0.1',
1707 'total': '0.00006',
1708 'tradeID': '7-1',
1709 'type': 'buy'
1710 },
1711 'order': '7',
1712 'price': 0.1,
1713 'side': 'buy',
1714 'symbol': 'ETH/BTC',
1715 'timestamp': 1521983754,
1716 'type': 'limit'
1717 },
1718 { # All good 2
1719 'amount': 0.0004,
1720 'cost': 0.000036,
1721 'datetime': '2018-03-25T15:16:54.000Z',
1722 'id': '7-2',
1723 'info': {
1724 'amount': '0.0004',
1725 'category': 'exchange',
1726 'date': '2018-03-25 15:16:54',
1727 'fee': '0.00150000',
1728 'globalTradeID': 2,
1729 'orderNumber': '7',
1730 'rate': '0.09',
1731 'total': '0.000036',
1732 'tradeID': '7-2',
1733 'type': 'buy'
1734 },
1735 'order': '7',
1736 'price': 0.09,
1737 'side': 'buy',
1738 'symbol': 'ETH/BTC',
1739 'timestamp': 1521983814,
1740 'type': 'limit'
1741 },
1742 ]
1743
1744 result = order.retrieve_order()
1745 self.assertTrue(result)
1746 self.assertEqual('7', order.results[0]["id"])
1747 self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750)
1748
1749 self.m.reset_mock()
1750 with self.subTest(similar_open_order=False, past_trades=False):
1751 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
1752 D("0.1"), "BTC", "long", self.m, "trade")
1753 order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55)
1754
1755 self.m.ccxt.order_precision.return_value = 8
1756 self.m.ccxt.fetch_orders.return_value = []
1757 self.m.ccxt.fetch_my_trades.return_value = []
1758 result = order.retrieve_order()
1759 self.assertFalse(result)
1760
3080f31d 1761@unittest.skipUnless("unit" in limits, "Unit skipped")
c682bdf4
IB
1762class MouvementTest(WebMockTestCase):
1763 def test_values(self):
1764 mouvement = portfolio.Mouvement("ETH", "BTC", {
1765 "tradeID": 42, "type": "buy", "fee": "0.0015",
1766 "date": "2017-12-30 12:00:12", "rate": "0.1",
1767 "amount": "10", "total": "1"
1768 })
1769 self.assertEqual("ETH", mouvement.currency)
1770 self.assertEqual("BTC", mouvement.base_currency)
1771 self.assertEqual(42, mouvement.id)
1772 self.assertEqual("buy", mouvement.action)
1773 self.assertEqual(D("0.0015"), mouvement.fee_rate)
e7d7c0e5 1774 self.assertEqual(portfolio.datetime.datetime(2017, 12, 30, 12, 0, 12), mouvement.date)
c682bdf4
IB
1775 self.assertEqual(D("0.1"), mouvement.rate)
1776 self.assertEqual(portfolio.Amount("ETH", "10"), mouvement.total)
1777 self.assertEqual(portfolio.Amount("BTC", "1"), mouvement.total_in_base)
1778
1779 mouvement = portfolio.Mouvement("ETH", "BTC", { "foo": "bar" })
1780 self.assertIsNone(mouvement.date)
1781 self.assertIsNone(mouvement.id)
1782 self.assertIsNone(mouvement.action)
1783 self.assertEqual(-1, mouvement.fee_rate)
1784 self.assertEqual(0, mouvement.rate)
1785 self.assertEqual(portfolio.Amount("ETH", 0), mouvement.total)
1786 self.assertEqual(portfolio.Amount("BTC", 0), mouvement.total_in_base)
1787
1788 def test__repr(self):
1789 mouvement = portfolio.Mouvement("ETH", "BTC", {
1790 "tradeID": 42, "type": "buy", "fee": "0.0015",
1791 "date": "2017-12-30 12:00:12", "rate": "0.1",
1792 "amount": "10", "total": "1"
1793 })
1794 self.assertEqual("Mouvement(2017-12-30 12:00:12 ; buy 10.00000000 ETH (1.00000000 BTC) fee: 0.1500%)", repr(mouvement))
1795
1796 mouvement = portfolio.Mouvement("ETH", "BTC", {
1797 "tradeID": 42, "type": "buy",
1798 "date": "garbage", "rate": "0.1",
1799 "amount": "10", "total": "1"
1800 })
1801 self.assertEqual("Mouvement(No date ; buy 10.00000000 ETH (1.00000000 BTC))", repr(mouvement))
1802
1803 def test_as_json(self):
1804 mouvement = portfolio.Mouvement("ETH", "BTC", {
1805 "tradeID": 42, "type": "buy", "fee": "0.0015",
1806 "date": "2017-12-30 12:00:12", "rate": "0.1",
1807 "amount": "10", "total": "1"
1808 })
1809 as_json = mouvement.as_json()
1810
1811 self.assertEqual(D("0.0015"), as_json["fee_rate"])
e7d7c0e5 1812 self.assertEqual(portfolio.datetime.datetime(2017, 12, 30, 12, 0, 12), as_json["date"])
c682bdf4
IB
1813 self.assertEqual("buy", as_json["action"])
1814 self.assertEqual(D("10"), as_json["total"])
1815 self.assertEqual(D("1"), as_json["total_in_base"])
1816 self.assertEqual("BTC", as_json["base_currency"])
1817 self.assertEqual("ETH", as_json["currency"])
1818
3080f31d 1819@unittest.skipUnless("unit" in limits, "Unit skipped")
c682bdf4
IB
1820class AmountTest(WebMockTestCase):
1821 def test_values(self):
1822 amount = portfolio.Amount("BTC", "0.65")
1823 self.assertEqual(D("0.65"), amount.value)
1824 self.assertEqual("BTC", amount.currency)
1825
1826 def test_in_currency(self):
1827 amount = portfolio.Amount("ETC", 10)
1828
1829 self.assertEqual(amount, amount.in_currency("ETC", self.m))
1830
1831 with self.subTest(desc="no ticker for currency"):
1832 self.m.get_ticker.return_value = None
1833
1834 self.assertRaises(Exception, amount.in_currency, "ETH", self.m)
1835
1836 with self.subTest(desc="nominal case"):
1837 self.m.get_ticker.return_value = {
1838 "bid": D("0.2"),
1839 "ask": D("0.4"),
1840 "average": D("0.3"),
1841 "foo": "bar",
1842 }
1843 converted_amount = amount.in_currency("ETH", self.m)
1844
1845 self.assertEqual(D("3.0"), converted_amount.value)
1846 self.assertEqual("ETH", converted_amount.currency)
1847 self.assertEqual(amount, converted_amount.linked_to)
1848 self.assertEqual("bar", converted_amount.ticker["foo"])
1849
1850 converted_amount = amount.in_currency("ETH", self.m, action="bid", compute_value="default")
1851 self.assertEqual(D("2"), converted_amount.value)
1852
1853 converted_amount = amount.in_currency("ETH", self.m, compute_value="ask")
1854 self.assertEqual(D("4"), converted_amount.value)
1855
1856 converted_amount = amount.in_currency("ETH", self.m, rate=D("0.02"))
1857 self.assertEqual(D("0.2"), converted_amount.value)
1858
1859 def test__round(self):
1860 amount = portfolio.Amount("BAR", portfolio.D("1.23456789876"))
1861 self.assertEqual(D("1.23456789"), round(amount).value)
1862 self.assertEqual(D("1.23"), round(amount, 2).value)
1863
1864 def test__abs(self):
1865 amount = portfolio.Amount("SC", -120)
1866 self.assertEqual(120, abs(amount).value)
1867 self.assertEqual("SC", abs(amount).currency)
1868
1869 amount = portfolio.Amount("SC", 10)
1870 self.assertEqual(10, abs(amount).value)
1871 self.assertEqual("SC", abs(amount).currency)
1872
1873 def test__add(self):
1874 amount1 = portfolio.Amount("XVG", "12.9")
1875 amount2 = portfolio.Amount("XVG", "13.1")
1876
1877 self.assertEqual(26, (amount1 + amount2).value)
1878 self.assertEqual("XVG", (amount1 + amount2).currency)
1879
1880 amount3 = portfolio.Amount("ETH", "1.6")
1881 with self.assertRaises(Exception):
1882 amount1 + amount3
1883
1884 amount4 = portfolio.Amount("ETH", 0.0)
1885 self.assertEqual(amount1, amount1 + amount4)
1886
1887 self.assertEqual(amount1, amount1 + 0)
1888
1889 def test__radd(self):
1890 amount = portfolio.Amount("XVG", "12.9")
1891
1892 self.assertEqual(amount, 0 + amount)
1893 with self.assertRaises(Exception):
1894 4 + amount
1895
1896 def test__sub(self):
1897 amount1 = portfolio.Amount("XVG", "13.3")
1898 amount2 = portfolio.Amount("XVG", "13.1")
1899
1900 self.assertEqual(D("0.2"), (amount1 - amount2).value)
1901 self.assertEqual("XVG", (amount1 - amount2).currency)
1902
1903 amount3 = portfolio.Amount("ETH", "1.6")
1904 with self.assertRaises(Exception):
1905 amount1 - amount3
1906
1907 amount4 = portfolio.Amount("ETH", 0.0)
1908 self.assertEqual(amount1, amount1 - amount4)
1909
1910 def test__rsub(self):
1911 amount = portfolio.Amount("ETH", "1.6")
1912 with self.assertRaises(Exception):
1913 3 - amount
1914
1915 self.assertEqual(portfolio.Amount("ETH", "-1.6"), 0-amount)
1916
1917 def test__mul(self):
1918 amount = portfolio.Amount("XEM", 11)
1919
1920 self.assertEqual(D("38.5"), (amount * D("3.5")).value)
1921 self.assertEqual(D("33"), (amount * 3).value)
1922
1923 with self.assertRaises(Exception):
1924 amount * amount
1925
1926 def test__rmul(self):
1927 amount = portfolio.Amount("XEM", 11)
1928
1929 self.assertEqual(D("38.5"), (D("3.5") * amount).value)
1930 self.assertEqual(D("33"), (3 * amount).value)
1931
1932 def test__floordiv(self):
1933 amount = portfolio.Amount("XEM", 11)
1934
1935 self.assertEqual(D("5.5"), (amount / 2).value)
1936 self.assertEqual(D("4.4"), (amount / D("2.5")).value)
1937
1938 with self.assertRaises(Exception):
1939 amount / amount
1940
1941 def test__truediv(self):
1942 amount = portfolio.Amount("XEM", 11)
1943
1944 self.assertEqual(D("5.5"), (amount / 2).value)
1945 self.assertEqual(D("4.4"), (amount / D("2.5")).value)
1946
1947 def test__lt(self):
1948 amount1 = portfolio.Amount("BTD", 11.3)
1949 amount2 = portfolio.Amount("BTD", 13.1)
1950
1951 self.assertTrue(amount1 < amount2)
1952 self.assertFalse(amount2 < amount1)
1953 self.assertFalse(amount1 < amount1)
1954
1955 amount3 = portfolio.Amount("BTC", 1.6)
1956 with self.assertRaises(Exception):
1957 amount1 < amount3
1958
1959 def test__le(self):
1960 amount1 = portfolio.Amount("BTD", 11.3)
1961 amount2 = portfolio.Amount("BTD", 13.1)
1962
1963 self.assertTrue(amount1 <= amount2)
1964 self.assertFalse(amount2 <= amount1)
1965 self.assertTrue(amount1 <= amount1)
1966
1967 amount3 = portfolio.Amount("BTC", 1.6)
1968 with self.assertRaises(Exception):
1969 amount1 <= amount3
1970
1971 def test__gt(self):
1972 amount1 = portfolio.Amount("BTD", 11.3)
1973 amount2 = portfolio.Amount("BTD", 13.1)
1974
1975 self.assertTrue(amount2 > amount1)
1976 self.assertFalse(amount1 > amount2)
1977 self.assertFalse(amount1 > amount1)
1978
1979 amount3 = portfolio.Amount("BTC", 1.6)
1980 with self.assertRaises(Exception):
1981 amount3 > amount1
1982
1983 def test__ge(self):
1984 amount1 = portfolio.Amount("BTD", 11.3)
1985 amount2 = portfolio.Amount("BTD", 13.1)
1986
1987 self.assertTrue(amount2 >= amount1)
1988 self.assertFalse(amount1 >= amount2)
1989 self.assertTrue(amount1 >= amount1)
1990
1991 amount3 = portfolio.Amount("BTC", 1.6)
1992 with self.assertRaises(Exception):
1993 amount3 >= amount1
1994
1995 def test__eq(self):
1996 amount1 = portfolio.Amount("BTD", 11.3)
1997 amount2 = portfolio.Amount("BTD", 13.1)
1998 amount3 = portfolio.Amount("BTD", 11.3)
1999
2000 self.assertFalse(amount1 == amount2)
2001 self.assertFalse(amount2 == amount1)
2002 self.assertTrue(amount1 == amount3)
2003 self.assertFalse(amount2 == 0)
2004
2005 amount4 = portfolio.Amount("BTC", 1.6)
2006 with self.assertRaises(Exception):
2007 amount1 == amount4
2008
2009 amount5 = portfolio.Amount("BTD", 0)
2010 self.assertTrue(amount5 == 0)
2011
2012 def test__ne(self):
2013 amount1 = portfolio.Amount("BTD", 11.3)
2014 amount2 = portfolio.Amount("BTD", 13.1)
2015 amount3 = portfolio.Amount("BTD", 11.3)
2016
2017 self.assertTrue(amount1 != amount2)
2018 self.assertTrue(amount2 != amount1)
2019 self.assertFalse(amount1 != amount3)
2020 self.assertTrue(amount2 != 0)
2021
2022 amount4 = portfolio.Amount("BTC", 1.6)
2023 with self.assertRaises(Exception):
2024 amount1 != amount4
2025
2026 amount5 = portfolio.Amount("BTD", 0)
2027 self.assertFalse(amount5 != 0)
2028
2029 def test__neg(self):
2030 amount1 = portfolio.Amount("BTD", "11.3")
2031
2032 self.assertEqual(portfolio.D("-11.3"), (-amount1).value)
2033
2034 def test__str(self):
2035 amount1 = portfolio.Amount("BTX", 32)
2036 self.assertEqual("32.00000000 BTX", str(amount1))
2037
2038 amount2 = portfolio.Amount("USDT", 12000)
2039 amount1.linked_to = amount2
2040 self.assertEqual("32.00000000 BTX [12000.00000000 USDT]", str(amount1))
2041
2042 def test__repr(self):
2043 amount1 = portfolio.Amount("BTX", 32)
2044 self.assertEqual("Amount(32.00000000 BTX)", repr(amount1))
2045
2046 amount2 = portfolio.Amount("USDT", 12000)
2047 amount1.linked_to = amount2
2048 self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT))", repr(amount1))
2049
2050 amount3 = portfolio.Amount("BTC", 0.1)
2051 amount2.linked_to = amount3
2052 self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1))
2053
2054 def test_as_json(self):
2055 amount = portfolio.Amount("BTX", 32)
2056 self.assertEqual({"currency": "BTX", "value": D("32")}, amount.as_json())
2057
2058 amount = portfolio.Amount("BTX", "1E-10")
2059 self.assertEqual({"currency": "BTX", "value": D("0")}, amount.as_json())
2060
2061 amount = portfolio.Amount("BTX", "1E-5")
2062 self.assertEqual({"currency": "BTX", "value": D("0.00001")}, amount.as_json())
2063 self.assertEqual("0.00001", str(amount.as_json()["value"]))
2064
2065