diff --git a/apiratelimiter.py b/apiratelimiter.py new file mode 100644 index 0000000..187adef --- /dev/null +++ b/apiratelimiter.py @@ -0,0 +1,21 @@ +import threading +import time + + +class APIRateLimiter: + """API rate limiter""" + + def __init__(self, calls_per_second=2): + self.min_interval = 1.0 / calls_per_second + self.last_call = {} + self.lock = threading.Lock() + + def wait(self, api_name): + """Wait until API can be called""" + with self.lock: + current_time = time.time() + if api_name in self.last_call: + elapsed = current_time - self.last_call[api_name] + if elapsed < self.min_interval: + time.sleep(self.min_interval - elapsed) + self.last_call[api_name] = current_time diff --git a/deepseekanalyzer.py b/deepseekanalyzer.py new file mode 100644 index 0000000..7786d51 --- /dev/null +++ b/deepseekanalyzer.py @@ -0,0 +1,516 @@ +import os + +class DeepSeekAnalyzer: + """DeepSeek AI Analyzer (KEN2.0 optimized)""" + + def __init__(self): + self.api_key = os.getenv('DEEPSEEK_API_KEY') + self.api_url = 'https://api.deepseek.com/v1/chat/completions' + + def analyze_market(self, symbol, market_data, technical_indicators): + """Use DeepSeek to analyze market (optimized version)""" + if not self.api_key: + logger.warning("DeepSeek API key not set, skipping AI analysis") + return None + + try: + # Prepare analysis data + latest = market_data.iloc[-1] + technical_summary = self._prepare_enhanced_technical_summary(technical_indicators, market_data) + + prompt = self._create_enhanced_analysis_prompt(symbol, latest, market_data, technical_summary) + response = self._call_deepseek_api(prompt) + + return self._parse_deepseek_response(response) + + except Exception as e: + logger.error(f"DeepSeek analysis error: {e}") + return None + + def _prepare_enhanced_technical_summary(self, indicators, market_data): + """Prepare enhanced technical indicator summary""" + summary = [] + + try: + # Price information + current_price = indicators.get('current_price', 0) + summary.append(f"Current price: ${current_price:.4f}") + + # Multi-period RSI + rsi_7 = indicators.get('rsi_7', 50) + rsi_14 = indicators.get('rsi', 50) + rsi_21 = indicators.get('rsi_21', 50) + if hasattr(rsi_7, '__len__'): + rsi_7, rsi_14, rsi_21 = rsi_7.iloc[-1], rsi_14.iloc[-1], rsi_21.iloc[-1] + + summary.append(f"RSI(7/14/21): {rsi_7:.1f}/{rsi_14:.1f}/{rsi_21:.1f}") + + # RSI status analysis + rsi_status = [] + for rsi_val, period in [(rsi_7, 7), (rsi_14, 14), (rsi_21, 21)]: + if rsi_val > 70: + rsi_status.append(f"RSI{period} overbought") + elif rsi_val < 30: + rsi_status.append(f"RSI{period} oversold") + if rsi_status: + summary.append(f"RSI status: {', '.join(rsi_status)}") + + # KDJ indicator + k, d, j = indicators.get('kdj', (50, 50, 50)) + if hasattr(k, '__len__'): + k, d, j = k.iloc[-1], d.iloc[-1], j.iloc[-1] + + summary.append(f"KDJ: K={k:.1f}, D={d:.1f}, J={j:.1f}") + if j > 80: + summary.append("KDJ status: Overbought area") + elif j < 20: + summary.append("KDJ status: Oversold area") + + # MACD indicator + macd_line, signal_line, histogram = indicators.get('macd', (0, 0, 0)) + if hasattr(macd_line, '__len__'): + macd_line, signal_line, histogram = macd_line.iloc[-1], signal_line.iloc[-1], histogram.iloc[-1] + + summary.append(f"MACD: line={macd_line:.4f}, signal={signal_line:.4f}, histogram={histogram:.4f}") + summary.append(f"MACD status: {'Golden cross' if macd_line > signal_line else 'Death cross'}") + + # Moving averages + sma_20 = indicators.get('sma_20', 0) + sma_50 = indicators.get('sma_50', 0) + sma_100 = indicators.get('sma_100', 0) + if hasattr(sma_20, '__len__'): + sma_20, sma_50, sma_100 = sma_20.iloc[-1], sma_50.iloc[-1], sma_100.iloc[-1] + + price_vs_ma = [] + if current_price > sma_20: price_vs_ma.append("Above 20-day MA") + else: price_vs_ma.append("Below 20-day MA") + if current_price > sma_50: price_vs_ma.append("Above 50-day MA") + else: price_vs_ma.append("Below 50-day MA") + + summary.append(f"Moving averages: 20-day=${sma_20:.2f}, 50-day=${sma_50:.2f}, 100-day=${sma_100:.2f}") + summary.append(f"Price position: {', '.join(price_vs_ma)}") + + # Bollinger Bands + upper, middle, lower = indicators.get('bollinger_2std', (0, 0, 0)) + if hasattr(upper, '__len__'): + upper, middle, lower = upper.iloc[-1], middle.iloc[-1], lower.iloc[-1] + + bb_position = (current_price - lower) / (upper - lower) * 100 if (upper - lower) > 0 else 50 + summary.append(f"Bollinger Band position: {bb_position:.1f}% (0%=lower band, 100%=upper band)") + + # Trend strength + trend_strength = indicators.get('trend_strength', 0) + summary.append(f"Trend strength: {trend_strength:.2f} (0-1, higher means stronger trend)") + + # Volatility + volatility = indicators.get('volatility', 0) * 100 # Convert to percentage + summary.append(f"Annualized volatility: {volatility:.1f}%") + + # ATR + atr = indicators.get('atr', 0) + if hasattr(atr, '__len__'): + atr = atr.iloc[-1] + atr_percent = (atr / current_price * 100) if current_price > 0 else 0 + summary.append(f"ATR: {atr:.4f} ({atr_percent:.2f}%)") + + # Support resistance information + sr_levels = TechnicalAnalyzer.calculate_support_resistance(market_data) + if sr_levels: + summary.append(f"Static support: ${sr_levels['static_support']:.4f}, resistance: ${sr_levels['static_resistance']:.4f}") + summary.append(f"Dynamic support: ${sr_levels['dynamic_support']:.4f}, resistance: ${sr_levels['dynamic_resistance']:.4f}") + summary.append(f"Relative resistance: {sr_levels['current_vs_resistance']:+.2f}%, relative support: {sr_levels['current_vs_support']:+.2f}%") + + return "\n".join(summary) + + except Exception as e: + logger.error(f"Error preparing technical summary: {e}") + return "Technical indicator calculation exception" + + def _create_enhanced_analysis_prompt(self, symbol, latest, market_data, technical_summary): + """Create enhanced analysis prompt""" + # Calculate price changes + price_change_24h = 0 + price_change_7d = 0 + + if len(market_data) >= 24: + price_change_24h = ((latest['close'] - market_data.iloc[-24]['close']) / market_data.iloc[-24]['close'] * 100) + + if len(market_data) >= 168: # 7 days data (hourly) + price_change_7d = ((latest['close'] - market_data.iloc[-168]['close']) / market_data.iloc[-168]['close'] * 100) + + # Calculate volume changes + volume_trend = "Rising" if latest['volume'] > market_data['volume'].mean() else "Falling" + + prompt = f""" +You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market. Please comprehensively analyze the {symbol} trading pair and execute trades. + +# Core Objective +**Maximize Sharpe Ratio** +Sharpe Ratio = Average Return / Return Volatility, which means: +Quality trades (high win rate, large profit/loss ratio) → Improve Sharpe +Stable returns, control drawdowns → Improve Sharpe +Frequent trading, small profits and losses → Increase volatility, severely reduce Sharpe + +【Market Overview】 +- Current price: ${latest['close']:.4f} +- 24-hour change: {price_change_24h:+.2f}% +- 7-day change: {price_change_7d:+.2f}% +- Volume: {latest['volume']:.0f} ({volume_trend}) +- Volume relative level: {'High' if latest['volume'] > market_data['volume'].quantile(0.7) else 'Normal' if latest['volume'] > market_data['volume'].quantile(0.3) else 'Low'} + +【Multi-dimensional Technical Analysis】 +{technical_summary} + +【Market Environment Assessment】 +Please consider comprehensively: +1. Trend direction and strength +2. Overbought/oversold status +3. Volume coordination +4. Volatility level +5. Support resistance positions +6. Multi-timeframe consistency + +【Risk Management Suggestions】 +- Suggested position: Adjust based on signal strength and volatility +- Stop-loss setting: Reference ATR and support levels +- Holding time: Based on trend strength + +【Output Format Requirements】 +Please reply strictly in the following JSON format: +{{ + "action": "BUY/SELL/HOLD", + "confidence": "HIGH/MEDIUM/LOW", + "position_size": "Suggested position ratio (0.1-0.5)", + "stop_loss": "Suggested stop-loss price or percentage", + "take_profit": "Suggested take-profit price or percentage", + "timeframe": "Suggested holding time (SHORT/MEDIUM/LONG)", + "reasoning": "Detailed analysis logic and risk explanation" +}} + +Please provide objective suggestions based on professional technical analysis and risk management principles. +""" + + return prompt + + def analyze_sell_proportion(self, symbol, position_info, market_analysis, current_price): + """Analyze sell proportion""" + if not self.api_key: + logger.warning("DeepSeek API key not set, using default sell proportion") + return self._get_fallback_sell_proportion(position_info, 'MEDIUM') + + try: + prompt = self._create_sell_proportion_prompt(symbol, position_info, market_analysis, current_price) + response = self._call_deepseek_api(prompt) + return self._parse_sell_proportion_response(response) + + except Exception as e: + logger.error(f"DeepSeek sell proportion analysis error: {e}") + return self._get_fallback_sell_proportion(position_info, 'MEDIUM') + + def _create_sell_proportion_prompt(self, symbol, position_info, market_analysis, current_price): + """Create sell proportion analysis prompt""" + + # Calculate profit situation + entry_price = position_info['entry_price'] + profit_ratio = (current_price - entry_price) / entry_price if entry_price > 0 else 0 + profit_status = "Profit" if profit_ratio > 0 else "Loss" + profit_amount = (current_price - entry_price) * position_info['base_amount'] + + # Calculate position ratio + position_ratio = position_info['position_ratio'] * 100 # Convert to percentage + + # Prepare technical indicator summary + technical_summary = self._prepare_enhanced_technical_summary( + market_analysis['technical_indicators'], + market_analysis['market_data'] + ) + + prompt = f""" +You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market. Analyze the {symbol} position situation and consider and execute sell decisions. + +【Current Position Situation】 +- Position quantity: {position_info['base_amount']:.8f} +- Average entry price: ${entry_price:.4f} +- Current price: ${current_price:.4f} +- Profit ratio: {profit_ratio:+.2%} ({profit_status}) +- Profit amount: ${profit_amount:+.2f} +- Position ratio: {position_ratio:.2f}% (percentage of total portfolio) +- Position value: ${position_info['position_value']:.2f} + +【Market Technical Analysis】 +{technical_summary} + +【Risk Situation】 +- Current price distance to stop-loss: {position_info['distance_to_stop_loss']:.2%} +- Current price distance to take-profit: {position_info['distance_to_take_profit']:.2%} +- Market volatility: {position_info['market_volatility']:.2%} +- Trend strength: {position_info.get('trend_strength', 0):.2f} + +【Sell Strategy Considerations】 +Please consider the following factors to determine sell proportion: +1. Profit ratio: Large profits can consider partial profit-taking, keep some position for higher gains +2. Position ratio: Overweight positions should reduce position to lower risk, light positions can consider holding +3. Technical signals: Strong bearish signals should increase sell proportion, bullish signals can reduce sell proportion +4. Market environment: High volatility markets should be more conservative, low volatility markets can be more aggressive +5. Risk control: Close to stop-loss should sell decisively, far from stop-loss can be more flexible + +【Position Management Principles】 +- Profit over 20%: Consider partial profit-taking (30-70%), lock in profits +- Profit 10-20%: Decide based on signal strength (20-50%) +- Small profit (0-10%): Mainly based on technical signals (0-30%) +- Small loss (0-5%): Based on risk control (50-80%) +- Large loss (>5%): Consider stop-loss or position reduction (80-100%) + +【Output Format Requirements】 +Please reply strictly in the following JSON format, only containing numeric ratio (0-1): +{{ + "sell_proportion": 0.75 +}} + +Explanation: +- 0.1 means sell 10% of position +- 0.5 means sell 50% of position +- 1.0 means sell all position +- 0.0 means don't sell (only in extremely bullish situations) + +Please provide reasonable sell proportion suggestions based on professional analysis and risk management. +""" + + return prompt + + def _parse_sell_proportion_response(self, response): + """Parse sell proportion response""" + try: + content = response['choices'][0]['message']['content'] + + # Try to parse JSON format + if '{' in content and '}' in content: + json_start = content.find('{') + json_end = content.rfind('}') + 1 + json_str = content[json_start:json_end] + + parsed = json.loads(json_str) + proportion = parsed.get('sell_proportion', 0.5) + + # Ensure proportion is within reasonable range + proportion = max(0.0, min(1.0, proportion)) + logger.info(f"DeepSeek suggested sell proportion: {proportion:.1%}") + return proportion + else: + # Text analysis: try to extract proportion from text + import re + patterns = [ + r'[\"\"]([\d.]+)%[\"\"]', # "50%" + r'sell\s*([\d.]+)%', # sell 50% + r'([\d.]+)%', # 50% + r'[\"\"]([\d.]+)[\"\"]', # "0.5" + ] + + for pattern in patterns: + match = re.search(pattern, content) + if match: + value = float(match.group(1)) + if value > 1: # If in percentage format + value = value / 100 + proportion = max(0.0, min(1.0, value)) + logger.info(f"Parsed sell proportion from text: {proportion:.1%}") + return proportion + + # Default value + logger.warning("Unable to parse sell proportion, using default 50%") + return 0.5 + + except Exception as e: + logger.error(f"Error parsing sell proportion response: {e}") + return 0.5 + + def _get_fallback_sell_proportion(self, position_info, decision_confidence): + """Fallback sell proportion decision (when DeepSeek is unavailable)""" + + profit_ratio = position_info['profit_ratio'] + position_ratio = position_info['position_ratio'] + market_volatility = position_info.get('market_volatility', 0) + trend_strength = position_info.get('trend_strength', 0) + + # Decision based on profit situation + if profit_ratio >= 0.20: # Profit over 20% + base_proportion = 0.6 if decision_confidence == 'HIGH' else 0.4 + elif profit_ratio >= 0.10: # Profit 10%-20% + base_proportion = 0.5 if decision_confidence == 'HIGH' else 0.3 + elif profit_ratio >= 0: # Profit 0%-10% + base_proportion = 0.4 if decision_confidence == 'HIGH' else 0.2 + elif profit_ratio >= -0.05: # Loss 0%-5% + base_proportion = 0.7 if decision_confidence == 'HIGH' else 0.5 + else: # Loss over 5% + base_proportion = 0.9 if decision_confidence == 'HIGH' else 0.7 + + # Adjustment based on position ratio + if position_ratio > 0.15: # Overweight position + base_proportion = min(1.0, base_proportion + 0.2) + elif position_ratio < 0.05: # Very light position + base_proportion = max(0.1, base_proportion - 0.1) + + # Adjustment based on market volatility + if market_volatility > 0.05: # High volatility market + base_proportion = min(1.0, base_proportion + 0.1) + elif market_volatility < 0.02: # Low volatility market + base_proportion = max(0.1, base_proportion - 0.1) + + # Adjustment based on trend strength + if trend_strength > 0.7: # Strong trend + base_proportion = max(0.1, base_proportion - 0.1) + elif trend_strength < 0.3: # Weak trend + base_proportion = min(1.0, base_proportion + 0.1) + + # Ensure proportion is within reasonable range + base_proportion = max(0.1, min(1.0, base_proportion)) + + logger.info(f"Fallback sell proportion decision: profit={profit_ratio:+.2%}, position ratio={position_ratio:.2%}, " + f"volatility={market_volatility:.2%}, trend strength={trend_strength:.2f}, final proportion={base_proportion:.1%}") + return base_proportion + + def _call_deepseek_api(self, prompt, max_retries=3): + """Call DeepSeek API (improved retry mechanism)""" + headers = { + 'Authorization': f'Bearer {self.api_key}', + 'Content-Type': 'application/json' + } + + data = { + 'model': 'deepseek-chat', + 'messages': [ + { + 'role': 'system', + 'content': 'You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market, pursuing stable and sustainable returns' + }, + { + 'role': 'user', + 'content': prompt + } + ], + 'temperature': 0.3, + 'max_tokens': 1000 + } + + for attempt in range(max_retries): + try: + response = requests.post( + self.api_url, + headers=headers, + json=data, + timeout=30 # 30 second timeout + ) + + if response.status_code == 200: + return response.json() + elif response.status_code == 429: # Rate limit + wait_time = 2 ** attempt # Exponential backoff + logger.warning(f"DeepSeek API rate limit, waiting {wait_time} seconds before retry") + time.sleep(wait_time) + continue + else: + logger.error(f"DeepSeek API call failed: {response.status_code} - {response.text}") + if attempt < max_retries - 1: + time.sleep(1) + continue + else: + raise Exception(f"API call failed: {response.status_code}") + + except requests.exceptions.Timeout: + logger.warning(f"DeepSeek API request timeout, attempt {attempt + 1}/{max_retries}") + if attempt < max_retries - 1: + time.sleep(1) + continue + else: + raise Exception("DeepSeek API request timeout") + except Exception as e: + logger.error(f"DeepSeek API call exception: {e}") + if attempt < max_retries - 1: + time.sleep(1) + continue + else: + raise + + def _parse_deepseek_response(self, response): + """Parse DeepSeek response - enhanced version""" + try: + content = response['choices'][0]['message']['content'].strip() + logger.debug(f"DeepSeek raw response: {content}") + + # Remove possible code block markers + if content.startswith('```json'): + content = content[7:] + if content.endswith('```'): + content = content[:-3] + content = content.strip() + + # Try to parse JSON format + if '{' in content and '}' in content: + json_start = content.find('{') + json_end = content.rfind('}') + 1 + json_str = content[json_start:json_end] + + try: + parsed = json.loads(json_str) + result = { + 'action': parsed.get('action', 'HOLD').upper(), + 'confidence': parsed.get('confidence', 'MEDIUM').upper(), + 'reasoning': parsed.get('reasoning', ''), + 'raw_response': content + } + + # Validate action and confidence legality + if result['action'] not in ['BUY', 'SELL', 'HOLD']: + logger.warning(f"AI returned illegal action: {result['action']}, using HOLD") + result['action'] = 'HOLD' + + if result['confidence'] not in ['HIGH', 'MEDIUM', 'LOW']: + logger.warning(f"AI returned illegal confidence: {result['confidence']}, using MEDIUM") + result['confidence'] = 'MEDIUM' + + logger.info(f"AI parsing successful: {result['action']} (confidence: {result['confidence']})") + return result + + except json.JSONDecodeError as e: + logger.warning(f"JSON parsing failed: {e}, trying text analysis") + return self._parse_text_response(content) + else: + # Text analysis + return self._parse_text_response(content) + + except Exception as e: + logger.error(f"Error parsing DeepSeek response: {e}") + # Return default values to avoid system crash + return { + 'action': 'HOLD', + 'confidence': 'MEDIUM', + 'reasoning': f'Parsing error: {str(e)}', + 'raw_response': str(response) + } + + def _parse_text_response(self, text): + """Parse text response - enhanced version""" + text_lower = text.lower() + + # More precise action recognition + action = 'HOLD' + if any(word in text_lower for word in ['买入', '做多', 'buy', 'long', '上涨', '看涨']): + action = 'BUY' + elif any(word in text_lower for word in ['卖出', '做空', 'sell', 'short', '下跌', '看跌']): + action = 'SELL' + + # More precise confidence level recognition + confidence = 'MEDIUM' + if any(word in text_lower for word in ['强烈', '高信心', 'high', '非常', '强烈建议']): + confidence = 'HIGH' + elif any(word in text_lower for word in ['低', '低信心', 'low', '轻微', '谨慎']): + confidence = 'LOW' + + logger.info(f"Text parsing result: {action} (confidence: {confidence})") + + return { + 'action': action, + 'confidence': confidence, + 'reasoning': text, + 'raw_response': text + } diff --git a/main.py b/main.py deleted file mode 100644 index 3818585..0000000 --- a/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from okxtrading2-0!") - - -if __name__ == "__main__": - main() diff --git a/multistrategyrunner.py b/multistrategyrunner.py new file mode 100644 index 0000000..888e389 --- /dev/null +++ b/multistrategyrunner.py @@ -0,0 +1,167 @@ +import atexit +import logging +import signal +import time +import threading + +from okxapiclient import OKXAPIClient +from deepseekanalyzer import DeepSeekAnalyzer +from riskmanager import RiskManager +from tradingstrategy import TradingStrategy + +logger = logging.getLogger(__name__) + + +class MultiStrategyRunner: + """Multi-Strategy Runner""" + + def __init__(self): + self.running = False + self.strategies = {} + self.threads = {} + + # Configure each currency strategy + self.symbol_configs = { + 'ETH-USDT': {'name': 'Ethereum', 'interval': 1800, 'timeframe': '1H'}, + 'BTC-USDT': {'name': 'Bitcoin', 'interval': 1800, 'timeframe': '1H'}, + 'SOL-USDT': {'name': 'Solana', 'interval': 1800, 'timeframe': '1H'}, + 'XRP-USDT': {'name': 'Ripple', 'interval': 1800, 'timeframe': '1H'}, + 'BNB-USDT': {'name': 'Binance Coin', 'interval': 1800, 'timeframe': '1H'}, + 'OKB-USDT': {'name': 'OKB', 'interval': 1800, 'timeframe': '1H'}, + } + + # Initialize API client + self.api = OKXAPIClient() + self.deepseek = DeepSeekAnalyzer() + self.risk_manager = RiskManager(self.api) + + # Initialize all strategies + for symbol, config in self.symbol_configs.items(): + self.strategies[symbol] = TradingStrategy(symbol, config, self.api, self.risk_manager, self.deepseek) + + # Register exit handlers + atexit.register(self.shutdown) + signal.signal(signal.SIGINT, lambda s, f: self.shutdown()) + signal.signal(signal.SIGTERM, lambda s, f: self.shutdown()) + + def start_strategy(self, symbol): + """Start single currency strategy""" + if symbol not in self.strategies: + logger.error(f"Unsupported {symbol} trading pair") + return False + + # Check if already running + if symbol in self.threads and self.threads[symbol].is_alive(): + logger.info(f"{symbol} strategy already running") + return True + + def strategy_worker(): + """Strategy worker thread""" + strategy = self.strategies[symbol] + logger.info(f"Starting {symbol} strategy") + + try: + # Directly run strategy, strategy has its own loop internally + strategy.run() + except Exception as e: + logger.error(f"{symbol} strategy execution error: {e}") + finally: + logger.info(f"{symbol} strategy thread ended") + # Remove from active thread list when thread ends + if symbol in self.threads: + del self.threads[symbol] + + # Create and start thread + thread = threading.Thread(target=strategy_worker, name=f"Strategy-{symbol}") + thread.daemon = True + thread.start() + self.threads[symbol] = thread + + logger.info(f"Started {symbol} strategy") + return True + + def start_all_strategies(self): + """Start all strategies""" + self.running = True + + for symbol in self.strategies.keys(): + self.start_strategy(symbol) + time.sleep(60) # Each currency starts with 60 second interval + + + logger.info("All strategies started") + + def stop_strategy(self, symbol): + """Stop single currency strategy""" + if symbol in self.strategies: + # First stop strategy + self.strategies[symbol].stop_strategy() + + if symbol in self.threads: + thread = self.threads[symbol] + thread.join(timeout=10) # Wait 10 seconds + if thread.is_alive(): + logger.warning(f"Unable to stop {symbol} strategy thread") + else: + del self.threads[symbol] + logger.info(f"Stopped {symbol} strategy") + + # Stop dynamic stop-loss monitoring + if symbol in self.strategies: + self.strategies[symbol].stop_dynamic_stop_monitor() + + def stop_all_strategies(self): + """Stop all strategies""" + self.running = False + + # First send stop signal to all strategies + for symbol in list(self.strategies.keys()): + self.strategies[symbol].stop_strategy() + + # Then stop threads + for symbol in list(self.threads.keys()): + self.stop_strategy(symbol) + + logger.info("All strategies stopped") + + def get_status(self): + """Get system status""" + status = { + 'running': self.running, + 'active_strategies': list(self.threads.keys()), + 'strategies_detail': {} + } + + for symbol, strategy in self.strategies.items(): + status['strategies_detail'][symbol] = { + 'base_amount': strategy.base_amount, + 'entry_price': strategy.entry_price, + 'config': self.symbol_configs[symbol] + } + + return status + + def shutdown(self): + """System shutdown handling""" + logger.info("System shutting down, stopping all strategies and monitoring...") + self.stop_all_strategies() + + # Stop all dynamic stop-loss monitoring + for symbol, strategy in self.strategies.items(): + try: + strategy.stop_dynamic_stop_monitor() + logger.info(f"Stopped {symbol} dynamic stop-loss monitoring") + except Exception as e: + logger.error(f"Error stopping {symbol} dynamic stop-loss monitoring: {e}") + + # Wait for all threads to end + for symbol, thread in list(self.threads.items()): + if thread.is_alive(): + thread.join(timeout=5) # Wait 5 seconds + if thread.is_alive(): + logger.warning(f"{symbol} strategy thread didn't exit normally") + + # Ensure all resources released + time.sleep(1) + logger.info("System shutdown completed") + print("👋 System safely exited, thank you for using!") diff --git a/okxapiclient.py b/okxapiclient.py new file mode 100644 index 0000000..18d0255 --- /dev/null +++ b/okxapiclient.py @@ -0,0 +1,398 @@ +import os +import okx.Account as Account +import okx.MarketData as MarketData +import okx.Trade as Trade +from apiratelimiter import APIRateLimiter +import pandas as pd +import numpy as np +import logging + +logger = logging.getLogger(__name__) + +tdmode = "cross" # cross for demo account + +class OKXAPIClient: + """OKX API Client (using official SDK)""" + + def __init__(self): + self.api_key = os.getenv('OKX_API_KEY') + self.secret_key = os.getenv('OKX_SECRET_KEY') + self.password = os.getenv('OKX_PASSWORD') + + if not all([self.api_key, self.secret_key, self.password]): + raise ValueError("Please set OKX API key and password") + + # Initialize OKX SDK client - using live trading environment + Flag = "1" # Live trading environment + self.account_api = Account.AccountAPI(self.api_key, self.secret_key, self.password, False, Flag) + self.market_api = MarketData.MarketAPI(self.api_key, self.secret_key, self.password, False, Flag) + self.trade_api = Trade.TradeAPI(self.api_key, self.secret_key, self.password, False, Flag) + + # API rate limiting + self.rate_limiter = APIRateLimiter(2) + self.log_http = False + + # Cache instrument info + self.instrument_cache = {} + + def get_market_data(self, symbol, timeframe='1H', limit=200): + """Get market data""" + self.rate_limiter.wait("market_data") + + try: + result = self.market_api.get_candlesticks( + instId=symbol, + bar=timeframe, + limit=str(limit) + ) + + if self.log_http: + logger.debug(f"HTTP Request: GET {symbol} {timeframe} {result.get('code', 'Unknown')}") + + # Error checking + if 'code' not in result or result['code'] != '0': + error_msg = result.get('msg', 'Unknown error') + error_code = result.get('code', 'Unknown code') + logger.error(f"Failed to get {symbol} market data: {error_msg} (code: {error_code})") + return None + + # Check if data exists + if 'data' not in result or not result['data']: + logger.warning(f"{symbol} market data is empty") + return None + + # Create DataFrame + columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCcyQuote', 'confirm'] + df = pd.DataFrame(result['data'], columns=columns) + + # Convert data types + df['timestamp'] = pd.to_datetime(df['timestamp'].astype(np.int64), unit='ms') + numeric_cols = ['open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCcyQuote'] + df[numeric_cols] = df[numeric_cols].apply(pd.to_numeric) + + return df.sort_values('timestamp') + + except Exception as e: + logger.error(f"Error getting market data: {e}") + return None + + def get_account_balance(self): + """Get account balance (USDT)""" + self.rate_limiter.wait("balance") + + try: + result = self.account_api.get_account_balance() + + if self.log_http: + logger.debug(f"HTTP Request: GET balance {result['code']}") + + # Correction: code "0" indicates success + if result['code'] != '0': + logger.error(f"Failed to get balance: {result['msg']} (code: {result['code']})") + return None + + # Extract USDT balance + for currency in result['data'][0]['details']: + if currency['ccy'] == 'USDT': + return float(currency['availBal']) + + return 0.0 + + except Exception as e: + logger.error(f"Error getting balance: {e}") + return None + + def get_currency_balances(self): + """Get all currency balances""" + self.rate_limiter.wait("balances") + + try: + result = self.account_api.get_account_balance() + + # Correction: code "0" indicates success + if result['code'] != '0': + logger.error(f"Failed to get balance: {result['msg']} (code: {result['code']})") + return {} + + # Check response data structure + if not result['data'] or len(result['data']) == 0: + logger.error("No balance data in API response") + return {} + + # Check if details field exists + if 'details' not in result['data'][0]: + logger.error("No details field in API response") + return {} + + balances = {} + for currency in result['data'][0]['details']: + if float(currency.get('availBal', 0)) > 0: + balances[currency['ccy']] = { + 'amount': float(currency.get('availBal', 0)), + 'frozen': float(currency.get('frozenBal', 0)) + } + + return balances + + except Exception as e: + logger.error(f"Error getting currency balances: {e}") + return {} + + def get_positions(self): + """Get exchange position information (based on currency balances)""" + try: + # Get all currency balances + balances = self.get_currency_balances() + if not balances: + return {} + + # Filter out non-USDT currencies as positions + positions = {} + for currency, balance in balances.items(): + if currency != 'USDT' and balance['amount'] > 0: + # Construct trading pair symbol + symbol = f"{currency}-USDT" + # Get current price to calculate position value + current_price = self.get_current_price(symbol) + if current_price: + positions[symbol] = { + 'amount': balance['amount'], + 'value': balance['amount'] * current_price, + 'avg_price': 0.0 # Spot positions don't have average price concept + } + else: + positions[symbol] = { + 'amount': balance['amount'], + 'value': 0.0, + 'avg_price': 0.0 + } + + return positions + + except Exception as e: + logger.error(f"Error getting positions: {e}") + return {} + + def get_current_price(self, symbol): + """Get current price""" + self.rate_limiter.wait("price") + + try: + result = self.market_api.get_ticker(instId=symbol) + + # Correction: code "0" indicates success + if result['code'] != '0': + logger.error(f"Failed to get price: {result['msg']} (code: {result['code']})") + return None + + return float(result['data'][0]['last']) + + except Exception as e: + logger.error(f"Error getting price: {e}") + return None + + def get_instrument_info(self, symbol): + """Get trading pair information""" + if symbol in self.instrument_cache: + return self.instrument_cache[symbol] + + self.rate_limiter.wait("instrument") + + try: + result = self.account_api.get_instruments(instType='SPOT') + + if result['code'] != '0': + logger.error(f"Failed to get instrument: {result['msg']} (code: {result['code']})") + return None, None + + # Find specified trading pair + for inst in result['data']: + if inst['instId'] == symbol: + min_sz = float(inst['minSz']) + lot_sz = float(inst['lotSz']) + logger.debug(f"Got {symbol} precision: minSz={min_sz}, lotSz={lot_sz}") + self.instrument_cache[symbol] = (min_sz, lot_sz) + return min_sz, lot_sz + + logger.error(f"Trading pair not found: {symbol}") + return None, None + + except Exception as e: + logger.error(f"Error getting instrument info: {e}") + return None, None + + def get_default_min_size(self, symbol): + """Get default minimum order size""" + # Set default minimum order size based on currency + defaults = { + 'BTC-USDT': 0.0001, + 'ETH-USDT': 0.001, + 'SOL-USDT': 0.01, + 'XRP-USDT': 1.0 + } + return defaults.get(symbol, 0.01) # Default 0.01 + + def create_order(self, symbol, side, amount, retries=3): + """Create order""" + for attempt in range(retries): + try: + self.rate_limiter.wait("order") + + # Parse trading pair symbol + parts = symbol.split('-') + if len(parts) != 2: + logger.error(f"Invalid trading pair format: {symbol}") + return None + + base_currency, quote_currency = parts + + # Adjust parameters based on buy/sell direction + if side == 'buy': + # When buying, amount is quote currency amount (USDT amount) + # Use amount-based order placement + order_params = { + 'instId': symbol, + 'tdMode': tdmode, + 'side': 'buy', + 'ordType': 'market', + 'sz': str(amount), # Quote currency amount + 'tgtCcy': 'quote_ccy' # Specify sz as quote currency + } + logger.info(f"[{symbol}] Create buy order: amount={amount:.2f} {quote_currency}") + + else: + # When selling, amount is base currency quantity + # Get precision info and adjust quantity + min_sz, lot_sz = self.get_instrument_info(symbol) + if min_sz is None: + min_sz = self.get_default_min_size(symbol) + if lot_sz is None: + lot_sz = min_sz + + # Adjust quantity to appropriate precision + if lot_sz > 0: + amount = (amount / lot_sz) * lot_sz + + amount_str = f"{amount:.10f}" + + order_params = { + 'instId': symbol, + 'tdMode': tdmode, + 'side': 'sell', + 'ordType': 'market', + 'sz': amount_str # Base currency quantity + } + logger.info(f"[{symbol}] Create sell order: quantity={amount_str} {base_currency}") + + # Use SDK to create order + result = self.trade_api.place_order(**order_params) + + if self.log_http: + logger.debug(f"HTTP Request: POST create order {result['code']}") + + # Check API response + if result['code'] != '0': + logger.error(f"Failed to create order: {result['msg']} (code: {result['code']})") + if 'data' in result and len(result['data']) > 0: + for item in result['data']: + logger.error(f"Detailed error: {item.get('sMsg', 'Unknown')} (sCode: {item.get('sCode', 'Unknown')})") + # Specific error handling + if result['code'] == '50113': # Insufficient permissions + logger.error("API key may not have trading permissions, please check API key settings") + elif result['code'] == '51020': # Minimum order amount + logger.error("Order amount below exchange minimum requirement") + if attempt < retries - 1: + wait_time = 2 ** attempt + time.sleep(wait_time) + continue + + # Check order status + if len(result['data']) > 0: + order_data = result['data'][0] + if order_data.get('sCode') != '0': + logger.error(f"Order creation failed: {order_data.get('sMsg', 'Unknown error')} (sCode: {order_data.get('sCode', 'Unknown')})") + if attempt < retries - 1: + wait_time = 2 ** attempt + time.sleep(wait_time) + continue + + order_id = order_data.get('ordId') + if order_id: + logger.info(f"Order created successfully: {order_id}") + return order_id + else: + logger.error("Order ID is empty") + if attempt < retries - 1: + wait_time = 2 ** attempt + time.sleep(wait_time) + continue + else: + logger.error("No order data in API response") + if attempt < retries - 1: + wait_time = 2 ** attempt + time.sleep(wait_time) + continue + + except Exception as e: + logger.error(f"Error creating order (attempt {attempt+1}/{retries}): {str(e)}") + if attempt < retries - 1: + wait_time = 2 ** attempt + time.sleep(wait_time) + else: + return None + + return None + + def get_order_status(self, symbol, order_id): + """Get order status""" + self.rate_limiter.wait("order_status") + + try: + result = self.trade_api.get_order(instId=symbol, ordId=order_id) + + # Correction: code "0" indicates success + if result['code'] != '0': + logger.error(f"Failed to get order status: {result['msg']} (code: {result['code']})") + return None + + if len(result['data']) > 0: + order_data = result['data'][0] + return { + 'state': order_data.get('state'), + 'avgPx': float(order_data.get('avgPx', 0)), + 'accFillSz': float(order_data.get('accFillSz', 0)), + 'fillPx': float(order_data.get('fillPx', 0)), + 'fillSz': float(order_data.get('fillSz', 0)), + 'fillTime': order_data.get('fillTime') + } + else: + logger.error("No order data in API response") + return None + + except Exception as e: + logger.error(f"Error getting order status: {e}") + return None + + def wait_for_order_completion(self, symbol, order_id, max_attempts=10, interval=1): + """Wait for order completion""" + for attempt in range(max_attempts): + order_status = self.get_order_status(symbol, order_id) + if order_status is None: + return None + + state = order_status['state'] + if state == 'filled': + logger.info(f"Order completed: {order_id}, fill price={order_status['avgPx']:.2f}, fill quantity={order_status['accFillSz']:.10f}") + return order_status + elif state == 'canceled': + logger.warning(f"Order canceled: {order_id}") + return None + elif state == 'partially_filled': + logger.info(f"Order partially filled: {order_id}, filled={order_status['accFillSz']:.10f}") + time.sleep(interval) + else: + logger.info(f"Order status: {state}, waiting...") + time.sleep(interval) + + logger.warning(f"Order not completed within specified time: {order_id}") + return None diff --git a/okxtrading2.0.py b/okxtrading2.0.py index b05ec29..ef94b68 100644 --- a/okxtrading2.0.py +++ b/okxtrading2.0.py @@ -1,6 +1,5 @@ import os import time -import threading import logging import pandas as pd import numpy as np @@ -10,17 +9,12 @@ import hashlib import hmac import base64 import schedule -import okx.Account as Account -import okx.MarketData as MarketData -import okx.Trade as Trade from datetime import datetime, timezone, timedelta from dotenv import load_dotenv from logging.handlers import RotatingFileHandler -from database_manager import DatabaseManager -import atexit -import signal -tdmode = 'cross' +from okxapiclient import OKXAPIClient +from multistrategyrunner import MultiStrategyRunner # Load environment variables load_dotenv() @@ -79,2820 +73,6 @@ def setup_logging(): logger = setup_logging() -class APIRateLimiter: - """API rate limiter""" - - def __init__(self, calls_per_second=2): - self.min_interval = 1.0 / calls_per_second - self.last_call = {} - self.lock = threading.Lock() - - def wait(self, api_name): - """Wait until API can be called""" - with self.lock: - current_time = time.time() - if api_name in self.last_call: - elapsed = current_time - self.last_call[api_name] - if elapsed < self.min_interval: - time.sleep(self.min_interval - elapsed) - self.last_call[api_name] = current_time - -class OKXAPIClient: - """OKX API Client (using official SDK)""" - - def __init__(self): - self.api_key = os.getenv('OKX_API_KEY') - self.secret_key = os.getenv('OKX_SECRET_KEY') - self.password = os.getenv('OKX_PASSWORD') - - if not all([self.api_key, self.secret_key, self.password]): - raise ValueError("Please set OKX API key and password") - - # Initialize OKX SDK client - using live trading environment - Flag = "1" # Live trading environment - self.account_api = Account.AccountAPI(self.api_key, self.secret_key, self.password, False, Flag) - self.market_api = MarketData.MarketAPI(self.api_key, self.secret_key, self.password, False, Flag) - self.trade_api = Trade.TradeAPI(self.api_key, self.secret_key, self.password, False, Flag) - - # API rate limiting - self.rate_limiter = APIRateLimiter(2) - self.log_http = False - - # Cache instrument info - self.instrument_cache = {} - - def get_market_data(self, symbol, timeframe='1H', limit=200): - """Get market data""" - self.rate_limiter.wait("market_data") - - try: - result = self.market_api.get_candlesticks( - instId=symbol, - bar=timeframe, - limit=str(limit) - ) - - if self.log_http: - logger.debug(f"HTTP Request: GET {symbol} {timeframe} {result.get('code', 'Unknown')}") - - # Error checking - if 'code' not in result or result['code'] != '0': - error_msg = result.get('msg', 'Unknown error') - error_code = result.get('code', 'Unknown code') - logger.error(f"Failed to get {symbol} market data: {error_msg} (code: {error_code})") - return None - - # Check if data exists - if 'data' not in result or not result['data']: - logger.warning(f"{symbol} market data is empty") - return None - - # Create DataFrame - columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCcyQuote', 'confirm'] - df = pd.DataFrame(result['data'], columns=columns) - - # Convert data types - df['timestamp'] = pd.to_datetime(df['timestamp'].astype(np.int64), unit='ms') - numeric_cols = ['open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCcyQuote'] - df[numeric_cols] = df[numeric_cols].apply(pd.to_numeric) - - return df.sort_values('timestamp') - - except Exception as e: - logger.error(f"Error getting market data: {e}") - return None - - def get_account_balance(self): - """Get account balance (USDT)""" - self.rate_limiter.wait("balance") - - try: - result = self.account_api.get_account_balance() - - if self.log_http: - logger.debug(f"HTTP Request: GET balance {result['code']}") - - # Correction: code "0" indicates success - if result['code'] != '0': - logger.error(f"Failed to get balance: {result['msg']} (code: {result['code']})") - return None - - # Extract USDT balance - for currency in result['data'][0]['details']: - if currency['ccy'] == 'USDT': - return float(currency['availBal']) - - return 0.0 - - except Exception as e: - logger.error(f"Error getting balance: {e}") - return None - - def get_currency_balances(self): - """Get all currency balances""" - self.rate_limiter.wait("balances") - - try: - result = self.account_api.get_account_balance() - - # Correction: code "0" indicates success - if result['code'] != '0': - logger.error(f"Failed to get balance: {result['msg']} (code: {result['code']})") - return {} - - # Check response data structure - if not result['data'] or len(result['data']) == 0: - logger.error("No balance data in API response") - return {} - - # Check if details field exists - if 'details' not in result['data'][0]: - logger.error("No details field in API response") - return {} - - balances = {} - for currency in result['data'][0]['details']: - if float(currency.get('availBal', 0)) > 0: - balances[currency['ccy']] = { - 'amount': float(currency.get('availBal', 0)), - 'frozen': float(currency.get('frozenBal', 0)) - } - - return balances - - except Exception as e: - logger.error(f"Error getting currency balances: {e}") - return {} - - def get_positions(self): - """Get exchange position information (based on currency balances)""" - try: - # Get all currency balances - balances = self.get_currency_balances() - if not balances: - return {} - - # Filter out non-USDT currencies as positions - positions = {} - for currency, balance in balances.items(): - if currency != 'USDT' and balance['amount'] > 0: - # Construct trading pair symbol - symbol = f"{currency}-USDT" - # Get current price to calculate position value - current_price = self.get_current_price(symbol) - if current_price: - positions[symbol] = { - 'amount': balance['amount'], - 'value': balance['amount'] * current_price, - 'avg_price': 0.0 # Spot positions don't have average price concept - } - else: - positions[symbol] = { - 'amount': balance['amount'], - 'value': 0.0, - 'avg_price': 0.0 - } - - return positions - - except Exception as e: - logger.error(f"Error getting positions: {e}") - return {} - - def get_current_price(self, symbol): - """Get current price""" - self.rate_limiter.wait("price") - - try: - result = self.market_api.get_ticker(instId=symbol) - - # Correction: code "0" indicates success - if result['code'] != '0': - logger.error(f"Failed to get price: {result['msg']} (code: {result['code']})") - return None - - return float(result['data'][0]['last']) - - except Exception as e: - logger.error(f"Error getting price: {e}") - return None - - def get_instrument_info(self, symbol): - """Get trading pair information""" - if symbol in self.instrument_cache: - return self.instrument_cache[symbol] - - self.rate_limiter.wait("instrument") - - try: - result = self.account_api.get_instruments(instType='SPOT') - - if result['code'] != '0': - logger.error(f"Failed to get instrument: {result['msg']} (code: {result['code']})") - return None, None - - # Find specified trading pair - for inst in result['data']: - if inst['instId'] == symbol: - min_sz = float(inst['minSz']) - lot_sz = float(inst['lotSz']) - logger.debug(f"Got {symbol} precision: minSz={min_sz}, lotSz={lot_sz}") - self.instrument_cache[symbol] = (min_sz, lot_sz) - return min_sz, lot_sz - - logger.error(f"Trading pair not found: {symbol}") - return None, None - - except Exception as e: - logger.error(f"Error getting instrument info: {e}") - return None, None - - def get_default_min_size(self, symbol): - """Get default minimum order size""" - # Set default minimum order size based on currency - defaults = { - 'BTC-USDT': 0.0001, - 'ETH-USDT': 0.001, - 'SOL-USDT': 0.01, - 'XRP-USDT': 1.0 - } - return defaults.get(symbol, 0.01) # Default 0.01 - - def create_order(self, symbol, side, amount, retries=3): - """Create order""" - for attempt in range(retries): - try: - self.rate_limiter.wait("order") - - # Parse trading pair symbol - parts = symbol.split('-') - if len(parts) != 2: - logger.error(f"Invalid trading pair format: {symbol}") - return None - - base_currency, quote_currency = parts - - # Adjust parameters based on buy/sell direction - if side == 'buy': - # When buying, amount is quote currency amount (USDT amount) - # Use amount-based order placement - order_params = { - 'instId': symbol, - 'tdMode': tdmode, - 'side': 'buy', - 'ordType': 'market', - 'sz': str(amount), # Quote currency amount - 'tgtCcy': 'quote_ccy' # Specify sz as quote currency - } - logger.info(f"[{symbol}] Create buy order: amount={amount:.2f} {quote_currency}") - - else: - # When selling, amount is base currency quantity - # Get precision info and adjust quantity - min_sz, lot_sz = self.get_instrument_info(symbol) - if min_sz is None: - min_sz = self.get_default_min_size(symbol) - if lot_sz is None: - lot_sz = min_sz - - # Adjust quantity to appropriate precision - if lot_sz > 0: - amount = (amount / lot_sz) * lot_sz - - amount_str = f"{amount:.10f}" - - order_params = { - 'instId': symbol, - 'tdMode': tdmode, - 'side': 'sell', - 'ordType': 'market', - 'sz': amount_str # Base currency quantity - } - logger.info(f"[{symbol}] Create sell order: quantity={amount_str} {base_currency}") - - # Use SDK to create order - result = self.trade_api.place_order(**order_params) - - if self.log_http: - logger.debug(f"HTTP Request: POST create order {result['code']}") - - # Check API response - if result['code'] != '0': - logger.error(f"Failed to create order: {result['msg']} (code: {result['code']})") - if 'data' in result and len(result['data']) > 0: - for item in result['data']: - logger.error(f"Detailed error: {item.get('sMsg', 'Unknown')} (sCode: {item.get('sCode', 'Unknown')})") - # Specific error handling - if result['code'] == '50113': # Insufficient permissions - logger.error("API key may not have trading permissions, please check API key settings") - elif result['code'] == '51020': # Minimum order amount - logger.error("Order amount below exchange minimum requirement") - if attempt < retries - 1: - wait_time = 2 ** attempt - time.sleep(wait_time) - continue - - # Check order status - if len(result['data']) > 0: - order_data = result['data'][0] - if order_data.get('sCode') != '0': - logger.error(f"Order creation failed: {order_data.get('sMsg', 'Unknown error')} (sCode: {order_data.get('sCode', 'Unknown')})") - if attempt < retries - 1: - wait_time = 2 ** attempt - time.sleep(wait_time) - continue - - order_id = order_data.get('ordId') - if order_id: - logger.info(f"Order created successfully: {order_id}") - return order_id - else: - logger.error("Order ID is empty") - if attempt < retries - 1: - wait_time = 2 ** attempt - time.sleep(wait_time) - continue - else: - logger.error("No order data in API response") - if attempt < retries - 1: - wait_time = 2 ** attempt - time.sleep(wait_time) - continue - - except Exception as e: - logger.error(f"Error creating order (attempt {attempt+1}/{retries}): {str(e)}") - if attempt < retries - 1: - wait_time = 2 ** attempt - time.sleep(wait_time) - else: - return None - - return None - - def get_order_status(self, symbol, order_id): - """Get order status""" - self.rate_limiter.wait("order_status") - - try: - result = self.trade_api.get_order(instId=symbol, ordId=order_id) - - # Correction: code "0" indicates success - if result['code'] != '0': - logger.error(f"Failed to get order status: {result['msg']} (code: {result['code']})") - return None - - if len(result['data']) > 0: - order_data = result['data'][0] - return { - 'state': order_data.get('state'), - 'avgPx': float(order_data.get('avgPx', 0)), - 'accFillSz': float(order_data.get('accFillSz', 0)), - 'fillPx': float(order_data.get('fillPx', 0)), - 'fillSz': float(order_data.get('fillSz', 0)), - 'fillTime': order_data.get('fillTime') - } - else: - logger.error("No order data in API response") - return None - - except Exception as e: - logger.error(f"Error getting order status: {e}") - return None - - def wait_for_order_completion(self, symbol, order_id, max_attempts=10, interval=1): - """Wait for order completion""" - for attempt in range(max_attempts): - order_status = self.get_order_status(symbol, order_id) - if order_status is None: - return None - - state = order_status['state'] - if state == 'filled': - logger.info(f"Order completed: {order_id}, fill price={order_status['avgPx']:.2f}, fill quantity={order_status['accFillSz']:.10f}") - return order_status - elif state == 'canceled': - logger.warning(f"Order canceled: {order_id}") - return None - elif state == 'partially_filled': - logger.info(f"Order partially filled: {order_id}, filled={order_status['accFillSz']:.10f}") - time.sleep(interval) - else: - logger.info(f"Order status: {state}, waiting...") - time.sleep(interval) - - logger.warning(f"Order not completed within specified time: {order_id}") - return None - -class DeepSeekAnalyzer: - """DeepSeek AI Analyzer (KEN2.0 optimized)""" - - def __init__(self): - self.api_key = os.getenv('DEEPSEEK_API_KEY') - self.api_url = 'https://api.deepseek.com/v1/chat/completions' - - def analyze_market(self, symbol, market_data, technical_indicators): - """Use DeepSeek to analyze market (optimized version)""" - if not self.api_key: - logger.warning("DeepSeek API key not set, skipping AI analysis") - return None - - try: - # Prepare analysis data - latest = market_data.iloc[-1] - technical_summary = self._prepare_enhanced_technical_summary(technical_indicators, market_data) - - prompt = self._create_enhanced_analysis_prompt(symbol, latest, market_data, technical_summary) - response = self._call_deepseek_api(prompt) - - return self._parse_deepseek_response(response) - - except Exception as e: - logger.error(f"DeepSeek analysis error: {e}") - return None - - def _prepare_enhanced_technical_summary(self, indicators, market_data): - """Prepare enhanced technical indicator summary""" - summary = [] - - try: - # Price information - current_price = indicators.get('current_price', 0) - summary.append(f"Current price: ${current_price:.4f}") - - # Multi-period RSI - rsi_7 = indicators.get('rsi_7', 50) - rsi_14 = indicators.get('rsi', 50) - rsi_21 = indicators.get('rsi_21', 50) - if hasattr(rsi_7, '__len__'): - rsi_7, rsi_14, rsi_21 = rsi_7.iloc[-1], rsi_14.iloc[-1], rsi_21.iloc[-1] - - summary.append(f"RSI(7/14/21): {rsi_7:.1f}/{rsi_14:.1f}/{rsi_21:.1f}") - - # RSI status analysis - rsi_status = [] - for rsi_val, period in [(rsi_7, 7), (rsi_14, 14), (rsi_21, 21)]: - if rsi_val > 70: - rsi_status.append(f"RSI{period} overbought") - elif rsi_val < 30: - rsi_status.append(f"RSI{period} oversold") - if rsi_status: - summary.append(f"RSI status: {', '.join(rsi_status)}") - - # KDJ indicator - k, d, j = indicators.get('kdj', (50, 50, 50)) - if hasattr(k, '__len__'): - k, d, j = k.iloc[-1], d.iloc[-1], j.iloc[-1] - - summary.append(f"KDJ: K={k:.1f}, D={d:.1f}, J={j:.1f}") - if j > 80: - summary.append("KDJ status: Overbought area") - elif j < 20: - summary.append("KDJ status: Oversold area") - - # MACD indicator - macd_line, signal_line, histogram = indicators.get('macd', (0, 0, 0)) - if hasattr(macd_line, '__len__'): - macd_line, signal_line, histogram = macd_line.iloc[-1], signal_line.iloc[-1], histogram.iloc[-1] - - summary.append(f"MACD: line={macd_line:.4f}, signal={signal_line:.4f}, histogram={histogram:.4f}") - summary.append(f"MACD status: {'Golden cross' if macd_line > signal_line else 'Death cross'}") - - # Moving averages - sma_20 = indicators.get('sma_20', 0) - sma_50 = indicators.get('sma_50', 0) - sma_100 = indicators.get('sma_100', 0) - if hasattr(sma_20, '__len__'): - sma_20, sma_50, sma_100 = sma_20.iloc[-1], sma_50.iloc[-1], sma_100.iloc[-1] - - price_vs_ma = [] - if current_price > sma_20: price_vs_ma.append("Above 20-day MA") - else: price_vs_ma.append("Below 20-day MA") - if current_price > sma_50: price_vs_ma.append("Above 50-day MA") - else: price_vs_ma.append("Below 50-day MA") - - summary.append(f"Moving averages: 20-day=${sma_20:.2f}, 50-day=${sma_50:.2f}, 100-day=${sma_100:.2f}") - summary.append(f"Price position: {', '.join(price_vs_ma)}") - - # Bollinger Bands - upper, middle, lower = indicators.get('bollinger_2std', (0, 0, 0)) - if hasattr(upper, '__len__'): - upper, middle, lower = upper.iloc[-1], middle.iloc[-1], lower.iloc[-1] - - bb_position = (current_price - lower) / (upper - lower) * 100 if (upper - lower) > 0 else 50 - summary.append(f"Bollinger Band position: {bb_position:.1f}% (0%=lower band, 100%=upper band)") - - # Trend strength - trend_strength = indicators.get('trend_strength', 0) - summary.append(f"Trend strength: {trend_strength:.2f} (0-1, higher means stronger trend)") - - # Volatility - volatility = indicators.get('volatility', 0) * 100 # Convert to percentage - summary.append(f"Annualized volatility: {volatility:.1f}%") - - # ATR - atr = indicators.get('atr', 0) - if hasattr(atr, '__len__'): - atr = atr.iloc[-1] - atr_percent = (atr / current_price * 100) if current_price > 0 else 0 - summary.append(f"ATR: {atr:.4f} ({atr_percent:.2f}%)") - - # Support resistance information - sr_levels = TechnicalAnalyzer.calculate_support_resistance(market_data) - if sr_levels: - summary.append(f"Static support: ${sr_levels['static_support']:.4f}, resistance: ${sr_levels['static_resistance']:.4f}") - summary.append(f"Dynamic support: ${sr_levels['dynamic_support']:.4f}, resistance: ${sr_levels['dynamic_resistance']:.4f}") - summary.append(f"Relative resistance: {sr_levels['current_vs_resistance']:+.2f}%, relative support: {sr_levels['current_vs_support']:+.2f}%") - - return "\n".join(summary) - - except Exception as e: - logger.error(f"Error preparing technical summary: {e}") - return "Technical indicator calculation exception" - - def _create_enhanced_analysis_prompt(self, symbol, latest, market_data, technical_summary): - """Create enhanced analysis prompt""" - # Calculate price changes - price_change_24h = 0 - price_change_7d = 0 - - if len(market_data) >= 24: - price_change_24h = ((latest['close'] - market_data.iloc[-24]['close']) / market_data.iloc[-24]['close'] * 100) - - if len(market_data) >= 168: # 7 days data (hourly) - price_change_7d = ((latest['close'] - market_data.iloc[-168]['close']) / market_data.iloc[-168]['close'] * 100) - - # Calculate volume changes - volume_trend = "Rising" if latest['volume'] > market_data['volume'].mean() else "Falling" - - prompt = f""" -You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market. Please comprehensively analyze the {symbol} trading pair and execute trades. - -# Core Objective -**Maximize Sharpe Ratio** -Sharpe Ratio = Average Return / Return Volatility, which means: -Quality trades (high win rate, large profit/loss ratio) → Improve Sharpe -Stable returns, control drawdowns → Improve Sharpe -Frequent trading, small profits and losses → Increase volatility, severely reduce Sharpe - -【Market Overview】 -- Current price: ${latest['close']:.4f} -- 24-hour change: {price_change_24h:+.2f}% -- 7-day change: {price_change_7d:+.2f}% -- Volume: {latest['volume']:.0f} ({volume_trend}) -- Volume relative level: {'High' if latest['volume'] > market_data['volume'].quantile(0.7) else 'Normal' if latest['volume'] > market_data['volume'].quantile(0.3) else 'Low'} - -【Multi-dimensional Technical Analysis】 -{technical_summary} - -【Market Environment Assessment】 -Please consider comprehensively: -1. Trend direction and strength -2. Overbought/oversold status -3. Volume coordination -4. Volatility level -5. Support resistance positions -6. Multi-timeframe consistency - -【Risk Management Suggestions】 -- Suggested position: Adjust based on signal strength and volatility -- Stop-loss setting: Reference ATR and support levels -- Holding time: Based on trend strength - -【Output Format Requirements】 -Please reply strictly in the following JSON format: -{{ - "action": "BUY/SELL/HOLD", - "confidence": "HIGH/MEDIUM/LOW", - "position_size": "Suggested position ratio (0.1-0.5)", - "stop_loss": "Suggested stop-loss price or percentage", - "take_profit": "Suggested take-profit price or percentage", - "timeframe": "Suggested holding time (SHORT/MEDIUM/LONG)", - "reasoning": "Detailed analysis logic and risk explanation" -}} - -Please provide objective suggestions based on professional technical analysis and risk management principles. -""" - - return prompt - - def analyze_sell_proportion(self, symbol, position_info, market_analysis, current_price): - """Analyze sell proportion""" - if not self.api_key: - logger.warning("DeepSeek API key not set, using default sell proportion") - return self._get_fallback_sell_proportion(position_info, 'MEDIUM') - - try: - prompt = self._create_sell_proportion_prompt(symbol, position_info, market_analysis, current_price) - response = self._call_deepseek_api(prompt) - return self._parse_sell_proportion_response(response) - - except Exception as e: - logger.error(f"DeepSeek sell proportion analysis error: {e}") - return self._get_fallback_sell_proportion(position_info, 'MEDIUM') - - def _create_sell_proportion_prompt(self, symbol, position_info, market_analysis, current_price): - """Create sell proportion analysis prompt""" - - # Calculate profit situation - entry_price = position_info['entry_price'] - profit_ratio = (current_price - entry_price) / entry_price if entry_price > 0 else 0 - profit_status = "Profit" if profit_ratio > 0 else "Loss" - profit_amount = (current_price - entry_price) * position_info['base_amount'] - - # Calculate position ratio - position_ratio = position_info['position_ratio'] * 100 # Convert to percentage - - # Prepare technical indicator summary - technical_summary = self._prepare_enhanced_technical_summary( - market_analysis['technical_indicators'], - market_analysis['market_data'] - ) - - prompt = f""" -You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market. Analyze the {symbol} position situation and consider and execute sell decisions. - -【Current Position Situation】 -- Position quantity: {position_info['base_amount']:.8f} -- Average entry price: ${entry_price:.4f} -- Current price: ${current_price:.4f} -- Profit ratio: {profit_ratio:+.2%} ({profit_status}) -- Profit amount: ${profit_amount:+.2f} -- Position ratio: {position_ratio:.2f}% (percentage of total portfolio) -- Position value: ${position_info['position_value']:.2f} - -【Market Technical Analysis】 -{technical_summary} - -【Risk Situation】 -- Current price distance to stop-loss: {position_info['distance_to_stop_loss']:.2%} -- Current price distance to take-profit: {position_info['distance_to_take_profit']:.2%} -- Market volatility: {position_info['market_volatility']:.2%} -- Trend strength: {position_info.get('trend_strength', 0):.2f} - -【Sell Strategy Considerations】 -Please consider the following factors to determine sell proportion: -1. Profit ratio: Large profits can consider partial profit-taking, keep some position for higher gains -2. Position ratio: Overweight positions should reduce position to lower risk, light positions can consider holding -3. Technical signals: Strong bearish signals should increase sell proportion, bullish signals can reduce sell proportion -4. Market environment: High volatility markets should be more conservative, low volatility markets can be more aggressive -5. Risk control: Close to stop-loss should sell decisively, far from stop-loss can be more flexible - -【Position Management Principles】 -- Profit over 20%: Consider partial profit-taking (30-70%), lock in profits -- Profit 10-20%: Decide based on signal strength (20-50%) -- Small profit (0-10%): Mainly based on technical signals (0-30%) -- Small loss (0-5%): Based on risk control (50-80%) -- Large loss (>5%): Consider stop-loss or position reduction (80-100%) - -【Output Format Requirements】 -Please reply strictly in the following JSON format, only containing numeric ratio (0-1): -{{ - "sell_proportion": 0.75 -}} - -Explanation: -- 0.1 means sell 10% of position -- 0.5 means sell 50% of position -- 1.0 means sell all position -- 0.0 means don't sell (only in extremely bullish situations) - -Please provide reasonable sell proportion suggestions based on professional analysis and risk management. -""" - - return prompt - - def _parse_sell_proportion_response(self, response): - """Parse sell proportion response""" - try: - content = response['choices'][0]['message']['content'] - - # Try to parse JSON format - if '{' in content and '}' in content: - json_start = content.find('{') - json_end = content.rfind('}') + 1 - json_str = content[json_start:json_end] - - parsed = json.loads(json_str) - proportion = parsed.get('sell_proportion', 0.5) - - # Ensure proportion is within reasonable range - proportion = max(0.0, min(1.0, proportion)) - logger.info(f"DeepSeek suggested sell proportion: {proportion:.1%}") - return proportion - else: - # Text analysis: try to extract proportion from text - import re - patterns = [ - r'[\"\"]([\d.]+)%[\"\"]', # "50%" - r'sell\s*([\d.]+)%', # sell 50% - r'([\d.]+)%', # 50% - r'[\"\"]([\d.]+)[\"\"]', # "0.5" - ] - - for pattern in patterns: - match = re.search(pattern, content) - if match: - value = float(match.group(1)) - if value > 1: # If in percentage format - value = value / 100 - proportion = max(0.0, min(1.0, value)) - logger.info(f"Parsed sell proportion from text: {proportion:.1%}") - return proportion - - # Default value - logger.warning("Unable to parse sell proportion, using default 50%") - return 0.5 - - except Exception as e: - logger.error(f"Error parsing sell proportion response: {e}") - return 0.5 - - def _get_fallback_sell_proportion(self, position_info, decision_confidence): - """Fallback sell proportion decision (when DeepSeek is unavailable)""" - - profit_ratio = position_info['profit_ratio'] - position_ratio = position_info['position_ratio'] - market_volatility = position_info.get('market_volatility', 0) - trend_strength = position_info.get('trend_strength', 0) - - # Decision based on profit situation - if profit_ratio >= 0.20: # Profit over 20% - base_proportion = 0.6 if decision_confidence == 'HIGH' else 0.4 - elif profit_ratio >= 0.10: # Profit 10%-20% - base_proportion = 0.5 if decision_confidence == 'HIGH' else 0.3 - elif profit_ratio >= 0: # Profit 0%-10% - base_proportion = 0.4 if decision_confidence == 'HIGH' else 0.2 - elif profit_ratio >= -0.05: # Loss 0%-5% - base_proportion = 0.7 if decision_confidence == 'HIGH' else 0.5 - else: # Loss over 5% - base_proportion = 0.9 if decision_confidence == 'HIGH' else 0.7 - - # Adjustment based on position ratio - if position_ratio > 0.15: # Overweight position - base_proportion = min(1.0, base_proportion + 0.2) - elif position_ratio < 0.05: # Very light position - base_proportion = max(0.1, base_proportion - 0.1) - - # Adjustment based on market volatility - if market_volatility > 0.05: # High volatility market - base_proportion = min(1.0, base_proportion + 0.1) - elif market_volatility < 0.02: # Low volatility market - base_proportion = max(0.1, base_proportion - 0.1) - - # Adjustment based on trend strength - if trend_strength > 0.7: # Strong trend - base_proportion = max(0.1, base_proportion - 0.1) - elif trend_strength < 0.3: # Weak trend - base_proportion = min(1.0, base_proportion + 0.1) - - # Ensure proportion is within reasonable range - base_proportion = max(0.1, min(1.0, base_proportion)) - - logger.info(f"Fallback sell proportion decision: profit={profit_ratio:+.2%}, position ratio={position_ratio:.2%}, " - f"volatility={market_volatility:.2%}, trend strength={trend_strength:.2f}, final proportion={base_proportion:.1%}") - return base_proportion - - def _call_deepseek_api(self, prompt, max_retries=3): - """Call DeepSeek API (improved retry mechanism)""" - headers = { - 'Authorization': f'Bearer {self.api_key}', - 'Content-Type': 'application/json' - } - - data = { - 'model': 'deepseek-chat', - 'messages': [ - { - 'role': 'system', - 'content': 'You are a professional cryptocurrency trading AI, conducting autonomous trading in the OKX digital currency market, pursuing stable and sustainable returns' - }, - { - 'role': 'user', - 'content': prompt - } - ], - 'temperature': 0.3, - 'max_tokens': 1000 - } - - for attempt in range(max_retries): - try: - response = requests.post( - self.api_url, - headers=headers, - json=data, - timeout=30 # 30 second timeout - ) - - if response.status_code == 200: - return response.json() - elif response.status_code == 429: # Rate limit - wait_time = 2 ** attempt # Exponential backoff - logger.warning(f"DeepSeek API rate limit, waiting {wait_time} seconds before retry") - time.sleep(wait_time) - continue - else: - logger.error(f"DeepSeek API call failed: {response.status_code} - {response.text}") - if attempt < max_retries - 1: - time.sleep(1) - continue - else: - raise Exception(f"API call failed: {response.status_code}") - - except requests.exceptions.Timeout: - logger.warning(f"DeepSeek API request timeout, attempt {attempt + 1}/{max_retries}") - if attempt < max_retries - 1: - time.sleep(1) - continue - else: - raise Exception("DeepSeek API request timeout") - except Exception as e: - logger.error(f"DeepSeek API call exception: {e}") - if attempt < max_retries - 1: - time.sleep(1) - continue - else: - raise - - def _parse_deepseek_response(self, response): - """Parse DeepSeek response - enhanced version""" - try: - content = response['choices'][0]['message']['content'].strip() - logger.debug(f"DeepSeek raw response: {content}") - - # Remove possible code block markers - if content.startswith('```json'): - content = content[7:] - if content.endswith('```'): - content = content[:-3] - content = content.strip() - - # Try to parse JSON format - if '{' in content and '}' in content: - json_start = content.find('{') - json_end = content.rfind('}') + 1 - json_str = content[json_start:json_end] - - try: - parsed = json.loads(json_str) - result = { - 'action': parsed.get('action', 'HOLD').upper(), - 'confidence': parsed.get('confidence', 'MEDIUM').upper(), - 'reasoning': parsed.get('reasoning', ''), - 'raw_response': content - } - - # Validate action and confidence legality - if result['action'] not in ['BUY', 'SELL', 'HOLD']: - logger.warning(f"AI returned illegal action: {result['action']}, using HOLD") - result['action'] = 'HOLD' - - if result['confidence'] not in ['HIGH', 'MEDIUM', 'LOW']: - logger.warning(f"AI returned illegal confidence: {result['confidence']}, using MEDIUM") - result['confidence'] = 'MEDIUM' - - logger.info(f"AI parsing successful: {result['action']} (confidence: {result['confidence']})") - return result - - except json.JSONDecodeError as e: - logger.warning(f"JSON parsing failed: {e}, trying text analysis") - return self._parse_text_response(content) - else: - # Text analysis - return self._parse_text_response(content) - - except Exception as e: - logger.error(f"Error parsing DeepSeek response: {e}") - # Return default values to avoid system crash - return { - 'action': 'HOLD', - 'confidence': 'MEDIUM', - 'reasoning': f'Parsing error: {str(e)}', - 'raw_response': str(response) - } - - def _parse_text_response(self, text): - """Parse text response - enhanced version""" - text_lower = text.lower() - - # More precise action recognition - action = 'HOLD' - if any(word in text_lower for word in ['买入', '做多', 'buy', 'long', '上涨', '看涨']): - action = 'BUY' - elif any(word in text_lower for word in ['卖出', '做空', 'sell', 'short', '下跌', '看跌']): - action = 'SELL' - - # More precise confidence level recognition - confidence = 'MEDIUM' - if any(word in text_lower for word in ['强烈', '高信心', 'high', '非常', '强烈建议']): - confidence = 'HIGH' - elif any(word in text_lower for word in ['低', '低信心', 'low', '轻微', '谨慎']): - confidence = 'LOW' - - logger.info(f"Text parsing result: {action} (confidence: {confidence})") - - return { - 'action': action, - 'confidence': confidence, - 'reasoning': text, - 'raw_response': text - } - -class TechnicalAnalyzer: - """Technical Analyzer""" - - @staticmethod - def calculate_indicators(df): - """Calculate technical indicators (optimized version)""" - if df is None or len(df) < 20: # Lower minimum data requirement - logger.warning("Insufficient data, unable to calculate technical indicators") - return {} - - try: - indicators = {} - - # KDJ indicator - k, d, j = TechnicalAnalyzer.calculate_kdj(df) - if k is not None: - indicators['kdj'] = (k, d, j) - - # RSI indicator (multiple time periods) - rsi_14 = TechnicalAnalyzer.calculate_rsi(df['close'], 14) - rsi_7 = TechnicalAnalyzer.calculate_rsi(df['close'], 7) - rsi_21 = TechnicalAnalyzer.calculate_rsi(df['close'], 21) - if rsi_14 is not None: - indicators['rsi'] = rsi_14 - indicators['rsi_7'] = rsi_7 - indicators['rsi_21'] = rsi_21 - - # ATR indicator - atr = TechnicalAnalyzer.calculate_atr(df) - if atr is not None: - indicators['atr'] = atr - - # MACD indicator - macd_line, signal_line, histogram = TechnicalAnalyzer.calculate_macd(df['close']) - if macd_line is not None: - indicators['macd'] = (macd_line, signal_line, histogram) - - # Bollinger Bands (multiple standard deviations) - upper1, middle1, lower1 = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 1) - upper2, middle2, lower2 = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 2) - if upper1 is not None: - indicators['bollinger_1std'] = (upper1, middle1, lower1) - indicators['bollinger_2std'] = (upper2, middle2, lower2) - - # Moving Averages (multiple periods) - sma_20 = TechnicalAnalyzer.calculate_sma(df['close'], 20) - sma_50 = TechnicalAnalyzer.calculate_sma(df['close'], 50) - sma_100 = TechnicalAnalyzer.calculate_sma(df['close'], 100) - ema_12 = TechnicalAnalyzer.calculate_ema(df['close'], 12) - ema_26 = TechnicalAnalyzer.calculate_ema(df['close'], 26) - if sma_20 is not None: - indicators['sma_20'] = sma_20 - indicators['sma_50'] = sma_50 - indicators['sma_100'] = sma_100 - indicators['ema_12'] = ema_12 - indicators['ema_26'] = ema_26 - - # Stochastic Oscillator - k, d = TechnicalAnalyzer.calculate_stochastic(df) - if k is not None: - indicators['stochastic'] = (k, d) - - # Trend strength - indicators['trend_strength'] = TechnicalAnalyzer.calculate_trend_strength(df) - - # Volatility - indicators['volatility'] = TechnicalAnalyzer.calculate_volatility(df) - - # Current price - indicators['current_price'] = df['close'].iloc[-1] - - return indicators - - except Exception as e: - logger.error(f"Error calculating technical indicators: {e}") - return {} - - @staticmethod - def calculate_ema(prices, period): - """Calculate Exponential Moving Average""" - try: - return prices.ewm(span=period, adjust=False).mean() - except Exception as e: - logger.error(f"Error calculating EMA: {e}") - return None - - @staticmethod - def calculate_trend_strength(df, period=20): - """Calculate trend strength""" - try: - if len(df) < period: - return 0 - - # Use linear regression to calculate trend strength - x = np.arange(len(df)) - y = df['close'].values - - # Linear regression - slope, intercept = np.polyfit(x[-period:], y[-period:], 1) - - # Calculate R² value as trend strength - y_pred = slope * x[-period:] + intercept - ss_res = np.sum((y[-period:] - y_pred) ** 2) - ss_tot = np.sum((y[-period:] - np.mean(y[-period:])) ** 2) - r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0 - - return abs(r_squared) # Take absolute value, trend strength doesn't distinguish positive/negative - - except Exception as e: - logger.error(f"Error calculating trend strength: {e}") - return 0 - - @staticmethod - def detect_rsi_divergence(df, rsi_period=14): - """Detect RSI bullish divergence""" - try: - if len(df) < 30: # Need sufficient data - return False - - # Calculate RSI - rsi = TechnicalAnalyzer.calculate_rsi(df['close'], rsi_period) - if rsi is None: - return False - - # Use pandas methods to simplify calculation - # Find lowest points in recent 10 periods - recent_lows = df['low'].tail(10) - recent_rsi = rsi.tail(10) - - # Find price lowest point and RSI lowest point - min_price_idx = recent_lows.idxmin() - min_rsi_idx = recent_rsi.idxmin() - - # If price lowest point appears later than RSI lowest point, possible bullish divergence - if min_price_idx > min_rsi_idx: - # Check if price is making new lows while RSI is rising - price_trend = (recent_lows.iloc[-1] < recent_lows.iloc[-5]) - rsi_trend = (recent_rsi.iloc[-1] > recent_rsi.iloc[-5]) - - return price_trend and rsi_trend and recent_rsi.iloc[-1] < 40 - - return False - - except Exception as e: - logger.error(f"RSI divergence detection error: {e}") - return False - - @staticmethod - def detect_macd_divergence(df): - """Detect MACD bullish divergence""" - try: - if len(df) < 30: - return False - - # Calculate MACD, especially focus on histogram - _, _, histogram = TechnicalAnalyzer.calculate_macd(df['close']) - if histogram is None: - return False - - # Use recent data - recent_lows = df['low'].tail(10) - recent_hist = histogram.tail(10) - - # Find price lowest point and MACD histogram lowest point - min_price_idx = recent_lows.idxmin() - min_hist_idx = recent_hist.idxmin() - - # If price lowest point appears later than histogram lowest point, possible bullish divergence - if min_price_idx > min_hist_idx: - # Check price trend and histogram trend - price_trend = (recent_lows.iloc[-1] < recent_lows.iloc[-5]) - hist_trend = (recent_hist.iloc[-1] > recent_hist.iloc[-5]) - - # Histogram rising from negative area is stronger signal - hist_improving = recent_hist.iloc[-1] > recent_hist.iloc[-3] - - return price_trend and hist_trend and hist_improving - - return False - - except Exception as e: - logger.error(f"MACD divergence detection error: {e}") - return False - - @staticmethod - def volume_confirmation(df, period=5): - """Bottom volume confirmation""" - try: - if len(df) < period + 5: - return False - - current_volume = df['volume'].iloc[-1] - avg_volume = df['volume'].tail(period).mean() - - # Current volume significantly higher than average - volume_ratio = current_volume / avg_volume if avg_volume > 0 else 1 - - # Price falling but volume increasing (possible bottom accumulation) - price_change = (df['close'].iloc[-1] - df['close'].iloc[-2]) / df['close'].iloc[-2] - - if volume_ratio > 1.5 and price_change < 0: - return True - - return False - except Exception as e: - logger.error(f"Volume confirmation error: {e}") - return False - - @staticmethod - def detect_hammer_pattern(df, lookback=5): - """Detect hammer reversal pattern""" - try: - if len(df) < lookback + 1: - return False - - latest = df.iloc[-1] - body_size = abs(latest['close'] - latest['open']) - total_range = latest['high'] - latest['low'] - - # Avoid division by zero - if total_range == 0: - return False - - # Hammer characteristics: lower shadow at least 2x body, very short upper shadow - lower_shadow = min(latest['open'], latest['close']) - latest['low'] - upper_shadow = latest['high'] - max(latest['open'], latest['close']) - - is_hammer = (lower_shadow >= 2 * body_size and - upper_shadow <= body_size * 0.5 and - body_size > 0) - - # Needs to be in downtrend - prev_trend = df['close'].iloc[-lookback] > df['close'].iloc[-1] - - return is_hammer and prev_trend - - except Exception as e: - logger.error(f"Hammer pattern detection error: {e}") - return False - - @staticmethod - def detect_reversal_patterns(df): - """Detect bottom reversal patterns""" - reversal_signals = [] - - try: - # RSI bullish divergence - if TechnicalAnalyzer.detect_rsi_divergence(df): - reversal_signals.append({ - 'type': 'RSI_DIVERGENCE_BULLISH', - 'action': 'BUY', - 'strength': 'MEDIUM', - 'description': 'RSI bullish divergence, momentum divergence bullish' - }) - - # MACD bullish divergence - if TechnicalAnalyzer.detect_macd_divergence(df): - reversal_signals.append({ - 'type': 'MACD_DIVERGENCE_BULLISH', - 'action': 'BUY', - 'strength': 'MEDIUM', - 'description': 'MACD histogram bullish divergence, momentum strengthening' - }) - - # Volume confirmation - if TechnicalAnalyzer.volume_confirmation(df): - reversal_signals.append({ - 'type': 'VOLUME_CONFIRMATION', - 'action': 'BUY', - 'strength': 'LOW', - 'description': 'Bottom volume increase, signs of capital entry' - }) - - # Add hammer pattern detection - if TechnicalAnalyzer.detect_hammer_pattern(df): - reversal_signals.append({ - 'type': 'HAMMER_PATTERN', - 'action': 'BUY', - 'strength': 'LOW', - 'description': 'Hammer pattern, short-term reversal signal' - }) - - except Exception as e: - logger.error(f"Reversal pattern detection error: {e}") - - return reversal_signals - - @staticmethod - def calculate_support_resistance(df, period=20): - """Calculate support resistance levels (fixed version)""" - try: - # Recent high resistance - resistance = df['high'].rolling(window=period).max().iloc[-1] - - # Recent low support - support = df['low'].rolling(window=period).min().iloc[-1] - - # Dynamic support resistance (based on Bollinger Bands) - # Fix: correctly receive three return values from Bollinger Bands - upper_bb, middle_bb, lower_bb = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 2) - - # Check if Bollinger Band calculation successful - if upper_bb is not None and lower_bb is not None: - resistance_bb = upper_bb.iloc[-1] if hasattr(upper_bb, 'iloc') else upper_bb - support_bb = lower_bb.iloc[-1] if hasattr(lower_bb, 'iloc') else lower_bb - else: - # If Bollinger Band calculation fails, use static values as backup - resistance_bb = resistance - support_bb = support - - current_price = df['close'].iloc[-1] - - return { - 'static_resistance': resistance, - 'static_support': support, - 'dynamic_resistance': resistance_bb, - 'dynamic_support': support_bb, - 'current_vs_resistance': (current_price - resistance) / resistance * 100 if resistance > 0 else 0, - 'current_vs_support': (current_price - support) / support * 100 if support > 0 else 0 - } - except Exception as e: - logger.error(f"Error calculating support resistance: {e}") - # Return default values to avoid subsequent errors - current_price = df['close'].iloc[-1] if len(df) > 0 else 0 - return { - 'static_resistance': current_price * 1.1 if current_price > 0 else 0, - 'static_support': current_price * 0.9 if current_price > 0 else 0, - 'dynamic_resistance': current_price * 1.05 if current_price > 0 else 0, - 'dynamic_support': current_price * 0.95 if current_price > 0 else 0, - 'current_vs_resistance': 0, - 'current_vs_support': 0 - } - - @staticmethod - def calculate_volatility(df, period=20): - """Calculate volatility""" - try: - returns = df['close'].pct_change().dropna() - volatility = returns.rolling(window=period).std() * np.sqrt(365) # Annualized volatility - return volatility.iloc[-1] if len(volatility) > 0 else 0 - except Exception as e: - logger.error(f"Error calculating volatility: {e}") - return 0 - - @staticmethod - def generate_weighted_signals(df): - """Generate weighted technical signals""" - signals = [] - weights = { - 'KDJ_GOLDEN_CROSS': 0.15, - 'MACD_GOLDEN_CROSS': 0.20, - 'MA_GOLDEN_CROSS': 0.25, - 'RSI_OVERSOLD': 0.15, - 'BOLLINGER_LOWER_TOUCH': 0.15, - 'STOCH_GOLDEN_CROSS': 0.10 - } - - # Calculate various indicator signals - technical_signals = TechnicalAnalyzer.generate_signals(df) - - # Calculate weighted score - total_score = 0 - signal_count = 0 - - for signal in technical_signals: - weight = weights.get(signal['type'], 0.05) - strength_multiplier = 1.0 if signal['strength'] == 'STRONG' else 0.7 - total_score += weight * strength_multiplier - signal_count += 1 - - # Normalize score - normalized_score = min(1.0, total_score) - - # Determine action - if normalized_score > 0.6: - action = 'BUY' - elif normalized_score < 0.3: - action = 'SELL' - else: - action = 'HOLD' - - return { - 'score': normalized_score, - 'action': action, - 'signal_count': signal_count, - 'signals': technical_signals, - 'confidence': 'HIGH' if normalized_score > 0.7 or normalized_score < 0.2 else 'MEDIUM' - } - - @staticmethod - def calculate_kdj(df, n=9, m1=3, m2=3): - """Calculate KDJ indicator""" - try: - if len(df) < n: - return pd.Series([np.nan] * len(df)), pd.Series([np.nan] * len(df)), pd.Series([np.nan] * len(df)) - - low_list = df['low'].rolling(window=n, min_periods=1).min() - high_list = df['high'].rolling(window=n, min_periods=1).max() - - # Avoid division by zero error - denominator = high_list - low_list - denominator = denominator.replace(0, np.nan) - - rsv = ((df['close'] - low_list) / denominator) * 100 - rsv = rsv.fillna(50) - - k_series = rsv.ewm(span=m1-1, adjust=False).mean() - d_series = k_series.ewm(span=m2-1, adjust=False).mean() - j_series = 3 * k_series - 2 * d_series - - return k_series, d_series, j_series - - except Exception as e: - logger.error(f"Error calculating KDJ: {e}") - return None, None, None - - @staticmethod - def calculate_rsi(prices, period=14): - """Calculate RSI""" - try: - if len(prices) < period: - return pd.Series([np.nan] * len(prices)) - - delta = prices.diff() - gain = (delta.where(delta > 0, 0)).rolling(window=period, min_periods=1).mean() - loss = (-delta.where(delta < 0, 0)).rolling(window=period, min_periods=1).mean() - - # Avoid division by zero error - rs = gain / loss.replace(0, np.nan) - rsi = 100 - (100 / (1 + rs)) - return rsi.fillna(50) - - except Exception as e: - logger.error(f"Error calculating RSI: {e}") - return None - - @staticmethod - def calculate_atr(df, period=14): - """Calculate ATR""" - try: - if len(df) < period: - return pd.Series([np.nan] * len(df)) - - high_low = df['high'] - df['low'] - high_close = np.abs(df['high'] - df['close'].shift()) - low_close = np.abs(df['low'] - df['close'].shift()) - - true_range = np.maximum(high_low, np.maximum(high_close, low_close)) - atr = true_range.rolling(window=period, min_periods=1).mean() - return atr - - except Exception as e: - logger.error(f"Error calculating ATR: {e}") - return None - - @staticmethod - def calculate_macd(prices, fast_period=12, slow_period=26, signal_period=9): - """Calculate MACD""" - try: - ema_fast = prices.ewm(span=fast_period, adjust=False).mean() - ema_slow = prices.ewm(span=slow_period, adjust=False).mean() - macd_line = ema_fast - ema_slow - signal_line = macd_line.ewm(span=signal_period, adjust=False).mean() - histogram = macd_line - signal_line - return macd_line, signal_line, histogram - except Exception as e: - logger.error(f"Error calculating MACD: {e}") - return None, None, None - - @staticmethod - def calculate_bollinger_bands(prices, period=20, std_dev=2): - """Calculate Bollinger Bands""" - try: - if len(prices) < period: - logger.warning(f"Data length {len(prices)} insufficient, unable to calculate {period}-period Bollinger Bands") - return None, None, None - - middle = prices.rolling(window=period).mean() - std = prices.rolling(window=period).std() - - # Handle NaN standard deviation - std = std.fillna(0) - - upper = middle + (std * std_dev) - lower = middle - (std * std_dev) - - return upper, middle, lower - except Exception as e: - logger.error(f"Error calculating Bollinger Bands: {e}") - return None, None, None - - @staticmethod - def calculate_sma(prices, period): - """Calculate Simple Moving Average""" - try: - return prices.rolling(window=period).mean() - except Exception as e: - logger.error(f"Error calculating SMA: {e}") - return None - - @staticmethod - def calculate_stochastic(df, k_period=14, d_period=3): - """Calculate Stochastic Oscillator""" - try: - low_min = df['low'].rolling(window=k_period).min() - high_max = df['high'].rolling(window=k_period).max() - k = 100 * ((df['close'] - low_min) / (high_max - low_min)) - d = k.rolling(window=d_period).mean() - return k, d - except Exception as e: - logger.error(f"Error calculating Stochastic: {e}") - return None, None - - @staticmethod - def generate_signals(df): - """Generate technical signals""" - signals = [] - - try: - # KDJ - k, d, j = TechnicalAnalyzer.calculate_kdj(df) - if k is not None and len(k) > 1: - latest_k, latest_d, latest_j = k.iloc[-1], d.iloc[-1], j.iloc[-1] - prev_k, prev_d, prev_j = k.iloc[-2], d.iloc[-2], j.iloc[-2] - - if prev_k <= prev_d and latest_k > latest_d: - signals.append({'type': 'KDJ_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) - elif prev_k >= prev_d and latest_k < latest_d: - signals.append({'type': 'KDJ_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) - - if latest_j < 20: - signals.append({'type': 'KDJ_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'}) - elif latest_j > 80: - signals.append({'type': 'KDJ_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'}) - - # RSI - rsi = TechnicalAnalyzer.calculate_rsi(df['close']) - if rsi is not None and len(rsi) > 0: - latest_rsi = rsi.iloc[-1] - - if latest_rsi < 30: - signals.append({'type': 'RSI_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'}) - elif latest_rsi > 70: - signals.append({'type': 'RSI_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'}) - - # MACD - macd_line, signal_line, _ = TechnicalAnalyzer.calculate_macd(df['close']) - if macd_line is not None and len(macd_line) > 1: - latest_macd = macd_line.iloc[-1] - latest_signal = signal_line.iloc[-1] - prev_macd = macd_line.iloc[-2] - prev_signal = signal_line.iloc[-2] - - if prev_macd <= prev_signal and latest_macd > latest_signal: - signals.append({'type': 'MACD_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) - elif prev_macd >= prev_signal and latest_macd < latest_signal: - signals.append({'type': 'MACD_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) - - # Bollinger Bands - upper, middle, lower = TechnicalAnalyzer.calculate_bollinger_bands(df['close']) - if upper is not None: - latest_close = df['close'].iloc[-1] - if latest_close <= lower.iloc[-1]: - signals.append({'type': 'BOLlinger_LOWER_TOUCH', 'action': 'BUY', 'strength': 'MEDIUM'}) - elif latest_close >= upper.iloc[-1]: - signals.append({'type': 'BOLlinger_UPPER_TOUCH', 'action': 'SELL', 'strength': 'MEDIUM'}) - - # Moving Averages - sma_short = TechnicalAnalyzer.calculate_sma(df['close'], 50) - sma_long = TechnicalAnalyzer.calculate_sma(df['close'], 200) - if sma_short is not None and sma_long is not None and len(sma_short) > 1: - latest_short = sma_short.iloc[-1] - latest_long = sma_long.iloc[-1] - prev_short = sma_short.iloc[-2] - prev_long = sma_long.iloc[-2] - - if prev_short <= prev_long and latest_short > latest_long: - signals.append({'type': 'MA_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) - elif prev_short >= prev_long and latest_short < latest_long: - signals.append({'type': 'MA_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) - - # Stochastic - k, d = TechnicalAnalyzer.calculate_stochastic(df) - if k is not None and len(k) > 1: - latest_k = k.iloc[-1] - latest_d = d.iloc[-1] - prev_k = k.iloc[-2] - prev_d = d.iloc[-2] - - if latest_k < 20: - signals.append({'type': 'STOCH_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'}) - elif latest_k > 80: - signals.append({'type': 'STOCH_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'}) - - if prev_k <= prev_d and latest_k > latest_d and latest_k < 80: - signals.append({'type': 'STOCH_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) - elif prev_k >= prev_d and latest_k < latest_d and latest_k > 20: - signals.append({'type': 'STOCH_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) - - except Exception as e: - logger.error(f"Error generating technical signals: {e}") - - return signals - -class RiskManager: - """Risk Control Manager (Dynamic Position Management Version)""" - - def __init__(self, api): - self.api = api - self.max_total_position = float(os.getenv('MAX_TOTAL_POSITION', '1')) # Maximum total position 100% - self.max_single_position = float(os.getenv('MAX_SINGLE_POSITION', '0.3')) # Single currency maximum position 30% - self.stop_loss = float(os.getenv('STOP_LOSS', '0.05')) # Stop-loss 5% - self.take_profit = float(os.getenv('TAKE_PROFIT', '0.15')) # Take-profit 15% - self.max_daily_trades = int(os.getenv('MAX_DAILY_TRADES', '10')) # Maximum daily trades - self.dust_threshold_value = 1.0 # Dust position value threshold (USDT) - self.low_position_ratio = 0.05 # Low position ratio threshold (5%) - - self.trailing_stop_percent = float(os.getenv('TRAILING_STOP_PERCENT', '0.03')) # Trailing stop percentage - self.monitor_interval = int(os.getenv('MONITOR_INTERVAL', '300')) # Monitoring interval seconds - - # Dynamic position parameters - self.volatility_adjustment = True - self.trend_adjustment = True - self.market_sentiment_adjustment = True - - # Trade records - self.daily_trade_count = {} - self.db = DatabaseManager() - - def calculate_total_assets(self): - """Calculate total assets (in USDT)""" - try: - # Get all currency balances - balances = self.api.get_currency_balances() - if not balances: - logger.error("Unable to get currency balances") - return 0.0, 0.0, {} - - total_usdt_value = 0.0 - position_details = {} - - # Calculate USDT balance (including available and frozen) - usdt_balance = balances.get('USDT', {}) - usdt_avail = usdt_balance.get('amount', 0.0) - usdt_frozen = usdt_balance.get('frozen', 0.0) - total_usdt = usdt_avail + usdt_frozen - total_usdt_value += total_usdt - - # Calculate value of other currencies - for currency, balance_info in balances.items(): - if currency != 'USDT': - avail = balance_info.get('amount', 0.0) - frozen = balance_info.get('frozen', 0.0) - total_amount = avail + frozen - if total_amount > 0: - symbol = f"{currency}-USDT" - current_price = self.api.get_current_price(symbol) - if current_price: - value = total_amount * current_price - total_usdt_value += value - position_details[symbol] = { - 'amount': total_amount, # Total quantity (available + frozen) - 'value': value, - 'current_price': current_price - } - else: - logger.warning(f"Unable to get {currency} price, skipping value calculation") - - logger.debug(f"Total asset calculation: USDT={total_usdt:.2f}(available{usdt_avail:.2f}+frozen{usdt_frozen:.2f}), position value={total_usdt_value - total_usdt:.2f}, total={total_usdt_value:.2f}") - - return total_usdt_value, usdt_avail, position_details - - except Exception as e: - logger.error(f"Error calculating total assets: {e}") - return 0.0, 0.0, {} - - def get_position_ratio(self, symbol): - """Get specified currency's position ratio""" - try: - total_assets, usdt_balance, positions = self.calculate_total_assets() - if total_assets == 0: - return 0.0 - - position_value = positions.get(symbol, {}).get('value', 0.0) - return position_value / total_assets - - except Exception as e: - logger.error(f"Error calculating position ratio: {e}") - return 0.0 - - def get_available_usdt_ratio(self): - """Get available USDT ratio""" - try: - total_assets, usdt_balance, _ = self.calculate_total_assets() - if total_assets == 0: - return 0.0 - - return usdt_balance / total_assets - - except Exception as e: - logger.error(f"Error calculating USDT ratio: {e}") - return 0.0 - - def get_position_size(self, symbol, confidence, current_price): - """Calculate position size based on confidence level and USDT availability""" - try: - total_assets, usdt_balance, positions = self.calculate_total_assets() - - if total_assets == 0: - logger.warning("Total assets is 0, unable to calculate position") - return 0.0 - - # Get current position value - current_pos_value = positions.get(symbol, {}).get('value', 0.0) - - # Calculate USDT available ratio - usdt_ratio = usdt_balance / total_assets - - # Adjust maximum position based on USDT availability - adjusted_max_single = self.max_single_position * min(1.0, usdt_ratio * 3) # More USDT allows larger positions - - # Calculate maximum additional position - max_single_add = max(0, total_assets * adjusted_max_single - current_pos_value) - max_total_add = max(0, total_assets * self.max_total_position - (total_assets - usdt_balance)) - - max_add = min(max_single_add, max_total_add, usdt_balance) - - # Adjust based on confidence level - multiplier = { - 'HIGH': 0.8, # High confidence uses 80% of available amount - 'MEDIUM': 0.5, # Medium confidence uses 50% - 'LOW': 0.2 # Low confidence uses 20% - }.get(confidence, 0.2) - - position_size = max_add * multiplier - - # Ensure doesn't exceed available USDT balance - position_size = min(position_size, usdt_balance) - - logger.info(f"Position calculation: {symbol}, total assets=${total_assets:.2f}, USDT=${usdt_balance:.2f}, " - f"current position=${current_pos_value:.2f}, suggested position=${position_size:.2f}") - - return position_size - - except Exception as e: - logger.error(f"Error calculating position size: {e}") - return 0.0 - - def get_dynamic_position_size(self, symbol, confidence, technical_indicators, current_price): - """Dynamic position calculation""" - try: - # Base position calculation - base_size = self.get_position_size(symbol, confidence, current_price) - if base_size == 0: - return 0.0 - - # Get market state parameters - volatility = technical_indicators.get('volatility', 0) - trend_strength = technical_indicators.get('trend_strength', 0) - market_state = self.assess_market_state(technical_indicators) - - # Volatility adjustment (high volatility reduces position) - volatility_factor = self._calculate_volatility_factor(volatility) - - # Trend strength adjustment (strong trend increases position) - trend_factor = self._calculate_trend_factor(trend_strength) - - # Market state adjustment - market_factor = self._calculate_market_factor(market_state) - - # Confidence level adjustment - confidence_factor = self._calculate_confidence_factor(confidence) - - # Calculate dynamic position - dynamic_size = base_size * volatility_factor * trend_factor * market_factor * confidence_factor - - logger.info(f"Dynamic position calculation: {symbol}, base={base_size:.2f}, " - f"volatility factor={volatility_factor:.2f}, trend factor={trend_factor:.2f}, " - f"market factor={market_factor:.2f}, confidence factor={confidence_factor:.2f}, " - f"final={dynamic_size:.2f}") - - return dynamic_size - - except Exception as e: - logger.error(f"Dynamic position calculation error: {e}") - return self.get_position_size(symbol, confidence, current_price) - - def _calculate_volatility_factor(self, volatility): - """Calculate volatility adjustment factor""" - if not self.volatility_adjustment: - return 1.0 - - # Annualized volatility conversion and adjustment - if volatility > 0.8: # 80%+ annualized volatility - return 0.3 - elif volatility > 0.6: # 60-80% - return 0.5 - elif volatility > 0.4: # 40-60% - return 0.7 - elif volatility > 0.2: # 20-40% - return 0.9 - else: # <20% - return 1.0 - - def _calculate_trend_factor(self, trend_strength): - """Calculate trend strength adjustment factor""" - if not self.trend_adjustment: - return 1.0 - - if trend_strength > 0.7: # Strong trend - return 1.3 - elif trend_strength > 0.4: # Medium trend - return 1.1 - elif trend_strength > 0.2: # Weak trend - return 1.0 - else: # No trend - return 0.8 - - def _calculate_market_factor(self, market_state): - """Calculate market state adjustment factor""" - if not self.market_sentiment_adjustment: - return 1.0 - - factors = { - 'STRONG_BULL': 1.2, - 'BULL': 1.1, - 'NEUTRAL': 1.0, - 'BEAR': 0.7, - 'STRONG_BEAR': 0.5 - } - return factors.get(market_state, 1.0) - - def _calculate_confidence_factor(self, confidence): - """Calculate confidence level adjustment factor""" - factors = { - 'HIGH': 1.0, - 'MEDIUM': 0.7, - 'LOW': 0.4 - } - return factors.get(confidence, 0.5) - - def assess_market_state(self, technical_indicators): - """Assess market state""" - try: - # Get key indicators - rsi = technical_indicators.get('rsi', 50) - if hasattr(rsi, '__len__'): - rsi = rsi.iloc[-1] - - macd_line, signal_line, _ = technical_indicators.get('macd', (0, 0, 0)) - if hasattr(macd_line, '__len__'): - macd_line, signal_line = macd_line.iloc[-1], signal_line.iloc[-1] - - sma_20 = technical_indicators.get('sma_20', 0) - sma_50 = technical_indicators.get('sma_50', 0) - current_price = technical_indicators.get('current_price', 0) - - if hasattr(sma_20, '__len__'): - sma_20, sma_50 = sma_20.iloc[-1], sma_50.iloc[-1] - - # Calculate bullish signal score - bull_signals = 0 - total_signals = 0 - - # RSI signal - if rsi > 50: bull_signals += 1 - total_signals += 1 - - # MACD signal - if macd_line > signal_line: bull_signals += 1 - total_signals += 1 - - # Moving average signal - if current_price > sma_20: bull_signals += 1 - if current_price > sma_50: bull_signals += 1 - total_signals += 2 - - # Determine market state - bull_ratio = bull_signals / total_signals - - if bull_ratio >= 0.8: - return 'STRONG_BULL' - elif bull_ratio >= 0.6: - return 'BULL' - elif bull_ratio >= 0.4: - return 'NEUTRAL' - elif bull_ratio >= 0.2: - return 'BEAR' - else: - return 'STRONG_BEAR' - - except Exception as e: - logger.error(f"Error assessing market state: {e}") - return 'NEUTRAL' - - def is_dust_position(self, symbol, current_price, base_amount): - """Check if dust position""" - hold_value = base_amount * current_price if current_price else 0 - return hold_value < self.dust_threshold_value - - def is_low_position_ratio(self, symbol, total_assets, current_price, base_amount): - """Check if position ratio is low""" - hold_value = base_amount * current_price if current_price else 0 - return (hold_value / total_assets) < self.low_position_ratio if total_assets > 0 else False - - def can_trade(self, symbol, amount, current_price): - """Check if can trade""" - try: - # Check daily trade count - today = datetime.now().date() - if symbol not in self.daily_trade_count: - self.daily_trade_count[symbol] = {'date': today, 'count': 0} - - if self.daily_trade_count[symbol]['date'] != today: - self.daily_trade_count[symbol] = {'date': today, 'count': 0} - - if self.daily_trade_count[symbol]['count'] >= self.max_daily_trades: - logger.warning(f"{symbol} daily trade count reached {self.max_daily_trades} limit") - return False - - return True - - except Exception as e: - logger.error(f"Error checking trade conditions: {e}") - return False - - def increment_trade_count(self, symbol): - """Increment trade count""" - try: - today = datetime.now().date() - if symbol in self.daily_trade_count and self.daily_trade_count[symbol]['date'] == today: - self.daily_trade_count[symbol]['count'] += 1 - except Exception as e: - logger.error(f"Error incrementing trade count: {e}") - -class TradingStrategy: - """Trading Strategy (Comprehensive Optimized Version)""" - - def __init__(self, symbol, config, api, risk_manager, deepseek): - self.symbol = symbol - self.config = config - self.api = api - self.risk_manager = risk_manager - self.deepseek = deepseek - self.technical_analyzer = TechnicalAnalyzer() - self.db = DatabaseManager() # Use default configuration - - # Position status - self.base_amount = 0.0 - self.entry_price = 0.0 - - # Dynamic stop-loss monitoring - using environment variable configured interval - self.monitor_interval = 300 # Default 5 minutes - if hasattr(risk_manager, 'monitor_interval'): - self.monitor_interval = risk_manager.monitor_interval - self.monitor_running = False - self.monitor_thread = None - - # Running status control - self.strategy_running = False - - # Load position status - self.load_position() - - # Start dynamic stop-loss monitoring - self.start_dynamic_stop_monitor() - - def load_position(self): - """Load position status from database""" - try: - position_data = self.db.load_position(self.symbol) - if position_data: - self.base_amount = position_data['base_amount'] - self.entry_price = position_data['entry_price'] - logger.info(f"Loaded {self.symbol} position: currency quantity={self.base_amount:.10f}, entry price=${self.entry_price:.2f}") - else: - # If no database record, sync with exchange position - self.sync_with_exchange() - except Exception as e: - logger.error(f"Error loading position status: {e}") - self.sync_with_exchange() - - def save_position(self): - """Save position status to database""" - try: - if self.base_amount > 0: - self.db.save_position(self.symbol, self.base_amount, self.entry_price) - else: - # If position is 0, delete record - self.db.delete_position(self.symbol) - # Also delete dynamic stop record - self.db.delete_dynamic_stop(self.symbol) - except Exception as e: - logger.error(f"Error saving position status: {e}") - - def sync_with_exchange(self): - """Sync with exchange position""" - try: - logger.debug(f"Starting {self.symbol} position status sync") - - # Get all currency balances - balances = self.api.get_currency_balances() - if not balances: - logger.warning("Unable to get currency balances") - return False - - # Extract base currency - base_currency = self.symbol.split('-')[0] - - # Update local balance - if base_currency in balances: - new_base_amount = balances[base_currency].get('amount', 0.0) - - # If position quantity changes, print log - if abs(new_base_amount - self.base_amount) > 1e-10: - logger.info(f"Synced {self.symbol} latest balance: {base_currency}={new_base_amount:.10f}") - - # If position decreases but entry price not updated, keep original entry price (for calculating profit/loss) - if new_base_amount < self.base_amount and self.entry_price > 0: - # Position decreased, reset entry price to conservative estimate (current price) - if new_base_amount <= 0: - self.entry_price = 0 - logger.info(f"{self.symbol} position cleared, reset entry price to 0") - else: - current_price = self.api.get_current_price(self.symbol) - if current_price: - self.entry_price = current_price - logger.info(f"{self.symbol} partial sell, reset entry price to current price: ${current_price:.2f}") - else: - logger.warning(f"{self.symbol} partial sell but unable to get current price, keep original entry price") - - # If from no position to having position, and no entry price record - elif self.base_amount == 0 and new_base_amount > 0 and self.entry_price == 0: - # Try to load historical entry price from database - position_data = self.db.load_position(self.symbol) - if position_data and position_data['entry_price'] > 0: - self.entry_price = position_data['entry_price'] - logger.info(f"Restored entry price from database: ${self.entry_price:.2f}") - else: - # Get current price as reference (but note this is reference price) - current_price = self.api.get_current_price(self.symbol) - if current_price: - self.entry_price = current_price - logger.info(f"Set reference entry price: ${current_price:.2f} (Note: This is reference price, actual entry price may differ)") - - self.base_amount = new_base_amount - - else: - self.base_amount = 0.0 - # Don't clear entry price, keep for historical record analysis - logger.info(f"Synced {self.symbol}: No position") - - # Save position status - self.save_position() - return True - - except Exception as e: - logger.error(f"Error syncing exchange position: {e}") - return False - - def get_actual_position_from_exchange(self): - """Get actual position data from exchange""" - try: - balances = self.api.get_currency_balances() - if not balances: - return 0.0 - - base_currency = self.symbol.split('-')[0] - return balances.get(base_currency, {}).get('amount', 0.0) - - except Exception as e: - logger.error(f"Error getting exchange position: {e}") - return 0.0 - - def analyze_market(self): - """Comprehensive market analysis (optimized version)""" - try: - # Get market data - df = self.api.get_market_data(self.symbol, self.config['timeframe']) - if df is None or len(df) < 20: - logger.error(f"Failed to get {self.symbol} market data") - return None - - # Calculate technical indicators - technical_indicators = self.technical_analyzer.calculate_indicators(df) - if not technical_indicators: - logger.warning(f"Failed to calculate {self.symbol} technical indicators") - return None - - # Weighted technical signals - weighted_signals = self.technical_analyzer.generate_weighted_signals(df) - - # DeepSeek AI analysis - deepseek_analysis = self.deepseek.analyze_market(self.symbol, df, technical_indicators) - - # Technical signals - technical_signals = self.technical_analyzer.generate_signals(df) - - return { - 'deepseek': deepseek_analysis, - 'technical_signals': technical_signals, - 'weighted_signals': weighted_signals, - 'technical_indicators': technical_indicators, - 'current_price': df['close'].iloc[-1], - 'market_data': df - } - - except Exception as e: - logger.error(f"{self.symbol} market analysis error: {e}") - return None - - def make_enhanced_decision(self, analysis): - """Enhanced trading decision""" - if analysis is None: - return {'action': 'HOLD', 'confidence': 'LOW', 'reason': 'Insufficient analysis data'} - - deepseek_signal = analysis['deepseek'] - weighted_signals = analysis['weighted_signals'] - technical_indicators = analysis['technical_indicators'] - market_data = analysis['market_data'] - - # Build decision matrix - decision_matrix = self._build_decision_matrix(deepseek_signal, weighted_signals, technical_indicators, market_data) - - return self._finalize_decision(decision_matrix) - - def _build_decision_matrix(self, ai_signal, weighted_signal, technical_indicators, market_data): - """Build decision matrix""" - matrix = { - 'ai_weight': 0.6, # AI analysis weight - 'technical_weight': 0.2, # Traditional technical indicator weight - 'reversal_weight': 0.05, # Reversal signal weight - 'support_resistance_weight': 0.1, # Support resistance weight - 'market_weight': 0.05 # Market state weight - } - - # AI signal scoring - ai_score = self._score_ai_signal(ai_signal) if ai_signal else 0.5 - - # Technical signal scoring - tech_score = self._score_technical_signal(weighted_signal) - - # Market state scoring - market_score = self._score_market_condition(technical_indicators) - - # Reversal signal scoring - reversal_signals = TechnicalAnalyzer.detect_reversal_patterns(market_data) - reversal_score = 0.5 - if reversal_signals: - # Score based on reversal signal quantity and strength - strong_signals = [s for s in reversal_signals if s['strength'] == 'STRONG'] - reversal_score = 0.5 + (len(strong_signals) * 0.1) + (len(reversal_signals) * 0.05) - reversal_score = min(0.9, reversal_score) - - # Support resistance scoring - sr_levels = TechnicalAnalyzer.calculate_support_resistance(market_data) - sr_score = 0.5 - if sr_levels: - # Near support level adds points, near resistance level subtracts points - if sr_levels['current_vs_support'] > -2: # Very close to support - sr_score = 0.7 - elif sr_levels['current_vs_resistance'] < 2: # Very close to resistance - sr_score = 0.3 - - # Comprehensive scoring - total_score = ( - ai_score * matrix['ai_weight'] + - tech_score * matrix['technical_weight'] + - reversal_score * matrix['reversal_weight'] + - sr_score * matrix['support_resistance_weight'] + - market_score * matrix['market_weight'] - ) - logger.info(f"[{self.symbol}] Decision matrix details: " - f"AI score={ai_score:.3f}, technical score={tech_score:.3f}, " - f"market score={market_score:.3f}, reversal score={reversal_score:.3f}, " - f"support resistance score={sr_score:.3f}, total score={total_score:.3f}") - - # Determine action and confidence - if total_score > 0.6: - action = 'BUY' - confidence = 'HIGH' if total_score > 0.75 else 'MEDIUM' - elif total_score < 0.4: - action = 'SELL' - confidence = 'HIGH' if total_score < 0.25 else 'MEDIUM' - else: - action = 'HOLD' - confidence = 'MEDIUM' if abs(total_score - 0.5) > 0.1 else 'LOW' - - return { - 'total_score': total_score, - 'action': action, - 'confidence': confidence, - 'components': { - 'ai_score': ai_score, - 'tech_score': tech_score, - 'market_score': market_score, - 'reversal_score': reversal_score, - 'sr_score': sr_score - } - } - - def _score_ai_signal(self, ai_signal): - """AI signal scoring""" - if not ai_signal: - logger.warning("AI signal is empty, using neutral score") - return 0.5 - - action = ai_signal.get('action', 'HOLD') - confidence = ai_signal.get('confidence', 'MEDIUM') - - # Correction: Redesign scoring logic to ensure score has differentiation - if action == 'BUY': - base_score = 0.7 # Buy base score - elif action == 'SELL': - base_score = 0.3 # Sell base score - else: # HOLD - base_score = 0.5 # Hold neutral score - - # Confidence adjustment - ensure final score is within reasonable range - confidence_adjustments = { - 'HIGH': 0.2, # High confidence +0.2 - 'MEDIUM': 0.0, # Medium confidence unchanged - 'LOW': -0.2 # Low confidence -0.2 - } - - adjustment = confidence_adjustments.get(confidence, 0.0) - final_score = base_score + adjustment - - # Ensure within 0-1 range - final_score = max(0.1, min(0.9, final_score)) - - logger.debug(f"AI signal scoring: action={action}, confidence={confidence}, " - f"base={base_score}, adjustment={adjustment}, final={final_score}") - - return final_score - - def _score_technical_signal(self, weighted_signal): - """Technical signal scoring""" - if not weighted_signal: - logger.warning("Technical signal is empty, using neutral score") - return 0.5 - - action = weighted_signal.get('action', 'HOLD') - score = weighted_signal.get('score', 0.5) - signal_count = weighted_signal.get('signal_count', 0) - - # Adjust based on signal strength and quantity - if action == 'BUY': - # Buy signal: 0.5-1.0 range - if score > 0.7 and signal_count >= 3: - final_score = 0.8 - elif score > 0.6: - final_score = 0.7 - else: - final_score = 0.6 - elif action == 'SELL': - # Sell signal: 0.0-0.5 range - if score < 0.3 and signal_count >= 3: - final_score = 0.2 - elif score < 0.4: - final_score = 0.3 - else: - final_score = 0.4 - else: # HOLD - final_score = 0.5 - - logger.debug(f"Technical signal scoring: action={action}, score={score}, " - f"signal_count={signal_count}, final={final_score}") - - return final_score - - def _score_market_condition(self, technical_indicators): - """Market state scoring""" - try: - # Assess market state based on multiple indicators - rsi = technical_indicators.get('rsi', 50) - if hasattr(rsi, '__len__'): - rsi = rsi.iloc[-1] - - trend_strength = technical_indicators.get('trend_strength', 0) - volatility = technical_indicators.get('volatility', 0) - - # RSI scoring (30-70 is neutral) - rsi_score = 0.5 - if rsi < 30: # Oversold - rsi_score = 0.8 - elif rsi > 70: # Overbought - rsi_score = 0.2 - - # Trend strength scoring - trend_score = 0.5 + (trend_strength * 0.3) # Strong trend favors buying - - # Volatility scoring (medium volatility is best) - vol_score = 0.7 if 0.3 < volatility < 0.6 else 0.5 - - # Comprehensive scoring - return (rsi_score + trend_score + vol_score) / 3 - - except Exception as e: - logger.error(f"Market state scoring error: {e}") - return 0.5 - - def _finalize_decision(self, decision_matrix): - """Final decision""" - action = decision_matrix['action'] - confidence = decision_matrix['confidence'] - total_score = decision_matrix['total_score'] - - reason = f"Comprehensive score: {total_score:.3f} (AI: {decision_matrix['components']['ai_score']:.3f}, " - reason += f"Technical: {decision_matrix['components']['tech_score']:.3f}, " - reason += f"Market: {decision_matrix['components']['market_score']:.3f}," - reason += f"Reversal: {decision_matrix['components']['reversal_score']:.3f}," - reason += f"Support Resistance: {decision_matrix['components']['sr_score']:.3f})," - - return { - 'action': action, - 'confidence': confidence, - 'reason': reason, - 'total_score': total_score, - 'source': 'Enhanced Decision Matrix' - } - - def execute_trade(self, decision, current_price): - """Execute trade""" - if decision['action'] == 'HOLD': - logger.info(f"[{self.symbol}] Decision is HOLD, not executing trade") - return - - # Sync exchange position - self.sync_with_exchange() - actual_position = self.base_amount # Use synced position - - logger.info(f"[{self.symbol}] Current position: {self.base_amount:.10f}") - - # Only get needed total asset information - total_assets, available_usdt, _ = self.risk_manager.calculate_total_assets() - - if decision['action'] == 'BUY': - # Get market analysis for dynamic position calculation - market_analysis = self.analyze_market() - if market_analysis is None: - logger.warning(f"[{self.symbol}] Unable to get market analysis, using base position") - buy_amount = self.risk_manager.get_position_size(self.symbol, decision['confidence'], current_price) - else: - # Use dynamic position calculation - buy_amount = self.risk_manager.get_dynamic_position_size( - self.symbol, decision['confidence'], market_analysis['technical_indicators'], current_price - ) - - if buy_amount == 0: - logger.warning(f"[{self.symbol}] Calculated position size is 0, skipping buy") - return - - # Get exchange minimum trade quantity for buy check - min_sz, _ = self.api.get_instrument_info(self.symbol) - if min_sz is None: - min_sz = self.api.get_default_min_size(self.symbol) - - # Calculate base currency quantity to buy - estimated_base_amount = buy_amount / current_price if current_price > 0 else 0 - - logger.info(f"[{self.symbol}] Pre-buy check: estimated quantity={estimated_base_amount:.10f}, " - f"minimum trade quantity={min_sz}, available USDT=${available_usdt:.2f}, " - f"buy amount=${buy_amount:.2f}") - - # Check if buy quantity meets exchange minimum requirement - if estimated_base_amount < min_sz: - logger.warning(f"[{self.symbol}] Estimated buy quantity {estimated_base_amount:.10f} less than minimum trade quantity {min_sz}, canceling buy") - return - - # Check if quote currency balance is sufficient - if available_usdt < buy_amount: - logger.warning(f"[{self.symbol}] Quote currency balance insufficient: need ${buy_amount:.2f}, current ${available_usdt:.2f}") - return - - logger.info(f"[{self.symbol}] Creating buy order: amount=${buy_amount:.2f}, price=${current_price:.2f}") - order_id = self.api.create_order( - symbol=self.symbol, - side='buy', - amount=buy_amount - ) - - if order_id: - # Wait for order completion - order_status = self.api.wait_for_order_completion(self.symbol, order_id) - if order_status is None: - logger.error(f"[{self.symbol}] Order not completed") - return - - # Get actual fill price and quantity - fill_price = order_status['avgPx'] - fill_amount = order_status['accFillSz'] - - # Update position status - self.base_amount += fill_amount - - # Update average entry price (weighted average) - if self.entry_price > 0: - # If adding position, calculate weighted average price - self.entry_price = (self.entry_price * (self.base_amount - fill_amount) + fill_price * fill_amount) / self.base_amount - logger.info(f"[{self.symbol}] Position addition operation, updated average entry price: ${self.entry_price:.2f}") - else: - self.entry_price = fill_price - - logger.info(f"[{self.symbol}] Buy successful: quantity={fill_amount:.10f}, price=${fill_price:.2f}") - logger.info(f"[{self.symbol}] Updated position: {self.symbol.split('-')[0]}={self.base_amount:.10f}") - - # Sync exchange position - self.sync_with_exchange() - - # Set dynamic stop-loss - trailing_percent = self.risk_manager.trailing_stop_percent - self.db.set_dynamic_stop(self.symbol, fill_price, trailing_percent=trailing_percent) - logger.info(f"[{self.symbol}] Set dynamic stop-loss: initial price=${fill_price:.2f}, trailing_percent={trailing_percent:.1%}") - - # Note: sync_with_exchange already saves position, no need to save again here - self.risk_manager.increment_trade_count(self.symbol) - else: - logger.error(f"[{self.symbol}] Buy order creation failed") - - elif decision['action'] == 'SELL': - if actual_position <= 0: - logger.info(f"[{self.symbol}] Exchange actual position is 0, ignoring sell signal") - # Update local status - self.base_amount = 0 - self.entry_price = 0 - self.save_position() - return - - # Prepare position information for AI analysis - position_info = self._prepare_position_info(actual_position, current_price, total_assets) - - # Get market analysis data (for technical indicators) - market_analysis = self.analyze_market() - - # Use DeepSeek to analyze sell proportion - try: - sell_proportion = self.deepseek.analyze_sell_proportion( - self.symbol, position_info, market_analysis, current_price - ) - except Exception as e: - logger.warning(f"[{self.symbol}] DeepSeek sell proportion analysis failed: {e}, using fallback decision") - sell_proportion = self.deepseek._get_fallback_sell_proportion(position_info, decision['confidence']) - - # Calculate sell quantity based on AI suggestion - sell_amount = actual_position * sell_proportion - - # Ensure sell quantity doesn't exceed actual position - sell_amount = min(sell_amount, actual_position) - - logger.info(f"[{self.symbol}] AI suggested sell proportion: {sell_proportion:.1%}, calculated sell quantity: {sell_amount:.10f}") - - # Get exchange minimum trade quantity for sell check - min_sz, _ = self.api.get_instrument_info(self.symbol) - if min_sz is None: - min_sz = self.api.get_default_min_size(self.symbol) - - # Check if sell quantity is less than exchange minimum trade quantity - if sell_amount < min_sz: - if sell_proportion < 1.0: # Partial sell - logger.info(f"[{self.symbol}] Sell quantity {sell_amount:.10f} less than minimum trade quantity {min_sz}, ignoring this partial sell") - return - else: # Full sell - logger.info(f"[{self.symbol}] Position quantity {sell_amount:.10f} less than minimum trade quantity {min_sz}, dust position, ignoring sell") - return - - # Record sell decision details - logger.info(f"[{self.symbol}] Final sell decision: proportion={sell_proportion:.1%}, quantity={sell_amount:.10f}, " - f"reason={decision.get('reason', 'AI decision')}") - - # Execute sell order - order_id = self.api.create_order( - symbol=self.symbol, - side='sell', - amount=sell_amount - ) - - if order_id: - # Wait for order completion - order_status = self.api.wait_for_order_completion(self.symbol, order_id) - if order_status is None: - logger.error(f"[{self.symbol}] Order not completed") - return - - # Get actual fill price and quantity - fill_price = order_status['avgPx'] - fill_amount = order_status['accFillSz'] - - # Calculate profit - profit = (fill_price - self.entry_price) * fill_amount - profit_percent = (fill_price / self.entry_price - 1) * 100 if self.entry_price > 0 else 0 - - # Sync exchange position - self.sync_with_exchange() - - logger.info(f"[{self.symbol}] Sell successful: quantity={fill_amount:.10f}, price=${fill_price:.2f}") - logger.info(f"[{self.symbol}] Profit: ${profit:.2f} ({profit_percent:+.2f}%)") - logger.info(f"[{self.symbol}] Updated position: {self.symbol.split('-')[0]}={self.base_amount:.10f}") - - # Save trade record - self.db.save_trade_record(self.symbol, 'sell', fill_amount, fill_price, order_id) - - # If fully sold, reset status - if self.base_amount <= 0: - self.entry_price = 0 - self.db.delete_dynamic_stop(self.symbol) - logger.info(f"[{self.symbol}] Position cleared, removed dynamic stop-loss") - else: - # After partial sell, update dynamic stop-loss - self.db.set_dynamic_stop(self.symbol, self.entry_price, trailing_percent=0.03, multiplier=2) - logger.info(f"[{self.symbol}] After partial sell, reset dynamic stop-loss") - - # Note: sync_with_exchange already saves position, no need to save again here - self.risk_manager.increment_trade_count(self.symbol) - else: - logger.error(f"[{self.symbol}] Sell order creation failed") - - def _prepare_position_info(self, actual_position, current_price, total_assets): - """Prepare position information for AI analysis""" - try: - # Calculate position value - position_value = actual_position * current_price - - # Calculate position ratio - position_ratio = position_value / total_assets if total_assets > 0 else 0 - - # Calculate profit ratio - profit_ratio = (current_price - self.entry_price) / self.entry_price if self.entry_price > 0 else 0 - - # Calculate distance to stop-loss/take-profit - stop_loss_price = self.entry_price * (1 - self.risk_manager.stop_loss) if self.entry_price > 0 else 0 - take_profit_price = self.entry_price * (1 + self.risk_manager.take_profit) if self.entry_price > 0 else 0 - - distance_to_stop_loss = (current_price - stop_loss_price) / current_price if current_price > 0 else 0 - distance_to_take_profit = (take_profit_price - current_price) / current_price if current_price > 0 else 0 - - # Get market volatility - market_data = self.api.get_market_data(self.symbol, '1H', limit=24) - if market_data is not None and len(market_data) > 0: - market_volatility = market_data['close'].pct_change().std() * 100 - else: - market_volatility = 0 - - # Get trend strength - market_analysis = self.analyze_market() - trend_strength = 0 - if market_analysis and 'technical_indicators' in market_analysis: - trend_strength = market_analysis['technical_indicators'].get('trend_strength', 0) - - position_info = { - 'base_amount': actual_position, - 'entry_price': self.entry_price, - 'current_price': current_price, - 'position_value': position_value, - 'position_ratio': position_ratio, - 'profit_ratio': profit_ratio, - 'distance_to_stop_loss': distance_to_stop_loss, - 'distance_to_take_profit': distance_to_take_profit, - 'market_volatility': market_volatility, - 'trend_strength': trend_strength, - 'total_assets': total_assets - } - - logger.debug(f"[{self.symbol}] Position analysis information: ratio={position_ratio:.2%}, profit={profit_ratio:+.2%}, " - f"distance to stop-loss={distance_to_stop_loss:.2%}, volatility={market_volatility:.2f}%") - - return position_info - - except Exception as e: - logger.error(f"[{self.symbol}] Error preparing position information: {e}") - # Return basic position information - return { - 'base_amount': actual_position, - 'entry_price': self.entry_price, - 'current_price': current_price, - 'position_value': actual_position * current_price, - 'position_ratio': 0, - 'profit_ratio': 0, - 'distance_to_stop_loss': 0, - 'distance_to_take_profit': 0, - 'market_volatility': 0, - 'trend_strength': 0, - 'total_assets': total_assets - } - - def start_dynamic_stop_monitor(self): - """Start dynamic stop-loss monitoring""" - if self.monitor_running and self.monitor_thread and self.monitor_thread.is_alive(): - logger.info(f"[{self.symbol}] Dynamic stop-loss monitoring already running") - return - - self.monitor_running = True - self.monitor_thread = threading.Thread(target=self._dynamic_stop_monitor_loop, name=f"StopMonitor-{self.symbol}") - self.monitor_thread.daemon = True - self.monitor_thread.start() - logger.info(f"[{self.symbol}] Dynamic stop-loss monitoring started") - - def stop_dynamic_stop_monitor(self): - """Stop dynamic stop-loss monitoring""" - self.monitor_running = False - if self.monitor_thread and self.monitor_thread.is_alive(): - self.monitor_thread.join(timeout=5) - if self.monitor_thread.is_alive(): - logger.warning(f"{self.symbol} dynamic stop-loss monitoring thread didn't exit normally") - else: - logger.info(f"{self.symbol} dynamic stop-loss monitoring stopped") - - def _dynamic_stop_monitor_loop(self): - """Dynamic stop-loss monitoring loop""" - while self.monitor_running: - try: - # Only monitor when there is position - if self.base_amount > 0: - self.check_dynamic_stops() - - # Monitor every 5 minutes - time.sleep(self.monitor_interval) - - except Exception as e: - logger.error(f"[{self.symbol}] Dynamic stop-loss monitoring error: {e}") - time.sleep(60) # Wait 1 minute before continuing when error occurs - - def check_dynamic_stops(self): - """Check dynamic stop-loss conditions""" - try: - # Get current price - current_price = self.api.get_current_price(self.symbol) - if not current_price: - logger.warning(f"[{self.symbol}] Unable to get current price, skipping stop-loss check") - return - - # Get dynamic stop-loss information - stop_info = self.db.get_dynamic_stop(self.symbol) - if not stop_info: - logger.debug(f"[{self.symbol}] No dynamic stop-loss settings") - return - - stop_loss_price = stop_info['current_stop_loss'] - take_profit_price = stop_info['current_take_profit'] - trailing_percent = stop_info['trailing_percent'] - - logger.debug(f"[{self.symbol}] Dynamic stop-loss check: current price=${current_price:.2f}, stop-loss=${stop_loss_price:.2f}, take-profit=${take_profit_price:.2f}") - - # Check stop-loss condition - if current_price <= stop_loss_price: - logger.warning(f"⚠️ {self.symbol} triggered dynamic stop-loss! Current price=${current_price:.2f} <= stop-loss price=${stop_loss_price:.2f}") - self.execute_trade({ - 'action': 'SELL', - 'confidence': 'HIGH', - 'reason': f'Dynamic stop-loss triggered: {current_price:.2f} <= {stop_loss_price:.2f}' - }, current_price) - return - - # Check take-profit condition - if current_price >= take_profit_price: - logger.info(f"🎯 {self.symbol} triggered dynamic take-profit! Current price=${current_price:.2f} >= take-profit price=${take_profit_price:.2f}") - self.execute_trade({ - 'action': 'SELL', - 'confidence': 'HIGH', - 'reason': f'Dynamic take-profit triggered: {current_price:.2f} >= {take_profit_price:.2f}' - }, current_price) - return - - # Update trailing stop-loss (only moves upward) - fix logic - if stop_info and current_price > stop_info.get('highest_price', 0): - new_stop_loss = self.db.update_dynamic_stop(self.symbol, current_price, trailing_percent=trailing_percent, multiplier=2) - if new_stop_loss: - logger.info(f"[{self.symbol}] Trailing stop-loss updated: new stop-loss price=${new_stop_loss:.2f}") - - except Exception as e: - logger.error(f"[{self.symbol}] Error checking dynamic stop-loss: {e}") - - def run(self): - """Run strategy (using enhanced decision)""" - self.strategy_running = True - - while self.strategy_running: - try: - logger.info(f"\n=== {self.symbol} Strategy Auto Execution ===") - - # Market analysis - analysis = self.analyze_market() - if analysis is None: - logger.warning("Analysis failed, waiting for next execution") - time.sleep(self.config['interval']) - continue - - # Make trading decision - decision = self.make_enhanced_decision(analysis) - logger.info(f"{self.symbol} Decision: {decision['action']} (Confidence: {decision['confidence']}, Score: {decision['total_score']:.3f})") - - if decision['reason']: - logger.info(f"Reason: {decision['reason']}") - - # Execute trade - self.execute_trade(decision, analysis['current_price']) - - # Risk management check - self.check_risk_management(analysis['current_price']) - - time.sleep(self.config['interval']) - - except Exception as e: - logger.error(f"Strategy execution error: {e}") - if "Network" in str(e) or "Connection" in str(e): - wait_time = min(300, self.config['interval'] * 2) # Network error wait longer - else: - wait_time = self.config['interval'] - - if self.strategy_running: - time.sleep(wait_time) - - def stop_strategy(self): - """Stop strategy running""" - with threading.Lock(): # Add thread lock - self.strategy_running = False - logger.info(f"{self.symbol} strategy stop signal sent") - - def check_risk_management(self, current_price): - """Check risk management (fixed stop-loss/take-profit)""" - if self.base_amount == 0: - return - - # Calculate profit/loss percentage - pnl_pct = (current_price - self.entry_price) / self.entry_price if self.entry_price > 0 else 0 - - # Fixed stop-loss/take-profit check - if pnl_pct <= -self.risk_manager.stop_loss: - logger.warning(f"⚠️ {self.symbol} triggered fixed stop-loss ({pnl_pct:.2%})") - self.execute_trade({ - 'action': 'SELL', - 'confidence': 'HIGH', - 'reason': 'Fixed stop-loss triggered' - }, current_price) - elif pnl_pct >= self.risk_manager.take_profit: - logger.info(f"🎯 {self.symbol} triggered fixed take-profit ({pnl_pct:.2%})") - self.execute_trade({ - 'action': 'SELL', - 'confidence': 'HIGH', - 'reason': 'Fixed take-profit triggered' - }, current_price) - -class MultiStrategyRunner: - """Multi-Strategy Runner""" - - def __init__(self): - self.running = False - self.strategies = {} - self.threads = {} - - # Configure each currency strategy - self.symbol_configs = { - 'ETH-USDT': {'name': 'Ethereum', 'interval': 1800, 'timeframe': '1H'}, - 'BTC-USDT': {'name': 'Bitcoin', 'interval': 1800, 'timeframe': '1H'}, - 'SOL-USDT': {'name': 'Solana', 'interval': 1800, 'timeframe': '1H'}, - 'XRP-USDT': {'name': 'Ripple', 'interval': 1800, 'timeframe': '1H'}, - 'BNB-USDT': {'name': 'Binance Coin', 'interval': 1800, 'timeframe': '1H'}, - 'OKB-USDT': {'name': 'OKB', 'interval': 1800, 'timeframe': '1H'}, - } - - # Initialize API client - self.api = OKXAPIClient() - self.deepseek = DeepSeekAnalyzer() - self.risk_manager = RiskManager(self.api) - - # Initialize all strategies - for symbol, config in self.symbol_configs.items(): - self.strategies[symbol] = TradingStrategy(symbol, config, self.api, self.risk_manager, self.deepseek) - - # Register exit handlers - atexit.register(self.shutdown) - signal.signal(signal.SIGINT, lambda s, f: self.shutdown()) - signal.signal(signal.SIGTERM, lambda s, f: self.shutdown()) - - def start_strategy(self, symbol): - """Start single currency strategy""" - if symbol not in self.strategies: - logger.error(f"Unsupported {symbol} trading pair") - return False - - # Check if already running - if symbol in self.threads and self.threads[symbol].is_alive(): - logger.info(f"{symbol} strategy already running") - return True - - def strategy_worker(): - """Strategy worker thread""" - strategy = self.strategies[symbol] - logger.info(f"Starting {symbol} strategy") - - try: - # Directly run strategy, strategy has its own loop internally - strategy.run() - except Exception as e: - logger.error(f"{symbol} strategy execution error: {e}") - finally: - logger.info(f"{symbol} strategy thread ended") - # Remove from active thread list when thread ends - if symbol in self.threads: - del self.threads[symbol] - - # Create and start thread - thread = threading.Thread(target=strategy_worker, name=f"Strategy-{symbol}") - thread.daemon = True - thread.start() - self.threads[symbol] = thread - - logger.info(f"Started {symbol} strategy") - return True - - def start_all_strategies(self): - """Start all strategies""" - self.running = True - - for symbol in self.strategies.keys(): - self.start_strategy(symbol) - time.sleep(60) # Each currency starts with 60 second interval - - - logger.info("All strategies started") - - def stop_strategy(self, symbol): - """Stop single currency strategy""" - if symbol in self.strategies: - # First stop strategy - self.strategies[symbol].stop_strategy() - - if symbol in self.threads: - thread = self.threads[symbol] - thread.join(timeout=10) # Wait 10 seconds - if thread.is_alive(): - logger.warning(f"Unable to stop {symbol} strategy thread") - else: - del self.threads[symbol] - logger.info(f"Stopped {symbol} strategy") - - # Stop dynamic stop-loss monitoring - if symbol in self.strategies: - self.strategies[symbol].stop_dynamic_stop_monitor() - - def stop_all_strategies(self): - """Stop all strategies""" - self.running = False - - # First send stop signal to all strategies - for symbol in list(self.strategies.keys()): - self.strategies[symbol].stop_strategy() - - # Then stop threads - for symbol in list(self.threads.keys()): - self.stop_strategy(symbol) - - logger.info("All strategies stopped") - - def get_status(self): - """Get system status""" - status = { - 'running': self.running, - 'active_strategies': list(self.threads.keys()), - 'strategies_detail': {} - } - - for symbol, strategy in self.strategies.items(): - status['strategies_detail'][symbol] = { - 'base_amount': strategy.base_amount, - 'entry_price': strategy.entry_price, - 'config': self.symbol_configs[symbol] - } - - return status - - def shutdown(self): - """System shutdown handling""" - logger.info("System shutting down, stopping all strategies and monitoring...") - self.stop_all_strategies() - - # Stop all dynamic stop-loss monitoring - for symbol, strategy in self.strategies.items(): - try: - strategy.stop_dynamic_stop_monitor() - logger.info(f"Stopped {symbol} dynamic stop-loss monitoring") - except Exception as e: - logger.error(f"Error stopping {symbol} dynamic stop-loss monitoring: {e}") - - # Wait for all threads to end - for symbol, thread in list(self.threads.items()): - if thread.is_alive(): - thread.join(timeout=5) # Wait 5 seconds - if thread.is_alive(): - logger.warning(f"{symbol} strategy thread didn't exit normally") - - # Ensure all resources released - time.sleep(1) - logger.info("System shutdown completed") - print("👋 System safely exited, thank you for using!") def main(): """Main function""" diff --git a/riskmanager.py b/riskmanager.py new file mode 100644 index 0000000..5f5bb25 --- /dev/null +++ b/riskmanager.py @@ -0,0 +1,338 @@ +import logging +import os +import time + +from database_manager import DatabaseManager + +logger = logging.getLogger(__name__) + +class RiskManager: + """Risk Control Manager (Dynamic Position Management Version)""" + + def __init__(self, api): + self.api = api + self.max_total_position = float(os.getenv('MAX_TOTAL_POSITION', '1')) # Maximum total position 100% + self.max_single_position = float(os.getenv('MAX_SINGLE_POSITION', '0.3')) # Single currency maximum position 30% + self.stop_loss = float(os.getenv('STOP_LOSS', '0.05')) # Stop-loss 5% + self.take_profit = float(os.getenv('TAKE_PROFIT', '0.15')) # Take-profit 15% + self.max_daily_trades = int(os.getenv('MAX_DAILY_TRADES', '10')) # Maximum daily trades + self.dust_threshold_value = 1.0 # Dust position value threshold (USDT) + self.low_position_ratio = 0.05 # Low position ratio threshold (5%) + + self.trailing_stop_percent = float(os.getenv('TRAILING_STOP_PERCENT', '0.03')) # Trailing stop percentage + self.monitor_interval = int(os.getenv('MONITOR_INTERVAL', '300')) # Monitoring interval seconds + + # Dynamic position parameters + self.volatility_adjustment = True + self.trend_adjustment = True + self.market_sentiment_adjustment = True + + # Trade records + self.daily_trade_count = {} + self.db = DatabaseManager() + + def calculate_total_assets(self): + """Calculate total assets (in USDT)""" + try: + # Get all currency balances + balances = self.api.get_currency_balances() + if not balances: + logger.error("Unable to get currency balances") + return 0.0, 0.0, {} + + total_usdt_value = 0.0 + position_details = {} + + # Calculate USDT balance (including available and frozen) + usdt_balance = balances.get('USDT', {}) + usdt_avail = usdt_balance.get('amount', 0.0) + usdt_frozen = usdt_balance.get('frozen', 0.0) + total_usdt = usdt_avail + usdt_frozen + total_usdt_value += total_usdt + + # Calculate value of other currencies + for currency, balance_info in balances.items(): + if currency != 'USDT': + avail = balance_info.get('amount', 0.0) + frozen = balance_info.get('frozen', 0.0) + total_amount = avail + frozen + if total_amount > 0: + symbol = f"{currency}-USDT" + current_price = self.api.get_current_price(symbol) + if current_price: + value = total_amount * current_price + total_usdt_value += value + position_details[symbol] = { + 'amount': total_amount, # Total quantity (available + frozen) + 'value': value, + 'current_price': current_price + } + else: + logger.warning(f"Unable to get {currency} price, skipping value calculation") + + logger.debug(f"Total asset calculation: USDT={total_usdt:.2f}(available{usdt_avail:.2f}+frozen{usdt_frozen:.2f}), position value={total_usdt_value - total_usdt:.2f}, total={total_usdt_value:.2f}") + + return total_usdt_value, usdt_avail, position_details + + except Exception as e: + logger.error(f"Error calculating total assets: {e}") + return 0.0, 0.0, {} + + def get_position_ratio(self, symbol): + """Get specified currency's position ratio""" + try: + total_assets, usdt_balance, positions = self.calculate_total_assets() + if total_assets == 0: + return 0.0 + + position_value = positions.get(symbol, {}).get('value', 0.0) + return position_value / total_assets + + except Exception as e: + logger.error(f"Error calculating position ratio: {e}") + return 0.0 + + def get_available_usdt_ratio(self): + """Get available USDT ratio""" + try: + total_assets, usdt_balance, _ = self.calculate_total_assets() + if total_assets == 0: + return 0.0 + + return usdt_balance / total_assets + + except Exception as e: + logger.error(f"Error calculating USDT ratio: {e}") + return 0.0 + + def get_position_size(self, symbol, confidence, current_price): + """Calculate position size based on confidence level and USDT availability""" + try: + total_assets, usdt_balance, positions = self.calculate_total_assets() + + if total_assets == 0: + logger.warning("Total assets is 0, unable to calculate position") + return 0.0 + + # Get current position value + current_pos_value = positions.get(symbol, {}).get('value', 0.0) + + # Calculate USDT available ratio + usdt_ratio = usdt_balance / total_assets + + # Adjust maximum position based on USDT availability + adjusted_max_single = self.max_single_position * min(1.0, usdt_ratio * 3) # More USDT allows larger positions + + # Calculate maximum additional position + max_single_add = max(0, total_assets * adjusted_max_single - current_pos_value) + max_total_add = max(0, total_assets * self.max_total_position - (total_assets - usdt_balance)) + + max_add = min(max_single_add, max_total_add, usdt_balance) + + # Adjust based on confidence level + multiplier = { + 'HIGH': 0.8, # High confidence uses 80% of available amount + 'MEDIUM': 0.5, # Medium confidence uses 50% + 'LOW': 0.2 # Low confidence uses 20% + }.get(confidence, 0.2) + + position_size = max_add * multiplier + + # Ensure doesn't exceed available USDT balance + position_size = min(position_size, usdt_balance) + + logger.info(f"Position calculation: {symbol}, total assets=${total_assets:.2f}, USDT=${usdt_balance:.2f}, " + f"current position=${current_pos_value:.2f}, suggested position=${position_size:.2f}") + + return position_size + + except Exception as e: + logger.error(f"Error calculating position size: {e}") + return 0.0 + + def get_dynamic_position_size(self, symbol, confidence, technical_indicators, current_price): + """Dynamic position calculation""" + try: + # Base position calculation + base_size = self.get_position_size(symbol, confidence, current_price) + if base_size == 0: + return 0.0 + + # Get market state parameters + volatility = technical_indicators.get('volatility', 0) + trend_strength = technical_indicators.get('trend_strength', 0) + market_state = self.assess_market_state(technical_indicators) + + # Volatility adjustment (high volatility reduces position) + volatility_factor = self._calculate_volatility_factor(volatility) + + # Trend strength adjustment (strong trend increases position) + trend_factor = self._calculate_trend_factor(trend_strength) + + # Market state adjustment + market_factor = self._calculate_market_factor(market_state) + + # Confidence level adjustment + confidence_factor = self._calculate_confidence_factor(confidence) + + # Calculate dynamic position + dynamic_size = base_size * volatility_factor * trend_factor * market_factor * confidence_factor + + logger.info(f"Dynamic position calculation: {symbol}, base={base_size:.2f}, " + f"volatility factor={volatility_factor:.2f}, trend factor={trend_factor:.2f}, " + f"market factor={market_factor:.2f}, confidence factor={confidence_factor:.2f}, " + f"final={dynamic_size:.2f}") + + return dynamic_size + + except Exception as e: + logger.error(f"Dynamic position calculation error: {e}") + return self.get_position_size(symbol, confidence, current_price) + + def _calculate_volatility_factor(self, volatility): + """Calculate volatility adjustment factor""" + if not self.volatility_adjustment: + return 1.0 + + # Annualized volatility conversion and adjustment + if volatility > 0.8: # 80%+ annualized volatility + return 0.3 + elif volatility > 0.6: # 60-80% + return 0.5 + elif volatility > 0.4: # 40-60% + return 0.7 + elif volatility > 0.2: # 20-40% + return 0.9 + else: # <20% + return 1.0 + + def _calculate_trend_factor(self, trend_strength): + """Calculate trend strength adjustment factor""" + if not self.trend_adjustment: + return 1.0 + + if trend_strength > 0.7: # Strong trend + return 1.3 + elif trend_strength > 0.4: # Medium trend + return 1.1 + elif trend_strength > 0.2: # Weak trend + return 1.0 + else: # No trend + return 0.8 + + def _calculate_market_factor(self, market_state): + """Calculate market state adjustment factor""" + if not self.market_sentiment_adjustment: + return 1.0 + + factors = { + 'STRONG_BULL': 1.2, + 'BULL': 1.1, + 'NEUTRAL': 1.0, + 'BEAR': 0.7, + 'STRONG_BEAR': 0.5 + } + return factors.get(market_state, 1.0) + + def _calculate_confidence_factor(self, confidence): + """Calculate confidence level adjustment factor""" + factors = { + 'HIGH': 1.0, + 'MEDIUM': 0.7, + 'LOW': 0.4 + } + return factors.get(confidence, 0.5) + + def assess_market_state(self, technical_indicators): + """Assess market state""" + try: + # Get key indicators + rsi = technical_indicators.get('rsi', 50) + if hasattr(rsi, '__len__'): + rsi = rsi.iloc[-1] + + macd_line, signal_line, _ = technical_indicators.get('macd', (0, 0, 0)) + if hasattr(macd_line, '__len__'): + macd_line, signal_line = macd_line.iloc[-1], signal_line.iloc[-1] + + sma_20 = technical_indicators.get('sma_20', 0) + sma_50 = technical_indicators.get('sma_50', 0) + current_price = technical_indicators.get('current_price', 0) + + if hasattr(sma_20, '__len__'): + sma_20, sma_50 = sma_20.iloc[-1], sma_50.iloc[-1] + + # Calculate bullish signal score + bull_signals = 0 + total_signals = 0 + + # RSI signal + if rsi > 50: bull_signals += 1 + total_signals += 1 + + # MACD signal + if macd_line > signal_line: bull_signals += 1 + total_signals += 1 + + # Moving average signal + if current_price > sma_20: bull_signals += 1 + if current_price > sma_50: bull_signals += 1 + total_signals += 2 + + # Determine market state + bull_ratio = bull_signals / total_signals + + if bull_ratio >= 0.8: + return 'STRONG_BULL' + elif bull_ratio >= 0.6: + return 'BULL' + elif bull_ratio >= 0.4: + return 'NEUTRAL' + elif bull_ratio >= 0.2: + return 'BEAR' + else: + return 'STRONG_BEAR' + + except Exception as e: + logger.error(f"Error assessing market state: {e}") + return 'NEUTRAL' + + def is_dust_position(self, symbol, current_price, base_amount): + """Check if dust position""" + hold_value = base_amount * current_price if current_price else 0 + return hold_value < self.dust_threshold_value + + def is_low_position_ratio(self, symbol, total_assets, current_price, base_amount): + """Check if position ratio is low""" + hold_value = base_amount * current_price if current_price else 0 + return (hold_value / total_assets) < self.low_position_ratio if total_assets > 0 else False + + def can_trade(self, symbol, amount, current_price): + """Check if can trade""" + try: + # Check daily trade count + today = datetime.now().date() + if symbol not in self.daily_trade_count: + self.daily_trade_count[symbol] = {'date': today, 'count': 0} + + if self.daily_trade_count[symbol]['date'] != today: + self.daily_trade_count[symbol] = {'date': today, 'count': 0} + + if self.daily_trade_count[symbol]['count'] >= self.max_daily_trades: + logger.warning(f"{symbol} daily trade count reached {self.max_daily_trades} limit") + return False + + return True + + except Exception as e: + logger.error(f"Error checking trade conditions: {e}") + return False + + def increment_trade_count(self, symbol): + """Increment trade count""" + try: + today = datetime.now().date() + if symbol in self.daily_trade_count and self.daily_trade_count[symbol]['date'] == today: + self.daily_trade_count[symbol]['count'] += 1 + except Exception as e: + logger.error(f"Error incrementing trade count: {e}") diff --git a/technicalanalyzer.py b/technicalanalyzer.py new file mode 100644 index 0000000..6cc95b7 --- /dev/null +++ b/technicalanalyzer.py @@ -0,0 +1,609 @@ +import logging +import numpy as np + +logger = logging.getLogger(__name__) + + +class TechnicalAnalyzer: + """Technical Analyzer""" + + @staticmethod + def calculate_indicators(df): + """Calculate technical indicators (optimized version)""" + if df is None or len(df) < 20: # Lower minimum data requirement + logger.warning("Insufficient data, unable to calculate technical indicators") + return {} + + try: + indicators = {} + + # KDJ indicator + k, d, j = TechnicalAnalyzer.calculate_kdj(df) + if k is not None: + indicators['kdj'] = (k, d, j) + + # RSI indicator (multiple time periods) + rsi_14 = TechnicalAnalyzer.calculate_rsi(df['close'], 14) + rsi_7 = TechnicalAnalyzer.calculate_rsi(df['close'], 7) + rsi_21 = TechnicalAnalyzer.calculate_rsi(df['close'], 21) + if rsi_14 is not None: + indicators['rsi'] = rsi_14 + indicators['rsi_7'] = rsi_7 + indicators['rsi_21'] = rsi_21 + + # ATR indicator + atr = TechnicalAnalyzer.calculate_atr(df) + if atr is not None: + indicators['atr'] = atr + + # MACD indicator + macd_line, signal_line, histogram = TechnicalAnalyzer.calculate_macd(df['close']) + if macd_line is not None: + indicators['macd'] = (macd_line, signal_line, histogram) + + # Bollinger Bands (multiple standard deviations) + upper1, middle1, lower1 = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 1) + upper2, middle2, lower2 = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 2) + if upper1 is not None: + indicators['bollinger_1std'] = (upper1, middle1, lower1) + indicators['bollinger_2std'] = (upper2, middle2, lower2) + + # Moving Averages (multiple periods) + sma_20 = TechnicalAnalyzer.calculate_sma(df['close'], 20) + sma_50 = TechnicalAnalyzer.calculate_sma(df['close'], 50) + sma_100 = TechnicalAnalyzer.calculate_sma(df['close'], 100) + ema_12 = TechnicalAnalyzer.calculate_ema(df['close'], 12) + ema_26 = TechnicalAnalyzer.calculate_ema(df['close'], 26) + if sma_20 is not None: + indicators['sma_20'] = sma_20 + indicators['sma_50'] = sma_50 + indicators['sma_100'] = sma_100 + indicators['ema_12'] = ema_12 + indicators['ema_26'] = ema_26 + + # Stochastic Oscillator + k, d = TechnicalAnalyzer.calculate_stochastic(df) + if k is not None: + indicators['stochastic'] = (k, d) + + # Trend strength + indicators['trend_strength'] = TechnicalAnalyzer.calculate_trend_strength(df) + + # Volatility + indicators['volatility'] = TechnicalAnalyzer.calculate_volatility(df) + + # Current price + indicators['current_price'] = df['close'].iloc[-1] + + return indicators + + except Exception as e: + logger.error(f"Error calculating technical indicators: {e}") + return {} + + @staticmethod + def calculate_ema(prices, period): + """Calculate Exponential Moving Average""" + try: + return prices.ewm(span=period, adjust=False).mean() + except Exception as e: + logger.error(f"Error calculating EMA: {e}") + return None + + @staticmethod + def calculate_trend_strength(df, period=20): + """Calculate trend strength""" + try: + if len(df) < period: + return 0 + + # Use linear regression to calculate trend strength + x = np.arange(len(df)) + y = df['close'].values + + # Linear regression + slope, intercept = np.polyfit(x[-period:], y[-period:], 1) + + # Calculate R² value as trend strength + y_pred = slope * x[-period:] + intercept + ss_res = np.sum((y[-period:] - y_pred) ** 2) + ss_tot = np.sum((y[-period:] - np.mean(y[-period:])) ** 2) + r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0 + + return abs(r_squared) # Take absolute value, trend strength doesn't distinguish positive/negative + + except Exception as e: + logger.error(f"Error calculating trend strength: {e}") + return 0 + + @staticmethod + def detect_rsi_divergence(df, rsi_period=14): + """Detect RSI bullish divergence""" + try: + if len(df) < 30: # Need sufficient data + return False + + # Calculate RSI + rsi = TechnicalAnalyzer.calculate_rsi(df['close'], rsi_period) + if rsi is None: + return False + + # Use pandas methods to simplify calculation + # Find lowest points in recent 10 periods + recent_lows = df['low'].tail(10) + recent_rsi = rsi.tail(10) + + # Find price lowest point and RSI lowest point + min_price_idx = recent_lows.idxmin() + min_rsi_idx = recent_rsi.idxmin() + + # If price lowest point appears later than RSI lowest point, possible bullish divergence + if min_price_idx > min_rsi_idx: + # Check if price is making new lows while RSI is rising + price_trend = (recent_lows.iloc[-1] < recent_lows.iloc[-5]) + rsi_trend = (recent_rsi.iloc[-1] > recent_rsi.iloc[-5]) + + return price_trend and rsi_trend and recent_rsi.iloc[-1] < 40 + + return False + + except Exception as e: + logger.error(f"RSI divergence detection error: {e}") + return False + + @staticmethod + def detect_macd_divergence(df): + """Detect MACD bullish divergence""" + try: + if len(df) < 30: + return False + + # Calculate MACD, especially focus on histogram + _, _, histogram = TechnicalAnalyzer.calculate_macd(df['close']) + if histogram is None: + return False + + # Use recent data + recent_lows = df['low'].tail(10) + recent_hist = histogram.tail(10) + + # Find price lowest point and MACD histogram lowest point + min_price_idx = recent_lows.idxmin() + min_hist_idx = recent_hist.idxmin() + + # If price lowest point appears later than histogram lowest point, possible bullish divergence + if min_price_idx > min_hist_idx: + # Check price trend and histogram trend + price_trend = (recent_lows.iloc[-1] < recent_lows.iloc[-5]) + hist_trend = (recent_hist.iloc[-1] > recent_hist.iloc[-5]) + + # Histogram rising from negative area is stronger signal + hist_improving = recent_hist.iloc[-1] > recent_hist.iloc[-3] + + return price_trend and hist_trend and hist_improving + + return False + + except Exception as e: + logger.error(f"MACD divergence detection error: {e}") + return False + + @staticmethod + def volume_confirmation(df, period=5): + """Bottom volume confirmation""" + try: + if len(df) < period + 5: + return False + + current_volume = df['volume'].iloc[-1] + avg_volume = df['volume'].tail(period).mean() + + # Current volume significantly higher than average + volume_ratio = current_volume / avg_volume if avg_volume > 0 else 1 + + # Price falling but volume increasing (possible bottom accumulation) + price_change = (df['close'].iloc[-1] - df['close'].iloc[-2]) / df['close'].iloc[-2] + + if volume_ratio > 1.5 and price_change < 0: + return True + + return False + except Exception as e: + logger.error(f"Volume confirmation error: {e}") + return False + + @staticmethod + def detect_hammer_pattern(df, lookback=5): + """Detect hammer reversal pattern""" + try: + if len(df) < lookback + 1: + return False + + latest = df.iloc[-1] + body_size = abs(latest['close'] - latest['open']) + total_range = latest['high'] - latest['low'] + + # Avoid division by zero + if total_range == 0: + return False + + # Hammer characteristics: lower shadow at least 2x body, very short upper shadow + lower_shadow = min(latest['open'], latest['close']) - latest['low'] + upper_shadow = latest['high'] - max(latest['open'], latest['close']) + + is_hammer = (lower_shadow >= 2 * body_size and + upper_shadow <= body_size * 0.5 and + body_size > 0) + + # Needs to be in downtrend + prev_trend = df['close'].iloc[-lookback] > df['close'].iloc[-1] + + return is_hammer and prev_trend + + except Exception as e: + logger.error(f"Hammer pattern detection error: {e}") + return False + + @staticmethod + def detect_reversal_patterns(df): + """Detect bottom reversal patterns""" + reversal_signals = [] + + try: + # RSI bullish divergence + if TechnicalAnalyzer.detect_rsi_divergence(df): + reversal_signals.append({ + 'type': 'RSI_DIVERGENCE_BULLISH', + 'action': 'BUY', + 'strength': 'MEDIUM', + 'description': 'RSI bullish divergence, momentum divergence bullish' + }) + + # MACD bullish divergence + if TechnicalAnalyzer.detect_macd_divergence(df): + reversal_signals.append({ + 'type': 'MACD_DIVERGENCE_BULLISH', + 'action': 'BUY', + 'strength': 'MEDIUM', + 'description': 'MACD histogram bullish divergence, momentum strengthening' + }) + + # Volume confirmation + if TechnicalAnalyzer.volume_confirmation(df): + reversal_signals.append({ + 'type': 'VOLUME_CONFIRMATION', + 'action': 'BUY', + 'strength': 'LOW', + 'description': 'Bottom volume increase, signs of capital entry' + }) + + # Add hammer pattern detection + if TechnicalAnalyzer.detect_hammer_pattern(df): + reversal_signals.append({ + 'type': 'HAMMER_PATTERN', + 'action': 'BUY', + 'strength': 'LOW', + 'description': 'Hammer pattern, short-term reversal signal' + }) + + except Exception as e: + logger.error(f"Reversal pattern detection error: {e}") + + return reversal_signals + + @staticmethod + def calculate_support_resistance(df, period=20): + """Calculate support resistance levels (fixed version)""" + try: + # Recent high resistance + resistance = df['high'].rolling(window=period).max().iloc[-1] + + # Recent low support + support = df['low'].rolling(window=period).min().iloc[-1] + + # Dynamic support resistance (based on Bollinger Bands) + # Fix: correctly receive three return values from Bollinger Bands + upper_bb, middle_bb, lower_bb = TechnicalAnalyzer.calculate_bollinger_bands(df['close'], 20, 2) + + # Check if Bollinger Band calculation successful + if upper_bb is not None and lower_bb is not None: + resistance_bb = upper_bb.iloc[-1] if hasattr(upper_bb, 'iloc') else upper_bb + support_bb = lower_bb.iloc[-1] if hasattr(lower_bb, 'iloc') else lower_bb + else: + # If Bollinger Band calculation fails, use static values as backup + resistance_bb = resistance + support_bb = support + + current_price = df['close'].iloc[-1] + + return { + 'static_resistance': resistance, + 'static_support': support, + 'dynamic_resistance': resistance_bb, + 'dynamic_support': support_bb, + 'current_vs_resistance': (current_price - resistance) / resistance * 100 if resistance > 0 else 0, + 'current_vs_support': (current_price - support) / support * 100 if support > 0 else 0 + } + except Exception as e: + logger.error(f"Error calculating support resistance: {e}") + # Return default values to avoid subsequent errors + current_price = df['close'].iloc[-1] if len(df) > 0 else 0 + return { + 'static_resistance': current_price * 1.1 if current_price > 0 else 0, + 'static_support': current_price * 0.9 if current_price > 0 else 0, + 'dynamic_resistance': current_price * 1.05 if current_price > 0 else 0, + 'dynamic_support': current_price * 0.95 if current_price > 0 else 0, + 'current_vs_resistance': 0, + 'current_vs_support': 0 + } + + @staticmethod + def calculate_volatility(df, period=20): + """Calculate volatility""" + try: + returns = df['close'].pct_change().dropna() + volatility = returns.rolling(window=period).std() * np.sqrt(365) # Annualized volatility + return volatility.iloc[-1] if len(volatility) > 0 else 0 + except Exception as e: + logger.error(f"Error calculating volatility: {e}") + return 0 + + @staticmethod + def generate_weighted_signals(df): + """Generate weighted technical signals""" + signals = [] + weights = { + 'KDJ_GOLDEN_CROSS': 0.15, + 'MACD_GOLDEN_CROSS': 0.20, + 'MA_GOLDEN_CROSS': 0.25, + 'RSI_OVERSOLD': 0.15, + 'BOLLINGER_LOWER_TOUCH': 0.15, + 'STOCH_GOLDEN_CROSS': 0.10 + } + + # Calculate various indicator signals + technical_signals = TechnicalAnalyzer.generate_signals(df) + + # Calculate weighted score + total_score = 0 + signal_count = 0 + + for signal in technical_signals: + weight = weights.get(signal['type'], 0.05) + strength_multiplier = 1.0 if signal['strength'] == 'STRONG' else 0.7 + total_score += weight * strength_multiplier + signal_count += 1 + + # Normalize score + normalized_score = min(1.0, total_score) + + # Determine action + if normalized_score > 0.6: + action = 'BUY' + elif normalized_score < 0.3: + action = 'SELL' + else: + action = 'HOLD' + + return { + 'score': normalized_score, + 'action': action, + 'signal_count': signal_count, + 'signals': technical_signals, + 'confidence': 'HIGH' if normalized_score > 0.7 or normalized_score < 0.2 else 'MEDIUM' + } + + @staticmethod + def calculate_kdj(df, n=9, m1=3, m2=3): + """Calculate KDJ indicator""" + try: + if len(df) < n: + return pd.Series([np.nan] * len(df)), pd.Series([np.nan] * len(df)), pd.Series([np.nan] * len(df)) + + low_list = df['low'].rolling(window=n, min_periods=1).min() + high_list = df['high'].rolling(window=n, min_periods=1).max() + + # Avoid division by zero error + denominator = high_list - low_list + denominator = denominator.replace(0, np.nan) + + rsv = ((df['close'] - low_list) / denominator) * 100 + rsv = rsv.fillna(50) + + k_series = rsv.ewm(span=m1-1, adjust=False).mean() + d_series = k_series.ewm(span=m2-1, adjust=False).mean() + j_series = 3 * k_series - 2 * d_series + + return k_series, d_series, j_series + + except Exception as e: + logger.error(f"Error calculating KDJ: {e}") + return None, None, None + + @staticmethod + def calculate_rsi(prices, period=14): + """Calculate RSI""" + try: + if len(prices) < period: + return pd.Series([np.nan] * len(prices)) + + delta = prices.diff() + gain = (delta.where(delta > 0, 0)).rolling(window=period, min_periods=1).mean() + loss = (-delta.where(delta < 0, 0)).rolling(window=period, min_periods=1).mean() + + # Avoid division by zero error + rs = gain / loss.replace(0, np.nan) + rsi = 100 - (100 / (1 + rs)) + return rsi.fillna(50) + + except Exception as e: + logger.error(f"Error calculating RSI: {e}") + return None + + @staticmethod + def calculate_atr(df, period=14): + """Calculate ATR""" + try: + if len(df) < period: + return pd.Series([np.nan] * len(df)) + + high_low = df['high'] - df['low'] + high_close = np.abs(df['high'] - df['close'].shift()) + low_close = np.abs(df['low'] - df['close'].shift()) + + true_range = np.maximum(high_low, np.maximum(high_close, low_close)) + atr = true_range.rolling(window=period, min_periods=1).mean() + return atr + + except Exception as e: + logger.error(f"Error calculating ATR: {e}") + return None + + @staticmethod + def calculate_macd(prices, fast_period=12, slow_period=26, signal_period=9): + """Calculate MACD""" + try: + ema_fast = prices.ewm(span=fast_period, adjust=False).mean() + ema_slow = prices.ewm(span=slow_period, adjust=False).mean() + macd_line = ema_fast - ema_slow + signal_line = macd_line.ewm(span=signal_period, adjust=False).mean() + histogram = macd_line - signal_line + return macd_line, signal_line, histogram + except Exception as e: + logger.error(f"Error calculating MACD: {e}") + return None, None, None + + @staticmethod + def calculate_bollinger_bands(prices, period=20, std_dev=2): + """Calculate Bollinger Bands""" + try: + if len(prices) < period: + logger.warning(f"Data length {len(prices)} insufficient, unable to calculate {period}-period Bollinger Bands") + return None, None, None + + middle = prices.rolling(window=period).mean() + std = prices.rolling(window=period).std() + + # Handle NaN standard deviation + std = std.fillna(0) + + upper = middle + (std * std_dev) + lower = middle - (std * std_dev) + + return upper, middle, lower + except Exception as e: + logger.error(f"Error calculating Bollinger Bands: {e}") + return None, None, None + + @staticmethod + def calculate_sma(prices, period): + """Calculate Simple Moving Average""" + try: + return prices.rolling(window=period).mean() + except Exception as e: + logger.error(f"Error calculating SMA: {e}") + return None + + @staticmethod + def calculate_stochastic(df, k_period=14, d_period=3): + """Calculate Stochastic Oscillator""" + try: + low_min = df['low'].rolling(window=k_period).min() + high_max = df['high'].rolling(window=k_period).max() + k = 100 * ((df['close'] - low_min) / (high_max - low_min)) + d = k.rolling(window=d_period).mean() + return k, d + except Exception as e: + logger.error(f"Error calculating Stochastic: {e}") + return None, None + + @staticmethod + def generate_signals(df): + """Generate technical signals""" + signals = [] + + try: + # KDJ + k, d, j = TechnicalAnalyzer.calculate_kdj(df) + if k is not None and len(k) > 1: + latest_k, latest_d, latest_j = k.iloc[-1], d.iloc[-1], j.iloc[-1] + prev_k, prev_d, prev_j = k.iloc[-2], d.iloc[-2], j.iloc[-2] + + if prev_k <= prev_d and latest_k > latest_d: + signals.append({'type': 'KDJ_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif prev_k >= prev_d and latest_k < latest_d: + signals.append({'type': 'KDJ_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) + + if latest_j < 20: + signals.append({'type': 'KDJ_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif latest_j > 80: + signals.append({'type': 'KDJ_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'}) + + # RSI + rsi = TechnicalAnalyzer.calculate_rsi(df['close']) + if rsi is not None and len(rsi) > 0: + latest_rsi = rsi.iloc[-1] + + if latest_rsi < 30: + signals.append({'type': 'RSI_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif latest_rsi > 70: + signals.append({'type': 'RSI_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'}) + + # MACD + macd_line, signal_line, _ = TechnicalAnalyzer.calculate_macd(df['close']) + if macd_line is not None and len(macd_line) > 1: + latest_macd = macd_line.iloc[-1] + latest_signal = signal_line.iloc[-1] + prev_macd = macd_line.iloc[-2] + prev_signal = signal_line.iloc[-2] + + if prev_macd <= prev_signal and latest_macd > latest_signal: + signals.append({'type': 'MACD_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif prev_macd >= prev_signal and latest_macd < latest_signal: + signals.append({'type': 'MACD_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) + + # Bollinger Bands + upper, middle, lower = TechnicalAnalyzer.calculate_bollinger_bands(df['close']) + if upper is not None: + latest_close = df['close'].iloc[-1] + if latest_close <= lower.iloc[-1]: + signals.append({'type': 'BOLlinger_LOWER_TOUCH', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif latest_close >= upper.iloc[-1]: + signals.append({'type': 'BOLlinger_UPPER_TOUCH', 'action': 'SELL', 'strength': 'MEDIUM'}) + + # Moving Averages + sma_short = TechnicalAnalyzer.calculate_sma(df['close'], 50) + sma_long = TechnicalAnalyzer.calculate_sma(df['close'], 200) + if sma_short is not None and sma_long is not None and len(sma_short) > 1: + latest_short = sma_short.iloc[-1] + latest_long = sma_long.iloc[-1] + prev_short = sma_short.iloc[-2] + prev_long = sma_long.iloc[-2] + + if prev_short <= prev_long and latest_short > latest_long: + signals.append({'type': 'MA_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif prev_short >= prev_long and latest_short < latest_long: + signals.append({'type': 'MA_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) + + # Stochastic + k, d = TechnicalAnalyzer.calculate_stochastic(df) + if k is not None and len(k) > 1: + latest_k = k.iloc[-1] + latest_d = d.iloc[-1] + prev_k = k.iloc[-2] + prev_d = d.iloc[-2] + + if latest_k < 20: + signals.append({'type': 'STOCH_OVERSOLD', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif latest_k > 80: + signals.append({'type': 'STOCH_OVERBOUGHT', 'action': 'SELL', 'strength': 'MEDIUM'}) + + if prev_k <= prev_d and latest_k > latest_d and latest_k < 80: + signals.append({'type': 'STOCH_GOLDEN_CROSS', 'action': 'BUY', 'strength': 'MEDIUM'}) + elif prev_k >= prev_d and latest_k < latest_d and latest_k > 20: + signals.append({'type': 'STOCH_DEATH_CROSS', 'action': 'SELL', 'strength': 'MEDIUM'}) + + except Exception as e: + logger.error(f"Error generating technical signals: {e}") + + return signals diff --git a/tradingstrategy.py b/tradingstrategy.py new file mode 100644 index 0000000..861e8ef --- /dev/null +++ b/tradingstrategy.py @@ -0,0 +1,816 @@ +import logging +import time +import threading +import pandas as pd + +from technicalanalyzer import TechnicalAnalyzer +from database_manager import DatabaseManager + +logger = logging.getLogger(__name__) + +class TradingStrategy: + """Trading Strategy (Comprehensive Optimized Version)""" + + def __init__(self, symbol, config, api, risk_manager, deepseek): + self.symbol = symbol + self.config = config + self.api = api + self.risk_manager = risk_manager + self.deepseek = deepseek + self.technical_analyzer = TechnicalAnalyzer() + self.db = DatabaseManager() # Use default configuration + + # Position status + self.base_amount = 0.0 + self.entry_price = 0.0 + + # Dynamic stop-loss monitoring - using environment variable configured interval + self.monitor_interval = 300 # Default 5 minutes + if hasattr(risk_manager, 'monitor_interval'): + self.monitor_interval = risk_manager.monitor_interval + self.monitor_running = False + self.monitor_thread = None + + # Running status control + self.strategy_running = False + + # Load position status + self.load_position() + + # Start dynamic stop-loss monitoring + self.start_dynamic_stop_monitor() + + def load_position(self): + """Load position status from database""" + try: + position_data = self.db.load_position(self.symbol) + if position_data: + self.base_amount = position_data['base_amount'] + self.entry_price = position_data['entry_price'] + logger.info(f"Loaded {self.symbol} position: currency quantity={self.base_amount:.10f}, entry price=${self.entry_price:.2f}") + else: + # If no database record, sync with exchange position + self.sync_with_exchange() + except Exception as e: + logger.error(f"Error loading position status: {e}") + self.sync_with_exchange() + + def save_position(self): + """Save position status to database""" + try: + if self.base_amount > 0: + self.db.save_position(self.symbol, self.base_amount, self.entry_price) + else: + # If position is 0, delete record + self.db.delete_position(self.symbol) + # Also delete dynamic stop record + self.db.delete_dynamic_stop(self.symbol) + except Exception as e: + logger.error(f"Error saving position status: {e}") + + def sync_with_exchange(self): + """Sync with exchange position""" + try: + logger.debug(f"Starting {self.symbol} position status sync") + + # Get all currency balances + balances = self.api.get_currency_balances() + if not balances: + logger.warning("Unable to get currency balances") + return False + + # Extract base currency + base_currency = self.symbol.split('-')[0] + + # Update local balance + if base_currency in balances: + new_base_amount = balances[base_currency].get('amount', 0.0) + + # If position quantity changes, print log + if abs(new_base_amount - self.base_amount) > 1e-10: + logger.info(f"Synced {self.symbol} latest balance: {base_currency}={new_base_amount:.10f}") + + # If position decreases but entry price not updated, keep original entry price (for calculating profit/loss) + if new_base_amount < self.base_amount and self.entry_price > 0: + # Position decreased, reset entry price to conservative estimate (current price) + if new_base_amount <= 0: + self.entry_price = 0 + logger.info(f"{self.symbol} position cleared, reset entry price to 0") + else: + current_price = self.api.get_current_price(self.symbol) + if current_price: + self.entry_price = current_price + logger.info(f"{self.symbol} partial sell, reset entry price to current price: ${current_price:.2f}") + else: + logger.warning(f"{self.symbol} partial sell but unable to get current price, keep original entry price") + + # If from no position to having position, and no entry price record + elif self.base_amount == 0 and new_base_amount > 0 and self.entry_price == 0: + # Try to load historical entry price from database + position_data = self.db.load_position(self.symbol) + if position_data and position_data['entry_price'] > 0: + self.entry_price = position_data['entry_price'] + logger.info(f"Restored entry price from database: ${self.entry_price:.2f}") + else: + # Get current price as reference (but note this is reference price) + current_price = self.api.get_current_price(self.symbol) + if current_price: + self.entry_price = current_price + logger.info(f"Set reference entry price: ${current_price:.2f} (Note: This is reference price, actual entry price may differ)") + + self.base_amount = new_base_amount + + else: + self.base_amount = 0.0 + # Don't clear entry price, keep for historical record analysis + logger.info(f"Synced {self.symbol}: No position") + + # Save position status + self.save_position() + return True + + except Exception as e: + logger.error(f"Error syncing exchange position: {e}") + return False + + def get_actual_position_from_exchange(self): + """Get actual position data from exchange""" + try: + balances = self.api.get_currency_balances() + if not balances: + return 0.0 + + base_currency = self.symbol.split('-')[0] + return balances.get(base_currency, {}).get('amount', 0.0) + + except Exception as e: + logger.error(f"Error getting exchange position: {e}") + return 0.0 + + def analyze_market(self): + """Comprehensive market analysis (optimized version)""" + try: + # Get market data + df = self.api.get_market_data(self.symbol, self.config['timeframe']) + if df is None or len(df) < 20: + logger.error(f"Failed to get {self.symbol} market data") + return None + + # Calculate technical indicators + technical_indicators = self.technical_analyzer.calculate_indicators(df) + if not technical_indicators: + logger.warning(f"Failed to calculate {self.symbol} technical indicators") + return None + + # Weighted technical signals + weighted_signals = self.technical_analyzer.generate_weighted_signals(df) + + # DeepSeek AI analysis + deepseek_analysis = self.deepseek.analyze_market(self.symbol, df, technical_indicators) + + # Technical signals + technical_signals = self.technical_analyzer.generate_signals(df) + + return { + 'deepseek': deepseek_analysis, + 'technical_signals': technical_signals, + 'weighted_signals': weighted_signals, + 'technical_indicators': technical_indicators, + 'current_price': df['close'].iloc[-1], + 'market_data': df + } + + except Exception as e: + logger.error(f"{self.symbol} market analysis error: {e}") + return None + + def make_enhanced_decision(self, analysis): + """Enhanced trading decision""" + if analysis is None: + return {'action': 'HOLD', 'confidence': 'LOW', 'reason': 'Insufficient analysis data'} + + deepseek_signal = analysis['deepseek'] + weighted_signals = analysis['weighted_signals'] + technical_indicators = analysis['technical_indicators'] + market_data = analysis['market_data'] + + # Build decision matrix + decision_matrix = self._build_decision_matrix(deepseek_signal, weighted_signals, technical_indicators, market_data) + + return self._finalize_decision(decision_matrix) + + def _build_decision_matrix(self, ai_signal, weighted_signal, technical_indicators, market_data): + """Build decision matrix""" + matrix = { + 'ai_weight': 0.6, # AI analysis weight + 'technical_weight': 0.2, # Traditional technical indicator weight + 'reversal_weight': 0.05, # Reversal signal weight + 'support_resistance_weight': 0.1, # Support resistance weight + 'market_weight': 0.05 # Market state weight + } + + # AI signal scoring + ai_score = self._score_ai_signal(ai_signal) if ai_signal else 0.5 + + # Technical signal scoring + tech_score = self._score_technical_signal(weighted_signal) + + # Market state scoring + market_score = self._score_market_condition(technical_indicators) + + # Reversal signal scoring + reversal_signals = TechnicalAnalyzer.detect_reversal_patterns(market_data) + reversal_score = 0.5 + if reversal_signals: + # Score based on reversal signal quantity and strength + strong_signals = [s for s in reversal_signals if s['strength'] == 'STRONG'] + reversal_score = 0.5 + (len(strong_signals) * 0.1) + (len(reversal_signals) * 0.05) + reversal_score = min(0.9, reversal_score) + + # Support resistance scoring + sr_levels = TechnicalAnalyzer.calculate_support_resistance(market_data) + sr_score = 0.5 + if sr_levels: + # Near support level adds points, near resistance level subtracts points + if sr_levels['current_vs_support'] > -2: # Very close to support + sr_score = 0.7 + elif sr_levels['current_vs_resistance'] < 2: # Very close to resistance + sr_score = 0.3 + + # Comprehensive scoring + total_score = ( + ai_score * matrix['ai_weight'] + + tech_score * matrix['technical_weight'] + + reversal_score * matrix['reversal_weight'] + + sr_score * matrix['support_resistance_weight'] + + market_score * matrix['market_weight'] + ) + logger.info(f"[{self.symbol}] Decision matrix details: " + f"AI score={ai_score:.3f}, technical score={tech_score:.3f}, " + f"market score={market_score:.3f}, reversal score={reversal_score:.3f}, " + f"support resistance score={sr_score:.3f}, total score={total_score:.3f}") + + # Determine action and confidence + if total_score > 0.6: + action = 'BUY' + confidence = 'HIGH' if total_score > 0.75 else 'MEDIUM' + elif total_score < 0.4: + action = 'SELL' + confidence = 'HIGH' if total_score < 0.25 else 'MEDIUM' + else: + action = 'HOLD' + confidence = 'MEDIUM' if abs(total_score - 0.5) > 0.1 else 'LOW' + + return { + 'total_score': total_score, + 'action': action, + 'confidence': confidence, + 'components': { + 'ai_score': ai_score, + 'tech_score': tech_score, + 'market_score': market_score, + 'reversal_score': reversal_score, + 'sr_score': sr_score + } + } + + def _score_ai_signal(self, ai_signal): + """AI signal scoring""" + if not ai_signal: + logger.warning("AI signal is empty, using neutral score") + return 0.5 + + action = ai_signal.get('action', 'HOLD') + confidence = ai_signal.get('confidence', 'MEDIUM') + + # Correction: Redesign scoring logic to ensure score has differentiation + if action == 'BUY': + base_score = 0.7 # Buy base score + elif action == 'SELL': + base_score = 0.3 # Sell base score + else: # HOLD + base_score = 0.5 # Hold neutral score + + # Confidence adjustment - ensure final score is within reasonable range + confidence_adjustments = { + 'HIGH': 0.2, # High confidence +0.2 + 'MEDIUM': 0.0, # Medium confidence unchanged + 'LOW': -0.2 # Low confidence -0.2 + } + + adjustment = confidence_adjustments.get(confidence, 0.0) + final_score = base_score + adjustment + + # Ensure within 0-1 range + final_score = max(0.1, min(0.9, final_score)) + + logger.debug(f"AI signal scoring: action={action}, confidence={confidence}, " + f"base={base_score}, adjustment={adjustment}, final={final_score}") + + return final_score + + def _score_technical_signal(self, weighted_signal): + """Technical signal scoring""" + if not weighted_signal: + logger.warning("Technical signal is empty, using neutral score") + return 0.5 + + action = weighted_signal.get('action', 'HOLD') + score = weighted_signal.get('score', 0.5) + signal_count = weighted_signal.get('signal_count', 0) + + # Adjust based on signal strength and quantity + if action == 'BUY': + # Buy signal: 0.5-1.0 range + if score > 0.7 and signal_count >= 3: + final_score = 0.8 + elif score > 0.6: + final_score = 0.7 + else: + final_score = 0.6 + elif action == 'SELL': + # Sell signal: 0.0-0.5 range + if score < 0.3 and signal_count >= 3: + final_score = 0.2 + elif score < 0.4: + final_score = 0.3 + else: + final_score = 0.4 + else: # HOLD + final_score = 0.5 + + logger.debug(f"Technical signal scoring: action={action}, score={score}, " + f"signal_count={signal_count}, final={final_score}") + + return final_score + + def _score_market_condition(self, technical_indicators): + """Market state scoring""" + try: + # Assess market state based on multiple indicators + rsi = technical_indicators.get('rsi', 50) + if hasattr(rsi, '__len__'): + rsi = rsi.iloc[-1] + + trend_strength = technical_indicators.get('trend_strength', 0) + volatility = technical_indicators.get('volatility', 0) + + # RSI scoring (30-70 is neutral) + rsi_score = 0.5 + if rsi < 30: # Oversold + rsi_score = 0.8 + elif rsi > 70: # Overbought + rsi_score = 0.2 + + # Trend strength scoring + trend_score = 0.5 + (trend_strength * 0.3) # Strong trend favors buying + + # Volatility scoring (medium volatility is best) + vol_score = 0.7 if 0.3 < volatility < 0.6 else 0.5 + + # Comprehensive scoring + return (rsi_score + trend_score + vol_score) / 3 + + except Exception as e: + logger.error(f"Market state scoring error: {e}") + return 0.5 + + def _finalize_decision(self, decision_matrix): + """Final decision""" + action = decision_matrix['action'] + confidence = decision_matrix['confidence'] + total_score = decision_matrix['total_score'] + + reason = f"Comprehensive score: {total_score:.3f} (AI: {decision_matrix['components']['ai_score']:.3f}, " + reason += f"Technical: {decision_matrix['components']['tech_score']:.3f}, " + reason += f"Market: {decision_matrix['components']['market_score']:.3f}," + reason += f"Reversal: {decision_matrix['components']['reversal_score']:.3f}," + reason += f"Support Resistance: {decision_matrix['components']['sr_score']:.3f})," + + return { + 'action': action, + 'confidence': confidence, + 'reason': reason, + 'total_score': total_score, + 'source': 'Enhanced Decision Matrix' + } + + def execute_trade(self, decision, current_price): + """Execute trade""" + if decision['action'] == 'HOLD': + logger.info(f"[{self.symbol}] Decision is HOLD, not executing trade") + return + + # Sync exchange position + self.sync_with_exchange() + actual_position = self.base_amount # Use synced position + + logger.info(f"[{self.symbol}] Current position: {self.base_amount:.10f}") + + # Only get needed total asset information + total_assets, available_usdt, _ = self.risk_manager.calculate_total_assets() + + if decision['action'] == 'BUY': + # Get market analysis for dynamic position calculation + market_analysis = self.analyze_market() + if market_analysis is None: + logger.warning(f"[{self.symbol}] Unable to get market analysis, using base position") + buy_amount = self.risk_manager.get_position_size(self.symbol, decision['confidence'], current_price) + else: + # Use dynamic position calculation + buy_amount = self.risk_manager.get_dynamic_position_size( + self.symbol, decision['confidence'], market_analysis['technical_indicators'], current_price + ) + + if buy_amount == 0: + logger.warning(f"[{self.symbol}] Calculated position size is 0, skipping buy") + return + + # Get exchange minimum trade quantity for buy check + min_sz, _ = self.api.get_instrument_info(self.symbol) + if min_sz is None: + min_sz = self.api.get_default_min_size(self.symbol) + + # Calculate base currency quantity to buy + estimated_base_amount = buy_amount / current_price if current_price > 0 else 0 + + logger.info(f"[{self.symbol}] Pre-buy check: estimated quantity={estimated_base_amount:.10f}, " + f"minimum trade quantity={min_sz}, available USDT=${available_usdt:.2f}, " + f"buy amount=${buy_amount:.2f}") + + # Check if buy quantity meets exchange minimum requirement + if estimated_base_amount < min_sz: + logger.warning(f"[{self.symbol}] Estimated buy quantity {estimated_base_amount:.10f} less than minimum trade quantity {min_sz}, canceling buy") + return + + # Check if quote currency balance is sufficient + if available_usdt < buy_amount: + logger.warning(f"[{self.symbol}] Quote currency balance insufficient: need ${buy_amount:.2f}, current ${available_usdt:.2f}") + return + + logger.info(f"[{self.symbol}] Creating buy order: amount=${buy_amount:.2f}, price=${current_price:.2f}") + order_id = self.api.create_order( + symbol=self.symbol, + side='buy', + amount=buy_amount + ) + + if order_id: + # Wait for order completion + order_status = self.api.wait_for_order_completion(self.symbol, order_id) + if order_status is None: + logger.error(f"[{self.symbol}] Order not completed") + return + + # Get actual fill price and quantity + fill_price = order_status['avgPx'] + fill_amount = order_status['accFillSz'] + + # Update position status + self.base_amount += fill_amount + + # Update average entry price (weighted average) + if self.entry_price > 0: + # If adding position, calculate weighted average price + self.entry_price = (self.entry_price * (self.base_amount - fill_amount) + fill_price * fill_amount) / self.base_amount + logger.info(f"[{self.symbol}] Position addition operation, updated average entry price: ${self.entry_price:.2f}") + else: + self.entry_price = fill_price + + logger.info(f"[{self.symbol}] Buy successful: quantity={fill_amount:.10f}, price=${fill_price:.2f}") + logger.info(f"[{self.symbol}] Updated position: {self.symbol.split('-')[0]}={self.base_amount:.10f}") + + # Sync exchange position + self.sync_with_exchange() + + # Set dynamic stop-loss + trailing_percent = self.risk_manager.trailing_stop_percent + self.db.set_dynamic_stop(self.symbol, fill_price, trailing_percent=trailing_percent) + logger.info(f"[{self.symbol}] Set dynamic stop-loss: initial price=${fill_price:.2f}, trailing_percent={trailing_percent:.1%}") + + # Note: sync_with_exchange already saves position, no need to save again here + self.risk_manager.increment_trade_count(self.symbol) + else: + logger.error(f"[{self.symbol}] Buy order creation failed") + + elif decision['action'] == 'SELL': + if actual_position <= 0: + logger.info(f"[{self.symbol}] Exchange actual position is 0, ignoring sell signal") + # Update local status + self.base_amount = 0 + self.entry_price = 0 + self.save_position() + return + + # Prepare position information for AI analysis + position_info = self._prepare_position_info(actual_position, current_price, total_assets) + + # Get market analysis data (for technical indicators) + market_analysis = self.analyze_market() + + # Use DeepSeek to analyze sell proportion + try: + sell_proportion = self.deepseek.analyze_sell_proportion( + self.symbol, position_info, market_analysis, current_price + ) + except Exception as e: + logger.warning(f"[{self.symbol}] DeepSeek sell proportion analysis failed: {e}, using fallback decision") + sell_proportion = self.deepseek._get_fallback_sell_proportion(position_info, decision['confidence']) + + # Calculate sell quantity based on AI suggestion + sell_amount = actual_position * sell_proportion + + # Ensure sell quantity doesn't exceed actual position + sell_amount = min(sell_amount, actual_position) + + logger.info(f"[{self.symbol}] AI suggested sell proportion: {sell_proportion:.1%}, calculated sell quantity: {sell_amount:.10f}") + + # Get exchange minimum trade quantity for sell check + min_sz, _ = self.api.get_instrument_info(self.symbol) + if min_sz is None: + min_sz = self.api.get_default_min_size(self.symbol) + + # Check if sell quantity is less than exchange minimum trade quantity + if sell_amount < min_sz: + if sell_proportion < 1.0: # Partial sell + logger.info(f"[{self.symbol}] Sell quantity {sell_amount:.10f} less than minimum trade quantity {min_sz}, ignoring this partial sell") + return + else: # Full sell + logger.info(f"[{self.symbol}] Position quantity {sell_amount:.10f} less than minimum trade quantity {min_sz}, dust position, ignoring sell") + return + + # Record sell decision details + logger.info(f"[{self.symbol}] Final sell decision: proportion={sell_proportion:.1%}, quantity={sell_amount:.10f}, " + f"reason={decision.get('reason', 'AI decision')}") + + # Execute sell order + order_id = self.api.create_order( + symbol=self.symbol, + side='sell', + amount=sell_amount + ) + + if order_id: + # Wait for order completion + order_status = self.api.wait_for_order_completion(self.symbol, order_id) + if order_status is None: + logger.error(f"[{self.symbol}] Order not completed") + return + + # Get actual fill price and quantity + fill_price = order_status['avgPx'] + fill_amount = order_status['accFillSz'] + + # Calculate profit + profit = (fill_price - self.entry_price) * fill_amount + profit_percent = (fill_price / self.entry_price - 1) * 100 if self.entry_price > 0 else 0 + + # Sync exchange position + self.sync_with_exchange() + + logger.info(f"[{self.symbol}] Sell successful: quantity={fill_amount:.10f}, price=${fill_price:.2f}") + logger.info(f"[{self.symbol}] Profit: ${profit:.2f} ({profit_percent:+.2f}%)") + logger.info(f"[{self.symbol}] Updated position: {self.symbol.split('-')[0]}={self.base_amount:.10f}") + + # Save trade record + self.db.save_trade_record(self.symbol, 'sell', fill_amount, fill_price, order_id) + + # If fully sold, reset status + if self.base_amount <= 0: + self.entry_price = 0 + self.db.delete_dynamic_stop(self.symbol) + logger.info(f"[{self.symbol}] Position cleared, removed dynamic stop-loss") + else: + # After partial sell, update dynamic stop-loss + self.db.set_dynamic_stop(self.symbol, self.entry_price, trailing_percent=0.03, multiplier=2) + logger.info(f"[{self.symbol}] After partial sell, reset dynamic stop-loss") + + # Note: sync_with_exchange already saves position, no need to save again here + self.risk_manager.increment_trade_count(self.symbol) + else: + logger.error(f"[{self.symbol}] Sell order creation failed") + + def _prepare_position_info(self, actual_position, current_price, total_assets): + """Prepare position information for AI analysis""" + try: + # Calculate position value + position_value = actual_position * current_price + + # Calculate position ratio + position_ratio = position_value / total_assets if total_assets > 0 else 0 + + # Calculate profit ratio + profit_ratio = (current_price - self.entry_price) / self.entry_price if self.entry_price > 0 else 0 + + # Calculate distance to stop-loss/take-profit + stop_loss_price = self.entry_price * (1 - self.risk_manager.stop_loss) if self.entry_price > 0 else 0 + take_profit_price = self.entry_price * (1 + self.risk_manager.take_profit) if self.entry_price > 0 else 0 + + distance_to_stop_loss = (current_price - stop_loss_price) / current_price if current_price > 0 else 0 + distance_to_take_profit = (take_profit_price - current_price) / current_price if current_price > 0 else 0 + + # Get market volatility + market_data = self.api.get_market_data(self.symbol, '1H', limit=24) + if market_data is not None and len(market_data) > 0: + market_volatility = market_data['close'].pct_change().std() * 100 + else: + market_volatility = 0 + + # Get trend strength + market_analysis = self.analyze_market() + trend_strength = 0 + if market_analysis and 'technical_indicators' in market_analysis: + trend_strength = market_analysis['technical_indicators'].get('trend_strength', 0) + + position_info = { + 'base_amount': actual_position, + 'entry_price': self.entry_price, + 'current_price': current_price, + 'position_value': position_value, + 'position_ratio': position_ratio, + 'profit_ratio': profit_ratio, + 'distance_to_stop_loss': distance_to_stop_loss, + 'distance_to_take_profit': distance_to_take_profit, + 'market_volatility': market_volatility, + 'trend_strength': trend_strength, + 'total_assets': total_assets + } + + logger.debug(f"[{self.symbol}] Position analysis information: ratio={position_ratio:.2%}, profit={profit_ratio:+.2%}, " + f"distance to stop-loss={distance_to_stop_loss:.2%}, volatility={market_volatility:.2f}%") + + return position_info + + except Exception as e: + logger.error(f"[{self.symbol}] Error preparing position information: {e}") + # Return basic position information + return { + 'base_amount': actual_position, + 'entry_price': self.entry_price, + 'current_price': current_price, + 'position_value': actual_position * current_price, + 'position_ratio': 0, + 'profit_ratio': 0, + 'distance_to_stop_loss': 0, + 'distance_to_take_profit': 0, + 'market_volatility': 0, + 'trend_strength': 0, + 'total_assets': total_assets + } + + def start_dynamic_stop_monitor(self): + """Start dynamic stop-loss monitoring""" + if self.monitor_running and self.monitor_thread and self.monitor_thread.is_alive(): + logger.info(f"[{self.symbol}] Dynamic stop-loss monitoring already running") + return + + self.monitor_running = True + self.monitor_thread = threading.Thread(target=self._dynamic_stop_monitor_loop, name=f"StopMonitor-{self.symbol}") + self.monitor_thread.daemon = True + self.monitor_thread.start() + logger.info(f"[{self.symbol}] Dynamic stop-loss monitoring started") + + def stop_dynamic_stop_monitor(self): + """Stop dynamic stop-loss monitoring""" + self.monitor_running = False + if self.monitor_thread and self.monitor_thread.is_alive(): + self.monitor_thread.join(timeout=5) + if self.monitor_thread.is_alive(): + logger.warning(f"{self.symbol} dynamic stop-loss monitoring thread didn't exit normally") + else: + logger.info(f"{self.symbol} dynamic stop-loss monitoring stopped") + + def _dynamic_stop_monitor_loop(self): + """Dynamic stop-loss monitoring loop""" + while self.monitor_running: + try: + # Only monitor when there is position + if self.base_amount > 0: + self.check_dynamic_stops() + + # Monitor every 5 minutes + time.sleep(self.monitor_interval) + + except Exception as e: + logger.error(f"[{self.symbol}] Dynamic stop-loss monitoring error: {e}") + time.sleep(60) # Wait 1 minute before continuing when error occurs + + def check_dynamic_stops(self): + """Check dynamic stop-loss conditions""" + try: + # Get current price + current_price = self.api.get_current_price(self.symbol) + if not current_price: + logger.warning(f"[{self.symbol}] Unable to get current price, skipping stop-loss check") + return + + # Get dynamic stop-loss information + stop_info = self.db.get_dynamic_stop(self.symbol) + if not stop_info: + logger.debug(f"[{self.symbol}] No dynamic stop-loss settings") + return + + stop_loss_price = stop_info['current_stop_loss'] + take_profit_price = stop_info['current_take_profit'] + trailing_percent = stop_info['trailing_percent'] + + logger.debug(f"[{self.symbol}] Dynamic stop-loss check: current price=${current_price:.2f}, stop-loss=${stop_loss_price:.2f}, take-profit=${take_profit_price:.2f}") + + # Check stop-loss condition + if current_price <= stop_loss_price: + logger.warning(f"⚠️ {self.symbol} triggered dynamic stop-loss! Current price=${current_price:.2f} <= stop-loss price=${stop_loss_price:.2f}") + self.execute_trade({ + 'action': 'SELL', + 'confidence': 'HIGH', + 'reason': f'Dynamic stop-loss triggered: {current_price:.2f} <= {stop_loss_price:.2f}' + }, current_price) + return + + # Check take-profit condition + if current_price >= take_profit_price: + logger.info(f"🎯 {self.symbol} triggered dynamic take-profit! Current price=${current_price:.2f} >= take-profit price=${take_profit_price:.2f}") + self.execute_trade({ + 'action': 'SELL', + 'confidence': 'HIGH', + 'reason': f'Dynamic take-profit triggered: {current_price:.2f} >= {take_profit_price:.2f}' + }, current_price) + return + + # Update trailing stop-loss (only moves upward) - fix logic + if stop_info and current_price > stop_info.get('highest_price', 0): + new_stop_loss = self.db.update_dynamic_stop(self.symbol, current_price, trailing_percent=trailing_percent, multiplier=2) + if new_stop_loss: + logger.info(f"[{self.symbol}] Trailing stop-loss updated: new stop-loss price=${new_stop_loss:.2f}") + + except Exception as e: + logger.error(f"[{self.symbol}] Error checking dynamic stop-loss: {e}") + + def run(self): + """Run strategy (using enhanced decision)""" + self.strategy_running = True + + while self.strategy_running: + try: + logger.info(f"\n=== {self.symbol} Strategy Auto Execution ===") + + # Market analysis + analysis = self.analyze_market() + if analysis is None: + logger.warning("Analysis failed, waiting for next execution") + time.sleep(self.config['interval']) + continue + + # Make trading decision + decision = self.make_enhanced_decision(analysis) + logger.info(f"{self.symbol} Decision: {decision['action']} (Confidence: {decision['confidence']}, Score: {decision['total_score']:.3f})") + + if decision['reason']: + logger.info(f"Reason: {decision['reason']}") + + # Execute trade + self.execute_trade(decision, analysis['current_price']) + + # Risk management check + self.check_risk_management(analysis['current_price']) + + time.sleep(self.config['interval']) + + except Exception as e: + logger.error(f"Strategy execution error: {e}") + if "Network" in str(e) or "Connection" in str(e): + wait_time = min(300, self.config['interval'] * 2) # Network error wait longer + else: + wait_time = self.config['interval'] + + if self.strategy_running: + time.sleep(wait_time) + + def stop_strategy(self): + """Stop strategy running""" + with threading.Lock(): # Add thread lock + self.strategy_running = False + logger.info(f"{self.symbol} strategy stop signal sent") + + def check_risk_management(self, current_price): + """Check risk management (fixed stop-loss/take-profit)""" + if self.base_amount == 0: + return + + # Calculate profit/loss percentage + pnl_pct = (current_price - self.entry_price) / self.entry_price if self.entry_price > 0 else 0 + + # Fixed stop-loss/take-profit check + if pnl_pct <= -self.risk_manager.stop_loss: + logger.warning(f"⚠️ {self.symbol} triggered fixed stop-loss ({pnl_pct:.2%})") + self.execute_trade({ + 'action': 'SELL', + 'confidence': 'HIGH', + 'reason': 'Fixed stop-loss triggered' + }, current_price) + elif pnl_pct >= self.risk_manager.take_profit: + logger.info(f"🎯 {self.symbol} triggered fixed take-profit ({pnl_pct:.2%})") + self.execute_trade({ + 'action': 'SELL', + 'confidence': 'HIGH', + 'reason': 'Fixed take-profit triggered' + }, current_price)