aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Makefile32
-rw-r--r--portfolio.py98
-rw-r--r--store.py33
-rw-r--r--test.py440
5 files changed, 530 insertions, 75 deletions
diff --git a/.gitignore b/.gitignore
index bee8a64..bd500b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
1__pycache__ 1__pycache__
2.coverage
3/htmlcov
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..1464886
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,32 @@
1test:
2 python test.py
3
4run:
5 python portfolio.py
6
7test_coverage_unit:
8 coverage run --source=. --omit=test.py test.py --onlyunit
9 coverage report -m
10
11test_coverage_unit_html: test_coverage_unit
12 coverage html
13 rm ~/hosts/www.immae.eu/htmlcov -rf && cp -r htmlcov ~/hosts/www.immae.eu
14 @echo "coverage in https://www.immae.eu/htmlcov"
15
16test_coverage_acceptance:
17 coverage run --source=. --omit=test.py test.py --onlyacceptance
18 coverage report -m
19
20test_coverage_acceptance_html: test_coverage_acceptance
21 coverage html
22 rm ~/hosts/www.immae.eu/htmlcov -rf && cp -r htmlcov ~/hosts/www.immae.eu
23 @echo "coverage in https://www.immae.eu/htmlcov"
24
25test_coverage_all:
26 coverage run --source=. --omit=test.py test.py
27 coverage report -m
28
29test_coverage_all_html: test_coverage_all
30 coverage html
31 rm ~/hosts/www.immae.eu/htmlcov -rf && cp -r htmlcov ~/hosts/www.immae.eu
32 @echo "coverage in https://www.immae.eu/htmlcov"
diff --git a/portfolio.py b/portfolio.py
index efd9b84..b629966 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -149,7 +149,7 @@ class Amount:
149 149
150 def __floordiv__(self, value): 150 def __floordiv__(self, value):
151 if not isinstance(value, (int, float, D)): 151 if not isinstance(value, (int, float, D)):
152 raise TypeError("Amount may only be multiplied by integers") 152 raise TypeError("Amount may only be divided by numbers")
153 return Amount(self.currency, self.value / value) 153 return Amount(self.currency, self.value / value)
154 154
155 def __truediv__(self, value): 155 def __truediv__(self, value):
@@ -290,11 +290,10 @@ class Trade:
290 else: 290 else:
291 return "long" 291 return "long"
292 292
293 @property 293 def filled_amount(self, in_base_currency=False):
294 def filled_amount(self):
295 filled_amount = 0 294 filled_amount = 0
296 for order in self.orders: 295 for order in self.orders:
297 filled_amount += order.filled_amount 296 filled_amount += order.filled_amount(in_base_currency=in_base_currency)
298 return filled_amount 297 return filled_amount
299 298
300 def update_order(self, order, tick): 299 def update_order(self, order, tick):
@@ -329,54 +328,69 @@ class Trade:
329 if inverted: 328 if inverted:
330 ticker = ticker["original"] 329 ticker = ticker["original"]
331 rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) 330 rate = Computation.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
332 # 0.1
333 331
334 delta_in_base = abs(self.value_from - self.value_to) 332 delta_in_base = abs(self.value_from - self.value_to)
335 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case) 333 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
336 334
337 if not inverted: 335 if not inverted:
338 currency = self.base_currency 336 base_currency = self.base_currency
339 # BTC 337 # BTC
340 if self.action == "dispose": 338 if self.action == "dispose":
341 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it 339 filled = self.filled_amount(in_base_currency=False)
342 # At rate 1 Foo = 0.1 BTC 340 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
343 value_from = self.value_from.linked_to 341 # I have 10 BTC worth of FOO, and I want to sell 9 BTC
344 # value_from = 100 FOO 342 # worth of it, computed first with rate 10 FOO = 1 BTC.
345 value_to = self.value_to.in_currency(self.currency, self.market, rate=1/self.value_from.rate) 343 # -> I "sell" "90" FOO at proposed rate "rate".
346 # value_to = 10 FOO (1 BTC * 1/0.1) 344
347 delta = abs(value_to - value_from) 345 delta = delta - filled
348 # delta = 90 FOO 346 # I already sold 60 FOO, 30 left
349 # Action: "sell" "90 FOO" at rate "0.1" "BTC" on "market"
350
351 # Note: no rounding error possible: if we have value_to == 0, then delta == value_from
352 else: 347 else:
353 delta = delta_in_base.in_currency(self.currency, self.market, rate=1/rate) 348 filled = self.filled_amount(in_base_currency=True)
354 # I want to buy 9 / 0.1 FOO 349 delta = (delta_in_base - filled).in_currency(self.currency, self.market, rate=1/rate)
355 # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market" 350 # I want to buy 9 BTC worth of FOO, computed with rate
351 # 10 FOO = 1 BTC
352 # -> I "buy" "9 / rate" FOO at proposed rate "rate"
353
354 # I already bought 3 / rate FOO, 6 / rate left
356 else: 355 else:
357 currency = self.currency 356 base_currency = self.currency
358 # FOO 357 # FOO
359 delta = delta_in_base 358 if self.action == "dispose":
360 # sell: 359 filled = self.filled_amount(in_base_currency=True)
361 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it 360 # Base is FOO
362 # At rate 1 Foo = 0.1 BTC 361
363 # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market 362 delta = (delta_in_base.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
364 # buy: 363 - filled).in_currency(self.base_currency, self.market, rate=1/rate)
365 # I want to buy 9 / 0.1 FOO 364 # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
366 # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market" 365 # computed at rate 1 Foo = 0.01 BTC
367 if self.value_to == 0: 366 # Computation says I should sell it at 125 FOO / BTC
368 rate = self.value_from.linked_to.value / self.value_from.value 367 # -> delta_in_base = 9 BTC
369 # Recompute the rate to avoid any rounding error 368 # -> delta = (9 * 1/0.01 FOO) * 1/125 = 7.2 BTC
369 # Action: "buy" "7.2 BTC" at rate "125" "FOO" on market
370
371 # I already bought 300/125 BTC, only 600/125 left
372 else:
373 filled = self.filled_amount(in_base_currency=False)
374 # Base is FOO
375
376 delta = delta_in_base
377 # I have 1 BTC worth of FOO, and I want to buy 9 BTC worth of it
378 # At rate 100 Foo / BTC
379 # Computation says I should buy it at 125 FOO / BTC
380 # -> delta_in_base = 9 BTC
381 # Action: "sell" "9 BTC" at rate "125" "FOO" on market
382
383 delta = delta - filled
384 # I already sold 4 BTC, only 5 left
370 385
371 close_if_possible = (self.value_to == 0) 386 close_if_possible = (self.value_to == 0)
372 387
373 if delta <= self.filled_amount: 388 if delta <= 0:
374 print("Less to do than already filled: {} <= {}".format(delta, 389 print("Less to do than already filled: {}".format(delta))
375 self.filled_amount))
376 return 390 return
377 391
378 self.orders.append(Order(self.order_action(inverted), 392 self.orders.append(Order(self.order_action(inverted),
379 delta - self.filled_amount, rate, currency, self.trade_type, 393 delta, rate, base_currency, self.trade_type,
380 self.market, self, close_if_possible=close_if_possible)) 394 self.market, self, close_if_possible=close_if_possible))
381 395
382 def __repr__(self): 396 def __repr__(self):
@@ -497,15 +511,17 @@ class Order:
497 def remaining_amount(self): 511 def remaining_amount(self):
498 if self.status == "open": 512 if self.status == "open":
499 self.fetch() 513 self.fetch()
500 return self.amount - self.filled_amount 514 return self.amount - self.filled_amount()
501 515
502 @property 516 def filled_amount(self, in_base_currency=False):
503 def filled_amount(self):
504 if self.status == "open": 517 if self.status == "open":
505 self.fetch() 518 self.fetch()
506 filled_amount = Amount(self.amount.currency, 0) 519 filled_amount = 0
507 for mouvement in self.mouvements: 520 for mouvement in self.mouvements:
508 filled_amount += mouvement.total 521 if in_base_currency:
522 filled_amount += mouvement.total_in_base
523 else:
524 filled_amount += mouvement.total
509 return filled_amount 525 return filled_amount
510 526
511 def fetch_mouvements(self): 527 def fetch_mouvements(self):
diff --git a/store.py b/store.py
index 4e46878..841a0fc 100644
--- a/store.py
+++ b/store.py
@@ -53,22 +53,27 @@ class TradeStore:
53 continue 53 continue
54 value_from = values_in_base.get(currency, portfolio.Amount(base_currency, 0)) 54 value_from = values_in_base.get(currency, portfolio.Amount(base_currency, 0))
55 value_to = new_repartition.get(currency, portfolio.Amount(base_currency, 0)) 55 value_to = new_repartition.get(currency, portfolio.Amount(base_currency, 0))
56
56 if value_from.value * value_to.value < 0: 57 if value_from.value * value_to.value < 0:
57 trade_1 = portfolio.Trade(value_from, portfolio.Amount(base_currency, 0), currency, market=market) 58 cls.add_trade_if_matching(
58 if only is None or trade_1.action == only: 59 value_from, portfolio.Amount(base_currency, 0),
59 cls.all.append(trade_1) 60 currency, only=only, market=market)
60 trade_2 = portfolio.Trade(portfolio.Amount(base_currency, 0), value_to, currency, market=market) 61 cls.add_trade_if_matching(
61 if only is None or trade_2.action == only: 62 portfolio.Amount(base_currency, 0), value_to,
62 cls.all.append(trade_2) 63 currency, only=only, market=market)
63 else: 64 else:
64 trade = portfolio.Trade( 65 cls.add_trade_if_matching(value_from, value_to,
65 value_from, 66 currency, only=only, market=market)
66 value_to, 67
67 currency, 68 @classmethod
68 market=market 69 def add_trade_if_matching(cls, value_from, value_to, currency,
69 ) 70 only=None, market=None):
70 if only is None or trade.action == only: 71 trade = portfolio.Trade(value_from, value_to, currency,
71 cls.all.append(trade) 72 market=market)
73 if only is None or trade.action == only:
74 cls.all.append(trade)
75 return True
76 return False
72 77
73 @classmethod 78 @classmethod
74 def prepare_orders(cls, only=None, compute_value="default"): 79 def prepare_orders(cls, only=None, compute_value="default"):
diff --git a/test.py b/test.py
index aae1dc8..be3ad4a 100644
--- a/test.py
+++ b/test.py
@@ -1,3 +1,4 @@
1import sys
1import portfolio 2import portfolio
2import unittest 3import unittest
3from decimal import Decimal as D 4from decimal import Decimal as D
@@ -7,6 +8,16 @@ import requests_mock
7from io import StringIO 8from io import StringIO
8import helper 9import helper
9 10
11limits = ["acceptance", "unit"]
12for test_type in limits:
13 if "--no{}".format(test_type) in sys.argv:
14 sys.argv.remove("--no{}".format(test_type))
15 limits.remove(test_type)
16 if "--only{}".format(test_type) in sys.argv:
17 sys.argv.remove("--only{}".format(test_type))
18 limits = [test_type]
19 break
20
10class WebMockTestCase(unittest.TestCase): 21class WebMockTestCase(unittest.TestCase):
11 import time 22 import time
12 23
@@ -39,6 +50,7 @@ class WebMockTestCase(unittest.TestCase):
39 self.wm.stop() 50 self.wm.stop()
40 super(WebMockTestCase, self).tearDown() 51 super(WebMockTestCase, self).tearDown()
41 52
53@unittest.skipUnless("unit" in limits, "Unit skipped")
42class PortfolioTest(WebMockTestCase): 54class PortfolioTest(WebMockTestCase):
43 def fill_data(self): 55 def fill_data(self):
44 if self.json_response is not None: 56 if self.json_response is not None:
@@ -140,6 +152,7 @@ class PortfolioTest(WebMockTestCase):
140 self.assertEqual(expected_medium, portfolio.Portfolio.repartition(liquidity="medium")) 152 self.assertEqual(expected_medium, portfolio.Portfolio.repartition(liquidity="medium"))
141 self.assertEqual(expected_high, portfolio.Portfolio.repartition(liquidity="high")) 153 self.assertEqual(expected_high, portfolio.Portfolio.repartition(liquidity="high"))
142 154
155@unittest.skipUnless("unit" in limits, "Unit skipped")
143class AmountTest(WebMockTestCase): 156class AmountTest(WebMockTestCase):
144 def test_values(self): 157 def test_values(self):
145 amount = portfolio.Amount("BTC", "0.65") 158 amount = portfolio.Amount("BTC", "0.65")
@@ -250,6 +263,9 @@ class AmountTest(WebMockTestCase):
250 self.assertEqual(D("5.5"), (amount / 2).value) 263 self.assertEqual(D("5.5"), (amount / 2).value)
251 self.assertEqual(D("4.4"), (amount / D("2.5")).value) 264 self.assertEqual(D("4.4"), (amount / D("2.5")).value)
252 265
266 with self.assertRaises(Exception):
267 amount / amount
268
253 def test__truediv(self): 269 def test__truediv(self):
254 amount = portfolio.Amount("XEM", 11) 270 amount = portfolio.Amount("XEM", 11)
255 271
@@ -363,6 +379,7 @@ class AmountTest(WebMockTestCase):
363 amount2.linked_to = amount3 379 amount2.linked_to = amount3
364 self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1)) 380 self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1))
365 381
382@unittest.skipUnless("unit" in limits, "Unit skipped")
366class BalanceTest(WebMockTestCase): 383class BalanceTest(WebMockTestCase):
367 def test_values(self): 384 def test_values(self):
368 balance = portfolio.Balance("BTC", { 385 balance = portfolio.Balance("BTC", {
@@ -405,16 +422,27 @@ class BalanceTest(WebMockTestCase):
405 "exchange_used": 1, "exchange_free": 2 }) 422 "exchange_used": 1, "exchange_free": 2 })
406 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance)) 423 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance))
407 424
425 balance = portfolio.Balance("BTX", { "exchange_total": 1, "exchange_used": 1})
426 self.assertEqual("Balance(BTX Exch: [❌1.00000000 BTX])", repr(balance))
427
408 balance = portfolio.Balance("BTX", { "margin_total": 3, 428 balance = portfolio.Balance("BTX", { "margin_total": 3,
409 "margin_borrowed": 1, "margin_free": 2 }) 429 "margin_borrowed": 1, "margin_free": 2 })
410 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + borrowed 1.00000000 BTX = 3.00000000 BTX])", repr(balance)) 430 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + borrowed 1.00000000 BTX = 3.00000000 BTX])", repr(balance))
411 431
432 balance = portfolio.Balance("BTX", { "margin_total": 2, "margin_free": 2 })
433 self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX])", repr(balance))
434
412 balance = portfolio.Balance("BTX", { "margin_total": -3, 435 balance = portfolio.Balance("BTX", { "margin_total": -3,
413 "margin_borrowed_base_price": D("0.1"), 436 "margin_borrowed_base_price": D("0.1"),
414 "margin_borrowed_base_currency": "BTC", 437 "margin_borrowed_base_currency": "BTC",
415 "margin_lending_fees": D("0.002") }) 438 "margin_lending_fees": D("0.002") })
416 self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance)) 439 self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance))
417 440
441 balance = portfolio.Balance("BTX", { "margin_total": 1,
442 "margin_borrowed": 1, "exchange_free": 2, "exchange_total": 2})
443 self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [borrowed 1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance))
444
445@unittest.skipUnless("unit" in limits, "Unit skipped")
418class HelperTest(WebMockTestCase): 446class HelperTest(WebMockTestCase):
419 def test_get_ticker(self): 447 def test_get_ticker(self):
420 market = mock.Mock() 448 market = mock.Mock()
@@ -612,15 +640,153 @@ class HelperTest(WebMockTestCase):
612 self.assertEqual(D("0.01"), call[0][0]["XVG"].value) 640 self.assertEqual(D("0.01"), call[0][0]["XVG"].value)
613 self.assertEqual(D("1.01"), call[0][1]["BTC"].value) 641 self.assertEqual(D("1.01"), call[0][1]["BTC"].value)
614 642
615 @unittest.skip("TODO") 643 @mock.patch.object(portfolio.time, "sleep")
616 def test_follow_orders(self): 644 @mock.patch.object(portfolio.TradeStore, "all_orders")
617 pass 645 def test_follow_orders(self, all_orders, time_mock):
618 646 for verbose, debug, sleep in [
619 647 (True, False, None), (False, False, None),
648 (True, True, None), (True, False, 12),
649 (True, True, 12)]:
650 with self.subTest(sleep=sleep, debug=debug, verbose=verbose), \
651 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
652 portfolio.TradeStore.debug = debug
653 order_mock1 = mock.Mock()
654 order_mock2 = mock.Mock()
655 order_mock3 = mock.Mock()
656 all_orders.side_effect = [
657 [order_mock1, order_mock2],
658 [order_mock1, order_mock2],
659
660 [order_mock1, order_mock3],
661 [order_mock1, order_mock3],
662
663 [order_mock1, order_mock3],
664 [order_mock1, order_mock3],
665
666 []
667 ]
668
669 order_mock1.get_status.side_effect = ["open", "open", "closed"]
670 order_mock2.get_status.side_effect = ["open"]
671 order_mock3.get_status.side_effect = ["open", "closed"]
672
673 order_mock1.trade = mock.Mock()
674 order_mock2.trade = mock.Mock()
675 order_mock3.trade = mock.Mock()
676
677 helper.follow_orders(verbose=verbose, sleep=sleep)
678
679 order_mock1.trade.update_order.assert_any_call(order_mock1, 1)
680 order_mock1.trade.update_order.assert_any_call(order_mock1, 2)
681 self.assertEqual(2, order_mock1.trade.update_order.call_count)
682 self.assertEqual(3, order_mock1.get_status.call_count)
683
684 order_mock2.trade.update_order.assert_any_call(order_mock2, 1)
685 self.assertEqual(1, order_mock2.trade.update_order.call_count)
686 self.assertEqual(1, order_mock2.get_status.call_count)
687
688 order_mock3.trade.update_order.assert_any_call(order_mock3, 2)
689 self.assertEqual(1, order_mock3.trade.update_order.call_count)
690 self.assertEqual(2, order_mock3.get_status.call_count)
691
692 if sleep is None:
693 if debug:
694 time_mock.assert_called_with(7)
695 else:
696 time_mock.assert_called_with(30)
697 else:
698 time_mock.assert_called_with(sleep)
699
700 if verbose:
701 self.assertNotEqual("", stdout_mock.getvalue())
702 else:
703 self.assertEqual("", stdout_mock.getvalue())
704
705@unittest.skipUnless("unit" in limits, "Unit skipped")
620class TradeStoreTest(WebMockTestCase): 706class TradeStoreTest(WebMockTestCase):
621 @unittest.skip("TODO") 707 @mock.patch.object(portfolio.BalanceStore, "currencies")
622 def test_compute_trades(self): 708 @mock.patch.object(portfolio.TradeStore, "add_trade_if_matching")
623 pass 709 def test_compute_trades(self, add_trade_if_matching, currencies):
710 currencies.return_value = ["XMR", "DASH", "XVG", "BTC", "ETH"]
711
712 values_in_base = {
713 "XMR": portfolio.Amount("BTC", D("0.9")),
714 "DASH": portfolio.Amount("BTC", D("0.4")),
715 "XVG": portfolio.Amount("BTC", D("-0.5")),
716 "BTC": portfolio.Amount("BTC", D("0.5")),
717 }
718 new_repartition = {
719 "DASH": portfolio.Amount("BTC", D("0.5")),
720 "XVG": portfolio.Amount("BTC", D("0.1")),
721 "BTC": portfolio.Amount("BTC", D("0.4")),
722 "ETH": portfolio.Amount("BTC", D("0.3")),
723 }
724
725 portfolio.TradeStore.compute_trades(values_in_base,
726 new_repartition, only="only", market="market")
727
728 self.assertEqual(5, add_trade_if_matching.call_count)
729 add_trade_if_matching.assert_any_call(
730 portfolio.Amount("BTC", D("0.9")),
731 portfolio.Amount("BTC", 0),
732 "XMR", only="only", market="market"
733 )
734 add_trade_if_matching.assert_any_call(
735 portfolio.Amount("BTC", D("0.4")),
736 portfolio.Amount("BTC", D("0.5")),
737 "DASH", only="only", market="market"
738 )
739 add_trade_if_matching.assert_any_call(
740 portfolio.Amount("BTC", D("-0.5")),
741 portfolio.Amount("BTC", D("0")),
742 "XVG", only="only", market="market"
743 )
744 add_trade_if_matching.assert_any_call(
745 portfolio.Amount("BTC", D("0")),
746 portfolio.Amount("BTC", D("0.1")),
747 "XVG", only="only", market="market"
748 )
749 add_trade_if_matching.assert_any_call(
750 portfolio.Amount("BTC", D("0")),
751 portfolio.Amount("BTC", D("0.3")),
752 "ETH", only="only", market="market"
753 )
754
755 def test_add_trade_if_matching(self):
756 result = portfolio.TradeStore.add_trade_if_matching(
757 portfolio.Amount("BTC", D("0")),
758 portfolio.Amount("BTC", D("0.3")),
759 "ETH", only="nope", market="market"
760 )
761 self.assertEqual(0, len(portfolio.TradeStore.all))
762 self.assertEqual(False, result)
763
764 portfolio.TradeStore.all = []
765 result = portfolio.TradeStore.add_trade_if_matching(
766 portfolio.Amount("BTC", D("0")),
767 portfolio.Amount("BTC", D("0.3")),
768 "ETH", only=None, market="market"
769 )
770 self.assertEqual(1, len(portfolio.TradeStore.all))
771 self.assertEqual(True, result)
772
773 portfolio.TradeStore.all = []
774 result = portfolio.TradeStore.add_trade_if_matching(
775 portfolio.Amount("BTC", D("0")),
776 portfolio.Amount("BTC", D("0.3")),
777 "ETH", only="acquire", market="market"
778 )
779 self.assertEqual(1, len(portfolio.TradeStore.all))
780 self.assertEqual(True, result)
781
782 portfolio.TradeStore.all = []
783 result = portfolio.TradeStore.add_trade_if_matching(
784 portfolio.Amount("BTC", D("0")),
785 portfolio.Amount("BTC", D("0.3")),
786 "ETH", only="dispose", market="market"
787 )
788 self.assertEqual(0, len(portfolio.TradeStore.all))
789 self.assertEqual(False, result)
624 790
625 def test_prepare_orders(self): 791 def test_prepare_orders(self):
626 trade_mock1 = mock.Mock() 792 trade_mock1 = mock.Mock()
@@ -709,7 +875,7 @@ class TradeStoreTest(WebMockTestCase):
709 order_mock2.get_status.assert_called() 875 order_mock2.get_status.assert_called()
710 order_mock3.get_status.assert_called() 876 order_mock3.get_status.assert_called()
711 877
712 878@unittest.skipUnless("unit" in limits, "Unit skipped")
713class BalanceStoreTest(WebMockTestCase): 879class BalanceStoreTest(WebMockTestCase):
714 def setUp(self): 880 def setUp(self):
715 super(BalanceStoreTest, self).setUp() 881 super(BalanceStoreTest, self).setUp()
@@ -802,12 +968,14 @@ class BalanceStoreTest(WebMockTestCase):
802 repartition.return_value = { 968 repartition.return_value = {
803 "XEM": (D("0.75"), "long"), 969 "XEM": (D("0.75"), "long"),
804 "BTC": (D("0.26"), "long"), 970 "BTC": (D("0.26"), "long"),
971 "DASH": (D("0.10"), "short"),
805 } 972 }
806 973
807 amounts = portfolio.BalanceStore.dispatch_assets(portfolio.Amount("BTC", "10.1")) 974 amounts = portfolio.BalanceStore.dispatch_assets(portfolio.Amount("BTC", "11.1"))
808 self.assertIn("XEM", portfolio.BalanceStore.currencies()) 975 self.assertIn("XEM", portfolio.BalanceStore.currencies())
809 self.assertEqual(D("2.6"), amounts["BTC"].value) 976 self.assertEqual(D("2.6"), amounts["BTC"].value)
810 self.assertEqual(D("7.5"), amounts["XEM"].value) 977 self.assertEqual(D("7.5"), amounts["XEM"].value)
978 self.assertEqual(D("-1.0"), amounts["DASH"].value)
811 979
812 def test_currencies(self): 980 def test_currencies(self):
813 portfolio.BalanceStore.all = { 981 portfolio.BalanceStore.all = {
@@ -824,6 +992,7 @@ class BalanceStoreTest(WebMockTestCase):
824 } 992 }
825 self.assertListEqual(["BTC", "ETH"], list(portfolio.BalanceStore.currencies())) 993 self.assertListEqual(["BTC", "ETH"], list(portfolio.BalanceStore.currencies()))
826 994
995@unittest.skipUnless("unit" in limits, "Unit skipped")
827class ComputationTest(WebMockTestCase): 996class ComputationTest(WebMockTestCase):
828 def test_compute_value(self): 997 def test_compute_value(self):
829 compute = mock.Mock() 998 compute = mock.Mock()
@@ -848,6 +1017,7 @@ class ComputationTest(WebMockTestCase):
848 compute.assert_called_with("foo", "bid") 1017 compute.assert_called_with("foo", "bid")
849 1018
850 1019
1020@unittest.skipUnless("unit" in limits, "Unit skipped")
851class TradeTest(WebMockTestCase): 1021class TradeTest(WebMockTestCase):
852 1022
853 def test_values_assertion(self): 1023 def test_values_assertion(self):
@@ -881,7 +1051,7 @@ class TradeTest(WebMockTestCase):
881 1051
882 value_from = portfolio.Amount("BTC", "1.0") 1052 value_from = portfolio.Amount("BTC", "1.0")
883 value_from.linked_to = portfolio.Amount("BTC", "1.0") 1053 value_from.linked_to = portfolio.Amount("BTC", "1.0")
884 value_to = portfolio.Amount("BTC", "1.0") 1054 value_to = portfolio.Amount("BTC", "2.0")
885 trade = portfolio.Trade(value_from, value_to, "BTC") 1055 trade = portfolio.Trade(value_from, value_to, "BTC")
886 1056
887 self.assertIsNone(trade.action) 1057 self.assertIsNone(trade.action)
@@ -939,22 +1109,251 @@ class TradeTest(WebMockTestCase):
939 trade = portfolio.Trade(value_from, value_to, "ETH") 1109 trade = portfolio.Trade(value_from, value_to, "ETH")
940 1110
941 order1 = mock.Mock() 1111 order1 = mock.Mock()
942 order1.filled_amount = portfolio.Amount("ETH", "0.3") 1112 order1.filled_amount.return_value = portfolio.Amount("ETH", "0.3")
943 1113
944 order2 = mock.Mock() 1114 order2 = mock.Mock()
945 order2.filled_amount = portfolio.Amount("ETH", "0.01") 1115 order2.filled_amount.return_value = portfolio.Amount("ETH", "0.01")
946 trade.orders.append(order1) 1116 trade.orders.append(order1)
947 trade.orders.append(order2) 1117 trade.orders.append(order2)
948 1118
949 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount) 1119 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount())
1120 order1.filled_amount.assert_called_with(in_base_currency=False)
1121 order2.filled_amount.assert_called_with(in_base_currency=False)
950 1122
951 @unittest.skip("TODO") 1123 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=False))
952 def test_prepare_order(self): 1124 order1.filled_amount.assert_called_with(in_base_currency=False)
953 pass 1125 order2.filled_amount.assert_called_with(in_base_currency=False)
1126
1127 self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=True))
1128 order1.filled_amount.assert_called_with(in_base_currency=True)
1129 order2.filled_amount.assert_called_with(in_base_currency=True)
1130
1131 @mock.patch.object(helper, "get_ticker")
1132 @mock.patch.object(portfolio.Computation, "compute_value")
1133 @mock.patch.object(portfolio.Trade, "filled_amount")
1134 @mock.patch.object(portfolio, "Order")
1135 def test_prepare_order(self, Order, filled_amount, compute_value, get_ticker):
1136 Order.return_value = "Order"
1137
1138 with self.subTest(desc="Nothing to do"):
1139 value_from = portfolio.Amount("BTC", "10")
1140 value_from.rate = D("0.1")
1141 value_from.linked_to = portfolio.Amount("FOO", "100")
1142 value_to = portfolio.Amount("BTC", "10")
1143 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1144
1145 trade.prepare_order()
1146
1147 filled_amount.assert_not_called()
1148 compute_value.assert_not_called()
1149 self.assertEqual(0, len(trade.orders))
1150 Order.assert_not_called()
1151
1152 get_ticker.return_value = { "inverted": False }
1153 with self.subTest(desc="Already filled"), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1154 filled_amount.return_value = portfolio.Amount("FOO", "100")
1155 compute_value.return_value = D("0.125")
1156
1157 value_from = portfolio.Amount("BTC", "10")
1158 value_from.rate = D("0.1")
1159 value_from.linked_to = portfolio.Amount("FOO", "100")
1160 value_to = portfolio.Amount("BTC", "0")
1161 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1162
1163 trade.prepare_order()
1164
1165 filled_amount.assert_called_with(in_base_currency=False)
1166 compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default")
1167 self.assertEqual(0, len(trade.orders))
1168 self.assertRegex(stdout_mock.getvalue(), "Less to do than already filled: ")
1169 Order.assert_not_called()
1170
1171 with self.subTest(action="dispose", inverted=False):
1172 filled_amount.return_value = portfolio.Amount("FOO", "60")
1173 compute_value.return_value = D("0.125")
1174
1175 value_from = portfolio.Amount("BTC", "10")
1176 value_from.rate = D("0.1")
1177 value_from.linked_to = portfolio.Amount("FOO", "100")
1178 value_to = portfolio.Amount("BTC", "1")
1179 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1180
1181 trade.prepare_order()
1182
1183 filled_amount.assert_called_with(in_base_currency=False)
1184 compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default")
1185 self.assertEqual(1, len(trade.orders))
1186 Order.assert_called_with("sell", portfolio.Amount("FOO", 30),
1187 D("0.125"), "BTC", "long", "market",
1188 trade, close_if_possible=False)
1189
1190 with self.subTest(action="acquire", inverted=False):
1191 filled_amount.return_value = portfolio.Amount("BTC", "3")
1192 compute_value.return_value = D("0.125")
1193
1194 value_from = portfolio.Amount("BTC", "1")
1195 value_from.rate = D("0.1")
1196 value_from.linked_to = portfolio.Amount("FOO", "10")
1197 value_to = portfolio.Amount("BTC", "10")
1198 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1199
1200 trade.prepare_order()
1201
1202 filled_amount.assert_called_with(in_base_currency=True)
1203 compute_value.assert_called_with(get_ticker.return_value, "buy", compute_value="default")
1204 self.assertEqual(1, len(trade.orders))
1205
1206 Order.assert_called_with("buy", portfolio.Amount("FOO", 48),
1207 D("0.125"), "BTC", "long", "market",
1208 trade, close_if_possible=False)
1209
1210 with self.subTest(close_if_possible=True):
1211 filled_amount.return_value = portfolio.Amount("FOO", "0")
1212 compute_value.return_value = D("0.125")
1213
1214 value_from = portfolio.Amount("BTC", "10")
1215 value_from.rate = D("0.1")
1216 value_from.linked_to = portfolio.Amount("FOO", "100")
1217 value_to = portfolio.Amount("BTC", "0")
1218 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1219
1220 trade.prepare_order()
1221
1222 filled_amount.assert_called_with(in_base_currency=False)
1223 compute_value.assert_called_with(get_ticker.return_value, "sell", compute_value="default")
1224 self.assertEqual(1, len(trade.orders))
1225 Order.assert_called_with("sell", portfolio.Amount("FOO", 100),
1226 D("0.125"), "BTC", "long", "market",
1227 trade, close_if_possible=True)
1228
1229 get_ticker.return_value = { "inverted": True, "original": {} }
1230 with self.subTest(action="dispose", inverted=True):
1231 filled_amount.return_value = portfolio.Amount("FOO", "300")
1232 compute_value.return_value = D("125")
1233
1234 value_from = portfolio.Amount("BTC", "10")
1235 value_from.rate = D("0.01")
1236 value_from.linked_to = portfolio.Amount("FOO", "1000")
1237 value_to = portfolio.Amount("BTC", "1")
1238 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1239
1240 trade.prepare_order(compute_value="foo")
1241
1242 filled_amount.assert_called_with(in_base_currency=True)
1243 compute_value.assert_called_with(get_ticker.return_value["original"], "buy", compute_value="foo")
1244 self.assertEqual(1, len(trade.orders))
1245 Order.assert_called_with("buy", portfolio.Amount("BTC", D("4.8")),
1246 D("125"), "FOO", "long", "market",
1247 trade, close_if_possible=False)
1248
1249 with self.subTest(action="acquire", inverted=True):
1250 filled_amount.return_value = portfolio.Amount("BTC", "4")
1251 compute_value.return_value = D("125")
1252
1253 value_from = portfolio.Amount("BTC", "1")
1254 value_from.rate = D("0.01")
1255 value_from.linked_to = portfolio.Amount("FOO", "100")
1256 value_to = portfolio.Amount("BTC", "10")
1257 trade = portfolio.Trade(value_from, value_to, "FOO", market="market")
1258
1259 trade.prepare_order(compute_value="foo")
1260
1261 filled_amount.assert_called_with(in_base_currency=False)
1262 compute_value.assert_called_with(get_ticker.return_value["original"], "sell", compute_value="foo")
1263 self.assertEqual(1, len(trade.orders))
1264 Order.assert_called_with("sell", portfolio.Amount("BTC", D("5")),
1265 D("125"), "FOO", "long", "market",
1266 trade, close_if_possible=False)
1267
1268
1269 @mock.patch.object(portfolio.Trade, "prepare_order")
1270 def test_update_order(self, prepare_order):
1271 order_mock = mock.Mock()
1272 new_order_mock = mock.Mock()
1273
1274 value_from = portfolio.Amount("BTC", "0.5")
1275 value_from.linked_to = portfolio.Amount("ETH", "10.0")
1276 value_to = portfolio.Amount("BTC", "1.0")
1277 trade = portfolio.Trade(value_from, value_to, "ETH")
1278 def _prepare_order(compute_value=None):
1279 trade.orders.append(new_order_mock)
1280 prepare_order.side_effect = _prepare_order
1281
1282 for i in [0, 1, 3, 4, 6]:
1283 with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1284 trade.update_order(order_mock, i)
1285 order_mock.cancel.assert_not_called()
1286 new_order_mock.run.assert_not_called()
1287 self.assertRegex(stdout_mock.getvalue(), "tick {}, waiting".format(i))
1288 self.assertEqual(0, len(trade.orders))
1289
1290 order_mock.reset_mock()
1291 new_order_mock.reset_mock()
1292 trade.orders = []
1293
1294 with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1295 trade.update_order(order_mock, 2)
1296 order_mock.cancel.assert_called()
1297 new_order_mock.run.assert_called()
1298 prepare_order.assert_called()
1299 self.assertRegex(stdout_mock.getvalue(), "tick 2, cancelling and adjusting")
1300 self.assertEqual(1, len(trade.orders))
1301
1302 order_mock.reset_mock()
1303 new_order_mock.reset_mock()
1304 trade.orders = []
1305
1306 with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1307 trade.update_order(order_mock, 5)
1308 order_mock.cancel.assert_called()
1309 new_order_mock.run.assert_called()
1310 prepare_order.assert_called()
1311 self.assertRegex(stdout_mock.getvalue(), "tick 5, cancelling and adjusting")
1312 self.assertEqual(1, len(trade.orders))
1313
1314 order_mock.reset_mock()
1315 new_order_mock.reset_mock()
1316 trade.orders = []
1317
1318 with mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1319 trade.update_order(order_mock, 7)
1320 order_mock.cancel.assert_called()
1321 new_order_mock.run.assert_called()
1322 prepare_order.assert_called_with(compute_value="default")
1323 self.assertRegex(stdout_mock.getvalue(), "tick 7, fallbacking to market value")
1324 self.assertRegex(stdout_mock.getvalue(), "tick 7, market value, cancelling and adjusting to")
1325 self.assertEqual(1, len(trade.orders))
1326
1327 order_mock.reset_mock()
1328 new_order_mock.reset_mock()
1329 trade.orders = []
1330
1331 for i in [10, 13, 16]:
1332 with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1333 trade.update_order(order_mock, i)
1334 order_mock.cancel.assert_called()
1335 new_order_mock.run.assert_called()
1336 prepare_order.assert_called_with(compute_value="default")
1337 self.assertNotRegex(stdout_mock.getvalue(), "tick {}, fallbacking to market value".format(i))
1338 self.assertRegex(stdout_mock.getvalue(), "tick {}, market value, cancelling and adjusting to".format(i))
1339 self.assertEqual(1, len(trade.orders))
1340
1341 order_mock.reset_mock()
1342 new_order_mock.reset_mock()
1343 trade.orders = []
1344
1345 for i in [8, 9, 11, 12]:
1346 with self.subTest(tick=i), mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
1347 trade.update_order(order_mock, i)
1348 order_mock.cancel.assert_not_called()
1349 new_order_mock.run.assert_not_called()
1350 self.assertEqual("", stdout_mock.getvalue())
1351 self.assertEqual(0, len(trade.orders))
1352
1353 order_mock.reset_mock()
1354 new_order_mock.reset_mock()
1355 trade.orders = []
954 1356
955 @unittest.skip("TODO")
956 def test_update_order(self):
957 pass
958 1357
959 @mock.patch('sys.stdout', new_callable=StringIO) 1358 @mock.patch('sys.stdout', new_callable=StringIO)
960 def test_print_with_order(self, mock_stdout): 1359 def test_print_with_order(self, mock_stdout):
@@ -987,6 +1386,7 @@ class TradeTest(WebMockTestCase):
987 1386
988 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(trade)) 1387 self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(trade))
989 1388
1389@unittest.skipUnless("acceptance" in limits, "Acceptance skipped")
990class AcceptanceTest(WebMockTestCase): 1390class AcceptanceTest(WebMockTestCase):
991 @unittest.expectedFailure 1391 @unittest.expectedFailure
992 def test_success_sell_only_necessary(self): 1392 def test_success_sell_only_necessary(self):