diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index f2f52b7dd..f5a52ff49 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -98,6 +98,38 @@ class MyAwesomeStrategy(IStrategy): !!! Note All overrides are optional and can be mixed/matched as necessary. +### Overriding Base estimator + +You can define your own estimator for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass. + +```python +class MyAwesomeStrategy(IStrategy): + class HyperOpt: + def generate_estimator(): + return "RF" + +``` + +Possible values are either one of "GP", "RF", "ET", "GBRT" (Details can be found in the [scikit-optimize documentation](https://scikit-optimize.github.io/)), or "an instance of a class that inherits from `RegressorMixin` (from sklearn) and where the `predict` method has an optional `return_std` argument, which returns `std(Y | x)` along with `E[Y | x]`". + +Some research will be necessary to find additional Regressors. + +Example for `ExtraTreesRegressor` ("ET") with additional parameters: + +```python +class MyAwesomeStrategy(IStrategy): + class HyperOpt: + def generate_estimator(): + from skopt.learning import ExtraTreesRegressor + # Corresponds to "ET" - but allows additional parameters. + return ExtraTreesRegressor(n_estimators=100) + +``` + +!!! Note + While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used. + If you're unsure about this, best use one of the Defaults (`"ET"` has proven to be the most versatile) without further parameters. + ## Space options For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types: diff --git a/docs/hyperopt.md b/docs/hyperopt.md index e69b761c4..09d43939a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -677,7 +677,7 @@ If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace f These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. -If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. +If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 69e12d5dc..b612a4ddf 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -165,6 +165,7 @@ Example to remove the first 10 pairs from the pairlist: ```json "pairlists": [ + // ... { "method": "OffsetFilter", "offset": 10 @@ -190,6 +191,19 @@ Sorts pairs by past trade performance, as follows: Trade count is used as a tie breaker. +You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window). +Not defining this parameter (or setting it to 0) will use all-time performance. + +```json +"pairlists": [ + // ... + { + "method": "PerformanceFilter", + "minutes": 1440 // rolling 24h + } +], +``` + !!! Note `PerformanceFilter` does not support backtesting mode. diff --git a/docs/leverage.md b/docs/leverage.md index c4b975a0b..9448c64c3 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -15,3 +15,7 @@ For longs, the currency which pays the interest fee for the `borrowed` will alre Rollover fee = P (borrowed money) * R (quat_hourly_interest) * ceiling(T/4) (in hours) I (interest) = Opening fee + Rollover fee [source](https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-) + +# TODO-lev: Mention that says you can't run 2 bots on the same account with leverage, + +#TODO-lev: Create a huge risk disclaimer \ No newline at end of file diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4409af6ea..2b9517f3b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -288,6 +288,12 @@ Stoploss values returned from `custom_stoploss()` always specify a percentage re The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. +### Calculating stoploss percentage from absolute price + +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. + +The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`. + #### Stepped stoploss Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index cfea60d22..725252b30 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -639,6 +639,167 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. +!!! Note + Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings. + This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade + is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `sell_reason` in + `confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when + `current_profit < open_relative_stop`. + +### *stoploss_from_absolute()* + +In some situations it may be confusing to deal with stops relative to current rate. Instead, you may define a stoploss level using an absolute price. + +??? Example "Returning a stoploss using absolute price from the custom stoploss function" + + If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)`. + + ``` python + + from datetime import datetime + from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, stoploss_from_open + + class AwesomeStrategy(IStrategy): + + use_custom_stoploss = True + + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) + return dataframe + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + candle = dataframe.iloc[-1].squeeze() + return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate) + + ``` + +### *@informative()* + +``` python +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to: + * {base}_{quote}_{column}_{timeframe} if asset is specified. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. + """ +``` + +In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation, +not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method. +When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter) +for more information. + +??? Example "Fast and easy way to define informative pairs" + + Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs. + + ``` python + + from datetime import datetime + from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, informative + + class AwesomeStrategy(IStrategy): + + # This method is not required. + # def informative_pairs(self): ... + + # Define informative upper timeframe for each pair. Decorators can be stacked on same + # method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'. + @informative('30m') + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. Available in populate_indicators and other methods as + # 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable + # instead of hardcoding actual stake currency. Available in populate_indicators and other + # methods as 'btc_usdt_rsi_1h' (when stake currency is USDT). + @informative('1h', 'BTC/{stake}') + def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/ETH informative pair. You must specify quote currency if it is different from + # stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'. + @informative('1h', 'ETH/BTC') + def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. A custom formatter may be specified for formatting + # column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom + # formatting. Available in populate_indicators and other methods as 'rsi_upper'. + @informative('1h', 'BTC/{stake}', '{column}') + def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Strategy timeframe indicators for current pair. + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + # Informative pairs are available in this method. + dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] + return dataframe + + ``` + +!!! Note + Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs + manually as described [in the DataProvider section](#complete-data-provider-sample). + +!!! Note + Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code. + + ``` python + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + stake = self.config['stake_currency'] + dataframe.loc[ + ( + (dataframe[f'btc_{stake}_rsi_1h'] < 35) + & + (dataframe['volume'] > 0) + ), + ['buy', 'buy_tag']] = (1, 'buy_signal_rsi') + + return dataframe + ``` + + Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`. + +!!! Warning "Duplicate method names" + Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method) + will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators + created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique! ## Additional data (Wallets) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index d2d30f399..ec1ff92cf 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -53,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if epochs and export_csv: HyperoptTools.export_csv_file( - config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv + config, epochs, export_csv ) diff --git a/freqtrade/configuration/PeriodicCache.py b/freqtrade/configuration/PeriodicCache.py new file mode 100644 index 000000000..25c0c47f3 --- /dev/null +++ b/freqtrade/configuration/PeriodicCache.py @@ -0,0 +1,19 @@ +from datetime import datetime, timezone + +from cachetools.ttl import TTLCache + + +class PeriodicCache(TTLCache): + """ + Special cache that expires at "straight" times + A timer with ttl of 3600 (1h) will expire at every full hour (:00). + """ + + def __init__(self, maxsize, ttl, getsizeof=None): + def local_timer(): + ts = datetime.now(timezone.utc).timestamp() + offset = (ts % ttl) + return ts - offset + + # Init with smlight offset + super().__init__(maxsize=maxsize, ttl=ttl-1e-5, timer=local_timer, getsizeof=getsizeof) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 730a4e47f..cf41c0ca9 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -4,4 +4,5 @@ from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.configuration import Configuration +from freqtrade.configuration.PeriodicCache import PeriodicCache from freqtrade.configuration.timerange import TimeRange diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index b945dd1bd..bee96c746 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -119,7 +119,7 @@ class Edge: ) # Download informative pairs too res = defaultdict(list) - for p, t in self.strategy.informative_pairs(): + for p, t in self.strategy.gather_informative_pairs(): res[t].append(p) for timeframe, inf_pairs in res.items(): timerange_startup = deepcopy(self._timerange) diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index f0c2dd00b..074dd2b10 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -20,4 +20,7 @@ class Bibox(Exchange): # fetchCurrencies API point requires authentication for Bibox, # so switch it off for Freqtrade load_markets() - _ccxt_config: Dict = {"has": {"fetchCurrencies": False}} + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {"has": {"fetchCurrencies": False}} diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index a7d64b32f..3e96bec7b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,5 +1,7 @@ """ Binance exchange subclass """ +import json import logging +from pathlib import Path from typing import Dict, List, Optional, Tuple import arrow @@ -31,9 +33,27 @@ class Binance(Exchange): # 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 + # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported ] + @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) @@ -47,8 +67,8 @@ class Binance(Exchange): ) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ creates a stoploss limit order. this stoploss-limit is binance-specific. @@ -76,7 +96,7 @@ class Binance(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, side, amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: @@ -87,6 +107,7 @@ class Binance(Exchange): rate = self.price_to_precision(pair, rate) + self._lev_prep(pair, leverage) order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, price=rate, params=params) logger.info('stoploss limit order added for %s. ' @@ -119,26 +140,35 @@ class Binance(Exchange): Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair """ - try: - leverage_brackets = self._api.load_leverage_brackets() - for pair, brackets in leverage_brackets.items(): - self._leverage_brackets[pair] = [ - [ - min_amount, - float(margin_req) - ] for [ - min_amount, - margin_req - ] in brackets - ] + if self.trading_mode == TradingMode.FUTURES: + try: + if self._config['dry_run']: + leverage_brackets_path = ( + Path(__file__).parent / 'binance_leverage_brackets.json' + ) + with open(leverage_brackets_path) as json_file: + leverage_brackets = json.load(json_file) + else: + leverage_brackets = self._api.load_leverage_brackets() - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError(f'Could not fetch leverage amounts due to' - f'{e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e + for pair, brackets in leverage_brackets.items(): + self._leverage_brackets[pair] = [ + [ + min_amount, + float(margin_req) + ] for [ + min_amount, + margin_req + ] in brackets + ] + + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError(f'Could not fetch leverage amounts due to' + f'{e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: """ @@ -166,9 +196,11 @@ class Binance(Exchange): """ trading_mode = trading_mode or self.trading_mode + if self._config['dry_run'] or trading_mode != TradingMode.FUTURES: + return + try: - if trading_mode == TradingMode.FUTURES: - self._api.set_leverage(symbol=pair, leverage=leverage) + self._api.set_leverage(symbol=pair, leverage=leverage) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: diff --git a/freqtrade/exchange/binance_leverage_brackets.json b/freqtrade/exchange/binance_leverage_brackets.json new file mode 100644 index 000000000..4450b015e --- /dev/null +++ b/freqtrade/exchange/binance_leverage_brackets.json @@ -0,0 +1,1214 @@ +{ + "1000SHIB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "1INCH/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "AAVE/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "ADA/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "ADA/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "AKRO/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ALGO/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "ALICE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ALPHA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ANKR/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ATA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ATOM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "AUDIO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "AVAX/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "AXS/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"], + [15000000.0, "0.5"] + ], + "BAKE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAND/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BCH/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BEL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BLZ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BNB/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "BNB/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTC/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.0, "0.5"] + ], + "BTC/USDT": [ + [0.0, "0.004"], + [50000.0, "0.005"], + [250000.0, "0.01"], + [1000000.0, "0.025"], + [5000000.0, "0.05"], + [20000000.0, "0.1"], + [50000000.0, "0.125"], + [100000000.0, "0.15"], + [200000000.0, "0.25"], + [300000000.0, "0.5"] + ], + "BTCBUSD_210129": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCBUSD_210226": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCDOM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTCSTUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTCUSDT_210326": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTCUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTCUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "BTS/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BZRX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "C98/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CELR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CHR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CHZ/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "COMP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "COTI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CRV/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CTK/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CVC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DASH/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DEFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DENT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DGB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DODO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DOGE/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "DOGE/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "DOT/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "DOTECOUSDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DYDX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "EGLD/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ENJ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "EOS/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETC/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETH/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.0, "0.5"] + ], + "ETH/USDT": [ + [0.0, "0.005"], + [10000.0, "0.0065"], + [100000.0, "0.01"], + [500000.0, "0.02"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "ETHUSDT_210326": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETHUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETHUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "FIL/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "FLM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "FTM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "FTT/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "GRT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "GTC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HBAR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HNT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HOT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ICP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ICX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOST/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOTA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOTX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KAVA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KEEP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KNC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KSM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LENDUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LINA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LINK/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "LIT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LRC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LTC/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "LUNA/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"], + [15000000.0, "0.5"] + ], + "MANA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MASK/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MATIC/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "MKR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MTL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NEAR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NEO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NKN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OCEAN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OGN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OMG/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ONE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ONT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "QTUM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RAY/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "REEF/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "REN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RLC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RSR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RUNE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RVN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SAND/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SFP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SKL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SNX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SOL/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "SOL/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.25"], + [10000000.0, "0.5"] + ], + "SRM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "STMX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "STORJ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SUSHI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SXP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "THETA/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "TLM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TOMO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TRB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TRX/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "UNFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "UNI/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "VET/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "WAVES/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "XEM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "XLM/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XMR/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XRP/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "XRP/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XTZ/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "YFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "YFII/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZEC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZEN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZIL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZRX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ] +} diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0c95f50d0..bb31b84f2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -49,9 +49,6 @@ class Exchange: _config: Dict = {} - # Parameters to add directly to ccxt sync/async initialization. - _ccxt_config: Dict = {} - # Parameters to add directly to buy/sell calls (like agreeing to trading agreement) _params: Dict = {} @@ -131,14 +128,25 @@ class Exchange: self._trades_pagination = self._ft_has['trades_pagination'] self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] + self.trading_mode: TradingMode = ( + TradingMode(config.get('trading_mode')) + if config.get('trading_mode') + else TradingMode.SPOT + ) + self.collateral: Optional[Collateral] = ( + Collateral(config.get('collateral')) + if config.get('collateral') + else None + ) + # Initialize ccxt objects - ccxt_config = self._ccxt_config.copy() + ccxt_config = self._ccxt_config ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) - ccxt_async_config = self._ccxt_config.copy() + ccxt_async_config = self._ccxt_config ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_async_config) ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), @@ -146,17 +154,6 @@ class Exchange: self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) - self.trading_mode: TradingMode = ( - TradingMode(config.get('trading_mode')) - if config.get('trading_mode') - else TradingMode.SPOT - ) - collateral: Optional[Collateral] = ( - Collateral(config.get('collateral')) - if config.get('collateral') - else None - ) - if self.trading_mode != TradingMode.SPOT: self.fill_leverage_brackets() @@ -177,7 +174,7 @@ class Exchange: self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_required_startup_candles(config.get('startup_candle_count', 0), config.get('timeframe', '')) - self.validate_trading_mode_and_collateral(self.trading_mode, collateral) + self.validate_trading_mode_and_collateral(self.trading_mode, self.collateral) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( "markets_refresh_interval", 60) * 60 @@ -210,7 +207,6 @@ class Exchange: 'secret': exchange_config.get('secret'), 'password': exchange_config.get('password'), 'uid': exchange_config.get('uid', ''), - 'options': exchange_config.get('options', {}) } if ccxt_kwargs: logger.info('Applying additional ccxt config: %s', ccxt_kwargs) @@ -231,6 +227,11 @@ class Exchange: return api + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {} + @property def name(self) -> str: """exchange Name (from ccxt)""" @@ -617,15 +618,13 @@ class Exchange: # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return self._apply_leverage_to_stake_amount( + return self._get_stake_amount_considering_leverage( max(min_stake_amounts) * amount_reserve_percent, leverage or 1.0 ) - def _apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): + def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float): """ - #TODO-lev: Find out how this works on Kraken and FTX - # * Should be implemented by child classes if leverage affects the stake_amount Takes the minimum stake amount for a pair with no leverage and returns the minimum stake amount when leverage is considered :param stake_amount: The stake amount for a pair before leverage is considered @@ -636,7 +635,7 @@ class Exchange: # Dry-run methods def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, params: Dict = {}) -> Dict[str, Any]: + rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) dry_order: Dict[str, Any] = { @@ -653,7 +652,8 @@ class Exchange: 'timestamp': arrow.utcnow().int_timestamp * 1000, 'status': "closed" if ordertype == "market" else "open", 'fee': None, - 'info': {} + 'info': {}, + 'leverage': leverage } if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: dry_order["info"] = {"stopPrice": dry_order["price"]} @@ -663,7 +663,7 @@ class Exchange: average = self.get_dry_market_fill_price(pair, side, amount, rate) dry_order.update({ 'average': average, - 'cost': dry_order['amount'] * average, + 'cost': (dry_order['amount'] * average) / leverage }) dry_order = self.add_dry_order_fee(pair, dry_order) @@ -771,19 +771,26 @@ class Exchange: # Order handling - def create_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, time_in_force: str = 'gtc', leverage=1.0) -> Dict: - - if self._config['dry_run']: - dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate) - return dry_order - + def _lev_prep(self, pair: str, leverage: float): if self.trading_mode != TradingMode.SPOT: + self.set_margin_mode(pair, self.collateral) self._set_leverage(leverage, pair) + + def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: params = self._params.copy() if time_in_force != 'gtc' and ordertype != 'market': param = self._ft_has.get('time_in_force_parameter', '') params.update({param: time_in_force}) + return params + + def create_order(self, pair: str, ordertype: str, side: str, amount: float, + rate: float, leverage: float = 1.0, time_in_force: str = 'gtc') -> Dict: + # TODO-lev: remove default for leverage + if self._config['dry_run']: + dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage) + return dry_order + + params = self._get_params(ordertype, leverage, time_in_force) try: # Set the precision for amount and price(rate) as accepted by the exchange @@ -792,6 +799,7 @@ class Exchange: or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) rate_for_order = self.price_to_precision(pair, rate) if needs_price else None + self._lev_prep(pair, leverage) order = self._api.create_order(pair, ordertype, side, amount, rate_for_order, params) self._log_exchange_response('create_order', order) @@ -822,8 +830,8 @@ class Exchange: """ raise OperationalException(f"stoploss is not implemented for {self.name}.") - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ creates a stoploss order. The precise ordertype is determined by the order_types dict or exchange default. @@ -1586,15 +1594,13 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - @retrier def fill_leverage_brackets(self): """ - #TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken + # TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair """ - raise OperationalException( - f"{self.name.capitalize()}.fill_leverage_brackets has not been implemented.") + return def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: """ @@ -1615,7 +1621,7 @@ class Exchange: Set's the leverage before making a trade, in order to not have the same leverage on every trade """ - if not self.exchange_has("setLeverage"): + if self._config['dry_run'] or not self.exchange_has("setLeverage"): # Some exchanges only support one collateral type return @@ -1635,7 +1641,7 @@ class Exchange: 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") ''' - if not self.exchange_has("setMarginMode"): + if self._config['dry_run'] or not self.exchange_has("setMarginMode"): # Some exchanges only support one collateral type return diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 095d8eaa1..62adea04c 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -49,8 +49,8 @@ class Ftx(Exchange): ) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ Creates a stoploss order. depending on order_types.stoploss configuration, uses 'market' or limit order. @@ -69,7 +69,7 @@ class Ftx(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, side, amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: @@ -81,6 +81,7 @@ class Ftx(Exchange): params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) + self._lev_prep(pair, leverage) order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, params=params) self._log_exchange_response('create_stoploss_order', order) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 661000d4d..c0d3a1b38 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -85,8 +85,8 @@ class Kraken(Exchange): )) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. @@ -108,7 +108,7 @@ class Kraken(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, side, amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: @@ -182,8 +182,16 @@ class Kraken(Exchange): Kraken set's the leverage as an option in the order object, so we need to add it to params """ + if leverage > 1.0: self._params['leverage'] = leverage else: if 'leverage' in self._params: del self._params['leverage'] + return + + def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: + params = super()._get_params(ordertype, leverage, time_in_force) + if leverage > 1.0: + params['leverage'] = leverage + return params diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9f58e3c36..bc27b94a8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -86,10 +86,10 @@ class FreqtradeBot(LoggingMixin): self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) - # Attach Dataprovider to Strategy baseclass - IStrategy.dp = self.dataprovider - # Attach Wallets to Strategy baseclass - IStrategy.wallets = self.wallets + # Attach Dataprovider to strategy instance + self.strategy.dp = self.dataprovider + # Attach Wallets to strategy instance + self.strategy.wallets = self.wallets # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ @@ -175,7 +175,7 @@ class FreqtradeBot(LoggingMixin): # Refreshing candles self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), - self.strategy.informative_pairs()) + self.strategy.gather_informative_pairs()) strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() @@ -812,10 +812,8 @@ class FreqtradeBot(LoggingMixin): exit_signal_type = "exit_short" if trade.is_short else "exit_long" # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal - if ( - self.config.get('use_sell_signal', True) or - self.config.get('ignore_roi_if_buy_signal', False) - ): + if (self.config.get('use_sell_signal', True) or + self.config.get('ignore_roi_if_buy_signal', False)): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, self.strategy.timeframe) @@ -847,7 +845,8 @@ class FreqtradeBot(LoggingMixin): amount=trade.amount, stop_price=stop_price, order_types=self.strategy.order_types, - side=trade.exit_side + side=trade.exit_side, + leverage=trade.leverage ) order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') @@ -947,7 +946,7 @@ class FreqtradeBot(LoggingMixin): return False - def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None: + def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None: """ Check to see if stoploss on exchange should be updated in case of trailing stoploss on exchange diff --git a/freqtrade/leverage/interest.py b/freqtrade/leverage/interest.py index aacbb3532..2878ad784 100644 --- a/freqtrade/leverage/interest.py +++ b/freqtrade/leverage/interest.py @@ -20,7 +20,7 @@ def interest( :param exchange_name: The exchanged being trading on :param borrowed: The amount of currency being borrowed - :param rate: The rate of interest + :param rate: The rate of interest (i.e daily interest rate) :param hours: The time in hours that the currency has been borrowed for Raises: @@ -36,7 +36,8 @@ def interest( # Rounded based on https://kraken-fees-calculator.github.io/ return borrowed * rate * (one+ceil(hours/four)) elif exchange_name == "ftx": - # TODO-lev: Add FTX interest formula - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + # As Explained under #Interest rates section in + # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer + return borrowed * rate * ceil(hours)/twenty_four else: raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 08e9d856c..81e406d87 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -157,7 +157,7 @@ class Backtesting: self.strategy: IStrategy = strategy strategy.dp = self.dataprovider # Attach Wallets to Strategy baseclass - IStrategy.wallets = self.wallets + strategy.wallets = self.wallets # Set stoploss_on_exchange to false for backtesting, # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 417faa685..f211da750 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -8,6 +8,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import TimeRange, validate_config_consistency +from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.optimize.optimize_reports import generate_edge_table from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -33,6 +34,7 @@ class EdgeCli: self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.strategy = StrategyResolver.load_strategy(self.config) + self.strategy.dp = DataProvider(config, None) validate_config_consistency(self.config) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 14b155546..9549b4054 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -45,7 +45,7 @@ progressbar.streams.wrap_stdout() logger = logging.getLogger(__name__) -INITIAL_POINTS = 30 +INITIAL_POINTS = 5 # Keep no more than SKOPT_MODEL_QUEUE_SIZE models # in the skopt model queue, to optimize memory consumption @@ -241,7 +241,7 @@ class Hyperopt: if HyperoptTools.has_space(self.config, 'buy'): logger.debug("Hyperopt has 'buy' space") - self.buy_space = self.custom_hyperopt.indicator_space() + self.buy_space = self.custom_hyperopt.buy_indicator_space() if HyperoptTools.has_space(self.config, 'sell'): logger.debug("Hyperopt has 'sell' space") @@ -365,10 +365,20 @@ class Hyperopt: } def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: + estimator = self.custom_hyperopt.generate_estimator() + + acq_optimizer = "sampling" + if isinstance(estimator, str): + if estimator not in ("GP", "RF", "ET", "GBRT"): + raise OperationalException(f"Estimator {estimator} not supported.") + else: + acq_optimizer = "auto" + + logger.info(f"Using estimator {estimator}.") return Optimizer( dimensions, - base_estimator="ET", - acq_optimizer="auto", + base_estimator=estimator, + acq_optimizer=acq_optimizer, n_initial_points=INITIAL_POINTS, acq_optimizer_kwargs={'n_jobs': cpu_count}, random_state=self.random_state, diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 1f11cec80..c1c769c72 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -12,7 +12,7 @@ from freqtrade.exceptions import OperationalException with suppress(ImportError): from skopt.space import Dimension -from freqtrade.optimize.hyperopt_interface import IHyperOpt +from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt def _format_exception_message(space: str) -> str: @@ -56,7 +56,7 @@ class HyperOptAuto(IHyperOpt): else: _format_exception_message(category) - def indicator_space(self) -> List['Dimension']: + def buy_indicator_space(self) -> List['Dimension']: return self._get_indicator_space('buy') def sell_indicator_space(self) -> List['Dimension']: @@ -79,3 +79,6 @@ class HyperOptAuto(IHyperOpt): def trailing_space(self) -> List['Dimension']: return self._get_func('trailing_space')() + + def generate_estimator(self) -> EstimatorType: + return self._get_func('generate_estimator')() diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 8fb40f557..53b4f087c 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -5,8 +5,9 @@ This module defines the interface to apply for hyperopt import logging import math from abc import ABC -from typing import Dict, List +from typing import Dict, List, Union +from sklearn.base import RegressorMixin from skopt.space import Categorical, Dimension, Integer from freqtrade.exchange import timeframe_to_minutes @@ -17,6 +18,8 @@ from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) +EstimatorType = Union[RegressorMixin, str] + class IHyperOpt(ABC): """ @@ -37,6 +40,14 @@ class IHyperOpt(ABC): IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED IHyperOpt.timeframe = str(config['timeframe']) + def generate_estimator(self) -> EstimatorType: + """ + Return base_estimator. + Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class + inheriting from RegressorMixin (from sklearn). + """ + return 'ET' + def generate_roi_table(self, params: Dict) -> Dict[int, float]: """ Create a ROI table. diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index b2e024f65..cfbc2757e 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Tuple import numpy as np +import pandas as pd import rapidjson import tabulate from colorama import Fore, Style @@ -298,8 +299,8 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str: - + def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, + has_drawdown: bool) -> pd.DataFrame: trials['Best'] = '' if 'results_metrics.winsdrawslosses' not in trials.columns: @@ -435,8 +436,7 @@ class HyperoptTools(): return table @staticmethod - def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, - csv_file: str) -> None: + def export_csv_file(config: dict, results: list, csv_file: str) -> None: """ Log result to csv-file """ diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 37cf4c45c..4ea524b61 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from decimal import Decimal from typing import Any, Dict, List, Optional @@ -1025,17 +1025,21 @@ class Trade(_DECL_BASE, LocalTrade): return total_open_stake_amount or 0 @staticmethod - def get_overall_performance() -> List[Dict[str, Any]]: + def get_overall_performance(minutes=None) -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, including profit and trade count NOTE: Not supported in Backtesting. """ + filters = [Trade.is_open.is_(False)] + if minutes: + start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes) + filters.append(Trade.close_date >= start_date) pair_rates = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ + ).filter(*filters)\ .group_by(Trade.pair) \ .order_by(desc('profit_sum_abs')) \ .all() diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index dc5cab31e..5627d82ce 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional import arrow from pandas import DataFrame +from freqtrade.configuration import PeriodicCache from freqtrade.exceptions import OperationalException from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList @@ -18,14 +19,15 @@ logger = logging.getLogger(__name__) class AgeFilter(IPairList): - # Checked symbols cache (dictionary of ticker symbol => timestamp) - _symbolsChecked: Dict[str, int] = {} - def __init__(self, exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: Dict[str, Any], pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + # Checked symbols cache (dictionary of ticker symbol => timestamp) + self._symbolsChecked: Dict[str, int] = {} + self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400) + self._min_days_listed = pairlistconfig.get('min_days_listed', 10) self._max_days_listed = pairlistconfig.get('max_days_listed', None) @@ -69,9 +71,12 @@ class AgeFilter(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new allowlist """ - needed_pairs = [(p, '1d') for p in pairlist if p not in self._symbolsChecked] + needed_pairs = [ + (p, '1d') for p in pairlist + if p not in self._symbolsChecked and p not in self._symbolsCheckFailed] if not needed_pairs: - return pairlist + # Remove pairs that have been removed before + return [p for p in pairlist if p not in self._symbolsCheckFailed] since_days = -( self._max_days_listed if self._max_days_listed else self._min_days_listed @@ -118,5 +123,6 @@ class AgeFilter(IPairList): " or more than " f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}" ) if self._max_days_listed else ''), logger.info) + self._symbolsCheckFailed[pair] = arrow.utcnow().int_timestamp * 1000 return False return False diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 46a289ae6..301ee57ab 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -2,7 +2,7 @@ Performance pair list filter """ import logging -from typing import Dict, List +from typing import Any, Dict, List import pandas as pd @@ -15,6 +15,13 @@ logger = logging.getLogger(__name__) class PerformanceFilter(IPairList): + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._minutes = pairlistconfig.get('minutes', 0) + @property def needstickers(self) -> bool: """ @@ -40,7 +47,7 @@ class PerformanceFilter(IPairList): """ # Get the trading performance for pairs from database try: - performance = pd.DataFrame(Trade.get_overall_performance()) + performance = pd.DataFrame(Trade.get_overall_performance(self._minutes)) except AttributeError: # Performancefilter does not work in backtesting. self.log_once("PerformanceFilter is not available in this mode.", logger.warning) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 3adbebc16..46187f571 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -46,6 +46,12 @@ class Balances(BaseModel): value: float stake: str note: str + starting_capital: float + starting_capital_ratio: float + starting_capital_pct: float + starting_capital_fiat: float + starting_capital_fiat_ratio: float + starting_capital_fiat_pct: float class Count(BaseModel): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 8128313b7..af850a89f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -459,6 +459,9 @@ class RPC: raise RPCException('Error getting current tickers.') self._freqtrade.wallets.update(require_update=False) + starting_capital = self._freqtrade.wallets.get_starting_balance() + starting_cap_fiat = self._fiat_converter.convert_amount( + starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0 for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: @@ -494,15 +497,25 @@ class RPC: else: raise RPCException('All balances are zero.') - symbol = fiat_display_currency - value = self._fiat_converter.convert_amount(total, stake_currency, - symbol) if self._fiat_converter else 0 + value = self._fiat_converter.convert_amount( + total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 + + starting_capital_ratio = 0.0 + starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0 + starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 + return { 'currencies': output, 'total': total, - 'symbol': symbol, + 'symbol': fiat_display_currency, 'value': value, 'stake': stake_currency, + 'starting_capital': starting_capital, + 'starting_capital_ratio': starting_capital_ratio, + 'starting_capital_pct': round(starting_capital_ratio * 100, 2), + 'starting_capital_fiat': starting_cap_fiat, + 'starting_capital_fiat_ratio': starting_cap_fiat_ratio, + 'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2), 'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else '' } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a988d2b60..19c58b63d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -603,12 +603,15 @@ class Telegram(RPCHandler): output = '' if self._config['dry_run']: - output += ( - f"*Warning:* Simulated balances in Dry Mode.\n" - "This mode is still experimental!\n" - "Starting capital: " - f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" - ) + output += "*Warning:* Simulated balances in Dry Mode.\n" + + output += ("Starting capital: " + f"`{result['starting_capital']}` {self._config['stake_currency']}" + ) + output += (f" `{result['starting_capital_fiat']}` " + f"{self._config['fiat_display_currency']}.\n" + ) if result['starting_capital_fiat'] > 0 else '.\n' + total_dust_balance = 0 total_dust_currencies = 0 for curr in result['currencies']: @@ -641,9 +644,12 @@ class Telegram(RPCHandler): f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n") output += ("\n*Estimated Value*:\n" - f"\t`{result['stake']}: {result['total']: .8f}`\n" + f"\t`{result['stake']}: " + f"{round_coin_value(result['total'], result['stake'], False)}`" + f" `({result['starting_capital_pct']}%)`\n" f"\t`{result['symbol']}: " - f"{round_coin_value(result['value'], result['symbol'], False)}`\n") + f"{round_coin_value(result['value'], result['symbol'], False)}`" + f" `({result['starting_capital_fiat_pct']}%)`\n") self._send_msg(output, reload_able=True, callback_path="update_balance", query=update.callback_query) except RPCException as e: diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index be655fc33..2ea0ad2b4 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,5 +3,7 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) +from freqtrade.strategy.informative_decorator import informative from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open +from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute, + stoploss_from_open) diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py new file mode 100644 index 000000000..4c5f21108 --- /dev/null +++ b/freqtrade/strategy/informative_decorator.py @@ -0,0 +1,128 @@ +from typing import Any, Callable, NamedTuple, Optional, Union + +from pandas import DataFrame + +from freqtrade.exceptions import OperationalException +from freqtrade.strategy.strategy_helper import merge_informative_pair + + +PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] + + +class InformativeData(NamedTuple): + asset: Optional[str] + timeframe: str + fmt: Union[str, Callable[[Any], str], None] + ffill: bool + + +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[Any], str]]] = None, + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to: + * {base}_{quote}_{column}_{timeframe} if asset is specified. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. + """ + _asset = asset + _timeframe = timeframe + _fmt = fmt + _ffill = ffill + + def decorator(fn: PopulateIndicators): + informative_pairs = getattr(fn, '_ft_informative', []) + informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill)) + setattr(fn, '_ft_informative', informative_pairs) + return fn + return decorator + + +def _format_pair_name(config, pair: str) -> str: + return pair.format(stake_currency=config['stake_currency'], + stake=config['stake_currency']).upper() + + +def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict, + inf_data: InformativeData, + populate_indicators: PopulateIndicators): + asset = inf_data.asset or '' + timeframe = inf_data.timeframe + fmt = inf_data.fmt + config = strategy.config + + if asset: + # Insert stake currency if needed. + asset = _format_pair_name(config, asset) + else: + # Not specifying an asset will define informative dataframe for current pair. + asset = metadata['pair'] + + if '/' in asset: + base, quote = asset.split('/') + else: + # When futures are supported this may need reevaluation. + # base, quote = asset, '' + raise OperationalException('Not implemented.') + + # Default format. This optimizes for the common case: informative pairs using same stake + # currency. When quote currency matches stake currency, column name will omit base currency. + # This allows easily reconfiguring strategy to use different base currency. In a rare case + # where it is desired to keep quote currency in column name at all times user should specify + # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. + if not fmt: + fmt = '{column}_{timeframe}' # Informatives of current pair + if inf_data.asset: + fmt = '{base}_{quote}_' + fmt # Informatives of other pairs + + inf_metadata = {'pair': asset, 'timeframe': timeframe} + inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) + inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) + + formatter: Any = None + if callable(fmt): + formatter = fmt # A custom user-specified formatter function. + else: + formatter = fmt.format # A default string formatter. + + fmt_args = { + 'BASE': base.upper(), + 'QUOTE': quote.upper(), + 'base': base.lower(), + 'quote': quote.lower(), + 'asset': asset, + 'timeframe': timeframe, + } + inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args), + inplace=True) + + date_column = formatter(column='date', **fmt_args) + if date_column in dataframe.columns: + raise OperationalException(f'Duplicate column name {date_column} exists in ' + f'dataframe! Ensure column names are unique!') + dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe, + ffill=inf_data.ffill, append_timeframe=False, + date_column=date_column) + return dataframe diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2b227c849..34cf9f749 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -19,6 +19,9 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.hyper import HyperStrategyMixin +from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators, + _create_and_merge_informative_pair, + _format_pair_name) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -118,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) # and wallets - access to the current balance. - dp: Optional[DataProvider] = None + dp: Optional[DataProvider] wallets: Optional[Wallets] = None # Filled from configuration stake_currency: str @@ -134,6 +137,24 @@ class IStrategy(ABC, HyperStrategyMixin): self._last_candle_seen_per_pair: Dict[str, datetime] = {} super().__init__(config) + # Gather informative pairs from @informative-decorated methods. + self._ft_informative: List[Tuple[InformativeData, PopulateIndicators]] = [] + for attr_name in dir(self.__class__): + cls_method = getattr(self.__class__, attr_name) + if not callable(cls_method): + continue + informative_data_list = getattr(cls_method, '_ft_informative', None) + if not isinstance(informative_data_list, list): + # Type check is required because mocker would return a mock object that evaluates to + # True, confusing this code. + continue + strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe) + for informative_data in informative_data_list: + if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes: + raise OperationalException('Informative timeframe must be equal or higher than ' + 'strategy timeframe!') + self._ft_informative.append((informative_data, cls_method)) + @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -379,6 +400,23 @@ class IStrategy(ABC, HyperStrategyMixin): # END - Intended to be overridden by strategy ### + def gather_informative_pairs(self) -> ListPairsWithTimeframes: + """ + Internal method which gathers all informative pairs (user or automatically defined). + """ + informative_pairs = self.informative_pairs() + for inf_data, _ in self._ft_informative: + if inf_data.asset: + pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe) + informative_pairs.append(pair_tf) + else: + if not self.dp: + raise OperationalException('@informative decorator with unspecified asset ' + 'requires DataProvider instance.') + for pair in self.dp.current_whitelist(): + informative_pairs.append((pair, inf_data.timeframe)) + return list(set(informative_pairs)) + def get_strategy_name(self) -> str: """ Returns strategy class name @@ -461,12 +499,12 @@ class IStrategy(ABC, HyperStrategyMixin): self.dp._set_cached_df(pair, self.timeframe, dataframe) else: logger.debug("Skipping TA Analysis for already analyzed candle") - dataframe['buy'] = 0 - dataframe['sell'] = 0 - dataframe['enter_short'] = 0 - dataframe['exit_short'] = 0 - dataframe['buy_tag'] = None - dataframe['short_tag'] = None + dataframe[SignalType.ENTER_LONG.value] = 0 + dataframe[SignalType.EXIT_LONG.value] = 0 + dataframe[SignalType.ENTER_SHORT.value] = 0 + dataframe[SignalType.EXIT_SHORT.value] = 0 + dataframe[SignalTagType.BUY_TAG.value] = None + dataframe[SignalTagType.SHORT_TAG.value] = None # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) @@ -862,10 +900,11 @@ class IStrategy(ABC, HyperStrategyMixin): Does not run advise_buy or advise_sell! Used by optimize operations only, not during dry / live runs. Using .copy() to get a fresh copy of the dataframe for every strategy run. + Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show. Has positive effects on memory usage for whatever reason - also when using only one strategy. """ - return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair}) + return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair}).copy() for pair, pair_data in data.items()} def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -877,6 +916,12 @@ class IStrategy(ABC, HyperStrategyMixin): :return: a Dataframe with all mandatory indicators for the strategies """ logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") + + # call populate_indicators_Nm() which were tagged with @informative decorator. + for inf_data, populate_fn in self._ft_informative: + dataframe = _create_and_merge_informative_pair( + self, dataframe, metadata, inf_data, populate_fn) + if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 9c4d2bf2d..126a9c6c5 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -5,7 +5,9 @@ from freqtrade.exchange import timeframe_to_minutes def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, - timeframe: str, timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: + timeframe: str, timeframe_inf: str, ffill: bool = True, + append_timeframe: bool = True, + date_column: str = 'date') -> pd.DataFrame: """ Correctly merge informative samples to the original dataframe, avoiding lookahead bias. @@ -25,6 +27,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :param timeframe: Timeframe of the original pair sample. :param timeframe_inf: Timeframe of the informative pair sample. :param ffill: Forwardfill missing values - optional but usually required + :param append_timeframe: Rename columns by appending timeframe. + :param date_column: A custom date column name. :return: Merged dataframe :raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe """ @@ -33,25 +37,29 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, minutes = timeframe_to_minutes(timeframe) if minutes == minutes_inf: # No need to forwardshift if the timeframes are identical - informative['date_merge'] = informative["date"] + informative['date_merge'] = informative[date_column] elif minutes < minutes_inf: # Subtract "small" timeframe so merging is not delayed by 1 small candle # Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073 informative['date_merge'] = ( - informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm') + informative[date_column] + pd.to_timedelta(minutes_inf, 'm') - + pd.to_timedelta(minutes, 'm') ) else: raise ValueError("Tried to merge a faster timeframe to a slower timeframe." "This would create new rows, and can throw off your regular indicators.") # Rename columns to be unique - informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] + date_merge = 'date_merge' + if append_timeframe: + date_merge = f'date_merge_{timeframe_inf}' + informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point dataframe = pd.merge(dataframe, informative, left_on='date', - right_on=f'date_merge_{timeframe_inf}', how='left') - dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1) + right_on=date_merge, how='left') + dataframe = dataframe.drop(date_merge, axis=1) if ffill: dataframe = dataframe.ffill() @@ -97,3 +105,28 @@ def stoploss_from_open( return min(stoploss, 0.0) else: return max(stoploss, 0.0) + + +def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: + """ + Given current price and desired stop price, return a stop loss value that is relative to current + price. + + The requested stop can be positive for a stop above the open price, or negative for + a stop below the open price. The return value is always >= 0. + + Returns 0 if the resulting stop price would be above the current price. + + :param stop_rate: Stop loss price. + :param current_rate: Current asset price. + :return: Positive stop loss value relative to current price + """ + + # formula is undefined for current_rate 0, return maximum value + if current_rate == 0: + return 1 + + stoploss = 1 - (stop_rate / current_rate) + + # negative stoploss values indicate the requested stop price is higher than the current price + return max(stoploss, 0.0) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 06d7cbc5c..3feff75c6 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -122,7 +122,7 @@ class {{ strategy }}(IStrategy): {{ buy_trend | indent(16) }} (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'buy'] = 1 + 'enter_long'] = 1 return dataframe @@ -138,6 +138,6 @@ class {{ strategy }}(IStrategy): {{ sell_trend | indent(16) }} (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'sell'] = 1 + 'exit_long'] = 1 return dataframe {{ additional_methods | indent(4) }} diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index b2d130059..13df9c2a8 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -354,7 +354,7 @@ class SampleStrategy(IStrategy): (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'buy'] = 1 + 'enter_long'] = 1 dataframe.loc[ ( @@ -383,7 +383,8 @@ class SampleStrategy(IStrategy): (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'sell'] = 1 + + 'exit_long'] = 1 dataframe.loc[ ( diff --git a/requirements-dev.txt b/requirements-dev.txt index 34d5607f3..4859e1cc6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,6 +14,8 @@ pytest-cov==2.12.1 pytest-mock==3.6.1 pytest-random-order==1.0.4 isort==5.9.3 +# For datetime mocking +time-machine==2.4.0 # Convert jupyter notebooks to markdown documents nbconvert==6.1.0 diff --git a/setup.sh b/setup.sh index 217500569..aee7c80b5 100755 --- a/setup.sh +++ b/setup.sh @@ -62,7 +62,7 @@ function updateenv() { then REQUIREMENTS_PLOT="-r requirements-plot.txt" fi - if [ "${SYS_ARCH}" == "armv7l" ]; then + if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then echo "Detected Raspberry, installing cython, skipping hyperopt installation." ${PYTHON} -m pip install --upgrade cython else diff --git a/tests/conftest.py b/tests/conftest.py index 2123c089a..4a0ad4c97 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,7 @@ from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo -from freqtrade.enums import RunMode +from freqtrade.enums import Collateral, RunMode, TradingMode from freqtrade.enums.signaltype import SignalDirection from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot @@ -90,7 +90,13 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None: +def patch_exchange( + mocker, + api_mock=None, + id='binance', + mock_markets=True, + mock_supported_modes=True +) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -99,10 +105,22 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) + if mock_markets: mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=get_markets())) + if mock_supported_modes: + mocker.patch( + f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_collateral_pairs', + PropertyMock(return_value=[ + (TradingMode.MARGIN, Collateral.CROSS), + (TradingMode.MARGIN, Collateral.ISOLATED), + (TradingMode.FUTURES, Collateral.CROSS), + (TradingMode.FUTURES, Collateral.ISOLATED) + ]) + ) + if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: @@ -110,8 +128,8 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No def get_patched_exchange(mocker, config, api_mock=None, id='binance', - mock_markets=True) -> Exchange: - patch_exchange(mocker, api_mock, id, mock_markets) + mock_markets=True, mock_supported_modes=True) -> Exchange: + patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes) config['exchange']['name'] = id try: exchange = ExchangeResolver.load_exchange(id, config) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 5a1087534..0c3e86fdd 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, PropertyMock import ccxt import pytest -from freqtrade.enums import TradingMode +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -48,13 +48,20 @@ def test_stoploss_order_binance( amount=1, stop_price=190, side=side, - order_types={'stoploss_on_exchange_limit_ratio': 1.05} + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + leverage=1.0 ) api_mock.create_order.reset_mock() order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio} - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types=order_types, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types=order_types, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -71,17 +78,31 @@ def test_stoploss_order_binance( with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) def test_stoploss_order_dry_run_binance(default_conf, mocker): @@ -94,12 +115,25 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') with pytest.raises(OperationalException): - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side="sell", - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side="sell", + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + leverage=1.0 + ) api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side="sell", + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -194,6 +228,9 @@ def test_fill_leverage_brackets_binance(default_conf, mocker): [1000000.0, 0.5]], }) + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['collateral'] = Collateral.ISOLATED exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange.fill_leverage_brackets() @@ -236,12 +273,59 @@ def test_fill_leverage_brackets_binance(default_conf, mocker): ) +def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): + api_mock = MagicMock() + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['collateral'] = Collateral.ISOLATED + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange.fill_leverage_brackets() + + leverage_brackets = { + "1000SHIB/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "1INCH/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "AAVE/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "ADA/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ] + } + + for key, value in leverage_brackets.items(): + assert exchange._leverage_brackets[key] == value + + def test__set_leverage_binance(mocker, default_conf): api_mock = MagicMock() api_mock.set_leverage = MagicMock() type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) - + default_conf['dry_run'] = False exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN) @@ -288,3 +372,15 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): assert exchange._api_async.fetch_ohlcv.call_count == 2 assert res == ohlcv assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) + + +@pytest.mark.parametrize("trading_mode,collateral,config", [ + ("", "", {}), + ("margin", "cross", {"options": {"defaultType": "margin"}}), + ("futures", "isolated", {"options": {"defaultType": "future"}}), +]) +def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config): + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = collateral + exchange = get_patched_exchange(mocker, default_conf, id="binance") + assert exchange._ccxt_config == config diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 535726b4b..cc9b5130d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -132,6 +132,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog) assert ex._api.headers == {'hello': 'world'} + assert ex._ccxt_config == {} Exchange._headers = {} # TODO-lev: Test with options @@ -403,7 +404,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0) assert isclose(result, expected_result/3) - # TODO-lev: Min stake for base, kraken and ftx # min amount is set markets["ETH/BTC"]["limits"] = { @@ -420,27 +420,21 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) assert isclose(result, expected_result/5) - # TODO-lev: Min stake for base, kraken and ftx - - # min amount and cost are set (cost is minimal) - markets["ETH/BTC"]["limits"] = { - 'cost': {'min': 2}, 'amount': {'min': 2} } mocker.patch( 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - expected_result = max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)) + result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) + expected_result=max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) # With Leverage - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) + result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) assert isclose(result, expected_result/10) - # TODO-lev: Min stake for base, kraken and ftx # min amount and cost are set (amount is minial) - markets["ETH/BTC"]["limits"] = { + markets["ETH/BTC"]["limits"]={ 'cost': {'min': 8}, 'amount': {'min': 2} } @@ -448,39 +442,28 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - expected_result = max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)) + result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) + expected_result=max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) # With Leverage - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) + result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) assert isclose(result, expected_result/7.0) - # TODO-lev: Min stake for base, kraken and ftx - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - expected_result = max(8, 2 * 2) * 1.5 + result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) + expected_result=max(8, 2 * 2) * 1.5 assert isclose(result, expected_result) # With Leverage - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) + result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) assert isclose(result, expected_result/8.0) - # TODO-lev: Min stake for base, kraken and ftx - - # Really big stoploss - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) - expected_result = max(8, 2 * 2) * 1.5 assert isclose(result, expected_result) # With Leverage - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) + result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) assert isclose(result, expected_result/12) - # TODO-lev: Min stake for base, kraken and ftx - - -def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: - exchange = get_patched_exchange(mocker, default_conf, id="binance") - stoploss = -0.05 - markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} + stoploss=-0.05 + markets={'ETH/BTC': {'symbol': 'ETH/BTC'}} # Real Binance data - markets["ETH/BTC"]["limits"] = { + markets["ETH/BTC"]["limits"]={ 'cost': {'min': 0.0001}, 'amount': {'min': 0.001} } @@ -488,28 +471,27 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) - result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) + result=exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) + expected_result=max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) assert round(result, 8) == round(expected_result, 8) - result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) + result=exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) assert round(result, 8) == round(expected_result/3, 8) - # TODO-lev: Min stake for base, kraken and ftx def test_set_sandbox(default_conf, mocker): """ Test working scenario """ - api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value={ + api_mock=MagicMock() + api_mock.load_markets=MagicMock(return_value = { 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' }) - url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com", + url_mock=PropertyMock(return_value = {'test': "api-public.sandbox.gdax.com", 'api': 'https://api.gdax.com'}) - type(api_mock).urls = url_mock - exchange = get_patched_exchange(mocker, default_conf, api_mock) - liveurl = exchange._api.urls['api'] - default_conf['exchange']['sandbox'] = True + type(api_mock).urls=url_mock + exchange=get_patched_exchange(mocker, default_conf, api_mock) + liveurl=exchange._api.urls['api'] + default_conf['exchange']['sandbox']=True exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') assert exchange._api.urls['api'] != liveurl @@ -518,16 +500,16 @@ def test_set_sandbox_exception(default_conf, mocker): """ Test Fail scenario """ - api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value={ + api_mock=MagicMock() + api_mock.load_markets=MagicMock(return_value = { 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' }) - url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'}) - type(api_mock).urls = url_mock + url_mock=PropertyMock(return_value = {'api': 'https://api.gdax.com'}) + type(api_mock).urls=url_mock - with pytest.raises(OperationalException, match=r'does not provide a sandbox api'): - exchange = get_patched_exchange(mocker, default_conf, api_mock) - default_conf['exchange']['sandbox'] = True + with pytest.raises(OperationalException, match = r'does not provide a sandbox api'): + exchange=get_patched_exchange(mocker, default_conf, api_mock) + default_conf['exchange']['sandbox']=True exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') @@ -537,13 +519,13 @@ def test__load_async_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange._load_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - exchange = Exchange(default_conf) - exchange._api_async.load_markets = get_mock_coro(None) + exchange=Exchange(default_conf) + exchange._api_async.load_markets=get_mock_coro(None) exchange._load_async_markets() assert exchange._api_async.load_markets.call_count == 1 caplog.set_level(logging.DEBUG) - exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef")) + exchange._api_async.load_markets=Mock(side_effect = ccxt.BaseError("deadbeef")) exchange._load_async_markets() assert log_has('Could not load async markets. Reason: deadbeef', caplog) @@ -551,8 +533,8 @@ def test__load_async_markets(default_conf, mocker, caplog): def test__load_markets(default_conf, mocker, caplog): caplog.set_level(logging.INFO) - api_mock = MagicMock() - api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError")) + api_mock=MagicMock() + api_mock.load_markets=MagicMock(side_effect = ccxt.BaseError("SomeError")) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') @@ -561,28 +543,28 @@ def test__load_markets(default_conf, mocker, caplog): Exchange(default_conf) assert log_has('Unable to initialize markets.', caplog) - expected_return = {'ETH/BTC': 'available'} - api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value=expected_return) + expected_return={'ETH/BTC': 'available'} + api_mock=MagicMock() + api_mock.load_markets=MagicMock(return_value = expected_return) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - default_conf['exchange']['pair_whitelist'] = ['ETH/BTC'] - ex = Exchange(default_conf) + default_conf['exchange']['pair_whitelist']=['ETH/BTC'] + ex=Exchange(default_conf) assert ex.markets == expected_return def test_reload_markets(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) - initial_markets = {'ETH/BTC': {}} - updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} + initial_markets={'ETH/BTC': {}} + updated_markets={'ETH/BTC': {}, "LTC/BTC": {}} - api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value=initial_markets) - default_conf['exchange']['markets_refresh_interval'] = 10 - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", - mock_markets=False) - exchange._load_async_markets = MagicMock() - exchange._last_markets_refresh = arrow.utcnow().int_timestamp + api_mock=MagicMock() + api_mock.load_markets=MagicMock(return_value = initial_markets) + default_conf['exchange']['markets_refresh_interval']=10 + exchange=get_patched_exchange(mocker, default_conf, api_mock, id = "binance", + mock_markets = False) + exchange._load_async_markets=MagicMock() + exchange._last_markets_refresh=arrow.utcnow().int_timestamp assert exchange.markets == initial_markets @@ -591,9 +573,9 @@ def test_reload_markets(default_conf, mocker, caplog): assert exchange.markets == initial_markets assert exchange._load_async_markets.call_count == 0 - api_mock.load_markets = MagicMock(return_value=updated_markets) + api_mock.load_markets=MagicMock(return_value = updated_markets) # more than 10 minutes have passed, reload is executed - exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60 + exchange._last_markets_refresh=arrow.utcnow().int_timestamp - 15 * 60 exchange.reload_markets() assert exchange.markets == updated_markets assert exchange._load_async_markets.call_count == 1 @@ -603,10 +585,10 @@ def test_reload_markets(default_conf, mocker, caplog): def test_reload_markets_exception(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) - api_mock = MagicMock() - api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError")) - default_conf['exchange']['markets_refresh_interval'] = 10 - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + api_mock=MagicMock() + api_mock.load_markets=MagicMock(side_effect = ccxt.NetworkError("LoadError")) + default_conf['exchange']['markets_refresh_interval']=10 + exchange=get_patched_exchange(mocker, default_conf, api_mock, id = "binance") # less than 10 minutes have passed, no reload exchange.reload_markets() @@ -614,11 +596,11 @@ def test_reload_markets_exception(default_conf, mocker, caplog): assert log_has_re(r"Could not reload markets.*", caplog) -@pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT']) +@ pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT']) def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog): - default_conf['stake_currency'] = stake_currency - api_mock = MagicMock() - type(api_mock).load_markets = MagicMock(return_value={ + default_conf['stake_currency']=stake_currency + api_mock=MagicMock() + type(api_mock).load_markets=MagicMock(return_value = { 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, }) @@ -630,9 +612,9 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog): def test_validate_stakecurrency_error(default_conf, mocker, caplog): - default_conf['stake_currency'] = 'XRP' - api_mock = MagicMock() - type(api_mock).load_markets = MagicMock(return_value={ + default_conf['stake_currency']='XRP' + api_mock=MagicMock() + type(api_mock).load_markets=MagicMock(return_value = { 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, }) @@ -1004,7 +986,13 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) order = exchange.create_dry_run_order( - pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) + pair='ETH/BTC', + ordertype='limit', + side=side, + amount=1, + rate=200, + leverage=1.0 + ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side @@ -1027,7 +1015,13 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, ) order = exchange.create_dry_run_order( - pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice) + pair='LTC/USDT', + ordertype='limit', + side=side, + amount=1, + rate=startprice, + leverage=1.0 + ) assert order_book_l2_usd.call_count == 1 assert 'id' in order assert f'dry_run_{side}_' in order["id"] @@ -1073,7 +1067,13 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou ) order = exchange.create_dry_run_order( - pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate) + pair='LTC/USDT', + ordertype='market', + side=side, + amount=amount, + rate=rate, + leverage=1.0 + ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side @@ -1083,10 +1083,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou assert round(order["average"], 4) == round(endprice, 4) -@pytest.mark.parametrize("side", [ - ("buy"), - ("sell") -]) +@pytest.mark.parametrize("side", ["buy", "sell"]) @pytest.mark.parametrize("ordertype,rate,marketprice", [ ("market", None, None), ("market", 200, True), @@ -1108,9 +1105,17 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange._set_leverage = MagicMock() + exchange.set_margin_mode = MagicMock() order = exchange.create_order( - pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200) + pair='ETH/BTC', + ordertype=ordertype, + side=side, + amount=1, + rate=200, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -1120,6 +1125,21 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, assert api_mock.create_order.call_args[0][2] == side assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][4] is rate + assert exchange._set_leverage.call_count == 0 + assert exchange.set_margin_mode.call_count == 0 + + exchange.trading_mode = TradingMode.FUTURES + order = exchange.create_order( + pair='ETH/BTC', + ordertype=ordertype, + side=side, + amount=1, + rate=200, + leverage=3.0 + ) + + assert exchange._set_leverage.call_count == 1 + assert exchange.set_margin_mode.call_count == 1 def test_buy_dry_run(default_conf, mocker): @@ -2658,7 +2678,14 @@ def test_get_fee(default_conf, mocker, exchange_name): def test_stoploss_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='bittrex') with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side="sell", + leverage=1.0 + ) with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): exchange.stoploss_adjust(1, {}, side="sell") @@ -3006,7 +3033,6 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: (3, 5, 5), (4, 5, 2), (5, 5, 1), - ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected @@ -3018,7 +3044,7 @@ def test_calculate_backoff(retrycount, max_retries, expected): (20.0, 5.0, 4.0), (100.0, 100.0, 1.0) ]) -def test_apply_leverage_to_stake_amount( +def test_get_stake_amount_considering_leverage( exchange, stake_amount, leverage, @@ -3027,7 +3053,8 @@ def test_apply_leverage_to_stake_amount( default_conf ): exchange = get_patched_exchange(mocker, default_conf, id=exchange) - assert exchange._apply_leverage_to_stake_amount(stake_amount, leverage) == min_stake_with_lev + assert exchange._get_stake_amount_considering_leverage( + stake_amount, leverage) == min_stake_with_lev @pytest.mark.parametrize("exchange_name,trading_mode", [ @@ -3040,6 +3067,7 @@ def test__set_leverage(mocker, default_conf, exchange_name, trading_mode): api_mock = MagicMock() api_mock.set_leverage = MagicMock() type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + default_conf['dry_run'] = False ccxt_exceptionhandlers( mocker, @@ -3063,6 +3091,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): api_mock = MagicMock() api_mock.set_margin_mode = MagicMock() type(api_mock).has = PropertyMock(return_value={'setMarginMode': True}) + default_conf['dry_run'] = False ccxt_exceptionhandlers( mocker, @@ -3117,7 +3146,8 @@ def test_validate_trading_mode_and_collateral( collateral, exception_thrown ): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange( + mocker, default_conf, id=exchange_name, mock_supported_modes=False) if (exception_thrown): with pytest.raises(OperationalException): exchange.validate_trading_mode_and_collateral(trading_mode, collateral) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index a8d715c9a..9cbac5211 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -37,8 +37,14 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side=side, - order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side=side, + order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio}, + leverage=1.0 + ) assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE @@ -52,7 +58,14 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -65,8 +78,13 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={'stoploss': 'limit'}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={'stoploss': 'limit'}, side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -83,17 +101,32 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) @pytest.mark.parametrize('side', [("sell"), ("buy")]) @@ -107,7 +140,14 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker, side): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -228,26 +268,3 @@ def test_fill_leverage_brackets_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id="ftx") exchange.fill_leverage_brackets() assert exchange._leverage_brackets == {} - - -@pytest.mark.parametrize("trading_mode", [ - (TradingMode.MARGIN), - (TradingMode.FUTURES) -]) -def test__set_leverage(mocker, default_conf, trading_mode): - - api_mock = MagicMock() - api_mock.set_leverage = MagicMock() - type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) - - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - "ftx", - "_set_leverage", - "set_leverage", - pair="XRP/USDT", - leverage=5.0, - trading_mode=trading_mode - ) diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 0055a0dd1..15edbd845 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -197,7 +197,9 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr order_types={ 'stoploss': ordertype, 'stoploss_on_exchange_limit_ratio': 0.99 - }) + }, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -221,17 +223,32 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) @pytest.mark.parametrize('side', ['buy', 'sell']) @@ -245,7 +262,14 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker, side): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -307,15 +331,3 @@ def test_fill_leverage_brackets_kraken(default_conf, mocker): 'XLTCUSDT': [1], 'LTC/ETH': [1] } - - -def test__set_leverage_kraken(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, id="kraken") - exchange._set_leverage(1) - assert 'leverage' not in exchange._params - exchange._set_leverage(3) - assert exchange._params['leverage'] == 3 - exchange._set_leverage(1.0) - assert 'leverage' not in exchange._params - exchange._set_leverage(3.0) - assert exchange._params['leverage'] == 3 diff --git a/tests/leverage/test_leverage.py b/tests/leverage/test_interest.py similarity index 83% rename from tests/leverage/test_leverage.py rename to tests/leverage/test_interest.py index 7b7ca0f9b..c7e787bdb 100644 --- a/tests/leverage/test_leverage.py +++ b/tests/leverage/test_interest.py @@ -22,9 +22,10 @@ twentyfive_hours = Decimal(25.0) ('kraken', 0.00025, five_hours, 0.045), ('kraken', 0.00025, twentyfive_hours, 0.12), # FTX - # TODO-lev: - implement FTX tests - # ('ftx', Decimal(0.0005), ten_mins, 0.06), - # ('ftx', Decimal(0.0005), five_hours, 0.045), + ('ftx', 0.0005, ten_mins, 0.00125), + ('ftx', 0.00025, ten_mins, 0.000625), + ('ftx', 0.00025, five_hours, 0.003125), + ('ftx', 0.00025, twentyfive_hours, 0.015625), ]) def test_interest(exchange, interest_rate, hours, expected): borrowed = Decimal(60.0) diff --git a/tests/optimize/hyperopts/short_hyperopt.py b/tests/optimize/hyperopts/short_hyperopt.py deleted file mode 100644 index df39188e0..000000000 --- a/tests/optimize/hyperopts/short_hyperopt.py +++ /dev/null @@ -1,271 +0,0 @@ -# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement - -from functools import reduce -from typing import Any, Callable, Dict, List - -import talib.abstract as ta -from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer - -import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.optimize.hyperopt_interface import IHyperOpt - - -class DefaultHyperOpt(IHyperOpt): - """ - Default hyperopt provided by the Freqtrade bot. - You can override it with your own Hyperopt - """ - @staticmethod - def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Add several indicators needed for buy and sell strategies defined below. - """ - # ADX - dataframe['adx'] = ta.ADX(dataframe) - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - # MFI - dataframe['mfi'] = ta.MFI(dataframe) - # RSI - dataframe['rsi'] = ta.RSI(dataframe) - # Stochastic Fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - # Minus-DI - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # Bollinger bands - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_upperband'] = bollinger['upper'] - # SAR - dataframe['sar'] = ta.SAR(dataframe) - - return dataframe - - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Buy strategy Hyperopt will build and use. - """ - long_conditions = [] - short_conditions = [] - - # GUARDS AND TRENDS - if 'mfi-enabled' in params and params['mfi-enabled']: - long_conditions.append(dataframe['mfi'] < params['mfi-value']) - short_conditions.append(dataframe['mfi'] > params['short-mfi-value']) - if 'fastd-enabled' in params and params['fastd-enabled']: - long_conditions.append(dataframe['fastd'] < params['fastd-value']) - short_conditions.append(dataframe['fastd'] > params['short-fastd-value']) - if 'adx-enabled' in params and params['adx-enabled']: - long_conditions.append(dataframe['adx'] > params['adx-value']) - short_conditions.append(dataframe['adx'] < params['short-adx-value']) - if 'rsi-enabled' in params and params['rsi-enabled']: - long_conditions.append(dataframe['rsi'] < params['rsi-value']) - short_conditions.append(dataframe['rsi'] > params['short-rsi-value']) - - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'boll': - long_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - short_conditions.append(dataframe['close'] > dataframe['bb_upperband']) - if params['trigger'] == 'macd_cross_signal': - long_conditions.append(qtpylib.crossed_above( - dataframe['macd'], - dataframe['macdsignal'] - )) - short_conditions.append(qtpylib.crossed_below( - dataframe['macd'], - dataframe['macdsignal'] - )) - if params['trigger'] == 'sar_reversal': - long_conditions.append(qtpylib.crossed_above( - dataframe['close'], - dataframe['sar'] - )) - short_conditions.append(qtpylib.crossed_below( - dataframe['close'], - dataframe['sar'] - )) - - if long_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, long_conditions), - 'buy'] = 1 - - if short_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, short_conditions), - 'enter_short'] = 1 - - return dataframe - - return populate_buy_trend - - @staticmethod - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching buy strategy parameters. - """ - return [ - Integer(10, 25, name='mfi-value'), - Integer(15, 45, name='fastd-value'), - Integer(20, 50, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Integer(75, 90, name='short-mfi-value'), - Integer(55, 85, name='short-fastd-value'), - Integer(50, 80, name='short-adx-value'), - Integer(60, 80, name='short-rsi-value'), - Categorical([True, False], name='mfi-enabled'), - Categorical([True, False], name='fastd-enabled'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger') - ] - - @staticmethod - def sell_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the sell strategy parameters to be used by Hyperopt. - """ - def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Sell strategy Hyperopt will build and use. - """ - exit_long_conditions = [] - exit_short_conditions = [] - - # GUARDS AND TRENDS - if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: - exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value']) - exit_short_conditions.append(dataframe['mfi'] < params['exit-short-mfi-value']) - if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: - exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value']) - exit_short_conditions.append(dataframe['fastd'] < params['exit-short-fastd-value']) - if 'sell-adx-enabled' in params and params['sell-adx-enabled']: - exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value']) - exit_short_conditions.append(dataframe['adx'] > params['exit-short-adx-value']) - if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: - exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value']) - exit_short_conditions.append(dataframe['rsi'] < params['exit-short-rsi-value']) - - # TRIGGERS - if 'sell-trigger' in params: - if params['sell-trigger'] == 'sell-boll': - exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband']) - exit_short_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['sell-trigger'] == 'sell-macd_cross_signal': - exit_long_conditions.append(qtpylib.crossed_above( - dataframe['macdsignal'], - dataframe['macd'] - )) - exit_short_conditions.append(qtpylib.crossed_below( - dataframe['macdsignal'], - dataframe['macd'] - )) - if params['sell-trigger'] == 'sell-sar_reversal': - exit_long_conditions.append(qtpylib.crossed_above( - dataframe['sar'], - dataframe['close'] - )) - exit_short_conditions.append(qtpylib.crossed_below( - dataframe['sar'], - dataframe['close'] - )) - - if exit_long_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, exit_long_conditions), - 'sell'] = 1 - - if exit_short_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, exit_short_conditions), - 'exit-short'] = 1 - - return dataframe - - return populate_sell_trend - - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters. - """ - return [ - Integer(75, 100, name='sell-mfi-value'), - Integer(50, 100, name='sell-fastd-value'), - Integer(50, 100, name='sell-adx-value'), - Integer(60, 100, name='sell-rsi-value'), - Integer(1, 25, name='exit-short-mfi-value'), - Integer(1, 50, name='exit-short-fastd-value'), - Integer(1, 50, name='exit-short-adx-value'), - Integer(1, 40, name='exit-short-rsi-value'), - Categorical([True, False], name='sell-mfi-enabled'), - Categorical([True, False], name='sell-fastd-enabled'), - Categorical([True, False], name='sell-adx-enabled'), - Categorical([True, False], name='sell-rsi-enabled'), - Categorical(['sell-boll', - 'sell-macd_cross_signal', - 'sell-sar_reversal'], - name='sell-trigger') - ] - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. Should be a copy of same method from strategy. - Must align to populate_indicators in this file. - Only used when --spaces does not include buy space. - """ - dataframe.loc[ - ( - (dataframe['close'] < dataframe['bb_lowerband']) & - (dataframe['mfi'] < 16) & - (dataframe['adx'] > 25) & - (dataframe['rsi'] < 21) - ), - 'buy'] = 1 - - dataframe.loc[ - ( - (dataframe['close'] > dataframe['bb_upperband']) & - (dataframe['mfi'] < 84) & - (dataframe['adx'] > 75) & - (dataframe['rsi'] < 79) - ), - 'enter_short'] = 1 - - return dataframe - - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. Should be a copy of same method from strategy. - Must align to populate_indicators in this file. - Only used when --spaces does not include sell space. - """ - dataframe.loc[ - ( - (qtpylib.crossed_above( - dataframe['macdsignal'], dataframe['macd'] - )) & - (dataframe['fastd'] > 54) - ), - 'sell'] = 1 - - dataframe.loc[ - ( - (qtpylib.crossed_below( - dataframe['macdsignal'], dataframe['macd'] - )) & - (dataframe['fastd'] < 46) - ), - 'exit_short'] = 1 - - return dataframe diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 2aeed5d9e..27496a1fc 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -887,6 +887,10 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: assert hyperopt.backtesting.strategy.buy_rsi.value != 35 assert hyperopt.backtesting.strategy.sell_rsi.value != 74 + hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1' + with pytest.raises(OperationalException, match="Estimator ET1 not supported."): + hyperopt.get_optimizer([], 2) + def test_SKDecimal(): space = SKDecimal(1, 2, decimals=2) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 5f0701a22..1ce8d172c 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -4,6 +4,7 @@ import time from unittest.mock import MagicMock, PropertyMock import pytest +import time_machine from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.exceptions import OperationalException @@ -11,7 +12,8 @@ from freqtrade.persistence import Trade from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver -from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re +from tests.conftest import (create_mock_trades, get_patched_exchange, get_patched_freqtradebot, + log_has, log_has_re) @pytest.fixture(scope="function") @@ -662,6 +664,31 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: assert log_has("PerformanceFilter is not available in this mode.", caplog) +@pytest.mark.usefixtures("init_persistence") +def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None: + whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC') + whitelist_conf['pairlists'] = [ + {"method": "StaticPairList"}, + {"method": "PerformanceFilter", "minutes": 60} + ] + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + exchange = get_patched_exchange(mocker, whitelist_conf) + pm = PairListManager(exchange, whitelist_conf) + pm.refresh_pairlist() + + assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + create_mock_trades(fee) + pm.refresh_pairlist() + assert pm.whitelist == ['XRP/BTC', 'ETH/BTC', 'TKN/BTC'] + + # Move to "outside" of lookback window, so original sorting is restored. + t.move_to("2021-09-01 07:00:00 +00:00") + pm.refresh_pairlist() + assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + + def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] @@ -815,32 +842,63 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history): - ohlcv_data = { - ('ETH/BTC', '1d'): ohlcv_history, - ('TKN/BTC', '1d'): ohlcv_history, - ('LTC/BTC', '1d'): ohlcv_history, - } - mocker.patch.multiple('freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), - ) + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + } + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), + ) - freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) - assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 - freqtrade.pairlists.refresh_pairlist() - assert len(freqtrade.pairlists.whitelist) == 3 - assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 - previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count - freqtrade.pairlists.refresh_pairlist() - assert len(freqtrade.pairlists.whitelist) == 3 - # Called once for XRP/BTC - assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + 1 + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + # Call to XRP/BTC cached + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 2 + + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history.iloc[[0]], + } + mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 + + # Move to next day + t.move_to("2021-09-02 01:00:00 +00:00") + mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 + + # Move another day with fresh mocks (now the pair is old enough) + t.move_to("2021-09-03 01:00:00 +00:00") + # Called once for XRP/BTC + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history, + } + mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 4 + # Called once (only for XRP/BTC) + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 def test_OffsetFilter_error(mocker, whitelist_conf) -> None: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2852486ed..7c98b2df7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -422,20 +422,22 @@ def test_api_stopbuy(botclient): assert ftbot.config['max_open_trades'] == 0 -def test_api_balance(botclient, mocker, rpc_balance): +def test_api_balance(botclient, mocker, rpc_balance, tickers): ftbot, client = botclient ftbot.config['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) + mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") ftbot.wallets.update() rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) - assert "currencies" in rc.json() - assert len(rc.json()["currencies"]) == 5 - assert rc.json()['currencies'][0] == { + response = rc.json() + assert "currencies" in response + assert len(response["currencies"]) == 5 + assert response['currencies'][0] == { 'currency': 'BTC', 'free': 12.0, 'balance': 12.0, @@ -443,6 +445,10 @@ def test_api_balance(botclient, mocker, rpc_balance): 'est_stake': 12.0, 'stake': 'BTC', } + assert 'starting_capital' in response + assert 'starting_capital_fiat' in response + assert 'starting_capital_pct' in response + assert 'starting_capital_ratio' in response def test_api_count(botclient, mocker, ticker, fee, markets): @@ -1218,6 +1224,7 @@ def test_api_strategies(botclient): assert_response(rc) assert rc.json() == {'strategies': [ 'HyperoptableStrategy', + 'InformativeDecoratorTest', 'StrategyTestV2', 'TestStrategyLegacyV1' ]} diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2013dad7d..21f1cd000 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -576,6 +576,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'total': 100.0, 'symbol': 100.0, 'value': 1000.0, + 'starting_capital': 1000, + 'starting_capital_fiat': 1000, }) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py new file mode 100644 index 000000000..a32ad79e8 --- /dev/null +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -0,0 +1,75 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +from pandas import DataFrame + +from freqtrade.strategy import informative, merge_informative_pair +from freqtrade.strategy.interface import IStrategy + + +class InformativeDecoratorTest(IStrategy): + """ + Strategy used by tests freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 2 + stoploss = -0.10 + timeframe = '5m' + startup_candle_count: int = 20 + + def informative_pairs(self): + return [('BTC/USDT', '5m')] + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['buy'] = 0 + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['sell'] = 0 + return dataframe + + # Decorator stacking test. + @informative('30m') + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Simple informative test. + @informative('1h', 'BTC/{stake}') + def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Quote currency different from stake currency test. + @informative('1h', 'ETH/BTC') + def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Formatting test. + @informative('30m', 'BTC/{stake}', '{column}_{BASE}_{QUOTE}_{base}_{quote}_{asset}_{timeframe}') + def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Custom formatter test + @informative('30m', 'ETH/{stake}', fmt=lambda column, **kwargs: column + '_from_callable') + def populate_indicators_eth_30m(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Strategy timeframe indicators for current pair. + dataframe['rsi'] = 14 + # Informative pairs are available in this method. + dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] + + # Mixing manual informative pairs with decorators. + informative = self.dp.get_pair_dataframe('BTC/USDT', '5m') + informative['rsi'] = 14 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, '5m', ffill=True) + + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 1b24c3297..6d936f3b3 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -648,7 +648,7 @@ def test_is_informative_pairs_callback(default_conf): strategy = StrategyResolver.load_strategy(default_conf) # Should return empty # Uses fallback to base implementation - assert [] == strategy.informative_pairs() + assert [] == strategy.gather_informative_pairs() @pytest.mark.parametrize('error', [ diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 3b84fc254..a01b55050 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -4,7 +4,9 @@ import numpy as np import pandas as pd import pytest -from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes +from freqtrade.data.dataprovider import DataProvider +from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open, + timeframe_to_minutes) def generate_test_data(timeframe: str, size: int): @@ -132,3 +134,65 @@ def test_stoploss_from_open(): assert stoploss == 0 else: assert isclose(stop_price, expected_stop_price, rel_tol=0.00001) + + +def test_stoploss_from_absolute(): + assert stoploss_from_absolute(90, 100) == 1 - (90 / 100) + assert stoploss_from_absolute(100, 100) == 0 + assert stoploss_from_absolute(110, 100) == 0 + assert stoploss_from_absolute(100, 0) == 1 + assert stoploss_from_absolute(0, 100) == 1 + + +def test_informative_decorator(mocker, default_conf): + test_data_5m = generate_test_data('5m', 40) + test_data_30m = generate_test_data('30m', 40) + test_data_1h = generate_test_data('1h', 40) + data = { + ('XRP/USDT', '5m'): test_data_5m, + ('XRP/USDT', '30m'): test_data_30m, + ('XRP/USDT', '1h'): test_data_1h, + ('LTC/USDT', '5m'): test_data_5m, + ('LTC/USDT', '30m'): test_data_30m, + ('LTC/USDT', '1h'): test_data_1h, + ('BTC/USDT', '30m'): test_data_30m, + ('BTC/USDT', '5m'): test_data_5m, + ('BTC/USDT', '1h'): test_data_1h, + ('ETH/USDT', '1h'): test_data_1h, + ('ETH/USDT', '30m'): test_data_30m, + ('ETH/BTC', '1h'): test_data_1h, + } + from .strats.informative_decorator_strategy import InformativeDecoratorTest + default_conf['stake_currency'] = 'USDT' + strategy = InformativeDecoratorTest(config=default_conf) + strategy.dp = DataProvider({}, None, None) + mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[ + 'XRP/USDT', 'LTC/USDT', 'BTC/USDT' + ]) + + assert len(strategy._ft_informative) == 6 # Equal to number of decorators used + informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'), + ('LTC/USDT', '30m'), ('BTC/USDT', '1h'), ('BTC/USDT', '30m'), + ('BTC/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')] + for inf_pair in informative_pairs: + assert inf_pair in strategy.gather_informative_pairs() + + def test_historic_ohlcv(pair, timeframe): + return data[(pair, timeframe or strategy.timeframe)].copy() + mocker.patch('freqtrade.data.dataprovider.DataProvider.historic_ohlcv', + side_effect=test_historic_ohlcv) + + analyzed = strategy.advise_all_indicators( + {p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')}) + expected_columns = [ + 'rsi_1h', 'rsi_30m', # Stacked informative decorators + 'btc_usdt_rsi_1h', # BTC 1h informative + 'rsi_BTC_USDT_btc_usdt_BTC/USDT_30m', # Column formatting + 'rsi_from_callable', # Custom column formatter + 'eth_btc_rsi_1h', # Quote currency not matching stake currency + 'rsi', 'rsi_less', # Non-informative columns + 'rsi_5m', # Manual informative dataframe + ] + for _, dataframe in analyzed.items(): + for col in expected_columns: + assert col in dataframe.columns diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index d6c1197ab..e7571b798 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 3 + assert len(strategies) == 4 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 4 + assert len(strategies) == 5 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 3 + assert len([x for x in strategies if x['class'] is not None]) == 4 assert len([x for x in strategies if x['class'] is None]) == 1 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9662118ec..3b72f55c1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -98,11 +98,15 @@ def test_bot_cleanup(mocker, default_conf, caplog) -> None: assert coo_mock.call_count == 1 -def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: +@pytest.mark.parametrize('runmode', [ + RunMode.DRY_RUN, + RunMode.LIVE +]) +def test_order_dict(default_conf, mocker, runmode, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) conf = default_conf.copy() - conf['runmode'] = RunMode.DRY_RUN + conf['runmode'] = runmode conf['order_types'] = { 'buy': 'market', 'sell': 'limit', @@ -112,45 +116,14 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: conf['bid_strategy']['price_side'] = 'ask' freqtrade = FreqtradeBot(conf) + if runmode == RunMode.LIVE: + assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) assert freqtrade.strategy.order_types['stoploss_on_exchange'] caplog.clear() # is left untouched conf = default_conf.copy() - conf['runmode'] = RunMode.DRY_RUN - conf['order_types'] = { - 'buy': 'market', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False, - } - freqtrade = FreqtradeBot(conf) - assert not freqtrade.strategy.order_types['stoploss_on_exchange'] - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) - - -def test_order_dict_live(default_conf, mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - - conf = default_conf.copy() - conf['runmode'] = RunMode.LIVE - conf['order_types'] = { - 'buy': 'market', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': True, - } - conf['bid_strategy']['price_side'] = 'ask' - - freqtrade = FreqtradeBot(conf) - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) - assert freqtrade.strategy.order_types['stoploss_on_exchange'] - - caplog.clear() - # is left untouched - conf = default_conf.copy() - conf['runmode'] = RunMode.LIVE + conf['runmode'] = runmode conf['order_types'] = { 'buy': 'market', 'sell': 'limit', @@ -239,8 +212,14 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: 'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 -def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: - +@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [ + # Override stoploss + (0.79, False), + # Override strategy stoploss + (0.85, True) +]) +def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, + buy_price_mult, ignore_strat_sl, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) @@ -254,9 +233,9 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': buy_price * 0.79, - 'ask': buy_price * 0.79, - 'last': buy_price * 0.79 + 'bid': buy_price * buy_price_mult, + 'ask': buy_price * buy_price_mult, + 'last': buy_price * buy_price_mult, }), get_fee=fee, ) @@ -273,46 +252,10 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf ############################################# # stoploss shoud be hit - assert freqtrade.handle_trade(trade) is True - assert log_has('Exit for NEO/BTC detected. Reason: stop_loss', caplog) - assert trade.sell_reason == SellType.STOP_LOSS.value - - -def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, - mocker, edge_conf) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - patch_edge(mocker) - edge_conf['max_open_trades'] = float('inf') - - # Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2 - # Thus, if price falls 15%, stoploss should not be triggered - # - # mocking the ticker: price is falling ... - buy_price = limit_buy_order['price'] - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': buy_price * 0.85, - 'ask': buy_price * 0.85, - 'last': buy_price * 0.85 - }), - get_fee=fee, - ) - ############################################# - - # Create a trade with "limit_buy_order" price - freqtrade = FreqtradeBot(edge_conf) - freqtrade.active_pair_whitelist = ['NEO/BTC'] - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.enter_positions() - trade = Trade.query.first() - trade.update(limit_buy_order) - ############################################# - - # stoploss shoud not be hit - assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_trade(trade) is not ignore_strat_sl + if not ignore_strat_sl: + assert log_has('Exit for NEO/BTC detected. Reason: stop_loss', caplog) + assert trade.sell_reason == SellType.STOP_LOSS.value def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None: @@ -406,37 +349,16 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, @pytest.mark.parametrize("is_short", [False, True]) -def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, - limit_sell_order_open, fee, mocker, is_short) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - enter_mock = ( - MagicMock(return_value=limit_sell_order_open) - if is_short else - MagicMock(return_value=limit_buy_order_open) - ) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=enter_mock, - get_fee=fee, - ) - default_conf['stake_amount'] = 0.0005 - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - freqtrade.create_trade('ETH/BTC') - rate, amount = enter_mock.call_args[1]['rate'], enter_mock.call_args[1]['amount'] - assert rate * amount <= default_conf['stake_amount'] - -# TODO-lev: paramatrize and convert to USDT -# @pytest.mark.parametrize("stake_amount,leverage", [ -# "buy, sell" -# ]) - - -def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker, caplog) -> None: +@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [ + (0.0005, True, True, 99), + (0.000000005, True, False, 99), + (0, False, True, 99), + (UNLIMITED_STAKE_AMOUNT, False, True, 0), +]) +def test_create_trade_minimal_amount( + default_conf, ticker, limit_buy_order_open, fee, mocker, + stake_amount, create, amount_enough, max_open_trades, caplog, is_short +) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value=limit_buy_order_open) @@ -446,58 +368,33 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord create_order=buy_mock, get_fee=fee, ) - + default_conf['max_open_trades'] = max_open_trades freqtrade = FreqtradeBot(default_conf) - freqtrade.config['stake_amount'] = 0.000000005 - + freqtrade.config['stake_amount'] = stake_amount patch_get_signal(freqtrade) - assert freqtrade.create_trade('ETH/BTC') - assert log_has_re(r"Stake amount for pair .* is too small.*", caplog) - - -def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - buy_mock = MagicMock(return_value=limit_buy_order_open) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=buy_mock, - get_fee=fee, - ) - - freqtrade = FreqtradeBot(default_conf) - freqtrade.config['stake_amount'] = 0 - - patch_get_signal(freqtrade) - - assert not freqtrade.create_trade('ETH/BTC') - - -def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=MagicMock(return_value=limit_buy_order_open), - get_fee=fee, - ) - default_conf['max_open_trades'] = 0 - default_conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 + if create: + assert freqtrade.create_trade('ETH/BTC') + if amount_enough: + rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] + assert rate * amount <= default_conf['stake_amount'] + else: + assert log_has_re( + r"Stake amount for pair .* is too small.*", + caplog + ) + else: + assert not freqtrade.create_trade('ETH/BTC') + if not max_open_trades: + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 +@pytest.mark.parametrize('whitelist,positions', [ + (["ETH/BTC"], 1), # No pairs left + ([], 0), # No pairs in whitelist +]) def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, - mocker, caplog) -> None: + whitelist, positions, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -506,36 +403,20 @@ def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_ope create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) - - default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] + default_conf['exchange']['pair_whitelist'] = whitelist freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) n = freqtrade.enter_positions() - assert n == 1 - assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) - n = freqtrade.enter_positions() - assert n == 0 - assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) - - -def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, - mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=MagicMock(return_value={'id': limit_buy_order['id']}), - get_fee=fee, - ) - default_conf['exchange']['pair_whitelist'] = [] - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - n = freqtrade.enter_positions() - assert n == 0 - assert log_has("Active pair whitelist is empty.", caplog) + assert n == positions + if positions: + assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) + n = freqtrade.enter_positions() + assert n == 0 + assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) + else: + assert n == 0 + assert log_has("Active pair whitelist is empty.", caplog) @pytest.mark.usefixtures("init_persistence") @@ -953,7 +834,7 @@ def test_execute_entry(mocker, default_conf, fee, limit_buy_order, limit_sell_or # Fail to get price... mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0)) - with pytest.raises(PricingError, match=f"Could not determine {'sell' if is_short else 'buy'} price."): + with pytest.raises(PricingError, match=f"Could not determine {enter_side(is_short)} price."): freqtrade.execute_entry(pair, stake_amount, is_short=is_short) # In case of custom entry price @@ -1338,6 +1219,7 @@ def test_create_stoploss_order_insufficient_funds( @pytest.mark.usefixtures("init_persistence") def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, is_short, limit_buy_order, limit_sell_order) -> None: + # TODO-lev: test for short # When trailing stoploss is set enter_order = limit_sell_order if is_short else limit_buy_order exit_order = limit_buy_order if is_short else limit_sell_order @@ -1437,7 +1319,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, is_shor pair='ETH/BTC', order_types=freqtrade.strategy.order_types, stop_price=0.00002346 * 0.95, - side="sell" + side="sell", + leverage=1.0 ) # price fell below stoploss, so dry-run sells trade. @@ -1540,6 +1423,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, is_s exit_order = limit_buy_order if is_short else limit_sell_order # When trailing stoploss is set + # TODO-lev: test for short stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) mocker.patch.multiple( @@ -1635,7 +1519,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, is_s pair='ETH/BTC', order_types=freqtrade.strategy.order_types, stop_price=0.00002346 * 0.96, - side="sell" + side="sell", + leverage=1.0 ) # price fell below stoploss, so dry-run sells trade. @@ -1764,34 +1649,32 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, is pair='NEO/BTC', order_types=freqtrade.strategy.order_types, stop_price=0.00002346 * 0.99, - side="sell" + side="sell", + leverage=1.0 ) -def test_enter_positions(mocker, default_conf, caplog) -> None: +@pytest.mark.parametrize('return_value,side_effect,log_message', [ + (False, None, 'Found no enter signals for whitelisted currencies. Trying again...'), + (None, DependencyException, 'Unable to create trade for ETH/BTC: ') +]) +def test_enter_positions(mocker, default_conf, return_value, side_effect, + log_message, caplog) -> None: caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_ct = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', - MagicMock(return_value=False)) - n = freqtrade.enter_positions() - assert n == 0 - assert log_has('Found no enter signals for whitelisted currencies. Trying again...', caplog) - # create_trade should be called once for every pair in the whitelist. - assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) - - -def test_enter_positions_exception(mocker, default_conf, caplog) -> None: - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_ct = mocker.patch( 'freqtrade.freqtradebot.FreqtradeBot.create_trade', - MagicMock(side_effect=DependencyException) + MagicMock( + return_value=return_value, + side_effect=side_effect + ) ) n = freqtrade.enter_positions() assert n == 0 + assert log_has(log_message, caplog) + # create_trade should be called once for every pair in the whitelist. assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) - assert log_has('Unable to create trade for ETH/BTC: ', caplog) @pytest.mark.parametrize("is_short", [False, True]) @@ -1896,9 +1779,15 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, @pytest.mark.parametrize("is_short", [False, True]) +@pytest.mark.parametrize('initial_amount,has_rounding_fee', [ + (90.99181073 + 1e-14, True), + (8.0, False) +]) def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, - limit_sell_order, is_short, mocker): + mocker, initial_amount, has_rounding_fee, + limit_sell_order, is_short, caplog): order = limit_sell_order if is_short else limit_buy_order + trades_for_order[0]['amount'] = initial_amount mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) @@ -1919,38 +1808,9 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ ) freqtrade.update_trade_state(trade, '123456', order) assert trade.amount != amount - assert trade.amount == order['amount'] - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_update_trade_state_withorderdict_rounding_fee( - default_conf, trades_for_order, fee, limit_buy_order, limit_sell_order, - mocker, caplog, is_short -): - order = limit_sell_order if is_short else limit_buy_order - trades_for_order[0]['amount'] = order['amount'] + 1e-14 - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # fetch_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) - patch_exchange(mocker) - amount = sum(x['amount'] for x in trades_for_order) - freqtrade = get_patched_freqtradebot(mocker, default_conf) - trade = Trade( - pair='LTC/ETH', - amount=amount, - exchange='binance', - open_rate=0.245441, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_order_id='123456', - is_open=True, - open_date=arrow.utcnow().datetime, - is_short=is_short - ) - freqtrade.update_trade_state(trade, '123456', order) - assert trade.amount != amount - assert trade.amount == order['amount'] - assert log_has_re(r'Applying fee on amount for .*', caplog) + assert trade.amount == limit_buy_order['amount'] + if has_rounding_fee: + assert log_has_re(r'Applying fee on amount for .*', caplog) @pytest.mark.parametrize("is_short", [False, True]) @@ -3354,16 +3214,28 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee, @pytest.mark.parametrize("is_short", [False, True]) -def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, - is_short, fee, mocker) -> None: +@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [ + # Enable profit + (True, 0.00001172, 0.00001173, False, True, SellType.SELL_SIGNAL.value), + # Disable profit + (False, 0.00002172, 0.00002173, True, False, SellType.SELL_SIGNAL.value), + # Enable loss + # * Shouldn't this be SellType.STOP_LOSS.value + (True, 0.00000172, 0.00000173, False, False, None), + # Disable loss + (False, 0.00000172, 0.00000173, True, False, SellType.SELL_SIGNAL.value), +]) +def test_sell_profit_only( + default_conf, limit_buy_order, limit_buy_order_open, is_short, + fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': 0.00001172, - 'ask': 0.00001173, - 'last': 0.00001172 + 'bid': bid, + 'ask': ask, + 'last': bid }), create_order=MagicMock(side_effect=[ limit_buy_order_open, @@ -3373,131 +3245,29 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy ) default_conf.update({ 'use_sell_signal': True, - 'sell_profit_only': True, + 'sell_profit_only': profit_only, 'sell_profit_offset': 0.1, }) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - + if sell_type == SellType.SELL_SIGNAL.value: + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) + else: + freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( + sell_type=SellType.NONE)) freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() patch_get_signal(freqtrade, enter_long=False, exit_long=True) - assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_trade(trade) is handle_first - freqtrade.strategy.sell_profit_offset = 0.0 - assert freqtrade.handle_trade(trade) is True + if handle_second: + freqtrade.strategy.sell_profit_offset = 0.0 + assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open, - is_short, fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00002172, - 'ask': 0.00002173, - 'last': 0.00002172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': False, - }) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - freqtrade.wallets.update() - patch_get_signal(freqtrade, enter_long=False, exit_long=True) - assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open, - is_short, fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00000172, - 'ask': 0.00000173, - 'last': 0.00000172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': True, - }) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( - sell_type=SellType.NONE)) - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - patch_get_signal(freqtrade, enter_long=False, exit_long=True) - assert freqtrade.handle_trade(trade) is False - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open, - is_short, fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.0000172, - 'ask': 0.0000173, - 'last': 0.0000172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': False, - }) - - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - freqtrade.wallets.update() - patch_get_signal(freqtrade, enter_long=False, exit_long=True) - assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value + assert trade.sell_reason == sell_type @pytest.mark.parametrize("is_short", [False, True]) @@ -3536,11 +3306,15 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_ assert trade.amount != amnt -def test__safe_exit_amount(default_conf, fee, caplog, mocker): +@pytest.mark.parametrize('amount_wallet,has_err', [ + (95.29, False), + (91.29, True) +]) +def test__safe_exit_amount(default_conf, fee, caplog, mocker, amount_wallet, has_err): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 - amount_wallet = 95.29 + amount_wallet = amount_wallet mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) wallet_update = mocker.patch('freqtrade.wallets.Wallets.update') trade = Trade( @@ -3554,37 +3328,19 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker): ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - - wallet_update.reset_mock() - assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet - assert log_has_re(r'.*Falling back to wallet-amount.', caplog) - assert wallet_update.call_count == 1 - caplog.clear() - wallet_update.reset_mock() - assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet - assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) - assert wallet_update.call_count == 1 - - -def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): - patch_RPCManager(mocker) - patch_exchange(mocker) - amount = 95.33 - amount_wallet = 91.29 - mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) - trade = Trade( - pair='LTC/ETH', - amount=amount, - exchange='binance', - open_rate=0.245441, - open_order_id="123456", - fee_open=fee.return_value, - fee_close=fee.return_value, - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - with pytest.raises(DependencyException, match=r"Not enough amount to exit."): - assert freqtrade._safe_exit_amount(trade.pair, trade.amount) + if has_err: + with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."): + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) + else: + wallet_update.reset_mock() + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet + assert log_has_re(r'.*Falling back to wallet-amount.', caplog) + assert wallet_update.call_count == 1 + caplog.clear() + wallet_update.reset_mock() + assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet + assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) + assert wallet_update.call_count == 1 @pytest.mark.parametrize("is_short", [False, True]) @@ -3847,8 +3603,8 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_orde def test_tsl_only_offset_reached(default_conf, limit_buy_order_usdt, limit_buy_order_usdt_open, fee, is_short, limit_sell_order_usdt, limit_sell_order_usdt_open, caplog, mocker) -> None: - limit_order = limit_sell_usdt_order if is_short else limit_buy_order_usdt - limit_order_open = limit_sell_order_usdt_open if is_short else limit_buy_order_open_usdt + limit_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt + limit_order_open = limit_sell_order_usdt_open if is_short else limit_buy_order_usdt_open enter_price = limit_order['price'] # enter_price: 2.0 @@ -3885,9 +3641,9 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order_usdt, limit_buy_o # Raise ticker above buy price mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ - 'bid': buy_price + 0.0000004, - 'ask': buy_price + 0.0000004, - 'last': buy_price + 0.0000004 + 'bid': enter_price + 0.0000004, + 'ask': enter_price + 0.0000004, + 'last': enter_price + 0.0000004 })) # stop-loss should not be adjusted as offset is not reached yet @@ -3900,9 +3656,9 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order_usdt, limit_buy_o # price rises above the offset (rises 12% when the offset is 5.5%) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ - 'bid': buy_price + 0.0000014, - 'ask': buy_price + 0.0000014, - 'last': buy_price + 0.0000014 + 'bid': enter_price + 0.0000014, + 'ask': enter_price + 0.0000014, + 'last': enter_price + 0.0000014 })) assert freqtrade.handle_trade(trade) is False @@ -4384,50 +4140,37 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o assert trade is None -def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: +@pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [ + (False, 0.045, 0.046, 2, None), + (True, 0.042, 0.046, 1, {'bids': [[]], 'asks': [[]]}) +]) +def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, exception_thrown, + ask, last, order_book_top, order_book, caplog) -> None: """ - test if function get_rate will return the order book price - instead of the ask rate + test if function get_rate will return the order book price instead of the ask rate """ patch_exchange(mocker) - ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046}) + ticker_mock = MagicMock(return_value={'ask': ask, 'last': last}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_l2_order_book=order_book_l2, + fetch_l2_order_book=MagicMock(return_value=order_book) if order_book else order_book_l2, fetch_ticker=ticker_mock, - ) default_conf['exchange']['name'] = 'binance' default_conf['bid_strategy']['use_order_book'] = True - default_conf['bid_strategy']['order_book_top'] = 2 + default_conf['bid_strategy']['order_book_top'] = order_book_top default_conf['bid_strategy']['ask_last_balance'] = 0 default_conf['telegram']['enabled'] = False freqtrade = FreqtradeBot(default_conf) - assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 - assert ticker_mock.call_count == 0 - - -def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None: - patch_exchange(mocker) - ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046}) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_l2_order_book=MagicMock(return_value={'bids': [[]], 'asks': [[]]}), - fetch_ticker=ticker_mock, - - ) - default_conf['exchange']['name'] = 'binance' - default_conf['bid_strategy']['use_order_book'] = True - default_conf['bid_strategy']['order_book_top'] = 1 - default_conf['bid_strategy']['ask_last_balance'] = 0 - default_conf['telegram']['enabled'] = False - - freqtrade = FreqtradeBot(default_conf) - # orderbook shall be used even if tickers would be lower. - with pytest.raises(PricingError): - freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") - assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog) + if exception_thrown: + with pytest.raises(PricingError): + freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") + assert log_has_re( + r'Buy Price at location 1 from orderbook could not be determined.', caplog) + else: + assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 + assert ticker_mock.call_count == 0 def test_check_depth_of_market(default_conf, mocker, order_book_l2) -> None: diff --git a/tests/test_periodiccache.py b/tests/test_periodiccache.py new file mode 100644 index 000000000..f874f9041 --- /dev/null +++ b/tests/test_periodiccache.py @@ -0,0 +1,32 @@ +import time_machine + +from freqtrade.configuration import PeriodicCache + + +def test_ttl_cache(): + + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + + cache = PeriodicCache(5, ttl=60) + cache1h = PeriodicCache(5, ttl=3600) + + assert cache.timer() == 1630472400.0 + cache['a'] = 1235 + cache1h['a'] = 555123 + assert 'a' in cache + assert 'a' in cache1h + + t.move_to("2021-09-01 05:00:59 +00:00") + assert 'a' in cache + assert 'a' in cache1h + + # Cache expired + t.move_to("2021-09-01 05:01:00 +00:00") + assert 'a' not in cache + assert 'a' in cache1h + + t.move_to("2021-09-01 05:59:59 +00:00") + assert 'a' in cache1h + + t.move_to("2021-09-01 06:00:00 +00:00") + assert 'a' not in cache1h