102 lines
3.5 KiB
Python
102 lines
3.5 KiB
Python
"""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,
|
||
},
|
||
)
|