alphax/app/strategies/box_retest_4h.py
2026-05-28 00:02:11 +08:00

172 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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