auto_trade_sys/trading_system/volatility_pullback.py
薇薇安 bc962d93f9 1
2026-07-08 13:11:48 +08:00

462 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
波动回调入场Volatility Pullback
即时波动模式(推荐):
- 大方向4H 定多空(急跌抄底只要求 4H 向上)
- 触发:当前进行中的 1H K 线快速下冲/上冲,或 15m 短周期波动
- 结构位:已完成 1H K 线的前低/前高(非整段区间极值)作入场与止损参考
"""
from __future__ import annotations
import logging
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
from . import config
logger = logging.getLogger(__name__)
BetaChecker = Callable[[], Awaitable[Dict[str, Any]]]
def _cfg(key: str, default: Any) -> Any:
return config.TRADING_CONFIG.get(key, default)
def _pullback_range_metrics(
klines: Optional[List],
current_price: float,
) -> Optional[Tuple[float, float, float]]:
if not klines or current_price is None:
return None
try:
cur = float(current_price)
except (TypeError, ValueError):
return None
if cur <= 0:
return None
highs: List[float] = []
lows: List[float] = []
for k in klines:
try:
highs.append(float(k[2]))
lows.append(float(k[3]))
except (IndexError, TypeError, ValueError):
continue
if len(highs) < 2:
return None
hi = max(highs)
lo = min(lows)
if hi <= lo:
return None
pos = (cur - lo) / (hi - lo)
return lo, hi, pos
def judge_trend_1h(
price: Optional[float],
ema20: Optional[float],
ema50: Optional[float],
) -> str:
if price is None or ema20 is None:
return "neutral"
try:
p = float(price)
e20 = float(ema20)
except (TypeError, ValueError):
return "neutral"
e50 = None
if ema50 is not None:
try:
e50 = float(ema50)
except (TypeError, ValueError):
e50 = None
if p >= e20 and (e50 is None or e20 >= e50):
return "up"
if p <= e20 and (e50 is None or e20 <= e50):
return "down"
return "neutral"
def _prior_structure_levels(
klines_1h: List,
prior_bars: int,
) -> Optional[Tuple[float, float]]:
"""取已完成 1H K 线(不含当前进行中一根)的前低/前高。"""
if not klines_1h or len(klines_1h) < 2:
return None
completed = klines_1h[:-1]
if not completed:
return None
slice_bars = completed[-max(1, prior_bars):]
lows: List[float] = []
highs: List[float] = []
for k in slice_bars:
try:
lows.append(float(k[3]))
highs.append(float(k[2]))
except (IndexError, TypeError, ValueError):
continue
if not lows or not highs:
return None
return min(lows), max(highs)
def _forming_1h_flush_pcts(klines_1h: List, current_price: float) -> Optional[Dict[str, float]]:
"""
当前进行中 1H K 线内的瞬时波动幅度(相对开盘价)。
flush_down: open -> min(low, price)
flush_up: max(high, price) -> open
"""
if not klines_1h:
return None
forming = klines_1h[-1]
try:
o = float(forming[1])
h = float(forming[2])
lo = float(forming[3])
cur = float(current_price)
except (IndexError, TypeError, ValueError):
return None
if o <= 0:
return None
trough = min(lo, cur)
peak = max(h, cur)
flush_down = max(0.0, (o - trough) / o)
flush_up = max(0.0, (peak - o) / o)
return {
"open": o,
"high": h,
"low": lo,
"flush_down_pct": flush_down,
"flush_up_pct": flush_up,
}
def _price_near_prior_low(price: float, prior_low: float, above_pct: float, below_pct: float) -> bool:
if prior_low <= 0:
return False
upper = prior_low * (1.0 + above_pct)
lower = prior_low * (1.0 - below_pct)
return lower <= price <= upper
def _price_near_prior_high(price: float, prior_high: float, below_pct: float, above_pct: float) -> bool:
if prior_high <= 0:
return False
lower = prior_high * (1.0 - below_pct)
upper = prior_high * (1.0 + above_pct)
return lower <= price <= upper
async def _resolve_live_price(client, symbol: str, symbol_info: Dict) -> Optional[float]:
for src in (
lambda: client.get_realtime_price(symbol) if hasattr(client, "get_realtime_price") else None,
):
try:
v = src()
if v is not None:
return float(v)
except Exception:
pass
raw = symbol_info.get("ticker_price") or symbol_info.get("price")
try:
return float(raw) if raw is not None else None
except (TypeError, ValueError):
return None
async def _period_change(client, symbol: str, interval: str, periods: int) -> Optional[float]:
try:
klines = await client.get_klines(symbol, interval, limit=max(periods + 1, 3))
if not klines or len(klines) < 2:
return None
first_close = float(klines[0][4])
last_close = float(klines[-1][4])
if first_close <= 0:
return None
return (last_close - first_close) / first_close
except Exception as e:
logger.debug(f"{symbol} {interval} 涨跌幅获取失败: {e}")
return None
async def _fetch_klines(client, symbol: str, interval: str, limit: int) -> List:
try:
rows = await client.get_klines(symbol, interval, limit=limit)
return rows or []
except Exception as e:
logger.debug(f"{symbol} 拉取 {interval} K线失败: {e}")
return []
def _fail(reason: str, trend_4h: Optional[str]) -> Dict[str, Any]:
return {
"should_trade": False,
"direction": None,
"reason": reason,
"strength": 0,
"trend_4h": trend_4h,
"trend_1h": None,
"strategy_type": "volatility_pullback",
"volatility_pullback": {},
}
async def _evaluate_instant_flush(
client,
symbol: str,
symbol_info: Dict,
trend_4h: Optional[str],
price_f: float,
beta_checker: Optional[BetaChecker],
) -> Dict[str, Any]:
"""1H 进行中 K 线快速波动 + 前结构低/高点入场(更贴近手动抄底/摸顶)。"""
t4 = (trend_4h or "neutral").strip().lower()
if t4 not in ("up", "down"):
return _fail(f"4H 趋势未明确(4H={t4}),即时模式需 up/down", trend_4h)
direction = "BUY" if t4 == "up" else "SELL"
lookback = max(8, int(_cfg("VP_RANGE_LOOKBACK_BARS", 24) or 24))
prior_bars = max(3, int(_cfg("VP_PRIOR_STRUCTURE_BARS", 12) or 12))
bars_1h = await _fetch_klines(client, symbol, "1h", lookback + 1)
if len(bars_1h) < 4:
return _fail(f"1H K线不足({len(bars_1h)})", trend_4h)
structure = _prior_structure_levels(bars_1h, prior_bars)
if structure is None:
return _fail("无法计算前结构低/高点", trend_4h)
prior_low, prior_high = structure
flush = _forming_1h_flush_pcts(bars_1h, price_f)
if flush is None:
return _fail("无法解析进行中 1H K线", trend_4h)
flush_min = float(_cfg("VP_1H_FLUSH_MIN_PCT", 0.005) or 0.005)
near_above = float(_cfg("VP_NEAR_PRIOR_LOW_PCT", 0.008) or 0.008)
near_below = float(_cfg("VP_MAX_BELOW_PRIOR_LOW_PCT", 0.004) or 0.004)
near_high_below = float(_cfg("VP_NEAR_PRIOR_HIGH_PCT", 0.008) or 0.008)
near_high_above = float(_cfg("VP_MAX_ABOVE_PRIOR_HIGH_PCT", 0.004) or 0.004)
vol_iv = str(_cfg("VP_VOL_INTERVAL", "15m") or "15m")
vol_bars = max(2, int(_cfg("VP_VOL_LOOKBACK_BARS", 3) or 3))
long_min_drop = float(_cfg("VP_LONG_MIN_DROP_PCT", 0.006) or 0.006)
short_min_rise = float(_cfg("VP_SHORT_MIN_RISE_PCT", 0.006) or 0.006)
vol_change = await _period_change(client, symbol, vol_iv, vol_bars)
trend_1h = judge_trend_1h(price_f, symbol_info.get("ema20"), symbol_info.get("ema50"))
reasons: List[str] = []
if direction == "BUY":
flush_ok = flush["flush_down_pct"] >= flush_min
vol_ok = vol_change is not None and vol_change <= -long_min_drop
if not flush_ok and not vol_ok:
fd = flush["flush_down_pct"] * 100
vc = (vol_change or 0) * 100
return _fail(
f"即时波动不足1H下冲{fd:.2f}%<{flush_min*100:.2f}% 且 "
f"{vol_iv}{vc:.2f}%不足-{long_min_drop*100:.2f}%",
trend_4h,
)
if not _price_near_prior_low(price_f, prior_low, near_above, near_below):
return _fail(
f"未贴近前低:现价={price_f:.6f} 前低={prior_low:.6f} "
f"(允许上方{near_above*100:.1f}%/下方{near_below*100:.1f}%)",
trend_4h,
)
if beta_checker is not None:
try:
beta = await beta_checker()
if beta.get("should_block_buy"):
return _fail(f"大盘共振过滤:{beta.get('reason', '禁多')}", trend_4h)
except Exception as e:
logger.debug(f"{symbol} Beta 检查异常(放行): {e}")
if flush_ok:
reasons.append(f"1H进行中下冲{flush['flush_down_pct']*100:.2f}%")
if vol_ok:
reasons.append(f"{vol_iv}{(vol_change or 0)*100:.2f}%")
reasons.append(f"贴近前低{prior_low:.6f}")
reasons.append(f"4H向上")
struct_key, struct_val = "prior_structure_low", prior_low
else:
flush_ok = flush["flush_up_pct"] >= flush_min
vol_ok = vol_change is not None and vol_change >= short_min_rise
if not flush_ok and not vol_ok:
fu = flush["flush_up_pct"] * 100
vc = (vol_change or 0) * 100
return _fail(
f"即时波动不足1H上冲{fu:.2f}%<{flush_min*100:.2f}% 且 "
f"{vol_iv}{vc:.2f}%不足{short_min_rise*100:.2f}%",
trend_4h,
)
if not _price_near_prior_high(price_f, prior_high, near_high_below, near_high_above):
return _fail(
f"未贴近前高:现价={price_f:.6f} 前高={prior_high:.6f} "
f"(允许下方{near_high_below*100:.1f}%/上方{near_high_above*100:.1f}%)",
trend_4h,
)
if flush_ok:
reasons.append(f"1H进行中上冲{flush['flush_up_pct']*100:.2f}%")
if vol_ok:
reasons.append(f"{vol_iv}{(vol_change or 0)*100:.2f}%")
reasons.append(f"贴近前高{prior_high:.6f}")
reasons.append(f"4H向下")
struct_key, struct_val = "prior_structure_high", prior_high
strength = int(_cfg("VP_SIGNAL_STRENGTH", 8) or 8)
strength = max(1, min(10, strength))
metrics = _pullback_range_metrics(bars_1h, price_f)
meta = {
"entry_mode": "instant_flush",
"trend_1h": trend_1h,
"prior_structure_low": prior_low,
"prior_structure_high": prior_high,
"forming_1h_open": flush.get("open"),
"forming_1h_flush_down_pct": round(flush["flush_down_pct"] * 100, 4),
"forming_1h_flush_up_pct": round(flush["flush_up_pct"] * 100, 4),
"vol_interval": vol_iv,
"vol_change_pct": round((vol_change or 0) * 100, 4),
struct_key: struct_val,
"recent_low": prior_low,
"recent_high": prior_high,
}
if metrics:
meta["range_pos"] = round(metrics[2], 4)
return {
"should_trade": True,
"direction": direction,
"reason": "VP即时波动: " + ", ".join(reasons),
"strength": strength,
"trend_4h": t4,
"trend_1h": trend_1h,
"strategy_type": "volatility_pullback",
"volatility_pullback": meta,
}
async def _evaluate_classic_align(
client,
symbol: str,
symbol_info: Dict,
trend_4h: Optional[str],
price_f: float,
beta_checker: Optional[BetaChecker],
) -> Dict[str, Any]:
"""原 4H+1H 同向 + 区间位置 + 15m 波动模式。"""
trend_1h = judge_trend_1h(price_f, symbol_info.get("ema20"), symbol_info.get("ema50"))
t4 = (trend_4h or "neutral").strip().lower()
t1 = trend_1h
require_align = bool(_cfg("VP_REQUIRE_1H_4H_ALIGN", True))
if require_align:
if t4 not in ("up", "down") or t1 not in ("up", "down"):
return _fail(f"4H/1H 趋势未明确(4H={t4}, 1H={t1})", trend_4h)
if t4 != t1:
return _fail(f"4H与1H方向不一致(4H={t4}, 1H={t1})", trend_4h)
direction = "BUY" if t4 == "up" else "SELL"
else:
if t4 == "up":
direction = "BUY"
elif t4 == "down":
direction = "SELL"
else:
return _fail(f"4H 趋势中性,跳过(4H={t4})", trend_4h)
range_iv = str(_cfg("VP_RANGE_INTERVAL", "1h") or "1h")
lookback = max(5, int(_cfg("VP_RANGE_LOOKBACK_BARS", 24) or 24))
min_bars = max(3, int(_cfg("VP_RANGE_MIN_BARS", 5) or 5))
max_long_pos = float(_cfg("VP_MAX_LONG_IN_RANGE", 0.42) or 0.42)
min_short_pos = float(_cfg("VP_MIN_SHORT_IN_RANGE", 0.58) or 0.58)
bars_1h = await _fetch_klines(client, symbol, range_iv, lookback)
metrics = _pullback_range_metrics(bars_1h, price_f)
if metrics is None or len(bars_1h) < min_bars:
return _fail(f"1H 区间数据不足({len(bars_1h)}<{min_bars})", trend_4h)
recent_low, recent_high, pos = metrics
vol_iv = str(_cfg("VP_VOL_INTERVAL", "15m") or "15m")
vol_bars = max(2, int(_cfg("VP_VOL_LOOKBACK_BARS", 5) or 5))
long_min_drop = float(_cfg("VP_LONG_MIN_DROP_PCT", 0.008) or 0.008)
short_min_rise = float(_cfg("VP_SHORT_MIN_RISE_PCT", 0.008) or 0.008)
vol_change = await _period_change(client, symbol, vol_iv, vol_bars)
if vol_change is None:
return _fail(f"{vol_iv} 波动数据不可用", trend_4h)
reasons: List[str] = []
if direction == "BUY":
if pos > max_long_pos:
return _fail(
f"1H 区间位置={pos:.2f} > 做多上限{max_long_pos:.2f}",
trend_4h,
)
if vol_change > -long_min_drop:
return _fail(
f"{vol_iv}×{vol_bars} 跌幅不足:{vol_change*100:.2f}%",
trend_4h,
)
if beta_checker is not None:
try:
beta = await beta_checker()
if beta.get("should_block_buy"):
return _fail(f"大盘共振过滤:{beta.get('reason', '禁多')}", trend_4h)
except Exception as e:
logger.debug(f"{symbol} Beta 检查异常(放行): {e}")
reasons.extend([f"4H/1H同向多", f"1H区间pos={pos:.2f}", f"{vol_iv}{vol_change*100:.2f}%"])
struct_key, struct_val = "recent_low", recent_low
else:
if pos < min_short_pos:
return _fail(f"1H 区间位置={pos:.2f} < 做空下限{min_short_pos:.2f}", trend_4h)
if vol_change < short_min_rise:
return _fail(f"{vol_iv}×{vol_bars} 涨幅不足:{vol_change*100:.2f}%", trend_4h)
reasons.extend([f"4H/1H同向空", f"1H区间pos={pos:.2f}", f"{vol_iv}{vol_change*100:.2f}%"])
struct_key, struct_val = "recent_high", recent_high
strength = max(1, min(10, int(_cfg("VP_SIGNAL_STRENGTH", 8) or 8)))
meta = {
"entry_mode": "classic_align",
"trend_1h": t1,
"range_interval": range_iv,
"range_pos": round(pos, 4),
"recent_low": recent_low,
"recent_high": recent_high,
"vol_interval": vol_iv,
"vol_change_pct": round(vol_change * 100, 4),
struct_key: struct_val,
}
return {
"should_trade": True,
"direction": direction,
"reason": "VP波动回调: " + ", ".join(reasons),
"strength": strength,
"trend_4h": t4,
"trend_1h": t1,
"strategy_type": "volatility_pullback",
"volatility_pullback": meta,
}
async def evaluate_volatility_pullback(
client,
symbol: str,
symbol_info: Dict,
trend_4h: Optional[str],
beta_checker: Optional[BetaChecker] = None,
) -> Dict[str, Any]:
if not bool(_cfg("VOLATILITY_PULLBACK_ENABLED", False)):
return _fail("波动回调策略未启用", trend_4h)
price_f = await _resolve_live_price(client, symbol, symbol_info)
if not price_f or price_f <= 0:
return _fail("无法获取有效价格", trend_4h)
if bool(_cfg("VP_INSTANT_FLUSH_ENABLED", True)):
return await _evaluate_instant_flush(
client, symbol, symbol_info, trend_4h, price_f, beta_checker
)
return await _evaluate_classic_align(
client, symbol, symbol_info, trend_4h, price_f, beta_checker
)