diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-01-21 20:46:39 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-01-21 21:41:26 +0100 |
commit | a9950fd073198f3c9dc938fd731d97c9821a3845 (patch) | |
tree | c613cdeb603e186c702f1dc4679ad6d0d9f5a7d0 | |
parent | c2644ba8db6e3890458af6a244aa3217e2ac4797 (diff) | |
download | Trader-a9950fd073198f3c9dc938fd731d97c9821a3845.tar.gz Trader-a9950fd073198f3c9dc938fd731d97c9821a3845.tar.zst Trader-a9950fd073198f3c9dc938fd731d97c9821a3845.zip |
Add some tests and cleanup exchange process
- Acceptance test for the whole exchange process
- Cut exchange two steps:
- Compute the outcome of the exchange
- Do all the sells
- Recompute the buys according to the sells result
- Do all the buys
-rw-r--r-- | portfolio.py | 91 | ||||
-rw-r--r-- | test.py | 263 |
2 files changed, 322 insertions, 32 deletions
diff --git a/portfolio.py b/portfolio.py index 6d51989..acb61b2 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -4,9 +4,7 @@ from decimal import Decimal as D | |||
4 | # Put your poloniex api key in market.py | 4 | # Put your poloniex api key in market.py |
5 | from market import market | 5 | from market import market |
6 | 6 | ||
7 | # FIXME: Améliorer le bid/ask | 7 | debug = False |
8 | # FIXME: J'essayais d'utiliser plus de bitcoins que j'en avais à disposition | ||
9 | # FIXME: better compute moves to avoid rounding errors | ||
10 | 8 | ||
11 | class Portfolio: | 9 | class Portfolio: |
12 | URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" | 10 | URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" |
@@ -78,8 +76,6 @@ class Portfolio: | |||
78 | } | 76 | } |
79 | 77 | ||
80 | class Amount: | 78 | class Amount: |
81 | MAX_DIGITS = 18 | ||
82 | |||
83 | def __init__(self, currency, value, linked_to=None, ticker=None, rate=None): | 79 | def __init__(self, currency, value, linked_to=None, ticker=None, rate=None): |
84 | self.currency = currency | 80 | self.currency = currency |
85 | self.value = D(value) | 81 | self.value = D(value) |
@@ -202,7 +198,7 @@ class Balance: | |||
202 | for key in hash_: | 198 | for key in hash_: |
203 | if key in ["info", "free", "used", "total"]: | 199 | if key in ["info", "free", "used", "total"]: |
204 | continue | 200 | continue |
205 | if hash_[key]["total"] > 0: | 201 | if hash_[key]["total"] > 0 or key in cls.known_balances: |
206 | cls.known_balances[key] = cls.from_hash(key, hash_[key]) | 202 | cls.known_balances[key] = cls.from_hash(key, hash_[key]) |
207 | 203 | ||
208 | @classmethod | 204 | @classmethod |
@@ -222,14 +218,22 @@ class Balance: | |||
222 | return amounts | 218 | return amounts |
223 | 219 | ||
224 | @classmethod | 220 | @classmethod |
225 | def prepare_trades(cls, market, base_currency="BTC", compute_value=None): | 221 | def prepare_trades(cls, market, base_currency="BTC", compute_value="average"): |
226 | cls.fetch_balances(market) | 222 | cls.fetch_balances(market) |
227 | values_in_base = cls.in_currency(base_currency, market) | 223 | values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) |
228 | total_base_value = sum(values_in_base.values()) | 224 | total_base_value = sum(values_in_base.values()) |
229 | new_repartition = cls.dispatch_assets(total_base_value) | 225 | new_repartition = cls.dispatch_assets(total_base_value) |
230 | # Recompute it in case we have new currencies | 226 | # Recompute it in case we have new currencies |
231 | values_in_base = cls.in_currency(base_currency, market) | 227 | values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) |
232 | Trade.compute_trades(values_in_base, new_repartition, market=market, compute_value=compute_value) | 228 | Trade.compute_trades(values_in_base, new_repartition, market=market) |
229 | |||
230 | @classmethod | ||
231 | def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None): | ||
232 | cls.fetch_balances(market) | ||
233 | values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value) | ||
234 | total_base_value = sum(values_in_base.values()) | ||
235 | new_repartition = cls.dispatch_assets(total_base_value) | ||
236 | Trade.compute_trades(values_in_base, new_repartition, only=only, market=market) | ||
233 | 237 | ||
234 | def __repr__(self): | 238 | def __repr__(self): |
235 | return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total)) | 239 | return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total)) |
@@ -302,21 +306,27 @@ class Trade: | |||
302 | return cls.get_ticker(c1, c2, market) | 306 | return cls.get_ticker(c1, c2, market) |
303 | 307 | ||
304 | @classmethod | 308 | @classmethod |
305 | def compute_trades(cls, values_in_base, new_repartition, market=None, compute_value=None): | 309 | def compute_trades(cls, values_in_base, new_repartition, only=None, market=None): |
306 | base_currency = sum(values_in_base.values()).currency | 310 | base_currency = sum(values_in_base.values()).currency |
307 | for currency in Balance.currencies(): | 311 | for currency in Balance.currencies(): |
308 | if currency == base_currency: | 312 | if currency == base_currency: |
309 | continue | 313 | continue |
310 | cls.trades[currency] = cls( | 314 | trade = cls( |
311 | values_in_base.get(currency, Amount(base_currency, 0)), | 315 | values_in_base.get(currency, Amount(base_currency, 0)), |
312 | new_repartition.get(currency, Amount(base_currency, 0)), | 316 | new_repartition.get(currency, Amount(base_currency, 0)), |
313 | currency, | 317 | currency, |
314 | market=market | 318 | market=market |
315 | ) | 319 | ) |
316 | if compute_value is not None: | 320 | if only is None or trade.action == only: |
317 | cls.trades[currency].prepare_order(compute_value=compute_value) | 321 | cls.trades[currency] = trade |
318 | return cls.trades | 322 | return cls.trades |
319 | 323 | ||
324 | @classmethod | ||
325 | def prepare_orders(cls, only=None, compute_value="default"): | ||
326 | for currency, trade in cls.trades.items(): | ||
327 | if only is None or trade.action == only: | ||
328 | trade.prepare_order(compute_value=compute_value) | ||
329 | |||
320 | @property | 330 | @property |
321 | def action(self): | 331 | def action(self): |
322 | if self.value_from == self.value_to: | 332 | if self.value_from == self.value_to: |
@@ -353,7 +363,7 @@ class Trade: | |||
353 | 363 | ||
354 | rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) | 364 | rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) |
355 | 365 | ||
356 | self.orders.append(Order(self.order_action(inverted), delta, rate, currency)) | 366 | self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.market)) |
357 | 367 | ||
358 | @classmethod | 368 | @classmethod |
359 | def compute_value(cls, ticker, action, compute_value="default"): | 369 | def compute_value(cls, ticker, action, compute_value="default"): |
@@ -362,22 +372,33 @@ class Trade: | |||
362 | return compute_value(ticker, action) | 372 | return compute_value(ticker, action) |
363 | 373 | ||
364 | @classmethod | 374 | @classmethod |
365 | def all_orders(cls): | 375 | def all_orders(cls, state=None): |
366 | return sum(map(lambda v: v.orders, cls.trades.values()), []) | 376 | all_orders = sum(map(lambda v: v.orders, cls.trades.values()), []) |
377 | if state is None: | ||
378 | return all_orders | ||
379 | else: | ||
380 | return list(filter(lambda o: o.status == state, all_orders)) | ||
381 | |||
382 | @classmethod | ||
383 | def run_orders(cls): | ||
384 | for order in cls.all_orders(state="pending"): | ||
385 | order.run() | ||
367 | 386 | ||
368 | @classmethod | 387 | @classmethod |
369 | def follow_orders(cls, market): | 388 | def follow_orders(cls, verbose=True, sleep=30): |
370 | orders = cls.all_orders() | 389 | orders = cls.all_orders() |
371 | finished_orders = [] | 390 | finished_orders = [] |
372 | while len(orders) != len(finished_orders): | 391 | while len(orders) != len(finished_orders): |
373 | time.sleep(30) | 392 | time.sleep(sleep) |
374 | for order in orders: | 393 | for order in orders: |
375 | if order in finished_orders: | 394 | if order in finished_orders: |
376 | continue | 395 | continue |
377 | if order.get_status(market) != "open": | 396 | if order.get_status() != "open": |
378 | finished_orders.append(order) | 397 | finished_orders.append(order) |
379 | print("finished {}".format(order)) | 398 | if verbose: |
380 | print("All orders finished") | 399 | print("finished {}".format(order)) |
400 | if verbose: | ||
401 | print("All orders finished") | ||
381 | 402 | ||
382 | def __repr__(self): | 403 | def __repr__(self): |
383 | return "Trade({} -> {} in {}, {})".format( | 404 | return "Trade({} -> {} in {}, {})".format( |
@@ -387,15 +408,16 @@ class Trade: | |||
387 | self.action) | 408 | self.action) |
388 | 409 | ||
389 | class Order: | 410 | class Order: |
390 | DEBUG = True | 411 | DEBUG = debug |
391 | 412 | ||
392 | def __init__(self, action, amount, rate, base_currency): | 413 | def __init__(self, action, amount, rate, base_currency, market): |
393 | self.action = action | 414 | self.action = action |
394 | self.amount = amount | 415 | self.amount = amount |
395 | self.rate = rate | 416 | self.rate = rate |
396 | self.base_currency = base_currency | 417 | self.base_currency = base_currency |
418 | self.market = market | ||
397 | self.result = None | 419 | self.result = None |
398 | self.status = "not run" | 420 | self.status = "pending" |
399 | 421 | ||
400 | def __repr__(self): | 422 | def __repr__(self): |
401 | return "Order({} {} at {} {} [{}])".format( | 423 | return "Order({} {} at {} {} [{}])".format( |
@@ -406,7 +428,15 @@ class Order: | |||
406 | self.status | 428 | self.status |
407 | ) | 429 | ) |
408 | 430 | ||
409 | def run(self, market): | 431 | @property |
432 | def pending(self): | ||
433 | return self.status == "pending" | ||
434 | |||
435 | @property | ||
436 | def finished(self): | ||
437 | return self.status == "closed" or self.status == "canceled" | ||
438 | |||
439 | def run(self): | ||
410 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) | 440 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) |
411 | amount = self.amount.value | 441 | amount = self.amount.value |
412 | 442 | ||
@@ -415,20 +445,21 @@ class Order: | |||
415 | symbol, self.action, amount, self.rate)) | 445 | symbol, self.action, amount, self.rate)) |
416 | else: | 446 | else: |
417 | try: | 447 | try: |
418 | self.result = market.create_order(symbol, 'limit', self.action, amount, price=self.rate) | 448 | self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate) |
419 | self.status = "open" | 449 | self.status = "open" |
420 | except Exception: | 450 | except Exception: |
421 | pass | 451 | pass |
422 | 452 | ||
423 | def get_status(self, market): | 453 | def get_status(self): |
424 | # other states are "closed" and "canceled" | 454 | # other states are "closed" and "canceled" |
425 | if self.status == "open": | 455 | if self.status == "open": |
426 | result = market.fetch_order(self.result['id']) | 456 | result = self.market.fetch_order(self.result['id']) |
427 | self.status = result["status"] | 457 | self.status = result["status"] |
428 | return self.status | 458 | return self.status |
429 | 459 | ||
430 | def print_orders(market, base_currency="BTC"): | 460 | def print_orders(market, base_currency="BTC"): |
431 | Balance.prepare_trades(market, base_currency=base_currency, compute_value="average") | 461 | Balance.prepare_trades(market, base_currency=base_currency, compute_value="average") |
462 | Trade.prepare_orders(compute_value="average") | ||
432 | for currency, balance in Balance.known_balances.items(): | 463 | for currency, balance in Balance.known_balances.items(): |
433 | print(balance) | 464 | print(balance) |
434 | for currency, trade in Trade.trades.items(): | 465 | for currency, trade in Trade.trades.items(): |
@@ -442,7 +473,7 @@ def make_orders(market, base_currency="BTC"): | |||
442 | print(trade) | 473 | print(trade) |
443 | for order in trade.orders: | 474 | for order in trade.orders: |
444 | print("\t", order, sep="") | 475 | print("\t", order, sep="") |
445 | order.run(market) | 476 | order.run() |
446 | 477 | ||
447 | if __name__ == '__main__': | 478 | if __name__ == '__main__': |
448 | print_orders(market) | 479 | print_orders(market) |
@@ -256,6 +256,11 @@ class BalanceTest(unittest.TestCase): | |||
256 | "info": "bar", | 256 | "info": "bar", |
257 | "used": "baz", | 257 | "used": "baz", |
258 | "total": "bazz", | 258 | "total": "bazz", |
259 | "ETC": { | ||
260 | "free": 0.0, | ||
261 | "used": 0.0, | ||
262 | "total": 0.0 | ||
263 | }, | ||
259 | "USDT": { | 264 | "USDT": { |
260 | "free": 6.0, | 265 | "free": 6.0, |
261 | "used": 1.2, | 266 | "used": 1.2, |
@@ -327,7 +332,12 @@ class BalanceTest(unittest.TestCase): | |||
327 | 332 | ||
328 | portfolio.Balance.fetch_balances(portfolio.market) | 333 | portfolio.Balance.fetch_balances(portfolio.market) |
329 | self.assertNotIn("XMR", portfolio.Balance.currencies()) | 334 | self.assertNotIn("XMR", portfolio.Balance.currencies()) |
330 | self.assertEqual(["USDT", "XVG"], list(portfolio.Balance.currencies())) | 335 | self.assertListEqual(["USDT", "XVG"], list(portfolio.Balance.currencies())) |
336 | |||
337 | portfolio.Balance.known_balances["ETC"] = portfolio.Balance("ETC", "1", "0", "1") | ||
338 | portfolio.Balance.fetch_balances(portfolio.market) | ||
339 | self.assertEqual(0, portfolio.Balance.known_balances["ETC"].total) | ||
340 | self.assertListEqual(["USDT", "XVG", "ETC"], list(portfolio.Balance.currencies())) | ||
331 | 341 | ||
332 | @mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand") | 342 | @mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand") |
333 | @mock.patch.object(portfolio.market, "fetch_balance") | 343 | @mock.patch.object(portfolio.market, "fetch_balance") |
@@ -362,7 +372,7 @@ class BalanceTest(unittest.TestCase): | |||
362 | return { "average": D("0.000001") } | 372 | return { "average": D("0.000001") } |
363 | if c1 == "XEM" and c2 == "BTC": | 373 | if c1 == "XEM" and c2 == "BTC": |
364 | return { "average": D("0.001") } | 374 | return { "average": D("0.001") } |
365 | raise Exception("Should be called with {}, {}".format(c1, c2)) | 375 | self.fail("Should be called with {}, {}".format(c1, c2)) |
366 | get_ticker.side_effect = _get_ticker | 376 | get_ticker.side_effect = _get_ticker |
367 | 377 | ||
368 | market = mock.Mock() | 378 | market = mock.Mock() |
@@ -388,6 +398,10 @@ class BalanceTest(unittest.TestCase): | |||
388 | self.assertEqual(D("0.2525"), call[0][1]["BTC"].value) | 398 | self.assertEqual(D("0.2525"), call[0][1]["BTC"].value) |
389 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) | 399 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) |
390 | 400 | ||
401 | @unittest.skip("TODO") | ||
402 | def test_update_trades(self): | ||
403 | pass | ||
404 | |||
391 | def test__repr(self): | 405 | def test__repr(self): |
392 | balance = portfolio.Balance("BTX", 3, 1, 2) | 406 | balance = portfolio.Balance("BTX", 3, 1, 2) |
393 | self.assertEqual("Balance(BTX [1.00000000 BTX/2.00000000 BTX/3.00000000 BTX])", repr(balance)) | 407 | self.assertEqual("Balance(BTX [1.00000000 BTX/2.00000000 BTX/3.00000000 BTX])", repr(balance)) |
@@ -520,5 +534,250 @@ class TradeTest(unittest.TestCase): | |||
520 | def tearDown(self): | 534 | def tearDown(self): |
521 | self.patcher.stop() | 535 | self.patcher.stop() |
522 | 536 | ||
537 | class AcceptanceTest(unittest.TestCase): | ||
538 | import time | ||
539 | |||
540 | def setUp(self): | ||
541 | super(AcceptanceTest, self).setUp() | ||
542 | |||
543 | self.patchers = [ | ||
544 | mock.patch.multiple(portfolio.Balance, known_balances={}), | ||
545 | mock.patch.multiple(portfolio.Portfolio, data=None, liquidities={}), | ||
546 | mock.patch.multiple(portfolio.Trade, | ||
547 | ticker_cache={}, | ||
548 | ticker_cache_timestamp=self.time.time(), | ||
549 | fees_cache={}, | ||
550 | trades={}), | ||
551 | mock.patch.multiple(portfolio.Computation, | ||
552 | computations=portfolio.Computation.computations) | ||
553 | ] | ||
554 | for patcher in self.patchers: | ||
555 | patcher.start() | ||
556 | |||
557 | def test_success_sell_only_necessary(self): | ||
558 | fetch_balance = { | ||
559 | "ETH": { | ||
560 | "free": D("1.0"), | ||
561 | "used": D("0.0"), | ||
562 | "total": D("1.0"), | ||
563 | }, | ||
564 | "ETC": { | ||
565 | "free": D("4.0"), | ||
566 | "used": D("0.0"), | ||
567 | "total": D("4.0"), | ||
568 | }, | ||
569 | "XVG": { | ||
570 | "free": D("1000.0"), | ||
571 | "used": D("0.0"), | ||
572 | "total": D("1000.0"), | ||
573 | }, | ||
574 | } | ||
575 | repartition = { | ||
576 | "ETH": 2500, | ||
577 | "ETC": 2500, | ||
578 | "BTC": 4000, | ||
579 | "BTD": 500, | ||
580 | "USDT": 500, | ||
581 | } | ||
582 | |||
583 | def fetch_ticker(symbol): | ||
584 | if symbol == "ETH/BTC": | ||
585 | return { | ||
586 | "symbol": "ETH/BTC", | ||
587 | "bid": D("0.14"), | ||
588 | "ask": D("0.16") | ||
589 | } | ||
590 | if symbol == "ETC/BTC": | ||
591 | return { | ||
592 | "symbol": "ETC/BTC", | ||
593 | "bid": D("0.002"), | ||
594 | "ask": D("0.003") | ||
595 | } | ||
596 | if symbol == "XVG/BTC": | ||
597 | return { | ||
598 | "symbol": "XVG/BTC", | ||
599 | "bid": D("0.00003"), | ||
600 | "ask": D("0.00005") | ||
601 | } | ||
602 | if symbol == "BTD/BTC": | ||
603 | return { | ||
604 | "symbol": "BTD/BTC", | ||
605 | "bid": D("0.0008"), | ||
606 | "ask": D("0.0012") | ||
607 | } | ||
608 | if symbol == "USDT/BTC": | ||
609 | raise portfolio.ccxt.ExchangeError | ||
610 | if symbol == "BTC/USDT": | ||
611 | return { | ||
612 | "symbol": "BTC/USDT", | ||
613 | "bid": D("14000"), | ||
614 | "ask": D("16000") | ||
615 | } | ||
616 | self.fail("Shouldn't have been called with {}".format(symbol)) | ||
617 | |||
618 | market = mock.Mock() | ||
619 | market.fetch_balance.return_value = fetch_balance | ||
620 | market.fetch_ticker.side_effect = fetch_ticker | ||
621 | with mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand", return_value=repartition): | ||
622 | # Action 1 | ||
623 | portfolio.Balance.prepare_trades(market) | ||
624 | |||
625 | balances = portfolio.Balance.known_balances | ||
626 | self.assertEqual(portfolio.Amount("ETH", 1), balances["ETH"].total) | ||
627 | self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total) | ||
628 | self.assertEqual(portfolio.Amount("XVG", 1000), balances["XVG"].total) | ||
629 | |||
630 | |||
631 | trades = portfolio.Trade.trades | ||
632 | self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades["ETH"].value_from) | ||
633 | self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades["ETH"].value_to) | ||
634 | self.assertEqual("sell", trades["ETH"].action) | ||
635 | |||
636 | self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["ETC"].value_from) | ||
637 | self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades["ETC"].value_to) | ||
638 | self.assertEqual("buy", trades["ETC"].action) | ||
639 | |||
640 | self.assertNotIn("BTC", trades) | ||
641 | |||
642 | self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["BTD"].value_from) | ||
643 | self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["BTD"].value_to) | ||
644 | self.assertEqual("buy", trades["BTD"].action) | ||
645 | |||
646 | self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["USDT"].value_from) | ||
647 | self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["USDT"].value_to) | ||
648 | self.assertEqual("buy", trades["USDT"].action) | ||
649 | |||
650 | self.assertEqual(portfolio.Amount("BTC", D("0.04")), trades["XVG"].value_from) | ||
651 | self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["XVG"].value_to) | ||
652 | self.assertEqual("sell", trades["XVG"].action) | ||
653 | |||
654 | # Action 2 | ||
655 | portfolio.Trade.prepare_orders(only="sell", compute_value=lambda x, y: x["bid"] * D("1.001")) | ||
656 | |||
657 | all_orders = portfolio.Trade.all_orders() | ||
658 | self.assertEqual(2, len(all_orders)) | ||
659 | self.assertEqual(2, 3*all_orders[0].amount.value) | ||
660 | self.assertEqual(D("0.14014"), all_orders[0].rate) | ||
661 | self.assertEqual(1000, all_orders[1].amount.value) | ||
662 | self.assertEqual(D("0.00003003"), all_orders[1].rate) | ||
663 | |||
664 | |||
665 | def create_order(symbol, type, action, amount, price=None): | ||
666 | self.assertEqual("limit", type) | ||
667 | if symbol == "ETH/BTC": | ||
668 | self.assertEqual("bid", action) | ||
669 | self.assertEqual(2, 3*amount) | ||
670 | self.assertEqual(D("0.14014"), price) | ||
671 | elif symbol == "XVG/BTC": | ||
672 | self.assertEqual("bid", action) | ||
673 | self.assertEqual(1000, amount) | ||
674 | self.assertEqual(D("0.00003003"), price) | ||
675 | else: | ||
676 | self.fail("I shouldn't have been called") | ||
677 | |||
678 | return { | ||
679 | "id": symbol, | ||
680 | } | ||
681 | market.create_order.side_effect = create_order | ||
682 | |||
683 | # Action 3 | ||
684 | portfolio.Trade.run_orders() | ||
685 | |||
686 | self.assertEqual("open", all_orders[0].status) | ||
687 | self.assertEqual("open", all_orders[1].status) | ||
688 | |||
689 | market.fetch_order.return_value = { "status": "closed" } | ||
690 | with mock.patch.object(portfolio.time, "sleep") as sleep: | ||
691 | # Action 4 | ||
692 | portfolio.Trade.follow_orders(verbose=False) | ||
693 | |||
694 | sleep.assert_called_with(30) | ||
695 | |||
696 | for order in all_orders: | ||
697 | self.assertEqual("closed", order.status) | ||
698 | |||
699 | fetch_balance = { | ||
700 | "ETH": { | ||
701 | "free": D("1.0") / 3, | ||
702 | "used": D("0.0"), | ||
703 | "total": D("1.0") / 3, | ||
704 | }, | ||
705 | "BTC": { | ||
706 | "free": D("0.134"), | ||
707 | "used": D("0.0"), | ||
708 | "total": D("0.134"), | ||
709 | }, | ||
710 | "ETC": { | ||
711 | "free": D("4.0"), | ||
712 | "used": D("0.0"), | ||
713 | "total": D("4.0"), | ||
714 | }, | ||
715 | "XVG": { | ||
716 | "free": D("0.0"), | ||
717 | "used": D("0.0"), | ||
718 | "total": D("0.0"), | ||
719 | }, | ||
720 | } | ||
721 | market.fetch_balance.return_value = fetch_balance | ||
722 | |||
723 | with mock.patch.object(portfolio.Portfolio, "repartition_pertenthousand", return_value=repartition): | ||
724 | # Action 5 | ||
725 | portfolio.Balance.update_trades(market, only="buy", compute_value="average") | ||
726 | |||
727 | balances = portfolio.Balance.known_balances | ||
728 | self.assertEqual(portfolio.Amount("ETH", 1 / D("3")), balances["ETH"].total) | ||
729 | self.assertEqual(portfolio.Amount("ETC", 4), balances["ETC"].total) | ||
730 | self.assertEqual(portfolio.Amount("BTC", D("0.134")), balances["BTC"].total) | ||
731 | self.assertEqual(portfolio.Amount("XVG", 0), balances["XVG"].total) | ||
732 | |||
733 | |||
734 | trades = portfolio.Trade.trades | ||
735 | self.assertEqual(portfolio.Amount("BTC", D("0.15")), trades["ETH"].value_from) | ||
736 | self.assertEqual(portfolio.Amount("BTC", D("0.05")), trades["ETH"].value_to) | ||
737 | self.assertEqual("sell", trades["ETH"].action) | ||
738 | |||
739 | self.assertEqual(portfolio.Amount("BTC", D("0.01")), trades["ETC"].value_from) | ||
740 | self.assertEqual(portfolio.Amount("BTC", D("0.0485")), trades["ETC"].value_to) | ||
741 | self.assertEqual("buy", trades["ETC"].action) | ||
742 | |||
743 | self.assertNotIn("BTC", trades) | ||
744 | |||
745 | self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["BTD"].value_from) | ||
746 | self.assertEqual(portfolio.Amount("BTC", D("0.0097")), trades["BTD"].value_to) | ||
747 | self.assertEqual("buy", trades["BTD"].action) | ||
748 | |||
749 | self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["USDT"].value_from) | ||
750 | self.assertEqual(portfolio.Amount("BTC", D("0.0097")), trades["USDT"].value_to) | ||
751 | self.assertEqual("buy", trades["USDT"].action) | ||
752 | |||
753 | self.assertEqual(portfolio.Amount("BTC", D("0.04")), trades["XVG"].value_from) | ||
754 | self.assertEqual(portfolio.Amount("BTC", D("0.00")), trades["XVG"].value_to) | ||
755 | self.assertEqual("sell", trades["XVG"].action) | ||
756 | |||
757 | # Action 6 | ||
758 | portfolio.Trade.prepare_orders(only="buy", compute_value=lambda x, y: x["ask"] * D("0.999")) | ||
759 | |||
760 | all_orders = portfolio.Trade.all_orders(state="pending") | ||
761 | self.assertEqual(3, len(all_orders)) | ||
762 | self.assertEqual(portfolio.Amount("ETC", D("15.4")), all_orders[0].amount) | ||
763 | self.assertEqual(D("0.002997"), all_orders[0].rate) | ||
764 | self.assertEqual("ask", all_orders[0].action) | ||
765 | self.assertEqual(portfolio.Amount("BTD", D("9.7")), all_orders[1].amount) | ||
766 | self.assertEqual(D("0.0011988"), all_orders[1].rate) | ||
767 | self.assertEqual("ask", all_orders[1].action) | ||
768 | self.assertEqual(portfolio.Amount("BTC", D("0.0097")), all_orders[2].amount) | ||
769 | self.assertEqual(D("15984"), all_orders[2].rate) | ||
770 | self.assertEqual("bid", all_orders[2].action) | ||
771 | |||
772 | with mock.patch.object(portfolio.time, "sleep") as sleep: | ||
773 | # Action 7 | ||
774 | portfolio.Trade.follow_orders(verbose=False) | ||
775 | |||
776 | sleep.assert_called_with(30) | ||
777 | |||
778 | def tearDown(self): | ||
779 | for patcher in self.patchers: | ||
780 | patcher.stop() | ||
781 | |||
523 | if __name__ == '__main__': | 782 | if __name__ == '__main__': |
524 | unittest.main() | 783 | unittest.main() |