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 = () => {