diff --git a/Dockerfile b/Dockerfile index e84a4d095..b3e5d5e88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.6-slim-bullseye as base +FROM python:3.10.7-slim-bullseye as base # Setup env ENV LANG C.UTF-8 diff --git a/docs/faq.md b/docs/faq.md index 381bbceb5..a72268ef9 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -4,7 +4,7 @@ Freqtrade supports spot trading only. -### Can I open short positions? +### Can my bot open short positions? Freqtrade can open short positions in futures markets. This requires the strategy to be made for this - and `"trading_mode": "futures"` in the configuration. @@ -12,9 +12,9 @@ Please make sure to read the [relevant documentation page](leverage.md) first. In spot markets, you can in some cases use leveraged spot tokens, which reflect an inverted pair (eg. BTCUP/USD, BTCDOWN/USD, ETHBULL/USD, ETHBEAR/USD,...) which can be traded with Freqtrade. -### Can I trade options or futures? +### Can my bot trade options or futures? -Futures trading is supported for selected exchanges. +Futures trading is supported for selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an uptodate list of supported exchanges. ## Beginner Tips & Tricks @@ -22,6 +22,13 @@ Futures trading is supported for selected exchanges. ## Freqtrade common issues +### Can freqtrade open multiple positions on the same pair in parallel? + +No. Freqtrade will only open one position per pair at a time. +You can however use the [`adjust_trade_position()` callback](strategy-callbacks.md#adjust-trade-position) to adjust an open position. + +Backtesting provides an option for this in `--eps` - however this is only there to highlight "hidden" signals, and will not work in live. + ### The bot does not start Running the bot with `freqtrade trade --config config.json` shows the output `freqtrade: command not found`. @@ -30,7 +37,7 @@ This could be caused by the following reasons: * The virtual environment is not active. * Run `source .env/bin/activate` to activate the virtual environment. -* The installation did not work correctly. +* The installation did not complete successfully. * Please check the [Installation documentation](installation.md). ### I have waited 5 minutes, why hasn't the bot made any trades yet? @@ -67,7 +74,7 @@ This is not a bot-problem, but will also happen while manual trading. While freqtrade can handle this (it'll sell 99 COIN), fees are often below the minimum tradable lot-size (you can only trade full COIN, not 0.9 COIN). Leaving the dust (0.9 COIN) on the exchange makes usually sense, as the next time freqtrade buys COIN, it'll eat into the remaining small balance, this time selling everything it bought, and therefore slowly declining the dust balance (although it most likely will never reach exactly 0). -Where possible (e.g. on binance), the use of the exchange's dedicated fee currency will fix this. +Where possible (e.g. on binance), the use of the exchange's dedicated fee currency will fix this. On binance, it's sufficient to have BNB in your account, and have "Pay fees in BNB" enabled in your profile. Your BNB balance will slowly decline (as it's used to pay fees) - but you'll no longer encounter dust (Freqtrade will include the fees in the profit calculations). Other exchanges don't offer such possibilities, where it's simply something you'll have to accept or move to a different exchange. @@ -109,7 +116,7 @@ This warning can point to one of the below problems: ### I'm getting the "RESTRICTED_MARKET" message in the log -Currently known to happen for US Bittrex users. +Currently known to happen for US Bittrex users. Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information. @@ -177,8 +184,8 @@ The GPU improvements would only apply to pandas-native calculations - or ones wr For hyperopt, freqtrade is using scikit-optimize, which is built on top of scikit-learn. Their statement about GPU support is [pretty clear](https://scikit-learn.org/stable/faq.html#will-you-add-gpu-support). -GPU's also are only good at crunching numbers (floating point operations). -For hyperopt, we need both number-crunching (find next parameters) and running python code (running backtesting). +GPU's also are only good at crunching numbers (floating point operations). +For hyperopt, we need both number-crunching (find next parameters) and running python code (running backtesting). As such, GPU's are not too well suited for most parts of hyperopt. The benefit of using GPU would therefore be pretty slim - and will not justify the complexity introduced by trying to add GPU support. @@ -219,9 +226,9 @@ already 8\*10^9\*10 evaluations. A roughly total of 80 billion evaluations. Did you run 100 000 evaluations? Congrats, you've done roughly 1 / 100 000 th of the search space, assuming that the bot never tests the same parameters more than once. -* The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 100000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. +* The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 100000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. -Example: 4% profit 650 times vs 0,3% profit a trade 10000 times in a year. If we assume you set the --timerange to 365 days. +Example: 4% profit 650 times vs 0,3% profit a trade 10000 times in a year. If we assume you set the --timerange to 365 days. Example: `freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601` diff --git a/docs/freqai.md b/docs/freqai.md index e790bbb81..3400b8428 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -114,6 +114,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training data set, as well as from incoming data points. See details about how it works [here](#removing-outliers-using-a-support-vector-machine-svm).
**Datatype:** Boolean. | `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. See details about some select parameters [here](#removing-outliers-using-a-support-vector-machine-svm).
**Datatype:** Dictionary. | `use_DBSCAN_to_remove_outliers` | Cluster data using DBSCAN to identify and remove outliers from training and prediction data. See details about how it works [here](#removing-outliers-with-dbscan).
**Datatype:** Boolean. +| `inlier_metric_window` | If set, FreqAI will add the `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`. Details of how the `inlier_metric` is computed can be found [here](#using-the-inliermetric)
**Datatype:** int. Default: 0 +| `noise_standard_deviation` | If > 0, FreqAI adds noise to the training features. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. Value should be kept relative to the normalized space between -1 and 1). In other words, since data is always normalized between -1 and 1 in FreqAI, the user can expect a `noise_standard_deviation: 0.05` to see 32% of data randomly increased/decreased by more than 2.5% (i.e. the percent of data falling within the first standard deviation). Good for preventing overfitting.
**Datatype:** int. Default: 0 | `outlier_protection_percentage` | If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection while keeping the original dataset intact. If the outlier protection is triggered, no predictions will be made based on the training data.
**Datatype:** Float. Default: `30` | `reverse_train_test_order` | If true, FreqAI will train on the latest data split and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, users should be careful to understand unorthodox nature of this parameter before employing it.
**Datatype:** Boolean. Default: False | | **Data split parameters** @@ -644,6 +646,18 @@ testing; the other points are used for training. The test data is used to evaluate the performance of the model after training. If the test score is high, the model is able to capture the behavior of the data well. If the test score is low, either the model either does not capture the complexity of the data, the test data is significantly different from the train data, or a different model should be used. +### Using the `inlier_metric` + +The `inlier_metric` is a metric aimed at quantifying how different a prediction data point is from the most recent historic data points. + +User can set `inlier_metric_window` to set the look back window. FreqAI will compute the distance between the present prediction point and each of the previous data points (total of `inlier_metric_window` points). + +This function goes one step further - during training, it computes the `inlier_metric` for all training data points and builds weibull distributions for each each lookback point. The cumulative distribution function for the weibull distribution is used to produce a quantile for each of the data points. The quantiles for each lookback point are averaged to create the `inlier_metric`. + +FreqAI adds this `inlier_metric` score to the training features! In other words, your model is trained to recognize how this temporal inlier metric is related to the user set labels. + +This function does **not** remove outliers from the data set. + ### Controlling the model learning process Model training parameters are unique to the machine learning library selected by the user. FreqAI allows the user to set any parameter for any library using the `model_training_parameters` dictionary in the user configuration file. The example configuration file (found in `config_examples/config_freqai.example.json`) show some of the example parameters associated with `Catboost` and `LightGBM`, but the user can add any parameters available in those libraries. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index ece8700de..b9324def4 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -90,7 +90,8 @@ Example configuration showing the different settings: "trailing_stop_loss": "on", "stop_loss": "on", "stoploss_on_exchange": "on", - "custom_exit": "silent" + "custom_exit": "silent", + "partial_exit": "on" }, "entry_cancel": "silent", "exit_cancel": "on", @@ -138,7 +139,7 @@ You can create your own keyboard in `config.json`: "enabled": true, "token": "your_telegram_token", "chat_id": "your_telegram_chat_id", - "keyboard": [ + "keyboard": [ ["/daily", "/stats", "/balance", "/profit"], ["/status table", "/performance"], ["/reload_config", "/count", "/logs"] @@ -225,16 +226,16 @@ Once all positions are sold, run `/stop` to completely stop the bot. For each open trade, the bot will send you the following message. Enter Tag is configurable via Strategy. -> **Trade ID:** `123` `(since 1 days ago)` -> **Current Pair:** CVC/BTC +> **Trade ID:** `123` `(since 1 days ago)` +> **Current Pair:** CVC/BTC > **Direction:** Long > **Leverage:** 1.0 -> **Amount:** `26.64180098` +> **Amount:** `26.64180098` > **Enter Tag:** Awesome Long Signal -> **Open Rate:** `0.00007489` -> **Current Rate:** `0.00007489` -> **Current Profit:** `12.95%` -> **Stoploss:** `0.00007389 (-0.02%)` +> **Open Rate:** `0.00007489` +> **Current Rate:** `0.00007489` +> **Current Profit:** `12.95%` +> **Stoploss:** `0.00007389 (-0.02%)` ### /status table @@ -261,26 +262,26 @@ current max Return a summary of your profit/loss and performance. -> **ROI:** Close trades -> ∙ `0.00485701 BTC (2.2%) (15.2 Σ%)` -> ∙ `62.968 USD` -> **ROI:** All trades -> ∙ `0.00255280 BTC (1.5%) (6.43 Σ%)` -> ∙ `33.095 EUR` -> -> **Total Trade Count:** `138` -> **First Trade opened:** `3 days ago` -> **Latest Trade opened:** `2 minutes ago` -> **Avg. Duration:** `2:33:45` -> **Best Performing:** `PAY/BTC: 50.23%` -> **Trading volume:** `0.5 BTC` -> **Profit factor:** `1.04` -> **Max Drawdown:** `9.23% (0.01255 BTC)` +> **ROI:** Close trades +> ∙ `0.00485701 BTC (2.2%) (15.2 Σ%)` +> ∙ `62.968 USD` +> **ROI:** All trades +> ∙ `0.00255280 BTC (1.5%) (6.43 Σ%)` +> ∙ `33.095 EUR` +> +> **Total Trade Count:** `138` +> **First Trade opened:** `3 days ago` +> **Latest Trade opened:** `2 minutes ago` +> **Avg. Duration:** `2:33:45` +> **Best Performing:** `PAY/BTC: 50.23%` +> **Trading volume:** `0.5 BTC` +> **Profit factor:** `1.04` +> **Max Drawdown:** `9.23% (0.01255 BTC)` -The relative profit of `1.2%` is the average profit per trade. -The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`. -Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. -Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy. +The relative profit of `1.2%` is the average profit per trade. +The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`. +Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. +Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy. Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`. ### /forceexit @@ -309,27 +310,27 @@ Note that for this to work, `force_entry_enable` needs to be set to true. ### /performance Return the performance of each crypto-currency the bot has sold. -> Performance: -> 1. `RCN/BTC 0.003 BTC (57.77%) (1)` -> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)` -> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)` -> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)` -> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)` -> ... +> Performance: +> 1. `RCN/BTC 0.003 BTC (57.77%) (1)` +> 2. `PAY/BTC 0.0012 BTC (56.91%) (1)` +> 3. `VIB/BTC 0.0011 BTC (47.07%) (1)` +> 4. `SALT/BTC 0.0010 BTC (30.24%) (1)` +> 5. `STORJ/BTC 0.0009 BTC (27.24%) (1)` +> ... ### /balance Return the balance of all crypto-currency your have on the exchange. -> **Currency:** BTC -> **Available:** 3.05890234 -> **Balance:** 3.05890234 -> **Pending:** 0.0 +> **Currency:** BTC +> **Available:** 3.05890234 +> **Balance:** 3.05890234 +> **Pending:** 0.0 -> **Currency:** CVC -> **Available:** 86.64180098 -> **Balance:** 86.64180098 -> **Pending:** 0.0 +> **Currency:** CVC +> **Available:** 86.64180098 +> **Balance:** 86.64180098 +> **Pending:** 0.0 ### /daily @@ -376,7 +377,7 @@ Month (count) Profit BTC Profit USD Profit % Shows the current whitelist -> Using whitelist `StaticPairList` with 22 pairs +> Using whitelist `StaticPairList` with 22 pairs > `IOTA/BTC, NEO/BTC, TRX/BTC, VET/BTC, ADA/BTC, ETC/BTC, NCASH/BTC, DASH/BTC, XRP/BTC, XVG/BTC, EOS/BTC, LTC/BTC, OMG/BTC, BTG/BTC, LSK/BTC, ZEC/BTC, HOT/BTC, IOTX/BTC, XMR/BTC, AST/BTC, XLM/BTC, NANO/BTC` ### /blacklist [pair] @@ -386,7 +387,7 @@ If Pair is set, then this pair will be added to the pairlist. Also supports multiple pairs, separated by a space. Use `/reload_config` to reset the blacklist. -> Using blacklist `StaticPairList` with 2 pairs +> Using blacklist `StaticPairList` with 2 pairs >`DODGE/BTC`, `HOT/BTC`. ### /edge diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 3d094da36..4240a8014 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -455,8 +455,6 @@ AVAILABLE_CLI_OPTIONS = { '-t', '--timeframes', help='Specify which tickers to download. Space-separated list. ' 'Default: `1m 5m`.', - choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', - '6h', '8h', '12h', '1d', '3d', '1w', '2w', '1M', '1y'], default=['1m', '5m'], nargs='+', ), diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c3dca43a8..33a56c530 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -205,7 +205,7 @@ class Exchange: logger.debug("Exchange object destroyed, closing async loop") if (self._api_async and inspect.iscoroutinefunction(self._api_async.close) and self._api_async.session): - logger.info("Closing async ccxt session.") + logger.debug("Closing async ccxt session.") self.loop.run_until_complete(self._api_async.close()) def validate_config(self, config): @@ -446,6 +446,15 @@ class Exchange: contract_size = self.get_contract_size(pair) return contracts_to_amount(num_contracts, contract_size) + def amount_to_contract_precision(self, pair: str, amount: float) -> float: + """ + Helper wrapper around amount_to_contract_precision + """ + contract_size = self.get_contract_size(pair) + + return amount_to_contract_precision(amount, self.get_precision_amount(pair), + self.precisionMode, contract_size) + def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None: if exchange_config.get('sandbox'): if api.urls.get('test'): @@ -2500,8 +2509,13 @@ class Exchange: cache=False, drop_incomplete=False, ) - funding_rates = candle_histories[funding_comb] - mark_rates = candle_histories[mark_comb] + try: + # we can't assume we always get histories - for example during exchange downtimes + funding_rates = candle_histories[funding_comb] + mark_rates = candle_histories[mark_comb] + except KeyError: + raise ExchangeError("Could not find funding rates.") from None + funding_mark_rates = self.combine_funding_and_mark( funding_rates=funding_rates, mark_rates=mark_rates) @@ -2581,6 +2595,8 @@ class Exchange: :param is_short: trade direction :param amount: Trade amount :param open_date: Open date of the trade + :return: funding fee since open_date + :raies: ExchangeError if something goes wrong. """ if self.trading_mode == TradingMode.FUTURES: if self._config['dry_run']: diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 828734520..01b2f1f3b 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -1,7 +1,8 @@ import copy -import datetime import logging import shutil +from datetime import datetime, timezone +from math import cos, sin from pathlib import Path from typing import Any, Dict, List, Tuple @@ -9,6 +10,7 @@ import numpy as np import numpy.typing as npt import pandas as pd from pandas import DataFrame +from scipy import stats from sklearn import linear_model from sklearn.cluster import DBSCAN from sklearn.metrics.pairwise import pairwise_distances @@ -360,7 +362,7 @@ class FreqaiDataKitchen: def denormalize_labels_from_metadata(self, df: DataFrame) -> DataFrame: """ - Normalize a set of data using the mean and standard deviation from + Denormalize a set of data using the mean and standard deviation from the associated training data. :param df: Dataframe of predictions to be denormalized """ @@ -399,7 +401,7 @@ class FreqaiDataKitchen: config_timerange = TimeRange.parse_timerange(self.config["timerange"]) if config_timerange.stopts == 0: config_timerange.stopts = int( - datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + datetime.now(tz=timezone.utc).timestamp() ) timerange_train = copy.deepcopy(full_timerange) timerange_backtest = copy.deepcopy(full_timerange) @@ -416,8 +418,8 @@ class FreqaiDataKitchen: timerange_train.stopts = timerange_train.startts + train_period_days first = False - start = datetime.datetime.utcfromtimestamp(timerange_train.startts) - stop = datetime.datetime.utcfromtimestamp(timerange_train.stopts) + start = datetime.fromtimestamp(timerange_train.startts, tz=timezone.utc) + stop = datetime.fromtimestamp(timerange_train.stopts, tz=timezone.utc) tr_training_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d")) tr_training_list_timerange.append(copy.deepcopy(timerange_train)) @@ -430,8 +432,8 @@ class FreqaiDataKitchen: if timerange_backtest.stopts > config_timerange.stopts: timerange_backtest.stopts = config_timerange.stopts - start = datetime.datetime.utcfromtimestamp(timerange_backtest.startts) - stop = datetime.datetime.utcfromtimestamp(timerange_backtest.stopts) + start = datetime.fromtimestamp(timerange_backtest.startts, tz=timezone.utc) + stop = datetime.fromtimestamp(timerange_backtest.stopts, tz=timezone.utc) tr_backtesting_list.append(start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d")) tr_backtesting_list_timerange.append(copy.deepcopy(timerange_backtest)) @@ -451,14 +453,28 @@ class FreqaiDataKitchen: it is sliced down to just the present training period. """ - start = datetime.datetime.fromtimestamp(timerange.startts, tz=datetime.timezone.utc) - stop = datetime.datetime.fromtimestamp(timerange.stopts, tz=datetime.timezone.utc) + start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) + stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc) df = df.loc[df["date"] >= start, :] if not self.live: df = df.loc[df["date"] < stop, :] return df + def remove_training_from_backtesting( + self + ) -> DataFrame: + """ + Function which takes the backtesting time range and + remove training data from dataframe + """ + tr = self.config["timerange"] + backtesting_timerange = TimeRange.parse_timerange(tr) + start = datetime.fromtimestamp(backtesting_timerange.startts, tz=timezone.utc) + df = self.return_dataframe + df = df.loc[df["date"] >= start, :] + return df + def principal_component_analysis(self) -> None: """ Performs Principal Component Analysis on the data for dimensionality reduction @@ -653,8 +669,6 @@ class FreqaiDataKitchen: is an outlier. """ - from math import cos, sin - if predict: if not self.data['DBSCAN_eps']: return @@ -747,6 +761,111 @@ class FreqaiDataKitchen: return + def compute_inlier_metric(self, set_='train') -> None: + """ + + Compute inlier metric from backwards distance distributions. + This metric defines how well features from a timepoint fit + into previous timepoints. + """ + + no_prev_pts = self.freqai_config["feature_parameters"]["inlier_metric_window"] + + if set_ == 'train': + compute_df = copy.deepcopy(self.data_dictionary['train_features']) + elif set_ == 'test': + compute_df = copy.deepcopy(self.data_dictionary['test_features']) + else: + compute_df = copy.deepcopy(self.data_dictionary['prediction_features']) + + compute_df_reindexed = compute_df.reindex( + index=np.flip(compute_df.index) + ) + + pairwise = pd.DataFrame( + np.triu( + pairwise_distances(compute_df_reindexed, n_jobs=self.thread_count) + ), + columns=compute_df_reindexed.index, + index=compute_df_reindexed.index + ) + pairwise = pairwise.round(5) + + column_labels = [ + '{}{}'.format('d', i) for i in range(1, no_prev_pts + 1) + ] + distances = pd.DataFrame( + columns=column_labels, index=compute_df.index + ) + + for index in compute_df.index[no_prev_pts:]: + current_row = pairwise.loc[[index]] + current_row_no_zeros = current_row.loc[ + :, (current_row != 0).any(axis=0) + ] + distances.loc[[index]] = current_row_no_zeros.iloc[ + :, :no_prev_pts + ] + distances = distances.replace([np.inf, -np.inf], np.nan) + drop_index = pd.isnull(distances).any(1) + distances = distances[drop_index == 0] + + inliers = pd.DataFrame(index=distances.index) + for key in distances.keys(): + current_distances = distances[key].dropna() + fit_params = stats.weibull_min.fit(current_distances) + quantiles = stats.weibull_min.cdf(current_distances, *fit_params) + + df_inlier = pd.DataFrame( + {key: quantiles}, index=distances.index + ) + inliers = pd.concat( + [inliers, df_inlier], axis=1 + ) + + inlier_metric = pd.DataFrame( + data=inliers.sum(axis=1) / no_prev_pts, + columns=['inlier_metric'], + index=compute_df.index + ) + + inlier_metric = (2 * (inlier_metric - inlier_metric.min()) / + (inlier_metric.max() - inlier_metric.min()) - 1) + + if set_ in ('train', 'test'): + inlier_metric = inlier_metric.iloc[no_prev_pts:] + compute_df = compute_df.iloc[no_prev_pts:] + self.remove_beginning_points_from_data_dict(set_, no_prev_pts) + self.data_dictionary[f'{set_}_features'] = pd.concat( + [compute_df, inlier_metric], axis=1) + else: + self.data_dictionary['prediction_features'] = pd.concat( + [compute_df, inlier_metric], axis=1) + self.data_dictionary['prediction_features'].fillna(0, inplace=True) + + logger.info('Inlier metric computed and added to features.') + + return None + + def remove_beginning_points_from_data_dict(self, set_='train', no_prev_pts: int = 10): + features = self.data_dictionary[f'{set_}_features'] + weights = self.data_dictionary[f'{set_}_weights'] + labels = self.data_dictionary[f'{set_}_labels'] + self.data_dictionary[f'{set_}_weights'] = weights[no_prev_pts:] + self.data_dictionary[f'{set_}_features'] = features.iloc[no_prev_pts:] + self.data_dictionary[f'{set_}_labels'] = labels.iloc[no_prev_pts:] + + def add_noise_to_training_features(self) -> None: + """ + Add noise to train features to reduce the risk of overfitting. + """ + mu = 0 # no shift + sigma = self.freqai_config["feature_parameters"]["noise_standard_deviation"] + compute_df = self.data_dictionary['train_features'] + noise = np.random.normal(mu, sigma, [compute_df.shape[0], compute_df.shape[1]]) + self.data_dictionary['train_features'] += noise + return + def find_features(self, dataframe: DataFrame) -> None: """ Find features in the strategy provided dataframe @@ -849,6 +968,7 @@ class FreqaiDataKitchen: to_keep = [col for col in dataframe.columns if not col.startswith("&")] self.return_dataframe = pd.concat([dataframe[to_keep], self.full_df], axis=1) + self.return_dataframe = self.remove_training_from_backtesting() self.full_df = DataFrame() return @@ -872,14 +992,14 @@ class FreqaiDataKitchen: "Please indicate the end date of your desired backtesting. " "timerange.") # backtest_timerange.stopts = int( - # datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + # datetime.now(tz=timezone.utc).timestamp() # ) backtest_timerange.startts = ( backtest_timerange.startts - backtest_period_days * SECONDS_IN_DAY ) - start = datetime.datetime.utcfromtimestamp(backtest_timerange.startts) - stop = datetime.datetime.utcfromtimestamp(backtest_timerange.stopts) + start = datetime.fromtimestamp(backtest_timerange.startts, tz=timezone.utc) + stop = datetime.fromtimestamp(backtest_timerange.stopts, tz=timezone.utc) full_timerange = start.strftime("%Y%m%d") + "-" + stop.strftime("%Y%m%d") self.full_path = Path( @@ -905,7 +1025,7 @@ class FreqaiDataKitchen: :return: bool = If the model is expired or not. """ - time = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + time = datetime.now(tz=timezone.utc).timestamp() elapsed_time = (time - trained_timestamp) / 3600 # hours max_time = self.freqai_config.get("expiration_hours", 0) if max_time > 0: @@ -917,7 +1037,7 @@ class FreqaiDataKitchen: self, trained_timestamp: int ) -> Tuple[bool, TimeRange, TimeRange]: - time = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() + time = datetime.now(tz=timezone.utc).timestamp() trained_timerange = TimeRange() data_load_timerange = TimeRange() diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index e4f77a9cf..6a6858eac 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -1,10 +1,9 @@ -# import contextlib -import datetime import logging import shutil import threading import time from abc import ABC, abstractmethod +from datetime import datetime, timezone from pathlib import Path from threading import Lock from typing import Any, Dict, List, Tuple @@ -59,7 +58,6 @@ class IFreqaiModel(ABC): "data_split_parameters", {}) self.model_training_parameters: Dict[str, Any] = config.get("freqai", {}).get( "model_training_parameters", {}) - self.feature_parameters = config.get("freqai", {}).get("feature_parameters") self.retrain = False self.first = True self.set_full_path() @@ -70,11 +68,14 @@ class IFreqaiModel(ABC): self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode) self.identifier: str = self.freqai_info.get("identifier", "no_id_provided") self.scanning = False + self.ft_params = self.freqai_info["feature_parameters"] self.keras: bool = self.freqai_info.get("keras", False) - if self.keras and self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0): - self.freqai_info["feature_parameters"]["DI_threshold"] = 0 + if self.keras and self.ft_params.get("DI_threshold", 0): + self.ft_params["DI_threshold"] = 0 logger.warning("DI threshold is not configured for Keras models yet. Deactivating.") self.CONV_WIDTH = self.freqai_info.get("conv_width", 2) + if self.ft_params.get("inlier_metric_window", 0): + self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2 self.pair_it = 0 self.pair_it_train = 0 self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist")) @@ -190,7 +191,7 @@ class IFreqaiModel(ABC): if retrain: self.train_timer('start') - self.train_model_in_series( + self.extract_data_and_train_model( new_trained_timerange, pair, strategy, dk, data_load_timerange ) self.train_timer('stop') @@ -230,12 +231,12 @@ class IFreqaiModel(ABC): dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) trained_timestamp = tr_train - tr_train_startts_str = datetime.datetime.utcfromtimestamp(tr_train.startts).strftime( - "%Y-%m-%d %H:%M:%S" - ) - tr_train_stopts_str = datetime.datetime.utcfromtimestamp(tr_train.stopts).strftime( - "%Y-%m-%d %H:%M:%S" - ) + tr_train_startts_str = datetime.fromtimestamp( + tr_train.startts, + tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + tr_train_stopts_str = datetime.fromtimestamp( + tr_train.stopts, + tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") logger.info( f"Training {metadata['pair']}, {self.pair_it}/{self.total_pairs} pairs" f" from {tr_train_startts_str} to {tr_train_stopts_str}, {train_it}/{total_trains} " @@ -420,24 +421,30 @@ class IFreqaiModel(ABC): def data_cleaning_train(self, dk: FreqaiDataKitchen) -> None: """ - Base data cleaning method for train - Any function inside this method should drop training data points from the filtered_dataframe - based on user decided logic. See FreqaiDataKitchen::use_SVM_to_remove_outliers() for an - example of how outlier data points are dropped from the dataframe used for training. + Base data cleaning method for train. + Functions here improve/modify the input data by identifying outliers, + computing additional metrics, adding noise, reducing dimensionality etc. """ - if self.freqai_info["feature_parameters"].get( + ft_params = self.freqai_info["feature_parameters"] + + if ft_params.get('inlier_metric_window', 0): + dk.compute_inlier_metric(set_='train') + if self.freqai_info["data_split_parameters"]["test_size"] > 0: + dk.compute_inlier_metric(set_='test') + + if ft_params.get( "principal_component_analysis", False ): dk.principal_component_analysis() - if self.freqai_info["feature_parameters"].get("use_SVM_to_remove_outliers", False): + if ft_params.get("use_SVM_to_remove_outliers", False): dk.use_SVM_to_remove_outliers(predict=False) - if self.freqai_info["feature_parameters"].get("DI_threshold", 0): + if ft_params.get("DI_threshold", 0): dk.data["avg_mean_dist"] = dk.compute_distances() - if self.freqai_info["feature_parameters"].get("use_DBSCAN_to_remove_outliers", False): + if ft_params.get("use_DBSCAN_to_remove_outliers", False): if dk.pair in self.dd.old_DBSCAN_eps: eps = self.dd.old_DBSCAN_eps[dk.pair] else: @@ -445,29 +452,31 @@ class IFreqaiModel(ABC): dk.use_DBSCAN_to_remove_outliers(predict=False, eps=eps) self.dd.old_DBSCAN_eps[dk.pair] = dk.data['DBSCAN_eps'] + if self.freqai_info["feature_parameters"].get('noise_standard_deviation', 0): + dk.add_noise_to_training_features() + def data_cleaning_predict(self, dk: FreqaiDataKitchen, dataframe: DataFrame) -> None: """ Base data cleaning method for predict. - These functions each modify dk.do_predict, which is a dataframe with equal length - to the number of candles coming from and returning to the strategy. Inside do_predict, - 1 allows prediction and < 0 signals to the strategy that the model is not confident in - the prediction. - See FreqaiDataKitchen::remove_outliers() for an example - of how the do_predict vector is modified. do_predict is ultimately passed back to strategy - for buy signals. + Functions here are complementary to the functions of data_cleaning_train. """ - if self.freqai_info["feature_parameters"].get( + ft_params = self.freqai_info["feature_parameters"] + + if ft_params.get('inlier_metric_window', 0): + dk.compute_inlier_metric(set_='predict') + + if ft_params.get( "principal_component_analysis", False ): - dk.pca_transform(dataframe) + dk.pca_transform(self.dk.data_dictionary['prediction_features']) - if self.freqai_info["feature_parameters"].get("use_SVM_to_remove_outliers", False): + if ft_params.get("use_SVM_to_remove_outliers", False): dk.use_SVM_to_remove_outliers(predict=True) - if self.freqai_info["feature_parameters"].get("DI_threshold", 0): + if ft_params.get("DI_threshold", 0): dk.check_if_pred_in_training_spaces() - if self.freqai_info["feature_parameters"].get("use_DBSCAN_to_remove_outliers", False): + if ft_params.get("use_DBSCAN_to_remove_outliers", False): dk.use_DBSCAN_to_remove_outliers(predict=True) def model_exists( @@ -503,7 +512,7 @@ class IFreqaiModel(ABC): Path(self.full_path, Path(self.config["config_files"][0]).name), ) - def train_model_in_series( + def extract_data_and_train_model( self, new_trained_timerange: TimeRange, pair: str, @@ -595,7 +604,7 @@ class IFreqaiModel(ABC): # # for keras type models, the conv_window needs to be prepended so # # viewing is correct in frequi - if self.freqai_info.get('keras', False): + if self.freqai_info.get('keras', False) or self.ft_params.get('inlier_metric_window', 0): n_lost_points = self.freqai_info.get('conv_width', 2) zeros_df = DataFrame(np.zeros((n_lost_points, len(hist_preds_df.columns))), columns=hist_preds_df.columns) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1d171ae89..6c001a8d6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -281,16 +281,17 @@ class FreqtradeBot(LoggingMixin): def update_funding_fees(self): if self.trading_mode == TradingMode.FUTURES: trades = Trade.get_open_trades() - for trade in trades: - funding_fees = self.exchange.get_funding_fees( - pair=trade.pair, - amount=trade.amount, - is_short=trade.is_short, - open_date=trade.date_last_filled_utc - ) - trade.funding_fees = funding_fees - else: - return 0.0 + try: + for trade in trades: + funding_fees = self.exchange.get_funding_fees( + pair=trade.pair, + amount=trade.amount, + is_short=trade.is_short, + open_date=trade.date_last_filled_utc + ) + trade.funding_fees = funding_fees + except ExchangeError: + logger.warning("Could not update funding fees for open trades.") def startup_backpopulate_precision(self): @@ -583,7 +584,9 @@ class FreqtradeBot(LoggingMixin): if stake_amount is not None and stake_amount < 0.0: # We should decrease our position - amount = abs(float(FtPrecise(stake_amount) / FtPrecise(current_exit_rate))) + amount = self.exchange.amount_to_contract_precision( + trade.pair, + abs(float(FtPrecise(stake_amount) / FtPrecise(current_exit_rate)))) if amount > trade.amount: # This is currently ineffective as remaining would become < min tradable # Fixing this would require checking for 0.0 there - @@ -592,9 +595,14 @@ class FreqtradeBot(LoggingMixin): f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}") amount = trade.amount + if amount == 0.0: + logger.info("Amount to sell is 0.0 due to exchange limits - not selling.") + return + remaining = (trade.amount - amount) * current_exit_rate if remaining < min_exit_stake: - logger.info(f'Remaining amount of {remaining} would be too small.') + logger.info(f"Remaining amount of {remaining} would be smaller " + f"than the minimum of {min_exit_stake}.") return self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple( @@ -664,14 +672,12 @@ class FreqtradeBot(LoggingMixin): if not stake_amount: return False - if pos_adjust: - logger.info(f"Position adjust: about to create a new order for {pair} with stake: " - f"{stake_amount} for {trade}") - else: - logger.info( - f"{name} signal found: about create a new trade for {pair} with stake_amount: " - f"{stake_amount} ...") - + msg = (f"Position adjust: about to create a new order for {pair} with stake: " + f"{stake_amount} for {trade}" if pos_adjust + else + f"{name} signal found: about create a new trade for {pair} with stake_amount: " + f"{stake_amount} ...") + logger.info(msg) amount = (stake_amount / enter_limit_requested) * leverage order_type = ordertype or self.strategy.order_types['entry'] @@ -734,8 +740,13 @@ class FreqtradeBot(LoggingMixin): # This is a new trade if trade is None: - funding_fees = self.exchange.get_funding_fees( - pair=pair, amount=amount, is_short=is_short, open_date=open_date) + funding_fees = 0.0 + try: + funding_fees = self.exchange.get_funding_fees( + pair=pair, amount=amount, is_short=is_short, open_date=open_date) + except ExchangeError: + logger.warning("Could not find funding fee.") + trade = Trade( pair=pair, base_currency=base_currency, @@ -912,7 +923,7 @@ class FreqtradeBot(LoggingMixin): 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': order.safe_amount_after_fee, + 'amount': order.safe_amount_after_fee if fill else order.amount, 'open_date': trade.open_date or datetime.utcnow(), 'current_rate': current_rate, 'sub_trade': sub_trade, @@ -1486,12 +1497,16 @@ class FreqtradeBot(LoggingMixin): :param exit_check: CheckTuple with signal and reason :return: True if it succeeds False """ - trade.funding_fees = self.exchange.get_funding_fees( - pair=trade.pair, - amount=trade.amount, - is_short=trade.is_short, - open_date=trade.date_last_filled_utc, - ) + try: + trade.funding_fees = self.exchange.get_funding_fees( + pair=trade.pair, + amount=trade.amount, + is_short=trade.is_short, + open_date=trade.date_last_filled_utc, + ) + except ExchangeError: + logger.warning("Could not update funding fee.") + exit_type = 'exit' exit_reason = exit_tag or exit_check.exit_reason if exit_check.exit_type in ( diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8f6b6b332..97418b72c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -537,7 +537,11 @@ class Backtesting: return pos_trade if stake_amount is not None and stake_amount < 0.0: - amount = abs(stake_amount) / current_rate + amount = amount_to_contract_precision( + abs(stake_amount) / current_rate, trade.amount_precision, + self.precision_mode, trade.contract_size) + if amount == 0.0: + return trade if amount > trade.amount: # This is currently ineffective as remaining would become < min tradable amount = trade.amount diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 43728dc1f..aa753a829 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -1,3 +1,5 @@ +from typing import Optional + import pandas as pd from freqtrade.exchange import timeframe_to_minutes @@ -6,7 +8,8 @@ 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, append_timeframe: bool = True, - date_column: str = 'date') -> pd.DataFrame: + date_column: str = 'date', + suffix: Optional[str] = None) -> pd.DataFrame: """ Correctly merge informative samples to the original dataframe, avoiding lookahead bias. @@ -28,6 +31,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :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. + :param suffix: A string suffix to add at the end of the informative columns. If specified, + append_timeframe must be false. :return: Merged dataframe :raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe """ @@ -50,10 +55,16 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, # Rename columns to be unique date_merge = 'date_merge' - if append_timeframe: + if suffix and append_timeframe: + raise ValueError("You can not specify `append_timeframe` as True and a `suffix`.") + elif append_timeframe: date_merge = f'date_merge_{timeframe_inf}' informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] + elif suffix: + date_merge = f'date_merge_{suffix}' + informative.columns = [f"{col}_{suffix}" for col in informative.columns] + # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point if ffill: diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 0e822a028..b172ab805 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -92,12 +92,10 @@ class FreqaiExampleStrategy(IStrategy): t = int(t) informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) - informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t) + informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, timeperiod=t) informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t) informative[f"%-{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t) - informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) - bollinger = qtpylib.bollinger_bands( qtpylib.typical_price(informative), window=t, stds=2.2 ) diff --git a/freqtrade/templates/FreqaiHybridExampleStrategy.py b/freqtrade/templates/FreqaiHybridExampleStrategy.py index 5d1e149dd..286ff012f 100644 --- a/freqtrade/templates/FreqaiHybridExampleStrategy.py +++ b/freqtrade/templates/FreqaiHybridExampleStrategy.py @@ -135,7 +135,7 @@ class FreqaiExampleHybridStrategy(IStrategy): t = int(t) informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t) informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t) - informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t) + informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, timeperiod=t) informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t) informative[f"%-{coin}ema-period_{t}"] = ta.EMA(informative, timeperiod=t) informative[f"%-{coin}roc-period_{t}"] = ta.ROC(informative, timeperiod=t) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3352019a9..71690ecdf 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -11,8 +11,9 @@ import pytest from pandas import DataFrame from freqtrade.enums import CandleType, MarginMode, TradingMode -from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, - OperationalException, PricingError, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError, + InvalidOrderException, OperationalException, PricingError, + TemporaryError) from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, amount_to_precision, date_minus_candles, market_is_active, price_to_precision, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, @@ -4179,17 +4180,24 @@ def test__fetch_and_calculate_funding_fees( type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) + ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock( return_value=['1h', '4h', '8h'])) - funding_fees = exchange._fetch_and_calculate_funding_fees( + funding_fees = ex._fetch_and_calculate_funding_fees( pair='ADA/USDT', amount=amount, is_short=True, open_date=d1, close_date=d2) assert pytest.approx(funding_fees) == expected_fees # Fees for Longs are inverted - funding_fees = exchange._fetch_and_calculate_funding_fees( + funding_fees = ex._fetch_and_calculate_funding_fees( pair='ADA/USDT', amount=amount, is_short=False, open_date=d1, close_date=d2) assert pytest.approx(funding_fees) == -expected_fees + # Return empty "refresh_latest" + mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", return_value={}) + ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) + with pytest.raises(ExchangeError, match="Could not find funding rates."): + ex._fetch_and_calculate_funding_fees( + pair='ADA/USDT', amount=amount, is_short=False, open_date=d1, close_date=d2) + @pytest.mark.parametrize('exchange,expected_fees', [ ('binance', -0.0009140999999999999), @@ -4456,6 +4464,39 @@ def test__amount_to_contracts( assert result_amount == param_amount +@pytest.mark.parametrize('pair,amount,expected_spot,expected_fut', [ + # Contract size of 0.01 + ('ADA/USDT:USDT', 40, 40, 40), + ('ADA/USDT:USDT', 10.4445555, 10.4, 10.444), + ('LTC/ETH', 30, 30, 30), + ('LTC/USD', 30, 30, 30), + # contract size of 10 + ('ETH/USDT:USDT', 10.111, 10.1, 10), + ('ETH/USDT:USDT', 10.188, 10.1, 10), + ('ETH/USDT:USDT', 10.988, 10.9, 10), +]) +def test_amount_to_contract_precision( + mocker, + default_conf, + pair, + amount, + expected_spot, + expected_fut, +): + api_mock = MagicMock() + default_conf['trading_mode'] = 'spot' + default_conf['margin_mode'] = 'isolated' + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + result_size = exchange.amount_to_contract_precision(pair, amount) + assert result_size == expected_spot + + default_conf['trading_mode'] = 'futures' + exchange = get_patched_exchange(mocker, default_conf, api_mock) + result_size = exchange.amount_to_contract_precision(pair, amount) + assert result_size == expected_fut + + @pytest.mark.parametrize('exchange_name,open_rate,is_short,trading_mode,margin_mode', [ # Bittrex ('bittrex', 2.0, False, 'spot', None), diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index db499631b..2c6210a0e 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -81,6 +81,37 @@ def get_patched_freqaimodel(mocker, freqaiconf): return freqaimodel +def make_unfiltered_dataframe(mocker, freqai_conf): + freqai_conf.update({"timerange": "20180110-20180130"}) + + strategy = get_patched_freqai_strategy(mocker, freqai_conf) + exchange = get_patched_exchange(mocker, freqai_conf) + strategy.dp = DataProvider(freqai_conf, exchange) + strategy.freqai_info = freqai_conf.get("freqai", {}) + freqai = strategy.freqai + freqai.live = True + freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.pair = "ADA/BTC" + data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk) + + freqai.dd.pair_dict = MagicMock() + + new_timerange = TimeRange.parse_timerange("20180120-20180130") + + corr_dataframes, base_dataframes = freqai.dd.get_base_and_corr_dataframes( + data_load_timerange, freqai.dk.pair, freqai.dk + ) + + unfiltered_dataframe = freqai.dk.use_strategy_to_populate_indicators( + strategy, corr_dataframes, base_dataframes, freqai.dk.pair + ) + + unfiltered_dataframe = freqai.dk.slice_dataframe(new_timerange, unfiltered_dataframe) + + return freqai, unfiltered_dataframe + + def make_data_dictionary(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) @@ -92,12 +123,11 @@ def make_data_dictionary(mocker, freqai_conf): freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) freqai.dk.pair = "ADA/BTC" - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) + data_load_timerange = TimeRange.parse_timerange("20180110-20180130") + freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk) freqai.dd.pair_dict = MagicMock() - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") corr_dataframes, base_dataframes = freqai.dd.get_base_and_corr_dataframes( diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index 908223c20..a9e7eac51 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -6,7 +6,8 @@ import pytest from freqtrade.exceptions import OperationalException from tests.conftest import log_has_re -from tests.freqai.conftest import get_patched_data_kitchen, make_data_dictionary +from tests.freqai.conftest import (get_patched_data_kitchen, make_data_dictionary, + make_unfiltered_dataframe) @pytest.mark.parametrize( @@ -91,3 +92,72 @@ def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf, "SVM detected 8.09%", caplog, ) + + +def test_compute_inlier_metric(mocker, freqai_conf, caplog): + freqai = make_data_dictionary(mocker, freqai_conf) + freqai_conf['freqai']['feature_parameters'].update({"inlier_metric_window": 10}) + freqai.dk.compute_inlier_metric(set_='train') + assert log_has_re( + "Inlier metric computed and added to features.", + caplog, + ) + + +def test_add_noise_to_training_features(mocker, freqai_conf): + freqai = make_data_dictionary(mocker, freqai_conf) + freqai_conf['freqai']['feature_parameters'].update({"noise_standard_deviation": 0.1}) + freqai.dk.add_noise_to_training_features() + + +def test_remove_beginning_points_from_data_dict(mocker, freqai_conf): + freqai = make_data_dictionary(mocker, freqai_conf) + freqai.dk.remove_beginning_points_from_data_dict(set_='train') + + +def test_principal_component_analysis(mocker, freqai_conf, caplog): + freqai = make_data_dictionary(mocker, freqai_conf) + freqai.dk.principal_component_analysis() + assert log_has_re( + "reduced feature dimension by", + caplog, + ) + + +def test_normalize_data(mocker, freqai_conf): + freqai = make_data_dictionary(mocker, freqai_conf) + data_dict = freqai.dk.data_dictionary + freqai.dk.normalize_data(data_dict) + assert len(freqai.dk.data) == 56 + + +def test_filter_features(mocker, freqai_conf): + freqai, unfiltered_dataframe = make_unfiltered_dataframe(mocker, freqai_conf) + freqai.dk.find_features(unfiltered_dataframe) + + filtered_df, labels = freqai.dk.filter_features( + unfiltered_dataframe, + freqai.dk.training_features_list, + freqai.dk.label_list, + training_filter=True, + ) + + assert len(filtered_df.columns) == 26 + + +def test_make_train_test_datasets(mocker, freqai_conf): + freqai, unfiltered_dataframe = make_unfiltered_dataframe(mocker, freqai_conf) + freqai.dk.find_features(unfiltered_dataframe) + + features_filtered, labels_filtered = freqai.dk.filter_features( + unfiltered_dataframe, + freqai.dk.training_features_list, + freqai.dk.label_list, + training_filter=True, + ) + + data_dictionary = freqai.dk.make_train_test_datasets(features_filtered, labels_filtered) + + assert data_dictionary + assert len(data_dictionary) == 7 + assert len(data_dictionary['train_features'].index) == 1916 diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index ff0eb24a9..d95ead90c 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -17,7 +17,7 @@ def is_arm() -> bool: return "arm" in machine or "aarch64" in machine -def test_train_model_in_series_LightGBM(mocker, freqai_conf): +def test_extract_data_and_train_model_LightGBM(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) @@ -35,7 +35,8 @@ def test_train_model_in_series_LightGBM(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() @@ -45,7 +46,7 @@ def test_train_model_in_series_LightGBM(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_train_model_in_series_LightGBMMultiModel(mocker, freqai_conf): +def test_extract_data_and_train_model_LightGBMMultiModel(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"strategy": "freqai_test_multimodel_strat"}) freqai_conf.update({"freqaimodel": "LightGBMRegressorMultiTarget"}) @@ -64,7 +65,8 @@ def test_train_model_in_series_LightGBMMultiModel(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert len(freqai.dk.label_list) == 2 assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() @@ -77,7 +79,7 @@ def test_train_model_in_series_LightGBMMultiModel(mocker, freqai_conf): @pytest.mark.skipif(is_arm(), reason="no ARM for Catboost ...") -def test_train_model_in_series_Catboost(mocker, freqai_conf): +def test_extract_data_and_train_model_Catboost(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"freqaimodel": "CatboostRegressor"}) # freqai_conf.get('freqai', {}).update( @@ -98,8 +100,8 @@ def test_train_model_in_series_Catboost(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", + strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() @@ -110,7 +112,7 @@ def test_train_model_in_series_Catboost(mocker, freqai_conf): @pytest.mark.skipif(is_arm(), reason="no ARM for Catboost ...") -def test_train_model_in_series_CatboostClassifier(mocker, freqai_conf): +def test_extract_data_and_train_model_CatboostClassifier(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"freqaimodel": "CatboostClassifier"}) freqai_conf.update({"strategy": "freqai_test_classifier"}) @@ -130,8 +132,8 @@ def test_train_model_in_series_CatboostClassifier(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", + strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() @@ -141,7 +143,7 @@ def test_train_model_in_series_CatboostClassifier(mocker, freqai_conf): shutil.rmtree(Path(freqai.dk.full_path)) -def test_train_model_in_series_LightGBMClassifier(mocker, freqai_conf): +def test_extract_data_and_train_model_LightGBMClassifier(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"freqaimodel": "LightGBMClassifier"}) freqai_conf.update({"strategy": "freqai_test_classifier"}) @@ -161,8 +163,8 @@ def test_train_model_in_series_LightGBMClassifier(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", - strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", + strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() @@ -358,7 +360,8 @@ def test_follow_mode(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() @@ -407,7 +410,8 @@ def test_principal_component_analysis(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - freqai.train_model_in_series(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) + freqai.extract_data_and_train_model( + new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_pca_object.pkl") diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index a7c2da26a..8cb990e87 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -117,6 +117,29 @@ def test_merge_informative_pair_lower(): merge_informative_pair(data, informative, '1h', '15m', ffill=True) +def test_merge_informative_pair_suffix(): + data = generate_test_data('15m', 20) + informative = generate_test_data('1h', 20) + + result = merge_informative_pair(data, informative, '15m', '1h', + append_timeframe=False, suffix="suf") + + assert 'date' in result.columns + assert result['date'].equals(data['date']) + assert 'date_suf' in result.columns + + assert 'open_suf' in result.columns + assert 'open_1h' not in result.columns + + +def test_merge_informative_pair_suffix_append_timeframe(): + data = generate_test_data('15m', 20) + informative = generate_test_data('1h', 20) + + with pytest.raises(ValueError, match=r"You can not specify `append_timeframe` .*"): + merge_informative_pair(data, informative, '15m', '1h', suffix="suf") + + def test_stoploss_from_open(): open_price_ranges = [ [0.01, 1.00, 30], diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index aff0504b3..565797d81 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -506,7 +506,7 @@ def test_create_trades_multiple_trades( def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker, - limit_buy_order_usdt_open) -> None: + limit_buy_order_usdt_open, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf_usdt['max_open_trades'] = 4 @@ -515,6 +515,7 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value=limit_buy_order_usdt_open), get_fee=fee, + get_funding_fees=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf_usdt) patch_get_signal(freqtrade) @@ -522,6 +523,7 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker, # Create 2 existing trades freqtrade.execute_entry('ETH/USDT', default_conf_usdt['stake_amount']) freqtrade.execute_entry('NEO/BTC', default_conf_usdt['stake_amount']) + assert log_has("Could not find funding fee.", caplog) assert len(Trade.get_open_trades()) == 2 # Change order_id for new orders @@ -3655,6 +3657,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 3 assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.ENTRY + assert rpc_mock.call_args_list[0][0][0]['amount'] > 20 assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.ENTRY_FILL assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.EXIT_FILL @@ -3665,7 +3668,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( (True, 29.70297029, 2.2, 2.3, -8.63762376, -0.1443212, 'loss'), ]) def test_execute_trade_exit_market_order( - default_conf_usdt, ticker_usdt, fee, is_short, current_rate, amount, + default_conf_usdt, ticker_usdt, fee, is_short, current_rate, amount, caplog, limit, profit_amount, profit_ratio, profit_or_loss, ticker_usdt_sell_up, mocker ) -> None: """ @@ -3693,6 +3696,7 @@ def test_execute_trade_exit_market_order( fetch_ticker=ticker_usdt, get_fee=fee, _is_dry_limit_order_filled=MagicMock(return_value=True), + get_funding_fees=MagicMock(side_effect=ExchangeError()), ) patch_whitelist(mocker, default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt) @@ -3718,6 +3722,7 @@ def test_execute_trade_exit_market_order( limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], exit_check=ExitCheckTuple(exit_type=ExitType.ROI) ) + assert log_has("Could not update funding fee.", caplog) assert not trade.is_open assert pytest.approx(trade.close_profit) == profit_ratio @@ -5429,6 +5434,16 @@ def test_update_funding_fees( )) +def test_update_funding_fees_error(mocker, default_conf, caplog): + mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', side_effect=ExchangeError()) + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + freqtrade = get_patched_freqtradebot(mocker, default_conf) + freqtrade.update_funding_fees() + + log_has("Could not update funding fees for open trades.", caplog) + + def test_position_adjust(mocker, default_conf_usdt, fee) -> None: patch_RPCManager(mocker) patch_exchange(mocker) diff --git a/tests/test_integration.py b/tests/test_integration.py index dd3488f81..77ed822d1 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -485,7 +485,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> Non assert len(trade.orders) == 1 assert pytest.approx(trade.stake_amount) == 60 assert pytest.approx(trade.amount) == 30.0 - assert log_has_re("Remaining amount of 1.6.* would be too small.", caplog) + assert log_has_re("Remaining amount of 1.6.* would be smaller than the minimum of 10.", caplog) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-20) @@ -504,9 +504,21 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> Non freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-50) freqtrade.process() assert log_has_re("Adjusting amount to trade.amount as it is higher.*", caplog) - assert log_has_re("Remaining amount of 0.0 would be too small.", caplog) + assert log_has_re("Remaining amount of 0.0 would be smaller than the minimum of 10.", caplog) trade = Trade.get_trades().first() assert len(trade.orders) == 2 assert trade.orders[-1].ft_order_side == 'sell' assert pytest.approx(trade.stake_amount) == 40.198 assert trade.is_open + + # use amount that would trunc to 0.0 once selling + mocker.patch("freqtrade.exchange.Exchange.amount_to_contract_precision", + lambda s, p, v: round(v, 1)) + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-0.01) + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + assert trade.orders[-1].ft_order_side == 'sell' + assert pytest.approx(trade.stake_amount) == 40.198 + assert trade.is_open + assert log_has_re('Amount to sell is 0.0 due to exchange limits - not selling.', caplog)