1292 lines
58 KiB
Python
1292 lines
58 KiB
Python
"""
|
||
交易决策器 - 基于市场信号和当前状态做出交易决策
|
||
|
||
职责:
|
||
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,500(A级,90%置信度,趋势加速)
|
||
|
||
分析:
|
||
- 价格距离 = (97500-95000)/95000 = 2.63% >= 1.5%
|
||
- 持仓盈利 = 5% >= 1.5%(但 < 2.5%,未达平仓线)
|
||
- A级信号,趋势在加速
|
||
- 决策:ADD(加仓)
|
||
- 理由:A级信号,趋势加速,持仓盈利中,价格距离合适
|
||
```
|
||
|
||
**示例2 - 滚仓**:
|
||
```
|
||
当前:BTC 做多持仓 @ $95,000(浮亏-1%)
|
||
新信号:BTC 做多 @ $92,000(A级,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%
|
||
- ✅ 新信号置信度 >= 60(C级以上)
|
||
- ✅ 价格没有在快速加速移动
|
||
|
||
**示例1:新增第2个挂单**
|
||
```
|
||
当前:BTC 做多挂单1 @ $94,000
|
||
新信号:BTC 做多 @ $92,700(B级,75%置信度)
|
||
|
||
分析:
|
||
- 价格差异:(94000-92700)/94000 = 1.38% > 1%
|
||
- 挂单数量:1个 < 3个
|
||
- 决策:OPEN(新增挂单2 @ $92,700)
|
||
- 理由:金字塔布局,在不同价位布置挂单
|
||
```
|
||
|
||
**示例2:新增第3个挂单**
|
||
```
|
||
当前:BTC 做多挂单1 @ $94,000,挂单2 @ $92,700
|
||
新信号:BTC 做多 @ $91,500(B级,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,800(B级,70%置信度)
|
||
|
||
分析:
|
||
- 已有3个挂单,达到上限
|
||
- 决策:HOLD
|
||
- 理由:同向挂单已达3个上限,不再新增
|
||
```
|
||
|
||
**示例4:价格差异太小**
|
||
```
|
||
当前:BTC 做多挂单1 @ $94,000
|
||
新信号:BTC 做多 @ $93,200(价格差异仅0.85%)
|
||
|
||
分析:
|
||
- 价格差异:(94000-93200)/94000 = 0.85% < 1%
|
||
- 太接近现有挂单,没有意义
|
||
- 决策:HOLD
|
||
- 理由:新价格与现有挂单太近,不新增
|
||
```
|
||
|
||
**⚠️ 例外情况(保持 HOLD)**:
|
||
- 价格正在快速加速移动(5m 连续大阳/阴线)
|
||
- 新信号置信度 < 60(D级信号,质量太低)
|
||
- 新信号入场价距离当前价格 >= 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,500(confidence 75%,B级)
|
||
|
||
分析:
|
||
- 价格距离 = (96500-95000)/95000 = 1.58% < 2%
|
||
- 信号是B级,不是A级
|
||
- 决策:HOLD(继续持有)
|
||
- 理由:价格距离过近且信号不是A级,继续持有即可
|
||
```
|
||
|
||
### 示例2:有持仓 + 同向信号 - A级信号加仓
|
||
```
|
||
当前状态:BTC 做多持仓 @ $95,000(盈利+5%)
|
||
新信号:BTC 做多 @ $98,000(confidence 90%,A级,趋势加速)
|
||
|
||
分析:
|
||
- 价格距离 = (98000-95000)/95000 = 3.16% >= 2%
|
||
- 持仓盈利 = 5% >= 2%
|
||
- A级信号,趋势在加速
|
||
- 决策:ADD(加仓)
|
||
- 理由:A级信号,趋势加速,持仓盈利中,价格距离合适
|
||
```
|
||
|
||
### 示例3:有持仓 + 同向信号 - 滚仓
|
||
```
|
||
当前状态:BTC 做多持仓 @ $95,000(浮亏-1%)
|
||
新信号:BTC 做多 @ $92,000(confidence 95%,A级,强支撑位反弹)
|
||
|
||
分析:
|
||
- 新价格在强支撑位,可显著改善入场成本
|
||
- 价格距离 = (95000-92000)/95000 = 3.16% >= 3%
|
||
- A级信号(95%置信度)
|
||
- 决策:CLOSE 当前持仓 + OPEN 新仓位 @ $92,000
|
||
- 理由:滚仓至更优价格,改善风险收益比
|
||
```
|
||
|
||
### 示例4:有持仓 + 反向信号
|
||
```
|
||
当前状态:BTC 做多持仓 @ $95,000(亏损-1%),订单ID: ord_123
|
||
新信号:BTC 做空 @ $94,500(confidence 85%,趋势反转)
|
||
|
||
分析:
|
||
- 趋势已明确反转
|
||
- 决策:CLOSE(平仓止损)
|
||
- orders_to_close: ["ord_123"]
|
||
- 理由:趋势反转,及时止损
|
||
```
|
||
|
||
### 示例5:有挂单 + 同向信号 - 金字塔布局(OPEN)
|
||
```
|
||
当前状态:BTC 做多挂单1 @ $94,500(未成交),订单ID: ord_456
|
||
新信号:BTC 做多 @ $93,500(confidence 75%,B级,回调至支撑位)
|
||
|
||
分析:
|
||
- 同向挂单数量:1个 < 3个
|
||
- 价格差异:(94500-93500)/94500 = 1.06% > 1%
|
||
- 满足金字塔布局条件
|
||
- 决策:OPEN(新增挂单2)
|
||
- 理由:金字塔布局,在不同价位布置挂单,提高成交概率
|
||
```
|
||
|
||
### 示例6:有挂单 + 同向信号 - 价格差异太小(HOLD)
|
||
```
|
||
当前状态:BTC 做多挂单1 @ $94,500,挂单2 @ $93,500
|
||
新信号:BTC 做多 @ $93,000(confidence 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,500(confidence 70%,B级)
|
||
|
||
分析:
|
||
- 同向挂单数量:3个(已达上限)
|
||
- 决策:HOLD
|
||
- 理由:同向挂单已达3个上限,不再新增
|
||
```
|
||
|
||
### 示例8:有挂单 + 同向信号 - 价格加速(HOLD)
|
||
```
|
||
当前状态:BTC 做多挂单 @ $94,000(未成交)
|
||
新信号:BTC 做多 @ $97,000(confidence 85%,B级,突破)
|
||
|
||
分析:
|
||
- 价格快速移动($94,000 → $97,000,涨幅3.2%)
|
||
- 5m 连续2根大阳线,正在加速
|
||
- 新信号入场价距离当前价格 >= 2%
|
||
- 决策:HOLD(不要追涨)
|
||
- 理由:价格正在加速,禁止追涨杀跌,等待回调
|
||
```
|
||
|
||
### 示例9:完全无持仓无挂单
|
||
```
|
||
当前状态:无持仓,无挂单
|
||
新信号:BTC 做多 @ $95,000(confidence 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()
|
||
}
|