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.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="报告时间段(小时)"),
|
||||||
|
|||||||
@ -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]:
|
||||||
"""
|
"""
|
||||||
取消挂单
|
取消挂单
|
||||||
|
|||||||
@ -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 || '未知错误'));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user