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