from __future__ import annotations from dataclasses import dataclass from datetime import date from typing import Optional import pandas as pd from dragon_bridge_predictive_break import ( allow_predictive_b1_break_long_exit, allow_predictive_b1_break_short_exit, allow_predictive_error_reentry, ) from dragon_deep_oversold_classifier import ( deep_oversold_base_entry, deep_oversold_requires_confirmation, deep_oversold_selective_veto, deep_oversold_subtype, ) from dragon_deep_oversold_confirmation import ( evaluate_pending_confirmation, should_increment_pending, ) from dragon_decision_types import StrategyDecision from dragon_glued_followthrough_confirmation import ( evaluate_glued_followthrough_confirmation, glued_followthrough_pending_allowed, glued_high_weak_rebound_subtype, ) from dragon_glued_followthrough_exit import should_hold_glued_followthrough_reentry_kdj_only from dragon_rule_catalog import classify_aux_reason, classify_entry_reason, classify_exit_reason from dragon_strategy_config import StrategyConfig @dataclass class StrategyContext: in_position: bool = False entry_date: Optional[date] = None entry_price: Optional[float] = None entry_a1: Optional[float] = None entry_b1: Optional[float] = None entry_c1: Optional[float] = None entry_reason: str = "" entry_reason_layer: str = "unknown" entry_reason_family: str = "unknown" entry_reason_code: str = "" first_exit_checked: bool = False c1_over_80_seen: bool = False a1_big_pos_count: int = 0 b1_big_pos_count: int = 0 b1_negative_sell_count: int = 0 prev_real_sell_c1: Optional[float] = None last_real_sell_date: Optional[date] = None last_real_sell_reason: str = "" last_real_sell_reason_layer: str = "unknown" last_real_sell_reason_family: str = "unknown" last_real_sell_reason_code: str = "" bridge_last_exit_predictive_break: bool = False bridge_last_exit_negative_a1_no_b1_recovery: bool = False last_big_regime_exit_date: Optional[date] = None last_kdj_buy_date: Optional[date] = None last_kdj_buy_a1: Optional[float] = None last_kdj_buy_b1: Optional[float] = None last_kdj_buy_c1: Optional[float] = None last_kdj_sell_date: Optional[date] = None last_ql_sell_date: Optional[date] = None last_aux_buy_date: Optional[date] = None last_aux_buy_c1: Optional[float] = None last_aux_sell_date: Optional[date] = None last_aux_sell_c1: Optional[float] = None last_aux_sell_b1: Optional[float] = None last_aux_sell_reason: str = "" flat_post_exit_ql_emitted: bool = False flat_post_exit_kdj_emitted: bool = False kdj_cross_count_since_big_regime_exit: int = 999 max_a1_since_entry: float = -999.0 max_b1_since_entry: float = -999.0 max_c1_since_entry: float = -999.0 a1_big_cycle_count: int = 0 b1_big_cycle_count: int = 0 combo_big_cycle_count: int = 0 sell_signal_count: int = 0 kdj_sell_signal_count: int = 0 ql_sell_signal_count: int = 0 prev_a1_big_flag: bool = False prev_b1_big_flag: bool = False prev_combo_big_flag: bool = False pending_deep_oversold_subtype: str = "" pending_deep_oversold_origin_date: Optional[date] = None pending_deep_oversold_reason: str = "" pending_deep_oversold_a1: Optional[float] = None pending_deep_oversold_b1: Optional[float] = None pending_deep_oversold_c1: Optional[float] = None pending_deep_oversold_bars_waited: int = 0 bridge_pending_deep_oversold_active: bool = False pending_glued_followthrough_subtype: str = "" pending_glued_followthrough_origin_date: Optional[date] = None pending_glued_followthrough_reason: str = "" pending_glued_followthrough_signal_close: Optional[float] = None pending_glued_followthrough_a1: Optional[float] = None pending_glued_followthrough_b1: Optional[float] = None pending_glued_followthrough_c1: Optional[float] = None pending_glued_followthrough_bars_waited: int = 0 bridge_pending_glued_followthrough_active: bool = False class DragonRuleEngine: """ Executable baseline for the Dragon narrative rules. This version focuses on the main rule trunk: - KDJ gold-cross driven entries - KDJ / QL sell-cross driven exits - hard vetoes and hard exits from A1/B1 thresholds - special low-C1 knife-catching entries - auxiliary bearish / bullish signal downgrades """ def __init__(self, config: Optional[StrategyConfig] = None): self.config = config or StrategyConfig() self.context = StrategyContext() def _rule_enabled(self, rule_name: str) -> bool: return rule_name not in self.config.disabled_rules def _classify_reason(self, side: str, layer: str, reason: str): if layer == "aux_signal": return classify_aux_reason(reason) if side == "BUY": return classify_entry_reason(reason) return classify_exit_reason(reason) def _build_decision(self, side: str, layer: str, reason: str) -> StrategyDecision: if side == "BUY": action = "BUY" if layer == "real_trade" else "AUX_BUY" else: action = "SELL" if layer == "real_trade" else "AUX_SELL" return StrategyDecision( action=action, reason=self._classify_reason(side, layer, reason), ) def _entry_reason_is(self, *reason_names: str) -> bool: entry_reason = self.context.entry_reason entry_family = entry_reason.split(":", 1)[0] if entry_reason in reason_names or entry_family in reason_names: return True if not self.context.entry_reason_code and not self.context.entry_reason_family: return False if "glued_buy" in reason_names and self.context.entry_reason_code == "entry_glued_buy": return True if "dual_gold_resonance_buy" in reason_names and self.context.entry_reason_code == "entry_dual_gold": return True if ( "deep_oversold_rebound_buy" in reason_names and self.context.entry_reason_family == "deep_oversold" ): return True if ( "oversold_reversal_after_ql_buy" in reason_names and self.context.entry_reason_code == "entry_oversold_reversal_after_ql" ): return True return False def _last_sell_reason_is(self, reason_name: str) -> bool: if self.context.last_real_sell_reason == reason_name: return True code_map = { "crash_protection_exit": "exit_crash_protection", "predictive_b1_break_exit": "exit_predictive_b1_break", } target_code = code_map.get(reason_name, "") return bool(target_code and self.context.last_real_sell_reason_code == target_code) def _clear_pending_deep_oversold(self) -> None: self.context.pending_deep_oversold_subtype = "" self.context.pending_deep_oversold_origin_date = None self.context.pending_deep_oversold_reason = "" self.context.pending_deep_oversold_a1 = None self.context.pending_deep_oversold_b1 = None self.context.pending_deep_oversold_c1 = None self.context.pending_deep_oversold_bars_waited = 0 self.context.bridge_pending_deep_oversold_active = False def _queue_pending_deep_oversold(self, row: pd.Series, subtype: str) -> None: self.context.pending_deep_oversold_subtype = subtype self.context.pending_deep_oversold_origin_date = row.name.date() self.context.pending_deep_oversold_reason = f"deep_oversold_rebound_buy:{subtype}" self.context.pending_deep_oversold_a1 = float(row["a1"]) self.context.pending_deep_oversold_b1 = float(row["b1"]) self.context.pending_deep_oversold_c1 = float(row["c1"]) self.context.pending_deep_oversold_bars_waited = 0 self.context.bridge_pending_deep_oversold_active = True def _clear_pending_glued_followthrough(self) -> None: self.context.pending_glued_followthrough_subtype = "" self.context.pending_glued_followthrough_origin_date = None self.context.pending_glued_followthrough_reason = "" self.context.pending_glued_followthrough_signal_close = None self.context.pending_glued_followthrough_a1 = None self.context.pending_glued_followthrough_b1 = None self.context.pending_glued_followthrough_c1 = None self.context.pending_glued_followthrough_bars_waited = 0 self.context.bridge_pending_glued_followthrough_active = False def _queue_pending_glued_followthrough(self, row: pd.Series, subtype: str) -> None: self.context.pending_glued_followthrough_subtype = subtype self.context.pending_glued_followthrough_origin_date = row.name.date() self.context.pending_glued_followthrough_reason = f"buy_block_glued_high_weak_rebound:{subtype}" self.context.pending_glued_followthrough_signal_close = float(row["close"]) self.context.pending_glued_followthrough_a1 = float(row["a1"]) self.context.pending_glued_followthrough_b1 = float(row["b1"]) self.context.pending_glued_followthrough_c1 = float(row["c1"]) self.context.pending_glued_followthrough_bars_waited = 0 self.context.bridge_pending_glued_followthrough_active = True @staticmethod def _is_glued(a1: float) -> bool: return abs(a1) < 0.02 @staticmethod def _is_big_positive(a1: float) -> bool: return a1 > 0.028 @staticmethod def _is_big_negative(a1: float) -> bool: return a1 < -0.04 @staticmethod def _is_b1_hard_negative(b1: float) -> bool: return b1 < -0.17 @staticmethod def _is_b1_strong_positive(b1: float) -> bool: return b1 > 0.17 def _holding_days(self, row_date: date) -> int: if self.context.entry_date is None: return 0 return (row_date - self.context.entry_date).days def _days_from_last_real_sell(self, row_date: date) -> Optional[int]: if self.context.last_real_sell_date is None: return None return (row_date - self.context.last_real_sell_date).days def _days_from_last_aux_buy(self, row_date: date) -> Optional[int]: if self.context.last_aux_buy_date is None: return None return (row_date - self.context.last_aux_buy_date).days def _days_from_last_aux_sell(self, row_date: date) -> Optional[int]: if self.context.last_aux_sell_date is None: return None return (row_date - self.context.last_aux_sell_date).days def _record_cross_counters(self, row: pd.Series) -> None: if bool(row["kdj_buy"]): self.context.last_kdj_buy_date = row.name.date() self.context.last_kdj_buy_a1 = float(row["a1"]) self.context.last_kdj_buy_b1 = float(row["b1"]) self.context.last_kdj_buy_c1 = float(row["c1"]) if self.context.kdj_cross_count_since_big_regime_exit != 999: self.context.kdj_cross_count_since_big_regime_exit += 1 if bool(row["kdj_sell"]): self.context.last_kdj_sell_date = row.name.date() if self.context.kdj_cross_count_since_big_regime_exit != 999: self.context.kdj_cross_count_since_big_regime_exit += 1 if bool(row["ql_sell"]): self.context.last_ql_sell_date = row.name.date() def _update_pending_states(self, row: pd.Series) -> None: if should_increment_pending( active=self.context.bridge_pending_deep_oversold_active, subtype=self.context.pending_deep_oversold_subtype, origin_date=self.context.pending_deep_oversold_origin_date, row_date=row.name.date(), ): self.context.pending_deep_oversold_bars_waited += 1 if should_increment_pending( active=self.context.bridge_pending_glued_followthrough_active, subtype=self.context.pending_glued_followthrough_subtype, origin_date=self.context.pending_glued_followthrough_origin_date, row_date=row.name.date(), ): self.context.pending_glued_followthrough_bars_waited += 1 if self.context.bridge_pending_glued_followthrough_active: if bool(row["kdj_sell"]) or bool(row["ql_sell"]): self._clear_pending_glued_followthrough() elif self.context.pending_glued_followthrough_bars_waited > self.config.glued_followthrough_confirm_window_bars: self._clear_pending_glued_followthrough() def _update_position_counters(self, row: pd.Series) -> None: if not self.context.in_position: return a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) self.context.max_a1_since_entry = max(self.context.max_a1_since_entry, a1) self.context.max_b1_since_entry = max(self.context.max_b1_since_entry, b1) self.context.max_c1_since_entry = max(self.context.max_c1_since_entry, c1) a1_big_flag = self._is_big_positive(a1) b1_big_flag = self._is_b1_strong_positive(b1) combo_big_flag = a1_big_flag and b1_big_flag if a1_big_flag and not self.context.prev_a1_big_flag: self.context.a1_big_cycle_count += 1 if b1_big_flag and not self.context.prev_b1_big_flag: self.context.b1_big_cycle_count += 1 if combo_big_flag and not self.context.prev_combo_big_flag: self.context.combo_big_cycle_count += 1 self.context.prev_a1_big_flag = a1_big_flag self.context.prev_b1_big_flag = b1_big_flag self.context.prev_combo_big_flag = combo_big_flag if self._is_big_positive(a1): self.context.a1_big_pos_count += 1 if self._is_b1_strong_positive(b1): self.context.b1_big_pos_count += 1 if c1 > 80: self.context.c1_over_80_seen = True def _is_high_regime_trade(self) -> bool: return ( self.context.c1_over_80_seen and self.context.max_c1_since_entry > 80 and self.context.max_a1_since_entry > 0.045 ) def _high_regime_exit_decision(self, row: pd.Series, source: str) -> Optional[tuple[str, str]]: if not self._is_high_regime_trade(): return None a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) kdj_sell = bool(row["kdj_sell"]) ql_sell = bool(row["ql_sell"]) if ( ql_sell and not kdj_sell and self.context.max_b1_since_entry > 0.15 and a1 <= 0.038 and b1 <= 0.12 and c1 > 80 and self._rule_enabled("ql_high_zone_take_profit") ): return "SELL", "ql_high_zone_take_profit" if ql_sell and not kdj_sell and c1 > 80 and a1 > 0.02: return "HOLD", "high_regime_wait_kdj_confirmation" if ( self.context.max_a1_since_entry > 0.09 and a1 > 0.055 and b1 < -0.05 and ql_sell and self._rule_enabled("high_regime_momentum_break") ): return "SELL", "high_regime_momentum_break" if a1 > 0.06 and c1 > 90 and b1 > -0.03 and not ql_sell: return "HOLD", "super_hot_trend_hold" if not kdj_sell: return None decayed_from_peak = ( self.context.max_b1_since_entry > 0.18 and a1 < self.context.max_a1_since_entry - 0.005 and b1 < min(0.13, self.context.max_b1_since_entry - 0.02) ) if 0.033 <= a1 <= 0.05 and c1 > 80 and decayed_from_peak: if self._rule_enabled("prewarning_reduction_exit"): return "SELL", "prewarning_reduction_exit" if ( b1 <= 0 and ( a1 <= 0.022 or c1 < 80 or self.context.kdj_sell_signal_count >= 2 or self.context.b1_negative_sell_count >= 2 ) ): if self._rule_enabled(f"high_regime_confirmed_exit:{source}"): return "SELL", f"high_regime_confirmed_exit:{source}" return "HOLD", f"high_regime_hold:{source}" def _ql_only_take_profit_exit(self, row: pd.Series) -> Optional[tuple[str, str]]: if not bool(row["ql_sell"]) or bool(row["kdj_sell"]) or not self.context.in_position: return None a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) if self.context.max_b1_since_entry <= 0.15: return None if self.context.max_c1_since_entry >= 78 and 0 < a1 <= 0.02 and b1 <= 0.12: if self._rule_enabled("ql_mid_zone_take_profit"): return "SELL", "ql_mid_zone_take_profit" if c1 > 80 and a1 <= 0.038 and b1 <= 0.12: if self._rule_enabled("ql_high_zone_take_profit"): return "SELL", "ql_high_zone_take_profit" return None def _buy_filter_early_downtrend(self, row: pd.Series, prev_row: Optional[pd.Series]) -> bool: if prev_row is None: return False c1 = float(row["c1"]) prev_c1 = float(prev_row["c1"]) if prev_c1 <= 80 or c1 >= prev_c1: return False if self.context.kdj_cross_count_since_big_regime_exit > 6: return False return True def _buy_filter_high_zone_after_hot_exit(self, row: pd.Series) -> bool: prev_sell_c1 = self.context.prev_real_sell_c1 if prev_sell_c1 is None: return False return prev_sell_c1 > 80 and float(row["c1"]) > 80 def _buy_filter_glued_b1_descending(self, row: pd.Series) -> bool: prev_b1 = self.context.last_kdj_buy_b1 if prev_b1 is None: return False b1 = float(row["b1"]) return b1 < 0 and prev_b1 < 0 and b1 < prev_b1 def _glued_high_weak_rebound_subtype(self, row: pd.Series) -> str: return glued_high_weak_rebound_subtype( a1=float(row["a1"]), b1=float(row["b1"]), c1=float(row["c1"]), ql_buy=bool(row["ql_buy"]), config=self.config, ) def _buy_filter_glued_high_weak_rebound(self, row: pd.Series) -> bool: return bool(self._glued_high_weak_rebound_subtype(row)) def _glued_followthrough_should_queue(self, subtype: str) -> bool: return glued_followthrough_pending_allowed( subtype=subtype, config=self.config, ) def _buy_filter_glued_selective_short_holding(self, row: pd.Series) -> bool: c1 = float(row["c1"]) b1 = float(row["b1"]) if ( self.config.glued_selective_hot_c1_min > 0 and c1 >= self.config.glued_selective_hot_c1_min and c1 < self.config.glued_selective_hot_c1_max and b1 >= self.config.glued_selective_hot_b1_min ): return True if ( self.config.glued_selective_low_c1_max > self.config.glued_selective_low_c1_min and self.config.glued_selective_low_c1_min <= c1 < self.config.glued_selective_low_c1_max and b1 <= self.config.glued_selective_low_b1_max ): return True return False def _buy_improving(self, row: pd.Series) -> bool: prev_a1 = self.context.last_kdj_buy_a1 prev_b1 = self.context.last_kdj_buy_b1 if prev_a1 is None or prev_b1 is None: return True a1 = float(row["a1"]) b1 = float(row["b1"]) return (a1 >= prev_a1) and (b1 >= prev_b1 or b1 > 0) def _should_emit_aux_buy(self, row: pd.Series) -> bool: if not bool(row["kdj_buy"]) or not self.context.in_position: return False a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) holding_days = self._holding_days(row.name.date()) days_from_last_aux_buy = self._days_from_last_aux_buy(row.name.date()) if days_from_last_aux_buy is not None and days_from_last_aux_buy <= 10: return False strong_dual_gold_reconfirm = bool(row["ql_buy"]) and a1 > 0.03 and (b1 > 0.15 or c1 > 60) early_strength_reconfirm = holding_days <= 35 and b1 > 0.24 and c1 > 45 super_hot_reconfirm = holding_days >= 20 and a1 > 0.07 and c1 > 90 if not (strong_dual_gold_reconfirm or early_strength_reconfirm or super_hot_reconfirm): return False if ( days_from_last_aux_buy is not None and days_from_last_aux_buy <= 20 and self.context.last_aux_buy_c1 is not None and c1 <= self.context.last_aux_buy_c1 + 8 and b1 < 0.24 ): return False return True def _should_emit_aux_sell(self, row: pd.Series) -> bool: b1 = float(row["b1"]) c1 = float(row["c1"]) ql_sell = bool(row["ql_sell"]) kdj_sell = bool(row["kdj_sell"]) days_from_last_sell = self._days_from_last_real_sell(row.name.date()) days_from_last_aux_sell = self._days_from_last_aux_sell(row.name.date()) current_source = "ql_sell" if ql_sell and not kdj_sell else "kdj_sell" if ( kdj_sell and not ql_sell and c1 > self.config.aux_sell_high_zone_kdj_only_block_c1 and b1 > self.config.aux_sell_high_zone_kdj_only_block_b1 ): return False if days_from_last_sell is not None and 0 < days_from_last_sell <= self.config.post_exit_confirmation_window_days: base_emit = True elif c1 > self.config.aux_sell_high_zone_warning_c1 and (ql_sell or kdj_sell): base_emit = True elif kdj_sell and c1 > self.config.aux_sell_strong_break_c1 and b1 < self.config.aux_sell_strong_break_b1: base_emit = True else: base_emit = False if not base_emit: return False if ( self.config.aux_sell_same_side_once_per_cycle and days_from_last_sell is not None and 0 < days_from_last_sell <= self.config.post_exit_confirmation_window_days ): if current_source == "ql_sell" and self.context.flat_post_exit_ql_emitted: return False if current_source == "kdj_sell" and self.context.flat_post_exit_kdj_emitted: return False if days_from_last_aux_sell is not None and days_from_last_aux_sell <= self.config.aux_sell_duplicate_cooldown_days: stronger = False if ( self.context.last_aux_sell_c1 is not None and c1 < self.context.last_aux_sell_c1 - self.config.aux_sell_stronger_c1_delta ): stronger = True if ( self.context.last_aux_sell_b1 is not None and b1 < self.context.last_aux_sell_b1 - self.config.aux_sell_stronger_b1_delta ): stronger = True if self.context.last_aux_sell_reason.endswith("ql_sell") and kdj_sell: stronger = True if not stronger and not ( c1 > self.config.aux_sell_high_zone_warning_c1 and self.context.last_aux_sell_c1 is not None and c1 > self.context.last_aux_sell_c1 + self.config.aux_sell_high_zone_rearm_c1_delta ): return False return True def _should_emit_state_aux_sell(self, row: pd.Series) -> bool: days_from_last_sell = self._days_from_last_real_sell(row.name.date()) if days_from_last_sell is None: return False if not self._last_sell_reason_is("crash_protection_exit"): return False if not (0 < days_from_last_sell <= self.config.state_crash_followthrough_window_days): return False if ( self._days_from_last_aux_sell(row.name.date()) is not None and self._days_from_last_aux_sell(row.name.date()) <= self.config.state_crash_followthrough_repeat_cooldown_days ): return False a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) return ( c1 < self.config.state_crash_followthrough_c1_max and a1 < self.config.state_crash_followthrough_a1_max and b1 < self.config.state_crash_followthrough_b1_max ) def _special_buy_knife1(self, row: pd.Series) -> bool: c1 = float(row["c1"]) if c1 >= 12: return False prev_a1 = self.context.last_kdj_buy_a1 prev_b1 = self.context.last_kdj_buy_b1 if prev_a1 is None or prev_b1 is None: return False a1 = float(row["a1"]) b1 = float(row["b1"]) if b1 <= prev_b1: return False if min(b1, prev_b1) < -0.17: return False if min(a1, prev_a1) < -0.04 and a1 <= prev_a1: return False return True def _special_buy_knife2(self, row: pd.Series) -> bool: c1 = float(row["c1"]) if c1 >= 12: return False prev_b1 = self.context.last_kdj_buy_b1 if prev_b1 is None: return False b1 = float(row["b1"]) return b1 > -0.03 and b1 > prev_b1 def _special_buy_deep_oversold_rebound(self, row: pd.Series) -> bool: a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) return deep_oversold_base_entry( a1=a1, b1=b1, c1=c1, config=self.config, ) def _deep_oversold_requires_confirmation(self, row: pd.Series, subtype: str) -> bool: return deep_oversold_requires_confirmation( subtype=subtype, ql_buy=bool(row["ql_buy"]), config=self.config, ) def _pending_deep_oversold_decision(self, row: pd.Series) -> tuple[str, str]: action, reason, should_clear = evaluate_pending_confirmation( active=self.context.bridge_pending_deep_oversold_active, subtype=self.context.pending_deep_oversold_subtype, in_position=self.context.in_position, kdj_sell=bool(row["kdj_sell"]), ql_sell=bool(row["ql_sell"]), bars_waited=self.context.pending_deep_oversold_bars_waited, window_bars=self.config.deep_oversold_confirm_window_bars, ql_buy=bool(row["ql_buy"]), ) if should_clear: self._clear_pending_deep_oversold() return action, reason def _pending_glued_followthrough_decision(self, row: pd.Series) -> tuple[str, str]: action, reason, should_clear = evaluate_glued_followthrough_confirmation( active=self.context.bridge_pending_glued_followthrough_active, subtype=self.context.pending_glued_followthrough_subtype, in_position=self.context.in_position, kdj_sell=bool(row["kdj_sell"]), ql_sell=bool(row["ql_sell"]), bars_waited=self.context.pending_glued_followthrough_bars_waited, ql_buy=bool(row["ql_buy"]), close=float(row["close"]), signal_close=float(self.context.pending_glued_followthrough_signal_close or 0.0), b1=float(row["b1"]), signal_b1=float(self.context.pending_glued_followthrough_b1 or 0.0), config=self.config, ) if should_clear: self._clear_pending_glued_followthrough() return action, reason def _deep_oversold_selective_veto(self, row: pd.Series, subtype: str) -> bool: c1 = float(row["c1"]) b1 = float(row["b1"]) return deep_oversold_selective_veto( subtype=subtype, c1=c1, b1=b1, ql_buy=bool(row["ql_buy"]), config=self.config, ) @staticmethod def _deep_oversold_subtype(a1: float, b1: float, c1: float) -> str: return deep_oversold_subtype(a1=a1, b1=b1, c1=c1) def _special_buy_oversold_recovery(self, row: pd.Series) -> bool: a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) return ( self.config.oversold_recovery_c1_low <= c1 < self.config.oversold_recovery_c1_high and self.config.oversold_recovery_a1_min <= a1 < self.config.oversold_recovery_a1_max and b1 > self.config.oversold_recovery_b1_min ) def _special_buy_oversold_reversal_after_ql(self, row: pd.Series, prev_row: Optional[pd.Series]) -> bool: if prev_row is None or not bool(prev_row["ql_sell"]): return False a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) if ( self.config.oversold_reversal_after_ql_block_c1_low < c1 < self.config.oversold_reversal_after_ql_block_c1_high and b1 > self.config.oversold_reversal_after_ql_block_b1_min and a1 > self.config.oversold_reversal_after_ql_block_a1_min ): return False return ( self.config.oversold_reversal_after_ql_entry_c1_low <= c1 < self.config.oversold_reversal_after_ql_entry_c1_high and self.config.oversold_reversal_after_ql_entry_a1_min <= a1 < self.config.oversold_reversal_after_ql_entry_a1_max and self.config.oversold_reversal_after_ql_entry_b1_min < b1 < self.config.oversold_reversal_after_ql_entry_b1_max ) def _special_buy_early_crash_probe(self, row: pd.Series) -> bool: a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) return c1 < 20 and -0.07 < a1 < -0.04 and -0.04 < b1 < 0 def _special_buy_post_sell_rebound(self, row: pd.Series) -> bool: if self.context.last_kdj_sell_date is None: return False days_from_last_kdj_sell = (row.name.date() - self.context.last_kdj_sell_date).days if days_from_last_kdj_sell < 0 or days_from_last_kdj_sell > 7: return False a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) if ( c1 > self.config.post_sell_rebound_block_high_c1 and a1 > self.config.post_sell_rebound_block_high_a1_min and b1 < self.config.post_sell_rebound_block_high_b1_max ): return False if ( c1 < self.config.post_sell_rebound_block_low_c1 and a1 > self.config.post_sell_rebound_block_low_a1_min and b1 < self.config.post_sell_rebound_block_low_b1_max ): return False if ( self.config.post_sell_rebound_entry1_c1_low <= c1 < self.config.post_sell_rebound_entry1_c1_high and self.config.post_sell_rebound_entry1_a1_min <= a1 < self.config.post_sell_rebound_entry1_a1_max and self.config.post_sell_rebound_entry1_b1_low < b1 < self.config.post_sell_rebound_entry1_b1_high ): return True if ( c1 < self.config.post_sell_rebound_entry2_c1_high and self.config.post_sell_rebound_entry2_a1_min <= a1 < self.config.post_sell_rebound_entry2_a1_max and self.config.post_sell_rebound_entry2_b1_low < b1 < self.config.post_sell_rebound_entry2_b1_high ): return True return False def _special_buy_post_washout_reentry(self, row: pd.Series) -> bool: if self.context.last_real_sell_date is None or self.context.prev_real_sell_c1 is None: return False days_from_last_sell = (row.name.date() - self.context.last_real_sell_date).days if days_from_last_sell < 10 or days_from_last_sell > 20: return False if self.context.prev_real_sell_c1 >= 15: return False if not self.context.bridge_last_exit_negative_a1_no_b1_recovery: return False if bool(row["kdj_buy"]) or bool(row["ql_buy"]) or bool(row["ql_sell"]) or not bool(row["kdj_sell"]): return False a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) return 0.028 <= a1 < 0.035 and b1 > 0.30 and 50 < c1 < 60 def _buy_decision(self, row: pd.Series, prev_row: Optional[pd.Series]) -> tuple[str, str]: a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) if ( not self.context.in_position and self._rule_enabled("post_washout_kdj_reentry_buy") and self._special_buy_post_washout_reentry(row) ): return "BUY", "post_washout_kdj_reentry_buy" has_buy_signal = bool(row["kdj_buy"] or row["ql_buy"]) if not has_buy_signal: return "NONE", "" dual_gold = bool(row["ql_buy"]) if self.context.in_position: if self._should_emit_aux_buy(row): return "AUX_BUY", "bullish_signal_while_holding" return "NONE", "" pending_action, pending_reason = self._pending_deep_oversold_decision(row) if pending_action != "NONE": return pending_action, pending_reason pending_action, pending_reason = self._pending_glued_followthrough_decision(row) if pending_action != "NONE": return pending_action, pending_reason if bool(row["ql_buy"]) and not bool(row["kdj_buy"]): if allow_predictive_error_reentry( enabled=self._rule_enabled("predictive_error_reentry_buy"), last_exit_predictive_break=self.context.bridge_last_exit_predictive_break, last_real_sell_date=self.context.last_real_sell_date, row_date=row.name.date(), a1=a1, b1=b1, c1=c1, ): return "BUY", "predictive_error_reentry_buy" if ( self._rule_enabled("hot_exit_reentry_buy") and self.context.prev_real_sell_c1 is not None and self.context.prev_real_sell_c1 > 80 and self.context.last_real_sell_date is not None and (row.name.date() - self.context.last_real_sell_date).days <= 10 and c1 > 80 and -0.02 < a1 < 0.03 and b1 > -0.03 ): return "BUY", "hot_exit_reentry_buy" return "NONE", "" if self._rule_enabled("early_crash_probe_buy") and self._special_buy_early_crash_probe(row): return "BUY", "early_crash_probe_buy" if self._rule_enabled("deep_oversold_rebound_buy") and self._special_buy_deep_oversold_rebound(row): subtype = self._deep_oversold_subtype(a1, b1, c1) if subtype == "positive_b1_rebound" and self.config.deep_oversold_block_positive_b1_rebound: pass elif ( subtype == "shallow_false_start" and not bool(row["ql_buy"]) and self.config.deep_oversold_block_shallow_false_start_without_ql ): pass elif subtype == "shallow_false_start" and bool(row["ql_buy"]) and self.config.deep_oversold_shallow_ql_fallback: pass elif ( subtype == "positive_b1_rebound" and a1 > self.config.deep_oversold_positive_b1_fallback_a1_min ): pass elif self._deep_oversold_selective_veto(row, subtype): pass elif self._deep_oversold_requires_confirmation(row, subtype): if not self.context.bridge_pending_deep_oversold_active: self._queue_pending_deep_oversold(row, subtype) else: return "BUY", f"deep_oversold_rebound_buy:{subtype}" if self._rule_enabled("oversold_recovery_buy") and self._special_buy_oversold_recovery(row): return "BUY", "oversold_recovery_buy" if self._rule_enabled("post_sell_rebound_buy") and self._special_buy_post_sell_rebound(row): return "BUY", "post_sell_rebound_buy" if ( self._rule_enabled("oversold_reversal_after_ql_buy") and self._special_buy_oversold_reversal_after_ql(row, prev_row) ): return "BUY", "oversold_reversal_after_ql_buy" if self._special_buy_knife1(row): return "BUY", "knife_catch_1" if self._special_buy_knife2(row): return "BUY", "knife_catch_2" if self._is_big_negative(a1) and not dual_gold: return "BLOCK", "buy_block_a1_too_negative" if self._is_b1_hard_negative(b1): return "BLOCK", "buy_block_b1_too_negative" if self._buy_filter_early_downtrend(row, prev_row): return "BLOCK", "buy_block_early_downtrend" if self._buy_filter_high_zone_after_hot_exit(row): return "BLOCK", "buy_block_c1_still_over_80_after_hot_exit" if self._is_glued(a1): if self._buy_filter_glued_b1_descending(row): return "BLOCK", "buy_block_glued_b1_still_descending" glued_block_subtype = self._glued_high_weak_rebound_subtype(row) if glued_block_subtype: if ( self._glued_followthrough_should_queue(glued_block_subtype) and not self.context.bridge_pending_glued_followthrough_active ): self._queue_pending_glued_followthrough(row, glued_block_subtype) return "BLOCK", "buy_block_glued_high_weak_rebound" if self._buy_filter_glued_selective_short_holding(row): return "BLOCK", "buy_block_glued_selective_short_holding" if self._rule_enabled("glued_buy"): return "BUY", "glued_buy" return "BLOCK", "buy_block_glued_rule_disabled" if ( not dual_gold and a1 > 0 and b1 > 0 and self._buy_improving(row) and self._rule_enabled("non_glued_positive_expansion_buy") ): return "BUY", "non_glued_positive_expansion_buy" if dual_gold and c1 > 18 and c1 < 20 and a1 > -0.05 and b1 < -0.09: return "BLOCK", "buy_block_dual_gold_false_rebound" if dual_gold and self._buy_improving(row) and self._rule_enabled("dual_gold_resonance_buy"): return "BUY", "dual_gold_resonance_buy" return "BLOCK", "buy_block_base_conditions_not_met" def _sell_decision(self, row: pd.Series, prev_row: Optional[pd.Series]) -> tuple[str, str]: a1 = float(row["a1"]) b1 = float(row["b1"]) c1 = float(row["c1"]) has_sell_signal = bool(row["kdj_sell"] or row["ql_sell"]) source = "ql_sell" if bool(row["ql_sell"]) and not bool(row["kdj_sell"]) else "kdj_sell" if not has_sell_signal: source = "state" if not self.context.in_position: if has_sell_signal and self._should_emit_aux_sell(row): return "AUX_SELL", f"bearish_signal_after_exit:{source}" if not has_sell_signal and self._should_emit_state_aux_sell(row): return "AUX_SELL", "bearish_signal_after_exit:state_crash_followthrough" return "NONE", "" if allow_predictive_b1_break_short_exit( enabled=self._rule_enabled("predictive_b1_break_exit"), has_sell_signal=has_sell_signal, entry_is_glued=self._entry_reason_is("glued_buy"), holding_days=self._holding_days(row.name.date()), a1=a1, b1=b1, c1=c1, config=self.config, ): return "SELL", "predictive_b1_break_exit" if allow_predictive_b1_break_long_exit( enabled=self._rule_enabled("predictive_b1_break_exit"), has_sell_signal=has_sell_signal, entry_is_glued=self._entry_reason_is("glued_buy"), holding_days=self._holding_days(row.name.date()), max_c1_since_entry=self.context.max_c1_since_entry, max_a1_since_entry=self.context.max_a1_since_entry, max_b1_since_entry=self.context.max_b1_since_entry, last_ql_sell_date=self.context.last_ql_sell_date, row_date=row.name.date(), a1=a1, b1=b1, c1=c1, config=self.config, ): return "SELL", "predictive_b1_break_exit" if ( not has_sell_signal and self._holding_days(row.name.date()) <= 2 and abs(a1) < 0.01 and -0.05 < b1 < 0 and 45 < c1 < 55 ): return "SELL", "early_failed_rebound_exit" if ( bool(row["ql_sell"]) and not bool(row["kdj_sell"]) and 14 <= c1 < 30 and -0.03 <= a1 < 0 and -0.05 < b1 < 0 ): return "HOLD", "low_zone_wait_kdj_confirmation" if ( self._is_big_negative(a1) and bool(row["ql_sell"]) and not bool(row["kdj_sell"]) and c1 < 12 and self._holding_days(row.name.date()) <= 2 and b1 > -0.10 ): return "HOLD", "deep_oversold_wait_kdj_confirmation" if (self._is_big_negative(a1) or self._is_b1_hard_negative(b1)) and (has_sell_signal or b1 < -0.12): return "SELL", f"hard_exit:{source}" if ( self.context.c1_over_80_seen and self.context.max_a1_since_entry > 0.05 and a1 < 0.03 and b1 < -0.08 ): return "SELL", "crash_protection_exit" if not has_sell_signal: if ( self.context.last_ql_sell_date is not None and 0 <= (row.name.date() - self.context.last_ql_sell_date).days <= 7 and self.context.max_c1_since_entry < 80 and 75 < c1 < 80 and a1 > 0.025 and b1 < 0.14 ): return "SELL", "post_ql_decay_exit" if ( self.context.last_kdj_sell_date is not None and self.context.last_ql_sell_date is not None and 0 <= (row.name.date() - self.context.last_kdj_sell_date).days <= 3 and 0 <= (row.name.date() - self.context.last_ql_sell_date).days <= 3 and 55 < c1 < 65 and 0.015 < a1 < 0.025 and 0.17 < b1 < 0.19 ): return "SELL", "post_dual_sell_decay_exit" if ( self.context.last_ql_sell_date is not None and 0 <= (row.name.date() - self.context.last_ql_sell_date).days <= 7 and self._entry_reason_is("glued_buy") and 84 < c1 < 86.5 and a1 < 0.028 and b1 < 0 and self.context.max_b1_since_entry < 0.14 ): return "SELL", "high_zone_post_ql_fade_exit" if ( self._entry_reason_is("deep_oversold_rebound_buy", "oversold_reversal_after_ql_buy") and bool(row["ql_buy"]) and self._holding_days(row.name.date()) >= 10 and c1 < 15 and a1 > -0.02 and b1 > 0 ): return "SELL", "oversold_rebound_take_profit" return "NONE", "" high_regime_decision = self._high_regime_exit_decision(row, source) if high_regime_decision is not None: return high_regime_decision ql_only_decision = self._ql_only_take_profit_exit(row) if ql_only_decision is not None: return ql_only_decision if ( bool(row["ql_sell"]) and not bool(row["kdj_sell"]) and self._entry_reason_is("glued_buy") and self._holding_days(row.name.date()) <= 7 and 60 < c1 < 72 and abs(a1) < 0.01 and -0.06 < b1 < 0 and self.context.max_c1_since_entry < 75 ): return "HOLD", "glued_mid_zone_wait_kdj_confirmation" if ( bool(row["ql_sell"]) and not bool(row["kdj_sell"]) and self._entry_reason_is("deep_oversold_rebound_buy", "oversold_reversal_after_ql_buy") and self._holding_days(row.name.date()) <= 20 and c1 < 18 and -0.03 <= a1 < -0.018 and 0 < b1 < 0.08 ): return "HOLD", "oversold_low_zone_wait_kdj_confirmation" if self.context.entry_a1 is not None and self.context.entry_b1 is not None and not self.context.first_exit_checked: entry_a1 = self.context.entry_a1 entry_b1 = self.context.entry_b1 if ( entry_a1 < 0 and entry_b1 < 0 and self.context.max_c1_since_entry < 80 and 0.02 < a1 < 0.028 and b1 < 0.17 and not self._is_glued(a1) and self._rule_enabled("knife_take_profit_1") ): return "SELL", "knife_take_profit_1" if self._is_glued(a1) and self.context.max_c1_since_entry < 80: if ( bool(row["ql_sell"]) and not bool(row["kdj_sell"]) and self._entry_reason_is("glued_buy") and self._holding_days(row.name.date()) >= 40 and 70 < c1 < 75 and 0 < a1 < 0.02 and 0 <= b1 < 0.02 and self.context.max_a1_since_entry > 0.15 and self.context.max_b1_since_entry > 0.30 ): return "HOLD", "long_glued_wait_predictive_break" if ( bool(row["ql_sell"]) and not bool(row["kdj_sell"]) and self._entry_reason_is("glued_buy") and self._holding_days(row.name.date()) >= 15 and 60 < c1 < 66 and a1 > 0.01 and b1 > 0.12 ): return "HOLD", "knife_take_profit_2_glued_wait_followthrough" if should_hold_glued_followthrough_reentry_kdj_only( enabled=self.config.glued_followthrough_exit_hold_kdj_only_enabled, entry_reason_code=self.context.entry_reason_code, kdj_sell=bool(row["kdj_sell"]), ql_sell=bool(row["ql_sell"]), holding_days=self._holding_days(row.name.date()), a1=a1, b1=b1, c1=c1, config=self.config, ): return "HOLD", "glued_followthrough_reentry_wait_ql_confirmation" if b1 < 0.17 and self._rule_enabled("knife_take_profit_2_glued"): return "SELL", "knife_take_profit_2_glued" if bool(row["ql_sell"]) and self.config.enable_knife_take_profit_2_wait_ql: return "SELL", "knife_take_profit_2_wait_ql_s" weakened_from_peak = ( self.context.c1_over_80_seen and self.context.combo_big_cycle_count >= 2 and self.context.max_a1_since_entry > 0.04 and a1 < self.context.max_a1_since_entry - 0.008 and b1 < min(0.17, self.context.max_b1_since_entry - 0.04) ) if weakened_from_peak and a1 < 0.05: if not bool(row["kdj_sell"]): return "HOLD", f"high_zone_wait_kdj_sell:{source}" return "SELL", "good_to_take_profit_1" large_regime_confirmed = self.context.a1_big_cycle_count >= 4 or self.context.a1_big_pos_count >= 4 if ( large_regime_confirmed and self.context.b1_negative_sell_count >= 2 and self.context.c1_over_80_seen and a1 < 0.05 ): return "SELL", f"good_to_take_profit_2:{source}" if a1 > 0.05: return "HOLD", f"hold_a1_above_5pct:{source}" if self._is_glued(a1): if large_regime_confirmed and self.context.b1_negative_sell_count < 2: return "HOLD", f"hold_glued_large_regime_wait_b1_negative_count:{source}" return "SELL", f"glued_exit:{source}" prev_a1 = float(prev_row["a1"]) if prev_row is not None else a1 prev_c1 = float(prev_row["c1"]) if prev_row is not None else c1 a1_declining = a1 < prev_a1 c1_declining = c1 < prev_c1 if 0.028 < a1 < 0.05: if ( bool(row["kdj_sell"]) and not bool(row["ql_sell"]) and c1 > 80 and b1 > 0.13 and self.context.combo_big_cycle_count >= 2 ): return "HOLD", "mid_hot_wait_ql_confirmation" if ( bool(row["kdj_sell"]) and not bool(row["ql_sell"]) and c1 > 84 and b1 < 0.11 and self.context.max_a1_since_entry < 0.04 and self.context.max_b1_since_entry > 0.18 ): return "SELL", "medium_hot_take_profit" if large_regime_confirmed and self.context.b1_negative_sell_count >= 2: return "SELL", f"good_to_take_profit_2:{source}" if self.context.combo_big_cycle_count >= 2 and a1 < 0.04 and a1_declining and b1 < 0.17: return "SELL", "good_to_take_profit_1" if self.context.b1_negative_sell_count >= 2 and c1_declining: return "SELL", f"good_to_take_profit_2:{source}" return "HOLD", f"hold_mid_positive_a1:{source}" if -0.04 < a1 < -0.02: if ( bool(row["kdj_sell"]) and self._entry_reason_is("dual_gold_resonance_buy") and c1 < 20 and b1 > 0 ): return "SELL", f"low_zone_dual_gold_exit:{source}" if b1 <= 0: return "SELL", f"negative_a1_no_b1_recovery:{source}" if b1 < 0.17 and a1_declining: return "SELL", f"negative_a1_b1_not_strong:{source}" return "HOLD", f"hold_negative_a1_wait_confirmation:{source}" if 0.02 < a1 < 0.028: if ( bool(row["kdj_sell"]) and not bool(row["ql_sell"]) and c1 > 60 and b1 > 0.20 and self.context.max_c1_since_entry < 80 and self.context.max_a1_since_entry < 0.025 and self.context.max_b1_since_entry < 0.25 and self._holding_days(row.name.date()) <= 20 ): return "SELL", "early_positive_take_profit" if ( bool(row["kdj_sell"]) and not bool(row["ql_sell"]) and c1 > 80 and b1 > 0.08 and self.context.max_b1_since_entry > 0.15 ): return "HOLD", f"wait_ql_confirmation_small_positive:{source}" if b1 < 0.17 and a1_declining: return "SELL", f"small_positive_a1_declining:{source}" return "HOLD", f"hold_small_positive_a1:{source}" if ( bool(row["ql_sell"]) and not bool(row["kdj_sell"]) and 55 < c1 < 75 and abs(a1) < 0.01 and -0.05 < b1 < 0 ): return "HOLD", "mid_zone_wait_kdj_confirmation" return "SELL", f"default_exit:{source}" def _post_real_buy(self, row: pd.Series, reason: str) -> None: self._clear_pending_deep_oversold() self._clear_pending_glued_followthrough() entry_meta = classify_entry_reason(reason) self.context.in_position = True self.context.entry_date = row.name.date() self.context.entry_price = float(row["close"]) self.context.entry_a1 = float(row["a1"]) self.context.entry_b1 = float(row["b1"]) self.context.entry_c1 = float(row["c1"]) self.context.entry_reason = reason self.context.entry_reason_layer = entry_meta.layer.value self.context.entry_reason_family = entry_meta.family.value self.context.entry_reason_code = entry_meta.code self.context.first_exit_checked = False self.context.c1_over_80_seen = float(row["c1"]) > 80 self.context.a1_big_pos_count = 1 if self._is_big_positive(float(row["a1"])) else 0 self.context.b1_big_pos_count = 1 if self._is_b1_strong_positive(float(row["b1"])) else 0 self.context.b1_negative_sell_count = 0 self.context.max_a1_since_entry = float(row["a1"]) self.context.max_b1_since_entry = float(row["b1"]) self.context.max_c1_since_entry = float(row["c1"]) self.context.a1_big_cycle_count = 1 if self._is_big_positive(float(row["a1"])) else 0 self.context.b1_big_cycle_count = 1 if self._is_b1_strong_positive(float(row["b1"])) else 0 self.context.combo_big_cycle_count = 1 if (self._is_big_positive(float(row["a1"])) and self._is_b1_strong_positive(float(row["b1"]))) else 0 self.context.sell_signal_count = 0 self.context.kdj_sell_signal_count = 0 self.context.ql_sell_signal_count = 0 self.context.prev_a1_big_flag = self._is_big_positive(float(row["a1"])) self.context.prev_b1_big_flag = self._is_b1_strong_positive(float(row["b1"])) self.context.prev_combo_big_flag = self.context.prev_a1_big_flag and self.context.prev_b1_big_flag def _post_real_sell(self, row: pd.Series, reason: str) -> None: self._clear_pending_deep_oversold() self._clear_pending_glued_followthrough() sell_meta = classify_exit_reason(reason) if self.context.a1_big_pos_count >= 4: self.context.last_big_regime_exit_date = row.name.date() self.context.kdj_cross_count_since_big_regime_exit = 0 self.context.prev_real_sell_c1 = float(row["c1"]) self.context.last_real_sell_date = row.name.date() self.context.last_real_sell_reason = reason self.context.last_real_sell_reason_layer = sell_meta.layer.value self.context.last_real_sell_reason_family = sell_meta.family.value self.context.last_real_sell_reason_code = sell_meta.code self.context.bridge_last_exit_predictive_break = sell_meta.code == "exit_predictive_b1_break" self.context.bridge_last_exit_negative_a1_no_b1_recovery = sell_meta.code == "exit_negative_a1_recovery" self.context.in_position = False self.context.entry_date = None self.context.entry_price = None self.context.entry_a1 = None self.context.entry_b1 = None self.context.entry_c1 = None self.context.entry_reason = "" self.context.entry_reason_layer = "unknown" self.context.entry_reason_family = "unknown" self.context.entry_reason_code = "" self.context.first_exit_checked = False self.context.c1_over_80_seen = False self.context.a1_big_pos_count = 0 self.context.b1_big_pos_count = 0 self.context.b1_negative_sell_count = 0 self.context.max_a1_since_entry = -999.0 self.context.max_b1_since_entry = -999.0 self.context.max_c1_since_entry = -999.0 self.context.a1_big_cycle_count = 0 self.context.b1_big_cycle_count = 0 self.context.combo_big_cycle_count = 0 self.context.sell_signal_count = 0 self.context.kdj_sell_signal_count = 0 self.context.ql_sell_signal_count = 0 self.context.flat_post_exit_ql_emitted = False self.context.flat_post_exit_kdj_emitted = False self.context.prev_a1_big_flag = False self.context.prev_b1_big_flag = False self.context.prev_combo_big_flag = False def _post_aux_buy(self, row: pd.Series) -> None: self.context.last_aux_buy_date = row.name.date() self.context.last_aux_buy_c1 = float(row["c1"]) def _post_aux_sell(self, row: pd.Series, reason: str) -> None: self.context.last_aux_sell_date = row.name.date() self.context.last_aux_sell_c1 = float(row["c1"]) self.context.last_aux_sell_b1 = float(row["b1"]) self.context.last_aux_sell_reason = reason days_from_last_sell = self._days_from_last_real_sell(row.name.date()) if days_from_last_sell is not None and 0 < days_from_last_sell <= self.config.post_exit_confirmation_window_days: if reason.endswith("ql_sell"): self.context.flat_post_exit_ql_emitted = True elif reason.endswith("kdj_sell"): self.context.flat_post_exit_kdj_emitted = True def run(self, df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]: from dragon_execution_runtime import run_compat_execution return run_compat_execution(self, df) def run_with_layered_engine(df: pd.DataFrame, config: Optional[StrategyConfig] = None) -> tuple[pd.DataFrame, pd.DataFrame]: from dragon_rule_engine_v2 import LayeredDragonRuleEngine engine = LayeredDragonRuleEngine(config=config) return engine.run(df)