diff --git a/README.md b/README.md index 2ab62793d..232326ba1 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even - [X] [Binance](https://www.binance.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [OKX](https://okx.com/) +- [X] [Bybit](https://bybit.com/) Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in. diff --git a/build_helpers/pyarrow-10.0.0-cp39-cp39-linux_armv7l.whl b/build_helpers/pyarrow-11.0.0-cp39-cp39-linux_armv7l.whl similarity index 62% rename from build_helpers/pyarrow-10.0.0-cp39-cp39-linux_armv7l.whl rename to build_helpers/pyarrow-11.0.0-cp39-cp39-linux_armv7l.whl index a6c879cf5..a7ad80bdf 100644 Binary files a/build_helpers/pyarrow-10.0.0-cp39-cp39-linux_armv7l.whl and b/build_helpers/pyarrow-11.0.0-cp39-cp39-linux_armv7l.whl differ diff --git a/docs/exchanges.md b/docs/exchanges.md index 48b14c470..5ceeccb19 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -255,6 +255,18 @@ OKX requires a passphrase for each api key, you will therefore need to add this Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0). The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value. +## Bybit + +Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode. +Users with unified accounts (there's no way back) can create a Sub-account which will start as "non-unified", and can therefore use isolated futures. +On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors. + +As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well. + +!!! Tip "Stoploss on Exchange" + Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange. + On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use. + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. diff --git a/docs/index.md b/docs/index.md index 40b9e98ad..c24d1f36b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,6 +52,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [Binance](https://www.binance.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [OKX](https://okx.com/) +- [X] [Bybit](https://bybit.com/) Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in. diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 774f40114..a2a947b50 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.0.5 +mkdocs-material==9.0.8 mdx_truly_sane_lists==1.3 pymdown-extensions==9.9.1 jinja2==3.1.2 diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f55cda5e2..cbb71e810 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -80,7 +80,7 @@ class AwesomeStrategy(IStrategy): ## Enter Tag When your strategy has multiple buy signals, you can name the signal that triggered. -Then you can access you buy signal on `custom_exit` +Then you can access your buy signal on `custom_exit` ```python def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 18b6c9130..db339bea3 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2023.1.dev' +__version__ = '2023.2.dev' if 'dev' in __version__: from pathlib import Path diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 3ebfd809c..60a058952 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -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: diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 7626198dc..6637663ff 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -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. diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d85b2fb28..22dfdc1d1 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -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 diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index d14c7c192..55bfbd232 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -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 diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f0bcee702..9b0419cd6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -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 diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 5d8c1ad29..2b37c45bd 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -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 diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 25ae5002a..a558f7bf4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -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, )) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 01138d79c..065a88f40 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -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, )) diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py index 9520123ee..88c97989a 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py @@ -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 diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py index fac96664d..f5fe4590e 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py @@ -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 diff --git a/freqtrade/rpc/api_server/api_ws.py b/freqtrade/rpc/api_server/api_ws.py index 18714f15f..b253d66c2 100644 --- a/freqtrade/rpc/api_server/api_ws.py +++ b/freqtrade/rpc/api_server/api_ws.py @@ -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 diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index db8d8d169..82ff4022d 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -3,7 +3,7 @@ # Required for freqai-rl torch==1.13.1 -stable-baselines3==1.6.2 -sb3-contrib==1.6.2 +stable-baselines3==1.7.0 +sb3-contrib==1.7.0 # Gym is forced to this version by stable-baselines3. gym==0.21 diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 575e4f1e1..914dbb745 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -6,6 +6,6 @@ scikit-learn==1.1.3 joblib==1.2.0 catboost==1.1.1; platform_machine != 'aarch64' -lightgbm==3.3.4 +lightgbm==3.3.5 xgboost==1.7.3 tensorboard==2.11.2 diff --git a/requirements.txt b/requirements.txt index 73e0e6576..e6b5ca464 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.1 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==2.6.65 +ccxt==2.7.12 # Pin cryptography for now due to rust build errors with piwheels cryptography==38.0.1; platform_machine == 'armv7l' cryptography==39.0.0; platform_machine != 'armv7l' @@ -22,7 +22,7 @@ jinja2==3.1.2 tables==3.8.0 blosc==1.11.1 joblib==1.2.0 -pyarrow==10.0.1; platform_machine != 'armv7l' +pyarrow==11.0.0; platform_machine != 'armv7l' # find first, C search in arrays py_find_1st==1.1.5 diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index e0c79d52a..c6b1dcc5a 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -437,6 +437,7 @@ def test_dp__add_external_df(default_conf_usdt): # Add the same dataframe again - dataframe size shall not change. res = dp._add_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is True + assert isinstance(res[1], int) assert res[1] == 0 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) assert len(df) == 24 @@ -446,6 +447,7 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df2, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is True + assert isinstance(res[1], int) assert res[1] == 0 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) assert len(df) == 48 @@ -455,6 +457,7 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df3, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is True + assert isinstance(res[1], int) assert res[1] == 0 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) # New length = 48 + 12 (since we have a 12 hour offset). @@ -478,6 +481,7 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is False # 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00 + assert isinstance(res[1], int) assert res[1] == 36 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) # New length = 61 + 1 @@ -488,4 +492,5 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is False # 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00 + assert isinstance(res[1], int) assert res[1] == 0 diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py new file mode 100644 index 000000000..7c8324bf6 --- /dev/null +++ b/tests/exchange/test_bybit.py @@ -0,0 +1,57 @@ +from unittest.mock import MagicMock + +from freqtrade.enums.marginmode import MarginMode +from freqtrade.enums.tradingmode import TradingMode +from freqtrade.exchange.exchange_utils import timeframe_to_msecs +from tests.conftest import get_mock_coro, get_patched_exchange +from tests.exchange.test_exchange import ccxt_exceptionhandlers + + +def test_additional_exchange_init_bybit(default_conf, mocker): + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + api_mock = MagicMock() + api_mock.set_position_mode = MagicMock(return_value={"dualSidePosition": False}) + get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) + assert api_mock.set_position_mode.call_count == 1 + ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'bybit', + "additional_exchange_init", "set_position_mode") + + +async def test_bybit_fetch_funding_rate(default_conf, mocker): + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + api_mock = MagicMock() + api_mock.fetch_funding_rate_history = get_mock_coro(return_value=[]) + exchange = get_patched_exchange(mocker, default_conf, id='bybit', api_mock=api_mock) + limit = 200 + # Test fetch_funding_rate_history (current data) + await exchange._fetch_funding_rate_history( + pair='BTC/USDT:USDT', + timeframe='4h', + limit=limit, + ) + + assert api_mock.fetch_funding_rate_history.call_count == 1 + assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT' + kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1] + assert kwargs['params'] == {} + assert kwargs['since'] is None + + api_mock.fetch_funding_rate_history.reset_mock() + since_ms = 1610000000000 + since_ms_end = since_ms + (timeframe_to_msecs('4h') * limit) + # Test fetch_funding_rate_history (current data) + await exchange._fetch_funding_rate_history( + pair='BTC/USDT:USDT', + timeframe='4h', + limit=limit, + since_ms=since_ms, + ) + + assert api_mock.fetch_funding_rate_history.call_count == 1 + assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT' + kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1] + assert kwargs['params'] == {'until': since_ms_end} + assert kwargs['since'] == since_ms diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 4d7216860..2ca92799f 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -43,7 +43,7 @@ EXCHANGES = { 'hasQuoteVolumeFutures': True, 'leverage_tiers_public': False, 'leverage_in_spot_market': False, - 'sample_order': { + 'sample_order': [{ "symbol": "SOLUSDT", "orderId": 3551312894, "orderListId": -1, @@ -60,7 +60,32 @@ EXCHANGES = { "workingTime": 1674493798550, "fills": [], "selfTradePreventionMode": "NONE", - } + }] + }, + 'binanceus': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '5m', + 'futures': False, + 'sample_order': [{ + "symbol": "SOLUSDT", + "orderId": 3551312894, + "orderListId": -1, + "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", + "transactTime": 1674493798550, + "price": "15.00000000", + "origQty": "1.00000000", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "workingTime": 1674493798550, + "fills": [], + "selfTradePreventionMode": "NONE", + }] }, 'kraken': { 'pair': 'BTC/USDT', @@ -77,6 +102,40 @@ EXCHANGES = { 'timeframe': '5m', 'leverage_tiers_public': False, 'leverage_in_spot_market': True, + 'sample_order': [ + {'id': '63d6742d0adc5570001d2bbf7'}, # create order + { + 'id': '63d6742d0adc5570001d2bbf7', + 'symbol': 'NAKA-USDT', + 'opType': 'DEAL', + 'type': 'limit', + 'side': 'buy', + 'price': '30', + 'size': '0.1', + 'funds': '0', + 'dealFunds': '0.032626', + 'dealSize': '0.1', + 'fee': '0.000065252', + 'feeCurrency': 'USDT', + 'stp': '', + 'stop': '', + 'stopTriggered': False, + 'stopPrice': '0', + 'timeInForce': 'GTC', + 'postOnly': False, + 'hidden': False, + 'iceberg': False, + 'visibleSize': '0', + 'cancelAfter': 0, + 'channel': 'API', + 'clientOid': '0a053870-11bf-41e5-be61-b272a4cb62e1', + 'remark': None, + 'tags': 'partner:ccxt', + 'isActive': False, + 'cancelExist': False, + 'createdAt': 1674493798550, + 'tradeType': 'TRADE' + }], }, 'gateio': { 'pair': 'BTC/USDT', @@ -100,6 +159,16 @@ EXCHANGES = { 'leverage_tiers_public': True, 'leverage_in_spot_market': True, }, + 'bybit': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '5m', + 'futures_pair': 'BTC/USDT:USDT', + 'futures': True, + 'leverage_tiers_public': True, + 'leverage_in_spot_market': True, + }, 'huobi': { 'pair': 'ETH/BTC', 'stake_currency': 'BTC', @@ -176,6 +245,7 @@ def exchange_futures(request, exchange_conf, class_mocker): class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees') class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init') class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init') + class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init') class_mocker.patch('freqtrade.exchange.exchange.Exchange.load_cached_leverage_tiers', return_value=None) class_mocker.patch('freqtrade.exchange.exchange.Exchange.cache_leverage_tiers') @@ -231,14 +301,18 @@ class TestCCXTExchange(): def test_ccxt_order_parse(self, exchange: EXCHANGE_FIXTURE_TYPE): exch, exchange_name = exchange - if stuff := EXCHANGES[exchange_name].get('sample_order'): - - po = exch._api.parse_order(stuff) - assert po['timestamp'] == 1674493798550 - assert isinstance(po['timestamp'], int) - assert isinstance(po['price'], float) - assert isinstance(po['amount'], float) - assert isinstance(po['status'], str) + if orders := EXCHANGES[exchange_name].get('sample_order'): + for order in orders: + po = exch._api.parse_order(order) + assert isinstance(po['id'], str) + assert po['id'] is not None + if len(order.keys()) > 1: + assert po['timestamp'] == 1674493798550 + assert isinstance(po['datetime'], str) + assert isinstance(po['timestamp'], int) + assert isinstance(po['price'], float) + assert isinstance(po['amount'], float) + assert isinstance(po['status'], str) else: pytest.skip(f"No sample order available for exchange {exchange_name}") @@ -553,23 +627,25 @@ class TestCCXTExchange(): ) liquidation_price = futures.dry_run_liquidation_price( - futures_pair, - 40000, - False, - 100, - 100, - 100, + pair=futures_pair, + open_rate=40000, + is_short=False, + amount=100, + stake_amount=100, + leverage=5, + wallet_balance=100, ) assert (isinstance(liquidation_price, float)) assert liquidation_price >= 0.0 liquidation_price = futures.dry_run_liquidation_price( - futures_pair, - 40000, - False, - 100, - 100, - 100, + pair=futures_pair, + open_rate=40000, + is_short=False, + amount=100, + stake_amount=100, + leverage=5, + wallet_balance=100, ) assert (isinstance(liquidation_price, float)) assert liquidation_price >= 0.0 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 0fa7f90ec..0ebdfd218 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3959,7 +3959,7 @@ def test_validate_trading_mode_and_margin_mode( ("binance", "margin", {"options": {"defaultType": "margin"}}), ("binance", "futures", {"options": {"defaultType": "swap"}}), ("bybit", "spot", {"options": {"defaultType": "spot"}}), - ("bybit", "futures", {"options": {"defaultType": "linear"}}), + ("bybit", "futures", {"options": {"defaultType": "swap"}}), ("gateio", "futures", {"options": {"defaultType": "swap"}}), ("hitbtc", "futures", {"options": {"defaultType": "swap"}}), ("kraken", "futures", {"options": {"defaultType": "swap"}}), @@ -4566,6 +4566,7 @@ def test_liquidation_price_is_none( is_short=is_short, amount=71200.81144, stake_amount=open_rate * 71200.81144, + leverage=5, wallet_balance=-56354.57, mm_ex_1=0.10, upnl_ex_1=0.0 @@ -4586,7 +4587,7 @@ def test_liquidation_price_is_none( ("binance", False, 'futures', 'cross', 1535443.01, 356512.508, -448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89) ]) -def test_liquidation_price( +def test_liquidation_price_binance( mocker, default_conf, exchange_name, open_rate, is_short, trading_mode, margin_mode, wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, amount, mm_ratio, expected ): @@ -4604,6 +4605,7 @@ def test_liquidation_price( upnl_ex_1=upnl_ex_1, amount=amount, stake_amount=open_rate * amount, + leverage=5, ), 2)) == expected @@ -5025,6 +5027,7 @@ def test__get_params(mocker, default_conf, exchange_name): def test_get_liquidation_price1(mocker, default_conf): api_mock = MagicMock() + leverage = 9.97 positions = [ { 'info': {}, @@ -5037,7 +5040,7 @@ def test_get_liquidation_price1(mocker, default_conf): 'maintenanceMarginPercentage': 0.025, 'entryPrice': 18.884, 'notional': 15.1072, - 'leverage': 9.97, + 'leverage': leverage, 'unrealizedPnl': 0.0048, 'contracts': 8, 'contractSize': 0.1, @@ -5067,6 +5070,7 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) assert liq_price == 17.47 @@ -5079,6 +5083,7 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) assert liq_price == 17.540699999999998 @@ -5091,6 +5096,7 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) assert liq_price is None @@ -5104,11 +5110,12 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) -@pytest.mark.parametrize('liquidation_buffer', [0.0, 0.05]) +@pytest.mark.parametrize('liquidation_buffer', [0.0]) @pytest.mark.parametrize( "is_short,trading_mode,exchange_name,margin_mode,leverage,open_rate,amount,expected_liq", [ (False, 'spot', 'binance', '', 5.0, 10.0, 1.0, None), @@ -5137,6 +5144,16 @@ def test_get_liquidation_price1(mocker, default_conf): (False, 'futures', 'gateio', 'isolated', 5.0, 10.0, 1.0, 8.085708510208207), (False, 'futures', 'gateio', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506), (False, 'futures', 'okx', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506), + # bybit, long + (False, 'futures', 'bybit', 'isolated', 1.0, 10.0, 1.0, 0.1), + (False, 'futures', 'bybit', 'isolated', 3.0, 10.0, 1.0, 6.7666666), + (False, 'futures', 'bybit', 'isolated', 5.0, 10.0, 1.0, 8.1), + (False, 'futures', 'bybit', 'isolated', 10.0, 10.0, 1.0, 9.1), + # bybit, short + (True, 'futures', 'bybit', 'isolated', 1.0, 10.0, 1.0, 19.9), + (True, 'futures', 'bybit', 'isolated', 3.0, 10.0, 1.0, 13.233333), + (True, 'futures', 'bybit', 'isolated', 5.0, 10.0, 1.0, 11.9), + (True, 'futures', 'bybit', 'isolated', 10.0, 10.0, 1.0, 10.9), ] ) def test_get_liquidation_price( @@ -5222,7 +5239,7 @@ def test_get_liquidation_price( amount=amount, stake_amount=amount * open_rate / leverage, wallet_balance=amount * open_rate / leverage, - # leverage=leverage, + leverage=leverage, is_short=is_short, ) if expected_liq is None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7efd0393d..91fc25a83 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -751,6 +751,8 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) (False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796), (True, 'futures', 'okx', 'isolated', 0.0, 11.87413417771621), (False, 'futures', 'okx', 'isolated', 0.0, 8.085708510208207), + (True, 'futures', 'bybit', 'isolated', 0.0, 11.9), + (False, 'futures', 'bybit', 'isolated', 0.0, 8.1), ]) def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, limit_order_open, is_short, trading_mode,