update
This commit is contained in:
parent
5288bbd4a3
commit
fd409f6825
@ -2168,6 +2168,7 @@ class CryptoAgent:
|
||||
async def _execute_bitget_close(self, decision: Dict[str, Any],
|
||||
current_price: float) -> Dict[str, Any]:
|
||||
"""执行 Bitget 市价平仓"""
|
||||
import math
|
||||
try:
|
||||
symbol = decision.get('symbol', '').replace('USDT', '')
|
||||
|
||||
@ -2181,20 +2182,37 @@ class CryptoAgent:
|
||||
if not position:
|
||||
return {"success": False, "error": "未找到持仓"}
|
||||
|
||||
size_in_coins = abs(position["size"])
|
||||
size_in_coins = abs(position["size"]) # 已经是 BTC 数量
|
||||
is_long = position["size"] > 0
|
||||
contracts = self.bitget.coins_to_contracts(symbol, size_in_coins)
|
||||
|
||||
if contracts < 1:
|
||||
return {"success": False, "error": f"持仓过小,无法下单({size_in_coins} 币 = {contracts} 张)"}
|
||||
# 精度处理:向下取整到 0.0001(Bitget 最小精度)
|
||||
size_in_coins = math.floor(size_in_coins * 10000) / 10000
|
||||
|
||||
result = self.bitget.place_market_order(
|
||||
symbol=symbol,
|
||||
is_buy=not is_long,
|
||||
size=contracts,
|
||||
reduce_only=True
|
||||
)
|
||||
return result
|
||||
if size_in_coins < 0.0001:
|
||||
return {"success": False, "error": f"持仓过小({size_in_coins} 币 < 最小 0.0001)"}
|
||||
|
||||
# 直接使用 BTC 数量平仓,不经过合约转换
|
||||
try:
|
||||
ccxt_symbol = self.bitget.trading_api._standardize_symbol(symbol + 'USDT')
|
||||
side = 'sell' if is_long else 'buy'
|
||||
order = self.bitget.trading_api.exchange.create_market_order(
|
||||
symbol=ccxt_symbol,
|
||||
side=side,
|
||||
amount=size_in_coins,
|
||||
params={
|
||||
'reduceOnly': True,
|
||||
'tdMode': 'cross',
|
||||
'marginCoin': 'USDT',
|
||||
}
|
||||
)
|
||||
if order:
|
||||
logger.info(f"✅ Bitget 平仓成功: {symbol} {side} {size_in_coins} BTC")
|
||||
return {"success": True, "order_id": str(order.get('id', '')), "symbol": symbol, "size": size_in_coins}
|
||||
else:
|
||||
return {"success": False, "error": "下单返回空"}
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Bitget 平仓下单失败: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Bitget 平仓失败: {e}")
|
||||
|
||||
@ -155,15 +155,18 @@ class BitgetLiveTradingService:
|
||||
raw_positions = self.trading_api.get_position()
|
||||
result = []
|
||||
for pos in raw_positions:
|
||||
contracts = float(pos.get('contracts', 0))
|
||||
if contracts == 0:
|
||||
# 使用 info.available 获取实际持仓量(币数量)
|
||||
# 注意:CCXT 的 'contracts' 字段对于 Bitget 实际上已经是币数量,不是张数
|
||||
info = pos.get('info', {})
|
||||
available = float(info.get('available', 0))
|
||||
if available == 0:
|
||||
continue
|
||||
|
||||
symbol_raw = pos.get('symbol', '') # e.g. "BTC/USDT:USDT"
|
||||
coin = symbol_raw.split('/')[0] if '/' in symbol_raw else symbol_raw
|
||||
coin = symbol_raw.split('/')[0] if '/' in symbol_raw else symbol_raw.replace('/USDT:USDT', '')
|
||||
|
||||
contract_size = self.get_contract_size(coin)
|
||||
coin_amount = contracts * contract_size
|
||||
# available 已经是币数量,不需要再乘以 contract_size
|
||||
coin_amount = available
|
||||
|
||||
side = pos.get('side', 'long')
|
||||
size = coin_amount if side == 'long' else -coin_amount
|
||||
@ -490,16 +493,44 @@ class BitgetLiveTradingService:
|
||||
|
||||
def market_close_all(self) -> Dict[str, Any]:
|
||||
"""市价平仓所有持仓"""
|
||||
import math
|
||||
results = []
|
||||
positions = self.get_open_positions()
|
||||
for pos in positions:
|
||||
coin = pos['coin']
|
||||
is_long = pos['size'] > 0
|
||||
contracts = self.coins_to_contracts(coin, abs(pos['size']))
|
||||
if contracts < 1:
|
||||
coin_amount = abs(pos['size'])
|
||||
|
||||
# 精度处理:向下取整到 0.0001(Bitget 最小精度)
|
||||
coin_amount = math.floor(coin_amount * 10000) / 10000
|
||||
|
||||
if coin_amount < 0.0001:
|
||||
logger.warning(f"{coin} 持仓过小 ({coin_amount}),跳过")
|
||||
continue
|
||||
result = self.place_market_order(coin, is_buy=not is_long, size=contracts, reduce_only=True)
|
||||
results.append(result)
|
||||
|
||||
# 直接使用币数量平仓,不经过合约转换
|
||||
try:
|
||||
ccxt_symbol = self.trading_api._standardize_symbol(coin + 'USDT')
|
||||
side = 'sell' if is_long else 'buy'
|
||||
order = self.trading_api.exchange.create_market_order(
|
||||
symbol=ccxt_symbol,
|
||||
side=side,
|
||||
amount=coin_amount,
|
||||
params={
|
||||
'reduceOnly': True,
|
||||
'tdMode': 'cross',
|
||||
'marginCoin': 'USDT',
|
||||
}
|
||||
)
|
||||
if order:
|
||||
logger.info(f"✅ Bitget 平仓成功: {coin} {side} {coin_amount}")
|
||||
results.append({"success": True, "coin": coin, "size": coin_amount})
|
||||
else:
|
||||
results.append({"success": False, "coin": coin, "error": "返回空"})
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Bitget 平仓失败: {coin} {e}")
|
||||
results.append({"success": False, "coin": coin, "error": str(e)})
|
||||
|
||||
all_ok = all(r.get('success') for r in results)
|
||||
return {"success": all_ok, "results": results}
|
||||
|
||||
|
||||
@ -253,7 +253,7 @@ class BitgetTradingAPI:
|
||||
- None: 自动判断(查询持仓后决定)
|
||||
- 'buy': 平空仓
|
||||
- 'sell': 平多仓
|
||||
size: 平仓数量(不传则全部平仓)
|
||||
size: 平仓数量(不传则全部平仓)- 传张数
|
||||
price: 平仓价格(不传则市价)
|
||||
|
||||
Returns:
|
||||
@ -271,8 +271,9 @@ class BitgetTradingAPI:
|
||||
# 查找有持仓的仓位
|
||||
position = None
|
||||
for pos in positions:
|
||||
contracts = float(pos.get('contracts', 0))
|
||||
if contracts != 0:
|
||||
# 使用 info.available 字段获取实际持仓量(BTC单位)
|
||||
available = float(pos.get('info', {}).get('available', 0))
|
||||
if available != 0:
|
||||
position = pos
|
||||
break
|
||||
|
||||
@ -280,7 +281,8 @@ class BitgetTradingAPI:
|
||||
logger.warning(f"{symbol} 持仓数量为 0,无需平仓")
|
||||
return None
|
||||
|
||||
current_size = abs(float(position.get('contracts', 0)))
|
||||
# 获取实际持仓量(BTC单位)
|
||||
current_size_btc = abs(float(position.get('info', {}).get('available', 0)))
|
||||
pos_side = position.get('side') # 'long' or 'short'
|
||||
|
||||
# 如果没有指定平仓方向,根据持仓方向自动判断
|
||||
@ -289,22 +291,45 @@ class BitgetTradingAPI:
|
||||
side = 'sell' if pos_side == 'long' else 'buy'
|
||||
|
||||
# 如果没有指定平仓数量,则全部平仓
|
||||
close_size = size if size else current_size
|
||||
# 注意:直接使用 BTC 数量,不再通过 place_order 转换
|
||||
close_size_btc = size * self._get_contract_size(symbol) if size else current_size_btc
|
||||
|
||||
logger.info(f"平仓: {symbol} 持仓方向={pos_side}, 平仓方向={side}, 数量={close_size}")
|
||||
# 精度处理:向下取整到 0.0001 BTC(Bitget 最小精度)
|
||||
import math
|
||||
close_size_btc = math.floor(close_size_btc * 10000) / 10000
|
||||
|
||||
# 执行平仓
|
||||
order_type = 'limit' if price else 'market'
|
||||
order = self.place_order(
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
order_type=order_type,
|
||||
size=close_size,
|
||||
price=price
|
||||
)
|
||||
if close_size_btc < 0.0001:
|
||||
logger.warning(f"{symbol} 平仓数量 {close_size_btc} BTC 小于最小交易单位 0.0001 BTC")
|
||||
return None
|
||||
|
||||
logger.info(f"平仓: {symbol} 持仓方向={pos_side}, 平仓方向={side}, 数量={close_size_btc} BTC")
|
||||
|
||||
# 直接使用 CCXT 下市价平仓单(绕过 place_order 的张数转换)
|
||||
order_type = 'market' if not price else 'limit'
|
||||
params = {
|
||||
'reduceOnly': True,
|
||||
'tdMode': 'cross',
|
||||
'marginCoin': 'USDT',
|
||||
}
|
||||
|
||||
if price:
|
||||
order = self.exchange.create_limit_order(
|
||||
symbol=ccxt_symbol,
|
||||
side=side,
|
||||
amount=close_size_btc,
|
||||
price=price,
|
||||
params=params
|
||||
)
|
||||
else:
|
||||
order = self.exchange.create_market_order(
|
||||
symbol=ccxt_symbol,
|
||||
side=side,
|
||||
amount=close_size_btc,
|
||||
params=params
|
||||
)
|
||||
|
||||
if order:
|
||||
logger.info(f"✅ 平仓成功: {symbol} {side} {close_size}张")
|
||||
logger.info(f"✅ 平仓成功: {symbol} {side} {close_size_btc} BTC")
|
||||
return order
|
||||
|
||||
return None
|
||||
@ -404,7 +429,7 @@ class BitgetTradingAPI:
|
||||
pos_side = position.get('side')
|
||||
mark_price = float(position.get('markPrice', 0))
|
||||
|
||||
logger.info(f"当前持仓: {symbol} {pos_side} {contracts}张, 标记价={mark_price}")
|
||||
logger.info(f"当前持仓: {symbol} {pos_side} contracts={contracts}, 标记价={mark_price}")
|
||||
|
||||
# 验证价格
|
||||
if pos_side == 'long':
|
||||
@ -425,26 +450,13 @@ class BitgetTradingAPI:
|
||||
# 使用独立的止损/止盈计划订单
|
||||
# 注意:这种方式需要在平仓时也取消这些计划订单
|
||||
|
||||
# 获取合约规格(用于转换张数为币数量)
|
||||
if 'BTC' in symbol:
|
||||
contract_size = 0.01
|
||||
elif 'ETH' in symbol:
|
||||
contract_size = 0.1
|
||||
elif 'SOL' in symbol:
|
||||
contract_size = 1
|
||||
elif 'BNB' in symbol:
|
||||
contract_size = 0.1
|
||||
elif 'XRP' in symbol:
|
||||
contract_size = 10
|
||||
elif 'DOGE' in symbol:
|
||||
contract_size = 100
|
||||
elif 'MATIC' in symbol or 'POL' in symbol:
|
||||
contract_size = 10
|
||||
else:
|
||||
contract_size = 1
|
||||
# CCXT 的 contracts 字段对于 Bitget 实际上已经是 BTC 数量
|
||||
# 所以我们直接使用,不需要再乘以 contract_size
|
||||
btc_amount = abs(contracts)
|
||||
|
||||
# 将张数转换为币数量
|
||||
contracts_amount = contracts * contract_size
|
||||
# 精度处理
|
||||
import math
|
||||
btc_amount = math.floor(btc_amount * 10000) / 10000
|
||||
|
||||
orders_created = []
|
||||
|
||||
@ -457,7 +469,7 @@ class BitgetTradingAPI:
|
||||
symbol=ccxt_symbol,
|
||||
type='stop_market',
|
||||
side=sl_side,
|
||||
amount=contracts_amount, # 使用币数量,不是张数
|
||||
amount=btc_amount,
|
||||
price=None,
|
||||
params={
|
||||
'stopPrice': stop_loss,
|
||||
@ -468,7 +480,7 @@ class BitgetTradingAPI:
|
||||
}
|
||||
)
|
||||
orders_created.append(('止损', sl_order))
|
||||
logger.info(f"✅ 止损单已下: {sl_side} {contracts}张 ({contracts_amount}币) @ ${stop_loss}")
|
||||
logger.info(f"✅ 止损单已下: {sl_side} {btc_amount} BTC @ ${stop_loss}")
|
||||
except Exception as e:
|
||||
logger.warning(f"下止损单失败: {e}")
|
||||
|
||||
@ -481,7 +493,7 @@ class BitgetTradingAPI:
|
||||
symbol=ccxt_symbol,
|
||||
type='limit',
|
||||
side=tp_side,
|
||||
amount=contracts_amount, # 使用币数量,不是张数
|
||||
amount=btc_amount,
|
||||
price=take_profit,
|
||||
params={
|
||||
'tdMode': 'cross',
|
||||
@ -490,7 +502,7 @@ class BitgetTradingAPI:
|
||||
}
|
||||
)
|
||||
orders_created.append(('止盈', tp_order))
|
||||
logger.info(f"✅ 止盈单已下: {tp_side} {contracts}张 ({contracts_amount}币) @ ${take_profit}")
|
||||
logger.info(f"✅ 止盈单已下: {tp_side} {btc_amount} BTC @ ${take_profit}")
|
||||
except Exception as e:
|
||||
logger.warning(f"下止盈单失败: {e}")
|
||||
|
||||
@ -779,6 +791,50 @@ class BitgetTradingAPI:
|
||||
# 默认返回原值
|
||||
return symbol
|
||||
|
||||
def _get_contract_size(self, symbol: str) -> float:
|
||||
"""
|
||||
获取合约面值(每张合约对应的币数量)
|
||||
|
||||
Args:
|
||||
symbol: 交易对
|
||||
|
||||
Returns:
|
||||
合约面值
|
||||
"""
|
||||
# Bitget 永续合约规格
|
||||
if 'BTC' in symbol:
|
||||
return 0.01
|
||||
elif 'ETH' in symbol:
|
||||
return 0.1
|
||||
elif 'SOL' in symbol:
|
||||
return 1.0
|
||||
elif 'BNB' in symbol:
|
||||
return 0.1
|
||||
elif 'XRP' in symbol:
|
||||
return 10.0
|
||||
elif 'DOGE' in symbol:
|
||||
return 100.0
|
||||
elif 'MATIC' in symbol or 'POL' in symbol:
|
||||
return 10.0
|
||||
elif 'AVAX' in symbol:
|
||||
return 1.0
|
||||
elif 'LINK' in symbol:
|
||||
return 1.0
|
||||
elif 'UNI' in symbol:
|
||||
return 1.0
|
||||
elif 'ATOM' in symbol:
|
||||
return 1.0
|
||||
elif 'LTC' in symbol:
|
||||
return 0.1
|
||||
elif 'BCH' in symbol:
|
||||
return 0.1
|
||||
elif 'FIL' in symbol:
|
||||
return 1.0
|
||||
elif 'DOT' in symbol:
|
||||
return 1.0
|
||||
else:
|
||||
return 1.0
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""
|
||||
测试 API 连接
|
||||
|
||||
625
backend/tests/test_bitget_live_integration.py
Normal file
625
backend/tests/test_bitget_live_integration.py
Normal file
@ -0,0 +1,625 @@
|
||||
"""
|
||||
Bitget 真实 API 集成测试
|
||||
|
||||
⚠️ 警告:此测试会使用真实 API 调用和真实订单!
|
||||
- 确保使用测试网或接受小额手续费
|
||||
- 市价单会立即成交,产生实际盈亏
|
||||
- 测试后检查是否有残留订单/持仓
|
||||
|
||||
运行方式:
|
||||
cd backend
|
||||
source venv/bin/activate
|
||||
python3 tests/test_bitget_live_integration.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv('/Users/aaron/source_code/Stock_Agent/.env')
|
||||
|
||||
from app.services.bitget_trading_api_sdk import BitgetTradingAPI
|
||||
from app.services.bitget_live_trading_service import BitgetLiveTradingService
|
||||
|
||||
|
||||
class BitgetIntegrationTest:
|
||||
"""Bitget 真实 API 集成测试"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.getenv('BITGET_API_KEY')
|
||||
self.api_secret = os.getenv('BITGET_API_SECRET')
|
||||
self.passphrase = os.getenv('BITGET_PASSPHRASE')
|
||||
self.use_testnet = os.getenv('BITGET_USE_TESTNET', 'true').lower() == 'true'
|
||||
|
||||
if not all([self.api_key, self.api_secret, self.passphrase]):
|
||||
raise ValueError("❌ 请在 .env 中配置 BITGET_API_KEY, BITGET_API_SECRET, BITGET_PASSPHRASE")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Bitget 真实 API 集成测试")
|
||||
print(f"{'='*60}")
|
||||
print(f"API Key: {self.api_key[:10]}...")
|
||||
print(f"测试网模式: {self.use_testnet}")
|
||||
print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# 初始化 API
|
||||
self.api = BitgetTradingAPI(
|
||||
api_key=self.api_key,
|
||||
api_secret=self.api_secret,
|
||||
passphrase=self.passphrase,
|
||||
use_testnet=self.use_testnet
|
||||
)
|
||||
|
||||
# 初始化 Service
|
||||
self.service = BitgetLiveTradingService.__new__(BitgetLiveTradingService)
|
||||
self.service.trading_api = self.api
|
||||
self.service.max_single_position = 100
|
||||
self.service.max_total_leverage = 5
|
||||
self.service.circuit_breaker_drawdown = 0.10
|
||||
self.service._initial_account_state = None
|
||||
self.service.initial_balance = None # 添加这个属性
|
||||
|
||||
# 测试状态
|
||||
self.limit_order_id = None
|
||||
self.limit_order_symbol = None
|
||||
self.market_entry_price = 0.0
|
||||
|
||||
# ==================== 基础连接测试 ====================
|
||||
|
||||
def test_01_connection(self):
|
||||
"""测试 1: API 连接"""
|
||||
print("\n📡 测试 1: API 连接")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
# 获取服务器时间
|
||||
server_time = self.api.exchange.fetch_time()
|
||||
print(f"✅ 服务器时间: {datetime.fromtimestamp(server_time/1000)}")
|
||||
|
||||
# 加载市场信息
|
||||
markets = self.api.exchange.load_markets()
|
||||
btc_market = markets.get('BTC/USDT:USDT', {})
|
||||
print(f"✅ BTC 市场ID: {btc_market.get('id', 'N/A')}")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 连接失败: {e}")
|
||||
return False
|
||||
|
||||
def test_02_get_balance(self):
|
||||
"""测试 2: 获取账户余额"""
|
||||
print("\n💰 测试 2: 获取账户余额")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
balance = self.api.get_balance()
|
||||
if balance:
|
||||
usdt = balance.get('USDT', {})
|
||||
print(f"✅ USDT 可用: {usdt.get('free', 0):.2f}")
|
||||
print(f"✅ USDT 冻结: {usdt.get('used', 0):.2f}")
|
||||
print(f"✅ USDT 总额: {usdt.get('total', 0):.2f}")
|
||||
return True
|
||||
else:
|
||||
print("❌ 余额为空")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 获取余额失败: {e}")
|
||||
return False
|
||||
|
||||
def test_03_get_positions(self):
|
||||
"""测试 3: 获取当前持仓"""
|
||||
print("\n📊 测试 3: 获取当前持仓")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
positions = self.api.get_position()
|
||||
if positions:
|
||||
active_positions = [p for p in positions if float(p.get('contracts', 0)) > 0]
|
||||
print(f"✅ 当前持仓数: {len(active_positions)}")
|
||||
for pos in active_positions:
|
||||
symbol = pos.get('symbol', 'N/A')
|
||||
contracts = float(pos.get('contracts', 0))
|
||||
side = pos.get('side', 'N/A')
|
||||
entry = float(pos.get('entryPrice', 0))
|
||||
print(f" - {symbol}: {side} {contracts} 张 @ ${entry:,.2f}")
|
||||
else:
|
||||
print("✅ 当前无持仓")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 获取持仓失败: {e}")
|
||||
return False
|
||||
|
||||
def test_04_set_leverage(self):
|
||||
"""测试 4: 设置杠杆"""
|
||||
print("\n⚙️ 测试 4: 设置杠杆")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
symbol = 'BTCUSDT'
|
||||
leverage = 5
|
||||
self.api.set_leverage(symbol, leverage)
|
||||
print(f"✅ {symbol} 杠杆已设置为 {leverage}x")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 设置杠杆失败: {e}")
|
||||
return False
|
||||
|
||||
# ==================== 限价单测试 ====================
|
||||
|
||||
def test_05_place_limit_order(self):
|
||||
"""测试 5: 下限价单(挂单,预期不成交)"""
|
||||
print("\n📝 测试 5: 下限价单")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
symbol = 'BTCUSDT'
|
||||
|
||||
# 获取当前价格
|
||||
ticker = self.api.exchange.fetch_ticker('BTC/USDT:USDT')
|
||||
current_price = ticker['last']
|
||||
print(f" 当前 BTC 价格: ${current_price:,.2f}")
|
||||
|
||||
# 计算挂单价格(当前价格 - 5%,预期不会成交)
|
||||
limit_price = current_price * 0.95
|
||||
print(f" 挂单价格: ${limit_price:,.2f} (低 5%,预期不成交)")
|
||||
|
||||
# 下 1 张多单(最小单位)
|
||||
order = self.api.place_order(
|
||||
symbol=symbol,
|
||||
side='buy',
|
||||
order_type='limit',
|
||||
size=1, # 1 张 = 0.01 BTC
|
||||
price=limit_price
|
||||
)
|
||||
|
||||
if order:
|
||||
self.limit_order_id = order.get('id')
|
||||
self.limit_order_symbol = symbol
|
||||
print(f"✅ 限价单已下: {self.limit_order_id}")
|
||||
print(f" 订单状态: {order.get('status')}")
|
||||
return True
|
||||
else:
|
||||
print("❌ 下单返回空")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 下限价单失败: {e}")
|
||||
return False
|
||||
|
||||
def test_06_get_open_orders(self):
|
||||
"""测试 6: 查询挂单"""
|
||||
print("\n📋 测试 6: 查询挂单")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
symbol = self.limit_order_symbol or 'BTCUSDT'
|
||||
orders = self.api.get_open_orders(symbol)
|
||||
|
||||
if orders:
|
||||
print(f"✅ 当前挂单数: {len(orders)}")
|
||||
for order in orders:
|
||||
order_id = order.get('id', 'N/A')
|
||||
side = order.get('side', 'N/A')
|
||||
price = order.get('price', 0)
|
||||
amount = order.get('amount', 0)
|
||||
print(f" - {order_id}: {side} {amount} @ ${price:,.2f}")
|
||||
else:
|
||||
print("⚠️ 无挂单")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 查询挂单失败: {e}")
|
||||
return False
|
||||
|
||||
def test_07_cancel_order(self):
|
||||
"""测试 7: 撤销挂单"""
|
||||
print("\n❌ 测试 7: 撤销挂单")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
if not self.limit_order_id:
|
||||
print("⚠️ 没有需要撤销的订单")
|
||||
return True
|
||||
|
||||
# 使用 cancel_all_orders 撤销该 symbol 所有挂单
|
||||
success = self.api.cancel_all_orders(self.limit_order_symbol)
|
||||
|
||||
if success:
|
||||
print(f"✅ 已撤销 {self.limit_order_symbol} 的所有挂单")
|
||||
self.limit_order_id = None
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 撤单失败")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 撤销订单失败: {e}")
|
||||
return False
|
||||
|
||||
# ==================== 市价单测试(真实成交!)====================
|
||||
|
||||
def test_08_market_order_open(self):
|
||||
"""测试 8: 市价开多单(真实成交!)"""
|
||||
print("\n⚡ 测试 8: 市价开多单")
|
||||
print("-" * 40)
|
||||
print("⚠️ 警告:此测试会产生真实订单和手续费!")
|
||||
|
||||
try:
|
||||
symbol = 'BTCUSDT'
|
||||
|
||||
# 获取当前价格
|
||||
ticker = self.api.exchange.fetch_ticker('BTC/USDT:USDT')
|
||||
current_price = ticker['last']
|
||||
print(f" 当前 BTC 价格: ${current_price:,.2f}")
|
||||
|
||||
# 下 1 张市价多单(最小单位 = 0.01 BTC)
|
||||
print(f" 下市价多单: 1 张 (0.01 BTC)...")
|
||||
order = self.api.place_order(
|
||||
symbol=symbol,
|
||||
side='buy',
|
||||
order_type='market',
|
||||
size=1 # 1 张 = 0.01 BTC
|
||||
)
|
||||
|
||||
if order:
|
||||
order_id = order.get('id')
|
||||
status = order.get('status')
|
||||
# 安全获取 average 价格
|
||||
avg_price = order.get('average') or order.get('price') or 0
|
||||
if avg_price:
|
||||
avg_price = float(avg_price)
|
||||
print(f"✅ 市价单已成交: {order_id}")
|
||||
print(f" 订单状态: {status}")
|
||||
print(f" 成交均价: ${avg_price:,.2f}")
|
||||
print(f" 成交数量: {order.get('filled', 0)} 张")
|
||||
|
||||
# 保存入场价
|
||||
self.market_entry_price = avg_price
|
||||
return True
|
||||
else:
|
||||
print("❌ 下单返回空")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 市价开仓失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_09_verify_position_opened(self):
|
||||
"""测试 9: 验证持仓已开启"""
|
||||
print("\n🔍 测试 9: 验证持仓")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
positions = self.api.get_position('BTCUSDT')
|
||||
|
||||
if positions:
|
||||
for pos in positions:
|
||||
contracts = float(pos.get('contracts', 0))
|
||||
if contracts > 0:
|
||||
side = pos.get('side', 'N/A')
|
||||
entry_price = float(pos.get('entryPrice', 0))
|
||||
unrealized_pnl = float(pos.get('unrealizedPnl', 0))
|
||||
print(f"✅ 持仓已确认:")
|
||||
print(f" 方向: {side}")
|
||||
print(f" 数量: {contracts} 张")
|
||||
print(f" 开仓价: ${entry_price:,.2f}")
|
||||
print(f" 未实现盈亏: ${unrealized_pnl:,.2f}")
|
||||
return True
|
||||
|
||||
print("❌ 未找到持仓")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 验证持仓失败: {e}")
|
||||
return False
|
||||
|
||||
def test_10_set_tp_sl(self):
|
||||
"""测试 10: 设置止盈止损"""
|
||||
print("\n🎯 测试 10: 设置止盈止损")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
# 获取当前价格
|
||||
ticker = self.api.exchange.fetch_ticker('BTC/USDT:USDT')
|
||||
current_price = ticker['last']
|
||||
|
||||
# 设置止盈(+1%)和止损(-0.5%)
|
||||
tp_price = current_price * 1.01 # +1%
|
||||
sl_price = current_price * 0.995 # -0.5%
|
||||
|
||||
print(f" 当前价格: ${current_price:,.2f}")
|
||||
print(f" 设置止盈: ${tp_price:,.2f} (+1%)")
|
||||
print(f" 设置止损: ${sl_price:,.2f} (-0.5%)")
|
||||
|
||||
result = self.api.modify_sl_tp(
|
||||
symbol='BTCUSDT',
|
||||
stop_loss=sl_price,
|
||||
take_profit=tp_price
|
||||
)
|
||||
|
||||
if result:
|
||||
print(f"✅ 止盈止损已设置")
|
||||
else:
|
||||
print(f"⚠️ 止盈止损设置返回 False")
|
||||
|
||||
return True # 无论成功与否都继续测试
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 设置止盈止损失败: {e}")
|
||||
return True # 不算失败,继续测试
|
||||
|
||||
def test_11_market_order_close(self):
|
||||
"""测试 11: 市价平仓"""
|
||||
print("\n🔒 测试 11: 市价平仓")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
# 获取当前价格
|
||||
ticker = self.api.exchange.fetch_ticker('BTC/USDT:USDT')
|
||||
current_price = ticker['last']
|
||||
print(f" 当前 BTC 价格: ${current_price:,.2f}")
|
||||
|
||||
# 先撤销止盈止损单
|
||||
try:
|
||||
self.api.cancel_all_orders('BTCUSDT')
|
||||
print(" 已撤销止盈止损单")
|
||||
except:
|
||||
pass
|
||||
|
||||
# 使用 close_position 方法平仓
|
||||
print(f" 下市价平仓单...")
|
||||
order = self.api.close_position(
|
||||
symbol='BTCUSDT',
|
||||
side='sell', # 平多
|
||||
size=0.01, # 0.01 张
|
||||
)
|
||||
|
||||
if order:
|
||||
order_id = order.get('id')
|
||||
avg_price = order.get('average') or order.get('price') or 0
|
||||
if avg_price:
|
||||
avg_price = float(avg_price)
|
||||
print(f"✅ 平仓成功: {order_id}")
|
||||
print(f" 平仓均价: ${avg_price:,.2f}")
|
||||
|
||||
# 计算盈亏
|
||||
if self.market_entry_price > 0 and avg_price > 0:
|
||||
pnl = (avg_price - self.market_entry_price) * 0.01 # 1张 = 0.01 BTC
|
||||
pnl_pct = (avg_price - self.market_entry_price) / self.market_entry_price * 100
|
||||
print(f" 实现盈亏: ${pnl:,.2f} ({pnl_pct:+.2f}%)")
|
||||
|
||||
return True
|
||||
else:
|
||||
print("❌ 平仓返回空")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 市价平仓失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_12_verify_position_closed(self):
|
||||
"""测试 12: 验证持仓已平"""
|
||||
print("\n✅ 测试 12: 验证持仓已平")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
positions = self.api.get_position('BTCUSDT')
|
||||
|
||||
has_position = False
|
||||
if positions:
|
||||
for pos in positions:
|
||||
if float(pos.get('contracts', 0)) > 0:
|
||||
has_position = True
|
||||
print(f"⚠️ 仍有持仓: {pos.get('contracts')} 张")
|
||||
|
||||
if not has_position:
|
||||
print("✅ 持仓已全部平仓")
|
||||
return True
|
||||
else:
|
||||
print("❌ 持仓未完全平仓")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 验证平仓失败: {e}")
|
||||
return False
|
||||
|
||||
# ==================== Service 层测试 ====================
|
||||
|
||||
def test_13_service_get_account_state(self):
|
||||
"""测试 13: Service 层获取账户状态"""
|
||||
print("\n🏦 测试 13: Service 获取账户状态")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
state = self.service.get_account_state()
|
||||
print(f"✅ 账户价值: ${state['account_value']:,.2f}")
|
||||
print(f"✅ 已用保证金: ${state['total_margin_used']:,.2f}")
|
||||
print(f"✅ 可用余额: ${state['available_balance']:,.2f}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 获取账户状态失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_14_service_get_positions(self):
|
||||
"""测试 14: Service 层获取持仓"""
|
||||
print("\n📈 测试 14: Service 获取持仓")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
positions = self.service.get_open_positions()
|
||||
if positions:
|
||||
print(f"✅ 当前持仓数: {len(positions)}")
|
||||
for pos in positions:
|
||||
# 安全获取字段
|
||||
symbol = pos.get('symbol', 'N/A')
|
||||
side = pos.get('side', 'N/A')
|
||||
size = pos.get('size', 0)
|
||||
entry = pos.get('entry_price', 0)
|
||||
print(f" - {symbol}: {side} {size} @ ${entry:,.2f}")
|
||||
else:
|
||||
print("✅ 当前无持仓")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 获取持仓失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_15_service_contract_size(self):
|
||||
"""测试 15: Service 合约面值"""
|
||||
print("\n📐 测试 15: 合约面值换算")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
# 测试已知币种
|
||||
btc_size = self.service.get_contract_size('BTC')
|
||||
eth_size = self.service.get_contract_size('ETH')
|
||||
sol_size = self.service.get_contract_size('SOL')
|
||||
|
||||
print(f"✅ BTC 合约面值: {btc_size} BTC/张")
|
||||
print(f"✅ ETH 合约面值: {eth_size} ETH/张")
|
||||
print(f"✅ SOL 合约面值: {sol_size} SOL/张")
|
||||
|
||||
# 测试币数转合约数
|
||||
contracts = self.service.coins_to_contracts('BTC', 0.05) # 0.05 BTC
|
||||
print(f"✅ 0.05 BTC = {contracts} 张")
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 合约面值测试失败: {e}")
|
||||
return False
|
||||
|
||||
def test_16_risk_check(self):
|
||||
"""测试 16: 风控检查"""
|
||||
print("\n⚠️ 测试 16: 风控检查")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
result = self.service.check_risk_limits()
|
||||
if result['allowed']:
|
||||
print(f"✅ 风控检查通过")
|
||||
else:
|
||||
print(f"⚠️ 风控检查未通过: {result['reason']}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"⚠️ 风控检查异常: {e}")
|
||||
return True # 不算失败
|
||||
|
||||
# ==================== 清理和运行 ====================
|
||||
|
||||
def cleanup(self):
|
||||
"""清理测试产生的订单和持仓"""
|
||||
print("\n🧹 清理测试订单...")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
# 撤销所有 BTC 挂单
|
||||
success = self.api.cancel_all_orders('BTCUSDT')
|
||||
if success:
|
||||
print("✅ 已撤销所有 BTC 挂单")
|
||||
else:
|
||||
print("⚠️ 撤销 BTC 挂单返回 False(可能无挂单)")
|
||||
|
||||
# 检查是否有残留持仓
|
||||
positions = self.api.get_position('BTCUSDT')
|
||||
if positions:
|
||||
for pos in positions:
|
||||
contracts = float(pos.get('contracts', 0))
|
||||
if contracts > 0:
|
||||
symbol = pos.get('symbol', '')
|
||||
print(f"⚠️ 发现残留持仓: {symbol} {contracts} 张")
|
||||
print(f" 请手动平仓!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 清理失败: {e}")
|
||||
|
||||
def run_all_tests(self):
|
||||
"""运行所有测试"""
|
||||
tests = [
|
||||
self.test_01_connection,
|
||||
self.test_02_get_balance,
|
||||
self.test_03_get_positions,
|
||||
self.test_04_set_leverage,
|
||||
self.test_05_place_limit_order,
|
||||
self.test_06_get_open_orders,
|
||||
self.test_07_cancel_order,
|
||||
self.test_08_market_order_open,
|
||||
self.test_09_verify_position_opened,
|
||||
self.test_10_set_tp_sl,
|
||||
self.test_11_market_order_close,
|
||||
self.test_12_verify_position_closed,
|
||||
self.test_13_service_get_account_state,
|
||||
self.test_14_service_get_positions,
|
||||
self.test_15_service_contract_size,
|
||||
self.test_16_risk_check,
|
||||
]
|
||||
|
||||
results = []
|
||||
for test in tests:
|
||||
try:
|
||||
result = test()
|
||||
results.append((test.__name__, result))
|
||||
time.sleep(0.5) # 避免触发频率限制
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
results.append((test.__name__, False))
|
||||
|
||||
# 清理
|
||||
self.cleanup()
|
||||
|
||||
# 汇总
|
||||
print("\n" + "=" * 60)
|
||||
print("测试结果汇总")
|
||||
print("=" * 60)
|
||||
|
||||
passed = sum(1 for _, r in results if r)
|
||||
total = len(results)
|
||||
|
||||
for name, result in results:
|
||||
status = "✅ PASS" if result else "❌ FAIL"
|
||||
print(f"{status}: {name}")
|
||||
|
||||
print(f"\n总计: {passed}/{total} 通过")
|
||||
print("=" * 60)
|
||||
|
||||
return passed == total
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("\n" + "!" * 60)
|
||||
print("⚠️ 警告:此测试将使用真实 API 调用!")
|
||||
print("!" * 60)
|
||||
print("请确认:")
|
||||
print("1. 你已了解这会产生真实 API 调用")
|
||||
print("2. 你使用的是测试网或接受小额手续费")
|
||||
print("3. 市价单会立即成交,产生实际盈亏")
|
||||
print("4. 测试后请检查是否有残留订单/持仓")
|
||||
print("!" * 60)
|
||||
|
||||
confirm = input("\n是否继续?(yes/no): ")
|
||||
|
||||
if confirm.lower() != 'yes':
|
||||
print("已取消测试")
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
tester = BitgetIntegrationTest()
|
||||
success = tester.run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
except Exception as e:
|
||||
print(f"\n❌ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
Loading…
Reference in New Issue
Block a user