276 lines
11 KiB
Python
276 lines
11 KiB
Python
"""
|
|
Bitget 实盘交易 API
|
|
提供 Bitget 实盘交易数据接口
|
|
"""
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from typing import Optional
|
|
|
|
from app.services.bitget_live_trading_service import get_all_bitget_live_services, get_bitget_live_service
|
|
from app.utils.logger import logger
|
|
|
|
|
|
router = APIRouter(prefix="/api/bitget", tags=["Bitget"])
|
|
|
|
|
|
def _get_service(account_id: str = "default"):
|
|
service = get_bitget_live_service(account_id)
|
|
if service is None:
|
|
return None
|
|
return service
|
|
|
|
|
|
@router.get("/account")
|
|
async def get_account():
|
|
"""获取 Bitget 账户状态"""
|
|
try:
|
|
service = _get_service()
|
|
if service is None:
|
|
return {"success": True, "enabled": False,
|
|
"message": "Bitget 服务未启用。请在 .env 中设置 bitget_trading_enabled=true"}
|
|
|
|
state = service.get_account_state()
|
|
positions = service.get_open_positions()
|
|
total_position_value = sum(abs(p["size"]) * p["entry_price"] for p in positions)
|
|
|
|
if state["account_value"] > 0:
|
|
current_leverage = total_position_value / state["account_value"]
|
|
else:
|
|
current_leverage = 0
|
|
|
|
if service.initial_balance and service.initial_balance > 0:
|
|
drawdown = (service.initial_balance - state["account_value"]) / service.initial_balance
|
|
else:
|
|
drawdown = 0
|
|
|
|
return {
|
|
"success": True,
|
|
"enabled": True,
|
|
"message": "Bitget 服务正常",
|
|
"data": {
|
|
"account_value": state["account_value"],
|
|
"available_balance": state["available_balance"],
|
|
"total_margin_used": state["total_margin_used"],
|
|
"total_position_value": total_position_value,
|
|
"current_total_leverage": current_leverage,
|
|
"max_total_leverage": service.max_total_leverage,
|
|
"initial_balance": service.initial_balance,
|
|
"drawdown_percent": drawdown * 100,
|
|
"circuit_breaker_threshold": service.circuit_breaker_drawdown * 100,
|
|
}
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"获取 Bitget 账户状态失败: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/positions")
|
|
async def get_positions(
|
|
symbol: Optional[str] = Query(None, description="币种筛选,如 BTC"),
|
|
account_id: str = Query("default", description="Bitget 账号 ID")
|
|
):
|
|
"""获取 Bitget 持仓信息"""
|
|
try:
|
|
service = _get_service(account_id)
|
|
if service is None:
|
|
return {"success": True, "enabled": False, "positions": []}
|
|
|
|
all_positions = service.get_open_positions()
|
|
|
|
if symbol:
|
|
coin = symbol.replace("USDT", "").upper()
|
|
all_positions = [p for p in all_positions if p["coin"] == coin]
|
|
|
|
positions_data = []
|
|
for pos in all_positions:
|
|
coin = pos["coin"]
|
|
tp_sl = service.get_tp_sl_prices(coin)
|
|
positions_data.append({
|
|
"symbol": f"{coin}USDT",
|
|
"side": "long" if pos["size"] > 0 else "short",
|
|
"size": abs(pos["size"]),
|
|
"entry_price": pos["entry_price"],
|
|
"unrealized_pnl": pos["unrealized_pnl"],
|
|
"leverage": pos.get("leverage", "N/A"),
|
|
"liquidation_price": pos.get("liquidation_price"),
|
|
"take_profit": tp_sl.get("take_profit"),
|
|
"stop_loss": tp_sl.get("stop_loss"),
|
|
})
|
|
|
|
return {"success": True, "enabled": True, "count": len(positions_data), "positions": positions_data}
|
|
except Exception as e:
|
|
logger.error(f"获取 Bitget 持仓失败: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/orders")
|
|
async def get_orders(
|
|
symbol: Optional[str] = Query(None, description="币种筛选,如 BTC"),
|
|
account_id: str = Query("default", description="Bitget 账号 ID")
|
|
):
|
|
"""获取 Bitget 挂单信息"""
|
|
try:
|
|
service = _get_service(account_id)
|
|
if service is None:
|
|
return {"success": True, "enabled": False, "orders": []}
|
|
|
|
coin = symbol.replace("USDT", "").upper() if symbol else None
|
|
all_orders = service.get_open_orders(coin)
|
|
|
|
entry_orders = [o for o in all_orders if not o.get("is_reduce_only")]
|
|
tp_sl_orders = [o for o in all_orders if o.get("is_reduce_only")]
|
|
|
|
return {
|
|
"success": True,
|
|
"enabled": True,
|
|
"counts": {
|
|
"entry_orders": len(entry_orders),
|
|
"tp_sl_orders": len(tp_sl_orders),
|
|
"total": len(all_orders),
|
|
},
|
|
"entry_orders": entry_orders,
|
|
"tp_sl_orders": tp_sl_orders,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"获取 Bitget 挂单失败: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.get("/summary")
|
|
async def get_summary():
|
|
"""获取 Bitget 交易摘要"""
|
|
try:
|
|
services = get_all_bitget_live_services()
|
|
if not services:
|
|
service = _get_service()
|
|
services = {"default": service} if service else {}
|
|
|
|
if not services:
|
|
return {"success": True, "enabled": False, "message": "Bitget 服务未启用"}
|
|
accounts = []
|
|
for account_id, service in services.items():
|
|
state = service.get_account_state()
|
|
positions = service.get_open_positions()
|
|
orders = service.get_open_orders()
|
|
total_position_value = sum(abs(p["size"]) * p["entry_price"] for p in positions)
|
|
current_leverage = total_position_value / state["account_value"] if state["account_value"] > 0 else 0
|
|
drawdown = 0
|
|
if service.initial_balance and service.initial_balance > 0:
|
|
drawdown = (service.initial_balance - state["account_value"]) / service.initial_balance
|
|
accounts.append({
|
|
"account_id": account_id,
|
|
"account": {
|
|
"account_value": state["account_value"],
|
|
"available_balance": state["available_balance"],
|
|
"total_margin_used": state["total_margin_used"],
|
|
},
|
|
"positions": {"count": len(positions), "total_value": total_position_value},
|
|
"orders": {
|
|
"count": len(orders),
|
|
"entry_orders": len([o for o in orders if not o.get("is_reduce_only")]),
|
|
"tp_sl_orders": len([o for o in orders if o.get("is_reduce_only")]),
|
|
},
|
|
"risk": {
|
|
"current_leverage": current_leverage,
|
|
"max_leverage": service.max_total_leverage,
|
|
"leverage_utilization": (current_leverage / service.max_total_leverage * 100) if service.max_total_leverage > 0 else 0,
|
|
"drawdown": drawdown * 100,
|
|
"circuit_breaker_threshold": service.circuit_breaker_drawdown * 100,
|
|
},
|
|
})
|
|
|
|
total_account_value = sum(item["account"]["account_value"] for item in accounts)
|
|
total_available = sum(item["account"]["available_balance"] for item in accounts)
|
|
total_margin = sum(item["account"]["total_margin_used"] for item in accounts)
|
|
total_positions = sum(item["positions"]["count"] for item in accounts)
|
|
total_position_value = sum(item["positions"]["total_value"] for item in accounts)
|
|
total_orders = sum(item["orders"]["count"] for item in accounts)
|
|
return {
|
|
"success": True,
|
|
"enabled": True,
|
|
"data": {
|
|
"account": {
|
|
"account_value": total_account_value,
|
|
"available_balance": total_available,
|
|
"total_margin_used": total_margin,
|
|
},
|
|
"positions": {"count": total_positions, "total_value": total_position_value},
|
|
"orders": {
|
|
"count": total_orders,
|
|
"entry_orders": sum(item["orders"]["entry_orders"] for item in accounts),
|
|
"tp_sl_orders": sum(item["orders"]["tp_sl_orders"] for item in accounts),
|
|
},
|
|
"risk": {
|
|
"current_leverage": total_position_value / total_account_value if total_account_value > 0 else 0,
|
|
"max_leverage": max((item["risk"]["max_leverage"] for item in accounts), default=0),
|
|
"leverage_utilization": 0,
|
|
"drawdown": max((item["risk"]["drawdown"] for item in accounts), default=0),
|
|
"circuit_breaker_threshold": max((item["risk"]["circuit_breaker_threshold"] for item in accounts), default=0),
|
|
},
|
|
"accounts": accounts,
|
|
}
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"获取 Bitget 摘要失败: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("/orders/cancel")
|
|
async def cancel_orders(
|
|
symbol: str = Query(..., description="币种,如 BTC")
|
|
):
|
|
"""取消指定币种的所有挂单"""
|
|
try:
|
|
service = _get_service()
|
|
if service is None:
|
|
return {"success": False, "message": "Bitget 服务未启用"}
|
|
|
|
coin = symbol.replace("USDT", "")
|
|
result = service.cancel_all_orders(coin)
|
|
if result.get("success"):
|
|
return {"success": True, "message": f"已取消 {symbol} 的所有挂单"}
|
|
else:
|
|
return {"success": False, "message": result.get("error", "取消失败")}
|
|
except Exception as e:
|
|
logger.error(f"取消 Bitget 挂单失败: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
@router.post("/positions/close")
|
|
async def close_position(
|
|
symbol: str = Query(..., description="币种,如 BTC")
|
|
):
|
|
"""市价平仓指定币种(⚠️ 立即以市价平仓)"""
|
|
try:
|
|
service = _get_service()
|
|
if service is None:
|
|
return {"success": False, "message": "Bitget 服务未启用"}
|
|
|
|
coin = symbol.replace("USDT", "")
|
|
position = service.get_position_for_symbol(coin)
|
|
if not position:
|
|
return {"success": False, "message": f"未找到 {symbol} 的持仓"}
|
|
|
|
service.cancel_tp_sl_orders(coin)
|
|
|
|
size_in_coins = abs(position["size"])
|
|
is_long = position["size"] > 0
|
|
contracts = service.coins_to_contracts(coin, size_in_coins)
|
|
|
|
if contracts < 1:
|
|
return {"success": False, "message": f"持仓过小,无法下单 ({size_in_coins} 币)"}
|
|
|
|
result = service.place_market_order(
|
|
symbol=coin,
|
|
is_buy=not is_long,
|
|
size=contracts,
|
|
reduce_only=True
|
|
)
|
|
|
|
if result.get("success"):
|
|
return {"success": True, "message": f"已平仓 {symbol} {contracts}张 @ 市价"}
|
|
else:
|
|
return {"success": False, "message": result.get("error", "平仓失败")}
|
|
except Exception as e:
|
|
logger.error(f"Bitget 平仓失败: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|