diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py index 494ec80..6750829 100644 --- a/backend/api/routes/account.py +++ b/backend/api/routes/account.py @@ -361,7 +361,7 @@ async def get_realtime_account_data(account_id: int = 1): logger.info(f" - API密钥长度: {len(api_key)} 字符") else: logger.warning(" - API密钥为空!") - + logger.info(f" - API密钥Secret存在: {bool(api_secret)}") if api_secret: logger.info(f" - API密钥Secret长度: {len(api_secret)} 字符") @@ -667,7 +667,7 @@ async def fetch_realtime_positions(account_id: int): trading_system_path = project_root / 'trading_system' sys.path.insert(0, str(trading_system_path)) from binance_client import BinanceClient - + client = BinanceClient(api_key=api_key, api_secret=api_secret, testnet=use_testnet) await client.connect() positions = await client.get_open_positions() @@ -758,9 +758,9 @@ async def fetch_realtime_positions(account_id: int): matched = None for db_trade in db_trades: try: - if abs(float(db_trade.get('entry_price', 0)) - entry_price) < 0.01: + if abs(float(db_trade.get('entry_price', 0)) - entry_price) < 0.01: matched = db_trade - break + break except Exception: continue if matched is None: @@ -779,7 +779,7 @@ async def fetch_realtime_positions(account_id: int): id = matched.get('id') except Exception as e: logger.debug(f"获取数据库信息失败: {e}") - + if entry_order_id: try: info = await client.client.futures_get_order(symbol=pos.get('symbol'), orderId=int(entry_order_id)) @@ -819,7 +819,7 @@ async def fetch_realtime_positions(account_id: int): take_profit_2 = tp_prices[1] if take_profit_price is None: take_profit_price = take_profit_1 - + formatted_positions.append({ "id": id, "symbol": pos.get('symbol'), @@ -938,8 +938,8 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) # 兼容旧逻辑:如果原始接口异常,回退到封装方法 if not nonzero_positions: try: - positions = await client.get_open_positions() - position = next((p for p in positions if p['symbol'] == symbol and float(p['positionAmt']) != 0), None) + positions = await client.get_open_positions() + position = next((p for p in positions if p['symbol'] == symbol and float(p['positionAmt']) != 0), None) if position: nonzero_positions = [(float(position["positionAmt"]), {"positionAmt": position["positionAmt"]})] except Exception: @@ -981,7 +981,7 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) "symbol": symbol, "status": "closed" } - + # 获取交易对精度信息,调整数量精度(平仓不要向上补 minQty,避免超过持仓数量) symbol_info = None try: @@ -995,7 +995,7 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) q = float(qty) if not symbol_info: return q - quantity_precision = symbol_info.get('quantityPrecision', 8) + quantity_precision = symbol_info.get('quantityPrecision', 8) step_size = float(symbol_info.get('stepSize', 0) or 0) if step_size and step_size > 0: # 向下取整,避免超过持仓 @@ -1013,7 +1013,7 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) if dual_side is None: if any(isinstance(p, dict) and (p.get("positionSide") in ("LONG", "SHORT")) for _, p in nonzero_positions): dual_side = True - else: + else: dual_side = False logger.info(f"{symbol} 持仓模式: {'HEDGE(对冲)' if dual_side else 'ONE-WAY(单向)'}") @@ -1067,14 +1067,14 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) oid = order.get("orderId") if oid: order_ids.append(oid) - except Exception as order_error: - error_msg = f"{symbol} 平仓失败:下单异常 - {str(order_error)}" - logger.error(error_msg) - logger.error(f" 错误类型: {type(order_error).__name__}") - import traceback - logger.error(f" 完整错误堆栈:\n{traceback.format_exc()}") - raise HTTPException(status_code=500, detail=error_msg) - + except Exception as order_error: + error_msg = f"{symbol} 平仓失败:下单异常 - {str(order_error)}" + logger.error(error_msg) + logger.error(f" 错误类型: {type(order_error).__name__}") + import traceback + logger.error(f" 完整错误堆栈:\n{traceback.format_exc()}") + raise HTTPException(status_code=500, detail=error_msg) + if not orders: raise HTTPException(status_code=400, detail=f"{symbol} 无可平仓的有效仓位(数量调整后为0或无持仓)") @@ -1103,17 +1103,17 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) try: # 1. 获取价格 order_info = await client.client.futures_get_order(symbol=symbol, orderId=oid) - if order_info: + if order_info: p = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0)) if p <= 0 and order_info.get('fills'): - total_qty = 0 - total_value = 0 - for fill in order_info.get('fills', []): - qty = float(fill.get('qty', 0)) - price = float(fill.get('price', 0)) - total_qty += qty - total_value += qty * price - if total_qty > 0: + total_qty = 0 + total_value = 0 + for fill in order_info.get('fills', []): + qty = float(fill.get('qty', 0)) + price = float(fill.get('price', 0)) + total_qty += qty + total_value += qty * price + if total_qty > 0: p = total_value / total_qty if p > 0: exit_prices[oid] = p @@ -1132,9 +1132,9 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) exit_realized_pnls[oid] = total_realized_pnl exit_commissions[oid] = total_commission exit_commission_assets[oid] = "/".join(commission_assets) if commission_assets else None - except Exception as e: + except Exception as e: logger.warning(f"获取订单详情失败 (orderId={oid}): {e}") - + # 兜底:如果无法获取订单价格,使用当前价格 fallback_exit_price = None try: @@ -1150,8 +1150,8 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) used_order_ids = set() for trade in open_trades: try: - entry_price = float(trade['entry_price']) - trade_quantity = float(trade['quantity']) + entry_price = float(trade['entry_price']) + trade_quantity = float(trade['quantity']) except Exception: continue @@ -1171,24 +1171,24 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) exit_price = fallback_exit_price or entry_price # 计算盈亏(数据库侧依旧按名义盈亏;收益率展示用保证金口径在前端/统计里另算) - if trade['side'] == 'BUY': - pnl = (exit_price - entry_price) * trade_quantity - pnl_percent = ((exit_price - entry_price) / entry_price) * 100 - else: - pnl = (entry_price - exit_price) * trade_quantity - pnl_percent = ((entry_price - exit_price) / entry_price) * 100 - - Trade.update_exit( - trade_id=trade['id'], - exit_price=exit_price, - exit_reason='manual', - pnl=pnl, - pnl_percent=pnl_percent, + if trade['side'] == 'BUY': + pnl = (exit_price - entry_price) * trade_quantity + pnl_percent = ((exit_price - entry_price) / entry_price) * 100 + else: + pnl = (entry_price - exit_price) * trade_quantity + pnl_percent = ((entry_price - exit_price) / entry_price) * 100 + + Trade.update_exit( + trade_id=trade['id'], + exit_price=exit_price, + exit_reason='manual', + pnl=pnl, + pnl_percent=pnl_percent, exit_order_id=chosen_oid, realized_pnl=exit_realized_pnls.get(chosen_oid), commission=exit_commissions.get(chosen_oid), commission_asset=exit_commission_assets.get(chosen_oid) - ) + ) logger.info(f"✓ 已更新数据库记录 trade_id={trade['id']} order_id={chosen_oid} (盈亏: {pnl:.2f} USDT, {pnl_percent:.2f}%)") logger.info(f"✓ {symbol} 平仓成功") @@ -1839,8 +1839,8 @@ async def sync_positions( pass if not exit_price or exit_price <= 0: - ticker = await client.get_ticker_24h(symbol) - exit_price = float(ticker['price']) if ticker else entry_price + ticker = await client.get_ticker_24h(symbol) + exit_price = float(ticker['price']) if ticker else entry_price # 计算盈亏 if trade['side'] == 'BUY': @@ -1869,7 +1869,7 @@ async def sync_positions( sync_realized_pnl = sum(float(t.get('realizedPnl', 0)) for t in related) except Exception as fee_err: logger.debug(f"同步 {symbol} 平仓手续费失败: {fee_err}") - + # 更新数据库记录 duration_minutes = None try: diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index addbf26..15c794f 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -412,7 +412,7 @@ async def sync_trades_from_binance( testnet=use_testnet, ) await client.connect() - + # 获取需要同步的 symbol 列表 symbol_list = [] auto_full_sync = False # 是否因 DB 无记录而自动全量 @@ -537,7 +537,7 @@ async def sync_trades_from_binance( # 按时间排序,从旧到新 all_orders.sort(key=lambda x: x.get('time', 0)) - + # 全量或自动全量时,对无法匹配的开仓订单也创建新记录 effective_sync_all = sync_all_symbols or auto_full_sync @@ -571,8 +571,8 @@ async def sync_trades_from_binance( try: # 这是平仓订单(close_orders 已经筛选出 reduceOnly=True 的订单) - # 首先检查是否已经通过订单号同步过(避免重复) - existing_trade = Trade.get_by_exit_order_id(order_id) + # 首先检查是否已经通过订单号同步过(避免重复) + existing_trade = Trade.get_by_exit_order_id(order_id) # 如果已有 exit_order_id 且 exit_reason 不是 sync,说明已完整同步,跳过 if existing_trade and existing_trade.get("exit_order_id") and existing_trade.get("exit_reason") not in (None, "", "sync"): skipped_existing += 1 @@ -608,21 +608,21 @@ async def sync_trades_from_binance( # 如果之前没有 exit_order_id,记录为补全 if not trade.get("exit_order_id") or str(trade.get("exit_order_id")).strip() in ("", "0"): exit_order_id_filled += 1 - - # 计算盈亏 - entry_price = float(trade['entry_price']) - entry_quantity = float(trade['quantity']) - - # 使用实际成交数量(可能部分平仓) - actual_quantity = min(quantity, entry_quantity) - - if trade['side'] == 'BUY': - pnl = (avg_price - entry_price) * actual_quantity - pnl_percent = ((avg_price - entry_price) / entry_price) * 100 - else: # SELL - pnl = (entry_price - avg_price) * actual_quantity - pnl_percent = ((entry_price - avg_price) / entry_price) * 100 - + + # 计算盈亏 + entry_price = float(trade['entry_price']) + entry_quantity = float(trade['quantity']) + + # 使用实际成交数量(可能部分平仓) + actual_quantity = min(quantity, entry_quantity) + + if trade['side'] == 'BUY': + pnl = (avg_price - entry_price) * actual_quantity + pnl_percent = ((avg_price - entry_price) / entry_price) * 100 + else: # SELL + pnl = (entry_price - avg_price) * actual_quantity + pnl_percent = ((entry_price - avg_price) / entry_price) * 100 + # 细分 exit_reason:优先使用币安订单类型,其次用价格接近止损/止盈做兜底 exit_reason = "sync" # 检查订单的 reduceOnly 字段:如果是 true,说明是自动平仓,不应该标记为 manual @@ -698,20 +698,20 @@ async def sync_trades_from_binance( duration_minutes = None # 更新数据库(包含订单号、手续费与实际盈亏) - Trade.update_exit( - trade_id=trade_id, - exit_price=avg_price, + Trade.update_exit( + trade_id=trade_id, + exit_price=avg_price, exit_reason=exit_reason, - pnl=pnl, - pnl_percent=pnl_percent, + pnl=pnl, + pnl_percent=pnl_percent, exit_order_id=order_id, # 保存订单号,确保唯一性 duration_minutes=duration_minutes, exit_time_ts=exit_time_ts, commission=sync_commission, commission_asset=sync_commission_asset, realized_pnl=sync_realized_pnl, - ) - updated_count += 1 + ) + updated_count += 1 logger.info( f"✓ 更新平仓记录: {symbol} (ID: {trade_id}, 订单号: {order_id}, " f"类型: {otype or '-'}, 原因: {exit_reason}, 成交价: {avg_price:.4f})" @@ -754,7 +754,7 @@ async def sync_trades_from_binance( existing_trade = Trade.get_by_entry_order_id(order_id) if existing_trade: # 如果已存在,跳过(开仓订单信息通常已完整) - logger.debug(f"开仓订单 {order_id} 已存在,跳过") + logger.debug(f"开仓订单 {order_id} 已存在,跳过") else: # 如果不存在,尝试查找没有 entry_order_id 的记录并补全,或创建新记录 try: diff --git a/backend/config_manager.py b/backend/config_manager.py index 9932d59..a766869 100644 --- a/backend/config_manager.py +++ b/backend/config_manager.py @@ -369,7 +369,7 @@ class ConfigManager: # 从环境变量获取SSL配置(如果未设置,使用默认值) ssl_cert_reqs = os.getenv('REDIS_SSL_CERT_REQS', 'required') ssl_ca_certs = os.getenv('REDIS_SSL_CA_CERTS', None) - + connection_kwargs['select'] = os.getenv('REDIS_SELECT', 0) if connection_kwargs['select'] is not None: connection_kwargs['select'] = int(connection_kwargs['select']) @@ -652,7 +652,7 @@ class ConfigManager: # 4. 返回默认值 return default - + def set(self, key, value, config_type='string', category='general', description=None): """设置配置(同时更新数据库、Redis缓存和本地缓存)""" # 账号私有:API Key/Secret/Testnet 写入 accounts 表 diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 71de13b..cdaae90 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -267,7 +267,7 @@ class BinanceClient: logger.warning("API密钥Secret已更新,但客户端已连接,需要重新连接才能使用新密钥") except Exception as e: logger.debug(f"从配置管理器刷新API密钥失败: {e},使用现有值") - + # 注意:redis_cache 已在 __init__ 中初始化,这里不需要再次初始化 async def connect(self, timeout: int = None, retries: int = None, requests_params: Dict = None): @@ -282,7 +282,7 @@ class BinanceClient: # 连接前刷新API密钥(确保使用最新值,支持热更新) # 但如果 API 密钥为空(只用于获取公开行情),则跳过 if self.api_key and self.api_secret: - self._refresh_api_credentials() + self._refresh_api_credentials() else: logger.info("BinanceClient: 使用公开 API(无需认证),只能获取行情数据") @@ -322,7 +322,7 @@ class BinanceClient: # 验证API密钥权限(仅当提供了有效的 API key 时) if self.api_key and self.api_secret: - await self._verify_api_permissions() + await self._verify_api_permissions() else: logger.info("✓ 使用公开 API,跳过权限验证(只能获取行情数据)") @@ -702,7 +702,7 @@ class BinanceClient: if self.client: await self.client.close_connection() logger.info("币安客户端已断开连接") - + def _resolve_api_symbol(self, symbol: str) -> str: """ 将显示名(含中文等非 ASCII)解析为交易所 API 使用的英文 symbol。 @@ -755,13 +755,13 @@ class BinanceClient: self._display_to_api_symbol.update(display_to_api) if display_to_api: logger.info(f"已映射 {len(display_to_api)} 个中文/非ASCII交易对到英文 symbol,均可正常下单") - logger.info(f"获取到 {len(usdt_pairs)} 个USDT永续合约交易对") + logger.info(f"获取到 {len(usdt_pairs)} 个USDT永续合约交易对") # 回写 DB 供下次使用 try: await loop.run_in_executor(None, lambda: _save_exchange_info_to_db(exchange_info)) except Exception as e: logger.debug("exchange_info 写入 DB 失败: %s", e) - return usdt_pairs + return usdt_pairs except asyncio.TimeoutError: if attempt < max_retries: @@ -772,9 +772,9 @@ class BinanceClient: logger.error(f"获取交易对失败:{max_retries}次重试后仍然超时") return [] - except BinanceAPIException as e: + except BinanceAPIException as e: logger.error(f"获取交易对失败(API错误): {e}") - return [] + return [] except Exception as e: if attempt < max_retries: @@ -870,7 +870,7 @@ class BinanceClient: from .market_ws_leader import KEY_KLINE_PREFIX shared_key = f"{KEY_KLINE_PREFIX}{symbol.upper()}:{interval.lower()}" # 使用较长的 TTL,因为这是共享缓存,多个账号都会使用 - ttl_map = { + ttl_map = { '1m': 60, '3m': 120, '5m': 180, '15m': 300, '30m': 600, '1h': 900, '2h': 1800, '4h': 3600, '6h': 5400, '8h': 7200, '12h': 10800, '1d': 21600 } @@ -902,10 +902,10 @@ class BinanceClient: """ 获取24小时行情数据(合约市场) 优先从WebSocket缓存读取,其次从Redis缓存读取,最后使用REST API - + Args: symbol: 交易对(支持中文名,会解析为 API symbol) - + Returns: 24小时行情数据 """ @@ -1152,7 +1152,7 @@ class BinanceClient: params["endTime"] = end_time out = await self._futures_data_get("takerlongshortRatio", params) return out if isinstance(out, list) else [] - + async def get_account_balance(self) -> Dict[str, float]: """ 获取U本位合约账户余额 @@ -1199,7 +1199,7 @@ class BinanceClient: except BinanceAPIException as e: error_code = e.code if hasattr(e, 'code') else None error_msg = str(e) - + # 合并成“单条多行日志”,避免日志/Redis 里刷屏 lines = [ "=" * 60, @@ -1280,19 +1280,19 @@ class BinanceClient: skipped_low.append((pos['symbol'], round(notional, 4))) continue open_positions.append({ - 'symbol': pos['symbol'], + 'symbol': pos['symbol'], 'positionAmt': amt, 'entryPrice': entry_price, - 'markPrice': float(pos.get('markPrice', 0)), - 'unRealizedProfit': float(pos['unRealizedProfit']), - 'leverage': int(pos['leverage']) + 'markPrice': float(pos.get('markPrice', 0)), + 'unRealizedProfit': float(pos['unRealizedProfit']), + 'leverage': int(pos['leverage']) }) if skipped_low and logger.isEnabledFor(logging.DEBUG): logger.debug( f"获取持仓: 过滤掉 {len(skipped_low)} 个名义价值 < {min_notional} USDT 的仓位 {skipped_low}," "与仪表板不一致时可设 POSITION_MIN_NOTIONAL_USDT=0 或更小" ) - return open_positions + return open_positions except (asyncio.TimeoutError, BinanceAPIException) as e: last_error = e # 如果是API异常,检查是否是网络相关或服务器错误 @@ -1366,16 +1366,16 @@ class BinanceClient: return [] if last_error: logger.warning(f"获取成交记录失败 {symbol} (已重试 {attempts_made} 次): {_format_exception(last_error)}") - return [] + return [] async def get_symbol_info(self, symbol: str) -> Optional[Dict]: """ 获取交易对的精度和限制信息 优先从 Redis 缓存读取,如果缓存不可用或过期则使用 REST API - + Args: symbol: 交易对(支持中文名,会解析为 API symbol) - + Returns: 交易对信息字典,包含 quantityPrecision, minQty, stepSize 等 """ @@ -1389,7 +1389,7 @@ class BinanceClient: if isinstance(cached, dict) and ("tickSize" not in cached or "pricePrecision" not in cached): logger.info(f"{symbol} symbol_info 缓存缺少 tickSize/pricePrecision,自动刷新一次") else: - return cached + return cached # 2. 降级到进程内存(仅当 Redis 不可用时会有数据) if symbol in self._symbol_info_cache: cached_mem = self._symbol_info_cache[symbol] @@ -1498,7 +1498,7 @@ class BinanceClient: _oldest = next(iter(self._symbol_info_cache), None) if _oldest is not None: self._symbol_info_cache.pop(_oldest, None) - self._symbol_info_cache[symbol] = info + self._symbol_info_cache[symbol] = info logger.debug(f"获取 {symbol} 精度信息: {info}") return info @@ -1821,11 +1821,11 @@ class BinanceClient: current_price = ticker['price'] except Exception as e: logger.debug(f"使用 bookTicker 估算价格失败,回退到 ticker: {e}") - ticker = await self.get_ticker_24h(symbol) - if not ticker: - logger.error(f"无法获取 {symbol} 的价格信息") - return None - current_price = ticker['price'] + ticker = await self.get_ticker_24h(symbol) + if not ticker: + logger.error(f"无法获取 {symbol} 的价格信息") + return None + current_price = ticker['price'] else: current_price = price @@ -1873,8 +1873,8 @@ class BinanceClient: position = positions[0] # 优先使用 API 返回的 leverage,不再限制必须有持仓 leverage_bracket = position.get('leverage') - if leverage_bracket: - current_leverage = int(leverage_bracket) + if leverage_bracket: + current_leverage = int(leverage_bracket) except Exception as e: logger.debug(f"无法获取 {symbol} 的杠杆信息,使用默认值: {current_leverage}x ({e})") @@ -1997,7 +1997,7 @@ class BinanceClient: prefix = (config.TRADING_CONFIG.get('SYSTEM_ORDER_ID_PREFIX') or '').strip() if prefix: order_params['newClientOrderId'] = f"{prefix}_{int(time.time() * 1000)}_{random.randint(0, 0xFFFF):04x}"[:36] - + # 如果是平仓订单,添加 reduceOnly 参数 # 根据币安API文档,reduceOnly 应该是字符串 "true" 或 "false" if reduce_only: @@ -2099,7 +2099,7 @@ class BinanceClient: if reduce_only: logger.warning(f"下单被拒绝 {symbol} {side}: ReduceOnly(-2022)(可能仓位已为0/方向腿不匹配),将由上层做幂等处理") else: - logger.error(f"下单失败 {symbol} {side}: ReduceOnly 订单被拒绝 - {e}") + logger.error(f"下单失败 {symbol} {side}: ReduceOnly 订单被拒绝 - {e}") elif "reduceOnly" in error_msg.lower() or "reduce only" in error_msg.lower(): logger.error(f"下单失败 {symbol} {side}: ReduceOnly 相关错误 - {e}") logger.error(f" 错误码: {error_code}") @@ -2123,11 +2123,11 @@ class BinanceClient: async def cancel_order(self, symbol: str, order_id: int) -> bool: """ 取消订单 - + Args: symbol: 交易对 order_id: 订单ID - + Returns: 是否成功 """ @@ -2172,7 +2172,7 @@ class BinanceClient: return False logger.error(f"取消订单失败: {e}") return False - + # ========================= # Algo Orders(条件单/止盈止损/计划委托) # 说明:币安在 2025-12 后将 USDT-M 合约的 STOP/TP/Trailing 等条件单迁移到 Algo 接口: @@ -2645,11 +2645,11 @@ class BinanceClient: """ 设置杠杆倍数 如果设置失败(比如超过交易对支持的最大杠杆),会自动降低杠杆重试 - + Args: symbol: 交易对 leverage: 杠杆倍数(可为 int 或 float,内部会转为 int) - + Returns: 成功设置的杠杆倍数,如果失败返回 0 """ @@ -2677,8 +2677,8 @@ class BinanceClient: else: logger.error(f"设置杠杆请求超时 ({symbol} {target_leverage}x),已重试 2 次仍失败") return 0 - except BinanceAPIException as e: - error_msg = str(e).lower() + except BinanceAPIException as e: + error_msg = str(e).lower() logger.warning(f"设置杠杆 {target_leverage}x 失败: {e},尝试降低杠杆...") # 如果是 leverage 相关错误,尝试降级 if 'leverage' in error_msg or 'invalid' in error_msg or 'max' in error_msg: @@ -2687,12 +2687,12 @@ class BinanceClient: continue try: await self.client.futures_change_leverage(symbol=symbol, leverage=fallback) - logger.warning( + logger.warning( f"{symbol} 杠杆降级成功: {target_leverage}x -> {fallback}x" ) return fallback except (TimeoutError, asyncio.TimeoutError, BinanceAPIException): - continue + continue logger.error(f"设置杠杆最终失败: {symbol} (目标: {target_leverage}x)") return 0 diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 797535e..b19ac0e 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -287,7 +287,7 @@ class PositionManager: # 判断是否应该交易 if not await self.risk_manager.should_trade(symbol, change_percent): return None - + # 设置杠杆(确保为 int,避免动态杠杆传入 float 导致 API/range 报错) actual_leverage = await self.client.set_leverage(symbol, int(leverage)) @@ -299,7 +299,7 @@ class PositionManager: if actual_leverage != int(leverage): logger.warning(f"{symbol} 杠杆被调整: {int(leverage)}x -> {actual_leverage}x") leverage = actual_leverage - + # 计算仓位大小(传入实际使用的杠杆) # ⚠️ 优化:先估算止损价格,用于固定风险百分比计算 logger.info(f"开始为 {symbol} 计算仓位大小...") @@ -498,9 +498,9 @@ class PositionManager: if DB_AVAILABLE and Trade: try: pending_trade_id = Trade.create( - symbol=symbol, - side=side, - quantity=quantity, + symbol=symbol, + side=side, + quantity=quantity, entry_price=entry_price, leverage=leverage, entry_reason=entry_reason, @@ -603,7 +603,7 @@ class PositionManager: entry_order_id = order.get("orderId") if isinstance(order, dict) else None except Exception: entry_order_id = None - break + break else: logger.info(f"{symbol} [智能入场] 限价超时,但偏离{drift_ratio*100:.2f}%>{max_drift_ratio*100:.2f}%,取消并放弃本次交易") try: @@ -611,7 +611,7 @@ class PositionManager: except Exception: pass self._pending_entry_orders.pop(symbol, None) - return None + return None # 震荡/不允许市价兜底:尝试追价(减小 offset -> 更靠近当前价),但不突破追价上限 try: @@ -626,7 +626,7 @@ class PositionManager: if side == "BUY": cap = initial_limit * (1 + max_drift_ratio) desired = min(desired, cap) - else: + else: cap = initial_limit * (1 - max_drift_ratio) desired = max(desired, cap) @@ -685,13 +685,13 @@ class PositionManager: logger.info(f"{symbol} [开仓] 撤单后发现已成交,继续完善记录 (成交价={actual_entry_price:.4f} 数量={filled_quantity:.4f})") else: self._pending_entry_orders.pop(symbol, None) - return None - + return None + if not actual_entry_price or actual_entry_price <= 0: logger.error(f"{symbol} [开仓] ❌ 无法获取实际成交价格,不保存到数据库") self._pending_entry_orders.pop(symbol, None) return None - + if filled_quantity <= 0: logger.error(f"{symbol} [开仓] ❌ 成交数量为0,不保存到数据库") self._pending_entry_orders.pop(symbol, None) @@ -851,30 +851,30 @@ class PositionManager: logger.info(f"✓ {symbol} 已完善 pending 记录 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})") if trade_id is None: # 无 pending 或未匹配到:走新建(兜底) - logger.info(f"正在保存 {symbol} 交易记录到数据库...") + logger.info(f"正在保存 {symbol} 交易记录到数据库...") fallback_client_order_id = (order.get("clientOrderId") if order else None) or client_order_id # 如果 REST 已获取到 entry_order_id,直接写入;否则留空,等待 WS 推送或后续同步补全 - trade_id = Trade.create( - symbol=symbol, - side=side, + trade_id = Trade.create( + symbol=symbol, + side=side, quantity=quantity, entry_price=entry_price, - leverage=leverage, - entry_reason=entry_reason, + leverage=leverage, + entry_reason=entry_reason, entry_order_id=entry_order_id, # REST 已获取则直接写入 client_order_id=fallback_client_order_id, - stop_loss_price=stop_loss_price, - take_profit_price=take_profit_price, - take_profit_1=take_profit_1, - take_profit_2=take_profit_2, - atr=atr, - notional_usdt=notional_usdt, - margin_usdt=margin_usdt, + stop_loss_price=stop_loss_price, + take_profit_price=take_profit_price, + take_profit_1=take_profit_1, + take_profit_2=take_profit_2, + atr=atr, + notional_usdt=notional_usdt, + margin_usdt=margin_usdt, entry_context=entry_context, account_id=self.account_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: logger.warning(f"⚠ {symbol} 交易记录已保存但 entry_order_id 为空 (ID: {trade_id}),等待 WS 推送或后续同步补全") # 如果有 client_order_id,尝试通过 REST 查询订单号补全 @@ -1065,48 +1065,48 @@ class PositionManager: position_info = self.active_positions[symbol] trade_id = position_info.get('tradeId') if trade_id: - logger.info(f"{symbol} [平仓] 更新数据库状态为已平仓 (ID: {trade_id})...") - ticker = await self.client.get_ticker_24h(symbol) - exit_price = float(ticker['price']) if ticker else float(position_info['entryPrice']) - entry_price = float(position_info['entryPrice']) - quantity = float(position_info['quantity']) - if position_info['side'] == 'BUY': - pnl = (exit_price - entry_price) * quantity - pnl_percent = ((exit_price - entry_price) / entry_price) * 100 - else: - pnl = (entry_price - exit_price) * quantity - pnl_percent = ((entry_price - exit_price) / entry_price) * 100 - entry_time = position_info.get('entryTime') - duration_minutes = None - if entry_time: - try: - if isinstance(entry_time, str): - entry_dt = datetime.strptime(entry_time, '%Y-%m-%d %H:%M:%S') - else: - entry_dt = entry_time + logger.info(f"{symbol} [平仓] 更新数据库状态为已平仓 (ID: {trade_id})...") + ticker = await self.client.get_ticker_24h(symbol) + exit_price = float(ticker['price']) if ticker else float(position_info['entryPrice']) + entry_price = float(position_info['entryPrice']) + quantity = float(position_info['quantity']) + if position_info['side'] == 'BUY': + pnl = (exit_price - entry_price) * quantity + pnl_percent = ((exit_price - entry_price) / entry_price) * 100 + else: + pnl = (entry_price - exit_price) * quantity + pnl_percent = ((entry_price - exit_price) / entry_price) * 100 + entry_time = position_info.get('entryTime') + duration_minutes = None + if entry_time: + try: + if isinstance(entry_time, str): + entry_dt = datetime.strptime(entry_time, '%Y-%m-%d %H:%M:%S') + else: + entry_dt = entry_time exit_dt = get_beijing_time() - duration = exit_dt - entry_dt - duration_minutes = int(duration.total_seconds() / 60) - except Exception as e: - logger.debug(f"计算持仓持续时间失败: {e}") - strategy_type = position_info.get('strategyType', 'trend_following') + duration = exit_dt - entry_dt + duration_minutes = int(duration.total_seconds() / 60) + except Exception as e: + logger.debug(f"计算持仓持续时间失败: {e}") + strategy_type = position_info.get('strategyType', 'trend_following') db_update_retries = 3 for db_attempt in range(db_update_retries): try: - Trade.update_exit( - trade_id=trade_id, - exit_price=exit_price, - exit_reason=reason, - pnl=pnl, - pnl_percent=pnl_percent, + Trade.update_exit( + trade_id=trade_id, + exit_price=exit_price, + exit_reason=reason, + pnl=pnl, + pnl_percent=pnl_percent, exit_order_id=None, - strategy_type=strategy_type, - duration_minutes=duration_minutes - ) - logger.info(f"{symbol} [平仓] ✓ 数据库状态已更新") - updated = True + strategy_type=strategy_type, + duration_minutes=duration_minutes + ) + logger.info(f"{symbol} [平仓] ✓ 数据库状态已更新") + updated = True break - except Exception as e: + except Exception as e: err_msg = str(e).strip() or f"{type(e).__name__}" if db_attempt < db_update_retries - 1: wait_sec = 2 @@ -1192,7 +1192,7 @@ class PositionManager: del self.active_positions[symbol] logger.info(f"{symbol} [平仓] ✓ 平仓完成: {side} {quantity:.4f} (原因: {reason})") return True - else: + else: # place_order 返回 None:可能是 -2022(ReduceOnly rejected)等竞态场景 # 兜底:再查一次实时持仓,如果已经为0,则当作“已平仓”处理,避免刷屏与误判失败 try: @@ -1201,10 +1201,10 @@ class PositionManager: live2 = None if live2 is None or abs(live2) <= 0: logger.warning(f"{symbol} [平仓] 下单返回None,但实时持仓已为0,按已平仓处理(可能竞态/手动平仓)") - await self._stop_position_monitoring(symbol) - if symbol in self.active_positions: - del self.active_positions[symbol] - return True + await self._stop_position_monitoring(symbol) + if symbol in self.active_positions: + del self.active_positions[symbol] + return True logger.error(f"{symbol} [平仓] ❌ 下单返回 None(实时持仓仍存在: {live2}),可能的原因:") logger.error(f" 1. ReduceOnly 被拒绝(-2022)但持仓未同步") @@ -1244,13 +1244,13 @@ class PositionManager: except Exception: amt0 = None if amt0 is not None and abs(amt0) <= 0: - try: - await self._stop_position_monitoring(symbol) + try: + await self._stop_position_monitoring(symbol) except Exception: pass try: - if symbol in self.active_positions: - del self.active_positions[symbol] + if symbol in self.active_positions: + del self.active_positions[symbol] except Exception: pass logger.warning(f"{symbol} [平仓] 异常后检查:币安持仓已为0,已清理本地记录") @@ -2032,7 +2032,7 @@ class PositionManager: logger.warning( f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) else: # 盈利超过阈值后,止损移至保护利润位(基于保证金) # 如果已经部分止盈,使用剩余仓位计算 @@ -2062,7 +2062,7 @@ class PositionManager: logger.warning( f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) else: 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')) @@ -2081,7 +2081,7 @@ class PositionManager: logger.warning( f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) else: # 未部分止盈,使用原始仓位计算;保护金额至少覆盖手续费 protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage)) @@ -2102,7 +2102,7 @@ class PositionManager: logger.warning( f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) else: # 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量 # 注意:对于做空,止损价应该高于开仓价,所以用加法 @@ -2124,8 +2124,8 @@ class PositionManager: logger.warning( f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) - + ) + # 检查止损(使用更新后的止损价,基于保证金收益比) # ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行 stop_loss_raw = position_info.get('stopLoss') @@ -2519,7 +2519,7 @@ class PositionManager: return await reconcile_pending_with_binance(self.client, self.account_id) except ImportError: return 0 - + async def sync_positions_with_binance(self): """ 同步币安实际持仓状态与数据库状态 @@ -2797,7 +2797,7 @@ class PositionManager: # 限制为最近1小时,避免匹配到几天的订单导致"时间倒流" start_time = end_time - (60 * 60 * 1000) logger.warning(f"{symbol} [状态同步] ⚠️ 交易记录缺少入场时间,将搜索范围限制为最近1小时") - + logger.debug( f"{symbol} [状态同步] 获取历史订单: " f"symbol={symbol}, startTime={start_time}, endTime={end_time}" @@ -2895,14 +2895,14 @@ class PositionManager: f"{symbol} [状态同步] ❌ 获取当前价格时KeyError: {key_error}, " f"ticker数据: {ticker if 'ticker' in locals() else 'N/A'}" ) - continue + continue except Exception as ticker_error: logger.warning( f"{symbol} [状态同步] 获取当前价格失败: " f"错误类型={type(ticker_error).__name__}, 错误消息={str(ticker_error)}," f"将保留open状态等待下次同步" ) - continue + continue # 计算盈亏(确保所有值都是float类型,避免Decimal类型问题) try: @@ -2928,12 +2928,12 @@ class PositionManager: pnl_percent = ((entry_price - exit_price) / entry_price) * 100 else: # 降级方案:使用价格差计算 - if trade.get('side') == 'BUY': - pnl = (exit_price - entry_price) * quantity - pnl_percent = ((exit_price - entry_price) / entry_price) * 100 - else: # SELL - pnl = (entry_price - exit_price) * quantity - pnl_percent = ((entry_price - exit_price) / entry_price) * 100 + if trade.get('side') == 'BUY': + pnl = (exit_price - entry_price) * quantity + pnl_percent = ((exit_price - entry_price) / entry_price) * 100 + else: # SELL + pnl = (entry_price - exit_price) * quantity + pnl_percent = ((entry_price - exit_price) / entry_price) * 100 logger.debug( f"{symbol} [状态同步] 盈亏计算: " @@ -3002,7 +3002,7 @@ class PositionManager: # 计算持仓时间和亏损比例(用于特征判断) entry_time = trade.get("entry_time") - duration_minutes = None + duration_minutes = None if entry_time and exit_time_ts: try: duration_minutes = (exit_time_ts - int(entry_time)) / 60.0 @@ -3038,7 +3038,7 @@ class PositionManager: # 检查是否有移动止损标记 if is_reduce_only: exit_reason = "trailing_stop" # 可能是移动止损 - else: + else: exit_reason = "manual" # 可能是手动平仓 else: # 亏损单:检查止损价格匹配 @@ -3140,9 +3140,9 @@ class PositionManager: 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: duration_minutes = int((xt - et) / 60) - except Exception as e: - logger.debug(f"计算持仓持续时间失败: {e}") - + except Exception as e: + logger.debug(f"计算持仓持续时间失败: {e}") + strategy_type = 'trend_following' # 默认策略类型 # ⚠️ 关键修复:使用基于保证金的盈亏百分比更新数据库(与实时监控逻辑一致) @@ -3437,29 +3437,29 @@ class PositionManager: ) elif sync_create_manual: # 为手动开仓的持仓创建数据库记录并启动监控(仅当显式开启且未走上面的「补建系统单」时) - for symbol in missing_in_db: - try: - # 获取币安持仓详情 - binance_position = next( - (p for p in binance_positions if p['symbol'] == symbol), - None - ) - if not binance_position: - continue - - position_amt = binance_position['positionAmt'] - entry_price = binance_position['entryPrice'] - quantity = abs(position_amt) - side = 'BUY' if position_amt > 0 else 'SELL' - + for symbol in missing_in_db: + try: + # 获取币安持仓详情 + binance_position = next( + (p for p in binance_positions if p['symbol'] == symbol), + None + ) + if not binance_position: + continue + + position_amt = binance_position['positionAmt'] + entry_price = binance_position['entryPrice'] + quantity = abs(position_amt) + side = 'BUY' if position_amt > 0 else 'SELL' + notional = (float(entry_price) * float(quantity)) if entry_price and quantity else 0 if notional < 1.0: logger.debug(f"{symbol} [状态同步] 跳过灰尘持仓 (名义 {notional:.4f} USDT < 1),不创建记录") continue - logger.info( - f"{symbol} [状态同步] 检测到手动开仓,创建数据库记录... " - f"({side} {quantity:.4f} @ {entry_price:.4f})" - ) + logger.info( + f"{symbol} [状态同步] 检测到手动开仓,创建数据库记录... " + f"({side} {quantity:.4f} @ {entry_price:.4f})" + ) # 尽量从币安成交取 entry_order_id 与真实开仓时间(limit=100、空时重试一次) entry_order_id = None entry_time_ts = None @@ -3486,82 +3486,82 @@ class PositionManager: except Exception: pass # 创建数据库记录(显式传入 account_id、真实开仓时间) - trade_id = Trade.create( - symbol=symbol, - side=side, - quantity=quantity, - entry_price=entry_price, - leverage=binance_position.get('leverage', 10), - entry_reason='manual_entry', # 标记为手动开仓 + trade_id = Trade.create( + symbol=symbol, + side=side, + quantity=quantity, + entry_price=entry_price, + leverage=binance_position.get('leverage', 10), + entry_reason='manual_entry', # 标记为手动开仓 entry_order_id=entry_order_id, 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, account_id=self.account_id, entry_time=entry_time_ts, - ) - - logger.info(f"{symbol} [状态同步] ✓ 数据库记录已创建 (ID: {trade_id})") - - # 创建本地持仓记录(用于监控) - ticker = await self.client.get_ticker_24h(symbol) - current_price = ticker['price'] if ticker else entry_price - - # 计算止损止盈(基于保证金) - leverage = binance_position.get('leverage', 10) - stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.08) + ) + + logger.info(f"{symbol} [状态同步] ✓ 数据库记录已创建 (ID: {trade_id})") + + # 创建本地持仓记录(用于监控) + ticker = await self.client.get_ticker_24h(symbol) + current_price = ticker['price'] if ticker else entry_price + + # 计算止损止盈(基于保证金) + leverage = binance_position.get('leverage', 10) + 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: 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: take_profit_pct_margin = take_profit_pct_margin / 100.0 - # 如果配置中没有设置止盈,则使用止损的2倍作为默认 - if take_profit_pct_margin is None or take_profit_pct_margin == 0: - take_profit_pct_margin = stop_loss_pct_margin * 2.0 - - stop_loss_price = self.risk_manager.get_stop_loss_price( - entry_price, side, quantity, leverage, - stop_loss_pct=stop_loss_pct_margin - ) - take_profit_price = self.risk_manager.get_take_profit_price( - entry_price, side, quantity, leverage, - take_profit_pct=take_profit_pct_margin - ) - - position_info = { - 'symbol': symbol, - 'side': side, - 'quantity': quantity, - 'entryPrice': entry_price, - 'changePercent': 0, # 手动开仓,无法计算涨跌幅 + # 如果配置中没有设置止盈,则使用止损的2倍作为默认 + if take_profit_pct_margin is None or take_profit_pct_margin == 0: + take_profit_pct_margin = stop_loss_pct_margin * 2.0 + + stop_loss_price = self.risk_manager.get_stop_loss_price( + entry_price, side, quantity, leverage, + stop_loss_pct=stop_loss_pct_margin + ) + take_profit_price = self.risk_manager.get_take_profit_price( + entry_price, side, quantity, leverage, + take_profit_pct=take_profit_pct_margin + ) + + position_info = { + 'symbol': symbol, + 'side': side, + 'quantity': quantity, + 'entryPrice': entry_price, + 'changePercent': 0, # 手动开仓,无法计算涨跌幅 'orderId': entry_order_id, - 'tradeId': trade_id, - 'stopLoss': stop_loss_price, - 'takeProfit': take_profit_price, - 'initialStopLoss': stop_loss_price, - 'leverage': leverage, - 'entryReason': 'manual_entry', + 'tradeId': trade_id, + 'stopLoss': stop_loss_price, + 'takeProfit': take_profit_price, + 'initialStopLoss': stop_loss_price, + 'leverage': leverage, + 'entryReason': 'manual_entry', 'entryTime': entry_time_ts if entry_time_ts is not None else get_beijing_time(), # 真实开仓时间(来自币安成交/订单) - 'atr': None, - 'maxProfit': 0.0, + 'atr': None, + 'maxProfit': 0.0, 'trailingStopActivated': False, 'breakevenStopSet': False - } - - 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: - logger.error(f"{symbol} [状态同步] ❌ 处理手动开仓失败: {e}") - import traceback - logger.error(f" 错误详情:\n{traceback.format_exc()}") + 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: + logger.error(f"{symbol} [状态同步] ❌ 处理手动开仓失败: {e}") + import traceback + logger.error(f" 错误详情:\n{traceback.format_exc()}") # 6. 同步挂单信息 (STOP_MARKET / TAKE_PROFIT_MARKET) if self.active_positions: @@ -3600,7 +3600,7 @@ class PositionManager: logger.warning(f"{symbol} 获取挂单失败: {orders}") except Exception as e: logger.error(f"同步挂单信息失败: {e}") - + logger.info("持仓状态同步完成") except Exception as e: @@ -3685,24 +3685,24 @@ class PositionManager: stop_loss_price = None take_profit_price = 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: 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: 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 if stop_loss_price is None: - stop_loss_price = self.risk_manager.get_stop_loss_price( - entry_price, side, quantity, leverage, + stop_loss_price = self.risk_manager.get_stop_loss_price( + entry_price, side, quantity, leverage, stop_loss_pct=stop_loss_pct_margin or 0.05 - ) + ) if take_profit_price is None: - take_profit_price = self.risk_manager.get_take_profit_price( - entry_price, side, quantity, leverage, - take_profit_pct=take_profit_pct_margin - ) + take_profit_price = self.risk_manager.get_take_profit_price( + entry_price, side, quantity, leverage, + take_profit_pct=take_profit_pct_margin + ) entry_reason = 'manual_entry_temp' if sync_create_manual else 'sync_recovered' position_info = { @@ -3754,7 +3754,7 @@ class PositionManager: except Exception: mp = None 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}") # 重启后立即按当前价做一次保本/移动止损检查并同步,不依赖首条 WS 推送 try: @@ -3829,7 +3829,7 @@ class PositionManager: task = self._monitor_tasks.pop(symbol, None) if task is None: return - + if not task.done(): task.cancel() try: @@ -4084,7 +4084,7 @@ class PositionManager: logger.warning( f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) else: # ⚠️ 优化:如果分步止盈第一目标已触发,移动止损不再更新剩余仓位的止损价 # 原因:分步止盈第一目标触发后,剩余50%仓位止损已移至成本价(保本),等待第二目标 @@ -4095,14 +4095,14 @@ class PositionManager: else: # 盈利超过阈值后,止损移至保护利润位(基于保证金);保护金额至少覆盖手续费 protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage)) - if position_info['side'] == 'BUY': - new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity + if position_info['side'] == 'BUY': + new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity new_stop_loss = max(new_stop_loss, self._breakeven_stop_price(entry_price, 'BUY')) - if new_stop_loss > position_info['stopLoss']: - position_info['stopLoss'] = new_stop_loss - logger.info( + if new_stop_loss > position_info['stopLoss']: + position_info['stopLoss'] = new_stop_loss + logger.info( 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)" ) try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) @@ -4111,15 +4111,15 @@ class PositionManager: logger.warning( f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) - else: # SELL + ) + else: # SELL new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL')) if new_stop_loss > position_info['stopLoss'] and pnl_amount > 0: - position_info['stopLoss'] = new_stop_loss - logger.info( + position_info['stopLoss'] = new_stop_loss + logger.info( 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)" ) try: await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) @@ -4128,7 +4128,7 @@ class PositionManager: logger.warning( f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}", exc_info=False, - ) + ) # 检查止损(基于保证金收益比) # ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行 diff --git a/trading_system/risk_manager.py b/trading_system/risk_manager.py index 8896d0e..fa0ae58 100644 --- a/trading_system/risk_manager.py +++ b/trading_system/risk_manager.py @@ -79,7 +79,7 @@ class RiskManager: # 获取账户余额(优先 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() + balance = await self.client.get_account_balance() available_balance = balance.get('available', 0) if available_balance <= 0: @@ -170,7 +170,7 @@ class RiskManager: # 获取当前持仓(优先 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() + positions = await self.client.get_open_positions() # 计算当前总保证金占用 current_position_values = [] @@ -202,7 +202,7 @@ class RiskManager: # 获取账户余额(优先 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() + balance = await self.client.get_account_balance() total_balance = balance.get('total', 0) available_balance = balance.get('available', 0) @@ -418,7 +418,7 @@ class RiskManager: 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, @@ -453,7 +453,7 @@ class RiskManager: # 获取账户余额(优先 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() + balance = await self.client.get_account_balance() available_balance = balance.get('available', 0) total_balance = balance.get('total', 0) @@ -664,19 +664,19 @@ class RiskManager: 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'] - + 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, + 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}%") - + ) + 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 @@ -790,7 +790,7 @@ class RiskManager: quantity = final_notional_value / current_price if current_price and current_price > 0 else quantity logger.info(f" 仓位放大后超过单笔上限,已截断至 MAX_POSITION_PERCENT 对应保证金") logger.info(f" 仓位放大系数: {position_scale:.2f} -> 最终数量: {quantity:.4f}") - + # 添加最小名义价值检查(0.2 USDT),避免下无意义的小单子 MIN_NOTIONAL_VALUE = 0.2 # 最小名义价值0.2 USDT if final_notional_value < MIN_NOTIONAL_VALUE: @@ -840,7 +840,7 @@ class RiskManager: # 检查是否已有持仓 / 总持仓数量限制(优先 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() + positions = await self.client.get_open_positions() try: max_open = int(config.TRADING_CONFIG.get("MAX_OPEN_POSITIONS", 0) or 0) except Exception: @@ -1090,10 +1090,10 @@ class RiskManager: logger.debug(f"优先使用ATR止损: {stop_loss_price:.4f}, 忽略保证金止损: {stop_loss_price_margin:.4f}") else: # ATR不可用,使用保证金止损和价格百分比止损中"更紧"的一个(保护资金) - if side == 'BUY': + if side == 'BUY': # 做多:取最大值(更高的止损价,更接近入场价) stop_loss_price = max(p[1] for p in candidate_prices if p[0] != 'ATR') - else: + else: # 做空:取最小值(更低的止损价,更接近入场价) stop_loss_price = min(p[1] for p in candidate_prices if p[0] != 'ATR') @@ -1159,15 +1159,15 @@ class RiskManager: final_stop_loss = atr_candidate[1] selected_method = 'ATR' else: - if side == 'BUY': + 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: + 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] + 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': @@ -1272,7 +1272,7 @@ class RiskManager: 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 "") @@ -1392,7 +1392,7 @@ class RiskManager: selected_method = 'ATR盈亏比' if use_margin_cap and take_profit_price_margin is not None: # 取「更近」的止盈,避免盈亏比止盈过远难以触及 - if side == 'BUY': + if side == 'BUY': if take_profit_price_margin < take_profit_price: take_profit_price = take_profit_price_margin selected_method = '保证金(止盈上限)' @@ -1406,10 +1406,10 @@ class RiskManager: # 如果没有基于盈亏比的止盈,选择最远的止盈(给利润更多空间,提高盈亏比) if side == 'BUY': # 做多:选择更高的止盈价(更远,给利润更多空间) - take_profit_price = max(p[1] for p in candidate_prices) - else: + take_profit_price = max(p[1] for p in candidate_prices) + else: # 做空:选择更低的止盈价(更远,给利润更多空间) - take_profit_price = min(p[1] for p in candidate_prices) + 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(