feat(config): 添加4H趋势过滤配置以优化交易策略

在配置管理中新增 `BLOCK_SHORT_WHEN_4H_UP` 参数,允许在4H上涨时禁止开空,增强策略灵活性与风险控制。同时,更新前端组件以展示该配置,提升用户体验。此改动确保在不同市场条件下,策略能够更有效地避免逆势操作。
This commit is contained in:
薇薇安 2026-02-22 22:51:36 +08:00
parent cbf778d560
commit cddcf35481
7 changed files with 162 additions and 20 deletions

View File

@ -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,
},
}

View File

@ -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 行情识别牛/熊/正常)。"},
]

View File

@ -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 行情识别牛/熊/正常)。' },
}

View File

@ -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,8 +687,10 @@ 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)
@ -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)
@ -750,7 +758,15 @@ class MarketScanner:
# 获取4H周期当前价格用于判断4H趋势
price_4h = close_prices_4h[-1] if len(close_prices_4h) > 0 else current_price
# 判断4H周期趋势方向
# 判断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:
@ -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, # 保留用于兼容性

View File

@ -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

View File

@ -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} 资金费率过滤未通过,跳过开仓")
@ -553,7 +560,15 @@ class TradingStrategy:
ema50_4h = symbol_info.get('ema50_4h')
macd_4h = symbol_info.get('macd_4h')
# 判断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

View File

@ -0,0 +1,96 @@
"""
trend_4h 缓存模块
基于已有 WS K 线缓存将计算得到的 trend_4hup/down/neutral写入 Redis
strategymarket_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_4H10 分钟
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