aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-01-21 20:46:39 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-01-21 21:41:26 +0100
commita9950fd073198f3c9dc938fd731d97c9821a3845 (patch)
treec613cdeb603e186c702f1dc4679ad6d0d9f5a7d0
parentc2644ba8db6e3890458af6a244aa3217e2ac4797 (diff)
downloadTrader-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.py91
-rw-r--r--test.py263
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
5from market import market 5from market import market
6 6
7# FIXME: Améliorer le bid/ask 7debug = 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
11class Portfolio: 9class 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
80class Amount: 78class 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
389class Order: 410class 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
430def print_orders(market, base_currency="BTC"): 460def 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
447if __name__ == '__main__': 478if __name__ == '__main__':
448 print_orders(market) 479 print_orders(market)
diff --git a/test.py b/test.py
index edf6d01..2589896 100644
--- a/test.py
+++ b/test.py
@@ -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
537class 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
523if __name__ == '__main__': 782if __name__ == '__main__':
524 unittest.main() 783 unittest.main()