add spice_rack to FreqAI
This commit is contained in:
parent
075748b21a
commit
b209490009
@ -807,3 +807,85 @@ Code review, software architecture brainstorming:
|
|||||||
Beta testing and bug reporting:
|
Beta testing and bug reporting:
|
||||||
@bloodhunter4rc, Salah Lamkadem @ikonx, @ken11o2, @longyu, @paranoidandy, @smidelis, @smarm,
|
@bloodhunter4rc, Salah Lamkadem @ikonx, @ken11o2, @longyu, @paranoidandy, @smidelis, @smarm,
|
||||||
Juha Nykänen @suikula, Wagner Costa @wagnercosta
|
Juha Nykänen @suikula, Wagner Costa @wagnercosta
|
||||||
|
|
||||||
|
## Using the `spice_rack`
|
||||||
|
|
||||||
|
<!-- Dont forget this section during the doc reorg! -->
|
||||||
|
|
||||||
|
The `spice_rack` is aimed at users who do not wish to deal with setting up `FreqAI` confgs, but instead prefer to interact with `FreqAI` similar to a `talib` indicator. In this case, the user can instead simply add two keys to their config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"freqai_spice_rack": true,
|
||||||
|
"freqai_identifier": "spicey-id",
|
||||||
|
```
|
||||||
|
|
||||||
|
Which tells `FreqAI` to set up a pre-set `FreqAI` instance automatically under the hood with preset parameters. Now the user can access a suite of custom `FreqAI` supercharged indicators inside their strategy:
|
||||||
|
|
||||||
|
```python
|
||||||
|
dataframe['dissimilarity_index'] = self.freqai.spice_rack(
|
||||||
|
'DI_values', dataframe, metadata, self)
|
||||||
|
dataframe['maxima'] = self.freqai.spice_rack(
|
||||||
|
'&s-maxima', dataframe, metadata, self)
|
||||||
|
dataframe['minima'] = self.freqai.spice_rack(
|
||||||
|
'&s-minima', dataframe, metadata, self)
|
||||||
|
self.freqai.close_spice_rack() # user must close the spicerack
|
||||||
|
```
|
||||||
|
|
||||||
|
Users can then use these columns, concert with all their own additional indicators added to `populate_indicators` in their entry/exit criteria and strategy callback methods the same way as any typical indicator. For example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
|
df.loc[
|
||||||
|
(
|
||||||
|
(df['dissimilarity_index'] < 1) &
|
||||||
|
(df['minima'] > 0.1)
|
||||||
|
),
|
||||||
|
'enter_long'] = 1
|
||||||
|
|
||||||
|
df.loc[
|
||||||
|
(
|
||||||
|
(df['dissimilarity_index'] < 1) &
|
||||||
|
(df['maxima'] > 0.1)
|
||||||
|
),
|
||||||
|
'enter_short'] = 1
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
|
df.loc[
|
||||||
|
(
|
||||||
|
(df['dissimilarity_index'] < 1) &
|
||||||
|
(df['maxima'] > 0.1)
|
||||||
|
),
|
||||||
|
|
||||||
|
'exit_long'] = 1
|
||||||
|
|
||||||
|
df.loc[
|
||||||
|
(
|
||||||
|
|
||||||
|
(df['dissimilarity_index'] < 1) &
|
||||||
|
(df['minima'] > 0.1)
|
||||||
|
),
|
||||||
|
'exit_short'] = 1
|
||||||
|
|
||||||
|
return df
|
||||||
|
```
|
||||||
|
|
||||||
|
The user does need to ensure their `informative_pairs()` contains the following (users can add their own `informative_pair` needs to the bottom of this template):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def informative_pairs(self):
|
||||||
|
whitelist_pairs = self.dp.current_whitelist()
|
||||||
|
corr_pairs = self.config["freqai"]["feature_parameters"]["include_corr_pairlist"]
|
||||||
|
informative_pairs = []
|
||||||
|
for tf in self.config["freqai"]["feature_parameters"]["include_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
|
||||||
|
```
|
||||||
|
@ -490,7 +490,7 @@ class FreqaiDataDrawer:
|
|||||||
f"Unable to load model, ensure model exists at " f"{dk.data_path} "
|
f"Unable to load model, ensure model exists at " f"{dk.data_path} "
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]:
|
if self.config["freqai"]["feature_parameters"].get("principal_component_analysis", False):
|
||||||
dk.pca = cloudpickle.load(
|
dk.pca = cloudpickle.load(
|
||||||
open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "rb")
|
open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "rb")
|
||||||
)
|
)
|
||||||
|
@ -98,6 +98,7 @@ class FreqaiDataKitchen:
|
|||||||
self.train_dates: DataFrame = pd.DataFrame()
|
self.train_dates: DataFrame = pd.DataFrame()
|
||||||
self.unique_classes: Dict[str, list] = {}
|
self.unique_classes: Dict[str, list] = {}
|
||||||
self.unique_class_list: list = []
|
self.unique_class_list: list = []
|
||||||
|
self.spice_dataframe: DataFrame = None
|
||||||
|
|
||||||
def set_paths(
|
def set_paths(
|
||||||
self,
|
self,
|
||||||
@ -1267,3 +1268,11 @@ class FreqaiDataKitchen:
|
|||||||
f"Could not find backtesting prediction file at {path_to_predictionfile}"
|
f"Could not find backtesting prediction file at {path_to_predictionfile}"
|
||||||
)
|
)
|
||||||
return file_exists
|
return file_exists
|
||||||
|
|
||||||
|
def spice_extractor(self, indicator: str, dataframe: DataFrame) -> npt.NDArray:
|
||||||
|
if indicator in dataframe:
|
||||||
|
return np.array(dataframe[indicator])
|
||||||
|
else:
|
||||||
|
logger.warning(f'User asked spice_rack for {indicator}, '
|
||||||
|
f'but it is not available. Returning 0s')
|
||||||
|
return np.zeros(len(dataframe.index))
|
||||||
|
@ -89,7 +89,7 @@ class IFreqaiModel(ABC):
|
|||||||
self.begin_time_train: float = 0
|
self.begin_time_train: float = 0
|
||||||
self.base_tf_seconds = timeframe_to_seconds(self.config['timeframe'])
|
self.base_tf_seconds = timeframe_to_seconds(self.config['timeframe'])
|
||||||
self.continual_learning = self.freqai_info.get('continual_learning', False)
|
self.continual_learning = self.freqai_info.get('continual_learning', False)
|
||||||
|
self.spice_rack_open: bool = False
|
||||||
self._threads: List[threading.Thread] = []
|
self._threads: List[threading.Thread] = []
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
|
|
||||||
@ -138,7 +138,7 @@ class IFreqaiModel(ABC):
|
|||||||
dk = self.start_backtesting(dataframe, metadata, self.dk)
|
dk = self.start_backtesting(dataframe, metadata, self.dk)
|
||||||
|
|
||||||
dataframe = dk.remove_features_from_df(dk.return_dataframe)
|
dataframe = dk.remove_features_from_df(dk.return_dataframe)
|
||||||
self.clean_up()
|
# self.clean_up()
|
||||||
if self.live:
|
if self.live:
|
||||||
self.inference_timer('stop')
|
self.inference_timer('stop')
|
||||||
return dataframe
|
return dataframe
|
||||||
@ -685,6 +685,18 @@ class IFreqaiModel(ABC):
|
|||||||
|
|
||||||
return init_model
|
return init_model
|
||||||
|
|
||||||
|
def spice_rack(self, indicator: str, dataframe: DataFrame,
|
||||||
|
metadata: dict, strategy: IStrategy) -> NDArray:
|
||||||
|
if not self.spice_rack_open:
|
||||||
|
dataframe = self.start(dataframe, metadata, strategy)
|
||||||
|
self.dk.spice_dataframe = dataframe
|
||||||
|
self.spice_rack_open = True
|
||||||
|
return self.dk.spice_extractor(indicator, dataframe)
|
||||||
|
else:
|
||||||
|
return self.dk.spice_extractor(indicator, self.dk.spice_dataframe)
|
||||||
|
|
||||||
|
def close_spice_rack(self):
|
||||||
|
self.spice_rack_open = False
|
||||||
# Following methods which are overridden by user made prediction models.
|
# Following methods which are overridden by user made prediction models.
|
||||||
# See freqai/prediction_models/CatboostPredictionModel.py for an example.
|
# See freqai/prediction_models/CatboostPredictionModel.py for an example.
|
||||||
|
|
||||||
|
35
freqtrade/freqai/spice_rack/lightgbm_config.json
Normal file
35
freqtrade/freqai/spice_rack/lightgbm_config.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
"freqai": {
|
||||||
|
"enabled": true,
|
||||||
|
"purge_old_models": true,
|
||||||
|
"train_period_days": 4,
|
||||||
|
"identifier": "uniqe-id",
|
||||||
|
"feature_parameters": {
|
||||||
|
"include_timeframes": [
|
||||||
|
"3m",
|
||||||
|
"15m",
|
||||||
|
"1h"
|
||||||
|
],
|
||||||
|
"include_corr_pairlist": [
|
||||||
|
"BTC/USDT",
|
||||||
|
"ETH/USDT"
|
||||||
|
],
|
||||||
|
"label_period_candles": 20,
|
||||||
|
"include_shifted_candles": 2,
|
||||||
|
"DI_threshold": 0.9,
|
||||||
|
"weight_factor": 0.9,
|
||||||
|
"indicator_periods_candles": [
|
||||||
|
10,
|
||||||
|
20
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data_split_parameters": {
|
||||||
|
"test_size": 0,
|
||||||
|
"random_state": 1
|
||||||
|
},
|
||||||
|
"model_training_parameters": {
|
||||||
|
"n_estimators": 800
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
# for spice rack
|
||||||
|
import pandas as pd
|
||||||
|
import talib.abstract as ta
|
||||||
|
from scipy.signal import argrelextrema
|
||||||
|
from technical import qtpylib
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.data.history.history_utils import refresh_backtest_ohlcv_data
|
from freqtrade.data.history.history_utils import refresh_backtest_ohlcv_data
|
||||||
@ -8,6 +15,7 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
from freqtrade.exchange.exchange import market_is_active
|
from freqtrade.exchange.exchange import market_is_active
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist
|
||||||
|
from freqtrade.strategy import merge_informative_pair
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -85,6 +93,83 @@ def get_required_data_timerange(
|
|||||||
return data_load_timerange
|
return data_load_timerange
|
||||||
|
|
||||||
|
|
||||||
|
def auto_populate_any_indicators(
|
||||||
|
self, pair, df, tf, informative=None, set_generalized_indicators=False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This is a premade `populate_any_indicators()` function which is set in
|
||||||
|
the user strategy is they enable `freqai_spice_rack: true` in their
|
||||||
|
configuration file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
coin = pair.split('/')[0]
|
||||||
|
|
||||||
|
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_candles"]:
|
||||||
|
|
||||||
|
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, timeperiod=t)
|
||||||
|
informative[f"%-{coin}sma-period_{t}"] = ta.SMA(informative, timeperiod=t)
|
||||||
|
informative[f"%-{coin}ema-period_{t}"] = ta.EMA(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)
|
||||||
|
|
||||||
|
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"]["include_shifted_candles"] + 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)
|
||||||
|
if set_generalized_indicators:
|
||||||
|
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
|
||||||
|
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
|
||||||
|
df["&s-minima"] = 0
|
||||||
|
df["&s-maxima"] = 0
|
||||||
|
min_peaks = argrelextrema(df["close"].values, np.less, order=80)
|
||||||
|
max_peaks = argrelextrema(df["close"].values, np.greater, order=80)
|
||||||
|
for mp in min_peaks[0]:
|
||||||
|
df.at[mp, "&s-minima"] = 1
|
||||||
|
for mp in max_peaks[0]:
|
||||||
|
df.at[mp, "&s-maxima"] = 1
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
# Keep below for when we wish to download heterogeneously lengthed data for FreqAI.
|
# Keep below for when we wish to download heterogeneously lengthed data for FreqAI.
|
||||||
# def download_all_data_for_training(dp: DataProvider, config: dict) -> None:
|
# def download_all_data_for_training(dp: DataProvider, config: dict) -> None:
|
||||||
# """
|
# """
|
||||||
|
@ -5,7 +5,7 @@ This module defines the interface to apply for strategies
|
|||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Dict, List, Optional, Tuple, Union
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@ -145,12 +145,27 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
self._ft_informative.append((informative_data, cls_method))
|
self._ft_informative.append((informative_data, cls_method))
|
||||||
|
|
||||||
def load_freqAI_model(self) -> None:
|
def load_freqAI_model(self) -> None:
|
||||||
if self.config.get('freqai', {}).get('enabled', False):
|
spice_rack = self.config.get('freqai_spice_rack', False)
|
||||||
|
if self.config.get('freqai', {}).get('enabled', False) or spice_rack:
|
||||||
|
spice_rack = self.config.get('freqai_spice_rack', False)
|
||||||
|
if spice_rack:
|
||||||
|
self.config = self.setup_freqai_spice_rack(self.config)
|
||||||
# Import here to avoid importing this if freqAI is disabled
|
# Import here to avoid importing this if freqAI is disabled
|
||||||
from freqtrade.freqai.utils import download_all_data_for_training
|
from freqtrade.freqai.utils import download_all_data_for_training
|
||||||
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
||||||
self.freqai = FreqaiModelResolver.load_freqaimodel(self.config)
|
self.freqai = FreqaiModelResolver.load_freqaimodel(self.config)
|
||||||
self.freqai_info = self.config["freqai"]
|
|
||||||
|
if spice_rack:
|
||||||
|
import types
|
||||||
|
|
||||||
|
from freqtrade.freqai.utils import auto_populate_any_indicators
|
||||||
|
self.populate_any_indicators = types.MethodType( # type: ignore
|
||||||
|
auto_populate_any_indicators, self)
|
||||||
|
# funcType = type(IStrategy.populate_any_indicators)
|
||||||
|
# self.populate_any_indicators = funcType(self.freqai.auto_populate_any_indicators,
|
||||||
|
# self, self.populate_any_indicators)
|
||||||
|
|
||||||
|
self.freqai_info = self.config["freqai"]
|
||||||
|
|
||||||
# download the desired data in dry/live
|
# download the desired data in dry/live
|
||||||
if self.config.get('runmode') in (RunMode.DRY_RUN, RunMode.LIVE):
|
if self.config.get('runmode') in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
@ -160,6 +175,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"already on disk."
|
"already on disk."
|
||||||
)
|
)
|
||||||
download_all_data_for_training(self.dp, self.config)
|
download_all_data_for_training(self.dp, self.config)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Gracious failures if freqAI is disabled but "start" is called.
|
# Gracious failures if freqAI is disabled but "start" is called.
|
||||||
class DummyClass():
|
class DummyClass():
|
||||||
@ -173,6 +189,17 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
self.freqai = DummyClass() # type: ignore
|
self.freqai = DummyClass() # type: ignore
|
||||||
|
|
||||||
|
def setup_freqai_spice_rack(self, config: dict) -> Dict[str, Any]:
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
with open(Path('freqtrade') / 'freqai' / 'spice_rack'
|
||||||
|
/ 'lightgbm_config.json') as json_file:
|
||||||
|
freqai_config = json.load(json_file)
|
||||||
|
config['freqai'] = freqai_config['freqai']
|
||||||
|
config['freqai']['identifier'] = config['freqai_identifier']
|
||||||
|
config.update({"freqaimodel": 'LightGBMRegressorMultiTarget'})
|
||||||
|
return config
|
||||||
|
|
||||||
def ft_bot_start(self, **kwargs) -> None:
|
def ft_bot_start(self, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Strategy init - runs after dataprovider has been added.
|
Strategy init - runs after dataprovider has been added.
|
||||||
|
Loading…
Reference in New Issue
Block a user