Spaces:
Sleeping
Sleeping
Update volume_analysis.py
Browse files- volume_analysis.py +198 -44
volume_analysis.py
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from typing import Dict, Any
|
| 2 |
|
| 3 |
import numpy as np
|
|
@@ -9,6 +22,14 @@ from config import (
|
|
| 9 |
VOLUME_CLIMAX_MULT,
|
| 10 |
VOLUME_WEAK_THRESHOLD,
|
| 11 |
BREAKOUT_LOOKBACK,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
)
|
| 13 |
|
| 14 |
|
|
@@ -16,105 +37,238 @@ def compute_volume_ma(df: pd.DataFrame, period: int = VOLUME_MA_PERIOD) -> pd.Se
|
|
| 16 |
return df["volume"].rolling(period).mean()
|
| 17 |
|
| 18 |
|
| 19 |
-
def detect_spikes(df: pd.DataFrame,
|
| 20 |
-
vol_ma = compute_volume_ma(df, period)
|
| 21 |
return df["volume"] > vol_ma * VOLUME_SPIKE_MULT
|
| 22 |
|
| 23 |
|
| 24 |
-
def detect_climax(df: pd.DataFrame,
|
| 25 |
-
vol_ma = compute_volume_ma(df, period)
|
| 26 |
return df["volume"] > vol_ma * VOLUME_CLIMAX_MULT
|
| 27 |
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
def compute_obv(df: pd.DataFrame) -> pd.Series:
|
| 30 |
direction = np.sign(df["close"].diff()).fillna(0)
|
| 31 |
return (df["volume"] * direction).cumsum()
|
| 32 |
|
| 33 |
|
| 34 |
-
def
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
def compute_delta_approx(df: pd.DataFrame) -> pd.Series:
|
| 44 |
body = df["close"] - df["open"]
|
| 45 |
wick = (df["high"] - df["low"]).replace(0, np.nan)
|
| 46 |
buy_ratio = ((body / wick) * 0.5 + 0.5).clip(0.0, 1.0).fillna(0.5)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
def compute_breakout_signal(df: pd.DataFrame, lookback: int = BREAKOUT_LOOKBACK) -> pd.Series:
|
| 53 |
-
prior_high = df["close"].rolling(lookback).max().shift(1)
|
| 54 |
-
prior_low = df["close"].rolling(lookback).min().shift(1)
|
| 55 |
-
spikes = detect_spikes(df)
|
| 56 |
signal = pd.Series(0, index=df.index)
|
| 57 |
-
signal[
|
| 58 |
-
signal[
|
| 59 |
return signal
|
| 60 |
|
| 61 |
|
| 62 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
vol_ma = compute_volume_ma(df, VOLUME_MA_PERIOD)
|
| 64 |
-
spike_series = detect_spikes(df,
|
| 65 |
-
climax_series = detect_climax(df,
|
| 66 |
-
|
| 67 |
obv = compute_obv(df)
|
|
|
|
| 68 |
delta = compute_delta_approx(df)
|
| 69 |
vwap_dev = compute_vwap_deviation(df, VOLUME_MA_PERIOD)
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
last_vol = float(df["volume"].iloc[-1])
|
| 72 |
last_vol_ma = float(vol_ma.iloc[-1]) if not np.isnan(vol_ma.iloc[-1]) else 1.0
|
| 73 |
last_spike = bool(spike_series.iloc[-1])
|
| 74 |
last_climax = bool(climax_series.iloc[-1])
|
|
|
|
| 75 |
last_breakout = int(breakout_series.iloc[-1])
|
|
|
|
|
|
|
| 76 |
last_vwap_dev = float(vwap_dev.iloc[-1]) if not np.isnan(vwap_dev.iloc[-1]) else 0.0
|
| 77 |
|
| 78 |
vol_ratio = last_vol / last_vol_ma if last_vol_ma > 0 else 1.0
|
|
|
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
obv_normalized = obv_slope / (abs(obv_recent.mean()) + 1e-10)
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
base_score = 0.
|
| 91 |
-
elif
|
|
|
|
|
|
|
| 92 |
base_score = 1.0
|
| 93 |
-
elif
|
| 94 |
-
base_score = 0.
|
|
|
|
|
|
|
| 95 |
elif vol_ratio >= 1.2:
|
| 96 |
-
base_score = 0.
|
| 97 |
elif vol_ratio >= 0.8:
|
| 98 |
-
base_score = 0.
|
| 99 |
else:
|
| 100 |
-
base_score = 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
-
|
| 103 |
-
vwap_bonus = 0.05 if last_vwap_dev > 0 and last_breakout == 1 else 0.0
|
| 104 |
-
volume_score = float(np.clip(base_score + obv_bonus + vwap_bonus, 0.0, 1.0))
|
| 105 |
|
| 106 |
return {
|
| 107 |
"vol_ratio": round(vol_ratio, 3),
|
| 108 |
"spike": last_spike,
|
| 109 |
"climax": last_climax,
|
|
|
|
| 110 |
"weak": weak_vol,
|
| 111 |
"breakout": last_breakout,
|
| 112 |
-
"
|
| 113 |
-
"
|
|
|
|
|
|
|
| 114 |
"delta_sign": delta_sign,
|
| 115 |
"vwap_deviation": round(last_vwap_dev, 4),
|
| 116 |
"volume_score": round(volume_score, 4),
|
| 117 |
"spike_series": spike_series,
|
| 118 |
"climax_series": climax_series,
|
|
|
|
| 119 |
"breakout_series": breakout_series,
|
|
|
|
| 120 |
}
|
|
|
|
| 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
|
|
|
|
| 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 |
|
|
|
|
| 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 |
}
|