"""4H box breakout retest strategy candidate. The detector factor is not the strategy. This module wraps that factor with market, freshness, entry-distance and risk semantics to produce a standard StrategySignal. """ from __future__ import annotations from app.core.factor_roles import ENTRY, RISK, TRIGGER from app.core.strategy_contract import StrategySignal, current_strategy_version from app.core.strategy_registry import BOX_RETEST_4H_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_box_retest_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: if not (detection or {}).get("detected"): return None entry_plan = entry_plan or {} market_regime = market_regime or {} risk_level = str(market_regime.get("risk_level") or "medium").lower() entry_zone = _safe_float(detection.get("entry_zone")) current_price = _safe_float(current_price) distance_pct = (current_price / entry_zone - 1) * 100 if entry_zone > 0 and current_price > 0 else 0 age = int(detection.get("pullback_age_bars") or 999) quality = str(detection.get("quality") or "") status = "candidate" reasons = [] if risk_level == "critical": status = "observe" reasons.append("全局风险 critical,仅观察") if age > 4: status = "observe" reasons.append(f"回踩已过去 {age} 根4H,时效偏旧") if quality not in {"良好", "优质"}: status = "observe" reasons.append(f"形态质量 {quality or '未知'},不直接交易") if distance_pct > 8: status = "observe" reasons.append(f"当前价离箱体上沿 {distance_pct:.1f}%,禁止追高") score = _safe_float(detection.get("score")) confidence = min(100.0, max(0.0, score * 8)) trigger = { "factor_code": "box_breakout_pullback_4h", "factor_label": "4H箱体突破回踩", "box_high": detection.get("box_high"), "box_low": detection.get("box_low"), "entry_zone": detection.get("entry_zone"), "stop_level": detection.get("stop_level"), "pullback_kind": detection.get("pullback_kind"), "pullback_age_bars": age, "quality": quality, "distance_to_entry_zone_pct": round(distance_pct, 4), "market_regime": market_regime.get("regime") or "", "risk_level": risk_level, } risk_plan = { "invalid_if": ["跌回箱体", "放量冲高回落", "回踩过久", "全局风险升为critical"], "stop_level": detection.get("stop_level"), "risk_reasons": reasons, } return StrategySignal( strategy_code=BOX_RETEST_4H_STRATEGY, strategy_version=current_strategy_version(), symbol=symbol, direction="long", status=status, confidence=confidence, score=score, trigger=trigger, factor_roles={ "box_breakout_pullback_4h": TRIGGER, "pullback_15m_confirm": ENTRY, "trend_exhaustion": RISK, "false_breakout": RISK, }, entry_plan=entry_plan, risk_plan=risk_plan, decision_log=decision_log or { "module": "box_retest_4h_v1", "decision": status, "reasons": reasons, }, )