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