"""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"]