alphax/app/core/market_regime.py
2026-05-28 00:02:11 +08:00

138 lines
5.1 KiB
Python

"""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"]