""" LEA FreqAI Strategy Based on "Deep Learning in Quantitative Trading" by Zhang & Zohren (2025) LSTM-based cryptocurrency trading with: - Volatility targeting for position sizing - Market regime detection via BTC/ETH correlation - Walk-forward validation - Sharpe ratio optimization """ from freqtrade.strategy.interface import IStrategy from pandas import DataFrame import numpy as np import talib.abstract as ta class LeaFreqAIStrategy(IStrategy): """ LEA (LSTM Enhanced Alpha) Strategy with FreqAI integration. """ # Basic settings timeframe = "5m" can_short = False # ROI and stoploss minimal_roi = { "0": 10 # 1000% - effectively disabled, let FreqAI handle exits } stoploss = -1.0 # -100% - effectively disabled, let FreqAI handle stops # Trailing stop trailing_stop = False # Run once per candle process_only_new_candles = True # Startup candle count startup_candle_count = 300 # Order types order_types = { "entry": "limit", "exit": "limit", "stoploss": "market", "stoploss_on_exchange": False } # Informative pairs for market regime detection def informative_pairs(self): """ Use BTC and ETH as informative pairs for market regime detection. """ pairs = [ ("BTC/USDT", "1h"), ("ETH/USDT", "1h") ] return pairs def populate_indicators(self, df: DataFrame, metadata: dict) -> DataFrame: """ Generate features for FreqAI model. Based on paper's feature engineering approach. """ # ============================================ # Price-based features # ============================================ # Returns at multiple horizons df["%ret_1"] = df["close"].pct_change(1) df["%ret_3"] = df["close"].pct_change(3) df["%ret_6"] = df["close"].pct_change(6) df["%mom_12"] = df["close"].pct_change(12) df["%mom_24"] = df["close"].pct_change(24) # Volatility features rng = (df["high"] - df["low"]).replace(0, np.nan) df["%atr14_rel"] = rng.rolling(14).mean() / df["close"] df["%rng_24"] = (df["high"].rolling(24).max() - df["low"].rolling(24).min()) / df["close"] # Z-scores (mean reversion signals) roll = df["close"].rolling(48) df["%z_48"] = (df["close"] - roll.mean()) / (roll.std().replace(0, np.nan)) # Volume features df["%vol_z_48"] = (df["volume"] - df["volume"].rolling(48).mean()) / ( df["volume"].rolling(48).std().replace(0, np.nan) ) # ============================================ # Technical indicators # ============================================ # RSI df["%rsi"] = ta.RSI(df, timeperiod=14) / 100.0 # Normalize to 0-1 # MACD macd = ta.MACD(df) df["%macd"] = macd["macd"] / df["close"] df["%macd_signal"] = macd["macdsignal"] / df["close"] # Bollinger Bands bollinger = ta.BBANDS(df) df["%bb_upper"] = (bollinger["upperband"] - df["close"]) / df["close"] df["%bb_lower"] = (df["close"] - bollinger["lowerband"]) / df["close"] # ============================================ # Market regime features (BTC/ETH) # ============================================ # Get informative pairs inf_btc = self.dp.get_pair_dataframe(pair="BTC/USDT", timeframe="1h") inf_eth = self.dp.get_pair_dataframe(pair="ETH/USDT", timeframe="1h") if not inf_btc.empty and not inf_eth.empty: # BTC trend strength inf_btc["%btc_trend"] = inf_btc["close"] / inf_btc["close"].rolling(168).mean() - 1.0 # Market volatility (from BTC) btc_ret_1h = inf_btc["close"].pct_change() inf_btc["%market_vol"] = btc_ret_1h.rolling(48).std() # BTC dominance signal btc_mom = inf_btc["close"].pct_change(24) eth_mom = inf_eth["close"].pct_change(24) inf_btc["%btc_dom"] = btc_mom - eth_mom # Merge with main dataframe df = self.merge_informative_pair( base_df=df, informative_df=inf_btc, timeframe="1h", ffill=True, append_timeframe=True, prefix="mkt", columns=["%btc_trend", "%market_vol", "%btc_dom"], ) # ============================================ # Cleanup # ============================================ # Replace infinities with NaN df.replace([np.inf, -np.inf], np.nan, inplace=True) # Forward fill NaN values df.fillna(method="ffill", inplace=True) df.fillna(method="bfill", inplace=True) return df def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: """ Entry signal generation. In FreqAI mode, we typically let the model decide entries. Setting enter_long=1 allows the model to enter when conditions are favorable. """ df.loc[:, "enter_long"] = 1 return df def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: """ Exit signal generation. In FreqAI mode, we typically let the model decide exits. Setting exit_long=0 prevents premature exits. """ df.loc[:, "exit_long"] = 0 return df def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time, entry_tag, **kwargs) -> bool: """ Optional: Add additional entry confirmation logic here. """ return True def custom_exit(self, pair: str, trade, current_time, current_rate, current_profit, **kwargs): """ Optional: Custom exit logic. For now, let FreqAI handle exits. """ return None