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