a
This commit is contained in:
parent
186b2f2424
commit
e5cea19b6a
|
|
@ -63,9 +63,40 @@ npm run dev
|
||||||
|
|
||||||
访问前端:http://localhost:3000
|
访问前端:http://localhost:3000
|
||||||
|
|
||||||
## 5. 启动交易系统
|
## 5. 安装交易系统依赖
|
||||||
|
|
||||||
|
### 方式1:使用安装脚本(推荐)
|
||||||
|
|
||||||
```bash
|
```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目录
|
# 进入trading_system目录
|
||||||
cd trading_system
|
cd trading_system
|
||||||
python main.py
|
python main.py
|
||||||
|
|
|
||||||
31
README.md
31
README.md
|
|
@ -41,7 +41,16 @@ cd backend
|
||||||
python init_config.py
|
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
|
```bash
|
||||||
cd backend
|
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
|
uvicorn api.main:app --host 0.0.0.0 --port 8000
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 启动前端
|
### 4. 启动前端
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
|
|
@ -58,17 +67,26 @@ npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 启动交易系统
|
### 5. 安装并启动交易系统
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 方式1:从trading_system目录运行(推荐)
|
# 方式1:使用安装脚本(推荐)
|
||||||
cd trading_system
|
cd trading_system
|
||||||
|
./setup.sh
|
||||||
|
|
||||||
|
# 然后运行(确保虚拟环境已激活)
|
||||||
|
source ../.venv/bin/activate # 或 source .venv/bin/activate
|
||||||
python main.py
|
python main.py
|
||||||
|
|
||||||
# 方式2:从项目根目录运行
|
# 方式2:手动安装
|
||||||
python -m trading_system.main
|
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个货币对
|
1. **自动市场扫描**:每1小时扫描所有USDT永续合约,发现涨跌幅最大的前10个货币对
|
||||||
|
|
@ -119,6 +137,7 @@ python -m trading_system.main
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
- `QUICK_START.md` - 快速开始指南
|
- `QUICK_START.md` - 快速开始指南
|
||||||
|
- `INSTALL.md` - 安装指南(虚拟环境设置)
|
||||||
- `DEPLOYMENT.md` - 部署指南
|
- `DEPLOYMENT.md` - 部署指南
|
||||||
- `README_ARCHITECTURE.md` - 架构说明
|
- `README_ARCHITECTURE.md` - 架构说明
|
||||||
- `PROJECT_SUMMARY.md` - 项目总结
|
- `PROJECT_SUMMARY.md` - 项目总结
|
||||||
|
|
|
||||||
173
backend/api/routes/account.py
Normal file
173
backend/api/routes/account.py
Normal file
|
|
@ -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)}")
|
||||||
|
|
@ -74,8 +74,37 @@ async def update_config(key: str, item: ConfigUpdate):
|
||||||
category = item.category or existing['category']
|
category = item.category or existing['category']
|
||||||
description = item.description or existing['description']
|
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)
|
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:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -86,7 +115,25 @@ async def update_config(key: str, item: ConfigUpdate):
|
||||||
async def update_configs_batch(configs: list[ConfigItem]):
|
async def update_configs_batch(configs: list[ConfigItem]):
|
||||||
"""批量更新配置"""
|
"""批量更新配置"""
|
||||||
try:
|
try:
|
||||||
|
updated_count = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
for item in configs:
|
for item in configs:
|
||||||
|
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(
|
TradingConfig.set(
|
||||||
item.key,
|
item.key,
|
||||||
item.value,
|
item.value,
|
||||||
|
|
@ -94,6 +141,22 @@ async def update_configs_batch(configs: list[ConfigItem]):
|
||||||
item.category,
|
item.category,
|
||||||
item.description
|
item.description
|
||||||
)
|
)
|
||||||
return {"message": f"{len(configs)} configs updated"}
|
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:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,44 @@
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-panel h2 {
|
.config-header {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel h2 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
color: #2c3e50;
|
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 {
|
.message {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
border-radius: 4px;
|
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 {
|
.message.success {
|
||||||
|
|
@ -67,6 +96,19 @@
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 1rem;
|
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,
|
.config-item input:disabled,
|
||||||
|
|
@ -75,6 +117,13 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-hint {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #FF9800;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.config-item .description {
|
.config-item .description {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #888;
|
color: #888;
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,23 @@ const ConfigPanel = () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
setMessage('')
|
setMessage('')
|
||||||
try {
|
try {
|
||||||
await api.updateConfig(key, {
|
const response = await api.updateConfig(key, {
|
||||||
value,
|
value,
|
||||||
type,
|
type,
|
||||||
category
|
category
|
||||||
})
|
})
|
||||||
setMessage('配置已更新')
|
setMessage(response.message || '配置已更新')
|
||||||
|
if (response.note) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setMessage(response.note)
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
// 重新加载配置
|
// 重新加载配置
|
||||||
await loadConfigs()
|
await loadConfigs()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage('更新失败: ' + error.message)
|
const errorMsg = error.message || '更新失败'
|
||||||
|
setMessage('更新失败: ' + errorMsg)
|
||||||
|
console.error('Config update error:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
|
|
@ -55,8 +62,17 @@ const ConfigPanel = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="config-panel">
|
<div className="config-panel">
|
||||||
|
<div className="config-header">
|
||||||
<h2>交易配置</h2>
|
<h2>交易配置</h2>
|
||||||
{message && <div className={`message ${message.includes('失败') ? 'error' : 'success'}`}>{message}</div>}
|
<div className="config-info">
|
||||||
|
<p>修改配置后,交易系统将在下次扫描时自动使用新配置</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{message && (
|
||||||
|
<div className={`message ${message.includes('失败') || message.includes('错误') ? 'error' : 'success'}`}>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{Object.entries(configCategories).map(([category, label]) => (
|
{Object.entries(configCategories).map(([category, label]) => (
|
||||||
<section key={category} className="config-section">
|
<section key={category} className="config-section">
|
||||||
|
|
@ -82,33 +98,58 @@ const ConfigPanel = () => {
|
||||||
|
|
||||||
const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
const [value, setValue] = useState(config.value)
|
const [value, setValue] = useState(config.value)
|
||||||
|
const [localValue, setLocalValue] = useState(config.value)
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(config.value)
|
setValue(config.value)
|
||||||
|
setLocalValue(config.value)
|
||||||
}, [config.value])
|
}, [config.value])
|
||||||
|
|
||||||
const handleChange = (newValue) => {
|
const handleChange = (newValue) => {
|
||||||
setValue(newValue)
|
setLocalValue(newValue)
|
||||||
|
setIsEditing(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setIsEditing(false)
|
||||||
|
if (localValue !== value) {
|
||||||
|
// 值发生变化,保存
|
||||||
|
let finalValue = localValue
|
||||||
if (config.type === 'number') {
|
if (config.type === 'number') {
|
||||||
onUpdate(parseFloat(newValue) || 0)
|
finalValue = parseFloat(localValue) || 0
|
||||||
|
// 百分比配置需要转换
|
||||||
|
if (label.includes('PERCENT')) {
|
||||||
|
finalValue = finalValue / 100
|
||||||
|
}
|
||||||
} else if (config.type === 'boolean') {
|
} else if (config.type === 'boolean') {
|
||||||
onUpdate(newValue === 'true' || newValue === true)
|
finalValue = localValue === 'true' || localValue === true
|
||||||
} else {
|
}
|
||||||
onUpdate(newValue)
|
onUpdate(finalValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyPress = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleBlur()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayValue = config.type === 'number' && label.includes('PERCENT')
|
const displayValue = config.type === 'number' && label.includes('PERCENT')
|
||||||
? (value * 100).toFixed(2)
|
? (localValue * 100).toFixed(2)
|
||||||
: value
|
: localValue
|
||||||
|
|
||||||
if (config.type === 'boolean') {
|
if (config.type === 'boolean') {
|
||||||
return (
|
return (
|
||||||
<div className="config-item">
|
<div className="config-item">
|
||||||
<label>{label}</label>
|
<label>{label}</label>
|
||||||
<select
|
<select
|
||||||
value={value ? 'true' : 'false'}
|
value={localValue ? 'true' : 'false'}
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
onChange={(e) => {
|
||||||
|
handleChange(e.target.value)
|
||||||
|
// 布尔值立即保存
|
||||||
|
onUpdate(e.target.value === 'true')
|
||||||
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<option value="true">是</option>
|
<option value="true">是</option>
|
||||||
|
|
@ -125,8 +166,12 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
<div className="config-item">
|
<div className="config-item">
|
||||||
<label>{label}</label>
|
<label>{label}</label>
|
||||||
<select
|
<select
|
||||||
value={value}
|
value={localValue}
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
onChange={(e) => {
|
||||||
|
handleChange(e.target.value)
|
||||||
|
// 下拉框立即保存
|
||||||
|
onUpdate(e.target.value)
|
||||||
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{options.map(opt => (
|
{options.map(opt => (
|
||||||
|
|
@ -143,16 +188,20 @@ const ConfigItem = ({ label, config, onUpdate, disabled }) => {
|
||||||
<label>{label}</label>
|
<label>{label}</label>
|
||||||
<input
|
<input
|
||||||
type={config.type === 'number' ? 'number' : 'text'}
|
type={config.type === 'number' ? 'number' : 'text'}
|
||||||
value={label.includes('PERCENT') ? displayValue : value}
|
value={label.includes('PERCENT') ? displayValue : localValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = config.type === 'number' && label.includes('PERCENT')
|
const newValue = config.type === 'number' && label.includes('PERCENT')
|
||||||
? parseFloat(e.target.value) / 100
|
? parseFloat(e.target.value) / 100
|
||||||
: e.target.value
|
: e.target.value
|
||||||
handleChange(newValue)
|
handleChange(newValue)
|
||||||
}}
|
}}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
step={config.type === 'number' ? '0.01' : undefined}
|
step={config.type === 'number' ? '0.01' : undefined}
|
||||||
|
className={isEditing ? 'editing' : ''}
|
||||||
/>
|
/>
|
||||||
|
{isEditing && <span className="edit-hint">按Enter保存</span>}
|
||||||
{config.description && <span className="description">{config.description}</span>}
|
{config.description && <span className="description">{config.description}</span>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ export const api = {
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.detail || '更新配置失败');
|
||||||
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# 交易系统依赖(已移至 trading_system/requirements.txt)
|
|
||||||
# 如需运行交易系统,请安装:
|
|
||||||
# pip install -r trading_system/requirements.txt
|
|
||||||
|
|
||||||
# 后端API依赖(已移至 backend/requirements.txt)
|
|
||||||
# 如需运行后端API,请安装:
|
|
||||||
# pip install -r backend/requirements.txt
|
|
||||||
|
|
@ -49,8 +49,42 @@ python -m trading_system.main
|
||||||
|
|
||||||
配置文件:`config.py`
|
配置文件:`config.py`
|
||||||
|
|
||||||
## 依赖
|
## 安装依赖
|
||||||
|
|
||||||
|
### 方式1:使用安装脚本(推荐)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd trading_system
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式2:手动创建虚拟环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在项目根目录创建虚拟环境
|
||||||
|
cd ..
|
||||||
|
python3 -m venv .venv
|
||||||
|
|
||||||
|
# 激活虚拟环境
|
||||||
|
source .venv/bin/activate # Linux/Mac
|
||||||
|
# 或 .venv\Scripts\activate # Windows
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
cd trading_system
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 方式3:使用项目根目录的虚拟环境
|
||||||
|
|
||||||
|
如果项目根目录已有虚拟环境(与backend共享):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 激活虚拟环境
|
||||||
|
source ../.venv/bin/activate # 从trading_system目录
|
||||||
|
# 或 source .venv/bin/activate # 从项目根目录
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:现代 Linux 系统(如 Ubuntu 22.04+)不允许直接在系统 Python 中安装包,必须使用虚拟环境。
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,8 @@ def _get_trading_config():
|
||||||
}
|
}
|
||||||
|
|
||||||
# 币安API配置(优先从数据库,回退到环境变量和默认值)
|
# 币安API配置(优先从数据库,回退到环境变量和默认值)
|
||||||
BINANCE_API_KEY: Optional[str] = _get_config_value('BINANCE_API_KEY', 'pMEXSgISMgpUIpGjyhikMXWQ7K7cCs1FFATyIvNIwWrUIQegoipVBskPUoUuvaVN')
|
BINANCE_API_KEY: Optional[str] = _get_config_value('BINANCE_API_KEY', '')
|
||||||
BINANCE_API_SECRET: Optional[str] = _get_config_value('BINANCE_API_SECRET', 'RklItVtBCjGV40mIquoSj78xlTGkdUxz0AFyTnsnuzSBfx776VG0S2Vw5BRLRRg2')
|
BINANCE_API_SECRET: Optional[str] = _get_config_value('BINANCE_API_SECRET', '')
|
||||||
USE_TESTNET: bool = _get_config_value('USE_TESTNET', False) if _get_config_value('USE_TESTNET') is not None else os.getenv('USE_TESTNET', 'False').lower() == 'true'
|
USE_TESTNET: bool = _get_config_value('USE_TESTNET', False) if _get_config_value('USE_TESTNET') is not None else os.getenv('USE_TESTNET', 'False').lower() == 'true'
|
||||||
|
|
||||||
# 交易参数配置(优先从数据库读取,支持动态重载)
|
# 交易参数配置(优先从数据库读取,支持动态重载)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user