This commit is contained in:
薇薇安 2026-01-17 18:29:52 +08:00
parent 270a3cb8d8
commit 1adb3137d6
9 changed files with 442 additions and 52 deletions

View File

@ -5,14 +5,17 @@ from fastapi import APIRouter, HTTPException
from api.models.config import ConfigItem, ConfigUpdate
import sys
from pathlib import Path
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
logger = logging.getLogger(__name__)
router = APIRouter()
@ -171,3 +174,112 @@ async def update_configs_batch(configs: list[ConfigItem]):
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/feasibility-check")
async def check_config_feasibility():
"""
检查配置可行性基于当前账户余额和杠杆倍数计算可行的配置建议
"""
try:
# 获取账户余额
try:
from api.routes.account import get_realtime_account_data
account_data = await get_realtime_account_data()
available_balance = account_data.get('available_balance', 0)
total_balance = account_data.get('total_balance', 0)
except Exception as e:
logger.warning(f"获取账户余额失败: {e},使用默认值")
available_balance = 0
total_balance = 0
if available_balance <= 0:
return {
"feasible": False,
"error": "无法获取账户余额请检查API配置",
"suggestions": []
}
# 获取当前配置
min_margin_usdt = TradingConfig.get_value('MIN_MARGIN_USDT', 5.0)
min_position_percent = TradingConfig.get_value('MIN_POSITION_PERCENT', 0.02)
max_position_percent = TradingConfig.get_value('MAX_POSITION_PERCENT', 0.08)
leverage = TradingConfig.get_value('LEVERAGE', 10)
# 计算最小保证金要求对应的最小仓位价值
required_position_value = min_margin_usdt * leverage
required_position_percent = required_position_value / available_balance if available_balance > 0 else 0
# 检查是否可行
is_feasible = required_position_percent <= max_position_percent
suggestions = []
if not is_feasible:
# 不可行,给出建议
# 方案1降低最小保证金
suggested_min_margin = (available_balance * max_position_percent) / leverage
suggestions.append({
"type": "reduce_min_margin",
"title": "降低最小保证金",
"description": f"将 MIN_MARGIN_USDT 调整为 {suggested_min_margin:.2f} USDT当前: {min_margin_usdt:.2f} USDT",
"config_key": "MIN_MARGIN_USDT",
"suggested_value": round(suggested_min_margin, 2),
"reason": f"当前配置需要 {required_position_percent*100:.1f}% 的仓位价值,但最大允许 {max_position_percent*100:.1f}%"
})
# 方案2增加账户余额
required_balance = required_position_value / max_position_percent
suggestions.append({
"type": "increase_balance",
"title": "增加账户余额",
"description": f"将账户余额增加到至少 {required_balance:.2f} USDT当前: {available_balance:.2f} USDT",
"config_key": None,
"suggested_value": round(required_balance, 2),
"reason": f"当前余额不足以满足最小保证金 {min_margin_usdt:.2f} USDT 的要求"
})
# 方案3降低杠杆倍数
suggested_leverage = int((available_balance * max_position_percent) / min_margin_usdt)
if suggested_leverage >= 1:
suggestions.append({
"type": "reduce_leverage",
"title": "降低杠杆倍数",
"description": f"将 LEVERAGE 调整为 {suggested_leverage}x当前: {leverage}x",
"config_key": "LEVERAGE",
"suggested_value": suggested_leverage,
"reason": f"降低杠杆可以减少所需仓位价值"
})
else:
# 可行,显示当前配置信息
actual_min_position_value = available_balance * min_position_percent
actual_min_margin = actual_min_position_value / leverage if leverage > 0 else actual_min_position_value
suggestions.append({
"type": "info",
"title": "配置可行",
"description": f"当前配置可以正常下单。最小仓位价值: {actual_min_position_value:.2f} USDT对应保证金: {actual_min_margin:.2f} USDT",
"config_key": None,
"suggested_value": None,
"reason": None
})
return {
"feasible": is_feasible,
"account_balance": available_balance,
"leverage": leverage,
"current_config": {
"min_margin_usdt": min_margin_usdt,
"min_position_percent": min_position_percent,
"max_position_percent": max_position_percent
},
"calculated_values": {
"required_position_value": required_position_value,
"required_position_percent": required_position_percent * 100,
"max_allowed_position_percent": max_position_percent * 100
},
"suggestions": suggestions
}
except Exception as e:
logger.error(f"检查配置可行性失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"检查配置可行性失败: {str(e)}")

View File

@ -1,11 +1,78 @@
-- 添加交易统计字段
-- 用于记录策略类型和持仓持续时间
-- 兼容MySQL 5.7+使用动态SQL检查列和索引是否存在
ALTER TABLE `trades`
ADD COLUMN IF NOT EXISTS `strategy_type` VARCHAR(50) COMMENT '策略类型: trend_following, mean_reversion' AFTER `exit_reason`,
ADD COLUMN IF NOT EXISTS `duration_minutes` INT COMMENT '持仓持续时间(分钟)' AFTER `strategy_type`;
USE `auto_trade_sys`;
-- 添加索引以便统计查询
ALTER TABLE `trades`
ADD INDEX IF NOT EXISTS `idx_strategy_type` (`strategy_type`),
ADD INDEX IF NOT EXISTS `idx_exit_reason` (`exit_reason`);
-- 1. 添加 strategy_type 列(如果不存在)
SET @column_exists = (
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = 'auto_trade_sys'
AND table_name = 'trades'
AND column_name = 'strategy_type'
);
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `trades` ADD COLUMN `strategy_type` VARCHAR(50) COMMENT ''策略类型: trend_following, mean_reversion'' AFTER `exit_reason`',
'SELECT "strategy_type 列已存在,跳过添加" as message'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 2. 添加 duration_minutes 列(如果不存在)
SET @column_exists2 = (
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = 'auto_trade_sys'
AND table_name = 'trades'
AND column_name = 'duration_minutes'
);
SET @sql2 = IF(@column_exists2 = 0,
'ALTER TABLE `trades` ADD COLUMN `duration_minutes` INT COMMENT ''持仓持续时间(分钟)'' AFTER `strategy_type`',
'SELECT "duration_minutes 列已存在,跳过添加" as message'
);
PREPARE stmt2 FROM @sql2;
EXECUTE stmt2;
DEALLOCATE PREPARE stmt2;
-- 3. 添加 idx_strategy_type 索引(如果不存在)
SET @index_exists = (
SELECT COUNT(*)
FROM information_schema.statistics
WHERE table_schema = 'auto_trade_sys'
AND table_name = 'trades'
AND index_name = 'idx_strategy_type'
);
SET @sql3 = IF(@index_exists = 0,
'ALTER TABLE `trades` ADD INDEX `idx_strategy_type` (`strategy_type`)',
'SELECT "idx_strategy_type 索引已存在,跳过添加" as message'
);
PREPARE stmt3 FROM @sql3;
EXECUTE stmt3;
DEALLOCATE PREPARE stmt3;
-- 4. 添加 idx_exit_reason 索引(如果不存在)
SET @index_exists2 = (
SELECT COUNT(*)
FROM information_schema.statistics
WHERE table_schema = 'auto_trade_sys'
AND table_name = 'trades'
AND index_name = 'idx_exit_reason'
);
SET @sql4 = IF(@index_exists2 = 0,
'ALTER TABLE `trades` ADD INDEX `idx_exit_reason` (`exit_reason`)',
'SELECT "idx_exit_reason 索引已存在,跳过添加" as message'
);
PREPARE stmt4 FROM @sql4;
EXECUTE stmt4;
DEALLOCATE PREPARE stmt4;
-- 验证:检查表结构
-- SHOW CREATE TABLE `trades`;
-- 或者
-- DESCRIBE `trades`;

View File

@ -170,7 +170,11 @@ INSERT INTO `trading_config` (`config_key`, `config_value`, `config_type`, `cate
('TRAILING_STOP_PROTECT', '0.05', 'number', 'strategy', '移动止损保护利润保护5%利润,更合理)'),
-- 持仓同步
<<<<<<< Current (Your changes)
('POSITION_SYNC_INTERVAL', '300', 'number', 'scan', '持仓状态同步间隔默认5分钟用于同步币安实际持仓与数据库状态'),
=======
('POSITION_SYNC_INTERVAL', '60', 'number', 'scan', '持仓状态同步间隔默认1分钟用于同步币安实际持仓与数据库状态'),
>>>>>>> Incoming (Background Agent changes)
-- API配置

View File

@ -464,3 +464,139 @@
min-width: 44px;
}
}
/* 配置可行性检查样式 */
.feasibility-check {
margin-bottom: 1.5rem;
padding: 1rem;
border-radius: 8px;
border: 2px solid;
animation: slideIn 0.3s ease-out;
}
.feasibility-check.feasible {
background: #e8f5e9;
border-color: #4CAF50;
}
.feasibility-check.infeasible {
background: #fff3e0;
border-color: #FF9800;
}
.feasibility-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.feasibility-header h4 {
margin: 0;
font-size: 1.1rem;
color: #2c3e50;
}
.refresh-btn {
padding: 0.4rem 0.8rem;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.3s;
}
.refresh-btn:hover:not(:disabled) {
background: #f5f5f5;
border-color: #999;
}
.refresh-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.feasibility-info {
margin-bottom: 0.75rem;
font-size: 0.9rem;
color: #555;
line-height: 1.6;
}
.feasibility-info p {
margin: 0.25rem 0;
}
.feasibility-info strong {
color: #2c3e50;
font-weight: 600;
}
.warning-text {
color: #d32f2f !important;
font-weight: 500;
}
.feasibility-suggestions {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(0,0,0,0.1);
}
.feasibility-suggestions h5 {
margin: 0 0 0.75rem 0;
font-size: 1rem;
color: #2c3e50;
}
.suggestion-item {
background: white;
padding: 0.75rem;
border-radius: 6px;
margin-bottom: 0.75rem;
border: 1px solid #e0e0e0;
}
.suggestion-item:last-child {
margin-bottom: 0;
}
.suggestion-header {
margin-bottom: 0.5rem;
}
.suggestion-header strong {
color: #1976D2;
font-size: 0.95rem;
}
.suggestion-desc {
margin: 0.5rem 0;
font-size: 0.9rem;
color: #666;
line-height: 1.5;
}
.apply-suggestion-btn {
margin-top: 0.5rem;
padding: 0.5rem 1rem;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.3s;
}
.apply-suggestion-btn:hover {
background: #1976D2;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(33, 150, 243, 0.3);
}
.apply-suggestion-btn:active {
transform: translateY(0);
}

View File

@ -8,6 +8,8 @@ const ConfigPanel = () => {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState('')
const [feasibilityCheck, setFeasibilityCheck] = useState(null)
const [checkingFeasibility, setCheckingFeasibility] = useState(false)
//
// 使8.08%0.08
@ -64,8 +66,22 @@ const ConfigPanel = () => {
useEffect(() => {
loadConfigs()
checkFeasibility()
}, [])
const checkFeasibility = async () => {
setCheckingFeasibility(true)
try {
const result = await api.checkConfigFeasibility()
setFeasibilityCheck(result)
} catch (error) {
console.error('检查配置可行性失败:', error)
//
} finally {
setCheckingFeasibility(false)
}
}
const loadConfigs = async () => {
try {
const data = await api.getConfigs()
@ -257,6 +273,73 @@ const ConfigPanel = () => {
</div>
</div>
</div>
{/* 配置可行性检查提示 */}
{feasibilityCheck && (
<div className={`feasibility-check ${feasibilityCheck.feasible ? 'feasible' : 'infeasible'}`}>
<div className="feasibility-header">
<h4>
{feasibilityCheck.feasible ? '✓ 配置可行' : '⚠ 配置冲突'}
</h4>
<button
onClick={checkFeasibility}
disabled={checkingFeasibility}
className="refresh-btn"
title="重新检查"
>
{checkingFeasibility ? '检查中...' : '🔄 刷新'}
</button>
</div>
<div className="feasibility-info">
<p>
账户余额: <strong>{feasibilityCheck.account_balance?.toFixed(2) || 'N/A'}</strong> USDT |
杠杆: <strong>{feasibilityCheck.leverage}x</strong> |
最小保证金: <strong>{feasibilityCheck.current_config?.min_margin_usdt?.toFixed(2) || 'N/A'}</strong> USDT
</p>
{!feasibilityCheck.feasible && (
<p className="warning-text">
需要仓位价值: <strong>{feasibilityCheck.calculated_values?.required_position_percent?.toFixed(1)}%</strong> |
最大允许: <strong>{feasibilityCheck.calculated_values?.max_allowed_position_percent?.toFixed(1)}%</strong>
</p>
)}
</div>
{feasibilityCheck.suggestions && feasibilityCheck.suggestions.length > 0 && (
<div className="feasibility-suggestions">
<h5>建议方案</h5>
{feasibilityCheck.suggestions.map((suggestion, index) => (
<div key={index} className="suggestion-item">
<div className="suggestion-header">
<strong>{suggestion.title}</strong>
</div>
<p className="suggestion-desc">{suggestion.description}</p>
{suggestion.config_key && suggestion.suggested_value !== null && (
<button
className="apply-suggestion-btn"
onClick={async () => {
try {
await api.updateConfig(suggestion.config_key, {
value: suggestion.suggested_value,
type: 'number',
category: 'position'
})
setMessage(`已应用建议: ${suggestion.title}`)
await loadConfigs()
await checkFeasibility()
} catch (error) {
setMessage('应用建议失败: ' + error.message)
}
}}
>
应用此建议
</button>
)}
</div>
))}
</div>
)}
</div>
)}
{message && (
<div className={`message ${message.includes('失败') || message.includes('错误') ? 'error' : 'success'}`}>
{message}

View File

@ -19,6 +19,12 @@ const TradeList = () => {
useEffect(() => {
loadData()
// 30
const interval = setInterval(() => {
loadData()
}, 30000) // 30
return () => clearInterval(interval)
}, [])
const loadData = async () => {

View File

@ -54,6 +54,16 @@ export const api = {
return response.json();
},
// 检查配置可行性
checkConfigFeasibility: async () => {
const response = await fetch(buildUrl('/api/config/feasibility-check'));
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '检查配置可行性失败' }));
throw new Error(error.detail || '检查配置可行性失败');
}
return response.json();
},
// 交易记录
getTrades: async (params = {}) => {
const query = new URLSearchParams(params).toString();

View File

@ -189,7 +189,11 @@ def _get_trading_config():
'USE_TRAILING_STOP': True,
'TRAILING_STOP_ACTIVATION': 0.10, # 移动止损激活提高到10%盈利10%后激活,给趋势更多空间)
'TRAILING_STOP_PROTECT': 0.05, # 保护利润提高到5%保护5%利润,更合理)
<<<<<<< Current (Your changes)
'POSITION_SYNC_INTERVAL': 300, # 持仓状态同步间隔默认5分钟
=======
'POSITION_SYNC_INTERVAL': 60, # 持仓状态同步间隔缩短到1分钟确保状态及时同步
>>>>>>> Incoming (Background Agent changes)
}
# 币安API配置优先从数据库回退到环境变量和默认值

View File

@ -1894,57 +1894,25 @@ class PositionManager:
f"数量: {quantity:.4f}"
)
# 更新数据库
if DB_AVAILABLE and Trade:
trade_id = position_info.get('tradeId')
if trade_id:
try:
if position_info['side'] == 'BUY':
pnl = (current_price_float - entry_price) * quantity
else: # SELL
pnl = (entry_price - current_price_float) * quantity
logger.info(f"{symbol} [自动平仓] 更新数据库记录 (ID: {trade_id})...")
# 计算持仓持续时间和策略类型
entry_time = position_info.get('entryTime')
duration_minutes = None
if entry_time:
try:
from datetime import datetime
if isinstance(entry_time, str):
entry_dt = datetime.strptime(entry_time, '%Y-%m-%d %H:%M:%S')
else:
entry_dt = entry_time
exit_dt = get_beijing_time() # 使用北京时间计算持续时间
duration = exit_dt - entry_dt
duration_minutes = int(duration.total_seconds() / 60)
except Exception as e:
logger.debug(f"计算持仓持续时间失败: {e}")
strategy_type = position_info.get('strategyType', 'trend_following')
Trade.update_exit(
trade_id=trade_id,
exit_price=current_price_float,
exit_reason=exit_reason,
pnl=pnl,
pnl_percent=pnl_percent_margin, # 修复:使用 pnl_percent_margin 而不是 pnl_percent
strategy_type=strategy_type,
duration_minutes=duration_minutes
)
logger.info(f"{symbol} [自动平仓] ✓ 数据库记录已更新 (盈亏: {pnl:.2f} USDT, {pnl_percent_margin:.2f}% of margin)")
except Exception as e:
logger.error(f"{symbol} [自动平仓] ❌ 更新数据库记录失败: {e}")
import traceback
logger.error(f" 错误详情:\n{traceback.format_exc()}")
# 执行平仓(注意:这里会停止监控,所以先更新数据库)
# 执行平仓(让 close_position 统一处理数据库更新,避免重复更新和状态不一致)
logger.info(f"{symbol} [自动平仓] 正在执行平仓订单...")
success = await self.close_position(symbol, reason=exit_reason)
if success:
logger.info(f"{symbol} [自动平仓] ✓ 平仓成功完成")
# 平仓成功后,立即触发一次状态同步,确保数据库状态与币安一致
try:
await asyncio.sleep(2) # 等待2秒让币安订单完全成交
await self.sync_positions_with_binance()
logger.debug(f"{symbol} [自动平仓] 已触发状态同步")
except Exception as sync_error:
logger.warning(f"{symbol} [自动平仓] 状态同步失败: {sync_error}")
else:
logger.error(f"{symbol} [自动平仓] ❌ 平仓失败")
# 即使平仓失败,也尝试同步状态(可能币安已经平仓了)
try:
await self.sync_positions_with_binance()
except Exception as sync_error:
logger.warning(f"{symbol} [自动平仓] 状态同步失败: {sync_error}")
async def diagnose_position(self, symbol: str):
"""