1
This commit is contained in:
parent
2fe6a1602b
commit
520428895b
@ -1137,10 +1137,10 @@ class CryptoAgent:
|
|||||||
next_decision = decision.get('next_decision')
|
next_decision = decision.get('next_decision')
|
||||||
|
|
||||||
if decision_type == 'HOLD':
|
if decision_type == 'HOLD':
|
||||||
reasoning = decision.get('reasoning', decision.get('reason', '观望'))
|
hold_reason = decision.get('reason', decision.get('reasoning', '观望'))
|
||||||
logger.info(f"\n📊 交易决策: {reasoning}")
|
logger.info(f"\n📊 交易决策: {hold_reason}")
|
||||||
await self._notify_signal_not_executed(
|
await self._notify_signal_not_executed(
|
||||||
market_signal, decision, current_price, reason=f"[模拟盘] {reasoning}", prefix="[模拟盘]"
|
market_signal, decision, current_price, reason=f"[模拟盘] {hold_reason}", prefix="[模拟盘]"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -2221,8 +2221,14 @@ class CryptoAgent:
|
|||||||
available = account.get('available', account.get('available_balance', 0))
|
available = account.get('available', account.get('available_balance', 0))
|
||||||
balance = account.get('current_balance', 0)
|
balance = account.get('current_balance', 0)
|
||||||
|
|
||||||
if available <= 0 or balance <= 0:
|
logger.info(f" 仓位计算: available=${available}, balance={balance}, account_keys={list(account.keys())}")
|
||||||
return 0, "账户余额无效"
|
|
||||||
|
if balance <= 0:
|
||||||
|
logger.warning(f" ❌ 账户权益无效: available={available}, balance={balance}")
|
||||||
|
return 0, "账户权益无效"
|
||||||
|
if available <= 0:
|
||||||
|
logger.warning(f" ❌ 可用保证金无效: available={available}, balance={balance}")
|
||||||
|
return 0, "可用保证金不足或账户可用余额读取失败"
|
||||||
|
|
||||||
# 应用平台规则
|
# 应用平台规则
|
||||||
rules = self.PLATFORM_RULES.get(platform_name, {})
|
rules = self.PLATFORM_RULES.get(platform_name, {})
|
||||||
@ -2583,10 +2589,10 @@ class CryptoAgent:
|
|||||||
next_decision = decision.get('next_decision')
|
next_decision = decision.get('next_decision')
|
||||||
|
|
||||||
if decision_type == 'HOLD':
|
if decision_type == 'HOLD':
|
||||||
reasoning = decision.get('reasoning', decision.get('reason', '观望'))
|
hold_reason = decision.get('reason', decision.get('reasoning', '观望'))
|
||||||
logger.info(f" Bitget 决策: {reasoning}")
|
logger.info(f" Bitget 决策: {hold_reason}")
|
||||||
await self._notify_signal_not_executed(
|
await self._notify_signal_not_executed(
|
||||||
market_signal, decision, current_price, reason=f"[Bitget] {reasoning}", prefix="[Bitget]"
|
market_signal, decision, current_price, reason=f"[Bitget] {hold_reason}", prefix="[Bitget]"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -2925,10 +2931,10 @@ class CryptoAgent:
|
|||||||
next_decision = decision.get('next_decision')
|
next_decision = decision.get('next_decision')
|
||||||
|
|
||||||
if decision_type == 'HOLD':
|
if decision_type == 'HOLD':
|
||||||
reasoning = decision.get('reasoning', decision.get('reason', '观望'))
|
hold_reason = decision.get('reason', decision.get('reasoning', '观望'))
|
||||||
logger.info(f" Hyperliquid 决策: {reasoning}")
|
logger.info(f" Hyperliquid 决策: {hold_reason}")
|
||||||
await self._notify_signal_not_executed(
|
await self._notify_signal_not_executed(
|
||||||
market_signal, decision, current_price, reason=f"[Hyperliquid] {reasoning}", prefix="[Hyperliquid]"
|
market_signal, decision, current_price, reason=f"[Hyperliquid] {hold_reason}", prefix="[Hyperliquid]"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -3446,11 +3452,14 @@ class CryptoAgent:
|
|||||||
|
|
||||||
# 决策信息
|
# 决策信息
|
||||||
decision_type = decision.get('decision', 'HOLD')
|
decision_type = decision.get('decision', 'HOLD')
|
||||||
|
decision_reason = decision.get('reason', '')
|
||||||
decision_reasoning = decision.get('reasoning', '')
|
decision_reasoning = decision.get('reasoning', '')
|
||||||
|
|
||||||
# 如果有外部传入的 reason(订单创建失败的具体原因),优先使用
|
# 如果有外部传入的 reason(订单创建失败的具体原因),优先使用
|
||||||
if reason:
|
if reason:
|
||||||
final_reason = reason
|
final_reason = reason
|
||||||
|
elif decision_reason:
|
||||||
|
final_reason = decision_reason
|
||||||
elif decision_reasoning:
|
elif decision_reasoning:
|
||||||
final_reason = decision_reasoning
|
final_reason = decision_reasoning
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -40,6 +40,17 @@ class BitgetLiveTradingService:
|
|||||||
接口与 HyperliquidTradingService 保持一致,方便 crypto_agent.py 统一调用。
|
接口与 HyperliquidTradingService 保持一致,方便 crypto_agent.py 统一调用。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _safe_float(*values: Any) -> float:
|
||||||
|
for value in values:
|
||||||
|
if value in (None, ''):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return 0.0
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.settings = get_settings()
|
self.settings = get_settings()
|
||||||
self.max_total_leverage: float = self.settings.bitget_max_total_leverage
|
self.max_total_leverage: float = self.settings.bitget_max_total_leverage
|
||||||
@ -86,12 +97,45 @@ class BitgetLiveTradingService:
|
|||||||
balance = self.trading_api.get_balance()
|
balance = self.trading_api.get_balance()
|
||||||
logger.debug(f"[Bitget] get_balance 原始返回: {balance}")
|
logger.debug(f"[Bitget] get_balance 原始返回: {balance}")
|
||||||
|
|
||||||
usdt = balance.get('USDT', {})
|
usdt = balance.get('USDT') or balance.get('usdt') or {}
|
||||||
available = float(usdt.get('available', 0) or 0)
|
if not usdt:
|
||||||
frozen = float(usdt.get('frozen', 0) or 0)
|
for currency, asset in balance.items():
|
||||||
equity = float(usdt.get('equity', 0) or 0)
|
if str(currency).upper() == 'USDT':
|
||||||
|
usdt = asset
|
||||||
|
break
|
||||||
|
|
||||||
|
available = self._safe_float(
|
||||||
|
usdt.get('available'),
|
||||||
|
usdt.get('availableBalance'),
|
||||||
|
usdt.get('crossedMaxAvailable'),
|
||||||
|
usdt.get('maxTransferOut'),
|
||||||
|
usdt.get('free'),
|
||||||
|
)
|
||||||
|
frozen = self._safe_float(
|
||||||
|
usdt.get('frozen'),
|
||||||
|
usdt.get('locked'),
|
||||||
|
usdt.get('occupied'),
|
||||||
|
usdt.get('used'),
|
||||||
|
)
|
||||||
|
equity = self._safe_float(
|
||||||
|
usdt.get('equity'),
|
||||||
|
usdt.get('accountEquity'),
|
||||||
|
usdt.get('usdtEquity'),
|
||||||
|
usdt.get('balance'),
|
||||||
|
usdt.get('accountBalance'),
|
||||||
|
usdt.get('totalEquity'),
|
||||||
|
)
|
||||||
account_value = equity if equity > 0 else (available + frozen)
|
account_value = equity if equity > 0 else (available + frozen)
|
||||||
|
|
||||||
|
if available <= 0 and account_value > 0:
|
||||||
|
inferred_available = max(account_value - frozen, 0.0)
|
||||||
|
if inferred_available > 0:
|
||||||
|
logger.warning(
|
||||||
|
f"[Bitget] 可用余额字段缺失,使用 account_value - frozen 回退: "
|
||||||
|
f"${account_value:.2f} - ${frozen:.2f} = ${inferred_available:.2f}"
|
||||||
|
)
|
||||||
|
available = inferred_available
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[Bitget] 账户状态: available=${available:.2f}, "
|
f"[Bitget] 账户状态: available=${available:.2f}, "
|
||||||
f"frozen=${frozen:.2f}, equity=${equity:.2f}, account_value=${account_value:.2f}"
|
f"frozen=${frozen:.2f}, equity=${equity:.2f}, account_value=${account_value:.2f}"
|
||||||
|
|||||||
@ -23,6 +23,15 @@ class BitgetTradingAPI:
|
|||||||
DEFAULT_PRODUCT_TYPE = 'USDT-FUTURES'
|
DEFAULT_PRODUCT_TYPE = 'USDT-FUTURES'
|
||||||
DEFAULT_MARGIN_COIN = 'USDT'
|
DEFAULT_MARGIN_COIN = 'USDT'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _pick_first_value(entry: Dict[str, Any], *keys: str, default: str = '0') -> str:
|
||||||
|
"""兼容 Bitget UTA 不同字段名,返回第一个非空值。"""
|
||||||
|
for key in keys:
|
||||||
|
value = entry.get(key)
|
||||||
|
if value not in (None, ''):
|
||||||
|
return str(value)
|
||||||
|
return default
|
||||||
|
|
||||||
def __init__(self, api_key: str, api_secret: str, passphrase: str = "", use_testnet: bool = True):
|
def __init__(self, api_key: str, api_secret: str, passphrase: str = "", use_testnet: bool = True):
|
||||||
"""
|
"""
|
||||||
初始化 Bitget 交易 API
|
初始化 Bitget 交易 API
|
||||||
@ -709,17 +718,49 @@ class BitgetTradingAPI:
|
|||||||
response = self.exchange.privateUtaGetV3AccountAssets({
|
response = self.exchange.privateUtaGetV3AccountAssets({
|
||||||
'coin': self.DEFAULT_MARGIN_COIN,
|
'coin': self.DEFAULT_MARGIN_COIN,
|
||||||
})
|
})
|
||||||
assets = response.get('data', {}).get('assets', []) or []
|
data = response.get('data', {})
|
||||||
|
if isinstance(data, list):
|
||||||
|
assets = data
|
||||||
|
else:
|
||||||
|
assets = data.get('assets') or data.get('assetList') or data.get('list') or []
|
||||||
result = {}
|
result = {}
|
||||||
for entry in assets:
|
for entry in assets:
|
||||||
currency = entry.get('coin')
|
currency = entry.get('coin') or entry.get('marginCoin') or entry.get('asset')
|
||||||
if not currency:
|
if not currency:
|
||||||
continue
|
continue
|
||||||
|
currency = str(currency).upper()
|
||||||
result[currency] = {
|
result[currency] = {
|
||||||
'available': str(entry.get('available', '0')),
|
'available': self._pick_first_value(
|
||||||
'frozen': str(entry.get('locked', '0')),
|
entry,
|
||||||
'locked': str(entry.get('locked', '0')),
|
'available',
|
||||||
'equity': str(entry.get('equity', entry.get('balance', '0'))),
|
'availableBalance',
|
||||||
|
'crossedMaxAvailable',
|
||||||
|
'maxTransferOut',
|
||||||
|
'free',
|
||||||
|
),
|
||||||
|
'frozen': self._pick_first_value(
|
||||||
|
entry,
|
||||||
|
'locked',
|
||||||
|
'frozen',
|
||||||
|
'occupied',
|
||||||
|
'used',
|
||||||
|
),
|
||||||
|
'locked': self._pick_first_value(
|
||||||
|
entry,
|
||||||
|
'locked',
|
||||||
|
'frozen',
|
||||||
|
'occupied',
|
||||||
|
'used',
|
||||||
|
),
|
||||||
|
'equity': self._pick_first_value(
|
||||||
|
entry,
|
||||||
|
'equity',
|
||||||
|
'accountEquity',
|
||||||
|
'usdtEquity',
|
||||||
|
'balance',
|
||||||
|
'accountBalance',
|
||||||
|
'totalEquity',
|
||||||
|
),
|
||||||
}
|
}
|
||||||
logger.debug(f"[UTA] 账户余额: {result}")
|
logger.debug(f"[UTA] 账户余额: {result}")
|
||||||
return result
|
return result
|
||||||
|
|||||||
@ -99,6 +99,20 @@ class TestGetAccountState:
|
|||||||
state = service.get_account_state()
|
state = service.get_account_state()
|
||||||
assert state['account_value'] == pytest.approx(0.0)
|
assert state['account_value'] == pytest.approx(0.0)
|
||||||
|
|
||||||
|
def test_alternative_fields_and_inferred_available_supported(self):
|
||||||
|
service, mock_api = make_service()
|
||||||
|
mock_api.get_balance.return_value = {
|
||||||
|
'USDT': {
|
||||||
|
'availableBalance': None,
|
||||||
|
'locked': '10.0',
|
||||||
|
'accountEquity': '210.0',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = service.get_account_state()
|
||||||
|
assert state['account_value'] == pytest.approx(210.0)
|
||||||
|
assert state['available_balance'] == pytest.approx(200.0)
|
||||||
|
assert state['total_margin_used'] == pytest.approx(10.0)
|
||||||
|
|
||||||
def test_api_exception_propagates(self):
|
def test_api_exception_propagates(self):
|
||||||
service, mock_api = make_service()
|
service, mock_api = make_service()
|
||||||
mock_api.get_balance.side_effect = Exception("network error")
|
mock_api.get_balance.side_effect = Exception("network error")
|
||||||
|
|||||||
@ -67,6 +67,37 @@ def test_get_balance_uses_usdt_futures_account_endpoint():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_balance_supports_alternative_uta_asset_fields():
|
||||||
|
BitgetTradingAPI = load_bitget_sdk_class()
|
||||||
|
api = BitgetTradingAPI.__new__(BitgetTradingAPI)
|
||||||
|
api.exchange = MagicMock()
|
||||||
|
api.use_unified_account = True
|
||||||
|
api.exchange.privateUtaGetV3AccountAssets.return_value = {
|
||||||
|
'code': '00000',
|
||||||
|
'data': {
|
||||||
|
'assetList': [
|
||||||
|
{
|
||||||
|
'asset': 'USDT',
|
||||||
|
'availableBalance': '222.2',
|
||||||
|
'occupied': '11.1',
|
||||||
|
'accountEquity': '233.3',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
balance = api.get_balance()
|
||||||
|
|
||||||
|
assert balance == {
|
||||||
|
'USDT': {
|
||||||
|
'available': '222.2',
|
||||||
|
'frozen': '11.1',
|
||||||
|
'locked': '11.1',
|
||||||
|
'equity': '233.3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_set_leverage_uses_mix_contract_endpoint():
|
def test_set_leverage_uses_mix_contract_endpoint():
|
||||||
BitgetTradingAPI = load_bitget_sdk_class()
|
BitgetTradingAPI = load_bitget_sdk_class()
|
||||||
api = BitgetTradingAPI.__new__(BitgetTradingAPI)
|
api = BitgetTradingAPI.__new__(BitgetTradingAPI)
|
||||||
|
|||||||
@ -782,8 +782,7 @@
|
|||||||
signal_grade_text: order.signal_grade || '-',
|
signal_grade_text: order.signal_grade || '-',
|
||||||
signal_type_text: this.getSignalTypeText(order.signal_type),
|
signal_type_text: this.getSignalTypeText(order.signal_type),
|
||||||
confidence_text: this.formatConfidence(order.confidence),
|
confidence_text: this.formatConfidence(order.confidence),
|
||||||
entry_type_text: this.getEntryTypeText(order.entry_type),
|
entry_type_text: this.getEntryTypeText(order.entry_type)
|
||||||
reason_preview: this.getPendingReasonPreview(order)
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -1134,14 +1133,6 @@
|
|||||||
return map[entryType] || entryType || '';
|
return map[entryType] || entryType || '';
|
||||||
},
|
},
|
||||||
|
|
||||||
getPendingReasonPreview(order) {
|
|
||||||
const reasons = Array.isArray(order.entry_reasons)
|
|
||||||
? order.entry_reasons.filter(Boolean)
|
|
||||||
: [];
|
|
||||||
const baseText = reasons.length > 0 ? reasons[0] : '暂无入场理由';
|
|
||||||
return baseText.length > 58 ? `${baseText.slice(0, 58)}...` : baseText;
|
|
||||||
},
|
|
||||||
|
|
||||||
getCloseReason(reason) {
|
getCloseReason(reason) {
|
||||||
const map = {
|
const map = {
|
||||||
'manual': '手动',
|
'manual': '手动',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user