Added backtesting methods back in
This commit is contained in:
parent
aed22f7dad
commit
2a26c6fbed
@ -1,8 +1,9 @@
|
|||||||
""" Binance exchange subclass """
|
""" Binance exchange subclass """
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
@ -29,7 +30,13 @@ class Binance(Exchange):
|
|||||||
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
||||||
}
|
}
|
||||||
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
|
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
|
||||||
# but the schedule won't check within this timeframe
|
_funding_interest_rates: Dict = {} # TODO-lev: delete
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
|
||||||
|
super().__init__(config, validate)
|
||||||
|
# TODO-lev: Uncomment once lev-exchange merged in
|
||||||
|
# if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
# self._funding_interest_rates = self._get_funding_interest_rates()
|
||||||
|
|
||||||
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
||||||
# TradingMode.SPOT always supported and not required in this list
|
# TradingMode.SPOT always supported and not required in this list
|
||||||
@ -211,6 +218,51 @@ class Binance(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
def _get_premium_index(self, pair: str, date: datetime) -> float:
|
||||||
|
raise OperationalException(f'_get_premium_index has not been implemented on {self.name}')
|
||||||
|
|
||||||
|
def _get_mark_price(self, pair: str, date: datetime) -> float:
|
||||||
|
raise OperationalException(f'_get_mark_price has not been implemented on {self.name}')
|
||||||
|
|
||||||
|
def _get_funding_interest_rates(self):
|
||||||
|
rates = self._api.fetch_funding_rates()
|
||||||
|
interest_rates = {}
|
||||||
|
for pair, data in rates.items():
|
||||||
|
interest_rates[pair] = data['interestRate']
|
||||||
|
return interest_rates
|
||||||
|
|
||||||
|
def _calculate_funding_rate(self, pair: str, premium_index: float) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Get's the funding_rate for a pair at a specific date and time in the past
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
premium_index +
|
||||||
|
max(min(self._funding_interest_rates[pair] - premium_index, 0.0005), -0.0005)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_funding_fee(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
contract_size: float,
|
||||||
|
mark_price: float,
|
||||||
|
premium_index: Optional[float],
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculates a single funding fee
|
||||||
|
:param contract_size: The amount/quanity
|
||||||
|
:param mark_price: The price of the asset that the contract is based off of
|
||||||
|
:param funding_rate: the interest rate and the premium
|
||||||
|
- interest rate: 0.03% daily, BNBUSDT, LINKUSDT, and LTCUSDT are 0%
|
||||||
|
- premium: varies by price difference between the perpetual contract and mark price
|
||||||
|
"""
|
||||||
|
if premium_index is None:
|
||||||
|
raise OperationalException("Premium index cannot be None for Binance._get_funding_fee")
|
||||||
|
nominal_value = mark_price * contract_size
|
||||||
|
funding_rate = self._calculate_funding_rate(pair, premium_index)
|
||||||
|
if funding_rate is None:
|
||||||
|
raise OperationalException("Funding rate should never be none on Binance")
|
||||||
|
return nominal_value * funding_rate
|
||||||
|
|
||||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int, is_new_pair: bool
|
since_ms: int, is_new_pair: bool
|
||||||
) -> List:
|
) -> List:
|
||||||
|
@ -7,7 +7,7 @@ import http
|
|||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
@ -1604,6 +1604,14 @@ class Exchange:
|
|||||||
self._async_get_trade_history(pair=pair, since=since,
|
self._async_get_trade_history(pair=pair, since=since,
|
||||||
until=until, from_id=from_id))
|
until=until, from_id=from_id))
|
||||||
|
|
||||||
|
# https://www.binance.com/en/support/faq/360033525031
|
||||||
|
def fetch_funding_rate(self, pair):
|
||||||
|
if not self.exchange_has("fetchFundingHistory"):
|
||||||
|
raise OperationalException(
|
||||||
|
f"fetch_funding_history() has not been implemented on ccxt.{self.name}")
|
||||||
|
|
||||||
|
return self._api.fetch_funding_rates()
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
|
def get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
|
||||||
"""
|
"""
|
||||||
@ -1659,6 +1667,37 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|
||||||
|
def _get_premium_index(self, pair: str, date: datetime) -> float:
|
||||||
|
raise OperationalException(f'_get_premium_index has not been implemented on {self.name}')
|
||||||
|
|
||||||
|
def _get_mark_price(self, pair: str, date: datetime) -> float:
|
||||||
|
raise OperationalException(f'_get_mark_price has not been implemented on {self.name}')
|
||||||
|
|
||||||
|
def _get_funding_rate(self, pair: str, when: datetime):
|
||||||
|
"""
|
||||||
|
Get's the funding_rate for a pair at a specific date and time in the past
|
||||||
|
"""
|
||||||
|
# TODO-lev: implement
|
||||||
|
raise OperationalException(f"get_funding_rate has not been implemented for {self.name}")
|
||||||
|
|
||||||
|
def _get_funding_fee(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
contract_size: float,
|
||||||
|
mark_price: float,
|
||||||
|
premium_index: Optional[float],
|
||||||
|
# index_price: float,
|
||||||
|
# interest_rate: float)
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculates a single funding fee
|
||||||
|
:param contract_size: The amount/quanity
|
||||||
|
:param mark_price: The price of the asset that the contract is based off of
|
||||||
|
:param funding_rate: the interest rate and the premium
|
||||||
|
- premium: varies by price difference between the perpetual contract and mark price
|
||||||
|
"""
|
||||||
|
raise OperationalException(f"Funding fee has not been implemented for {self.name}")
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def _set_leverage(
|
def _set_leverage(
|
||||||
self,
|
self,
|
||||||
@ -1684,6 +1723,19 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
def _get_funding_fee_dates(self, d1, d2):
|
||||||
|
d1 = datetime(d1.year, d1.month, d1.day, d1.hour)
|
||||||
|
d2 = datetime(d2.year, d2.month, d2.day, d2.hour)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
d3 = d1
|
||||||
|
while d3 < d2:
|
||||||
|
d3 += timedelta(hours=1)
|
||||||
|
if d3.hour in self.funding_fee_times:
|
||||||
|
results.append(d3)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}):
|
def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}):
|
||||||
'''
|
'''
|
||||||
@ -1704,6 +1756,34 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
def calculate_funding_fees(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
amount: float,
|
||||||
|
open_date: datetime,
|
||||||
|
close_date: datetime
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
calculates the sum of all funding fees that occurred for a pair during a futures trade
|
||||||
|
:param pair: The quote/base pair of the trade
|
||||||
|
:param amount: The quantity of the trade
|
||||||
|
:param open_date: The date and time that the trade started
|
||||||
|
:param close_date: The date and time that the trade ended
|
||||||
|
"""
|
||||||
|
|
||||||
|
fees: float = 0
|
||||||
|
for date in self._get_funding_fee_dates(open_date, close_date):
|
||||||
|
premium_index = self._get_premium_index(pair, date)
|
||||||
|
mark_price = self._get_mark_price(pair, date)
|
||||||
|
fees += self._get_funding_fee(
|
||||||
|
pair=pair,
|
||||||
|
contract_size=amount,
|
||||||
|
mark_price=mark_price,
|
||||||
|
premium_index=premium_index
|
||||||
|
)
|
||||||
|
|
||||||
|
return fees
|
||||||
|
|
||||||
|
|
||||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
||||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
""" FTX exchange subclass """
|
""" FTX exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List, Tuple
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
@ -168,3 +169,40 @@ class Ftx(Exchange):
|
|||||||
if order['type'] == 'stop':
|
if order['type'] == 'stop':
|
||||||
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
||||||
return order['id']
|
return order['id']
|
||||||
|
|
||||||
|
def fill_leverage_brackets(self):
|
||||||
|
"""
|
||||||
|
FTX leverage is static across the account, and doesn't change from pair to pair,
|
||||||
|
so _leverage_brackets doesn't need to be set
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
|
||||||
|
"""
|
||||||
|
Returns the maximum leverage that a pair can be traded at, which is always 20 on ftx
|
||||||
|
:param pair: Here for super method, not used on FTX
|
||||||
|
:nominal_value: Here for super method, not used on FTX
|
||||||
|
"""
|
||||||
|
return 20.0
|
||||||
|
|
||||||
|
def _get_funding_rate(self, pair: str, when: datetime) -> Optional[float]:
|
||||||
|
"""FTX doesn't use this"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_funding_fee(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
contract_size: float,
|
||||||
|
mark_price: float,
|
||||||
|
premium_index: Optional[float],
|
||||||
|
# index_price: float,
|
||||||
|
# interest_rate: float)
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculates a single funding fee
|
||||||
|
Always paid in USD on FTX # TODO: How do we account for this
|
||||||
|
: param contract_size: The amount/quanity
|
||||||
|
: param mark_price: The price of the asset that the contract is based off of
|
||||||
|
: param funding_rate: Must be None on ftx
|
||||||
|
"""
|
||||||
|
return (contract_size * mark_price) / 24
|
||||||
|
@ -707,6 +707,7 @@ class LocalTrade():
|
|||||||
return float(self._calc_base_close(amount, rate, fee) - total_interest)
|
return float(self._calc_base_close(amount, rate, fee) - total_interest)
|
||||||
|
|
||||||
elif (trading_mode == TradingMode.FUTURES):
|
elif (trading_mode == TradingMode.FUTURES):
|
||||||
|
self.add_funding_fees()
|
||||||
funding_fees = self.funding_fees or 0.0
|
funding_fees = self.funding_fees or 0.0
|
||||||
if self.is_short:
|
if self.is_short:
|
||||||
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
|
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
|
||||||
@ -788,6 +789,19 @@ class LocalTrade():
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def add_funding_fees(self):
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
# TODO-lev: Calculate this correctly and add it
|
||||||
|
# if self.config['runmode'].value in ('backtest', 'hyperopt'):
|
||||||
|
# self.funding_fees = getattr(Exchange, self.exchange).calculate_funding_fees(
|
||||||
|
# self.exchange,
|
||||||
|
# self.pair,
|
||||||
|
# self.amount,
|
||||||
|
# self.open_date_utc,
|
||||||
|
# self.close_date_utc
|
||||||
|
# )
|
||||||
|
return
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
def get_trades_proxy(*, pair: str = None, is_open: bool = None,
|
||||||
open_date: datetime = None, close_date: datetime = None,
|
open_date: datetime = None, close_date: datetime = None,
|
||||||
|
@ -342,6 +342,14 @@ def test__set_leverage_binance(mocker, default_conf):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_funding_rate():
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def test__get_funding_fee():
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog):
|
async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog):
|
||||||
ohlcv = [
|
ohlcv = [
|
||||||
|
@ -3280,3 +3280,15 @@ def test_get_max_leverage(default_conf, mocker, pair, nominal_value, max_lev):
|
|||||||
# Binance has a different method of getting the max leverage
|
# Binance has a different method of getting the max leverage
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id="kraken")
|
exchange = get_patched_exchange(mocker, default_conf, id="kraken")
|
||||||
assert exchange.get_max_leverage(pair, nominal_value) == max_lev
|
assert exchange.get_max_leverage(pair, nominal_value) == max_lev
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_mark_price():
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_funding_fee_dates():
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_funding_fees():
|
||||||
|
return
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@ -250,3 +251,35 @@ def test_get_order_id(mocker, default_conf):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert exchange.get_order_id_conditional(order) == '1111'
|
assert exchange.get_order_id_conditional(order) == '1111'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('pair,nominal_value,max_lev', [
|
||||||
|
("ADA/BTC", 0.0, 20.0),
|
||||||
|
("BTC/EUR", 100.0, 20.0),
|
||||||
|
("ZEC/USD", 173.31, 20.0),
|
||||||
|
])
|
||||||
|
def test_get_max_leverage_ftx(default_conf, mocker, pair, nominal_value, max_lev):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="ftx")
|
||||||
|
assert exchange.get_max_leverage(pair, nominal_value) == max_lev
|
||||||
|
|
||||||
|
|
||||||
|
def test_fill_leverage_brackets_ftx(default_conf, mocker):
|
||||||
|
# FTX only has one account wide leverage, so there's no leverage brackets
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="ftx")
|
||||||
|
exchange.fill_leverage_brackets()
|
||||||
|
assert exchange._leverage_brackets == {}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("pair,when", [
|
||||||
|
('XRP/USDT', datetime.utcnow()),
|
||||||
|
('ADA/BTC', datetime.utcnow()),
|
||||||
|
('XRP/USDT', datetime.utcnow() - timedelta(hours=30)),
|
||||||
|
])
|
||||||
|
def test__get_funding_rate(default_conf, mocker, pair, when):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="ftx")
|
||||||
|
assert exchange._get_funding_rate(pair, when) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test__get_funding_fee():
|
||||||
|
return
|
||||||
|
Loading…
Reference in New Issue
Block a user