diff --git a/backend/config_manager.py b/backend/config_manager.py index 67323df..e1cb2db 100644 --- a/backend/config_manager.py +++ b/backend/config_manager.py @@ -814,7 +814,7 @@ class ConfigManager: short_filter_default = False if is_fast else True max_trend_move_default = 0.08 if is_fast else 0.05 - return { + result = { # 仓位控制 'MAX_POSITION_PERCENT': eff_get('MAX_POSITION_PERCENT', 0.12), # 单笔最大保证金占比(12%,加大单笔盈利空间) 'MAX_TOTAL_POSITION_PERCENT': eff_get('MAX_TOTAL_POSITION_PERCENT', 0.40), # 总保证金占比上限 @@ -947,8 +947,47 @@ class ConfigManager: 'SYMBOL_LOSS_COOLDOWN_SEC': eff_get('SYMBOL_LOSS_COOLDOWN_SEC', 3600), # 第一目标止盈最小盈亏比(相对止损距离) 'MIN_RR_FOR_TP1': eff_get('MIN_RR_FOR_TP1', 1.5), # 2026-02-12:保证 TP1 至少 1.5 倍止损距离,改善盈亏比 + + # 市场状态方案(便于在不同行情间切换) + '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 下跌时禁止开多(熊市/保守用) } + # 根据市场方案覆盖关键参数(便于快速切换熊市/牛市/保守等预设) + _SCHEME_PRESETS = { + 'normal': { + 'MIN_STOP_LOSS_PRICE_PCT': 0.03, + 'MAX_POSITION_PERCENT': 0.12, + 'ATR_STOP_LOSS_MULTIPLIER': 2.5, + 'BLOCK_LONG_WHEN_4H_DOWN': False, + }, + '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 下跌不开多 + 'BETA_FILTER_ENABLED': True, + }, + 'bull': { + 'MIN_STOP_LOSS_PRICE_PCT': 0.03, + 'MAX_POSITION_PERCENT': 0.12, + 'ATR_STOP_LOSS_MULTIPLIER': 2.0, + 'BLOCK_LONG_WHEN_4H_DOWN': False, + }, + '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, + 'BETA_FILTER_ENABLED': True, + }, + } + scheme = result.get('MARKET_SCHEME', 'normal') or 'normal' + if scheme in _SCHEME_PRESETS: + for k, v in _SCHEME_PRESETS[scheme].items(): + result[k] = v + return result + def _sync_to_redis(self): """将配置同步到Redis缓存(账号维度)""" if not self._redis_connected or not self._redis_client: diff --git a/backend/sync_global_config_defaults.py b/backend/sync_global_config_defaults.py index 42e37b9..6f338ee 100644 --- a/backend/sync_global_config_defaults.py +++ b/backend/sync_global_config_defaults.py @@ -57,6 +57,13 @@ DEFAULTS_TO_SYNC = [ "description": "止盈按保证金封顶,避免过远"}, {"config_key": "USE_MARGIN_CAP_FOR_SL", "config_value": "true", "config_type": "boolean", "category": "risk", "description": "止损按保证金封顶,避免扛单"}, + # 市场状态方案(2026-02 三项优化 + 方案切换) + {"config_key": "MARKET_SCHEME", "config_value": "normal", "config_type": "string", "category": "strategy", + "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": "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 305a992..209007a 100644 --- a/frontend/src/components/GlobalConfig.jsx +++ b/frontend/src/components/GlobalConfig.jsx @@ -381,6 +381,57 @@ const GlobalConfig = () => { FIXED_RISK_PERCENT: 0.01, // 1% 风险 }, }, + // 市场状态方案:便于在不同行情间一键切换(熊市/正常/牛市/保守) + scheme_bear: { + name: '熊市', + desc: '【熊市专用】放宽止损5%、单仓≤8%、4H下跌禁止开多、大盘过滤开启。避免逆势抄底、减少回撤。', + signatureKeys: ['MARKET_SCHEME'], + configs: { + MARKET_SCHEME: 'bear', + MIN_STOP_LOSS_PRICE_PCT: 0.05, + MAX_POSITION_PERCENT: 0.08, + ATR_STOP_LOSS_MULTIPLIER: 2.5, + BLOCK_LONG_WHEN_4H_DOWN: true, + BETA_FILTER_ENABLED: true, + }, + }, + scheme_normal: { + name: '正常', + desc: '【常规市场】止损3%、单仓12%、4H下跌可开多。适合震荡或温和行情。', + signatureKeys: ['MARKET_SCHEME'], + configs: { + MARKET_SCHEME: 'normal', + MIN_STOP_LOSS_PRICE_PCT: 0.03, + MAX_POSITION_PERCENT: 0.12, + ATR_STOP_LOSS_MULTIPLIER: 2.5, + BLOCK_LONG_WHEN_4H_DOWN: false, + }, + }, + scheme_bull: { + name: '牛市', + desc: '【牛市专用】止损3%、单仓12%、4H下跌可开多、ATR倍数略低。偏积极。', + signatureKeys: ['MARKET_SCHEME'], + configs: { + MARKET_SCHEME: 'bull', + MIN_STOP_LOSS_PRICE_PCT: 0.03, + MAX_POSITION_PERCENT: 0.12, + ATR_STOP_LOSS_MULTIPLIER: 2.0, + BLOCK_LONG_WHEN_4H_DOWN: false, + }, + }, + scheme_conservative: { + name: '保守', + desc: '【极保守】止损6%、单仓6%、4H下跌禁止开多。最小仓位、最严趋势过滤。', + signatureKeys: ['MARKET_SCHEME'], + configs: { + MARKET_SCHEME: '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, + BETA_FILTER_ENABLED: true, + }, + }, aggressive: { name: '激进短线(高风险)', desc: '针对晚间高波动:2%风险/单,交易频率高,止损较紧但止盈合理。注意:回撤风险较高。', @@ -445,6 +496,9 @@ const GlobalConfig = () => { SYNC_RECOVER_MISSING_POSITIONS: { value: true, type: 'boolean', category: 'position', description: '同步时补建「币安有仓、DB 无记录」的交易记录。' }, SYNC_RECOVER_ONLY_WHEN_HAS_SLTP: { value: true, type: 'boolean', category: 'position', description: '仅当该持仓存在止损/止盈单时才补建(未配置 SYSTEM_ORDER_ID_PREFIX 时生效)。' }, 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。' }, + AUTO_MARKET_SCHEME_ENABLED: { value: false, type: 'boolean', category: 'strategy', description: '开启后,crontab 定时运行 update_market_scheme.py --apply 时自动更新 MARKET_SCHEME(根据 BTC 行情识别牛/熊/正常)。' }, } const loadConfigs = async () => { @@ -967,9 +1021,13 @@ const GlobalConfig = () => { swing: { group: 'backup', tag: '纯限价' }, strict: { group: 'backup', tag: '精选低频' }, conservative: { group: 'backup', tag: '保守' }, + scheme_bear: { group: 'scheme', tag: '熊市' }, + scheme_normal: { group: 'scheme', tag: '正常' }, + scheme_bull: { group: 'scheme', tag: '牛市' }, + scheme_conservative: { group: 'scheme', tag: '保守' }, } - // 快速方案:当前推荐(含增加机会/盈利放大/之前盈利)+ 备选 + // 快速方案:当前推荐(含增加机会/盈利放大/之前盈利)+ 备选 + 市场状态方案 const presetGroups = [ { key: 'altcoin', @@ -983,6 +1041,12 @@ const GlobalConfig = () => { desc: '需要更少出单或纯限价时可选:波段回归、精选低频、保守传统。', presetKeys: ['swing', 'strict', 'conservative'], }, + { + key: 'scheme', + title: '市场状态方案', + desc: '按当前行情切换:熊市(放宽止损、降仓、4H 下跌不开多)→ 正常 → 牛市 → 保守(最小仓位、最严过滤)。', + presetKeys: ['scheme_bear', 'scheme_normal', 'scheme_bull', 'scheme_conservative'], + }, ] diff --git a/scripts/UPDATE_MARKET_SCHEME_README.md b/scripts/UPDATE_MARKET_SCHEME_README.md new file mode 100644 index 0000000..c53bad1 --- /dev/null +++ b/scripts/UPDATE_MARKET_SCHEME_README.md @@ -0,0 +1,62 @@ +# 市场行情自动识别与 MARKET_SCHEME 更新 + +## 1. 功能说明 + +`update_market_scheme.py` 基于 Binance 公开 API 自动识别当前市场行情(牛/熊/正常),并可选更新全局配置 `MARKET_SCHEME`,从而自动切换止损、仓位、4H 趋势过滤等参数。 + +**数据来源(无需 API Key):** + +- 24h 涨跌幅:`/fapi/v1/ticker/24hr` +- 资金费率:`/fapi/v1/premiumIndex` +- 大户多空比:`/futures/data/topLongShortPositionRatio` +- 4H K 线:`/fapi/v1/klines`(计算 trend_4h) + +**打分规则:** + +| 指标 | 偏多 (+1) | 偏空 (-1) | +|--------------|-------------|--------------| +| 24h 涨跌幅 | ≥ 2% | ≤ -2% | +| 4H 趋势 | up | down | +| 资金费率 | ≥ 0.00005 | ≤ -0.00005 | +| 大户多空比 | ≥ 1.1 | ≤ 0.9 | + +总分 ≥ 2 → bull,≤ -2 → bear,否则 normal。 + +## 2. 使用方式 + +```bash +# 仅检测并打印(不更新 DB) +python scripts/update_market_scheme.py + +# 若开启 AUTO_MARKET_SCHEME_ENABLED,则更新 DB +python scripts/update_market_scheme.py --apply + +# 强制更新(忽略 AUTO_MARKET_SCHEME_ENABLED 开关) +python scripts/update_market_scheme.py --apply --force +``` + +## 3. 启用自动更新 + +1. 在「全局配置」中设置 `AUTO_MARKET_SCHEME_ENABLED` 为 `true` +2. 或执行一次同步默认配置:`cd backend && python sync_global_config_defaults.py`,然后在全局配置页手动改为 true +3. 配置 crontab 定时执行 `update_market_scheme.py --apply` + +## 4. Crontab 配置示例 + +每 4 小时执行一次(0 点、4 点、8 点…): + +```cron +0 */4 * * * cd /path/to/auto_trade_sys && /path/to/.venv/bin/python scripts/update_market_scheme.py --apply >> /tmp/update_market_scheme.log 2>&1 +``` + +或每 6 小时: + +```cron +0 */6 * * * cd /path/to/auto_trade_sys && /path/to/.venv/bin/python scripts/update_market_scheme.py --apply >> /tmp/update_market_scheme.log 2>&1 +``` + +## 5. 注意事项 + +- 脚本会清除 `global_strategy_config_v5` Redis 缓存,使交易进程下次读取时从 DB 获取最新配置 +- 若希望立即生效,可在前端点击「清除全局缓存」并「重启所有账号交易」 +- 若不开启 `AUTO_MARKET_SCHEME_ENABLED`,脚本仅打印识别结果,不修改 DB;可用 `--force` 临时强制更新 diff --git a/scripts/update_market_scheme.py b/scripts/update_market_scheme.py new file mode 100644 index 0000000..9afda61 --- /dev/null +++ b/scripts/update_market_scheme.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +定时任务:自动识别市场行情并更新 MARKET_SCHEME(需开启 AUTO_MARKET_SCHEME_ENABLED)。 + +用法: + cd /path/to/auto_trade_sys && python scripts/update_market_scheme.py # 仅检测并打印 + python scripts/update_market_scheme.py --apply # 若开启自动模式则更新 DB + python scripts/update_market_scheme.py --apply --force # 强制更新(忽略 AUTO 开关) + +crontab 示例(每 4 小时执行): + 0 */4 * * * cd /path/to/auto_trade_sys && python scripts/update_market_scheme.py --apply >> /tmp/update_market_scheme.log 2>&1 +""" +import argparse +import logging +import os +import sys +from pathlib import Path + +# 项目根目录 +proj = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(proj / "backend")) +sys.path.insert(0, str(proj)) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) +logger = logging.getLogger(__name__) + + +def _get_auto_enabled() -> bool: + """从 DB 读取 AUTO_MARKET_SCHEME_ENABLED,默认 False。""" + try: + from database.models import GlobalStrategyConfig + val = GlobalStrategyConfig.get_value("AUTO_MARKET_SCHEME_ENABLED", False) + if isinstance(val, bool): + return val + if isinstance(val, str): + return val.lower() in ("true", "1", "yes") + return bool(val) + except Exception as e: + logger.warning(f"读取 AUTO_MARKET_SCHEME_ENABLED 失败: {e},默认不自动更新") + return False + + +def _update_market_scheme(scheme: str) -> bool: + """更新 MARKET_SCHEME 到 DB,并清除全局配置 Redis 缓存。""" + try: + from database.models import GlobalStrategyConfig + except ImportError as e: + logger.error(f"导入 GlobalStrategyConfig 失败: {e}") + return False + + try: + GlobalStrategyConfig.set( + "MARKET_SCHEME", + scheme, + "string", + "strategy", + "市场方案:normal / bear / bull / conservative。由 update_market_scheme 定时任务自动更新。", + updated_by="update_market_scheme", + ) + logger.info(f"已更新 MARKET_SCHEME = {scheme}") + + # 清除全局配置 Redis 缓存,让 trading_system 下次读取时从 DB 获取 + try: + import redis + redis_url = os.getenv("REDIS_URL", "redis://localhost:6379") + redis_use_tls = os.getenv("REDIS_USE_TLS", "False").lower() == "true" + redis_username = os.getenv("REDIS_USERNAME", None) + redis_password = os.getenv("REDIS_PASSWORD", None) + if redis_use_tls and not redis_url.startswith("rediss://"): + redis_url = redis_url.replace("redis://", "rediss://", 1) + r = redis.from_url( + redis_url, + username=redis_username, + password=redis_password, + decode_responses=True, + ) + r.ping() + r.delete("global_strategy_config_v5") + logger.info("已清除全局配置 Redis 缓存") + except Exception as e: + logger.warning(f"清除 Redis 缓存失败(不影响 DB 更新): {e}") + + return True + except Exception as e: + logger.error(f"更新 MARKET_SCHEME 失败: {e}", exc_info=True) + return False + + +def main(): + parser = argparse.ArgumentParser(description="自动识别市场行情并可选更新 MARKET_SCHEME") + parser.add_argument("--apply", action="store_true", help="若开启自动模式则更新 DB") + parser.add_argument("--force", action="store_true", help="强制更新(忽略 AUTO_MARKET_SCHEME_ENABLED)") + args = parser.parse_args() + + from trading_system.market_regime_detector import detect_market_regime + + regime, details = detect_market_regime() + logger.info(f"市场行情识别: {regime} | 详情: {details}") + + if not args.apply: + print(f"REGIME={regime} DETAILS={details}") + return 0 + + if not args.force and not _get_auto_enabled(): + logger.info("AUTO_MARKET_SCHEME_ENABLED 未开启,跳过更新。如需强制更新请加 --force") + return 0 + + ok = _update_market_scheme(regime) + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/trading_system/market_regime_detector.py b/trading_system/market_regime_detector.py new file mode 100644 index 0000000..903908a --- /dev/null +++ b/trading_system/market_regime_detector.py @@ -0,0 +1,257 @@ +""" +市场行情自动识别模块 + +基于 Binance 公开 API 判断当前市场状态(牛/熊/正常),用于自动切换 MARKET_SCHEME。 +可被 crontab 定时任务调用(如每 4 小时执行一次)。 + +使用的接口(无需 API Key): +- GET /fapi/v1/ticker/24hr:24h 涨跌幅 +- GET /fapi/v1/premiumIndex:资金费率 +- GET /futures/data/topLongShortPositionRatio:大户多空比 +- GET /fapi/v1/klines:4H K 线(计算 trend_4h) +""" +import json +import logging +import ssl +import urllib.request +from typing import Optional, Tuple + +logger = logging.getLogger(__name__) + +BINANCE_FUTURES_BASE = "https://fapi.binance.com" +BINANCE_FUTURES_DATA = "https://fapi.binance.com/futures/data" +REQUEST_TIMEOUT = 15 + + +def _http_get(url: str, params: Optional[dict] = None) -> Optional[dict]: + """发起 GET 请求,返回 JSON 或 None。""" + if params: + qs = "&".join(f"{k}={v}" for k, v in params.items()) + url = f"{url}?{qs}" + try: + req = urllib.request.Request(url, headers={"Accept": "application/json"}) + ctx = ssl.create_default_context() + with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT, context=ctx) as resp: + raw = resp.read().decode("utf-8") + data = json.loads(raw) + return data + except Exception as e: + logger.warning(f"HTTP GET 失败 {url[:80]}: {e}") + return None + + +def fetch_btc_24h_change() -> Optional[float]: + """获取 BTCUSDT 24h 涨跌幅百分比。""" + data = _http_get(f"{BINANCE_FUTURES_BASE}/fapi/v1/ticker/24hr", {"symbol": "BTCUSDT"}) + if not data or not isinstance(data, dict): + return None + try: + return float(data.get("priceChangePercent", 0)) + except (TypeError, ValueError): + return None + + +def fetch_btc_funding_rate() -> Optional[float]: + """获取 BTCUSDT 最近资金费率。正=多头付费,负=空头付费。""" + data = _http_get(f"{BINANCE_FUTURES_BASE}/fapi/v1/premiumIndex", {"symbol": "BTCUSDT"}) + if not data or not isinstance(data, dict): + return None + try: + return float(data.get("lastFundingRate", 0)) + except (TypeError, ValueError): + return None + + +def fetch_btc_long_short_ratio(period: str = "1d", limit: int = 1) -> Optional[float]: + """获取 BTCUSDT 大户持仓多空比。>1 偏多,<1 偏空。""" + data = _http_get( + f"{BINANCE_FUTURES_DATA}/topLongShortPositionRatio", + {"symbol": "BTCUSDT", "period": period, "limit": limit}, + ) + if not data or not isinstance(data, list) or len(data) == 0: + return None + try: + return float(data[-1].get("longShortRatio", 1)) + except (TypeError, ValueError): + return None + + +def fetch_btc_klines_4h(limit: int = 60) -> Optional[list]: + """获取 BTCUSDT 4H K 线。""" + data = _http_get( + f"{BINANCE_FUTURES_BASE}/fapi/v1/klines", + {"symbol": "BTCUSDT", "interval": "4h", "limit": limit}, + ) + if not data or not isinstance(data, list): + return None + return data + + +def _judge_trend_4h( + price_4h: float, + ema20_4h: Optional[float], + ema50_4h: Optional[float], + macd_hist: Optional[float], +) -> str: + """根据 4H 价格、EMA、MACD 判断趋势方向。""" + if ema20_4h is None: + return "neutral" + + score = 0 + total = 0 + + if price_4h > ema20_4h: + score += 1 + elif price_4h < ema20_4h: + score -= 1 + total += 1 + + if ema50_4h is not None: + if ema20_4h > ema50_4h: + score += 1 + elif ema20_4h < ema50_4h: + score -= 1 + total += 1 + + if macd_hist is not None: + if macd_hist > 0: + score += 1 + elif macd_hist < 0: + score -= 1 + total += 1 + + if total == 0: + return "neutral" + if score >= 2: + return "up" + if score <= -2: + return "down" + return "neutral" + + +def compute_trend_4h_from_klines(klines: list) -> str: + """从 4H K 线计算 trend_4h。""" + try: + from .indicators import TechnicalIndicators + except ImportError: + from indicators import TechnicalIndicators + + closes = [float(k[4]) for k in klines] # 收盘价 + if len(closes) < 50: + return "neutral" + + ema20 = TechnicalIndicators.calculate_ema(closes, 20) + ema50 = TechnicalIndicators.calculate_ema(closes, 50) + macd = TechnicalIndicators.calculate_macd(closes, 12, 26, 9) + macd_hist = macd.get("histogram") if macd else None + + price_4h = closes[-1] + return _judge_trend_4h(price_4h, ema20, ema50, macd_hist) + + +def detect_regime( + change_24h: Optional[float] = None, + funding_rate: Optional[float] = None, + long_short_ratio: Optional[float] = None, + trend_4h: Optional[str] = None, + *, + change_bull: float = 2.0, + change_bear: float = -2.0, + funding_bull: float = 0.00005, + funding_bear: float = -0.00005, + lsr_bull: float = 1.1, + lsr_bear: float = 0.9, +) -> Tuple[str, dict]: + """ + 根据各项指标打分,综合判断市场行情。 + + Returns: + (regime, details): regime 为 'bull'|'bear'|'normal',details 为各项得分详情。 + """ + score = 0 + details = {} + + # 1. 24h 涨跌幅 + if change_24h is not None: + details["change_24h"] = round(change_24h, 2) + if change_24h >= change_bull: + score += 1 + details["change_score"] = 1 + elif change_24h <= change_bear: + score -= 1 + details["change_score"] = -1 + else: + details["change_score"] = 0 + + # 2. 4H 趋势 + if trend_4h: + details["trend_4h"] = trend_4h + if trend_4h == "up": + score += 1 + details["trend_score"] = 1 + elif trend_4h == "down": + score -= 1 + details["trend_score"] = -1 + else: + details["trend_score"] = 0 + + # 3. 资金费率 + if funding_rate is not None: + details["funding_rate"] = round(funding_rate, 6) + if funding_rate >= funding_bull: + score += 1 + details["funding_score"] = 1 + elif funding_rate <= funding_bear: + score -= 1 + details["funding_score"] = -1 + else: + details["funding_score"] = 0 + + # 4. 大户多空比 + if long_short_ratio is not None: + details["long_short_ratio"] = round(long_short_ratio, 4) + if long_short_ratio >= lsr_bull: + score += 1 + details["lsr_score"] = 1 + elif long_short_ratio <= lsr_bear: + score -= 1 + details["lsr_score"] = -1 + else: + details["lsr_score"] = 0 + + details["total_score"] = score + + if score >= 2: + return "bull", details + if score <= -2: + return "bear", details + return "normal", details + + +def detect_market_regime(symbol: str = "BTCUSDT") -> Tuple[str, dict]: + """ + 主入口:拉取 Binance 数据并判断市场行情。 + + Args: + symbol: 基准交易对,默认 BTCUSDT + + Returns: + (regime, details): regime 为 'bull'|'bear'|'normal' + """ + if symbol != "BTCUSDT": + logger.warning("当前仅支持 BTCUSDT,将使用 BTCUSDT") + symbol = "BTCUSDT" + + change_24h = fetch_btc_24h_change() + funding_rate = fetch_btc_funding_rate() + long_short_ratio = fetch_btc_long_short_ratio(period="1d", limit=1) + klines = fetch_btc_klines_4h(limit=60) + trend_4h = compute_trend_4h_from_klines(klines) if klines else None + + regime, details = detect_regime( + change_24h=change_24h, + funding_rate=funding_rate, + long_short_ratio=long_short_ratio, + trend_4h=trend_4h, + ) + return regime, details diff --git a/trading_system/strategy.py b/trading_system/strategy.py index 3553b8b..bac958f 100644 --- a/trading_system/strategy.py +++ b/trading_system/strategy.py @@ -198,6 +198,13 @@ class TradingStrategy: # 确定交易方向(基于技术指标) trade_direction = trade_signal['direction'] + # 4H 下跌禁止开多(熊市/保守方案:避免逆势抄底) + block_long_4h_down = bool(config.TRADING_CONFIG.get("BLOCK_LONG_WHEN_4H_DOWN", False)) + if block_long_4h_down and trade_direction == "BUY" and trade_signal.get("trend_4h") == "down": + logger.info( + f"{symbol} 4H 趋势下跌,禁止开多(BLOCK_LONG_WHEN_4H_DOWN=true),跳过" + ) + continue # 开仓前资金费率过滤:避免在费率极端不利于己方时进场 if not await self._check_funding_rate_filter(symbol, trade_direction): logger.info(f"{symbol} 资金费率过滤未通过,跳过开仓")