From e024bf8ebe36d20211a248d9612d0989e4852311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Sun, 15 Feb 2026 09:44:56 +0800 Subject: [PATCH] 1 --- trading_system/config.py | 3 ++ trading_system/position_manager.py | 75 ++++++++++++++++++++---------- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/trading_system/config.py b/trading_system/config.py index 22c96e2..de933dd 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -312,6 +312,9 @@ DEFAULT_TRADING_CONFIG = { # ===== 系统单标识(用于同步时区分本系统开仓 vs 手动开仓)===== # 下单时写入 newClientOrderId = SYSTEM_ORDER_ID_PREFIX_时间戳_随机,同步/补建时根据订单 clientOrderId 前缀判断是否系统单 'SYSTEM_ORDER_ID_PREFIX': 'SYS', + # ===== 仅币安有仓时的监管策略 ===== + # 当 SYNC_CREATE_MANUAL_ENTRY_RECORD=False 时,仍对「仅币安有仓且存在止损/止盈条件单」的持仓接入监控并补建记录(多为系统单) + 'SYNC_MONITOR_BINANCE_POSITIONS_WITH_SLTP': True, } def _get_trading_config(): diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index c3bc7e0..67f9109 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -1293,6 +1293,21 @@ class PositionManager: q = round(q, qty_precision) return max(0.0, q) + async def _symbol_has_sltp_orders(self, symbol: str) -> bool: + """该交易对在交易所是否存在止损/止盈类条件单(用于识别「可能为系统单」的持仓)。""" + try: + for o in (await self.client.get_open_orders(symbol)) or []: + t = str(o.get("type") or "").upper() + if t in ("STOP_MARKET", "TAKE_PROFIT_MARKET", "STOP", "TAKE_PROFIT"): + return True + for o in (await self.client.futures_get_open_algo_orders(symbol, algo_type="CONDITIONAL")) or []: + t = str(o.get("orderType") or o.get("type") or "").upper() + if t in ("STOP_MARKET", "TAKE_PROFIT_MARKET", "STOP", "TAKE_PROFIT"): + return True + except Exception: + pass + return False + async def _ensure_exchange_sltp_orders(self, symbol: str, position_info: Dict, current_price: Optional[float] = None) -> None: """ 在币安侧挂止损/止盈保护单(STOP_MARKET + TAKE_PROFIT_MARKET)。 @@ -2844,22 +2859,6 @@ class PositionManager: sync_recover = config.TRADING_CONFIG.get("SYNC_RECOVER_MISSING_POSITIONS", True) sync_recover_only_has_sltp = config.TRADING_CONFIG.get("SYNC_RECOVER_ONLY_WHEN_HAS_SLTP", True) - def _order_is_sltp(o, type_key="type"): - t = str(o.get(type_key) or o.get("orderType") or "").upper() - return t in ("STOP_MARKET", "TAKE_PROFIT_MARKET", "STOP", "TAKE_PROFIT") - - async def _symbol_has_sltp(sym): - try: - for o in (await self.client.get_open_orders(sym)) or []: - if _order_is_sltp(o, "type"): - return True - for o in (await self.client.futures_get_open_algo_orders(sym, algo_type="CONDITIONAL")) or []: - if _order_is_sltp(o, "orderType"): - return True - except Exception: - pass - return False - if sync_recover: system_order_prefix = (config.TRADING_CONFIG.get("SYSTEM_ORDER_ID_PREFIX") or "").strip() if system_order_prefix: @@ -2907,7 +2906,7 @@ class PositionManager: # 无法获取订单或 cid 为空(历史单/未带前缀)时不视为手动单,继续补建 else: # 未配置前缀时,用「是否有止损/止盈单」区分 - if sync_recover_only_has_sltp and not (await _symbol_has_sltp(symbol)): + if sync_recover_only_has_sltp and not (await self._symbol_has_sltp_orders(symbol)): logger.debug(f" {symbol} 无止损/止盈单,跳过补建") continue if is_clearly_manual: @@ -3146,18 +3145,25 @@ class PositionManager: logger.info(f"本地持仓记录: {len(active_symbols)} 个 ({', '.join(active_symbols) if active_symbols else '无'})") # 仅为本系统已有记录的持仓启动监控;若未开启「同步创建手动开仓记录」,则不为「仅币安有仓」创建临时记录或监控 + # 例外:SYNC_MONITOR_BINANCE_POSITIONS_WITH_SLTP=True 时,对「仅币安有仓且存在止损/止盈单」的视为可监管(多为系统单),补建并监控 only_binance = binance_symbols - active_symbols - if only_binance and not sync_create_manual: - logger.info(f"跳过 {len(only_binance)} 个仅币安持仓的监控(SYNC_CREATE_MANUAL_ENTRY_RECORD=False): {', '.join(only_binance)}") + monitor_binance_with_sltp = config.TRADING_CONFIG.get("SYNC_MONITOR_BINANCE_POSITIONS_WITH_SLTP", True) + if only_binance and not sync_create_manual and not monitor_binance_with_sltp: + logger.info(f"跳过 {len(only_binance)} 个仅币安持仓的监控(SYNC_CREATE_MANUAL_ENTRY_RECORD=False 且 SYNC_MONITOR_BINANCE_POSITIONS_WITH_SLTP=False): {', '.join(only_binance)}") for position in positions: symbol = position['symbol'] if symbol not in self._monitor_tasks: - # 若不在 active_positions 且未开启「同步创建手动开仓记录」,不创建临时记录、不为其启动监控 + # 若不在 active_positions:要么开启「同步创建手动开仓」则全部接入,要么仅对「有止损/止盈单」的接入(视为系统单) if symbol not in self.active_positions: - if not sync_create_manual: + has_sltp = await self._symbol_has_sltp_orders(symbol) if monitor_binance_with_sltp else False + should_create = sync_create_manual or (monitor_binance_with_sltp and has_sltp) + if not should_create: continue - logger.warning(f"{symbol} 在币安有持仓但不在本地记录中,可能是手动开仓,尝试创建记录...") + if sync_create_manual: + logger.warning(f"{symbol} 在币安有持仓但不在本地记录中,可能是手动开仓,尝试创建记录...") + else: + logger.info(f"{symbol} 仅币安有仓且存在止损/止盈单,按系统单接入监控并补建记录") try: entry_price = position.get('entryPrice', 0) position_amt = position['positionAmt'] @@ -3195,6 +3201,7 @@ class PositionManager: take_profit_pct=take_profit_pct_margin ) + entry_reason = 'manual_entry_temp' if sync_create_manual else 'sync_recovered' position_info = { 'symbol': symbol, 'side': side, @@ -3207,13 +3214,33 @@ class PositionManager: 'takeProfit': take_profit_price, 'initialStopLoss': stop_loss_price, 'leverage': leverage, - 'entryReason': 'manual_entry_temp', + 'entryReason': entry_reason, 'atr': None, 'maxProfit': 0.0, 'trailingStopActivated': False } + if not sync_create_manual and DB_AVAILABLE and Trade: + try: + notional = float(entry_price) * quantity + trade_id = Trade.create( + symbol=symbol, + side=side, + quantity=quantity, + entry_price=entry_price, + leverage=leverage, + entry_reason="sync_recovered", + entry_order_id=None, + notional_usdt=notional, + margin_usdt=(notional / leverage) if leverage else None, + account_id=self.account_id, + stop_loss_price=stop_loss_price, + take_profit_price=take_profit_price, + ) + position_info['tradeId'] = trade_id + except Exception as db_e: + logger.debug(f"{symbol} 补建 DB 记录失败(不影响监控): {db_e}") self.active_positions[symbol] = position_info - logger.info(f"{symbol} 已创建临时持仓记录用于监控") + logger.info(f"{symbol} 已创建持仓记录用于监控" + (" (已写入 DB)" if position_info.get("tradeId") else "")) # 也为“现有持仓”补挂交易所保护单(重启/掉线更安全) try: mp = None