diff --git a/backend/app/api/paper_trading.py b/backend/app/api/paper_trading.py index 8df622e..6eaee36 100644 --- a/backend/app/api/paper_trading.py +++ b/backend/app/api/paper_trading.py @@ -162,6 +162,26 @@ async def get_statistics( raise HTTPException(status_code=500, detail=str(e)) +@router.get("/account") +async def get_account_status(): + """ + 获取账户状态 + + 返回账户余额、已用保证金、可用保证金等信息 + """ + try: + service = get_paper_trading_service() + account = service.get_account_status() + + return { + "success": True, + "account": account + } + except Exception as e: + logger.error(f"获取账户状态失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + @router.get("/statistics/by-grade") async def get_statistics_by_grade(): """按信号等级获取统计""" diff --git a/backend/app/config.py b/backend/app/config.py index 4100c33..5dcfc4c 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -112,6 +112,11 @@ class Settings(BaseSettings): # 模拟交易配置 paper_trading_enabled: bool = True # 是否启用模拟交易 + paper_trading_initial_balance: float = 10000 # 初始本金 (USDT) + paper_trading_leverage: int = 10 # 杠杆倍数 + paper_trading_margin_per_order: float = 1000 # 每单保证金 (USDT) + paper_trading_max_orders: int = 10 # 最大持仓+挂单总数 + # 废弃的配置(保留兼容性) paper_trading_position_a: float = 1000 # A级信号仓位 (USDT) paper_trading_position_b: float = 500 # B级信号仓位 (USDT) paper_trading_position_c: float = 200 # C级信号仓位 (USDT) diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index f628d6f..00cc992 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -11,15 +11,6 @@ from app.config import get_settings from app.utils.logger import logger -# 仓位大小配置 -POSITION_SIZE = { - 'A': 1000, # A级信号 1000 USDT - 'B': 500, # B级信号 500 USDT - 'C': 200, # C级信号 200 USDT - 'D': 0 # D级信号不开仓 -} - - class PaperTradingService: """模拟交易服务""" @@ -28,6 +19,12 @@ class PaperTradingService: self.settings = get_settings() self.active_orders: Dict[str, PaperOrder] = {} # 内存缓存活跃订单 + # 合约交易配置 + self.initial_balance = self.settings.paper_trading_initial_balance # 初始本金 + self.leverage = self.settings.paper_trading_leverage # 杠杆倍数 + self.margin_per_order = self.settings.paper_trading_margin_per_order # 每单保证金 + self.max_orders = self.settings.paper_trading_max_orders # 最大订单数 + # 确保表已创建 self._ensure_table_exists() @@ -89,16 +86,17 @@ class PaperTradingService: entry_price = signal.get('entry_price') or signal.get('price', 0) # === 限制检查 === - # 1. 同一交易对同一方向最多 3 个订单 + # 1. 检查总订单数(持仓+挂单)是否超过最大限制 + total_orders = len(self.active_orders) + if total_orders >= self.max_orders: + logger.info(f"订单限制: 已达到最大订单数 {self.max_orders},跳过") + return None + + # 2. 检查是否有接近的挂单(价格差距 < 1%) same_direction_orders = [ order for order in self.active_orders.values() if order.symbol == symbol and order.side == side ] - if len(same_direction_orders) >= 3: - logger.info(f"订单限制: {symbol} {side.value} 方向已有 {len(same_direction_orders)} 个订单,跳过") - return None - - # 2. 检查是否有接近的挂单(价格差距 < 1%) pending_orders = [ order for order in same_direction_orders if order.status == OrderStatus.PENDING @@ -115,10 +113,10 @@ class PaperTradingService: logger.info(f"D级信号不开仓: {signal.get('symbol')}") return None - # 确定仓位大小 - quantity = POSITION_SIZE.get(grade, 0) - if quantity == 0: - return None + # 固定使用保证金(不再根据等级区分) + margin = self.margin_per_order # 每单固定 1000 USDT 保证金 + position_value = margin * self.leverage # 持仓价值 = 保证金 × 杠杆 + quantity = position_value # 订单数量(以 USDT 计价) # 确定入场类型 entry_type_str = signal.get('entry_type', 'market') @@ -170,7 +168,8 @@ class PaperTradingService: entry_type_text = "现价" if entry_type == EntryType.MARKET else "挂单" status_text = "已开仓" if status == OrderStatus.OPEN else "等待触发" - logger.info(f"创建模拟订单: {order_id} | {symbol} {side.value} [{entry_type_text}] @ ${entry_price:,.2f} | {status_text} | 仓位: ${quantity}") + logger.info(f"创建模拟订单: {order_id} | {symbol} {side.value} [{entry_type_text}] @ ${entry_price:,.2f} | {status_text}") + logger.info(f" 保证金: ${margin:,.0f} | 杠杆: {self.leverage}x | 持仓价值: ${position_value:,.0f} | 当前订单数: {len(self.active_orders)}/{self.max_orders}") return order except Exception as e: @@ -574,6 +573,56 @@ class PaperTradingService: 'by_symbol': {} } + def get_account_status(self) -> Dict[str, Any]: + """ + 获取账户状态 + + Returns: + 账户状态信息,包括余额、已用保证金、可用保证金等 + """ + # 计算已用保证金(每个活跃订单占用固定保证金) + active_count = len(self.active_orders) + used_margin = active_count * self.margin_per_order + + # 计算已实现盈亏(从历史订单) + db = db_service.get_session() + try: + closed_orders = db.query(PaperOrder).filter( + PaperOrder.status.in_([ + OrderStatus.CLOSED_TP, + OrderStatus.CLOSED_SL, + OrderStatus.CLOSED_MANUAL + ]) + ).all() + + realized_pnl = sum(o.pnl_amount for o in closed_orders) + finally: + db.close() + + # 计算当前余额 + current_balance = self.initial_balance + realized_pnl + + # 计算可用保证金 + available_margin = current_balance - used_margin + + # 计算可开仓数 + available_orders = self.max_orders - active_count + + return { + 'initial_balance': self.initial_balance, + 'realized_pnl': round(realized_pnl, 2), + 'current_balance': round(current_balance, 2), + 'used_margin': round(used_margin, 2), + 'available_margin': round(available_margin, 2), + 'leverage': self.leverage, + 'margin_per_order': self.margin_per_order, + 'active_orders': active_count, + 'max_orders': self.max_orders, + 'available_orders': available_orders, + 'total_position_value': round(used_margin * self.leverage, 2), + 'margin_ratio': round((used_margin / current_balance * 100), 2) if current_balance > 0 else 0 + } + def _calculate_grade_statistics(self, orders: List[PaperOrder]) -> Dict[str, Any]: """按信号等级统计""" result = {} diff --git a/frontend/paper-trading.html b/frontend/paper-trading.html index 88570e8..8065052 100644 --- a/frontend/paper-trading.html +++ b/frontend/paper-trading.html @@ -517,6 +517,46 @@