"""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_1H_STRATEGY, 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 _safe_int(value, default=999) -> int: try: if value is None or value == "": return default return int(value) except Exception: return default def _build_box_retest_signal( *, symbol: str, current_price: float, detection: dict, strategy_code: str, timeframe_label: str, factor_code: str, factor_label: str, max_fresh_age_bars: int, max_chase_distance_pct: float, 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 = _safe_int(detection.get("pullback_age_bars")) quality = str(detection.get("quality") or "") status = "candidate" reasons = [] if risk_level == "critical": status = "observe" reasons.append("全局风险 critical,仅观察") if age > max_fresh_age_bars: status = "observe" reasons.append(f"回踩已过去 {age} 根{timeframe_label},时效偏旧") if quality not in {"良好", "优质"}: status = "observe" reasons.append(f"形态质量 {quality or '未知'},不直接交易") if distance_pct > max_chase_distance_pct: 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": factor_code, "factor_label": factor_label, "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=strategy_code, strategy_version=current_strategy_version(), symbol=symbol, direction="long", status=status, confidence=confidence, score=score, trigger=trigger, factor_roles={ factor_code: 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, }, ) def build_box_retest_4h_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: return _build_box_retest_signal( symbol=symbol, current_price=current_price, detection=detection, strategy_code=BOX_RETEST_4H_STRATEGY, timeframe_label="4H", factor_code="box_breakout_pullback_4h", factor_label="4H箱体突破回踩", max_fresh_age_bars=4, max_chase_distance_pct=8, entry_plan=entry_plan, market_regime=market_regime, decision_log=decision_log, ) def build_box_retest_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: return _build_box_retest_signal( symbol=symbol, current_price=current_price, detection=detection, strategy_code=BOX_RETEST_1H_STRATEGY, timeframe_label="1H", factor_code="box_breakout_pullback_1h", factor_label="1H箱体突破回踩", max_fresh_age_bars=6, max_chase_distance_pct=6, entry_plan=entry_plan, market_regime=market_regime, decision_log=decision_log, ) def build_box_retest_signal(**kwargs) -> StrategySignal | None: """Backward-compatible alias for the original 4H strategy builder.""" return build_box_retest_4h_signal(**kwargs)