Merge pull request #5888 from samgermain/contract-sizes

Convert contract size to underlying asset size
This commit is contained in:
Matthias 2022-01-03 21:55:19 +01:00 committed by GitHub
commit e57c2d64a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 459 additions and 37 deletions

View File

@ -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(

View File

@ -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': {}
}
} }

View File

@ -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)
@ -3549,7 +3651,7 @@ def test__calculate_funding_fees(
assert pytest.approx(funding_fees) == expected_fees assert pytest.approx(funding_fees) == expected_fees
@ pytest.mark.parametrize('exchange,expected_fees', [ @pytest.mark.parametrize('exchange,expected_fees', [
('binance', -0.0009140999999999999), ('binance', -0.0009140999999999999),
('gateio', -0.0009140999999999999), ('gateio', -0.0009140999999999999),
]) ])
@ -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

View File

@ -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'
} }