aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--helper.py320
-rw-r--r--main.py161
-rw-r--r--market.py239
-rw-r--r--portfolio.py79
-rw-r--r--store.py175
-rw-r--r--test.py1257
-rw-r--r--test_samples/poloniexETest.test_fetch_all_balances.1.json1459
-rw-r--r--test_samples/poloniexETest.test_fetch_all_balances.2.json101
-rw-r--r--test_samples/poloniexETest.test_fetch_all_balances.3.json11
-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 @@
1from datetime import datetime
2import argparse
3import configparser
4import psycopg2
5import os
6import sys
7
8import portfolio
9
10def 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
60def 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
66def 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
97def 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
115def 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
127def 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
140def 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
149def 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
155def 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
162class 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)
diff --git a/main.py b/main.py
index d4bab02..856d449 100644
--- a/main.py
+++ b/main.py
@@ -1,18 +1,155 @@
1from datetime import datetime
2import argparse
3import configparser
4import psycopg2
5import os
1import sys 6import sys
2import helper, market
3 7
4args = helper.main_parse_args(sys.argv[1:]) 8import market
9import portfolio
5 10
6pg_config, report_path = helper.main_parse_config(args.config) 11__all__ = ["make_order", "get_user_market"]
7 12
8for market_config, user_id in helper.main_fetch_markets(pg_config, args.user): 13def 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
63def 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
68def 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
80def 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
98def 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
131def 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: 139def 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
154if __name__ == '__main__': # pragma: no cover
155 main(sys.argv[1:])
diff --git a/market.py b/market.py
index 3381d1e..8672c59 100644
--- a/market.py
+++ b/market.py
@@ -3,6 +3,8 @@ import ccxt_wrapper as ccxt
3import time 3import time
4from store import * 4from store import *
5from cachetools.func import ttl_cache 5from cachetools.func import ttl_cache
6from datetime import datetime
7import portfolio
6 8
7class Market: 9class 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
194class 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 @@
1import time 1from datetime import datetime
2from datetime import datetime, timedelta
3from decimal import Decimal as D, ROUND_DOWN 2from decimal import Decimal as D, ROUND_DOWN
4from json import JSONDecodeError
5from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError
6from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound 3from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound
7from retry import retry 4from retry import retry
8import requests
9 5
10# FIXME: correctly handle web call timeouts 6# FIXME: correctly handle web call timeouts
11 7
12class 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
85class Computation: 8class Computation:
86 computations = { 9 computations = {
87 "default": lambda x, y: x[y], 10 "default": lambda x, y: x[y],
diff --git a/store.py b/store.py
index d25dd35..f655be5 100644
--- a/store.py
+++ b/store.py
@@ -1,10 +1,14 @@
1import time
2import requests
1import portfolio 3import portfolio
2import simplejson as json 4import simplejson as json
3from decimal import Decimal as D, ROUND_DOWN 5from decimal import Decimal as D, ROUND_DOWN
4from datetime import date, datetime 6from datetime import date, datetime, timedelta
5import inspect 7import inspect
8from json import JSONDecodeError
9from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError
6 10
7__all__ = ["BalanceStore", "ReportStore", "TradeStore"] 11__all__ = ["Portfolio", "BalanceStore", "ReportStore", "TradeStore"]
8 12
9class ReportStore: 13class 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
312class NoopLock:
313 def __enter__(self, *args):
314 pass
315 def __exit__(self, *args):
316 pass
317
318class 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
342class 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
diff --git a/test.py b/test.py
index a45010b..f61e739 100644
--- a/test.py
+++ b/test.py
@@ -7,7 +7,8 @@ from unittest import mock
7import requests 7import requests
8import requests_mock 8import requests_mock
9from io import StringIO 9from io import StringIO
10import portfolio, helper, market 10import threading
11import portfolio, market, main, store
11 12
12limits = ["acceptance", "unit"] 13limits = ["acceptance", "unit"]
13for test_type in limits: 14for 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")
130class PortfolioTest(WebMockTestCase): 450class 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")
457class 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")
543class 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")
299class AmountTest(WebMockTestCase): 766class 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")
912class TradeStoreTest(WebMockTestCase): 1544class 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")
2755class HelperTest(WebMockTestCase): 3400class 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")
3069class ProcessorTest(WebMockTestCase): 3698class 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