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