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