stock-ai-agent/backend/app/api/real_trading.py
2026-02-23 00:22:17 +08:00

364 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
实盘交易 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))