alphax/app/strategies/short_breakdown.py
2026-05-31 19:00:46 +08:00

114 lines
4.3 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
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 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,
},
)
__all__ = ["build_breakdown_retest_short_1h_signal"]