Added new configurations for a layered profit locking system, allowing for gradual profit protection at specified thresholds. Introduced manual trading options, including reduced and blocked symbol lists, to enhance trading strategy flexibility. Updated relevant backend and frontend components to reflect these changes, improving risk management and user control over trading activities.
1591 lines
77 KiB
Python
1591 lines
77 KiB
Python
"""
|
||
配置管理API
|
||
"""
|
||
from fastapi import APIRouter, HTTPException, Header, Depends, Query
|
||
from api.models.config import ConfigItem, ConfigUpdate
|
||
import sys
|
||
from pathlib import Path
|
||
import logging
|
||
from typing import Dict, Any, Optional
|
||
|
||
# 添加项目根目录到路径
|
||
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, Account
|
||
from api.auth_deps import get_current_user, get_account_id, require_admin, require_account_owner
|
||
|
||
logger = logging.getLogger(__name__)
|
||
router = APIRouter()
|
||
|
||
# 全局策略账号(管理员统一维护策略核心)。默认 1,可用环境变量覆盖。
|
||
def _global_strategy_account_id() -> int:
|
||
try:
|
||
return int((__import__("os").getenv("ATS_GLOBAL_STRATEGY_ACCOUNT_ID") or "1").strip() or "1")
|
||
except Exception:
|
||
return 1
|
||
|
||
# 产品模式:平台兜底(策略核心由管理员统一控制),普通用户仅能调“风险旋钮”
|
||
# - admin:可修改所有配置
|
||
# - 非 admin(account owner):只允许修改少量风险类配置 + 账号私有密钥/测试网
|
||
USER_RISK_KNOBS = {
|
||
# 风险暴露(保证金占用比例/最小保证金)
|
||
"MIN_MARGIN_USDT",
|
||
"MIN_POSITION_PERCENT",
|
||
"MAX_POSITION_PERCENT",
|
||
"MAX_TOTAL_POSITION_PERCENT",
|
||
# 行为控制(傻瓜化)
|
||
"AUTO_TRADE_ENABLED", # 总开关:关闭则只生成推荐不自动下单
|
||
"MAX_OPEN_POSITIONS", # 同时持仓数量上限
|
||
"MAX_DAILY_ENTRIES", # 每日最多开仓次数
|
||
"SUNDAY_MAX_OPENS", # 周日开仓上限(0=不限制)
|
||
"SUNDAY_MIN_SIGNAL_STRENGTH", # 周日最低信号强度(0=不提高)
|
||
"NIGHT_HOURS_NO_OPEN_ENABLED",
|
||
"NIGHT_HOURS_START",
|
||
"NIGHT_HOURS_END",
|
||
"NIGHT_HOURS_ONLY_SUNDAY",
|
||
"NO_OPEN_HOURS_BJ",
|
||
# 策略筛选(允许用户调整)
|
||
"TOP_N_SYMBOLS",
|
||
"MIN_SIGNAL_STRENGTH",
|
||
"MIN_VOLUME_24H",
|
||
"MIN_VOLATILITY",
|
||
"SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT",
|
||
"EXCLUDE_MAJOR_COINS",
|
||
# 用户自定义扫描参数
|
||
"MAX_SCAN_SYMBOLS",
|
||
"SCAN_INTERVAL",
|
||
}
|
||
|
||
# 普通用户/关联账号可见的配置项默认值(含 category),确保「市场扫描」「仓位控制」「风险控制」分组都能展示
|
||
USER_VISIBLE_DEFAULTS = {
|
||
"MIN_MARGIN_USDT": {"value": 5.0, "type": "number", "category": "risk", "description": "最小单笔保证金(USDT),低于此值不下单"},
|
||
"MIN_POSITION_PERCENT": {"value": 0.0, "type": "number", "category": "position", "description": "最小单笔保证金占比(%),0 表示不限制"},
|
||
"MAX_POSITION_PERCENT": {"value": 2.0, "type": "number", "category": "position", "description": "单笔最大保证金占比(%)"},
|
||
"MAX_TOTAL_POSITION_PERCENT": {"value": 20.0, "type": "number", "category": "position", "description": "总仓位最大保证金占比(%)"},
|
||
"AUTO_TRADE_ENABLED": {"value": True, "type": "boolean", "category": "risk", "description": "自动交易总开关:关闭后仅生成推荐,不会自动下单"},
|
||
"MAX_OPEN_POSITIONS": {"value": 3, "type": "number", "category": "position", "description": "同时持仓数量上限"},
|
||
"MAX_DAILY_ENTRIES": {"value": 8, "type": "number", "category": "risk", "description": "每日最多开仓次数"},
|
||
"SUNDAY_MAX_OPENS": {"value": 3, "type": "number", "category": "risk", "description": "周日最多开仓次数,0=不限制(与每日一致)"},
|
||
"SUNDAY_MIN_SIGNAL_STRENGTH": {"value": 8, "type": "number", "category": "risk", "description": "周日最低信号强度(0-10),0=不提高门槛"},
|
||
"NIGHT_HOURS_NO_OPEN_ENABLED": {"value": True, "type": "boolean", "category": "risk", "description": "晚间/凌晨(21:00~06:00北京)禁止开仓"},
|
||
"NIGHT_HOURS_START": {"value": 21, "type": "number", "category": "risk", "description": "禁止开仓开始小时(含)"},
|
||
"NIGHT_HOURS_END": {"value": 6, "type": "number", "category": "risk", "description": "禁止开仓结束小时(不含)"},
|
||
"NIGHT_HOURS_ONLY_SUNDAY": {"value": True, "type": "boolean", "category": "risk", "description": "True=仅周六21:00~周日06:00;False=每天"},
|
||
"NO_OPEN_HOURS_BJ": {"value": "", "type": "string", "category": "risk", "description": "禁止开仓小时(北京),逗号分隔如17,19,22;空则不限制"},
|
||
"TOP_N_SYMBOLS": {"value": 8, "type": "number", "category": "scan", "description": "每次扫描后优先处理的交易对数量"},
|
||
"MIN_SIGNAL_STRENGTH": {"value": 8, "type": "number", "category": "scan", "description": "最小信号强度(0-10)"},
|
||
"MIN_VOLUME_24H": {"value": 30_000_000, "type": "number", "category": "scan", "description": "24h 成交量下限(USD)"},
|
||
"MIN_VOLATILITY": {"value": 3.0, "type": "number", "category": "scan", "description": "最小波动率过滤"},
|
||
"SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT": {"value": 8, "type": "number", "category": "scan", "description": "智能补单多返回的候选数量"},
|
||
"EXCLUDE_MAJOR_COINS": {"value": True, "type": "boolean", "category": "scan", "description": "是否排除主流币(BTC/ETH 等)"},
|
||
"MAX_SCAN_SYMBOLS": {"value": 1500, "type": "number", "category": "scan", "description": "最大扫描交易对数量"},
|
||
"SCAN_INTERVAL": {"value": 1800, "type": "number", "category": "scan", "description": "市场扫描间隔(秒)"},
|
||
"MANUAL_REDUCED_SYMBOLS": {"value": "", "type": "string", "category": "strategy", "description": "手动减仓交易对列表,逗号/空格/换行分隔。命中后缩小仓位并提高自动交易门槛。"},
|
||
"MANUAL_BLOCKED_SYMBOLS": {"value": "", "type": "string", "category": "strategy", "description": "手动停做交易对列表,逗号/空格/换行分隔。命中后禁止自动开仓。"},
|
||
"MANUAL_REDUCED_SYMBOL_POSITION_FACTOR": {"value": 0.5, "type": "number", "category": "strategy", "description": "手动减仓交易对的仓位系数。0.5 表示只开正常仓位的一半。"},
|
||
"MANUAL_REDUCED_SYMBOL_SIGNAL_BOOST": {"value": 1, "type": "number", "category": "strategy", "description": "手动减仓交易对额外提高的最小信号门槛。1 表示在当前基础上 +1。"},
|
||
}
|
||
|
||
RISK_KNOBS_DEFAULTS = {
|
||
"AUTO_TRADE_ENABLED": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "risk",
|
||
"description": "自动交易总开关:关闭后仅生成推荐,不会自动下单(适合先观察/体验)。",
|
||
},
|
||
"MAX_OPEN_POSITIONS": {
|
||
"value": 3,
|
||
"type": "number",
|
||
"category": "risk",
|
||
"description": "同时持仓数量上限(防止仓位过多/难管理)。建议 1-5。",
|
||
},
|
||
"MAX_DAILY_ENTRIES": {
|
||
"value": 8,
|
||
"type": "number",
|
||
"category": "risk",
|
||
"description": "每日最多开仓次数(防止高频下单/过度交易)。建议 3-15。",
|
||
},
|
||
"SUNDAY_MAX_OPENS": {
|
||
"value": 3,
|
||
"type": "number",
|
||
"category": "risk",
|
||
"description": "周日最多开仓次数。0=不限制(与每日一致);设为 2-3 可降低周日亏损又不完全停单。",
|
||
},
|
||
"SUNDAY_MIN_SIGNAL_STRENGTH": {
|
||
"value": 8,
|
||
"type": "number",
|
||
"category": "risk",
|
||
"description": "周日最低信号强度(0-10)。仅当信号强度>=此值才开仓,0=不提高(与平日一致)。建议 8~9。",
|
||
},
|
||
"NIGHT_HOURS_NO_OPEN_ENABLED": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "risk",
|
||
"description": "晚间/凌晨禁止开新仓。开启后 21:00~06:00(北京时间)不开新仓,亏损多从昨晚21点后开始。",
|
||
},
|
||
"NIGHT_HOURS_START": {"value": 21, "type": "number", "category": "risk", "description": "禁止开仓开始小时(北京),含"},
|
||
"NIGHT_HOURS_END": {"value": 6, "type": "number", "category": "risk", "description": "禁止开仓结束小时(北京),不含"},
|
||
"NIGHT_HOURS_ONLY_SUNDAY": {"value": True, "type": "boolean", "category": "risk", "description": "仅周日夜间。True=周六21:00~周日06:00;False=每天。"},
|
||
"NO_OPEN_HOURS_BJ": {"value": "", "type": "string", "category": "risk", "description": "禁止开仓小时(北京),逗号分隔如 17,19,22 表示 17/19/22 时不开新仓;空则不限制。按历史亏损时段配置。"},
|
||
}
|
||
|
||
# API key/secret 脱敏
|
||
def _mask(s: str) -> str:
|
||
s = "" if s is None else str(s)
|
||
if not s:
|
||
return ""
|
||
if len(s) <= 8:
|
||
return "****"
|
||
return f"{s[:4]}...{s[-4:]}"
|
||
# 智能入场(方案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": "下单后确认成交等待时间(秒)。",
|
||
},
|
||
"ALGO_ORDER_TIMEOUT_SEC": {
|
||
"value": 45,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "Algo 条件单(止损/止盈)单次请求超时(秒)。币安接口高负载时易超时,可调大至 60。",
|
||
},
|
||
"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_RANGING": {
|
||
"value": False,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "当不强制 only_trending 时,是否允许在震荡市自动交易。默认关闭,避免在横盘里来回扫损。",
|
||
},
|
||
"AUTO_TRADE_ALLOW_UNKNOWN": {
|
||
"value": False,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "当市场状态=unknown(K线不足或未判定)时,是否允许自动交易。默认关闭,更稳健。",
|
||
},
|
||
"AUTO_TRADE_ALLOW_4H_NEUTRAL": {
|
||
"value": False,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "是否允许4H趋势=neutral时自动交易。默认关闭(中性趋势最容易被扫损);若你希望更积极可开启。",
|
||
},
|
||
"RANGING_MARKET_SIGNAL_BOOST": {
|
||
"value": 2,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "震荡市额外提高的最小信号门槛。比如 2 表示在基础 MIN_SIGNAL_STRENGTH 上再 +2。",
|
||
},
|
||
}
|
||
|
||
# 风险/策略预设(用于一键切换“稳健 / 快速验证”等模式)
|
||
PROFILE_CONFIG_DEFAULTS = {
|
||
"TRADING_PROFILE": {
|
||
"value": "conservative",
|
||
"type": "string",
|
||
"category": "strategy",
|
||
"description": "交易预设:conservative(稳健,低频+高门槛) / fast(快速验证,高频+宽松过滤)。仅作为默认值,具体参数仍可单独调整。",
|
||
},
|
||
}
|
||
|
||
|
||
# 核心策略参数(仅管理员可见/在全局策略账号中修改)
|
||
CORE_STRATEGY_CONFIG_DEFAULTS = {
|
||
"ATR_STOP_LOSS_MULTIPLIER": {
|
||
"value": 2.0, # 2026-01-29优化:从1.5提高到2.0,减少被正常波动扫出
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "ATR止损倍数。建议 2.0-3.0。放宽止损可以给波动留出空间,提高胜率。2026-01-29优化:默认值从1.5提高到2.0。",
|
||
},
|
||
"ATR_TAKE_PROFIT_MULTIPLIER": {
|
||
"value": 1.5,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "ATR止盈倍数。建议 1.0-2.0。对应盈亏比 1:1 到 2:1,更容易触及目标。",
|
||
},
|
||
"RISK_REWARD_RATIO": {
|
||
"value": 1.5,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "目标盈亏比(止损距离的倍数)。配合ATR止盈使用。",
|
||
},
|
||
"MIN_STOP_LOSS_PRICE_PCT": {
|
||
"value": 0.03,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "最小止损距离(%)。例如 0.03 表示 3%。适当放宽止损,减少在正常波动中被扫损。",
|
||
},
|
||
"MIN_TAKE_PROFIT_PRICE_PCT": {
|
||
"value": 0.02,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "最小止盈距离(%)。例如 0.02 表示 2%。防止止盈过近。",
|
||
},
|
||
"POSITION_DETAILED_LOG_ENABLED": {
|
||
"value": False,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "是否启用持仓详细监控日志(记录每次检查的当前价/止损/止盈/ROE 等)。仅用于排查问题时临时打开,平时建议关闭以减少日志噪音。",
|
||
},
|
||
}
|
||
|
||
|
||
@router.get("")
|
||
@router.get("/")
|
||
async def get_all_configs(
|
||
user: Dict[str, Any] = Depends(get_current_user),
|
||
account_id: int = Depends(get_account_id),
|
||
):
|
||
"""获取所有配置"""
|
||
try:
|
||
configs = TradingConfig.get_all(account_id=account_id)
|
||
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']
|
||
}
|
||
|
||
# 合并账号级 API Key/Secret(从 accounts 表读,避免把密钥当普通配置存)
|
||
try:
|
||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||
except Exception:
|
||
api_key, api_secret, use_testnet, status = "", "", False, "active"
|
||
# 仅用于配置页展示/更新:不返回 secret 明文;api_key 仅脱敏展示
|
||
result["BINANCE_API_KEY"] = {
|
||
"value": _mask(api_key or ""),
|
||
"type": "string",
|
||
"category": "api",
|
||
"description": "币安API密钥(账号私有,仅脱敏展示;账号 owner/admin 可修改)",
|
||
}
|
||
result["BINANCE_API_SECRET"] = {
|
||
"value": "",
|
||
"type": "string",
|
||
"category": "api",
|
||
"description": "币安API密钥Secret(账号私有,不回传明文;账号 owner/admin 可修改)",
|
||
}
|
||
result["USE_TESTNET"] = {
|
||
"value": bool(use_testnet),
|
||
"type": "boolean",
|
||
"category": "api",
|
||
"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
|
||
|
||
# 交易预设(profile):用于前端一键切换“稳健 / 快速验证”
|
||
for k, meta in PROFILE_CONFIG_DEFAULTS.items():
|
||
if k not in result:
|
||
result[k] = meta
|
||
|
||
for k, meta in RISK_KNOBS_DEFAULTS.items():
|
||
if k not in result:
|
||
result[k] = meta
|
||
|
||
# 固定风险百分比配置(策略核心,仅管理员可见)
|
||
FIXED_RISK_CONFIG_DEFAULTS = {
|
||
"USE_FIXED_RISK_SIZING": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "risk",
|
||
"description": "使用固定风险百分比计算仓位(凯利公式)。启用后,每笔单子承受的风险固定为 FIXED_RISK_PERCENT,避免大额亏损。",
|
||
},
|
||
"FIXED_RISK_PERCENT": {
|
||
"value": 0.02, # 2%
|
||
"type": "number",
|
||
"category": "risk",
|
||
"description": "每笔单子承受的风险百分比(相对于总资金)。例如 0.02 表示 2%。启用固定风险后,每笔亏损限制在该百分比内。",
|
||
},
|
||
}
|
||
|
||
for k, meta in FIXED_RISK_CONFIG_DEFAULTS.items():
|
||
if k not in result:
|
||
result[k] = meta
|
||
|
||
for k, meta in CORE_STRATEGY_CONFIG_DEFAULTS.items():
|
||
if k not in result:
|
||
result[k] = meta
|
||
|
||
# 普通用户:只展示风险旋钮 + 账号密钥(尽量傻瓜化,避免改坏策略)
|
||
# 管理员:若当前不是“全局策略账号”,同样只展示风险旋钮,避免误以为这里改策略能生效
|
||
is_admin = (user.get("role") or "user") == "admin"
|
||
gid = _global_strategy_account_id()
|
||
if (not is_admin) or (is_admin and int(account_id) != int(gid)):
|
||
# 先补齐用户可见项的默认值(含 category),避免关联账号因 DB 无记录而缺少「市场扫描」「仓位控制」等分组
|
||
for k in USER_RISK_KNOBS:
|
||
if k not in result and k in USER_VISIBLE_DEFAULTS:
|
||
result[k] = USER_VISIBLE_DEFAULTS[k]
|
||
allowed = set(USER_RISK_KNOBS) | {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}
|
||
result = {k: v for k, v in result.items() if k in allowed}
|
||
return result
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
# ⚠️ 重要:全局配置路由必须在 /{key} 之前,否则会被动态路由匹配
|
||
@router.get("/global")
|
||
async def get_global_configs(
|
||
user: Dict[str, Any] = Depends(get_current_user),
|
||
):
|
||
"""获取全局策略配置(仅管理员)"""
|
||
if (user.get("role") or "user") != "admin":
|
||
raise HTTPException(status_code=403, detail="仅管理员可访问全局策略配置")
|
||
|
||
try:
|
||
from database.models import GlobalStrategyConfig
|
||
configs = GlobalStrategyConfig.get_all()
|
||
logger.info(f"从数据库加载了 {len(configs)} 个全局配置项")
|
||
result = {}
|
||
for config in configs:
|
||
key = config['config_key']
|
||
value = GlobalStrategyConfig._convert_value(
|
||
config['config_value'],
|
||
config['config_type']
|
||
)
|
||
result[key] = {
|
||
"value": value,
|
||
"type": config['config_type'],
|
||
"category": config['category'],
|
||
"description": config.get('description'),
|
||
}
|
||
logger.debug(f"加载配置项: {key} = {value} (type: {config['config_type']}, category: {config['category']})")
|
||
|
||
# 添加默认配置(如果数据库中没有)
|
||
for k, meta in CORE_STRATEGY_CONFIG_DEFAULTS.items():
|
||
if k not in result:
|
||
result[k] = meta
|
||
|
||
# 全局交易预设(profile),用于控制一组参数的默认值
|
||
for k, meta in PROFILE_CONFIG_DEFAULTS.items():
|
||
if k not in result:
|
||
result[k] = meta
|
||
|
||
# 固定风险百分比配置
|
||
FIXED_RISK_CONFIG_DEFAULTS = {
|
||
"USE_FIXED_RISK_SIZING": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "risk",
|
||
"description": "使用固定风险百分比计算仓位(凯利公式)。启用后,每笔单子承受的风险固定为 FIXED_RISK_PERCENT,避免大额亏损。",
|
||
},
|
||
"FIXED_RISK_PERCENT": {
|
||
"value": 0.02,
|
||
"type": "number",
|
||
"category": "risk",
|
||
"description": "每笔单子承受的风险百分比(相对于总资金)。例如 0.02 表示 2%。启用固定风险后,每笔亏损限制在该百分比内。",
|
||
},
|
||
}
|
||
for k, meta in FIXED_RISK_CONFIG_DEFAULTS.items():
|
||
if k not in result:
|
||
result[k] = meta
|
||
|
||
# 周日/晚间控制(周六 21 点~周日 06 点等),确保全局配置页可见可改
|
||
for k, meta in RISK_KNOBS_DEFAULTS.items():
|
||
if k not in result:
|
||
result[k] = meta
|
||
|
||
# 添加更多核心策略配置的默认值(确保前端能显示所有重要配置)
|
||
ADDITIONAL_STRATEGY_DEFAULTS = {
|
||
"BETA_FILTER_ENABLED": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "大盘共振过滤:BTC/ETH 下跌时屏蔽多单",
|
||
},
|
||
"BETA_FILTER_THRESHOLD": {
|
||
"value": -0.005,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "大盘共振阈值(比例,如 -0.005 表示 -0.5%)",
|
||
},
|
||
"USE_ATR_STOP_LOSS": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "是否使用ATR动态止损(优先于固定百分比)",
|
||
},
|
||
"ATR_PERIOD": {
|
||
"value": 14,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "ATR计算周期(默认14)",
|
||
},
|
||
"USE_DYNAMIC_ATR_MULTIPLIER": {
|
||
"value": False,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "是否根据波动率动态调整ATR倍数",
|
||
},
|
||
"ATR_MULTIPLIER_MIN": {
|
||
"value": 1.5,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "动态ATR倍数最小值",
|
||
},
|
||
"ATR_MULTIPLIER_MAX": {
|
||
"value": 2.5,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "动态ATR倍数最大值",
|
||
},
|
||
"MIN_SIGNAL_STRENGTH": {
|
||
"value": 8, # 2026-01-29优化:从7提高到8,减少低质量信号
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "最小信号强度(0-10),只交易高质量信号。2026-01-29优化:默认值从7提高到8。",
|
||
},
|
||
"SCAN_INTERVAL": {
|
||
"value": 1800,
|
||
"type": "number",
|
||
"category": "scan",
|
||
"description": "市场扫描间隔(秒),默认30分钟",
|
||
},
|
||
"TOP_N_SYMBOLS": {
|
||
"value": 8,
|
||
"type": "number",
|
||
"category": "scan",
|
||
"description": "每次扫描后优先处理的交易对数量",
|
||
},
|
||
"MAX_SCAN_SYMBOLS": {
|
||
"value": 1500,
|
||
"type": "number",
|
||
"category": "scan",
|
||
"description": "最大扫描交易对数量,控制市场扫描的交易对总数,数值越大覆盖范围越广但扫描时间越长",
|
||
},
|
||
"SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT": {
|
||
"value": 8,
|
||
"type": "number",
|
||
"category": "scan",
|
||
"description": "智能补单:多返回的候选数量。当前 TOP_N 中部分因冷却等被跳过时,仍会尝试这批额外候选,避免无单可下。",
|
||
},
|
||
"TAKE_PROFIT_1_PERCENT": {
|
||
"value": 0.15,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "分步止盈第一目标(保证金百分比,如 0.15=15%)。第一目标触发后了结50%仓位,剩余追求第二目标。",
|
||
},
|
||
"MIN_VOLUME_24H_STRICT": {
|
||
"value": 10000000,
|
||
"type": "number",
|
||
"category": "scan",
|
||
"description": "严格成交量过滤,24H Volume低于此值(USD)直接剔除",
|
||
},
|
||
"EXCLUDE_MAJOR_COINS": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "scan",
|
||
"description": "是否排除主流币(BTC、ETH、BNB等),专注于山寨币。山寨币策略建议开启。",
|
||
},
|
||
"PROFIT_PROTECTION_ENABLED": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "盈利保护总开关:True=启用保本+移动止损,False=全部关闭(不做保本、不做移动止损)",
|
||
},
|
||
"LOCK_PROFIT_AT_BREAKEVEN_AFTER_PCT": {
|
||
"value": 0.03,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "盈利达保证金的该比例时移至保本(如 0.03=3%,0=关闭保本步骤)",
|
||
},
|
||
"LOCK_PROFIT_STAGE1_TRIGGER_PCT": {
|
||
"value": 0.08,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "分层锁盈第一层触发阈值(相对保证金,如 0.08=8%)。达到后将止损从保本抬到小幅锁利。",
|
||
},
|
||
"LOCK_PROFIT_STAGE1_PCT": {
|
||
"value": 0.02,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "分层锁盈第一层锁利比例(相对保证金,如 0.02=2%)。",
|
||
},
|
||
"LOCK_PROFIT_STAGE2_TRIGGER_PCT": {
|
||
"value": 0.15,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "分层锁盈第二层触发阈值(相对保证金,如 0.15=15%)。达到后进一步抬高止损。",
|
||
},
|
||
"LOCK_PROFIT_STAGE2_PCT": {
|
||
"value": 0.05,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "分层锁盈第二层锁利比例(相对保证金,如 0.05=5%)。",
|
||
},
|
||
"USE_TRAILING_STOP": {
|
||
"value": False,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "是否启用移动止损(默认关闭,让利润奔跑)",
|
||
},
|
||
"BLOCK_SHORT_WHEN_BULL_MARKET": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "市场方案=牛市时禁止开空(与 MARKET_SCHEME 一致时生效,默认 True)",
|
||
},
|
||
"BLOCK_LONG_WHEN_BEAR_MARKET": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "市场方案=熊市时禁止开多(与 MARKET_SCHEME 一致时生效,默认 True)",
|
||
},
|
||
"ONLY_AUTO_TRADE_CREATES_RECORDS": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "position",
|
||
"description": "为 True 时仅自动开仓写入 DB,不补建「仅币安有仓」;改为 False 并保持 SYNC_RECOVER_MISSING_POSITIONS=True 可使币安持仓与 DB 一致(补建缺失记录并挂 SL/TP)",
|
||
},
|
||
"SYNC_RECOVER_MISSING_POSITIONS": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "position",
|
||
"description": "同步时补建「币安有仓、DB 无记录」的交易记录(须 ONLY_AUTO_TRADE_CREATES_RECORDS=False 才生效)",
|
||
},
|
||
"SYNC_RECOVER_ONLY_WHEN_HAS_SLTP": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "position",
|
||
"description": "仅当该持仓存在止损/止盈单时才补建(未配置 SYSTEM_ORDER_ID_PREFIX 时生效)",
|
||
},
|
||
"SYSTEM_ORDER_ID_PREFIX": {
|
||
"value": "SYS",
|
||
"type": "string",
|
||
"category": "position",
|
||
"description": "系统单标识:下单时写入 newClientOrderId 前缀,同步时仅对「开仓订单 clientOrderId 以此前缀开头」的持仓补建;设空则用「是否有止损止盈单」判断",
|
||
},
|
||
"SMART_ENTRY_ENABLED": {
|
||
"value": False,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "智能入场开关。关闭后回归纯限价单模式",
|
||
},
|
||
"SYMBOL_LOSS_COOLDOWN_ENABLED": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "是否启用同一交易对连续亏损后的冷却(避免连续亏损后继续交易)。2026-01-29新增。",
|
||
},
|
||
"SYMBOL_MAX_CONSECUTIVE_LOSSES": {
|
||
"value": 2,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "最大允许连续亏损次数(超过则禁止交易该交易对一段时间)。2026-01-29新增。",
|
||
},
|
||
"SYMBOL_LOSS_COOLDOWN_SEC": {
|
||
"value": 3600,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "连续亏损后的冷却时间(秒),默认1小时。2026-01-29新增。",
|
||
},
|
||
"MAX_RSI_FOR_LONG": {
|
||
"value": 70,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "做多时 RSI 超过此值则不开多(避免超买区追多)。2026-01-31新增。",
|
||
},
|
||
"MAX_CHANGE_PERCENT_FOR_LONG": {
|
||
"value": 25,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "做多时 24h 涨跌幅超过此值则不开多(避免追大涨)。单位:百分比数值,如 25 表示 25%。2026-01-31新增。",
|
||
},
|
||
"MIN_RSI_FOR_SHORT": {
|
||
"value": 30,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "做空时 RSI 低于此值则不做空(避免深超卖反弹)。2026-01-31新增。",
|
||
},
|
||
"MAX_CHANGE_PERCENT_FOR_SHORT": {
|
||
"value": 10,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "做空时 24h 涨跌幅超过此值则不做空(24h 仍大涨时不做空)。单位:百分比数值。2026-01-31新增。",
|
||
},
|
||
# 多账号错峰启动扫描(避免多个账号同时扫同时下单)
|
||
"SCAN_STAGGER_BY_ACCOUNT": {
|
||
"value": False,
|
||
"type": "boolean",
|
||
"category": "scan",
|
||
"description": "多账号错峰扫描总开关:True=不同账号在首次扫描前随机/按账号ID延迟一段时间,减少并发压力;单账号可保持 False。",
|
||
},
|
||
"SCAN_STAGGER_RANDOM": {
|
||
"value": True,
|
||
"type": "boolean",
|
||
"category": "scan",
|
||
"description": "错峰方式:True=在区间 [SCAN_STAGGER_MIN_SEC, SCAN_STAGGER_MAX_SEC] 内随机延迟;False=按账号ID固定间隔延迟 (account_id-1)*SCAN_STAGGER_SEC。",
|
||
},
|
||
"SCAN_STAGGER_MIN_SEC": {
|
||
"value": 10,
|
||
"type": "number",
|
||
"category": "scan",
|
||
"description": "随机错峰模式下的最小延迟秒数(含)。例如 10 表示至少延迟 10 秒。",
|
||
},
|
||
"SCAN_STAGGER_MAX_SEC": {
|
||
"value": 120,
|
||
"type": "number",
|
||
"category": "scan",
|
||
"description": "随机错峰模式下的最大延迟秒数(含)。例如 120 表示至多延迟 120 秒。",
|
||
},
|
||
"SCAN_STAGGER_SEC": {
|
||
"value": 60,
|
||
"type": "number",
|
||
"category": "scan",
|
||
"description": "固定错峰模式下每个账号之间的步长秒数:延迟 = (account_id-1)*SCAN_STAGGER_SEC。",
|
||
},
|
||
# ===== 滞涨早止盈(涨到约 N% 后长时间不创新高就分批减仓+抬止损)=====
|
||
"STAGNATION_EARLY_EXIT_ENABLED": {
|
||
"value": False,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "是否启用「滞涨早止盈」:曾达到一定浮盈后,长时间未创新高则主动分批减仓并抬升止损锁利。",
|
||
},
|
||
"STAGNATION_MIN_RUNUP_PCT": {
|
||
"value": 10.0,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "触发滞涨判断的最低历史浮盈百分比(基于保证金),如 10 表示曾浮盈≥10% 才会考虑滞涨早止盈。",
|
||
},
|
||
"STAGNATION_NO_NEW_HIGH_HOURS": {
|
||
"value": 3.0,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "在达到最高浮盈后,若连续这么多个小时未再创新高,则视为动能衰减并触发滞涨早止盈。",
|
||
},
|
||
"STAGNATION_PARTIAL_CLOSE_PCT": {
|
||
"value": 0.5,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "滞涨触发时首先平掉的仓位比例,如 0.5 表示先平掉 50% 锁定一半利润。",
|
||
},
|
||
"STAGNATION_LOCK_PCT": {
|
||
"value": 5.0,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "滞涨触发后对剩余仓位抬升止损时,至少要锁住的利润百分比(与“最高浮盈的一半”取较大者)。",
|
||
},
|
||
}
|
||
for k, meta in ADDITIONAL_STRATEGY_DEFAULTS.items():
|
||
if k not in result:
|
||
result[k] = meta
|
||
|
||
logger.info(f"返回全局配置项数量: {len(result)}")
|
||
return result
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.get("/feasibility-check")
|
||
async def check_config_feasibility(
|
||
user: Dict[str, Any] = Depends(get_current_user),
|
||
account_id: int = Depends(get_account_id),
|
||
):
|
||
"""
|
||
检查配置可行性,基于当前账户余额和杠杆倍数计算可行的配置建议
|
||
"""
|
||
try:
|
||
# 获取账户余额
|
||
try:
|
||
from api.routes.account import get_realtime_account_data
|
||
account_data = await get_realtime_account_data(account_id=account_id)
|
||
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": []
|
||
}
|
||
|
||
# 获取当前“有效配置”(平台兜底:策略核心可能来自全局账号)
|
||
try:
|
||
import config_manager as _cfg_mgr # type: ignore
|
||
|
||
mgr = _cfg_mgr.ConfigManager.for_account(int(account_id)) if hasattr(_cfg_mgr, "ConfigManager") else None
|
||
tc = mgr.get_trading_config() if mgr else {}
|
||
except Exception:
|
||
tc = {}
|
||
|
||
def _tc(key: str, default):
|
||
try:
|
||
return tc.get(key, default)
|
||
except Exception:
|
||
return default
|
||
|
||
min_margin_usdt = float(_tc('MIN_MARGIN_USDT', 5.0))
|
||
min_position_percent = float(_tc('MIN_POSITION_PERCENT', 0.02))
|
||
max_position_percent = float(_tc('MAX_POSITION_PERCENT', 0.08))
|
||
base_leverage = int(_tc('LEVERAGE', 10))
|
||
max_leverage = int(_tc('MAX_LEVERAGE', 15))
|
||
use_dynamic_leverage = bool(_tc('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
|
||
})
|
||
|
||
# 普通用户:只返回可调整的风险旋钮建议,避免前端一键应用时触发 403
|
||
if (user.get("role") or "user") != "admin":
|
||
try:
|
||
suggestions = [s for s in (suggestions or []) if s.get("config_key") in USER_RISK_KNOBS]
|
||
except Exception:
|
||
suggestions = []
|
||
|
||
max_open_positions = int(_tc("MAX_OPEN_POSITIONS", 3))
|
||
max_daily_entries = int(_tc("MAX_DAILY_ENTRIES", 8))
|
||
max_single_position_margin_usdt = round(available_balance * max_position_percent, 2)
|
||
|
||
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,
|
||
"max_open_positions": max_open_positions,
|
||
"max_daily_entries": max_daily_entries,
|
||
"max_single_position_margin_usdt": max_single_position_margin_usdt,
|
||
},
|
||
"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.put("/global/{key}")
|
||
async def update_global_config(
|
||
key: str,
|
||
item: ConfigUpdate,
|
||
user: Dict[str, Any] = Depends(get_current_user),
|
||
):
|
||
"""更新单个全局策略配置(仅管理员)"""
|
||
if (user.get("role") or "user") != "admin":
|
||
raise HTTPException(status_code=403, detail="仅管理员可修改全局策略配置")
|
||
|
||
try:
|
||
from database.models import GlobalStrategyConfig
|
||
|
||
# 验证配置值
|
||
config_type = item.type or "string"
|
||
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 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%)"
|
||
)
|
||
|
||
# 更新数据库
|
||
GlobalStrategyConfig.set(
|
||
key,
|
||
item.value,
|
||
config_type,
|
||
item.category or "strategy",
|
||
item.description,
|
||
updated_by=user.get("username")
|
||
)
|
||
|
||
# 更新缓存 (ConfigManager)
|
||
try:
|
||
import config_manager
|
||
# 更新 GlobalStrategyConfigManager 的缓存
|
||
if hasattr(config_manager, 'GlobalStrategyConfigManager'):
|
||
mgr = config_manager.GlobalStrategyConfigManager()
|
||
# 写入Redis (会更新 redis 和 _cache)
|
||
mgr._set_to_redis(key, item.value)
|
||
# 手动更新本地缓存,确保当前进程也生效
|
||
mgr._cache[key] = item.value
|
||
logger.info(f"全局配置已更新到Redis缓存: {key} = {item.value}")
|
||
except Exception as e:
|
||
logger.warning(f"更新全局配置缓存失败: {e}")
|
||
|
||
return {
|
||
"message": "全局配置已更新",
|
||
"key": key,
|
||
"value": item.value
|
||
}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/global/batch")
|
||
async def update_global_configs_batch(
|
||
configs: list[ConfigItem],
|
||
user: Dict[str, Any] = Depends(get_current_user),
|
||
):
|
||
"""批量更新全局策略配置(仅管理员)"""
|
||
if (user.get("role") or "user") != "admin":
|
||
raise HTTPException(status_code=403, detail="仅管理员可修改全局策略配置")
|
||
|
||
try:
|
||
from database.models import GlobalStrategyConfig
|
||
import config_manager
|
||
|
||
updated_count = 0
|
||
mgr = None
|
||
if hasattr(config_manager, 'GlobalStrategyConfigManager'):
|
||
mgr = config_manager.GlobalStrategyConfigManager()
|
||
|
||
for item in configs:
|
||
# 简单验证
|
||
config_type = item.type
|
||
val = item.value
|
||
if config_type == 'number':
|
||
try:
|
||
val = float(val)
|
||
except:
|
||
continue
|
||
elif config_type == 'boolean':
|
||
if not isinstance(val, bool):
|
||
val = str(val).lower() in ('true', '1', 'yes', 'on')
|
||
|
||
# 更新DB
|
||
GlobalStrategyConfig.set(
|
||
item.key,
|
||
val,
|
||
config_type,
|
||
item.category,
|
||
item.description,
|
||
updated_by=user.get("username")
|
||
)
|
||
|
||
# 更新缓存
|
||
if mgr:
|
||
mgr._set_to_redis(item.key, val)
|
||
mgr._cache[item.key] = val
|
||
|
||
updated_count += 1
|
||
|
||
return {"message": f"成功更新 {updated_count} 个全局配置项"}
|
||
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
|
||
|
||
|
||
@router.get("/meta")
|
||
async def get_config_meta(user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
||
is_admin = (user.get("role") or "user") == "admin"
|
||
return {
|
||
"is_admin": bool(is_admin),
|
||
"user_risk_knobs": sorted(list(USER_RISK_KNOBS)),
|
||
"note": "平台兜底模式:策略核心由全局配置表统一管理(管理员专用);普通用户仅可调整风险旋钮。",
|
||
}
|
||
|
||
|
||
@router.get("/{key}")
|
||
async def get_config(
|
||
key: str,
|
||
user: Dict[str, Any] = Depends(get_current_user),
|
||
account_id: int = Depends(get_account_id),
|
||
):
|
||
"""获取单个配置"""
|
||
try:
|
||
# 虚拟字段:从 accounts 表读取
|
||
if key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||
if key == "BINANCE_API_KEY":
|
||
return {"key": key, "value": _mask(api_key or ""), "type": "string", "category": "api", "description": "币安API密钥(仅脱敏展示)"}
|
||
if key == "BINANCE_API_SECRET":
|
||
return {"key": key, "value": "", "type": "string", "category": "api", "description": "币安API密钥Secret(不回传明文)"}
|
||
return {"key": key, "value": bool(use_testnet), "type": "boolean", "category": "api", "description": "是否使用测试网(账号私有)"}
|
||
|
||
config = TradingConfig.get(key, account_id=account_id)
|
||
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,
|
||
user: Dict[str, Any] = Depends(get_current_user),
|
||
account_id: int = Depends(get_account_id),
|
||
):
|
||
"""更新配置"""
|
||
try:
|
||
# 非管理员:必须是该账号 owner 才允许修改配置
|
||
if (user.get("role") or "user") != "admin":
|
||
require_account_owner(account_id, user)
|
||
|
||
# 管理员:若不是全局策略账号,则禁止修改策略核心(避免误操作)
|
||
if (user.get("role") or "user") == "admin":
|
||
gid = _global_strategy_account_id()
|
||
if int(account_id) != int(gid):
|
||
if key not in (USER_RISK_KNOBS | {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}):
|
||
raise HTTPException(status_code=403, detail=f"该配置由全局策略账号 #{gid} 统一管理,请切换到该账号修改")
|
||
|
||
# 产品模式:普通用户只能改“风险旋钮”与账号私有密钥/测试网
|
||
if (user.get("role") or "user") != "admin":
|
||
if key not in (USER_RISK_KNOBS | {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}):
|
||
raise HTTPException(status_code=403, detail="该配置由平台统一管理(仅管理员可修改)")
|
||
|
||
# API Key/Secret/Testnet:写入 accounts 表(账号私有)
|
||
if key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
||
if (user.get("role") or "user") != "admin":
|
||
require_account_owner(account_id, user)
|
||
try:
|
||
if key == "BINANCE_API_KEY":
|
||
Account.update_credentials(account_id, api_key=str(item.value or ""))
|
||
elif key == "BINANCE_API_SECRET":
|
||
Account.update_credentials(account_id, api_secret=str(item.value or ""))
|
||
else:
|
||
Account.update_credentials(account_id, use_testnet=bool(item.value))
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"更新账号API配置失败: {e}")
|
||
return {
|
||
"message": "配置已更新",
|
||
"key": key,
|
||
"value": item.value,
|
||
"note": "账号API配置已更新(建议重启对应账号的交易进程以立即生效)",
|
||
}
|
||
|
||
# 获取现有配置以确定类型和分类
|
||
existing = TradingConfig.get(key, account_id=account_id)
|
||
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) or RISK_KNOBS_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, account_id=account_id)
|
||
|
||
# 更新config_manager的缓存(包括Redis)
|
||
try:
|
||
import config_manager
|
||
if hasattr(config_manager, 'ConfigManager') and hasattr(config_manager.ConfigManager, "for_account"):
|
||
mgr = config_manager.ConfigManager.for_account(account_id)
|
||
mgr.set(key, item.value, config_type, category, description)
|
||
logger.info(f"配置已更新到Redis缓存(account_id={account_id}): {key} = {item.value}")
|
||
elif 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],
|
||
user: Dict[str, Any] = Depends(get_current_user),
|
||
account_id: int = Depends(get_account_id),
|
||
):
|
||
"""批量更新配置"""
|
||
try:
|
||
# 非管理员:必须是该账号 owner 才允许修改配置
|
||
if (user.get("role") or "user") != "admin":
|
||
require_account_owner(account_id, user)
|
||
|
||
# 管理员:若不是全局策略账号,则批量只允许风险旋钮/密钥
|
||
if (user.get("role") or "user") == "admin":
|
||
gid = _global_strategy_account_id()
|
||
if int(account_id) != int(gid):
|
||
# 直接过滤掉不允许的项(给出 errors,避免“部分成功但实际无效”的错觉)
|
||
pass
|
||
updated_count = 0
|
||
errors = []
|
||
|
||
for item in configs:
|
||
try:
|
||
if (user.get("role") or "user") == "admin":
|
||
gid = _global_strategy_account_id()
|
||
if int(account_id) != int(gid):
|
||
if item.key not in (USER_RISK_KNOBS | {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}):
|
||
errors.append(f"{item.key}: 该配置由全局策略账号 #{gid} 统一管理,请切换账号修改")
|
||
continue
|
||
|
||
# 产品模式:普通用户只能改“风险旋钮”与账号私有密钥/测试网
|
||
if (user.get("role") or "user") != "admin":
|
||
if item.key not in (USER_RISK_KNOBS | {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}):
|
||
errors.append(f"{item.key}: 该配置由平台统一管理(仅管理员可修改)")
|
||
continue
|
||
|
||
if item.key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
||
if (user.get("role") or "user") != "admin":
|
||
require_account_owner(account_id, user)
|
||
# 验证配置值
|
||
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
|
||
|
||
if item.key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
||
# 账号私有API配置:写入 accounts
|
||
if item.key == "BINANCE_API_KEY":
|
||
Account.update_credentials(account_id, api_key=str(item.value or ""))
|
||
elif item.key == "BINANCE_API_SECRET":
|
||
Account.update_credentials(account_id, api_secret=str(item.value or ""))
|
||
else:
|
||
Account.update_credentials(account_id, use_testnet=bool(item.value))
|
||
else:
|
||
TradingConfig.set(
|
||
item.key,
|
||
item.value,
|
||
item.type,
|
||
item.category,
|
||
item.description,
|
||
account_id=account_id,
|
||
)
|
||
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))
|
||
|
||
|
||
@router.put("/global/{key}")
|
||
async def update_global_config(
|
||
key: str,
|
||
item: ConfigUpdate,
|
||
user: Dict[str, Any] = Depends(get_current_user),
|
||
):
|
||
"""更新全局策略配置(仅管理员)"""
|
||
if (user.get("role") or "user") != "admin":
|
||
raise HTTPException(status_code=403, detail="仅管理员可修改全局策略配置")
|
||
|
||
try:
|
||
from database.models import GlobalStrategyConfig
|
||
|
||
# 获取现有配置以确定类型和分类
|
||
existing = GlobalStrategyConfig.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:
|
||
# 从默认配置获取
|
||
meta = CORE_STRATEGY_CONFIG_DEFAULTS.get(key) or {
|
||
"USE_FIXED_RISK_SIZING": {"type": "boolean", "category": "risk", "description": "使用固定风险百分比计算仓位"},
|
||
"FIXED_RISK_PERCENT": {"type": "number", "category": "risk", "description": "每笔单子承受的风险百分比"},
|
||
}.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):
|
||
item.value = str(item.value).lower() in ('true', '1', 'yes', 'on')
|
||
|
||
# 更新全局配置
|
||
GlobalStrategyConfig.set(
|
||
key,
|
||
item.value,
|
||
config_type,
|
||
category,
|
||
description,
|
||
updated_by=user.get("username")
|
||
)
|
||
|
||
# 更新Redis缓存
|
||
try:
|
||
from config_manager import GlobalStrategyConfigManager
|
||
global_mgr = GlobalStrategyConfigManager()
|
||
if isinstance(item.value, (dict, list, bool, int, float)):
|
||
import json
|
||
value_str = json.dumps(item.value, ensure_ascii=False)
|
||
else:
|
||
value_str = str(item.value)
|
||
global_mgr._set_to_redis(key, item.value)
|
||
except Exception as e:
|
||
logger.warning(f"更新全局配置Redis缓存失败: {e}")
|
||
|
||
return {"message": f"全局配置 {key} 已更新", "key": key, "value": item.value}
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.post("/global/batch")
|
||
async def update_global_configs_batch(
|
||
configs: list[ConfigItem],
|
||
user: Dict[str, Any] = Depends(get_current_user),
|
||
):
|
||
"""批量更新全局策略配置(仅管理员)"""
|
||
if (user.get("role") or "user") != "admin":
|
||
raise HTTPException(status_code=403, detail="仅管理员可修改全局策略配置")
|
||
|
||
try:
|
||
from database.models import GlobalStrategyConfig
|
||
from config_manager import GlobalStrategyConfigManager
|
||
|
||
updated_count = 0
|
||
errors = []
|
||
global_mgr = GlobalStrategyConfigManager()
|
||
|
||
for item in configs:
|
||
try:
|
||
# 获取现有配置
|
||
existing = GlobalStrategyConfig.get(item.key)
|
||
if existing:
|
||
config_type = item.type or existing['config_type']
|
||
category = item.category or existing['category']
|
||
description = item.description or existing['description']
|
||
else:
|
||
# 从默认配置获取
|
||
meta = CORE_STRATEGY_CONFIG_DEFAULTS.get(item.key) or {
|
||
"USE_FIXED_RISK_SIZING": {"type": "boolean", "category": "risk"},
|
||
"FIXED_RISK_PERCENT": {"type": "number", "category": "risk"},
|
||
}.get(item.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"{item.key}配置")
|
||
|
||
# 验证配置值
|
||
if config_type == 'number':
|
||
try:
|
||
float(item.value)
|
||
except (ValueError, TypeError):
|
||
errors.append(f"{item.key}: Invalid number value")
|
||
continue
|
||
|
||
# 更新全局配置
|
||
GlobalStrategyConfig.set(
|
||
item.key,
|
||
item.value,
|
||
config_type,
|
||
category,
|
||
description,
|
||
updated_by=user.get("username")
|
||
)
|
||
|
||
# 更新Redis缓存
|
||
global_mgr._set_to_redis(item.key, item.value)
|
||
updated_count += 1
|
||
except Exception as e:
|
||
errors.append(f"{item.key}: {str(e)}")
|
||
|
||
if errors:
|
||
return {
|
||
"message": f"成功更新 {updated_count} 个配置,{len(errors)} 个失败",
|
||
"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))
|