Spaces:
Sleeping
Sleeping
Delete volume_analysis.py
Browse files- volume_analysis.py +0 -274
volume_analysis.py
DELETED
|
@@ -1,274 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
volume_analysis.py — Volume & order flow with absorption detection,
|
| 3 |
-
multi-bar breakout confirmation, and fake breakout identification.
|
| 4 |
-
|
| 5 |
-
Key fixes vs prior version:
|
| 6 |
-
- Absorption detection: high-volume small-body bars at resistance = institutional selling
|
| 7 |
-
- Multi-bar breakout confirmation (BREAKOUT_CONFIRMATION_BARS) before firing signal
|
| 8 |
-
- ATR buffer on breakout level (price must exceed level by N*ATR, not just 1 tick)
|
| 9 |
-
- OBV slope computed over configurable window, normalized vs rolling stddev
|
| 10 |
-
- Climax threshold lowered (3.0x) and now triggers a hard absorption check
|
| 11 |
-
- Failed retest detection: breakout that closes back below the level = fake
|
| 12 |
-
"""
|
| 13 |
-
|
| 14 |
-
from typing import Dict, Any
|
| 15 |
-
|
| 16 |
-
import numpy as np
|
| 17 |
-
import pandas as pd
|
| 18 |
-
|
| 19 |
-
from config import (
|
| 20 |
-
VOLUME_MA_PERIOD,
|
| 21 |
-
VOLUME_SPIKE_MULT,
|
| 22 |
-
VOLUME_CLIMAX_MULT,
|
| 23 |
-
VOLUME_WEAK_THRESHOLD,
|
| 24 |
-
BREAKOUT_LOOKBACK,
|
| 25 |
-
BREAKOUT_ATR_BUFFER,
|
| 26 |
-
BREAKOUT_CONFIRMATION_BARS,
|
| 27 |
-
BREAKOUT_RETEST_BARS,
|
| 28 |
-
ABSORPTION_WICK_RATIO,
|
| 29 |
-
ABSORPTION_VOL_MULT,
|
| 30 |
-
ABSORPTION_BODY_RATIO,
|
| 31 |
-
OBV_SLOPE_BARS,
|
| 32 |
-
ATR_PERIOD,
|
| 33 |
-
)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def compute_volume_ma(df: pd.DataFrame, period: int = VOLUME_MA_PERIOD) -> pd.Series:
|
| 37 |
-
return df["volume"].rolling(period).mean()
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def detect_spikes(df: pd.DataFrame, vol_ma: pd.Series) -> pd.Series:
|
| 41 |
-
return df["volume"] > vol_ma * VOLUME_SPIKE_MULT
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def detect_climax(df: pd.DataFrame, vol_ma: pd.Series) -> pd.Series:
|
| 45 |
-
return df["volume"] > vol_ma * VOLUME_CLIMAX_MULT
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def detect_absorption(df: pd.DataFrame, vol_ma: pd.Series) -> pd.Series:
|
| 49 |
-
"""
|
| 50 |
-
Absorption = high-volume bar with small body and large upper wick,
|
| 51 |
-
occurring near recent highs (institutional supply absorbing retail demand).
|
| 52 |
-
|
| 53 |
-
Conditions (all must be true):
|
| 54 |
-
- Volume > ABSORPTION_VOL_MULT * MA
|
| 55 |
-
- Body / range < ABSORPTION_BODY_RATIO (small real body)
|
| 56 |
-
- Upper wick / range > ABSORPTION_WICK_RATIO (large upper wick)
|
| 57 |
-
- Close is in lower half of the bar's range (sellers won the bar)
|
| 58 |
-
"""
|
| 59 |
-
bar_range = (df["high"] - df["low"]).replace(0, np.nan)
|
| 60 |
-
body = (df["close"] - df["open"]).abs()
|
| 61 |
-
upper_wick = df["high"] - df[["close", "open"]].max(axis=1)
|
| 62 |
-
|
| 63 |
-
body_ratio = body / bar_range
|
| 64 |
-
wick_ratio = upper_wick / bar_range
|
| 65 |
-
close_in_lower_half = df["close"] < (df["low"] + bar_range * 0.5)
|
| 66 |
-
|
| 67 |
-
high_volume = df["volume"] > vol_ma * ABSORPTION_VOL_MULT
|
| 68 |
-
small_body = body_ratio < ABSORPTION_BODY_RATIO
|
| 69 |
-
large_wick = wick_ratio > ABSORPTION_WICK_RATIO
|
| 70 |
-
|
| 71 |
-
return high_volume & small_body & large_wick & close_in_lower_half
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def compute_obv(df: pd.DataFrame) -> pd.Series:
|
| 75 |
-
direction = np.sign(df["close"].diff()).fillna(0)
|
| 76 |
-
return (df["volume"] * direction).cumsum()
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def compute_obv_slope(obv: pd.Series, bars: int = OBV_SLOPE_BARS) -> pd.Series:
|
| 80 |
-
"""
|
| 81 |
-
OBV slope normalized by rolling stddev of OBV to make it comparable
|
| 82 |
-
across different price scales. Values > 1 = strong upward flow.
|
| 83 |
-
"""
|
| 84 |
-
x = np.arange(bars)
|
| 85 |
-
|
| 86 |
-
def slope_normalized(window):
|
| 87 |
-
if len(window) < bars:
|
| 88 |
-
return np.nan
|
| 89 |
-
s = np.polyfit(x, window, 1)[0]
|
| 90 |
-
std = np.std(window)
|
| 91 |
-
return s / std if std > 0 else 0.0
|
| 92 |
-
|
| 93 |
-
return obv.rolling(bars).apply(slope_normalized, raw=True)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def compute_delta_approx(df: pd.DataFrame) -> pd.Series:
|
| 97 |
-
body = df["close"] - df["open"]
|
| 98 |
-
wick = (df["high"] - df["low"]).replace(0, np.nan)
|
| 99 |
-
buy_ratio = ((body / wick) * 0.5 + 0.5).clip(0.0, 1.0).fillna(0.5)
|
| 100 |
-
return df["volume"] * buy_ratio - df["volume"] * (1 - buy_ratio)
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
def compute_vwap_deviation(df: pd.DataFrame, period: int = VOLUME_MA_PERIOD) -> pd.Series:
|
| 104 |
-
typical = (df["high"] + df["low"] + df["close"]) / 3
|
| 105 |
-
cum_vp = (typical * df["volume"]).rolling(period).sum()
|
| 106 |
-
cum_vol = df["volume"].rolling(period).sum().replace(0, np.nan)
|
| 107 |
-
vwap = cum_vp / cum_vol
|
| 108 |
-
atr_approx = (df["high"] - df["low"]).rolling(ATR_PERIOD).mean().replace(0, np.nan)
|
| 109 |
-
return (df["close"] - vwap) / atr_approx
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def compute_confirmed_breakout(
|
| 113 |
-
df: pd.DataFrame,
|
| 114 |
-
atr_series: pd.Series,
|
| 115 |
-
vol_ma: pd.Series,
|
| 116 |
-
lookback: int = BREAKOUT_LOOKBACK,
|
| 117 |
-
confirm_bars: int = BREAKOUT_CONFIRMATION_BARS,
|
| 118 |
-
atr_buffer: float = BREAKOUT_ATR_BUFFER,
|
| 119 |
-
) -> pd.Series:
|
| 120 |
-
"""
|
| 121 |
-
Genuine breakout requires ALL of:
|
| 122 |
-
1. Close exceeds prior N-bar high/low by at least atr_buffer * ATR
|
| 123 |
-
2. Close holds above/below that level for confirm_bars consecutive bars
|
| 124 |
-
3. Volume spike on at least one of the confirmation bars
|
| 125 |
-
4. No absorption signal on the breakout bar or confirmation bars
|
| 126 |
-
|
| 127 |
-
Returns: +1 confirmed bull breakout, -1 confirmed bear, 0 none
|
| 128 |
-
"""
|
| 129 |
-
prior_high = df["high"].rolling(lookback).max().shift(lookback)
|
| 130 |
-
prior_low = df["low"].rolling(lookback).min().shift(lookback)
|
| 131 |
-
spike = detect_spikes(df, vol_ma)
|
| 132 |
-
absorption = detect_absorption(df, vol_ma)
|
| 133 |
-
|
| 134 |
-
# Level cleared with buffer
|
| 135 |
-
cleared_up = df["close"] > prior_high + atr_series * atr_buffer
|
| 136 |
-
cleared_dn = df["close"] < prior_low - atr_series * atr_buffer
|
| 137 |
-
|
| 138 |
-
# Rolling confirmation: all bars in last confirm_bars cleared the level
|
| 139 |
-
held_up = cleared_up.rolling(confirm_bars).min().fillna(0).astype(bool)
|
| 140 |
-
held_dn = cleared_dn.rolling(confirm_bars).min().fillna(0).astype(bool)
|
| 141 |
-
|
| 142 |
-
# Volume spike in confirmation window
|
| 143 |
-
vol_ok = spike.rolling(confirm_bars).max().fillna(0).astype(bool)
|
| 144 |
-
|
| 145 |
-
# No absorption in confirmation window
|
| 146 |
-
no_absorption = (~absorption).rolling(confirm_bars).min().fillna(1).astype(bool)
|
| 147 |
-
|
| 148 |
-
signal = pd.Series(0, index=df.index)
|
| 149 |
-
signal[held_up & vol_ok & no_absorption] = 1
|
| 150 |
-
signal[held_dn & vol_ok & no_absorption] = -1
|
| 151 |
-
return signal
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
def detect_failed_breakout(
|
| 155 |
-
df: pd.DataFrame,
|
| 156 |
-
breakout_series: pd.Series,
|
| 157 |
-
atr_series: pd.Series,
|
| 158 |
-
retest_bars: int = BREAKOUT_RETEST_BARS,
|
| 159 |
-
) -> pd.Series:
|
| 160 |
-
"""
|
| 161 |
-
A breakout that closes back below/above the breakout level within
|
| 162 |
-
retest_bars is flagged as a failed (fake) breakout.
|
| 163 |
-
Returns: True where a prior confirmed breakout has since failed.
|
| 164 |
-
"""
|
| 165 |
-
prior_high = df["high"].rolling(BREAKOUT_LOOKBACK).max().shift(BREAKOUT_LOOKBACK)
|
| 166 |
-
prior_low = df["low"].rolling(BREAKOUT_LOOKBACK).min().shift(BREAKOUT_LOOKBACK)
|
| 167 |
-
|
| 168 |
-
had_bull_bo = breakout_series.shift(1).rolling(retest_bars).max().fillna(0) > 0
|
| 169 |
-
had_bear_bo = breakout_series.shift(1).rolling(retest_bars).min().fillna(0) < 0
|
| 170 |
-
|
| 171 |
-
# Failed: price returned below the breakout level
|
| 172 |
-
bull_failed = had_bull_bo & (df["close"] < prior_high.shift(retest_bars))
|
| 173 |
-
bear_failed = had_bear_bo & (df["close"] > prior_low.shift(retest_bars))
|
| 174 |
-
|
| 175 |
-
return bull_failed | bear_failed
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
def analyze_volume(df: pd.DataFrame, atr_series: pd.Series = None) -> Dict[str, Any]:
|
| 179 |
-
if atr_series is None:
|
| 180 |
-
# Fallback: compute simple ATR if not provided
|
| 181 |
-
high, low, prev_close = df["high"], df["low"], df["close"].shift(1)
|
| 182 |
-
tr = pd.concat(
|
| 183 |
-
[high - low, (high - prev_close).abs(), (low - prev_close).abs()],
|
| 184 |
-
axis=1,
|
| 185 |
-
).max(axis=1)
|
| 186 |
-
atr_series = tr.ewm(alpha=1.0 / ATR_PERIOD, adjust=False).mean()
|
| 187 |
-
|
| 188 |
-
vol_ma = compute_volume_ma(df, VOLUME_MA_PERIOD)
|
| 189 |
-
spike_series = detect_spikes(df, vol_ma)
|
| 190 |
-
climax_series = detect_climax(df, vol_ma)
|
| 191 |
-
absorption_series = detect_absorption(df, vol_ma)
|
| 192 |
-
obv = compute_obv(df)
|
| 193 |
-
obv_slope_series = compute_obv_slope(obv, OBV_SLOPE_BARS)
|
| 194 |
-
delta = compute_delta_approx(df)
|
| 195 |
-
vwap_dev = compute_vwap_deviation(df, VOLUME_MA_PERIOD)
|
| 196 |
-
|
| 197 |
-
breakout_series = compute_confirmed_breakout(
|
| 198 |
-
df, atr_series, vol_ma,
|
| 199 |
-
lookback=BREAKOUT_LOOKBACK,
|
| 200 |
-
confirm_bars=BREAKOUT_CONFIRMATION_BARS,
|
| 201 |
-
atr_buffer=BREAKOUT_ATR_BUFFER,
|
| 202 |
-
)
|
| 203 |
-
failed_breakout_series = detect_failed_breakout(df, breakout_series, atr_series)
|
| 204 |
-
|
| 205 |
-
last_vol = float(df["volume"].iloc[-1])
|
| 206 |
-
last_vol_ma = float(vol_ma.iloc[-1]) if not np.isnan(vol_ma.iloc[-1]) else 1.0
|
| 207 |
-
last_spike = bool(spike_series.iloc[-1])
|
| 208 |
-
last_climax = bool(climax_series.iloc[-1])
|
| 209 |
-
last_absorption = bool(absorption_series.iloc[-1])
|
| 210 |
-
last_breakout = int(breakout_series.iloc[-1])
|
| 211 |
-
last_failed_bo = bool(failed_breakout_series.iloc[-1])
|
| 212 |
-
last_obv_slope = float(obv_slope_series.iloc[-1]) if not np.isnan(obv_slope_series.iloc[-1]) else 0.0
|
| 213 |
-
last_vwap_dev = float(vwap_dev.iloc[-1]) if not np.isnan(vwap_dev.iloc[-1]) else 0.0
|
| 214 |
-
|
| 215 |
-
vol_ratio = last_vol / last_vol_ma if last_vol_ma > 0 else 1.0
|
| 216 |
-
weak_vol = vol_ratio < VOLUME_WEAK_THRESHOLD
|
| 217 |
-
|
| 218 |
-
delta_5 = float(delta.iloc[-5:].sum())
|
| 219 |
-
delta_sign = 1 if delta_5 > 0 else -1
|
| 220 |
-
|
| 221 |
-
# Recent failed breakout count (rolling 10 bars) — context for trust level
|
| 222 |
-
recent_failed = int(failed_breakout_series.iloc[-10:].sum())
|
| 223 |
-
|
| 224 |
-
# Score construction
|
| 225 |
-
if last_absorption:
|
| 226 |
-
# Absorption at high: bearish signal masquerading as bullish
|
| 227 |
-
base_score = 0.15
|
| 228 |
-
elif last_climax:
|
| 229 |
-
base_score = 0.25
|
| 230 |
-
elif last_breakout != 0 and not last_failed_bo:
|
| 231 |
-
base_score = 1.0
|
| 232 |
-
elif last_breakout != 0 and last_failed_bo:
|
| 233 |
-
base_score = 0.20
|
| 234 |
-
elif last_spike and not last_absorption:
|
| 235 |
-
base_score = 0.60
|
| 236 |
-
elif vol_ratio >= 1.2:
|
| 237 |
-
base_score = 0.45
|
| 238 |
-
elif vol_ratio >= 0.8:
|
| 239 |
-
base_score = 0.30
|
| 240 |
-
else:
|
| 241 |
-
base_score = 0.10
|
| 242 |
-
|
| 243 |
-
# OBV slope bonus/penalty (normalized)
|
| 244 |
-
obv_bonus = float(np.clip(last_obv_slope * 0.08, -0.12, 0.12))
|
| 245 |
-
|
| 246 |
-
# VWAP deviation bonus for on-side entries
|
| 247 |
-
vwap_bonus = 0.05 if (last_vwap_dev > 0 and last_breakout == 1) else 0.0
|
| 248 |
-
vwap_bonus += 0.05 if (last_vwap_dev < 0 and last_breakout == -1) else 0.0
|
| 249 |
-
|
| 250 |
-
# Penalty for recent failed breakouts (trust decay)
|
| 251 |
-
fake_penalty = min(0.20, recent_failed * 0.05)
|
| 252 |
-
|
| 253 |
-
volume_score = float(np.clip(base_score + obv_bonus + vwap_bonus - fake_penalty, 0.0, 1.0))
|
| 254 |
-
|
| 255 |
-
return {
|
| 256 |
-
"vol_ratio": round(vol_ratio, 3),
|
| 257 |
-
"spike": last_spike,
|
| 258 |
-
"climax": last_climax,
|
| 259 |
-
"absorption": last_absorption,
|
| 260 |
-
"weak": weak_vol,
|
| 261 |
-
"breakout": last_breakout,
|
| 262 |
-
"failed_breakout": last_failed_bo,
|
| 263 |
-
"recent_failed_count": recent_failed,
|
| 264 |
-
"obv_slope_norm": round(last_obv_slope, 4),
|
| 265 |
-
"delta_sum_5": round(delta_5, 2),
|
| 266 |
-
"delta_sign": delta_sign,
|
| 267 |
-
"vwap_deviation": round(last_vwap_dev, 4),
|
| 268 |
-
"volume_score": round(volume_score, 4),
|
| 269 |
-
"spike_series": spike_series,
|
| 270 |
-
"climax_series": climax_series,
|
| 271 |
-
"absorption_series": absorption_series,
|
| 272 |
-
"breakout_series": breakout_series,
|
| 273 |
-
"failed_breakout_series": failed_breakout_series,
|
| 274 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|