]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
chore: initial quick adapter strategy v3 commit
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 22 Jan 2025 10:21:46 +0000 (11:21 +0100)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 22 Jan 2025 10:21:46 +0000 (11:21 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
  # nouveau fichier : quickadapter/user_data/data/.gitkeep

14 files changed:
quickadapter/docker-compose.yml [new file with mode: 0644]
quickadapter/docker/Dockerfile.custom [new file with mode: 0644]
quickadapter/user_data/backtest_results/.gitkeep [new file with mode: 0644]
quickadapter/user_data/config-template.json [new file with mode: 0644]
quickadapter/user_data/data/.gitkeep [new file with mode: 0644]
quickadapter/user_data/freqaimodels/XGBoostRegressorQuickAdapterV3.py [new file with mode: 0644]
quickadapter/user_data/freqaimodels/XGBoostRegressorQuickAdapterV35.py [new file with mode: 0644]
quickadapter/user_data/hyperopt_results/.gitkeep [new file with mode: 0644]
quickadapter/user_data/hyperopts/.gitkeep [new file with mode: 0644]
quickadapter/user_data/logs/.gitkeep [new file with mode: 0644]
quickadapter/user_data/models/.gitkeep [new file with mode: 0644]
quickadapter/user_data/notebooks/.gitkeep [new file with mode: 0644]
quickadapter/user_data/plot/.gitkeep [new file with mode: 0644]
quickadapter/user_data/strategies/QuickAdapterV3.py [new file with mode: 0644]

diff --git a/quickadapter/docker-compose.yml b/quickadapter/docker-compose.yml
new file mode 100644 (file)
index 0000000..6b32790
--- /dev/null
@@ -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 (file)
index 0000000..b25bb43
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/quickadapter/user_data/config-template.json b/quickadapter/user_data/config-template.json
new file mode 100644 (file)
index 0000000..8acbc1f
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/quickadapter/user_data/freqaimodels/XGBoostRegressorQuickAdapterV3.py b/quickadapter/user_data/freqaimodels/XGBoostRegressorQuickAdapterV3.py
new file mode 100644 (file)
index 0000000..fc56fd4
--- /dev/null
@@ -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 (file)
index 0000000..9955672
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/quickadapter/user_data/hyperopts/.gitkeep b/quickadapter/user_data/hyperopts/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quickadapter/user_data/logs/.gitkeep b/quickadapter/user_data/logs/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quickadapter/user_data/models/.gitkeep b/quickadapter/user_data/models/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quickadapter/user_data/notebooks/.gitkeep b/quickadapter/user_data/notebooks/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quickadapter/user_data/plot/.gitkeep b/quickadapter/user_data/plot/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py
new file mode 100644 (file)
index 0000000..2cb830e
--- /dev/null
@@ -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))