416 lines
13 KiB
Python
416 lines
13 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="数据源: 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:
|
||
logger.info("[stats] 开始获取统计数据")
|
||
|
||
# 获取账户信息
|
||
account = {}
|
||
trading_api = get_bitget_trading_api()
|
||
logger.info(f"[stats] trading_api: {trading_api}")
|
||
|
||
if trading_api:
|
||
try:
|
||
logger.info("[stats] 开始获取账户信息")
|
||
balance_info = trading_api.get_balance()
|
||
logger.info(f"[stats] balance_info: {balance_info}")
|
||
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))
|
||
|
||
# 获取持仓价值
|
||
logger.info("[stats] 开始获取持仓")
|
||
positions = trading_api.get_position()
|
||
logger.info(f"[stats] positions count: {len(positions)}")
|
||
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
|
||
}
|
||
logger.info(f"[stats] account: {account}")
|
||
except Exception as e:
|
||
logger.error(f"[stats] 获取账户信息失败: {e}")
|
||
import traceback
|
||
logger.error(traceback.format_exc())
|
||
account = {}
|
||
|
||
# 尝试从数据库获取统计
|
||
stats = {
|
||
"total_trades": 0,
|
||
"winning_trades": 0,
|
||
"losing_trades": 0,
|
||
"win_rate": 0,
|
||
"total_pnl": 0,
|
||
"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),
|
||
}
|
||
|
||
logger.info(f"[stats] 返回统计数据: {stats}")
|
||
|
||
return {
|
||
"success": True,
|
||
"stats": stats
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"获取实盘交易统计失败: {e}")
|
||
import traceback
|
||
logger.error(traceback.format_exc())
|
||
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))
|