stock-ai-agent/backend/app/services/bitget_live_trading_service.py
2026-04-25 13:20:28 +08:00

820 lines
31 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

"""
Bitget 实盘交易服务
封装 Bitget U 本位合约实盘交易能力,供 crypto_agent.py 的执行层使用。
"""
import math
from datetime import datetime
from typing import Dict, List, Optional, Any
from app.config import get_settings
from app.services.bitget_trading_api_sdk import get_bitget_trading_api
from app.utils.logger import logger
# 合约面值表(张 → 币数量)
CONTRACT_SIZES: Dict[str, float] = {
'BTC': 0.01,
'ETH': 0.1,
'LTC': 0.1,
'BCH': 0.1,
'BNB': 0.1,
'SOL': 1.0,
'AVAX': 1.0,
'LINK': 1.0,
'UNI': 1.0,
'ATOM': 1.0,
'FIL': 1.0,
'DOT': 1.0,
'XRP': 10.0,
'DOGE': 100.0,
'MATIC': 10.0,
'POL': 10.0,
}
class BitgetLiveTradingService:
"""
Bitget 实盘交易服务
Bitget 实盘交易服务,统一处理账户、持仓、下单和保护单。
"""
@staticmethod
def _safe_float(*values: Any) -> float:
for value in values:
if value in (None, ''):
continue
try:
return float(value)
except (TypeError, ValueError):
continue
return 0.0
def __init__(self, account_id: str = "default", trading_api: Any = None):
self.account_id = (account_id or "default").strip() or "default"
self.settings = get_settings()
self.max_total_leverage: float = self.settings.bitget_max_total_leverage
self.max_single_position: float = self.settings.bitget_max_single_position
self.circuit_breaker_drawdown: float = self.settings.account_max_drawdown
self.trading_api = trading_api or get_bitget_trading_api(self.account_id)
if not self.trading_api:
raise RuntimeError(f"Bitget 交易 API 初始化失败,请检查账号 {self.account_id} 的 API Key 配置")
# 初始余额(用于回撤计算)
self.initial_balance: Optional[float] = None
self._initialize_account()
logger.info(
f"✅ BitgetLiveTradingService 初始化完成 "
f"(account={self.account_id}, "
f"(最大总杠杆: {self.max_total_leverage}x, "
f"单笔上限: ${self.max_single_position}, "
f"熔断阈值: {self.circuit_breaker_drawdown * 100:.0f}%)"
)
def _initialize_account(self):
"""初始化账户状态,记录初始余额"""
try:
state = self.get_account_state()
self.initial_balance = state["account_value"]
logger.info(f"Bitget 初始账户余额: ${self.initial_balance:.2f}")
except Exception as e:
logger.warning(f"Bitget 初始化账户余额失败: {e}")
# ==================== 账户 ====================
def get_account_state(self) -> Dict[str, Any]:
"""
获取账户状态
Returns:
{
"account_value": float, # 账户总价值USDT
"total_margin_used": float, # 已用保证金
"available_balance": float, # 可用余额
}
"""
balance = self.trading_api.get_balance()
account_tag = f"[Bitget:{getattr(self, 'account_id', 'default')}]"
if not balance:
logger.warning(f"{account_tag} get_balance() 返回空API 调用可能失败")
else:
logger.debug(f"{account_tag} get_balance 原始返回: {balance}")
usdt = balance.get('USDT') or balance.get('usdt') or {}
if not usdt:
for currency, asset in balance.items():
if str(currency).upper() == 'USDT':
usdt = asset
break
available = self._safe_float(
usdt.get('available'),
usdt.get('availableBalance'),
usdt.get('crossedMaxAvailable'),
usdt.get('maxTransferOut'),
usdt.get('free'),
)
frozen = self._safe_float(
usdt.get('frozen'),
usdt.get('locked'),
usdt.get('occupied'),
usdt.get('used'),
)
equity = self._safe_float(
usdt.get('equity'),
usdt.get('accountEquity'),
usdt.get('usdtEquity'),
usdt.get('balance'),
usdt.get('accountBalance'),
usdt.get('totalEquity'),
)
account_value = equity if equity > 0 else (available + frozen)
if available <= 0 and account_value > 0:
inferred_available = max(account_value - frozen, 0.0)
if inferred_available > 0:
logger.warning(
f"{account_tag} 可用余额字段缺失,使用 account_value - frozen 回退: "
f"${account_value:.2f} - ${frozen:.2f} = ${inferred_available:.2f}"
)
available = inferred_available
logger.info(
f"{account_tag} 账户状态: available=${available:.2f}, "
f"frozen=${frozen:.2f}, equity=${equity:.2f}, account_value=${account_value:.2f}"
)
return {
"account_value": account_value,
"current_balance": account_value,
"total_margin_used": frozen,
"available_balance": available,
}
def check_risk_limits(self) -> Dict[str, Any]:
"""
检查风控限制
Returns:
{"allowed": bool, "reason": str}
"""
try:
state = self.get_account_state()
account_value = state["account_value"]
# 1. 熔断检查
if self.initial_balance and self.initial_balance > 0:
drawdown = (self.initial_balance - account_value) / self.initial_balance
if drawdown >= self.circuit_breaker_drawdown:
return {
"allowed": False,
"reason": f"熔断触发: 回撤 {drawdown * 100:.1f}% >= 阈值 {self.circuit_breaker_drawdown * 100:.0f}%"
}
# 2. 总杠杆检查
positions = self.get_open_positions()
total_position_value = sum(
abs(p["size"]) * p["entry_price"] for p in positions
)
if account_value > 0:
current_leverage = total_position_value / account_value
if current_leverage >= self.max_total_leverage:
return {
"allowed": False,
"reason": f"总杠杆超限: {current_leverage:.1f}x >= {self.max_total_leverage}x"
}
return {"allowed": True, "reason": ""}
except Exception as e:
logger.error(f"Bitget 风控检查异常: {e}")
return {"allowed": False, "reason": f"风控检查异常: {e}"}
# ==================== 持仓 ====================
def get_open_positions(self) -> List[Dict[str, Any]]:
"""
获取所有持仓
Returns:
[
{
"coin": "BTC",
"size": float, # 正数=多, 负数=空(以币为单位)
"entry_price": float,
"unrealized_pnl": float,
"leverage": int,
"liquidation_price": Optional[float],
"position": dict, # 原始数据
}
]
"""
raw_positions = self.trading_api.get_position()
result = []
for pos in raw_positions:
# 使用 info.available 获取实际持仓量(币数量)
# 注意CCXT 的 'contracts' 字段对于 Bitget 实际上已经是币数量,不是张数
info = pos.get('info', {})
available = float(info.get('available', 0))
if available == 0:
continue
symbol_raw = pos.get('symbol', '') # e.g. "BTC/USDT:USDT"
coin = symbol_raw.split('/')[0] if '/' in symbol_raw else symbol_raw.replace('/USDT:USDT', '')
# available 已经是币数量,不需要再乘以 contract_size
coin_amount = available
side = pos.get('side', 'long')
size = coin_amount if side == 'long' else -coin_amount
tp_sl_prices = self.get_tp_sl_prices(coin)
opened_at_raw = info.get('cTime') or pos.get('timestamp')
if isinstance(opened_at_raw, (int, float)):
opened_at = datetime.fromtimestamp(opened_at_raw / 1000).isoformat()
else:
opened_at = pos.get('datetime') or opened_at_raw
result.append({
"coin": coin,
"symbol": f"{coin}USDT",
"side": "buy" if size > 0 else "sell",
"size": size,
"entry_price": float(pos.get('entryPrice', 0) or 0),
"unrealized_pnl": float(pos.get('unrealizedPnl', 0) or 0),
"leverage": int(float(pos.get('leverage', 1) or 1)),
"liquidation_price": float(pos.get('liquidationPrice', 0) or 0) or None,
"stop_loss": tp_sl_prices.get('stop_loss'),
"take_profit": tp_sl_prices.get('take_profit'),
"opened_at": opened_at,
"position": pos,
})
return result
def get_position_for_symbol(self, symbol: str) -> Optional[Dict[str, Any]]:
"""获取指定币种的持仓,无持仓返回 None"""
coin = symbol.replace('USDT', '').replace('/', '').upper()
for pos in self.get_open_positions():
if pos['coin'] == coin:
return pos
return None
# ==================== 下单 ====================
def place_market_order(
self,
symbol: str,
is_buy: bool,
size: int,
reduce_only: bool = False
) -> Dict[str, Any]:
"""
市价下单
Args:
symbol: 币种,如 "BTC"
is_buy: True=买入/做多, False=卖出/做空
size: 合约数量(张)
reduce_only: 是否只减仓
Returns:
{"success": bool, "order_id": str, "symbol": str, "side": str, "size": int, "error"?: str}
"""
try:
side = 'buy' if is_buy else 'sell'
params = {}
if reduce_only:
params['reduceOnly'] = True
ccxt_symbol = self.trading_api._standardize_symbol(symbol)
contract_size = self.get_contract_size(symbol)
actual_amount = size * contract_size
order = self.trading_api.exchange.create_order(
symbol=ccxt_symbol,
type='market',
side=side,
amount=actual_amount,
params=self.trading_api._with_account_mode_params(
{
'hedged': True,
'marginCoin': 'USDT',
**({'reduceOnly': True} if reduce_only else {}),
} if self.trading_api.use_unified_account else {
'tdMode': 'cross',
'marginCoin': 'USDT',
'holdMode': 'oneWay',
**params,
}
)
)
if not order:
return {"success": False, "error": "下单返回空", "symbol": symbol, "side": side, "size": size}
logger.info(f"✅ Bitget 市价单成功: {symbol} {side} {size}")
return {
"success": True,
"order_id": str(order.get('id', '')),
"symbol": symbol,
"side": side,
"size": size,
}
except Exception as e:
logger.error(f"❌ Bitget 市价单失败: {symbol} {e}")
return {"success": False, "error": str(e), "symbol": symbol, "side": "buy" if is_buy else "sell", "size": size}
def place_limit_order(
self,
symbol: str,
is_buy: bool,
size: int,
price: float,
reduce_only: bool = False
) -> Dict[str, Any]:
"""
限价下单
Returns:
{
"success": bool,
"order_status": "resting" | "filled",
"order_id": str,
"symbol": str,
"side": str,
"size": int,
"price": float,
"error"?: str
}
"""
try:
side = 'buy' if is_buy else 'sell'
if self.trading_api.use_unified_account:
order_params = {
'hedged': True,
'marginCoin': 'USDT',
}
if reduce_only:
order_params['reduceOnly'] = True
else:
order_params = {
'tdMode': 'cross',
'marginCoin': 'USDT',
'holdMode': 'oneWay',
}
if reduce_only:
order_params['reduceOnly'] = True
ccxt_symbol = self.trading_api._standardize_symbol(symbol)
contract_size = self.get_contract_size(symbol)
actual_amount = size * contract_size
order = self.trading_api.exchange.create_order(
symbol=ccxt_symbol,
type='limit',
side=side,
amount=actual_amount,
price=price,
params=self.trading_api._with_account_mode_params(order_params)
)
if not order:
return {
"success": False,
"error": "下单返回空",
"symbol": symbol,
"side": side,
"size": size,
"price": price,
}
# 判断订单状态open → resting挂单中closed → filled立即成交
raw_status = order.get('status', 'open')
order_status = 'filled' if raw_status == 'closed' else 'resting'
logger.info(f"✅ Bitget 限价单: {symbol} {side} {size}张 @ {price} [{order_status}]")
return {
"success": True,
"order_status": order_status,
"order_id": str(order.get('id', '')),
"symbol": symbol,
"side": side,
"size": size,
"price": price,
}
except Exception as e:
logger.error(f"❌ Bitget 限价单失败: {symbol} {e}")
return {
"success": False,
"error": str(e),
"symbol": symbol,
"side": "buy" if is_buy else "sell",
"size": size,
"price": price,
}
# ==================== 止盈止损 ====================
def set_tp_sl(
self,
symbol: str,
is_long: bool,
size: int,
tp_price: Optional[float] = None,
sl_price: Optional[float] = None
) -> Dict[str, Any]:
"""
设置止盈止损
Returns:
{"success": bool, "tp_set": bool, "sl_set": bool, "errors": [...]}
"""
try:
result = self.trading_api.modify_sl_tp(
symbol=symbol,
stop_loss=sl_price,
take_profit=tp_price,
)
if result.get("success"):
logger.info(f"✅ Bitget TP/SL 设置成功: {symbol} TP={tp_price} SL={sl_price}")
else:
errors = result.get("errors", [])
tp_set = result.get("tp_set", False)
sl_set = result.get("sl_set", False)
if tp_set or sl_set:
logger.warning(f"⚠️ Bitget TP/SL 部分成功: {symbol} tp_set={tp_set} sl_set={sl_set} errors={errors}")
else:
logger.error(f"❌ Bitget TP/SL 设置失败: {symbol} errors={errors}")
return result
except Exception as e:
logger.error(f"❌ Bitget 设置 TP/SL 失败: {symbol} {e}")
return {"success": False, "tp_set": False, "sl_set": False, "errors": [str(e)]}
def get_tp_sl_prices(self, symbol: str) -> Dict[str, Optional[float]]:
"""
从挂单中读取止盈止损价格
Returns:
{"take_profit": float|None, "stop_loss": float|None}
"""
result = {"take_profit": None, "stop_loss": None}
try:
orders = self.trading_api.get_open_orders(symbol)
for order in orders:
if not order.get('reduceOnly'):
continue
order_side = order.get('side', '')
info = order.get('info') or {}
price = self._safe_float(
order.get('price'),
order.get('stopPrice'),
order.get('triggerPrice'),
info.get('triggerPrice'),
info.get('executePrice'),
info.get('stopSurplusTriggerPrice'),
info.get('stopLossTriggerPrice'),
info.get('takeProfit'),
info.get('stopLoss'),
)
order_type = order.get('type', '')
plan_type = str(info.get('planType') or info.get('strategyType') or '').lower()
take_profit_price = self._safe_float(
info.get('takeProfit'),
info.get('tpTriggerPrice'),
info.get('stopSurplusTriggerPrice'),
)
stop_loss_price = self._safe_float(
info.get('stopLoss'),
info.get('slTriggerPrice'),
info.get('stopLossTriggerPrice'),
)
if take_profit_price:
result['take_profit'] = take_profit_price
if stop_loss_price:
result['stop_loss'] = stop_loss_price
# 兼容独立 TP/SL 策略单和经典账户 reduce-only 挂单。
if 'loss' in plan_type or 'stop_loss' in order_type.lower() or 'stop-loss' in order_type.lower():
result['stop_loss'] = stop_loss_price or price
elif 'profit' in plan_type or 'take_profit' in order_type.lower() or 'take-profit' in order_type.lower():
result['take_profit'] = take_profit_price or price
elif order_type == 'uta_tpsl':
continue
elif 'stop' in order_type.lower():
result['stop_loss'] = price
elif order_type == 'limit' and price:
result['take_profit'] = price
except Exception as e:
logger.warning(f"Bitget 获取 TP/SL 价格失败: {symbol} {e}")
return result
# ==================== 挂单管理 ====================
def get_open_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
"""
获取挂单列表
Returns:
[{"order_id", "symbol", "side", "size", "price", "is_reduce_only"}]
"""
raw_orders = self.trading_api.get_open_orders(symbol)
result = []
for order in raw_orders:
sym_raw = order.get('symbol', '')
coin = sym_raw.split('/')[0] if '/' in sym_raw else sym_raw
contracts = float(order.get('amount', 0) or 0)
contract_size = self.get_contract_size(coin)
size_in_coins = contracts # ccxt amount 已是币数量
created_at_raw = order.get('timestamp')
if isinstance(created_at_raw, (int, float)):
created_at = datetime.fromtimestamp(created_at_raw / 1000).isoformat()
else:
created_at = order.get('datetime') or created_at_raw
info = order.get('info') or {}
display_price = self._safe_float(
order.get('price'),
order.get('stopPrice'),
order.get('triggerPrice'),
info.get('takeProfit'),
info.get('stopLoss'),
)
result.append({
"order_id": str(order.get('id', '')),
"symbol": coin,
"side": order.get('side', ''),
"size": size_in_coins,
"price": display_price,
"is_reduce_only": bool(order.get('reduceOnly', False)),
"order_type": order.get('type', ''),
"take_profit": info.get('takeProfit') or info.get('tpTriggerPrice'),
"stop_loss": info.get('stopLoss') or info.get('slTriggerPrice'),
"created_at": created_at,
})
return result
def cancel_order(self, symbol: str, order_id: str) -> Dict[str, Any]:
"""
撤销单个挂单
Returns:
{"success": bool, "order_id": str, "error"?: str}
"""
try:
open_orders = self.get_open_orders(symbol)
normalized_order_id = str(order_id)
matched_order = next((o for o in open_orders if str(o.get('order_id', '')) == normalized_order_id), None)
if not matched_order:
logger.info(f" Bitget 挂单已不存在,视为撤单完成: {symbol} #{order_id}")
return {
"success": True,
"order_id": normalized_order_id,
"symbol": symbol,
"already_closed": True,
"message": "订单已不在挂单列表,可能已成交、已撤销或已失效",
}
success = self.trading_api.cancel_order(symbol=symbol, order_id=order_id)
if success:
logger.info(f"✅ Bitget 单笔撤单成功: {symbol} #{order_id}")
return {"success": True, "order_id": normalized_order_id, "symbol": symbol}
return {"success": False, "order_id": str(order_id), "error": "cancel_order 返回 False"}
except Exception as e:
logger.error(f"❌ Bitget 单笔撤单失败: {symbol} #{order_id} {e}")
return {"success": False, "order_id": str(order_id), "error": str(e)}
def cancel_all_orders(self, symbol: Optional[str] = None) -> Dict[str, Any]:
"""
撤销指定币种的所有挂单
Returns:
{"success": bool, "cancelled": int, "error"?: str}
"""
try:
success = self.trading_api.cancel_all_orders(symbol or '')
if success:
logger.info(f"✅ Bitget 撤销挂单成功: {symbol or '全部'}")
return {"success": True, "cancelled": 1}
else:
return {"success": False, "cancelled": 0, "error": "cancel_all_orders 返回 False"}
except Exception as e:
logger.error(f"❌ Bitget 撤销挂单失败: {symbol} {e}")
return {"success": False, "cancelled": 0, "error": str(e)}
def cancel_tp_sl_orders(self, symbol: str) -> Dict[str, Any]:
"""撤销止盈止损单reduce-only 挂单)"""
return self.cancel_all_orders(symbol)
# ==================== 杠杆 ====================
def update_leverage(self, symbol: str, leverage: int) -> bool:
"""设置杠杆倍数"""
try:
return self.trading_api.set_leverage(symbol, leverage)
except Exception as e:
logger.warning(f"Bitget 设置杠杆失败: {symbol} {leverage}x: {e}")
return False
def sync_default_leverage(self, symbols: List[str], leverage: Optional[int] = None) -> Dict[str, Any]:
"""将默认杠杆同步到一组交易对"""
target_leverage = int(leverage or self.settings.bitget_default_leverage or 10)
normalized_symbols = []
for symbol in symbols:
if not symbol:
continue
normalized = symbol.replace("/", "").strip().upper()
if normalized.endswith("USDT"):
normalized = normalized[:-4]
if normalized:
normalized_symbols.append(normalized)
if not normalized_symbols:
logger.info("Bitget 默认杠杆同步跳过:没有可用交易对")
return {"success": True, "leverage": target_leverage, "results": {}}
deduped_symbols = list(dict.fromkeys(normalized_symbols))
logger.info(f"开始同步 Bitget 默认杠杆: {target_leverage}x -> {deduped_symbols}")
results: Dict[str, bool] = {}
for symbol in deduped_symbols:
results[symbol] = self.update_leverage(symbol, target_leverage)
success = all(results.values()) if results else True
if success:
logger.info(f"Bitget 默认杠杆同步完成: {target_leverage}x")
else:
failed_symbols = [symbol for symbol, ok in results.items() if not ok]
logger.warning(f"Bitget 默认杠杆同步存在失败: {failed_symbols}")
return {
"success": success,
"leverage": target_leverage,
"results": results,
}
# ==================== 辅助方法 ====================
def get_contract_size(self, symbol: str) -> float:
"""
获取合约面值1张合约对应的币数量
优先从硬编码表获取,不存在则查询 ccxt 市场信息。
"""
coin = symbol.replace('USDT', '').replace('/', '').upper()
if coin in CONTRACT_SIZES:
return CONTRACT_SIZES[coin]
# fallback: 从 ccxt market info 获取
try:
ccxt_symbol = self.trading_api._standardize_symbol(symbol)
market = self.trading_api.exchange.market(ccxt_symbol)
size = float(market.get('contractSize', 1) or 1)
logger.info(f"Bitget 从市场信息获取合约面值: {coin} = {size}")
return size
except Exception:
logger.warning(f"Bitget 无法获取 {coin} 合约面值,使用默认值 1")
return 1.0
def coins_to_contracts(self, symbol: str, coin_amount: float, price: float = 1.0) -> int:
"""
将币数量转换为合约张数(向下取整)
Args:
symbol: 币种
coin_amount: 要转换的币数量
price: 当前价格(未使用,保留接口兼容)
Returns:
整数合约张数
"""
contract_size = self.get_contract_size(symbol)
if contract_size <= 0:
return 0
return math.floor(coin_amount / contract_size)
def market_close_all(self) -> Dict[str, Any]:
"""市价平仓所有持仓"""
results = []
positions = self.get_open_positions()
for pos in positions:
results.append(self.market_close_position(pos['coin']))
all_ok = all(r.get('success') for r in results)
return {"success": all_ok, "results": results}
def _floor_amount(self, symbol: str, amount: float) -> float:
"""根据交易对精度向下取整数量(使用 CCXT 内置精度处理)"""
try:
ccxt_symbol = self.trading_api._standardize_symbol(symbol)
return float(self.trading_api.exchange.amount_to_precision(ccxt_symbol, amount))
except Exception:
pass
# fallback: 4 位小数截断
return math.floor(amount * 10000) / 10000
def market_close_position(self, symbol: str) -> Dict[str, Any]:
"""按交易对市价平仓单个持仓"""
coin = symbol.replace('USDT', '').replace('/', '').upper()
position = self.get_position_for_symbol(coin)
if not position:
return {"success": False, "coin": coin, "error": "未找到持仓"}
is_long = position['size'] > 0
coin_amount = self._floor_amount(coin + 'USDT', abs(position['size']))
if coin_amount < 0.0001:
logger.warning(f"{coin} 持仓过小 ({coin_amount}),跳过")
return {"success": True, "coin": coin, "size": 0}
self.cancel_tp_sl_orders(coin)
try:
ccxt_symbol = self.trading_api._standardize_symbol(coin + 'USDT')
side = 'sell' if is_long else 'buy'
if self.trading_api.use_unified_account:
close_params = {
'hedged': True,
'reduceOnly': True,
'marginCoin': 'USDT',
}
else:
close_params = {
'reduceOnly': True,
'tdMode': 'cross',
'marginCoin': 'USDT',
}
order = self.trading_api.exchange.create_market_order(
symbol=ccxt_symbol,
side=side,
amount=coin_amount,
params=self.trading_api._with_account_mode_params(close_params)
)
if order:
logger.info(f"✅ Bitget 单币种平仓成功: {coin} {side} {coin_amount}")
return {
"success": True,
"coin": coin,
"size": coin_amount,
"order_id": str(order.get('id', '')),
}
return {"success": False, "coin": coin, "error": "返回空"}
except Exception as e:
logger.error(f"❌ Bitget 单币种平仓失败: {coin} {e}")
return {"success": False, "coin": coin, "error": str(e)}
# ==================== 单例工厂 ====================
_bitget_live_services: Dict[str, BitgetLiveTradingService] = {}
def get_bitget_live_service(account_id: str = "default") -> Optional[BitgetLiveTradingService]:
"""
获取 BitgetLiveTradingService 单例(按账号)。
bitget_trading_enabled=False 时返回 None功能关闭
"""
global _bitget_live_services
settings = get_settings()
if not settings.bitget_trading_enabled:
return None
normalized_account_id = (account_id or "default").strip() or "default"
existing = _bitget_live_services.get(normalized_account_id)
if existing is not None:
return existing
try:
service = BitgetLiveTradingService(account_id=normalized_account_id)
_bitget_live_services[normalized_account_id] = service
return service
except Exception as e:
logger.error(f"❌ BitgetLiveTradingService 初始化失败: account={normalized_account_id} error={e}")
return None
def get_all_bitget_live_services() -> Dict[str, BitgetLiveTradingService]:
"""获取所有已启用 Bitget 账号的服务实例。"""
settings = get_settings()
services: Dict[str, BitgetLiveTradingService] = {}
for account in settings.get_enabled_bitget_accounts():
account_id = account.get("account_id") or "default"
service = get_bitget_live_service(account_id)
if service:
services[account_id] = service
return services
def reset_bitget_live_service(account_id: Optional[str] = None):
"""重置单例(测试用)"""
global _bitget_live_services
if account_id is None:
_bitget_live_services = {}
return
normalized_account_id = (account_id or "default").strip() or "default"
_bitget_live_services.pop(normalized_account_id, None)