""" Freqtrade Execution Strategy for ads-anomaly-detection ====================================================== PURE EXECUTION LAYER - NO INTERNAL LOGIC This strategy acts as a minimal execution wrapper that: 1. Receives pre-tagged trading decisions from ads-anomaly-detection 2. Executes those decisions within Freqtrade's framework 3. Contains NO internal trading logic, indicators, or filters 4. All intelligence comes from the external strategy/memory system Trading Decision Sources: - Random trades (testing) - Test trades (validation) - Confirmed anomaly trades (production) Usage: Paper trading mode recommended for initial deployment """ import logging import redis import json from typing import Optional, Dict, Any from datetime import datetime, timezone from pandas import DataFrame # Note: Freqtrade imports will be available when running in Freqtrade environment # For development/testing, these imports may show as unresolved try: from freqtrade.strategy import IStrategy except ImportError: # Fallback for development environment class IStrategy: pass logger = logging.getLogger(__name__) class AdsExecutionStrategy(IStrategy): """ Minimal Freqtrade execution strategy for ads-anomaly-detection system. This strategy is intentionally thin - it's the trigger, not the brain. All trading logic resides in the ads-anomaly-detection memory system. """ # Strategy metadata INTERFACE_VERSION: int = 3 # Minimal timeframe - we don't analyze, just execute timeframe = '1m' # Minimal startup candles - we don't need history startup_candle_count: int = 1 # Risk management (external system should handle this, but Freqtrade requires it) stoploss = -0.10 # 10% safety net # No trailing stop - external system controls exits trailing_stop = False # ROI table - disabled, external system controls exits minimal_roi = { "0": 100 # Never sell based on ROI, external system decides } # External system connection redis_client = None def __init__(self, config: dict) -> None: super().__init__(config) self.setup_external_connection() def setup_external_connection(self): """Initialize connection to ads-anomaly-detection system via Redis""" try: self.redis_client = redis.Redis( host=self.config.get('redis_host', 'localhost'), port=self.config.get('redis_port', 6379), db=self.config.get('redis_db', 0), decode_responses=True ) # Test connection self.redis_client.ping() logger.info("✅ Connected to ads-anomaly-detection system via Redis") except Exception as e: logger.error(f"❌ Failed to connect to ads-anomaly-detection system: {e}") self.redis_client = None def get_external_signal(self, pair: str) -> Optional[Dict[str, Any]]: """ Retrieve trading signal from ads-anomaly-detection system Expected signal format: { "action": "buy" | "sell" | "hold", "pair": "BTC/USDT", "signal_type": "random" | "test" | "anomaly", "confidence": 0.0-1.0, "timestamp": "2025-08-06T00:30:00Z", "metadata": { "anomaly_severity": "critical", "detector": "statistical_detector", "reason": "spike_detected" } } """ if not self.redis_client: return None try: # Check for signals for this pair signal_key = f"freqtrade_signal:{pair}" signal_data = self.redis_client.get(signal_key) if signal_data and isinstance(signal_data, str): signal = json.loads(signal_data) # Remove consumed signal self.redis_client.delete(signal_key) logger.info(f"📡 Received external signal for {pair}: {signal}") return signal except Exception as e: logger.error(f"❌ Error retrieving external signal for {pair}: {e}") return None def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Minimal indicators - we don't analyze, just need basic data structure The external system provides all analysis """ # Only add timestamp for external system reference dataframe['timestamp'] = dataframe['date'].astype(str) # Add basic volume indicator for Freqtrade compatibility (simple moving average) dataframe['volume_sma'] = dataframe['volume'].rolling(window=1).mean() return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Entry logic - purely based on external signals from ads-anomaly-detection NO internal analysis, indicators, or filters """ pair = metadata['pair'] # Initialize entry signals dataframe['enter_long'] = False dataframe['enter_short'] = False dataframe['enter_tag'] = '' # Get external signal signal = self.get_external_signal(pair) if signal and signal.get('action') == 'buy': # Execute buy signal from external system current_idx = len(dataframe) - 1 dataframe.loc[current_idx, 'enter_long'] = True # Tag with signal type and metadata signal_type = signal.get('signal_type', 'unknown') confidence = signal.get('confidence', 0) tag = f"{signal_type}_buy_conf_{confidence:.2f}" dataframe.loc[current_idx, 'enter_tag'] = tag logger.info(f"🚀 EXECUTING BUY for {pair} - Signal: {signal_type} (confidence: {confidence})") # Log metadata for tracking metadata_info = signal.get('metadata', {}) if metadata_info: logger.info(f"📊 Trade metadata: {metadata_info}") return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Exit logic - purely based on external signals from ads-anomaly-detection NO internal analysis, indicators, or filters """ pair = metadata['pair'] # Initialize exit signals dataframe['exit_long'] = False dataframe['exit_short'] = False dataframe['exit_tag'] = '' # Get external signal signal = self.get_external_signal(pair) if signal and signal.get('action') == 'sell': # Execute sell signal from external system current_idx = len(dataframe) - 1 dataframe.loc[current_idx, 'exit_long'] = True # Tag with signal type and metadata signal_type = signal.get('signal_type', 'unknown') confidence = signal.get('confidence', 0) tag = f"{signal_type}_sell_conf_{confidence:.2f}" dataframe.loc[current_idx, 'exit_tag'] = tag logger.info(f"🛑 EXECUTING SELL for {pair} - Signal: {signal_type} (confidence: {confidence})") # Log metadata for tracking metadata_info = signal.get('metadata', {}) if metadata_info: logger.info(f"📊 Trade metadata: {metadata_info}") return dataframe 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: """ Final confirmation before trade execution Can be used by external system for last-minute validation """ logger.info(f"🔍 Trade confirmation requested:") logger.info(f" Pair: {pair}") logger.info(f" Side: {side}") logger.info(f" Amount: {amount}") logger.info(f" Rate: {rate}") logger.info(f" Tag: {entry_tag}") # Could check with external system for final confirmation here # For now, always confirm (external system already decided) return True def confirm_trade_exit(self, pair: str, trade, order_type: str, amount: float, rate: float, time_in_force: str, exit_reason: str, current_time: datetime, **kwargs) -> bool: """ Final confirmation before trade exit Can be used by external system for last-minute validation """ logger.info(f"🔍 Exit confirmation requested:") logger.info(f" Pair: {pair}") logger.info(f" Exit reason: {exit_reason}") logger.info(f" Amount: {amount}") logger.info(f" Rate: {rate}") # Could check with external system for final confirmation here # For now, always confirm (external system already decided) return True def custom_stoploss(self, pair: str, trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: """ Custom stoploss - can be controlled by external system """ if not self.redis_client: return self.stoploss try: # Check if external system wants to adjust stoploss stoploss_key = f"freqtrade_stoploss:{pair}" custom_sl = self.redis_client.get(stoploss_key) if custom_sl and isinstance(custom_sl, str): return float(custom_sl) except Exception as e: logger.error(f"❌ Error getting custom stoploss: {e}") return self.stoploss def check_entry_timeout(self, pair: str, trade, order, current_time: datetime, **kwargs) -> bool: """Entry timeout - external system can control this""" # Default timeout handling return False def check_exit_timeout(self, pair: str, trade, order, current_time: datetime, **kwargs) -> bool: """Exit timeout - external system can control this""" # Default timeout handling return False def informative_pairs(self): """No additional pairs needed - we're execution only""" return []