AI_Strategy
♡
Basics
mode: spot
timeframe: 5m
interface version: 3
Settings
stoploss: -0.012
has minimal roi
process only new candles
startup candle count: 200
Indicators
ADX
ATR
CCI
DEMA
EMA
MFI
OBV
PPO
ROC
RSI
SAR
SMA
TEMA
TRIX
UO
WMA
Williams_R
talib
Other
requests
15 related strategies (⧉ identical code, ≈ similar name)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | from freqtrade.strategy import IStrategy, Trade from pandas import DataFrame from typing import Dict import talib.abstract as ta import logging import requests import json class AI_Strategy(IStrategy): """AI-driven Freqtrade strategy.""" # Put open router API keys FROM DIFFERENT ACCOUNTS here (bypass daily ratelimit) API_KEYS = [ "", "", "", "", "", "", ] API_URL = "https://openrouter.ai/api/v1/chat/completions" API_MODEL = "deepseek/deepseek-chat-v3-0324:free" logger = logging.getLogger(__name__) logger.info("AI_Strategy loading...") INTERFACE_VERSION = 3 # Strategy interface version timeframe = '5m' # Timeframe for the strategy can_short = False # Can this strategy go short? minimal_roi = {"0": 0.02, "60": 0.01, "180": 0} # Exit immediately after X minutes with X% profit stoploss = -0.012 # Exit immediately with 1.2% loss trailing_stop = False # False = Fixed stop loss process_only_new_candles = True # Process only new candles startup_candle_count = 200 # Number of candles the strategy requires before producing valid signals buy_at_confidence = 0.8 # Buy at AI confidence level order_types = { 'entry': 'market', # Market for immediate execution 'exit': 'market', # Market for sells (lock in profits) 'stoploss': 'market', 'stoploss_on_exchange': False } # Variables for AI current_api_key_index = 0 crypto_data = {} # Store the data for the AI free_usdt = 0 json_response = None # Remove empty API keys from the list API_KEYS = [key for key in API_KEYS if key != ""] def informative_pairs(self): """Get pair data for the strategy""" #Serve as Init function self.crypto_data = {} self.json_response = None # Get the whitelist pairs and return them return [(pair, self.timeframe) for pair in self.dp.current_whitelist()] def populate_indicators(self, dataframe: DataFrame, metadata: Dict) -> DataFrame: """Calculate indicators and collect the latest values for AI.""" if self.crypto_data != {}: return dataframe if len(self.API_KEYS) == 0: self.logger.error("You need at least one API key") return dataframe # Check if max open trades reached - more efficient way if len(Trade.get_trades_proxy(is_open=True)) >= self.config.get('max_open_trades', 3): return dataframe # Collect the data for the AI def get_value(indicator): if hasattr(indicator, 'empty') and hasattr(indicator, 'iloc'): value = indicator.iloc[-1] if not indicator.empty else None if value is not None and isinstance(value, (int, float)): rounded = round(value, 4) formatted = f"{rounded:.4f}".rstrip('0').rstrip('.') return formatted if formatted else "0" return value elif isinstance(indicator, (int, float)): rounded = round(indicator, 4) formatted = f"{rounded:.4f}".rstrip('0').rstrip('.') return formatted if formatted else "0" self.free_usdt = self.wallets.get_free('USDT') for pair in self.dp.current_whitelist(): alt_df = self.dp.get_pair_dataframe(pair, self.timeframe) base_currency = pair.split('/')[0] current_holdings = self.wallets.get_free(base_currency) # Collect the data for the AI self.crypto_data[pair] = { # Wallet information 'current_holdings': current_holdings, # Price information 'current_price': get_value(alt_df['close']), 'previous_price_5m': get_value(alt_df['close'].shift(1)), 'previous_price_10m': get_value(alt_df['close'].shift(2)), 'previous_price_15m': get_value(alt_df['close'].shift(3)), 'previous_price_30m': get_value(alt_df['close'].shift(6)), 'previous_price_45m': get_value(alt_df['close'].shift(9)), 'previous_price_1h': get_value(alt_df['close'].shift(12)), 'previous_price_2h': get_value(alt_df['close'].shift(24)), 'previous_price_3h': get_value(alt_df['close'].shift(36)), 'previous_price_6h': get_value(alt_df['close'].shift(72)), 'previous_price_8h': get_value(alt_df['close'].shift(96)), 'previous_price_12h': get_value(alt_df['close'].shift(144)), # Percentage changes 'change_15m': get_value(((alt_df['close'] - alt_df['close'].shift(3)) / alt_df['close'].shift(3)) * 100), 'change_30m': get_value(((alt_df['close'] - alt_df['close'].shift(6)) / alt_df['close'].shift(6)) * 100), 'change_45m': get_value(((alt_df['close'] - alt_df['close'].shift(9)) / alt_df['close'].shift(9)) * 100), 'change_1h': get_value(((alt_df['close'] - alt_df['close'].shift(12)) / alt_df['close'].shift(12)) * 100), 'change_2h': get_value(((alt_df['close'] - alt_df['close'].shift(24)) / alt_df['close'].shift(24)) * 100), 'change_3h': get_value(((alt_df['close'] - alt_df['close'].shift(36)) / alt_df['close'].shift(36)) * 100), 'change_12h': get_value(((alt_df['close'] - alt_df['close'].shift(144)) / alt_df['close'].shift(144)) * 100), 'last_available_max': alt_df['high'].max(), 'last_available_min': alt_df['low'].min(), 'current_to_max_ratio': get_value(alt_df['close'] / alt_df['high'].max()), 'current_to_min_ratio': get_value(alt_df['close'] / alt_df['low'].min()), # Momentum 'rsi': get_value(ta.RSI(alt_df, timeperiod=14)), 'willr': get_value(ta.WILLR(alt_df, timeperiod=14)), 'cci': get_value(ta.CCI(alt_df, timeperiod=14)), 'roc': get_value(ta.ROC(alt_df, timeperiod=10)), 'mom': get_value(ta.MOM(alt_df, timeperiod=10)), 'ultosc': get_value(ta.ULTOSC(alt_df)), 'adx': get_value(ta.ADX(alt_df, timeperiod=14)), 'apo': get_value(ta.APO(alt_df, fastperiod=12, slowperiod=26)), 'ppo': get_value(ta.PPO(alt_df, fastperiod=12, slowperiod=26)), 'bop': get_value(ta.BOP(alt_df)), # Volatility 'atr': get_value(ta.ATR(alt_df, timeperiod=14)), 'natr': get_value(ta.NATR(alt_df, timeperiod=14)), 'trange': get_value(ta.TRANGE(alt_df)), # Trend 'ema10': get_value(ta.EMA(alt_df, timeperiod=10)), 'ema20': get_value(ta.EMA(alt_df, timeperiod=20)), 'ema50': get_value(ta.EMA(alt_df, timeperiod=50)), 'ema100': get_value(ta.EMA(alt_df, timeperiod=100)), 'ema200': get_value(ta.EMA(alt_df, timeperiod=200)), 'sma10': get_value(ta.SMA(alt_df, timeperiod=10)), 'sma20': get_value(ta.SMA(alt_df, timeperiod=20)), 'sma50': get_value(ta.SMA(alt_df, timeperiod=50)), 'sma100': get_value(ta.SMA(alt_df, timeperiod=100)), 'sma200': get_value(ta.SMA(alt_df, timeperiod=200)), 'wma20': get_value(ta.WMA(alt_df, timeperiod=20)), 'dema20': get_value(ta.DEMA(alt_df, timeperiod=20)), 'tema20': get_value(ta.TEMA(alt_df, timeperiod=20)), 'trix': get_value(ta.TRIX(alt_df, timeperiod=15)), 'ht_trendline': get_value(ta.HT_TRENDLINE(alt_df)), 'sar': get_value(ta.SAR(alt_df)), # Volume 'obv': get_value(ta.OBV(alt_df, alt_df['close'])), 'adosc': get_value(ta.ADOSC(alt_df, fastperiod=3, slowperiod=10)), 'ad': get_value(ta.AD(alt_df)), 'mfi': get_value(ta.MFI(alt_df, timeperiod=14)), 'chaikin_ad': get_value(ta.AD(alt_df)), } #for key, value in self.crypto_data[pair].items(): #self.logger.info(f"{key}: {value}") # AI prompt = f""" You are a cryptocurrency trading AI. Analyze the following market data and provide trading recommendations. PORTFOLIO STATUS: - Free Capital: ${self.free_usdt:.2f} USDT MARKET DATA SUMMARY: """ # Add summary of each pair's data for pair, data in self.crypto_data.items(): prompt += f""" {pair}:""" for index, value in data.items(): prompt += f""" - {index}: {value}""" prompt += """ INSTRUCTIONS: Analyze the market data, Try to make the best buy decision for short term, The goal is +5% profit in the next hour, Provide a JSON response with the following structure: { "analysis": "Brief and short market analysis (1 line max)", "recommendations": [ { "pair": "BTC/USDT", "reason": "Short explanation for the recommendation (1 line max)", "suggested_amount_usdt": 100.0, "confidence": 0.85, }, ... ], } Focus on pairs with strong signals and provide specific, actionable recommend Analyse all data, and take the best trades possible, but remember to be very very safe Use these analysis parameters: - Consider technical indicators - Evaluate volume trends and support/resistance levels - Apply risk-adjusted position sizing - Use small parts of your capital - Keep it safe, don't be too aggressive, the goal is to win the maxium amount of trades with +5% profit in the next hour. """ # Ask AI def ask_AI(api_key, model): headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} payload = {"model": model, "messages": [{"role": "user", "content": prompt}]} try: self.logger.info("Sending request to AI...") response = requests.post(self.API_URL, headers=headers, json=payload, timeout=30) self.logger.info(f"Response status: {response.status_code}") if response.status_code == 200: response_data = response.json() if 'choices' in response_data and len(response_data['choices']) > 0: ai_content = response_data['choices'][0]['message']['content'] try: start_idx = ai_content.find('{') end_idx = ai_content.rfind('}') + 1 if start_idx != -1 and end_idx > start_idx: json_str = ai_content[start_idx:end_idx] self.json_response = json.loads(json_str) self.logger.info(f"AI Analysis: {self.json_response.get('analysis', 'No analysis provided')}") recommendations = self.json_response.get('recommendations', []) if recommendations: self.logger.info(f"AI Recommendations: {len(recommendations)} recommendations received") for rec in recommendations: self.logger.info(f" - {rec.get('pair', 'Unknown')}: {rec.get('reason', 'No reason')} (Confidence: {rec.get('confidence', 0)})") else: self.logger.info("No trading recommendations from AI") return False # Success - no need to change API key else: self.logger.warning("No JSON found in AI response") except json.JSONDecodeError as e: self.logger.error(f"Failed to parse JSON from AI response: {e}") else: self.logger.error("No choices in AI response") elif response.status_code == 429: self.logger.warning(f"Rate limit hit for API key {self.current_api_key_index + 1}") return True # Need to change API key elif response.status_code == 401: self.logger.warning(f"Authentication failed for API key {self.current_api_key_index + 1} (401 Unauthorized), you need to change key") return True # Need to change API key else: self.logger.error(f"Unexpected HTTP status: {response.status_code}") return True # Need to change API key except requests.exceptions.RequestException as e: self.logger.error(f"Request failed: {e}") except Exception as e: self.logger.error(f"Unexpected error: {e}") return True # Default to trying next key if any error occurred if self.current_api_key_index >= len(self.API_KEYS): self.current_api_key_index = 0 for index in range(self.current_api_key_index, len(self.API_KEYS)): self.current_api_key_index = index self.logger.info(f"Using API key: {self.current_api_key_index + 1}/{len(self.API_KEYS)}") if not ask_AI(self.API_KEYS[index], self.API_MODEL): # Success - no need to change API key) break if self.json_response is None: self.logger.warning(f"All {len(self.API_KEYS)} API keys hit rate limits or are not valid, skipping...") return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame: """Define entry signals""" if self.json_response is None: return dataframe recommendations = self.json_response.get('recommendations', []) pair_recommendation = None for rec in recommendations: if rec.get('pair') == metadata['pair']: pair_recommendation = rec break if pair_recommendation is None: return dataframe if pair_recommendation.get('confidence', 0) >= self.buy_at_confidence: dataframe.loc[dataframe.index[-1], 'buy'] = True self.logger.info(f"Buy signal set for {metadata['pair']} with confidence {pair_recommendation.get('confidence', 0)}") return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: Dict) -> DataFrame: return dataframe |
Strategy League — fixed backtest that feeds the ranking
No trades. This strategy never entered a position over the League window (33 pairs · 20210101-20260101), so there's nothing to backtest or rank — its entry conditions didn't trigger on the tested pairs and timeframe.
Backtests — over a market period
Backtest this strategy over a chosen crypto-cycle period. These don't affect the League ranking, and need that period's candle data downloaded.
Log in or sign up to run backtests.
| Period | Range | Total % | Win % | Max DD | Trades | |
|---|---|---|---|---|---|---|
| 2020 · DeFi Summer & Pre-Halving Rally | 20200101-20210101 | not run | ||||
| 2021 · Institutional Bull Market | 20210101-20220101 | not run | ||||
| 2022 · Post-Bull Crash & Macro Tightening | 20220101-20230101 | not run | ||||
| 2023–2024 · Recovery & ETF Anticipation | 20230101-20250101 | not run | ||||
| 2025–2026 · Current Cycle | 20250101-20260101 | not run | ||||
Walk forward
Out-of-sample backtest on recent data · 33 pairs · 20260101-20260701.
Backtest trust check
5 potential lookahead pattern(s) found · 1 warning(s) · 4 to review
| Line | Pattern | Detail | |
|---|---|---|---|
| 279 | review | slow_loop | a `for i in range(len(...))` pass over the dataframe runs in Python for every candle of every pair. Vectorise it, or the run is likely to hit the sandbox timeout before producing a result |
| 124 | leak | whole_series_reduction | .max() over the whole column sees future rows (use .rolling(window).max() for a causal value) |
| 125 | leak | whole_series_reduction | .min() over the whole column sees future rows (use .rolling(window).min() for a causal value) |
| 80 | leak | iloc_last | .iloc[-1] in populate_* applies the newest candle to all rows |
| 126 | leak | whole_series_reduction | .max() over the whole column sees future rows (use .rolling(window).max() for a causal value) |
| 127 | leak | whole_series_reduction | .min() over the whole column sees future rows (use .rolling(window).min() for a causal value) |
| 309 | realism | dead_v2_signal | writes the freqtrade v2 column 'buy' from a v3 populate_* method -- v3 reads enter_*/exit_* only, so this signal is dead code and never trades |
| 35 | review | startup_candles_too_small | startup_candle_count is 200, but EMA(timeperiod=200) needing 3x warmup needs at least 600 candles -- so the first 400+ candles of every backtest use an indicator that hasn't warmed up. Recursive indicators (EMA/RSI/ADX/ATR) want several times their period, not exactly it |
| 161 | review | unbounded_recursive_indicator | uses AD, OBV, which accumulates over the WHOLE series with no decay floor -- raising startup_candle_count doesn't converge it, so `freqtrade recursive-analysis` will keep flagging it no matter how large the warmup is. That's inherent to the indicator, not a misconfiguration; judge it on whether the strategy only ever compares it to itself (a running total drifting is fine) rather than to a fixed threshold (which it will eventually cross by drift alone) |
| 54 | review | unused_informative | informative_pairs() declares an extra timeframe, but nothing merges it into the dataframe (no merge_informative_pair, no @informative) -- that data is fetched and discarded, and any higher-timeframe filter you think is running isn't |
ran by Ron · took s
Lookahead analysis
freqtrade lookahead-analysis: detects strategies peeking at future candles.