--- /dev/null
+---
+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
--- /dev/null
+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
--- /dev/null
+{
+ "$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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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))