Introduced new configuration options to manage trading activities on Sundays and during night hours. This includes limits on the number of trades on Sundays, minimum signal strength requirements for Sunday trades, and restrictions on opening new positions during specified night hours. Updated relevant backend and frontend components to reflect these changes, enhancing risk control and user awareness of trading conditions.
1502 lines
79 KiB
Python
1502 lines
79 KiB
Python
"""
|
||
风险管理模块 - 严格控制仓位和风险
|
||
"""
|
||
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__)
|
||
|
||
# 可选:优先使用 User Data Stream 缓存(与 position_manager 一致)
|
||
def _get_stream_instance():
|
||
try:
|
||
from .user_data_stream import get_stream_instance
|
||
return get_stream_instance()
|
||
except Exception:
|
||
return None
|
||
|
||
async def _get_balance_from_cache(client: Optional[BinanceClient] = None):
|
||
"""从缓存获取余额(优先 Redis,降级到进程内存)"""
|
||
try:
|
||
from .user_data_stream import get_balance_from_cache
|
||
redis_cache = getattr(client, "redis_cache", None) if client else None
|
||
return await get_balance_from_cache(redis_cache)
|
||
except Exception:
|
||
return None
|
||
|
||
async def _get_positions_from_cache(client: Optional[BinanceClient] = None):
|
||
"""从缓存获取持仓(优先 Redis,降级到进程内存)"""
|
||
try:
|
||
from .user_data_stream import get_positions_from_cache
|
||
redis_cache = getattr(client, "redis_cache", None) if client else None
|
||
min_notional = float(getattr(config, "POSITION_MIN_NOTIONAL_USDT", 1.0) or 1.0)
|
||
return await get_positions_from_cache(min_notional, redis_cache)
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
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} 单笔仓位大小...")
|
||
|
||
# 获取账户余额(优先 WS 缓存,Redis)
|
||
balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None
|
||
if balance is None:
|
||
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_position_pct = config.get_effective_config('MAX_POSITION_PERCENT', 0.20)
|
||
max_margin_value = available_balance * max_position_pct
|
||
min_margin_value = available_balance * config.TRADING_CONFIG['MIN_POSITION_PERCENT']
|
||
max_margin_pct = max_position_pct * 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:
|
||
# 获取当前持仓(优先 WS 缓存,Redis)
|
||
positions = await _get_positions_from_cache(self.client) if _get_stream_instance() else None
|
||
if positions is None:
|
||
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
|
||
|
||
# 获取账户余额(优先 WS 缓存,Redis)
|
||
balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None
|
||
if balance is None:
|
||
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
|
||
|
||
# 币安 -2019:可用保证金不足时直接拒绝,避免下单被拒
|
||
if available_balance is not None and float(available_balance) <= 0:
|
||
logger.warning(
|
||
"可用保证金不足或为负 (available=%.2f USDT),无法开仓,跳过",
|
||
float(available_balance),
|
||
)
|
||
return False
|
||
if available_balance is not None and float(new_position_margin) > float(available_balance):
|
||
logger.warning(
|
||
"新仓位保证金 %.2f USDT > 可用保证金 %.2f USDT,无法开仓",
|
||
float(new_position_margin),
|
||
float(available_balance),
|
||
)
|
||
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 可避免被压到 2–4x 导致单笔盈利过少)
|
||
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} 的仓位大小...")
|
||
|
||
# 获取账户余额(优先 WS 缓存,Redis)
|
||
balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None
|
||
if balance is None:
|
||
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.get_effective_config('MAX_POSITION_PERCENT', 0.20)
|
||
|
||
# ⚠️ 如果启用了固定风险模型,且计算出的仓位合理(风险可控),则放宽 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%仓位(高质量信号)")
|
||
|
||
# 根据涨跌幅调整仓位大小(涨跌幅越大,保证金占比可以适当增加)
|
||
effective_max = config.get_effective_config('MAX_POSITION_PERCENT', 0.20)
|
||
base_position_percent = effective_max * signal_multiplier
|
||
max_position_percent = effective_max * 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.get_effective_config('MAX_POSITION_PERCENT', 0.20)
|
||
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
|
||
|
||
# 基于 7 天统计的 symbol / 小时软黑名单:差标的 & 差时段进一步缩小仓位
|
||
stats_scale = 1.0
|
||
try:
|
||
from datetime import datetime, timezone, timedelta
|
||
from config_manager import GlobalStrategyConfigManager
|
||
|
||
mgr = GlobalStrategyConfigManager()
|
||
stats_symbol = mgr.get("STATS_SYMBOL_FILTERS", {}) or {}
|
||
stats_hour = mgr.get("STATS_HOUR_FILTERS", {}) or {}
|
||
|
||
bl = stats_symbol.get("blacklist") or []
|
||
if symbol and isinstance(bl, list):
|
||
if any((item.get("symbol") or "").upper() == symbol.upper() for item in bl):
|
||
# 软黑名单:默认 0.5 倍仓位
|
||
stats_scale *= 0.5
|
||
|
||
# 小时过滤
|
||
try:
|
||
bj = timezone(timedelta(hours=8))
|
||
h = datetime.now(bj).hour
|
||
hours = stats_hour.get("hours") or []
|
||
for item in hours:
|
||
if int(item.get("hour", -1)) == int(h):
|
||
pf = item.get("position_factor")
|
||
try:
|
||
pf_val = float(pf)
|
||
except (TypeError, ValueError):
|
||
pf_val = 1.0
|
||
# 仅在 <1 时缩小仓位;>1 的“好时段”放大留给 POSITION_SCALE_FACTOR 处理
|
||
if pf_val > 0 and pf_val < 1.0:
|
||
stats_scale *= pf_val
|
||
break
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
stats_scale = 1.0
|
||
|
||
total_scale = position_scale * stats_scale
|
||
if total_scale != 1.0:
|
||
quantity = quantity * total_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.get_effective_config('MAX_POSITION_PERCENT', 0.20)
|
||
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={position_scale:.2f}, STATS_SCALE={stats_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
|
||
|
||
# 检查是否已有持仓 / 总持仓数量限制(优先 WS 缓存)
|
||
positions = await _get_positions_from_cache(self.client) if _get_stream_instance() else None
|
||
if positions is None:
|
||
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
|
||
|
||
try:
|
||
if bool(config.TRADING_CONFIG.get("NIGHT_HOURS_NO_OPEN_ENABLED", False)):
|
||
bj = timezone(timedelta(hours=8))
|
||
now_bj = datetime.now(bj)
|
||
h = now_bj.hour
|
||
wd = now_bj.weekday()
|
||
start_h = int(config.TRADING_CONFIG.get("NIGHT_HOURS_START", 21) or 21)
|
||
end_h = int(config.TRADING_CONFIG.get("NIGHT_HOURS_END", 6) or 6)
|
||
only_sunday = bool(config.TRADING_CONFIG.get("NIGHT_HOURS_ONLY_SUNDAY", True))
|
||
if start_h > end_h:
|
||
in_night = (h >= start_h) or (h < end_h)
|
||
else:
|
||
in_night = (h >= start_h and h < end_h)
|
||
if in_night:
|
||
if only_sunday:
|
||
apply_block = (wd == 5 and h >= start_h) or (wd == 6 and h < end_h)
|
||
else:
|
||
apply_block = True
|
||
if apply_block:
|
||
logger.info(
|
||
f"{symbol} 晚间禁止开仓({'仅周六晚~周日晨' if only_sunday else '每日'},北京{h}时),跳过"
|
||
)
|
||
return False
|
||
except Exception:
|
||
pass
|
||
|
||
# 每日开仓次数限制(Redis 计数;无 Redis 时降级为内存计数)
|
||
try:
|
||
max_daily = int(config.TRADING_CONFIG.get("MAX_DAILY_ENTRIES", 0) or 0)
|
||
bj = timezone(timedelta(hours=8))
|
||
today_bj = datetime.now(bj)
|
||
is_sunday = (today_bj.weekday() == 6) # 0=Mon, 6=Sun
|
||
sunday_max = int(config.TRADING_CONFIG.get("SUNDAY_MAX_OPENS", 0) or 0)
|
||
effective_max = max_daily
|
||
if is_sunday and sunday_max > 0:
|
||
effective_max = min(max_daily, sunday_max)
|
||
except Exception:
|
||
max_daily = 0
|
||
effective_max = 0
|
||
is_sunday = False
|
||
sunday_max = 0
|
||
if max_daily > 0:
|
||
c = await self._get_daily_entries_count()
|
||
if c >= effective_max:
|
||
reason = "(周日限制)" if (is_sunday and sunday_max > 0) else ""
|
||
logger.info(f"{symbol} 今日开仓次数已达上限{reason}:{c}/{effective_max},跳过")
|
||
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次已平仓的交易(多查一次,确保能判断是否连续)
|
||
# 限制 limit 避免单 symbol 拉取全表导致内存暴增(2 CPU 4G 多账号场景)
|
||
need_count = max_consecutive + 10
|
||
recent_trades = Trade.get_all(
|
||
symbol=symbol,
|
||
status='closed',
|
||
account_id=int(os.getenv("ATS_ACCOUNT_ID") or os.getenv("ACCOUNT_ID") or 1),
|
||
limit=min(need_count, 200),
|
||
)
|
||
|
||
# 按平仓时间倒序排序(最新的在前)
|
||
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
|