From e0e89624d04d6322ff10103d5647ecd57a349b7a Mon Sep 17 00:00:00 2001 From: aaron <> Date: Mon, 30 Mar 2026 09:20:59 +0800 Subject: [PATCH] 1 --- .../app/services/bitget_trading_api_sdk.py | 38 +-- backend/tests/test_bitget_balance.py | 288 +++++++----------- 2 files changed, 114 insertions(+), 212 deletions(-) diff --git a/backend/app/services/bitget_trading_api_sdk.py b/backend/app/services/bitget_trading_api_sdk.py index 57cb507..50b0bf6 100644 --- a/backend/app/services/bitget_trading_api_sdk.py +++ b/backend/app/services/bitget_trading_api_sdk.py @@ -701,41 +701,14 @@ class BitgetTradingAPI: Returns: 余额信息 {USDT: {available: "...", frozen: "...", locked: "...", equity: "..."}} - 查询策略: - 1. 优先使用 ccxt 统一接口 fetch_balance(),兼容 UTA / 经典账户 - 2. fetch_balance() 失败时,按 use_unified_account 回退到原始 API + 注意:Bitget UTA(统一账户)模式下,ccxt fetch_balance() 会报错 + "Classic Account API is not supported",必须使用 privateUtaGetV3AccountAssets。 """ - # ---------- 策略 1: ccxt fetch_balance ---------- - try: - balance = self.exchange.fetch_balance({'type': 'swap'}) - if balance and isinstance(balance, dict): - usdt_info = balance.get('USDT') - if usdt_info and isinstance(usdt_info, dict): - available = usdt_info.get('free', 0) or 0 - used = usdt_info.get('used', 0) or 0 - total = usdt_info.get('total', 0) or 0 - result = { - 'USDT': { - 'available': str(available), - 'frozen': str(used), - 'locked': str(used), - 'equity': str(total if total else (available + used)), - } - } - logger.debug(f"[fetch_balance] USDT available={available}, used={used}, total={total}") - return result - else: - logger.warning(f"[fetch_balance] 返回中无 USDT 字段: {list(balance.keys())[:10]}") - except Exception as e: - logger.warning(f"[fetch_balance] 查询失败,回退到原始 API: {e}") - - # ---------- 策略 2: 原始 API(UTA / 合约账户) ---------- try: if self.use_unified_account: response = self.exchange.privateUtaGetV3AccountAssets({ 'coin': self.DEFAULT_MARGIN_COIN, }) - logger.debug(f"[UTA API] 原始响应: {response}") assets = response.get('data', {}).get('assets', []) or [] result = {} for entry in assets: @@ -748,14 +721,12 @@ class BitgetTradingAPI: 'locked': str(entry.get('locked', '0')), 'equity': str(entry.get('equity', entry.get('balance', '0'))), } - logger.debug(f"[UTA API] 账户余额: {result}") + logger.debug(f"[UTA] 账户余额: {result}") return result response = self.exchange.privateMixGetV2MixAccountAccounts({ 'productType': self.DEFAULT_PRODUCT_TYPE, }) - logger.debug(f"[合约 API] 原始响应: {response}") - result = {} for entry in response.get('data', []) or []: currency = entry.get('marginCoin') @@ -767,8 +738,7 @@ class BitgetTradingAPI: 'locked': str(entry.get('locked', '0')), 'equity': str(entry.get('accountEquity', entry.get('usdtEquity', '0'))), } - - logger.debug(f"[合约 API] 账户余额: {result}") + logger.debug(f"[合约] 账户余额: {result}") return result except ccxt.BaseError as e: diff --git a/backend/tests/test_bitget_balance.py b/backend/tests/test_bitget_balance.py index 85dacbb..4dab021 100644 --- a/backend/tests/test_bitget_balance.py +++ b/backend/tests/test_bitget_balance.py @@ -2,20 +2,18 @@ BitgetTradingAPI.get_balance() 单元测试 覆盖场景: - 1. fetch_balance 成功 → 直接返回(不调原始 API) - 2. fetch_balance 无 USDT → 回退到原始 API (UTA) - 3. fetch_balance 无 USDT → 回退到原始 API (合约) - 4. fetch_balance 抛异常 → 回退到原始 API (UTA) - 5. fetch_balance 和原始 API 都失败 → 返回 {} - 6. 余额为 0 / None 的边界情况 - 7. get_account_state 端到端解析 + 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, patch +from unittest.mock import MagicMock sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) @@ -32,13 +30,11 @@ def load_bitget_sdk_class(): ccxt_module.bitget = MagicMock() sys.modules['ccxt'] = ccxt_module - # mock app.config if 'app.config' not in sys.modules: mock_cfg = types.ModuleType('app.config') mock_cfg.get_settings = MagicMock() sys.modules['app.config'] = mock_cfg - # mock app.utils.logger if 'app.utils.logger' not in sys.modules: mock_log = types.ModuleType('app.utils.logger') mock_log.logger = MagicMock() @@ -53,81 +49,24 @@ def load_bitget_sdk_class(): # ────────────────────────────────────────────── -# 测试 1: fetch_balance 成功,直接返回 +# 测试 1: UTA 模式,正常返回(匹配真实 API) # ────────────────────────────────────────────── -def test_fetch_balance_success(): - """fetch_balance 返回 USDT,不走原始 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.fetch_balance.return_value = { - 'USDT': {'free': 1975.0, 'used': 50.0, 'total': 2025.0}, - 'free': {'USDT': 1975.0}, - 'used': {'USDT': 50.0}, - 'total': {'USDT': 2025.0}, - } - - balance = api.get_balance() - - assert balance == { - 'USDT': { - 'available': '1975.0', - 'frozen': '50.0', - 'locked': '50.0', - 'equity': '2025.0', - } - } - # 不应调原始 API - api.exchange.privateUtaGetV3AccountAssets.assert_not_called() - - -# ────────────────────────────────────────────── -# 测试 2: fetch_balance 成功,余额为 0 -# ────────────────────────────────────────────── -def test_fetch_balance_zero_balance(): - """fetch_balance 返回 0 余额,应正确解析""" - BitgetTradingAPI = load_bitget_sdk_class() - api = BitgetTradingAPI.__new__(BitgetTradingAPI) - api.exchange = MagicMock() - api.use_unified_account = True - - api.exchange.fetch_balance.return_value = { - 'USDT': {'free': 0, 'used': 0, 'total': 0}, - } - - balance = api.get_balance() - - assert balance['USDT']['available'] == '0' - assert balance['USDT']['equity'] == '0' - - -# ────────────────────────────────────────────── -# 测试 3: fetch_balance 无 USDT → 回退 UTA -# ────────────────────────────────────────────── -def test_fetch_balance_no_usdt_fallback_uta(): - """fetch_balance 返回无 USDT → 回退到 UTA 原始 API""" - BitgetTradingAPI = load_bitget_sdk_class() - api = BitgetTradingAPI.__new__(BitgetTradingAPI) - api.exchange = MagicMock() - api.use_unified_account = True - - # fetch_balance 没有USDT - api.exchange.fetch_balance.return_value = { - 'BTC': {'free': 0.5, 'used': 0, 'total': 0.5}, - } - - # UTA 回退返回正常数据 api.exchange.privateUtaGetV3AccountAssets.return_value = { 'code': '00000', 'data': { 'assets': [ { 'coin': 'USDT', - 'available': '1975.00', + 'available': '1975.37457492', 'locked': '0', - 'equity': '1975.00', + 'equity': '1975.37457492', } ], }, @@ -135,25 +74,49 @@ def test_fetch_balance_no_usdt_fallback_uta(): balance = api.get_balance() - assert balance['USDT']['available'] == '1975.00' - assert balance['USDT']['equity'] == '1975.00' - api.exchange.privateUtaGetV3AccountAssets.assert_called_once() + 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'}) # ────────────────────────────────────────────── -# 测试 4: fetch_balance 无 USDT → 回退合约 API +# 测试 2: UTA 多币种返回 # ────────────────────────────────────────────── -def test_fetch_balance_no_usdt_fallback_contract(): - """use_unified_account=False → 回退到合约 API""" +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 - # fetch_balance 没有USDT - api.exchange.fetch_balance.return_value = {} - - # 合约 API 回退 api.exchange.privateMixGetV2MixAccountAccounts.return_value = { 'code': '00000', 'data': [ @@ -174,49 +137,16 @@ def test_fetch_balance_no_usdt_fallback_contract(): # ────────────────────────────────────────────── -# 测试 5: fetch_balance 抛异常 → 回退 UTA +# 测试 4: API 异常 → 返回 {} # ────────────────────────────────────────────── -def test_fetch_balance_exception_fallback(): - """fetch_balance 抛异常 → 回退到原始 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.fetch_balance.side_effect = Exception("network error") - - api.exchange.privateUtaGetV3AccountAssets.return_value = { - 'code': '00000', - 'data': { - 'assets': [ - { - 'coin': 'USDT', - 'available': '500.00', - 'locked': '0', - 'equity': '500.00', - } - ], - }, - } - - balance = api.get_balance() - - assert balance['USDT']['available'] == '500.00' - api.exchange.privateUtaGetV3AccountAssets.assert_called_once() - - -# ────────────────────────────────────────────── -# 测试 6: 两个策略都失败 → 返回 {} -# ────────────────────────────────────────────── -def test_both_strategies_fail(): - """fetch_balance 和原始 API 都失败 → 返回空字典""" - BitgetTradingAPI = load_bitget_sdk_class() - api = BitgetTradingAPI.__new__(BitgetTradingAPI) - api.exchange = MagicMock() - api.use_unified_account = True - - api.exchange.fetch_balance.side_effect = Exception("timeout") - api.exchange.privateUtaGetV3AccountAssets.side_effect = Exception("auth fail") + api.exchange.privateUtaGetV3AccountAssets.side_effect = Exception("network error") balance = api.get_balance() @@ -224,47 +154,7 @@ def test_both_strategies_fail(): # ────────────────────────────────────────────── -# 测试 7: fetch_balance 返回 free=None -# ────────────────────────────────────────────── -def test_fetch_balance_none_values(): - """fetch_balance 中 free/used 为 None → 解析为 0""" - BitgetTradingAPI = load_bitget_sdk_class() - api = BitgetTradingAPI.__new__(BitgetTradingAPI) - api.exchange = MagicMock() - api.use_unified_account = True - - api.exchange.fetch_balance.return_value = { - 'USDT': {'free': None, 'used': None, 'total': None}, - } - - balance = api.get_balance() - - assert balance['USDT']['available'] == '0' - assert balance['USDT']['frozen'] == '0' - assert balance['USDT']['equity'] == '0' - - -# ────────────────────────────────────────────── -# 测试 8: fetch_balance 返回 free=0, total=0 → equity 用 free+used -# ────────────────────────────────────────────── -def test_fetch_balance_zero_total(): - """total=0 时 equity = free + used""" - BitgetTradingAPI = load_bitget_sdk_class() - api = BitgetTradingAPI.__new__(BitgetTradingAPI) - api.exchange = MagicMock() - api.use_unified_account = True - - api.exchange.fetch_balance.return_value = { - 'USDT': {'free': 100, 'used': 50, 'total': 0}, - } - - balance = api.get_balance() - - assert balance['USDT']['equity'] == '150' - - -# ────────────────────────────────────────────── -# 测试 9: UTA 返回空 assets → 回退到 {} +# 测试 5: UTA 空 assets → 返回 {} # ────────────────────────────────────────────── def test_uta_empty_assets(): """UTA API 返回空 assets 列表""" @@ -273,7 +163,6 @@ def test_uta_empty_assets(): api.exchange = MagicMock() api.use_unified_account = True - api.exchange.fetch_balance.return_value = {} api.exchange.privateUtaGetV3AccountAssets.return_value = { 'code': '00000', 'data': {'assets': []}, @@ -285,17 +174,61 @@ def test_uta_empty_assets(): # ────────────────────────────────────────────── -# 测试 10: get_account_state 端到端解析 +# 测试 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'] == 'None' # str(None) = 'None', float('None') → downstream handles + + +# ────────────────────────────────────────────── +# 测试 8: get_account_state 端到端解析(正常余额) # ────────────────────────────────────────────── def test_get_account_state_e2e(): """get_account_state 正确解析 balance 返回值""" - BitgetTradingAPI = load_bitget_sdk_class() - - # Mock get_bitget_live_service 的依赖链 - # 直接测 get_account_state 解析逻辑 - from app.services.bitget_live_trading_service import CONTRACT_SIZES - - # 模拟 balance 返回 balance_return = { 'USDT': { 'available': '1975.50', @@ -305,7 +238,6 @@ def test_get_account_state_e2e(): } } - # 直接验证解析逻辑(不初始化完整 service) usdt = balance_return.get('USDT', {}) available = float(usdt.get('available', 0) or 0) frozen = float(usdt.get('frozen', 0) or 0) @@ -319,7 +251,7 @@ def test_get_account_state_e2e(): # ────────────────────────────────────────────── -# 测试 11: balance 返回 {} 时 get_account_state 各字段为 0 +# 测试 9: balance 返回 {} 时 get_account_state 各字段为 0 # ────────────────────────────────────────────── def test_get_account_state_empty_balance(): """balance 返回空 → 全部为 0""" @@ -337,20 +269,20 @@ def test_get_account_state_empty_balance(): # ────────────────────────────────────────────── -# 测试 12: fetch_balance 返回字符串数值 +# 测试 10: UTA 返回 data=None → 返回 {} # ────────────────────────────────────────────── -def test_fetch_balance_string_values(): - """ccxt 有时返回字符串类型的余额""" +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.fetch_balance.return_value = { - 'USDT': {'free': '1975.50', 'used': '24.50', 'total': '2000.00'}, + api.exchange.privateUtaGetV3AccountAssets.return_value = { + 'code': '00000', + 'data': None, } balance = api.get_balance() - assert balance['USDT']['available'] == '1975.50' - assert balance['USDT']['equity'] == '2000.00' + assert balance == {}