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