Merge branch 'feat/short' into todo-rpc
This commit is contained in:
commit
f101419a47
@ -1,6 +1,6 @@
|
|||||||
""" Bibox exchange subclass """
|
""" Bibox exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
from typing import Dict
|
||||||
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
@ -26,5 +26,3 @@ class Bibox(Exchange):
|
|||||||
config = {"has": {"fetchCurrencies": False}}
|
config = {"has": {"fetchCurrencies": False}}
|
||||||
config.update(super()._ccxt_config)
|
config.update(super()._ccxt_config)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
""" 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 Dict, List, Optional, Tuple
|
||||||
|
|
||||||
@ -29,8 +30,6 @@ class Binance(Exchange):
|
|||||||
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
||||||
"ccxt_futures_name": "future"
|
"ccxt_futures_name": "future"
|
||||||
}
|
}
|
||||||
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
|
|
||||||
# but the schedule won't check within this timeframe
|
|
||||||
|
|
||||||
_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,3 +210,11 @@ class Binance(Exchange):
|
|||||||
f"{arrow.get(since_ms // 1000).isoformat()}.")
|
f"{arrow.get(since_ms // 1000).isoformat()}.")
|
||||||
return await super()._async_get_historic_ohlcv(
|
return await super()._async_get_historic_ohlcv(
|
||||||
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair)
|
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair)
|
||||||
|
|
||||||
|
def funding_fee_cutoff(self, open_date: datetime):
|
||||||
|
"""
|
||||||
|
# TODO-lev: Double check that gateio, ftx, and kraken don't also have this
|
||||||
|
:param open_date: The open date for a trade
|
||||||
|
:return: The cutoff open time for when a funding fee is charged
|
||||||
|
"""
|
||||||
|
return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15)
|
||||||
|
@ -24,8 +24,6 @@ class Bybit(Exchange):
|
|||||||
"ccxt_futures_name": "linear"
|
"ccxt_futures_name": "linear"
|
||||||
}
|
}
|
||||||
|
|
||||||
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
|
|
||||||
|
|
||||||
_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
|
||||||
# TODO-lev: Uncomment once supported
|
# TODO-lev: Uncomment once supported
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -69,14 +69,11 @@ class Exchange:
|
|||||||
"trades_pagination_arg": "since",
|
"trades_pagination_arg": "since",
|
||||||
"l2_limit_range": None,
|
"l2_limit_range": None,
|
||||||
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
||||||
|
"mark_ohlcv_price": "mark",
|
||||||
"ccxt_futures_name": "swap"
|
"ccxt_futures_name": "swap"
|
||||||
}
|
}
|
||||||
_ft_has: Dict = {}
|
_ft_has: Dict = {}
|
||||||
|
|
||||||
# funding_fee_times is currently unused, but should ideally be used to properly
|
|
||||||
# schedule refresh times
|
|
||||||
funding_fee_times: List[int] = [] # hours of the day
|
|
||||||
|
|
||||||
_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
|
||||||
]
|
]
|
||||||
@ -90,6 +87,7 @@ class Exchange:
|
|||||||
self._api: ccxt.Exchange = None
|
self._api: ccxt.Exchange = None
|
||||||
self._api_async: ccxt_async.Exchange = None
|
self._api_async: ccxt_async.Exchange = None
|
||||||
self._markets: Dict = {}
|
self._markets: Dict = {}
|
||||||
|
self._leverage_brackets: Dict = {}
|
||||||
|
|
||||||
self._config.update(config)
|
self._config.update(config)
|
||||||
|
|
||||||
@ -180,7 +178,6 @@ class Exchange:
|
|||||||
self.markets_refresh_interval: int = exchange_config.get(
|
self.markets_refresh_interval: int = exchange_config.get(
|
||||||
"markets_refresh_interval", 60) * 60
|
"markets_refresh_interval", 60) * 60
|
||||||
|
|
||||||
self._leverage_brackets: Dict = {}
|
|
||||||
if self.trading_mode != TradingMode.SPOT:
|
if self.trading_mode != TradingMode.SPOT:
|
||||||
self.fill_leverage_brackets()
|
self.fill_leverage_brackets()
|
||||||
|
|
||||||
@ -1619,17 +1616,18 @@ class Exchange:
|
|||||||
until=until, from_id=from_id))
|
until=until, from_id=from_id))
|
||||||
|
|
||||||
@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:
|
||||||
"""
|
"""
|
||||||
Returns the sum of all funding fees that were exchanged for a pair within a timeframe
|
Returns the sum of all funding fees that were exchanged for a pair within a timeframe
|
||||||
|
Dry-run handling happens as part of _calculate_funding_fees.
|
||||||
:param pair: (e.g. ADA/USDT)
|
:param pair: (e.g. ADA/USDT)
|
||||||
:param since: The earliest time of consideration for calculating funding fees,
|
:param since: The earliest time of consideration for calculating funding fees,
|
||||||
in unix time or as a datetime
|
in unix time or as a datetime
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.exchange_has("fetchFundingHistory"):
|
if not self.exchange_has("fetchFundingHistory"):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"fetch_funding_history() has not been implemented on ccxt.{self.name}")
|
f"fetch_funding_history() is not available using {self.name}"
|
||||||
|
)
|
||||||
|
|
||||||
if type(since) is datetime:
|
if type(since) is datetime:
|
||||||
since = int(since.timestamp()) * 1000 # * 1000 for ms
|
since = int(since.timestamp()) * 1000 # * 1000 for ms
|
||||||
@ -1672,6 +1670,25 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|
||||||
|
def _get_funding_fee(
|
||||||
|
self,
|
||||||
|
size: float,
|
||||||
|
funding_rate: float,
|
||||||
|
mark_price: float,
|
||||||
|
time_in_ratio: Optional[float] = None
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Calculates a single funding fee
|
||||||
|
:param size: contract size * number of contracts
|
||||||
|
: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:
|
||||||
|
- premium: varies by price difference between the perpetual contract and mark price
|
||||||
|
:param time_in_ratio: Not used by most exchange classes
|
||||||
|
"""
|
||||||
|
nominal_value = mark_price * size
|
||||||
|
return nominal_value * funding_rate
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def _set_leverage(
|
def _set_leverage(
|
||||||
self,
|
self,
|
||||||
@ -1697,11 +1714,18 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
def funding_fee_cutoff(self, open_date: datetime):
|
||||||
|
"""
|
||||||
|
:param open_date: The open date for a trade
|
||||||
|
:return: The cutoff open time for when a funding fee is charged
|
||||||
|
"""
|
||||||
|
return open_date.minute > 0 or open_date.second > 0
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}):
|
def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}):
|
||||||
"""
|
"""
|
||||||
Set's the margin mode on the exchange to cross or isolated for a specific pair
|
Set's the margin mode on the exchange to cross or isolated for a specific pair
|
||||||
:param symbol: base/quote currency pair (e.g. "ADA/USDT")
|
:param pair: base/quote currency pair (e.g. "ADA/USDT")
|
||||||
"""
|
"""
|
||||||
if self._config['dry_run'] or not self.exchange_has("setMarginMode"):
|
if self._config['dry_run'] or not self.exchange_has("setMarginMode"):
|
||||||
# Some exchanges only support one collateral type
|
# Some exchanges only support one collateral type
|
||||||
@ -1717,6 +1741,150 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def _get_mark_price_history(self, pair: str, since: int) -> Dict:
|
||||||
|
"""
|
||||||
|
Get's the mark price history for a pair
|
||||||
|
:param pair: The quote/base pair of the trade
|
||||||
|
:param since: The earliest time to start downloading candles, in ms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
candles = self._api.fetch_ohlcv(
|
||||||
|
pair,
|
||||||
|
timeframe="1h",
|
||||||
|
since=since,
|
||||||
|
params={
|
||||||
|
'price': self._ft_has["mark_ohlcv_price"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
history = {}
|
||||||
|
for candle in candles:
|
||||||
|
d = datetime.fromtimestamp(int(candle[0] / 1000), timezone.utc)
|
||||||
|
# Round down to the nearest hour, in case of a delayed timestamp
|
||||||
|
# The millisecond timestamps can be delayed ~20ms
|
||||||
|
time = timeframe_to_prev_date('1h', d).timestamp() * 1000
|
||||||
|
opening_mark_price = candle[1]
|
||||||
|
history[time] = opening_mark_price
|
||||||
|
return history
|
||||||
|
except ccxt.NotSupported as e:
|
||||||
|
raise OperationalException(
|
||||||
|
f'Exchange {self._api.name} does not support fetching historical '
|
||||||
|
f'mark price candle (OHLCV) data. Message: {e}') from e
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(f'Could not fetch historical mark price candle (OHLCV) data '
|
||||||
|
f'for pair {pair} due to {e.__class__.__name__}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(f'Could not fetch historical mark price candle (OHLCV) data '
|
||||||
|
f'for pair {pair}. Message: {e}') from e
|
||||||
|
|
||||||
|
def _calculate_funding_fees(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
amount: float,
|
||||||
|
open_date: datetime,
|
||||||
|
close_date: Optional[datetime] = None
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
calculates the sum of all funding fees that occurred for a pair during a futures trade
|
||||||
|
Only used during dry-run or if the exchange does not provide a funding_rates endpoint.
|
||||||
|
: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
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.funding_fee_cutoff(open_date):
|
||||||
|
open_date += timedelta(hours=1)
|
||||||
|
|
||||||
|
open_date = timeframe_to_prev_date('1h', open_date)
|
||||||
|
|
||||||
|
fees: float = 0
|
||||||
|
if not close_date:
|
||||||
|
close_date = datetime.now(timezone.utc)
|
||||||
|
open_timestamp = int(open_date.timestamp()) * 1000
|
||||||
|
# close_timestamp = int(close_date.timestamp()) * 1000
|
||||||
|
funding_rate_history = self.get_funding_rate_history(
|
||||||
|
pair,
|
||||||
|
open_timestamp
|
||||||
|
)
|
||||||
|
mark_price_history = self._get_mark_price_history(
|
||||||
|
pair,
|
||||||
|
open_timestamp
|
||||||
|
)
|
||||||
|
for timestamp in funding_rate_history.keys():
|
||||||
|
funding_rate = funding_rate_history[timestamp]
|
||||||
|
if timestamp in mark_price_history:
|
||||||
|
mark_price = mark_price_history[timestamp]
|
||||||
|
fees += self._get_funding_fee(
|
||||||
|
size=amount,
|
||||||
|
mark_price=mark_price,
|
||||||
|
funding_rate=funding_rate
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Mark price for {pair} at timestamp {timestamp} not found in "
|
||||||
|
f"funding_rate_history Funding fee calculation may be incorrect"
|
||||||
|
)
|
||||||
|
|
||||||
|
return fees
|
||||||
|
|
||||||
|
def get_funding_fees(self, pair: str, amount: float, open_date: datetime) -> float:
|
||||||
|
"""
|
||||||
|
Fetch funding fees, either from the exchange (live) or calculates them
|
||||||
|
based on funding rate/mark price history
|
||||||
|
:param pair: The quote/base pair of the trade
|
||||||
|
:param amount: Trade amount
|
||||||
|
:param open_date: Open date of the trade
|
||||||
|
"""
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
if self._config['dry_run']:
|
||||||
|
funding_fees = self._calculate_funding_fees(pair, amount, open_date)
|
||||||
|
else:
|
||||||
|
funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
|
||||||
|
return funding_fees
|
||||||
|
else:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_funding_rate_history(self, pair: str, since: int) -> Dict:
|
||||||
|
"""
|
||||||
|
:param pair: quote/base currency pair
|
||||||
|
:param since: timestamp in ms of the beginning time
|
||||||
|
:param end: timestamp in ms of the end time
|
||||||
|
"""
|
||||||
|
if not self.exchange_has("fetchFundingRateHistory"):
|
||||||
|
raise ExchangeError(
|
||||||
|
f"fetch_funding_rate_history is not available using {self.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO-lev: Gateio has a max limit into the past of 333 days, okex has a limit of 3 months
|
||||||
|
try:
|
||||||
|
funding_history: Dict = {}
|
||||||
|
response = self._api.fetch_funding_rate_history(
|
||||||
|
pair,
|
||||||
|
limit=1000,
|
||||||
|
since=since
|
||||||
|
)
|
||||||
|
for fund in response:
|
||||||
|
d = datetime.fromtimestamp(int(fund['timestamp'] / 1000), timezone.utc)
|
||||||
|
# Round down to the nearest hour, in case of a delayed timestamp
|
||||||
|
# The millisecond timestamps can be delayed ~20ms
|
||||||
|
time = int(timeframe_to_prev_date('1h', d).timestamp() * 1000)
|
||||||
|
|
||||||
|
funding_history[time] = fund['fundingRate']
|
||||||
|
return funding_history
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
@ -20,8 +20,8 @@ class Ftx(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"ohlcv_candle_limit": 1500,
|
"ohlcv_candle_limit": 1500,
|
||||||
|
"mark_ohlcv_price": "index"
|
||||||
}
|
}
|
||||||
funding_fee_times: List[int] = list(range(0, 24))
|
|
||||||
|
|
||||||
_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
|
||||||
|
@ -26,8 +26,6 @@ class Gateio(Exchange):
|
|||||||
|
|
||||||
_headers = {'X-Gate-Channel-Id': 'freqtrade'}
|
_headers = {'X-Gate-Channel-Id': 'freqtrade'}
|
||||||
|
|
||||||
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
|
|
||||||
|
|
||||||
_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
|
||||||
# TODO-lev: Uncomment once supported
|
# TODO-lev: Uncomment once supported
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
from typing import Dict
|
||||||
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
@ -21,5 +21,3 @@ class Hitbtc(Exchange):
|
|||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
"ohlcv_params": {"sort": "DESC"}
|
"ohlcv_params": {"sort": "DESC"}
|
||||||
}
|
}
|
||||||
|
|
||||||
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
|
|
||||||
|
@ -23,7 +23,6 @@ class Kraken(Exchange):
|
|||||||
"trades_pagination": "id",
|
"trades_pagination": "id",
|
||||||
"trades_pagination_arg": "since",
|
"trades_pagination_arg": "since",
|
||||||
}
|
}
|
||||||
funding_fee_times: List[int] = [0, 4, 8, 12, 16, 20] # hours of the day
|
|
||||||
|
|
||||||
_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
|
||||||
@ -157,3 +156,29 @@ class Kraken(Exchange):
|
|||||||
if leverage > 1.0:
|
if leverage > 1.0:
|
||||||
params['leverage'] = leverage
|
params['leverage'] = leverage
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
def _get_funding_fee(
|
||||||
|
self,
|
||||||
|
size: float,
|
||||||
|
funding_rate: float,
|
||||||
|
mark_price: float,
|
||||||
|
time_in_ratio: Optional[float] = None
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
# ! This method will always error when run by Freqtrade because time_in_ratio is never
|
||||||
|
# ! passed to _get_funding_fee. For kraken futures to work in dry run and backtesting
|
||||||
|
# ! functionality must be added that passes the parameter time_in_ratio to
|
||||||
|
# ! _get_funding_fee when using Kraken
|
||||||
|
Calculates a single funding fee
|
||||||
|
:param size: contract size * number of contracts
|
||||||
|
: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:
|
||||||
|
- premium: varies by price difference between the perpetual contract and mark price
|
||||||
|
:param time_in_ratio: time elapsed within funding period without position alteration
|
||||||
|
"""
|
||||||
|
if not time_in_ratio:
|
||||||
|
raise OperationalException(
|
||||||
|
f"time_in_ratio is required for {self.name}._get_funding_fee")
|
||||||
|
nominal_value = mark_price * size
|
||||||
|
return nominal_value * funding_rate * time_in_ratio
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
""" Kucoin exchange subclass """
|
""" Kucoin exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
from typing import Dict
|
||||||
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
@ -24,5 +24,3 @@ class Kucoin(Exchange):
|
|||||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||||
"time_in_force_parameter": "timeInForce",
|
"time_in_force_parameter": "timeInForce",
|
||||||
}
|
}
|
||||||
|
|
||||||
funding_fee_times: List[int] = [4, 12, 20] # hours of the day
|
|
||||||
|
@ -17,7 +17,6 @@ class Okex(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 100,
|
"ohlcv_candle_limit": 100,
|
||||||
}
|
}
|
||||||
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
|
|
||||||
|
|
||||||
_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
|
||||||
|
@ -268,12 +268,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
def update_funding_fees(self):
|
def update_funding_fees(self):
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
for trade in Trade.get_open_trades():
|
trades = Trade.get_open_trades()
|
||||||
funding_fees = self.exchange.get_funding_fees_from_exchange(
|
for trade in trades:
|
||||||
|
funding_fees = self.exchange.get_funding_fees(
|
||||||
trade.pair,
|
trade.pair,
|
||||||
|
trade.amount,
|
||||||
trade.open_date
|
trade.open_date
|
||||||
)
|
)
|
||||||
trade.funding_fees = funding_fees
|
trade.funding_fees = funding_fees
|
||||||
|
else:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
def startup_update_open_orders(self):
|
def startup_update_open_orders(self):
|
||||||
"""
|
"""
|
||||||
@ -703,10 +707,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
open_date = datetime.now(timezone.utc)
|
open_date = datetime.now(timezone.utc)
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
funding_fees = self.exchange.get_funding_fees(pair, amount, open_date)
|
||||||
funding_fees = self.exchange.get_funding_fees_from_exchange(pair, open_date)
|
|
||||||
else:
|
|
||||||
funding_fees = 0.0
|
|
||||||
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
@ -1260,6 +1261,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:param sell_reason: Reason the sell was triggered
|
:param sell_reason: Reason the sell was triggered
|
||||||
:return: True if it succeeds (supported) False (not supported)
|
:return: True if it succeeds (supported) False (not supported)
|
||||||
"""
|
"""
|
||||||
|
trade.funding_fees = self.exchange.get_funding_fees(
|
||||||
|
trade.pair,
|
||||||
|
trade.amount,
|
||||||
|
trade.open_date
|
||||||
|
)
|
||||||
exit_type = 'sell' # TODO-lev: Update to exit
|
exit_type = 'sell' # TODO-lev: Update to exit
|
||||||
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||||
exit_type = 'stoploss'
|
exit_type = 'stoploss'
|
||||||
|
@ -2368,3 +2368,131 @@ def limit_order_open(limit_buy_order_usdt_open, limit_sell_order_usdt_open):
|
|||||||
'buy': limit_buy_order_usdt_open,
|
'buy': limit_buy_order_usdt_open,
|
||||||
'sell': limit_sell_order_usdt_open
|
'sell': limit_sell_order_usdt_open
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def mark_ohlcv():
|
||||||
|
return [
|
||||||
|
[1630454400000, 2.77, 2.77, 2.73, 2.73, 0],
|
||||||
|
[1630458000000, 2.73, 2.76, 2.72, 2.74, 0],
|
||||||
|
[1630461600000, 2.74, 2.76, 2.74, 2.76, 0],
|
||||||
|
[1630465200000, 2.76, 2.76, 2.74, 2.76, 0],
|
||||||
|
[1630468800000, 2.76, 2.77, 2.75, 2.77, 0],
|
||||||
|
[1630472400000, 2.77, 2.79, 2.75, 2.78, 0],
|
||||||
|
[1630476000000, 2.78, 2.80, 2.77, 2.77, 0],
|
||||||
|
[1630479600000, 2.78, 2.79, 2.77, 2.77, 0],
|
||||||
|
[1630483200000, 2.77, 2.79, 2.77, 2.78, 0],
|
||||||
|
[1630486800000, 2.77, 2.84, 2.77, 2.84, 0],
|
||||||
|
[1630490400000, 2.84, 2.85, 2.81, 2.81, 0],
|
||||||
|
[1630494000000, 2.81, 2.83, 2.81, 2.81, 0],
|
||||||
|
[1630497600000, 2.81, 2.84, 2.81, 2.82, 0],
|
||||||
|
[1630501200000, 2.82, 2.83, 2.81, 2.81, 0],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def funding_rate_history_hourly():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": -0.000008,
|
||||||
|
"timestamp": 1630454400000,
|
||||||
|
"datetime": "2021-09-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": -0.000004,
|
||||||
|
"timestamp": 1630458000000,
|
||||||
|
"datetime": "2021-09-01T01:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": 0.000012,
|
||||||
|
"timestamp": 1630461600000,
|
||||||
|
"datetime": "2021-09-01T02:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": -0.000003,
|
||||||
|
"timestamp": 1630465200000,
|
||||||
|
"datetime": "2021-09-01T03:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": -0.000007,
|
||||||
|
"timestamp": 1630468800000,
|
||||||
|
"datetime": "2021-09-01T04:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": 0.000003,
|
||||||
|
"timestamp": 1630472400000,
|
||||||
|
"datetime": "2021-09-01T05:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": 0.000019,
|
||||||
|
"timestamp": 1630476000000,
|
||||||
|
"datetime": "2021-09-01T06:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": 0.000003,
|
||||||
|
"timestamp": 1630479600000,
|
||||||
|
"datetime": "2021-09-01T07:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": -0.000003,
|
||||||
|
"timestamp": 1630483200000,
|
||||||
|
"datetime": "2021-09-01T08:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": 0,
|
||||||
|
"timestamp": 1630486800000,
|
||||||
|
"datetime": "2021-09-01T09:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": 0.000013,
|
||||||
|
"timestamp": 1630490400000,
|
||||||
|
"datetime": "2021-09-01T10:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": 0.000077,
|
||||||
|
"timestamp": 1630494000000,
|
||||||
|
"datetime": "2021-09-01T11:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": 0.000072,
|
||||||
|
"timestamp": 1630497600000,
|
||||||
|
"datetime": "2021-09-01T12:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": 0.000097,
|
||||||
|
"timestamp": 1630501200000,
|
||||||
|
"datetime": "2021-09-01T13:00:00.000Z"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def funding_rate_history_octohourly():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": -0.000008,
|
||||||
|
"timestamp": 1630454400000,
|
||||||
|
"datetime": "2021-09-01T00:00:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ADA/USDT",
|
||||||
|
"fundingRate": -0.000003,
|
||||||
|
"timestamp": 1630483200000,
|
||||||
|
"datetime": "2021-09-01T08:00:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
@ -12,7 +12,7 @@ import pytest
|
|||||||
|
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
from tests.conftest import get_default_conf
|
from tests.conftest import get_default_conf_usdt
|
||||||
|
|
||||||
|
|
||||||
# Exchanges that should be tested
|
# Exchanges that should be tested
|
||||||
@ -33,9 +33,11 @@ EXCHANGES = {
|
|||||||
'timeframe': '5m',
|
'timeframe': '5m',
|
||||||
},
|
},
|
||||||
'ftx': {
|
'ftx': {
|
||||||
'pair': 'BTC/USDT',
|
'pair': 'BTC/USD',
|
||||||
'hasQuoteVolume': True,
|
'hasQuoteVolume': True,
|
||||||
'timeframe': '5m',
|
'timeframe': '5m',
|
||||||
|
'futures_pair': 'BTC-PERP',
|
||||||
|
'futures': True,
|
||||||
},
|
},
|
||||||
'kucoin': {
|
'kucoin': {
|
||||||
'pair': 'BTC/USDT',
|
'pair': 'BTC/USDT',
|
||||||
@ -46,18 +48,24 @@ EXCHANGES = {
|
|||||||
'pair': 'BTC/USDT',
|
'pair': 'BTC/USDT',
|
||||||
'hasQuoteVolume': True,
|
'hasQuoteVolume': True,
|
||||||
'timeframe': '5m',
|
'timeframe': '5m',
|
||||||
|
'futures': True,
|
||||||
|
'futures_fundingrate_tf': '8h',
|
||||||
|
'futures_pair': 'BTC/USDT:USDT',
|
||||||
},
|
},
|
||||||
'okex': {
|
'okex': {
|
||||||
'pair': 'BTC/USDT',
|
'pair': 'BTC/USDT',
|
||||||
'hasQuoteVolume': True,
|
'hasQuoteVolume': True,
|
||||||
'timeframe': '5m',
|
'timeframe': '5m',
|
||||||
|
'futures_fundingrate_tf': '8h',
|
||||||
|
'futures_pair': 'BTC/USDT:USDT',
|
||||||
|
'futures': True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
def exchange_conf():
|
def exchange_conf():
|
||||||
config = get_default_conf((Path(__file__).parent / "testdata").resolve())
|
config = get_default_conf_usdt((Path(__file__).parent / "testdata").resolve())
|
||||||
config['exchange']['pair_whitelist'] = []
|
config['exchange']['pair_whitelist'] = []
|
||||||
config['exchange']['key'] = ''
|
config['exchange']['key'] = ''
|
||||||
config['exchange']['secret'] = ''
|
config['exchange']['secret'] = ''
|
||||||
@ -73,6 +81,19 @@ def exchange(request, exchange_conf):
|
|||||||
yield exchange, request.param
|
yield exchange, request.param
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=EXCHANGES, scope="class")
|
||||||
|
def exchange_futures(request, exchange_conf):
|
||||||
|
if not EXCHANGES[request.param].get('futures') is True:
|
||||||
|
yield None, request.param
|
||||||
|
else:
|
||||||
|
exchange_conf['exchange']['name'] = request.param
|
||||||
|
exchange_conf['trading_mode'] = 'futures'
|
||||||
|
exchange_conf['collateral'] = 'cross'
|
||||||
|
exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True)
|
||||||
|
|
||||||
|
yield exchange, request.param
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.longrun
|
@pytest.mark.longrun
|
||||||
class TestCCXTExchange():
|
class TestCCXTExchange():
|
||||||
|
|
||||||
@ -149,6 +170,25 @@ class TestCCXTExchange():
|
|||||||
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
|
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
|
||||||
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
|
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
|
||||||
|
|
||||||
|
@pytest.mark.skip("No futures support yet")
|
||||||
|
def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
|
||||||
|
# TODO-lev: enable this test once Futures mode is enabled.
|
||||||
|
exchange, exchangename = exchange_futures
|
||||||
|
if not exchange:
|
||||||
|
# exchange_futures only returns values for supported exchanges
|
||||||
|
return
|
||||||
|
|
||||||
|
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||||
|
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
|
||||||
|
|
||||||
|
rate = exchange.get_funding_rate_history(pair, since)
|
||||||
|
assert isinstance(rate, dict)
|
||||||
|
expected_tf = EXCHANGES[exchangename].get('futures_fundingrate_tf', '1h')
|
||||||
|
this_hour = timeframe_to_prev_date(expected_tf)
|
||||||
|
prev_tick = timeframe_to_prev_date(expected_tf, this_hour - timedelta(minutes=1))
|
||||||
|
assert rate[int(this_hour.timestamp() * 1000)] != 0.0
|
||||||
|
assert rate[int(prev_tick.timestamp() * 1000)] != 0.0
|
||||||
|
|
||||||
# TODO: tests fetch_trades (?)
|
# TODO: tests fetch_trades (?)
|
||||||
|
|
||||||
def test_ccxt_get_fee(self, exchange):
|
def test_ccxt_get_fee(self, exchange):
|
||||||
|
@ -2896,6 +2896,8 @@ def test_timeframe_to_prev_date():
|
|||||||
# Does not round
|
# Does not round
|
||||||
time = datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)
|
time = datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)
|
||||||
assert timeframe_to_prev_date('5m', time) == time
|
assert timeframe_to_prev_date('5m', time) == time
|
||||||
|
time = datetime(2019, 8, 12, 13, 0, 0, tzinfo=timezone.utc)
|
||||||
|
assert timeframe_to_prev_date('1h', time) == time
|
||||||
|
|
||||||
|
|
||||||
def test_timeframe_to_next_date():
|
def test_timeframe_to_next_date():
|
||||||
@ -3058,7 +3060,7 @@ def test_calculate_backoff(retrycount, max_retries, expected):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", ['binance', 'ftx'])
|
@pytest.mark.parametrize("exchange_name", ['binance', 'ftx'])
|
||||||
def test_get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
|
def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.fetch_funding_history = MagicMock(return_value=[
|
api_mock.fetch_funding_history = MagicMock(return_value=[
|
||||||
{
|
{
|
||||||
@ -3101,11 +3103,11 @@ def test_get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
|
|||||||
date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ')
|
date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ')
|
||||||
unix_time = int(date_time.timestamp())
|
unix_time = int(date_time.timestamp())
|
||||||
expected_fees = -0.001 # 0.14542341 + -0.14642341
|
expected_fees = -0.001 # 0.14542341 + -0.14642341
|
||||||
fees_from_datetime = exchange.get_funding_fees_from_exchange(
|
fees_from_datetime = exchange._get_funding_fees_from_exchange(
|
||||||
pair='XRP/USDT',
|
pair='XRP/USDT',
|
||||||
since=date_time
|
since=date_time
|
||||||
)
|
)
|
||||||
fees_from_unix_time = exchange.get_funding_fees_from_exchange(
|
fees_from_unix_time = exchange._get_funding_fees_from_exchange(
|
||||||
pair='XRP/USDT',
|
pair='XRP/USDT',
|
||||||
since=unix_time
|
since=unix_time
|
||||||
)
|
)
|
||||||
@ -3118,7 +3120,7 @@ def test_get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
|
|||||||
default_conf,
|
default_conf,
|
||||||
api_mock,
|
api_mock,
|
||||||
exchange_name,
|
exchange_name,
|
||||||
"get_funding_fees_from_exchange",
|
"_get_funding_fees_from_exchange",
|
||||||
"fetch_funding_history",
|
"fetch_funding_history",
|
||||||
pair="XRP/USDT",
|
pair="XRP/USDT",
|
||||||
since=unix_time
|
since=unix_time
|
||||||
@ -3288,3 +3290,226 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'size,funding_rate,mark_price,time_in_ratio,funding_fee,kraken_fee', [
|
||||||
|
(10, 0.0001, 2.0, 1.0, 0.002, 0.002),
|
||||||
|
(10, 0.0002, 2.0, 0.01, 0.004, 0.00004),
|
||||||
|
(10, 0.0002, 2.5, None, 0.005, None),
|
||||||
|
])
|
||||||
|
def test__get_funding_fee(
|
||||||
|
default_conf,
|
||||||
|
mocker,
|
||||||
|
size,
|
||||||
|
funding_rate,
|
||||||
|
mark_price,
|
||||||
|
funding_fee,
|
||||||
|
kraken_fee,
|
||||||
|
time_in_ratio
|
||||||
|
):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
kraken = get_patched_exchange(mocker, default_conf, id="kraken")
|
||||||
|
|
||||||
|
assert exchange._get_funding_fee(size, funding_rate, mark_price, time_in_ratio) == funding_fee
|
||||||
|
|
||||||
|
if (kraken_fee is None):
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
kraken._get_funding_fee(size, funding_rate, mark_price, time_in_ratio)
|
||||||
|
else:
|
||||||
|
assert kraken._get_funding_fee(size, funding_rate, mark_price, time_in_ratio) == kraken_fee
|
||||||
|
|
||||||
|
|
||||||
|
def test__get_mark_price_history(mocker, default_conf, mark_ohlcv):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv)
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||||
|
|
||||||
|
# mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
mark_prices = exchange._get_mark_price_history("ADA/USDT", 1630454400000)
|
||||||
|
assert mark_prices == {
|
||||||
|
1630454400000: 2.77,
|
||||||
|
1630458000000: 2.73,
|
||||||
|
1630461600000: 2.74,
|
||||||
|
1630465200000: 2.76,
|
||||||
|
1630468800000: 2.76,
|
||||||
|
1630472400000: 2.77,
|
||||||
|
1630476000000: 2.78,
|
||||||
|
1630479600000: 2.78,
|
||||||
|
1630483200000: 2.77,
|
||||||
|
1630486800000: 2.77,
|
||||||
|
1630490400000: 2.84,
|
||||||
|
1630494000000: 2.81,
|
||||||
|
1630497600000: 2.81,
|
||||||
|
1630501200000: 2.82,
|
||||||
|
}
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
api_mock,
|
||||||
|
"binance",
|
||||||
|
"_get_mark_price_history",
|
||||||
|
"fetch_ohlcv",
|
||||||
|
pair="ADA/USDT",
|
||||||
|
since=1635580800001
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_funding_rate_history(mocker, default_conf, funding_rate_history_hourly):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history_hourly)
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True})
|
||||||
|
|
||||||
|
# mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
funding_rates = exchange.get_funding_rate_history('ADA/USDT', 1635580800001)
|
||||||
|
|
||||||
|
assert funding_rates == {
|
||||||
|
1630454400000: -0.000008,
|
||||||
|
1630458000000: -0.000004,
|
||||||
|
1630461600000: 0.000012,
|
||||||
|
1630465200000: -0.000003,
|
||||||
|
1630468800000: -0.000007,
|
||||||
|
1630472400000: 0.000003,
|
||||||
|
1630476000000: 0.000019,
|
||||||
|
1630479600000: 0.000003,
|
||||||
|
1630483200000: -0.000003,
|
||||||
|
1630486800000: 0,
|
||||||
|
1630490400000: 0.000013,
|
||||||
|
1630494000000: 0.000077,
|
||||||
|
1630497600000: 0.000072,
|
||||||
|
1630501200000: 0.000097,
|
||||||
|
}
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
api_mock,
|
||||||
|
"binance",
|
||||||
|
"get_funding_rate_history",
|
||||||
|
"fetch_funding_rate_history",
|
||||||
|
pair="ADA/USDT",
|
||||||
|
since=1630454400000
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [
|
||||||
|
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
|
||||||
|
('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
|
||||||
|
('binance', 1, 2, "2021-09-01 01:00:14", "2021-09-01 08:00:00", 30.0, -0.0002493),
|
||||||
|
('binance', 1, 2, "2021-09-01 00:00:16", "2021-09-01 08:00:00", 30.0, -0.0002493),
|
||||||
|
('binance', 0, 1, "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0006647999999999999),
|
||||||
|
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999),
|
||||||
|
('binance', 0, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
|
||||||
|
# TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee
|
||||||
|
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0014937),
|
||||||
|
# ('kraken', "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0008289),
|
||||||
|
# ('kraken', "2021-09-01 01:00:14", "2021-09-01 08:00:00", 30.0, -0.0008289),
|
||||||
|
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0012443999999999999),
|
||||||
|
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0045759),
|
||||||
|
# ('kraken', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0008289),
|
||||||
|
('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, 0.0010008000000000003),
|
||||||
|
('ftx', 0, 13, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0146691),
|
||||||
|
('ftx', 1, 9, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, 0.0016656000000000002),
|
||||||
|
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
|
||||||
|
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999),
|
||||||
|
('gateio', 1, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493),
|
||||||
|
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235000000000001),
|
||||||
|
# TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee
|
||||||
|
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0024895),
|
||||||
|
('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.0016680000000000002),
|
||||||
|
])
|
||||||
|
def test__calculate_funding_fees(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
funding_rate_history_hourly,
|
||||||
|
funding_rate_history_octohourly,
|
||||||
|
rate_start,
|
||||||
|
rate_end,
|
||||||
|
mark_ohlcv,
|
||||||
|
exchange,
|
||||||
|
d1,
|
||||||
|
d2,
|
||||||
|
amount,
|
||||||
|
expected_fees
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
nominal_value = mark_price * size
|
||||||
|
funding_fee = nominal_value * funding_rate
|
||||||
|
size: 30
|
||||||
|
time: 0, mark: 2.77, nominal_value: 83.1, fundRate: -0.000008, fundFee: -0.0006648
|
||||||
|
time: 1, mark: 2.73, nominal_value: 81.9, fundRate: -0.000004, fundFee: -0.0003276
|
||||||
|
time: 2, mark: 2.74, nominal_value: 82.2, fundRate: 0.000012, fundFee: 0.0009864
|
||||||
|
time: 3, mark: 2.76, nominal_value: 82.8, fundRate: -0.000003, fundFee: -0.0002484
|
||||||
|
time: 4, mark: 2.76, nominal_value: 82.8, fundRate: -0.000007, fundFee: -0.0005796
|
||||||
|
time: 5, mark: 2.77, nominal_value: 83.1, fundRate: 0.000003, fundFee: 0.0002493
|
||||||
|
time: 6, mark: 2.78, nominal_value: 83.4, fundRate: 0.000019, fundFee: 0.0015846
|
||||||
|
time: 7, mark: 2.78, nominal_value: 83.4, fundRate: 0.000003, fundFee: 0.0002502
|
||||||
|
time: 8, mark: 2.77, nominal_value: 83.1, fundRate: -0.000003, fundFee: -0.0002493
|
||||||
|
time: 9, mark: 2.77, nominal_value: 83.1, fundRate: 0, fundFee: 0.0
|
||||||
|
time: 10, mark: 2.84, nominal_value: 85.2, fundRate: 0.000013, fundFee: 0.0011076
|
||||||
|
time: 11, mark: 2.81, nominal_value: 84.3, fundRate: 0.000077, fundFee: 0.0064911
|
||||||
|
time: 12, mark: 2.81, nominal_value: 84.3, fundRate: 0.000072, fundFee: 0.0060696
|
||||||
|
time: 13, mark: 2.82, nominal_value: 84.6, fundRate: 0.000097, fundFee: 0.0082062
|
||||||
|
|
||||||
|
size: 50
|
||||||
|
time: 0, mark: 2.77, nominal_value: 138.5, fundRate: -0.000008, fundFee: -0.001108
|
||||||
|
time: 1, mark: 2.73, nominal_value: 136.5, fundRate: -0.000004, fundFee: -0.000546
|
||||||
|
time: 2, mark: 2.74, nominal_value: 137.0, fundRate: 0.000012, fundFee: 0.001644
|
||||||
|
time: 3, mark: 2.76, nominal_value: 138.0, fundRate: -0.000003, fundFee: -0.000414
|
||||||
|
time: 4, mark: 2.76, nominal_value: 138.0, fundRate: -0.000007, fundFee: -0.000966
|
||||||
|
time: 5, mark: 2.77, nominal_value: 138.5, fundRate: 0.000003, fundFee: 0.0004155
|
||||||
|
time: 6, mark: 2.78, nominal_value: 139.0, fundRate: 0.000019, fundFee: 0.002641
|
||||||
|
time: 7, mark: 2.78, nominal_value: 139.0, fundRate: 0.000003, fundFee: 0.000417
|
||||||
|
time: 8, mark: 2.77, nominal_value: 138.5, fundRate: -0.000003, fundFee: -0.0004155
|
||||||
|
time: 9, mark: 2.77, nominal_value: 138.5, fundRate: 0, fundFee: 0.0
|
||||||
|
time: 10, mark: 2.84, nominal_value: 142.0, fundRate: 0.000013, fundFee: 0.001846
|
||||||
|
time: 11, mark: 2.81, nominal_value: 140.5, fundRate: 0.000077, fundFee: 0.0108185
|
||||||
|
time: 12, mark: 2.81, nominal_value: 140.5, fundRate: 0.000072, fundFee: 0.010116
|
||||||
|
time: 13, mark: 2.82, nominal_value: 141.0, fundRate: 0.000097, fundFee: 0.013677
|
||||||
|
'''
|
||||||
|
d1 = datetime.strptime(f"{d1} +0000", '%Y-%m-%d %H:%M:%S %z')
|
||||||
|
d2 = datetime.strptime(f"{d2} +0000", '%Y-%m-%d %H:%M:%S %z')
|
||||||
|
funding_rate_history = {
|
||||||
|
'binance': funding_rate_history_octohourly,
|
||||||
|
'ftx': funding_rate_history_hourly,
|
||||||
|
'gateio': funding_rate_history_octohourly,
|
||||||
|
}[exchange][rate_start:rate_end]
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history)
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv)
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True})
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange)
|
||||||
|
funding_fees = exchange._calculate_funding_fees('ADA/USDT', amount, d1, d2)
|
||||||
|
assert funding_fees == expected_fees
|
||||||
|
|
||||||
|
|
||||||
|
@ pytest.mark.parametrize('exchange,expected_fees', [
|
||||||
|
('binance', -0.0009140999999999999),
|
||||||
|
('gateio', -0.0009140999999999999),
|
||||||
|
])
|
||||||
|
def test__calculate_funding_fees_datetime_called(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
funding_rate_history_octohourly,
|
||||||
|
mark_ohlcv,
|
||||||
|
exchange,
|
||||||
|
time_machine,
|
||||||
|
expected_fees
|
||||||
|
):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv)
|
||||||
|
api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history_octohourly)
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True})
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange)
|
||||||
|
d1 = datetime.strptime("2021-09-01 00:00:00 +0000", '%Y-%m-%d %H:%M:%S %z')
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -4700,8 +4700,8 @@ def test_leverage_prep():
|
|||||||
('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:07"),
|
('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:07"),
|
||||||
('futures', 33, "2021-08-31 23:59:58", "2021-09-01 08:00:07"),
|
('futures', 33, "2021-08-31 23:59:58", "2021-09-01 08:00:07"),
|
||||||
])
|
])
|
||||||
def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_machine,
|
def test_update_funding_fees_schedule(mocker, default_conf, trading_mode, calls, time_machine,
|
||||||
t1, t2):
|
t1, t2):
|
||||||
time_machine.move_to(f"{t1} +00:00")
|
time_machine.move_to(f"{t1} +00:00")
|
||||||
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
@ -4716,3 +4716,159 @@ def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_mac
|
|||||||
freqtrade._schedule.run_pending()
|
freqtrade._schedule.run_pending()
|
||||||
|
|
||||||
assert freqtrade.update_funding_fees.call_count == calls
|
assert freqtrade.update_funding_fees.call_count == calls
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('schedule_off', [False, True])
|
||||||
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
|
def test_update_funding_fees(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
time_machine,
|
||||||
|
fee,
|
||||||
|
ticker_usdt_sell_up,
|
||||||
|
is_short,
|
||||||
|
limit_order_open,
|
||||||
|
schedule_off
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
nominal_value = mark_price * size
|
||||||
|
funding_fee = nominal_value * funding_rate
|
||||||
|
size = 123
|
||||||
|
"LTC/BTC"
|
||||||
|
time: 0, mark: 3.3, fundRate: 0.00032583, nominal_value: 405.9, fundFee: 0.132254397
|
||||||
|
time: 8, mark: 3.2, fundRate: 0.00024472, nominal_value: 393.6, fundFee: 0.096321792
|
||||||
|
"ETH/BTC"
|
||||||
|
time: 0, mark: 2.4, fundRate: 0.0001, nominal_value: 295.2, fundFee: 0.02952
|
||||||
|
time: 8, mark: 2.5, fundRate: 0.0001, nominal_value: 307.5, fundFee: 0.03075
|
||||||
|
"ETC/BTC"
|
||||||
|
time: 0, mark: 4.3, fundRate: 0.00031077, nominal_value: 528.9, fundFee: 0.164366253
|
||||||
|
time: 8, mark: 4.1, fundRate: 0.00022655, nominal_value: 504.3, fundFee: 0.114249165
|
||||||
|
"XRP/BTC"
|
||||||
|
time: 0, mark: 1.2, fundRate: 0.00049426, nominal_value: 147.6, fundFee: 0.072952776
|
||||||
|
time: 8, mark: 1.2, fundRate: 0.00032715, nominal_value: 147.6, fundFee: 0.04828734
|
||||||
|
'''
|
||||||
|
# SETUP
|
||||||
|
time_machine.move_to("2021-09-01 00:00:00 +00:00")
|
||||||
|
|
||||||
|
open_order = limit_order_open[enter_side(is_short)]
|
||||||
|
open_exit_order = limit_order_open[exit_side(is_short)]
|
||||||
|
bid = 0.11
|
||||||
|
enter_rate_mock = MagicMock(return_value=bid)
|
||||||
|
enter_mm = MagicMock(return_value=open_order)
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
default_conf['trading_mode'] = 'futures'
|
||||||
|
default_conf['collateral'] = 'isolated'
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
timestamp_midnight = 1630454400000
|
||||||
|
timestamp_eight = 1630483200000
|
||||||
|
funding_rates_midnight = {
|
||||||
|
"LTC/BTC": {
|
||||||
|
timestamp_midnight: 0.00032583,
|
||||||
|
},
|
||||||
|
"ETH/BTC": {
|
||||||
|
timestamp_midnight: 0.0001,
|
||||||
|
},
|
||||||
|
"XRP/BTC": {
|
||||||
|
timestamp_midnight: 0.00049426,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
funding_rates_eight = {
|
||||||
|
"LTC/BTC": {
|
||||||
|
timestamp_midnight: 0.00032583,
|
||||||
|
timestamp_eight: 0.00024472,
|
||||||
|
},
|
||||||
|
"ETH/BTC": {
|
||||||
|
timestamp_midnight: 0.0001,
|
||||||
|
timestamp_eight: 0.0001,
|
||||||
|
},
|
||||||
|
"XRP/BTC": {
|
||||||
|
timestamp_midnight: 0.00049426,
|
||||||
|
timestamp_eight: 0.00032715,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_prices = {
|
||||||
|
"LTC/BTC": {
|
||||||
|
timestamp_midnight: 3.3,
|
||||||
|
timestamp_eight: 3.2,
|
||||||
|
},
|
||||||
|
"ETH/BTC": {
|
||||||
|
timestamp_midnight: 2.4,
|
||||||
|
timestamp_eight: 2.5,
|
||||||
|
},
|
||||||
|
"XRP/BTC": {
|
||||||
|
timestamp_midnight: 1.2,
|
||||||
|
timestamp_eight: 1.2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange._get_mark_price_history',
|
||||||
|
side_effect=lambda pair, since: mark_prices[pair]
|
||||||
|
)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.get_funding_rate_history',
|
||||||
|
side_effect=lambda pair, since: funding_rates_midnight[pair]
|
||||||
|
)
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_rate=enter_rate_mock,
|
||||||
|
fetch_ticker=MagicMock(return_value={
|
||||||
|
'bid': 1.9,
|
||||||
|
'ask': 2.2,
|
||||||
|
'last': 1.9
|
||||||
|
}),
|
||||||
|
create_order=enter_mm,
|
||||||
|
get_min_pair_stake_amount=MagicMock(return_value=1),
|
||||||
|
get_fee=fee,
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
# initial funding fees,
|
||||||
|
freqtrade.execute_entry('ETH/BTC', 123)
|
||||||
|
freqtrade.execute_entry('LTC/BTC', 2.0)
|
||||||
|
freqtrade.execute_entry('XRP/BTC', 123)
|
||||||
|
|
||||||
|
trades = Trade.get_open_trades()
|
||||||
|
assert len(trades) == 3
|
||||||
|
for trade in trades:
|
||||||
|
assert trade.funding_fees == (
|
||||||
|
trade.amount *
|
||||||
|
mark_prices[trade.pair][timestamp_midnight] *
|
||||||
|
funding_rates_midnight[trade.pair][timestamp_midnight]
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.create_order', return_value=open_exit_order)
|
||||||
|
# create_mock_trades(fee, False)
|
||||||
|
time_machine.move_to("2021-09-01 08:00:00 +00:00")
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.get_funding_rate_history',
|
||||||
|
side_effect=lambda pair, since: funding_rates_eight[pair]
|
||||||
|
)
|
||||||
|
if schedule_off:
|
||||||
|
for trade in trades:
|
||||||
|
assert trade.funding_fees == (
|
||||||
|
trade.amount *
|
||||||
|
mark_prices[trade.pair][timestamp_midnight] *
|
||||||
|
funding_rates_eight[trade.pair][timestamp_midnight]
|
||||||
|
)
|
||||||
|
freqtrade.execute_trade_exit(
|
||||||
|
trade=trade,
|
||||||
|
# The values of the next 2 params are irrelevant for this test
|
||||||
|
limit=ticker_usdt_sell_up()['bid'],
|
||||||
|
sell_reason=SellCheckTuple(sell_type=SellType.ROI)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
freqtrade._schedule.run_pending()
|
||||||
|
|
||||||
|
# Funding fees for 00:00 and 08:00
|
||||||
|
for trade in trades:
|
||||||
|
assert trade.funding_fees == sum([
|
||||||
|
trade.amount *
|
||||||
|
mark_prices[trade.pair][time] *
|
||||||
|
funding_rates_eight[trade.pair][time] for time in mark_prices[trade.pair].keys()
|
||||||
|
])
|
||||||
|
Loading…
Reference in New Issue
Block a user