""" LEA FreqAI Strategy - Base Implementation LSTM Ensemble Algorithmic Trading Strategy Based on: Deep Learning in Quantitative Trading (Zhang & Zohren, 2025) """ import logging from functools import reduce import numpy as np import pandas as pd from pandas import DataFrame import talib.abstract as ta from technical import qtpylib from freqtrade.strategy import IStrategy, merge_informative_pair logger = logging.getLogger(__name__) class LeaFreqAIStrategy(IStrategy): """ LEA Base Strategy with FreqAI LSTM predictions Features: - LSTM-based price prediction (via FreqAI) - Stationary feature engineering - Market regime detection - Risk-aware position management """ # Strategy metadata INTERFACE_VERSION = 3 can_short = False # Timeframe timeframe = "5m" # Startup candles needed for indicators startup_candle_count = 200 # ROI table - dynamic based on forecast minimal_roi = { "0": 0.10, # 10% if immediate "30": 0.05, # 5% after 30 min "60": 0.02, # 2% after 1 hour "120": 0.01 # 1% after 2 hours } # Stoploss stoploss = -0.15 # 15% hard stop # Trailing stop trailing_stop = True trailing_stop_positive = 0.01 # Activate at 1% profit trailing_stop_positive_offset = 0.02 # Trail when 2% profit trailing_only_offset_is_reached = True # Exit settings use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = False # Process only new candles process_only_new_candles = True # Optimal order types order_types = { "entry": "limit", "exit": "limit", "stoploss": "market", "stoploss_on_exchange": False } order_time_in_force = { "entry": "GTC", "exit": "GTC" } # Plot configuration @property def plot_config(self): return { "main_plot": { "ema_50": {"color": "blue"}, "ema_200": {"color": "orange"}, }, "subplots": { "RSI": { "rsi": {"color": "red"}, }, "MACD": { "macd": {"color": "blue"}, "macdsignal": {"color": "orange"}, }, "Predictions": { "&-prediction": {"color": "green"}, } } } def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame: """ Create stationary features for all timeframes """ # Price returns (stationary) dataframe[f"%ret_1"] = dataframe["close"].pct_change(1) dataframe[f"%ret_3"] = dataframe["close"].pct_change(3) dataframe[f"%ret_12"] = dataframe["close"].pct_change(12) # Volatility (ATR-based, relative) dataframe["atr14"] = ta.ATR(dataframe, timeperiod=14) dataframe[f"%atr14_rel"] = dataframe["atr14"] / dataframe["close"] # Range (stationary) dataframe[f"%rng_24"] = (dataframe["high"].rolling(24).max() - dataframe["low"].rolling(24).min()) / dataframe["close"] # Z-score (mean reversion indicator) returns = dataframe["close"].pct_change() dataframe[f"%z_48"] = (returns - returns.rolling(48).mean()) / returns.rolling(48).std() # Volume indicators dataframe[f"%vol_z_48"] = ((dataframe["volume"] - dataframe["volume"].rolling(48).mean()) / dataframe["volume"].rolling(48).std()) # RSI (momentum) dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) # MACD macd = ta.MACD(dataframe) dataframe["macd"] = macd["macd"] dataframe["macdsignal"] = macd["macdsignal"] dataframe["macdhist"] = macd["macdhist"] # Bollinger Bands bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=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"] # EMAs for trend dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50) dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200) return dataframe def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: """ Basic feature engineering for main timeframe """ dataframe = self.feature_engineering_expand_all(dataframe, period=1, metadata=metadata) return dataframe def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: """ Market regime features (BTC correlation) """ # Get BTC data for regime detection if self.dp: btc_dataframe = self.dp.get_pair_dataframe(pair="BTC/USDT", timeframe=self.timeframe) if not btc_dataframe.empty: # BTC trend strength btc_dataframe["btc_ema_50"] = ta.EMA(btc_dataframe, timeperiod=50) btc_dataframe["%btc_trend"] = (btc_dataframe["close"] - btc_dataframe["btc_ema_50"]) / btc_dataframe["btc_ema_50"] # Market volatility btc_dataframe["%market_vol"] = btc_dataframe["close"].pct_change().rolling(48).std() # Merge with main dataframe dataframe = merge_informative_pair(dataframe, btc_dataframe, self.timeframe, self.timeframe, ffill=True, suffix="_btc") return dataframe def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: """ Define the prediction target Target: Future return over next 12 candles (1 hour at 5m) """ dataframe["&-target"] = dataframe["close"].shift(-12).pct_change(12) return dataframe def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ FreqAI will populate predictions here """ # FreqAI will add the prediction column dataframe = self.freqai.start(dataframe, metadata, self) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Entry signals based on LSTM predictions + filters """ conditions = [] # Main signal: LSTM predicts positive return conditions.append(dataframe["&-prediction"] > 0.0) # Filter 1: Not overbought conditions.append(dataframe["rsi"] < 75) # Filter 2: Sufficient volume conditions.append(dataframe["volume"] > 0) # Filter 3: BTC not crashing (if available) if "%btc_trend_btc" in dataframe.columns: conditions.append(dataframe["%btc_trend_btc"] > -0.10) # Filter 4: Price above EMA 200 (trend filter) conditions.append(dataframe["close"] > dataframe["ema_200"]) # Combine all conditions if conditions: dataframe.loc[ reduce(lambda x, y: x & y, conditions), "enter_long" ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Exit signals based on LSTM predictions """ conditions = [] # Main signal: LSTM predicts negative return conditions.append(dataframe["&-prediction"] < 0.0) # Alternative: Extreme overbought conditions.append(dataframe["rsi"] > 85) # Combine with OR logic (exit if either condition) if conditions: dataframe.loc[ reduce(lambda x, y: x | y, conditions), "exit_long" ] = 1 return dataframe def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time, entry_tag, side: str, **kwargs) -> bool: """ Additional trade confirmation before entry """ dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) last_candle = dataframe.iloc[-1] # Require strong prediction confidence if last_candle["&-prediction"] < 0.005: # Less than 0.5% predicted return return False # Check volume is not too low if last_candle["volume"] < last_candle["volume"].rolling(20).mean() * 0.5: return False return True def custom_stake_amount(self, pair: str, current_time, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, entry_tag, side: str, **kwargs) -> float: """ Dynamic position sizing based on prediction confidence """ dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) last_candle = dataframe.iloc[-1] # Get prediction confidence prediction = last_candle["&-prediction"] # Scale stake by prediction magnitude (0.5x to 1.5x) confidence_multiplier = np.clip(1.0 + (prediction * 10), 0.5, 1.5) adjusted_stake = proposed_stake * confidence_multiplier # Ensure within limits return np.clip(adjusted_stake, min_stake, max_stake)