1209 lines
57 KiB
Python
1209 lines
57 KiB
Python
"""
|
||
配置管理API
|
||
"""
|
||
from fastapi import APIRouter, HTTPException, Header, Depends
|
||
from api.models.config import ConfigItem, ConfigUpdate
|
||
import sys
|
||
from pathlib import Path
|
||
import logging
|
||
from typing import Dict, Any
|
||
|
||
# 添加项目根目录到路径
|
||
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", # 每日最多开仓次数
|
||
}
|
||
|
||
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。",
|
||
},
|
||
}
|
||
|
||
# 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": "下单后确认成交等待时间(秒)。",
|
||
},
|
||
"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时自动交易。默认关闭(中性趋势最容易被扫损);若你希望更积极可开启。",
|
||
},
|
||
}
|
||
|
||
# 风险/策略预设(用于一键切换“稳健 / 快速验证”等模式)
|
||
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.025, # 2026-01-29优化:从0.02提高到0.025,给波动更多空间
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "最小止损距离(%)。例如 0.025 表示 2.5%。防止止损过紧。2026-01-29优化:默认值从2%提高到2.5%。",
|
||
},
|
||
"MIN_TAKE_PROFIT_PRICE_PCT": {
|
||
"value": 0.02,
|
||
"type": "number",
|
||
"category": "strategy",
|
||
"description": "最小止盈距离(%)。例如 0.02 表示 2%。防止止盈过近。",
|
||
},
|
||
}
|
||
|
||
|
||
@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)):
|
||
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
|
||
|
||
# 添加更多核心策略配置的默认值(确保前端能显示所有重要配置)
|
||
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": "每次扫描后优先处理的交易对数量",
|
||
},
|
||
"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等),专注于山寨币。山寨币策略建议开启。",
|
||
},
|
||
"USE_TRAILING_STOP": {
|
||
"value": False,
|
||
"type": "boolean",
|
||
"category": "strategy",
|
||
"description": "是否启用移动止损(默认关闭,让利润奔跑)",
|
||
},
|
||
"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新增。",
|
||
},
|
||
}
|
||
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 = []
|
||
|
||
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,
|
||
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.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.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))
|