]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blob - test.py
Add Makefile and test coverage
[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
47 def tearDown(self):
48 for patcher in self.patchers:
49 patcher.stop()
50 self.wm.stop()
51 super(WebMockTestCase, self).tearDown()
52
53 @unittest.skipUnless("unit" in limits, "Unit skipped")
54 class PortfolioTest(WebMockTestCase):
55 def fill_data(self):
56 if self.json_response is not None:
57 portfolio.Portfolio.data = self.json_response
58
59 def setUp(self):
60 super(PortfolioTest, self).setUp()
61
62 with open("test_portfolio.json") as example:
63 self.json_response = example.read()
64
65 self.wm.get(portfolio.Portfolio.URL, text=self.json_response)
66
67 def test_get_cryptoportfolio(self):
68 self.wm.get(portfolio.Portfolio.URL, [
69 {"text":'{ "foo": "bar" }', "status_code": 200},
70 {"text": "System Error", "status_code": 500},
71 {"exc": requests.exceptions.ConnectTimeout},
72 ])
73 portfolio.Portfolio.get_cryptoportfolio()
74 self.assertIn("foo", portfolio.Portfolio.data)
75 self.assertEqual("bar", portfolio.Portfolio.data["foo"])
76 self.assertTrue(self.wm.called)
77 self.assertEqual(1, self.wm.call_count)
78
79 portfolio.Portfolio.get_cryptoportfolio()
80 self.assertIsNone(portfolio.Portfolio.data)
81 self.assertEqual(2, self.wm.call_count)
82
83 portfolio.Portfolio.data = "Foo"
84 portfolio.Portfolio.get_cryptoportfolio()
85 self.assertEqual("Foo", portfolio.Portfolio.data)
86 self.assertEqual(3, self.wm.call_count)
87
88 def test_parse_cryptoportfolio(self):
89 portfolio.Portfolio.parse_cryptoportfolio()
90
91 self.assertListEqual(
92 ["medium", "high"],
93 list(portfolio.Portfolio.liquidities.keys()))
94
95 liquidities = portfolio.Portfolio.liquidities
96 self.assertEqual(10, len(liquidities["medium"].keys()))
97 self.assertEqual(10, len(liquidities["high"].keys()))
98
99 expected = {
100 'BTC': (D("0.2857"), "long"),
101 'DGB': (D("0.1015"), "long"),
102 'DOGE': (D("0.1805"), "long"),
103 'SC': (D("0.0623"), "long"),
104 'ZEC': (D("0.3701"), "long"),
105 }
106 self.assertDictEqual(expected, liquidities["high"]['2018-01-08'])
107
108 expected = {
109 'BTC': (D("1.1102e-16"), "long"),
110 'ETC': (D("0.1"), "long"),
111 'FCT': (D("0.1"), "long"),
112 'GAS': (D("0.1"), "long"),
113 'NAV': (D("0.1"), "long"),
114 'OMG': (D("0.1"), "long"),
115 'OMNI': (D("0.1"), "long"),
116 'PPC': (D("0.1"), "long"),
117 'RIC': (D("0.1"), "long"),
118 'VIA': (D("0.1"), "long"),
119 'XCP': (D("0.1"), "long"),
120 }
121 self.assertDictEqual(expected, liquidities["medium"]['2018-01-08'])
122
123 # It doesn't refetch the data when available
124 portfolio.Portfolio.parse_cryptoportfolio()
125
126 self.assertEqual(1, self.wm.call_count)
127
128 def test_repartition(self):
129 expected_medium = {
130 'BTC': (D("1.1102e-16"), "long"),
131 'USDT': (D("0.1"), "long"),
132 'ETC': (D("0.1"), "long"),
133 'FCT': (D("0.1"), "long"),
134 'OMG': (D("0.1"), "long"),
135 'STEEM': (D("0.1"), "long"),
136 'STRAT': (D("0.1"), "long"),
137 'XEM': (D("0.1"), "long"),
138 'XMR': (D("0.1"), "long"),
139 'XVC': (D("0.1"), "long"),
140 'ZRX': (D("0.1"), "long"),
141 }
142 expected_high = {
143 'USDT': (D("0.1226"), "long"),
144 'BTC': (D("0.1429"), "long"),
145 'ETC': (D("0.1127"), "long"),
146 'ETH': (D("0.1569"), "long"),
147 'FCT': (D("0.3341"), "long"),
148 'GAS': (D("0.1308"), "long"),
149 }
150
151 self.assertEqual(expected_medium, portfolio.Portfolio.repartition())
152 self.assertEqual(expected_medium, portfolio.Portfolio.repartition(liquidity="medium"))
153 self.assertEqual(expected_high, portfolio.Portfolio.repartition(liquidity="high"))
154
155 @unittest.skipUnless("unit" in limits, "Unit skipped")
156 class AmountTest(WebMockTestCase):
157 def test_values(self):
158 amount = portfolio.Amount("BTC", "0.65")
159 self.assertEqual(D("0.65"), amount.value)
160 self.assertEqual("BTC", amount.currency)
161
162 def test_in_currency(self):
163 amount = portfolio.Amount("ETC", 10)
164
165 self.assertEqual(amount, amount.in_currency("ETC", None))
166
167 ticker_mock = unittest.mock.Mock()
168 with mock.patch.object(helper, 'get_ticker', new=ticker_mock):
169 ticker_mock.return_value = None
170
171 self.assertRaises(Exception, amount.in_currency, "ETH", None)
172
173 with mock.patch.object(helper, 'get_ticker', new=ticker_mock):
174 ticker_mock.return_value = {
175 "bid": D("0.2"),
176 "ask": D("0.4"),
177 "average": D("0.3"),
178 "foo": "bar",
179 }
180 converted_amount = amount.in_currency("ETH", None)
181
182 self.assertEqual(D("3.0"), converted_amount.value)
183 self.assertEqual("ETH", converted_amount.currency)
184 self.assertEqual(amount, converted_amount.linked_to)
185 self.assertEqual("bar", converted_amount.ticker["foo"])
186
187 converted_amount = amount.in_currency("ETH", None, action="bid", compute_value="default")
188 self.assertEqual(D("2"), converted_amount.value)
189
190 converted_amount = amount.in_currency("ETH", None, compute_value="ask")
191 self.assertEqual(D("4"), converted_amount.value)
192
193 converted_amount = amount.in_currency("ETH", None, rate=D("0.02"))
194 self.assertEqual(D("0.2"), converted_amount.value)
195
196 def test__round(self):
197 amount = portfolio.Amount("BAR", portfolio.D("1.23456789876"))
198 self.assertEqual(D("1.23456789"), round(amount).value)
199 self.assertEqual(D("1.23"), round(amount, 2).value)
200
201 def test__abs(self):
202 amount = portfolio.Amount("SC", -120)
203 self.assertEqual(120, abs(amount).value)
204 self.assertEqual("SC", abs(amount).currency)
205
206 amount = portfolio.Amount("SC", 10)
207 self.assertEqual(10, abs(amount).value)
208 self.assertEqual("SC", abs(amount).currency)
209
210 def test__add(self):
211 amount1 = portfolio.Amount("XVG", "12.9")
212 amount2 = portfolio.Amount("XVG", "13.1")
213
214 self.assertEqual(26, (amount1 + amount2).value)
215 self.assertEqual("XVG", (amount1 + amount2).currency)
216
217 amount3 = portfolio.Amount("ETH", "1.6")
218 with self.assertRaises(Exception):
219 amount1 + amount3
220
221 amount4 = portfolio.Amount("ETH", 0.0)
222 self.assertEqual(amount1, amount1 + amount4)
223
224 def test__radd(self):
225 amount = portfolio.Amount("XVG", "12.9")
226
227 self.assertEqual(amount, 0 + amount)
228 with self.assertRaises(Exception):
229 4 + amount
230
231 def test__sub(self):
232 amount1 = portfolio.Amount("XVG", "13.3")
233 amount2 = portfolio.Amount("XVG", "13.1")
234
235 self.assertEqual(D("0.2"), (amount1 - amount2).value)
236 self.assertEqual("XVG", (amount1 - amount2).currency)
237
238 amount3 = portfolio.Amount("ETH", "1.6")
239 with self.assertRaises(Exception):
240 amount1 - amount3
241
242 amount4 = portfolio.Amount("ETH", 0.0)
243 self.assertEqual(amount1, amount1 - amount4)
244
245 def test__mul(self):
246 amount = portfolio.Amount("XEM", 11)
247
248 self.assertEqual(D("38.5"), (amount * D("3.5")).value)
249 self.assertEqual(D("33"), (amount * 3).value)
250
251 with self.assertRaises(Exception):
252 amount * amount
253
254 def test__rmul(self):
255 amount = portfolio.Amount("XEM", 11)
256
257 self.assertEqual(D("38.5"), (D("3.5") * amount).value)
258 self.assertEqual(D("33"), (3 * amount).value)
259
260 def test__floordiv(self):
261 amount = portfolio.Amount("XEM", 11)
262
263 self.assertEqual(D("5.5"), (amount / 2).value)
264 self.assertEqual(D("4.4"), (amount / D("2.5")).value)
265
266 with self.assertRaises(Exception):
267 amount / amount
268
269 def test__truediv(self):
270 amount = portfolio.Amount("XEM", 11)
271
272 self.assertEqual(D("5.5"), (amount / 2).value)
273 self.assertEqual(D("4.4"), (amount / D("2.5")).value)
274
275 def test__lt(self):
276 amount1 = portfolio.Amount("BTD", 11.3)
277 amount2 = portfolio.Amount("BTD", 13.1)
278
279 self.assertTrue(amount1 < amount2)
280 self.assertFalse(amount2 < amount1)
281 self.assertFalse(amount1 < amount1)
282
283 amount3 = portfolio.Amount("BTC", 1.6)
284 with self.assertRaises(Exception):
285 amount1 < amount3
286
287 def test__le(self):
288 amount1 = portfolio.Amount("BTD", 11.3)
289 amount2 = portfolio.Amount("BTD", 13.1)
290
291 self.assertTrue(amount1 <= amount2)
292 self.assertFalse(amount2 <= amount1)
293 self.assertTrue(amount1 <= amount1)
294
295 amount3 = portfolio.Amount("BTC", 1.6)
296 with self.assertRaises(Exception):
297 amount1 <= amount3
298
299 def test__gt(self):
300 amount1 = portfolio.Amount("BTD", 11.3)
301 amount2 = portfolio.Amount("BTD", 13.1)
302
303 self.assertTrue(amount2 > amount1)
304 self.assertFalse(amount1 > amount2)
305 self.assertFalse(amount1 > amount1)
306
307 amount3 = portfolio.Amount("BTC", 1.6)
308 with self.assertRaises(Exception):
309 amount3 > amount1
310
311 def test__ge(self):
312 amount1 = portfolio.Amount("BTD", 11.3)
313 amount2 = portfolio.Amount("BTD", 13.1)
314
315 self.assertTrue(amount2 >= amount1)
316 self.assertFalse(amount1 >= amount2)
317 self.assertTrue(amount1 >= amount1)
318
319 amount3 = portfolio.Amount("BTC", 1.6)
320 with self.assertRaises(Exception):
321 amount3 >= amount1
322
323 def test__eq(self):
324 amount1 = portfolio.Amount("BTD", 11.3)
325 amount2 = portfolio.Amount("BTD", 13.1)
326 amount3 = portfolio.Amount("BTD", 11.3)
327
328 self.assertFalse(amount1 == amount2)
329 self.assertFalse(amount2 == amount1)
330 self.assertTrue(amount1 == amount3)
331 self.assertFalse(amount2 == 0)
332
333 amount4 = portfolio.Amount("BTC", 1.6)
334 with self.assertRaises(Exception):
335 amount1 == amount4
336
337 amount5 = portfolio.Amount("BTD", 0)
338 self.assertTrue(amount5 == 0)
339
340 def test__ne(self):
341 amount1 = portfolio.Amount("BTD", 11.3)
342 amount2 = portfolio.Amount("BTD", 13.1)
343 amount3 = portfolio.Amount("BTD", 11.3)
344
345 self.assertTrue(amount1 != amount2)
346 self.assertTrue(amount2 != amount1)
347 self.assertFalse(amount1 != amount3)
348 self.assertTrue(amount2 != 0)
349
350 amount4 = portfolio.Amount("BTC", 1.6)
351 with self.assertRaises(Exception):
352 amount1 != amount4
353
354 amount5 = portfolio.Amount("BTD", 0)
355 self.assertFalse(amount5 != 0)
356
357 def test__neg(self):
358 amount1 = portfolio.Amount("BTD", "11.3")
359
360 self.assertEqual(portfolio.D("-11.3"), (-amount1).value)
361
362 def test__str(self):
363 amount1 = portfolio.Amount("BTX", 32)
364 self.assertEqual("32.00000000 BTX", str(amount1))
365
366 amount2 = portfolio.Amount("USDT", 12000)
367 amount1.linked_to = amount2
368 self.assertEqual("32.00000000 BTX [12000.00000000 USDT]", str(amount1))
369
370 def test__repr(self):
371 amount1 = portfolio.Amount("BTX", 32)
372 self.assertEqual("Amount(32.00000000 BTX)", repr(amount1))
373
374 amount2 = portfolio.Amount("USDT", 12000)
375 amount1.linked_to = amount2
376 self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT))", repr(amount1))
377
378 amount3 = portfolio.Amount("BTC", 0.1)
379 amount2.linked_to = amount3
380 self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1))
381
382 @unittest.skipUnless("unit" in limits, "Unit skipped")
383 class BalanceTest(WebMockTestCase):
384 def test_values(self):
385 balance = portfolio.Balance("BTC", {
386 "exchange_total": "0.65",
387 "exchange_free": "0.35",
388 "exchange_used": "0.30",
389 "margin_total": "-10",
390 "margin_borrowed": "-10",
391 "margin_free": "0",
392 "margin_position_type": "short",
393 "margin_borrowed_base_currency": "USDT",
394 "margin_liquidation_price": "1.20",
395 "margin_pending_gain": "10",
396 "margin_lending_fees": "0.4",
397 "margin_borrowed_base_price": "0.15",
398 })
399 self.assertEqual(portfolio.D("0.65"), balance.exchange_total.value)
400 self.assertEqual(portfolio.D("0.35"), balance.exchange_free.value)
401 self.assertEqual(portfolio.D("0.30"), balance.exchange_used.value)
402 self.assertEqual("BTC", balance.exchange_total.currency)
403 self.assertEqual("BTC", balance.exchange_free.currency)
404 self.assertEqual("BTC", balance.exchange_total.currency)
405
406 self.assertEqual(portfolio.D("-10"), balance.margin_total.value)
407 self.assertEqual(portfolio.D("-10"), balance.margin_borrowed.value)
408 self.assertEqual(portfolio.D("0"), balance.margin_free.value)
409 self.assertEqual("BTC", balance.margin_total.currency)
410 self.assertEqual("BTC", balance.margin_borrowed.currency)
411 self.assertEqual("BTC", balance.margin_free.currency)
412
413 self.assertEqual("BTC", balance.currency)
414
415 self.assertEqual(portfolio.D("0.4"), balance.margin_lending_fees.value)
416 self.assertEqual("USDT", balance.margin_lending_fees.currency)
417
418 def test__repr(self):
419 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX])",
420 repr(portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 })))
421 balance = portfolio.Balance("BTX", { "exchange_total": 3,
422 "exchange_used": 1, "exchange_free": 2 })
423 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance))
424
425 balance = portfolio.Balance("BTX", { "exchange_total": 1, "exchange_used": 1})
426 self.assertEqual("Balance(BTX Exch: [❌1.00000000 BTX])", repr(balance))
427
428 balance = portfolio.Balance("BTX", { "margin_total": 3,
429 "margin_borrowed": 1, "margin_free": 2 })
430 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + borrowed 1.00000000 BTX = 3.00000000 BTX])", repr(balance))
431
432 balance = portfolio.Balance("BTX", { "margin_total": 2, "margin_free": 2 })
433 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX])", repr(balance))
434
435 balance = portfolio.Balance("BTX", { "margin_total": -3,
436 "margin_borrowed_base_price": D("0.1"),
437 "margin_borrowed_base_currency": "BTC",
438 "margin_lending_fees": D("0.002") })
439 self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance))
440
441 balance = portfolio.Balance("BTX", { "margin_total": 1,
442 "margin_borrowed": 1, "exchange_free": 2, "exchange_total": 2})
443 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [borrowed 1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance))
444
445 @unittest.skipUnless("unit" in limits, "Unit skipped")
446 class HelperTest(WebMockTestCase):
447 def test_get_ticker(self):
448 market = mock.Mock()
449 market.fetch_ticker.side_effect = [
450 { "bid": 1, "ask": 3 },
451 helper.ExchangeError("foo"),
452 { "bid": 10, "ask": 40 },
453 helper.ExchangeError("foo"),
454 helper.ExchangeError("foo"),
455 ]
456
457 ticker = helper.get_ticker("ETH", "ETC", market)
458 market.fetch_ticker.assert_called_with("ETH/ETC")
459 self.assertEqual(1, ticker["bid"])
460 self.assertEqual(3, ticker["ask"])
461 self.assertEqual(2, ticker["average"])
462 self.assertFalse(ticker["inverted"])
463
464 ticker = helper.get_ticker("ETH", "XVG", market)
465 self.assertEqual(0.0625, ticker["average"])
466 self.assertTrue(ticker["inverted"])
467 self.assertIn("original", ticker)
468 self.assertEqual(10, ticker["original"]["bid"])
469
470 ticker = helper.get_ticker("XVG", "XMR", market)
471 self.assertIsNone(ticker)
472
473 market.fetch_ticker.assert_has_calls([
474 mock.call("ETH/ETC"),
475 mock.call("ETH/XVG"),
476 mock.call("XVG/ETH"),
477 mock.call("XVG/XMR"),
478 mock.call("XMR/XVG"),
479 ])
480
481 market2 = mock.Mock()
482 market2.fetch_ticker.side_effect = [
483 { "bid": 1, "ask": 3 },
484 { "bid": 1.2, "ask": 3.5 },
485 ]
486 ticker1 = helper.get_ticker("ETH", "ETC", market2)
487 ticker2 = helper.get_ticker("ETH", "ETC", market2)
488 ticker3 = helper.get_ticker("ETC", "ETH", market2)
489 market2.fetch_ticker.assert_called_once_with("ETH/ETC")
490 self.assertEqual(1, ticker1["bid"])
491 self.assertDictEqual(ticker1, ticker2)
492 self.assertDictEqual(ticker1, ticker3["original"])
493
494 ticker4 = helper.get_ticker("ETH", "ETC", market2, refresh=True)
495 ticker5 = helper.get_ticker("ETH", "ETC", market2)
496 self.assertEqual(1.2, ticker4["bid"])
497 self.assertDictEqual(ticker4, ticker5)
498
499 market3 = mock.Mock()
500 market3.fetch_ticker.side_effect = [
501 { "bid": 1, "ask": 3 },
502 { "bid": 1.2, "ask": 3.5 },
503 ]
504 ticker6 = helper.get_ticker("ETH", "ETC", market3)
505 helper.ticker_cache_timestamp -= 4
506 ticker7 = helper.get_ticker("ETH", "ETC", market3)
507 helper.ticker_cache_timestamp -= 2
508 ticker8 = helper.get_ticker("ETH", "ETC", market3)
509 self.assertDictEqual(ticker6, ticker7)
510 self.assertEqual(1.2, ticker8["bid"])
511
512 def test_fetch_fees(self):
513 market = mock.Mock()
514 market.fetch_fees.return_value = "Foo"
515 self.assertEqual("Foo", helper.fetch_fees(market))
516 market.fetch_fees.assert_called_once()
517 self.assertEqual("Foo", helper.fetch_fees(market))
518 market.fetch_fees.assert_called_once()
519
520 @mock.patch.object(portfolio.Portfolio, "repartition")
521 @mock.patch.object(helper, "get_ticker")
522 @mock.patch.object(portfolio.TradeStore, "compute_trades")
523 def test_prepare_trades(self, compute_trades, get_ticker, repartition):
524 repartition.return_value = {
525 "XEM": (D("0.75"), "long"),
526 "BTC": (D("0.25"), "long"),
527 }
528 def _get_ticker(c1, c2, market):
529 if c1 == "USDT" and c2 == "BTC":
530 return { "average": D("0.0001") }
531 if c1 == "XVG" and c2 == "BTC":
532 return { "average": D("0.000001") }
533 if c1 == "XEM" and c2 == "BTC":
534 return { "average": D("0.001") }
535 self.fail("Should be called with {}, {}".format(c1, c2))
536 get_ticker.side_effect = _get_ticker
537
538 market = mock.Mock()
539 market.fetch_all_balances.return_value = {
540 "USDT": {
541 "exchange_free": D("10000.0"),
542 "exchange_used": D("0.0"),
543 "exchange_total": D("10000.0"),
544 "total": D("10000.0")
545 },
546 "XVG": {
547 "exchange_free": D("10000.0"),
548 "exchange_used": D("0.0"),
549 "exchange_total": D("10000.0"),
550 "total": D("10000.0")
551 },
552 }
553 helper.prepare_trades(market)
554 compute_trades.assert_called()
555
556 call = compute_trades.call_args
557 self.assertEqual(market, call[1]["market"])
558 self.assertEqual(1, call[0][0]["USDT"].value)
559 self.assertEqual(D("0.01"), call[0][0]["XVG"].value)
560 self.assertEqual(D("0.2525"), call[0][1]["BTC"].value)
561 self.assertEqual(D("0.7575"), call[0][1]["XEM"].value)
562
563 @mock.patch.object(portfolio.Portfolio, "repartition")
564 @mock.patch.object(helper, "get_ticker")
565 @mock.patch.object(portfolio.TradeStore, "compute_trades")
566 def test_update_trades(self, compute_trades, get_ticker, repartition):
567 repartition.return_value = {
568 "XEM": (D("0.75"), "long"),
569 "BTC": (D("0.25"), "long"),
570 }
571 def _get_ticker(c1, c2, market):
572 if c1 == "USDT" and c2 == "BTC":
573 return { "average": D("0.0001") }
574 if c1 == "XVG" and c2 == "BTC":
575 return { "average": D("0.000001") }
576 if c1 == "XEM" and c2 == "BTC":
577 return { "average": D("0.001") }
578 self.fail("Should be called with {}, {}".format(c1, c2))
579 get_ticker.side_effect = _get_ticker
580
581 market = mock.Mock()
582 market.fetch_all_balances.return_value = {
583 "USDT": {
584 "exchange_free": D("10000.0"),
585 "exchange_used": D("0.0"),
586 "exchange_total": D("10000.0"),
587 "total": D("10000.0")
588 },
589 "XVG": {
590 "exchange_free": D("10000.0"),
591 "exchange_used": D("0.0"),
592 "exchange_total": D("10000.0"),
593 "total": D("10000.0")
594 },
595 }
596 helper.update_trades(market)
597 compute_trades.assert_called()
598
599 call = compute_trades.call_args
600 self.assertEqual(market, call[1]["market"])
601 self.assertEqual(1, call[0][0]["USDT"].value)
602 self.assertEqual(D("0.01"), call[0][0]["XVG"].value)
603 self.assertEqual(D("0.2525"), call[0][1]["BTC"].value)
604 self.assertEqual(D("0.7575"), call[0][1]["XEM"].value)
605
606 @mock.patch.object(portfolio.Portfolio, "repartition")
607 @mock.patch.object(helper, "get_ticker")
608 @mock.patch.object(portfolio.TradeStore, "compute_trades")
609 def test_prepare_trades_to_sell_all(self, compute_trades, get_ticker, repartition):
610 def _get_ticker(c1, c2, market):
611 if c1 == "USDT" and c2 == "BTC":
612 return { "average": D("0.0001") }
613 if c1 == "XVG" and c2 == "BTC":
614 return { "average": D("0.000001") }
615 self.fail("Should be called with {}, {}".format(c1, c2))
616 get_ticker.side_effect = _get_ticker
617
618 market = mock.Mock()
619 market.fetch_all_balances.return_value = {
620 "USDT": {
621 "exchange_free": D("10000.0"),
622 "exchange_used": D("0.0"),
623 "exchange_total": D("10000.0"),
624 "total": D("10000.0")
625 },
626 "XVG": {
627 "exchange_free": D("10000.0"),
628 "exchange_used": D("0.0"),
629 "exchange_total": D("10000.0"),
630 "total": D("10000.0")
631 },
632 }
633 helper.prepare_trades_to_sell_all(market)
634 repartition.assert_not_called()
635 compute_trades.assert_called()
636
637 call = compute_trades.call_args
638 self.assertEqual(market, call[1]["market"])
639 self.assertEqual(1, call[0][0]["USDT"].value)
640 self.assertEqual(D("0.01"), call[0][0]["XVG"].value)
641 self.assertEqual(D("1.01"), call[0][1]["BTC"].value)
642
643 @mock.patch.object(portfolio.time, "sleep")
644 @mock.patch.object(portfolio.TradeStore, "all_orders")
645 def test_follow_orders(self, all_orders, time_mock):
646 for verbose, debug, sleep in [
647 (True, False, None), (False, False, None),
648 (True, True, None), (True, False, 12),
649 (True, True, 12)]:
650 with self.subTest(sleep=sleep, debug=debug, verbose=verbose), \
651 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
652 portfolio.TradeStore.debug = debug
653 order_mock1 = mock.Mock()
654 order_mock2 = mock.Mock()
655 order_mock3 = mock.Mock()
656 all_orders.side_effect = [
657 [order_mock1, order_mock2],
658 [order_mock1, order_mock2],
659
660 [order_mock1, order_mock3],
661 [order_mock1, order_mock3],
662
663 [order_mock1, order_mock3],
664 [order_mock1, order_mock3],
665
666 []
667 ]
668
669 order_mock1.get_status.side_effect = ["open", "open", "closed"]
670 order_mock2.get_status.side_effect = ["open"]
671 order_mock3.get_status.side_effect = ["open", "closed"]
672
673 order_mock1.trade = mock.Mock()
674 order_mock2.trade = mock.Mock()
675 order_mock3.trade = mock.Mock()
676
677 helper.follow_orders(verbose=verbose, sleep=sleep)
678
679 order_mock1.trade.update_order.assert_any_call(order_mock1, 1)
680 order_mock1.trade.update_order.assert_any_call(order_mock1, 2)
681 self.assertEqual(2, order_mock1.trade.update_order.call_count)
682 self.assertEqual(3, order_mock1.get_status.call_count)
683
684 order_mock2.trade.update_order.assert_any_call(order_mock2, 1)
685 self.assertEqual(1, order_mock2.trade.update_order.call_count)
686 self.assertEqual(1, order_mock2.get_status.call_count)
687
688 order_mock3.trade.update_order.assert_any_call(order_mock3, 2)
689 self.assertEqual(1, order_mock3.trade.update_order.call_count)
690 self.assertEqual(2, order_mock3.get_status.call_count)
691
692 if sleep is None:
693 if debug:
694 time_mock.assert_called_with(7)
695 else:
696 time_mock.assert_called_with(30)
697 else:
698 time_mock.assert_called_with(sleep)
699
700 if verbose:
701 self.assertNotEqual("", stdout_mock.getvalue())
702 else:
703 self.assertEqual("", stdout_mock.getvalue())
704
705 @unittest.skipUnless("unit" in limits, "Unit skipped")
706 class TradeStoreTest(WebMockTestCase):
707 @mock.patch.object(portfolio.BalanceStore, "currencies")
708 @mock.patch.object(portfolio.TradeStore, "add_trade_if_matching")
709 def test_compute_trades(self, add_trade_if_matching, currencies):
710 currencies.return_value = ["XMR", "DASH", "XVG", "BTC", "ETH"]
711
712 values_in_base = {
713 "XMR": portfolio.Amount("BTC", D("0.9")),
714 "DASH": portfolio.Amount("BTC", D("0.4")),
715 "XVG": portfolio.Amount("BTC", D("-0.5")),
716 "BTC": portfolio.Amount("BTC", D("0.5")),
717 }
718 new_repartition = {
719 "DASH": portfolio.Amount("BTC", D("0.5")),
720 "XVG": portfolio.Amount("BTC", D("0.1")),
721 "BTC": portfolio.Amount("BTC", D("0.4")),
722 "ETH": portfolio.Amount("BTC", D("0.3")),
723 }
724
725 portfolio.TradeStore.compute_trades(values_in_base,
726 new_repartition, only="only", market="market")
727
728 self.assertEqual(5, add_trade_if_matching.call_count)
729 add_trade_if_matching.assert_any_call(
730 portfolio.Amount("BTC", D("0.9")),
731 portfolio.Amount("BTC", 0),
732 "XMR", only="only", market="market"
733 )
734 add_trade_if_matching.assert_any_call(
735 portfolio.Amount("BTC", D("0.4")),
736 portfolio.Amount("BTC", D("0.5")),
737 "DASH", only="only", market="market"
738 )
739 add_trade_if_matching.assert_any_call(
740 portfolio.Amount("BTC", D("-0.5")),
741 portfolio.Amount("BTC", D("0")),
742 "XVG", only="only", market="market"
743 )
744 add_trade_if_matching.assert_any_call(
745 portfolio.Amount("BTC", D("0")),
746 portfolio.Amount("BTC", D("0.1")),
747 "XVG", only="only", market="market"
748 )
749 add_trade_if_matching.assert_any_call(
750 portfolio.Amount("BTC", D("0")),
751 portfolio.Amount("BTC", D("0.3")),
752 "ETH", only="only", market="market"
753 )
754
755 def test_add_trade_if_matching(self):
756 result = portfolio.TradeStore.add_trade_if_matching(
757 portfolio.Amount("BTC", D("0")),
758 portfolio.Amount("BTC", D("0.3")),
759 "ETH", only="nope", market="market"
760 )
761 self.assertEqual(0, len(portfolio.TradeStore.all))
762 self.assertEqual(False, result)
763
764 portfolio.TradeStore.all = []
765 result = portfolio.TradeStore.add_trade_if_matching(
766 portfolio.Amount("BTC", D("0")),
767 portfolio.Amount("BTC", D("0.3")),
768 "ETH", only=None, market="market"
769 )
770 self.assertEqual(1, len(portfolio.TradeStore.all))
771 self.assertEqual(True, result)
772
773 portfolio.TradeStore.all = []
774 result = portfolio.TradeStore.add_trade_if_matching(
775 portfolio.Amount("BTC", D("0")),
776 portfolio.Amount("BTC", D("0.3")),
777 "ETH", only="acquire", market="market"
778 )
779 self.assertEqual(1, len(portfolio.TradeStore.all))
780 self.assertEqual(True, result)
781
782 portfolio.TradeStore.all = []
783 result = portfolio.TradeStore.add_trade_if_matching(
784 portfolio.Amount("BTC", D("0")),
785 portfolio.Amount("BTC", D("0.3")),
786 "ETH", only="dispose", market="market"
787 )
788 self.assertEqual(0, len(portfolio.TradeStore.all))
789 self.assertEqual(False, result)
790
791 def test_prepare_orders(self):
792 trade_mock1 = mock.Mock()
793 trade_mock2 = mock.Mock()
794
795 portfolio.TradeStore.all.append(trade_mock1)
796 portfolio.TradeStore.all.append(trade_mock2)
797
798 portfolio.TradeStore.prepare_orders()
799 trade_mock1.prepare_order.assert_called_with(compute_value="default")
800 trade_mock2.prepare_order.assert_called_with(compute_value="default")
801
802 portfolio.TradeStore.prepare_orders(compute_value="bla")
803 trade_mock1.prepare_order.assert_called_with(compute_value="bla")
804 trade_mock2.prepare_order.assert_called_with(compute_value="bla")
805
806 trade_mock1.prepare_order.reset_mock()
807 trade_mock2.prepare_order.reset_mock()
808
809 trade_mock1.action = "foo"
810 trade_mock2.action = "bar"
811 portfolio.TradeStore.prepare_orders(only="bar")
812 trade_mock1.prepare_order.assert_not_called()
813 trade_mock2.prepare_order.assert_called_with(compute_value="default")
814
815 def test_print_all_with_order(self):
816 trade_mock1 = mock.Mock()
817 trade_mock2 = mock.Mock()
818 trade_mock3 = mock.Mock()
819 portfolio.TradeStore.all = [trade_mock1, trade_mock2, trade_mock3]
820
821 portfolio.TradeStore.print_all_with_order()
822
823 trade_mock1.print_with_order.assert_called()
824 trade_mock2.print_with_order.assert_called()
825 trade_mock3.print_with_order.assert_called()
826
827 @mock.patch.object(portfolio.TradeStore, "all_orders")
828 def test_run_orders(self, all_orders):
829 order_mock1 = mock.Mock()
830 order_mock2 = mock.Mock()
831 order_mock3 = mock.Mock()
832 all_orders.return_value = [order_mock1, order_mock2, order_mock3]
833 portfolio.TradeStore.run_orders()
834 all_orders.assert_called_with(state="pending")
835
836 order_mock1.run.assert_called()
837 order_mock2.run.assert_called()
838 order_mock3.run.assert_called()
839
840 def test_all_orders(self):
841 trade_mock1 = mock.Mock()
842 trade_mock2 = mock.Mock()
843
844 order_mock1 = mock.Mock()
845 order_mock2 = mock.Mock()
846 order_mock3 = mock.Mock()
847
848 trade_mock1.orders = [order_mock1, order_mock2]
849 trade_mock2.orders = [order_mock3]
850
851 order_mock1.status = "pending"
852 order_mock2.status = "open"
853 order_mock3.status = "open"
854
855 portfolio.TradeStore.all.append(trade_mock1)
856 portfolio.TradeStore.all.append(trade_mock2)
857
858 orders = portfolio.TradeStore.all_orders()
859 self.assertEqual(3, len(orders))
860
861 open_orders = portfolio.TradeStore.all_orders(state="open")
862 self.assertEqual(2, len(open_orders))
863 self.assertEqual([order_mock2, order_mock3], open_orders)
864
865 @mock.patch.object(portfolio.TradeStore, "all_orders")
866 def test_update_all_orders_status(self, all_orders):
867 order_mock1 = mock.Mock()
868 order_mock2 = mock.Mock()
869 order_mock3 = mock.Mock()
870 all_orders.return_value = [order_mock1, order_mock2, order_mock3]
871 portfolio.TradeStore.update_all_orders_status()
872 all_orders.assert_called_with(state="open")
873
874 order_mock1.get_status.assert_called()
875 order_mock2.get_status.assert_called()
876 order_mock3.get_status.assert_called()
877
878 @unittest.skipUnless("unit" in limits, "Unit skipped")
879 class BalanceStoreTest(WebMockTestCase):
880 def setUp(self):
881 super(BalanceStoreTest, self).setUp()
882
883 self.fetch_balance = {
884 "ETC": {
885 "exchange_free": 0,
886 "exchange_used": 0,
887 "exchange_total": 0,
888 "margin_total": 0,
889 },
890 "USDT": {
891 "exchange_free": D("6.0"),
892 "exchange_used": D("1.2"),
893 "exchange_total": D("7.2"),
894 "margin_total": 0,
895 },
896 "XVG": {
897 "exchange_free": 16,
898 "exchange_used": 0,
899 "exchange_total": 16,
900 "margin_total": 0,
901 },
902 "XMR": {
903 "exchange_free": 0,
904 "exchange_used": 0,
905 "exchange_total": 0,
906 "margin_total": D("-1.0"),
907 "margin_free": 0,
908 },
909 }
910
911 @mock.patch.object(helper, "get_ticker")
912 def test_in_currency(self, get_ticker):
913 portfolio.BalanceStore.all = {
914 "BTC": portfolio.Balance("BTC", {
915 "total": "0.65",
916 "exchange_total":"0.65",
917 "exchange_free": "0.35",
918 "exchange_used": "0.30"}),
919 "ETH": portfolio.Balance("ETH", {
920 "total": 3,
921 "exchange_total": 3,
922 "exchange_free": 3,
923 "exchange_used": 0}),
924 }
925 market = mock.Mock()
926 get_ticker.return_value = {
927 "bid": D("0.09"),
928 "ask": D("0.11"),
929 "average": D("0.1"),
930 }
931
932 amounts = portfolio.BalanceStore.in_currency("BTC", market)
933 self.assertEqual("BTC", amounts["ETH"].currency)
934 self.assertEqual(D("0.65"), amounts["BTC"].value)
935 self.assertEqual(D("0.30"), amounts["ETH"].value)
936
937 amounts = portfolio.BalanceStore.in_currency("BTC", market, compute_value="bid")
938 self.assertEqual(D("0.65"), amounts["BTC"].value)
939 self.assertEqual(D("0.27"), amounts["ETH"].value)
940
941 amounts = portfolio.BalanceStore.in_currency("BTC", market, compute_value="bid", type="exchange_used")
942 self.assertEqual(D("0.30"), amounts["BTC"].value)
943 self.assertEqual(0, amounts["ETH"].value)
944
945 def test_fetch_balances(self):
946 market = mock.Mock()
947 market.fetch_all_balances.return_value = self.fetch_balance
948
949 portfolio.BalanceStore.fetch_balances(market)
950 self.assertNotIn("ETC", portfolio.BalanceStore.currencies())
951 self.assertListEqual(["USDT", "XVG", "XMR"], list(portfolio.BalanceStore.currencies()))
952
953 portfolio.BalanceStore.all["ETC"] = portfolio.Balance("ETC", {
954 "exchange_total": "1", "exchange_free": "0",
955 "exchange_used": "1" })
956 portfolio.BalanceStore.fetch_balances(market)
957 self.assertEqual(0, portfolio.BalanceStore.all["ETC"].total)
958 self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(portfolio.BalanceStore.currencies()))
959
960 @mock.patch.object(portfolio.Portfolio, "repartition")
961 def test_dispatch_assets(self, repartition):
962 market = mock.Mock()
963 market.fetch_all_balances.return_value = self.fetch_balance
964 portfolio.BalanceStore.fetch_balances(market)
965
966 self.assertNotIn("XEM", portfolio.BalanceStore.currencies())
967
968 repartition.return_value = {
969 "XEM": (D("0.75"), "long"),
970 "BTC": (D("0.26"), "long"),
971 "DASH": (D("0.10"), "short"),
972 }
973
974 amounts = portfolio.BalanceStore.dispatch_assets(portfolio.Amount("BTC", "11.1"))
975 self.assertIn("XEM", portfolio.BalanceStore.currencies())
976 self.assertEqual(D("2.6"), amounts["BTC"].value)
977 self.assertEqual(D("7.5"), amounts["XEM"].value)
978 self.assertEqual(D("-1.0"), amounts["DASH"].value)
979
980 def test_currencies(self):
981 portfolio.BalanceStore.all = {
982 "BTC": portfolio.Balance("BTC", {
983 "total": "0.65",
984 "exchange_total":"0.65",
985 "exchange_free": "0.35",
986 "exchange_used": "0.30"}),
987 "ETH": portfolio.Balance("ETH", {
988 "total": 3,
989 "exchange_total": 3,
990 "exchange_free": 3,
991 "exchange_used": 0}),
992 }
993 self.assertListEqual(["BTC", "ETH"], list(portfolio.BalanceStore.currencies()))
994
995 @unittest.skipUnless("unit" in limits, "Unit skipped")
996 class ComputationTest(WebMockTestCase):
997 def test_compute_value(self):
998 compute = mock.Mock()
999 portfolio.Computation.compute_value("foo", "buy", compute_value=compute)
1000 compute.assert_called_with("foo", "ask")
1001
1002 compute.reset_mock()
1003 portfolio.Computation.compute_value("foo", "sell", compute_value=compute)
1004 compute.assert_called_with("foo", "bid")
1005
1006 compute.reset_mock()
1007 portfolio.Computation.compute_value("foo", "ask", compute_value=compute)
1008 compute.assert_called_with("foo", "ask")
1009
1010 compute.reset_mock()
1011 portfolio.Computation.compute_value("foo", "bid", compute_value=compute)
1012 compute.assert_called_with("foo", "bid")
1013
1014 compute.reset_mock()
1015 portfolio.Computation.computations["test"] = compute
1016 portfolio.Computation.compute_value("foo", "bid", compute_value="test")
1017 compute.assert_called_with("foo", "bid")
1018
1019
1020 @unittest.skipUnless("unit" in limits, "Unit skipped")
1021 class TradeTest(WebMockTestCase):
1022
1023 def test_values_assertion(self):
1024 value_from = portfolio.Amount("BTC", "1.0")
1025 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1026 value_to = portfolio.Amount("BTC", "1.0")
1027 trade = portfolio.Trade(value_from, value_to, "ETH")
1028 self.assertEqual("BTC", trade.base_currency)
1029 self.assertEqual("ETH", trade.currency)
1030
1031 with self.assertRaises(AssertionError):
1032 portfolio.Trade(value_from, value_to, "ETC")
1033 with self.assertRaises(AssertionError):
1034 value_from.linked_to = None
1035 portfolio.Trade(value_from, value_to, "ETH")
1036 with self.assertRaises(AssertionError):
1037 value_from.currency = "ETH"
1038 portfolio.Trade(value_from, value_to, "ETH")
1039
1040 value_from = portfolio.Amount("BTC", 0)
1041 trade = portfolio.Trade(value_from, value_to, "ETH")
1042 self.assertEqual(0, trade.value_from.linked_to)
1043
1044 def test_action(self):
1045 value_from = portfolio.Amount("BTC", "1.0")
1046 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1047 value_to = portfolio.Amount("BTC", "1.0")
1048 trade = portfolio.Trade(value_from, value_to, "ETH")
1049
1050 self.assertIsNone(trade.action)
1051
1052 value_from = portfolio.Amount("BTC", "1.0")
1053 value_from.linked_to = portfolio.Amount("BTC", "1.0")
1054 value_to = portfolio.Amount("BTC", "2.0")
1055 trade = portfolio.Trade(value_from, value_to, "BTC")
1056
1057 self.assertIsNone(trade.action)
1058
1059 value_from = portfolio.Amount("BTC", "0.5")
1060 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1061 value_to = portfolio.Amount("BTC", "1.0")
1062 trade = portfolio.Trade(value_from, value_to, "ETH")
1063
1064 self.assertEqual("acquire", trade.action)
1065
1066 value_from = portfolio.Amount("BTC", "0")
1067 value_from.linked_to = portfolio.Amount("ETH", "0")
1068 value_to = portfolio.Amount("BTC", "-1.0")
1069 trade = portfolio.Trade(value_from, value_to, "ETH")
1070
1071 self.assertEqual("dispose", trade.action)
1072
1073 def test_order_action(self):
1074 value_from = portfolio.Amount("BTC", "0.5")
1075 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1076 value_to = portfolio.Amount("BTC", "1.0")
1077 trade = portfolio.Trade(value_from, value_to, "ETH")
1078
1079 self.assertEqual("buy", trade.order_action(False))
1080 self.assertEqual("sell", trade.order_action(True))
1081
1082 value_from = portfolio.Amount("BTC", "0")
1083 value_from.linked_to = portfolio.Amount("ETH", "0")
1084 value_to = portfolio.Amount("BTC", "-1.0")
1085 trade = portfolio.Trade(value_from, value_to, "ETH")
1086
1087 self.assertEqual("sell", trade.order_action(False))
1088 self.assertEqual("buy", trade.order_action(True))
1089
1090 def test_trade_type(self):
1091 value_from = portfolio.Amount("BTC", "0.5")
1092 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1093 value_to = portfolio.Amount("BTC", "1.0")
1094 trade = portfolio.Trade(value_from, value_to, "ETH")
1095
1096 self.assertEqual("long", trade.trade_type)
1097
1098 value_from = portfolio.Amount("BTC", "0")
1099 value_from.linked_to = portfolio.Amount("ETH", "0")
1100 value_to = portfolio.Amount("BTC", "-1.0")
1101 trade = portfolio.Trade(value_from, value_to, "ETH")
1102
1103 self.assertEqual("short", trade.trade_type)
1104
1105 def test_filled_amount(self):
1106 value_from = portfolio.Amount("BTC", "0.5")
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 order1 = mock.Mock()
1112 order1.filled_amount.return_value = portfolio.Amount("ETH", "0.3")
1113
1114 order2 = mock.Mock()
1115 order2.filled_amount.return_value = portfolio.Amount("ETH", "0.01")
1116 trade.orders.append(order1)
1117 trade.orders.append(order2)
1118
1119 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount())
1120 order1.filled_amount.assert_called_with(in_base_currency=False)
1121 order2.filled_amount.assert_called_with(in_base_currency=False)
1122
1123 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=False))
1124 order1.filled_amount.assert_called_with(in_base_currency=False)
1125 order2.filled_amount.assert_called_with(in_base_currency=False)
1126
1127 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=True))
1128 order1.filled_amount.assert_called_with(in_base_currency=True)
1129 order2.filled_amount.assert_called_with(in_base_currency=True)
1130
1131 @mock.patch.object(helper, "get_ticker")
1132 @mock.patch.object(portfolio.Computation, "compute_value")
1133 @mock.patch.object(portfolio.Trade, "filled_amount")
1134 @mock.patch.object(portfolio, "Order")
1135 def test_prepare_order(self, Order, filled_amount, compute_value, get_ticker):
1136 Order.return_value = "Order"
1137
1138 with self.subTest(desc="Nothing to do"):
1139 value_from = portfolio.Amount("BTC", "10")
1140 value_from.rate = D("0.1")
1141 value_from.linked_to = portfolio.Amount("FOO", "100")
1142 value_to = portfolio.Amount("BTC", "10")
1143 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1144
1145 trade.prepare_order()
1146
1147 filled_amount.assert_not_called()
1148 compute_value.assert_not_called()
1149 self.assertEqual(0, len(trade.orders))
1150 Order.assert_not_called()
1151
1152 get_ticker.return_value = { "inverted": False }
1153 with self.subTest(desc="Already filled"), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1154 filled_amount.return_value = portfolio.Amount("FOO", "100")
1155 compute_value.return_value = D("0.125")
1156
1157 value_from = portfolio.Amount("BTC", "10")
1158 value_from.rate = D("0.1")
1159 value_from.linked_to = portfolio.Amount("FOO", "100")
1160 value_to = portfolio.Amount("BTC", "0")
1161 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1162
1163 trade.prepare_order()
1164
1165 filled_amount.assert_called_with(in_base_currency=False)
1166 compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default")
1167 self.assertEqual(0, len(trade.orders))
1168 self.assertRegex(stdout_mock.getvalue(), "Less to do than already filled: ")
1169 Order.assert_not_called()
1170
1171 with self.subTest(action="dispose", inverted=False):
1172 filled_amount.return_value = portfolio.Amount("FOO", "60")
1173 compute_value.return_value = D("0.125")
1174
1175 value_from = portfolio.Amount("BTC", "10")
1176 value_from.rate = D("0.1")
1177 value_from.linked_to = portfolio.Amount("FOO", "100")
1178 value_to = portfolio.Amount("BTC", "1")
1179 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1180
1181 trade.prepare_order()
1182
1183 filled_amount.assert_called_with(in_base_currency=False)
1184 compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default")
1185 self.assertEqual(1, len(trade.orders))
1186 Order.assert_called_with("sell", portfolio.Amount("FOO", 30),
1187 D("0.125"), "BTC", "long", "market",
1188 trade, close_if_possible=False)
1189
1190 with self.subTest(action="acquire", inverted=False):
1191 filled_amount.return_value = portfolio.Amount("BTC", "3")
1192 compute_value.return_value = D("0.125")
1193
1194 value_from = portfolio.Amount("BTC", "1")
1195 value_from.rate = D("0.1")
1196 value_from.linked_to = portfolio.Amount("FOO", "10")
1197 value_to = portfolio.Amount("BTC", "10")
1198 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1199
1200 trade.prepare_order()
1201
1202 filled_amount.assert_called_with(in_base_currency=True)
1203 compute_value.assert_called_with(get_ticker.return_value, "buy", compute_value="default")
1204 self.assertEqual(1, len(trade.orders))
1205
1206 Order.assert_called_with("buy", portfolio.Amount("FOO", 48),
1207 D("0.125"), "BTC", "long", "market",
1208 trade, close_if_possible=False)
1209
1210 with self.subTest(close_if_possible=True):
1211 filled_amount.return_value = portfolio.Amount("FOO", "0")
1212 compute_value.return_value = D("0.125")
1213
1214 value_from = portfolio.Amount("BTC", "10")
1215 value_from.rate = D("0.1")
1216 value_from.linked_to = portfolio.Amount("FOO", "100")
1217 value_to = portfolio.Amount("BTC", "0")
1218 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1219
1220 trade.prepare_order()
1221
1222 filled_amount.assert_called_with(in_base_currency=False)
1223 compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default")
1224 self.assertEqual(1, len(trade.orders))
1225 Order.assert_called_with("sell", portfolio.Amount("FOO", 100),
1226 D("0.125"), "BTC", "long", "market",
1227 trade, close_if_possible=True)
1228
1229 get_ticker.return_value = { "inverted": True, "original": {} }
1230 with self.subTest(action="dispose", inverted=True):
1231 filled_amount.return_value = portfolio.Amount("FOO", "300")
1232 compute_value.return_value = D("125")
1233
1234 value_from = portfolio.Amount("BTC", "10")
1235 value_from.rate = D("0.01")
1236 value_from.linked_to = portfolio.Amount("FOO", "1000")
1237 value_to = portfolio.Amount("BTC", "1")
1238 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1239
1240 trade.prepare_order(compute_value="foo")
1241
1242 filled_amount.assert_called_with(in_base_currency=True)
1243 compute_value.assert_called_with(get_ticker.return_value["original"], "buy", compute_value="foo")
1244 self.assertEqual(1, len(trade.orders))
1245 Order.assert_called_with("buy", portfolio.Amount("BTC", D("4.8")),
1246 D("125"), "FOO", "long", "market",
1247 trade, close_if_possible=False)
1248
1249 with self.subTest(action="acquire", inverted=True):
1250 filled_amount.return_value = portfolio.Amount("BTC", "4")
1251 compute_value.return_value = D("125")
1252
1253 value_from = portfolio.Amount("BTC", "1")
1254 value_from.rate = D("0.01")
1255 value_from.linked_to = portfolio.Amount("FOO", "100")
1256 value_to = portfolio.Amount("BTC", "10")
1257 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1258
1259 trade.prepare_order(compute_value="foo")
1260
1261 filled_amount.assert_called_with(in_base_currency=False)
1262 compute_value.assert_called_with(get_ticker.return_value["original"], "sell", compute_value="foo")
1263 self.assertEqual(1, len(trade.orders))
1264 Order.assert_called_with("sell", portfolio.Amount("BTC", D("5")),
1265 D("125"), "FOO", "long", "market",
1266 trade, close_if_possible=False)
1267
1268
1269 @mock.patch.object(portfolio.Trade, "prepare_order")
1270 def test_update_order(self, prepare_order):
1271 order_mock = mock.Mock()
1272 new_order_mock = mock.Mock()
1273
1274 value_from = portfolio.Amount("BTC", "0.5")
1275 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1276 value_to = portfolio.Amount("BTC", "1.0")
1277 trade = portfolio.Trade(value_from, value_to, "ETH")
1278 def _prepare_order(compute_value=None):
1279 trade.orders.append(new_order_mock)
1280 prepare_order.side_effect = _prepare_order
1281
1282 for i in [0, 1, 3, 4, 6]:
1283 with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1284 trade.update_order(order_mock, i)
1285 order_mock.cancel.assert_not_called()
1286 new_order_mock.run.assert_not_called()
1287 self.assertRegex(stdout_mock.getvalue(), "tick {}, waiting".format(i))
1288 self.assertEqual(0, len(trade.orders))
1289
1290 order_mock.reset_mock()
1291 new_order_mock.reset_mock()
1292 trade.orders = []
1293
1294 with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1295 trade.update_order(order_mock, 2)
1296 order_mock.cancel.assert_called()
1297 new_order_mock.run.assert_called()
1298 prepare_order.assert_called()
1299 self.assertRegex(stdout_mock.getvalue(), "tick 2, cancelling and adjusting")
1300 self.assertEqual(1, len(trade.orders))
1301
1302 order_mock.reset_mock()
1303 new_order_mock.reset_mock()
1304 trade.orders = []
1305
1306 with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1307 trade.update_order(order_mock, 5)
1308 order_mock.cancel.assert_called()
1309 new_order_mock.run.assert_called()
1310 prepare_order.assert_called()
1311 self.assertRegex(stdout_mock.getvalue(), "tick 5, cancelling and adjusting")
1312 self.assertEqual(1, len(trade.orders))
1313
1314 order_mock.reset_mock()
1315 new_order_mock.reset_mock()
1316 trade.orders = []
1317
1318 with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1319 trade.update_order(order_mock, 7)
1320 order_mock.cancel.assert_called()
1321 new_order_mock.run.assert_called()
1322 prepare_order.assert_called_with(compute_value="default")
1323 self.assertRegex(stdout_mock.getvalue(), "tick 7, fallbacking to market value")
1324 self.assertRegex(stdout_mock.getvalue(), "tick 7, market value, cancelling and adjusting to")
1325 self.assertEqual(1, len(trade.orders))
1326
1327 order_mock.reset_mock()
1328 new_order_mock.reset_mock()
1329 trade.orders = []
1330
1331 for i in [10, 13, 16]:
1332 with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1333 trade.update_order(order_mock, i)
1334 order_mock.cancel.assert_called()
1335 new_order_mock.run.assert_called()
1336 prepare_order.assert_called_with(compute_value="default")
1337 self.assertNotRegex(stdout_mock.getvalue(), "tick {}, fallbacking to market value".format(i))
1338 self.assertRegex(stdout_mock.getvalue(), "tick {}, market value, cancelling and adjusting to".format(i))
1339 self.assertEqual(1, len(trade.orders))
1340
1341 order_mock.reset_mock()
1342 new_order_mock.reset_mock()
1343 trade.orders = []
1344
1345 for i in [8, 9, 11, 12]:
1346 with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1347 trade.update_order(order_mock, i)
1348 order_mock.cancel.assert_not_called()
1349 new_order_mock.run.assert_not_called()
1350 self.assertEqual("", stdout_mock.getvalue())
1351 self.assertEqual(0, len(trade.orders))
1352
1353 order_mock.reset_mock()
1354 new_order_mock.reset_mock()
1355 trade.orders = []
1356
1357
1358 @mock.patch('sys.stdout', new_callable=StringIO)
1359 def test_print_with_order(self, mock_stdout):
1360 value_from = portfolio.Amount("BTC", "0.5")
1361 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1362 value_to = portfolio.Amount("BTC", "1.0")
1363 trade = portfolio.Trade(value_from, value_to, "ETH")
1364
1365 order_mock1 = mock.Mock()
1366 order_mock1.__repr__ = mock.Mock()
1367 order_mock1.__repr__.return_value = "Mock 1"
1368 order_mock2 = mock.Mock()
1369 order_mock2.__repr__ = mock.Mock()
1370 order_mock2.__repr__.return_value = "Mock 2"
1371 trade.orders.append(order_mock1)
1372 trade.orders.append(order_mock2)
1373
1374 trade.print_with_order()
1375
1376 out = mock_stdout.getvalue().split("\n")
1377 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", out[0])
1378 self.assertEqual("\tMock 1", out[1])
1379 self.assertEqual("\tMock 2", out[2])
1380
1381 def test__repr(self):
1382 value_from = portfolio.Amount("BTC", "0.5")
1383 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1384 value_to = portfolio.Amount("BTC", "1.0")
1385 trade = portfolio.Trade(value_from, value_to, "ETH")
1386
1387 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(trade))
1388
1389 @unittest.skipUnless("acceptance" in limits, "Acceptance skipped")
1390 class AcceptanceTest(WebMockTestCase):
1391 @unittest.expectedFailure
1392 def test_success_sell_only_necessary(self):
1393 fetch_balance = {
1394 "ETH": {
1395 "exchange_free": D("1.0"),
1396 "exchange_used": D("0.0"),
1397 "exchange_total": D("1.0"),
1398 "total": D("1.0"),
1399 },
1400 "ETC": {
1401 "exchange_free": D("4.0"),
1402 "exchange_used": D("0.0"),
1403 "exchange_total": D("4.0"),
1404 "total": D("4.0"),
1405 },
1406 "XVG": {
1407 "exchange_free": D("1000.0"),
1408 "exchange_used": D("0.0"),
1409 "exchange_total": D("1000.0"),
1410 "total": D("1000.0"),
1411 },
1412 }
1413 repartition = {
1414 "ETH": (D("0.25"), "long"),
1415 "ETC": (D("0.25"), "long"),
1416 "BTC": (D("0.4"), "long"),
1417 "BTD": (D("0.01"), "short"),
1418 "B2X": (D("0.04"), "long"),
1419 "USDT": (D("0.05"), "long"),
1420 }
1421
1422 def fetch_ticker(symbol):
1423 if symbol == "ETH/BTC":
1424 return {
1425 "symbol": "ETH/BTC",
1426 "bid": D("0.14"),
1427 "ask": D("0.16")
1428 }
1429 if symbol == "ETC/BTC":
1430 return {
1431 "symbol": "ETC/BTC",
1432 "bid": D("0.002"),
1433 "ask": D("0.003")
1434 }
1435 if symbol == "XVG/BTC":
1436 return {
1437 "symbol": "XVG/BTC",
1438 "bid": D("0.00003"),
1439 "ask": D("0.00005")
1440 }
1441 if symbol == "BTD/BTC":
1442 return {
1443 "symbol": "BTD/BTC",
1444 "bid": D("0.0008"),
1445 "ask": D("0.0012")
1446 }
1447 if symbol == "B2X/BTC":
1448 return {
1449 "symbol": "B2X/BTC",
1450 "bid": D("0.0008"),
1451 "ask": D("0.0012")
1452 }
1453 if symbol == "USDT/BTC":
1454 raise helper.ExchangeError
1455 if symbol == "BTC/USDT":
1456 return {
1457 "symbol": "BTC/USDT",
1458 "bid": D("14000"),
1459 "ask": D("16000")
1460 }
1461 self.fail("Shouldn't have been called with {}".format(symbol))
1462
1463 market = mock.Mock()
1464 market.fetch_all_balances.return_value = fetch_balance
1465 market.fetch_ticker.side_effect = fetch_ticker
1466 with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition):
1467 # Action 1
1468 helper.prepare_trades(market)
1469
1470 balances = portfolio.BalanceStore.all
1471 self.assertEqual(portfolio.Amount("ETH", 1), balances["ETH"].total)
1472 self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total)
1473 self.assertEqual(portfolio.Amount("XVG", 1000), balances["XVG"].total)
1474
1475
1476 trades = TradeStore.all
1477 self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades[0].value_from)
1478 self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[0].value_to)
1479 self.assertEqual("dispose", trades[0].action)
1480
1481 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades[1].value_from)
1482 self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[1].value_to)
1483 self.assertEqual("acquire", trades[1].action)
1484
1485 self.assertEqual(portfolio.Amount("BTC", D("0.04")), trades[2].value_from)
1486 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[2].value_to)
1487 self.assertEqual("dispose", trades[2].action)
1488
1489 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[3].value_from)
1490 self.assertEqual(portfolio.Amount("BTC", D("-0.002")), trades[3].value_to)
1491 self.assertEqual("dispose", trades[3].action)
1492
1493 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[4].value_from)
1494 self.assertEqual(portfolio.Amount("BTC", D("0.008")), trades[4].value_to)
1495 self.assertEqual("acquire", trades[4].action)
1496
1497 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[5].value_from)
1498 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades[5].value_to)
1499 self.assertEqual("acquire", trades[5].action)
1500
1501 # Action 2
1502 portfolio.Trade.prepare_orders(only="dispose", compute_value=lambda x, y: x["bid"] * D("1.001"))
1503
1504 all_orders = portfolio.Trade.all_orders()
1505 self.assertEqual(2, len(all_orders))
1506 self.assertEqual(2, 3*all_orders[0].amount.value)
1507 self.assertEqual(D("0.14014"), all_orders[0].rate)
1508 self.assertEqual(1000, all_orders[1].amount.value)
1509 self.assertEqual(D("0.00003003"), all_orders[1].rate)
1510
1511
1512 def create_order(symbol, type, action, amount, price=None, account="exchange"):
1513 self.assertEqual("limit", type)
1514 if symbol == "ETH/BTC":
1515 self.assertEqual("sell", action)
1516 self.assertEqual(D('0.66666666'), amount)
1517 self.assertEqual(D("0.14014"), price)
1518 elif symbol == "XVG/BTC":
1519 self.assertEqual("sell", action)
1520 self.assertEqual(1000, amount)
1521 self.assertEqual(D("0.00003003"), price)
1522 else:
1523 self.fail("I shouldn't have been called")
1524
1525 return {
1526 "id": symbol,
1527 }
1528 market.create_order.side_effect = create_order
1529 market.order_precision.return_value = 8
1530
1531 # Action 3
1532 portfolio.TradeStore.run_orders()
1533
1534 self.assertEqual("open", all_orders[0].status)
1535 self.assertEqual("open", all_orders[1].status)
1536
1537 market.fetch_order.return_value = { "status": "closed" }
1538 with mock.patch.object(portfolio.time, "sleep") as sleep:
1539 # Action 4
1540 helper.follow_orders(verbose=False)
1541
1542 sleep.assert_called_with(30)
1543
1544 for order in all_orders:
1545 self.assertEqual("closed", order.status)
1546
1547 fetch_balance = {
1548 "ETH": {
1549 "free": D("1.0") / 3,
1550 "used": D("0.0"),
1551 "total": D("1.0") / 3,
1552 },
1553 "BTC": {
1554 "free": D("0.134"),
1555 "used": D("0.0"),
1556 "total": D("0.134"),
1557 },
1558 "ETC": {
1559 "free": D("4.0"),
1560 "used": D("0.0"),
1561 "total": D("4.0"),
1562 },
1563 "XVG": {
1564 "free": D("0.0"),
1565 "used": D("0.0"),
1566 "total": D("0.0"),
1567 },
1568 }
1569 market.fetch_balance.return_value = fetch_balance
1570
1571 with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition):
1572 # Action 5
1573 helper.update_trades(market, only="buy", compute_value="average")
1574
1575 balances = portfolio.BalanceStore.all
1576 self.assertEqual(portfolio.Amount("ETH", 1 / D("3")), balances["ETH"].total)
1577 self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total)
1578 self.assertEqual(portfolio.Amount("BTC", D("0.134")), balances["BTC"].total)
1579 self.assertEqual(portfolio.Amount("XVG", 0), balances["XVG"].total)
1580
1581
1582 trades = TradeStore.all
1583 self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades["ETH"].value_from)
1584 self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades["ETH"].value_to)
1585 self.assertEqual("sell", trades["ETH"].action)
1586
1587 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["ETC"].value_from)
1588 self.assertEqual(portfolio.Amount("BTC", D("0.0485")), trades["ETC"].value_to)
1589 self.assertEqual("buy", trades["ETC"].action)
1590
1591 self.assertNotIn("BTC", trades)
1592
1593 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["BTD"].value_from)
1594 self.assertEqual(portfolio.Amount("BTC", D("0.00194")), trades["BTD"].value_to)
1595 self.assertEqual("buy", trades["BTD"].action)
1596
1597 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["B2X"].value_from)
1598 self.assertEqual(portfolio.Amount("BTC", D("0.00776")), trades["B2X"].value_to)
1599 self.assertEqual("buy", trades["B2X"].action)
1600
1601 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["USDT"].value_from)
1602 self.assertEqual(portfolio.Amount("BTC", D("0.0097")), trades["USDT"].value_to)
1603 self.assertEqual("buy", trades["USDT"].action)
1604
1605 self.assertEqual(portfolio.Amount("BTC", D("0.04")), trades["XVG"].value_from)
1606 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["XVG"].value_to)
1607 self.assertEqual("sell", trades["XVG"].action)
1608
1609 # Action 6
1610 portfolio.Trade.prepare_orders(only="buy", compute_value=lambda x, y: x["ask"])
1611
1612 all_orders = portfolio.Trade.all_orders(state="pending")
1613 self.assertEqual(4, len(all_orders))
1614 self.assertEqual(portfolio.Amount("ETC", D("12.83333333")), round(all_orders[0].amount))
1615 self.assertEqual(D("0.003"), all_orders[0].rate)
1616 self.assertEqual("buy", all_orders[0].action)
1617 self.assertEqual("long", all_orders[0].trade_type)
1618
1619 self.assertEqual(portfolio.Amount("BTD", D("1.61666666")), round(all_orders[1].amount))
1620 self.assertEqual(D("0.0012"), all_orders[1].rate)
1621 self.assertEqual("sell", all_orders[1].action)
1622 self.assertEqual("short", all_orders[1].trade_type)
1623
1624 diff = portfolio.Amount("B2X", D("19.4")/3) - all_orders[2].amount
1625 self.assertAlmostEqual(0, diff.value)
1626 self.assertEqual(D("0.0012"), all_orders[2].rate)
1627 self.assertEqual("buy", all_orders[2].action)
1628 self.assertEqual("long", all_orders[2].trade_type)
1629
1630 self.assertEqual(portfolio.Amount("BTC", D("0.0097")), all_orders[3].amount)
1631 self.assertEqual(D("16000"), all_orders[3].rate)
1632 self.assertEqual("sell", all_orders[3].action)
1633 self.assertEqual("long", all_orders[3].trade_type)
1634
1635 # Action 6b
1636 # TODO:
1637 # Move balances to margin
1638
1639 # Action 7
1640 # TODO
1641 # portfolio.TradeStore.run_orders()
1642
1643 with mock.patch.object(portfolio.time, "sleep") as sleep:
1644 # Action 8
1645 helper.follow_orders(verbose=False)
1646
1647 sleep.assert_called_with(30)
1648
1649 if __name__ == '__main__':
1650 unittest.main()