This commit is contained in:
aaron 2026-03-30 13:47:58 +08:00
parent 8533f7c4b4
commit aeefb21f4e
3 changed files with 130 additions and 82 deletions

View File

@ -289,12 +289,18 @@ class BitgetLiveTradingService:
type='market', type='market',
side=side, side=side,
amount=actual_amount, amount=actual_amount,
params=self.trading_api._with_account_mode_params({ params=self.trading_api._with_account_mode_params(
'tdMode': 'cross', {
'marginCoin': 'USDT', 'hedged': True,
'holdMode': 'oneWay', 'marginCoin': 'USDT',
**params **({'reduceOnly': True} if reduce_only else {}),
}) } if self.trading_api.use_unified_account else {
'tdMode': 'cross',
'marginCoin': 'USDT',
'holdMode': 'oneWay',
**params,
}
)
) )
if not order: if not order:
@ -337,13 +343,22 @@ class BitgetLiveTradingService:
""" """
try: try:
side = 'buy' if is_buy else 'sell' side = 'buy' if is_buy else 'sell'
params = {
'tdMode': 'cross', if self.trading_api.use_unified_account:
'marginCoin': 'USDT', order_params = {
'holdMode': 'oneWay', 'hedged': True,
} 'marginCoin': 'USDT',
if reduce_only: }
params['reduceOnly'] = True 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) ccxt_symbol = self.trading_api._standardize_symbol(symbol)
contract_size = self.get_contract_size(symbol) contract_size = self.get_contract_size(symbol)
@ -355,7 +370,7 @@ class BitgetLiveTradingService:
side=side, side=side,
amount=actual_amount, amount=actual_amount,
price=price, price=price,
params=self.trading_api._with_account_mode_params(params) params=self.trading_api._with_account_mode_params(order_params)
) )
if not order: if not order:
@ -626,17 +641,13 @@ class BitgetLiveTradingService:
return {"success": all_ok, "results": results} return {"success": all_ok, "results": results}
def _floor_amount(self, symbol: str, amount: float) -> float: def _floor_amount(self, symbol: str, amount: float) -> float:
"""根据交易对精度向下取整数量""" """根据交易对精度向下取整数量(使用 CCXT 内置精度处理)"""
try: try:
ccxt_symbol = self.trading_api._standardize_symbol(symbol) ccxt_symbol = self.trading_api._standardize_symbol(symbol)
market = self.trading_api.exchange.market(ccxt_symbol) return float(self.trading_api.exchange.amount_to_precision(ccxt_symbol, amount))
precision = market.get('precision', {}).get('amount')
if precision and precision > 0:
factor = 10 ** precision
return math.floor(amount * factor) / factor
except Exception: except Exception:
pass pass
# fallback: 4 位小数 # fallback: 4 位小数截断
return math.floor(amount * 10000) / 10000 return math.floor(amount * 10000) / 10000
def market_close_position(self, symbol: str) -> Dict[str, Any]: def market_close_position(self, symbol: str) -> Dict[str, Any]:
@ -658,15 +669,23 @@ class BitgetLiveTradingService:
try: try:
ccxt_symbol = self.trading_api._standardize_symbol(coin + 'USDT') ccxt_symbol = self.trading_api._standardize_symbol(coin + 'USDT')
side = 'sell' if is_long else 'buy' 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( order = self.trading_api.exchange.create_market_order(
symbol=ccxt_symbol, symbol=ccxt_symbol,
side=side, side=side,
amount=coin_amount, amount=coin_amount,
params=self.trading_api._with_account_mode_params({ params=self.trading_api._with_account_mode_params(close_params)
'reduceOnly': True,
'tdMode': 'cross',
'marginCoin': 'USDT',
})
) )
if order: if order:
logger.info(f"✅ Bitget 单币种平仓成功: {coin} {side} {coin_amount}") logger.info(f"✅ Bitget 单币种平仓成功: {coin} {side} {coin_amount}")

View File

@ -116,12 +116,20 @@ class BitgetTradingAPI:
actual_amount = size * contract_size actual_amount = size * contract_size
# 构建订单参数 # 构建订单参数
# 单向持仓模式 + 联合保证金模式 # UTA 账户统一账户使用双向持仓模式hedge mode
params = { # hedged=True → CCXT 设置 posSide: 'long'(buy) 或 'short'(sell)
'tdMode': 'cross', # 联合保证金模式(全仓) # 经典账户使用单向持仓模式holdMode: 'oneWay'
'marginCoin': 'USDT', # 保证金币种 if self.use_unified_account:
'holdMode': 'oneWay', # 单向持仓模式 params = {
} 'hedged': True,
'marginCoin': 'USDT',
}
else:
params = {
'tdMode': 'cross',
'marginCoin': 'USDT',
'holdMode': 'oneWay',
}
params = self._with_account_mode_params(params) params = self._with_account_mode_params(params)
if client_order_id: if client_order_id:
@ -133,9 +141,6 @@ class BitgetTradingAPI:
if take_profit: if take_profit:
params['takeProfit'] = str(take_profit) params['takeProfit'] = str(take_profit)
# Bitget 合约交易特殊参数
params['holdMode'] = 'oneWay' # 单向持仓模式
# 调试:打印下单参数 # 调试:打印下单参数
logger.info(f"下单参数: symbol={ccxt_symbol}, type={order_type}, side={side}, 张数={size}, 实际amount={actual_amount}") logger.info(f"下单参数: symbol={ccxt_symbol}, type={order_type}, side={side}, 张数={size}, 实际amount={actual_amount}")
logger.debug(f"完整参数: {params}") logger.debug(f"完整参数: {params}")
@ -284,11 +289,18 @@ class BitgetTradingAPI:
# 直接使用 CCXT 下市价平仓单(绕过 place_order 的张数转换) # 直接使用 CCXT 下市价平仓单(绕过 place_order 的张数转换)
order_type = 'market' if not price else 'limit' order_type = 'market' if not price else 'limit'
params = { if self.use_unified_account:
'reduceOnly': True, params = {
'tdMode': 'cross', 'hedged': True,
'marginCoin': 'USDT', 'reduceOnly': True,
} 'marginCoin': 'USDT',
}
else:
params = {
'reduceOnly': True,
'tdMode': 'cross',
'marginCoin': 'USDT',
}
params = self._with_account_mode_params(params) params = self._with_account_mode_params(params)
if price: if price:
@ -353,12 +365,21 @@ class BitgetTradingAPI:
close_side = 'sell' if pos_side == 'long' else 'buy' close_side = 'sell' if pos_side == 'long' else 'buy'
# 使用 CCXT 统一接口下 trailing stop 条件单 # 使用 CCXT 统一接口下 trailing stop 条件单
params = self._with_account_mode_params({ if self.use_unified_account:
'tdMode': 'cross', ts_params = {
'marginCoin': 'USDT', 'hedged': True,
'reduceOnly': True, 'reduceOnly': True,
'trailingPercent': callback_rate * 100 if callback_rate else None, # CCXT 需要百分比 'marginCoin': 'USDT',
}) 'trailingPercent': callback_rate * 100 if callback_rate else None,
}
else:
ts_params = {
'tdMode': 'cross',
'marginCoin': 'USDT',
'reduceOnly': True,
'trailingPercent': callback_rate * 100 if callback_rate else None,
}
params = self._with_account_mode_params(ts_params)
if activation_price: if activation_price:
params['activationPrice'] = activation_price params['activationPrice'] = activation_price
@ -455,20 +476,29 @@ class BitgetTradingAPI:
sl_side = 'sell' if pos_side == 'long' else 'buy' sl_side = 'sell' if pos_side == 'long' else 'buy'
try: try:
# 使用普通的 create_order 创建止损市价单 # 使用普通的 create_order 创建止损市价单
if self.use_unified_account:
sl_params = {
'stopPrice': stop_loss,
'triggerBy': 'mark_price',
'hedged': True,
'reduceOnly': True,
'marginCoin': 'USDT',
}
else:
sl_params = {
'stopPrice': stop_loss,
'triggerBy': 'mark_price',
'tdMode': 'cross',
'marginCoin': 'USDT',
'reduceOnly': True,
}
sl_order = self.exchange.create_order( sl_order = self.exchange.create_order(
symbol=ccxt_symbol, symbol=ccxt_symbol,
type='stop_market', type='stop_market',
side=sl_side, side=sl_side,
amount=btc_amount, amount=btc_amount,
price=None, price=None,
params={ params=self._with_account_mode_params(sl_params),
'stopPrice': stop_loss,
'triggerBy': 'mark_price',
'tdMode': 'cross',
'marginCoin': 'USDT',
'reduceOnly': True, # 只平仓
**self._with_account_mode_params(),
}
) )
orders_created.append(('止损', sl_order)) orders_created.append(('止损', sl_order))
logger.info(f"✅ 止损单已下: {sl_side} {btc_amount} BTC @ ${stop_loss}") logger.info(f"✅ 止损单已下: {sl_side} {btc_amount} BTC @ ${stop_loss}")
@ -480,18 +510,25 @@ class BitgetTradingAPI:
tp_side = 'sell' if pos_side == 'long' else 'buy' tp_side = 'sell' if pos_side == 'long' else 'buy'
try: try:
# 使用普通的 create_order 创建止盈限价单 # 使用普通的 create_order 创建止盈限价单
if self.use_unified_account:
tp_params = {
'hedged': True,
'reduceOnly': True,
'marginCoin': 'USDT',
}
else:
tp_params = {
'tdMode': 'cross',
'marginCoin': 'USDT',
'reduceOnly': True,
}
tp_order = self.exchange.create_order( tp_order = self.exchange.create_order(
symbol=ccxt_symbol, symbol=ccxt_symbol,
type='limit', type='limit',
side=tp_side, side=tp_side,
amount=btc_amount, amount=btc_amount,
price=take_profit, price=take_profit,
params={ params=self._with_account_mode_params(tp_params),
'tdMode': 'cross',
'marginCoin': 'USDT',
'reduceOnly': True, # 只平仓
**self._with_account_mode_params(),
}
) )
orders_created.append(('止盈', tp_order)) orders_created.append(('止盈', tp_order))
logger.info(f"✅ 止盈单已下: {tp_side} {btc_amount} BTC @ ${take_profit}") logger.info(f"✅ 止盈单已下: {tp_side} {btc_amount} BTC @ ${take_profit}")
@ -904,15 +941,13 @@ class BitgetTradingAPI:
return 1.0 return 1.0
def _floor_amount(self, ccxt_symbol: str, amount: float) -> float: def _floor_amount(self, ccxt_symbol: str, amount: float) -> float:
"""根据交易对精度向下取整数量""" """根据交易对精度向下取整数量(使用 CCXT 内置精度处理)"""
try: try:
market = self.exchange.market(ccxt_symbol) # CCXT amount_to_precision 正确处理 tick_size 和 decimal_places 两种模式
precision = market.get('precision', {}).get('amount') return float(self.exchange.amount_to_precision(ccxt_symbol, amount))
if precision and precision > 0:
factor = 10 ** precision
return math.floor(amount * factor) / factor
except Exception: except Exception:
pass pass
# 回退4 位小数截断
return math.floor(amount * 10000) / 10000 return math.floor(amount * 10000) / 10000
def _get_min_amount(self, ccxt_symbol: str) -> float: def _get_min_amount(self, ccxt_symbol: str) -> float:

View File

@ -950,19 +950,18 @@ class TestUTAParams:
call_kwargs = mock_api.exchange.create_market_order.call_args[1] call_kwargs = mock_api.exchange.create_market_order.call_args[1]
params = call_kwargs.get('params', {}) params = call_kwargs.get('params', {})
assert params.get('uta') is True, f"market_close_position 缺少 uta 参数: {params}" assert params.get('uta') is True, f"market_close_position 缺少 uta 参数: {params}"
assert params.get('reduceOnly') is True assert params.get('hedged') is True, f"market_close_position 缺少 hedged=True: {params}"
def test_place_market_order_preserves_cross_margin_params(self): def test_place_market_order_preserves_cross_margin_params(self):
"""确保 UTA 参数不覆盖其他必要参数""" """确保 UTA V3 hedge mode 参数正确传递"""
service, mock_api = make_service() service, mock_api = make_service()
mock_api.exchange.create_order.return_value = {'id': 'o1', 'status': 'closed'} mock_api.exchange.create_order.return_value = {'id': 'o1', 'status': 'closed'}
service.place_market_order('BTC', is_buy=True, size=1) service.place_market_order('BTC', is_buy=True, size=1)
call_kwargs = mock_api.exchange.create_order.call_args[1] call_kwargs = mock_api.exchange.create_order.call_args[1]
params = call_kwargs.get('params', {}) params = call_kwargs.get('params', {})
assert params.get('tdMode') == 'cross' assert params.get('hedged') is True
assert params.get('marginCoin') == 'USDT' assert params.get('marginCoin') == 'USDT'
assert params.get('holdMode') == 'oneWay'
assert params.get('uta') is True assert params.get('uta') is True
@ -972,35 +971,30 @@ class TestFloorAmount:
"""测试动态精度向下取整""" """测试动态精度向下取整"""
def test_floor_with_market_precision(self): def test_floor_with_market_precision(self):
"""从 market info 获取精度""" """amount_to_precision 正确截断"""
service, mock_api = make_service() service, mock_api = make_service()
mock_api.exchange.market.return_value = { mock_api.exchange.amount_to_precision.return_value = '1.23'
'precision': {'amount': 2}
}
result = service._floor_amount('BTCUSDT', 1.23456) result = service._floor_amount('BTCUSDT', 1.23456)
assert result == pytest.approx(1.23) assert result == pytest.approx(1.23)
mock_api.exchange.amount_to_precision.assert_called_once_with('BTC/USDT:USDT', 1.23456)
def test_floor_with_4_decimal_precision(self): def test_floor_with_4_decimal_precision(self):
service, mock_api = make_service() service, mock_api = make_service()
mock_api.exchange.market.return_value = { mock_api.exchange.amount_to_precision.return_value = '0.1234'
'precision': {'amount': 4}
}
result = service._floor_amount('BTCUSDT', 0.12345) result = service._floor_amount('BTCUSDT', 0.12345)
assert result == pytest.approx(0.1234) assert result == pytest.approx(0.1234)
def test_floor_fallback_when_market_unavailable(self): def test_floor_fallback_when_market_unavailable(self):
"""market info 失败时回退到 4 位小数""" """amount_to_precision 失败时回退到 4 位小数"""
service, mock_api = make_service() service, mock_api = make_service()
mock_api.exchange.market.side_effect = Exception("not found") mock_api.exchange.amount_to_precision.side_effect = Exception("not found")
result = service._floor_amount('UNKNOWN', 1.23456789) result = service._floor_amount('UNKNOWN', 1.23456789)
assert result == pytest.approx(1.2345) assert result == pytest.approx(1.2345)
def test_floor_truncates_not_rounds(self): def test_floor_truncates_not_rounds(self):
"""必须向下取整,不能四舍五入""" """必须向下取整,不能四舍五入"""
service, mock_api = make_service() service, mock_api = make_service()
mock_api.exchange.market.return_value = { mock_api.exchange.amount_to_precision.return_value = '1.99'
'precision': {'amount': 2}
}
result = service._floor_amount('BTCUSDT', 1.999) result = service._floor_amount('BTCUSDT', 1.999)
assert result == pytest.approx(1.99) assert result == pytest.approx(1.99)