11
This commit is contained in:
parent
8533f7c4b4
commit
aeefb21f4e
@ -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}")
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user