This commit is contained in:
薇薇安 2026-01-13 20:43:43 +08:00
parent 186b2f2424
commit e5cea19b6a
10 changed files with 461 additions and 46 deletions

View File

@ -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

View File

@ -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` - 项目总结

View 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)}")

View File

@ -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,14 +115,48 @@ 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:
TradingConfig.set( try:
item.key, # 验证配置值
item.value, if item.type == 'number':
item.type, try:
item.category, float(item.value)
item.description except (ValueError, TypeError):
) errors.append(f"{item.key}: Invalid number value")
return {"message": f"{len(configs)} configs updated"} 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: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))

View File

@ -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;

View File

@ -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">
<h2>交易配置</h2> <div className="config-header">
{message && <div className={`message ${message.includes('失败') ? 'error' : 'success'}`}>{message}</div>} <h2>交易配置</h2>
<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)
if (config.type === 'number') { setIsEditing(true)
onUpdate(parseFloat(newValue) || 0) }
} else if (config.type === 'boolean') {
onUpdate(newValue === 'true' || newValue === true) const handleBlur = () => {
} else { setIsEditing(false)
onUpdate(newValue) if (localValue !== value) {
//
let finalValue = localValue
if (config.type === 'number') {
finalValue = parseFloat(localValue) || 0
//
if (label.includes('PERCENT')) {
finalValue = finalValue / 100
}
} else if (config.type === 'boolean') {
finalValue = localValue === 'true' || localValue === true
}
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>
) )

View File

@ -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();
}, },

View File

@ -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

View File

@ -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 中安装包,必须使用虚拟环境。

View File

@ -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'
# 交易参数配置(优先从数据库读取,支持动态重载) # 交易参数配置(优先从数据库读取,支持动态重载)