aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-25 22:01:59 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-25 22:01:59 +0200
commitd004a2a5e15a78991870dcb90cd6db63ab40a4e6 (patch)
treedfade60890ffe5529dc80fec0b23702b97ea0383
parent45fffd4963005a1f3957868f9ddb1aa7ec66c0e3 (diff)
parent7f3e7d27409fd7355db9ae4f8fe9a346cd50c712 (diff)
downloadTrader-d004a2a5e15a78991870dcb90cd6db63ab40a4e6.tar.gz
Trader-d004a2a5e15a78991870dcb90cd6db63ab40a4e6.tar.zst
Trader-d004a2a5e15a78991870dcb90cd6db63ab40a4e6.zip
Merge branch 'timeouts' into dev
Fixes https://git.immae.eu/mantisbt/view.php?id=58
-rw-r--r--market.py22
-rw-r--r--portfolio.py79
-rw-r--r--test.py634
3 files changed, 721 insertions, 14 deletions
diff --git a/market.py b/market.py
index 055967c..ca65bca 100644
--- a/market.py
+++ b/market.py
@@ -1,10 +1,11 @@
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 4import psycopg2
5from store import * 5from store import *
6from cachetools.func import ttl_cache 6from cachetools.func import ttl_cache
7from datetime import datetime 7from datetime import datetime
8from retry import retry
8import portfolio 9import portfolio
9 10
10class Market: 11class Market:
@@ -88,6 +89,7 @@ class Market:
88 finally: 89 finally:
89 self.store_report() 90 self.store_report()
90 91
92 @retry(RequestTimeout, tries=5)
91 def move_balances(self): 93 def move_balances(self):
92 needed_in_margin = {} 94 needed_in_margin = {}
93 moving_to_margin = {} 95 moving_to_margin = {}
@@ -102,13 +104,21 @@ class Market:
102 current_balance = self.balances.all[currency].margin_available 104 current_balance = self.balances.all[currency].margin_available
103 moving_to_margin[currency] = (needed - current_balance) 105 moving_to_margin[currency] = (needed - current_balance)
104 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
105 if self.debug and delta != 0: 109 if self.debug and delta != 0:
106 self.report.log_debug_action("Moving {} from exchange to margin".format(moving_to_margin[currency])) 110 self.report.log_debug_action(action)
107 continue 111 continue
108 if delta > 0: 112 try:
109 self.ccxt.transfer_balance(currency, delta, "exchange", "margin") 113 if delta > 0:
110 elif delta < 0: 114 self.ccxt.transfer_balance(currency, delta, "exchange", "margin")
111 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
112 self.report.log_move_balances(needed_in_margin, moving_to_margin) 122 self.report.log_move_balances(needed_in_margin, moving_to_margin)
113 123
114 self.balances.fetch_balances() 124 self.balances.fetch_balances()
diff --git a/portfolio.py b/portfolio.py
index 69e3755..9c58676 100644
--- a/portfolio.py
+++ b/portfolio.py
@@ -1,9 +1,7 @@
1from datetime import datetime 1from datetime import datetime
2from decimal import Decimal as D, ROUND_DOWN
3from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound
4from retry import retry 2from retry import retry
5 3from decimal import Decimal as D, ROUND_DOWN
6# FIXME: correctly handle web call timeouts 4from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout
7 5
8class Computation: 6class Computation:
9 computations = { 7 computations = {
@@ -414,6 +412,9 @@ class Trade:
414 for mouvement in order.mouvements: 412 for mouvement in order.mouvements:
415 self.market.report.print_log("{}\t\t{}".format(ind, mouvement)) 413 self.market.report.print_log("{}\t\t{}".format(ind, mouvement))
416 414
415class RetryException(Exception):
416 pass
417
417class Order: 418class Order:
418 def __init__(self, action, amount, rate, base_currency, trade_type, market, 419 def __init__(self, action, amount, rate, base_currency, trade_type, market,
419 trade, close_if_possible=False): 420 trade, close_if_possible=False):
@@ -430,6 +431,7 @@ class Order:
430 self.close_if_possible = close_if_possible 431 self.close_if_possible = close_if_possible
431 self.id = None 432 self.id = None
432 self.tries = 0 433 self.tries = 0
434 self.start_date = None
433 435
434 def as_json(self): 436 def as_json(self):
435 return { 437 return {
@@ -475,18 +477,18 @@ class Order:
475 def finished(self): 477 def finished(self):
476 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"
477 479
478 @retry(InsufficientFunds) 480 @retry((InsufficientFunds, RetryException))
479 def run(self): 481 def run(self):
480 self.tries += 1 482 self.tries += 1
481 symbol = "{}/{}".format(self.amount.currency, self.base_currency) 483 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
482 amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value 484 amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value
483 485
486 action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account)
484 if self.market.debug: 487 if self.market.debug:
485 self.market.report.log_debug_action("market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( 488 self.market.report.log_debug_action(action)
486 symbol, self.action, amount, self.rate, self.account))
487 self.results.append({"debug": True, "id": -1}) 489 self.results.append({"debug": True, "id": -1})
488 else: 490 else:
489 action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account) 491 self.start_date = datetime.now()
490 try: 492 try:
491 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))
492 except InvalidOrder: 494 except InvalidOrder:
@@ -494,6 +496,19 @@ class Order:
494 self.status = "closed" 496 self.status = "closed"
495 self.mark_finished_order() 497 self.mark_finished_order()
496 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")
497 except InsufficientFunds as e: 512 except InsufficientFunds as e:
498 if self.tries < 5: 513 if self.tries < 5:
499 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)
@@ -585,6 +600,54 @@ class Order:
585 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)
586 self.fetch() 601 self.fetch()
587 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
588class Mouvement: 651class Mouvement:
589 def __init__(self, currency, base_currency, hash_): 652 def __init__(self, currency, base_currency, hash_):
590 self.currency = currency 653 self.currency = currency
diff --git a/test.py b/test.py
index 18616c1..5b9e2b4 100644
--- a/test.py
+++ b/test.py
@@ -1444,6 +1444,139 @@ class MarketTest(WebMockTestCase):
1444 self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin") 1444 self.ccxt.transfer_balance.assert_any_call("USDT", 100, "exchange", "margin")
1445 self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange") 1445 self.ccxt.transfer_balance.assert_any_call("ETC", 5, "margin", "exchange")
1446 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
1447 def test_store_file_report(self): 1580 def test_store_file_report(self):
1448 file_open = mock.mock_open() 1581 file_open = mock.mock_open()
1449 m = market.Market(self.ccxt, self.market_args(), report_path="present", user_id=1) 1582 m = market.Market(self.ccxt, self.market_args(), report_path="present", user_id=1)
@@ -3080,6 +3213,507 @@ class OrderTest(WebMockTestCase):
3080 self.assertEqual(5, self.m.report.log_error.call_count) 3213 self.assertEqual(5, self.m.report.log_error.call_count)
3081 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)
3082 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)
3083 3717
3084@unittest.skipUnless("unit" in limits, "Unit skipped") 3718@unittest.skipUnless("unit" in limits, "Unit skipped")
3085class MouvementTest(WebMockTestCase): 3719class MouvementTest(WebMockTestCase):