stock-ai-agent/backend/tests/test_bitget_balance.py
2026-03-30 11:56:28 +08:00

289 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
BitgetTradingAPI.get_balance() 单元测试
覆盖场景:
1. UTA 模式 → privateUtaGetV3AccountAssets真实路径
2. 经典账户模式 → privateMixGetV2MixAccountAccounts
3. API 异常 → 返回 {}
4. 空 assets / 无 USDT
5. get_account_state 端到端解析
"""
import importlib.util
import os
import sys
import types
from pathlib import Path
from unittest.mock import MagicMock
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
def load_bitget_sdk_class():
sdk_path = (
Path(__file__).resolve().parents[1]
/ 'app' / 'services' / 'bitget_trading_api_sdk.py'
)
if 'ccxt' not in sys.modules:
ccxt_module = types.ModuleType('ccxt')
ccxt_module.BaseError = Exception
ccxt_module.bitget = MagicMock()
sys.modules['ccxt'] = ccxt_module
if 'app.config' not in sys.modules:
mock_cfg = types.ModuleType('app.config')
mock_cfg.get_settings = MagicMock()
sys.modules['app.config'] = mock_cfg
if 'app.utils.logger' not in sys.modules:
mock_log = types.ModuleType('app.utils.logger')
mock_log.logger = MagicMock()
sys.modules['app.utils.logger'] = mock_log
module_name = 'app.services.bitget_trading_api_sdk_bal_test'
spec = importlib.util.spec_from_file_location(module_name, sdk_path)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module.BitgetTradingAPI
# ──────────────────────────────────────────────
# 测试 1: UTA 模式,正常返回(匹配真实 API
# ──────────────────────────────────────────────
def test_uta_balance_success():
"""UTA 模式 → privateUtaGetV3AccountAssets 返回 1975.37"""
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': {
'assets': [
{
'coin': 'USDT',
'available': '1975.37457492',
'locked': '0',
'equity': '1975.37457492',
}
],
},
}
balance = api.get_balance()
assert balance['USDT']['available'] == '1975.37457492'
assert balance['USDT']['equity'] == '1975.37457492'
assert balance['USDT']['frozen'] == '0'
api.exchange.privateUtaGetV3AccountAssets.assert_called_once_with({'coin': 'USDT'})
# ──────────────────────────────────────────────
# 测试 2: UTA 多币种返回
# ──────────────────────────────────────────────
def test_uta_multi_coins():
"""UTA 返回多个币种,正确解析"""
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': {
'assets': [
{'coin': 'USDT', 'available': '1000.00', 'locked': '200.00', 'equity': '1200.00'},
{'coin': 'BGB', 'available': '50.00', 'locked': '0', 'equity': '50.00'},
],
},
}
balance = api.get_balance()
assert balance['USDT']['available'] == '1000.00'
assert balance['USDT']['frozen'] == '200.00'
assert balance['BGB']['available'] == '50.00'
# ──────────────────────────────────────────────
# 测试 3: 经典合约账户模式
# ──────────────────────────────────────────────
def test_contract_account_balance():
"""经典账户 → privateMixGetV2MixAccountAccounts"""
BitgetTradingAPI = load_bitget_sdk_class()
api = BitgetTradingAPI.__new__(BitgetTradingAPI)
api.exchange = MagicMock()
api.use_unified_account = False
api.exchange.privateMixGetV2MixAccountAccounts.return_value = {
'code': '00000',
'data': [
{
'marginCoin': 'USDT',
'available': '800.50',
'locked': '100.00',
'accountEquity': '900.50',
}
],
}
balance = api.get_balance()
assert balance['USDT']['available'] == '800.50'
assert balance['USDT']['frozen'] == '100.00'
assert balance['USDT']['equity'] == '900.50'
# ──────────────────────────────────────────────
# 测试 4: API 异常 → 返回 {}
# ──────────────────────────────────────────────
def test_api_exception_returns_empty():
"""API 抛异常 → 返回空字典"""
BitgetTradingAPI = load_bitget_sdk_class()
api = BitgetTradingAPI.__new__(BitgetTradingAPI)
api.exchange = MagicMock()
api.use_unified_account = True
api.exchange.privateUtaGetV3AccountAssets.side_effect = Exception("network error")
balance = api.get_balance()
assert balance == {}
# ──────────────────────────────────────────────
# 测试 5: UTA 空 assets → 返回 {}
# ──────────────────────────────────────────────
def test_uta_empty_assets():
"""UTA API 返回空 assets 列表"""
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': {'assets': []},
}
balance = api.get_balance()
assert balance == {}
# ──────────────────────────────────────────────
# 测试 6: UTA assets 无 coin 字段 → 跳过
# ──────────────────────────────────────────────
def test_uta_skip_no_coin():
"""assets 中 entry 无 coin → 跳过该条目"""
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': {
'assets': [
{'coin': '', 'available': '100'},
{'coin': 'USDT', 'available': '500.00', 'locked': '0', 'equity': '500.00'},
],
},
}
balance = api.get_balance()
assert 'USDT' in balance
assert balance['USDT']['available'] == '500.00'
assert len(balance) == 1
# ──────────────────────────────────────────────
# 测试 7: UTA assets 中字段为 None → 解析为 '0'
# ──────────────────────────────────────────────
def test_uta_none_fields():
"""UTA assets 中 available/locked 为 None → 解析为 '0'"""
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': {
'assets': [
{'coin': 'USDT', 'available': None, 'locked': None, 'equity': None},
],
},
}
balance = api.get_balance()
assert balance['USDT']['available'] == '0' # _pick_first_value 对 None 返回默认值 '0'
# ──────────────────────────────────────────────
# 测试 8: get_account_state 端到端解析(正常余额)
# ──────────────────────────────────────────────
def test_get_account_state_e2e():
"""get_account_state 正确解析 balance 返回值"""
balance_return = {
'USDT': {
'available': '1975.50',
'frozen': '24.50',
'locked': '24.50',
'equity': '2000.00',
}
}
usdt = balance_return.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)
account_value = equity if equity > 0 else (available + frozen)
assert available == 1975.50
assert frozen == 24.50
assert equity == 2000.00
assert account_value == 2000.00
# ──────────────────────────────────────────────
# 测试 9: balance 返回 {} 时 get_account_state 各字段为 0
# ──────────────────────────────────────────────
def test_get_account_state_empty_balance():
"""balance 返回空 → 全部为 0"""
balance_return = {}
usdt = balance_return.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)
account_value = equity if equity > 0 else (available + frozen)
assert available == 0.0
assert frozen == 0.0
assert account_value == 0.0
# ──────────────────────────────────────────────
# 测试 10: UTA 返回 data=None → 返回 {}
# ──────────────────────────────────────────────
def test_uta_data_none():
"""UTA API 返回 data=None"""
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': None,
}
balance = api.get_balance()
assert balance == {}