From: Jérôme Benoit Date: Wed, 22 Jan 2025 10:21:46 +0000 (+0100) Subject: chore: initial quick adapter strategy v3 commit X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=19eaf995a96bc9b3057e776c29d1b70dbea25872;p=freqai-strategies.git chore: initial quick adapter strategy v3 commit Signed-off-by: Jérôme Benoit # nouveau fichier : quickadapter/user_data/data/.gitkeep --- 19eaf995a96bc9b3057e776c29d1b70dbea25872 diff --git a/quickadapter/docker-compose.yml b/quickadapter/docker-compose.yml new file mode 100644 index 0000000..6b32790 --- /dev/null +++ b/quickadapter/docker-compose.yml @@ -0,0 +1,35 @@ +--- +services: + freqtrade: + # image: freqtradeorg/freqtrade:stable_freqairl + # # Enable GPU Image and GPU Resources + # # Make sure to uncomment the whole deploy section + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] + + # Build step - only needed when additional dependencies are needed + build: + context: . + dockerfile: "./docker/Dockerfile.custom" + restart: unless-stopped + container_name: freqtrade-quickadapter + volumes: + - "./user_data:/freqtrade/user_data" + # Expose api on port 8081 + # Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation + # for more information. + ports: + - "0.0.0.0:8081:8080" + # Default command used when running `docker compose up` + command: > + trade + --logfile /freqtrade/user_data/logs/freqtrade-quickadapter.log + --db-url sqlite:////freqtrade/user_data/freqtrade-quickadapter-tradesv3.sqlite + --config /freqtrade/user_data/config.json + --freqaimodel XGBoostRegressorQuickAdapterV35 + --strategy QuickAdapterV3 diff --git a/quickadapter/docker/Dockerfile.custom b/quickadapter/docker/Dockerfile.custom new file mode 100644 index 0000000..b25bb43 --- /dev/null +++ b/quickadapter/docker/Dockerfile.custom @@ -0,0 +1,11 @@ +FROM freqtradeorg/freqtrade:stable_freqairl + +# Switch user to root if you must install something from apt +# Don't forget to switch the user back below! +# USER root + +# The below dependency - pyti - serves as an example. Please use whatever you need! +RUN pip install --user optuna + +# Switch back to user (only if you required root above) +# USER ftuser \ No newline at end of file diff --git a/quickadapter/user_data/backtest_results/.gitkeep b/quickadapter/user_data/backtest_results/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quickadapter/user_data/config-template.json b/quickadapter/user_data/config-template.json new file mode 100644 index 0000000..8acbc1f --- /dev/null +++ b/quickadapter/user_data/config-template.json @@ -0,0 +1,191 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "max_open_trades": 10, + "stake_currency": "USDT", + "stake_amount": "unlimited", + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "dry_run": true, + "dry_run_wallet": 1000, + "cancel_open_orders_on_exit": false, + // "trading_mode": "futures", + // "margin_mode": "isolated", + "trading_mode": "spot", + "unfilledtimeout": { + "entry": 10, + "exit": 10, + "exit_timeout_count": 0, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "other", + "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, + "price_last_balance": 0.0 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "walletAddress": "", + "privateKey": "", + "ccxt_config": { + "enableRateLimit": true, + "rateLimit": 60 + }, + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 60 + }, + // Spot top 5 + "pair_whitelist": [ + "BTC/USDT", + "ETH/USDT", + "SOL/USDT", + "BNB/USDT", + "XRP/USDT" + ], + // // Spot IA + // "pair_whitelist": [ + // "NEAR/USDT", + // "ICP/USDT", + // "RENDER/USDT", + // "TAO/USDT", + // "FET/USDT" + // ], + // // Spot restaking + // "pair_whitelist": [ + // "PENDLE/USDT", + // "EIGEN/USDT", + // "ETHFI/USDT" + // ], + // // Spot meme + // "pair_whitelist": [ + // "DOGE/USDT", + // "PENGU/USDT", + // "SHIB/USDT", + // "PEPE/USDT", + // "BONK/USDT" + // ], + "pair_blacklist": [ + // Exchange + "(1000.*).*/.*", + // Leverage + ".*(_PREMIUM|BEAR|BULL|HALF|HEDGE|UP|DOWN|[1235][SL])/.*", + // Fiat + "(ARS|AUD|BIDR|BRZ|BRL|CAD|CHF|EUR|GBP|HKD|IDRT|JPY|NGN|PLN|RON|RUB|SGD|TRY|UAH|USD|ZAR)/.*", + // Stable + "(AEUR|FDUSD|BUSD|CUSD|CUSDT|DAI|PAXG|SUSD|TUSD|USDC|USDN|USDP|USDT|VAI|UST|USTC|AUSD)/.*", + // FAN + "(ACM|AFA|ALA|ALL|ALPINE|APL|ASR|ATM|BAR|CAI|CHZ|CITY|FOR|GAL|GOZ|IBFK|JUV|LEG|LOCK-1|NAVI|NMR|NOV|PFL|PSG|ROUSH|STV|TH|TRA|UCH|UFC|YBO)/.*", + // Others + "(1EARTH|ILA|BOBA|CWAR|OMG|DMTR|MLS|TORN|LUNA|BTS|QKC|ACA|FTT|SRM|YFII|SNM|ANC|AION|MIR|WABI|QLC|NEBL|AUTO|VGX|DREP|PNT|PERL|LOOM|ID|NULS|TOMO|WTC|1000SATS|ORDI|XMR|ANT|MULTI|VAI|DREP|MOB|PNT|BTCDOM|WAVES|WNXM|XEM|ZEC|ELF|ARK|MDX|BETA|KP3R|AKRO|AMB|BOND|FIRO|OAX|EPX|OOKI|ONDO|MAGA|MAGAETH|TREMP|BODEN|STRUMP|TOOKER|TMANIA|BOBBY|BABYTRUMP|PTTRUMP|DTI|TRUMPIE|MAGAPEPE|PEPEMAGA|HARD|MBL|GAL|DOCK|POLS|CTXC|JASMY|BAL|SNT|CREAM|REN|LINA|REEF|UNFI|IRIS|CVP|GFT|KEY|WRX|BLZ|DAR|TROY|STMX|FTM|URO|FRED)/.*" + ] + }, + "pairlists": [ + { + "method": "StaticPairList" + }, + { + "method": "VolumePairList", + "number_assets": 10, + "sort_key": "quoteVolume", + "refresh_period": 1800 + } + ], + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": false, + "jwt_secret_key": "", + "ws_token": "", + "CORS_origins": [], + "username": "freqtrader", + "password": "freqtrader" + }, + "freqai": { + "enabled": true, + "conv_width": 1, + "purge_old_models": 2, + "expiration_hours": 12, + "train_period_days": 14, + "backtest_period_days": 2, + "write_metrics_to_disk": false, + "identifier": "quickadapter-xgboost", + "fit_live_predictions_candles": 600, + "track_performance": false, + "weibull_outlier_threshold": 0.999, + "optuna_hyperopt": false, + "extra_returns_per_train": { + "DI_value_param1": 0, + "DI_value_param2": 0, + "DI_value_param3": 0, + "DI_cutoff": 2, + "&s-minima_sort_threshold": -2, + "&s-maxima_sort_threshold": 2 + }, + "feature_parameters": { + "include_corr_pairlist": [ + "BTC/USDT", + "ETH/USDT" + ], + "include_timeframes": [ + "5m", + "15m", + "1h", + "4h" + ], + "label_period_candles": 100, + "include_shifted_candles": 6, + "DI_threshold": 10, + "weight_factor": 0.9, + "principal_component_analysis": false, + "use_SVM_to_remove_outliers": false, + "use_DBSCAN_to_remove_outliers": false, + "indicator_periods_candles": [ + 8, + 16, + 32 + ], + "inlier_metric_window": 0, + "noise_standard_deviation": 0.02, + "reverse_test_train_order": false, + "plot_feature_importances": 10, + "buffer_train_data_candles": 100 + }, + "data_split_parameters": { + "test_size": 0, + "random_state": 1, + "shuffle": false + }, + "model_training_parameters": { + // "device": "gpu", + // "use_rmm:": true, + "verbosity": 1 + } + }, + "bot_name": "freqtrade-quickadapter", + "initial_state": "running", + "timeframe": "5m", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} \ No newline at end of file diff --git a/quickadapter/user_data/data/.gitkeep b/quickadapter/user_data/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quickadapter/user_data/freqaimodels/XGBoostRegressorQuickAdapterV3.py b/quickadapter/user_data/freqaimodels/XGBoostRegressorQuickAdapterV3.py new file mode 100644 index 0000000..fc56fd4 --- /dev/null +++ b/quickadapter/user_data/freqaimodels/XGBoostRegressorQuickAdapterV3.py @@ -0,0 +1,147 @@ +import logging +from typing import Any, Dict, Tuple + +from xgboost import XGBRegressor +import time +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +import pandas as pd +import scipy as spy +import numpy.typing as npt +from pandas import DataFrame +import numpy as np + +import warnings + +warnings.simplefilter(action="ignore", category=FutureWarning) + +logger = logging.getLogger(__name__) + + +class XGBoostRegressorQuickAdapterV3(BaseRegressionModel): + """ + The following freqaimodel is released to sponsors of the non-profit FreqAI open-source project. + If you find the FreqAI project useful, please consider supporting it by becoming a sponsor. + We use sponsor money to help stimulate new features and to pay for running these public + experiments, with a an objective of helping the community make smarter choices in their + ML journey. + + This strategy is experimental (as with all strategies released to sponsors). Do *not* expect + returns. The goal is to demonstrate gratitude to people who support the project and to + help them find a good starting point for their own creativity. + + If you have questions, please direct them to our discord: https://discord.gg/xE4RMg4QYw + + https://github.com/sponsors/robcaulk + """ + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + + X = data_dictionary["train_features"] + y = data_dictionary["train_labels"] + + if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0: + eval_set = None + eval_weights = None + else: + eval_set = [ + (data_dictionary["test_features"], data_dictionary["test_labels"]) + ] + eval_weights = [data_dictionary["test_weights"]] + + sample_weight = data_dictionary["train_weights"] + + xgb_model = self.get_init_model(dk.pair) + + model = XGBRegressor(**self.model_training_parameters) + + start = time.time() + model.fit( + X=X, + y=y, + sample_weight=sample_weight, + eval_set=eval_set, + sample_weight_eval_set=eval_weights, + xgb_model=xgb_model, + ) + time_spent = time.time() - start + self.dd.update_metric_tracker("fit_time", time_spent, dk.pair) + + return model + + def fit_live_predictions(self, dk: FreqaiDataKitchen, pair: str) -> None: + warmed_up = True + + num_candles = self.freqai_info.get("fit_live_predictions_candles", 100) + if self.live: + if not hasattr(self, "exchange_candles"): + self.exchange_candles = len(self.dd.model_return_values[pair].index) + candle_diff = len(self.dd.historic_predictions[pair].index) - ( + num_candles + self.exchange_candles + ) + if candle_diff < 0: + logger.warning( + f"Fit live predictions not warmed up yet. Still {abs(candle_diff)} candles to go" + ) + warmed_up = False + + pred_df_full = ( + self.dd.historic_predictions[pair].tail(num_candles).reset_index(drop=True) + ) + pred_df_sorted = pd.DataFrame() + for label in pred_df_full.keys(): + if pred_df_full[label].dtype == object: + continue + pred_df_sorted[label] = pred_df_full[label] + + # pred_df_sorted = pred_df_sorted + for col in pred_df_sorted: + pred_df_sorted[col] = pred_df_sorted[col].sort_values( + ascending=False, ignore_index=True + ) + frequency = num_candles / ( + self.freqai_info["feature_parameters"]["label_period_candles"] * 2 + ) + max_pred = pred_df_sorted.iloc[: int(frequency)].mean() + min_pred = pred_df_sorted.iloc[-int(frequency) :].mean() + + if not warmed_up: + dk.data["extra_returns_per_train"]["&s-maxima_sort_threshold"] = 2 + dk.data["extra_returns_per_train"]["&s-minima_sort_threshold"] = -2 + else: + dk.data["extra_returns_per_train"]["&s-maxima_sort_threshold"] = max_pred[ + "&s-extrema" + ] + dk.data["extra_returns_per_train"]["&s-minima_sort_threshold"] = min_pred[ + "&s-extrema" + ] + + dk.data["labels_mean"], dk.data["labels_std"] = {}, {} + for ft in dk.label_list: + # f = spy.stats.norm.fit(pred_df_full[ft]) + dk.data["labels_std"][ft] = 0 # f[1] + dk.data["labels_mean"][ft] = 0 # f[0] + + # fit the DI_threshold + if not warmed_up: + f = [0, 0, 0] + cutoff = 2 + else: + di_values = pd.to_numeric(pred_df_full["DI_values"], errors="coerce") + di_values = di_values.dropna() + f = spy.stats.weibull_min.fit(di_values) + cutoff = spy.stats.weibull_min.ppf( + self.freqai_info.get("weibull_outlier_threshold", 0.999), *f + ) + + dk.data["DI_value_mean"] = pred_df_full["DI_values"].mean() + dk.data["DI_value_std"] = pred_df_full["DI_values"].std() + dk.data["extra_returns_per_train"]["DI_value_param1"] = f[0] + dk.data["extra_returns_per_train"]["DI_value_param2"] = f[1] + dk.data["extra_returns_per_train"]["DI_value_param3"] = f[2] + dk.data["extra_returns_per_train"]["DI_cutoff"] = cutoff diff --git a/quickadapter/user_data/freqaimodels/XGBoostRegressorQuickAdapterV35.py b/quickadapter/user_data/freqaimodels/XGBoostRegressorQuickAdapterV35.py new file mode 100644 index 0000000..9955672 --- /dev/null +++ b/quickadapter/user_data/freqaimodels/XGBoostRegressorQuickAdapterV35.py @@ -0,0 +1,193 @@ +import logging +from typing import Any, Dict, Tuple + +from xgboost import XGBRegressor +import time +from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +import pandas as pd +import scipy as spy +import optuna +import sklearn + +N_TRIALS = 26 + +import warnings + +warnings.simplefilter(action="ignore", category=FutureWarning) + +logger = logging.getLogger(__name__) + + +class XGBoostRegressorQuickAdapterV35(BaseRegressionModel): + """ + The following freqaimodel is released to sponsors of the non-profit FreqAI open-source project. + If you find the FreqAI project useful, please consider supporting it by becoming a sponsor. + We use sponsor money to help stimulate new features and to pay for running these public + experiments, with a an objective of helping the community make smarter choices in their + ML journey. + + This strategy is experimental (as with all strategies released to sponsors). Do *not* expect + returns. The goal is to demonstrate gratitude to people who support the project and to + help them find a good starting point for their own creativity. + + If you have questions, please direct them to our discord: https://discord.gg/xE4RMg4QYw + + https://github.com/sponsors/robcaulk + """ + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + """ + + X = data_dictionary["train_features"] + y = data_dictionary["train_labels"] + + if self.freqai_info.get("data_split_parameters", {}).get("test_size", 0.1) == 0: + eval_set = None + eval_weights = None + else: + eval_set = [ + (data_dictionary["test_features"], data_dictionary["test_labels"]) + ] + eval_weights = [data_dictionary["test_weights"]] + + sample_weight = data_dictionary["train_weights"] + + xgb_model = self.get_init_model(dk.pair) + start = time.time() + hp = {} + if self.freqai_info.get("optuna_hyperopt", False): + study = optuna.create_study(direction="minimize") + study.optimize( + lambda trial: objective( + trial, + X, + y, + sample_weight, + data_dictionary["test_features"], + data_dictionary["test_labels"], + self.model_training_parameters, + ), + n_trials=N_TRIALS, + n_jobs=1, + ) + + # display params + hp = study.best_params + # trial = study.best_trial + for key, value in hp.items(): + logger.debug(f"Optuna {key:>20s} : {value}") + logger.info(f"Optuna {'best objective value':>20s} : {study.best_value}") + + window = hp.get("train_period_candles", 4032) + X = X.tail(window) + y = y.tail(window) + sample_weight = sample_weight[-window:] + model = XGBRegressor(**self.model_training_parameters) + + model.fit( + X=X, + y=y, + sample_weight=sample_weight, + eval_set=eval_set, + sample_weight_eval_set=eval_weights, + xgb_model=xgb_model, + ) + time_spent = time.time() - start + self.dd.update_metric_tracker("fit_time", time_spent, dk.pair) + + return model + + def fit_live_predictions(self, dk: FreqaiDataKitchen, pair: str) -> None: + warmed_up = True + + num_candles = self.freqai_info.get("fit_live_predictions_candles", 100) + if self.live: + if not hasattr(self, "exchange_candles"): + self.exchange_candles = len(self.dd.model_return_values[pair].index) + candle_diff = len(self.dd.historic_predictions[pair].index) - ( + num_candles + self.exchange_candles + ) + if candle_diff < 0: + logger.warning( + f"Fit live predictions not warmed up yet. Still {abs(candle_diff)} candles to go" + ) + warmed_up = False + + pred_df_full = ( + self.dd.historic_predictions[pair].tail(num_candles).reset_index(drop=True) + ) + pred_df_sorted = pd.DataFrame() + for label in pred_df_full.keys(): + if pred_df_full[label].dtype == object: + continue + pred_df_sorted[label] = pred_df_full[label] + + # pred_df_sorted = pred_df_sorted + for col in pred_df_sorted: + pred_df_sorted[col] = pred_df_sorted[col].sort_values( + ascending=False, ignore_index=True + ) + frequency = num_candles / ( + self.freqai_info["feature_parameters"]["label_period_candles"] * 2 + ) + max_pred = pred_df_sorted.iloc[: int(frequency)].mean() + min_pred = pred_df_sorted.iloc[-int(frequency) :].mean() + + if not warmed_up: + dk.data["extra_returns_per_train"]["&s-maxima_sort_threshold"] = 2 + dk.data["extra_returns_per_train"]["&s-minima_sort_threshold"] = -2 + else: + dk.data["extra_returns_per_train"]["&s-maxima_sort_threshold"] = max_pred[ + "&s-extrema" + ] + dk.data["extra_returns_per_train"]["&s-minima_sort_threshold"] = min_pred[ + "&s-extrema" + ] + + dk.data["labels_mean"], dk.data["labels_std"] = {}, {} + for ft in dk.label_list: + # f = spy.stats.norm.fit(pred_df_full[ft]) + dk.data["labels_std"][ft] = 0 # f[1] + dk.data["labels_mean"][ft] = 0 # f[0] + + # fit the DI_threshold + if not warmed_up: + f = [0, 0, 0] + cutoff = 2 + else: + di_values = pd.to_numeric(pred_df_full["DI_values"], errors="coerce") + di_values = di_values.dropna() + f = spy.stats.weibull_min.fit(di_values) + cutoff = spy.stats.weibull_min.ppf( + self.freqai_info.get("weibull_outlier_threshold", 0.999), *f + ) + + dk.data["DI_value_mean"] = pred_df_full["DI_values"].mean() + dk.data["DI_value_std"] = pred_df_full["DI_values"].std() + dk.data["extra_returns_per_train"]["DI_value_param1"] = f[0] + dk.data["extra_returns_per_train"]["DI_value_param2"] = f[1] + dk.data["extra_returns_per_train"]["DI_value_param3"] = f[2] + dk.data["extra_returns_per_train"]["DI_cutoff"] = cutoff + + +def objective(trial, X, y, weights, X_test, y_test, params): + """Define the objective function""" + + window = trial.suggest_int("train_period_candles", 1152, 17280, step=600) + + # Fit the model + model = XGBRegressor(**params) + X = X.tail(window) + y = y.tail(window) + weights = weights[-window:] + model.fit(X, y, sample_weight=weights, eval_set=[(X_test, y_test)]) + y_pred = model.predict(X_test) + + error = sklearn.metrics.mean_squared_error(y_test, y_pred) + + return error diff --git a/quickadapter/user_data/hyperopt_results/.gitkeep b/quickadapter/user_data/hyperopt_results/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quickadapter/user_data/hyperopts/.gitkeep b/quickadapter/user_data/hyperopts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quickadapter/user_data/logs/.gitkeep b/quickadapter/user_data/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quickadapter/user_data/models/.gitkeep b/quickadapter/user_data/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quickadapter/user_data/notebooks/.gitkeep b/quickadapter/user_data/notebooks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quickadapter/user_data/plot/.gitkeep b/quickadapter/user_data/plot/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py new file mode 100644 index 0000000..2cb830e --- /dev/null +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -0,0 +1,417 @@ +import logging +from functools import reduce +import datetime +from datetime import timedelta +import talib.abstract as ta +from pandas import DataFrame, Series +from technical import qtpylib +from typing import Optional +from freqtrade.strategy.interface import IStrategy +from technical.pivots_points import pivots_points +from freqtrade.exchange import timeframe_to_prev_date +from freqtrade.persistence import Trade +from scipy.signal import argrelextrema +import numpy as np +import pandas_ta as pta + +logger = logging.getLogger(__name__) + + +class QuickAdapterV3(IStrategy): + """ + The following freqaimodel is released to sponsors of the non-profit FreqAI open-source project. + If you find the FreqAI project useful, please consider supporting it by becoming a sponsor. + We use sponsor money to help stimulate new features and to pay for running these public + experiments, with a an objective of helping the community make smarter choices in their + ML journey. + + This strategy is experimental (as with all strategies released to sponsors). Do *not* expect + returns. The goal is to demonstrate gratitude to people who support the project and to + help them find a good starting point for their own creativity. + + If you have questions, please direct them to our discord: https://discord.gg/xE4RMg4QYw + + https://github.com/sponsors/robcaulk + """ + + position_adjustment_enable = False + + # Attempts to handle large drops with DCA. High stoploss is required. + stoploss = -0.04 + + order_types = { + "entry": "limit", + "exit": "market", + "emergency_exit": "market", + "force_exit": "market", + "force_entry": "market", + "stoploss": "market", + "stoploss_on_exchange": False, + "stoploss_on_exchange_interval": 120, + } + + # Example specific variables + max_entry_position_adjustment = 1 + # This number is explained a bit further down + max_dca_multiplier = 2 + + minimal_roi = {"0": 0.03, "5000": -1} + + process_only_new_candles = True + + can_short = False + + plot_config = { + "main_plot": {}, + "subplots": { + "accuracy": {"accuracy_score": {"color": "#c28ce3", "type": "line"}}, + "extrema": { + "&s-extrema": {"color": "#f53580", "type": "line"}, + "&s-minima_sort_threshold": {"color": "#4ae747", "type": "line"}, + "&s-maxima_sort_threshold": {"color": "#5b5e4b", "type": "line"}, + }, + "min_max": { + "maxima": {"color": "#a29db9", "type": "line"}, + "minima": {"color": "#ac7fc", "type": "bar"}, + }, + }, + } + + @property + def protections(self): + return [ + {"method": "CooldownPeriod", "stop_duration_candles": 4}, + { + "method": "MaxDrawdown", + "lookback_period_candles": 48, + "trade_limit": 20, + "stop_duration_candles": 4, + "max_allowed_drawdown": 0.2, + }, + { + "method": "StoplossGuard", + "lookback_period_candles": 300, + "trade_limit": 1, + "stop_duration_candles": 300, + "only_per_pair": True, + }, + ] + + use_exit_signal = True + startup_candle_count: int = 80 + + # # Trailing stop: + # trailing_stop = True + # trailing_stop_positive = 0.01 + # trailing_stop_positive_offset = 0.025 + # trailing_only_offset_is_reached = True + + def feature_engineering_expand_all(self, dataframe, period, **kwargs): + dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) + dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period) + dataframe["%-adx-period"] = ta.ADX(dataframe, window=period) + dataframe["%-cci-period"] = ta.CCI(dataframe, timeperiod=period) + dataframe["%-er-period"] = pta.er(dataframe["close"], length=period) + dataframe["%-rocr-period"] = ta.ROCR(dataframe, timeperiod=period) + dataframe["%-trix-period"] = ta.TRIX(dataframe, timeperiod=period) + dataframe["%-cmf-period"] = chaikin_mf(dataframe, periods=period) + dataframe["%-tcp-period"] = top_percent_change(dataframe, period) + dataframe["%-cti-period"] = pta.cti(dataframe["close"], length=period) + dataframe["%-chop-period"] = qtpylib.chopiness(dataframe, period) + dataframe["%-linear-period"] = ta.LINEARREG_ANGLE( + dataframe["close"], timeperiod=period + ) + dataframe["%-atr-period"] = ta.ATR(dataframe, timeperiod=period) + dataframe["%-atr-periodp"] = ( + dataframe["%-atr-period"] / dataframe["close"] * 1000 + ) + return dataframe + + def feature_engineering_expand_basic(self, dataframe, **kwargs): + dataframe["%-pct-change"] = dataframe["close"].pct_change() + dataframe["%-raw_volume"] = dataframe["volume"] + dataframe["%-obv"] = ta.OBV(dataframe) + # Added + bollinger = qtpylib.bollinger_bands( + qtpylib.typical_price(dataframe), window=14, stds=2.2 + ) + dataframe["bb_lowerband"] = bollinger["lower"] + dataframe["bb_middleband"] = bollinger["mid"] + dataframe["bb_upperband"] = bollinger["upper"] + dataframe["%-bb_width"] = ( + dataframe["bb_upperband"] - dataframe["bb_lowerband"] + ) / dataframe["bb_middleband"] + dataframe["%-ibs"] = (dataframe["close"] - dataframe["low"]) / ( + dataframe["high"] - dataframe["low"] + ) + # dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50) + # dataframe["ema_12"] = ta.EMA(dataframe, timeperiod=12) + # dataframe["ema_26"] = ta.EMA(dataframe, timeperiod=26) + # dataframe["%-distema50"] = get_distance(dataframe["close"], dataframe["ema_50"]) + # dataframe["%-distema12"] = get_distance(dataframe["close"], dataframe["ema_12"]) + # dataframe["%-distema26"] = get_distance(dataframe["close"], dataframe["ema_26"]) + dataframe["zlema_50"] = pta.zlma(dataframe["close"], length=50, mamode="ema") + dataframe["zlema_12"] = pta.zlma(dataframe["close"], length=12, mamode="ema") + dataframe["zlema_26"] = pta.zlma(dataframe["close"], length=26, mamode="ema") + dataframe["%-distzlema50"] = get_distance( + dataframe["close"], dataframe["zlema_50"] + ) + dataframe["%-distzlema12"] = get_distance( + dataframe["close"], dataframe["zlema_12"] + ) + dataframe["%-distzlema26"] = get_distance( + dataframe["close"], dataframe["zlema_26"] + ) + macd = ta.MACD(dataframe) + dataframe["%-macd"] = macd["macd"] + dataframe["%-macdsignal"] = macd["macdsignal"] + dataframe["%-macdhist"] = macd["macdhist"] + dataframe["%-dist_to_macdsignal"] = get_distance( + dataframe["%-macd"], dataframe["%-macdsignal"] + ) + dataframe["%-dist_to_zerohist"] = get_distance(0, dataframe["%-macdhist"]) + # VWAP + vwap_low, vwap, vwap_high = VWAPB(dataframe, 20, 1) + dataframe["vwap_upperband"] = vwap_high + dataframe["vwap_middleband"] = vwap + dataframe["vwap_lowerband"] = vwap_low + dataframe["%-vwap_width"] = ( + (dataframe["vwap_upperband"] - dataframe["vwap_lowerband"]) + / dataframe["vwap_middleband"] + ) * 100 + dataframe = dataframe.copy() + dataframe["%-dist_to_vwap_upperband"] = get_distance( + dataframe["close"], dataframe["vwap_upperband"] + ) + dataframe["%-dist_to_vwap_middleband"] = get_distance( + dataframe["close"], dataframe["vwap_middleband"] + ) + dataframe["%-dist_to_vwap_lowerband"] = get_distance( + dataframe["close"], dataframe["vwap_lowerband"] + ) + dataframe["%-tail"] = (dataframe["close"] - dataframe["low"]).abs() + dataframe["%-wick"] = (dataframe["high"] - dataframe["close"]).abs() + pp = pivots_points(dataframe) + dataframe["pivot"] = pp["pivot"] + dataframe["r1"] = pp["r1"] + dataframe["s1"] = pp["s1"] + dataframe["r2"] = pp["r2"] + dataframe["s2"] = pp["s2"] + dataframe["r3"] = pp["r3"] + dataframe["s3"] = pp["s3"] + dataframe["%-dist_to_r1"] = get_distance(dataframe["close"], dataframe["r1"]) + dataframe["%-dist_to_r2"] = get_distance(dataframe["close"], dataframe["r2"]) + dataframe["%-dist_to_r3"] = get_distance(dataframe["close"], dataframe["r3"]) + dataframe["%-dist_to_s1"] = get_distance(dataframe["close"], dataframe["s1"]) + dataframe["%-dist_to_s2"] = get_distance(dataframe["close"], dataframe["s2"]) + dataframe["%-dist_to_s3"] = get_distance(dataframe["close"], dataframe["s3"]) + dataframe["%-raw_price"] = dataframe["close"] + dataframe["%-raw_open"] = dataframe["open"] + dataframe["%-raw_low"] = dataframe["low"] + dataframe["%-raw_high"] = dataframe["high"] + return dataframe + + def feature_engineering_standard(self, dataframe, **kwargs): + dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 + dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25 + return dataframe + + def set_freqai_targets(self, dataframe, **kwargs): + dataframe["&s-extrema"] = 0 + min_peaks = argrelextrema( + dataframe["low"].values, + np.less, + order=self.freqai_info["feature_parameters"]["label_period_candles"], + ) + max_peaks = argrelextrema( + dataframe["high"].values, + np.greater, + order=self.freqai_info["feature_parameters"]["label_period_candles"], + ) + for mp in min_peaks[0]: + dataframe.at[mp, "&s-extrema"] = -1 + for mp in max_peaks[0]: + dataframe.at[mp, "&s-extrema"] = 1 + dataframe["minima"] = np.where(dataframe["&s-extrema"] == -1, 1, 0) + dataframe["maxima"] = np.where(dataframe["&s-extrema"] == 1, 1, 0) + dataframe["&s-extrema"] = ( + dataframe["&s-extrema"] + .rolling(window=5, win_type="gaussian", center=True) + .mean(std=0.5) + ) + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe = self.freqai.start(dataframe, metadata, self) + + dataframe["DI_catch"] = np.where( + dataframe["DI_values"] > dataframe["DI_cutoff"], + 0, + 1, + ) + + dataframe["minima_sort_threshold"] = dataframe["&s-minima_sort_threshold"] + dataframe["maxima_sort_threshold"] = dataframe["&s-maxima_sort_threshold"] + return dataframe + + def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: + enter_long_conditions = [ + df["do_predict"] == 1, + df["DI_catch"] == 1, + df["&s-extrema"] < df["minima_sort_threshold"], + ] + + 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["DI_catch"] == 1, + df["&s-extrema"] > df["maxima_sort_threshold"], + ] + + 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: + return df + + def custom_exit( + self, + pair: str, + trade: Trade, + current_time: datetime, + current_rate: float, + current_profit: float, + **kwargs, + ): + dataframe, _ = self.dp.get_analyzed_dataframe( + pair=pair, timeframe=self.timeframe + ) + + last_candle = dataframe.iloc[-1].squeeze() + trade_date = timeframe_to_prev_date( + self.timeframe, + (trade.open_date_utc - timedelta(minutes=int(self.timeframe[:-1]))), + ) + trade_candle = dataframe.loc[(dataframe["date"] == trade_date)] + if trade_candle.empty: + return None + trade_candle = trade_candle.squeeze() + + entry_tag = trade.enter_tag + + trade_duration = (current_time - trade.open_date_utc).seconds / 60 + + if trade_duration > 1000: + return "trade expired" + + if last_candle["DI_catch"] == 0: + return "Outlier detected" + + if ( + last_candle["&s-extrema"] < last_candle["minima_sort_threshold"] + and entry_tag == "short" + ): + return "minima_detected_short" + + if ( + last_candle["&s-extrema"] > last_candle["maxima_sort_threshold"] + and entry_tag == "long" + ): + return "maxima_detected_long" + + def confirm_trade_entry( + self, + pair: str, + order_type: str, + amount: float, + rate: float, + time_in_force: str, + current_time: datetime, + entry_tag: Optional[str], + side: str, + **kwargs, + ) -> bool: + open_trades = Trade.get_trades(trade_filter=Trade.is_open.is_(True)) + + num_shorts, num_longs = 0, 0 + for trade in open_trades: + if "short" in trade.enter_tag: + num_shorts += 1 + elif "long" in trade.enter_tag: + num_longs += 1 + + if side == "long" and num_longs >= 5: + return False + + if side == "short" and num_shorts >= 5: + return False + + 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 + + +def top_percent_change(dataframe: DataFrame, length: int) -> float: + """ + Percentage change of the current close from the range maximum Open price + :param dataframe: DataFrame The original OHLC dataframe + :param length: int The length to look back + """ + if length == 0: + return (dataframe["open"] - dataframe["close"]) / dataframe["close"] + else: + return ( + dataframe["open"].rolling(length).max() - dataframe["close"] + ) / dataframe["close"] + + +def chaikin_mf(df, periods=20): + close = df["close"] + low = df["low"] + high = df["high"] + volume = df["volume"] + mfv = ((close - low) - (high - close)) / (high - low) + mfv = mfv.fillna(0.0) + mfv *= volume + cmf = mfv.rolling(periods).sum() / volume.rolling(periods).sum() + return Series(cmf, name="cmf") + + +# VWAP bands +def VWAPB(dataframe, window_size=20, num_of_std=1): + df = dataframe.copy() + df["vwap"] = qtpylib.rolling_vwap(df, window=window_size) + rolling_std = df["vwap"].rolling(window=window_size).std() + df["vwap_low"] = df["vwap"] - (rolling_std * num_of_std) + df["vwap_high"] = df["vwap"] + (rolling_std * num_of_std) + return df["vwap_low"], df["vwap"], df["vwap_high"] + + +def EWO(dataframe, sma1_length=5, sma2_length=35): + df = dataframe.copy() + sma1 = ta.EMA(df, timeperiod=sma1_length) + sma2 = ta.EMA(df, timeperiod=sma2_length) + smadif = (sma1 - sma2) / df["close"] * 100 + return smadif + + +def get_distance(p1, p2): + return abs((p1) - (p2))