a
This commit is contained in:
parent
87e7865cbb
commit
5b1370a5a2
|
|
@ -20,6 +20,49 @@ from api.auth_deps import get_current_user, get_account_id, require_admin, requi
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
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 脱敏
|
# API key/secret 脱敏
|
||||||
def _mask(s: str) -> str:
|
def _mask(s: str) -> str:
|
||||||
s = "" if s is None else str(s)
|
s = "" if s is None else str(s)
|
||||||
|
|
@ -163,6 +206,18 @@ async def get_all_configs(
|
||||||
for k, meta in AUTO_TRADE_FILTER_DEFAULTS.items():
|
for k, meta in AUTO_TRADE_FILTER_DEFAULTS.items():
|
||||||
if k not in result:
|
if k not in result:
|
||||||
result[k] = meta
|
result[k] = meta
|
||||||
|
|
||||||
|
for k, meta in RISK_KNOBS_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
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
@ -170,6 +225,7 @@ async def get_all_configs(
|
||||||
|
|
||||||
@router.get("/feasibility-check")
|
@router.get("/feasibility-check")
|
||||||
async def check_config_feasibility(
|
async def check_config_feasibility(
|
||||||
|
user: Dict[str, Any] = Depends(get_current_user),
|
||||||
account_id: int = Depends(get_account_id),
|
account_id: int = Depends(get_account_id),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|
@ -194,13 +250,27 @@ async def check_config_feasibility(
|
||||||
"suggestions": []
|
"suggestions": []
|
||||||
}
|
}
|
||||||
|
|
||||||
# 获取当前配置
|
# 获取当前“有效配置”(平台兜底:策略核心可能来自全局账号)
|
||||||
min_margin_usdt = TradingConfig.get_value('MIN_MARGIN_USDT', 5.0, account_id=account_id)
|
try:
|
||||||
min_position_percent = TradingConfig.get_value('MIN_POSITION_PERCENT', 0.02, account_id=account_id)
|
import config_manager as _cfg_mgr # type: ignore
|
||||||
max_position_percent = TradingConfig.get_value('MAX_POSITION_PERCENT', 0.08, account_id=account_id)
|
|
||||||
base_leverage = TradingConfig.get_value('LEVERAGE', 10, account_id=account_id)
|
mgr = _cfg_mgr.ConfigManager.for_account(int(account_id)) if hasattr(_cfg_mgr, "ConfigManager") else None
|
||||||
max_leverage = TradingConfig.get_value('MAX_LEVERAGE', 15, account_id=account_id)
|
tc = mgr.get_trading_config() if mgr else {}
|
||||||
use_dynamic_leverage = TradingConfig.get_value('USE_DYNAMIC_LEVERAGE', True, account_id=account_id)
|
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]
|
leverage_to_check = [base_leverage]
|
||||||
|
|
@ -425,6 +495,13 @@ async def check_config_feasibility(
|
||||||
"leverage_results": leverage_results
|
"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 {
|
return {
|
||||||
"feasible": is_feasible,
|
"feasible": is_feasible,
|
||||||
"account_balance": available_balance,
|
"account_balance": available_balance,
|
||||||
|
|
@ -506,6 +583,18 @@ async def update_config(
|
||||||
if (user.get("role") or "user") != "admin":
|
if (user.get("role") or "user") != "admin":
|
||||||
require_account_owner(account_id, user)
|
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 表(账号私有)
|
# API Key/Secret/Testnet:写入 accounts 表(账号私有)
|
||||||
if key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
if key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
||||||
if (user.get("role") or "user") != "admin":
|
if (user.get("role") or "user") != "admin":
|
||||||
|
|
@ -534,7 +623,7 @@ async def update_config(
|
||||||
description = item.description or existing['description']
|
description = item.description or existing['description']
|
||||||
else:
|
else:
|
||||||
# 允许创建新配置(用于新功能首次上线,DB 里还没有 key 的情况)
|
# 允许创建新配置(用于新功能首次上线,DB 里还没有 key 的情况)
|
||||||
meta = SMART_ENTRY_CONFIG_DEFAULTS.get(key) or AUTO_TRADE_FILTER_DEFAULTS.get(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")
|
config_type = item.type or (meta.get("type") if meta else "string")
|
||||||
category = item.category or (meta.get("category") if meta else "strategy")
|
category = item.category or (meta.get("category") if meta else "strategy")
|
||||||
description = item.description or (meta.get("description") if meta else f"{key}配置")
|
description = item.description or (meta.get("description") if meta else f"{key}配置")
|
||||||
|
|
@ -602,11 +691,31 @@ async def update_configs_batch(
|
||||||
# 非管理员:必须是该账号 owner 才允许修改配置
|
# 非管理员:必须是该账号 owner 才允许修改配置
|
||||||
if (user.get("role") or "user") != "admin":
|
if (user.get("role") or "user") != "admin":
|
||||||
require_account_owner(account_id, user)
|
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
|
updated_count = 0
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
for item in configs:
|
for item in configs:
|
||||||
try:
|
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 item.key in {"BINANCE_API_KEY", "BINANCE_API_SECRET", "USE_TESTNET"}:
|
||||||
if (user.get("role") or "user") != "admin":
|
if (user.get("role") or "user") != "admin":
|
||||||
require_account_owner(account_id, user)
|
require_account_owner(account_id, user)
|
||||||
|
|
@ -660,3 +769,15 @@ async def update_configs_batch(
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(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]:
|
||||||
|
gid = _global_strategy_account_id()
|
||||||
|
is_admin = (user.get("role") or "user") == "admin"
|
||||||
|
return {
|
||||||
|
"global_strategy_account_id": int(gid),
|
||||||
|
"is_admin": bool(is_admin),
|
||||||
|
"user_risk_knobs": sorted(list(USER_RISK_KNOBS)),
|
||||||
|
"note": "平台兜底模式:策略核心由全局策略账号统一管理;普通用户仅可调整风险旋钮。",
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,24 @@ import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 平台兜底:策略核心使用全局账号配置(默认 account_id=1),普通用户账号只允许调整“风险旋钮”
|
||||||
|
# - 风险旋钮:每个账号独立(仓位/频次等)
|
||||||
|
# - 其它策略参数:统一从全局账号读取,避免每个用户乱改导致策略不可控
|
||||||
|
try:
|
||||||
|
GLOBAL_STRATEGY_ACCOUNT_ID = int(os.getenv("ATS_GLOBAL_STRATEGY_ACCOUNT_ID") or "1")
|
||||||
|
except Exception:
|
||||||
|
GLOBAL_STRATEGY_ACCOUNT_ID = 1
|
||||||
|
|
||||||
|
RISK_KNOBS_KEYS = {
|
||||||
|
"MIN_MARGIN_USDT",
|
||||||
|
"MIN_POSITION_PERCENT",
|
||||||
|
"MAX_POSITION_PERCENT",
|
||||||
|
"MAX_TOTAL_POSITION_PERCENT",
|
||||||
|
"AUTO_TRADE_ENABLED",
|
||||||
|
"MAX_OPEN_POSITIONS",
|
||||||
|
"MAX_DAILY_ENTRIES",
|
||||||
|
}
|
||||||
|
|
||||||
# 尝试导入同步Redis客户端(用于配置缓存)
|
# 尝试导入同步Redis客户端(用于配置缓存)
|
||||||
try:
|
try:
|
||||||
import redis
|
import redis
|
||||||
|
|
@ -474,71 +492,105 @@ class ConfigManager:
|
||||||
|
|
||||||
def get_trading_config(self):
|
def get_trading_config(self):
|
||||||
"""获取交易配置字典(兼容原有config.py的TRADING_CONFIG)"""
|
"""获取交易配置字典(兼容原有config.py的TRADING_CONFIG)"""
|
||||||
|
# 全局策略配置管理器(避免递归:当 self 就是全局账号时,不做跨账号读取)
|
||||||
|
global_mgr = None
|
||||||
|
if self.account_id != int(GLOBAL_STRATEGY_ACCOUNT_ID or 1):
|
||||||
|
try:
|
||||||
|
global_mgr = ConfigManager.for_account(int(GLOBAL_STRATEGY_ACCOUNT_ID or 1))
|
||||||
|
except Exception:
|
||||||
|
global_mgr = None
|
||||||
|
# 预热全局 cache:避免每个 key 都 HGET 一次
|
||||||
|
if global_mgr is not None:
|
||||||
|
try:
|
||||||
|
global_mgr.reload_from_redis()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def eff_get(key: str, default: Any):
|
||||||
|
"""
|
||||||
|
策略核心:默认从全局账号读取(GLOBAL_STRATEGY_ACCOUNT_ID)。
|
||||||
|
风险旋钮:从当前账号读取。
|
||||||
|
"""
|
||||||
|
# API key/secret/testnet 永远按账号读取(在 get() 内部已处理)
|
||||||
|
if key in RISK_KNOBS_KEYS or global_mgr is None:
|
||||||
|
return self.get(key, default)
|
||||||
|
try:
|
||||||
|
if key in global_mgr._cache: # noqa: SLF001
|
||||||
|
return global_mgr._cache.get(key, default) # noqa: SLF001
|
||||||
|
return global_mgr.get(key, default)
|
||||||
|
except Exception:
|
||||||
|
return self.get(key, default)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
# 仓位控制
|
# 仓位控制
|
||||||
'MAX_POSITION_PERCENT': self.get('MAX_POSITION_PERCENT', 0.08), # 提高单笔仓位到8%
|
'MAX_POSITION_PERCENT': eff_get('MAX_POSITION_PERCENT', 0.08), # 单笔最大保证金占比
|
||||||
'MAX_TOTAL_POSITION_PERCENT': self.get('MAX_TOTAL_POSITION_PERCENT', 0.40), # 提高总仓位到40%
|
'MAX_TOTAL_POSITION_PERCENT': eff_get('MAX_TOTAL_POSITION_PERCENT', 0.40), # 总保证金占比上限
|
||||||
'MIN_POSITION_PERCENT': self.get('MIN_POSITION_PERCENT', 0.02), # 提高最小仓位到2%
|
'MIN_POSITION_PERCENT': eff_get('MIN_POSITION_PERCENT', 0.02), # 最小保证金占比
|
||||||
'MIN_MARGIN_USDT': self.get('MIN_MARGIN_USDT', 5.0), # 提高最小保证金到5美元
|
'MIN_MARGIN_USDT': eff_get('MIN_MARGIN_USDT', 5.0), # 最小保证金(USDT)
|
||||||
|
|
||||||
|
# 用户风险旋钮:自动交易开关/频次控制
|
||||||
|
'AUTO_TRADE_ENABLED': eff_get('AUTO_TRADE_ENABLED', True),
|
||||||
|
'MAX_OPEN_POSITIONS': eff_get('MAX_OPEN_POSITIONS', 3),
|
||||||
|
'MAX_DAILY_ENTRIES': eff_get('MAX_DAILY_ENTRIES', 8),
|
||||||
|
|
||||||
# 涨跌幅阈值
|
# 涨跌幅阈值
|
||||||
'MIN_CHANGE_PERCENT': self.get('MIN_CHANGE_PERCENT', 2.0),
|
'MIN_CHANGE_PERCENT': eff_get('MIN_CHANGE_PERCENT', 2.0),
|
||||||
'TOP_N_SYMBOLS': self.get('TOP_N_SYMBOLS', 10),
|
'TOP_N_SYMBOLS': eff_get('TOP_N_SYMBOLS', 10),
|
||||||
|
|
||||||
# 风险控制
|
# 风险控制
|
||||||
'STOP_LOSS_PERCENT': self.get('STOP_LOSS_PERCENT', 0.10), # 默认10%
|
'STOP_LOSS_PERCENT': eff_get('STOP_LOSS_PERCENT', 0.10), # 默认10%
|
||||||
'TAKE_PROFIT_PERCENT': self.get('TAKE_PROFIT_PERCENT', 0.30), # 默认30%(盈亏比3:1)
|
'TAKE_PROFIT_PERCENT': eff_get('TAKE_PROFIT_PERCENT', 0.30), # 默认30%(盈亏比3:1)
|
||||||
'MIN_STOP_LOSS_PRICE_PCT': self.get('MIN_STOP_LOSS_PRICE_PCT', 0.02), # 默认2%
|
'MIN_STOP_LOSS_PRICE_PCT': eff_get('MIN_STOP_LOSS_PRICE_PCT', 0.02), # 默认2%
|
||||||
'MIN_TAKE_PROFIT_PRICE_PCT': self.get('MIN_TAKE_PROFIT_PRICE_PCT', 0.03), # 默认3%
|
'MIN_TAKE_PROFIT_PRICE_PCT': eff_get('MIN_TAKE_PROFIT_PRICE_PCT', 0.03), # 默认3%
|
||||||
'USE_ATR_STOP_LOSS': self.get('USE_ATR_STOP_LOSS', True), # 是否使用ATR动态止损
|
'USE_ATR_STOP_LOSS': eff_get('USE_ATR_STOP_LOSS', True), # 是否使用ATR动态止损
|
||||||
'ATR_STOP_LOSS_MULTIPLIER': self.get('ATR_STOP_LOSS_MULTIPLIER', 1.8), # ATR止损倍数(1.5-2倍)
|
'ATR_STOP_LOSS_MULTIPLIER': eff_get('ATR_STOP_LOSS_MULTIPLIER', 1.8), # ATR止损倍数(1.5-2倍)
|
||||||
'ATR_TAKE_PROFIT_MULTIPLIER': self.get('ATR_TAKE_PROFIT_MULTIPLIER', 3.0), # ATR止盈倍数(3倍ATR)
|
'ATR_TAKE_PROFIT_MULTIPLIER': eff_get('ATR_TAKE_PROFIT_MULTIPLIER', 3.0), # ATR止盈倍数(3倍ATR)
|
||||||
'RISK_REWARD_RATIO': self.get('RISK_REWARD_RATIO', 3.0), # 盈亏比(止损距离的倍数)
|
'RISK_REWARD_RATIO': eff_get('RISK_REWARD_RATIO', 3.0), # 盈亏比(止损距离的倍数)
|
||||||
'ATR_PERIOD': self.get('ATR_PERIOD', 14), # ATR计算周期
|
'ATR_PERIOD': eff_get('ATR_PERIOD', 14), # ATR计算周期
|
||||||
'USE_DYNAMIC_ATR_MULTIPLIER': self.get('USE_DYNAMIC_ATR_MULTIPLIER', False), # 是否根据波动率动态调整ATR倍数
|
'USE_DYNAMIC_ATR_MULTIPLIER': eff_get('USE_DYNAMIC_ATR_MULTIPLIER', False), # 是否根据波动率动态调整ATR倍数
|
||||||
'ATR_MULTIPLIER_MIN': self.get('ATR_MULTIPLIER_MIN', 1.5), # 动态ATR倍数最小值
|
'ATR_MULTIPLIER_MIN': eff_get('ATR_MULTIPLIER_MIN', 1.5), # 动态ATR倍数最小值
|
||||||
'ATR_MULTIPLIER_MAX': self.get('ATR_MULTIPLIER_MAX', 2.5), # 动态ATR倍数最大值
|
'ATR_MULTIPLIER_MAX': eff_get('ATR_MULTIPLIER_MAX', 2.5), # 动态ATR倍数最大值
|
||||||
|
|
||||||
# 市场扫描(1小时主周期)
|
# 市场扫描(1小时主周期)
|
||||||
'SCAN_INTERVAL': self.get('SCAN_INTERVAL', 3600), # 1小时
|
'SCAN_INTERVAL': eff_get('SCAN_INTERVAL', 3600), # 1小时
|
||||||
'TOP_N_SYMBOLS': self.get('TOP_N_SYMBOLS', 10), # 每次扫描后处理的交易对数量
|
'TOP_N_SYMBOLS': eff_get('TOP_N_SYMBOLS', 10), # 每次扫描后处理的交易对数量
|
||||||
'MAX_SCAN_SYMBOLS': self.get('MAX_SCAN_SYMBOLS', 500), # 扫描的最大交易对数量(0表示扫描所有)
|
'MAX_SCAN_SYMBOLS': eff_get('MAX_SCAN_SYMBOLS', 500), # 扫描的最大交易对数量(0表示扫描所有)
|
||||||
'KLINE_INTERVAL': self.get('KLINE_INTERVAL', '1h'),
|
'KLINE_INTERVAL': eff_get('KLINE_INTERVAL', '1h'),
|
||||||
'PRIMARY_INTERVAL': self.get('PRIMARY_INTERVAL', '1h'),
|
'PRIMARY_INTERVAL': eff_get('PRIMARY_INTERVAL', '1h'),
|
||||||
'CONFIRM_INTERVAL': self.get('CONFIRM_INTERVAL', '4h'),
|
'CONFIRM_INTERVAL': eff_get('CONFIRM_INTERVAL', '4h'),
|
||||||
'ENTRY_INTERVAL': self.get('ENTRY_INTERVAL', '15m'),
|
'ENTRY_INTERVAL': eff_get('ENTRY_INTERVAL', '15m'),
|
||||||
|
|
||||||
# 过滤条件
|
# 过滤条件
|
||||||
'MIN_VOLUME_24H': self.get('MIN_VOLUME_24H', 10000000),
|
'MIN_VOLUME_24H': eff_get('MIN_VOLUME_24H', 10000000),
|
||||||
'MIN_VOLATILITY': self.get('MIN_VOLATILITY', 0.02),
|
'MIN_VOLATILITY': eff_get('MIN_VOLATILITY', 0.02),
|
||||||
|
|
||||||
# 高胜率策略参数
|
# 高胜率策略参数
|
||||||
'MIN_SIGNAL_STRENGTH': self.get('MIN_SIGNAL_STRENGTH', 5),
|
'MIN_SIGNAL_STRENGTH': eff_get('MIN_SIGNAL_STRENGTH', 5),
|
||||||
'LEVERAGE': self.get('LEVERAGE', 10),
|
'LEVERAGE': eff_get('LEVERAGE', 10),
|
||||||
'USE_DYNAMIC_LEVERAGE': self.get('USE_DYNAMIC_LEVERAGE', True),
|
'USE_DYNAMIC_LEVERAGE': eff_get('USE_DYNAMIC_LEVERAGE', True),
|
||||||
'MAX_LEVERAGE': self.get('MAX_LEVERAGE', 15), # 降低到15,更保守,配合更大的保证金
|
'MAX_LEVERAGE': eff_get('MAX_LEVERAGE', 15), # 降低到15,更保守,配合更大的保证金
|
||||||
'USE_TRAILING_STOP': self.get('USE_TRAILING_STOP', True),
|
'USE_TRAILING_STOP': eff_get('USE_TRAILING_STOP', True),
|
||||||
'TRAILING_STOP_ACTIVATION': self.get('TRAILING_STOP_ACTIVATION', 0.10), # 默认10%(给趋势更多空间)
|
'TRAILING_STOP_ACTIVATION': eff_get('TRAILING_STOP_ACTIVATION', 0.10), # 默认10%(给趋势更多空间)
|
||||||
'TRAILING_STOP_PROTECT': self.get('TRAILING_STOP_PROTECT', 0.05), # 默认5%(保护更多利润)
|
'TRAILING_STOP_PROTECT': eff_get('TRAILING_STOP_PROTECT', 0.05), # 默认5%(保护更多利润)
|
||||||
|
|
||||||
# 自动交易过滤(用于提升胜率/控频)
|
# 自动交易过滤(用于提升胜率/控频)
|
||||||
# 说明:这两个 key 需要出现在 TRADING_CONFIG 中,否则 trading_system 在每次 reload_from_redis 后会丢失它们,
|
# 说明:这两个 key 需要出现在 TRADING_CONFIG 中,否则 trading_system 在每次 reload_from_redis 后会丢失它们,
|
||||||
# 导致始终按默认值拦截自动交易(用户在配置页怎么开都没用)。
|
# 导致始终按默认值拦截自动交易(用户在配置页怎么开都没用)。
|
||||||
'AUTO_TRADE_ONLY_TRENDING': self.get('AUTO_TRADE_ONLY_TRENDING', True),
|
'AUTO_TRADE_ONLY_TRENDING': eff_get('AUTO_TRADE_ONLY_TRENDING', True),
|
||||||
'AUTO_TRADE_ALLOW_4H_NEUTRAL': self.get('AUTO_TRADE_ALLOW_4H_NEUTRAL', False),
|
'AUTO_TRADE_ALLOW_4H_NEUTRAL': eff_get('AUTO_TRADE_ALLOW_4H_NEUTRAL', False),
|
||||||
|
|
||||||
# 智能入场/限价偏移(部分逻辑会直接读取 TRADING_CONFIG)
|
# 智能入场/限价偏移(部分逻辑会直接读取 TRADING_CONFIG)
|
||||||
'LIMIT_ORDER_OFFSET_PCT': self.get('LIMIT_ORDER_OFFSET_PCT', 0.5),
|
'LIMIT_ORDER_OFFSET_PCT': eff_get('LIMIT_ORDER_OFFSET_PCT', 0.5),
|
||||||
'SMART_ENTRY_ENABLED': self.get('SMART_ENTRY_ENABLED', False),
|
'SMART_ENTRY_ENABLED': eff_get('SMART_ENTRY_ENABLED', False),
|
||||||
'SMART_ENTRY_STRONG_SIGNAL': self.get('SMART_ENTRY_STRONG_SIGNAL', 8),
|
'SMART_ENTRY_STRONG_SIGNAL': eff_get('SMART_ENTRY_STRONG_SIGNAL', 8),
|
||||||
'ENTRY_SYMBOL_COOLDOWN_SEC': self.get('ENTRY_SYMBOL_COOLDOWN_SEC', 120),
|
'ENTRY_SYMBOL_COOLDOWN_SEC': eff_get('ENTRY_SYMBOL_COOLDOWN_SEC', 120),
|
||||||
'ENTRY_TIMEOUT_SEC': self.get('ENTRY_TIMEOUT_SEC', 180),
|
'ENTRY_TIMEOUT_SEC': eff_get('ENTRY_TIMEOUT_SEC', 180),
|
||||||
'ENTRY_STEP_WAIT_SEC': self.get('ENTRY_STEP_WAIT_SEC', 15),
|
'ENTRY_STEP_WAIT_SEC': eff_get('ENTRY_STEP_WAIT_SEC', 15),
|
||||||
'ENTRY_CHASE_MAX_STEPS': self.get('ENTRY_CHASE_MAX_STEPS', 4),
|
'ENTRY_CHASE_MAX_STEPS': eff_get('ENTRY_CHASE_MAX_STEPS', 4),
|
||||||
'ENTRY_MARKET_FALLBACK_AFTER_SEC': self.get('ENTRY_MARKET_FALLBACK_AFTER_SEC', 45),
|
'ENTRY_MARKET_FALLBACK_AFTER_SEC': eff_get('ENTRY_MARKET_FALLBACK_AFTER_SEC', 45),
|
||||||
'ENTRY_CONFIRM_TIMEOUT_SEC': self.get('ENTRY_CONFIRM_TIMEOUT_SEC', 30),
|
'ENTRY_CONFIRM_TIMEOUT_SEC': eff_get('ENTRY_CONFIRM_TIMEOUT_SEC', 30),
|
||||||
'ENTRY_MAX_DRIFT_PCT_TRENDING': self.get('ENTRY_MAX_DRIFT_PCT_TRENDING', 0.6),
|
'ENTRY_MAX_DRIFT_PCT_TRENDING': eff_get('ENTRY_MAX_DRIFT_PCT_TRENDING', 0.6),
|
||||||
'ENTRY_MAX_DRIFT_PCT_RANGING': self.get('ENTRY_MAX_DRIFT_PCT_RANGING', 0.3),
|
'ENTRY_MAX_DRIFT_PCT_RANGING': eff_get('ENTRY_MAX_DRIFT_PCT_RANGING', 0.3),
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,22 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
const [accountTradingStatus, setAccountTradingStatus] = useState(null)
|
const [accountTradingStatus, setAccountTradingStatus] = useState(null)
|
||||||
const [accountTradingErr, setAccountTradingErr] = useState('')
|
const [accountTradingErr, setAccountTradingErr] = useState('')
|
||||||
const [currentAccountMeta, setCurrentAccountMeta] = useState(null)
|
const [currentAccountMeta, setCurrentAccountMeta] = useState(null)
|
||||||
|
const [configMeta, setConfigMeta] = useState(null)
|
||||||
|
|
||||||
// 多账号:当前账号(仅用于配置页提示;全局切换器在顶部导航)
|
// 多账号:当前账号(仅用于配置页提示;全局切换器在顶部导航)
|
||||||
const [accountId, setAccountId] = useState(getCurrentAccountId())
|
const [accountId, setAccountId] = useState(getCurrentAccountId())
|
||||||
|
|
||||||
const isAdmin = (currentUser?.role || '') === 'admin'
|
const isAdmin = (currentUser?.role || '') === 'admin'
|
||||||
|
const globalStrategyAccountId = parseInt(String(configMeta?.global_strategy_account_id || '1'), 10) || 1
|
||||||
|
const isGlobalStrategyAccount = isAdmin && accountId === globalStrategyAccountId
|
||||||
|
const loadConfigMeta = async () => {
|
||||||
|
try {
|
||||||
|
const m = await api.getConfigMeta()
|
||||||
|
setConfigMeta(m || null)
|
||||||
|
} catch (e) {
|
||||||
|
setConfigMeta(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 账号管理(超管)
|
// 账号管理(超管)
|
||||||
const [accountsAdmin, setAccountsAdmin] = useState([])
|
const [accountsAdmin, setAccountsAdmin] = useState([])
|
||||||
|
|
@ -394,6 +405,7 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
loadConfigMeta()
|
||||||
loadConfigs()
|
loadConfigs()
|
||||||
checkFeasibility()
|
checkFeasibility()
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
|
|
@ -808,6 +820,46 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
<div className="account-switch">
|
<div className="account-switch">
|
||||||
<span className="account-hint">当前账号:#{accountId}(在顶部导航切换)</span>
|
<span className="account-hint">当前账号:#{accountId}(在顶部导航切换)</span>
|
||||||
</div>
|
</div>
|
||||||
|
{isAdmin ? (
|
||||||
|
<div
|
||||||
|
className="system-hint"
|
||||||
|
style={{
|
||||||
|
marginTop: '8px',
|
||||||
|
padding: '10px',
|
||||||
|
border: '1px solid #e0b400',
|
||||||
|
background: '#fff8e1',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: 700 }}>
|
||||||
|
{isGlobalStrategyAccount ? (
|
||||||
|
<>
|
||||||
|
你正在编辑 <span style={{ fontWeight: 900 }}>全局策略账号 #{globalStrategyAccountId}</span>:
|
||||||
|
此处修改将影响所有用户的策略核心。
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
你正在编辑账号 #{accountId}:这里只允许调整 <span style={{ fontWeight: 900 }}>风险旋钮</span>(仓位/次数/自动交易开关等)。
|
||||||
|
策略核心统一来自 <span style={{ fontWeight: 900 }}>全局策略账号 #{globalStrategyAccountId}</span>。
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isGlobalStrategyAccount ? (
|
||||||
|
<div style={{ marginTop: '8px' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="system-btn primary"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentAccountId(globalStrategyAccountId)
|
||||||
|
setAccountId(globalStrategyAccountId)
|
||||||
|
setMessage('已切换到全局策略账号(策略核心统一在这里维护)')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
切换到全局策略账号 #{globalStrategyAccountId}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
<button
|
<button
|
||||||
|
|
@ -1315,7 +1367,8 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* 预设方案快速切换 */}
|
{/* 预设方案快速切换(仅管理员 + 全局策略账号:策略核心统一管理) */}
|
||||||
|
{isAdmin && isGlobalStrategyAccount ? (
|
||||||
<div className="preset-section">
|
<div className="preset-section">
|
||||||
<div className="preset-header">
|
<div className="preset-header">
|
||||||
<h3>快速切换方案</h3>
|
<h3>快速切换方案</h3>
|
||||||
|
|
@ -1413,6 +1466,11 @@ const ConfigPanel = ({ currentUser }) => {
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="system-hint" style={{ marginTop: '12px' }}>
|
||||||
|
平台已开启“傻瓜化模式”:策略核心由管理员统一管理。你只需要配置密钥、充值余额,并调整少量风控参数(如最小/最大仓位、每日开仓次数等)。
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 配置可行性检查提示 */}
|
{/* 配置可行性检查提示 */}
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,14 @@ export const api = {
|
||||||
},
|
},
|
||||||
|
|
||||||
// 配置管理
|
// 配置管理
|
||||||
|
getConfigMeta: async () => {
|
||||||
|
const response = await fetch(buildUrl('/api/config/meta'), { headers: withAuthHeaders() })
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '获取配置元信息失败' }))
|
||||||
|
throw new Error(error.detail || '获取配置元信息失败')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
},
|
||||||
getConfigs: async () => {
|
getConfigs: async () => {
|
||||||
const response = await fetch(buildUrl('/api/config'), { headers: withAccountHeaders() });
|
const response = await fetch(buildUrl('/api/config'), { headers: withAccountHeaders() });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,11 @@ def _get_trading_config():
|
||||||
return _config_manager.get_trading_config()
|
return _config_manager.get_trading_config()
|
||||||
# 回退到默认配置
|
# 回退到默认配置
|
||||||
return {
|
return {
|
||||||
|
# ===== 用户风险旋钮(傻瓜化)=====
|
||||||
|
'AUTO_TRADE_ENABLED': True, # 自动交易总开关
|
||||||
|
'MAX_OPEN_POSITIONS': 3, # 同时持仓数量上限
|
||||||
|
'MAX_DAILY_ENTRIES': 8, # 每日最多开仓次数
|
||||||
|
|
||||||
'MAX_POSITION_PERCENT': 0.08, # 提高单笔仓位到8%(原来5%),增加收益
|
'MAX_POSITION_PERCENT': 0.08, # 提高单笔仓位到8%(原来5%),增加收益
|
||||||
'MAX_TOTAL_POSITION_PERCENT': 0.40, # 提高总仓位到40%(原来30%),允许更多持仓
|
'MAX_TOTAL_POSITION_PERCENT': 0.40, # 提高总仓位到40%(原来30%),允许更多持仓
|
||||||
'MIN_POSITION_PERCENT': 0.02, # 提高最小仓位到2%(原来1%),避免过小仓位
|
'MIN_POSITION_PERCENT': 0.02, # 提高最小仓位到2%(原来1%),避免过小仓位
|
||||||
|
|
@ -242,6 +247,11 @@ TRADING_CONFIG = _get_trading_config()
|
||||||
|
|
||||||
# 确保包含所有必要的默认值
|
# 确保包含所有必要的默认值
|
||||||
defaults = {
|
defaults = {
|
||||||
|
# 用户风险旋钮(即使DB里没配置,也能用)
|
||||||
|
'AUTO_TRADE_ENABLED': True,
|
||||||
|
'MAX_OPEN_POSITIONS': 3,
|
||||||
|
'MAX_DAILY_ENTRIES': 8,
|
||||||
|
|
||||||
'SCAN_INTERVAL': 1800,
|
'SCAN_INTERVAL': 1800,
|
||||||
'KLINE_INTERVAL': '1h',
|
'KLINE_INTERVAL': '1h',
|
||||||
'PRIMARY_INTERVAL': '1h',
|
'PRIMARY_INTERVAL': '1h',
|
||||||
|
|
|
||||||
|
|
@ -629,6 +629,12 @@ class PositionManager:
|
||||||
if self._monitoring_enabled:
|
if self._monitoring_enabled:
|
||||||
await self._start_position_monitoring(symbol)
|
await self._start_position_monitoring(symbol)
|
||||||
|
|
||||||
|
# 记录“今日开仓次数”(用于用户风控旋钮 MAX_DAILY_ENTRIES)
|
||||||
|
try:
|
||||||
|
await self.risk_manager.record_entry(symbol)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return position_info
|
return position_info
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,60 @@ class RedisCache:
|
||||||
self._memory_cache[key] = value
|
self._memory_cache[key] = value
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def get_int(self, key: str, default: int = 0) -> int:
|
||||||
|
"""读取一个整数值(用于计数器等)"""
|
||||||
|
try:
|
||||||
|
if self.redis and self._connected:
|
||||||
|
v = await self.redis.get(key)
|
||||||
|
if v is None:
|
||||||
|
return int(default or 0)
|
||||||
|
try:
|
||||||
|
return int(v)
|
||||||
|
except Exception:
|
||||||
|
return int(default or 0)
|
||||||
|
if key in self._memory_cache:
|
||||||
|
try:
|
||||||
|
return int(self._memory_cache.get(key) or 0)
|
||||||
|
except Exception:
|
||||||
|
return int(default or 0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return int(default or 0)
|
||||||
|
|
||||||
|
async def incr(self, key: str, amount: int = 1, ttl: int = None) -> int:
|
||||||
|
"""
|
||||||
|
自增计数器(用于每日开仓次数等)。
|
||||||
|
- Redis 可用:INCRBY + EXPIRE
|
||||||
|
- Redis 不可用:降级到内存计数
|
||||||
|
"""
|
||||||
|
inc = int(amount or 1)
|
||||||
|
if inc <= 0:
|
||||||
|
inc = 1
|
||||||
|
try:
|
||||||
|
if self.redis and self._connected:
|
||||||
|
n = await self.redis.incrby(key, inc)
|
||||||
|
if ttl:
|
||||||
|
try:
|
||||||
|
await self.redis.expire(key, int(ttl))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return int(n)
|
||||||
|
except Exception:
|
||||||
|
return int(await self.get_int(key, 0))
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Redis incr失败 {key}: {e}")
|
||||||
|
|
||||||
|
# 内存兜底(不做 TTL)
|
||||||
|
cur = 0
|
||||||
|
try:
|
||||||
|
cur = int(self._memory_cache.get(key) or 0)
|
||||||
|
except Exception:
|
||||||
|
cur = 0
|
||||||
|
cur += inc
|
||||||
|
self._memory_cache[key] = cur
|
||||||
|
return int(cur)
|
||||||
|
|
||||||
async def delete(self, key: str):
|
async def delete(self, key: str):
|
||||||
"""删除缓存"""
|
"""删除缓存"""
|
||||||
if self.redis and self._connected:
|
if self.redis and self._connected:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
风险管理模块 - 严格控制仓位和风险
|
风险管理模块 - 严格控制仓位和风险
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
try:
|
try:
|
||||||
from .binance_client import BinanceClient
|
from .binance_client import BinanceClient
|
||||||
|
|
@ -417,13 +419,26 @@ class RiskManager:
|
||||||
Returns:
|
Returns:
|
||||||
是否应该交易
|
是否应该交易
|
||||||
"""
|
"""
|
||||||
|
# 用户风险旋钮:自动交易总开关
|
||||||
|
if not bool(config.TRADING_CONFIG.get("AUTO_TRADE_ENABLED", True)):
|
||||||
|
logger.info(f"{symbol} 自动交易已关闭(AUTO_TRADE_ENABLED=false),跳过")
|
||||||
|
return False
|
||||||
|
|
||||||
# 检查最小涨跌幅阈值
|
# 检查最小涨跌幅阈值
|
||||||
if abs(change_percent) < config.TRADING_CONFIG['MIN_CHANGE_PERCENT']:
|
if abs(change_percent) < config.TRADING_CONFIG['MIN_CHANGE_PERCENT']:
|
||||||
logger.debug(f"{symbol} 涨跌幅 {change_percent:.2f}% 小于阈值")
|
logger.debug(f"{symbol} 涨跌幅 {change_percent:.2f}% 小于阈值")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 检查是否已有持仓
|
# 检查是否已有持仓 / 总持仓数量限制
|
||||||
positions = await self.client.get_open_positions()
|
positions = await self.client.get_open_positions()
|
||||||
|
try:
|
||||||
|
max_open = int(config.TRADING_CONFIG.get("MAX_OPEN_POSITIONS", 0) or 0)
|
||||||
|
except Exception:
|
||||||
|
max_open = 0
|
||||||
|
if max_open > 0 and len(positions) >= max_open:
|
||||||
|
logger.info(f"{symbol} 持仓数量已达上限:{len(positions)}/{max_open},跳过开仓")
|
||||||
|
return False
|
||||||
|
|
||||||
existing_position = next(
|
existing_position = next(
|
||||||
(p for p in positions if p['symbol'] == symbol),
|
(p for p in positions if p['symbol'] == symbol),
|
||||||
None
|
None
|
||||||
|
|
@ -433,8 +448,58 @@ class RiskManager:
|
||||||
logger.info(f"{symbol} 已有持仓,跳过")
|
logger.info(f"{symbol} 已有持仓,跳过")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 每日开仓次数限制(Redis 计数;无 Redis 时降级为内存计数)
|
||||||
|
try:
|
||||||
|
max_daily = int(config.TRADING_CONFIG.get("MAX_DAILY_ENTRIES", 0) or 0)
|
||||||
|
except Exception:
|
||||||
|
max_daily = 0
|
||||||
|
if max_daily > 0:
|
||||||
|
c = await self._get_daily_entries_count()
|
||||||
|
if c >= max_daily:
|
||||||
|
logger.info(f"{symbol} 今日开仓次数已达上限:{c}/{max_daily},跳过")
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _daily_entries_key(self) -> str:
|
||||||
|
try:
|
||||||
|
aid = int(os.getenv("ATS_ACCOUNT_ID") or os.getenv("ACCOUNT_ID") or 1)
|
||||||
|
except Exception:
|
||||||
|
aid = 1
|
||||||
|
bj = timezone(timedelta(hours=8))
|
||||||
|
d = datetime.now(bj).strftime("%Y%m%d")
|
||||||
|
return f"ats:acc:{aid}:daily_entries:{d}"
|
||||||
|
|
||||||
|
def _seconds_until_beijing_day_end(self) -> int:
|
||||||
|
bj = timezone(timedelta(hours=8))
|
||||||
|
now = datetime.now(bj)
|
||||||
|
end = (now.replace(hour=23, minute=59, second=59, microsecond=0))
|
||||||
|
return max(60, int((end - now).total_seconds()) + 1)
|
||||||
|
|
||||||
|
async def _get_daily_entries_count(self) -> int:
|
||||||
|
key = self._daily_entries_key()
|
||||||
|
try:
|
||||||
|
# redis_cache 已有内存降级逻辑
|
||||||
|
return int(await self.client.redis_cache.get_int(key, 0))
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async def record_entry(self, symbol: str = "") -> None:
|
||||||
|
"""在“开仓真正成功”后调用,用于累计每日开仓次数。"""
|
||||||
|
try:
|
||||||
|
max_daily = int(config.TRADING_CONFIG.get("MAX_DAILY_ENTRIES", 0) or 0)
|
||||||
|
except Exception:
|
||||||
|
max_daily = 0
|
||||||
|
if max_daily <= 0:
|
||||||
|
return
|
||||||
|
key = self._daily_entries_key()
|
||||||
|
ttl = self._seconds_until_beijing_day_end()
|
||||||
|
try:
|
||||||
|
n = await self.client.redis_cache.incr(key, 1, ttl=ttl)
|
||||||
|
logger.info(f"{symbol} 今日开仓计数 +1:{n}/{max_daily}")
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
def get_stop_loss_price(
|
def get_stop_loss_price(
|
||||||
self,
|
self,
|
||||||
entry_price: float,
|
entry_price: float,
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,12 @@ class TradingStrategy:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"自动生成推荐失败 {symbol}: {e}")
|
logger.warning(f"自动生成推荐失败 {symbol}: {e}")
|
||||||
|
|
||||||
|
# 用户风险旋钮:自动交易总开关(关闭则只生成推荐)
|
||||||
|
auto_enabled = bool(config.TRADING_CONFIG.get("AUTO_TRADE_ENABLED", True))
|
||||||
|
if not auto_enabled:
|
||||||
|
logger.info(f"{symbol} 自动交易已关闭(AUTO_TRADE_ENABLED=false),跳过自动下单(推荐已生成)")
|
||||||
|
continue
|
||||||
|
|
||||||
# 提升胜率:可配置的“仅 trending 自动交易”过滤
|
# 提升胜率:可配置的“仅 trending 自动交易”过滤
|
||||||
only_trending = bool(config.TRADING_CONFIG.get("AUTO_TRADE_ONLY_TRENDING", True))
|
only_trending = bool(config.TRADING_CONFIG.get("AUTO_TRADE_ONLY_TRENDING", True))
|
||||||
if only_trending and market_regime != 'trending':
|
if only_trending and market_regime != 'trending':
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user