diff --git a/backend/api/main.py b/backend/api/main.py index df7f72f..1730efd 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -3,7 +3,7 @@ FastAPI应用主入口 """ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from api.routes import config, trades, stats, dashboard, account +from api.routes import config, trades, stats, dashboard, account, recommendations import os import logging from pathlib import Path @@ -118,6 +118,7 @@ app.include_router(trades.router, prefix="/api/trades", tags=["交易记录"]) app.include_router(stats.router, prefix="/api/stats", tags=["统计分析"]) app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表板"]) app.include_router(account.router, prefix="/api/account", tags=["账户数据"]) +app.include_router(recommendations.router, tags=["交易推荐"]) @app.get("/") diff --git a/backend/api/routes/recommendations.py b/backend/api/routes/recommendations.py new file mode 100644 index 0000000..4728030 --- /dev/null +++ b/backend/api/routes/recommendations.py @@ -0,0 +1,263 @@ +""" +推荐交易对API路由 +""" +from fastapi import APIRouter, HTTPException, Query +from typing import Optional, List +from datetime import datetime, timedelta +from database.models import TradeRecommendation +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/recommendations", tags=["recommendations"]) + + +@router.get("") +async def get_recommendations( + status: Optional[str] = Query(None, description="状态过滤: active, executed, expired, cancelled"), + direction: Optional[str] = Query(None, description="方向过滤: BUY, SELL"), + limit: int = Query(50, ge=1, le=200, description="返回数量限制"), + start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD)"), + end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD)") +): + """ + 获取推荐交易对列表 + + Args: + status: 状态过滤 + direction: 方向过滤 + limit: 返回数量限制 + start_date: 开始日期 + end_date: 结束日期 + """ + try: + # 转换日期字符串 + start_dt = None + end_dt = None + if start_date: + try: + start_dt = datetime.strptime(start_date, "%Y-%m-%d") + except ValueError: + raise HTTPException(status_code=400, detail="开始日期格式错误,应为 YYYY-MM-DD") + if end_date: + try: + end_dt = datetime.strptime(end_date, "%Y-%m-%d") + # 设置为当天的23:59:59 + end_dt = end_dt.replace(hour=23, minute=59, second=59) + except ValueError: + raise HTTPException(status_code=400, detail="结束日期格式错误,应为 YYYY-MM-DD") + + recommendations = TradeRecommendation.get_all( + status=status, + direction=direction, + limit=limit, + start_date=start_dt, + end_date=end_dt + ) + + return { + "success": True, + "count": len(recommendations), + "data": recommendations + } + except Exception as e: + logger.error(f"获取推荐列表失败: {e}") + raise HTTPException(status_code=500, detail=f"获取推荐列表失败: {str(e)}") + + +@router.get("/active") +async def get_active_recommendations(): + """ + 获取当前有效的推荐(未过期、未执行、未取消) + """ + try: + recommendations = TradeRecommendation.get_active() + return { + "success": True, + "count": len(recommendations), + "data": recommendations + } + except Exception as e: + logger.error(f"获取有效推荐失败: {e}") + raise HTTPException(status_code=500, detail=f"获取有效推荐失败: {str(e)}") + + +@router.get("/{recommendation_id}") +async def get_recommendation(recommendation_id: int): + """ + 根据ID获取推荐详情 + """ + try: + recommendation = TradeRecommendation.get_by_id(recommendation_id) + if not recommendation: + raise HTTPException(status_code=404, detail="推荐不存在") + + return { + "success": True, + "data": recommendation + } + except HTTPException: + raise + except Exception as e: + logger.error(f"获取推荐详情失败: {e}") + raise HTTPException(status_code=500, detail=f"获取推荐详情失败: {str(e)}") + + +@router.get("/symbol/{symbol}") +async def get_recommendations_by_symbol( + symbol: str, + limit: int = Query(10, ge=1, le=50, description="返回数量限制") +): + """ + 根据交易对获取推荐记录 + """ + try: + recommendations = TradeRecommendation.get_by_symbol(symbol, limit=limit) + return { + "success": True, + "count": len(recommendations), + "data": recommendations + } + except Exception as e: + logger.error(f"获取交易对推荐失败: {e}") + raise HTTPException(status_code=500, detail=f"获取交易对推荐失败: {str(e)}") + + +@router.post("/{recommendation_id}/execute") +async def mark_recommendation_executed( + recommendation_id: int, + trade_id: Optional[int] = None +): + """ + 标记推荐已执行 + + Args: + recommendation_id: 推荐ID + trade_id: 关联的交易记录ID(可选) + """ + try: + recommendation = TradeRecommendation.get_by_id(recommendation_id) + if not recommendation: + raise HTTPException(status_code=404, detail="推荐不存在") + + if recommendation['status'] != 'active': + raise HTTPException( + status_code=400, + detail=f"推荐状态为 {recommendation['status']},无法标记为已执行" + ) + + TradeRecommendation.mark_executed(recommendation_id, trade_id) + + return { + "success": True, + "message": "推荐已标记为已执行" + } + except HTTPException: + raise + except Exception as e: + logger.error(f"标记推荐已执行失败: {e}") + raise HTTPException(status_code=500, detail=f"标记推荐已执行失败: {str(e)}") + + +@router.post("/{recommendation_id}/cancel") +async def cancel_recommendation( + recommendation_id: int, + notes: Optional[str] = None +): + """ + 取消推荐 + + Args: + recommendation_id: 推荐ID + notes: 取消原因备注 + """ + try: + recommendation = TradeRecommendation.get_by_id(recommendation_id) + if not recommendation: + raise HTTPException(status_code=404, detail="推荐不存在") + + if recommendation['status'] != 'active': + raise HTTPException( + status_code=400, + detail=f"推荐状态为 {recommendation['status']},无法取消" + ) + + TradeRecommendation.mark_cancelled(recommendation_id, notes) + + return { + "success": True, + "message": "推荐已取消" + } + except HTTPException: + raise + except Exception as e: + logger.error(f"取消推荐失败: {e}") + raise HTTPException(status_code=500, detail=f"取消推荐失败: {str(e)}") + + +@router.post("/generate") +async def generate_recommendations( + min_signal_strength: int = Query(5, ge=0, le=10, description="最小信号强度"), + max_recommendations: int = Query(20, ge=1, le=50, description="最大推荐数量") +): + """ + 生成新的交易推荐 + + Args: + min_signal_strength: 最小信号强度 + max_recommendations: 最大推荐数量 + """ + try: + # 导入推荐器 + import sys + from pathlib import Path + project_root = Path(__file__).parent.parent.parent + trading_system_path = project_root / 'trading_system' + + if not trading_system_path.exists(): + raise HTTPException(status_code=500, detail="交易系统模块不存在") + + sys.path.insert(0, str(trading_system_path)) + + from binance_client import BinanceClient + from market_scanner import MarketScanner + from risk_manager import RiskManager + from trade_recommender import TradeRecommender + import config + + # 初始化组件 + client = BinanceClient( + api_key=config.BINANCE_API_KEY, + api_secret=config.BINANCE_API_SECRET, + testnet=config.USE_TESTNET + ) + + await client.connect() + + try: + scanner = MarketScanner(client) + risk_manager = RiskManager(client) + recommender = TradeRecommender(client, scanner, risk_manager) + + # 生成推荐 + recommendations = await recommender.generate_recommendations( + min_signal_strength=min_signal_strength, + max_recommendations=max_recommendations + ) + + return { + "success": True, + "count": len(recommendations), + "message": f"成功生成 {len(recommendations)} 个推荐", + "data": recommendations + } + finally: + await client.disconnect() + + except HTTPException: + raise + except Exception as e: + logger.error(f"生成推荐失败: {e}") + import traceback + logger.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=f"生成推荐失败: {str(e)}") diff --git a/backend/database/init.sql b/backend/database/init.sql index 92aefe0..95b3a88 100644 --- a/backend/database/init.sql +++ b/backend/database/init.sql @@ -80,6 +80,51 @@ CREATE TABLE IF NOT EXISTS `trading_signals` ( INDEX `idx_executed` (`executed`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易信号表'; +-- 推荐交易对表 +CREATE TABLE IF NOT EXISTS `trade_recommendations` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `symbol` VARCHAR(20) NOT NULL, + `direction` VARCHAR(10) NOT NULL COMMENT 'BUY, SELL', + `recommendation_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `current_price` DECIMAL(20, 8) NOT NULL, + `change_percent` DECIMAL(10, 4) COMMENT '24小时涨跌幅', + `recommendation_reason` TEXT NOT NULL COMMENT '推荐原因', + `signal_strength` INT NOT NULL COMMENT '信号强度 0-10', + `market_regime` VARCHAR(20) COMMENT 'trending, ranging', + `trend_4h` VARCHAR(10) COMMENT '4H周期趋势: up, down, neutral', + -- 技术指标 + `rsi` DECIMAL(10, 4) COMMENT 'RSI指标', + `macd_histogram` DECIMAL(20, 8) COMMENT 'MACD柱状图', + `bollinger_upper` DECIMAL(20, 8) COMMENT '布林带上轨', + `bollinger_middle` DECIMAL(20, 8) COMMENT '布林带中轨', + `bollinger_lower` DECIMAL(20, 8) COMMENT '布林带下轨', + `ema20` DECIMAL(20, 8) COMMENT 'EMA20', + `ema50` DECIMAL(20, 8) COMMENT 'EMA50', + `ema20_4h` DECIMAL(20, 8) COMMENT '4H周期EMA20', + `atr` DECIMAL(20, 8) COMMENT '平均真实波幅', + -- 建议参数 + `suggested_stop_loss` DECIMAL(20, 8) COMMENT '建议止损价', + `suggested_take_profit_1` DECIMAL(20, 8) COMMENT '建议第一目标止盈价(盈亏比1:1)', + `suggested_take_profit_2` DECIMAL(20, 8) COMMENT '建议第二目标止盈价', + `suggested_position_percent` DECIMAL(10, 4) COMMENT '建议仓位百分比', + `suggested_leverage` INT DEFAULT 10 COMMENT '建议杠杆倍数', + -- 市场数据 + `volume_24h` DECIMAL(20, 8) COMMENT '24小时成交量', + `volatility` DECIMAL(10, 4) COMMENT '波动率', + -- 状态 + `status` VARCHAR(20) DEFAULT 'active' COMMENT 'active: 有效, expired: 已过期, executed: 已执行, cancelled: 已取消', + `executed_at` TIMESTAMP NULL COMMENT '执行时间', + `execution_result` VARCHAR(50) COMMENT '执行结果: success, failed', + `execution_trade_id` INT COMMENT '关联的交易记录ID', + `expires_at` TIMESTAMP NULL COMMENT '推荐过期时间(默认24小时后)', + `notes` TEXT COMMENT '备注信息', + INDEX `idx_symbol` (`symbol`), + INDEX `idx_recommendation_time` (`recommendation_time`), + INDEX `idx_status` (`status`), + INDEX `idx_direction` (`direction`), + INDEX `idx_signal_strength` (`signal_strength`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐交易对表'; + -- 初始化默认配置 diff --git a/backend/database/models.py b/backend/database/models.py index 31b562e..3e8574b 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -228,3 +228,121 @@ class TradingSignal: "SELECT * FROM trading_signals ORDER BY signal_time DESC LIMIT %s", (limit,) ) + + +class TradeRecommendation: + """推荐交易对模型""" + + @staticmethod + def create( + symbol, direction, current_price, change_percent, recommendation_reason, + signal_strength, market_regime=None, trend_4h=None, + rsi=None, macd_histogram=None, + bollinger_upper=None, bollinger_middle=None, bollinger_lower=None, + ema20=None, ema50=None, ema20_4h=None, atr=None, + suggested_stop_loss=None, suggested_take_profit_1=None, suggested_take_profit_2=None, + suggested_position_percent=None, suggested_leverage=10, + volume_24h=None, volatility=None, notes=None + ): + """创建推荐记录(使用北京时间)""" + recommendation_time = get_beijing_time() + # 默认24小时后过期 + expires_at = recommendation_time + timedelta(hours=24) + + db.execute_update( + """INSERT INTO trade_recommendations + (symbol, direction, recommendation_time, current_price, change_percent, + recommendation_reason, signal_strength, market_regime, trend_4h, + rsi, macd_histogram, bollinger_upper, bollinger_middle, bollinger_lower, + ema20, ema50, ema20_4h, atr, + suggested_stop_loss, suggested_take_profit_1, suggested_take_profit_2, + suggested_position_percent, suggested_leverage, + volume_24h, volatility, expires_at, notes) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""", + (symbol, direction, recommendation_time, current_price, change_percent, + recommendation_reason, signal_strength, market_regime, trend_4h, + rsi, macd_histogram, bollinger_upper, bollinger_middle, bollinger_lower, + ema20, ema50, ema20_4h, atr, + suggested_stop_loss, suggested_take_profit_1, suggested_take_profit_2, + suggested_position_percent, suggested_leverage, + volume_24h, volatility, expires_at, notes) + ) + return db.execute_one("SELECT LAST_INSERT_ID() as id")['id'] + + @staticmethod + def mark_executed(recommendation_id, trade_id=None, execution_result='success'): + """标记推荐已执行""" + executed_at = get_beijing_time() + db.execute_update( + """UPDATE trade_recommendations + SET status = 'executed', executed_at = %s, execution_result = %s, execution_trade_id = %s + WHERE id = %s""", + (executed_at, execution_result, trade_id, recommendation_id) + ) + + @staticmethod + def mark_expired(recommendation_id): + """标记推荐已过期""" + db.execute_update( + "UPDATE trade_recommendations SET status = 'expired' WHERE id = %s", + (recommendation_id,) + ) + + @staticmethod + def mark_cancelled(recommendation_id, notes=None): + """标记推荐已取消""" + db.execute_update( + "UPDATE trade_recommendations SET status = 'cancelled', notes = %s WHERE id = %s", + (notes, recommendation_id) + ) + + @staticmethod + def get_all(status=None, direction=None, limit=100, start_date=None, end_date=None): + """获取推荐记录""" + query = "SELECT * FROM trade_recommendations WHERE 1=1" + params = [] + + if status: + query += " AND status = %s" + params.append(status) + if direction: + query += " AND direction = %s" + params.append(direction) + if start_date: + query += " AND recommendation_time >= %s" + params.append(start_date) + if end_date: + query += " AND recommendation_time <= %s" + params.append(end_date) + + query += " ORDER BY recommendation_time DESC, signal_strength DESC LIMIT %s" + params.append(limit) + + return db.execute_query(query, params) + + @staticmethod + def get_active(): + """获取当前有效的推荐(未过期、未执行、未取消)""" + return db.execute_query( + """SELECT * FROM trade_recommendations + WHERE status = 'active' AND (expires_at IS NULL OR expires_at > NOW()) + ORDER BY signal_strength DESC, recommendation_time DESC""" + ) + + @staticmethod + def get_by_id(recommendation_id): + """根据ID获取推荐""" + return db.execute_one( + "SELECT * FROM trade_recommendations WHERE id = %s", + (recommendation_id,) + ) + + @staticmethod + def get_by_symbol(symbol, limit=10): + """根据交易对获取推荐记录""" + return db.execute_query( + """SELECT * FROM trade_recommendations + WHERE symbol = %s + ORDER BY recommendation_time DESC LIMIT %s""", + (symbol, limit) + ) diff --git a/trading_system/trade_recommender.py b/trading_system/trade_recommender.py new file mode 100644 index 0000000..020b45a --- /dev/null +++ b/trading_system/trade_recommender.py @@ -0,0 +1,341 @@ +""" +推荐交易对模块 - 生成交易推荐供手动参考 +""" +import asyncio +import logging +from typing import List, Dict, Optional +from datetime import datetime, timedelta +try: + from .binance_client import BinanceClient + from .market_scanner import MarketScanner + from .risk_manager import RiskManager + from . import config +except ImportError: + from binance_client import BinanceClient + from market_scanner import MarketScanner + from risk_manager import RiskManager + import config + +logger = logging.getLogger(__name__) + +# 尝试导入数据库模型 +DB_AVAILABLE = False +TradeRecommendation = None +try: + import sys + from pathlib import Path + project_root = Path(__file__).parent.parent + backend_path = project_root / 'backend' + if backend_path.exists(): + sys.path.insert(0, str(backend_path)) + from database.models import TradeRecommendation + DB_AVAILABLE = True + logger.info("✓ 数据库模型导入成功,推荐记录将保存到数据库") + else: + logger.warning("⚠ backend目录不存在,无法使用数据库功能") + DB_AVAILABLE = False +except ImportError as e: + logger.warning(f"⚠ 无法导入数据库模型: {e}") + DB_AVAILABLE = False +except Exception as e: + logger.warning(f"⚠ 数据库初始化失败: {e}") + DB_AVAILABLE = False + + +class TradeRecommender: + """推荐交易对生成器""" + + def __init__( + self, + client: BinanceClient, + scanner: MarketScanner, + risk_manager: RiskManager + ): + """ + 初始化推荐器 + + Args: + client: 币安客户端 + scanner: 市场扫描器 + risk_manager: 风险管理器 + """ + self.client = client + self.scanner = scanner + self.risk_manager = risk_manager + + async def generate_recommendations( + self, + min_signal_strength: int = 5, + max_recommendations: int = 20 + ) -> List[Dict]: + """ + 生成交易推荐 + + Args: + min_signal_strength: 最小信号强度(默认5,低于此强度的不推荐) + max_recommendations: 最大推荐数量 + + Returns: + 推荐列表 + """ + logger.info("开始生成交易推荐...") + + # 1. 扫描市场 + top_symbols = await self.scanner.scan_market() + if not top_symbols: + logger.warning("未找到符合条件的交易对") + return [] + + recommendations = [] + + # 2. 对每个交易对进行分析 + for symbol_info in top_symbols: + if len(recommendations) >= max_recommendations: + break + + symbol = symbol_info['symbol'] + current_price = symbol_info['price'] + change_percent = symbol_info.get('changePercent', 0) + + # 3. 分析交易信号(使用策略模块的逻辑) + trade_signal = await self._analyze_trade_signal(symbol_info) + + # 4. 如果信号强度足够,生成推荐 + if trade_signal['should_trade'] and trade_signal['strength'] >= min_signal_strength: + recommendation = await self._create_recommendation( + symbol_info, trade_signal + ) + if recommendation: + recommendations.append(recommendation) + + logger.info(f"生成了 {len(recommendations)} 个交易推荐") + return recommendations + + async def _analyze_trade_signal(self, symbol_info: Dict) -> Dict: + """ + 分析交易信号(复用策略模块的逻辑) + + Args: + symbol_info: 交易对信息 + + Returns: + 交易信号字典 + """ + symbol = symbol_info['symbol'] + current_price = symbol_info['price'] + rsi = symbol_info.get('rsi') + macd = symbol_info.get('macd') + bollinger = symbol_info.get('bollinger') + market_regime = symbol_info.get('marketRegime', 'unknown') + ema20 = symbol_info.get('ema20') + ema50 = symbol_info.get('ema50') + ema20_4h = symbol_info.get('ema20_4h') + price_4h = symbol_info.get('price_4h', current_price) + + # 判断4H周期趋势方向 + trend_4h = None + if ema20_4h is not None: + if price_4h > ema20_4h: + trend_4h = 'up' + elif price_4h < ema20_4h: + trend_4h = 'down' + else: + trend_4h = 'neutral' + + signal_strength = 0 + reasons = [] + direction = None + + # 策略1:均值回归(震荡市场) + if market_regime == 'ranging': + if rsi and rsi < 30: + if trend_4h in ('up', 'neutral', None): + signal_strength += 4 + reasons.append(f"RSI超卖({rsi:.1f})") + if direction is None: + direction = 'BUY' + elif rsi and rsi > 70: + if trend_4h in ('down', 'neutral', None): + signal_strength += 4 + reasons.append(f"RSI超买({rsi:.1f})") + if direction is None: + direction = 'SELL' + + if bollinger and current_price <= bollinger.get('lower'): + if trend_4h in ('up', 'neutral', None): + signal_strength += 3 + reasons.append("触及布林带下轨") + if direction is None: + direction = 'BUY' + elif bollinger and current_price >= bollinger.get('upper'): + if trend_4h in ('down', 'neutral', None): + signal_strength += 3 + reasons.append("触及布林带上轨") + if direction is None: + direction = 'SELL' + + # 策略2:趋势跟踪(趋势市场) + elif market_regime == 'trending': + if macd and macd.get('macd', 0) > macd.get('signal', 0) and macd.get('histogram', 0) > 0: + if trend_4h in ('up', 'neutral', None): + signal_strength += 3 + reasons.append("MACD金叉") + if direction is None: + direction = 'BUY' + elif macd and macd.get('macd', 0) < macd.get('signal', 0) and macd.get('histogram', 0) < 0: + if trend_4h in ('down', 'neutral', None): + signal_strength += 3 + reasons.append("MACD死叉") + if direction is None: + direction = 'SELL' + + if ema20 and ema50: + if current_price > ema20 > ema50: + if trend_4h in ('up', 'neutral', None): + signal_strength += 2 + reasons.append("价格在均线之上") + if direction is None: + direction = 'BUY' + elif current_price < ema20 < ema50: + if trend_4h in ('down', 'neutral', None): + signal_strength += 2 + reasons.append("价格在均线之下") + if direction is None: + direction = 'SELL' + + # 多周期共振加分 + if direction and trend_4h: + if (direction == 'BUY' and trend_4h == 'up') or (direction == 'SELL' and trend_4h == 'down'): + signal_strength += 2 + reasons.append("4H周期共振确认") + + # 判断是否应该交易 + min_signal_strength = config.TRADING_CONFIG.get('MIN_SIGNAL_STRENGTH', 7) + should_trade = signal_strength >= min_signal_strength + + # 禁止逆4H趋势交易 + if direction and trend_4h: + if (direction == 'BUY' and trend_4h == 'down') or (direction == 'SELL' and trend_4h == 'up'): + should_trade = False + reasons.append("❌ 禁止逆4H趋势交易") + + return { + 'should_trade': should_trade, + 'direction': direction, + 'reason': ', '.join(reasons) if reasons else '无明确信号', + 'strength': signal_strength, + 'trend_4h': trend_4h + } + + async def _create_recommendation( + self, + symbol_info: Dict, + trade_signal: Dict + ) -> Optional[Dict]: + """ + 创建推荐记录 + + Args: + symbol_info: 交易对信息 + trade_signal: 交易信号 + + Returns: + 推荐字典 + """ + try: + symbol = symbol_info['symbol'] + current_price = symbol_info['price'] + direction = trade_signal['direction'] + + # 计算建议的止损止盈 + entry_price = current_price + stop_loss_price = self.risk_manager.get_stop_loss_price( + entry_price, + direction, + klines=symbol_info.get('klines'), + bollinger=symbol_info.get('bollinger'), + atr=symbol_info.get('atr') + ) + + # 计算止损百分比 + if direction == 'BUY': + stop_loss_pct = (entry_price - stop_loss_price) / entry_price + else: + stop_loss_pct = (stop_loss_price - entry_price) / entry_price + + # 第一目标:盈亏比1:1 + if direction == 'BUY': + take_profit_1 = entry_price + (entry_price - stop_loss_price) + else: + take_profit_1 = entry_price - (stop_loss_price - entry_price) + + # 第二目标:止损的2.5倍 + take_profit_2_pct = stop_loss_pct * 2.5 + take_profit_2 = self.risk_manager.get_take_profit_price( + entry_price, direction, take_profit_pct=take_profit_2_pct + ) + + # 建议仓位(根据信号强度调整) + base_position_pct = config.TRADING_CONFIG.get('MAX_POSITION_PERCENT', 0.05) + signal_strength = trade_signal['strength'] + # 信号强度越高,建议仓位可以适当增加(但不超过1.5倍) + position_multiplier = min(1.0 + (signal_strength - 5) * 0.1, 1.5) + suggested_position_pct = base_position_pct * position_multiplier + + # 准备推荐数据 + recommendation_data = { + 'symbol': symbol, + 'direction': direction, + 'current_price': current_price, + 'change_percent': symbol_info.get('changePercent', 0), + 'recommendation_reason': trade_signal['reason'], + 'signal_strength': signal_strength, + 'market_regime': symbol_info.get('marketRegime'), + 'trend_4h': trade_signal.get('trend_4h'), + 'rsi': symbol_info.get('rsi'), + 'macd_histogram': symbol_info.get('macd', {}).get('histogram') if symbol_info.get('macd') else None, + 'bollinger_upper': symbol_info.get('bollinger', {}).get('upper') if symbol_info.get('bollinger') else None, + 'bollinger_middle': symbol_info.get('bollinger', {}).get('middle') if symbol_info.get('bollinger') else None, + 'bollinger_lower': symbol_info.get('bollinger', {}).get('lower') if symbol_info.get('bollinger') else None, + 'ema20': symbol_info.get('ema20'), + 'ema50': symbol_info.get('ema50'), + 'ema20_4h': symbol_info.get('ema20_4h'), + 'atr': symbol_info.get('atr'), + 'suggested_stop_loss': stop_loss_price, + 'suggested_take_profit_1': take_profit_1, + 'suggested_take_profit_2': take_profit_2, + 'suggested_position_percent': suggested_position_pct, + 'suggested_leverage': config.TRADING_CONFIG.get('LEVERAGE', 10), + 'volume_24h': symbol_info.get('volume24h'), + 'volatility': symbol_info.get('volatility') + } + + # 保存到数据库 + if DB_AVAILABLE and TradeRecommendation: + try: + recommendation_id = TradeRecommendation.create(**recommendation_data) + logger.info( + f"✓ 推荐已保存: {symbol} {direction} " + f"(信号强度: {signal_strength}/10, ID: {recommendation_id})" + ) + recommendation_data['id'] = recommendation_id + except Exception as e: + logger.error(f"保存推荐到数据库失败: {e}") + + return recommendation_data + + except Exception as e: + logger.error(f"创建推荐失败 {symbol_info.get('symbol', 'unknown')}: {e}") + return None + + async def get_active_recommendations(self) -> List[Dict]: + """获取当前有效的推荐""" + if DB_AVAILABLE and TradeRecommendation: + return TradeRecommendation.get_active() + return [] + + async def mark_recommendation_executed(self, recommendation_id: int, trade_id: int = None): + """标记推荐已执行""" + if DB_AVAILABLE and TradeRecommendation: + TradeRecommendation.mark_executed(recommendation_id, trade_id) + logger.info(f"推荐 {recommendation_id} 已标记为已执行")