stock-ai-agent/backend/app/crypto_agent/trading_decision_maker.py
2026-03-27 00:45:37 +08:00

1292 lines
58 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. **RSI 背离**:价格创新高/低但 RSI 不创新高/低权重2
2. **MACD 柱状图缩短**动能衰竭信号权重1
3. **MACD 金叉/死叉**趋势反转信号权重1
4. **量价背离**价格上涨但成交量下降权重1
5. **关键K线形态**吞没、锤子线、十字星权重1-2
6. **多周期趋势不一致**小周期反转但大周期未反应权重1
### 🔴 当检测到反转信号时(必须遵守!):
**1. 如果有同方向持仓 → 强制平仓**
- 检测到看跌反转 + 有做多持仓 → **CLOSE立即平仓**
- 检测到看涨反转 + 有做空持仓 → **CLOSE立即平仓**
- 不要等待反弹/回调,反转可能很快发生
**2. 严禁继续原方向开新仓**
- 检测到反转信号后 → **停止原方向任何新操作**
- 之前做多,现在检测到看跌反转 → **严禁开新多单**
- 之前做空,现在检测到看涨反转 → **严禁开新空单**
**3. 可以考虑反手操作(仅当反转置信度 ≥ 70%**
- FLIP_POSITION平掉旧仓位 + 开立新方向仓位
- 或者先平仓观望,等待反转确认后再入场
**4. 如果不确定 → 先平仓观望**
- 宁可错过机会,也不要被套在反向位置
## 🌅 趋势阶段处理(避免晚期被套)
### 系统会判断趋势处于哪个阶段:
- **早期**:刚突破关键位,均线刚开始排列,动能开始释放
- **中期**:均线排列稳定,价格沿趋势移动,量能健康
- **晚期**价格过度延伸RSI极端区量价背离
### 不同阶段的处理规则:
**✅ 早期阶段(可积极入场):**
- 可以顺势轻仓入场
- 设置止损后可持有更长时间
- 目标可看更大空间3-5%
**✅ 中期阶段(稳健持仓):**
- 等待回调/反弹入场
- 顺势持仓,让利润奔跑
- 不要被小波动洗出
**🔴 晚期阶段(强制谨慎!):**
- **严禁追涨/追空开新仓**
- **现有盈利持仓建议逐步止盈**
- **等待明确反转信号后再决策**
- 宁可错过最后一段利润,也不要被套
### ⚠️ 铁律:趋势晚期 + 检测到反转信号 = 立即平仓
- 晚期阶段本身风险就大
- 如果再检测到反转信号 → 必须立即平仓
- 不要幻想"最后一段利润"
### 🚨 盈亏比铁律(违反即拒绝)
**所有交易必须满足盈亏比 ≥ 1:1.2,回调入场≥ 1:1.5**
```
盈亏比 = (目标盈利 - 入场价) / (入场价 - 止损价)
做多:盈亏比 = (止盈价 - 入场价) / (入场价 - 止损价)
做空:盈亏比 = (入场价 - 止盈价) / (止损价 - 入场价)
示例(突破追涨):
- BTC 做多:入场 65000止损 64500-0.77%),止盈 65970+1.5%
- 盈亏比 = (65970 - 65000) / (65000 - 64500) = 970 / 500 ≈ 1.94 ✅
示例(回调做多):
- BTC 做多:入场 64800回调止损 64300-0.77%),止盈 65800+1.54%
- 盈亏比 = (65800 - 64800) / (64800 - 64300) = 1000 / 500 = 2.0 ✅
如果盈亏比 < 1:1.2,绝对不要开仓!
```
### 日内交易参数
| 参数 | 设定值 |
|------|--------|
| 止损幅度 | 优先 1.5×ATR(30m),参考范围 0.8-2% |
| 目标盈利 | 2-3%(日内快速获利,达到即走) |
| 盈亏比要求 | ≥ 1:1.2回调入场≥1:1.5 |
| 单笔持仓时长 | 不超过4小时达到目标立即平仓 |
| 仓位大小 | 轻仓为主light/micro禁止heavy |
## 决策流程(必须按顺序执行)
### 第一步:检查现有仓位和挂单(最重要!)
在考虑任何新操作之前,先分析当前状态:
1. **是否有相同方向的持仓?**
- 如果有 → 考虑是继续持有、加仓、还是减仓
- 如果没有 → 进入下一步检查
2. **是否有反向挂单需要取消?**
- 如果新信号是 buy → 检查是否有 sell 挂单需要取消
- 如果新信号是 sell → 检查是否有 buy 挂单需要取消
3. **是否有同向挂单?**
- 挂单价格是否合理?是否需要调整?
- 是否距离新信号价格太近(< 2%
### 第二步:根据现有状态决策
#### 情况A有相同方向持仓 + 新信号同向
**默认选择HOLD继续持有**
**只有在信号非常强烈时才考虑以下操作:**
**1. 加仓ADD** - 必须同时满足:
- ✅ 新信号是 **A级**confidence >= 90
- ✅ 当前持仓盈利 >= 1.5%(但 < 2.5%,达到目标应该平仓)
- ✅ 新信号价格距离持仓价格 >= 1.5%
- ✅ 趋势在加强(突破加速,不是延续)
- ✅ 有足够的可用杠杆空间
- ❌ **日内交易达到目标盈利2-3%)后立即平仓,不考虑加仓**
**2. 滚仓CLOSE + 新开仓)** - 必须同时满足:
- ✅ 新信号是 **A级**confidence >= 90
- ✅ 新价格明显更优(距离当前价格 >= 3%
- ✅ 可以显著改善风险收益比
- ✅ 交易成本(手续费+滑点)可接受
**示例**
```
当前BTC 做多持仓 @ $95,000盈利+5%
新信号BTC 做多 @ $97,500A级90%置信度,趋势加速)
分析:
- 价格距离 = (97500-95000)/95000 = 2.63% >= 1.5%
- 持仓盈利 = 5% >= 1.5%(但 < 2.5%,未达平仓线)
- A级信号趋势在加速
- 决策ADD加仓
- 理由A级信号趋势加速持仓盈利中价格距离合适
```
**示例2 - 滚仓**
```
当前BTC 做多持仓 @ $95,000浮亏-1%
新信号BTC 做多 @ $92,000A级92%置信度,强支撑位)
分析:
- 价格距离 = (95000-92000)/95000 = 3.16% >= 3%
- 新价格在强支撑位,可改善入场成本
- A级信号92%置信度)
- 决策CLOSE 当前持仓 + OPEN 新仓位
- 理由:滚仓至更优价格,改善风险收益比
```
**❌ 严禁**
- 价格距离 < 2% 时加仓
- 持仓亏损时加仓(摊平成本是坏习惯)
- 信号不是A级时加仓
- 信号不是A级时滚仓
#### 情况B有相同方向持仓 + 新信号反向
**优先选择**
1. **CLOSE平仓** - 如果趋势反转明确
2. **REDUCE减仓** - 如果趋势不明但需降低风险
3. **HOLD观望** - 如果反转信号不强
**特殊情况:检测到多级别反转信号(优先级最高!)**
- 如果信号中包含 **"🔄 强反转"** 或 **"⚡ 趋势启动"** 标记
- 说明小级别已反转但大级别滞后,这是**提前布局的机会**
- **优先选择FLIP_POSITION反手操作**
- 平掉当前持仓
- 开立新方向仓位(如果信号质量足够)
- 理由:抓住小级别反转机会,避免等待大级别确认而错失最佳点位
#### 情况C无持仓 + 有同向挂单
**优先选择OPEN新增挂单- 金字塔式布局**
**核心原则:允许合理的多挂单策略**
- 同方向可以最多有3个挂单金字塔布局
- 新挂单价格与现有挂单价格差异 > 1%
- 不同价格位分散风险,提高成交概率
**允许新增挂单的条件**
- ✅ 当前同向挂单数量 < 3个
- ✅ 新信号入场价与所有现有挂单价格差异 > 1%
- ✅ 新信号置信度 >= 60C级以上
- ✅ 价格没有在快速加速移动
**示例1新增第2个挂单**
```
当前BTC 做多挂单1 @ $94,000
新信号BTC 做多 @ $92,700B级75%置信度)
分析:
- 价格差异:(94000-92700)/94000 = 1.38% > 1%
- 挂单数量1个 < 3个
- 决策OPEN新增挂单2 @ $92,700
- 理由:金字塔布局,在不同价位布置挂单
```
**示例2新增第3个挂单**
```
当前BTC 做多挂单1 @ $94,000挂单2 @ $92,700
新信号BTC 做多 @ $91,500B级70%置信度)
分析:
- 新价格与挂单2差异(92700-91500)/92700 = 1.29% > 1%
- 挂单数量2个 < 3个
- 决策OPEN新增挂单3 @ $91,500
- 理由:金字塔布局,继续分散挂单位置
```
**示例3达到3个挂单上限**
```
当前BTC 做多挂单1 @ $94,000挂单2 @ $92,700挂单3 @ $91,500
新信号BTC 做多 @ $90,800B级70%置信度)
分析:
- 已有3个挂单达到上限
- 决策HOLD
- 理由同向挂单已达3个上限不再新增
```
**示例4价格差异太小**
```
当前BTC 做多挂单1 @ $94,000
新信号BTC 做多 @ $93,200价格差异仅0.85%
分析:
- 价格差异:(94000-93200)/94000 = 0.85% < 1%
- 太接近现有挂单,没有意义
- 决策HOLD
- 理由:新价格与现有挂单太近,不新增
```
**⚠️ 例外情况(保持 HOLD**
- 价格正在快速加速移动5m 连续大阳/阴线)
- 新信号置信度 < 60D级信号质量太低
- 新信号入场价距离当前价格 >= 2%(追涨杀跌风险)
- 新信号是 market 入场(改成 market 去追)
- 价格差异 < 1%(太接近现有挂单)
**❌ 严禁**
- 同向挂单数量 >= 3个时继续新增
- 价格差异 < 1% 时新增挂单
- 取消挂单后市价追涨/杀跌
#### 情况D无持仓 + 有反向挂单
**优先选择**
1. **CANCEL_PENDING取消反向挂单**
2. 然后根据新信号决定是否开新仓
#### 情况E完全无持仓无挂单
**这时才考虑开新仓OPEN**
### 第三步:开新仓的规则
**金字塔式挂单策略**
- 无持仓无挂单:可以开新仓
- 无持仓 + 同向挂单 < 3个可以新增挂单价格差异 > 1%
- 无持仓 + 同向挂单 >= 3个不再新增
- 有持仓 + 无同向挂单:可以考虑加仓
- 有持仓 + 有同向挂单:优先持仓,挂单次之
**开新仓条件**
- 信号质量足够高confidence >= 60
- 可用杠杆空间充足
- 价格和止损合理
- 不在价格加速移动中
## 🚨 铁律(违反即拒绝)
### 1. 金字塔挂单规则(同方向)
- **最多3个挂单**同一标的同一方向最多允许3个挂单
- **价格差异 > 1%**:新挂单与现有挂单价格差异必须 > 1%
- **挂单优先**:优先使用 limit 挂单,慎用 market 市价
- **不要重复**:价格差异 < 1% 时不要新增挂单
- **有持仓时**:优先考虑持仓管理,挂单次之
- **价格距离 < 2% 时不加仓**:与持仓价格太近时不加仓
### 2. 趋势与信号一致性
| 当前趋势 | 信号方向 | 允许操作 |
|---------|---------|---------|
| `uptrend` (上升) | buy (做多) | ✅ 允许 |
| `uptrend` (上升) | sell (做空) | ❌ 禁止(除非有多重反转信号 + confidence >= 90 |
| `downtrend` (下降) | sell (做空) | ✅ 允许 |
| `downtrend` (下降) | buy (做多) | ❌ 禁止(除非有多重反转信号 + confidence >= 90 |
| `neutral` (震荡) | buy/sell | ✅ 允许但轻仓 |
### 3. 取消挂单规则
- **只能取消反向挂单**buy信号取消sell挂单sell信号取消buy挂单
- **绝不取消同向挂单**buy信号不应取消buy挂单
- **绝不取消挂单去市价追涨**:这是持续止损的主要原因
- **只能取消当前交易对的挂单**:不要取消其他交易对的订单
### 4. 价格加速检测规则(防止追涨杀跌)
**以下情况强制 HOLD禁止任何操作**
- ❌ 信号入场价距离当前价格 >= 2%(价格正在快速移动)
- ❌ 15m RSI > 70或 < 30极端区间
- ❌ 价格偏离 EMA5 > 1.5%(过度延伸)
- ❌ 5m 连续2根以上大阳/阴线(加速中)
**价格加速时的操作原则**
- ✅ 有持仓:继续持有,考虑止盈
- ✅ 有挂单:等待成交,不要调整
- ❌ 无持仓无挂单:强制 HOLD不要追涨杀跌
## 仓位大小规则(日内交易保守策略)
### 信号等级决定仓位上限
- **A级85-100分**medium/light/micro 可选
- **B级60-84分**:只能 light/micro
- **C级40-59分**:只能 micro
- **D级<40分**:不开仓
### 趋势强度调整仓位
| 趋势 | 顺势仓位 | 逆势仓位 |
|-----|---------|---------|
| strong | 100% | 禁止 |
| medium | 80% | 禁止 |
| weak | 60% | 禁止 |
| neutral | 50% | 30% |
### 具体保证金金额(已降低风险)
- **medium**:账户余额 × 4%原6%
- **light**:账户余额 × 2%原3%
- **micro**:账户余额 × 1%原1.5%
- **heavy**:❌ 日内交易禁用
### ⚠️ 最小保证金要求Bitget 合约限制)
**不同币种的最小保证金要求10x 杠杆下)**
- **BTC** (0.01 BTC/张): 最小保证金 $85
- **ETH** (0.1 ETH/张): 最小保证金 $35
- **SOL** (1 SOL/张): 最小保证金 $14
- **其他币种**: 参考合约规格,确保保证金 × 杠杆 ≥ 1 张合约价值
**计算规则**
```
最小保证金 = (合约规格 × 当前价格) ÷ 杠杆倍数
```
**必须确保**:计算的保证金金额 ≥ 该币种的最小保证金,否则无法开仓!
### ⚠️ 日内交易特别规则
- 达到目标盈利2-3%**立即平仓**,不贪婪
- 持仓超过3小时**考虑强制平仓**
- 亏损达到止损**立即平仓**,不幻想
## 输出格式
```json
{
"decision": "OPEN/CLOSE/ADD/REDUCE/CANCEL_PENDING/HOLD",
"action": "buy/sell",
"entry_type": "market/limit", // market=现价入场, limit=挂单等待
"quantity": 保证金金额USDT,
"entry_price": 入场价格,
"stop_loss": 止损价格,
"take_profit": 止盈价格,
"orders_to_close": ["order_id_1"], // CLOSE/REDUCE 时指定要平仓的订单ID
"orders_to_cancel": ["order_id_1"], // CANCEL_PENDING 时指定要取消的订单ID
"reasoning": "决策理由(必须说明当前持仓/挂单状态以及为什么选择这个操作)",
"risk_analysis": "风险分析"
}
```
**重要提示**
- 当 `decision` 为 `CLOSE` 时,**必须**在 `orders_to_close` 中指定要平仓的订单ID列表
- 当 `decision` 为 `CANCEL_PENDING` 时,**必须**在 `orders_to_cancel` 中指定要取消的订单ID列表
- 同方向允许最多3个挂单金字塔布局价格差异需 > 1%
- 如果需要平仓所有持仓,`orders_to_close` 应包含所有持仓订单的ID
## 决策示例
### 示例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%订单ID: ord_123
新信号BTC 做空 @ $94,500confidence 85%,趋势反转)
分析:
- 趋势已明确反转
- 决策CLOSE平仓止损
- orders_to_close: ["ord_123"]
- 理由:趋势反转,及时止损
```
### 示例5有挂单 + 同向信号 - 金字塔布局OPEN
```
当前状态BTC 做多挂单1 @ $94,500未成交订单ID: ord_456
新信号BTC 做多 @ $93,500confidence 75%B级回调至支撑位
分析:
- 同向挂单数量1个 < 3个
- 价格差异:(94500-93500)/94500 = 1.06% > 1%
- 满足金字塔布局条件
- 决策OPEN新增挂单2
- 理由:金字塔布局,在不同价位布置挂单,提高成交概率
```
### 示例6有挂单 + 同向信号 - 价格差异太小HOLD
```
当前状态BTC 做多挂单1 @ $94,500挂单2 @ $93,500
新信号BTC 做多 @ $93,000confidence 70%B级
分析:
- 同向挂单数量2个 < 3个
- 新价格与挂单2差异(93500-93000)/93500 = 0.53% < 1%
- 太接近现有挂单
- 决策HOLD
- 理由:新价格与现有挂单太近,不新增
```
### 示例7有挂单 + 同向信号 - 达到上限HOLD
```
当前状态BTC 做多挂单1 @ $94,500挂单2 @ $93,500挂单3 @ $92,500
新信号BTC 做多 @ $91,500confidence 70%B级
分析:
- 同向挂单数量3个已达上限
- 决策HOLD
- 理由同向挂单已达3个上限不再新增
```
### 示例8有挂单 + 同向信号 - 价格加速HOLD
```
当前状态BTC 做多挂单 @ $94,000未成交
新信号BTC 做多 @ $97,000confidence 85%B级突破
分析:
- 价格快速移动($94,000 → $97,000涨幅3.2%
- 5m 连续2根大阳线正在加速
- 新信号入场价距离当前价格 >= 2%
- 决策HOLD不要追涨
- 理由:价格正在加速,禁止追涨杀跌,等待回调
```
### 示例9完全无持仓无挂单
```
当前状态:无持仓,无挂单
新信号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_close": ["order_id_1"], // CLOSE/REDUCE 时指定要平仓的订单ID
"orders_to_cancel": ["order_id_1"], // CANCEL_PENDING 时指定要取消的订单ID
"reasoning": "决策理由(必须说明当前持仓/挂单状态以及为什么选择这个操作)",
"risk_analysis": "风险分析"
}
```
**重要提示**
- 当 `decision` 为 `CLOSE` 时,**必须**在 `orders_to_close` 中指定要平仓的订单ID列表
- 当 `decision` 为 `CANCEL_PENDING` 时,**必须**在 `orders_to_cancel` 中指定要取消的订单ID列表
## 入场价格选择策略
- 使用信号中的 `entry_price` 作为入场价格
- 如果选择 `limit`(挂单)方式,等待价格达到 `entry_price` 时成交
## 重要原则
1. **仓位管理优先**:先管理现有仓位,再考虑开新仓
2. **避免重复开仓**同一标的同一方向最多1个持仓 + 1个挂单
3. **安全第一**:宁可错过机会,也不要冒过大风险
4. **遵守杠杆限制**:总杠杆永远不超过 20 倍
5. **理性决策**:不要被 FOMO 情绪左右
记住:你是仓位管理者,不是信号执行器。你的首要任务是管理好现有仓位!
"""
def __init__(self, leverage: int = 20, max_total_leverage: float = 10):
"""
初始化交易决策器
Args:
leverage: 单笔订单杠杆倍数
max_total_leverage: 总杠杆上限(持仓+挂单,倍数)
"""
self.leverage = leverage
self.max_total_leverage = max_total_leverage
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 = self.leverage
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) >= 90]
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级信号**,价格距离 >= 1.5%")
prompt_parts.append(" ⭐ A级信号confidence >= 90可考虑加仓或滚仓")
else:
prompt_parts.append("2⃣ 然后检查是否需要加仓(价格距离 >= 1.5%,盈利 >= 1.5%")
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 >= 90可考虑取消挂单现价入场")
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 >= 90:
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 >= 90:
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"- 此规则**没有例外**,无论信号等级多高都必须遵守")
# 价格加速检测规则(新增 - 防止追涨杀跌)
prompt_parts.append(f"\n## 🚨 价格加速检测(防止追涨杀跌)")
prompt_parts.append(f"**以下情况强制 HOLD禁止任何新开仓操作**")
prompt_parts.append(f"1. 信号入场价距离当前价格 >= 2% → 价格正在快速移动,不要追")
prompt_parts.append(f"2. 15m RSI > 70或 < 30→ 极端区间,不要追")
prompt_parts.append(f"3. 价格偏离 EMA5 > 1.5% → 过度延伸,不要追")
prompt_parts.append(f"4. 5m 连续2根以上大阳/阴线 → 加速中,不要追")
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:1.2,回调入场>= 1:1.5")
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: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 >= 1.5:
status = f"✅ 通过 (1:{rr_ratio:.1f})"
else:
status = f"❌ 拒绝 (1:{rr_ratio:.1f} < 1: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 = self.leverage # 使用配置的杠杆值
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})"
)
# 最小保证金检查Bitget 合约限制)
# 不同币种的最小保证金要求10x 杠杆下)
MIN_MARGIN_REQUIREMENTS = {
'BTC': 85, # 0.01 BTC/张 ≈ $850
'ETH': 35, # 0.1 ETH/张 ≈ $350
'SOL': 14, # 1 SOL/张 ≈ $140
'BNB': 7, # 0.1 BNB/张 ≈ $70
'XRP': 10, # 10 XRP/张 ≈ $100
'DOGE': 8, # 100 DOGE/张 ≈ $80
'ADA': 8, # 10 ADA/张 ≈ $80 (估计)
'AVAX': 10, # 1 AVAX/张 ≈ $100
'LINK': 8, # 1 LINK/张 ≈ $80
'DOT': 5, # 1 DOT/张 ≈ $50
'MATIC': 8, # 10 MATIC/张 ≈ $80
'POL': 8, # 10 POL/张 ≈ $80
}
symbol = decision.get('symbol', '').replace('USDT', '').upper()
min_margin = MIN_MARGIN_REQUIREMENTS.get(symbol, 10) # 默认最小 $10
if margin > 0 and margin < min_margin:
logger.warning(f"⚠️ {symbol} 保证金不足: ${margin:.2f} < 最小要求 ${min_margin:.2f}")
logger.info(f" 自动调整保证金: ${margin:.2f} → ${min_margin:.2f}")
decision['quantity'] = min_margin
logger.info(f" ✅ 保证金已调整为最小值: ${min_margin:.2f}")
# 盈亏比检查:所有交易必须满足盈亏比 >= 1: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 = 1.2 # 最小盈亏比 1:1.2
if rr_ratio < min_rr_ratio:
logger.warning(f"⚠️ 决策被拒绝: 盈亏比不足 (1:{rr_ratio:.2f} < 1:{min_rr_ratio:.1f})")
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:.1f})"
)
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()
}