diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-02-11 13:36:54 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-02-11 13:36:54 +0100 |
commit | 1aa7d4fa2ec3c2b3268bef31a666ca6e1aaa6563 (patch) | |
tree | f47f804070e935bf8fdf621cc59f4320606cf46f /test.py | |
parent | 6ca5a1ec669593fa915a2824efca068c975f9caa (diff) | |
download | Trader-1aa7d4fa2ec3c2b3268bef31a666ca6e1aaa6563.tar.gz Trader-1aa7d4fa2ec3c2b3268bef31a666ca6e1aaa6563.tar.zst Trader-1aa7d4fa2ec3c2b3268bef31a666ca6e1aaa6563.zip |
Add Makefile and test coverage
Fix order preparation and add tests for the step
Separate tests between acceptance and unit
Add more tests
Diffstat (limited to 'test.py')
-rw-r--r-- | test.py | 440 |
1 files changed, 420 insertions, 20 deletions
@@ -1,3 +1,4 @@ | |||
1 | import sys | ||
1 | import portfolio | 2 | import portfolio |
2 | import unittest | 3 | import unittest |
3 | from decimal import Decimal as D | 4 | from decimal import Decimal as D |
@@ -7,6 +8,16 @@ import requests_mock | |||
7 | from io import StringIO | 8 | from io import StringIO |
8 | import helper | 9 | import helper |
9 | 10 | ||
11 | limits = ["acceptance", "unit"] | ||
12 | for test_type in limits: | ||
13 | if "--no{}".format(test_type) in sys.argv: | ||
14 | sys.argv.remove("--no{}".format(test_type)) | ||
15 | limits.remove(test_type) | ||
16 | if "--only{}".format(test_type) in sys.argv: | ||
17 | sys.argv.remove("--only{}".format(test_type)) | ||
18 | limits = [test_type] | ||
19 | break | ||
20 | |||
10 | class WebMockTestCase(unittest.TestCase): | 21 | class 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") | ||
42 | class PortfolioTest(WebMockTestCase): | 54 | class 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") | ||
143 | class AmountTest(WebMockTestCase): | 156 | class 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") | ||
366 | class BalanceTest(WebMockTestCase): | 383 | class 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") | ||
418 | class HelperTest(WebMockTestCase): | 446 | class 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") | ||
620 | class TradeStoreTest(WebMockTestCase): | 706 | class 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") | |
713 | class BalanceStoreTest(WebMockTestCase): | 879 | class 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") | ||
827 | class ComputationTest(WebMockTestCase): | 996 | class 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") | ||
851 | class TradeTest(WebMockTestCase): | 1021 | class 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") | ||
990 | class AcceptanceTest(WebMockTestCase): | 1390 | class AcceptanceTest(WebMockTestCase): |
991 | @unittest.expectedFailure | 1391 | @unittest.expectedFailure |
992 | def test_success_sell_only_necessary(self): | 1392 | def test_success_sell_only_necessary(self): |