feat(risk): Implement global risk lock mechanisms for daily and consecutive losses

Added configuration options for daily loss limits and global consecutive loss thresholds to enhance risk management. Implemented checks in the risk manager to prevent new trades when loss limits are reached, ensuring adherence to risk control principles. Updated logging to reflect the status of these new risk controls in the trading system.
This commit is contained in:
薇薇安 2026-04-26 16:16:18 +08:00
parent c220a9034a
commit 70bc31dd31
3 changed files with 108 additions and 0 deletions

View File

@ -186,6 +186,12 @@ DEFAULT_TRADING_CONFIG = {
'AUTO_TRADE_ENABLED': True, # 自动交易总开关
'MAX_OPEN_POSITIONS': 4, # 同时持仓数量上限总仓位12% / 单笔1.5% = 最多4个
'MAX_DAILY_ENTRIES': 15, # 每日最多15笔快速验证模式提高上限以快速验证策略
# 全局熔断:按账户维度限制“继续恶化”
'DAILY_LOSS_LOCK_ENABLED': True, # 开启日亏损熔断
'DAILY_MAX_LOSS_USDT': 5.0, # 当日净亏pnl-commission达到该值后停止新开仓次日恢复
'GLOBAL_CONSECUTIVE_LOSS_LOCK_ENABLED': True, # 开启全局连续亏损熔断
'GLOBAL_MAX_CONSECUTIVE_LOSSES': 3, # 全局连续亏损达到N笔后熔断
'GLOBAL_LOSS_COOLDOWN_SEC': 3600, # 连亏熔断冷却时间(秒)
# 周日开仓上限周日最多允许开仓次数0=不限制(与 MAX_DAILY_ENTRIES 一致)。设为 23 可降低周日亏损又不完全停单
'SUNDAY_MAX_OPENS': 3,
# 周日信号门槛:周日仅当信号强度>=此值才开仓0=不提高。建议 89

View File

@ -518,6 +518,15 @@ async def main():
logger.info("【风险控制】")
logger.info(f" 固定止损: {config.TRADING_CONFIG['STOP_LOSS_PERCENT']*100:.1f}%")
logger.info(f" 固定止盈: {config.TRADING_CONFIG['TAKE_PROFIT_PERCENT']*100:.1f}%")
logger.info(
f" 日亏熔断: {'开启' if config.TRADING_CONFIG.get('DAILY_LOSS_LOCK_ENABLED', True) else '关闭'} "
f"(上限: -{float(config.TRADING_CONFIG.get('DAILY_MAX_LOSS_USDT', 0) or 0):.2f} USDT/日)"
)
logger.info(
f" 全局连亏熔断: {'开启' if config.TRADING_CONFIG.get('GLOBAL_CONSECUTIVE_LOSS_LOCK_ENABLED', True) else '关闭'} "
f"(阈值: {int(config.TRADING_CONFIG.get('GLOBAL_MAX_CONSECUTIVE_LOSSES', 0) or 0)} 笔, "
f"冷却: {int(config.TRADING_CONFIG.get('GLOBAL_LOSS_COOLDOWN_SEC', 0) or 0)} 秒)"
)
logger.info(f" ATR止损: {'开启' if config.TRADING_CONFIG.get('USE_ATR_STOP_LOSS') else '关闭'}")
logger.info(f" ATR止损倍数: {config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 2.0)}")
logger.info(f" ATR止盈倍数: {config.TRADING_CONFIG.get('ATR_TAKE_PROFIT_MULTIPLIER', 3.0)}")

View File

@ -872,6 +872,12 @@ class RiskManager:
logger.info(f"{symbol} 自动交易已关闭AUTO_TRADE_ENABLED=false跳过")
return False
# 全局风险熔断:日亏上限/全局连亏冷却
global_lock, global_reason = await self._check_global_risk_lock()
if global_lock:
logger.info(f"{symbol} {global_reason},跳过自动开仓")
return False
# 交易对分层:停做名单直接拒绝自动开仓。
try:
symbol_policy = resolve_symbol_trading_policy(symbol)
@ -996,6 +1002,93 @@ class RiskManager:
return True
async def _check_global_risk_lock(self) -> Tuple[bool, str]:
"""
全局风险熔断检查按账户维度
1) 日亏损达到上限 -> 当日禁止开新仓
2) 全局连续亏损达到阈值 -> 进入冷却期冷却内禁止开新仓
"""
try:
# 局部导入,避免非交易场景下不必要的依赖
from database.models import Trade
except Exception:
# 无数据库模型时放行,避免影响主流程
return False, ""
try:
account_id = int(os.getenv("ATS_ACCOUNT_ID") or os.getenv("ACCOUNT_ID") or 1)
except Exception:
account_id = 1
# 1) 日亏损熔断
daily_lock_enabled = bool(config.TRADING_CONFIG.get("DAILY_LOSS_LOCK_ENABLED", True))
if daily_lock_enabled:
limit_usdt = float(config.TRADING_CONFIG.get("DAILY_MAX_LOSS_USDT", 0) or 0)
if limit_usdt > 0:
day_start = self._beijing_day_start_ts()
now_ts = int(time.time())
closed_today = Trade.get_all(
start_timestamp=day_start,
end_timestamp=now_ts,
status='closed',
account_id=account_id,
time_filter='exit',
limit=1000,
) or []
net_pnl = 0.0
for t in closed_today:
pnl = float(t.get('pnl', 0) or 0)
commission = float(t.get('commission', 0) or 0)
net_pnl += (pnl - commission)
if net_pnl <= -abs(limit_usdt):
return True, (
f"[日亏熔断] 今日净盈亏 {net_pnl:.2f} USDT 已达到/低于上限 "
f"-{abs(limit_usdt):.2f} USDT次日自动恢复"
)
# 2) 全局连续亏损冷却
streak_enabled = bool(config.TRADING_CONFIG.get("GLOBAL_CONSECUTIVE_LOSS_LOCK_ENABLED", True))
if streak_enabled:
max_losses = int(config.TRADING_CONFIG.get("GLOBAL_MAX_CONSECUTIVE_LOSSES", 3) or 3)
cooldown_sec = int(config.TRADING_CONFIG.get("GLOBAL_LOSS_COOLDOWN_SEC", 3600) or 3600)
if max_losses > 0 and cooldown_sec > 0:
recent = Trade.get_all(
status='closed',
account_id=account_id,
time_filter='exit',
limit=min(max_losses + 20, 200),
) or []
if recent:
latest_exit_time = int(recent[0].get('exit_time') or recent[0].get('entry_time') or 0)
now_ts = int(time.time())
if latest_exit_time > 0 and now_ts - latest_exit_time < cooldown_sec:
losses = 0
for t in recent:
reason = str(t.get('exit_reason', '') or '').lower()
# 手动干预单不计入策略连亏熔断,避免误伤
if reason in ('manual', 'manual_close'):
continue
pnl = float(t.get('pnl', 0) or 0)
if pnl < 0:
losses += 1
if losses >= max_losses:
remain = max(0, cooldown_sec - (now_ts - latest_exit_time))
return True, (
f"[连亏熔断] 全局连续亏损 {losses} 笔,冷却中 "
f"{remain}s/{cooldown_sec}s"
)
else:
break
return False, ""
def _beijing_day_start_ts(self) -> int:
bj = timezone(timedelta(hours=8))
now_bj = datetime.now(bj)
day_start = now_bj.replace(hour=0, minute=0, second=0, microsecond=0)
return int(day_start.timestamp())
async def _check_recent_losses(self, symbol: str, max_consecutive: int, cooldown_sec: int) -> int:
"""
检查同一交易对最近的连续亏损次数