"""Plan Engine — Entry / Stop / Target / RR only when Decision is tradable.""" from __future__ import annotations from crypto_wyckoff.domain_models import DecisionSignal, EngineResult _TRADABLE = { DecisionSignal.STRONG_BUY.value, DecisionSignal.BUY.value, DecisionSignal.SELL.value, } class PlanEngine: name = "Plan" version = "1.0.0" def run(self, daily_feature: EngineResult, decision: EngineResult) -> EngineResult: f = daily_feature.payload close = float(f.get("close") or 0) atr = float(f.get("atr") or 0) or close * 0.02 swing_low = float(f.get("swing_low") or close - 2 * atr) swing_high = float(f.get("swing_high") or close + 2 * atr) range_high = float(f.get("range_high") or swing_high) signal = decision.payload.get("decision_signal", DecisionSignal.WATCH.value) entry = stop = t1 = t2 = rr = None reasons: list[str] = [] if signal not in _TRADABLE or close <= 0: reasons.append(f"无交易计划(信号={signal})") return EngineResult( name=self.name, version=self.version, confidence=decision.confidence, score=decision.score, reasons=reasons, payload={ "entry": None, "stop": None, "target1": None, "target2": None, "rr": None, }, ) if signal in (DecisionSignal.STRONG_BUY.value, DecisionSignal.BUY.value): entry = round(close, 4) stop = round(min(swing_low, close - 1.5 * atr), 4) risk = max(entry - stop, 1e-6) t1 = round(entry + 2.0 * risk, 4) t2 = round(max(range_high, entry + 3.0 * risk), 4) rr = round((t1 - entry) / risk, 2) reasons.append(f"入场={entry} 止损={stop} 目标一={t1} 盈亏比={rr}") else: # Sell entry = round(close, 4) stop = round(max(swing_high, close + 1.5 * atr), 4) risk = max(stop - entry, 1e-6) t1 = round(entry - 2.0 * risk, 4) t2 = round(entry - 3.0 * risk, 4) rr = round((entry - t1) / risk, 2) reasons.append(f"做空计划 入场={entry} 止损={stop} 目标一={t1}") return EngineResult( name=self.name, version=self.version, confidence=decision.confidence, score=decision.score, reasons=reasons, payload={ "entry": entry, "stop": stop, "target1": t1, "target2": t2, "rr": rr, }, )