diff options
-rw-r--r-- | market.py | 22 | ||||
-rw-r--r-- | test.py | 133 |
2 files changed, 149 insertions, 6 deletions
@@ -1,10 +1,11 @@ | |||
1 | from ccxt import ExchangeError, NotSupported | 1 | from ccxt import ExchangeError, NotSupported, RequestTimeout |
2 | import ccxt_wrapper as ccxt | 2 | import ccxt_wrapper as ccxt |
3 | import time | 3 | import time |
4 | import psycopg2 | 4 | import psycopg2 |
5 | from store import * | 5 | from store import * |
6 | from cachetools.func import ttl_cache | 6 | from cachetools.func import ttl_cache |
7 | from datetime import datetime | 7 | from datetime import datetime |
8 | from retry import retry | ||
8 | import portfolio | 9 | import portfolio |
9 | 10 | ||
10 | class Market: | 11 | class 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() |
@@ -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) |