Create regime.py
Browse files
regime.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
from config import ATR_PERIOD, STRUCTURE_LOOKBACK, VOLATILITY_EXPANSION_MULTIPLIER
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def compute_atr(df: pd.DataFrame, period: int = ATR_PERIOD) -> pd.Series:
|
| 8 |
+
high = df["high"]
|
| 9 |
+
low = df["low"]
|
| 10 |
+
close = df["close"]
|
| 11 |
+
prev_close = close.shift(1)
|
| 12 |
+
tr = pd.concat(
|
| 13 |
+
[
|
| 14 |
+
high - low,
|
| 15 |
+
(high - prev_close).abs(),
|
| 16 |
+
(low - prev_close).abs(),
|
| 17 |
+
],
|
| 18 |
+
axis=1,
|
| 19 |
+
).max(axis=1)
|
| 20 |
+
atr = tr.ewm(span=period, adjust=False).mean()
|
| 21 |
+
return atr
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def detect_structure(df: pd.DataFrame, lookback: int = STRUCTURE_LOOKBACK) -> pd.Series:
|
| 25 |
+
highs = df["high"].rolling(lookback).max()
|
| 26 |
+
lows = df["low"].rolling(lookback).min()
|
| 27 |
+
prev_highs = highs.shift(lookback // 2)
|
| 28 |
+
prev_lows = lows.shift(lookback // 2)
|
| 29 |
+
hh = highs > prev_highs
|
| 30 |
+
hl = lows > prev_lows
|
| 31 |
+
lh = highs < prev_highs
|
| 32 |
+
ll = lows < prev_lows
|
| 33 |
+
structure = pd.Series(0, index=df.index)
|
| 34 |
+
structure[hh & hl] = 1
|
| 35 |
+
structure[lh & ll] = -1
|
| 36 |
+
return structure
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def compute_volatility_regime(df: pd.DataFrame, period: int = ATR_PERIOD) -> pd.Series:
|
| 40 |
+
atr = compute_atr(df, period)
|
| 41 |
+
atr_ma = atr.rolling(period * 2).mean()
|
| 42 |
+
ratio = atr / atr_ma.replace(0, np.nan)
|
| 43 |
+
return ratio
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def detect_regime(df: pd.DataFrame) -> Dict[str, Any]:
|
| 47 |
+
atr = compute_atr(df, ATR_PERIOD)
|
| 48 |
+
structure = detect_structure(df, STRUCTURE_LOOKBACK)
|
| 49 |
+
vol_ratio = compute_volatility_regime(df, ATR_PERIOD)
|
| 50 |
+
|
| 51 |
+
last_atr = atr.iloc[-1]
|
| 52 |
+
last_structure = structure.iloc[-1]
|
| 53 |
+
last_vol_ratio = vol_ratio.iloc[-1]
|
| 54 |
+
|
| 55 |
+
recent_structure = structure.iloc[-5:]
|
| 56 |
+
bullish_count = (recent_structure == 1).sum()
|
| 57 |
+
bearish_count = (recent_structure == -1).sum()
|
| 58 |
+
|
| 59 |
+
if bullish_count >= 3:
|
| 60 |
+
trend = "bullish"
|
| 61 |
+
elif bearish_count >= 3:
|
| 62 |
+
trend = "bearish"
|
| 63 |
+
else:
|
| 64 |
+
trend = "ranging"
|
| 65 |
+
|
| 66 |
+
volatility_expanding = last_vol_ratio > VOLATILITY_EXPANSION_MULTIPLIER
|
| 67 |
+
|
| 68 |
+
if trend == "bullish" and not volatility_expanding:
|
| 69 |
+
regime_score = 1.0
|
| 70 |
+
elif trend == "bullish" and volatility_expanding:
|
| 71 |
+
regime_score = 0.6
|
| 72 |
+
elif trend == "ranging":
|
| 73 |
+
regime_score = 0.3
|
| 74 |
+
elif trend == "bearish" and not volatility_expanding:
|
| 75 |
+
regime_score = 0.2
|
| 76 |
+
else:
|
| 77 |
+
regime_score = 0.1
|
| 78 |
+
|
| 79 |
+
close = df["close"].iloc[-1]
|
| 80 |
+
atr_pct = last_atr / close if close > 0 else 0.0
|
| 81 |
+
|
| 82 |
+
return {
|
| 83 |
+
"atr": last_atr,
|
| 84 |
+
"atr_pct": atr_pct,
|
| 85 |
+
"structure": last_structure,
|
| 86 |
+
"trend": trend,
|
| 87 |
+
"vol_ratio": last_vol_ratio,
|
| 88 |
+
"volatility_expanding": volatility_expanding,
|
| 89 |
+
"regime_score": regime_score,
|
| 90 |
+
"atr_series": atr,
|
| 91 |
+
"structure_series": structure,
|
| 92 |
+
"vol_ratio_series": vol_ratio,
|
| 93 |
+
}
|