GoshawkVortexAI commited on
Commit
59aa1c9
·
verified ·
1 Parent(s): 4d1fcc1

Delete volume_analysis.py

Browse files
Files changed (1) hide show
  1. 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
- }