From f737c32ea23077725c8075fa85b3a3c873600276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Fri, 16 Jan 2026 11:45:43 +0800 Subject: [PATCH] a --- backend/api/routes/trades.py | 162 ++++++++++++++++++++++++++ backend/database/models.py | 3 +- frontend/src/components/TradeList.jsx | 18 ++- trading_system/position_manager.py | 99 ++++++++++++++-- 4 files changed, 265 insertions(+), 17 deletions(-) diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index 93e6030..a6a8561 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta import sys from pathlib import Path import logging +import asyncio project_root = Path(__file__).parent.parent.parent.parent sys.path.insert(0, str(project_root)) @@ -173,3 +174,164 @@ async def get_trade_stats( except Exception as e: logger.error(f"获取交易统计失败: {e}", exc_info=True) raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/sync-binance") +async def sync_trades_from_binance( + days: int = Query(7, ge=1, le=30, description="同步最近N天的订单") +): + """ + 从币安同步历史订单,确保数据库与币安一致 + + Args: + days: 同步最近N天的订单(默认7天) + """ + try: + logger.info(f"开始从币安同步历史订单(最近{days}天)...") + + # 导入必要的模块 + trading_system_path = project_root / 'trading_system' + if not trading_system_path.exists(): + alternative_path = project_root / 'backend' / 'trading_system' + if alternative_path.exists(): + trading_system_path = alternative_path + else: + raise HTTPException(status_code=500, detail="交易系统模块不存在") + + sys.path.insert(0, str(trading_system_path)) + sys.path.insert(0, str(project_root)) + + from binance_client import BinanceClient + import config + + # 初始化客户端 + client = BinanceClient( + api_key=config.BINANCE_API_KEY, + api_secret=config.BINANCE_API_SECRET, + testnet=config.USE_TESTNET + ) + + await client.connect() + + try: + import time + from datetime import datetime, timedelta + + # 计算时间范围 + end_time = int(time.time() * 1000) # 当前时间(毫秒) + start_time = int((datetime.now() - timedelta(days=days)).timestamp() * 1000) + + # 获取所有已成交的订单(包括开仓和平仓) + all_orders = [] + try: + # 获取所有交易对的订单 + # 注意:币安API可能需要分交易对查询,这里先获取所有交易对 + symbols = await client.client.futures_exchange_info() + symbol_list = [s['symbol'] for s in symbols.get('symbols', []) if s.get('contractType') == 'PERPETUAL'] + + logger.info(f"开始同步 {len(symbol_list)} 个交易对的订单...") + + for symbol in symbol_list: + try: + # 获取该交易对的历史订单 + orders = await client.client.futures_get_all_orders( + symbol=symbol, + startTime=start_time, + endTime=end_time + ) + + # 只保留已成交的订单 + filled_orders = [o for o in orders if o.get('status') == 'FILLED'] + all_orders.extend(filled_orders) + + # 避免请求过快 + await asyncio.sleep(0.1) + except Exception as e: + logger.debug(f"获取 {symbol} 订单失败: {e}") + continue + + logger.info(f"从币安获取到 {len(all_orders)} 个已成交订单") + except Exception as e: + logger.error(f"获取币安订单失败: {e}") + raise HTTPException(status_code=500, detail=f"获取币安订单失败: {str(e)}") + + # 同步订单到数据库 + synced_count = 0 + updated_count = 0 + + # 按时间排序,从旧到新 + all_orders.sort(key=lambda x: x.get('time', 0)) + + for order in all_orders: + symbol = order.get('symbol') + order_id = order.get('orderId') + side = order.get('side') + quantity = float(order.get('executedQty', 0)) + avg_price = float(order.get('avgPrice', 0)) + order_time = datetime.fromtimestamp(order.get('time', 0) / 1000) + reduce_only = order.get('reduceOnly', False) + + if quantity <= 0 or avg_price <= 0: + continue + + try: + if reduce_only: + # 这是平仓订单,更新数据库中的对应记录 + # 查找数据库中该交易对的open状态记录 + open_trades = Trade.get_by_symbol(symbol, status='open') + if open_trades: + # 找到匹配的交易记录(通过symbol匹配,如果有多个则取最近的) + trade = open_trades[0] # 取第一个 + trade_id = trade['id'] + + # 计算盈亏 + entry_price = float(trade['entry_price']) + entry_quantity = float(trade['quantity']) + + # 使用实际成交数量(可能部分平仓) + actual_quantity = min(quantity, entry_quantity) + + if trade['side'] == 'BUY': + pnl = (avg_price - entry_price) * actual_quantity + pnl_percent = ((avg_price - entry_price) / entry_price) * 100 + else: # SELL + pnl = (entry_price - avg_price) * actual_quantity + pnl_percent = ((entry_price - avg_price) / entry_price) * 100 + + # 更新数据库 + Trade.update_exit( + trade_id=trade_id, + exit_price=avg_price, + exit_reason='sync', + pnl=pnl, + pnl_percent=pnl_percent + ) + updated_count += 1 + logger.debug(f"✓ 更新平仓记录: {symbol} (ID: {trade_id}, 成交价: {avg_price:.4f})") + else: + # 这是开仓订单,检查数据库中是否已存在 + # 这里可以添加逻辑来创建缺失的开仓记录 + # 但为了简化,暂时只处理平仓订单 + pass + except Exception as e: + logger.warning(f"同步订单失败 {symbol} (订单ID: {order_id}): {e}") + continue + + result = { + "success": True, + "message": f"同步完成:更新了 {updated_count} 条平仓记录", + "total_orders": len(all_orders), + "updated_trades": updated_count + } + + logger.info(f"✓ 同步完成:处理了 {len(all_orders)} 个订单,更新了 {updated_count} 条记录") + return result + + finally: + await client.disconnect() + + except HTTPException: + raise + except Exception as e: + logger.error(f"同步币安订单失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"同步币安订单失败: {str(e)}") diff --git a/backend/database/models.py b/backend/database/models.py index 9d62d01..cb771cc 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -136,7 +136,8 @@ class Trade: query += " AND exit_reason = %s" params.append(exit_reason) - query += " ORDER BY entry_time DESC" + # 按平仓时间倒序排序,如果没有平仓时间则按入场时间倒序 + query += " ORDER BY COALESCE(exit_time, entry_time) DESC, entry_time DESC" return db.execute_query(query, params) @staticmethod diff --git a/frontend/src/components/TradeList.jsx b/frontend/src/components/TradeList.jsx index 74ae429..58b8065 100644 --- a/frontend/src/components/TradeList.jsx +++ b/frontend/src/components/TradeList.jsx @@ -250,7 +250,8 @@ const TradeList = () => { 盈亏比例 状态 平仓类型 - 时间 + 入场时间 + 平仓时间 @@ -304,6 +305,7 @@ const TradeList = () => { {trade.exit_reason_display || '-'} {formatTime(trade.entry_time)} + {trade.exit_time ? formatTime(trade.exit_time) : '-'} ) })} @@ -384,12 +386,18 @@ const TradeList = () => { )} -
+
+
+ 入场: {formatTime(trade.entry_time)} - {trade.exit_time && ( - 平仓: {formatTime(trade.exit_time)} - )}
+ {trade.exit_time && ( +
+ 平仓: + {formatTime(trade.exit_time)} +
+ )} +
) })} diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 28ea069..0a21e47 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -347,14 +347,55 @@ class PositionManager: raise # 重新抛出异常,让外层捕获 if order: - logger.info(f"{symbol} [平仓] ✓ 平仓订单已提交 (订单ID: {order.get('orderId', 'N/A')})") - # 获取平仓价格(确保是float类型) - ticker = await self.client.get_ticker_24h(symbol) - if not ticker: - logger.warning(f"无法获取 {symbol} 价格,使用订单价格") - exit_price = float(order.get('avgPrice', 0)) or float(order.get('price', 0)) - else: - exit_price = float(ticker['price']) + order_id = order.get('orderId') + logger.info(f"{symbol} [平仓] ✓ 平仓订单已提交 (订单ID: {order_id})") + + # 等待订单成交,然后从币安获取实际成交价格 + exit_price = None + try: + # 等待一小段时间让订单成交 + await asyncio.sleep(1) + + # 从币安获取订单详情,获取实际成交价格 + try: + order_info = await self.client.client.futures_get_order(symbol=symbol, orderId=order_id) + if order_info: + # 优先使用平均成交价格(avgPrice),如果没有则使用价格字段 + exit_price = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0)) + if exit_price > 0: + logger.info(f"{symbol} [平仓] 从币安订单获取实际成交价格: {exit_price:.4f} USDT") + else: + # 如果订单还没有完全成交,尝试从成交记录获取 + if order_info.get('status') == 'FILLED' 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: + exit_price = total_value / total_qty + logger.info(f"{symbol} [平仓] 从成交记录计算平均成交价格: {exit_price:.4f} USDT") + except Exception as order_error: + logger.warning(f"{symbol} [平仓] 获取订单详情失败: {order_error},使用备用方法") + + # 如果无法从订单获取价格,使用当前价格作为备用 + if not exit_price or exit_price <= 0: + ticker = await self.client.get_ticker_24h(symbol) + if ticker: + exit_price = float(ticker['price']) + logger.warning(f"{symbol} [平仓] 使用当前价格作为平仓价格: {exit_price:.4f} USDT") + else: + exit_price = float(order.get('avgPrice', 0)) or float(order.get('price', 0)) + if exit_price <= 0: + logger.error(f"{symbol} [平仓] 无法获取平仓价格,使用订单价格字段") + exit_price = float(order.get('price', 0)) + except Exception as price_error: + logger.warning(f"{symbol} [平仓] 获取成交价格时出错: {price_error},使用当前价格") + ticker = await self.client.get_ticker_24h(symbol) + exit_price = float(ticker['price']) if ticker else float(order.get('price', 0)) # 更新数据库记录 if DB_AVAILABLE and Trade and symbol in self.active_positions: @@ -825,9 +866,45 @@ class PositionManager: try: logger.info(f"{symbol} [状态同步] 更新交易记录状态 (ID: {trade_id})...") - # 获取当前价格作为平仓价格 - ticker = await self.client.get_ticker_24h(symbol) - exit_price = float(ticker['price']) if ticker else float(trade['entry_price']) + # 尝试从币安历史订单获取实际平仓价格 + exit_price = None + try: + # 获取最近的平仓订单(reduceOnly=True的订单) + import time + end_time = int(time.time() * 1000) # 当前时间(毫秒) + start_time = end_time - (7 * 24 * 60 * 60 * 1000) # 最近7天 + + # 获取历史订单 + orders = await self.client.client.futures_get_all_orders( + symbol=symbol, + startTime=start_time, + endTime=end_time + ) + + # 查找最近的平仓订单(reduceOnly=True且已成交) + close_orders = [ + o for o in orders + if o.get('reduceOnly') == True + and o.get('status') == 'FILLED' + ] + + if close_orders: + # 按时间倒序排序,取最近的 + close_orders.sort(key=lambda x: x.get('updateTime', 0), reverse=True) + latest_order = close_orders[0] + + # 获取平均成交价格 + exit_price = float(latest_order.get('avgPrice', 0)) + if exit_price > 0: + logger.info(f"{symbol} [状态同步] 从币安历史订单获取平仓价格: {exit_price:.4f} USDT") + except Exception as order_error: + logger.debug(f"{symbol} [状态同步] 获取历史订单失败: {order_error}") + + # 如果无法从订单获取,使用当前价格 + if not exit_price or exit_price <= 0: + ticker = await self.client.get_ticker_24h(symbol) + exit_price = float(ticker['price']) if ticker else float(trade['entry_price']) + logger.warning(f"{symbol} [状态同步] 使用当前价格作为平仓价格: {exit_price:.4f} USDT") # 计算盈亏(确保所有值都是float类型,避免Decimal类型问题) entry_price = float(trade['entry_price'])