aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-05-01 17:35:55 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-05-01 17:35:55 +0200
commit9eb0de20f243bb78de0bf9118289f01f1ea1f77c (patch)
tree762d3a59d666bf6e1d4d1ea5901c31ab06f7e88e
parent6cffa4af8b5a04b17ffd95738b8e843c4605d4e7 (diff)
parent2b1ee8f4d54fa1672510141a71a5817120ac031c (diff)
downloadTrader-9eb0de20f243bb78de0bf9118289f01f1ea1f77c.tar.gz
Trader-9eb0de20f243bb78de0bf9118289f01f1ea1f77c.tar.zst
Trader-9eb0de20f243bb78de0bf9118289f01f1ea1f77c.zip
Merge branch 'dev'v1.5
-rw-r--r--ccxt_wrapper.py82
-rw-r--r--main.py2
-rw-r--r--market.py4
-rw-r--r--portfolio.py13
-rw-r--r--store.py31
-rw-r--r--tests/test_ccxt_wrapper.py142
-rw-r--r--tests/test_main.py4
-rw-r--r--tests/test_market.py4
-rw-r--r--tests/test_portfolio.py13
-rw-r--r--tests/test_store.py133
10 files changed, 283 insertions, 145 deletions
diff --git a/ccxt_wrapper.py b/ccxt_wrapper.py
index d2c9b4c..c4aa94d 100644
--- a/ccxt_wrapper.py
+++ b/ccxt_wrapper.py
@@ -66,6 +66,13 @@ class poloniexE(poloniex):
66 def nanoseconds(): 66 def nanoseconds():
67 return int(time.time() * 1000000000) 67 return int(time.time() * 1000000000)
68 68
69 def is_dust_trade(self, amount, rate):
70 if abs(amount) < decimal.Decimal("0.000001"):
71 return True
72 if abs(amount * rate) < decimal.Decimal("0.0001"):
73 return True
74 return False
75
69 def fetch_margin_balance(self): 76 def fetch_margin_balance(self):
70 """ 77 """
71 portfolio.market.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"}) 78 portfolio.market.privatePostGetMarginPosition({"currencyPair": "BTC_DASH"})
@@ -232,39 +239,9 @@ class poloniexE(poloniex):
232 239
233 return all_balances 240 return all_balances
234 241
235 def create_exchange_order(self, symbol, type, side, amount, price=None, params={}):
236 return super().create_order(symbol, type, side, amount, price=price, params=params)
237
238 def create_margin_order(self, symbol, type, side, amount, price=None, lending_rate=None, params={}):
239 if type == 'market':
240 raise ExchangeError(self.id + ' allows limit orders only')
241 self.load_markets()
242 method = 'privatePostMargin' + self.capitalize(side)
243 market = self.market(symbol)
244 price = float(price)
245 amount = float(amount)
246 if lending_rate is not None:
247 params = self.extend({"lendingRate": lending_rate}, params)
248 response = getattr(self, method)(self.extend({
249 'currencyPair': market['id'],
250 'rate': self.price_to_precision(symbol, price),
251 'amount': self.amount_to_precision(symbol, amount),
252 }, params))
253 timestamp = self.milliseconds()
254 order = self.parse_order(self.extend({
255 'timestamp': timestamp,
256 'status': 'open',
257 'type': type,
258 'side': side,
259 'price': price,
260 'amount': amount,
261 }, response), market)
262 id = order['id']
263 self.orders[id] = order
264 return self.extend({'info': response}, order)
265
266 def order_precision(self, symbol): 242 def order_precision(self, symbol):
267 return 8 243 self.load_markets()
244 return self.markets[symbol]['precision']['price']
268 245
269 def transfer_balance(self, currency, amount, from_account, to_account): 246 def transfer_balance(self, currency, amount, from_account, to_account):
270 result = self.privatePostTransferBalance({ 247 result = self.privatePostTransferBalance({
@@ -382,14 +359,49 @@ class poloniexE(poloniex):
382 359
383 def create_order(self, symbol, type, side, amount, price=None, account="exchange", lending_rate=None, params={}): 360 def create_order(self, symbol, type, side, amount, price=None, account="exchange", lending_rate=None, params={}):
384 """ 361 """
385 Wrapped to handle margin and exchange accounts 362 Wrapped to handle margin and exchange accounts, and get decimals
386 """ 363 """
364 if type == 'market':
365 raise ExchangeError(self.id + ' allows limit orders only')
366 self.load_markets()
387 if account == "exchange": 367 if account == "exchange":
388 return self.create_exchange_order(symbol, type, side, amount, price=price, params=params) 368 method = 'privatePost' + self.capitalize(side)
389 elif account == "margin": 369 elif account == "margin":
390 return self.create_margin_order(symbol, type, side, amount, price=price, lending_rate=lending_rate, params=params) 370 method = 'privatePostMargin' + self.capitalize(side)
371 if lending_rate is not None:
372 params = self.extend({"lendingRate": lending_rate}, params)
391 else: 373 else:
392 raise NotImplementedError 374 raise NotImplementedError
375 market = self.market(symbol)
376 response = getattr(self, method)(self.extend({
377 'currencyPair': market['id'],
378 'rate': self.price_to_precision(symbol, price),
379 'amount': self.amount_to_precision(symbol, amount),
380 }, params))
381 timestamp = self.milliseconds()
382 order = self.parse_order(self.extend({
383 'timestamp': timestamp,
384 'status': 'open',
385 'type': type,
386 'side': side,
387 'price': price,
388 'amount': amount,
389 }, response), market)
390 id = order['id']
391 self.orders[id] = order
392 return self.extend({'info': response}, order)
393
394 def price_to_precision(self, symbol, price):
395 """
396 Wrapped to avoid float
397 """
398 return ('{:.' + str(self.markets[symbol]['precision']['price']) + 'f}').format(price).rstrip("0").rstrip(".")
399
400 def amount_to_precision(self, symbol, amount):
401 """
402 Wrapped to avoid float
403 """
404 return ('{:.' + str(self.markets[symbol]['precision']['amount']) + 'f}').format(amount).rstrip("0").rstrip(".")
393 405
394 def common_currency_code(self, currency): 406 def common_currency_code(self, currency):
395 """ 407 """
diff --git a/main.py b/main.py
index 13c2240..a461207 100644
--- a/main.py
+++ b/main.py
@@ -63,7 +63,7 @@ def get_user_market(config_path, user_id, debug=False):
63 if debug: 63 if debug:
64 args.append("--debug") 64 args.append("--debug")
65 args = parse_args(args) 65 args = parse_args(args)
66 pg_config = parse_config(args) 66 pg_config, redis_config = parse_config(args)
67 market_id, market_config, user_id = list(fetch_markets(pg_config, str(user_id)))[0] 67 market_id, market_config, user_id = list(fetch_markets(pg_config, str(user_id)))[0]
68 return market.Market.from_config(market_config, args, 68 return market.Market.from_config(market_config, args,
69 pg_config=pg_config, market_id=market_id, 69 pg_config=pg_config, market_id=market_id,
diff --git a/market.py b/market.py
index eff670c..fc6f9f6 100644
--- a/market.py
+++ b/market.py
@@ -391,14 +391,14 @@ class Processor:
391 process_name = "process_{}__{}_{}".format(scenario_name, step["number"], step["name"]) 391 process_name = "process_{}__{}_{}".format(scenario_name, step["number"], step["name"])
392 self.market.report.log_stage("{}_begin".format(process_name)) 392 self.market.report.log_stage("{}_begin".format(process_name))
393 if "begin" in step.get("fetch_balances", []): 393 if "begin" in step.get("fetch_balances", []):
394 self.market.balances.fetch_balances(tag="{}_begin".format(process_name)) 394 self.market.balances.fetch_balances(tag="{}_begin".format(process_name), log_tickers=True)
395 395
396 for action in self.ordered_actions: 396 for action in self.ordered_actions:
397 if action in step: 397 if action in step:
398 self.run_action(action, step[action], kwargs) 398 self.run_action(action, step[action], kwargs)
399 399
400 if "end" in step.get("fetch_balances", []): 400 if "end" in step.get("fetch_balances", []):
401 self.market.balances.fetch_balances(tag="{}_end".format(process_name)) 401 self.market.balances.fetch_balances(tag="{}_end".format(process_name), log_tickers=True)
402 self.market.report.log_stage("{}_end".format(process_name)) 402 self.market.report.log_stage("{}_end".format(process_name))
403 403
404 def method_arguments(self, action): 404 def method_arguments(self, action):
diff --git a/portfolio.py b/portfolio.py
index 1067b0b..d8a5465 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -320,8 +320,6 @@ class Trade:
320 ticker = ticker["original"] 320 ticker = ticker["original"]
321 rate = Computation.compute_value(ticker, self.order_action(), compute_value=compute_value) 321 rate = Computation.compute_value(ticker, self.order_action(), compute_value=compute_value)
322 322
323 # FIXME: Dust amount should be removed from there if they werent
324 # honored in other sales
325 delta_in_base = abs(self.delta) 323 delta_in_base = abs(self.delta)
326 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case) 324 # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
327 325
@@ -580,12 +578,12 @@ class Order:
580 self.fetch_mouvements() 578 self.fetch_mouvements()
581 579
582 self.mark_disappeared_order() 580 self.mark_disappeared_order()
583 581 self.mark_dust_amount_remaining_order()
584 self.mark_finished_order() 582 self.mark_finished_order()
585 # FIXME: consider open order with dust remaining as closed
586 583
587 def dust_amount_remaining(self): 584 def mark_dust_amount_remaining_order(self):
588 return self.remaining_amount() < Amount(self.amount.currency, D("0.001")) 585 if self.market.ccxt.is_dust_trade(self.remaining_amount().value, self.rate):
586 self.status = "closed_dust_remaining"
589 587
590 def remaining_amount(self): 588 def remaining_amount(self):
591 return self.amount - self.filled_amount() 589 return self.amount - self.filled_amount()
@@ -618,7 +616,7 @@ class Order:
618 self.market.report.log_debug_action("Mark {} as cancelled".format(self)) 616 self.market.report.log_debug_action("Mark {} as cancelled".format(self))
619 self.status = "canceled" 617 self.status = "canceled"
620 return 618 return
621 if self.open and self.id is not None: 619 if (self.status == "closed_dust_remaining" or self.open) and self.id is not None:
622 try: 620 try:
623 self.market.ccxt.cancel_order(self.id) 621 self.market.ccxt.cancel_order(self.id)
624 except OrderNotFound as e: # Closed inbetween 622 except OrderNotFound as e: # Closed inbetween
@@ -650,7 +648,6 @@ class Order:
650 return True 648 return True
651 649
652 similar_trades = self.market.ccxt.fetch_my_trades(symbol=symbol, since=start_timestamp) 650 similar_trades = self.market.ccxt.fetch_my_trades(symbol=symbol, since=start_timestamp)
653 # FIXME: use set instead of sorted(list(...))
654 for order_id in sorted(list(map(lambda x: x["order"], similar_trades))): 651 for order_id in sorted(list(map(lambda x: x["order"], similar_trades))):
655 trades = list(filter(lambda x: x["order"] == order_id, similar_trades)) 652 trades = list(filter(lambda x: x["order"] == order_id, similar_trades))
656 if any(x["timestamp"] < start_timestamp for x in trades): 653 if any(x["timestamp"] < start_timestamp for x in trades):
diff --git a/store.py b/store.py
index 072d3a2..cd0bf7b 100644
--- a/store.py
+++ b/store.py
@@ -98,7 +98,8 @@ class ReportStore:
98 "args": args, 98 "args": args,
99 }) 99 })
100 100
101 def log_balances(self, tag=None): 101 def log_balances(self, tag=None, tickers=None,
102 ticker_currency=None, compute_value=None, type=None):
102 self.print_log("[Balance]") 103 self.print_log("[Balance]")
103 for currency, balance in self.market.balances.all.items(): 104 for currency, balance in self.market.balances.all.items():
104 self.print_log("\t{}".format(balance)) 105 self.print_log("\t{}".format(balance))
@@ -109,11 +110,22 @@ class ReportStore:
109 "balances": self.market.balances.as_json() 110 "balances": self.market.balances.as_json()
110 } 111 }
111 112
113 if tickers is not None:
114 log["tickers"] = self._ticker_hash(tickers, ticker_currency,
115 compute_value, type)
116
112 self.add_log(log.copy()) 117 self.add_log(log.copy())
113 self.add_redis_status(log) 118 self.add_redis_status(log)
114 119
115 def log_tickers(self, amounts, other_currency, 120 def log_tickers(self, amounts, other_currency,
116 compute_value, type): 121 compute_value, type):
122 log = self._ticker_hash(amounts, other_currency, compute_value,
123 type)
124 log["type"] = "tickers"
125
126 self.add_log(log)
127
128 def _ticker_hash(self, amounts, other_currency, compute_value, type):
117 values = {} 129 values = {}
118 rates = {} 130 rates = {}
119 if callable(compute_value): 131 if callable(compute_value):
@@ -122,8 +134,7 @@ class ReportStore:
122 for currency, amount in amounts.items(): 134 for currency, amount in amounts.items():
123 values[currency] = amount.as_json()["value"] 135 values[currency] = amount.as_json()["value"]
124 rates[currency] = amount.rate 136 rates[currency] = amount.rate
125 log = { 137 return {
126 "type": "tickers",
127 "compute_value": compute_value, 138 "compute_value": compute_value,
128 "balance_type": type, 139 "balance_type": type,
129 "currency": other_currency, 140 "currency": other_currency,
@@ -132,9 +143,6 @@ class ReportStore:
132 "total": sum(amounts.values()).as_json()["value"] 143 "total": sum(amounts.values()).as_json()["value"]
133 } 144 }
134 145
135 self.add_log(log.copy())
136 self.add_redis_status(log)
137
138 def log_dispatch(self, amount, amounts, liquidity, repartition): 146 def log_dispatch(self, amount, amounts, liquidity, repartition):
139 self.add_log({ 147 self.add_log({
140 "type": "dispatch", 148 "type": "dispatch",
@@ -294,13 +302,20 @@ class BalanceStore:
294 compute_value, type) 302 compute_value, type)
295 return amounts 303 return amounts
296 304
297 def fetch_balances(self, tag=None): 305 def fetch_balances(self, tag=None, log_tickers=False,
306 ticker_currency="BTC", ticker_compute_value="average", ticker_type="total"):
298 all_balances = self.market.ccxt.fetch_all_balances() 307 all_balances = self.market.ccxt.fetch_all_balances()
299 for currency, balance in all_balances.items(): 308 for currency, balance in all_balances.items():
300 if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \ 309 if balance["exchange_total"] != 0 or balance["margin_total"] != 0 or \
301 currency in self.all: 310 currency in self.all:
302 self.all[currency] = portfolio.Balance(currency, balance) 311 self.all[currency] = portfolio.Balance(currency, balance)
303 self.market.report.log_balances(tag=tag) 312 if log_tickers:
313 tickers = self.in_currency(ticker_currency, compute_value=ticker_compute_value, type=ticker_type)
314 self.market.report.log_balances(tag=tag,
315 tickers=tickers, ticker_currency=ticker_currency,
316 compute_value=ticker_compute_value, type=ticker_type)
317 else:
318 self.market.report.log_balances(tag=tag)
304 319
305 def dispatch_assets(self, amount, liquidity="medium", repartition=None): 320 def dispatch_assets(self, amount, liquidity="medium", repartition=None):
306 if repartition is None: 321 if repartition is None:
diff --git a/tests/test_ccxt_wrapper.py b/tests/test_ccxt_wrapper.py
index 10e334d..c326f0a 100644
--- a/tests/test_ccxt_wrapper.py
+++ b/tests/test_ccxt_wrapper.py
@@ -110,7 +110,23 @@ class poloniexETest(unittest.TestCase):
110 retry_call.assert_not_called() 110 retry_call.assert_not_called()
111 111
112 def test_order_precision(self): 112 def test_order_precision(self):
113 self.assertEqual(8, self.s.order_precision("FOO")) 113 self.s.markets = {
114 "FOO": {
115 "precision": {
116 "price": 5,
117 "amount": 6,
118 }
119 },
120 "BAR": {
121 "precision": {
122 "price": 7,
123 "amount": 8,
124 }
125 }
126 }
127 with mock.patch.object(self.s, "load_markets") as load_markets:
128 self.assertEqual(5, self.s.order_precision("FOO"))
129 load_markets.assert_called_once()
114 130
115 def test_transfer_balance(self): 131 def test_transfer_balance(self):
116 with self.subTest(success=True),\ 132 with self.subTest(success=True),\
@@ -167,33 +183,6 @@ class poloniexETest(unittest.TestCase):
167 } 183 }
168 self.assertEqual(expected, self.s.margin_summary()) 184 self.assertEqual(expected, self.s.margin_summary())
169 185
170 def test_create_order(self):
171 with mock.patch.object(self.s, "create_exchange_order") as exchange,\
172 mock.patch.object(self.s, "create_margin_order") as margin:
173 with self.subTest(account="unspecified"):
174 self.s.create_order("symbol", "type", "side", "amount", price="price", lending_rate="lending_rate", params="params")
175 exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params")
176 margin.assert_not_called()
177 exchange.reset_mock()
178 margin.reset_mock()
179
180 with self.subTest(account="exchange"):
181 self.s.create_order("symbol", "type", "side", "amount", account="exchange", price="price", lending_rate="lending_rate", params="params")
182 exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params")
183 margin.assert_not_called()
184 exchange.reset_mock()
185 margin.reset_mock()
186
187 with self.subTest(account="margin"):
188 self.s.create_order("symbol", "type", "side", "amount", account="margin", price="price", lending_rate="lending_rate", params="params")
189 margin.assert_called_once_with("symbol", "type", "side", "amount", lending_rate="lending_rate", price="price", params="params")
190 exchange.assert_not_called()
191 exchange.reset_mock()
192 margin.reset_mock()
193
194 with self.subTest(account="unknown"), self.assertRaises(NotImplementedError):
195 self.s.create_order("symbol", "type", "side", "amount", account="unknown")
196
197 def test_parse_ticker(self): 186 def test_parse_ticker(self):
198 ticker = { 187 ticker = {
199 "high24hr": "12", 188 "high24hr": "12",
@@ -439,11 +428,13 @@ class poloniexETest(unittest.TestCase):
439 self.assertEqual(expected_doge, result["DOGE"]) 428 self.assertEqual(expected_doge, result["DOGE"])
440 self.assertEqual(expected_btc, result["BTC"]) 429 self.assertEqual(expected_btc, result["BTC"])
441 430
442 def test_create_margin_order(self): 431 def test_create_order(self):
443 with self.assertRaises(market.ExchangeError): 432 with self.subTest(type="market"),\
444 self.s.create_margin_order("FOO", "market", "buy", "10") 433 self.assertRaises(market.ExchangeError):
434 self.s.create_order("FOO", "market", "buy", "10")
445 435
446 with mock.patch.object(self.s, "load_markets") as load_markets,\ 436 with self.subTest(type="limit", account="margin"),\
437 mock.patch.object(self.s, "load_markets") as load_markets,\
447 mock.patch.object(self.s, "privatePostMarginBuy") as margin_buy,\ 438 mock.patch.object(self.s, "privatePostMarginBuy") as margin_buy,\
448 mock.patch.object(self.s, "privatePostMarginSell") as margin_sell,\ 439 mock.patch.object(self.s, "privatePostMarginSell") as margin_sell,\
449 mock.patch.object(self.s, "market") as market_mock,\ 440 mock.patch.object(self.s, "market") as market_mock,\
@@ -460,26 +451,101 @@ class poloniexETest(unittest.TestCase):
460 ptp.return_value = D("0.1") 451 ptp.return_value = D("0.1")
461 atp.return_value = D("12") 452 atp.return_value = D("12")
462 453
463 order = self.s.create_margin_order("BTC_ETC", "margin", "buy", "12", price="0.1") 454 order = self.s.create_order("BTC_ETC", "limit", "buy", "12",
455 account="margin", price="0.1")
464 self.assertEqual(123, order["id"]) 456 self.assertEqual(123, order["id"])
465 margin_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")}) 457 margin_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")})
466 margin_sell.assert_not_called() 458 margin_sell.assert_not_called()
467 margin_buy.reset_mock() 459 margin_buy.reset_mock()
468 margin_sell.reset_mock() 460 margin_sell.reset_mock()
469 461
470 order = self.s.create_margin_order("BTC_ETC", "margin", "sell", "12", lending_rate="0.01", price="0.1") 462 order = self.s.create_order("BTC_ETC", "limit", "sell",
463 "12", account="margin", lending_rate="0.01", price="0.1")
471 self.assertEqual(456, order["id"]) 464 self.assertEqual(456, order["id"])
472 margin_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12"), "lendingRate": "0.01"}) 465 margin_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12"), "lendingRate": "0.01"})
473 margin_buy.assert_not_called() 466 margin_buy.assert_not_called()
474 467
475 def test_create_exchange_order(self): 468 with self.subTest(type="limit", account="exchange"),\
476 with mock.patch.object(market.ccxt.poloniex, "create_order") as create_order: 469 mock.patch.object(self.s, "load_markets") as load_markets,\
477 self.s.create_order("symbol", "type", "side", "amount", price="price", params="params") 470 mock.patch.object(self.s, "privatePostBuy") as exchange_buy,\
471 mock.patch.object(self.s, "privatePostSell") as exchange_sell,\
472 mock.patch.object(self.s, "market") as market_mock,\
473 mock.patch.object(self.s, "price_to_precision") as ptp,\
474 mock.patch.object(self.s, "amount_to_precision") as atp:
475
476 exchange_buy.return_value = {
477 "orderNumber": 123
478 }
479 exchange_sell.return_value = {
480 "orderNumber": 456
481 }
482 market_mock.return_value = { "id": "BTC_ETC", "symbol": "BTC_ETC" }
483 ptp.return_value = D("0.1")
484 atp.return_value = D("12")
485
486 order = self.s.create_order("BTC_ETC", "limit", "buy", "12",
487 account="exchange", price="0.1")
488 self.assertEqual(123, order["id"])
489 exchange_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")})
490 exchange_sell.assert_not_called()
491 exchange_buy.reset_mock()
492 exchange_sell.reset_mock()
478 493
479 create_order.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") 494 order = self.s.create_order("BTC_ETC", "limit", "sell",
495 "12", account="exchange", lending_rate="0.01", price="0.1")
496 self.assertEqual(456, order["id"])
497 exchange_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")})
498 exchange_buy.assert_not_called()
499
500 with self.subTest(account="unknown"), self.assertRaises(NotImplementedError),\
501 mock.patch.object(self.s, "load_markets") as load_markets:
502 self.s.create_order("symbol", "type", "side", "amount", account="unknown")
480 503
481 def test_common_currency_code(self): 504 def test_common_currency_code(self):
482 self.assertEqual("FOO", self.s.common_currency_code("FOO")) 505 self.assertEqual("FOO", self.s.common_currency_code("FOO"))
483 506
484 def test_currency_id(self): 507 def test_currency_id(self):
485 self.assertEqual("FOO", self.s.currency_id("FOO")) 508 self.assertEqual("FOO", self.s.currency_id("FOO"))
509
510 def test_amount_to_precision(self):
511 self.s.markets = {
512 "FOO": {
513 "precision": {
514 "price": 5,
515 "amount": 6,
516 }
517 },
518 "BAR": {
519 "precision": {
520 "price": 7,
521 "amount": 8,
522 }
523 }
524 }
525 self.assertEqual("0.0001", self.s.amount_to_precision("FOO", D("0.0001")))
526 self.assertEqual("0.0000001", self.s.amount_to_precision("BAR", D("0.0000001")))
527 self.assertEqual("0.000001", self.s.amount_to_precision("FOO", D("0.000001")))
528
529 def test_price_to_precision(self):
530 self.s.markets = {
531 "FOO": {
532 "precision": {
533 "price": 5,
534 "amount": 6,
535 }
536 },
537 "BAR": {
538 "precision": {
539 "price": 7,
540 "amount": 8,
541 }
542 }
543 }
544 self.assertEqual("0.0001", self.s.price_to_precision("FOO", D("0.0001")))
545 self.assertEqual("0.0000001", self.s.price_to_precision("BAR", D("0.0000001")))
546 self.assertEqual("0", self.s.price_to_precision("FOO", D("0.000001")))
547
548 def test_is_dust_trade(self):
549 self.assertTrue(self.s.is_dust_trade(D("0.0000009"), D("1000")))
550 self.assertTrue(self.s.is_dust_trade(D("0.000001"), D("10")))
551 self.assertFalse(self.s.is_dust_trade(D("0.000001"), D("100")))
diff --git a/tests/test_main.py b/tests/test_main.py
index b650870..55b1382 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -103,7 +103,7 @@ class MainTest(WebMockTestCase):
103 mock.patch("main.parse_config") as main_parse_config: 103 mock.patch("main.parse_config") as main_parse_config:
104 with self.subTest(debug=False): 104 with self.subTest(debug=False):
105 main_parse_args.return_value = self.market_args() 105 main_parse_args.return_value = self.market_args()
106 main_parse_config.return_value = "pg_config" 106 main_parse_config.return_value = ["pg_config", "redis_config"]
107 main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)] 107 main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)]
108 m = main.get_user_market("config_path.ini", 1) 108 m = main.get_user_market("config_path.ini", 1)
109 109
@@ -114,7 +114,7 @@ class MainTest(WebMockTestCase):
114 main_parse_args.reset_mock() 114 main_parse_args.reset_mock()
115 with self.subTest(debug=True): 115 with self.subTest(debug=True):
116 main_parse_args.return_value = self.market_args(debug=True) 116 main_parse_args.return_value = self.market_args(debug=True)
117 main_parse_config.return_value = "pg_config" 117 main_parse_config.return_value = ["pg_config", "redis_config"]
118 main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)] 118 main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)]
119 m = main.get_user_market("config_path.ini", 1, debug=True) 119 m = main.get_user_market("config_path.ini", 1, debug=True)
120 120
diff --git a/tests/test_market.py b/tests/test_market.py
index 53630b7..6a3322c 100644
--- a/tests/test_market.py
+++ b/tests/test_market.py
@@ -993,8 +993,8 @@ class ProcessorTest(WebMockTestCase):
993 mock.call("process_foo__1_sell_end"), 993 mock.call("process_foo__1_sell_end"),
994 ]) 994 ])
995 self.m.balances.fetch_balances.assert_has_calls([ 995 self.m.balances.fetch_balances.assert_has_calls([
996 mock.call(tag="process_foo__1_sell_begin"), 996 mock.call(tag="process_foo__1_sell_begin", log_tickers=True),
997 mock.call(tag="process_foo__1_sell_end"), 997 mock.call(tag="process_foo__1_sell_end", log_tickers=True),
998 ]) 998 ])
999 999
1000 self.assertEqual(5, run_action.call_count) 1000 self.assertEqual(5, run_action.call_count)
diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py
index 4d78996..969f5d4 100644
--- a/tests/test_portfolio.py
+++ b/tests/test_portfolio.py
@@ -823,14 +823,16 @@ class OrderTest(WebMockTestCase):
823 order.cancel() 823 order.cancel()
824 self.m.ccxt.cancel_order.assert_not_called() 824 self.m.ccxt.cancel_order.assert_not_called()
825 825
826 def test_dust_amount_remaining(self): 826 def test_mark_dust_amount_remaining(self):
827 order = portfolio.Order("buy", portfolio.Amount("ETH", 10), 827 order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
828 D("0.1"), "BTC", "long", self.m, "trade") 828 D("0.1"), "BTC", "long", self.m, "trade")
829 order.remaining_amount = mock.Mock(return_value=portfolio.Amount("ETH", 1)) 829 self.m.ccxt.is_dust_trade.return_value = False
830 self.assertFalse(order.dust_amount_remaining()) 830 order.mark_dust_amount_remaining_order()
831 self.assertEqual("pending", order.status)
831 832
832 order.remaining_amount = mock.Mock(return_value=portfolio.Amount("ETH", D("0.0001"))) 833 self.m.ccxt.is_dust_trade.return_value = True
833 self.assertTrue(order.dust_amount_remaining()) 834 order.mark_dust_amount_remaining_order()
835 self.assertEqual("closed_dust_remaining", order.status)
834 836
835 @mock.patch.object(portfolio.Order, "fetch") 837 @mock.patch.object(portfolio.Order, "fetch")
836 @mock.patch.object(portfolio.Order, "filled_amount", return_value=portfolio.Amount("ETH", 1)) 838 @mock.patch.object(portfolio.Order, "filled_amount", return_value=portfolio.Amount("ETH", 1))
@@ -965,6 +967,7 @@ class OrderTest(WebMockTestCase):
965 "status": "foo", 967 "status": "foo",
966 "datetime": "timestamp" 968 "datetime": "timestamp"
967 } 969 }
970 self.m.ccxt.is_dust_trade.return_value = False
968 order.fetch() 971 order.fetch()
969 972
970 self.m.ccxt.fetch_order.assert_called_once_with(45) 973 self.m.ccxt.fetch_order.assert_called_once_with(45)
diff --git a/tests/test_store.py b/tests/test_store.py
index df113b7..12999d3 100644
--- a/tests/test_store.py
+++ b/tests/test_store.py
@@ -369,17 +369,27 @@ class BalanceStoreTest(WebMockTestCase):
369 369
370 balance_store = market.BalanceStore(self.m) 370 balance_store = market.BalanceStore(self.m)
371 371
372 balance_store.fetch_balances() 372 with self.subTest(log_tickers=False):
373 self.assertNotIn("ETC", balance_store.currencies()) 373 balance_store.fetch_balances()
374 self.assertListEqual(["USDT", "XVG", "XMR"], list(balance_store.currencies())) 374 self.assertNotIn("ETC", balance_store.currencies())
375 375 self.assertListEqual(["USDT", "XVG", "XMR"], list(balance_store.currencies()))
376 balance_store.all["ETC"] = portfolio.Balance("ETC", { 376
377 "exchange_total": "1", "exchange_free": "0", 377 balance_store.all["ETC"] = portfolio.Balance("ETC", {
378 "exchange_used": "1" }) 378 "exchange_total": "1", "exchange_free": "0",
379 balance_store.fetch_balances(tag="foo") 379 "exchange_used": "1" })
380 self.assertEqual(0, balance_store.all["ETC"].total) 380 balance_store.fetch_balances(tag="foo")
381 self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies())) 381 self.assertEqual(0, balance_store.all["ETC"].total)
382 self.m.report.log_balances.assert_called_with(tag="foo") 382 self.assertListEqual(["USDT", "XVG", "XMR", "ETC"], list(balance_store.currencies()))
383 self.m.report.log_balances.assert_called_with(tag="foo")
384
385 with self.subTest(log_tickers=True),\
386 mock.patch.object(balance_store, "in_currency") as in_currency:
387 in_currency.return_value = "tickers"
388 balance_store.fetch_balances(log_tickers=True, ticker_currency="FOO",
389 ticker_compute_value="compute", ticker_type="type")
390 self.m.report.log_balances.assert_called_with(compute_value='compute',
391 tag=None, ticker_currency='FOO', tickers='tickers',
392 type='type')
383 393
384 @mock.patch.object(market.Portfolio, "repartition") 394 @mock.patch.object(market.Portfolio, "repartition")
385 def test_dispatch_assets(self, repartition): 395 def test_dispatch_assets(self, repartition):
@@ -586,27 +596,77 @@ class ReportStoreTest(WebMockTestCase):
586 self.m.balances.as_json.return_value = "json" 596 self.m.balances.as_json.return_value = "json"
587 self.m.balances.all = { "FOO": "bar", "BAR": "baz" } 597 self.m.balances.all = { "FOO": "bar", "BAR": "baz" }
588 598
589 report_store.log_balances(tag="tag") 599 with self.subTest(tickers=None):
590 print_log.assert_has_calls([ 600 report_store.log_balances(tag="tag")
591 mock.call("[Balance]"), 601 print_log.assert_has_calls([
592 mock.call("\tbar"), 602 mock.call("[Balance]"),
593 mock.call("\tbaz"), 603 mock.call("\tbar"),
594 ]) 604 mock.call("\tbaz"),
595 add_log.assert_called_once_with({ 605 ])
596 'type': 'balance', 606 add_log.assert_called_once_with({
597 'balances': 'json', 607 'type': 'balance',
598 'tag': 'tag' 608 'balances': 'json',
599 }) 609 'tag': 'tag'
600 add_redis_status.assert_called_once_with({ 610 })
601 'type': 'balance', 611 add_redis_status.assert_called_once_with({
602 'balances': 'json', 612 'type': 'balance',
603 'tag': 'tag' 613 'balances': 'json',
604 }) 614 'tag': 'tag'
615 })
616 add_log.reset_mock()
617 add_redis_status.reset_mock()
618 with self.subTest(tickers="present"):
619 amounts = {
620 "BTC": portfolio.Amount("BTC", 10),
621 "ETH": portfolio.Amount("BTC", D("0.3"))
622 }
623 amounts["ETH"].rate = D("0.1")
624
625 report_store.log_balances(tag="tag", tickers=amounts,
626 ticker_currency="BTC", compute_value="default",
627 type="total")
628 add_log.assert_called_once_with({
629 'type': 'balance',
630 'balances': 'json',
631 'tag': 'tag',
632 'tickers': {
633 'compute_value': 'default',
634 'balance_type': 'total',
635 'currency': 'BTC',
636 'balances': {
637 'BTC': D('10'),
638 'ETH': D('0.3')
639 },
640 'rates': {
641 'BTC': None,
642 'ETH': D('0.1')
643 },
644 'total': D('10.3')
645 },
646 })
647 add_redis_status.assert_called_once_with({
648 'type': 'balance',
649 'balances': 'json',
650 'tag': 'tag',
651 'tickers': {
652 'compute_value': 'default',
653 'balance_type': 'total',
654 'currency': 'BTC',
655 'balances': {
656 'BTC': D('10'),
657 'ETH': D('0.3')
658 },
659 'rates': {
660 'BTC': None,
661 'ETH': D('0.1')
662 },
663 'total': D('10.3')
664 },
665 })
605 666
606 @mock.patch.object(market.ReportStore, "print_log") 667 @mock.patch.object(market.ReportStore, "print_log")
607 @mock.patch.object(market.ReportStore, "add_log") 668 @mock.patch.object(market.ReportStore, "add_log")
608 @mock.patch.object(market.ReportStore, "add_redis_status") 669 def test_log_tickers(self, add_log, print_log):
609 def test_log_tickers(self, add_redis_status, add_log, print_log):
610 report_store = market.ReportStore(self.m) 670 report_store = market.ReportStore(self.m)
611 amounts = { 671 amounts = {
612 "BTC": portfolio.Amount("BTC", 10), 672 "BTC": portfolio.Amount("BTC", 10),
@@ -631,21 +691,6 @@ class ReportStoreTest(WebMockTestCase):
631 }, 691 },
632 'total': D('10.3') 692 'total': D('10.3')
633 }) 693 })
634 add_redis_status.assert_called_once_with({
635 'type': 'tickers',
636 'compute_value': 'default',
637 'balance_type': 'total',
638 'currency': 'BTC',
639 'balances': {
640 'BTC': D('10'),
641 'ETH': D('0.3')
642 },
643 'rates': {
644 'BTC': None,
645 'ETH': D('0.1')
646 },
647 'total': D('10.3')
648 })
649 694
650 add_log.reset_mock() 695 add_log.reset_mock()
651 compute_value = lambda x: x["bid"] 696 compute_value = lambda x: x["bid"]