Add pattern features to Torch forecaster
This commit is contained in:
@@ -24,6 +24,18 @@ DEFAULT_TORCH_FEATURES = (
|
||||
"macd_hist_percent",
|
||||
"ema50_gap_percent",
|
||||
"ema200_gap_percent",
|
||||
"pattern_score",
|
||||
"pattern_bullish",
|
||||
"pattern_bearish",
|
||||
"pattern_range",
|
||||
"pattern_pullback",
|
||||
"pattern_oversold_reversal",
|
||||
"pattern_stabilized_drop",
|
||||
"pattern_breakout",
|
||||
"pattern_breakdown",
|
||||
"pattern_fast_drop",
|
||||
"pattern_volume_spike",
|
||||
"pattern_range_position_20",
|
||||
)
|
||||
|
||||
|
||||
@@ -220,9 +232,196 @@ def _feature_value(name: str, candles: list[Candle], index: int, candle: Candle)
|
||||
return _safe_feature((candle.close - candle.ema_50) / close) if candle.ema_50 is not None else 0.0
|
||||
if name == "ema200_gap_percent":
|
||||
return _safe_feature((candle.close - candle.ema_200) / close) if candle.ema_200 is not None else 0.0
|
||||
if name.startswith("pattern_"):
|
||||
return _pattern_feature_value(name, candles, index)
|
||||
return 0.0
|
||||
|
||||
|
||||
def _pattern_feature_value(name: str, candles: list[Candle], index: int) -> float:
|
||||
pattern = _pattern_snapshot(candles, index)
|
||||
if name == "pattern_score":
|
||||
return pattern["score"]
|
||||
if name == "pattern_bullish":
|
||||
return pattern["bullish"]
|
||||
if name == "pattern_bearish":
|
||||
return pattern["bearish"]
|
||||
if name == "pattern_range":
|
||||
return pattern["range"]
|
||||
if name == "pattern_pullback":
|
||||
return pattern["pullback"]
|
||||
if name == "pattern_oversold_reversal":
|
||||
return pattern["oversold_reversal"]
|
||||
if name == "pattern_stabilized_drop":
|
||||
return pattern["stabilized_drop"]
|
||||
if name == "pattern_breakout":
|
||||
return pattern["breakout"]
|
||||
if name == "pattern_breakdown":
|
||||
return pattern["breakdown"]
|
||||
if name == "pattern_fast_drop":
|
||||
return pattern["fast_drop"]
|
||||
if name == "pattern_volume_spike":
|
||||
return pattern["volume_spike"]
|
||||
if name == "pattern_range_position_20":
|
||||
return pattern["range_position_20"]
|
||||
return 0.0
|
||||
|
||||
|
||||
def _pattern_snapshot(candles: list[Candle], index: int) -> dict[str, float]:
|
||||
if index < 29:
|
||||
return {
|
||||
"score": 0.0,
|
||||
"bullish": 0.0,
|
||||
"bearish": 0.0,
|
||||
"range": 0.0,
|
||||
"pullback": 0.0,
|
||||
"oversold_reversal": 0.0,
|
||||
"stabilized_drop": 0.0,
|
||||
"breakout": 0.0,
|
||||
"breakdown": 0.0,
|
||||
"fast_drop": 0.0,
|
||||
"volume_spike": 0.0,
|
||||
"range_position_20": 0.5,
|
||||
}
|
||||
|
||||
window = candles[: index + 1]
|
||||
latest = window[-1]
|
||||
previous = window[-2]
|
||||
high20 = max(candle.high for candle in window[-20:])
|
||||
low20 = min(candle.low for candle in window[-20:])
|
||||
width20 = max(0.0, high20 - low20)
|
||||
range_position_20 = _clamp((latest.close - low20) / width20, 0.0, 1.0) if width20 else 0.5
|
||||
close_3 = window[-4].close if len(window) >= 4 else window[0].close
|
||||
close_10 = window[-11].close if len(window) >= 11 else window[0].close
|
||||
close_20 = window[-21].close if len(window) >= 21 else window[0].close
|
||||
ret_3 = _percent_change(latest.close, close_3)
|
||||
ret_10 = _percent_change(latest.close, close_10)
|
||||
ret_20 = _percent_change(latest.close, close_20)
|
||||
body = abs(latest.close - latest.open)
|
||||
lower_wick = max(0.0, min(latest.open, latest.close) - latest.low)
|
||||
atr_percent = (latest.atr_14 / latest.close * 100) if latest.atr_14 and latest.close else 0.0
|
||||
volume_ratio = (
|
||||
latest.volume / latest.volume_ma_20
|
||||
if latest.volume_ma_20 and latest.volume_ma_20 > 0
|
||||
else 0.0
|
||||
)
|
||||
ema_gap_percent = (
|
||||
(latest.ema_50 - latest.ema_200) / latest.ema_200 * 100
|
||||
if latest.ema_50 and latest.ema_200
|
||||
else 0.0
|
||||
)
|
||||
uptrend = bool(
|
||||
latest.ema_20
|
||||
and latest.ema_50
|
||||
and latest.ema_200
|
||||
and latest.ema_20 >= latest.ema_50 >= latest.ema_200
|
||||
and latest.close >= latest.ema_50
|
||||
)
|
||||
downtrend = bool(
|
||||
latest.ema_20
|
||||
and latest.ema_50
|
||||
and latest.ema_200
|
||||
and latest.ema_20 <= latest.ema_50 <= latest.ema_200
|
||||
and latest.close <= latest.ema_50
|
||||
)
|
||||
pullback = bool(
|
||||
latest.ema_20
|
||||
and uptrend
|
||||
and latest.close <= latest.ema_20 * 1.012
|
||||
and latest.rsi_14 is not None
|
||||
and 35 <= latest.rsi_14 <= 58
|
||||
)
|
||||
oversold_reversal = bool(
|
||||
latest.rsi_14 is not None
|
||||
and latest.rsi_14 <= 35
|
||||
and latest.close > previous.close
|
||||
and lower_wick >= body * 1.2
|
||||
)
|
||||
stabilized_drop = _pattern_stabilized_drop(
|
||||
candles=window,
|
||||
latest=latest,
|
||||
previous=previous,
|
||||
ret_3=ret_3,
|
||||
ret_10=ret_10,
|
||||
ret_20=ret_20,
|
||||
atr_percent=atr_percent,
|
||||
volume_ratio=volume_ratio,
|
||||
lower_wick=lower_wick,
|
||||
body=body,
|
||||
)
|
||||
breakout = bool(latest.close >= high20 * 0.995 and volume_ratio >= 1.15 and latest.close > latest.open)
|
||||
breakdown = bool(latest.close <= low20 * 1.005 and volume_ratio >= 1.1 and latest.close < latest.open)
|
||||
fast_drop = bool(ret_3 <= -max(1.2, atr_percent * 1.8) or (latest.rsi_14 or 100) <= 25)
|
||||
range_market = bool(abs(ret_20) <= max(0.8, atr_percent * 1.2) and abs(ema_gap_percent) <= 0.35)
|
||||
volume_spike = bool(volume_ratio >= 1.6)
|
||||
|
||||
score = 0.50
|
||||
if fast_drop and breakdown:
|
||||
score = 0.18
|
||||
elif breakdown:
|
||||
score = 0.24
|
||||
elif pullback:
|
||||
score = 0.76
|
||||
elif oversold_reversal:
|
||||
score = 0.68
|
||||
elif stabilized_drop:
|
||||
score = 0.58
|
||||
elif breakout:
|
||||
score = 0.72
|
||||
elif uptrend:
|
||||
score = 0.64
|
||||
elif range_market:
|
||||
score = 0.48
|
||||
elif downtrend:
|
||||
score = 0.28
|
||||
bullish = float(pullback or oversold_reversal or stabilized_drop or breakout or uptrend)
|
||||
bearish = float((fast_drop and breakdown) or breakdown or downtrend)
|
||||
return {
|
||||
"score": score,
|
||||
"bullish": bullish,
|
||||
"bearish": bearish,
|
||||
"range": float(range_market),
|
||||
"pullback": float(pullback),
|
||||
"oversold_reversal": float(oversold_reversal),
|
||||
"stabilized_drop": float(stabilized_drop),
|
||||
"breakout": float(breakout),
|
||||
"breakdown": float(breakdown),
|
||||
"fast_drop": float(fast_drop),
|
||||
"volume_spike": float(volume_spike),
|
||||
"range_position_20": range_position_20,
|
||||
}
|
||||
|
||||
|
||||
def _percent_change(current: float, previous: float) -> float:
|
||||
return ((current - previous) / previous * 100) if previous else 0.0
|
||||
|
||||
|
||||
def _pattern_stabilized_drop(
|
||||
*,
|
||||
candles: list[Candle],
|
||||
latest: Candle,
|
||||
previous: Candle,
|
||||
ret_3: float,
|
||||
ret_10: float,
|
||||
ret_20: float,
|
||||
atr_percent: float,
|
||||
volume_ratio: float,
|
||||
lower_wick: float,
|
||||
body: float,
|
||||
) -> bool:
|
||||
recent_drop = ret_10 <= -max(0.35, atr_percent * 1.1) or ret_20 <= -max(0.6, atr_percent * 1.6)
|
||||
if not recent_drop or latest.rsi_14 is None or latest.rsi_14 > 52:
|
||||
return False
|
||||
recent_lows = [candle.low for candle in candles[-5:-1]]
|
||||
no_new_low = bool(recent_lows) and latest.low >= min(recent_lows) * 0.999
|
||||
bounce_from_low = ((latest.close - min(candle.low for candle in candles[-6:])) / latest.close * 100) if latest.close else 0.0
|
||||
body_base = max(body, latest.close * 0.0001)
|
||||
absorption = lower_wick >= body_base * 0.6 or bounce_from_low >= max(0.08, atr_percent * 0.3)
|
||||
momentum_stabilized = latest.close >= previous.close or abs(ret_3) <= max(0.25, atr_percent * 0.8) or no_new_low
|
||||
volume_present = volume_ratio >= 0.55
|
||||
continuing_drop = latest.close < previous.close and not no_new_low and ret_3 <= -max(0.6, atr_percent * 1.2)
|
||||
return bool(momentum_stabilized and absorption and volume_present and not continuing_drop)
|
||||
|
||||
|
||||
def _log_change(current: float, previous: float) -> float:
|
||||
if current <= 0 or previous <= 0:
|
||||
return 0.0
|
||||
|
||||
Reference in New Issue
Block a user