diff --git a/QUICK_START.md b/QUICK_START.md index 944e0d9..cb3b568 100644 --- a/QUICK_START.md +++ b/QUICK_START.md @@ -63,9 +63,40 @@ npm run dev 访问前端:http://localhost:3000 -## 5. 启动交易系统 +## 5. 安装交易系统依赖 + +### 方式1:使用安装脚本(推荐) ```bash +cd trading_system +./setup.sh +``` + +### 方式2:使用项目根目录虚拟环境 + +```bash +# 如果项目根目录已有虚拟环境 +source .venv/bin/activate # 从项目根目录 +cd trading_system +pip install -r requirements.txt +``` + +### 方式3:手动创建虚拟环境 + +```bash +cd trading_system +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +## 6. 启动交易系统 + +```bash +# 确保虚拟环境已激活 +source .venv/bin/activate # 从项目根目录 +# 或 source ../.venv/bin/activate # 从trading_system目录 + # 进入trading_system目录 cd trading_system python main.py diff --git a/README.md b/README.md index edaeb62..bc2bd4a 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,16 @@ cd backend python init_config.py ``` -### 2. 启动后端服务 +### 2. 创建虚拟环境(推荐) + +```bash +# 在项目根目录创建虚拟环境(供backend和trading_system共享) +python3 -m venv .venv +source .venv/bin/activate # Linux/Mac +# 或 .venv\Scripts\activate # Windows +``` + +### 3. 启动后端服务 ```bash cd backend @@ -50,7 +59,7 @@ export DB_HOST=localhost DB_USER=root DB_PASSWORD=xxx DB_NAME=auto_trade_sys uvicorn api.main:app --host 0.0.0.0 --port 8000 ``` -### 3. 启动前端 +### 4. 启动前端 ```bash cd frontend @@ -58,17 +67,26 @@ npm install npm run dev ``` -### 4. 启动交易系统 +### 5. 安装并启动交易系统 ```bash -# 方式1:从trading_system目录运行(推荐) +# 方式1:使用安装脚本(推荐) cd trading_system +./setup.sh + +# 然后运行(确保虚拟环境已激活) +source ../.venv/bin/activate # 或 source .venv/bin/activate python main.py -# 方式2:从项目根目录运行 -python -m trading_system.main +# 方式2:手动安装 +cd trading_system +source ../.venv/bin/activate # 激活虚拟环境 +pip install -r requirements.txt +python main.py ``` +**注意**:现代 Linux 系统(如 Ubuntu 22.04+)不允许直接在系统 Python 中安装包,必须使用虚拟环境。详见 `INSTALL.md`。 + ## 功能特性 1. **自动市场扫描**:每1小时扫描所有USDT永续合约,发现涨跌幅最大的前10个货币对 @@ -119,6 +137,7 @@ python -m trading_system.main ## 文档 - `QUICK_START.md` - 快速开始指南 +- `INSTALL.md` - 安装指南(虚拟环境设置) - `DEPLOYMENT.md` - 部署指南 - `README_ARCHITECTURE.md` - 架构说明 - `PROJECT_SUMMARY.md` - 项目总结 diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py new file mode 100644 index 0000000..e451d25 --- /dev/null +++ b/backend/api/routes/account.py @@ -0,0 +1,173 @@ +""" +账户实时数据API - 从币安API获取实时账户和订单数据 +""" +from fastapi import APIRouter, HTTPException +import sys +from pathlib import Path +import asyncio +import logging + +project_root = Path(__file__).parent.parent.parent.parent +sys.path.insert(0, str(project_root)) +sys.path.insert(0, str(project_root / 'backend')) +sys.path.insert(0, str(project_root / 'trading_system')) + +from database.models import TradingConfig +from fastapi import HTTPException + +logger = logging.getLogger(__name__) +router = APIRouter() + + +async def get_realtime_account_data(): + """从币安API实时获取账户数据""" + try: + # 从数据库读取API密钥 + api_key = TradingConfig.get_value('BINANCE_API_KEY') + api_secret = TradingConfig.get_value('BINANCE_API_SECRET') + use_testnet = TradingConfig.get_value('USE_TESTNET', False) + + if not api_key or not api_secret: + raise HTTPException( + status_code=400, + detail="API密钥未配置,请在配置界面设置BINANCE_API_KEY和BINANCE_API_SECRET" + ) + + # 导入交易系统的BinanceClient + try: + from binance_client import BinanceClient + except ImportError: + # 如果直接导入失败,尝试从trading_system导入 + trading_system_path = project_root / 'trading_system' + sys.path.insert(0, str(trading_system_path)) + from binance_client import BinanceClient + + # 创建客户端 + client = BinanceClient( + api_key=api_key, + api_secret=api_secret, + testnet=use_testnet + ) + + await client.connect() + + # 获取账户余额 + balance = await client.get_account_balance() + + # 获取持仓 + positions = await client.get_open_positions() + + # 计算总仓位价值和总盈亏 + total_position_value = 0 + total_pnl = 0 + open_positions_count = 0 + + for pos in positions: + position_amt = float(pos.get('positionAmt', 0)) + if position_amt == 0: + continue + + entry_price = float(pos.get('entryPrice', 0)) + mark_price = float(pos.get('markPrice', 0)) + unrealized_pnl = float(pos.get('unRealizedProfit', 0)) + + if mark_price == 0: + # 如果没有标记价格,使用入场价 + mark_price = entry_price + + position_value = abs(position_amt * mark_price) + total_position_value += position_value + total_pnl += unrealized_pnl + open_positions_count += 1 + + await client.disconnect() + + return { + "total_balance": balance.get('total', 0), + "available_balance": balance.get('available', 0), + "total_position_value": total_position_value, + "total_pnl": total_pnl, + "open_positions": open_positions_count + } + except HTTPException: + raise + except Exception as e: + logger.error(f"获取账户数据失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"获取账户数据失败: {str(e)}") + + +@router.get("/realtime") +async def get_realtime_account(): + """获取实时账户数据""" + return await get_realtime_account_data() + + +@router.get("/positions") +async def get_realtime_positions(): + """获取实时持仓数据""" + try: + # 从数据库读取API密钥 + api_key = TradingConfig.get_value('BINANCE_API_KEY') + api_secret = TradingConfig.get_value('BINANCE_API_SECRET') + use_testnet = TradingConfig.get_value('USE_TESTNET', False) + + if not api_key or not api_secret: + raise HTTPException( + status_code=400, + detail="API密钥未配置" + ) + + # 导入BinanceClient + try: + from binance_client import BinanceClient + except ImportError: + trading_system_path = project_root / 'trading_system' + sys.path.insert(0, str(trading_system_path)) + from binance_client import BinanceClient + + client = BinanceClient( + api_key=api_key, + api_secret=api_secret, + testnet=use_testnet + ) + + await client.connect() + positions = await client.get_open_positions() + await client.disconnect() + + # 格式化持仓数据 + formatted_positions = [] + for pos in positions: + position_amt = float(pos.get('positionAmt', 0)) + if position_amt == 0: + continue + + entry_price = float(pos.get('entryPrice', 0)) + mark_price = float(pos.get('markPrice', 0)) + unrealized_pnl = float(pos.get('unRealizedProfit', 0)) + + if mark_price == 0: + mark_price = entry_price + + position_value = abs(position_amt * mark_price) + pnl_percent = 0 + if entry_price > 0 and position_value > 0: + pnl_percent = (unrealized_pnl / position_value) * 100 + + formatted_positions.append({ + "symbol": pos.get('symbol'), + "side": "BUY" if position_amt > 0 else "SELL", + "quantity": abs(position_amt), + "entry_price": entry_price, + "mark_price": mark_price, + "pnl": unrealized_pnl, + "pnl_percent": pnl_percent, + "leverage": int(pos.get('leverage', 1)) + }) + + return formatted_positions + except HTTPException: + raise + except Exception as e: + logger.error(f"获取持仓数据失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"获取持仓数据失败: {str(e)}") diff --git a/backend/api/routes/config.py b/backend/api/routes/config.py index 7b8fa2a..10c495e 100644 --- a/backend/api/routes/config.py +++ b/backend/api/routes/config.py @@ -74,8 +74,37 @@ async def update_config(key: str, item: ConfigUpdate): category = item.category or existing['category'] description = item.description or existing['description'] + # 验证配置值 + if config_type == 'number': + try: + float(item.value) + except (ValueError, TypeError): + raise HTTPException(status_code=400, detail=f"Invalid number value for {key}") + elif config_type == 'boolean': + if not isinstance(item.value, bool): + # 尝试转换 + if isinstance(item.value, str): + item.value = item.value.lower() in ('true', '1', 'yes', 'on') + else: + item.value = bool(item.value) + + # 特殊验证:百分比配置应该在0-1之间 + if 'PERCENT' in key and config_type == 'number': + if not (0 <= float(item.value) <= 1): + raise HTTPException( + status_code=400, + detail=f"{key} must be between 0 and 1 (0% to 100%)" + ) + + # 更新配置 TradingConfig.set(key, item.value, config_type, category, description) - return {"message": "Config updated", "key": key} + + return { + "message": "配置已更新", + "key": key, + "value": item.value, + "note": "交易系统将在下次扫描时自动使用新配置" + } except HTTPException: raise except Exception as e: @@ -86,14 +115,48 @@ async def update_config(key: str, item: ConfigUpdate): async def update_configs_batch(configs: list[ConfigItem]): """批量更新配置""" try: + updated_count = 0 + errors = [] + for item in configs: - TradingConfig.set( - item.key, - item.value, - item.type, - item.category, - item.description - ) - return {"message": f"{len(configs)} configs updated"} + try: + # 验证配置值 + if item.type == 'number': + try: + float(item.value) + except (ValueError, TypeError): + errors.append(f"{item.key}: Invalid number value") + continue + + # 特殊验证:百分比配置 + if 'PERCENT' in item.key and item.type == 'number': + if not (0 <= float(item.value) <= 1): + errors.append(f"{item.key}: Must be between 0 and 1") + continue + + TradingConfig.set( + item.key, + item.value, + item.type, + item.category, + item.description + ) + updated_count += 1 + except Exception as e: + errors.append(f"{item.key}: {str(e)}") + + if errors: + return { + "message": f"部分配置更新成功: {updated_count}/{len(configs)}", + "updated": updated_count, + "errors": errors, + "note": "交易系统将在下次扫描时自动使用新配置" + } + + return { + "message": f"成功更新 {updated_count} 个配置", + "updated": updated_count, + "note": "交易系统将在下次扫描时自动使用新配置" + } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) diff --git a/frontend/src/components/ConfigPanel.css b/frontend/src/components/ConfigPanel.css index e06d2bf..b5cbbda 100644 --- a/frontend/src/components/ConfigPanel.css +++ b/frontend/src/components/ConfigPanel.css @@ -5,15 +5,44 @@ box-shadow: 0 2px 4px rgba(0,0,0,0.1); } -.config-panel h2 { +.config-header { margin-bottom: 2rem; +} + +.config-panel h2 { + margin-bottom: 0.5rem; color: #2c3e50; } +.config-info { + background: #e7f3ff; + padding: 0.75rem 1rem; + border-radius: 4px; + border-left: 3px solid #2196F3; +} + +.config-info p { + margin: 0; + color: #1976D2; + font-size: 0.9rem; +} + .message { padding: 1rem; margin-bottom: 1rem; border-radius: 4px; + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } } .message.success { @@ -67,6 +96,19 @@ border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; + transition: border-color 0.3s, box-shadow 0.3s; +} + +.config-item input:focus, +.config-item select:focus { + outline: none; + border-color: #2196F3; + box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1); +} + +.config-item input.editing { + border-color: #FF9800; + background-color: #FFF8E1; } .config-item input:disabled, @@ -75,6 +117,13 @@ cursor: not-allowed; } +.edit-hint { + font-size: 0.75rem; + color: #FF9800; + margin-left: 0.5rem; + font-style: italic; +} + .config-item .description { font-size: 0.85rem; color: #888; diff --git a/frontend/src/components/ConfigPanel.jsx b/frontend/src/components/ConfigPanel.jsx index 6d3ee89..28cbeaf 100644 --- a/frontend/src/components/ConfigPanel.jsx +++ b/frontend/src/components/ConfigPanel.jsx @@ -28,16 +28,23 @@ const ConfigPanel = () => { setSaving(true) setMessage('') try { - await api.updateConfig(key, { + const response = await api.updateConfig(key, { value, type, category }) - setMessage('配置已更新') + setMessage(response.message || '配置已更新') + if (response.note) { + setTimeout(() => { + setMessage(response.note) + }, 2000) + } // 重新加载配置 await loadConfigs() } catch (error) { - setMessage('更新失败: ' + error.message) + const errorMsg = error.message || '更新失败' + setMessage('更新失败: ' + errorMsg) + console.error('Config update error:', error) } finally { setSaving(false) } @@ -55,8 +62,17 @@ const ConfigPanel = () => { return (
修改配置后,交易系统将在下次扫描时自动使用新配置
+