This commit is contained in:
aaron 2026-02-26 09:31:51 +08:00
parent 9783773f97
commit dc44b2aa4a
3 changed files with 319 additions and 31 deletions

View File

@ -9,6 +9,7 @@ from pydantic import BaseModel
from app.services.paper_trading_service import get_paper_trading_service 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.price_monitor_service import get_price_monitor_service
from app.services.bitget_service import bitget_service from app.services.bitget_service import bitget_service
from app.services.db_service import db_service
from app.utils.logger import logger from app.utils.logger import logger
@ -20,6 +21,12 @@ class CloseOrderRequest(BaseModel):
exit_price: float exit_price: float
class DeleteOrdersRequest(BaseModel):
"""批量删除订单请求"""
order_ids: list[str]
recalculate: bool = True # 是否重新计算统计数据
class OrderResponse(BaseModel): class OrderResponse(BaseModel):
"""订单响应""" """订单响应"""
success: bool success: bool
@ -130,26 +137,48 @@ async def close_order(order_id: str, request: CloseOrderRequest):
@router.delete("/orders/{order_id}") @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 - order_id: 订单ID
- recalculate: 是否重新计算统计数据默认 True
返回:
- 订单删除前的信息
- 删除后的统计数据变化
""" """
try: try:
service = get_paper_trading_service() service = get_paper_trading_service()
result = service.delete_order(order_id) result = service.delete_order(order_id, recalculate=recalculate)
if not result: if not result:
raise HTTPException(status_code=404, detail="订单不存在") raise HTTPException(status_code=404, detail="订单不存在")
return { return {
"success": True, "success": True,
"message": "订单已删除", "message": result.get('message', '订单已删除'),
"result": result "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: except HTTPException:
raise raise
@ -158,6 +187,66 @@ async def delete_order(order_id: str):
raise HTTPException(status_code=500, detail=str(e)) 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") @router.get("/statistics")
async def get_statistics( async def get_statistics(
symbol: Optional[str] = Query(None, description="交易对筛选"), symbol: Optional[str] = Query(None, description="交易对筛选"),
@ -321,6 +410,40 @@ async def reset_paper_trading():
raise HTTPException(status_code=500, detail=str(e)) 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") @router.post("/report")
async def send_report( async def send_report(
hours: int = Query(4, description="报告时间段(小时)"), hours: int = Query(4, description="报告时间段(小时)"),

View File

@ -973,47 +973,67 @@ class PaperTradingService:
return self._close_order(order, OrderStatus.CLOSED_MANUAL, exit_price) 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: Args:
order_id: 订单ID order_id: 订单ID
recalculate: 是否重新计算统计数据默认 True
Returns: 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() db = db_service.get_session()
try: try:
# 从数据库中彻底删除订单 # 1. 先查询数据库中的订单(支持删除历史订单)
db_order = db.query(PaperOrder).filter(PaperOrder.order_id == order_id).first() 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 not db_order:
if order_id in self.active_orders: 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] 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 { return {
'order_id': order_id, 'order_id': order_id,
'symbol': symbol, 'symbol': symbol,
'side': side, 'side': side,
'status': status, 'status': status,
'message': '订单已删除(不影响收益率)' 'pnl_amount': pnl_amount,
'was_active': was_in_active,
'recalculated': recalculate,
'statistics_change': recalc_result,
'message': f'订单已删除,统计数据已重新计算'
} }
except Exception as e: except Exception as e:
logger.error(f"删除订单失败: {e}") logger.error(f"删除订单失败: {e}")
@ -1022,6 +1042,82 @@ class PaperTradingService:
finally: finally:
db.close() 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]: def cancel_order(self, order_id: str) -> Dict[str, Any]:
""" """
取消挂单 取消挂单

View File

@ -1358,6 +1358,7 @@
<th>盈亏</th> <th>盈亏</th>
<th>状态</th> <th>状态</th>
<th>平仓时间</th> <th>平仓时间</th>
<th>操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -1376,6 +1377,11 @@
</td> </td>
<td><span class="status-badge" :class="order.status">{{ formatStatus(order.status) }}</span></td> <td><span class="status-badge" :class="order.status">{{ formatStatus(order.status) }}</span></td>
<td>{{ formatTime(order.closed_at) }}</td> <td>{{ formatTime(order.closed_at) }}</td>
<td class="action-cell">
<button class="action-btn danger" @click="deleteHistoryOrder(order)" title="删除订单(会重新计算收益)">
删除
</button>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -1966,7 +1972,16 @@
}, },
async deleteOrder(order) { 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; return;
} }
@ -1975,8 +1990,62 @@
method: 'DELETE' method: 'DELETE'
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { 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(); this.refreshData();
} else { } else {
alert('删除失败: ' + (data.detail || '未知错误')); alert('删除失败: ' + (data.detail || '未知错误'));