update
This commit is contained in:
parent
9783773f97
commit
dc44b2aa4a
@ -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="报告时间段(小时)"),
|
||||
|
||||
@ -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]:
|
||||
"""
|
||||
取消挂单
|
||||
|
||||
@ -1358,6 +1358,7 @@
|
||||
<th>盈亏</th>
|
||||
<th>状态</th>
|
||||
<th>平仓时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -1376,6 +1377,11 @@
|
||||
</td>
|
||||
<td><span class="status-badge" :class="order.status">{{ formatStatus(order.status) }}</span></td>
|
||||
<td>{{ formatTime(order.closed_at) }}</td>
|
||||
<td class="action-cell">
|
||||
<button class="action-btn danger" @click="deleteHistoryOrder(order)" title="删除订单(会重新计算收益)">
|
||||
删除
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -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 || '未知错误'));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user