diff options
-rw-r--r-- | main.py | 5 | ||||
-rw-r--r-- | market.py | 52 | ||||
-rw-r--r-- | tests/test_market.py | 112 |
3 files changed, 76 insertions, 93 deletions
@@ -149,7 +149,10 @@ def parse_args(argv): | |||
149 | parser.add_argument("--db-database", default="cryptoportfolio", | 149 | parser.add_argument("--db-database", default="cryptoportfolio", |
150 | help="Database access to database (default: cryptoportfolio)") | 150 | help="Database access to database (default: cryptoportfolio)") |
151 | 151 | ||
152 | return parser.parse_args(argv) | 152 | parsed = parser.parse_args(argv) |
153 | if parsed.action is None: | ||
154 | parsed.action = ["sell_all"] | ||
155 | return parsed | ||
153 | 156 | ||
154 | def process(market_config, market_id, user_id, args, pg_config): | 157 | def process(market_config, market_id, user_id, args, pg_config): |
155 | try: | 158 | try: |
@@ -76,17 +76,13 @@ class Market: | |||
76 | 76 | ||
77 | def process(self, actions, before=False, after=False): | 77 | def process(self, actions, before=False, after=False): |
78 | try: | 78 | try: |
79 | if len(actions or []) == 0: | 79 | for action in actions: |
80 | if before: | 80 | if bool(before) is bool(after): |
81 | self.processor.process("sell_all", steps="before") | 81 | self.processor.process(action, steps="all") |
82 | if after: | 82 | elif before: |
83 | self.processor.process("sell_all", steps="after") | 83 | self.processor.process(action, steps="before") |
84 | else: | 84 | elif after: |
85 | for action in actions: | 85 | self.processor.process(action, steps="after") |
86 | if hasattr(self, action): | ||
87 | getattr(self, action)() | ||
88 | else: | ||
89 | self.report.log_error("market_process", message="Unknown action {}".format(action)) | ||
90 | except Exception as e: | 86 | except Exception as e: |
91 | self.report.log_error("market_process", exception=e) | 87 | self.report.log_error("market_process", exception=e) |
92 | finally: | 88 | finally: |
@@ -212,16 +208,7 @@ class Market: | |||
212 | liquidity=liquidity, repartition=repartition) | 208 | liquidity=liquidity, repartition=repartition) |
213 | self.trades.compute_trades(values_in_base, new_repartition, only=only) | 209 | self.trades.compute_trades(values_in_base, new_repartition, only=only) |
214 | 210 | ||
215 | # Helpers | 211 | def print_tickers(self, base_currency="BTC"): |
216 | def print_orders(self, base_currency="BTC"): | ||
217 | self.report.log_stage("print_orders") | ||
218 | self.balances.fetch_balances(tag="print_orders") | ||
219 | self.prepare_trades(base_currency=base_currency, compute_value="average") | ||
220 | self.trades.prepare_orders(compute_value="average") | ||
221 | |||
222 | def print_balances(self, base_currency="BTC"): | ||
223 | self.report.log_stage("print_balances") | ||
224 | self.balances.fetch_balances() | ||
225 | if base_currency is not None: | 212 | if base_currency is not None: |
226 | self.report.print_log("total:") | 213 | self.report.print_log("total:") |
227 | self.report.print_log(sum(self.balances.in_currency(base_currency).values())) | 214 | self.report.print_log(sum(self.balances.in_currency(base_currency).values())) |
@@ -237,12 +224,20 @@ class Processor: | |||
237 | "wait_for_recent": {}, | 224 | "wait_for_recent": {}, |
238 | }, | 225 | }, |
239 | ], | 226 | ], |
227 | "print_balances": [ | ||
228 | { | ||
229 | "name": "print_balances", | ||
230 | "number": 1, | ||
231 | "fetch_balances": ["begin"], | ||
232 | "print_tickers": { "base_currency": "BTC" }, | ||
233 | } | ||
234 | ], | ||
240 | "print_orders": [ | 235 | "print_orders": [ |
241 | { | 236 | { |
242 | "name": "wait", | 237 | "name": "wait", |
243 | "number": 1, | 238 | "number": 1, |
244 | "before": False, | 239 | "before": True, |
245 | "after": True, | 240 | "after": False, |
246 | "wait_for_recent": {}, | 241 | "wait_for_recent": {}, |
247 | }, | 242 | }, |
248 | { | 243 | { |
@@ -328,7 +323,7 @@ class Processor: | |||
328 | ordered_actions = [ | 323 | ordered_actions = [ |
329 | "wait_for_recent", "prepare_trades", "prepare_orders", | 324 | "wait_for_recent", "prepare_trades", "prepare_orders", |
330 | "move_balances", "run_orders", "follow_orders", | 325 | "move_balances", "run_orders", "follow_orders", |
331 | "close_trades"] | 326 | "close_trades", "print_tickers"] |
332 | 327 | ||
333 | def __init__(self, market): | 328 | def __init__(self, market): |
334 | self.market = market | 329 | self.market = market |
@@ -337,7 +332,7 @@ class Processor: | |||
337 | if step == "all": | 332 | if step == "all": |
338 | return scenario | 333 | return scenario |
339 | elif step == "before" or step == "after": | 334 | elif step == "before" or step == "after": |
340 | return list(filter(lambda x: step in x and x[step], scenario)) | 335 | return list(filter(lambda x: x.get(step, False), scenario)) |
341 | elif type(step) == int: | 336 | elif type(step) == int: |
342 | return [scenario[step-1]] | 337 | return [scenario[step-1]] |
343 | elif type(step) == str: | 338 | elif type(step) == str: |
@@ -345,7 +340,12 @@ class Processor: | |||
345 | else: | 340 | else: |
346 | raise TypeError("Unknown step {}".format(step)) | 341 | raise TypeError("Unknown step {}".format(step)) |
347 | 342 | ||
343 | def can_process(self, scenario_name): | ||
344 | return scenario_name in self.scenarios | ||
345 | |||
348 | def process(self, scenario_name, steps="all", **kwargs): | 346 | def process(self, scenario_name, steps="all", **kwargs): |
347 | if not self.can_process(scenario_name): | ||
348 | raise TypeError("Unknown scenario {}".format(scenario_name)) | ||
349 | scenario = self.scenarios[scenario_name] | 349 | scenario = self.scenarios[scenario_name] |
350 | selected_steps = [] | 350 | selected_steps = [] |
351 | 351 | ||
@@ -388,6 +388,8 @@ class Processor: | |||
388 | method = self.market.follow_orders | 388 | method = self.market.follow_orders |
389 | elif action == "close_trades": | 389 | elif action == "close_trades": |
390 | method = self.market.trades.close_trades | 390 | method = self.market.trades.close_trades |
391 | elif action == "print_tickers": | ||
392 | method = self.market.print_tickers | ||
391 | 393 | ||
392 | signature = inspect.getfullargspec(method) | 394 | signature = inspect.getfullargspec(method) |
393 | defaults = signature.defaults or [] | 395 | defaults = signature.defaults or [] |
diff --git a/tests/test_market.py b/tests/test_market.py index 14b23b5..b41cd6a 100644 --- a/tests/test_market.py +++ b/tests/test_market.py | |||
@@ -607,21 +607,7 @@ class MarketTest(WebMockTestCase): | |||
607 | file_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) | 607 | file_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) |
608 | db_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) | 608 | db_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) |
609 | 609 | ||
610 | def test_print_orders(self): | 610 | def test_print_tickers(self): |
611 | m = market.Market(self.ccxt, self.market_args()) | ||
612 | with mock.patch.object(m.report, "log_stage") as log_stage,\ | ||
613 | mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ | ||
614 | mock.patch.object(m, "prepare_trades") as prepare_trades,\ | ||
615 | mock.patch.object(m.trades, "prepare_orders") as prepare_orders: | ||
616 | m.print_orders() | ||
617 | |||
618 | log_stage.assert_called_with("print_orders") | ||
619 | fetch_balances.assert_called_with(tag="print_orders") | ||
620 | prepare_trades.assert_called_with(base_currency="BTC", | ||
621 | compute_value="average") | ||
622 | prepare_orders.assert_called_with(compute_value="average") | ||
623 | |||
624 | def test_print_balances(self): | ||
625 | m = market.Market(self.ccxt, self.market_args()) | 611 | m = market.Market(self.ccxt, self.market_args()) |
626 | 612 | ||
627 | with mock.patch.object(m.balances, "in_currency") as in_currency,\ | 613 | with mock.patch.object(m.balances, "in_currency") as in_currency,\ |
@@ -634,10 +620,8 @@ class MarketTest(WebMockTestCase): | |||
634 | "ETH": portfolio.Amount("BTC", "0.3"), | 620 | "ETH": portfolio.Amount("BTC", "0.3"), |
635 | } | 621 | } |
636 | 622 | ||
637 | m.print_balances() | 623 | m.print_tickers() |
638 | 624 | ||
639 | log_stage.assert_called_once_with("print_balances") | ||
640 | fetch_balances.assert_called_with() | ||
641 | print_log.assert_has_calls([ | 625 | print_log.assert_has_calls([ |
642 | mock.call("total:"), | 626 | mock.call("total:"), |
643 | mock.call(portfolio.Amount("BTC", "0.95")), | 627 | mock.call(portfolio.Amount("BTC", "0.95")), |
@@ -648,8 +632,8 @@ class MarketTest(WebMockTestCase): | |||
648 | @mock.patch("market.Market.store_report") | 632 | @mock.patch("market.Market.store_report") |
649 | def test_process(self, store_report, log_error, process): | 633 | def test_process(self, store_report, log_error, process): |
650 | m = market.Market(self.ccxt, self.market_args()) | 634 | m = market.Market(self.ccxt, self.market_args()) |
651 | with self.subTest(before=False, after=False): | 635 | with self.subTest(actions=[], before=False, after=False): |
652 | m.process(None) | 636 | m.process([]) |
653 | 637 | ||
654 | process.assert_not_called() | 638 | process.assert_not_called() |
655 | store_report.assert_called_once() | 639 | store_report.assert_called_once() |
@@ -659,9 +643,9 @@ class MarketTest(WebMockTestCase): | |||
659 | log_error.reset_mock() | 643 | log_error.reset_mock() |
660 | store_report.reset_mock() | 644 | store_report.reset_mock() |
661 | with self.subTest(before=True, after=False): | 645 | with self.subTest(before=True, after=False): |
662 | m.process(None, before=True) | 646 | m.process(["foo"], before=True) |
663 | 647 | ||
664 | process.assert_called_once_with("sell_all", steps="before") | 648 | process.assert_called_once_with("foo", steps="before") |
665 | store_report.assert_called_once() | 649 | store_report.assert_called_once() |
666 | log_error.assert_not_called() | 650 | log_error.assert_not_called() |
667 | 651 | ||
@@ -669,7 +653,7 @@ class MarketTest(WebMockTestCase): | |||
669 | log_error.reset_mock() | 653 | log_error.reset_mock() |
670 | store_report.reset_mock() | 654 | store_report.reset_mock() |
671 | with self.subTest(before=False, after=True): | 655 | with self.subTest(before=False, after=True): |
672 | m.process(None, after=True) | 656 | m.process(["sell_all"], after=True) |
673 | 657 | ||
674 | process.assert_called_once_with("sell_all", steps="after") | 658 | process.assert_called_once_with("sell_all", steps="after") |
675 | store_report.assert_called_once() | 659 | store_report.assert_called_once() |
@@ -678,54 +662,30 @@ class MarketTest(WebMockTestCase): | |||
678 | process.reset_mock() | 662 | process.reset_mock() |
679 | log_error.reset_mock() | 663 | log_error.reset_mock() |
680 | store_report.reset_mock() | 664 | store_report.reset_mock() |
681 | with self.subTest(before=True, after=True): | 665 | with self.subTest(before=False, after=False): |
682 | m.process(None, before=True, after=True) | 666 | m.process(["foo"]) |
683 | 667 | ||
684 | process.assert_has_calls([ | 668 | process.assert_called_once_with("foo", steps="all") |
685 | mock.call("sell_all", steps="before"), | ||
686 | mock.call("sell_all", steps="after"), | ||
687 | ]) | ||
688 | store_report.assert_called_once() | 669 | store_report.assert_called_once() |
689 | log_error.assert_not_called() | 670 | log_error.assert_not_called() |
690 | 671 | ||
691 | process.reset_mock() | 672 | process.reset_mock() |
692 | log_error.reset_mock() | 673 | log_error.reset_mock() |
693 | store_report.reset_mock() | 674 | store_report.reset_mock() |
694 | with self.subTest(action="print_balances"),\ | 675 | with self.subTest(before=True, after=True): |
695 | mock.patch.object(m, "print_balances") as print_balances: | 676 | m.process(["sell_all"], before=True, after=True) |
696 | m.process(["print_balances"]) | ||
697 | 677 | ||
698 | process.assert_not_called() | 678 | process.assert_called_once_with("sell_all", steps="all") |
699 | log_error.assert_not_called() | ||
700 | store_report.assert_called_once() | 679 | store_report.assert_called_once() |
701 | print_balances.assert_called_once_with() | ||
702 | |||
703 | log_error.reset_mock() | ||
704 | store_report.reset_mock() | ||
705 | with self.subTest(action="print_orders"),\ | ||
706 | mock.patch.object(m, "print_orders") as print_orders,\ | ||
707 | mock.patch.object(m, "print_balances") as print_balances: | ||
708 | m.process(["print_orders", "print_balances"]) | ||
709 | |||
710 | process.assert_not_called() | ||
711 | log_error.assert_not_called() | 680 | log_error.assert_not_called() |
712 | store_report.assert_called_once() | ||
713 | print_orders.assert_called_once_with() | ||
714 | print_balances.assert_called_once_with() | ||
715 | |||
716 | log_error.reset_mock() | ||
717 | store_report.reset_mock() | ||
718 | with self.subTest(action="unknown"): | ||
719 | m.process(["unknown"]) | ||
720 | log_error.assert_called_once_with("market_process", message="Unknown action unknown") | ||
721 | store_report.assert_called_once() | ||
722 | 681 | ||
682 | process.reset_mock() | ||
723 | log_error.reset_mock() | 683 | log_error.reset_mock() |
724 | store_report.reset_mock() | 684 | store_report.reset_mock() |
725 | with self.subTest(unhandled_exception=True): | 685 | with self.subTest(unhandled_exception=True): |
726 | process.side_effect = Exception("bouh") | 686 | process.side_effect = Exception("bouh") |
727 | 687 | ||
728 | m.process(None, before=True) | 688 | m.process(["some_action"], before=True) |
729 | log_error.assert_called_with("market_process", exception=mock.ANY) | 689 | log_error.assert_called_with("market_process", exception=mock.ANY) |
730 | store_report.assert_called_once() | 690 | store_report.assert_called_once() |
731 | 691 | ||
@@ -768,24 +728,39 @@ class ProcessorTest(WebMockTestCase): | |||
768 | with self.assertRaises(TypeError): | 728 | with self.assertRaises(TypeError): |
769 | processor.select_steps(scenario, ["wait"]) | 729 | processor.select_steps(scenario, ["wait"]) |
770 | 730 | ||
731 | def test_can_process(self): | ||
732 | processor = market.Processor(self.m) | ||
733 | |||
734 | with self.subTest(True): | ||
735 | self.assertTrue(processor.can_process("sell_all")) | ||
736 | |||
737 | with self.subTest(False): | ||
738 | self.assertFalse(processor.can_process("unknown_action")) | ||
739 | |||
771 | @mock.patch("market.Processor.process_step") | 740 | @mock.patch("market.Processor.process_step") |
772 | def test_process(self, process_step): | 741 | def test_process(self, process_step): |
773 | processor = market.Processor(self.m) | 742 | with self.subTest("unknown action"): |
743 | processor = market.Processor(self.m) | ||
744 | with self.assertRaises(TypeError): | ||
745 | processor.process("unknown_action") | ||
746 | |||
747 | with self.subTest("nominal case"): | ||
748 | processor = market.Processor(self.m) | ||
774 | 749 | ||
775 | processor.process("sell_all", foo="bar") | 750 | processor.process("sell_all", foo="bar") |
776 | self.assertEqual(3, process_step.call_count) | 751 | self.assertEqual(3, process_step.call_count) |
777 | 752 | ||
778 | steps = list(map(lambda x: x[1][1]["name"], process_step.mock_calls)) | 753 | steps = list(map(lambda x: x[1][1]["name"], process_step.mock_calls)) |
779 | scenario_names = list(map(lambda x: x[1][0], process_step.mock_calls)) | 754 | scenario_names = list(map(lambda x: x[1][0], process_step.mock_calls)) |
780 | kwargs = list(map(lambda x: x[1][2], process_step.mock_calls)) | 755 | kwargs = list(map(lambda x: x[1][2], process_step.mock_calls)) |
781 | self.assertEqual(["all_sell", "wait", "all_buy"], steps) | 756 | self.assertEqual(["all_sell", "wait", "all_buy"], steps) |
782 | self.assertEqual(["sell_all", "sell_all", "sell_all"], scenario_names) | 757 | self.assertEqual(["sell_all", "sell_all", "sell_all"], scenario_names) |
783 | self.assertEqual([{"foo":"bar"}, {"foo":"bar"}, {"foo":"bar"}], kwargs) | 758 | self.assertEqual([{"foo":"bar"}, {"foo":"bar"}, {"foo":"bar"}], kwargs) |
784 | 759 | ||
785 | process_step.reset_mock() | 760 | process_step.reset_mock() |
786 | 761 | ||
787 | processor.process("sell_needed", steps=["before", "after"]) | 762 | processor.process("sell_needed", steps=["before", "after"]) |
788 | self.assertEqual(3, process_step.call_count) | 763 | self.assertEqual(3, process_step.call_count) |
789 | 764 | ||
790 | def test_method_arguments(self): | 765 | def test_method_arguments(self): |
791 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) | 766 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) |
@@ -816,6 +791,9 @@ class ProcessorTest(WebMockTestCase): | |||
816 | method, arguments = processor.method_arguments("close_trades") | 791 | method, arguments = processor.method_arguments("close_trades") |
817 | self.assertEqual(m.trades.close_trades, method) | 792 | self.assertEqual(m.trades.close_trades, method) |
818 | 793 | ||
794 | method, arguments = processor.method_arguments("print_tickers") | ||
795 | self.assertEqual(m.print_tickers, method) | ||
796 | |||
819 | def test_process_step(self): | 797 | def test_process_step(self): |
820 | processor = market.Processor(self.m) | 798 | processor = market.Processor(self.m) |
821 | 799 | ||