diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-02 13:59:25 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-02 14:08:00 +0100 |
commit | 9db7d156833cd384baa64b6148b5c646bfcc41f8 (patch) | |
tree | d451957ff1d7f0473db6c15d1baa5a56e973832c | |
parent | 7bd830a83b662874c145ea9548edfde79eadc68f (diff) | |
download | Trader-9db7d156833cd384baa64b6148b5c646bfcc41f8.tar.gz Trader-9db7d156833cd384baa64b6148b5c646bfcc41f8.tar.zst Trader-9db7d156833cd384baa64b6148b5c646bfcc41f8.zip |
Add processors
Fixes https://git.immae.eu/mantisbt/view.php?id=45
-rw-r--r-- | helper.py | 189 | ||||
-rw-r--r-- | market.py | 22 | ||||
-rw-r--r-- | test.py | 111 |
3 files changed, 184 insertions, 138 deletions
@@ -127,10 +127,9 @@ def main_fetch_markets(pg_config, user): | |||
127 | def main_process_market(user_market, actions, before=False, after=False): | 127 | def main_process_market(user_market, actions, before=False, after=False): |
128 | if len(actions or []) == 0: | 128 | if len(actions or []) == 0: |
129 | if before: | 129 | if before: |
130 | process_sell_all__1_all_sell(user_market) | 130 | Processor(user_market).process("sell_all", steps="before") |
131 | if after: | 131 | if after: |
132 | process_sell_all__2_wait(user_market) | 132 | Processor(user_market).process("sell_all", steps="after") |
133 | process_sell_all__3_all_buy(user_market) | ||
134 | else: | 133 | else: |
135 | for action in actions: | 134 | for action in actions: |
136 | if action in globals(): | 135 | if action in globals(): |
@@ -160,51 +159,159 @@ def print_balances(market, base_currency="BTC"): | |||
160 | market.report.print_log("total:") | 159 | market.report.print_log("total:") |
161 | market.report.print_log(sum(market.balances.in_currency(base_currency).values())) | 160 | market.report.print_log(sum(market.balances.in_currency(base_currency).values())) |
162 | 161 | ||
162 | class Processor: | ||
163 | scenarios = { | ||
164 | "sell_needed": [ | ||
165 | { | ||
166 | "name": "wait", | ||
167 | "number": 0, | ||
168 | "before": False, | ||
169 | "after": True, | ||
170 | "wait_for_recent": {}, | ||
171 | }, | ||
172 | { | ||
173 | "name": "sell", | ||
174 | "number": 1, | ||
175 | "before": False, | ||
176 | "after": True, | ||
177 | "fetch_balances": ["begin", "end"], | ||
178 | "prepare_trades": {}, | ||
179 | "prepare_orders": { "only": "dispose", "compute_value": "average" }, | ||
180 | "run_orders": {}, | ||
181 | "follow_orders": {}, | ||
182 | }, | ||
183 | { | ||
184 | "name": "buy", | ||
185 | "number": 2, | ||
186 | "before": False, | ||
187 | "after": True, | ||
188 | "fetch_balances": ["begin", "end"], | ||
189 | "prepare_trades": { "only": "acquire" }, | ||
190 | "prepare_orders": { "only": "acquire", "compute_value": "average" }, | ||
191 | "move_balances": {}, | ||
192 | "run_orders": {}, | ||
193 | "follow_orders": {}, | ||
194 | }, | ||
195 | ], | ||
196 | "sell_all": [ | ||
197 | { | ||
198 | "name": "all_sell", | ||
199 | "number": 1, | ||
200 | "before": True, | ||
201 | "after": False, | ||
202 | "fetch_balances": ["begin", "end"], | ||
203 | "prepare_trades": { "repartition": { "BTC": (1, "long") } }, | ||
204 | "prepare_orders": { "compute_value": "average" }, | ||
205 | "run_orders": {}, | ||
206 | "follow_orders": {}, | ||
207 | }, | ||
208 | { | ||
209 | "name": "wait", | ||
210 | "number": 2, | ||
211 | "before": False, | ||
212 | "after": True, | ||
213 | "wait_for_recent": {}, | ||
214 | }, | ||
215 | { | ||
216 | "name": "all_buy", | ||
217 | "number": 3, | ||
218 | "before": False, | ||
219 | "after": True, | ||
220 | "fetch_balances": ["begin", "end"], | ||
221 | "prepare_trades": {}, | ||
222 | "prepare_orders": { "compute_value": "average" }, | ||
223 | "move_balances": {}, | ||
224 | "run_orders": {}, | ||
225 | "follow_orders": {}, | ||
226 | }, | ||
227 | ] | ||
228 | } | ||
229 | |||
230 | allowed_arguments = { | ||
231 | "wait_for_recent": ["delta"], | ||
232 | "prepare_trades": ["base_currency", "liquidity", "compute_value", "repartition", "only"], | ||
233 | "prepare_orders": ["only", "compute_value"], | ||
234 | "move_balances": [], | ||
235 | "run_orders": [], | ||
236 | "follow_orders": ["sleep"], | ||
237 | } | ||
238 | |||
239 | def __init__(self, market): | ||
240 | self.market = market | ||
241 | |||
242 | def select_steps(self, scenario, step): | ||
243 | if step == "all": | ||
244 | return scenario | ||
245 | elif step == "before" or step == "after": | ||
246 | return list(filter(lambda x: step in x and x[step], scenario)) | ||
247 | elif type(step) == int: | ||
248 | return [scenario[step-1]] | ||
249 | elif type(step) == str: | ||
250 | return list(filter(lambda x: x["name"] == step, scenario)) | ||
251 | else: | ||
252 | raise TypeError("Unknown step {}".format(step)) | ||
253 | |||
254 | def process(self, scenario_name, steps="all", **kwargs): | ||
255 | scenario = self.scenarios[scenario_name] | ||
256 | selected_steps = [] | ||
257 | |||
258 | if type(steps) == str or type(steps) == int: | ||
259 | selected_steps += self.select_steps(scenario, steps) | ||
260 | else: | ||
261 | for step in steps: | ||
262 | selected_steps += self.select_steps(scenario, step) | ||
263 | for step in selected_steps: | ||
264 | self.process_step(scenario_name, step, **kwargs) | ||
265 | |||
266 | def process_step(self, scenario_name, step, **kwargs): | ||
267 | process_name = "process_{}__{}_{}".format(scenario_name, step["number"], step["name"]) | ||
268 | self.market.report.log_stage("{}_begin".format(process_name)) | ||
269 | if "begin" in step.get("fetch_balances", []): | ||
270 | self.market.balances.fetch_balances(tag="{}_begin".format(process_name)) | ||
271 | |||
272 | for action in ["wait_for_recent", "prepare_trades", | ||
273 | "prepare_orders", "move_balances", "run_orders", | ||
274 | "follow_orders"]: | ||
275 | if action in step: | ||
276 | self.run_action(action, step[action], **kwargs) | ||
277 | |||
278 | if "end" in step.get("fetch_balances", []): | ||
279 | self.market.balances.fetch_balances(tag="{}_end".format(process_name)) | ||
280 | self.market.report.log_stage("{}_end".format(process_name)) | ||
281 | |||
282 | def run_action(self, action, default_args, **kwargs): | ||
283 | args = {k: v for k, v in {**default_args, **kwargs}.items() if k in self.allowed_arguments[action] } | ||
284 | |||
285 | if action == "wait_for_recent": | ||
286 | portfolio.Portfolio.wait_for_recent(self.market, **args) | ||
287 | if action == "prepare_trades": | ||
288 | self.market.prepare_trades(**args) | ||
289 | if action == "prepare_orders": | ||
290 | self.market.trades.prepare_orders(**args) | ||
291 | if action == "move_balances": | ||
292 | self.market.move_balances(**args) | ||
293 | if action == "run_orders": | ||
294 | self.market.trades.run_orders(**args) | ||
295 | if action == "follow_orders": | ||
296 | self.market.follow_orders(**args) | ||
297 | |||
163 | def process_sell_needed__1_sell(market, liquidity="medium", base_currency="BTC"): | 298 | def process_sell_needed__1_sell(market, liquidity="medium", base_currency="BTC"): |
164 | market.report.log_stage("process_sell_needed__1_sell_begin") | 299 | Processor(market).process("sell_needed", steps="sell", |
165 | market.balances.fetch_balances(tag="process_sell_needed__1_sell_begin") | 300 | liquidity=liquidity, base_currency=base_currency) |
166 | market.prepare_trades(liquidity=liquidity, base_currency=base_currency) | ||
167 | market.trades.prepare_orders(compute_value="average", only="dispose") | ||
168 | market.trades.run_orders() | ||
169 | market.follow_orders() | ||
170 | market.balances.fetch_balances(tag="process_sell_needed__1_sell_end") | ||
171 | market.report.log_stage("process_sell_needed__1_sell_end") | ||
172 | 301 | ||
173 | def process_sell_needed__2_buy(market, liquidity="medium", base_currency="BTC"): | 302 | def process_sell_needed__2_buy(market, liquidity="medium", base_currency="BTC"): |
174 | market.report.log_stage("process_sell_needed__2_buy_begin") | 303 | Processor(market).process("sell_needed", steps="buy", |
175 | market.balances.fetch_balances(tag="process_sell_needed__2_buy_begin") | 304 | liquidity=liquidity, base_currency=base_currency) |
176 | market.prepare_trades(base_currency=base_currency, liquidity=liquidity, only="acquire") | ||
177 | market.trades.prepare_orders(compute_value="average", only="acquire") | ||
178 | market.move_balances() | ||
179 | market.trades.run_orders() | ||
180 | market.follow_orders() | ||
181 | market.balances.fetch_balances(tag="process_sell_needed__2_buy_end") | ||
182 | market.report.log_stage("process_sell_needed__2_buy_end") | ||
183 | 305 | ||
184 | def process_sell_all__1_all_sell(market, base_currency="BTC", liquidity="medium"): | 306 | def process_sell_all__1_all_sell(market, base_currency="BTC", liquidity="medium"): |
185 | market.report.log_stage("process_sell_all__1_all_sell_begin") | 307 | Processor(market).process("sell_all", steps="all_sell", |
186 | market.balances.fetch_balances(tag="process_sell_all__1_all_sell_begin") | 308 | liquidity=liquidity, base_currency=base_currency) |
187 | market.prepare_trades_to_sell_all(base_currency=base_currency) | ||
188 | market.trades.prepare_orders(compute_value="average") | ||
189 | market.trades.run_orders() | ||
190 | market.follow_orders() | ||
191 | market.balances.fetch_balances(tag="process_sell_all__1_all_sell_end") | ||
192 | market.report.log_stage("process_sell_all__1_all_sell_end") | ||
193 | 309 | ||
194 | def process_sell_all__2_wait(market, liquidity="medium", base_currency="BTC"): | 310 | def process_sell_all__2_wait(market, liquidity="medium", base_currency="BTC"): |
195 | market.report.log_stage("process_sell_all__2_wait_begin") | 311 | Processor(market).process("sell_all", steps="wait", |
196 | portfolio.Portfolio.wait_for_recent(market) | 312 | liquidity=liquidity, base_currency=base_currency) |
197 | market.report.log_stage("process_sell_all__2_wait_end") | ||
198 | 313 | ||
199 | def process_sell_all__3_all_buy(market, base_currency="BTC", liquidity="medium"): | 314 | def process_sell_all__3_all_buy(market, base_currency="BTC", liquidity="medium"): |
200 | market.report.log_stage("process_sell_all__3_all_buy_begin") | 315 | Processor(market).process("sell_all", steps="all_buy", |
201 | market.balances.fetch_balances(tag="process_sell_all__3_all_buy_begin") | 316 | liquidity=liquidity, base_currency=base_currency) |
202 | market.prepare_trades(liquidity=liquidity, base_currency=base_currency) | ||
203 | market.trades.prepare_orders(compute_value="average") | ||
204 | market.move_balances() | ||
205 | market.trades.run_orders() | ||
206 | market.follow_orders() | ||
207 | market.balances.fetch_balances(tag="process_sell_all__3_all_buy_end") | ||
208 | market.report.log_stage("process_sell_all__3_all_buy_end") | ||
209 | |||
210 | 317 | ||
@@ -128,20 +128,18 @@ class Market: | |||
128 | order.trade.update_order(order, tick) | 128 | order.trade.update_order(order, tick) |
129 | self.report.log_stage("follow_orders_end") | 129 | self.report.log_stage("follow_orders_end") |
130 | 130 | ||
131 | def prepare_trades(self, base_currency="BTC", liquidity="medium", compute_value="average", only=None): | 131 | def prepare_trades(self, base_currency="BTC", liquidity="medium", |
132 | compute_value="average", repartition=None, only=None): | ||
133 | |||
132 | self.report.log_stage("prepare_trades", | 134 | self.report.log_stage("prepare_trades", |
133 | base_currency=base_currency, liquidity=liquidity, | 135 | base_currency=base_currency, liquidity=liquidity, |
134 | compute_value=compute_value, only=only) | 136 | compute_value=compute_value, only=only, |
135 | values_in_base = self.balances.in_currency(base_currency, compute_value=compute_value) | 137 | repartition=repartition) |
136 | total_base_value = sum(values_in_base.values()) | ||
137 | new_repartition = self.balances.dispatch_assets(total_base_value, liquidity=liquidity) | ||
138 | self.trades.compute_trades(values_in_base, new_repartition, only=only) | ||
139 | 138 | ||
140 | def prepare_trades_to_sell_all(self, base_currency="BTC", compute_value="average"): | 139 | values_in_base = self.balances.in_currency(base_currency, |
141 | self.report.log_stage("prepare_trades_to_sell_all") | 140 | compute_value=compute_value) |
142 | values_in_base = self.balances.in_currency(base_currency, compute_value=compute_value) | ||
143 | total_base_value = sum(values_in_base.values()) | 141 | total_base_value = sum(values_in_base.values()) |
144 | new_repartition = self.balances.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") }) | 142 | new_repartition = self.balances.dispatch_assets(total_base_value, |
145 | self.trades.compute_trades(values_in_base, new_repartition) | 143 | liquidity=liquidity, repartition=repartition) |
146 | 144 | self.trades.compute_trades(values_in_base, new_repartition, only=only) | |
147 | 145 | ||
@@ -783,51 +783,9 @@ class MarketTest(WebMockTestCase): | |||
783 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) | 783 | self.assertEqual(D("0.7575"), call[0][1]["XEM"].value) |
784 | m.report.log_stage.assert_called_once_with("prepare_trades", | 784 | m.report.log_stage.assert_called_once_with("prepare_trades", |
785 | base_currency='BTC', compute_value='average', | 785 | base_currency='BTC', compute_value='average', |
786 | liquidity='medium', only=None) | 786 | liquidity='medium', only=None, repartition=None) |
787 | m.report.log_balances.assert_called_once_with(tag="tag") | 787 | m.report.log_balances.assert_called_once_with(tag="tag") |
788 | 788 | ||
789 | @mock.patch.object(portfolio.Portfolio, "repartition") | ||
790 | @mock.patch.object(market.Market, "get_ticker") | ||
791 | @mock.patch.object(market.TradeStore, "compute_trades") | ||
792 | def test_prepare_trades_to_sell_all(self, compute_trades, get_ticker, repartition): | ||
793 | def _get_ticker(c1, c2): | ||
794 | if c1 == "USDT" and c2 == "BTC": | ||
795 | return { "average": D("0.0001") } | ||
796 | if c1 == "XVG" and c2 == "BTC": | ||
797 | return { "average": D("0.000001") } | ||
798 | self.fail("Should be called with {}, {}".format(c1, c2)) | ||
799 | get_ticker.side_effect = _get_ticker | ||
800 | |||
801 | with mock.patch("market.ReportStore"): | ||
802 | m = market.Market(self.ccxt) | ||
803 | self.ccxt.fetch_all_balances.return_value = { | ||
804 | "USDT": { | ||
805 | "exchange_free": D("10000.0"), | ||
806 | "exchange_used": D("0.0"), | ||
807 | "exchange_total": D("10000.0"), | ||
808 | "total": D("10000.0") | ||
809 | }, | ||
810 | "XVG": { | ||
811 | "exchange_free": D("10000.0"), | ||
812 | "exchange_used": D("0.0"), | ||
813 | "exchange_total": D("10000.0"), | ||
814 | "total": D("10000.0") | ||
815 | }, | ||
816 | } | ||
817 | |||
818 | m.balances.fetch_balances(tag="tag") | ||
819 | |||
820 | m.prepare_trades_to_sell_all() | ||
821 | |||
822 | repartition.assert_not_called() | ||
823 | compute_trades.assert_called() | ||
824 | |||
825 | call = compute_trades.call_args | ||
826 | self.assertEqual(1, call[0][0]["USDT"].value) | ||
827 | self.assertEqual(D("0.01"), call[0][0]["XVG"].value) | ||
828 | self.assertEqual(D("1.01"), call[0][1]["BTC"].value) | ||
829 | m.report.log_stage.assert_called_once_with("prepare_trades_to_sell_all") | ||
830 | m.report.log_balances.assert_called_once_with(tag="tag") | ||
831 | 789 | ||
832 | @mock.patch.object(portfolio.time, "sleep") | 790 | @mock.patch.object(portfolio.time, "sleep") |
833 | @mock.patch.object(market.TradeStore, "all_orders") | 791 | @mock.patch.object(market.TradeStore, "all_orders") |
@@ -2840,57 +2798,41 @@ class HelperTest(WebMockTestCase): | |||
2840 | 2798 | ||
2841 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | 2799 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") |
2842 | 2800 | ||
2843 | @mock.patch("helper.process_sell_all__1_all_sell") | 2801 | @mock.patch("helper.Processor.process") |
2844 | @mock.patch("helper.process_sell_all__2_wait") | 2802 | def test_main_process_market(self, process): |
2845 | @mock.patch("helper.process_sell_all__3_all_buy") | ||
2846 | def test_main_process_market(self, buy, wait, sell): | ||
2847 | with self.subTest(before=False, after=False): | 2803 | with self.subTest(before=False, after=False): |
2848 | helper.main_process_market("user", None) | 2804 | m = mock.Mock() |
2849 | 2805 | helper.main_process_market(m, None) | |
2850 | wait.assert_not_called() | 2806 | |
2851 | buy.assert_not_called() | 2807 | process.assert_not_called() |
2852 | sell.assert_not_called() | ||
2853 | 2808 | ||
2854 | buy.reset_mock() | 2809 | process.reset_mock() |
2855 | wait.reset_mock() | ||
2856 | sell.reset_mock() | ||
2857 | with self.subTest(before=True, after=False): | 2810 | with self.subTest(before=True, after=False): |
2858 | helper.main_process_market("user", None, before=True) | 2811 | helper.main_process_market(m, None, before=True) |
2859 | |||
2860 | wait.assert_not_called() | ||
2861 | buy.assert_not_called() | ||
2862 | sell.assert_called_once_with("user") | ||
2863 | 2812 | ||
2864 | buy.reset_mock() | 2813 | process.assert_called_once_with("sell_all", steps="before") |
2865 | wait.reset_mock() | 2814 | |
2866 | sell.reset_mock() | 2815 | process.reset_mock() |
2867 | with self.subTest(before=False, after=True): | 2816 | with self.subTest(before=False, after=True): |
2868 | helper.main_process_market("user", None, after=True) | 2817 | helper.main_process_market(m, None, after=True) |
2869 | 2818 | ||
2870 | wait.assert_called_once_with("user") | 2819 | process.assert_called_once_with("sell_all", steps="after") |
2871 | buy.assert_called_once_with("user") | ||
2872 | sell.assert_not_called() | ||
2873 | 2820 | ||
2874 | buy.reset_mock() | 2821 | process.reset_mock() |
2875 | wait.reset_mock() | ||
2876 | sell.reset_mock() | ||
2877 | with self.subTest(before=True, after=True): | 2822 | with self.subTest(before=True, after=True): |
2878 | helper.main_process_market("user", None, before=True, after=True) | 2823 | helper.main_process_market(m, None, before=True, after=True) |
2879 | |||
2880 | wait.assert_called_once_with("user") | ||
2881 | buy.assert_called_once_with("user") | ||
2882 | sell.assert_called_once_with("user") | ||
2883 | 2824 | ||
2884 | buy.reset_mock() | 2825 | process.assert_has_calls([ |
2885 | wait.reset_mock() | 2826 | mock.call("sell_all", steps="before"), |
2886 | sell.reset_mock() | 2827 | mock.call("sell_all", steps="after"), |
2828 | ]) | ||
2829 | |||
2830 | process.reset_mock() | ||
2887 | with self.subTest(action="print_balances"),\ | 2831 | with self.subTest(action="print_balances"),\ |
2888 | mock.patch("helper.print_balances") as print_balances: | 2832 | mock.patch("helper.print_balances") as print_balances: |
2889 | helper.main_process_market("user", ["print_balances"]) | 2833 | helper.main_process_market("user", ["print_balances"]) |
2890 | 2834 | ||
2891 | buy.assert_not_called() | 2835 | process.assert_not_called() |
2892 | wait.assert_not_called() | ||
2893 | sell.assert_not_called() | ||
2894 | print_balances.assert_called_once_with("user") | 2836 | print_balances.assert_called_once_with("user") |
2895 | 2837 | ||
2896 | with self.subTest(action="print_orders"),\ | 2838 | with self.subTest(action="print_orders"),\ |
@@ -2898,9 +2840,7 @@ class HelperTest(WebMockTestCase): | |||
2898 | mock.patch("helper.print_balances") as print_balances: | 2840 | mock.patch("helper.print_balances") as print_balances: |
2899 | helper.main_process_market("user", ["print_orders", "print_balances"]) | 2841 | helper.main_process_market("user", ["print_orders", "print_balances"]) |
2900 | 2842 | ||
2901 | buy.assert_not_called() | 2843 | process.assert_not_called() |
2902 | wait.assert_not_called() | ||
2903 | sell.assert_not_called() | ||
2904 | print_orders.assert_called_once_with("user") | 2844 | print_orders.assert_called_once_with("user") |
2905 | print_balances.assert_called_once_with("user") | 2845 | print_balances.assert_called_once_with("user") |
2906 | 2846 | ||
@@ -3074,7 +3014,8 @@ class HelperTest(WebMockTestCase): | |||
3074 | mock.call(tag="process_sell_all__1_all_sell_begin"), | 3014 | mock.call(tag="process_sell_all__1_all_sell_begin"), |
3075 | mock.call(tag="process_sell_all__1_all_sell_end"), | 3015 | mock.call(tag="process_sell_all__1_all_sell_end"), |
3076 | ]) | 3016 | ]) |
3077 | self.m.prepare_trades_to_sell_all.assert_called_with(base_currency="BTC") | 3017 | self.m.prepare_trades.assert_called_with(base_currency="BTC", |
3018 | liquidity="medium", repartition={'BTC': (1, 'long')}) | ||
3078 | self.m.trades.prepare_orders.assert_called_with(compute_value="average") | 3019 | self.m.trades.prepare_orders.assert_called_with(compute_value="average") |
3079 | self.m.trades.run_orders.assert_called() | 3020 | self.m.trades.run_orders.assert_called() |
3080 | self.m.follow_orders.assert_called() | 3021 | self.m.follow_orders.assert_called() |