Merge feat/freqai into develop to get new features
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -7,6 +7,10 @@ logfile.txt | |||||||
| user_data/* | user_data/* | ||||||
| !user_data/strategy/sample_strategy.py | !user_data/strategy/sample_strategy.py | ||||||
| !user_data/notebooks | !user_data/notebooks | ||||||
|  | !user_data/models | ||||||
|  | !user_data/freqaimodels | ||||||
|  | user_data/freqaimodels/* | ||||||
|  | user_data/models/* | ||||||
| user_data/notebooks/* | user_data/notebooks/* | ||||||
| freqtrade-plot.html | freqtrade-plot.html | ||||||
| freqtrade-profit-plot.html | freqtrade-profit-plot.html | ||||||
| @@ -105,3 +109,5 @@ target/ | |||||||
| !config_examples/config_ftx.example.json | !config_examples/config_ftx.example.json | ||||||
| !config_examples/config_full.example.json | !config_examples/config_full.example.json | ||||||
| !config_examples/config_kraken.example.json | !config_examples/config_kraken.example.json | ||||||
|  | !config_examples/config_freqai_futures.example.json | ||||||
|  | !config_examples/config_freqai_spot.example.json | ||||||
|   | |||||||
							
								
								
									
										102
									
								
								config_examples/config_freqai_futures.example.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								config_examples/config_freqai_futures.example.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | |||||||
|  | { | ||||||
|  |     "trading_mode": "futures", | ||||||
|  |     "margin_mode": "isolated", | ||||||
|  |     "max_open_trades": 5, | ||||||
|  |     "stake_currency": "USDT", | ||||||
|  |     "stake_amount": 200, | ||||||
|  |     "tradable_balance_ratio": 1, | ||||||
|  |     "fiat_display_currency": "USD", | ||||||
|  |     "dry_run": true, | ||||||
|  |     "timeframe": "3m", | ||||||
|  |     "dry_run_wallet": 1000, | ||||||
|  |     "cancel_open_orders_on_exit": true, | ||||||
|  |     "unfilledtimeout": { | ||||||
|  |         "entry": 10, | ||||||
|  |         "exit": 30 | ||||||
|  |     }, | ||||||
|  |     "exchange": { | ||||||
|  |         "name": "okx", | ||||||
|  |         "key": "", | ||||||
|  |         "secret": "", | ||||||
|  |         "ccxt_config": { | ||||||
|  |             "enableRateLimit": true | ||||||
|  |         }, | ||||||
|  |         "ccxt_async_config": { | ||||||
|  |             "enableRateLimit": true, | ||||||
|  |             "rateLimit": 200 | ||||||
|  |         }, | ||||||
|  |         "pair_whitelist": [ | ||||||
|  |             "AGLD/USDT:USDT", | ||||||
|  |             "1INCH/USDT:USDT", | ||||||
|  |             "AAVE/USDT:USDT", | ||||||
|  |             "ALGO/USDT:USDT", | ||||||
|  |             "ALPHA/USDT:USDT", | ||||||
|  |             "API3/USDT:USDT", | ||||||
|  |             "AVAX/USDT:USDT", | ||||||
|  |             "AXS/USDT:USDT", | ||||||
|  |             "BCH/USDT:USDT" | ||||||
|  |         ], | ||||||
|  |         "pair_blacklist": [] | ||||||
|  |     }, | ||||||
|  |     "entry_pricing": { | ||||||
|  |         "price_side": "same", | ||||||
|  |         "use_order_book": true, | ||||||
|  |         "order_book_top": 1, | ||||||
|  |         "price_last_balance": 0.0, | ||||||
|  |         "check_depth_of_market": { | ||||||
|  |             "enabled": false, | ||||||
|  |             "bids_to_ask_delta": 1 | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     "exit_pricing": { | ||||||
|  |         "price_side": "other", | ||||||
|  |         "use_order_book": true, | ||||||
|  |         "order_book_top": 1 | ||||||
|  |     }, | ||||||
|  |     "pairlists": [ | ||||||
|  |         { | ||||||
|  |             "method": "StaticPairList" | ||||||
|  |         } | ||||||
|  |     ], | ||||||
|  |     "freqai": { | ||||||
|  |         "startup_candles": 10000, | ||||||
|  |         "timeframes": [ | ||||||
|  |             "3m", | ||||||
|  |             "15m", | ||||||
|  |             "1h" | ||||||
|  |         ], | ||||||
|  |         "train_period": 20, | ||||||
|  |         "backtest_period": 0.001, | ||||||
|  |         "identifier": "constant_retrain_live", | ||||||
|  |         "live_trained_timestamp": 0, | ||||||
|  |         "corr_pairlist": [ | ||||||
|  |             "BTC/USDT:USDT", | ||||||
|  |             "ETH/USDT:USDT" | ||||||
|  |         ], | ||||||
|  |         "feature_parameters": { | ||||||
|  |             "period": 20, | ||||||
|  |             "shift": 2, | ||||||
|  |             "DI_threshold": 0.9, | ||||||
|  |             "weight_factor": 0.9, | ||||||
|  |             "principal_component_analysis": false, | ||||||
|  |             "use_SVM_to_remove_outliers": true, | ||||||
|  |             "stratify": 0, | ||||||
|  |             "indicator_max_period": 20, | ||||||
|  |             "indicator_periods": [10, 20] | ||||||
|  |         }, | ||||||
|  |         "data_split_parameters": { | ||||||
|  |             "test_size": 0.33, | ||||||
|  |             "random_state": 1 | ||||||
|  |         }, | ||||||
|  |         "model_training_parameters": { | ||||||
|  |             "n_estimators": 1000, | ||||||
|  |             "task_type": "CPU" | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     "bot_name": "", | ||||||
|  |     "force_entry_enable": true, | ||||||
|  |     "initial_state": "running", | ||||||
|  |     "internals": { | ||||||
|  |         "process_throttle_secs": 5 | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										97
									
								
								config_examples/config_freqai_spot.example.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								config_examples/config_freqai_spot.example.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | |||||||
|  | { | ||||||
|  |     "max_open_trades": 1, | ||||||
|  |     "stake_currency": "USDT", | ||||||
|  |     "stake_amount": 900, | ||||||
|  |     "tradable_balance_ratio": 1, | ||||||
|  |     "fiat_display_currency": "USD", | ||||||
|  |     "dry_run": true, | ||||||
|  |     "timeframe": "5m", | ||||||
|  |     "dry_run_wallet": 4000, | ||||||
|  |     "dataformat_ohlcv": "json", | ||||||
|  |     "cancel_open_orders_on_exit": true, | ||||||
|  |     "unfilledtimeout": { | ||||||
|  |         "entry": 10, | ||||||
|  |         "exit": 30 | ||||||
|  |     }, | ||||||
|  |     "exchange": { | ||||||
|  |         "name": "binance", | ||||||
|  |         "key": "", | ||||||
|  |         "secret": "", | ||||||
|  |         "ccxt_config": { | ||||||
|  |             "enableRateLimit": true | ||||||
|  |         }, | ||||||
|  |         "ccxt_async_config": { | ||||||
|  |             "enableRateLimit": true, | ||||||
|  |             "rateLimit": 200 | ||||||
|  |         }, | ||||||
|  |         "pair_whitelist": [ | ||||||
|  |             "BTC/USDT", | ||||||
|  |             "ETH/USDT" | ||||||
|  |         ], | ||||||
|  |         "pair_blacklist": [] | ||||||
|  |     }, | ||||||
|  |     "entry_pricing": { | ||||||
|  |         "price_side": "same", | ||||||
|  |         "use_order_book": true, | ||||||
|  |         "order_book_top": 1, | ||||||
|  |         "price_last_balance": 0.0, | ||||||
|  |         "check_depth_of_market": { | ||||||
|  |             "enabled": false, | ||||||
|  |             "bids_to_ask_delta": 1 | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     "exit_pricing": { | ||||||
|  |         "price_side": "other", | ||||||
|  |         "use_order_book": true, | ||||||
|  |         "order_book_top": 1 | ||||||
|  |     }, | ||||||
|  |     "pairlists": [ | ||||||
|  |         { | ||||||
|  |             "method": "StaticPairList" | ||||||
|  |         } | ||||||
|  |     ], | ||||||
|  |     "freqai": { | ||||||
|  |         "startup_candles": 10000, | ||||||
|  |         "timeframes": [ | ||||||
|  |             "5m", | ||||||
|  |             "15m", | ||||||
|  |             "4h" | ||||||
|  |         ], | ||||||
|  |         "train_period": 30, | ||||||
|  |         "backtest_period": 7, | ||||||
|  |         "identifier": "example", | ||||||
|  |         "live_trained_timestamp": 0, | ||||||
|  |         "corr_pairlist": [ | ||||||
|  |             "BTC/USDT", | ||||||
|  |             "ETH/USDT", | ||||||
|  |             "DOT/USDT", | ||||||
|  |             "MATIC/USDT", | ||||||
|  |             "SOL/USDT" | ||||||
|  |         ], | ||||||
|  |         "feature_parameters": { | ||||||
|  |             "period": 500, | ||||||
|  |             "shift": 1, | ||||||
|  |             "DI_threshold": 0, | ||||||
|  |             "weight_factor": 0.9, | ||||||
|  |             "principal_component_analysis": false, | ||||||
|  |             "use_SVM_to_remove_outliers": false, | ||||||
|  |             "stratify": 0, | ||||||
|  |             "indicator_max_period": 50, | ||||||
|  |             "indicator_periods": [10, 20] | ||||||
|  |         }, | ||||||
|  |         "data_split_parameters": { | ||||||
|  |             "test_size": 0.33, | ||||||
|  |             "random_state": 1 | ||||||
|  |         }, | ||||||
|  |         "model_training_parameters": { | ||||||
|  |             "n_estimators": 1000, | ||||||
|  |             "task_type": "CPU" | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     "bot_name": "", | ||||||
|  |     "initial_state": "running", | ||||||
|  |     "forcebuy_enable": false, | ||||||
|  |     "internals": { | ||||||
|  |         "process_throttle_secs": 5 | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										17
									
								
								docker/Dockerfile.freqai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								docker/Dockerfile.freqai
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | ARG sourceimage=freqtradeorg/freqtrade | ||||||
|  | ARG sourcetag=develop | ||||||
|  | FROM ${sourceimage}:${sourcetag} | ||||||
|  |  | ||||||
|  | USER root | ||||||
|  |  | ||||||
|  | RUN apt-get install -y libgomp1 | ||||||
|  |  | ||||||
|  | USER ftuser | ||||||
|  |  | ||||||
|  | # Install dependencies | ||||||
|  | COPY requirements-freqai.txt /freqtrade/ | ||||||
|  |  | ||||||
|  | RUN pip install -r requirements-freqai.txt --user --no-cache-dir | ||||||
|  | # Temporary step - as the source image will contain the wrong (non-freqai) sourcecode | ||||||
|  | COPY --chown=ftuser:ftuser . /freqtrade/ | ||||||
|  |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								docs/assets/weights_factor.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/assets/weights_factor.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 126 KiB | 
							
								
								
									
										534
									
								
								docs/freqai.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										534
									
								
								docs/freqai.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,534 @@ | |||||||
|  | # Freqai | ||||||
|  |  | ||||||
|  | !!! Note | ||||||
|  |         Freqai is still experimental, and should be used at the user's own discretion. | ||||||
|  |  | ||||||
|  | Freqai is a module designed to automate a variety of tasks associated with | ||||||
|  | training a predictive model to provide signals based on input features. | ||||||
|  |  | ||||||
|  | Among the the features included: | ||||||
|  |  | ||||||
|  | * Easy large feature set construction based on simple user input | ||||||
|  | * Sweep model training and backtesting to simulate consistent model retraining through time | ||||||
|  | * Smart outlier removal of data points from prediction sets using a Dissimilarity Index. | ||||||
|  | * Data dimensionality reduction with Principal Component Analysis | ||||||
|  | * Automatic file management for storage of models to be reused during live | ||||||
|  | * Smart and safe data standardization | ||||||
|  | * Cleaning of NaNs from the data set before training and prediction. | ||||||
|  | * Automated live retraining (still VERY experimental. Proceed with caution.) | ||||||
|  |  | ||||||
|  | ## General approach | ||||||
|  |  | ||||||
|  | The user provides FreqAI with a set of custom indicators (created inside the strategy the same way | ||||||
|  | a typical Freqtrade strategy is created) as well as a target value (typically some price change into | ||||||
|  | the future). FreqAI trains a model to predict the target value based on the input of custom indicators.  | ||||||
|  | FreqAI will train and save a new model for each pair in the config whitelist.  | ||||||
|  | Users employ FreqAI to backtest a strategy (emulate reality with retraining a model as new data is  | ||||||
|  | introduced) and run the model live to generate buy and sell signals.  | ||||||
|  |  | ||||||
|  | ## Background and vocabulary | ||||||
|  |  | ||||||
|  | **Features** are the quantities with which a model is trained. $X_i$ represents the | ||||||
|  | vector of all features for a single candle. In Freqai, the user | ||||||
|  | builds the features from anything they can construct in the strategy. | ||||||
|  |  | ||||||
|  | **Labels** are the target values with which the weights inside a model are trained | ||||||
|  | toward. Each set of features is associated with a single label, which is also | ||||||
|  | defined within the strategy by the user. These labels look forward into the | ||||||
|  | future, and are not available to the model during dryrun/live/backtesting. | ||||||
|  |  | ||||||
|  | **Training** refers to the process of feeding individual feature sets into the | ||||||
|  | model with associated labels with the goal of matching input feature sets to | ||||||
|  | associated labels. | ||||||
|  |  | ||||||
|  | **Train data** is a subset of the historic data which is fed to the model during | ||||||
|  | training to adjust weights. This data directly influences weight connections | ||||||
|  | in the model. | ||||||
|  |  | ||||||
|  | **Test data** is a subset of the historic data which is used to evaluate the | ||||||
|  | intermediate performance of the model during training. This data does not | ||||||
|  | directly influence nodal weights within the model. | ||||||
|  |  | ||||||
|  | ## Install prerequisites | ||||||
|  |  | ||||||
|  | Use `pip` to install the prerequisites with: | ||||||
|  |  | ||||||
|  | `pip install -r requirements-freqai.txt` | ||||||
|  |  | ||||||
|  | ## Running from the example files | ||||||
|  |  | ||||||
|  | An example strategy, an example prediction model, and example config can all be found in  | ||||||
|  | `freqtrade/templates/ExampleFreqaiStrategy.py`,  | ||||||
|  | `freqtrade/freqai/prediction_models/CatboostPredictionModel.py`,  | ||||||
|  | `config_examples/config_freqai.example.json`, respectively. Assuming the user has downloaded | ||||||
|  | the necessary data, Freqai can be executed from these templates with: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | freqtrade backtesting --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel CatboostPredictionModel --strategy-path freqtrade/templates --timerange 20220101-20220201 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Configuring the bot | ||||||
|  |  | ||||||
|  | ### Example config file | ||||||
|  |  | ||||||
|  | The user interface is isolated to the typical config file. A typical Freqai | ||||||
|  | config setup includes: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  |     "freqai": { | ||||||
|  |                 "startup_candles": 10000, | ||||||
|  |                 "timeframes" : ["5m","15m","4h"], | ||||||
|  |                 "train_period" : 30, | ||||||
|  |                 "backtest_period" : 7, | ||||||
|  |                 "identifier" :  "unique-id", | ||||||
|  |                 "corr_pairlist": [ | ||||||
|  |                         "ETH/USD", | ||||||
|  |                         "LINK/USD", | ||||||
|  |                         "BNB/USD" | ||||||
|  |                 ], | ||||||
|  |                 "feature_parameters" : { | ||||||
|  |                         "period": 24, | ||||||
|  |                         "shift": 2, | ||||||
|  |                         "weight_factor":  0, | ||||||
|  |                 }, | ||||||
|  |                 "data_split_parameters" : { | ||||||
|  |                     "test_size": 0.25, | ||||||
|  |                     "random_state": 42 | ||||||
|  |                 }, | ||||||
|  |                 "model_training_parameters" : { | ||||||
|  |                     "n_estimators": 100, | ||||||
|  |                     "random_state": 42, | ||||||
|  |                     "learning_rate": 0.02, | ||||||
|  |                     "task_type": "CPU", | ||||||
|  |                 }, | ||||||
|  |         } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Building the feature set | ||||||
|  |  | ||||||
|  | !! slightly out of date, please refer to templates/FreqaiExampleStrategy.py for updated method !! | ||||||
|  | Features are added by the user inside the `populate_any_indicators()` method of the strategy  | ||||||
|  | by prepending indicators with `%`: | ||||||
|  |  | ||||||
|  | ```python | ||||||
|  |     def populate_any_indicators(self, metadata, pair, df, tf, informative=None, coin=""): | ||||||
|  |         informative['%-' + coin + "rsi"] = ta.RSI(informative, timeperiod=14) | ||||||
|  |         informative['%-' + coin + "mfi"] = ta.MFI(informative, timeperiod=25) | ||||||
|  |         informative['%-' + coin + "adx"] = ta.ADX(informative, window=20) | ||||||
|  |         bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(informative), window=14, stds=2.2) | ||||||
|  |         informative[coin + "bb_lowerband"] = bollinger["lower"] | ||||||
|  |         informative[coin + "bb_middleband"] = bollinger["mid"] | ||||||
|  |         informative[coin + "bb_upperband"] = bollinger["upper"] | ||||||
|  |         informative['%-' + coin + "bb_width"] = ( | ||||||
|  |             informative[coin + "bb_upperband"] - informative[coin + "bb_lowerband"] | ||||||
|  |         ) / informative[coin + "bb_middleband"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  |          | ||||||
|  |         # The following code automatically adds features according to the `shift` parameter passed | ||||||
|  |         # in the config. Do not remove | ||||||
|  |         indicators = [col for col in informative if col.startswith('%')] | ||||||
|  |         for n in range(self.freqai_info["feature_parameters"]["shift"] + 1): | ||||||
|  |             if n == 0: | ||||||
|  |                 continue | ||||||
|  |             informative_shift = informative[indicators].shift(n) | ||||||
|  |             informative_shift = informative_shift.add_suffix("_shift-" + str(n)) | ||||||
|  |             informative = pd.concat((informative, informative_shift), axis=1) | ||||||
|  |  | ||||||
|  |         # The following code safely merges into the base timeframe. | ||||||
|  |         # Do not remove. | ||||||
|  |         df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True) | ||||||
|  |         skip_columns = [(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]] | ||||||
|  |         df = df.drop(columns=skip_columns) | ||||||
|  | ``` | ||||||
|  | The user of the present example does not want to pass the `bb_lowerband` as a feature to the model,  | ||||||
|  | and has therefore not prepended it with `%`. The user does, however, wish to pass `bb_width` to the | ||||||
|  | model for training/prediction and has therfore prepended it with `%`._ | ||||||
|  |  | ||||||
|  | Note: features **must** be defined in `populate_any_indicators()`. Making features in `populate_indicators()` | ||||||
|  | will fail in live/dry. If the user wishes to add generalized features that are not associated with  | ||||||
|  | a specific pair or timeframe, they should use the following structure inside `populate_any_indicators()` | ||||||
|  | (as exemplified in `freqtrade/templates/FreqaiExampleStrategy.py`: | ||||||
|  |  | ||||||
|  | ```python | ||||||
|  |     def populate_any_indicators(self, metadata, pair, df, tf, informative=None, coin=""): | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         # Add generalized indicators here (because in live, it will call only this function to populate  | ||||||
|  |         # indicators for retraining). Notice how we ensure not to add them multiple times by associating | ||||||
|  |         # these generalized indicators to the basepair/timeframe | ||||||
|  |         if pair == metadata['pair'] and tf == self.timeframe: | ||||||
|  |             df['%-day_of_week'] = (df["date"].dt.dayofweek + 1) / 7 | ||||||
|  |             df['%-hour_of_day'] = (df['date'].dt.hour + 1) / 25 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | (Please see the example script located in `freqtrade/templates/FreqaiExampleStrategy.py` for a full example of `populate_any_indicators()`) | ||||||
|  |  | ||||||
|  | The `timeframes` from the example config above are the timeframes of each `populate_any_indicator()` | ||||||
|  |  included metric for inclusion in the feature set. In the present case, the user is asking for the | ||||||
|  | `5m`, `15m`, and `4h` timeframes of the `rsi`, `mfi`, `roc`, and `bb_width` to be included | ||||||
|  | in the feature set. | ||||||
|  |  | ||||||
|  | In addition, the user can ask for each of these features to be included from | ||||||
|  | informative pairs using the `corr_pairlist`. This means that the present feature | ||||||
|  | set will include all the `base_features` on all the `timeframes` for each of | ||||||
|  | `ETH/USD`, `LINK/USD`, and `BNB/USD`. | ||||||
|  |  | ||||||
|  | `shift` is another user controlled parameter which indicates the number of previous | ||||||
|  | candles to include in the present feature set. In other words, `shift: 2`, tells | ||||||
|  | Freqai to include the the past 2 candles for each of the features included | ||||||
|  | in the dataset. | ||||||
|  |  | ||||||
|  | In total, the number of features the present user has created is:_ | ||||||
|  |  | ||||||
|  | no. `timeframes` * no. `base_features` * no. `corr_pairlist` * no. `shift`_ | ||||||
|  | 3 * 3 * 3 * 2 = 54._ | ||||||
|  |  | ||||||
|  | ### Deciding the sliding training window and backtesting duration | ||||||
|  |  | ||||||
|  | Users define the backtesting timerange with the typical `--timerange` parameter in the user | ||||||
|  | configuration file. `train_period` is the duration of the sliding training window, while | ||||||
|  | `backtest_period` is the sliding backtesting window, both in number of days (backtest_period can be | ||||||
|  | a float to indicate sub daily retraining in live/dry mode). In the present example, | ||||||
|  | the user is asking Freqai to use a training period of 30 days and backtest the subsequent 7 days. | ||||||
|  | This means that if the user sets `--timerange 20210501-20210701`,  | ||||||
|  | Freqai will train 8 separate models (because the full range comprises 8 weeks), | ||||||
|  | and then backtest the subsequent week associated with each of the 8 training | ||||||
|  | data set timerange months. Users can think of this as a "sliding window" which | ||||||
|  | emulates Freqai retraining itself once per week in live using the previous | ||||||
|  | month of data._ | ||||||
|  |  | ||||||
|  | In live, the required training data is automatically computed and downloaded. However, in backtesting | ||||||
|  | the user must manually enter the required number of `startup_candles` in the config. This value | ||||||
|  | is used to increase the available data to FreqAI and should be sufficient to enable all indicators  | ||||||
|  | to be NaN free at the beginning of the first training timerange. This boils down to identifying the  | ||||||
|  | highest timeframe (`4h` in present example)  and the longest indicator period (25 in present example) | ||||||
|  | and adding this to the `train_period`. The units need to be in the base candle time frame:_ | ||||||
|  |  | ||||||
|  | `startup_candles` = ( 4 hours * 25 max period * 60 minutes/hour + 30 day train_period * 1440 minutes per day ) / 5 min (base time frame) = 1488. | ||||||
|  |  | ||||||
|  | !!! Note: in dry/live, this is all precomputed and handled automatically. Thus, `startup_candle` has no  | ||||||
|  | influence on dry/live. | ||||||
|  |  | ||||||
|  | ## Running Freqai | ||||||
|  |  | ||||||
|  | ### Training and backtesting | ||||||
|  |  | ||||||
|  | The freqai training/backtesting module can be executed with the following command: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | freqtrade backtesting --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel CatboostPredictionModel --timerange 20210501-20210701 | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | If this command has never been executed with the existing config file, then it will train a new model  | ||||||
|  | for each pair, for each backtesting window within the bigger `--timerange`._ | ||||||
|  |  | ||||||
|  | --- | ||||||
|  | **NOTE** | ||||||
|  | Once the training is completed, the user can execute this again with the same config file and  | ||||||
|  | FreqAI will find the trained models and load them instead of spending time training. This is useful  | ||||||
|  | if the user wants to tweak (or even hyperopt) buy and sell criteria inside the strategy. IF the user | ||||||
|  | *wants* to retrain a new model with the same config file, then he/she should simply change the `identifier`.  | ||||||
|  | This way, the user can return to using any model they wish by simply changing the `identifier`. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | ### Building a freqai strategy | ||||||
|  |  | ||||||
|  | The Freqai strategy requires the user to include the following lines of code in the strategy: | ||||||
|  |  | ||||||
|  | ```python | ||||||
|  |     from freqtrade.freqai.strategy_bridge import CustomModel | ||||||
|  |  | ||||||
|  |     def informative_pairs(self): | ||||||
|  |         whitelist_pairs = self.dp.current_whitelist() | ||||||
|  |         corr_pairs = self.config["freqai"]["corr_pairlist"] | ||||||
|  |         informative_pairs = [] | ||||||
|  |         for tf in self.config["freqai"]["timeframes"]: | ||||||
|  |             for pair in whitelist_pairs: | ||||||
|  |                 informative_pairs.append((pair, tf)) | ||||||
|  |             for pair in corr_pairs: | ||||||
|  |                 if pair in whitelist_pairs: | ||||||
|  |                     continue  # avoid duplication | ||||||
|  |                 informative_pairs.append((pair, tf)) | ||||||
|  |         return informative_pairs | ||||||
|  |  | ||||||
|  |     def bot_start(self): | ||||||
|  |         self.model = CustomModel(self.config) | ||||||
|  |  | ||||||
|  |     def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: | ||||||
|  |             self.freqai_info = self.config['freqai'] | ||||||
|  |  | ||||||
|  |             # the following loops are necessary for building the features  | ||||||
|  |             # indicated by the user in the configuration file. | ||||||
|  |             for tf in self.freqai_info['timeframes']: | ||||||
|  |                     for i in self.freqai_info['corr_pairlist']: | ||||||
|  |                     dataframe = self.populate_any_indicators(i, | ||||||
|  |                                     dataframe.copy(), tf, coin=i.split("/")[0]+'-') | ||||||
|  |  | ||||||
|  |             # the model will return 4 values, its prediction, an indication of whether or not the prediction  | ||||||
|  |             # should be accepted, the target mean/std values from the labels used during each training period. | ||||||
|  |             (dataframe['prediction'], dataframe['do_predict'],  | ||||||
|  |                     dataframe['target_mean'], dataframe['target_std']) = self.model.bridge.start(dataframe, metadata) | ||||||
|  |  | ||||||
|  |             return dataframe | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The user should also include `populate_any_indicators()` from `templates/FreqaiExampleStrategy.py` which builds  | ||||||
|  | the feature set with a proper naming convention for the IFreqaiModel to use later. | ||||||
|  |  | ||||||
|  | ### Building an IFreqaiModel | ||||||
|  |  | ||||||
|  | Freqai has an example prediction model based on the popular `Catboost` regression (`freqai/prediction_models/CatboostPredictionModel.py`). However, users can customize and create | ||||||
|  | their own prediction models using the `IFreqaiModel` class. Users are encouraged to inherit `train()`, `predict()`,  | ||||||
|  | and `make_labels()` to let them customize various aspects of their training procedures. | ||||||
|  |  | ||||||
|  | ### Running the model live | ||||||
|  |  | ||||||
|  | Freqai can be run dry/live using the following command | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | freqtrade trade --strategy FreqaiExampleStrategy --config config_freqai.example.json --freqaimodel ExamplePredictionModel | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | By default, Freqai will not find find any existing models and will start by training a new one  | ||||||
|  | given the user configuration settings. Following training, it will use that model to predict for the | ||||||
|  | duration of `backtest_period`. After a full `backtest_period` has elapsed, Freqai will auto retrain  | ||||||
|  | a new model, and begin making predictions with the updated model. FreqAI backtesting and live both | ||||||
|  | permit the user to use fractional days (i.e. 0.1) in the `backtest_period`, which enables more frequent  | ||||||
|  | retraining. But the user should be careful that using a fractional `backtest_period` with a large | ||||||
|  | `--timerange` in backtesting will result in a huge amount of required trainings/models. | ||||||
|  |  | ||||||
|  | If the user wishes to start dry/live from a backtested saved model, the user only needs to reuse | ||||||
|  | the same `identifier` parameter | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  |     "freqai": { | ||||||
|  |         "identifier": "example", | ||||||
|  |     } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | In this case, although Freqai will initiate with a  | ||||||
|  | pre-trained model, it will still check to see how much time has elapsed since the model was trained, | ||||||
|  | and if a full `backtest_period` has elapsed since the end of the loaded model, FreqAI will self retrain. | ||||||
|  |  | ||||||
|  | ## Data anylsis techniques | ||||||
|  |  | ||||||
|  | ### Controlling the model learning process | ||||||
|  |  | ||||||
|  | The user can define model settings for the data split `data_split_parameters` and learning parameters | ||||||
|  | `model_training_parameters`. Users are encouraged to visit the Catboost documentation | ||||||
|  | for more information on how to select these values. `n_estimators` increases the | ||||||
|  | computational effort and the fit to the training data. If a user has a GPU | ||||||
|  | installed in their system, they may benefit from changing `task_type` to `GPU`. | ||||||
|  | The `weight_factor` allows the user to weight more recent data more strongly | ||||||
|  | than past data via an exponential function: | ||||||
|  |  | ||||||
|  | $$ W_i = \exp(\frac{-i}{\alpha*n}) $$ | ||||||
|  |  | ||||||
|  | where $W_i$ is the weight of data point $i$ in a total set of $n$ data points._ | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Finally, `period` defines the offset used for the `labels`. In the present example, | ||||||
|  | the user is asking for `labels` that are 24 candles in the future. | ||||||
|  |  | ||||||
|  | ### Removing outliers with the Dissimilarity Index | ||||||
|  |  | ||||||
|  | The Dissimilarity Index (DI) aims to quantify the uncertainty associated with each | ||||||
|  | prediction by the model. To do so, Freqai measures the distance between each training | ||||||
|  | data point and all other training data points: | ||||||
|  |  | ||||||
|  | $$ d_{ab} = \sqrt{\sum_{j=1}^p(X_{a,j}-X_{b,j})^2} $$ | ||||||
|  |  | ||||||
|  | where $d_{ab}$ is the distance between the normalized points $a$ and $b$. $p$ | ||||||
|  | is the number of features i.e. the length of the vector $X$. The | ||||||
|  | characteristic distance, $\overline{d}$ for a set of training data points is simply the mean | ||||||
|  | of the average distances: | ||||||
|  |  | ||||||
|  | $$ \overline{d} = \sum_{a=1}^n(\sum_{b=1}^n(d_{ab}/n)/n) $$ | ||||||
|  |  | ||||||
|  | $\overline{d}$ quantifies the spread of the training data, which is compared to | ||||||
|  | the distance between the new prediction feature vectors, $X_k$ and all the training | ||||||
|  | data: | ||||||
|  |  | ||||||
|  | $$ d_k = \argmin_i d_{k,i} $$ | ||||||
|  |  | ||||||
|  | which enables the estimation of a Dissimilarity Index: | ||||||
|  |  | ||||||
|  | $$ DI_k = d_k/\overline{d} $$ | ||||||
|  |  | ||||||
|  | Equity and crypto markets suffer from a high level of non-patterned noise in the | ||||||
|  | form of outlier data points. The dissimilarity index allows predictions which | ||||||
|  | are outliers and not existent in the model feature space, to be thrown out due | ||||||
|  | to low levels of certainty. Activating the Dissimilarity Index can be achieved with: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  |     "freqai": { | ||||||
|  |         "feature_parameters" : { | ||||||
|  |                 "DI_threshold": 1 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | The user can tweak the DI with `DI_threshold` to increase or decrease the extrapolation of the  | ||||||
|  | trained model. | ||||||
|  |  | ||||||
|  | ### Reducing data dimensionality with Principal Component Analysis | ||||||
|  |  | ||||||
|  | Users can reduce the dimensionality of their features by activating the `principal_component_analysis`: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  |     "freqai": { | ||||||
|  |         "feature_parameters" : { | ||||||
|  |                 "principal_component_analysis": true | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Which will perform PCA on the features and reduce the dimensionality of the data so that the explained | ||||||
|  | variance of the data set is >= 0.999. | ||||||
|  |  | ||||||
|  | ### Removing outliers using a Support Vector Machine (SVM) | ||||||
|  |  | ||||||
|  | The user can tell Freqai to remove outlier data points from the training/test data sets by setting: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  |     "freqai": { | ||||||
|  |         "feature_parameters" : { | ||||||
|  |             "use_SVM_to_remove_outliers: true | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Freqai will train an SVM on the training data (or components if the user activated | ||||||
|  | `principal_component_analysis`) and remove any data point that it deems to be sit beyond the  | ||||||
|  | feature space. | ||||||
|  |  | ||||||
|  | ### Stratifying the data | ||||||
|  |  | ||||||
|  | The user can stratify the training/testing data using: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  |     "freqai": { | ||||||
|  |         "feature_parameters" : { | ||||||
|  |             "stratify": 3 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | which will split the data chronologically so that every Xth data points is a testing data point. In the | ||||||
|  | present example, the user is asking for every third data point in the dataframe to be used for  | ||||||
|  | testing, the other points are used for training.  | ||||||
|  |  | ||||||
|  | ### Setting up a follower | ||||||
|  |  | ||||||
|  | The user can define: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  |     "freqai": { | ||||||
|  |         "follow_mode": true, | ||||||
|  |         "identifier": "example" | ||||||
|  |     } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | to indicate to the bot that it should not train models, but instead should look for models trained  | ||||||
|  | by a leader with the same `identifier`. In this example, the user has a leader bot with the  | ||||||
|  | `identifier: "example"` already running or launching simultaneously as the present follower.  | ||||||
|  | The follower will load models created by the leader and inference them to obtain predictions. | ||||||
|  |  | ||||||
|  | ### Purging old model data | ||||||
|  |  | ||||||
|  | FreqAI stores new model files each time it retrains. These files become obsolete as new models  | ||||||
|  | are trained and FreqAI adapts to the new market conditions. Users planning to leave FreqAI running  | ||||||
|  | for extended periods of time with high frequency retraining should set `purge_old_models` in their  | ||||||
|  | config: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  |     "freqai": { | ||||||
|  |         "purge_old_models": true, | ||||||
|  |     } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | which will automatically purge all models older than the two most recently trained ones. | ||||||
|  |  | ||||||
|  | ## Defining model expirations | ||||||
|  |  | ||||||
|  | During dry/live, FreqAI trains each pair sequentially (on separate threads/GPU from the main | ||||||
|  | Freqtrade bot). This means there is always an age discrepancy between models. If a user is training | ||||||
|  | on 50 pairs, and each pair requires 5 minutes to train, the oldest model will be over 4 hours old.  | ||||||
|  | This may be undesirable if the characteristic time scale (read trade duration target) for a strategy  | ||||||
|  | is much less than 4 hours. The user can decide to only make trade entries if the model is less than  | ||||||
|  | a certain number of hours in age by setting the `expiration_hours` in the config file: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  |     "freqai": { | ||||||
|  |         "expiration_hours": 0.5, | ||||||
|  |     } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | In the present example, the user will only allow predictions on models that are less than 1/2 hours | ||||||
|  | old.  | ||||||
|  |  | ||||||
|  | <!-- ## Dynamic target expectation | ||||||
|  |  | ||||||
|  | The labels used for model training have a unique statistical distribution for each separate model training.  | ||||||
|  | We can use this information to know if our current prediction is in the realm of what the model was trained on,  | ||||||
|  | and if so, what is the statistical probability of the current prediction. With this information, we can | ||||||
|  | make more informed prediction._ | ||||||
|  | FreqAI builds this label distribution and provides a quantile to the strategy, which can be optionally used as a | ||||||
|  | dynamic threshold. The `target_quantile: X` means that X% of the labels are below this value. So setting: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  |     "freqai": { | ||||||
|  |         "feature_parameters" : { | ||||||
|  |             "target_quantile": 0.9 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Means the user will get back in the strategy the label threshold at which 90% of the labels were  | ||||||
|  | below this value. An example usage in the strategy may look something like: | ||||||
|  |  | ||||||
|  | ```python | ||||||
|  |  | ||||||
|  |     def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: | ||||||
|  |  | ||||||
|  |         # ... # | ||||||
|  |  | ||||||
|  |         ( | ||||||
|  |             dataframe["prediction"], | ||||||
|  |             dataframe["do_predict"], | ||||||
|  |             dataframe["target_upper_quantile"], | ||||||
|  |             dataframe["target_lower_quantile"], | ||||||
|  |         ) = self.model.bridge.start(dataframe, metadata, self) | ||||||
|  |  | ||||||
|  |         return dataframe | ||||||
|  |  | ||||||
|  |     def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: | ||||||
|  |  | ||||||
|  |         buy_conditions = [ | ||||||
|  |             (dataframe["prediction"] > dataframe["target_upper_quantile"]) & (dataframe["do_predict"] == 1) | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         if buy_conditions: | ||||||
|  |             dataframe.loc[reduce(lambda x, y: x | y, buy_conditions), "buy"] = 1 | ||||||
|  |  | ||||||
|  |         return dataframe | ||||||
|  |  | ||||||
|  | ``` --> | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Additional information | ||||||
|  |  | ||||||
|  | ### Feature normalization | ||||||
|  |  | ||||||
|  | The feature set created by the user is automatically normalized to the training | ||||||
|  | data only. This includes all test data and unseen prediction data (dry/live/backtest). | ||||||
|  |  | ||||||
|  | ### File structure | ||||||
|  |  | ||||||
|  | `user_data_dir/models/` contains all the data associated with the trainings and | ||||||
|  | backtests. This file structure is heavily controlled and read by the `FreqaiDataKitchen()` | ||||||
|  | and should thus not be modified. | ||||||
| @@ -12,7 +12,8 @@ from freqtrade.constants import DEFAULT_CONFIG | |||||||
|  |  | ||||||
| ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] | ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] | ||||||
|  |  | ||||||
| ARGS_STRATEGY = ["strategy", "strategy_path", "recursive_strategy_search"] | ARGS_STRATEGY = ["strategy", "strategy_path", "recursive_strategy_search", "freqaimodel", | ||||||
|  |                  "freqaimodel_path"] | ||||||
|  |  | ||||||
| ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] | ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -647,4 +647,14 @@ AVAILABLE_CLI_OPTIONS = { | |||||||
|         nargs='+', |         nargs='+', | ||||||
|         default=[], |         default=[], | ||||||
|     ), |     ), | ||||||
|  |     "freqaimodel": Arg( | ||||||
|  |         '--freqaimodel', | ||||||
|  |         help='Specify a custom freqaimodels.', | ||||||
|  |         metavar='NAME', | ||||||
|  |     ), | ||||||
|  |     "freqaimodel_path": Arg( | ||||||
|  |         '--freqaimodel-path', | ||||||
|  |         help='Specify additional lookup path for freqaimodels.', | ||||||
|  |         metavar='PATH', | ||||||
|  |     ), | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ from freqtrade.enums import CandleType, RunMode, TradingMode | |||||||
| from freqtrade.exceptions import OperationalException | from freqtrade.exceptions import OperationalException | ||||||
| from freqtrade.exchange import timeframe_to_minutes | from freqtrade.exchange import timeframe_to_minutes | ||||||
| from freqtrade.exchange.exchange import market_is_active | from freqtrade.exchange.exchange import market_is_active | ||||||
| from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist | from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist | ||||||
| from freqtrade.resolvers import ExchangeResolver | from freqtrade.resolvers import ExchangeResolver | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -50,7 +50,8 @@ def start_download_data(args: Dict[str, Any]) -> None: | |||||||
|     exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) |     exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) | ||||||
|     markets = [p for p, m in exchange.markets.items() if market_is_active(m) |     markets = [p for p, m in exchange.markets.items() if market_is_active(m) | ||||||
|                or config.get('include_inactive')] |                or config.get('include_inactive')] | ||||||
|     expanded_pairs = expand_pairlist(config['pairs'], markets) |  | ||||||
|  |     expanded_pairs = dynamic_expand_pairlist(config, markets) | ||||||
|  |  | ||||||
|     # Manual validations of relevant settings |     # Manual validations of relevant settings | ||||||
|     if not config['exchange'].get('skip_pair_validation', False): |     if not config['exchange'].get('skip_pair_validation', False): | ||||||
|   | |||||||
| @@ -85,6 +85,7 @@ def validate_config_consistency(conf: Dict[str, Any], preliminary: bool = False) | |||||||
|     _validate_unlimited_amount(conf) |     _validate_unlimited_amount(conf) | ||||||
|     _validate_ask_orderbook(conf) |     _validate_ask_orderbook(conf) | ||||||
|     validate_migrated_strategy_settings(conf) |     validate_migrated_strategy_settings(conf) | ||||||
|  |     _validate_freqai(conf) | ||||||
|  |  | ||||||
|     # validate configuration before returning |     # validate configuration before returning | ||||||
|     logger.info('Validating configuration ...') |     logger.info('Validating configuration ...') | ||||||
| @@ -163,6 +164,21 @@ def _validate_edge(conf: Dict[str, Any]) -> None: | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _validate_freqai(conf: Dict[str, Any]) -> None: | ||||||
|  |     """ | ||||||
|  |     Freqai param validator | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     if not conf.get('freqai', {}): | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     for param in constants.SCHEMA_FREQAI_REQUIRED: | ||||||
|  |         if param not in conf.get('freqai', {}): | ||||||
|  |             raise OperationalException( | ||||||
|  |                 f'{param} not found in Freqai config' | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def _validate_whitelist(conf: Dict[str, Any]) -> None: | def _validate_whitelist(conf: Dict[str, Any]) -> None: | ||||||
|     """ |     """ | ||||||
|     Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does. |     Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does. | ||||||
|   | |||||||
| @@ -97,6 +97,8 @@ class Configuration: | |||||||
|  |  | ||||||
|         self._process_analyze_options(config) |         self._process_analyze_options(config) | ||||||
|  |  | ||||||
|  |         self._process_freqai_options(config) | ||||||
|  |  | ||||||
|         # Check if the exchange set by the user is supported |         # Check if the exchange set by the user is supported | ||||||
|         check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) |         check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) | ||||||
|  |  | ||||||
| @@ -461,6 +463,16 @@ class Configuration: | |||||||
|  |  | ||||||
|         config.update({'runmode': self.runmode}) |         config.update({'runmode': self.runmode}) | ||||||
|  |  | ||||||
|  |     def _process_freqai_options(self, config: Dict[str, Any]) -> None: | ||||||
|  |  | ||||||
|  |         self._args_to_config(config, argname='freqaimodel', | ||||||
|  |                              logstring='Using freqaimodel class name: {}') | ||||||
|  |  | ||||||
|  |         self._args_to_config(config, argname='freqaimodel_path', | ||||||
|  |                              logstring='Using freqaimodel path: {}') | ||||||
|  |  | ||||||
|  |         return | ||||||
|  |  | ||||||
|     def _args_to_config(self, config: Dict[str, Any], argname: str, |     def _args_to_config(self, config: Dict[str, Any], argname: str, | ||||||
|                         logstring: str, logfun: Optional[Callable] = None, |                         logstring: str, logfun: Optional[Callable] = None, | ||||||
|                         deprecated_msg: Optional[str] = None) -> None: |                         deprecated_msg: Optional[str] = None) -> None: | ||||||
|   | |||||||
| @@ -55,6 +55,7 @@ FTHYPT_FILEVERSION = 'fthypt_fileversion' | |||||||
| USERPATH_HYPEROPTS = 'hyperopts' | USERPATH_HYPEROPTS = 'hyperopts' | ||||||
| USERPATH_STRATEGIES = 'strategies' | USERPATH_STRATEGIES = 'strategies' | ||||||
| USERPATH_NOTEBOOKS = 'notebooks' | USERPATH_NOTEBOOKS = 'notebooks' | ||||||
|  | USERPATH_FREQAIMODELS = 'freqaimodels' | ||||||
|  |  | ||||||
| TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] | TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] | ||||||
| WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw'] | WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw'] | ||||||
| @@ -472,7 +473,44 @@ CONF_SCHEMA = { | |||||||
|                 'remove_pumps': {'type': 'boolean'} |                 'remove_pumps': {'type': 'boolean'} | ||||||
|             }, |             }, | ||||||
|             'required': ['process_throttle_secs', 'allowed_risk'] |             'required': ['process_throttle_secs', 'allowed_risk'] | ||||||
|         } |         }, | ||||||
|  |         "freqai": { | ||||||
|  |             "type": "object", | ||||||
|  |             "properties": { | ||||||
|  |                 "timeframes": {"type": "list"}, | ||||||
|  |                 "train_period": {"type": "integer", "default": 0}, | ||||||
|  |                 "backtest_period": {"type": "float", "default": 7}, | ||||||
|  |                 "identifier": {"type": "str", "default": "example"}, | ||||||
|  |                 "corr_pairlist": {"type": "list"}, | ||||||
|  |                 "feature_parameters": { | ||||||
|  |                     "type": "object", | ||||||
|  |                     "properties": { | ||||||
|  |                         "period": {"type": "integer"}, | ||||||
|  |                         "shift": {"type": "integer", "default": 0}, | ||||||
|  |                         "DI_threshold": {"type": "float", "default": 0}, | ||||||
|  |                         "weight_factor": {"type": "number", "default": 0}, | ||||||
|  |                         "principal_component_analysis": {"type": "boolean", "default": False}, | ||||||
|  |                         "use_SVM_to_remove_outliers": {"type": "boolean", "default": False}, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 "data_split_parameters": { | ||||||
|  |                     "type": "object", | ||||||
|  |                     "properties": { | ||||||
|  |                         "test_size": {"type": "number"}, | ||||||
|  |                         "random_state": {"type": "integer"}, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 "model_training_parameters": { | ||||||
|  |                     "type": "object", | ||||||
|  |                     "properties": { | ||||||
|  |                         "n_estimators": {"type": "integer", "default": 2000}, | ||||||
|  |                         "random_state": {"type": "integer", "default": 1}, | ||||||
|  |                         "learning_rate": {"type": "number", "default": 0.02}, | ||||||
|  |                         "task_type": {"type": "string", "default": "CPU"}, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -516,6 +554,17 @@ SCHEMA_MINIMAL_REQUIRED = [ | |||||||
|     'dataformat_trades', |     'dataformat_trades', | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | SCHEMA_FREQAI_REQUIRED = [ | ||||||
|  |     'timeframes', | ||||||
|  |     'train_period', | ||||||
|  |     'backtest_period', | ||||||
|  |     'identifier', | ||||||
|  |     'corr_pairlist', | ||||||
|  |     'feature_parameters', | ||||||
|  |     'data_split_parameters', | ||||||
|  |     'model_training_parameters' | ||||||
|  | ] | ||||||
|  |  | ||||||
| CANCEL_REASON = { | CANCEL_REASON = { | ||||||
|     "TIMEOUT": "cancelled due to timeout", |     "TIMEOUT": "cancelled due to timeout", | ||||||
|     "PARTIALLY_FILLED_KEEP_OPEN": "partially filled - keeping order open", |     "PARTIALLY_FILLED_KEEP_OPEN": "partially filled - keeping order open", | ||||||
|   | |||||||
| @@ -86,7 +86,7 @@ class Exchange: | |||||||
|         # TradingMode.SPOT always supported and not required in this list |         # TradingMode.SPOT always supported and not required in this list | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: |     def __init__(self, config: Dict[str, Any], validate: bool = True, freqai: bool = False) -> None: | ||||||
|         """ |         """ | ||||||
|         Initializes this module with the given config, |         Initializes this module with the given config, | ||||||
|         it does basic validation whether the specified exchange and pairs are valid. |         it does basic validation whether the specified exchange and pairs are valid. | ||||||
| @@ -196,7 +196,7 @@ class Exchange: | |||||||
|         self.markets_refresh_interval: int = exchange_config.get( |         self.markets_refresh_interval: int = exchange_config.get( | ||||||
|             "markets_refresh_interval", 60) * 60 |             "markets_refresh_interval", 60) * 60 | ||||||
|  |  | ||||||
|         if self.trading_mode != TradingMode.SPOT: |         if self.trading_mode != TradingMode.SPOT and freqai is False: | ||||||
|             self.fill_leverage_tiers() |             self.fill_leverage_tiers() | ||||||
|         self.additional_exchange_init() |         self.additional_exchange_init() | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										314
									
								
								freqtrade/freqai/data_drawer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								freqtrade/freqai/data_drawer.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,314 @@ | |||||||
|  | import collections | ||||||
|  | import json | ||||||
|  | import logging | ||||||
|  | import re | ||||||
|  | import shutil | ||||||
|  | import threading | ||||||
|  | from pathlib import Path | ||||||
|  | from typing import Any, Dict, Tuple | ||||||
|  |  | ||||||
|  | # import pickle as pk | ||||||
|  | import numpy as np | ||||||
|  | import pandas as pd | ||||||
|  | from pandas import DataFrame | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # from freqtrade.freqai.data_kitchen import FreqaiDataKitchen | ||||||
|  |  | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FreqaiDataDrawer: | ||||||
|  |     """ | ||||||
|  |     Class aimed at holding all pair models/info in memory for better inferencing/retrainig/saving | ||||||
|  |     /loading to/from disk. | ||||||
|  |     This object remains persistent throughout live/dry, unlike FreqaiDataKitchen, which is | ||||||
|  |     reinstantiated for each coin. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, full_path: Path, config: dict, follow_mode: bool = False): | ||||||
|  |  | ||||||
|  |         self.config = config | ||||||
|  |         self.freqai_info = config.get("freqai", {}) | ||||||
|  |         # dictionary holding all pair metadata necessary to load in from disk | ||||||
|  |         self.pair_dict: Dict[str, Any] = {} | ||||||
|  |         # dictionary holding all actively inferenced models in memory given a model filename | ||||||
|  |         self.model_dictionary: Dict[str, Any] = {} | ||||||
|  |         self.model_return_values: Dict[str, Any] = {} | ||||||
|  |         self.pair_data_dict: Dict[str, Any] = {} | ||||||
|  |         self.historic_data: Dict[str, Any] = {} | ||||||
|  |         self.follower_dict: Dict[str, Any] = {} | ||||||
|  |         self.full_path = full_path | ||||||
|  |         self.follow_mode = follow_mode | ||||||
|  |         if follow_mode: | ||||||
|  |             self.create_follower_dict() | ||||||
|  |         self.load_drawer_from_disk() | ||||||
|  |         self.training_queue: Dict[str, int] = {} | ||||||
|  |         self.history_lock = threading.Lock() | ||||||
|  |  | ||||||
|  |     def load_drawer_from_disk(self): | ||||||
|  |         """ | ||||||
|  |         Locate and load a previously saved data drawer full of all pair model metadata in | ||||||
|  |         present model folder. | ||||||
|  |         :returns: | ||||||
|  |         exists: bool = whether or not the drawer was located | ||||||
|  |         """ | ||||||
|  |         exists = Path(self.full_path / str("pair_dictionary.json")).resolve().exists() | ||||||
|  |         if exists: | ||||||
|  |             with open(self.full_path / str("pair_dictionary.json"), "r") as fp: | ||||||
|  |                 self.pair_dict = json.load(fp) | ||||||
|  |         elif not self.follow_mode: | ||||||
|  |             logger.info("Could not find existing datadrawer, starting from scratch") | ||||||
|  |         else: | ||||||
|  |             logger.warning( | ||||||
|  |                 f"Follower could not find pair_dictionary at {self.full_path} " | ||||||
|  |                 "sending null values back to strategy" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         return exists | ||||||
|  |  | ||||||
|  |     def save_drawer_to_disk(self): | ||||||
|  |         """ | ||||||
|  |         Save data drawer full of all pair model metadata in present model folder. | ||||||
|  |         """ | ||||||
|  |         with open(self.full_path / str("pair_dictionary.json"), "w") as fp: | ||||||
|  |             json.dump(self.pair_dict, fp, default=self.np_encoder) | ||||||
|  |  | ||||||
|  |     def save_follower_dict_to_disk(self): | ||||||
|  |         """ | ||||||
|  |         Save follower dictionary to disk (used by strategy for persistent prediction targets) | ||||||
|  |         """ | ||||||
|  |         follower_name = self.config.get("bot_name", "follower1") | ||||||
|  |         with open( | ||||||
|  |             self.full_path / str("follower_dictionary-" + follower_name + ".json"), "w" | ||||||
|  |         ) as fp: | ||||||
|  |             json.dump(self.follower_dict, fp, default=self.np_encoder) | ||||||
|  |  | ||||||
|  |     def create_follower_dict(self): | ||||||
|  |         """ | ||||||
|  |         Create or dictionary for each follower to maintain unique persistent prediction targets | ||||||
|  |         """ | ||||||
|  |         follower_name = self.config.get("bot_name", "follower1") | ||||||
|  |         whitelist_pairs = self.config.get("exchange", {}).get("pair_whitelist") | ||||||
|  |  | ||||||
|  |         exists = ( | ||||||
|  |             Path(self.full_path / str("follower_dictionary-" + follower_name + ".json")) | ||||||
|  |             .resolve() | ||||||
|  |             .exists() | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         if exists: | ||||||
|  |             logger.info("Found an existing follower dictionary") | ||||||
|  |  | ||||||
|  |         for pair in whitelist_pairs: | ||||||
|  |             self.follower_dict[pair] = {} | ||||||
|  |  | ||||||
|  |         with open( | ||||||
|  |             self.full_path / str("follower_dictionary-" + follower_name + ".json"), "w" | ||||||
|  |         ) as fp: | ||||||
|  |             json.dump(self.follower_dict, fp, default=self.np_encoder) | ||||||
|  |  | ||||||
|  |     def np_encoder(self, object): | ||||||
|  |         if isinstance(object, np.generic): | ||||||
|  |             return object.item() | ||||||
|  |  | ||||||
|  |     def get_pair_dict_info(self, pair: str) -> Tuple[str, int, bool, bool]: | ||||||
|  |         """ | ||||||
|  |         Locate and load existing model metadata from persistent storage. If not located, | ||||||
|  |         create a new one and append the current pair to it and prepare it for its first | ||||||
|  |         training | ||||||
|  |         :params: | ||||||
|  |         metadata: dict = strategy furnished pair metadata | ||||||
|  |         :returns: | ||||||
|  |         model_filename: str = unique filename used for loading persistent objects from disk | ||||||
|  |         trained_timestamp: int = the last time the coin was trained | ||||||
|  |         coin_first: bool = If the coin is fresh without metadata | ||||||
|  |         return_null_array: bool = Follower could not find pair metadata | ||||||
|  |         """ | ||||||
|  |         pair_in_dict = self.pair_dict.get(pair) | ||||||
|  |         data_path_set = self.pair_dict.get(pair, {}).get("data_path", None) | ||||||
|  |         return_null_array = False | ||||||
|  |  | ||||||
|  |         if pair_in_dict: | ||||||
|  |             model_filename = self.pair_dict[pair]["model_filename"] | ||||||
|  |             trained_timestamp = self.pair_dict[pair]["trained_timestamp"] | ||||||
|  |             coin_first = self.pair_dict[pair]["first"] | ||||||
|  |         elif not self.follow_mode: | ||||||
|  |             self.pair_dict[pair] = {} | ||||||
|  |             model_filename = self.pair_dict[pair]["model_filename"] = "" | ||||||
|  |             coin_first = self.pair_dict[pair]["first"] = True | ||||||
|  |             trained_timestamp = self.pair_dict[pair]["trained_timestamp"] = 0 | ||||||
|  |             self.pair_dict[pair]["priority"] = len(self.pair_dict) | ||||||
|  |  | ||||||
|  |         if not data_path_set and self.follow_mode: | ||||||
|  |             logger.warning( | ||||||
|  |                 f"Follower could not find current pair {pair} in " | ||||||
|  |                 f"pair_dictionary at path {self.full_path}, sending null values " | ||||||
|  |                 "back to strategy." | ||||||
|  |             ) | ||||||
|  |             return_null_array = True | ||||||
|  |  | ||||||
|  |         return model_filename, trained_timestamp, coin_first, return_null_array | ||||||
|  |  | ||||||
|  |     def set_pair_dict_info(self, metadata: dict) -> None: | ||||||
|  |         pair_in_dict = self.pair_dict.get(metadata["pair"]) | ||||||
|  |         if pair_in_dict: | ||||||
|  |             return | ||||||
|  |         else: | ||||||
|  |             self.pair_dict[metadata["pair"]] = {} | ||||||
|  |             self.pair_dict[metadata["pair"]]["model_filename"] = "" | ||||||
|  |             self.pair_dict[metadata["pair"]]["first"] = True | ||||||
|  |             self.pair_dict[metadata["pair"]]["trained_timestamp"] = 0 | ||||||
|  |             self.pair_dict[metadata["pair"]]["priority"] = len(self.pair_dict) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |     def pair_to_end_of_training_queue(self, pair: str) -> None: | ||||||
|  |         # march all pairs up in the queue | ||||||
|  |         for p in self.pair_dict: | ||||||
|  |             self.pair_dict[p]["priority"] -= 1 | ||||||
|  |         # send pair to end of queue | ||||||
|  |         self.pair_dict[pair]["priority"] = len(self.pair_dict) | ||||||
|  |  | ||||||
|  |     def set_initial_return_values(self, pair: str, dk, pred_df, do_preds) -> None: | ||||||
|  |         """ | ||||||
|  |         Set the initial return values to a persistent dataframe. This avoids needing to repredict on | ||||||
|  |         historical candles, and also stores historical predictions despite retrainings (so stored | ||||||
|  |         predictions are true predictions, not just inferencing on trained data) | ||||||
|  |         """ | ||||||
|  |         self.model_return_values[pair] = pd.DataFrame() | ||||||
|  |         for label in dk.label_list: | ||||||
|  |             self.model_return_values[pair][label] = pred_df[label] | ||||||
|  |             self.model_return_values[pair][f"{label}_mean"] = dk.data["labels_mean"][label] | ||||||
|  |             self.model_return_values[pair][f"{label}_std"] = dk.data["labels_std"][label] | ||||||
|  |  | ||||||
|  |         if self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0) > 0: | ||||||
|  |             self.model_return_values[pair]["DI_values"] = dk.DI_values | ||||||
|  |  | ||||||
|  |         self.model_return_values[pair]["do_predict"] = do_preds | ||||||
|  |  | ||||||
|  |     def append_model_predictions(self, pair: str, predictions, do_preds, dk, len_df) -> None: | ||||||
|  |  | ||||||
|  |         # strat seems to feed us variable sized dataframes - and since we are trying to build our | ||||||
|  |         # own return array in the same shape, we need to figure out how the size has changed | ||||||
|  |         # and adapt our stored/returned info accordingly. | ||||||
|  |         length_difference = len(self.model_return_values[pair]) - len_df | ||||||
|  |         i = 0 | ||||||
|  |  | ||||||
|  |         if length_difference == 0: | ||||||
|  |             i = 1 | ||||||
|  |         elif length_difference > 0: | ||||||
|  |             i = length_difference + 1 | ||||||
|  |  | ||||||
|  |         df = self.model_return_values[pair] = self.model_return_values[pair].shift(-i) | ||||||
|  |  | ||||||
|  |         for label in dk.label_list: | ||||||
|  |             df[label].iloc[-1] = predictions[label].iloc[-1] | ||||||
|  |             df[f"{label}_mean"].iloc[-1] = dk.data["labels_mean"][label] | ||||||
|  |             df[f"{label}_std"].iloc[-1] = dk.data["labels_std"][label] | ||||||
|  |         # df['prediction'].iloc[-1] = predictions[-1] | ||||||
|  |         df["do_predict"].iloc[-1] = do_preds[-1] | ||||||
|  |  | ||||||
|  |         if self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0) > 0: | ||||||
|  |             df["DI_values"].iloc[-1] = dk.DI_values[-1] | ||||||
|  |  | ||||||
|  |         if length_difference < 0: | ||||||
|  |             prepend_df = pd.DataFrame( | ||||||
|  |                 np.zeros((abs(length_difference) - 1, len(df.columns))), columns=df.columns | ||||||
|  |             ) | ||||||
|  |             df = pd.concat([prepend_df, df], axis=0) | ||||||
|  |  | ||||||
|  |     def attach_return_values_to_return_dataframe(self, pair: str, dataframe) -> DataFrame: | ||||||
|  |         """ | ||||||
|  |         Attach the return values to the strat dataframe | ||||||
|  |         :params: | ||||||
|  |         dataframe: DataFrame = strat dataframe | ||||||
|  |         :returns: | ||||||
|  |         dataframe: DataFrame = strat dataframe with return values attached | ||||||
|  |         """ | ||||||
|  |         df = self.model_return_values[pair] | ||||||
|  |         to_keep = [col for col in dataframe.columns if not col.startswith("&")] | ||||||
|  |         dataframe = pd.concat([dataframe[to_keep], df], axis=1) | ||||||
|  |         return dataframe | ||||||
|  |  | ||||||
|  |     def return_null_values_to_strategy(self, dataframe: DataFrame, dk) -> None: | ||||||
|  |         """ | ||||||
|  |         Build 0 filled dataframe to return to strategy | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         dk.find_features(dataframe) | ||||||
|  |  | ||||||
|  |         for label in dk.label_list: | ||||||
|  |             dataframe[label] = 0 | ||||||
|  |             dataframe[f"{label}_mean"] = 0 | ||||||
|  |             dataframe[f"{label}_std"] = 0 | ||||||
|  |  | ||||||
|  |         # dataframe['prediction'] = 0 | ||||||
|  |         dataframe["do_predict"] = 0 | ||||||
|  |  | ||||||
|  |         if self.freqai_info.get("feature_parameters", {}).get("DI_threshold", 0) > 0: | ||||||
|  |             dataframe["DI_value"] = 0 | ||||||
|  |  | ||||||
|  |         dk.return_dataframe = dataframe | ||||||
|  |  | ||||||
|  |     def purge_old_models(self) -> None: | ||||||
|  |  | ||||||
|  |         model_folders = [x for x in self.full_path.iterdir() if x.is_dir()] | ||||||
|  |  | ||||||
|  |         pattern = re.compile(r"sub-train-(\w+)(\d{10})") | ||||||
|  |  | ||||||
|  |         delete_dict: Dict[str, Any] = {} | ||||||
|  |  | ||||||
|  |         for dir in model_folders: | ||||||
|  |             result = pattern.match(str(dir.name)) | ||||||
|  |             if result is None: | ||||||
|  |                 break | ||||||
|  |             coin = result.group(1) | ||||||
|  |             timestamp = result.group(2) | ||||||
|  |  | ||||||
|  |             if coin not in delete_dict: | ||||||
|  |                 delete_dict[coin] = {} | ||||||
|  |                 delete_dict[coin]["num_folders"] = 1 | ||||||
|  |                 delete_dict[coin]["timestamps"] = {int(timestamp): dir} | ||||||
|  |             else: | ||||||
|  |                 delete_dict[coin]["num_folders"] += 1 | ||||||
|  |                 delete_dict[coin]["timestamps"][int(timestamp)] = dir | ||||||
|  |  | ||||||
|  |         for coin in delete_dict: | ||||||
|  |             if delete_dict[coin]["num_folders"] > 2: | ||||||
|  |                 sorted_dict = collections.OrderedDict( | ||||||
|  |                     sorted(delete_dict[coin]["timestamps"].items()) | ||||||
|  |                 ) | ||||||
|  |                 num_delete = len(sorted_dict) - 2 | ||||||
|  |                 deleted = 0 | ||||||
|  |                 for k, v in sorted_dict.items(): | ||||||
|  |                     if deleted >= num_delete: | ||||||
|  |                         break | ||||||
|  |                     logger.info(f"Freqai purging old model file {v}") | ||||||
|  |                     shutil.rmtree(v) | ||||||
|  |                     deleted += 1 | ||||||
|  |  | ||||||
|  |     def update_follower_metadata(self): | ||||||
|  |         # follower needs to load from disk to get any changes made by leader to pair_dict | ||||||
|  |         self.load_drawer_from_disk() | ||||||
|  |         if self.config.get("freqai", {}).get("purge_old_models", False): | ||||||
|  |             self.purge_old_models() | ||||||
|  |  | ||||||
|  |     # to be used if we want to send predictions directly to the follower instead of forcing | ||||||
|  |     # follower to load models and inference | ||||||
|  |     # def save_model_return_values_to_disk(self) -> None: | ||||||
|  |     #     with open(self.full_path / str('model_return_values.json'), "w") as fp: | ||||||
|  |     #         json.dump(self.model_return_values, fp, default=self.np_encoder) | ||||||
|  |  | ||||||
|  |     # def load_model_return_values_from_disk(self, dk: FreqaiDataKitchen) -> FreqaiDataKitchen: | ||||||
|  |     #     exists = Path(self.full_path / str('model_return_values.json')).resolve().exists() | ||||||
|  |     #     if exists: | ||||||
|  |     #         with open(self.full_path / str('model_return_values.json'), "r") as fp: | ||||||
|  |     #             self.model_return_values = json.load(fp) | ||||||
|  |     #     elif not self.follow_mode: | ||||||
|  |     #         logger.info("Could not find existing datadrawer, starting from scratch") | ||||||
|  |     #     else: | ||||||
|  |     #         logger.warning(f'Follower could not find pair_dictionary at {self.full_path} ' | ||||||
|  |     #                        'sending null values back to strategy') | ||||||
|  |  | ||||||
|  |     #     return exists, dk | ||||||
							
								
								
									
										1305
									
								
								freqtrade/freqai/data_kitchen.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1305
									
								
								freqtrade/freqai/data_kitchen.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										559
									
								
								freqtrade/freqai/freqai_interface.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										559
									
								
								freqtrade/freqai/freqai_interface.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,559 @@ | |||||||
|  | # import contextlib | ||||||
|  | import datetime | ||||||
|  | import gc | ||||||
|  | import logging | ||||||
|  | import shutil | ||||||
|  | import threading | ||||||
|  | import time | ||||||
|  | from abc import ABC, abstractmethod | ||||||
|  | from pathlib import Path | ||||||
|  | from typing import Any, Dict, Tuple | ||||||
|  |  | ||||||
|  | import numpy as np | ||||||
|  | import numpy.typing as npt | ||||||
|  | import pandas as pd | ||||||
|  | from pandas import DataFrame | ||||||
|  |  | ||||||
|  | from freqtrade.configuration import TimeRange | ||||||
|  | from freqtrade.enums import RunMode | ||||||
|  | from freqtrade.exceptions import OperationalException | ||||||
|  | from freqtrade.freqai.data_drawer import FreqaiDataDrawer | ||||||
|  | from freqtrade.freqai.data_kitchen import FreqaiDataKitchen | ||||||
|  | from freqtrade.strategy.interface import IStrategy | ||||||
|  |  | ||||||
|  |  | ||||||
|  | pd.options.mode.chained_assignment = None | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def threaded(fn): | ||||||
|  |     def wrapper(*args, **kwargs): | ||||||
|  |         threading.Thread(target=fn, args=args, kwargs=kwargs).start() | ||||||
|  |  | ||||||
|  |     return wrapper | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class IFreqaiModel(ABC): | ||||||
|  |     """ | ||||||
|  |     Class containing all tools for training and prediction in the strategy. | ||||||
|  |     User models should inherit from this class as shown in | ||||||
|  |     templates/ExamplePredictionModel.py where the user overrides | ||||||
|  |     train(), predict(), fit(), and make_labels(). | ||||||
|  |     Author: Robert Caulk, rob.caulk@gmail.com | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, config: Dict[str, Any]) -> None: | ||||||
|  |  | ||||||
|  |         self.config = config | ||||||
|  |         self.assert_config(self.config) | ||||||
|  |         self.freqai_info = config["freqai"] | ||||||
|  |         self.data_split_parameters = config.get("freqai", {}).get("data_split_parameters") | ||||||
|  |         self.model_training_parameters = config.get("freqai", {}).get("model_training_parameters") | ||||||
|  |         self.feature_parameters = config.get("freqai", {}).get("feature_parameters") | ||||||
|  |         self.time_last_trained = None | ||||||
|  |         self.current_time = None | ||||||
|  |         self.model = None | ||||||
|  |         self.predictions = None | ||||||
|  |         self.training_on_separate_thread = False | ||||||
|  |         self.retrain = False | ||||||
|  |         self.first = True | ||||||
|  |         self.update_historic_data = 0 | ||||||
|  |         self.set_full_path() | ||||||
|  |         self.follow_mode = self.freqai_info.get("follow_mode", False) | ||||||
|  |         self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode) | ||||||
|  |         self.lock = threading.Lock() | ||||||
|  |         self.follow_mode = self.freqai_info.get("follow_mode", False) | ||||||
|  |         self.identifier = self.freqai_info.get("identifier", "no_id_provided") | ||||||
|  |         self.scanning = False | ||||||
|  |         self.ready_to_scan = False | ||||||
|  |         self.first = True | ||||||
|  |         self.keras = self.freqai_info.get("keras", False) | ||||||
|  |         self.CONV_WIDTH = self.freqai_info.get("conv_width", 2) | ||||||
|  |  | ||||||
|  |     def assert_config(self, config: Dict[str, Any]) -> None: | ||||||
|  |  | ||||||
|  |         if not config.get("freqai", {}): | ||||||
|  |             raise OperationalException("No freqai parameters found in configuration file.") | ||||||
|  |  | ||||||
|  |     def start(self, dataframe: DataFrame, metadata: dict, strategy: IStrategy) -> DataFrame: | ||||||
|  |         """ | ||||||
|  |         Entry point to the FreqaiModel from a specific pair, it will train a new model if | ||||||
|  |         necessary before making the prediction. | ||||||
|  |  | ||||||
|  |         :params: | ||||||
|  |         :dataframe: Full dataframe coming from strategy - it contains entire | ||||||
|  |         backtesting timerange + additional historical data necessary to train | ||||||
|  |         the model. | ||||||
|  |         :metadata: pair metadata coming from strategy. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         self.live = strategy.dp.runmode in (RunMode.DRY_RUN, RunMode.LIVE) | ||||||
|  |         self.dd.set_pair_dict_info(metadata) | ||||||
|  |  | ||||||
|  |         if self.live: | ||||||
|  |             self.dk = FreqaiDataKitchen(self.config, self.dd, self.live, metadata["pair"]) | ||||||
|  |             dk = self.start_live(dataframe, metadata, strategy, self.dk) | ||||||
|  |  | ||||||
|  |         # For backtesting, each pair enters and then gets trained for each window along the | ||||||
|  |         # sliding window defined by "train_period" (training window) and "backtest_period" | ||||||
|  |         # (backtest window, i.e. window immediately following the training window). | ||||||
|  |         # FreqAI slides the window and sequentially builds the backtesting results before returning | ||||||
|  |         # the concatenated results for the full backtesting period back to the strategy. | ||||||
|  |         elif not self.follow_mode: | ||||||
|  |             self.dk = FreqaiDataKitchen(self.config, self.dd, self.live, metadata["pair"]) | ||||||
|  |             logger.info(f"Training {len(self.dk.training_timeranges)} timeranges") | ||||||
|  |             dk = self.start_backtesting(dataframe, metadata, self.dk) | ||||||
|  |  | ||||||
|  |         dataframe = self.remove_features_from_df(dk.return_dataframe) | ||||||
|  |         return self.return_values(dataframe, dk) | ||||||
|  |  | ||||||
|  |     @threaded | ||||||
|  |     def start_scanning(self, strategy: IStrategy) -> None: | ||||||
|  |         """ | ||||||
|  |         Function designed to constantly scan pairs for retraining on a separate thread (intracandle) | ||||||
|  |         to improve model youth. This function is agnostic to data preparation/collection/storage, | ||||||
|  |         it simply trains on what ever data is available in the self.dd. | ||||||
|  |         :params: | ||||||
|  |         strategy: IStrategy = The user defined strategy class | ||||||
|  |         """ | ||||||
|  |         while 1: | ||||||
|  |             time.sleep(1) | ||||||
|  |             for pair in self.config.get("exchange", {}).get("pair_whitelist"): | ||||||
|  |  | ||||||
|  |                 (_, trained_timestamp, _, _) = self.dd.get_pair_dict_info(pair) | ||||||
|  |  | ||||||
|  |                 if self.dd.pair_dict[pair]["priority"] != 1: | ||||||
|  |                     continue | ||||||
|  |                 dk = FreqaiDataKitchen(self.config, self.dd, self.live, pair) | ||||||
|  |  | ||||||
|  |                 # file_exists = False | ||||||
|  |  | ||||||
|  |                 dk.set_paths(pair, trained_timestamp) | ||||||
|  |                 # file_exists = self.model_exists(pair, | ||||||
|  |                 #                                 dk, | ||||||
|  |                 #                                 trained_timestamp=trained_timestamp, | ||||||
|  |                 #                                 model_filename=model_filename, | ||||||
|  |                 #                                 scanning=True) | ||||||
|  |  | ||||||
|  |                 ( | ||||||
|  |                     retrain, | ||||||
|  |                     new_trained_timerange, | ||||||
|  |                     data_load_timerange, | ||||||
|  |                 ) = dk.check_if_new_training_required(trained_timestamp) | ||||||
|  |                 dk.set_paths(pair, new_trained_timerange.stopts) | ||||||
|  |  | ||||||
|  |                 if retrain:  # or not file_exists: | ||||||
|  |                     self.train_model_in_series( | ||||||
|  |                         new_trained_timerange, pair, strategy, dk, data_load_timerange | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |     def start_backtesting( | ||||||
|  |         self, dataframe: DataFrame, metadata: dict, dk: FreqaiDataKitchen | ||||||
|  |     ) -> FreqaiDataKitchen: | ||||||
|  |         """ | ||||||
|  |         The main broad execution for backtesting. For backtesting, each pair enters and then gets | ||||||
|  |         trained for each window along the sliding window defined by "train_period" (training window) | ||||||
|  |         and "backtest_period" (backtest window, i.e. window immediately following the | ||||||
|  |         training window). FreqAI slides the window and sequentially builds the backtesting results | ||||||
|  |         before returning the concatenated results for the full backtesting period back to the | ||||||
|  |         strategy. | ||||||
|  |         :params: | ||||||
|  |         dataframe: DataFrame = strategy passed dataframe | ||||||
|  |         metadata: Dict = pair metadata | ||||||
|  |         dk: FreqaiDataKitchen = Data management/analysis tool assoicated to present pair only | ||||||
|  |         :returns: | ||||||
|  |         dk: FreqaiDataKitchen = Data management/analysis tool assoicated to present pair only | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         # Loop enforcing the sliding window training/backtesting paradigm | ||||||
|  |         # tr_train is the training time range e.g. 1 historical month | ||||||
|  |         # tr_backtest is the backtesting time range e.g. the week directly | ||||||
|  |         # following tr_train. Both of these windows slide through the | ||||||
|  |         # entire backtest | ||||||
|  |         for tr_train, tr_backtest in zip(dk.training_timeranges, dk.backtesting_timeranges): | ||||||
|  |             (_, _, _, _) = self.dd.get_pair_dict_info(metadata["pair"]) | ||||||
|  |             gc.collect() | ||||||
|  |             dk.data = {}  # clean the pair specific data between training window sliding | ||||||
|  |             self.training_timerange = tr_train | ||||||
|  |             # self.training_timerange_timerange = tr_train | ||||||
|  |             dataframe_train = dk.slice_dataframe(tr_train, dataframe) | ||||||
|  |             dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe) | ||||||
|  |  | ||||||
|  |             trained_timestamp = tr_train  # TimeRange.parse_timerange(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" | ||||||
|  |             ) | ||||||
|  |             logger.info("Training %s", metadata["pair"]) | ||||||
|  |             logger.info(f"Training {tr_train_startts_str} to {tr_train_stopts_str}") | ||||||
|  |  | ||||||
|  |             dk.data_path = Path( | ||||||
|  |                 dk.full_path | ||||||
|  |                 / str( | ||||||
|  |                     "sub-train" | ||||||
|  |                     + "-" | ||||||
|  |                     + metadata["pair"].split("/")[0] | ||||||
|  |                     + str(int(trained_timestamp.stopts)) | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             if not self.model_exists( | ||||||
|  |                 metadata["pair"], dk, trained_timestamp=trained_timestamp.stopts | ||||||
|  |             ): | ||||||
|  |                 self.model = self.train(dataframe_train, metadata["pair"], dk) | ||||||
|  |                 self.dd.pair_dict[metadata["pair"]]["trained_timestamp"] = trained_timestamp.stopts | ||||||
|  |                 dk.set_new_model_names(metadata["pair"], trained_timestamp) | ||||||
|  |                 dk.save_data(self.model, metadata["pair"], keras_model=self.keras) | ||||||
|  |             else: | ||||||
|  |                 self.model = dk.load_data(metadata["pair"], keras_model=self.keras) | ||||||
|  |  | ||||||
|  |             self.check_if_feature_list_matches_strategy(dataframe_train, dk) | ||||||
|  |  | ||||||
|  |             preds, do_preds = self.predict(dataframe_backtest, dk) | ||||||
|  |  | ||||||
|  |             dk.append_predictions(preds, do_preds, len(dataframe_backtest)) | ||||||
|  |             print("predictions", len(dk.full_predictions), "do_predict", len(dk.full_do_predict)) | ||||||
|  |  | ||||||
|  |         dk.fill_predictions(len(dataframe)) | ||||||
|  |  | ||||||
|  |         return dk | ||||||
|  |  | ||||||
|  |     def start_live( | ||||||
|  |         self, dataframe: DataFrame, metadata: dict, strategy: IStrategy, dk: FreqaiDataKitchen | ||||||
|  |     ) -> FreqaiDataKitchen: | ||||||
|  |         """ | ||||||
|  |         The main broad execution for dry/live. This function will check if a retraining should be | ||||||
|  |         performed, and if so, retrain and reset the model. | ||||||
|  |         :params: | ||||||
|  |         dataframe: DataFrame = strategy passed dataframe | ||||||
|  |         metadata: Dict = pair metadata | ||||||
|  |         strategy: IStrategy = currently employed strategy | ||||||
|  |         dk: FreqaiDataKitchen = Data management/analysis tool assoicated to present pair only | ||||||
|  |         :returns: | ||||||
|  |         dk: FreqaiDataKitchen = Data management/analysis tool assoicated to present pair only | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         # update follower | ||||||
|  |         if self.follow_mode: | ||||||
|  |             self.dd.update_follower_metadata() | ||||||
|  |  | ||||||
|  |         # get the model metadata associated with the current pair | ||||||
|  |         (_, trained_timestamp, _, return_null_array) = self.dd.get_pair_dict_info(metadata["pair"]) | ||||||
|  |  | ||||||
|  |         # if the metadata doesnt exist, the follower returns null arrays to strategy | ||||||
|  |         if self.follow_mode and return_null_array: | ||||||
|  |             logger.info("Returning null array from follower to strategy") | ||||||
|  |             self.dd.return_null_values_to_strategy(dataframe, dk) | ||||||
|  |             return dk | ||||||
|  |  | ||||||
|  |         # append the historic data once per round | ||||||
|  |         if self.dd.historic_data: | ||||||
|  |             dk.update_historic_data(strategy) | ||||||
|  |             logger.debug(f'Updating historic data on pair {metadata["pair"]}') | ||||||
|  |  | ||||||
|  |         # if trainable, check if model needs training, if so compute new timerange, | ||||||
|  |         # then save model and metadata. | ||||||
|  |         # if not trainable, load existing data | ||||||
|  |         if not self.follow_mode: | ||||||
|  |  | ||||||
|  |             (_, new_trained_timerange, data_load_timerange) = dk.check_if_new_training_required( | ||||||
|  |                 trained_timestamp | ||||||
|  |             ) | ||||||
|  |             dk.set_paths(metadata["pair"], new_trained_timerange.stopts) | ||||||
|  |  | ||||||
|  |             # download candle history if it is not already in memory | ||||||
|  |             if not self.dd.historic_data: | ||||||
|  |                 logger.info( | ||||||
|  |                     "Downloading all training data for all pairs in whitelist and " | ||||||
|  |                     "corr_pairlist, this may take a while if you do not have the " | ||||||
|  |                     "data saved" | ||||||
|  |                 ) | ||||||
|  |                 dk.download_all_data_for_training(data_load_timerange) | ||||||
|  |                 dk.load_all_pair_histories(data_load_timerange) | ||||||
|  |  | ||||||
|  |             if not self.scanning: | ||||||
|  |                 self.scanning = True | ||||||
|  |                 self.start_scanning(strategy) | ||||||
|  |  | ||||||
|  |         elif self.follow_mode: | ||||||
|  |             dk.set_paths(metadata["pair"], trained_timestamp) | ||||||
|  |             logger.info( | ||||||
|  |                 "FreqAI instance set to follow_mode, finding existing pair" | ||||||
|  |                 f"using { self.identifier }" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # load the model and associated data into the data kitchen | ||||||
|  |         self.model = dk.load_data(coin=metadata["pair"], keras_model=self.keras) | ||||||
|  |  | ||||||
|  |         if not self.model: | ||||||
|  |             logger.warning("No model ready, returning null values to strategy.") | ||||||
|  |             self.dd.return_null_values_to_strategy(dataframe, dk) | ||||||
|  |             return dk | ||||||
|  |  | ||||||
|  |         # ensure user is feeding the correct indicators to the model | ||||||
|  |         self.check_if_feature_list_matches_strategy(dataframe, dk) | ||||||
|  |  | ||||||
|  |         self.build_strategy_return_arrays(dataframe, dk, metadata["pair"], trained_timestamp) | ||||||
|  |  | ||||||
|  |         return dk | ||||||
|  |  | ||||||
|  |     def build_strategy_return_arrays( | ||||||
|  |         self, dataframe: DataFrame, dk: FreqaiDataKitchen, pair: str, trained_timestamp: int | ||||||
|  |     ) -> None: | ||||||
|  |  | ||||||
|  |         # hold the historical predictions in memory so we are sending back | ||||||
|  |         # correct array to strategy | ||||||
|  |  | ||||||
|  |         if pair not in self.dd.model_return_values: | ||||||
|  |             pred_df, do_preds = self.predict(dataframe, dk) | ||||||
|  |             self.dd.set_initial_return_values(pair, dk, pred_df, do_preds) | ||||||
|  |             dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe) | ||||||
|  |             return | ||||||
|  |         elif self.dk.check_if_model_expired(trained_timestamp): | ||||||
|  |             pred_df = DataFrame(np.zeros((2, len(dk.label_list))), columns=dk.label_list) | ||||||
|  |             do_preds, dk.DI_values = np.ones(2) * 2, np.zeros(2) | ||||||
|  |             logger.warning( | ||||||
|  |                 "Model expired, returning null values to strategy. Strategy " | ||||||
|  |                 "construction should take care to consider this event with " | ||||||
|  |                 "prediction == 0 and do_predict == 2" | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             # Only feed in the most recent candle for prediction in live scenario | ||||||
|  |             pred_df, do_preds = self.predict(dataframe.iloc[-self.CONV_WIDTH:], dk, first=False) | ||||||
|  |  | ||||||
|  |         self.dd.append_model_predictions(pair, pred_df, do_preds, dk, len(dataframe)) | ||||||
|  |         dk.return_dataframe = self.dd.attach_return_values_to_return_dataframe(pair, dataframe) | ||||||
|  |  | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     def check_if_feature_list_matches_strategy( | ||||||
|  |         self, dataframe: DataFrame, dk: FreqaiDataKitchen | ||||||
|  |     ) -> None: | ||||||
|  |         """ | ||||||
|  |         Ensure user is passing the proper feature set if they are reusing an `identifier` pointing | ||||||
|  |         to a folder holding existing models. | ||||||
|  |         :params: | ||||||
|  |         dataframe: DataFrame = strategy provided dataframe | ||||||
|  |         dk: FreqaiDataKitchen = non-persistent data container/analyzer for current coin/bot loop | ||||||
|  |         """ | ||||||
|  |         dk.find_features(dataframe) | ||||||
|  |         if "training_features_list_raw" in dk.data: | ||||||
|  |             feature_list = dk.data["training_features_list_raw"] | ||||||
|  |         else: | ||||||
|  |             feature_list = dk.training_features_list | ||||||
|  |         if dk.training_features_list != feature_list: | ||||||
|  |             raise OperationalException( | ||||||
|  |                 "Trying to access pretrained model with `identifier` " | ||||||
|  |                 "but found different features furnished by current strategy." | ||||||
|  |                 "Change `identifer` to train from scratch, or ensure the" | ||||||
|  |                 "strategy is furnishing the same features as the pretrained" | ||||||
|  |                 "model" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |     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::remove_outliers() for an example | ||||||
|  |         of how outlier data points are dropped from the dataframe used for training. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if self.freqai_info.get("feature_parameters", {}).get("principal_component_analysis"): | ||||||
|  |             dk.principal_component_analysis() | ||||||
|  |  | ||||||
|  |         if self.freqai_info.get("feature_parameters", {}).get("use_SVM_to_remove_outliers"): | ||||||
|  |             dk.use_SVM_to_remove_outliers(predict=False) | ||||||
|  |  | ||||||
|  |         if self.freqai_info.get("feature_parameters", {}).get("DI_threshold"): | ||||||
|  |             dk.data["avg_mean_dist"] = dk.compute_distances() | ||||||
|  |  | ||||||
|  |         # if self.feature_parameters["determine_statistical_distributions"]: | ||||||
|  |         #     dk.determine_statistical_distributions() | ||||||
|  |         # if self.feature_parameters["remove_outliers"]: | ||||||
|  |         #     dk.remove_outliers(predict=False) | ||||||
|  |  | ||||||
|  |     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. | ||||||
|  |         """ | ||||||
|  |         if self.freqai_info.get("feature_parameters", {}).get("principal_component_analysis"): | ||||||
|  |             dk.pca_transform(dataframe) | ||||||
|  |  | ||||||
|  |         if self.freqai_info.get("feature_parameters", {}).get("use_SVM_to_remove_outliers"): | ||||||
|  |             dk.use_SVM_to_remove_outliers(predict=True) | ||||||
|  |  | ||||||
|  |         if self.freqai_info.get("feature_parameters", {}).get("DI_threshold"): | ||||||
|  |             dk.check_if_pred_in_training_spaces() | ||||||
|  |  | ||||||
|  |         # if self.feature_parameters["determine_statistical_distributions"]: | ||||||
|  |         #     dk.determine_statistical_distributions() | ||||||
|  |         # if self.feature_parameters["remove_outliers"]: | ||||||
|  |         #     dk.remove_outliers(predict=True)  # creates dropped index | ||||||
|  |  | ||||||
|  |     def model_exists( | ||||||
|  |         self, | ||||||
|  |         pair: str, | ||||||
|  |         dk: FreqaiDataKitchen, | ||||||
|  |         trained_timestamp: int = None, | ||||||
|  |         model_filename: str = "", | ||||||
|  |         scanning: bool = False, | ||||||
|  |     ) -> bool: | ||||||
|  |         """ | ||||||
|  |         Given a pair and path, check if a model already exists | ||||||
|  |         :param pair: pair e.g. BTC/USD | ||||||
|  |         :param path: path to model | ||||||
|  |         """ | ||||||
|  |         coin, _ = pair.split("/") | ||||||
|  |  | ||||||
|  |         if not self.live: | ||||||
|  |             dk.model_filename = model_filename = "cb_" + coin.lower() + "_" + str(trained_timestamp) | ||||||
|  |  | ||||||
|  |         path_to_modelfile = Path(dk.data_path / str(model_filename + "_model.joblib")) | ||||||
|  |         file_exists = path_to_modelfile.is_file() | ||||||
|  |         if file_exists and not scanning: | ||||||
|  |             logger.info("Found model at %s", dk.data_path / dk.model_filename) | ||||||
|  |         elif not scanning: | ||||||
|  |             logger.info("Could not find model at %s", dk.data_path / dk.model_filename) | ||||||
|  |         return file_exists | ||||||
|  |  | ||||||
|  |     def set_full_path(self) -> None: | ||||||
|  |         self.full_path = Path( | ||||||
|  |             self.config["user_data_dir"] / "models" / str(self.freqai_info.get("identifier")) | ||||||
|  |         ) | ||||||
|  |         self.full_path.mkdir(parents=True, exist_ok=True) | ||||||
|  |         shutil.copy( | ||||||
|  |             self.config["config_files"][0], | ||||||
|  |             Path(self.full_path, Path(self.config["config_files"][0]).name), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def remove_features_from_df(self, dataframe: DataFrame) -> DataFrame: | ||||||
|  |         """ | ||||||
|  |         Remove the features from the dataframe before returning it to strategy. This keeps it | ||||||
|  |         compact for Frequi purposes. | ||||||
|  |         """ | ||||||
|  |         to_keep = [ | ||||||
|  |             col for col in dataframe.columns if not col.startswith("%") or col.startswith("%%") | ||||||
|  |         ] | ||||||
|  |         return dataframe[to_keep] | ||||||
|  |  | ||||||
|  |     def train_model_in_series( | ||||||
|  |         self, | ||||||
|  |         new_trained_timerange: TimeRange, | ||||||
|  |         pair: str, | ||||||
|  |         strategy: IStrategy, | ||||||
|  |         dk: FreqaiDataKitchen, | ||||||
|  |         data_load_timerange: TimeRange, | ||||||
|  |     ): | ||||||
|  |         """ | ||||||
|  |         Retreive data and train model in single threaded mode (only used if model directory is empty | ||||||
|  |         upon startup for dry/live ) | ||||||
|  |         :params: | ||||||
|  |         new_trained_timerange: TimeRange = the timerange to train the model on | ||||||
|  |         metadata: dict = strategy provided metadata | ||||||
|  |         strategy: IStrategy = user defined strategy object | ||||||
|  |         dk: FreqaiDataKitchen = non-persistent data container for current coin/loop | ||||||
|  |         data_load_timerange: TimeRange = the amount of data to be loaded for populate_any_indicators | ||||||
|  |         (larger than new_trained_timerange so that new_trained_timerange does not contain any NaNs) | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         corr_dataframes, base_dataframes = dk.get_base_and_corr_dataframes( | ||||||
|  |             data_load_timerange, pair | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         unfiltered_dataframe = dk.use_strategy_to_populate_indicators( | ||||||
|  |             strategy, corr_dataframes, base_dataframes, pair | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         unfiltered_dataframe = dk.slice_dataframe(new_trained_timerange, unfiltered_dataframe) | ||||||
|  |  | ||||||
|  |         # find the features indicated by strategy and store in datakitchen | ||||||
|  |         dk.find_features(unfiltered_dataframe) | ||||||
|  |  | ||||||
|  |         model = self.train(unfiltered_dataframe, pair, dk) | ||||||
|  |  | ||||||
|  |         self.dd.pair_dict[pair]["trained_timestamp"] = new_trained_timerange.stopts | ||||||
|  |         dk.set_new_model_names(pair, new_trained_timerange) | ||||||
|  |         self.dd.pair_dict[pair]["first"] = False | ||||||
|  |         if self.dd.pair_dict[pair]["priority"] == 1 and self.scanning: | ||||||
|  |             with self.lock: | ||||||
|  |                 self.dd.pair_to_end_of_training_queue(pair) | ||||||
|  |         dk.save_data(model, coin=pair, keras_model=self.keras) | ||||||
|  |  | ||||||
|  |         if self.freqai_info.get("purge_old_models", False): | ||||||
|  |             self.dd.purge_old_models() | ||||||
|  |         # self.retrain = False | ||||||
|  |  | ||||||
|  |     # Following methods which are overridden by user made prediction models. | ||||||
|  |     # See freqai/prediction_models/CatboostPredictionModlel.py for an example. | ||||||
|  |  | ||||||
|  |     @abstractmethod | ||||||
|  |     def train(self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen) -> Any: | ||||||
|  |         """ | ||||||
|  |         Filter the training data and train a model to it. Train makes heavy use of the datahandler | ||||||
|  |         for storing, saving, loading, and analyzing the data. | ||||||
|  |         :params: | ||||||
|  |         :unfiltered_dataframe: Full dataframe for the current training period | ||||||
|  |         :metadata: pair metadata from strategy. | ||||||
|  |         :returns: | ||||||
|  |         :model: Trained model which can be used to inference (self.predict) | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     @abstractmethod | ||||||
|  |     def fit(self) -> Any: | ||||||
|  |         """ | ||||||
|  |         Most regressors use the same function names and arguments e.g. user | ||||||
|  |         can drop in LGBMRegressor in place of CatBoostRegressor and all data | ||||||
|  |         management will be properly handled by Freqai. | ||||||
|  |         :params: | ||||||
|  |         data_dictionary: Dict = the dictionary constructed by DataHandler to hold | ||||||
|  |         all the training and test data/labels. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     @abstractmethod | ||||||
|  |     def predict( | ||||||
|  |         self, dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = True | ||||||
|  |     ) -> Tuple[DataFrame, npt.ArrayLike]: | ||||||
|  |         """ | ||||||
|  |         Filter the prediction features data and predict with it. | ||||||
|  |         :param: | ||||||
|  |         unfiltered_dataframe: Full dataframe for the current backtest period. | ||||||
|  |         dk: FreqaiDataKitchen = Data management/analysis tool assoicated to present pair only | ||||||
|  |         :return: | ||||||
|  |         :predictions: np.array of predictions | ||||||
|  |         :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove | ||||||
|  |         data (NaNs) or felt uncertain about data (i.e. SVM and/or DI index) | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     def make_labels(self, dataframe: DataFrame, dk: FreqaiDataKitchen) -> DataFrame: | ||||||
|  |         """ | ||||||
|  |         User defines the labels here (target values). | ||||||
|  |         :params: | ||||||
|  |         dataframe: DataFrame = the full dataframe for the present training period | ||||||
|  |         dk: FreqaiDataKitchen = Data management/analysis tool assoicated to present pair only | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     @abstractmethod | ||||||
|  |     def return_values(self, dataframe: DataFrame, dk: FreqaiDataKitchen) -> DataFrame: | ||||||
|  |         """ | ||||||
|  |         User defines the dataframe to be returned to strategy here. | ||||||
|  |         :params: | ||||||
|  |         dataframe: DataFrame = the full dataframe for the current prediction (live) | ||||||
|  |         or --timerange (backtesting) | ||||||
|  |         dk: FreqaiDataKitchen = Data management/analysis tool assoicated to present pair only | ||||||
|  |         :returns: | ||||||
|  |         dataframe: DataFrame = dataframe filled with user defined data | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return | ||||||
							
								
								
									
										154
									
								
								freqtrade/freqai/prediction_models/CatboostPredictionModel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								freqtrade/freqai/prediction_models/CatboostPredictionModel.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | |||||||
|  | import logging | ||||||
|  | from typing import Any, Dict, Tuple | ||||||
|  |  | ||||||
|  | from catboost import CatBoostRegressor, Pool | ||||||
|  | from pandas import DataFrame | ||||||
|  |  | ||||||
|  | from freqtrade.freqai.data_kitchen import FreqaiDataKitchen | ||||||
|  | from freqtrade.freqai.freqai_interface import IFreqaiModel | ||||||
|  |  | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CatboostPredictionModel(IFreqaiModel): | ||||||
|  |     """ | ||||||
|  |     User created prediction model. The class needs to override three necessary | ||||||
|  |     functions, predict(), train(), fit(). The class inherits ModelHandler which | ||||||
|  |     has its own DataHandler where data is held, saved, loaded, and managed. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def return_values(self, dataframe: DataFrame, dk: FreqaiDataKitchen) -> DataFrame: | ||||||
|  |         """ | ||||||
|  |         User uses this function to add any additional return values to the dataframe. | ||||||
|  |         e.g. | ||||||
|  |         dataframe['volatility'] = dk.volatility_values | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return dataframe | ||||||
|  |  | ||||||
|  |     def make_labels(self, dataframe: DataFrame, dk: FreqaiDataKitchen) -> DataFrame: | ||||||
|  |         """ | ||||||
|  |         User defines the labels here (target values). | ||||||
|  |         :params: | ||||||
|  |         :dataframe: the full dataframe for the present training period | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         dataframe["s"] = ( | ||||||
|  |             dataframe["close"] | ||||||
|  |             .shift(-self.feature_parameters["period"]) | ||||||
|  |             .rolling(self.feature_parameters["period"]) | ||||||
|  |             .mean() | ||||||
|  |             / dataframe["close"] | ||||||
|  |             - 1 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return dataframe["s"] | ||||||
|  |  | ||||||
|  |     def train( | ||||||
|  |         self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen | ||||||
|  |     ) -> Tuple[DataFrame, DataFrame]: | ||||||
|  |         """ | ||||||
|  |         Filter the training data and train a model to it. Train makes heavy use of the datahkitchen | ||||||
|  |         for storing, saving, loading, and analyzing the data. | ||||||
|  |         :params: | ||||||
|  |         :unfiltered_dataframe: Full dataframe for the current training period | ||||||
|  |         :metadata: pair metadata from strategy. | ||||||
|  |         :returns: | ||||||
|  |         :model: Trained model which can be used to inference (self.predict) | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         logger.info("--------------------Starting training " f"{pair} --------------------") | ||||||
|  |  | ||||||
|  |         # unfiltered_labels = self.make_labels(unfiltered_dataframe, dk) | ||||||
|  |         # filter the features requested by user in the configuration file and elegantly handle NaNs | ||||||
|  |         features_filtered, labels_filtered = dk.filter_features( | ||||||
|  |             unfiltered_dataframe, | ||||||
|  |             dk.training_features_list, | ||||||
|  |             dk.label_list, | ||||||
|  |             training_filter=True, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # split data into train/test data. | ||||||
|  |         data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered) | ||||||
|  |         dk.fit_labels()  # fit labels to a cauchy distribution so we know what to expect in strategy | ||||||
|  |         # normalize all data based on train_dataset only | ||||||
|  |         data_dictionary = dk.normalize_data(data_dictionary) | ||||||
|  |  | ||||||
|  |         # optional additional data cleaning/analysis | ||||||
|  |         self.data_cleaning_train(dk) | ||||||
|  |  | ||||||
|  |         logger.info( | ||||||
|  |             f'Training model on {len(dk.data_dictionary["train_features"].columns)}' " features" | ||||||
|  |         ) | ||||||
|  |         logger.info(f'Training model on {len(data_dictionary["train_features"])} data points') | ||||||
|  |  | ||||||
|  |         model = self.fit(data_dictionary) | ||||||
|  |  | ||||||
|  |         logger.info(f"--------------------done training {pair}--------------------") | ||||||
|  |  | ||||||
|  |         return model | ||||||
|  |  | ||||||
|  |     def fit(self, data_dictionary: Dict) -> Any: | ||||||
|  |         """ | ||||||
|  |         User sets up the training and test data to fit their desired model here | ||||||
|  |         :params: | ||||||
|  |         :data_dictionary: the dictionary constructed by DataHandler to hold | ||||||
|  |         all the training and test data/labels. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         train_data = Pool( | ||||||
|  |             data=data_dictionary["train_features"], | ||||||
|  |             label=data_dictionary["train_labels"], | ||||||
|  |             weight=data_dictionary["train_weights"], | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         test_data = Pool( | ||||||
|  |             data=data_dictionary["test_features"], | ||||||
|  |             label=data_dictionary["test_labels"], | ||||||
|  |             weight=data_dictionary["test_weights"], | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         model = CatBoostRegressor( | ||||||
|  |             allow_writing_files=False, | ||||||
|  |             verbose=100, | ||||||
|  |             early_stopping_rounds=400, | ||||||
|  |             **self.model_training_parameters, | ||||||
|  |         ) | ||||||
|  |         model.fit(X=train_data, eval_set=test_data) | ||||||
|  |  | ||||||
|  |         return model | ||||||
|  |  | ||||||
|  |     def predict( | ||||||
|  |         self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False | ||||||
|  |     ) -> Tuple[DataFrame, DataFrame]: | ||||||
|  |         """ | ||||||
|  |         Filter the prediction features data and predict with it. | ||||||
|  |         :param: unfiltered_dataframe: Full dataframe for the current backtest period. | ||||||
|  |         :return: | ||||||
|  |         :pred_df: dataframe containing the predictions | ||||||
|  |         :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove | ||||||
|  |         data (NaNs) or felt uncertain about data (PCA and DI index) | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         dk.find_features(unfiltered_dataframe) | ||||||
|  |         filtered_dataframe, _ = dk.filter_features( | ||||||
|  |             unfiltered_dataframe, dk.training_features_list, training_filter=False | ||||||
|  |         ) | ||||||
|  |         filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) | ||||||
|  |         dk.data_dictionary["prediction_features"] = filtered_dataframe | ||||||
|  |  | ||||||
|  |         # optional additional data cleaning/analysis | ||||||
|  |         self.data_cleaning_predict(dk, filtered_dataframe) | ||||||
|  |  | ||||||
|  |         predictions = self.model.predict(dk.data_dictionary["prediction_features"]) | ||||||
|  |         pred_df = DataFrame(predictions, columns=dk.label_list) | ||||||
|  |  | ||||||
|  |         for label in dk.label_list: | ||||||
|  |             pred_df[label] = ( | ||||||
|  |                 (pred_df[label] + 1) | ||||||
|  |                 * (dk.data["labels_max"][label] - dk.data["labels_min"][label]) | ||||||
|  |                 / 2 | ||||||
|  |             ) + dk.data["labels_min"][label] | ||||||
|  |  | ||||||
|  |         return (pred_df, dk.do_predict) | ||||||
| @@ -0,0 +1,133 @@ | |||||||
|  | import logging | ||||||
|  | from typing import Any, Dict, Tuple | ||||||
|  |  | ||||||
|  | from catboost import CatBoostRegressor  # , Pool | ||||||
|  | from pandas import DataFrame | ||||||
|  | from sklearn.multioutput import MultiOutputRegressor | ||||||
|  |  | ||||||
|  | from freqtrade.freqai.data_kitchen import FreqaiDataKitchen | ||||||
|  | from freqtrade.freqai.freqai_interface import IFreqaiModel | ||||||
|  |  | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CatboostPredictionMultiModel(IFreqaiModel): | ||||||
|  |     """ | ||||||
|  |     User created prediction model. The class needs to override three necessary | ||||||
|  |     functions, predict(), train(), fit(). The class inherits ModelHandler which | ||||||
|  |     has its own DataHandler where data is held, saved, loaded, and managed. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def return_values(self, dataframe: DataFrame, dk: FreqaiDataKitchen) -> DataFrame: | ||||||
|  |         """ | ||||||
|  |         User uses this function to add any additional return values to the dataframe. | ||||||
|  |         e.g. | ||||||
|  |         dataframe['volatility'] = dk.volatility_values | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return dataframe | ||||||
|  |  | ||||||
|  |     def train( | ||||||
|  |         self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen | ||||||
|  |     ) -> Tuple[DataFrame, DataFrame]: | ||||||
|  |         """ | ||||||
|  |         Filter the training data and train a model to it. Train makes heavy use of the datahkitchen | ||||||
|  |         for storing, saving, loading, and analyzing the data. | ||||||
|  |         :params: | ||||||
|  |         :unfiltered_dataframe: Full dataframe for the current training period | ||||||
|  |         :metadata: pair metadata from strategy. | ||||||
|  |         :returns: | ||||||
|  |         :model: Trained model which can be used to inference (self.predict) | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         logger.info("--------------------Starting training " f"{pair} --------------------") | ||||||
|  |  | ||||||
|  |         # unfiltered_labels = self.make_labels(unfiltered_dataframe, dk) | ||||||
|  |         # filter the features requested by user in the configuration file and elegantly handle NaNs | ||||||
|  |         features_filtered, labels_filtered = dk.filter_features( | ||||||
|  |             unfiltered_dataframe, | ||||||
|  |             dk.training_features_list, | ||||||
|  |             dk.label_list, | ||||||
|  |             training_filter=True, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # split data into train/test data. | ||||||
|  |         data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered) | ||||||
|  |         dk.fit_labels()  # fit labels to a cauchy distribution so we know what to expect in strategy | ||||||
|  |         # normalize all data based on train_dataset only | ||||||
|  |         data_dictionary = dk.normalize_data(data_dictionary) | ||||||
|  |  | ||||||
|  |         # optional additional data cleaning/analysis | ||||||
|  |         self.data_cleaning_train(dk) | ||||||
|  |  | ||||||
|  |         logger.info( | ||||||
|  |             f'Training model on {len(dk.data_dictionary["train_features"].columns)}' " features" | ||||||
|  |         ) | ||||||
|  |         logger.info(f'Training model on {len(data_dictionary["train_features"])} data points') | ||||||
|  |  | ||||||
|  |         model = self.fit(data_dictionary) | ||||||
|  |  | ||||||
|  |         logger.info(f"--------------------done training {pair}--------------------") | ||||||
|  |  | ||||||
|  |         return model | ||||||
|  |  | ||||||
|  |     def fit(self, data_dictionary: Dict) -> Any: | ||||||
|  |         """ | ||||||
|  |         User sets up the training and test data to fit their desired model here | ||||||
|  |         :params: | ||||||
|  |         :data_dictionary: the dictionary constructed by DataHandler to hold | ||||||
|  |         all the training and test data/labels. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         cbr = CatBoostRegressor( | ||||||
|  |             allow_writing_files=False, | ||||||
|  |             gpu_ram_part=0.5, | ||||||
|  |             verbose=100, | ||||||
|  |             early_stopping_rounds=400, | ||||||
|  |             **self.model_training_parameters, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         X = data_dictionary["train_features"] | ||||||
|  |         y = data_dictionary["train_labels"] | ||||||
|  |         # eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) | ||||||
|  |         sample_weight = data_dictionary["train_weights"] | ||||||
|  |  | ||||||
|  |         model = MultiOutputRegressor(estimator=cbr) | ||||||
|  |         model.fit(X=X, y=y, sample_weight=sample_weight)  # , eval_set=eval_set) | ||||||
|  |  | ||||||
|  |         return model | ||||||
|  |  | ||||||
|  |     def predict( | ||||||
|  |         self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen, first: bool = False | ||||||
|  |     ) -> Tuple[DataFrame, DataFrame]: | ||||||
|  |         """ | ||||||
|  |         Filter the prediction features data and predict with it. | ||||||
|  |         :param: unfiltered_dataframe: Full dataframe for the current backtest period. | ||||||
|  |         :return: | ||||||
|  |         :pred_df: dataframe containing the predictions | ||||||
|  |         :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove | ||||||
|  |         data (NaNs) or felt uncertain about data (PCA and DI index) | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         dk.find_features(unfiltered_dataframe) | ||||||
|  |         filtered_dataframe, _ = dk.filter_features( | ||||||
|  |             unfiltered_dataframe, dk.training_features_list, training_filter=False | ||||||
|  |         ) | ||||||
|  |         filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) | ||||||
|  |         dk.data_dictionary["prediction_features"] = filtered_dataframe | ||||||
|  |  | ||||||
|  |         # optional additional data cleaning/analysis | ||||||
|  |         self.data_cleaning_predict(dk, filtered_dataframe) | ||||||
|  |  | ||||||
|  |         predictions = self.model.predict(dk.data_dictionary["prediction_features"]) | ||||||
|  |         pred_df = DataFrame(predictions, columns=dk.label_list) | ||||||
|  |  | ||||||
|  |         for label in dk.label_list: | ||||||
|  |             pred_df[label] = ( | ||||||
|  |                 (pred_df[label] + 1) | ||||||
|  |                 * (dk.data["labels_max"][label] - dk.data["labels_min"][label]) | ||||||
|  |                 / 2 | ||||||
|  |             ) + dk.data["labels_min"][label] | ||||||
|  |  | ||||||
|  |         return (pred_df, dk.do_predict) | ||||||
							
								
								
									
										127
									
								
								freqtrade/freqai/prediction_models/LightGBMPredictionModel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								freqtrade/freqai/prediction_models/LightGBMPredictionModel.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | |||||||
|  | import logging | ||||||
|  | from typing import Any, Dict, Tuple | ||||||
|  |  | ||||||
|  | from lightgbm import LGBMRegressor | ||||||
|  | from pandas import DataFrame | ||||||
|  |  | ||||||
|  | from freqtrade.freqai.data_kitchen import FreqaiDataKitchen | ||||||
|  | from freqtrade.freqai.freqai_interface import IFreqaiModel | ||||||
|  |  | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LightGBMPredictionModel(IFreqaiModel): | ||||||
|  |     """ | ||||||
|  |     User created prediction model. The class needs to override three necessary | ||||||
|  |     functions, predict(), train(), fit(). The class inherits ModelHandler which | ||||||
|  |     has its own DataHandler where data is held, saved, loaded, and managed. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def return_values(self, dataframe: DataFrame, dk: FreqaiDataKitchen) -> DataFrame: | ||||||
|  |         """ | ||||||
|  |         User uses this function to add any additional return values to the dataframe. | ||||||
|  |         e.g. | ||||||
|  |         dataframe['volatility'] = dk.volatility_values | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         return dataframe | ||||||
|  |  | ||||||
|  |     def train( | ||||||
|  |         self, unfiltered_dataframe: DataFrame, pair: str, dk: FreqaiDataKitchen | ||||||
|  |     ) -> Tuple[DataFrame, DataFrame]: | ||||||
|  |         """ | ||||||
|  |         Filter the training data and train a model to it. Train makes heavy use of the datahkitchen | ||||||
|  |         for storing, saving, loading, and analyzing the data. | ||||||
|  |         :params: | ||||||
|  |         :unfiltered_dataframe: Full dataframe for the current training period | ||||||
|  |         :metadata: pair metadata from strategy. | ||||||
|  |         :returns: | ||||||
|  |         :model: Trained model which can be used to inference (self.predict) | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         logger.info("--------------------Starting training " f"{pair} --------------------") | ||||||
|  |  | ||||||
|  |         # unfiltered_labels = self.make_labels(unfiltered_dataframe, dk) | ||||||
|  |         # filter the features requested by user in the configuration file and elegantly handle NaNs | ||||||
|  |         features_filtered, labels_filtered = dk.filter_features( | ||||||
|  |             unfiltered_dataframe, | ||||||
|  |             dk.training_features_list, | ||||||
|  |             dk.label_list, | ||||||
|  |             training_filter=True, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # split data into train/test data. | ||||||
|  |         data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered) | ||||||
|  |         dk.fit_labels()  # fit labels to a cauchy distribution so we know what to expect in strategy | ||||||
|  |         # normalize all data based on train_dataset only | ||||||
|  |         data_dictionary = dk.normalize_data(data_dictionary) | ||||||
|  |  | ||||||
|  |         # optional additional data cleaning/analysis | ||||||
|  |         self.data_cleaning_train(dk) | ||||||
|  |  | ||||||
|  |         logger.info( | ||||||
|  |             f'Training model on {len(dk.data_dictionary["train_features"].columns)}' " features" | ||||||
|  |         ) | ||||||
|  |         logger.info(f'Training model on {len(data_dictionary["train_features"])} data points') | ||||||
|  |  | ||||||
|  |         model = self.fit(data_dictionary) | ||||||
|  |  | ||||||
|  |         logger.info(f"--------------------done training {pair}--------------------") | ||||||
|  |  | ||||||
|  |         return model | ||||||
|  |  | ||||||
|  |     def fit(self, data_dictionary: Dict) -> Any: | ||||||
|  |         """ | ||||||
|  |         Most regressors use the same function names and arguments e.g. user | ||||||
|  |         can drop in LGBMRegressor in place of CatBoostRegressor and all data | ||||||
|  |         management will be properly handled by Freqai. | ||||||
|  |         :params: | ||||||
|  |         :data_dictionary: the dictionary constructed by DataHandler to hold | ||||||
|  |         all the training and test data/labels. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) | ||||||
|  |         X = data_dictionary["train_features"] | ||||||
|  |         y = data_dictionary["train_labels"] | ||||||
|  |  | ||||||
|  |         model = LGBMRegressor(seed=42, n_estimators=2000, verbosity=1, force_col_wise=True) | ||||||
|  |         model.fit(X=X, y=y, eval_set=eval_set) | ||||||
|  |  | ||||||
|  |         return model | ||||||
|  |  | ||||||
|  |     def predict( | ||||||
|  |         self, unfiltered_dataframe: DataFrame, dk: FreqaiDataKitchen | ||||||
|  |     ) -> Tuple[DataFrame, DataFrame]: | ||||||
|  |         """ | ||||||
|  |         Filter the prediction features data and predict with it. | ||||||
|  |         :param: unfiltered_dataframe: Full dataframe for the current backtest period. | ||||||
|  |         :return: | ||||||
|  |         :predictions: np.array of predictions | ||||||
|  |         :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove | ||||||
|  |         data (NaNs) or felt uncertain about data (PCA and DI index) | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         # logger.info("--------------------Starting prediction--------------------") | ||||||
|  |  | ||||||
|  |         original_feature_list = dk.find_features(unfiltered_dataframe) | ||||||
|  |         filtered_dataframe, _ = dk.filter_features( | ||||||
|  |             unfiltered_dataframe, original_feature_list, training_filter=False | ||||||
|  |         ) | ||||||
|  |         filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) | ||||||
|  |         dk.data_dictionary["prediction_features"] = filtered_dataframe | ||||||
|  |  | ||||||
|  |         # optional additional data cleaning/analysis | ||||||
|  |         self.data_cleaning_predict(dk, filtered_dataframe) | ||||||
|  |  | ||||||
|  |         predictions = self.model.predict(dk.data_dictionary["prediction_features"]) | ||||||
|  |         pred_df = DataFrame(predictions, columns=dk.label_list) | ||||||
|  |  | ||||||
|  |         for label in dk.label_list: | ||||||
|  |             pred_df[label] = ( | ||||||
|  |                 (pred_df[label] + 1) | ||||||
|  |                 * (dk.data["labels_max"][label] - dk.data["labels_min"][label]) | ||||||
|  |                 / 2 | ||||||
|  |             ) + dk.data["labels_min"][label] | ||||||
|  |  | ||||||
|  |         return (pred_df, dk.do_predict) | ||||||
							
								
								
									
										12
									
								
								freqtrade/freqai/strategy_bridge.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								freqtrade/freqai/strategy_bridge.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CustomModel: | ||||||
|  |     """ | ||||||
|  |     A bridge between the user defined IFreqaiModel class | ||||||
|  |     and the strategy. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, config): | ||||||
|  |  | ||||||
|  |         self.bridge = FreqaiModelResolver.load_freqaimodel(config) | ||||||
| @@ -206,6 +206,11 @@ class Backtesting: | |||||||
|         """ |         """ | ||||||
|         self.progress.init_step(BacktestState.DATALOAD, 1) |         self.progress.init_step(BacktestState.DATALOAD, 1) | ||||||
|  |  | ||||||
|  |         if self.config.get('freqai') is not None: | ||||||
|  |             self.required_startup += int(self.config.get('freqai', {}).get('startup_candles', 1000)) | ||||||
|  |             logger.info(f'Increasing startup_candle_count for freqai to {self.required_startup}') | ||||||
|  |             self.config['startup_candle_count'] = self.required_startup | ||||||
|  |  | ||||||
|         data = history.load_data( |         data = history.load_data( | ||||||
|             datadir=self.config['datadir'], |             datadir=self.config['datadir'], | ||||||
|             pairs=self.pairlists.whitelist, |             pairs=self.pairlists.whitelist, | ||||||
|   | |||||||
| @@ -40,3 +40,14 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str], | |||||||
|             except re.error as err: |             except re.error as err: | ||||||
|                 raise ValueError(f"Wildcard error in {pair_wc}, {err}") |                 raise ValueError(f"Wildcard error in {pair_wc}, {err}") | ||||||
|     return result |     return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def dynamic_expand_pairlist(config: dict, markets: list) -> List[str]: | ||||||
|  |     if config.get('freqai', {}): | ||||||
|  |         full_pairs = config['pairs'] + [pair for pair in config['freqai']['corr_pairlist'] | ||||||
|  |                                         if pair not in config['pairs']] | ||||||
|  |         expanded_pairs = expand_pairlist(full_pairs, markets) | ||||||
|  |     else: | ||||||
|  |         expanded_pairs = expand_pairlist(config['pairs'], markets) | ||||||
|  |  | ||||||
|  |     return expanded_pairs | ||||||
|   | |||||||
| @@ -18,7 +18,8 @@ class ExchangeResolver(IResolver): | |||||||
|     object_type = Exchange |     object_type = Exchange | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def load_exchange(exchange_name: str, config: dict, validate: bool = True) -> Exchange: |     def load_exchange(exchange_name: str, config: dict, validate: bool = True, | ||||||
|  |                       freqai: bool = False) -> Exchange: | ||||||
|         """ |         """ | ||||||
|         Load the custom class from config parameter |         Load the custom class from config parameter | ||||||
|         :param exchange_name: name of the Exchange to load |         :param exchange_name: name of the Exchange to load | ||||||
| @@ -31,7 +32,8 @@ class ExchangeResolver(IResolver): | |||||||
|         try: |         try: | ||||||
|             exchange = ExchangeResolver._load_exchange(exchange_name, |             exchange = ExchangeResolver._load_exchange(exchange_name, | ||||||
|                                                        kwargs={'config': config, |                                                        kwargs={'config': config, | ||||||
|                                                                'validate': validate}) |                                                                'validate': validate, | ||||||
|  |                                                                'freqai': freqai}) | ||||||
|         except ImportError: |         except ImportError: | ||||||
|             logger.info( |             logger.info( | ||||||
|                 f"No {exchange_name} specific subclass found. Using the generic class instead.") |                 f"No {exchange_name} specific subclass found. Using the generic class instead.") | ||||||
|   | |||||||
							
								
								
									
										50
									
								
								freqtrade/resolvers/freqaimodel_resolver.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								freqtrade/resolvers/freqaimodel_resolver.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | # pragma pylint: disable=attribute-defined-outside-init | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | This module load a custom model for freqai | ||||||
|  | """ | ||||||
|  | import logging | ||||||
|  | from pathlib import Path | ||||||
|  | from typing import Dict | ||||||
|  |  | ||||||
|  | from freqtrade.constants import USERPATH_FREQAIMODELS | ||||||
|  | from freqtrade.exceptions import OperationalException | ||||||
|  | from freqtrade.freqai.freqai_interface import IFreqaiModel | ||||||
|  | from freqtrade.resolvers import IResolver | ||||||
|  |  | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FreqaiModelResolver(IResolver): | ||||||
|  |     """ | ||||||
|  |     This class contains all the logic to load custom hyperopt loss class | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     object_type = IFreqaiModel | ||||||
|  |     object_type_str = "FreqaiModel" | ||||||
|  |     user_subdir = USERPATH_FREQAIMODELS | ||||||
|  |     initial_search_path = Path(__file__).parent.parent.joinpath( | ||||||
|  |                                                        "freqai/prediction_models").resolve() | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def load_freqaimodel(config: Dict) -> IFreqaiModel: | ||||||
|  |         """ | ||||||
|  |         Load the custom class from config parameter | ||||||
|  |         :param config: configuration dictionary | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         freqaimodel_name = config.get("freqaimodel") | ||||||
|  |         if not freqaimodel_name: | ||||||
|  |             raise OperationalException( | ||||||
|  |                 "No freqaimodel set. Please use `--freqaimodel` to " | ||||||
|  |                 "specify the FreqaiModel class to use.\n" | ||||||
|  |             ) | ||||||
|  |         freqaimodel = FreqaiModelResolver.load_object( | ||||||
|  |             freqaimodel_name, | ||||||
|  |             config, | ||||||
|  |             kwargs={"config": config}, | ||||||
|  |             extra_dir=config.get("freqaimodel_path"), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return freqaimodel | ||||||
| @@ -546,6 +546,23 @@ class IStrategy(ABC, HyperStrategyMixin): | |||||||
|         """ |         """ | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|  |     def populate_any_indicators(self, basepair: str, pair: str, df: DataFrame, tf: str, | ||||||
|  |                                 informative: DataFrame = None, coin: str = "", | ||||||
|  |                                 set_generalized_indicators: bool = False) -> DataFrame: | ||||||
|  |         """ | ||||||
|  |         Function designed to automatically generate, name and merge features | ||||||
|  |         from user indicated timeframes in the configuration file. User can add | ||||||
|  |         additional features here, but must follow the naming convention. | ||||||
|  |         Defined in IStrategy because Freqai needs to know it exists. | ||||||
|  |         :params: | ||||||
|  |         :pair: pair to be used as informative | ||||||
|  |         :df: strategy dataframe which will receive merges from informatives | ||||||
|  |         :tf: timeframe of the dataframe which will modify the feature names | ||||||
|  |         :informative: the dataframe associated with the informative pair | ||||||
|  |         :coin: the name of the coin which will modify the feature names. | ||||||
|  |         """ | ||||||
|  |         return df | ||||||
|  |  | ||||||
| ### | ### | ||||||
| # END - Intended to be overridden by strategy | # END - Intended to be overridden by strategy | ||||||
| ### | ### | ||||||
|   | |||||||
							
								
								
									
										342
									
								
								freqtrade/templates/FreqaiExampleStrategy.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										342
									
								
								freqtrade/templates/FreqaiExampleStrategy.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,342 @@ | |||||||
|  | import logging | ||||||
|  | from functools import reduce | ||||||
|  |  | ||||||
|  | import pandas as pd | ||||||
|  | import talib.abstract as ta | ||||||
|  | from pandas import DataFrame | ||||||
|  | from technical import qtpylib | ||||||
|  |  | ||||||
|  | from freqtrade.exchange import timeframe_to_prev_date | ||||||
|  | from freqtrade.freqai.strategy_bridge import CustomModel | ||||||
|  | from freqtrade.persistence import Trade | ||||||
|  | from freqtrade.strategy import DecimalParameter, IntParameter, merge_informative_pair | ||||||
|  | from freqtrade.strategy.interface import IStrategy | ||||||
|  |  | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FreqaiExampleStrategy(IStrategy): | ||||||
|  |     """ | ||||||
|  |     Example strategy showing how the user connects their own | ||||||
|  |     IFreqaiModel to the strategy. Namely, the user uses: | ||||||
|  |     self.model = CustomModel(self.config) | ||||||
|  |     self.model.bridge.start(dataframe, metadata) | ||||||
|  |  | ||||||
|  |     to make predictions on their data. populate_any_indicators() automatically | ||||||
|  |     generates the variety of features indicated by the user in the | ||||||
|  |     canonical freqtrade configuration file under config['freqai']. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     minimal_roi = {"0": 0.1, "240": -1} | ||||||
|  |  | ||||||
|  |     plot_config = { | ||||||
|  |         "main_plot": {}, | ||||||
|  |         "subplots": { | ||||||
|  |             "prediction": {"prediction": {"color": "blue"}}, | ||||||
|  |             "target_roi": { | ||||||
|  |                 "target_roi": {"color": "brown"}, | ||||||
|  |             }, | ||||||
|  |             "do_predict": { | ||||||
|  |                 "do_predict": {"color": "brown"}, | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     process_only_new_candles = True | ||||||
|  |     stoploss = -0.05 | ||||||
|  |     use_exit_signal = True | ||||||
|  |     startup_candle_count: int = 300 | ||||||
|  |     can_short = False | ||||||
|  |  | ||||||
|  |     linear_roi_offset = DecimalParameter( | ||||||
|  |         0.00, 0.02, default=0.005, space="sell", optimize=False, load=True | ||||||
|  |     ) | ||||||
|  |     max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True) | ||||||
|  |  | ||||||
|  |     def informative_pairs(self): | ||||||
|  |         whitelist_pairs = self.dp.current_whitelist() | ||||||
|  |         corr_pairs = self.config["freqai"]["corr_pairlist"] | ||||||
|  |         informative_pairs = [] | ||||||
|  |         for tf in self.config["freqai"]["timeframes"]: | ||||||
|  |             for pair in whitelist_pairs: | ||||||
|  |                 informative_pairs.append((pair, tf)) | ||||||
|  |             for pair in corr_pairs: | ||||||
|  |                 if pair in whitelist_pairs: | ||||||
|  |                     continue  # avoid duplication | ||||||
|  |                 informative_pairs.append((pair, tf)) | ||||||
|  |         return informative_pairs | ||||||
|  |  | ||||||
|  |     def bot_start(self): | ||||||
|  |         self.model = CustomModel(self.config) | ||||||
|  |  | ||||||
|  |     def populate_any_indicators( | ||||||
|  |         self, metadata, pair, df, tf, informative=None, coin="", set_generalized_indicators=False | ||||||
|  |     ): | ||||||
|  |         """ | ||||||
|  |         Function designed to automatically generate, name and merge features | ||||||
|  |         from user indicated timeframes in the configuration file. User controls the indicators | ||||||
|  |         passed to the training/prediction by prepending indicators with `'%-' + coin ` | ||||||
|  |         (see convention below). I.e. user should not prepend any supporting metrics | ||||||
|  |         (e.g. bb_lowerband below) with % unless they explicitly want to pass that metric to the | ||||||
|  |         model. | ||||||
|  |         :params: | ||||||
|  |         :pair: pair to be used as informative | ||||||
|  |         :df: strategy dataframe which will receive merges from informatives | ||||||
|  |         :tf: timeframe of the dataframe which will modify the feature names | ||||||
|  |         :informative: the dataframe associated with the informative pair | ||||||
|  |         :coin: the name of the coin which will modify the feature names. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         with self.model.bridge.lock: | ||||||
|  |             if informative is None: | ||||||
|  |                 informative = self.dp.get_pair_dataframe(pair, tf) | ||||||
|  |  | ||||||
|  |             # first loop is automatically duplicating indicators for time periods | ||||||
|  |             for t in self.freqai_info["feature_parameters"]["indicator_periods"]: | ||||||
|  |  | ||||||
|  |                 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}20sma-period_{t}"] = ta.SMA(informative, timeperiod=t) | ||||||
|  |                 informative[f"{coin}21ema-period_{t}"] = ta.EMA(informative, timeperiod=t) | ||||||
|  |                 informative[f"%-{coin}close_over_20sma-period_{t}"] = ( | ||||||
|  |                     informative["close"] / informative[f"{coin}20sma-period_{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 | ||||||
|  |                 ) | ||||||
|  |                 informative[f"{coin}bb_lowerband-period_{t}"] = bollinger["lower"] | ||||||
|  |                 informative[f"{coin}bb_middleband-period_{t}"] = bollinger["mid"] | ||||||
|  |                 informative[f"{coin}bb_upperband-period_{t}"] = bollinger["upper"] | ||||||
|  |  | ||||||
|  |                 informative[f"%-{coin}bb_width-period_{t}"] = ( | ||||||
|  |                     informative[f"{coin}bb_upperband-period_{t}"] | ||||||
|  |                     - informative[f"{coin}bb_lowerband-period_{t}"] | ||||||
|  |                 ) / informative[f"{coin}bb_middleband-period_{t}"] | ||||||
|  |                 informative[f"%-{coin}close-bb_lower-period_{t}"] = ( | ||||||
|  |                     informative["close"] / informative[f"{coin}bb_lowerband-period_{t}"] | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |                 informative[f"%-{coin}roc-period_{t}"] = ta.ROC(informative, timeperiod=t) | ||||||
|  |                 macd = ta.MACD(informative, timeperiod=t) | ||||||
|  |                 informative[f"%-{coin}macd-period_{t}"] = macd["macd"] | ||||||
|  |  | ||||||
|  |                 informative[f"%-{coin}relative_volume-period_{t}"] = ( | ||||||
|  |                     informative["volume"] / informative["volume"].rolling(t).mean() | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |             informative[f"%-{coin}pct-change"] = informative["close"].pct_change() | ||||||
|  |             informative[f"%-{coin}raw_volume"] = informative["volume"] | ||||||
|  |             informative[f"%-{coin}raw_price"] = informative["close"] | ||||||
|  |  | ||||||
|  |             indicators = [col for col in informative if col.startswith("%")] | ||||||
|  |             # This loop duplicates and shifts all indicators to add a sense of recency to data | ||||||
|  |             for n in range(self.freqai_info["feature_parameters"]["shift"] + 1): | ||||||
|  |                 if n == 0: | ||||||
|  |                     continue | ||||||
|  |                 informative_shift = informative[indicators].shift(n) | ||||||
|  |                 informative_shift = informative_shift.add_suffix("_shift-" + str(n)) | ||||||
|  |                 informative = pd.concat((informative, informative_shift), axis=1) | ||||||
|  |  | ||||||
|  |             df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True) | ||||||
|  |             skip_columns = [ | ||||||
|  |                 (s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"] | ||||||
|  |             ] | ||||||
|  |             df = df.drop(columns=skip_columns) | ||||||
|  |  | ||||||
|  |             # Add generalized indicators here (because in live, it will call this | ||||||
|  |             # function to populate indicators during training). Notice how we ensure not to | ||||||
|  |             # add them multiple times | ||||||
|  |             if set_generalized_indicators: | ||||||
|  |                 df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7 | ||||||
|  |                 df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25 | ||||||
|  |  | ||||||
|  |                 # user adds targets here by prepending them with &- (see convention below) | ||||||
|  |                 # If user wishes to use multiple targets, a multioutput prediction model | ||||||
|  |                 # needs to be used such as templates/CatboostPredictionMultiModel.py | ||||||
|  |                 df["&-s_close"] = ( | ||||||
|  |                     df["close"] | ||||||
|  |                     .shift(-self.freqai_info["feature_parameters"]["period"]) | ||||||
|  |                     .rolling(self.freqai_info["feature_parameters"]["period"]) | ||||||
|  |                     .mean() | ||||||
|  |                     / df["close"] | ||||||
|  |                     - 1 | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |         return df | ||||||
|  |  | ||||||
|  |     def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: | ||||||
|  |  | ||||||
|  |         self.freqai_info = self.config["freqai"] | ||||||
|  |         self.pair = metadata["pair"] | ||||||
|  |         sgi = True | ||||||
|  |         # the following loops are necessary for building the features | ||||||
|  |         # indicated by the user in the configuration file. | ||||||
|  |         # All indicators must be populated by populate_any_indicators() for live functionality | ||||||
|  |         # to work correctly. | ||||||
|  |         for tf in self.freqai_info["timeframes"]: | ||||||
|  |             dataframe = self.populate_any_indicators( | ||||||
|  |                 metadata, | ||||||
|  |                 self.pair, | ||||||
|  |                 dataframe.copy(), | ||||||
|  |                 tf, | ||||||
|  |                 coin=self.pair.split("/")[0] + "-", | ||||||
|  |                 set_generalized_indicators=sgi, | ||||||
|  |             ) | ||||||
|  |             sgi = False | ||||||
|  |             for pair in self.freqai_info["corr_pairlist"]: | ||||||
|  |                 if metadata["pair"] in pair: | ||||||
|  |                     continue  # do not include whitelisted pair twice if it is in corr_pairlist | ||||||
|  |                 dataframe = self.populate_any_indicators( | ||||||
|  |                     metadata, pair, dataframe.copy(), tf, coin=pair.split("/")[0] + "-" | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |         # the model will return 4 values, its prediction, an indication of whether or not the | ||||||
|  |         # prediction should be accepted, the target mean/std values from the labels used during | ||||||
|  |         # each training period. | ||||||
|  |         dataframe = self.model.bridge.start(dataframe, metadata, self) | ||||||
|  |  | ||||||
|  |         dataframe["target_roi"] = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * 1.25 | ||||||
|  |         dataframe["sell_roi"] = dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * 1.25 | ||||||
|  |         return dataframe | ||||||
|  |  | ||||||
|  |     def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: | ||||||
|  |  | ||||||
|  |         enter_long_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"]] | ||||||
|  |  | ||||||
|  |         if enter_long_conditions: | ||||||
|  |             df.loc[ | ||||||
|  |                 reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"] | ||||||
|  |             ] = (1, "long") | ||||||
|  |  | ||||||
|  |         enter_short_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"]] | ||||||
|  |  | ||||||
|  |         if enter_short_conditions: | ||||||
|  |             df.loc[ | ||||||
|  |                 reduce(lambda x, y: x & y, enter_short_conditions), ["enter_short", "enter_tag"] | ||||||
|  |             ] = (1, "short") | ||||||
|  |  | ||||||
|  |         return df | ||||||
|  |  | ||||||
|  |     def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: | ||||||
|  |         exit_long_conditions = [df["do_predict"] == 1, df["&-s_close"] < df["sell_roi"] * 0.25] | ||||||
|  |         if exit_long_conditions: | ||||||
|  |             df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1 | ||||||
|  |  | ||||||
|  |         exit_short_conditions = [df["do_predict"] == 1, df["&-s_close"] > df["target_roi"] * 0.25] | ||||||
|  |         if exit_short_conditions: | ||||||
|  |             df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1 | ||||||
|  |  | ||||||
|  |         return df | ||||||
|  |  | ||||||
|  |     def get_ticker_indicator(self): | ||||||
|  |         return int(self.config["timeframe"][:-1]) | ||||||
|  |  | ||||||
|  |     def custom_exit( | ||||||
|  |         self, pair: str, trade: Trade, current_time, current_rate, current_profit, **kwargs | ||||||
|  |     ): | ||||||
|  |  | ||||||
|  |         dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) | ||||||
|  |  | ||||||
|  |         trade_date = timeframe_to_prev_date(self.config["timeframe"], trade.open_date_utc) | ||||||
|  |         trade_candle = dataframe.loc[(dataframe["date"] == trade_date)] | ||||||
|  |  | ||||||
|  |         if trade_candle.empty: | ||||||
|  |             return None | ||||||
|  |         trade_candle = trade_candle.squeeze() | ||||||
|  |  | ||||||
|  |         follow_mode = self.config.get("freqai", {}).get("follow_mode", False) | ||||||
|  |  | ||||||
|  |         if not follow_mode: | ||||||
|  |             pair_dict = self.model.bridge.dd.pair_dict | ||||||
|  |         else: | ||||||
|  |             pair_dict = self.model.bridge.dd.follower_dict | ||||||
|  |  | ||||||
|  |         entry_tag = trade.enter_tag | ||||||
|  |  | ||||||
|  |         if ( | ||||||
|  |             "prediction" + entry_tag not in pair_dict[pair] | ||||||
|  |             or pair_dict[pair]["prediction" + entry_tag] > 0 | ||||||
|  |         ): | ||||||
|  |             with self.model.bridge.lock: | ||||||
|  |                 pair_dict[pair]["prediction" + entry_tag] = abs(trade_candle["&-s_close"]) | ||||||
|  |                 if not follow_mode: | ||||||
|  |                     self.model.bridge.dd.save_drawer_to_disk() | ||||||
|  |                 else: | ||||||
|  |                     self.model.bridge.dd.save_follower_dict_to_disk() | ||||||
|  |  | ||||||
|  |         roi_price = pair_dict[pair]["prediction" + entry_tag] | ||||||
|  |         roi_time = self.max_roi_time_long.value | ||||||
|  |  | ||||||
|  |         roi_decay = roi_price * ( | ||||||
|  |             1 - ((current_time - trade.open_date_utc).seconds) / (roi_time * 60) | ||||||
|  |         ) | ||||||
|  |         if roi_decay < 0: | ||||||
|  |             roi_decay = self.linear_roi_offset.value | ||||||
|  |         else: | ||||||
|  |             roi_decay += self.linear_roi_offset.value | ||||||
|  |  | ||||||
|  |         if current_profit > roi_decay: | ||||||
|  |             return "roi_custom_win" | ||||||
|  |  | ||||||
|  |         if current_profit < -roi_decay: | ||||||
|  |             return "roi_custom_loss" | ||||||
|  |  | ||||||
|  |     def confirm_trade_exit( | ||||||
|  |         self, | ||||||
|  |         pair: str, | ||||||
|  |         trade: Trade, | ||||||
|  |         order_type: str, | ||||||
|  |         amount: float, | ||||||
|  |         rate: float, | ||||||
|  |         time_in_force: str, | ||||||
|  |         exit_reason: str, | ||||||
|  |         current_time, | ||||||
|  |         **kwargs, | ||||||
|  |     ) -> bool: | ||||||
|  |  | ||||||
|  |         entry_tag = trade.enter_tag | ||||||
|  |         follow_mode = self.config.get("freqai", {}).get("follow_mode", False) | ||||||
|  |         if not follow_mode: | ||||||
|  |             pair_dict = self.model.bridge.dd.pair_dict | ||||||
|  |         else: | ||||||
|  |             pair_dict = self.model.bridge.dd.follower_dict | ||||||
|  |  | ||||||
|  |         with self.model.bridge.lock: | ||||||
|  |             pair_dict[pair]["prediction" + entry_tag] = 0 | ||||||
|  |             if not follow_mode: | ||||||
|  |                 self.model.bridge.dd.save_drawer_to_disk() | ||||||
|  |             else: | ||||||
|  |                 self.model.bridge.dd.save_follower_dict_to_disk() | ||||||
|  |  | ||||||
|  |         return True | ||||||
|  |  | ||||||
|  |     def confirm_trade_entry( | ||||||
|  |         self, | ||||||
|  |         pair: str, | ||||||
|  |         order_type: str, | ||||||
|  |         amount: float, | ||||||
|  |         rate: float, | ||||||
|  |         time_in_force: str, | ||||||
|  |         current_time, | ||||||
|  |         entry_tag, | ||||||
|  |         side: str, | ||||||
|  |         **kwargs, | ||||||
|  |     ) -> bool: | ||||||
|  |  | ||||||
|  |         df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) | ||||||
|  |         last_candle = df.iloc[-1].squeeze() | ||||||
|  |  | ||||||
|  |         if side == "long": | ||||||
|  |             if rate > (last_candle["close"] * (1 + 0.0025)): | ||||||
|  |                 return False | ||||||
|  |         else: | ||||||
|  |             if rate < (last_candle["close"] * (1 - 0.0025)): | ||||||
|  |                 return False | ||||||
|  |  | ||||||
|  |         return True | ||||||
| @@ -35,6 +35,7 @@ nav: | |||||||
|         - Edge Positioning: edge.md |         - Edge Positioning: edge.md | ||||||
|         - Advanced Strategy: strategy-advanced.md |         - Advanced Strategy: strategy-advanced.md | ||||||
|         - Advanced Hyperopt: advanced-hyperopt.md |         - Advanced Hyperopt: advanced-hyperopt.md | ||||||
|  |         - Freqai: freqai.md | ||||||
|         - Sandbox Testing: sandbox-testing.md |         - Sandbox Testing: sandbox-testing.md | ||||||
|     - FAQ: faq.md |     - FAQ: faq.md | ||||||
|     - SQL Cheat-sheet: sql_cheatsheet.md |     - SQL Cheat-sheet: sql_cheatsheet.md | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								requirements-freqai.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								requirements-freqai.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | # Include all requirements to run the bot. | ||||||
|  | -r requirements.txt | ||||||
|  |  | ||||||
|  | # Required for freqai | ||||||
|  | scikit-learn==1.0.2 | ||||||
|  | scikit-optimize==0.9.0 | ||||||
|  | joblib==1.1.0 | ||||||
|  | catboost==1.0.4 | ||||||
|  | lightgbm==3.3.2 | ||||||
							
								
								
									
										0
									
								
								user_data/freqaimodels/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								user_data/freqaimodels/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
		Reference in New Issue
	
	Block a user