auto_trade_sys/trading_system/risk_manager.py
薇薇安 7cf6613540 1
2026-02-15 00:47:55 +08:00

1369 lines
72 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.

"""
风险管理模块 - 严格控制仓位和风险
"""
import logging
import os
import time
from datetime import datetime, timezone, timedelta
from typing import Dict, List, Optional, Tuple
try:
from .binance_client import BinanceClient
from . import config
from .atr_strategy import ATRStrategy
except ImportError:
from binance_client import BinanceClient
import config
from atr_strategy import ATRStrategy
logger = logging.getLogger(__name__)
class RiskManager:
"""风险管理类"""
def __init__(self, client: BinanceClient):
"""
初始化风险管理器
Args:
client: 币安客户端
"""
self.client = client
# 不保存引用,每次都从 config.TRADING_CONFIG 读取最新配置
# self.config = config.TRADING_CONFIG # 移除,避免使用旧配置
# 初始化ATR策略
self.atr_strategy = ATRStrategy()
async def check_position_size(self, symbol: str, quantity: float, leverage: Optional[int] = None) -> bool:
"""
检查单笔仓位大小是否符合要求
Args:
symbol: 交易对
quantity: 下单数量
leverage: 杠杆倍数(用于换算保证金);若不传则使用配置的基础杠杆
Returns:
是否通过检查
"""
try:
logger.info(f"检查 {symbol} 单笔仓位大小...")
# 获取账户余额
balance = await self.client.get_account_balance()
available_balance = balance.get('available', 0)
if available_balance <= 0:
logger.warning(f"{symbol} 账户可用余额不足: {available_balance:.2f} USDT")
return False
# 计算名义价值与保证金(名义价值/杠杆)
ticker = await self.client.get_ticker_24h(symbol)
if not ticker:
logger.warning(f"{symbol} 无法获取价格数据")
return False
current_price = ticker['price']
notional_value = quantity * current_price
actual_leverage = leverage if leverage is not None else config.TRADING_CONFIG.get('LEVERAGE', 10)
if not actual_leverage or actual_leverage <= 0:
actual_leverage = 10
margin_value = notional_value / actual_leverage
# 重要语义POSITION_PERCENT 均按“保证金占用比例”计算(更符合 stop_loss/take_profit 的 margin 逻辑)
max_margin_value = available_balance * config.TRADING_CONFIG['MAX_POSITION_PERCENT']
min_margin_value = available_balance * config.TRADING_CONFIG['MIN_POSITION_PERCENT']
max_margin_pct = config.TRADING_CONFIG['MAX_POSITION_PERCENT'] * 100
min_margin_pct = config.TRADING_CONFIG['MIN_POSITION_PERCENT'] * 100
logger.info(f" 数量: {quantity:.4f}")
logger.info(f" 价格: {current_price:.4f} USDT")
logger.info(f" 名义价值: {notional_value:.2f} USDT")
logger.info(f" 杠杆: {actual_leverage}x")
logger.info(f" 保证金: {margin_value:.4f} USDT")
logger.info(f" 单笔最大保证金: {max_margin_value:.2f} USDT ({max_margin_pct:.1f}%)")
logger.info(f" 单笔最小保证金: {min_margin_value:.2f} USDT ({min_margin_pct:.1f}%)")
# 使用小的容差来处理浮点数精度问题0.01 USDT
tolerance = 0.01
if margin_value > max_margin_value + tolerance:
logger.warning(
f"{symbol} 单笔保证金过大: {margin_value:.4f} USDT > "
f"最大限制: {max_margin_value:.2f} USDT "
f"(超出: {margin_value - max_margin_value:.4f} USDT)"
)
return False
elif margin_value > max_margin_value:
# 在容差范围内,允许通过(浮点数精度问题)
logger.info(
f"{symbol} 保证金略超限制但 within 容差: "
f"{margin_value:.4f} USDT vs {max_margin_value:.2f} USDT "
f"(差异: {margin_value - max_margin_value:.4f} USDT)"
)
if margin_value < min_margin_value:
logger.warning(
f"{symbol} 单笔保证金过小: {margin_value:.4f} USDT < "
f"最小限制: {min_margin_value:.2f} USDT"
)
return False
logger.info(f"{symbol} 单笔仓位大小检查通过")
# 检查总仓位是否超过限制
logger.info(f"检查 {symbol} 总仓位限制...")
if not await self.check_total_position(margin_value):
return False
logger.info(
f"{symbol} 所有仓位检查通过: 保证金 {margin_value:.4f} USDT "
f"(账户可用余额: {available_balance:.2f} USDT)"
)
return True
except Exception as e:
logger.error(f"检查仓位大小失败 {symbol}: {e}", exc_info=True)
return False
async def check_total_position(self, new_position_margin: float) -> bool:
"""
检查总仓位是否超过限制
Args:
new_position_margin: 新仓位保证金占用USDT
Returns:
是否通过检查
"""
try:
# 获取当前持仓
positions = await self.client.get_open_positions()
# 计算当前总保证金占用
current_position_values = []
total_margin_value = 0
for pos in positions:
notional_value = abs(pos['positionAmt'] * pos['entryPrice'])
lv = pos.get('leverage', None)
try:
lv = int(lv) if lv is not None else None
except Exception:
lv = None
if not lv or lv <= 0:
lv = config.TRADING_CONFIG.get('LEVERAGE', 10) or 10
margin_value = notional_value / lv
current_position_values.append({
'symbol': pos['symbol'],
'notional': notional_value,
'margin': margin_value,
'leverage': lv,
'amount': pos['positionAmt'],
'entryPrice': pos['entryPrice']
})
total_margin_value += margin_value
# 加上新仓位
total_with_new = total_margin_value + new_position_margin
# 获取账户余额
balance = await self.client.get_account_balance()
total_balance = balance.get('total', 0)
available_balance = balance.get('available', 0)
if total_balance <= 0:
logger.warning("账户总余额为0无法开仓")
return False
max_total_margin = total_balance * config.TRADING_CONFIG['MAX_TOTAL_POSITION_PERCENT']
max_total_margin_pct = config.TRADING_CONFIG['MAX_TOTAL_POSITION_PERCENT'] * 100
# 详细日志
logger.info("=" * 60)
logger.info("总仓位检查详情:")
logger.info(f" 账户总余额: {total_balance:.2f} USDT")
logger.info(f" 账户可用余额: {available_balance:.2f} USDT")
logger.info(f" 总保证金上限: {max_total_margin:.2f} USDT ({max_total_margin_pct:.1f}%)")
logger.info(f" 当前持仓数量: {len(positions)}")
if current_position_values:
logger.info(" 当前持仓明细:")
for pos_info in current_position_values:
logger.info(
f" - {pos_info['symbol']}: "
f"保证金 {pos_info['margin']:.4f} USDT "
f"(名义 {pos_info['notional']:.2f} USDT, {pos_info['leverage']}x, "
f"数量: {pos_info['amount']:.4f}, 入场价: {pos_info['entryPrice']:.4f})"
)
logger.info(f" 当前总保证金: {total_margin_value:.4f} USDT")
logger.info(f" 新仓位保证金: {new_position_margin:.4f} USDT")
logger.info(f" 开仓后总保证金: {total_with_new:.4f} USDT")
logger.info(f" 剩余可用保证金: {max_total_margin - total_margin_value:.4f} USDT")
if total_with_new > max_total_margin:
logger.warning("=" * 60)
logger.warning(
f"❌ 总保证金超限: {total_with_new:.4f} USDT > "
f"最大限制: {max_total_margin:.2f} USDT"
)
logger.warning(
f" 超出: {total_with_new - max_total_margin:.4f} USDT "
f"({((total_with_new - max_total_margin) / max_total_margin * 100):.1f}%)"
)
logger.warning(" 建议: 平掉部分持仓或等待现有持仓平仓后再开新仓")
logger.warning("=" * 60)
return False
logger.info(
f"✓ 总保证金检查通过: {total_with_new:.4f} USDT / "
f"最大限制: {max_total_margin:.2f} USDT "
f"({(total_with_new / max_total_margin * 100):.1f}%)"
)
logger.info("=" * 60)
return True
except Exception as e:
logger.error(f"检查总仓位失败: {e}", exc_info=True)
return False
async def calculate_dynamic_leverage(
self,
*args,
symbol: Optional[str] = None,
entry_price: Optional[float] = None,
stop_loss_price: Optional[float] = None,
atr: Optional[float] = None,
side: Optional[str] = None,
signal_strength: Optional[int] = None,
**kwargs
) -> int:
"""
计算动态杠杆 - 综合考虑信号强度和风险控制
兼容两种调用方式:
1. 新版本calculate_dynamic_leverage(symbol=..., entry_price=..., stop_loss_price=..., ...)
2. 旧版本calculate_dynamic_leverage(signal_strength, symbol, atr=..., entry_price=...)
Args:
symbol: 交易对
entry_price: 入场价格
stop_loss_price: 止损价格
atr: ATR值
side: 交易方向
signal_strength: 信号强度 (0-10)
Returns:
建议杠杆倍数
"""
# 兼容旧调用方式:第一个位置参数可能是 signal_strength
if args and len(args) > 0:
first_arg = args[0]
if isinstance(first_arg, int) and 0 <= first_arg <= 10:
signal_strength = first_arg
elif isinstance(first_arg, str):
symbol = first_arg
if len(args) > 1 and isinstance(args[1], str) and symbol is None:
symbol = args[1]
if len(args) > 2 and isinstance(args[2], (int, float)) and entry_price is None:
entry_price = args[2]
# 从 kwargs 中提取(兼容旧调用)
if 'signal_strength' in kwargs and signal_strength is None:
signal_strength = kwargs.get('signal_strength')
if 'symbol' in kwargs and symbol is None:
symbol = kwargs.get('symbol')
if 'entry_price' in kwargs and entry_price is None:
entry_price = kwargs.get('entry_price')
if 'atr' in kwargs and atr is None:
atr = kwargs.get('atr')
# 默认使用配置杠杆
default_leverage = config.TRADING_CONFIG.get('LEVERAGE', 10)
if not config.TRADING_CONFIG.get('USE_DYNAMIC_LEVERAGE', False):
return default_leverage
# 1. 基于信号强度的杠杆计算 (进攻性)
signal_leverage = default_leverage
if signal_strength is not None:
base_leverage = default_leverage
max_leverage = config.TRADING_CONFIG.get('MAX_LEVERAGE', 20)
min_signal_strength = config.TRADING_CONFIG.get('MIN_SIGNAL_STRENGTH', 7)
if signal_strength >= min_signal_strength:
signal_range = 10 - min_signal_strength
leverage_range = max_leverage - base_leverage
if signal_range > 0:
strength_above_min = signal_strength - min_signal_strength
leverage_increase = (strength_above_min / signal_range) * leverage_range
signal_leverage = base_leverage + leverage_increase
signal_leverage = min(signal_leverage, max_leverage)
logger.info(f" 📊 信号强度杠杆 ({symbol or 'N/A'}): 信号={signal_strength} -> {int(signal_leverage)}x")
# 2. 基于ATR波动率的限制 (小众币保护)
atr_limit_leverage = 100 # 初始设为很高
if atr and entry_price and entry_price > 0:
atr_percent = atr / entry_price
atr_leverage_reduction_threshold = config.TRADING_CONFIG.get('ATR_LEVERAGE_REDUCTION_THRESHOLD', 0.05) # 5%
max_leverage_small_cap = config.TRADING_CONFIG.get('MAX_LEVERAGE_SMALL_CAP', 8) # 默认 8与之前盈利阶段一致
if atr_percent >= atr_leverage_reduction_threshold:
atr_limit_leverage = max_leverage_small_cap
logger.info(f" ⚠️ ATR波动率 {atr_percent*100:.2f}% (高波动), 限制最大杠杆为 {atr_limit_leverage}x")
# 3. 基于止损宽度的风险控制 (防御性 - 核心逻辑)
risk_safe_leverage = 100 # 初始设为很高
# 尝试获取止损价格(如果没有传入,尝试估算)
target_stop_loss = stop_loss_price
if target_stop_loss is None and atr and atr > 0 and entry_price:
atr_multiplier = config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 3.0)
if side == 'BUY':
target_stop_loss = entry_price - (atr * atr_multiplier)
elif side == 'SELL':
target_stop_loss = entry_price + (atr * atr_multiplier)
if target_stop_loss and entry_price:
# 计算止损宽度比例
stop_loss_width = abs(entry_price - target_stop_loss) / entry_price
if stop_loss_width > 0:
# 获取最大单笔亏损率限制 (默认20%)
max_loss_pct = config.TRADING_CONFIG.get('MAX_SINGLE_TRADE_LOSS_PERCENT', 20.0) / 100.0
# 理论杠杆 = 最大允许亏损比例 / 止损宽度比例
# 例如最大亏损20%止损宽5%,则最大杠杆 = 4倍 (4 * 5% = 20%)
theoretical_leverage = max_loss_pct / stop_loss_width
risk_safe_leverage = int(theoretical_leverage)
logger.info(f" 🛡️ 风险安全杠杆 ({symbol or 'N/A'}):")
logger.info(f" 止损宽度: {stop_loss_width*100:.2f}%")
logger.info(f" 最大单笔亏损限制: {max_loss_pct*100:.1f}%")
logger.info(f" -> 理论最大杠杆: {theoretical_leverage:.2f}x")
# 4. 综合计算最终杠杆 (取最小值)
final_leverage = min(int(signal_leverage), int(atr_limit_leverage), int(risk_safe_leverage))
# 限制在系统最大杠杆范围内
max_config_leverage = config.TRADING_CONFIG.get('MAX_LEVERAGE', 20)
final_leverage = min(final_leverage, max_config_leverage)
# 杠杆下限:不低于 MIN_LEVERAGE之前盈利阶段多为固定 8x下限 8 可避免被压到 24x 导致单笔盈利过少)
min_leverage = max(1, int(config.TRADING_CONFIG.get('MIN_LEVERAGE', 1) or 1))
final_leverage = max(min_leverage, final_leverage)
# 检查交易对最大杠杆限制
if symbol:
try:
symbol_info = await self.client.get_symbol_info(symbol)
if symbol_info and 'maxLeverage' in symbol_info:
symbol_max_leverage = symbol_info['maxLeverage']
if final_leverage > symbol_max_leverage:
logger.warning(f" ⚠️ {symbol} 交易所限制最大杠杆 {symbol_max_leverage}x, 调整 {final_leverage}x -> {symbol_max_leverage}x")
final_leverage = symbol_max_leverage
except Exception as e:
pass
logger.info(f" ⚖️ 最终动态杠杆: {final_leverage}x (信号:{int(signal_leverage)}x, ATR限制:{int(atr_limit_leverage)}x, 风险安全:{int(risk_safe_leverage)}x)")
return final_leverage
async def calculate_position_size(
self,
symbol: str,
change_percent: float,
leverage: Optional[int] = None,
entry_price: Optional[float] = None,
stop_loss_price: Optional[float] = None,
side: Optional[str] = None,
atr: Optional[float] = None,
signal_strength: Optional[int] = None
) -> Tuple[Optional[float], int]:
"""
根据涨跌幅和风险参数计算合适的仓位大小
⚠️ 优化:支持固定风险百分比计算(凯利公式)和信号强度分级
Args:
symbol: 交易对
change_percent: 涨跌幅百分比
leverage: 杠杆倍数(可选)
entry_price: 入场价格(可选,如果提供则用于固定风险计算)
stop_loss_price: 止损价格(可选,如果提供则用于固定风险计算)
side: 交易方向 'BUY''SELL'(可选,用于固定风险计算)
atr: ATR值可选用于估算止损
signal_strength: 信号强度(可选,用于仓位分级)
Returns:
Tuple[Optional[float], int]: (仓位数量, 实际使用的杠杆倍数)。如果无法开仓仓位数量为None。
"""
try:
logger.info(f"开始计算 {symbol} 的仓位大小...")
# 获取账户余额
balance = await self.client.get_account_balance()
available_balance = balance.get('available', 0)
total_balance = balance.get('total', 0)
logger.info(f" 账户可用余额: {available_balance:.2f} USDT")
logger.info(f" 账户总余额: {total_balance:.2f} USDT")
if available_balance <= 0:
logger.warning(f"{symbol} 账户可用余额不足: {available_balance:.2f} USDT")
return None, 10
# 获取当前价格
ticker = await self.client.get_ticker_24h(symbol)
if not ticker:
logger.warning(f"{symbol} 无法获取价格数据")
return None, 10
current_price = ticker['price']
logger.info(f" 当前价格: {current_price:.4f} USDT")
# -------------------------------------------------------------------------
# 动态杠杆计算逻辑 (针对 ZROUSDT 亏损案例优化)
# -------------------------------------------------------------------------
calculated_leverage = None
if config.TRADING_CONFIG.get('USE_DYNAMIC_LEVERAGE', False):
calculated_leverage = await self.calculate_dynamic_leverage(
symbol=symbol,
entry_price=entry_price if entry_price else current_price,
stop_loss_price=stop_loss_price,
atr=atr,
side=side
)
# 确定最终使用的杠杆
# 优先级: 动态计算杠杆 > 传入的leverage参数 > 配置的默认LEVERAGE
if calculated_leverage is not None:
actual_leverage = calculated_leverage
if leverage is not None and leverage != actual_leverage:
logger.warning(f" ⚠️ 覆盖传入的杠杆 {leverage}x -> 动态调整为 {actual_leverage}x")
else:
actual_leverage = leverage if leverage is not None else config.TRADING_CONFIG.get('LEVERAGE', 10)
if not actual_leverage or actual_leverage <= 0:
actual_leverage = 10
# ⚠️ 优化3固定风险百分比仓位计算凯利公式
# 公式:仓位大小 = (总资金 * 每笔单子承受的风险%) / (入场价 - 止损价)
use_fixed_risk = config.TRADING_CONFIG.get('USE_FIXED_RISK_SIZING', True)
fixed_risk_percent = config.TRADING_CONFIG.get('FIXED_RISK_PERCENT', 0.02) # 默认2%
quantity = None # 初始化为None确保变量存在
if use_fixed_risk and entry_price and side:
try:
# 如果未提供止损价格,先估算
if stop_loss_price is None:
# 尝试使用ATR估算止损距离
if atr and atr > 0:
atr_multiplier = config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 3.0) # 默认3.0,放宽止损提升胜率
if side == 'BUY':
estimated_stop_loss = entry_price - (atr * atr_multiplier)
else: # SELL
estimated_stop_loss = entry_price + (atr * atr_multiplier)
stop_loss_price = estimated_stop_loss
else:
# 使用固定百分比估算(基于保证金)
stop_loss_pct = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.10)
# 先估算一个临时仓位来计算止损距离
temp_margin = total_balance * 0.05 # 临时使用5%保证金
temp_notional = temp_margin * actual_leverage
temp_quantity = temp_notional / entry_price if entry_price > 0 else 0
if temp_quantity > 0:
stop_loss_amount = temp_margin * stop_loss_pct
if side == 'BUY':
estimated_stop_loss = entry_price - (stop_loss_amount / temp_quantity)
else: # SELL
estimated_stop_loss = entry_price + (stop_loss_amount / temp_quantity)
stop_loss_price = estimated_stop_loss
else:
stop_loss_price = None
# 计算止损距离
if stop_loss_price is not None and stop_loss_price > 0:
if side == 'BUY':
stop_distance = entry_price - stop_loss_price
else: # SELL
stop_distance = stop_loss_price - entry_price
if stop_distance > 0:
# ⚠️ 关键安全检查:验证止损距离是否在强平风险区内
# 强平距离估算Entry / Leverage
# 考虑到维持保证金强平会更早发生。安全系数取0.8即亏损80%保证金时强制止损,防止强平)
max_safe_stop_distance = (entry_price / actual_leverage) * 0.8
if stop_distance > max_safe_stop_distance:
logger.warning(
f" ⚠️ 止损距离过大 ({stop_distance:.4f}),超过当前杠杆的安全强平距离 ({max_safe_stop_distance:.4f}, 杠杆{actual_leverage}x)"
)
# 尝试降低杠杆以适应宽止损
# 安全杠杆 = (Entry * 0.8) / StopDistance
ideal_leverage_float = (entry_price * 0.8) / stop_distance
ideal_leverage = max(1, int(ideal_leverage_float))
if ideal_leverage < actual_leverage:
logger.info(f" 🛡️ 自动降低杠杆: {actual_leverage}x -> {ideal_leverage}x 以适应止损距离")
actual_leverage = ideal_leverage
# 重新计算安全距离
max_safe_stop_distance = (entry_price / actual_leverage) * 0.8
# 如果即使降到1倍杠杆还是不够极少见或者无法降杠杆则必须截断止损
if stop_distance > max_safe_stop_distance:
logger.warning(f" ⚠️ 即使降杠杆也无法满足安全距离,必须截断止损")
logger.info(f" ✓ 自动调整止损距离至安全范围: {max_safe_stop_distance:.4f}")
# 调整止损距离和价格
stop_distance = max_safe_stop_distance
if side == 'BUY':
stop_loss_price = entry_price - stop_distance
else:
stop_loss_price = entry_price + stop_distance
logger.info(f" ✓ 修正后的止损价格: {stop_loss_price:.4f}")
# 固定风险金额
risk_amount = total_balance * fixed_risk_percent
# ⚠️ 优化:将信号强度乘数应用于固定风险金额
signal_multiplier = 1.0
if signal_strength is not None:
# 获取配置如果配置为None可能从DB读取时为空则使用默认值
signal_multipliers = config.TRADING_CONFIG.get('SIGNAL_STRENGTH_POSITION_MULTIPLIER')
if signal_multipliers is None:
signal_multipliers = {7: 0.8, 8: 1.0, 9: 1.2, 10: 1.5}
# 确保是字典类型
if isinstance(signal_multipliers, dict):
signal_multiplier = signal_multipliers.get(signal_strength, 1.0)
if signal_multiplier != 1.0:
logger.info(f" ⚡️ 应用信号强度乘数: {signal_multiplier} (信号强度: {signal_strength})")
risk_amount = risk_amount * signal_multiplier
else:
logger.warning(f" ⚠️ SIGNAL_STRENGTH_POSITION_MULTIPLIER 配置格式错误: {type(signal_multipliers)}")
# 根据止损距离反算仓位
# 风险金额 = (入场价 - 止损价) × 数量
# 所以:数量 = 风险金额 / (入场价 - 止损价)
quantity = risk_amount / stop_distance
# 计算对应的保证金和名义价值
notional_value = quantity * entry_price
margin_value = notional_value / actual_leverage
logger.info(f" ⚠️ 使用固定风险百分比计算仓位:")
logger.info(f" 固定风险: {fixed_risk_percent*100:.2f}% = {risk_amount:.4f} USDT")
logger.info(f" 止损距离: {stop_distance:.4f} USDT ({stop_distance/entry_price*100:.2f}%)")
logger.info(f" 计算数量: {quantity:.4f}")
logger.info(f" 名义价值: {notional_value:.2f} USDT")
logger.info(f" 保证金: {margin_value:.4f} USDT ({margin_value/total_balance*100:.2f}%)")
# 检查是否超过最大仓位限制
max_position_percent = config.TRADING_CONFIG['MAX_POSITION_PERCENT']
# ⚠️ 如果启用了固定风险模型,且计算出的仓位合理(风险可控),则放宽 MAX_POSITION_PERCENT 限制
# 因为固定风险模型的核心是控制亏损额,而不是仓位大小
if use_fixed_risk:
# 在固定风险模式下,允许使用高达 95% 的可用余额作为保证金(只要风险金额符合设定)
max_margin_value = available_balance * 0.95
if margin_value > max_margin_value:
logger.warning(f" ⚠️ 固定风险计算的保证金 {margin_value:.4f} USDT 超过账户可用余额 95%")
logger.info(f" ✓ 调整为最大可用余额限制: {max_margin_value:.2f} USDT")
margin_value = max_margin_value
notional_value = margin_value * actual_leverage
quantity = notional_value / entry_price if entry_price > 0 else None
else:
if margin_value > (available_balance * max_position_percent):
logger.info(f" 固定风险模式:突破单仓位限制 ({max_position_percent*100:.1f}%) -> 使用计算值 {margin_value/available_balance*100:.1f}%")
else:
max_margin_value = available_balance * max_position_percent
if margin_value > max_margin_value:
# 如果超过最大仓位,使用最大仓位
logger.warning(f" ⚠️ 固定风险计算的保证金 {margin_value:.4f} USDT > 最大限制 {max_margin_value:.2f} USDT")
logger.info(f" ✓ 调整为最大仓位限制: {max_margin_value:.2f} USDT")
margin_value = max_margin_value
notional_value = margin_value * actual_leverage
quantity = notional_value / entry_price if entry_price > 0 else None
else:
logger.warning(f" ⚠️ 止损距离无效 (stop_distance={stop_distance:.4f}),将使用传统方法计算仓位")
else:
logger.warning(f" ⚠️ 无法估算止损价格,将使用传统方法计算仓位")
except Exception as e:
logger.warning(f" ⚠️ 固定风险百分比计算失败: {e},将使用传统方法计算仓位")
quantity = None # 确保quantity为None使用传统方法
# 如果未使用固定风险计算或计算失败,使用原来的方法
if quantity is None:
# ⚠️ 优化3信号强度分级 - 9-10分高权重8分轻仓
signal_multiplier = 1.0
if signal_strength is not None:
signal_multipliers = config.TRADING_CONFIG.get('SIGNAL_STRENGTH_POSITION_MULTIPLIER', {8: 0.5, 9: 1.0, 10: 1.0})
signal_multiplier = signal_multipliers.get(signal_strength, 1.0)
if signal_strength == 8:
logger.info(f" ⚠️ 信号强度8分使用50%仓位(轻仓试探)")
elif signal_strength >= 9:
logger.info(f" ✓ 信号强度{signal_strength}使用100%仓位(高质量信号)")
# 根据涨跌幅调整仓位大小(涨跌幅越大,保证金占比可以适当增加)
base_position_percent = config.TRADING_CONFIG['MAX_POSITION_PERCENT'] * signal_multiplier
max_position_percent = config.TRADING_CONFIG['MAX_POSITION_PERCENT'] * signal_multiplier
min_position_percent = config.TRADING_CONFIG['MIN_POSITION_PERCENT']
# 涨跌幅超过5%时,可以适当增加保证金占比,但必须遵守 MAX_POSITION_PERCENT 上限
if abs(change_percent) > 5:
position_percent = min(
base_position_percent * 1.5,
max_position_percent
)
logger.info(f" 涨跌幅 {change_percent:.2f}% > 5%,使用增强仓位比例: {position_percent*100:.1f}%")
else:
position_percent = base_position_percent
logger.info(f" 涨跌幅 {change_percent:.2f}%,使用标准仓位比例: {position_percent*100:.1f}%")
# 计算保证金与名义价值
margin_value = available_balance * position_percent
notional_value = margin_value * actual_leverage
logger.info(f" 计算保证金: {margin_value:.4f} USDT ({position_percent*100:.1f}% of {available_balance:.2f})")
logger.info(f" 计算名义价值: {notional_value:.2f} USDT (保证金 {margin_value:.4f} × 杠杆 {actual_leverage}x)")
# 计算数量
quantity = notional_value / current_price
logger.info(f" 计算数量: {quantity:.4f}")
# 计算名义价值和保证金(如果还未计算)
if 'notional_value' not in locals() or 'margin_value' not in locals():
notional_value = quantity * current_price
margin_value = notional_value / actual_leverage
# 确保仓位价值满足最小名义价值要求币安要求至少5 USDT
min_notional = 5.0 # 币安合约最小名义价值
calculated_notional = quantity * current_price
if calculated_notional < min_notional:
# 如果计算出的名义价值仍然不足,增加数量
required_quantity = min_notional / current_price
logger.warning(f" ⚠ 计算出的名义价值 {calculated_notional:.2f} USDT < {min_notional:.2f} USDT")
logger.info(f" ✓ 调整数量从 {quantity:.4f}{required_quantity:.4f}")
quantity = required_quantity
notional_value = required_quantity * current_price
margin_value = notional_value / actual_leverage
# 计算名义价值和保证金(如果还未计算)
if 'notional_value' not in locals() or 'margin_value' not in locals():
notional_value = quantity * current_price
margin_value = notional_value / actual_leverage
# 确保仓位价值满足最小名义价值要求币安要求至少5 USDT
min_notional = 5.0 # 币安合约最小名义价值
calculated_notional = quantity * current_price
if calculated_notional < min_notional:
# 如果计算出的名义价值仍然不足,增加数量
required_quantity = min_notional / current_price
logger.warning(f" ⚠ 计算出的名义价值 {calculated_notional:.2f} USDT < {min_notional:.2f} USDT")
logger.info(f" ✓ 调整数量从 {quantity:.4f}{required_quantity:.4f}")
quantity = required_quantity
notional_value = required_quantity * current_price
margin_value = notional_value / actual_leverage
# 检查最小保证金要求margin 语义MIN_MARGIN_USDT 本身就是保证金下限)
# 2026-02-13 优化:默认最小保证金提高到 10.0 USDT避免无效小单
min_margin_usdt = config.TRADING_CONFIG.get('MIN_MARGIN_USDT', 10.0)
logger.info(f" 当前保证金: {margin_value:.4f} USDT (杠杆: {actual_leverage}x)")
if margin_value < min_margin_usdt:
# 保证金不足,需要增加保证金
logger.warning(
f" ⚠ 保证金 {margin_value:.4f} USDT < 最小保证金要求 {min_margin_usdt:.2f} USDT"
)
# 检查是否可以使用更大的仓位价值(但不超过最大仓位限制)
if use_fixed_risk:
# 固定风险模式下放宽最大仓位限制允许高达95%可用余额)
max_position_percent = 0.95
max_margin_value = available_balance * 0.95
else:
max_position_percent = config.TRADING_CONFIG['MAX_POSITION_PERCENT']
max_margin_value = available_balance * max_position_percent
if min_margin_usdt <= max_margin_value:
margin_value = min_margin_usdt
notional_value = margin_value * actual_leverage
quantity = notional_value / current_price
logger.info(
f" ✓ 调整保证金到 {margin_value:.2f} USDT "
f"(名义 {notional_value:.2f} USDT) 以满足最小保证金要求"
)
else:
# 即使使用最大仓位也无法满足最小保证金要求
max_margin = max_margin_value
logger.warning(
f" ❌ 无法满足最小保证金要求: "
f"需要 {min_margin_usdt:.2f} USDT 保证金,"
f"但最大允许 {max_margin:.2f} USDT 保证金 (MAX_POSITION_PERCENT={max_position_percent*100:.2f}%)"
)
logger.warning(
f" 💡 建议: 增加账户余额到至少 "
f"{min_margin_usdt / max_position_percent:.2f} USDT "
f"才能满足最小保证金要求"
)
return None, actual_leverage
# 检查是否通过风险控制
logger.info(f" 检查仓位大小是否符合风险控制要求...")
# 计算最终的名义价值与保证金
final_notional_value = quantity * current_price
final_margin = final_notional_value / actual_leverage if actual_leverage > 0 else final_notional_value
# 仓位放大系数盈利时适当放大仓位1.0=正常1.2=+20%上限2.0,仍受 MAX_POSITION_PERCENT 约束)
position_scale = config.TRADING_CONFIG.get('POSITION_SCALE_FACTOR', 1.0)
try:
position_scale = float(position_scale or 1.0)
position_scale = max(1.0, min(position_scale, 2.0))
except (TypeError, ValueError):
position_scale = 1.0
if position_scale > 1.0:
quantity = quantity * position_scale
final_notional_value = quantity * current_price
final_margin = final_notional_value / actual_leverage if actual_leverage > 0 else final_notional_value
max_margin_cap = available_balance * config.TRADING_CONFIG.get('MAX_POSITION_PERCENT', 0.08)
if final_margin > max_margin_cap:
final_margin = max_margin_cap
final_notional_value = final_margin * actual_leverage
quantity = final_notional_value / current_price if current_price and current_price > 0 else quantity
logger.info(f" 仓位放大后超过单笔上限,已截断至 MAX_POSITION_PERCENT 对应保证金")
logger.info(f" 仓位放大系数: {position_scale:.2f} -> 最终数量: {quantity:.4f}")
# 添加最小名义价值检查0.2 USDT避免下无意义的小单子
MIN_NOTIONAL_VALUE = 0.2 # 最小名义价值0.2 USDT
if final_notional_value < MIN_NOTIONAL_VALUE:
logger.warning(
f"{symbol} 名义价值 {final_notional_value:.4f} USDT < 最小要求 {MIN_NOTIONAL_VALUE:.2f} USDT"
)
logger.warning(f" 💡 此类小单子意义不大,拒绝开仓")
return None, actual_leverage
if await self.check_position_size(symbol, quantity, leverage=actual_leverage):
logger.info(
f"{symbol} 仓位计算成功: {quantity:.4f} "
f"(保证金: {final_margin:.4f} USDT, "
f"名义价值: {final_notional_value:.2f} USDT, "
f"保证金: {final_margin:.4f} USDT, 杠杆: {actual_leverage}x)"
)
return quantity, actual_leverage
else:
logger.warning(f"{symbol} 仓位检查未通过,无法开仓")
return None, actual_leverage
except Exception as e:
logger.error(f"计算仓位大小失败 {symbol}: {e}", exc_info=True)
return None, 10
async def should_trade(self, symbol: str, change_percent: float) -> bool:
"""
判断是否应该交易
Args:
symbol: 交易对
change_percent: 涨跌幅百分比
Returns:
是否应该交易
"""
# 用户风险旋钮:自动交易总开关
if not bool(config.TRADING_CONFIG.get("AUTO_TRADE_ENABLED", True)):
logger.info(f"{symbol} 自动交易已关闭AUTO_TRADE_ENABLED=false跳过")
return False
# 检查最小涨跌幅阈值
if abs(change_percent) < config.TRADING_CONFIG['MIN_CHANGE_PERCENT']:
logger.debug(f"{symbol} 涨跌幅 {change_percent:.2f}% 小于阈值")
return False
# 检查是否已有持仓 / 总持仓数量限制
positions = await self.client.get_open_positions()
try:
max_open = int(config.TRADING_CONFIG.get("MAX_OPEN_POSITIONS", 0) or 0)
except Exception:
max_open = 0
if max_open > 0 and len(positions) >= max_open:
logger.debug(f"{symbol} 持仓数量已达上限:{len(positions)}/{max_open},跳过开仓")
return False
existing_position = next(
(p for p in positions if p['symbol'] == symbol),
None
)
if existing_position:
logger.info(f"{symbol} 已有持仓,跳过")
return False
# 每日开仓次数限制Redis 计数;无 Redis 时降级为内存计数)
try:
max_daily = int(config.TRADING_CONFIG.get("MAX_DAILY_ENTRIES", 0) or 0)
except Exception:
max_daily = 0
if max_daily > 0:
c = await self._get_daily_entries_count()
if c >= max_daily:
logger.info(f"{symbol} 今日开仓次数已达上限:{c}/{max_daily},跳过")
return False
# ⚠️ 2026-01-29新增检查同一交易对连续亏损情况避免连续亏损后继续交易
try:
loss_cooldown_enabled = bool(config.TRADING_CONFIG.get("SYMBOL_LOSS_COOLDOWN_ENABLED", True))
if loss_cooldown_enabled:
max_consecutive_losses = int(config.TRADING_CONFIG.get("SYMBOL_MAX_CONSECUTIVE_LOSSES", 2) or 2)
loss_cooldown_sec = int(config.TRADING_CONFIG.get("SYMBOL_LOSS_COOLDOWN_SEC", 3600) or 3600) # 默认1小时
# 查询该交易对最近的交易记录(仅查询已平仓的)
recent_losses = await self._check_recent_losses(symbol, max_consecutive_losses, loss_cooldown_sec)
if recent_losses >= max_consecutive_losses:
logger.info(
f"{symbol} [连续亏损过滤] 最近{max_consecutive_losses}次交易连续亏损,"
f"禁止交易{loss_cooldown_sec}秒(冷却中)"
)
return False
except Exception as e:
logger.debug(f"{symbol} 检查连续亏损时出错(忽略,允许交易): {e}")
return True
async def _check_recent_losses(self, symbol: str, max_consecutive: int, cooldown_sec: int) -> int:
"""
检查同一交易对最近的连续亏损次数
Args:
symbol: 交易对
max_consecutive: 最大允许连续亏损次数
cooldown_sec: 冷却时间(秒)
Returns:
连续亏损次数(如果>=max_consecutive则应该禁止交易
"""
try:
# 尝试从数据库查询最近的交易记录
from database.models import Trade
# 查询最近N+1次已平仓的交易多查一次确保能判断是否连续
recent_trades = Trade.get_all(
symbol=symbol,
status='closed',
account_id=int(os.getenv("ATS_ACCOUNT_ID") or os.getenv("ACCOUNT_ID") or 1)
)
# 按平仓时间倒序排序(最新的在前)
recent_trades = sorted(
recent_trades,
key=lambda x: (x.get('exit_time') or x.get('entry_time') or 0),
reverse=True
)[:max_consecutive + 1] # 只取最近N+1次
if not recent_trades:
return 0
# 检查是否在冷却时间内
now = int(time.time())
latest_trade = recent_trades[0]
latest_exit_time = latest_trade.get('exit_time') or latest_trade.get('entry_time') or 0
# 如果最新交易还在冷却时间内,检查连续亏损
if now - latest_exit_time < cooldown_sec:
consecutive_losses = 0
for trade in recent_trades:
pnl = float(trade.get('pnl', 0) or 0)
exit_reason = str(trade.get('exit_reason', '') or '').lower()
# 2026-02-13 优化手动平仓manual通常是人为干预如发现信号消失或误操作
# 不应计入策略的连续亏损统计,避免误触发冷却。
if exit_reason == 'manual' or exit_reason == 'manual_close':
continue
if pnl < 0: # 亏损
consecutive_losses += 1
else: # 盈利,中断连续亏损
break
return consecutive_losses
# 如果最新交易已超过冷却时间,不限制
return 0
except Exception as e:
logger.debug(f"查询{symbol}最近交易记录失败(忽略,允许交易): {e}")
return 0
def _daily_entries_key(self) -> str:
try:
aid = int(os.getenv("ATS_ACCOUNT_ID") or os.getenv("ACCOUNT_ID") or 1)
except Exception:
aid = 1
bj = timezone(timedelta(hours=8))
d = datetime.now(bj).strftime("%Y%m%d")
return f"ats:acc:{aid}:daily_entries:{d}"
def _seconds_until_beijing_day_end(self) -> int:
bj = timezone(timedelta(hours=8))
now = datetime.now(bj)
end = (now.replace(hour=23, minute=59, second=59, microsecond=0))
return max(60, int((end - now).total_seconds()) + 1)
async def _get_daily_entries_count(self) -> int:
key = self._daily_entries_key()
try:
# redis_cache 已有内存降级逻辑
return int(await self.client.redis_cache.get_int(key, 0))
except Exception:
return 0
async def record_entry(self, symbol: str = "") -> None:
"""在“开仓真正成功”后调用,用于累计每日开仓次数。"""
try:
max_daily = int(config.TRADING_CONFIG.get("MAX_DAILY_ENTRIES", 0) or 0)
except Exception:
max_daily = 0
if max_daily <= 0:
return
key = self._daily_entries_key()
ttl = self._seconds_until_beijing_day_end()
try:
n = await self.client.redis_cache.incr(key, 1, ttl=ttl)
logger.info(f"{symbol} 今日开仓计数 +1{n}/{max_daily}")
except Exception:
return
def get_stop_loss_price(
self,
entry_price: float,
side: str,
quantity: float,
leverage: int,
stop_loss_pct: Optional[float] = None,
klines: Optional[List] = None,
bollinger: Optional[Dict] = None,
atr: Optional[float] = None
) -> float:
"""
计算止损价格(基于保证金的盈亏金额)
Args:
entry_price: 入场价格
side: 方向 'BUY''SELL'
quantity: 持仓数量
leverage: 杠杆倍数
stop_loss_pct: 止损百分比相对于保证金如果为None则使用配置值
klines: K线数据用于计算支撑/阻力位(作为辅助参考)
bollinger: 布林带数据,用于计算动态止损(作为辅助参考)
atr: 平均真实波幅,用于计算动态止损(作为辅助参考)
Returns:
止损价格
"""
# 计算保证金和仓位价值
position_value = entry_price * quantity
margin = position_value / leverage if leverage > 0 else position_value
# 优先使用ATR动态止损如果启用且ATR可用
# 计算ATR百分比如果提供了ATR绝对值
atr_percent = None
if atr is not None and atr > 0 and entry_price > 0:
atr_percent = atr / entry_price
# 获取市场波动率(如果可用)
volatility = None # 可以从symbol_info中获取这里暂时为None
# 使用ATR策略计算止损
stop_loss_price_atr, stop_distance_atr, atr_details = self.atr_strategy.calculate_stop_loss(
entry_price, side, atr, atr_percent, volatility
)
if stop_loss_price_atr is None:
logger.debug(f"ATR不可用使用固定百分比止损")
# 获取止损百分比(相对于保证金)
stop_loss_percent = stop_loss_pct or config.TRADING_CONFIG['STOP_LOSS_PERCENT']
# ⚠️ 关键修复:配置值格式转换(兼容百分比形式和比例形式)
# 如果值>1认为是百分比形式转换为比例形式
if stop_loss_percent > 1:
stop_loss_percent = stop_loss_percent / 100.0
# 计算止损金额(相对于保证金)
stop_loss_amount = margin * stop_loss_percent
# 计算基于保证金的止损价
# 止损金额 = (开仓价 - 止损价) × 数量
# 所以:止损价 = 开仓价 - (止损金额 / 数量)
if side == 'BUY': # 做多,止损价低于入场价
stop_loss_price_margin = entry_price - (stop_loss_amount / quantity)
else: # 做空,止损价高于入场价
stop_loss_price_margin = entry_price + (stop_loss_amount / quantity)
# 同时计算基于价格百分比的止损价(作为最小值保护)
# 获取最小价格变动百分比(如果配置了)
min_price_change_pct = config.TRADING_CONFIG.get('MIN_STOP_LOSS_PRICE_PCT', None)
if min_price_change_pct is not None:
# 基于价格百分比的止损价
if side == 'BUY':
stop_loss_price_price = entry_price * (1 - min_price_change_pct)
else:
stop_loss_price_price = entry_price * (1 + min_price_change_pct)
else:
stop_loss_price_price = None
# 选择最终的止损价优先ATR其次保证金最后价格百分比取更宽松的
candidate_prices = []
if stop_loss_price_atr is not None:
candidate_prices.append(('ATR', stop_loss_price_atr))
candidate_prices.append(('保证金', stop_loss_price_margin))
if stop_loss_price_price is not None:
candidate_prices.append(('价格百分比', stop_loss_price_price))
# ⚠️ 优化如果ATR可用优先使用ATR止损不强制取"更紧"的止损
# 只有在ATR不可用时才使用保证金止损作为兜底
if stop_loss_price_atr is not None:
stop_loss_price = stop_loss_price_atr
selected_method = 'ATR'
# 记录被覆盖的保证金止损(仅供参考)
logger.debug(f"优先使用ATR止损: {stop_loss_price:.4f}, 忽略保证金止损: {stop_loss_price_margin:.4f}")
else:
# ATR不可用使用保证金止损和价格百分比止损中"更紧"的一个(保护资金)
if side == 'BUY':
# 做多:取最大值(更高的止损价,更接近入场价)
stop_loss_price = max(p[1] for p in candidate_prices if p[0] != 'ATR')
else:
# 做空:取最小值(更低的止损价,更接近入场价)
stop_loss_price = min(p[1] for p in candidate_prices if p[0] != 'ATR')
# 找到对应的策略名称
matched = [p[0] for p in candidate_prices if abs(p[1] - stop_loss_price) < 1e-9]
selected_method = matched[0] if matched else '保证金'
# 如果提供了技术分析数据,计算技术止损(允许更紧的止损,但需要在保证金止损范围内)
technical_stop = None
if klines and len(klines) >= 10:
# 计算支撑/阻力位
low_prices = [float(k[3]) for k in klines[-20:]] # 最近20根K线的最低价
high_prices = [float(k[2]) for k in klines[-20:]] # 最近20根K线的最高价
if side == 'BUY': # 做多,止损放在支撑位下方
# 找到近期波段低点
recent_low = min(low_prices)
# 止损放在低点下方0.5%
buffer = entry_price * 0.005 # 0.5%缓冲
technical_stop = recent_low - buffer
# 如果布林带可用,也可以考虑布林带下轨
if bollinger and bollinger.get('lower'):
bollinger_stop = bollinger['lower'] * 0.995 # 布林带下轨下方0.5%
technical_stop = max(technical_stop, bollinger_stop)
# 技术止损更紧,但需要确保在保证金止损范围内(不能超过保证金止损)
if technical_stop < stop_loss_price and technical_stop >= stop_loss_price_margin:
# 技术止损在合理范围内,可以考虑使用
candidate_prices.append(('技术分析', technical_stop))
logger.debug(
f"技术止损 (BUY): {technical_stop:.4f} "
f"(在保证金止损范围内)"
)
else: # 做空,止损放在阻力位上方
# 找到近期波段高点
recent_high = max(high_prices)
# 止损放在高点上方0.5%
buffer = entry_price * 0.005 # 0.5%缓冲
technical_stop = recent_high + buffer
# 如果布林带可用,也可以考虑布林带上轨
if bollinger and bollinger.get('upper'):
bollinger_stop = bollinger['upper'] * 1.005 # 布林带上轨上方0.5%
technical_stop = min(technical_stop, bollinger_stop)
# 技术止损更紧,但需要确保在保证金止损范围内(不能超过保证金止损)
if technical_stop > stop_loss_price and technical_stop <= stop_loss_price_margin:
# 技术止损在合理范围内,可以考虑使用
candidate_prices.append(('技术分析', technical_stop))
logger.debug(
f"技术止损 (SELL): {technical_stop:.4f} "
f"(在保证金止损范围内)"
)
# 重新选择最终的止损价(包括技术止损)
# ⚠️ 优化如果列表中包含ATR止损优先使用ATR止损通常更宽能容忍波动
# 否则使用"更紧"的止损来保护资金
atr_candidate = next((p for p in candidate_prices if p[0] == 'ATR'), None)
if atr_candidate:
final_stop_loss = atr_candidate[1]
selected_method = 'ATR'
else:
if side == 'BUY':
# 做多:选择更高的止损价(更紧)
final_stop_loss = max(p[1] for p in candidate_prices)
selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
else:
# ⚠️ 关键修复:做空必须选择更低的止损价(更接近入场价,更紧)
# 注意对于SELL单止损价高于入场价所以"更低的止损价"意味着更接近入场价
final_stop_loss = min(p[1] for p in candidate_prices)
selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
# ⚠️ 关键修复:验证最终止损价对应的保证金百分比不超过配置值
if side == 'BUY':
final_stop_loss_amount = (entry_price - final_stop_loss) * quantity
else:
final_stop_loss_amount = (final_stop_loss - entry_price) * quantity
final_stop_loss_pct_margin = (final_stop_loss_amount / margin * 100) if margin > 0 else 0
# ⚠️ 优化如果使用的是ATR止损或技术止损允许突破配置的保证金百分比限制更宽可以
# 因为仓位大小已经根据风险进行了调整,所以即使单笔亏损比例大,总亏损金额仍受控
is_atr_or_tech = selected_method in ['ATR', '技术分析']
# 与策略一致ATR/技术止损不得比配置的 STOP_LOSS_PERCENT 更紧(避免低波动时极窄止损被扫)
# 仅用 STOP_LOSS_PERCENT 作为下限,不引入额外参数,避免与 STOP_LOSS_PERCENT/ATR 策略冲突
if is_atr_or_tech and margin > 0 and final_stop_loss_pct_margin < (stop_loss_percent * 100):
atr_pct_before = final_stop_loss_pct_margin
final_stop_loss = stop_loss_price_margin
final_stop_loss_pct_margin = stop_loss_percent * 100
selected_method = 'ATR/技术→保证金下限'
logger.info(
f" ATR/技术止损({atr_pct_before:.2f}% 保证金) 紧于配置 STOP_LOSS_PERCENT({stop_loss_percent*100:.1f}%)"
f"已采用保证金止损以与策略一致"
)
# 如果最终止损价对应的保证金百分比超过配置值,强制使用保证金止损(含 ATR/技术止损)
# 避免 ATR 过宽导致止损 -58%-78% 保证金、长期扛单;与 USE_MARGIN_CAP_FOR_TP 一致
use_margin_cap_sl = bool(config.TRADING_CONFIG.get('USE_MARGIN_CAP_FOR_SL', True))
if final_stop_loss_pct_margin > (stop_loss_percent * 100):
if not is_atr_or_tech:
logger.warning(
f"⚠️ 最终止损价({final_stop_loss:.4f}, 使用{selected_method})对应的保证金百分比({final_stop_loss_pct_margin:.2f}%) "
f"超过配置值({stop_loss_percent*100:.1f}%),强制使用保证金止损({stop_loss_price_margin:.4f})"
)
final_stop_loss = stop_loss_price_margin
selected_method = '保证金(强制)'
final_stop_loss_pct_margin = stop_loss_percent * 100
elif use_margin_cap_sl:
logger.info(
f"止损已按保证金上限封顶: {selected_method}止损({final_stop_loss_pct_margin:.1f}% 保证金) "
f"超过 STOP_LOSS_PERCENT({stop_loss_percent*100:.1f}%),采用保证金止损({stop_loss_price_margin:.4f})"
)
final_stop_loss = stop_loss_price_margin
selected_method = '保证金(止损上限)'
final_stop_loss_pct_margin = stop_loss_percent * 100
else:
logger.info(
f" {selected_method}止损 ({final_stop_loss:.4f}) 超过保证金配置值 "
f"({final_stop_loss_pct_margin:.2f}% > {stop_loss_percent*100:.1f}%)"
f"但予以保留USE_MARGIN_CAP_FOR_SL=False"
)
# ⚠️ 最终强制检查:最小止损距离(防止 -2021 Order would immediately trigger
# 此检查必须在最后执行,覆盖之前的逻辑,因为这是交易所/系统的硬性约束
if stop_loss_price_price is not None:
adjusted = False
if side == 'BUY':
# 做多:止损价必须足够低 (SL <= Entry * (1 - MinDist))
if final_stop_loss > stop_loss_price_price:
logger.warning(
f"⚠️ 止损价({final_stop_loss:.4f})离入场价太近,"
f"强制调整为最小距离止损价({stop_loss_price_price:.4f})"
)
final_stop_loss = stop_loss_price_price
selected_method = '最小距离(强制)'
adjusted = True
else:
# 做空:止损价必须足够高 (SL >= Entry * (1 + MinDist))
if final_stop_loss < stop_loss_price_price:
logger.warning(
f"⚠️ 止损价({final_stop_loss:.4f})离入场价太近,"
f"强制调整为最小距离止损价({stop_loss_price_price:.4f})"
)
final_stop_loss = stop_loss_price_price
selected_method = '最小距离(强制)'
adjusted = True
if adjusted:
# 重新计算保证金百分比用于日志
if side == 'BUY':
final_stop_loss_amount = (entry_price - final_stop_loss) * quantity
else:
final_stop_loss_amount = (final_stop_loss - entry_price) * quantity
final_stop_loss_pct_margin = (final_stop_loss_amount / margin * 100) if margin > 0 else 0
# ⚠️ 最终安全检查:确保止损价不等于入场价,且方向正确
# 如果计算出的止损价无效例如配置为0导致等于入场价强制使用最小安全距离
default_min_dist = 0.005 # 默认0.5%
if side == 'BUY':
if final_stop_loss >= entry_price:
safe_sl = entry_price * (1 - default_min_dist)
logger.warning(
f"⚠️ 计算的止损价({final_stop_loss:.4f}) >= 入场价({entry_price:.4f}) 无效 (BUY)"
f"强制调整为安全止损价: {safe_sl:.4f} (-0.5%)"
)
final_stop_loss = safe_sl
else: # SELL
if final_stop_loss <= entry_price:
safe_sl = entry_price * (1 + default_min_dist)
logger.warning(
f"⚠️ 计算的止损价({final_stop_loss:.4f}) <= 入场价({entry_price:.4f}) 无效 (SELL)"
f"强制调整为安全止损价: {safe_sl:.4f} (+0.5%)"
)
final_stop_loss = safe_sl
logger.info(
f"最终止损 ({side}): {final_stop_loss:.4f} (使用{selected_method}), "
+ (f"ATR={stop_loss_price_atr:.4f}, " if stop_loss_price_atr else "")
+ f"保证金={stop_loss_price_margin:.4f}, "
+ (f"价格={stop_loss_price_price:.4f}, " if stop_loss_price_price else "")
+ (f"技术={technical_stop:.4f}, " if technical_stop else "")
+ f"止损金额={stop_loss_amount:.2f} USDT ({stop_loss_percent*100:.1f}% of margin), "
+ f"实际保证金百分比={final_stop_loss_pct_margin:.2f}%"
)
return final_stop_loss
def get_take_profit_price(
self,
entry_price: float,
side: str,
quantity: float,
leverage: int,
take_profit_pct: Optional[float] = None,
atr: Optional[float] = None,
stop_distance: Optional[float] = None
) -> float:
"""
计算止盈价格基于保证金的盈亏金额支持ATR动态止盈
Args:
entry_price: 入场价格
side: 方向 'BUY''SELL'
quantity: 持仓数量
leverage: 杠杆倍数
take_profit_pct: 止盈百分比相对于保证金如果为None则使用配置值
atr: 平均真实波幅,用于计算动态止盈(可选)
Returns:
止盈价格
"""
# 计算保证金和仓位价值
position_value = entry_price * quantity
margin = position_value / leverage if leverage > 0 else position_value
# 优先使用ATR动态止盈如果启用且ATR可用
# 计算ATR百分比如果提供了ATR绝对值
atr_percent = None
if atr is not None and atr > 0 and entry_price > 0:
atr_percent = atr / entry_price
# 尝试从止损计算中获取止损距离(用于盈亏比计算)
# 如果止损已经计算过,可以使用止损距离来计算止盈
stop_distance_for_rr = None
# 注意这里无法直接获取止损距离需要调用方传递或者使用ATR倍数计算
# 使用ATR策略计算止盈
# 优先使用盈亏比方法基于止损距离如果没有止损距离则使用ATR倍数
take_profit_price_atr, take_profit_distance_atr, atr_tp_details = self.atr_strategy.calculate_take_profit(
entry_price, side, stop_distance, atr, atr_percent,
use_risk_reward_ratio=(stop_distance is not None)
)
if take_profit_price_atr is None:
logger.debug(f"ATR不可用使用固定百分比止盈")
# 获取止盈百分比(相对于保证金)
take_profit_percent = take_profit_pct or config.TRADING_CONFIG['TAKE_PROFIT_PERCENT']
# ⚠️ 关键修复:配置值格式转换(兼容百分比形式和比例形式)
# 如果值>1认为是百分比形式转换为比例形式
if take_profit_percent > 1:
take_profit_percent = take_profit_percent / 100.0
# 计算止盈金额(相对于保证金)
take_profit_amount = margin * take_profit_percent
# 计算基于保证金的止盈价
# 止盈金额 = (止盈价 - 开仓价) × 数量
# 所以:止盈价 = 开仓价 + (止盈金额 / 数量)
if side == 'BUY': # 做多,止盈价高于入场价
take_profit_price_margin = entry_price + (take_profit_amount / quantity)
else: # 做空,止盈价低于入场价
take_profit_price_margin = entry_price - (take_profit_amount / quantity)
# 同时计算基于价格百分比的止盈价(作为最小值保护)
# 获取最小价格变动百分比(如果配置了)
min_price_change_pct = config.TRADING_CONFIG.get('MIN_TAKE_PROFIT_PRICE_PCT', None)
if min_price_change_pct is not None:
# 基于价格百分比的止盈价
if side == 'BUY':
take_profit_price_price = entry_price * (1 + min_price_change_pct)
else:
take_profit_price_price = entry_price * (1 - min_price_change_pct)
else:
take_profit_price_price = None
# ⚠️ 2026-01-29优化优先使用基于止损距离的盈亏比止盈确保达到3:1目标
# 如果ATR止盈是基于止损距离×盈亏比计算的优先使用它确保达到3:1盈亏比
candidate_prices = []
# 检查是否是基于盈亏比计算的通过details判断
is_rr_based = (atr_tp_details and atr_tp_details.get('method') == 'ATR止损距离×盈亏比')
# 优先添加基于止损距离的盈亏比止盈如果可用确保达到3:1目标
if take_profit_price_atr is not None:
if is_rr_based:
candidate_prices.append(('ATR盈亏比', take_profit_price_atr))
else:
# ATR倍数止盈作为备选
candidate_prices.append(('ATR倍数', take_profit_price_atr))
# 添加固定百分比止盈(作为备选)
candidate_prices.append(('保证金', take_profit_price_margin))
if take_profit_price_price is not None:
candidate_prices.append(('价格百分比', take_profit_price_price))
# ⚠️ 2026-01-29优化优先使用基于止损距离的盈亏比止盈确保达到3:1目标
# 若开启 USE_MARGIN_CAP_FOR_TP当盈亏比算出的止盈过远时用「保证金止盈」封顶便于获利离场
use_margin_cap = bool(config.TRADING_CONFIG.get('USE_MARGIN_CAP_FOR_TP', True))
if is_rr_based and take_profit_price_atr is not None:
take_profit_price = take_profit_price_atr
selected_method = 'ATR盈亏比'
if use_margin_cap and take_profit_price_margin is not None:
# 取「更近」的止盈,避免盈亏比止盈过远难以触及
if side == 'BUY':
if take_profit_price_margin < take_profit_price:
take_profit_price = take_profit_price_margin
selected_method = '保证金(止盈上限)'
logger.info(f"止盈已按保证金上限封顶: 原盈亏比止盈过远,采用 TAKE_PROFIT_PERCENT 对应价格")
else:
if take_profit_price_margin > take_profit_price:
take_profit_price = take_profit_price_margin
selected_method = '保证金(止盈上限)'
logger.info(f"止盈已按保证金上限封顶: 原盈亏比止盈过远,采用 TAKE_PROFIT_PERCENT 对应价格")
else:
# 如果没有基于盈亏比的止盈,选择最远的止盈(给利润更多空间,提高盈亏比)
if side == 'BUY':
# 做多:选择更高的止盈价(更远,给利润更多空间)
take_profit_price = max(p[1] for p in candidate_prices)
else:
# 做空:选择更低的止盈价(更远,给利润更多空间)
take_profit_price = min(p[1] for p in candidate_prices)
selected_method = [p[0] for p in candidate_prices if p[1] == take_profit_price][0]
logger.info(
f"止盈计算 ({side}): "
+ (f"ATR={take_profit_price_atr:.4f}, " if take_profit_price_atr else "")
+ f"基于保证金={take_profit_price_margin:.4f}, "
+ (f"基于价格={take_profit_price_price:.4f}, " if take_profit_price_price else "")
+ f"最终止盈={take_profit_price:.4f} (使用{selected_method}"
+ (", 确保3:1盈亏比" if is_rr_based else ", 选择最远止盈")
+ "), "
+ f"止盈金额={take_profit_amount:.4f} USDT ({take_profit_percent*100:.1f}% of margin)"
)
return take_profit_price