]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blob - test.py
Add missing tests
[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 "id": 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 "id": 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 "id": 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 "id": 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 def test_mark_finished_order(self):
1595 market = mock.Mock()
1596 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1597 D("0.1"), "BTC", "short", market, "trade",
1598 close_if_possible=True)
1599 order.status = "closed"
1600 portfolio.TradeStore.debug = False
1601
1602 order.mark_finished_order()
1603 market.close_margin_position.assert_called_with("ETH", "BTC")
1604 market.close_margin_position.reset_mock()
1605
1606 order.status = "open"
1607 order.mark_finished_order()
1608 market.close_margin_position.assert_not_called()
1609
1610 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1611 D("0.1"), "BTC", "short", market, "trade",
1612 close_if_possible=False)
1613 order.status = "closed"
1614 order.mark_finished_order()
1615 market.close_margin_position.assert_not_called()
1616
1617 order = portfolio.Order("sell", portfolio.Amount("ETH", 10),
1618 D("0.1"), "BTC", "short", market, "trade",
1619 close_if_possible=True)
1620 order.status = "closed"
1621 order.mark_finished_order()
1622 market.close_margin_position.assert_not_called()
1623
1624 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1625 D("0.1"), "BTC", "long", market, "trade",
1626 close_if_possible=True)
1627 order.status = "closed"
1628 order.mark_finished_order()
1629 market.close_margin_position.assert_not_called()
1630
1631 portfolio.TradeStore.debug = True
1632
1633 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1634 D("0.1"), "BTC", "short", market, "trade",
1635 close_if_possible=True)
1636 order.status = "closed"
1637
1638 order.mark_finished_order()
1639 market.close_margin_position.assert_not_called()
1640
1641 @mock.patch.object(portfolio.Order, "fetch_mouvements")
1642 def test_fetch(self, fetch_mouvements):
1643 time = self.time.time()
1644 with mock.patch.object(portfolio.time, "time") as time_mock:
1645 market = mock.Mock()
1646 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1647 D("0.1"), "BTC", "long", market, "trade")
1648 order.id = 45
1649 with self.subTest(debug=True):
1650 portfolio.TradeStore.debug = True
1651 order.fetch()
1652 time_mock.assert_not_called()
1653 order.fetch(force=True)
1654 time_mock.assert_not_called()
1655 market.fetch_order.assert_not_called()
1656 fetch_mouvements.assert_not_called()
1657 self.assertIsNone(order.fetch_cache_timestamp)
1658
1659 with self.subTest(debug=False):
1660 portfolio.TradeStore.debug = False
1661 time_mock.return_value = time
1662 market.fetch_order.return_value = {
1663 "status": "foo",
1664 "datetime": "timestamp"
1665 }
1666 order.fetch()
1667
1668 market.fetch_order.assert_called_once()
1669 fetch_mouvements.assert_called_once()
1670 self.assertEqual("foo", order.status)
1671 self.assertEqual("timestamp", order.timestamp)
1672 self.assertEqual(time, order.fetch_cache_timestamp)
1673 self.assertEqual(1, len(order.results))
1674
1675 market.fetch_order.reset_mock()
1676 fetch_mouvements.reset_mock()
1677
1678 time_mock.return_value = time + 8
1679 order.fetch()
1680 market.fetch_order.assert_not_called()
1681 fetch_mouvements.assert_not_called()
1682
1683 order.fetch(force=True)
1684 market.fetch_order.assert_called_once()
1685 fetch_mouvements.assert_called_once()
1686
1687 market.fetch_order.reset_mock()
1688 fetch_mouvements.reset_mock()
1689
1690 time_mock.return_value = time + 19
1691 order.fetch()
1692 market.fetch_order.assert_called_once()
1693 fetch_mouvements.assert_called_once()
1694
1695 @mock.patch.object(portfolio.Order, "fetch")
1696 @mock.patch.object(portfolio.Order, "mark_finished_order")
1697 def test_get_status(self, mark_finished_order, fetch):
1698 with self.subTest(debug=True):
1699 portfolio.TradeStore.debug = True
1700 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1701 D("0.1"), "BTC", "long", "market", "trade")
1702 self.assertEqual("pending", order.get_status())
1703 fetch.assert_not_called()
1704
1705 with self.subTest(debug=False, finished=False):
1706 portfolio.TradeStore.debug = False
1707 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1708 D("0.1"), "BTC", "long", "market", "trade")
1709 def _fetch(order):
1710 def update_status():
1711 order.status = "open"
1712 return update_status
1713 fetch.side_effect = _fetch(order)
1714 self.assertEqual("open", order.get_status())
1715 mark_finished_order.assert_not_called()
1716 fetch.assert_called_once()
1717
1718 mark_finished_order.reset_mock()
1719 fetch.reset_mock()
1720 with self.subTest(debug=False, finished=True):
1721 portfolio.TradeStore.debug = False
1722 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1723 D("0.1"), "BTC", "long", "market", "trade")
1724 def _fetch(order):
1725 def update_status():
1726 order.status = "closed"
1727 return update_status
1728 fetch.side_effect = _fetch(order)
1729 self.assertEqual("closed", order.get_status())
1730 mark_finished_order.assert_called_once()
1731 fetch.assert_called_once()
1732
1733 def test_run(self):
1734 market = mock.Mock()
1735
1736 market.order_precision.return_value = 4
1737 with self.subTest(debug=True),\
1738 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1739 portfolio.TradeStore.debug = True
1740 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1741 D("0.1"), "BTC", "long", market, "trade")
1742 order.run()
1743 market.create_order.assert_not_called()
1744 self.assertEqual("market.create_order('ETH/BTC', 'limit', 'buy', 10.0000, price=0.1, account=exchange)\n", stdout_mock.getvalue())
1745 self.assertEqual("open", order.status)
1746 self.assertEqual(1, len(order.results))
1747 self.assertEqual(-1, order.id)
1748
1749 market.create_order.reset_mock()
1750 with self.subTest(debug=False):
1751 portfolio.TradeStore.debug = False
1752 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1753 D("0.1"), "BTC", "long", market, "trade")
1754 market.create_order.return_value = { "id": 123 }
1755 order.run()
1756 market.create_order.assert_called_once()
1757 self.assertEqual(1, len(order.results))
1758 self.assertEqual("open", order.status)
1759
1760 market.create_order.reset_mock()
1761 with self.subTest(exception=True),\
1762 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1763 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1764 D("0.1"), "BTC", "long", market, "trade")
1765 market.create_order.side_effect = Exception("bouh")
1766 order.run()
1767 market.create_order.assert_called_once()
1768 self.assertEqual(0, len(order.results))
1769 self.assertEqual("error", order.status)
1770 self.assertRegex(stdout_mock.getvalue(), "error when running market.create_order")
1771 self.assertRegex(stdout_mock.getvalue(), "Exception: bouh")
1772
1773 @unittest.skipUnless("unit" in limits, "Unit skipped")
1774 class MouvementTest(WebMockTestCase):
1775 def test_values(self):
1776 mouvement = portfolio.Mouvement("ETH", "BTC", {
1777 "id": 42, "type": "buy", "fee": "0.0015",
1778 "date": "2017-12-30 12:00:12", "rate": "0.1",
1779 "amount": "10", "total": "1"
1780 })
1781 self.assertEqual("ETH", mouvement.currency)
1782 self.assertEqual("BTC", mouvement.base_currency)
1783 self.assertEqual(42, mouvement.id)
1784 self.assertEqual("buy", mouvement.action)
1785 self.assertEqual(D("0.0015"), mouvement.fee_rate)
1786 self.assertEqual(portfolio.datetime(2017, 12, 30, 12, 0, 12), mouvement.date)
1787 self.assertEqual(D("0.1"), mouvement.rate)
1788 self.assertEqual(portfolio.Amount("ETH", "10"), mouvement.total)
1789 self.assertEqual(portfolio.Amount("BTC", "1"), mouvement.total_in_base)
1790
1791 @unittest.skipUnless("acceptance" in limits, "Acceptance skipped")
1792 class AcceptanceTest(WebMockTestCase):
1793 @unittest.expectedFailure
1794 def test_success_sell_only_necessary(self):
1795 fetch_balance = {
1796 "ETH": {
1797 "exchange_free": D("1.0"),
1798 "exchange_used": D("0.0"),
1799 "exchange_total": D("1.0"),
1800 "total": D("1.0"),
1801 },
1802 "ETC": {
1803 "exchange_free": D("4.0"),
1804 "exchange_used": D("0.0"),
1805 "exchange_total": D("4.0"),
1806 "total": D("4.0"),
1807 },
1808 "XVG": {
1809 "exchange_free": D("1000.0"),
1810 "exchange_used": D("0.0"),
1811 "exchange_total": D("1000.0"),
1812 "total": D("1000.0"),
1813 },
1814 }
1815 repartition = {
1816 "ETH": (D("0.25"), "long"),
1817 "ETC": (D("0.25"), "long"),
1818 "BTC": (D("0.4"), "long"),
1819 "BTD": (D("0.01"), "short"),
1820 "B2X": (D("0.04"), "long"),
1821 "USDT": (D("0.05"), "long"),
1822 }
1823
1824 def fetch_ticker(symbol):
1825 if symbol == "ETH/BTC":
1826 return {
1827 "symbol": "ETH/BTC",
1828 "bid": D("0.14"),
1829 "ask": D("0.16")
1830 }
1831 if symbol == "ETC/BTC":
1832 return {
1833 "symbol": "ETC/BTC",
1834 "bid": D("0.002"),
1835 "ask": D("0.003")
1836 }
1837 if symbol == "XVG/BTC":
1838 return {
1839 "symbol": "XVG/BTC",
1840 "bid": D("0.00003"),
1841 "ask": D("0.00005")
1842 }
1843 if symbol == "BTD/BTC":
1844 return {
1845 "symbol": "BTD/BTC",
1846 "bid": D("0.0008"),
1847 "ask": D("0.0012")
1848 }
1849 if symbol == "B2X/BTC":
1850 return {
1851 "symbol": "B2X/BTC",
1852 "bid": D("0.0008"),
1853 "ask": D("0.0012")
1854 }
1855 if symbol == "USDT/BTC":
1856 raise helper.ExchangeError
1857 if symbol == "BTC/USDT":
1858 return {
1859 "symbol": "BTC/USDT",
1860 "bid": D("14000"),
1861 "ask": D("16000")
1862 }
1863 self.fail("Shouldn't have been called with {}".format(symbol))
1864
1865 market = mock.Mock()
1866 market.fetch_all_balances.return_value = fetch_balance
1867 market.fetch_ticker.side_effect = fetch_ticker
1868 with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition):
1869 # Action 1
1870 helper.prepare_trades(market)
1871
1872 balances = portfolio.BalanceStore.all
1873 self.assertEqual(portfolio.Amount("ETH", 1), balances["ETH"].total)
1874 self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total)
1875 self.assertEqual(portfolio.Amount("XVG", 1000), balances["XVG"].total)
1876
1877
1878 trades = portfolio.TradeStore.all
1879 self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades[0].value_from)
1880 self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[0].value_to)
1881 self.assertEqual("dispose", trades[0].action)
1882
1883 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades[1].value_from)
1884 self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[1].value_to)
1885 self.assertEqual("acquire", trades[1].action)
1886
1887 self.assertEqual(portfolio.Amount("BTC", D("0.04")), trades[2].value_from)
1888 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[2].value_to)
1889 self.assertEqual("dispose", trades[2].action)
1890
1891 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[3].value_from)
1892 self.assertEqual(portfolio.Amount("BTC", D("-0.002")), trades[3].value_to)
1893 self.assertEqual("acquire", trades[3].action)
1894
1895 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[4].value_from)
1896 self.assertEqual(portfolio.Amount("BTC", D("0.008")), trades[4].value_to)
1897 self.assertEqual("acquire", trades[4].action)
1898
1899 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[5].value_from)
1900 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades[5].value_to)
1901 self.assertEqual("acquire", trades[5].action)
1902
1903 # Action 2
1904 portfolio.TradeStore.prepare_orders(only="dispose", compute_value=lambda x, y: x["bid"] * D("1.001"))
1905
1906 all_orders = portfolio.TradeStore.all_orders(state="pending")
1907 self.assertEqual(2, len(all_orders))
1908 self.assertEqual(2, 3*all_orders[0].amount.value)
1909 self.assertEqual(D("0.14014"), all_orders[0].rate)
1910 self.assertEqual(1000, all_orders[1].amount.value)
1911 self.assertEqual(D("0.00003003"), all_orders[1].rate)
1912
1913
1914 def create_order(symbol, type, action, amount, price=None, account="exchange"):
1915 self.assertEqual("limit", type)
1916 if symbol == "ETH/BTC":
1917 self.assertEqual("sell", action)
1918 self.assertEqual(D('0.66666666'), amount)
1919 self.assertEqual(D("0.14014"), price)
1920 elif symbol == "XVG/BTC":
1921 self.assertEqual("sell", action)
1922 self.assertEqual(1000, amount)
1923 self.assertEqual(D("0.00003003"), price)
1924 else:
1925 self.fail("I shouldn't have been called")
1926
1927 return {
1928 "id": symbol,
1929 }
1930 market.create_order.side_effect = create_order
1931 market.order_precision.return_value = 8
1932
1933 # Action 3
1934 portfolio.TradeStore.run_orders()
1935
1936 self.assertEqual("open", all_orders[0].status)
1937 self.assertEqual("open", all_orders[1].status)
1938
1939 market.fetch_order.return_value = { "status": "closed", "datetime": "2018-01-20 13:40:00" }
1940 market.privatePostReturnOrderTrades.return_value = [
1941 {
1942 "id": 42, "type": "buy", "fee": "0.0015",
1943 "date": "2017-12-30 12:00:12", "rate": "0.1",
1944 "amount": "10", "total": "1"
1945 }
1946 ]
1947 with mock.patch.object(portfolio.time, "sleep") as sleep:
1948 # Action 4
1949 helper.follow_orders(verbose=False)
1950
1951 sleep.assert_called_with(30)
1952
1953 for order in all_orders:
1954 self.assertEqual("closed", order.status)
1955
1956 fetch_balance = {
1957 "ETH": {
1958 "exchange_free": D("1.0") / 3,
1959 "exchange_used": D("0.0"),
1960 "exchange_total": D("1.0") / 3,
1961 "margin_total": 0,
1962 "total": D("1.0") / 3,
1963 },
1964 "BTC": {
1965 "exchange_free": D("0.134"),
1966 "exchange_used": D("0.0"),
1967 "exchange_total": D("0.134"),
1968 "margin_total": 0,
1969 "total": D("0.134"),
1970 },
1971 "ETC": {
1972 "exchange_free": D("4.0"),
1973 "exchange_used": D("0.0"),
1974 "exchange_total": D("4.0"),
1975 "margin_total": 0,
1976 "total": D("4.0"),
1977 },
1978 "XVG": {
1979 "exchange_free": D("0.0"),
1980 "exchange_used": D("0.0"),
1981 "exchange_total": D("0.0"),
1982 "margin_total": 0,
1983 "total": D("0.0"),
1984 },
1985 }
1986 market.fetch_all_balances.return_value = fetch_balance
1987
1988 with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition):
1989 # Action 5
1990 helper.update_trades(market, only="acquire", compute_value="average")
1991
1992 balances = portfolio.BalanceStore.all
1993 self.assertEqual(portfolio.Amount("ETH", 1 / D("3")), balances["ETH"].total)
1994 self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total)
1995 self.assertEqual(portfolio.Amount("BTC", D("0.134")), balances["BTC"].total)
1996 self.assertEqual(portfolio.Amount("XVG", 0), balances["XVG"].total)
1997
1998
1999 trades = portfolio.TradeStore.all
2000 self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades[0].value_from)
2001 self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[0].value_to)
2002 self.assertEqual("dispose", trades[0].action)
2003
2004 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades[1].value_from)
2005 self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[1].value_to)
2006 self.assertEqual("acquire", trades[1].action)
2007
2008 self.assertNotIn("BTC", trades)
2009
2010 self.assertEqual(portfolio.Amount("BTC", D("0.04")), trades[2].value_from)
2011 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[2].value_to)
2012 self.assertEqual("dispose", trades[2].action)
2013
2014 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[3].value_from)
2015 self.assertEqual(portfolio.Amount("BTC", D("-0.002")), trades[3].value_to)
2016 self.assertEqual("acquire", trades[3].action)
2017
2018 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[4].value_from)
2019 self.assertEqual(portfolio.Amount("BTC", D("0.008")), trades[4].value_to)
2020 self.assertEqual("acquire", trades[4].action)
2021
2022 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[5].value_from)
2023 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades[5].value_to)
2024 self.assertEqual("acquire", trades[5].action)
2025
2026 # Action 6
2027 portfolio.TradeStore.prepare_orders(only="acquire", compute_value=lambda x, y: x["ask"])
2028
2029 all_orders = portfolio.TradeStore.all_orders(state="pending")
2030 self.assertEqual(4, len(all_orders))
2031 self.assertEqual(portfolio.Amount("ETC", D("12.83333333")), round(all_orders[0].amount))
2032 self.assertEqual(D("0.003"), all_orders[0].rate)
2033 self.assertEqual("buy", all_orders[0].action)
2034 self.assertEqual("long", all_orders[0].trade_type)
2035
2036 self.assertEqual(portfolio.Amount("BTD", D("1.61666666")), round(all_orders[1].amount))
2037 self.assertEqual(D("0.0012"), all_orders[1].rate)
2038 self.assertEqual("sell", all_orders[1].action)
2039 self.assertEqual("short", all_orders[1].trade_type)
2040
2041 diff = portfolio.Amount("B2X", D("19.4")/3) - all_orders[2].amount
2042 self.assertAlmostEqual(0, diff.value)
2043 self.assertEqual(D("0.0012"), all_orders[2].rate)
2044 self.assertEqual("buy", all_orders[2].action)
2045 self.assertEqual("long", all_orders[2].trade_type)
2046
2047 self.assertEqual(portfolio.Amount("BTC", D("0.0097")), all_orders[3].amount)
2048 self.assertEqual(D("16000"), all_orders[3].rate)
2049 self.assertEqual("sell", all_orders[3].action)
2050 self.assertEqual("long", all_orders[3].trade_type)
2051
2052 # Action 6b
2053 # TODO:
2054 # Move balances to margin
2055
2056 # Action 7
2057 # TODO
2058 # portfolio.TradeStore.run_orders()
2059
2060 with mock.patch.object(portfolio.time, "sleep") as sleep:
2061 # Action 8
2062 helper.follow_orders(verbose=False)
2063
2064 sleep.assert_called_with(30)
2065
2066 if __name__ == '__main__':
2067 unittest.main()