]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blob - test.py
Move helper methods to their due places
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / test.py
1 import sys
2 import portfolio
3 import unittest
4 import datetime
5 from decimal import Decimal as D
6 from unittest import mock
7 import requests
8 import requests_mock
9 from io import StringIO
10 import portfolio, market, main
11
12 limits = ["acceptance", "unit"]
13 for test_type in limits:
14 if "--no{}".format(test_type) in sys.argv:
15 sys.argv.remove("--no{}".format(test_type))
16 limits.remove(test_type)
17 if "--only{}".format(test_type) in sys.argv:
18 sys.argv.remove("--only{}".format(test_type))
19 limits = [test_type]
20 break
21
22 class WebMockTestCase(unittest.TestCase):
23 import time
24
25 def setUp(self):
26 super(WebMockTestCase, self).setUp()
27 self.wm = requests_mock.Mocker()
28 self.wm.start()
29
30 # market
31 self.m = mock.Mock(name="Market", spec=market.Market)
32 self.m.debug = False
33
34 self.patchers = [
35 mock.patch.multiple(portfolio.Portfolio, last_date=None, data=None, liquidities={}),
36 mock.patch.multiple(portfolio.Computation,
37 computations=portfolio.Computation.computations),
38 ]
39 for patcher in self.patchers:
40 patcher.start()
41
42 def tearDown(self):
43 for patcher in self.patchers:
44 patcher.stop()
45 self.wm.stop()
46 super(WebMockTestCase, self).tearDown()
47
48 @unittest.skipUnless("unit" in limits, "Unit skipped")
49 class poloniexETest(unittest.TestCase):
50 def setUp(self):
51 super(poloniexETest, self).setUp()
52 self.wm = requests_mock.Mocker()
53 self.wm.start()
54
55 self.s = market.ccxt.poloniexE()
56
57 def tearDown(self):
58 self.wm.stop()
59 super(poloniexETest, self).tearDown()
60
61 def test_nanoseconds(self):
62 with mock.patch.object(market.ccxt.time, "time") as time:
63 time.return_value = 123456.7890123456
64 self.assertEqual(123456789012345, self.s.nanoseconds())
65
66 def test_nonce(self):
67 with mock.patch.object(market.ccxt.time, "time") as time:
68 time.return_value = 123456.7890123456
69 self.assertEqual(123456789012345, self.s.nonce())
70
71 def test_order_precision(self):
72 self.assertEqual(8, self.s.order_precision("FOO"))
73
74 def test_transfer_balance(self):
75 with self.subTest(success=True),\
76 mock.patch.object(self.s, "privatePostTransferBalance") as t:
77 t.return_value = { "success": 1 }
78 result = self.s.transfer_balance("FOO", 12, "exchange", "margin")
79 t.assert_called_once_with({
80 "currency": "FOO",
81 "amount": 12,
82 "fromAccount": "exchange",
83 "toAccount": "margin",
84 "confirmed": 1
85 })
86 self.assertTrue(result)
87
88 with self.subTest(success=False),\
89 mock.patch.object(self.s, "privatePostTransferBalance") as t:
90 t.return_value = { "success": 0 }
91 self.assertFalse(self.s.transfer_balance("FOO", 12, "exchange", "margin"))
92
93 def test_close_margin_position(self):
94 with mock.patch.object(self.s, "privatePostCloseMarginPosition") as c:
95 self.s.close_margin_position("FOO", "BAR")
96 c.assert_called_with({"currencyPair": "BAR_FOO"})
97
98 def test_tradable_balances(self):
99 with mock.patch.object(self.s, "privatePostReturnTradableBalances") as r:
100 r.return_value = {
101 "FOO": { "exchange": "12.1234", "margin": "0.0123" },
102 "BAR": { "exchange": "1", "margin": "0" },
103 }
104 balances = self.s.tradable_balances()
105 self.assertEqual(["FOO", "BAR"], list(balances.keys()))
106 self.assertEqual(["exchange", "margin"], list(balances["FOO"].keys()))
107 self.assertEqual(D("12.1234"), balances["FOO"]["exchange"])
108 self.assertEqual(["exchange", "margin"], list(balances["BAR"].keys()))
109
110 def test_margin_summary(self):
111 with mock.patch.object(self.s, "privatePostReturnMarginAccountSummary") as r:
112 r.return_value = {
113 "currentMargin": "1.49680968",
114 "lendingFees": "0.0000001",
115 "pl": "0.00008254",
116 "totalBorrowedValue": "0.00673602",
117 "totalValue": "0.01000000",
118 "netValue": "0.01008254",
119 }
120 expected = {
121 'current_margin': D('1.49680968'),
122 'gains': D('0.00008254'),
123 'lending_fees': D('0.0000001'),
124 'total': D('0.01000000'),
125 'total_borrowed': D('0.00673602')
126 }
127 self.assertEqual(expected, self.s.margin_summary())
128
129 @unittest.skipUnless("unit" in limits, "Unit skipped")
130 class PortfolioTest(WebMockTestCase):
131 def fill_data(self):
132 if self.json_response is not None:
133 portfolio.Portfolio.data = self.json_response
134
135 def setUp(self):
136 super(PortfolioTest, self).setUp()
137
138 with open("test_portfolio.json") as example:
139 self.json_response = example.read()
140
141 self.wm.get(portfolio.Portfolio.URL, text=self.json_response)
142
143 def test_get_cryptoportfolio(self):
144 self.wm.get(portfolio.Portfolio.URL, [
145 {"text":'{ "foo": "bar" }', "status_code": 200},
146 {"text": "System Error", "status_code": 500},
147 {"exc": requests.exceptions.ConnectTimeout},
148 ])
149 portfolio.Portfolio.get_cryptoportfolio(self.m)
150 self.assertIn("foo", portfolio.Portfolio.data)
151 self.assertEqual("bar", portfolio.Portfolio.data["foo"])
152 self.assertTrue(self.wm.called)
153 self.assertEqual(1, self.wm.call_count)
154 self.m.report.log_error.assert_not_called()
155 self.m.report.log_http_request.assert_called_once()
156 self.m.report.log_http_request.reset_mock()
157
158 portfolio.Portfolio.get_cryptoportfolio(self.m)
159 self.assertIsNone(portfolio.Portfolio.data)
160 self.assertEqual(2, self.wm.call_count)
161 self.m.report.log_error.assert_not_called()
162 self.m.report.log_http_request.assert_called_once()
163 self.m.report.log_http_request.reset_mock()
164
165
166 portfolio.Portfolio.data = "Foo"
167 portfolio.Portfolio.get_cryptoportfolio(self.m)
168 self.assertEqual("Foo", portfolio.Portfolio.data)
169 self.assertEqual(3, self.wm.call_count)
170 self.m.report.log_error.assert_called_once_with("get_cryptoportfolio",
171 exception=mock.ANY)
172 self.m.report.log_http_request.assert_not_called()
173
174 def test_parse_cryptoportfolio(self):
175 portfolio.Portfolio.parse_cryptoportfolio(self.m)
176
177 self.assertListEqual(
178 ["medium", "high"],
179 list(portfolio.Portfolio.liquidities.keys()))
180
181 liquidities = portfolio.Portfolio.liquidities
182 self.assertEqual(10, len(liquidities["medium"].keys()))
183 self.assertEqual(10, len(liquidities["high"].keys()))
184
185 expected = {
186 'BTC': (D("0.2857"), "long"),
187 'DGB': (D("0.1015"), "long"),
188 'DOGE': (D("0.1805"), "long"),
189 'SC': (D("0.0623"), "long"),
190 'ZEC': (D("0.3701"), "long"),
191 }
192 date = portfolio.datetime(2018, 1, 8)
193 self.assertDictEqual(expected, liquidities["high"][date])
194
195 expected = {
196 'BTC': (D("1.1102e-16"), "long"),
197 'ETC': (D("0.1"), "long"),
198 'FCT': (D("0.1"), "long"),
199 'GAS': (D("0.1"), "long"),
200 'NAV': (D("0.1"), "long"),
201 'OMG': (D("0.1"), "long"),
202 'OMNI': (D("0.1"), "long"),
203 'PPC': (D("0.1"), "long"),
204 'RIC': (D("0.1"), "long"),
205 'VIA': (D("0.1"), "long"),
206 'XCP': (D("0.1"), "long"),
207 }
208 self.assertDictEqual(expected, liquidities["medium"][date])
209 self.assertEqual(portfolio.datetime(2018, 1, 15), portfolio.Portfolio.last_date)
210
211 self.m.report.log_http_request.assert_called_once_with("GET",
212 portfolio.Portfolio.URL, None, mock.ANY, mock.ANY)
213 self.m.report.log_http_request.reset_mock()
214
215 # It doesn't refetch the data when available
216 portfolio.Portfolio.parse_cryptoportfolio(self.m)
217 self.m.report.log_http_request.assert_not_called()
218
219 self.assertEqual(1, self.wm.call_count)
220
221 portfolio.Portfolio.parse_cryptoportfolio(self.m, refetch=True)
222 self.assertEqual(2, self.wm.call_count)
223 self.m.report.log_http_request.assert_called_once()
224
225 def test_repartition(self):
226 expected_medium = {
227 'BTC': (D("1.1102e-16"), "long"),
228 'USDT': (D("0.1"), "long"),
229 'ETC': (D("0.1"), "long"),
230 'FCT': (D("0.1"), "long"),
231 'OMG': (D("0.1"), "long"),
232 'STEEM': (D("0.1"), "long"),
233 'STRAT': (D("0.1"), "long"),
234 'XEM': (D("0.1"), "long"),
235 'XMR': (D("0.1"), "long"),
236 'XVC': (D("0.1"), "long"),
237 'ZRX': (D("0.1"), "long"),
238 }
239 expected_high = {
240 'USDT': (D("0.1226"), "long"),
241 'BTC': (D("0.1429"), "long"),
242 'ETC': (D("0.1127"), "long"),
243 'ETH': (D("0.1569"), "long"),
244 'FCT': (D("0.3341"), "long"),
245 'GAS': (D("0.1308"), "long"),
246 }
247
248 self.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m))
249 self.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m, liquidity="medium"))
250 self.assertEqual(expected_high, portfolio.Portfolio.repartition(self.m, liquidity="high"))
251
252 self.assertEqual(1, self.wm.call_count)
253
254 portfolio.Portfolio.repartition(self.m)
255 self.assertEqual(1, self.wm.call_count)
256
257 portfolio.Portfolio.repartition(self.m, refetch=True)
258 self.assertEqual(2, self.wm.call_count)
259 self.m.report.log_http_request.assert_called()
260 self.assertEqual(2, self.m.report.log_http_request.call_count)
261
262 @mock.patch.object(portfolio.time, "sleep")
263 @mock.patch.object(portfolio.Portfolio, "repartition")
264 def test_wait_for_recent(self, repartition, sleep):
265 self.call_count = 0
266 def _repartition(market, refetch):
267 self.assertEqual(self.m, market)
268 self.assertTrue(refetch)
269 self.call_count += 1
270 portfolio.Portfolio.last_date = portfolio.datetime.now()\
271 - portfolio.timedelta(10)\
272 + portfolio.timedelta(self.call_count)
273 repartition.side_effect = _repartition
274
275 portfolio.Portfolio.wait_for_recent(self.m)
276 sleep.assert_called_with(30)
277 self.assertEqual(6, sleep.call_count)
278 self.assertEqual(7, repartition.call_count)
279 self.m.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio")
280
281 sleep.reset_mock()
282 repartition.reset_mock()
283 portfolio.Portfolio.last_date = None
284 self.call_count = 0
285 portfolio.Portfolio.wait_for_recent(self.m, delta=15)
286 sleep.assert_not_called()
287 self.assertEqual(1, repartition.call_count)
288
289 sleep.reset_mock()
290 repartition.reset_mock()
291 portfolio.Portfolio.last_date = None
292 self.call_count = 0
293 portfolio.Portfolio.wait_for_recent(self.m, delta=1)
294 sleep.assert_called_with(30)
295 self.assertEqual(9, sleep.call_count)
296 self.assertEqual(10, repartition.call_count)
297
298 @unittest.skipUnless("unit" in limits, "Unit skipped")
299 class AmountTest(WebMockTestCase):
300 def test_values(self):
301 amount = portfolio.Amount("BTC", "0.65")
302 self.assertEqual(D("0.65"), amount.value)
303 self.assertEqual("BTC", amount.currency)
304
305 def test_in_currency(self):
306 amount = portfolio.Amount("ETC", 10)
307
308 self.assertEqual(amount, amount.in_currency("ETC", self.m))
309
310 with self.subTest(desc="no ticker for currency"):
311 self.m.get_ticker.return_value = None
312
313 self.assertRaises(Exception, amount.in_currency, "ETH", self.m)
314
315 with self.subTest(desc="nominal case"):
316 self.m.get_ticker.return_value = {
317 "bid": D("0.2"),
318 "ask": D("0.4"),
319 "average": D("0.3"),
320 "foo": "bar",
321 }
322 converted_amount = amount.in_currency("ETH", self.m)
323
324 self.assertEqual(D("3.0"), converted_amount.value)
325 self.assertEqual("ETH", converted_amount.currency)
326 self.assertEqual(amount, converted_amount.linked_to)
327 self.assertEqual("bar", converted_amount.ticker["foo"])
328
329 converted_amount = amount.in_currency("ETH", self.m, action="bid", compute_value="default")
330 self.assertEqual(D("2"), converted_amount.value)
331
332 converted_amount = amount.in_currency("ETH", self.m, compute_value="ask")
333 self.assertEqual(D("4"), converted_amount.value)
334
335 converted_amount = amount.in_currency("ETH", self.m, rate=D("0.02"))
336 self.assertEqual(D("0.2"), converted_amount.value)
337
338 def test__round(self):
339 amount = portfolio.Amount("BAR", portfolio.D("1.23456789876"))
340 self.assertEqual(D("1.23456789"), round(amount).value)
341 self.assertEqual(D("1.23"), round(amount, 2).value)
342
343 def test__abs(self):
344 amount = portfolio.Amount("SC", -120)
345 self.assertEqual(120, abs(amount).value)
346 self.assertEqual("SC", abs(amount).currency)
347
348 amount = portfolio.Amount("SC", 10)
349 self.assertEqual(10, abs(amount).value)
350 self.assertEqual("SC", abs(amount).currency)
351
352 def test__add(self):
353 amount1 = portfolio.Amount("XVG", "12.9")
354 amount2 = portfolio.Amount("XVG", "13.1")
355
356 self.assertEqual(26, (amount1 + amount2).value)
357 self.assertEqual("XVG", (amount1 + amount2).currency)
358
359 amount3 = portfolio.Amount("ETH", "1.6")
360 with self.assertRaises(Exception):
361 amount1 + amount3
362
363 amount4 = portfolio.Amount("ETH", 0.0)
364 self.assertEqual(amount1, amount1 + amount4)
365
366 self.assertEqual(amount1, amount1 + 0)
367
368 def test__radd(self):
369 amount = portfolio.Amount("XVG", "12.9")
370
371 self.assertEqual(amount, 0 + amount)
372 with self.assertRaises(Exception):
373 4 + amount
374
375 def test__sub(self):
376 amount1 = portfolio.Amount("XVG", "13.3")
377 amount2 = portfolio.Amount("XVG", "13.1")
378
379 self.assertEqual(D("0.2"), (amount1 - amount2).value)
380 self.assertEqual("XVG", (amount1 - amount2).currency)
381
382 amount3 = portfolio.Amount("ETH", "1.6")
383 with self.assertRaises(Exception):
384 amount1 - amount3
385
386 amount4 = portfolio.Amount("ETH", 0.0)
387 self.assertEqual(amount1, amount1 - amount4)
388
389 def test__rsub(self):
390 amount = portfolio.Amount("ETH", "1.6")
391 with self.assertRaises(Exception):
392 3 - amount
393
394 self.assertEqual(portfolio.Amount("ETH", "-1.6"), 0-amount)
395
396 def test__mul(self):
397 amount = portfolio.Amount("XEM", 11)
398
399 self.assertEqual(D("38.5"), (amount * D("3.5")).value)
400 self.assertEqual(D("33"), (amount * 3).value)
401
402 with self.assertRaises(Exception):
403 amount * amount
404
405 def test__rmul(self):
406 amount = portfolio.Amount("XEM", 11)
407
408 self.assertEqual(D("38.5"), (D("3.5") * amount).value)
409 self.assertEqual(D("33"), (3 * amount).value)
410
411 def test__floordiv(self):
412 amount = portfolio.Amount("XEM", 11)
413
414 self.assertEqual(D("5.5"), (amount / 2).value)
415 self.assertEqual(D("4.4"), (amount / D("2.5")).value)
416
417 with self.assertRaises(Exception):
418 amount / amount
419
420 def test__truediv(self):
421 amount = portfolio.Amount("XEM", 11)
422
423 self.assertEqual(D("5.5"), (amount / 2).value)
424 self.assertEqual(D("4.4"), (amount / D("2.5")).value)
425
426 def test__lt(self):
427 amount1 = portfolio.Amount("BTD", 11.3)
428 amount2 = portfolio.Amount("BTD", 13.1)
429
430 self.assertTrue(amount1 < amount2)
431 self.assertFalse(amount2 < amount1)
432 self.assertFalse(amount1 < amount1)
433
434 amount3 = portfolio.Amount("BTC", 1.6)
435 with self.assertRaises(Exception):
436 amount1 < amount3
437
438 def test__le(self):
439 amount1 = portfolio.Amount("BTD", 11.3)
440 amount2 = portfolio.Amount("BTD", 13.1)
441
442 self.assertTrue(amount1 <= amount2)
443 self.assertFalse(amount2 <= amount1)
444 self.assertTrue(amount1 <= amount1)
445
446 amount3 = portfolio.Amount("BTC", 1.6)
447 with self.assertRaises(Exception):
448 amount1 <= amount3
449
450 def test__gt(self):
451 amount1 = portfolio.Amount("BTD", 11.3)
452 amount2 = portfolio.Amount("BTD", 13.1)
453
454 self.assertTrue(amount2 > amount1)
455 self.assertFalse(amount1 > amount2)
456 self.assertFalse(amount1 > amount1)
457
458 amount3 = portfolio.Amount("BTC", 1.6)
459 with self.assertRaises(Exception):
460 amount3 > amount1
461
462 def test__ge(self):
463 amount1 = portfolio.Amount("BTD", 11.3)
464 amount2 = portfolio.Amount("BTD", 13.1)
465
466 self.assertTrue(amount2 >= amount1)
467 self.assertFalse(amount1 >= amount2)
468 self.assertTrue(amount1 >= amount1)
469
470 amount3 = portfolio.Amount("BTC", 1.6)
471 with self.assertRaises(Exception):
472 amount3 >= amount1
473
474 def test__eq(self):
475 amount1 = portfolio.Amount("BTD", 11.3)
476 amount2 = portfolio.Amount("BTD", 13.1)
477 amount3 = portfolio.Amount("BTD", 11.3)
478
479 self.assertFalse(amount1 == amount2)
480 self.assertFalse(amount2 == amount1)
481 self.assertTrue(amount1 == amount3)
482 self.assertFalse(amount2 == 0)
483
484 amount4 = portfolio.Amount("BTC", 1.6)
485 with self.assertRaises(Exception):
486 amount1 == amount4
487
488 amount5 = portfolio.Amount("BTD", 0)
489 self.assertTrue(amount5 == 0)
490
491 def test__ne(self):
492 amount1 = portfolio.Amount("BTD", 11.3)
493 amount2 = portfolio.Amount("BTD", 13.1)
494 amount3 = portfolio.Amount("BTD", 11.3)
495
496 self.assertTrue(amount1 != amount2)
497 self.assertTrue(amount2 != amount1)
498 self.assertFalse(amount1 != amount3)
499 self.assertTrue(amount2 != 0)
500
501 amount4 = portfolio.Amount("BTC", 1.6)
502 with self.assertRaises(Exception):
503 amount1 != amount4
504
505 amount5 = portfolio.Amount("BTD", 0)
506 self.assertFalse(amount5 != 0)
507
508 def test__neg(self):
509 amount1 = portfolio.Amount("BTD", "11.3")
510
511 self.assertEqual(portfolio.D("-11.3"), (-amount1).value)
512
513 def test__str(self):
514 amount1 = portfolio.Amount("BTX", 32)
515 self.assertEqual("32.00000000 BTX", str(amount1))
516
517 amount2 = portfolio.Amount("USDT", 12000)
518 amount1.linked_to = amount2
519 self.assertEqual("32.00000000 BTX [12000.00000000 USDT]", str(amount1))
520
521 def test__repr(self):
522 amount1 = portfolio.Amount("BTX", 32)
523 self.assertEqual("Amount(32.00000000 BTX)", repr(amount1))
524
525 amount2 = portfolio.Amount("USDT", 12000)
526 amount1.linked_to = amount2
527 self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT))", repr(amount1))
528
529 amount3 = portfolio.Amount("BTC", 0.1)
530 amount2.linked_to = amount3
531 self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1))
532
533 def test_as_json(self):
534 amount = portfolio.Amount("BTX", 32)
535 self.assertEqual({"currency": "BTX", "value": D("32")}, amount.as_json())
536
537 amount = portfolio.Amount("BTX", "1E-10")
538 self.assertEqual({"currency": "BTX", "value": D("0")}, amount.as_json())
539
540 amount = portfolio.Amount("BTX", "1E-5")
541 self.assertEqual({"currency": "BTX", "value": D("0.00001")}, amount.as_json())
542 self.assertEqual("0.00001", str(amount.as_json()["value"]))
543
544 @unittest.skipUnless("unit" in limits, "Unit skipped")
545 class BalanceTest(WebMockTestCase):
546 def test_values(self):
547 balance = portfolio.Balance("BTC", {
548 "exchange_total": "0.65",
549 "exchange_free": "0.35",
550 "exchange_used": "0.30",
551 "margin_total": "-10",
552 "margin_borrowed": "10",
553 "margin_available": "0",
554 "margin_in_position": "0",
555 "margin_position_type": "short",
556 "margin_borrowed_base_currency": "USDT",
557 "margin_liquidation_price": "1.20",
558 "margin_pending_gain": "10",
559 "margin_lending_fees": "0.4",
560 "margin_borrowed_base_price": "0.15",
561 })
562 self.assertEqual(portfolio.D("0.65"), balance.exchange_total.value)
563 self.assertEqual(portfolio.D("0.35"), balance.exchange_free.value)
564 self.assertEqual(portfolio.D("0.30"), balance.exchange_used.value)
565 self.assertEqual("BTC", balance.exchange_total.currency)
566 self.assertEqual("BTC", balance.exchange_free.currency)
567 self.assertEqual("BTC", balance.exchange_total.currency)
568
569 self.assertEqual(portfolio.D("-10"), balance.margin_total.value)
570 self.assertEqual(portfolio.D("10"), balance.margin_borrowed.value)
571 self.assertEqual(portfolio.D("0"), balance.margin_available.value)
572 self.assertEqual("BTC", balance.margin_total.currency)
573 self.assertEqual("BTC", balance.margin_borrowed.currency)
574 self.assertEqual("BTC", balance.margin_available.currency)
575
576 self.assertEqual("BTC", balance.currency)
577
578 self.assertEqual(portfolio.D("0.4"), balance.margin_lending_fees.value)
579 self.assertEqual("USDT", balance.margin_lending_fees.currency)
580
581 def test__repr(self):
582 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX])",
583 repr(portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 })))
584 balance = portfolio.Balance("BTX", { "exchange_total": 3,
585 "exchange_used": 1, "exchange_free": 2 })
586 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance))
587
588 balance = portfolio.Balance("BTX", { "exchange_total": 1, "exchange_used": 1})
589 self.assertEqual("Balance(BTX Exch: [❌1.00000000 BTX])", repr(balance))
590
591 balance = portfolio.Balance("BTX", { "margin_total": 3,
592 "margin_in_position": 1, "margin_available": 2 })
593 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance))
594
595 balance = portfolio.Balance("BTX", { "margin_total": 2, "margin_available": 2 })
596 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX])", repr(balance))
597
598 balance = portfolio.Balance("BTX", { "margin_total": -3,
599 "margin_borrowed_base_price": D("0.1"),
600 "margin_borrowed_base_currency": "BTC",
601 "margin_lending_fees": D("0.002") })
602 self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance))
603
604 balance = portfolio.Balance("BTX", { "margin_total": 1,
605 "margin_in_position": 1, "exchange_free": 2, "exchange_total": 2})
606 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [❌1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance))
607
608 def test_as_json(self):
609 balance = portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 })
610 as_json = balance.as_json()
611 self.assertEqual(set(portfolio.Balance.base_keys), set(as_json.keys()))
612 self.assertEqual(D(0), as_json["total"])
613 self.assertEqual(D(2), as_json["exchange_total"])
614 self.assertEqual(D(2), as_json["exchange_free"])
615 self.assertEqual(D(0), as_json["exchange_used"])
616 self.assertEqual(D(0), as_json["margin_total"])
617 self.assertEqual(D(0), as_json["margin_available"])
618 self.assertEqual(D(0), as_json["margin_borrowed"])
619
620 @unittest.skipUnless("unit" in limits, "Unit skipped")
621 class MarketTest(WebMockTestCase):
622 def setUp(self):
623 super(MarketTest, self).setUp()
624
625 self.ccxt = mock.Mock(spec=market.ccxt.poloniexE)
626
627 def test_values(self):
628 m = market.Market(self.ccxt)
629
630 self.assertEqual(self.ccxt, m.ccxt)
631 self.assertFalse(m.debug)
632 self.assertIsInstance(m.report, market.ReportStore)
633 self.assertIsInstance(m.trades, market.TradeStore)
634 self.assertIsInstance(m.balances, market.BalanceStore)
635 self.assertEqual(m, m.report.market)
636 self.assertEqual(m, m.trades.market)
637 self.assertEqual(m, m.balances.market)
638 self.assertEqual(m, m.ccxt._market)
639
640 m = market.Market(self.ccxt, debug=True)
641 self.assertTrue(m.debug)
642
643 m = market.Market(self.ccxt, debug=False)
644 self.assertFalse(m.debug)
645
646 @mock.patch("market.ccxt")
647 def test_from_config(self, ccxt):
648 with mock.patch("market.ReportStore"):
649 ccxt.poloniexE.return_value = self.ccxt
650 self.ccxt.session.request.return_value = "response"
651
652 m = market.Market.from_config({"key": "key", "secred": "secret"})
653
654 self.assertEqual(self.ccxt, m.ccxt)
655
656 self.ccxt.session.request("GET", "URL", data="data",
657 headers="headers")
658 m.report.log_http_request.assert_called_with('GET', 'URL', 'data',
659 'headers', 'response')
660
661 m = market.Market.from_config({"key": "key", "secred": "secret"}, debug=True)
662 self.assertEqual(True, m.debug)
663
664 def test_get_tickers(self):
665 self.ccxt.fetch_tickers.side_effect = [
666 "tickers",
667 market.NotSupported
668 ]
669
670 m = market.Market(self.ccxt)
671 self.assertEqual("tickers", m.get_tickers())
672 self.assertEqual("tickers", m.get_tickers())
673 self.ccxt.fetch_tickers.assert_called_once()
674
675 self.assertIsNone(m.get_tickers(refresh=self.time.time()))
676
677 def test_get_ticker(self):
678 with self.subTest(get_tickers=True):
679 self.ccxt.fetch_tickers.return_value = {
680 "ETH/ETC": { "bid": 1, "ask": 3 },
681 "XVG/ETH": { "bid": 10, "ask": 40 },
682 }
683 m = market.Market(self.ccxt)
684
685 ticker = m.get_ticker("ETH", "ETC")
686 self.assertEqual(1, ticker["bid"])
687 self.assertEqual(3, ticker["ask"])
688 self.assertEqual(2, ticker["average"])
689 self.assertFalse(ticker["inverted"])
690
691 ticker = m.get_ticker("ETH", "XVG")
692 self.assertEqual(0.0625, ticker["average"])
693 self.assertTrue(ticker["inverted"])
694 self.assertIn("original", ticker)
695 self.assertEqual(10, ticker["original"]["bid"])
696 self.assertEqual(25, ticker["original"]["average"])
697
698 ticker = m.get_ticker("XVG", "XMR")
699 self.assertIsNone(ticker)
700
701 with self.subTest(get_tickers=False):
702 self.ccxt.fetch_tickers.return_value = None
703 self.ccxt.fetch_ticker.side_effect = [
704 { "bid": 1, "ask": 3 },
705 market.ExchangeError("foo"),
706 { "bid": 10, "ask": 40 },
707 market.ExchangeError("foo"),
708 market.ExchangeError("foo"),
709 ]
710
711 m = market.Market(self.ccxt)
712
713 ticker = m.get_ticker("ETH", "ETC")
714 self.ccxt.fetch_ticker.assert_called_with("ETH/ETC")
715 self.assertEqual(1, ticker["bid"])
716 self.assertEqual(3, ticker["ask"])
717 self.assertEqual(2, ticker["average"])
718 self.assertFalse(ticker["inverted"])
719
720 ticker = m.get_ticker("ETH", "XVG")
721 self.assertEqual(0.0625, ticker["average"])
722 self.assertTrue(ticker["inverted"])
723 self.assertIn("original", ticker)
724 self.assertEqual(10, ticker["original"]["bid"])
725 self.assertEqual(25, ticker["original"]["average"])
726
727 ticker = m.get_ticker("XVG", "XMR")
728 self.assertIsNone(ticker)
729
730 def test_fetch_fees(self):
731 m = market.Market(self.ccxt)
732 self.ccxt.fetch_fees.return_value = "Foo"
733 self.assertEqual("Foo", m.fetch_fees())
734 self.ccxt.fetch_fees.assert_called_once()
735 self.ccxt.reset_mock()
736 self.assertEqual("Foo", m.fetch_fees())
737 self.ccxt.fetch_fees.assert_not_called()
738
739 @mock.patch.object(portfolio.Portfolio, "repartition")
740 @mock.patch.object(market.Market, "get_ticker")
741 @mock.patch.object(market.TradeStore, "compute_trades")
742 def test_prepare_trades(self, compute_trades, get_ticker, repartition):
743 repartition.return_value = {
744 "XEM": (D("0.75"), "long"),
745 "BTC": (D("0.25"), "long"),
746 }
747 def _get_ticker(c1, c2):
748 if c1 == "USDT" and c2 == "BTC":
749 return { "average": D("0.0001") }
750 if c1 == "XVG" and c2 == "BTC":
751 return { "average": D("0.000001") }
752 if c1 == "XEM" and c2 == "BTC":
753 return { "average": D("0.001") }
754 self.fail("Should be called with {}, {}".format(c1, c2))
755 get_ticker.side_effect = _get_ticker
756
757 with mock.patch("market.ReportStore"):
758 m = market.Market(self.ccxt)
759 self.ccxt.fetch_all_balances.return_value = {
760 "USDT": {
761 "exchange_free": D("10000.0"),
762 "exchange_used": D("0.0"),
763 "exchange_total": D("10000.0"),
764 "total": D("10000.0")
765 },
766 "XVG": {
767 "exchange_free": D("10000.0"),
768 "exchange_used": D("0.0"),
769 "exchange_total": D("10000.0"),
770 "total": D("10000.0")
771 },
772 }
773
774 m.balances.fetch_balances(tag="tag")
775
776 m.prepare_trades()
777 compute_trades.assert_called()
778
779 call = compute_trades.call_args
780 self.assertEqual(1, call[0][0]["USDT"].value)
781 self.assertEqual(D("0.01"), call[0][0]["XVG"].value)
782 self.assertEqual(D("0.2525"), call[0][1]["BTC"].value)
783 self.assertEqual(D("0.7575"), call[0][1]["XEM"].value)
784 m.report.log_stage.assert_called_once_with("prepare_trades",
785 base_currency='BTC', compute_value='average',
786 liquidity='medium', only=None, repartition=None)
787 m.report.log_balances.assert_called_once_with(tag="tag")
788
789
790 @mock.patch.object(portfolio.time, "sleep")
791 @mock.patch.object(market.TradeStore, "all_orders")
792 def test_follow_orders(self, all_orders, time_mock):
793 for debug, sleep in [
794 (False, None), (True, None),
795 (False, 12), (True, 12)]:
796 with self.subTest(sleep=sleep, debug=debug), \
797 mock.patch("market.ReportStore"):
798 m = market.Market(self.ccxt, debug=debug)
799
800 order_mock1 = mock.Mock()
801 order_mock2 = mock.Mock()
802 order_mock3 = mock.Mock()
803 all_orders.side_effect = [
804 [order_mock1, order_mock2],
805 [order_mock1, order_mock2],
806
807 [order_mock1, order_mock3],
808 [order_mock1, order_mock3],
809
810 [order_mock1, order_mock3],
811 [order_mock1, order_mock3],
812
813 []
814 ]
815
816 order_mock1.get_status.side_effect = ["open", "open", "closed"]
817 order_mock2.get_status.side_effect = ["open"]
818 order_mock3.get_status.side_effect = ["open", "closed"]
819
820 order_mock1.trade = mock.Mock()
821 order_mock2.trade = mock.Mock()
822 order_mock3.trade = mock.Mock()
823
824 m.follow_orders(sleep=sleep)
825
826 order_mock1.trade.update_order.assert_any_call(order_mock1, 1)
827 order_mock1.trade.update_order.assert_any_call(order_mock1, 2)
828 self.assertEqual(2, order_mock1.trade.update_order.call_count)
829 self.assertEqual(3, order_mock1.get_status.call_count)
830
831 order_mock2.trade.update_order.assert_any_call(order_mock2, 1)
832 self.assertEqual(1, order_mock2.trade.update_order.call_count)
833 self.assertEqual(1, order_mock2.get_status.call_count)
834
835 order_mock3.trade.update_order.assert_any_call(order_mock3, 2)
836 self.assertEqual(1, order_mock3.trade.update_order.call_count)
837 self.assertEqual(2, order_mock3.get_status.call_count)
838 m.report.log_stage.assert_called()
839 calls = [
840 mock.call("follow_orders_begin"),
841 mock.call("follow_orders_tick_1"),
842 mock.call("follow_orders_tick_2"),
843 mock.call("follow_orders_tick_3"),
844 mock.call("follow_orders_end"),
845 ]
846 m.report.log_stage.assert_has_calls(calls)
847 m.report.log_orders.assert_called()
848 self.assertEqual(3, m.report.log_orders.call_count)
849 calls = [
850 mock.call([order_mock1, order_mock2], tick=1),
851 mock.call([order_mock1, order_mock3], tick=2),
852 mock.call([order_mock1, order_mock3], tick=3),
853 ]
854 m.report.log_orders.assert_has_calls(calls)
855 calls = [
856 mock.call(order_mock1, 3, finished=True),
857 mock.call(order_mock3, 3, finished=True),
858 ]
859 m.report.log_order.assert_has_calls(calls)
860
861 if sleep is None:
862 if debug:
863 m.report.log_debug_action.assert_called_with("Set follow_orders tick to 7s")
864 time_mock.assert_called_with(7)
865 else:
866 time_mock.assert_called_with(30)
867 else:
868 time_mock.assert_called_with(sleep)
869
870 @mock.patch.object(market.BalanceStore, "fetch_balances")
871 def test_move_balance(self, fetch_balances):
872 for debug in [True, False]:
873 with self.subTest(debug=debug),\
874 mock.patch("market.ReportStore"):
875 m = market.Market(self.ccxt, debug=debug)
876
877 value_from = portfolio.Amount("BTC", "1.0")
878 value_from.linked_to = portfolio.Amount("ETH", "10.0")
879 value_to = portfolio.Amount("BTC", "10.0")
880 trade1 = portfolio.Trade(value_from, value_to, "ETH", m)
881
882 value_from = portfolio.Amount("BTC", "0.0")
883 value_from.linked_to = portfolio.Amount("ETH", "0.0")
884 value_to = portfolio.Amount("BTC", "-3.0")
885 trade2 = portfolio.Trade(value_from, value_to, "ETH", m)
886
887 value_from = portfolio.Amount("USDT", "0.0")
888 value_from.linked_to = portfolio.Amount("XVG", "0.0")
889 value_to = portfolio.Amount("USDT", "-50.0")
890 trade3 = portfolio.Trade(value_from, value_to, "XVG", m)
891
892 m.trades.all = [trade1, trade2, trade3]
893 balance1 = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" })
894 balance2 = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "50" })
895 balance3 = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "15" })
896 m.balances.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3}
897
898 m.move_balances()
899
900 fetch_balances.assert_called_with()
901 m.report.log_move_balances.assert_called_once()
902
903 if debug:
904 m.report.log_debug_action.assert_called()
905 self.assertEqual(3, m.report.log_debug_action.call_count)
906 else:
907 self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin")
908 self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin")
909 self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange")
910
911 def test_store_report(self):
912
913 file_open = mock.mock_open()
914 with self.subTest(file=None), mock.patch("market.open", file_open):
915 m = market.Market(self.ccxt, user_id=1)
916 m.store_report()
917 file_open.assert_not_called()
918
919 file_open = mock.mock_open()
920 m = market.Market(self.ccxt, report_path="present", user_id=1)
921 with self.subTest(file="present"),\
922 mock.patch("market.open", file_open),\
923 mock.patch.object(m, "report") as report,\
924 mock.patch.object(market, "datetime") as time_mock:
925
926 time_mock.now.return_value = datetime.datetime(2018, 2, 25)
927 report.to_json.return_value = "json_content"
928
929 m.store_report()
930
931 file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w")
932 file_open().write.assert_called_once_with("json_content")
933 m.report.to_json.assert_called_once_with()
934
935 m = market.Market(self.ccxt, report_path="error", user_id=1)
936 with self.subTest(file="error"),\
937 mock.patch("market.open") as file_open,\
938 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
939 file_open.side_effect = FileNotFoundError
940
941 m.store_report()
942
943 self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;")
944
945 def test_print_orders(self):
946 m = market.Market(self.ccxt)
947 with mock.patch.object(m.report, "log_stage") as log_stage,\
948 mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\
949 mock.patch.object(m, "prepare_trades") as prepare_trades,\
950 mock.patch.object(m.trades, "prepare_orders") as prepare_orders:
951 m.print_orders()
952
953 log_stage.assert_called_with("print_orders")
954 fetch_balances.assert_called_with(tag="print_orders")
955 prepare_trades.assert_called_with(base_currency="BTC",
956 compute_value="average")
957 prepare_orders.assert_called_with(compute_value="average")
958
959 def test_print_balances(self):
960 m = market.Market(self.ccxt)
961
962 with mock.patch.object(m.balances, "in_currency") as in_currency,\
963 mock.patch.object(m.report, "log_stage") as log_stage,\
964 mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\
965 mock.patch.object(m.report, "print_log") as print_log:
966
967 in_currency.return_value = {
968 "BTC": portfolio.Amount("BTC", "0.65"),
969 "ETH": portfolio.Amount("BTC", "0.3"),
970 }
971
972 m.print_balances()
973
974 log_stage.assert_called_once_with("print_balances")
975 fetch_balances.assert_called_with()
976 print_log.assert_has_calls([
977 mock.call("total:"),
978 mock.call(portfolio.Amount("BTC", "0.95")),
979 ])
980
981 @mock.patch("market.Processor.process")
982 @mock.patch("market.ReportStore.log_error")
983 @mock.patch("market.Market.store_report")
984 def test_process(self, store_report, log_error, process):
985 m = market.Market(self.ccxt)
986 with self.subTest(before=False, after=False):
987 m.process(None)
988
989 process.assert_not_called()
990 store_report.assert_called_once()
991 log_error.assert_not_called()
992
993 process.reset_mock()
994 log_error.reset_mock()
995 store_report.reset_mock()
996 with self.subTest(before=True, after=False):
997 m.process(None, before=True)
998
999 process.assert_called_once_with("sell_all", steps="before")
1000 store_report.assert_called_once()
1001 log_error.assert_not_called()
1002
1003 process.reset_mock()
1004 log_error.reset_mock()
1005 store_report.reset_mock()
1006 with self.subTest(before=False, after=True):
1007 m.process(None, after=True)
1008
1009 process.assert_called_once_with("sell_all", steps="after")
1010 store_report.assert_called_once()
1011 log_error.assert_not_called()
1012
1013 process.reset_mock()
1014 log_error.reset_mock()
1015 store_report.reset_mock()
1016 with self.subTest(before=True, after=True):
1017 m.process(None, before=True, after=True)
1018
1019 process.assert_has_calls([
1020 mock.call("sell_all", steps="before"),
1021 mock.call("sell_all", steps="after"),
1022 ])
1023 store_report.assert_called_once()
1024 log_error.assert_not_called()
1025
1026 process.reset_mock()
1027 log_error.reset_mock()
1028 store_report.reset_mock()
1029 with self.subTest(action="print_balances"),\
1030 mock.patch.object(m, "print_balances") as print_balances:
1031 m.process(["print_balances"])
1032
1033 process.assert_not_called()
1034 log_error.assert_not_called()
1035 store_report.assert_called_once()
1036 print_balances.assert_called_once_with()
1037
1038 log_error.reset_mock()
1039 store_report.reset_mock()
1040 with self.subTest(action="print_orders"),\
1041 mock.patch.object(m, "print_orders") as print_orders,\
1042 mock.patch.object(m, "print_balances") as print_balances:
1043 m.process(["print_orders", "print_balances"])
1044
1045 process.assert_not_called()
1046 log_error.assert_not_called()
1047 store_report.assert_called_once()
1048 print_orders.assert_called_once_with()
1049 print_balances.assert_called_once_with()
1050
1051 log_error.reset_mock()
1052 store_report.reset_mock()
1053 with self.subTest(action="unknown"):
1054 m.process(["unknown"])
1055 log_error.assert_called_once_with("market_process", message="Unknown action unknown")
1056 store_report.assert_called_once()
1057
1058 log_error.reset_mock()
1059 store_report.reset_mock()
1060 with self.subTest(unhandled_exception=True):
1061 process.side_effect = Exception("bouh")
1062
1063 m.process(None, before=True)
1064 log_error.assert_called_with("market_process", exception=mock.ANY)
1065 store_report.assert_called_once()
1066
1067 @unittest.skipUnless("unit" in limits, "Unit skipped")
1068 class TradeStoreTest(WebMockTestCase):
1069 def test_compute_trades(self):
1070 self.m.balances.currencies.return_value = ["XMR", "DASH", "XVG", "BTC", "ETH"]
1071
1072 values_in_base = {
1073 "XMR": portfolio.Amount("BTC", D("0.9")),
1074 "DASH": portfolio.Amount("BTC", D("0.4")),
1075 "XVG": portfolio.Amount("BTC", D("-0.5")),
1076 "BTC": portfolio.Amount("BTC", D("0.5")),
1077 }
1078 new_repartition = {
1079 "DASH": portfolio.Amount("BTC", D("0.5")),
1080 "XVG": portfolio.Amount("BTC", D("0.1")),
1081 "BTC": portfolio.Amount("BTC", D("0.4")),
1082 "ETH": portfolio.Amount("BTC", D("0.3")),
1083 }
1084 side_effect = [
1085 (True, 1),
1086 (False, 2),
1087 (False, 3),
1088 (True, 4),
1089 (True, 5)
1090 ]
1091
1092 with mock.patch.object(market.TradeStore, "trade_if_matching") as trade_if_matching:
1093 trade_store = market.TradeStore(self.m)
1094 trade_if_matching.side_effect = side_effect
1095
1096 trade_store.compute_trades(values_in_base,
1097 new_repartition, only="only")
1098
1099 self.assertEqual(5, trade_if_matching.call_count)
1100 self.assertEqual(3, len(trade_store.all))
1101 self.assertEqual([1, 4, 5], trade_store.all)
1102 self.m.report.log_trades.assert_called_with(side_effect, "only")
1103
1104 def test_trade_if_matching(self):
1105
1106 with self.subTest(only="nope"):
1107 trade_store = market.TradeStore(self.m)
1108 result = trade_store.trade_if_matching(
1109 portfolio.Amount("BTC", D("0")),
1110 portfolio.Amount("BTC", D("0.3")),
1111 "ETH", only="nope")
1112 self.assertEqual(False, result[0])
1113 self.assertIsInstance(result[1], portfolio.Trade)
1114
1115 with self.subTest(only=None):
1116 trade_store = market.TradeStore(self.m)
1117 result = trade_store.trade_if_matching(
1118 portfolio.Amount("BTC", D("0")),
1119 portfolio.Amount("BTC", D("0.3")),
1120 "ETH", only=None)
1121 self.assertEqual(True, result[0])
1122
1123 with self.subTest(only="acquire"):
1124 trade_store = market.TradeStore(self.m)
1125 result = trade_store.trade_if_matching(
1126 portfolio.Amount("BTC", D("0")),
1127 portfolio.Amount("BTC", D("0.3")),
1128 "ETH", only="acquire")
1129 self.assertEqual(True, result[0])
1130
1131 with self.subTest(only="dispose"):
1132 trade_store = market.TradeStore(self.m)
1133 result = trade_store.trade_if_matching(
1134 portfolio.Amount("BTC", D("0")),
1135 portfolio.Amount("BTC", D("0.3")),
1136 "ETH", only="dispose")
1137 self.assertEqual(False, result[0])
1138
1139 def test_prepare_orders(self):
1140 trade_store = market.TradeStore(self.m)
1141
1142 trade_mock1 = mock.Mock()
1143 trade_mock2 = mock.Mock()
1144 trade_mock3 = mock.Mock()
1145
1146 trade_mock1.prepare_order.return_value = 1
1147 trade_mock2.prepare_order.return_value = 2
1148 trade_mock3.prepare_order.return_value = 3
1149
1150 trade_mock1.pending = True
1151 trade_mock2.pending = True
1152 trade_mock3.pending = False
1153
1154 trade_store.all.append(trade_mock1)
1155 trade_store.all.append(trade_mock2)
1156 trade_store.all.append(trade_mock3)
1157
1158 trade_store.prepare_orders()
1159 trade_mock1.prepare_order.assert_called_with(compute_value="default")
1160 trade_mock2.prepare_order.assert_called_with(compute_value="default")
1161 trade_mock3.prepare_order.assert_not_called()
1162 self.m.report.log_orders.assert_called_once_with([1, 2], None, "default")
1163
1164 self.m.report.log_orders.reset_mock()
1165
1166 trade_store.prepare_orders(compute_value="bla")
1167 trade_mock1.prepare_order.assert_called_with(compute_value="bla")
1168 trade_mock2.prepare_order.assert_called_with(compute_value="bla")
1169 self.m.report.log_orders.assert_called_once_with([1, 2], None, "bla")
1170
1171 trade_mock1.prepare_order.reset_mock()
1172 trade_mock2.prepare_order.reset_mock()
1173 self.m.report.log_orders.reset_mock()
1174
1175 trade_mock1.action = "foo"
1176 trade_mock2.action = "bar"
1177 trade_store.prepare_orders(only="bar")
1178 trade_mock1.prepare_order.assert_not_called()
1179 trade_mock2.prepare_order.assert_called_with(compute_value="default")
1180 self.m.report.log_orders.assert_called_once_with([2], "bar", "default")
1181
1182 def test_print_all_with_order(self):
1183 trade_mock1 = mock.Mock()
1184 trade_mock2 = mock.Mock()
1185 trade_mock3 = mock.Mock()
1186 trade_store = market.TradeStore(self.m)
1187 trade_store.all = [trade_mock1, trade_mock2, trade_mock3]
1188
1189 trade_store.print_all_with_order()
1190
1191 trade_mock1.print_with_order.assert_called()
1192 trade_mock2.print_with_order.assert_called()
1193 trade_mock3.print_with_order.assert_called()
1194
1195 def test_run_orders(self):
1196 with mock.patch.object(market.TradeStore, "all_orders") as all_orders:
1197 order_mock1 = mock.Mock()
1198 order_mock2 = mock.Mock()
1199 order_mock3 = mock.Mock()
1200 trade_store = market.TradeStore(self.m)
1201
1202 all_orders.return_value = [order_mock1, order_mock2, order_mock3]
1203
1204 trade_store.run_orders()
1205
1206 all_orders.assert_called_with(state="pending")
1207
1208 order_mock1.run.assert_called()
1209 order_mock2.run.assert_called()
1210 order_mock3.run.assert_called()
1211
1212 self.m.report.log_stage.assert_called_with("run_orders")
1213 self.m.report.log_orders.assert_called_with([order_mock1, order_mock2,
1214 order_mock3])
1215
1216 def test_all_orders(self):
1217 trade_mock1 = mock.Mock()
1218 trade_mock2 = mock.Mock()
1219
1220 order_mock1 = mock.Mock()
1221 order_mock2 = mock.Mock()
1222 order_mock3 = mock.Mock()
1223
1224 trade_mock1.orders = [order_mock1, order_mock2]
1225 trade_mock2.orders = [order_mock3]
1226
1227 order_mock1.status = "pending"
1228 order_mock2.status = "open"
1229 order_mock3.status = "open"
1230
1231 trade_store = market.TradeStore(self.m)
1232 trade_store.all.append(trade_mock1)
1233 trade_store.all.append(trade_mock2)
1234
1235 orders = trade_store.all_orders()
1236 self.assertEqual(3, len(orders))
1237
1238 open_orders = trade_store.all_orders(state="open")
1239 self.assertEqual(2, len(open_orders))
1240 self.assertEqual([order_mock2, order_mock3], open_orders)
1241
1242 def test_update_all_orders_status(self):
1243 with mock.patch.object(market.TradeStore, "all_orders") as all_orders:
1244 order_mock1 = mock.Mock()
1245 order_mock2 = mock.Mock()
1246 order_mock3 = mock.Mock()
1247
1248 all_orders.return_value = [order_mock1, order_mock2, order_mock3]
1249
1250 trade_store = market.TradeStore(self.m)
1251
1252 trade_store.update_all_orders_status()
1253 all_orders.assert_called_with(state="open")
1254
1255 order_mock1.get_status.assert_called()
1256 order_mock2.get_status.assert_called()
1257 order_mock3.get_status.assert_called()
1258
1259 def test_close_trades(self):
1260 trade_mock1 = mock.Mock()
1261 trade_mock2 = mock.Mock()
1262 trade_mock3 = mock.Mock()
1263
1264 trade_store = market.TradeStore(self.m)
1265
1266 trade_store.all.append(trade_mock1)
1267 trade_store.all.append(trade_mock2)
1268 trade_store.all.append(trade_mock3)
1269
1270 trade_store.close_trades()
1271
1272 trade_mock1.close.assert_called_once_with()
1273 trade_mock2.close.assert_called_once_with()
1274 trade_mock3.close.assert_called_once_with()
1275
1276 def test_pending(self):
1277 trade_mock1 = mock.Mock()
1278 trade_mock1.pending = True
1279 trade_mock2 = mock.Mock()
1280 trade_mock2.pending = True
1281 trade_mock3 = mock.Mock()
1282 trade_mock3.pending = False
1283
1284 trade_store = market.TradeStore(self.m)
1285
1286 trade_store.all.append(trade_mock1)
1287 trade_store.all.append(trade_mock2)
1288 trade_store.all.append(trade_mock3)
1289
1290 self.assertEqual([trade_mock1, trade_mock2], trade_store.pending)
1291
1292 @unittest.skipUnless("unit" in limits, "Unit skipped")
1293 class BalanceStoreTest(WebMockTestCase):
1294 def setUp(self):
1295 super(BalanceStoreTest, self).setUp()
1296
1297 self.fetch_balance = {
1298 "ETC": {
1299 "exchange_free": 0,
1300 "exchange_used": 0,
1301 "exchange_total": 0,
1302 "margin_total": 0,
1303 },
1304 "USDT": {
1305 "exchange_free": D("6.0"),
1306 "exchange_used": D("1.2"),
1307 "exchange_total": D("7.2"),
1308 "margin_total": 0,
1309 },
1310 "XVG": {
1311 "exchange_free": 16,
1312 "exchange_used": 0,
1313 "exchange_total": 16,
1314 "margin_total": 0,
1315 },
1316 "XMR": {
1317 "exchange_free": 0,
1318 "exchange_used": 0,
1319 "exchange_total": 0,
1320 "margin_total": D("-1.0"),
1321 "margin_free": 0,
1322 },
1323 }
1324
1325 def test_in_currency(self):
1326 self.m.get_ticker.return_value = {
1327 "bid": D("0.09"),
1328 "ask": D("0.11"),
1329 "average": D("0.1"),
1330 }
1331
1332 balance_store = market.BalanceStore(self.m)
1333 balance_store.all = {
1334 "BTC": portfolio.Balance("BTC", {
1335 "total": "0.65",
1336 "exchange_total":"0.65",
1337 "exchange_free": "0.35",
1338 "exchange_used": "0.30"}),
1339 "ETH": portfolio.Balance("ETH", {
1340 "total": 3,
1341 "exchange_total": 3,
1342 "exchange_free": 3,
1343 "exchange_used": 0}),
1344 }
1345
1346 amounts = balance_store.in_currency("BTC")
1347 self.assertEqual("BTC", amounts["ETH"].currency)
1348 self.assertEqual(D("0.65"), amounts["BTC"].value)
1349 self.assertEqual(D("0.30"), amounts["ETH"].value)
1350 self.m.report.log_tickers.assert_called_once_with(amounts, "BTC",
1351 "average", "total")
1352 self.m.report.log_tickers.reset_mock()
1353
1354 amounts = balance_store.in_currency("BTC", compute_value="bid")
1355 self.assertEqual(D("0.65"), amounts["BTC"].value)
1356 self.assertEqual(D("0.27"), amounts["ETH"].value)
1357 self.m.report.log_tickers.assert_called_once_with(amounts, "BTC",
1358 "bid", "total")
1359 self.m.report.log_tickers.reset_mock()
1360
1361 amounts = balance_store.in_currency("BTC", compute_value="bid", type="exchange_used")
1362 self.assertEqual(D("0.30"), amounts["BTC"].value)
1363 self.assertEqual(0, amounts["ETH"].value)
1364 self.m.report.log_tickers.assert_called_once_with(amounts, "BTC",
1365 "bid", "exchange_used")
1366 self.m.report.log_tickers.reset_mock()
1367
1368 def test_fetch_balances(self):
1369 self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance
1370
1371 balance_store = market.BalanceStore(self.m)
1372
1373 balance_store.fetch_balances()
1374 self.assertNotIn("ETC", balance_store.currencies())
1375 self.assertListEqual(["USDT", "XVG", "XMR"], list(balance_store.currencies()))
1376
1377 balance_store.all["ETC"] = portfolio.Balance("ETC", {
1378 "exchange_total": "1", "exchange_free": "0",
1379 "exchange_used": "1" })
1380 balance_store.fetch_balances(tag="foo")
1381 self.assertEqual(0, balance_store.all["ETC"].total)
1382 self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies()))
1383 self.m.report.log_balances.assert_called_with(tag="foo")
1384
1385 @mock.patch.object(portfolio.Portfolio, "repartition")
1386 def test_dispatch_assets(self, repartition):
1387 self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance
1388
1389 balance_store = market.BalanceStore(self.m)
1390 balance_store.fetch_balances()
1391
1392 self.assertNotIn("XEM", balance_store.currencies())
1393
1394 repartition_hash = {
1395 "XEM": (D("0.75"), "long"),
1396 "BTC": (D("0.26"), "long"),
1397 "DASH": (D("0.10"), "short"),
1398 }
1399 repartition.return_value = repartition_hash
1400
1401 amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1"))
1402 repartition.assert_called_with(self.m, liquidity="medium")
1403 self.assertIn("XEM", balance_store.currencies())
1404 self.assertEqual(D("2.6"), amounts["BTC"].value)
1405 self.assertEqual(D("7.5"), amounts["XEM"].value)
1406 self.assertEqual(D("-1.0"), amounts["DASH"].value)
1407 self.m.report.log_balances.assert_called_with(tag=None)
1408 self.m.report.log_dispatch.assert_called_once_with(portfolio.Amount("BTC",
1409 "11.1"), amounts, "medium", repartition_hash)
1410
1411 def test_currencies(self):
1412 balance_store = market.BalanceStore(self.m)
1413
1414 balance_store.all = {
1415 "BTC": portfolio.Balance("BTC", {
1416 "total": "0.65",
1417 "exchange_total":"0.65",
1418 "exchange_free": "0.35",
1419 "exchange_used": "0.30"}),
1420 "ETH": portfolio.Balance("ETH", {
1421 "total": 3,
1422 "exchange_total": 3,
1423 "exchange_free": 3,
1424 "exchange_used": 0}),
1425 }
1426 self.assertListEqual(["BTC", "ETH"], list(balance_store.currencies()))
1427
1428 def test_as_json(self):
1429 balance_mock1 = mock.Mock()
1430 balance_mock1.as_json.return_value = 1
1431
1432 balance_mock2 = mock.Mock()
1433 balance_mock2.as_json.return_value = 2
1434
1435 balance_store = market.BalanceStore(self.m)
1436 balance_store.all = {
1437 "BTC": balance_mock1,
1438 "ETH": balance_mock2,
1439 }
1440
1441 as_json = balance_store.as_json()
1442 self.assertEqual(1, as_json["BTC"])
1443 self.assertEqual(2, as_json["ETH"])
1444
1445
1446 @unittest.skipUnless("unit" in limits, "Unit skipped")
1447 class ComputationTest(WebMockTestCase):
1448 def test_compute_value(self):
1449 compute = mock.Mock()
1450 portfolio.Computation.compute_value("foo", "buy", compute_value=compute)
1451 compute.assert_called_with("foo", "ask")
1452
1453 compute.reset_mock()
1454 portfolio.Computation.compute_value("foo", "sell", compute_value=compute)
1455 compute.assert_called_with("foo", "bid")
1456
1457 compute.reset_mock()
1458 portfolio.Computation.compute_value("foo", "ask", compute_value=compute)
1459 compute.assert_called_with("foo", "ask")
1460
1461 compute.reset_mock()
1462 portfolio.Computation.compute_value("foo", "bid", compute_value=compute)
1463 compute.assert_called_with("foo", "bid")
1464
1465 compute.reset_mock()
1466 portfolio.Computation.computations["test"] = compute
1467 portfolio.Computation.compute_value("foo", "bid", compute_value="test")
1468 compute.assert_called_with("foo", "bid")
1469
1470
1471 @unittest.skipUnless("unit" in limits, "Unit skipped")
1472 class TradeTest(WebMockTestCase):
1473
1474 def test_values_assertion(self):
1475 value_from = portfolio.Amount("BTC", "1.0")
1476 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1477 value_to = portfolio.Amount("BTC", "1.0")
1478 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1479 self.assertEqual("BTC", trade.base_currency)
1480 self.assertEqual("ETH", trade.currency)
1481 self.assertEqual(self.m, trade.market)
1482
1483 with self.assertRaises(AssertionError):
1484 portfolio.Trade(value_from, -value_to, "ETH", self.m)
1485 with self.assertRaises(AssertionError):
1486 portfolio.Trade(value_from, value_to, "ETC", self.m)
1487 with self.assertRaises(AssertionError):
1488 value_from.currency = "ETH"
1489 portfolio.Trade(value_from, value_to, "ETH", self.m)
1490 value_from.currency = "BTC"
1491 with self.assertRaises(AssertionError):
1492 value_from2 = portfolio.Amount("BTC", "1.0")
1493 portfolio.Trade(value_from2, value_to, "ETH", self.m)
1494
1495 value_from = portfolio.Amount("BTC", 0)
1496 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1497 self.assertEqual(0, trade.value_from.linked_to)
1498
1499 def test_action(self):
1500 value_from = portfolio.Amount("BTC", "1.0")
1501 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1502 value_to = portfolio.Amount("BTC", "1.0")
1503 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1504
1505 self.assertIsNone(trade.action)
1506
1507 value_from = portfolio.Amount("BTC", "1.0")
1508 value_from.linked_to = portfolio.Amount("BTC", "1.0")
1509 value_to = portfolio.Amount("BTC", "2.0")
1510 trade = portfolio.Trade(value_from, value_to, "BTC", self.m)
1511
1512 self.assertIsNone(trade.action)
1513
1514 value_from = portfolio.Amount("BTC", "0.5")
1515 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1516 value_to = portfolio.Amount("BTC", "1.0")
1517 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1518
1519 self.assertEqual("acquire", trade.action)
1520
1521 value_from = portfolio.Amount("BTC", "0")
1522 value_from.linked_to = portfolio.Amount("ETH", "0")
1523 value_to = portfolio.Amount("BTC", "-1.0")
1524 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1525
1526 self.assertEqual("acquire", trade.action)
1527
1528 def test_order_action(self):
1529 value_from = portfolio.Amount("BTC", "0.5")
1530 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1531 value_to = portfolio.Amount("BTC", "1.0")
1532 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1533
1534 self.assertEqual("buy", trade.order_action(False))
1535 self.assertEqual("sell", trade.order_action(True))
1536
1537 value_from = portfolio.Amount("BTC", "0")
1538 value_from.linked_to = portfolio.Amount("ETH", "0")
1539 value_to = portfolio.Amount("BTC", "-1.0")
1540 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1541
1542 self.assertEqual("sell", trade.order_action(False))
1543 self.assertEqual("buy", trade.order_action(True))
1544
1545 def test_trade_type(self):
1546 value_from = portfolio.Amount("BTC", "0.5")
1547 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1548 value_to = portfolio.Amount("BTC", "1.0")
1549 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1550
1551 self.assertEqual("long", trade.trade_type)
1552
1553 value_from = portfolio.Amount("BTC", "0")
1554 value_from.linked_to = portfolio.Amount("ETH", "0")
1555 value_to = portfolio.Amount("BTC", "-1.0")
1556 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1557
1558 self.assertEqual("short", trade.trade_type)
1559
1560 def test_is_fullfiled(self):
1561 value_from = portfolio.Amount("BTC", "0.5")
1562 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1563 value_to = portfolio.Amount("BTC", "1.0")
1564 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1565
1566 order1 = mock.Mock()
1567 order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")
1568
1569 order2 = mock.Mock()
1570 order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
1571 trade.orders.append(order1)
1572 trade.orders.append(order2)
1573
1574 self.assertFalse(trade.is_fullfiled)
1575
1576 order3 = mock.Mock()
1577 order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
1578 trade.orders.append(order3)
1579
1580 self.assertTrue(trade.is_fullfiled)
1581
1582 def test_filled_amount(self):
1583 value_from = portfolio.Amount("BTC", "0.5")
1584 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1585 value_to = portfolio.Amount("BTC", "1.0")
1586 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1587
1588 order1 = mock.Mock()
1589 order1.filled_amount.return_value = portfolio.Amount("ETH", "0.3")
1590
1591 order2 = mock.Mock()
1592 order2.filled_amount.return_value = portfolio.Amount("ETH", "0.01")
1593 trade.orders.append(order1)
1594 trade.orders.append(order2)
1595
1596 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount())
1597 order1.filled_amount.assert_called_with(in_base_currency=False)
1598 order2.filled_amount.assert_called_with(in_base_currency=False)
1599
1600 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=False))
1601 order1.filled_amount.assert_called_with(in_base_currency=False)
1602 order2.filled_amount.assert_called_with(in_base_currency=False)
1603
1604 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=True))
1605 order1.filled_amount.assert_called_with(in_base_currency=True)
1606 order2.filled_amount.assert_called_with(in_base_currency=True)
1607
1608 @mock.patch.object(portfolio.Computation, "compute_value")
1609 @mock.patch.object(portfolio.Trade, "filled_amount")
1610 @mock.patch.object(portfolio, "Order")
1611 def test_prepare_order(self, Order, filled_amount, compute_value):
1612 Order.return_value = "Order"
1613
1614 with self.subTest(desc="Nothing to do"):
1615 value_from = portfolio.Amount("BTC", "10")
1616 value_from.rate = D("0.1")
1617 value_from.linked_to = portfolio.Amount("FOO", "100")
1618 value_to = portfolio.Amount("BTC", "10")
1619 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
1620
1621 trade.prepare_order()
1622
1623 filled_amount.assert_not_called()
1624 compute_value.assert_not_called()
1625 self.assertEqual(0, len(trade.orders))
1626 Order.assert_not_called()
1627
1628 self.m.get_ticker.return_value = { "inverted": False }
1629 with self.subTest(desc="Already filled"):
1630 filled_amount.return_value = portfolio.Amount("FOO", "100")
1631 compute_value.return_value = D("0.125")
1632
1633 value_from = portfolio.Amount("BTC", "10")
1634 value_from.rate = D("0.1")
1635 value_from.linked_to = portfolio.Amount("FOO", "100")
1636 value_to = portfolio.Amount("BTC", "0")
1637 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
1638
1639 trade.prepare_order()
1640
1641 filled_amount.assert_called_with(in_base_currency=False)
1642 compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
1643 self.assertEqual(0, len(trade.orders))
1644 self.m.report.log_error.assert_called_with("prepare_order", message=mock.ANY)
1645 Order.assert_not_called()
1646
1647 with self.subTest(action="dispose", inverted=False):
1648 filled_amount.return_value = portfolio.Amount("FOO", "60")
1649 compute_value.return_value = D("0.125")
1650
1651 value_from = portfolio.Amount("BTC", "10")
1652 value_from.rate = D("0.1")
1653 value_from.linked_to = portfolio.Amount("FOO", "100")
1654 value_to = portfolio.Amount("BTC", "1")
1655 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
1656
1657 trade.prepare_order()
1658
1659 filled_amount.assert_called_with(in_base_currency=False)
1660 compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
1661 self.assertEqual(1, len(trade.orders))
1662 Order.assert_called_with("sell", portfolio.Amount("FOO", 30),
1663 D("0.125"), "BTC", "long", self.m,
1664 trade, close_if_possible=False)
1665
1666 with self.subTest(action="dispose", inverted=False, close_if_possible=True):
1667 filled_amount.return_value = portfolio.Amount("FOO", "60")
1668 compute_value.return_value = D("0.125")
1669
1670 value_from = portfolio.Amount("BTC", "10")
1671 value_from.rate = D("0.1")
1672 value_from.linked_to = portfolio.Amount("FOO", "100")
1673 value_to = portfolio.Amount("BTC", "1")
1674 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
1675
1676 trade.prepare_order(close_if_possible=True)
1677
1678 filled_amount.assert_called_with(in_base_currency=False)
1679 compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
1680 self.assertEqual(1, len(trade.orders))
1681 Order.assert_called_with("sell", portfolio.Amount("FOO", 30),
1682 D("0.125"), "BTC", "long", self.m,
1683 trade, close_if_possible=True)
1684
1685 with self.subTest(action="acquire", inverted=False):
1686 filled_amount.return_value = portfolio.Amount("BTC", "3")
1687 compute_value.return_value = D("0.125")
1688
1689 value_from = portfolio.Amount("BTC", "1")
1690 value_from.rate = D("0.1")
1691 value_from.linked_to = portfolio.Amount("FOO", "10")
1692 value_to = portfolio.Amount("BTC", "10")
1693 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
1694
1695 trade.prepare_order()
1696
1697 filled_amount.assert_called_with(in_base_currency=True)
1698 compute_value.assert_called_with(self.m.get_ticker.return_value, "buy", compute_value="default")
1699 self.assertEqual(1, len(trade.orders))
1700
1701 Order.assert_called_with("buy", portfolio.Amount("FOO", 48),
1702 D("0.125"), "BTC", "long", self.m,
1703 trade, close_if_possible=False)
1704
1705 with self.subTest(close_if_possible=True):
1706 filled_amount.return_value = portfolio.Amount("FOO", "0")
1707 compute_value.return_value = D("0.125")
1708
1709 value_from = portfolio.Amount("BTC", "10")
1710 value_from.rate = D("0.1")
1711 value_from.linked_to = portfolio.Amount("FOO", "100")
1712 value_to = portfolio.Amount("BTC", "0")
1713 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
1714
1715 trade.prepare_order()
1716
1717 filled_amount.assert_called_with(in_base_currency=False)
1718 compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
1719 self.assertEqual(1, len(trade.orders))
1720 Order.assert_called_with("sell", portfolio.Amount("FOO", 100),
1721 D("0.125"), "BTC", "long", self.m,
1722 trade, close_if_possible=True)
1723
1724 self.m.get_ticker.return_value = { "inverted": True, "original": {} }
1725 with self.subTest(action="dispose", inverted=True):
1726 filled_amount.return_value = portfolio.Amount("FOO", "300")
1727 compute_value.return_value = D("125")
1728
1729 value_from = portfolio.Amount("BTC", "10")
1730 value_from.rate = D("0.01")
1731 value_from.linked_to = portfolio.Amount("FOO", "1000")
1732 value_to = portfolio.Amount("BTC", "1")
1733 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
1734
1735 trade.prepare_order(compute_value="foo")
1736
1737 filled_amount.assert_called_with(in_base_currency=True)
1738 compute_value.assert_called_with(self.m.get_ticker.return_value["original"], "buy", compute_value="foo")
1739 self.assertEqual(1, len(trade.orders))
1740 Order.assert_called_with("buy", portfolio.Amount("BTC", D("4.8")),
1741 D("125"), "FOO", "long", self.m,
1742 trade, close_if_possible=False)
1743
1744 with self.subTest(action="acquire", inverted=True):
1745 filled_amount.return_value = portfolio.Amount("BTC", "4")
1746 compute_value.return_value = D("125")
1747
1748 value_from = portfolio.Amount("BTC", "1")
1749 value_from.rate = D("0.01")
1750 value_from.linked_to = portfolio.Amount("FOO", "100")
1751 value_to = portfolio.Amount("BTC", "10")
1752 trade = portfolio.Trade(value_from, value_to, "FOO", self.m)
1753
1754 trade.prepare_order(compute_value="foo")
1755
1756 filled_amount.assert_called_with(in_base_currency=False)
1757 compute_value.assert_called_with(self.m.get_ticker.return_value["original"], "sell", compute_value="foo")
1758 self.assertEqual(1, len(trade.orders))
1759 Order.assert_called_with("sell", portfolio.Amount("BTC", D("5")),
1760 D("125"), "FOO", "long", self.m,
1761 trade, close_if_possible=False)
1762
1763
1764 @mock.patch.object(portfolio.Trade, "prepare_order")
1765 def test_update_order(self, prepare_order):
1766 order_mock = mock.Mock()
1767 new_order_mock = mock.Mock()
1768
1769 value_from = portfolio.Amount("BTC", "0.5")
1770 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1771 value_to = portfolio.Amount("BTC", "1.0")
1772 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1773 prepare_order.return_value = new_order_mock
1774
1775 for i in [0, 1, 3, 4, 6]:
1776 with self.subTest(tick=i):
1777 trade.update_order(order_mock, i)
1778 order_mock.cancel.assert_not_called()
1779 new_order_mock.run.assert_not_called()
1780 self.m.report.log_order.assert_called_once_with(order_mock, i,
1781 update="waiting", compute_value=None, new_order=None)
1782
1783 order_mock.reset_mock()
1784 new_order_mock.reset_mock()
1785 trade.orders = []
1786 self.m.report.log_order.reset_mock()
1787
1788 trade.update_order(order_mock, 2)
1789 order_mock.cancel.assert_called()
1790 new_order_mock.run.assert_called()
1791 prepare_order.assert_called()
1792 self.m.report.log_order.assert_called()
1793 self.assertEqual(2, self.m.report.log_order.call_count)
1794 calls = [
1795 mock.call(order_mock, 2, update="adjusting",
1796 compute_value=mock.ANY,
1797 new_order=new_order_mock),
1798 mock.call(order_mock, 2, new_order=new_order_mock),
1799 ]
1800 self.m.report.log_order.assert_has_calls(calls)
1801
1802 order_mock.reset_mock()
1803 new_order_mock.reset_mock()
1804 trade.orders = []
1805 self.m.report.log_order.reset_mock()
1806
1807 trade.update_order(order_mock, 5)
1808 order_mock.cancel.assert_called()
1809 new_order_mock.run.assert_called()
1810 prepare_order.assert_called()
1811 self.assertEqual(2, self.m.report.log_order.call_count)
1812 self.m.report.log_order.assert_called()
1813 calls = [
1814 mock.call(order_mock, 5, update="adjusting",
1815 compute_value=mock.ANY,
1816 new_order=new_order_mock),
1817 mock.call(order_mock, 5, new_order=new_order_mock),
1818 ]
1819 self.m.report.log_order.assert_has_calls(calls)
1820
1821 order_mock.reset_mock()
1822 new_order_mock.reset_mock()
1823 trade.orders = []
1824 self.m.report.log_order.reset_mock()
1825
1826 trade.update_order(order_mock, 7)
1827 order_mock.cancel.assert_called()
1828 new_order_mock.run.assert_called()
1829 prepare_order.assert_called_with(compute_value="default")
1830 self.m.report.log_order.assert_called()
1831 self.assertEqual(2, self.m.report.log_order.call_count)
1832 calls = [
1833 mock.call(order_mock, 7, update="market_fallback",
1834 compute_value='default',
1835 new_order=new_order_mock),
1836 mock.call(order_mock, 7, new_order=new_order_mock),
1837 ]
1838 self.m.report.log_order.assert_has_calls(calls)
1839
1840 order_mock.reset_mock()
1841 new_order_mock.reset_mock()
1842 trade.orders = []
1843 self.m.report.log_order.reset_mock()
1844
1845 for i in [10, 13, 16]:
1846 with self.subTest(tick=i):
1847 trade.update_order(order_mock, i)
1848 order_mock.cancel.assert_called()
1849 new_order_mock.run.assert_called()
1850 prepare_order.assert_called_with(compute_value="default")
1851 self.m.report.log_order.assert_called()
1852 self.assertEqual(2, self.m.report.log_order.call_count)
1853 calls = [
1854 mock.call(order_mock, i, update="market_adjust",
1855 compute_value='default',
1856 new_order=new_order_mock),
1857 mock.call(order_mock, i, new_order=new_order_mock),
1858 ]
1859 self.m.report.log_order.assert_has_calls(calls)
1860
1861 order_mock.reset_mock()
1862 new_order_mock.reset_mock()
1863 trade.orders = []
1864 self.m.report.log_order.reset_mock()
1865
1866 for i in [8, 9, 11, 12]:
1867 with self.subTest(tick=i):
1868 trade.update_order(order_mock, i)
1869 order_mock.cancel.assert_not_called()
1870 new_order_mock.run.assert_not_called()
1871 self.m.report.log_order.assert_called_once_with(order_mock, i, update="waiting",
1872 compute_value=None, new_order=None)
1873
1874 order_mock.reset_mock()
1875 new_order_mock.reset_mock()
1876 trade.orders = []
1877 self.m.report.log_order.reset_mock()
1878
1879
1880 def test_print_with_order(self):
1881 value_from = portfolio.Amount("BTC", "0.5")
1882 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1883 value_to = portfolio.Amount("BTC", "1.0")
1884 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1885
1886 order_mock1 = mock.Mock()
1887 order_mock1.__repr__ = mock.Mock()
1888 order_mock1.__repr__.return_value = "Mock 1"
1889 order_mock2 = mock.Mock()
1890 order_mock2.__repr__ = mock.Mock()
1891 order_mock2.__repr__.return_value = "Mock 2"
1892 order_mock1.mouvements = []
1893 mouvement_mock1 = mock.Mock()
1894 mouvement_mock1.__repr__ = mock.Mock()
1895 mouvement_mock1.__repr__.return_value = "Mouvement 1"
1896 mouvement_mock2 = mock.Mock()
1897 mouvement_mock2.__repr__ = mock.Mock()
1898 mouvement_mock2.__repr__.return_value = "Mouvement 2"
1899 order_mock2.mouvements = [
1900 mouvement_mock1, mouvement_mock2
1901 ]
1902 trade.orders.append(order_mock1)
1903 trade.orders.append(order_mock2)
1904
1905 with mock.patch.object(trade, "filled_amount") as filled:
1906 filled.return_value = portfolio.Amount("BTC", "0.1")
1907
1908 trade.print_with_order()
1909
1910 self.m.report.print_log.assert_called()
1911 calls = self.m.report.print_log.mock_calls
1912 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(calls[0][1][0]))
1913 self.assertEqual("\tMock 1", str(calls[1][1][0]))
1914 self.assertEqual("\tMock 2", str(calls[2][1][0]))
1915 self.assertEqual("\t\tMouvement 1", str(calls[3][1][0]))
1916 self.assertEqual("\t\tMouvement 2", str(calls[4][1][0]))
1917
1918 self.m.report.print_log.reset_mock()
1919
1920 filled.return_value = portfolio.Amount("BTC", "0.5")
1921 trade.print_with_order()
1922 calls = self.m.report.print_log.mock_calls
1923 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire ✔)", str(calls[0][1][0]))
1924
1925 self.m.report.print_log.reset_mock()
1926
1927 filled.return_value = portfolio.Amount("BTC", "0.1")
1928 trade.closed = True
1929 trade.print_with_order()
1930 calls = self.m.report.print_log.mock_calls
1931 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire ❌)", str(calls[0][1][0]))
1932
1933 def test_close(self):
1934 value_from = portfolio.Amount("BTC", "0.5")
1935 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1936 value_to = portfolio.Amount("BTC", "1.0")
1937 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1938 order1 = mock.Mock()
1939 trade.orders.append(order1)
1940
1941 trade.close()
1942
1943 self.assertEqual(True, trade.closed)
1944 order1.cancel.assert_called_once_with()
1945
1946 def test_pending(self):
1947 value_from = portfolio.Amount("BTC", "0.5")
1948 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1949 value_to = portfolio.Amount("BTC", "1.0")
1950 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1951
1952 trade.closed = True
1953 self.assertEqual(False, trade.pending)
1954
1955 trade.closed = False
1956 self.assertEqual(True, trade.pending)
1957
1958 order1 = mock.Mock()
1959 order1.filled_amount.return_value = portfolio.Amount("BTC", "0.5")
1960 trade.orders.append(order1)
1961 self.assertEqual(False, trade.pending)
1962
1963 def test__repr(self):
1964 value_from = portfolio.Amount("BTC", "0.5")
1965 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1966 value_to = portfolio.Amount("BTC", "1.0")
1967 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1968
1969 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(trade))
1970
1971 def test_as_json(self):
1972 value_from = portfolio.Amount("BTC", "0.5")
1973 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1974 value_to = portfolio.Amount("BTC", "1.0")
1975 trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
1976
1977 as_json = trade.as_json()
1978 self.assertEqual("acquire", as_json["action"])
1979 self.assertEqual(D("0.5"), as_json["from"])
1980 self.assertEqual(D("1.0"), as_json["to"])
1981 self.assertEqual("ETH", as_json["currency"])
1982 self.assertEqual("BTC", as_json["base_currency"])
1983
1984 @unittest.skipUnless("unit" in limits, "Unit skipped")
1985 class OrderTest(WebMockTestCase):
1986 def test_values(self):
1987 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
1988 D("0.1"), "BTC", "long", "market", "trade")
1989 self.assertEqual("buy", order.action)
1990 self.assertEqual(10, order.amount.value)
1991 self.assertEqual("ETH", order.amount.currency)
1992 self.assertEqual(D("0.1"), order.rate)
1993 self.assertEqual("BTC", order.base_currency)
1994 self.assertEqual("market", order.market)
1995 self.assertEqual("long", order.trade_type)
1996 self.assertEqual("pending", order.status)
1997 self.assertEqual("trade", order.trade)
1998 self.assertIsNone(order.id)
1999 self.assertFalse(order.close_if_possible)
2000
2001 def test__repr(self):
2002 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2003 D("0.1"), "BTC", "long", "market", "trade")
2004 self.assertEqual("Order(buy long 10.00000000 ETH at 0.1 BTC [pending])", repr(order))
2005
2006 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2007 D("0.1"), "BTC", "long", "market", "trade",
2008 close_if_possible=True)
2009 self.assertEqual("Order(buy long 10.00000000 ETH at 0.1 BTC [pending] ✂)", repr(order))
2010
2011 def test_as_json(self):
2012 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2013 D("0.1"), "BTC", "long", "market", "trade")
2014 mouvement_mock1 = mock.Mock()
2015 mouvement_mock1.as_json.return_value = 1
2016 mouvement_mock2 = mock.Mock()
2017 mouvement_mock2.as_json.return_value = 2
2018
2019 order.mouvements = [mouvement_mock1, mouvement_mock2]
2020 as_json = order.as_json()
2021 self.assertEqual("buy", as_json["action"])
2022 self.assertEqual("long", as_json["trade_type"])
2023 self.assertEqual(10, as_json["amount"])
2024 self.assertEqual("ETH", as_json["currency"])
2025 self.assertEqual("BTC", as_json["base_currency"])
2026 self.assertEqual(D("0.1"), as_json["rate"])
2027 self.assertEqual("pending", as_json["status"])
2028 self.assertEqual(False, as_json["close_if_possible"])
2029 self.assertIsNone(as_json["id"])
2030 self.assertEqual([1, 2], as_json["mouvements"])
2031
2032 def test_account(self):
2033 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2034 D("0.1"), "BTC", "long", "market", "trade")
2035 self.assertEqual("exchange", order.account)
2036
2037 order = portfolio.Order("sell", portfolio.Amount("ETH", 10),
2038 D("0.1"), "BTC", "short", "market", "trade")
2039 self.assertEqual("margin", order.account)
2040
2041 def test_pending(self):
2042 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2043 D("0.1"), "BTC", "long", "market", "trade")
2044 self.assertTrue(order.pending)
2045 order.status = "open"
2046 self.assertFalse(order.pending)
2047
2048 def test_open(self):
2049 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2050 D("0.1"), "BTC", "long", "market", "trade")
2051 self.assertFalse(order.open)
2052 order.status = "open"
2053 self.assertTrue(order.open)
2054
2055 def test_finished(self):
2056 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2057 D("0.1"), "BTC", "long", "market", "trade")
2058 self.assertFalse(order.finished)
2059 order.status = "closed"
2060 self.assertTrue(order.finished)
2061 order.status = "canceled"
2062 self.assertTrue(order.finished)
2063 order.status = "error"
2064 self.assertTrue(order.finished)
2065
2066 @mock.patch.object(portfolio.Order, "fetch")
2067 def test_cancel(self, fetch):
2068 with self.subTest(debug=True):
2069 self.m.debug = True
2070 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2071 D("0.1"), "BTC", "long", self.m, "trade")
2072 order.status = "open"
2073
2074 order.cancel()
2075 self.m.ccxt.cancel_order.assert_not_called()
2076 self.m.report.log_debug_action.assert_called_once()
2077 self.m.report.log_debug_action.reset_mock()
2078 self.assertEqual("canceled", order.status)
2079
2080 with self.subTest(desc="Nominal case"):
2081 self.m.debug = False
2082 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2083 D("0.1"), "BTC", "long", self.m, "trade")
2084 order.status = "open"
2085 order.id = 42
2086
2087 order.cancel()
2088 self.m.ccxt.cancel_order.assert_called_with(42)
2089 fetch.assert_called_once_with()
2090 self.m.report.log_debug_action.assert_not_called()
2091
2092 with self.subTest(exception=True):
2093 self.m.ccxt.cancel_order.side_effect = portfolio.OrderNotFound
2094 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2095 D("0.1"), "BTC", "long", self.m, "trade")
2096 order.status = "open"
2097 order.id = 42
2098 order.cancel()
2099 self.m.ccxt.cancel_order.assert_called_with(42)
2100 self.m.report.log_error.assert_called_once()
2101
2102 self.m.reset_mock()
2103 with self.subTest(id=None):
2104 self.m.ccxt.cancel_order.side_effect = portfolio.OrderNotFound
2105 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2106 D("0.1"), "BTC", "long", self.m, "trade")
2107 order.status = "open"
2108 order.cancel()
2109 self.m.ccxt.cancel_order.assert_not_called()
2110
2111 self.m.reset_mock()
2112 with self.subTest(open=False):
2113 self.m.ccxt.cancel_order.side_effect = portfolio.OrderNotFound
2114 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2115 D("0.1"), "BTC", "long", self.m, "trade")
2116 order.status = "closed"
2117 order.cancel()
2118 self.m.ccxt.cancel_order.assert_not_called()
2119
2120 def test_dust_amount_remaining(self):
2121 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2122 D("0.1"), "BTC", "long", self.m, "trade")
2123 order.remaining_amount = mock.Mock(return_value=portfolio.Amount("ETH", 1))
2124 self.assertFalse(order.dust_amount_remaining())
2125
2126 order.remaining_amount = mock.Mock(return_value=portfolio.Amount("ETH", D("0.0001")))
2127 self.assertTrue(order.dust_amount_remaining())
2128
2129 @mock.patch.object(portfolio.Order, "fetch")
2130 @mock.patch.object(portfolio.Order, "filled_amount", return_value=portfolio.Amount("ETH", 1))
2131 def test_remaining_amount(self, filled_amount, fetch):
2132 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2133 D("0.1"), "BTC", "long", self.m, "trade")
2134
2135 self.assertEqual(9, order.remaining_amount().value)
2136
2137 order.status = "open"
2138 self.assertEqual(9, order.remaining_amount().value)
2139
2140 @mock.patch.object(portfolio.Order, "fetch")
2141 def test_filled_amount(self, fetch):
2142 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2143 D("0.1"), "BTC", "long", self.m, "trade")
2144 order.mouvements.append(portfolio.Mouvement("ETH", "BTC", {
2145 "tradeID": 42, "type": "buy", "fee": "0.0015",
2146 "date": "2017-12-30 12:00:12", "rate": "0.1",
2147 "amount": "3", "total": "0.3"
2148 }))
2149 order.mouvements.append(portfolio.Mouvement("ETH", "BTC", {
2150 "tradeID": 43, "type": "buy", "fee": "0.0015",
2151 "date": "2017-12-30 13:00:12", "rate": "0.2",
2152 "amount": "2", "total": "0.4"
2153 }))
2154 self.assertEqual(portfolio.Amount("ETH", 5), order.filled_amount())
2155 fetch.assert_not_called()
2156 order.status = "open"
2157 self.assertEqual(portfolio.Amount("ETH", 5), order.filled_amount(in_base_currency=False))
2158 fetch.assert_called_once()
2159 self.assertEqual(portfolio.Amount("BTC", "0.7"), order.filled_amount(in_base_currency=True))
2160
2161 def test_fetch_mouvements(self):
2162 self.m.ccxt.privatePostReturnOrderTrades.return_value = [
2163 {
2164 "tradeID": 42, "type": "buy", "fee": "0.0015",
2165 "date": "2017-12-30 12:00:12", "rate": "0.1",
2166 "amount": "3", "total": "0.3"
2167 },
2168 {
2169 "tradeID": 43, "type": "buy", "fee": "0.0015",
2170 "date": "2017-12-30 13:00:12", "rate": "0.2",
2171 "amount": "2", "total": "0.4"
2172 }
2173 ]
2174 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2175 D("0.1"), "BTC", "long", self.m, "trade")
2176 order.id = 12
2177 order.mouvements = ["Foo", "Bar", "Baz"]
2178
2179 order.fetch_mouvements()
2180
2181 self.m.ccxt.privatePostReturnOrderTrades.assert_called_with({"orderNumber": 12})
2182 self.assertEqual(2, len(order.mouvements))
2183 self.assertEqual(42, order.mouvements[0].id)
2184 self.assertEqual(43, order.mouvements[1].id)
2185
2186 self.m.ccxt.privatePostReturnOrderTrades.side_effect = portfolio.ExchangeError
2187 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2188 D("0.1"), "BTC", "long", self.m, "trade")
2189 order.fetch_mouvements()
2190 self.assertEqual(0, len(order.mouvements))
2191
2192 def test_mark_finished_order(self):
2193 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2194 D("0.1"), "BTC", "short", self.m, "trade",
2195 close_if_possible=True)
2196 order.status = "closed"
2197 self.m.debug = False
2198
2199 order.mark_finished_order()
2200 self.m.ccxt.close_margin_position.assert_called_with("ETH", "BTC")
2201 self.m.ccxt.close_margin_position.reset_mock()
2202
2203 order.status = "open"
2204 order.mark_finished_order()
2205 self.m.ccxt.close_margin_position.assert_not_called()
2206
2207 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2208 D("0.1"), "BTC", "short", self.m, "trade",
2209 close_if_possible=False)
2210 order.status = "closed"
2211 order.mark_finished_order()
2212 self.m.ccxt.close_margin_position.assert_not_called()
2213
2214 order = portfolio.Order("sell", portfolio.Amount("ETH", 10),
2215 D("0.1"), "BTC", "short", self.m, "trade",
2216 close_if_possible=True)
2217 order.status = "closed"
2218 order.mark_finished_order()
2219 self.m.ccxt.close_margin_position.assert_not_called()
2220
2221 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2222 D("0.1"), "BTC", "long", self.m, "trade",
2223 close_if_possible=True)
2224 order.status = "closed"
2225 order.mark_finished_order()
2226 self.m.ccxt.close_margin_position.assert_not_called()
2227
2228 self.m.debug = True
2229
2230 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2231 D("0.1"), "BTC", "short", self.m, "trade",
2232 close_if_possible=True)
2233 order.status = "closed"
2234
2235 order.mark_finished_order()
2236 self.m.ccxt.close_margin_position.assert_not_called()
2237 self.m.report.log_debug_action.assert_called_once()
2238
2239 @mock.patch.object(portfolio.Order, "fetch_mouvements")
2240 def test_fetch(self, fetch_mouvements):
2241 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2242 D("0.1"), "BTC", "long", self.m, "trade")
2243 order.id = 45
2244 with self.subTest(debug=True):
2245 self.m.debug = True
2246 order.fetch()
2247 self.m.report.log_debug_action.assert_called_once()
2248 self.m.report.log_debug_action.reset_mock()
2249 self.m.ccxt.fetch_order.assert_not_called()
2250 fetch_mouvements.assert_not_called()
2251
2252 with self.subTest(debug=False):
2253 self.m.debug = False
2254 self.m.ccxt.fetch_order.return_value = {
2255 "status": "foo",
2256 "datetime": "timestamp"
2257 }
2258 order.fetch()
2259
2260 self.m.ccxt.fetch_order.assert_called_once_with(45)
2261 fetch_mouvements.assert_called_once()
2262 self.assertEqual("foo", order.status)
2263 self.assertEqual("timestamp", order.timestamp)
2264 self.assertEqual(1, len(order.results))
2265 self.m.report.log_debug_action.assert_not_called()
2266
2267 with self.subTest(missing_order=True):
2268 self.m.ccxt.fetch_order.side_effect = [
2269 portfolio.OrderNotCached,
2270 ]
2271 order.fetch()
2272 self.assertEqual("closed_unknown", order.status)
2273
2274 @mock.patch.object(portfolio.Order, "fetch")
2275 @mock.patch.object(portfolio.Order, "mark_finished_order")
2276 def test_get_status(self, mark_finished_order, fetch):
2277 with self.subTest(debug=True):
2278 self.m.debug = True
2279 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2280 D("0.1"), "BTC", "long", self.m, "trade")
2281 self.assertEqual("pending", order.get_status())
2282 fetch.assert_not_called()
2283 self.m.report.log_debug_action.assert_called_once()
2284
2285 with self.subTest(debug=False, finished=False):
2286 self.m.debug = False
2287 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2288 D("0.1"), "BTC", "long", self.m, "trade")
2289 def _fetch(order):
2290 def update_status():
2291 order.status = "open"
2292 return update_status
2293 fetch.side_effect = _fetch(order)
2294 self.assertEqual("open", order.get_status())
2295 mark_finished_order.assert_not_called()
2296 fetch.assert_called_once()
2297
2298 mark_finished_order.reset_mock()
2299 fetch.reset_mock()
2300 with self.subTest(debug=False, finished=True):
2301 self.m.debug = False
2302 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2303 D("0.1"), "BTC", "long", self.m, "trade")
2304 def _fetch(order):
2305 def update_status():
2306 order.status = "closed"
2307 return update_status
2308 fetch.side_effect = _fetch(order)
2309 self.assertEqual("closed", order.get_status())
2310 mark_finished_order.assert_called_once()
2311 fetch.assert_called_once()
2312
2313 def test_run(self):
2314 self.m.ccxt.order_precision.return_value = 4
2315 with self.subTest(debug=True):
2316 self.m.debug = True
2317 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2318 D("0.1"), "BTC", "long", self.m, "trade")
2319 order.run()
2320 self.m.ccxt.create_order.assert_not_called()
2321 self.m.report.log_debug_action.assert_called_with("market.ccxt.create_order('ETH/BTC', 'limit', 'buy', 10.0000, price=0.1, account=exchange)")
2322 self.assertEqual("open", order.status)
2323 self.assertEqual(1, len(order.results))
2324 self.assertEqual(-1, order.id)
2325
2326 self.m.ccxt.create_order.reset_mock()
2327 with self.subTest(debug=False):
2328 self.m.debug = False
2329 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2330 D("0.1"), "BTC", "long", self.m, "trade")
2331 self.m.ccxt.create_order.return_value = { "id": 123 }
2332 order.run()
2333 self.m.ccxt.create_order.assert_called_once()
2334 self.assertEqual(1, len(order.results))
2335 self.assertEqual("open", order.status)
2336
2337 self.m.ccxt.create_order.reset_mock()
2338 with self.subTest(exception=True):
2339 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
2340 D("0.1"), "BTC", "long", self.m, "trade")
2341 self.m.ccxt.create_order.side_effect = Exception("bouh")
2342 order.run()
2343 self.m.ccxt.create_order.assert_called_once()
2344 self.assertEqual(0, len(order.results))
2345 self.assertEqual("error", order.status)
2346 self.m.report.log_error.assert_called_once()
2347
2348 self.m.ccxt.create_order.reset_mock()
2349 with self.subTest(dust_amount_exception=True),\
2350 mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
2351 order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001),
2352 D("0.1"), "BTC", "long", self.m, "trade")
2353 self.m.ccxt.create_order.side_effect = portfolio.InvalidOrder
2354 order.run()
2355 self.m.ccxt.create_order.assert_called_once()
2356 self.assertEqual(0, len(order.results))
2357 self.assertEqual("closed", order.status)
2358 mark_finished_order.assert_called_once()
2359
2360 self.m.ccxt.order_precision.return_value = 8
2361 self.m.ccxt.create_order.reset_mock()
2362 with self.subTest(insufficient_funds=True),\
2363 mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
2364 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
2365 D("0.1"), "BTC", "long", self.m, "trade")
2366 self.m.ccxt.create_order.side_effect = [
2367 portfolio.InsufficientFunds,
2368 portfolio.InsufficientFunds,
2369 portfolio.InsufficientFunds,
2370 { "id": 123 },
2371 ]
2372 order.run()
2373 self.m.ccxt.create_order.assert_has_calls([
2374 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
2375 mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')),
2376 mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')),
2377 mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')),
2378 ])
2379 self.assertEqual(4, self.m.ccxt.create_order.call_count)
2380 self.assertEqual(1, len(order.results))
2381 self.assertEqual("open", order.status)
2382 self.assertEqual(4, order.tries)
2383 self.m.report.log_error.assert_called()
2384 self.assertEqual(4, self.m.report.log_error.call_count)
2385
2386 self.m.ccxt.order_precision.return_value = 8
2387 self.m.ccxt.create_order.reset_mock()
2388 self.m.report.log_error.reset_mock()
2389 with self.subTest(insufficient_funds=True),\
2390 mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
2391 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
2392 D("0.1"), "BTC", "long", self.m, "trade")
2393 self.m.ccxt.create_order.side_effect = [
2394 portfolio.InsufficientFunds,
2395 portfolio.InsufficientFunds,
2396 portfolio.InsufficientFunds,
2397 portfolio.InsufficientFunds,
2398 portfolio.InsufficientFunds,
2399 ]
2400 order.run()
2401 self.m.ccxt.create_order.assert_has_calls([
2402 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
2403 mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')),
2404 mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')),
2405 mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')),
2406 mock.call('ETH/BTC', 'limit', 'buy', D('0.00096059'), account='exchange', price=D('0.1')),
2407 ])
2408 self.assertEqual(5, self.m.ccxt.create_order.call_count)
2409 self.assertEqual(0, len(order.results))
2410 self.assertEqual("error", order.status)
2411 self.assertEqual(5, order.tries)
2412 self.m.report.log_error.assert_called()
2413 self.assertEqual(5, self.m.report.log_error.call_count)
2414 self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00096060 ETH at 0.1 BTC [pending])", exception=mock.ANY)
2415
2416
2417 @unittest.skipUnless("unit" in limits, "Unit skipped")
2418 class MouvementTest(WebMockTestCase):
2419 def test_values(self):
2420 mouvement = portfolio.Mouvement("ETH", "BTC", {
2421 "tradeID": 42, "type": "buy", "fee": "0.0015",
2422 "date": "2017-12-30 12:00:12", "rate": "0.1",
2423 "amount": "10", "total": "1"
2424 })
2425 self.assertEqual("ETH", mouvement.currency)
2426 self.assertEqual("BTC", mouvement.base_currency)
2427 self.assertEqual(42, mouvement.id)
2428 self.assertEqual("buy", mouvement.action)
2429 self.assertEqual(D("0.0015"), mouvement.fee_rate)
2430 self.assertEqual(portfolio.datetime(2017, 12, 30, 12, 0, 12), mouvement.date)
2431 self.assertEqual(D("0.1"), mouvement.rate)
2432 self.assertEqual(portfolio.Amount("ETH", "10"), mouvement.total)
2433 self.assertEqual(portfolio.Amount("BTC", "1"), mouvement.total_in_base)
2434
2435 mouvement = portfolio.Mouvement("ETH", "BTC", { "foo": "bar" })
2436 self.assertIsNone(mouvement.date)
2437 self.assertIsNone(mouvement.id)
2438 self.assertIsNone(mouvement.action)
2439 self.assertEqual(-1, mouvement.fee_rate)
2440 self.assertEqual(0, mouvement.rate)
2441 self.assertEqual(portfolio.Amount("ETH", 0), mouvement.total)
2442 self.assertEqual(portfolio.Amount("BTC", 0), mouvement.total_in_base)
2443
2444 def test__repr(self):
2445 mouvement = portfolio.Mouvement("ETH", "BTC", {
2446 "tradeID": 42, "type": "buy", "fee": "0.0015",
2447 "date": "2017-12-30 12:00:12", "rate": "0.1",
2448 "amount": "10", "total": "1"
2449 })
2450 self.assertEqual("Mouvement(2017-12-30 12:00:12 ; buy 10.00000000 ETH (1.00000000 BTC) fee: 0.1500%)", repr(mouvement))
2451
2452 mouvement = portfolio.Mouvement("ETH", "BTC", {
2453 "tradeID": 42, "type": "buy",
2454 "date": "garbage", "rate": "0.1",
2455 "amount": "10", "total": "1"
2456 })
2457 self.assertEqual("Mouvement(No date ; buy 10.00000000 ETH (1.00000000 BTC))", repr(mouvement))
2458
2459 def test_as_json(self):
2460 mouvement = portfolio.Mouvement("ETH", "BTC", {
2461 "tradeID": 42, "type": "buy", "fee": "0.0015",
2462 "date": "2017-12-30 12:00:12", "rate": "0.1",
2463 "amount": "10", "total": "1"
2464 })
2465 as_json = mouvement.as_json()
2466
2467 self.assertEqual(D("0.0015"), as_json["fee_rate"])
2468 self.assertEqual(portfolio.datetime(2017, 12, 30, 12, 0, 12), as_json["date"])
2469 self.assertEqual("buy", as_json["action"])
2470 self.assertEqual(D("10"), as_json["total"])
2471 self.assertEqual(D("1"), as_json["total_in_base"])
2472 self.assertEqual("BTC", as_json["base_currency"])
2473 self.assertEqual("ETH", as_json["currency"])
2474
2475 @unittest.skipUnless("unit" in limits, "Unit skipped")
2476 class ReportStoreTest(WebMockTestCase):
2477 def test_add_log(self):
2478 report_store = market.ReportStore(self.m)
2479 report_store.add_log({"foo": "bar"})
2480
2481 self.assertEqual({"foo": "bar", "date": mock.ANY}, report_store.logs[0])
2482
2483 def test_set_verbose(self):
2484 report_store = market.ReportStore(self.m)
2485 with self.subTest(verbose=True):
2486 report_store.set_verbose(True)
2487 self.assertTrue(report_store.verbose_print)
2488
2489 with self.subTest(verbose=False):
2490 report_store.set_verbose(False)
2491 self.assertFalse(report_store.verbose_print)
2492
2493 def test_print_log(self):
2494 report_store = market.ReportStore(self.m)
2495 with self.subTest(verbose=True),\
2496 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
2497 report_store.set_verbose(True)
2498 report_store.print_log("Coucou")
2499 report_store.print_log(portfolio.Amount("BTC", 1))
2500 self.assertEqual(stdout_mock.getvalue(), "Coucou\n1.00000000 BTC\n")
2501
2502 with self.subTest(verbose=False),\
2503 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
2504 report_store.set_verbose(False)
2505 report_store.print_log("Coucou")
2506 report_store.print_log(portfolio.Amount("BTC", 1))
2507 self.assertEqual(stdout_mock.getvalue(), "")
2508
2509 def test_to_json(self):
2510 report_store = market.ReportStore(self.m)
2511 report_store.logs.append({"foo": "bar"})
2512 self.assertEqual('[\n {\n "foo": "bar"\n }\n]', report_store.to_json())
2513 report_store.logs.append({"date": portfolio.datetime(2018, 2, 24)})
2514 self.assertEqual('[\n {\n "foo": "bar"\n },\n {\n "date": "2018-02-24T00:00:00"\n }\n]', report_store.to_json())
2515 report_store.logs.append({"amount": portfolio.Amount("BTC", 1)})
2516 self.assertEqual('[\n {\n "foo": "bar"\n },\n {\n "date": "2018-02-24T00:00:00"\n },\n {\n "amount": "1.00000000 BTC"\n }\n]', report_store.to_json())
2517
2518 @mock.patch.object(market.ReportStore, "print_log")
2519 @mock.patch.object(market.ReportStore, "add_log")
2520 def test_log_stage(self, add_log, print_log):
2521 report_store = market.ReportStore(self.m)
2522 c = lambda x: x
2523 report_store.log_stage("foo", bar="baz", c=c, d=portfolio.Amount("BTC", 1))
2524 print_log.assert_has_calls([
2525 mock.call("-----------"),
2526 mock.call("[Stage] foo bar=baz, c=c = lambda x: x, d={'currency': 'BTC', 'value': Decimal('1')}"),
2527 ])
2528 add_log.assert_called_once_with({
2529 'type': 'stage',
2530 'stage': 'foo',
2531 'args': {
2532 'bar': 'baz',
2533 'c': 'c = lambda x: x',
2534 'd': {
2535 'currency': 'BTC',
2536 'value': D('1')
2537 }
2538 }
2539 })
2540
2541 @mock.patch.object(market.ReportStore, "print_log")
2542 @mock.patch.object(market.ReportStore, "add_log")
2543 def test_log_balances(self, add_log, print_log):
2544 report_store = market.ReportStore(self.m)
2545 self.m.balances.as_json.return_value = "json"
2546 self.m.balances.all = { "FOO": "bar", "BAR": "baz" }
2547
2548 report_store.log_balances(tag="tag")
2549 print_log.assert_has_calls([
2550 mock.call("[Balance]"),
2551 mock.call("\tbar"),
2552 mock.call("\tbaz"),
2553 ])
2554 add_log.assert_called_once_with({
2555 'type': 'balance',
2556 'balances': 'json',
2557 'tag': 'tag'
2558 })
2559
2560 @mock.patch.object(market.ReportStore, "print_log")
2561 @mock.patch.object(market.ReportStore, "add_log")
2562 def test_log_tickers(self, add_log, print_log):
2563 report_store = market.ReportStore(self.m)
2564 amounts = {
2565 "BTC": portfolio.Amount("BTC", 10),
2566 "ETH": portfolio.Amount("BTC", D("0.3"))
2567 }
2568 amounts["ETH"].rate = D("0.1")
2569
2570 report_store.log_tickers(amounts, "BTC", "default", "total")
2571 print_log.assert_not_called()
2572 add_log.assert_called_once_with({
2573 'type': 'tickers',
2574 'compute_value': 'default',
2575 'balance_type': 'total',
2576 'currency': 'BTC',
2577 'balances': {
2578 'BTC': D('10'),
2579 'ETH': D('0.3')
2580 },
2581 'rates': {
2582 'BTC': None,
2583 'ETH': D('0.1')
2584 },
2585 'total': D('10.3')
2586 })
2587
2588 add_log.reset_mock()
2589 compute_value = lambda x: x["bid"]
2590 report_store.log_tickers(amounts, "BTC", compute_value, "total")
2591 add_log.assert_called_once_with({
2592 'type': 'tickers',
2593 'compute_value': 'compute_value = lambda x: x["bid"]',
2594 'balance_type': 'total',
2595 'currency': 'BTC',
2596 'balances': {
2597 'BTC': D('10'),
2598 'ETH': D('0.3')
2599 },
2600 'rates': {
2601 'BTC': None,
2602 'ETH': D('0.1')
2603 },
2604 'total': D('10.3')
2605 })
2606
2607 @mock.patch.object(market.ReportStore, "print_log")
2608 @mock.patch.object(market.ReportStore, "add_log")
2609 def test_log_dispatch(self, add_log, print_log):
2610 report_store = market.ReportStore(self.m)
2611 amount = portfolio.Amount("BTC", "10.3")
2612 amounts = {
2613 "BTC": portfolio.Amount("BTC", 10),
2614 "ETH": portfolio.Amount("BTC", D("0.3"))
2615 }
2616 report_store.log_dispatch(amount, amounts, "medium", "repartition")
2617 print_log.assert_not_called()
2618 add_log.assert_called_once_with({
2619 'type': 'dispatch',
2620 'liquidity': 'medium',
2621 'repartition_ratio': 'repartition',
2622 'total_amount': {
2623 'currency': 'BTC',
2624 'value': D('10.3')
2625 },
2626 'repartition': {
2627 'BTC': D('10'),
2628 'ETH': D('0.3')
2629 }
2630 })
2631
2632 @mock.patch.object(market.ReportStore, "print_log")
2633 @mock.patch.object(market.ReportStore, "add_log")
2634 def test_log_trades(self, add_log, print_log):
2635 report_store = market.ReportStore(self.m)
2636 trade_mock1 = mock.Mock()
2637 trade_mock2 = mock.Mock()
2638 trade_mock1.as_json.return_value = { "trade": "1" }
2639 trade_mock2.as_json.return_value = { "trade": "2" }
2640
2641 matching_and_trades = [
2642 (True, trade_mock1),
2643 (False, trade_mock2),
2644 ]
2645 report_store.log_trades(matching_and_trades, "only")
2646
2647 print_log.assert_not_called()
2648 add_log.assert_called_with({
2649 'type': 'trades',
2650 'only': 'only',
2651 'debug': False,
2652 'trades': [
2653 {'trade': '1', 'skipped': False},
2654 {'trade': '2', 'skipped': True}
2655 ]
2656 })
2657
2658 @mock.patch.object(market.ReportStore, "print_log")
2659 @mock.patch.object(market.ReportStore, "add_log")
2660 def test_log_orders(self, add_log, print_log):
2661 report_store = market.ReportStore(self.m)
2662
2663 order_mock1 = mock.Mock()
2664 order_mock2 = mock.Mock()
2665
2666 order_mock1.as_json.return_value = "order1"
2667 order_mock2.as_json.return_value = "order2"
2668
2669 orders = [order_mock1, order_mock2]
2670
2671 report_store.log_orders(orders, tick="tick",
2672 only="only", compute_value="compute_value")
2673
2674 print_log.assert_called_once_with("[Orders]")
2675 self.m.trades.print_all_with_order.assert_called_once_with(ind="\t")
2676
2677 add_log.assert_called_with({
2678 'type': 'orders',
2679 'only': 'only',
2680 'compute_value': 'compute_value',
2681 'tick': 'tick',
2682 'orders': ['order1', 'order2']
2683 })
2684
2685 add_log.reset_mock()
2686 def compute_value(x, y):
2687 return x[y]
2688 report_store.log_orders(orders, tick="tick",
2689 only="only", compute_value=compute_value)
2690 add_log.assert_called_with({
2691 'type': 'orders',
2692 'only': 'only',
2693 'compute_value': 'def compute_value(x, y):\n return x[y]',
2694 'tick': 'tick',
2695 'orders': ['order1', 'order2']
2696 })
2697
2698
2699 @mock.patch.object(market.ReportStore, "print_log")
2700 @mock.patch.object(market.ReportStore, "add_log")
2701 def test_log_order(self, add_log, print_log):
2702 report_store = market.ReportStore(self.m)
2703 order_mock = mock.Mock()
2704 order_mock.as_json.return_value = "order"
2705 new_order_mock = mock.Mock()
2706 new_order_mock.as_json.return_value = "new_order"
2707 order_mock.__repr__ = mock.Mock()
2708 order_mock.__repr__.return_value = "Order Mock"
2709 new_order_mock.__repr__ = mock.Mock()
2710 new_order_mock.__repr__.return_value = "New order Mock"
2711
2712 with self.subTest(finished=True):
2713 report_store.log_order(order_mock, 1, finished=True)
2714 print_log.assert_called_once_with("[Order] Finished Order Mock")
2715 add_log.assert_called_once_with({
2716 'type': 'order',
2717 'tick': 1,
2718 'update': None,
2719 'order': 'order',
2720 'compute_value': None,
2721 'new_order': None
2722 })
2723
2724 add_log.reset_mock()
2725 print_log.reset_mock()
2726
2727 with self.subTest(update="waiting"):
2728 report_store.log_order(order_mock, 1, update="waiting")
2729 print_log.assert_called_once_with("[Order] Order Mock, tick 1, waiting")
2730 add_log.assert_called_once_with({
2731 'type': 'order',
2732 'tick': 1,
2733 'update': 'waiting',
2734 'order': 'order',
2735 'compute_value': None,
2736 'new_order': None
2737 })
2738
2739 add_log.reset_mock()
2740 print_log.reset_mock()
2741 with self.subTest(update="adjusting"):
2742 compute_value = lambda x: (x["bid"] + x["ask"]*2)/3
2743 report_store.log_order(order_mock, 3,
2744 update="adjusting", new_order=new_order_mock,
2745 compute_value=compute_value)
2746 print_log.assert_called_once_with("[Order] Order Mock, tick 3, cancelling and adjusting to New order Mock")
2747 add_log.assert_called_once_with({
2748 'type': 'order',
2749 'tick': 3,
2750 'update': 'adjusting',
2751 'order': 'order',
2752 'compute_value': 'compute_value = lambda x: (x["bid"] + x["ask"]*2)/3',
2753 'new_order': 'new_order'
2754 })
2755
2756 add_log.reset_mock()
2757 print_log.reset_mock()
2758 with self.subTest(update="market_fallback"):
2759 report_store.log_order(order_mock, 7,
2760 update="market_fallback", new_order=new_order_mock)
2761 print_log.assert_called_once_with("[Order] Order Mock, tick 7, fallbacking to market value")
2762 add_log.assert_called_once_with({
2763 'type': 'order',
2764 'tick': 7,
2765 'update': 'market_fallback',
2766 'order': 'order',
2767 'compute_value': None,
2768 'new_order': 'new_order'
2769 })
2770
2771 add_log.reset_mock()
2772 print_log.reset_mock()
2773 with self.subTest(update="market_adjusting"):
2774 report_store.log_order(order_mock, 17,
2775 update="market_adjust", new_order=new_order_mock)
2776 print_log.assert_called_once_with("[Order] Order Mock, tick 17, market value, cancelling and adjusting to New order Mock")
2777 add_log.assert_called_once_with({
2778 'type': 'order',
2779 'tick': 17,
2780 'update': 'market_adjust',
2781 'order': 'order',
2782 'compute_value': None,
2783 'new_order': 'new_order'
2784 })
2785
2786 @mock.patch.object(market.ReportStore, "print_log")
2787 @mock.patch.object(market.ReportStore, "add_log")
2788 def test_log_move_balances(self, add_log, print_log):
2789 report_store = market.ReportStore(self.m)
2790 needed = {
2791 "BTC": portfolio.Amount("BTC", 10),
2792 "USDT": 1
2793 }
2794 moving = {
2795 "BTC": portfolio.Amount("BTC", 3),
2796 "USDT": -2
2797 }
2798 report_store.log_move_balances(needed, moving)
2799 print_log.assert_not_called()
2800 add_log.assert_called_once_with({
2801 'type': 'move_balances',
2802 'debug': False,
2803 'needed': {
2804 'BTC': D('10'),
2805 'USDT': 1
2806 },
2807 'moving': {
2808 'BTC': D('3'),
2809 'USDT': -2
2810 }
2811 })
2812
2813 @mock.patch.object(market.ReportStore, "print_log")
2814 @mock.patch.object(market.ReportStore, "add_log")
2815 def test_log_http_request(self, add_log, print_log):
2816 report_store = market.ReportStore(self.m)
2817 response = mock.Mock()
2818 response.status_code = 200
2819 response.text = "Hey"
2820
2821 report_store.log_http_request("method", "url", "body",
2822 "headers", response)
2823 print_log.assert_not_called()
2824 add_log.assert_called_once_with({
2825 'type': 'http_request',
2826 'method': 'method',
2827 'url': 'url',
2828 'body': 'body',
2829 'headers': 'headers',
2830 'status': 200,
2831 'response': 'Hey'
2832 })
2833
2834 @mock.patch.object(market.ReportStore, "print_log")
2835 @mock.patch.object(market.ReportStore, "add_log")
2836 def test_log_error(self, add_log, print_log):
2837 report_store = market.ReportStore(self.m)
2838 with self.subTest(message=None, exception=None):
2839 report_store.log_error("action")
2840 print_log.assert_called_once_with("[Error] action")
2841 add_log.assert_called_once_with({
2842 'type': 'error',
2843 'action': 'action',
2844 'exception_class': None,
2845 'exception_message': None,
2846 'message': None
2847 })
2848
2849 print_log.reset_mock()
2850 add_log.reset_mock()
2851 with self.subTest(message="Hey", exception=None):
2852 report_store.log_error("action", message="Hey")
2853 print_log.assert_has_calls([
2854 mock.call("[Error] action"),
2855 mock.call("\tHey")
2856 ])
2857 add_log.assert_called_once_with({
2858 'type': 'error',
2859 'action': 'action',
2860 'exception_class': None,
2861 'exception_message': None,
2862 'message': "Hey"
2863 })
2864
2865 print_log.reset_mock()
2866 add_log.reset_mock()
2867 with self.subTest(message=None, exception=Exception("bouh")):
2868 report_store.log_error("action", exception=Exception("bouh"))
2869 print_log.assert_has_calls([
2870 mock.call("[Error] action"),
2871 mock.call("\tException: bouh")
2872 ])
2873 add_log.assert_called_once_with({
2874 'type': 'error',
2875 'action': 'action',
2876 'exception_class': "Exception",
2877 'exception_message': "bouh",
2878 'message': None
2879 })
2880
2881 print_log.reset_mock()
2882 add_log.reset_mock()
2883 with self.subTest(message="Hey", exception=Exception("bouh")):
2884 report_store.log_error("action", message="Hey", exception=Exception("bouh"))
2885 print_log.assert_has_calls([
2886 mock.call("[Error] action"),
2887 mock.call("\tException: bouh"),
2888 mock.call("\tHey")
2889 ])
2890 add_log.assert_called_once_with({
2891 'type': 'error',
2892 'action': 'action',
2893 'exception_class': "Exception",
2894 'exception_message': "bouh",
2895 'message': "Hey"
2896 })
2897
2898 @mock.patch.object(market.ReportStore, "print_log")
2899 @mock.patch.object(market.ReportStore, "add_log")
2900 def test_log_debug_action(self, add_log, print_log):
2901 report_store = market.ReportStore(self.m)
2902 report_store.log_debug_action("Hey")
2903
2904 print_log.assert_called_once_with("[Debug] Hey")
2905 add_log.assert_called_once_with({
2906 'type': 'debug_action',
2907 'action': 'Hey'
2908 })
2909
2910 @unittest.skipUnless("unit" in limits, "Unit skipped")
2911 class MainTest(WebMockTestCase):
2912 def test_make_order(self):
2913 self.m.get_ticker.return_value = {
2914 "inverted": False,
2915 "average": D("0.1"),
2916 "bid": D("0.09"),
2917 "ask": D("0.11"),
2918 }
2919
2920 with self.subTest(description="nominal case"):
2921 main.make_order(self.m, 10, "ETH")
2922
2923 self.m.report.log_stage.assert_has_calls([
2924 mock.call("make_order_begin"),
2925 mock.call("make_order_end"),
2926 ])
2927 self.m.balances.fetch_balances.assert_has_calls([
2928 mock.call(tag="make_order_begin"),
2929 mock.call(tag="make_order_end"),
2930 ])
2931 self.m.trades.all.append.assert_called_once()
2932 trade = self.m.trades.all.append.mock_calls[0][1][0]
2933 self.assertEqual(False, trade.orders[0].close_if_possible)
2934 self.assertEqual(0, trade.value_from)
2935 self.assertEqual("ETH", trade.currency)
2936 self.assertEqual("BTC", trade.base_currency)
2937 self.m.report.log_orders.assert_called_once_with([trade.orders[0]], None, "average")
2938 self.m.trades.run_orders.assert_called_once_with()
2939 self.m.follow_orders.assert_called_once_with()
2940
2941 order = trade.orders[0]
2942 self.assertEqual(D("0.10"), order.rate)
2943
2944 self.m.reset_mock()
2945 with self.subTest(compute_value="default"):
2946 main.make_order(self.m, 10, "ETH", action="dispose",
2947 compute_value="ask")
2948
2949 trade = self.m.trades.all.append.mock_calls[0][1][0]
2950 order = trade.orders[0]
2951 self.assertEqual(D("0.11"), order.rate)
2952
2953 self.m.reset_mock()
2954 with self.subTest(follow=False):
2955 result = main.make_order(self.m, 10, "ETH", follow=False)
2956
2957 self.m.report.log_stage.assert_has_calls([
2958 mock.call("make_order_begin"),
2959 mock.call("make_order_end_not_followed"),
2960 ])
2961 self.m.balances.fetch_balances.assert_called_once_with(tag="make_order_begin")
2962
2963 self.m.trades.all.append.assert_called_once()
2964 trade = self.m.trades.all.append.mock_calls[0][1][0]
2965 self.assertEqual(0, trade.value_from)
2966 self.assertEqual("ETH", trade.currency)
2967 self.assertEqual("BTC", trade.base_currency)
2968 self.m.report.log_orders.assert_called_once_with([trade.orders[0]], None, "average")
2969 self.m.trades.run_orders.assert_called_once_with()
2970 self.m.follow_orders.assert_not_called()
2971 self.assertEqual(trade.orders[0], result)
2972
2973 self.m.reset_mock()
2974 with self.subTest(base_currency="USDT"):
2975 main.make_order(self.m, 1, "BTC", base_currency="USDT")
2976
2977 trade = self.m.trades.all.append.mock_calls[0][1][0]
2978 self.assertEqual("BTC", trade.currency)
2979 self.assertEqual("USDT", trade.base_currency)
2980
2981 self.m.reset_mock()
2982 with self.subTest(close_if_possible=True):
2983 main.make_order(self.m, 10, "ETH", close_if_possible=True)
2984
2985 trade = self.m.trades.all.append.mock_calls[0][1][0]
2986 self.assertEqual(True, trade.orders[0].close_if_possible)
2987
2988 self.m.reset_mock()
2989 with self.subTest(action="dispose"):
2990 main.make_order(self.m, 10, "ETH", action="dispose")
2991
2992 trade = self.m.trades.all.append.mock_calls[0][1][0]
2993 self.assertEqual(0, trade.value_to)
2994 self.assertEqual(1, trade.value_from.value)
2995 self.assertEqual("ETH", trade.currency)
2996 self.assertEqual("BTC", trade.base_currency)
2997
2998 self.m.reset_mock()
2999 with self.subTest(compute_value="default"):
3000 main.make_order(self.m, 10, "ETH", action="dispose",
3001 compute_value="bid")
3002
3003 trade = self.m.trades.all.append.mock_calls[0][1][0]
3004 self.assertEqual(D("0.9"), trade.value_from.value)
3005
3006 def test_get_user_market(self):
3007 with mock.patch("main.fetch_markets") as main_fetch_markets,\
3008 mock.patch("main.parse_config") as main_parse_config:
3009 with self.subTest(debug=False):
3010 main_parse_config.return_value = ["pg_config", "report_path"]
3011 main_fetch_markets.return_value = [({"key": "market_config"},)]
3012 m = main.get_user_market("config_path.ini", 1)
3013
3014 self.assertIsInstance(m, market.Market)
3015 self.assertFalse(m.debug)
3016
3017 with self.subTest(debug=True):
3018 main_parse_config.return_value = ["pg_config", "report_path"]
3019 main_fetch_markets.return_value = [({"key": "market_config"},)]
3020 m = main.get_user_market("config_path.ini", 1, debug=True)
3021
3022 self.assertIsInstance(m, market.Market)
3023 self.assertTrue(m.debug)
3024
3025 def test_main(self):
3026 with mock.patch("main.parse_args") as parse_args,\
3027 mock.patch("main.parse_config") as parse_config,\
3028 mock.patch("main.fetch_markets") as fetch_markets,\
3029 mock.patch("market.Market") as market_mock,\
3030 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
3031
3032 args_mock = mock.Mock()
3033 args_mock.action = "action"
3034 args_mock.config = "config"
3035 args_mock.user = "user"
3036 args_mock.debug = "debug"
3037 args_mock.before = "before"
3038 args_mock.after = "after"
3039 parse_args.return_value = args_mock
3040
3041 parse_config.return_value = ["pg_config", "report_path"]
3042
3043 fetch_markets.return_value = [["config1", 1], ["config2", 2]]
3044
3045 main.main(["Foo", "Bar"])
3046
3047 parse_args.assert_called_with(["Foo", "Bar"])
3048 parse_config.assert_called_with("config")
3049 fetch_markets.assert_called_with("pg_config", "user")
3050
3051 self.assertEqual(2, market_mock.from_config.call_count)
3052 market_mock.from_config.assert_has_calls([
3053 mock.call("config1", debug="debug", user_id=1, report_path="report_path"),
3054 mock.call().process("action", before="before", after="after"),
3055 mock.call("config2", debug="debug", user_id=2, report_path="report_path"),
3056 mock.call().process("action", before="before", after="after")
3057 ])
3058
3059 self.assertEqual("", stdout_mock.getvalue())
3060
3061 with self.subTest(exception=True):
3062 market_mock.from_config.side_effect = Exception("boo")
3063 main.main(["Foo", "Bar"])
3064 self.assertEqual("Exception: boo\nException: boo\n", stdout_mock.getvalue())
3065
3066 @mock.patch.object(main.sys, "exit")
3067 @mock.patch("main.configparser")
3068 @mock.patch("main.os")
3069 def test_parse_config(self, os, configparser, exit):
3070 with self.subTest(pg_config=True, report_path=None):
3071 config_mock = mock.MagicMock()
3072 configparser.ConfigParser.return_value = config_mock
3073 def config(element):
3074 return element == "postgresql"
3075
3076 config_mock.__contains__.side_effect = config
3077 config_mock.__getitem__.return_value = "pg_config"
3078
3079 result = main.parse_config("configfile")
3080
3081 config_mock.read.assert_called_with("configfile")
3082
3083 self.assertEqual(["pg_config", None], result)
3084
3085 with self.subTest(pg_config=True, report_path="present"):
3086 config_mock = mock.MagicMock()
3087 configparser.ConfigParser.return_value = config_mock
3088
3089 config_mock.__contains__.return_value = True
3090 config_mock.__getitem__.side_effect = [
3091 {"report_path": "report_path"},
3092 {"report_path": "report_path"},
3093 "pg_config",
3094 ]
3095
3096 os.path.exists.return_value = False
3097 result = main.parse_config("configfile")
3098
3099 config_mock.read.assert_called_with("configfile")
3100 self.assertEqual(["pg_config", "report_path"], result)
3101 os.path.exists.assert_called_once_with("report_path")
3102 os.makedirs.assert_called_once_with("report_path")
3103
3104 with self.subTest(pg_config=False),\
3105 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
3106 config_mock = mock.MagicMock()
3107 configparser.ConfigParser.return_value = config_mock
3108 result = main.parse_config("configfile")
3109
3110 config_mock.read.assert_called_with("configfile")
3111 exit.assert_called_once_with(1)
3112 self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue())
3113
3114 @mock.patch.object(main.sys, "exit")
3115 def test_parse_args(self, exit):
3116 with self.subTest(config="config.ini"):
3117 args = main.parse_args([])
3118 self.assertEqual("config.ini", args.config)
3119 self.assertFalse(args.before)
3120 self.assertFalse(args.after)
3121 self.assertFalse(args.debug)
3122
3123 args = main.parse_args(["--before", "--after", "--debug"])
3124 self.assertTrue(args.before)
3125 self.assertTrue(args.after)
3126 self.assertTrue(args.debug)
3127
3128 exit.assert_not_called()
3129
3130 with self.subTest(config="inexistant"),\
3131 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
3132 args = main.parse_args(["--config", "foo.bar"])
3133 exit.assert_called_once_with(1)
3134 self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue())
3135
3136 @mock.patch.object(main, "psycopg2")
3137 def test_fetch_markets(self, psycopg2):
3138 connect_mock = mock.Mock()
3139 cursor_mock = mock.MagicMock()
3140 cursor_mock.__iter__.return_value = ["row_1", "row_2"]
3141
3142 connect_mock.cursor.return_value = cursor_mock
3143 psycopg2.connect.return_value = connect_mock
3144
3145 with self.subTest(user=None):
3146 rows = list(main.fetch_markets({"foo": "bar"}, None))
3147
3148 psycopg2.connect.assert_called_once_with(foo="bar")
3149 cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs")
3150
3151 self.assertEqual(["row_1", "row_2"], rows)
3152
3153 psycopg2.connect.reset_mock()
3154 cursor_mock.execute.reset_mock()
3155 with self.subTest(user=1):
3156 rows = list(main.fetch_markets({"foo": "bar"}, 1))
3157
3158 psycopg2.connect.assert_called_once_with(foo="bar")
3159 cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1)
3160
3161 self.assertEqual(["row_1", "row_2"], rows)
3162
3163
3164 @unittest.skipUnless("unit" in limits, "Unit skipped")
3165 class ProcessorTest(WebMockTestCase):
3166 def test_values(self):
3167 processor = market.Processor(self.m)
3168
3169 self.assertEqual(self.m, processor.market)
3170
3171 def test_run_action(self):
3172 processor = market.Processor(self.m)
3173
3174 with mock.patch.object(processor, "parse_args") as parse_args:
3175 method_mock = mock.Mock()
3176 parse_args.return_value = [method_mock, { "foo": "bar" }]
3177
3178 processor.run_action("foo", "bar", "baz")
3179
3180 parse_args.assert_called_with("foo", "bar", "baz")
3181
3182 method_mock.assert_called_with(foo="bar")
3183
3184 processor.run_action("wait_for_recent", "bar", "baz")
3185
3186 method_mock.assert_called_with(self.m, foo="bar")
3187
3188 def test_select_step(self):
3189 processor = market.Processor(self.m)
3190
3191 scenario = processor.scenarios["sell_all"]
3192
3193 self.assertEqual(scenario, processor.select_steps(scenario, "all"))
3194 self.assertEqual(["all_sell"], list(map(lambda x: x["name"], processor.select_steps(scenario, "before"))))
3195 self.assertEqual(["wait", "all_buy"], list(map(lambda x: x["name"], processor.select_steps(scenario, "after"))))
3196 self.assertEqual(["wait"], list(map(lambda x: x["name"], processor.select_steps(scenario, 2))))
3197 self.assertEqual(["wait"], list(map(lambda x: x["name"], processor.select_steps(scenario, "wait"))))
3198
3199 with self.assertRaises(TypeError):
3200 processor.select_steps(scenario, ["wait"])
3201
3202 @mock.patch("market.Processor.process_step")
3203 def test_process(self, process_step):
3204 processor = market.Processor(self.m)
3205
3206 processor.process("sell_all", foo="bar")
3207 self.assertEqual(3, process_step.call_count)
3208
3209 steps = list(map(lambda x: x[1][1]["name"], process_step.mock_calls))
3210 scenario_names = list(map(lambda x: x[1][0], process_step.mock_calls))
3211 kwargs = list(map(lambda x: x[1][2], process_step.mock_calls))
3212 self.assertEqual(["all_sell", "wait", "all_buy"], steps)
3213 self.assertEqual(["sell_all", "sell_all", "sell_all"], scenario_names)
3214 self.assertEqual([{"foo":"bar"}, {"foo":"bar"}, {"foo":"bar"}], kwargs)
3215
3216 process_step.reset_mock()
3217
3218 processor.process("sell_needed", steps=["before", "after"])
3219 self.assertEqual(3, process_step.call_count)
3220
3221 def test_method_arguments(self):
3222 ccxt = mock.Mock(spec=market.ccxt.poloniexE)
3223 m = market.Market(ccxt)
3224
3225 processor = market.Processor(m)
3226
3227 method, arguments = processor.method_arguments("wait_for_recent")
3228 self.assertEqual(portfolio.Portfolio.wait_for_recent, method)
3229 self.assertEqual(["delta"], arguments)
3230
3231 method, arguments = processor.method_arguments("prepare_trades")
3232 self.assertEqual(m.prepare_trades, method)
3233 self.assertEqual(['base_currency', 'liquidity', 'compute_value', 'repartition', 'only'], arguments)
3234
3235 method, arguments = processor.method_arguments("prepare_orders")
3236 self.assertEqual(m.trades.prepare_orders, method)
3237
3238 method, arguments = processor.method_arguments("move_balances")
3239 self.assertEqual(m.move_balances, method)
3240
3241 method, arguments = processor.method_arguments("run_orders")
3242 self.assertEqual(m.trades.run_orders, method)
3243
3244 method, arguments = processor.method_arguments("follow_orders")
3245 self.assertEqual(m.follow_orders, method)
3246
3247 method, arguments = processor.method_arguments("close_trades")
3248 self.assertEqual(m.trades.close_trades, method)
3249
3250 def test_process_step(self):
3251 processor = market.Processor(self.m)
3252
3253 with mock.patch.object(processor, "run_action") as run_action:
3254 step = processor.scenarios["sell_needed"][1]
3255
3256 processor.process_step("foo", step, {"foo":"bar"})
3257
3258 self.m.report.log_stage.assert_has_calls([
3259 mock.call("process_foo__1_sell_begin"),
3260 mock.call("process_foo__1_sell_end"),
3261 ])
3262 self.m.balances.fetch_balances.assert_has_calls([
3263 mock.call(tag="process_foo__1_sell_begin"),
3264 mock.call(tag="process_foo__1_sell_end"),
3265 ])
3266
3267 self.assertEqual(5, run_action.call_count)
3268
3269 run_action.assert_has_calls([
3270 mock.call('prepare_trades', {}, {'foo': 'bar'}),
3271 mock.call('prepare_orders', {'only': 'dispose', 'compute_value': 'average'}, {'foo': 'bar'}),
3272 mock.call('run_orders', {}, {'foo': 'bar'}),
3273 mock.call('follow_orders', {}, {'foo': 'bar'}),
3274 mock.call('close_trades', {}, {'foo': 'bar'}),
3275 ])
3276
3277 self.m.reset_mock()
3278 with mock.patch.object(processor, "run_action") as run_action:
3279 step = processor.scenarios["sell_needed"][0]
3280
3281 processor.process_step("foo", step, {"foo":"bar"})
3282 self.m.balances.fetch_balances.assert_not_called()
3283
3284 def test_parse_args(self):
3285 processor = market.Processor(self.m)
3286
3287 with mock.patch.object(processor, "method_arguments") as method_arguments:
3288 method_mock = mock.Mock()
3289 method_arguments.return_value = [
3290 method_mock,
3291 ["foo2", "foo"]
3292 ]
3293 method, args = processor.parse_args("action", {"foo": "bar", "foo2": "bar"}, {"foo": "bar2", "bla": "bla"})
3294
3295 self.assertEqual(method_mock, method)
3296 self.assertEqual({"foo": "bar2", "foo2": "bar"}, args)
3297
3298 with mock.patch.object(processor, "method_arguments") as method_arguments:
3299 method_mock = mock.Mock()
3300 method_arguments.return_value = [
3301 method_mock,
3302 ["repartition"]
3303 ]
3304 method, args = processor.parse_args("action", {"repartition": { "base_currency": 1 }}, {})
3305
3306 self.assertEqual(1, len(args["repartition"]))
3307 self.assertIn("BTC", args["repartition"])
3308
3309 with mock.patch.object(processor, "method_arguments") as method_arguments:
3310 method_mock = mock.Mock()
3311 method_arguments.return_value = [
3312 method_mock,
3313 ["repartition", "base_currency"]
3314 ]
3315 method, args = processor.parse_args("action", {"repartition": { "base_currency": 1 }}, {"base_currency": "USDT"})
3316
3317 self.assertEqual(1, len(args["repartition"]))
3318 self.assertIn("USDT", args["repartition"])
3319
3320 with mock.patch.object(processor, "method_arguments") as method_arguments:
3321 method_mock = mock.Mock()
3322 method_arguments.return_value = [
3323 method_mock,
3324 ["repartition", "base_currency"]
3325 ]
3326 method, args = processor.parse_args("action", {"repartition": { "ETH": 1 }}, {"base_currency": "USDT"})
3327
3328 self.assertEqual(1, len(args["repartition"]))
3329 self.assertIn("ETH", args["repartition"])
3330
3331
3332 @unittest.skipUnless("acceptance" in limits, "Acceptance skipped")
3333 class AcceptanceTest(WebMockTestCase):
3334 @unittest.expectedFailure
3335 def test_success_sell_only_necessary(self):
3336 # FIXME: catch stdout
3337 self.m.report.verbose_print = False
3338 fetch_balance = {
3339 "ETH": {
3340 "exchange_free": D("1.0"),
3341 "exchange_used": D("0.0"),
3342 "exchange_total": D("1.0"),
3343 "total": D("1.0"),
3344 },
3345 "ETC": {
3346 "exchange_free": D("4.0"),
3347 "exchange_used": D("0.0"),
3348 "exchange_total": D("4.0"),
3349 "total": D("4.0"),
3350 },
3351 "XVG": {
3352 "exchange_free": D("1000.0"),
3353 "exchange_used": D("0.0"),
3354 "exchange_total": D("1000.0"),
3355 "total": D("1000.0"),
3356 },
3357 }
3358 repartition = {
3359 "ETH": (D("0.25"), "long"),
3360 "ETC": (D("0.25"), "long"),
3361 "BTC": (D("0.4"), "long"),
3362 "BTD": (D("0.01"), "short"),
3363 "B2X": (D("0.04"), "long"),
3364 "USDT": (D("0.05"), "long"),
3365 }
3366
3367 def fetch_ticker(symbol):
3368 if symbol == "ETH/BTC":
3369 return {
3370 "symbol": "ETH/BTC",
3371 "bid": D("0.14"),
3372 "ask": D("0.16")
3373 }
3374 if symbol == "ETC/BTC":
3375 return {
3376 "symbol": "ETC/BTC",
3377 "bid": D("0.002"),
3378 "ask": D("0.003")
3379 }
3380 if symbol == "XVG/BTC":
3381 return {
3382 "symbol": "XVG/BTC",
3383 "bid": D("0.00003"),
3384 "ask": D("0.00005")
3385 }
3386 if symbol == "BTD/BTC":
3387 return {
3388 "symbol": "BTD/BTC",
3389 "bid": D("0.0008"),
3390 "ask": D("0.0012")
3391 }
3392 if symbol == "B2X/BTC":
3393 return {
3394 "symbol": "B2X/BTC",
3395 "bid": D("0.0008"),
3396 "ask": D("0.0012")
3397 }
3398 if symbol == "USDT/BTC":
3399 raise helper.ExchangeError
3400 if symbol == "BTC/USDT":
3401 return {
3402 "symbol": "BTC/USDT",
3403 "bid": D("14000"),
3404 "ask": D("16000")
3405 }
3406 self.fail("Shouldn't have been called with {}".format(symbol))
3407
3408 market = mock.Mock()
3409 market.fetch_all_balances.return_value = fetch_balance
3410 market.fetch_ticker.side_effect = fetch_ticker
3411 with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition):
3412 # Action 1
3413 helper.prepare_trades(market)
3414
3415 balances = portfolio.BalanceStore.all
3416 self.assertEqual(portfolio.Amount("ETH", 1), balances["ETH"].total)
3417 self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total)
3418 self.assertEqual(portfolio.Amount("XVG", 1000), balances["XVG"].total)
3419
3420
3421 trades = portfolio.TradeStore.all
3422 self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades[0].value_from)
3423 self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[0].value_to)
3424 self.assertEqual("dispose", trades[0].action)
3425
3426 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades[1].value_from)
3427 self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[1].value_to)
3428 self.assertEqual("acquire", trades[1].action)
3429
3430 self.assertEqual(portfolio.Amount("BTC", D("0.04")), trades[2].value_from)
3431 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[2].value_to)
3432 self.assertEqual("dispose", trades[2].action)
3433
3434 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[3].value_from)
3435 self.assertEqual(portfolio.Amount("BTC", D("-0.002")), trades[3].value_to)
3436 self.assertEqual("acquire", trades[3].action)
3437
3438 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[4].value_from)
3439 self.assertEqual(portfolio.Amount("BTC", D("0.008")), trades[4].value_to)
3440 self.assertEqual("acquire", trades[4].action)
3441
3442 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[5].value_from)
3443 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades[5].value_to)
3444 self.assertEqual("acquire", trades[5].action)
3445
3446 # Action 2
3447 portfolio.TradeStore.prepare_orders(only="dispose", compute_value=lambda x, y: x["bid"] * D("1.001"))
3448
3449 all_orders = portfolio.TradeStore.all_orders(state="pending")
3450 self.assertEqual(2, len(all_orders))
3451 self.assertEqual(2, 3*all_orders[0].amount.value)
3452 self.assertEqual(D("0.14014"), all_orders[0].rate)
3453 self.assertEqual(1000, all_orders[1].amount.value)
3454 self.assertEqual(D("0.00003003"), all_orders[1].rate)
3455
3456
3457 def create_order(symbol, type, action, amount, price=None, account="exchange"):
3458 self.assertEqual("limit", type)
3459 if symbol == "ETH/BTC":
3460 self.assertEqual("sell", action)
3461 self.assertEqual(D('0.66666666'), amount)
3462 self.assertEqual(D("0.14014"), price)
3463 elif symbol == "XVG/BTC":
3464 self.assertEqual("sell", action)
3465 self.assertEqual(1000, amount)
3466 self.assertEqual(D("0.00003003"), price)
3467 else:
3468 self.fail("I shouldn't have been called")
3469
3470 return {
3471 "id": symbol,
3472 }
3473 market.create_order.side_effect = create_order
3474 market.order_precision.return_value = 8
3475
3476 # Action 3
3477 portfolio.TradeStore.run_orders()
3478
3479 self.assertEqual("open", all_orders[0].status)
3480 self.assertEqual("open", all_orders[1].status)
3481
3482 market.fetch_order.return_value = { "status": "closed", "datetime": "2018-01-20 13:40:00" }
3483 market.privatePostReturnOrderTrades.return_value = [
3484 {
3485 "tradeID": 42, "type": "buy", "fee": "0.0015",
3486 "date": "2017-12-30 12:00:12", "rate": "0.1",
3487 "amount": "10", "total": "1"
3488 }
3489 ]
3490 with mock.patch.object(portfolio.time, "sleep") as sleep:
3491 # Action 4
3492 helper.follow_orders(verbose=False)
3493
3494 sleep.assert_called_with(30)
3495
3496 for order in all_orders:
3497 self.assertEqual("closed", order.status)
3498
3499 fetch_balance = {
3500 "ETH": {
3501 "exchange_free": D("1.0") / 3,
3502 "exchange_used": D("0.0"),
3503 "exchange_total": D("1.0") / 3,
3504 "margin_total": 0,
3505 "total": D("1.0") / 3,
3506 },
3507 "BTC": {
3508 "exchange_free": D("0.134"),
3509 "exchange_used": D("0.0"),
3510 "exchange_total": D("0.134"),
3511 "margin_total": 0,
3512 "total": D("0.134"),
3513 },
3514 "ETC": {
3515 "exchange_free": D("4.0"),
3516 "exchange_used": D("0.0"),
3517 "exchange_total": D("4.0"),
3518 "margin_total": 0,
3519 "total": D("4.0"),
3520 },
3521 "XVG": {
3522 "exchange_free": D("0.0"),
3523 "exchange_used": D("0.0"),
3524 "exchange_total": D("0.0"),
3525 "margin_total": 0,
3526 "total": D("0.0"),
3527 },
3528 }
3529 market.fetch_all_balances.return_value = fetch_balance
3530
3531 with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition):
3532 # Action 5
3533 helper.prepare_trades(market, only="acquire", compute_value="average")
3534
3535 balances = portfolio.BalanceStore.all
3536 self.assertEqual(portfolio.Amount("ETH", 1 / D("3")), balances["ETH"].total)
3537 self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total)
3538 self.assertEqual(portfolio.Amount("BTC", D("0.134")), balances["BTC"].total)
3539 self.assertEqual(portfolio.Amount("XVG", 0), balances["XVG"].total)
3540
3541
3542 trades = portfolio.TradeStore.all
3543 self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades[0].value_from)
3544 self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[0].value_to)
3545 self.assertEqual("dispose", trades[0].action)
3546
3547 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades[1].value_from)
3548 self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades[1].value_to)
3549 self.assertEqual("acquire", trades[1].action)
3550
3551 self.assertNotIn("BTC", trades)
3552
3553 self.assertEqual(portfolio.Amount("BTC", D("0.04")), trades[2].value_from)
3554 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[2].value_to)
3555 self.assertEqual("dispose", trades[2].action)
3556
3557 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[3].value_from)
3558 self.assertEqual(portfolio.Amount("BTC", D("-0.002")), trades[3].value_to)
3559 self.assertEqual("acquire", trades[3].action)
3560
3561 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[4].value_from)
3562 self.assertEqual(portfolio.Amount("BTC", D("0.008")), trades[4].value_to)
3563 self.assertEqual("acquire", trades[4].action)
3564
3565 self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades[5].value_from)
3566 self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades[5].value_to)
3567 self.assertEqual("acquire", trades[5].action)
3568
3569 # Action 6
3570 portfolio.TradeStore.prepare_orders(only="acquire", compute_value=lambda x, y: x["ask"])
3571
3572 all_orders = portfolio.TradeStore.all_orders(state="pending")
3573 self.assertEqual(4, len(all_orders))
3574 self.assertEqual(portfolio.Amount("ETC", D("12.83333333")), round(all_orders[0].amount))
3575 self.assertEqual(D("0.003"), all_orders[0].rate)
3576 self.assertEqual("buy", all_orders[0].action)
3577 self.assertEqual("long", all_orders[0].trade_type)
3578
3579 self.assertEqual(portfolio.Amount("BTD", D("1.61666666")), round(all_orders[1].amount))
3580 self.assertEqual(D("0.0012"), all_orders[1].rate)
3581 self.assertEqual("sell", all_orders[1].action)
3582 self.assertEqual("short", all_orders[1].trade_type)
3583
3584 diff = portfolio.Amount("B2X", D("19.4")/3) - all_orders[2].amount
3585 self.assertAlmostEqual(0, diff.value)
3586 self.assertEqual(D("0.0012"), all_orders[2].rate)
3587 self.assertEqual("buy", all_orders[2].action)
3588 self.assertEqual("long", all_orders[2].trade_type)
3589
3590 self.assertEqual(portfolio.Amount("BTC", D("0.0097")), all_orders[3].amount)
3591 self.assertEqual(D("16000"), all_orders[3].rate)
3592 self.assertEqual("sell", all_orders[3].action)
3593 self.assertEqual("long", all_orders[3].trade_type)
3594
3595 # Action 6b
3596 # TODO:
3597 # Move balances to margin
3598
3599 # Action 7
3600 # TODO
3601 # portfolio.TradeStore.run_orders()
3602
3603 with mock.patch.object(portfolio.time, "sleep") as sleep:
3604 # Action 8
3605 helper.follow_orders(verbose=False)
3606
3607 sleep.assert_called_with(30)
3608
3609 if __name__ == '__main__':
3610 unittest.main()