auto_trade_sys/backend/api/routes/config.py
薇薇安 0c489bfdee a
2026-01-23 14:59:57 +08:00

1040 lines
49 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
配置管理API
"""
from fastapi import APIRouter, HTTPException, 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可修改所有配置
# - 非 adminaccount 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时自动交易。默认关闭中性趋势最容易被扫损若你希望更积极可开启。",
},
}
# 核心策略参数(仅管理员可见/在全局策略账号中修改)
CORE_STRATEGY_CONFIG_DEFAULTS = {
"ATR_STOP_LOSS_MULTIPLIER": {
"value": 2.5,
"type": "number",
"category": "strategy",
"description": "ATR止损倍数。建议 2.0-3.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.02,
"type": "number",
"category": "strategy",
"description": "最小止损距离(%)。例如 0.02 表示 2%。防止止损过紧。",
},
"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 = Account.get_credentials(account_id)
except Exception:
api_key, api_secret, use_testnet = "", "", False
# 仅用于配置页展示/更新:不返回 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
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))
@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} USDTMIN_POSITION_PERCENT={min_position_percent*100:.2f}%),最小保证金要求: {min_margin_usdt:.2f} USDT",
"config_key": None,
"suggested_value": None,
"reason": None
})
# 如果启用了动态杠杆,显示所有杠杆倍数的检查结果
if use_dynamic_leverage and len(leverage_to_check) > 1:
suggestions.append({
"type": "leverage_analysis",
"title": "各杠杆倍数检查结果(全部可行)",
"description": "详细检查结果见下方",
"config_key": None,
"suggested_value": None,
"reason": None,
"leverage_results": leverage_results
})
# 普通用户:只返回可调整的风险旋钮建议,避免前端一键应用时触发 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 = 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.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()
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'),
}
# 添加默认配置(如果数据库中没有)
for k, meta in CORE_STRATEGY_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
return result
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))