""" 配置管理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} USDT(MIN_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))