|
|
@@ -6,6 +6,21 @@ 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_rule_catalog import classify_aux_reason, classify_entry_reason, classify_exit_reason
|
|
|
from dragon_strategy_config import StrategyConfig
|
|
|
@@ -219,13 +234,12 @@ class DragonRuleEngine:
|
|
|
self.context.last_ql_sell_date = row.name.date()
|
|
|
|
|
|
def _update_pending_states(self, row: pd.Series) -> None:
|
|
|
- if (
|
|
|
- not self.context.bridge_pending_deep_oversold_active
|
|
|
- or not self.context.pending_deep_oversold_subtype
|
|
|
- or self.context.pending_deep_oversold_origin_date is 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(),
|
|
|
):
|
|
|
- return
|
|
|
- if row.name.date() != self.context.pending_deep_oversold_origin_date:
|
|
|
self.context.pending_deep_oversold_bars_waited += 1
|
|
|
|
|
|
def _update_position_counters(self, row: pd.Series) -> None:
|
|
|
@@ -553,96 +567,49 @@ class DragonRuleEngine:
|
|
|
a1 = float(row["a1"])
|
|
|
b1 = float(row["b1"])
|
|
|
c1 = float(row["c1"])
|
|
|
- if (
|
|
|
- self.config.deep_oversold_filter1_c1_low < c1 < self.config.deep_oversold_filter1_c1_high
|
|
|
- and a1 > self.config.deep_oversold_filter1_a1_min
|
|
|
- and b1 < self.config.deep_oversold_filter1_b1_max
|
|
|
- ):
|
|
|
- return False
|
|
|
- if (
|
|
|
- self.config.deep_oversold_filter2_c1_low < c1 < self.config.deep_oversold_filter2_c1_high
|
|
|
- and a1 > self.config.deep_oversold_filter2_a1_min
|
|
|
- and b1 > self.config.deep_oversold_filter2_b1_min
|
|
|
- ):
|
|
|
- return False
|
|
|
- return (
|
|
|
- c1 < self.config.deep_oversold_entry_c1_max
|
|
|
- and a1 > self.config.deep_oversold_entry_a1_min
|
|
|
- and b1 > self.config.deep_oversold_entry_b1_min
|
|
|
+ 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:
|
|
|
- if not self.config.deep_oversold_confirm_weak_with_ql:
|
|
|
- return False
|
|
|
- if subtype not in {"positive_b1_rebound", "shallow_false_start"}:
|
|
|
- return False
|
|
|
- if bool(row["ql_buy"]):
|
|
|
- return False
|
|
|
- return True
|
|
|
+ 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]:
|
|
|
- if (
|
|
|
- not self.context.bridge_pending_deep_oversold_active
|
|
|
- or not self.context.pending_deep_oversold_subtype
|
|
|
- ):
|
|
|
- return "NONE", ""
|
|
|
- if self.context.in_position:
|
|
|
- self._clear_pending_deep_oversold()
|
|
|
- return "NONE", ""
|
|
|
- if bool(row["kdj_sell"] or row["ql_sell"]):
|
|
|
- self._clear_pending_deep_oversold()
|
|
|
- return "NONE", ""
|
|
|
- if self.context.pending_deep_oversold_bars_waited > self.config.deep_oversold_confirm_window_bars:
|
|
|
- self._clear_pending_deep_oversold()
|
|
|
- return "NONE", ""
|
|
|
- if (
|
|
|
- self.context.pending_deep_oversold_bars_waited >= 1
|
|
|
- and bool(row["ql_buy"])
|
|
|
- ):
|
|
|
- subtype = self.context.pending_deep_oversold_subtype
|
|
|
+ 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 "BUY", f"deep_oversold_rebound_buy:confirmed_{subtype}"
|
|
|
- return "NONE", ""
|
|
|
+ return action, reason
|
|
|
|
|
|
def _deep_oversold_selective_veto(self, row: pd.Series, subtype: str) -> bool:
|
|
|
c1 = float(row["c1"])
|
|
|
b1 = float(row["b1"])
|
|
|
- if (
|
|
|
- subtype == "positive_b1_rebound"
|
|
|
- and self.config.deep_oversold_selective_positive_b1_c1_max > 0
|
|
|
- and c1 < self.config.deep_oversold_selective_positive_b1_c1_max
|
|
|
- ):
|
|
|
- return True
|
|
|
- if (
|
|
|
- subtype == "shallow_false_start"
|
|
|
- and self.config.deep_oversold_selective_shallow_c1_min > 0
|
|
|
- and c1 >= self.config.deep_oversold_selective_shallow_c1_min
|
|
|
- and b1 > self.config.deep_oversold_selective_shallow_b1_min
|
|
|
- ):
|
|
|
- return True
|
|
|
- if (
|
|
|
- subtype == "mixed_oversold"
|
|
|
- and self.config.deep_oversold_selective_mixed_c1_max > 0
|
|
|
- and c1 < self.config.deep_oversold_selective_mixed_c1_max
|
|
|
- and (
|
|
|
- not self.config.deep_oversold_selective_mixed_require_no_ql
|
|
|
- or not bool(row["ql_buy"])
|
|
|
- )
|
|
|
- ):
|
|
|
- return True
|
|
|
- return False
|
|
|
+ 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:
|
|
|
- if b1 > 0:
|
|
|
- return "positive_b1_rebound"
|
|
|
- if c1 < 11 and a1 < -0.05 and b1 < -0.08:
|
|
|
- return "deep_capitulation"
|
|
|
- if c1 < 12 and b1 < -0.06:
|
|
|
- return "classic_oversold"
|
|
|
- if c1 >= 15 or a1 > -0.03 or b1 > -0.03:
|
|
|
- return "shallow_false_start"
|
|
|
- return "mixed_oversold"
|
|
|
+ return deep_oversold_subtype(a1=a1, b1=b1, c1=c1)
|
|
|
|
|
|
def _special_buy_oversold_recovery(self, row: pd.Series) -> bool:
|
|
|
a1 = float(row["a1"])
|
|
|
@@ -757,15 +724,14 @@ class DragonRuleEngine:
|
|
|
return pending_action, pending_reason
|
|
|
|
|
|
if bool(row["ql_buy"]) and not bool(row["kdj_buy"]):
|
|
|
- if (
|
|
|
- self._rule_enabled("predictive_error_reentry_buy")
|
|
|
- and
|
|
|
- self.context.bridge_last_exit_predictive_break
|
|
|
- and self.context.last_real_sell_date is not None
|
|
|
- and (row.name.date() - self.context.last_real_sell_date).days <= 3
|
|
|
- and -0.02 < a1 < 0.01
|
|
|
- and b1 > -0.16
|
|
|
- and c1 > 50
|
|
|
+ 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 (
|
|
|
@@ -872,30 +838,32 @@ class DragonRuleEngine:
|
|
|
return "AUX_SELL", "bearish_signal_after_exit:state_crash_followthrough"
|
|
|
return "NONE", ""
|
|
|
|
|
|
- if (
|
|
|
- not has_sell_signal
|
|
|
- and self._entry_reason_is("glued_buy")
|
|
|
- and self._holding_days(row.name.date()) <= self.config.predictive_b1_break_short_holding_days_max
|
|
|
- and self.config.predictive_b1_break_short_a1_min < a1 < self.config.predictive_b1_break_short_a1_max
|
|
|
- and b1 < self.config.predictive_b1_break_short_b1_max
|
|
|
- and self.config.predictive_b1_break_short_c1_low < c1 < self.config.predictive_b1_break_short_c1_high
|
|
|
- and self._rule_enabled("predictive_b1_break_exit")
|
|
|
+ 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 (
|
|
|
- not has_sell_signal
|
|
|
- and self._entry_reason_is("glued_buy")
|
|
|
- and self._holding_days(row.name.date()) >= self.config.predictive_b1_break_long_holding_days_min
|
|
|
- and self.context.max_c1_since_entry < self.config.predictive_b1_break_long_max_c1
|
|
|
- and self.context.max_a1_since_entry > self.config.predictive_b1_break_long_max_a1
|
|
|
- and self.context.max_b1_since_entry > self.config.predictive_b1_break_long_max_b1
|
|
|
- and self.context.last_ql_sell_date is not None
|
|
|
- and 0 < (row.name.date() - self.context.last_ql_sell_date).days <= self.config.predictive_b1_break_long_ql_days_max
|
|
|
- and self.config.predictive_b1_break_long_a1_min < a1 < self.config.predictive_b1_break_long_a1_max
|
|
|
- and b1 < self.config.predictive_b1_break_long_b1_max
|
|
|
- and self.config.predictive_b1_break_long_c1_low < c1 < self.config.predictive_b1_break_long_c1_high
|
|
|
- and self._rule_enabled("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"
|
|
|
|