From aeefb21f4efdc517188751eb25d53d37ab2b9ff3 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Mon, 30 Mar 2026 13:47:58 +0800 Subject: [PATCH] 11 --- .../services/bitget_live_trading_service.py | 71 +++++++---- .../app/services/bitget_trading_api_sdk.py | 115 ++++++++++++------ .../tests/test_bitget_live_trading_service.py | 26 ++-- 3 files changed, 130 insertions(+), 82 deletions(-) diff --git a/backend/app/services/bitget_live_trading_service.py b/backend/app/services/bitget_live_trading_service.py index 80e9e77..6764135 100644 --- a/backend/app/services/bitget_live_trading_service.py +++ b/backend/app/services/bitget_live_trading_service.py @@ -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}") diff --git a/backend/app/services/bitget_trading_api_sdk.py b/backend/app/services/bitget_trading_api_sdk.py index 3e39d2b..db818df 100644 --- a/backend/app/services/bitget_trading_api_sdk.py +++ b/backend/app/services/bitget_trading_api_sdk.py @@ -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: diff --git a/backend/tests/test_bitget_live_trading_service.py b/backend/tests/test_bitget_live_trading_service.py index fe8b225..0099e96 100644 --- a/backend/tests/test_bitget_live_trading_service.py +++ b/backend/tests/test_bitget_live_trading_service.py @@ -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)