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 ed50b57..69e3755 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 921af9f..ac9a6cd 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)
@@ -2372,6 +3004,19 @@ class ReportStoreTest(WebMockTestCase):
2372 report_store.set_verbose(False) 3004 report_store.set_verbose(False)
2373 self.assertFalse(report_store.verbose_print) 3005 self.assertFalse(report_store.verbose_print)
2374 3006
3007 def test_merge(self):
3008 report_store1 = market.ReportStore(self.m, verbose_print=False)
3009 report_store2 = market.ReportStore(None, verbose_print=False)
3010
3011 report_store2.log_stage("1")
3012 report_store1.log_stage("2")
3013 report_store2.log_stage("3")
3014
3015 report_store1.merge(report_store2)
3016
3017 self.assertEqual(3, len(report_store1.logs))
3018 self.assertEqual(["1", "2", "3"], list(map(lambda x: x["stage"], report_store1.logs)))
3019
2375 def test_print_log(self): 3020 def test_print_log(self):
2376 report_store = market.ReportStore(self.m) 3021 report_store = market.ReportStore(self.m)
2377 with self.subTest(verbose=True),\ 3022 with self.subTest(verbose=True),\
@@ -2790,7 +3435,7 @@ class ReportStoreTest(WebMockTestCase):
2790 }) 3435 })
2791 3436
2792@unittest.skipUnless("unit" in limits, "Unit skipped") 3437@unittest.skipUnless("unit" in limits, "Unit skipped")
2793class HelperTest(WebMockTestCase): 3438class MainTest(WebMockTestCase):
2794 def test_make_order(self): 3439 def test_make_order(self):
2795 self.m.get_ticker.return_value = { 3440 self.m.get_ticker.return_value = {
2796 "inverted": False, 3441 "inverted": False,
@@ -2800,7 +3445,7 @@ class HelperTest(WebMockTestCase):
2800 } 3445 }
2801 3446
2802 with self.subTest(description="nominal case"): 3447 with self.subTest(description="nominal case"):
2803 helper.make_order(self.m, 10, "ETH") 3448 main.make_order(self.m, 10, "ETH")
2804 3449
2805 self.m.report.log_stage.assert_has_calls([ 3450 self.m.report.log_stage.assert_has_calls([
2806 mock.call("make_order_begin"), 3451 mock.call("make_order_begin"),
@@ -2825,7 +3470,7 @@ class HelperTest(WebMockTestCase):
2825 3470
2826 self.m.reset_mock() 3471 self.m.reset_mock()
2827 with self.subTest(compute_value="default"): 3472 with self.subTest(compute_value="default"):
2828 helper.make_order(self.m, 10, "ETH", action="dispose", 3473 main.make_order(self.m, 10, "ETH", action="dispose",
2829 compute_value="ask") 3474 compute_value="ask")
2830 3475
2831 trade = self.m.trades.all.append.mock_calls[0][1][0] 3476 trade = self.m.trades.all.append.mock_calls[0][1][0]
@@ -2834,7 +3479,7 @@ class HelperTest(WebMockTestCase):
2834 3479
2835 self.m.reset_mock() 3480 self.m.reset_mock()
2836 with self.subTest(follow=False): 3481 with self.subTest(follow=False):
2837 result = helper.make_order(self.m, 10, "ETH", follow=False) 3482 result = main.make_order(self.m, 10, "ETH", follow=False)
2838 3483
2839 self.m.report.log_stage.assert_has_calls([ 3484 self.m.report.log_stage.assert_has_calls([
2840 mock.call("make_order_begin"), 3485 mock.call("make_order_begin"),
@@ -2854,7 +3499,7 @@ class HelperTest(WebMockTestCase):
2854 3499
2855 self.m.reset_mock() 3500 self.m.reset_mock()
2856 with self.subTest(base_currency="USDT"): 3501 with self.subTest(base_currency="USDT"):
2857 helper.make_order(self.m, 1, "BTC", base_currency="USDT") 3502 main.make_order(self.m, 1, "BTC", base_currency="USDT")
2858 3503
2859 trade = self.m.trades.all.append.mock_calls[0][1][0] 3504 trade = self.m.trades.all.append.mock_calls[0][1][0]
2860 self.assertEqual("BTC", trade.currency) 3505 self.assertEqual("BTC", trade.currency)
@@ -2862,14 +3507,14 @@ class HelperTest(WebMockTestCase):
2862 3507
2863 self.m.reset_mock() 3508 self.m.reset_mock()
2864 with self.subTest(close_if_possible=True): 3509 with self.subTest(close_if_possible=True):
2865 helper.make_order(self.m, 10, "ETH", close_if_possible=True) 3510 main.make_order(self.m, 10, "ETH", close_if_possible=True)
2866 3511
2867 trade = self.m.trades.all.append.mock_calls[0][1][0] 3512 trade = self.m.trades.all.append.mock_calls[0][1][0]
2868 self.assertEqual(True, trade.orders[0].close_if_possible) 3513 self.assertEqual(True, trade.orders[0].close_if_possible)
2869 3514
2870 self.m.reset_mock() 3515 self.m.reset_mock()
2871 with self.subTest(action="dispose"): 3516 with self.subTest(action="dispose"):
2872 helper.make_order(self.m, 10, "ETH", action="dispose") 3517 main.make_order(self.m, 10, "ETH", action="dispose")
2873 3518
2874 trade = self.m.trades.all.append.mock_calls[0][1][0] 3519 trade = self.m.trades.all.append.mock_calls[0][1][0]
2875 self.assertEqual(0, trade.value_to) 3520 self.assertEqual(0, trade.value_to)
@@ -2879,19 +3524,19 @@ class HelperTest(WebMockTestCase):
2879 3524
2880 self.m.reset_mock() 3525 self.m.reset_mock()
2881 with self.subTest(compute_value="default"): 3526 with self.subTest(compute_value="default"):
2882 helper.make_order(self.m, 10, "ETH", action="dispose", 3527 main.make_order(self.m, 10, "ETH", action="dispose",
2883 compute_value="bid") 3528 compute_value="bid")
2884 3529
2885 trade = self.m.trades.all.append.mock_calls[0][1][0] 3530 trade = self.m.trades.all.append.mock_calls[0][1][0]
2886 self.assertEqual(D("0.9"), trade.value_from.value) 3531 self.assertEqual(D("0.9"), trade.value_from.value)
2887 3532
2888 def test_user_market(self): 3533 def test_get_user_market(self):
2889 with mock.patch("helper.main_fetch_markets") as main_fetch_markets,\ 3534 with mock.patch("main.fetch_markets") as main_fetch_markets,\
2890 mock.patch("helper.main_parse_config") as main_parse_config: 3535 mock.patch("main.parse_config") as main_parse_config:
2891 with self.subTest(debug=False): 3536 with self.subTest(debug=False):
2892 main_parse_config.return_value = ["pg_config", "report_path"] 3537 main_parse_config.return_value = ["pg_config", "report_path"]
2893 main_fetch_markets.return_value = [({"key": "market_config"},)] 3538 main_fetch_markets.return_value = [({"key": "market_config"},)]
2894 m = helper.get_user_market("config_path.ini", 1) 3539 m = main.get_user_market("config_path.ini", 1)
2895 3540
2896 self.assertIsInstance(m, market.Market) 3541 self.assertIsInstance(m, market.Market)
2897 self.assertFalse(m.debug) 3542 self.assertFalse(m.debug)
@@ -2899,141 +3544,100 @@ class HelperTest(WebMockTestCase):
2899 with self.subTest(debug=True): 3544 with self.subTest(debug=True):
2900 main_parse_config.return_value = ["pg_config", "report_path"] 3545 main_parse_config.return_value = ["pg_config", "report_path"]
2901 main_fetch_markets.return_value = [({"key": "market_config"},)] 3546 main_fetch_markets.return_value = [({"key": "market_config"},)]
2902 m = helper.get_user_market("config_path.ini", 1, debug=True) 3547 m = main.get_user_market("config_path.ini", 1, debug=True)
2903 3548
2904 self.assertIsInstance(m, market.Market) 3549 self.assertIsInstance(m, market.Market)
2905 self.assertTrue(m.debug) 3550 self.assertTrue(m.debug)
2906 3551
2907 def test_main_store_report(self): 3552 def test_process(self):
2908 file_open = mock.mock_open() 3553 with mock.patch("market.Market") as market_mock,\
2909 with self.subTest(file=None), mock.patch("__main__.open", file_open):
2910 helper.main_store_report(None, 1, self.m)
2911 file_open.assert_not_called()
2912
2913 file_open = mock.mock_open()
2914 with self.subTest(file="present"), mock.patch("helper.open", file_open),\
2915 mock.patch.object(helper, "datetime") as time_mock:
2916 time_mock.now.return_value = datetime.datetime(2018, 2, 25)
2917 self.m.report.to_json.return_value = "json_content"
2918
2919 helper.main_store_report("present", 1, self.m)
2920
2921 file_open.assert_any_call("present/2018-02-25T00:00:00_1.json", "w")
2922 file_open().write.assert_called_once_with("json_content")
2923 self.m.report.to_json.assert_called_once_with()
2924
2925 with self.subTest(file="error"),\
2926 mock.patch("helper.open") as file_open,\
2927 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: 3554 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
2928 file_open.side_effect = FileNotFoundError
2929 3555
2930 helper.main_store_report("error", 1, self.m) 3556 args_mock = mock.Mock()
2931 3557 args_mock.action = "action"
2932 self.assertRegex(stdout_mock.getvalue(), "impossible to store report file: FileNotFoundError;") 3558 args_mock.config = "config"
2933 3559 args_mock.user = "user"
2934 @mock.patch("helper.Processor.process") 3560 args_mock.debug = "debug"
2935 def test_main_process_market(self, process): 3561 args_mock.before = "before"
2936 with self.subTest(before=False, after=False): 3562 args_mock.after = "after"
2937 m = mock.Mock() 3563 self.assertEqual("", stdout_mock.getvalue())
2938 helper.main_process_market(m, None)
2939
2940 process.assert_not_called()
2941
2942 process.reset_mock()
2943 with self.subTest(before=True, after=False):
2944 helper.main_process_market(m, None, before=True)
2945
2946 process.assert_called_once_with("sell_all", steps="before")
2947
2948 process.reset_mock()
2949 with self.subTest(before=False, after=True):
2950 helper.main_process_market(m, None, after=True)
2951
2952 process.assert_called_once_with("sell_all", steps="after")
2953 3564
2954 process.reset_mock() 3565 main.process("config", 1, "report_path", args_mock)
2955 with self.subTest(before=True, after=True):
2956 helper.main_process_market(m, None, before=True, after=True)
2957 3566
2958 process.assert_has_calls([ 3567 market_mock.from_config.assert_has_calls([
2959 mock.call("sell_all", steps="before"), 3568 mock.call("config", debug="debug", user_id=1, report_path="report_path"),
2960 mock.call("sell_all", steps="after"), 3569 mock.call().process("action", before="before", after="after"),
2961 ]) 3570 ])
2962 3571
2963 process.reset_mock() 3572 with self.subTest(exception=True):
2964 with self.subTest(action="print_balances"),\ 3573 market_mock.from_config.side_effect = Exception("boo")
2965 mock.patch("helper.print_balances") as print_balances: 3574 main.process("config", 1, "report_path", args_mock)
2966 helper.main_process_market("user", ["print_balances"]) 3575 self.assertEqual("Exception: boo\n", stdout_mock.getvalue())
2967
2968 process.assert_not_called()
2969 print_balances.assert_called_once_with("user")
2970
2971 with self.subTest(action="print_orders"),\
2972 mock.patch("helper.print_orders") as print_orders,\
2973 mock.patch("helper.print_balances") as print_balances:
2974 helper.main_process_market("user", ["print_orders", "print_balances"])
2975
2976 process.assert_not_called()
2977 print_orders.assert_called_once_with("user")
2978 print_balances.assert_called_once_with("user")
2979
2980 with self.subTest(action="unknown"),\
2981 self.assertRaises(NotImplementedError):
2982 helper.main_process_market("user", ["unknown"])
2983 3576
2984 @mock.patch.object(helper, "psycopg2") 3577 def test_main(self):
2985 def test_fetch_markets(self, psycopg2): 3578 with self.subTest(parallel=False):
2986 connect_mock = mock.Mock() 3579 with mock.patch("main.parse_args") as parse_args,\
2987 cursor_mock = mock.MagicMock() 3580 mock.patch("main.parse_config") as parse_config,\
2988 cursor_mock.__iter__.return_value = ["row_1", "row_2"] 3581 mock.patch("main.fetch_markets") as fetch_markets,\
2989 3582 mock.patch("main.process") as process:
2990 connect_mock.cursor.return_value = cursor_mock
2991 psycopg2.connect.return_value = connect_mock
2992
2993 with self.subTest(user=None):
2994 rows = list(helper.main_fetch_markets({"foo": "bar"}, None))
2995
2996 psycopg2.connect.assert_called_once_with(foo="bar")
2997 cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs")
2998
2999 self.assertEqual(["row_1", "row_2"], rows)
3000
3001 psycopg2.connect.reset_mock()
3002 cursor_mock.execute.reset_mock()
3003 with self.subTest(user=1):
3004 rows = list(helper.main_fetch_markets({"foo": "bar"}, 1))
3005 3583
3006 psycopg2.connect.assert_called_once_with(foo="bar") 3584 args_mock = mock.Mock()
3007 cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1) 3585 args_mock.parallel = False
3586 args_mock.config = "config"
3587 args_mock.user = "user"
3588 parse_args.return_value = args_mock
3008 3589
3009 self.assertEqual(["row_1", "row_2"], rows) 3590 parse_config.return_value = ["pg_config", "report_path"]
3010 3591
3011 @mock.patch.object(helper.sys, "exit") 3592 fetch_markets.return_value = [["config1", 1], ["config2", 2]]
3012 def test_main_parse_args(self, exit):
3013 with self.subTest(config="config.ini"):
3014 args = helper.main_parse_args([])
3015 self.assertEqual("config.ini", args.config)
3016 self.assertFalse(args.before)
3017 self.assertFalse(args.after)
3018 self.assertFalse(args.debug)
3019 3593
3020 args = helper.main_parse_args(["--before", "--after", "--debug"]) 3594 main.main(["Foo", "Bar"])
3021 self.assertTrue(args.before)
3022 self.assertTrue(args.after)
3023 self.assertTrue(args.debug)
3024 3595
3025 exit.assert_not_called() 3596 parse_args.assert_called_with(["Foo", "Bar"])
3597 parse_config.assert_called_with("config")
3598 fetch_markets.assert_called_with("pg_config", "user")
3026 3599
3027 with self.subTest(config="inexistant"),\ 3600 self.assertEqual(2, process.call_count)
3028 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: 3601 process.assert_has_calls([
3029 args = helper.main_parse_args(["--config", "foo.bar"]) 3602 mock.call("config1", 1, "report_path", args_mock),
3030 exit.assert_called_once_with(1) 3603 mock.call("config2", 2, "report_path", args_mock),
3031 self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue()) 3604 ])
3605 with self.subTest(parallel=True):
3606 with mock.patch("main.parse_args") as parse_args,\
3607 mock.patch("main.parse_config") as parse_config,\
3608 mock.patch("main.fetch_markets") as fetch_markets,\
3609 mock.patch("main.process") as process,\
3610 mock.patch("store.Portfolio.start_worker") as start:
3611
3612 args_mock = mock.Mock()
3613 args_mock.parallel = True
3614 args_mock.config = "config"
3615 args_mock.user = "user"
3616 parse_args.return_value = args_mock
3617
3618 parse_config.return_value = ["pg_config", "report_path"]
3619
3620 fetch_markets.return_value = [["config1", 1], ["config2", 2]]
3621
3622 main.main(["Foo", "Bar"])
3623
3624 parse_args.assert_called_with(["Foo", "Bar"])
3625 parse_config.assert_called_with("config")
3626 fetch_markets.assert_called_with("pg_config", "user")
3627
3628 start.assert_called_once_with()
3629 self.assertEqual(2, process.call_count)
3630 process.assert_has_calls([
3631 mock.call.__bool__(),
3632 mock.call("config1", 1, "report_path", args_mock),
3633 mock.call.__bool__(),
3634 mock.call("config2", 2, "report_path", args_mock),
3635 ])
3032 3636
3033 @mock.patch.object(helper.sys, "exit") 3637 @mock.patch.object(main.sys, "exit")
3034 @mock.patch("helper.configparser") 3638 @mock.patch("main.configparser")
3035 @mock.patch("helper.os") 3639 @mock.patch("main.os")
3036 def test_main_parse_config(self, os, configparser, exit): 3640 def test_parse_config(self, os, configparser, exit):
3037 with self.subTest(pg_config=True, report_path=None): 3641 with self.subTest(pg_config=True, report_path=None):
3038 config_mock = mock.MagicMock() 3642 config_mock = mock.MagicMock()
3039 configparser.ConfigParser.return_value = config_mock 3643 configparser.ConfigParser.return_value = config_mock
@@ -3043,7 +3647,7 @@ class HelperTest(WebMockTestCase):
3043 config_mock.__contains__.side_effect = config 3647 config_mock.__contains__.side_effect = config
3044 config_mock.__getitem__.return_value = "pg_config" 3648 config_mock.__getitem__.return_value = "pg_config"
3045 3649
3046 result = helper.main_parse_config("configfile") 3650 result = main.parse_config("configfile")
3047 3651
3048 config_mock.read.assert_called_with("configfile") 3652 config_mock.read.assert_called_with("configfile")
3049 3653
@@ -3061,7 +3665,7 @@ class HelperTest(WebMockTestCase):
3061 ] 3665 ]
3062 3666
3063 os.path.exists.return_value = False 3667 os.path.exists.return_value = False
3064 result = helper.main_parse_config("configfile") 3668 result = main.parse_config("configfile")
3065 3669
3066 config_mock.read.assert_called_with("configfile") 3670 config_mock.read.assert_called_with("configfile")
3067 self.assertEqual(["pg_config", "report_path"], result) 3671 self.assertEqual(["pg_config", "report_path"], result)
@@ -3072,46 +3676,71 @@ class HelperTest(WebMockTestCase):
3072 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock: 3676 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
3073 config_mock = mock.MagicMock() 3677 config_mock = mock.MagicMock()
3074 configparser.ConfigParser.return_value = config_mock 3678 configparser.ConfigParser.return_value = config_mock
3075 result = helper.main_parse_config("configfile") 3679 result = main.parse_config("configfile")
3076 3680
3077 config_mock.read.assert_called_with("configfile") 3681 config_mock.read.assert_called_with("configfile")
3078 exit.assert_called_once_with(1) 3682 exit.assert_called_once_with(1)
3079 self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue()) 3683 self.assertEqual("no configuration for postgresql in config file\n", stdout_mock.getvalue())
3080 3684
3685 @mock.patch.object(main.sys, "exit")
3686 def test_parse_args(self, exit):
3687 with self.subTest(config="config.ini"):
3688 args = main.parse_args([])
3689 self.assertEqual("config.ini", args.config)
3690 self.assertFalse(args.before)
3691 self.assertFalse(args.after)
3692 self.assertFalse(args.debug)
3081 3693
3082 def test_print_orders(self): 3694 args = main.parse_args(["--before", "--after", "--debug"])
3083 helper.print_orders(self.m) 3695 self.assertTrue(args.before)
3696 self.assertTrue(args.after)
3697 self.assertTrue(args.debug)
3084 3698
3085 self.m.report.log_stage.assert_called_with("print_orders") 3699 exit.assert_not_called()
3086 self.m.balances.fetch_balances.assert_called_with(tag="print_orders") 3700
3087 self.m.prepare_trades.assert_called_with(base_currency="BTC", 3701 with self.subTest(config="inexistant"),\
3088 compute_value="average") 3702 mock.patch('sys.stdout', new_callable=StringIO) as stdout_mock:
3089 self.m.trades.prepare_orders.assert_called_with(compute_value="average") 3703 args = main.parse_args(["--config", "foo.bar"])
3704 exit.assert_called_once_with(1)
3705 self.assertEqual("no config file found, exiting\n", stdout_mock.getvalue())
3706
3707 @mock.patch.object(main, "psycopg2")
3708 def test_fetch_markets(self, psycopg2):
3709 connect_mock = mock.Mock()
3710 cursor_mock = mock.MagicMock()
3711 cursor_mock.__iter__.return_value = ["row_1", "row_2"]
3712
3713 connect_mock.cursor.return_value = cursor_mock
3714 psycopg2.connect.return_value = connect_mock
3715
3716 with self.subTest(user=None):
3717 rows = list(main.fetch_markets({"foo": "bar"}, None))
3718
3719 psycopg2.connect.assert_called_once_with(foo="bar")
3720 cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs")
3721
3722 self.assertEqual(["row_1", "row_2"], rows)
3723
3724 psycopg2.connect.reset_mock()
3725 cursor_mock.execute.reset_mock()
3726 with self.subTest(user=1):
3727 rows = list(main.fetch_markets({"foo": "bar"}, 1))
3728
3729 psycopg2.connect.assert_called_once_with(foo="bar")
3730 cursor_mock.execute.assert_called_once_with("SELECT config,user_id FROM market_configs WHERE user_id = %s", 1)
3731
3732 self.assertEqual(["row_1", "row_2"], rows)
3090 3733
3091 def test_print_balances(self):
3092 self.m.balances.in_currency.return_value = {
3093 "BTC": portfolio.Amount("BTC", "0.65"),
3094 "ETH": portfolio.Amount("BTC", "0.3"),
3095 }
3096
3097 helper.print_balances(self.m)
3098
3099 self.m.report.log_stage.assert_called_once_with("print_balances")
3100 self.m.balances.fetch_balances.assert_called_with()
3101 self.m.report.print_log.assert_has_calls([
3102 mock.call("total:"),
3103 mock.call(portfolio.Amount("BTC", "0.95")),
3104 ])
3105 3734
3106@unittest.skipUnless("unit" in limits, "Unit skipped") 3735@unittest.skipUnless("unit" in limits, "Unit skipped")
3107class ProcessorTest(WebMockTestCase): 3736class ProcessorTest(WebMockTestCase):
3108 def test_values(self): 3737 def test_values(self):
3109 processor = helper.Processor(self.m) 3738 processor = market.Processor(self.m)
3110 3739
3111 self.assertEqual(self.m, processor.market) 3740 self.assertEqual(self.m, processor.market)
3112 3741
3113 def test_run_action(self): 3742 def test_run_action(self):
3114 processor = helper.Processor(self.m) 3743 processor = market.Processor(self.m)
3115 3744
3116 with mock.patch.object(processor, "parse_args") as parse_args: 3745 with mock.patch.object(processor, "parse_args") as parse_args:
3117 method_mock = mock.Mock() 3746 method_mock = mock.Mock()
@@ -3125,10 +3754,10 @@ class ProcessorTest(WebMockTestCase):
3125 3754
3126 processor.run_action("wait_for_recent", "bar", "baz") 3755 processor.run_action("wait_for_recent", "bar", "baz")
3127 3756
3128 method_mock.assert_called_with(self.m, foo="bar") 3757 method_mock.assert_called_with(foo="bar")
3129 3758
3130 def test_select_step(self): 3759 def test_select_step(self):
3131 processor = helper.Processor(self.m) 3760 processor = market.Processor(self.m)
3132 3761
3133 scenario = processor.scenarios["sell_all"] 3762 scenario = processor.scenarios["sell_all"]
3134 3763
@@ -3141,9 +3770,9 @@ class ProcessorTest(WebMockTestCase):
3141 with self.assertRaises(TypeError): 3770 with self.assertRaises(TypeError):
3142 processor.select_steps(scenario, ["wait"]) 3771 processor.select_steps(scenario, ["wait"])
3143 3772
3144 @mock.patch("helper.Processor.process_step") 3773 @mock.patch("market.Processor.process_step")
3145 def test_process(self, process_step): 3774 def test_process(self, process_step):
3146 processor = helper.Processor(self.m) 3775 processor = market.Processor(self.m)
3147 3776
3148 processor.process("sell_all", foo="bar") 3777 processor.process("sell_all", foo="bar")
3149 self.assertEqual(3, process_step.call_count) 3778 self.assertEqual(3, process_step.call_count)
@@ -3164,11 +3793,11 @@ class ProcessorTest(WebMockTestCase):
3164 ccxt = mock.Mock(spec=market.ccxt.poloniexE) 3793 ccxt = mock.Mock(spec=market.ccxt.poloniexE)
3165 m = market.Market(ccxt) 3794 m = market.Market(ccxt)
3166 3795
3167 processor = helper.Processor(m) 3796 processor = market.Processor(m)
3168 3797
3169 method, arguments = processor.method_arguments("wait_for_recent") 3798 method, arguments = processor.method_arguments("wait_for_recent")
3170 self.assertEqual(portfolio.Portfolio.wait_for_recent, method) 3799 self.assertEqual(market.Portfolio.wait_for_recent, method)
3171 self.assertEqual(["delta"], arguments) 3800 self.assertEqual(["delta", "poll"], arguments)
3172 3801
3173 method, arguments = processor.method_arguments("prepare_trades") 3802 method, arguments = processor.method_arguments("prepare_trades")
3174 self.assertEqual(m.prepare_trades, method) 3803 self.assertEqual(m.prepare_trades, method)
@@ -3190,7 +3819,7 @@ class ProcessorTest(WebMockTestCase):
3190 self.assertEqual(m.trades.close_trades, method) 3819 self.assertEqual(m.trades.close_trades, method)
3191 3820
3192 def test_process_step(self): 3821 def test_process_step(self):
3193 processor = helper.Processor(self.m) 3822 processor = market.Processor(self.m)
3194 3823
3195 with mock.patch.object(processor, "run_action") as run_action: 3824 with mock.patch.object(processor, "run_action") as run_action:
3196 step = processor.scenarios["sell_needed"][1] 3825 step = processor.scenarios["sell_needed"][1]
@@ -3224,7 +3853,7 @@ class ProcessorTest(WebMockTestCase):
3224 self.m.balances.fetch_balances.assert_not_called() 3853 self.m.balances.fetch_balances.assert_not_called()
3225 3854
3226 def test_parse_args(self): 3855 def test_parse_args(self):
3227 processor = helper.Processor(self.m) 3856 processor = market.Processor(self.m)
3228 3857
3229 with mock.patch.object(processor, "method_arguments") as method_arguments: 3858 with mock.patch.object(processor, "method_arguments") as method_arguments:
3230 method_mock = mock.Mock() 3859 method_mock = mock.Mock()
@@ -3350,7 +3979,7 @@ class AcceptanceTest(WebMockTestCase):
3350 market = mock.Mock() 3979 market = mock.Mock()
3351 market.fetch_all_balances.return_value = fetch_balance 3980 market.fetch_all_balances.return_value = fetch_balance
3352 market.fetch_ticker.side_effect = fetch_ticker 3981 market.fetch_ticker.side_effect = fetch_ticker
3353 with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): 3982 with mock.patch.object(market.Portfolio, "repartition", return_value=repartition):
3354 # Action 1 3983 # Action 1
3355 helper.prepare_trades(market) 3984 helper.prepare_trades(market)
3356 3985
@@ -3429,7 +4058,7 @@ class AcceptanceTest(WebMockTestCase):
3429 "amount": "10", "total": "1" 4058 "amount": "10", "total": "1"
3430 } 4059 }
3431 ] 4060 ]
3432 with mock.patch.object(portfolio.time, "sleep") as sleep: 4061 with mock.patch.object(market.time, "sleep") as sleep:
3433 # Action 4 4062 # Action 4
3434 helper.follow_orders(verbose=False) 4063 helper.follow_orders(verbose=False)
3435 4064
@@ -3470,7 +4099,7 @@ class AcceptanceTest(WebMockTestCase):
3470 } 4099 }
3471 market.fetch_all_balances.return_value = fetch_balance 4100 market.fetch_all_balances.return_value = fetch_balance
3472 4101
3473 with mock.patch.object(portfolio.Portfolio, "repartition", return_value=repartition): 4102 with mock.patch.object(market.Portfolio, "repartition", return_value=repartition):
3474 # Action 5 4103 # Action 5
3475 helper.prepare_trades(market, only="acquire", compute_value="average") 4104 helper.prepare_trades(market, only="acquire", compute_value="average")
3476 4105
@@ -3542,7 +4171,7 @@ class AcceptanceTest(WebMockTestCase):
3542 # TODO 4171 # TODO
3543 # portfolio.TradeStore.run_orders() 4172 # portfolio.TradeStore.run_orders()
3544 4173
3545 with mock.patch.object(portfolio.time, "sleep") as sleep: 4174 with mock.patch.object(market.time, "sleep") as sleep:
3546 # Action 8 4175 # Action 8
3547 helper.follow_orders(verbose=False) 4176 helper.follow_orders(verbose=False)
3548 4177
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