diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-25 22:04:02 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-25 22:04:02 +0200 |
commit | bfe841c557094afad2db0d2c63deadeea4ba63c6 (patch) | |
tree | dfade60890ffe5529dc80fec0b23702b97ea0383 | |
parent | bd7ba362442f27fe3f53729a0040f2473b85a068 (diff) | |
parent | d004a2a5e15a78991870dcb90cd6db63ab40a4e6 (diff) | |
download | Trader-bfe841c557094afad2db0d2c63deadeea4ba63c6.tar.gz Trader-bfe841c557094afad2db0d2c63deadeea4ba63c6.tar.zst Trader-bfe841c557094afad2db0d2c63deadeea4ba63c6.zip |
Merge branch 'dev'v1.0
-rw-r--r-- | ccxt_wrapper.py | 45 | ||||
-rw-r--r-- | helper.py | 320 | ||||
-rw-r--r-- | main.py | 171 | ||||
-rw-r--r-- | market.py | 300 | ||||
-rw-r--r-- | portfolio.py | 158 | ||||
-rw-r--r-- | store.py | 203 | ||||
-rw-r--r-- | tasks/import_reports_to_database.py | 4 | ||||
-rw-r--r-- | test.py | 2113 | ||||
-rw-r--r-- | test_samples/poloniexETest.test_fetch_all_balances.1.json | 1459 | ||||
-rw-r--r-- | test_samples/poloniexETest.test_fetch_all_balances.2.json | 101 | ||||
-rw-r--r-- | test_samples/poloniexETest.test_fetch_all_balances.3.json | 11 | ||||
-rw-r--r-- | test_samples/test_portfolio.json (renamed from test_portfolio.json) | 0 |
12 files changed, 4098 insertions, 787 deletions
diff --git a/ccxt_wrapper.py b/ccxt_wrapper.py index d37c306..4ed37d9 100644 --- a/ccxt_wrapper.py +++ b/ccxt_wrapper.py | |||
@@ -1,12 +1,57 @@ | |||
1 | from ccxt import * | 1 | from ccxt import * |
2 | import decimal | 2 | import decimal |
3 | import time | 3 | import time |
4 | from retry.api import retry_call | ||
5 | import re | ||
4 | 6 | ||
5 | def _cw_exchange_sum(self, *args): | 7 | def _cw_exchange_sum(self, *args): |
6 | return sum([arg for arg in args if isinstance(arg, (float, int, decimal.Decimal))]) | 8 | return sum([arg for arg in args if isinstance(arg, (float, int, decimal.Decimal))]) |
7 | Exchange.sum = _cw_exchange_sum | 9 | Exchange.sum = _cw_exchange_sum |
8 | 10 | ||
9 | class poloniexE(poloniex): | 11 | class poloniexE(poloniex): |
12 | RETRIABLE_CALLS = [ | ||
13 | re.compile(r"^return"), | ||
14 | re.compile(r"^cancel"), | ||
15 | re.compile(r"^closeMarginPosition$"), | ||
16 | re.compile(r"^getMarginPosition$"), | ||
17 | ] | ||
18 | |||
19 | def request(self, path, api='public', method='GET', params={}, headers=None, body=None): | ||
20 | """ | ||
21 | Wrapped to allow retry of non-posting requests" | ||
22 | """ | ||
23 | |||
24 | origin_request = super(poloniexE, self).request | ||
25 | kwargs = { | ||
26 | "api": api, | ||
27 | "method": method, | ||
28 | "params": params, | ||
29 | "headers": headers, | ||
30 | "body": body | ||
31 | } | ||
32 | |||
33 | retriable = any(re.match(call, path) for call in self.RETRIABLE_CALLS) | ||
34 | if api == "public" or method == "GET" or retriable: | ||
35 | return retry_call(origin_request, fargs=[path], fkwargs=kwargs, | ||
36 | tries=10, delay=1, exceptions=(RequestTimeout,)) | ||
37 | else: | ||
38 | return origin_request(path, **kwargs) | ||
39 | |||
40 | def __init__(self, *args, **kwargs): | ||
41 | super(poloniexE, self).__init__(*args, **kwargs) | ||
42 | |||
43 | # For requests logging | ||
44 | self.session.origin_request = self.session.request | ||
45 | self.session._parent = self | ||
46 | |||
47 | def request_wrap(self, *args, **kwargs): | ||
48 | r = self.origin_request(*args, **kwargs) | ||
49 | self._parent._market.report.log_http_request(args[0], | ||
50 | args[1], kwargs["data"], kwargs["headers"], r) | ||
51 | return r | ||
52 | self.session.request = request_wrap.__get__(self.session, | ||
53 | self.session.__class__) | ||
54 | |||
10 | @staticmethod | 55 | @staticmethod |
11 | def nanoseconds(): | 56 | def nanoseconds(): |
12 | return int(time.time() * 1000000000) | 57 | return int(time.time() * 1000000000) |
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,165 @@ | |||
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", |
14 | close_if_possible=False, base_currency="BTC", follow=True, | ||
15 | compute_value="average"): | ||
16 | """ | ||
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_id, market_config, user_id = list(fetch_markets(pg_config, str(user_id)))[0] | ||
66 | args = type('Args', (object,), { "debug": debug, "quiet": False })() | ||
67 | return market.Market.from_config(market_config, args, | ||
68 | pg_config=pg_config, market_id=market_id, | ||
69 | user_id=user_id, report_path=report_path) | ||
70 | |||
71 | def fetch_markets(pg_config, user): | ||
72 | connection = psycopg2.connect(**pg_config) | ||
73 | cursor = connection.cursor() | ||
74 | |||
75 | if user is None: | ||
76 | cursor.execute("SELECT id,config,user_id FROM market_configs") | ||
77 | else: | ||
78 | cursor.execute("SELECT id,config,user_id FROM market_configs WHERE user_id = %s", user) | ||
79 | |||
80 | for row in cursor: | ||
81 | yield row | ||
82 | |||
83 | def parse_config(config_file): | ||
84 | config = configparser.ConfigParser() | ||
85 | config.read(config_file) | ||
86 | |||
87 | if "postgresql" not in config: | ||
88 | print("no configuration for postgresql in config file") | ||
89 | sys.exit(1) | ||
90 | |||
91 | if "app" in config and "report_path" in config["app"]: | ||
92 | report_path = config["app"]["report_path"] | ||
93 | |||
94 | if not os.path.exists(report_path): | ||
95 | os.makedirs(report_path) | ||
96 | else: | ||
97 | report_path = None | ||
98 | |||
99 | return [config["postgresql"], report_path] | ||
100 | |||
101 | def parse_args(argv): | ||
102 | parser = argparse.ArgumentParser( | ||
103 | description="Run the trade bot") | ||
104 | |||
105 | parser.add_argument("-c", "--config", | ||
106 | default="config.ini", | ||
107 | required=False, | ||
108 | help="Config file to load (default: config.ini)") | ||
109 | parser.add_argument("--before", | ||
110 | default=False, action='store_const', const=True, | ||
111 | help="Run the steps before the cryptoportfolio update") | ||
112 | parser.add_argument("--after", | ||
113 | default=False, action='store_const', const=True, | ||
114 | help="Run the steps after the cryptoportfolio update") | ||
115 | parser.add_argument("--quiet", | ||
116 | default=False, action='store_const', const=True, | ||
117 | help="Don't print messages") | ||
118 | parser.add_argument("--debug", | ||
119 | default=False, action='store_const', const=True, | ||
120 | help="Run in debug mode") | ||
121 | parser.add_argument("--user", | ||
122 | default=None, required=False, help="Only run for that user") | ||
123 | parser.add_argument("--action", | ||
124 | action='append', | ||
125 | help="Do a different action than trading (add several times to chain)") | ||
126 | parser.add_argument("--parallel", action='store_true', default=True, dest="parallel") | ||
127 | parser.add_argument("--no-parallel", action='store_false', dest="parallel") | ||
128 | |||
129 | args = parser.parse_args(argv) | ||
130 | |||
131 | if not os.path.exists(args.config): | ||
132 | print("no config file found, exiting") | ||
133 | sys.exit(1) | ||
134 | |||
135 | return args | ||
136 | |||
137 | def process(market_config, market_id, user_id, args, report_path, pg_config): | ||
9 | try: | 138 | try: |
10 | user_market = market.Market.from_config(market_config, debug=args.debug) | 139 | market.Market\ |
11 | helper.main_process_market(user_market, args.action, before=args.before, after=args.after) | 140 | .from_config(market_config, args, |
141 | pg_config=pg_config, market_id=market_id, | ||
142 | user_id=user_id, report_path=report_path)\ | ||
143 | .process(args.action, before=args.before, after=args.after) | ||
12 | except Exception as e: | 144 | except Exception as e: |
13 | try: | 145 | print("{}: {}".format(e.__class__.__name__, e)) |
14 | user_market.report.log_error("main", exception=e) | 146 | |
15 | except: | 147 | def main(argv): |
16 | print("{}: {}".format(e.__class__.__name__, e)) | 148 | args = parse_args(argv) |
17 | finally: | 149 | |
18 | helper.main_store_report(report_path, user_id, user_market) | 150 | pg_config, report_path = parse_config(args.config) |
151 | |||
152 | if args.parallel: | ||
153 | import threading | ||
154 | market.Portfolio.start_worker() | ||
155 | |||
156 | def process_(*args): | ||
157 | threading.Thread(target=process, args=args).start() | ||
158 | else: | ||
159 | process_ = process | ||
160 | |||
161 | for market_id, market_config, user_id in fetch_markets(pg_config, args.user): | ||
162 | process_(market_config, market_id, user_id, args, report_path, pg_config) | ||
163 | |||
164 | if __name__ == '__main__': # pragma: no cover | ||
165 | main(sys.argv[1:]) | ||
@@ -1,8 +1,12 @@ | |||
1 | from ccxt import ExchangeError, NotSupported | 1 | from ccxt import ExchangeError, NotSupported, RequestTimeout |
2 | import ccxt_wrapper as ccxt | 2 | import ccxt_wrapper as ccxt |
3 | import time | 3 | import time |
4 | import psycopg2 | ||
4 | from store import * | 5 | from store import * |
5 | from cachetools.func import ttl_cache | 6 | from cachetools.func import ttl_cache |
7 | from datetime import datetime | ||
8 | from retry import retry | ||
9 | import portfolio | ||
6 | 10 | ||
7 | class Market: | 11 | class Market: |
8 | debug = False | 12 | debug = False |
@@ -11,34 +15,81 @@ class Market: | |||
11 | trades = None | 15 | trades = None |
12 | balances = None | 16 | balances = None |
13 | 17 | ||
14 | def __init__(self, ccxt_instance, debug=False): | 18 | def __init__(self, ccxt_instance, args, **kwargs): |
15 | self.debug = debug | 19 | self.args = args |
20 | self.debug = args.debug | ||
16 | self.ccxt = ccxt_instance | 21 | self.ccxt = ccxt_instance |
17 | self.ccxt._market = self | 22 | self.ccxt._market = self |
18 | self.report = ReportStore(self) | 23 | self.report = ReportStore(self, verbose_print=(not args.quiet)) |
19 | self.trades = TradeStore(self) | 24 | self.trades = TradeStore(self) |
20 | self.balances = BalanceStore(self) | 25 | self.balances = BalanceStore(self) |
26 | self.processor = Processor(self) | ||
27 | |||
28 | for key in ["user_id", "market_id", "report_path", "pg_config"]: | ||
29 | setattr(self, key, kwargs.get(key, None)) | ||
21 | 30 | ||
22 | @classmethod | 31 | @classmethod |
23 | def from_config(cls, config, debug=False): | 32 | def from_config(cls, config, args, **kwargs): |
24 | config["apiKey"] = config.pop("key") | 33 | config["apiKey"] = config.pop("key", None) |
25 | 34 | ||
26 | ccxt_instance = ccxt.poloniexE(config) | 35 | ccxt_instance = ccxt.poloniexE(config) |
27 | 36 | ||
28 | # For requests logging | 37 | return cls(ccxt_instance, args, **kwargs) |
29 | ccxt_instance.session.origin_request = ccxt_instance.session.request | 38 | |
30 | ccxt_instance.session._parent = ccxt_instance | 39 | def store_report(self): |
40 | self.report.merge(Portfolio.report) | ||
41 | date = datetime.now() | ||
42 | if self.report_path is not None: | ||
43 | self.store_file_report(date) | ||
44 | if self.pg_config is not None: | ||
45 | self.store_database_report(date) | ||
46 | |||
47 | def store_file_report(self, date): | ||
48 | try: | ||
49 | report_file = "{}/{}_{}".format(self.report_path, date.isoformat(), self.user_id) | ||
50 | with open(report_file + ".json", "w") as f: | ||
51 | f.write(self.report.to_json()) | ||
52 | with open(report_file + ".log", "w") as f: | ||
53 | f.write("\n".join(map(lambda x: x[1], self.report.print_logs))) | ||
54 | except Exception as e: | ||
55 | print("impossible to store report file: {}; {}".format(e.__class__.__name__, e)) | ||
56 | |||
57 | def store_database_report(self, date): | ||
58 | try: | ||
59 | report_query = 'INSERT INTO reports("date", "market_config_id", "debug") VALUES (%s, %s, %s) RETURNING id;' | ||
60 | line_query = 'INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);' | ||
61 | connection = psycopg2.connect(**self.pg_config) | ||
62 | cursor = connection.cursor() | ||
63 | cursor.execute(report_query, (date, self.market_id, self.debug)) | ||
64 | report_id = cursor.fetchone()[0] | ||
65 | for date, type_, payload in self.report.to_json_array(): | ||
66 | cursor.execute(line_query, (date, report_id, type_, payload)) | ||
31 | 67 | ||
32 | def request_wrap(self, *args, **kwargs): | 68 | connection.commit() |
33 | r = self.origin_request(*args, **kwargs) | 69 | cursor.close() |
34 | self._parent._market.report.log_http_request(args[0], | 70 | connection.close() |
35 | args[1], kwargs["data"], kwargs["headers"], r) | 71 | except Exception as e: |
36 | return r | 72 | print("impossible to store report to database: {}; {}".format(e.__class__.__name__, e)) |
37 | ccxt_instance.session.request = request_wrap.__get__(ccxt_instance.session, | ||
38 | ccxt_instance.session.__class__) | ||
39 | 73 | ||
40 | return cls(ccxt_instance, debug=debug) | 74 | def process(self, actions, before=False, after=False): |
75 | try: | ||
76 | if len(actions or []) == 0: | ||
77 | if before: | ||
78 | self.processor.process("sell_all", steps="before") | ||
79 | if after: | ||
80 | self.processor.process("sell_all", steps="after") | ||
81 | else: | ||
82 | for action in actions: | ||
83 | if hasattr(self, action): | ||
84 | getattr(self, action)() | ||
85 | else: | ||
86 | self.report.log_error("market_process", message="Unknown action {}".format(action)) | ||
87 | except Exception as e: | ||
88 | self.report.log_error("market_process", exception=e) | ||
89 | finally: | ||
90 | self.store_report() | ||
41 | 91 | ||
92 | @retry(RequestTimeout, tries=5) | ||
42 | def move_balances(self): | 93 | def move_balances(self): |
43 | needed_in_margin = {} | 94 | needed_in_margin = {} |
44 | moving_to_margin = {} | 95 | moving_to_margin = {} |
@@ -53,13 +104,21 @@ class Market: | |||
53 | current_balance = self.balances.all[currency].margin_available | 104 | current_balance = self.balances.all[currency].margin_available |
54 | moving_to_margin[currency] = (needed - current_balance) | 105 | moving_to_margin[currency] = (needed - current_balance) |
55 | delta = moving_to_margin[currency].value | 106 | delta = moving_to_margin[currency].value |
107 | action = "Moving {} from exchange to margin".format(moving_to_margin[currency]) | ||
108 | |||
56 | if self.debug and delta != 0: | 109 | if self.debug and delta != 0: |
57 | self.report.log_debug_action("Moving {} from exchange to margin".format(moving_to_margin[currency])) | 110 | self.report.log_debug_action(action) |
58 | continue | 111 | continue |
59 | if delta > 0: | 112 | try: |
60 | self.ccxt.transfer_balance(currency, delta, "exchange", "margin") | 113 | if delta > 0: |
61 | elif delta < 0: | 114 | self.ccxt.transfer_balance(currency, delta, "exchange", "margin") |
62 | self.ccxt.transfer_balance(currency, -delta, "margin", "exchange") | 115 | elif delta < 0: |
116 | self.ccxt.transfer_balance(currency, -delta, "margin", "exchange") | ||
117 | except RequestTimeout as e: | ||
118 | self.report.log_error(action, message="Retrying", exception=e) | ||
119 | self.report.log_move_balances(needed_in_margin, moving_to_margin) | ||
120 | self.balances.fetch_balances() | ||
121 | raise e | ||
63 | self.report.log_move_balances(needed_in_margin, moving_to_margin) | 122 | self.report.log_move_balances(needed_in_margin, moving_to_margin) |
64 | 123 | ||
65 | self.balances.fetch_balances() | 124 | self.balances.fetch_balances() |
@@ -143,3 +202,200 @@ class Market: | |||
143 | liquidity=liquidity, repartition=repartition) | 202 | liquidity=liquidity, repartition=repartition) |
144 | self.trades.compute_trades(values_in_base, new_repartition, only=only) | 203 | self.trades.compute_trades(values_in_base, new_repartition, only=only) |
145 | 204 | ||
205 | # Helpers | ||
206 | def print_orders(self, base_currency="BTC"): | ||
207 | self.report.log_stage("print_orders") | ||
208 | self.balances.fetch_balances(tag="print_orders") | ||
209 | self.prepare_trades(base_currency=base_currency, compute_value="average") | ||
210 | self.trades.prepare_orders(compute_value="average") | ||
211 | |||
212 | def print_balances(self, base_currency="BTC"): | ||
213 | self.report.log_stage("print_balances") | ||
214 | self.balances.fetch_balances() | ||
215 | if base_currency is not None: | ||
216 | self.report.print_log("total:") | ||
217 | self.report.print_log(sum(self.balances.in_currency(base_currency).values())) | ||
218 | |||
219 | class Processor: | ||
220 | scenarios = { | ||
221 | "wait_for_cryptoportfolio": [ | ||
222 | { | ||
223 | "name": "wait", | ||
224 | "number": 1, | ||
225 | "before": False, | ||
226 | "after": True, | ||
227 | "wait_for_recent": {}, | ||
228 | }, | ||
229 | ], | ||
230 | "print_orders": [ | ||
231 | { | ||
232 | "name": "wait", | ||
233 | "number": 1, | ||
234 | "before": False, | ||
235 | "after": True, | ||
236 | "wait_for_recent": {}, | ||
237 | }, | ||
238 | { | ||
239 | "name": "make_orders", | ||
240 | "number": 2, | ||
241 | "before": False, | ||
242 | "after": True, | ||
243 | "fetch_balances": ["begin"], | ||
244 | "prepare_trades": { "compute_value": "average" }, | ||
245 | "prepare_orders": { "compute_value": "average" }, | ||
246 | }, | ||
247 | ], | ||
248 | "sell_needed": [ | ||
249 | { | ||
250 | "name": "wait", | ||
251 | "number": 0, | ||
252 | "before": False, | ||
253 | "after": True, | ||
254 | "wait_for_recent": {}, | ||
255 | }, | ||
256 | { | ||
257 | "name": "sell", | ||
258 | "number": 1, | ||
259 | "before": False, | ||
260 | "after": True, | ||
261 | "fetch_balances": ["begin", "end"], | ||
262 | "prepare_trades": {}, | ||
263 | "prepare_orders": { "only": "dispose", "compute_value": "average" }, | ||
264 | "run_orders": {}, | ||
265 | "follow_orders": {}, | ||
266 | "close_trades": {}, | ||
267 | }, | ||
268 | { | ||
269 | "name": "buy", | ||
270 | "number": 2, | ||
271 | "before": False, | ||
272 | "after": True, | ||
273 | "fetch_balances": ["begin", "end"], | ||
274 | "prepare_trades": { "only": "acquire" }, | ||
275 | "prepare_orders": { "only": "acquire", "compute_value": "average" }, | ||
276 | "move_balances": {}, | ||
277 | "run_orders": {}, | ||
278 | "follow_orders": {}, | ||
279 | "close_trades": {}, | ||
280 | }, | ||
281 | ], | ||
282 | "sell_all": [ | ||
283 | { | ||
284 | "name": "all_sell", | ||
285 | "number": 1, | ||
286 | "before": True, | ||
287 | "after": False, | ||
288 | "fetch_balances": ["begin", "end"], | ||
289 | "prepare_trades": { "repartition": { "base_currency": (1, "long") } }, | ||
290 | "prepare_orders": { "compute_value": "average" }, | ||
291 | "run_orders": {}, | ||
292 | "follow_orders": {}, | ||
293 | "close_trades": {}, | ||
294 | }, | ||
295 | { | ||
296 | "name": "wait", | ||
297 | "number": 2, | ||
298 | "before": False, | ||
299 | "after": True, | ||
300 | "wait_for_recent": {}, | ||
301 | }, | ||
302 | { | ||
303 | "name": "all_buy", | ||
304 | "number": 3, | ||
305 | "before": False, | ||
306 | "after": True, | ||
307 | "fetch_balances": ["begin", "end"], | ||
308 | "prepare_trades": {}, | ||
309 | "prepare_orders": { "compute_value": "average" }, | ||
310 | "move_balances": {}, | ||
311 | "run_orders": {}, | ||
312 | "follow_orders": {}, | ||
313 | "close_trades": {}, | ||
314 | }, | ||
315 | ] | ||
316 | } | ||
317 | |||
318 | ordered_actions = [ | ||
319 | "wait_for_recent", "prepare_trades", "prepare_orders", | ||
320 | "move_balances", "run_orders", "follow_orders", | ||
321 | "close_trades"] | ||
322 | |||
323 | def __init__(self, market): | ||
324 | self.market = market | ||
325 | |||
326 | def select_steps(self, scenario, step): | ||
327 | if step == "all": | ||
328 | return scenario | ||
329 | elif step == "before" or step == "after": | ||
330 | return list(filter(lambda x: step in x and x[step], scenario)) | ||
331 | elif type(step) == int: | ||
332 | return [scenario[step-1]] | ||
333 | elif type(step) == str: | ||
334 | return list(filter(lambda x: x["name"] == step, scenario)) | ||
335 | else: | ||
336 | raise TypeError("Unknown step {}".format(step)) | ||
337 | |||
338 | def process(self, scenario_name, steps="all", **kwargs): | ||
339 | scenario = self.scenarios[scenario_name] | ||
340 | selected_steps = [] | ||
341 | |||
342 | if type(steps) == str or type(steps) == int: | ||
343 | selected_steps += self.select_steps(scenario, steps) | ||
344 | else: | ||
345 | for step in steps: | ||
346 | selected_steps += self.select_steps(scenario, step) | ||
347 | for step in selected_steps: | ||
348 | self.process_step(scenario_name, step, kwargs) | ||
349 | |||
350 | def process_step(self, scenario_name, step, kwargs): | ||
351 | process_name = "process_{}__{}_{}".format(scenario_name, step["number"], step["name"]) | ||
352 | self.market.report.log_stage("{}_begin".format(process_name)) | ||
353 | if "begin" in step.get("fetch_balances", []): | ||
354 | self.market.balances.fetch_balances(tag="{}_begin".format(process_name)) | ||
355 | |||
356 | for action in self.ordered_actions: | ||
357 | if action in step: | ||
358 | self.run_action(action, step[action], kwargs) | ||
359 | |||
360 | if "end" in step.get("fetch_balances", []): | ||
361 | self.market.balances.fetch_balances(tag="{}_end".format(process_name)) | ||
362 | self.market.report.log_stage("{}_end".format(process_name)) | ||
363 | |||
364 | def method_arguments(self, action): | ||
365 | import inspect | ||
366 | |||
367 | if action == "wait_for_recent": | ||
368 | method = Portfolio.wait_for_recent | ||
369 | elif action == "prepare_trades": | ||
370 | method = self.market.prepare_trades | ||
371 | elif action == "prepare_orders": | ||
372 | method = self.market.trades.prepare_orders | ||
373 | elif action == "move_balances": | ||
374 | method = self.market.move_balances | ||
375 | elif action == "run_orders": | ||
376 | method = self.market.trades.run_orders | ||
377 | elif action == "follow_orders": | ||
378 | method = self.market.follow_orders | ||
379 | elif action == "close_trades": | ||
380 | method = self.market.trades.close_trades | ||
381 | |||
382 | signature = inspect.getfullargspec(method) | ||
383 | defaults = signature.defaults or [] | ||
384 | kwargs = signature.args[-len(defaults):] | ||
385 | |||
386 | return [method, kwargs] | ||
387 | |||
388 | def parse_args(self, action, default_args, kwargs): | ||
389 | method, allowed_arguments = self.method_arguments(action) | ||
390 | args = {k: v for k, v in {**default_args, **kwargs}.items() if k in allowed_arguments } | ||
391 | |||
392 | if "repartition" in args and "base_currency" in args["repartition"]: | ||
393 | r = args["repartition"] | ||
394 | r[args.get("base_currency", "BTC")] = r.pop("base_currency") | ||
395 | |||
396 | return method, args | ||
397 | |||
398 | def run_action(self, action, default_args, kwargs): | ||
399 | method, args = self.parse_args(action, default_args, kwargs) | ||
400 | |||
401 | method(**args) | ||
diff --git a/portfolio.py b/portfolio.py index ed50b57..9c58676 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -1,86 +1,7 @@ | |||
1 | import time | 1 | from datetime import datetime |
2 | from datetime import datetime, timedelta | ||
3 | from decimal import Decimal as D, ROUND_DOWN | ||
4 | from json import JSONDecodeError | ||
5 | from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError | ||
6 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound | ||
7 | from retry import retry | 2 | from retry import retry |
8 | import requests | 3 | from decimal import Decimal as D, ROUND_DOWN |
9 | 4 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout | |
10 | # FIXME: correctly handle web call timeouts | ||
11 | |||
12 | class Portfolio: | ||
13 | URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" | ||
14 | liquidities = {} | ||
15 | data = None | ||
16 | last_date = None | ||
17 | |||
18 | @classmethod | ||
19 | def wait_for_recent(cls, market, delta=4): | ||
20 | cls.repartition(market, refetch=True) | ||
21 | while cls.last_date is None or datetime.now() - cls.last_date > timedelta(delta): | ||
22 | time.sleep(30) | ||
23 | market.report.print_log("Attempt to fetch up-to-date cryptoportfolio") | ||
24 | cls.repartition(market, refetch=True) | ||
25 | |||
26 | @classmethod | ||
27 | def repartition(cls, market, liquidity="medium", refetch=False): | ||
28 | cls.parse_cryptoportfolio(market, refetch=refetch) | ||
29 | liquidities = cls.liquidities[liquidity] | ||
30 | return liquidities[cls.last_date] | ||
31 | |||
32 | @classmethod | ||
33 | def get_cryptoportfolio(cls, market): | ||
34 | try: | ||
35 | r = requests.get(cls.URL) | ||
36 | market.report.log_http_request(r.request.method, | ||
37 | r.request.url, r.request.body, r.request.headers, r) | ||
38 | except Exception as e: | ||
39 | market.report.log_error("get_cryptoportfolio", exception=e) | ||
40 | return | ||
41 | try: | ||
42 | cls.data = r.json(parse_int=D, parse_float=D) | ||
43 | except (JSONDecodeError, SimpleJSONDecodeError): | ||
44 | cls.data = None | ||
45 | |||
46 | @classmethod | ||
47 | def parse_cryptoportfolio(cls, market, refetch=False): | ||
48 | if refetch or cls.data is None: | ||
49 | cls.get_cryptoportfolio(market) | ||
50 | |||
51 | def filter_weights(weight_hash): | ||
52 | if weight_hash[1][0] == 0: | ||
53 | return False | ||
54 | if weight_hash[0] == "_row": | ||
55 | return False | ||
56 | return True | ||
57 | |||
58 | def clean_weights(i): | ||
59 | def clean_weights_(h): | ||
60 | if h[0].endswith("s"): | ||
61 | return [h[0][0:-1], (h[1][i], "short")] | ||
62 | else: | ||
63 | return [h[0], (h[1][i], "long")] | ||
64 | return clean_weights_ | ||
65 | |||
66 | def parse_weights(portfolio_hash): | ||
67 | weights_hash = portfolio_hash["weights"] | ||
68 | weights = {} | ||
69 | for i in range(len(weights_hash["_row"])): | ||
70 | date = datetime.strptime(weights_hash["_row"][i], "%Y-%m-%d") | ||
71 | weights[date] = dict(filter( | ||
72 | filter_weights, | ||
73 | map(clean_weights(i), weights_hash.items()))) | ||
74 | return weights | ||
75 | |||
76 | high_liquidity = parse_weights(cls.data["portfolio_1"]) | ||
77 | medium_liquidity = parse_weights(cls.data["portfolio_2"]) | ||
78 | |||
79 | cls.liquidities = { | ||
80 | "medium": medium_liquidity, | ||
81 | "high": high_liquidity, | ||
82 | } | ||
83 | cls.last_date = max(max(medium_liquidity.keys()), max(high_liquidity.keys())) | ||
84 | 5 | ||
85 | class Computation: | 6 | class Computation: |
86 | computations = { | 7 | computations = { |
@@ -491,6 +412,9 @@ class Trade: | |||
491 | for mouvement in order.mouvements: | 412 | for mouvement in order.mouvements: |
492 | self.market.report.print_log("{}\t\t{}".format(ind, mouvement)) | 413 | self.market.report.print_log("{}\t\t{}".format(ind, mouvement)) |
493 | 414 | ||
415 | class RetryException(Exception): | ||
416 | pass | ||
417 | |||
494 | class Order: | 418 | class Order: |
495 | def __init__(self, action, amount, rate, base_currency, trade_type, market, | 419 | def __init__(self, action, amount, rate, base_currency, trade_type, market, |
496 | trade, close_if_possible=False): | 420 | trade, close_if_possible=False): |
@@ -507,6 +431,7 @@ class Order: | |||
507 | self.close_if_possible = close_if_possible | 431 | self.close_if_possible = close_if_possible |
508 | self.id = None | 432 | self.id = None |
509 | self.tries = 0 | 433 | self.tries = 0 |
434 | self.start_date = None | ||
510 | 435 | ||
511 | def as_json(self): | 436 | def as_json(self): |
512 | return { | 437 | return { |
@@ -552,18 +477,18 @@ class Order: | |||
552 | def finished(self): | 477 | def finished(self): |
553 | return self.status.startswith("closed") or self.status == "canceled" or self.status == "error" | 478 | return self.status.startswith("closed") or self.status == "canceled" or self.status == "error" |
554 | 479 | ||
555 | @retry(InsufficientFunds) | 480 | @retry((InsufficientFunds, RetryException)) |
556 | def run(self): | 481 | def run(self): |
557 | self.tries += 1 | 482 | self.tries += 1 |
558 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) | 483 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) |
559 | amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value | 484 | amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value |
560 | 485 | ||
486 | action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account) | ||
561 | if self.market.debug: | 487 | if self.market.debug: |
562 | self.market.report.log_debug_action("market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( | 488 | self.market.report.log_debug_action(action) |
563 | symbol, self.action, amount, self.rate, self.account)) | ||
564 | self.results.append({"debug": True, "id": -1}) | 489 | self.results.append({"debug": True, "id": -1}) |
565 | else: | 490 | else: |
566 | action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account) | 491 | self.start_date = datetime.now() |
567 | try: | 492 | try: |
568 | self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)) | 493 | self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)) |
569 | except InvalidOrder: | 494 | except InvalidOrder: |
@@ -571,6 +496,19 @@ class Order: | |||
571 | self.status = "closed" | 496 | self.status = "closed" |
572 | self.mark_finished_order() | 497 | self.mark_finished_order() |
573 | return | 498 | return |
499 | except RequestTimeout as e: | ||
500 | if not self.retrieve_order(): | ||
501 | if self.tries < 5: | ||
502 | self.market.report.log_error(action, message="Retrying after timeout", exception=e) | ||
503 | # We make a specific call in case retrieve_order | ||
504 | # would raise itself | ||
505 | raise RetryException | ||
506 | else: | ||
507 | self.market.report.log_error(action, message="Giving up {} after timeouts".format(self), exception=e) | ||
508 | self.status = "error" | ||
509 | return | ||
510 | else: | ||
511 | self.market.report.log_error(action, message="Timeout, found the order") | ||
574 | except InsufficientFunds as e: | 512 | except InsufficientFunds as e: |
575 | if self.tries < 5: | 513 | if self.tries < 5: |
576 | self.market.report.log_error(action, message="Retrying with reduced amount", exception=e) | 514 | self.market.report.log_error(action, message="Retrying with reduced amount", exception=e) |
@@ -662,6 +600,54 @@ class Order: | |||
662 | self.market.report.log_error("cancel_order", message="Already cancelled order", exception=e) | 600 | self.market.report.log_error("cancel_order", message="Already cancelled order", exception=e) |
663 | self.fetch() | 601 | self.fetch() |
664 | 602 | ||
603 | def retrieve_order(self): | ||
604 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) | ||
605 | amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value | ||
606 | start_timestamp = self.start_date.timestamp() - 5 | ||
607 | |||
608 | similar_open_orders = self.market.ccxt.fetch_orders(symbol=symbol, since=start_timestamp) | ||
609 | for order in similar_open_orders: | ||
610 | if (order["info"]["margin"] == 1 and self.account == "exchange") or\ | ||
611 | (order["info"]["margin"] != 1 and self.account == "margin"): | ||
612 | i_m_tested = True # coverage bug ?! | ||
613 | continue | ||
614 | if order["info"]["side"] != self.action: | ||
615 | continue | ||
616 | amount_diff = round( | ||
617 | abs(D(order["info"]["startingAmount"]) - amount), | ||
618 | self.market.ccxt.order_precision(symbol)) | ||
619 | rate_diff = round( | ||
620 | abs(D(order["info"]["rate"]) - self.rate), | ||
621 | self.market.ccxt.order_precision(symbol)) | ||
622 | if amount_diff != 0 or rate_diff != 0: | ||
623 | continue | ||
624 | self.results.append({"id": order["id"]}) | ||
625 | return True | ||
626 | |||
627 | similar_trades = self.market.ccxt.fetch_my_trades(symbol=symbol, since=start_timestamp) | ||
628 | # FIXME: use set instead of sorted(list(...)) | ||
629 | for order_id in sorted(list(map(lambda x: x["order"], similar_trades))): | ||
630 | trades = list(filter(lambda x: x["order"] == order_id, similar_trades)) | ||
631 | if any(x["timestamp"] < start_timestamp for x in trades): | ||
632 | continue | ||
633 | if any(x["side"] != self.action for x in trades): | ||
634 | continue | ||
635 | if any(x["info"]["category"] == "exchange" and self.account == "margin" for x in trades) or\ | ||
636 | any(x["info"]["category"] == "marginTrade" and self.account == "exchange" for x in trades): | ||
637 | continue | ||
638 | trade_sum = sum(D(x["info"]["amount"]) for x in trades) | ||
639 | amount_diff = round(abs(trade_sum - amount), | ||
640 | self.market.ccxt.order_precision(symbol)) | ||
641 | if amount_diff != 0: | ||
642 | continue | ||
643 | if (self.action == "sell" and any(D(x["info"]["rate"]) < self.rate for x in trades)) or\ | ||
644 | (self.action == "buy" and any(D(x["info"]["rate"]) > self.rate for x in trades)): | ||
645 | continue | ||
646 | self.results.append({"id": order_id}) | ||
647 | return True | ||
648 | |||
649 | return False | ||
650 | |||
665 | class Mouvement: | 651 | class Mouvement: |
666 | def __init__(self, currency, base_currency, hash_): | 652 | def __init__(self, currency, base_currency, hash_): |
667 | self.currency = currency | 653 | self.currency = currency |
@@ -1,20 +1,34 @@ | |||
1 | import time | ||
2 | import requests | ||
1 | import portfolio | 3 | import portfolio |
2 | import simplejson as json | 4 | import simplejson as json |
3 | from decimal import Decimal as D, ROUND_DOWN | 5 | from decimal import Decimal as D, ROUND_DOWN |
4 | from datetime import date, datetime | 6 | from datetime import date, datetime, timedelta |
5 | import inspect | 7 | import inspect |
8 | from json import JSONDecodeError | ||
9 | from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError | ||
6 | 10 | ||
7 | __all__ = ["BalanceStore", "ReportStore", "TradeStore"] | 11 | __all__ = ["Portfolio", "BalanceStore", "ReportStore", "TradeStore"] |
8 | 12 | ||
9 | class ReportStore: | 13 | class ReportStore: |
10 | def __init__(self, market, verbose_print=True): | 14 | def __init__(self, market, verbose_print=True): |
11 | self.market = market | 15 | self.market = market |
12 | self.verbose_print = verbose_print | 16 | self.verbose_print = verbose_print |
13 | 17 | ||
18 | self.print_logs = [] | ||
14 | self.logs = [] | 19 | self.logs = [] |
15 | 20 | ||
21 | def merge(self, other_report): | ||
22 | self.logs += other_report.logs | ||
23 | self.logs.sort(key=lambda x: x["date"]) | ||
24 | |||
25 | self.print_logs += other_report.print_logs | ||
26 | self.print_logs.sort(key=lambda x: x[0]) | ||
27 | |||
16 | def print_log(self, message): | 28 | def print_log(self, message): |
17 | message = str(message) | 29 | now = datetime.now() |
30 | message = "{:%Y-%m-%d %H:%M:%S}: {}".format(now, str(message)) | ||
31 | self.print_logs.append([now, message]) | ||
18 | if self.verbose_print: | 32 | if self.verbose_print: |
19 | print(message) | 33 | print(message) |
20 | 34 | ||
@@ -22,12 +36,22 @@ class ReportStore: | |||
22 | hash_["date"] = datetime.now() | 36 | hash_["date"] = datetime.now() |
23 | self.logs.append(hash_) | 37 | self.logs.append(hash_) |
24 | 38 | ||
39 | @staticmethod | ||
40 | def default_json_serial(obj): | ||
41 | if isinstance(obj, (datetime, date)): | ||
42 | return obj.isoformat() | ||
43 | return str(obj) | ||
44 | |||
25 | def to_json(self): | 45 | def to_json(self): |
26 | def default_json_serial(obj): | 46 | return json.dumps(self.logs, default=self.default_json_serial, indent=" ") |
27 | if isinstance(obj, (datetime, date)): | 47 | |
28 | return obj.isoformat() | 48 | def to_json_array(self): |
29 | return str(obj) | 49 | for log in (x.copy() for x in self.logs): |
30 | return json.dumps(self.logs, default=default_json_serial, indent=" ") | 50 | yield ( |
51 | log.pop("date"), | ||
52 | log.pop("type"), | ||
53 | json.dumps(log, default=self.default_json_serial, indent=" ") | ||
54 | ) | ||
31 | 55 | ||
32 | def set_verbose(self, verbose_print): | 56 | def set_verbose(self, verbose_print): |
33 | self.verbose_print = verbose_print | 57 | self.verbose_print = verbose_print |
@@ -213,7 +237,7 @@ class BalanceStore: | |||
213 | 237 | ||
214 | def dispatch_assets(self, amount, liquidity="medium", repartition=None): | 238 | def dispatch_assets(self, amount, liquidity="medium", repartition=None): |
215 | if repartition is None: | 239 | if repartition is None: |
216 | repartition = portfolio.Portfolio.repartition(self.market, liquidity=liquidity) | 240 | repartition = Portfolio.repartition(liquidity=liquidity) |
217 | sum_ratio = sum([v[0] for k, v in repartition.items()]) | 241 | sum_ratio = sum([v[0] for k, v in repartition.items()]) |
218 | amounts = {} | 242 | amounts = {} |
219 | for currency, (ptt, trade_type) in repartition.items(): | 243 | for currency, (ptt, trade_type) in repartition.items(): |
@@ -301,4 +325,165 @@ class TradeStore: | |||
301 | for order in self.all_orders(state="open"): | 325 | for order in self.all_orders(state="open"): |
302 | order.get_status() | 326 | order.get_status() |
303 | 327 | ||
328 | class NoopLock: | ||
329 | def __enter__(self, *args): | ||
330 | pass | ||
331 | def __exit__(self, *args): | ||
332 | pass | ||
333 | |||
334 | class LockedVar: | ||
335 | def __init__(self, value): | ||
336 | self.lock = NoopLock() | ||
337 | self.val = value | ||
338 | |||
339 | def start_lock(self): | ||
340 | import threading | ||
341 | self.lock = threading.Lock() | ||
342 | |||
343 | def set(self, value): | ||
344 | with self.lock: | ||
345 | self.val = value | ||
346 | |||
347 | def get(self, key=None): | ||
348 | with self.lock: | ||
349 | if key is not None and isinstance(self.val, dict): | ||
350 | return self.val.get(key) | ||
351 | else: | ||
352 | return self.val | ||
353 | |||
354 | def __getattr__(self, key): | ||
355 | with self.lock: | ||
356 | return getattr(self.val, key) | ||
357 | |||
358 | class Portfolio: | ||
359 | URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" | ||
360 | data = LockedVar(None) | ||
361 | liquidities = LockedVar({}) | ||
362 | last_date = LockedVar(None) | ||
363 | report = LockedVar(ReportStore(None)) | ||
364 | worker = None | ||
365 | worker_started = False | ||
366 | worker_notify = None | ||
367 | callback = None | ||
368 | |||
369 | @classmethod | ||
370 | def start_worker(cls, poll=30): | ||
371 | import threading | ||
372 | |||
373 | cls.worker = threading.Thread(name="portfolio", daemon=True, | ||
374 | target=cls.wait_for_notification, kwargs={"poll": poll}) | ||
375 | cls.worker_notify = threading.Event() | ||
376 | cls.callback = threading.Event() | ||
377 | |||
378 | cls.last_date.start_lock() | ||
379 | cls.liquidities.start_lock() | ||
380 | cls.report.start_lock() | ||
381 | |||
382 | cls.worker_started = True | ||
383 | cls.worker.start() | ||
384 | |||
385 | @classmethod | ||
386 | def is_worker_thread(cls): | ||
387 | if cls.worker is None: | ||
388 | return False | ||
389 | else: | ||
390 | import threading | ||
391 | return cls.worker == threading.current_thread() | ||
392 | |||
393 | @classmethod | ||
394 | def wait_for_notification(cls, poll=30): | ||
395 | if not cls.is_worker_thread(): | ||
396 | raise RuntimeError("This method needs to be ran with the worker") | ||
397 | while cls.worker_started: | ||
398 | cls.worker_notify.wait() | ||
399 | cls.worker_notify.clear() | ||
400 | cls.report.print_log("Fetching cryptoportfolio") | ||
401 | cls.get_cryptoportfolio(refetch=True) | ||
402 | cls.callback.set() | ||
403 | time.sleep(poll) | ||
404 | |||
405 | @classmethod | ||
406 | def notify_and_wait(cls): | ||
407 | cls.callback.clear() | ||
408 | cls.worker_notify.set() | ||
409 | cls.callback.wait() | ||
410 | |||
411 | @classmethod | ||
412 | def wait_for_recent(cls, delta=4, poll=30): | ||
413 | cls.get_cryptoportfolio() | ||
414 | while cls.last_date.get() is None or datetime.now() - cls.last_date.get() > timedelta(delta): | ||
415 | if cls.worker is None: | ||
416 | time.sleep(poll) | ||
417 | cls.report.print_log("Attempt to fetch up-to-date cryptoportfolio") | ||
418 | cls.get_cryptoportfolio(refetch=True) | ||
419 | |||
420 | @classmethod | ||
421 | def repartition(cls, liquidity="medium"): | ||
422 | cls.get_cryptoportfolio() | ||
423 | liquidities = cls.liquidities.get(liquidity) | ||
424 | return liquidities[cls.last_date.get()] | ||
425 | |||
426 | @classmethod | ||
427 | def get_cryptoportfolio(cls, refetch=False): | ||
428 | if cls.data.get() is not None and not refetch: | ||
429 | return | ||
430 | if cls.worker is not None and not cls.is_worker_thread(): | ||
431 | cls.notify_and_wait() | ||
432 | return | ||
433 | try: | ||
434 | r = requests.get(cls.URL) | ||
435 | cls.report.log_http_request(r.request.method, | ||
436 | r.request.url, r.request.body, r.request.headers, r) | ||
437 | except Exception as e: | ||
438 | cls.report.log_error("get_cryptoportfolio", exception=e) | ||
439 | return | ||
440 | try: | ||
441 | cls.data.set(r.json(parse_int=D, parse_float=D)) | ||
442 | cls.parse_cryptoportfolio() | ||
443 | except (JSONDecodeError, SimpleJSONDecodeError): | ||
444 | cls.data.set(None) | ||
445 | cls.last_date.set(None) | ||
446 | cls.liquidities.set({}) | ||
447 | |||
448 | @classmethod | ||
449 | def parse_cryptoportfolio(cls): | ||
450 | def filter_weights(weight_hash): | ||
451 | if weight_hash[1][0] == 0: | ||
452 | return False | ||
453 | if weight_hash[0] == "_row": | ||
454 | return False | ||
455 | return True | ||
456 | |||
457 | def clean_weights(i): | ||
458 | def clean_weights_(h): | ||
459 | if h[0].endswith("s"): | ||
460 | return [h[0][0:-1], (h[1][i], "short")] | ||
461 | else: | ||
462 | return [h[0], (h[1][i], "long")] | ||
463 | return clean_weights_ | ||
464 | |||
465 | def parse_weights(portfolio_hash): | ||
466 | if "weights" not in portfolio_hash: | ||
467 | return {} | ||
468 | weights_hash = portfolio_hash["weights"] | ||
469 | weights = {} | ||
470 | for i in range(len(weights_hash["_row"])): | ||
471 | date = datetime.strptime(weights_hash["_row"][i], "%Y-%m-%d") | ||
472 | weights[date] = dict(filter( | ||
473 | filter_weights, | ||
474 | map(clean_weights(i), weights_hash.items()))) | ||
475 | return weights | ||
476 | |||
477 | high_liquidity = parse_weights(cls.data.get("portfolio_1")) | ||
478 | medium_liquidity = parse_weights(cls.data.get("portfolio_2")) | ||
479 | |||
480 | cls.liquidities.set({ | ||
481 | "medium": medium_liquidity, | ||
482 | "high": high_liquidity, | ||
483 | }) | ||
484 | cls.last_date.set(max( | ||
485 | max(medium_liquidity.keys(), default=datetime(1, 1, 1)), | ||
486 | max(high_liquidity.keys(), default=datetime(1, 1, 1)) | ||
487 | )) | ||
488 | |||
304 | 489 | ||
diff --git a/tasks/import_reports_to_database.py b/tasks/import_reports_to_database.py index 56aefa5..6031cbe 100644 --- a/tasks/import_reports_to_database.py +++ b/tasks/import_reports_to_database.py | |||
@@ -6,12 +6,12 @@ from decimal import Decimal as D | |||
6 | import psycopg2 | 6 | import psycopg2 |
7 | 7 | ||
8 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | 8 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
9 | from helper import main_parse_config | 9 | from main import parse_config |
10 | 10 | ||
11 | config = sys.argv[1] | 11 | config = sys.argv[1] |
12 | reports = sys.argv[2:] | 12 | reports = sys.argv[2:] |
13 | 13 | ||
14 | pg_config, report_path = main_parse_config(config) | 14 | pg_config, report_path = parse_config(config) |
15 | 15 | ||
16 | connection = psycopg2.connect(**pg_config) | 16 | connection = psycopg2.connect(**pg_config) |
17 | cursor = connection.cursor() | 17 | cursor = connection.cursor() |
@@ -7,7 +7,8 @@ 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 threading |
11 | import portfolio, market, main, store | ||
11 | 12 | ||
12 | limits = ["acceptance", "unit"] | 13 | limits = ["acceptance", "unit"] |
13 | for test_type in limits: | 14 | for test_type in limits: |
@@ -22,6 +23,9 @@ for test_type in limits: | |||
22 | class WebMockTestCase(unittest.TestCase): | 23 | class WebMockTestCase(unittest.TestCase): |
23 | import time | 24 | import time |
24 | 25 | ||
26 | def market_args(self, debug=False, quiet=False): | ||
27 | return type('Args', (object,), { "debug": debug, "quiet": quiet })() | ||
28 | |||
25 | def setUp(self): | 29 | def setUp(self): |
26 | super(WebMockTestCase, self).setUp() | 30 | super(WebMockTestCase, self).setUp() |
27 | self.wm = requests_mock.Mocker() | 31 | self.wm = requests_mock.Mocker() |
@@ -32,7 +36,15 @@ class WebMockTestCase(unittest.TestCase): | |||
32 | self.m.debug = False | 36 | self.m.debug = False |
33 | 37 | ||
34 | self.patchers = [ | 38 | self.patchers = [ |
35 | mock.patch.multiple(portfolio.Portfolio, last_date=None, data=None, liquidities={}), | 39 | mock.patch.multiple(market.Portfolio, |
40 | data=store.LockedVar(None), | ||
41 | liquidities=store.LockedVar({}), | ||
42 | last_date=store.LockedVar(None), | ||
43 | report=mock.Mock(), | ||
44 | worker=None, | ||
45 | worker_notify=None, | ||
46 | worker_started=False, | ||
47 | callback=None), | ||
36 | mock.patch.multiple(portfolio.Computation, | 48 | mock.patch.multiple(portfolio.Computation, |
37 | computations=portfolio.Computation.computations), | 49 | computations=portfolio.Computation.computations), |
38 | ] | 50 | ] |
@@ -58,6 +70,18 @@ class poloniexETest(unittest.TestCase): | |||
58 | self.wm.stop() | 70 | self.wm.stop() |
59 | super(poloniexETest, self).tearDown() | 71 | super(poloniexETest, self).tearDown() |
60 | 72 | ||
73 | def test__init(self): | ||
74 | with mock.patch("market.ccxt.poloniexE.session") as session: | ||
75 | session.request.return_value = "response" | ||
76 | ccxt = market.ccxt.poloniexE() | ||
77 | ccxt._market = mock.Mock | ||
78 | ccxt._market.report = mock.Mock() | ||
79 | |||
80 | ccxt.session.request("GET", "URL", data="data", | ||
81 | headers="headers") | ||
82 | ccxt._market.report.log_http_request.assert_called_with('GET', 'URL', 'data', | ||
83 | 'headers', 'response') | ||
84 | |||
61 | def test_nanoseconds(self): | 85 | def test_nanoseconds(self): |
62 | with mock.patch.object(market.ccxt.time, "time") as time: | 86 | with mock.patch.object(market.ccxt.time, "time") as time: |
63 | time.return_value = 123456.7890123456 | 87 | time.return_value = 123456.7890123456 |
@@ -68,6 +92,58 @@ class poloniexETest(unittest.TestCase): | |||
68 | time.return_value = 123456.7890123456 | 92 | time.return_value = 123456.7890123456 |
69 | self.assertEqual(123456789012345, self.s.nonce()) | 93 | self.assertEqual(123456789012345, self.s.nonce()) |
70 | 94 | ||
95 | def test_request(self): | ||
96 | with mock.patch.object(market.ccxt.poloniex, "request") as request,\ | ||
97 | mock.patch("market.ccxt.retry_call") as retry_call: | ||
98 | with self.subTest(wrapped=True): | ||
99 | with self.subTest(desc="public"): | ||
100 | self.s.request("foo") | ||
101 | retry_call.assert_called_with(request, | ||
102 | delay=1, tries=10, fargs=["foo"], | ||
103 | fkwargs={'api': 'public', 'method': 'GET', 'params': {}, 'headers': None, 'body': None}, | ||
104 | exceptions=(market.ccxt.RequestTimeout,)) | ||
105 | request.assert_not_called() | ||
106 | |||
107 | with self.subTest(desc="private GET"): | ||
108 | self.s.request("foo", api="private") | ||
109 | retry_call.assert_called_with(request, | ||
110 | delay=1, tries=10, fargs=["foo"], | ||
111 | fkwargs={'api': 'private', 'method': 'GET', 'params': {}, 'headers': None, 'body': None}, | ||
112 | exceptions=(market.ccxt.RequestTimeout,)) | ||
113 | request.assert_not_called() | ||
114 | |||
115 | with self.subTest(desc="private POST regexp"): | ||
116 | self.s.request("returnFoo", api="private", method="POST") | ||
117 | retry_call.assert_called_with(request, | ||
118 | delay=1, tries=10, fargs=["returnFoo"], | ||
119 | fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None}, | ||
120 | exceptions=(market.ccxt.RequestTimeout,)) | ||
121 | request.assert_not_called() | ||
122 | |||
123 | with self.subTest(desc="private POST non-regexp"): | ||
124 | self.s.request("getMarginPosition", api="private", method="POST") | ||
125 | retry_call.assert_called_with(request, | ||
126 | delay=1, tries=10, fargs=["getMarginPosition"], | ||
127 | fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None}, | ||
128 | exceptions=(market.ccxt.RequestTimeout,)) | ||
129 | request.assert_not_called() | ||
130 | retry_call.reset_mock() | ||
131 | request.reset_mock() | ||
132 | with self.subTest(wrapped=False): | ||
133 | with self.subTest(desc="private POST non-matching regexp"): | ||
134 | self.s.request("marginBuy", api="private", method="POST") | ||
135 | request.assert_called_with("marginBuy", | ||
136 | api="private", method="POST", params={}, | ||
137 | headers=None, body=None) | ||
138 | retry_call.assert_not_called() | ||
139 | |||
140 | with self.subTest(desc="private POST non-matching non-regexp"): | ||
141 | self.s.request("closeMarginPositionOther", api="private", method="POST") | ||
142 | request.assert_called_with("closeMarginPositionOther", | ||
143 | api="private", method="POST", params={}, | ||
144 | headers=None, body=None) | ||
145 | retry_call.assert_not_called() | ||
146 | |||
71 | def test_order_precision(self): | 147 | def test_order_precision(self): |
72 | self.assertEqual(8, self.s.order_precision("FOO")) | 148 | self.assertEqual(8, self.s.order_precision("FOO")) |
73 | 149 | ||
@@ -126,174 +202,632 @@ class poloniexETest(unittest.TestCase): | |||
126 | } | 202 | } |
127 | self.assertEqual(expected, self.s.margin_summary()) | 203 | self.assertEqual(expected, self.s.margin_summary()) |
128 | 204 | ||
205 | def test_create_order(self): | ||
206 | with mock.patch.object(self.s, "create_exchange_order") as exchange,\ | ||
207 | mock.patch.object(self.s, "create_margin_order") as margin: | ||
208 | with self.subTest(account="unspecified"): | ||
209 | self.s.create_order("symbol", "type", "side", "amount", price="price", lending_rate="lending_rate", params="params") | ||
210 | exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | ||
211 | margin.assert_not_called() | ||
212 | exchange.reset_mock() | ||
213 | margin.reset_mock() | ||
214 | |||
215 | with self.subTest(account="exchange"): | ||
216 | self.s.create_order("symbol", "type", "side", "amount", account="exchange", price="price", lending_rate="lending_rate", params="params") | ||
217 | exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | ||
218 | margin.assert_not_called() | ||
219 | exchange.reset_mock() | ||
220 | margin.reset_mock() | ||
221 | |||
222 | with self.subTest(account="margin"): | ||
223 | self.s.create_order("symbol", "type", "side", "amount", account="margin", price="price", lending_rate="lending_rate", params="params") | ||
224 | margin.assert_called_once_with("symbol", "type", "side", "amount", lending_rate="lending_rate", price="price", params="params") | ||
225 | exchange.assert_not_called() | ||
226 | exchange.reset_mock() | ||
227 | margin.reset_mock() | ||
228 | |||
229 | with self.subTest(account="unknown"), self.assertRaises(NotImplementedError): | ||
230 | self.s.create_order("symbol", "type", "side", "amount", account="unknown") | ||
231 | |||
232 | def test_parse_ticker(self): | ||
233 | ticker = { | ||
234 | "high24hr": "12", | ||
235 | "low24hr": "10", | ||
236 | "highestBid": "10.5", | ||
237 | "lowestAsk": "11.5", | ||
238 | "last": "11", | ||
239 | "percentChange": "0.1", | ||
240 | "quoteVolume": "10", | ||
241 | "baseVolume": "20" | ||
242 | } | ||
243 | market = { | ||
244 | "symbol": "BTC/ETC" | ||
245 | } | ||
246 | with mock.patch.object(self.s, "milliseconds") as ms: | ||
247 | ms.return_value = 1520292715123 | ||
248 | result = self.s.parse_ticker(ticker, market) | ||
249 | |||
250 | expected = { | ||
251 | "symbol": "BTC/ETC", | ||
252 | "timestamp": 1520292715123, | ||
253 | "datetime": "2018-03-05T23:31:55.123Z", | ||
254 | "high": D("12"), | ||
255 | "low": D("10"), | ||
256 | "bid": D("10.5"), | ||
257 | "ask": D("11.5"), | ||
258 | "vwap": None, | ||
259 | "open": None, | ||
260 | "close": None, | ||
261 | "first": None, | ||
262 | "last": D("11"), | ||
263 | "change": D("0.1"), | ||
264 | "percentage": None, | ||
265 | "average": None, | ||
266 | "baseVolume": D("10"), | ||
267 | "quoteVolume": D("20"), | ||
268 | "info": ticker | ||
269 | } | ||
270 | self.assertEqual(expected, result) | ||
271 | |||
272 | def test_fetch_margin_balance(self): | ||
273 | with mock.patch.object(self.s, "privatePostGetMarginPosition") as get_margin_position: | ||
274 | get_margin_position.return_value = { | ||
275 | "BTC_DASH": { | ||
276 | "amount": "-0.1", | ||
277 | "basePrice": "0.06818560", | ||
278 | "lendingFees": "0.00000001", | ||
279 | "liquidationPrice": "0.15107132", | ||
280 | "pl": "-0.00000371", | ||
281 | "total": "0.00681856", | ||
282 | "type": "short" | ||
283 | }, | ||
284 | "BTC_ETC": { | ||
285 | "amount": "-0.6", | ||
286 | "basePrice": "0.1", | ||
287 | "lendingFees": "0.00000001", | ||
288 | "liquidationPrice": "0.6", | ||
289 | "pl": "0.00000371", | ||
290 | "total": "0.06", | ||
291 | "type": "short" | ||
292 | }, | ||
293 | "BTC_ETH": { | ||
294 | "amount": "0", | ||
295 | "basePrice": "0", | ||
296 | "lendingFees": "0", | ||
297 | "liquidationPrice": "-1", | ||
298 | "pl": "0", | ||
299 | "total": "0", | ||
300 | "type": "none" | ||
301 | } | ||
302 | } | ||
303 | balances = self.s.fetch_margin_balance() | ||
304 | self.assertEqual(2, len(balances)) | ||
305 | expected = { | ||
306 | "DASH": { | ||
307 | "amount": D("-0.1"), | ||
308 | "borrowedPrice": D("0.06818560"), | ||
309 | "lendingFees": D("1E-8"), | ||
310 | "pl": D("-0.00000371"), | ||
311 | "liquidationPrice": D("0.15107132"), | ||
312 | "type": "short", | ||
313 | "total": D("0.00681856"), | ||
314 | "baseCurrency": "BTC" | ||
315 | }, | ||
316 | "ETC": { | ||
317 | "amount": D("-0.6"), | ||
318 | "borrowedPrice": D("0.1"), | ||
319 | "lendingFees": D("1E-8"), | ||
320 | "pl": D("0.00000371"), | ||
321 | "liquidationPrice": D("0.6"), | ||
322 | "type": "short", | ||
323 | "total": D("0.06"), | ||
324 | "baseCurrency": "BTC" | ||
325 | } | ||
326 | } | ||
327 | self.assertEqual(expected, balances) | ||
328 | |||
329 | def test_sum(self): | ||
330 | self.assertEqual(D("1.1"), self.s.sum(D("1"), D("0.1"))) | ||
331 | |||
332 | def test_fetch_balance(self): | ||
333 | with mock.patch.object(self.s, "load_markets") as load_markets,\ | ||
334 | mock.patch.object(self.s, "privatePostReturnCompleteBalances") as balances,\ | ||
335 | mock.patch.object(self.s, "common_currency_code") as ccc: | ||
336 | ccc.side_effect = ["ETH", "BTC", "DASH"] | ||
337 | balances.return_value = { | ||
338 | "ETH": { | ||
339 | "available": "10", | ||
340 | "onOrders": "1", | ||
341 | }, | ||
342 | "BTC": { | ||
343 | "available": "1", | ||
344 | "onOrders": "0", | ||
345 | }, | ||
346 | "DASH": { | ||
347 | "available": "0", | ||
348 | "onOrders": "3" | ||
349 | } | ||
350 | } | ||
351 | |||
352 | expected = { | ||
353 | "info": { | ||
354 | "ETH": {"available": "10", "onOrders": "1"}, | ||
355 | "BTC": {"available": "1", "onOrders": "0"}, | ||
356 | "DASH": {"available": "0", "onOrders": "3"} | ||
357 | }, | ||
358 | "ETH": {"free": D("10"), "used": D("1"), "total": D("11")}, | ||
359 | "BTC": {"free": D("1"), "used": D("0"), "total": D("1")}, | ||
360 | "DASH": {"free": D("0"), "used": D("3"), "total": D("3")}, | ||
361 | "free": {"ETH": D("10"), "BTC": D("1"), "DASH": D("0")}, | ||
362 | "used": {"ETH": D("1"), "BTC": D("0"), "DASH": D("3")}, | ||
363 | "total": {"ETH": D("11"), "BTC": D("1"), "DASH": D("3")} | ||
364 | } | ||
365 | result = self.s.fetch_balance() | ||
366 | load_markets.assert_called_once() | ||
367 | self.assertEqual(expected, result) | ||
368 | |||
369 | def test_fetch_balance_per_type(self): | ||
370 | with mock.patch.object(self.s, "privatePostReturnAvailableAccountBalances") as balances: | ||
371 | balances.return_value = { | ||
372 | "exchange": { | ||
373 | "BLK": "159.83673869", | ||
374 | "BTC": "0.00005959", | ||
375 | "USDT": "0.00002625", | ||
376 | "XMR": "0.18719303" | ||
377 | }, | ||
378 | "margin": { | ||
379 | "BTC": "0.03019227" | ||
380 | } | ||
381 | } | ||
382 | expected = { | ||
383 | "info": { | ||
384 | "exchange": { | ||
385 | "BLK": "159.83673869", | ||
386 | "BTC": "0.00005959", | ||
387 | "USDT": "0.00002625", | ||
388 | "XMR": "0.18719303" | ||
389 | }, | ||
390 | "margin": { | ||
391 | "BTC": "0.03019227" | ||
392 | } | ||
393 | }, | ||
394 | "exchange": { | ||
395 | "BLK": D("159.83673869"), | ||
396 | "BTC": D("0.00005959"), | ||
397 | "USDT": D("0.00002625"), | ||
398 | "XMR": D("0.18719303") | ||
399 | }, | ||
400 | "margin": {"BTC": D("0.03019227")}, | ||
401 | "BLK": {"exchange": D("159.83673869")}, | ||
402 | "BTC": {"exchange": D("0.00005959"), "margin": D("0.03019227")}, | ||
403 | "USDT": {"exchange": D("0.00002625")}, | ||
404 | "XMR": {"exchange": D("0.18719303")} | ||
405 | } | ||
406 | result = self.s.fetch_balance_per_type() | ||
407 | self.assertEqual(expected, result) | ||
408 | |||
409 | def test_fetch_all_balances(self): | ||
410 | import json | ||
411 | with mock.patch.object(self.s, "load_markets") as load_markets,\ | ||
412 | mock.patch.object(self.s, "privatePostGetMarginPosition") as margin_balance,\ | ||
413 | mock.patch.object(self.s, "privatePostReturnCompleteBalances") as balance,\ | ||
414 | mock.patch.object(self.s, "privatePostReturnAvailableAccountBalances") as balance_per_type: | ||
415 | |||
416 | with open("test_samples/poloniexETest.test_fetch_all_balances.1.json") as f: | ||
417 | balance.return_value = json.load(f) | ||
418 | with open("test_samples/poloniexETest.test_fetch_all_balances.2.json") as f: | ||
419 | margin_balance.return_value = json.load(f) | ||
420 | with open("test_samples/poloniexETest.test_fetch_all_balances.3.json") as f: | ||
421 | balance_per_type.return_value = json.load(f) | ||
422 | |||
423 | result = self.s.fetch_all_balances() | ||
424 | expected_doge = { | ||
425 | "total": D("-12779.79821852"), | ||
426 | "exchange_used": D("0E-8"), | ||
427 | "exchange_total": D("0E-8"), | ||
428 | "exchange_free": D("0E-8"), | ||
429 | "margin_available": 0, | ||
430 | "margin_in_position": 0, | ||
431 | "margin_borrowed": D("12779.79821852"), | ||
432 | "margin_total": D("-12779.79821852"), | ||
433 | "margin_pending_gain": 0, | ||
434 | "margin_lending_fees": D("-9E-8"), | ||
435 | "margin_pending_base_gain": D("0.00024059"), | ||
436 | "margin_position_type": "short", | ||
437 | "margin_liquidation_price": D("0.00000246"), | ||
438 | "margin_borrowed_base_price": D("0.00599149"), | ||
439 | "margin_borrowed_base_currency": "BTC" | ||
440 | } | ||
441 | expected_btc = {"total": D("0.05432165"), | ||
442 | "exchange_used": D("0E-8"), | ||
443 | "exchange_total": D("0.00005959"), | ||
444 | "exchange_free": D("0.00005959"), | ||
445 | "margin_available": D("0.03019227"), | ||
446 | "margin_in_position": D("0.02406979"), | ||
447 | "margin_borrowed": 0, | ||
448 | "margin_total": D("0.05426206"), | ||
449 | "margin_pending_gain": D("0.00093955"), | ||
450 | "margin_lending_fees": 0, | ||
451 | "margin_pending_base_gain": 0, | ||
452 | "margin_position_type": None, | ||
453 | "margin_liquidation_price": 0, | ||
454 | "margin_borrowed_base_price": 0, | ||
455 | "margin_borrowed_base_currency": None | ||
456 | } | ||
457 | expected_xmr = {"total": D("0.18719303"), | ||
458 | "exchange_used": D("0E-8"), | ||
459 | "exchange_total": D("0.18719303"), | ||
460 | "exchange_free": D("0.18719303"), | ||
461 | "margin_available": 0, | ||
462 | "margin_in_position": 0, | ||
463 | "margin_borrowed": 0, | ||
464 | "margin_total": 0, | ||
465 | "margin_pending_gain": 0, | ||
466 | "margin_lending_fees": 0, | ||
467 | "margin_pending_base_gain": 0, | ||
468 | "margin_position_type": None, | ||
469 | "margin_liquidation_price": 0, | ||
470 | "margin_borrowed_base_price": 0, | ||
471 | "margin_borrowed_base_currency": None | ||
472 | } | ||
473 | self.assertEqual(expected_xmr, result["XMR"]) | ||
474 | self.assertEqual(expected_doge, result["DOGE"]) | ||
475 | self.assertEqual(expected_btc, result["BTC"]) | ||
476 | |||
477 | def test_create_margin_order(self): | ||
478 | with self.assertRaises(market.ExchangeError): | ||
479 | self.s.create_margin_order("FOO", "market", "buy", "10") | ||
480 | |||
481 | with mock.patch.object(self.s, "load_markets") as load_markets,\ | ||
482 | mock.patch.object(self.s, "privatePostMarginBuy") as margin_buy,\ | ||
483 | mock.patch.object(self.s, "privatePostMarginSell") as margin_sell,\ | ||
484 | mock.patch.object(self.s, "market") as market_mock,\ | ||
485 | mock.patch.object(self.s, "price_to_precision") as ptp,\ | ||
486 | mock.patch.object(self.s, "amount_to_precision") as atp: | ||
487 | |||
488 | margin_buy.return_value = { | ||
489 | "orderNumber": 123 | ||
490 | } | ||
491 | margin_sell.return_value = { | ||
492 | "orderNumber": 456 | ||
493 | } | ||
494 | market_mock.return_value = { "id": "BTC_ETC", "symbol": "BTC_ETC" } | ||
495 | ptp.return_value = D("0.1") | ||
496 | atp.return_value = D("12") | ||
497 | |||
498 | order = self.s.create_margin_order("BTC_ETC", "margin", "buy", "12", price="0.1") | ||
499 | self.assertEqual(123, order["id"]) | ||
500 | margin_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")}) | ||
501 | margin_sell.assert_not_called() | ||
502 | margin_buy.reset_mock() | ||
503 | margin_sell.reset_mock() | ||
504 | |||
505 | order = self.s.create_margin_order("BTC_ETC", "margin", "sell", "12", lending_rate="0.01", price="0.1") | ||
506 | self.assertEqual(456, order["id"]) | ||
507 | margin_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12"), "lendingRate": "0.01"}) | ||
508 | margin_buy.assert_not_called() | ||
509 | |||
510 | def test_create_exchange_order(self): | ||
511 | with mock.patch.object(market.ccxt.poloniex, "create_order") as create_order: | ||
512 | self.s.create_order("symbol", "type", "side", "amount", price="price", params="params") | ||
513 | |||
514 | create_order.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | ||
515 | |||
129 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 516 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
130 | class PortfolioTest(WebMockTestCase): | 517 | class NoopLockTest(unittest.TestCase): |
131 | def fill_data(self): | 518 | def test_with(self): |
132 | if self.json_response is not None: | 519 | noop_lock = store.NoopLock() |
133 | portfolio.Portfolio.data = self.json_response | 520 | with noop_lock: |
521 | self.assertTrue(True) | ||
134 | 522 | ||
523 | @unittest.skipUnless("unit" in limits, "Unit skipped") | ||
524 | class LockedVar(unittest.TestCase): | ||
525 | |||
526 | def test_values(self): | ||
527 | locked_var = store.LockedVar("Foo") | ||
528 | self.assertIsInstance(locked_var.lock, store.NoopLock) | ||
529 | self.assertEqual("Foo", locked_var.val) | ||
530 | |||
531 | def test_get(self): | ||
532 | with self.subTest(desc="Normal case"): | ||
533 | locked_var = store.LockedVar("Foo") | ||
534 | self.assertEqual("Foo", locked_var.get()) | ||
535 | with self.subTest(desc="Dict"): | ||
536 | locked_var = store.LockedVar({"foo": "bar"}) | ||
537 | self.assertEqual({"foo": "bar"}, locked_var.get()) | ||
538 | self.assertEqual("bar", locked_var.get("foo")) | ||
539 | self.assertIsNone(locked_var.get("other")) | ||
540 | |||
541 | def test_set(self): | ||
542 | locked_var = store.LockedVar("Foo") | ||
543 | locked_var.set("Bar") | ||
544 | self.assertEqual("Bar", locked_var.get()) | ||
545 | |||
546 | def test__getattr(self): | ||
547 | dummy = type('Dummy', (object,), {})() | ||
548 | dummy.attribute = "Hey" | ||
549 | |||
550 | locked_var = store.LockedVar(dummy) | ||
551 | self.assertEqual("Hey", locked_var.attribute) | ||
552 | with self.assertRaises(AttributeError): | ||
553 | locked_var.other | ||
554 | |||
555 | def test_start_lock(self): | ||
556 | locked_var = store.LockedVar("Foo") | ||
557 | locked_var.start_lock() | ||
558 | self.assertEqual("lock", locked_var.lock.__class__.__name__) | ||
559 | |||
560 | thread1 = threading.Thread(target=locked_var.set, args=["Bar1"]) | ||
561 | thread2 = threading.Thread(target=locked_var.set, args=["Bar2"]) | ||
562 | thread3 = threading.Thread(target=locked_var.set, args=["Bar3"]) | ||
563 | |||
564 | with locked_var.lock: | ||
565 | thread1.start() | ||
566 | thread2.start() | ||
567 | thread3.start() | ||
568 | |||
569 | self.assertEqual("Foo", locked_var.val) | ||
570 | thread1.join() | ||
571 | thread2.join() | ||
572 | thread3.join() | ||
573 | self.assertEqual("Bar", locked_var.get()[0:3]) | ||
574 | |||
575 | def test_wait_for_notification(self): | ||
576 | with self.assertRaises(RuntimeError): | ||
577 | store.Portfolio.wait_for_notification() | ||
578 | |||
579 | with mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\ | ||
580 | mock.patch.object(store.Portfolio, "report") as report,\ | ||
581 | mock.patch.object(store.time, "sleep") as sleep: | ||
582 | store.Portfolio.start_worker(poll=3) | ||
583 | |||
584 | store.Portfolio.worker_notify.set() | ||
585 | |||
586 | store.Portfolio.callback.wait() | ||
587 | |||
588 | report.print_log.assert_called_once_with("Fetching cryptoportfolio") | ||
589 | get.assert_called_once_with(refetch=True) | ||
590 | sleep.assert_called_once_with(3) | ||
591 | self.assertFalse(store.Portfolio.worker_notify.is_set()) | ||
592 | self.assertTrue(store.Portfolio.worker.is_alive()) | ||
593 | |||
594 | store.Portfolio.callback.clear() | ||
595 | store.Portfolio.worker_started = False | ||
596 | store.Portfolio.worker_notify.set() | ||
597 | store.Portfolio.callback.wait() | ||
598 | |||
599 | self.assertFalse(store.Portfolio.worker.is_alive()) | ||
600 | |||
601 | def test_notify_and_wait(self): | ||
602 | with mock.patch.object(store.Portfolio, "callback") as callback,\ | ||
603 | mock.patch.object(store.Portfolio, "worker_notify") as worker_notify: | ||
604 | store.Portfolio.notify_and_wait() | ||
605 | callback.clear.assert_called_once_with() | ||
606 | worker_notify.set.assert_called_once_with() | ||
607 | callback.wait.assert_called_once_with() | ||
608 | |||
609 | @unittest.skipUnless("unit" in limits, "Unit skipped") | ||
610 | class PortfolioTest(WebMockTestCase): | ||
135 | def setUp(self): | 611 | def setUp(self): |
136 | super(PortfolioTest, self).setUp() | 612 | super(PortfolioTest, self).setUp() |
137 | 613 | ||
138 | with open("test_portfolio.json") as example: | 614 | with open("test_samples/test_portfolio.json") as example: |
139 | self.json_response = example.read() | 615 | self.json_response = example.read() |
140 | 616 | ||
141 | self.wm.get(portfolio.Portfolio.URL, text=self.json_response) | 617 | self.wm.get(market.Portfolio.URL, text=self.json_response) |
142 | 618 | ||
143 | def test_get_cryptoportfolio(self): | 619 | @mock.patch.object(market.Portfolio, "parse_cryptoportfolio") |
144 | self.wm.get(portfolio.Portfolio.URL, [ | 620 | def test_get_cryptoportfolio(self, parse_cryptoportfolio): |
145 | {"text":'{ "foo": "bar" }', "status_code": 200}, | 621 | with self.subTest(parallel=False): |
146 | {"text": "System Error", "status_code": 500}, | 622 | self.wm.get(market.Portfolio.URL, [ |
147 | {"exc": requests.exceptions.ConnectTimeout}, | 623 | {"text":'{ "foo": "bar" }', "status_code": 200}, |
148 | ]) | 624 | {"text": "System Error", "status_code": 500}, |
149 | portfolio.Portfolio.get_cryptoportfolio(self.m) | 625 | {"exc": requests.exceptions.ConnectTimeout}, |
150 | self.assertIn("foo", portfolio.Portfolio.data) | 626 | ]) |
151 | self.assertEqual("bar", portfolio.Portfolio.data["foo"]) | 627 | market.Portfolio.get_cryptoportfolio() |
152 | self.assertTrue(self.wm.called) | 628 | self.assertIn("foo", market.Portfolio.data.get()) |
153 | self.assertEqual(1, self.wm.call_count) | 629 | self.assertEqual("bar", market.Portfolio.data.get()["foo"]) |
154 | self.m.report.log_error.assert_not_called() | 630 | self.assertTrue(self.wm.called) |
155 | self.m.report.log_http_request.assert_called_once() | 631 | self.assertEqual(1, self.wm.call_count) |
156 | self.m.report.log_http_request.reset_mock() | 632 | market.Portfolio.report.log_error.assert_not_called() |
157 | 633 | market.Portfolio.report.log_http_request.assert_called_once() | |
158 | portfolio.Portfolio.get_cryptoportfolio(self.m) | 634 | parse_cryptoportfolio.assert_called_once_with() |
159 | self.assertIsNone(portfolio.Portfolio.data) | 635 | market.Portfolio.report.log_http_request.reset_mock() |
160 | self.assertEqual(2, self.wm.call_count) | 636 | parse_cryptoportfolio.reset_mock() |
161 | self.m.report.log_error.assert_not_called() | 637 | market.Portfolio.data = store.LockedVar(None) |
162 | self.m.report.log_http_request.assert_called_once() | 638 | |
163 | self.m.report.log_http_request.reset_mock() | 639 | market.Portfolio.get_cryptoportfolio() |
164 | 640 | self.assertIsNone(market.Portfolio.data.get()) | |
165 | 641 | self.assertEqual(2, self.wm.call_count) | |
166 | portfolio.Portfolio.data = "Foo" | 642 | parse_cryptoportfolio.assert_not_called() |
167 | portfolio.Portfolio.get_cryptoportfolio(self.m) | 643 | market.Portfolio.report.log_error.assert_not_called() |
168 | self.assertEqual("Foo", portfolio.Portfolio.data) | 644 | market.Portfolio.report.log_http_request.assert_called_once() |
169 | self.assertEqual(3, self.wm.call_count) | 645 | market.Portfolio.report.log_http_request.reset_mock() |
170 | self.m.report.log_error.assert_called_once_with("get_cryptoportfolio", | 646 | parse_cryptoportfolio.reset_mock() |
171 | exception=mock.ANY) | 647 | |
172 | self.m.report.log_http_request.assert_not_called() | 648 | market.Portfolio.data = store.LockedVar("Foo") |
649 | market.Portfolio.get_cryptoportfolio() | ||
650 | self.assertEqual(2, self.wm.call_count) | ||
651 | parse_cryptoportfolio.assert_not_called() | ||
652 | |||
653 | market.Portfolio.get_cryptoportfolio(refetch=True) | ||
654 | self.assertEqual("Foo", market.Portfolio.data.get()) | ||
655 | self.assertEqual(3, self.wm.call_count) | ||
656 | market.Portfolio.report.log_error.assert_called_once_with("get_cryptoportfolio", | ||
657 | exception=mock.ANY) | ||
658 | market.Portfolio.report.log_http_request.assert_not_called() | ||
659 | with self.subTest(parallel=True): | ||
660 | with mock.patch.object(market.Portfolio, "is_worker_thread") as is_worker,\ | ||
661 | mock.patch.object(market.Portfolio, "notify_and_wait") as notify: | ||
662 | with self.subTest(worker=True): | ||
663 | market.Portfolio.data = store.LockedVar(None) | ||
664 | market.Portfolio.worker = mock.Mock() | ||
665 | is_worker.return_value = True | ||
666 | self.wm.get(market.Portfolio.URL, [ | ||
667 | {"text":'{ "foo": "bar" }', "status_code": 200}, | ||
668 | ]) | ||
669 | market.Portfolio.get_cryptoportfolio() | ||
670 | self.assertIn("foo", market.Portfolio.data.get()) | ||
671 | parse_cryptoportfolio.reset_mock() | ||
672 | with self.subTest(worker=False): | ||
673 | market.Portfolio.data = store.LockedVar(None) | ||
674 | market.Portfolio.worker = mock.Mock() | ||
675 | is_worker.return_value = False | ||
676 | market.Portfolio.get_cryptoportfolio() | ||
677 | notify.assert_called_once_with() | ||
678 | parse_cryptoportfolio.assert_not_called() | ||
173 | 679 | ||
174 | def test_parse_cryptoportfolio(self): | 680 | def test_parse_cryptoportfolio(self): |
175 | portfolio.Portfolio.parse_cryptoportfolio(self.m) | 681 | with self.subTest(description="Normal case"): |
176 | 682 | market.Portfolio.data = store.LockedVar(store.json.loads( | |
177 | self.assertListEqual( | 683 | self.json_response, parse_int=D, parse_float=D)) |
178 | ["medium", "high"], | 684 | market.Portfolio.parse_cryptoportfolio() |
179 | list(portfolio.Portfolio.liquidities.keys())) | ||
180 | |||
181 | liquidities = portfolio.Portfolio.liquidities | ||
182 | self.assertEqual(10, len(liquidities["medium"].keys())) | ||
183 | self.assertEqual(10, len(liquidities["high"].keys())) | ||
184 | |||
185 | expected = { | ||
186 | 'BTC': (D("0.2857"), "long"), | ||
187 | 'DGB': (D("0.1015"), "long"), | ||
188 | 'DOGE': (D("0.1805"), "long"), | ||
189 | 'SC': (D("0.0623"), "long"), | ||
190 | 'ZEC': (D("0.3701"), "long"), | ||
191 | } | ||
192 | date = portfolio.datetime(2018, 1, 8) | ||
193 | self.assertDictEqual(expected, liquidities["high"][date]) | ||
194 | |||
195 | expected = { | ||
196 | 'BTC': (D("1.1102e-16"), "long"), | ||
197 | 'ETC': (D("0.1"), "long"), | ||
198 | 'FCT': (D("0.1"), "long"), | ||
199 | 'GAS': (D("0.1"), "long"), | ||
200 | 'NAV': (D("0.1"), "long"), | ||
201 | 'OMG': (D("0.1"), "long"), | ||
202 | 'OMNI': (D("0.1"), "long"), | ||
203 | 'PPC': (D("0.1"), "long"), | ||
204 | 'RIC': (D("0.1"), "long"), | ||
205 | 'VIA': (D("0.1"), "long"), | ||
206 | 'XCP': (D("0.1"), "long"), | ||
207 | } | ||
208 | self.assertDictEqual(expected, liquidities["medium"][date]) | ||
209 | self.assertEqual(portfolio.datetime(2018, 1, 15), portfolio.Portfolio.last_date) | ||
210 | |||
211 | self.m.report.log_http_request.assert_called_once_with("GET", | ||
212 | portfolio.Portfolio.URL, None, mock.ANY, mock.ANY) | ||
213 | self.m.report.log_http_request.reset_mock() | ||
214 | |||
215 | # It doesn't refetch the data when available | ||
216 | portfolio.Portfolio.parse_cryptoportfolio(self.m) | ||
217 | self.m.report.log_http_request.assert_not_called() | ||
218 | |||
219 | self.assertEqual(1, self.wm.call_count) | ||
220 | |||
221 | portfolio.Portfolio.parse_cryptoportfolio(self.m, refetch=True) | ||
222 | self.assertEqual(2, self.wm.call_count) | ||
223 | self.m.report.log_http_request.assert_called_once() | ||
224 | |||
225 | def test_repartition(self): | ||
226 | expected_medium = { | ||
227 | 'BTC': (D("1.1102e-16"), "long"), | ||
228 | 'USDT': (D("0.1"), "long"), | ||
229 | 'ETC': (D("0.1"), "long"), | ||
230 | 'FCT': (D("0.1"), "long"), | ||
231 | 'OMG': (D("0.1"), "long"), | ||
232 | 'STEEM': (D("0.1"), "long"), | ||
233 | 'STRAT': (D("0.1"), "long"), | ||
234 | 'XEM': (D("0.1"), "long"), | ||
235 | 'XMR': (D("0.1"), "long"), | ||
236 | 'XVC': (D("0.1"), "long"), | ||
237 | 'ZRX': (D("0.1"), "long"), | ||
238 | } | ||
239 | expected_high = { | ||
240 | 'USDT': (D("0.1226"), "long"), | ||
241 | 'BTC': (D("0.1429"), "long"), | ||
242 | 'ETC': (D("0.1127"), "long"), | ||
243 | 'ETH': (D("0.1569"), "long"), | ||
244 | 'FCT': (D("0.3341"), "long"), | ||
245 | 'GAS': (D("0.1308"), "long"), | ||
246 | } | ||
247 | 685 | ||
248 | self.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m)) | 686 | self.assertListEqual( |
249 | self.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m, liquidity="medium")) | 687 | ["medium", "high"], |
250 | self.assertEqual(expected_high, portfolio.Portfolio.repartition(self.m, liquidity="high")) | 688 | list(market.Portfolio.liquidities.get().keys())) |
251 | 689 | ||
252 | self.assertEqual(1, self.wm.call_count) | 690 | liquidities = market.Portfolio.liquidities.get() |
691 | self.assertEqual(10, len(liquidities["medium"].keys())) | ||
692 | self.assertEqual(10, len(liquidities["high"].keys())) | ||
253 | 693 | ||
254 | portfolio.Portfolio.repartition(self.m) | 694 | expected = { |
255 | self.assertEqual(1, self.wm.call_count) | 695 | 'BTC': (D("0.2857"), "long"), |
696 | 'DGB': (D("0.1015"), "long"), | ||
697 | 'DOGE': (D("0.1805"), "long"), | ||
698 | 'SC': (D("0.0623"), "long"), | ||
699 | 'ZEC': (D("0.3701"), "long"), | ||
700 | } | ||
701 | date = portfolio.datetime(2018, 1, 8) | ||
702 | self.assertDictEqual(expected, liquidities["high"][date]) | ||
256 | 703 | ||
257 | portfolio.Portfolio.repartition(self.m, refetch=True) | 704 | expected = { |
258 | self.assertEqual(2, self.wm.call_count) | 705 | 'BTC': (D("1.1102e-16"), "long"), |
259 | self.m.report.log_http_request.assert_called() | 706 | 'ETC': (D("0.1"), "long"), |
260 | self.assertEqual(2, self.m.report.log_http_request.call_count) | 707 | 'FCT': (D("0.1"), "long"), |
708 | 'GAS': (D("0.1"), "long"), | ||
709 | 'NAV': (D("0.1"), "long"), | ||
710 | 'OMG': (D("0.1"), "long"), | ||
711 | 'OMNI': (D("0.1"), "long"), | ||
712 | 'PPC': (D("0.1"), "long"), | ||
713 | 'RIC': (D("0.1"), "long"), | ||
714 | 'VIA': (D("0.1"), "long"), | ||
715 | 'XCP': (D("0.1"), "long"), | ||
716 | } | ||
717 | self.assertDictEqual(expected, liquidities["medium"][date]) | ||
718 | self.assertEqual(portfolio.datetime(2018, 1, 15), market.Portfolio.last_date.get()) | ||
719 | |||
720 | with self.subTest(description="Missing weight"): | ||
721 | data = store.json.loads(self.json_response, parse_int=D, parse_float=D) | ||
722 | del(data["portfolio_2"]["weights"]) | ||
723 | market.Portfolio.data = store.LockedVar(data) | ||
724 | |||
725 | market.Portfolio.parse_cryptoportfolio() | ||
726 | self.assertListEqual( | ||
727 | ["medium", "high"], | ||
728 | list(market.Portfolio.liquidities.get().keys())) | ||
729 | self.assertEqual({}, market.Portfolio.liquidities.get("medium")) | ||
730 | |||
731 | with self.subTest(description="All missing weights"): | ||
732 | data = store.json.loads(self.json_response, parse_int=D, parse_float=D) | ||
733 | del(data["portfolio_1"]["weights"]) | ||
734 | del(data["portfolio_2"]["weights"]) | ||
735 | market.Portfolio.data = store.LockedVar(data) | ||
736 | |||
737 | market.Portfolio.parse_cryptoportfolio() | ||
738 | self.assertEqual({}, market.Portfolio.liquidities.get("medium")) | ||
739 | self.assertEqual({}, market.Portfolio.liquidities.get("high")) | ||
740 | self.assertEqual(datetime.datetime(1,1,1), market.Portfolio.last_date.get()) | ||
741 | |||
742 | |||
743 | @mock.patch.object(market.Portfolio, "get_cryptoportfolio") | ||
744 | def test_repartition(self, get_cryptoportfolio): | ||
745 | market.Portfolio.liquidities = store.LockedVar({ | ||
746 | "medium": { | ||
747 | "2018-03-01": "medium_2018-03-01", | ||
748 | "2018-03-08": "medium_2018-03-08", | ||
749 | }, | ||
750 | "high": { | ||
751 | "2018-03-01": "high_2018-03-01", | ||
752 | "2018-03-08": "high_2018-03-08", | ||
753 | } | ||
754 | }) | ||
755 | market.Portfolio.last_date = store.LockedVar("2018-03-08") | ||
756 | |||
757 | self.assertEqual("medium_2018-03-08", market.Portfolio.repartition()) | ||
758 | get_cryptoportfolio.assert_called_once_with() | ||
759 | self.assertEqual("medium_2018-03-08", market.Portfolio.repartition(liquidity="medium")) | ||
760 | self.assertEqual("high_2018-03-08", market.Portfolio.repartition(liquidity="high")) | ||
261 | 761 | ||
262 | @mock.patch.object(portfolio.time, "sleep") | 762 | @mock.patch.object(market.time, "sleep") |
263 | @mock.patch.object(portfolio.Portfolio, "repartition") | 763 | @mock.patch.object(market.Portfolio, "get_cryptoportfolio") |
264 | def test_wait_for_recent(self, repartition, sleep): | 764 | def test_wait_for_recent(self, get_cryptoportfolio, sleep): |
265 | self.call_count = 0 | 765 | self.call_count = 0 |
266 | def _repartition(market, refetch): | 766 | def _get(refetch=False): |
267 | self.assertEqual(self.m, market) | 767 | if self.call_count != 0: |
268 | self.assertTrue(refetch) | 768 | self.assertTrue(refetch) |
769 | else: | ||
770 | self.assertFalse(refetch) | ||
269 | self.call_count += 1 | 771 | self.call_count += 1 |
270 | portfolio.Portfolio.last_date = portfolio.datetime.now()\ | 772 | market.Portfolio.last_date = store.LockedVar(store.datetime.now()\ |
271 | - portfolio.timedelta(10)\ | 773 | - store.timedelta(10)\ |
272 | + portfolio.timedelta(self.call_count) | 774 | + store.timedelta(self.call_count)) |
273 | repartition.side_effect = _repartition | 775 | get_cryptoportfolio.side_effect = _get |
274 | 776 | ||
275 | portfolio.Portfolio.wait_for_recent(self.m) | 777 | market.Portfolio.wait_for_recent() |
276 | sleep.assert_called_with(30) | 778 | sleep.assert_called_with(30) |
277 | self.assertEqual(6, sleep.call_count) | 779 | self.assertEqual(6, sleep.call_count) |
278 | self.assertEqual(7, repartition.call_count) | 780 | self.assertEqual(7, get_cryptoportfolio.call_count) |
279 | self.m.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio") | 781 | market.Portfolio.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio") |
280 | 782 | ||
281 | sleep.reset_mock() | 783 | sleep.reset_mock() |
282 | repartition.reset_mock() | 784 | get_cryptoportfolio.reset_mock() |
283 | portfolio.Portfolio.last_date = None | 785 | market.Portfolio.last_date = store.LockedVar(None) |
284 | self.call_count = 0 | 786 | self.call_count = 0 |
285 | portfolio.Portfolio.wait_for_recent(self.m, delta=15) | 787 | market.Portfolio.wait_for_recent(delta=15) |
286 | sleep.assert_not_called() | 788 | sleep.assert_not_called() |
287 | self.assertEqual(1, repartition.call_count) | 789 | self.assertEqual(1, get_cryptoportfolio.call_count) |
288 | 790 | ||
289 | sleep.reset_mock() | 791 | sleep.reset_mock() |
290 | repartition.reset_mock() | 792 | get_cryptoportfolio.reset_mock() |
291 | portfolio.Portfolio.last_date = None | 793 | market.Portfolio.last_date = store.LockedVar(None) |
292 | self.call_count = 0 | 794 | self.call_count = 0 |
293 | portfolio.Portfolio.wait_for_recent(self.m, delta=1) | 795 | market.Portfolio.wait_for_recent(delta=1) |
294 | sleep.assert_called_with(30) | 796 | sleep.assert_called_with(30) |
295 | self.assertEqual(9, sleep.call_count) | 797 | self.assertEqual(9, sleep.call_count) |
296 | self.assertEqual(10, repartition.call_count) | 798 | self.assertEqual(10, get_cryptoportfolio.call_count) |
799 | |||
800 | def test_is_worker_thread(self): | ||
801 | with self.subTest(worker=None): | ||
802 | self.assertFalse(store.Portfolio.is_worker_thread()) | ||
803 | |||
804 | with self.subTest(worker="not self"),\ | ||
805 | mock.patch("threading.current_thread") as current_thread: | ||
806 | current = mock.Mock() | ||
807 | current_thread.return_value = current | ||
808 | store.Portfolio.worker = mock.Mock() | ||
809 | self.assertFalse(store.Portfolio.is_worker_thread()) | ||
810 | |||
811 | with self.subTest(worker="self"),\ | ||
812 | mock.patch("threading.current_thread") as current_thread: | ||
813 | current = mock.Mock() | ||
814 | current_thread.return_value = current | ||
815 | store.Portfolio.worker = current | ||
816 | self.assertTrue(store.Portfolio.is_worker_thread()) | ||
817 | |||
818 | def test_start_worker(self): | ||
819 | with mock.patch.object(store.Portfolio, "wait_for_notification") as notification: | ||
820 | store.Portfolio.start_worker() | ||
821 | notification.assert_called_once_with(poll=30) | ||
822 | |||
823 | self.assertEqual("lock", store.Portfolio.last_date.lock.__class__.__name__) | ||
824 | self.assertEqual("lock", store.Portfolio.liquidities.lock.__class__.__name__) | ||
825 | store.Portfolio.report.start_lock.assert_called_once_with() | ||
826 | |||
827 | self.assertIsNotNone(store.Portfolio.worker) | ||
828 | self.assertIsNotNone(store.Portfolio.worker_notify) | ||
829 | self.assertIsNotNone(store.Portfolio.callback) | ||
830 | self.assertTrue(store.Portfolio.worker_started) | ||
297 | 831 | ||
298 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 832 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
299 | class AmountTest(WebMockTestCase): | 833 | class AmountTest(WebMockTestCase): |
@@ -625,7 +1159,7 @@ class MarketTest(WebMockTestCase): | |||
625 | self.ccxt = mock.Mock(spec=market.ccxt.poloniexE) | 1159 | self.ccxt = mock.Mock(spec=market.ccxt.poloniexE) |
626 | 1160 | ||
627 | def test_values(self): | 1161 | def test_values(self): |
628 | m = market.Market(self.ccxt) | 1162 | m = market.Market(self.ccxt, self.market_args()) |
629 | 1163 | ||
630 | self.assertEqual(self.ccxt, m.ccxt) | 1164 | self.assertEqual(self.ccxt, m.ccxt) |
631 | self.assertFalse(m.debug) | 1165 | self.assertFalse(m.debug) |
@@ -637,28 +1171,30 @@ class MarketTest(WebMockTestCase): | |||
637 | self.assertEqual(m, m.balances.market) | 1171 | self.assertEqual(m, m.balances.market) |
638 | self.assertEqual(m, m.ccxt._market) | 1172 | self.assertEqual(m, m.ccxt._market) |
639 | 1173 | ||
640 | m = market.Market(self.ccxt, debug=True) | 1174 | m = market.Market(self.ccxt, self.market_args(debug=True)) |
641 | self.assertTrue(m.debug) | 1175 | self.assertTrue(m.debug) |
642 | 1176 | ||
643 | m = market.Market(self.ccxt, debug=False) | 1177 | m = market.Market(self.ccxt, self.market_args(debug=False)) |
644 | self.assertFalse(m.debug) | 1178 | self.assertFalse(m.debug) |
645 | 1179 | ||
1180 | with mock.patch("market.ReportStore") as report_store: | ||
1181 | with self.subTest(quiet=False): | ||
1182 | m = market.Market(self.ccxt, self.market_args(quiet=False)) | ||
1183 | report_store.assert_called_with(m, verbose_print=True) | ||
1184 | with self.subTest(quiet=True): | ||
1185 | m = market.Market(self.ccxt, self.market_args(quiet=True)) | ||
1186 | report_store.assert_called_with(m, verbose_print=False) | ||
1187 | |||
646 | @mock.patch("market.ccxt") | 1188 | @mock.patch("market.ccxt") |
647 | def test_from_config(self, ccxt): | 1189 | def test_from_config(self, ccxt): |
648 | with mock.patch("market.ReportStore"): | 1190 | with mock.patch("market.ReportStore"): |
649 | ccxt.poloniexE.return_value = self.ccxt | 1191 | ccxt.poloniexE.return_value = self.ccxt |
650 | self.ccxt.session.request.return_value = "response" | ||
651 | 1192 | ||
652 | m = market.Market.from_config({"key": "key", "secred": "secret"}) | 1193 | m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args()) |
653 | 1194 | ||
654 | self.assertEqual(self.ccxt, m.ccxt) | 1195 | self.assertEqual(self.ccxt, m.ccxt) |
655 | 1196 | ||
656 | self.ccxt.session.request("GET", "URL", data="data", | 1197 | m = market.Market.from_config({"key": "key", "secred": "secret"}, self.market_args(debug=True)) |
657 | headers="headers") | ||
658 | m.report.log_http_request.assert_called_with('GET', 'URL', 'data', | ||
659 | 'headers', 'response') | ||
660 | |||
661 | m = market.Market.from_config({"key": "key", "secred": "secret"}, debug=True) | ||
662 | self.assertEqual(True, m.debug) | 1198 | self.assertEqual(True, m.debug) |
663 | 1199 | ||
664 | def test_get_tickers(self): | 1200 | def test_get_tickers(self): |
@@ -667,7 +1203,7 @@ class MarketTest(WebMockTestCase): | |||
667 | market.NotSupported | 1203 | market.NotSupported |
668 | ] | 1204 | ] |
669 | 1205 | ||
670 | m = market.Market(self.ccxt) | 1206 | m = market.Market(self.ccxt, self.market_args()) |
671 | self.assertEqual("tickers", m.get_tickers()) | 1207 | self.assertEqual("tickers", m.get_tickers()) |
672 | self.assertEqual("tickers", m.get_tickers()) | 1208 | self.assertEqual("tickers", m.get_tickers()) |
673 | self.ccxt.fetch_tickers.assert_called_once() | 1209 | self.ccxt.fetch_tickers.assert_called_once() |
@@ -680,7 +1216,7 @@ class MarketTest(WebMockTestCase): | |||
680 | "ETH/ETC": { "bid": 1, "ask": 3 }, | 1216 | "ETH/ETC": { "bid": 1, "ask": 3 }, |
681 | "XVG/ETH": { "bid": 10, "ask": 40 }, | 1217 | "XVG/ETH": { "bid": 10, "ask": 40 }, |
682 | } | 1218 | } |
683 | m = market.Market(self.ccxt) | 1219 | m = market.Market(self.ccxt, self.market_args()) |
684 | 1220 | ||
685 | ticker = m.get_ticker("ETH", "ETC") | 1221 | ticker = m.get_ticker("ETH", "ETC") |
686 | self.assertEqual(1, ticker["bid"]) | 1222 | self.assertEqual(1, ticker["bid"]) |
@@ -708,7 +1244,7 @@ class MarketTest(WebMockTestCase): | |||
708 | market.ExchangeError("foo"), | 1244 | market.ExchangeError("foo"), |
709 | ] | 1245 | ] |
710 | 1246 | ||
711 | m = market.Market(self.ccxt) | 1247 | m = market.Market(self.ccxt, self.market_args()) |
712 | 1248 | ||
713 | ticker = m.get_ticker("ETH", "ETC") | 1249 | ticker = m.get_ticker("ETH", "ETC") |
714 | self.ccxt.fetch_ticker.assert_called_with("ETH/ETC") | 1250 | self.ccxt.fetch_ticker.assert_called_with("ETH/ETC") |
@@ -728,7 +1264,7 @@ class MarketTest(WebMockTestCase): | |||
728 | self.assertIsNone(ticker) | 1264 | self.assertIsNone(ticker) |
729 | 1265 | ||
730 | def test_fetch_fees(self): | 1266 | def test_fetch_fees(self): |
731 | m = market.Market(self.ccxt) | 1267 | m = market.Market(self.ccxt, self.market_args()) |
732 | self.ccxt.fetch_fees.return_value = "Foo" | 1268 | self.ccxt.fetch_fees.return_value = "Foo" |
733 | self.assertEqual("Foo", m.fetch_fees()) | 1269 | self.assertEqual("Foo", m.fetch_fees()) |
734 | self.ccxt.fetch_fees.assert_called_once() | 1270 | self.ccxt.fetch_fees.assert_called_once() |
@@ -736,7 +1272,7 @@ class MarketTest(WebMockTestCase): | |||
736 | self.assertEqual("Foo", m.fetch_fees()) | 1272 | self.assertEqual("Foo", m.fetch_fees()) |
737 | self.ccxt.fetch_fees.assert_not_called() | 1273 | self.ccxt.fetch_fees.assert_not_called() |
738 | 1274 | ||
739 | @mock.patch.object(portfolio.Portfolio, "repartition") | 1275 | @mock.patch.object(market.Portfolio, "repartition") |
740 | @mock.patch.object(market.Market, "get_ticker") | 1276 | @mock.patch.object(market.Market, "get_ticker") |
741 | @mock.patch.object(market.TradeStore, "compute_trades") | 1277 | @mock.patch.object(market.TradeStore, "compute_trades") |
742 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): | 1278 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): |
@@ -755,7 +1291,7 @@ class MarketTest(WebMockTestCase): | |||
755 | get_ticker.side_effect = _get_ticker | 1291 | get_ticker.side_effect = _get_ticker |
756 | 1292 | ||
757 | with mock.patch("market.ReportStore"): | 1293 | with mock.patch("market.ReportStore"): |
758 | m = market.Market(self.ccxt) | 1294 | m = market.Market(self.ccxt, self.market_args()) |
759 | self.ccxt.fetch_all_balances.return_value = { | 1295 | self.ccxt.fetch_all_balances.return_value = { |
760 | "USDT": { | 1296 | "USDT": { |
761 | "exchange_free": D("10000.0"), | 1297 | "exchange_free": D("10000.0"), |
@@ -787,7 +1323,7 @@ class MarketTest(WebMockTestCase): | |||
787 | m.report.log_balances.assert_called_once_with(tag="tag") | 1323 | m.report.log_balances.assert_called_once_with(tag="tag") |
788 | 1324 | ||
789 | 1325 | ||
790 | @mock.patch.object(portfolio.time, "sleep") | 1326 | @mock.patch.object(market.time, "sleep") |
791 | @mock.patch.object(market.TradeStore, "all_orders") | 1327 | @mock.patch.object(market.TradeStore, "all_orders") |
792 | def test_follow_orders(self, all_orders, time_mock): | 1328 | def test_follow_orders(self, all_orders, time_mock): |
793 | for debug, sleep in [ | 1329 | for debug, sleep in [ |
@@ -795,7 +1331,7 @@ class MarketTest(WebMockTestCase): | |||
795 | (False, 12), (True, 12)]: | 1331 | (False, 12), (True, 12)]: |
796 | with self.subTest(sleep=sleep, debug=debug), \ | 1332 | with self.subTest(sleep=sleep, debug=debug), \ |
797 | mock.patch("market.ReportStore"): | 1333 | mock.patch("market.ReportStore"): |
798 | m = market.Market(self.ccxt, debug=debug) | 1334 | m = market.Market(self.ccxt, self.market_args(debug=debug)) |
799 | 1335 | ||
800 | order_mock1 = mock.Mock() | 1336 | order_mock1 = mock.Mock() |
801 | order_mock2 = mock.Mock() | 1337 | order_mock2 = mock.Mock() |
@@ -872,7 +1408,7 @@ class MarketTest(WebMockTestCase): | |||
872 | for debug in [True, False]: | 1408 | for debug in [True, False]: |
873 | with self.subTest(debug=debug),\ | 1409 | with self.subTest(debug=debug),\ |
874 | mock.patch("market.ReportStore"): | 1410 | mock.patch("market.ReportStore"): |
875 | m = market.Market(self.ccxt, debug=debug) | 1411 | m = market.Market(self.ccxt, self.market_args(debug=debug)) |
876 | 1412 | ||
877 | value_from = portfolio.Amount("BTC", "1.0") | 1413 | value_from = portfolio.Amount("BTC", "1.0") |
878 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | 1414 | value_from.linked_to = portfolio.Amount("ETH", "10.0") |
@@ -907,7 +1443,389 @@ class MarketTest(WebMockTestCase): | |||
907 | self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") | 1443 | self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") |
908 | self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") | 1444 | self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") |
909 | self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") | 1445 | self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") |
910 | 1446 | ||
1447 | m.report.reset_mock() | ||
1448 | fetch_balances.reset_mock() | ||
1449 | with self.subTest(retry=True): | ||
1450 | with mock.patch("market.ReportStore"): | ||
1451 | m = market.Market(self.ccxt, self.market_args()) | ||
1452 | |||
1453 | value_from = portfolio.Amount("BTC", "0.0") | ||
1454 | value_from.linked_to = portfolio.Amount("ETH", "0.0") | ||
1455 | value_to = portfolio.Amount("BTC", "-3.0") | ||
1456 | trade = portfolio.Trade(value_from, value_to, "ETH", m) | ||
1457 | |||
1458 | m.trades.all = [trade] | ||
1459 | balance = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" }) | ||
1460 | m.balances.all = {"BTC": balance} | ||
1461 | |||
1462 | m.ccxt.transfer_balance.side_effect = [ | ||
1463 | market.ccxt.RequestTimeout, | ||
1464 | True | ||
1465 | ] | ||
1466 | m.move_balances() | ||
1467 | self.ccxt.transfer_balance.assert_has_calls([ | ||
1468 | mock.call("BTC", 3, "exchange", "margin"), | ||
1469 | mock.call("BTC", 3, "exchange", "margin") | ||
1470 | ]) | ||
1471 | self.assertEqual(2, fetch_balances.call_count) | ||
1472 | m.report.log_error.assert_called_with(mock.ANY, message="Retrying", exception=mock.ANY) | ||
1473 | self.assertEqual(2, m.report.log_move_balances.call_count) | ||
1474 | |||
1475 | self.ccxt.transfer_balance.reset_mock() | ||
1476 | m.report.reset_mock() | ||
1477 | fetch_balances.reset_mock() | ||
1478 | with self.subTest(retry=True, too_much=True): | ||
1479 | with mock.patch("market.ReportStore"): | ||
1480 | m = market.Market(self.ccxt, self.market_args()) | ||
1481 | |||
1482 | value_from = portfolio.Amount("BTC", "0.0") | ||
1483 | value_from.linked_to = portfolio.Amount("ETH", "0.0") | ||
1484 | value_to = portfolio.Amount("BTC", "-3.0") | ||
1485 | trade = portfolio.Trade(value_from, value_to, "ETH", m) | ||
1486 | |||
1487 | m.trades.all = [trade] | ||
1488 | balance = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" }) | ||
1489 | m.balances.all = {"BTC": balance} | ||
1490 | |||
1491 | m.ccxt.transfer_balance.side_effect = [ | ||
1492 | market.ccxt.RequestTimeout, | ||
1493 | market.ccxt.RequestTimeout, | ||
1494 | market.ccxt.RequestTimeout, | ||
1495 | market.ccxt.RequestTimeout, | ||
1496 | market.ccxt.RequestTimeout, | ||
1497 | ] | ||
1498 | with self.assertRaises(market.ccxt.RequestTimeout): | ||
1499 | m.move_balances() | ||
1500 | |||
1501 | self.ccxt.transfer_balance.reset_mock() | ||
1502 | m.report.reset_mock() | ||
1503 | fetch_balances.reset_mock() | ||
1504 | with self.subTest(retry=True, partial_result=True): | ||
1505 | with mock.patch("market.ReportStore"): | ||
1506 | m = market.Market(self.ccxt, self.market_args()) | ||
1507 | |||
1508 | value_from = portfolio.Amount("BTC", "1.0") | ||
1509 | value_from.linked_to = portfolio.Amount("ETH", "10.0") | ||
1510 | value_to = portfolio.Amount("BTC", "10.0") | ||
1511 | trade1 = portfolio.Trade(value_from, value_to, "ETH", m) | ||
1512 | |||
1513 | value_from = portfolio.Amount("BTC", "0.0") | ||
1514 | value_from.linked_to = portfolio.Amount("ETH", "0.0") | ||
1515 | value_to = portfolio.Amount("BTC", "-3.0") | ||
1516 | trade2 = portfolio.Trade(value_from, value_to, "ETH", m) | ||
1517 | |||
1518 | value_from = portfolio.Amount("USDT", "0.0") | ||
1519 | value_from.linked_to = portfolio.Amount("XVG", "0.0") | ||
1520 | value_to = portfolio.Amount("USDT", "-50.0") | ||
1521 | trade3 = portfolio.Trade(value_from, value_to, "XVG", m) | ||
1522 | |||
1523 | m.trades.all = [trade1, trade2, trade3] | ||
1524 | balance1 = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "0" }) | ||
1525 | balance2 = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "50" }) | ||
1526 | balance3 = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "15" }) | ||
1527 | m.balances.all = {"BTC": balance1, "USDT": balance2, "ETC": balance3} | ||
1528 | |||
1529 | call_counts = { "BTC": 0, "USDT": 0, "ETC": 0 } | ||
1530 | def _transfer_balance(currency, amount, from_, to_): | ||
1531 | call_counts[currency] += 1 | ||
1532 | if currency == "BTC": | ||
1533 | m.balances.all["BTC"] = portfolio.Balance("BTC", { "margin_in_position": "0", "margin_available": "3" }) | ||
1534 | if currency == "USDT": | ||
1535 | if call_counts["USDT"] == 1: | ||
1536 | raise market.ccxt.RequestTimeout | ||
1537 | else: | ||
1538 | m.balances.all["USDT"] = portfolio.Balance("USDT", { "margin_in_position": "100", "margin_available": "150" }) | ||
1539 | if currency == "ETC": | ||
1540 | m.balances.all["ETC"] = portfolio.Balance("ETC", { "margin_in_position": "10", "margin_available": "10" }) | ||
1541 | |||
1542 | |||
1543 | m.ccxt.transfer_balance.side_effect = _transfer_balance | ||
1544 | |||
1545 | m.move_balances() | ||
1546 | self.ccxt.transfer_balance.assert_has_calls([ | ||
1547 | mock.call("BTC", 3, "exchange", "margin"), | ||
1548 | mock.call('USDT', 100, 'exchange', 'margin'), | ||
1549 | mock.call('USDT', 100, 'exchange', 'margin'), | ||
1550 | mock.call("ETC", 5, "margin", "exchange") | ||
1551 | ]) | ||
1552 | self.assertEqual(2, fetch_balances.call_count) | ||
1553 | m.report.log_error.assert_called_with(mock.ANY, message="Retrying", exception=mock.ANY) | ||
1554 | self.assertEqual(2, m.report.log_move_balances.call_count) | ||
1555 | m.report.log_move_balances.asser_has_calls([ | ||
1556 | mock.call( | ||
1557 | { | ||
1558 | 'BTC': portfolio.Amount("BTC", "3"), | ||
1559 | 'USDT': portfolio.Amount("USDT", "150"), | ||
1560 | 'ETC': portfolio.Amount("ETC", "10"), | ||
1561 | }, | ||
1562 | { | ||
1563 | 'BTC': portfolio.Amount("BTC", "3"), | ||
1564 | 'USDT': portfolio.Amount("USDT", "100"), | ||
1565 | }), | ||
1566 | mock.call( | ||
1567 | { | ||
1568 | 'BTC': portfolio.Amount("BTC", "3"), | ||
1569 | 'USDT': portfolio.Amount("USDT", "150"), | ||
1570 | 'ETC': portfolio.Amount("ETC", "10"), | ||
1571 | }, | ||
1572 | { | ||
1573 | 'BTC': portfolio.Amount("BTC", "0"), | ||
1574 | 'USDT': portfolio.Amount("USDT", "100"), | ||
1575 | 'ETC': portfolio.Amount("ETC", "-5"), | ||
1576 | }), | ||
1577 | ]) | ||
1578 | |||
1579 | |||
1580 | def test_store_file_report(self): | ||
1581 | file_open = mock.mock_open() | ||
1582 | m = market.Market(self.ccxt, self.market_args(), report_path="present", user_id=1) | ||
1583 | with self.subTest(file="present"),\ | ||
1584 | mock.patch("market.open", file_open),\ | ||
1585 | mock.patch.object(m, "report") as report,\ | ||
1586 | mock.patch.object(market, "datetime") as time_mock: | ||
1587 | |||
1588 | report.print_logs = [[time_mock.now(), "Foo"], [time_mock.now(), "Bar"]] | ||
1589 | report.to_json.return_value = "json_content" | ||
1590 | |||
1591 | m.store_file_report(datetime.datetime(2018, 2, 25)) | ||
1592 | |||
1593 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") | ||
1594 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.log", "w") | ||
1595 | file_open().write.assert_any_call("json_content") | ||
1596 | file_open().write.assert_any_call("Foo\nBar") | ||
1597 | m.report.to_json.assert_called_once_with() | ||
1598 | |||
1599 | m = market.Market(self.ccxt, self.market_args(), report_path="error", user_id=1) | ||
1600 | with self.subTest(file="error"),\ | ||
1601 | mock.patch("market.open") as file_open,\ | ||
1602 | mock.patch.object(m, "report") as report,\ | ||
1603 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
1604 | file_open.side_effect = FileNotFoundError | ||
1605 | |||
1606 | m.store_file_report(datetime.datetime(2018, 2, 25)) | ||
1607 | |||
1608 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | ||
1609 | |||
1610 | @mock.patch.object(market, "psycopg2") | ||
1611 | def test_store_database_report(self, psycopg2): | ||
1612 | connect_mock = mock.Mock() | ||
1613 | cursor_mock = mock.MagicMock() | ||
1614 | |||
1615 | connect_mock.cursor.return_value = cursor_mock | ||
1616 | psycopg2.connect.return_value = connect_mock | ||
1617 | m = market.Market(self.ccxt, self.market_args(), | ||
1618 | pg_config={"config": "pg_config"}, user_id=1) | ||
1619 | cursor_mock.fetchone.return_value = [42] | ||
1620 | |||
1621 | with self.subTest(error=False),\ | ||
1622 | mock.patch.object(m, "report") as report: | ||
1623 | report.to_json_array.return_value = [ | ||
1624 | ("date1", "type1", "payload1"), | ||
1625 | ("date2", "type2", "payload2"), | ||
1626 | ] | ||
1627 | m.store_database_report(datetime.datetime(2018, 3, 24)) | ||
1628 | connect_mock.assert_has_calls([ | ||
1629 | mock.call.cursor(), | ||
1630 | mock.call.cursor().execute('INSERT INTO reports("date", "market_config_id", "debug") VALUES (%s, %s, %s) RETURNING id;', (datetime.datetime(2018, 3, 24), None, False)), | ||
1631 | mock.call.cursor().fetchone(), | ||
1632 | mock.call.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date1', 42, 'type1', 'payload1')), | ||
1633 | mock.call.cursor().execute('INSERT INTO report_lines("date", "report_id", "type", "payload") VALUES (%s, %s, %s, %s);', ('date2', 42, 'type2', 'payload2')), | ||
1634 | mock.call.commit(), | ||
1635 | mock.call.cursor().close(), | ||
1636 | mock.call.close() | ||
1637 | ]) | ||
1638 | |||
1639 | connect_mock.reset_mock() | ||
1640 | with self.subTest(error=True),\ | ||
1641 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
1642 | psycopg2.connect.side_effect = Exception("Bouh") | ||
1643 | m.store_database_report(datetime.datetime(2018, 3, 24)) | ||
1644 | self.assertEqual(stdout_mock.getvalue(), "impossible to store report to database: Exception; Bouh\n") | ||
1645 | |||
1646 | def test_store_report(self): | ||
1647 | m = market.Market(self.ccxt, self.market_args(), user_id=1) | ||
1648 | with self.subTest(file=None, pg_config=None),\ | ||
1649 | mock.patch.object(m, "report") as report,\ | ||
1650 | mock.patch.object(m, "store_database_report") as db_report,\ | ||
1651 | mock.patch.object(m, "store_file_report") as file_report: | ||
1652 | m.store_report() | ||
1653 | report.merge.assert_called_with(store.Portfolio.report) | ||
1654 | |||
1655 | file_report.assert_not_called() | ||
1656 | db_report.assert_not_called() | ||
1657 | |||
1658 | report.reset_mock() | ||
1659 | m = market.Market(self.ccxt, self.market_args(), report_path="present", user_id=1) | ||
1660 | with self.subTest(file="present", pg_config=None),\ | ||
1661 | mock.patch.object(m, "report") as report,\ | ||
1662 | mock.patch.object(m, "store_file_report") as file_report,\ | ||
1663 | mock.patch.object(m, "store_database_report") as db_report,\ | ||
1664 | mock.patch.object(market, "datetime") as time_mock: | ||
1665 | |||
1666 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
1667 | |||
1668 | m.store_report() | ||
1669 | |||
1670 | report.merge.assert_called_with(store.Portfolio.report) | ||
1671 | file_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) | ||
1672 | db_report.assert_not_called() | ||
1673 | |||
1674 | report.reset_mock() | ||
1675 | m = market.Market(self.ccxt, self.market_args(), pg_config="present", user_id=1) | ||
1676 | with self.subTest(file=None, pg_config="present"),\ | ||
1677 | mock.patch.object(m, "report") as report,\ | ||
1678 | mock.patch.object(m, "store_file_report") as file_report,\ | ||
1679 | mock.patch.object(m, "store_database_report") as db_report,\ | ||
1680 | mock.patch.object(market, "datetime") as time_mock: | ||
1681 | |||
1682 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
1683 | |||
1684 | m.store_report() | ||
1685 | |||
1686 | report.merge.assert_called_with(store.Portfolio.report) | ||
1687 | file_report.assert_not_called() | ||
1688 | db_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) | ||
1689 | |||
1690 | report.reset_mock() | ||
1691 | m = market.Market(self.ccxt, self.market_args(), | ||
1692 | pg_config="pg_config", report_path="present", user_id=1) | ||
1693 | with self.subTest(file="present", pg_config="present"),\ | ||
1694 | mock.patch.object(m, "report") as report,\ | ||
1695 | mock.patch.object(m, "store_file_report") as file_report,\ | ||
1696 | mock.patch.object(m, "store_database_report") as db_report,\ | ||
1697 | mock.patch.object(market, "datetime") as time_mock: | ||
1698 | |||
1699 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
1700 | |||
1701 | m.store_report() | ||
1702 | |||
1703 | report.merge.assert_called_with(store.Portfolio.report) | ||
1704 | file_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) | ||
1705 | db_report.assert_called_once_with(datetime.datetime(2018, 2, 25)) | ||
1706 | |||
1707 | def test_print_orders(self): | ||
1708 | m = market.Market(self.ccxt, self.market_args()) | ||
1709 | with mock.patch.object(m.report, "log_stage") as log_stage,\ | ||
1710 | mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ | ||
1711 | mock.patch.object(m, "prepare_trades") as prepare_trades,\ | ||
1712 | mock.patch.object(m.trades, "prepare_orders") as prepare_orders: | ||
1713 | m.print_orders() | ||
1714 | |||
1715 | log_stage.assert_called_with("print_orders") | ||
1716 | fetch_balances.assert_called_with(tag="print_orders") | ||
1717 | prepare_trades.assert_called_with(base_currency="BTC", | ||
1718 | compute_value="average") | ||
1719 | prepare_orders.assert_called_with(compute_value="average") | ||
1720 | |||
1721 | def test_print_balances(self): | ||
1722 | m = market.Market(self.ccxt, self.market_args()) | ||
1723 | |||
1724 | with mock.patch.object(m.balances, "in_currency") as in_currency,\ | ||
1725 | mock.patch.object(m.report, "log_stage") as log_stage,\ | ||
1726 | mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ | ||
1727 | mock.patch.object(m.report, "print_log") as print_log: | ||
1728 | |||
1729 | in_currency.return_value = { | ||
1730 | "BTC": portfolio.Amount("BTC", "0.65"), | ||
1731 | "ETH": portfolio.Amount("BTC", "0.3"), | ||
1732 | } | ||
1733 | |||
1734 | m.print_balances() | ||
1735 | |||
1736 | log_stage.assert_called_once_with("print_balances") | ||
1737 | fetch_balances.assert_called_with() | ||
1738 | print_log.assert_has_calls([ | ||
1739 | mock.call("total:"), | ||
1740 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
1741 | ]) | ||
1742 | |||
1743 | @mock.patch("market.Processor.process") | ||
1744 | @mock.patch("market.ReportStore.log_error") | ||
1745 | @mock.patch("market.Market.store_report") | ||
1746 | def test_process(self, store_report, log_error, process): | ||
1747 | m = market.Market(self.ccxt, self.market_args()) | ||
1748 | with self.subTest(before=False, after=False): | ||
1749 | m.process(None) | ||
1750 | |||
1751 | process.assert_not_called() | ||
1752 | store_report.assert_called_once() | ||
1753 | log_error.assert_not_called() | ||
1754 | |||
1755 | process.reset_mock() | ||
1756 | log_error.reset_mock() | ||
1757 | store_report.reset_mock() | ||
1758 | with self.subTest(before=True, after=False): | ||
1759 | m.process(None, before=True) | ||
1760 | |||
1761 | process.assert_called_once_with("sell_all", steps="before") | ||
1762 | store_report.assert_called_once() | ||
1763 | log_error.assert_not_called() | ||
1764 | |||
1765 | process.reset_mock() | ||
1766 | log_error.reset_mock() | ||
1767 | store_report.reset_mock() | ||
1768 | with self.subTest(before=False, after=True): | ||
1769 | m.process(None, after=True) | ||
1770 | |||
1771 | process.assert_called_once_with("sell_all", steps="after") | ||
1772 | store_report.assert_called_once() | ||
1773 | log_error.assert_not_called() | ||
1774 | |||
1775 | process.reset_mock() | ||
1776 | log_error.reset_mock() | ||
1777 | store_report.reset_mock() | ||
1778 | with self.subTest(before=True, after=True): | ||
1779 | m.process(None, before=True, after=True) | ||
1780 | |||
1781 | process.assert_has_calls([ | ||
1782 | mock.call("sell_all", steps="before"), | ||
1783 | mock.call("sell_all", steps="after"), | ||
1784 | ]) | ||
1785 | store_report.assert_called_once() | ||
1786 | log_error.assert_not_called() | ||
1787 | |||
1788 | process.reset_mock() | ||
1789 | log_error.reset_mock() | ||
1790 | store_report.reset_mock() | ||
1791 | with self.subTest(action="print_balances"),\ | ||
1792 | mock.patch.object(m, "print_balances") as print_balances: | ||
1793 | m.process(["print_balances"]) | ||
1794 | |||
1795 | process.assert_not_called() | ||
1796 | log_error.assert_not_called() | ||
1797 | store_report.assert_called_once() | ||
1798 | print_balances.assert_called_once_with() | ||
1799 | |||
1800 | log_error.reset_mock() | ||
1801 | store_report.reset_mock() | ||
1802 | with self.subTest(action="print_orders"),\ | ||
1803 | mock.patch.object(m, "print_orders") as print_orders,\ | ||
1804 | mock.patch.object(m, "print_balances") as print_balances: | ||
1805 | m.process(["print_orders", "print_balances"]) | ||
1806 | |||
1807 | process.assert_not_called() | ||
1808 | log_error.assert_not_called() | ||
1809 | store_report.assert_called_once() | ||
1810 | print_orders.assert_called_once_with() | ||
1811 | print_balances.assert_called_once_with() | ||
1812 | |||
1813 | log_error.reset_mock() | ||
1814 | store_report.reset_mock() | ||
1815 | with self.subTest(action="unknown"): | ||
1816 | m.process(["unknown"]) | ||
1817 | log_error.assert_called_once_with("market_process", message="Unknown action unknown") | ||
1818 | store_report.assert_called_once() | ||
1819 | |||
1820 | log_error.reset_mock() | ||
1821 | store_report.reset_mock() | ||
1822 | with self.subTest(unhandled_exception=True): | ||
1823 | process.side_effect = Exception("bouh") | ||
1824 | |||
1825 | m.process(None, before=True) | ||
1826 | log_error.assert_called_with("market_process", exception=mock.ANY) | ||
1827 | store_report.assert_called_once() | ||
1828 | |||
911 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 1829 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
912 | class TradeStoreTest(WebMockTestCase): | 1830 | class TradeStoreTest(WebMockTestCase): |
913 | def test_compute_trades(self): | 1831 | def test_compute_trades(self): |
@@ -1226,7 +2144,7 @@ class BalanceStoreTest(WebMockTestCase): | |||
1226 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies())) | 2144 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies())) |
1227 | self.m.report.log_balances.assert_called_with(tag="foo") | 2145 | self.m.report.log_balances.assert_called_with(tag="foo") |
1228 | 2146 | ||
1229 | @mock.patch.object(portfolio.Portfolio, "repartition") | 2147 | @mock.patch.object(market.Portfolio, "repartition") |
1230 | def test_dispatch_assets(self, repartition): | 2148 | def test_dispatch_assets(self, repartition): |
1231 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance | 2149 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance |
1232 | 2150 | ||
@@ -1243,7 +2161,7 @@ class BalanceStoreTest(WebMockTestCase): | |||
1243 | repartition.return_value = repartition_hash | 2161 | repartition.return_value = repartition_hash |
1244 | 2162 | ||
1245 | amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1")) | 2163 | amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1")) |
1246 | repartition.assert_called_with(self.m, liquidity="medium") | 2164 | repartition.assert_called_with(liquidity="medium") |
1247 | self.assertIn("XEM", balance_store.currencies()) | 2165 | self.assertIn("XEM", balance_store.currencies()) |
1248 | self.assertEqual(D("2.6"), amounts["BTC"].value) | 2166 | self.assertEqual(D("2.6"), amounts["BTC"].value) |
1249 | self.assertEqual(D("7.5"), amounts["XEM"].value) | 2167 | self.assertEqual(D("7.5"), amounts["XEM"].value) |
@@ -2295,6 +3213,507 @@ class OrderTest(WebMockTestCase): | |||
2295 | self.assertEqual(5, self.m.report.log_error.call_count) | 3213 | self.assertEqual(5, self.m.report.log_error.call_count) |
2296 | self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00096060 ETH at 0.1 BTC [pending])", exception=mock.ANY) | 3214 | self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00096060 ETH at 0.1 BTC [pending])", exception=mock.ANY) |
2297 | 3215 | ||
3216 | self.m.reset_mock() | ||
3217 | with self.subTest(request_timeout=True): | ||
3218 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3219 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3220 | with self.subTest(retrieved=False), \ | ||
3221 | mock.patch.object(order, "retrieve_order") as retrieve: | ||
3222 | self.m.ccxt.create_order.side_effect = [ | ||
3223 | portfolio.RequestTimeout, | ||
3224 | portfolio.RequestTimeout, | ||
3225 | { "id": 123 }, | ||
3226 | ] | ||
3227 | retrieve.return_value = False | ||
3228 | order.run() | ||
3229 | self.m.ccxt.create_order.assert_has_calls([ | ||
3230 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3231 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3232 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3233 | ]) | ||
3234 | self.assertEqual(3, self.m.ccxt.create_order.call_count) | ||
3235 | self.assertEqual(3, order.tries) | ||
3236 | self.m.report.log_error.assert_called() | ||
3237 | self.assertEqual(2, self.m.report.log_error.call_count) | ||
3238 | self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after timeout", exception=mock.ANY) | ||
3239 | self.assertEqual(123, order.id) | ||
3240 | |||
3241 | self.m.reset_mock() | ||
3242 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3243 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3244 | with self.subTest(retrieved=True), \ | ||
3245 | mock.patch.object(order, "retrieve_order") as retrieve: | ||
3246 | self.m.ccxt.create_order.side_effect = [ | ||
3247 | portfolio.RequestTimeout, | ||
3248 | ] | ||
3249 | def _retrieve(): | ||
3250 | order.results.append({"id": 123}) | ||
3251 | return True | ||
3252 | retrieve.side_effect = _retrieve | ||
3253 | order.run() | ||
3254 | self.m.ccxt.create_order.assert_has_calls([ | ||
3255 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3256 | ]) | ||
3257 | self.assertEqual(1, self.m.ccxt.create_order.call_count) | ||
3258 | self.assertEqual(1, order.tries) | ||
3259 | self.m.report.log_error.assert_called() | ||
3260 | self.assertEqual(1, self.m.report.log_error.call_count) | ||
3261 | self.m.report.log_error.assert_called_with(mock.ANY, message="Timeout, found the order") | ||
3262 | self.assertEqual(123, order.id) | ||
3263 | |||
3264 | self.m.reset_mock() | ||
3265 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3266 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3267 | with self.subTest(retrieved=False), \ | ||
3268 | mock.patch.object(order, "retrieve_order") as retrieve: | ||
3269 | self.m.ccxt.create_order.side_effect = [ | ||
3270 | portfolio.RequestTimeout, | ||
3271 | portfolio.RequestTimeout, | ||
3272 | portfolio.RequestTimeout, | ||
3273 | portfolio.RequestTimeout, | ||
3274 | portfolio.RequestTimeout, | ||
3275 | ] | ||
3276 | retrieve.return_value = False | ||
3277 | order.run() | ||
3278 | self.m.ccxt.create_order.assert_has_calls([ | ||
3279 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3280 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3281 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3282 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3283 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
3284 | ]) | ||
3285 | self.assertEqual(5, self.m.ccxt.create_order.call_count) | ||
3286 | self.assertEqual(5, order.tries) | ||
3287 | self.m.report.log_error.assert_called() | ||
3288 | self.assertEqual(5, self.m.report.log_error.call_count) | ||
3289 | self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00100000 ETH at 0.1 BTC [pending]) after timeouts", exception=mock.ANY) | ||
3290 | self.assertEqual("error", order.status) | ||
3291 | |||
3292 | def test_retrieve_order(self): | ||
3293 | with self.subTest(similar_open_order=True): | ||
3294 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3295 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3296 | order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55) | ||
3297 | |||
3298 | self.m.ccxt.order_precision.return_value = 8 | ||
3299 | self.m.ccxt.fetch_orders.return_value = [ | ||
3300 | { # Wrong amount | ||
3301 | 'amount': 0.002, 'cost': 0.1, | ||
3302 | 'datetime': '2018-03-25T15:15:51.000Z', | ||
3303 | 'fee': None, 'filled': 0.0, | ||
3304 | 'id': '1', | ||
3305 | 'info': { | ||
3306 | 'amount': '0.002', | ||
3307 | 'date': '2018-03-25 15:15:51', | ||
3308 | 'margin': 0, 'orderNumber': '1', | ||
3309 | 'price': '0.1', 'rate': '0.1', | ||
3310 | 'side': 'buy', 'startingAmount': '0.002', | ||
3311 | 'status': 'open', 'total': '0.0002', | ||
3312 | 'type': 'limit' | ||
3313 | }, | ||
3314 | 'price': 0.1, 'remaining': 0.002, 'side': 'buy', | ||
3315 | 'status': 'open', 'symbol': 'ETH/BTC', | ||
3316 | 'timestamp': 1521990951000, 'trades': None, | ||
3317 | 'type': 'limit' | ||
3318 | }, | ||
3319 | { # Margin | ||
3320 | 'amount': 0.001, 'cost': 0.1, | ||
3321 | 'datetime': '2018-03-25T15:15:51.000Z', | ||
3322 | 'fee': None, 'filled': 0.0, | ||
3323 | 'id': '2', | ||
3324 | 'info': { | ||
3325 | 'amount': '0.001', | ||
3326 | 'date': '2018-03-25 15:15:51', | ||
3327 | 'margin': 1, 'orderNumber': '2', | ||
3328 | 'price': '0.1', 'rate': '0.1', | ||
3329 | 'side': 'buy', 'startingAmount': '0.001', | ||
3330 | 'status': 'open', 'total': '0.0001', | ||
3331 | 'type': 'limit' | ||
3332 | }, | ||
3333 | 'price': 0.1, 'remaining': 0.001, 'side': 'buy', | ||
3334 | 'status': 'open', 'symbol': 'ETH/BTC', | ||
3335 | 'timestamp': 1521990951000, 'trades': None, | ||
3336 | 'type': 'limit' | ||
3337 | }, | ||
3338 | { # selling | ||
3339 | 'amount': 0.001, 'cost': 0.1, | ||
3340 | 'datetime': '2018-03-25T15:15:51.000Z', | ||
3341 | 'fee': None, 'filled': 0.0, | ||
3342 | 'id': '3', | ||
3343 | 'info': { | ||
3344 | 'amount': '0.001', | ||
3345 | 'date': '2018-03-25 15:15:51', | ||
3346 | 'margin': 0, 'orderNumber': '3', | ||
3347 | 'price': '0.1', 'rate': '0.1', | ||
3348 | 'side': 'sell', 'startingAmount': '0.001', | ||
3349 | 'status': 'open', 'total': '0.0001', | ||
3350 | 'type': 'limit' | ||
3351 | }, | ||
3352 | 'price': 0.1, 'remaining': 0.001, 'side': 'sell', | ||
3353 | 'status': 'open', 'symbol': 'ETH/BTC', | ||
3354 | 'timestamp': 1521990951000, 'trades': None, | ||
3355 | 'type': 'limit' | ||
3356 | }, | ||
3357 | { # Wrong rate | ||
3358 | 'amount': 0.001, 'cost': 0.15, | ||
3359 | 'datetime': '2018-03-25T15:15:51.000Z', | ||
3360 | 'fee': None, 'filled': 0.0, | ||
3361 | 'id': '4', | ||
3362 | 'info': { | ||
3363 | 'amount': '0.001', | ||
3364 | 'date': '2018-03-25 15:15:51', | ||
3365 | 'margin': 0, 'orderNumber': '4', | ||
3366 | 'price': '0.15', 'rate': '0.15', | ||
3367 | 'side': 'buy', 'startingAmount': '0.001', | ||
3368 | 'status': 'open', 'total': '0.0001', | ||
3369 | 'type': 'limit' | ||
3370 | }, | ||
3371 | 'price': 0.15, 'remaining': 0.001, 'side': 'buy', | ||
3372 | 'status': 'open', 'symbol': 'ETH/BTC', | ||
3373 | 'timestamp': 1521990951000, 'trades': None, | ||
3374 | 'type': 'limit' | ||
3375 | }, | ||
3376 | { # All good | ||
3377 | 'amount': 0.001, 'cost': 0.1, | ||
3378 | 'datetime': '2018-03-25T15:15:51.000Z', | ||
3379 | 'fee': None, 'filled': 0.0, | ||
3380 | 'id': '5', | ||
3381 | 'info': { | ||
3382 | 'amount': '0.001', | ||
3383 | 'date': '2018-03-25 15:15:51', | ||
3384 | 'margin': 0, 'orderNumber': '1', | ||
3385 | 'price': '0.1', 'rate': '0.1', | ||
3386 | 'side': 'buy', 'startingAmount': '0.001', | ||
3387 | 'status': 'open', 'total': '0.0001', | ||
3388 | 'type': 'limit' | ||
3389 | }, | ||
3390 | 'price': 0.1, 'remaining': 0.001, 'side': 'buy', | ||
3391 | 'status': 'open', 'symbol': 'ETH/BTC', | ||
3392 | 'timestamp': 1521990951000, 'trades': None, | ||
3393 | 'type': 'limit' | ||
3394 | } | ||
3395 | ] | ||
3396 | result = order.retrieve_order() | ||
3397 | self.assertTrue(result) | ||
3398 | self.assertEqual('5', order.results[0]["id"]) | ||
3399 | self.m.ccxt.fetch_my_trades.assert_not_called() | ||
3400 | self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750) | ||
3401 | |||
3402 | self.m.reset_mock() | ||
3403 | with self.subTest(similar_open_order=False, past_trades=True): | ||
3404 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3405 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3406 | order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55) | ||
3407 | |||
3408 | self.m.ccxt.order_precision.return_value = 8 | ||
3409 | self.m.ccxt.fetch_orders.return_value = [] | ||
3410 | self.m.ccxt.fetch_my_trades.return_value = [ | ||
3411 | { # Wrong timestamp 1 | ||
3412 | 'amount': 0.0006, | ||
3413 | 'cost': 0.00006, | ||
3414 | 'datetime': '2018-03-25T15:15:14.000Z', | ||
3415 | 'id': '1-1', | ||
3416 | 'info': { | ||
3417 | 'amount': '0.0006', | ||
3418 | 'category': 'exchange', | ||
3419 | 'date': '2018-03-25 15:15:14', | ||
3420 | 'fee': '0.00150000', | ||
3421 | 'globalTradeID': 1, | ||
3422 | 'orderNumber': '1', | ||
3423 | 'rate': '0.1', | ||
3424 | 'total': '0.00006', | ||
3425 | 'tradeID': '1-1', | ||
3426 | 'type': 'buy' | ||
3427 | }, | ||
3428 | 'order': '1', | ||
3429 | 'price': 0.1, | ||
3430 | 'side': 'buy', | ||
3431 | 'symbol': 'ETH/BTC', | ||
3432 | 'timestamp': 1521983714, | ||
3433 | 'type': 'limit' | ||
3434 | }, | ||
3435 | { # Wrong timestamp 2 | ||
3436 | 'amount': 0.0004, | ||
3437 | 'cost': 0.00004, | ||
3438 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3439 | 'id': '1-2', | ||
3440 | 'info': { | ||
3441 | 'amount': '0.0004', | ||
3442 | 'category': 'exchange', | ||
3443 | 'date': '2018-03-25 15:16:54', | ||
3444 | 'fee': '0.00150000', | ||
3445 | 'globalTradeID': 2, | ||
3446 | 'orderNumber': '1', | ||
3447 | 'rate': '0.1', | ||
3448 | 'total': '0.00004', | ||
3449 | 'tradeID': '1-2', | ||
3450 | 'type': 'buy' | ||
3451 | }, | ||
3452 | 'order': '1', | ||
3453 | 'price': 0.1, | ||
3454 | 'side': 'buy', | ||
3455 | 'symbol': 'ETH/BTC', | ||
3456 | 'timestamp': 1521983814, | ||
3457 | 'type': 'limit' | ||
3458 | }, | ||
3459 | { # Wrong side 1 | ||
3460 | 'amount': 0.0006, | ||
3461 | 'cost': 0.00006, | ||
3462 | 'datetime': '2018-03-25T15:15:54.000Z', | ||
3463 | 'id': '2-1', | ||
3464 | 'info': { | ||
3465 | 'amount': '0.0006', | ||
3466 | 'category': 'exchange', | ||
3467 | 'date': '2018-03-25 15:15:54', | ||
3468 | 'fee': '0.00150000', | ||
3469 | 'globalTradeID': 1, | ||
3470 | 'orderNumber': '2', | ||
3471 | 'rate': '0.1', | ||
3472 | 'total': '0.00006', | ||
3473 | 'tradeID': '2-1', | ||
3474 | 'type': 'sell' | ||
3475 | }, | ||
3476 | 'order': '2', | ||
3477 | 'price': 0.1, | ||
3478 | 'side': 'sell', | ||
3479 | 'symbol': 'ETH/BTC', | ||
3480 | 'timestamp': 1521983754, | ||
3481 | 'type': 'limit' | ||
3482 | }, | ||
3483 | { # Wrong side 2 | ||
3484 | 'amount': 0.0004, | ||
3485 | 'cost': 0.00004, | ||
3486 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3487 | 'id': '2-2', | ||
3488 | 'info': { | ||
3489 | 'amount': '0.0004', | ||
3490 | 'category': 'exchange', | ||
3491 | 'date': '2018-03-25 15:16:54', | ||
3492 | 'fee': '0.00150000', | ||
3493 | 'globalTradeID': 2, | ||
3494 | 'orderNumber': '2', | ||
3495 | 'rate': '0.1', | ||
3496 | 'total': '0.00004', | ||
3497 | 'tradeID': '2-2', | ||
3498 | 'type': 'buy' | ||
3499 | }, | ||
3500 | 'order': '2', | ||
3501 | 'price': 0.1, | ||
3502 | 'side': 'buy', | ||
3503 | 'symbol': 'ETH/BTC', | ||
3504 | 'timestamp': 1521983814, | ||
3505 | 'type': 'limit' | ||
3506 | }, | ||
3507 | { # Margin trade 1 | ||
3508 | 'amount': 0.0006, | ||
3509 | 'cost': 0.00006, | ||
3510 | 'datetime': '2018-03-25T15:15:54.000Z', | ||
3511 | 'id': '3-1', | ||
3512 | 'info': { | ||
3513 | 'amount': '0.0006', | ||
3514 | 'category': 'marginTrade', | ||
3515 | 'date': '2018-03-25 15:15:54', | ||
3516 | 'fee': '0.00150000', | ||
3517 | 'globalTradeID': 1, | ||
3518 | 'orderNumber': '3', | ||
3519 | 'rate': '0.1', | ||
3520 | 'total': '0.00006', | ||
3521 | 'tradeID': '3-1', | ||
3522 | 'type': 'buy' | ||
3523 | }, | ||
3524 | 'order': '3', | ||
3525 | 'price': 0.1, | ||
3526 | 'side': 'buy', | ||
3527 | 'symbol': 'ETH/BTC', | ||
3528 | 'timestamp': 1521983754, | ||
3529 | 'type': 'limit' | ||
3530 | }, | ||
3531 | { # Margin trade 2 | ||
3532 | 'amount': 0.0004, | ||
3533 | 'cost': 0.00004, | ||
3534 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3535 | 'id': '3-2', | ||
3536 | 'info': { | ||
3537 | 'amount': '0.0004', | ||
3538 | 'category': 'marginTrade', | ||
3539 | 'date': '2018-03-25 15:16:54', | ||
3540 | 'fee': '0.00150000', | ||
3541 | 'globalTradeID': 2, | ||
3542 | 'orderNumber': '3', | ||
3543 | 'rate': '0.1', | ||
3544 | 'total': '0.00004', | ||
3545 | 'tradeID': '3-2', | ||
3546 | 'type': 'buy' | ||
3547 | }, | ||
3548 | 'order': '3', | ||
3549 | 'price': 0.1, | ||
3550 | 'side': 'buy', | ||
3551 | 'symbol': 'ETH/BTC', | ||
3552 | 'timestamp': 1521983814, | ||
3553 | 'type': 'limit' | ||
3554 | }, | ||
3555 | { # Wrong amount 1 | ||
3556 | 'amount': 0.0005, | ||
3557 | 'cost': 0.00005, | ||
3558 | 'datetime': '2018-03-25T15:15:54.000Z', | ||
3559 | 'id': '4-1', | ||
3560 | 'info': { | ||
3561 | 'amount': '0.0005', | ||
3562 | 'category': 'exchange', | ||
3563 | 'date': '2018-03-25 15:15:54', | ||
3564 | 'fee': '0.00150000', | ||
3565 | 'globalTradeID': 1, | ||
3566 | 'orderNumber': '4', | ||
3567 | 'rate': '0.1', | ||
3568 | 'total': '0.00005', | ||
3569 | 'tradeID': '4-1', | ||
3570 | 'type': 'buy' | ||
3571 | }, | ||
3572 | 'order': '4', | ||
3573 | 'price': 0.1, | ||
3574 | 'side': 'buy', | ||
3575 | 'symbol': 'ETH/BTC', | ||
3576 | 'timestamp': 1521983754, | ||
3577 | 'type': 'limit' | ||
3578 | }, | ||
3579 | { # Wrong amount 2 | ||
3580 | 'amount': 0.0004, | ||
3581 | 'cost': 0.00004, | ||
3582 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3583 | 'id': '4-2', | ||
3584 | 'info': { | ||
3585 | 'amount': '0.0004', | ||
3586 | 'category': 'exchange', | ||
3587 | 'date': '2018-03-25 15:16:54', | ||
3588 | 'fee': '0.00150000', | ||
3589 | 'globalTradeID': 2, | ||
3590 | 'orderNumber': '4', | ||
3591 | 'rate': '0.1', | ||
3592 | 'total': '0.00004', | ||
3593 | 'tradeID': '4-2', | ||
3594 | 'type': 'buy' | ||
3595 | }, | ||
3596 | 'order': '4', | ||
3597 | 'price': 0.1, | ||
3598 | 'side': 'buy', | ||
3599 | 'symbol': 'ETH/BTC', | ||
3600 | 'timestamp': 1521983814, | ||
3601 | 'type': 'limit' | ||
3602 | }, | ||
3603 | { # Wrong price 1 | ||
3604 | 'amount': 0.0006, | ||
3605 | 'cost': 0.000066, | ||
3606 | 'datetime': '2018-03-25T15:15:54.000Z', | ||
3607 | 'id': '5-1', | ||
3608 | 'info': { | ||
3609 | 'amount': '0.0006', | ||
3610 | 'category': 'exchange', | ||
3611 | 'date': '2018-03-25 15:15:54', | ||
3612 | 'fee': '0.00150000', | ||
3613 | 'globalTradeID': 1, | ||
3614 | 'orderNumber': '5', | ||
3615 | 'rate': '0.11', | ||
3616 | 'total': '0.000066', | ||
3617 | 'tradeID': '5-1', | ||
3618 | 'type': 'buy' | ||
3619 | }, | ||
3620 | 'order': '5', | ||
3621 | 'price': 0.11, | ||
3622 | 'side': 'buy', | ||
3623 | 'symbol': 'ETH/BTC', | ||
3624 | 'timestamp': 1521983754, | ||
3625 | 'type': 'limit' | ||
3626 | }, | ||
3627 | { # Wrong price 2 | ||
3628 | 'amount': 0.0004, | ||
3629 | 'cost': 0.00004, | ||
3630 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3631 | 'id': '5-2', | ||
3632 | 'info': { | ||
3633 | 'amount': '0.0004', | ||
3634 | 'category': 'exchange', | ||
3635 | 'date': '2018-03-25 15:16:54', | ||
3636 | 'fee': '0.00150000', | ||
3637 | 'globalTradeID': 2, | ||
3638 | 'orderNumber': '5', | ||
3639 | 'rate': '0.1', | ||
3640 | 'total': '0.00004', | ||
3641 | 'tradeID': '5-2', | ||
3642 | 'type': 'buy' | ||
3643 | }, | ||
3644 | 'order': '5', | ||
3645 | 'price': 0.1, | ||
3646 | 'side': 'buy', | ||
3647 | 'symbol': 'ETH/BTC', | ||
3648 | 'timestamp': 1521983814, | ||
3649 | 'type': 'limit' | ||
3650 | }, | ||
3651 | { # All good 1 | ||
3652 | 'amount': 0.0006, | ||
3653 | 'cost': 0.00006, | ||
3654 | 'datetime': '2018-03-25T15:15:54.000Z', | ||
3655 | 'id': '7-1', | ||
3656 | 'info': { | ||
3657 | 'amount': '0.0006', | ||
3658 | 'category': 'exchange', | ||
3659 | 'date': '2018-03-25 15:15:54', | ||
3660 | 'fee': '0.00150000', | ||
3661 | 'globalTradeID': 1, | ||
3662 | 'orderNumber': '7', | ||
3663 | 'rate': '0.1', | ||
3664 | 'total': '0.00006', | ||
3665 | 'tradeID': '7-1', | ||
3666 | 'type': 'buy' | ||
3667 | }, | ||
3668 | 'order': '7', | ||
3669 | 'price': 0.1, | ||
3670 | 'side': 'buy', | ||
3671 | 'symbol': 'ETH/BTC', | ||
3672 | 'timestamp': 1521983754, | ||
3673 | 'type': 'limit' | ||
3674 | }, | ||
3675 | { # All good 2 | ||
3676 | 'amount': 0.0004, | ||
3677 | 'cost': 0.000036, | ||
3678 | 'datetime': '2018-03-25T15:16:54.000Z', | ||
3679 | 'id': '7-2', | ||
3680 | 'info': { | ||
3681 | 'amount': '0.0004', | ||
3682 | 'category': 'exchange', | ||
3683 | 'date': '2018-03-25 15:16:54', | ||
3684 | 'fee': '0.00150000', | ||
3685 | 'globalTradeID': 2, | ||
3686 | 'orderNumber': '7', | ||
3687 | 'rate': '0.09', | ||
3688 | 'total': '0.000036', | ||
3689 | 'tradeID': '7-2', | ||
3690 | 'type': 'buy' | ||
3691 | }, | ||
3692 | 'order': '7', | ||
3693 | 'price': 0.09, | ||
3694 | 'side': 'buy', | ||
3695 | 'symbol': 'ETH/BTC', | ||
3696 | 'timestamp': 1521983814, | ||
3697 | 'type': 'limit' | ||
3698 | }, | ||
3699 | ] | ||
3700 | |||
3701 | result = order.retrieve_order() | ||
3702 | self.assertTrue(result) | ||
3703 | self.assertEqual('7', order.results[0]["id"]) | ||
3704 | self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750) | ||
3705 | |||
3706 | self.m.reset_mock() | ||
3707 | with self.subTest(similar_open_order=False, past_trades=False): | ||
3708 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
3709 | D("0.1"), "BTC", "long", self.m, "trade") | ||
3710 | order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55) | ||
3711 | |||
3712 | self.m.ccxt.order_precision.return_value = 8 | ||
3713 | self.m.ccxt.fetch_orders.return_value = [] | ||
3714 | self.m.ccxt.fetch_my_trades.return_value = [] | ||
3715 | result = order.retrieve_order() | ||
3716 | self.assertFalse(result) | ||
2298 | 3717 | ||
2299 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 3718 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
2300 | class MouvementTest(WebMockTestCase): | 3719 | class MouvementTest(WebMockTestCase): |
@@ -2372,14 +3791,30 @@ class ReportStoreTest(WebMockTestCase): | |||
2372 | report_store.set_verbose(False) | 3791 | report_store.set_verbose(False) |
2373 | self.assertFalse(report_store.verbose_print) | 3792 | self.assertFalse(report_store.verbose_print) |
2374 | 3793 | ||
3794 | def test_merge(self): | ||
3795 | report_store1 = market.ReportStore(self.m, verbose_print=False) | ||
3796 | report_store2 = market.ReportStore(None, verbose_print=False) | ||
3797 | |||
3798 | report_store2.log_stage("1") | ||
3799 | report_store1.log_stage("2") | ||
3800 | report_store2.log_stage("3") | ||
3801 | |||
3802 | report_store1.merge(report_store2) | ||
3803 | |||
3804 | self.assertEqual(3, len(report_store1.logs)) | ||
3805 | self.assertEqual(["1", "2", "3"], list(map(lambda x: x["stage"], report_store1.logs))) | ||
3806 | self.assertEqual(6, len(report_store1.print_logs)) | ||
3807 | |||
2375 | def test_print_log(self): | 3808 | def test_print_log(self): |
2376 | report_store = market.ReportStore(self.m) | 3809 | report_store = market.ReportStore(self.m) |
2377 | with self.subTest(verbose=True),\ | 3810 | with self.subTest(verbose=True),\ |
3811 | mock.patch.object(store, "datetime") as time_mock,\ | ||
2378 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3812 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
3813 | time_mock.now.return_value = datetime.datetime(2018, 2, 25, 2, 20, 10) | ||
2379 | report_store.set_verbose(True) | 3814 | report_store.set_verbose(True) |
2380 | report_store.print_log("Coucou") | 3815 | report_store.print_log("Coucou") |
2381 | report_store.print_log(portfolio.Amount("BTC", 1)) | 3816 | report_store.print_log(portfolio.Amount("BTC", 1)) |
2382 | self.assertEqual(stdout_mock.getvalue(), "Coucou\n1.00000000 BTC\n") | 3817 | self.assertEqual(stdout_mock.getvalue(), "2018-02-25 02:20:10: Coucou\n2018-02-25 02:20:10: 1.00000000 BTC\n") |
2383 | 3818 | ||
2384 | with self.subTest(verbose=False),\ | 3819 | with self.subTest(verbose=False),\ |
2385 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3820 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
@@ -2388,6 +3823,14 @@ class ReportStoreTest(WebMockTestCase): | |||
2388 | report_store.print_log(portfolio.Amount("BTC", 1)) | 3823 | report_store.print_log(portfolio.Amount("BTC", 1)) |
2389 | self.assertEqual(stdout_mock.getvalue(), "") | 3824 | self.assertEqual(stdout_mock.getvalue(), "") |
2390 | 3825 | ||
3826 | def test_default_json_serial(self): | ||
3827 | report_store = market.ReportStore(self.m) | ||
3828 | |||
3829 | self.assertEqual("2018-02-24T00:00:00", | ||
3830 | report_store.default_json_serial(portfolio.datetime(2018, 2, 24))) | ||
3831 | self.assertEqual("1.00000000 BTC", | ||
3832 | report_store.default_json_serial(portfolio.Amount("BTC", 1))) | ||
3833 | |||
2391 | def test_to_json(self): | 3834 | def test_to_json(self): |
2392 | report_store = market.ReportStore(self.m) | 3835 | report_store = market.ReportStore(self.m) |
2393 | report_store.logs.append({"foo": "bar"}) | 3836 | report_store.logs.append({"foo": "bar"}) |
@@ -2397,6 +3840,20 @@ class ReportStoreTest(WebMockTestCase): | |||
2397 | report_store.logs.append({"amount": portfolio.Amount("BTC", 1)}) | 3840 | report_store.logs.append({"amount": portfolio.Amount("BTC", 1)}) |
2398 | self.assertEqual('[\n {\n "foo": "bar"\n },\n {\n "date": "2018-02-24T00:00:00"\n },\n {\n "amount": "1.00000000 BTC"\n }\n]', report_store.to_json()) | 3841 | self.assertEqual('[\n {\n "foo": "bar"\n },\n {\n "date": "2018-02-24T00:00:00"\n },\n {\n "amount": "1.00000000 BTC"\n }\n]', report_store.to_json()) |
2399 | 3842 | ||
3843 | def test_to_json_array(self): | ||
3844 | report_store = market.ReportStore(self.m) | ||
3845 | report_store.logs.append({ | ||
3846 | "date": "date1", "type": "type1", "foo": "bar", "bla": "bla" | ||
3847 | }) | ||
3848 | report_store.logs.append({ | ||
3849 | "date": "date2", "type": "type2", "foo": "bar", "bla": "bla" | ||
3850 | }) | ||
3851 | logs = list(report_store.to_json_array()) | ||
3852 | |||
3853 | self.assertEqual(2, len(logs)) | ||
3854 | self.assertEqual(("date1", "type1", '{\n "foo": "bar",\n "bla": "bla"\n}'), logs[0]) | ||
3855 | self.assertEqual(("date2", "type2", '{\n "foo": "bar",\n "bla": "bla"\n}'), logs[1]) | ||
3856 | |||
2400 | @mock.patch.object(market.ReportStore, "print_log") | 3857 | @mock.patch.object(market.ReportStore, "print_log") |
2401 | @mock.patch.object(market.ReportStore, "add_log") | 3858 | @mock.patch.object(market.ReportStore, "add_log") |
2402 | def test_log_stage(self, add_log, print_log): | 3859 | def test_log_stage(self, add_log, print_log): |
@@ -2790,7 +4247,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2790 | }) | 4247 | }) |
2791 | 4248 | ||
2792 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 4249 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
2793 | class HelperTest(WebMockTestCase): | 4250 | class MainTest(WebMockTestCase): |
2794 | def test_make_order(self): | 4251 | def test_make_order(self): |
2795 | self.m.get_ticker.return_value = { | 4252 | self.m.get_ticker.return_value = { |
2796 | "inverted": False, | 4253 | "inverted": False, |
@@ -2800,7 +4257,7 @@ class HelperTest(WebMockTestCase): | |||
2800 | } | 4257 | } |
2801 | 4258 | ||
2802 | with self.subTest(description="nominal case"): | 4259 | with self.subTest(description="nominal case"): |
2803 | helper.make_order(self.m, 10, "ETH") | 4260 | main.make_order(self.m, 10, "ETH") |
2804 | 4261 | ||
2805 | self.m.report.log_stage.assert_has_calls([ | 4262 | self.m.report.log_stage.assert_has_calls([ |
2806 | mock.call("make_order_begin"), | 4263 | mock.call("make_order_begin"), |
@@ -2825,7 +4282,7 @@ class HelperTest(WebMockTestCase): | |||
2825 | 4282 | ||
2826 | self.m.reset_mock() | 4283 | self.m.reset_mock() |
2827 | with self.subTest(compute_value="default"): | 4284 | with self.subTest(compute_value="default"): |
2828 | helper.make_order(self.m, 10, "ETH", action="dispose", | 4285 | main.make_order(self.m, 10, "ETH", action="dispose", |
2829 | compute_value="ask") | 4286 | compute_value="ask") |
2830 | 4287 | ||
2831 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 4288 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
@@ -2834,7 +4291,7 @@ class HelperTest(WebMockTestCase): | |||
2834 | 4291 | ||
2835 | self.m.reset_mock() | 4292 | self.m.reset_mock() |
2836 | with self.subTest(follow=False): | 4293 | with self.subTest(follow=False): |
2837 | result = helper.make_order(self.m, 10, "ETH", follow=False) | 4294 | result = main.make_order(self.m, 10, "ETH", follow=False) |
2838 | 4295 | ||
2839 | self.m.report.log_stage.assert_has_calls([ | 4296 | self.m.report.log_stage.assert_has_calls([ |
2840 | mock.call("make_order_begin"), | 4297 | mock.call("make_order_begin"), |
@@ -2854,7 +4311,7 @@ class HelperTest(WebMockTestCase): | |||
2854 | 4311 | ||
2855 | self.m.reset_mock() | 4312 | self.m.reset_mock() |
2856 | with self.subTest(base_currency="USDT"): | 4313 | with self.subTest(base_currency="USDT"): |
2857 | helper.make_order(self.m, 1, "BTC", base_currency="USDT") | 4314 | main.make_order(self.m, 1, "BTC", base_currency="USDT") |
2858 | 4315 | ||
2859 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 4316 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2860 | self.assertEqual("BTC", trade.currency) | 4317 | self.assertEqual("BTC", trade.currency) |
@@ -2862,14 +4319,14 @@ class HelperTest(WebMockTestCase): | |||
2862 | 4319 | ||
2863 | self.m.reset_mock() | 4320 | self.m.reset_mock() |
2864 | with self.subTest(close_if_possible=True): | 4321 | with self.subTest(close_if_possible=True): |
2865 | helper.make_order(self.m, 10, "ETH", close_if_possible=True) | 4322 | main.make_order(self.m, 10, "ETH", close_if_possible=True) |
2866 | 4323 | ||
2867 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 4324 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2868 | self.assertEqual(True, trade.orders[0].close_if_possible) | 4325 | self.assertEqual(True, trade.orders[0].close_if_possible) |
2869 | 4326 | ||
2870 | self.m.reset_mock() | 4327 | self.m.reset_mock() |
2871 | with self.subTest(action="dispose"): | 4328 | with self.subTest(action="dispose"): |
2872 | helper.make_order(self.m, 10, "ETH", action="dispose") | 4329 | main.make_order(self.m, 10, "ETH", action="dispose") |
2873 | 4330 | ||
2874 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 4331 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2875 | self.assertEqual(0, trade.value_to) | 4332 | self.assertEqual(0, trade.value_to) |
@@ -2879,161 +4336,120 @@ class HelperTest(WebMockTestCase): | |||
2879 | 4336 | ||
2880 | self.m.reset_mock() | 4337 | self.m.reset_mock() |
2881 | with self.subTest(compute_value="default"): | 4338 | with self.subTest(compute_value="default"): |
2882 | helper.make_order(self.m, 10, "ETH", action="dispose", | 4339 | main.make_order(self.m, 10, "ETH", action="dispose", |
2883 | compute_value="bid") | 4340 | compute_value="bid") |
2884 | 4341 | ||
2885 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 4342 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2886 | self.assertEqual(D("0.9"), trade.value_from.value) | 4343 | self.assertEqual(D("0.9"), trade.value_from.value) |
2887 | 4344 | ||
2888 | def test_user_market(self): | 4345 | def test_get_user_market(self): |
2889 | with mock.patch("helper.main_fetch_markets") as main_fetch_markets,\ | 4346 | with mock.patch("main.fetch_markets") as main_fetch_markets,\ |
2890 | mock.patch("helper.main_parse_config") as main_parse_config: | 4347 | mock.patch("main.parse_config") as main_parse_config: |
2891 | with self.subTest(debug=False): | 4348 | with self.subTest(debug=False): |
2892 | main_parse_config.return_value = ["pg_config", "report_path"] | 4349 | main_parse_config.return_value = ["pg_config", "report_path"] |
2893 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 4350 | main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)] |
2894 | m = helper.get_user_market("config_path.ini", 1) | 4351 | m = main.get_user_market("config_path.ini", 1) |
2895 | 4352 | ||
2896 | self.assertIsInstance(m, market.Market) | 4353 | self.assertIsInstance(m, market.Market) |
2897 | self.assertFalse(m.debug) | 4354 | self.assertFalse(m.debug) |
2898 | 4355 | ||
2899 | with self.subTest(debug=True): | 4356 | with self.subTest(debug=True): |
2900 | main_parse_config.return_value = ["pg_config", "report_path"] | 4357 | main_parse_config.return_value = ["pg_config", "report_path"] |
2901 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 4358 | main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)] |
2902 | m = helper.get_user_market("config_path.ini", 1, debug=True) | 4359 | m = main.get_user_market("config_path.ini", 1, debug=True) |
2903 | 4360 | ||
2904 | self.assertIsInstance(m, market.Market) | 4361 | self.assertIsInstance(m, market.Market) |
2905 | self.assertTrue(m.debug) | 4362 | self.assertTrue(m.debug) |
2906 | 4363 | ||
2907 | def test_main_store_report(self): | 4364 | def test_process(self): |
2908 | file_open = mock.mock_open() | 4365 | with mock.patch("market.Market") as market_mock,\ |
2909 | with self.subTest(file=None), mock.patch("__main__.open", file_open): | ||
2910 | helper.main_store_report(None, 1, self.m) | ||
2911 | file_open.assert_not_called() | ||
2912 | |||
2913 | file_open = mock.mock_open() | ||
2914 | with self.subTest(file="present"), mock.patch("helper.open", file_open),\ | ||
2915 | mock.patch.object(helper, "datetime") as time_mock: | ||
2916 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
2917 | self.m.report.to_json.return_value = "json_content" | ||
2918 | |||
2919 | helper.main_store_report("present", 1, self.m) | ||
2920 | |||
2921 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") | ||
2922 | file_open().write.assert_called_once_with("json_content") | ||
2923 | self.m.report.to_json.assert_called_once_with() | ||
2924 | |||
2925 | with self.subTest(file="error"),\ | ||
2926 | mock.patch("helper.open") as file_open,\ | ||
2927 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 4366 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
2928 | file_open.side_effect = FileNotFoundError | ||
2929 | 4367 | ||
2930 | helper.main_store_report("error", 1, self.m) | 4368 | args_mock = mock.Mock() |
4369 | args_mock.action = "action" | ||
4370 | args_mock.config = "config" | ||
4371 | args_mock.user = "user" | ||
4372 | args_mock.debug = "debug" | ||
4373 | args_mock.before = "before" | ||
4374 | args_mock.after = "after" | ||
4375 | self.assertEqual("", stdout_mock.getvalue()) | ||
2931 | 4376 | ||
2932 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | 4377 | main.process("config", 3, 1, args_mock, "report_path", "pg_config") |
2933 | 4378 | ||
2934 | @mock.patch("helper.Processor.process") | 4379 | market_mock.from_config.assert_has_calls([ |
2935 | def test_main_process_market(self, process): | 4380 | mock.call("config", args_mock, pg_config="pg_config", market_id=3, user_id=1, report_path="report_path"), |
2936 | with self.subTest(before=False, after=False): | 4381 | mock.call().process("action", before="before", after="after"), |
2937 | m = mock.Mock() | ||
2938 | helper.main_process_market(m, None) | ||
2939 | |||
2940 | process.assert_not_called() | ||
2941 | |||
2942 | process.reset_mock() | ||
2943 | with self.subTest(before=True, after=False): | ||
2944 | helper.main_process_market(m, None, before=True) | ||
2945 | |||
2946 | process.assert_called_once_with("sell_all", steps="before") | ||
2947 | |||
2948 | process.reset_mock() | ||
2949 | with self.subTest(before=False, after=True): | ||
2950 | helper.main_process_market(m, None, after=True) | ||
2951 | |||
2952 | process.assert_called_once_with("sell_all", steps="after") | ||
2953 | |||
2954 | process.reset_mock() | ||
2955 | with self.subTest(before=True, after=True): | ||
2956 | helper.main_process_market(m, None, before=True, after=True) | ||
2957 | |||
2958 | process.assert_has_calls([ | ||
2959 | mock.call("sell_all", steps="before"), | ||
2960 | mock.call("sell_all", steps="after"), | ||
2961 | ]) | 4382 | ]) |
2962 | 4383 | ||
2963 | process.reset_mock() | 4384 | with self.subTest(exception=True): |
2964 | with self.subTest(action="print_balances"),\ | 4385 | market_mock.from_config.side_effect = Exception("boo") |
2965 | mock.patch("helper.print_balances") as print_balances: | 4386 | main.process(3, "config", 1, "report_path", args_mock, "pg_config") |
2966 | helper.main_process_market("user", ["print_balances"]) | 4387 | self.assertEqual("Exception: boo\n", stdout_mock.getvalue()) |
2967 | |||
2968 | process.assert_not_called() | ||
2969 | print_balances.assert_called_once_with("user") | ||
2970 | |||
2971 | with self.subTest(action="print_orders"),\ | ||
2972 | mock.patch("helper.print_orders") as print_orders,\ | ||
2973 | mock.patch("helper.print_balances") as print_balances: | ||
2974 | helper.main_process_market("user", ["print_orders", "print_balances"]) | ||
2975 | |||
2976 | process.assert_not_called() | ||
2977 | print_orders.assert_called_once_with("user") | ||
2978 | print_balances.assert_called_once_with("user") | ||
2979 | |||
2980 | with self.subTest(action="unknown"),\ | ||
2981 | self.assertRaises(NotImplementedError): | ||
2982 | helper.main_process_market("user", ["unknown"]) | ||
2983 | |||
2984 | @mock.patch.object(helper, "psycopg2") | ||
2985 | def test_fetch_markets(self, psycopg2): | ||
2986 | connect_mock = mock.Mock() | ||
2987 | cursor_mock = mock.MagicMock() | ||
2988 | cursor_mock.__iter__.return_value = ["row_1", "row_2"] | ||
2989 | |||
2990 | connect_mock.cursor.return_value = cursor_mock | ||
2991 | psycopg2.connect.return_value = connect_mock | ||
2992 | |||
2993 | with self.subTest(user=None): | ||
2994 | rows = list(helper.main_fetch_markets({"foo": "bar"}, None)) | ||
2995 | 4388 | ||
2996 | psycopg2.connect.assert_called_once_with(foo="bar") | 4389 | def test_main(self): |
2997 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs") | 4390 | with self.subTest(parallel=False): |
4391 | with mock.patch("main.parse_args") as parse_args,\ | ||
4392 | mock.patch("main.parse_config") as parse_config,\ | ||
4393 | mock.patch("main.fetch_markets") as fetch_markets,\ | ||
4394 | mock.patch("main.process") as process: | ||
2998 | 4395 | ||
2999 | self.assertEqual(["row_1", "row_2"], rows) | 4396 | args_mock = mock.Mock() |
4397 | args_mock.parallel = False | ||
4398 | args_mock.config = "config" | ||
4399 | args_mock.user = "user" | ||
4400 | parse_args.return_value = args_mock | ||
3000 | 4401 | ||
3001 | psycopg2.connect.reset_mock() | 4402 | parse_config.return_value = ["pg_config", "report_path"] |
3002 | cursor_mock.execute.reset_mock() | ||
3003 | with self.subTest(user=1): | ||
3004 | rows = list(helper.main_fetch_markets({"foo": "bar"}, 1)) | ||
3005 | 4403 | ||
3006 | psycopg2.connect.assert_called_once_with(foo="bar") | 4404 | fetch_markets.return_value = [[3, "config1", 1], [1, "config2", 2]] |
3007 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1) | ||
3008 | 4405 | ||
3009 | self.assertEqual(["row_1", "row_2"], rows) | 4406 | main.main(["Foo", "Bar"]) |
3010 | 4407 | ||
3011 | @mock.patch.object(helper.sys, "exit") | 4408 | parse_args.assert_called_with(["Foo", "Bar"]) |
3012 | def test_main_parse_args(self, exit): | 4409 | parse_config.assert_called_with("config") |
3013 | with self.subTest(config="config.ini"): | 4410 | fetch_markets.assert_called_with("pg_config", "user") |
3014 | args = helper.main_parse_args([]) | ||
3015 | self.assertEqual("config.ini", args.config) | ||
3016 | self.assertFalse(args.before) | ||
3017 | self.assertFalse(args.after) | ||
3018 | self.assertFalse(args.debug) | ||
3019 | |||
3020 | args = helper.main_parse_args(["--before", "--after", "--debug"]) | ||
3021 | self.assertTrue(args.before) | ||
3022 | self.assertTrue(args.after) | ||
3023 | self.assertTrue(args.debug) | ||
3024 | |||
3025 | exit.assert_not_called() | ||
3026 | 4411 | ||
3027 | with self.subTest(config="inexistant"),\ | 4412 | self.assertEqual(2, process.call_count) |
3028 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 4413 | process.assert_has_calls([ |
3029 | args = helper.main_parse_args(["--config", "foo.bar"]) | 4414 | mock.call("config1", 3, 1, args_mock, "report_path", "pg_config"), |
3030 | exit.assert_called_once_with(1) | 4415 | mock.call("config2", 1, 2, args_mock, "report_path", "pg_config"), |
3031 | self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) | 4416 | ]) |
4417 | with self.subTest(parallel=True): | ||
4418 | with mock.patch("main.parse_args") as parse_args,\ | ||
4419 | mock.patch("main.parse_config") as parse_config,\ | ||
4420 | mock.patch("main.fetch_markets") as fetch_markets,\ | ||
4421 | mock.patch("main.process") as process,\ | ||
4422 | mock.patch("store.Portfolio.start_worker") as start: | ||
4423 | |||
4424 | args_mock = mock.Mock() | ||
4425 | args_mock.parallel = True | ||
4426 | args_mock.config = "config" | ||
4427 | args_mock.user = "user" | ||
4428 | parse_args.return_value = args_mock | ||
4429 | |||
4430 | parse_config.return_value = ["pg_config", "report_path"] | ||
4431 | |||
4432 | fetch_markets.return_value = [[3, "config1", 1], [1, "config2", 2]] | ||
4433 | |||
4434 | main.main(["Foo", "Bar"]) | ||
4435 | |||
4436 | parse_args.assert_called_with(["Foo", "Bar"]) | ||
4437 | parse_config.assert_called_with("config") | ||
4438 | fetch_markets.assert_called_with("pg_config", "user") | ||
4439 | |||
4440 | start.assert_called_once_with() | ||
4441 | self.assertEqual(2, process.call_count) | ||
4442 | process.assert_has_calls([ | ||
4443 | mock.call.__bool__(), | ||
4444 | mock.call("config1", 3, 1, args_mock, "report_path", "pg_config"), | ||
4445 | mock.call.__bool__(), | ||
4446 | mock.call("config2", 1, 2, args_mock, "report_path", "pg_config"), | ||
4447 | ]) | ||
3032 | 4448 | ||
3033 | @mock.patch.object(helper.sys, "exit") | 4449 | @mock.patch.object(main.sys, "exit") |
3034 | @mock.patch("helper.configparser") | 4450 | @mock.patch("main.configparser") |
3035 | @mock.patch("helper.os") | 4451 | @mock.patch("main.os") |
3036 | def test_main_parse_config(self, os, configparser, exit): | 4452 | def test_parse_config(self, os, configparser, exit): |
3037 | with self.subTest(pg_config=True, report_path=None): | 4453 | with self.subTest(pg_config=True, report_path=None): |
3038 | config_mock = mock.MagicMock() | 4454 | config_mock = mock.MagicMock() |
3039 | configparser.ConfigParser.return_value = config_mock | 4455 | configparser.ConfigParser.return_value = config_mock |
@@ -3043,7 +4459,7 @@ class HelperTest(WebMockTestCase): | |||
3043 | config_mock.__contains__.side_effect = config | 4459 | config_mock.__contains__.side_effect = config |
3044 | config_mock.__getitem__.return_value = "pg_config" | 4460 | config_mock.__getitem__.return_value = "pg_config" |
3045 | 4461 | ||
3046 | result = helper.main_parse_config("configfile") | 4462 | result = main.parse_config("configfile") |
3047 | 4463 | ||
3048 | config_mock.read.assert_called_with("configfile") | 4464 | config_mock.read.assert_called_with("configfile") |
3049 | 4465 | ||
@@ -3061,7 +4477,7 @@ class HelperTest(WebMockTestCase): | |||
3061 | ] | 4477 | ] |
3062 | 4478 | ||
3063 | os.path.exists.return_value = False | 4479 | os.path.exists.return_value = False |
3064 | result = helper.main_parse_config("configfile") | 4480 | result = main.parse_config("configfile") |
3065 | 4481 | ||
3066 | config_mock.read.assert_called_with("configfile") | 4482 | config_mock.read.assert_called_with("configfile") |
3067 | self.assertEqual(["pg_config", "report_path"], result) | 4483 | self.assertEqual(["pg_config", "report_path"], result) |
@@ -3072,46 +4488,71 @@ class HelperTest(WebMockTestCase): | |||
3072 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 4488 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
3073 | config_mock = mock.MagicMock() | 4489 | config_mock = mock.MagicMock() |
3074 | configparser.ConfigParser.return_value = config_mock | 4490 | configparser.ConfigParser.return_value = config_mock |
3075 | result = helper.main_parse_config("configfile") | 4491 | result = main.parse_config("configfile") |
3076 | 4492 | ||
3077 | config_mock.read.assert_called_with("configfile") | 4493 | config_mock.read.assert_called_with("configfile") |
3078 | exit.assert_called_once_with(1) | 4494 | exit.assert_called_once_with(1) |
3079 | self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) | 4495 | self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) |
3080 | 4496 | ||
4497 | @mock.patch.object(main.sys, "exit") | ||
4498 | def test_parse_args(self, exit): | ||
4499 | with self.subTest(config="config.ini"): | ||
4500 | args = main.parse_args([]) | ||
4501 | self.assertEqual("config.ini", args.config) | ||
4502 | self.assertFalse(args.before) | ||
4503 | self.assertFalse(args.after) | ||
4504 | self.assertFalse(args.debug) | ||
3081 | 4505 | ||
3082 | def test_print_orders(self): | 4506 | args = main.parse_args(["--before", "--after", "--debug"]) |
3083 | helper.print_orders(self.m) | 4507 | self.assertTrue(args.before) |
4508 | self.assertTrue(args.after) | ||
4509 | self.assertTrue(args.debug) | ||
3084 | 4510 | ||
3085 | self.m.report.log_stage.assert_called_with("print_orders") | 4511 | exit.assert_not_called() |
3086 | self.m.balances.fetch_balances.assert_called_with(tag="print_orders") | 4512 | |
3087 | self.m.prepare_trades.assert_called_with(base_currency="BTC", | 4513 | with self.subTest(config="inexistant"),\ |
3088 | compute_value="average") | 4514 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
3089 | self.m.trades.prepare_orders.assert_called_with(compute_value="average") | 4515 | args = main.parse_args(["--config", "foo.bar"]) |
4516 | exit.assert_called_once_with(1) | ||
4517 | self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) | ||
4518 | |||
4519 | @mock.patch.object(main, "psycopg2") | ||
4520 | def test_fetch_markets(self, psycopg2): | ||
4521 | connect_mock = mock.Mock() | ||
4522 | cursor_mock = mock.MagicMock() | ||
4523 | cursor_mock.__iter__.return_value = ["row_1", "row_2"] | ||
4524 | |||
4525 | connect_mock.cursor.return_value = cursor_mock | ||
4526 | psycopg2.connect.return_value = connect_mock | ||
4527 | |||
4528 | with self.subTest(user=None): | ||
4529 | rows = list(main.fetch_markets({"foo": "bar"}, None)) | ||
4530 | |||
4531 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
4532 | cursor_mock.execute.assert_called_once_with("SELECT id,config,user_id FROM market_configs") | ||
4533 | |||
4534 | self.assertEqual(["row_1", "row_2"], rows) | ||
4535 | |||
4536 | psycopg2.connect.reset_mock() | ||
4537 | cursor_mock.execute.reset_mock() | ||
4538 | with self.subTest(user=1): | ||
4539 | rows = list(main.fetch_markets({"foo": "bar"}, 1)) | ||
4540 | |||
4541 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
4542 | cursor_mock.execute.assert_called_once_with("SELECT id,config,user_id FROM market_configs WHERE user_id = %s", 1) | ||
4543 | |||
4544 | self.assertEqual(["row_1", "row_2"], rows) | ||
3090 | 4545 | ||
3091 | def test_print_balances(self): | ||
3092 | self.m.balances.in_currency.return_value = { | ||
3093 | "BTC": portfolio.Amount("BTC", "0.65"), | ||
3094 | "ETH": portfolio.Amount("BTC", "0.3"), | ||
3095 | } | ||
3096 | |||
3097 | helper.print_balances(self.m) | ||
3098 | |||
3099 | self.m.report.log_stage.assert_called_once_with("print_balances") | ||
3100 | self.m.balances.fetch_balances.assert_called_with() | ||
3101 | self.m.report.print_log.assert_has_calls([ | ||
3102 | mock.call("total:"), | ||
3103 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
3104 | ]) | ||
3105 | 4546 | ||
3106 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 4547 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
3107 | class ProcessorTest(WebMockTestCase): | 4548 | class ProcessorTest(WebMockTestCase): |
3108 | def test_values(self): | 4549 | def test_values(self): |
3109 | processor = helper.Processor(self.m) | 4550 | processor = market.Processor(self.m) |
3110 | 4551 | ||
3111 | self.assertEqual(self.m, processor.market) | 4552 | self.assertEqual(self.m, processor.market) |
3112 | 4553 | ||
3113 | def test_run_action(self): | 4554 | def test_run_action(self): |
3114 | processor = helper.Processor(self.m) | 4555 | processor = market.Processor(self.m) |
3115 | 4556 | ||
3116 | with mock.patch.object(processor, "parse_args") as parse_args: | 4557 | with mock.patch.object(processor, "parse_args") as parse_args: |
3117 | method_mock = mock.Mock() | 4558 | method_mock = mock.Mock() |
@@ -3125,10 +4566,10 @@ class ProcessorTest(WebMockTestCase): | |||
3125 | 4566 | ||
3126 | processor.run_action("wait_for_recent", "bar", "baz") | 4567 | processor.run_action("wait_for_recent", "bar", "baz") |
3127 | 4568 | ||
3128 | method_mock.assert_called_with(self.m, foo="bar") | 4569 | method_mock.assert_called_with(foo="bar") |
3129 | 4570 | ||
3130 | def test_select_step(self): | 4571 | def test_select_step(self): |
3131 | processor = helper.Processor(self.m) | 4572 | processor = market.Processor(self.m) |
3132 | 4573 | ||
3133 | scenario = processor.scenarios["sell_all"] | 4574 | scenario = processor.scenarios["sell_all"] |
3134 | 4575 | ||
@@ -3141,9 +4582,9 @@ class ProcessorTest(WebMockTestCase): | |||
3141 | with self.assertRaises(TypeError): | 4582 | with self.assertRaises(TypeError): |
3142 | processor.select_steps(scenario, ["wait"]) | 4583 | processor.select_steps(scenario, ["wait"]) |
3143 | 4584 | ||
3144 | @mock.patch("helper.Processor.process_step") | 4585 | @mock.patch("market.Processor.process_step") |
3145 | def test_process(self, process_step): | 4586 | def test_process(self, process_step): |
3146 | processor = helper.Processor(self.m) | 4587 | processor = market.Processor(self.m) |
3147 | 4588 | ||
3148 | processor.process("sell_all", foo="bar") | 4589 | processor.process("sell_all", foo="bar") |
3149 | self.assertEqual(3, process_step.call_count) | 4590 | self.assertEqual(3, process_step.call_count) |
@@ -3162,13 +4603,13 @@ class ProcessorTest(WebMockTestCase): | |||
3162 | 4603 | ||
3163 | def test_method_arguments(self): | 4604 | def test_method_arguments(self): |
3164 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) | 4605 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) |
3165 | m = market.Market(ccxt) | 4606 | m = market.Market(ccxt, self.market_args()) |
3166 | 4607 | ||
3167 | processor = helper.Processor(m) | 4608 | processor = market.Processor(m) |
3168 | 4609 | ||
3169 | method, arguments = processor.method_arguments("wait_for_recent") | 4610 | method, arguments = processor.method_arguments("wait_for_recent") |
3170 | self.assertEqual(portfolio.Portfolio.wait_for_recent, method) | 4611 | self.assertEqual(market.Portfolio.wait_for_recent, method) |
3171 | self.assertEqual(["delta"], arguments) | 4612 | self.assertEqual(["delta", "poll"], arguments) |
3172 | 4613 | ||
3173 | method, arguments = processor.method_arguments("prepare_trades") | 4614 | method, arguments = processor.method_arguments("prepare_trades") |
3174 | self.assertEqual(m.prepare_trades, method) | 4615 | self.assertEqual(m.prepare_trades, method) |
@@ -3190,7 +4631,7 @@ class ProcessorTest(WebMockTestCase): | |||
3190 | self.assertEqual(m.trades.close_trades, method) | 4631 | self.assertEqual(m.trades.close_trades, method) |
3191 | 4632 | ||
3192 | def test_process_step(self): | 4633 | def test_process_step(self): |
3193 | processor = helper.Processor(self.m) | 4634 | processor = market.Processor(self.m) |
3194 | 4635 | ||
3195 | with mock.patch.object(processor, "run_action") as run_action: | 4636 | with mock.patch.object(processor, "run_action") as run_action: |
3196 | step = processor.scenarios["sell_needed"][1] | 4637 | step = processor.scenarios["sell_needed"][1] |
@@ -3224,7 +4665,7 @@ class ProcessorTest(WebMockTestCase): | |||
3224 | self.m.balances.fetch_balances.assert_not_called() | 4665 | self.m.balances.fetch_balances.assert_not_called() |
3225 | 4666 | ||
3226 | def test_parse_args(self): | 4667 | def test_parse_args(self): |
3227 | processor = helper.Processor(self.m) | 4668 | processor = market.Processor(self.m) |
3228 | 4669 | ||
3229 | with mock.patch.object(processor, "method_arguments") as method_arguments: | 4670 | with mock.patch.object(processor, "method_arguments") as method_arguments: |
3230 | method_mock = mock.Mock() | 4671 | method_mock = mock.Mock() |
@@ -3350,7 +4791,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3350 | market = mock.Mock() | 4791 | market = mock.Mock() |
3351 | market.fetch_all_balances.return_value = fetch_balance | 4792 | market.fetch_all_balances.return_value = fetch_balance |
3352 | market.fetch_ticker.side_effect = fetch_ticker | 4793 | market.fetch_ticker.side_effect = fetch_ticker |
3353 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): | 4794 | with mock.patch.object(market.Portfolio, "repartition", return_value=repartition): |
3354 | # Action 1 | 4795 | # Action 1 |
3355 | helper.prepare_trades(market) | 4796 | helper.prepare_trades(market) |
3356 | 4797 | ||
@@ -3429,7 +4870,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3429 | "amount": "10", "total": "1" | 4870 | "amount": "10", "total": "1" |
3430 | } | 4871 | } |
3431 | ] | 4872 | ] |
3432 | with mock.patch.object(portfolio.time, "sleep") as sleep: | 4873 | with mock.patch.object(market.time, "sleep") as sleep: |
3433 | # Action 4 | 4874 | # Action 4 |
3434 | helper.follow_orders(verbose=False) | 4875 | helper.follow_orders(verbose=False) |
3435 | 4876 | ||
@@ -3470,7 +4911,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3470 | } | 4911 | } |
3471 | market.fetch_all_balances.return_value = fetch_balance | 4912 | market.fetch_all_balances.return_value = fetch_balance |
3472 | 4913 | ||
3473 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): | 4914 | with mock.patch.object(market.Portfolio, "repartition", return_value=repartition): |
3474 | # Action 5 | 4915 | # Action 5 |
3475 | helper.prepare_trades(market, only="acquire", compute_value="average") | 4916 | helper.prepare_trades(market, only="acquire", compute_value="average") |
3476 | 4917 | ||
@@ -3542,7 +4983,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3542 | # TODO | 4983 | # TODO |
3543 | # portfolio.TradeStore.run_orders() | 4984 | # portfolio.TradeStore.run_orders() |
3544 | 4985 | ||
3545 | with mock.patch.object(portfolio.time, "sleep") as sleep: | 4986 | with mock.patch.object(market.time, "sleep") as sleep: |
3546 | # Action 8 | 4987 | # Action 8 |
3547 | helper.follow_orders(verbose=False) | 4988 | helper.follow_orders(verbose=False) |
3548 | 4989 | ||
diff --git a/test_samples/poloniexETest.test_fetch_all_balances.1.json b/test_samples/poloniexETest.test_fetch_all_balances.1.json new file mode 100644 index 0000000..b04648c --- /dev/null +++ b/test_samples/poloniexETest.test_fetch_all_balances.1.json | |||
@@ -0,0 +1,1459 @@ | |||
1 | { | ||
2 | "1CR": { | ||
3 | "available": "0.00000000", | ||
4 | "onOrders": "0.00000000", | ||
5 | "btcValue": "0.00000000" | ||
6 | }, | ||
7 | "ABY": { | ||
8 | "available": "0.00000000", | ||
9 | "onOrders": "0.00000000", | ||
10 | "btcValue": "0.00000000" | ||
11 | }, | ||
12 | "AC": { | ||
13 | "available": "0.00000000", | ||
14 | "onOrders": "0.00000000", | ||
15 | "btcValue": "0.00000000" | ||
16 | }, | ||
17 | "ACH": { | ||
18 | "available": "0.00000000", | ||
19 | "onOrders": "0.00000000", | ||
20 | "btcValue": "0.00000000" | ||
21 | }, | ||
22 | "ADN": { | ||
23 | "available": "0.00000000", | ||
24 | "onOrders": "0.00000000", | ||
25 | "btcValue": "0.00000000" | ||
26 | }, | ||
27 | "AEON": { | ||
28 | "available": "0.00000000", | ||
29 | "onOrders": "0.00000000", | ||
30 | "btcValue": "0.00000000" | ||
31 | }, | ||
32 | "AERO": { | ||
33 | "available": "0.00000000", | ||
34 | "onOrders": "0.00000000", | ||
35 | "btcValue": "0.00000000" | ||
36 | }, | ||
37 | "AIR": { | ||
38 | "available": "0.00000000", | ||
39 | "onOrders": "0.00000000", | ||
40 | "btcValue": "0.00000000" | ||
41 | }, | ||
42 | "AMP": { | ||
43 | "available": "0.00000000", | ||
44 | "onOrders": "0.00000000", | ||
45 | "btcValue": "0.00000000" | ||
46 | }, | ||
47 | "APH": { | ||
48 | "available": "0.00000000", | ||
49 | "onOrders": "0.00000000", | ||
50 | "btcValue": "0.00000000" | ||
51 | }, | ||
52 | "ARCH": { | ||
53 | "available": "0.00000000", | ||
54 | "onOrders": "0.00000000", | ||
55 | "btcValue": "0.00000000" | ||
56 | }, | ||
57 | "ARDR": { | ||
58 | "available": "0.00000000", | ||
59 | "onOrders": "0.00000000", | ||
60 | "btcValue": "0.00000000" | ||
61 | }, | ||
62 | "AUR": { | ||
63 | "available": "0.00000000", | ||
64 | "onOrders": "0.00000000", | ||
65 | "btcValue": "0.00000000" | ||
66 | }, | ||
67 | "AXIS": { | ||
68 | "available": "0.00000000", | ||
69 | "onOrders": "0.00000000", | ||
70 | "btcValue": "0.00000000" | ||
71 | }, | ||
72 | "BALLS": { | ||
73 | "available": "0.00000000", | ||
74 | "onOrders": "0.00000000", | ||
75 | "btcValue": "0.00000000" | ||
76 | }, | ||
77 | "BANK": { | ||
78 | "available": "0.00000000", | ||
79 | "onOrders": "0.00000000", | ||
80 | "btcValue": "0.00000000" | ||
81 | }, | ||
82 | "BBL": { | ||
83 | "available": "0.00000000", | ||
84 | "onOrders": "0.00000000", | ||
85 | "btcValue": "0.00000000" | ||
86 | }, | ||
87 | "BBR": { | ||
88 | "available": "0.00000000", | ||
89 | "onOrders": "0.00000000", | ||
90 | "btcValue": "0.00000000" | ||
91 | }, | ||
92 | "BCC": { | ||
93 | "available": "0.00000000", | ||
94 | "onOrders": "0.00000000", | ||
95 | "btcValue": "0.00000000" | ||
96 | }, | ||
97 | "BCH": { | ||
98 | "available": "0.00000000", | ||
99 | "onOrders": "0.00000000", | ||
100 | "btcValue": "0.00000000" | ||
101 | }, | ||
102 | "BCN": { | ||
103 | "available": "0.00000000", | ||
104 | "onOrders": "0.00000000", | ||
105 | "btcValue": "0.00000000" | ||
106 | }, | ||
107 | "BCY": { | ||
108 | "available": "0.00000000", | ||
109 | "onOrders": "0.00000000", | ||
110 | "btcValue": "0.00000000" | ||
111 | }, | ||
112 | "BDC": { | ||
113 | "available": "0.00000000", | ||
114 | "onOrders": "0.00000000", | ||
115 | "btcValue": "0.00000000" | ||
116 | }, | ||
117 | "BDG": { | ||
118 | "available": "0.00000000", | ||
119 | "onOrders": "0.00000000", | ||
120 | "btcValue": "0.00000000" | ||
121 | }, | ||
122 | "BELA": { | ||
123 | "available": "0.00000000", | ||
124 | "onOrders": "0.00000000", | ||
125 | "btcValue": "0.00000000" | ||
126 | }, | ||
127 | "BITCNY": { | ||
128 | "available": "0.00000000", | ||
129 | "onOrders": "0.00000000", | ||
130 | "btcValue": "0.00000000" | ||
131 | }, | ||
132 | "BITS": { | ||
133 | "available": "0.00000000", | ||
134 | "onOrders": "0.00000000", | ||
135 | "btcValue": "0.00000000" | ||
136 | }, | ||
137 | "BITUSD": { | ||
138 | "available": "0.00000000", | ||
139 | "onOrders": "0.00000000", | ||
140 | "btcValue": "0.00000000" | ||
141 | }, | ||
142 | "BLK": { | ||
143 | "available": "159.83673869", | ||
144 | "onOrders": "0.00000000", | ||
145 | "btcValue": "0.00562305" | ||
146 | }, | ||
147 | "BLOCK": { | ||
148 | "available": "0.00000000", | ||
149 | "onOrders": "0.00000000", | ||
150 | "btcValue": "0.00000000" | ||
151 | }, | ||
152 | "BLU": { | ||
153 | "available": "0.00000000", | ||
154 | "onOrders": "0.00000000", | ||
155 | "btcValue": "0.00000000" | ||
156 | }, | ||
157 | "BNS": { | ||
158 | "available": "0.00000000", | ||
159 | "onOrders": "0.00000000", | ||
160 | "btcValue": "0.00000000" | ||
161 | }, | ||
162 | "BONES": { | ||
163 | "available": "0.00000000", | ||
164 | "onOrders": "0.00000000", | ||
165 | "btcValue": "0.00000000" | ||
166 | }, | ||
167 | "BOST": { | ||
168 | "available": "0.00000000", | ||
169 | "onOrders": "0.00000000", | ||
170 | "btcValue": "0.00000000" | ||
171 | }, | ||
172 | "BTC": { | ||
173 | "available": "0.03025186", | ||
174 | "onOrders": "0.00000000", | ||
175 | "btcValue": "0.03025186" | ||
176 | }, | ||
177 | "BTCD": { | ||
178 | "available": "0.00000000", | ||
179 | "onOrders": "0.00000000", | ||
180 | "btcValue": "0.00000000" | ||
181 | }, | ||
182 | "BTCS": { | ||
183 | "available": "0.00000000", | ||
184 | "onOrders": "0.00000000", | ||
185 | "btcValue": "0.00000000" | ||
186 | }, | ||
187 | "BTM": { | ||
188 | "available": "0.00000000", | ||
189 | "onOrders": "0.00000000", | ||
190 | "btcValue": "0.00000000" | ||
191 | }, | ||
192 | "BTS": { | ||
193 | "available": "0.00000000", | ||
194 | "onOrders": "0.00000000", | ||
195 | "btcValue": "0.00000000" | ||
196 | }, | ||
197 | "BURN": { | ||
198 | "available": "0.00000000", | ||
199 | "onOrders": "0.00000000", | ||
200 | "btcValue": "0.00000000" | ||
201 | }, | ||
202 | "BURST": { | ||
203 | "available": "0.00000000", | ||
204 | "onOrders": "0.00000000", | ||
205 | "btcValue": "0.00000000" | ||
206 | }, | ||
207 | "C2": { | ||
208 | "available": "0.00000000", | ||
209 | "onOrders": "0.00000000", | ||
210 | "btcValue": "0.00000000" | ||
211 | }, | ||
212 | "CACH": { | ||
213 | "available": "0.00000000", | ||
214 | "onOrders": "0.00000000", | ||
215 | "btcValue": "0.00000000" | ||
216 | }, | ||
217 | "CAI": { | ||
218 | "available": "0.00000000", | ||
219 | "onOrders": "0.00000000", | ||
220 | "btcValue": "0.00000000" | ||
221 | }, | ||
222 | "CC": { | ||
223 | "available": "0.00000000", | ||
224 | "onOrders": "0.00000000", | ||
225 | "btcValue": "0.00000000" | ||
226 | }, | ||
227 | "CCN": { | ||
228 | "available": "0.00000000", | ||
229 | "onOrders": "0.00000000", | ||
230 | "btcValue": "0.00000000" | ||
231 | }, | ||
232 | "CGA": { | ||
233 | "available": "0.00000000", | ||
234 | "onOrders": "0.00000000", | ||
235 | "btcValue": "0.00000000" | ||
236 | }, | ||
237 | "CHA": { | ||
238 | "available": "0.00000000", | ||
239 | "onOrders": "0.00000000", | ||
240 | "btcValue": "0.00000000" | ||
241 | }, | ||
242 | "CINNI": { | ||
243 | "available": "0.00000000", | ||
244 | "onOrders": "0.00000000", | ||
245 | "btcValue": "0.00000000" | ||
246 | }, | ||
247 | "CLAM": { | ||
248 | "available": "0.00000000", | ||
249 | "onOrders": "0.00000000", | ||
250 | "btcValue": "0.00000000" | ||
251 | }, | ||
252 | "CNL": { | ||
253 | "available": "0.00000000", | ||
254 | "onOrders": "0.00000000", | ||
255 | "btcValue": "0.00000000" | ||
256 | }, | ||
257 | "CNMT": { | ||
258 | "available": "0.00000000", | ||
259 | "onOrders": "0.00000000", | ||
260 | "btcValue": "0.00000000" | ||
261 | }, | ||
262 | "CNOTE": { | ||
263 | "available": "0.00000000", | ||
264 | "onOrders": "0.00000000", | ||
265 | "btcValue": "0.00000000" | ||
266 | }, | ||
267 | "COMM": { | ||
268 | "available": "0.00000000", | ||
269 | "onOrders": "0.00000000", | ||
270 | "btcValue": "0.00000000" | ||
271 | }, | ||
272 | "CON": { | ||
273 | "available": "0.00000000", | ||
274 | "onOrders": "0.00000000", | ||
275 | "btcValue": "0.00000000" | ||
276 | }, | ||
277 | "CORG": { | ||
278 | "available": "0.00000000", | ||
279 | "onOrders": "0.00000000", | ||
280 | "btcValue": "0.00000000" | ||
281 | }, | ||
282 | "CRYPT": { | ||
283 | "available": "0.00000000", | ||
284 | "onOrders": "0.00000000", | ||
285 | "btcValue": "0.00000000" | ||
286 | }, | ||
287 | "CURE": { | ||
288 | "available": "0.00000000", | ||
289 | "onOrders": "0.00000000", | ||
290 | "btcValue": "0.00000000" | ||
291 | }, | ||
292 | "CVC": { | ||
293 | "available": "0.00000000", | ||
294 | "onOrders": "0.00000000", | ||
295 | "btcValue": "0.00000000" | ||
296 | }, | ||
297 | "CYC": { | ||
298 | "available": "0.00000000", | ||
299 | "onOrders": "0.00000000", | ||
300 | "btcValue": "0.00000000" | ||
301 | }, | ||
302 | "DAO": { | ||
303 | "available": "0.00000000", | ||
304 | "onOrders": "0.00000000", | ||
305 | "btcValue": "0.00000000" | ||
306 | }, | ||
307 | "DASH": { | ||
308 | "available": "0.00000000", | ||
309 | "onOrders": "0.00000000", | ||
310 | "btcValue": "0.00000000" | ||
311 | }, | ||
312 | "DCR": { | ||
313 | "available": "0.00000000", | ||
314 | "onOrders": "0.00000000", | ||
315 | "btcValue": "0.00000000" | ||
316 | }, | ||
317 | "DGB": { | ||
318 | "available": "0.00000000", | ||
319 | "onOrders": "0.00000000", | ||
320 | "btcValue": "0.00000000" | ||
321 | }, | ||
322 | "DICE": { | ||
323 | "available": "0.00000000", | ||
324 | "onOrders": "0.00000000", | ||
325 | "btcValue": "0.00000000" | ||
326 | }, | ||
327 | "DIEM": { | ||
328 | "available": "0.00000000", | ||
329 | "onOrders": "0.00000000", | ||
330 | "btcValue": "0.00000000" | ||
331 | }, | ||
332 | "DIME": { | ||
333 | "available": "0.00000000", | ||
334 | "onOrders": "0.00000000", | ||
335 | "btcValue": "0.00000000" | ||
336 | }, | ||
337 | "DIS": { | ||
338 | "available": "0.00000000", | ||
339 | "onOrders": "0.00000000", | ||
340 | "btcValue": "0.00000000" | ||
341 | }, | ||
342 | "DNS": { | ||
343 | "available": "0.00000000", | ||
344 | "onOrders": "0.00000000", | ||
345 | "btcValue": "0.00000000" | ||
346 | }, | ||
347 | "DOGE": { | ||
348 | "available": "0.00000000", | ||
349 | "onOrders": "0.00000000", | ||
350 | "btcValue": "0.00000000" | ||
351 | }, | ||
352 | "DRKC": { | ||
353 | "available": "0.00000000", | ||
354 | "onOrders": "0.00000000", | ||
355 | "btcValue": "0.00000000" | ||
356 | }, | ||
357 | "DRM": { | ||
358 | "available": "0.00000000", | ||
359 | "onOrders": "0.00000000", | ||
360 | "btcValue": "0.00000000" | ||
361 | }, | ||
362 | "DSH": { | ||
363 | "available": "0.00000000", | ||
364 | "onOrders": "0.00000000", | ||
365 | "btcValue": "0.00000000" | ||
366 | }, | ||
367 | "DVK": { | ||
368 | "available": "0.00000000", | ||
369 | "onOrders": "0.00000000", | ||
370 | "btcValue": "0.00000000" | ||
371 | }, | ||
372 | "EAC": { | ||
373 | "available": "0.00000000", | ||
374 | "onOrders": "0.00000000", | ||
375 | "btcValue": "0.00000000" | ||
376 | }, | ||
377 | "EBT": { | ||
378 | "available": "0.00000000", | ||
379 | "onOrders": "0.00000000", | ||
380 | "btcValue": "0.00000000" | ||
381 | }, | ||
382 | "ECC": { | ||
383 | "available": "0.00000000", | ||
384 | "onOrders": "0.00000000", | ||
385 | "btcValue": "0.00000000" | ||
386 | }, | ||
387 | "EFL": { | ||
388 | "available": "0.00000000", | ||
389 | "onOrders": "0.00000000", | ||
390 | "btcValue": "0.00000000" | ||
391 | }, | ||
392 | "EMC2": { | ||
393 | "available": "0.00000000", | ||
394 | "onOrders": "0.00000000", | ||
395 | "btcValue": "0.00000000" | ||
396 | }, | ||
397 | "EMO": { | ||
398 | "available": "0.00000000", | ||
399 | "onOrders": "0.00000000", | ||
400 | "btcValue": "0.00000000" | ||
401 | }, | ||
402 | "ENC": { | ||
403 | "available": "0.00000000", | ||
404 | "onOrders": "0.00000000", | ||
405 | "btcValue": "0.00000000" | ||
406 | }, | ||
407 | "ETC": { | ||
408 | "available": "0.00000000", | ||
409 | "onOrders": "0.00000000", | ||
410 | "btcValue": "0.00000000" | ||
411 | }, | ||
412 | "ETH": { | ||
413 | "available": "0.00000000", | ||
414 | "onOrders": "0.00000000", | ||
415 | "btcValue": "0.00000000" | ||
416 | }, | ||
417 | "eTOK": { | ||
418 | "available": "0.00000000", | ||
419 | "onOrders": "0.00000000", | ||
420 | "btcValue": "0.00000000" | ||
421 | }, | ||
422 | "EXE": { | ||
423 | "available": "0.00000000", | ||
424 | "onOrders": "0.00000000", | ||
425 | "btcValue": "0.00000000" | ||
426 | }, | ||
427 | "EXP": { | ||
428 | "available": "0.00000000", | ||
429 | "onOrders": "0.00000000", | ||
430 | "btcValue": "0.00000000" | ||
431 | }, | ||
432 | "FAC": { | ||
433 | "available": "0.00000000", | ||
434 | "onOrders": "0.00000000", | ||
435 | "btcValue": "0.00000000" | ||
436 | }, | ||
437 | "FCN": { | ||
438 | "available": "0.00000000", | ||
439 | "onOrders": "0.00000000", | ||
440 | "btcValue": "0.00000000" | ||
441 | }, | ||
442 | "FCT": { | ||
443 | "available": "0.00000000", | ||
444 | "onOrders": "0.00000000", | ||
445 | "btcValue": "0.00000000" | ||
446 | }, | ||
447 | "FIBRE": { | ||
448 | "available": "0.00000000", | ||
449 | "onOrders": "0.00000000", | ||
450 | "btcValue": "0.00000000" | ||
451 | }, | ||
452 | "FLAP": { | ||
453 | "available": "0.00000000", | ||
454 | "onOrders": "0.00000000", | ||
455 | "btcValue": "0.00000000" | ||
456 | }, | ||
457 | "FLDC": { | ||
458 | "available": "0.00000000", | ||
459 | "onOrders": "0.00000000", | ||
460 | "btcValue": "0.00000000" | ||
461 | }, | ||
462 | "FLO": { | ||
463 | "available": "0.00000000", | ||
464 | "onOrders": "0.00000000", | ||
465 | "btcValue": "0.00000000" | ||
466 | }, | ||
467 | "FLT": { | ||
468 | "available": "0.00000000", | ||
469 | "onOrders": "0.00000000", | ||
470 | "btcValue": "0.00000000" | ||
471 | }, | ||
472 | "FOX": { | ||
473 | "available": "0.00000000", | ||
474 | "onOrders": "0.00000000", | ||
475 | "btcValue": "0.00000000" | ||
476 | }, | ||
477 | "FRAC": { | ||
478 | "available": "0.00000000", | ||
479 | "onOrders": "0.00000000", | ||
480 | "btcValue": "0.00000000" | ||
481 | }, | ||
482 | "FRK": { | ||
483 | "available": "0.00000000", | ||
484 | "onOrders": "0.00000000", | ||
485 | "btcValue": "0.00000000" | ||
486 | }, | ||
487 | "FRQ": { | ||
488 | "available": "0.00000000", | ||
489 | "onOrders": "0.00000000", | ||
490 | "btcValue": "0.00000000" | ||
491 | }, | ||
492 | "FVZ": { | ||
493 | "available": "0.00000000", | ||
494 | "onOrders": "0.00000000", | ||
495 | "btcValue": "0.00000000" | ||
496 | }, | ||
497 | "FZ": { | ||
498 | "available": "0.00000000", | ||
499 | "onOrders": "0.00000000", | ||
500 | "btcValue": "0.00000000" | ||
501 | }, | ||
502 | "FZN": { | ||
503 | "available": "0.00000000", | ||
504 | "onOrders": "0.00000000", | ||
505 | "btcValue": "0.00000000" | ||
506 | }, | ||
507 | "GAME": { | ||
508 | "available": "0.00000000", | ||
509 | "onOrders": "0.00000000", | ||
510 | "btcValue": "0.00000000" | ||
511 | }, | ||
512 | "GAP": { | ||
513 | "available": "0.00000000", | ||
514 | "onOrders": "0.00000000", | ||
515 | "btcValue": "0.00000000" | ||
516 | }, | ||
517 | "GAS": { | ||
518 | "available": "0.00000000", | ||
519 | "onOrders": "0.00000000", | ||
520 | "btcValue": "0.00000000" | ||
521 | }, | ||
522 | "GDN": { | ||
523 | "available": "0.00000000", | ||
524 | "onOrders": "0.00000000", | ||
525 | "btcValue": "0.00000000" | ||
526 | }, | ||
527 | "GEMZ": { | ||
528 | "available": "0.00000000", | ||
529 | "onOrders": "0.00000000", | ||
530 | "btcValue": "0.00000000" | ||
531 | }, | ||
532 | "GEO": { | ||
533 | "available": "0.00000000", | ||
534 | "onOrders": "0.00000000", | ||
535 | "btcValue": "0.00000000" | ||
536 | }, | ||
537 | "GIAR": { | ||
538 | "available": "0.00000000", | ||
539 | "onOrders": "0.00000000", | ||
540 | "btcValue": "0.00000000" | ||
541 | }, | ||
542 | "GLB": { | ||
543 | "available": "0.00000000", | ||
544 | "onOrders": "0.00000000", | ||
545 | "btcValue": "0.00000000" | ||
546 | }, | ||
547 | "GML": { | ||
548 | "available": "0.00000000", | ||
549 | "onOrders": "0.00000000", | ||
550 | "btcValue": "0.00000000" | ||
551 | }, | ||
552 | "GNO": { | ||
553 | "available": "0.00000000", | ||
554 | "onOrders": "0.00000000", | ||
555 | "btcValue": "0.00000000" | ||
556 | }, | ||
557 | "GNS": { | ||
558 | "available": "0.00000000", | ||
559 | "onOrders": "0.00000000", | ||
560 | "btcValue": "0.00000000" | ||
561 | }, | ||
562 | "GNT": { | ||
563 | "available": "0.00000000", | ||
564 | "onOrders": "0.00000000", | ||
565 | "btcValue": "0.00000000" | ||
566 | }, | ||
567 | "GOLD": { | ||
568 | "available": "0.00000000", | ||
569 | "onOrders": "0.00000000", | ||
570 | "btcValue": "0.00000000" | ||
571 | }, | ||
572 | "GPC": { | ||
573 | "available": "0.00000000", | ||
574 | "onOrders": "0.00000000", | ||
575 | "btcValue": "0.00000000" | ||
576 | }, | ||
577 | "GPUC": { | ||
578 | "available": "0.00000000", | ||
579 | "onOrders": "0.00000000", | ||
580 | "btcValue": "0.00000000" | ||
581 | }, | ||
582 | "GRC": { | ||
583 | "available": "0.00000000", | ||
584 | "onOrders": "0.00000000", | ||
585 | "btcValue": "0.00000000" | ||
586 | }, | ||
587 | "GRCX": { | ||
588 | "available": "0.00000000", | ||
589 | "onOrders": "0.00000000", | ||
590 | "btcValue": "0.00000000" | ||
591 | }, | ||
592 | "GRS": { | ||
593 | "available": "0.00000000", | ||
594 | "onOrders": "0.00000000", | ||
595 | "btcValue": "0.00000000" | ||
596 | }, | ||
597 | "GUE": { | ||
598 | "available": "0.00000000", | ||
599 | "onOrders": "0.00000000", | ||
600 | "btcValue": "0.00000000" | ||
601 | }, | ||
602 | "H2O": { | ||
603 | "available": "0.00000000", | ||
604 | "onOrders": "0.00000000", | ||
605 | "btcValue": "0.00000000" | ||
606 | }, | ||
607 | "HIRO": { | ||
608 | "available": "0.00000000", | ||
609 | "onOrders": "0.00000000", | ||
610 | "btcValue": "0.00000000" | ||
611 | }, | ||
612 | "HOT": { | ||
613 | "available": "0.00000000", | ||
614 | "onOrders": "0.00000000", | ||
615 | "btcValue": "0.00000000" | ||
616 | }, | ||
617 | "HUC": { | ||
618 | "available": "0.00000000", | ||
619 | "onOrders": "0.00000000", | ||
620 | "btcValue": "0.00000000" | ||
621 | }, | ||
622 | "HUGE": { | ||
623 | "available": "0.00000000", | ||
624 | "onOrders": "0.00000000", | ||
625 | "btcValue": "0.00000000" | ||
626 | }, | ||
627 | "HVC": { | ||
628 | "available": "0.00000000", | ||
629 | "onOrders": "0.00000000", | ||
630 | "btcValue": "0.00000000" | ||
631 | }, | ||
632 | "HYP": { | ||
633 | "available": "0.00000000", | ||
634 | "onOrders": "0.00000000", | ||
635 | "btcValue": "0.00000000" | ||
636 | }, | ||
637 | "HZ": { | ||
638 | "available": "0.00000000", | ||
639 | "onOrders": "0.00000000", | ||
640 | "btcValue": "0.00000000" | ||
641 | }, | ||
642 | "IFC": { | ||
643 | "available": "0.00000000", | ||
644 | "onOrders": "0.00000000", | ||
645 | "btcValue": "0.00000000" | ||
646 | }, | ||
647 | "INDEX": { | ||
648 | "available": "0.00000000", | ||
649 | "onOrders": "0.00000000", | ||
650 | "btcValue": "0.00000000" | ||
651 | }, | ||
652 | "IOC": { | ||
653 | "available": "0.00000000", | ||
654 | "onOrders": "0.00000000", | ||
655 | "btcValue": "0.00000000" | ||
656 | }, | ||
657 | "ITC": { | ||
658 | "available": "0.00000000", | ||
659 | "onOrders": "0.00000000", | ||
660 | "btcValue": "0.00000000" | ||
661 | }, | ||
662 | "IXC": { | ||
663 | "available": "0.00000000", | ||
664 | "onOrders": "0.00000000", | ||
665 | "btcValue": "0.00000000" | ||
666 | }, | ||
667 | "JLH": { | ||
668 | "available": "0.00000000", | ||
669 | "onOrders": "0.00000000", | ||
670 | "btcValue": "0.00000000" | ||
671 | }, | ||
672 | "JPC": { | ||
673 | "available": "0.00000000", | ||
674 | "onOrders": "0.00000000", | ||
675 | "btcValue": "0.00000000" | ||
676 | }, | ||
677 | "JUG": { | ||
678 | "available": "0.00000000", | ||
679 | "onOrders": "0.00000000", | ||
680 | "btcValue": "0.00000000" | ||
681 | }, | ||
682 | "KDC": { | ||
683 | "available": "0.00000000", | ||
684 | "onOrders": "0.00000000", | ||
685 | "btcValue": "0.00000000" | ||
686 | }, | ||
687 | "KEY": { | ||
688 | "available": "0.00000000", | ||
689 | "onOrders": "0.00000000", | ||
690 | "btcValue": "0.00000000" | ||
691 | }, | ||
692 | "LBC": { | ||
693 | "available": "0.00000000", | ||
694 | "onOrders": "0.00000000", | ||
695 | "btcValue": "0.00000000" | ||
696 | }, | ||
697 | "LC": { | ||
698 | "available": "0.00000000", | ||
699 | "onOrders": "0.00000000", | ||
700 | "btcValue": "0.00000000" | ||
701 | }, | ||
702 | "LCL": { | ||
703 | "available": "0.00000000", | ||
704 | "onOrders": "0.00000000", | ||
705 | "btcValue": "0.00000000" | ||
706 | }, | ||
707 | "LEAF": { | ||
708 | "available": "0.00000000", | ||
709 | "onOrders": "0.00000000", | ||
710 | "btcValue": "0.00000000" | ||
711 | }, | ||
712 | "LGC": { | ||
713 | "available": "0.00000000", | ||
714 | "onOrders": "0.00000000", | ||
715 | "btcValue": "0.00000000" | ||
716 | }, | ||
717 | "LOL": { | ||
718 | "available": "0.00000000", | ||
719 | "onOrders": "0.00000000", | ||
720 | "btcValue": "0.00000000" | ||
721 | }, | ||
722 | "LOVE": { | ||
723 | "available": "0.00000000", | ||
724 | "onOrders": "0.00000000", | ||
725 | "btcValue": "0.00000000" | ||
726 | }, | ||
727 | "LQD": { | ||
728 | "available": "0.00000000", | ||
729 | "onOrders": "0.00000000", | ||
730 | "btcValue": "0.00000000" | ||
731 | }, | ||
732 | "LSK": { | ||
733 | "available": "0.00000000", | ||
734 | "onOrders": "0.00000000", | ||
735 | "btcValue": "0.00000000" | ||
736 | }, | ||
737 | "LTBC": { | ||
738 | "available": "0.00000000", | ||
739 | "onOrders": "0.00000000", | ||
740 | "btcValue": "0.00000000" | ||
741 | }, | ||
742 | "LTC": { | ||
743 | "available": "0.00000000", | ||
744 | "onOrders": "0.00000000", | ||
745 | "btcValue": "0.00000000" | ||
746 | }, | ||
747 | "LTCX": { | ||
748 | "available": "0.00000000", | ||
749 | "onOrders": "0.00000000", | ||
750 | "btcValue": "0.00000000" | ||
751 | }, | ||
752 | "MAID": { | ||
753 | "available": "0.00000000", | ||
754 | "onOrders": "0.00000000", | ||
755 | "btcValue": "0.00000000" | ||
756 | }, | ||
757 | "MAST": { | ||
758 | "available": "0.00000000", | ||
759 | "onOrders": "0.00000000", | ||
760 | "btcValue": "0.00000000" | ||
761 | }, | ||
762 | "MAX": { | ||
763 | "available": "0.00000000", | ||
764 | "onOrders": "0.00000000", | ||
765 | "btcValue": "0.00000000" | ||
766 | }, | ||
767 | "MCN": { | ||
768 | "available": "0.00000000", | ||
769 | "onOrders": "0.00000000", | ||
770 | "btcValue": "0.00000000" | ||
771 | }, | ||
772 | "MEC": { | ||
773 | "available": "0.00000000", | ||
774 | "onOrders": "0.00000000", | ||
775 | "btcValue": "0.00000000" | ||
776 | }, | ||
777 | "METH": { | ||
778 | "available": "0.00000000", | ||
779 | "onOrders": "0.00000000", | ||
780 | "btcValue": "0.00000000" | ||
781 | }, | ||
782 | "MIL": { | ||
783 | "available": "0.00000000", | ||
784 | "onOrders": "0.00000000", | ||
785 | "btcValue": "0.00000000" | ||
786 | }, | ||
787 | "MIN": { | ||
788 | "available": "0.00000000", | ||
789 | "onOrders": "0.00000000", | ||
790 | "btcValue": "0.00000000" | ||
791 | }, | ||
792 | "MINT": { | ||
793 | "available": "0.00000000", | ||
794 | "onOrders": "0.00000000", | ||
795 | "btcValue": "0.00000000" | ||
796 | }, | ||
797 | "MMC": { | ||
798 | "available": "0.00000000", | ||
799 | "onOrders": "0.00000000", | ||
800 | "btcValue": "0.00000000" | ||
801 | }, | ||
802 | "MMNXT": { | ||
803 | "available": "0.00000000", | ||
804 | "onOrders": "0.00000000", | ||
805 | "btcValue": "0.00000000" | ||
806 | }, | ||
807 | "MMXIV": { | ||
808 | "available": "0.00000000", | ||
809 | "onOrders": "0.00000000", | ||
810 | "btcValue": "0.00000000" | ||
811 | }, | ||
812 | "MNTA": { | ||
813 | "available": "0.00000000", | ||
814 | "onOrders": "0.00000000", | ||
815 | "btcValue": "0.00000000" | ||
816 | }, | ||
817 | "MON": { | ||
818 | "available": "0.00000000", | ||
819 | "onOrders": "0.00000000", | ||
820 | "btcValue": "0.00000000" | ||
821 | }, | ||
822 | "MRC": { | ||
823 | "available": "0.00000000", | ||
824 | "onOrders": "0.00000000", | ||
825 | "btcValue": "0.00000000" | ||
826 | }, | ||
827 | "MRS": { | ||
828 | "available": "0.00000000", | ||
829 | "onOrders": "0.00000000", | ||
830 | "btcValue": "0.00000000" | ||
831 | }, | ||
832 | "MTS": { | ||
833 | "available": "0.00000000", | ||
834 | "onOrders": "0.00000000", | ||
835 | "btcValue": "0.00000000" | ||
836 | }, | ||
837 | "MUN": { | ||
838 | "available": "0.00000000", | ||
839 | "onOrders": "0.00000000", | ||
840 | "btcValue": "0.00000000" | ||
841 | }, | ||
842 | "MYR": { | ||
843 | "available": "0.00000000", | ||
844 | "onOrders": "0.00000000", | ||
845 | "btcValue": "0.00000000" | ||
846 | }, | ||
847 | "MZC": { | ||
848 | "available": "0.00000000", | ||
849 | "onOrders": "0.00000000", | ||
850 | "btcValue": "0.00000000" | ||
851 | }, | ||
852 | "N5X": { | ||
853 | "available": "0.00000000", | ||
854 | "onOrders": "0.00000000", | ||
855 | "btcValue": "0.00000000" | ||
856 | }, | ||
857 | "NAS": { | ||
858 | "available": "0.00000000", | ||
859 | "onOrders": "0.00000000", | ||
860 | "btcValue": "0.00000000" | ||
861 | }, | ||
862 | "NAUT": { | ||
863 | "available": "0.00000000", | ||
864 | "onOrders": "0.00000000", | ||
865 | "btcValue": "0.00000000" | ||
866 | }, | ||
867 | "NAV": { | ||
868 | "available": "0.00000000", | ||
869 | "onOrders": "0.00000000", | ||
870 | "btcValue": "0.00000000" | ||
871 | }, | ||
872 | "NBT": { | ||
873 | "available": "0.00000000", | ||
874 | "onOrders": "0.00000000", | ||
875 | "btcValue": "0.00000000" | ||
876 | }, | ||
877 | "NEOS": { | ||
878 | "available": "0.00000000", | ||
879 | "onOrders": "0.00000000", | ||
880 | "btcValue": "0.00000000" | ||
881 | }, | ||
882 | "NL": { | ||
883 | "available": "0.00000000", | ||
884 | "onOrders": "0.00000000", | ||
885 | "btcValue": "0.00000000" | ||
886 | }, | ||
887 | "NMC": { | ||
888 | "available": "0.00000000", | ||
889 | "onOrders": "0.00000000", | ||
890 | "btcValue": "0.00000000" | ||
891 | }, | ||
892 | "NOBL": { | ||
893 | "available": "0.00000000", | ||
894 | "onOrders": "0.00000000", | ||
895 | "btcValue": "0.00000000" | ||
896 | }, | ||
897 | "NOTE": { | ||
898 | "available": "0.00000000", | ||
899 | "onOrders": "0.00000000", | ||
900 | "btcValue": "0.00000000" | ||
901 | }, | ||
902 | "NOXT": { | ||
903 | "available": "0.00000000", | ||
904 | "onOrders": "0.00000000", | ||
905 | "btcValue": "0.00000000" | ||
906 | }, | ||
907 | "NRS": { | ||
908 | "available": "0.00000000", | ||
909 | "onOrders": "0.00000000", | ||
910 | "btcValue": "0.00000000" | ||
911 | }, | ||
912 | "NSR": { | ||
913 | "available": "0.00000000", | ||
914 | "onOrders": "0.00000000", | ||
915 | "btcValue": "0.00000000" | ||
916 | }, | ||
917 | "NTX": { | ||
918 | "available": "0.00000000", | ||
919 | "onOrders": "0.00000000", | ||
920 | "btcValue": "0.00000000" | ||
921 | }, | ||
922 | "NXC": { | ||
923 | "available": "0.00000000", | ||
924 | "onOrders": "0.00000000", | ||
925 | "btcValue": "0.00000000" | ||
926 | }, | ||
927 | "NXT": { | ||
928 | "available": "0.00000000", | ||
929 | "onOrders": "0.00000000", | ||
930 | "btcValue": "0.00000000" | ||
931 | }, | ||
932 | "NXTI": { | ||
933 | "available": "0.00000000", | ||
934 | "onOrders": "0.00000000", | ||
935 | "btcValue": "0.00000000" | ||
936 | }, | ||
937 | "OMG": { | ||
938 | "available": "0.00000000", | ||
939 | "onOrders": "0.00000000", | ||
940 | "btcValue": "0.00000000" | ||
941 | }, | ||
942 | "OMNI": { | ||
943 | "available": "0.00000000", | ||
944 | "onOrders": "0.00000000", | ||
945 | "btcValue": "0.00000000" | ||
946 | }, | ||
947 | "OPAL": { | ||
948 | "available": "0.00000000", | ||
949 | "onOrders": "0.00000000", | ||
950 | "btcValue": "0.00000000" | ||
951 | }, | ||
952 | "PAND": { | ||
953 | "available": "0.00000000", | ||
954 | "onOrders": "0.00000000", | ||
955 | "btcValue": "0.00000000" | ||
956 | }, | ||
957 | "PASC": { | ||
958 | "available": "0.00000000", | ||
959 | "onOrders": "0.00000000", | ||
960 | "btcValue": "0.00000000" | ||
961 | }, | ||
962 | "PAWN": { | ||
963 | "available": "0.00000000", | ||
964 | "onOrders": "0.00000000", | ||
965 | "btcValue": "0.00000000" | ||
966 | }, | ||
967 | "PIGGY": { | ||
968 | "available": "0.00000000", | ||
969 | "onOrders": "0.00000000", | ||
970 | "btcValue": "0.00000000" | ||
971 | }, | ||
972 | "PINK": { | ||
973 | "available": "0.00000000", | ||
974 | "onOrders": "0.00000000", | ||
975 | "btcValue": "0.00000000" | ||
976 | }, | ||
977 | "PLX": { | ||
978 | "available": "0.00000000", | ||
979 | "onOrders": "0.00000000", | ||
980 | "btcValue": "0.00000000" | ||
981 | }, | ||
982 | "PMC": { | ||
983 | "available": "0.00000000", | ||
984 | "onOrders": "0.00000000", | ||
985 | "btcValue": "0.00000000" | ||
986 | }, | ||
987 | "POT": { | ||
988 | "available": "0.00000000", | ||
989 | "onOrders": "0.00000000", | ||
990 | "btcValue": "0.00000000" | ||
991 | }, | ||
992 | "PPC": { | ||
993 | "available": "0.00000000", | ||
994 | "onOrders": "0.00000000", | ||
995 | "btcValue": "0.00000000" | ||
996 | }, | ||
997 | "PRC": { | ||
998 | "available": "0.00000000", | ||
999 | "onOrders": "0.00000000", | ||
1000 | "btcValue": "0.00000000" | ||
1001 | }, | ||
1002 | "PRT": { | ||
1003 | "available": "0.00000000", | ||
1004 | "onOrders": "0.00000000", | ||
1005 | "btcValue": "0.00000000" | ||
1006 | }, | ||
1007 | "PTS": { | ||
1008 | "available": "0.00000000", | ||
1009 | "onOrders": "0.00000000", | ||
1010 | "btcValue": "0.00000000" | ||
1011 | }, | ||
1012 | "Q2C": { | ||
1013 | "available": "0.00000000", | ||
1014 | "onOrders": "0.00000000", | ||
1015 | "btcValue": "0.00000000" | ||
1016 | }, | ||
1017 | "QBK": { | ||
1018 | "available": "0.00000000", | ||
1019 | "onOrders": "0.00000000", | ||
1020 | "btcValue": "0.00000000" | ||
1021 | }, | ||
1022 | "QCN": { | ||
1023 | "available": "0.00000000", | ||
1024 | "onOrders": "0.00000000", | ||
1025 | "btcValue": "0.00000000" | ||
1026 | }, | ||
1027 | "QORA": { | ||
1028 | "available": "0.00000000", | ||
1029 | "onOrders": "0.00000000", | ||
1030 | "btcValue": "0.00000000" | ||
1031 | }, | ||
1032 | "QTL": { | ||
1033 | "available": "0.00000000", | ||
1034 | "onOrders": "0.00000000", | ||
1035 | "btcValue": "0.00000000" | ||
1036 | }, | ||
1037 | "RADS": { | ||
1038 | "available": "0.00000000", | ||
1039 | "onOrders": "0.00000000", | ||
1040 | "btcValue": "0.00000000" | ||
1041 | }, | ||
1042 | "RBY": { | ||
1043 | "available": "0.00000000", | ||
1044 | "onOrders": "0.00000000", | ||
1045 | "btcValue": "0.00000000" | ||
1046 | }, | ||
1047 | "RDD": { | ||
1048 | "available": "0.00000000", | ||
1049 | "onOrders": "0.00000000", | ||
1050 | "btcValue": "0.00000000" | ||
1051 | }, | ||
1052 | "REP": { | ||
1053 | "available": "0.00000000", | ||
1054 | "onOrders": "0.00000000", | ||
1055 | "btcValue": "0.00000000" | ||
1056 | }, | ||
1057 | "RIC": { | ||
1058 | "available": "0.00000000", | ||
1059 | "onOrders": "0.00000000", | ||
1060 | "btcValue": "0.00000000" | ||
1061 | }, | ||
1062 | "RZR": { | ||
1063 | "available": "0.00000000", | ||
1064 | "onOrders": "0.00000000", | ||
1065 | "btcValue": "0.00000000" | ||
1066 | }, | ||
1067 | "SBD": { | ||
1068 | "available": "0.00000000", | ||
1069 | "onOrders": "0.00000000", | ||
1070 | "btcValue": "0.00000000" | ||
1071 | }, | ||
1072 | "SC": { | ||
1073 | "available": "0.00000000", | ||
1074 | "onOrders": "0.00000000", | ||
1075 | "btcValue": "0.00000000" | ||
1076 | }, | ||
1077 | "SDC": { | ||
1078 | "available": "0.00000000", | ||
1079 | "onOrders": "0.00000000", | ||
1080 | "btcValue": "0.00000000" | ||
1081 | }, | ||
1082 | "SHIBE": { | ||
1083 | "available": "0.00000000", | ||
1084 | "onOrders": "0.00000000", | ||
1085 | "btcValue": "0.00000000" | ||
1086 | }, | ||
1087 | "SHOPX": { | ||
1088 | "available": "0.00000000", | ||
1089 | "onOrders": "0.00000000", | ||
1090 | "btcValue": "0.00000000" | ||
1091 | }, | ||
1092 | "SILK": { | ||
1093 | "available": "0.00000000", | ||
1094 | "onOrders": "0.00000000", | ||
1095 | "btcValue": "0.00000000" | ||
1096 | }, | ||
1097 | "SJCX": { | ||
1098 | "available": "0.00000000", | ||
1099 | "onOrders": "0.00000000", | ||
1100 | "btcValue": "0.00000000" | ||
1101 | }, | ||
1102 | "SLR": { | ||
1103 | "available": "0.00000000", | ||
1104 | "onOrders": "0.00000000", | ||
1105 | "btcValue": "0.00000000" | ||
1106 | }, | ||
1107 | "SMC": { | ||
1108 | "available": "0.00000000", | ||
1109 | "onOrders": "0.00000000", | ||
1110 | "btcValue": "0.00000000" | ||
1111 | }, | ||
1112 | "SOC": { | ||
1113 | "available": "0.00000000", | ||
1114 | "onOrders": "0.00000000", | ||
1115 | "btcValue": "0.00000000" | ||
1116 | }, | ||
1117 | "SPA": { | ||
1118 | "available": "0.00000000", | ||
1119 | "onOrders": "0.00000000", | ||
1120 | "btcValue": "0.00000000" | ||
1121 | }, | ||
1122 | "SQL": { | ||
1123 | "available": "0.00000000", | ||
1124 | "onOrders": "0.00000000", | ||
1125 | "btcValue": "0.00000000" | ||
1126 | }, | ||
1127 | "SRCC": { | ||
1128 | "available": "0.00000000", | ||
1129 | "onOrders": "0.00000000", | ||
1130 | "btcValue": "0.00000000" | ||
1131 | }, | ||
1132 | "SRG": { | ||
1133 | "available": "0.00000000", | ||
1134 | "onOrders": "0.00000000", | ||
1135 | "btcValue": "0.00000000" | ||
1136 | }, | ||
1137 | "SSD": { | ||
1138 | "available": "0.00000000", | ||
1139 | "onOrders": "0.00000000", | ||
1140 | "btcValue": "0.00000000" | ||
1141 | }, | ||
1142 | "STEEM": { | ||
1143 | "available": "0.00000000", | ||
1144 | "onOrders": "0.00000000", | ||
1145 | "btcValue": "0.00000000" | ||
1146 | }, | ||
1147 | "STORJ": { | ||
1148 | "available": "0.00000000", | ||
1149 | "onOrders": "0.00000000", | ||
1150 | "btcValue": "0.00000000" | ||
1151 | }, | ||
1152 | "STR": { | ||
1153 | "available": "0.00000000", | ||
1154 | "onOrders": "0.00000000", | ||
1155 | "btcValue": "0.00000000" | ||
1156 | }, | ||
1157 | "STRAT": { | ||
1158 | "available": "0.00000000", | ||
1159 | "onOrders": "0.00000000", | ||
1160 | "btcValue": "0.00000000" | ||
1161 | }, | ||
1162 | "SUM": { | ||
1163 | "available": "0.00000000", | ||
1164 | "onOrders": "0.00000000", | ||
1165 | "btcValue": "0.00000000" | ||
1166 | }, | ||
1167 | "SUN": { | ||
1168 | "available": "0.00000000", | ||
1169 | "onOrders": "0.00000000", | ||
1170 | "btcValue": "0.00000000" | ||
1171 | }, | ||
1172 | "SWARM": { | ||
1173 | "available": "0.00000000", | ||
1174 | "onOrders": "0.00000000", | ||
1175 | "btcValue": "0.00000000" | ||
1176 | }, | ||
1177 | "SXC": { | ||
1178 | "available": "0.00000000", | ||
1179 | "onOrders": "0.00000000", | ||
1180 | "btcValue": "0.00000000" | ||
1181 | }, | ||
1182 | "SYNC": { | ||
1183 | "available": "0.00000000", | ||
1184 | "onOrders": "0.00000000", | ||
1185 | "btcValue": "0.00000000" | ||
1186 | }, | ||
1187 | "SYS": { | ||
1188 | "available": "0.00000000", | ||
1189 | "onOrders": "0.00000000", | ||
1190 | "btcValue": "0.00000000" | ||
1191 | }, | ||
1192 | "TAC": { | ||
1193 | "available": "0.00000000", | ||
1194 | "onOrders": "0.00000000", | ||
1195 | "btcValue": "0.00000000" | ||
1196 | }, | ||
1197 | "TOR": { | ||
1198 | "available": "0.00000000", | ||
1199 | "onOrders": "0.00000000", | ||
1200 | "btcValue": "0.00000000" | ||
1201 | }, | ||
1202 | "TRUST": { | ||
1203 | "available": "0.00000000", | ||
1204 | "onOrders": "0.00000000", | ||
1205 | "btcValue": "0.00000000" | ||
1206 | }, | ||
1207 | "TWE": { | ||
1208 | "available": "0.00000000", | ||
1209 | "onOrders": "0.00000000", | ||
1210 | "btcValue": "0.00000000" | ||
1211 | }, | ||
1212 | "UIS": { | ||
1213 | "available": "0.00000000", | ||
1214 | "onOrders": "0.00000000", | ||
1215 | "btcValue": "0.00000000" | ||
1216 | }, | ||
1217 | "ULTC": { | ||
1218 | "available": "0.00000000", | ||
1219 | "onOrders": "0.00000000", | ||
1220 | "btcValue": "0.00000000" | ||
1221 | }, | ||
1222 | "UNITY": { | ||
1223 | "available": "0.00000000", | ||
1224 | "onOrders": "0.00000000", | ||
1225 | "btcValue": "0.00000000" | ||
1226 | }, | ||
1227 | "URO": { | ||
1228 | "available": "0.00000000", | ||
1229 | "onOrders": "0.00000000", | ||
1230 | "btcValue": "0.00000000" | ||
1231 | }, | ||
1232 | "USDE": { | ||
1233 | "available": "0.00000000", | ||
1234 | "onOrders": "0.00000000", | ||
1235 | "btcValue": "0.00000000" | ||
1236 | }, | ||
1237 | "USDT": { | ||
1238 | "available": "0.00002625", | ||
1239 | "onOrders": "0.00000000", | ||
1240 | "btcValue": "0.00000000" | ||
1241 | }, | ||
1242 | "UTC": { | ||
1243 | "available": "0.00000000", | ||
1244 | "onOrders": "0.00000000", | ||
1245 | "btcValue": "0.00000000" | ||
1246 | }, | ||
1247 | "UTIL": { | ||
1248 | "available": "0.00000000", | ||
1249 | "onOrders": "0.00000000", | ||
1250 | "btcValue": "0.00000000" | ||
1251 | }, | ||
1252 | "UVC": { | ||
1253 | "available": "0.00000000", | ||
1254 | "onOrders": "0.00000000", | ||
1255 | "btcValue": "0.00000000" | ||
1256 | }, | ||
1257 | "VIA": { | ||
1258 | "available": "0.00000000", | ||
1259 | "onOrders": "0.00000000", | ||
1260 | "btcValue": "0.00000000" | ||
1261 | }, | ||
1262 | "VOOT": { | ||
1263 | "available": "0.00000000", | ||
1264 | "onOrders": "0.00000000", | ||
1265 | "btcValue": "0.00000000" | ||
1266 | }, | ||
1267 | "VOX": { | ||
1268 | "available": "0.00000000", | ||
1269 | "onOrders": "0.00000000", | ||
1270 | "btcValue": "0.00000000" | ||
1271 | }, | ||
1272 | "VRC": { | ||
1273 | "available": "0.00000000", | ||
1274 | "onOrders": "0.00000000", | ||
1275 | "btcValue": "0.00000000" | ||
1276 | }, | ||
1277 | "VTC": { | ||
1278 | "available": "0.00000000", | ||
1279 | "onOrders": "0.00000000", | ||
1280 | "btcValue": "0.00000000" | ||
1281 | }, | ||
1282 | "WC": { | ||
1283 | "available": "0.00000000", | ||
1284 | "onOrders": "0.00000000", | ||
1285 | "btcValue": "0.00000000" | ||
1286 | }, | ||
1287 | "WDC": { | ||
1288 | "available": "0.00000000", | ||
1289 | "onOrders": "0.00000000", | ||
1290 | "btcValue": "0.00000000" | ||
1291 | }, | ||
1292 | "WIKI": { | ||
1293 | "available": "0.00000000", | ||
1294 | "onOrders": "0.00000000", | ||
1295 | "btcValue": "0.00000000" | ||
1296 | }, | ||
1297 | "WOLF": { | ||
1298 | "available": "0.00000000", | ||
1299 | "onOrders": "0.00000000", | ||
1300 | "btcValue": "0.00000000" | ||
1301 | }, | ||
1302 | "X13": { | ||
1303 | "available": "0.00000000", | ||
1304 | "onOrders": "0.00000000", | ||
1305 | "btcValue": "0.00000000" | ||
1306 | }, | ||
1307 | "XAI": { | ||
1308 | "available": "0.00000000", | ||
1309 | "onOrders": "0.00000000", | ||
1310 | "btcValue": "0.00000000" | ||
1311 | }, | ||
1312 | "XAP": { | ||
1313 | "available": "0.00000000", | ||
1314 | "onOrders": "0.00000000", | ||
1315 | "btcValue": "0.00000000" | ||
1316 | }, | ||
1317 | "XBC": { | ||
1318 | "available": "0.00000000", | ||
1319 | "onOrders": "0.00000000", | ||
1320 | "btcValue": "0.00000000" | ||
1321 | }, | ||
1322 | "XC": { | ||
1323 | "available": "0.00000000", | ||
1324 | "onOrders": "0.00000000", | ||
1325 | "btcValue": "0.00000000" | ||
1326 | }, | ||
1327 | "XCH": { | ||
1328 | "available": "0.00000000", | ||
1329 | "onOrders": "0.00000000", | ||
1330 | "btcValue": "0.00000000" | ||
1331 | }, | ||
1332 | "XCN": { | ||
1333 | "available": "0.00000000", | ||
1334 | "onOrders": "0.00000000", | ||
1335 | "btcValue": "0.00000000" | ||
1336 | }, | ||
1337 | "XCP": { | ||
1338 | "available": "0.00000000", | ||
1339 | "onOrders": "0.00000000", | ||
1340 | "btcValue": "0.00000000" | ||
1341 | }, | ||
1342 | "XCR": { | ||
1343 | "available": "0.00000000", | ||
1344 | "onOrders": "0.00000000", | ||
1345 | "btcValue": "0.00000000" | ||
1346 | }, | ||
1347 | "XDN": { | ||
1348 | "available": "0.00000000", | ||
1349 | "onOrders": "0.00000000", | ||
1350 | "btcValue": "0.00000000" | ||
1351 | }, | ||
1352 | "XDP": { | ||
1353 | "available": "0.00000000", | ||
1354 | "onOrders": "0.00000000", | ||
1355 | "btcValue": "0.00000000" | ||
1356 | }, | ||
1357 | "XEM": { | ||
1358 | "available": "0.00000000", | ||
1359 | "onOrders": "0.00000000", | ||
1360 | "btcValue": "0.00000000" | ||
1361 | }, | ||
1362 | "XHC": { | ||
1363 | "available": "0.00000000", | ||
1364 | "onOrders": "0.00000000", | ||
1365 | "btcValue": "0.00000000" | ||
1366 | }, | ||
1367 | "XLB": { | ||
1368 | "available": "0.00000000", | ||
1369 | "onOrders": "0.00000000", | ||
1370 | "btcValue": "0.00000000" | ||
1371 | }, | ||
1372 | "XMG": { | ||
1373 | "available": "0.00000000", | ||
1374 | "onOrders": "0.00000000", | ||
1375 | "btcValue": "0.00000000" | ||
1376 | }, | ||
1377 | "XMR": { | ||
1378 | "available": "0.18719303", | ||
1379 | "onOrders": "0.00000000", | ||
1380 | "btcValue": "0.00598102" | ||
1381 | }, | ||
1382 | "XPB": { | ||
1383 | "available": "0.00000000", | ||
1384 | "onOrders": "0.00000000", | ||
1385 | "btcValue": "0.00000000" | ||
1386 | }, | ||
1387 | "XPM": { | ||
1388 | "available": "0.00000000", | ||
1389 | "onOrders": "0.00000000", | ||
1390 | "btcValue": "0.00000000" | ||
1391 | }, | ||
1392 | "XRP": { | ||
1393 | "available": "0.00000000", | ||
1394 | "onOrders": "0.00000000", | ||
1395 | "btcValue": "0.00000000" | ||
1396 | }, | ||
1397 | "XSI": { | ||
1398 | "available": "0.00000000", | ||
1399 | "onOrders": "0.00000000", | ||
1400 | "btcValue": "0.00000000" | ||
1401 | }, | ||
1402 | "XST": { | ||
1403 | "available": "0.00000000", | ||
1404 | "onOrders": "0.00000000", | ||
1405 | "btcValue": "0.00000000" | ||
1406 | }, | ||
1407 | "XSV": { | ||
1408 | "available": "0.00000000", | ||
1409 | "onOrders": "0.00000000", | ||
1410 | "btcValue": "0.00000000" | ||
1411 | }, | ||
1412 | "XUSD": { | ||
1413 | "available": "0.00000000", | ||
1414 | "onOrders": "0.00000000", | ||
1415 | "btcValue": "0.00000000" | ||
1416 | }, | ||
1417 | "XVC": { | ||
1418 | "available": "0.00000000", | ||
1419 | "onOrders": "0.00000000", | ||
1420 | "btcValue": "0.00000000" | ||
1421 | }, | ||
1422 | "XXC": { | ||
1423 | "available": "0.00000000", | ||
1424 | "onOrders": "0.00000000", | ||
1425 | "btcValue": "0.00000000" | ||
1426 | }, | ||
1427 | "YACC": { | ||
1428 | "available": "0.00000000", | ||
1429 | "onOrders": "0.00000000", | ||
1430 | "btcValue": "0.00000000" | ||
1431 | }, | ||
1432 | "YANG": { | ||
1433 | "available": "0.00000000", | ||
1434 | "onOrders": "0.00000000", | ||
1435 | "btcValue": "0.00000000" | ||
1436 | }, | ||
1437 | "YC": { | ||
1438 | "available": "0.00000000", | ||
1439 | "onOrders": "0.00000000", | ||
1440 | "btcValue": "0.00000000" | ||
1441 | }, | ||
1442 | "YIN": { | ||
1443 | "available": "0.00000000", | ||
1444 | "onOrders": "0.00000000", | ||
1445 | "btcValue": "0.00000000" | ||
1446 | }, | ||
1447 | "ZEC": { | ||
1448 | "available": "0.00000000", | ||
1449 | "onOrders": "0.00000000", | ||
1450 | "btcValue": "0.00000000" | ||
1451 | }, | ||
1452 | "ZRX": { | ||
1453 | "available": "0.00000000", | ||
1454 | "onOrders": "0.00000000", | ||
1455 | "btcValue": "0.00000000" | ||
1456 | } | ||
1457 | } | ||
1458 | |||
1459 | |||
diff --git a/test_samples/poloniexETest.test_fetch_all_balances.2.json b/test_samples/poloniexETest.test_fetch_all_balances.2.json new file mode 100644 index 0000000..3d2f741 --- /dev/null +++ b/test_samples/poloniexETest.test_fetch_all_balances.2.json | |||
@@ -0,0 +1,101 @@ | |||
1 | { | ||
2 | "BTC_BTS": { | ||
3 | "amount": "-309.05320945", | ||
4 | "total": "0.00602465", | ||
5 | "basePrice": "0.00001949", | ||
6 | "liquidationPrice": "0.00010211", | ||
7 | "pl": "0.00024394", | ||
8 | "lendingFees": "0.00000000", | ||
9 | "type": "short" | ||
10 | }, | ||
11 | "BTC_CLAM": { | ||
12 | "type": "none", | ||
13 | "amount": "0.00000000", | ||
14 | "total": "0.00000000", | ||
15 | "basePrice": "0.00000000", | ||
16 | "liquidationPrice": -1, | ||
17 | "pl": "0.00000000", | ||
18 | "lendingFees": "0.00000000" | ||
19 | }, | ||
20 | "BTC_DASH": { | ||
21 | "amount": "-0.11204647", | ||
22 | "total": "0.00602391", | ||
23 | "basePrice": "0.05376260", | ||
24 | "liquidationPrice": "0.28321001", | ||
25 | "pl": "0.00006304", | ||
26 | "lendingFees": "0.00000000", | ||
27 | "type": "short" | ||
28 | }, | ||
29 | "BTC_DOGE": { | ||
30 | "amount": "-12779.79821852", | ||
31 | "total": "0.00599149", | ||
32 | "basePrice": "0.00000046", | ||
33 | "liquidationPrice": "0.00000246", | ||
34 | "pl": "0.00024059", | ||
35 | "lendingFees": "-0.00000009", | ||
36 | "type": "short" | ||
37 | }, | ||
38 | "BTC_LTC": { | ||
39 | "type": "none", | ||
40 | "amount": "0.00000000", | ||
41 | "total": "0.00000000", | ||
42 | "basePrice": "0.00000000", | ||
43 | "liquidationPrice": -1, | ||
44 | "pl": "0.00000000", | ||
45 | "lendingFees": "0.00000000" | ||
46 | }, | ||
47 | "BTC_MAID": { | ||
48 | "type": "none", | ||
49 | "amount": "0.00000000", | ||
50 | "total": "0.00000000", | ||
51 | "basePrice": "0.00000000", | ||
52 | "liquidationPrice": -1, | ||
53 | "pl": "0.00000000", | ||
54 | "lendingFees": "0.00000000" | ||
55 | }, | ||
56 | "BTC_STR": { | ||
57 | "type": "none", | ||
58 | "amount": "0.00000000", | ||
59 | "total": "0.00000000", | ||
60 | "basePrice": "0.00000000", | ||
61 | "liquidationPrice": -1, | ||
62 | "pl": "0.00000000", | ||
63 | "lendingFees": "0.00000000" | ||
64 | }, | ||
65 | "BTC_XMR": { | ||
66 | "type": "none", | ||
67 | "amount": "0.00000000", | ||
68 | "total": "0.00000000", | ||
69 | "basePrice": "0.00000000", | ||
70 | "liquidationPrice": -1, | ||
71 | "pl": "0.00000000", | ||
72 | "lendingFees": "0.00000000" | ||
73 | }, | ||
74 | "BTC_XRP": { | ||
75 | "amount": "-68.66952474", | ||
76 | "total": "0.00602974", | ||
77 | "basePrice": "0.00008780", | ||
78 | "liquidationPrice": "0.00045756", | ||
79 | "pl": "0.00039198", | ||
80 | "lendingFees": "0.00000000", | ||
81 | "type": "short" | ||
82 | }, | ||
83 | "BTC_ETH": { | ||
84 | "type": "none", | ||
85 | "amount": "0.00000000", | ||
86 | "total": "0.00000000", | ||
87 | "basePrice": "0.00000000", | ||
88 | "liquidationPrice": -1, | ||
89 | "pl": "0.00000000", | ||
90 | "lendingFees": "0.00000000" | ||
91 | }, | ||
92 | "BTC_FCT": { | ||
93 | "type": "none", | ||
94 | "amount": "0.00000000", | ||
95 | "total": "0.00000000", | ||
96 | "basePrice": "0.00000000", | ||
97 | "liquidationPrice": -1, | ||
98 | "pl": "0.00000000", | ||
99 | "lendingFees": "0.00000000" | ||
100 | } | ||
101 | } | ||
diff --git a/test_samples/poloniexETest.test_fetch_all_balances.3.json b/test_samples/poloniexETest.test_fetch_all_balances.3.json new file mode 100644 index 0000000..e805f6f --- /dev/null +++ b/test_samples/poloniexETest.test_fetch_all_balances.3.json | |||
@@ -0,0 +1,11 @@ | |||
1 | { | ||
2 | "exchange": { | ||
3 | "BLK": "159.83673869", | ||
4 | "BTC": "0.00005959", | ||
5 | "USDT": "0.00002625", | ||
6 | "XMR": "0.18719303" | ||
7 | }, | ||
8 | "margin": { | ||
9 | "BTC": "0.03019227" | ||
10 | } | ||
11 | } | ||
diff --git a/test_portfolio.json b/test_samples/test_portfolio.json index a2ba3f0..a2ba3f0 100644 --- a/test_portfolio.json +++ b/test_samples/test_portfolio.json | |||