auto_trade_sys/backend/api/routes/config.py
薇薇安 fad6950964 a
2026-01-19 23:25:12 +08:00

557 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
配置管理API
"""
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()
# 智能入场方案C配置为了“配置页可见”即使数据库尚未创建也在 GET /api/config 返回默认项
SMART_ENTRY_CONFIG_DEFAULTS = {
"SMART_ENTRY_ENABLED": {
"value": False,
"type": "boolean",
"category": "strategy",
"description": "智能入场开关。关闭后回归“纯限价单模式”(不追价/不市价兜底/未成交则撤单跳过),更适合低频波段。",
},
"SMART_ENTRY_STRONG_SIGNAL": {
"value": 8,
"type": "number",
"category": "strategy",
"description": "强信号阈值0-10。≥该值且4H趋势明确时允许更积极的入场可控市价兜底",
},
"ENTRY_SYMBOL_COOLDOWN_SEC": {
"value": 120,
"type": "number",
"category": "strategy",
"description": "同一交易对入场冷却时间(秒)。避免短时间内反复挂单/重入导致高频噪音单。",
},
"ENTRY_TIMEOUT_SEC": {
"value": 180,
"type": "number",
"category": "strategy",
"description": "智能入场总预算时间(秒)。超过预算仍未成交将根据规则取消/兜底。",
},
"ENTRY_STEP_WAIT_SEC": {
"value": 15,
"type": "number",
"category": "strategy",
"description": "每次追价/调整前等待成交时间(秒)。",
},
"ENTRY_CHASE_MAX_STEPS": {
"value": 4,
"type": "number",
"category": "strategy",
"description": "最大追价步数(逐步减小限价回调幅度,靠近当前价)。",
},
"ENTRY_MARKET_FALLBACK_AFTER_SEC": {
"value": 45,
"type": "number",
"category": "strategy",
"description": "趋势强时:超过该时间仍未成交,会在偏离不超过上限时转市价兜底(减少错过)。",
},
"ENTRY_CONFIRM_TIMEOUT_SEC": {
"value": 30,
"type": "number",
"category": "strategy",
"description": "下单后确认成交等待时间(秒)。",
},
"ENTRY_MAX_DRIFT_PCT_TRENDING": {
"value": 0.006, # 0.6%
"type": "number",
"category": "strategy",
"description": "趋势强时最大追价偏离(%)。例如 0.6 表示 0.6%。越小越保守。",
},
"ENTRY_MAX_DRIFT_PCT_RANGING": {
"value": 0.003, # 0.3%
"type": "number",
"category": "strategy",
"description": "震荡/弱趋势最大追价偏离(%)。例如 0.3 表示 0.3%。越小越保守。",
},
}
# 自动交易过滤项:用于“提升胜率/控频”,避免震荡行情来回扫损导致胜率极低
AUTO_TRADE_FILTER_DEFAULTS = {
"AUTO_TRADE_ONLY_TRENDING": {
"value": True,
"type": "boolean",
"category": "strategy",
"description": "自动交易仅在市场状态=trending时执行ranging/unknown只生成推荐不自动下单。用于显著降低震荡扫损与交易次数。",
},
"AUTO_TRADE_ALLOW_4H_NEUTRAL": {
"value": False,
"type": "boolean",
"category": "strategy",
"description": "是否允许4H趋势=neutral时自动交易。默认关闭中性趋势最容易被扫损若你希望更积极可开启。",
},
}
@router.get("")
@router.get("/")
async def get_all_configs():
"""获取所有配置"""
try:
configs = TradingConfig.get_all()
result = {}
for config in configs:
result[config['config_key']] = {
'value': TradingConfig._convert_value(
config['config_value'],
config['config_type']
),
'type': config['config_type'],
'category': config['category'],
'description': config['description']
}
# 合并“默认但未入库”的配置项(用于新功能上线时直接在配置页可见)
for k, meta in SMART_ENTRY_CONFIG_DEFAULTS.items():
if k not in result:
result[k] = meta
for k, meta in AUTO_TRADE_FILTER_DEFAULTS.items():
if k not in result:
result[k] = meta
return result
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)
base_leverage = TradingConfig.get_value('LEVERAGE', 10)
max_leverage = TradingConfig.get_value('MAX_LEVERAGE', 15)
use_dynamic_leverage = TradingConfig.get_value('USE_DYNAMIC_LEVERAGE', True)
# 检查所有可能的杠杆倍数(考虑动态杠杆)
leverage_to_check = [base_leverage]
if use_dynamic_leverage and max_leverage > base_leverage:
# 检查基础杠杆、最大杠杆以及中间的关键值如15x, 20x, 30x, 40x
leverage_to_check.append(max_leverage)
# 添加常见的杠杆倍数
for lev in [15, 20, 25, 30, 40, 50]:
if base_leverage < lev <= max_leverage and lev not in leverage_to_check:
leverage_to_check.append(lev)
leverage_to_check = sorted(set(leverage_to_check))
# 检查每个杠杆倍数下的可行性
leverage_results = []
all_feasible = True
worst_case_leverage = None
worst_case_margin = None
for leverage in leverage_to_check:
# 重要语义说明(与 trading_system 保持一致):
# - MAX/MIN_POSITION_PERCENT 表示“保证金占用比例”(不是名义价值比例)
# - MIN_MARGIN_USDT 是最小保证金USDT
# 同时考虑币安最小名义价值约束:名义价值>=5USDT => margin >= 5/leverage
min_notional = 5.0
required_margin_for_notional = (min_notional / leverage) if leverage and leverage > 0 else min_notional
required_position_value = max(min_margin_usdt, required_margin_for_notional) # 实际需要的最小保证金USDT
required_position_percent = required_position_value / available_balance if available_balance > 0 else 0
# 计算使用最小仓位百分比时,实际能得到的保证金(直接就是保证金)
min_position_value = available_balance * min_position_percent
actual_min_margin = min_position_value
# 检查是否可行:
# 1) 最大保证金占比是否能满足 required_position_value
# 2) 如果 MIN_POSITION_PERCENT > 0则最小保证金占比也应 >= required_position_value否则最小仓位配置会“阻碍”满足最小保证金/最小名义价值)
condition1_ok = required_position_percent <= max_position_percent
condition2_ok = (min_position_percent <= 0) or (actual_min_margin >= required_position_value)
is_feasible_at_leverage = condition1_ok and condition2_ok
leverage_results.append({
'leverage': leverage,
'required_position_value': required_position_value, # 实际需要的最小保证金USDT
'required_position_percent': required_position_percent * 100,
'actual_min_margin': actual_min_margin,
'feasible': is_feasible_at_leverage,
'condition1_ok': condition1_ok,
'condition2_ok': condition2_ok
})
if not is_feasible_at_leverage:
all_feasible = False
# 记录最坏情况(实际保证金最小的)
if worst_case_margin is None or actual_min_margin < worst_case_margin:
worst_case_leverage = leverage
worst_case_margin = actual_min_margin
# 使用基础杠杆的结果作为主要判断
base_result = next((r for r in leverage_results if r['leverage'] == base_leverage), leverage_results[0])
is_feasible = all_feasible
suggestions = []
if not is_feasible:
# 不可行,给出建议
# 找出不可行的杠杆倍数
infeasible_leverages = [r for r in leverage_results if not r['feasible']]
if infeasible_leverages:
# 找出最坏情况(实际保证金最小的)
worst = min(infeasible_leverages, key=lambda x: x['actual_min_margin'])
worst_leverage = worst['leverage']
worst_margin = worst['actual_min_margin']
# 方案1基于最坏情况最大杠杆降低最小保证金
# 建议值应该比计算值小一点确保可行比如0.51 -> 0.5
if not worst['condition2_ok']:
# 计算建议值比实际支持值小0.01-0.05,确保可行
suggested_margin = max(0.01, worst_margin - 0.01) # 至少保留0.01的余量
# 如果差值较大,可以多减一点
if worst_margin > 1.0:
suggested_margin = worst_margin - 0.05
# 保留2位小数向下取整
suggested_margin = round(suggested_margin - 0.005, 2) # 减0.005确保向下取整
if suggested_margin < 0.01:
suggested_margin = 0.01
suggestions.append({
"type": "reduce_min_margin_to_supported",
"title": f"降低最小保证金(考虑{worst_leverage}x杠杆",
"description": f"将 MIN_MARGIN_USDT 调整为 {suggested_margin:.2f} USDT当前: {min_margin_usdt:.2f} USDT。在{worst_leverage}x杠杆下实际支持 {worst_margin:.2f} USDT建议设置为 {suggested_margin:.2f} USDT 以确保可行",
"config_key": "MIN_MARGIN_USDT",
"suggested_value": suggested_margin,
"reason": f"{worst_leverage}x杠杆下使用最小仓位 {min_position_percent*100:.1f}% 时,实际保证金只有 {worst_margin:.2f} USDT无法满足 {min_margin_usdt:.2f} USDT 的要求"
})
# 方案1b增加最小仓位百分比作为替代方案
# 计算需要的最小仓位百分比min_position_percent >= (min_margin_usdt * leverage) / available_balance
required_min_position_percent = (min_margin_usdt * worst_leverage) / available_balance if available_balance > 0 else 0
# 建议值应该比计算值大一点确保可行比如0.0102 -> 0.011
suggested_min_position_percent = required_min_position_percent + 0.001 # 多0.1%
# 如果差值较大,可以多加一点
if required_min_position_percent > 0.02:
suggested_min_position_percent = required_min_position_percent + 0.002 # 多0.2%
# 确保不超过最大仓位百分比
if suggested_min_position_percent > max_position_percent:
suggested_min_position_percent = max_position_percent
# 保留4位小数
suggested_min_position_percent = round(suggested_min_position_percent, 4)
if suggested_min_position_percent > min_position_percent and suggested_min_position_percent <= max_position_percent:
suggestions.append({
"type": "increase_min_position_percent",
"title": f"增加最小仓位百分比(考虑{worst_leverage}x杠杆",
"description": f"将 MIN_POSITION_PERCENT 调整为 {suggested_min_position_percent*100:.2f}%(当前: {min_position_percent*100:.2f}%)。在{worst_leverage}x杠杆下需要至少 {required_min_position_percent*100:.2f}% 才能满足最小保证金 {min_margin_usdt:.2f} USDT 的要求,建议设置为 {suggested_min_position_percent*100:.2f}% 以确保可行",
"config_key": "MIN_POSITION_PERCENT",
"suggested_value": suggested_min_position_percent,
"reason": f"{worst_leverage}x杠杆下当前最小仓位 {min_position_percent*100:.2f}% 只能提供 {worst_margin:.2f} USDT 保证金,需要至少 {required_min_position_percent*100:.2f}% 才能满足 {min_margin_usdt:.2f} USDT 的要求"
})
# 方案2基于最大杠杆计算需要的最小保证金
max_leverage_result = next((r for r in leverage_results if r['leverage'] == max_leverage), None)
if max_leverage_result and not max_leverage_result['feasible']:
if max_leverage_result['required_position_percent'] > max_position_percent:
suggested_min_margin_max_lev = (available_balance * max_position_percent) / max_leverage
# 同样,建议值应该比计算值小一点
suggested_margin_max_lev = max(0.01, suggested_min_margin_max_lev - 0.01)
if suggested_min_margin_max_lev > 1.0:
suggested_margin_max_lev = suggested_min_margin_max_lev - 0.05
suggested_margin_max_lev = round(suggested_margin_max_lev - 0.005, 2)
if suggested_margin_max_lev < 0.01:
suggested_margin_max_lev = 0.01
suggestions.append({
"type": "reduce_min_margin_for_max_leverage",
"title": f"降低最小保证金(支持{max_leverage}x杠杆",
"description": f"将 MIN_MARGIN_USDT 调整为 {suggested_margin_max_lev:.2f} USDT当前: {min_margin_usdt:.2f} USDT以支持{max_leverage}x杠杆",
"config_key": "MIN_MARGIN_USDT",
"suggested_value": suggested_margin_max_lev,
"reason": f"{max_leverage}x杠杆下需要 {max_leverage_result['required_position_percent']:.1f}% 的仓位价值,但最大允许 {max_position_percent*100:.1f}%"
})
# 方案2b如果condition2不满足也可以建议增加最小仓位百分比
if not max_leverage_result['condition2_ok']:
required_min_position_percent_max = (min_margin_usdt * max_leverage) / available_balance if available_balance > 0 else 0
suggested_min_position_percent_max = required_min_position_percent_max + 0.001
if required_min_position_percent_max > 0.02:
suggested_min_position_percent_max = required_min_position_percent_max + 0.002
if suggested_min_position_percent_max > max_position_percent:
suggested_min_position_percent_max = max_position_percent
suggested_min_position_percent_max = round(suggested_min_position_percent_max, 4)
if suggested_min_position_percent_max > min_position_percent and suggested_min_position_percent_max <= max_position_percent:
suggestions.append({
"type": "increase_min_position_percent_for_max_leverage",
"title": f"增加最小仓位百分比(支持{max_leverage}x杠杆",
"description": f"将 MIN_POSITION_PERCENT 调整为 {suggested_min_position_percent_max*100:.2f}%(当前: {min_position_percent*100:.2f}%),以支持{max_leverage}x杠杆下的最小保证金要求",
"config_key": "MIN_POSITION_PERCENT",
"suggested_value": suggested_min_position_percent_max,
"reason": f"{max_leverage}x杠杆下需要至少 {required_min_position_percent_max*100:.2f}% 的最小仓位才能满足 {min_margin_usdt:.2f} USDT 的要求"
})
# 方案3降低最大杠杆
if use_dynamic_leverage:
# 计算能支持的最大杠杆
max_supported_leverage = int((available_balance * max_position_percent) / min_margin_usdt)
if max_supported_leverage < max_leverage and max_supported_leverage >= base_leverage:
suggestions.append({
"type": "reduce_max_leverage",
"title": "降低最大杠杆倍数",
"description": f"将 MAX_LEVERAGE 调整为 {max_supported_leverage}x当前: {max_leverage}x以支持当前配置",
"config_key": "MAX_LEVERAGE",
"suggested_value": max_supported_leverage,
"reason": f"当前配置下,最大只能支持 {max_supported_leverage}x 杠杆才能满足最小保证金要求"
})
# 方案4增加账户余额
if worst['required_position_percent'] > max_position_percent:
required_balance = worst['required_position_value'] / max_position_percent
suggestions.append({
"type": "increase_balance",
"title": "增加账户余额",
"description": f"将账户余额增加到至少 {required_balance:.2f} USDT当前: {available_balance:.2f} USDT以支持{worst_leverage}x杠杆",
"config_key": None,
"suggested_value": round(required_balance, 2),
"reason": f"{worst_leverage}x杠杆下当前余额不足以满足最小保证金 {min_margin_usdt:.2f} USDT 的要求"
})
# 显示所有杠杆倍数的检查结果
suggestions.append({
"type": "leverage_analysis",
"title": "各杠杆倍数检查结果",
"description": "详细检查结果见下方",
"config_key": None,
"suggested_value": None,
"reason": None,
"leverage_results": leverage_results
})
else:
# 可行,显示当前配置信息
actual_min_position_value = available_balance * min_position_percent
actual_min_margin = base_result['actual_min_margin']
suggestions.append({
"type": "info",
"title": "配置可行",
"description": f"当前配置可以正常下单。最小保证金占比对应保证金: {actual_min_margin:.2f} USDTMIN_POSITION_PERCENT={min_position_percent*100:.2f}%),最小保证金要求: {min_margin_usdt:.2f} USDT",
"config_key": None,
"suggested_value": None,
"reason": None
})
# 如果启用了动态杠杆,显示所有杠杆倍数的检查结果
if use_dynamic_leverage and len(leverage_to_check) > 1:
suggestions.append({
"type": "leverage_analysis",
"title": "各杠杆倍数检查结果(全部可行)",
"description": "详细检查结果见下方",
"config_key": None,
"suggested_value": None,
"reason": None,
"leverage_results": leverage_results
})
return {
"feasible": is_feasible,
"account_balance": available_balance,
"base_leverage": base_leverage,
"max_leverage": max_leverage,
"use_dynamic_leverage": use_dynamic_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": base_result['required_position_value'],
"required_position_percent": base_result['required_position_percent'],
"max_allowed_position_percent": max_position_percent * 100,
"min_position_value": available_balance * min_position_percent,
"actual_min_margin": base_result['actual_min_margin'],
"min_position_percent": min_position_percent * 100
},
"leverage_analysis": {
"leverages_checked": leverage_to_check,
"all_feasible": all_feasible,
"results": leverage_results
},
"suggestions": suggestions
}
except Exception as e:
logger.error(f"检查配置可行性失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=f"检查配置可行性失败: {str(e)}")
@router.get("/{key}")
async def get_config(key: str):
"""获取单个配置"""
try:
config = TradingConfig.get(key)
if not config:
raise HTTPException(status_code=404, detail="Config not found")
return {
'key': config['config_key'],
'value': TradingConfig._convert_value(
config['config_value'],
config['config_type']
),
'type': config['config_type'],
'category': config['category'],
'description': config['description']
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{key}")
async def update_config(key: str, item: ConfigUpdate):
"""更新配置"""
try:
# 获取现有配置以确定类型和分类
existing = TradingConfig.get(key)
if existing:
config_type = item.type or existing['config_type']
category = item.category or existing['category']
description = item.description or existing['description']
else:
# 允许创建新配置用于新功能首次上线DB 里还没有 key 的情况)
meta = SMART_ENTRY_CONFIG_DEFAULTS.get(key) or AUTO_TRADE_FILTER_DEFAULTS.get(key)
config_type = item.type or (meta.get("type") if meta else "string")
category = item.category or (meta.get("category") if meta else "strategy")
description = item.description or (meta.get("description") if meta else f"{key}配置")
# 验证配置值
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之间
# 兼容PERCENT / PCT
if ('PERCENT' in key or 'PCT' 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%)"
)
# 更新配置会同时更新数据库和Redis缓存
TradingConfig.set(key, item.value, config_type, category, description)
# 更新config_manager的缓存包括Redis
try:
import config_manager
if hasattr(config_manager, 'config_manager') and config_manager.config_manager:
# 调用set方法会同时更新数据库、Redis和本地缓存
config_manager.config_manager.set(key, item.value, config_type, category, description)
logger.info(f"配置已更新到Redis缓存: {key} = {item.value}")
except Exception as e:
logger.warning(f"更新配置缓存失败: {e}")
return {
"message": "配置已更新",
"key": key,
"value": item.value,
"note": "配置已同步到Redis交易系统将立即使用新配置"
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/batch")
async def update_configs_batch(configs: list[ConfigItem]):
"""批量更新配置"""
try:
updated_count = 0
errors = []
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
# 特殊验证:百分比配置(兼容 PERCENT / PCT
if ('PERCENT' in item.key or 'PCT' 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))