This commit is contained in:
薇薇安 2026-02-26 17:32:03 +08:00
parent 99281395c1
commit 83bb687a97

View File

@ -662,7 +662,7 @@ class PositionManager:
entry_order_id = order.get("orderId") if isinstance(order, dict) else None entry_order_id = order.get("orderId") if isinstance(order, dict) else None
except Exception: except Exception:
entry_order_id = None entry_order_id = None
break break
else: else:
logger.info(f"{symbol} [智能入场] 限价超时,但偏离{drift_ratio*100:.2f}%>{max_drift_ratio*100:.2f}%,取消并放弃本次交易") logger.info(f"{symbol} [智能入场] 限价超时,但偏离{drift_ratio*100:.2f}%>{max_drift_ratio*100:.2f}%,取消并放弃本次交易")
try: try:
@ -670,7 +670,7 @@ class PositionManager:
except Exception: except Exception:
pass pass
self._pending_entry_orders.pop(symbol, None) self._pending_entry_orders.pop(symbol, None)
return None return None
# 震荡/不允许市价兜底:尝试追价(减小 offset -> 更靠近当前价),但不突破追价上限 # 震荡/不允许市价兜底:尝试追价(减小 offset -> 更靠近当前价),但不突破追价上限
try: try:
@ -685,7 +685,7 @@ class PositionManager:
if side == "BUY": if side == "BUY":
cap = initial_limit * (1 + max_drift_ratio) cap = initial_limit * (1 + max_drift_ratio)
desired = min(desired, cap) desired = min(desired, cap)
else: else:
cap = initial_limit * (1 - max_drift_ratio) cap = initial_limit * (1 - max_drift_ratio)
desired = max(desired, cap) desired = max(desired, cap)
@ -928,27 +928,27 @@ class PositionManager:
fallback_client_order_id = (order.get("clientOrderId") if order else None) or client_order_id fallback_client_order_id = (order.get("clientOrderId") if order else None) or client_order_id
logger.info(f"[DB] {symbol} 无 pending 记录,新建 open 记录 client_order_id={fallback_client_order_id!r} entry_order_id={entry_order_id}") logger.info(f"[DB] {symbol} 无 pending 记录,新建 open 记录 client_order_id={fallback_client_order_id!r} entry_order_id={entry_order_id}")
# 如果 REST 已获取到 entry_order_id直接写入否则留空等待 WS 推送或后续同步补全 # 如果 REST 已获取到 entry_order_id直接写入否则留空等待 WS 推送或后续同步补全
trade_id = Trade.create( trade_id = Trade.create(
symbol=symbol, symbol=symbol,
side=side, side=side,
quantity=quantity, quantity=quantity,
entry_price=entry_price, entry_price=entry_price,
leverage=leverage, leverage=leverage,
entry_reason=entry_reason, entry_reason=entry_reason,
entry_order_id=entry_order_id, # REST 已获取则直接写入 entry_order_id=entry_order_id, # REST 已获取则直接写入
client_order_id=fallback_client_order_id, client_order_id=fallback_client_order_id,
stop_loss_price=stop_loss_price, stop_loss_price=stop_loss_price,
take_profit_price=take_profit_price, take_profit_price=take_profit_price,
take_profit_1=take_profit_1, take_profit_1=take_profit_1,
take_profit_2=take_profit_2, take_profit_2=take_profit_2,
atr=atr, atr=atr,
notional_usdt=notional_usdt, notional_usdt=notional_usdt,
margin_usdt=margin_usdt, margin_usdt=margin_usdt,
entry_context=entry_context, entry_context=entry_context,
account_id=self.account_id, account_id=self.account_id,
) )
if entry_order_id: if entry_order_id:
logger.info(f"{symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})") logger.info(f"{symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})")
else: else:
logger.warning(f"{symbol} 交易记录已保存但 entry_order_id 为空 (ID: {trade_id}),等待 WS 推送或后续同步补全") logger.warning(f"{symbol} 交易记录已保存但 entry_order_id 为空 (ID: {trade_id}),等待 WS 推送或后续同步补全")
# 如果有 client_order_id尝试通过 REST 查询订单号补全 # 如果有 client_order_id尝试通过 REST 查询订单号补全
@ -1167,20 +1167,20 @@ class PositionManager:
db_update_retries = 3 db_update_retries = 3
for db_attempt in range(db_update_retries): for db_attempt in range(db_update_retries):
try: try:
Trade.update_exit( Trade.update_exit(
trade_id=trade_id, trade_id=trade_id,
exit_price=exit_price, exit_price=exit_price,
exit_reason=reason, exit_reason=reason,
pnl=pnl, pnl=pnl,
pnl_percent=pnl_percent, pnl_percent=pnl_percent,
exit_order_id=None, exit_order_id=None,
strategy_type=strategy_type, strategy_type=strategy_type,
duration_minutes=duration_minutes duration_minutes=duration_minutes
) )
logger.info(f"{symbol} [平仓] ✓ 数据库状态已更新") logger.info(f"{symbol} [平仓] ✓ 数据库状态已更新")
updated = True updated = True
break break
except Exception as e: except Exception as e:
err_msg = str(e).strip() or f"{type(e).__name__}" err_msg = str(e).strip() or f"{type(e).__name__}"
if db_attempt < db_update_retries - 1: if db_attempt < db_update_retries - 1:
wait_sec = 2 wait_sec = 2
@ -1266,7 +1266,7 @@ class PositionManager:
del self.active_positions[symbol] del self.active_positions[symbol]
logger.info(f"{symbol} [平仓] ✓ 平仓完成: {side} {quantity:.4f} (原因: {reason})") logger.info(f"{symbol} [平仓] ✓ 平仓完成: {side} {quantity:.4f} (原因: {reason})")
return True return True
else: else:
# place_order 返回 None可能是 -2022ReduceOnly rejected等竞态场景 # place_order 返回 None可能是 -2022ReduceOnly rejected等竞态场景
# 兜底再查一次实时持仓如果已经为0则当作“已平仓”处理避免刷屏与误判失败 # 兜底再查一次实时持仓如果已经为0则当作“已平仓”处理避免刷屏与误判失败
try: try:
@ -1318,13 +1318,13 @@ class PositionManager:
except Exception: except Exception:
amt0 = None amt0 = None
if amt0 is not None and abs(amt0) <= 0: if amt0 is not None and abs(amt0) <= 0:
try: try:
await self._stop_position_monitoring(symbol) await self._stop_position_monitoring(symbol)
except Exception: except Exception:
pass pass
try: try:
if symbol in self.active_positions: if symbol in self.active_positions:
del self.active_positions[symbol] del self.active_positions[symbol]
except Exception: except Exception:
pass pass
logger.warning(f"{symbol} [平仓] 异常后检查币安持仓已为0已清理本地记录") logger.warning(f"{symbol} [平仓] 异常后检查币安持仓已为0已清理本地记录")
@ -2106,6 +2106,14 @@ class PositionManager:
lock_pct = 0.03 lock_pct = 0.03
if lock_pct and lock_pct > 1: if lock_pct and lock_pct > 1:
lock_pct = lock_pct / 100.0 lock_pct = lock_pct / 100.0
# 诊断:盈利达标时打印一次,便于排查「盈利高却未移止损」
if pnl_percent_margin >= (lock_pct * 100) or pnl_percent_margin > (trailing_activation * 100):
logger.info(
f"{symbol} [定时检查-保本/移动止损] roe={pnl_percent_margin:.2f}% | "
f"保本阈值={lock_pct*100:.0f}% 已设保本={position_info.get('breakevenStopSet', False)} | "
f"移动止损阈值={trailing_activation*100:.0f}% 已激活={position_info.get('trailingStopActivated', False)} | "
f"当前止损={position_info.get('stopLoss')}"
)
if not position_info.get('trailingStopActivated', False): if not position_info.get('trailingStopActivated', False):
# 盈利达一定比例时尽早将止损移至含手续费保本(与实时监控一致) # 盈利达一定比例时尽早将止损移至含手续费保本(与实时监控一致)
@ -2148,7 +2156,7 @@ class PositionManager:
logger.warning( logger.warning(
f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False, exc_info=False,
) )
else: else:
# 盈利超过阈值后,止损移至保护利润位(基于保证金) # 盈利超过阈值后,止损移至保护利润位(基于保证金)
# 如果已经部分止盈,使用剩余仓位计算 # 如果已经部分止盈,使用剩余仓位计算
@ -2181,7 +2189,7 @@ class PositionManager:
logger.warning( logger.warning(
f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False, exc_info=False,
) )
else: else:
new_stop_loss = entry_price + (remaining_pnl - protect_amount) / remaining_quantity new_stop_loss = entry_price + (remaining_pnl - protect_amount) / remaining_quantity
new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL')) new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL'))
@ -2203,7 +2211,7 @@ class PositionManager:
logger.warning( logger.warning(
f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False, exc_info=False,
) )
else: else:
# 未部分止盈,使用原始仓位计算;保护金额至少覆盖手续费 # 未部分止盈,使用原始仓位计算;保护金额至少覆盖手续费
protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage)) protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage))
@ -2227,7 +2235,7 @@ class PositionManager:
logger.warning( logger.warning(
f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False, exc_info=False,
) )
else: else:
# 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量 # 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量
# 注意:对于做空,止损价应该高于开仓价,所以用加法 # 注意:对于做空,止损价应该高于开仓价,所以用加法
@ -2252,7 +2260,7 @@ class PositionManager:
logger.warning( logger.warning(
f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False, exc_info=False,
) )
# 检查止损(使用更新后的止损价,基于保证金收益比) # 检查止损(使用更新后的止损价,基于保证金收益比)
# ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行 # ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行
@ -3023,14 +3031,14 @@ class PositionManager:
f"{symbol} [状态同步] ❌ 获取当前价格时KeyError: {key_error}, " f"{symbol} [状态同步] ❌ 获取当前价格时KeyError: {key_error}, "
f"ticker数据: {ticker if 'ticker' in locals() else 'N/A'}" f"ticker数据: {ticker if 'ticker' in locals() else 'N/A'}"
) )
continue continue
except Exception as ticker_error: except Exception as ticker_error:
logger.warning( logger.warning(
f"{symbol} [状态同步] 获取当前价格失败: " f"{symbol} [状态同步] 获取当前价格失败: "
f"错误类型={type(ticker_error).__name__}, 错误消息={str(ticker_error)}" f"错误类型={type(ticker_error).__name__}, 错误消息={str(ticker_error)}"
f"将保留open状态等待下次同步" f"将保留open状态等待下次同步"
) )
continue continue
# 计算盈亏确保所有值都是float类型避免Decimal类型问题 # 计算盈亏确保所有值都是float类型避免Decimal类型问题
try: try:
@ -3056,12 +3064,12 @@ class PositionManager:
pnl_percent = ((entry_price - exit_price) / entry_price) * 100 pnl_percent = ((entry_price - exit_price) / entry_price) * 100
else: else:
# 降级方案:使用价格差计算 # 降级方案:使用价格差计算
if trade.get('side') == 'BUY': if trade.get('side') == 'BUY':
pnl = (exit_price - entry_price) * quantity pnl = (exit_price - entry_price) * quantity
pnl_percent = ((exit_price - entry_price) / entry_price) * 100 pnl_percent = ((exit_price - entry_price) / entry_price) * 100
else: # SELL else: # SELL
pnl = (entry_price - exit_price) * quantity pnl = (entry_price - exit_price) * quantity
pnl_percent = ((entry_price - exit_price) / entry_price) * 100 pnl_percent = ((entry_price - exit_price) / entry_price) * 100
logger.debug( logger.debug(
f"{symbol} [状态同步] 盈亏计算: " f"{symbol} [状态同步] 盈亏计算: "
@ -3130,7 +3138,7 @@ class PositionManager:
# 计算持仓时间和亏损比例(用于特征判断) # 计算持仓时间和亏损比例(用于特征判断)
entry_time = trade.get("entry_time") entry_time = trade.get("entry_time")
duration_minutes = None duration_minutes = None
if entry_time and exit_time_ts: if entry_time and exit_time_ts:
try: try:
duration_minutes = (exit_time_ts - int(entry_time)) / 60.0 duration_minutes = (exit_time_ts - int(entry_time)) / 60.0
@ -3268,8 +3276,8 @@ class PositionManager:
xt = int(exit_time_ts) if exit_time_ts is not None else int(get_beijing_time().timestamp()) xt = int(exit_time_ts) if exit_time_ts is not None else int(get_beijing_time().timestamp())
if et is not None and xt >= et: if et is not None and xt >= et:
duration_minutes = int((xt - et) / 60) duration_minutes = int((xt - et) / 60)
except Exception as e: except Exception as e:
logger.debug(f"计算持仓持续时间失败: {e}") logger.debug(f"计算持仓持续时间失败: {e}")
strategy_type = 'trend_following' # 默认策略类型 strategy_type = 'trend_following' # 默认策略类型
@ -3624,29 +3632,29 @@ class PositionManager:
) )
elif sync_create_manual: elif sync_create_manual:
# 为手动开仓的持仓创建数据库记录并启动监控(仅当显式开启且未走上面的「补建系统单」时) # 为手动开仓的持仓创建数据库记录并启动监控(仅当显式开启且未走上面的「补建系统单」时)
for symbol in missing_in_db: for symbol in missing_in_db:
try: try:
# 获取币安持仓详情 # 获取币安持仓详情
binance_position = next( binance_position = next(
(p for p in binance_positions if p['symbol'] == symbol), (p for p in binance_positions if p['symbol'] == symbol),
None None
) )
if not binance_position: if not binance_position:
continue continue
position_amt = binance_position['positionAmt'] position_amt = binance_position['positionAmt']
entry_price = binance_position['entryPrice'] entry_price = binance_position['entryPrice']
quantity = abs(position_amt) quantity = abs(position_amt)
side = 'BUY' if position_amt > 0 else 'SELL' side = 'BUY' if position_amt > 0 else 'SELL'
notional = (float(entry_price) * float(quantity)) if entry_price and quantity else 0 notional = (float(entry_price) * float(quantity)) if entry_price and quantity else 0
if notional < 1.0: if notional < 1.0:
logger.debug(f"{symbol} [状态同步] 跳过灰尘持仓 (名义 {notional:.4f} USDT < 1),不创建记录") logger.debug(f"{symbol} [状态同步] 跳过灰尘持仓 (名义 {notional:.4f} USDT < 1),不创建记录")
continue continue
logger.info( logger.info(
f"{symbol} [状态同步] 检测到手动开仓,创建数据库记录... " f"{symbol} [状态同步] 检测到手动开仓,创建数据库记录... "
f"({side} {quantity:.4f} @ {entry_price:.4f})" f"({side} {quantity:.4f} @ {entry_price:.4f})"
) )
# 尽量从币安成交取 entry_order_id 与真实开仓时间limit=100、空时重试一次 # 尽量从币安成交取 entry_order_id 与真实开仓时间limit=100、空时重试一次
entry_order_id = None entry_order_id = None
entry_time_ts = None entry_time_ts = None
@ -3674,13 +3682,13 @@ class PositionManager:
pass pass
# 创建数据库记录(显式传入 account_id、真实开仓时间 # 创建数据库记录(显式传入 account_id、真实开仓时间
try: try:
trade_id = Trade.create( trade_id = Trade.create(
symbol=symbol, symbol=symbol,
side=side, side=side,
quantity=quantity, quantity=quantity,
entry_price=entry_price, entry_price=entry_price,
leverage=binance_position.get('leverage', 10), leverage=binance_position.get('leverage', 10),
entry_reason='manual_entry', # 标记为手动开仓 entry_reason='manual_entry', # 标记为手动开仓
entry_order_id=entry_order_id, entry_order_id=entry_order_id,
notional_usdt=notional, notional_usdt=notional,
margin_usdt=(notional / float(binance_position.get('leverage', 10) or 10)) if float(binance_position.get('leverage', 10) or 0) > 0 else None, margin_usdt=(notional / float(binance_position.get('leverage', 10) or 10)) if float(binance_position.get('leverage', 10) or 0) > 0 else None,
@ -3700,10 +3708,10 @@ class PositionManager:
error_type=type(create_ex).__name__, error_message=str(create_ex) error_type=type(create_ex).__name__, error_message=str(create_ex)
) )
continue continue
# 创建本地持仓记录(用于监控) # 创建本地持仓记录(用于监控)
ticker = await self.client.get_ticker_24h(symbol) ticker = await self.client.get_ticker_24h(symbol)
current_price = ticker['price'] if ticker else entry_price current_price = ticker['price'] if ticker else entry_price
# ---------- 手动开仓补建 SL/TP 顺序:先读交易所 → 决定 SL/TP → 再写 position_info ---------- # ---------- 手动开仓补建 SL/TP 顺序:先读交易所 → 决定 SL/TP → 再写 position_info ----------
sl_from_ex, tp_from_ex = await self._get_sltp_from_exchange(symbol, side) sl_from_ex, tp_from_ex = await self._get_sltp_from_exchange(symbol, side)
breakeven = self._breakeven_stop_price(entry_price, side, None) breakeven = self._breakeven_stop_price(entry_price, side, None)
@ -3715,16 +3723,16 @@ class PositionManager:
use_exchange_sl = True use_exchange_sl = True
# 计算止损止盈(缺失时用 risk_manager 基于保证金) # 计算止损止盈(缺失时用 risk_manager 基于保证金)
leverage = binance_position.get('leverage', 10) leverage = binance_position.get('leverage', 10)
stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.08) stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.08)
if stop_loss_pct_margin is not None and stop_loss_pct_margin > 1: if stop_loss_pct_margin is not None and stop_loss_pct_margin > 1:
stop_loss_pct_margin = stop_loss_pct_margin / 100.0 stop_loss_pct_margin = stop_loss_pct_margin / 100.0
take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.15) take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.15)
if take_profit_pct_margin is not None and take_profit_pct_margin > 1: if take_profit_pct_margin is not None and take_profit_pct_margin > 1:
take_profit_pct_margin = take_profit_pct_margin / 100.0 take_profit_pct_margin = take_profit_pct_margin / 100.0
if take_profit_pct_margin is None or take_profit_pct_margin == 0: if take_profit_pct_margin is None or take_profit_pct_margin == 0:
take_profit_pct_margin = stop_loss_pct_margin * 2.0 take_profit_pct_margin = stop_loss_pct_margin * 2.0
if use_exchange_sl: if use_exchange_sl:
stop_loss_price = sl_from_ex stop_loss_price = sl_from_ex
initial_stop_loss = self.risk_manager.get_stop_loss_price( initial_stop_loss = self.risk_manager.get_stop_loss_price(
@ -3733,52 +3741,52 @@ class PositionManager:
) )
logger.info(f" {symbol} [补建-手动] 使用交易所已有止损(保本/移动sl={stop_loss_price},不覆盖为初始止损 {initial_stop_loss}") logger.info(f" {symbol} [补建-手动] 使用交易所已有止损(保本/移动sl={stop_loss_price},不覆盖为初始止损 {initial_stop_loss}")
else: else:
stop_loss_price = self.risk_manager.get_stop_loss_price( stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage, entry_price, side, quantity, leverage,
stop_loss_pct=stop_loss_pct_margin stop_loss_pct=stop_loss_pct_margin
) )
initial_stop_loss = stop_loss_price initial_stop_loss = stop_loss_price
if tp_from_ex is not None: if tp_from_ex is not None:
take_profit_price = tp_from_ex take_profit_price = tp_from_ex
else: else:
take_profit_price = self.risk_manager.get_take_profit_price( take_profit_price = self.risk_manager.get_take_profit_price(
entry_price, side, quantity, leverage, entry_price, side, quantity, leverage,
take_profit_pct=take_profit_pct_margin take_profit_pct=take_profit_pct_margin
) )
position_info = { position_info = {
'symbol': symbol, 'symbol': symbol,
'side': side, 'side': side,
'quantity': quantity, 'quantity': quantity,
'entryPrice': entry_price, 'entryPrice': entry_price,
'changePercent': 0, 'changePercent': 0,
'orderId': entry_order_id, 'orderId': entry_order_id,
'tradeId': trade_id, 'tradeId': trade_id,
'stopLoss': stop_loss_price, 'stopLoss': stop_loss_price,
'takeProfit': take_profit_price, 'takeProfit': take_profit_price,
'initialStopLoss': initial_stop_loss, 'initialStopLoss': initial_stop_loss,
'leverage': leverage, 'leverage': leverage,
'entryReason': 'manual_entry', 'entryReason': 'manual_entry',
'entryTime': entry_time_ts if entry_time_ts is not None else get_beijing_time(), 'entryTime': entry_time_ts if entry_time_ts is not None else get_beijing_time(),
'atr': None, 'atr': None,
'maxProfit': 0.0, 'maxProfit': 0.0,
'trailingStopActivated': False, 'trailingStopActivated': False,
'breakevenStopSet': use_exchange_sl 'breakevenStopSet': use_exchange_sl
} }
self.active_positions[symbol] = position_info
# 启动WebSocket监控
if self._monitoring_enabled:
await self._start_position_monitoring(symbol)
logger.info(f"[账号{self.account_id}] {symbol} [状态同步] ✓ 已启动实时监控")
logger.info(f"{symbol} [状态同步] ✓ 手动开仓同步完成")
except Exception as e: self.active_positions[symbol] = position_info
logger.error(f"{symbol} [状态同步] ❌ 处理手动开仓失败: {e}")
import traceback # 启动WebSocket监控
logger.error(f" 错误详情:\n{traceback.format_exc()}") if self._monitoring_enabled:
await self._start_position_monitoring(symbol)
logger.info(f"[账号{self.account_id}] {symbol} [状态同步] ✓ 已启动实时监控")
logger.info(f"{symbol} [状态同步] ✓ 手动开仓同步完成")
except Exception as e:
logger.error(f"{symbol} [状态同步] ❌ 处理手动开仓失败: {e}")
import traceback
logger.error(f" 错误详情:\n{traceback.format_exc()}")
# 6. 同步挂单信息 (STOP_MARKET / TAKE_PROFIT_MARKET) # 6. 同步挂单信息 (STOP_MARKET / TAKE_PROFIT_MARKET)
if self.active_positions: if self.active_positions:
@ -3902,24 +3910,24 @@ class PositionManager:
stop_loss_price = None stop_loss_price = None
take_profit_price = None take_profit_price = None
if stop_loss_price is None or take_profit_price is None: if stop_loss_price is None or take_profit_price is None:
stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.08) stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.08)
if stop_loss_pct_margin is not None and stop_loss_pct_margin > 1: if stop_loss_pct_margin is not None and stop_loss_pct_margin > 1:
stop_loss_pct_margin = stop_loss_pct_margin / 100.0 stop_loss_pct_margin = stop_loss_pct_margin / 100.0
take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.15) take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.15)
if take_profit_pct_margin is not None and take_profit_pct_margin > 1: if take_profit_pct_margin is not None and take_profit_pct_margin > 1:
take_profit_pct_margin = take_profit_pct_margin / 100.0 take_profit_pct_margin = take_profit_pct_margin / 100.0
if take_profit_pct_margin is None or take_profit_pct_margin == 0: if take_profit_pct_margin is None or take_profit_pct_margin == 0:
take_profit_pct_margin = (stop_loss_pct_margin or 0.05) * 2.0 take_profit_pct_margin = (stop_loss_pct_margin or 0.05) * 2.0
if stop_loss_price is None: if stop_loss_price is None:
stop_loss_price = self.risk_manager.get_stop_loss_price( stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage, entry_price, side, quantity, leverage,
stop_loss_pct=stop_loss_pct_margin or 0.05 stop_loss_pct=stop_loss_pct_margin or 0.05
) )
if take_profit_price is None: if take_profit_price is None:
take_profit_price = self.risk_manager.get_take_profit_price( take_profit_price = self.risk_manager.get_take_profit_price(
entry_price, side, quantity, leverage, entry_price, side, quantity, leverage,
take_profit_pct=take_profit_pct_margin take_profit_pct=take_profit_pct_margin
) )
entry_reason = 'manual_entry_temp' if sync_create_manual else 'sync_recovered' entry_reason = 'manual_entry_temp' if sync_create_manual else 'sync_recovered'
position_info = { position_info = {
@ -3971,7 +3979,7 @@ class PositionManager:
except Exception: except Exception:
mp = None mp = None
await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=mp or current_price) await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=mp or current_price)
except Exception as e: except Exception as e:
logger.warning(f"{symbol} 补挂币安止盈止损失败(不影响监控): {e}") logger.warning(f"{symbol} 补挂币安止盈止损失败(不影响监控): {e}")
# 重启后立即按当前价做一次保本/移动止损检查并同步,不依赖首条 WS 推送 # 重启后立即按当前价做一次保本/移动止损检查并同步,不依赖首条 WS 推送
try: try:
@ -4247,6 +4255,14 @@ class PositionManager:
lock_pct = 0.03 # 未配置时默认 3% 移至保本 lock_pct = 0.03 # 未配置时默认 3% 移至保本
if lock_pct and lock_pct > 1: if lock_pct and lock_pct > 1:
lock_pct = lock_pct / 100.0 lock_pct = lock_pct / 100.0
# 诊断:盈利达标时打印一次,便于排查「盈利高却未移止损」
if pnl_percent_margin >= (lock_pct * 100) or pnl_percent_margin > (trailing_activation * 100):
logger.info(
f"[账号{self.account_id}] {symbol} [实时监控-保本/移动止损] roe={pnl_percent_margin:.2f}% | "
f"保本阈值={lock_pct*100:.0f}% 已设保本={position_info.get('breakevenStopSet', False)} | "
f"移动止损阈值={trailing_activation*100:.0f}% 已激活={position_info.get('trailingStopActivated', False)} | "
f"当前止损={position_info.get('stopLoss')}"
)
if not position_info.get('trailingStopActivated', False): if not position_info.get('trailingStopActivated', False):
# 盈利达一定比例时尽早将止损移至含手续费保本,避免先盈后亏 # 盈利达一定比例时尽早将止损移至含手续费保本,避免先盈后亏
@ -4307,7 +4323,7 @@ class PositionManager:
logger.warning( logger.warning(
f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False, exc_info=False,
) )
else: else:
# ⚠️ 优化:如果分步止盈第一目标已触发,移动止损不再更新剩余仓位的止损价 # ⚠️ 优化:如果分步止盈第一目标已触发,移动止损不再更新剩余仓位的止损价
# 原因分步止盈第一目标触发后剩余50%仓位止损已移至成本价(保本),等待第二目标 # 原因分步止盈第一目标触发后剩余50%仓位止损已移至成本价(保本),等待第二目标
@ -4318,46 +4334,46 @@ class PositionManager:
else: else:
# 盈利超过阈值后,止损移至保护利润位(基于保证金);保护金额至少覆盖手续费 # 盈利超过阈值后,止损移至保护利润位(基于保证金);保护金额至少覆盖手续费
protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage)) protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage))
if position_info['side'] == 'BUY': if position_info['side'] == 'BUY':
new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity
new_stop_loss = max(new_stop_loss, self._breakeven_stop_price(entry_price, 'BUY')) new_stop_loss = max(new_stop_loss, self._breakeven_stop_price(entry_price, 'BUY'))
if new_stop_loss > position_info['stopLoss']: if new_stop_loss > position_info['stopLoss']:
position_info['stopLoss'] = new_stop_loss position_info['stopLoss'] = new_stop_loss
logger.info( logger.info(
f"[账号{self.account_id}] {symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} " f"[账号{self.account_id}] {symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} "
f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)" f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)"
)
_log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="实时监控")
try:
await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float)
_log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=new_stop_loss, source="实时监控")
logger.info(f"[账号{self.account_id}] {symbol} [实时监控] 已同步移动止损至交易所")
except Exception as sync_e:
_log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=new_stop_loss, error=str(sync_e), source="实时监控")
logger.warning(
f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False,
) )
else: # SELL _log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="实时监控")
new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity try:
new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL')) await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float)
if new_stop_loss > position_info['stopLoss'] and pnl_amount > 0: _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=new_stop_loss, source="实时监控")
position_info['stopLoss'] = new_stop_loss logger.info(f"[账号{self.account_id}] {symbol} [实时监控] 已同步移动止损至交易所")
logger.info( except Exception as sync_e:
f"[账号{self.account_id}] {symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} " _log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=new_stop_loss, error=str(sync_e), source="实时监控")
f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)" logger.warning(
) f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
_log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="实时监控") exc_info=False,
try: )
await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) else: # SELL
_log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=new_stop_loss, source="实时监控") new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity
logger.info(f"[账号{self.account_id}] {symbol} [实时监控] 已同步移动止损至交易所") new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL'))
except Exception as sync_e: if new_stop_loss > position_info['stopLoss'] and pnl_amount > 0:
_log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=new_stop_loss, error=str(sync_e), source="实时监控") position_info['stopLoss'] = new_stop_loss
logger.warning( logger.info(
f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", f"[账号{self.account_id}] {symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} "
exc_info=False, f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)"
) )
_log_trailing_stop_event(self.account_id, symbol, "trailing_update", new_stop_loss=new_stop_loss, source="实时监控")
try:
await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float)
_log_trailing_stop_event(self.account_id, symbol, "trailing_sync_ok", new_stop_loss=new_stop_loss, source="实时监控")
logger.info(f"[账号{self.account_id}] {symbol} [实时监控] 已同步移动止损至交易所")
except Exception as sync_e:
_log_trailing_stop_event(self.account_id, symbol, "trailing_sync_fail", new_stop_loss=new_stop_loss, error=str(sync_e), source="实时监控")
logger.warning(
f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False,
)
# 检查止损(基于保证金收益比) # 检查止损(基于保证金收益比)
# ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行 # ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行