diff --git a/backend/config_manager.py b/backend/config_manager.py index e1cb2db..9932d59 100644 --- a/backend/config_manager.py +++ b/backend/config_manager.py @@ -951,6 +951,7 @@ class ConfigManager: # 市场状态方案(便于在不同行情间切换) 'MARKET_SCHEME': str(eff_get('MARKET_SCHEME', 'normal') or 'normal').lower(), 'BLOCK_LONG_WHEN_4H_DOWN': eff_get('BLOCK_LONG_WHEN_4H_DOWN', False), # 4H 下跌时禁止开多(熊市/保守用) + 'BLOCK_SHORT_WHEN_4H_UP': eff_get('BLOCK_SHORT_WHEN_4H_UP', True), # 4H 上涨时禁止开空(默认 True,避免逆势做空) } # 根据市场方案覆盖关键参数(便于快速切换熊市/牛市/保守等预设) @@ -960,12 +961,14 @@ class ConfigManager: 'MAX_POSITION_PERCENT': 0.12, 'ATR_STOP_LOSS_MULTIPLIER': 2.5, 'BLOCK_LONG_WHEN_4H_DOWN': False, + 'BLOCK_SHORT_WHEN_4H_UP': True, # 4H 上涨不开空 }, 'bear': { 'MIN_STOP_LOSS_PRICE_PCT': 0.05, # 放宽止损约 -5% 'MAX_POSITION_PERCENT': 0.08, # 单仓 ≤ 8% 'ATR_STOP_LOSS_MULTIPLIER': 2.5, 'BLOCK_LONG_WHEN_4H_DOWN': True, # 4H 下跌不开多 + 'BLOCK_SHORT_WHEN_4H_UP': True, # 4H 上涨不开空 'BETA_FILTER_ENABLED': True, }, 'bull': { @@ -973,12 +976,14 @@ class ConfigManager: 'MAX_POSITION_PERCENT': 0.12, 'ATR_STOP_LOSS_MULTIPLIER': 2.0, 'BLOCK_LONG_WHEN_4H_DOWN': False, + 'BLOCK_SHORT_WHEN_4H_UP': True, # 4H 上涨不开空(牛市尤需) }, 'conservative': { 'MIN_STOP_LOSS_PRICE_PCT': 0.06, # 最宽松止损 'MAX_POSITION_PERCENT': 0.06, # 最小仓位 'ATR_STOP_LOSS_MULTIPLIER': 2.5, 'BLOCK_LONG_WHEN_4H_DOWN': True, + 'BLOCK_SHORT_WHEN_4H_UP': True, 'BETA_FILTER_ENABLED': True, }, } diff --git a/backend/sync_global_config_defaults.py b/backend/sync_global_config_defaults.py index 6f338ee..2f5ace7 100644 --- a/backend/sync_global_config_defaults.py +++ b/backend/sync_global_config_defaults.py @@ -62,6 +62,8 @@ DEFAULTS_TO_SYNC = [ "description": "市场方案:normal / bear / bull / conservative。切换后自动覆盖止损、仓位、趋势过滤等参数。"}, {"config_key": "BLOCK_LONG_WHEN_4H_DOWN", "config_value": "false", "config_type": "boolean", "category": "strategy", "description": "4H 趋势下跌时禁止开多。bear / conservative 方案下自动为 true。"}, + {"config_key": "BLOCK_SHORT_WHEN_4H_UP", "config_value": "true", "config_type": "boolean", "category": "strategy", + "description": "4H 趋势上涨时禁止开空。默认 true,避免逆势做空导致止损。"}, {"config_key": "AUTO_MARKET_SCHEME_ENABLED", "config_value": "false", "config_type": "boolean", "category": "strategy", "description": "开启后,crontab 定时运行 scripts/update_market_scheme.py --apply 时自动更新 MARKET_SCHEME(根据 BTC 行情识别牛/熊/正常)。"}, ] diff --git a/frontend/src/components/GlobalConfig.jsx b/frontend/src/components/GlobalConfig.jsx index 2f6ff02..b052e0b 100644 --- a/frontend/src/components/GlobalConfig.jsx +++ b/frontend/src/components/GlobalConfig.jsx @@ -384,7 +384,7 @@ const GlobalConfig = () => { // 市场状态方案:便于在不同行情间一键切换(熊市/正常/牛市/保守) scheme_bear: { name: '熊市', - desc: '【熊市专用】放宽止损5%、单仓≤8%、4H下跌禁止开多、大盘过滤开启。避免逆势抄底、减少回撤。', + desc: '【熊市专用】放宽止损5%、单仓≤8%、4H下跌禁止开多、4H上涨禁止开空、大盘过滤开启。避免逆势抄底/做空、减少回撤。', signatureKeys: ['MARKET_SCHEME'], configs: { MARKET_SCHEME: 'bear', @@ -392,12 +392,13 @@ const GlobalConfig = () => { MAX_POSITION_PERCENT: 0.08, ATR_STOP_LOSS_MULTIPLIER: 2.5, BLOCK_LONG_WHEN_4H_DOWN: true, + BLOCK_SHORT_WHEN_4H_UP: true, BETA_FILTER_ENABLED: true, }, }, scheme_normal: { name: '正常', - desc: '【常规市场】止损3%、单仓12%、4H下跌可开多。适合震荡或温和行情。', + desc: '【常规市场】止损3%、单仓12%、4H下跌可开多、4H上涨禁止开空。适合震荡或温和行情。', signatureKeys: ['MARKET_SCHEME'], configs: { MARKET_SCHEME: 'normal', @@ -405,11 +406,12 @@ const GlobalConfig = () => { MAX_POSITION_PERCENT: 0.12, ATR_STOP_LOSS_MULTIPLIER: 2.5, BLOCK_LONG_WHEN_4H_DOWN: false, + BLOCK_SHORT_WHEN_4H_UP: true, }, }, scheme_bull: { name: '牛市', - desc: '【牛市专用】止损3%、单仓12%、4H下跌可开多、ATR倍数略低。偏积极。', + desc: '【牛市专用】止损3%、单仓12%、4H下跌可开多、4H上涨禁止开空、ATR倍数略低。偏积极。', signatureKeys: ['MARKET_SCHEME'], configs: { MARKET_SCHEME: 'bull', @@ -417,11 +419,12 @@ const GlobalConfig = () => { MAX_POSITION_PERCENT: 0.12, ATR_STOP_LOSS_MULTIPLIER: 2.0, BLOCK_LONG_WHEN_4H_DOWN: false, + BLOCK_SHORT_WHEN_4H_UP: true, }, }, scheme_conservative: { name: '保守', - desc: '【极保守】止损6%、单仓6%、4H下跌禁止开多。最小仓位、最严趋势过滤。', + desc: '【极保守】止损6%、单仓6%、4H下跌禁止开多、4H上涨禁止开空。最小仓位、最严趋势过滤。', signatureKeys: ['MARKET_SCHEME'], configs: { MARKET_SCHEME: 'conservative', @@ -429,6 +432,7 @@ const GlobalConfig = () => { MAX_POSITION_PERCENT: 0.06, ATR_STOP_LOSS_MULTIPLIER: 2.5, BLOCK_LONG_WHEN_4H_DOWN: true, + BLOCK_SHORT_WHEN_4H_UP: true, BETA_FILTER_ENABLED: true, }, }, @@ -498,6 +502,7 @@ const GlobalConfig = () => { SYSTEM_ORDER_ID_PREFIX: { value: 'SYS', type: 'string', category: 'position', description: '系统单标识:下单时写入 newClientOrderId 前缀,同步时仅对「开仓订单 clientOrderId 以此前缀开头」的持仓补建;设空则用「是否有止损止盈单」判断。' }, MARKET_SCHEME: { value: 'normal', type: 'string', category: 'strategy', description: '市场方案:normal / bear / bull / conservative。切换后自动覆盖止损、仓位、4H趋势过滤等参数。' }, BLOCK_LONG_WHEN_4H_DOWN: { value: false, type: 'boolean', category: 'strategy', description: '4H 趋势下跌时禁止开多。bear / conservative 方案下自动为 true。' }, + BLOCK_SHORT_WHEN_4H_UP: { value: true, type: 'boolean', category: 'strategy', description: '4H 趋势上涨时禁止开空。默认 true,避免逆势做空导致止损。' }, AUTO_MARKET_SCHEME_ENABLED: { value: false, type: 'boolean', category: 'strategy', description: '开启后,crontab 定时运行 update_market_scheme.py --apply 时自动更新 MARKET_SCHEME(根据 BTC 行情识别牛/熊/正常)。' }, } diff --git a/trading_system/market_scanner.py b/trading_system/market_scanner.py index 776f5ed..8b5f356 100644 --- a/trading_system/market_scanner.py +++ b/trading_system/market_scanner.py @@ -660,6 +660,8 @@ class MarketScanner: except Exception: pass + ema50_4h = None + macd_4h = None use_cached_indicators = False if cached_indicators and cached_indicators.get('last_kline_time') == last_kline_time: # 缓存命中,使用缓存的技术指标 @@ -672,6 +674,8 @@ class MarketScanner: ema20 = cached_indicators.get('ema20') ema50 = cached_indicators.get('ema50') ema20_4h = cached_indicators.get('ema20_4h') + ema50_4h = cached_indicators.get('ema50_4h') + macd_4h = cached_indicators.get('macd_4h') market_regime = cached_indicators.get('marketRegime') else: # 缓存未命中,重新计算技术指标 @@ -683,12 +687,14 @@ class MarketScanner: ema20 = TechnicalIndicators.calculate_ema(close_prices, period=20) ema50 = TechnicalIndicators.calculate_ema(close_prices, period=50) - # 计算4H周期的EMA20用于多周期共振检查 + # 计算4H周期的EMA、MACD用于多周期共振检查 ema20_4h = TechnicalIndicators.calculate_ema(close_prices_4h, period=20) if len(close_prices_4h) >= 20 else None - + ema50_4h = TechnicalIndicators.calculate_ema(close_prices_4h, period=50) if len(close_prices_4h) >= 50 else None + macd_4h = TechnicalIndicators.calculate_macd(close_prices_4h) if len(close_prices_4h) >= 35 else None + # 判断市场状态 market_regime = TechnicalIndicators.detect_market_regime(close_prices) - + # 保存技术指标计算结果到缓存(TTL: 30秒,与K线缓存一致) try: indicators_cache = { @@ -700,6 +706,8 @@ class MarketScanner: 'ema20': ema20, 'ema50': ema50, 'ema20_4h': ema20_4h, + 'ema50_4h': ema50_4h, + 'macd_4h': macd_4h, 'marketRegime': market_regime, } await self.client.redis_cache.set(cache_key_indicators, indicators_cache, ttl=30) @@ -749,16 +757,24 @@ class MarketScanner: # 获取4H周期当前价格(用于判断4H趋势) price_4h = close_prices_4h[-1] if len(close_prices_4h) > 0 else current_price - - # 判断4H周期趋势方向 - trend_4h = None - if ema20_4h is not None: - if price_4h > ema20_4h: - trend_4h = 'up' - elif price_4h < ema20_4h: - trend_4h = 'down' - else: - trend_4h = 'neutral' + + # 判断4H周期趋势方向(优先从 Redis 缓存读取,与 strategy 共用) + try: + from .trend_4h_cache import get_trend_4h_cached + trend_4h = await get_trend_4h_cached( + self.client.redis_cache if self.client else None, + symbol, price_4h, ema20_4h, ema50_4h, macd_4h, + ) + except Exception as e: + logger.debug(f"{symbol} trend_4h 缓存获取失败: {e}") + trend_4h = None + if ema20_4h is not None: + if price_4h > ema20_4h: + trend_4h = 'up' + elif price_4h < ema20_4h: + trend_4h = 'down' + else: + trend_4h = 'neutral' # 策略权重配置(与strategy.py保持一致) TREND_SIGNAL_WEIGHTS = { @@ -881,6 +897,8 @@ class MarketScanner: 'ema20': ema20, 'ema50': ema50, 'ema20_4h': ema20_4h, # 4H周期EMA20,用于多周期共振 + 'ema50_4h': ema50_4h, # 4H周期EMA50 + 'macd_4h': macd_4h, # 4H周期MACD 'price_4h': close_prices_4h[-1] if len(close_prices_4h) > 0 else current_price, # 4H周期当前价格 'marketRegime': market_regime, 'signalScore': signal_score, # 保留用于兼容性 diff --git a/trading_system/redis_ttl.py b/trading_system/redis_ttl.py index 317afc8..338368c 100644 --- a/trading_system/redis_ttl.py +++ b/trading_system/redis_ttl.py @@ -24,6 +24,7 @@ TTL_KLINES_REST = 1800 # REST 拉取的 K 线默认 30 分钟 TTL_KLINES_REST_OLD = 300 # 旧格式 klines:{s}:{i}:{limit} 默认 5 分钟 TTL_LISTEN_KEY = 55 * 60 # 55 分钟(listenKey 缓存) TTL_TREND_STATE = 3600 +TTL_TREND_4H = 600 # 10 分钟(trend_4h 基于 4H K 线,同根 K 线内变化缓慢) TTL_INDICATORS = 30 TTL_RECO_SNAPSHOT = 7200 TTL_RECO_ITEM = 3600 diff --git a/trading_system/strategy.py b/trading_system/strategy.py index bac958f..e88eec0 100644 --- a/trading_system/strategy.py +++ b/trading_system/strategy.py @@ -205,6 +205,13 @@ class TradingStrategy: f"{symbol} 4H 趋势下跌,禁止开多(BLOCK_LONG_WHEN_4H_DOWN=true),跳过" ) continue + # 4H 上涨禁止开空(对称过滤:避免逆势做空,减少上涨行情中空单止损) + block_short_4h_up = bool(config.TRADING_CONFIG.get("BLOCK_SHORT_WHEN_4H_UP", True)) + if block_short_4h_up and trade_direction == "SELL" and trade_signal.get("trend_4h") == "up": + logger.info( + f"{symbol} 4H 趋势上涨,禁止开空(BLOCK_SHORT_WHEN_4H_UP=true),跳过" + ) + continue # 开仓前资金费率过滤:避免在费率极端不利于己方时进场 if not await self._check_funding_rate_filter(symbol, trade_direction): logger.info(f"{symbol} 资金费率过滤未通过,跳过开仓") @@ -552,9 +559,17 @@ class TradingStrategy: ema20_4h = symbol_info.get('ema20_4h') ema50_4h = symbol_info.get('ema50_4h') macd_4h = symbol_info.get('macd_4h') - - # 判断4H周期趋势方向(多指标投票) - trend_4h = self._judge_trend_4h(price_4h, ema20_4h, ema50_4h, macd_4h) + + # 优先从 Redis 缓存读取 trend_4h(基于 WS K 线,同根 4H 内减少重复计算) + try: + from .trend_4h_cache import get_trend_4h_cached + redis_cache = self.client.redis_cache if self.client else None + trend_4h = await get_trend_4h_cached( + redis_cache, symbol, price_4h, ema20_4h, ema50_4h, macd_4h + ) + except Exception as e: + logger.debug(f"{symbol} trend_4h 缓存获取失败,回退本地计算: {e}") + trend_4h = self._judge_trend_4h(price_4h, ema20_4h, ema50_4h, macd_4h) signal_strength = 0 reasons = [] diff --git a/trading_system/trend_4h_cache.py b/trading_system/trend_4h_cache.py new file mode 100644 index 0000000..47226d5 --- /dev/null +++ b/trading_system/trend_4h_cache.py @@ -0,0 +1,96 @@ +""" +trend_4h 缓存模块 + +基于已有 WS K 线缓存,将计算得到的 trend_4h(up/down/neutral)写入 Redis, +供 strategy、market_scanner 等复用,减少重复 EMA/MACD 计算。 +Redis Key: trend_4h:{symbol},TTL 10 分钟。 +""" +import logging +from typing import Optional, Dict, Any + +logger = logging.getLogger(__name__) + +KEY_PREFIX = "trend_4h:" +try: + from .redis_ttl import TTL_TREND_4H +except ImportError: + TTL_TREND_4H = 600 # 10 分钟 + + +def _judge_trend_4h( + price_4h: float, + ema20_4h: Optional[float], + ema50_4h: Optional[float], + macd_4h: Optional[Dict], +) -> str: + """ + 多指标投票判断 4H 趋势(与 strategy._judge_trend_4h 一致)。 + Returns: 'up' | 'down' | 'neutral' + """ + if ema20_4h is None: + return 'neutral' + score = 0 + if price_4h > ema20_4h: + score += 1 + elif price_4h < ema20_4h: + score -= 1 + if ema50_4h is not None: + if ema20_4h > ema50_4h: + score += 1 + elif ema20_4h < ema50_4h: + score -= 1 + if macd_4h and isinstance(macd_4h, dict): + macd_hist = macd_4h.get('histogram', 0) + if macd_hist > 0: + score += 1 + elif macd_hist < 0: + score -= 1 + if score >= 2: + return 'up' + if score <= -2: + return 'down' + return 'neutral' + + +async def get_trend_4h_cached( + redis_cache: Any, + symbol: str, + price_4h: float, + ema20_4h: Optional[float], + ema50_4h: Optional[float], + macd_4h: Optional[Dict], + ttl_sec: int = None, +) -> str: + """ + 优先从 Redis 读取 trend_4h,未命中则计算并写入缓存。 + + Args: + redis_cache: RedisCache 实例(可为 None,则直接计算不缓存) + symbol: 交易对 + price_4h, ema20_4h, ema50_4h, macd_4h: 计算 trend_4h 所需输入 + ttl_sec: 缓存 TTL,默认使用 redis_ttl.TTL_TREND_4H(10 分钟) + + Returns: + 'up' | 'down' | 'neutral' + """ + if ttl_sec is None: + ttl_sec = TTL_TREND_4H + key = f"{KEY_PREFIX}{symbol}" + if redis_cache: + try: + await redis_cache.connect() + cached = await redis_cache.get(key) + if cached and cached in ('up', 'down', 'neutral'): + return cached + except Exception as e: + logger.debug(f"trend_4h 缓存读取失败 {symbol}: {e}") + + trend = _judge_trend_4h(price_4h, ema20_4h, ema50_4h, macd_4h) + + if redis_cache: + try: + await redis_cache.set(key, trend, ttl=ttl_sec) + except Exception as e: + logger.debug(f"trend_4h 缓存写入失败 {symbol}: {e}") + + return trend