commit 65276c5beb7ad7465ac6a7be1060f6371000515d Author: aaron <> Date: Thu Aug 14 10:06:19 2025 +0800 commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2c25ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,123 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# PEP 582 +__pypackages__/ + +# Celery +celerybeat-schedule +celery.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Spyder +.spyderproject +.spyproject + +# Rope +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Trading AI specific +*.db +*.sqlite +*.sqlite3 +trading.db +coin_selection.log +*.log +logs/ + +# Configuration files with secrets +config.py +.env + +# Data files +data/ +*.csv +*.json +backups/ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Docker +.dockerignore + +# Temporary files +*.tmp +*.temp +temp/ +tmp/ \ No newline at end of file diff --git a/DOCKER_DEPLOYMENT.md b/DOCKER_DEPLOYMENT.md new file mode 100644 index 0000000..292cdc7 --- /dev/null +++ b/DOCKER_DEPLOYMENT.md @@ -0,0 +1,172 @@ +# Trading AI - Docker 部署指南 + +## 🚀 快速开始 + +### 1. 准备配置文件 + +复制配置模板并填写你的配置: +```bash +cp config_example.py config.py +# 编辑 config.py,填入你的钉钉webhook配置 +``` + +### 2. 环境变量配置(可选) + +创建 `.env` 文件: +```bash +# 钉钉机器人配置 +DINGTALK_WEBHOOK_URL=https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN +DINGTALK_WEBHOOK_SECRET=SEC_YOUR_SECRET + +# 选币配置 +USE_MARKET_CAP_RANKING=true +COIN_LIMIT=50 +``` + +### 3. 启动服务 + +```bash +# 构建并启动所有服务 +docker-compose up -d + +# 查看服务状态 +docker-compose ps + +# 查看日志 +docker-compose logs -f +``` + +## 📋 服务说明 + +### 服务架构 +- **coin-selector**: 选币引擎,执行技术分析和选币逻辑 +- **web-dashboard**: Web仪表板,端口5000,提供可视化界面 +- **scheduler**: 定时调度服务,自动执行选币任务 + +### 端口映射 +- Web界面: http://localhost:5000 + +## 🛠️ 管理命令 + +```bash +# 查看运行状态 +docker-compose ps + +# 重启服务 +docker-compose restart + +# 停止服务 +docker-compose down + +# 查看特定服务日志 +docker-compose logs coin-selector +docker-compose logs web-dashboard +docker-compose logs scheduler + +# 进入容器调试 +docker-compose exec coin-selector bash + +# 更新服务 +docker-compose pull +docker-compose up -d --build +``` + +## 📁 数据持久化 + +- `./data/`: 数据文件目录 +- `./logs/`: 日志文件目录 +- `trading-db`: 数据库卷 +- `config.py`: 配置文件(只读挂载) + +## 🔧 配置说明 + +### 环境变量优先级 +1. `.env` 文件中的环境变量 +2. `config.py` 文件中的配置 +3. 默认值 + +### 重要配置项 +- `USE_MARKET_CAP_RANKING`: 是否按市值排名选币 +- `COIN_LIMIT`: 分析币种数量限制 +- `DINGTALK_WEBHOOK_URL`: 钉钉机器人地址 +- `DINGTALK_WEBHOOK_SECRET`: 钉钉加签密钥 + +## 🔍 健康检查 + +所有服务都配置了健康检查: +- **coin-selector**: 检查数据库文件存在性 +- **web-dashboard**: HTTP健康检查端点 +- **scheduler**: 进程存活检查 + +## 📊 监控和日志 + +```bash +# 实时查看所有服务日志 +docker-compose logs -f + +# 查看特定时间范围的日志 +docker-compose logs --since="2h" coin-selector + +# 查看系统资源使用 +docker stats +``` + +## 🚨 故障排除 + +### 常见问题 + +1. **TA-Lib安装失败** + ```bash + # 重新构建镜像 + docker-compose build --no-cache + ``` + +2. **数据库权限问题** + ```bash + # 检查数据卷权限 + docker-compose exec coin-selector ls -la /app/ + ``` + +3. **钉钉通知失败** + ```bash + # 检查配置 + docker-compose exec coin-selector python setup_dingtalk.py test + ``` + +4. **端口冲突** + ```bash + # 修改docker-compose.yml中的端口映射 + ports: + - "5001:5000" # 改为5001端口 + ``` + +### 调试模式 + +```bash +# 以调试模式启动 +docker-compose -f docker-compose.yml -f docker-compose.debug.yml up + +# 单独测试选币功能 +docker-compose run --rm coin-selector python coin_selection_engine.py +``` + +## 🔄 定期维护 + +```bash +# 清理未使用的Docker资源 +docker system prune -a + +# 备份数据库 +docker-compose exec coin-selector cp /app/trading.db /app/data/backup_$(date +%Y%m%d).db + +# 清理旧日志 +find ./logs -name "*.log" -mtime +7 -delete +``` + +## 📈 生产环境建议 + +1. **资源限制**: 在docker-compose.yml中添加资源限制 +2. **日志轮转**: 配置日志轮转避免磁盘空间不足 +3. **监控报警**: 集成监控系统(如Prometheus) +4. **备份策略**: 定期备份数据库和配置文件 +5. **安全加固**: 使用非root用户运行容器 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4dd27cd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +FROM python:3.11-slim + +# 设置工作目录 +WORKDIR /app + +# 设置环境变量 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV TZ=Asia/Shanghai + +# 安装系统依赖(TA-Lib需要) +RUN apt-get update && apt-get install -y \ + build-essential \ + wget \ + tar \ + && rm -rf /var/lib/apt/lists/* + +# 安装TA-Lib +RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz && \ + tar -xzf ta-lib-0.4.0-src.tar.gz && \ + cd ta-lib/ && \ + ./configure --prefix=/usr && \ + make && \ + make install && \ + cd .. && \ + rm -rf ta-lib ta-lib-0.4.0-src.tar.gz + +# 复制依赖文件 +COPY requirements.txt . + +# 安装Python依赖 +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# 复制应用代码 +COPY . . + +# 创建必要的目录 +RUN mkdir -p logs data + +# 暴露端口 +EXPOSE 5000 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import requests; requests.get('http://localhost:5000/health', timeout=5)" || exit 1 + +# 启动命令(可通过docker-compose覆盖) +CMD ["python", "coin_selection_engine.py"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6667a74 Binary files /dev/null and b/README.md differ diff --git a/coin_selection_engine.py b/coin_selection_engine.py new file mode 100644 index 0000000..29593c7 --- /dev/null +++ b/coin_selection_engine.py @@ -0,0 +1,357 @@ +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) \ No newline at end of file diff --git a/data_fetcher.py b/data_fetcher.py new file mode 100644 index 0000000..6e3a5f8 --- /dev/null +++ b/data_fetcher.py @@ -0,0 +1,297 @@ +from binance.client import Client +from binance.exceptions import BinanceAPIException, BinanceOrderException +import pandas as pd +import time +import logging +from datetime import datetime, timedelta +from typing import List, Dict, Optional +import requests + +class BinanceDataFetcher: + def __init__(self, api_key=None, secret=None): + """初始化Binance数据获取器""" + try: + # 如果没有提供API密钥,使用公共客户端 + if api_key and secret: + self.client = Client(api_key, secret) + else: + self.client = Client() # 公共客户端,只能访问市场数据 + + # 测试连接 + self.client.ping() + print("Binance API连接成功") + + except Exception as e: + logging.error(f"Binance API初始化失败: {e}") + print(f"Binance API初始化失败: {e}") + raise + + def get_top_usdt_pairs(self, limit=100) -> List[str]: + """获取交易量最大的USDT交易对,排除稳定币""" + try: + print(f"正在获取前{limit}个USDT交易对...") + + # 定义需要排除的稳定币 + stable_coins = { + 'USDCUSDT', 'BUSDUSDT', 'TUSDUSDT', 'PAXUSDT', 'DAIUSDT', + 'FDUSDUSDT', 'USTCUSDT', 'SUSDUSDT', 'GUSDUSDT', 'USDPUSDT' + } + + # 获取24小时ticker统计 + tickers = self.client.get_ticker() + print(f"获取到{len(tickers)}个交易对") + usdt_pairs = [] + + for ticker in tickers: + symbol = ticker['symbol'] + if (symbol.endswith('USDT') and + float(ticker['quoteVolume']) > 2000 * 10000 and + symbol not in stable_coins): + usdt_pairs.append({ + 'symbol': symbol, + 'volume': float(ticker['quoteVolume']), + 'price': float(ticker['lastPrice']) + }) + + # 按交易量排序 + usdt_pairs.sort(key=lambda x: x['volume'], reverse=True) + top_pairs = [pair['symbol'] for pair in usdt_pairs[:limit]] + + logging.info(f"获取到{len(top_pairs)}个USDT交易对(已排除稳定币)") + print(f"成功获取{len(top_pairs)}个USDT交易对(已排除稳定币)") + return top_pairs + + except Exception as e: + logging.error(f"获取交易对失败: {e}") + print(f"获取交易对失败: {e}") + return [] + + def _convert_timeframe(self, timeframe: str) -> str: + """转换时间周期格式""" + timeframe_mapping = { + '1m': Client.KLINE_INTERVAL_1MINUTE, + '5m': Client.KLINE_INTERVAL_5MINUTE, + '15m': Client.KLINE_INTERVAL_15MINUTE, + '30m': Client.KLINE_INTERVAL_30MINUTE, + '1h': Client.KLINE_INTERVAL_1HOUR, + '4h': Client.KLINE_INTERVAL_4HOUR, + '1d': Client.KLINE_INTERVAL_1DAY, + '3d': Client.KLINE_INTERVAL_3DAY, + '1w': Client.KLINE_INTERVAL_1WEEK + } + return timeframe_mapping.get(timeframe, Client.KLINE_INTERVAL_4HOUR) + + def fetch_ohlcv_data(self, symbol: str, timeframe: str, limit: int = 500) -> pd.DataFrame: + """获取K线数据""" + try: + interval = self._convert_timeframe(timeframe) + klines = self.client.get_klines( + symbol=symbol, + interval=interval, + limit=limit + ) + + # 转换为DataFrame + df = pd.DataFrame(klines, columns=[ + 'timestamp', 'open', 'high', 'low', 'close', 'volume', + 'close_time', 'quote_asset_volume', 'number_of_trades', + 'taker_buy_base_asset_volume', 'taker_buy_quote_asset_volume', 'ignore' + ]) + + # 数据类型转换 + df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') + for col in ['open', 'high', 'low', 'close', 'volume']: + df[col] = pd.to_numeric(df[col]) + + # 设置索引 + df.set_index('timestamp', inplace=True) + + # 只保留需要的列 + df = df[['open', 'high', 'low', 'close', 'volume']] + + return df + + except BinanceAPIException as e: + logging.error(f"获取{symbol} {timeframe}数据失败: {e}") + print(f"获取{symbol} {timeframe}数据失败: {e}") + return pd.DataFrame() + except Exception as e: + logging.error(f"获取{symbol} {timeframe}数据时发生未知错误: {e}") + return pd.DataFrame() + + def get_multi_timeframe_data(self, symbol: str, timeframes: List[str] = None) -> Dict[str, pd.DataFrame]: + """获取多时间周期数据""" + if timeframes is None: + timeframes = ['4h', '1h', '15m'] + + data = {} + for tf in timeframes: + df = self.fetch_ohlcv_data(symbol, tf) + if not df.empty: + data[tf] = df + time.sleep(0.1) # 避免频率限制 + + return data + + def get_current_price(self, symbol: str) -> Optional[float]: + """获取当前价格""" + try: + ticker = self.client.get_symbol_ticker(symbol=symbol) + return float(ticker['price']) + except Exception as e: + logging.error(f"获取{symbol}当前价格失败: {e}") + return None + + def get_24h_stats(self, symbol: str) -> Dict: + """获取24小时统计数据""" + try: + ticker = self.client.get_ticker(symbol=symbol) + return { + 'price_change_percent': float(ticker['priceChangePercent']), + 'volume': float(ticker['volume']), + 'quote_volume': float(ticker['quoteVolume']), + 'high_24h': float(ticker['highPrice']), + 'low_24h': float(ticker['lowPrice']) + } + except Exception as e: + logging.error(f"获取{symbol} 24h统计数据失败: {e}") + return {} + + def batch_fetch_data(self, symbols: List[str], timeframes: List[str] = None) -> Dict[str, Dict]: + """批量获取多个币种的多时间周期数据""" + if timeframes is None: + timeframes = ['4h', '1h', '15m'] + + all_data = {} + total = len(symbols) + + for i, symbol in enumerate(symbols): + try: + logging.info(f"正在获取 {symbol} 数据 ({i+1}/{total})") + print(f"正在获取 {symbol} 数据 ({i+1}/{total})") + + # 获取多时间周期K线数据 + timeframe_data = self.get_multi_timeframe_data(symbol, timeframes) + + if timeframe_data: + # 获取24小时统计数据 + stats = self.get_24h_stats(symbol) + + # 组织数据结构以匹配分析器期望 + all_data[symbol] = { + 'timeframes': timeframe_data, + 'volume_24h_usd': stats.get('quote_volume', 0), # 这是24小时USDT交易量 + 'stats': stats + } + + logging.info(f"{symbol} 24小时交易量: ${stats.get('quote_volume', 0):,.0f}") + + # 控制请求频率,避免被限制 + time.sleep(0.2) + + except Exception as e: + logging.error(f"获取{symbol}数据时出错: {e}") + print(f"获取{symbol}数据时出错: {e}") + continue + + logging.info(f"成功获取{len(all_data)}个币种的数据") + print(f"成功获取{len(all_data)}个币种的数据") + return all_data + + def get_top_market_cap_usdt_pairs(self, limit=100) -> List[str]: + """获取市值排名前N的USDT交易对""" + try: + print(f"正在获取市值排名前{limit}个币种...") + + # 从CoinGecko获取市值排名数据 + url = "https://api.coingecko.com/api/v3/coins/markets" + params = { + 'vs_currency': 'usd', + 'order': 'market_cap_desc', + 'per_page': min(limit * 2, 250), # 获取更多数据以确保有足够的匹配 + 'page': 1, + 'sparkline': 'false' + } + + response = requests.get(url, params=params, timeout=10) + if response.status_code != 200: + print(f"CoinGecko API请求失败: {response.status_code}") + # 回退到按交易量获取 + return self.get_top_usdt_pairs(limit) + + market_data = response.json() + print(f"从CoinGecko获取到{len(market_data)}个币种的市值数据") + + # 获取Binance支持的所有USDT交易对 + binance_tickers = self.client.get_ticker() + binance_usdt_symbols = set() + + # 排除稳定币 + stable_coins = { + 'USDCUSDT', 'BUSDUSDT', 'TUSDUSDT', 'PAXUSDT', 'DAIUSDT', + 'FDUSDUSDT', 'USTCUSDT', 'SUSDUSDT', 'GUSDUSDT', 'USDPUSDT' + } + + for ticker in binance_tickers: + symbol = ticker['symbol'] + if (symbol.endswith('USDT') and + float(ticker['quoteVolume']) > 1000000 and # 最低交易量要求 + symbol not in stable_coins): + binance_usdt_symbols.add(symbol) + + print(f"Binance支持的USDT交易对: {len(binance_usdt_symbols)}个") + + # 匹配市值排名和Binance交易对 + matched_pairs = [] + for coin in market_data: + # 尝试不同的符号格式匹配 + symbol_variants = [ + f"{coin['symbol'].upper()}USDT", + f"{coin['id'].upper().replace('-', '')}USDT" + ] + + for variant in symbol_variants: + if variant in binance_usdt_symbols: + matched_pairs.append({ + 'symbol': variant, + 'market_cap_rank': coin['market_cap_rank'], + 'market_cap': coin['market_cap'], + 'name': coin['name'] + }) + print(f"匹配成功: {coin['name']}({coin['symbol']}) -> {variant} (排名#{coin['market_cap_rank']})") + break + + if len(matched_pairs) >= limit: + break + + # 按市值排名排序 + matched_pairs.sort(key=lambda x: x['market_cap_rank'] if x['market_cap_rank'] else 999999) + top_pairs = [pair['symbol'] for pair in matched_pairs[:limit]] + + print(f"成功匹配{len(top_pairs)}个市值排名前{limit}的USDT交易对") + + # 如果匹配数量不足,用交易量排序的方式补充 + if len(top_pairs) < limit: + print(f"市值匹配数量不足({len(top_pairs)}/{limit}),用交易量排序补充...") + volume_pairs = self.get_top_usdt_pairs(limit) + + # 补充未包含的交易对 + for pair in volume_pairs: + if pair not in top_pairs and len(top_pairs) < limit: + top_pairs.append(pair) + + return top_pairs[:limit] + + except Exception as e: + logging.error(f"获取市值排名失败: {e}") + print(f"获取市值排名失败: {e},回退到按交易量获取") + # 出错时回退到原有的交易量排序方式 + return self.get_top_usdt_pairs(limit) + + def get_exchange_info(self): + """获取交易所信息""" + try: + return self.client.get_exchange_info() + except Exception as e: + logging.error(f"获取交易所信息失败: {e}") + return None \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..ceee503 --- /dev/null +++ b/database.py @@ -0,0 +1,266 @@ +import sqlite3 +from datetime import datetime, timezone, timedelta +import logging + +# 设置东八区时区 +BEIJING_TZ = timezone(timedelta(hours=8)) + +def utc_to_beijing(utc_time_str): + """将UTC时间字符串转换为东八区时间字符串""" + try: + # 解析UTC时间字符串 + utc_dt = datetime.fromisoformat(utc_time_str.replace('Z', '+00:00')) + if utc_dt.tzinfo is None: + utc_dt = utc_dt.replace(tzinfo=timezone.utc) + + # 转换为东八区时间 + beijing_dt = utc_dt.astimezone(BEIJING_TZ) + return beijing_dt.strftime('%Y-%m-%d %H:%M:%S') + except: + return utc_time_str + +def get_utc_time(): + """获取UTC时间""" + return datetime.now(timezone.utc) + +class DatabaseManager: + def __init__(self, db_path="trading.db"): + self.db_path = db_path + self.init_database() + + def init_database(self): + """初始化数据库表结构""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 币种基础信息表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS coins ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT UNIQUE NOT NULL, + base_asset TEXT NOT NULL, + quote_asset TEXT NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # K线数据表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS klines ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + timeframe TEXT NOT NULL, + open_time TIMESTAMP NOT NULL, + close_time TIMESTAMP NOT NULL, + open_price REAL NOT NULL, + high_price REAL NOT NULL, + low_price REAL NOT NULL, + close_price REAL NOT NULL, + volume REAL NOT NULL, + quote_volume REAL NOT NULL, + trades_count INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(symbol, timeframe, open_time) + ) + ''') + + # 选币结果表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS coin_selections ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + score REAL NOT NULL, + reason TEXT NOT NULL, + entry_price REAL NOT NULL, + stop_loss REAL NOT NULL, + take_profit REAL NOT NULL, + timeframe TEXT NOT NULL, + selection_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status TEXT DEFAULT 'active', + actual_entry_price REAL, + exit_price REAL, + exit_time TIMESTAMP, + pnl_percentage REAL, + notes TEXT, + strategy_type TEXT NOT NULL DEFAULT '中线', + holding_period INTEGER NOT NULL DEFAULT 7, + risk_reward_ratio REAL NOT NULL DEFAULT 2.0, + expiry_time TIMESTAMP, + is_expired BOOLEAN DEFAULT FALSE, + action_suggestion TEXT DEFAULT '等待回调买入' + ) + ''') + + # 检查并添加新列(如果表已存在) + try: + cursor.execute("ALTER TABLE coin_selections ADD COLUMN strategy_type TEXT NOT NULL DEFAULT '中线'") + except: + pass + + try: + cursor.execute("ALTER TABLE coin_selections ADD COLUMN holding_period INTEGER NOT NULL DEFAULT 7") + except: + pass + + try: + cursor.execute("ALTER TABLE coin_selections ADD COLUMN risk_reward_ratio REAL NOT NULL DEFAULT 2.0") + except: + pass + + try: + cursor.execute("ALTER TABLE coin_selections ADD COLUMN expiry_time TIMESTAMP") + except: + pass + + try: + cursor.execute("ALTER TABLE coin_selections ADD COLUMN is_expired BOOLEAN DEFAULT FALSE") + except: + pass + + try: + cursor.execute("ALTER TABLE coin_selections ADD COLUMN action_suggestion TEXT DEFAULT '等待回调买入'") + except: + pass + + # 添加多空支持字段 + try: + cursor.execute("ALTER TABLE coin_selections ADD COLUMN signal_type TEXT DEFAULT 'LONG'") + except: + pass + + try: + cursor.execute("ALTER TABLE coin_selections ADD COLUMN direction TEXT DEFAULT 'BUY'") + except: + pass + + # 技术指标表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS technical_indicators ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol TEXT NOT NULL, + timeframe TEXT NOT NULL, + indicator_time TIMESTAMP NOT NULL, + ma20 REAL, + ma50 REAL, + ma200 REAL, + rsi REAL, + macd REAL, + macd_signal REAL, + macd_hist REAL, + bb_upper REAL, + bb_middle REAL, + bb_lower REAL, + volume_ma REAL, + fib_618 REAL, + fib_382 REAL, + support_level REAL, + resistance_level REAL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(symbol, timeframe, indicator_time) + ) + ''') + + # 创建索引 + cursor.execute('CREATE INDEX IF NOT EXISTS idx_klines_symbol_time ON klines(symbol, timeframe, open_time)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_selections_symbol_time ON coin_selections(symbol, selection_time)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_indicators_symbol_time ON technical_indicators(symbol, timeframe, indicator_time)') + + conn.commit() + conn.close() + logging.info("数据库初始化完成") + + def get_connection(self): + """获取数据库连接""" + return sqlite3.connect(self.db_path) + + def insert_coin_selection(self, symbol, score, reason, entry_price, stop_loss, take_profit, + timeframe, strategy_type, holding_period, risk_reward_ratio, expiry_hours, action_suggestion, + signal_type="LONG", direction="BUY"): + """插入选币结果 - 支持多空方向""" + conn = self.get_connection() + cursor = conn.cursor() + + # 计算过期时间 + expiry_time = datetime.now(timezone.utc) + timedelta(hours=expiry_hours) + + cursor.execute(''' + INSERT INTO coin_selections + (symbol, score, reason, entry_price, stop_loss, take_profit, timeframe, + strategy_type, holding_period, risk_reward_ratio, expiry_time, action_suggestion, + signal_type, direction) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (symbol, score, reason, entry_price, stop_loss, take_profit, timeframe, + strategy_type, holding_period, risk_reward_ratio, expiry_time, action_suggestion, + signal_type, direction)) + + selection_id = cursor.lastrowid + conn.commit() + conn.close() + return selection_id + + def get_active_selections(self, limit=20, offset=0): + """获取活跃的选币结果,自动标记过期的 - 支持分页""" + conn = self.get_connection() + cursor = conn.cursor() + + # 首先标记过期的选币 + cursor.execute(''' + UPDATE coin_selections + SET is_expired = TRUE, status = 'expired' + WHERE expiry_time < datetime('now') AND status = 'active' + ''') + + cursor.execute(''' + SELECT * FROM coin_selections + WHERE status = 'active' AND is_expired = FALSE + ORDER BY selection_time DESC, score DESC + LIMIT ? OFFSET ? + ''', (limit, offset)) + + results = cursor.fetchall() + conn.commit() # 提交过期状态更新 + conn.close() + return results + + def check_and_expire_selections(self): + """检查并标记过期的选币""" + conn = self.get_connection() + cursor = conn.cursor() + + cursor.execute(''' + UPDATE coin_selections + SET is_expired = TRUE, status = 'expired' + WHERE expiry_time < datetime('now') AND status = 'active' + ''') + + expired_count = cursor.rowcount + conn.commit() + conn.close() + + if expired_count > 0: + logging.info(f"标记了{expired_count}个过期的选币结果") + + return expired_count + + def update_selection_status(self, selection_id, status, exit_price=None, pnl_percentage=None): + """更新选币结果状态""" + conn = self.get_connection() + cursor = conn.cursor() + + if exit_price and pnl_percentage: + cursor.execute(''' + UPDATE coin_selections + SET status = ?, exit_price = ?, exit_time = CURRENT_TIMESTAMP, pnl_percentage = ? + WHERE id = ? + ''', (status, exit_price, pnl_percentage, selection_id)) + else: + cursor.execute(''' + UPDATE coin_selections + SET status = ? + WHERE id = ? + ''', (status, selection_id)) + + conn.commit() + conn.close() \ No newline at end of file diff --git a/dingtalk_notifier.py b/dingtalk_notifier.py new file mode 100644 index 0000000..6df5c76 --- /dev/null +++ b/dingtalk_notifier.py @@ -0,0 +1,244 @@ +import requests +import json +import logging +import hashlib +import hmac +import base64 +import urllib.parse +import time +from datetime import datetime +from typing import List, Optional +from technical_analyzer import CoinSignal + +class DingTalkNotifier: + def __init__(self, webhook_url: str = None, secret: str = None): + """初始化钉钉通知器 + + Args: + webhook_url: 钉钉机器人的webhook URL + secret: 钉钉机器人的加签密钥(可选) + """ + self.webhook_url = webhook_url + self.secret = secret + self.enabled = webhook_url is not None and webhook_url.strip() != "" + + if not self.enabled: + logging.warning("钉钉webhook URL未配置,通知功能已禁用") + elif self.secret: + logging.info("钉钉通知已启用(加签模式)") + else: + logging.info("钉钉通知已启用(普通模式)") + + def send_coin_selection_notification(self, signals: List[CoinSignal]) -> bool: + """发送选币结果通知 + + Args: + signals: 选币信号列表 + + Returns: + bool: 发送是否成功 + """ + if not self.enabled: + logging.info("钉钉通知未启用,跳过发送") + return False + + if not signals: + return self._send_no_signals_notification() + + try: + # 按多头和空头分组 + long_signals = [s for s in signals if s.signal_type == "LONG"] + short_signals = [s for s in signals if s.signal_type == "SHORT"] + + # 构建消息内容 + message = self._build_selection_message(long_signals, short_signals) + + # 发送消息 + return self._send_message(message) + + except Exception as e: + logging.error(f"发送钉钉通知失败: {e}") + return False + + def _build_selection_message(self, long_signals: List[CoinSignal], short_signals: List[CoinSignal]) -> dict: + """构建选币结果消息""" + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # 构建markdown格式消息 + markdown_text = f"# 🎯 加密货币选币结果\n\n" + markdown_text += f"**时间**: {current_time}\n" + markdown_text += f"**总计**: {len(long_signals + short_signals)}个信号 (多头{len(long_signals)}个, 空头{len(short_signals)}个)\n\n" + + # 多头信号 + if long_signals: + markdown_text += "## 📈 多头信号\n\n" + for i, signal in enumerate(long_signals, 1): + confidence_emoji = "🔥" if signal.confidence == "高" else "⚡" if signal.confidence == "中" else "💡" + strategy_emoji = self._get_strategy_emoji(signal.strategy_type) + + markdown_text += f"### {i}. {signal.symbol} {confidence_emoji}\n" + markdown_text += f"- **策略**: {strategy_emoji} {signal.strategy_type}\n" + markdown_text += f"- **评分**: {signal.score:.1f}分 ({signal.confidence}信心)\n" + markdown_text += f"- **建议**: {signal.action_suggestion}\n" + markdown_text += f"- **入场**: ${signal.entry_price:.4f}\n" + markdown_text += f"- **止损**: ${signal.stop_loss:.4f} ({self._get_percentage_change(signal.entry_price, signal.stop_loss):.1f}%)\n" + markdown_text += f"- **止盈**: ${signal.take_profit:.4f} ({self._get_percentage_change(signal.entry_price, signal.take_profit):.1f}%)\n" + markdown_text += f"- **风险回报比**: 1:{signal.risk_reward_ratio:.2f}\n" + markdown_text += f"- **持仓周期**: {signal.holding_period}天\n\n" + + # 空头信号 + if short_signals: + markdown_text += "## 📉 空头信号\n\n" + for i, signal in enumerate(short_signals, 1): + confidence_emoji = "🔥" if signal.confidence == "高" else "⚡" if signal.confidence == "中" else "💡" + strategy_emoji = self._get_strategy_emoji(signal.strategy_type) + + markdown_text += f"### {i}. {signal.symbol} {confidence_emoji}\n" + markdown_text += f"- **策略**: {strategy_emoji} {signal.strategy_type}\n" + markdown_text += f"- **评分**: {signal.score:.1f}分 ({signal.confidence}信心)\n" + markdown_text += f"- **建议**: {signal.action_suggestion}\n" + markdown_text += f"- **入场**: ${signal.entry_price:.4f}\n" + markdown_text += f"- **止损**: ${signal.stop_loss:.4f} ({self._get_percentage_change(signal.entry_price, signal.stop_loss):.1f}%)\n" + markdown_text += f"- **止盈**: ${signal.take_profit:.4f} ({self._get_percentage_change(signal.entry_price, signal.take_profit):.1f}%)\n" + markdown_text += f"- **风险回报比**: 1:{signal.risk_reward_ratio:.2f}\n" + markdown_text += f"- **持仓周期**: {signal.holding_period}天\n\n" + + # 添加风险提示 + markdown_text += "---\n" + markdown_text += "⚠️ **风险提示**: 投资有风险,决策需谨慎。本信号仅供参考,不构成投资建议。\n" + markdown_text += "🤖 *由AI选币系统自动生成*" + + return { + "msgtype": "markdown", + "markdown": { + "title": f"选币结果 ({len(long_signals + short_signals)}个信号)", + "text": markdown_text + } + } + + def _send_no_signals_notification(self) -> bool: + """发送无信号通知""" + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + message = { + "msgtype": "text", + "text": { + "content": f"📊 加密货币选币结果\n\n" + f"时间: {current_time}\n" + f"结果: 暂无符合条件的投资机会\n" + f"建议: 继续观察市场动态\n\n" + f"🤖 由AI选币系统自动生成" + } + } + + return self._send_message(message) + + def _generate_sign_url(self) -> str: + """生成带加签的webhook URL""" + if not self.secret: + return self.webhook_url + + # 当前时间戳(毫秒) + timestamp = str(round(time.time() * 1000)) + + # 拼接字符串 + string_to_sign = f"{timestamp}\n{self.secret}" + string_to_sign_enc = string_to_sign.encode('utf-8') + secret_enc = self.secret.encode('utf-8') + + # HMAC-SHA256加密 + hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest() + + # Base64编码 + sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) + + # 拼接最终URL + if '?' in self.webhook_url: + signed_url = f"{self.webhook_url}×tamp={timestamp}&sign={sign}" + else: + signed_url = f"{self.webhook_url}?timestamp={timestamp}&sign={sign}" + + return signed_url + + def _send_message(self, message: dict) -> bool: + """发送消息到钉钉""" + try: + # 生成签名URL(如果配置了密钥) + url = self._generate_sign_url() + + headers = {'Content-Type': 'application/json'} + response = requests.post( + url, + data=json.dumps(message), + headers=headers, + timeout=10 + ) + + if response.status_code == 200: + result = response.json() + if result.get('errcode') == 0: + logging.info("钉钉通知发送成功") + return True + else: + logging.error(f"钉钉通知发送失败: {result.get('errmsg', '未知错误')}") + return False + else: + logging.error(f"钉钉通知HTTP请求失败: {response.status_code}") + return False + + except Exception as e: + logging.error(f"发送钉钉消息异常: {e}") + return False + + def _get_strategy_emoji(self, strategy: str) -> str: + """获取策略对应的emoji""" + strategy_emojis = { + "超短线": "⚡", + "短线": "🏃", + "中线": "🚶", + "波段": "🌊", + "长线": "🐢", + "趋势": "📈" + } + return strategy_emojis.get(strategy, "📊") + + def _get_percentage_change(self, entry_price: float, target_price: float) -> float: + """计算价格变化百分比""" + if entry_price == 0: + return 0 + return ((target_price - entry_price) / entry_price) * 100 + + def send_test_message(self) -> bool: + """发送测试消息""" + if not self.enabled: + print("钉钉webhook URL未配置,无法发送测试消息") + return False + + test_message = { + "msgtype": "text", + "text": { + "content": f"🤖 加密货币选币系统测试消息\n\n" + f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + f"状态: 系统运行正常\n" + f"功能: 钉钉通知已配置成功" + } + } + + success = self._send_message(test_message) + if success: + print("✅ 钉钉测试消息发送成功") + else: + print("❌ 钉钉测试消息发送失败") + + return success + +# 测试脚本 +if __name__ == "__main__": + # 测试用例 + webhook_url = input("请输入钉钉机器人webhook URL: ").strip() + + if webhook_url: + notifier = DingTalkNotifier(webhook_url) + notifier.send_test_message() + else: + print("未提供webhook URL,退出测试") \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b253d73 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,104 @@ +version: '3.8' + +services: + # 选币引擎服务 + coin-selector: + build: . + container_name: trading-ai-selector + restart: unless-stopped + volumes: + - ./data:/app/data + - ./logs:/app/logs + - ./config.py:/app/config.py:ro # 只读挂载配置文件 + - trading-db:/app/trading.db + environment: + - PYTHONPATH=/app + - TZ=Asia/Shanghai + # 钉钉配置(可选,也可以通过config.py配置) + - DINGTALK_WEBHOOK_URL=${DINGTALK_WEBHOOK_URL:-} + - DINGTALK_WEBHOOK_SECRET=${DINGTALK_WEBHOOK_SECRET:-} + # 选币配置 + - USE_MARKET_CAP_RANKING=${USE_MARKET_CAP_RANKING:-true} + - COIN_LIMIT=${COIN_LIMIT:-50} + command: python coin_selection_engine.py + healthcheck: + test: ["CMD", "python", "-c", "import os; exit(0 if os.path.exists('/app/trading.db') else 1)"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - trading-network + + # Web仪表板服务 + web-dashboard: + build: . + container_name: trading-ai-web + restart: unless-stopped + ports: + - "5000:5000" + volumes: + - ./data:/app/data + - ./logs:/app/logs + - ./static:/app/static + - ./templates:/app/templates + - trading-db:/app/trading.db + environment: + - PYTHONPATH=/app + - TZ=Asia/Shanghai + - FLASK_ENV=production + command: python web_app.py + depends_on: + - coin-selector + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - trading-network + + # 定时调度服务 + scheduler: + build: . + container_name: trading-ai-scheduler + restart: unless-stopped + volumes: + - ./data:/app/data + - ./logs:/app/logs + - ./config.py:/app/config.py:ro + - trading-db:/app/trading.db + environment: + - PYTHONPATH=/app + - TZ=Asia/Shanghai + - DINGTALK_WEBHOOK_URL=${DINGTALK_WEBHOOK_URL:-} + - DINGTALK_WEBHOOK_SECRET=${DINGTALK_WEBHOOK_SECRET:-} + command: python scheduler.py + depends_on: + - coin-selector + networks: + - trading-network + +# 网络配置 +networks: + trading-network: + driver: bridge + +# 数据卷 +volumes: + trading-db: + driver: local + +# 环境变量文件示例 +# 创建 .env 文件并设置以下变量: +# +# # 钉钉机器人配置 +# DINGTALK_WEBHOOK_URL=https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN +# DINGTALK_WEBHOOK_SECRET=SEC_YOUR_SECRET +# +# # 选币配置 +# USE_MARKET_CAP_RANKING=true +# COIN_LIMIT=50 +# +# # Binance API(可选) +# BINANCE_API_KEY=your_api_key +# BINANCE_SECRET=your_secret \ No newline at end of file diff --git a/install_dependencies.sh b/install_dependencies.sh new file mode 100755 index 0000000..1c7ee7f --- /dev/null +++ b/install_dependencies.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +echo "正在安装加密货币选币引擎依赖..." + +# 检查Python版本 +python_version=$(python3 -c "import sys; print('.'.join(map(str, sys.version_info[:2])))") +echo "Python版本: $python_version" + +# 安装基础依赖 +echo "安装基础Python包..." +pip3 install requests pandas numpy python-binance ccxt fastapi uvicorn jinja2 aiofiles schedule + +# 安装TA-Lib +echo "安装TA-Lib..." +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + echo "检测到macOS系统" + if command -v brew &> /dev/null; then + echo "使用Homebrew安装TA-Lib..." + brew install ta-lib + pip3 install TA-Lib + else + echo "请先安装Homebrew,或手动安装TA-Lib" + echo "访问: https://github.com/mrjbq7/ta-lib#installation" + fi +elif [[ "$OSTYPE" == "linux"* ]]; then + # Linux + echo "检测到Linux系统" + # 尝试使用包管理器安装 + if command -v apt-get &> /dev/null; then + sudo apt-get update + sudo apt-get install -y build-essential wget + + # 下载并编译TA-Lib + cd /tmp + wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz + tar -xzf ta-lib-0.4.0-src.tar.gz + cd ta-lib/ + ./configure --prefix=/usr/local + make + sudo make install + cd ~ + + pip3 install TA-Lib + else + echo "请手动安装TA-Lib依赖" + fi +else + # Windows或其他系统 + echo "其他系统,请手动安装TA-Lib" + echo "Windows用户可以下载预编译包:" + echo "https://github.com/cgohlke/talib-builds/releases" +fi + +echo "依赖安装完成!" +echo "运行测试: python3 -c 'import talib; print(\"TA-Lib安装成功\")'" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..246ec34 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,23 @@ +# 核心依赖 +requests>=2.25.0 +pandas>=1.3.0 +numpy>=1.20.0 +python-binance>=1.0.15 +TA-Lib>=0.4.24 +urllib3<2.0.0 + +# Web框架 +flask>=2.3.0 +flask-cors>=4.0.0 +jinja2>=3.0.0 + +# 调度任务 +schedule>=1.1.0 +APScheduler>=3.10.0 + +# 日志和工具 +python-dateutil>=2.8.0 +pytz>=2023.3 + +# 开发工具(可选) +pytest>=7.4.0 \ No newline at end of file diff --git a/scheduler.py b/scheduler.py new file mode 100644 index 0000000..d14b8cd --- /dev/null +++ b/scheduler.py @@ -0,0 +1,166 @@ +import schedule +import time +import logging +from datetime import datetime, timezone, timedelta +from coin_selection_engine import CoinSelectionEngine +import threading + +# 东八区时区 +BEIJING_TZ = timezone(timedelta(hours=8)) + +def get_beijing_time(): + """获取当前东八区时间用于显示""" + return datetime.now(BEIJING_TZ) + +class ScheduledCoinSelector: + def __init__(self, api_key=None, secret=None, db_path="trading.db"): + """初始化定时选币器""" + self.engine = CoinSelectionEngine(api_key, secret, db_path) + self.is_running = False + self.scheduler_thread = None + + # 配置日志 + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('coin_selection.log'), + logging.StreamHandler() + ] + ) + self.logger = logging.getLogger(__name__) + + def scheduled_selection_job(self): + """定时选币任务""" + self.logger.info("开始执行定时选币任务...") + + try: + # 执行选币 + selected_coins = self.engine.run_coin_selection() + + if selected_coins: + self.logger.info(f"定时选币完成,共选出{len(selected_coins)}个币种") + + # 打印选币结果 + print("\n" + "="*60) + print(f"定时选币结果 - {get_beijing_time().strftime('%Y-%m-%d %H:%M:%S CST')}") + print("="*60) + + for i, signal in enumerate(selected_coins, 1): + print(f"{i}. {signal.symbol} - 评分: {signal.score:.1f}") + print(f" 入场: ${signal.entry_price:.4f}") + print(f" 止损: ${signal.stop_loss:.4f}") + print(f" 止盈: ${signal.take_profit:.4f}") + print(f" 理由: {signal.reason}") + print("-" * 40) + + else: + self.logger.warning("本次定时选币未找到符合条件的币种") + + except Exception as e: + self.logger.error(f"定时选币任务执行失败: {e}") + + def start_scheduler(self): + """启动定时器""" + if self.is_running: + self.logger.warning("定时器已经在运行中") + return + + self.logger.info("启动定时选币器...") + + # 设置定时任务 + # 每天8点和20点执行选币 + schedule.every().day.at("08:00").do(self.scheduled_selection_job) + schedule.every().day.at("20:00").do(self.scheduled_selection_job) + + # 也可以设置每隔4小时执行一次 + # schedule.every(4).hours.do(self.scheduled_selection_job) + + self.is_running = True + + # 在单独的线程中运行调度器 + def run_scheduler(): + while self.is_running: + schedule.run_pending() + time.sleep(60) # 每分钟检查一次 + + self.scheduler_thread = threading.Thread(target=run_scheduler, daemon=True) + self.scheduler_thread.start() + + self.logger.info("定时选币器已启动,计划每日8:00和20:00执行") + + def stop_scheduler(self): + """停止定时器""" + if not self.is_running: + self.logger.warning("定时器未在运行") + return + + self.logger.info("停止定时选币器...") + self.is_running = False + schedule.clear() + + if self.scheduler_thread and self.scheduler_thread.is_alive(): + self.scheduler_thread.join(timeout=5) + + self.logger.info("定时选币器已停止") + + def run_once(self): + """立即执行一次选币""" + self.logger.info("立即执行选币...") + self.scheduled_selection_job() + + def get_next_run_time(self): + """获取下次执行时间""" + jobs = schedule.get_jobs() + if jobs: + next_run = min(job.next_run for job in jobs) + # 转换为东八区时间显示 + beijing_time = next_run.replace(tzinfo=timezone.utc).astimezone(BEIJING_TZ) + return beijing_time.strftime('%Y-%m-%d %H:%M:%S CST') + return "无计划任务" + + +def main(): + """主函数""" + import argparse + + parser = argparse.ArgumentParser(description='加密货币定时选币器') + parser.add_argument('--api-key', help='Binance API Key') + parser.add_argument('--secret', help='Binance Secret Key') + parser.add_argument('--db-path', default='trading.db', help='数据库路径') + parser.add_argument('--run-once', action='store_true', help='立即执行一次选币') + parser.add_argument('--daemon', action='store_true', help='以守护进程方式运行') + + args = parser.parse_args() + + # 创建定时选币器 + selector = ScheduledCoinSelector(args.api_key, args.secret, args.db_path) + + if args.run_once: + # 立即执行一次 + selector.run_once() + elif args.daemon: + # 启动定时器并保持运行 + selector.start_scheduler() + + try: + print("定时选币器正在运行...") + print(f"下次执行时间: {selector.get_next_run_time()}") + print("按 Ctrl+C 停止") + + while True: + time.sleep(1) + + except KeyboardInterrupt: + print("\n收到停止信号...") + selector.stop_scheduler() + print("定时选币器已停止") + else: + print("请使用 --run-once 立即执行或 --daemon 启动定时器") + print("示例:") + print(" python scheduler.py --run-once") + print(" python scheduler.py --daemon") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/setup_dingtalk.py b/setup_dingtalk.py new file mode 100644 index 0000000..31d92e1 --- /dev/null +++ b/setup_dingtalk.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +""" +钉钉通知快速设置和测试脚本 +""" + +import os +import sys +from coin_selection_engine import CoinSelectionEngine + +def setup_dingtalk(): + """设置钉钉webhook""" + print("🤖 加密货币选币系统 - 钉钉通知设置") + print("=" * 50) + + # 获取当前配置 + current_webhook = os.getenv('DINGTALK_WEBHOOK_URL') + current_secret = os.getenv('DINGTALK_WEBHOOK_SECRET') + + if current_webhook: + print(f"当前配置的webhook: {current_webhook[:50]}...") + if current_secret: + print("已配置加签密钥(安全模式)") + use_current = input("是否使用当前配置? (y/n): ").lower().strip() + if use_current == 'y': + return current_webhook, current_secret + + # 输入新的webhook + print("\n📱 请按以下步骤获取钉钉机器人webhook URL:") + print("1. 在钉钉群中添加机器人") + print("2. 选择'自定义机器人'") + print("3. 复制webhook URL") + print("4. 如果启用了加签安全,也复制密钥") + print() + + webhook_url = input("请输入钉钉机器人webhook URL: ").strip() + + if not webhook_url: + print("❌ 未输入webhook URL") + return None, None + + if not webhook_url.startswith('https://oapi.dingtalk.com/robot/send'): + print("❌ webhook URL格式不正确") + return None, None + + # 询问是否使用加签 + use_sign = input("是否启用了加签安全设置? (y/n): ").lower().strip() + webhook_secret = None + + if use_sign == 'y': + webhook_secret = input("请输入加签密钥: ").strip() + if webhook_secret and not webhook_secret.startswith('SEC'): + print("⚠️ 加签密钥通常以'SEC'开头,请确认输入正确") + + # 保存到环境变量 (临时) + os.environ['DINGTALK_WEBHOOK_URL'] = webhook_url + if webhook_secret: + os.environ['DINGTALK_WEBHOOK_SECRET'] = webhook_secret + + security_mode = "加签模式" if webhook_secret else "普通模式" + print(f"✅ 钉钉通知配置完成 ({security_mode})") + + return webhook_url, webhook_secret + +def test_notification(): + """测试钉钉通知""" + print("\n🧪 测试钉钉通知...") + + try: + # 创建选币引擎实例 + engine = CoinSelectionEngine() + + # 发送测试消息 + success = engine.test_dingtalk_notification() + + if success: + print("✅ 钉钉通知测试成功!") + return True + else: + print("❌ 钉钉通知测试失败") + return False + + except Exception as e: + print(f"❌ 测试过程中出错: {e}") + return False + +def run_selection_with_notification(): + """运行选币并发送通知""" + print("\n🎯 开始执行选币...") + + try: + engine = CoinSelectionEngine() + selected_coins = engine.run_coin_selection() + + print(f"\n📊 选币完成,共发现 {len(selected_coins)} 个投资机会") + + # 打印简要结果 + if selected_coins: + long_signals = [s for s in selected_coins if s.signal_type == "LONG"] + short_signals = [s for s in selected_coins if s.signal_type == "SHORT"] + print(f"多头信号: {len(long_signals)}个") + print(f"空头信号: {len(short_signals)}个") + + print("\n前5个信号:") + for i, signal in enumerate(selected_coins[:5], 1): + direction = "📈多头" if signal.signal_type == "LONG" else "📉空头" + print(f"{i}. {signal.symbol} {direction} - {signal.score:.1f}分 ({signal.confidence}信心)") + + return selected_coins + + except Exception as e: + print(f"❌ 选币过程中出错: {e}") + return [] + +def create_env_file(webhook_url, webhook_secret=None): + """创建 .env 文件保存配置""" + env_content = f"""# 钉钉机器人配置 +DINGTALK_WEBHOOK_URL={webhook_url} +""" + + if webhook_secret: + env_content += f"DINGTALK_WEBHOOK_SECRET={webhook_secret}\n" + + env_content += """ +# 选币配置 +USE_MARKET_CAP_RANKING=True +""" + + try: + with open('.env', 'w', encoding='utf-8') as f: + f.write(env_content) + print("✅ 配置已保存到 .env 文件") + except Exception as e: + print(f"⚠️ 保存配置文件失败: {e}") + +def main(): + """主函数""" + if len(sys.argv) > 1: + command = sys.argv[1].lower() + + if command == 'test': + test_notification() + return + elif command == 'run': + run_selection_with_notification() + return + elif command == 'setup': + webhook, secret = setup_dingtalk() + if webhook: + create_env_file(webhook, secret) + test_notification() + return + + # 交互式菜单 + while True: + print("\n🤖 加密货币选币系统 - 钉钉通知") + print("=" * 40) + print("1. 设置钉钉webhook") + print("2. 测试钉钉通知") + print("3. 执行选币并发送通知") + print("4. 退出") + print() + + choice = input("请选择操作 (1-4): ").strip() + + if choice == '1': + webhook, secret = setup_dingtalk() + if webhook: + create_env_file(webhook, secret) + + elif choice == '2': + test_notification() + + elif choice == '3': + run_selection_with_notification() + + elif choice == '4': + print("👋 再见!") + break + + else: + print("❌ 无效选择,请重试") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/technical_analyzer.py b/technical_analyzer.py new file mode 100644 index 0000000..17eb88d --- /dev/null +++ b/technical_analyzer.py @@ -0,0 +1,2350 @@ +import pandas as pd +import talib +from typing import Dict, List, Tuple, Optional +import logging +from dataclasses import dataclass + +# 配置日志输出到控制台 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), # 输出到控制台 + logging.FileHandler('technical_analysis.log') # 输出到文件 + ], + force=True # 强制重新配置日志 +) + +@dataclass +class CoinSignal: + symbol: str + score: float + reason: str + entry_price: float + stop_loss: float + take_profit: float + timeframe: str + confidence: str + strategy_type: str # 短线/中线/长线 + holding_period: int # 预期持仓周期(天) + risk_reward_ratio: float # 风险回报比 + expiry_hours: int # 选币信号有效期(小时) + action_suggestion: str # 操作建议 + signal_type: str = "LONG" # 新增:信号类型 LONG/SHORT + direction: str = "BUY" # 新增:操作方向 BUY/SELL + +@dataclass +class StrategyConfig: + name: str + primary_timeframe: str + confirm_timeframe: str + holding_period_days: int + expiry_hours: int + risk_reward_min: float + risk_reward_max: float + rsi_period: int + macd_fast: int + macd_slow: int + macd_signal: int + +class TechnicalAnalyzer: + def __init__(self): + self.min_score = 60 # 提高最低选币分数,注重质量 + self.max_selections = 15 # 适中的选币数量,注重质量而非数量 + + # 策略配置 - 重新设计时间周期 + self.strategies = { + '超短线': StrategyConfig( + name='超短线', + primary_timeframe='5m', + confirm_timeframe='1m', + holding_period_days=1, + expiry_hours=24, + risk_reward_min=1.2, + risk_reward_max=1.8, + rsi_period=5, + macd_fast=3, + macd_slow=8, + macd_signal=2 + ), + '短线': StrategyConfig( + name='短线', + primary_timeframe='15m', + confirm_timeframe='5m', + holding_period_days=3, + expiry_hours=72, + risk_reward_min=1.5, + risk_reward_max=2.5, + rsi_period=7, + macd_fast=5, + macd_slow=13, + macd_signal=3 + ), + '中线': StrategyConfig( + name='中线', + primary_timeframe='1h', + confirm_timeframe='15m', + holding_period_days=7, + expiry_hours=168, # 7天 + risk_reward_min=2.0, + risk_reward_max=3.5, + rsi_period=14, + macd_fast=12, + macd_slow=26, + macd_signal=9 + ), + '波段': StrategyConfig( + name='波段', + primary_timeframe='4h', + confirm_timeframe='1h', + holding_period_days=15, + expiry_hours=360, # 15天 + risk_reward_min=2.5, + risk_reward_max=4.0, + rsi_period=14, + macd_fast=12, + macd_slow=26, + macd_signal=9 + ), + '长线': StrategyConfig( + name='长线', + primary_timeframe='1d', + confirm_timeframe='4h', + holding_period_days=30, + expiry_hours=720, # 30天 + risk_reward_min=3.0, + risk_reward_max=6.0, + rsi_period=21, + macd_fast=26, + macd_slow=50, + macd_signal=12 + ), + '趋势': StrategyConfig( + name='趋势', + primary_timeframe='1w', + confirm_timeframe='1d', + holding_period_days=60, + expiry_hours=1440, # 60天 + risk_reward_min=4.0, + risk_reward_max=8.0, + rsi_period=21, + macd_fast=26, + macd_slow=50, + macd_signal=12 + ) + } + def get_required_timeframes(self) -> Dict[str, List[str]]: + """获取每个策略需要的时间周期""" + return { + '超短线': ['1m', '5m', '15m'], # 需要更短周期确认 + '短线': ['5m', '15m', '1h'], # 短线需要分钟级别 + '中线': ['15m', '1h', '4h'], # 中线需要小时级别 + '波段': ['1h', '4h', '1d'], # 波段需要日内到日线 + '长线': ['4h', '1d', '3d'], # 长线需要日线级别 + '趋势': ['1d', '3d', '1w'] # 趋势需要周线级别 + } + + def get_optimal_timeframes_for_analysis(self, market_conditions: Dict = None) -> List[str]: + """根据市场条件动态选择最优时间周期组合""" + # 基础时间周期组合 - 包含所有策略需要的时间周期 + base_timeframes = ['1m', '5m', '15m', '1h', '4h', '1d', '3d', '1w'] + + # 如果有市场条件信息,可以进一步优化 + if market_conditions: + volatility = market_conditions.get('volatility', 'medium') + trend_strength = market_conditions.get('trend_strength', 'medium') + + if volatility == 'high': + # 高波动市场,增加短周期权重 + return ['1m', '5m', '15m', '1h', '4h', '1d'] + elif volatility == 'low': + # 低波动市场,增加长周期权重 + return ['1h', '4h', '1d', '3d', '1w'] + + return base_timeframes + + def determine_best_strategy(self, df: pd.DataFrame) -> str: + """根据币种特性确定最佳策略 - 增强版""" + try: + if len(df) < 50: + logging.info("数据不足50条,使用默认中线策略") + return '中线' # 默认策略 + + recent_data = df.tail(20) + + # 1. 计算波动率 + volatility = recent_data['close'].std() / recent_data['close'].mean() + + # 2. 计算价格变化速度 + price_change_5d = (df.iloc[-1]['close'] - df.iloc[-5]['close']) / df.iloc[-5]['close'] + + # 3. 成交量特性 + volume_recent = recent_data['volume'].mean() + volume_avg = df['volume'].mean() + volume_ratio = volume_recent / volume_avg if volume_avg > 0 else 1 + + logging.info(f"策略选择指标: 波动率={volatility:.3f}, 5日价格变化={price_change_5d:.3f}, 成交量比率={volume_ratio:.2f}") + + # 添加详细的策略选择调试信息 + logging.info(f"策略选择条件判断:") + logging.info(f" 超短线: volatility({volatility:.3f}) > 0.20? {volatility > 0.20}, abs_change({abs(price_change_5d):.3f}) > 0.10? {abs(price_change_5d) > 0.10}") + logging.info(f" 短线: volatility({volatility:.3f}) > 0.10? {volatility > 0.10}, abs_change({abs(price_change_5d):.3f}) > 0.05? {abs(price_change_5d) > 0.05}") + logging.info(f" 中线: volatility({volatility:.3f}) > 0.06? {volatility > 0.06}, volume_ratio({volume_ratio:.2f}) > 1.1? {volume_ratio > 1.1}") + logging.info(f" 波段: 0.03 < volatility({volatility:.3f}) <= 0.08? {0.03 < volatility <= 0.08}") + + # 策略选择逻辑 - 修复条件,确保所有策略都能被选中 + if volatility > 0.20 and abs(price_change_5d) > 0.10: + # 极高波动,快速变化 → 超短线策略 + logging.info("选择超短线策略: 极高波动且快速变化") + return '超短线' + elif volatility > 0.08 and abs(price_change_5d) > 0.03: + # 高波动,较快变化 → 短线策略(进一步降低门槛) + logging.info("选择短线策略: 高波动且较快变化") + return '短线' + elif volatility > 0.06 and volume_ratio > 1.1: + # 中等波动,有资金关注 → 中线策略 + logging.info("选择中线策略: 中等波动且有资金关注") + return '中线' + elif volatility > 0.03 and volatility <= 0.08: + # 中低波动,适合波段操作 → 波段策略 + logging.info("选择波段策略: 中低波动适合波段操作") + return '波段' + elif volatility <= 0.04 and volume_ratio < 1.0: + # 低波动,适合长期持有 → 长线策略 + logging.info("选择长线策略: 低波动适合长期持有") + return '长线' + elif volatility <= 0.02: + # 极低波动,趋势跟踪 → 趋势策略 + logging.info("选择趋势策略: 极低波动适合趋势跟踪") + return '趋势' + else: + # 根据波动率范围选择策略 + if volatility <= 0.03: + logging.info("选择趋势策略: 基于波动率范围") + return '趋势' + elif volatility <= 0.06: + logging.info("选择长线策略: 基于波动率范围") + return '长线' + elif volatility <= 0.10: + logging.info("选择波段策略: 基于波动率范围") + return '波段' + else: + logging.info("选择中线策略: 默认选择") + return '中线' + + except Exception as e: + logging.warning(f"策略选择失败,使用默认中线策略: {e}") + return '中线' + + def calculate_technical_indicators(self, df: pd.DataFrame, strategy: str = '中线') -> pd.DataFrame: + """计算技术指标""" + if df.empty or len(df) < 50: + return df + + config = self.strategies[strategy] + + # 移动平均线 + df['ma20'] = talib.SMA(df['close'].values, timeperiod=20) + df['ma50'] = talib.SMA(df['close'].values, timeperiod=50) + df['ma200'] = talib.SMA(df['close'].values, timeperiod=200) + + # 根据策略使用不同参数的RSI + df['rsi'] = talib.RSI(df['close'].values, timeperiod=config.rsi_period) + + # 根据策略使用不同参数的MACD + df['macd'], df['macd_signal'], df['macd_hist'] = talib.MACD( + df['close'].values, + fastperiod=config.macd_fast, + slowperiod=config.macd_slow, + signalperiod=config.macd_signal + ) + + # 布林带 + df['bb_upper'], df['bb_middle'], df['bb_lower'] = talib.BBANDS( + df['close'].values, timeperiod=20, nbdevup=2, nbdevdn=2, matype=0 + ) + + # 成交量移动平均 + df['volume_ma'] = talib.SMA(df['volume'].values, timeperiod=20) + + # ATR (平均真实波动范围) + df['atr'] = talib.ATR(df['high'].values, df['low'].values, df['close'].values, timeperiod=14) + + return df + + def calculate_fibonacci_levels(self, df: pd.DataFrame, lookback=50) -> Dict[str, float]: + """计算斐波那契回调位""" + if len(df) < lookback: + return {} + + recent_data = df.tail(lookback) + high = recent_data['high'].max() + low = recent_data['low'].min() + diff = high - low + + return { + 'fib_0': high, + 'fib_236': high - 0.236 * diff, + 'fib_382': high - 0.382 * diff, + 'fib_500': high - 0.500 * diff, + 'fib_618': high - 0.618 * diff, + 'fib_786': high - 0.786 * diff, + 'fib_100': low + } + + def find_support_resistance(self, df: pd.DataFrame, window=20) -> Tuple[float, float]: + """寻找支撑阻力位""" + if len(df) < window * 2: + return 0, 0 + + # 寻找局部高点和低点 + recent_data = df.tail(window) + + # 获取最近的支撑和阻力 + resistance = recent_data['high'].max() + support = recent_data['low'].min() + + return support, resistance + + def simple_long_analysis(self, df: pd.DataFrame) -> float: + """简化的多头分析 - 只看核心指标""" + if len(df) < 10: + return 0 + + score = 0 + current = df.iloc[-1] + recent_data = df.tail(5) + + # 1. 价格趋势 (30分) - 最简单直接 + price_changes = recent_data['close'].pct_change().dropna() + positive_days = (price_changes > 0).sum() + recent_return = (current['close'] - df.iloc[-5]['close']) / df.iloc[-5]['close'] + + if positive_days >= 3: # 5天中3天上涨 + score += 15 + if 0.02 <= recent_return <= 0.15: # 5日涨幅2%-15% + score += 15 + + # 2. 量能确认 (20分) - 简化量价分析 + if 'volume' in df.columns: + recent_volume = recent_data['volume'].mean() + prev_volume = df.iloc[-10:-5]['volume'].mean() if len(df) >= 10 else recent_volume + + if recent_volume > prev_volume * 1.2: # 量能放大 + score += 20 + + # 3. 相对位置 (20分) - 避免高位追高 + if len(df) >= 20: + high_20 = df.tail(20)['high'].max() + low_20 = df.tail(20)['low'].min() + position = (current['close'] - low_20) / (high_20 - low_20) if high_20 != low_20 else 0.5 + + if 0.2 <= position <= 0.7: # 中低位置 + score += 20 + elif position > 0.85: # 过高位置 + score -= 10 + + # 4. 动量指标 (10分) - 只看简单RSI + if 'rsi' in df.columns and not df['rsi'].isna().all(): + rsi = current.get('rsi', 50) + if 40 <= rsi <= 65: # RSI健康区间 + score += 10 + elif rsi > 75: # 过热 + score -= 5 + + return min(max(score, 0), 70) # 限制区间 + + def simple_short_analysis(self, df: pd.DataFrame) -> float: + """简化的空头分析 - 只看核心指标""" + if len(df) < 10: + return 0 + + score = 0 + current = df.iloc[-1] + recent_data = df.tail(5) + + # 1. 价格趋势 (30分) - 直接看下跌 + price_changes = recent_data['close'].pct_change().dropna() + negative_days = (price_changes < 0).sum() + recent_return = (current['close'] - df.iloc[-5]['close']) / df.iloc[-5]['close'] + + if negative_days >= 3: # 5天中3天下跌 + score += 15 + if -0.15 <= recent_return <= -0.02: # 5日跌幅2%-15% + score += 15 + + # 2. 量能确认 (20分) - 价跌量增 + if 'volume' in df.columns: + recent_volume = recent_data['volume'].mean() + prev_volume = df.iloc[-10:-5]['volume'].mean() if len(df) >= 10 else recent_volume + + if recent_volume > prev_volume * 1.2: # 量能放大 + score += 20 + + # 3. 相对位置 (20分) - 高位做空 + if len(df) >= 20: + high_20 = df.tail(20)['high'].max() + low_20 = df.tail(20)['low'].min() + position = (current['close'] - low_20) / (high_20 - low_20) if high_20 != low_20 else 0.5 + + if 0.6 <= position <= 0.95: # 高位区间 + score += 20 + elif position < 0.3: # 过低位置 + score -= 10 + + # 4. 动量指标 (10分) - 只看RSI + if 'rsi' in df.columns and not df['rsi'].isna().all(): + rsi = current.get('rsi', 50) + if 35 <= rsi <= 60: # RSI适中区间 + score += 10 + elif rsi < 25: # 过度超卖 + score -= 5 + + return min(max(score, 0), 70) # 限制区间 + + def _analyze_consecutive_moves(self, df: pd.DataFrame) -> float: + """分析连续上涨动量""" + if len(df) < 10: + return 0 + + score = 0 + recent_closes = df['close'].tail(10).values + + # 计算连续上涨天数 + consecutive_up = 0 + for i in range(1, len(recent_closes)): + if recent_closes[i] > recent_closes[i-1]: + consecutive_up += 1 + else: + break + + # 计算最近3日涨幅 + recent_gain = (recent_closes[-1] - recent_closes[-4]) / recent_closes[-4] if len(recent_closes) >= 4 else 0 + + # 连续上涨但涨幅适中得高分 + if consecutive_up >= 3 and 0.03 <= recent_gain <= 0.12: # 3%-12%涨幅 + score += 8 + elif consecutive_up >= 2 and 0.02 <= recent_gain <= 0.08: + score += 5 + elif consecutive_up >= 4: # 连续上涨但可能过热 + score += 3 + + # 价格加速上涨警告 + if recent_gain > 0.20: # 超过20%涨幅减分 + score -= 5 + + return max(score, 0) + + def _analyze_price_breakthrough(self, df: pd.DataFrame) -> float: + """分析价格突破情况""" + if len(df) < 50: + return 0 + + score = 0 + current_price = df.iloc[-1]['close'] + + # 1. 突破早期高点 (10分) + # 计算过去20-50日的高点 + historical_data = df.iloc[-50:-10] # 排除最近10天 + if len(historical_data) > 0: + resistance_levels = [] + # 寻找局部高点 + for i in range(5, len(historical_data)-5): + local_high = historical_data.iloc[i]['high'] + is_peak = True + for j in range(max(0, i-5), min(len(historical_data), i+6)): + if j != i and historical_data.iloc[j]['high'] > local_high: + is_peak = False + break + if is_peak: + resistance_levels.append(local_high) + + # 检查是否突破这些阻力位 + for resistance in resistance_levels: + if current_price > resistance * 1.01: # 突破且有一定幅度 + # 检查突破的时间和幅度 + breakthrough_ratio = (current_price - resistance) / resistance + if breakthrough_ratio <= 0.05: # 刚刚突破,没有过度追高 + score += 8 + elif breakthrough_ratio <= 0.10: + score += 5 + break + + # 2. 突破近期高点 (5分) + recent_high = df.tail(10)['high'].max() + if current_price > recent_high * 1.005: # 突破近期高点 + score += 5 + + return min(score, 15) + + def _analyze_wave_structure(self, df: pd.DataFrame) -> float: + """分析波动结构 - 寻找低点抬高模式""" + if len(df) < 30: + return 0 + + score = 0 + recent_data = df.tail(30) + + # 寻找近30天的低点 + lows = [] + highs = [] + + for i in range(3, len(recent_data)-3): + current_low = recent_data.iloc[i]['low'] + current_high = recent_data.iloc[i]['high'] + + # 判断是否为局部低点 + is_local_low = True + is_local_high = True + + for j in range(i-3, i+4): + if j != i: + if recent_data.iloc[j]['low'] < current_low: + is_local_low = False + if recent_data.iloc[j]['high'] > current_high: + is_local_high = False + + if is_local_low: + lows.append((i, current_low)) + if is_local_high: + highs.append((i, current_high)) + + # 分析低点抬高趋势 + if len(lows) >= 2: + # 检查最近两个低点是否抬高 + latest_lows = sorted(lows, key=lambda x: x[0])[-2:] + if latest_lows[1][1] > latest_lows[0][1]: # 低点抬高 + score += 5 + + # 分析高点突破趋势 + if len(highs) >= 2: + latest_highs = sorted(highs, key=lambda x: x[0])[-2:] + if latest_highs[1][1] > latest_highs[0][1]: # 高点突破 + score += 5 + + return min(score, 10) + + def analyze_volume_price_relationship(self, df: pd.DataFrame) -> float: + """分析量价关系 - 识别资金流入模式 (最高30分)""" + if len(df) < 20: + return 0 + + score = 0 + + # 1. 价涨量增模式分析 (15分) + price_volume_score = self._analyze_price_volume_trend(df) + score += price_volume_score + + # 2. 量能突破分析 (10分) + volume_breakthrough_score = self._analyze_volume_breakthrough(df) + score += volume_breakthrough_score + + # 3. 资金流入模式 (5分) + accumulation_score = self._analyze_accumulation_pattern(df) + score += accumulation_score + + return min(score, 30) + + def _analyze_price_volume_trend(self, df: pd.DataFrame) -> float: + """分析价涨量增模式""" + if len(df) < 15: + return 0 + + score = 0 + recent_data = df.tail(15) + + # 计算价格和成交量的相关性 + price_changes = recent_data['close'].pct_change().dropna() + volume_changes = recent_data['volume'].pct_change().dropna() + + if len(price_changes) >= 10 and len(volume_changes) >= 10: + # 只分析上涨日的量价关系 + up_days = price_changes > 0 + up_price_changes = price_changes[up_days] + up_volume_changes = volume_changes[up_days] + + if len(up_price_changes) >= 5: + # 计算上涨日的量价相关性 + correlation = up_price_changes.corr(up_volume_changes) + + if correlation > 0.5: # 强正相关 + score += 10 + elif correlation > 0.3: + score += 6 + elif correlation > 0.1: + score += 3 + + # 分析最近5天的量价配合 + recent_5 = df.tail(5) + up_days_recent = (recent_5['close'].diff() > 0).sum() + avg_volume_recent = recent_5['volume'].mean() + avg_volume_before = df.iloc[-20:-5]['volume'].mean() if len(df) >= 20 else avg_volume_recent + + volume_ratio = avg_volume_recent / avg_volume_before if avg_volume_before > 0 else 1 + + # 上涨日多且成交量放大 + if up_days_recent >= 3 and volume_ratio > 1.2: + score += 5 + + return min(score, 15) + + def _analyze_volume_breakthrough(self, df: pd.DataFrame) -> float: + """分析量能突破""" + if len(df) < 30: + return 0 + + score = 0 + current_volume = df.iloc[-1]['volume'] + avg_volume_30 = df.tail(30)['volume'].mean() + avg_volume_10 = df.tail(10)['volume'].mean() + + # 1. 近期量能突破 + volume_ratio_current = current_volume / avg_volume_30 if avg_volume_30 > 0 else 1 + volume_ratio_recent = avg_volume_10 / avg_volume_30 if avg_volume_30 > 0 else 1 + + if volume_ratio_current > 2.0: # 当日爆量 + # 检查是否伴随价格上涨 + price_change = (df.iloc[-1]['close'] - df.iloc[-2]['close']) / df.iloc[-2]['close'] + if price_change > 0.02: # 价涨量增 + score += 6 + elif price_change > 0: # 价涨但幅度不大 + score += 3 + + if 1.3 <= volume_ratio_recent <= 2.5: # 温和持续放量 + score += 4 + + return min(score, 10) + + def _analyze_accumulation_pattern(self, df: pd.DataFrame) -> float: + """分析资金积累模式""" + if len(df) < 20: + return 0 + + score = 0 + recent_data = df.tail(20) + + # 分析OBV(能量潮)趋势 + # 简化版OBV计算 + obv = [] + obv_value = 0 + + for i in range(len(recent_data)): + if i == 0: + obv.append(obv_value) + continue + + price_change = recent_data.iloc[i]['close'] - recent_data.iloc[i-1]['close'] + volume = recent_data.iloc[i]['volume'] + + if price_change > 0: + obv_value += volume + elif price_change < 0: + obv_value -= volume + + obv.append(obv_value) + + # 分析OBV趋势 + if len(obv) >= 10: + obv_recent = obv[-5:] # 最近5天 + obv_before = obv[-10:-5] # 之前5天 + + if sum(obv_recent) > sum(obv_before): # OBV上升 + score += 3 + + # 检查价格横盘但量能放大的情况(积累) + price_volatility = recent_data['close'].std() / recent_data['close'].mean() + volume_avg_recent = recent_data.tail(10)['volume'].mean() + volume_avg_before = recent_data.head(10)['volume'].mean() + + if (price_volatility < 0.08 and # 价格相对稳定 + volume_avg_recent > volume_avg_before * 1.2): # 但量能放大 + score += 2 + + return min(score, 5) + + def analyze_technical_patterns(self, df: pd.DataFrame) -> float: + """分析技术形态 - 综合K线形态和价格结构 (最高25分)""" + if len(df) < 20: + return 0 + + score = 0 + + # 1. K线形态识别 (15分) + candlestick_score = self._analyze_enhanced_candlestick_patterns(df) + score += candlestick_score + + # 2. 价格结构分析 (10分) + structure_score = self._analyze_price_structure(df) + score += structure_score + + return min(score, 25) + + def _analyze_enhanced_candlestick_patterns(self, df: pd.DataFrame) -> float: + """增强型K线形态分析""" + if len(df) < 10: + return 0 + + score = 0 + + # 使用talib的K线形态识别 + patterns = { + 'hammer': talib.CDLHAMMER(df['open'], df['high'], df['low'], df['close']), + 'doji': talib.CDLDOJI(df['open'], df['high'], df['low'], df['close']), + 'engulfing': talib.CDLENGULFING(df['open'], df['high'], df['low'], df['close']), + 'morning_star': talib.CDLMORNINGSTAR(df['open'], df['high'], df['low'], df['close']), + 'three_white_soldiers': talib.CDL3WHITESOLDIERS(df['open'], df['high'], df['low'], df['close']), + 'piercing': talib.CDLPIERCING(df['open'], df['high'], df['low'], df['close']), + 'harami_bullish': talib.CDLHARAMI(df['open'], df['high'], df['low'], df['close']) + } + + # 检查最近3天的K线形态 + recent_signals = 0 + for pattern_name, pattern_data in patterns.items(): + if len(pattern_data) > 3: + # 检查最近3天是否有信号 + for i in range(-3, 0): + if pattern_data.iloc[i] > 0: # 看涨信号 + recent_signals += 1 + if pattern_name in ['morning_star', 'three_white_soldiers', 'piercing']: + score += 4 # 强看涨形态 + elif pattern_name in ['hammer', 'engulfing']: + score += 3 # 中度看涨形态 + else: + score += 2 # 弱看涨形态 + break + + # 手动分析一些简单形态 + manual_score = self._analyze_manual_patterns(df.tail(10)) + score += manual_score + + return min(score, 15) + + def _analyze_manual_patterns(self, df: pd.DataFrame) -> float: + """手动分析一些简单形态""" + if len(df) < 5: + return 0 + + score = 0 + + # 分析最近5天的走势 + last_5 = df.tail(5) + + # 1. 阶段性低点上移模式 + lows = last_5['low'].tolist() + if len(lows) >= 3: + # 检查是否有低点逐步抬高的趋势 + ascending_lows = True + for i in range(1, len(lows)): + if lows[i] < lows[i-1] * 0.98: # 允许小幅波动 + ascending_lows = False + break + if ascending_lows: + score += 3 + + # 2. 突破前高点模式 + current_high = df.iloc[-1]['high'] + prev_highs = df.iloc[-5:-1]['high'].tolist() + if current_high > max(prev_highs) * 1.01: # 突破前高 + score += 2 + + return min(score, 5) + + def _analyze_price_structure(self, df: pd.DataFrame) -> float: + """分析价格结构""" + if len(df) < 30: + return 0 + + score = 0 + + # 1. 分析关键支撑位突破 (6分) + support_score = self._analyze_support_breakthrough(df) + score += support_score + + # 2. 分析价格区间突破 (4分) + range_score = self._analyze_range_breakthrough(df) + score += range_score + + return min(score, 10) + + def _analyze_support_breakthrough(self, df: pd.DataFrame) -> float: + """分析支撑位突破""" + if len(df) < 30: + return 0 + + score = 0 + current_price = df.iloc[-1]['close'] + + # 寻找近30天内的重要支撑位 + recent_30 = df.tail(30) + + # 计算支撑位(多次触及的低点) + support_levels = [] + for i in range(5, len(recent_30)-5): + low_price = recent_30.iloc[i]['low'] + # 检查这个低点是否被多次测试 + test_count = 0 + for j in range(len(recent_30)): + if abs(recent_30.iloc[j]['low'] - low_price) / low_price < 0.02: # 2%范围内 + test_count += 1 + + if test_count >= 2: # 至少被测试2次 + support_levels.append(low_price) + + # 检查是否突破了这些支撑位 + for support in support_levels: + if current_price > support * 1.03: # 突破支撑位3%以上 + score += 3 + break + + return min(score, 6) + + def _analyze_range_breakthrough(self, df: pd.DataFrame) -> float: + """分析区间突破""" + if len(df) < 20: + return 0 + + score = 0 + recent_20 = df.tail(20) + current_price = df.iloc[-1]['close'] + + # 计算近20天的价格区间 + price_high = recent_20['high'].max() + price_low = recent_20['low'].min() + price_range = price_high - price_low + + # 检查是否在盘整后突破 + if price_range > 0: + # 检查是否经历了盘整阶段 + middle_period = recent_20.iloc[5:15] # 中间一段 + consolidation_range = middle_period['high'].max() - middle_period['low'].min() + + # 如果中间阶段波动较小,说明有盘整 + if consolidation_range < price_range * 0.6: + # 检查是否突破了区间高点 + if current_price > price_high * 0.995: # 接近或突破高点 + score += 4 + + return min(score, 4) + + def analyze_price_behavior(self, df: pd.DataFrame) -> float: + """分析价格行为 - 核心指标 (最高35分)""" + if len(df) < 30: + return 0 + + score = 0 + recent_data = df.tail(20) + + # 1. 连续上涨动量分析 (12分) + consecutive_score = self._analyze_consecutive_moves(df) + score += consecutive_score + + # 2. 价格突破分析 (15分) + breakthrough_score = self._analyze_price_breakthrough(df) + score += breakthrough_score + + # 3. 波动结构分析 (8分) + wave_score = self._analyze_wave_structure(df) + score += wave_score + + return min(score, 35) + + def analyze_breakout_potential(self, df: pd.DataFrame) -> float: + """分析突破潜力 - 辅助指标 (最高20分)""" + if len(df) < 30: + return 0 + + score = 0 + current = df.iloc[-1] + recent_data = df.tail(20) + + # 1. 价格位置分析 - 寻找埋伏位置 (8分) + recent_high = recent_data['high'].max() + recent_low = recent_data['low'].min() + price_range = recent_high - recent_low + current_position = (current['close'] - recent_low) / price_range if price_range > 0 else 0 + + # 偏好在区间中下部的币种 + if 0.2 <= current_position <= 0.5: # 理想埋伏位置 + score += 6 + elif 0.5 < current_position <= 0.7: # 次优位置 + score += 3 + elif current_position > 0.8: # 已经在高位,减分 + score -= 5 + + # 2. 整理形态分析 (6分) + volatility = recent_data['close'].std() / recent_data['close'].mean() + if 0.05 <= volatility <= 0.12: # 适度整理,蓄势待发 + score += 5 + elif volatility < 0.03: # 过度整理,缺乏动力 + score += 1 + elif volatility > 0.20: # 波动过大,风险较高 + score -= 3 + + # 3. 量能配合 (6分) + volume_ratio = recent_data['volume'] / recent_data['volume'].rolling(10).mean() + moderate_volume_days = ((volume_ratio >= 1.1) & (volume_ratio <= 1.8)).sum() + explosive_volume_days = (volume_ratio > 2.5).sum() + + if moderate_volume_days >= 3: # 持续温和放量 + score += 4 + if explosive_volume_days <= 1: # 没有过度爆量 + score += 2 + + return max(min(score, 20), 0) + + def analyze_short_signals(self, df: pd.DataFrame, strategy: str = '中线') -> Optional[CoinSignal]: + """分析做空信号 - 识别下跌趋势币种""" + if df.empty or len(df) < 50: + return None + + try: + df = self.calculate_technical_indicators(df, strategy) + config = self.strategies[strategy] + + total_score = 0 + reasons = [] + + # === 做空信号评分体系 === + + # 1. 空头价格行为分析 (35分) + bear_price_score = self.analyze_bearish_price_behavior(df) + if bear_price_score > 0: + total_score += bear_price_score + if bear_price_score >= 25: + reasons.append(f"强势下跌信号({bear_price_score}分)") + elif bear_price_score >= 15: + reasons.append(f"下跌信号({bear_price_score}分)") + + # 2. 空头量价关系 (30分) + bear_volume_score = self.analyze_bearish_volume_price(df) + if bear_volume_score > 0: + total_score += bear_volume_score + if bear_volume_score >= 20: + reasons.append(f"理想空头量价({bear_volume_score}分)") + elif bear_volume_score >= 10: + reasons.append(f"空头量价({bear_volume_score}分)") + + # 3. 空头技术形态 (25分) + bear_pattern_score = self.analyze_bearish_patterns(df) + if bear_pattern_score > 0: + total_score += bear_pattern_score + if bear_pattern_score >= 15: + reasons.append(f"强空头形态({bear_pattern_score}分)") + elif bear_pattern_score >= 8: + reasons.append(f"空头形态({bear_pattern_score}分)") + + # 4. 下跌突破潜力 (20分) + breakdown_score = self.analyze_breakdown_potential(df) + if breakdown_score > 0: + total_score += breakdown_score + if breakdown_score >= 15: + reasons.append(f"高下跌潜力({breakdown_score}分)") + elif breakdown_score >= 8: + reasons.append(f"下跌潜力({breakdown_score}分)") + + # 5. 均线空头系统 (10分) + bear_ma_score = self.analyze_bearish_moving_average(df) + if bear_ma_score > 0: + total_score += bear_ma_score + if bear_ma_score >= 6: + reasons.append(f"均线空头({int(bear_ma_score)}分)") + + # 过滤低分币种 + if total_score < 65: # 做空也需要足够的信号强度 + return None + + # 确定信心等级 + if total_score >= 80: + confidence = "高" + elif total_score >= 65: + confidence = "中" + else: + confidence = "低" + + # 计算空头入场和出场位 + entry_price, stop_loss, take_profit, risk_reward = self.calculate_short_entry_exit_levels(df, strategy) + + # 生成空头操作建议 + current_price = df.iloc[-1]['close'] + action_suggestion = self.generate_short_action_suggestion(df, entry_price, current_price, total_score) + + return CoinSignal( + symbol=None, # 将在调用时设置 + score=total_score, + reason=" | ".join(reasons), + entry_price=entry_price, + stop_loss=stop_loss, + take_profit=take_profit, + timeframe=config.primary_timeframe, + confidence=confidence, + strategy_type=strategy, + holding_period=config.holding_period_days, + risk_reward_ratio=risk_reward, + expiry_hours=config.expiry_hours, + action_suggestion=action_suggestion, + signal_type="SHORT", + direction="SELL" + ) + + except Exception as e: + logging.error(f"分析空头信号失败: {e}") + return None + + def analyze_bearish_price_behavior(self, df: pd.DataFrame) -> float: + """分析空头价格行为 - 核心指标 (35分)""" + if len(df) < 30: + return 0 + + score = 0 + recent_data = df.tail(20) + + # 1. 连续下跌动量 (10分) + consecutive_down = 0 + recent_closes = recent_data['close'].tail(10).values + for i in range(1, len(recent_closes)): + if recent_closes[i] < recent_closes[i-1]: + consecutive_down += 1 + else: + break + + recent_decline = (recent_closes[-1] - recent_closes[-4]) / recent_closes[-4] if len(recent_closes) >= 4 else 0 + + if consecutive_down >= 3 and -0.12 <= recent_decline <= -0.03: # 3%-12%下跌 + score += 8 + elif consecutive_down >= 2 and -0.08 <= recent_decline <= -0.02: + score += 5 + + # 2. 价格跌破分析 (15分) + current_price = df.iloc[-1]['close'] + + # 跌破支撑位 + historical_data = df.iloc[-50:-10] + if len(historical_data) > 0: + support_levels = [] + for i in range(5, len(historical_data)-5): + local_low = historical_data.iloc[i]['low'] + is_valley = True + for j in range(max(0, i-5), min(len(historical_data), i+6)): + if j != i and historical_data.iloc[j]['low'] < local_low: + is_valley = False + break + if is_valley: + support_levels.append(local_low) + + for support in support_levels: + if current_price < support * 0.97: # 跌破支撑位3%以上 + breakdown_ratio = (support - current_price) / support + if breakdown_ratio <= 0.05: # 刚刚跌破 + score += 10 + elif breakdown_ratio <= 0.10: + score += 6 + break + + # 3. 高点逐步下移模式 (10分) + recent_highs = [] + for i in range(3, len(recent_data)-3): + current_high = recent_data.iloc[i]['high'] + is_local_high = True + for j in range(i-3, i+4): + if j != i and recent_data.iloc[j]['high'] > current_high: + is_local_high = False + break + if is_local_high: + recent_highs.append((i, current_high)) + + if len(recent_highs) >= 2: + latest_highs = sorted(recent_highs, key=lambda x: x[0])[-2:] + if latest_highs[1][1] < latest_highs[0][1]: # 高点下移 + score += 8 + + return min(score, 35) + + def _comprehensive_long_analysis(self, main_df: pd.DataFrame, timeframe_data: Dict, strategy: str) -> float: + """综合多头分析 - 优化策略本身而非降低分数""" + if len(main_df) < 30: + return 0 + + total_score = 0 + score_details = {} # 用于记录各项得分 + + # 1. 价格行为分析 (35分) + price_score = self.analyze_price_behavior(main_df) + total_score += price_score + score_details['价格行为'] = price_score + + # 2. 量价关系 (30分) + volume_score = self.analyze_volume_price_relationship(main_df) + total_score += volume_score + score_details['量价关系'] = volume_score + + # 3. 技术形态 (25分) + pattern_score = self.analyze_technical_patterns(main_df) + total_score += pattern_score + score_details['技术形态'] = pattern_score + + # 4. 突破潜力 (20分) + breakout_score = self.analyze_breakout_potential(main_df) + total_score += breakout_score + score_details['突破潜力'] = breakout_score + + # 5. 多时间框架确认 (15分) - 新增 + multi_tf_score = self._analyze_multi_timeframe_confirmation(timeframe_data, strategy, 'LONG') + total_score += multi_tf_score + score_details['多时间框架确认'] = multi_tf_score + + # 6. 风险评估调整 (-30到+10分) - 新增 + risk_adjustment = self._analyze_risk_factors(main_df, 'LONG') + total_score += risk_adjustment + score_details['风险评估调整'] = risk_adjustment + + # 记录详细评分 - 改为100分制 + final_score = min(total_score, 100) + logging.info(f"多头分析详细评分: 价格行为({price_score:.1f}) + 量价关系({volume_score:.1f}) + " + f"技术形态({pattern_score:.1f}) + 突破潜力({breakout_score:.1f}) + " + f"多时间框架({multi_tf_score:.1f}) + 风险调整({risk_adjustment:.1f}) = {final_score:.1f}") + + return final_score + + def _comprehensive_short_analysis(self, main_df: pd.DataFrame, timeframe_data: Dict, strategy: str) -> float: + """综合空头分析 - 优化策略本身而非降低分数""" + if len(main_df) < 30: + return 0 + + total_score = 0 + score_details = {} # 用于记录各项得分 + + # 1. 空头价格行为 (35分) + bear_price_score = self.analyze_bearish_price_behavior(main_df) + total_score += bear_price_score + score_details['空头价格行为'] = bear_price_score + + # 2. 空头量价关系 (30分) + bear_volume_score = self.analyze_bearish_volume_price(main_df) + total_score += bear_volume_score + score_details['空头量价关系'] = bear_volume_score + + # 3. 空头技术形态 (25分) + bear_pattern_score = self.analyze_bearish_patterns(main_df) + total_score += bear_pattern_score + score_details['空头技术形态'] = bear_pattern_score + + # 4. 下跌突破潜力 (20分) + breakdown_score = self.analyze_breakdown_potential(main_df) + total_score += breakdown_score + score_details['下跌突破潜力'] = breakdown_score + + # 5. 多时间框架确认 (15分) - 新增 + multi_tf_score = self._analyze_multi_timeframe_confirmation(timeframe_data, strategy, 'SHORT') + total_score += multi_tf_score + score_details['多时间框架确认'] = multi_tf_score + + # 6. 风险评估调整 (-30到+10分) - 新增 + risk_adjustment = self._analyze_risk_factors(main_df, 'SHORT') + total_score += risk_adjustment + score_details['风险评估调整'] = risk_adjustment + + # 记录详细评分 - 改为100分制 + final_score = min(total_score, 100) + logging.info(f"空头分析详细评分: 空头价格行为({bear_price_score:.1f}) + 空头量价关系({bear_volume_score:.1f}) + " + f"空头技术形态({bear_pattern_score:.1f}) + 下跌突破潜力({breakdown_score:.1f}) + " + f"多时间框架({multi_tf_score:.1f}) + 风险调整({risk_adjustment:.1f}) = {final_score:.1f}") + + return final_score + + def _analyze_multi_timeframe_confirmation(self, timeframe_data: Dict, strategy: str, signal_type: str) -> float: + """多时间框架确认分析""" + score = 0 + config = self.strategies[strategy] + + # 获取确认时间框架数据 + confirm_df = timeframe_data.get(config.confirm_timeframe) + if confirm_df is None or len(confirm_df) < 20: + return 0 + + confirm_df = self.calculate_technical_indicators(confirm_df, strategy) + + if signal_type == 'LONG': + # 确认时间框架的多头信号 + confirm_price_score = self.simple_long_analysis(confirm_df) + if confirm_price_score >= 30: + score += 15 + elif confirm_price_score >= 20: + score += 10 + elif confirm_price_score >= 15: + score += 5 + else: # SHORT + # 确认时间框架的空头信号 + confirm_price_score = self.simple_short_analysis(confirm_df) + if confirm_price_score >= 40: + score += 15 + elif confirm_price_score >= 30: + score += 10 + elif confirm_price_score >= 20: + score += 5 + + return score + + def _analyze_risk_factors(self, df: pd.DataFrame, signal_type: str) -> float: + """风险因素分析""" + if len(df) < 20: + return 0 + + score = 0 + current = df.iloc[-1] + recent_data = df.tail(20) + + # 1. RSI过热/过冷检查 + if hasattr(current, 'rsi'): + rsi = current['rsi'] + if signal_type == 'LONG': + if rsi > 80: # 严重超买 + score -= 15 + elif rsi > 70: # 超买 + score -= 8 + elif 40 <= rsi <= 65: # 健康区间 + score += 5 + else: # SHORT + if rsi < 20: # 严重超卖 + score -= 15 + elif rsi < 30: # 超卖 + score -= 8 + elif 35 <= rsi <= 60: # 健康区间 + score += 5 + + # 2. 价格位置风险 + recent_high = recent_data['high'].max() + recent_low = recent_data['low'].min() + price_range = recent_high - recent_low + current_position = (current['close'] - recent_low) / price_range if price_range > 0 else 0.5 + + if signal_type == 'LONG': + if current_position > 0.9: # 极高位置 + score -= 20 + elif current_position > 0.8: # 高位置 + score -= 10 + elif 0.2 <= current_position <= 0.6: # 理想位置 + score += 5 + else: # SHORT + if current_position < 0.1: # 极低位置 + score -= 20 + elif current_position < 0.2: # 低位置 + score -= 10 + elif 0.4 <= current_position <= 0.8: # 理想位置 + score += 5 + + # 3. 成交量异常检查 + if hasattr(current, 'volume') and hasattr(current, 'volume_ma'): + volume_ratio = current['volume'] / current['volume_ma'] if current['volume_ma'] > 0 else 1 + if volume_ratio > 5: # 异常爆量 + score -= 10 + elif volume_ratio < 0.3: # 成交量萎缩 + score -= 5 + + return max(min(score, 10), -30) + + def analyze_volume_accumulation(self, df: pd.DataFrame) -> float: + """分析成交量积累模式""" + if len(df) < 20: + return 0 + + score = 0 + recent_data = df.tail(10) + + # 寻找温和放量而非暴涨放量 + volume_ratio = recent_data['volume'] / recent_data['volume_ma'] + + # 温和持续放量(1.2-2倍)比突然暴涨放量更好 + moderate_volume_days = ((volume_ratio >= 1.2) & (volume_ratio <= 2.0)).sum() + explosive_volume_days = (volume_ratio > 3.0).sum() + + if moderate_volume_days >= 3: # 持续温和放量 + score += 15 + if explosive_volume_days >= 2: # 已经暴涨放量,减分 + score -= 10 + + return min(score, 20) + + def check_early_trend_signals(self, df: pd.DataFrame) -> float: + """检查早期趋势信号""" + if len(df) < 50: + return 0 + + score = 0 + current = df.iloc[-1] + prev = df.iloc[-2] + + # 1. 均线金叉但涨幅不大 + if (prev['ma20'] <= prev['ma50'] and current['ma20'] > current['ma50']): + # 刚刚金叉,还未大涨 + recent_gain = (current['close'] - df.iloc[-5]['close']) / df.iloc[-5]['close'] + if recent_gain < 0.10: # 5日涨幅小于10% + score += 20 + elif recent_gain > 0.25: # 已经涨幅过大 + score -= 15 + + # 2. RSI从超卖区域回升但未超买 + if 35 <= current['rsi'] <= 55: # 从底部回升但未过热 + score += 10 + elif current['rsi'] > 70: # 已经超买 + score -= 20 + + # 3. MACD即将金叉或刚刚金叉 + if current['macd'] > current['macd_signal']: + # 检查金叉时间 + macd_cross_days = 0 + for i in range(1, min(6, len(df))): + if df.iloc[-i]['macd'] <= df.iloc[-i]['macd_signal']: + macd_cross_days = i - 1 + break + + if macd_cross_days <= 2: # 刚刚金叉2天内 + score += 15 + elif macd_cross_days >= 10: # 金叉很久了 + score -= 10 + + return min(score, 25) + + def analyze_candlestick_patterns(self, df: pd.DataFrame) -> float: + """分析K线形态""" + if len(df) < 20: + return 0 + + score = 0 + + # 使用talib的K线形态识别 + patterns = { + 'hammer': talib.CDLHAMMER(df['open'], df['high'], df['low'], df['close']), + 'doji': talib.CDLDOJI(df['open'], df['high'], df['low'], df['close']), + 'engulfing': talib.CDLENGULFING(df['open'], df['high'], df['low'], df['close']), + 'morning_star': talib.CDLMORNINGSTAR(df['open'], df['high'], df['low'], df['close']), + 'three_white_soldiers': talib.CDL3WHITESOLDIERS(df['open'], df['high'], df['low'], df['close']) + } + + # 检查最近的K线形态 + recent_signals = 0 + for pattern_name, pattern_data in patterns.items(): + if len(pattern_data) > 0 and pattern_data.iloc[-1] > 0: + recent_signals += 1 + if pattern_name in ['morning_star', 'three_white_soldiers']: + score += 15 + elif pattern_name in ['hammer', 'engulfing']: + score += 10 + else: + score += 5 + + return min(score, 20) + + def analyze_moving_average_trend(self, df: pd.DataFrame) -> float: + """分析均线系统""" + if len(df) < 50: + return 0 + + score = 0 + current = df.iloc[-1] + + # 均线多头排列 + if current['close'] > current['ma20'] > current['ma50']: + score += 15 + + # 价格突破关键均线 + prev = df.iloc[-2] + if prev['close'] <= prev['ma20'] and current['close'] > current['ma20']: + score += 10 + + # 均线斜率向上 + ma20_slope = (current['ma20'] - df.iloc[-5]['ma20']) / 5 + if ma20_slope > 0: + score += 5 + + return min(score, 25) + + def analyze_momentum_indicators(self, df: pd.DataFrame) -> float: + """分析动量指标""" + if len(df) < 20: + return 0 + + score = 0 + current = df.iloc[-1] + + # RSI分析 + if 30 <= current['rsi'] <= 50: # RSI从超卖区域回升 + score += 10 + elif 50 <= current['rsi'] <= 70: # RSI强势但未超买 + score += 5 + + # MACD分析 + if current['macd'] > current['macd_signal'] and current['macd_hist'] > 0: + score += 10 + + # MACD金叉 + prev = df.iloc[-2] + if prev['macd'] <= prev['macd_signal'] and current['macd'] > current['macd_signal']: + score += 15 + + return min(score, 25) + + def calculate_entry_exit_levels(self, df: pd.DataFrame, fib_levels: Dict, strategy: str) -> Tuple[float, float, float]: + """计算入场、止损、止盈位""" + current_price = df.iloc[-1]['close'] + atr = df.iloc[-1]['atr'] + support, resistance = self.find_support_resistance(df) + config = self.strategies[strategy] + + # 入场价策略 - 不使用当前市价,而是基于技术分析 + ma20 = df.iloc[-1]['ma20'] + ma50 = df.iloc[-1]['ma50'] + + # 策略1: 均线回踩入场 + if current_price > ma20 > ma50: # 多头趋势 + # 等待回踩MA20附近入场,给出5%的缓冲 + entry_price = ma20 * 1.02 # MA20上方2% + else: + # 策略2: 突破确认入场 + # 等待突破近期阻力位 + recent_high = df.tail(10)['high'].max() + if current_price >= recent_high * 0.98: # 接近突破 + entry_price = recent_high * 1.005 # 突破确认价位 + else: + # 策略3: 斐波那契回调入场 + if fib_levels and 'fib_618' in fib_levels: + entry_price = max(fib_levels['fib_618'], current_price * 0.98) + else: + # 策略4: 当前价格适当下方等待 + entry_price = current_price * 0.995 # 当前价下方0.5% + + # 确保入场价不会过低(距离当前价不超过5%) + if entry_price < current_price * 0.95: + entry_price = current_price * 0.98 + + # 止损位: 支撑位下方或ATR的1.5倍 + stop_loss_atr = entry_price - (atr * 1.5) + stop_loss_support = support * 0.98 if support > 0 else entry_price * 0.92 + stop_loss = max(stop_loss_atr, stop_loss_support) + + # 根据策略动态设置风险回报比 + risk = entry_price - stop_loss + target_ratio = (config.risk_reward_min + config.risk_reward_max) / 2 + take_profit_ratio = entry_price + (risk * target_ratio) + + # 如果有阻力位,取较小值 + if resistance > entry_price: + take_profit_resistance = resistance * 0.98 + take_profit = min(take_profit_ratio, take_profit_resistance) + else: + take_profit = take_profit_ratio + + # 计算实际风险回报比 + actual_risk_reward = (take_profit - entry_price) / risk if risk > 0 else target_ratio + + return entry_price, stop_loss, take_profit, actual_risk_reward + + def generate_action_suggestion(self, df: pd.DataFrame, signal: CoinSignal, current_price: float) -> str: + """根据评分和价格位置生成操作建议""" + try: + # 计算价格差异百分比 + entry_diff = (current_price - signal.entry_price) / signal.entry_price * 100 + + # 获取技术指标状态 + current = df.iloc[-1] + recent_data = df.tail(10) + + # 计算价格在最近区间的位置 + recent_high = recent_data['high'].max() + recent_low = recent_data['low'].min() + price_range = recent_high - recent_low + current_position = (current_price - recent_low) / price_range if price_range > 0 else 0.5 + + # 基于评分等级的基础建议 + if signal.score >= 85: # 高分 + if entry_diff <= 1: # 接近建议价位 + if current_position <= 0.4: # 在低位 + return "立即买入" + elif current_position <= 0.6: + return "现价买入" + else: + return "等待回调买入" + elif entry_diff <= 3: # 稍高于建议价位 + if signal.strategy_type == "短线": + return "现价买入" + else: + return "等待回调买入" + else: # 明显高于建议价位 + return "等待大幅回调" + + elif signal.score >= 70: # 中等分数 + if entry_diff <= -2: # 低于建议价位 + return "现价买入" + elif entry_diff <= 1: # 接近建议价位 + if current_position <= 0.5: + return "现价买入" + else: + return "等待回调买入" + elif entry_diff <= 5: # 高于建议价位 + return "等待回调买入" + else: + return "暂时观望" + + else: # 较低分数 (60-70) + if entry_diff <= -3: # 明显低于建议价位 + return "分批买入" + elif entry_diff <= 0: # 低于或等于建议价位 + if current_position <= 0.3: + return "小仓位试探" + else: + return "等待回调买入" + elif entry_diff <= 3: + return "等待回调买入" + else: + return "暂时观望" + + return "等待回调买入" # 默认建议 + + except Exception as e: + logging.error(f"生成操作建议时出错: {e}") + return "谨慎观望" + + def analyze_single_coin(self, symbol: str, timeframe_data: Dict[str, pd.DataFrame], volume_24h_usd: float = 0) -> List[CoinSignal]: + """分析单个币种 - 优化策略""" + try: + logging.info(f"\n=== 开始分析 {symbol} ===") + logging.info(f"24小时交易量: ${volume_24h_usd:,.0f}") + + # 先执行基础分析,即使交易量不够也要显示评分 + basic_analysis_executed = False + + # 24小时交易量过滤 - 但仍然显示评分 + if volume_24h_usd < 10_000_000: # 降低到1000万美金进行调试 + logging.info(f"{symbol} 交易量不足1000万美金,但仍显示评分") + # 继续执行分析以显示评分,但最后返回空列表 + basic_analysis_executed = True + + signals = [] + + # 首先使用4h数据确定最适合的策略 + initial_df = timeframe_data.get('4h') + if initial_df is None or len(initial_df) < 20: + logging.info(f"{symbol} 4h数据不足,跳过分析") + return [] + + # 确定最佳策略 + best_strategy = self.determine_best_strategy(initial_df) + config = self.strategies[best_strategy] + logging.info(f"{symbol} 选择策略: {best_strategy} (主时间周期: {config.primary_timeframe}, 确认时间周期: {config.confirm_timeframe})") + + # 检查可用的时间周期数据 + available_timeframes = list(timeframe_data.keys()) + logging.info(f"{symbol} 可用时间周期: {available_timeframes}") + + # 获取策略对应的主要时间周期数据 + main_df = timeframe_data.get(config.primary_timeframe) + if main_df is None or len(main_df) < 20: + # 尝试使用该策略的确认时间周期 + confirm_df = timeframe_data.get(config.confirm_timeframe) + if confirm_df is not None and len(confirm_df) >= 20: + main_df = confirm_df + logging.info(f"{symbol} 使用确认时间周期{config.confirm_timeframe}替代主时间周期{config.primary_timeframe}") + else: + # 最后才回退到初始数据和中线策略 + main_df = initial_df + config = self.strategies['中线'] + best_strategy = '中线' + logging.info(f"{symbol} 数据不足,回退到中线策略") + + # 使用策略相应的参数计算技术指标 + main_df = self.calculate_technical_indicators(main_df, best_strategy) + + # === 综合多头信号分析 === + logging.info(f"\n--- {symbol} 多头分析 ---") + long_score = self._comprehensive_long_analysis(main_df, timeframe_data, best_strategy) + + # 记录多头分析结果(不管是否通过筛选) + logging.info(f"{symbol} 多头分析结果: {long_score:.1f}/100分 (门槛: 70分)") + + if long_score >= 70: # 70分入选门槛 + logging.info(f"✓ {symbol} 多头信号通过筛选") + + # 计算入场出场位 + entry_price, stop_loss, take_profit, risk_reward = self.calculate_entry_exit_levels( + main_df, {}, best_strategy + ) + + # 确定信心等级 - 基于100分制 + if long_score >= 85: + confidence = "高" + elif long_score >= 75: + confidence = "中" + else: + confidence = "低" + + # 生成操作建议 - 基于综合分析 + current_price = main_df.iloc[-1]['close'] + action_suggestion = "现价买入" if long_score >= 85 else "等待回调买入" + + logging.info(f"{symbol} 多头信号详情: 入场${entry_price:.4f}, 止损${stop_loss:.4f}, 止盈${take_profit:.4f}, 风险回报比1:{risk_reward:.2f}") + + long_signal = CoinSignal( + symbol=symbol, + score=long_score, + reason=f"综合多头分析: 高质量信号({long_score}分)", + entry_price=entry_price, + stop_loss=stop_loss, + take_profit=take_profit, + timeframe=config.primary_timeframe, + confidence=confidence, + strategy_type=best_strategy, + holding_period=config.holding_period_days, + risk_reward_ratio=risk_reward, + expiry_hours=config.expiry_hours, + action_suggestion=action_suggestion, + signal_type="LONG", + direction="BUY" + ) + signals.append(long_signal) + else: + logging.info(f"✗ {symbol} 多头信号未通过筛选 (差距: {70 - long_score:.1f}分)") + + # === 综合空头信号分析 === + logging.info(f"\n--- {symbol} 空头分析 ---") + short_score = self._comprehensive_short_analysis(main_df, timeframe_data, best_strategy) + + # 记录空头分析结果(不管是否通过筛选) + logging.info(f"{symbol} 空头分析结果: {short_score:.1f}/100分 (门槛: 70分)") + + if short_score >= 70: # 与多头门槛保持一致 + logging.info(f"✓ {symbol} 空头信号通过筛选") + + # 计算入场出场位 + entry_price, stop_loss, take_profit, risk_reward = self.calculate_short_entry_exit_levels( + main_df, best_strategy + ) + + # 确定信心等级 - 基于100分制 + if short_score >= 85: + confidence = "高" + elif short_score >= 75: + confidence = "中" + else: + confidence = "低" + + # 生成操作建议 - 基于综合分析 + current_price = main_df.iloc[-1]['close'] + action_suggestion = "现价做空" if short_score >= 85 else "等待反弹做空" + + logging.info(f"{symbol} 空头信号详情: 入场${entry_price:.4f}, 止损${stop_loss:.4f}, 止盈${take_profit:.4f}, 风险回报比1:{risk_reward:.2f}") + + short_signal = CoinSignal( + symbol=symbol, + score=short_score, + reason=f"综合空头分析: 高质量信号({short_score}分)", + entry_price=entry_price, + stop_loss=stop_loss, + take_profit=take_profit, + timeframe=config.primary_timeframe, + confidence=confidence, + strategy_type=best_strategy, + holding_period=config.holding_period_days, + risk_reward_ratio=risk_reward, + expiry_hours=config.expiry_hours, + action_suggestion=action_suggestion, + signal_type="SHORT", + direction="SELL" + ) + signals.append(short_signal) + else: + logging.info(f"✗ {symbol} 空头信号未通过筛选 (差距: {70 - short_score:.1f}分)") + + # 总结该币种分析结果 + if signals: + signal_types = [s.signal_type for s in signals] + logging.info(f"{symbol} 分析完成 - 生成{len(signals)}个信号: {', '.join(signal_types)}") + else: + logging.info(f"{symbol} 分析完成 - 无符合条件的信号 (多头:{long_score:.1f}, 空头:{short_score:.1f})") + + # 如果是因为交易量不足而跳过,返回空列表 + if basic_analysis_executed: + logging.info(f"{symbol} 因交易量不足,不生成实际交易信号") + return [] + + return signals + + except Exception as e: + logging.error(f"分析{symbol}时出错: {e}") + return [] + + def analyze_single_coin_with_strategy(self, symbol: str, timeframe_data: Dict[str, pd.DataFrame], + volume_24h_usd: float, strategy_name: str) -> List[CoinSignal]: + """使用指定策略分析单个币种""" + try: + # 24小时交易量过滤 + if volume_24h_usd < 50_000_000: + return [] + + # 验证策略是否存在 + if strategy_name not in self.strategies: + logging.error(f"未知策略: {strategy_name}") + return [] + + config = self.strategies[strategy_name] + + # 获取策略对应的主要时间周期数据 + main_df = timeframe_data.get(config.primary_timeframe) + if main_df is None or len(main_df) < 20: + return [] + + # 使用策略相应的参数计算技术指标 + main_df = self.calculate_technical_indicators(main_df, strategy_name) + + signals = [] + + # 综合多头信号分析 + long_score = self._comprehensive_long_analysis(main_df, timeframe_data, strategy_name) + if long_score >= 80: + # 生成多头信号 + entry_price, stop_loss, take_profit, risk_reward = self.calculate_entry_exit_levels( + main_df, {}, strategy_name + ) + + confidence = "高" if long_score >= 85 else "中" if long_score >= 75 else "低" + action_suggestion = "现价买入" if long_score >= 85 else "等待回调买入" + + long_signal = CoinSignal( + symbol=symbol, + score=long_score, + reason=f"{strategy_name}策略多头信号({long_score}分)", + entry_price=entry_price, + stop_loss=stop_loss, + take_profit=take_profit, + timeframe=config.primary_timeframe, + confidence=confidence, + strategy_type=strategy_name, + holding_period=config.holding_period_days, + risk_reward_ratio=risk_reward, + expiry_hours=config.expiry_hours, + action_suggestion=action_suggestion, + signal_type="LONG", + direction="BUY" + ) + signals.append(long_signal) + + # 综合空头信号分析 + short_score = self._comprehensive_short_analysis(main_df, timeframe_data, strategy_name) + if short_score >= 90: + # 生成空头信号 + entry_price, stop_loss, take_profit, risk_reward = self.calculate_short_entry_exit_levels( + main_df, strategy_name + ) + + confidence = "高" if short_score >= 85 else "中" if short_score >= 75 else "低" + action_suggestion = "现价做空" if short_score >= 85 else "等待反弹做空" + + short_signal = CoinSignal( + symbol=symbol, + score=short_score, + reason=f"{strategy_name}策略空头信号({short_score}分)", + entry_price=entry_price, + stop_loss=stop_loss, + take_profit=take_profit, + timeframe=config.primary_timeframe, + confidence=confidence, + strategy_type=strategy_name, + holding_period=config.holding_period_days, + risk_reward_ratio=risk_reward, + expiry_hours=config.expiry_hours, + action_suggestion=action_suggestion, + signal_type="SHORT", + direction="SELL" + ) + signals.append(short_signal) + + return signals + + except Exception as e: + logging.error(f"使用{strategy_name}策略分析{symbol}时出错: {e}") + return [] + + def _analyze_long_signal(self, symbol: str, main_df: pd.DataFrame, timeframe_data: Dict, config: StrategyConfig, best_strategy: str) -> Optional[CoinSignal]: + """分析多头信号 - 原有逻辑""" + total_score = 0 + reasons = [] + + # === 核心评分体系:以价格行为和技术形态为主导 === + + # 1. 价格行为分析 - 最重要 (最高35分) + price_behavior_score = self.analyze_price_behavior(main_df) + if price_behavior_score > 0: + total_score += price_behavior_score + if price_behavior_score >= 25: + reasons.append(f"强势价格行为({price_behavior_score}分)") + elif price_behavior_score >= 15: + reasons.append(f"积极价格行为({price_behavior_score}分)") + + # 2. 量价关系分析 - 核心指标 (最高30分) + volume_price_score = self.analyze_volume_price_relationship(main_df) + if volume_price_score > 0: + total_score += volume_price_score + if volume_price_score >= 20: + reasons.append(f"理想量价配合({volume_price_score}分)") + elif volume_price_score >= 10: + reasons.append(f"量价配合({volume_price_score}分)") + + # 3. 技术形态确认 - 重要指标 (最高25分) + pattern_score = self.analyze_technical_patterns(main_df) + if pattern_score > 0: + total_score += pattern_score + if pattern_score >= 15: + reasons.append(f"强势技术形态({pattern_score}分)") + elif pattern_score >= 8: + reasons.append(f"技术形态({pattern_score}分)") + + # 4. 突破潜力分析 - 寻找爆发机会 (最高20分) + breakout_score = self.analyze_breakout_potential(main_df) + if breakout_score > 0: + total_score += breakout_score + if breakout_score >= 15: + reasons.append(f"高突破潜力({breakout_score}分)") + elif breakout_score >= 8: + reasons.append(f"突破潜力({breakout_score}分)") + + # 5. 均线系统 - 辅助确认 (最高10分,权重降低) + ma_score = self.analyze_moving_average_trend(main_df) + if ma_score > 0: + ma_score = min(ma_score * 0.4, 10) # 大幅降低权重 + total_score += ma_score + if ma_score >= 6: + reasons.append(f"均线支撑({int(ma_score)}分)") + + # 6. 确认时间周期验证 - 多周期确认 (最高5分) + confirm_df = timeframe_data.get(config.confirm_timeframe) + if confirm_df is not None and len(confirm_df) > 20: + confirm_df = self.calculate_technical_indicators(confirm_df, best_strategy) + confirm_score = self.analyze_price_behavior(confirm_df) * 0.2 # 降低权重 + if confirm_score > 3: + total_score += min(confirm_score, 5) + reasons.append(f"{config.confirm_timeframe}确认") + + # 过滤低分币种 - 适度放宽条件 + if price_behavior_score < 5 and volume_price_score < 5: # 降低要求 + return None + + # 适度放宽的分数过滤 + min_total_score = 50 # 降低最低门槛 + if total_score < min_total_score: + return None + + # 计算新波那契回调位 + fib_levels = self.calculate_fibonacci_levels(main_df) + + # 计算入场和出场位 - 使用对应策略 + entry_price, stop_loss, take_profit, risk_reward = self.calculate_entry_exit_levels( + main_df, fib_levels, best_strategy + ) + + # 确定信心等级 - 适度放宽标准 + if total_score >= 75: # 降低高信心阈值 + confidence = "高" + elif total_score >= 55: # 降低中等信心阈值 + confidence = "中" + else: + confidence = "低" + + # 生成操作建议 + current_price = main_df.iloc[-1]['close'] + temp_signal = CoinSignal( + symbol=symbol, + score=total_score, + reason=" | ".join(reasons), + entry_price=entry_price, + stop_loss=stop_loss, + take_profit=take_profit, + timeframe=config.primary_timeframe, + confidence=confidence, + strategy_type=best_strategy, + holding_period=config.holding_period_days, + risk_reward_ratio=risk_reward, + expiry_hours=config.expiry_hours, + action_suggestion="", # 临时占位 + signal_type="LONG", + direction="BUY" + ) + + # 生成操作建议 + action_suggestion = self.generate_action_suggestion(main_df, temp_signal, current_price) + + return CoinSignal( + symbol=symbol, + score=total_score, + reason=" | ".join(reasons), + entry_price=entry_price, + stop_loss=stop_loss, + take_profit=take_profit, + timeframe=config.primary_timeframe, + confidence=confidence, + strategy_type=best_strategy, + holding_period=config.holding_period_days, + risk_reward_ratio=risk_reward, + expiry_hours=config.expiry_hours, + action_suggestion=action_suggestion, + signal_type="LONG", + direction="BUY" + ) + + def select_coins(self, market_data: Dict[str, Dict]) -> Dict[str, List[CoinSignal]]: + """批量选币 - 使用优化策略""" + logging.info(f"\n{'='*60}") + logging.info(f"开始批量选币分析,共{len(market_data)}个币种") + logging.info(f"{'='*60}") + + long_signals = [] + short_signals = [] + filtered_count = 0 + low_volume_count = 0 + strategy_stats = {} # 统计各策略使用次数 + all_scores = [] # 记录所有币种的评分 + + for i, (symbol, data) in enumerate(market_data.items(), 1): + logging.info(f"\n进度: {i}/{len(market_data)} - 分析{symbol}") + + timeframe_data = data.get('timeframes', {}) + volume_24h_usd = data.get('volume_24h_usd', 0) + + # 24小时交易量过滤统计 + if volume_24h_usd < 10_000_000: # 降低到1000万进行调试 + low_volume_count += 1 + logging.info(f"{symbol} 交易量过滤: ${volume_24h_usd:,.0f} < $10,000,000") + continue + + signals = self.analyze_single_coin(symbol, timeframe_data, volume_24h_usd) + + # 获取该币种的评分信息(用于统计) + try: + # 重新计算评分用于统计(避免重复计算,这里简化处理) + initial_df = timeframe_data.get('4h') + if initial_df is not None and len(initial_df) >= 20: + best_strategy = self.determine_best_strategy(initial_df) + config = self.strategies[best_strategy] + main_df = timeframe_data.get(config.primary_timeframe, initial_df) + if len(main_df) >= 20: + main_df = self.calculate_technical_indicators(main_df, best_strategy) + temp_long_score = self._comprehensive_long_analysis(main_df, timeframe_data, best_strategy) + temp_short_score = self._comprehensive_short_analysis(main_df, timeframe_data, best_strategy) + + all_scores.append({ + 'symbol': symbol, + 'strategy': best_strategy, + 'long_score': temp_long_score, + 'short_score': temp_short_score, + 'volume_24h': volume_24h_usd + }) + except Exception as e: + logging.debug(f"获取{symbol}评分统计信息失败: {e}") + + for signal in signals: + if signal: + # 统计策略使用情况 + strategy_key = signal.strategy_type + if strategy_key not in strategy_stats: + strategy_stats[strategy_key] = {'long': 0, 'short': 0, 'total': 0} + + # 使用统一的70分门槛 + if signal.score >= 70: # 统一70分入选门槛 + if signal.signal_type == "LONG": + long_signals.append(signal) + strategy_stats[strategy_key]['long'] += 1 + strategy_stats[strategy_key]['total'] += 1 + logging.info(f"✓ {symbol} 多头信号入选: {signal.score:.1f}分 ({signal.strategy_type})") + elif signal.signal_type == "SHORT": + short_signals.append(signal) + strategy_stats[strategy_key]['short'] += 1 + strategy_stats[strategy_key]['total'] += 1 + logging.info(f"✓ {symbol} 空头信号入选: {signal.score:.1f}分 ({signal.strategy_type})") + else: + filtered_count += 1 + logging.info(f"✗ {symbol} {signal.signal_type}信号过滤: {signal.score:.1f}分 < 80分") + + # 按分数排序 + long_signals.sort(key=lambda x: x.score, reverse=True) + short_signals.sort(key=lambda x: x.score, reverse=True) + + # 选择最优信号 - 统一70分门槛 + def filter_quality_signals(signals): + quality_signals = [] + for signal in signals: + # 只选择符合70分门槛的信号 + if signal.score >= 80: # 优质信号优先 + quality_signals.append(signal) + elif len(quality_signals) < self.max_selections and signal.score >= 70: # 补充达标信号 + quality_signals.append(signal) + return quality_signals[:self.max_selections] + + selected_long_signals = filter_quality_signals(long_signals) + selected_short_signals = filter_quality_signals(short_signals) + + # 详细统计信息 + logging.info(f"\n{'='*60}") + logging.info("批量选币分析完成 - 详细统计") + logging.info(f"{'='*60}") + logging.info(f"总币种数量: {len(market_data)}") + logging.info(f"交易量过滤: {low_volume_count}个") + logging.info(f"分数过滤: {filtered_count}个") + logging.info(f"原始信号: 多头{len(long_signals)}个, 空头{len(short_signals)}个") + logging.info(f"最终选择: 多头{len(selected_long_signals)}个, 空头{len(selected_short_signals)}个") + + # 策略分布统计 + if strategy_stats: + logging.info(f"\n策略使用分布:") + for strategy, stats in sorted(strategy_stats.items(), key=lambda x: x[1]['total'], reverse=True): + logging.info(f" {strategy}: {stats['total']}个信号 (多头{stats['long']}, 空头{stats['short']})") + + # 评分分布统计 + if all_scores: + logging.info(f"\n全部币种评分分布:") + long_scores = [item['long_score'] for item in all_scores] + short_scores = [item['short_score'] for item in all_scores] + + # 多头评分统计 + long_high = len([s for s in long_scores if s >= 80]) + long_medium = len([s for s in long_scores if 60 <= s < 80]) + long_low = len([s for s in long_scores if 40 <= s < 60]) + long_very_low = len([s for s in long_scores if s < 40]) + + logging.info(f" 多头评分分布: ≥80分({long_high}个), 60-79分({long_medium}个), 40-59分({long_low}个), <40分({long_very_low}个)") + + # 空头评分统计 + short_high = len([s for s in short_scores if s >= 90]) + short_medium = len([s for s in short_scores if 70 <= s < 90]) + short_low = len([s for s in short_scores if 50 <= s < 70]) + short_very_low = len([s for s in short_scores if s < 50]) + + logging.info(f" 空头评分分布: ≥90分({short_high}个), 70-89分({short_medium}个), 50-69分({short_low}个), <50分({short_very_low}个)") + + # 显示前10名多头和空头候选 + top_long_candidates = sorted(all_scores, key=lambda x: x['long_score'], reverse=True)[:10] + top_short_candidates = sorted(all_scores, key=lambda x: x['short_score'], reverse=True)[:10] + + logging.info(f"\n前10名多头候选:") + for i, item in enumerate(top_long_candidates, 1): + status = "✓入选" if any(s.symbol == item['symbol'] and s.signal_type == "LONG" for s in selected_long_signals) else "未选中" + logging.info(f" {i:2d}. {item['symbol']:12s}: {item['long_score']:5.1f}分 ({item['strategy']}) - {status}") + + logging.info(f"\n前10名空头候选:") + for i, item in enumerate(top_short_candidates, 1): + status = "✓入选" if any(s.symbol == item['symbol'] and s.signal_type == "SHORT" for s in selected_short_signals) else "未选中" + logging.info(f" {i:2d}. {item['symbol']:12s}: {item['short_score']:5.1f}分 ({item['strategy']}) - {status}") + + # 最终信号详情 + if selected_long_signals: + logging.info(f"\n最终多头信号详情:") + for i, signal in enumerate(selected_long_signals, 1): + logging.info(f" {i}. {signal.symbol}: {signal.score:.1f}分 ({signal.strategy_type}-{signal.confidence})") + + if selected_short_signals: + logging.info(f"\n最终空头信号详情:") + for i, signal in enumerate(selected_short_signals, 1): + logging.info(f" {i}. {signal.symbol}: {signal.score:.1f}分 ({signal.strategy_type}-{signal.confidence})") + + return { + 'long': selected_long_signals, + 'short': selected_short_signals, + 'all': selected_long_signals + selected_short_signals + } + + # === 空头分析方法 === + + def analyze_bearish_volume_price(self, df: pd.DataFrame) -> float: + """分析空头量价关系 (30分)""" + if len(df) < 20: + return 0 + + score = 0 + recent_data = df.tail(15) + + # 计算价格和成交量的相关性 + price_changes = recent_data['close'].pct_change().dropna() + volume_changes = recent_data['volume'].pct_change().dropna() + + if len(price_changes) >= 10 and len(volume_changes) >= 10: + # 分析下跌日的量价关系 + down_days = price_changes < 0 + down_price_changes = price_changes[down_days] + down_volume_changes = volume_changes[down_days] + + if len(down_price_changes) >= 5: + # 计算下跌日的量价相关性 + correlation = down_price_changes.corr(down_volume_changes) + + if correlation < -0.5: # 强负相关(价跌量增) + score += 15 + elif correlation < -0.3: + score += 10 + elif correlation < -0.1: + score += 5 + + # 分析最近5天的量价配合 + recent_5 = df.tail(5) + down_days_recent = (recent_5['close'].diff() < 0).sum() + avg_volume_recent = recent_5['volume'].mean() + avg_volume_before = df.iloc[-20:-5]['volume'].mean() if len(df) >= 20 else avg_volume_recent + + volume_ratio = avg_volume_recent / avg_volume_before if avg_volume_before > 0 else 1 + + # 下跌日多且成交量放大 + if down_days_recent >= 3 and volume_ratio > 1.2: + score += 15 + + return min(score, 30) + + def analyze_bearish_patterns(self, df: pd.DataFrame) -> float: + """分析空头技术形态 (25分)""" + if len(df) < 20: + return 0 + + score = 0 + + # 使用talib的K线形态识别(空头形态) + patterns = { + 'hanging_man': talib.CDLHANGINGMAN(df['open'], df['high'], df['low'], df['close']), + 'shooting_star': talib.CDLSHOOTINGSTAR(df['open'], df['high'], df['low'], df['close']), + 'dark_cloud': talib.CDLDARKCLOUDCOVER(df['open'], df['high'], df['low'], df['close']), + 'evening_star': talib.CDLEVENINGSTAR(df['open'], df['high'], df['low'], df['close']), + 'three_black_crows': talib.CDL3BLACKCROWS(df['open'], df['high'], df['low'], df['close']), + 'bearish_engulfing': talib.CDLENGULFING(df['open'], df['high'], df['low'], df['close']) + } + + # 检查最近3天的K线形态 + for pattern_name, pattern_data in patterns.items(): + if len(pattern_data) > 3: + # 检查最近3天是否有信号 + for i in range(-3, 0): + if pattern_data.iloc[i] < 0: # 看跌信号 + if pattern_name in ['evening_star', 'three_black_crows', 'dark_cloud']: + score += 6 # 强看跌形态 + elif pattern_name in ['hanging_man', 'shooting_star']: + score += 4 # 中度看跌形态 + else: + score += 3 # 弱看跌形态 + break + + # 手动分析一些简单空头形态 + manual_score = self._analyze_manual_bearish_patterns(df.tail(10)) + score += manual_score + + return min(score, 25) + + def _analyze_manual_bearish_patterns(self, df: pd.DataFrame) -> float: + """手动分析空头形态""" + if len(df) < 5: + return 0 + + score = 0 + + # 分析最近5天的走势 + last_5 = df.tail(5) + + # 1. 阶段性高点下移模式 + highs = last_5['high'].tolist() + if len(highs) >= 3: + # 检查是否有高点逐步下移的趋势 + descending_highs = True + for i in range(1, len(highs)): + if highs[i] > highs[i-1] * 1.02: # 允许小幅波动 + descending_highs = False + break + if descending_highs: + score += 4 + + # 2. 跌破前低点模式 + current_low = df.iloc[-1]['low'] + prev_lows = df.iloc[-5:-1]['low'].tolist() + if current_low < min(prev_lows) * 0.99: # 跌破前低 + score += 3 + + return min(score, 7) + + def analyze_breakdown_potential(self, df: pd.DataFrame) -> float: + """分析下跌突破潜力 (20分)""" + if len(df) < 30: + return 0 + + score = 0 + current = df.iloc[-1] + recent_data = df.tail(20) + + # 1. 价格位置分析 - 寻找做空机会 (8分) + recent_high = recent_data['high'].max() + recent_low = recent_data['low'].min() + price_range = recent_high - recent_low + current_position = (current['close'] - recent_low) / price_range if price_range > 0 else 0 + + # 偏好在高位的币种 + if 0.7 <= current_position <= 0.9: # 高位做空机会 + score += 6 + elif 0.5 <= current_position < 0.7: + score += 3 + elif current_position < 0.3: # 已经低位,做空机会不大 + score -= 5 + + # 2. 顶部形态分析 (6分) + volatility = recent_data['close'].std() / recent_data['close'].mean() + if 0.08 <= volatility <= 0.15: # 高位整理,可能形成顶部 + score += 5 + elif volatility > 0.20: # 高波动,可能已经开始下跌 + score += 3 + + # 3. 量能衰竭 (6分) + volume_ratio = recent_data['volume'] / recent_data['volume'].rolling(10).mean() + weak_volume_days = ((volume_ratio < 0.8)).sum() # 量能萱缩 + + if weak_volume_days >= 3: # 量能持续萱缩 + score += 4 + + return max(min(score, 20), 0) + + def analyze_bearish_moving_average(self, df: pd.DataFrame) -> float: + """分析空头均线系统 (10分)""" + if len(df) < 50: + return 0 + + score = 0 + current = df.iloc[-1] + + # 1. 均线空头排列 + if current['close'] < current['ma20'] < current['ma50']: + score += 6 + + # 2. 价格跌破关键均线 + prev = df.iloc[-2] + if prev['close'] >= prev['ma20'] and current['close'] < current['ma20']: + score += 3 + + # 3. 均线斜率向下 + ma20_slope = (current['ma20'] - df.iloc[-5]['ma20']) / 5 + if ma20_slope < 0: + score += 1 + + return min(score, 10) + + def calculate_short_entry_exit_levels(self, df: pd.DataFrame, strategy: str) -> Tuple[float, float, float, float]: + """计算空头入场、止损、止盈位""" + current_price = df.iloc[-1]['close'] + atr = df.iloc[-1]['atr'] + support, resistance = self.find_support_resistance(df) + config = self.strategies[strategy] + + # 空头入场价策略 + ma20 = df.iloc[-1]['ma20'] + ma50 = df.iloc[-1]['ma50'] + + # 策略1: 均线反弹入场 + if current_price < ma20 < ma50: # 空头趋势 + # 等待反弹至MA20附近做空 + entry_price = ma20 * 0.98 # MA20下方2% + else: + # 策略2: 跌破确认入场 + recent_low = df.tail(10)['low'].min() + if current_price <= recent_low * 1.02: # 接近跌破 + entry_price = recent_low * 0.995 # 跌破确认价位 + else: + # 策略3: 当前价格适当上方等待 + entry_price = current_price * 1.005 # 当前价上方0.5% + + # 确保入场价不会过高(距离当前价不超过5%) + if entry_price > current_price * 1.05: + entry_price = current_price * 1.02 + + # 止损位: 阻力位上方或ATR的1.5倍 + stop_loss_atr = entry_price + (atr * 1.5) + stop_loss_resistance = resistance * 1.02 if resistance > 0 else entry_price * 1.08 + stop_loss = min(stop_loss_atr, stop_loss_resistance) + + # 根据策略动态设置风险回报比 + risk = stop_loss - entry_price + target_ratio = (config.risk_reward_min + config.risk_reward_max) / 2 + take_profit_ratio = entry_price - (risk * target_ratio) + + # 如果有支撑位,取较大值 + if support > 0 and support < entry_price: + take_profit_support = support * 1.02 + take_profit = max(take_profit_ratio, take_profit_support) + else: + take_profit = take_profit_ratio + + # 计算实际风险回报比 + actual_risk_reward = (entry_price - take_profit) / risk if risk > 0 else target_ratio + + return entry_price, stop_loss, take_profit, actual_risk_reward + + def generate_short_action_suggestion(self, df: pd.DataFrame, entry_price: float, current_price: float, score: float) -> str: + """生成空头操作建议""" + try: + # 计算价格差异百分比 + entry_diff = (current_price - entry_price) / entry_price * 100 + + # 获取技术指标状态 + current = df.iloc[-1] + recent_data = df.tail(10) + + # 计算价格在最近区间的位置 + recent_high = recent_data['high'].max() + recent_low = recent_data['low'].min() + price_range = recent_high - recent_low + current_position = (current_price - recent_low) / price_range if price_range > 0 else 0.5 + + # 基于评分等级的基础建议 + if score >= 80: # 高分 + if entry_diff >= -1: # 接近或高于建议价位 + if current_position >= 0.7: # 在高位 + return "立即做空" + elif current_position >= 0.5: + return "现价做空" + else: + return "等待反弹做空" + elif entry_diff >= -3: # 稍低于建议价位 + return "等待反弹做空" + else: # 明显低于建议价位 + return "等待大幅反弹" + + elif score >= 65: # 中等分数 + if entry_diff <= 2: # 高于建议价位 + return "现价做空" + elif entry_diff <= -1: # 接近建议价位 + if current_position >= 0.6: + return "现价做空" + else: + return "等待反弹做空" + elif entry_diff <= -5: # 低于建议价位 + return "等待反弹做空" + else: + return "暂时观望" + + else: # 较低分数 + if entry_diff <= 3: # 明显高于建议价位 + return "小仓位做空" + elif entry_diff <= 0: + if current_position >= 0.8: + return "小仓位试探" + else: + return "等待反弹做空" + else: + return "暂时观望" + + return "等待反弹做空" # 默认建议 + + except Exception as e: + logging.error(f"生成空头操作建议时出错: {e}") + return "谨慎观望" + + def test_strategy_distribution(self, market_data: Dict[str, Dict]) -> Dict[str, int]: + """测试策略分布,确保所有策略都能被选中""" + strategy_counts = {name: 0 for name in self.strategies.keys()} + + for symbol, data in list(market_data.items())[:20]: # 只测试前20个 + timeframe_data = data.get('timeframes', {}) + initial_df = timeframe_data.get('4h') + + if initial_df is not None and len(initial_df) >= 20: + strategy = self.determine_best_strategy(initial_df) + strategy_counts[strategy] += 1 + logging.info(f"测试策略分布: {symbol} -> {strategy}") + + logging.info(f"策略分布测试结果: {strategy_counts}") + return strategy_counts \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..cbec047 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,775 @@ + + + + + + AI选币系统 + + + + + + +
+ +
+
+
+ +
+
-
+
总选币数
+
+
+
+ +
+
-
+
活跃选币
+
+
+
+ +
+
-
+
已完成
+
+
+
+ +
+
-%
+
平均收益
+
+
+ + +
+
+
+ + 选币结果 +
+
+ 最后更新: {{ last_update }} +
+
+ +
+ +
正在执行选币分析...
+
+ +
+ {% if grouped_selections %} + {% for group_time, selections in grouped_selections.items() %} +
+
+ {{ group_time }} ({{ selections|length }}个币种) +
+
+
+ {% for selection in selections %} +
+
+
+
{{ selection.symbol.replace('USDT', '') }}
+
+ {{ '做空' if selection.signal_type == 'SHORT' else '做多' }} +
+
+
+
{{ "%.1f"|format(selection.score) }}
+
+ {{ selection.strategy_type }} +
+
+
+ + +
+ {{ selection.action_suggestion }} +
+ +
+ 选择理由: {{ selection.reason }} +
+ +
+
+ 持仓周期: {{ selection.holding_period }}天 + 风险回报: 1:{{ "%.1f"|format(selection.risk_reward_ratio) }} +
+ {% if selection.expiry_time %} +
+ 有效期至: {{ selection.expiry_time[:16] }} +
+ {% endif %} +
+ +
+
+
+ {% if selection.signal_type == 'SHORT' %}做空入场价{% else %}做多入场价{% endif %} +
+
${{ "%.4f"|format(selection.entry_price) }}
+
+
+
当前市价
+ {% if selection.current_price %} +
+ ${{ "%.4f"|format(selection.current_price) }} + {% if (selection.signal_type == 'LONG' and selection.current_price < selection.entry_price) or (selection.signal_type == 'SHORT' and selection.current_price > selection.entry_price) %} +
✓ 可入场 + {% else %} +
⚠ 价格不利 + {% endif %} +
+ {% else %} +
-
+ {% endif %} +
+
+
止损位
+
${{ "%.4f"|format(selection.stop_loss) }}
+
+
+
止盈位
+
${{ "%.4f"|format(selection.take_profit) }}
+
+
+ + +
+ {% endfor %} +
+
+
+ {% endfor %} + + + {% if total_count >= current_limit %} +
+ +
+ {% endif %} + {% else %} +
+ +

暂无选币结果

+

点击"执行选币"开始分析市场

+
+ {% endif %} +
+
+
+ + + + \ No newline at end of file diff --git a/web_app.py b/web_app.py new file mode 100644 index 0000000..54a2b59 --- /dev/null +++ b/web_app.py @@ -0,0 +1,218 @@ +from fastapi import FastAPI, Request, HTTPException +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse, JSONResponse +import uvicorn +import os +from coin_selection_engine import CoinSelectionEngine +from datetime import datetime, timezone, timedelta +from database import utc_to_beijing +import asyncio +from functools import lru_cache +import time + +# 东八区时区 +BEIJING_TZ = timezone(timedelta(hours=8)) + +def get_beijing_time(): + """获取当前东八区时间用于显示""" + return datetime.now(BEIJING_TZ) + +app = FastAPI(title="加密货币选币系统", version="1.0.0") + +# 设置模板和静态文件目录 +templates = Jinja2Templates(directory="templates") + +# 创建静态文件目录 +os.makedirs("static", exist_ok=True) +app.mount("/static", StaticFiles(directory="static"), name="static") + +# 全局变量 +engine = CoinSelectionEngine() + +# 缓存配置 +CACHE_DURATION = 60 # 缓存60秒 +cache_data = { + 'selections': {'data': None, 'timestamp': 0}, + 'stats': {'data': None, 'timestamp': 0} +} + +def get_cached_data(cache_key, fetch_func, *args, **kwargs): + """获取缓存数据或重新获取""" + current_time = time.time() + cache_entry = cache_data.get(cache_key, {'data': None, 'timestamp': 0}) + + # 检查缓存是否过期 + if current_time - cache_entry['timestamp'] > CACHE_DURATION or cache_entry['data'] is None: + cache_entry['data'] = fetch_func(*args, **kwargs) + cache_entry['timestamp'] = current_time + cache_data[cache_key] = cache_entry + + return cache_entry['data'] + +@app.get("/", response_class=HTMLResponse) +async def dashboard(request: Request, limit: int = 20, offset: int = 0): + """主页面 - 支持分页""" + try: + # 使用缓存获取数据 + selections = get_cached_data( + f'selections_{limit}_{offset}', + lambda: engine.get_latest_selections(limit + 5, offset) + ) + + # 按年月日时分分组选币结果,转换时间为东八区显示 + grouped_selections = {} + latest_update_time = None + + for selection in selections: + # 将UTC时间转换为东八区时间显示 + utc_time = selection['selection_time'] + beijing_time = utc_to_beijing(utc_time) + + # 跟踪最新的更新时间 + if latest_update_time is None or utc_time > latest_update_time: + latest_update_time = beijing_time + + # 提取年月日时分部分 (YYYY-MM-DD HH:MM) + time_key = beijing_time[:16] # "YYYY-MM-DD HH:MM" + + if time_key not in grouped_selections: + grouped_selections[time_key] = [] + + # 更新选币记录的显示时间,但不修改原始时间 + selection_copy = selection.copy() + selection_copy['selection_time'] = beijing_time + grouped_selections[time_key].append(selection_copy) + + # 按时间降序排序(最新的在前面) + sorted_grouped_selections = dict(sorted( + grouped_selections.items(), + key=lambda x: x[0], + reverse=True + )) + + return templates.TemplateResponse("dashboard.html", { + "request": request, + "grouped_selections": sorted_grouped_selections, + "last_update": latest_update_time + " CST" if latest_update_time else "暂无数据", + "total_count": len(selections), + "current_limit": limit, + "current_offset": offset + }) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/api/selections") +async def get_selections(limit: int = 20, offset: int = 0): + """获取选币结果API - 支持分页""" + try: + # 限制每页数据量 + limit = min(limit, 100) + # 使用缓存 + selections = get_cached_data( + f'selections_{limit}_{offset}', + lambda: engine.get_latest_selections(limit, offset) + ) + return JSONResponse({ + "status": "success", + "data": selections, + "count": len(selections), + "limit": limit, + "offset": offset + }) + except Exception as e: + return JSONResponse({ + "status": "error", + "message": str(e) + }, status_code=500) + +@app.post("/api/run_selection") +async def run_selection(): + """执行选币API""" + try: + # 清除相关缓存 + cache_data.clear() + + # 异步执行选币 + selected_coins = engine.run_coin_selection() + + return JSONResponse({ + "status": "success", + "message": f"选币完成,共选出{len(selected_coins)}个币种", + "count": len(selected_coins) + }) + except Exception as e: + return JSONResponse({ + "status": "error", + "message": str(e) + }, status_code=500) + +@app.put("/api/selections/{selection_id}/status") +async def update_selection_status(selection_id: int, status: str, exit_price: float = None): + """更新选币状态API""" + try: + # 清除相关缓存 + for key in list(cache_data.keys()): + if 'selections' in key or 'stats' in key: + del cache_data[key] + + engine.update_selection_status(selection_id, status, exit_price) + return JSONResponse({ + "status": "success", + "message": f"更新选币{selection_id}状态为{status}" + }) + except Exception as e: + return JSONResponse({ + "status": "error", + "message": str(e) + }, status_code=500) + +@app.get("/api/stats") +async def get_stats(): + """获取统计信息API""" + try: + # 使用缓存获取统计数据 + stats = get_cached_data('stats', lambda: _get_fresh_stats()) + + return JSONResponse({ + "status": "success", + "data": stats + }) + except Exception as e: + return JSONResponse({ + "status": "error", + "message": str(e) + }, status_code=500) + +def _get_fresh_stats(): + """获取新鲜的统计数据""" + conn = engine.db.get_connection() + cursor = conn.cursor() + + # 总选币数 + cursor.execute("SELECT COUNT(*) FROM coin_selections") + total_selections = cursor.fetchone()[0] + + # 活跃选币数 + cursor.execute("SELECT COUNT(*) FROM coin_selections WHERE status = 'active'") + active_selections = cursor.fetchone()[0] + + # 完成选币数 + cursor.execute("SELECT COUNT(*) FROM coin_selections WHERE status = 'completed'") + completed_selections = cursor.fetchone()[0] + + # 平均收益率 + cursor.execute("SELECT AVG(pnl_percentage) FROM coin_selections WHERE pnl_percentage IS NOT NULL") + avg_pnl = cursor.fetchone()[0] or 0 + + conn.close() + + return { + "total_selections": total_selections, + "active_selections": active_selections, + "completed_selections": completed_selections, + "avg_pnl": round(avg_pnl, 2) + } + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file