356 lines
10 KiB
Python
356 lines
10 KiB
Python
"""
|
||
模拟交易 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))
|