This commit is contained in:
aaron 2026-02-15 20:18:51 +08:00
parent c464ae6e4d
commit 92c1a6cbd2
4 changed files with 230 additions and 13 deletions

View File

@ -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 # 是否自动平掉反向持仓(智能策略)

View File

@ -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]:

View File

@ -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}%)",

View File

@ -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]]:
"""
检查当前价格是否触发挂单入场或止盈止损