Merge pull request #5888 from samgermain/contract-sizes
Convert contract size to underlying asset size
This commit is contained in:
commit
e57c2d64a5
@ -370,6 +370,49 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
return DataFrame()
|
return DataFrame()
|
||||||
|
|
||||||
|
def _get_contract_size(self, pair: str) -> int:
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
market = self.markets[pair]
|
||||||
|
contract_size = 1
|
||||||
|
if 'contractSize' in market and market['contractSize'] is not None:
|
||||||
|
contract_size = market['contractSize']
|
||||||
|
return contract_size
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def _trades_contracts_to_amount(self, trades: List) -> List:
|
||||||
|
if len(trades) > 0 and 'symbol' in trades[0]:
|
||||||
|
contract_size = self._get_contract_size(trades[0]['symbol'])
|
||||||
|
if contract_size != 1:
|
||||||
|
for trade in trades:
|
||||||
|
trade['amount'] = trade['amount'] * contract_size
|
||||||
|
return trades
|
||||||
|
|
||||||
|
def _order_contracts_to_amount(self, order: Dict) -> Dict:
|
||||||
|
if 'symbol' in order:
|
||||||
|
contract_size = self._get_contract_size(order['symbol'])
|
||||||
|
if contract_size != 1:
|
||||||
|
for prop in ['amount', 'cost', 'filled', 'remaining']:
|
||||||
|
if prop in order and order[prop] is not None:
|
||||||
|
order[prop] = order[prop] * contract_size
|
||||||
|
return order
|
||||||
|
|
||||||
|
def _amount_to_contracts(self, pair: str, amount: float):
|
||||||
|
|
||||||
|
contract_size = self._get_contract_size(pair)
|
||||||
|
if contract_size and contract_size != 1:
|
||||||
|
return amount / contract_size
|
||||||
|
else:
|
||||||
|
return amount
|
||||||
|
|
||||||
|
def _contracts_to_amount(self, pair: str, num_contracts: float):
|
||||||
|
|
||||||
|
contract_size = self._get_contract_size(pair)
|
||||||
|
if contract_size and contract_size != 1:
|
||||||
|
return num_contracts * contract_size
|
||||||
|
else:
|
||||||
|
return num_contracts
|
||||||
|
|
||||||
def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None:
|
def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None:
|
||||||
if exchange_config.get('sandbox'):
|
if exchange_config.get('sandbox'):
|
||||||
if api.urls.get('test'):
|
if api.urls.get('test'):
|
||||||
@ -587,6 +630,7 @@ class Exchange:
|
|||||||
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
|
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
|
||||||
based on our definitions.
|
based on our definitions.
|
||||||
"""
|
"""
|
||||||
|
amount = self._amount_to_contracts(pair, amount)
|
||||||
if self.markets[pair]['precision']['amount']:
|
if self.markets[pair]['precision']['amount']:
|
||||||
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
|
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
|
||||||
precision=self.markets[pair]['precision']['amount'],
|
precision=self.markets[pair]['precision']['amount'],
|
||||||
@ -645,11 +689,21 @@ class Exchange:
|
|||||||
limits = market['limits']
|
limits = market['limits']
|
||||||
if ('cost' in limits and 'min' in limits['cost']
|
if ('cost' in limits and 'min' in limits['cost']
|
||||||
and limits['cost']['min'] is not None):
|
and limits['cost']['min'] is not None):
|
||||||
min_stake_amounts.append(limits['cost']['min'])
|
min_stake_amounts.append(
|
||||||
|
self._contracts_to_amount(
|
||||||
|
pair,
|
||||||
|
limits['cost']['min']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if ('amount' in limits and 'min' in limits['amount']
|
if ('amount' in limits and 'min' in limits['amount']
|
||||||
and limits['amount']['min'] is not None):
|
and limits['amount']['min'] is not None):
|
||||||
min_stake_amounts.append(limits['amount']['min'] * price)
|
min_stake_amounts.append(
|
||||||
|
self._contracts_to_amount(
|
||||||
|
pair,
|
||||||
|
limits['amount']['min'] * price
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if not min_stake_amounts:
|
if not min_stake_amounts:
|
||||||
return None
|
return None
|
||||||
@ -685,7 +739,7 @@ class Exchange:
|
|||||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||||
rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]:
|
rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]:
|
||||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||||
_amount = self.amount_to_precision(pair, amount)
|
_amount = self._contracts_to_amount(pair, self.amount_to_precision(pair, amount))
|
||||||
dry_order: Dict[str, Any] = {
|
dry_order: Dict[str, Any] = {
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'symbol': pair,
|
'symbol': pair,
|
||||||
@ -861,6 +915,7 @@ class Exchange:
|
|||||||
params
|
params
|
||||||
)
|
)
|
||||||
self._log_exchange_response('create_order', order)
|
self._log_exchange_response('create_order', order)
|
||||||
|
order = self._order_contracts_to_amount(order)
|
||||||
return order
|
return order
|
||||||
|
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
@ -909,6 +964,7 @@ class Exchange:
|
|||||||
try:
|
try:
|
||||||
order = self._api.fetch_order(order_id, pair)
|
order = self._api.fetch_order(order_id, pair)
|
||||||
self._log_exchange_response('fetch_order', order)
|
self._log_exchange_response('fetch_order', order)
|
||||||
|
order = self._order_contracts_to_amount(order)
|
||||||
return order
|
return order
|
||||||
except ccxt.OrderNotFound as e:
|
except ccxt.OrderNotFound as e:
|
||||||
raise RetryableOrderError(
|
raise RetryableOrderError(
|
||||||
@ -963,6 +1019,7 @@ class Exchange:
|
|||||||
try:
|
try:
|
||||||
order = self._api.cancel_order(order_id, pair)
|
order = self._api.cancel_order(order_id, pair)
|
||||||
self._log_exchange_response('cancel_order', order)
|
self._log_exchange_response('cancel_order', order)
|
||||||
|
order = self._order_contracts_to_amount(order)
|
||||||
return order
|
return order
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise InvalidOrderException(
|
raise InvalidOrderException(
|
||||||
@ -1230,6 +1287,9 @@ class Exchange:
|
|||||||
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
||||||
|
|
||||||
self._log_exchange_response('get_trades_for_order', matched_trades)
|
self._log_exchange_response('get_trades_for_order', matched_trades)
|
||||||
|
|
||||||
|
matched_trades = self._trades_contracts_to_amount(matched_trades)
|
||||||
|
|
||||||
return matched_trades
|
return matched_trades
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
@ -1572,6 +1632,7 @@ class Exchange:
|
|||||||
'(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else ''
|
'(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else ''
|
||||||
)
|
)
|
||||||
trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
|
trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
|
||||||
|
trades = self._trades_contracts_to_amount(trades)
|
||||||
return trades_dict_to_list(trades)
|
return trades_dict_to_list(trades)
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
@ -914,6 +914,7 @@ def get_markets():
|
|||||||
'active': True,
|
'active': True,
|
||||||
'spot': True,
|
'spot': True,
|
||||||
'type': 'spot',
|
'type': 'spot',
|
||||||
|
'contractSize': None,
|
||||||
'precision': {
|
'precision': {
|
||||||
'amount': 8,
|
'amount': 8,
|
||||||
'price': 8
|
'price': 8
|
||||||
@ -937,7 +938,8 @@ def get_markets():
|
|||||||
'quote': 'USDT',
|
'quote': 'USDT',
|
||||||
'active': True,
|
'active': True,
|
||||||
'spot': False,
|
'spot': False,
|
||||||
'type': 'SomethingElse',
|
'type': 'swap',
|
||||||
|
'contractSize': 0.01,
|
||||||
'precision': {
|
'precision': {
|
||||||
'amount': 8,
|
'amount': 8,
|
||||||
'price': 8
|
'price': 8
|
||||||
@ -985,6 +987,59 @@ def get_markets():
|
|||||||
'info': {
|
'info': {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'ETH/USDT:USDT': {
|
||||||
|
'id': 'ETH_USDT',
|
||||||
|
'symbol': 'ETH/USDT:USDT',
|
||||||
|
'base': 'ETH',
|
||||||
|
'quote': 'USDT',
|
||||||
|
'settle': 'USDT',
|
||||||
|
'baseId': 'ETH',
|
||||||
|
'quoteId': 'USDT',
|
||||||
|
'settleId': 'USDT',
|
||||||
|
'type': 'swap',
|
||||||
|
'spot': False,
|
||||||
|
'margin': False,
|
||||||
|
'swap': True,
|
||||||
|
'futures': False,
|
||||||
|
'option': False,
|
||||||
|
'derivative': True,
|
||||||
|
'contract': True,
|
||||||
|
'linear': True,
|
||||||
|
'inverse': False,
|
||||||
|
'tierBased': False,
|
||||||
|
'percentage': True,
|
||||||
|
'taker': 0.0006,
|
||||||
|
'maker': 0.0002,
|
||||||
|
'contractSize': 10,
|
||||||
|
'active': True,
|
||||||
|
'expiry': None,
|
||||||
|
'expiryDatetime': None,
|
||||||
|
'strike': None,
|
||||||
|
'optionType': None,
|
||||||
|
'limits': {
|
||||||
|
'leverage': {
|
||||||
|
'min': 1,
|
||||||
|
'max': 100
|
||||||
|
},
|
||||||
|
'amount': {
|
||||||
|
'min': 1,
|
||||||
|
'max': 300000
|
||||||
|
},
|
||||||
|
'price': {
|
||||||
|
'min': None,
|
||||||
|
'max': None,
|
||||||
|
},
|
||||||
|
'cost': {
|
||||||
|
'min': None,
|
||||||
|
'max': None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'precision': {
|
||||||
|
'price': 0.05,
|
||||||
|
'amount': 1
|
||||||
|
},
|
||||||
|
'info': {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -224,27 +224,46 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
|
|||||||
ex.validate_order_time_in_force(tif2)
|
ex.validate_order_time_in_force(tif2)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("amount,precision_mode,precision,expected", [
|
@pytest.mark.parametrize("amount,precision_mode,precision,contract_size,expected,trading_mode", [
|
||||||
(2.34559, 2, 4, 2.3455),
|
(2.34559, 2, 4, 1, 2.3455, 'spot'),
|
||||||
(2.34559, 2, 5, 2.34559),
|
(2.34559, 2, 5, 1, 2.34559, 'spot'),
|
||||||
(2.34559, 2, 3, 2.345),
|
(2.34559, 2, 3, 1, 2.345, 'spot'),
|
||||||
(2.9999, 2, 3, 2.999),
|
(2.9999, 2, 3, 1, 2.999, 'spot'),
|
||||||
(2.9909, 2, 3, 2.990),
|
(2.9909, 2, 3, 1, 2.990, 'spot'),
|
||||||
# Tests for Tick-size
|
# Tests for Tick-size
|
||||||
(2.34559, 4, 0.0001, 2.3455),
|
(2.34559, 4, 0.0001, 1, 2.3455, 'spot'),
|
||||||
(2.34559, 4, 0.00001, 2.34559),
|
(2.34559, 4, 0.00001, 1, 2.34559, 'spot'),
|
||||||
(2.34559, 4, 0.001, 2.345),
|
(2.34559, 4, 0.001, 1, 2.345, 'spot'),
|
||||||
(2.9999, 4, 0.001, 2.999),
|
(2.9999, 4, 0.001, 1, 2.999, 'spot'),
|
||||||
(2.9909, 4, 0.001, 2.990),
|
(2.9909, 4, 0.001, 1, 2.990, 'spot'),
|
||||||
(2.9909, 4, 0.005, 2.990),
|
(2.9909, 4, 0.005, 0.01, 299.09, 'futures'),
|
||||||
(2.9999, 4, 0.005, 2.995),
|
(2.9999, 4, 0.005, 10, 0.295, 'futures'),
|
||||||
])
|
])
|
||||||
def test_amount_to_precision(default_conf, mocker, amount, precision_mode, precision, expected):
|
def test_amount_to_precision(
|
||||||
|
default_conf,
|
||||||
|
mocker,
|
||||||
|
amount,
|
||||||
|
precision_mode,
|
||||||
|
precision,
|
||||||
|
contract_size,
|
||||||
|
expected,
|
||||||
|
trading_mode
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Test rounds down
|
Test rounds down
|
||||||
"""
|
"""
|
||||||
|
|
||||||
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': precision}}})
|
markets = PropertyMock(return_value={
|
||||||
|
'ETH/BTC': {
|
||||||
|
'contractSize': contract_size,
|
||||||
|
'precision': {
|
||||||
|
'amount': precision
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
default_conf['trading_mode'] = trading_mode
|
||||||
|
default_conf['collateral'] = 'isolated'
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
# digits counting mode
|
# digits counting mode
|
||||||
@ -465,6 +484,28 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
|
|||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0)
|
||||||
assert isclose(result, expected_result/12)
|
assert isclose(result, expected_result/12)
|
||||||
|
|
||||||
|
markets["ETH/BTC"]["contractSize"] = 0.01
|
||||||
|
default_conf['trading_mode'] = 'futures'
|
||||||
|
default_conf['collateral'] = 'isolated'
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.markets',
|
||||||
|
PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Contract size 0.01
|
||||||
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1)
|
||||||
|
assert isclose(result, expected_result * 0.01)
|
||||||
|
|
||||||
|
markets["ETH/BTC"]["contractSize"] = 10
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.markets',
|
||||||
|
PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
# With Leverage, Contract size 10
|
||||||
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0)
|
||||||
|
assert isclose(result, (expected_result/12) * 10.0)
|
||||||
|
|
||||||
|
|
||||||
def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
|
def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
@ -1020,6 +1061,7 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name):
|
|||||||
assert order["side"] == side
|
assert order["side"] == side
|
||||||
assert order["type"] == "limit"
|
assert order["type"] == "limit"
|
||||||
assert order["symbol"] == "ETH/BTC"
|
assert order["symbol"] == "ETH/BTC"
|
||||||
|
assert order["amount"] == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("side,startprice,endprice", [
|
@pytest.mark.parametrize("side,startprice,endprice", [
|
||||||
@ -1127,9 +1169,12 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
|
|||||||
'id': order_id,
|
'id': order_id,
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
}
|
},
|
||||||
|
'symbol': 'XLTCUSDT',
|
||||||
|
'amount': 1
|
||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
default_conf['collateral'] = 'isolated'
|
||||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
@ -1137,7 +1182,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
|
|||||||
exchange.set_margin_mode = MagicMock()
|
exchange.set_margin_mode = MagicMock()
|
||||||
|
|
||||||
order = exchange.create_order(
|
order = exchange.create_order(
|
||||||
pair='ETH/BTC',
|
pair='XLTCUSDT',
|
||||||
ordertype=ordertype,
|
ordertype=ordertype,
|
||||||
side=side,
|
side=side,
|
||||||
amount=1,
|
amount=1,
|
||||||
@ -1148,7 +1193,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
|
|||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert order['id'] == order_id
|
assert order['id'] == order_id
|
||||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
assert order['amount'] == 1
|
||||||
|
assert api_mock.create_order.call_args[0][0] == 'XLTCUSDT'
|
||||||
assert api_mock.create_order.call_args[0][1] == ordertype
|
assert api_mock.create_order.call_args[0][1] == ordertype
|
||||||
assert api_mock.create_order.call_args[0][2] == side
|
assert api_mock.create_order.call_args[0][2] == side
|
||||||
assert api_mock.create_order.call_args[0][3] == 1
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
@ -1158,7 +1204,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
|
|||||||
|
|
||||||
exchange.trading_mode = TradingMode.FUTURES
|
exchange.trading_mode = TradingMode.FUTURES
|
||||||
order = exchange.create_order(
|
order = exchange.create_order(
|
||||||
pair='ETH/BTC',
|
pair='XLTCUSDT',
|
||||||
ordertype=ordertype,
|
ordertype=ordertype,
|
||||||
side=side,
|
side=side,
|
||||||
amount=1,
|
amount=1,
|
||||||
@ -1168,6 +1214,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
|
|||||||
|
|
||||||
assert exchange._set_leverage.call_count == 1
|
assert exchange._set_leverage.call_count == 1
|
||||||
assert exchange.set_margin_mode.call_count == 1
|
assert exchange.set_margin_mode.call_count == 1
|
||||||
|
assert order['amount'] == 0.01
|
||||||
|
|
||||||
|
|
||||||
def test_buy_dry_run(default_conf, mocker):
|
def test_buy_dry_run(default_conf, mocker):
|
||||||
@ -1189,6 +1236,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
|
|||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
|
'symbol': 'ETH/BTC',
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
}
|
}
|
||||||
@ -1264,6 +1312,7 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
|
|||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
|
'symbol': 'ETH/BTC',
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
}
|
}
|
||||||
@ -1326,6 +1375,7 @@ def test_sell_prod(default_conf, mocker, exchange_name):
|
|||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
|
'symbol': 'ETH/BTC',
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
}
|
}
|
||||||
@ -1392,6 +1442,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
|
|||||||
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
|
'symbol': 'ETH/BTC',
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
}
|
}
|
||||||
@ -2243,7 +2294,7 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na
|
|||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
|
async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
|
||||||
fetch_trades_result):
|
fetch_trades_result):
|
||||||
|
# TODO-lev: Test for contract sizes of 0.01 and 10
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
# Monkey-patch async function
|
# Monkey-patch async function
|
||||||
@ -2287,6 +2338,43 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
|
|||||||
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
|
await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
|
async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, exchange_name,
|
||||||
|
fetch_trades_result):
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
default_conf['collateral'] = 'isolated'
|
||||||
|
default_conf['trading_mode'] = 'futures'
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
|
# Monkey-patch async function
|
||||||
|
exchange._api_async.fetch_trades = get_mock_coro([
|
||||||
|
{'info': {'a': 126181333,
|
||||||
|
'p': '0.01952600',
|
||||||
|
'q': '0.01200000',
|
||||||
|
'f': 138604158,
|
||||||
|
'l': 138604158,
|
||||||
|
'T': 1565798399872,
|
||||||
|
'm': True,
|
||||||
|
'M': True},
|
||||||
|
'timestamp': 1565798399872,
|
||||||
|
'datetime': '2019-08-14T15:59:59.872Z',
|
||||||
|
'symbol': 'ETH/USDT:USDT',
|
||||||
|
'id': '126181383',
|
||||||
|
'order': None,
|
||||||
|
'type': None,
|
||||||
|
'takerOrMaker': None,
|
||||||
|
'side': 'sell',
|
||||||
|
'price': 2.0,
|
||||||
|
'amount': 30.0,
|
||||||
|
'cost': 60.0,
|
||||||
|
'fee': None}]
|
||||||
|
)
|
||||||
|
|
||||||
|
pair = 'ETH/USDT:USDT'
|
||||||
|
res = await exchange._async_fetch_trades(pair, since=None, params=None)
|
||||||
|
assert res[0][5] == 300
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
async def test__async_get_trade_history_id(default_conf, mocker, exchange_name,
|
async def test__async_get_trade_history_id(default_conf, mocker, exchange_name,
|
||||||
@ -2595,6 +2683,8 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog):
|
|||||||
default_conf['exchange']['log_responses'] = True
|
default_conf['exchange']['log_responses'] = True
|
||||||
order = MagicMock()
|
order = MagicMock()
|
||||||
order.myid = 123
|
order.myid = 123
|
||||||
|
order.symbol = 'TKN/BTC'
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
exchange._dry_run_open_orders['X'] = order
|
exchange._dry_run_open_orders['X'] = order
|
||||||
assert exchange.fetch_order('X', 'TKN/BTC').myid == 123
|
assert exchange.fetch_order('X', 'TKN/BTC').myid == 123
|
||||||
@ -2604,10 +2694,15 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog):
|
|||||||
|
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.fetch_order = MagicMock(return_value=456)
|
api_mock.fetch_order = MagicMock(return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'})
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
assert exchange.fetch_order('X', 'TKN/BTC') == 456
|
assert exchange.fetch_order(
|
||||||
assert log_has("API fetch_order: 456", caplog)
|
'X', 'TKN/BTC') == {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}
|
||||||
|
assert log_has(
|
||||||
|
("API fetch_order: {\'id\': \'123\', \'amount\': 2, \'symbol\': \'TKN/BTC\'}"
|
||||||
|
),
|
||||||
|
caplog
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(InvalidOrderException):
|
with pytest.raises(InvalidOrderException):
|
||||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
@ -2651,9 +2746,9 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
|
|||||||
|
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.fetch_order = MagicMock(return_value=456)
|
api_mock.fetch_order = MagicMock(return_value={'id': '123', 'symbol': 'TKN/BTC'})
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == 456
|
assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == {'id': '123', 'symbol': 'TKN/BTC'}
|
||||||
|
|
||||||
with pytest.raises(InvalidOrderException):
|
with pytest.raises(InvalidOrderException):
|
||||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
@ -2700,12 +2795,17 @@ def test_name(default_conf, mocker, exchange_name):
|
|||||||
assert exchange.id == exchange_name
|
assert exchange.id == exchange_name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("trading_mode,amount", [
|
||||||
|
('spot', 0.2340606),
|
||||||
|
('futures', 2.340606),
|
||||||
|
])
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
def test_get_trades_for_order(default_conf, mocker, exchange_name):
|
def test_get_trades_for_order(default_conf, mocker, exchange_name, trading_mode, amount):
|
||||||
|
|
||||||
order_id = 'ABCD-ABCD'
|
order_id = 'ABCD-ABCD'
|
||||||
since = datetime(2018, 5, 5, 0, 0, 0)
|
since = datetime(2018, 5, 5, 0, 0, 0)
|
||||||
default_conf["dry_run"] = False
|
default_conf["dry_run"] = False
|
||||||
|
default_conf["trading_mode"] = trading_mode
|
||||||
|
default_conf["collateral"] = 'isolated'
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
|
|
||||||
@ -2722,22 +2822,24 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name):
|
|||||||
'id': 'ABCD-ABCD'},
|
'id': 'ABCD-ABCD'},
|
||||||
'timestamp': 1519860024438,
|
'timestamp': 1519860024438,
|
||||||
'datetime': '2018-02-28T23:20:24.438Z',
|
'datetime': '2018-02-28T23:20:24.438Z',
|
||||||
'symbol': 'LTC/BTC',
|
'symbol': 'ETH/USDT:USDT',
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
'price': 165.0,
|
'price': 165.0,
|
||||||
'amount': 0.2340606,
|
'amount': 0.2340606,
|
||||||
'fee': {'cost': 0.06179, 'currency': 'BTC'}
|
'fee': {'cost': 0.06179, 'currency': 'BTC'}
|
||||||
}])
|
}])
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
|
||||||
orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
orders = exchange.get_trades_for_order(order_id, 'ETH/USDT:USDT', since)
|
||||||
assert len(orders) == 1
|
assert len(orders) == 1
|
||||||
assert orders[0]['price'] == 165
|
assert orders[0]['price'] == 165
|
||||||
|
assert isclose(orders[0]['amount'], amount)
|
||||||
assert api_mock.fetch_my_trades.call_count == 1
|
assert api_mock.fetch_my_trades.call_count == 1
|
||||||
# since argument should be
|
# since argument should be
|
||||||
assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int)
|
assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int)
|
||||||
assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC'
|
assert api_mock.fetch_my_trades.call_args[0][0] == 'ETH/USDT:USDT'
|
||||||
# Same test twice, hardcoded number and doing the same calculation
|
# Same test twice, hardcoded number and doing the same calculation
|
||||||
assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000
|
assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000
|
||||||
assert api_mock.fetch_my_trades.call_args[0][1] == int(since.replace(
|
assert api_mock.fetch_my_trades.call_args[0][1] == int(since.replace(
|
||||||
@ -2745,10 +2847,10 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name):
|
|||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||||
'get_trades_for_order', 'fetch_my_trades',
|
'get_trades_for_order', 'fetch_my_trades',
|
||||||
order_id=order_id, pair='LTC/BTC', since=since)
|
order_id=order_id, pair='ETH/USDT:USDT', since=since)
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
|
||||||
assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == []
|
assert exchange.get_trades_for_order(order_id, 'ETH/USDT:USDT', since) == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||||
@ -3575,3 +3677,205 @@ def test__calculate_funding_fees_datetime_called(
|
|||||||
time_machine.move_to("2021-09-01 08:00:00 +00:00")
|
time_machine.move_to("2021-09-01 08:00:00 +00:00")
|
||||||
funding_fees = exchange._calculate_funding_fees('ADA/USDT', 30.0, d1)
|
funding_fees = exchange._calculate_funding_fees('ADA/USDT', 30.0, d1)
|
||||||
assert funding_fees == expected_fees
|
assert funding_fees == expected_fees
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('pair,expected_size,trading_mode', [
|
||||||
|
('XLTCUSDT', 1, 'spot'),
|
||||||
|
('LTC/USD', 1, 'futures'),
|
||||||
|
('XLTCUSDT', 0.01, 'futures'),
|
||||||
|
('LTC/ETH', 1, 'futures'),
|
||||||
|
('ETH/USDT:USDT', 10, 'futures')
|
||||||
|
])
|
||||||
|
def test__get_contract_size(mocker, default_conf, pair, expected_size, trading_mode):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
default_conf['trading_mode'] = trading_mode
|
||||||
|
default_conf['collateral'] = 'isolated'
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.markets', {
|
||||||
|
'LTC/USD': {
|
||||||
|
'symbol': 'LTC/USD',
|
||||||
|
'contractSize': None,
|
||||||
|
},
|
||||||
|
'XLTCUSDT': {
|
||||||
|
'symbol': 'XLTCUSDT',
|
||||||
|
'contractSize': 0.01,
|
||||||
|
},
|
||||||
|
'LTC/ETH': {
|
||||||
|
'symbol': 'LTC/ETH',
|
||||||
|
},
|
||||||
|
'ETH/USDT:USDT': {
|
||||||
|
'symbol': 'ETH/USDT:USDT',
|
||||||
|
'contractSize': 10,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
size = exchange._get_contract_size(pair)
|
||||||
|
assert expected_size == size
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('pair,contract_size,trading_mode', [
|
||||||
|
('XLTCUSDT', 1, 'spot'),
|
||||||
|
('LTC/USD', 1, 'futures'),
|
||||||
|
('XLTCUSDT', 0.01, 'futures'),
|
||||||
|
('LTC/ETH', 1, 'futures'),
|
||||||
|
('ETH/USDT:USDT', 10, 'futures'),
|
||||||
|
])
|
||||||
|
def test__order_contracts_to_amount(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
markets,
|
||||||
|
pair,
|
||||||
|
contract_size,
|
||||||
|
trading_mode,
|
||||||
|
):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
default_conf['trading_mode'] = trading_mode
|
||||||
|
default_conf['collateral'] = 'isolated'
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
|
orders = [
|
||||||
|
{
|
||||||
|
'id': '123456320',
|
||||||
|
'clientOrderId': '12345632018',
|
||||||
|
'timestamp': 1640124992000,
|
||||||
|
'datetime': 'Tue 21 Dec 2021 22:16:32 UTC',
|
||||||
|
'lastTradeTimestamp': 1640124911000,
|
||||||
|
'status': 'active',
|
||||||
|
'symbol': pair,
|
||||||
|
'type': 'limit',
|
||||||
|
'timeInForce': 'gtc',
|
||||||
|
'postOnly': None,
|
||||||
|
'side': 'buy',
|
||||||
|
'price': 2.0,
|
||||||
|
'stopPrice': None,
|
||||||
|
'average': None,
|
||||||
|
'amount': 30.0,
|
||||||
|
'cost': 60.0,
|
||||||
|
'filled': None,
|
||||||
|
'remaining': 30.0,
|
||||||
|
'fee': 0.06,
|
||||||
|
'fees': [{
|
||||||
|
'currency': 'USDT',
|
||||||
|
'cost': 0.06,
|
||||||
|
}],
|
||||||
|
'trades': None,
|
||||||
|
'info': {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': '123456380',
|
||||||
|
'clientOrderId': '12345638203',
|
||||||
|
'timestamp': 1640124992000,
|
||||||
|
'datetime': 'Tue 21 Dec 2021 22:16:32 UTC',
|
||||||
|
'lastTradeTimestamp': 1640124911000,
|
||||||
|
'status': 'active',
|
||||||
|
'symbol': pair,
|
||||||
|
'type': 'limit',
|
||||||
|
'timeInForce': 'gtc',
|
||||||
|
'postOnly': None,
|
||||||
|
'side': 'sell',
|
||||||
|
'price': 2.2,
|
||||||
|
'stopPrice': None,
|
||||||
|
'average': None,
|
||||||
|
'amount': 40.0,
|
||||||
|
'cost': 80.0,
|
||||||
|
'filled': None,
|
||||||
|
'remaining': 40.0,
|
||||||
|
'fee': 0.08,
|
||||||
|
'fees': [{
|
||||||
|
'currency': 'USDT',
|
||||||
|
'cost': 0.08,
|
||||||
|
}],
|
||||||
|
'trades': None,
|
||||||
|
'info': {},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
order1 = exchange._order_contracts_to_amount(orders[0])
|
||||||
|
order2 = exchange._order_contracts_to_amount(orders[1])
|
||||||
|
assert order1['amount'] == 30.0 * contract_size
|
||||||
|
assert order2['amount'] == 40.0 * contract_size
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('pair,contract_size,trading_mode', [
|
||||||
|
('XLTCUSDT', 1, 'spot'),
|
||||||
|
('LTC/USD', 1, 'futures'),
|
||||||
|
('XLTCUSDT', 0.01, 'futures'),
|
||||||
|
('LTC/ETH', 1, 'futures'),
|
||||||
|
('ETH/USDT:USDT', 10, 'futures'),
|
||||||
|
])
|
||||||
|
def test__trades_contracts_to_amount(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
markets,
|
||||||
|
pair,
|
||||||
|
contract_size,
|
||||||
|
trading_mode,
|
||||||
|
):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
default_conf['trading_mode'] = trading_mode
|
||||||
|
default_conf['collateral'] = 'isolated'
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.markets', markets)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
|
trades = [
|
||||||
|
{
|
||||||
|
'symbol': pair,
|
||||||
|
'amount': 30.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'symbol': pair,
|
||||||
|
'amount': 40.0,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
new_amount_trades = exchange._trades_contracts_to_amount(trades)
|
||||||
|
assert new_amount_trades[0]['amount'] == 30.0 * contract_size
|
||||||
|
assert new_amount_trades[1]['amount'] == 40.0 * contract_size
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('pair,param_amount,param_size', [
|
||||||
|
('XLTCUSDT', 40, 4000),
|
||||||
|
('LTC/ETH', 30, 30),
|
||||||
|
('LTC/USD', 30, 30),
|
||||||
|
('ETH/USDT:USDT', 10, 1),
|
||||||
|
])
|
||||||
|
def test__amount_to_contracts(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
markets,
|
||||||
|
pair,
|
||||||
|
param_amount,
|
||||||
|
param_size
|
||||||
|
):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
default_conf['trading_mode'] = 'spot'
|
||||||
|
default_conf['collateral'] = 'isolated'
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.markets', {
|
||||||
|
'LTC/USD': {
|
||||||
|
'symbol': 'LTC/USD',
|
||||||
|
'contractSize': None,
|
||||||
|
},
|
||||||
|
'XLTCUSDT': {
|
||||||
|
'symbol': 'XLTCUSDT',
|
||||||
|
'contractSize': 0.01,
|
||||||
|
},
|
||||||
|
'LTC/ETH': {
|
||||||
|
'symbol': 'LTC/ETH',
|
||||||
|
},
|
||||||
|
'ETH/USDT:USDT': {
|
||||||
|
'symbol': 'ETH/USDT:USDT',
|
||||||
|
'contractSize': 10,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
result_size = exchange._amount_to_contracts(pair, param_amount)
|
||||||
|
assert result_size == param_amount
|
||||||
|
result_amount = exchange._contracts_to_amount(pair, param_size)
|
||||||
|
assert result_amount == param_size
|
||||||
|
|
||||||
|
default_conf['trading_mode'] = 'futures'
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
result_size = exchange._amount_to_contracts(pair, param_amount)
|
||||||
|
assert result_size == param_size
|
||||||
|
result_amount = exchange._contracts_to_amount(pair, param_size)
|
||||||
|
assert result_amount == param_amount
|
||||||
|
@ -21,6 +21,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
|
|||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
|
'symbol': 'ETH/BTC',
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
}
|
}
|
||||||
@ -53,6 +54,7 @@ def test_sell_kraken_trading_agreement(default_conf, mocker):
|
|||||||
api_mock.options = {}
|
api_mock.options = {}
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
|
'symbol': 'ETH/BTC',
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user