alphax/app/strategies/altcoin_breakout.py
2026-05-29 10:09:30 +08:00

177 lines
7.5 KiB
Python

"""Altcoin breakout strategy profiles.
These builders turn existing evidence into complete strategy signals. A factor
can support a strategy, but the strategy still owns trigger freshness, entry
quality and risk semantics.
"""
from __future__ import annotations
from app.core.factor_roles import CONFIRMATION, ENTRY, PREREQUISITE, RISK, TRIGGER
from app.core.strategy_contract import StrategySignal, current_strategy_version
from app.core.strategy_registry import (
COMPRESSION_BREAKOUT_4H_STRATEGY,
INTRADAY_MOMENTUM_15M_STRATEGY,
VOLUME_IGNITION_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 _signals_text(result: dict) -> str:
return " ".join(str(x or "") for x in (result or {}).get("signals") or [])
def _trigger_context(result: dict) -> dict:
return (result or {}).get("trigger_context") or ((result or {}).get("market_context") or {}).get("trigger_context") or {}
def _has_current_trigger(result: dict) -> bool:
trigger = _trigger_context(result)
text = _signals_text(result)
return bool(trigger.get("current_triggers")) or "15min即刻入场" in text or "15min强突破" in text or "15min 强突破" in text
def _status_for_entry(result: dict, entry_plan: dict | None = None, *, require_current_trigger: bool = False, allow_wait: bool = True) -> tuple[str, list[str]]:
reasons = []
entry_action = str((entry_plan or {}).get("entry_action") or ((result or {}).get("entry_plan") or {}).get("entry_action") or (result or {}).get("entry_action") or "").strip()
if require_current_trigger and not _has_current_trigger(result):
return "observe", ["缺少当前低周期触发"]
if entry_action in ("可即刻买入", "即刻买入"):
return "candidate", reasons
if entry_action == "等回踩" and allow_wait:
return "candidate", reasons
if entry_action:
reasons.append(f"当前入场动作 {entry_action} 不满足策略执行条件")
return "observe", reasons
def build_volume_ignition_1h_signal(*, symbol: str, result: dict, entry_plan: dict | None = None) -> StrategySignal | None:
text = _signals_text(result)
has_vp = "量价齐飞" in text or ("连续" in text and "放量" in text)
has_breakout = "1H" in text and ("突破" in text or "起爆" in text)
if not (has_vp or has_breakout):
return None
status, reasons = _status_for_entry(result, entry_plan, require_current_trigger=False, allow_wait=True)
score = _safe_float(result.get("score"))
confidence = min(100.0, max(0.0, score * 7 + (12 if _has_current_trigger(result) else 0)))
trigger = {
"factor_code": "vp_fly_1h_current" if has_vp else "ignition_1h_current",
"factor_label": "1H放量突破启动",
"has_current_trigger": _has_current_trigger(result),
"trigger_status": _trigger_context(result).get("trigger_status") or "",
"entry_action": (entry_plan or {}).get("entry_action") or "",
}
return StrategySignal(
strategy_code=VOLUME_IGNITION_1H_STRATEGY,
strategy_version=current_strategy_version(),
symbol=symbol,
direction="long",
status=status,
confidence=confidence,
score=score,
trigger=trigger,
factor_roles={
"vp_fly_1h_current": TRIGGER,
"volume_consecutive_1h": CONFIRMATION,
"breakout_15m_current": ENTRY,
"pullback_15m_confirm": ENTRY,
"false_breakout": RISK,
"risk_reward_bad": RISK,
},
entry_plan=entry_plan or {},
risk_plan={
"invalid_if": ["放量后不能延续", "15m假突破", "跌回启动K低点", "RR不足"],
"risk_reasons": reasons,
},
decision_log={"module": VOLUME_IGNITION_1H_STRATEGY, "decision": status, "reasons": reasons},
)
def build_compression_breakout_4h_signal(*, symbol: str, result: dict, entry_plan: dict | None = None) -> StrategySignal | None:
text = _signals_text(result)
has_compression = any(key in text for key in ("静K", "压缩", "布林收窄", "底部抬高"))
has_breakout_context = any(key in text for key in ("突破", "起爆", "量价齐飞", "回踩"))
if not (has_compression and has_breakout_context):
return None
status, reasons = _status_for_entry(result, entry_plan, require_current_trigger=False, allow_wait=True)
score = _safe_float(result.get("score"))
confidence = min(100.0, max(0.0, score * 6 + (10 if "底部" in text else 0) + (8 if _has_current_trigger(result) else 0)))
return StrategySignal(
strategy_code=COMPRESSION_BREAKOUT_4H_STRATEGY,
strategy_version=current_strategy_version(),
symbol=symbol,
direction="long",
status=status,
confidence=confidence,
score=score,
trigger={
"factor_code": "compression_surge_4h",
"factor_label": "4H压缩蓄力突破",
"has_current_trigger": _has_current_trigger(result),
"trigger_status": _trigger_context(result).get("trigger_status") or "",
},
factor_roles={
"static_accum_4h": PREREQUISITE,
"higher_lows_4h": PREREQUISITE,
"compression_surge_4h": TRIGGER,
"ignition_4h_current": CONFIRMATION,
"vp_fly_1h_current": CONFIRMATION,
"pullback_15m_confirm": ENTRY,
"false_breakout": RISK,
},
entry_plan=entry_plan or {},
risk_plan={
"invalid_if": ["突破后跌回压缩区间", "回踩放量跌破", "无量反抽失败", "市场风险升高"],
"risk_reasons": reasons,
},
decision_log={"module": COMPRESSION_BREAKOUT_4H_STRATEGY, "decision": status, "reasons": reasons},
)
def build_intraday_momentum_15m_signal(*, symbol: str, result: dict, entry_plan: dict | None = None) -> StrategySignal | None:
text = _signals_text(result)
has_short_tf = any(key in text for key in ("15min", "15m", "短周期", "5m/15m"))
if not has_short_tf:
return None
status, reasons = _status_for_entry(result, entry_plan, require_current_trigger=True, allow_wait=False)
score = _safe_float(result.get("score"))
confidence = min(100.0, max(0.0, score * 6 + 18))
return StrategySignal(
strategy_code=INTRADAY_MOMENTUM_15M_STRATEGY,
strategy_version=current_strategy_version(),
symbol=symbol,
direction="long",
status=status,
confidence=confidence,
score=score,
trigger={
"factor_code": "short_tf_15m_ignition",
"factor_label": "15m日内动量延续",
"has_current_trigger": _has_current_trigger(result),
"trigger_status": _trigger_context(result).get("trigger_status") or "",
},
factor_roles={
"short_tf_5m_ignition": PREREQUISITE,
"short_tf_15m_ignition": TRIGGER,
"short_tf_resonance": CONFIRMATION,
"vp_fly_1h_current": CONFIRMATION,
"breakout_15m_current": ENTRY,
"false_breakout": RISK,
"trend_exhaustion": RISK,
},
entry_plan=entry_plan or {},
risk_plan={
"invalid_if": ["15m跌回突破K", "短周期量能衰减", "快速冲高回落", "RR不足"],
"risk_reasons": reasons,
},
decision_log={"module": INTRADAY_MOMENTUM_15M_STRATEGY, "decision": status, "reasons": reasons},
)