fix(config_manager, account, trades, position_manager, risk_manager): 清理多余空行并优化代码风格

在多个模块中,移除多余的空行以提升代码可读性,并确保遵循一致的代码风格。此外,优化了部分逻辑的缩进和结构,增强了代码的整洁性和可维护性。这一改动旨在提升代码质量,确保团队协作时的代码一致性。
This commit is contained in:
薇薇安 2026-02-25 22:22:47 +08:00
parent 41b2a21c3d
commit f3ce4d5d11
6 changed files with 347 additions and 347 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:
@ -995,7 +995,7 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
q = float(qty) q = float(qty)
if not symbol_info: if not symbol_info:
return q 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) step_size = float(symbol_info.get('stepSize', 0) or 0)
if step_size and step_size > 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 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

@ -571,8 +571,8 @@ async def sync_trades_from_binance(
try: try:
# 这是平仓订单close_orders 已经筛选出 reduceOnly=True 的订单) # 这是平仓订单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说明已完整同步跳过 # 如果已有 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"): if existing_trade and existing_trade.get("exit_order_id") and existing_trade.get("exit_reason") not in (None, "", "sync"):
skipped_existing += 1 skipped_existing += 1
@ -609,19 +609,19 @@ async def sync_trades_from_binance(
if not trade.get("exit_order_id") or str(trade.get("exit_order_id")).strip() in ("", "0"): if not trade.get("exit_order_id") or str(trade.get("exit_order_id")).strip() in ("", "0"):
exit_order_id_filled += 1 exit_order_id_filled += 1
# 计算盈亏 # 计算盈亏
entry_price = float(trade['entry_price']) entry_price = float(trade['entry_price'])
entry_quantity = float(trade['quantity']) entry_quantity = float(trade['quantity'])
# 使用实际成交数量(可能部分平仓) # 使用实际成交数量(可能部分平仓)
actual_quantity = min(quantity, entry_quantity) actual_quantity = min(quantity, entry_quantity)
if trade['side'] == 'BUY': if trade['side'] == 'BUY':
pnl = (avg_price - entry_price) * actual_quantity pnl = (avg_price - entry_price) * actual_quantity
pnl_percent = ((avg_price - entry_price) / entry_price) * 100 pnl_percent = ((avg_price - entry_price) / entry_price) * 100
else: # SELL else: # SELL
pnl = (entry_price - avg_price) * actual_quantity pnl = (entry_price - avg_price) * actual_quantity
pnl_percent = ((entry_price - avg_price) / entry_price) * 100 pnl_percent = ((entry_price - avg_price) / entry_price) * 100
# 细分 exit_reason优先使用币安订单类型其次用价格接近止损/止盈做兜底 # 细分 exit_reason优先使用币安订单类型其次用价格接近止损/止盈做兜底
exit_reason = "sync" exit_reason = "sync"
@ -698,20 +698,20 @@ async def sync_trades_from_binance(
duration_minutes = None duration_minutes = None
# 更新数据库(包含订单号、手续费与实际盈亏) # 更新数据库(包含订单号、手续费与实际盈亏)
Trade.update_exit( Trade.update_exit(
trade_id=trade_id, trade_id=trade_id,
exit_price=avg_price, exit_price=avg_price,
exit_reason=exit_reason, exit_reason=exit_reason,
pnl=pnl, pnl=pnl,
pnl_percent=pnl_percent, pnl_percent=pnl_percent,
exit_order_id=order_id, # 保存订单号,确保唯一性 exit_order_id=order_id, # 保存订单号,确保唯一性
duration_minutes=duration_minutes, duration_minutes=duration_minutes,
exit_time_ts=exit_time_ts, exit_time_ts=exit_time_ts,
commission=sync_commission, commission=sync_commission,
commission_asset=sync_commission_asset, commission_asset=sync_commission_asset,
realized_pnl=sync_realized_pnl, realized_pnl=sync_realized_pnl,
) )
updated_count += 1 updated_count += 1
logger.info( logger.info(
f"✓ 更新平仓记录: {symbol} (ID: {trade_id}, 订单号: {order_id}, " f"✓ 更新平仓记录: {symbol} (ID: {trade_id}, 订单号: {order_id}, "
f"类型: {otype or '-'}, 原因: {exit_reason}, 成交价: {avg_price:.4f})" 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) existing_trade = Trade.get_by_entry_order_id(order_id)
if existing_trade: if existing_trade:
# 如果已存在,跳过(开仓订单信息通常已完整) # 如果已存在,跳过(开仓订单信息通常已完整)
logger.debug(f"开仓订单 {order_id} 已存在,跳过") logger.debug(f"开仓订单 {order_id} 已存在,跳过")
else: else:
# 如果不存在,尝试查找没有 entry_order_id 的记录并补全,或创建新记录 # 如果不存在,尝试查找没有 entry_order_id 的记录并补全,或创建新记录
try: try:

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
} }
@ -1280,19 +1280,19 @@ class BinanceClient:
skipped_low.append((pos['symbol'], round(notional, 4))) skipped_low.append((pos['symbol'], round(notional, 4)))
continue continue
open_positions.append({ open_positions.append({
'symbol': pos['symbol'], 'symbol': pos['symbol'],
'positionAmt': amt, 'positionAmt': amt,
'entryPrice': entry_price, 'entryPrice': entry_price,
'markPrice': float(pos.get('markPrice', 0)), 'markPrice': float(pos.get('markPrice', 0)),
'unRealizedProfit': float(pos['unRealizedProfit']), 'unRealizedProfit': float(pos['unRealizedProfit']),
'leverage': int(pos['leverage']) 'leverage': int(pos['leverage'])
}) })
if skipped_low and logger.isEnabledFor(logging.DEBUG): if skipped_low and logger.isEnabledFor(logging.DEBUG):
logger.debug( logger.debug(
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异常检查是否是网络相关或服务器错误
@ -1366,7 +1366,7 @@ class BinanceClient:
return [] return []
if last_error: if last_error:
logger.warning(f"获取成交记录失败 {symbol} (已重试 {attempts_made} 次): {_format_exception(last_error)}") logger.warning(f"获取成交记录失败 {symbol} (已重试 {attempts_made} 次): {_format_exception(last_error)}")
return [] return []
async def get_symbol_info(self, symbol: str) -> Optional[Dict]: async def get_symbol_info(self, symbol: str) -> Optional[Dict]:
""" """
@ -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]
@ -1498,7 +1498,7 @@ class BinanceClient:
_oldest = next(iter(self._symbol_info_cache), None) _oldest = next(iter(self._symbol_info_cache), None)
if _oldest is not None: if _oldest is not None:
self._symbol_info_cache.pop(_oldest, 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}") logger.debug(f"获取 {symbol} 精度信息: {info}")
return info return info
@ -1821,11 +1821,11 @@ class BinanceClient:
current_price = ticker['price'] current_price = ticker['price']
except Exception as e: except Exception as e:
logger.debug(f"使用 bookTicker 估算价格失败,回退到 ticker: {e}") logger.debug(f"使用 bookTicker 估算价格失败,回退到 ticker: {e}")
ticker = await self.get_ticker_24h(symbol) ticker = await self.get_ticker_24h(symbol)
if not ticker: if not ticker:
logger.error(f"无法获取 {symbol} 的价格信息") logger.error(f"无法获取 {symbol} 的价格信息")
return None return None
current_price = ticker['price'] current_price = ticker['price']
else: else:
current_price = price current_price = price
@ -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

@ -498,9 +498,9 @@ class PositionManager:
if DB_AVAILABLE and Trade: if DB_AVAILABLE and Trade:
try: try:
pending_trade_id = Trade.create( pending_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,
@ -603,7 +603,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:
@ -611,7 +611,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:
@ -626,7 +626,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)
@ -685,7 +685,7 @@ class PositionManager:
logger.info(f"{symbol} [开仓] 撤单后发现已成交,继续完善记录 (成交价={actual_entry_price:.4f} 数量={filled_quantity:.4f})") logger.info(f"{symbol} [开仓] 撤单后发现已成交,继续完善记录 (成交价={actual_entry_price:.4f} 数量={filled_quantity:.4f})")
else: else:
self._pending_entry_orders.pop(symbol, None) self._pending_entry_orders.pop(symbol, None)
return None return None
if not actual_entry_price or actual_entry_price <= 0: if not actual_entry_price or actual_entry_price <= 0:
logger.error(f"{symbol} [开仓] ❌ 无法获取实际成交价格,不保存到数据库") logger.error(f"{symbol} [开仓] ❌ 无法获取实际成交价格,不保存到数据库")
@ -851,30 +851,30 @@ class PositionManager:
logger.info(f"{symbol} 已完善 pending 记录 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})") logger.info(f"{symbol} 已完善 pending 记录 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})")
if trade_id is None: if trade_id is None:
# 无 pending 或未匹配到:走新建(兜底) # 无 pending 或未匹配到:走新建(兜底)
logger.info(f"正在保存 {symbol} 交易记录到数据库...") logger.info(f"正在保存 {symbol} 交易记录到数据库...")
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
# 如果 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 查询订单号补全
@ -1065,48 +1065,48 @@ class PositionManager:
position_info = self.active_positions[symbol] position_info = self.active_positions[symbol]
trade_id = position_info.get('tradeId') trade_id = position_info.get('tradeId')
if trade_id: if trade_id:
logger.info(f"{symbol} [平仓] 更新数据库状态为已平仓 (ID: {trade_id})...") logger.info(f"{symbol} [平仓] 更新数据库状态为已平仓 (ID: {trade_id})...")
ticker = await self.client.get_ticker_24h(symbol) ticker = await self.client.get_ticker_24h(symbol)
exit_price = float(ticker['price']) if ticker else float(position_info['entryPrice']) exit_price = float(ticker['price']) if ticker else float(position_info['entryPrice'])
entry_price = float(position_info['entryPrice']) entry_price = float(position_info['entryPrice'])
quantity = float(position_info['quantity']) quantity = float(position_info['quantity'])
if position_info['side'] == 'BUY': if position_info['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: else:
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
entry_time = position_info.get('entryTime') entry_time = position_info.get('entryTime')
duration_minutes = None duration_minutes = None
if entry_time: if entry_time:
try: try:
if isinstance(entry_time, str): if isinstance(entry_time, str):
entry_dt = datetime.strptime(entry_time, '%Y-%m-%d %H:%M:%S') entry_dt = datetime.strptime(entry_time, '%Y-%m-%d %H:%M:%S')
else: else:
entry_dt = entry_time entry_dt = entry_time
exit_dt = get_beijing_time() exit_dt = get_beijing_time()
duration = exit_dt - entry_dt duration = exit_dt - entry_dt
duration_minutes = int(duration.total_seconds() / 60) duration_minutes = int(duration.total_seconds() / 60)
except Exception as e: except Exception as e:
logger.debug(f"计算持仓持续时间失败: {e}") logger.debug(f"计算持仓持续时间失败: {e}")
strategy_type = position_info.get('strategyType', 'trend_following') strategy_type = position_info.get('strategyType', 'trend_following')
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
@ -1192,7 +1192,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:
@ -1201,10 +1201,10 @@ class PositionManager:
live2 = None live2 = None
if live2 is None or abs(live2) <= 0: if live2 is None or abs(live2) <= 0:
logger.warning(f"{symbol} [平仓] 下单返回None但实时持仓已为0按已平仓处理可能竞态/手动平仓)") logger.warning(f"{symbol} [平仓] 下单返回None但实时持仓已为0按已平仓处理可能竞态/手动平仓)")
await self._stop_position_monitoring(symbol) await self._stop_position_monitoring(symbol)
if symbol in self.active_positions: if symbol in self.active_positions:
del self.active_positions[symbol] del self.active_positions[symbol]
return True return True
logger.error(f"{symbol} [平仓] ❌ 下单返回 None实时持仓仍存在: {live2}),可能的原因:") logger.error(f"{symbol} [平仓] ❌ 下单返回 None实时持仓仍存在: {live2}),可能的原因:")
logger.error(f" 1. ReduceOnly 被拒绝(-2022但持仓未同步") logger.error(f" 1. ReduceOnly 被拒绝(-2022但持仓未同步")
@ -1244,13 +1244,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已清理本地记录")
@ -2032,7 +2032,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:
# 盈利超过阈值后,止损移至保护利润位(基于保证金) # 盈利超过阈值后,止损移至保护利润位(基于保证金)
# 如果已经部分止盈,使用剩余仓位计算 # 如果已经部分止盈,使用剩余仓位计算
@ -2062,7 +2062,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'))
@ -2081,7 +2081,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))
@ -2102,7 +2102,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:
# 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量 # 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量
# 注意:对于做空,止损价应该高于开仓价,所以用加法 # 注意:对于做空,止损价应该高于开仓价,所以用加法
@ -2124,7 +2124,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,
) )
# 检查止损(使用更新后的止损价,基于保证金收益比) # 检查止损(使用更新后的止损价,基于保证金收益比)
# ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行 # ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行
@ -2895,14 +2895,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:
@ -2928,12 +2928,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} [状态同步] 盈亏计算: "
@ -3002,7 +3002,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
@ -3038,7 +3038,7 @@ class PositionManager:
# 检查是否有移动止损标记 # 检查是否有移动止损标记
if is_reduce_only: if is_reduce_only:
exit_reason = "trailing_stop" # 可能是移动止损 exit_reason = "trailing_stop" # 可能是移动止损
else: else:
exit_reason = "manual" # 可能是手动平仓 exit_reason = "manual" # 可能是手动平仓
else: else:
# 亏损单:检查止损价格匹配 # 亏损单:检查止损价格匹配
@ -3140,8 +3140,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' # 默认策略类型
@ -3437,29 +3437,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
@ -3486,82 +3486,82 @@ class PositionManager:
except Exception: except Exception:
pass pass
# 创建数据库记录(显式传入 account_id、真实开仓时间 # 创建数据库记录(显式传入 account_id、真实开仓时间
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,
account_id=self.account_id, account_id=self.account_id,
entry_time=entry_time_ts, entry_time=entry_time_ts,
) )
logger.info(f"{symbol} [状态同步] ✓ 数据库记录已创建 (ID: {trade_id})") logger.info(f"{symbol} [状态同步] ✓ 数据库记录已创建 (ID: {trade_id})")
# 创建本地持仓记录(用于监控) # 创建本地持仓记录(用于监控)
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
# 计算止损止盈(基于保证金) # 计算止损止盈(基于保证金)
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
# 如果配置中没有设置止盈则使用止损的2倍作为默认 # 如果配置中没有设置止盈则使用止损的2倍作为默认
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
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
) )
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': stop_loss_price, 'initialStopLoss': stop_loss_price,
'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': False 'breakevenStopSet': False
} }
self.active_positions[symbol] = position_info self.active_positions[symbol] = position_info
# 启动WebSocket监控 # 启动WebSocket监控
if self._monitoring_enabled: if self._monitoring_enabled:
await self._start_position_monitoring(symbol) await self._start_position_monitoring(symbol)
logger.info(f"[账号{self.account_id}] {symbol} [状态同步] ✓ 已启动实时监控") logger.info(f"[账号{self.account_id}] {symbol} [状态同步] ✓ 已启动实时监控")
logger.info(f"{symbol} [状态同步] ✓ 手动开仓同步完成") logger.info(f"{symbol} [状态同步] ✓ 手动开仓同步完成")
except Exception as e: except Exception as e:
logger.error(f"{symbol} [状态同步] ❌ 处理手动开仓失败: {e}") logger.error(f"{symbol} [状态同步] ❌ 处理手动开仓失败: {e}")
import traceback import traceback
logger.error(f" 错误详情:\n{traceback.format_exc()}") 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:
@ -3685,24 +3685,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 = {
@ -3754,7 +3754,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:
@ -4084,7 +4084,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%仓位止损已移至成本价(保本),等待第二目标
@ -4095,14 +4095,14 @@ 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)"
) )
try: try:
await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float)
@ -4111,15 +4111,15 @@ 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: # SELL else: # SELL
new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity new_stop_loss = entry_price + (pnl_amount - protect_amount) / 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'))
if new_stop_loss > position_info['stopLoss'] and pnl_amount > 0: if new_stop_loss > position_info['stopLoss'] and pnl_amount > 0:
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)"
) )
try: try:
await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float) await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float)
@ -4128,7 +4128,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,
) )
# 检查止损(基于保证金收益比) # 检查止损(基于保证金收益比)
# ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行 # ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行

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)
@ -664,18 +664,18 @@ class RiskManager:
effective_max = config.get_effective_config('MAX_POSITION_PERCENT', 0.20) effective_max = config.get_effective_config('MAX_POSITION_PERCENT', 0.20)
base_position_percent = effective_max * signal_multiplier base_position_percent = effective_max * signal_multiplier
max_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 上限 # 涨跌幅超过5%时,可以适当增加保证金占比,但必须遵守 MAX_POSITION_PERCENT 上限
if abs(change_percent) > 5: if abs(change_percent) > 5:
position_percent = min( position_percent = min(
base_position_percent * 1.5, base_position_percent * 1.5,
max_position_percent max_position_percent
) )
logger.info(f" 涨跌幅 {change_percent:.2f}% > 5%,使用增强仓位比例: {position_percent*100:.1f}%") logger.info(f" 涨跌幅 {change_percent:.2f}% > 5%,使用增强仓位比例: {position_percent*100:.1f}%")
else: else:
position_percent = base_position_percent position_percent = base_position_percent
logger.info(f" 涨跌幅 {change_percent:.2f}%,使用标准仓位比例: {position_percent*100:.1f}%") logger.info(f" 涨跌幅 {change_percent:.2f}%,使用标准仓位比例: {position_percent*100:.1f}%")
# 计算保证金与名义价值 # 计算保证金与名义价值
margin_value = available_balance * position_percent margin_value = available_balance * position_percent
@ -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:
@ -1090,10 +1090,10 @@ class RiskManager:
logger.debug(f"优先使用ATR止损: {stop_loss_price:.4f}, 忽略保证金止损: {stop_loss_price_margin:.4f}") logger.debug(f"优先使用ATR止损: {stop_loss_price:.4f}, 忽略保证金止损: {stop_loss_price_margin:.4f}")
else: else:
# ATR不可用使用保证金止损和价格百分比止损中"更紧"的一个(保护资金) # ATR不可用使用保证金止损和价格百分比止损中"更紧"的一个(保护资金)
if side == 'BUY': if side == 'BUY':
# 做多:取最大值(更高的止损价,更接近入场价) # 做多:取最大值(更高的止损价,更接近入场价)
stop_loss_price = max(p[1] for p in candidate_prices if p[0] != 'ATR') 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') 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] final_stop_loss = atr_candidate[1]
selected_method = 'ATR' selected_method = 'ATR'
else: else:
if side == 'BUY': if side == 'BUY':
# 做多:选择更高的止损价(更紧) # 做多:选择更高的止损价(更紧)
final_stop_loss = max(p[1] for p in candidate_prices) 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] selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
else: else:
# ⚠️ 关键修复:做空必须选择更低的止损价(更接近入场价,更紧) # ⚠️ 关键修复:做空必须选择更低的止损价(更接近入场价,更紧)
# 注意对于SELL单止损价高于入场价所以"更低的止损价"意味着更接近入场价 # 注意对于SELL单止损价高于入场价所以"更低的止损价"意味着更接近入场价
final_stop_loss = min(p[1] for p in candidate_prices) 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] selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
# ⚠️ 关键修复:验证最终止损价对应的保证金百分比不超过配置值 # ⚠️ 关键修复:验证最终止损价对应的保证金百分比不超过配置值
if side == 'BUY': if side == 'BUY':
@ -1392,7 +1392,7 @@ class RiskManager:
selected_method = 'ATR盈亏比' selected_method = 'ATR盈亏比'
if use_margin_cap and take_profit_price_margin is not None: 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: if take_profit_price_margin < take_profit_price:
take_profit_price = take_profit_price_margin take_profit_price = take_profit_price_margin
selected_method = '保证金(止盈上限)' selected_method = '保证金(止盈上限)'
@ -1406,10 +1406,10 @@ class RiskManager:
# 如果没有基于盈亏比的止盈,选择最远的止盈(给利润更多空间,提高盈亏比) # 如果没有基于盈亏比的止盈,选择最远的止盈(给利润更多空间,提高盈亏比)
if side == 'BUY': if side == 'BUY':
# 做多:选择更高的止盈价(更远,给利润更多空间) # 做多:选择更高的止盈价(更远,给利润更多空间)
take_profit_price = max(p[1] for p in candidate_prices) take_profit_price = max(p[1] for p in candidate_prices)
else: 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] selected_method = [p[0] for p in candidate_prices if p[1] == take_profit_price][0]
logger.info( logger.info(