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
4 changed files with 459 additions and 37 deletions

View File

@@ -914,6 +914,7 @@ def get_markets():
'active': True,
'spot': True,
'type': 'spot',
'contractSize': None,
'precision': {
'amount': 8,
'price': 8
@@ -937,7 +938,8 @@ def get_markets():
'quote': 'USDT',
'active': True,
'spot': False,
'type': 'SomethingElse',
'type': 'swap',
'contractSize': 0.01,
'precision': {
'amount': 8,
'price': 8
@@ -985,6 +987,59 @@ def get_markets():
'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)
@pytest.mark.parametrize("amount,precision_mode,precision,expected", [
(2.34559, 2, 4, 2.3455),
(2.34559, 2, 5, 2.34559),
(2.34559, 2, 3, 2.345),
(2.9999, 2, 3, 2.999),
(2.9909, 2, 3, 2.990),
@pytest.mark.parametrize("amount,precision_mode,precision,contract_size,expected,trading_mode", [
(2.34559, 2, 4, 1, 2.3455, 'spot'),
(2.34559, 2, 5, 1, 2.34559, 'spot'),
(2.34559, 2, 3, 1, 2.345, 'spot'),
(2.9999, 2, 3, 1, 2.999, 'spot'),
(2.9909, 2, 3, 1, 2.990, 'spot'),
# Tests for Tick-size
(2.34559, 4, 0.0001, 2.3455),
(2.34559, 4, 0.00001, 2.34559),
(2.34559, 4, 0.001, 2.345),
(2.9999, 4, 0.001, 2.999),
(2.9909, 4, 0.001, 2.990),
(2.9909, 4, 0.005, 2.990),
(2.9999, 4, 0.005, 2.995),
(2.34559, 4, 0.0001, 1, 2.3455, 'spot'),
(2.34559, 4, 0.00001, 1, 2.34559, 'spot'),
(2.34559, 4, 0.001, 1, 2.345, 'spot'),
(2.9999, 4, 0.001, 1, 2.999, 'spot'),
(2.9909, 4, 0.001, 1, 2.990, 'spot'),
(2.9909, 4, 0.005, 0.01, 299.09, 'futures'),
(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
"""
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")
# 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)
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:
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["type"] == "limit"
assert order["symbol"] == "ETH/BTC"
assert order["amount"] == 1
@pytest.mark.parametrize("side,startprice,endprice", [
@@ -1127,9 +1169,12 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
'id': order_id,
'info': {
'foo': 'bar'
}
},
'symbol': 'XLTCUSDT',
'amount': 1
})
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.price_to_precision', lambda s, x, y: y)
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()
order = exchange.create_order(
pair='ETH/BTC',
pair='XLTCUSDT',
ordertype=ordertype,
side=side,
amount=1,
@@ -1148,7 +1193,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
assert 'id' in order
assert 'info' in order
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][2] == side
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
order = exchange.create_order(
pair='ETH/BTC',
pair='XLTCUSDT',
ordertype=ordertype,
side=side,
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_margin_mode.call_count == 1
assert order['amount'] == 0.01
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.create_order = MagicMock(return_value={
'id': order_id,
'symbol': 'ETH/BTC',
'info': {
'foo': 'bar'
}
@@ -1264,6 +1312,7 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'symbol': 'ETH/BTC',
'info': {
'foo': 'bar'
}
@@ -1326,6 +1375,7 @@ def test_sell_prod(default_conf, mocker, exchange_name):
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'symbol': 'ETH/BTC',
'info': {
'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))
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'symbol': 'ETH/BTC',
'info': {
'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)
async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name,
fetch_trades_result):
# TODO-lev: Test for contract sizes of 0.01 and 10
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
# 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)
@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.parametrize("exchange_name", EXCHANGES)
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
order = MagicMock()
order.myid = 123
order.symbol = 'TKN/BTC'
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange._dry_run_open_orders['X'] = order
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
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)
assert exchange.fetch_order('X', 'TKN/BTC') == 456
assert log_has("API fetch_order: 456", caplog)
assert exchange.fetch_order(
'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):
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
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)
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):
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
@pytest.mark.parametrize("trading_mode,amount", [
('spot', 0.2340606),
('futures', 2.340606),
])
@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'
since = datetime(2018, 5, 5, 0, 0, 0)
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)
api_mock = MagicMock()
@@ -2722,22 +2822,24 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name):
'id': 'ABCD-ABCD'},
'timestamp': 1519860024438,
'datetime': '2018-02-28T23:20:24.438Z',
'symbol': 'LTC/BTC',
'symbol': 'ETH/USDT:USDT',
'type': 'limit',
'side': 'buy',
'price': 165.0,
'amount': 0.2340606,
'fee': {'cost': 0.06179, 'currency': 'BTC'}
}])
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 orders[0]['price'] == 165
assert isclose(orders[0]['amount'], amount)
assert api_mock.fetch_my_trades.call_count == 1
# since argument should be
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
assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000
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,
'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))
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)
@@ -3549,7 +3651,7 @@ def test__calculate_funding_fees(
assert pytest.approx(funding_fees) == expected_fees
@ pytest.mark.parametrize('exchange,expected_fees', [
@pytest.mark.parametrize('exchange,expected_fees', [
('binance', -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")
funding_fees = exchange._calculate_funding_fees('ADA/USDT', 30.0, d1)
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.create_order = MagicMock(return_value={
'id': order_id,
'symbol': 'ETH/BTC',
'info': {
'foo': 'bar'
}
@@ -53,6 +54,7 @@ def test_sell_kraken_trading_agreement(default_conf, mocker):
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'symbol': 'ETH/BTC',
'info': {
'foo': 'bar'
}