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',
side=side,
amount=actual_amount,
params=self.trading_api._with_account_mode_params({
'tdMode': 'cross',
'marginCoin': 'USDT',
'holdMode': 'oneWay',
**params
})
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:
@ -337,13 +343,22 @@ class BitgetLiveTradingService:
"""
try:
side = 'buy' if is_buy else 'sell'
params = {
'tdMode': 'cross',
'marginCoin': 'USDT',
'holdMode': 'oneWay',
}
if reduce_only:
params['reduceOnly'] = True
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)
@ -355,7 +370,7 @@ class BitgetLiveTradingService:
side=side,
amount=actual_amount,
price=price,
params=self.trading_api._with_account_mode_params(params)
params=self.trading_api._with_account_mode_params(order_params)
)
if not order:
@ -626,17 +641,13 @@ class BitgetLiveTradingService:
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)
market = self.trading_api.exchange.market(ccxt_symbol)
precision = market.get('precision', {}).get('amount')
if precision and precision > 0:
factor = 10 ** precision
return math.floor(amount * factor) / factor
return float(self.trading_api.exchange.amount_to_precision(ccxt_symbol, amount))
except Exception:
pass
# fallback: 4 位小数
# fallback: 4 位小数截断
return math.floor(amount * 10000) / 10000
def market_close_position(self, symbol: str) -> Dict[str, Any]:
@ -658,15 +669,23 @@ class BitgetLiveTradingService:
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({
'reduceOnly': True,
'tdMode': 'cross',
'marginCoin': 'USDT',
})
params=self.trading_api._with_account_mode_params(close_params)
)
if order:
logger.info(f"✅ Bitget 单币种平仓成功: {coin} {side} {coin_amount}")

View File

@ -116,12 +116,20 @@ class BitgetTradingAPI:
actual_amount = size * contract_size
# 构建订单参数
# 单向持仓模式 + 联合保证金模式
params = {
'tdMode': 'cross', # 联合保证金模式(全仓)
'marginCoin': 'USDT', # 保证金币种
'holdMode': 'oneWay', # 单向持仓模式
}
# UTA 账户统一账户使用双向持仓模式hedge mode
# hedged=True → CCXT 设置 posSide: 'long'(buy) 或 'short'(sell)
# 经典账户使用单向持仓模式holdMode: 'oneWay'
if self.use_unified_account:
params = {
'hedged': True,
'marginCoin': 'USDT',
}
else:
params = {
'tdMode': 'cross',
'marginCoin': 'USDT',
'holdMode': 'oneWay',
}
params = self._with_account_mode_params(params)
if client_order_id:
@ -133,9 +141,6 @@ class BitgetTradingAPI:
if 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.debug(f"完整参数: {params}")
@ -284,11 +289,18 @@ class BitgetTradingAPI:
# 直接使用 CCXT 下市价平仓单(绕过 place_order 的张数转换)
order_type = 'market' if not price else 'limit'
params = {
'reduceOnly': True,
'tdMode': 'cross',
'marginCoin': 'USDT',
}
if self.use_unified_account:
params = {
'hedged': True,
'reduceOnly': True,
'marginCoin': 'USDT',
}
else:
params = {
'reduceOnly': True,
'tdMode': 'cross',
'marginCoin': 'USDT',
}
params = self._with_account_mode_params(params)
if price:
@ -353,12 +365,21 @@ class BitgetTradingAPI:
close_side = 'sell' if pos_side == 'long' else 'buy'
# 使用 CCXT 统一接口下 trailing stop 条件单
params = self._with_account_mode_params({
'tdMode': 'cross',
'marginCoin': 'USDT',
'reduceOnly': True,
'trailingPercent': callback_rate * 100 if callback_rate else None, # CCXT 需要百分比
})
if self.use_unified_account:
ts_params = {
'hedged': True,
'reduceOnly': True,
'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:
params['activationPrice'] = activation_price
@ -455,20 +476,29 @@ class BitgetTradingAPI:
sl_side = 'sell' if pos_side == 'long' else 'buy'
try:
# 使用普通的 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(
symbol=ccxt_symbol,
type='stop_market',
side=sl_side,
amount=btc_amount,
price=None,
params={
'stopPrice': stop_loss,
'triggerBy': 'mark_price',
'tdMode': 'cross',
'marginCoin': 'USDT',
'reduceOnly': True, # 只平仓
**self._with_account_mode_params(),
}
params=self._with_account_mode_params(sl_params),
)
orders_created.append(('止损', sl_order))
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'
try:
# 使用普通的 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(
symbol=ccxt_symbol,
type='limit',
side=tp_side,
amount=btc_amount,
price=take_profit,
params={
'tdMode': 'cross',
'marginCoin': 'USDT',
'reduceOnly': True, # 只平仓
**self._with_account_mode_params(),
}
params=self._with_account_mode_params(tp_params),
)
orders_created.append(('止盈', tp_order))
logger.info(f"✅ 止盈单已下: {tp_side} {btc_amount} BTC @ ${take_profit}")
@ -904,15 +941,13 @@ class BitgetTradingAPI:
return 1.0
def _floor_amount(self, ccxt_symbol: str, amount: float) -> float:
"""根据交易对精度向下取整数量"""
"""根据交易对精度向下取整数量(使用 CCXT 内置精度处理)"""
try:
market = self.exchange.market(ccxt_symbol)
precision = market.get('precision', {}).get('amount')
if precision and precision > 0:
factor = 10 ** precision
return math.floor(amount * factor) / factor
# CCXT amount_to_precision 正确处理 tick_size 和 decimal_places 两种模式
return float(self.exchange.amount_to_precision(ccxt_symbol, amount))
except Exception:
pass
# 回退4 位小数截断
return math.floor(amount * 10000) / 10000
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]
params = call_kwargs.get('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):
"""确保 UTA 参数不覆盖其他必要参数"""
"""确保 UTA V3 hedge mode 参数正确传递"""
service, mock_api = make_service()
mock_api.exchange.create_order.return_value = {'id': 'o1', 'status': 'closed'}
service.place_market_order('BTC', is_buy=True, size=1)
call_kwargs = mock_api.exchange.create_order.call_args[1]
params = call_kwargs.get('params', {})
assert params.get('tdMode') == 'cross'
assert params.get('hedged') is True
assert params.get('marginCoin') == 'USDT'
assert params.get('holdMode') == 'oneWay'
assert params.get('uta') is True
@ -972,35 +971,30 @@ class TestFloorAmount:
"""测试动态精度向下取整"""
def test_floor_with_market_precision(self):
"""从 market info 获取精度"""
"""amount_to_precision 正确截断"""
service, mock_api = make_service()
mock_api.exchange.market.return_value = {
'precision': {'amount': 2}
}
mock_api.exchange.amount_to_precision.return_value = '1.23'
result = service._floor_amount('BTCUSDT', 1.23456)
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):
service, mock_api = make_service()
mock_api.exchange.market.return_value = {
'precision': {'amount': 4}
}
mock_api.exchange.amount_to_precision.return_value = '0.1234'
result = service._floor_amount('BTCUSDT', 0.12345)
assert result == pytest.approx(0.1234)
def test_floor_fallback_when_market_unavailable(self):
"""market info 失败时回退到 4 位小数"""
"""amount_to_precision 失败时回退到 4 位小数"""
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)
assert result == pytest.approx(1.2345)
def test_floor_truncates_not_rounds(self):
"""必须向下取整,不能四舍五入"""
service, mock_api = make_service()
mock_api.exchange.market.return_value = {
'precision': {'amount': 2}
}
mock_api.exchange.amount_to_precision.return_value = '1.99'
result = service._floor_amount('BTCUSDT', 1.999)
assert result == pytest.approx(1.99)