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
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:
@ -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:
@ -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,13 +1067,13 @@ 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,7 +1132,7 @@ 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}")
# 兜底:如果无法获取订单价格,使用当前价格
@ -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,15 +1839,15 @@ 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':
pnl = (exit_price - entry_price) * quantity
else:
pnl = (entry_price - exit_price) * quantity
# 计算基于保证金的盈亏百分比
leverage = float(trade.get('leverage', 10))
entry_value = entry_price * quantity

View File

@ -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跳过权限验证只能获取行情数据")
@ -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
}
@ -1292,7 +1292,7 @@ class BinanceClient:
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异常检查是否是网络相关或服务器错误
@ -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]
@ -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})")
@ -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}")
@ -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

View File

@ -3563,18 +3563,18 @@ class PositionManager:
)
logger.info(f" {symbol} [补建-手动] 使用交易所已有止损(保本/移动sl={stop_loss_price},不覆盖为初始止损 {initial_stop_loss}")
else:
stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage,
stop_loss_pct=stop_loss_pct_margin
)
stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage,
stop_loss_pct=stop_loss_pct_margin
)
initial_stop_loss = stop_loss_price
if tp_from_ex is not None:
take_profit_price = tp_from_ex
else:
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
)
position_info = {
'symbol': symbol,

View File

@ -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)
@ -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)
@ -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: