364 lines
11 KiB
Python
364 lines
11 KiB
Python
"""
|
||
实盘交易 API
|
||
"""
|
||
from fastapi import APIRouter, HTTPException, Query
|
||
from typing import Optional
|
||
from datetime import datetime
|
||
from pydantic import BaseModel
|
||
|
||
from app.services.real_trading_service import get_real_trading_service
|
||
from app.services.bitget_trading_api_sdk import get_bitget_trading_api
|
||
from app.utils.logger import logger
|
||
|
||
|
||
router = APIRouter(prefix="/api/real-trading", tags=["实盘交易"])
|
||
|
||
|
||
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, exchange"),
|
||
limit: int = Query(100, description="返回数量限制")
|
||
):
|
||
"""
|
||
获取实盘订单列表
|
||
|
||
- symbol: 可选,按交易对筛选
|
||
- status: 可选
|
||
- active: 本地数据库的活跃订单
|
||
- closed: 本地数据库的历史订单
|
||
- exchange: 交易所的历史订单(推荐)
|
||
- limit: 返回数量限制,默认100
|
||
"""
|
||
try:
|
||
# 如果请求交易所历史订单,直接从交易所获取
|
||
if status == "exchange":
|
||
trading_api = get_bitget_trading_api()
|
||
|
||
if not trading_api:
|
||
return {
|
||
"success": False,
|
||
"message": "Bitget API 未配置",
|
||
"count": 0,
|
||
"orders": []
|
||
}
|
||
|
||
orders = trading_api.get_closed_orders(symbol, limit)
|
||
|
||
return {
|
||
"success": True,
|
||
"count": len(orders),
|
||
"orders": orders,
|
||
"source": "exchange"
|
||
}
|
||
|
||
# 否则从本地数据库获取
|
||
service = get_real_trading_service()
|
||
|
||
if not service:
|
||
return {
|
||
"success": False,
|
||
"message": "实盘交易服务未启用",
|
||
"count": 0,
|
||
"orders": []
|
||
}
|
||
|
||
if status == "active":
|
||
orders = service.get_active_orders()
|
||
elif status == "closed":
|
||
# 从数据库获取历史订单
|
||
from app.services.db_service import db_service
|
||
from app.models.real_trading import RealOrder
|
||
from app.models.paper_trading import OrderStatus
|
||
|
||
db = db_service.get_session()
|
||
try:
|
||
query = db.query(RealOrder).filter(
|
||
RealOrder.status.in_([OrderStatus.CLOSED, OrderStatus.CANCELLED])
|
||
)
|
||
|
||
if symbol:
|
||
query = query.filter(RealOrder.symbol == symbol)
|
||
|
||
orders = [order.to_dict() for order in query.order_by(
|
||
RealOrder.created_at.desc()
|
||
).limit(limit).all()]
|
||
finally:
|
||
db.close()
|
||
else:
|
||
# 返回所有订单
|
||
active = service.get_active_orders()
|
||
# TODO: 获取历史订单
|
||
orders = active
|
||
|
||
return {
|
||
"success": True,
|
||
"count": len(orders),
|
||
"orders": orders,
|
||
"source": "database"
|
||
}
|
||
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_real_trading_service()
|
||
|
||
if not service:
|
||
return {
|
||
"success": False,
|
||
"message": "实盘交易服务未启用",
|
||
"count": 0,
|
||
"orders": []
|
||
}
|
||
|
||
orders = service.get_active_orders()
|
||
|
||
# 如果指定了交易对,进行过滤
|
||
if symbol:
|
||
orders = [o for o in orders if o.get('symbol') == 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_real_trading_service()
|
||
|
||
if not service:
|
||
raise HTTPException(status_code=404, detail="实盘交易服务未启用")
|
||
|
||
order = service.get_order(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.get("/positions")
|
||
async def get_positions():
|
||
"""获取实盘持仓(从交易所同步)"""
|
||
try:
|
||
# 即使实盘交易未启用,也可以查看交易所持仓
|
||
trading_api = get_bitget_trading_api()
|
||
|
||
if not trading_api:
|
||
return {
|
||
"success": False,
|
||
"message": "Bitget API 未配置",
|
||
"positions": []
|
||
}
|
||
|
||
positions = trading_api.get_position()
|
||
|
||
return {
|
||
"success": True,
|
||
"count": len(positions),
|
||
"positions": positions
|
||
}
|
||
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:
|
||
# 直接使用交易 API,不依赖实盘交易服务
|
||
trading_api = get_bitget_trading_api()
|
||
|
||
if not trading_api:
|
||
return {
|
||
"success": False,
|
||
"message": "Bitget API 未配置",
|
||
"account": None
|
||
}
|
||
|
||
# 获取账户余额
|
||
balance_info = trading_api.get_balance()
|
||
usdt_info = balance_info.get('USDT', {})
|
||
|
||
available = float(usdt_info.get('available', 0))
|
||
frozen = float(usdt_info.get('frozen', 0))
|
||
locked = float(usdt_info.get('locked', 0))
|
||
|
||
# 获取持仓价值
|
||
positions = trading_api.get_position()
|
||
total_position_value = sum(
|
||
float(p.get('notional', 0)) for p in positions
|
||
)
|
||
|
||
account = {
|
||
'current_balance': available + frozen + locked,
|
||
'available': available,
|
||
'used_margin': frozen + locked,
|
||
'total_position_value': total_position_value
|
||
}
|
||
|
||
return {
|
||
"success": True,
|
||
"account": account
|
||
}
|
||
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, exit_price: float = Query(..., description="平仓价格")):
|
||
"""
|
||
手动平仓
|
||
|
||
注意:实盘交易通常由交易所自动执行止损/止盈,
|
||
此接口主要用于紧急情况下的手动平仓
|
||
"""
|
||
try:
|
||
service = get_real_trading_service()
|
||
|
||
if not service:
|
||
raise HTTPException(status_code=404, detail="实盘交易服务未启用")
|
||
|
||
# 获取订单
|
||
order = service.get_order(order_id)
|
||
if not order:
|
||
raise HTTPException(status_code=404, detail="订单不存在")
|
||
|
||
# 调用交易所API平仓
|
||
# TODO: 实现手动平仓逻辑
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "平仓指令已发送",
|
||
"order_id": order_id
|
||
}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"实盘手动平仓失败: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.get("/stats")
|
||
async def get_trading_stats():
|
||
"""获取实盘交易统计"""
|
||
try:
|
||
service = get_real_trading_service()
|
||
|
||
if not service:
|
||
return {
|
||
"success": False,
|
||
"message": "实盘交易服务未启用",
|
||
"stats": None
|
||
}
|
||
|
||
# 获取账户信息
|
||
account = service.get_account_status()
|
||
|
||
# 获取历史订单统计
|
||
from app.services.db_service import db_service
|
||
from app.models.real_trading import RealOrder
|
||
from app.models.paper_trading import OrderStatus
|
||
|
||
db = db_service.get_session()
|
||
try:
|
||
# 获取已平仓订单
|
||
closed_orders = db.query(RealOrder).filter(
|
||
RealOrder.status == OrderStatus.CLOSED
|
||
).all()
|
||
|
||
# 计算统计数据
|
||
total_trades = len(closed_orders)
|
||
winning_trades = len([o for o in closed_orders if o.pnl > 0])
|
||
losing_trades = len([o for o in closed_orders if o.pnl < 0])
|
||
total_pnl = sum([o.pnl or 0 for o in closed_orders])
|
||
win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
|
||
|
||
# 计算最大回撤等指标
|
||
# TODO: 实现更详细的统计
|
||
|
||
stats = {
|
||
"total_trades": total_trades,
|
||
"winning_trades": winning_trades,
|
||
"losing_trades": losing_trades,
|
||
"win_rate": round(win_rate, 2),
|
||
"total_pnl": round(total_pnl, 2),
|
||
"current_balance": account.get('current_balance', 0),
|
||
"available": account.get('available', 0),
|
||
"used_margin": account.get('used_margin', 0),
|
||
"total_position_value": account.get('total_position_value', 0),
|
||
}
|
||
finally:
|
||
db.close()
|
||
|
||
return {
|
||
"success": True,
|
||
"stats": stats
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"获取实盘交易统计失败: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.get("/status")
|
||
async def get_service_status():
|
||
"""获取实盘交易服务状态"""
|
||
try:
|
||
from app.config import get_settings
|
||
settings = get_settings()
|
||
|
||
service = get_real_trading_service()
|
||
|
||
status = {
|
||
"enabled": settings.real_trading_enabled,
|
||
"api_configured": bool(settings.bitget_api_key and settings.bitget_api_secret),
|
||
"use_testnet": settings.bitget_use_testnet,
|
||
"service_running": service is not None,
|
||
"max_single_position": settings.real_trading_max_single_position,
|
||
"default_leverage": settings.real_trading_default_leverage,
|
||
"max_orders": settings.real_trading_max_orders,
|
||
}
|
||
|
||
if service:
|
||
account = service.get_account_status()
|
||
status["account"] = account
|
||
|
||
return {
|
||
"success": True,
|
||
"status": status
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"获取实盘交易服务状态失败: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|