diff --git a/backend/app/config.py b/backend/app/config.py index c675800..81d6ad1 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -113,7 +113,7 @@ class Settings(BaseSettings): # 模拟交易配置 paper_trading_enabled: bool = True # 是否启用模拟交易 paper_trading_initial_balance: float = 10000 # 初始本金 (USDT) - paper_trading_leverage: int = 10 # 杠杆倍数 + paper_trading_leverage: int = 20 # 杠杆倍数(全仓模式下的最大杠杆) paper_trading_margin_per_order: float = 1000 # 每单保证金 (USDT) paper_trading_max_orders: int = 10 # 最大持仓+挂单总数 paper_trading_auto_close_opposite: bool = False # 是否自动平掉反向持仓(智能策略) diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index 0c77d0c..ce5b437 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -260,9 +260,16 @@ class CryptoAgent: price_change_24h = self._calculate_price_change(data['1h']) logger.info(f"💰 当前价格: ${current_price:,.2f} ({price_change_24h})") - # 2. LLM 分析(包含新闻舆情) + # 获取当前持仓信息(供 LLM 仓位决策) + position_info = self.paper_trading.get_position_info() + + # 2. LLM 分析(包含新闻舆情和持仓信息) logger.info(f"\n🤖 【LLM 分析中...】") - result = await self.llm_analyzer.analyze(symbol, data, symbols=self.symbols) + result = await self.llm_analyzer.analyze( + symbol, data, + symbols=self.symbols, + position_info=position_info + ) # 输出分析摘要 summary = result.get('analysis_summary', '无') @@ -345,10 +352,11 @@ class CryptoAgent: # 5. 创建模拟订单 if self.paper_trading_enabled and self.paper_trading: grade = best_signal.get('grade', 'D') + position_size = best_signal.get('position_size', 'light') if grade != 'D': # 转换信号格式以兼容 paper_trading paper_signal = self._convert_to_paper_signal(symbol, best_signal, current_price) - result = self.paper_trading.create_order_from_signal(paper_signal) + result = self.paper_trading.create_order_from_signal(paper_signal, current_price) # 发送被取消挂单的通知 cancelled_orders = result.get('cancelled_orders', []) @@ -358,7 +366,7 @@ class CryptoAgent: # 记录新订单 order = result.get('order') if order: - logger.info(f" 📝 已创建模拟订单: {order.order_id}") + logger.info(f" 📝 已创建模拟订单: {order.order_id} | 仓位: {position_size}") else: if best_signal: logger.info(f"\n⏸️ 信号冷却中或置信度不足,不发送通知") @@ -389,6 +397,7 @@ class CryptoAgent: 'confidence': signal.get('confidence', 0), 'signal_grade': signal.get('grade', 'D'), 'signal_type': type_map.get(signal_type, 'swing'), + 'position_size': signal.get('position_size', 'light'), # LLM 建议的仓位大小 'reasons': [signal.get('reason', '')], 'timestamp': datetime.now() } @@ -444,7 +453,14 @@ class CryptoAgent: if not self._validate_data(data): return {'error': '数据不完整'} - result = await self.llm_analyzer.analyze(symbol, data, symbols=self.symbols) + # 获取持仓信息 + position_info = self.paper_trading.get_position_info() + + result = await self.llm_analyzer.analyze( + symbol, data, + symbols=self.symbols, + position_info=position_info + ) return result def get_status(self) -> Dict[str, Any]: diff --git a/backend/app/crypto_agent/llm_signal_analyzer.py b/backend/app/crypto_agent/llm_signal_analyzer.py index 9ee1e33..245cdcd 100644 --- a/backend/app/crypto_agent/llm_signal_analyzer.py +++ b/backend/app/crypto_agent/llm_signal_analyzer.py @@ -112,6 +112,8 @@ class LLMSignalAnalyzer: "entry_type": "market/limit", "confidence": 0-100, "grade": "A/B/C/D", + "position_size": "heavy/medium/light", + "position_reason": "仓位建议理由(20字以内)", "entry_price": 建议入场价, "stop_loss": 止损价, "take_profit": 止盈价, @@ -132,6 +134,27 @@ class LLMSignalAnalyzer: - **C级**(40-59):有机会但量价不够理想 - **D级**(<40):量价背离或信号矛盾 +## 七、仓位管理(重要) +你需要根据信号质量和当前持仓情况,建议合适的仓位大小。 + +### 仓位等级 +- **heavy**(重仓):机会极佳,建议使用较大仓位 +- **medium**(中仓):机会不错,建议使用中等仓位 +- **light**(轻仓):机会一般或风险较高,建议轻仓试探 + +### 仓位决策规则 +1. **A级信号**:可建议 heavy 或 medium +2. **B级信号**:建议 medium 或 light +3. **C级信号**:只能建议 light +4. **已有同向持仓时**:新仓位应降一级(避免过度集中) +5. **已有反向持仓时**:谨慎开仓,除非信号极强 +6. **市场波动剧烈时**:仓位应保守 + +### 安全底线(必须遵守) +- 总杠杆永远不得超过 20 倍 +- 单一交易对持仓不宜过大 +- 如果当前持仓已经较重,即使有好机会也要控制仓位 + ## 重要原则 1. **量价优先** - 任何信号都必须有量能配合才可靠 2. **积极但不冒进** - 有合理依据就给出信号,不要过于保守 @@ -139,7 +162,8 @@ class LLMSignalAnalyzer: 4. 止损必须明确,风险收益比至少 1:1.5 5. reason 字段必须包含量价分析(如"放量突破+RSI=45,量比1.8确认有效") 6. entry_type 必须明确:信号已触发用 market,等待更好价位用 limit -7. 短线信号止损控制在 1-2%,中线信号止损控制在 2-4%""" +7. 短线信号止损控制在 1-2%,中线信号止损控制在 2-4% +8. **position_size 必须明确**:根据信号质量和持仓情况给出 heavy/medium/light""" def __init__(self): """初始化分析器""" @@ -150,7 +174,8 @@ class LLMSignalAnalyzer: logger.info(f"LLM 信号分析器初始化完成(含新闻舆情,模型: {self.model_override or '默认'})") async def analyze(self, symbol: str, data: Dict[str, pd.DataFrame], - symbols: List[str] = None) -> Dict[str, Any]: + symbols: List[str] = None, + position_info: Dict[str, Any] = None) -> Dict[str, Any]: """ 使用 LLM 分析市场数据 @@ -158,6 +183,11 @@ class LLMSignalAnalyzer: symbol: 交易对,如 'BTCUSDT' data: 多周期K线数据 {'5m': df, '15m': df, '1h': df, '4h': df} symbols: 所有监控的交易对(用于过滤相关新闻) + position_info: 当前持仓信息,用于仓位管理决策 + - account_balance: 账户余额 + - total_position_value: 总持仓价值 + - current_leverage: 当前杠杆倍数 + - positions: 各交易对持仓列表 Returns: 分析结果 @@ -167,7 +197,7 @@ class LLMSignalAnalyzer: news_text = await self._get_news_context(symbol, symbols or [symbol]) # 构建数据提示 - data_prompt = self._build_data_prompt(symbol, data, news_text) + data_prompt = self._build_data_prompt(symbol, data, news_text, position_info) # 调用 LLM response = llm_service.chat([ @@ -207,8 +237,51 @@ class LLMSignalAnalyzer: # 暂时禁用新闻获取,只做技术面分析 return "" + def _format_position_info(self, symbol: str, position_info: Dict[str, Any]) -> str: + """格式化持仓信息供 LLM 参考""" + lines = [] + + # 账户概况 + balance = position_info.get('account_balance', 0) + total_value = position_info.get('total_position_value', 0) + current_leverage = position_info.get('current_leverage', 0) + max_leverage = 20 # 最大杠杆限制 + + lines.append(f"- 账户余额: ${balance:,.2f}") + lines.append(f"- 总持仓价值: ${total_value:,.2f}") + lines.append(f"- 当前杠杆: {current_leverage:.1f}x / {max_leverage}x") + + # 可用杠杆空间 + available_leverage = max_leverage - current_leverage + if available_leverage > 0: + available_value = balance * available_leverage + lines.append(f"- 可开仓空间: ${available_value:,.2f} ({available_leverage:.1f}x)") + else: + lines.append("- ⚠️ 已达最大杠杆,不建议加仓") + + # 当前交易对持仓 + positions = position_info.get('positions', []) + symbol_positions = [p for p in positions if p.get('symbol') == symbol] + + if symbol_positions: + lines.append(f"\n**{symbol} 当前持仓**:") + for pos in symbol_positions: + side = "做多" if pos.get('side') == 'long' else "做空" + entry = pos.get('entry_price', 0) + pnl = pos.get('pnl_percent', 0) + lines.append(f" - {side} @ ${entry:,.2f} | 盈亏: {pnl:+.2f}%") + else: + lines.append(f"\n**{symbol}**: 无持仓") + + # 其他交易对持仓概况 + other_positions = [p for p in positions if p.get('symbol') != symbol and p.get('status') == 'open'] + if other_positions: + lines.append(f"\n**其他持仓**: {len(other_positions)} 个") + + return "\n".join(lines) + def _build_data_prompt(self, symbol: str, data: Dict[str, pd.DataFrame], - news_text: str = "") -> str: + news_text: str = "", position_info: Dict[str, Any] = None) -> str: """构建数据提示词""" parts = [f"# {symbol} 市场数据分析\n"] parts.append(f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") @@ -219,6 +292,11 @@ class LLMSignalAnalyzer: current_price = float(data['5m'].iloc[-1]['close']) parts.append(f"**当前价格**: ${current_price:,.2f}\n") + # === 新增:账户和持仓信息 === + if position_info: + parts.append("\n## 账户与持仓状态") + parts.append(self._format_position_info(symbol, position_info)) + # === 新增:关键价位分析 === key_levels = self._calculate_key_levels(data) if key_levels: @@ -871,6 +949,18 @@ class LLMSignalAnalyzer: if entry_type not in ['market', 'limit']: signal['entry_type'] = 'market' # 默认现价入场 + # 验证仓位大小(默认根据等级设置) + position_size = signal.get('position_size', '') + if position_size not in ['heavy', 'medium', 'light']: + # 根据信号等级设置默认仓位 + grade = signal.get('grade', 'C') + if grade == 'A': + signal['position_size'] = 'medium' # A级默认中仓 + elif grade == 'B': + signal['position_size'] = 'light' # B级默认轻仓 + else: + signal['position_size'] = 'light' # C级默认轻仓 + return True def _extract_summary(self, text: str) -> str: @@ -946,6 +1036,12 @@ class LLMSignalAnalyzer: entry_type_text = '现价入场' if entry_type == 'market' else '挂单等待' entry_type_icon = '⚡' if entry_type == 'market' else '⏳' + # 仓位大小 + position_size = signal.get('position_size', 'light') + position_map = {'heavy': '重仓', 'medium': '中仓', 'light': '轻仓'} + position_icon = {'heavy': '🔥', 'medium': '📊', 'light': '🌱'}.get(position_size, '🌱') + position_text = position_map.get(position_size, '轻仓') + # 计算风险收益比 entry = signal.get('entry_price', 0) sl = signal.get('stop_loss', 0) @@ -957,6 +1053,7 @@ class LLMSignalAnalyzer: {action_icon} **方向**: {action} {entry_type_icon} **入场**: {entry_type_text} +{position_icon} **仓位**: {position_text} ⭐ **等级**: {grade} {grade_icon} 📈 **置信度**: {confidence}% @@ -1006,6 +1103,12 @@ class LLMSignalAnalyzer: entry_type_text = '现价入场' if entry_type == 'market' else '挂单等待' entry_type_icon = '⚡' if entry_type == 'market' else '⏳' + # 仓位大小 + position_size = signal.get('position_size', 'light') + position_map = {'heavy': '重仓', 'medium': '中仓', 'light': '轻仓'} + position_icon = {'heavy': '🔥', 'medium': '📊', 'light': '🌱'}.get(position_size, '🌱') + position_text = position_map.get(position_size, '轻仓') + # 标题和颜色 if signal['action'] == 'buy': title = f"🟢 {symbol} {signal_type}做多信号 [{entry_type_text}]" @@ -1025,6 +1128,7 @@ class LLMSignalAnalyzer: content_parts = [ f"**{signal_type}** | **{grade}**{grade_icon} | **{confidence}%** 置信度", f"{entry_type_icon} **入场方式**: {entry_type_text}", + f"{position_icon} **建议仓位**: {position_text}", "", f"💰 **入场**: ${entry:,.2f}", f"🛑 **止损**: ${sl:,.2f} ({sl_percent:+.1f}%)", diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index 5e34272..98ddd62 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -131,9 +131,14 @@ class PaperTradingService: logger.info(f"D级信号不开仓: {signal.get('symbol')}") return result - # 固定使用保证金(不再根据等级区分) - margin = self.margin_per_order # 每单固定 1000 USDT 保证金 - position_value = margin * self.leverage # 持仓价值 = 保证金 × 杠杆 + # === 动态仓位计算 === + position_size = signal.get('position_size', 'light') + margin, position_value = self._calculate_dynamic_position(position_size, symbol) + + if margin <= 0: + logger.info(f"无可用保证金: {symbol} | 当前杠杆已达上限") + return result + quantity = position_value # 订单数量(以 USDT 计价) # 确定入场类型 @@ -198,6 +203,98 @@ class PaperTradingService: finally: db.close() + def _calculate_dynamic_position(self, position_size: str, symbol: str) -> tuple: + """ + 根据 LLM 建议的仓位大小计算实际保证金和持仓价值 + + Args: + position_size: 'heavy' / 'medium' / 'light' + symbol: 交易对 + + Returns: + (margin, position_value) 元组 + """ + # 获取当前账户状态 + account = self.get_account_status() + balance = account['current_balance'] + used_margin = account['used_margin'] + max_leverage = self.leverage # 最大杠杆 20x + + # 计算可用保证金空间 + # 全仓模式下:最大持仓价值 = 余额 × 最大杠杆 + max_position_value = balance * max_leverage + current_position_value = account['total_position_value'] + available_position_value = max_position_value - current_position_value + + if available_position_value <= 0: + logger.warning(f"已达最大杠杆限制,无法开仓") + return 0, 0 + + # 根据 position_size 确定仓位比例 + # heavy: 可用空间的 30% + # medium: 可用空间的 15% + # light: 可用空间的 5% + size_ratio = { + 'heavy': 0.30, + 'medium': 0.15, + 'light': 0.05 + }.get(position_size, 0.05) + + # 计算目标持仓价值 + target_position_value = available_position_value * size_ratio + + # 设置最小和最大限制 + min_position_value = 1000 # 最小持仓价值 1000 USDT + max_single_position = balance * 5 # 单笔最大不超过 5x 杠杆 + + position_value = max(min_position_value, min(target_position_value, max_single_position)) + + # 确保不超过可用空间 + position_value = min(position_value, available_position_value) + + # 计算对应的保证金 + margin = position_value / max_leverage + + logger.info(f"动态仓位计算: {position_size} | 可用空间: ${available_position_value:,.0f} | " + f"目标仓位: ${position_value:,.0f} | 保证金: ${margin:,.0f}") + + return margin, position_value + + def get_position_info(self) -> Dict[str, Any]: + """ + 获取当前持仓信息(供 LLM 分析使用) + + Returns: + 持仓信息字典 + """ + account = self.get_account_status() + active_orders = self.get_active_orders() + + # 计算当前杠杆 + balance = account['current_balance'] + total_position_value = account['total_position_value'] + current_leverage = total_position_value / balance if balance > 0 else 0 + + # 格式化持仓列表 + positions = [] + for order in active_orders: + positions.append({ + 'symbol': order.get('symbol'), + 'side': order.get('side'), + 'status': order.get('status'), + 'entry_price': order.get('filled_price') or order.get('entry_price'), + 'quantity': order.get('quantity'), + 'pnl_percent': order.get('pnl_percent', 0) + }) + + return { + 'account_balance': balance, + 'total_position_value': total_position_value, + 'current_leverage': current_leverage, + 'max_leverage': self.leverage, + 'positions': positions + } + def check_price_triggers(self, symbol: str, current_price: float) -> List[Dict[str, Any]]: """ 检查当前价格是否触发挂单入场或止盈止损