diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-06 00:21:52 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-06 00:21:52 +0100 |
commit | 1f117ac79e10c3c9728d3b267d134dec2a165603 (patch) | |
tree | 681df08b12cb059d0eba02710746f33a133fd187 | |
parent | f9226903cb53a9b303a26de562e321159349f8df (diff) | |
download | Trader-1f117ac79e10c3c9728d3b267d134dec2a165603.tar.gz Trader-1f117ac79e10c3c9728d3b267d134dec2a165603.tar.zst Trader-1f117ac79e10c3c9728d3b267d134dec2a165603.zip |
Move helper methods to their due places
-rw-r--r-- | helper.py | 320 | ||||
-rw-r--r-- | main.py | 149 | ||||
-rw-r--r-- | market.py | 215 | ||||
-rw-r--r-- | test.py | 428 |
4 files changed, 610 insertions, 502 deletions
diff --git a/helper.py b/helper.py deleted file mode 100644 index 8f726d5..0000000 --- a/helper.py +++ /dev/null | |||
@@ -1,320 +0,0 @@ | |||
1 | from datetime import datetime | ||
2 | import argparse | ||
3 | import configparser | ||
4 | import psycopg2 | ||
5 | import os | ||
6 | import sys | ||
7 | |||
8 | import portfolio | ||
9 | |||
10 | def make_order(market, value, currency, action="acquire", | ||
11 | close_if_possible=False, base_currency="BTC", follow=True, | ||
12 | compute_value="average"): | ||
13 | """ | ||
14 | Make an order on market | ||
15 | "market": The market on which to place the order | ||
16 | "value": The value in *base_currency* to acquire, | ||
17 | or in *currency* to dispose. | ||
18 | use negative for margin trade. | ||
19 | "action": "acquire" or "dispose". | ||
20 | "acquire" will buy long or sell short, | ||
21 | "dispose" will sell long or buy short. | ||
22 | "currency": The currency to acquire or dispose | ||
23 | "base_currency": The base currency. The value is expressed in that | ||
24 | currency (default: BTC) | ||
25 | "follow": Whether to follow the order once run (default: True) | ||
26 | "close_if_possible": Whether to try to close the position at the end | ||
27 | of the trade, i.e. reach exactly 0 at the end | ||
28 | (only meaningful in "dispose"). May have | ||
29 | unwanted effects if the end value of the | ||
30 | currency is not 0. | ||
31 | "compute_value": Compute value to place the order | ||
32 | """ | ||
33 | market.report.log_stage("make_order_begin") | ||
34 | market.balances.fetch_balances(tag="make_order_begin") | ||
35 | if action == "acquire": | ||
36 | trade = portfolio.Trade( | ||
37 | portfolio.Amount(base_currency, 0), | ||
38 | portfolio.Amount(base_currency, value), | ||
39 | currency, market) | ||
40 | else: | ||
41 | amount = portfolio.Amount(currency, value) | ||
42 | trade = portfolio.Trade( | ||
43 | amount.in_currency(base_currency, market, compute_value=compute_value), | ||
44 | portfolio.Amount(base_currency, 0), | ||
45 | currency, market) | ||
46 | market.trades.all.append(trade) | ||
47 | order = trade.prepare_order( | ||
48 | close_if_possible=close_if_possible, | ||
49 | compute_value=compute_value) | ||
50 | market.report.log_orders([order], None, compute_value) | ||
51 | market.trades.run_orders() | ||
52 | if follow: | ||
53 | market.follow_orders() | ||
54 | market.balances.fetch_balances(tag="make_order_end") | ||
55 | else: | ||
56 | market.report.log_stage("make_order_end_not_followed") | ||
57 | return order | ||
58 | market.report.log_stage("make_order_end") | ||
59 | |||
60 | def get_user_market(config_path, user_id, debug=False): | ||
61 | import market | ||
62 | pg_config, report_path = main_parse_config(config_path) | ||
63 | market_config = list(main_fetch_markets(pg_config, str(user_id)))[0][0] | ||
64 | return market.Market.from_config(market_config, debug=debug) | ||
65 | |||
66 | def main_parse_args(argv): | ||
67 | parser = argparse.ArgumentParser( | ||
68 | description="Run the trade bot") | ||
69 | |||
70 | parser.add_argument("-c", "--config", | ||
71 | default="config.ini", | ||
72 | required=False, | ||
73 | help="Config file to load (default: config.ini)") | ||
74 | parser.add_argument("--before", | ||
75 | default=False, action='store_const', const=True, | ||
76 | help="Run the steps before the cryptoportfolio update") | ||
77 | parser.add_argument("--after", | ||
78 | default=False, action='store_const', const=True, | ||
79 | help="Run the steps after the cryptoportfolio update") | ||
80 | parser.add_argument("--debug", | ||
81 | default=False, action='store_const', const=True, | ||
82 | help="Run in debug mode") | ||
83 | parser.add_argument("--user", | ||
84 | default=None, required=False, help="Only run for that user") | ||
85 | parser.add_argument("--action", | ||
86 | action='append', | ||
87 | help="Do a different action than trading (add several times to chain)") | ||
88 | |||
89 | args = parser.parse_args(argv) | ||
90 | |||
91 | if not os.path.exists(args.config): | ||
92 | print("no config file found, exiting") | ||
93 | sys.exit(1) | ||
94 | |||
95 | return args | ||
96 | |||
97 | def main_parse_config(config_file): | ||
98 | config = configparser.ConfigParser() | ||
99 | config.read(config_file) | ||
100 | |||
101 | if "postgresql" not in config: | ||
102 | print("no configuration for postgresql in config file") | ||
103 | sys.exit(1) | ||
104 | |||
105 | if "app" in config and "report_path" in config["app"]: | ||
106 | report_path = config["app"]["report_path"] | ||
107 | |||
108 | if not os.path.exists(report_path): | ||
109 | os.makedirs(report_path) | ||
110 | else: | ||
111 | report_path = None | ||
112 | |||
113 | return [config["postgresql"], report_path] | ||
114 | |||
115 | def main_fetch_markets(pg_config, user): | ||
116 | connection = psycopg2.connect(**pg_config) | ||
117 | cursor = connection.cursor() | ||
118 | |||
119 | if user is None: | ||
120 | cursor.execute("SELECT config,user_id FROM market_configs") | ||
121 | else: | ||
122 | cursor.execute("SELECT config,user_id FROM market_configs WHERE user_id = %s", user) | ||
123 | |||
124 | for row in cursor: | ||
125 | yield row | ||
126 | |||
127 | def main_process_market(user_market, actions, before=False, after=False): | ||
128 | if len(actions or []) == 0: | ||
129 | if before: | ||
130 | Processor(user_market).process("sell_all", steps="before") | ||
131 | if after: | ||
132 | Processor(user_market).process("sell_all", steps="after") | ||
133 | else: | ||
134 | for action in actions: | ||
135 | if action in globals(): | ||
136 | (globals()[action])(user_market) | ||
137 | else: | ||
138 | raise NotImplementedError("Unknown action {}".format(action)) | ||
139 | |||
140 | def main_store_report(report_path, user_id, user_market): | ||
141 | try: | ||
142 | if report_path is not None: | ||
143 | report_file = "{}/{}_{}.json".format(report_path, datetime.now().isoformat(), user_id) | ||
144 | with open(report_file, "w") as f: | ||
145 | f.write(user_market.report.to_json()) | ||
146 | except Exception as e: | ||
147 | print("impossible to store report file: {}; {}".format(e.__class__.__name__, e)) | ||
148 | |||
149 | def print_orders(market, base_currency="BTC"): | ||
150 | market.report.log_stage("print_orders") | ||
151 | market.balances.fetch_balances(tag="print_orders") | ||
152 | market.prepare_trades(base_currency=base_currency, compute_value="average") | ||
153 | market.trades.prepare_orders(compute_value="average") | ||
154 | |||
155 | def print_balances(market, base_currency="BTC"): | ||
156 | market.report.log_stage("print_balances") | ||
157 | market.balances.fetch_balances() | ||
158 | if base_currency is not None: | ||
159 | market.report.print_log("total:") | ||
160 | market.report.print_log(sum(market.balances.in_currency(base_currency).values())) | ||
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 | "close_trades": {}, | ||
183 | }, | ||
184 | { | ||
185 | "name": "buy", | ||
186 | "number": 2, | ||
187 | "before": False, | ||
188 | "after": True, | ||
189 | "fetch_balances": ["begin", "end"], | ||
190 | "prepare_trades": { "only": "acquire" }, | ||
191 | "prepare_orders": { "only": "acquire", "compute_value": "average" }, | ||
192 | "move_balances": {}, | ||
193 | "run_orders": {}, | ||
194 | "follow_orders": {}, | ||
195 | "close_trades": {}, | ||
196 | }, | ||
197 | ], | ||
198 | "sell_all": [ | ||
199 | { | ||
200 | "name": "all_sell", | ||
201 | "number": 1, | ||
202 | "before": True, | ||
203 | "after": False, | ||
204 | "fetch_balances": ["begin", "end"], | ||
205 | "prepare_trades": { "repartition": { "base_currency": (1, "long") } }, | ||
206 | "prepare_orders": { "compute_value": "average" }, | ||
207 | "run_orders": {}, | ||
208 | "follow_orders": {}, | ||
209 | "close_trades": {}, | ||
210 | }, | ||
211 | { | ||
212 | "name": "wait", | ||
213 | "number": 2, | ||
214 | "before": False, | ||
215 | "after": True, | ||
216 | "wait_for_recent": {}, | ||
217 | }, | ||
218 | { | ||
219 | "name": "all_buy", | ||
220 | "number": 3, | ||
221 | "before": False, | ||
222 | "after": True, | ||
223 | "fetch_balances": ["begin", "end"], | ||
224 | "prepare_trades": {}, | ||
225 | "prepare_orders": { "compute_value": "average" }, | ||
226 | "move_balances": {}, | ||
227 | "run_orders": {}, | ||
228 | "follow_orders": {}, | ||
229 | "close_trades": {}, | ||
230 | }, | ||
231 | ] | ||
232 | } | ||
233 | |||
234 | ordered_actions = [ | ||
235 | "wait_for_recent", "prepare_trades", "prepare_orders", | ||
236 | "move_balances", "run_orders", "follow_orders", | ||
237 | "close_trades"] | ||
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 self.ordered_actions: | ||
273 | if action in step: | ||
274 | self.run_action(action, step[action], kwargs) | ||
275 | |||
276 | if "end" in step.get("fetch_balances", []): | ||
277 | self.market.balances.fetch_balances(tag="{}_end".format(process_name)) | ||
278 | self.market.report.log_stage("{}_end".format(process_name)) | ||
279 | |||
280 | def method_arguments(self, action): | ||
281 | import inspect | ||
282 | |||
283 | if action == "wait_for_recent": | ||
284 | method = portfolio.Portfolio.wait_for_recent | ||
285 | elif action == "prepare_trades": | ||
286 | method = self.market.prepare_trades | ||
287 | elif action == "prepare_orders": | ||
288 | method = self.market.trades.prepare_orders | ||
289 | elif action == "move_balances": | ||
290 | method = self.market.move_balances | ||
291 | elif action == "run_orders": | ||
292 | method = self.market.trades.run_orders | ||
293 | elif action == "follow_orders": | ||
294 | method = self.market.follow_orders | ||
295 | elif action == "close_trades": | ||
296 | method = self.market.trades.close_trades | ||
297 | |||
298 | signature = inspect.getfullargspec(method) | ||
299 | defaults = signature.defaults or [] | ||
300 | kwargs = signature.args[-len(defaults):] | ||
301 | |||
302 | return [method, kwargs] | ||
303 | |||
304 | def parse_args(self, action, default_args, kwargs): | ||
305 | method, allowed_arguments = self.method_arguments(action) | ||
306 | args = {k: v for k, v in {**default_args, **kwargs}.items() if k in allowed_arguments } | ||
307 | |||
308 | if "repartition" in args and "base_currency" in args["repartition"]: | ||
309 | r = args["repartition"] | ||
310 | r[args.get("base_currency", "BTC")] = r.pop("base_currency") | ||
311 | |||
312 | return method, args | ||
313 | |||
314 | def run_action(self, action, default_args, kwargs): | ||
315 | method, args = self.parse_args(action, default_args, kwargs) | ||
316 | |||
317 | if action == "wait_for_recent": | ||
318 | method(self.market, **args) | ||
319 | else: | ||
320 | method(**args) | ||
@@ -1,18 +1,143 @@ | |||
1 | from datetime import datetime | ||
2 | import argparse | ||
3 | import configparser | ||
4 | import psycopg2 | ||
5 | import os | ||
1 | import sys | 6 | import sys |
2 | import helper, market | ||
3 | 7 | ||
4 | args = helper.main_parse_args(sys.argv[1:]) | 8 | import market |
9 | import portfolio | ||
5 | 10 | ||
6 | pg_config, report_path = helper.main_parse_config(args.config) | 11 | __all__ = ["make_order", "get_user_market"] |
7 | 12 | ||
8 | for market_config, user_id in helper.main_fetch_markets(pg_config, args.user): | 13 | def make_order(market, value, currency, action="acquire", |
9 | try: | 14 | close_if_possible=False, base_currency="BTC", follow=True, |
10 | user_market = market.Market.from_config(market_config, debug=args.debug) | 15 | compute_value="average"): |
11 | helper.main_process_market(user_market, args.action, before=args.before, after=args.after) | 16 | """ |
12 | except Exception as e: | 17 | Make an order on market |
18 | "market": The market on which to place the order | ||
19 | "value": The value in *base_currency* to acquire, | ||
20 | or in *currency* to dispose. | ||
21 | use negative for margin trade. | ||
22 | "action": "acquire" or "dispose". | ||
23 | "acquire" will buy long or sell short, | ||
24 | "dispose" will sell long or buy short. | ||
25 | "currency": The currency to acquire or dispose | ||
26 | "base_currency": The base currency. The value is expressed in that | ||
27 | currency (default: BTC) | ||
28 | "follow": Whether to follow the order once run (default: True) | ||
29 | "close_if_possible": Whether to try to close the position at the end | ||
30 | of the trade, i.e. reach exactly 0 at the end | ||
31 | (only meaningful in "dispose"). May have | ||
32 | unwanted effects if the end value of the | ||
33 | currency is not 0. | ||
34 | "compute_value": Compute value to place the order | ||
35 | """ | ||
36 | market.report.log_stage("make_order_begin") | ||
37 | market.balances.fetch_balances(tag="make_order_begin") | ||
38 | if action == "acquire": | ||
39 | trade = portfolio.Trade( | ||
40 | portfolio.Amount(base_currency, 0), | ||
41 | portfolio.Amount(base_currency, value), | ||
42 | currency, market) | ||
43 | else: | ||
44 | amount = portfolio.Amount(currency, value) | ||
45 | trade = portfolio.Trade( | ||
46 | amount.in_currency(base_currency, market, compute_value=compute_value), | ||
47 | portfolio.Amount(base_currency, 0), | ||
48 | currency, market) | ||
49 | market.trades.all.append(trade) | ||
50 | order = trade.prepare_order( | ||
51 | close_if_possible=close_if_possible, | ||
52 | compute_value=compute_value) | ||
53 | market.report.log_orders([order], None, compute_value) | ||
54 | market.trades.run_orders() | ||
55 | if follow: | ||
56 | market.follow_orders() | ||
57 | market.balances.fetch_balances(tag="make_order_end") | ||
58 | else: | ||
59 | market.report.log_stage("make_order_end_not_followed") | ||
60 | return order | ||
61 | market.report.log_stage("make_order_end") | ||
62 | |||
63 | def get_user_market(config_path, user_id, debug=False): | ||
64 | pg_config, report_path = parse_config(config_path) | ||
65 | market_config = list(fetch_markets(pg_config, str(user_id)))[0][0] | ||
66 | return market.Market.from_config(market_config, debug=debug) | ||
67 | |||
68 | def fetch_markets(pg_config, user): | ||
69 | connection = psycopg2.connect(**pg_config) | ||
70 | cursor = connection.cursor() | ||
71 | |||
72 | if user is None: | ||
73 | cursor.execute("SELECT config,user_id FROM market_configs") | ||
74 | else: | ||
75 | cursor.execute("SELECT config,user_id FROM market_configs WHERE user_id = %s", user) | ||
76 | |||
77 | for row in cursor: | ||
78 | yield row | ||
79 | |||
80 | def parse_config(config_file): | ||
81 | config = configparser.ConfigParser() | ||
82 | config.read(config_file) | ||
83 | |||
84 | if "postgresql" not in config: | ||
85 | print("no configuration for postgresql in config file") | ||
86 | sys.exit(1) | ||
87 | |||
88 | if "app" in config and "report_path" in config["app"]: | ||
89 | report_path = config["app"]["report_path"] | ||
90 | |||
91 | if not os.path.exists(report_path): | ||
92 | os.makedirs(report_path) | ||
93 | else: | ||
94 | report_path = None | ||
95 | |||
96 | return [config["postgresql"], report_path] | ||
97 | |||
98 | def parse_args(argv): | ||
99 | parser = argparse.ArgumentParser( | ||
100 | description="Run the trade bot") | ||
101 | |||
102 | parser.add_argument("-c", "--config", | ||
103 | default="config.ini", | ||
104 | required=False, | ||
105 | help="Config file to load (default: config.ini)") | ||
106 | parser.add_argument("--before", | ||
107 | default=False, action='store_const', const=True, | ||
108 | help="Run the steps before the cryptoportfolio update") | ||
109 | parser.add_argument("--after", | ||
110 | default=False, action='store_const', const=True, | ||
111 | help="Run the steps after the cryptoportfolio update") | ||
112 | parser.add_argument("--debug", | ||
113 | default=False, action='store_const', const=True, | ||
114 | help="Run in debug mode") | ||
115 | parser.add_argument("--user", | ||
116 | default=None, required=False, help="Only run for that user") | ||
117 | parser.add_argument("--action", | ||
118 | action='append', | ||
119 | help="Do a different action than trading (add several times to chain)") | ||
120 | |||
121 | args = parser.parse_args(argv) | ||
122 | |||
123 | if not os.path.exists(args.config): | ||
124 | print("no config file found, exiting") | ||
125 | sys.exit(1) | ||
126 | |||
127 | return args | ||
128 | |||
129 | def main(argv): | ||
130 | args = parse_args(argv) | ||
131 | |||
132 | pg_config, report_path = parse_config(args.config) | ||
133 | |||
134 | for market_config, user_id in fetch_markets(pg_config, args.user): | ||
13 | try: | 135 | try: |
14 | user_market.report.log_error("main", exception=e) | 136 | market.Market\ |
15 | except: | 137 | .from_config(market_config, debug=args.debug, user_id=user_id, report_path=report_path)\ |
138 | .process(args.action, before=args.before, after=args.after) | ||
139 | except Exception as e: | ||
16 | print("{}: {}".format(e.__class__.__name__, e)) | 140 | print("{}: {}".format(e.__class__.__name__, e)) |
17 | finally: | 141 | |
18 | helper.main_store_report(report_path, user_id, user_market) | 142 | if __name__ == '__main__': # pragma: no cover |
143 | main(sys.argv[1:]) | ||
@@ -3,6 +3,8 @@ import ccxt_wrapper as ccxt | |||
3 | import time | 3 | import time |
4 | from store import * | 4 | from store import * |
5 | from cachetools.func import ttl_cache | 5 | from cachetools.func import ttl_cache |
6 | from datetime import datetime | ||
7 | import portfolio | ||
6 | 8 | ||
7 | class Market: | 9 | class Market: |
8 | debug = False | 10 | debug = False |
@@ -11,17 +13,21 @@ class Market: | |||
11 | trades = None | 13 | trades = None |
12 | balances = None | 14 | balances = None |
13 | 15 | ||
14 | def __init__(self, ccxt_instance, debug=False): | 16 | def __init__(self, ccxt_instance, debug=False, user_id=None, report_path=None): |
15 | self.debug = debug | 17 | self.debug = debug |
16 | self.ccxt = ccxt_instance | 18 | self.ccxt = ccxt_instance |
17 | self.ccxt._market = self | 19 | self.ccxt._market = self |
18 | self.report = ReportStore(self) | 20 | self.report = ReportStore(self) |
19 | self.trades = TradeStore(self) | 21 | self.trades = TradeStore(self) |
20 | self.balances = BalanceStore(self) | 22 | self.balances = BalanceStore(self) |
23 | self.processor = Processor(self) | ||
24 | |||
25 | self.user_id = user_id | ||
26 | self.report_path = report_path | ||
21 | 27 | ||
22 | @classmethod | 28 | @classmethod |
23 | def from_config(cls, config, debug=False): | 29 | def from_config(cls, config, debug=False, user_id=None, report_path=None): |
24 | config["apiKey"] = config.pop("key") | 30 | config["apiKey"] = config.pop("key", None) |
25 | 31 | ||
26 | ccxt_instance = ccxt.poloniexE(config) | 32 | ccxt_instance = ccxt.poloniexE(config) |
27 | 33 | ||
@@ -37,7 +43,34 @@ class Market: | |||
37 | ccxt_instance.session.request = request_wrap.__get__(ccxt_instance.session, | 43 | ccxt_instance.session.request = request_wrap.__get__(ccxt_instance.session, |
38 | ccxt_instance.session.__class__) | 44 | ccxt_instance.session.__class__) |
39 | 45 | ||
40 | return cls(ccxt_instance, debug=debug) | 46 | return cls(ccxt_instance, debug=debug, user_id=user_id, report_path=report_path) |
47 | |||
48 | def store_report(self): | ||
49 | try: | ||
50 | if self.report_path is not None: | ||
51 | report_file = "{}/{}_{}.json".format(self.report_path, datetime.now().isoformat(), self.user_id) | ||
52 | with open(report_file, "w") as f: | ||
53 | f.write(self.report.to_json()) | ||
54 | except Exception as e: | ||
55 | print("impossible to store report file: {}; {}".format(e.__class__.__name__, e)) | ||
56 | |||
57 | def process(self, actions, before=False, after=False): | ||
58 | try: | ||
59 | if len(actions or []) == 0: | ||
60 | if before: | ||
61 | self.processor.process("sell_all", steps="before") | ||
62 | if after: | ||
63 | self.processor.process("sell_all", steps="after") | ||
64 | else: | ||
65 | for action in actions: | ||
66 | if hasattr(self, action): | ||
67 | getattr(self, action)() | ||
68 | else: | ||
69 | self.report.log_error("market_process", message="Unknown action {}".format(action)) | ||
70 | except Exception as e: | ||
71 | self.report.log_error("market_process", exception=e) | ||
72 | finally: | ||
73 | self.store_report() | ||
41 | 74 | ||
42 | def move_balances(self): | 75 | def move_balances(self): |
43 | needed_in_margin = {} | 76 | needed_in_margin = {} |
@@ -143,3 +176,177 @@ class Market: | |||
143 | liquidity=liquidity, repartition=repartition) | 176 | liquidity=liquidity, repartition=repartition) |
144 | self.trades.compute_trades(values_in_base, new_repartition, only=only) | 177 | self.trades.compute_trades(values_in_base, new_repartition, only=only) |
145 | 178 | ||
179 | # Helpers | ||
180 | def print_orders(self, base_currency="BTC"): | ||
181 | self.report.log_stage("print_orders") | ||
182 | self.balances.fetch_balances(tag="print_orders") | ||
183 | self.prepare_trades(base_currency=base_currency, compute_value="average") | ||
184 | self.trades.prepare_orders(compute_value="average") | ||
185 | |||
186 | def print_balances(self, base_currency="BTC"): | ||
187 | self.report.log_stage("print_balances") | ||
188 | self.balances.fetch_balances() | ||
189 | if base_currency is not None: | ||
190 | self.report.print_log("total:") | ||
191 | self.report.print_log(sum(self.balances.in_currency(base_currency).values())) | ||
192 | |||
193 | class Processor: | ||
194 | scenarios = { | ||
195 | "sell_needed": [ | ||
196 | { | ||
197 | "name": "wait", | ||
198 | "number": 0, | ||
199 | "before": False, | ||
200 | "after": True, | ||
201 | "wait_for_recent": {}, | ||
202 | }, | ||
203 | { | ||
204 | "name": "sell", | ||
205 | "number": 1, | ||
206 | "before": False, | ||
207 | "after": True, | ||
208 | "fetch_balances": ["begin", "end"], | ||
209 | "prepare_trades": {}, | ||
210 | "prepare_orders": { "only": "dispose", "compute_value": "average" }, | ||
211 | "run_orders": {}, | ||
212 | "follow_orders": {}, | ||
213 | "close_trades": {}, | ||
214 | }, | ||
215 | { | ||
216 | "name": "buy", | ||
217 | "number": 2, | ||
218 | "before": False, | ||
219 | "after": True, | ||
220 | "fetch_balances": ["begin", "end"], | ||
221 | "prepare_trades": { "only": "acquire" }, | ||
222 | "prepare_orders": { "only": "acquire", "compute_value": "average" }, | ||
223 | "move_balances": {}, | ||
224 | "run_orders": {}, | ||
225 | "follow_orders": {}, | ||
226 | "close_trades": {}, | ||
227 | }, | ||
228 | ], | ||
229 | "sell_all": [ | ||
230 | { | ||
231 | "name": "all_sell", | ||
232 | "number": 1, | ||
233 | "before": True, | ||
234 | "after": False, | ||
235 | "fetch_balances": ["begin", "end"], | ||
236 | "prepare_trades": { "repartition": { "base_currency": (1, "long") } }, | ||
237 | "prepare_orders": { "compute_value": "average" }, | ||
238 | "run_orders": {}, | ||
239 | "follow_orders": {}, | ||
240 | "close_trades": {}, | ||
241 | }, | ||
242 | { | ||
243 | "name": "wait", | ||
244 | "number": 2, | ||
245 | "before": False, | ||
246 | "after": True, | ||
247 | "wait_for_recent": {}, | ||
248 | }, | ||
249 | { | ||
250 | "name": "all_buy", | ||
251 | "number": 3, | ||
252 | "before": False, | ||
253 | "after": True, | ||
254 | "fetch_balances": ["begin", "end"], | ||
255 | "prepare_trades": {}, | ||
256 | "prepare_orders": { "compute_value": "average" }, | ||
257 | "move_balances": {}, | ||
258 | "run_orders": {}, | ||
259 | "follow_orders": {}, | ||
260 | "close_trades": {}, | ||
261 | }, | ||
262 | ] | ||
263 | } | ||
264 | |||
265 | ordered_actions = [ | ||
266 | "wait_for_recent", "prepare_trades", "prepare_orders", | ||
267 | "move_balances", "run_orders", "follow_orders", | ||
268 | "close_trades"] | ||
269 | |||
270 | def __init__(self, market): | ||
271 | self.market = market | ||
272 | |||
273 | def select_steps(self, scenario, step): | ||
274 | if step == "all": | ||
275 | return scenario | ||
276 | elif step == "before" or step == "after": | ||
277 | return list(filter(lambda x: step in x and x[step], scenario)) | ||
278 | elif type(step) == int: | ||
279 | return [scenario[step-1]] | ||
280 | elif type(step) == str: | ||
281 | return list(filter(lambda x: x["name"] == step, scenario)) | ||
282 | else: | ||
283 | raise TypeError("Unknown step {}".format(step)) | ||
284 | |||
285 | def process(self, scenario_name, steps="all", **kwargs): | ||
286 | scenario = self.scenarios[scenario_name] | ||
287 | selected_steps = [] | ||
288 | |||
289 | if type(steps) == str or type(steps) == int: | ||
290 | selected_steps += self.select_steps(scenario, steps) | ||
291 | else: | ||
292 | for step in steps: | ||
293 | selected_steps += self.select_steps(scenario, step) | ||
294 | for step in selected_steps: | ||
295 | self.process_step(scenario_name, step, kwargs) | ||
296 | |||
297 | def process_step(self, scenario_name, step, kwargs): | ||
298 | process_name = "process_{}__{}_{}".format(scenario_name, step["number"], step["name"]) | ||
299 | self.market.report.log_stage("{}_begin".format(process_name)) | ||
300 | if "begin" in step.get("fetch_balances", []): | ||
301 | self.market.balances.fetch_balances(tag="{}_begin".format(process_name)) | ||
302 | |||
303 | for action in self.ordered_actions: | ||
304 | if action in step: | ||
305 | self.run_action(action, step[action], kwargs) | ||
306 | |||
307 | if "end" in step.get("fetch_balances", []): | ||
308 | self.market.balances.fetch_balances(tag="{}_end".format(process_name)) | ||
309 | self.market.report.log_stage("{}_end".format(process_name)) | ||
310 | |||
311 | def method_arguments(self, action): | ||
312 | import inspect | ||
313 | |||
314 | if action == "wait_for_recent": | ||
315 | method = portfolio.Portfolio.wait_for_recent | ||
316 | elif action == "prepare_trades": | ||
317 | method = self.market.prepare_trades | ||
318 | elif action == "prepare_orders": | ||
319 | method = self.market.trades.prepare_orders | ||
320 | elif action == "move_balances": | ||
321 | method = self.market.move_balances | ||
322 | elif action == "run_orders": | ||
323 | method = self.market.trades.run_orders | ||
324 | elif action == "follow_orders": | ||
325 | method = self.market.follow_orders | ||
326 | elif action == "close_trades": | ||
327 | method = self.market.trades.close_trades | ||
328 | |||
329 | signature = inspect.getfullargspec(method) | ||
330 | defaults = signature.defaults or [] | ||
331 | kwargs = signature.args[-len(defaults):] | ||
332 | |||
333 | return [method, kwargs] | ||
334 | |||
335 | def parse_args(self, action, default_args, kwargs): | ||
336 | method, allowed_arguments = self.method_arguments(action) | ||
337 | args = {k: v for k, v in {**default_args, **kwargs}.items() if k in allowed_arguments } | ||
338 | |||
339 | if "repartition" in args and "base_currency" in args["repartition"]: | ||
340 | r = args["repartition"] | ||
341 | r[args.get("base_currency", "BTC")] = r.pop("base_currency") | ||
342 | |||
343 | return method, args | ||
344 | |||
345 | def run_action(self, action, default_args, kwargs): | ||
346 | method, args = self.parse_args(action, default_args, kwargs) | ||
347 | |||
348 | if action == "wait_for_recent": | ||
349 | method(self.market, **args) | ||
350 | else: | ||
351 | method(**args) | ||
352 | |||
@@ -7,7 +7,7 @@ from unittest import mock | |||
7 | import requests | 7 | import requests |
8 | import requests_mock | 8 | import requests_mock |
9 | from io import StringIO | 9 | from io import StringIO |
10 | import portfolio, helper, market | 10 | import portfolio, market, main |
11 | 11 | ||
12 | limits = ["acceptance", "unit"] | 12 | limits = ["acceptance", "unit"] |
13 | for test_type in limits: | 13 | for test_type in limits: |
@@ -907,7 +907,163 @@ class MarketTest(WebMockTestCase): | |||
907 | self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") | 907 | self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") |
908 | self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") | 908 | self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") |
909 | self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") | 909 | self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") |
910 | 910 | ||
911 | def test_store_report(self): | ||
912 | |||
913 | file_open = mock.mock_open() | ||
914 | with self.subTest(file=None), mock.patch("market.open", file_open): | ||
915 | m = market.Market(self.ccxt, user_id=1) | ||
916 | m.store_report() | ||
917 | file_open.assert_not_called() | ||
918 | |||
919 | file_open = mock.mock_open() | ||
920 | m = market.Market(self.ccxt, report_path="present", user_id=1) | ||
921 | with self.subTest(file="present"),\ | ||
922 | mock.patch("market.open", file_open),\ | ||
923 | mock.patch.object(m, "report") as report,\ | ||
924 | mock.patch.object(market, "datetime") as time_mock: | ||
925 | |||
926 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
927 | report.to_json.return_value = "json_content" | ||
928 | |||
929 | m.store_report() | ||
930 | |||
931 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") | ||
932 | file_open().write.assert_called_once_with("json_content") | ||
933 | m.report.to_json.assert_called_once_with() | ||
934 | |||
935 | m = market.Market(self.ccxt, report_path="error", user_id=1) | ||
936 | with self.subTest(file="error"),\ | ||
937 | mock.patch("market.open") as file_open,\ | ||
938 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
939 | file_open.side_effect = FileNotFoundError | ||
940 | |||
941 | m.store_report() | ||
942 | |||
943 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | ||
944 | |||
945 | def test_print_orders(self): | ||
946 | m = market.Market(self.ccxt) | ||
947 | with mock.patch.object(m.report, "log_stage") as log_stage,\ | ||
948 | mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ | ||
949 | mock.patch.object(m, "prepare_trades") as prepare_trades,\ | ||
950 | mock.patch.object(m.trades, "prepare_orders") as prepare_orders: | ||
951 | m.print_orders() | ||
952 | |||
953 | log_stage.assert_called_with("print_orders") | ||
954 | fetch_balances.assert_called_with(tag="print_orders") | ||
955 | prepare_trades.assert_called_with(base_currency="BTC", | ||
956 | compute_value="average") | ||
957 | prepare_orders.assert_called_with(compute_value="average") | ||
958 | |||
959 | def test_print_balances(self): | ||
960 | m = market.Market(self.ccxt) | ||
961 | |||
962 | with mock.patch.object(m.balances, "in_currency") as in_currency,\ | ||
963 | mock.patch.object(m.report, "log_stage") as log_stage,\ | ||
964 | mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ | ||
965 | mock.patch.object(m.report, "print_log") as print_log: | ||
966 | |||
967 | in_currency.return_value = { | ||
968 | "BTC": portfolio.Amount("BTC", "0.65"), | ||
969 | "ETH": portfolio.Amount("BTC", "0.3"), | ||
970 | } | ||
971 | |||
972 | m.print_balances() | ||
973 | |||
974 | log_stage.assert_called_once_with("print_balances") | ||
975 | fetch_balances.assert_called_with() | ||
976 | print_log.assert_has_calls([ | ||
977 | mock.call("total:"), | ||
978 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
979 | ]) | ||
980 | |||
981 | @mock.patch("market.Processor.process") | ||
982 | @mock.patch("market.ReportStore.log_error") | ||
983 | @mock.patch("market.Market.store_report") | ||
984 | def test_process(self, store_report, log_error, process): | ||
985 | m = market.Market(self.ccxt) | ||
986 | with self.subTest(before=False, after=False): | ||
987 | m.process(None) | ||
988 | |||
989 | process.assert_not_called() | ||
990 | store_report.assert_called_once() | ||
991 | log_error.assert_not_called() | ||
992 | |||
993 | process.reset_mock() | ||
994 | log_error.reset_mock() | ||
995 | store_report.reset_mock() | ||
996 | with self.subTest(before=True, after=False): | ||
997 | m.process(None, before=True) | ||
998 | |||
999 | process.assert_called_once_with("sell_all", steps="before") | ||
1000 | store_report.assert_called_once() | ||
1001 | log_error.assert_not_called() | ||
1002 | |||
1003 | process.reset_mock() | ||
1004 | log_error.reset_mock() | ||
1005 | store_report.reset_mock() | ||
1006 | with self.subTest(before=False, after=True): | ||
1007 | m.process(None, after=True) | ||
1008 | |||
1009 | process.assert_called_once_with("sell_all", steps="after") | ||
1010 | store_report.assert_called_once() | ||
1011 | log_error.assert_not_called() | ||
1012 | |||
1013 | process.reset_mock() | ||
1014 | log_error.reset_mock() | ||
1015 | store_report.reset_mock() | ||
1016 | with self.subTest(before=True, after=True): | ||
1017 | m.process(None, before=True, after=True) | ||
1018 | |||
1019 | process.assert_has_calls([ | ||
1020 | mock.call("sell_all", steps="before"), | ||
1021 | mock.call("sell_all", steps="after"), | ||
1022 | ]) | ||
1023 | store_report.assert_called_once() | ||
1024 | log_error.assert_not_called() | ||
1025 | |||
1026 | process.reset_mock() | ||
1027 | log_error.reset_mock() | ||
1028 | store_report.reset_mock() | ||
1029 | with self.subTest(action="print_balances"),\ | ||
1030 | mock.patch.object(m, "print_balances") as print_balances: | ||
1031 | m.process(["print_balances"]) | ||
1032 | |||
1033 | process.assert_not_called() | ||
1034 | log_error.assert_not_called() | ||
1035 | store_report.assert_called_once() | ||
1036 | print_balances.assert_called_once_with() | ||
1037 | |||
1038 | log_error.reset_mock() | ||
1039 | store_report.reset_mock() | ||
1040 | with self.subTest(action="print_orders"),\ | ||
1041 | mock.patch.object(m, "print_orders") as print_orders,\ | ||
1042 | mock.patch.object(m, "print_balances") as print_balances: | ||
1043 | m.process(["print_orders", "print_balances"]) | ||
1044 | |||
1045 | process.assert_not_called() | ||
1046 | log_error.assert_not_called() | ||
1047 | store_report.assert_called_once() | ||
1048 | print_orders.assert_called_once_with() | ||
1049 | print_balances.assert_called_once_with() | ||
1050 | |||
1051 | log_error.reset_mock() | ||
1052 | store_report.reset_mock() | ||
1053 | with self.subTest(action="unknown"): | ||
1054 | m.process(["unknown"]) | ||
1055 | log_error.assert_called_once_with("market_process", message="Unknown action unknown") | ||
1056 | store_report.assert_called_once() | ||
1057 | |||
1058 | log_error.reset_mock() | ||
1059 | store_report.reset_mock() | ||
1060 | with self.subTest(unhandled_exception=True): | ||
1061 | process.side_effect = Exception("bouh") | ||
1062 | |||
1063 | m.process(None, before=True) | ||
1064 | log_error.assert_called_with("market_process", exception=mock.ANY) | ||
1065 | store_report.assert_called_once() | ||
1066 | |||
911 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 1067 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
912 | class TradeStoreTest(WebMockTestCase): | 1068 | class TradeStoreTest(WebMockTestCase): |
913 | def test_compute_trades(self): | 1069 | def test_compute_trades(self): |
@@ -2752,7 +2908,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2752 | }) | 2908 | }) |
2753 | 2909 | ||
2754 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 2910 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
2755 | class HelperTest(WebMockTestCase): | 2911 | class MainTest(WebMockTestCase): |
2756 | def test_make_order(self): | 2912 | def test_make_order(self): |
2757 | self.m.get_ticker.return_value = { | 2913 | self.m.get_ticker.return_value = { |
2758 | "inverted": False, | 2914 | "inverted": False, |
@@ -2762,7 +2918,7 @@ class HelperTest(WebMockTestCase): | |||
2762 | } | 2918 | } |
2763 | 2919 | ||
2764 | with self.subTest(description="nominal case"): | 2920 | with self.subTest(description="nominal case"): |
2765 | helper.make_order(self.m, 10, "ETH") | 2921 | main.make_order(self.m, 10, "ETH") |
2766 | 2922 | ||
2767 | self.m.report.log_stage.assert_has_calls([ | 2923 | self.m.report.log_stage.assert_has_calls([ |
2768 | mock.call("make_order_begin"), | 2924 | mock.call("make_order_begin"), |
@@ -2787,7 +2943,7 @@ class HelperTest(WebMockTestCase): | |||
2787 | 2943 | ||
2788 | self.m.reset_mock() | 2944 | self.m.reset_mock() |
2789 | with self.subTest(compute_value="default"): | 2945 | with self.subTest(compute_value="default"): |
2790 | helper.make_order(self.m, 10, "ETH", action="dispose", | 2946 | main.make_order(self.m, 10, "ETH", action="dispose", |
2791 | compute_value="ask") | 2947 | compute_value="ask") |
2792 | 2948 | ||
2793 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 2949 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
@@ -2796,7 +2952,7 @@ class HelperTest(WebMockTestCase): | |||
2796 | 2952 | ||
2797 | self.m.reset_mock() | 2953 | self.m.reset_mock() |
2798 | with self.subTest(follow=False): | 2954 | with self.subTest(follow=False): |
2799 | result = helper.make_order(self.m, 10, "ETH", follow=False) | 2955 | result = main.make_order(self.m, 10, "ETH", follow=False) |
2800 | 2956 | ||
2801 | self.m.report.log_stage.assert_has_calls([ | 2957 | self.m.report.log_stage.assert_has_calls([ |
2802 | mock.call("make_order_begin"), | 2958 | mock.call("make_order_begin"), |
@@ -2816,7 +2972,7 @@ class HelperTest(WebMockTestCase): | |||
2816 | 2972 | ||
2817 | self.m.reset_mock() | 2973 | self.m.reset_mock() |
2818 | with self.subTest(base_currency="USDT"): | 2974 | with self.subTest(base_currency="USDT"): |
2819 | helper.make_order(self.m, 1, "BTC", base_currency="USDT") | 2975 | main.make_order(self.m, 1, "BTC", base_currency="USDT") |
2820 | 2976 | ||
2821 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 2977 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2822 | self.assertEqual("BTC", trade.currency) | 2978 | self.assertEqual("BTC", trade.currency) |
@@ -2824,14 +2980,14 @@ class HelperTest(WebMockTestCase): | |||
2824 | 2980 | ||
2825 | self.m.reset_mock() | 2981 | self.m.reset_mock() |
2826 | with self.subTest(close_if_possible=True): | 2982 | with self.subTest(close_if_possible=True): |
2827 | helper.make_order(self.m, 10, "ETH", close_if_possible=True) | 2983 | main.make_order(self.m, 10, "ETH", close_if_possible=True) |
2828 | 2984 | ||
2829 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 2985 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2830 | self.assertEqual(True, trade.orders[0].close_if_possible) | 2986 | self.assertEqual(True, trade.orders[0].close_if_possible) |
2831 | 2987 | ||
2832 | self.m.reset_mock() | 2988 | self.m.reset_mock() |
2833 | with self.subTest(action="dispose"): | 2989 | with self.subTest(action="dispose"): |
2834 | helper.make_order(self.m, 10, "ETH", action="dispose") | 2990 | main.make_order(self.m, 10, "ETH", action="dispose") |
2835 | 2991 | ||
2836 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 2992 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2837 | self.assertEqual(0, trade.value_to) | 2993 | self.assertEqual(0, trade.value_to) |
@@ -2841,19 +2997,19 @@ class HelperTest(WebMockTestCase): | |||
2841 | 2997 | ||
2842 | self.m.reset_mock() | 2998 | self.m.reset_mock() |
2843 | with self.subTest(compute_value="default"): | 2999 | with self.subTest(compute_value="default"): |
2844 | helper.make_order(self.m, 10, "ETH", action="dispose", | 3000 | main.make_order(self.m, 10, "ETH", action="dispose", |
2845 | compute_value="bid") | 3001 | compute_value="bid") |
2846 | 3002 | ||
2847 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3003 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2848 | self.assertEqual(D("0.9"), trade.value_from.value) | 3004 | self.assertEqual(D("0.9"), trade.value_from.value) |
2849 | 3005 | ||
2850 | def test_user_market(self): | 3006 | def test_get_user_market(self): |
2851 | with mock.patch("helper.main_fetch_markets") as main_fetch_markets,\ | 3007 | with mock.patch("main.fetch_markets") as main_fetch_markets,\ |
2852 | mock.patch("helper.main_parse_config") as main_parse_config: | 3008 | mock.patch("main.parse_config") as main_parse_config: |
2853 | with self.subTest(debug=False): | 3009 | with self.subTest(debug=False): |
2854 | main_parse_config.return_value = ["pg_config", "report_path"] | 3010 | main_parse_config.return_value = ["pg_config", "report_path"] |
2855 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 3011 | main_fetch_markets.return_value = [({"key": "market_config"},)] |
2856 | m = helper.get_user_market("config_path.ini", 1) | 3012 | m = main.get_user_market("config_path.ini", 1) |
2857 | 3013 | ||
2858 | self.assertIsInstance(m, market.Market) | 3014 | self.assertIsInstance(m, market.Market) |
2859 | self.assertFalse(m.debug) | 3015 | self.assertFalse(m.debug) |
@@ -2861,141 +3017,56 @@ class HelperTest(WebMockTestCase): | |||
2861 | with self.subTest(debug=True): | 3017 | with self.subTest(debug=True): |
2862 | main_parse_config.return_value = ["pg_config", "report_path"] | 3018 | main_parse_config.return_value = ["pg_config", "report_path"] |
2863 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 3019 | main_fetch_markets.return_value = [({"key": "market_config"},)] |
2864 | m = helper.get_user_market("config_path.ini", 1, debug=True) | 3020 | m = main.get_user_market("config_path.ini", 1, debug=True) |
2865 | 3021 | ||
2866 | self.assertIsInstance(m, market.Market) | 3022 | self.assertIsInstance(m, market.Market) |
2867 | self.assertTrue(m.debug) | 3023 | self.assertTrue(m.debug) |
2868 | 3024 | ||
2869 | def test_main_store_report(self): | 3025 | def test_main(self): |
2870 | file_open = mock.mock_open() | 3026 | with mock.patch("main.parse_args") as parse_args,\ |
2871 | with self.subTest(file=None), mock.patch("__main__.open", file_open): | 3027 | mock.patch("main.parse_config") as parse_config,\ |
2872 | helper.main_store_report(None, 1, self.m) | 3028 | mock.patch("main.fetch_markets") as fetch_markets,\ |
2873 | file_open.assert_not_called() | 3029 | mock.patch("market.Market") as market_mock,\ |
2874 | |||
2875 | file_open = mock.mock_open() | ||
2876 | with self.subTest(file="present"), mock.patch("helper.open", file_open),\ | ||
2877 | mock.patch.object(helper, "datetime") as time_mock: | ||
2878 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
2879 | self.m.report.to_json.return_value = "json_content" | ||
2880 | |||
2881 | helper.main_store_report("present", 1, self.m) | ||
2882 | |||
2883 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") | ||
2884 | file_open().write.assert_called_once_with("json_content") | ||
2885 | self.m.report.to_json.assert_called_once_with() | ||
2886 | |||
2887 | with self.subTest(file="error"),\ | ||
2888 | mock.patch("helper.open") as file_open,\ | ||
2889 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3030 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
2890 | file_open.side_effect = FileNotFoundError | ||
2891 | 3031 | ||
2892 | helper.main_store_report("error", 1, self.m) | 3032 | args_mock = mock.Mock() |
3033 | args_mock.action = "action" | ||
3034 | args_mock.config = "config" | ||
3035 | args_mock.user = "user" | ||
3036 | args_mock.debug = "debug" | ||
3037 | args_mock.before = "before" | ||
3038 | args_mock.after = "after" | ||
3039 | parse_args.return_value = args_mock | ||
2893 | 3040 | ||
2894 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | 3041 | parse_config.return_value = ["pg_config", "report_path"] |
2895 | 3042 | ||
2896 | @mock.patch("helper.Processor.process") | 3043 | fetch_markets.return_value = [["config1", 1], ["config2", 2]] |
2897 | def test_main_process_market(self, process): | ||
2898 | with self.subTest(before=False, after=False): | ||
2899 | m = mock.Mock() | ||
2900 | helper.main_process_market(m, None) | ||
2901 | 3044 | ||
2902 | process.assert_not_called() | 3045 | main.main(["Foo", "Bar"]) |
2903 | 3046 | ||
2904 | process.reset_mock() | 3047 | parse_args.assert_called_with(["Foo", "Bar"]) |
2905 | with self.subTest(before=True, after=False): | 3048 | parse_config.assert_called_with("config") |
2906 | helper.main_process_market(m, None, before=True) | 3049 | fetch_markets.assert_called_with("pg_config", "user") |
2907 | 3050 | ||
2908 | process.assert_called_once_with("sell_all", steps="before") | 3051 | self.assertEqual(2, market_mock.from_config.call_count) |
2909 | 3052 | market_mock.from_config.assert_has_calls([ | |
2910 | process.reset_mock() | 3053 | mock.call("config1", debug="debug", user_id=1, report_path="report_path"), |
2911 | with self.subTest(before=False, after=True): | 3054 | mock.call().process("action", before="before", after="after"), |
2912 | helper.main_process_market(m, None, after=True) | 3055 | mock.call("config2", debug="debug", user_id=2, report_path="report_path"), |
2913 | 3056 | mock.call().process("action", before="before", after="after") | |
2914 | process.assert_called_once_with("sell_all", steps="after") | ||
2915 | |||
2916 | process.reset_mock() | ||
2917 | with self.subTest(before=True, after=True): | ||
2918 | helper.main_process_market(m, None, before=True, after=True) | ||
2919 | |||
2920 | process.assert_has_calls([ | ||
2921 | mock.call("sell_all", steps="before"), | ||
2922 | mock.call("sell_all", steps="after"), | ||
2923 | ]) | 3057 | ]) |
2924 | 3058 | ||
2925 | process.reset_mock() | 3059 | self.assertEqual("", stdout_mock.getvalue()) |
2926 | with self.subTest(action="print_balances"),\ | ||
2927 | mock.patch("helper.print_balances") as print_balances: | ||
2928 | helper.main_process_market("user", ["print_balances"]) | ||
2929 | |||
2930 | process.assert_not_called() | ||
2931 | print_balances.assert_called_once_with("user") | ||
2932 | |||
2933 | with self.subTest(action="print_orders"),\ | ||
2934 | mock.patch("helper.print_orders") as print_orders,\ | ||
2935 | mock.patch("helper.print_balances") as print_balances: | ||
2936 | helper.main_process_market("user", ["print_orders", "print_balances"]) | ||
2937 | |||
2938 | process.assert_not_called() | ||
2939 | print_orders.assert_called_once_with("user") | ||
2940 | print_balances.assert_called_once_with("user") | ||
2941 | |||
2942 | with self.subTest(action="unknown"),\ | ||
2943 | self.assertRaises(NotImplementedError): | ||
2944 | helper.main_process_market("user", ["unknown"]) | ||
2945 | |||
2946 | @mock.patch.object(helper, "psycopg2") | ||
2947 | def test_fetch_markets(self, psycopg2): | ||
2948 | connect_mock = mock.Mock() | ||
2949 | cursor_mock = mock.MagicMock() | ||
2950 | cursor_mock.__iter__.return_value = ["row_1", "row_2"] | ||
2951 | |||
2952 | connect_mock.cursor.return_value = cursor_mock | ||
2953 | psycopg2.connect.return_value = connect_mock | ||
2954 | |||
2955 | with self.subTest(user=None): | ||
2956 | rows = list(helper.main_fetch_markets({"foo": "bar"}, None)) | ||
2957 | |||
2958 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
2959 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs") | ||
2960 | |||
2961 | self.assertEqual(["row_1", "row_2"], rows) | ||
2962 | |||
2963 | psycopg2.connect.reset_mock() | ||
2964 | cursor_mock.execute.reset_mock() | ||
2965 | with self.subTest(user=1): | ||
2966 | rows = list(helper.main_fetch_markets({"foo": "bar"}, 1)) | ||
2967 | |||
2968 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
2969 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1) | ||
2970 | |||
2971 | self.assertEqual(["row_1", "row_2"], rows) | ||
2972 | |||
2973 | @mock.patch.object(helper.sys, "exit") | ||
2974 | def test_main_parse_args(self, exit): | ||
2975 | with self.subTest(config="config.ini"): | ||
2976 | args = helper.main_parse_args([]) | ||
2977 | self.assertEqual("config.ini", args.config) | ||
2978 | self.assertFalse(args.before) | ||
2979 | self.assertFalse(args.after) | ||
2980 | self.assertFalse(args.debug) | ||
2981 | |||
2982 | args = helper.main_parse_args(["--before", "--after", "--debug"]) | ||
2983 | self.assertTrue(args.before) | ||
2984 | self.assertTrue(args.after) | ||
2985 | self.assertTrue(args.debug) | ||
2986 | |||
2987 | exit.assert_not_called() | ||
2988 | 3060 | ||
2989 | with self.subTest(config="inexistant"),\ | 3061 | with self.subTest(exception=True): |
2990 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3062 | market_mock.from_config.side_effect = Exception("boo") |
2991 | args = helper.main_parse_args(["--config", "foo.bar"]) | 3063 | main.main(["Foo", "Bar"]) |
2992 | exit.assert_called_once_with(1) | 3064 | self.assertEqual("Exception: boo\nException: boo\n", stdout_mock.getvalue()) |
2993 | self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) | ||
2994 | 3065 | ||
2995 | @mock.patch.object(helper.sys, "exit") | 3066 | @mock.patch.object(main.sys, "exit") |
2996 | @mock.patch("helper.configparser") | 3067 | @mock.patch("main.configparser") |
2997 | @mock.patch("helper.os") | 3068 | @mock.patch("main.os") |
2998 | def test_main_parse_config(self, os, configparser, exit): | 3069 | def test_parse_config(self, os, configparser, exit): |
2999 | with self.subTest(pg_config=True, report_path=None): | 3070 | with self.subTest(pg_config=True, report_path=None): |
3000 | config_mock = mock.MagicMock() | 3071 | config_mock = mock.MagicMock() |
3001 | configparser.ConfigParser.return_value = config_mock | 3072 | configparser.ConfigParser.return_value = config_mock |
@@ -3005,7 +3076,7 @@ class HelperTest(WebMockTestCase): | |||
3005 | config_mock.__contains__.side_effect = config | 3076 | config_mock.__contains__.side_effect = config |
3006 | config_mock.__getitem__.return_value = "pg_config" | 3077 | config_mock.__getitem__.return_value = "pg_config" |
3007 | 3078 | ||
3008 | result = helper.main_parse_config("configfile") | 3079 | result = main.parse_config("configfile") |
3009 | 3080 | ||
3010 | config_mock.read.assert_called_with("configfile") | 3081 | config_mock.read.assert_called_with("configfile") |
3011 | 3082 | ||
@@ -3023,7 +3094,7 @@ class HelperTest(WebMockTestCase): | |||
3023 | ] | 3094 | ] |
3024 | 3095 | ||
3025 | os.path.exists.return_value = False | 3096 | os.path.exists.return_value = False |
3026 | result = helper.main_parse_config("configfile") | 3097 | result = main.parse_config("configfile") |
3027 | 3098 | ||
3028 | config_mock.read.assert_called_with("configfile") | 3099 | config_mock.read.assert_called_with("configfile") |
3029 | self.assertEqual(["pg_config", "report_path"], result) | 3100 | self.assertEqual(["pg_config", "report_path"], result) |
@@ -3034,46 +3105,71 @@ class HelperTest(WebMockTestCase): | |||
3034 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3105 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
3035 | config_mock = mock.MagicMock() | 3106 | config_mock = mock.MagicMock() |
3036 | configparser.ConfigParser.return_value = config_mock | 3107 | configparser.ConfigParser.return_value = config_mock |
3037 | result = helper.main_parse_config("configfile") | 3108 | result = main.parse_config("configfile") |
3038 | 3109 | ||
3039 | config_mock.read.assert_called_with("configfile") | 3110 | config_mock.read.assert_called_with("configfile") |
3040 | exit.assert_called_once_with(1) | 3111 | exit.assert_called_once_with(1) |
3041 | self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) | 3112 | self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) |
3042 | 3113 | ||
3114 | @mock.patch.object(main.sys, "exit") | ||
3115 | def test_parse_args(self, exit): | ||
3116 | with self.subTest(config="config.ini"): | ||
3117 | args = main.parse_args([]) | ||
3118 | self.assertEqual("config.ini", args.config) | ||
3119 | self.assertFalse(args.before) | ||
3120 | self.assertFalse(args.after) | ||
3121 | self.assertFalse(args.debug) | ||
3043 | 3122 | ||
3044 | def test_print_orders(self): | 3123 | args = main.parse_args(["--before", "--after", "--debug"]) |
3045 | helper.print_orders(self.m) | 3124 | self.assertTrue(args.before) |
3125 | self.assertTrue(args.after) | ||
3126 | self.assertTrue(args.debug) | ||
3046 | 3127 | ||
3047 | self.m.report.log_stage.assert_called_with("print_orders") | 3128 | exit.assert_not_called() |
3048 | self.m.balances.fetch_balances.assert_called_with(tag="print_orders") | 3129 | |
3049 | self.m.prepare_trades.assert_called_with(base_currency="BTC", | 3130 | with self.subTest(config="inexistant"),\ |
3050 | compute_value="average") | 3131 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
3051 | self.m.trades.prepare_orders.assert_called_with(compute_value="average") | 3132 | args = main.parse_args(["--config", "foo.bar"]) |
3133 | exit.assert_called_once_with(1) | ||
3134 | self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) | ||
3135 | |||
3136 | @mock.patch.object(main, "psycopg2") | ||
3137 | def test_fetch_markets(self, psycopg2): | ||
3138 | connect_mock = mock.Mock() | ||
3139 | cursor_mock = mock.MagicMock() | ||
3140 | cursor_mock.__iter__.return_value = ["row_1", "row_2"] | ||
3141 | |||
3142 | connect_mock.cursor.return_value = cursor_mock | ||
3143 | psycopg2.connect.return_value = connect_mock | ||
3144 | |||
3145 | with self.subTest(user=None): | ||
3146 | rows = list(main.fetch_markets({"foo": "bar"}, None)) | ||
3147 | |||
3148 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
3149 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs") | ||
3150 | |||
3151 | self.assertEqual(["row_1", "row_2"], rows) | ||
3152 | |||
3153 | psycopg2.connect.reset_mock() | ||
3154 | cursor_mock.execute.reset_mock() | ||
3155 | with self.subTest(user=1): | ||
3156 | rows = list(main.fetch_markets({"foo": "bar"}, 1)) | ||
3157 | |||
3158 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
3159 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1) | ||
3160 | |||
3161 | self.assertEqual(["row_1", "row_2"], rows) | ||
3052 | 3162 | ||
3053 | def test_print_balances(self): | ||
3054 | self.m.balances.in_currency.return_value = { | ||
3055 | "BTC": portfolio.Amount("BTC", "0.65"), | ||
3056 | "ETH": portfolio.Amount("BTC", "0.3"), | ||
3057 | } | ||
3058 | |||
3059 | helper.print_balances(self.m) | ||
3060 | |||
3061 | self.m.report.log_stage.assert_called_once_with("print_balances") | ||
3062 | self.m.balances.fetch_balances.assert_called_with() | ||
3063 | self.m.report.print_log.assert_has_calls([ | ||
3064 | mock.call("total:"), | ||
3065 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
3066 | ]) | ||
3067 | 3163 | ||
3068 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 3164 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
3069 | class ProcessorTest(WebMockTestCase): | 3165 | class ProcessorTest(WebMockTestCase): |
3070 | def test_values(self): | 3166 | def test_values(self): |
3071 | processor = helper.Processor(self.m) | 3167 | processor = market.Processor(self.m) |
3072 | 3168 | ||
3073 | self.assertEqual(self.m, processor.market) | 3169 | self.assertEqual(self.m, processor.market) |
3074 | 3170 | ||
3075 | def test_run_action(self): | 3171 | def test_run_action(self): |
3076 | processor = helper.Processor(self.m) | 3172 | processor = market.Processor(self.m) |
3077 | 3173 | ||
3078 | with mock.patch.object(processor, "parse_args") as parse_args: | 3174 | with mock.patch.object(processor, "parse_args") as parse_args: |
3079 | method_mock = mock.Mock() | 3175 | method_mock = mock.Mock() |
@@ -3090,7 +3186,7 @@ class ProcessorTest(WebMockTestCase): | |||
3090 | method_mock.assert_called_with(self.m, foo="bar") | 3186 | method_mock.assert_called_with(self.m, foo="bar") |
3091 | 3187 | ||
3092 | def test_select_step(self): | 3188 | def test_select_step(self): |
3093 | processor = helper.Processor(self.m) | 3189 | processor = market.Processor(self.m) |
3094 | 3190 | ||
3095 | scenario = processor.scenarios["sell_all"] | 3191 | scenario = processor.scenarios["sell_all"] |
3096 | 3192 | ||
@@ -3103,9 +3199,9 @@ class ProcessorTest(WebMockTestCase): | |||
3103 | with self.assertRaises(TypeError): | 3199 | with self.assertRaises(TypeError): |
3104 | processor.select_steps(scenario, ["wait"]) | 3200 | processor.select_steps(scenario, ["wait"]) |
3105 | 3201 | ||
3106 | @mock.patch("helper.Processor.process_step") | 3202 | @mock.patch("market.Processor.process_step") |
3107 | def test_process(self, process_step): | 3203 | def test_process(self, process_step): |
3108 | processor = helper.Processor(self.m) | 3204 | processor = market.Processor(self.m) |
3109 | 3205 | ||
3110 | processor.process("sell_all", foo="bar") | 3206 | processor.process("sell_all", foo="bar") |
3111 | self.assertEqual(3, process_step.call_count) | 3207 | self.assertEqual(3, process_step.call_count) |
@@ -3126,7 +3222,7 @@ class ProcessorTest(WebMockTestCase): | |||
3126 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) | 3222 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) |
3127 | m = market.Market(ccxt) | 3223 | m = market.Market(ccxt) |
3128 | 3224 | ||
3129 | processor = helper.Processor(m) | 3225 | processor = market.Processor(m) |
3130 | 3226 | ||
3131 | method, arguments = processor.method_arguments("wait_for_recent") | 3227 | method, arguments = processor.method_arguments("wait_for_recent") |
3132 | self.assertEqual(portfolio.Portfolio.wait_for_recent, method) | 3228 | self.assertEqual(portfolio.Portfolio.wait_for_recent, method) |
@@ -3152,7 +3248,7 @@ class ProcessorTest(WebMockTestCase): | |||
3152 | self.assertEqual(m.trades.close_trades, method) | 3248 | self.assertEqual(m.trades.close_trades, method) |
3153 | 3249 | ||
3154 | def test_process_step(self): | 3250 | def test_process_step(self): |
3155 | processor = helper.Processor(self.m) | 3251 | processor = market.Processor(self.m) |
3156 | 3252 | ||
3157 | with mock.patch.object(processor, "run_action") as run_action: | 3253 | with mock.patch.object(processor, "run_action") as run_action: |
3158 | step = processor.scenarios["sell_needed"][1] | 3254 | step = processor.scenarios["sell_needed"][1] |
@@ -3186,7 +3282,7 @@ class ProcessorTest(WebMockTestCase): | |||
3186 | self.m.balances.fetch_balances.assert_not_called() | 3282 | self.m.balances.fetch_balances.assert_not_called() |
3187 | 3283 | ||
3188 | def test_parse_args(self): | 3284 | def test_parse_args(self): |
3189 | processor = helper.Processor(self.m) | 3285 | processor = market.Processor(self.m) |
3190 | 3286 | ||
3191 | with mock.patch.object(processor, "method_arguments") as method_arguments: | 3287 | with mock.patch.object(processor, "method_arguments") as method_arguments: |
3192 | method_mock = mock.Mock() | 3288 | method_mock = mock.Mock() |