""" 模拟交易 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.utils.logger import logger router = APIRouter(prefix="/api/paper-trading", tags=["模拟交易"]) class CloseOrderRequest(BaseModel): """手动平仓请求""" exit_price: float 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): """ 删除订单(不产生盈亏,仅删除记录) 注意:此操作会直接删除订单记录,不会产生任何盈亏计算, 也不会影响收益率等统计指标。主要用于删除错误订单或测试数据。 - order_id: 订单ID """ try: service = get_paper_trading_service() result = service.delete_order(order_id) 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.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("/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))