alphax/app/strategies/short_breakdown.py
2026-06-07 20:29:45 +08:00

240 lines
10 KiB
Python

"""Independent short strategy builders.
Short setups are not inverted long setups. They need their own trigger,
confirmation, invalidation and review lineage.
"""
from __future__ import annotations
from app.core.factor_roles import CONFIRMATION, ENTRY, RISK, TRIGGER
from app.core.strategy_contract import StrategySignal, current_strategy_version
from app.core.strategy_registry import BREAKDOWN_RETEST_SHORT_1H_STRATEGY, SHORT_WEAK_BOUNCE_FAILURE_STRATEGY
def _safe_float(value, default=0.0) -> float:
try:
if value is None or value == "":
return default
return float(value)
except Exception:
return default
def detect_breakdown_retest_short_1h(df, *, change_24h: float = 0.0) -> dict:
"""Detect a 1H breakdown followed by a failed retest of the broken box.
This is intentionally conservative: the setup needs a prior box, a close
below support, a retest near the broken support, and rejection back below it.
"""
if df is None or len(df) < 60:
return {"detected": False, "reason": "insufficient_data"}
try:
work = df.tail(72).copy()
prev = work.iloc[:-8]
recent = work.iloc[-8:]
current = float(work["close"].iloc[-1])
box_low = float(prev["low"].tail(48).min())
box_high = float(prev["high"].tail(48).max())
if current <= 0 or box_low <= 0 or box_high <= box_low:
return {"detected": False, "reason": "invalid_box"}
box_width_pct = (box_high / box_low - 1) * 100
if box_width_pct > 38:
return {"detected": False, "reason": "box_too_wide", "box_width_pct": round(box_width_pct, 2)}
closes = [float(x) for x in recent["close"].tolist()]
highs = [float(x) for x in recent["high"].tolist()]
opens = [float(x) for x in recent["open"].tolist()]
breakdown_closes = [x for x in closes if x < box_low * 0.992]
touched_retest = any(box_low * 0.988 <= h <= box_low * 1.035 for h in highs[-5:])
latest_rejected = current < box_low * 0.992 and closes[-1] < opens[-1]
below_ema = current < float(work["close"].ewm(span=25, adjust=False).mean().iloc[-1])
if not breakdown_closes or not touched_retest:
return {
"detected": False,
"reason": "no_breakdown_retest",
"box_low": round(box_low, 8),
"box_high": round(box_high, 8),
}
stop_level = max(max(highs[-5:]) * 1.012, box_low * 1.025)
risk = max(stop_level - current, current * 0.025)
target_1 = max(current - risk * 1.8, current * 0.72)
quality_score = 0
quality_score += 3 if latest_rejected else 1
quality_score += 2 if below_ema else 0
quality_score += 2 if float(change_24h or 0) <= -3 else 0
quality_score += 1 if box_width_pct <= 24 else 0
quality = "优质" if quality_score >= 7 else "良好" if quality_score >= 5 else "普通"
return {
"detected": True,
"breakdown_level": round(box_low, 8),
"box_high": round(box_high, 8),
"box_width_pct": round(box_width_pct, 2),
"retest_zone": round(box_low, 8),
"stop_level": round(stop_level, 8),
"target_1": round(target_1, 8),
"quality": quality,
"quality_score": quality_score,
"score": quality_score,
"retest_rejected": latest_rejected,
"relative_weakness": bool(below_ema or float(change_24h or 0) <= -3),
"pullback_age_bars": 0,
"signals": [
f"1H破位反抽做空(破位{box_low:.6g})",
"反抽失败确认" if latest_rejected else "等待反抽失败确认",
],
}
except Exception as exc:
return {"detected": False, "reason": f"error:{exc}"}
def build_breakdown_retest_short_1h_signal(
*,
symbol: str,
current_price: float,
detection: dict,
entry_plan: dict | None = None,
market_regime: dict | None = None,
decision_log: dict | None = None,
) -> StrategySignal | None:
"""Build a short signal for breakdown -> failed retest setups.
Expected detection fields are intentionally simple so scanner/confirm
implementations can evolve without changing the strategy contract:
detected, breakdown_level, retest_zone, stop_level, target_1, quality,
retest_rejected, score.
"""
if not (detection or {}).get("detected"):
return None
entry_plan = dict(entry_plan or {})
market_regime = market_regime or {}
quality = str(detection.get("quality") or "")
retest_rejected = bool(detection.get("retest_rejected"))
current_price = _safe_float(current_price)
retest_zone = _safe_float(detection.get("retest_zone") or detection.get("breakdown_level"))
distance_pct = abs(current_price / retest_zone - 1) * 100 if current_price > 0 and retest_zone > 0 else 0.0
risk_level = str(market_regime.get("risk_level") or "medium").lower()
reasons = []
status = "candidate"
if quality not in {"良好", "优质"}:
status = "observe"
reasons.append(f"反抽质量 {quality or '未知'},不直接做空")
if not retest_rejected:
status = "observe"
reasons.append("尚未确认反抽失败")
if risk_level in {"low", "medium"} and not bool(detection.get("relative_weakness")):
status = "observe"
reasons.append("市场并非明显弱势,且缺少相对弱势确认")
if distance_pct > 8:
status = "observe"
reasons.append(f"当前价离反抽区 {distance_pct:.1f}%,不追空")
entry_plan.setdefault("side", "short")
entry_plan.setdefault("entry_action", "可即刻买入" if status == "candidate" else "观察")
entry_plan.setdefault("entry_price", current_price)
entry_plan.setdefault("stop_loss", detection.get("stop_level"))
entry_plan.setdefault("tp1", detection.get("target_1"))
entry_plan.setdefault("risk_reward_ok", True)
score = _safe_float(detection.get("score"))
confidence = min(100.0, max(0.0, score * 8 + (12 if retest_rejected else 0)))
return StrategySignal(
strategy_code=BREAKDOWN_RETEST_SHORT_1H_STRATEGY,
strategy_version=current_strategy_version(),
symbol=symbol,
direction="short",
status=status,
confidence=confidence,
score=score,
trigger={
"factor_code": "breakdown_retest_1h_short",
"factor_label": "1H破位反抽做空",
"breakdown_level": detection.get("breakdown_level"),
"retest_zone": retest_zone,
"stop_level": detection.get("stop_level"),
"target_1": detection.get("target_1"),
"quality": quality,
"retest_rejected": retest_rejected,
"distance_to_retest_zone_pct": round(distance_pct, 4),
"risk_level": risk_level,
},
factor_roles={
"breakdown_retest_1h_short": TRIGGER,
"retest_reject_15m_short": ENTRY,
"market_risk_off_short": CONFIRMATION,
"false_breakout": RISK,
"funding_extreme": RISK,
},
entry_plan=entry_plan,
risk_plan={
"invalid_if": ["重新站回破位区", "反抽放量突破", "BTC/ETH快速转强", "RR不足"],
"risk_reasons": reasons,
},
decision_log=decision_log or {
"module": BREAKDOWN_RETEST_SHORT_1H_STRATEGY,
"decision": status,
"reasons": reasons,
},
)
def build_short_weak_bounce_failure_signal(
*,
symbol: str,
result: dict,
entry_plan: dict | None = None,
market_regime: dict | None = None,
) -> StrategySignal | None:
"""Build a short signal for weak bounce failures in risk-off regimes."""
result = result or {}
entry_plan = dict(entry_plan or {})
text = " ".join(str(x or "") for x in result.get("signals") or [])
market_regime = market_regime or result.get("market_regime") or (result.get("market_context") or {}).get("market_regime") or {}
risk_level = str(market_regime.get("risk_level") or "").lower()
regime = str(market_regime.get("regime") or "").lower()
has_weak_bounce = any(key in text for key in ("弱反弹", "反抽失败", "空头加速", "放量阴线", "破位"))
has_entry = any(key in text for key in ("15min", "15m", "反抽失败", "空头加速"))
risk_off = risk_level in {"high", "critical"} or regime in {"risk_off", "bearish", "downtrend"}
if not (has_weak_bounce and has_entry and risk_off):
return None
action = str(entry_plan.get("entry_action") or result.get("entry_action") or "").strip()
status = "candidate" if action in {"可即刻买入", "即刻买入", "可开空"} else "observe"
reasons = [] if status == "candidate" else ["弱反弹失败已出现,但还缺少当前开空动作"]
score = _safe_float(result.get("score"))
confidence = min(100.0, max(0.0, score * 7 + 12))
entry_plan.setdefault("side", "short")
entry_plan.setdefault("entry_action", "可即刻买入" if status == "candidate" else "观察")
return StrategySignal(
strategy_code=SHORT_WEAK_BOUNCE_FAILURE_STRATEGY,
strategy_version=current_strategy_version(),
symbol=symbol,
direction="short",
status=status,
confidence=confidence,
score=score,
trigger={
"factor_code": "weak_bounce_failure_15m_1h_short",
"factor_label": "弱势反弹失败做空",
"risk_level": risk_level,
"regime": regime,
"entry_action": action,
},
factor_roles={
"weak_bounce_failure_15m_1h_short": TRIGGER,
"retest_reject_15m_short": ENTRY,
"market_risk_off_short": CONFIRMATION,
"funding_extreme": RISK,
},
entry_plan=entry_plan,
risk_plan={
"invalid_if": ["重新站回反抽高点", "BTC/ETH快速转强", "15m空头结构失效", "RR不足"],
"risk_reasons": reasons,
},
decision_log={"module": SHORT_WEAK_BOUNCE_FAILURE_STRATEGY, "decision": status, "reasons": reasons},
)
__all__ = [
"build_breakdown_retest_short_1h_signal",
"build_short_weak_bounce_failure_signal",
"detect_breakdown_retest_short_1h",
]