1
This commit is contained in:
parent
2fe6a1602b
commit
520428895b
@ -1137,10 +1137,10 @@ class CryptoAgent:
|
||||
next_decision = decision.get('next_decision')
|
||||
|
||||
if decision_type == 'HOLD':
|
||||
reasoning = decision.get('reasoning', decision.get('reason', '观望'))
|
||||
logger.info(f"\n📊 交易决策: {reasoning}")
|
||||
hold_reason = decision.get('reason', decision.get('reasoning', '观望'))
|
||||
logger.info(f"\n📊 交易决策: {hold_reason}")
|
||||
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
|
||||
|
||||
@ -2221,8 +2221,14 @@ class CryptoAgent:
|
||||
available = account.get('available', account.get('available_balance', 0))
|
||||
balance = account.get('current_balance', 0)
|
||||
|
||||
if available <= 0 or balance <= 0:
|
||||
return 0, "账户余额无效"
|
||||
logger.info(f" 仓位计算: available=${available}, balance={balance}, account_keys={list(account.keys())}")
|
||||
|
||||
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, {})
|
||||
@ -2583,10 +2589,10 @@ class CryptoAgent:
|
||||
next_decision = decision.get('next_decision')
|
||||
|
||||
if decision_type == 'HOLD':
|
||||
reasoning = decision.get('reasoning', decision.get('reason', '观望'))
|
||||
logger.info(f" Bitget 决策: {reasoning}")
|
||||
hold_reason = decision.get('reason', decision.get('reasoning', '观望'))
|
||||
logger.info(f" Bitget 决策: {hold_reason}")
|
||||
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
|
||||
|
||||
@ -2925,10 +2931,10 @@ class CryptoAgent:
|
||||
next_decision = decision.get('next_decision')
|
||||
|
||||
if decision_type == 'HOLD':
|
||||
reasoning = decision.get('reasoning', decision.get('reason', '观望'))
|
||||
logger.info(f" Hyperliquid 决策: {reasoning}")
|
||||
hold_reason = decision.get('reason', decision.get('reasoning', '观望'))
|
||||
logger.info(f" Hyperliquid 决策: {hold_reason}")
|
||||
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
|
||||
|
||||
@ -3446,11 +3452,14 @@ class CryptoAgent:
|
||||
|
||||
# 决策信息
|
||||
decision_type = decision.get('decision', 'HOLD')
|
||||
decision_reason = decision.get('reason', '')
|
||||
decision_reasoning = decision.get('reasoning', '')
|
||||
|
||||
# 如果有外部传入的 reason(订单创建失败的具体原因),优先使用
|
||||
if reason:
|
||||
final_reason = reason
|
||||
elif decision_reason:
|
||||
final_reason = decision_reason
|
||||
elif decision_reasoning:
|
||||
final_reason = decision_reasoning
|
||||
else:
|
||||
|
||||
@ -40,6 +40,17 @@ class BitgetLiveTradingService:
|
||||
接口与 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):
|
||||
self.settings = get_settings()
|
||||
self.max_total_leverage: float = self.settings.bitget_max_total_leverage
|
||||
@ -86,12 +97,45 @@ class BitgetLiveTradingService:
|
||||
balance = self.trading_api.get_balance()
|
||||
logger.debug(f"[Bitget] get_balance 原始返回: {balance}")
|
||||
|
||||
usdt = balance.get('USDT', {})
|
||||
available = float(usdt.get('available', 0) or 0)
|
||||
frozen = float(usdt.get('frozen', 0) or 0)
|
||||
equity = float(usdt.get('equity', 0) or 0)
|
||||
usdt = balance.get('USDT') or balance.get('usdt') or {}
|
||||
if not usdt:
|
||||
for currency, asset in balance.items():
|
||||
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)
|
||||
|
||||
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(
|
||||
f"[Bitget] 账户状态: available=${available:.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_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):
|
||||
"""
|
||||
初始化 Bitget 交易 API
|
||||
@ -709,17 +718,49 @@ class BitgetTradingAPI:
|
||||
response = self.exchange.privateUtaGetV3AccountAssets({
|
||||
'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 = {}
|
||||
for entry in assets:
|
||||
currency = entry.get('coin')
|
||||
currency = entry.get('coin') or entry.get('marginCoin') or entry.get('asset')
|
||||
if not currency:
|
||||
continue
|
||||
currency = str(currency).upper()
|
||||
result[currency] = {
|
||||
'available': str(entry.get('available', '0')),
|
||||
'frozen': str(entry.get('locked', '0')),
|
||||
'locked': str(entry.get('locked', '0')),
|
||||
'equity': str(entry.get('equity', entry.get('balance', '0'))),
|
||||
'available': self._pick_first_value(
|
||||
entry,
|
||||
'available',
|
||||
'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}")
|
||||
return result
|
||||
|
||||
@ -99,6 +99,20 @@ class TestGetAccountState:
|
||||
state = service.get_account_state()
|
||||
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):
|
||||
service, mock_api = make_service()
|
||||
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():
|
||||
BitgetTradingAPI = load_bitget_sdk_class()
|
||||
api = BitgetTradingAPI.__new__(BitgetTradingAPI)
|
||||
|
||||
@ -782,8 +782,7 @@
|
||||
signal_grade_text: order.signal_grade || '-',
|
||||
signal_type_text: this.getSignalTypeText(order.signal_type),
|
||||
confidence_text: this.formatConfidence(order.confidence),
|
||||
entry_type_text: this.getEntryTypeText(order.entry_type),
|
||||
reason_preview: this.getPendingReasonPreview(order)
|
||||
entry_type_text: this.getEntryTypeText(order.entry_type)
|
||||
};
|
||||
});
|
||||
},
|
||||
@ -1134,14 +1133,6 @@
|
||||
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) {
|
||||
const map = {
|
||||
'manual': '手动',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user