import logging from datetime import datetime, timezone, timedelta from typing import List, Optional from data_fetcher import BinanceDataFetcher from technical_analyzer import TechnicalAnalyzer, CoinSignal from database import DatabaseManager from dingtalk_notifier import DingTalkNotifier import os # 东八区时区 BEIJING_TZ = timezone(timedelta(hours=8)) def get_beijing_time(): """获取当前东八区时间用于显示""" return datetime.now(BEIJING_TZ) class CoinSelectionEngine: def __init__(self, api_key=None, secret=None, db_path="trading.db", use_market_cap_ranking=True, dingtalk_webhook=None, dingtalk_secret=None): """初始化选币引擎 Args: api_key: Binance API密钥 secret: Binance API密钥 db_path: 数据库路径 use_market_cap_ranking: 是否使用市值排名(True=市值排名,False=交易量排名) dingtalk_webhook: 钉钉机器人webhook URL dingtalk_secret: 钉钉机器人加签密钥 """ self.data_fetcher = BinanceDataFetcher(api_key, secret) self.analyzer = TechnicalAnalyzer() self.db = DatabaseManager(db_path) self.use_market_cap_ranking = use_market_cap_ranking # 初始化钉钉通知器 # 优先使用传入的参数,其次使用环境变量 webhook_url = dingtalk_webhook or os.getenv('DINGTALK_WEBHOOK_URL') webhook_secret = dingtalk_secret or os.getenv('DINGTALK_WEBHOOK_SECRET') self.dingtalk_notifier = DingTalkNotifier(webhook_url, webhook_secret) # 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(), # 确保输出到控制台 logging.FileHandler('coin_selection.log') ], force=True # 强制重新配置日志 ) self.logger = logging.getLogger(__name__) def run_coin_selection(self) -> List[CoinSignal]: """执行完整的选币流程 - 使用动态时间周期""" self.logger.info("开始执行选币流程...") try: # 1. 获取Top50 USDT交易对(按市值或交易量排序) if self.use_market_cap_ranking: self.logger.info("获取市值排名Top50 USDT交易对...") top_symbols = self.data_fetcher.get_top_market_cap_usdt_pairs(50) else: self.logger.info("获取交易量Top50 USDT交易对...") top_symbols = self.data_fetcher.get_top_usdt_pairs(50) if not top_symbols: self.logger.error("无法获取交易对数据") return [] # 2. 获取动态时间周期配置 optimal_timeframes = self.analyzer.get_optimal_timeframes_for_analysis() self.logger.info(f"使用动态时间周期: {optimal_timeframes}") # 3. 批量获取市场数据 self.logger.info(f"获取{len(top_symbols)}个币种的市场数据...") market_data = self.data_fetcher.batch_fetch_data(top_symbols, optimal_timeframes) if not market_data: self.logger.error("无法获取市场数据") return [] # 4. 执行技术分析选币 - 同时输出多空信号 self.logger.info("执行技术分析,寻找多空投资机会...") # 先测试策略分布(调试用) self.logger.info("测试策略分布...") self.analyzer.test_strategy_distribution(market_data) signals_result = self.analyzer.select_coins(market_data) # 获取所有信号 all_signals = signals_result.get('all', []) long_signals = signals_result.get('long', []) short_signals = signals_result.get('short', []) if not all_signals: self.logger.warning("没有找到符合条件的多空信号") return [] # 5. 打印策略分布统计 self._log_strategy_distribution(all_signals) # 6. 保存选币结果到数据库 self.logger.info(f"保存{len(all_signals)}个选币结果到数据库...") saved_count = 0 for signal in all_signals: try: selection_id = self.db.insert_coin_selection( symbol=signal.symbol, score=signal.score, reason=signal.reason, entry_price=signal.entry_price, stop_loss=signal.stop_loss, take_profit=signal.take_profit, timeframe=signal.timeframe, strategy_type=signal.strategy_type, holding_period=signal.holding_period, risk_reward_ratio=signal.risk_reward_ratio, expiry_hours=signal.expiry_hours, action_suggestion=signal.action_suggestion, signal_type=signal.signal_type, direction=signal.direction ) signal_type_cn = "多头" if signal.signal_type == "LONG" else "空头" self.logger.info(f"保存{signal.symbol}({signal.strategy_type}-{signal_type_cn})选币结果,ID: {selection_id}") saved_count += 1 except Exception as e: self.logger.error(f"保存{signal.symbol}选币结果失败: {e}") # 检查并标记过期的选币 self.db.check_and_expire_selections() self.logger.info(f"选币完成!成功保存{saved_count}个信号(多头: {len(long_signals)}个, 空头: {len(short_signals)}个)") # # 发送钉钉通知 # try: # self.logger.info("发送钉钉通知...") # notification_sent = self.dingtalk_notifier.send_coin_selection_notification(all_signals) # if notification_sent: # self.logger.info("✅ 钉钉通知发送成功") # else: # self.logger.info("📱 钉钉通知发送失败或未配置") # except Exception as e: # self.logger.error(f"发送钉钉通知时出错: {e}") return all_signals except Exception as e: self.logger.error(f"选币流程执行失败: {e}") return [] def _log_strategy_distribution(self, signals: List[CoinSignal]): """统计并记录策略分布""" strategy_stats = {} for signal in signals: strategy = signal.strategy_type signal_type = signal.signal_type key = f"{strategy}-{signal_type}" if key not in strategy_stats: strategy_stats[key] = {'count': 0, 'avg_score': 0, 'scores': []} strategy_stats[key]['count'] += 1 strategy_stats[key]['scores'].append(signal.score) # 计算平均分数 for key, stats in strategy_stats.items(): stats['avg_score'] = sum(stats['scores']) / len(stats['scores']) self.logger.info("策略分布统计:") for key, stats in sorted(strategy_stats.items(), key=lambda x: x[1]['count'], reverse=True): self.logger.info(f" {key}: {stats['count']}个信号, 平均分数: {stats['avg_score']:.1f}") def run_strategy_specific_analysis(self, symbols: List[str], strategy_name: str) -> List[CoinSignal]: """针对特定策略运行专门的分析""" try: # 获取该策略需要的时间周期 required_timeframes = self.analyzer.get_required_timeframes().get(strategy_name, ['1h', '4h', '1d']) self.logger.info(f"对{len(symbols)}个币种执行{strategy_name}策略分析,使用时间周期: {required_timeframes}") # 获取对应的市场数据 market_data = self.data_fetcher.batch_fetch_data(symbols, required_timeframes) if not market_data: self.logger.error("无法获取市场数据") return [] # 强制使用指定策略进行分析 signals = [] for symbol, data in market_data.items(): timeframe_data = data.get('timeframes', {}) volume_24h_usd = data.get('volume_24h_usd', 0) # 直接使用指定策略分析 symbol_signals = self.analyzer.analyze_single_coin_with_strategy( symbol, timeframe_data, volume_24h_usd, strategy_name ) signals.extend(symbol_signals) self.logger.info(f"{strategy_name}策略分析完成,找到{len(signals)}个信号") return signals except Exception as e: self.logger.error(f"{strategy_name}策略分析失败: {e}") return [] def get_latest_selections(self, limit=20, offset=0) -> List[dict]: """获取最新的选币结果 - 支持分页""" try: results = self.db.get_active_selections(limit, offset) selections = [] for row in results: # 使用字段名而不是索引来避免错误 conn = self.db.get_connection() cursor = conn.cursor() cursor.execute(''' SELECT id, symbol, score, reason, entry_price, stop_loss, take_profit, timeframe, selection_time, status, actual_entry_price, exit_price, exit_time, pnl_percentage, notes, strategy_type, holding_period, risk_reward_ratio, expiry_time, is_expired, action_suggestion, signal_type, direction FROM coin_selections WHERE id = ? ''', (row[0],)) detailed_row = cursor.fetchone() conn.close() if detailed_row: selection = { 'id': detailed_row[0], 'symbol': detailed_row[1], 'score': detailed_row[2], 'reason': detailed_row[3], 'entry_price': detailed_row[4], 'stop_loss': detailed_row[5], 'take_profit': detailed_row[6], 'timeframe': detailed_row[7], 'selection_time': detailed_row[8], # 保持UTC时间,在Web层转换 'status': detailed_row[9], 'actual_entry_price': detailed_row[10], 'exit_price': detailed_row[11], 'exit_time': detailed_row[12], 'pnl_percentage': detailed_row[13], 'notes': detailed_row[14], 'strategy_type': detailed_row[15] or '中线', 'holding_period': detailed_row[16] or 7, 'risk_reward_ratio': detailed_row[17] or 2.0, 'expiry_time': detailed_row[18], 'is_expired': detailed_row[19] or False, 'action_suggestion': detailed_row[20] or '等待回调买入', 'signal_type': detailed_row[21] or 'LONG', 'direction': detailed_row[22] or 'BUY' } # 获取当前价格 current_price = self.data_fetcher.get_current_price(detailed_row[1]) if current_price: selection['current_price'] = current_price selection['price_change_percent'] = ((current_price - detailed_row[4]) / detailed_row[4]) * 100 selections.append(selection) return selections except Exception as e: self.logger.error(f"获取选币结果失败: {e}") return [] def update_selection_status(self, selection_id: int, status: str, exit_price: Optional[float] = None): """更新选币状态""" try: pnl_percentage = None if exit_price and status in ['completed', 'stopped']: # 计算收益率 conn = self.db.get_connection() cursor = conn.cursor() cursor.execute('SELECT entry_price FROM coin_selections WHERE id = ?', (selection_id,)) result = cursor.fetchone() if result: entry_price = result[0] pnl_percentage = ((exit_price - entry_price) / entry_price) * 100 conn.close() self.db.update_selection_status(selection_id, status, exit_price, pnl_percentage) self.logger.info(f"更新选币{selection_id}状态为{status}") except Exception as e: self.logger.error(f"更新选币状态失败: {e}") def set_dingtalk_webhook(self, webhook_url: str, webhook_secret: str = None): """设置钉钉webhook URL和密钥 Args: webhook_url: 钉钉机器人webhook URL webhook_secret: 钉钉机器人加签密钥(可选) """ self.dingtalk_notifier = DingTalkNotifier(webhook_url, webhook_secret) self.logger.info("钉钉webhook配置已更新") def test_dingtalk_notification(self) -> bool: """测试钉钉通知功能 Returns: bool: 测试是否成功 """ return self.dingtalk_notifier.send_test_message() def print_selection_summary(self, signals: List[CoinSignal]): """打印选币结果摘要""" if not signals: self.logger.warning("没有找到符合条件的币种") print("没有找到符合条件的币种") return summary = f"\n=== 选币结果 ({get_beijing_time().strftime('%Y-%m-%d %H:%M:%S CST')}) ===\n" summary += f"共选出 {len(signals)} 个币种:\n\n" for i, signal in enumerate(signals, 1): summary += f"{i}. {signal.symbol} [{signal.strategy_type}] - {signal.action_suggestion}\n" summary += f" 评分: {signal.score:.1f}分 ({signal.confidence}信心)\n" summary += f" 理由: {signal.reason}\n" summary += f" 入场: ${signal.entry_price:.4f}\n" summary += f" 止损: ${signal.stop_loss:.4f} ({((signal.stop_loss - signal.entry_price) / signal.entry_price * 100):.2f}%)\n" summary += f" 止盈: ${signal.take_profit:.4f} ({((signal.take_profit - signal.entry_price) / signal.entry_price * 100):.2f}%)\n" summary += f" 风险回报比: 1:{signal.risk_reward_ratio:.2f}\n" summary += f" 持仓周期: {signal.holding_period}天 | 有效期: {signal.expiry_hours}小时\n" summary += "-" * 50 + "\n" self.logger.info("选币结果摘要:") print(summary) if __name__ == "__main__": # 创建选币引擎实例 from config import * engine = CoinSelectionEngine( api_key=BINANCE_API_KEY, secret=BINANCE_SECRET, db_path=DATABASE_PATH, use_market_cap_ranking=USE_MARKET_CAP_RANKING, dingtalk_webhook=DINGTALK_WEBHOOK_URL, dingtalk_secret=DINGTALK_WEBHOOK_SECRET ) # 执行选币 selected_coins = engine.run_coin_selection() # 打印结果 engine.print_selection_summary(selected_coins)