4294ec401d
- engine/indicators/regime.py: RegimeDetector(四法投票) + MultiTimeframeRegime(多周期并行) 四法: EMA200斜率 / 价格vsEMA200 / ATH回撤 / 窄幅盘整(<3%振幅) 全部 O(1)/bar 增量计算,适用于回测和实时 - engine/example/regime_display.py: 多周期牛熊矩阵展示脚本 独立加载各周期数据 → 运行判定 → 日线对齐矩阵 + 详细拆解 + 统计 输出 engine/backtest/REGIME_MATRIX_BTCUSDT.md - engine/example/regime_mtf_strategy.py: 多周期共识策略 + 四策略对比回测 MTF Consensus: 1w定方向 + 1d确认 + 4h EMA入场 vs Old Regime(单TF基线) vs Long/Short(无过滤) - engine/indicators/__init__.py: 导出 RegimeDetector, MultiTimeframeRegime
269 lines
8.7 KiB
Python
269 lines
8.7 KiB
Python
"""
|
||
多周期牛熊判定模块
|
||
|
||
基于 EMA200 斜率、价格 vs EMA200、ATH 回撤三法投票,支持单周期判定
|
||
和多周期并行分析。每根 bar O(1) 增量更新,适用于回测和实时分析。
|
||
|
||
用法:
|
||
# 单周期
|
||
det = RegimeDetector()
|
||
for price in closes:
|
||
det.update(price)
|
||
regime = det.detect(closes[-1], len(closes) - 1)
|
||
|
||
# 多周期
|
||
mtf = MultiTimeframeRegime(['1h', '4h', '1d', '1w'])
|
||
for price in closes:
|
||
mtf.update(price)
|
||
matrix = mtf.detect_all(closes[-1], len(closes) - 1)
|
||
"""
|
||
|
||
from .incremental import EmaInc
|
||
|
||
|
||
class RegimeDetector:
|
||
"""单周期牛熊判定器 — 四法投票
|
||
|
||
四个独立判定维度:
|
||
1. EMA200 斜率:近 20 根 bar 的 EMA200 变化率 → bull/bear/sideways
|
||
2. 价格 vs EMA200:当前价在 EMA200 上方/下方 → bull/bear
|
||
3. ATH 回撤:当前价距历史高点的回撤幅度 → bull/bear/sideways
|
||
4. 窄幅盘整:近 20 根 bar 收盘价振幅 < 3% → sideways(否则 unknown,不投票)
|
||
|
||
最终判定:四选二投票(>=2 票即定论)。
|
||
"""
|
||
|
||
def __init__(self, range_period: int = 20, range_threshold: float = 0.03):
|
||
self._ath = 0.0
|
||
self._e200 = EmaInc(200)
|
||
self._range_period = range_period
|
||
self._range_threshold = range_threshold
|
||
self._recent_high: list[float] = []
|
||
self._recent_low: list[float] = []
|
||
|
||
def update(self, price: float) -> None:
|
||
"""每根 bar 调一次:更新 ATH + EMA200 + 价格区间窗口"""
|
||
if price > self._ath:
|
||
self._ath = price
|
||
self._e200.update(price)
|
||
# 滚动窗口:维持最近 range_period 根 bar 的最高/最低(只用 close 做代理)
|
||
self._recent_high.append(price)
|
||
self._recent_low.append(price)
|
||
if len(self._recent_high) > self._range_period:
|
||
self._recent_high.pop(0)
|
||
self._recent_low.pop(0)
|
||
|
||
def detect(self, price: float, idx: int) -> str:
|
||
"""返回当前 bar 的牛熊判定:bull / bear / sideways"""
|
||
r1 = self._ema200_slope(idx)
|
||
r2 = self._price_vs_ema200(price, idx)
|
||
r3 = self._ath_drawdown(price)
|
||
r4 = self._price_range()
|
||
|
||
b = sum(1 for r in [r1, r2, r3, r4] if r == "bull")
|
||
br = sum(1 for r in [r1, r2, r3, r4] if r == "bear")
|
||
sx = sum(1 for r in [r1, r2, r3, r4] if r == "sideways")
|
||
|
||
if b >= 2:
|
||
return "bull"
|
||
if br >= 2:
|
||
return "bear"
|
||
if sx >= 2:
|
||
return "sideways"
|
||
return "sideways"
|
||
|
||
def detect_detail(self, price: float, idx: int) -> dict:
|
||
"""返回详细判定(含各子法的结果 + 三态票数)"""
|
||
r1 = self._ema200_slope(idx)
|
||
r2 = self._price_vs_ema200(price, idx)
|
||
r3 = self._ath_drawdown(price)
|
||
r4 = self._price_range()
|
||
b = sum(1 for r in [r1, r2, r3, r4] if r == "bull")
|
||
br = sum(1 for r in [r1, r2, r3, r4] if r == "bear")
|
||
sx = sum(1 for r in [r1, r2, r3, r4] if r == "sideways")
|
||
if b >= 2:
|
||
final = "bull"
|
||
elif br >= 2:
|
||
final = "bear"
|
||
elif sx >= 2:
|
||
final = "sideways"
|
||
else:
|
||
final = "sideways"
|
||
return {
|
||
"final": final,
|
||
"ema200_slope": r1,
|
||
"price_vs_ema200": r2,
|
||
"ath_drawdown": r3,
|
||
"price_range": r4,
|
||
"bull_votes": b,
|
||
"bear_votes": br,
|
||
"sideways_votes": sx,
|
||
"range_pct": self._range_pct(),
|
||
}
|
||
|
||
def _ema200_slope(self, idx: int) -> str:
|
||
if idx < 220:
|
||
return "unknown"
|
||
e200 = self._e200
|
||
if e200[idx - 20] == 0:
|
||
return "unknown"
|
||
slope = (e200[idx] - e200[idx - 20]) / e200[idx - 20]
|
||
if slope > 0.002:
|
||
return "bull"
|
||
if slope < -0.002:
|
||
return "bear"
|
||
return "sideways"
|
||
|
||
def _price_vs_ema200(self, price: float, idx: int) -> str:
|
||
if idx < 210:
|
||
return "unknown"
|
||
e = self._e200[idx]
|
||
if e == 0:
|
||
return "unknown"
|
||
return "bull" if price > e else "bear"
|
||
|
||
def _ath_drawdown(self, price: float) -> str:
|
||
if self._ath == 0:
|
||
return "unknown"
|
||
dd = (price - self._ath) / self._ath
|
||
if dd > -0.20:
|
||
return "bull"
|
||
if dd < -0.40:
|
||
return "bear"
|
||
return "sideways"
|
||
|
||
def _price_range(self) -> str:
|
||
"""窄幅盘整检测:近 range_period 根 bar 振幅 < 阈值 → sideways"""
|
||
if len(self._recent_high) < self._range_period:
|
||
return "unknown"
|
||
highest = max(self._recent_high)
|
||
lowest = min(self._recent_low)
|
||
if highest == 0:
|
||
return "unknown"
|
||
range_pct = (highest - lowest) / highest
|
||
return "sideways" if range_pct < self._range_threshold else "unknown"
|
||
|
||
def _range_pct(self) -> float:
|
||
"""当前波动区间百分比(用于展示)"""
|
||
if len(self._recent_high) < self._range_period:
|
||
return 0.0
|
||
highest = max(self._recent_high)
|
||
if highest == 0:
|
||
return 0.0
|
||
return (highest - min(self._recent_low)) / highest
|
||
|
||
@property
|
||
def ema200(self) -> float:
|
||
"""最新 EMA200 值"""
|
||
return self._e200.current
|
||
|
||
@property
|
||
def ath(self) -> float:
|
||
"""历史最高价"""
|
||
return self._ath
|
||
|
||
@property
|
||
def ready(self) -> bool:
|
||
"""是否已有足够数据做判定"""
|
||
return len(self._e200) >= 220
|
||
|
||
|
||
class MultiTimeframeRegime:
|
||
"""多周期牛熊并行分析
|
||
|
||
管理多个 RegimeDetector 实例,每个对应一个周期。
|
||
所有检测器共用同一价格序列更新,保证 O(1)/bar 的增量计算。
|
||
|
||
Attributes:
|
||
timeframes: 周期列表(如 ['1h', '4h', '1d', '1w'])
|
||
detectors: 周期 → RegimeDetector 映射
|
||
_prices: 价格序列(所有检测器共享)
|
||
"""
|
||
|
||
def __init__(self, timeframes: list[str]):
|
||
self.timeframes = timeframes
|
||
self.detectors: dict[str, RegimeDetector] = {
|
||
tf: RegimeDetector() for tf in timeframes
|
||
}
|
||
self._prices: list[float] = []
|
||
|
||
def update(self, price: float) -> None:
|
||
"""更新所有周期检测器"""
|
||
self._prices.append(price)
|
||
for det in self.detectors.values():
|
||
det.update(price)
|
||
|
||
def detect_all(self) -> dict[str, str]:
|
||
"""返回各周期当前判定(需至少 220 根 bar)"""
|
||
n = len(self._prices)
|
||
if n < 220:
|
||
return {tf: "unknown" for tf in self.timeframes}
|
||
price = self._prices[-1]
|
||
return {
|
||
tf: self.detectors[tf].detect(price, n - 1)
|
||
for tf in self.timeframes
|
||
}
|
||
|
||
def detect_all_detail(self) -> dict[str, dict]:
|
||
"""返回各周期详细判定"""
|
||
n = len(self._prices)
|
||
if n < 220:
|
||
return {tf: {"final": "unknown"} for tf in self.timeframes}
|
||
price = self._prices[-1]
|
||
return {
|
||
tf: self.detectors[tf].detect_detail(price, n - 1)
|
||
for tf in self.timeframes
|
||
}
|
||
|
||
def detect_at(self, idx: int) -> dict[str, str]:
|
||
"""返回指定 bar 位置各周期的判定"""
|
||
if idx < 0 or idx >= len(self._prices):
|
||
raise IndexError(f"idx {idx} out of range [0, {len(self._prices)})")
|
||
price = self._prices[idx]
|
||
result = {}
|
||
for tf in self.timeframes:
|
||
if idx < 220:
|
||
result[tf] = "unknown"
|
||
else:
|
||
result[tf] = self.detectors[tf].detect(price, idx)
|
||
return result
|
||
|
||
def score(self) -> float:
|
||
"""加权综合评分 [-1, 1]
|
||
|
||
各周期赋分:bull=+1, sideways=0, bear=-1
|
||
权重:周期越长权重越大
|
||
|
||
Returns:
|
||
-1.0 (强熊) ~ +1.0 (强牛)
|
||
"""
|
||
n = len(self._prices)
|
||
if n < 220:
|
||
return 0.0
|
||
|
||
# 周期权重:1h=1, 4h=2, 1d=3, 1w=4, 1mon=5
|
||
tf_weights = {
|
||
"1m": 1, "3m": 1, "5m": 1, "15m": 2, "30m": 2,
|
||
"1h": 3, "2h": 3, "4h": 4, "6h": 4, "8h": 4,
|
||
"1d": 5, "1w": 6, "1mon": 7,
|
||
}
|
||
|
||
score_map = {"bull": 1.0, "sideways": 0.0, "bear": -1.0, "unknown": 0.0}
|
||
regimes = self.detect_all()
|
||
total_weight = 0.0
|
||
weighted_score = 0.0
|
||
|
||
for tf, regime in regimes.items():
|
||
w = tf_weights.get(tf, 1)
|
||
weighted_score += score_map[regime] * w
|
||
total_weight += w
|
||
|
||
if total_weight == 0:
|
||
return 0.0
|
||
return weighted_score / total_weight
|
||
|
||
@property
|
||
def ready(self) -> bool:
|
||
"""所有检测器是否都已就绪"""
|
||
return len(self._prices) >= 220
|