"""Market regime classification for strategy risk controls.""" from __future__ import annotations def _safe_float(value, default: float = 0.0) -> float: try: if value is None or value == "": return default return float(value) except Exception: return default def _benchmark_change(overview: dict, symbol: str) -> float: benchmarks = overview.get("benchmarks") if isinstance(overview, dict) else {} item = benchmarks.get(symbol) if isinstance(benchmarks, dict) else {} return _safe_float((item or {}).get("change_24h")) def classify_market_regime(overview: dict | None) -> dict: """Return a plain-language market regime from the latest market snapshot. The first version intentionally uses broad market facts that already exist in `market_overview`: BTC/ETH 24h change, alt breadth, hot/crash counts and funding heat. It is meant to be a guardrail, not a prediction model. """ data = overview if isinstance(overview, dict) else {} if not data or data.get("snapshot_missing") or _safe_float(data.get("sample_count")) <= 0: return { "regime": "unknown", "label": "数据不足", "confidence": 0.2, "risk_level": "medium", "position_multiplier": 0.75, "reasons": ["市场快照不足,保守运行但不直接停止交易"], "metrics": {}, } btc_change = _benchmark_change(data, "BTC/USDT") eth_change = _benchmark_change(data, "ETH/USDT") adv_dec = _safe_float(data.get("advance_decline_ratio")) avg_change = _safe_float(data.get("avg_change_24h")) hot_count = int(_safe_float(data.get("hot_count_5pct"))) crash_count = int(_safe_float(data.get("crash_count_5pct"))) funding = data.get("funding") if isinstance(data.get("funding"), dict) else {} extreme_positive = int(_safe_float(funding.get("extreme_positive_count"))) avg_funding = _safe_float(funding.get("avg_funding_rate")) metrics = { "btc_change_24h": btc_change, "eth_change_24h": eth_change, "advance_decline_ratio": adv_dec, "avg_change_24h": avg_change, "hot_count_5pct": hot_count, "crash_count_5pct": crash_count, "extreme_positive_funding_count": extreme_positive, "avg_funding_rate": avg_funding, } if btc_change <= -3 or eth_change <= -4 or adv_dec < 0.55 or crash_count >= 30: return { "regime": "risk_off", "label": "风险释放期", "confidence": 0.88, "risk_level": "critical", "position_multiplier": 0.25, "reasons": ["主流币或山寨广度明显走弱,新开山寨仓位必须显著降仓并提高质量门槛"], "metrics": metrics, } if crash_count >= 18 or adv_dec < 0.75 or (btc_change <= -1.5 and avg_change < 0): return { "regime": "risk_off", "label": "偏风险释放", "confidence": 0.75, "risk_level": "high", "position_multiplier": 0.35, "reasons": ["市场下跌覆盖面较大,只允许特别高质量的机会"], "metrics": metrics, } if hot_count >= 35 and extreme_positive >= 20 and avg_funding >= 0.00045: return { "regime": "meme_frenzy", "label": "情绪过热期", "confidence": 0.72, "risk_level": "high", "position_multiplier": 0.5, "reasons": ["强势币和正 funding 同时过热,追高回撤风险升高"], "metrics": metrics, } if avg_change >= 0.8 and adv_dec >= 1.2 and hot_count >= 15 and btc_change > -1: return { "regime": "altcoin_rotation", "label": "山寨轮动期", "confidence": 0.78, "risk_level": "medium", "position_multiplier": 1.0, "reasons": ["上涨覆盖面和强势币数量支持继续寻找精选机会"], "metrics": metrics, } if btc_change >= 1.2 and eth_change >= 0.8 and adv_dec >= 0.85: return { "regime": "btc_main_uptrend", "label": "主流带动期", "confidence": 0.7, "risk_level": "medium", "position_multiplier": 0.8, "reasons": ["BTC/ETH 提供方向支撑,但山寨仍要精选"], "metrics": metrics, } if abs(avg_change) <= 0.6 and 0.75 <= adv_dec <= 1.25: return { "regime": "sideways_chop", "label": "横盘震荡期", "confidence": 0.65, "risk_level": "medium", "position_multiplier": 0.65, "reasons": ["市场没有单边方向,追突破容易来回挨打,偏向等回踩"], "metrics": metrics, } return { "regime": "unknown", "label": "结构性行情", "confidence": 0.45, "risk_level": "medium", "position_multiplier": 0.75, "reasons": ["市场环境不够清晰,保持精选和轻仓"], "metrics": metrics, } __all__ = ["classify_market_regime"]