dragon_strategy.py 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324
  1. from __future__ import annotations
  2. from dataclasses import dataclass
  3. from datetime import date
  4. from typing import Optional
  5. import pandas as pd
  6. from dragon_bridge_predictive_break import (
  7. allow_predictive_b1_break_long_exit,
  8. allow_predictive_b1_break_short_exit,
  9. allow_predictive_error_reentry,
  10. )
  11. from dragon_deep_oversold_classifier import (
  12. deep_oversold_base_entry,
  13. deep_oversold_requires_confirmation,
  14. deep_oversold_selective_veto,
  15. deep_oversold_subtype,
  16. )
  17. from dragon_deep_oversold_confirmation import (
  18. evaluate_pending_confirmation,
  19. should_increment_pending,
  20. )
  21. from dragon_decision_types import StrategyDecision
  22. from dragon_glued_followthrough_confirmation import (
  23. evaluate_glued_followthrough_confirmation,
  24. glued_followthrough_pending_allowed,
  25. glued_high_weak_rebound_subtype,
  26. )
  27. from dragon_glued_followthrough_exit import should_hold_glued_followthrough_reentry_kdj_only
  28. from dragon_rule_catalog import classify_aux_reason, classify_entry_reason, classify_exit_reason
  29. from dragon_strategy_config import StrategyConfig
  30. @dataclass
  31. class StrategyContext:
  32. in_position: bool = False
  33. entry_date: Optional[date] = None
  34. entry_price: Optional[float] = None
  35. entry_a1: Optional[float] = None
  36. entry_b1: Optional[float] = None
  37. entry_c1: Optional[float] = None
  38. entry_reason: str = ""
  39. entry_reason_layer: str = "unknown"
  40. entry_reason_family: str = "unknown"
  41. entry_reason_code: str = ""
  42. first_exit_checked: bool = False
  43. c1_over_80_seen: bool = False
  44. a1_big_pos_count: int = 0
  45. b1_big_pos_count: int = 0
  46. b1_negative_sell_count: int = 0
  47. prev_real_sell_c1: Optional[float] = None
  48. last_real_sell_date: Optional[date] = None
  49. last_real_sell_reason: str = ""
  50. last_real_sell_reason_layer: str = "unknown"
  51. last_real_sell_reason_family: str = "unknown"
  52. last_real_sell_reason_code: str = ""
  53. bridge_last_exit_predictive_break: bool = False
  54. bridge_last_exit_negative_a1_no_b1_recovery: bool = False
  55. last_big_regime_exit_date: Optional[date] = None
  56. last_kdj_buy_date: Optional[date] = None
  57. last_kdj_buy_a1: Optional[float] = None
  58. last_kdj_buy_b1: Optional[float] = None
  59. last_kdj_buy_c1: Optional[float] = None
  60. last_kdj_sell_date: Optional[date] = None
  61. last_ql_sell_date: Optional[date] = None
  62. last_aux_buy_date: Optional[date] = None
  63. last_aux_buy_c1: Optional[float] = None
  64. last_aux_sell_date: Optional[date] = None
  65. last_aux_sell_c1: Optional[float] = None
  66. last_aux_sell_b1: Optional[float] = None
  67. last_aux_sell_reason: str = ""
  68. flat_post_exit_ql_emitted: bool = False
  69. flat_post_exit_kdj_emitted: bool = False
  70. kdj_cross_count_since_big_regime_exit: int = 999
  71. max_a1_since_entry: float = -999.0
  72. max_b1_since_entry: float = -999.0
  73. max_c1_since_entry: float = -999.0
  74. a1_big_cycle_count: int = 0
  75. b1_big_cycle_count: int = 0
  76. combo_big_cycle_count: int = 0
  77. sell_signal_count: int = 0
  78. kdj_sell_signal_count: int = 0
  79. ql_sell_signal_count: int = 0
  80. prev_a1_big_flag: bool = False
  81. prev_b1_big_flag: bool = False
  82. prev_combo_big_flag: bool = False
  83. pending_deep_oversold_subtype: str = ""
  84. pending_deep_oversold_origin_date: Optional[date] = None
  85. pending_deep_oversold_reason: str = ""
  86. pending_deep_oversold_a1: Optional[float] = None
  87. pending_deep_oversold_b1: Optional[float] = None
  88. pending_deep_oversold_c1: Optional[float] = None
  89. pending_deep_oversold_bars_waited: int = 0
  90. bridge_pending_deep_oversold_active: bool = False
  91. pending_glued_followthrough_subtype: str = ""
  92. pending_glued_followthrough_origin_date: Optional[date] = None
  93. pending_glued_followthrough_reason: str = ""
  94. pending_glued_followthrough_signal_close: Optional[float] = None
  95. pending_glued_followthrough_a1: Optional[float] = None
  96. pending_glued_followthrough_b1: Optional[float] = None
  97. pending_glued_followthrough_c1: Optional[float] = None
  98. pending_glued_followthrough_bars_waited: int = 0
  99. bridge_pending_glued_followthrough_active: bool = False
  100. class DragonRuleEngine:
  101. """
  102. Executable baseline for the Dragon narrative rules.
  103. This version focuses on the main rule trunk:
  104. - KDJ gold-cross driven entries
  105. - KDJ / QL sell-cross driven exits
  106. - hard vetoes and hard exits from A1/B1 thresholds
  107. - special low-C1 knife-catching entries
  108. - auxiliary bearish / bullish signal downgrades
  109. """
  110. def __init__(self, config: Optional[StrategyConfig] = None):
  111. self.config = config or StrategyConfig()
  112. self.context = StrategyContext()
  113. def _rule_enabled(self, rule_name: str) -> bool:
  114. return rule_name not in self.config.disabled_rules
  115. def _classify_reason(self, side: str, layer: str, reason: str):
  116. if layer == "aux_signal":
  117. return classify_aux_reason(reason)
  118. if side == "BUY":
  119. return classify_entry_reason(reason)
  120. return classify_exit_reason(reason)
  121. def _build_decision(self, side: str, layer: str, reason: str) -> StrategyDecision:
  122. if side == "BUY":
  123. action = "BUY" if layer == "real_trade" else "AUX_BUY"
  124. else:
  125. action = "SELL" if layer == "real_trade" else "AUX_SELL"
  126. return StrategyDecision(
  127. action=action,
  128. reason=self._classify_reason(side, layer, reason),
  129. )
  130. def _entry_reason_is(self, *reason_names: str) -> bool:
  131. entry_reason = self.context.entry_reason
  132. entry_family = entry_reason.split(":", 1)[0]
  133. if entry_reason in reason_names or entry_family in reason_names:
  134. return True
  135. if not self.context.entry_reason_code and not self.context.entry_reason_family:
  136. return False
  137. if "glued_buy" in reason_names and self.context.entry_reason_code == "entry_glued_buy":
  138. return True
  139. if "dual_gold_resonance_buy" in reason_names and self.context.entry_reason_code == "entry_dual_gold":
  140. return True
  141. if (
  142. "deep_oversold_rebound_buy" in reason_names
  143. and self.context.entry_reason_family == "deep_oversold"
  144. ):
  145. return True
  146. if (
  147. "oversold_reversal_after_ql_buy" in reason_names
  148. and self.context.entry_reason_code == "entry_oversold_reversal_after_ql"
  149. ):
  150. return True
  151. return False
  152. def _last_sell_reason_is(self, reason_name: str) -> bool:
  153. if self.context.last_real_sell_reason == reason_name:
  154. return True
  155. code_map = {
  156. "crash_protection_exit": "exit_crash_protection",
  157. "predictive_b1_break_exit": "exit_predictive_b1_break",
  158. }
  159. target_code = code_map.get(reason_name, "")
  160. return bool(target_code and self.context.last_real_sell_reason_code == target_code)
  161. def _clear_pending_deep_oversold(self) -> None:
  162. self.context.pending_deep_oversold_subtype = ""
  163. self.context.pending_deep_oversold_origin_date = None
  164. self.context.pending_deep_oversold_reason = ""
  165. self.context.pending_deep_oversold_a1 = None
  166. self.context.pending_deep_oversold_b1 = None
  167. self.context.pending_deep_oversold_c1 = None
  168. self.context.pending_deep_oversold_bars_waited = 0
  169. self.context.bridge_pending_deep_oversold_active = False
  170. def _queue_pending_deep_oversold(self, row: pd.Series, subtype: str) -> None:
  171. self.context.pending_deep_oversold_subtype = subtype
  172. self.context.pending_deep_oversold_origin_date = row.name.date()
  173. self.context.pending_deep_oversold_reason = f"deep_oversold_rebound_buy:{subtype}"
  174. self.context.pending_deep_oversold_a1 = float(row["a1"])
  175. self.context.pending_deep_oversold_b1 = float(row["b1"])
  176. self.context.pending_deep_oversold_c1 = float(row["c1"])
  177. self.context.pending_deep_oversold_bars_waited = 0
  178. self.context.bridge_pending_deep_oversold_active = True
  179. def _clear_pending_glued_followthrough(self) -> None:
  180. self.context.pending_glued_followthrough_subtype = ""
  181. self.context.pending_glued_followthrough_origin_date = None
  182. self.context.pending_glued_followthrough_reason = ""
  183. self.context.pending_glued_followthrough_signal_close = None
  184. self.context.pending_glued_followthrough_a1 = None
  185. self.context.pending_glued_followthrough_b1 = None
  186. self.context.pending_glued_followthrough_c1 = None
  187. self.context.pending_glued_followthrough_bars_waited = 0
  188. self.context.bridge_pending_glued_followthrough_active = False
  189. def _queue_pending_glued_followthrough(self, row: pd.Series, subtype: str) -> None:
  190. self.context.pending_glued_followthrough_subtype = subtype
  191. self.context.pending_glued_followthrough_origin_date = row.name.date()
  192. self.context.pending_glued_followthrough_reason = f"buy_block_glued_high_weak_rebound:{subtype}"
  193. self.context.pending_glued_followthrough_signal_close = float(row["close"])
  194. self.context.pending_glued_followthrough_a1 = float(row["a1"])
  195. self.context.pending_glued_followthrough_b1 = float(row["b1"])
  196. self.context.pending_glued_followthrough_c1 = float(row["c1"])
  197. self.context.pending_glued_followthrough_bars_waited = 0
  198. self.context.bridge_pending_glued_followthrough_active = True
  199. @staticmethod
  200. def _is_glued(a1: float) -> bool:
  201. return abs(a1) < 0.02
  202. @staticmethod
  203. def _is_big_positive(a1: float) -> bool:
  204. return a1 > 0.028
  205. @staticmethod
  206. def _is_big_negative(a1: float) -> bool:
  207. return a1 < -0.04
  208. @staticmethod
  209. def _is_b1_hard_negative(b1: float) -> bool:
  210. return b1 < -0.17
  211. @staticmethod
  212. def _is_b1_strong_positive(b1: float) -> bool:
  213. return b1 > 0.17
  214. def _holding_days(self, row_date: date) -> int:
  215. if self.context.entry_date is None:
  216. return 0
  217. return (row_date - self.context.entry_date).days
  218. def _days_from_last_real_sell(self, row_date: date) -> Optional[int]:
  219. if self.context.last_real_sell_date is None:
  220. return None
  221. return (row_date - self.context.last_real_sell_date).days
  222. def _days_from_last_aux_buy(self, row_date: date) -> Optional[int]:
  223. if self.context.last_aux_buy_date is None:
  224. return None
  225. return (row_date - self.context.last_aux_buy_date).days
  226. def _days_from_last_aux_sell(self, row_date: date) -> Optional[int]:
  227. if self.context.last_aux_sell_date is None:
  228. return None
  229. return (row_date - self.context.last_aux_sell_date).days
  230. def _record_cross_counters(self, row: pd.Series) -> None:
  231. if bool(row["kdj_buy"]):
  232. self.context.last_kdj_buy_date = row.name.date()
  233. self.context.last_kdj_buy_a1 = float(row["a1"])
  234. self.context.last_kdj_buy_b1 = float(row["b1"])
  235. self.context.last_kdj_buy_c1 = float(row["c1"])
  236. if self.context.kdj_cross_count_since_big_regime_exit != 999:
  237. self.context.kdj_cross_count_since_big_regime_exit += 1
  238. if bool(row["kdj_sell"]):
  239. self.context.last_kdj_sell_date = row.name.date()
  240. if self.context.kdj_cross_count_since_big_regime_exit != 999:
  241. self.context.kdj_cross_count_since_big_regime_exit += 1
  242. if bool(row["ql_sell"]):
  243. self.context.last_ql_sell_date = row.name.date()
  244. def _update_pending_states(self, row: pd.Series) -> None:
  245. if should_increment_pending(
  246. active=self.context.bridge_pending_deep_oversold_active,
  247. subtype=self.context.pending_deep_oversold_subtype,
  248. origin_date=self.context.pending_deep_oversold_origin_date,
  249. row_date=row.name.date(),
  250. ):
  251. self.context.pending_deep_oversold_bars_waited += 1
  252. if should_increment_pending(
  253. active=self.context.bridge_pending_glued_followthrough_active,
  254. subtype=self.context.pending_glued_followthrough_subtype,
  255. origin_date=self.context.pending_glued_followthrough_origin_date,
  256. row_date=row.name.date(),
  257. ):
  258. self.context.pending_glued_followthrough_bars_waited += 1
  259. if self.context.bridge_pending_glued_followthrough_active:
  260. if bool(row["kdj_sell"]) or bool(row["ql_sell"]):
  261. self._clear_pending_glued_followthrough()
  262. elif self.context.pending_glued_followthrough_bars_waited > self.config.glued_followthrough_confirm_window_bars:
  263. self._clear_pending_glued_followthrough()
  264. def _update_position_counters(self, row: pd.Series) -> None:
  265. if not self.context.in_position:
  266. return
  267. a1 = float(row["a1"])
  268. b1 = float(row["b1"])
  269. c1 = float(row["c1"])
  270. self.context.max_a1_since_entry = max(self.context.max_a1_since_entry, a1)
  271. self.context.max_b1_since_entry = max(self.context.max_b1_since_entry, b1)
  272. self.context.max_c1_since_entry = max(self.context.max_c1_since_entry, c1)
  273. a1_big_flag = self._is_big_positive(a1)
  274. b1_big_flag = self._is_b1_strong_positive(b1)
  275. combo_big_flag = a1_big_flag and b1_big_flag
  276. if a1_big_flag and not self.context.prev_a1_big_flag:
  277. self.context.a1_big_cycle_count += 1
  278. if b1_big_flag and not self.context.prev_b1_big_flag:
  279. self.context.b1_big_cycle_count += 1
  280. if combo_big_flag and not self.context.prev_combo_big_flag:
  281. self.context.combo_big_cycle_count += 1
  282. self.context.prev_a1_big_flag = a1_big_flag
  283. self.context.prev_b1_big_flag = b1_big_flag
  284. self.context.prev_combo_big_flag = combo_big_flag
  285. if self._is_big_positive(a1):
  286. self.context.a1_big_pos_count += 1
  287. if self._is_b1_strong_positive(b1):
  288. self.context.b1_big_pos_count += 1
  289. if c1 > 80:
  290. self.context.c1_over_80_seen = True
  291. def _is_high_regime_trade(self) -> bool:
  292. return (
  293. self.context.c1_over_80_seen
  294. and self.context.max_c1_since_entry > 80
  295. and self.context.max_a1_since_entry > 0.045
  296. )
  297. def _high_regime_exit_decision(self, row: pd.Series, source: str) -> Optional[tuple[str, str]]:
  298. if not self._is_high_regime_trade():
  299. return None
  300. a1 = float(row["a1"])
  301. b1 = float(row["b1"])
  302. c1 = float(row["c1"])
  303. kdj_sell = bool(row["kdj_sell"])
  304. ql_sell = bool(row["ql_sell"])
  305. if (
  306. ql_sell
  307. and not kdj_sell
  308. and self.context.max_b1_since_entry > 0.15
  309. and a1 <= 0.038
  310. and b1 <= 0.12
  311. and c1 > 80
  312. and self._rule_enabled("ql_high_zone_take_profit")
  313. ):
  314. return "SELL", "ql_high_zone_take_profit"
  315. if ql_sell and not kdj_sell and c1 > 80 and a1 > 0.02:
  316. return "HOLD", "high_regime_wait_kdj_confirmation"
  317. if (
  318. self.context.max_a1_since_entry > 0.09
  319. and a1 > 0.055
  320. and b1 < -0.05
  321. and ql_sell
  322. and self._rule_enabled("high_regime_momentum_break")
  323. ):
  324. return "SELL", "high_regime_momentum_break"
  325. if a1 > 0.06 and c1 > 90 and b1 > -0.03 and not ql_sell:
  326. return "HOLD", "super_hot_trend_hold"
  327. if not kdj_sell:
  328. return None
  329. decayed_from_peak = (
  330. self.context.max_b1_since_entry > 0.18
  331. and a1 < self.context.max_a1_since_entry - 0.005
  332. and b1 < min(0.13, self.context.max_b1_since_entry - 0.02)
  333. )
  334. if 0.033 <= a1 <= 0.05 and c1 > 80 and decayed_from_peak:
  335. if self._rule_enabled("prewarning_reduction_exit"):
  336. return "SELL", "prewarning_reduction_exit"
  337. if (
  338. b1 <= 0
  339. and (
  340. a1 <= 0.022
  341. or c1 < 80
  342. or self.context.kdj_sell_signal_count >= 2
  343. or self.context.b1_negative_sell_count >= 2
  344. )
  345. ):
  346. if self._rule_enabled(f"high_regime_confirmed_exit:{source}"):
  347. return "SELL", f"high_regime_confirmed_exit:{source}"
  348. return "HOLD", f"high_regime_hold:{source}"
  349. def _ql_only_take_profit_exit(self, row: pd.Series) -> Optional[tuple[str, str]]:
  350. if not bool(row["ql_sell"]) or bool(row["kdj_sell"]) or not self.context.in_position:
  351. return None
  352. a1 = float(row["a1"])
  353. b1 = float(row["b1"])
  354. c1 = float(row["c1"])
  355. if self.context.max_b1_since_entry <= 0.15:
  356. return None
  357. if self.context.max_c1_since_entry >= 78 and 0 < a1 <= 0.02 and b1 <= 0.12:
  358. if self._rule_enabled("ql_mid_zone_take_profit"):
  359. return "SELL", "ql_mid_zone_take_profit"
  360. if c1 > 80 and a1 <= 0.038 and b1 <= 0.12:
  361. if self._rule_enabled("ql_high_zone_take_profit"):
  362. return "SELL", "ql_high_zone_take_profit"
  363. return None
  364. def _buy_filter_early_downtrend(self, row: pd.Series, prev_row: Optional[pd.Series]) -> bool:
  365. if prev_row is None:
  366. return False
  367. c1 = float(row["c1"])
  368. prev_c1 = float(prev_row["c1"])
  369. if prev_c1 <= 80 or c1 >= prev_c1:
  370. return False
  371. if self.context.kdj_cross_count_since_big_regime_exit > 6:
  372. return False
  373. return True
  374. def _buy_filter_high_zone_after_hot_exit(self, row: pd.Series) -> bool:
  375. prev_sell_c1 = self.context.prev_real_sell_c1
  376. if prev_sell_c1 is None:
  377. return False
  378. return prev_sell_c1 > 80 and float(row["c1"]) > 80
  379. def _buy_filter_glued_b1_descending(self, row: pd.Series) -> bool:
  380. prev_b1 = self.context.last_kdj_buy_b1
  381. if prev_b1 is None:
  382. return False
  383. b1 = float(row["b1"])
  384. return b1 < 0 and prev_b1 < 0 and b1 < prev_b1
  385. def _glued_high_weak_rebound_subtype(self, row: pd.Series) -> str:
  386. return glued_high_weak_rebound_subtype(
  387. a1=float(row["a1"]),
  388. b1=float(row["b1"]),
  389. c1=float(row["c1"]),
  390. ql_buy=bool(row["ql_buy"]),
  391. config=self.config,
  392. )
  393. def _buy_filter_glued_high_weak_rebound(self, row: pd.Series) -> bool:
  394. return bool(self._glued_high_weak_rebound_subtype(row))
  395. def _glued_followthrough_should_queue(self, subtype: str) -> bool:
  396. return glued_followthrough_pending_allowed(
  397. subtype=subtype,
  398. config=self.config,
  399. )
  400. def _buy_filter_glued_selective_short_holding(self, row: pd.Series) -> bool:
  401. c1 = float(row["c1"])
  402. b1 = float(row["b1"])
  403. if (
  404. self.config.glued_selective_hot_c1_min > 0
  405. and c1 >= self.config.glued_selective_hot_c1_min
  406. and c1 < self.config.glued_selective_hot_c1_max
  407. and b1 >= self.config.glued_selective_hot_b1_min
  408. ):
  409. return True
  410. if (
  411. self.config.glued_selective_low_c1_max > self.config.glued_selective_low_c1_min
  412. and self.config.glued_selective_low_c1_min <= c1 < self.config.glued_selective_low_c1_max
  413. and b1 <= self.config.glued_selective_low_b1_max
  414. ):
  415. return True
  416. return False
  417. def _buy_improving(self, row: pd.Series) -> bool:
  418. prev_a1 = self.context.last_kdj_buy_a1
  419. prev_b1 = self.context.last_kdj_buy_b1
  420. if prev_a1 is None or prev_b1 is None:
  421. return True
  422. a1 = float(row["a1"])
  423. b1 = float(row["b1"])
  424. return (a1 >= prev_a1) and (b1 >= prev_b1 or b1 > 0)
  425. def _should_emit_aux_buy(self, row: pd.Series) -> bool:
  426. if not bool(row["kdj_buy"]) or not self.context.in_position:
  427. return False
  428. a1 = float(row["a1"])
  429. b1 = float(row["b1"])
  430. c1 = float(row["c1"])
  431. holding_days = self._holding_days(row.name.date())
  432. days_from_last_aux_buy = self._days_from_last_aux_buy(row.name.date())
  433. if days_from_last_aux_buy is not None and days_from_last_aux_buy <= 10:
  434. return False
  435. strong_dual_gold_reconfirm = bool(row["ql_buy"]) and a1 > 0.03 and (b1 > 0.15 or c1 > 60)
  436. early_strength_reconfirm = holding_days <= 35 and b1 > 0.24 and c1 > 45
  437. super_hot_reconfirm = holding_days >= 20 and a1 > 0.07 and c1 > 90
  438. if not (strong_dual_gold_reconfirm or early_strength_reconfirm or super_hot_reconfirm):
  439. return False
  440. if (
  441. days_from_last_aux_buy is not None
  442. and days_from_last_aux_buy <= 20
  443. and self.context.last_aux_buy_c1 is not None
  444. and c1 <= self.context.last_aux_buy_c1 + 8
  445. and b1 < 0.24
  446. ):
  447. return False
  448. return True
  449. def _should_emit_aux_sell(self, row: pd.Series) -> bool:
  450. b1 = float(row["b1"])
  451. c1 = float(row["c1"])
  452. ql_sell = bool(row["ql_sell"])
  453. kdj_sell = bool(row["kdj_sell"])
  454. days_from_last_sell = self._days_from_last_real_sell(row.name.date())
  455. days_from_last_aux_sell = self._days_from_last_aux_sell(row.name.date())
  456. current_source = "ql_sell" if ql_sell and not kdj_sell else "kdj_sell"
  457. if (
  458. kdj_sell
  459. and not ql_sell
  460. and c1 > self.config.aux_sell_high_zone_kdj_only_block_c1
  461. and b1 > self.config.aux_sell_high_zone_kdj_only_block_b1
  462. ):
  463. return False
  464. if days_from_last_sell is not None and 0 < days_from_last_sell <= self.config.post_exit_confirmation_window_days:
  465. base_emit = True
  466. elif c1 > self.config.aux_sell_high_zone_warning_c1 and (ql_sell or kdj_sell):
  467. base_emit = True
  468. elif kdj_sell and c1 > self.config.aux_sell_strong_break_c1 and b1 < self.config.aux_sell_strong_break_b1:
  469. base_emit = True
  470. else:
  471. base_emit = False
  472. if not base_emit:
  473. return False
  474. if (
  475. self.config.aux_sell_same_side_once_per_cycle
  476. and days_from_last_sell is not None
  477. and 0 < days_from_last_sell <= self.config.post_exit_confirmation_window_days
  478. ):
  479. if current_source == "ql_sell" and self.context.flat_post_exit_ql_emitted:
  480. return False
  481. if current_source == "kdj_sell" and self.context.flat_post_exit_kdj_emitted:
  482. return False
  483. if days_from_last_aux_sell is not None and days_from_last_aux_sell <= self.config.aux_sell_duplicate_cooldown_days:
  484. stronger = False
  485. if (
  486. self.context.last_aux_sell_c1 is not None
  487. and c1 < self.context.last_aux_sell_c1 - self.config.aux_sell_stronger_c1_delta
  488. ):
  489. stronger = True
  490. if (
  491. self.context.last_aux_sell_b1 is not None
  492. and b1 < self.context.last_aux_sell_b1 - self.config.aux_sell_stronger_b1_delta
  493. ):
  494. stronger = True
  495. if self.context.last_aux_sell_reason.endswith("ql_sell") and kdj_sell:
  496. stronger = True
  497. if not stronger and not (
  498. c1 > self.config.aux_sell_high_zone_warning_c1
  499. and self.context.last_aux_sell_c1 is not None
  500. and c1 > self.context.last_aux_sell_c1 + self.config.aux_sell_high_zone_rearm_c1_delta
  501. ):
  502. return False
  503. return True
  504. def _should_emit_state_aux_sell(self, row: pd.Series) -> bool:
  505. days_from_last_sell = self._days_from_last_real_sell(row.name.date())
  506. if days_from_last_sell is None:
  507. return False
  508. if not self._last_sell_reason_is("crash_protection_exit"):
  509. return False
  510. if not (0 < days_from_last_sell <= self.config.state_crash_followthrough_window_days):
  511. return False
  512. if (
  513. self._days_from_last_aux_sell(row.name.date()) is not None
  514. and self._days_from_last_aux_sell(row.name.date()) <= self.config.state_crash_followthrough_repeat_cooldown_days
  515. ):
  516. return False
  517. a1 = float(row["a1"])
  518. b1 = float(row["b1"])
  519. c1 = float(row["c1"])
  520. return (
  521. c1 < self.config.state_crash_followthrough_c1_max
  522. and a1 < self.config.state_crash_followthrough_a1_max
  523. and b1 < self.config.state_crash_followthrough_b1_max
  524. )
  525. def _special_buy_knife1(self, row: pd.Series) -> bool:
  526. c1 = float(row["c1"])
  527. if c1 >= 12:
  528. return False
  529. prev_a1 = self.context.last_kdj_buy_a1
  530. prev_b1 = self.context.last_kdj_buy_b1
  531. if prev_a1 is None or prev_b1 is None:
  532. return False
  533. a1 = float(row["a1"])
  534. b1 = float(row["b1"])
  535. if b1 <= prev_b1:
  536. return False
  537. if min(b1, prev_b1) < -0.17:
  538. return False
  539. if min(a1, prev_a1) < -0.04 and a1 <= prev_a1:
  540. return False
  541. return True
  542. def _special_buy_knife2(self, row: pd.Series) -> bool:
  543. c1 = float(row["c1"])
  544. if c1 >= 12:
  545. return False
  546. prev_b1 = self.context.last_kdj_buy_b1
  547. if prev_b1 is None:
  548. return False
  549. b1 = float(row["b1"])
  550. return b1 > -0.03 and b1 > prev_b1
  551. def _special_buy_deep_oversold_rebound(self, row: pd.Series) -> bool:
  552. a1 = float(row["a1"])
  553. b1 = float(row["b1"])
  554. c1 = float(row["c1"])
  555. return deep_oversold_base_entry(
  556. a1=a1,
  557. b1=b1,
  558. c1=c1,
  559. config=self.config,
  560. )
  561. def _deep_oversold_requires_confirmation(self, row: pd.Series, subtype: str) -> bool:
  562. return deep_oversold_requires_confirmation(
  563. subtype=subtype,
  564. ql_buy=bool(row["ql_buy"]),
  565. config=self.config,
  566. )
  567. def _pending_deep_oversold_decision(self, row: pd.Series) -> tuple[str, str]:
  568. action, reason, should_clear = evaluate_pending_confirmation(
  569. active=self.context.bridge_pending_deep_oversold_active,
  570. subtype=self.context.pending_deep_oversold_subtype,
  571. in_position=self.context.in_position,
  572. kdj_sell=bool(row["kdj_sell"]),
  573. ql_sell=bool(row["ql_sell"]),
  574. bars_waited=self.context.pending_deep_oversold_bars_waited,
  575. window_bars=self.config.deep_oversold_confirm_window_bars,
  576. ql_buy=bool(row["ql_buy"]),
  577. )
  578. if should_clear:
  579. self._clear_pending_deep_oversold()
  580. return action, reason
  581. def _pending_glued_followthrough_decision(self, row: pd.Series) -> tuple[str, str]:
  582. action, reason, should_clear = evaluate_glued_followthrough_confirmation(
  583. active=self.context.bridge_pending_glued_followthrough_active,
  584. subtype=self.context.pending_glued_followthrough_subtype,
  585. in_position=self.context.in_position,
  586. kdj_sell=bool(row["kdj_sell"]),
  587. ql_sell=bool(row["ql_sell"]),
  588. bars_waited=self.context.pending_glued_followthrough_bars_waited,
  589. ql_buy=bool(row["ql_buy"]),
  590. close=float(row["close"]),
  591. signal_close=float(self.context.pending_glued_followthrough_signal_close or 0.0),
  592. b1=float(row["b1"]),
  593. signal_b1=float(self.context.pending_glued_followthrough_b1 or 0.0),
  594. config=self.config,
  595. )
  596. if should_clear:
  597. self._clear_pending_glued_followthrough()
  598. return action, reason
  599. def _deep_oversold_selective_veto(self, row: pd.Series, subtype: str) -> bool:
  600. c1 = float(row["c1"])
  601. b1 = float(row["b1"])
  602. return deep_oversold_selective_veto(
  603. subtype=subtype,
  604. c1=c1,
  605. b1=b1,
  606. ql_buy=bool(row["ql_buy"]),
  607. config=self.config,
  608. )
  609. @staticmethod
  610. def _deep_oversold_subtype(a1: float, b1: float, c1: float) -> str:
  611. return deep_oversold_subtype(a1=a1, b1=b1, c1=c1)
  612. def _special_buy_oversold_recovery(self, row: pd.Series) -> bool:
  613. a1 = float(row["a1"])
  614. b1 = float(row["b1"])
  615. c1 = float(row["c1"])
  616. return (
  617. self.config.oversold_recovery_c1_low <= c1 < self.config.oversold_recovery_c1_high
  618. and self.config.oversold_recovery_a1_min <= a1 < self.config.oversold_recovery_a1_max
  619. and b1 > self.config.oversold_recovery_b1_min
  620. )
  621. def _special_buy_oversold_reversal_after_ql(self, row: pd.Series, prev_row: Optional[pd.Series]) -> bool:
  622. if prev_row is None or not bool(prev_row["ql_sell"]):
  623. return False
  624. a1 = float(row["a1"])
  625. b1 = float(row["b1"])
  626. c1 = float(row["c1"])
  627. if (
  628. self.config.oversold_reversal_after_ql_block_c1_low < c1 < self.config.oversold_reversal_after_ql_block_c1_high
  629. and b1 > self.config.oversold_reversal_after_ql_block_b1_min
  630. and a1 > self.config.oversold_reversal_after_ql_block_a1_min
  631. ):
  632. return False
  633. return (
  634. self.config.oversold_reversal_after_ql_entry_c1_low <= c1 < self.config.oversold_reversal_after_ql_entry_c1_high
  635. and self.config.oversold_reversal_after_ql_entry_a1_min <= a1 < self.config.oversold_reversal_after_ql_entry_a1_max
  636. and self.config.oversold_reversal_after_ql_entry_b1_min < b1 < self.config.oversold_reversal_after_ql_entry_b1_max
  637. )
  638. def _special_buy_early_crash_probe(self, row: pd.Series) -> bool:
  639. a1 = float(row["a1"])
  640. b1 = float(row["b1"])
  641. c1 = float(row["c1"])
  642. return c1 < 20 and -0.07 < a1 < -0.04 and -0.04 < b1 < 0
  643. def _special_buy_post_sell_rebound(self, row: pd.Series) -> bool:
  644. if self.context.last_kdj_sell_date is None:
  645. return False
  646. days_from_last_kdj_sell = (row.name.date() - self.context.last_kdj_sell_date).days
  647. if days_from_last_kdj_sell < 0 or days_from_last_kdj_sell > 7:
  648. return False
  649. a1 = float(row["a1"])
  650. b1 = float(row["b1"])
  651. c1 = float(row["c1"])
  652. if (
  653. c1 > self.config.post_sell_rebound_block_high_c1
  654. and a1 > self.config.post_sell_rebound_block_high_a1_min
  655. and b1 < self.config.post_sell_rebound_block_high_b1_max
  656. ):
  657. return False
  658. if (
  659. c1 < self.config.post_sell_rebound_block_low_c1
  660. and a1 > self.config.post_sell_rebound_block_low_a1_min
  661. and b1 < self.config.post_sell_rebound_block_low_b1_max
  662. ):
  663. return False
  664. if (
  665. self.config.post_sell_rebound_entry1_c1_low <= c1 < self.config.post_sell_rebound_entry1_c1_high
  666. and self.config.post_sell_rebound_entry1_a1_min <= a1 < self.config.post_sell_rebound_entry1_a1_max
  667. and self.config.post_sell_rebound_entry1_b1_low < b1 < self.config.post_sell_rebound_entry1_b1_high
  668. ):
  669. return True
  670. if (
  671. c1 < self.config.post_sell_rebound_entry2_c1_high
  672. and self.config.post_sell_rebound_entry2_a1_min <= a1 < self.config.post_sell_rebound_entry2_a1_max
  673. and self.config.post_sell_rebound_entry2_b1_low < b1 < self.config.post_sell_rebound_entry2_b1_high
  674. ):
  675. return True
  676. return False
  677. def _special_buy_post_washout_reentry(self, row: pd.Series) -> bool:
  678. if self.context.last_real_sell_date is None or self.context.prev_real_sell_c1 is None:
  679. return False
  680. days_from_last_sell = (row.name.date() - self.context.last_real_sell_date).days
  681. if days_from_last_sell < 10 or days_from_last_sell > 20:
  682. return False
  683. if self.context.prev_real_sell_c1 >= 15:
  684. return False
  685. if not self.context.bridge_last_exit_negative_a1_no_b1_recovery:
  686. return False
  687. if bool(row["kdj_buy"]) or bool(row["ql_buy"]) or bool(row["ql_sell"]) or not bool(row["kdj_sell"]):
  688. return False
  689. a1 = float(row["a1"])
  690. b1 = float(row["b1"])
  691. c1 = float(row["c1"])
  692. return 0.028 <= a1 < 0.035 and b1 > 0.30 and 50 < c1 < 60
  693. def _buy_decision(self, row: pd.Series, prev_row: Optional[pd.Series]) -> tuple[str, str]:
  694. a1 = float(row["a1"])
  695. b1 = float(row["b1"])
  696. c1 = float(row["c1"])
  697. if (
  698. not self.context.in_position
  699. and self._rule_enabled("post_washout_kdj_reentry_buy")
  700. and self._special_buy_post_washout_reentry(row)
  701. ):
  702. return "BUY", "post_washout_kdj_reentry_buy"
  703. has_buy_signal = bool(row["kdj_buy"] or row["ql_buy"])
  704. if not has_buy_signal:
  705. return "NONE", ""
  706. dual_gold = bool(row["ql_buy"])
  707. if self.context.in_position:
  708. if self._should_emit_aux_buy(row):
  709. return "AUX_BUY", "bullish_signal_while_holding"
  710. return "NONE", ""
  711. pending_action, pending_reason = self._pending_deep_oversold_decision(row)
  712. if pending_action != "NONE":
  713. return pending_action, pending_reason
  714. pending_action, pending_reason = self._pending_glued_followthrough_decision(row)
  715. if pending_action != "NONE":
  716. return pending_action, pending_reason
  717. if bool(row["ql_buy"]) and not bool(row["kdj_buy"]):
  718. if allow_predictive_error_reentry(
  719. enabled=self._rule_enabled("predictive_error_reentry_buy"),
  720. last_exit_predictive_break=self.context.bridge_last_exit_predictive_break,
  721. last_real_sell_date=self.context.last_real_sell_date,
  722. row_date=row.name.date(),
  723. a1=a1,
  724. b1=b1,
  725. c1=c1,
  726. ):
  727. return "BUY", "predictive_error_reentry_buy"
  728. if (
  729. self._rule_enabled("hot_exit_reentry_buy")
  730. and
  731. self.context.prev_real_sell_c1 is not None
  732. and self.context.prev_real_sell_c1 > 80
  733. and self.context.last_real_sell_date is not None
  734. and (row.name.date() - self.context.last_real_sell_date).days <= 10
  735. and c1 > 80
  736. and -0.02 < a1 < 0.03
  737. and b1 > -0.03
  738. ):
  739. return "BUY", "hot_exit_reentry_buy"
  740. return "NONE", ""
  741. if self._rule_enabled("early_crash_probe_buy") and self._special_buy_early_crash_probe(row):
  742. return "BUY", "early_crash_probe_buy"
  743. if self._rule_enabled("deep_oversold_rebound_buy") and self._special_buy_deep_oversold_rebound(row):
  744. subtype = self._deep_oversold_subtype(a1, b1, c1)
  745. if subtype == "positive_b1_rebound" and self.config.deep_oversold_block_positive_b1_rebound:
  746. pass
  747. elif (
  748. subtype == "shallow_false_start"
  749. and not bool(row["ql_buy"])
  750. and self.config.deep_oversold_block_shallow_false_start_without_ql
  751. ):
  752. pass
  753. elif subtype == "shallow_false_start" and bool(row["ql_buy"]) and self.config.deep_oversold_shallow_ql_fallback:
  754. pass
  755. elif (
  756. subtype == "positive_b1_rebound"
  757. and a1 > self.config.deep_oversold_positive_b1_fallback_a1_min
  758. ):
  759. pass
  760. elif self._deep_oversold_selective_veto(row, subtype):
  761. pass
  762. elif self._deep_oversold_requires_confirmation(row, subtype):
  763. if not self.context.bridge_pending_deep_oversold_active:
  764. self._queue_pending_deep_oversold(row, subtype)
  765. else:
  766. return "BUY", f"deep_oversold_rebound_buy:{subtype}"
  767. if self._rule_enabled("oversold_recovery_buy") and self._special_buy_oversold_recovery(row):
  768. return "BUY", "oversold_recovery_buy"
  769. if self._rule_enabled("post_sell_rebound_buy") and self._special_buy_post_sell_rebound(row):
  770. return "BUY", "post_sell_rebound_buy"
  771. if (
  772. self._rule_enabled("oversold_reversal_after_ql_buy")
  773. and self._special_buy_oversold_reversal_after_ql(row, prev_row)
  774. ):
  775. return "BUY", "oversold_reversal_after_ql_buy"
  776. if self._special_buy_knife1(row):
  777. return "BUY", "knife_catch_1"
  778. if self._special_buy_knife2(row):
  779. return "BUY", "knife_catch_2"
  780. if self._is_big_negative(a1) and not dual_gold:
  781. return "BLOCK", "buy_block_a1_too_negative"
  782. if self._is_b1_hard_negative(b1):
  783. return "BLOCK", "buy_block_b1_too_negative"
  784. if self._buy_filter_early_downtrend(row, prev_row):
  785. return "BLOCK", "buy_block_early_downtrend"
  786. if self._buy_filter_high_zone_after_hot_exit(row):
  787. return "BLOCK", "buy_block_c1_still_over_80_after_hot_exit"
  788. if self._is_glued(a1):
  789. if self._buy_filter_glued_b1_descending(row):
  790. return "BLOCK", "buy_block_glued_b1_still_descending"
  791. glued_block_subtype = self._glued_high_weak_rebound_subtype(row)
  792. if glued_block_subtype:
  793. if (
  794. self._glued_followthrough_should_queue(glued_block_subtype)
  795. and not self.context.bridge_pending_glued_followthrough_active
  796. ):
  797. self._queue_pending_glued_followthrough(row, glued_block_subtype)
  798. return "BLOCK", "buy_block_glued_high_weak_rebound"
  799. if self._buy_filter_glued_selective_short_holding(row):
  800. return "BLOCK", "buy_block_glued_selective_short_holding"
  801. if self._rule_enabled("glued_buy"):
  802. return "BUY", "glued_buy"
  803. return "BLOCK", "buy_block_glued_rule_disabled"
  804. if (
  805. not dual_gold
  806. and a1 > 0
  807. and b1 > 0
  808. and self._buy_improving(row)
  809. and self._rule_enabled("non_glued_positive_expansion_buy")
  810. ):
  811. return "BUY", "non_glued_positive_expansion_buy"
  812. if dual_gold and c1 > 18 and c1 < 20 and a1 > -0.05 and b1 < -0.09:
  813. return "BLOCK", "buy_block_dual_gold_false_rebound"
  814. if dual_gold and self._buy_improving(row) and self._rule_enabled("dual_gold_resonance_buy"):
  815. return "BUY", "dual_gold_resonance_buy"
  816. return "BLOCK", "buy_block_base_conditions_not_met"
  817. def _sell_decision(self, row: pd.Series, prev_row: Optional[pd.Series]) -> tuple[str, str]:
  818. a1 = float(row["a1"])
  819. b1 = float(row["b1"])
  820. c1 = float(row["c1"])
  821. has_sell_signal = bool(row["kdj_sell"] or row["ql_sell"])
  822. source = "ql_sell" if bool(row["ql_sell"]) and not bool(row["kdj_sell"]) else "kdj_sell"
  823. if not has_sell_signal:
  824. source = "state"
  825. if not self.context.in_position:
  826. if has_sell_signal and self._should_emit_aux_sell(row):
  827. return "AUX_SELL", f"bearish_signal_after_exit:{source}"
  828. if not has_sell_signal and self._should_emit_state_aux_sell(row):
  829. return "AUX_SELL", "bearish_signal_after_exit:state_crash_followthrough"
  830. return "NONE", ""
  831. if allow_predictive_b1_break_short_exit(
  832. enabled=self._rule_enabled("predictive_b1_break_exit"),
  833. has_sell_signal=has_sell_signal,
  834. entry_is_glued=self._entry_reason_is("glued_buy"),
  835. holding_days=self._holding_days(row.name.date()),
  836. a1=a1,
  837. b1=b1,
  838. c1=c1,
  839. config=self.config,
  840. ):
  841. return "SELL", "predictive_b1_break_exit"
  842. if allow_predictive_b1_break_long_exit(
  843. enabled=self._rule_enabled("predictive_b1_break_exit"),
  844. has_sell_signal=has_sell_signal,
  845. entry_is_glued=self._entry_reason_is("glued_buy"),
  846. holding_days=self._holding_days(row.name.date()),
  847. max_c1_since_entry=self.context.max_c1_since_entry,
  848. max_a1_since_entry=self.context.max_a1_since_entry,
  849. max_b1_since_entry=self.context.max_b1_since_entry,
  850. last_ql_sell_date=self.context.last_ql_sell_date,
  851. row_date=row.name.date(),
  852. a1=a1,
  853. b1=b1,
  854. c1=c1,
  855. config=self.config,
  856. ):
  857. return "SELL", "predictive_b1_break_exit"
  858. if (
  859. not has_sell_signal
  860. and self._holding_days(row.name.date()) <= 2
  861. and abs(a1) < 0.01
  862. and -0.05 < b1 < 0
  863. and 45 < c1 < 55
  864. ):
  865. return "SELL", "early_failed_rebound_exit"
  866. if (
  867. bool(row["ql_sell"])
  868. and not bool(row["kdj_sell"])
  869. and 14 <= c1 < 30
  870. and -0.03 <= a1 < 0
  871. and -0.05 < b1 < 0
  872. ):
  873. return "HOLD", "low_zone_wait_kdj_confirmation"
  874. if (
  875. self._is_big_negative(a1)
  876. and bool(row["ql_sell"])
  877. and not bool(row["kdj_sell"])
  878. and c1 < 12
  879. and self._holding_days(row.name.date()) <= 2
  880. and b1 > -0.10
  881. ):
  882. return "HOLD", "deep_oversold_wait_kdj_confirmation"
  883. if (self._is_big_negative(a1) or self._is_b1_hard_negative(b1)) and (has_sell_signal or b1 < -0.12):
  884. return "SELL", f"hard_exit:{source}"
  885. if (
  886. self.context.c1_over_80_seen
  887. and self.context.max_a1_since_entry > 0.05
  888. and a1 < 0.03
  889. and b1 < -0.08
  890. ):
  891. return "SELL", "crash_protection_exit"
  892. if not has_sell_signal:
  893. if (
  894. self.context.last_ql_sell_date is not None
  895. and 0 <= (row.name.date() - self.context.last_ql_sell_date).days <= 7
  896. and self.context.max_c1_since_entry < 80
  897. and 75 < c1 < 80
  898. and a1 > 0.025
  899. and b1 < 0.14
  900. ):
  901. return "SELL", "post_ql_decay_exit"
  902. if (
  903. self.context.last_kdj_sell_date is not None
  904. and self.context.last_ql_sell_date is not None
  905. and 0 <= (row.name.date() - self.context.last_kdj_sell_date).days <= 3
  906. and 0 <= (row.name.date() - self.context.last_ql_sell_date).days <= 3
  907. and 55 < c1 < 65
  908. and 0.015 < a1 < 0.025
  909. and 0.17 < b1 < 0.19
  910. ):
  911. return "SELL", "post_dual_sell_decay_exit"
  912. if (
  913. self.context.last_ql_sell_date is not None
  914. and 0 <= (row.name.date() - self.context.last_ql_sell_date).days <= 7
  915. and self._entry_reason_is("glued_buy")
  916. and 84 < c1 < 86.5
  917. and a1 < 0.028
  918. and b1 < 0
  919. and self.context.max_b1_since_entry < 0.14
  920. ):
  921. return "SELL", "high_zone_post_ql_fade_exit"
  922. if (
  923. self._entry_reason_is("deep_oversold_rebound_buy", "oversold_reversal_after_ql_buy")
  924. and bool(row["ql_buy"])
  925. and self._holding_days(row.name.date()) >= 10
  926. and c1 < 15
  927. and a1 > -0.02
  928. and b1 > 0
  929. ):
  930. return "SELL", "oversold_rebound_take_profit"
  931. return "NONE", ""
  932. high_regime_decision = self._high_regime_exit_decision(row, source)
  933. if high_regime_decision is not None:
  934. return high_regime_decision
  935. ql_only_decision = self._ql_only_take_profit_exit(row)
  936. if ql_only_decision is not None:
  937. return ql_only_decision
  938. if (
  939. bool(row["ql_sell"])
  940. and not bool(row["kdj_sell"])
  941. and self._entry_reason_is("glued_buy")
  942. and self._holding_days(row.name.date()) <= 7
  943. and 60 < c1 < 72
  944. and abs(a1) < 0.01
  945. and -0.06 < b1 < 0
  946. and self.context.max_c1_since_entry < 75
  947. ):
  948. return "HOLD", "glued_mid_zone_wait_kdj_confirmation"
  949. if (
  950. bool(row["ql_sell"])
  951. and not bool(row["kdj_sell"])
  952. and self._entry_reason_is("deep_oversold_rebound_buy", "oversold_reversal_after_ql_buy")
  953. and self._holding_days(row.name.date()) <= 20
  954. and c1 < 18
  955. and -0.03 <= a1 < -0.018
  956. and 0 < b1 < 0.08
  957. ):
  958. return "HOLD", "oversold_low_zone_wait_kdj_confirmation"
  959. if self.context.entry_a1 is not None and self.context.entry_b1 is not None and not self.context.first_exit_checked:
  960. entry_a1 = self.context.entry_a1
  961. entry_b1 = self.context.entry_b1
  962. if (
  963. entry_a1 < 0
  964. and entry_b1 < 0
  965. and self.context.max_c1_since_entry < 80
  966. and 0.02 < a1 < 0.028
  967. and b1 < 0.17
  968. and not self._is_glued(a1)
  969. and self._rule_enabled("knife_take_profit_1")
  970. ):
  971. return "SELL", "knife_take_profit_1"
  972. if self._is_glued(a1) and self.context.max_c1_since_entry < 80:
  973. if (
  974. bool(row["ql_sell"])
  975. and not bool(row["kdj_sell"])
  976. and self._entry_reason_is("glued_buy")
  977. and self._holding_days(row.name.date()) >= 40
  978. and 70 < c1 < 75
  979. and 0 < a1 < 0.02
  980. and 0 <= b1 < 0.02
  981. and self.context.max_a1_since_entry > 0.15
  982. and self.context.max_b1_since_entry > 0.30
  983. ):
  984. return "HOLD", "long_glued_wait_predictive_break"
  985. if (
  986. bool(row["ql_sell"])
  987. and not bool(row["kdj_sell"])
  988. and self._entry_reason_is("glued_buy")
  989. and self._holding_days(row.name.date()) >= 15
  990. and 60 < c1 < 66
  991. and a1 > 0.01
  992. and b1 > 0.12
  993. ):
  994. return "HOLD", "knife_take_profit_2_glued_wait_followthrough"
  995. if should_hold_glued_followthrough_reentry_kdj_only(
  996. enabled=self.config.glued_followthrough_exit_hold_kdj_only_enabled,
  997. entry_reason_code=self.context.entry_reason_code,
  998. kdj_sell=bool(row["kdj_sell"]),
  999. ql_sell=bool(row["ql_sell"]),
  1000. holding_days=self._holding_days(row.name.date()),
  1001. a1=a1,
  1002. b1=b1,
  1003. c1=c1,
  1004. config=self.config,
  1005. ):
  1006. return "HOLD", "glued_followthrough_reentry_wait_ql_confirmation"
  1007. if b1 < 0.17 and self._rule_enabled("knife_take_profit_2_glued"):
  1008. return "SELL", "knife_take_profit_2_glued"
  1009. if bool(row["ql_sell"]) and self.config.enable_knife_take_profit_2_wait_ql:
  1010. return "SELL", "knife_take_profit_2_wait_ql_s"
  1011. weakened_from_peak = (
  1012. self.context.c1_over_80_seen
  1013. and self.context.combo_big_cycle_count >= 2
  1014. and self.context.max_a1_since_entry > 0.04
  1015. and a1 < self.context.max_a1_since_entry - 0.008
  1016. and b1 < min(0.17, self.context.max_b1_since_entry - 0.04)
  1017. )
  1018. if weakened_from_peak and a1 < 0.05:
  1019. if not bool(row["kdj_sell"]):
  1020. return "HOLD", f"high_zone_wait_kdj_sell:{source}"
  1021. return "SELL", "good_to_take_profit_1"
  1022. large_regime_confirmed = self.context.a1_big_cycle_count >= 4 or self.context.a1_big_pos_count >= 4
  1023. if (
  1024. large_regime_confirmed
  1025. and self.context.b1_negative_sell_count >= 2
  1026. and self.context.c1_over_80_seen
  1027. and a1 < 0.05
  1028. ):
  1029. return "SELL", f"good_to_take_profit_2:{source}"
  1030. if a1 > 0.05:
  1031. return "HOLD", f"hold_a1_above_5pct:{source}"
  1032. if self._is_glued(a1):
  1033. if large_regime_confirmed and self.context.b1_negative_sell_count < 2:
  1034. return "HOLD", f"hold_glued_large_regime_wait_b1_negative_count:{source}"
  1035. return "SELL", f"glued_exit:{source}"
  1036. prev_a1 = float(prev_row["a1"]) if prev_row is not None else a1
  1037. prev_c1 = float(prev_row["c1"]) if prev_row is not None else c1
  1038. a1_declining = a1 < prev_a1
  1039. c1_declining = c1 < prev_c1
  1040. if 0.028 < a1 < 0.05:
  1041. if (
  1042. bool(row["kdj_sell"])
  1043. and not bool(row["ql_sell"])
  1044. and c1 > 80
  1045. and b1 > 0.13
  1046. and self.context.combo_big_cycle_count >= 2
  1047. ):
  1048. return "HOLD", "mid_hot_wait_ql_confirmation"
  1049. if (
  1050. bool(row["kdj_sell"])
  1051. and not bool(row["ql_sell"])
  1052. and c1 > 84
  1053. and b1 < 0.11
  1054. and self.context.max_a1_since_entry < 0.04
  1055. and self.context.max_b1_since_entry > 0.18
  1056. ):
  1057. return "SELL", "medium_hot_take_profit"
  1058. if large_regime_confirmed and self.context.b1_negative_sell_count >= 2:
  1059. return "SELL", f"good_to_take_profit_2:{source}"
  1060. if self.context.combo_big_cycle_count >= 2 and a1 < 0.04 and a1_declining and b1 < 0.17:
  1061. return "SELL", "good_to_take_profit_1"
  1062. if self.context.b1_negative_sell_count >= 2 and c1_declining:
  1063. return "SELL", f"good_to_take_profit_2:{source}"
  1064. return "HOLD", f"hold_mid_positive_a1:{source}"
  1065. if -0.04 < a1 < -0.02:
  1066. if (
  1067. bool(row["kdj_sell"])
  1068. and self._entry_reason_is("dual_gold_resonance_buy")
  1069. and c1 < 20
  1070. and b1 > 0
  1071. ):
  1072. return "SELL", f"low_zone_dual_gold_exit:{source}"
  1073. if b1 <= 0:
  1074. return "SELL", f"negative_a1_no_b1_recovery:{source}"
  1075. if b1 < 0.17 and a1_declining:
  1076. return "SELL", f"negative_a1_b1_not_strong:{source}"
  1077. return "HOLD", f"hold_negative_a1_wait_confirmation:{source}"
  1078. if 0.02 < a1 < 0.028:
  1079. if (
  1080. bool(row["kdj_sell"])
  1081. and not bool(row["ql_sell"])
  1082. and c1 > 60
  1083. and b1 > 0.20
  1084. and self.context.max_c1_since_entry < 80
  1085. and self.context.max_a1_since_entry < 0.025
  1086. and self.context.max_b1_since_entry < 0.25
  1087. and self._holding_days(row.name.date()) <= 20
  1088. ):
  1089. return "SELL", "early_positive_take_profit"
  1090. if (
  1091. bool(row["kdj_sell"])
  1092. and not bool(row["ql_sell"])
  1093. and c1 > 80
  1094. and b1 > 0.08
  1095. and self.context.max_b1_since_entry > 0.15
  1096. ):
  1097. return "HOLD", f"wait_ql_confirmation_small_positive:{source}"
  1098. if b1 < 0.17 and a1_declining:
  1099. return "SELL", f"small_positive_a1_declining:{source}"
  1100. return "HOLD", f"hold_small_positive_a1:{source}"
  1101. if (
  1102. bool(row["ql_sell"])
  1103. and not bool(row["kdj_sell"])
  1104. and 55 < c1 < 75
  1105. and abs(a1) < 0.01
  1106. and -0.05 < b1 < 0
  1107. ):
  1108. return "HOLD", "mid_zone_wait_kdj_confirmation"
  1109. return "SELL", f"default_exit:{source}"
  1110. def _post_real_buy(self, row: pd.Series, reason: str) -> None:
  1111. self._clear_pending_deep_oversold()
  1112. self._clear_pending_glued_followthrough()
  1113. entry_meta = classify_entry_reason(reason)
  1114. self.context.in_position = True
  1115. self.context.entry_date = row.name.date()
  1116. self.context.entry_price = float(row["close"])
  1117. self.context.entry_a1 = float(row["a1"])
  1118. self.context.entry_b1 = float(row["b1"])
  1119. self.context.entry_c1 = float(row["c1"])
  1120. self.context.entry_reason = reason
  1121. self.context.entry_reason_layer = entry_meta.layer.value
  1122. self.context.entry_reason_family = entry_meta.family.value
  1123. self.context.entry_reason_code = entry_meta.code
  1124. self.context.first_exit_checked = False
  1125. self.context.c1_over_80_seen = float(row["c1"]) > 80
  1126. self.context.a1_big_pos_count = 1 if self._is_big_positive(float(row["a1"])) else 0
  1127. self.context.b1_big_pos_count = 1 if self._is_b1_strong_positive(float(row["b1"])) else 0
  1128. self.context.b1_negative_sell_count = 0
  1129. self.context.max_a1_since_entry = float(row["a1"])
  1130. self.context.max_b1_since_entry = float(row["b1"])
  1131. self.context.max_c1_since_entry = float(row["c1"])
  1132. self.context.a1_big_cycle_count = 1 if self._is_big_positive(float(row["a1"])) else 0
  1133. self.context.b1_big_cycle_count = 1 if self._is_b1_strong_positive(float(row["b1"])) else 0
  1134. 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
  1135. self.context.sell_signal_count = 0
  1136. self.context.kdj_sell_signal_count = 0
  1137. self.context.ql_sell_signal_count = 0
  1138. self.context.prev_a1_big_flag = self._is_big_positive(float(row["a1"]))
  1139. self.context.prev_b1_big_flag = self._is_b1_strong_positive(float(row["b1"]))
  1140. self.context.prev_combo_big_flag = self.context.prev_a1_big_flag and self.context.prev_b1_big_flag
  1141. def _post_real_sell(self, row: pd.Series, reason: str) -> None:
  1142. self._clear_pending_deep_oversold()
  1143. self._clear_pending_glued_followthrough()
  1144. sell_meta = classify_exit_reason(reason)
  1145. if self.context.a1_big_pos_count >= 4:
  1146. self.context.last_big_regime_exit_date = row.name.date()
  1147. self.context.kdj_cross_count_since_big_regime_exit = 0
  1148. self.context.prev_real_sell_c1 = float(row["c1"])
  1149. self.context.last_real_sell_date = row.name.date()
  1150. self.context.last_real_sell_reason = reason
  1151. self.context.last_real_sell_reason_layer = sell_meta.layer.value
  1152. self.context.last_real_sell_reason_family = sell_meta.family.value
  1153. self.context.last_real_sell_reason_code = sell_meta.code
  1154. self.context.bridge_last_exit_predictive_break = sell_meta.code == "exit_predictive_b1_break"
  1155. self.context.bridge_last_exit_negative_a1_no_b1_recovery = sell_meta.code == "exit_negative_a1_recovery"
  1156. self.context.in_position = False
  1157. self.context.entry_date = None
  1158. self.context.entry_price = None
  1159. self.context.entry_a1 = None
  1160. self.context.entry_b1 = None
  1161. self.context.entry_c1 = None
  1162. self.context.entry_reason = ""
  1163. self.context.entry_reason_layer = "unknown"
  1164. self.context.entry_reason_family = "unknown"
  1165. self.context.entry_reason_code = ""
  1166. self.context.first_exit_checked = False
  1167. self.context.c1_over_80_seen = False
  1168. self.context.a1_big_pos_count = 0
  1169. self.context.b1_big_pos_count = 0
  1170. self.context.b1_negative_sell_count = 0
  1171. self.context.max_a1_since_entry = -999.0
  1172. self.context.max_b1_since_entry = -999.0
  1173. self.context.max_c1_since_entry = -999.0
  1174. self.context.a1_big_cycle_count = 0
  1175. self.context.b1_big_cycle_count = 0
  1176. self.context.combo_big_cycle_count = 0
  1177. self.context.sell_signal_count = 0
  1178. self.context.kdj_sell_signal_count = 0
  1179. self.context.ql_sell_signal_count = 0
  1180. self.context.flat_post_exit_ql_emitted = False
  1181. self.context.flat_post_exit_kdj_emitted = False
  1182. self.context.prev_a1_big_flag = False
  1183. self.context.prev_b1_big_flag = False
  1184. self.context.prev_combo_big_flag = False
  1185. def _post_aux_buy(self, row: pd.Series) -> None:
  1186. self.context.last_aux_buy_date = row.name.date()
  1187. self.context.last_aux_buy_c1 = float(row["c1"])
  1188. def _post_aux_sell(self, row: pd.Series, reason: str) -> None:
  1189. self.context.last_aux_sell_date = row.name.date()
  1190. self.context.last_aux_sell_c1 = float(row["c1"])
  1191. self.context.last_aux_sell_b1 = float(row["b1"])
  1192. self.context.last_aux_sell_reason = reason
  1193. days_from_last_sell = self._days_from_last_real_sell(row.name.date())
  1194. if days_from_last_sell is not None and 0 < days_from_last_sell <= self.config.post_exit_confirmation_window_days:
  1195. if reason.endswith("ql_sell"):
  1196. self.context.flat_post_exit_ql_emitted = True
  1197. elif reason.endswith("kdj_sell"):
  1198. self.context.flat_post_exit_kdj_emitted = True
  1199. def run(self, df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
  1200. from dragon_execution_runtime import run_compat_execution
  1201. return run_compat_execution(self, df)
  1202. def run_with_layered_engine(df: pd.DataFrame, config: Optional[StrategyConfig] = None) -> tuple[pd.DataFrame, pd.DataFrame]:
  1203. from dragon_rule_engine_v2 import LayeredDragonRuleEngine
  1204. engine = LayeredDragonRuleEngine(config=config)
  1205. return engine.run(df)