From 7376fefc10125d70b6274545b3d38a15375b3785 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Sun, 22 Feb 2026 23:54:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20bitget=20=E5=AE=9E?= =?UTF-8?q?=E7=9B=98=E7=9A=84=E6=95=B0=E6=8D=AE=E5=9F=BA=E7=A1=80=E8=AE=BE?= =?UTF-8?q?=E6=96=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 21 + backend/app/config.py | 17 + backend/app/models/real_trading.py | 109 ++++ .../app/services/bitget_trading_api_sdk.py | 542 ++++++++++++++++++ backend/app/services/real_trading_service.py | 433 ++++++++++++++ backend/requirements.txt | 1 + docs/BITGET_SDK_MIGRATION.md | 191 ++++++ docs/REAL_TRADING_MIGRATION_PLAN.md | 486 ++++++++++++++++ docs/REAL_TRADING_SETUP_GUIDE.md | 258 +++++++++ scripts/test_bitget_sdk.py | 170 ++++++ scripts/test_bitget_v2.py | 133 +++++ scripts/test_real_trading.py | 285 +++++++++ 12 files changed, 2646 insertions(+) create mode 100644 backend/app/models/real_trading.py create mode 100644 backend/app/services/bitget_trading_api_sdk.py create mode 100644 backend/app/services/real_trading_service.py create mode 100644 docs/BITGET_SDK_MIGRATION.md create mode 100644 docs/REAL_TRADING_MIGRATION_PLAN.md create mode 100644 docs/REAL_TRADING_SETUP_GUIDE.md create mode 100644 scripts/test_bitget_sdk.py create mode 100644 scripts/test_bitget_v2.py create mode 100644 scripts/test_real_trading.py diff --git a/.env.example b/.env.example index bd43af8..4de533d 100644 --- a/.env.example +++ b/.env.example @@ -150,6 +150,27 @@ PAPER_TRADING_POSITION_A=1000 PAPER_TRADING_POSITION_B=500 PAPER_TRADING_POSITION_C=200 +# ============================================================================ +# 实盘交易配置 - Bitget +# ============================================================================ +# Bitget API 配置(必需) +BITGET_API_KEY=your_bitget_api_key_here +BITGET_API_SECRET=your_bitget_api_secret_here +BITGET_PASSPHRASE=your_bitget_passphrase_here + +# 是否使用测试网(强烈建议先在测试网测试!) +BITGET_USE_TESTNET=true + +# 实盘交易总开关(false 时仅模拟交易生效) +REAL_TRADING_ENABLED=false + +# 实盘交易风险控制 +REAL_TRADING_MAX_SINGLE_POSITION=1000 # 单笔最大持仓金额 (USDT) +REAL_TRADING_MAX_TOTAL_RATIO=0.5 # 最大总仓位比例(账户的50%) +REAL_TRADING_DEFAULT_LEVERAGE=10 # 实盘默认杠杆(低于模拟) +REAL_TRADING_RISK_PER_TRADE=0.02 # 每笔交易风险比例(2%) +REAL_TRADING_MAX_ORDERS=5 # 实盘最大同时持仓数 + # ---------------------------------------------------------------------------- # Agent 模型配置 # ---------------------------------------------------------------------------- diff --git a/backend/app/config.py b/backend/app/config.py index dc2c83b..31de243 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -140,6 +140,23 @@ class Settings(BaseSettings): paper_trading_position_b: float = 500 # B级信号仓位 (USDT) paper_trading_position_c: float = 200 # C级信号仓位 (USDT) + # ========== 实盘交易配置 ========== + # Bitget API 配置 + bitget_api_key: str = "" # Bitget API Key + bitget_api_secret: str = "" # Bitget API Secret + bitget_passphrase: str = "" # Bitget API Passphrase + bitget_use_testnet: bool = True # 是否使用测试网(测试时设为 true) + + # 实盘交易开关 + real_trading_enabled: bool = False # 实盘交易总开关(false 时仅模拟交易生效) + + # 实盘交易风险控制 + real_trading_max_single_position: float = 1000 # 单笔最大持仓金额 (USDT) + real_trading_max_total_ratio: float = 0.5 # 最大总仓位比例(账户的50%) + real_trading_default_leverage: int = 10 # 实盘默认杠杆(低于模拟) + real_trading_risk_per_trade: float = 0.02 # 每笔交易风险比例(2%) + real_trading_max_orders: int = 5 # 实盘最大同时持仓数 + # Agent 模型配置 (可选值: zhipu, deepseek) smart_agent_model: str = "deepseek" # SmartAgent 使用的模型 crypto_agent_model: str = "deepseek" # CryptoAgent 使用的模型 diff --git a/backend/app/models/real_trading.py b/backend/app/models/real_trading.py new file mode 100644 index 0000000..641a083 --- /dev/null +++ b/backend/app/models/real_trading.py @@ -0,0 +1,109 @@ +""" +实盘交易数据模型 + +与模拟交易使用相同的订单状态和方向枚举 +""" +from datetime import datetime +from sqlalchemy import Column, Integer, String, Float, DateTime, JSON, Text, Enum as SQLEnum +from app.models.database import Base +from app.models.paper_trading import OrderStatus, OrderSide, SignalGrade, EntryType + + +class RealOrder(Base): + """实盘交易订单表""" + __tablename__ = "real_orders" + + id = Column(Integer, primary_key=True, index=True) + + # 订单标识 + order_id = Column(String(64), unique=True, nullable=False, index=True) # 本地订单ID + exchange_order_id = Column(String(64), nullable=True, index=True) # 交易所订单ID + client_order_id = Column(String(64), nullable=True, index=True) # 自定义订单ID + + # 交易对信息 + symbol = Column(String(20), nullable=False, index=True) + side = Column(SQLEnum(OrderSide), nullable=False) + + # 价格信息 + entry_price = Column(Float, nullable=False) # 目标入场价 + stop_loss = Column(Float, nullable=False) # 止损价 + take_profit = Column(Float, nullable=False) # 止盈价 + filled_price = Column(Float, nullable=True) # 实际成交价 + exit_price = Column(Float, nullable=True) # 出场价 + + # 仓位信息 + quantity = Column(Float, default=1000) # 仓位大小 (USDT) + leverage = Column(Integer, default=10) # 杠杆倍数 + size = Column(Float, nullable=True) # 合约数量(张数) + + # 信号信息 + signal_grade = Column(SQLEnum(SignalGrade), default=SignalGrade.D) + signal_type = Column(String(20), default="swing") # swing / short_term + confidence = Column(Float, default=0) # 置信度 (0-100) + trend = Column(String(20), nullable=True) # 趋势方向 + entry_type = Column(SQLEnum(EntryType, values_callable=lambda x: [e.value for e in x]), default=EntryType.MARKET) + + # 订单状态 + status = Column(SQLEnum(OrderStatus), default=OrderStatus.PENDING, index=True) + + # 盈亏信息 + pnl_amount = Column(Float, default=0) # 盈亏金额 (USDT) + pnl_percent = Column(Float, default=0) # 盈亏百分比 + fee_amount = Column(Float, default=0) # 手续费 + + # 风险指标 + max_drawdown = Column(Float, default=0) # 持仓期间最大回撤 + max_profit = Column(Float, default=0) # 持仓期间最大盈利 + + # 移动止损相关 + breakeven_triggered = Column(Integer, default=0) # 保本止损是否已触发 + trailing_stop_triggered = Column(Integer, default=0) # 移动止损是否已触发 + trailing_stop_base_profit = Column(Float, default=0) # 移动止损基准盈利 + + # 时间戳 + created_at = Column(DateTime, default=datetime.now, index=True) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + filled_at = Column(DateTime, nullable=True) # 成交时间 + closed_at = Column(DateTime, nullable=True) # 平仓时间 + + # 额外数据 + extra_data = Column(JSON, nullable=True) # 存储额外信息(metadata是保留字) + notes = Column(Text, nullable=True) # 备注 + + def to_dict(self) -> dict: + """转换为字典""" + return { + 'id': self.id, + 'order_id': self.order_id, + 'exchange_order_id': self.exchange_order_id, + 'client_order_id': self.client_order_id, + 'symbol': self.symbol, + 'side': self.side.value if self.side else None, + 'entry_price': self.entry_price, + 'stop_loss': self.stop_loss, + 'take_profit': self.take_profit, + 'filled_price': self.filled_price, + 'exit_price': self.exit_price, + 'quantity': self.quantity, + 'leverage': self.leverage, + 'size': self.size, + 'signal_grade': self.signal_grade.value if self.signal_grade else None, + 'signal_type': self.signal_type, + 'confidence': self.confidence, + 'trend': self.trend, + 'entry_type': self.entry_type.value if self.entry_type else None, + 'status': self.status.value if self.status else None, + 'pnl_amount': self.pnl_amount, + 'pnl_percent': self.pnl_percent, + 'fee_amount': self.fee_amount, + 'max_drawdown': self.max_drawdown, + 'max_profit': self.max_profit, + 'breakeven_triggered': bool(self.breakeven_triggered), + 'trailing_stop_triggered': bool(self.trailing_stop_triggered), + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None, + 'filled_at': self.filled_at.isoformat() if self.filled_at else None, + 'closed_at': self.closed_at.isoformat() if self.closed_at else None, + 'extra_data': self.extra_data, + 'notes': self.notes + } diff --git a/backend/app/services/bitget_trading_api_sdk.py b/backend/app/services/bitget_trading_api_sdk.py new file mode 100644 index 0000000..4b50ddb --- /dev/null +++ b/backend/app/services/bitget_trading_api_sdk.py @@ -0,0 +1,542 @@ +""" +Bitget 实盘交易 API (基于 CCXT SDK) + +处理 Bitget 合约交易的所有 API 调用,包括: +- 下单(开仓/平仓) +- 撤单 +- 查询订单 +- 查询持仓 +- 查询账户余额 +- 设置杠杆 + +使用 CCXT 统一交易所接口,提供更稳定的 API 交互。 +""" +import ccxt +from typing import Dict, List, Optional, Any +from datetime import datetime +from app.utils.logger import logger + + +class BitgetTradingAPI: + """Bitget 实盘交易 API (基于 CCXT)""" + + def __init__(self, api_key: str, api_secret: str, passphrase: str = "", use_testnet: bool = True): + """ + 初始化 Bitget 交易 API + + Args: + api_key: API Key + api_secret: API Secret + passphrase: API Passphrase (Bitget 不需要,保留兼容性) + use_testnet: 是否使用测试网 + """ + self.api_key = api_key + self.api_secret = api_secret + self.use_testnet = use_testnet + + # 创建 CCXT Bitget 实例 + config = { + 'apiKey': api_key, + 'secret': api_secret, + 'enableRateLimit': True, # 启用速率限制 + 'options': { + 'defaultType': 'swap', # 使用永续合约 API + } + } + + # CCXT Bitget 使用 password 字段存储 passphrase + # 如果提供了 passphrase,添加到配置中 + if passphrase: + config['password'] = passphrase + + self.exchange = ccxt.bitget(config) + + # 设置测试网(sandbox 模式) + if use_testnet: + if hasattr(self.exchange, 'set_sandbox_mode'): + self.exchange.set_sandbox_mode(True) + logger.info("✅ Bitget 测试网模式已启用") + + logger.info(f"Bitget 交易 API 初始化完成 ({'测试网' if use_testnet else '生产网'})") + + # ==================== 订单操作 ==================== + + def place_order(self, symbol: str, side: str, order_type: str, + size: float, price: float = None, client_order_id: str = None) -> Optional[Dict]: + """ + 下单 + + Args: + symbol: 交易对 (如 BTC/USDT:USDT) + side: 订单方向 (buy/sell) + order_type: 订单类型 (limit/market) + size: 数量(张数) + price: 价格(限价单必需) + client_order_id: 自定义订单ID + + Returns: + 订单信息 + """ + try: + # CCXT 标准化交易对格式 + ccxt_symbol = self._standardize_symbol(symbol) + + # 构建订单参数 + params = {} + if client_order_id: + params['clientOrderId'] = client_order_id + + # 下单 + if order_type == 'market': + order = self.exchange.create_market_order( + symbol=ccxt_symbol, + side=side, + amount=size, + params=params + ) + else: # limit + if not price: + logger.error("限价单必须指定价格") + return None + order = self.exchange.create_limit_order( + symbol=ccxt_symbol, + side=side, + amount=size, + price=price, + params=params + ) + + logger.info(f"✅ 下单成功: {symbol} {side} {size}张 @ {price or '市价'}") + logger.debug(f"订单详情: {order}") + return order + + except ccxt.BaseError as e: + logger.error(f"❌ 下单失败: {e}") + return None + except Exception as e: + logger.error(f"❌ 下单异常: {e}") + return None + + def cancel_order(self, symbol: str, order_id: str = None, client_order_id: str = None) -> bool: + """ + 撤单 + + Args: + symbol: 交易对 + order_id: 订单ID + client_order_id: 自定义订单ID + + Returns: + 是否成功 + """ + try: + ccxt_symbol = self._standardize_symbol(symbol) + + if order_id: + self.exchange.cancel_order(order_id, ccxt_symbol) + elif client_order_id: + self.exchange.cancel_order_by_client_order_id(client_order_id, ccxt_symbol) + else: + logger.error("必须提供 order_id 或 client_order_id") + return False + + logger.info(f"✅ 撤单成功: {symbol} order_id={order_id or client_order_id}") + return True + + except ccxt.BaseError as e: + logger.error(f"❌ 撤单失败: {e}") + return False + except Exception as e: + logger.error(f"❌ 撤单异常: {e}") + return False + + def cancel_all_orders(self, symbol: str) -> bool: + """ + 撤销所有挂单 + + Args: + symbol: 交易对 + + Returns: + 是否成功 + """ + try: + ccxt_symbol = self._standardize_symbol(symbol) + self.exchange.cancel_all_orders(ccxt_symbol) + + logger.info(f"✅ 撤销所有挂单成功: {symbol}") + return True + + except ccxt.BaseError as e: + logger.error(f"❌ 撤销所有挂单失败: {e}") + return False + except Exception as e: + logger.error(f"❌ 撤销所有挂单异常: {e}") + return False + + def close_position(self, symbol: str, side: str, size: float = None, + price: float = None) -> Optional[Dict]: + """ + 平仓 + + Args: + symbol: 交易对 + side: 平仓方向 (buy=平空仓/sell=平多仓) + size: 平仓数量(不传则全部平仓) + price: 平仓价格(不传则市价) + + Returns: + 订单信息 + """ + try: + ccxt_symbol = self._standardize_symbol(symbol) + + # 获取当前持仓 + positions = self.get_position(symbol) + if not positions: + logger.warning(f"没有找到 {symbol} 的持仓") + return None + + position = positions[0] + current_size = abs(float(position.get('contracts', 0))) + + if current_size == 0: + logger.warning(f"{symbol} 持仓数量为 0,无需平仓") + return None + + # 如果没有指定平仓数量,则全部平仓 + close_size = size if size else current_size + + # 执行平仓 + 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 order: + logger.info(f"✅ 平仓成功: {symbol} {side} {close_size}张") + return order + + return None + + except Exception as e: + logger.error(f"❌ 平仓异常: {e}") + return None + + # ==================== 查询操作 ==================== + + def get_order(self, symbol: str, order_id: str = None, client_order_id: str = None) -> Optional[Dict]: + """ + 查询订单 + + Args: + symbol: 交易对 + order_id: 订单ID + client_order_id: 自定义订单ID + + Returns: + 订单信息 + """ + try: + ccxt_symbol = self._standardize_symbol(symbol) + + if order_id: + order = self.exchange.fetch_order(order_id, ccxt_symbol) + elif client_order_id: + order = self.exchange.fetch_order_by_client_order_id(client_order_id, ccxt_symbol) + else: + logger.error("必须提供 order_id 或 client_order_id") + return None + + return order + + except ccxt.BaseError as e: + logger.error(f"❌ 查询订单失败: {e}") + return None + except Exception as e: + logger.error(f"❌ 查询订单异常: {e}") + return None + + def get_open_orders(self, symbol: str = None) -> List[Dict]: + """ + 查询当前挂单 + + Args: + symbol: 交易对(不传则查询所有) + + Returns: + 挂单列表 + """ + try: + ccxt_symbol = self._standardize_symbol(symbol) if symbol else None + orders = self.exchange.fetch_open_orders(ccxt_symbol) + + logger.debug(f"查询到 {len(orders)} 个挂单") + return orders + + except ccxt.BaseError as e: + logger.error(f"❌ 查询挂单失败: {e}") + return [] + except Exception as e: + logger.error(f"❌ 查询挂单异常: {e}") + return [] + + def get_history_orders(self, symbol: str, limit: int = 100) -> List[Dict]: + """ + 查询历史订单 + + Args: + symbol: 交易对 + limit: 返回数量 + + Returns: + 历史订单列表 + """ + try: + ccxt_symbol = self._standardize_symbol(symbol) + orders = self.exchange.fetch_my_trades(ccxt_symbol, limit=limit) + + logger.debug(f"查询到 {len(orders)} 条历史订单") + return orders + + except ccxt.BaseError as e: + logger.error(f"❌ 查询历史订单失败: {e}") + return [] + except Exception as e: + logger.error(f"❌ 查询历史订单异常: {e}") + return [] + + # ==================== 持仓操作 ==================== + + def get_position(self, symbol: str = None) -> List[Dict]: + """ + 查询持仓 + + Args: + symbol: 交易对(不传则查询所有) + + Returns: + 持仓列表 + """ + try: + # 获取所有持仓 + positions = self.exchange.fetch_positions() + + # 筛选非零持仓 + active_positions = [] + for pos in positions: + size = float(pos.get('contracts', 0)) + if size != 0: + # 如果指定了交易对,进行过滤 + if symbol: + ccxt_symbol = self._standardize_symbol(symbol) + if pos['symbol'] == ccxt_symbol: + active_positions.append(pos) + else: + active_positions.append(pos) + + logger.debug(f"查询到 {len(active_positions)} 个持仓") + return active_positions + + except ccxt.BaseError as e: + logger.error(f"❌ 查询持仓失败: {e}") + return [] + except Exception as e: + logger.error(f"❌ 查询持仓异常: {e}") + return [] + + def get_single_position(self, symbol: str) -> Optional[Dict]: + """ + 查询单个交易对的持仓 + + Args: + symbol: 交易对 + + Returns: + 持仓信息 + """ + positions = self.get_position(symbol) + if positions: + return positions[0] + return None + + # ==================== 账户操作 ==================== + + def get_balance(self) -> Dict: + """ + 查询账户余额 + + Returns: + 余额信息 {USDT: {available: "...", frozen: "...", locked: "..."}} + """ + try: + balance = self.exchange.fetch_balance() + + # 转换为统一格式 + result = {} + for currency, info in balance.get('total', {}).items(): + if info and info > 0: # 只返回有余额的币种 + result[currency] = { + 'available': str(balance.get('free', {}).get(currency, 0)), + 'frozen': str(balance.get('used', {}).get(currency, 0)), + 'locked': '0' + } + + logger.debug(f"账户余额: {result}") + return result + + except ccxt.BaseError as e: + logger.error(f"❌ 查询余额失败: {e}") + return {} + except Exception as e: + logger.error(f"❌ 查询余额异常: {e}") + return {} + + def get_account_info(self) -> Dict: + """ + 查询账户信息 + + Returns: + 账户信息 + """ + try: + balance = self.exchange.fetch_balance() + return balance + except ccxt.BaseError as e: + logger.error(f"❌ 查询账户信息失败: {e}") + return {} + except Exception as e: + logger.error(f"❌ 查询账户信息异常: {e}") + return {} + + def set_leverage(self, symbol: str, leverage: int, hold_side: str = "long") -> bool: + """ + 设置杠杆倍数 + + Args: + symbol: 交易对 + leverage: 杠杆倍数 (1-125) + hold_side: 持仓方向 (long/short) - CCXT 通常设置为 both + + Returns: + 是否成功 + """ + try: + ccxt_symbol = self._standardize_symbol(symbol) + + # CCXT 设置杠杆 + self.exchange.set_leverage(leverage, ccxt_symbol) + + logger.info(f"✅ 设置杠杆成功: {symbol} {leverage}x") + return True + + except ccxt.BaseError as e: + logger.error(f"❌ 设置杠杆失败: {e}") + return False + except Exception as e: + logger.error(f"❌ 设置杠杆异常: {e}") + return False + + # ==================== 辅助方法 ==================== + + def _standardize_symbol(self, symbol: str) -> str: + """ + 标准化交易对格式为 CCXT 格式 + + Args: + symbol: 原始交易对 (如 BTCUSDT) + + Returns: + CCXT 标准格式 (如 BTC/USDT:USDT) + """ + # 如果已经是 CCXT 格式,直接返回 + if '/' in symbol: + return symbol + + # 简单的转换逻辑(可以根据实际情况扩展) + # 例如:BTCUSDT -> BTC/USDT:USDT + if symbol.endswith('USDT'): + base = symbol[:-4] # 去掉 USDT + return f"{base}/USDT:USDT" + + # 默认返回原值 + return symbol + + def test_connection(self) -> bool: + """ + 测试 API 连接 + + Returns: + 是否连接成功 + """ + try: + balance = self.get_balance() + if balance is not None: + usdt_balance = balance.get('USDT', {}).get('available', 'N/A') + logger.info(f"✅ API 连接成功,USDT 余额: {usdt_balance}") + return True + return False + except Exception as e: + logger.error(f"❌ API 连接失败: {e}") + return False + + def close(self): + """关闭连接""" + if self.exchange: + # CCXT exchange 对象不需要显式关闭 + # 但可以清理 WebSocket 连接(如果有的话) + try: + if hasattr(self.exchange, 'close'): + self.exchange.close() + except Exception as e: + logger.debug(f"关闭连接时出错(可忽略): {e}") + logger.info("Bitget API 连接已关闭") + + +# 全局实例(延迟初始化) +_trading_api: Optional[BitgetTradingAPI] = None + + +def get_bitget_trading_api() -> Optional[BitgetTradingAPI]: + """ + 获取 Bitget 交易 API 实例(单例) + + Returns: + BitgetTradingAPI 实例或 None(如果未配置) + """ + global _trading_api + + if _trading_api: + return _trading_api + + from app.config import get_settings + + settings = get_settings() + + # 检查是否配置了 API Key + if not settings.bitget_api_key or not settings.bitget_api_secret: + logger.warning("Bitget API Key 未配置,实盘交易功能不可用") + return None + + # 创建实例 + _trading_api = BitgetTradingAPI( + api_key=settings.bitget_api_key, + api_secret=settings.bitget_api_secret, + passphrase=settings.bitget_passphrase, + use_testnet=settings.bitget_use_testnet + ) + + return _trading_api + + +def reset_bitget_trading_api(): + """重置全局实例(用于测试或配置更新)""" + global _trading_api + if _trading_api: + _trading_api.close() + _trading_api = None + logger.info("Bitget API 实例已重置") diff --git a/backend/app/services/real_trading_service.py b/backend/app/services/real_trading_service.py new file mode 100644 index 0000000..234adbe --- /dev/null +++ b/backend/app/services/real_trading_service.py @@ -0,0 +1,433 @@ +""" +实盘交易服务 - Bitget 合约交易 + +提供与模拟交易服务类似的接口,但执行的是真实交易 +""" +import uuid +from datetime import datetime, timedelta +from typing import Dict, Any, List, Optional + +from app.models.real_trading import RealOrder +from app.models.paper_trading import OrderStatus, OrderSide, SignalGrade, EntryType +from app.services.db_service import db_service +from app.config import get_settings +from app.utils.logger import logger + + +class RealTradingService: + """实盘交易服务""" + + def __init__(self): + """初始化实盘交易服务""" + self.settings = get_settings() + self.active_orders: Dict[str, RealOrder] = {} # 内存缓存活跃订单 + + # 实盘交易配置 + self.max_single_position = self.settings.real_trading_max_single_position + self.max_total_ratio = self.settings.real_trading_max_total_ratio + self.default_leverage = self.settings.real_trading_default_leverage + self.risk_per_trade = self.settings.real_trading_risk_per_trade + self.max_orders = self.settings.real_trading_max_orders + + # 获取交易 API (使用 CCXT SDK 版本) + from app.services.bitget_trading_api_sdk import get_bitget_trading_api + self.trading_api = get_bitget_trading_api() + + if not self.trading_api: + logger.error("Bitget 交易 API 未初始化,实盘交易功能不可用") + return + + # 确保表已创建 + self._ensure_table_exists() + + # 加载活跃订单 + self._load_active_orders() + + logger.info(f"实盘交易服务初始化完成(最大单笔: ${self.max_single_position}," + f"杠杆: {self.default_leverage}x,最大持仓: {self.max_orders})") + + def _ensure_table_exists(self): + """确保数据表已创建""" + from app.models.real_trading import RealOrder + from app.models.database import Base + Base.metadata.create_all(bind=db_service.engine) + + def _load_active_orders(self): + """从数据库加载活跃订单""" + db = db_service.get_session() + try: + orders = db.query(RealOrder).filter( + RealOrder.status.in_([OrderStatus.PENDING, OrderStatus.OPEN]) + ).all() + + for order in orders: + self.active_orders[order.order_id] = order + + logger.info(f"实盘交易: 加载了 {len(orders)} 个活跃订单") + + finally: + db.close() + + def create_order_from_signal(self, signal: Dict[str, Any], current_price: float = None) -> Dict[str, Any]: + """ + 从信号创建实盘订单 + + Args: + signal: LLM 分析信号 + current_price: 当前价格 + + Returns: + 创建结果 + """ + if not self.trading_api: + return { + 'success': False, + 'message': '交易 API 未初始化' + } + + db = db_service.get_session() + result = { + 'success': False, + 'message': '', + 'order_id': None + } + + try: + # 检查实盘交易是否启用 + if not self.settings.real_trading_enabled: + return { + 'success': False, + 'message': '实盘交易未启用,请检查配置 REAL_TRADING_ENABLED=true' + } + + # 获取信号信息 + symbol = signal.get('symbol') + side = signal.get('side') # 'long' or 'short' + entry_type = signal.get('entry_type', 'market') # 'market' or 'limit' + entry_price = signal.get('entry_price') + stop_loss = signal.get('stop_loss') + take_profit = signal.get('take_profit') + grade = signal.get('grade', 'D') + confidence = signal.get('confidence', 0) + + # 验证必需参数 + if not all([symbol, side, stop_loss, take_profit]): + return { + 'success': False, + 'message': '信号缺少必需参数' + } + + # 获取当前价格 + if not current_price: + from app.services.bitget_service import bitget_service + current_price = bitget_service.get_current_price(symbol) + + if not current_price: + return { + 'success': False, + 'message': '无法获取当前价格' + } + + # 设置入场价 + if entry_type == 'market': + entry_price = current_price + elif not entry_price: + entry_price = current_price + + # 风险检查 - 检查订单数量 + if len(self.active_orders) >= self.max_orders: + return { + 'success': False, + 'message': f'已达最大持仓数 {self.max_orders}' + } + + # 风险检查 - 检查账户余额 + balance_info = self._check_balance_risk(symbol, entry_price, stop_loss) + if not balance_info['can_trade']: + return { + 'success': False, + 'message': balance_info['reason'] + } + + # 创建订单对象 + order_id = str(uuid.uuid4()) + order = RealOrder( + order_id=order_id, + symbol=symbol, + side=OrderSide.LONG if side == 'long' else OrderSide.SHORT, + entry_price=entry_price, + stop_loss=stop_loss, + take_profit=take_profit, + quantity=balance_info['quantity'], + leverage=self.default_leverage, + signal_grade=SignalGrade[grade.upper()] if grade.upper() in ['A', 'B', 'C', 'D'] else SignalGrade.D, + signal_type=signal.get('signal_type', 'swing'), + confidence=confidence, + trend=signal.get('trend'), + entry_type=EntryType.MARKET if entry_type == 'market' else EntryType.LIMIT, + status=OrderStatus.PENDING + ) + + db.add(order) + db.commit() + db.refresh(order) + + # 调用 Bitget API 下单 + exchange_result = self._place_bitget_order(order, current_price) + + if exchange_result['success']: + # 更新订单状态 + order.exchange_order_id = exchange_result.get('order_id') + order.client_order_id = exchange_result.get('client_order_id') + order.filled_price = exchange_result.get('filled_price', entry_price) + order.filled_at = datetime.now() + order.status = OrderStatus.OPEN + + db.commit() + + # 添加到内存缓存 + self.active_orders[order_id] = order + + result['success'] = True + result['message'] = '实盘订单创建成功' + result['order_id'] = order_id + result['exchange_order_id'] = exchange_result.get('order_id') + + logger.info(f"✅ 实盘订单创建成功: {symbol} {side} ${entry_price} -> {exchange_result.get('order_id')}") + else: + # 下单失败,删除记录 + db.delete(order) + db.commit() + + result['message'] = f"下单失败: {exchange_result.get('message', '未知错误')}" + + logger.error(f"❌ 实盘订单下单失败: {exchange_result.get('message')}") + + except Exception as e: + db.rollback() + result['message'] = f"创建订单失败: {str(e)}" + logger.error(f"创建实盘订单失败: {e}") + + finally: + db.close() + + return result + + def _check_balance_risk(self, symbol: str, entry_price: float, stop_loss: float) -> Dict: + """ + 检查余额和风险 + + Returns: + {'can_trade': bool, 'reason': str, 'quantity': float} + """ + try: + # 获取账户余额 + balance_info = self.trading_api.get_balance() + usdt_balance = balance_info.get('USDT', {}) + available = float(usdt_balance.get('available', 0)) + + if available < 10: + return { + 'can_trade': False, + 'reason': f'可用余额不足 (${available:.2f})', + 'quantity': 0 + } + + # 计算风险 + risk_amount = entry_price - stop_loss + risk_percent = (risk_amount / entry_price) * 100 + + # 根据风险计算仓位 + max_quantity = (available * self.risk_per_trade) / (risk_percent / 100) + + # 限制单笔最大仓位 + max_quantity = min(max_quantity, self.max_single_position) + + # 最小仓位限制 + min_quantity = 10 + if max_quantity < min_quantity: + return { + 'can_trade': False, + 'reason': f'风险过高,计算仓位 ${max_quantity:.2f} 小于最小值 ${min_quantity}', + 'quantity': 0 + } + + return { + 'can_trade': True, + 'reason': '', + 'quantity': round(max_quantity, 2) + } + + except Exception as e: + logger.error(f"检查余额风险失败: {e}") + return { + 'can_trade': False, + 'reason': f'风险检查失败: {str(e)}', + 'quantity': 0 + } + + def _place_bitget_order(self, order: RealOrder, current_price: float) -> Dict: + """ + 调用 Bitget API 下单 (使用 CCXT SDK) + + Returns: + {'success': bool, 'order_id': str, 'client_order_id': str, 'filled_price': float, 'message': str} + """ + try: + # CCXT 使用标准的 buy/sell 方向 + # 对于合约:buy = 开多/平空,sell = 开空/平多 + # 这里我们简化为:long = buy, short = sell + side_map = { + OrderSide.LONG: 'buy', + OrderSide.SHORT: 'sell' + } + + # 映射订单类型 + order_type = 'market' if order.entry_type == EntryType.MARKET else 'limit' + + # 计算合约数量(张数) + # Bitget U本位合约:1张 = 1 USDT(大多数情况) + size = order.quantity # 简化处理,实际应该根据合约规格计算 + + # 生成自定义订单ID + client_order_id = f"real_{order.order_id[:8]}" + + # 调用 API 下单 + result = self.trading_api.place_order( + symbol=order.symbol, + side=side_map[order.side], + order_type=order_type, + size=size, + price=order.entry_price if order_type == 'limit' else None, + client_order_id=client_order_id + ) + + if result: + # CCXT 返回的订单对象格式 + # result['id'] 是交易所订单ID + # result['price'] 是委托价格 + # result['average'] 是成交均价(如果已成交) + order_id = result.get('id') + filled_price = float(result.get('average', 0)) or current_price + + return { + 'success': True, + 'order_id': order_id, + 'client_order_id': client_order_id, + 'filled_price': filled_price + } + else: + return { + 'success': False, + 'message': 'API 调用失败' + } + + except Exception as e: + logger.error(f"Bitget 下单失败: {e}") + return { + 'success': False, + 'message': str(e) + } + + def get_active_orders(self) -> List[Dict]: + """获取活跃订单列表""" + return [order.to_dict() for order in self.active_orders.values()] + + def get_order(self, order_id: str) -> Optional[Dict]: + """获取指定订单""" + order = self.active_orders.get(order_id) + if order: + return order.to_dict() + return None + + def sync_positions_from_exchange(self) -> List[Dict]: + """ + 从交易所同步持仓状态 + + Returns: + 同步后的持仓列表 + """ + if not self.trading_api: + return [] + + try: + # 获取交易所实际持仓 + positions = self.trading_api.get_position() + + logger.info(f"从交易所同步了 {len(positions)} 个持仓") + return positions + + except Exception as e: + logger.error(f"同步持仓失败: {e}") + return [] + + def get_account_status(self) -> Dict: + """获取账户状态""" + if not self.trading_api: + return { + 'current_balance': 0, + 'used_margin': 0, + 'total_position_value': 0, + 'available': 0 + } + + try: + balance_info = self.trading_api.get_balance() + usdt_info = balance_info.get('USDT', {}) + + available = float(usdt_info.get('available', 0)) + frozen = float(usdt_info.get('frozen', 0)) + locked = float(usdt_info.get('locked', 0)) + + # 计算持仓价值 + total_position_value = 0 + for order in self.active_orders.values(): + if order.status == OrderStatus.OPEN: + total_position_value += order.quantity + + return { + 'current_balance': available + frozen + locked, + 'available': available, + 'used_margin': frozen + locked, + 'total_position_value': total_position_value + } + + except Exception as e: + logger.error(f"获取账户状态失败: {e}") + return { + 'current_balance': 0, + 'used_margin': 0, + 'total_position_value': 0, + 'available': 0 + } + + +# 全局实例 +_real_trading_service: Optional[RealTradingService] = None + + +def get_real_trading_service() -> Optional[RealTradingService]: + """ + 获取实盘交易服务实例(单例) + + Returns: + RealTradingService 实例或 None(如果未配置或未启用) + """ + global _real_trading_service + + if _real_trading_service: + return _real_trading_service + + settings = get_settings() + + # 检查是否启用实盘交易 + if not settings.real_trading_enabled: + return None + + # 检查是否配置了 API Key + if not settings.bitget_api_key or not settings.bitget_api_secret: + logger.warning("Bitget API Key 未配置,实盘交易功能不可用") + return None + + _real_trading_service = RealTradingService() + return _real_trading_service diff --git a/backend/requirements.txt b/backend/requirements.txt index 3fa51a8..61deb29 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -22,3 +22,4 @@ python-jose[cryptography]==3.3.0 # 加密货币交易智能体依赖 python-binance>=1.0.19 httpx>=0.27.0 +ccxt>=4.0.0 # 统一交易所API接口,支持Bitget等主流交易所 diff --git a/docs/BITGET_SDK_MIGRATION.md b/docs/BITGET_SDK_MIGRATION.md new file mode 100644 index 0000000..175f7bd --- /dev/null +++ b/docs/BITGET_SDK_MIGRATION.md @@ -0,0 +1,191 @@ +# Bitget Python SDK 迁移指南 + +## 概述 + +已将 Bitget 交易 API 从手动实现迁移到使用 **CCXT Python SDK**。 + +## 主要变化 + +### 1. 依赖变更 + +**新增依赖:** +```bash +pip install ccxt>=4.0.0 +``` + +### 2. 文件结构 + +| 旧文件 | 新文件 | 说明 | +|--------|--------|------| +| `bitget_trading_api.py` | `bitget_trading_api_sdk.py` | 使用 CCXT 的新实现 | +| - | `scripts/test_bitget_sdk.py` | SDK 测试脚本 | + +### 3. API 响应格式变化 + +#### 旧版本(直接 API) +```python +# 订单响应格式 +{ + 'orderId': '123456789', + 'fillPrice': '50000.5', + ... +} +``` + +#### 新版本(CCXT) +```python +# 订单响应格式 +{ + 'id': '123456789', + 'price': '50000.0', # 委托价格 + 'average': '50000.5', # 成交均价 + 'status': 'closed', # CCXT 标准状态 + ... +} +``` + +### 4. 订单方向变化 + +#### 旧版本 +```python +side_map = { + OrderSide.LONG: 'open_long', + OrderSide.SHORT: 'open_short' +} +``` + +#### 新版本(CCXT 标准化) +```python +side_map = { + OrderSide.LONG: 'buy', + OrderSide.SHORT: 'sell' +} +``` + +### 5. 交易对格式 + +#### 旧版本 +``` +BTCUSDT +ETHUSDT +``` + +#### 新版本(CCXT 标准化) +``` +BTC/USDT:USDT +ETH/USDT:USDT +``` + +新实现会自动进行格式转换。 + +## 安装步骤 + +### 1. 安装依赖 + +```bash +cd backend +pip install -r requirements.txt +``` + +### 2. 配置环境变量 + +在 `.env` 文件中配置: + +```bash +# Bitget API 配置 +BITGET_API_KEY=your_api_key_here +BITGET_API_SECRET=your_api_secret_here +BITGET_PASSPHRASE= # Bitget 不需要,保留兼容性 +BITGET_USE_TESTNET=true # 测试时设为 true + +# 实盘交易开关 +REAL_TRADING_ENABLED=false # 生产环境再启用 +``` + +### 3. 测试连接 + +```bash +cd scripts +python test_bitget_sdk.py +``` + +预期输出: +``` +============================================================ +测试 1: API 连接测试 +============================================================ +✅ API 连接成功,USDT 余额: 10000.00 + +============================================================ +测试 2: 查询账户余额 +============================================================ +✅ 余额查询成功 + USDT: 可用=10000.00, 冻结=0.00 +... +``` + +## CCXT 的优势 + +1. **统一接口** - 同样的代码可以支持多个交易所(Binance, OKX, Bybit 等) +2. **更好的维护** - CCXT 团队持续更新,修复 API 变更 +3. **内置重试** - 自动处理网络问题和速率限制 +4. **标准化格式** - 统一的订单状态、交易对格式 +5. **异步支持** - 支持 async/await 模式 + +## API 功能对比 + +| 功能 | 旧实现 | 新实现(CCXT) | +|------|--------|----------------| +| 下单 | ✅ | ✅ | +| 撤单 | ✅ | ✅ | +| 查询订单 | ✅ | ✅ | +| 查询挂单 | ✅ | ✅ | +| 查询持仓 | ✅ | ✅ | +| 查询余额 | ✅ | ✅ | +| 设置杠杆 | ✅ | ✅ | +| 测试网支持 | ✅ | ✅ | +| 自动重试 | ❌ | ✅ | +| 速率限制 | ❌ | ✅ | + +## 兼容性说明 + +- ✅ `real_trading_service.py` 已更新使用新 SDK +- ✅ 保持相同的公共接口 +- ✅ 数据库模型无需更改 +- ⚠️ 需要重新测试实盘交易功能 + +## 下一步 + +1. **在测试网验证** - 确保所有功能正常 +2. **小资金实盘测试** - 验证订单执行 +3. **监控日志** - 检查是否有任何错误 +4. **逐步迁移** - 确认稳定后使用新版本 + +## 回滚方案 + +如果需要回滚到旧版本: + +1. 修改 `real_trading_service.py` 第 33 行: +```python +# 从 +from app.services.bitget_trading_api_sdk import get_bitget_trading_api + +# 改回 +from app.services.bitget_trading_api import get_bitget_trading_api +``` + +2. 重启服务 + +## 相关资源 + +- CCXT 官方文档: https://docs.ccxt.com/ +- CCXT Bitget 支持: https://docs.ccxt.com/#exchange-sources +- Bitget 官方文档: https://www.bitget.com/api-doc + +## 支持 + +如有问题,请检查: +1. CCXT 版本是否 >= 4.0.0 +2. API 密钥是否正确 +3. 网络连接是否正常 +4. 测试网是否可用 diff --git a/docs/REAL_TRADING_MIGRATION_PLAN.md b/docs/REAL_TRADING_MIGRATION_PLAN.md new file mode 100644 index 0000000..614d55c --- /dev/null +++ b/docs/REAL_TRADING_MIGRATION_PLAN.md @@ -0,0 +1,486 @@ +# Bitget 实盘交易迁移方案 + +> **目标**: 将当前的模拟交易系统切换到 Bitget 实盘合约交易 +> +> **状态**: 方案制定中 - 未实施 + +--- + +## 一、当前系统架构分析 + +### 1.1 模拟交易系统现状 + +**核心文件**: `backend/app/services/paper_trading_service.py` + +**主要功能**: +- ✅ 订单创建 (`create_order_from_signal`) +- ✅ 订单平仓 (`_close_order`, `close_order_manual`) +- ✅ 止盈止损检查 (`check_price_triggers`) +- ✅ 移动止损逻辑 +- ✅ 账户余额管理 +- ✅ 持仓信息查询 + +**订单状态管理**: +- `PENDING` - 挂单中(等待入场价格) +- `OPEN` - 已开仓 +- `CLOSED` - 已平仓 + +**配置参数** (`.env`): +```bash +PAPER_TRADING_ENABLED=true # 启用模拟交易 +PAPER_TRADING_INITIAL_BALANCE=10000 # 初始本金 +PAPER_TRADING_LEVERAGE=20 # 杠杆倍数 +PAPER_TRADING_MARGIN_PER_ORDER=1000 # 每单保证金 +PAPER_TRADING_MAX_ORDERS=10 # 最大订单数 +PAPER_TRADING_AUTO_CLOSE_OPPOSITE=false # 自动平反向持仓 +PAPER_TRADING_TRAILING_STOP_ENABLED=true # 移动止损 +``` + +### 1.2 数据流向 + +``` +LLM 分析信号 → PaperTradingService → 数据库记录 + ↓ + 本地模拟执行 + ↓ + 止盈止损检查(本地价格轮询) +``` + +--- + +## 二、实盘交易架构设计 + +### 2.1 整体方案 + +**方案 A - 双模式并存** (推荐) +- 保留模拟交易作为回测和测试 +- 新增实盘交易服务 +- 通过配置开关切换 + +**方案 B - 完全替换** +- 直接将模拟交易改为实盘 +- 风险较高,不推荐 + +### 2.2 核心组件设计 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 交易服务层 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ PaperTradingService │ │ RealTradingService │ │ +│ │ (模拟交易) │ │ (实盘交易) │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ │ │ │ +│ └────────────┬───────────────┘ │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ TradingInterface │ (统一接口) │ +│ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ BitgetTradingAPI │ │ +│ │ (实盘交易API) │ │ +│ └──────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 三、Bitget 实盘 API 需求 + +### 3.1 需要 API 功能 + +| 功能 | Bitget API 端点 | 说明 | +|------|----------------|------| +| **下单** | `POST /api/mix/v1/order/placeOrder` | U本位合约下单 | +| **撤单** | `POST /api/mix/v1/order/cancelOrder` | 撤销挂单 | +| **平仓** | `POST /api/mix v1/order/closePosition` | 平仓持仓 | +| **查询持仓** | `GET /api/mix/v1/position/allPosition` | 获取当前持仓 | +| **查询订单** | `GET /api/mix/v1/order/orderInfo` | 获取订单状态 | +| **修改订单** | `POST /api/mix/v1/order/modifyOrder` | 修改挂单价格 | +| **账户余额** | `GET /api/mix/v1/account/account` | 获取账户余额 | +| **设置杠杆** | `POST /api/mix/v1/account/setLeverage` | 设置杠杆倍数 | + +### 3.2 Bitget 订单类型 + +```python +# 订单方向 +side_map = { + 'long': 'open_long', # 开多 + 'short': 'open_short', # 开空 +} + +# 订单类型 +order_type_map = { + 'limit': 'limit', # 限价单(挂单) + 'market': 'market', # 市价单 +} + +# 产品类型 +product_type = 'USDT-FUTURES' # U本位永续合约 +``` + +### 3.3 订单参数映射 + +| 模拟交易参数 | Bitget API 参数 | 说明 | +|-------------|----------------|------| +| `symbol` | `symbol` | 交易对 (如 BTCUSDT) | +| `side` | `side` | open_long/close_long/open_short/close_short | +| `order_type` | `orderType` | limit/market | +| `quantity` | `size` | 数量(张数) | +| `entry_price` | `price` | 委托价格 | +| `stop_loss` | `stopLoss` | 止损价格 | +| `take_profit` | `takeProfit` | 止盈价格 | +| `leverage` | `leverage` | 杠杆倍数 | + +--- + +## 四、实施步骤 + +### 阶段一:准备工作 (1-2天) + +#### 1.1 Bitget API 密钥申请 +- [ ] 注册 Bitget 账户 +- [ ] 开启合约交易功能 +- [ ] 创建 API Key +- [ ] 配置 IP 白名单 +- [ ] **选择测试网进行初期测试** + +#### 1.2 测试网环境搭建 +- [ ] 获取 Bitget 测试网凭证 +- [ ] 配置测试网 API 端点:`https://api-testnet.bitget.com` +- [ ] 验证 API 连接性 + +#### 1.3 风险控制参数设置 +```bash +# .env 新增配置 +BITGET_API_KEY=your_api_key +BITGET_API_SECRET=your_api_secret +BITGET_PASSPHRASE=your_passphrase # 如果需要 +BITGET_USE_TESTNET=true # 初期使用测试网 + +# 实盘交易配置 +REAL_TRADING_ENABLED=false # 实盘交易总开关 +REAL_TRADING_MAX_POSITION=5000 # 单笔最大持仓 (USDT) +REAL_TRADING_MAX_TOTAL_RATIO=0.5 # 最大总仓位比例(账户的50%) +REAL_TRADING_DEFAULT_LEVERAGE=10 # 默认杠杆(低于模拟) +REAL_TRADING_RISK_PER_TRADE=0.02 # 每笔交易风险(2%) +``` + +### 阶段二:实盘交易服务开发 (2-3天) + +#### 2.1 创建 `BitgetTradingAPI` 类 + +**文件**: `backend/app/services/bitget_trading_api.py` + +```python +class BitgetTradingAPI: + """Bitget 实盘交易 API""" + + def __init__(self, api_key: str, api_secret: str, use_testnet: bool = True): + self.api_key = api_key + self.api_secret = api_secret + self.base_url = TESTNET_URL if use_testnet else PROD_URL + + def place_order(self, symbol: str, side: str, size: float, + price: float = None, order_type: str = 'limit') -> dict: + """下单""" + # POST /api/mix/v1/order/placeOrder + + def cancel_order(self, symbol: str, order_id: str) -> dict: + """撤单""" + # POST /api/mix/v1/order/cancelOrder + + def close_position(self, symbol: str, side: str, size: float) -> dict: + """平仓""" + # POST /api/mix/v1/order/closePosition + + def get_position(self, symbol: str = None) -> list: + """查询持仓""" + # GET /api/mix/v1/position/allPosition + + def get_balance(self) -> dict: + """查询余额""" + # GET /api/mix/v1/account/account + + def set_leverage(self, symbol: str, leverage: int) -> dict: + """设置杠杆""" + # POST /api/mix/v1/account/setLeverage +``` + +#### 2.2 创建 `RealTradingService` 类 + +**文件**: `backend/app/services/real_trading_service.py` + +**核心方法**: +```python +class RealTradingService: + """实盘交易服务 - 接口与 PaperTradingService 保持一致""" + + def create_order_from_signal(self, signal: Dict, current_price: float) -> Dict: + """从信号创建实盘订单""" + # 1. 风险检查(仓位、杠杆、余额) + # 2. 调用 BitgetTradingAPI.place_order() + # 3. 记录订单到数据库 + # 4. 发送通知 + + def check_price_triggers(self, symbol: str, price: float) -> List: + """检查止盈止损(从 Bitget 获取持仓状态)""" + # 1. 获取实际持仓状态 + # 2. 判断是否触发止盈止损 + # 3. 调用平仓 API + + def get_account_status(self) -> Dict: + """获取账户状态""" + # 调用 Bitget API 获取真实余额和持仓 +``` + +### 阶段三:集成与切换 (1-2天) + +#### 3.1 配置开关设计 + +```python +# config.py +class Settings(BaseSettings): + # 交易模式选择 + trading_mode: str = "paper" # "paper" 或 "real" + + # 模拟交易配置(保留) + paper_trading_enabled: bool = True + + # 实盘交易配置 + real_trading_enabled: bool = False + + # Bitget API 配置 + bitget_api_key: str = "" + bitget_api_secret: str = "" + bitget_use_testnet: bool = True +``` + +#### 3.2 服务切换逻辑 + +```python +# crypto_agent.py 或根据 trading_mode 动态选择 +if settings.trading_mode == "paper": + from app.services.paper_trading_service import get_paper_trading_service + trading_service = get_paper_trading_service() +else: + from app.services.real_trading_service import get_real_trading_service + trading_service = get_real_trading_service() +``` + +#### 3.3 修改的文件清单 + +| 文件 | 修改内容 | +|------|---------| +| `config.py` | 添加实盘交易配置项 | +| `crypto_agent.py` | 添加交易模式选择逻辑 | +| `main.py` | 价格监控适配实盘交易 | +| `api/paper_trading.py` | 添加实盘交易 API 端点 | + +### 阶段四:测试验证 (2-3天) + +#### 4.1 测试网测试 +- [ ] 下单功能测试 +- [ ] 撤单功能测试 +- [ ] 平仓功能测试 +- [ ] 持仓查询测试 +- [ ] 止盈止损触发测试 + +#### 4.2 小资金实盘测试 +- [ ] 使用小额资金(如 100 USDT) +- [ ] 执行 10-20 笔交易 +- [ ] 验证盈亏计算准确性 +- [ ] 验证通知及时性 + +#### 4.3 压力测试 +- [ ] 极端行情下系统稳定性 +- [ ] API 限频处理 +- [ ] 网络异常恢复 +- [ ] 订单状态同步 + +--- + +## 五、风险控制 + +### 5.1 技术风险 + +| 风险 | 影响 | 缓解措施 | +|------|------|---------| +| API 故障 | 无法交易 | 多重异常处理 + 飞书告警 | +| 网络延迟 | 滑点增大 | 限价单为主,设置合理价格偏差 | +| 订单状态不同步 | 重复开仓 | 定期同步持仓状态 | +| 限频被封 | 暂停交易 | 请求频率控制 + 重试机制 | + +### 5.2 交易风险 + +| 风险 | 影响 | 缓解措施 | +|------|------|---------| +| 策略失效 | 亏损 | 测试网充分验证 + 小资金试运行 | +| 极端行情 | 爆仓 | 严格止损 + 仓位控制 | +| 误操作 | 意外亏损 | 双重确认 + 实盘交易前测试 | +| API 泄露 | 资金风险 | IP 白名单 + 只读权限分离 | + +### 5.3 资金管理建议 + +```python +# 资金管理配置 +MAX_ACCOUNT_RATIO = 0.5 # 最大使用账户50%资金 +MAX_SINGLE_POSITION = 0.1 # 单笔最大10%仓位 +MAX_TOTAL_POSITION = 0.3 # 总持仓最大30% +DEFAULT_LEVERAGE = 10 # 实盘杠杆低于模拟(20x) +STOP_LOSS_PERCENT = 0.02 # 每笔最大亏损2% +``` + +--- + +## 六、监控与告警 + +### 6.1 必需监控指标 + +```python +# 实时监控 +- 账户余额变化 +- 持仓盈亏实时状态 +- 订单执行状态 +- API 调用成功率 +- 系统响应时间 +``` + +### 6.2 告警机制 + +```python +# 飞书告警场景 +- 订单执行失败 +- 止损/止盈触发 +- 账户余额异常变化 +- API 连接失败 +- 系统异常错误 +``` + +### 6.3 日志记录 + +```python +# 实盘交易必需日志 +- 每笔订单的完整生命周期 +- API 请求和响应记录 +- 价格监控日志 +- 错误和异常详情 +``` + +--- + +## 七、实施时间表 + +| 阶段 | 任务 | 预计时间 | 依赖 | +|------|------|---------|------| +| **阶段一** | 准备工作 | 1-2天 | - | +| **阶段二** | 实盘服务开发 | 2-3天 | 阶段一完成 | +| **阶段三** | 集成与切换 | 1-2天 | 阶段二完成 | +| **阶段四** | 测试验证 | 2-3天 | 阶段三完成 | +| **总计** | | **6-10天** | | + +--- + +## 八、关键决策点 + +### 8.1 必须解决的问题 + +1. ✅ **订单映射** + - 模拟交易订单 ↔ Bitget 实盘订单 ID + - 状态同步机制 + +2. ✅ **持仓同步** + - 定期从 Bitget 获取实际持仓 + - 与本地数据库保持一致 + +3. ✅ **止损策略** + - 模拟交易:本地价格轮询检查 + - 实盘交易:可使用 Bitget 条件单 API + +4. ✅ **错误处理** + - API 失败时的降级策略 + - 部分成交处理 + +### 8.2 可选优化 + +1. ⭕ **条件单 API** + - 使用 Bitget 条件单实现止损止盈 + - 减少本地轮询,降低 API 调用 + +2. ⭕ **WebSocket 推送** + - 使用 Bitget WebSocket 实时获取成交和持仓更新 + - 降低延迟 + +3. ⭕ **仓位管理优化** + - 根据账户余额动态调整仓位 + - 凯利公式资金管理 + +--- + +## 九、推荐实施顺序 + +### 第一步:最小化实盘功能 (3-4天) +- ✅ 实现 `BitgetTradingAPI` 基础下单/撤单/平仓 +- ✅ 实现 `RealTradingService` 核心功能 +- ✅ 测试网验证 + +### 第二步:完整集成 (2-3天) +- ✅ 添加配置开关 +- ✅ 修改现有调用逻辑 +- ✅ 数据库适配 + +### 第三步:生产验证 (持续) +- ✅ 小资金试运行 +- ✅ 监控告警完善 +- ✅ 逐步增加资金规模 + +--- + +## 十、总结与建议 + +### ✅ 可行性评估 + +| 方面 | 评估 | 说明 | +|------|------|------| +| **技术可行性** | ✅ 高 | Bitget API 完善,所有功能都支持 | +| **实施复杂度** | ⚠️ 中 | 需要处理订单同步、状态管理 | +| **风险可控性** | ✅ 高 | 测试网 + 小资金 + 双重确认 | +| **时间投入** | ⚠️ 中 | 6-10天完整实施 | + +### ⚠️ 重要提醒 + +1. **先在测试网充分验证** + - 所有功能都在测试网验证通过 + - 模拟各种异常情况 + +2. **小资金试运行** + - 初期使用最小可交易资金 + - 验证1-2周后再逐步增加 + +3. **保留模拟交易** + - 双模式并存,随时可切回模拟 + - 用于策略回测和新功能验证 + +4. **严格风控** + - 实盘杠杆低于模拟 + - 仓位控制更保守 + - 止损必须执行 + +### 📋 下一步行动 + +**如果你确认要实施实盘交易,建议按以下顺序进行**: + +1. **立即**: 申请 Bitget 测试网账户和 API 密钥 +2. **第1天**: 创建 `BitgetTradingAPI` 基础类 +3. **第2-3天**: 实现核心交易功能(下单、撤单、平仓) +4. **第4-5天**: 测试网验证所有功能 +5. **第6-7天**: 小资金实盘测试(100 USDT) +6. **验证稳定后**: 逐步增加资金规模 + +--- + +**方案制定完成,等待确认后开始实施** diff --git a/docs/REAL_TRADING_SETUP_GUIDE.md b/docs/REAL_TRADING_SETUP_GUIDE.md new file mode 100644 index 0000000..d62a9ba --- /dev/null +++ b/docs/REAL_TRADING_SETUP_GUIDE.md @@ -0,0 +1,258 @@ +# Bitget 实盘交易配置指南 + +## 一、获取 Bitget API 密钥 + +### 1.1 注册 Bitget 账户 + +1. 访问 [Bitget 官网](https://www.bitget.com/) +2. 注册账户并完成 KYC 认证 + +### 1.2 开通合约交易 + +1. 登录后进入「合约」页面 +2. 完成合约交易风险测评 +3. 开通 U 本位合约交易 + +### 1.3 创建 API 密钥 + +1. 进入「账户」→「API 管理」 +2. 点击「创建 API」 +3. 设置以下内容: + - **备注名**: Stock Agent + - **权限**: + - ✅ 读取 + - ✅ 提现 (可关闭) + - ✅ 交易 (必需) + - **IP 白名单**: 添加你的服务器 IP(可选但推荐) + - **Google 验证**: 完成绑定 + +4. 创建后获得: + - **API Key** + - **API Secret** + - **Passphrase** (如果设置了) + +--- + +## 二、配置项目 + +### 2.1 编辑 `.env` 文件 + +在项目根目录的 `.env` 文件中添加以下配置: + +```bash +# ============================================================================ +# Bitget API 配置 +# ============================================================================ +BITGET_API_KEY=你的API_Key +BITGET_API_SECRET=你的API_Secret +BITGET_PASSPHRASE=你的Passphrase + +# 使用测试网(强烈建议先在测试网测试!) +BITGET_USE_TESTNET=true + +# 实盘交易总开关(false 时仅模拟交易生效) +REAL_TRADING_ENABLED=false + +# 风险控制参数 +REAL_TRADING_MAX_SINGLE_POSITION=1000 +REAL_TRADING_MAX_TOTAL_RATIO=0.5 +REAL_TRADING_DEFAULT_LEVERAGE=10 +REAL_TRADING_RISK_PER_TRADE=0.02 +REAL_TRADING_MAX_ORDERS=5 +``` + +### 2.2 配置项说明 + +| 配置项 | 说明 | 推荐值 | +|--------|------|--------| +| `BITGET_API_KEY` | Bitget API Key | 从 Bitget 获取 | +| `BITGET_API_SECRET` | Bitget API Secret | 从 Bitget 获取 | +| `BITGET_PASSPHRASE` | API Passphrase | 从 Bitget 获取(如果需要) | +| `BITGET_USE_TESTNET` | 是否使用测试网 | `true`(测试网) | +| `REAL_TRADING_ENABLED` | 实盘交易开关 | `false`(测试前保持 false) | +| `REAL_TRADING_MAX_SINGLE_POSITION` | 单笔最大持仓 | `1000` USDT | +| `REAL_TRADING_DEFAULT_LEVERAGE` | 默认杠杆 | `10`x(低于模拟的20x) | +| `REAL_TRADING_RISK_PER_TRADE` | 每笔风险 | `0.02` (2%) | + +--- + +## 三、测试网测试 + +### 3.1 Bitget 测试网 + +**测试网地址**: https://testnet.bitget.com/ + +**特点**: +- 与生产网相同的 API 接口 +- 虚拟资金,无风险 +- 适合测试所有功能 + +### 3.2 配置测试网 + +确保 `.env` 中设置: +```bash +BITGET_USE_TESTNET=true +``` + +### 3.3 运行测试脚本 + +```bash +# 激活虚拟环境 +source backend/venv/bin/activate + +# 运行测试脚本 +python scripts/test_real_trading.py +``` + +### 3.4 测试内容 + +测试脚本会依次执行: +1. ✅ API 连接测试 +2. ✅ 查询账户余额 +3. ✅ 查询当前持仓 +4. ⚠️ 下单测试(已注释,需手动取消注释) + +--- + +## 四、生产网注意事项 + +### 4.1 切换到生产网 + +⚠️ **警告**: 切换到生产网后,所有交易都是真实资金! + +1. 修改 `.env`: + ```bash + BITGET_USE_TESTNET=false + ``` + +2. **强烈建议**: + - 先在测试网充分测试所有功能 + - 使用最小资金进行初期测试 + - 设置严格的风险控制参数 + - 保留模拟交易作为回测 + +### 4.2 风险控制建议 + +```bash +# 保守配置(推荐初期使用) +REAL_TRADING_MAX_SINGLE_POSITION=100 # 单笔最大 100 USDT +REAL_TRADING_DEFAULT_LEVERAGE=5 # 杠杆降低到 5x +REAL_TRADING_RISK_PER_TRADE=0.01 # 每笔风险 1% +REAL_TRADING_MAX_ORDERS=2 # 最多 2 个持仓 +``` + +### 4.3 实盘交易检查清单 + +- [ ] API Key 配置正确 +- [ ] 测试网所有功能验证通过 +- [ ] 风险参数已设置 +- [ ] 小资金试运行(100-1000 USDT) +- [ ] 监控告警已配置 +- [ ] 紧急撤单方案已准备 + +--- + +## 五、功能验证 + +### 5.1 API 连接验证 + +```bash +python scripts/test_real_trading.py +``` + +**期望输出**: +``` +✅ API 连接成功,USDT 余额: xxxxx +``` + +### 5.2 查询功能验证 + +测试脚本会自动验证: +- ✅ 账户余额查询 +- ✅ 持仓查询 +- ✅ 订单状态查询 + +### 5.3 下单功能验证(测试网) + +在测试网环境下,可以取消注释测试脚本中的下单代码进行真实下单测试: + +```python +# 在 test_real_trading.py 中找到此部分并取消注释 +result = api.place_order( + symbol=symbol, + side='open_long', + order_type='market', + size=test_size +) +``` + +--- + +## 六、故障排查 + +### 6.1 API 连接失败 + +**错误**: `API 连接失败` + +**解决**: +1. 检查 API Key 和 Secret 是否正确 +2. 检查是否在正确的网络(测试网/生产网) +3. 检查 IP 白名单配置 + +### 6.2 签名验证失败 + +**错误**: `API 签名验证失败` + +**解决**: +1. 检查系统时间是否准确 +2. 检查 API Secret 是否完整复制 +3. 检查 passphrase 是否正确 + +### 6.3 权限不足 + +**错误**: `权限不足` + +**解决**: +1. 确认 API Key 已开通「交易」权限 +2. 重新创建 API Key 并勾选所有必需权限 + +--- + +## 七、安全最佳实践 + +### 7.1 API Key 安全 + +- ✅ 不要将 API Key 提交到 Git 仓库 +- ✅ 使用 `.env` 文件并添加到 `.gitignore` +- ✅ 定期轮换 API Key +- ✅ 设置 IP 白名单 + +### 7.2 资金安全 + +- ✅ 使用测试网充分测试 +- ✅ 从小资金开始 +- ✅ 设置合理的止损 +- ✅ 不要使用全部资金 + +### 7.3 系统安全 + +- ✅ 保留实盘交易日志 +- ✅ 配置飞书/Telegram 告警 +- ✅ 定期检查持仓状态 +- ✅ 设置紧急停机方案 + +--- + +## 八、下一步 + +配置完成后: + +1. ✅ 运行测试脚本验证连接 +2. ✅ 在测试网测试所有功能 +3. ✅ 小资金生产网测试(可选) +4. ✅ 配置监控告警 +5. ✅ 逐步增加资金规模 + +--- + +**祝交易顺利!** 🚀 diff --git a/scripts/test_bitget_sdk.py b/scripts/test_bitget_sdk.py new file mode 100644 index 0000000..262de27 --- /dev/null +++ b/scripts/test_bitget_sdk.py @@ -0,0 +1,170 @@ +""" +测试 Bitget CCXT SDK 实现 + +在运行之前,请确保: +1. 已安装 ccxt: pip install ccxt +2. 已在 .env 文件中配置 Bitget API 密钥 +3. 使用测试网进行测试(BITGET_USE_TESTNET=true) +""" +import sys +import os +from pathlib import Path + +# 添加项目路径 +script_dir = Path(__file__).parent +project_root = script_dir.parent +backend_dir = project_root / "backend" + +sys.path.insert(0, str(backend_dir)) +sys.path.insert(0, str(project_root)) + +# 设置工作目录 +os.chdir(project_root) + +from app.services.bitget_trading_api_sdk import BitgetTradingAPI +from app.config import get_settings +from app.utils.logger import logger + + +def test_connection(): + """测试 API 连接""" + logger.info("=" * 60) + logger.info("测试 1: API 连接测试") + logger.info("=" * 60) + + settings = get_settings() + + api = BitgetTradingAPI( + api_key=settings.bitget_api_key, + api_secret=settings.bitget_api_secret, + passphrase=settings.bitget_passphrase, + use_testnet=settings.bitget_use_testnet + ) + + result = api.test_connection() + logger.info(f"连接测试结果: {'✅ 成功' if result else '❌ 失败'}") + + return api if result else None + + +def test_get_balance(api): + """测试查询余额""" + logger.info("\n" + "=" * 60) + logger.info("测试 2: 查询账户余额") + logger.info("=" * 60) + + balance = api.get_balance() + + if balance: + logger.info(f"✅ 余额查询成功") + for currency, info in balance.items(): + available = info.get('available', '0') + frozen = info.get('frozen', '0') + logger.info(f" {currency}: 可用={available}, 冻结={frozen}") + else: + logger.error("❌ 余额查询失败") + + +def test_get_position(api): + """测试查询持仓""" + logger.info("\n" + "=" * 60) + logger.info("测试 3: 查询持仓") + logger.info("=" * 60) + + positions = api.get_position() + + if positions: + logger.info(f"✅ 持仓查询成功,共 {len(positions)} 个持仓") + for pos in positions: + symbol = pos.get('symbol', 'N/A') + size = pos.get('contracts', 0) + side = pos.get('side', 'N/A') + unrealized_pnl = pos.get('unrealizedPnl', 0) + logger.info(f" {symbol} {side} {size}张 (未实现盈亏: {unrealized_pnl})") + else: + logger.info(" 当前无持仓") + + +def test_get_open_orders(api): + """测试查询挂单""" + logger.info("\n" + "=" * 60) + logger.info("测试 4: 查询当前挂单") + logger.info("=" * 60) + + orders = api.get_open_orders() + + if orders: + logger.info(f"✅ 挂单查询成功,共 {len(orders)} 个挂单") + for order in orders[:5]: # 只显示前 5 个 + symbol = order.get('symbol', 'N/A') + side = order.get('side', 'N/A') + price = order.get('price', 'N/A') + amount = order.get('amount', 'N/A') + logger.info(f" {symbol} {side} @ {price} x {amount}") + else: + logger.info(" 当前无挂单") + + +def test_market_ticker(api, symbol='BTC/USDT:USDT'): + """测试获取市场行情""" + logger.info("\n" + "=" * 60) + logger.info(f"测试 5: 获取 {symbol} 市场行情") + logger.info("=" * 60) + + try: + ticker = api.exchange.fetch_ticker(symbol) + if ticker: + logger.info(f"✅ 行情获取成功") + logger.info(f" 交易对: {symbol}") + logger.info(f" 最新价: {ticker.get('last', 'N/A')}") + logger.info(f" 24h涨跌: {ticker.get('percentage', 'N/A')}%") + logger.info(f" 24h成交量: {ticker.get('baseVolume', 'N/A')}") + except Exception as e: + logger.error(f"❌ 行情获取失败: {e}") + + +def main(): + """主测试函数""" + logger.info("🚀 开始测试 Bitget CCXT SDK 实现") + + # 检查配置 + settings = get_settings() + if not settings.bitget_api_key or not settings.bitget_api_secret: + logger.error("❌ 未配置 Bitget API 密钥,请在 .env 文件中设置:") + logger.error(" BITGET_API_KEY=your_api_key") + logger.error(" BITGET_API_SECRET=your_api_secret") + return + + # 测试连接 + api = test_connection() + if not api: + logger.error("❌ API 连接失败,请检查:") + logger.error(" 1. API 密钥是否正确") + logger.error(" 2. 网络连接是否正常") + logger.error(" 3. 测试网是否可用") + return + + # 运行测试 + try: + test_get_balance(api) + test_get_position(api) + test_get_open_orders(api) + test_market_ticker(api) + + logger.info("\n" + "=" * 60) + logger.info("✅ 所有测试完成") + logger.info("=" * 60) + + except Exception as e: + logger.error(f"❌ 测试过程中出错: {e}") + import traceback + traceback.print_exc() + + finally: + # 关闭连接 + api.close() + logger.info("\n🔌 API 连接已关闭") + + +if __name__ == "__main__": + main() diff --git a/scripts/test_bitget_v2.py b/scripts/test_bitget_v2.py new file mode 100644 index 0000000..ebf8c96 --- /dev/null +++ b/scripts/test_bitget_v2.py @@ -0,0 +1,133 @@ +""" +测试 Bitget V2 API +""" +import sys +sys.path.insert(0, '/Users/aaron/source_code/Stock_Agent') + +from backend.app.services.bitget_trading_api import get_bitget_trading_api +from backend.app.utils.logger import logger + + +def test_bitget_v2(): + """测试 Bitget V2 API 各个功能""" + + print("\n" + "="*60) + print("测试 Bitget V2 API") + print("="*60) + + # 获取 API 实例 + api = get_bitget_trading_api() + + if not api: + print("❌ 无法初始化 Bitget API(请检查 .env 配置)") + return False + + print(f"\n📡 API 端点: {api.base_url}") + print(f"🔑 API Key: {api.api_key[:10]}...{api.api_key[-4:]}") + + # 测试 1: 连接测试和账户余额 + print("\n" + "-"*60) + print("测试 1: 账户余额查询") + print("-"*60) + + try: + balance = api.get_balance() + if balance: + usdt = balance.get('USDT', {}) + print(f"✅ USDT 可用余额: {usdt.get('available', '0')}") + print(f" 冻结: {usdt.get('frozen', '0')}") + print(f" 锁定: {usdt.get('locked', '0')}") + else: + print("⚠️ 余额查询返回空数据") + except Exception as e: + print(f"❌ 余额查询失败: {e}") + return False + + # 测试 2: 账户信息 + print("\n" + "-"*60) + print("测试 2: 账户信息查询") + print("-"*60) + + try: + account_info = api.get_account_info() + if account_info: + print(f"✅ 账户信息: {list(account_info.keys())}") + else: + print("⚠️ 账户信息查询返回空数据") + except Exception as e: + print(f"❌ 账户信息查询失败: {e}") + + # 测试 3: 查询持仓 + print("\n" + "-"*60) + print("测试 3: 持仓查询") + print("-"*60) + + try: + positions = api.get_position() + if positions: + print(f"✅ 当前持仓数: {len(positions)}") + for pos in positions: + symbol = pos.get('symbol', 'N/A') + hold_side = pos.get('holdSide', 'N/A') + total = pos.get('total', '0') + leverage = pos.get('leverage', 'N/A') + print(f" {symbol} {hold_side}: {total} 张 (杠杆 {leverage}x)") + else: + print("✅ 当前无持仓") + except Exception as e: + print(f"❌ 持仓查询失败: {e}") + return False + + # 测试 4: 查询当前挂单 + print("\n" + "-"*60) + print("测试 4: 当前挂单查询") + print("-"*60) + + try: + open_orders = api.get_open_orders() + if open_orders: + print(f"✅ 当前挂单数: {len(open_orders)}") + for order in open_orders[:5]: # 只显示前5个 + symbol = order.get('symbol', 'N/A') + side = order.get('side', 'N/A') + size = order.get('size', '0') + price = order.get('price', '市价') + print(f" {symbol} {side} {size} @ {price}") + if len(open_orders) > 5: + print(f" ... 还有 {len(open_orders) - 5} 个挂单") + else: + print("✅ 当前无挂单") + except Exception as e: + print(f"❌ 挂单查询失败: {e}") + return False + + # 测试 5: 查询历史订单 + print("\n" + "-"*60) + print("测试 5: 历史订单查询 (BTCUSDT)") + print("-"*60) + + try: + history_orders = api.get_history_orders('BTCUSDT', limit=10) + if history_orders: + print(f"✅ 最近 {len(history_orders)} 条历史订单") + for order in history_orders[:3]: # 只显示前3个 + order_id = order.get('orderId', 'N/A') + side = order.get('side', 'N/A') + order_type = order.get('orderType', 'N/A') + state = order.get('state', 'N/A') + print(f" 订单 {order_id}: {side} {order_type} - {state}") + else: + print("✅ 暂无历史订单") + except Exception as e: + print(f"❌ 历史订单查询失败: {e}") + + print("\n" + "="*60) + print("✅ 所有核心功能测试完成!V2 API 工作正常") + print("="*60 + "\n") + + return True + + +if __name__ == "__main__": + success = test_bitget_v2() + sys.exit(0 if success else 1) diff --git a/scripts/test_real_trading.py b/scripts/test_real_trading.py new file mode 100644 index 0000000..839ca47 --- /dev/null +++ b/scripts/test_real_trading.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +""" +实盘交易功能测试脚本 + +测试 Bitget 实盘交易 API 的各项功能 +⚠️ 注意: 此脚本会进行真实交易,请确保在测试网环境运行! +""" +import sys +import os +from pathlib import Path + +# 添加项目路径 +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root / "backend")) + +from app.config import get_settings +from app.services.bitget_trading_api import BitgetTradingAPI, get_bitget_trading_api +from app.services.real_trading_service import get_real_trading_service + + +def print_section(title: str): + """打印分节标题""" + print("\n" + "=" * 80) + print(f" {title}") + print("=" * 80) + + +def test_api_connection(): + """测试 API 连接""" + print_section("1. 测试 API 连接") + + settings = get_settings() + + print(f"\n配置信息:") + print(f" API Key: {settings.bitget_api_key[:10]}..." if settings.bitget_api_key else " API Key: 未配置") + print(f" 测试网: {'是 ✅' if settings.bitget_use_testnet else '否 ⚠️'}") + + if not settings.bitget_api_key or not settings.bitget_api_secret: + print("\n❌ API Key 未配置,请在 .env 文件中设置:") + print(" BITGET_API_KEY=your_api_key") + print(" BITGET_API_SECRET=your_api_secret") + print(" BITGET_PASSPHRASE=your_passphrase") + print(" BITGET_USE_TESTNET=true") + return False + + # 创建 API 实例 + api = BitgetTradingAPI( + api_key=settings.bitget_api_key, + api_secret=settings.bitget_api_secret, + passphrase=settings.bitget_passphrase, + use_testnet=settings.bitget_use_testnet + ) + + # 测试连接 + if api.test_connection(): + print("\n✅ API 连接成功!") + return True + else: + print("\n❌ API 连接失败!") + print(" 请检查:") + print(" 1. API Key 和 Secret 是否正确") + print(" 2. 是否在测试网环境") + print(" 3. API 权限是否正确") + return False + + +def test_query_account(): + """测试查询账户""" + print_section("2. 测试查询账户") + + api = get_bitget_trading_api() + if not api: + print("\n❌ 交易 API 未初始化") + return False + + # 查询余额 + print("\n查询账户余额...") + balance = api.get_balance() + + if balance: + usdt_info = balance.get('USDT', {}) + print(f"\n✅ USDT 余额:") + print(f" 可用: ${float(usdt_info.get('available', 0)):,.2f}") + print(f" 冻结: ${float(usdt_info.get('frozen', 0)):,.2f}") + print(f" 锁定: ${float(usdt_info.get('locked', 0)):,.2f}") + return True + else: + print("\n❌ 查询余额失败") + return False + + +def test_query_position(): + """测试查询持仓""" + print_section("3. 测试查询持仓") + + api = get_bitget_trading_api() + if not api: + print("\n❌ 交易 API 未初始化") + return False + + # 查询持仓 + print("\n查询当前持仓...") + positions = api.get_position() + + print(f"\n✅ 当前持仓数量: {len(positions)}") + + if positions: + for pos in positions: + symbol = pos.get('symbol', 'N/A') + total = pos.get('total', 0) + available = pos.get('available', 0) + hold_side = pos.get('holdSide', 'N/A') + avg_price = pos.get('averageOpenPrice', 0) + + print(f"\n {symbol} ({hold_side})") + print(f" 持仓: {total}") + print(f" 可用: {available}") + print(f" 均价: ${avg_price}") + else: + print(" 无持仓") + + return True + + +def test_place_small_order(): + """ + 测试下单(小仓位测试) + + ⚠️ 警告: 此函数会进行真实交易! + 建议在测试网运行,并使用最小仓位 + """ + print_section("4. 测试下单 (⚠️ 实盘交易)") + + # 确认是否要测试 + print("\n⚠️ 警告: 此测试会进行真实下单!") + print(" 建议: 1. 确保在测试网环境") + print(" 2. 使用最小仓位测试") + print(" 3. 先手动撤销测试订单") + + api = get_bitget_trading_api() + if not api: + print("\n❌ 交易 API 未初始化") + return False + + # 使用 BTCUSDT 进行测试 + symbol = "BTCUSDT" + + # 查询当前价格 + from app.services.bitget_service import bitget_service + current_price = bitget_service.get_current_price(symbol) + + if not current_price: + print(f"\n❌ 无法获取 {symbol} 当前价格") + return False + + print(f"\n{symbol} 当前价格: ${current_price:,.2f}") + + # 使用最小数量(1张,通常约等于1 USDT) + test_size = 1 + + # 使用市价单测试(快速成交) + print(f"\n准备下测试单:") + print(f" 交易对: {symbol}") + print(f" 方向: 开多 (open_long)") + print(f" 类型: 市价单") + print(f" 数量: {test_size} 张 (约 ${test_size})") + + # 取消注释以实际下单 + print("\n❗ 下单功能已禁用,避免意外交易") + print(" 如需测试,请手动取消下面的注释并确认:") + print("") + + """ + # 实际下单代码(已注释,需手动取消注释) + result = api.place_order( + symbol=symbol, + side='open_long', + order_type='market', + size=test_size + ) + + if result: + order_id = result.get('orderId') + print(f"\n✅ 下单成功!") + print(f" 订单ID: {order_id}") + print(f" 请在交易所检查订单状态") + + # 询问是否立即撤单 + print("\n是否立即撤单?(测试用)") + # input("按回车键撤单...") + + # 撤单 + if api.cancel_order(symbol, order_id=order_id): + print(f"\n✅ 撤单成功") + else: + print(f"\n❌ 撤单失败,请手动在交易所撤单") + + return True + else: + print(f"\n❌ 下单失败") + return False + """ + + return False + + +def test_real_trading_service(): + """测试实盘交易服务""" + print_section("5. 测试实盘交易服务") + + service = get_real_trading_service() + + if not service: + print("\n❌ 实盘交易服务未启用") + print(" 请在 .env 中设置:") + print(" REAL_TRADING_ENABLED=true") + return False + + # 查询账户状态 + print("\n查询账户状态...") + account = service.get_account_status() + + print(f"\n✅ 账户状态:") + print(f" 总余额: ${account['current_balance']:,.2f}") + print(f" 可用: ${account['available']:,.2f}") + print(f" 已用: ${account['used_margin']:,.2f}") + print(f" 持仓: ${account['total_position_value']:,.2f}") + + # 同步持仓 + print("\n同步交易所持仓...") + positions = service.sync_positions_from_exchange() + + print(f"\n✅ 交易所持仓: {len(positions)} 个") + + return True + + +def main(): + """主函数""" + print("\n" + "🚀" * 40) + print("\nBitget 实盘交易功能测试") + print(f"测试时间: {datetime.now()}") + + settings = get_settings() + + print("\n⚠️ 重要提醒:") + print(f" 当前环境: {'测试网 ✅' if settings.bitget_use_testnet else '生产网 ⚠️'}") + + try: + # 1. 测试 API 连接 + if not test_api_connection(): + print("\n❌ API 连接测试失败,请检查配置") + return + + # 2. 测试查询账户 + if not test_query_account(): + print("\n❌ 查询账户测试失败") + return + + # 3. 测试查询持仓 + if not test_query_position(): + print("\n❌ 查询持仓测试失败") + return + + # 4. 测试下单(需要手动取消注释) + # test_place_small_order() + + # 5. 测试实盘交易服务 + if not test_real_trading_service(): + print("\n⚠️ 实盘交易服务未启用(可忽略)") + + print("\n" + "=" * 80) + print(" ✅ 所有测试通过!") + print("=" * 80) + print("\n实盘交易功能已就绪!") + + except Exception as e: + print(f"\n❌ 测试出错: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + from datetime import datetime + main()