diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-05-01 17:35:55 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-05-01 17:35:55 +0200 |
commit | 9eb0de20f243bb78de0bf9118289f01f1ea1f77c (patch) | |
tree | 762d3a59d666bf6e1d4d1ea5901c31ab06f7e88e | |
parent | 6cffa4af8b5a04b17ffd95738b8e843c4605d4e7 (diff) | |
parent | 2b1ee8f4d54fa1672510141a71a5817120ac031c (diff) | |
download | Trader-9eb0de20f243bb78de0bf9118289f01f1ea1f77c.tar.gz Trader-9eb0de20f243bb78de0bf9118289f01f1ea1f77c.tar.zst Trader-9eb0de20f243bb78de0bf9118289f01f1ea1f77c.zip |
Merge branch 'dev'v1.5
-rw-r--r-- | ccxt_wrapper.py | 82 | ||||
-rw-r--r-- | main.py | 2 | ||||
-rw-r--r-- | market.py | 4 | ||||
-rw-r--r-- | portfolio.py | 13 | ||||
-rw-r--r-- | store.py | 31 | ||||
-rw-r--r-- | tests/test_ccxt_wrapper.py | 142 | ||||
-rw-r--r-- | tests/test_main.py | 4 | ||||
-rw-r--r-- | tests/test_market.py | 4 | ||||
-rw-r--r-- | tests/test_portfolio.py | 13 | ||||
-rw-r--r-- | tests/test_store.py | 133 |
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 | """ |
@@ -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, |
@@ -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): |
@@ -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"] |