trading.ai/coin_selection_engine.py
2025-08-17 21:27:42 +08:00

365 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
saved_long = 0
saved_short = 0
for signal in all_signals:
try:
selection_id = self.db.insert_coin_selection(
symbol=signal.symbol,
qualified_factors=signal.qualified_factors,
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
# 统计实际保存的多空数量
if signal.signal_type == "LONG":
saved_long += 1
else:
saved_short += 1
except Exception as e:
self.logger.error(f"保存{signal.symbol}选币结果失败: {e}")
# 检查并标记过期的选币
self.db.check_and_expire_selections()
self.logger.info(f"选币完成!成功保存{saved_count}个信号(多头: {saved_long}个, 空头: {saved_short}个)")
# 发送钉钉通知
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_factors': 0, 'factors': []}
strategy_stats[key]['count'] += 1
strategy_stats[key]['factors'].append(signal.qualified_factors)
# 计算平均符合因子数
for key, stats in strategy_stats.items():
stats['avg_factors'] = sum(stats['factors']) / len(stats['factors'])
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_factors']:.1f}/4")
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, qualified_factors, 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],
'qualified_factors': 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.qualified_factors}/4 ({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)