fix(account, binance_client, position_manager, risk_manager): 优化异常处理和代码风格

在多个模块中,增强了异常处理逻辑,确保在调用交易所API时能够正确捕获并记录错误信息。同时,调整了代码缩进和结构,提升了可读性和一致性。这一改动旨在提升系统的稳定性和风险控制能力,确保交易策略的有效性与安全性。
This commit is contained in:
薇薇安 2026-02-26 09:17:34 +08:00
parent c53b67e294
commit ff1d985859
4 changed files with 71 additions and 71 deletions

View File

@ -758,9 +758,9 @@ async def fetch_realtime_positions(account_id: int):
matched = None matched = None
for db_trade in db_trades: for db_trade in db_trades:
try: 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 matched = db_trade
break break
except Exception: except Exception:
continue continue
if matched is None: if matched is None:
@ -938,8 +938,8 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
# 兼容旧逻辑:如果原始接口异常,回退到封装方法 # 兼容旧逻辑:如果原始接口异常,回退到封装方法
if not nonzero_positions: if not nonzero_positions:
try: try:
positions = await client.get_open_positions() positions = await client.get_open_positions()
position = next((p for p in positions if p['symbol'] == symbol and float(p['positionAmt']) != 0), None) position = next((p for p in positions if p['symbol'] == symbol and float(p['positionAmt']) != 0), None)
if position: if position:
nonzero_positions = [(float(position["positionAmt"]), {"positionAmt": position["positionAmt"]})] nonzero_positions = [(float(position["positionAmt"]), {"positionAmt": position["positionAmt"]})]
except Exception: except Exception:
@ -1013,7 +1013,7 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
if dual_side is None: if dual_side is None:
if any(isinstance(p, dict) and (p.get("positionSide") in ("LONG", "SHORT")) for _, p in nonzero_positions): if any(isinstance(p, dict) and (p.get("positionSide") in ("LONG", "SHORT")) for _, p in nonzero_positions):
dual_side = True dual_side = True
else: else:
dual_side = False dual_side = False
logger.info(f"{symbol} 持仓模式: {'HEDGE(对冲)' if dual_side else 'ONE-WAY(单向)'}") logger.info(f"{symbol} 持仓模式: {'HEDGE(对冲)' if dual_side else 'ONE-WAY(单向)'}")
@ -1067,13 +1067,13 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
oid = order.get("orderId") oid = order.get("orderId")
if oid: if oid:
order_ids.append(oid) order_ids.append(oid)
except Exception as order_error: except Exception as order_error:
error_msg = f"{symbol} 平仓失败:下单异常 - {str(order_error)}" error_msg = f"{symbol} 平仓失败:下单异常 - {str(order_error)}"
logger.error(error_msg) logger.error(error_msg)
logger.error(f" 错误类型: {type(order_error).__name__}") logger.error(f" 错误类型: {type(order_error).__name__}")
import traceback import traceback
logger.error(f" 完整错误堆栈:\n{traceback.format_exc()}") logger.error(f" 完整错误堆栈:\n{traceback.format_exc()}")
raise HTTPException(status_code=500, detail=error_msg) raise HTTPException(status_code=500, detail=error_msg)
if not orders: if not orders:
raise HTTPException(status_code=400, detail=f"{symbol} 无可平仓的有效仓位数量调整后为0或无持仓") 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: try:
# 1. 获取价格 # 1. 获取价格
order_info = await client.client.futures_get_order(symbol=symbol, orderId=oid) 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)) p = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0))
if p <= 0 and order_info.get('fills'): if p <= 0 and order_info.get('fills'):
total_qty = 0 total_qty = 0
total_value = 0 total_value = 0
for fill in order_info.get('fills', []): for fill in order_info.get('fills', []):
qty = float(fill.get('qty', 0)) qty = float(fill.get('qty', 0))
price = float(fill.get('price', 0)) price = float(fill.get('price', 0))
total_qty += qty total_qty += qty
total_value += qty * price total_value += qty * price
if total_qty > 0: if total_qty > 0:
p = total_value / total_qty p = total_value / total_qty
if p > 0: if p > 0:
exit_prices[oid] = p exit_prices[oid] = p
@ -1132,7 +1132,7 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
exit_realized_pnls[oid] = total_realized_pnl exit_realized_pnls[oid] = total_realized_pnl
exit_commissions[oid] = total_commission exit_commissions[oid] = total_commission
exit_commission_assets[oid] = "/".join(commission_assets) if commission_assets else None 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}") logger.warning(f"获取订单详情失败 (orderId={oid}): {e}")
# 兜底:如果无法获取订单价格,使用当前价格 # 兜底:如果无法获取订单价格,使用当前价格
@ -1150,8 +1150,8 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
used_order_ids = set() used_order_ids = set()
for trade in open_trades: for trade in open_trades:
try: try:
entry_price = float(trade['entry_price']) entry_price = float(trade['entry_price'])
trade_quantity = float(trade['quantity']) trade_quantity = float(trade['quantity'])
except Exception: except Exception:
continue 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 exit_price = fallback_exit_price or entry_price
# 计算盈亏(数据库侧依旧按名义盈亏;收益率展示用保证金口径在前端/统计里另算) # 计算盈亏(数据库侧依旧按名义盈亏;收益率展示用保证金口径在前端/统计里另算)
if trade['side'] == 'BUY': if trade['side'] == 'BUY':
pnl = (exit_price - entry_price) * trade_quantity pnl = (exit_price - entry_price) * trade_quantity
pnl_percent = ((exit_price - entry_price) / entry_price) * 100 pnl_percent = ((exit_price - entry_price) / entry_price) * 100
else: else:
pnl = (entry_price - exit_price) * trade_quantity pnl = (entry_price - exit_price) * trade_quantity
pnl_percent = ((entry_price - exit_price) / entry_price) * 100 pnl_percent = ((entry_price - exit_price) / entry_price) * 100
Trade.update_exit( Trade.update_exit(
trade_id=trade['id'], trade_id=trade['id'],
exit_price=exit_price, exit_price=exit_price,
exit_reason='manual', exit_reason='manual',
pnl=pnl, pnl=pnl,
pnl_percent=pnl_percent, pnl_percent=pnl_percent,
exit_order_id=chosen_oid, exit_order_id=chosen_oid,
realized_pnl=exit_realized_pnls.get(chosen_oid), realized_pnl=exit_realized_pnls.get(chosen_oid),
commission=exit_commissions.get(chosen_oid), commission=exit_commissions.get(chosen_oid),
commission_asset=exit_commission_assets.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"✓ 已更新数据库记录 trade_id={trade['id']} order_id={chosen_oid} (盈亏: {pnl:.2f} USDT, {pnl_percent:.2f}%)")
logger.info(f"{symbol} 平仓成功") logger.info(f"{symbol} 平仓成功")
@ -1839,8 +1839,8 @@ async def sync_positions(
pass pass
if not exit_price or exit_price <= 0: if not exit_price or exit_price <= 0:
ticker = await client.get_ticker_24h(symbol) ticker = await client.get_ticker_24h(symbol)
exit_price = float(ticker['price']) if ticker else entry_price exit_price = float(ticker['price']) if ticker else entry_price
# 计算盈亏 # 计算盈亏
if trade['side'] == 'BUY': if trade['side'] == 'BUY':

View File

@ -282,7 +282,7 @@ class BinanceClient:
# 连接前刷新API密钥确保使用最新值支持热更新 # 连接前刷新API密钥确保使用最新值支持热更新
# 但如果 API 密钥为空(只用于获取公开行情),则跳过 # 但如果 API 密钥为空(只用于获取公开行情),则跳过
if self.api_key and self.api_secret: if self.api_key and self.api_secret:
self._refresh_api_credentials() self._refresh_api_credentials()
else: else:
logger.info("BinanceClient: 使用公开 API无需认证只能获取行情数据") logger.info("BinanceClient: 使用公开 API无需认证只能获取行情数据")
@ -322,7 +322,7 @@ class BinanceClient:
# 验证API密钥权限仅当提供了有效的 API key 时) # 验证API密钥权限仅当提供了有效的 API key 时)
if self.api_key and self.api_secret: if self.api_key and self.api_secret:
await self._verify_api_permissions() await self._verify_api_permissions()
else: else:
logger.info("✓ 使用公开 API跳过权限验证只能获取行情数据") logger.info("✓ 使用公开 API跳过权限验证只能获取行情数据")
@ -755,13 +755,13 @@ class BinanceClient:
self._display_to_api_symbol.update(display_to_api) self._display_to_api_symbol.update(display_to_api)
if display_to_api: if display_to_api:
logger.info(f"已映射 {len(display_to_api)} 个中文/非ASCII交易对到英文 symbol均可正常下单") logger.info(f"已映射 {len(display_to_api)} 个中文/非ASCII交易对到英文 symbol均可正常下单")
logger.info(f"获取到 {len(usdt_pairs)} 个USDT永续合约交易对") logger.info(f"获取到 {len(usdt_pairs)} 个USDT永续合约交易对")
# 回写 DB 供下次使用 # 回写 DB 供下次使用
try: try:
await loop.run_in_executor(None, lambda: _save_exchange_info_to_db(exchange_info)) await loop.run_in_executor(None, lambda: _save_exchange_info_to_db(exchange_info))
except Exception as e: except Exception as e:
logger.debug("exchange_info 写入 DB 失败: %s", e) logger.debug("exchange_info 写入 DB 失败: %s", e)
return usdt_pairs return usdt_pairs
except asyncio.TimeoutError: except asyncio.TimeoutError:
if attempt < max_retries: if attempt < max_retries:
@ -772,9 +772,9 @@ class BinanceClient:
logger.error(f"获取交易对失败:{max_retries}次重试后仍然超时") logger.error(f"获取交易对失败:{max_retries}次重试后仍然超时")
return [] return []
except BinanceAPIException as e: except BinanceAPIException as e:
logger.error(f"获取交易对失败API错误: {e}") logger.error(f"获取交易对失败API错误: {e}")
return [] return []
except Exception as e: except Exception as e:
if attempt < max_retries: if attempt < max_retries:
@ -870,7 +870,7 @@ class BinanceClient:
from .market_ws_leader import KEY_KLINE_PREFIX from .market_ws_leader import KEY_KLINE_PREFIX
shared_key = f"{KEY_KLINE_PREFIX}{symbol.upper()}:{interval.lower()}" shared_key = f"{KEY_KLINE_PREFIX}{symbol.upper()}:{interval.lower()}"
# 使用较长的 TTL因为这是共享缓存多个账号都会使用 # 使用较长的 TTL因为这是共享缓存多个账号都会使用
ttl_map = { ttl_map = {
'1m': 60, '3m': 120, '5m': 180, '15m': 300, '30m': 600, '1m': 60, '3m': 120, '5m': 180, '15m': 300, '30m': 600,
'1h': 900, '2h': 1800, '4h': 3600, '6h': 5400, '8h': 7200, '12h': 10800, '1d': 21600 '1h': 900, '2h': 1800, '4h': 3600, '6h': 5400, '8h': 7200, '12h': 10800, '1d': 21600
} }
@ -1292,7 +1292,7 @@ class BinanceClient:
f"获取持仓: 过滤掉 {len(skipped_low)} 个名义价值 < {min_notional} USDT 的仓位 {skipped_low}" f"获取持仓: 过滤掉 {len(skipped_low)} 个名义价值 < {min_notional} USDT 的仓位 {skipped_low}"
"与仪表板不一致时可设 POSITION_MIN_NOTIONAL_USDT=0 或更小" "与仪表板不一致时可设 POSITION_MIN_NOTIONAL_USDT=0 或更小"
) )
return open_positions return open_positions
except (asyncio.TimeoutError, BinanceAPIException) as e: except (asyncio.TimeoutError, BinanceAPIException) as e:
last_error = e last_error = e
# 如果是API异常检查是否是网络相关或服务器错误 # 如果是API异常检查是否是网络相关或服务器错误
@ -1389,7 +1389,7 @@ class BinanceClient:
if isinstance(cached, dict) and ("tickSize" not in cached or "pricePrecision" not in cached): if isinstance(cached, dict) and ("tickSize" not in cached or "pricePrecision" not in cached):
logger.info(f"{symbol} symbol_info 缓存缺少 tickSize/pricePrecision自动刷新一次") logger.info(f"{symbol} symbol_info 缓存缺少 tickSize/pricePrecision自动刷新一次")
else: else:
return cached return cached
# 2. 降级到进程内存(仅当 Redis 不可用时会有数据) # 2. 降级到进程内存(仅当 Redis 不可用时会有数据)
if symbol in self._symbol_info_cache: if symbol in self._symbol_info_cache:
cached_mem = self._symbol_info_cache[symbol] cached_mem = self._symbol_info_cache[symbol]
@ -1873,8 +1873,8 @@ class BinanceClient:
position = positions[0] position = positions[0]
# 优先使用 API 返回的 leverage不再限制必须有持仓 # 优先使用 API 返回的 leverage不再限制必须有持仓
leverage_bracket = position.get('leverage') leverage_bracket = position.get('leverage')
if leverage_bracket: if leverage_bracket:
current_leverage = int(leverage_bracket) current_leverage = int(leverage_bracket)
except Exception as e: except Exception as e:
logger.debug(f"无法获取 {symbol} 的杠杆信息,使用默认值: {current_leverage}x ({e})") logger.debug(f"无法获取 {symbol} 的杠杆信息,使用默认值: {current_leverage}x ({e})")
@ -2099,7 +2099,7 @@ class BinanceClient:
if reduce_only: if reduce_only:
logger.warning(f"下单被拒绝 {symbol} {side}: ReduceOnly(-2022)可能仓位已为0/方向腿不匹配),将由上层做幂等处理") logger.warning(f"下单被拒绝 {symbol} {side}: ReduceOnly(-2022)可能仓位已为0/方向腿不匹配),将由上层做幂等处理")
else: 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(): elif "reduceOnly" in error_msg.lower() or "reduce only" in error_msg.lower():
logger.error(f"下单失败 {symbol} {side}: ReduceOnly 相关错误 - {e}") logger.error(f"下单失败 {symbol} {side}: ReduceOnly 相关错误 - {e}")
logger.error(f" 错误码: {error_code}") logger.error(f" 错误码: {error_code}")
@ -2677,8 +2677,8 @@ class BinanceClient:
else: else:
logger.error(f"设置杠杆请求超时 ({symbol} {target_leverage}x),已重试 2 次仍失败") logger.error(f"设置杠杆请求超时 ({symbol} {target_leverage}x),已重试 2 次仍失败")
return 0 return 0
except BinanceAPIException as e: except BinanceAPIException as e:
error_msg = str(e).lower() error_msg = str(e).lower()
logger.warning(f"设置杠杆 {target_leverage}x 失败: {e},尝试降低杠杆...") logger.warning(f"设置杠杆 {target_leverage}x 失败: {e},尝试降低杠杆...")
# 如果是 leverage 相关错误,尝试降级 # 如果是 leverage 相关错误,尝试降级
if 'leverage' in error_msg or 'invalid' in error_msg or 'max' in error_msg: if 'leverage' in error_msg or 'invalid' in error_msg or 'max' in error_msg:
@ -2687,12 +2687,12 @@ class BinanceClient:
continue continue
try: try:
await self.client.futures_change_leverage(symbol=symbol, leverage=fallback) await self.client.futures_change_leverage(symbol=symbol, leverage=fallback)
logger.warning( logger.warning(
f"{symbol} 杠杆降级成功: {target_leverage}x -> {fallback}x" f"{symbol} 杠杆降级成功: {target_leverage}x -> {fallback}x"
) )
return fallback return fallback
except (TimeoutError, asyncio.TimeoutError, BinanceAPIException): except (TimeoutError, asyncio.TimeoutError, BinanceAPIException):
continue continue
logger.error(f"设置杠杆最终失败: {symbol} (目标: {target_leverage}x)") logger.error(f"设置杠杆最终失败: {symbol} (目标: {target_leverage}x)")
return 0 return 0

View File

@ -3563,18 +3563,18 @@ 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,

View File

@ -79,7 +79,7 @@ class RiskManager:
# 获取账户余额(优先 WS 缓存Redis # 获取账户余额(优先 WS 缓存Redis
balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None
if balance is 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) available_balance = balance.get('available', 0)
if available_balance <= 0: if available_balance <= 0:
@ -170,7 +170,7 @@ class RiskManager:
# 获取当前持仓(优先 WS 缓存Redis # 获取当前持仓(优先 WS 缓存Redis
positions = await _get_positions_from_cache(self.client) if _get_stream_instance() else None positions = await _get_positions_from_cache(self.client) if _get_stream_instance() else None
if positions is None: if positions is None:
positions = await self.client.get_open_positions() positions = await self.client.get_open_positions()
# 计算当前总保证金占用 # 计算当前总保证金占用
current_position_values = [] current_position_values = []
@ -202,7 +202,7 @@ class RiskManager:
# 获取账户余额(优先 WS 缓存Redis # 获取账户余额(优先 WS 缓存Redis
balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None
if balance is 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) total_balance = balance.get('total', 0)
available_balance = balance.get('available', 0) available_balance = balance.get('available', 0)
@ -453,7 +453,7 @@ class RiskManager:
# 获取账户余额(优先 WS 缓存Redis # 获取账户余额(优先 WS 缓存Redis
balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None
if balance is 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) available_balance = balance.get('available', 0)
total_balance = balance.get('total', 0) total_balance = balance.get('total', 0)
@ -840,7 +840,7 @@ class RiskManager:
# 检查是否已有持仓 / 总持仓数量限制(优先 WS 缓存) # 检查是否已有持仓 / 总持仓数量限制(优先 WS 缓存)
positions = await _get_positions_from_cache(self.client) if _get_stream_instance() else None positions = await _get_positions_from_cache(self.client) if _get_stream_instance() else None
if positions is None: if positions is None:
positions = await self.client.get_open_positions() positions = await self.client.get_open_positions()
try: try:
max_open = int(config.TRADING_CONFIG.get("MAX_OPEN_POSITIONS", 0) or 0) max_open = int(config.TRADING_CONFIG.get("MAX_OPEN_POSITIONS", 0) or 0)
except Exception: except Exception: