""" 交易 API """ from fastapi import APIRouter, HTTPException, Query from typing import Optional from datetime import datetime 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 router = APIRouter(prefix="/api/trading", tags=["交易"]) class CloseOrderRequest(BaseModel): """手动平仓请求""" exit_price: float class DeleteOrdersRequest(BaseModel): """批量删除订单请求""" order_ids: list[str] recalculate: bool = True # 是否重新计算统计数据 class OrderResponse(BaseModel): """订单响应""" success: bool message: str data: Optional[dict] = None @router.get("/orders") async def get_orders( symbol: Optional[str] = Query(None, description="交易对筛选"), status: Optional[str] = Query(None, description="状态筛选: active, closed"), limit: int = Query(100, description="返回数量限制") ): """ 获取订单列表 - symbol: 可选,按交易对筛选 - status: 可选,active=活跃订单, closed=已平仓订单 - limit: 返回数量限制,默认100 """ try: service = get_paper_trading_service() if status == "active": orders = service.get_active_orders(symbol) elif status == "closed": orders = service.get_order_history(symbol, limit) else: # 返回所有订单 active = service.get_active_orders(symbol) history = service.get_order_history(symbol, limit) orders = active + history return { "success": True, "count": len(orders), "orders": orders } except Exception as e: logger.error(f"获取订单列表失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/orders/active") async def get_active_orders( symbol: Optional[str] = Query(None, description="交易对筛选") ): """获取活跃订单""" try: service = get_paper_trading_service() orders = service.get_active_orders(symbol) return { "success": True, "count": len(orders), "orders": orders } except Exception as e: logger.error(f"获取活跃订单失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/orders/{order_id}") async def get_order(order_id: str): """获取订单详情""" try: service = get_paper_trading_service() order = service.get_order_by_id(order_id) if not order: raise HTTPException(status_code=404, detail="订单不存在") return { "success": True, "order": order } except HTTPException: raise except Exception as e: logger.error(f"获取订单详情失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/orders/{order_id}/close") async def close_order(order_id: str, request: CloseOrderRequest): """ 手动平仓 - order_id: 订单ID - exit_price: 平仓价格 """ try: service = get_paper_trading_service() result = service.close_order_manual(order_id, request.exit_price) if not result: raise HTTPException(status_code=404, detail="订单不存在或已平仓") return { "success": True, "message": "平仓成功", "result": result } except HTTPException: raise except Exception as e: logger.error(f"手动平仓失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/orders/{order_id}") 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, recalculate=recalculate) if not result: raise HTTPException(status_code=404, detail="订单不存在") return { "success": True, "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 except Exception as e: logger.error(f"删除订单失败: {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") async def get_statistics( symbol: Optional[str] = Query(None, description="交易对筛选"), start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD)"), end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD)") ): """ 获取交易统计 - symbol: 可选,按交易对筛选 - start_date: 可选,开始日期 - end_date: 可选,结束日期 """ try: service = get_paper_trading_service() # 解析日期 start = datetime.strptime(start_date, "%Y-%m-%d") if start_date else None end = datetime.strptime(end_date, "%Y-%m-%d") if end_date else None stats = service.calculate_statistics(symbol, start, end) return { "success": True, "statistics": stats } except ValueError as e: raise HTTPException(status_code=400, detail=f"日期格式错误: {e}") except Exception as e: logger.error(f"获取统计数据失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/account") async def get_account_status(): """ 获取账户状态 返回账户余额、已用保证金、可用保证金等信息 """ try: service = get_paper_trading_service() account = service.get_account_status() return { "success": True, "account": account } except Exception as e: logger.error(f"获取账户状态失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/statistics/by-grade") async def get_statistics_by_grade(): """按信号等级获取统计""" try: service = get_paper_trading_service() stats = service.calculate_statistics() return { "success": True, "by_grade": stats.get("by_grade", {}) } except Exception as e: logger.error(f"获取等级统计失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/statistics/by-symbol") async def get_statistics_by_symbol(): """按交易对获取统计""" try: service = get_paper_trading_service() stats = service.calculate_statistics() return { "success": True, "by_symbol": stats.get("by_symbol", {}) } except Exception as e: logger.error(f"获取交易对统计失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/daily-returns") async def get_daily_returns( days: int = Query(30, description="获取最近多少天的数据", ge=1, le=365) ): """ 获取每日收益率数据 - days: 获取最近多少天的数据,默认30天,最大365天 """ try: service = get_paper_trading_service() daily_returns = service.get_daily_returns(days=days) return { "success": True, "data": daily_returns, "count": len(daily_returns) } except Exception as e: logger.error(f"获取每日收益率失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/monitor/status") async def get_monitor_status(): """获取价格监控状态和实时价格""" try: from app.config import get_settings monitor = get_price_monitor_service() settings = get_settings() # 始终显示配置的交易对价格 configured_symbols = settings.crypto_symbols.split(',') # 获取价格 - 优先使用监控服务的缓存价格 latest_prices = dict(monitor.latest_prices) # 获取所有配置的交易对价格 for symbol in configured_symbols: symbol = symbol.strip() if symbol not in latest_prices or latest_prices[symbol] is None: price = bitget_service.get_current_price(symbol) if price: latest_prices[symbol] = price return { "success": True, "running": True, "subscribed_symbols": configured_symbols, "latest_prices": latest_prices } except Exception as e: logger.error(f"获取监控状态失败: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/reset") async def reset_paper_trading(): """ 重置所有模拟交易数据 警告:此操作将删除所有订单记录,不可恢复! """ try: service = get_paper_trading_service() result = service.reset_all_data() return { "success": True, "message": f"交易数据已重置,删除 {result['deleted_count']} 条订单", "result": result } except Exception as e: logger.error(f"重置交易数据失败: {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") async def send_report( hours: int = Query(4, description="报告时间段(小时)"), send_telegram: bool = Query(True, description="是否发送到Telegram") ): """ 手动触发发送模拟交易报告 - hours: 报告时间段,默认4小时 - send_telegram: 是否发送到Telegram,默认True """ try: from app.services.telegram_service import get_telegram_service service = get_paper_trading_service() report = service.generate_report(hours=hours) result = { "success": True, "report": report, "telegram_sent": False } if send_telegram: telegram = get_telegram_service() sent = await telegram.send_message(report, parse_mode="HTML") result["telegram_sent"] = sent return result except Exception as e: logger.error(f"生成报告失败: {e}") raise HTTPException(status_code=500, detail=str(e))