Merge branch 'freqtrade:develop' into develop
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2023.1.dev'
|
||||
__version__ = '2023.2.dev'
|
||||
|
||||
if 'dev' in __version__:
|
||||
from pathlib import Path
|
||||
|
@@ -9,7 +9,7 @@ from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from pandas import DataFrame, to_timedelta
|
||||
from pandas import DataFrame, Timedelta, Timestamp, to_timedelta
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes,
|
||||
@@ -206,9 +206,11 @@ class DataProvider:
|
||||
existing_df, _ = self.__producer_pairs_df[producer_name][pair_key]
|
||||
|
||||
# CHECK FOR MISSING CANDLES
|
||||
timeframe_delta = to_timedelta(timeframe) # Convert the timeframe to a timedelta for pandas
|
||||
local_last = existing_df.iloc[-1]['date'] # We want the last date from our copy
|
||||
incoming_first = dataframe.iloc[0]['date'] # We want the first date from the incoming
|
||||
# Convert the timeframe to a timedelta for pandas
|
||||
timeframe_delta: Timedelta = to_timedelta(timeframe)
|
||||
local_last: Timestamp = existing_df.iloc[-1]['date'] # We want the last date from our copy
|
||||
# We want the first date from the incoming
|
||||
incoming_first: Timestamp = dataframe.iloc[0]['date']
|
||||
|
||||
# Remove existing candles that are newer than the incoming first candle
|
||||
existing_df1 = existing_df[existing_df['date'] < incoming_first]
|
||||
@@ -221,7 +223,7 @@ class DataProvider:
|
||||
# we missed some candles between our data and the incoming
|
||||
# so return False and candle_difference.
|
||||
if candle_difference > 1:
|
||||
return (False, candle_difference)
|
||||
return (False, int(candle_difference))
|
||||
if existing_df1.empty:
|
||||
appended_df = dataframe
|
||||
else:
|
||||
|
@@ -308,7 +308,7 @@ class IDataHandler(ABC):
|
||||
timerange=timerange_startup,
|
||||
candle_type=candle_type
|
||||
)
|
||||
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True):
|
||||
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
|
||||
return pairdf
|
||||
else:
|
||||
enddate = pairdf.iloc[-1]['date']
|
||||
@@ -316,7 +316,7 @@ class IDataHandler(ABC):
|
||||
if timerange_startup:
|
||||
self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup)
|
||||
pairdf = trim_dataframe(pairdf, timerange_startup)
|
||||
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
|
||||
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True):
|
||||
return pairdf
|
||||
|
||||
# incomplete candles should only be dropped if we didn't trim the end beforehand.
|
||||
|
@@ -28,7 +28,6 @@ class Binance(Exchange):
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "fromId",
|
||||
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
||||
"ccxt_futures_name": "swap"
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"stoploss_order_types": {"limit": "stop", "market": "stop_market"},
|
||||
@@ -78,7 +77,9 @@ class Binance(Exchange):
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
|
||||
) from e
|
||||
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@@ -87,7 +88,8 @@ class Binance(Exchange):
|
||||
self,
|
||||
leverage: float,
|
||||
pair: Optional[str] = None,
|
||||
trading_mode: Optional[TradingMode] = None
|
||||
trading_mode: Optional[TradingMode] = None,
|
||||
accept_fail: bool = False,
|
||||
):
|
||||
"""
|
||||
Set's the leverage before making a trade, in order to not
|
||||
@@ -150,6 +152,7 @@ class Binance(Exchange):
|
||||
is_short: bool,
|
||||
amount: float,
|
||||
stake_amount: float,
|
||||
leverage: float,
|
||||
wallet_balance: float, # Or margin balance
|
||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
||||
@@ -159,11 +162,12 @@ class Binance(Exchange):
|
||||
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
|
||||
PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
|
||||
|
||||
:param exchange_name:
|
||||
:param pair: Pair to calculate liquidation price for
|
||||
:param open_rate: Entry price of position
|
||||
:param is_short: True if the trade is a short, false otherwise
|
||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||
:param leverage: Leverage used for this position.
|
||||
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||
:param margin_mode: Either ISOLATED or CROSS
|
||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||
|
@@ -1,9 +1,16 @@
|
||||
""" Bybit exchange subclass """
|
||||
import logging
|
||||
from typing import Dict, List, Tuple
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.constants import BuySell
|
||||
from freqtrade.enums import MarginMode, TradingMode
|
||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
from freqtrade.exchange.exchange_utils import timeframe_to_msecs
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,17 +28,20 @@ class Bybit(Exchange):
|
||||
|
||||
_ft_has: Dict = {
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"ccxt_futures_name": "linear",
|
||||
"ohlcv_has_history": False,
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"ohlcv_candle_limit": 200,
|
||||
"ohlcv_has_history": True,
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
}
|
||||
|
||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||
# TradingMode.SPOT always supported and not required in this list
|
||||
# (TradingMode.FUTURES, MarginMode.CROSS),
|
||||
# (TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||
(TradingMode.FUTURES, MarginMode.ISOLATED)
|
||||
]
|
||||
|
||||
@property
|
||||
@@ -47,3 +57,158 @@ class Bybit(Exchange):
|
||||
})
|
||||
config.update(super()._ccxt_config)
|
||||
return config
|
||||
|
||||
def market_is_future(self, market: Dict[str, Any]) -> bool:
|
||||
main = super().market_is_future(market)
|
||||
# For ByBit, we'll only support USDT markets for now.
|
||||
return (
|
||||
main and market['settle'] == 'USDT'
|
||||
)
|
||||
|
||||
@retrier
|
||||
def additional_exchange_init(self) -> None:
|
||||
"""
|
||||
Additional exchange initialization logic.
|
||||
.api will be available at this point.
|
||||
Must be overridden in child methods if required.
|
||||
"""
|
||||
try:
|
||||
if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']:
|
||||
position_mode = self._api.set_position_mode(False)
|
||||
self._log_exchange_response('set_position_mode', position_mode)
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}'
|
||||
) from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
async def _fetch_funding_rate_history(
|
||||
self,
|
||||
pair: str,
|
||||
timeframe: str,
|
||||
limit: int,
|
||||
since_ms: Optional[int] = None,
|
||||
) -> List[List]:
|
||||
"""
|
||||
Fetch funding rate history
|
||||
Necessary workaround until https://github.com/ccxt/ccxt/issues/15990 is fixed.
|
||||
"""
|
||||
params = {}
|
||||
if since_ms:
|
||||
until = since_ms + (timeframe_to_msecs(timeframe) * self._ft_has['ohlcv_candle_limit'])
|
||||
params.update({'until': until})
|
||||
# Funding rate
|
||||
data = await self._api_async.fetch_funding_rate_history(
|
||||
pair, since=since_ms,
|
||||
params=params)
|
||||
# Convert funding rate to candle pattern
|
||||
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
|
||||
return data
|
||||
|
||||
def _lev_prep(self, pair: str, leverage: float, side: BuySell):
|
||||
if self.trading_mode != TradingMode.SPOT:
|
||||
params = {'leverage': leverage}
|
||||
self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
|
||||
self._set_leverage(leverage, pair, accept_fail=True)
|
||||
|
||||
def _get_params(
|
||||
self,
|
||||
side: BuySell,
|
||||
ordertype: str,
|
||||
leverage: float,
|
||||
reduceOnly: bool,
|
||||
time_in_force: str = 'GTC',
|
||||
) -> Dict:
|
||||
params = super()._get_params(
|
||||
side=side,
|
||||
ordertype=ordertype,
|
||||
leverage=leverage,
|
||||
reduceOnly=reduceOnly,
|
||||
time_in_force=time_in_force,
|
||||
)
|
||||
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
|
||||
params['position_idx'] = 0
|
||||
return params
|
||||
|
||||
def dry_run_liquidation_price(
|
||||
self,
|
||||
pair: str,
|
||||
open_rate: float, # Entry price of position
|
||||
is_short: bool,
|
||||
amount: float,
|
||||
stake_amount: float,
|
||||
leverage: float,
|
||||
wallet_balance: float, # Or margin balance
|
||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
||||
) -> Optional[float]:
|
||||
"""
|
||||
Important: Must be fetching data from cached values as this is used by backtesting!
|
||||
PERPETUAL:
|
||||
bybit:
|
||||
https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067
|
||||
|
||||
Long:
|
||||
Liquidation Price = (
|
||||
Entry Price * (1 - Initial Margin Rate + Maintenance Margin Rate)
|
||||
- Extra Margin Added/ Contract)
|
||||
Short:
|
||||
Liquidation Price = (
|
||||
Entry Price * (1 + Initial Margin Rate - Maintenance Margin Rate)
|
||||
+ Extra Margin Added/ Contract)
|
||||
|
||||
Implementation Note: Extra margin is currently not used.
|
||||
|
||||
:param pair: Pair to calculate liquidation price for
|
||||
:param open_rate: Entry price of position
|
||||
:param is_short: True if the trade is a short, false otherwise
|
||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||
:param leverage: Leverage used for this position.
|
||||
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||
:param margin_mode: Either ISOLATED or CROSS
|
||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||
Cross-Margin Mode: crossWalletBalance
|
||||
Isolated-Margin Mode: isolatedWalletBalance
|
||||
"""
|
||||
|
||||
market = self.markets[pair]
|
||||
mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
|
||||
|
||||
if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
|
||||
|
||||
if market['inverse']:
|
||||
raise OperationalException(
|
||||
"Freqtrade does not yet support inverse contracts")
|
||||
initial_margin_rate = 1 / leverage
|
||||
|
||||
# See docstring - ignores extra margin!
|
||||
if is_short:
|
||||
return open_rate * (1 + initial_margin_rate - mm_ratio)
|
||||
else:
|
||||
return open_rate * (1 - initial_margin_rate + mm_ratio)
|
||||
|
||||
else:
|
||||
raise OperationalException(
|
||||
"Freqtrade only supports isolated futures for leverage trading")
|
||||
|
||||
def get_funding_fees(
|
||||
self, pair: str, amount: float, is_short: bool, 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 is_short: trade direction
|
||||
:param amount: Trade amount
|
||||
:param open_date: Open date of the trade
|
||||
:return: funding fee since open_date
|
||||
:raises: ExchangeError if something goes wrong.
|
||||
"""
|
||||
# Bybit does not provide "applied" funding fees per position.
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
return self._fetch_and_calculate_funding_fees(
|
||||
pair, amount, is_short, open_date)
|
||||
return 0.0
|
||||
|
@@ -2484,7 +2484,8 @@ class Exchange:
|
||||
self,
|
||||
leverage: float,
|
||||
pair: Optional[str] = None,
|
||||
trading_mode: Optional[TradingMode] = None
|
||||
trading_mode: Optional[TradingMode] = None,
|
||||
accept_fail: bool = False,
|
||||
):
|
||||
"""
|
||||
Set's the leverage before making a trade, in order to not
|
||||
@@ -2499,6 +2500,10 @@ class Exchange:
|
||||
self._log_exchange_response('set_leverage', res)
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except ccxt.BadRequest as e:
|
||||
if not accept_fail:
|
||||
raise TemporaryError(
|
||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@@ -2520,7 +2525,8 @@ class Exchange:
|
||||
return open_date.minute > 0 or open_date.second > 0
|
||||
|
||||
@retrier
|
||||
def set_margin_mode(self, pair: str, margin_mode: MarginMode, params: dict = {}):
|
||||
def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False,
|
||||
params: dict = {}):
|
||||
"""
|
||||
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")
|
||||
@@ -2534,6 +2540,10 @@ class Exchange:
|
||||
self._log_exchange_response('set_margin_mode', res)
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except ccxt.BadRequest as e:
|
||||
if not accept_fail:
|
||||
raise TemporaryError(
|
||||
f'Could not set margin mode due to {e.__class__.__name__}. Message: {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
|
||||
@@ -2687,6 +2697,7 @@ class Exchange:
|
||||
is_short: bool,
|
||||
amount: float, # Absolute value of position size
|
||||
stake_amount: float,
|
||||
leverage: float,
|
||||
wallet_balance: float,
|
||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
||||
@@ -2708,6 +2719,7 @@ class Exchange:
|
||||
open_rate=open_rate,
|
||||
is_short=is_short,
|
||||
amount=amount,
|
||||
leverage=leverage,
|
||||
stake_amount=stake_amount,
|
||||
wallet_balance=wallet_balance,
|
||||
mm_ex_1=mm_ex_1,
|
||||
@@ -2737,6 +2749,7 @@ class Exchange:
|
||||
is_short: bool,
|
||||
amount: float,
|
||||
stake_amount: float,
|
||||
leverage: float,
|
||||
wallet_balance: float, # Or margin balance
|
||||
mm_ex_1: float = 0.0, # (Binance) Cross only
|
||||
upnl_ex_1: float = 0.0, # (Binance) Cross only
|
||||
@@ -2758,6 +2771,7 @@ class Exchange:
|
||||
:param is_short: True if the trade is a short, false otherwise
|
||||
:param amount: Absolute value of position size incl. leverage (in base currency)
|
||||
:param stake_amount: Stake amount - Collateral in settle currency.
|
||||
:param leverage: Leverage used for this position.
|
||||
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
|
||||
:param margin_mode: Either ISOLATED or CROSS
|
||||
:param wallet_balance: Amount of margin_mode in the wallet being used to trade
|
||||
|
@@ -158,7 +158,8 @@ class Kraken(Exchange):
|
||||
self,
|
||||
leverage: float,
|
||||
pair: Optional[str] = None,
|
||||
trading_mode: Optional[TradingMode] = None
|
||||
trading_mode: Optional[TradingMode] = None,
|
||||
accept_fail: bool = False,
|
||||
):
|
||||
"""
|
||||
Kraken set's the leverage as an option in the order object, so we need to
|
||||
|
@@ -1789,6 +1789,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
is_short=trade.is_short,
|
||||
amount=trade.amount,
|
||||
stake_amount=trade.stake_amount,
|
||||
leverage=trade.leverage,
|
||||
wallet_balance=trade.stake_amount,
|
||||
))
|
||||
|
||||
|
@@ -775,6 +775,11 @@ class Backtesting:
|
||||
trade: Optional[LocalTrade] = None,
|
||||
requested_rate: Optional[float] = None,
|
||||
requested_stake: Optional[float] = None) -> Optional[LocalTrade]:
|
||||
"""
|
||||
:param trade: Trade to adjust - initial entry if None
|
||||
:param requested_rate: Adjusted entry rate
|
||||
:param requested_stake: Stake amount for adjusted orders (`adjust_entry_price`).
|
||||
"""
|
||||
|
||||
current_time = row[DATE_IDX].to_pydatetime()
|
||||
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
|
||||
@@ -800,7 +805,7 @@ class Backtesting:
|
||||
return trade
|
||||
time_in_force = self.strategy.order_time_in_force['entry']
|
||||
|
||||
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
||||
if stake_amount and (not min_stake_amount or stake_amount >= min_stake_amount):
|
||||
self.order_id_counter += 1
|
||||
base_currency = self.exchange.get_pair_base_currency(pair)
|
||||
amount_p = (stake_amount / propose_rate) * leverage
|
||||
@@ -863,6 +868,7 @@ class Backtesting:
|
||||
open_rate=propose_rate,
|
||||
amount=amount,
|
||||
stake_amount=trade.stake_amount,
|
||||
leverage=trade.leverage,
|
||||
wallet_balance=trade.stake_amount,
|
||||
is_short=is_short,
|
||||
))
|
||||
|
@@ -44,7 +44,7 @@ class SharpeHyperOptLossDaily(IHyperOptLoss):
|
||||
|
||||
sum_daily = (
|
||||
results.resample(resample_freq, on='close_date').agg(
|
||||
{"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0)
|
||||
{"profit_ratio_after_slippage": 'sum'}).reindex(t_index).fillna(0)
|
||||
)
|
||||
|
||||
total_profit = sum_daily["profit_ratio_after_slippage"] - risk_free_rate
|
||||
|
@@ -46,7 +46,7 @@ class SortinoHyperOptLossDaily(IHyperOptLoss):
|
||||
|
||||
sum_daily = (
|
||||
results.resample(resample_freq, on='close_date').agg(
|
||||
{"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0)
|
||||
{"profit_ratio_after_slippage": 'sum'}).reindex(t_index).fillna(0)
|
||||
)
|
||||
|
||||
total_profit = sum_daily["profit_ratio_after_slippage"] - minimum_acceptable_return
|
||||
|
@@ -90,7 +90,7 @@ async def _process_consumer_request(
|
||||
|
||||
elif type == RPCRequestType.ANALYZED_DF:
|
||||
# Limit the amount of candles per dataframe to 'limit' or 1500
|
||||
limit = min(data.get('limit', 1500), 1500) if data else None
|
||||
limit = int(min(data.get('limit', 1500), 1500)) if data else None
|
||||
pair = data.get('pair', None) if data else None
|
||||
|
||||
# For every pair in the generator, send a separate message
|
||||
|
Reference in New Issue
Block a user