diff --git a/trading_system/config.py b/trading_system/config.py index a10fb20..1ab5f05 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -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 一致)。设为 2~3 可降低周日亏损又不完全停单 'SUNDAY_MAX_OPENS': 3, # 周日信号门槛:周日仅当信号强度>=此值才开仓,0=不提高。建议 8~9 diff --git a/trading_system/main.py b/trading_system/main.py index 9aa2ec5..f15ac75 100644 --- a/trading_system/main.py +++ b/trading_system/main.py @@ -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)}") diff --git a/trading_system/risk_manager.py b/trading_system/risk_manager.py index 08e97a3..2d5120f 100644 --- a/trading_system/risk_manager.py +++ b/trading_system/risk_manager.py @@ -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: """ 检查同一交易对最近的连续亏损次数