stock-ai-agent/backend/app/api/real_trading.py
2026-02-23 11:35:28 +08:00

409 lines
12 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="数据源: trades=成交记录, orders=历史订单, exchange=历史订单"),
limit: int = Query(100, description="返回数量限制")
):
"""
获取实盘交易历史数据
- symbol: 可选,按交易对筛选
- status: 可选
- trades: 交易所成交记录(包含每笔成交和手续费)
- orders: 交易所历史订单(包含订单状态)
- exchange: 交易所历史订单(同 orders
- limit: 返回数量限制默认100
"""
try:
trading_api = get_bitget_trading_api()
if not trading_api:
return {
"success": False,
"message": "Bitget API 未配置",
"count": 0,
"orders": []
}
# 获取成交记录(推荐,包含盈亏信息)
if status == "trades":
orders = trading_api.get_closed_orders(symbol, limit)
return {
"success": True,
"count": len(orders),
"orders": orders,
"source": "trades"
}
# 获取历史订单
if status in ["orders", "exchange"]:
try:
if symbol:
ccxt_symbol = trading_api._standardize_symbol(symbol)
orders = trading_api.exchange.fetch_closed_orders(ccxt_symbol, limit=limit)
else:
orders = trading_api.exchange.fetch_closed_orders(limit=limit)
return {
"success": True,
"count": len(orders),
"orders": orders,
"source": "orders"
}
except Exception as e:
logger.error(f"获取历史订单失败: {e}")
return {
"success": False,
"message": f"获取历史订单失败: {str(e)}",
"count": 0,
"orders": []
}
# 默认返回成交记录
orders = trading_api.get_closed_orders(symbol, limit)
return {
"success": True,
"count": len(orders),
"orders": orders,
"source": "trades"
}
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
status["auto_trading_enabled"] = service.get_auto_trading_status()
return {
"success": True,
"status": status
}
except Exception as e:
logger.error(f"获取实盘交易服务状态失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/auto-trading")
async def set_auto_trading(enabled: bool = Query(..., description="是否启用自动交易")):
"""
设置实盘自动交易开关
Args:
enabled: true=启用自动交易false=禁用自动交易
"""
try:
service = get_real_trading_service()
if not service:
raise HTTPException(status_code=404, detail="实盘交易服务未初始化,请检查 API 配置")
success = service.set_auto_trading(enabled)
if success:
status_text = "启用" if enabled else "禁用"
return {
"success": True,
"message": f"实盘自动交易已{status_text}",
"auto_trading_enabled": enabled
}
else:
raise HTTPException(status_code=500, detail="设置自动交易失败")
except HTTPException:
raise
except Exception as e:
logger.error(f"设置自动交易失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/auto-trading")
async def get_auto_trading_status():
"""获取实盘自动交易状态"""
try:
service = get_real_trading_service()
if not service:
return {
"success": False,
"message": "实盘交易服务未初始化",
"auto_trading_enabled": False
}
enabled = service.get_auto_trading_status()
return {
"success": True,
"auto_trading_enabled": enabled
}
except Exception as e:
logger.error(f"获取自动交易状态失败: {e}")
raise HTTPException(status_code=500, detail=str(e))