# AUTO-GENERATED by Auto-Quant Factory # Source strategy: AIStrategy # Do not edit manually — re-run Auto-Quant to regenerate. # MultiMa Strategy V2 # Author: @Mablue (Masoud Azizi) # github: https://github.com/mablue/ # --- Do not remove these libs --- from freqtrade.strategy import IntParameter, IStrategy from pandas import DataFrame from datetime import datetime # -------------------------------- # Add your lib to import here import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib from functools import reduce import pandas as pd class AIStrategy_Optimized(IStrategy): atr_dict = { "ARB/USDT": 0.01, "FIL/USDT": 0.01, "MANA/USDT": 0.01, } stability_dict = { "ARB/USDT": 100.0, "FIL/USDT": 100.0, "MANA/USDT": 100.0, } def custom_stoploss(self, pair: str, trade, current_time, current_rate: float, current_profit: float, after_fill: bool, **kwargs) -> float | None: """Three-tier aggressive trailing stoploss with profit lock-in. Tier 1: If profit >= 2%, lock stoploss at +0.5% Tier 2: If profit >= 4%, lock stoploss at +1.5% Tier 3: If profit >= 8%, lock stoploss at +3.0% """ from freqtrade.strategy import stoploss_from_open if current_profit >= 0.08: # 8% return stoploss_from_open(0.03, current_profit, is_short=trade.is_short, leverage=trade.leverage) or 1 if current_profit >= 0.04: # 4% return stoploss_from_open(0.015, current_profit, is_short=trade.is_short, leverage=trade.leverage) or 1 if current_profit >= 0.02: # 2% return stoploss_from_open(0.005, current_profit, is_short=trade.is_short, leverage=trade.leverage) or 1 return None # 111/2000: 18 trades. 12/4/2 Wins/Draws/Losses. Avg profit 9.72%. Median profit 3.01%. Total profit 733.01234143 USDT ( 73.30%). Avg duration 2 days, 18:40:00 min. Objective: 1.67048 INTERFACE_VERSION: int = 3 # Buy hyperspace params: buy_params = { "buy_ma_count": 4, "buy_ma_gap": 15, } # Sell hyperspace params: sell_params = { "sell_ma_count": 12, "sell_ma_gap": 68, } # ROI table: minimal_roi = { "0": 0.523, "1553": 0.123, "2332": 0.076, "3169": 0 } # Stoploss: stoploss = -0.30 # Trailing stop: trailing_stop = False # value loaded from strategy trailing_stop_positive = None # value loaded from strategy trailing_stop_positive_offset = 0.0 # value loaded from strategy trailing_only_offset_is_reached = False # value loaded from strategy # Optimal Timeframe timeframe = "5m" count_max = 20 gap_max = 100 buy_ma_count = IntParameter(1, count_max, default=7, space="buy") buy_ma_gap = IntParameter(1, gap_max, default=7, space="buy") sell_ma_count = IntParameter(1, count_max, default=7, space="sell") sell_ma_gap = IntParameter(1, gap_max, default=94, space="sell") def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: periods = set() for ma_count in range(1, int(self.buy_ma_count.value)): periods.add(ma_count * int(self.buy_ma_gap.value)) for ma_count in range(1, int(self.sell_ma_count.value)): periods.add(ma_count * int(self.sell_ma_gap.value)) periods = sorted([p for p in periods if p > 1]) new_cols = {} for p in periods: if p not in dataframe.columns: new_cols[p] = ta.TEMA(dataframe, timeperiod=int(p)) if new_cols: dataframe = pd.concat([dataframe, pd.DataFrame(new_cols)], axis=1) print(" ", metadata['pair'], end="\t\r") return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # I used range(self.buy_ma_count.value) instade of self.buy_ma_count.range # Cuz it returns range(7,8) but we need range(8) for all modes hyperopt, backtest and etc for ma_count in range(self.buy_ma_count.value): key = ma_count*self.buy_ma_gap.value past_key = (ma_count-1)*self.buy_ma_gap.value if past_key > 1 and key in dataframe.keys() and past_key in dataframe.keys(): conditions.append(dataframe[key] < dataframe[past_key]) 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: conditions = [] for ma_count in range(self.sell_ma_count.value): key = ma_count*self.sell_ma_gap.value past_key = (ma_count-1)*self.sell_ma_gap.value if past_key > 1 and key in dataframe.keys() and past_key in dataframe.keys(): conditions.append(dataframe[key] > dataframe[past_key]) if conditions: dataframe.loc[reduce(lambda x, y: x | y, conditions), "exit_long"] = 1 return dataframe def custom_stake_amount(self, pair: str, current_time, current_rate: float, proposed_stake: float, min_stake: float | None, max_stake: float, leverage: float, entry_tag: str | None, side: str, **kwargs) -> float: """Calculate position size based on ATR and stability score for dual-factor sizing. Formula: position_size = proposed_stake * (target_risk_pct / (atr / current_rate)) * (stability_score / 100) This method implements production-grade edge-case guards to prevent exchange execution errors: - Division-by-zero guard for ATR and current_rate - KeyError guard using .get() for dictionary access - Zero-stability fallback to min_stake """ target_risk_pct = 0.02 # 2% risk per trade # DIVISION-BY-ZERO GUARD: Check if atr or current_rate is invalid atr = self.atr_dict.get(pair, current_rate * 0.02) # fallback to 2% of price if atr <= 0 or current_rate <= 0: return min_stake if min_stake is not None else proposed_stake # Calculate ATR percentage atr_pct = atr / current_rate if atr_pct <= 0: return min_stake if min_stake is not None else proposed_stake # KEYERROR RUNTIME GUARD: Use .get() for stability_dict access stability_score = self.stability_dict.get(pair, 50.0) # fallback to 50% # Apply dual-factor sizing formula position_size = proposed_stake * (target_risk_pct / atr_pct) * (stability_score / 100.0) return position_size