diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-09 19:18:02 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-09 19:18:02 +0100 |
commit | 34eb08f759a440af0376727664d9422041dfbd18 (patch) | |
tree | d1a94be893451a4e182f3e75e9afb01749172bb4 | |
parent | f9226903cb53a9b303a26de562e321159349f8df (diff) | |
parent | dc1ca9a306f09886c6c57f8d426c59a9d084b2b3 (diff) | |
download | Trader-34eb08f759a440af0376727664d9422041dfbd18.tar.gz Trader-34eb08f759a440af0376727664d9422041dfbd18.tar.zst Trader-34eb08f759a440af0376727664d9422041dfbd18.zip |
Merge branch 'immae/parallelize' into dev
Fixes https://git.immae.eu/mantisbt/view.php?id=51
-rw-r--r-- | helper.py | 320 | ||||
-rw-r--r-- | main.py | 161 | ||||
-rw-r--r-- | market.py | 239 | ||||
-rw-r--r-- | portfolio.py | 79 | ||||
-rw-r--r-- | store.py | 175 | ||||
-rw-r--r-- | test.py | 1257 | ||||
-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 |
10 files changed, 3071 insertions, 731 deletions
diff --git a/helper.py b/helper.py deleted file mode 100644 index 8f726d5..0000000 --- a/helper.py +++ /dev/null | |||
@@ -1,320 +0,0 @@ | |||
1 | from datetime import datetime | ||
2 | import argparse | ||
3 | import configparser | ||
4 | import psycopg2 | ||
5 | import os | ||
6 | import sys | ||
7 | |||
8 | import portfolio | ||
9 | |||
10 | def make_order(market, value, currency, action="acquire", | ||
11 | close_if_possible=False, base_currency="BTC", follow=True, | ||
12 | compute_value="average"): | ||
13 | """ | ||
14 | Make an order on market | ||
15 | "market": The market on which to place the order | ||
16 | "value": The value in *base_currency* to acquire, | ||
17 | or in *currency* to dispose. | ||
18 | use negative for margin trade. | ||
19 | "action": "acquire" or "dispose". | ||
20 | "acquire" will buy long or sell short, | ||
21 | "dispose" will sell long or buy short. | ||
22 | "currency": The currency to acquire or dispose | ||
23 | "base_currency": The base currency. The value is expressed in that | ||
24 | currency (default: BTC) | ||
25 | "follow": Whether to follow the order once run (default: True) | ||
26 | "close_if_possible": Whether to try to close the position at the end | ||
27 | of the trade, i.e. reach exactly 0 at the end | ||
28 | (only meaningful in "dispose"). May have | ||
29 | unwanted effects if the end value of the | ||
30 | currency is not 0. | ||
31 | "compute_value": Compute value to place the order | ||
32 | """ | ||
33 | market.report.log_stage("make_order_begin") | ||
34 | market.balances.fetch_balances(tag="make_order_begin") | ||
35 | if action == "acquire": | ||
36 | trade = portfolio.Trade( | ||
37 | portfolio.Amount(base_currency, 0), | ||
38 | portfolio.Amount(base_currency, value), | ||
39 | currency, market) | ||
40 | else: | ||
41 | amount = portfolio.Amount(currency, value) | ||
42 | trade = portfolio.Trade( | ||
43 | amount.in_currency(base_currency, market, compute_value=compute_value), | ||
44 | portfolio.Amount(base_currency, 0), | ||
45 | currency, market) | ||
46 | market.trades.all.append(trade) | ||
47 | order = trade.prepare_order( | ||
48 | close_if_possible=close_if_possible, | ||
49 | compute_value=compute_value) | ||
50 | market.report.log_orders([order], None, compute_value) | ||
51 | market.trades.run_orders() | ||
52 | if follow: | ||
53 | market.follow_orders() | ||
54 | market.balances.fetch_balances(tag="make_order_end") | ||
55 | else: | ||
56 | market.report.log_stage("make_order_end_not_followed") | ||
57 | return order | ||
58 | market.report.log_stage("make_order_end") | ||
59 | |||
60 | def get_user_market(config_path, user_id, debug=False): | ||
61 | import market | ||
62 | pg_config, report_path = main_parse_config(config_path) | ||
63 | market_config = list(main_fetch_markets(pg_config, str(user_id)))[0][0] | ||
64 | return market.Market.from_config(market_config, debug=debug) | ||
65 | |||
66 | def main_parse_args(argv): | ||
67 | parser = argparse.ArgumentParser( | ||
68 | description="Run the trade bot") | ||
69 | |||
70 | parser.add_argument("-c", "--config", | ||
71 | default="config.ini", | ||
72 | required=False, | ||
73 | help="Config file to load (default: config.ini)") | ||
74 | parser.add_argument("--before", | ||
75 | default=False, action='store_const', const=True, | ||
76 | help="Run the steps before the cryptoportfolio update") | ||
77 | parser.add_argument("--after", | ||
78 | default=False, action='store_const', const=True, | ||
79 | help="Run the steps after the cryptoportfolio update") | ||
80 | parser.add_argument("--debug", | ||
81 | default=False, action='store_const', const=True, | ||
82 | help="Run in debug mode") | ||
83 | parser.add_argument("--user", | ||
84 | default=None, required=False, help="Only run for that user") | ||
85 | parser.add_argument("--action", | ||
86 | action='append', | ||
87 | help="Do a different action than trading (add several times to chain)") | ||
88 | |||
89 | args = parser.parse_args(argv) | ||
90 | |||
91 | if not os.path.exists(args.config): | ||
92 | print("no config file found, exiting") | ||
93 | sys.exit(1) | ||
94 | |||
95 | return args | ||
96 | |||
97 | def main_parse_config(config_file): | ||
98 | config = configparser.ConfigParser() | ||
99 | config.read(config_file) | ||
100 | |||
101 | if "postgresql" not in config: | ||
102 | print("no configuration for postgresql in config file") | ||
103 | sys.exit(1) | ||
104 | |||
105 | if "app" in config and "report_path" in config["app"]: | ||
106 | report_path = config["app"]["report_path"] | ||
107 | |||
108 | if not os.path.exists(report_path): | ||
109 | os.makedirs(report_path) | ||
110 | else: | ||
111 | report_path = None | ||
112 | |||
113 | return [config["postgresql"], report_path] | ||
114 | |||
115 | def main_fetch_markets(pg_config, user): | ||
116 | connection = psycopg2.connect(**pg_config) | ||
117 | cursor = connection.cursor() | ||
118 | |||
119 | if user is None: | ||
120 | cursor.execute("SELECT config,user_id FROM market_configs") | ||
121 | else: | ||
122 | cursor.execute("SELECT config,user_id FROM market_configs WHERE user_id = %s", user) | ||
123 | |||
124 | for row in cursor: | ||
125 | yield row | ||
126 | |||
127 | def main_process_market(user_market, actions, before=False, after=False): | ||
128 | if len(actions or []) == 0: | ||
129 | if before: | ||
130 | Processor(user_market).process("sell_all", steps="before") | ||
131 | if after: | ||
132 | Processor(user_market).process("sell_all", steps="after") | ||
133 | else: | ||
134 | for action in actions: | ||
135 | if action in globals(): | ||
136 | (globals()[action])(user_market) | ||
137 | else: | ||
138 | raise NotImplementedError("Unknown action {}".format(action)) | ||
139 | |||
140 | def main_store_report(report_path, user_id, user_market): | ||
141 | try: | ||
142 | if report_path is not None: | ||
143 | report_file = "{}/{}_{}.json".format(report_path, datetime.now().isoformat(), user_id) | ||
144 | with open(report_file, "w") as f: | ||
145 | f.write(user_market.report.to_json()) | ||
146 | except Exception as e: | ||
147 | print("impossible to store report file: {}; {}".format(e.__class__.__name__, e)) | ||
148 | |||
149 | def print_orders(market, base_currency="BTC"): | ||
150 | market.report.log_stage("print_orders") | ||
151 | market.balances.fetch_balances(tag="print_orders") | ||
152 | market.prepare_trades(base_currency=base_currency, compute_value="average") | ||
153 | market.trades.prepare_orders(compute_value="average") | ||
154 | |||
155 | def print_balances(market, base_currency="BTC"): | ||
156 | market.report.log_stage("print_balances") | ||
157 | market.balances.fetch_balances() | ||
158 | if base_currency is not None: | ||
159 | market.report.print_log("total:") | ||
160 | market.report.print_log(sum(market.balances.in_currency(base_currency).values())) | ||
161 | |||
162 | class Processor: | ||
163 | scenarios = { | ||
164 | "sell_needed": [ | ||
165 | { | ||
166 | "name": "wait", | ||
167 | "number": 0, | ||
168 | "before": False, | ||
169 | "after": True, | ||
170 | "wait_for_recent": {}, | ||
171 | }, | ||
172 | { | ||
173 | "name": "sell", | ||
174 | "number": 1, | ||
175 | "before": False, | ||
176 | "after": True, | ||
177 | "fetch_balances": ["begin", "end"], | ||
178 | "prepare_trades": {}, | ||
179 | "prepare_orders": { "only": "dispose", "compute_value": "average" }, | ||
180 | "run_orders": {}, | ||
181 | "follow_orders": {}, | ||
182 | "close_trades": {}, | ||
183 | }, | ||
184 | { | ||
185 | "name": "buy", | ||
186 | "number": 2, | ||
187 | "before": False, | ||
188 | "after": True, | ||
189 | "fetch_balances": ["begin", "end"], | ||
190 | "prepare_trades": { "only": "acquire" }, | ||
191 | "prepare_orders": { "only": "acquire", "compute_value": "average" }, | ||
192 | "move_balances": {}, | ||
193 | "run_orders": {}, | ||
194 | "follow_orders": {}, | ||
195 | "close_trades": {}, | ||
196 | }, | ||
197 | ], | ||
198 | "sell_all": [ | ||
199 | { | ||
200 | "name": "all_sell", | ||
201 | "number": 1, | ||
202 | "before": True, | ||
203 | "after": False, | ||
204 | "fetch_balances": ["begin", "end"], | ||
205 | "prepare_trades": { "repartition": { "base_currency": (1, "long") } }, | ||
206 | "prepare_orders": { "compute_value": "average" }, | ||
207 | "run_orders": {}, | ||
208 | "follow_orders": {}, | ||
209 | "close_trades": {}, | ||
210 | }, | ||
211 | { | ||
212 | "name": "wait", | ||
213 | "number": 2, | ||
214 | "before": False, | ||
215 | "after": True, | ||
216 | "wait_for_recent": {}, | ||
217 | }, | ||
218 | { | ||
219 | "name": "all_buy", | ||
220 | "number": 3, | ||
221 | "before": False, | ||
222 | "after": True, | ||
223 | "fetch_balances": ["begin", "end"], | ||
224 | "prepare_trades": {}, | ||
225 | "prepare_orders": { "compute_value": "average" }, | ||
226 | "move_balances": {}, | ||
227 | "run_orders": {}, | ||
228 | "follow_orders": {}, | ||
229 | "close_trades": {}, | ||
230 | }, | ||
231 | ] | ||
232 | } | ||
233 | |||
234 | ordered_actions = [ | ||
235 | "wait_for_recent", "prepare_trades", "prepare_orders", | ||
236 | "move_balances", "run_orders", "follow_orders", | ||
237 | "close_trades"] | ||
238 | |||
239 | def __init__(self, market): | ||
240 | self.market = market | ||
241 | |||
242 | def select_steps(self, scenario, step): | ||
243 | if step == "all": | ||
244 | return scenario | ||
245 | elif step == "before" or step == "after": | ||
246 | return list(filter(lambda x: step in x and x[step], scenario)) | ||
247 | elif type(step) == int: | ||
248 | return [scenario[step-1]] | ||
249 | elif type(step) == str: | ||
250 | return list(filter(lambda x: x["name"] == step, scenario)) | ||
251 | else: | ||
252 | raise TypeError("Unknown step {}".format(step)) | ||
253 | |||
254 | def process(self, scenario_name, steps="all", **kwargs): | ||
255 | scenario = self.scenarios[scenario_name] | ||
256 | selected_steps = [] | ||
257 | |||
258 | if type(steps) == str or type(steps) == int: | ||
259 | selected_steps += self.select_steps(scenario, steps) | ||
260 | else: | ||
261 | for step in steps: | ||
262 | selected_steps += self.select_steps(scenario, step) | ||
263 | for step in selected_steps: | ||
264 | self.process_step(scenario_name, step, kwargs) | ||
265 | |||
266 | def process_step(self, scenario_name, step, kwargs): | ||
267 | process_name = "process_{}__{}_{}".format(scenario_name, step["number"], step["name"]) | ||
268 | self.market.report.log_stage("{}_begin".format(process_name)) | ||
269 | if "begin" in step.get("fetch_balances", []): | ||
270 | self.market.balances.fetch_balances(tag="{}_begin".format(process_name)) | ||
271 | |||
272 | for action in self.ordered_actions: | ||
273 | if action in step: | ||
274 | self.run_action(action, step[action], kwargs) | ||
275 | |||
276 | if "end" in step.get("fetch_balances", []): | ||
277 | self.market.balances.fetch_balances(tag="{}_end".format(process_name)) | ||
278 | self.market.report.log_stage("{}_end".format(process_name)) | ||
279 | |||
280 | def method_arguments(self, action): | ||
281 | import inspect | ||
282 | |||
283 | if action == "wait_for_recent": | ||
284 | method = portfolio.Portfolio.wait_for_recent | ||
285 | elif action == "prepare_trades": | ||
286 | method = self.market.prepare_trades | ||
287 | elif action == "prepare_orders": | ||
288 | method = self.market.trades.prepare_orders | ||
289 | elif action == "move_balances": | ||
290 | method = self.market.move_balances | ||
291 | elif action == "run_orders": | ||
292 | method = self.market.trades.run_orders | ||
293 | elif action == "follow_orders": | ||
294 | method = self.market.follow_orders | ||
295 | elif action == "close_trades": | ||
296 | method = self.market.trades.close_trades | ||
297 | |||
298 | signature = inspect.getfullargspec(method) | ||
299 | defaults = signature.defaults or [] | ||
300 | kwargs = signature.args[-len(defaults):] | ||
301 | |||
302 | return [method, kwargs] | ||
303 | |||
304 | def parse_args(self, action, default_args, kwargs): | ||
305 | method, allowed_arguments = self.method_arguments(action) | ||
306 | args = {k: v for k, v in {**default_args, **kwargs}.items() if k in allowed_arguments } | ||
307 | |||
308 | if "repartition" in args and "base_currency" in args["repartition"]: | ||
309 | r = args["repartition"] | ||
310 | r[args.get("base_currency", "BTC")] = r.pop("base_currency") | ||
311 | |||
312 | return method, args | ||
313 | |||
314 | def run_action(self, action, default_args, kwargs): | ||
315 | method, args = self.parse_args(action, default_args, kwargs) | ||
316 | |||
317 | if action == "wait_for_recent": | ||
318 | method(self.market, **args) | ||
319 | else: | ||
320 | method(**args) | ||
@@ -1,18 +1,155 @@ | |||
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_config = list(fetch_markets(pg_config, str(user_id)))[0][0] | ||
66 | return market.Market.from_config(market_config, debug=debug) | ||
67 | |||
68 | def fetch_markets(pg_config, user): | ||
69 | connection = psycopg2.connect(**pg_config) | ||
70 | cursor = connection.cursor() | ||
71 | |||
72 | if user is None: | ||
73 | cursor.execute("SELECT config,user_id FROM market_configs") | ||
74 | else: | ||
75 | cursor.execute("SELECT config,user_id FROM market_configs WHERE user_id = %s", user) | ||
76 | |||
77 | for row in cursor: | ||
78 | yield row | ||
79 | |||
80 | def parse_config(config_file): | ||
81 | config = configparser.ConfigParser() | ||
82 | config.read(config_file) | ||
83 | |||
84 | if "postgresql" not in config: | ||
85 | print("no configuration for postgresql in config file") | ||
86 | sys.exit(1) | ||
87 | |||
88 | if "app" in config and "report_path" in config["app"]: | ||
89 | report_path = config["app"]["report_path"] | ||
90 | |||
91 | if not os.path.exists(report_path): | ||
92 | os.makedirs(report_path) | ||
93 | else: | ||
94 | report_path = None | ||
95 | |||
96 | return [config["postgresql"], report_path] | ||
97 | |||
98 | def parse_args(argv): | ||
99 | parser = argparse.ArgumentParser( | ||
100 | description="Run the trade bot") | ||
101 | |||
102 | parser.add_argument("-c", "--config", | ||
103 | default="config.ini", | ||
104 | required=False, | ||
105 | help="Config file to load (default: config.ini)") | ||
106 | parser.add_argument("--before", | ||
107 | default=False, action='store_const', const=True, | ||
108 | help="Run the steps before the cryptoportfolio update") | ||
109 | parser.add_argument("--after", | ||
110 | default=False, action='store_const', const=True, | ||
111 | help="Run the steps after the cryptoportfolio update") | ||
112 | parser.add_argument("--debug", | ||
113 | default=False, action='store_const', const=True, | ||
114 | help="Run in debug mode") | ||
115 | parser.add_argument("--user", | ||
116 | default=None, required=False, help="Only run for that user") | ||
117 | parser.add_argument("--action", | ||
118 | action='append', | ||
119 | help="Do a different action than trading (add several times to chain)") | ||
120 | parser.add_argument("--parallel", action='store_true', default=True, dest="parallel") | ||
121 | parser.add_argument("--no-parallel", action='store_false', dest="parallel") | ||
122 | |||
123 | args = parser.parse_args(argv) | ||
124 | |||
125 | if not os.path.exists(args.config): | ||
126 | print("no config file found, exiting") | ||
127 | sys.exit(1) | ||
128 | |||
129 | return args | ||
130 | |||
131 | def process(market_config, user_id, report_path, args): | ||
9 | try: | 132 | try: |
10 | user_market = market.Market.from_config(market_config, debug=args.debug) | 133 | market.Market\ |
11 | helper.main_process_market(user_market, args.action, before=args.before, after=args.after) | 134 | .from_config(market_config, debug=args.debug, user_id=user_id, report_path=report_path)\ |
135 | .process(args.action, before=args.before, after=args.after) | ||
12 | except Exception as e: | 136 | except Exception as e: |
13 | try: | 137 | print("{}: {}".format(e.__class__.__name__, e)) |
14 | user_market.report.log_error("main", exception=e) | 138 | |
15 | except: | 139 | def main(argv): |
16 | print("{}: {}".format(e.__class__.__name__, e)) | 140 | args = parse_args(argv) |
17 | finally: | 141 | |
18 | helper.main_store_report(report_path, user_id, user_market) | 142 | pg_config, report_path = parse_config(args.config) |
143 | |||
144 | if args.parallel: | ||
145 | import threading | ||
146 | market.Portfolio.start_worker() | ||
147 | |||
148 | for market_config, user_id in fetch_markets(pg_config, args.user): | ||
149 | threading.Thread(target=process, args=[market_config, user_id, report_path, args]).start() | ||
150 | else: | ||
151 | for market_config, user_id in fetch_markets(pg_config, args.user): | ||
152 | process(market_config, user_id, report_path, args) | ||
153 | |||
154 | if __name__ == '__main__': # pragma: no cover | ||
155 | main(sys.argv[1:]) | ||
@@ -3,6 +3,8 @@ import ccxt_wrapper as ccxt | |||
3 | import time | 3 | import time |
4 | from store import * | 4 | from store import * |
5 | from cachetools.func import ttl_cache | 5 | from cachetools.func import ttl_cache |
6 | from datetime import datetime | ||
7 | import portfolio | ||
6 | 8 | ||
7 | class Market: | 9 | class Market: |
8 | debug = False | 10 | debug = False |
@@ -11,17 +13,21 @@ class Market: | |||
11 | trades = None | 13 | trades = None |
12 | balances = None | 14 | balances = None |
13 | 15 | ||
14 | def __init__(self, ccxt_instance, debug=False): | 16 | def __init__(self, ccxt_instance, debug=False, user_id=None, report_path=None): |
15 | self.debug = debug | 17 | self.debug = debug |
16 | self.ccxt = ccxt_instance | 18 | self.ccxt = ccxt_instance |
17 | self.ccxt._market = self | 19 | self.ccxt._market = self |
18 | self.report = ReportStore(self) | 20 | self.report = ReportStore(self) |
19 | self.trades = TradeStore(self) | 21 | self.trades = TradeStore(self) |
20 | self.balances = BalanceStore(self) | 22 | self.balances = BalanceStore(self) |
23 | self.processor = Processor(self) | ||
24 | |||
25 | self.user_id = user_id | ||
26 | self.report_path = report_path | ||
21 | 27 | ||
22 | @classmethod | 28 | @classmethod |
23 | def from_config(cls, config, debug=False): | 29 | def from_config(cls, config, debug=False, user_id=None, report_path=None): |
24 | config["apiKey"] = config.pop("key") | 30 | config["apiKey"] = config.pop("key", None) |
25 | 31 | ||
26 | ccxt_instance = ccxt.poloniexE(config) | 32 | ccxt_instance = ccxt.poloniexE(config) |
27 | 33 | ||
@@ -37,7 +43,35 @@ class Market: | |||
37 | ccxt_instance.session.request = request_wrap.__get__(ccxt_instance.session, | 43 | ccxt_instance.session.request = request_wrap.__get__(ccxt_instance.session, |
38 | ccxt_instance.session.__class__) | 44 | ccxt_instance.session.__class__) |
39 | 45 | ||
40 | return cls(ccxt_instance, debug=debug) | 46 | return cls(ccxt_instance, debug=debug, user_id=user_id, report_path=report_path) |
47 | |||
48 | def store_report(self): | ||
49 | self.report.merge(Portfolio.report) | ||
50 | try: | ||
51 | if self.report_path is not None: | ||
52 | report_file = "{}/{}_{}.json".format(self.report_path, datetime.now().isoformat(), self.user_id) | ||
53 | with open(report_file, "w") as f: | ||
54 | f.write(self.report.to_json()) | ||
55 | except Exception as e: | ||
56 | print("impossible to store report file: {}; {}".format(e.__class__.__name__, e)) | ||
57 | |||
58 | def process(self, actions, before=False, after=False): | ||
59 | try: | ||
60 | if len(actions or []) == 0: | ||
61 | if before: | ||
62 | self.processor.process("sell_all", steps="before") | ||
63 | if after: | ||
64 | self.processor.process("sell_all", steps="after") | ||
65 | else: | ||
66 | for action in actions: | ||
67 | if hasattr(self, action): | ||
68 | getattr(self, action)() | ||
69 | else: | ||
70 | self.report.log_error("market_process", message="Unknown action {}".format(action)) | ||
71 | except Exception as e: | ||
72 | self.report.log_error("market_process", exception=e) | ||
73 | finally: | ||
74 | self.store_report() | ||
41 | 75 | ||
42 | def move_balances(self): | 76 | def move_balances(self): |
43 | needed_in_margin = {} | 77 | needed_in_margin = {} |
@@ -143,3 +177,200 @@ class Market: | |||
143 | liquidity=liquidity, repartition=repartition) | 177 | liquidity=liquidity, repartition=repartition) |
144 | self.trades.compute_trades(values_in_base, new_repartition, only=only) | 178 | self.trades.compute_trades(values_in_base, new_repartition, only=only) |
145 | 179 | ||
180 | # Helpers | ||
181 | def print_orders(self, base_currency="BTC"): | ||
182 | self.report.log_stage("print_orders") | ||
183 | self.balances.fetch_balances(tag="print_orders") | ||
184 | self.prepare_trades(base_currency=base_currency, compute_value="average") | ||
185 | self.trades.prepare_orders(compute_value="average") | ||
186 | |||
187 | def print_balances(self, base_currency="BTC"): | ||
188 | self.report.log_stage("print_balances") | ||
189 | self.balances.fetch_balances() | ||
190 | if base_currency is not None: | ||
191 | self.report.print_log("total:") | ||
192 | self.report.print_log(sum(self.balances.in_currency(base_currency).values())) | ||
193 | |||
194 | class Processor: | ||
195 | scenarios = { | ||
196 | "wait_for_cryptoportfolio": [ | ||
197 | { | ||
198 | "name": "wait", | ||
199 | "number": 1, | ||
200 | "before": False, | ||
201 | "after": True, | ||
202 | "wait_for_recent": {}, | ||
203 | }, | ||
204 | ], | ||
205 | "print_orders": [ | ||
206 | { | ||
207 | "name": "wait", | ||
208 | "number": 1, | ||
209 | "before": False, | ||
210 | "after": True, | ||
211 | "wait_for_recent": {}, | ||
212 | }, | ||
213 | { | ||
214 | "name": "make_orders", | ||
215 | "number": 2, | ||
216 | "before": False, | ||
217 | "after": True, | ||
218 | "fetch_balances": ["begin"], | ||
219 | "prepare_trades": { "compute_value": "average" }, | ||
220 | "prepare_orders": { "compute_value": "average" }, | ||
221 | }, | ||
222 | ], | ||
223 | "sell_needed": [ | ||
224 | { | ||
225 | "name": "wait", | ||
226 | "number": 0, | ||
227 | "before": False, | ||
228 | "after": True, | ||
229 | "wait_for_recent": {}, | ||
230 | }, | ||
231 | { | ||
232 | "name": "sell", | ||
233 | "number": 1, | ||
234 | "before": False, | ||
235 | "after": True, | ||
236 | "fetch_balances": ["begin", "end"], | ||
237 | "prepare_trades": {}, | ||
238 | "prepare_orders": { "only": "dispose", "compute_value": "average" }, | ||
239 | "run_orders": {}, | ||
240 | "follow_orders": {}, | ||
241 | "close_trades": {}, | ||
242 | }, | ||
243 | { | ||
244 | "name": "buy", | ||
245 | "number": 2, | ||
246 | "before": False, | ||
247 | "after": True, | ||
248 | "fetch_balances": ["begin", "end"], | ||
249 | "prepare_trades": { "only": "acquire" }, | ||
250 | "prepare_orders": { "only": "acquire", "compute_value": "average" }, | ||
251 | "move_balances": {}, | ||
252 | "run_orders": {}, | ||
253 | "follow_orders": {}, | ||
254 | "close_trades": {}, | ||
255 | }, | ||
256 | ], | ||
257 | "sell_all": [ | ||
258 | { | ||
259 | "name": "all_sell", | ||
260 | "number": 1, | ||
261 | "before": True, | ||
262 | "after": False, | ||
263 | "fetch_balances": ["begin", "end"], | ||
264 | "prepare_trades": { "repartition": { "base_currency": (1, "long") } }, | ||
265 | "prepare_orders": { "compute_value": "average" }, | ||
266 | "run_orders": {}, | ||
267 | "follow_orders": {}, | ||
268 | "close_trades": {}, | ||
269 | }, | ||
270 | { | ||
271 | "name": "wait", | ||
272 | "number": 2, | ||
273 | "before": False, | ||
274 | "after": True, | ||
275 | "wait_for_recent": {}, | ||
276 | }, | ||
277 | { | ||
278 | "name": "all_buy", | ||
279 | "number": 3, | ||
280 | "before": False, | ||
281 | "after": True, | ||
282 | "fetch_balances": ["begin", "end"], | ||
283 | "prepare_trades": {}, | ||
284 | "prepare_orders": { "compute_value": "average" }, | ||
285 | "move_balances": {}, | ||
286 | "run_orders": {}, | ||
287 | "follow_orders": {}, | ||
288 | "close_trades": {}, | ||
289 | }, | ||
290 | ] | ||
291 | } | ||
292 | |||
293 | ordered_actions = [ | ||
294 | "wait_for_recent", "prepare_trades", "prepare_orders", | ||
295 | "move_balances", "run_orders", "follow_orders", | ||
296 | "close_trades"] | ||
297 | |||
298 | def __init__(self, market): | ||
299 | self.market = market | ||
300 | |||
301 | def select_steps(self, scenario, step): | ||
302 | if step == "all": | ||
303 | return scenario | ||
304 | elif step == "before" or step == "after": | ||
305 | return list(filter(lambda x: step in x and x[step], scenario)) | ||
306 | elif type(step) == int: | ||
307 | return [scenario[step-1]] | ||
308 | elif type(step) == str: | ||
309 | return list(filter(lambda x: x["name"] == step, scenario)) | ||
310 | else: | ||
311 | raise TypeError("Unknown step {}".format(step)) | ||
312 | |||
313 | def process(self, scenario_name, steps="all", **kwargs): | ||
314 | scenario = self.scenarios[scenario_name] | ||
315 | selected_steps = [] | ||
316 | |||
317 | if type(steps) == str or type(steps) == int: | ||
318 | selected_steps += self.select_steps(scenario, steps) | ||
319 | else: | ||
320 | for step in steps: | ||
321 | selected_steps += self.select_steps(scenario, step) | ||
322 | for step in selected_steps: | ||
323 | self.process_step(scenario_name, step, kwargs) | ||
324 | |||
325 | def process_step(self, scenario_name, step, kwargs): | ||
326 | process_name = "process_{}__{}_{}".format(scenario_name, step["number"], step["name"]) | ||
327 | self.market.report.log_stage("{}_begin".format(process_name)) | ||
328 | if "begin" in step.get("fetch_balances", []): | ||
329 | self.market.balances.fetch_balances(tag="{}_begin".format(process_name)) | ||
330 | |||
331 | for action in self.ordered_actions: | ||
332 | if action in step: | ||
333 | self.run_action(action, step[action], kwargs) | ||
334 | |||
335 | if "end" in step.get("fetch_balances", []): | ||
336 | self.market.balances.fetch_balances(tag="{}_end".format(process_name)) | ||
337 | self.market.report.log_stage("{}_end".format(process_name)) | ||
338 | |||
339 | def method_arguments(self, action): | ||
340 | import inspect | ||
341 | |||
342 | if action == "wait_for_recent": | ||
343 | method = Portfolio.wait_for_recent | ||
344 | elif action == "prepare_trades": | ||
345 | method = self.market.prepare_trades | ||
346 | elif action == "prepare_orders": | ||
347 | method = self.market.trades.prepare_orders | ||
348 | elif action == "move_balances": | ||
349 | method = self.market.move_balances | ||
350 | elif action == "run_orders": | ||
351 | method = self.market.trades.run_orders | ||
352 | elif action == "follow_orders": | ||
353 | method = self.market.follow_orders | ||
354 | elif action == "close_trades": | ||
355 | method = self.market.trades.close_trades | ||
356 | |||
357 | signature = inspect.getfullargspec(method) | ||
358 | defaults = signature.defaults or [] | ||
359 | kwargs = signature.args[-len(defaults):] | ||
360 | |||
361 | return [method, kwargs] | ||
362 | |||
363 | def parse_args(self, action, default_args, kwargs): | ||
364 | method, allowed_arguments = self.method_arguments(action) | ||
365 | args = {k: v for k, v in {**default_args, **kwargs}.items() if k in allowed_arguments } | ||
366 | |||
367 | if "repartition" in args and "base_currency" in args["repartition"]: | ||
368 | r = args["repartition"] | ||
369 | r[args.get("base_currency", "BTC")] = r.pop("base_currency") | ||
370 | |||
371 | return method, args | ||
372 | |||
373 | def run_action(self, action, default_args, kwargs): | ||
374 | method, args = self.parse_args(action, default_args, kwargs) | ||
375 | |||
376 | method(**args) | ||
diff --git a/portfolio.py b/portfolio.py index 0f2c011..554b34f 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -1,87 +1,10 @@ | |||
1 | import time | 1 | from datetime import datetime |
2 | from datetime import datetime, timedelta | ||
3 | from decimal import Decimal as D, ROUND_DOWN | 2 | 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 | 3 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound |
7 | from retry import retry | 4 | from retry import retry |
8 | import requests | ||
9 | 5 | ||
10 | # FIXME: correctly handle web call timeouts | 6 | # FIXME: correctly handle web call timeouts |
11 | 7 | ||
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 | |||
85 | class Computation: | 8 | class Computation: |
86 | computations = { | 9 | computations = { |
87 | "default": lambda x, y: x[y], | 10 | "default": lambda x, y: x[y], |
@@ -1,10 +1,14 @@ | |||
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): |
@@ -13,6 +17,10 @@ class ReportStore: | |||
13 | 17 | ||
14 | self.logs = [] | 18 | self.logs = [] |
15 | 19 | ||
20 | def merge(self, other_report): | ||
21 | self.logs += other_report.logs | ||
22 | self.logs.sort(key=lambda x: x["date"]) | ||
23 | |||
16 | def print_log(self, message): | 24 | def print_log(self, message): |
17 | message = str(message) | 25 | message = str(message) |
18 | if self.verbose_print: | 26 | if self.verbose_print: |
@@ -213,7 +221,7 @@ class BalanceStore: | |||
213 | 221 | ||
214 | def dispatch_assets(self, amount, liquidity="medium", repartition=None): | 222 | def dispatch_assets(self, amount, liquidity="medium", repartition=None): |
215 | if repartition is None: | 223 | if repartition is None: |
216 | repartition = portfolio.Portfolio.repartition(self.market, liquidity=liquidity) | 224 | repartition = Portfolio.repartition(liquidity=liquidity) |
217 | sum_ratio = sum([v[0] for k, v in repartition.items()]) | 225 | sum_ratio = sum([v[0] for k, v in repartition.items()]) |
218 | amounts = {} | 226 | amounts = {} |
219 | for currency, (ptt, trade_type) in repartition.items(): | 227 | for currency, (ptt, trade_type) in repartition.items(): |
@@ -301,4 +309,165 @@ class TradeStore: | |||
301 | for order in self.all_orders(state="open"): | 309 | for order in self.all_orders(state="open"): |
302 | order.get_status() | 310 | order.get_status() |
303 | 311 | ||
312 | class NoopLock: | ||
313 | def __enter__(self, *args): | ||
314 | pass | ||
315 | def __exit__(self, *args): | ||
316 | pass | ||
317 | |||
318 | class LockedVar: | ||
319 | def __init__(self, value): | ||
320 | self.lock = NoopLock() | ||
321 | self.val = value | ||
322 | |||
323 | def start_lock(self): | ||
324 | import threading | ||
325 | self.lock = threading.Lock() | ||
326 | |||
327 | def set(self, value): | ||
328 | with self.lock: | ||
329 | self.val = value | ||
330 | |||
331 | def get(self, key=None): | ||
332 | with self.lock: | ||
333 | if key is not None and isinstance(self.val, dict): | ||
334 | return self.val.get(key) | ||
335 | else: | ||
336 | return self.val | ||
337 | |||
338 | def __getattr__(self, key): | ||
339 | with self.lock: | ||
340 | return getattr(self.val, key) | ||
341 | |||
342 | class Portfolio: | ||
343 | URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json" | ||
344 | data = LockedVar(None) | ||
345 | liquidities = LockedVar({}) | ||
346 | last_date = LockedVar(None) | ||
347 | report = LockedVar(ReportStore(None)) | ||
348 | worker = None | ||
349 | worker_started = False | ||
350 | worker_notify = None | ||
351 | callback = None | ||
352 | |||
353 | @classmethod | ||
354 | def start_worker(cls, poll=30): | ||
355 | import threading | ||
356 | |||
357 | cls.worker = threading.Thread(name="portfolio", daemon=True, | ||
358 | target=cls.wait_for_notification, kwargs={"poll": poll}) | ||
359 | cls.worker_notify = threading.Event() | ||
360 | cls.callback = threading.Event() | ||
361 | |||
362 | cls.last_date.start_lock() | ||
363 | cls.liquidities.start_lock() | ||
364 | cls.report.start_lock() | ||
365 | |||
366 | cls.worker_started = True | ||
367 | cls.worker.start() | ||
368 | |||
369 | @classmethod | ||
370 | def is_worker_thread(cls): | ||
371 | if cls.worker is None: | ||
372 | return False | ||
373 | else: | ||
374 | import threading | ||
375 | return cls.worker == threading.current_thread() | ||
376 | |||
377 | @classmethod | ||
378 | def wait_for_notification(cls, poll=30): | ||
379 | if not cls.is_worker_thread(): | ||
380 | raise RuntimeError("This method needs to be ran with the worker") | ||
381 | while cls.worker_started: | ||
382 | cls.worker_notify.wait() | ||
383 | cls.worker_notify.clear() | ||
384 | cls.report.print_log("Fetching cryptoportfolio") | ||
385 | cls.get_cryptoportfolio(refetch=True) | ||
386 | cls.callback.set() | ||
387 | time.sleep(poll) | ||
388 | |||
389 | @classmethod | ||
390 | def notify_and_wait(cls): | ||
391 | cls.callback.clear() | ||
392 | cls.worker_notify.set() | ||
393 | cls.callback.wait() | ||
394 | |||
395 | @classmethod | ||
396 | def wait_for_recent(cls, delta=4, poll=30): | ||
397 | cls.get_cryptoportfolio() | ||
398 | while cls.last_date.get() is None or datetime.now() - cls.last_date.get() > timedelta(delta): | ||
399 | if cls.worker is None: | ||
400 | time.sleep(poll) | ||
401 | cls.report.print_log("Attempt to fetch up-to-date cryptoportfolio") | ||
402 | cls.get_cryptoportfolio(refetch=True) | ||
403 | |||
404 | @classmethod | ||
405 | def repartition(cls, liquidity="medium"): | ||
406 | cls.get_cryptoportfolio() | ||
407 | liquidities = cls.liquidities.get(liquidity) | ||
408 | return liquidities[cls.last_date.get()] | ||
409 | |||
410 | @classmethod | ||
411 | def get_cryptoportfolio(cls, refetch=False): | ||
412 | if cls.data.get() is not None and not refetch: | ||
413 | return | ||
414 | if cls.worker is not None and not cls.is_worker_thread(): | ||
415 | cls.notify_and_wait() | ||
416 | return | ||
417 | try: | ||
418 | r = requests.get(cls.URL) | ||
419 | cls.report.log_http_request(r.request.method, | ||
420 | r.request.url, r.request.body, r.request.headers, r) | ||
421 | except Exception as e: | ||
422 | cls.report.log_error("get_cryptoportfolio", exception=e) | ||
423 | return | ||
424 | try: | ||
425 | cls.data.set(r.json(parse_int=D, parse_float=D)) | ||
426 | cls.parse_cryptoportfolio() | ||
427 | except (JSONDecodeError, SimpleJSONDecodeError): | ||
428 | cls.data.set(None) | ||
429 | cls.last_date.set(None) | ||
430 | cls.liquidities.set({}) | ||
431 | |||
432 | @classmethod | ||
433 | def parse_cryptoportfolio(cls): | ||
434 | def filter_weights(weight_hash): | ||
435 | if weight_hash[1][0] == 0: | ||
436 | return False | ||
437 | if weight_hash[0] == "_row": | ||
438 | return False | ||
439 | return True | ||
440 | |||
441 | def clean_weights(i): | ||
442 | def clean_weights_(h): | ||
443 | if h[0].endswith("s"): | ||
444 | return [h[0][0:-1], (h[1][i], "short")] | ||
445 | else: | ||
446 | return [h[0], (h[1][i], "long")] | ||
447 | return clean_weights_ | ||
448 | |||
449 | def parse_weights(portfolio_hash): | ||
450 | if "weights" not in portfolio_hash: | ||
451 | return {} | ||
452 | weights_hash = portfolio_hash["weights"] | ||
453 | weights = {} | ||
454 | for i in range(len(weights_hash["_row"])): | ||
455 | date = datetime.strptime(weights_hash["_row"][i], "%Y-%m-%d") | ||
456 | weights[date] = dict(filter( | ||
457 | filter_weights, | ||
458 | map(clean_weights(i), weights_hash.items()))) | ||
459 | return weights | ||
460 | |||
461 | high_liquidity = parse_weights(cls.data.get("portfolio_1")) | ||
462 | medium_liquidity = parse_weights(cls.data.get("portfolio_2")) | ||
463 | |||
464 | cls.liquidities.set({ | ||
465 | "medium": medium_liquidity, | ||
466 | "high": high_liquidity, | ||
467 | }) | ||
468 | cls.last_date.set(max( | ||
469 | max(medium_liquidity.keys(), default=datetime(1, 1, 1)), | ||
470 | max(high_liquidity.keys(), default=datetime(1, 1, 1)) | ||
471 | )) | ||
472 | |||
304 | 473 | ||
@@ -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: |
@@ -32,7 +33,15 @@ class WebMockTestCase(unittest.TestCase): | |||
32 | self.m.debug = False | 33 | self.m.debug = False |
33 | 34 | ||
34 | self.patchers = [ | 35 | self.patchers = [ |
35 | mock.patch.multiple(portfolio.Portfolio, last_date=None, data=None, liquidities={}), | 36 | mock.patch.multiple(market.Portfolio, |
37 | data=store.LockedVar(None), | ||
38 | liquidities=store.LockedVar({}), | ||
39 | last_date=store.LockedVar(None), | ||
40 | report=mock.Mock(), | ||
41 | worker=None, | ||
42 | worker_notify=None, | ||
43 | worker_started=False, | ||
44 | callback=None), | ||
36 | mock.patch.multiple(portfolio.Computation, | 45 | mock.patch.multiple(portfolio.Computation, |
37 | computations=portfolio.Computation.computations), | 46 | computations=portfolio.Computation.computations), |
38 | ] | 47 | ] |
@@ -126,174 +135,632 @@ class poloniexETest(unittest.TestCase): | |||
126 | } | 135 | } |
127 | self.assertEqual(expected, self.s.margin_summary()) | 136 | self.assertEqual(expected, self.s.margin_summary()) |
128 | 137 | ||
138 | def test_create_order(self): | ||
139 | with mock.patch.object(self.s, "create_exchange_order") as exchange,\ | ||
140 | mock.patch.object(self.s, "create_margin_order") as margin: | ||
141 | with self.subTest(account="unspecified"): | ||
142 | self.s.create_order("symbol", "type", "side", "amount", price="price", lending_rate="lending_rate", params="params") | ||
143 | exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | ||
144 | margin.assert_not_called() | ||
145 | exchange.reset_mock() | ||
146 | margin.reset_mock() | ||
147 | |||
148 | with self.subTest(account="exchange"): | ||
149 | self.s.create_order("symbol", "type", "side", "amount", account="exchange", price="price", lending_rate="lending_rate", params="params") | ||
150 | exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | ||
151 | margin.assert_not_called() | ||
152 | exchange.reset_mock() | ||
153 | margin.reset_mock() | ||
154 | |||
155 | with self.subTest(account="margin"): | ||
156 | self.s.create_order("symbol", "type", "side", "amount", account="margin", price="price", lending_rate="lending_rate", params="params") | ||
157 | margin.assert_called_once_with("symbol", "type", "side", "amount", lending_rate="lending_rate", price="price", params="params") | ||
158 | exchange.assert_not_called() | ||
159 | exchange.reset_mock() | ||
160 | margin.reset_mock() | ||
161 | |||
162 | with self.subTest(account="unknown"), self.assertRaises(NotImplementedError): | ||
163 | self.s.create_order("symbol", "type", "side", "amount", account="unknown") | ||
164 | |||
165 | def test_parse_ticker(self): | ||
166 | ticker = { | ||
167 | "high24hr": "12", | ||
168 | "low24hr": "10", | ||
169 | "highestBid": "10.5", | ||
170 | "lowestAsk": "11.5", | ||
171 | "last": "11", | ||
172 | "percentChange": "0.1", | ||
173 | "quoteVolume": "10", | ||
174 | "baseVolume": "20" | ||
175 | } | ||
176 | market = { | ||
177 | "symbol": "BTC/ETC" | ||
178 | } | ||
179 | with mock.patch.object(self.s, "milliseconds") as ms: | ||
180 | ms.return_value = 1520292715123 | ||
181 | result = self.s.parse_ticker(ticker, market) | ||
182 | |||
183 | expected = { | ||
184 | "symbol": "BTC/ETC", | ||
185 | "timestamp": 1520292715123, | ||
186 | "datetime": "2018-03-05T23:31:55.123Z", | ||
187 | "high": D("12"), | ||
188 | "low": D("10"), | ||
189 | "bid": D("10.5"), | ||
190 | "ask": D("11.5"), | ||
191 | "vwap": None, | ||
192 | "open": None, | ||
193 | "close": None, | ||
194 | "first": None, | ||
195 | "last": D("11"), | ||
196 | "change": D("0.1"), | ||
197 | "percentage": None, | ||
198 | "average": None, | ||
199 | "baseVolume": D("10"), | ||
200 | "quoteVolume": D("20"), | ||
201 | "info": ticker | ||
202 | } | ||
203 | self.assertEqual(expected, result) | ||
204 | |||
205 | def test_fetch_margin_balance(self): | ||
206 | with mock.patch.object(self.s, "privatePostGetMarginPosition") as get_margin_position: | ||
207 | get_margin_position.return_value = { | ||
208 | "BTC_DASH": { | ||
209 | "amount": "-0.1", | ||
210 | "basePrice": "0.06818560", | ||
211 | "lendingFees": "0.00000001", | ||
212 | "liquidationPrice": "0.15107132", | ||
213 | "pl": "-0.00000371", | ||
214 | "total": "0.00681856", | ||
215 | "type": "short" | ||
216 | }, | ||
217 | "BTC_ETC": { | ||
218 | "amount": "-0.6", | ||
219 | "basePrice": "0.1", | ||
220 | "lendingFees": "0.00000001", | ||
221 | "liquidationPrice": "0.6", | ||
222 | "pl": "0.00000371", | ||
223 | "total": "0.06", | ||
224 | "type": "short" | ||
225 | }, | ||
226 | "BTC_ETH": { | ||
227 | "amount": "0", | ||
228 | "basePrice": "0", | ||
229 | "lendingFees": "0", | ||
230 | "liquidationPrice": "-1", | ||
231 | "pl": "0", | ||
232 | "total": "0", | ||
233 | "type": "none" | ||
234 | } | ||
235 | } | ||
236 | balances = self.s.fetch_margin_balance() | ||
237 | self.assertEqual(2, len(balances)) | ||
238 | expected = { | ||
239 | "DASH": { | ||
240 | "amount": D("-0.1"), | ||
241 | "borrowedPrice": D("0.06818560"), | ||
242 | "lendingFees": D("1E-8"), | ||
243 | "pl": D("-0.00000371"), | ||
244 | "liquidationPrice": D("0.15107132"), | ||
245 | "type": "short", | ||
246 | "total": D("0.00681856"), | ||
247 | "baseCurrency": "BTC" | ||
248 | }, | ||
249 | "ETC": { | ||
250 | "amount": D("-0.6"), | ||
251 | "borrowedPrice": D("0.1"), | ||
252 | "lendingFees": D("1E-8"), | ||
253 | "pl": D("0.00000371"), | ||
254 | "liquidationPrice": D("0.6"), | ||
255 | "type": "short", | ||
256 | "total": D("0.06"), | ||
257 | "baseCurrency": "BTC" | ||
258 | } | ||
259 | } | ||
260 | self.assertEqual(expected, balances) | ||
261 | |||
262 | def test_sum(self): | ||
263 | self.assertEqual(D("1.1"), self.s.sum(D("1"), D("0.1"))) | ||
264 | |||
265 | def test_fetch_balance(self): | ||
266 | with mock.patch.object(self.s, "load_markets") as load_markets,\ | ||
267 | mock.patch.object(self.s, "privatePostReturnCompleteBalances") as balances,\ | ||
268 | mock.patch.object(self.s, "common_currency_code") as ccc: | ||
269 | ccc.side_effect = ["ETH", "BTC", "DASH"] | ||
270 | balances.return_value = { | ||
271 | "ETH": { | ||
272 | "available": "10", | ||
273 | "onOrders": "1", | ||
274 | }, | ||
275 | "BTC": { | ||
276 | "available": "1", | ||
277 | "onOrders": "0", | ||
278 | }, | ||
279 | "DASH": { | ||
280 | "available": "0", | ||
281 | "onOrders": "3" | ||
282 | } | ||
283 | } | ||
284 | |||
285 | expected = { | ||
286 | "info": { | ||
287 | "ETH": {"available": "10", "onOrders": "1"}, | ||
288 | "BTC": {"available": "1", "onOrders": "0"}, | ||
289 | "DASH": {"available": "0", "onOrders": "3"} | ||
290 | }, | ||
291 | "ETH": {"free": D("10"), "used": D("1"), "total": D("11")}, | ||
292 | "BTC": {"free": D("1"), "used": D("0"), "total": D("1")}, | ||
293 | "DASH": {"free": D("0"), "used": D("3"), "total": D("3")}, | ||
294 | "free": {"ETH": D("10"), "BTC": D("1"), "DASH": D("0")}, | ||
295 | "used": {"ETH": D("1"), "BTC": D("0"), "DASH": D("3")}, | ||
296 | "total": {"ETH": D("11"), "BTC": D("1"), "DASH": D("3")} | ||
297 | } | ||
298 | result = self.s.fetch_balance() | ||
299 | load_markets.assert_called_once() | ||
300 | self.assertEqual(expected, result) | ||
301 | |||
302 | def test_fetch_balance_per_type(self): | ||
303 | with mock.patch.object(self.s, "privatePostReturnAvailableAccountBalances") as balances: | ||
304 | balances.return_value = { | ||
305 | "exchange": { | ||
306 | "BLK": "159.83673869", | ||
307 | "BTC": "0.00005959", | ||
308 | "USDT": "0.00002625", | ||
309 | "XMR": "0.18719303" | ||
310 | }, | ||
311 | "margin": { | ||
312 | "BTC": "0.03019227" | ||
313 | } | ||
314 | } | ||
315 | expected = { | ||
316 | "info": { | ||
317 | "exchange": { | ||
318 | "BLK": "159.83673869", | ||
319 | "BTC": "0.00005959", | ||
320 | "USDT": "0.00002625", | ||
321 | "XMR": "0.18719303" | ||
322 | }, | ||
323 | "margin": { | ||
324 | "BTC": "0.03019227" | ||
325 | } | ||
326 | }, | ||
327 | "exchange": { | ||
328 | "BLK": D("159.83673869"), | ||
329 | "BTC": D("0.00005959"), | ||
330 | "USDT": D("0.00002625"), | ||
331 | "XMR": D("0.18719303") | ||
332 | }, | ||
333 | "margin": {"BTC": D("0.03019227")}, | ||
334 | "BLK": {"exchange": D("159.83673869")}, | ||
335 | "BTC": {"exchange": D("0.00005959"), "margin": D("0.03019227")}, | ||
336 | "USDT": {"exchange": D("0.00002625")}, | ||
337 | "XMR": {"exchange": D("0.18719303")} | ||
338 | } | ||
339 | result = self.s.fetch_balance_per_type() | ||
340 | self.assertEqual(expected, result) | ||
341 | |||
342 | def test_fetch_all_balances(self): | ||
343 | import json | ||
344 | with mock.patch.object(self.s, "load_markets") as load_markets,\ | ||
345 | mock.patch.object(self.s, "privatePostGetMarginPosition") as margin_balance,\ | ||
346 | mock.patch.object(self.s, "privatePostReturnCompleteBalances") as balance,\ | ||
347 | mock.patch.object(self.s, "privatePostReturnAvailableAccountBalances") as balance_per_type: | ||
348 | |||
349 | with open("test_samples/poloniexETest.test_fetch_all_balances.1.json") as f: | ||
350 | balance.return_value = json.load(f) | ||
351 | with open("test_samples/poloniexETest.test_fetch_all_balances.2.json") as f: | ||
352 | margin_balance.return_value = json.load(f) | ||
353 | with open("test_samples/poloniexETest.test_fetch_all_balances.3.json") as f: | ||
354 | balance_per_type.return_value = json.load(f) | ||
355 | |||
356 | result = self.s.fetch_all_balances() | ||
357 | expected_doge = { | ||
358 | "total": D("-12779.79821852"), | ||
359 | "exchange_used": D("0E-8"), | ||
360 | "exchange_total": D("0E-8"), | ||
361 | "exchange_free": D("0E-8"), | ||
362 | "margin_available": 0, | ||
363 | "margin_in_position": 0, | ||
364 | "margin_borrowed": D("12779.79821852"), | ||
365 | "margin_total": D("-12779.79821852"), | ||
366 | "margin_pending_gain": 0, | ||
367 | "margin_lending_fees": D("-9E-8"), | ||
368 | "margin_pending_base_gain": D("0.00024059"), | ||
369 | "margin_position_type": "short", | ||
370 | "margin_liquidation_price": D("0.00000246"), | ||
371 | "margin_borrowed_base_price": D("0.00599149"), | ||
372 | "margin_borrowed_base_currency": "BTC" | ||
373 | } | ||
374 | expected_btc = {"total": D("0.05432165"), | ||
375 | "exchange_used": D("0E-8"), | ||
376 | "exchange_total": D("0.00005959"), | ||
377 | "exchange_free": D("0.00005959"), | ||
378 | "margin_available": D("0.03019227"), | ||
379 | "margin_in_position": D("0.02406979"), | ||
380 | "margin_borrowed": 0, | ||
381 | "margin_total": D("0.05426206"), | ||
382 | "margin_pending_gain": D("0.00093955"), | ||
383 | "margin_lending_fees": 0, | ||
384 | "margin_pending_base_gain": 0, | ||
385 | "margin_position_type": None, | ||
386 | "margin_liquidation_price": 0, | ||
387 | "margin_borrowed_base_price": 0, | ||
388 | "margin_borrowed_base_currency": None | ||
389 | } | ||
390 | expected_xmr = {"total": D("0.18719303"), | ||
391 | "exchange_used": D("0E-8"), | ||
392 | "exchange_total": D("0.18719303"), | ||
393 | "exchange_free": D("0.18719303"), | ||
394 | "margin_available": 0, | ||
395 | "margin_in_position": 0, | ||
396 | "margin_borrowed": 0, | ||
397 | "margin_total": 0, | ||
398 | "margin_pending_gain": 0, | ||
399 | "margin_lending_fees": 0, | ||
400 | "margin_pending_base_gain": 0, | ||
401 | "margin_position_type": None, | ||
402 | "margin_liquidation_price": 0, | ||
403 | "margin_borrowed_base_price": 0, | ||
404 | "margin_borrowed_base_currency": None | ||
405 | } | ||
406 | self.assertEqual(expected_xmr, result["XMR"]) | ||
407 | self.assertEqual(expected_doge, result["DOGE"]) | ||
408 | self.assertEqual(expected_btc, result["BTC"]) | ||
409 | |||
410 | def test_create_margin_order(self): | ||
411 | with self.assertRaises(market.ExchangeError): | ||
412 | self.s.create_margin_order("FOO", "market", "buy", "10") | ||
413 | |||
414 | with mock.patch.object(self.s, "load_markets") as load_markets,\ | ||
415 | mock.patch.object(self.s, "privatePostMarginBuy") as margin_buy,\ | ||
416 | mock.patch.object(self.s, "privatePostMarginSell") as margin_sell,\ | ||
417 | mock.patch.object(self.s, "market") as market_mock,\ | ||
418 | mock.patch.object(self.s, "price_to_precision") as ptp,\ | ||
419 | mock.patch.object(self.s, "amount_to_precision") as atp: | ||
420 | |||
421 | margin_buy.return_value = { | ||
422 | "orderNumber": 123 | ||
423 | } | ||
424 | margin_sell.return_value = { | ||
425 | "orderNumber": 456 | ||
426 | } | ||
427 | market_mock.return_value = { "id": "BTC_ETC", "symbol": "BTC_ETC" } | ||
428 | ptp.return_value = D("0.1") | ||
429 | atp.return_value = D("12") | ||
430 | |||
431 | order = self.s.create_margin_order("BTC_ETC", "margin", "buy", "12", price="0.1") | ||
432 | self.assertEqual(123, order["id"]) | ||
433 | margin_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")}) | ||
434 | margin_sell.assert_not_called() | ||
435 | margin_buy.reset_mock() | ||
436 | margin_sell.reset_mock() | ||
437 | |||
438 | order = self.s.create_margin_order("BTC_ETC", "margin", "sell", "12", lending_rate="0.01", price="0.1") | ||
439 | self.assertEqual(456, order["id"]) | ||
440 | margin_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12"), "lendingRate": "0.01"}) | ||
441 | margin_buy.assert_not_called() | ||
442 | |||
443 | def test_create_exchange_order(self): | ||
444 | with mock.patch.object(market.ccxt.poloniex, "create_order") as create_order: | ||
445 | self.s.create_order("symbol", "type", "side", "amount", price="price", params="params") | ||
446 | |||
447 | create_order.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | ||
448 | |||
129 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 449 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
130 | class PortfolioTest(WebMockTestCase): | 450 | class NoopLockTest(unittest.TestCase): |
131 | def fill_data(self): | 451 | def test_with(self): |
132 | if self.json_response is not None: | 452 | noop_lock = store.NoopLock() |
133 | portfolio.Portfolio.data = self.json_response | 453 | with noop_lock: |
454 | self.assertTrue(True) | ||
455 | |||
456 | @unittest.skipUnless("unit" in limits, "Unit skipped") | ||
457 | class LockedVar(unittest.TestCase): | ||
458 | |||
459 | def test_values(self): | ||
460 | locked_var = store.LockedVar("Foo") | ||
461 | self.assertIsInstance(locked_var.lock, store.NoopLock) | ||
462 | self.assertEqual("Foo", locked_var.val) | ||
463 | |||
464 | def test_get(self): | ||
465 | with self.subTest(desc="Normal case"): | ||
466 | locked_var = store.LockedVar("Foo") | ||
467 | self.assertEqual("Foo", locked_var.get()) | ||
468 | with self.subTest(desc="Dict"): | ||
469 | locked_var = store.LockedVar({"foo": "bar"}) | ||
470 | self.assertEqual({"foo": "bar"}, locked_var.get()) | ||
471 | self.assertEqual("bar", locked_var.get("foo")) | ||
472 | self.assertIsNone(locked_var.get("other")) | ||
473 | |||
474 | def test_set(self): | ||
475 | locked_var = store.LockedVar("Foo") | ||
476 | locked_var.set("Bar") | ||
477 | self.assertEqual("Bar", locked_var.get()) | ||
478 | |||
479 | def test__getattr(self): | ||
480 | dummy = type('Dummy', (object,), {})() | ||
481 | dummy.attribute = "Hey" | ||
482 | |||
483 | locked_var = store.LockedVar(dummy) | ||
484 | self.assertEqual("Hey", locked_var.attribute) | ||
485 | with self.assertRaises(AttributeError): | ||
486 | locked_var.other | ||
487 | |||
488 | def test_start_lock(self): | ||
489 | locked_var = store.LockedVar("Foo") | ||
490 | locked_var.start_lock() | ||
491 | self.assertEqual("lock", locked_var.lock.__class__.__name__) | ||
492 | |||
493 | thread1 = threading.Thread(target=locked_var.set, args=["Bar1"]) | ||
494 | thread2 = threading.Thread(target=locked_var.set, args=["Bar2"]) | ||
495 | thread3 = threading.Thread(target=locked_var.set, args=["Bar3"]) | ||
496 | |||
497 | with locked_var.lock: | ||
498 | thread1.start() | ||
499 | thread2.start() | ||
500 | thread3.start() | ||
501 | |||
502 | self.assertEqual("Foo", locked_var.val) | ||
503 | thread1.join() | ||
504 | thread2.join() | ||
505 | thread3.join() | ||
506 | self.assertEqual("Bar", locked_var.get()[0:3]) | ||
507 | |||
508 | def test_wait_for_notification(self): | ||
509 | with self.assertRaises(RuntimeError): | ||
510 | store.Portfolio.wait_for_notification() | ||
511 | |||
512 | with mock.patch.object(store.Portfolio, "get_cryptoportfolio") as get,\ | ||
513 | mock.patch.object(store.Portfolio, "report") as report,\ | ||
514 | mock.patch.object(store.time, "sleep") as sleep: | ||
515 | store.Portfolio.start_worker(poll=3) | ||
516 | |||
517 | store.Portfolio.worker_notify.set() | ||
518 | |||
519 | store.Portfolio.callback.wait() | ||
520 | |||
521 | report.print_log.assert_called_once_with("Fetching cryptoportfolio") | ||
522 | get.assert_called_once_with(refetch=True) | ||
523 | sleep.assert_called_once_with(3) | ||
524 | self.assertFalse(store.Portfolio.worker_notify.is_set()) | ||
525 | self.assertTrue(store.Portfolio.worker.is_alive()) | ||
526 | |||
527 | store.Portfolio.callback.clear() | ||
528 | store.Portfolio.worker_started = False | ||
529 | store.Portfolio.worker_notify.set() | ||
530 | store.Portfolio.callback.wait() | ||
531 | |||
532 | self.assertFalse(store.Portfolio.worker.is_alive()) | ||
533 | |||
534 | def test_notify_and_wait(self): | ||
535 | with mock.patch.object(store.Portfolio, "callback") as callback,\ | ||
536 | mock.patch.object(store.Portfolio, "worker_notify") as worker_notify: | ||
537 | store.Portfolio.notify_and_wait() | ||
538 | callback.clear.assert_called_once_with() | ||
539 | worker_notify.set.assert_called_once_with() | ||
540 | callback.wait.assert_called_once_with() | ||
134 | 541 | ||
542 | @unittest.skipUnless("unit" in limits, "Unit skipped") | ||
543 | class PortfolioTest(WebMockTestCase): | ||
135 | def setUp(self): | 544 | def setUp(self): |
136 | super(PortfolioTest, self).setUp() | 545 | super(PortfolioTest, self).setUp() |
137 | 546 | ||
138 | with open("test_portfolio.json") as example: | 547 | with open("test_samples/test_portfolio.json") as example: |
139 | self.json_response = example.read() | 548 | self.json_response = example.read() |
140 | 549 | ||
141 | self.wm.get(portfolio.Portfolio.URL, text=self.json_response) | 550 | self.wm.get(market.Portfolio.URL, text=self.json_response) |
142 | 551 | ||
143 | def test_get_cryptoportfolio(self): | 552 | @mock.patch.object(market.Portfolio, "parse_cryptoportfolio") |
144 | self.wm.get(portfolio.Portfolio.URL, [ | 553 | def test_get_cryptoportfolio(self, parse_cryptoportfolio): |
145 | {"text":'{ "foo": "bar" }', "status_code": 200}, | 554 | with self.subTest(parallel=False): |
146 | {"text": "System Error", "status_code": 500}, | 555 | self.wm.get(market.Portfolio.URL, [ |
147 | {"exc": requests.exceptions.ConnectTimeout}, | 556 | {"text":'{ "foo": "bar" }', "status_code": 200}, |
148 | ]) | 557 | {"text": "System Error", "status_code": 500}, |
149 | portfolio.Portfolio.get_cryptoportfolio(self.m) | 558 | {"exc": requests.exceptions.ConnectTimeout}, |
150 | self.assertIn("foo", portfolio.Portfolio.data) | 559 | ]) |
151 | self.assertEqual("bar", portfolio.Portfolio.data["foo"]) | 560 | market.Portfolio.get_cryptoportfolio() |
152 | self.assertTrue(self.wm.called) | 561 | self.assertIn("foo", market.Portfolio.data.get()) |
153 | self.assertEqual(1, self.wm.call_count) | 562 | self.assertEqual("bar", market.Portfolio.data.get()["foo"]) |
154 | self.m.report.log_error.assert_not_called() | 563 | self.assertTrue(self.wm.called) |
155 | self.m.report.log_http_request.assert_called_once() | 564 | self.assertEqual(1, self.wm.call_count) |
156 | self.m.report.log_http_request.reset_mock() | 565 | market.Portfolio.report.log_error.assert_not_called() |
157 | 566 | market.Portfolio.report.log_http_request.assert_called_once() | |
158 | portfolio.Portfolio.get_cryptoportfolio(self.m) | 567 | parse_cryptoportfolio.assert_called_once_with() |
159 | self.assertIsNone(portfolio.Portfolio.data) | 568 | market.Portfolio.report.log_http_request.reset_mock() |
160 | self.assertEqual(2, self.wm.call_count) | 569 | parse_cryptoportfolio.reset_mock() |
161 | self.m.report.log_error.assert_not_called() | 570 | market.Portfolio.data = store.LockedVar(None) |
162 | self.m.report.log_http_request.assert_called_once() | 571 | |
163 | self.m.report.log_http_request.reset_mock() | 572 | market.Portfolio.get_cryptoportfolio() |
164 | 573 | self.assertIsNone(market.Portfolio.data.get()) | |
165 | 574 | self.assertEqual(2, self.wm.call_count) | |
166 | portfolio.Portfolio.data = "Foo" | 575 | parse_cryptoportfolio.assert_not_called() |
167 | portfolio.Portfolio.get_cryptoportfolio(self.m) | 576 | market.Portfolio.report.log_error.assert_not_called() |
168 | self.assertEqual("Foo", portfolio.Portfolio.data) | 577 | market.Portfolio.report.log_http_request.assert_called_once() |
169 | self.assertEqual(3, self.wm.call_count) | 578 | market.Portfolio.report.log_http_request.reset_mock() |
170 | self.m.report.log_error.assert_called_once_with("get_cryptoportfolio", | 579 | parse_cryptoportfolio.reset_mock() |
171 | exception=mock.ANY) | 580 | |
172 | self.m.report.log_http_request.assert_not_called() | 581 | market.Portfolio.data = store.LockedVar("Foo") |
582 | market.Portfolio.get_cryptoportfolio() | ||
583 | self.assertEqual(2, self.wm.call_count) | ||
584 | parse_cryptoportfolio.assert_not_called() | ||
585 | |||
586 | market.Portfolio.get_cryptoportfolio(refetch=True) | ||
587 | self.assertEqual("Foo", market.Portfolio.data.get()) | ||
588 | self.assertEqual(3, self.wm.call_count) | ||
589 | market.Portfolio.report.log_error.assert_called_once_with("get_cryptoportfolio", | ||
590 | exception=mock.ANY) | ||
591 | market.Portfolio.report.log_http_request.assert_not_called() | ||
592 | with self.subTest(parallel=True): | ||
593 | with mock.patch.object(market.Portfolio, "is_worker_thread") as is_worker,\ | ||
594 | mock.patch.object(market.Portfolio, "notify_and_wait") as notify: | ||
595 | with self.subTest(worker=True): | ||
596 | market.Portfolio.data = store.LockedVar(None) | ||
597 | market.Portfolio.worker = mock.Mock() | ||
598 | is_worker.return_value = True | ||
599 | self.wm.get(market.Portfolio.URL, [ | ||
600 | {"text":'{ "foo": "bar" }', "status_code": 200}, | ||
601 | ]) | ||
602 | market.Portfolio.get_cryptoportfolio() | ||
603 | self.assertIn("foo", market.Portfolio.data.get()) | ||
604 | parse_cryptoportfolio.reset_mock() | ||
605 | with self.subTest(worker=False): | ||
606 | market.Portfolio.data = store.LockedVar(None) | ||
607 | market.Portfolio.worker = mock.Mock() | ||
608 | is_worker.return_value = False | ||
609 | market.Portfolio.get_cryptoportfolio() | ||
610 | notify.assert_called_once_with() | ||
611 | parse_cryptoportfolio.assert_not_called() | ||
173 | 612 | ||
174 | def test_parse_cryptoportfolio(self): | 613 | def test_parse_cryptoportfolio(self): |
175 | portfolio.Portfolio.parse_cryptoportfolio(self.m) | 614 | with self.subTest(description="Normal case"): |
176 | 615 | market.Portfolio.data = store.LockedVar(store.json.loads( | |
177 | self.assertListEqual( | 616 | self.json_response, parse_int=D, parse_float=D)) |
178 | ["medium", "high"], | 617 | 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 | 618 | ||
248 | self.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m)) | 619 | self.assertListEqual( |
249 | self.assertEqual(expected_medium, portfolio.Portfolio.repartition(self.m, liquidity="medium")) | 620 | ["medium", "high"], |
250 | self.assertEqual(expected_high, portfolio.Portfolio.repartition(self.m, liquidity="high")) | 621 | list(market.Portfolio.liquidities.get().keys())) |
251 | 622 | ||
252 | self.assertEqual(1, self.wm.call_count) | 623 | liquidities = market.Portfolio.liquidities.get() |
624 | self.assertEqual(10, len(liquidities["medium"].keys())) | ||
625 | self.assertEqual(10, len(liquidities["high"].keys())) | ||
253 | 626 | ||
254 | portfolio.Portfolio.repartition(self.m) | 627 | expected = { |
255 | self.assertEqual(1, self.wm.call_count) | 628 | 'BTC': (D("0.2857"), "long"), |
629 | 'DGB': (D("0.1015"), "long"), | ||
630 | 'DOGE': (D("0.1805"), "long"), | ||
631 | 'SC': (D("0.0623"), "long"), | ||
632 | 'ZEC': (D("0.3701"), "long"), | ||
633 | } | ||
634 | date = portfolio.datetime(2018, 1, 8) | ||
635 | self.assertDictEqual(expected, liquidities["high"][date]) | ||
256 | 636 | ||
257 | portfolio.Portfolio.repartition(self.m, refetch=True) | 637 | expected = { |
258 | self.assertEqual(2, self.wm.call_count) | 638 | 'BTC': (D("1.1102e-16"), "long"), |
259 | self.m.report.log_http_request.assert_called() | 639 | 'ETC': (D("0.1"), "long"), |
260 | self.assertEqual(2, self.m.report.log_http_request.call_count) | 640 | 'FCT': (D("0.1"), "long"), |
641 | 'GAS': (D("0.1"), "long"), | ||
642 | 'NAV': (D("0.1"), "long"), | ||
643 | 'OMG': (D("0.1"), "long"), | ||
644 | 'OMNI': (D("0.1"), "long"), | ||
645 | 'PPC': (D("0.1"), "long"), | ||
646 | 'RIC': (D("0.1"), "long"), | ||
647 | 'VIA': (D("0.1"), "long"), | ||
648 | 'XCP': (D("0.1"), "long"), | ||
649 | } | ||
650 | self.assertDictEqual(expected, liquidities["medium"][date]) | ||
651 | self.assertEqual(portfolio.datetime(2018, 1, 15), market.Portfolio.last_date.get()) | ||
652 | |||
653 | with self.subTest(description="Missing weight"): | ||
654 | data = store.json.loads(self.json_response, parse_int=D, parse_float=D) | ||
655 | del(data["portfolio_2"]["weights"]) | ||
656 | market.Portfolio.data = store.LockedVar(data) | ||
657 | |||
658 | market.Portfolio.parse_cryptoportfolio() | ||
659 | self.assertListEqual( | ||
660 | ["medium", "high"], | ||
661 | list(market.Portfolio.liquidities.get().keys())) | ||
662 | self.assertEqual({}, market.Portfolio.liquidities.get("medium")) | ||
663 | |||
664 | with self.subTest(description="All missing weights"): | ||
665 | data = store.json.loads(self.json_response, parse_int=D, parse_float=D) | ||
666 | del(data["portfolio_1"]["weights"]) | ||
667 | del(data["portfolio_2"]["weights"]) | ||
668 | market.Portfolio.data = store.LockedVar(data) | ||
669 | |||
670 | market.Portfolio.parse_cryptoportfolio() | ||
671 | self.assertEqual({}, market.Portfolio.liquidities.get("medium")) | ||
672 | self.assertEqual({}, market.Portfolio.liquidities.get("high")) | ||
673 | self.assertEqual(datetime.datetime(1,1,1), market.Portfolio.last_date.get()) | ||
674 | |||
675 | |||
676 | @mock.patch.object(market.Portfolio, "get_cryptoportfolio") | ||
677 | def test_repartition(self, get_cryptoportfolio): | ||
678 | market.Portfolio.liquidities = store.LockedVar({ | ||
679 | "medium": { | ||
680 | "2018-03-01": "medium_2018-03-01", | ||
681 | "2018-03-08": "medium_2018-03-08", | ||
682 | }, | ||
683 | "high": { | ||
684 | "2018-03-01": "high_2018-03-01", | ||
685 | "2018-03-08": "high_2018-03-08", | ||
686 | } | ||
687 | }) | ||
688 | market.Portfolio.last_date = store.LockedVar("2018-03-08") | ||
261 | 689 | ||
262 | @mock.patch.object(portfolio.time, "sleep") | 690 | self.assertEqual("medium_2018-03-08", market.Portfolio.repartition()) |
263 | @mock.patch.object(portfolio.Portfolio, "repartition") | 691 | get_cryptoportfolio.assert_called_once_with() |
264 | def test_wait_for_recent(self, repartition, sleep): | 692 | self.assertEqual("medium_2018-03-08", market.Portfolio.repartition(liquidity="medium")) |
693 | self.assertEqual("high_2018-03-08", market.Portfolio.repartition(liquidity="high")) | ||
694 | |||
695 | @mock.patch.object(market.time, "sleep") | ||
696 | @mock.patch.object(market.Portfolio, "get_cryptoportfolio") | ||
697 | def test_wait_for_recent(self, get_cryptoportfolio, sleep): | ||
265 | self.call_count = 0 | 698 | self.call_count = 0 |
266 | def _repartition(market, refetch): | 699 | def _get(refetch=False): |
267 | self.assertEqual(self.m, market) | 700 | if self.call_count != 0: |
268 | self.assertTrue(refetch) | 701 | self.assertTrue(refetch) |
702 | else: | ||
703 | self.assertFalse(refetch) | ||
269 | self.call_count += 1 | 704 | self.call_count += 1 |
270 | portfolio.Portfolio.last_date = portfolio.datetime.now()\ | 705 | market.Portfolio.last_date = store.LockedVar(store.datetime.now()\ |
271 | - portfolio.timedelta(10)\ | 706 | - store.timedelta(10)\ |
272 | + portfolio.timedelta(self.call_count) | 707 | + store.timedelta(self.call_count)) |
273 | repartition.side_effect = _repartition | 708 | get_cryptoportfolio.side_effect = _get |
274 | 709 | ||
275 | portfolio.Portfolio.wait_for_recent(self.m) | 710 | market.Portfolio.wait_for_recent() |
276 | sleep.assert_called_with(30) | 711 | sleep.assert_called_with(30) |
277 | self.assertEqual(6, sleep.call_count) | 712 | self.assertEqual(6, sleep.call_count) |
278 | self.assertEqual(7, repartition.call_count) | 713 | self.assertEqual(7, get_cryptoportfolio.call_count) |
279 | self.m.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio") | 714 | market.Portfolio.report.print_log.assert_called_with("Attempt to fetch up-to-date cryptoportfolio") |
280 | 715 | ||
281 | sleep.reset_mock() | 716 | sleep.reset_mock() |
282 | repartition.reset_mock() | 717 | get_cryptoportfolio.reset_mock() |
283 | portfolio.Portfolio.last_date = None | 718 | market.Portfolio.last_date = store.LockedVar(None) |
284 | self.call_count = 0 | 719 | self.call_count = 0 |
285 | portfolio.Portfolio.wait_for_recent(self.m, delta=15) | 720 | market.Portfolio.wait_for_recent(delta=15) |
286 | sleep.assert_not_called() | 721 | sleep.assert_not_called() |
287 | self.assertEqual(1, repartition.call_count) | 722 | self.assertEqual(1, get_cryptoportfolio.call_count) |
288 | 723 | ||
289 | sleep.reset_mock() | 724 | sleep.reset_mock() |
290 | repartition.reset_mock() | 725 | get_cryptoportfolio.reset_mock() |
291 | portfolio.Portfolio.last_date = None | 726 | market.Portfolio.last_date = store.LockedVar(None) |
292 | self.call_count = 0 | 727 | self.call_count = 0 |
293 | portfolio.Portfolio.wait_for_recent(self.m, delta=1) | 728 | market.Portfolio.wait_for_recent(delta=1) |
294 | sleep.assert_called_with(30) | 729 | sleep.assert_called_with(30) |
295 | self.assertEqual(9, sleep.call_count) | 730 | self.assertEqual(9, sleep.call_count) |
296 | self.assertEqual(10, repartition.call_count) | 731 | self.assertEqual(10, get_cryptoportfolio.call_count) |
732 | |||
733 | def test_is_worker_thread(self): | ||
734 | with self.subTest(worker=None): | ||
735 | self.assertFalse(store.Portfolio.is_worker_thread()) | ||
736 | |||
737 | with self.subTest(worker="not self"),\ | ||
738 | mock.patch("threading.current_thread") as current_thread: | ||
739 | current = mock.Mock() | ||
740 | current_thread.return_value = current | ||
741 | store.Portfolio.worker = mock.Mock() | ||
742 | self.assertFalse(store.Portfolio.is_worker_thread()) | ||
743 | |||
744 | with self.subTest(worker="self"),\ | ||
745 | mock.patch("threading.current_thread") as current_thread: | ||
746 | current = mock.Mock() | ||
747 | current_thread.return_value = current | ||
748 | store.Portfolio.worker = current | ||
749 | self.assertTrue(store.Portfolio.is_worker_thread()) | ||
750 | |||
751 | def test_start_worker(self): | ||
752 | with mock.patch.object(store.Portfolio, "wait_for_notification") as notification: | ||
753 | store.Portfolio.start_worker() | ||
754 | notification.assert_called_once_with(poll=30) | ||
755 | |||
756 | self.assertEqual("lock", store.Portfolio.last_date.lock.__class__.__name__) | ||
757 | self.assertEqual("lock", store.Portfolio.liquidities.lock.__class__.__name__) | ||
758 | store.Portfolio.report.start_lock.assert_called_once_with() | ||
759 | |||
760 | self.assertIsNotNone(store.Portfolio.worker) | ||
761 | self.assertIsNotNone(store.Portfolio.worker_notify) | ||
762 | self.assertIsNotNone(store.Portfolio.callback) | ||
763 | self.assertTrue(store.Portfolio.worker_started) | ||
297 | 764 | ||
298 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 765 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
299 | class AmountTest(WebMockTestCase): | 766 | class AmountTest(WebMockTestCase): |
@@ -736,7 +1203,7 @@ class MarketTest(WebMockTestCase): | |||
736 | self.assertEqual("Foo", m.fetch_fees()) | 1203 | self.assertEqual("Foo", m.fetch_fees()) |
737 | self.ccxt.fetch_fees.assert_not_called() | 1204 | self.ccxt.fetch_fees.assert_not_called() |
738 | 1205 | ||
739 | @mock.patch.object(portfolio.Portfolio, "repartition") | 1206 | @mock.patch.object(market.Portfolio, "repartition") |
740 | @mock.patch.object(market.Market, "get_ticker") | 1207 | @mock.patch.object(market.Market, "get_ticker") |
741 | @mock.patch.object(market.TradeStore, "compute_trades") | 1208 | @mock.patch.object(market.TradeStore, "compute_trades") |
742 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): | 1209 | def test_prepare_trades(self, compute_trades, get_ticker, repartition): |
@@ -787,7 +1254,7 @@ class MarketTest(WebMockTestCase): | |||
787 | m.report.log_balances.assert_called_once_with(tag="tag") | 1254 | m.report.log_balances.assert_called_once_with(tag="tag") |
788 | 1255 | ||
789 | 1256 | ||
790 | @mock.patch.object(portfolio.time, "sleep") | 1257 | @mock.patch.object(market.time, "sleep") |
791 | @mock.patch.object(market.TradeStore, "all_orders") | 1258 | @mock.patch.object(market.TradeStore, "all_orders") |
792 | def test_follow_orders(self, all_orders, time_mock): | 1259 | def test_follow_orders(self, all_orders, time_mock): |
793 | for debug, sleep in [ | 1260 | for debug, sleep in [ |
@@ -907,7 +1374,172 @@ class MarketTest(WebMockTestCase): | |||
907 | self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") | 1374 | self.ccxt.transfer_balance.assert_any_call("BTC", 3, "exchange", "margin") |
908 | self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") | 1375 | self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") |
909 | self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") | 1376 | self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") |
910 | 1377 | ||
1378 | def test_store_report(self): | ||
1379 | |||
1380 | file_open = mock.mock_open() | ||
1381 | m = market.Market(self.ccxt, user_id=1) | ||
1382 | with self.subTest(file=None),\ | ||
1383 | mock.patch.object(m, "report") as report,\ | ||
1384 | mock.patch("market.open", file_open): | ||
1385 | m.store_report() | ||
1386 | report.merge.assert_called_with(store.Portfolio.report) | ||
1387 | file_open.assert_not_called() | ||
1388 | |||
1389 | report.reset_mock() | ||
1390 | file_open = mock.mock_open() | ||
1391 | m = market.Market(self.ccxt, report_path="present", user_id=1) | ||
1392 | with self.subTest(file="present"),\ | ||
1393 | mock.patch("market.open", file_open),\ | ||
1394 | mock.patch.object(m, "report") as report,\ | ||
1395 | mock.patch.object(market, "datetime") as time_mock: | ||
1396 | |||
1397 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
1398 | report.to_json.return_value = "json_content" | ||
1399 | |||
1400 | m.store_report() | ||
1401 | |||
1402 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") | ||
1403 | file_open().write.assert_called_once_with("json_content") | ||
1404 | m.report.to_json.assert_called_once_with() | ||
1405 | report.merge.assert_called_with(store.Portfolio.report) | ||
1406 | |||
1407 | report.reset_mock() | ||
1408 | |||
1409 | m = market.Market(self.ccxt, report_path="error", user_id=1) | ||
1410 | with self.subTest(file="error"),\ | ||
1411 | mock.patch("market.open") as file_open,\ | ||
1412 | mock.patch.object(m, "report") as report,\ | ||
1413 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | ||
1414 | file_open.side_effect = FileNotFoundError | ||
1415 | |||
1416 | m.store_report() | ||
1417 | |||
1418 | report.merge.assert_called_with(store.Portfolio.report) | ||
1419 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | ||
1420 | |||
1421 | def test_print_orders(self): | ||
1422 | m = market.Market(self.ccxt) | ||
1423 | with mock.patch.object(m.report, "log_stage") as log_stage,\ | ||
1424 | mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ | ||
1425 | mock.patch.object(m, "prepare_trades") as prepare_trades,\ | ||
1426 | mock.patch.object(m.trades, "prepare_orders") as prepare_orders: | ||
1427 | m.print_orders() | ||
1428 | |||
1429 | log_stage.assert_called_with("print_orders") | ||
1430 | fetch_balances.assert_called_with(tag="print_orders") | ||
1431 | prepare_trades.assert_called_with(base_currency="BTC", | ||
1432 | compute_value="average") | ||
1433 | prepare_orders.assert_called_with(compute_value="average") | ||
1434 | |||
1435 | def test_print_balances(self): | ||
1436 | m = market.Market(self.ccxt) | ||
1437 | |||
1438 | with mock.patch.object(m.balances, "in_currency") as in_currency,\ | ||
1439 | mock.patch.object(m.report, "log_stage") as log_stage,\ | ||
1440 | mock.patch.object(m.balances, "fetch_balances") as fetch_balances,\ | ||
1441 | mock.patch.object(m.report, "print_log") as print_log: | ||
1442 | |||
1443 | in_currency.return_value = { | ||
1444 | "BTC": portfolio.Amount("BTC", "0.65"), | ||
1445 | "ETH": portfolio.Amount("BTC", "0.3"), | ||
1446 | } | ||
1447 | |||
1448 | m.print_balances() | ||
1449 | |||
1450 | log_stage.assert_called_once_with("print_balances") | ||
1451 | fetch_balances.assert_called_with() | ||
1452 | print_log.assert_has_calls([ | ||
1453 | mock.call("total:"), | ||
1454 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
1455 | ]) | ||
1456 | |||
1457 | @mock.patch("market.Processor.process") | ||
1458 | @mock.patch("market.ReportStore.log_error") | ||
1459 | @mock.patch("market.Market.store_report") | ||
1460 | def test_process(self, store_report, log_error, process): | ||
1461 | m = market.Market(self.ccxt) | ||
1462 | with self.subTest(before=False, after=False): | ||
1463 | m.process(None) | ||
1464 | |||
1465 | process.assert_not_called() | ||
1466 | store_report.assert_called_once() | ||
1467 | log_error.assert_not_called() | ||
1468 | |||
1469 | process.reset_mock() | ||
1470 | log_error.reset_mock() | ||
1471 | store_report.reset_mock() | ||
1472 | with self.subTest(before=True, after=False): | ||
1473 | m.process(None, before=True) | ||
1474 | |||
1475 | process.assert_called_once_with("sell_all", steps="before") | ||
1476 | store_report.assert_called_once() | ||
1477 | log_error.assert_not_called() | ||
1478 | |||
1479 | process.reset_mock() | ||
1480 | log_error.reset_mock() | ||
1481 | store_report.reset_mock() | ||
1482 | with self.subTest(before=False, after=True): | ||
1483 | m.process(None, after=True) | ||
1484 | |||
1485 | process.assert_called_once_with("sell_all", steps="after") | ||
1486 | store_report.assert_called_once() | ||
1487 | log_error.assert_not_called() | ||
1488 | |||
1489 | process.reset_mock() | ||
1490 | log_error.reset_mock() | ||
1491 | store_report.reset_mock() | ||
1492 | with self.subTest(before=True, after=True): | ||
1493 | m.process(None, before=True, after=True) | ||
1494 | |||
1495 | process.assert_has_calls([ | ||
1496 | mock.call("sell_all", steps="before"), | ||
1497 | mock.call("sell_all", steps="after"), | ||
1498 | ]) | ||
1499 | store_report.assert_called_once() | ||
1500 | log_error.assert_not_called() | ||
1501 | |||
1502 | process.reset_mock() | ||
1503 | log_error.reset_mock() | ||
1504 | store_report.reset_mock() | ||
1505 | with self.subTest(action="print_balances"),\ | ||
1506 | mock.patch.object(m, "print_balances") as print_balances: | ||
1507 | m.process(["print_balances"]) | ||
1508 | |||
1509 | process.assert_not_called() | ||
1510 | log_error.assert_not_called() | ||
1511 | store_report.assert_called_once() | ||
1512 | print_balances.assert_called_once_with() | ||
1513 | |||
1514 | log_error.reset_mock() | ||
1515 | store_report.reset_mock() | ||
1516 | with self.subTest(action="print_orders"),\ | ||
1517 | mock.patch.object(m, "print_orders") as print_orders,\ | ||
1518 | mock.patch.object(m, "print_balances") as print_balances: | ||
1519 | m.process(["print_orders", "print_balances"]) | ||
1520 | |||
1521 | process.assert_not_called() | ||
1522 | log_error.assert_not_called() | ||
1523 | store_report.assert_called_once() | ||
1524 | print_orders.assert_called_once_with() | ||
1525 | print_balances.assert_called_once_with() | ||
1526 | |||
1527 | log_error.reset_mock() | ||
1528 | store_report.reset_mock() | ||
1529 | with self.subTest(action="unknown"): | ||
1530 | m.process(["unknown"]) | ||
1531 | log_error.assert_called_once_with("market_process", message="Unknown action unknown") | ||
1532 | store_report.assert_called_once() | ||
1533 | |||
1534 | log_error.reset_mock() | ||
1535 | store_report.reset_mock() | ||
1536 | with self.subTest(unhandled_exception=True): | ||
1537 | process.side_effect = Exception("bouh") | ||
1538 | |||
1539 | m.process(None, before=True) | ||
1540 | log_error.assert_called_with("market_process", exception=mock.ANY) | ||
1541 | store_report.assert_called_once() | ||
1542 | |||
911 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 1543 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
912 | class TradeStoreTest(WebMockTestCase): | 1544 | class TradeStoreTest(WebMockTestCase): |
913 | def test_compute_trades(self): | 1545 | def test_compute_trades(self): |
@@ -1226,7 +1858,7 @@ class BalanceStoreTest(WebMockTestCase): | |||
1226 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies())) | 1858 | self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies())) |
1227 | self.m.report.log_balances.assert_called_with(tag="foo") | 1859 | self.m.report.log_balances.assert_called_with(tag="foo") |
1228 | 1860 | ||
1229 | @mock.patch.object(portfolio.Portfolio, "repartition") | 1861 | @mock.patch.object(market.Portfolio, "repartition") |
1230 | def test_dispatch_assets(self, repartition): | 1862 | def test_dispatch_assets(self, repartition): |
1231 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance | 1863 | self.m.ccxt.fetch_all_balances.return_value = self.fetch_balance |
1232 | 1864 | ||
@@ -1243,7 +1875,7 @@ class BalanceStoreTest(WebMockTestCase): | |||
1243 | repartition.return_value = repartition_hash | 1875 | repartition.return_value = repartition_hash |
1244 | 1876 | ||
1245 | amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1")) | 1877 | amounts = balance_store.dispatch_assets(portfolio.Amount("BTC", "11.1")) |
1246 | repartition.assert_called_with(self.m, liquidity="medium") | 1878 | repartition.assert_called_with(liquidity="medium") |
1247 | self.assertIn("XEM", balance_store.currencies()) | 1879 | self.assertIn("XEM", balance_store.currencies()) |
1248 | self.assertEqual(D("2.6"), amounts["BTC"].value) | 1880 | self.assertEqual(D("2.6"), amounts["BTC"].value) |
1249 | self.assertEqual(D("7.5"), amounts["XEM"].value) | 1881 | self.assertEqual(D("7.5"), amounts["XEM"].value) |
@@ -2334,6 +2966,19 @@ class ReportStoreTest(WebMockTestCase): | |||
2334 | report_store.set_verbose(False) | 2966 | report_store.set_verbose(False) |
2335 | self.assertFalse(report_store.verbose_print) | 2967 | self.assertFalse(report_store.verbose_print) |
2336 | 2968 | ||
2969 | def test_merge(self): | ||
2970 | report_store1 = market.ReportStore(self.m, verbose_print=False) | ||
2971 | report_store2 = market.ReportStore(None, verbose_print=False) | ||
2972 | |||
2973 | report_store2.log_stage("1") | ||
2974 | report_store1.log_stage("2") | ||
2975 | report_store2.log_stage("3") | ||
2976 | |||
2977 | report_store1.merge(report_store2) | ||
2978 | |||
2979 | self.assertEqual(3, len(report_store1.logs)) | ||
2980 | self.assertEqual(["1", "2", "3"], list(map(lambda x: x["stage"], report_store1.logs))) | ||
2981 | |||
2337 | def test_print_log(self): | 2982 | def test_print_log(self): |
2338 | report_store = market.ReportStore(self.m) | 2983 | report_store = market.ReportStore(self.m) |
2339 | with self.subTest(verbose=True),\ | 2984 | with self.subTest(verbose=True),\ |
@@ -2752,7 +3397,7 @@ class ReportStoreTest(WebMockTestCase): | |||
2752 | }) | 3397 | }) |
2753 | 3398 | ||
2754 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 3399 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
2755 | class HelperTest(WebMockTestCase): | 3400 | class MainTest(WebMockTestCase): |
2756 | def test_make_order(self): | 3401 | def test_make_order(self): |
2757 | self.m.get_ticker.return_value = { | 3402 | self.m.get_ticker.return_value = { |
2758 | "inverted": False, | 3403 | "inverted": False, |
@@ -2762,7 +3407,7 @@ class HelperTest(WebMockTestCase): | |||
2762 | } | 3407 | } |
2763 | 3408 | ||
2764 | with self.subTest(description="nominal case"): | 3409 | with self.subTest(description="nominal case"): |
2765 | helper.make_order(self.m, 10, "ETH") | 3410 | main.make_order(self.m, 10, "ETH") |
2766 | 3411 | ||
2767 | self.m.report.log_stage.assert_has_calls([ | 3412 | self.m.report.log_stage.assert_has_calls([ |
2768 | mock.call("make_order_begin"), | 3413 | mock.call("make_order_begin"), |
@@ -2787,7 +3432,7 @@ class HelperTest(WebMockTestCase): | |||
2787 | 3432 | ||
2788 | self.m.reset_mock() | 3433 | self.m.reset_mock() |
2789 | with self.subTest(compute_value="default"): | 3434 | with self.subTest(compute_value="default"): |
2790 | helper.make_order(self.m, 10, "ETH", action="dispose", | 3435 | main.make_order(self.m, 10, "ETH", action="dispose", |
2791 | compute_value="ask") | 3436 | compute_value="ask") |
2792 | 3437 | ||
2793 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3438 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
@@ -2796,7 +3441,7 @@ class HelperTest(WebMockTestCase): | |||
2796 | 3441 | ||
2797 | self.m.reset_mock() | 3442 | self.m.reset_mock() |
2798 | with self.subTest(follow=False): | 3443 | with self.subTest(follow=False): |
2799 | result = helper.make_order(self.m, 10, "ETH", follow=False) | 3444 | result = main.make_order(self.m, 10, "ETH", follow=False) |
2800 | 3445 | ||
2801 | self.m.report.log_stage.assert_has_calls([ | 3446 | self.m.report.log_stage.assert_has_calls([ |
2802 | mock.call("make_order_begin"), | 3447 | mock.call("make_order_begin"), |
@@ -2816,7 +3461,7 @@ class HelperTest(WebMockTestCase): | |||
2816 | 3461 | ||
2817 | self.m.reset_mock() | 3462 | self.m.reset_mock() |
2818 | with self.subTest(base_currency="USDT"): | 3463 | with self.subTest(base_currency="USDT"): |
2819 | helper.make_order(self.m, 1, "BTC", base_currency="USDT") | 3464 | main.make_order(self.m, 1, "BTC", base_currency="USDT") |
2820 | 3465 | ||
2821 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3466 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2822 | self.assertEqual("BTC", trade.currency) | 3467 | self.assertEqual("BTC", trade.currency) |
@@ -2824,14 +3469,14 @@ class HelperTest(WebMockTestCase): | |||
2824 | 3469 | ||
2825 | self.m.reset_mock() | 3470 | self.m.reset_mock() |
2826 | with self.subTest(close_if_possible=True): | 3471 | with self.subTest(close_if_possible=True): |
2827 | helper.make_order(self.m, 10, "ETH", close_if_possible=True) | 3472 | main.make_order(self.m, 10, "ETH", close_if_possible=True) |
2828 | 3473 | ||
2829 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3474 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2830 | self.assertEqual(True, trade.orders[0].close_if_possible) | 3475 | self.assertEqual(True, trade.orders[0].close_if_possible) |
2831 | 3476 | ||
2832 | self.m.reset_mock() | 3477 | self.m.reset_mock() |
2833 | with self.subTest(action="dispose"): | 3478 | with self.subTest(action="dispose"): |
2834 | helper.make_order(self.m, 10, "ETH", action="dispose") | 3479 | main.make_order(self.m, 10, "ETH", action="dispose") |
2835 | 3480 | ||
2836 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3481 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2837 | self.assertEqual(0, trade.value_to) | 3482 | self.assertEqual(0, trade.value_to) |
@@ -2841,19 +3486,19 @@ class HelperTest(WebMockTestCase): | |||
2841 | 3486 | ||
2842 | self.m.reset_mock() | 3487 | self.m.reset_mock() |
2843 | with self.subTest(compute_value="default"): | 3488 | with self.subTest(compute_value="default"): |
2844 | helper.make_order(self.m, 10, "ETH", action="dispose", | 3489 | main.make_order(self.m, 10, "ETH", action="dispose", |
2845 | compute_value="bid") | 3490 | compute_value="bid") |
2846 | 3491 | ||
2847 | trade = self.m.trades.all.append.mock_calls[0][1][0] | 3492 | trade = self.m.trades.all.append.mock_calls[0][1][0] |
2848 | self.assertEqual(D("0.9"), trade.value_from.value) | 3493 | self.assertEqual(D("0.9"), trade.value_from.value) |
2849 | 3494 | ||
2850 | def test_user_market(self): | 3495 | def test_get_user_market(self): |
2851 | with mock.patch("helper.main_fetch_markets") as main_fetch_markets,\ | 3496 | with mock.patch("main.fetch_markets") as main_fetch_markets,\ |
2852 | mock.patch("helper.main_parse_config") as main_parse_config: | 3497 | mock.patch("main.parse_config") as main_parse_config: |
2853 | with self.subTest(debug=False): | 3498 | with self.subTest(debug=False): |
2854 | main_parse_config.return_value = ["pg_config", "report_path"] | 3499 | main_parse_config.return_value = ["pg_config", "report_path"] |
2855 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 3500 | main_fetch_markets.return_value = [({"key": "market_config"},)] |
2856 | m = helper.get_user_market("config_path.ini", 1) | 3501 | m = main.get_user_market("config_path.ini", 1) |
2857 | 3502 | ||
2858 | self.assertIsInstance(m, market.Market) | 3503 | self.assertIsInstance(m, market.Market) |
2859 | self.assertFalse(m.debug) | 3504 | self.assertFalse(m.debug) |
@@ -2861,141 +3506,100 @@ class HelperTest(WebMockTestCase): | |||
2861 | with self.subTest(debug=True): | 3506 | with self.subTest(debug=True): |
2862 | main_parse_config.return_value = ["pg_config", "report_path"] | 3507 | main_parse_config.return_value = ["pg_config", "report_path"] |
2863 | main_fetch_markets.return_value = [({"key": "market_config"},)] | 3508 | main_fetch_markets.return_value = [({"key": "market_config"},)] |
2864 | m = helper.get_user_market("config_path.ini", 1, debug=True) | 3509 | m = main.get_user_market("config_path.ini", 1, debug=True) |
2865 | 3510 | ||
2866 | self.assertIsInstance(m, market.Market) | 3511 | self.assertIsInstance(m, market.Market) |
2867 | self.assertTrue(m.debug) | 3512 | self.assertTrue(m.debug) |
2868 | 3513 | ||
2869 | def test_main_store_report(self): | 3514 | def test_process(self): |
2870 | file_open = mock.mock_open() | 3515 | with mock.patch("market.Market") as market_mock,\ |
2871 | with self.subTest(file=None), mock.patch("__main__.open", file_open): | ||
2872 | helper.main_store_report(None, 1, self.m) | ||
2873 | file_open.assert_not_called() | ||
2874 | |||
2875 | file_open = mock.mock_open() | ||
2876 | with self.subTest(file="present"), mock.patch("helper.open", file_open),\ | ||
2877 | mock.patch.object(helper, "datetime") as time_mock: | ||
2878 | time_mock.now.return_value = datetime.datetime(2018, 2, 25) | ||
2879 | self.m.report.to_json.return_value = "json_content" | ||
2880 | |||
2881 | helper.main_store_report("present", 1, self.m) | ||
2882 | |||
2883 | file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w") | ||
2884 | file_open().write.assert_called_once_with("json_content") | ||
2885 | self.m.report.to_json.assert_called_once_with() | ||
2886 | |||
2887 | with self.subTest(file="error"),\ | ||
2888 | mock.patch("helper.open") as file_open,\ | ||
2889 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3516 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
2890 | file_open.side_effect = FileNotFoundError | ||
2891 | 3517 | ||
2892 | helper.main_store_report("error", 1, self.m) | 3518 | args_mock = mock.Mock() |
2893 | 3519 | args_mock.action = "action" | |
2894 | self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") | 3520 | args_mock.config = "config" |
2895 | 3521 | args_mock.user = "user" | |
2896 | @mock.patch("helper.Processor.process") | 3522 | args_mock.debug = "debug" |
2897 | def test_main_process_market(self, process): | 3523 | args_mock.before = "before" |
2898 | with self.subTest(before=False, after=False): | 3524 | args_mock.after = "after" |
2899 | m = mock.Mock() | 3525 | self.assertEqual("", stdout_mock.getvalue()) |
2900 | helper.main_process_market(m, None) | ||
2901 | |||
2902 | process.assert_not_called() | ||
2903 | |||
2904 | process.reset_mock() | ||
2905 | with self.subTest(before=True, after=False): | ||
2906 | helper.main_process_market(m, None, before=True) | ||
2907 | |||
2908 | process.assert_called_once_with("sell_all", steps="before") | ||
2909 | |||
2910 | process.reset_mock() | ||
2911 | with self.subTest(before=False, after=True): | ||
2912 | helper.main_process_market(m, None, after=True) | ||
2913 | |||
2914 | process.assert_called_once_with("sell_all", steps="after") | ||
2915 | 3526 | ||
2916 | process.reset_mock() | 3527 | main.process("config", 1, "report_path", args_mock) |
2917 | with self.subTest(before=True, after=True): | ||
2918 | helper.main_process_market(m, None, before=True, after=True) | ||
2919 | 3528 | ||
2920 | process.assert_has_calls([ | 3529 | market_mock.from_config.assert_has_calls([ |
2921 | mock.call("sell_all", steps="before"), | 3530 | mock.call("config", debug="debug", user_id=1, report_path="report_path"), |
2922 | mock.call("sell_all", steps="after"), | 3531 | mock.call().process("action", before="before", after="after"), |
2923 | ]) | 3532 | ]) |
2924 | 3533 | ||
2925 | process.reset_mock() | 3534 | with self.subTest(exception=True): |
2926 | with self.subTest(action="print_balances"),\ | 3535 | market_mock.from_config.side_effect = Exception("boo") |
2927 | mock.patch("helper.print_balances") as print_balances: | 3536 | main.process("config", 1, "report_path", args_mock) |
2928 | helper.main_process_market("user", ["print_balances"]) | 3537 | self.assertEqual("Exception: boo\n", stdout_mock.getvalue()) |
2929 | |||
2930 | process.assert_not_called() | ||
2931 | print_balances.assert_called_once_with("user") | ||
2932 | |||
2933 | with self.subTest(action="print_orders"),\ | ||
2934 | mock.patch("helper.print_orders") as print_orders,\ | ||
2935 | mock.patch("helper.print_balances") as print_balances: | ||
2936 | helper.main_process_market("user", ["print_orders", "print_balances"]) | ||
2937 | |||
2938 | process.assert_not_called() | ||
2939 | print_orders.assert_called_once_with("user") | ||
2940 | print_balances.assert_called_once_with("user") | ||
2941 | |||
2942 | with self.subTest(action="unknown"),\ | ||
2943 | self.assertRaises(NotImplementedError): | ||
2944 | helper.main_process_market("user", ["unknown"]) | ||
2945 | 3538 | ||
2946 | @mock.patch.object(helper, "psycopg2") | 3539 | def test_main(self): |
2947 | def test_fetch_markets(self, psycopg2): | 3540 | with self.subTest(parallel=False): |
2948 | connect_mock = mock.Mock() | 3541 | with mock.patch("main.parse_args") as parse_args,\ |
2949 | cursor_mock = mock.MagicMock() | 3542 | mock.patch("main.parse_config") as parse_config,\ |
2950 | cursor_mock.__iter__.return_value = ["row_1", "row_2"] | 3543 | mock.patch("main.fetch_markets") as fetch_markets,\ |
2951 | 3544 | mock.patch("main.process") as process: | |
2952 | connect_mock.cursor.return_value = cursor_mock | ||
2953 | psycopg2.connect.return_value = connect_mock | ||
2954 | |||
2955 | with self.subTest(user=None): | ||
2956 | rows = list(helper.main_fetch_markets({"foo": "bar"}, None)) | ||
2957 | |||
2958 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
2959 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs") | ||
2960 | |||
2961 | self.assertEqual(["row_1", "row_2"], rows) | ||
2962 | |||
2963 | psycopg2.connect.reset_mock() | ||
2964 | cursor_mock.execute.reset_mock() | ||
2965 | with self.subTest(user=1): | ||
2966 | rows = list(helper.main_fetch_markets({"foo": "bar"}, 1)) | ||
2967 | 3545 | ||
2968 | psycopg2.connect.assert_called_once_with(foo="bar") | 3546 | args_mock = mock.Mock() |
2969 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1) | 3547 | args_mock.parallel = False |
3548 | args_mock.config = "config" | ||
3549 | args_mock.user = "user" | ||
3550 | parse_args.return_value = args_mock | ||
2970 | 3551 | ||
2971 | self.assertEqual(["row_1", "row_2"], rows) | 3552 | parse_config.return_value = ["pg_config", "report_path"] |
2972 | 3553 | ||
2973 | @mock.patch.object(helper.sys, "exit") | 3554 | fetch_markets.return_value = [["config1", 1], ["config2", 2]] |
2974 | def test_main_parse_args(self, exit): | ||
2975 | with self.subTest(config="config.ini"): | ||
2976 | args = helper.main_parse_args([]) | ||
2977 | self.assertEqual("config.ini", args.config) | ||
2978 | self.assertFalse(args.before) | ||
2979 | self.assertFalse(args.after) | ||
2980 | self.assertFalse(args.debug) | ||
2981 | 3555 | ||
2982 | args = helper.main_parse_args(["--before", "--after", "--debug"]) | 3556 | main.main(["Foo", "Bar"]) |
2983 | self.assertTrue(args.before) | ||
2984 | self.assertTrue(args.after) | ||
2985 | self.assertTrue(args.debug) | ||
2986 | 3557 | ||
2987 | exit.assert_not_called() | 3558 | parse_args.assert_called_with(["Foo", "Bar"]) |
3559 | parse_config.assert_called_with("config") | ||
3560 | fetch_markets.assert_called_with("pg_config", "user") | ||
2988 | 3561 | ||
2989 | with self.subTest(config="inexistant"),\ | 3562 | self.assertEqual(2, process.call_count) |
2990 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3563 | process.assert_has_calls([ |
2991 | args = helper.main_parse_args(["--config", "foo.bar"]) | 3564 | mock.call("config1", 1, "report_path", args_mock), |
2992 | exit.assert_called_once_with(1) | 3565 | mock.call("config2", 2, "report_path", args_mock), |
2993 | self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) | 3566 | ]) |
3567 | with self.subTest(parallel=True): | ||
3568 | with mock.patch("main.parse_args") as parse_args,\ | ||
3569 | mock.patch("main.parse_config") as parse_config,\ | ||
3570 | mock.patch("main.fetch_markets") as fetch_markets,\ | ||
3571 | mock.patch("main.process") as process,\ | ||
3572 | mock.patch("store.Portfolio.start_worker") as start: | ||
3573 | |||
3574 | args_mock = mock.Mock() | ||
3575 | args_mock.parallel = True | ||
3576 | args_mock.config = "config" | ||
3577 | args_mock.user = "user" | ||
3578 | parse_args.return_value = args_mock | ||
3579 | |||
3580 | parse_config.return_value = ["pg_config", "report_path"] | ||
3581 | |||
3582 | fetch_markets.return_value = [["config1", 1], ["config2", 2]] | ||
3583 | |||
3584 | main.main(["Foo", "Bar"]) | ||
3585 | |||
3586 | parse_args.assert_called_with(["Foo", "Bar"]) | ||
3587 | parse_config.assert_called_with("config") | ||
3588 | fetch_markets.assert_called_with("pg_config", "user") | ||
3589 | |||
3590 | start.assert_called_once_with() | ||
3591 | self.assertEqual(2, process.call_count) | ||
3592 | process.assert_has_calls([ | ||
3593 | mock.call.__bool__(), | ||
3594 | mock.call("config1", 1, "report_path", args_mock), | ||
3595 | mock.call.__bool__(), | ||
3596 | mock.call("config2", 2, "report_path", args_mock), | ||
3597 | ]) | ||
2994 | 3598 | ||
2995 | @mock.patch.object(helper.sys, "exit") | 3599 | @mock.patch.object(main.sys, "exit") |
2996 | @mock.patch("helper.configparser") | 3600 | @mock.patch("main.configparser") |
2997 | @mock.patch("helper.os") | 3601 | @mock.patch("main.os") |
2998 | def test_main_parse_config(self, os, configparser, exit): | 3602 | def test_parse_config(self, os, configparser, exit): |
2999 | with self.subTest(pg_config=True, report_path=None): | 3603 | with self.subTest(pg_config=True, report_path=None): |
3000 | config_mock = mock.MagicMock() | 3604 | config_mock = mock.MagicMock() |
3001 | configparser.ConfigParser.return_value = config_mock | 3605 | configparser.ConfigParser.return_value = config_mock |
@@ -3005,7 +3609,7 @@ class HelperTest(WebMockTestCase): | |||
3005 | config_mock.__contains__.side_effect = config | 3609 | config_mock.__contains__.side_effect = config |
3006 | config_mock.__getitem__.return_value = "pg_config" | 3610 | config_mock.__getitem__.return_value = "pg_config" |
3007 | 3611 | ||
3008 | result = helper.main_parse_config("configfile") | 3612 | result = main.parse_config("configfile") |
3009 | 3613 | ||
3010 | config_mock.read.assert_called_with("configfile") | 3614 | config_mock.read.assert_called_with("configfile") |
3011 | 3615 | ||
@@ -3023,7 +3627,7 @@ class HelperTest(WebMockTestCase): | |||
3023 | ] | 3627 | ] |
3024 | 3628 | ||
3025 | os.path.exists.return_value = False | 3629 | os.path.exists.return_value = False |
3026 | result = helper.main_parse_config("configfile") | 3630 | result = main.parse_config("configfile") |
3027 | 3631 | ||
3028 | config_mock.read.assert_called_with("configfile") | 3632 | config_mock.read.assert_called_with("configfile") |
3029 | self.assertEqual(["pg_config", "report_path"], result) | 3633 | self.assertEqual(["pg_config", "report_path"], result) |
@@ -3034,46 +3638,71 @@ class HelperTest(WebMockTestCase): | |||
3034 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: | 3638 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
3035 | config_mock = mock.MagicMock() | 3639 | config_mock = mock.MagicMock() |
3036 | configparser.ConfigParser.return_value = config_mock | 3640 | configparser.ConfigParser.return_value = config_mock |
3037 | result = helper.main_parse_config("configfile") | 3641 | result = main.parse_config("configfile") |
3038 | 3642 | ||
3039 | config_mock.read.assert_called_with("configfile") | 3643 | config_mock.read.assert_called_with("configfile") |
3040 | exit.assert_called_once_with(1) | 3644 | exit.assert_called_once_with(1) |
3041 | self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) | 3645 | self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) |
3042 | 3646 | ||
3647 | @mock.patch.object(main.sys, "exit") | ||
3648 | def test_parse_args(self, exit): | ||
3649 | with self.subTest(config="config.ini"): | ||
3650 | args = main.parse_args([]) | ||
3651 | self.assertEqual("config.ini", args.config) | ||
3652 | self.assertFalse(args.before) | ||
3653 | self.assertFalse(args.after) | ||
3654 | self.assertFalse(args.debug) | ||
3043 | 3655 | ||
3044 | def test_print_orders(self): | 3656 | args = main.parse_args(["--before", "--after", "--debug"]) |
3045 | helper.print_orders(self.m) | 3657 | self.assertTrue(args.before) |
3658 | self.assertTrue(args.after) | ||
3659 | self.assertTrue(args.debug) | ||
3046 | 3660 | ||
3047 | self.m.report.log_stage.assert_called_with("print_orders") | 3661 | exit.assert_not_called() |
3048 | self.m.balances.fetch_balances.assert_called_with(tag="print_orders") | 3662 | |
3049 | self.m.prepare_trades.assert_called_with(base_currency="BTC", | 3663 | with self.subTest(config="inexistant"),\ |
3050 | compute_value="average") | 3664 | mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: |
3051 | self.m.trades.prepare_orders.assert_called_with(compute_value="average") | 3665 | args = main.parse_args(["--config", "foo.bar"]) |
3666 | exit.assert_called_once_with(1) | ||
3667 | self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) | ||
3668 | |||
3669 | @mock.patch.object(main, "psycopg2") | ||
3670 | def test_fetch_markets(self, psycopg2): | ||
3671 | connect_mock = mock.Mock() | ||
3672 | cursor_mock = mock.MagicMock() | ||
3673 | cursor_mock.__iter__.return_value = ["row_1", "row_2"] | ||
3674 | |||
3675 | connect_mock.cursor.return_value = cursor_mock | ||
3676 | psycopg2.connect.return_value = connect_mock | ||
3677 | |||
3678 | with self.subTest(user=None): | ||
3679 | rows = list(main.fetch_markets({"foo": "bar"}, None)) | ||
3680 | |||
3681 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
3682 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs") | ||
3683 | |||
3684 | self.assertEqual(["row_1", "row_2"], rows) | ||
3685 | |||
3686 | psycopg2.connect.reset_mock() | ||
3687 | cursor_mock.execute.reset_mock() | ||
3688 | with self.subTest(user=1): | ||
3689 | rows = list(main.fetch_markets({"foo": "bar"}, 1)) | ||
3690 | |||
3691 | psycopg2.connect.assert_called_once_with(foo="bar") | ||
3692 | cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1) | ||
3693 | |||
3694 | self.assertEqual(["row_1", "row_2"], rows) | ||
3052 | 3695 | ||
3053 | def test_print_balances(self): | ||
3054 | self.m.balances.in_currency.return_value = { | ||
3055 | "BTC": portfolio.Amount("BTC", "0.65"), | ||
3056 | "ETH": portfolio.Amount("BTC", "0.3"), | ||
3057 | } | ||
3058 | |||
3059 | helper.print_balances(self.m) | ||
3060 | |||
3061 | self.m.report.log_stage.assert_called_once_with("print_balances") | ||
3062 | self.m.balances.fetch_balances.assert_called_with() | ||
3063 | self.m.report.print_log.assert_has_calls([ | ||
3064 | mock.call("total:"), | ||
3065 | mock.call(portfolio.Amount("BTC", "0.95")), | ||
3066 | ]) | ||
3067 | 3696 | ||
3068 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 3697 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
3069 | class ProcessorTest(WebMockTestCase): | 3698 | class ProcessorTest(WebMockTestCase): |
3070 | def test_values(self): | 3699 | def test_values(self): |
3071 | processor = helper.Processor(self.m) | 3700 | processor = market.Processor(self.m) |
3072 | 3701 | ||
3073 | self.assertEqual(self.m, processor.market) | 3702 | self.assertEqual(self.m, processor.market) |
3074 | 3703 | ||
3075 | def test_run_action(self): | 3704 | def test_run_action(self): |
3076 | processor = helper.Processor(self.m) | 3705 | processor = market.Processor(self.m) |
3077 | 3706 | ||
3078 | with mock.patch.object(processor, "parse_args") as parse_args: | 3707 | with mock.patch.object(processor, "parse_args") as parse_args: |
3079 | method_mock = mock.Mock() | 3708 | method_mock = mock.Mock() |
@@ -3087,10 +3716,10 @@ class ProcessorTest(WebMockTestCase): | |||
3087 | 3716 | ||
3088 | processor.run_action("wait_for_recent", "bar", "baz") | 3717 | processor.run_action("wait_for_recent", "bar", "baz") |
3089 | 3718 | ||
3090 | method_mock.assert_called_with(self.m, foo="bar") | 3719 | method_mock.assert_called_with(foo="bar") |
3091 | 3720 | ||
3092 | def test_select_step(self): | 3721 | def test_select_step(self): |
3093 | processor = helper.Processor(self.m) | 3722 | processor = market.Processor(self.m) |
3094 | 3723 | ||
3095 | scenario = processor.scenarios["sell_all"] | 3724 | scenario = processor.scenarios["sell_all"] |
3096 | 3725 | ||
@@ -3103,9 +3732,9 @@ class ProcessorTest(WebMockTestCase): | |||
3103 | with self.assertRaises(TypeError): | 3732 | with self.assertRaises(TypeError): |
3104 | processor.select_steps(scenario, ["wait"]) | 3733 | processor.select_steps(scenario, ["wait"]) |
3105 | 3734 | ||
3106 | @mock.patch("helper.Processor.process_step") | 3735 | @mock.patch("market.Processor.process_step") |
3107 | def test_process(self, process_step): | 3736 | def test_process(self, process_step): |
3108 | processor = helper.Processor(self.m) | 3737 | processor = market.Processor(self.m) |
3109 | 3738 | ||
3110 | processor.process("sell_all", foo="bar") | 3739 | processor.process("sell_all", foo="bar") |
3111 | self.assertEqual(3, process_step.call_count) | 3740 | self.assertEqual(3, process_step.call_count) |
@@ -3126,11 +3755,11 @@ class ProcessorTest(WebMockTestCase): | |||
3126 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) | 3755 | ccxt = mock.Mock(spec=market.ccxt.poloniexE) |
3127 | m = market.Market(ccxt) | 3756 | m = market.Market(ccxt) |
3128 | 3757 | ||
3129 | processor = helper.Processor(m) | 3758 | processor = market.Processor(m) |
3130 | 3759 | ||
3131 | method, arguments = processor.method_arguments("wait_for_recent") | 3760 | method, arguments = processor.method_arguments("wait_for_recent") |
3132 | self.assertEqual(portfolio.Portfolio.wait_for_recent, method) | 3761 | self.assertEqual(market.Portfolio.wait_for_recent, method) |
3133 | self.assertEqual(["delta"], arguments) | 3762 | self.assertEqual(["delta", "poll"], arguments) |
3134 | 3763 | ||
3135 | method, arguments = processor.method_arguments("prepare_trades") | 3764 | method, arguments = processor.method_arguments("prepare_trades") |
3136 | self.assertEqual(m.prepare_trades, method) | 3765 | self.assertEqual(m.prepare_trades, method) |
@@ -3152,7 +3781,7 @@ class ProcessorTest(WebMockTestCase): | |||
3152 | self.assertEqual(m.trades.close_trades, method) | 3781 | self.assertEqual(m.trades.close_trades, method) |
3153 | 3782 | ||
3154 | def test_process_step(self): | 3783 | def test_process_step(self): |
3155 | processor = helper.Processor(self.m) | 3784 | processor = market.Processor(self.m) |
3156 | 3785 | ||
3157 | with mock.patch.object(processor, "run_action") as run_action: | 3786 | with mock.patch.object(processor, "run_action") as run_action: |
3158 | step = processor.scenarios["sell_needed"][1] | 3787 | step = processor.scenarios["sell_needed"][1] |
@@ -3186,7 +3815,7 @@ class ProcessorTest(WebMockTestCase): | |||
3186 | self.m.balances.fetch_balances.assert_not_called() | 3815 | self.m.balances.fetch_balances.assert_not_called() |
3187 | 3816 | ||
3188 | def test_parse_args(self): | 3817 | def test_parse_args(self): |
3189 | processor = helper.Processor(self.m) | 3818 | processor = market.Processor(self.m) |
3190 | 3819 | ||
3191 | with mock.patch.object(processor, "method_arguments") as method_arguments: | 3820 | with mock.patch.object(processor, "method_arguments") as method_arguments: |
3192 | method_mock = mock.Mock() | 3821 | method_mock = mock.Mock() |
@@ -3312,7 +3941,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3312 | market = mock.Mock() | 3941 | market = mock.Mock() |
3313 | market.fetch_all_balances.return_value = fetch_balance | 3942 | market.fetch_all_balances.return_value = fetch_balance |
3314 | market.fetch_ticker.side_effect = fetch_ticker | 3943 | market.fetch_ticker.side_effect = fetch_ticker |
3315 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): | 3944 | with mock.patch.object(market.Portfolio, "repartition", return_value=repartition): |
3316 | # Action 1 | 3945 | # Action 1 |
3317 | helper.prepare_trades(market) | 3946 | helper.prepare_trades(market) |
3318 | 3947 | ||
@@ -3391,7 +4020,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3391 | "amount": "10", "total": "1" | 4020 | "amount": "10", "total": "1" |
3392 | } | 4021 | } |
3393 | ] | 4022 | ] |
3394 | with mock.patch.object(portfolio.time, "sleep") as sleep: | 4023 | with mock.patch.object(market.time, "sleep") as sleep: |
3395 | # Action 4 | 4024 | # Action 4 |
3396 | helper.follow_orders(verbose=False) | 4025 | helper.follow_orders(verbose=False) |
3397 | 4026 | ||
@@ -3432,7 +4061,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3432 | } | 4061 | } |
3433 | market.fetch_all_balances.return_value = fetch_balance | 4062 | market.fetch_all_balances.return_value = fetch_balance |
3434 | 4063 | ||
3435 | with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): | 4064 | with mock.patch.object(market.Portfolio, "repartition", return_value=repartition): |
3436 | # Action 5 | 4065 | # Action 5 |
3437 | helper.prepare_trades(market, only="acquire", compute_value="average") | 4066 | helper.prepare_trades(market, only="acquire", compute_value="average") |
3438 | 4067 | ||
@@ -3504,7 +4133,7 @@ class AcceptanceTest(WebMockTestCase): | |||
3504 | # TODO | 4133 | # TODO |
3505 | # portfolio.TradeStore.run_orders() | 4134 | # portfolio.TradeStore.run_orders() |
3506 | 4135 | ||
3507 | with mock.patch.object(portfolio.time, "sleep") as sleep: | 4136 | with mock.patch.object(market.time, "sleep") as sleep: |
3508 | # Action 8 | 4137 | # Action 8 |
3509 | helper.follow_orders(verbose=False) | 4138 | helper.follow_orders(verbose=False) |
3510 | 4139 | ||
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 | |||