From 66f1aed55a17d65e0b5993c59688769dcc2b7b58 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Isma=C3=ABl=20Bouya?= Date: Sun, 4 Mar 2018 14:49:54 +0100 Subject: [PATCH] Write tests for Processor class Make default repartition less sensitive to BTC Automatic signature for methods Fixes https://git.immae.eu/mantisbt/view.php?id=49 https://git.immae.eu/mantisbt/view.php?id=39 --- helper.py | 96 +++++++++++------------ test.py | 230 +++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 206 insertions(+), 120 deletions(-) diff --git a/helper.py b/helper.py index 95b233a..8f726d5 100644 --- a/helper.py +++ b/helper.py @@ -202,7 +202,7 @@ class Processor: "before": True, "after": False, "fetch_balances": ["begin", "end"], - "prepare_trades": { "repartition": { "BTC": (1, "long") } }, + "prepare_trades": { "repartition": { "base_currency": (1, "long") } }, "prepare_orders": { "compute_value": "average" }, "run_orders": {}, "follow_orders": {}, @@ -226,18 +226,15 @@ class Processor: "move_balances": {}, "run_orders": {}, "follow_orders": {}, + "close_trades": {}, }, ] } - allowed_arguments = { - "wait_for_recent": ["delta"], - "prepare_trades": ["base_currency", "liquidity", "compute_value", "repartition", "only"], - "prepare_orders": ["only", "compute_value"], - "move_balances": [], - "run_orders": [], - "follow_orders": ["sleep"], - } + ordered_actions = [ + "wait_for_recent", "prepare_trades", "prepare_orders", + "move_balances", "run_orders", "follow_orders", + "close_trades"] def __init__(self, market): self.market = market @@ -264,57 +261,60 @@ class Processor: for step in steps: selected_steps += self.select_steps(scenario, step) for step in selected_steps: - self.process_step(scenario_name, step, **kwargs) + self.process_step(scenario_name, step, kwargs) - def process_step(self, scenario_name, step, **kwargs): + def process_step(self, scenario_name, step, kwargs): process_name = "process_{}__{}_{}".format(scenario_name, step["number"], step["name"]) self.market.report.log_stage("{}_begin".format(process_name)) if "begin" in step.get("fetch_balances", []): self.market.balances.fetch_balances(tag="{}_begin".format(process_name)) - for action in ["wait_for_recent", "prepare_trades", - "prepare_orders", "move_balances", "run_orders", - "follow_orders"]: + for action in self.ordered_actions: if action in step: - self.run_action(action, step[action], **kwargs) + self.run_action(action, step[action], kwargs) if "end" in step.get("fetch_balances", []): self.market.balances.fetch_balances(tag="{}_end".format(process_name)) self.market.report.log_stage("{}_end".format(process_name)) - def run_action(self, action, default_args, **kwargs): - args = {k: v for k, v in {**default_args, **kwargs}.items() if k in self.allowed_arguments[action] } + def method_arguments(self, action): + import inspect if action == "wait_for_recent": - portfolio.Portfolio.wait_for_recent(self.market, **args) - if action == "prepare_trades": - self.market.prepare_trades(**args) - if action == "prepare_orders": - self.market.trades.prepare_orders(**args) - if action == "move_balances": - self.market.move_balances(**args) - if action == "run_orders": - self.market.trades.run_orders(**args) - if action == "follow_orders": - self.market.follow_orders(**args) - -def process_sell_needed__1_sell(market, liquidity="medium", base_currency="BTC"): - Processor(market).process("sell_needed", steps="sell", - liquidity=liquidity, base_currency=base_currency) - -def process_sell_needed__2_buy(market, liquidity="medium", base_currency="BTC"): - Processor(market).process("sell_needed", steps="buy", - liquidity=liquidity, base_currency=base_currency) - -def process_sell_all__1_all_sell(market, base_currency="BTC", liquidity="medium"): - Processor(market).process("sell_all", steps="all_sell", - liquidity=liquidity, base_currency=base_currency) - -def process_sell_all__2_wait(market, liquidity="medium", base_currency="BTC"): - Processor(market).process("sell_all", steps="wait", - liquidity=liquidity, base_currency=base_currency) - -def process_sell_all__3_all_buy(market, base_currency="BTC", liquidity="medium"): - Processor(market).process("sell_all", steps="all_buy", - liquidity=liquidity, base_currency=base_currency) + method = portfolio.Portfolio.wait_for_recent + elif action == "prepare_trades": + method = self.market.prepare_trades + elif action == "prepare_orders": + method = self.market.trades.prepare_orders + elif action == "move_balances": + method = self.market.move_balances + elif action == "run_orders": + method = self.market.trades.run_orders + elif action == "follow_orders": + method = self.market.follow_orders + elif action == "close_trades": + method = self.market.trades.close_trades + + signature = inspect.getfullargspec(method) + defaults = signature.defaults or [] + kwargs = signature.args[-len(defaults):] + + return [method, kwargs] + + def parse_args(self, action, default_args, kwargs): + method, allowed_arguments = self.method_arguments(action) + args = {k: v for k, v in {**default_args, **kwargs}.items() if k in allowed_arguments } + + if "repartition" in args and "base_currency" in args["repartition"]: + r = args["repartition"] + r[args.get("base_currency", "BTC")] = r.pop("base_currency") + + return method, args + + def run_action(self, action, default_args, kwargs): + method, args = self.parse_args(action, default_args, kwargs) + if action == "wait_for_recent": + method(self.market, **args) + else: + method(**args) diff --git a/test.py b/test.py index 78de76e..a894503 100644 --- a/test.py +++ b/test.py @@ -3014,87 +3014,173 @@ class HelperTest(WebMockTestCase): mock.call(portfolio.Amount("BTC", "0.95")), ]) - def test_process_sell_needed__1_sell(self): - helper.process_sell_needed__1_sell(self.m) +@unittest.skipUnless("unit" in limits, "Unit skipped") +class ProcessorTest(WebMockTestCase): + def test_values(self): + processor = helper.Processor(self.m) - self.m.balances.fetch_balances.assert_has_calls([ - mock.call(tag="process_sell_needed__1_sell_begin"), - mock.call(tag="process_sell_needed__1_sell_end"), - ]) - self.m.prepare_trades.assert_called_with(base_currency="BTC", - liquidity="medium") - self.m.trades.prepare_orders.assert_called_with(compute_value="average", - only="dispose") - self.m.trades.run_orders.assert_called() - self.m.follow_orders.assert_called() - self.m.report.log_stage.assert_has_calls([ - mock.call("process_sell_needed__1_sell_begin"), - mock.call("process_sell_needed__1_sell_end") - ]) + self.assertEqual(self.m, processor.market) - def test_process_sell_needed__2_buy(self): - helper.process_sell_needed__2_buy(self.m) + def test_run_action(self): + processor = helper.Processor(self.m) - self.m.balances.fetch_balances.assert_has_calls([ - mock.call(tag="process_sell_needed__2_buy_begin"), - mock.call(tag="process_sell_needed__2_buy_end"), - ]) - self.m.prepare_trades.assert_called_with(base_currency="BTC", - liquidity="medium", only="acquire") - self.m.trades.prepare_orders.assert_called_with(compute_value="average", - only="acquire") - self.m.move_balances.assert_called_with() - self.m.trades.run_orders.assert_called() - self.m.follow_orders.assert_called() - self.m.report.log_stage.assert_has_calls([ - mock.call("process_sell_needed__2_buy_begin"), - mock.call("process_sell_needed__2_buy_end") - ]) + with mock.patch.object(processor, "parse_args") as parse_args: + method_mock = mock.Mock() + parse_args.return_value = [method_mock, { "foo": "bar" }] - def test_process_sell_all__1_sell(self): - helper.process_sell_all__1_all_sell(self.m) + processor.run_action("foo", "bar", "baz") - self.m.balances.fetch_balances.assert_has_calls([ - mock.call(tag="process_sell_all__1_all_sell_begin"), - mock.call(tag="process_sell_all__1_all_sell_end"), - ]) - self.m.prepare_trades.assert_called_with(base_currency="BTC", - liquidity="medium", repartition={'BTC': (1, 'long')}) - self.m.trades.prepare_orders.assert_called_with(compute_value="average") - self.m.trades.run_orders.assert_called() - self.m.follow_orders.assert_called() - self.m.report.log_stage.assert_has_calls([ - mock.call("process_sell_all__1_all_sell_begin"), - mock.call("process_sell_all__1_all_sell_end") - ]) + parse_args.assert_called_with("foo", "bar", "baz") - @mock.patch("portfolio.Portfolio.wait_for_recent") - def test_process_sell_all__2_wait(self, wait): - helper.process_sell_all__2_wait(self.m) + method_mock.assert_called_with(foo="bar") - wait.assert_called_once_with(self.m) - self.m.report.log_stage.assert_has_calls([ - mock.call("process_sell_all__2_wait_begin"), - mock.call("process_sell_all__2_wait_end") - ]) + processor.run_action("wait_for_recent", "bar", "baz") - def test_process_sell_all__3_all_buy(self): - helper.process_sell_all__3_all_buy(self.m) + method_mock.assert_called_with(self.m, foo="bar") + + def test_select_step(self): + processor = helper.Processor(self.m) + + scenario = processor.scenarios["sell_all"] + + self.assertEqual(scenario, processor.select_steps(scenario, "all")) + self.assertEqual(["all_sell"], list(map(lambda x: x["name"], processor.select_steps(scenario, "before")))) + self.assertEqual(["wait", "all_buy"], list(map(lambda x: x["name"], processor.select_steps(scenario, "after")))) + self.assertEqual(["wait"], list(map(lambda x: x["name"], processor.select_steps(scenario, 2)))) + self.assertEqual(["wait"], list(map(lambda x: x["name"], processor.select_steps(scenario, "wait")))) + + with self.assertRaises(TypeError): + processor.select_steps(scenario, ["wait"]) + + @mock.patch("helper.Processor.process_step") + def test_process(self, process_step): + processor = helper.Processor(self.m) + + processor.process("sell_all", foo="bar") + self.assertEqual(3, process_step.call_count) + + steps = list(map(lambda x: x[1][1]["name"], process_step.mock_calls)) + scenario_names = list(map(lambda x: x[1][0], process_step.mock_calls)) + kwargs = list(map(lambda x: x[1][2], process_step.mock_calls)) + self.assertEqual(["all_sell", "wait", "all_buy"], steps) + self.assertEqual(["sell_all", "sell_all", "sell_all"], scenario_names) + self.assertEqual([{"foo":"bar"}, {"foo":"bar"}, {"foo":"bar"}], kwargs) + + process_step.reset_mock() + + processor.process("sell_needed", steps=["before", "after"]) + self.assertEqual(3, process_step.call_count) + + def test_method_arguments(self): + ccxt = mock.Mock(spec=market.ccxt.poloniexE) + m = market.Market(ccxt) + + processor = helper.Processor(m) + + method, arguments = processor.method_arguments("wait_for_recent") + self.assertEqual(portfolio.Portfolio.wait_for_recent, method) + self.assertEqual(["delta"], arguments) + + method, arguments = processor.method_arguments("prepare_trades") + self.assertEqual(m.prepare_trades, method) + self.assertEqual(['base_currency', 'liquidity', 'compute_value', 'repartition', 'only'], arguments) + + method, arguments = processor.method_arguments("prepare_orders") + self.assertEqual(m.trades.prepare_orders, method) + + method, arguments = processor.method_arguments("move_balances") + self.assertEqual(m.move_balances, method) + + method, arguments = processor.method_arguments("run_orders") + self.assertEqual(m.trades.run_orders, method) + + method, arguments = processor.method_arguments("follow_orders") + self.assertEqual(m.follow_orders, method) + + method, arguments = processor.method_arguments("close_trades") + self.assertEqual(m.trades.close_trades, method) + + def test_process_step(self): + processor = helper.Processor(self.m) + + with mock.patch.object(processor, "run_action") as run_action: + step = processor.scenarios["sell_needed"][1] + + processor.process_step("foo", step, {"foo":"bar"}) + + self.m.report.log_stage.assert_has_calls([ + mock.call("process_foo__1_sell_begin"), + mock.call("process_foo__1_sell_end"), + ]) + self.m.balances.fetch_balances.assert_has_calls([ + mock.call(tag="process_foo__1_sell_begin"), + mock.call(tag="process_foo__1_sell_end"), + ]) + + self.assertEqual(5, run_action.call_count) + + run_action.assert_has_calls([ + mock.call('prepare_trades', {}, {'foo': 'bar'}), + mock.call('prepare_orders', {'only': 'dispose', 'compute_value': 'average'}, {'foo': 'bar'}), + mock.call('run_orders', {}, {'foo': 'bar'}), + mock.call('follow_orders', {}, {'foo': 'bar'}), + mock.call('close_trades', {}, {'foo': 'bar'}), + ]) + + self.m.reset_mock() + with mock.patch.object(processor, "run_action") as run_action: + step = processor.scenarios["sell_needed"][0] + + processor.process_step("foo", step, {"foo":"bar"}) + self.m.balances.fetch_balances.assert_not_called() + + def test_parse_args(self): + processor = helper.Processor(self.m) + + with mock.patch.object(processor, "method_arguments") as method_arguments: + method_mock = mock.Mock() + method_arguments.return_value = [ + method_mock, + ["foo2", "foo"] + ] + method, args = processor.parse_args("action", {"foo": "bar", "foo2": "bar"}, {"foo": "bar2", "bla": "bla"}) + + self.assertEqual(method_mock, method) + self.assertEqual({"foo": "bar2", "foo2": "bar"}, args) + + with mock.patch.object(processor, "method_arguments") as method_arguments: + method_mock = mock.Mock() + method_arguments.return_value = [ + method_mock, + ["repartition"] + ] + method, args = processor.parse_args("action", {"repartition": { "base_currency": 1 }}, {}) + + self.assertEqual(1, len(args["repartition"])) + self.assertIn("BTC", args["repartition"]) + + with mock.patch.object(processor, "method_arguments") as method_arguments: + method_mock = mock.Mock() + method_arguments.return_value = [ + method_mock, + ["repartition", "base_currency"] + ] + method, args = processor.parse_args("action", {"repartition": { "base_currency": 1 }}, {"base_currency": "USDT"}) + + self.assertEqual(1, len(args["repartition"])) + self.assertIn("USDT", args["repartition"]) + + with mock.patch.object(processor, "method_arguments") as method_arguments: + method_mock = mock.Mock() + method_arguments.return_value = [ + method_mock, + ["repartition", "base_currency"] + ] + method, args = processor.parse_args("action", {"repartition": { "ETH": 1 }}, {"base_currency": "USDT"}) + + self.assertEqual(1, len(args["repartition"])) + self.assertIn("ETH", args["repartition"]) - self.m.balances.fetch_balances.assert_has_calls([ - mock.call(tag="process_sell_all__3_all_buy_begin"), - mock.call(tag="process_sell_all__3_all_buy_end"), - ]) - self.m.prepare_trades.assert_called_with(base_currency="BTC", - liquidity="medium") - self.m.trades.prepare_orders.assert_called_with(compute_value="average") - self.m.move_balances.assert_called_with() - self.m.trades.run_orders.assert_called() - self.m.follow_orders.assert_called() - self.m.report.log_stage.assert_has_calls([ - mock.call("process_sell_all__3_all_buy_begin"), - mock.call("process_sell_all__3_all_buy_end") - ]) @unittest.skipUnless("acceptance" in limits, "Acceptance skipped") class AcceptanceTest(WebMockTestCase): -- 2.41.0