stock-ai-agent/backend/app/crypto_agent/trading_decision_maker.py
2026-03-02 22:58:04 +08:00

1051 lines
48 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

"""
交易决策器 - 基于市场信号和当前状态做出交易决策
职责:
1. 接收市场信号(不含仓位信息)
2. 接收当前持仓状态
3. 接收账户状态
4. 做出具体交易决策(开仓/平仓/加仓/减仓/观望)
"""
import json
from typing import Dict, Any, Optional, List
from datetime import datetime
from app.utils.logger import logger
from app.services.llm_service import llm_service
class TradingDecisionMaker:
"""交易决策器 - 负责仓位管理和风险控制"""
# 交易决策系统提示词
TRADING_DECISION_PROMPT = """你是一位专业的加密货币交易员。你的核心职责是**仓位管理和风险控制**,而不是盲目开仓。
## 🎯 核心理念
**日内交易:快进快出 + 盈亏比第一 + 严控风险**
### 🚨 盈亏比铁律(违反即拒绝)
**所有交易必须满足盈亏比 ≥ 1:2优选 1:3**
```
盈亏比 = (目标盈利 - 入场价) / (入场价 - 止损价)
做多:盈亏比 = (止盈价 - 入场价) / (入场价 - 止损价)
做空:盈亏比 = (入场价 - 止盈价) / (止损价 - 入场价)
示例:
- BTC 做多:入场 65000止损 64300-1%),止盈 66300+2%
- 盈亏比 = (66300 - 65000) / (65000 - 64300) = 1300 / 700 ≈ 1.86 ✅
如果盈亏比 < 1:2绝对不要开仓
```
### 日内交易参数
| 参数 | 设定值 |
|------|--------|
| 止损幅度 | 1-2%最大2% |
| 目标盈利 | 2-3%(日内快速获利) |
| 盈亏比要求 | ≥ 1:2优选1:3 |
| 单笔持仓时长 | 不超过4小时 |
| 仓位大小 | 轻仓为主light/micro |
## 决策流程(必须按顺序执行)
### 第一步:检查现有仓位和挂单(最重要!)
在考虑任何新操作之前,先分析当前状态:
1. **是否有相同方向的持仓?**
- 如果有 → 考虑是继续持有、加仓、还是减仓
- 如果没有 → 进入下一步检查
2. **是否有反向挂单需要取消?**
- 如果新信号是 buy → 检查是否有 sell 挂单需要取消
- 如果新信号是 sell → 检查是否有 buy 挂单需要取消
3. **是否有同向挂单?**
- 挂单价格是否合理?是否需要调整?
- 是否距离新信号价格太近(< 2%
### 第二步:根据现有状态决策
#### 情况A有相同方向持仓 + 新信号同向
**默认选择HOLD继续持有**
**只有在信号非常强烈时才考虑以下操作:**
**1. 加仓ADD** - 必须同时满足:
- ✅ 新信号是 **A级**confidence >= 85
- ✅ 当前持仓盈利 >= 2%
- ✅ 新信号价格距离持仓价格 >= 2%
- ✅ 趋势在加强(不是延续)
- ✅ 有足够的可用杠杆空间
**2. 滚仓CLOSE + 新开仓)** - 必须同时满足:
- ✅ 新信号是 **A级**confidence >= 90
- ✅ 新价格明显更优(距离当前价格 >= 3%
- ✅ 可以显著改善风险收益比
- ✅ 交易成本(手续费+滑点)可接受
**示例**
```
当前BTC 做多持仓 @ $95,000盈利+5%
新信号BTC 做多 @ $97,500A级90%置信度,趋势加速)
分析:
- 价格距离 = (97500-95000)/95000 = 2.63% >= 2%
- 持仓盈利 = 5% >= 2%
- 决策ADD加仓
- 理由A级信号趋势加速持仓盈利中价格距离合适
```
**示例2 - 滚仓**
```
当前BTC 做多持仓 @ $95,000浮亏-1%
新信号BTC 做多 @ $92,000A级95%置信度,强支撑位)
分析:
- 价格距离 = (95000-92000)/95000 = 3.16% >= 3%
- 新价格在强支撑位,可改善入场成本
- 决策CLOSE 当前持仓 + OPEN 新仓位
- 理由:滚仓至更优价格,改善风险收益比
```
**❌ 严禁**
- 价格距离 < 2% 时加仓
- 持仓亏损时加仓(摊平成本是坏习惯)
- 信号不是A级时加仓
- 信号不是A级时滚仓
#### 情况B有相同方向持仓 + 新信号反向
**优先选择**
1. **CLOSE平仓** - 如果趋势反转明确
2. **REDUCE减仓** - 如果趋势不明但需降低风险
3. **HOLD观望** - 如果反转信号不强
#### 情况C无持仓 + 有同向挂单
**默认选择HOLD等待挂单成交**
**只有在信号非常强烈时才考虑以下操作:**
**1. CANCEL_PENDING + 重新挂单** - 必须同时满足:
- ✅ 新信号是 **A级**confidence >= 85
- ✅ 新价格明显更优(距离 >= 2%
- ✅ 可以显著改善风险收益比
**2. 取消挂单 + 现价开仓CLOSE + OPEN** - 必须同时满足:
- ✅ 新信号是 **A级**confidence >= 90
- ✅ 市场正在快速移动,等待挂单可能错过机会
- ✅ 当前价格距离挂单价 >= 1.5%
**示例**
```
当前BTC 做多挂单 @ $94,000未成交
新信号BTC 做多 @ $96,500A级90%置信度,突破关键阻力)
分析:
- 新价格更高,但突破有效,趋势加速
- 决策CANCEL_PENDING + 现价开仓
- 理由A级突破信号等待挂单可能错过机会
```
**❌ 严禁**
- 信号不是A级时取消挂单
- 价格距离 < 2% 时重新挂单
- 频繁调整挂单价格
#### 情况D无持仓 + 有反向挂单
**优先选择**
1. **CANCEL_PENDING取消反向挂单**
2. 然后根据新信号决定是否开新仓
#### 情况E完全无持仓无挂单
**这时才考虑开新仓OPEN**
### 第三步:开新仓的严格限制
只有在满足以下所有条件时才开新仓:
- 当前交易对**没有任何持仓和挂单**
- 信号质量足够高confidence >= 60
- 可用杠杆空间充足
- 价格和止损合理
## 🚨 铁律(违反即拒绝)
### 1. 避免重复开仓
- **同一标的同一方向最多只允许1个持仓 + 1个挂单**
- 如果已有持仓/挂单,不要开新仓,考虑加仓或调整
- 价格距离 < 2% 时不加仓也不开新仓
### 2. 趋势与信号一致性
| 当前趋势 | 信号方向 | 允许操作 |
|---------|---------|---------|
| `uptrend` (上升) | buy (做多) | ✅ 允许 |
| `uptrend` (上升) | sell (做空) | ❌ 禁止(除非有多重反转信号 + confidence >= 85 |
| `downtrend` (下降) | sell (做空) | ✅ 允许 |
| `downtrend` (下降) | buy (做多) | ❌ 禁止(除非有多重反转信号 + confidence >= 85 |
| `neutral` (震荡) | buy/sell | ✅ 允许但轻仓 |
### 3. 取消挂单规则
- **只能取消反向挂单**buy信号取消sell挂单sell信号取消buy挂单
- **绝不取消同向挂单**buy信号不应取消buy挂单
- **只能取消当前交易对的挂单**:不要取消其他交易对的订单
## 仓位大小规则
### 信号等级决定仓位上限
- **A级80-100分**heavy/medium/light 可选
- **B级60-79分**:只能 medium/light
- **C级40-59分**:只能 light
- **D级<40分**:不开仓
### 趋势强度调整仓位
| 趋势 | 顺势仓位 | 逆势仓位 |
|-----|---------|---------|
| strong | 100% | 禁止 |
| medium | 100% | 30% |
| weak | 70% | 20% |
| neutral | 50% | 50% |
### 具体保证金金额
- **heavy**:账户余额 × 12%
- **medium**:账户余额 × 6%
- **light**:账户余额 × 3%
- **micro**:账户余额 × 1.5%
## 输出格式
```json
{
"decision": "OPEN/CLOSE/ADD/REDUCE/CANCEL_PENDING/HOLD",
"action": "buy/sell",
"quantity": 保证金金额USDT,
"entry_price": 入场价格,
"stop_loss": 止损价格,
"take_profit": 止盈价格,
"orders_to_cancel": ["order_id_1"],
"reasoning": "决策理由(必须说明当前持仓/挂单状态以及为什么选择这个操作)",
"risk_analysis": "风险分析"
}
```
## 决策示例
### 示例1有持仓 + 同向信号 - 普通情况HOLD
```
当前状态BTC 做多持仓 @ $95,000盈利+3%
新信号BTC 做多 @ $96,500confidence 75%B级
分析:
- 价格距离 = (96500-95000)/95000 = 1.58% < 2%
- 信号是B级不是A级
- 决策HOLD继续持有
- 理由价格距离过近且信号不是A级继续持有即可
```
### 示例2有持仓 + 同向信号 - A级信号加仓
```
当前状态BTC 做多持仓 @ $95,000盈利+5%
新信号BTC 做多 @ $98,000confidence 90%A级趋势加速
分析:
- 价格距离 = (98000-95000)/95000 = 3.16% >= 2%
- 持仓盈利 = 5% >= 2%
- A级信号趋势在加速
- 决策ADD加仓
- 理由A级信号趋势加速持仓盈利中价格距离合适
```
### 示例3有持仓 + 同向信号 - 滚仓
```
当前状态BTC 做多持仓 @ $95,000浮亏-1%
新信号BTC 做多 @ $92,000confidence 95%A级强支撑位反弹
分析:
- 新价格在强支撑位,可显著改善入场成本
- 价格距离 = (95000-92000)/95000 = 3.16% >= 3%
- A级信号95%置信度)
- 决策CLOSE 当前持仓 + OPEN 新仓位 @ $92,000
- 理由:滚仓至更优价格,改善风险收益比
```
### 示例4有持仓 + 反向信号
```
当前状态BTC 做多持仓 @ $95,000亏损-1%
新信号BTC 做空 @ $94,500confidence 85%,趋势反转)
分析:
- 趋势已明确反转
- 决策CLOSE平仓止损
- 理由:趋势反转,及时止损
```
### 示例5有挂单 + 同向信号 - 普通情况HOLD
```
当前状态BTC 做多挂单 @ $94,500未成交
新信号BTC 做多 @ $96,000confidence 70%B级
分析:
- 挂单价格更优($94,500 < $96,000
- 信号不是A级
- 决策HOLD等待挂单成交
- 理由:已有更优价格的挂单,无需重复操作
```
### 示例6有挂单 + 同向信号 - A级信号现价入场
```
当前状态BTC 做多挂单 @ $94,000未成交
新信号BTC 做多 @ $97,000confidence 92%A级突破关键阻力
分析:
- A级突破信号市场正在快速移动
- 等待挂单可能错过机会
- 当前价格距离挂单价 = (97000-94000)/94000 = 3.19% >= 1.5%
- 决策CANCEL_PENDING + OPEN现价开仓
- 理由A级突破信号等待挂单可能错过机会
```
### 示例7完全无持仓无挂单
```
当前状态:无持仓,无挂单
新信号BTC 做多 @ $95,000confidence 80%uptrend
分析:
- 满足开新仓的所有条件
- 决策OPEN开仓
- 理由:首次入场,信号质量高,趋势向好
```
## 杠杆和风险控制
- **最大杠杆 20 倍**:最大仓位金额 = 账户余额 × 20
- **当前杠杆**:当前杠杆 = 当前持仓价值 / 账户余额
- **可用杠杆空间百分比**(最大仓位金额 - 当前持仓价值) / 最大仓位金额 × 100%
- **可用杠杆空间 >= 3%** 才能开新仓
## 输出格式要求
```json
{
"decision": "OPEN/CLOSE/ADD/REDUCE/CANCEL_PENDING/HOLD",
"action": "buy/sell",
"quantity": 保证金金额USDT,
"entry_price": 入场价格,
"stop_loss": 止损价格,
"take_profit": 止盈价格,
"orders_to_cancel": ["order_id_1"],
"reasoning": "决策理由(必须说明当前持仓/挂单状态以及为什么选择这个操作)",
"risk_analysis": "风险分析"
}
```
## 入场价格选择策略
- 使用信号中的 `entry_price` 作为入场价格
- 如果选择 `limit`(挂单)方式,等待价格达到 `entry_price` 时成交
## 重要原则
1. **仓位管理优先**:先管理现有仓位,再考虑开新仓
2. **避免重复开仓**同一标的同一方向最多1个持仓 + 1个挂单
3. **安全第一**:宁可错过机会,也不要冒过大风险
4. **遵守杠杆限制**:总杠杆永远不超过 20 倍
5. **理性决策**:不要被 FOMO 情绪左右
记住:你是仓位管理者,不是信号执行器。你的首要任务是管理好现有仓位!
"""
def __init__(self):
pass
async def make_decision(self,
market_signal: Dict[str, Any],
positions: List[Dict[str, Any]],
account: Dict[str, Any],
current_price: float = None,
pending_orders: List[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
做出交易决策
Args:
market_signal: 市场信号(来自 MarketSignalAnalyzer
positions: 当前持仓列表
account: 账户状态
current_price: 当前价格(用于判断入场方式)
pending_orders: 未成交的挂单列表
Returns:
交易决策字典
"""
try:
# 1. 准备决策上下文
decision_context = self._prepare_decision_context(
market_signal, positions, account, current_price, pending_orders or []
)
# 2. 构建提示词
prompt = self._build_decision_prompt(decision_context)
# 3. 调用 LLM 做决策
messages = [
{"role": "system", "content": self.TRADING_DECISION_PROMPT},
{"role": "user", "content": prompt}
]
response = await llm_service.achat(messages)
# 4. 解析结果
result = self._parse_decision_response(response, market_signal['symbol'])
# 5. 验证决策安全性
result = self._validate_decision(
result, positions, account,
pending_orders=pending_orders or [],
market_signal=market_signal
)
return result
except Exception as e:
logger.error(f"交易决策失败: {e}")
import traceback
logger.debug(traceback.format_exc())
return self._get_hold_decision(market_signal['symbol'], "决策系统异常")
def _prepare_decision_context(self,
market_signal: Dict[str, Any],
positions: List[Dict[str, Any]],
account: Dict[str, Any],
current_price: float = None,
pending_orders: List[Dict[str, Any]] = None) -> Dict[str, Any]:
"""准备决策上下文"""
context = {
'symbol': market_signal.get('symbol'),
'market_state': market_signal.get('market_state'),
'trend': market_signal.get('trend'),
'trend_direction': market_signal.get('trend_direction'), # 新增:趋势方向
'trend_strength': market_signal.get('trend_strength'), # 新增:趋势强度
'signals': market_signal.get('signals', []),
'key_levels': market_signal.get('key_levels', {}),
'positions': positions,
'pending_orders': pending_orders or [], # 新增:挂单列表
'account': account,
'current_price': current_price
}
# 计算账户状态
balance = float(account.get('current_balance', 0))
total_position_value = float(account.get('total_position_value', 0))
used_margin = float(account.get('used_margin', 0))
# 当前杠杆(全仓模式)
max_leverage = 20
max_position_value = balance * max_leverage # 最大仓位金额
current_leverage = (total_position_value / balance) if balance > 0 else 0
available_position_value = max(0, max_position_value - total_position_value) # 剩余可用仓位金额
available_leverage_percent = (available_position_value / max_position_value * 100) if max_position_value > 0 else 0 # 可用杠杆空间百分比
context['leverage_info'] = {
'balance': balance,
'current_leverage': current_leverage,
'total_position_value': total_position_value,
'max_position_value': max_position_value,
'available_position_value': available_position_value,
'available_leverage_percent': available_leverage_percent,
'max_leverage': max_leverage
}
# 价格距离检查信息(用于 LLM 判断)
context['price_distance_check'] = {
'enabled': True,
'min_distance_percent': 2.0, # 最小价格距离 2%
'no_exception': True # 没有例外情况
}
return context
def _build_decision_prompt(self, context: Dict[str, Any]) -> str:
"""构建决策提示词"""
prompt_parts = []
# ============================================================
# 第一步:仓位管理决策流程摘要(最优先!)
# ============================================================
prompt_parts.append("="*60)
prompt_parts.append("## 🎯 仓位管理决策流程(按顺序执行)")
prompt_parts.append("="*60)
positions = context.get('positions', [])
pending_orders = context.get('pending_orders', [])
signals = context.get('signals', [])
# 分析当前状态
has_positions = len([p for p in positions if p.get('symbol') == context['symbol']]) > 0
has_pending = len([o for o in pending_orders if o.get('symbol') == context['symbol']]) > 0
# 检查是否有强烈信号
strong_signals = [s for s in signals if s.get('confidence', 0) >= 85]
has_strong_signal = len(strong_signals) > 0
if has_positions:
prompt_parts.append("📊 当前状态:**有持仓**")
prompt_parts.append("")
prompt_parts.append("决策优先级:")
prompt_parts.append("1⃣ 首先检查是否需要平仓/减仓(信号反向或趋势减弱)")
if has_strong_signal:
prompt_parts.append("2⃣ 然后检查是否需要加仓/滚仓(**有A级信号**,价格距离 >= 2%")
prompt_parts.append(" ⭐ A级信号confidence >= 85可考虑加仓或滚仓")
else:
prompt_parts.append("2⃣ 然后检查是否需要加仓(价格距离 >= 2%,盈利 >= 2%")
prompt_parts.append(" ⚠️ 当前信号不是A级不建议加仓")
prompt_parts.append("3⃣ ❌ 不要开新仓(已有持仓时优先管理现有仓位)")
prompt_parts.append("")
prompt_parts.append("⚠️ 严禁重复开仓!在有持仓时只选择 HOLD/ADD/CLOSE/REDUCE")
elif has_pending:
prompt_parts.append("📝 当前状态:**有挂单,无持仓**")
prompt_parts.append("")
prompt_parts.append("决策优先级:")
prompt_parts.append("1⃣ 首先检查是否需要取消挂单(信号反向或价格不优)")
if has_strong_signal:
prompt_parts.append("2⃣ 然后检查是否需要调整挂单或现价入场(**有A级信号**")
prompt_parts.append(" ⭐ A级信号confidence >= 85可考虑取消挂单现价入场")
else:
prompt_parts.append("2⃣ 然后检查是否需要调整挂单价格")
prompt_parts.append(" ⚠️ 当前信号不是A级不建议调整挂单")
prompt_parts.append("3⃣ ❌ 不要开新仓(已有挂单时等待成交或调整)")
prompt_parts.append("")
prompt_parts.append("⚠️ 严禁重复挂单!在有挂单时只选择 HOLD/CANCEL_PENDING")
else:
prompt_parts.append("✨ 当前状态:**完全无持仓无挂单**")
prompt_parts.append("")
prompt_parts.append("决策优先级:")
prompt_parts.append("1⃣ 这时才考虑开新仓OPEN")
prompt_parts.append("2⃣ 必须满足所有开仓条件(信号质量、杠杆空间、价格合理)")
if not has_strong_signal:
prompt_parts.append(" ⚠️ 当前信号不是A级建议轻仓")
prompt_parts.append("")
prompt_parts.append("✅ 可以开新仓,但必须谨慎评估")
prompt_parts.append("")
prompt_parts.append("="*60)
prompt_parts.append("")
# 市场信号
prompt_parts.append(f"## 市场信号")
prompt_parts.append(f"交易对: {context['symbol']}")
prompt_parts.append(f"市场状态: {context.get('market_state')}")
# 趋势信息(新增)
trend_direction = context.get('trend_direction', 'neutral')
trend_strength = context.get('trend_strength', 'weak')
direction_text = {'uptrend': '📈 上升趋势', 'downtrend': '📉 下降趋势', 'neutral': ' 震荡'}.get(trend_direction, trend_direction)
strength_text = {'strong': '强势', 'medium': '中等', 'weak': '弱势'}.get(trend_strength, trend_strength)
prompt_parts.append(f"趋势: {direction_text} ({strength_text})")
# 当前价格(如果有)
current_price = context.get('current_price')
if current_price:
prompt_parts.append(f"当前价格: ${current_price:,.2f}")
# 信号列表
signals = context.get('signals', [])
if signals:
prompt_parts.append(f"\n## 信号列表")
for i, sig in enumerate(signals, 1):
# timeframe 是 short_term/medium_term/long_term
timeframe = sig.get('timeframe', 'N/A')
action = sig.get('action', 'N/A')
prompt_parts.append(f"{i}. {timeframe} | {action}")
prompt_parts.append(f" 信心度: {sig.get('confidence', 0)}")
# 添加入场价格信息
entry_price = sig.get('entry_price')
if entry_price:
prompt_parts.append(f" 建议入场价: ${entry_price:,.2f}")
prompt_parts.append(f" 理由: {sig.get('reasoning', 'N/A')}")
# 趋势一致性检查(新增)
trend_direction = context.get('trend_direction', 'neutral')
trend_strength = context.get('trend_strength', 'weak')
prompt_parts.append(f"\n## 🚨 趋势一致性检查(第一优先级)")
for i, sig in enumerate(signals, 1):
action = sig.get('action', 'hold')
is_aligned = (trend_direction == 'uptrend' and action == 'buy') or \
(trend_direction == 'downtrend' and action == 'sell') or \
(trend_direction == 'neutral')
if is_aligned:
prompt_parts.append(f"✅ 信号#{i} ({action}) 与趋势 ({trend_direction}) 一致 → 可正常开仓")
else:
# 逆势信号
if trend_strength == 'strong':
prompt_parts.append(f"❌ 信号#{i} ({action}) 与强趋势 ({trend_direction}) 相反 → **严禁逆势,返回 HOLD**")
elif trend_strength == 'medium':
confidence = sig.get('confidence', 0)
if confidence >= 85:
prompt_parts.append(f"⚠️ 信号#{i} ({action}) 与中等趋势相反,但 confidence={confidence}>=85 → 可谨慎 micro 仓位")
else:
prompt_parts.append(f"❌ 信号#{i} ({action}) 与中等趋势相反confidence不足 → 返回 HOLD")
else: # weak or neutral
confidence = sig.get('confidence', 0)
if confidence >= 85:
prompt_parts.append(f"⚠️ 信号#{i} ({action}) 与弱趋势相反,但 confidence={confidence}>=85 → 可 micro 仓位")
else:
prompt_parts.append(f"❌ 信号#{i} ({action}) 与弱趋势相反confidence不足 → 返回 HOLD")
# 关键价位
key_levels = context.get('key_levels', {})
if key_levels:
prompt_parts.append(f"\n## 关键价位")
if key_levels.get('support'):
# 提取数字并格式化
import re
def extract_num(val):
if isinstance(val, (int, float)):
return float(val)
if isinstance(val, str):
match = re.search(r'[\d,]+\.?\d*', val.replace(',', ''))
if match:
return float(match.group())
return None
supports = [extract_num(s) for s in key_levels['support'][:3]]
supports_str = ', '.join([f"${s:,.2f}" for s in supports if s is not None])
prompt_parts.append(f"支撑位: {supports_str}")
if key_levels.get('resistance'):
import re
def extract_num(val):
if isinstance(val, (int, float)):
return float(val)
if isinstance(val, str):
match = re.search(r'[\d,]+\.?\d*', val.replace(',', ''))
if match:
return float(match.group())
return None
resistances = [extract_num(r) for r in key_levels['resistance'][:3]]
resistances_str = ', '.join([f"${r:,.2f}" for r in resistances if r is not None])
prompt_parts.append(f"阻力位: {resistances_str}")
# 当前持仓
positions = context.get('positions', [])
prompt_parts.append(f"\n## 当前持仓")
if positions:
for pos in positions:
if pos.get('holding', 0) > 0:
prompt_parts.append(f"- {pos.get('symbol')}: {pos.get('side')} {pos.get('holding')} USDT")
prompt_parts.append(f" 开仓价: ${pos.get('entry_price')}")
prompt_parts.append(f" 止损: ${pos.get('stop_loss')}")
prompt_parts.append(f" 止盈: ${pos.get('take_profit')}")
else:
prompt_parts.append("无持仓")
# 当前挂单
pending_orders = context.get('pending_orders', [])
prompt_parts.append(f"\n## 当前挂单(仅 {context['symbol']} 的挂单)")
if pending_orders:
prompt_parts.append(f"⚠️ 重要:以下挂单都属于当前交易对 {context['symbol']}取消订单时只能选择这些订单ID")
prompt_parts.append(f"⚠️ 取消规则:做空信号时只能取消做多(🟢long)挂单,做多信号时只能取消做空(🔴short)挂单")
# 分类统计挂单方向
long_orders = [o for o in pending_orders if o.get('side') == 'long']
short_orders = [o for o in pending_orders if o.get('side') == 'short']
# 如果只有同向挂单明确提示LLM
signals = context.get('signals', [])
if signals:
main_action = signals[0].get('action', 'hold') if signals else 'hold'
if main_action == 'sell' and not long_orders:
prompt_parts.append(f"📌 注意:当前只有做空挂单,与做空信号同向,无需取消!")
elif main_action == 'buy' and not short_orders:
prompt_parts.append(f"📌 注意:当前只有做多挂单,与做多信号同向,无需取消!")
for order in pending_orders:
side_icon = "🟢" if order.get('side') == 'long' else "🔴"
entry_type = "现价单" if order.get('entry_type') == 'market' else "挂单"
side_text = "做多" if order.get('side') == 'long' else "做空"
prompt_parts.append(f"- {side_icon} {order.get('symbol')}: {side_text}({order.get('side')}) | {entry_type}")
prompt_parts.append(f" 挂单价: ${order.get('entry_price')} | 数量: {order.get('quantity')} USDT")
prompt_parts.append(f" 订单ID: {order.get('order_id')}")
else:
prompt_parts.append("无挂单")
# 账户状态
account = context.get('account', {})
lev_info = context.get('leverage_info', {})
prompt_parts.append(f"\n## 账户状态")
prompt_parts.append(f"余额: ${account.get('current_balance', 0):.2f}")
prompt_parts.append(f"可用: ${account.get('available', 0):.2f}")
prompt_parts.append(f"已用保证金: ${account.get('used_margin', 0):.2f}")
prompt_parts.append(f"持仓价值: ${account.get('total_position_value', 0):.2f}")
prompt_parts.append(f"\n## 杠杆信息")
prompt_parts.append(f"当前杠杆: {lev_info.get('current_leverage', 0):.1f}x")
prompt_parts.append(f"最大仓位金额: ${lev_info.get('max_position_value', 0):,.2f}")
prompt_parts.append(f"可用仓位金额: ${lev_info.get('available_position_value', 0):,.2f}")
prompt_parts.append(f"可用杠杆空间: {lev_info.get('available_leverage_percent', 0):.1f}%")
prompt_parts.append(f"最大杠杆限制: {lev_info.get('max_leverage', 20)}x")
# 价格距离检查规则
price_check = context.get('price_distance_check', {})
if price_check.get('enabled'):
min_distance = price_check.get('min_distance_percent', 2)
prompt_parts.append(f"\n## 价格距离限制(必须遵守)")
prompt_parts.append(f"⚠️ 重要:如果有相同方向的持仓/挂单,价格距离必须 >= {min_distance}%")
prompt_parts.append(f"- 低于此距离不开新仓,避免风险过度集中")
prompt_parts.append(f"- 此规则**没有例外**,无论信号等级多高都必须遵守")
# 计算并显示当前价格距离
current_price = context.get('current_price')
signals = context.get('signals', [])
positions = context.get('positions', [])
pending_orders = context.get('pending_orders', [])
if signals and current_price:
for sig in signals:
sig_action = sig.get('action')
sig_entry = sig.get('entry_price')
if sig_action and sig_entry:
try:
sig_entry = float(sig_entry)
prompt_parts.append(f"\n当前信号 {sig_action} @ ${sig_entry:,.2f} 的价格距离检查:")
# 检查持仓
for pos in positions:
if pos.get('symbol') == context['symbol']:
pos_side = pos.get('side')
if (sig_action == 'buy' and pos_side == 'long') or (sig_action == 'sell' and pos_side == 'short'):
pos_entry = float(pos.get('entry_price', 0))
distance = abs(sig_entry - pos_entry) / pos_entry * 100
status = "✅ 通过" if distance >= min_distance else f"❌ 拒绝 (距离 {distance:.2f}% < {min_distance}%)"
prompt_parts.append(f" - 持仓 {pos_side} @ ${pos_entry:,.2f}: 距离 {distance:.2f}% {status}")
# 检查挂单
for order in pending_orders:
if order.get('symbol') == context['symbol']:
order_side = order.get('side')
if (sig_action == 'buy' and order_side == 'long') or (sig_action == 'sell' and order_side == 'short'):
order_entry = float(order.get('entry_price', 0))
distance = abs(sig_entry - order_entry) / order_entry * 100
status = "✅ 通过" if distance >= min_distance else f"❌ 拒绝 (距离 {distance:.2f}% < {min_distance}%)"
prompt_parts.append(f" - 挂单 {order_side} @ ${order_entry:,.2f}: 距离 {distance:.2f}% {status}")
except (ValueError, TypeError):
pass
# 盈亏比检查规则(新增)
prompt_parts.append(f"\n## 盈亏比检查(日内交易铁律)")
prompt_parts.append(f"⚠️ 所有交易必须满足盈亏比 >= 1:2优选 1:3")
prompt_parts.append(f"")
prompt_parts.append(f"盈亏比计算公式:")
prompt_parts.append(f" 做多盈亏比 = (止盈价 - 入场价) / (入场价 - 止损价)")
prompt_parts.append(f" 做空盈亏比 = (入场价 - 止盈价) / (止损价 - 入场价)")
prompt_parts.append(f"")
prompt_parts.append(f"⚠️ 如果盈亏比 < 1:2**不要开仓(返回 HOLD**")
# 计算并显示盈亏比
signals = context.get('signals', [])
if signals:
for sig in signals:
action = sig.get('action')
entry = sig.get('entry_price')
stop_loss = sig.get('stop_loss')
take_profit = sig.get('take_profit')
if action and entry and stop_loss and take_profit:
try:
entry = float(entry)
stop_loss = float(stop_loss)
take_profit = float(take_profit)
if action == 'buy':
risk_ratio = entry - stop_loss
reward_ratio = take_profit - entry
elif action == 'sell':
risk_ratio = stop_loss - entry
reward_ratio = entry - take_profit
else:
continue
if risk_ratio > 0 and reward_ratio > 0:
rr_ratio = reward_ratio / risk_ratio
rr_percent = (risk_ratio / entry) * 100
if rr_ratio >= 2.0:
status = f"✅ 通过 (1:{rr_ratio:.1f})"
else:
status = f"❌ 拒绝 (1:{rr_ratio:.1f} < 1:2)"
prompt_parts.append(f"\n信号 {action} @ ${entry:,.2f}:")
prompt_parts.append(f" - 止损: ${stop_loss:,.2f} (风险 {risk_ratio:.0f} / {rr_percent:.1f}%)")
prompt_parts.append(f" - 止盈: ${take_profit:,.2f} (盈利 {reward_ratio:.0f})")
prompt_parts.append(f" - 盈亏比: 1:{rr_ratio:.2f} {status}")
except (ValueError, TypeError, ZeroDivisionError):
pass
prompt_parts.append(f"\n请根据以上信息,做出交易决策。")
return "\n".join(prompt_parts)
def _parse_decision_response(self, response: str, symbol: str) -> Dict[str, Any]:
"""解析决策响应"""
try:
import re
# 尝试提取 JSON
json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response)
if json_match:
json_str = json_match.group(1)
else:
json_match = re.search(r'\{[\s\S]*\}', response)
if json_match:
json_str = json_match.group(0)
else:
raise ValueError("无法找到 JSON 响应")
# 清理 JSON 字符串
json_str = self._clean_json_string(json_str)
result = json.loads(json_str)
# 清理价格字段 - 转换为 float
result = self._clean_price_fields(result)
# 添加元数据
result['symbol'] = symbol
result['timestamp'] = datetime.now().isoformat()
result['raw_response'] = response
logger.info(f"✅ 交易决策完成: {symbol} | {result.get('decision', 'HOLD')}")
return result
except Exception as e:
logger.warning(f"解析决策响应失败: {e}")
logger.warning(f"原始响应: {response[:1000]}...") # 打印前1000字符
return self._get_hold_decision(symbol, "解析失败,默认观望")
def _clean_price_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""清理价格字段,转换为 float"""
def clean_price(price_value):
if price_value is None:
return None
if isinstance(price_value, (int, float)):
return float(price_value)
if isinstance(price_value, str):
# 移除 $ 符号和逗号
cleaned = price_value.replace('$', '').replace(',', '').strip()
if cleaned:
try:
return float(cleaned)
except ValueError:
return None
return None
# 清理顶层价格字段
price_fields = ['stop_loss', 'take_profit', 'quantity']
for field in price_fields:
if field in data:
data[field] = clean_price(data[field])
# 验证止损止盈价格的合理性
data = self._validate_price_fields(data)
return data
def _validate_price_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""验证止损止盈价格的合理性,拒绝明显错误的值"""
entry = data.get('entry_price')
stop_loss = data.get('stop_loss')
take_profit = data.get('take_profit')
action = data.get('decision', '') # OPEN/CLOSE/HOLD
if not entry or entry <= 0:
return data
# 判断是做多还是做空
is_long = action == 'OPEN' and data.get('action') == 'buy'
is_short = action == 'OPEN' and data.get('action') == 'sell'
# 检查止损价格是否合理(偏离入场价不超过 50%
MAX_REASONABLE_DEVIATION = 0.50 # 50%
if stop_loss is not None:
deviation = abs(stop_loss - entry) / entry
# 如果止损价格偏离入场价超过 50%,认为是错误的
if deviation > MAX_REASONABLE_DEVIATION:
logger.warning(f"⚠️ 止损价格不合理: entry={entry}, stop_loss={stop_loss}, 偏离={deviation*100:.1f}%,已忽略")
data['stop_loss'] = None
else:
# 做多:止损应该低于入场价
if is_long and stop_loss >= entry:
logger.warning(f"⚠️ 做多止损错误: entry={entry}, stop_loss={stop_loss} 应该 < entry已忽略")
data['stop_loss'] = None
# 做空:止损应该高于入场价
elif is_short and stop_loss <= entry:
logger.warning(f"⚠️ 做空止损错误: entry={entry}, stop_loss={stop_loss} 应该 > entry已忽略")
data['stop_loss'] = None
if take_profit is not None:
deviation = abs(take_profit - entry) / entry
# 如果止盈价格偏离入场价超过 50%,认为是错误的
if deviation > MAX_REASONABLE_DEVIATION:
logger.warning(f"⚠️ 止盈价格不合理: entry={entry}, take_profit={take_profit}, 偏离={deviation*100:.1f}%,已忽略")
data['take_profit'] = None
else:
# 做多:止盈应该高于入场价
if is_long and take_profit <= entry:
logger.warning(f"⚠️ 做多止盈错误: entry={entry}, take_profit={take_profit} 应该 > entry已忽略")
data['take_profit'] = None
# 做空:止盈应该低于入场价
elif is_short and take_profit >= entry:
logger.warning(f"⚠️ 做空止盈错误: entry={entry}, take_profit={take_profit} 应该 < entry已忽略")
data['take_profit'] = None
return data
def _clean_json_string(self, json_str: str) -> str:
"""清理 JSON 字符串,移除可能导致解析错误的内容"""
import re
# 移除单行注释 // ...
json_str = re.sub(r'//.*?(?=\n|$)', '', json_str)
# 移除多行注释 /* ... */
json_str = re.sub(r'/\*[\s\S]*?\*/', '', json_str)
# 移除尾随逗号(例如 {"a": 1,} -> {"a": 1}
json_str = re.sub(r',\s*([}\]])', r'\1', json_str)
return json_str
def _validate_decision(self, decision: Dict[str, Any],
positions: List[Dict[str, Any]],
account: Dict[str, Any],
pending_orders: List[Dict[str, Any]] = None,
market_signal: Dict[str, Any] = None) -> Dict[str, Any]:
"""验证决策安全性"""
# 检查杠杆限制
if decision.get('decision') in ['OPEN', 'ADD']:
balance = float(account.get('current_balance', 0))
total_position_value = float(account.get('total_position_value', 0))
max_leverage = 20
max_position_value = balance * max_leverage
# quantity 是保证金金额,需要乘以杠杆得到持仓价值
margin = float(decision.get('quantity', 0))
position_value = margin * max_leverage # 使用最大杠杆计算持仓价值
new_total_value = total_position_value + position_value
if new_total_value > max_position_value:
logger.warning(f"⚠️ 决策被拒绝: 超过最大仓位金额 (保证金 ${margin:.2f} → 持仓价值 ${position_value:.2f}, 总计 ${new_total_value:,.2f} > ${max_position_value:,.2f})")
return self._get_hold_decision(
decision['symbol'],
f"超过最大仓位金额 (保证金 ${margin:.2f} → 持仓价值 ${position_value:.2f}, 总计 ${new_total_value:,.2f} > ${max_position_value:,.2f})"
)
# 盈亏比检查:所有交易必须满足盈亏比 >= 1:2
action = decision.get('action', '')
entry_price = decision.get('entry_price')
stop_loss = decision.get('stop_loss')
take_profit = decision.get('take_profit')
if action and entry_price and stop_loss and take_profit:
try:
entry_price = float(entry_price)
stop_loss = float(stop_loss)
take_profit = float(take_profit)
# 计算盈亏比
if action == 'buy':
risk = entry_price - stop_loss
reward = take_profit - entry_price
elif action == 'sell':
risk = stop_loss - entry_price
reward = entry_price - take_profit
else:
risk = 0
reward = 0
# 验证价格方向正确性
if action == 'buy':
if stop_loss >= entry_price:
logger.warning(f"⚠️ 决策被拒绝: 做多止损错误 (entry={entry_price}, stop_loss={stop_loss} 应该 < entry)")
return self._get_hold_decision(decision['symbol'], f"做多止损价格错误")
if take_profit <= entry_price:
logger.warning(f"⚠️ 决策被拒绝: 做多止盈错误 (entry={entry_price}, take_profit={take_profit} 应该 > entry)")
return self._get_hold_decision(decision['symbol'], f"做多止盈价格错误")
elif action == 'sell':
if stop_loss <= entry_price:
logger.warning(f"⚠️ 决策被拒绝: 做空止损错误 (entry={entry_price}, stop_loss={stop_loss} 应该 > entry)")
return self._get_hold_decision(decision['symbol'], f"做空止损价格错误")
if take_profit >= entry_price:
logger.warning(f"⚠️ 决策被拒绝: 做空止盈错误 (entry={entry_price}, take_profit={take_profit} 应该 < entry)")
return self._get_hold_decision(decision['symbol'], f"做空止盈价格错误")
# 检查盈亏比
if risk > 0 and reward > 0:
rr_ratio = reward / risk
min_rr_ratio = 2.0 # 最小盈亏比 1:2
if rr_ratio < min_rr_ratio:
logger.warning(f"⚠️ 决策被拒绝: 盈亏比不足 (1:{rr_ratio:.2f} < 1:{min_rr_ratio:.0f})")
logger.warning(f" entry={entry_price}, stop_loss={stop_loss}, take_profit={take_profit}")
logger.warning(f" 风险={risk:.0f}, 盈利={reward:.0f}, 盈亏比=1:{rr_ratio:.2f}")
return self._get_hold_decision(
decision['symbol'],
f"盈亏比不足 (1:{rr_ratio:.2f} < 1:{min_rr_ratio:.0f})"
)
except (ValueError, TypeError, ZeroDivisionError) as e:
logger.warning(f"盈亏比检查失败: {e}")
# 价格距离检查:相同方向相同标的的挂单,价格距离 < 2% 时不加仓/开仓
action = decision.get('action', '')
new_entry_price = decision.get('entry_price')
if action and new_entry_price:
try:
new_entry_price = float(new_entry_price)
min_distance_percent = 2.0
# 检查持仓
for pos in positions or []:
if pos.get('symbol') == decision.get('symbol'):
pos_side = pos.get('side', '') # 'long' or 'short'
pos_entry = float(pos.get('entry_price', 0))
# 相同方向的持仓
if (action == 'buy' and pos_side == 'long') or (action == 'sell' and pos_side == 'short'):
distance_percent = abs(new_entry_price - pos_entry) / pos_entry * 100
if distance_percent < min_distance_percent:
logger.warning(f"⚠️ 决策被拒绝: 价格距离过近 (新价格 ${new_entry_price:,.2f} vs 持仓 ${pos_entry:,.2f}, 距离 {distance_percent:.2f}% < {min_distance_percent}%)")
return self._get_hold_decision(
decision['symbol'],
f"价格距离持仓过近 (新价格 ${new_entry_price:,.2f} vs 持仓 ${pos_entry:,.2f}, 距离 {distance_percent:.2f}% < {min_distance_percent}%)"
)
# 检查挂单
for order in pending_orders or []:
if order.get('symbol') == decision.get('symbol'):
order_side = order.get('side', '')
order_entry = float(order.get('entry_price', 0))
# 相同方向的挂单
if (action == 'buy' and order_side == 'long') or (action == 'sell' and order_side == 'short'):
distance_percent = abs(new_entry_price - order_entry) / order_entry * 100
if distance_percent < min_distance_percent:
logger.warning(f"⚠️ 决策被拒绝: 价格距离过近 (新价格 ${new_entry_price:,.2f} vs 挂单 ${order_entry:,.2f}, 距离 {distance_percent:.2f}% < {min_distance_percent}%)")
return self._get_hold_decision(
decision['symbol'],
f"价格距离挂单过近 (新价格 ${new_entry_price:,.2f} vs 挂单 ${order_entry:,.2f}, 距离 {distance_percent:.2f}% < {min_distance_percent}%)"
)
except (ValueError, TypeError) as e:
logger.warning(f"价格距离检查失败: {e}")
return decision
def _get_hold_decision(self, symbol: str, reason: str = "") -> Dict[str, Any]:
"""返回观望决策"""
return {
'decision': 'HOLD',
'symbol': symbol,
'action': 'hold',
'reasoning': f'观望: {reason}',
'timestamp': datetime.now().isoformat()
}