This commit is contained in:
aaron 2026-03-30 01:32:35 +08:00
parent 53863709dc
commit df55f2ff37
9 changed files with 320 additions and 53 deletions

View File

@ -159,10 +159,12 @@ class Settings(BaseSettings):
bitget_api_secret: str = "" # Bitget API Secret
bitget_passphrase: str = "" # Bitget API Passphrase
bitget_use_testnet: bool = True # 是否使用测试网(测试时设为 true
bitget_use_unified_account: bool = True # 使用统一账户(UTA)接口
# 实盘风险控制Bitget 实盘共用)
bitget_max_single_position: float = 1000 # 单笔最大持仓金额 (USDT)
bitget_max_total_leverage: float = 10 # 总杠杆上限(倍数)
bitget_default_leverage: int = 10 # 默认执行杠杆(启动时同步到交易对)
# 账户级止损(所有平台通用)
account_max_drawdown: float = 0.25 # 账户最大回撤25%),超过则停止交易并平仓

View File

@ -176,6 +176,12 @@ class CryptoAgent:
# 配置
self.symbols = self.settings.crypto_symbols.split(',')
if self.bitget:
sync_result = self.bitget.sync_default_leverage(
self.symbols,
leverage=self.settings.bitget_default_leverage
)
logger.info(f"Bitget 默认杠杆同步结果: {sync_result}")
# 运行状态
self.running = False

View File

@ -22,11 +22,12 @@ class BitgetExecutor(BaseExecutor):
"""执行开仓"""
try:
symbol = decision.get('symbol', '').replace('USDT', '')
action = decision.get('action') # buy/sell
action = decision.get('signal_action', decision.get('action')) # buy/sell
margin = decision.get('margin', decision.get('quantity', 0))
entry_price = decision.get('entry_price', current_price)
stop_loss = decision.get('stop_loss')
take_profit = decision.get('take_profit')
leverage = min(decision.get('leverage', self.bitget.settings.bitget_default_leverage), 10)
# 决定订单类型
order_type, order_reason = self.decide_order_type(decision, current_price)
@ -37,17 +38,19 @@ class BitgetExecutor(BaseExecutor):
available = account_state.get('available_balance', 0)
adjusted_margin = self.calculate_effective_margin(available, margin)
# 讣算合约张数
contracts = self._calculate_contracts(symbol, adjusted_margin, entry_price)
# 计算合约张数,必须与实际执行杠杆保持一致
contracts = self._calculate_contracts(symbol, adjusted_margin, entry_price, leverage)
if contracts < 1:
return {
'success': False,
'error': f'仓位计算结果 {contracts} 张,低于最小下单量'
'error': (
f'仓位计算结果 {contracts} 张,低于最小下单量 '
f'(保证金=${adjusted_margin:.2f}, 杠杆={leverage}x)'
)
}
# 设置杠杆
leverage = min(decision.get('leverage', 5), 10)
self.bitget.update_leverage(symbol, leverage)
# 下单
@ -319,14 +322,14 @@ class BitgetExecutor(BaseExecutor):
# ==================== 辅助方法 ====================
def _calculate_contracts(self, symbol: str, margin: float, price: float) -> int:
def _calculate_contracts(self, symbol: str, margin: float, price: float, leverage: int) -> int:
"""计算合约张数"""
try:
# 获取合约规格
contract_size = self.bitget.get_contract_size(symbol.replace('USDT', ''))
# 计算持仓价值
position_value = margin * 5 # 假设 5x 杠杆
position_value = margin * leverage
# 计算币数量
coin_amount = position_value / price
@ -334,7 +337,10 @@ class BitgetExecutor(BaseExecutor):
# 计算合约张数(向下取整)
contracts = int(coin_amount / contract_size)
logger.info(f" 仓位计算: ${margin:.2f} USD → {coin_amount:.6f} {symbol}{contracts}")
logger.info(
f" 仓位计算: ${margin:.2f} × {leverage}x = ${position_value:.2f} "
f"{coin_amount:.6f} {symbol}{contracts}"
)
return contracts

View File

@ -21,7 +21,7 @@ class HyperliquidExecutor(BaseExecutor):
"""执行开仓"""
try:
symbol = decision.get('symbol', '').replace('USDT', '')
action = decision.get('action') # buy/sell
action = decision.get('signal_action', decision.get('action')) # buy/sell
margin = decision.get('margin', decision.get('quantity', 0))
entry_price = decision.get('entry_price', current_price)
stop_loss = decision.get('stop_loss')
@ -37,7 +37,7 @@ class HyperliquidExecutor(BaseExecutor):
adjusted_margin = self.calculate_effective_margin(available, margin)
# 计算仓位大小
leverage = min(decision.get('leverage', 5), 10)
leverage = min(decision.get('leverage', 10), 10)
position_size = self._calculate_position_size(symbol, adjusted_margin, entry_price, leverage)
if position_size <= 0:

View File

@ -88,7 +88,8 @@ class BitgetLiveTradingService:
usdt = balance.get('USDT', {})
available = float(usdt.get('available', 0) or 0)
frozen = float(usdt.get('frozen', 0) or 0)
account_value = available + frozen
equity = float(usdt.get('equity', 0) or 0)
account_value = equity if equity > 0 else (available + frozen)
return {
"account_value": account_value,
@ -481,12 +482,49 @@ class BitgetLiveTradingService:
# ==================== 杠杆 ====================
def update_leverage(self, symbol: str, leverage: int):
def update_leverage(self, symbol: str, leverage: int) -> bool:
"""设置杠杆倍数"""
try:
self.trading_api.set_leverage(symbol, leverage)
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,
}
# ==================== 辅助方法 ====================

View File

@ -20,6 +20,9 @@ from app.utils.logger import logger
class BitgetTradingAPI:
"""Bitget 实盘交易 API (基于 CCXT)"""
DEFAULT_PRODUCT_TYPE = 'USDT-FUTURES'
DEFAULT_MARGIN_COIN = 'USDT'
def __init__(self, api_key: str, api_secret: str, passphrase: str = "", use_testnet: bool = True):
"""
初始化 Bitget 交易 API
@ -33,6 +36,9 @@ class BitgetTradingAPI:
self.api_key = api_key
self.api_secret = api_secret
self.use_testnet = use_testnet
from app.config import get_settings
self.settings = get_settings()
self.use_unified_account = getattr(self.settings, 'bitget_use_unified_account', True)
# 创建 CCXT Bitget 实例
config = {
@ -137,6 +143,7 @@ class BitgetTradingAPI:
'marginCoin': 'USDT', # 保证金币种
'holdMode': 'oneWay', # 单向持仓模式
}
params = self._with_account_mode_params(params)
if client_order_id:
params['clientOrderId'] = client_order_id
@ -193,9 +200,9 @@ class BitgetTradingAPI:
ccxt_symbol = self._standardize_symbol(symbol)
if order_id:
self.exchange.cancel_order(order_id, ccxt_symbol)
self.exchange.cancel_order(order_id, ccxt_symbol, self._with_account_mode_params())
elif client_order_id:
self.exchange.cancel_order_by_client_order_id(client_order_id, ccxt_symbol)
self.exchange.cancel_order_by_client_order_id(client_order_id, ccxt_symbol, self._with_account_mode_params())
else:
logger.error("必须提供 order_id 或 client_order_id")
return False
@ -222,7 +229,7 @@ class BitgetTradingAPI:
"""
try:
ccxt_symbol = self._standardize_symbol(symbol)
self.exchange.cancel_all_orders(ccxt_symbol)
self.exchange.cancel_all_orders(ccxt_symbol, self._with_account_mode_params())
logger.info(f"✅ 撤销所有挂单成功: {symbol}")
return True
@ -303,6 +310,7 @@ class BitgetTradingAPI:
'tdMode': 'cross',
'marginCoin': 'USDT',
}
params = self._with_account_mode_params(params)
if price:
order = self.exchange.create_limit_order(
@ -365,6 +373,7 @@ class BitgetTradingAPI:
'symbol': ccxt_symbol,
'trailingStopCallbackRate': callback_rate,
}
params = self._with_account_mode_params(params)
if activation_price:
params['trailingStopActivationPrice'] = activation_price
@ -469,6 +478,7 @@ class BitgetTradingAPI:
'tdMode': 'cross',
'marginCoin': 'USDT',
'reduceOnly': True, # 只平仓
**self._with_account_mode_params(),
}
)
orders_created.append(('止损', sl_order))
@ -491,6 +501,7 @@ class BitgetTradingAPI:
'tdMode': 'cross',
'marginCoin': 'USDT',
'reduceOnly': True, # 只平仓
**self._with_account_mode_params(),
}
)
orders_created.append(('止盈', tp_order))
@ -533,9 +544,9 @@ class BitgetTradingAPI:
ccxt_symbol = self._standardize_symbol(symbol)
if order_id:
order = self.exchange.fetch_order(order_id, ccxt_symbol)
order = self.exchange.fetch_order(order_id, ccxt_symbol, self._with_account_mode_params())
elif client_order_id:
order = self.exchange.fetch_order_by_client_order_id(client_order_id, ccxt_symbol)
order = self.exchange.fetch_order_by_client_order_id(client_order_id, ccxt_symbol, self._with_account_mode_params())
else:
logger.error("必须提供 order_id 或 client_order_id")
return None
@ -561,7 +572,7 @@ class BitgetTradingAPI:
"""
try:
ccxt_symbol = self._standardize_symbol(symbol) if symbol else None
orders = self.exchange.fetch_open_orders(ccxt_symbol)
orders = self.exchange.fetch_open_orders(ccxt_symbol, None, None, self._with_account_mode_params())
logger.debug(f"查询到 {len(orders)} 个挂单")
return orders
@ -586,7 +597,7 @@ class BitgetTradingAPI:
"""
try:
ccxt_symbol = self._standardize_symbol(symbol)
orders = self.exchange.fetch_my_trades(ccxt_symbol, limit=limit)
orders = self.exchange.fetch_my_trades(ccxt_symbol, limit=limit, params=self._with_account_mode_params())
logger.debug(f"查询到 {len(orders)} 条历史订单")
return orders
@ -612,7 +623,7 @@ class BitgetTradingAPI:
"""
try:
# 获取所有持仓
positions = self.exchange.fetch_positions()
positions = self.exchange.fetch_positions(None, self._with_account_mode_params())
# 筛选非零持仓
active_positions = []
@ -691,17 +702,40 @@ class BitgetTradingAPI:
余额信息 {USDT: {available: "...", frozen: "...", locked: "..."}}
"""
try:
balance = self.exchange.fetch_balance()
# 转换为统一格式
result = {}
for currency, info in balance.get('total', {}).items():
if info and info > 0: # 只返回有余额的币种
if self.use_unified_account:
response = self.exchange.privateUtaGetV3AccountAssets({
'coin': self.DEFAULT_MARGIN_COIN,
})
assets = response.get('data', {}).get('assets', []) or []
result = {}
for entry in assets:
currency = entry.get('coin')
if not currency:
continue
result[currency] = {
'available': str(balance.get('free', {}).get(currency, 0)),
'frozen': str(balance.get('used', {}).get(currency, 0)),
'locked': '0'
'available': str(entry.get('available', '0')),
'frozen': str(entry.get('locked', '0')),
'locked': str(entry.get('locked', '0')),
'equity': str(entry.get('equity', entry.get('balance', '0'))),
}
logger.debug(f"账户余额: {result}")
return result
response = self.exchange.privateMixGetV2MixAccountAccounts({
'productType': self.DEFAULT_PRODUCT_TYPE,
})
result = {}
for entry in response.get('data', []) or []:
currency = entry.get('marginCoin')
if not currency:
continue
result[currency] = {
'available': str(entry.get('available', '0')),
'frozen': str(entry.get('locked', '0')),
'locked': str(entry.get('locked', '0')),
'equity': str(entry.get('accountEquity', entry.get('usdtEquity', '0'))),
}
logger.debug(f"账户余额: {result}")
return result
@ -721,8 +755,13 @@ class BitgetTradingAPI:
账户信息
"""
try:
balance = self.exchange.fetch_balance()
return balance
if self.use_unified_account:
return self.exchange.privateUtaGetV3AccountAssets({
'coin': self.DEFAULT_MARGIN_COIN,
})
return self.exchange.privateMixGetV2MixAccountAccounts({
'productType': self.DEFAULT_PRODUCT_TYPE,
})
except ccxt.BaseError as e:
logger.error(f"❌ 查询账户信息失败: {e}")
return {}
@ -743,10 +782,24 @@ class BitgetTradingAPI:
是否成功
"""
try:
ccxt_symbol = self._standardize_symbol(symbol)
if self.use_unified_account:
response = self.exchange.privateUtaPostV3AccountSetLeverage({
'symbol': self._to_contract_symbol_id(symbol),
'coin': self.DEFAULT_MARGIN_COIN,
'category': self.DEFAULT_PRODUCT_TYPE,
'leverage': str(leverage),
})
else:
response = self.exchange.privateMixPostV2MixAccountSetLeverage({
'symbol': self._to_contract_symbol_id(symbol),
'marginCoin': self.DEFAULT_MARGIN_COIN,
'productType': self.DEFAULT_PRODUCT_TYPE,
'leverage': str(leverage),
})
# CCXT 设置杠杆
self.exchange.set_leverage(leverage, ccxt_symbol)
if response.get('code') != '00000':
logger.error(f"❌ 设置杠杆失败: {response}")
return False
logger.info(f"✅ 设置杠杆成功: {symbol} {leverage}x")
return True
@ -784,6 +837,22 @@ class BitgetTradingAPI:
# 例如BTC -> BTC/USDT:USDT
return f"{symbol}/USDT:USDT"
def _to_contract_symbol_id(self, symbol: str) -> str:
"""转成 Bitget U 本位合约 symbol id例如 BTCUSDT"""
normalized = symbol.strip().upper()
if '/' in normalized:
base = normalized.split('/')[0]
return f"{base}USDT"
if ':' in normalized:
normalized = normalized.split(':')[0]
return normalized if normalized.endswith('USDT') else f"{normalized}USDT"
def _with_account_mode_params(self, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
merged = dict(params or {})
if self.use_unified_account:
merged['uta'] = True
return merged
def _get_contract_size(self, symbol: str) -> float:
"""
获取合约面值每张合约对应的币数量

View File

@ -75,6 +75,14 @@ class TestGetAccountState:
assert state['available_balance'] == pytest.approx(8000.0)
assert state['total_margin_used'] == pytest.approx(2000.0)
def test_equity_preferred_when_present(self):
service, mock_api = make_service()
mock_api.get_balance.return_value = {
'USDT': {'available': '8000.0', 'frozen': '2000.0', 'locked': '2000.0', 'equity': '10500.0'}
}
state = service.get_account_state()
assert state['account_value'] == pytest.approx(10500.0)
def test_usdt_not_present(self):
service, mock_api = make_service()
mock_api.get_balance.return_value = {}
@ -105,6 +113,12 @@ class TestGetOpenPositions:
def _make_raw_position(self, coin='BTC', contracts=1.0, side='long',
entry_price=50000.0, pnl=100.0, leverage=10,
liq_price=45000.0):
contract_sizes = {
'BTC': 0.01,
'ETH': 0.1,
'SOL': 1.0,
}
available = contracts * contract_sizes.get(coin, 1.0)
return {
'symbol': f'{coin}/USDT:USDT',
'contracts': contracts,
@ -113,6 +127,7 @@ class TestGetOpenPositions:
'unrealizedPnl': pnl,
'leverage': leverage,
'liquidationPrice': liq_price,
'info': {'available': str(available)},
}
def test_long_position(self):
@ -189,6 +204,7 @@ class TestGetPositionForSymbol:
'unrealizedPnl': 0.0,
'leverage': 10,
'liquidationPrice': 45000.0,
'info': {'available': '0.01'},
}]
pos = service.get_position_for_symbol('BTC')
assert pos is not None
@ -210,6 +226,7 @@ class TestGetPositionForSymbol:
'unrealizedPnl': 0.0,
'leverage': 5,
'liquidationPrice': 2500.0,
'info': {'available': '0.5'},
}]
pos = service.get_position_for_symbol('ETHUSDT')
assert pos is not None
@ -529,6 +546,7 @@ class TestCheckRiskLimits:
'unrealizedPnl': 0.0,
'leverage': 10,
'liquidationPrice': 50000.0,
'info': {'available': '2'},
}]
result = service.check_risk_limits()
assert result['allowed'] is False
@ -718,15 +736,16 @@ class TestMarketCloseAll:
'unrealizedPnl': 0.0,
'leverage': 10,
'liquidationPrice': 45000.0,
'info': {'available': '0.02'},
}]
mock_api.exchange.create_order.return_value = {'id': 'close1', 'status': 'closed'}
mock_api.exchange.create_market_order.return_value = {'id': 'close1', 'status': 'closed'}
result = service.market_close_all()
assert result['success'] is True
assert len(result['results']) == 1
# 多仓平仓用 sell
call_args = mock_api.exchange.create_order.call_args
assert call_args[1].get('side') or call_args[0][2] == 'sell'
kwargs = mock_api.exchange.create_market_order.call_args.kwargs
assert kwargs['side'] == 'sell'
def test_close_single_short(self):
"""单个空仓:发出 buy 方向的市价单"""
@ -739,24 +758,25 @@ class TestMarketCloseAll:
'unrealizedPnl': 0.0,
'leverage': 5,
'liquidationPrice': 3500.0,
'info': {'available': '0.5'},
}]
mock_api.exchange.create_order.return_value = {'id': 'close2', 'status': 'closed'}
mock_api.exchange.create_market_order.return_value = {'id': 'close2', 'status': 'closed'}
result = service.market_close_all()
assert result['success'] is True
call_args = mock_api.exchange.create_order.call_args
assert call_args[1].get('side') or call_args[0][2] == 'buy'
kwargs = mock_api.exchange.create_market_order.call_args.kwargs
assert kwargs['side'] == 'buy'
def test_close_multiple_positions(self):
"""多个持仓,全部成功"""
service, mock_api = make_service()
mock_api.get_position.return_value = [
{'symbol': 'BTC/USDT:USDT', 'contracts': 1.0, 'side': 'long',
'entryPrice': 50000.0, 'unrealizedPnl': 0.0, 'leverage': 10, 'liquidationPrice': None},
'entryPrice': 50000.0, 'unrealizedPnl': 0.0, 'leverage': 10, 'liquidationPrice': None, 'info': {'available': '0.01'}},
{'symbol': 'ETH/USDT:USDT', 'contracts': 3.0, 'side': 'short',
'entryPrice': 3000.0, 'unrealizedPnl': 0.0, 'leverage': 5, 'liquidationPrice': None},
'entryPrice': 3000.0, 'unrealizedPnl': 0.0, 'leverage': 5, 'liquidationPrice': None, 'info': {'available': '0.3'}},
]
mock_api.exchange.create_order.return_value = {'id': 'x', 'status': 'closed'}
mock_api.exchange.create_market_order.return_value = {'id': 'x', 'status': 'closed'}
result = service.market_close_all()
assert result['success'] is True
@ -775,35 +795,36 @@ class TestMarketCloseAll:
service, mock_api = make_service()
mock_api.get_position.return_value = [
{'symbol': 'BTC/USDT:USDT', 'contracts': 1.0, 'side': 'long',
'entryPrice': 50000.0, 'unrealizedPnl': 0.0, 'leverage': 10, 'liquidationPrice': None},
'entryPrice': 50000.0, 'unrealizedPnl': 0.0, 'leverage': 10, 'liquidationPrice': None, 'info': {'available': '0.01'}},
{'symbol': 'ETH/USDT:USDT', 'contracts': 3.0, 'side': 'long',
'entryPrice': 3000.0, 'unrealizedPnl': 0.0, 'leverage': 5, 'liquidationPrice': None},
'entryPrice': 3000.0, 'unrealizedPnl': 0.0, 'leverage': 5, 'liquidationPrice': None, 'info': {'available': '0.3'}},
]
# 第一次下单成功,第二次失败
mock_api.exchange.create_order.side_effect = [
mock_api.exchange.create_market_order.side_effect = [
{'id': 'ok1', 'status': 'closed'},
Exception("rate limit"),
]
result = service.market_close_all()
assert result['success'] is False
def test_position_too_small_skipped(self):
"""持仓量小于 1 张时跳过,不报错"""
def test_position_above_min_coin_amount_is_closed(self):
"""只要币数量高于最小下单精度,就应允许平仓"""
service, mock_api = make_service()
# BTC 合约面值 0.01,持仓 0.005 BTC → 0 张 → 跳过
mock_api.exchange.create_market_order.return_value = {'id': 'close-small', 'status': 'closed'}
mock_api.get_position.return_value = [{
'symbol': 'BTC/USDT:USDT',
'contracts': 0.5, # 0.5张 × 0.01 = 0.005 BTC → floor(0.005/0.01) = 0张
'contracts': 0.5,
'side': 'long',
'entryPrice': 50000.0,
'unrealizedPnl': 0.0,
'leverage': 10,
'liquidationPrice': None,
'info': {'available': '0.005'},
}]
result = service.market_close_all()
assert result['success'] is True
assert result['results'] == []
mock_api.exchange.create_order.assert_not_called()
assert len(result['results']) == 1
mock_api.exchange.create_market_order.assert_called_once()
# ==================== TestInitializeAccount ====================

View File

@ -0,0 +1,88 @@
"""
BitgetTradingAPI 单元测试
覆盖重点
- U 本位账户余额固定走 mix account 接口
- 杠杆设置固定走 mix set-leverage 接口
"""
import importlib.util
import os
import sys
import types
from pathlib import Path
from unittest.mock import MagicMock
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
def load_bitget_sdk_class():
sdk_path = Path(__file__).resolve().parents[1] / 'app' / 'services' / 'bitget_trading_api_sdk.py'
if 'ccxt' not in sys.modules:
ccxt_module = types.ModuleType('ccxt')
ccxt_module.BaseError = Exception
ccxt_module.bitget = MagicMock()
sys.modules['ccxt'] = ccxt_module
module_name = 'app.services.bitget_trading_api_sdk_test'
spec = importlib.util.spec_from_file_location(module_name, sdk_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module.BitgetTradingAPI
def test_get_balance_uses_usdt_futures_account_endpoint():
BitgetTradingAPI = load_bitget_sdk_class()
api = BitgetTradingAPI.__new__(BitgetTradingAPI)
api.exchange = MagicMock()
api.use_unified_account = True
api.exchange.privateUtaGetV3AccountAssets.return_value = {
'code': '00000',
'data': {
'assets': [
{
'coin': 'USDT',
'available': '123.45',
'locked': '6.78',
'equity': '130.23',
}
],
},
}
balance = api.get_balance()
api.exchange.privateUtaGetV3AccountAssets.assert_called_once_with({
'coin': 'USDT',
})
assert balance == {
'USDT': {
'available': '123.45',
'frozen': '6.78',
'locked': '6.78',
'equity': '130.23',
}
}
def test_set_leverage_uses_mix_contract_endpoint():
BitgetTradingAPI = load_bitget_sdk_class()
api = BitgetTradingAPI.__new__(BitgetTradingAPI)
api.exchange = MagicMock()
api.use_unified_account = True
api.exchange.privateUtaPostV3AccountSetLeverage.return_value = {
'code': '00000',
'msg': 'success',
}
success = api.set_leverage('BTC', 10)
assert success is True
api.exchange.privateUtaPostV3AccountSetLeverage.assert_called_once_with({
'symbol': 'BTCUSDT',
'coin': 'USDT',
'category': 'USDT-FUTURES',
'leverage': '10',
})

View File

@ -127,6 +127,7 @@ def test_bitget_executor_close_uses_symbol_close_not_close_all():
executor = BitgetExecutor.__new__(BitgetExecutor)
executor.bitget = MagicMock()
executor.bitget.settings.bitget_default_leverage = 10
executor.send_execution_notification = AsyncMock()
executor.bitget.market_close_position.return_value = {'success': True, 'coin': 'BTC'}
@ -156,3 +157,39 @@ def test_bitget_executor_move_stop_loss_uses_set_tp_sl():
tp_price=55000.0,
sl_price=49000.0,
)
def test_bitget_executor_open_uses_actual_leverage_for_contracts():
BitgetExecutor = load_bitget_executor_class()
executor = BitgetExecutor.__new__(BitgetExecutor)
executor.bitget = MagicMock()
executor.bitget.settings.bitget_default_leverage = 10
executor.send_execution_notification = AsyncMock()
executor.decide_order_type = MagicMock(return_value=('market', 'test'))
executor.calculate_effective_margin = MagicMock(return_value=32.0)
executor.bitget.get_account_state.return_value = {'available_balance': 1000.0}
executor.bitget.get_contract_size.return_value = 0.1
executor.bitget.place_market_order.return_value = {
'success': True,
'order_id': 'oid-1',
'order_status': 'filled',
}
result = asyncio.run(
executor.execute_open(
{
'symbol': 'ETHUSDT',
'action': 'buy',
'margin': 32.0,
'entry_price': 2000.0,
'stop_loss': 1980.0,
'take_profit': 2040.0,
},
2000.0,
)
)
assert result['success'] is True
executor.bitget.update_leverage.assert_called_once_with('ETH', 10)
executor.bitget.place_market_order.assert_called_once_with('ETH', is_buy=True, size=1)