350 lines
14 KiB
Python
350 lines
14 KiB
Python
"""
|
||
仓位管理模块 - 管理持仓和订单
|
||
"""
|
||
import logging
|
||
from typing import Dict, List, Optional
|
||
from binance_client import BinanceClient
|
||
from risk_manager import RiskManager
|
||
import config
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class PositionManager:
|
||
"""仓位管理类"""
|
||
|
||
def __init__(self, client: BinanceClient, risk_manager: RiskManager):
|
||
"""
|
||
初始化仓位管理器
|
||
|
||
Args:
|
||
client: 币安客户端
|
||
risk_manager: 风险管理器
|
||
"""
|
||
self.client = client
|
||
self.risk_manager = risk_manager
|
||
self.active_positions: Dict[str, Dict] = {}
|
||
|
||
async def open_position(
|
||
self,
|
||
symbol: str,
|
||
change_percent: float,
|
||
leverage: int = 10,
|
||
trade_direction: Optional[str] = None,
|
||
entry_reason: str = '',
|
||
atr: Optional[float] = None
|
||
) -> Optional[Dict]:
|
||
"""
|
||
开仓
|
||
|
||
Args:
|
||
symbol: 交易对
|
||
change_percent: 涨跌幅百分比
|
||
leverage: 杠杆倍数
|
||
|
||
Returns:
|
||
订单信息,失败返回None
|
||
"""
|
||
try:
|
||
# 判断是否应该交易
|
||
if not await self.risk_manager.should_trade(symbol, change_percent):
|
||
return None
|
||
|
||
# 设置杠杆
|
||
await self.client.set_leverage(symbol, leverage)
|
||
|
||
# 计算仓位大小
|
||
logger.info(f"开始为 {symbol} 计算仓位大小...")
|
||
quantity = await self.risk_manager.calculate_position_size(
|
||
symbol, change_percent
|
||
)
|
||
|
||
if quantity is None:
|
||
logger.warning(f"❌ {symbol} 仓位计算失败,跳过交易")
|
||
logger.warning(f" 可能原因:")
|
||
logger.warning(f" 1. 账户余额不足")
|
||
logger.warning(f" 2. 单笔仓位超过限制")
|
||
logger.warning(f" 3. 总仓位超过限制")
|
||
logger.warning(f" 4. 无法获取价格数据")
|
||
return None
|
||
|
||
logger.info(f"✓ {symbol} 仓位计算成功: {quantity:.4f}")
|
||
|
||
# 确定交易方向(优先使用技术指标信号)
|
||
if trade_direction:
|
||
side = trade_direction
|
||
else:
|
||
side = 'BUY' if change_percent > 0 else 'SELL'
|
||
|
||
# 获取当前价格
|
||
ticker = await self.client.get_ticker_24h(symbol)
|
||
if not ticker:
|
||
return None
|
||
|
||
entry_price = ticker['price']
|
||
|
||
# 计算动态止损止盈(使用ATR或固定比例)
|
||
if atr and atr > 0:
|
||
# 使用ATR计算动态止损(2倍ATR)
|
||
atr_stop_loss_pct = (atr * 2) / entry_price
|
||
# 限制在合理范围内(1%-5%)
|
||
atr_stop_loss_pct = max(0.01, min(0.05, atr_stop_loss_pct))
|
||
stop_loss_price = self.risk_manager.get_stop_loss_price(
|
||
entry_price, side, stop_loss_pct=atr_stop_loss_pct
|
||
)
|
||
# 止盈为止损的1.5-2倍
|
||
take_profit_pct = atr_stop_loss_pct * 1.8
|
||
take_profit_price = self.risk_manager.get_take_profit_price(
|
||
entry_price, side, take_profit_pct=take_profit_pct
|
||
)
|
||
else:
|
||
# 使用固定止损止盈
|
||
stop_loss_price = self.risk_manager.get_stop_loss_price(entry_price, side)
|
||
take_profit_price = self.risk_manager.get_take_profit_price(entry_price, side)
|
||
|
||
# 下单
|
||
order = await self.client.place_order(
|
||
symbol=symbol,
|
||
side=side,
|
||
quantity=quantity,
|
||
order_type='MARKET'
|
||
)
|
||
|
||
if order:
|
||
# 记录持仓信息(包含动态止损止盈)
|
||
position_info = {
|
||
'symbol': symbol,
|
||
'side': side,
|
||
'quantity': quantity,
|
||
'entryPrice': entry_price,
|
||
'changePercent': change_percent,
|
||
'orderId': order.get('orderId'),
|
||
'stopLoss': stop_loss_price,
|
||
'takeProfit': take_profit_price,
|
||
'initialStopLoss': stop_loss_price, # 初始止损(用于移动止损)
|
||
'leverage': leverage,
|
||
'entryReason': entry_reason,
|
||
'atr': atr,
|
||
'maxProfit': 0.0, # 记录最大盈利(用于移动止损)
|
||
'trailingStopActivated': False # 移动止损是否已激活
|
||
}
|
||
|
||
self.active_positions[symbol] = position_info
|
||
|
||
logger.info(
|
||
f"开仓成功: {symbol} {side} {quantity} @ {entry_price:.4f} "
|
||
f"(涨跌幅: {change_percent:.2f}%)"
|
||
)
|
||
|
||
return position_info
|
||
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.error(f"开仓失败 {symbol}: {e}")
|
||
return None
|
||
|
||
async def close_position(self, symbol: str) -> bool:
|
||
"""
|
||
平仓
|
||
|
||
Args:
|
||
symbol: 交易对
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
try:
|
||
# 获取当前持仓
|
||
positions = await self.client.get_open_positions()
|
||
position = next(
|
||
(p for p in positions if p['symbol'] == symbol),
|
||
None
|
||
)
|
||
|
||
if not position:
|
||
logger.warning(f"{symbol} 没有持仓")
|
||
return False
|
||
|
||
# 确定平仓方向(与开仓相反)
|
||
position_amt = position['positionAmt']
|
||
side = 'SELL' if position_amt > 0 else 'BUY'
|
||
quantity = abs(position_amt)
|
||
|
||
# 平仓
|
||
order = await self.client.place_order(
|
||
symbol=symbol,
|
||
side=side,
|
||
quantity=quantity,
|
||
order_type='MARKET'
|
||
)
|
||
|
||
if order:
|
||
# 移除持仓记录
|
||
if symbol in self.active_positions:
|
||
del self.active_positions[symbol]
|
||
|
||
logger.info(f"平仓成功: {symbol} {side} {quantity}")
|
||
return True
|
||
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error(f"平仓失败 {symbol}: {e}")
|
||
return False
|
||
|
||
async def check_stop_loss_take_profit(self) -> List[str]:
|
||
"""
|
||
检查止损止盈
|
||
|
||
Returns:
|
||
需要平仓的交易对列表
|
||
"""
|
||
closed_positions = []
|
||
|
||
try:
|
||
# 获取当前持仓
|
||
positions = await self.client.get_open_positions()
|
||
position_dict = {p['symbol']: p for p in positions}
|
||
|
||
for symbol, position_info in list(self.active_positions.items()):
|
||
if symbol not in position_dict:
|
||
# 持仓已不存在,移除记录
|
||
del self.active_positions[symbol]
|
||
continue
|
||
|
||
current_position = position_dict[symbol]
|
||
entry_price = position_info['entryPrice']
|
||
# 获取当前标记价格
|
||
current_price = current_position.get('markPrice', 0)
|
||
if current_price == 0:
|
||
# 如果标记价格为0,尝试从ticker获取
|
||
ticker = await self.client.get_ticker_24h(symbol)
|
||
if ticker:
|
||
current_price = ticker['price']
|
||
else:
|
||
current_price = entry_price
|
||
|
||
# 计算当前盈亏
|
||
if position_info['side'] == 'BUY':
|
||
pnl_percent = ((current_price - entry_price) / entry_price) * 100
|
||
else:
|
||
pnl_percent = ((entry_price - current_price) / entry_price) * 100
|
||
|
||
# 更新最大盈利
|
||
if pnl_percent > position_info.get('maxProfit', 0):
|
||
position_info['maxProfit'] = pnl_percent
|
||
|
||
# 移动止损逻辑(盈利后保护利润)
|
||
use_trailing = config.TRADING_CONFIG.get('USE_TRAILING_STOP', True)
|
||
if use_trailing:
|
||
trailing_activation = config.TRADING_CONFIG.get('TRAILING_STOP_ACTIVATION', 0.01)
|
||
trailing_protect = config.TRADING_CONFIG.get('TRAILING_STOP_PROTECT', 0.01)
|
||
|
||
if not position_info.get('trailingStopActivated', False):
|
||
# 盈利超过阈值后,激活移动止损
|
||
if pnl_percent > trailing_activation * 100:
|
||
position_info['trailingStopActivated'] = True
|
||
# 将止损移至成本价(保本)
|
||
position_info['stopLoss'] = entry_price
|
||
logger.info(
|
||
f"{symbol} 移动止损激活: 止损移至成本价 {entry_price:.4f} "
|
||
f"(盈利: {pnl_percent:.2f}%)"
|
||
)
|
||
else:
|
||
# 盈利超过2%后,止损移至保护利润位
|
||
if pnl_percent > 2.0:
|
||
if position_info['side'] == 'BUY':
|
||
new_stop_loss = entry_price * (1 + trailing_protect)
|
||
if new_stop_loss > position_info['stopLoss']:
|
||
position_info['stopLoss'] = new_stop_loss
|
||
logger.info(
|
||
f"{symbol} 移动止损更新: {new_stop_loss:.4f} "
|
||
f"(保护{trailing_protect*100:.1f}%利润)"
|
||
)
|
||
else:
|
||
new_stop_loss = entry_price * (1 - trailing_protect)
|
||
if new_stop_loss < position_info['stopLoss']:
|
||
position_info['stopLoss'] = new_stop_loss
|
||
logger.info(
|
||
f"{symbol} 移动止损更新: {new_stop_loss:.4f} "
|
||
f"(保护{trailing_protect*100:.1f}%利润)"
|
||
)
|
||
|
||
# 检查止损(使用更新后的止损价)
|
||
stop_loss = position_info['stopLoss']
|
||
if position_info['side'] == 'BUY' and current_price <= stop_loss:
|
||
logger.warning(
|
||
f"{symbol} 触发止损: {current_price:.4f} <= {stop_loss:.4f} "
|
||
f"(盈亏: {pnl_percent:.2f}%)"
|
||
)
|
||
if await self.close_position(symbol):
|
||
closed_positions.append(symbol)
|
||
continue
|
||
|
||
if position_info['side'] == 'SELL' and current_price >= stop_loss:
|
||
logger.warning(
|
||
f"{symbol} 触发止损: {current_price:.4f} >= {stop_loss:.4f} "
|
||
f"(盈亏: {pnl_percent:.2f}%)"
|
||
)
|
||
if await self.close_position(symbol):
|
||
closed_positions.append(symbol)
|
||
continue
|
||
|
||
# 检查止盈
|
||
take_profit = position_info['takeProfit']
|
||
if position_info['side'] == 'BUY' and current_price >= take_profit:
|
||
logger.info(
|
||
f"{symbol} 触发止盈: {current_price:.4f} >= {take_profit:.4f} "
|
||
f"(盈亏: {pnl_percent:.2f}%)"
|
||
)
|
||
if await self.close_position(symbol):
|
||
closed_positions.append(symbol)
|
||
continue
|
||
|
||
if position_info['side'] == 'SELL' and current_price <= take_profit:
|
||
logger.info(
|
||
f"{symbol} 触发止盈: {current_price:.4f} <= {take_profit:.4f} "
|
||
f"(盈亏: {pnl_percent:.2f}%)"
|
||
)
|
||
if await self.close_position(symbol):
|
||
closed_positions.append(symbol)
|
||
continue
|
||
|
||
except Exception as e:
|
||
logger.error(f"检查止损止盈失败: {e}")
|
||
|
||
return closed_positions
|
||
|
||
async def get_position_summary(self) -> Dict:
|
||
"""
|
||
获取持仓摘要
|
||
|
||
Returns:
|
||
持仓摘要信息
|
||
"""
|
||
try:
|
||
positions = await self.client.get_open_positions()
|
||
balance = await self.client.get_account_balance()
|
||
|
||
total_pnl = sum(p['unRealizedProfit'] for p in positions)
|
||
|
||
return {
|
||
'totalPositions': len(positions),
|
||
'totalBalance': balance.get('total', 0),
|
||
'availableBalance': balance.get('available', 0),
|
||
'totalPnL': total_pnl,
|
||
'positions': [
|
||
{
|
||
'symbol': p['symbol'],
|
||
'positionAmt': p['positionAmt'],
|
||
'entryPrice': p['entryPrice'],
|
||
'pnl': p['unRealizedProfit']
|
||
}
|
||
for p in positions
|
||
]
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"获取持仓摘要失败: {e}")
|
||
return {}
|