diff --git a/backend/app/api/paper_trading.py b/backend/app/api/paper_trading.py index b93f45d..a497bd3 100644 --- a/backend/app/api/paper_trading.py +++ b/backend/app/api/paper_trading.py @@ -9,6 +9,7 @@ from pydantic import BaseModel from app.services.paper_trading_service import get_paper_trading_service from app.services.price_monitor_service import get_price_monitor_service from app.services.bitget_service import bitget_service +from app.services.db_service import db_service from app.utils.logger import logger @@ -20,6 +21,12 @@ class CloseOrderRequest(BaseModel): exit_price: float +class DeleteOrdersRequest(BaseModel): + """批量删除订单请求""" + order_ids: list[str] + recalculate: bool = True # 是否重新计算统计数据 + + class OrderResponse(BaseModel): """订单响应""" success: bool @@ -130,26 +137,48 @@ async def close_order(order_id: str, request: CloseOrderRequest): @router.delete("/orders/{order_id}") -async def delete_order(order_id: str): +async def delete_order( + order_id: str, + recalculate: bool = Query(True, description="是否重新计算统计数据,默认True") +): """ - 删除订单(不产生盈亏,仅删除记录) + 删除订单(支持历史订单和活跃订单) - 注意:此操作会直接删除订单记录,不会产生任何盈亏计算, - 也不会影响收益率等统计指标。主要用于删除错误订单或测试数据。 + 删除订单后会重新计算账户统计数据,包括: + - 已实现盈亏总和 + - 当前余额 + - 胜率 + - 最大回撤 + - 收益率 + 参数: - order_id: 订单ID + - recalculate: 是否重新计算统计数据,默认 True + + 返回: + - 订单删除前的信息 + - 删除后的统计数据变化 """ try: service = get_paper_trading_service() - result = service.delete_order(order_id) + result = service.delete_order(order_id, recalculate=recalculate) if not result: raise HTTPException(status_code=404, detail="订单不存在") return { "success": True, - "message": "订单已删除", - "result": result + "message": result.get('message', '订单已删除'), + "deleted_order": { + "order_id": result['order_id'], + "symbol": result['symbol'], + "side": result['side'], + "status": result['status'], + "pnl_amount": result['pnl_amount'], + "was_active": result['was_active'] + }, + "statistics_update": result.get('statistics_change', {}), + "recalculated": result.get('recalculated', False) } except HTTPException: raise @@ -158,6 +187,66 @@ async def delete_order(order_id: str): raise HTTPException(status_code=500, detail=str(e)) +@router.post("/orders/batch-delete") +async def batch_delete_orders(request: DeleteOrdersRequest): + """ + 批量删除订单 + + 可以一次性删除多个订单,最后统一重新计算统计数据。 + + 请求体: + { + "order_ids": ["PT-BTCUSDT-20240101-abc123", "PT-ETHUSDT-20240101-def456"], + "recalculate": true // 是否重新计算统计数据,默认 true + } + + 返回: + - 成功删除的订单列表 + - 失败的订单列表 + - 统计数据变化(只在 recalculate=true 时返回) + """ + try: + service = get_paper_trading_service() + + deleted_orders = [] + failed_orders = [] + + # 先删除所有订单(不重新计算) + for order_id in request.order_ids: + result = service.delete_order(order_id, recalculate=False) + if result: + deleted_orders.append({ + "order_id": order_id, + "symbol": result['symbol'], + "status": result['status'], + "pnl_amount": result['pnl_amount'] + }) + else: + failed_orders.append(order_id) + + # 最后统一重新计算一次统计数据 + statistics_update = {} + if request.recalculate and deleted_orders: + db = db_service.get_session() + try: + statistics_update = service._recalculate_account_statistics(db) + finally: + db.close() + + return { + "success": True, + "message": f"成功删除 {len(deleted_orders)} 个订单", + "deleted_count": len(deleted_orders), + "failed_count": len(failed_orders), + "deleted_orders": deleted_orders, + "failed_orders": failed_orders, + "statistics_update": statistics_update if request.recalculate else {} + } + except Exception as e: + logger.error(f"批量删除订单失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @router.get("/statistics") async def get_statistics( symbol: Optional[str] = Query(None, description="交易对筛选"), @@ -321,6 +410,40 @@ async def reset_paper_trading(): raise HTTPException(status_code=500, detail=str(e)) +@router.post("/recalculate-statistics") +async def recalculate_statistics(): + """ + 手动重新计算账户统计数据 + + 当您怀疑统计数据不准确时,可以调用此接口重新计算: + - 已实现盈亏 + - 当前余额 + - 胜率 + - 最大回撤 + - 收益率 + + 此操作不会删除或修改任何订单,只是重新计算统计数据。 + """ + try: + service = get_paper_trading_service() + db = db_service.get_session() + + try: + stats = service._recalculate_account_statistics(db) + + return { + "success": True, + "message": "统计数据已重新计算", + "statistics": stats + } + finally: + db.close() + + except Exception as e: + logger.error(f"重新计算统计数据失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @router.post("/report") async def send_report( hours: int = Query(4, description="报告时间段(小时)"), diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index 4951684..b3c82a7 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -973,47 +973,67 @@ class PaperTradingService: return self._close_order(order, OrderStatus.CLOSED_MANUAL, exit_price) - def delete_order(self, order_id: str) -> Optional[Dict[str, Any]]: + def delete_order(self, order_id: str, recalculate: bool = True) -> Optional[Dict[str, Any]]: """ - 删除订单(不产生盈亏,仅删除记录) + 删除订单(历史订单或活跃订单) - 注意:此操作会直接从数据库中删除订单记录,不会产生任何盈亏计算, - 也不会影响收益率等统计指标。主要用于删除错误订单或测试数据。 + 删除订单后会重新计算账户统计数据,包括: + - 已实现盈亏 + - 当前余额 + - 账户最大回撤 + - 胜率等统计指标 Args: order_id: 订单ID + recalculate: 是否重新计算统计数据,默认 True Returns: - 删除结果字典 + 删除结果字典,包含删除前后的统计变化 """ - if order_id not in self.active_orders: - logger.warning(f"订单不存在或已删除: {order_id}") - return None - - order = self.active_orders[order_id] - symbol = order.symbol - side = order.side.value - status = order.status.value - db = db_service.get_session() try: - # 从数据库中彻底删除订单 + # 1. 先查询数据库中的订单(支持删除历史订单) db_order = db.query(PaperOrder).filter(PaperOrder.order_id == order_id).first() - if db_order: - db.delete(db_order) - db.commit() - logger.info(f"订单已从数据库删除: {order_id} | {symbol} {side} | 原状态: {status}") - # 从活跃订单缓存中移除 - if order_id in self.active_orders: + if not db_order: + logger.warning(f"订单不存在: {order_id}") + return None + + # 2. 记录删除前的订单信息 + symbol = db_order.symbol + side = db_order.side.value + status = db_order.status.value + pnl_amount = db_order.pnl_amount + was_in_active = order_id in self.active_orders + + logger.info(f"准备删除订单: {order_id} | {symbol} {side} | 状态: {status} | 盈亏: ${pnl_amount:.2f}") + + # 3. 如果是活跃订单,从缓存中移除 + if was_in_active: del self.active_orders[order_id] + logger.info(f"订单已从活跃订单缓存中移除: {order_id}") + + # 4. 从数据库中删除订单 + db.delete(db_order) + db.commit() + logger.info(f"订单已从数据库删除: {order_id}") + + # 5. 重新计算统计数据 + recalc_result = {} + if recalculate: + logger.info(f"开始重新计算统计数据...") + recalc_result = self._recalculate_account_statistics(db) return { 'order_id': order_id, 'symbol': symbol, 'side': side, 'status': status, - 'message': '订单已删除(不影响收益率)' + 'pnl_amount': pnl_amount, + 'was_active': was_in_active, + 'recalculated': recalculate, + 'statistics_change': recalc_result, + 'message': f'订单已删除,统计数据已重新计算' } except Exception as e: logger.error(f"删除订单失败: {e}") @@ -1022,6 +1042,82 @@ class PaperTradingService: finally: db.close() + def _recalculate_account_statistics(self, db) -> Dict[str, Any]: + """ + 重新计算账户统计数据 + + 在删除订单后调用,重新计算: + - 已实现盈亏总和 + - 当前余额 + - 胜率 + - 最大回撤 + - 收益率 + + Returns: + 统计数据变化 + """ + try: + # 获取所有已平仓订单 + closed_orders = db.query(PaperOrder).filter( + PaperOrder.status.in_([ + OrderStatus.CLOSED_TP, + OrderStatus.CLOSED_SL, + OrderStatus.CLOSED_BE, + OrderStatus.CLOSED_MANUAL + ]) + ).all() + + # 计算新的统计数据 + total_count = len(closed_orders) + if total_count == 0: + return { + 'realized_pnl': 0, + 'current_balance': self.initial_balance, + 'win_rate': 0, + 'total_trades': 0, + 'max_drawdown': 0, + 'return_percent': 0 + } + + # 重新计算盈亏 + new_realized_pnl = sum(o.pnl_amount for o in closed_orders) + new_current_balance = self.initial_balance + new_realized_pnl + + # 重新计算胜率 + win_count = len([o for o in closed_orders if o.pnl_amount > 0]) + new_win_rate = (win_count / total_count * 100) if total_count > 0 else 0 + + # 重新计算最大回撤 + new_max_drawdown = self._calculate_account_max_drawdown(closed_orders) + + # 重新计算收益率 + new_return_percent = (new_realized_pnl / self.initial_balance * 100) if self.initial_balance > 0 else 0 + + logger.info(f"统计数据已重新计算:") + logger.info(f" 总交易: {total_count} 单") + logger.info(f" 已实现盈亏: ${new_realized_pnl:.2f}") + logger.info(f" 当前余额: ${new_current_balance:.2f}") + logger.info(f" 胜率: {new_win_rate:.1f}%") + logger.info(f" 最大回撤: {new_max_drawdown:.2f}%") + logger.info(f" 收益率: {new_return_percent:.2f}%") + + return { + 'realized_pnl': round(new_realized_pnl, 2), + 'current_balance': round(new_current_balance, 2), + 'win_rate': round(new_win_rate, 1), + 'total_trades': total_count, + 'win_count': win_count, + 'loss_count': total_count - win_count, + 'max_drawdown': round(new_max_drawdown, 2), + 'return_percent': round(new_return_percent, 2) + } + + except Exception as e: + logger.error(f"重新计算统计数据失败: {e}") + import traceback + logger.error(traceback.format_exc()) + return {} + def cancel_order(self, order_id: str) -> Dict[str, Any]: """ 取消挂单 diff --git a/frontend/paper-trading.html b/frontend/paper-trading.html index 5ea5fdd..0010bc3 100644 --- a/frontend/paper-trading.html +++ b/frontend/paper-trading.html @@ -1358,6 +1358,7 @@ 盈亏 状态 平仓时间 + 操作 @@ -1376,6 +1377,11 @@ {{ formatStatus(order.status) }} {{ formatTime(order.closed_at) }} + + + @@ -1966,7 +1972,16 @@ }, async deleteOrder(order) { - if (!confirm(`确定要删除订单 ${order.order_id.slice(-12)} 吗?\n\n此操作会直接删除订单记录,不会产生任何盈亏计算,也不会影响收益率等统计指标。`)) { + const pnlText = order.pnl_amount ? (order.pnl_amount >= 0 ? `+$${order.pnl_amount?.toFixed(2)}` : `-$${Math.abs(order.pnl_amount)?.toFixed(2)}`) : '未实现'; + + if (!confirm( + `确定要删除订单 ${order.order_id.slice(-12)} 吗?\n\n` + + `交易对: ${order.symbol}\n` + + `方向: ${order.side === 'long' ? '做多' : '做空'}\n` + + `盈亏: ${pnlText}\n` + + `状态: ${this.formatStatus(order.status)}\n\n` + + `⚠️ 删除后会自动重新计算账户统计数据!` + )) { return; } @@ -1975,8 +1990,62 @@ method: 'DELETE' }); const data = await response.json(); + if (data.success) { - alert('订单已删除'); + // 显示统计变化 + let message = '订单已删除'; + if (data.statistics_update && Object.keys(data.statistics_update).length > 0) { + const stats = data.statistics_update; + message += '\n\n统计数据已更新:\n'; + message += `• 当前余额: $${stats.current_balance?.toLocaleString()}\n`; + message += `• 已实现盈亏: $${stats.realized_pnl >= 0 ? '+' : ''}${stats.realized_pnl?.toLocaleString()}\n`; + message += `• 胜率: ${stats.win_rate}%\n`; + message += `• 总交易: ${stats.total_trades} 单\n`; + message += `• 收益率: ${stats.return_percent >= 0 ? '+' : ''}${stats.return_percent}%`; + } + alert(message); + this.refreshData(); + } else { + alert('删除失败: ' + (data.detail || '未知错误')); + } + } catch (e) { + alert('删除失败: ' + e.message); + } + }, + + async deleteHistoryOrder(order) { + const pnlText = order.pnl_amount >= 0 ? `+$${order.pnl_amount?.toFixed(2)}` : `-$${Math.abs(order.pnl_amount)?.toFixed(2)}`; + + if (!confirm( + `确定要删除历史订单 ${order.order_id.slice(-12)} 吗?\n\n` + + `交易对: ${order.symbol}\n` + + `方向: ${order.side === 'long' ? '做多' : '做空'}\n` + + `盈亏: ${pnlText}\n` + + `状态: ${this.formatStatus(order.status)}\n\n` + + `⚠️ 删除后会自动重新计算账户统计数据!` + )) { + return; + } + + try { + const response = await fetch(`/api/paper-trading/orders/${order.order_id}`, { + method: 'DELETE' + }); + const data = await response.json(); + + if (data.success) { + // 显示统计变化 + let message = '订单已删除'; + if (data.statistics_update && Object.keys(data.statistics_update).length > 0) { + const stats = data.statistics_update; + message += '\n\n统计数据已更新:\n'; + message += `• 当前余额: $${stats.current_balance?.toLocaleString()}\n`; + message += `• 已实现盈亏: $${stats.realized_pnl >= 0 ? '+' : ''}${stats.realized_pnl?.toLocaleString()}\n`; + message += `• 胜率: ${stats.win_rate}%\n`; + message += `• 总交易: ${stats.total_trades} 单\n`; + message += `• 收益率: ${stats.return_percent >= 0 ? '+' : ''}${stats.return_percent}%`; + } + alert(message); this.refreshData(); } else { alert('删除失败: ' + (data.detail || '未知错误'));