Merge branch 'feat/short' into futures_pairlist

This commit is contained in:
Matthias
2021-11-15 19:12:36 +01:00
25 changed files with 966 additions and 244 deletions

View File

@@ -1,6 +1,6 @@
""" Bibox exchange subclass """
import logging
from typing import Dict, List
from typing import Dict
from freqtrade.exchange import Exchange
@@ -23,6 +23,6 @@ class Bibox(Exchange):
@property
def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization.
return {"has": {"fetchCurrencies": False}}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
config = {"has": {"fetchCurrencies": False}}
config.update(super()._ccxt_config)
return config

View File

@@ -1,6 +1,7 @@
""" Binance exchange subclass """
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
@@ -27,35 +28,17 @@ class Binance(Exchange):
"trades_pagination": "id",
"trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
"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]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported
# TODO-lev: Uncomment once supported
# (TradingMode.MARGIN, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.ISOLATED)
]
@property
def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization.
if self.trading_mode == TradingMode.MARGIN:
return {
"options": {
"defaultType": "margin"
}
}
elif self.trading_mode == TradingMode.FUTURES:
return {
"options": {
"defaultType": "future"
}
}
else:
return {}
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
@@ -139,8 +122,8 @@ class Binance(Exchange):
@retrier
def fill_leverage_brackets(self):
"""
Assigns property _leverage_brackets to a dictionary of information about the leverage
allowed on each pair
Assigns property _leverage_brackets to a dictionary of information about the leverage
allowed on each pair
"""
if self.trading_mode == TradingMode.FUTURES:
try:
@@ -174,9 +157,9 @@ class Binance(Exchange):
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
"""
Returns the maximum leverage that a pair can be traded at
:param pair: The base/quote currency pair being traded
:nominal_value: The total value of the trade in quote currency (collateral + debt)
Returns the maximum leverage that a pair can be traded at
:param pair: The base/quote currency pair being traded
:nominal_value: The total value of the trade in quote currency (collateral + debt)
"""
if pair not in self._leverage_brackets:
return 1.0
@@ -195,8 +178,8 @@ class Binance(Exchange):
trading_mode: Optional[TradingMode] = None
):
"""
Set's the leverage before making a trade, in order to not
have the same leverage on every trade
Set's the leverage before making a trade, in order to not
have the same leverage on every trade
"""
trading_mode = trading_mode or self.trading_mode
@@ -229,3 +212,11 @@ class Binance(Exchange):
f"{arrow.get(since_ms // 1000).isoformat()}.")
return await super()._async_get_historic_ohlcv(
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)

View File

@@ -21,12 +21,12 @@ class Bybit(Exchange):
_ft_has: Dict = {
"ohlcv_candle_limit": 200,
"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]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported
# TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.ISOLATED)
]

View File

@@ -7,7 +7,7 @@ import http
import inspect
import logging
from copy import deepcopy
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from math import ceil
from typing import Any, Dict, List, Optional, Tuple, Union
@@ -69,13 +69,11 @@ class Exchange:
"trades_pagination_arg": "since",
"l2_limit_range": None,
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
"mark_ohlcv_price": "mark",
"ccxt_futures_name": "swap"
}
_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]] = [
# TradingMode.SPOT always supported and not required in this list
]
@@ -89,6 +87,7 @@ class Exchange:
self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {}
self._leverage_brackets: Dict = {}
self._config.update(config)
@@ -179,7 +178,6 @@ class Exchange:
self.markets_refresh_interval: int = exchange_config.get(
"markets_refresh_interval", 60) * 60
self._leverage_brackets: Dict = {}
if self.trading_mode != TradingMode.SPOT:
self.fill_leverage_brackets()
@@ -234,7 +232,20 @@ class Exchange:
@property
def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization.
return {}
if self.trading_mode == TradingMode.MARGIN:
return {
"options": {
"defaultType": "margin"
}
}
elif self.trading_mode == TradingMode.FUTURES:
return {
"options": {
"defaultType": self._ft_has["ccxt_futures_name"]
}
}
else:
return {}
@property
def name(self) -> str:
@@ -532,10 +543,10 @@ class Exchange:
collateral: Optional[Collateral] # Only None when trading_mode = TradingMode.SPOT
):
"""
Checks if freqtrade can perform trades using the configured
trading mode(Margin, Futures) and Collateral(Cross, Isolated)
Throws OperationalException:
If the trading_mode/collateral type are not supported by freqtrade on this exchange
Checks if freqtrade can perform trades using the configured
trading mode(Margin, Futures) and Collateral(Cross, Isolated)
Throws OperationalException:
If the trading_mode/collateral type are not supported by freqtrade on this exchange
"""
if trading_mode != TradingMode.SPOT and (
(trading_mode, collateral) not in self._supported_trading_mode_collateral_pairs
@@ -1622,18 +1633,18 @@ class Exchange:
until=until, from_id=from_id))
@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
:param pair: (e.g. ADA/USDT)
:param since: The earliest time of consideration for calculating funding fees,
in unix time or as a datetime
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 since: The earliest time of consideration for calculating funding fees,
in unix time or as a datetime
"""
# TODO-lev: Add dry-run handling for this.
if not self.exchange_has("fetchFundingHistory"):
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:
since = int(since.timestamp()) * 1000 # * 1000 for ms
@@ -1654,17 +1665,17 @@ class Exchange:
def fill_leverage_brackets(self):
"""
Assigns property _leverage_brackets to a dictionary of information about the leverage
allowed on each pair
Not used if the exchange has a static max leverage value for the account or each pair
Assigns property _leverage_brackets to a dictionary of information about the leverage
allowed on each pair
Not used if the exchange has a static max leverage value for the account or each pair
"""
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
:param pair: The base/quote currency pair being traded
:nominal_value: The total value of the trade in quote currency (collateral + debt)
Returns the maximum leverage that a pair can be traded at
:param pair: The base/quote currency pair being traded
:param nominal_value: The total value of the trade in quote currency (collateral + debt)
"""
market = self.markets[pair]
if (
@@ -1676,6 +1687,25 @@ class Exchange:
else:
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
def _set_leverage(
self,
@@ -1684,8 +1714,8 @@ class Exchange:
trading_mode: Optional[TradingMode] = None
):
"""
Set's the leverage before making a trade, in order to not
have the same leverage on every trade
Set's the leverage before making a trade, in order to not
have the same leverage on every trade
"""
if self._config['dry_run'] or not self.exchange_has("setLeverage"):
# Some exchanges only support one collateral type
@@ -1701,12 +1731,19 @@ class Exchange:
except ccxt.BaseError as 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
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
:param symbol: base/quote currency pair (e.g. "ADA/USDT")
'''
"""
Set's the margin mode on the exchange to cross or isolated for a specific pair
:param pair: base/quote currency pair (e.g. "ADA/USDT")
"""
if self._config['dry_run'] or not self.exchange_has("setMarginMode"):
# Some exchanges only support one collateral type
return
@@ -1721,6 +1758,150 @@ class Exchange:
except ccxt.BaseError as 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:
return exchange_name in ccxt_exchanges(ccxt_module)

View File

@@ -20,13 +20,14 @@ class Ftx(Exchange):
_ft_has: Dict = {
"stoploss_on_exchange": True,
"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]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: Uncomment once supported
# TODO-lev: Uncomment once supported
# (TradingMode.MARGIN, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.CROSS)
]
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:

View File

@@ -26,33 +26,14 @@ class Gateio(Exchange):
_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]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported
# TODO-lev: Uncomment once supported
# (TradingMode.MARGIN, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.ISOLATED)
]
@property
def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization.
if self.trading_mode == TradingMode.MARGIN:
return {
"options": {
"defaultType": "margin"
}
}
elif self.trading_mode == TradingMode.FUTURES:
return {
"options": {
"defaultType": "swap"
}
}
else:
return {}
def validate_ordertypes(self, order_types: Dict) -> None:
super().validate_ordertypes(order_types)

View File

@@ -1,5 +1,5 @@
import logging
from typing import Dict, List
from typing import Dict
from freqtrade.exchange import Exchange
@@ -21,5 +21,3 @@ class Hitbtc(Exchange):
"ohlcv_candle_limit": 1000,
"ohlcv_params": {"sort": "DESC"}
}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day

View File

@@ -23,12 +23,12 @@ class Kraken(Exchange):
"trades_pagination": "id",
"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]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: No CCXT support
# TODO-lev: Uncomment once supported
# (TradingMode.MARGIN, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.CROSS)
]
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
@@ -146,8 +146,8 @@ class Kraken(Exchange):
trading_mode: Optional[TradingMode] = None
):
"""
Kraken set's the leverage as an option in the order object, so we need to
add it to params
Kraken set's the leverage as an option in the order object, so we need to
add it to params
"""
return
@@ -156,3 +156,29 @@ class Kraken(Exchange):
if leverage > 1.0:
params['leverage'] = leverage
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

View File

@@ -1,6 +1,6 @@
""" Kucoin exchange subclass """
import logging
from typing import Dict, List
from typing import Dict
from freqtrade.exchange import Exchange
@@ -24,5 +24,3 @@ class Kucoin(Exchange):
"order_time_in_force": ['gtc', 'fok', 'ioc'],
"time_in_force_parameter": "timeInForce",
}
funding_fee_times: List[int] = [4, 12, 20] # hours of the day

View File

@@ -17,29 +17,11 @@ class Okex(Exchange):
_ft_has: Dict = {
"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]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported
# TODO-lev: Uncomment once supported
# (TradingMode.MARGIN, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.ISOLATED)
]
@property
def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization.
if self.trading_mode == TradingMode.MARGIN:
return {
"options": {
"defaultType": "margin"
}
}
elif self.trading_mode == TradingMode.FUTURES:
return {
"options": {
"defaultType": "swap"
}
}
else:
return {}