diff --git a/backend/app/api/real_trading.py b/backend/app/api/real_trading.py index 7f66df9..737b111 100644 --- a/backend/app/api/real_trading.py +++ b/backend/app/api/real_trading.py @@ -340,6 +340,7 @@ async def get_service_status(): if service: account = service.get_account_status() status["account"] = account + status["auto_trading_enabled"] = service.get_auto_trading_status() return { "success": True, @@ -348,3 +349,60 @@ async def get_service_status(): except Exception as e: logger.error(f"获取实盘交易服务状态失败: {e}") raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/auto-trading") +async def set_auto_trading(enabled: bool = Query(..., description="是否启用自动交易")): + """ + 设置实盘自动交易开关 + + Args: + enabled: true=启用自动交易,false=禁用自动交易 + """ + try: + service = get_real_trading_service() + + if not service: + raise HTTPException(status_code=404, detail="实盘交易服务未初始化,请检查 API 配置") + + success = service.set_auto_trading(enabled) + + if success: + status_text = "启用" if enabled else "禁用" + return { + "success": True, + "message": f"实盘自动交易已{status_text}", + "auto_trading_enabled": enabled + } + else: + raise HTTPException(status_code=500, detail="设置自动交易失败") + + except HTTPException: + raise + except Exception as e: + logger.error(f"设置自动交易失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/auto-trading") +async def get_auto_trading_status(): + """获取实盘自动交易状态""" + try: + service = get_real_trading_service() + + if not service: + return { + "success": False, + "message": "实盘交易服务未初始化", + "auto_trading_enabled": False + } + + enabled = service.get_auto_trading_status() + + return { + "success": True, + "auto_trading_enabled": enabled + } + except Exception as e: + logger.error(f"获取自动交易状态失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index 32982cd..44fcf81 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -43,12 +43,16 @@ class CryptoAgent: self.llm_analyzer = LLMSignalAnalyzer() self.signal_db = get_signal_db_service() # 信号数据库服务 - # 模拟交易服务 - self.paper_trading_enabled = self.settings.paper_trading_enabled - if self.paper_trading_enabled: - self.paper_trading = get_paper_trading_service() - else: - self.paper_trading = None + # 模拟交易服务(始终启用) + self.paper_trading = get_paper_trading_service() + + # 实盘交易服务(如果配置了 API) + self.real_trading = None + try: + from app.services.real_trading_service import get_real_trading_service + self.real_trading = get_real_trading_service() + except Exception as e: + logger.warning(f"实盘交易服务初始化失败: {e}") # 状态管理 self.last_signals: Dict[str, Dict[str, Any]] = {} @@ -75,8 +79,13 @@ class CryptoAgent: }) logger.info(f"加密货币智能体初始化完成(LLM 驱动),监控交易对: {self.symbols}") - if self.paper_trading_enabled: - logger.info(f"模拟交易已启用") + logger.info(f"模拟交易: 始终启用") + + if self.real_trading: + auto_status = "启用" if self.real_trading.get_auto_trading_status() else "禁用" + logger.info(f"实盘交易: 已配置 (自动交易: {auto_status})") + else: + logger.info(f"实盘交易: 未配置") def _on_price_update(self, symbol: str, price: float): """处理实时价格更新(用于模拟交易)""" @@ -495,8 +504,8 @@ class CryptoAgent: self.last_signals[symbol] = best_signal self.signal_cooldown[symbol] = datetime.now() - # 5. 创建模拟订单 - if self.paper_trading_enabled and self.paper_trading: + # 5. 创建模拟订单(始终执行) + if self.paper_trading: grade = best_signal.get('grade', 'D') position_size = best_signal.get('position_size', 'light') if grade != 'D': @@ -513,6 +522,24 @@ class CryptoAgent: order = result.get('order') if order: logger.info(f" 📝 已创建模拟订单: {order.order_id} | 仓位: {position_size}") + + # 6. 创建实盘订单(如果启用了自动交易) + if self.real_trading and self.real_trading.get_auto_trading_status(): + grade = best_signal.get('grade', 'D') + position_size = best_signal.get('position_size', 'light') + if grade != 'D': + # 转换信号格式以兼容 real_trading + real_signal = self._convert_to_real_signal(symbol, best_signal, current_price) + result = self.real_trading.create_order_from_signal(real_signal, current_price) + + if result.get('success'): + logger.info(f" 💰 已创建实盘订单: {result.get('order_id')} | 仓位: {position_size} | 数量: ${result.get('quantity', 0):.2f}") + + # 发送实盘订单成交通知 + await self._notify_real_order_created(symbol, best_signal, result) + elif not result.get('skipped'): + # 只有非跳过的情况才记录错误 + logger.warning(f" ⚠️ 实盘订单创建失败: {result.get('message')}") else: if best_signal: logger.info(f"\n⏸️ 信号冷却中或置信度不足,不发送通知") @@ -698,6 +725,62 @@ class CryptoAgent: except Exception as e: logger.error(f"持仓回顾失败: {e}", exc_info=True) + def _convert_to_real_signal(self, symbol: str, signal: Dict[str, Any], + current_price: float) -> Dict[str, Any]: + """转换 LLM 信号格式为实盘交易格式""" + signal_type = signal.get('type', 'medium_term') + type_map = {'short_term': 'short_term', 'medium_term': 'swing', 'long_term': 'swing'} + + # 获取入场类型和入场价 + entry_type = signal.get('entry_type', 'market') + entry_price = signal.get('entry_price', current_price) + + # 映射 action: buy -> long, sell -> short + action = signal.get('action', 'hold') + side_map = {'buy': 'long', 'sell': 'short'} + + return { + 'symbol': symbol, + 'side': side_map.get(action, 'long'), + 'entry_type': entry_type, + 'entry_price': entry_price, + 'stop_loss': signal.get('stop_loss', 0), + 'take_profit': signal.get('take_profit', 0), + 'confidence': signal.get('confidence', 0), + 'grade': signal.get('grade', 'D'), + 'signal_type': type_map.get(signal_type, 'swing'), + 'position_size': signal.get('position_size', 'light'), # LLM 建议的仓位大小 + 'trend': signal.get('trend') + } + + async def _notify_real_order_created( + self, + symbol: str, + signal: Dict[str, Any], + result: Dict[str, Any] + ): + """发送实盘订单创建通知""" + side = signal.get('action', 'buy') + side_text = "做多" if side == 'buy' else "做空" + grade = signal.get('grade', 'N/A') + position_size = result.get('position_size', 'light') + quantity = result.get('quantity', 0) + + message = f"""💰 实盘订单已创建 + +交易对: {symbol} +方向: {side_text} +等级: {grade} +仓位: {position_size} +数量: ${quantity:.2f} +订单 ID: {result.get('order_id', '')[:12]}... + +⚠️ 真实资金交易中""" + + await self.feishu.send_text(message) + await self.telegram.send_message(message) + logger.info(f"已发送实盘订单创建通知: {result.get('order_id')}") + async def _notify_position_adjustment( self, symbol: str, diff --git a/backend/app/services/position_manager.py b/backend/app/services/position_manager.py new file mode 100644 index 0000000..8087b40 --- /dev/null +++ b/backend/app/services/position_manager.py @@ -0,0 +1,198 @@ +""" +仓位管理服务 - 供模拟盘和实盘共用 +基于 LLM 建议动态计算仓位大小 +""" +from typing import Dict, Any, Optional, Tuple +from abc import ABC, abstractmethod +from app.utils.logger import logger + + +class PositionCalculator(ABC): + """仓位计算器抽象基类""" + + @abstractmethod + def get_account_status(self) -> Dict[str, Any]: + """获取账户状态""" + pass + + @abstractmethod + def get_max_leverage(self) -> int: + """获取最大杠杆""" + pass + + +class PositionManager: + """ + 仓位管理器 + + 根据 LLM 建议的仓位大小(heavy/medium/light)动态计算实际保证金和持仓价值 + 模拟盘和实盘共用同一套仓位管理逻辑 + """ + + def __init__(self, calculator: PositionCalculator): + """ + 初始化仓位管理器 + + Args: + calculator: 仓位计算器(模拟盘或实盘) + """ + self.calculator = calculator + + def calculate_position( + self, + position_size: str, + symbol: str, + custom_ratios: Optional[Dict[str, float]] = None + ) -> Tuple[float, float]: + """ + 根据 LLM 建议计算仓位 + + Args: + position_size: LLM 建议的仓位大小 ('heavy', 'medium', 'light') + symbol: 交易对 + custom_ratios: 自定义仓位比例,可选 + + Returns: + (margin, position_value) 元组 + """ + account = self.calculator.get_account_status() + max_leverage = self.calculator.get_max_leverage() + + balance = account['current_balance'] + current_position_value = account.get('total_position_value', 0) + + # 计算可用保证金空间 + max_position_value = balance * max_leverage + available_position_value = max_position_value - current_position_value + + if available_position_value <= 0: + logger.warning(f"已达最大杠杆限制,无法开仓") + return 0, 0 + + # 使用自定义比例或默认比例 + ratios = custom_ratios or { + 'heavy': 0.30, + 'medium': 0.15, + 'light': 0.05 + } + + size_ratio = ratios.get(position_size, 0.05) + + # 计算目标持仓价值 + target_position_value = available_position_value * size_ratio + + # 设置最小和最大限制 + min_position_value = 1000 # 最小持仓价值 + max_single_position = balance * 5 # 单笔最大不超过 5x 杠杆 + + position_value = max(min_position_value, min(target_position_value, max_single_position)) + position_value = min(position_value, available_position_value) + position_value = round(position_value, 2) + + # 计算对应的保证金 + margin = round(position_value / max_leverage, 2) + + logger.info(f"动态仓位计算: {position_size} | 可用空间: ${available_position_value:,.0f} | " + f"目标仓位: ${position_value:,.0f} | 保证金: ${margin:,.0f}") + + return margin, position_value + + +class PaperPositionCalculator(PositionCalculator): + """模拟盘仓位计算器""" + + def __init__(self, account_status_getter, max_leverage: int = 20): + """ + 初始化模拟盘计算器 + + Args: + account_status_getter: 获取账户状态的函数 + max_leverage: 最大杠杆倍数 + """ + self.account_status_getter = account_status_getter + self._max_leverage = max_leverage + + def get_account_status(self) -> Dict[str, Any]: + return self.account_status_getter() + + def get_max_leverage(self) -> int: + return self._max_leverage + + +class RealPositionCalculator(PositionCalculator): + """实盘仓位计算器""" + + def __init__(self, balance: float, used_margin: float, total_position_value: float, max_leverage: int = 10): + """ + 初始化实盘计算器 + + Args: + balance: 账户余额 + used_margin: 已用保证金 + total_position_value: 总持仓价值 + max_leverage: 最大杠杆倍数 + """ + self._account_status = { + 'current_balance': balance, + 'used_margin': used_margin, + 'total_position_value': total_position_value + } + self._max_leverage = max_leverage + + def get_account_status(self) -> Dict[str, Any]: + return self._account_status + + def get_max_leverage(self) -> int: + return self._max_leverage + + +def calculate_paper_position( + account_status_getter, + position_size: str, + symbol: str, + max_leverage: int = 20 +) -> Tuple[float, float]: + """ + 计算模拟盘仓位(快捷方法) + + Args: + account_status_getter: 获取账户状态的函数 + position_size: LLM 建议的仓位大小 + symbol: 交易对 + max_leverage: 最大杠杆倍数 + + Returns: + (margin, position_value) 元组 + """ + calculator = PaperPositionCalculator(account_status_getter, max_leverage) + manager = PositionManager(calculator) + return manager.calculate_position(position_size, symbol) + + +def calculate_real_position( + balance: float, + used_margin: float, + total_position_value: float, + position_size: str, + symbol: str, + max_leverage: int = 10, + custom_ratios: Optional[Dict[str, float]] = None +) -> Tuple[float, float]: + """ + 计算实盘仓位(快捷方法) + + Args: + balance: 账户余额 + used_margin: 已用保证金 + total_position_value: 总持仓价值 + position_size: LLM 建议的仓位大小 + symbol: 交易对 + max_leverage: 最大杠杆倍数 + custom_ratios: 自定义仓位比例(实盘可用更保守的配置) + + Returns: + (margin, position_value) 元组 + """ + calculator = RealPositionCalculator(balance, used_margin, total_position_value, max_leverage) + manager = PositionManager(calculator) + return manager.calculate_position(position_size, symbol, custom_ratios) diff --git a/backend/app/services/real_trading_service.py b/backend/app/services/real_trading_service.py index 234adbe..118b850 100644 --- a/backend/app/services/real_trading_service.py +++ b/backend/app/services/real_trading_service.py @@ -2,6 +2,7 @@ 实盘交易服务 - Bitget 合约交易 提供与模拟交易服务类似的接口,但执行的是真实交易 +集成 LLM 仓位管理决策 """ import uuid from datetime import datetime, timedelta @@ -10,6 +11,7 @@ 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.services.position_manager import calculate_real_position from app.config import get_settings from app.utils.logger import logger @@ -29,6 +31,9 @@ class RealTradingService: self.risk_per_trade = self.settings.real_trading_risk_per_trade self.max_orders = self.settings.real_trading_max_orders + # 自动交易开关(从数据库加载) + self.auto_trading_enabled = self._load_auto_trading_status() + # 获取交易 API (使用 CCXT SDK 版本) from app.services.bitget_trading_api_sdk import get_bitget_trading_api self.trading_api = get_bitget_trading_api() @@ -44,14 +49,81 @@ class RealTradingService: self._load_active_orders() logger.info(f"实盘交易服务初始化完成(最大单笔: ${self.max_single_position}," - f"杠杆: {self.default_leverage}x,最大持仓: {self.max_orders})") + f"杠杆: {self.default_leverage}x,最大持仓: {self.max_orders}," + f"自动交易: {'启用' if self.auto_trading_enabled else '禁用'})") def _ensure_table_exists(self): """确保数据表已创建""" from app.models.real_trading import RealOrder from app.models.database import Base + from sqlalchemy import text Base.metadata.create_all(bind=db_service.engine) + # 创建自动交易开关表 + db = db_service.get_session() + try: + db.execute(text(""" + CREATE TABLE IF NOT EXISTS real_trading_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """)) + db.commit() + + # 初始化自动交易开关 + db.execute(text(""" + INSERT OR IGNORE INTO real_trading_settings (key, value) + VALUES ('auto_trading_enabled', '0') + """)) + db.commit() + except Exception as e: + logger.warning(f"创建设置表失败: {e}") + db.rollback() + finally: + db.close() + + def _load_auto_trading_status(self) -> bool: + """从数据库加载自动交易开关状态""" + db = db_service.get_session() + try: + from sqlalchemy import text + result = db.execute(text("SELECT value FROM real_trading_settings WHERE key = 'auto_trading_enabled'")).fetchone() + if result: + return result[0] == '1' + return False + except Exception as e: + logger.warning(f"加载自动交易状态失败: {e}") + return False + finally: + db.close() + + def set_auto_trading(self, enabled: bool) -> bool: + """设置自动交易开关""" + db = db_service.get_session() + try: + from sqlalchemy import text + db.execute(text(""" + UPDATE real_trading_settings + SET value = :value, updated_at = CURRENT_TIMESTAMP + WHERE key = 'auto_trading_enabled' + """), {'value': '1' if enabled else '0'}) + db.commit() + + self.auto_trading_enabled = enabled + logger.info(f"实盘自动交易已{'启用' if enabled else '禁用'}") + return True + except Exception as e: + logger.error(f"设置自动交易失败: {e}") + db.rollback() + return False + finally: + db.close() + + def get_auto_trading_status(self) -> bool: + """获取自动交易状态""" + return self.auto_trading_enabled + def _load_active_orders(self): """从数据库加载活跃订单""" db = db_service.get_session() @@ -70,15 +142,33 @@ class RealTradingService: def create_order_from_signal(self, signal: Dict[str, Any], current_price: float = None) -> Dict[str, Any]: """ - 从信号创建实盘订单 + 从信号创建实盘订单(集成 LLM 仓位管理) Args: signal: LLM 分析信号 + - symbol: 交易对 + - side: 'long' or 'short' + - entry_type: 'market' or 'limit' + - entry_price: 入场价 + - stop_loss: 止损价 + - take_profit: 止盈价 + - grade: 信号等级 + - confidence: 置信度 + - position_size: LLM 建议的仓位大小 ('heavy', 'medium', 'light') current_price: 当前价格 Returns: 创建结果 """ + # 检查自动交易开关 + if not self.auto_trading_enabled: + logger.info(f"实盘自动交易已禁用,跳过信号执行") + return { + 'success': False, + 'message': '实盘自动交易已禁用', + 'skipped': True + } + if not self.trading_api: return { 'success': False, @@ -109,6 +199,7 @@ class RealTradingService: take_profit = signal.get('take_profit') grade = signal.get('grade', 'D') confidence = signal.get('confidence', 0) + position_size = signal.get('position_size', 'light') # LLM 建议的仓位大小 # 验证必需参数 if not all([symbol, side, stop_loss, take_profit]): @@ -141,12 +232,52 @@ class RealTradingService: 'message': f'已达最大持仓数 {self.max_orders}' } - # 风险检查 - 检查账户余额 - balance_info = self._check_balance_risk(symbol, entry_price, stop_loss) - if not balance_info['can_trade']: + # 获取账户状态 + account = self.get_account_status() + balance = account['current_balance'] + available = account['available'] + used_margin = account['used_margin'] + total_position_value = account['total_position_value'] + + if available < 10: return { 'success': False, - 'message': balance_info['reason'] + 'message': f'可用余额不足 (${available:.2f})' + } + + # === 使用 LLM 建议的仓位大小计算仓位 === + # 实盘使用更保守的比例配置 + custom_ratios = { + 'heavy': 0.15, # 激进方案:heavy 15%(模拟盘是 30%) + 'medium': 0.08, # 激进方案:medium 8%(模拟盘是 15%) + 'light': 0.03 # 激进方案:light 3%(模拟盘是 5%) + } + + # 计算仓位(使用统一的仓位管理器) + margin, position_value = calculate_real_position( + balance=balance, + used_margin=used_margin, + total_position_value=total_position_value, + position_size=position_size, + symbol=symbol, + max_leverage=self.default_leverage, + custom_ratios=custom_ratios + ) + + if margin <= 0 or position_value <= 0: + return { + 'success': False, + 'message': '无法开仓:仓位计算失败或已达杠杆限制' + } + + quantity = position_value # 订单数量(以 USDT 计价) + + # 最小仓位限制(100 美金测试,最小 5 USDT) + min_quantity = 5 + if quantity < min_quantity: + return { + 'success': False, + 'message': f'计算仓位 ${quantity:.2f} 小于最小值 ${min_quantity}' } # 创建订单对象 @@ -158,14 +289,14 @@ class RealTradingService: entry_price=entry_price, stop_loss=stop_loss, take_profit=take_profit, - quantity=balance_info['quantity'], + quantity=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 + status=OrderStatus.OPEN if entry_type == 'market' else OrderStatus.PENDING ) db.add(order) @@ -189,11 +320,15 @@ class RealTradingService: self.active_orders[order_id] = order result['success'] = True - result['message'] = '实盘订单创建成功' + result['message'] = f'实盘订单创建成功 (仓位: {position_size})' result['order_id'] = order_id result['exchange_order_id'] = exchange_result.get('order_id') + result['position_size'] = position_size + result['quantity'] = quantity - logger.info(f"✅ 实盘订单创建成功: {symbol} {side} ${entry_price} -> {exchange_result.get('order_id')}") + logger.info(f"✅ 实盘订单创建成功: {symbol} {side} ${entry_price} | " + f"仓位: {position_size} | 数量: ${quantity:.2f} | " + f"-> {exchange_result.get('order_id')}") else: # 下单失败,删除记录 db.delete(order) @@ -213,59 +348,6 @@ class RealTradingService: 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) @@ -401,6 +483,43 @@ class RealTradingService: 'available': 0 } + def get_position_info(self) -> Dict[str, Any]: + """ + 获取当前持仓信息(供 LLM 分析使用) + + Returns: + 持仓信息字典 + """ + account = self.get_account_status() + active_orders = self.get_active_orders() + + # 计算当前杠杆 + balance = account['current_balance'] + total_position_value = account['total_position_value'] + current_leverage = total_position_value / balance if balance > 0 else 0 + + # 格式化持仓列表 + positions = [] + for order in active_orders: + positions.append({ + 'symbol': order.get('symbol'), + 'side': order.get('side'), + 'status': order.get('status'), + 'entry_price': order.get('filled_price') or order.get('entry_price'), + 'quantity': order.get('quantity'), + 'pnl_percent': order.get('pnl_percent', 0) + }) + + return { + 'account_balance': balance, + 'total_position_value': total_position_value, + 'current_leverage': current_leverage, + 'max_leverage': self.default_leverage, + 'active_order_count': len(active_orders), + 'max_orders': self.max_orders, + 'positions': positions + } + # 全局实例 _real_trading_service: Optional[RealTradingService] = None @@ -410,8 +529,11 @@ def get_real_trading_service() -> Optional[RealTradingService]: """ 获取实盘交易服务实例(单例) + 注意:不再检查 REAL_TRADING_ENABLED 配置 + 只要 API 配置了就初始化服务,自动交易可以单独控制 + Returns: - RealTradingService 实例或 None(如果未配置或未启用) + RealTradingService 实例或 None(如果未配置 API) """ global _real_trading_service @@ -420,11 +542,7 @@ def get_real_trading_service() -> Optional[RealTradingService]: settings = get_settings() - # 检查是否启用实盘交易 - if not settings.real_trading_enabled: - return None - - # 检查是否配置了 API Key + # 检查是否配置了 API Key(不再检查 REAL_TRADING_ENABLED) if not settings.bitget_api_key or not settings.bitget_api_secret: logger.warning("Bitget API Key 未配置,实盘交易功能不可用") return None diff --git a/frontend/real-trading.html b/frontend/real-trading.html index 3d24b26..b0d1d86 100644 --- a/frontend/real-trading.html +++ b/frontend/real-trading.html @@ -362,6 +362,81 @@ color: var(--text-secondary); margin-top: 4px; } + + .switch-container { + display: flex; + align-items: center; + gap: 12px; + } + + .switch-label { + font-size: 14px; + color: var(--text-primary); + } + + .switch { + position: relative; + display: inline-block; + width: 50px; + height: 26px; + } + + .switch input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #444; + transition: .4s; + border-radius: 26px; + } + + .slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; + } + + input:checked + .slider { + background-color: var(--accent); + } + + input:checked + .slider:before { + transform: translateX(24px); + } + + .switch-disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .status-text { + font-size: 12px; + color: var(--text-secondary); + min-width: 60px; + } + + .status-text.enabled { + color: #00ff41; + } + + .status-text.disabled { + color: #ff4444; + } @@ -374,9 +449,26 @@ 实盘交易 LIVE - +
+ +
+ 自动交易 + + + {{ autoTradingEnabled ? '已启用' : '已禁用' }} + +
+ +
@@ -560,6 +652,8 @@ serviceEnabled: false, apiConfigured: false, useTestnet: true, + autoTradingEnabled: false, + switching: false, account: { current_balance: 0, available: 0, @@ -622,6 +716,8 @@ this.serviceEnabled = status.enabled; this.apiConfigured = status.api_configured; this.useTestnet = status.use_testnet; + // 获取自动交易状态 + this.autoTradingEnabled = status.auto_trading_enabled || false; if (status.account) { this.account = status.account; } @@ -631,6 +727,34 @@ } }, + async toggleAutoTrading() { + if (this.switching) return; + + this.switching = true; + try { + const response = await axios.post('/api/real-trading/auto-trading', null, { + params: { enabled: this.autoTradingEnabled } + }); + + if (response.data.success) { + // 刷新状态 + await this.fetchServiceStatus(); + alert(response.data.message); + } else { + // 恢复原状态 + this.autoTradingEnabled = !this.autoTradingEnabled; + alert('设置失败: ' + (response.data.message || '未知错误')); + } + } catch (error) { + console.error('设置自动交易失败:', error); + // 恢复原状态 + this.autoTradingEnabled = !this.autoTradingEnabled; + alert('设置失败: ' + (error.response?.data?.detail || error.message)); + } finally { + this.switching = false; + } + }, + async fetchAccountStatus() { try { const response = await axios.get('/api/real-trading/account');