From 7b8bcd758d9dfa5d2415bf104950ba67645a6602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Tue, 3 Feb 2026 09:48:37 +0800 Subject: [PATCH] a --- backend/api/routes/account.py | 34 +++++++++++++++++- backend/database/models.py | 43 +++++++++++++++-------- trading_system/binance_client.py | 17 +++++++++ trading_system/position_manager.py | 55 ++++++++++++++++++++++++++++-- 4 files changed, 132 insertions(+), 17 deletions(-) diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py index b96c26d..8ab3ade 100644 --- a/backend/api/routes/account.py +++ b/backend/api/routes/account.py @@ -989,8 +989,22 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) # 获取订单详情(可能多个订单,按订单号分别取价) exit_prices = {} + exit_commissions = {} + exit_realized_pnls = {} + exit_commission_assets = {} + + # 新增:获取最近成交记录以计算佣金和实际盈亏 + try: + # 等待一小段时间确保成交记录已生成 + await asyncio.sleep(1) + recent_trades = await client.get_recent_trades(symbol, limit=20) + except Exception as e: + logger.warning(f"获取最近成交记录失败: {e}") + recent_trades = [] + for oid in order_ids: try: + # 1. 获取价格 order_info = await client.client.futures_get_order(symbol=symbol, orderId=oid) if order_info: p = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0)) @@ -1006,6 +1020,21 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) p = total_value / total_qty if p > 0: exit_prices[oid] = p + + # 2. 计算佣金和实际盈亏(从 recent_trades 匹配) + related_trades = [t for t in recent_trades if str(t.get('orderId')) == str(oid)] + if related_trades: + total_realized_pnl = 0.0 + total_commission = 0.0 + commission_assets = set() + for t in related_trades: + total_realized_pnl += float(t.get('realizedPnl', 0)) + total_commission += float(t.get('commission', 0)) + commission_assets.add(t.get('commissionAsset')) + + 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: logger.warning(f"获取订单详情失败 (orderId={oid}): {e}") @@ -1058,7 +1087,10 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id)) exit_reason='manual', pnl=pnl, pnl_percent=pnl_percent, - exit_order_id=chosen_oid + 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}%)") diff --git a/backend/database/models.py b/backend/database/models.py index 416c084..62dc1aa 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -501,6 +501,9 @@ class Trade: strategy_type=None, duration_minutes=None, exit_time_ts=None, + realized_pnl=None, + commission=None, + commission_asset=None, ): """更新平仓信息(使用北京时间) @@ -511,6 +514,9 @@ class Trade: pnl: 盈亏 pnl_percent: 盈亏百分比 exit_order_id: 币安平仓订单号(可选,用于对账) + realized_pnl: 币安实际结算盈亏(可选) + commission: 交易手续费(可选) + commission_asset: 手续费币种(可选) 注意:如果 exit_order_id 已存在且属于其他交易记录,会跳过更新 exit_order_id 以避免唯一约束冲突 """ @@ -520,14 +526,33 @@ class Trade: except Exception: exit_time = get_beijing_time() + # 准备额外字段更新 helper + def _append_extra_fields(fields, values): + if strategy_type is not None: + fields.append("strategy_type = %s") + values.append(strategy_type) + if duration_minutes is not None: + fields.append("duration_minutes = %s") + values.append(duration_minutes) + + # 新增字段(检查是否存在) + if realized_pnl is not None and _table_has_column("trades", "realized_pnl"): + fields.append("realized_pnl = %s") + values.append(realized_pnl) + if commission is not None and _table_has_column("trades", "commission"): + fields.append("commission = %s") + values.append(commission) + if commission_asset is not None and _table_has_column("trades", "commission_asset"): + fields.append("commission_asset = %s") + values.append(commission_asset) + # 如果提供了 exit_order_id,先检查是否已被其他交易记录使用 if exit_order_id is not None: try: existing_trade = Trade.get_by_exit_order_id(exit_order_id) if existing_trade: if existing_trade['id'] == trade_id: - # exit_order_id 属于当前交易记录:允许继续更新(比如补写 exit_reason / exit_time / duration) - # 不需要提前 return + # exit_order_id 属于当前交易记录:允许继续更新 logger.debug( f"交易记录 {trade_id} 的 exit_order_id {exit_order_id} 已存在,将继续更新其他字段" ) @@ -544,12 +569,7 @@ class Trade: ] update_values = [exit_price, exit_time, exit_reason, pnl, pnl_percent] - if strategy_type is not None: - update_fields.append("strategy_type = %s") - update_values.append(strategy_type) - if duration_minutes is not None: - update_fields.append("duration_minutes = %s") - update_values.append(duration_minutes) + _append_extra_fields(update_fields, update_values) update_values.append(trade_id) db.execute_update( @@ -570,12 +590,7 @@ class Trade: ] update_values = [exit_price, exit_time, exit_reason, pnl, pnl_percent, exit_order_id] - if strategy_type is not None: - update_fields.append("strategy_type = %s") - update_values.append(strategy_type) - if duration_minutes is not None: - update_fields.append("duration_minutes = %s") - update_values.append(duration_minutes) + _append_extra_fields(update_fields, update_values) update_values.append(trade_id) db.execute_update( diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 92f8ce9..3df847d 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -731,6 +731,23 @@ class BinanceClient: except BinanceAPIException as e: logger.error(f"获取持仓信息失败: {e}") return [] + + async def get_recent_trades(self, symbol: str, limit: int = 50) -> List[Dict]: + """ + 获取最近的成交记录 + + Args: + symbol: 交易对 + limit: 获取数量 + + Returns: + 成交记录列表 + """ + try: + return await self.client.futures_account_trades(symbol=symbol, limit=limit) + except Exception as e: + logger.error(f"获取成交记录失败 {symbol}: {e}") + return [] async def get_symbol_info(self, symbol: str) -> Optional[Dict]: """ diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 80af03f..92e94cf 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -975,6 +975,54 @@ class PositionManager: if exit_order_id: logger.info(f"{symbol} [平仓] 币安订单号: {exit_order_id}") + # ----------------------------------------------------------- + # 新增:获取实际成交详情(佣金、资金费率、实际盈亏) + # ----------------------------------------------------------- + realized_pnl = None + commission = None + commission_asset = None + + try: + # 等待一小段时间确保成交记录已生成 + await asyncio.sleep(2) + + # 获取最近的成交记录 + recent_trades = await self.client.get_recent_trades(symbol, limit=10) + + # 筛选属于当前平仓订单的成交记录 + # 注意:一次平仓可能对应多条成交记录(分批成交) + related_trades = [] + if exit_order_id: + related_trades = [t for t in recent_trades if str(t.get('orderId')) == str(exit_order_id)] + else: + # 如果没有订单号(极少见),尝试通过时间匹配 + # TODO: 暂时跳过,风险较大 + pass + + if related_trades: + total_realized_pnl = 0.0 + total_commission = 0.0 + commission_assets = set() + + for t in related_trades: + total_realized_pnl += float(t.get('realizedPnl', 0)) + total_commission += float(t.get('commission', 0)) + commission_assets.add(t.get('commissionAsset')) + + realized_pnl = total_realized_pnl + commission = total_commission + commission_asset = "/".join(commission_assets) if commission_assets else None + + logger.info( + f"{symbol} [平仓] 获取到实际成交详情: " + f"实际盈亏={realized_pnl} USDT, 佣金={commission} {commission_asset}" + ) + else: + logger.warning(f"{symbol} [平仓] 未找到订单 {exit_order_id} 的成交记录,无法记录佣金") + + except Exception as fee_error: + logger.warning(f"{symbol} [平仓] 获取成交详情失败: {fee_error}") + # 计算持仓持续时间 entry_time = position_info.get('entryTime') duration_minutes = None @@ -1001,7 +1049,10 @@ class PositionManager: pnl_percent=pnl_percent, exit_order_id=exit_order_id, # 保存币安平仓订单号 strategy_type=strategy_type, - duration_minutes=duration_minutes + duration_minutes=duration_minutes, + realized_pnl=realized_pnl, + commission=commission, + commission_asset=commission_asset ) logger.info( f"{symbol} [平仓] ✓ 数据库记录已更新 " @@ -1690,7 +1741,7 @@ class PositionManager: Trade.update_exit( trade_id=trade_id, exit_price=current_price, - exit_reason=exit_reason, + exit_reason=exit_reason_sl, pnl=pnl_amount, pnl_percent=pnl_percent_margin, strategy_type=strategy_type,