Pārlūkot izejas kodu

Isolate predictive and deep-oversold logic via OpenSpec

erwin 1 mēnesi atpakaļ
vecāks
revīzija
857a7fa578

+ 15 - 0
research/dragon/v2/MEMORY.md

@@ -1074,3 +1074,18 @@
 - acceptance report published at:
 - `openspec/changes/rc1-layered-governed-optimization/acceptance-summary.md`
 - tasks for this OpenSpec change are now fully checked in `tasks.md`.
+- Then continued with a second OpenSpec change focused on physical isolation of weak-family predicates:
+- `openspec/changes/predictive-deepoversold-physical-isolation/`
+- Added dedicated modules:
+- `dragon_bridge_predictive_break.py`
+- `dragon_deep_oversold_classifier.py`
+- `dragon_deep_oversold_confirmation.py`
+- `dragon_strategy.py` now delegates predictive/deep-oversold predicate checks to those modules (compatibility-preserving).
+- Added module tests:
+- `tests/test_predictive_bridge_module.py`
+- `tests/test_deep_oversold_modules.py`
+- Validation remains green:
+- RC1 golden summary and core hashes unchanged,
+- attribution mapping still has no unknown reason mapping,
+- full test suite reached `14` passing tests,
+- daily signal pipeline smoke still passes.

+ 85 - 0
research/dragon/v2/dragon_bridge_predictive_break.py

@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+from datetime import date
+from typing import Optional
+
+from dragon_strategy_config import StrategyConfig
+
+
+def allow_predictive_error_reentry(
+    *,
+    enabled: bool,
+    last_exit_predictive_break: bool,
+    last_real_sell_date: Optional[date],
+    row_date: date,
+    a1: float,
+    b1: float,
+    c1: float,
+) -> bool:
+    if not enabled:
+        return False
+    if not last_exit_predictive_break:
+        return False
+    if last_real_sell_date is None:
+        return False
+    return (
+        (row_date - last_real_sell_date).days <= 3
+        and -0.02 < a1 < 0.01
+        and b1 > -0.16
+        and c1 > 50
+    )
+
+
+def allow_predictive_b1_break_short_exit(
+    *,
+    enabled: bool,
+    has_sell_signal: bool,
+    entry_is_glued: bool,
+    holding_days: int,
+    a1: float,
+    b1: float,
+    c1: float,
+    config: StrategyConfig,
+) -> bool:
+    if not enabled or has_sell_signal or not entry_is_glued:
+        return False
+    return (
+        holding_days <= config.predictive_b1_break_short_holding_days_max
+        and config.predictive_b1_break_short_a1_min < a1 < config.predictive_b1_break_short_a1_max
+        and b1 < config.predictive_b1_break_short_b1_max
+        and config.predictive_b1_break_short_c1_low < c1 < config.predictive_b1_break_short_c1_high
+    )
+
+
+def allow_predictive_b1_break_long_exit(
+    *,
+    enabled: bool,
+    has_sell_signal: bool,
+    entry_is_glued: bool,
+    holding_days: int,
+    max_c1_since_entry: float,
+    max_a1_since_entry: float,
+    max_b1_since_entry: float,
+    last_ql_sell_date: Optional[date],
+    row_date: date,
+    a1: float,
+    b1: float,
+    c1: float,
+    config: StrategyConfig,
+) -> bool:
+    if not enabled or has_sell_signal or not entry_is_glued:
+        return False
+    if last_ql_sell_date is None:
+        return False
+    ql_days = (row_date - last_ql_sell_date).days
+    return (
+        holding_days >= config.predictive_b1_break_long_holding_days_min
+        and max_c1_since_entry < config.predictive_b1_break_long_max_c1
+        and max_a1_since_entry > config.predictive_b1_break_long_max_a1
+        and max_b1_since_entry > config.predictive_b1_break_long_max_b1
+        and 0 < ql_days <= config.predictive_b1_break_long_ql_days_max
+        and config.predictive_b1_break_long_a1_min < a1 < config.predictive_b1_break_long_a1_max
+        and b1 < config.predictive_b1_break_long_b1_max
+        and config.predictive_b1_break_long_c1_low < c1 < config.predictive_b1_break_long_c1_high
+    )
+

+ 82 - 0
research/dragon/v2/dragon_deep_oversold_classifier.py

@@ -0,0 +1,82 @@
+from __future__ import annotations
+
+from dragon_strategy_config import StrategyConfig
+
+
+def deep_oversold_base_entry(*, a1: float, b1: float, c1: float, config: StrategyConfig) -> bool:
+    if (
+        config.deep_oversold_filter1_c1_low < c1 < config.deep_oversold_filter1_c1_high
+        and a1 > config.deep_oversold_filter1_a1_min
+        and b1 < config.deep_oversold_filter1_b1_max
+    ):
+        return False
+    if (
+        config.deep_oversold_filter2_c1_low < c1 < config.deep_oversold_filter2_c1_high
+        and a1 > config.deep_oversold_filter2_a1_min
+        and b1 > config.deep_oversold_filter2_b1_min
+    ):
+        return False
+    return (
+        c1 < config.deep_oversold_entry_c1_max
+        and a1 > config.deep_oversold_entry_a1_min
+        and b1 > config.deep_oversold_entry_b1_min
+    )
+
+
+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"
+
+
+def deep_oversold_requires_confirmation(
+    *,
+    subtype: str,
+    ql_buy: bool,
+    config: StrategyConfig,
+) -> bool:
+    if not config.deep_oversold_confirm_weak_with_ql:
+        return False
+    if subtype not in {"positive_b1_rebound", "shallow_false_start"}:
+        return False
+    if ql_buy:
+        return False
+    return True
+
+
+def deep_oversold_selective_veto(
+    *,
+    subtype: str,
+    c1: float,
+    b1: float,
+    ql_buy: bool,
+    config: StrategyConfig,
+) -> bool:
+    if (
+        subtype == "positive_b1_rebound"
+        and config.deep_oversold_selective_positive_b1_c1_max > 0
+        and c1 < config.deep_oversold_selective_positive_b1_c1_max
+    ):
+        return True
+    if (
+        subtype == "shallow_false_start"
+        and config.deep_oversold_selective_shallow_c1_min > 0
+        and c1 >= config.deep_oversold_selective_shallow_c1_min
+        and b1 > config.deep_oversold_selective_shallow_b1_min
+    ):
+        return True
+    if (
+        subtype == "mixed_oversold"
+        and config.deep_oversold_selective_mixed_c1_max > 0
+        and c1 < config.deep_oversold_selective_mixed_c1_max
+        and (not config.deep_oversold_selective_mixed_require_no_ql or not ql_buy)
+    ):
+        return True
+    return False
+

+ 41 - 0
research/dragon/v2/dragon_deep_oversold_confirmation.py

@@ -0,0 +1,41 @@
+from __future__ import annotations
+
+from datetime import date
+from typing import Optional
+
+
+def should_increment_pending(
+    *,
+    active: bool,
+    subtype: str,
+    origin_date: Optional[date],
+    row_date: date,
+) -> bool:
+    if not active or not subtype or origin_date is None:
+        return False
+    return row_date != origin_date
+
+
+def evaluate_pending_confirmation(
+    *,
+    active: bool,
+    subtype: str,
+    in_position: bool,
+    kdj_sell: bool,
+    ql_sell: bool,
+    bars_waited: int,
+    window_bars: int,
+    ql_buy: bool,
+) -> tuple[str, str, bool]:
+    if not active or not subtype:
+        return "NONE", "", False
+    if in_position:
+        return "NONE", "", True
+    if kdj_sell or ql_sell:
+        return "NONE", "", True
+    if bars_waited > window_bars:
+        return "NONE", "", True
+    if bars_waited >= 1 and ql_buy:
+        return "BUY", f"deep_oversold_rebound_buy:confirmed_{subtype}", True
+    return "NONE", "", False
+

+ 1 - 1
research/dragon/v2/dragon_rc1_golden_manifest.json

@@ -1,5 +1,5 @@
 {
-  "generated_at": "2026-04-09T02:41:07",
+  "generated_at": "2026-04-09T09:14:52",
   "release_version": "RC1",
   "branch": "alpha_first_glued_refined_hot_cap",
   "evaluation_window": {

+ 81 - 113
research/dragon/v2/dragon_strategy.py

@@ -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"
 

+ 16 - 0
research/dragon/v2/memory/2026-04-09.md

@@ -75,3 +75,19 @@
 - `openspec/changes/rc1-layered-governed-optimization/acceptance-summary.md`
 - Updated OpenSpec task status to fully checked in:
 - `openspec/changes/rc1-layered-governed-optimization/tasks.md`
+- Continued OpenSpec推进 with a new change pack:
+- `openspec/changes/predictive-deepoversold-physical-isolation/`
+- Added proposal/design/tasks/specs plus acceptance summary for this change.
+- Implemented physical isolation modules:
+- `dragon_bridge_predictive_break.py`
+- `dragon_deep_oversold_classifier.py`
+- `dragon_deep_oversold_confirmation.py`
+- Replaced predictive/deep-oversold inline predicates in `dragon_strategy.py` with module calls (compatibility path preserved).
+- Added module-level tests:
+- `tests/test_predictive_bridge_module.py`
+- `tests/test_deep_oversold_modules.py`
+- Validation completed:
+- golden baseline unchanged (`91` trades, core hashes unchanged),
+- layer attribution remains `no unknown reason mapping`,
+- full test suite now `14` tests passed,
+- daily signal pipeline smoke (`--as-of 2026-04-08`) passed.

+ 2 - 0
research/dragon/v2/openspec/changes/predictive-deepoversold-physical-isolation/.openspec.yaml

@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-04-09

+ 46 - 0
research/dragon/v2/openspec/changes/predictive-deepoversold-physical-isolation/acceptance-summary.md

@@ -0,0 +1,46 @@
+# Predictive + Deep Oversold Physical Isolation - Acceptance Summary
+
+Date: 2026-04-09
+
+## 1) Scope Executed
+
+- Added predictive bridge module:
+  - `dragon_bridge_predictive_break.py`
+- Added deep-oversold modules:
+  - `dragon_deep_oversold_classifier.py`
+  - `dragon_deep_oversold_confirmation.py`
+- Replaced inline predictive/deep-oversold predicates in `dragon_strategy.py` with module calls.
+
+## 2) Regression And Guardrails
+
+- Golden baseline regenerated successfully.
+- RC1 summary remains:
+  - `trade_count = 91`
+  - `event_count = 272`
+  - `win_rate = 52.75%`
+  - `avg_return = 3.42%`
+- Core hashes unchanged:
+  - events: `8965d1b539a998d7d0aff04432aa2a47cf30ee40df013b9d8b7eb66a3d50a331`
+  - trades: `1298be56b0898266b0b854d62a979c00c20b01629393c82bb8c804faf852cb97`
+- Rule attribution mapping remains clean:
+  - `dragon_rule_layer_attribution.md` reports `no unknown reason mapping`.
+
+## 3) Tests
+
+Added module-focused tests:
+
+- `tests/test_predictive_bridge_module.py`
+- `tests/test_deep_oversold_modules.py`
+
+Validation run:
+
+- `py -3 -m unittest discover -s tests -v`
+- result: `14` tests passed (`OK`).
+
+## 4) Pipeline Smoke
+
+- `py -3 dragon_daily_signal_pipeline.py --as-of 2026-04-08` completed successfully.
+
+## 5) OpenSpec Task Status
+
+- `openspec/changes/predictive-deepoversold-physical-isolation/tasks.md` now has all tasks marked complete.

+ 44 - 0
research/dragon/v2/openspec/changes/predictive-deepoversold-physical-isolation/design.md

@@ -0,0 +1,44 @@
+## Context
+
+`dragon_strategy.py` has already been converted into a compatibility-first facade with layered orchestration support, but weak-family execution predicates remain embedded in the monolith. We now isolate these families physically while keeping RC1 path stability.
+
+## Goals / Non-Goals
+
+**Goals**
+- Extract predictive bridge predicates into a dedicated module.
+- Extract deep-oversold subtype and confirmation predicates into dedicated modules.
+- Preserve RC1 behavior on golden core fields.
+- Strengthen testability with module-focused regression tests.
+
+**Non-Goals**
+- Introduce new predictive or deep-oversold optimization thresholds in RC1.
+- Change report schemas or remove legacy reason strings.
+- Promote experiment-branch behavior into RC1.
+
+## Decisions
+
+### 1. Predicate extraction first, orchestration rewrite later
+Only predicate logic moves now. Action ordering remains controlled by existing strategy flow to avoid path drift.
+
+### 2. Keep strategy context as source of truth
+New modules consume primitive inputs and context-derived values; they do not own position state mutation.
+
+### 3. Preserve compatibility API
+`dragon_strategy.py` remains callable exactly as before (`DragonRuleEngine.run(...)` unchanged).
+
+## Risks / Trade-offs
+
+- [Predicate mismatch after extraction] -> pin with no-silent-path test and golden hash.
+- [Context contract drift] -> pass explicit arguments rather than sharing mutable objects directly.
+- [Over-extraction complexity] -> limit this phase to predictive/deep-oversold families only.
+
+## Migration Plan
+
+1. Add predictive bridge module and deep-oversold modules.
+2. Replace inline predicates in `dragon_strategy.py` with module calls.
+3. Add module-focused tests.
+4. Run golden baseline, full tests, and daily pipeline smoke.
+
+## Rollback Plan
+
+If any regression gate fails, revert extraction commit and keep previous compatibility layout.

+ 26 - 0
research/dragon/v2/openspec/changes/predictive-deepoversold-physical-isolation/proposal.md

@@ -0,0 +1,26 @@
+## Why
+
+The previous OpenSpec change established layered governance and structured-state compatibility, but `predictive_*` and `deep_oversold_*` logic still lives mostly inside `dragon_strategy.py`. This keeps the highest-risk weak families coupled to the compatibility facade and limits isolated testing.
+
+## What Changes
+
+- Physically isolate predictive bridge logic into a dedicated module.
+- Physically isolate deep-oversold subtype/classification/confirmation logic into dedicated modules.
+- Keep behavior compatibility by calling new modules from `dragon_strategy.py` while preserving legacy output schema.
+- Add module-level tests and rerun golden + pipeline gates.
+
+## Capabilities
+
+### New Capabilities
+- `predictive-bridge-module`: Dedicated bridge module for predictive break reentry/exit predicates.
+- `deep-oversold-module`: Dedicated classifier and confirmation predicates for deep oversold flows.
+- `module-regression-gates`: Focused tests for predictive and deep-oversold module behavior.
+
+### Modified Capabilities
+- `dragon-strategy-compatibility-facade`: Delegates weak-family decision predicates to isolated modules without changing external strategy outputs.
+
+## Impact
+
+- Affected code: `dragon_strategy.py`, new predictive/deep-oversold modules, tests.
+- Expected runtime impact: none material; predicate calls moved to module functions.
+- Governance impact: weaker family logic becomes independently testable and easier to audit.

+ 15 - 0
research/dragon/v2/openspec/changes/predictive-deepoversold-physical-isolation/specs/deep-oversold-module/spec.md

@@ -0,0 +1,15 @@
+## ADDED Requirements
+
+### Requirement: Deep oversold subtype and confirmation predicates SHALL be isolated
+Deep oversold subtype classification, base-entry checks, selective-veto checks, and confirmation predicates SHALL be moved into dedicated modules and consumed by the compatibility strategy facade.
+
+#### Scenario: Strategy evaluates deep-oversold buy path
+- **WHEN** deep-oversold entry logic is evaluated
+- **THEN** the strategy facade delegates subtype/classification/confirmation predicate evaluation to dedicated deep-oversold modules
+
+### Requirement: Deep oversold extraction SHALL remain path-compatible by default
+Module extraction for deep-oversold predicates SHALL keep RC1 behavior unchanged unless explicitly approved as behavior-changing research.
+
+#### Scenario: Deep-oversold module extraction is validated
+- **WHEN** no behavior change is declared
+- **THEN** compatibility regression tests and golden checks pass without core-path drift

+ 15 - 0
research/dragon/v2/openspec/changes/predictive-deepoversold-physical-isolation/specs/predictive-bridge-module/spec.md

@@ -0,0 +1,15 @@
+## ADDED Requirements
+
+### Requirement: Predictive bridge predicates SHALL be isolated in a dedicated module
+Predictive reentry and predictive break exit predicates SHALL be implemented in a dedicated bridge module and consumed by the compatibility strategy facade.
+
+#### Scenario: Strategy evaluates predictive bridge conditions
+- **WHEN** predictive reentry or predictive break exit checks are needed
+- **THEN** the strategy calls the dedicated predictive bridge module predicates instead of inlining those predicate conditions in the main facade
+
+### Requirement: Predictive extraction SHALL preserve RC1 compatibility behavior
+Refactoring predictive predicates into a module SHALL preserve RC1 golden-core behavior unless an explicit behavior-change proposal is approved.
+
+#### Scenario: Predictive module extraction is merged
+- **WHEN** golden regression is run on RC1 release window
+- **THEN** core hash and summary outputs remain aligned with the approved baseline

+ 22 - 0
research/dragon/v2/openspec/changes/predictive-deepoversold-physical-isolation/tasks.md

@@ -0,0 +1,22 @@
+## 1. Predictive Bridge Physical Isolation
+
+- [x] 1.1 Add `dragon_bridge_predictive_break.py` with predictive reentry/exit predicates.
+- [x] 1.2 Replace predictive inline checks in `dragon_strategy.py` with module calls.
+- [x] 1.3 Verify bridge-chain behavior remains stable.
+
+## 2. Deep Oversold Physical Isolation
+
+- [x] 2.1 Add `dragon_deep_oversold_classifier.py` for subtype and base-entry predicates.
+- [x] 2.2 Add `dragon_deep_oversold_confirmation.py` for confirmation-state predicates.
+- [x] 2.3 Replace deep-oversold inline checks in `dragon_strategy.py` with module calls.
+
+## 3. Validation
+
+- [x] 3.1 Add module-focused regression tests for predictive and deep-oversold predicates.
+- [x] 3.2 Re-run golden baseline and verify core hashes remain stable.
+- [x] 3.3 Re-run full unittest suite.
+- [x] 3.4 Re-run daily signal pipeline smoke.
+
+## 4. Acceptance
+
+- [x] 4.1 Publish acceptance summary in this OpenSpec change folder.

+ 70 - 0
research/dragon/v2/tests/test_deep_oversold_modules.py

@@ -0,0 +1,70 @@
+from __future__ import annotations
+
+from datetime import date
+import unittest
+
+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_strategy_config import StrategyConfig
+
+
+class TestDeepOversoldModules(unittest.TestCase):
+    def test_subtype_classification(self) -> None:
+        self.assertEqual(deep_oversold_subtype(a1=-0.01, b1=0.01, c1=13.0), "positive_b1_rebound")
+        self.assertEqual(deep_oversold_subtype(a1=-0.06, b1=-0.09, c1=10.5), "deep_capitulation")
+        self.assertEqual(deep_oversold_subtype(a1=-0.05, b1=-0.07, c1=11.5), "classic_oversold")
+
+    def test_base_entry_and_selective_veto(self) -> None:
+        cfg = StrategyConfig(
+            deep_oversold_selective_positive_b1_c1_max=15.3,
+            deep_oversold_confirm_weak_with_ql=True,
+        )
+        self.assertTrue(deep_oversold_base_entry(a1=-0.03, b1=-0.08, c1=12.0, config=cfg))
+        self.assertTrue(
+            deep_oversold_selective_veto(
+                subtype="positive_b1_rebound",
+                c1=14.0,
+                b1=0.02,
+                ql_buy=True,
+                config=cfg,
+            )
+        )
+        self.assertTrue(
+            deep_oversold_requires_confirmation(
+                subtype="shallow_false_start",
+                ql_buy=False,
+                config=cfg,
+            )
+        )
+
+    def test_pending_confirmation_flow(self) -> None:
+        self.assertTrue(
+            should_increment_pending(
+                active=True,
+                subtype="shallow_false_start",
+                origin_date=date(2026, 4, 8),
+                row_date=date(2026, 4, 9),
+            )
+        )
+        action, reason, clear = evaluate_pending_confirmation(
+            active=True,
+            subtype="shallow_false_start",
+            in_position=False,
+            kdj_sell=False,
+            ql_sell=False,
+            bars_waited=1,
+            window_bars=2,
+            ql_buy=True,
+        )
+        self.assertEqual(action, "BUY")
+        self.assertIn("confirmed_shallow_false_start", reason)
+        self.assertTrue(clear)
+
+
+if __name__ == "__main__":
+    unittest.main()

+ 107 - 0
research/dragon/v2/tests/test_predictive_bridge_module.py

@@ -0,0 +1,107 @@
+from __future__ import annotations
+
+from datetime import date, timedelta
+import unittest
+
+from dragon_bridge_predictive_break import (
+    allow_predictive_b1_break_long_exit,
+    allow_predictive_b1_break_short_exit,
+    allow_predictive_error_reentry,
+)
+from dragon_strategy_config import StrategyConfig
+
+
+class TestPredictiveBridgeModule(unittest.TestCase):
+    def test_predictive_error_reentry_gate(self) -> None:
+        d = date(2026, 4, 9)
+        self.assertTrue(
+            allow_predictive_error_reentry(
+                enabled=True,
+                last_exit_predictive_break=True,
+                last_real_sell_date=d - timedelta(days=2),
+                row_date=d,
+                a1=-0.005,
+                b1=-0.10,
+                c1=55.0,
+            )
+        )
+        self.assertFalse(
+            allow_predictive_error_reentry(
+                enabled=True,
+                last_exit_predictive_break=False,
+                last_real_sell_date=d - timedelta(days=2),
+                row_date=d,
+                a1=-0.005,
+                b1=-0.10,
+                c1=55.0,
+            )
+        )
+
+    def test_predictive_short_exit_gate(self) -> None:
+        cfg = StrategyConfig()
+        self.assertTrue(
+            allow_predictive_b1_break_short_exit(
+                enabled=True,
+                has_sell_signal=False,
+                entry_is_glued=True,
+                holding_days=1,
+                a1=-0.01,
+                b1=-0.14,
+                c1=55.0,
+                config=cfg,
+            )
+        )
+        self.assertFalse(
+            allow_predictive_b1_break_short_exit(
+                enabled=True,
+                has_sell_signal=True,
+                entry_is_glued=True,
+                holding_days=1,
+                a1=-0.01,
+                b1=-0.14,
+                c1=55.0,
+                config=cfg,
+            )
+        )
+
+    def test_predictive_long_exit_gate(self) -> None:
+        cfg = StrategyConfig()
+        d = date(2026, 4, 9)
+        self.assertTrue(
+            allow_predictive_b1_break_long_exit(
+                enabled=True,
+                has_sell_signal=False,
+                entry_is_glued=True,
+                holding_days=45,
+                max_c1_since_entry=70.0,
+                max_a1_since_entry=0.2,
+                max_b1_since_entry=0.35,
+                last_ql_sell_date=d - timedelta(days=3),
+                row_date=d,
+                a1=-0.01,
+                b1=-0.13,
+                c1=62.0,
+                config=cfg,
+            )
+        )
+        self.assertFalse(
+            allow_predictive_b1_break_long_exit(
+                enabled=True,
+                has_sell_signal=False,
+                entry_is_glued=True,
+                holding_days=45,
+                max_c1_since_entry=70.0,
+                max_a1_since_entry=0.2,
+                max_b1_since_entry=0.35,
+                last_ql_sell_date=d - timedelta(days=10),
+                row_date=d,
+                a1=-0.01,
+                b1=-0.13,
+                c1=62.0,
+                config=cfg,
+            )
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()