diff --git a/backend/api/routes.py b/backend/api/routes.py index 22cbf36..accd658 100644 --- a/backend/api/routes.py +++ b/backend/api/routes.py @@ -19,6 +19,8 @@ from trading.position_service import get_position_service from services.prompt_service import get_trading_strategy, set_trading_strategy +from functools import wraps +from fastapi import HTTPException, status router = APIRouter() @@ -49,6 +51,19 @@ class CacheInfoResponse(BaseModel): symbol_details: Dict[str, Dict[str, Any]] +def check_control_permission(func): + """装饰器:检查控制操作权限""" + @wraps(func) + async def wrapper(*args, **kwargs): + if not config.system.allow_control_operations: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Control operations are disabled for security reasons. Set allow_control_operations=true in agent.yaml to enable." + ) + return await func(*args, **kwargs) + return wrapper + + @router.get("/health", response_model=HealthResponse) async def health_check(): """System health check""" @@ -182,7 +197,8 @@ async def get_system_config(): "system": { "host": config.system.host, "port": config.system.port, - "log_level": config.system.log_level + "log_level": config.system.log_level, + "allow_control_operations": config.system.allow_control_operations }, "risk": { "max_position_size_percent": config.default_risk.max_position_size_percent, @@ -370,6 +386,7 @@ class AgentStatusResponse(BaseModel): # Agent control endpoints @router.post("/agent/start", response_model=AgentControlResponse) +@check_control_permission async def start_agent(): """启动 AI Agent 调度器""" try: @@ -405,6 +422,7 @@ async def start_agent(): @router.post("/agent/stop", response_model=AgentControlResponse) +@check_control_permission async def stop_agent(): """停止 AI Agent 调度器""" try: @@ -670,6 +688,7 @@ async def get_trade_stats(days: int = 30): @router.post("/trading/history/reset") +@check_control_permission async def reset_trading_history(init_time: Optional[str] = None): """重置交易历史系统(清空所有数据并重新初始化)""" try: @@ -695,6 +714,7 @@ async def reset_trading_history(init_time: Optional[str] = None): @router.post("/trading/history/sync") +@check_control_permission async def sync_trading_history(full_sync: bool = False): """手动同步交易历史""" try: @@ -775,6 +795,7 @@ async def get_current_trading_strategy(): @router.post("/trading/strategy", response_model=TradingStrategyUpdateResponse) +@check_control_permission async def update_trading_strategy(request: TradingStrategyRequest): """更新用户自定义交易策略""" try: @@ -801,6 +822,7 @@ async def update_trading_strategy(request: TradingStrategyRequest): @router.delete("/trading/strategy", response_model=TradingStrategyUpdateResponse) +@check_control_permission async def reset_trading_strategy(): """重置交易策略为默认值(删除数据库中的自定义配置)""" try: diff --git a/backend/config/agent.yaml b/backend/config/agent.yaml index ea2f70b..730cb53 100644 --- a/backend/config/agent.yaml +++ b/backend/config/agent.yaml @@ -5,7 +5,7 @@ agent: model_name: "deepseek-chat" # Model name (gpt-4o, claude-3-5-sonnet, deepseek-chat, qwen-plus, etc.) base_url: "https://api.deepseek.com/v1" # API base URL (null for OpenAI, or custom like "https://api.deepseek.com/v1") api_key: "${OPENAI_API_KEY}" # API key environment variable - + # Trading Configuration decision_interval: 180 # seconds between decisions symbols: @@ -17,7 +17,7 @@ agent: - "3m" - "1h" - "4h" - + # 用户可配置的交易策略(会被数据库覆盖) trading_strategy: | 1. 单一币种仓位上限为可用余额的 20% @@ -33,15 +33,15 @@ exchange: api_key: "${BINANCE_API_KEY}" api_secret: "${BINANCE_API_SECRET}" testnet: false # Use testnet for development - + # WebSocket and REST API endpoints websocket_url: "wss://fstream.binance.com/stream" # Production rest_api_url: "https://fapi.binance.com" # Production - + # Testnet endpoints (used when testnet: true) testnet_websocket_url: "wss://stream.binancefuture.com/stream" testnet_rest_api_url: "https://testnet.binancefuture.com" - + # Futures trading settings (for CCXT) default_leverage: 1 margin_mode: "cross" # cross or isolated @@ -49,14 +49,14 @@ exchange: timeout: 10000 # milliseconds retries: 3 sandbox: false # CCXT sandbox mode (alternative to testnet) - + # Risk Management (these can be overridden in user prompts) default_risk: max_position_size_percent: 0.1 # 10% of account per position max_daily_loss_percent: 0.05 # 5% max daily loss # Maximum leverage stop_loss_percent: 0.02 # 2% stop loss - + # Account Snapshot Configuration account_snapshot: enabled: true @@ -69,9 +69,10 @@ logging: save_decisions: true save_executions: true save_snapshots: true - -# System Configuration + +# System Configuration system: host: "0.0.0.0" port: 8000 - max_concurrent_decisions: 1 # Prevent overlapping decisions \ No newline at end of file + max_concurrent_decisions: 1 # Prevent overlapping decisions + allow_control_operations: true # 是否允许前端控制操作(启动/停止 agent、修改策略等) \ No newline at end of file diff --git a/backend/config/agent_config.py b/backend/config/agent_config.py index 9041588..4f88bd6 100644 --- a/backend/config/agent_config.py +++ b/backend/config/agent_config.py @@ -123,6 +123,7 @@ class SystemConfig(BaseModel): port: int = 8000 log_level: str = "INFO" max_concurrent_decisions: int = 1 + allow_control_operations: bool = False # 是否允许前端控制操作 class AppConfig(BaseModel): diff --git a/backend/services/prompt_service.py b/backend/services/prompt_service.py index af5a6cc..4401e1f 100644 --- a/backend/services/prompt_service.py +++ b/backend/services/prompt_service.py @@ -23,16 +23,17 @@ _strategy_cache: Optional[str] = None _cache_valid = False + async def get_trading_strategy() -> str: """ 获取交易策略配置,按优先级:数据库 > 配置文件 > 代码默认 """ global _strategy_cache, _cache_valid - + # 先检查缓存 if _cache_valid and _strategy_cache is not None: return _strategy_cache - + try: # 1. 优先级最高:检查数据库 async with get_session_maker()() as session: @@ -40,16 +41,16 @@ async def get_trading_strategy() -> str: select(SystemConfig).where(SystemConfig.key == "trading_strategy") ) config_row = result.scalar_one_or_none() - + if config_row and config_row.value.strip(): logger.info("使用数据库中的交易策略配置") _strategy_cache = config_row.value.strip() _cache_valid = True return _strategy_cache - + except Exception as e: logger.warning(f"读取数据库交易策略失败: {e}") - + # 2. 次优先级:检查配置文件 try: config_strategy = getattr(config.agent, 'trading_strategy', None) @@ -60,32 +61,33 @@ async def get_trading_strategy() -> str: return _strategy_cache except Exception as e: logger.warning(f"读取配置文件交易策略失败: {e}") - + # 3. 最低优先级:使用代码默认 logger.info("使用代码默认的交易策略配置") _strategy_cache = DEFAULT_TRADING_STRATEGY _cache_valid = True return _strategy_cache + async def set_trading_strategy(strategy: str) -> bool: """ 设置用户自定义的交易策略(存储到数据库) """ global _strategy_cache, _cache_valid - + try: if not strategy or not strategy.strip(): raise ValueError("交易策略内容不能为空") - + strategy = strategy.strip() - + async with get_session_maker()() as session: # 查找现有配置 result = await session.execute( select(SystemConfig).where(SystemConfig.key == "trading_strategy") ) config_row = result.scalar_one_or_none() - + if config_row: # 更新现有配置 config_row.value = strategy @@ -99,19 +101,20 @@ async def set_trading_strategy(strategy: str) -> bool: ) session.add(new_config) logger.info("创建新的交易策略配置") - + await session.commit() - + # 清除缓存,强制下次重新读取 _strategy_cache = None _cache_valid = False - + return True - + except Exception as e: logger.error(f"设置交易策略失败: {e}") return False + def clear_strategy_cache(): """清除策略缓存(用于测试或强制刷新)""" global _strategy_cache, _cache_valid diff --git a/frontend/src/app/settings/page.tsx b/frontend/src/app/settings/page.tsx index 2c619d0..722dfa6 100644 --- a/frontend/src/app/settings/page.tsx +++ b/frontend/src/app/settings/page.tsx @@ -14,6 +14,7 @@ import type { AgentStatus } from "@/lib/api"; import Toast from "@/components/Toast"; import Sidebar from "@/components/Sidebar"; import Header from "@/components/Header"; +import { isControlOperationsAllowed } from "@/lib/config"; export default function SettingsPage() { const [strategy, setStrategy] = useState(""); @@ -25,6 +26,7 @@ export default function SettingsPage() { text: string; } | null>(null); const [sidebarOpen, setSidebarOpen] = useState(false); + const [controlAllowed, setControlAllowed] = useState(false); // Bot status const [botStatus, setBotStatus] = useState({ @@ -43,6 +45,7 @@ export default function SettingsPage() { useEffect(() => { loadStrategy(); loadBotStatus(); + isControlOperationsAllowed().then(setControlAllowed); }, []); const loadStrategy = async () => { @@ -82,9 +85,8 @@ export default function SettingsPage() { await loadBotStatus(); // Reload status } catch (error) { - const fallbackMessage = `Failed to ${ - botStatus.is_running ? "stop" : "start" - } trading bot`; + const fallbackMessage = `Failed to ${botStatus.is_running ? "stop" : "start" + } trading bot`; setMessage({ type: "error", text: error instanceof Error ? error.message : fallbackMessage, @@ -204,11 +206,10 @@ export default function SettingsPage() { Trading Bot Status: {botStatus.is_running ? "RUNNING" : "STOPPED"} @@ -218,12 +219,11 @@ export default function SettingsPage() {