Explorar o código

Execute OpenSpec RC1 layered optimization workstream

erwin hai 1 mes
pai
achega
b421fa8089

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

@@ -1053,3 +1053,24 @@
 - Added local OpenSpec governance package under `dragon/v2`:
 - `openspec/config.yaml`
 - change set `openspec/changes/rc1-layered-governed-optimization/` with proposal/design/tasks/spec docs covering layered rule-engine migration, structured state migration, weak-family governance, and golden regression gates.
+- Later executed the full OpenSpec task pack in one uninterrupted run on user request.
+- Main implementation outcomes:
+- layered orchestration modules added:
+- `dragon_rule_engine_v2.py`
+- `dragon_rules_core.py`
+- `dragon_rules_secondary.py`
+- `dragon_rules_bridge.py`
+- `dragon_strategy.py` kept as compatibility facade while adding structured control fields and bridge flags for predictive/pending chains.
+- new regression tests added and all passing (`8` total tests):
+- `test_no_silent_path_change.py`
+- `test_bridge_chain_regression.py`
+- `test_deep_oversold_pending_paths.py`
+- weak-family experiment pack executed with outputs:
+- `dragon_weak_family_experiment_summary.csv`
+- `dragon_weak_family_trade_diff.csv`
+- `dragon_weak_family_execution_stress.csv`
+- plus markdown/config snapshots.
+- layered attribution outputs added via `dragon_layered_pnl_attribution.py`.
+- 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`.

+ 1 - 0
research/dragon/v2/USER.md

@@ -23,3 +23,4 @@
 - User preference: live signal checks should support intraday realtime evaluation, using the current market price as the provisional `close` for today's bar when the official daily bar is not yet available.
 - User relies on the assistant for directional judgment when a live signal looks discretionary or when it is unclear whether a veto is filtering alpha or protecting quality; answers should be explicit rather than hedged.
 - User preference: strategy-governance and optimization execution should be advanced in OpenSpec form (`openspec/changes/...`) for traceable proposal/design/tasks/spec workflow.
+- User preference: when requesting bulk execution ("全部执行完,再找我"), do not send intermediate progress updates and report only after full completion.

+ 36 - 0
research/dragon/v2/dragon_layered_pnl_attribution.md

@@ -0,0 +1,36 @@
+# Dragon Layered PnL Attribution
+
+- window: `2016-01-01 -> 2025-12-31`
+- branch: `alpha_first_glued_refined_hot_cap` (RC1)
+- indicator source: `dragon_indicator_snapshot_full.csv`
+- trades: `91`
+
+## Entry-Layer x Exit-Layer
+- core -> core: trades `55`, win_rate `56.36%`, avg_return `4.42%`, PF `6.73`
+- secondary -> core: trades `16`, win_rate `56.25%`, avg_return `1.68%`, PF `5.32`
+- secondary -> secondary: trades `11`, win_rate `18.18%`, avg_return `-1.87%`, PF `0.04`
+- core -> secondary: trades `4`, win_rate `50.00%`, avg_return `1.91%`, PF `3.56`
+- bridge -> core: trades `3`, win_rate `66.67%`, avg_return `1.80%`, PF `2.89`
+- core -> bridge: trades `1`, win_rate `100.00%`, avg_return `36.85%`, PF `inf`
+- secondary -> bridge: trades `1`, win_rate `100.00%`, avg_return `12.07%`, PF `inf`
+
+## Top Entry Families
+- core / glued: trades `50`, avg_return `4.92%`, PF `7.15`
+- secondary / dual_gold: trades `13`, avg_return `1.06%`, PF `2.08`
+- secondary / deep_oversold: trades `10`, avg_return `-0.19%`, PF `0.80`
+- core / early_crash: trades `6`, avg_return `4.62%`, PF `10.52`
+- secondary / post_sell_rebound: trades `4`, avg_return `1.84%`, PF `2.56`
+- core / oversold_recovery: trades `4`, avg_return `3.35%`, PF `6.65`
+- bridge / bridge_reentry: trades `2`, avg_return `1.71%`, PF `2.20`
+- bridge / predictive_break: trades `1`, avg_return `1.99%`, PF `inf`
+- secondary / oversold_recovery: trades `1`, avg_return `-0.77%`, PF `0.00`
+
+## Top Exit Families
+- core / glued: trades `54`, avg_return `0.18%`, PF `1.22`
+- core / high_regime: trades `11`, avg_return `11.63%`, PF `107.38`
+- secondary / post_sell_rebound: trades `10`, avg_return `-2.00%`, PF `0.02`
+- core / risk_management: trades `9`, avg_return `15.26%`, PF `28.68`
+- secondary / high_regime: trades `3`, avg_return `2.93%`, PF `5.76`
+- bridge / predictive_break: trades `2`, avg_return `24.46%`, PF `inf`
+- secondary / dual_gold: trades `1`, avg_return `-2.07%`, PF `0.00`
+- secondary / deep_oversold: trades `1`, avg_return `0.40%`, PF `inf`

+ 113 - 0
research/dragon/v2/dragon_layered_pnl_attribution.py

@@ -0,0 +1,113 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+import pandas as pd
+
+from dragon_branch_configs import alpha_first_glued_refined_hot_cap_config
+from dragon_rc1_golden_baseline import _load_indicator_snapshot
+from dragon_shared import END_DATE, START_DATE, format_num, format_pct, profit_factor
+from dragon_strategy import DragonRuleEngine
+
+
+def _summary(frame: pd.DataFrame, group_cols: list[str]) -> pd.DataFrame:
+    grouped = frame.groupby(group_cols, dropna=False)
+    rows: list[dict[str, object]] = []
+    for keys, group in grouped:
+        if not isinstance(keys, tuple):
+            keys = (keys,)
+        returns = group["return_pct"].astype(float)
+        row = {group_cols[i]: keys[i] for i in range(len(group_cols))}
+        row.update(
+            {
+                "trades": int(len(group)),
+                "win_rate": float((returns > 0).mean()),
+                "avg_return": float(returns.mean()),
+                "median_return": float(returns.median()),
+                "compounded_return": float((1.0 + returns).prod() - 1.0),
+                "profit_factor": profit_factor(returns),
+            }
+        )
+        rows.append(row)
+    return pd.DataFrame(rows).sort_values("trades", ascending=False).reset_index(drop=True)
+
+
+def main() -> None:
+    base_dir = Path(__file__).resolve().parent
+    indexed, source = _load_indicator_snapshot(base_dir)
+
+    engine = DragonRuleEngine(config=alpha_first_glued_refined_hot_cap_config())
+    _, trades = engine.run(indexed)
+    trades = trades[
+        (trades["buy_date"] >= START_DATE)
+        & (trades["buy_date"] <= END_DATE)
+        & (trades["sell_date"] >= START_DATE)
+        & (trades["sell_date"] <= END_DATE)
+    ].copy()
+
+    layer_summary = _summary(trades, ["buy_reason_layer", "sell_reason_layer"])
+    family_summary = _summary(trades, ["buy_reason_family", "sell_reason_family"])
+    entry_summary = _summary(trades, ["buy_reason_layer", "buy_reason_family"])
+    exit_summary = _summary(trades, ["sell_reason_layer", "sell_reason_family"])
+
+    layer_summary.to_csv(base_dir / "dragon_layered_pnl_attribution.csv", index=False, encoding="utf-8-sig")
+    family_summary.to_csv(base_dir / "dragon_layered_family_pnl_attribution.csv", index=False, encoding="utf-8-sig")
+    entry_summary.to_csv(base_dir / "dragon_layered_entry_pnl_attribution.csv", index=False, encoding="utf-8-sig")
+    exit_summary.to_csv(base_dir / "dragon_layered_exit_pnl_attribution.csv", index=False, encoding="utf-8-sig")
+
+    lines: list[str] = [
+        "# Dragon Layered PnL Attribution",
+        "",
+        f"- window: `{START_DATE} -> {END_DATE}`",
+        "- branch: `alpha_first_glued_refined_hot_cap` (RC1)",
+        f"- indicator source: `{source}`",
+        f"- trades: `{int(len(trades))}`",
+        "",
+        "## Entry-Layer x Exit-Layer",
+    ]
+    for _, row in layer_summary.iterrows():
+        lines.append(
+            "- "
+            f"{row['buy_reason_layer']} -> {row['sell_reason_layer']}: "
+            f"trades `{int(row['trades'])}`, "
+            f"win_rate `{format_pct(float(row['win_rate']))}`, "
+            f"avg_return `{format_pct(float(row['avg_return']))}`, "
+            f"PF `{format_num(float(row['profit_factor']))}`"
+        )
+
+    lines.extend(
+        [
+            "",
+            "## Top Entry Families",
+        ]
+    )
+    for _, row in entry_summary.head(10).iterrows():
+        lines.append(
+            "- "
+            f"{row['buy_reason_layer']} / {row['buy_reason_family']}: "
+            f"trades `{int(row['trades'])}`, "
+            f"avg_return `{format_pct(float(row['avg_return']))}`, "
+            f"PF `{format_num(float(row['profit_factor']))}`"
+        )
+
+    lines.extend(
+        [
+            "",
+            "## Top Exit Families",
+        ]
+    )
+    for _, row in exit_summary.head(10).iterrows():
+        lines.append(
+            "- "
+            f"{row['sell_reason_layer']} / {row['sell_reason_family']}: "
+            f"trades `{int(row['trades'])}`, "
+            f"avg_return `{format_pct(float(row['avg_return']))}`, "
+            f"PF `{format_num(float(row['profit_factor']))}`"
+        )
+
+    (base_dir / "dragon_layered_pnl_attribution.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
+
+
+if __name__ == "__main__":
+    main()
+

+ 45 - 0
research/dragon/v2/dragon_migration_audit.md

@@ -0,0 +1,45 @@
+# Dragon Migration Audit (Phase 3 Compatibility)
+
+## Scope
+
+- phase: `rc1-layered-governed-optimization`
+- objective: preserve RC1 behavior while introducing layered orchestration and structured state migration
+- window: `2016-01-01 -> 2025-12-31`
+
+## Compatibility-Preserving Moves
+
+- Added layered orchestration modules:
+  - `dragon_rule_engine_v2.py`
+  - `dragon_rules_core.py`
+  - `dragon_rules_secondary.py`
+  - `dragon_rules_bridge.py`
+- Kept `dragon_strategy.py` as compatibility facade and legacy behavior source.
+- Added structured context metadata fields:
+  - `entry_reason_layer/family/code`
+  - `last_real_sell_reason_layer/family/code`
+  - bridge flags for predictive and pending deep-oversold paths.
+- Migrated high-risk control checks from raw string coupling to structured state in:
+  - predictive reentry gate
+  - post-washout gate
+  - state crash followthrough gate
+  - glued/dual-gold entry-family checks via `_entry_reason_is(...)`
+
+## New Governance Tests
+
+- `tests/test_no_silent_path_change.py`
+- `tests/test_bridge_chain_regression.py`
+- `tests/test_deep_oversold_pending_paths.py`
+
+## Acceptance Gates
+
+Compatibility refactors are accepted only if all pass:
+
+1. golden core hash regression (`tests/test_rc1_golden_regression.py`)
+2. no-silent-path-change layered-vs-legacy regression
+3. bridge-chain regression checks
+4. signal-pipeline smoke run
+
+## Behavior-Change Notes
+
+- No intentional RC1 trade-path change is introduced in compatibility mode.
+- Any behavior-changing weak-family optimization remains branch-isolated and must ship added/removed trade attribution.

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

@@ -1,5 +1,5 @@
 {
-  "generated_at": "2026-04-09T00:57:54",
+  "generated_at": "2026-04-09T02:41:07",
   "release_version": "RC1",
   "branch": "alpha_first_glued_refined_hot_cap",
   "evaluation_window": {

+ 77 - 0
research/dragon/v2/dragon_rule_engine_v2.py

@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Optional
+
+import pandas as pd
+
+from dragon_decision_types import StrategyDecision
+from dragon_reason_types import RuleLayer
+from dragon_rules_bridge import BridgeRuleLayer
+from dragon_rules_core import CoreRuleLayer
+from dragon_rules_secondary import SecondaryRuleLayer
+from dragon_strategy import DragonRuleEngine
+from dragon_strategy_config import StrategyConfig
+
+
+@dataclass(frozen=True)
+class LayerRoutingResult:
+    decision: StrategyDecision
+    routed_layer: str
+
+
+class LayeredDragonRuleEngine:
+    """
+    Compatibility-first layered orchestrator.
+
+    The current behavior path is still produced by the legacy DragonRuleEngine.
+    This orchestrator adds explicit core -> secondary -> bridge routing semantics
+    on top of legacy decisions without changing the trading path.
+    """
+
+    def __init__(self, config: Optional[StrategyConfig] = None):
+        self.config = config or StrategyConfig()
+        self.legacy = DragonRuleEngine(config=self.config)
+        self.layers = (
+            CoreRuleLayer(),
+            SecondaryRuleLayer(),
+            BridgeRuleLayer(),
+        )
+
+    def _route_decision(self, side: str, event_layer: str, reason: str) -> LayerRoutingResult:
+        for layer in self.layers:
+            routed = layer.route(side=side, event_layer=event_layer, reason=reason)
+            if routed is not None:
+                return LayerRoutingResult(decision=routed, routed_layer=layer.layer.value)
+
+        fallback = self.legacy._build_decision(side=side, layer=event_layer, reason=reason)
+        return LayerRoutingResult(decision=fallback, routed_layer=RuleLayer.UNKNOWN.value)
+
+    def _enrich_events_with_layer_routing(self, events: pd.DataFrame) -> pd.DataFrame:
+        if events.empty:
+            return events
+        rows = [
+            self._route_decision(
+                side=str(row["side"]),
+                event_layer=str(row["layer"]),
+                reason=str(row["reason"]),
+            )
+            for _, row in events.iterrows()
+        ]
+        enriched = events.copy()
+        enriched["orchestrated_layer"] = [r.routed_layer for r in rows]
+        if "reason_layer" not in enriched.columns:
+            enriched["reason_layer"] = [r.decision.reason.layer.value if r.decision.reason else "unknown" for r in rows]
+            enriched["reason_family"] = [r.decision.reason.family.value if r.decision.reason else "unknown" for r in rows]
+            enriched["reason_code"] = [r.decision.reason.code if r.decision.reason else "" for r in rows]
+        return enriched
+
+    def run(self, df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
+        events, trades = self.legacy.run(df)
+        events = self._enrich_events_with_layer_routing(events)
+        if not trades.empty and "orchestrated_entry_layer" not in trades.columns:
+            trades = trades.copy()
+            trades["orchestrated_entry_layer"] = trades.get("buy_reason_layer", pd.Series(["unknown"] * len(trades))).astype(str)
+            trades["orchestrated_exit_layer"] = trades.get("sell_reason_layer", pd.Series(["unknown"] * len(trades))).astype(str)
+        return events, trades
+

+ 32 - 0
research/dragon/v2/dragon_rules_bridge.py

@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+from dragon_decision_types import StrategyDecision
+from dragon_reason_types import DecisionReason, RuleLayer
+from dragon_rule_catalog import classify_aux_reason, classify_entry_reason, classify_exit_reason
+
+
+def _classify_reason(side: str, event_layer: str, reason: str) -> DecisionReason:
+    if event_layer == "aux_signal":
+        return classify_aux_reason(reason)
+    if side == "BUY":
+        return classify_entry_reason(reason)
+    return classify_exit_reason(reason)
+
+
+class BridgeRuleLayer:
+    layer = RuleLayer.BRIDGE
+
+    def route(self, side: str, event_layer: str, reason: str) -> StrategyDecision | None:
+        meta = _classify_reason(side, event_layer, reason)
+        if meta.layer != self.layer:
+            return None
+        action = "BUY" if side == "BUY" and event_layer == "real_trade" else (
+            "SELL" if side == "SELL" and event_layer == "real_trade" else (
+                "AUX_BUY" if side == "BUY" else "AUX_SELL"
+            )
+        )
+        return StrategyDecision(
+            action=action,
+            reason=meta,
+            metadata={"orchestrated_layer": self.layer.value},
+        )

+ 32 - 0
research/dragon/v2/dragon_rules_core.py

@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+from dragon_decision_types import StrategyDecision
+from dragon_reason_types import DecisionReason, RuleLayer
+from dragon_rule_catalog import classify_aux_reason, classify_entry_reason, classify_exit_reason
+
+
+def _classify_reason(side: str, event_layer: str, reason: str) -> DecisionReason:
+    if event_layer == "aux_signal":
+        return classify_aux_reason(reason)
+    if side == "BUY":
+        return classify_entry_reason(reason)
+    return classify_exit_reason(reason)
+
+
+class CoreRuleLayer:
+    layer = RuleLayer.CORE
+
+    def route(self, side: str, event_layer: str, reason: str) -> StrategyDecision | None:
+        meta = _classify_reason(side, event_layer, reason)
+        if meta.layer != self.layer:
+            return None
+        action = "BUY" if side == "BUY" and event_layer == "real_trade" else (
+            "SELL" if side == "SELL" and event_layer == "real_trade" else (
+                "AUX_BUY" if side == "BUY" else "AUX_SELL"
+            )
+        )
+        return StrategyDecision(
+            action=action,
+            reason=meta,
+            metadata={"orchestrated_layer": self.layer.value},
+        )

+ 32 - 0
research/dragon/v2/dragon_rules_secondary.py

@@ -0,0 +1,32 @@
+from __future__ import annotations
+
+from dragon_decision_types import StrategyDecision
+from dragon_reason_types import DecisionReason, RuleLayer
+from dragon_rule_catalog import classify_aux_reason, classify_entry_reason, classify_exit_reason
+
+
+def _classify_reason(side: str, event_layer: str, reason: str) -> DecisionReason:
+    if event_layer == "aux_signal":
+        return classify_aux_reason(reason)
+    if side == "BUY":
+        return classify_entry_reason(reason)
+    return classify_exit_reason(reason)
+
+
+class SecondaryRuleLayer:
+    layer = RuleLayer.SECONDARY
+
+    def route(self, side: str, event_layer: str, reason: str) -> StrategyDecision | None:
+        meta = _classify_reason(side, event_layer, reason)
+        if meta.layer != self.layer:
+            return None
+        action = "BUY" if side == "BUY" and event_layer == "real_trade" else (
+            "SELL" if side == "SELL" and event_layer == "real_trade" else (
+                "AUX_BUY" if side == "BUY" else "AUX_SELL"
+            )
+        )
+        return StrategyDecision(
+            action=action,
+            reason=meta,
+            metadata={"orchestrated_layer": self.layer.value},
+        )

+ 80 - 14
research/dragon/v2/dragon_strategy.py

@@ -20,6 +20,9 @@ class StrategyContext:
     entry_b1: Optional[float] = None
     entry_c1: Optional[float] = None
     entry_reason: str = ""
+    entry_reason_layer: str = "unknown"
+    entry_reason_family: str = "unknown"
+    entry_reason_code: str = ""
     first_exit_checked: bool = False
     c1_over_80_seen: bool = False
     a1_big_pos_count: int = 0
@@ -28,6 +31,11 @@ class StrategyContext:
     prev_real_sell_c1: Optional[float] = None
     last_real_sell_date: Optional[date] = None
     last_real_sell_reason: str = ""
+    last_real_sell_reason_layer: str = "unknown"
+    last_real_sell_reason_family: str = "unknown"
+    last_real_sell_reason_code: str = ""
+    bridge_last_exit_predictive_break: bool = False
+    bridge_last_exit_negative_a1_no_b1_recovery: bool = False
     last_big_regime_exit_date: Optional[date] = None
     last_kdj_buy_date: Optional[date] = None
     last_kdj_buy_a1: Optional[float] = None
@@ -63,6 +71,7 @@ class StrategyContext:
     pending_deep_oversold_b1: Optional[float] = None
     pending_deep_oversold_c1: Optional[float] = None
     pending_deep_oversold_bars_waited: int = 0
+    bridge_pending_deep_oversold_active: bool = False
 
 
 class DragonRuleEngine:
@@ -104,7 +113,35 @@ class DragonRuleEngine:
     def _entry_reason_is(self, *reason_names: str) -> bool:
         entry_reason = self.context.entry_reason
         entry_family = entry_reason.split(":", 1)[0]
-        return entry_reason in reason_names or entry_family in reason_names
+        if entry_reason in reason_names or entry_family in reason_names:
+            return True
+        if not self.context.entry_reason_code and not self.context.entry_reason_family:
+            return False
+        if "glued_buy" in reason_names and self.context.entry_reason_code == "entry_glued_buy":
+            return True
+        if "dual_gold_resonance_buy" in reason_names and self.context.entry_reason_code == "entry_dual_gold":
+            return True
+        if (
+            "deep_oversold_rebound_buy" in reason_names
+            and self.context.entry_reason_family == "deep_oversold"
+        ):
+            return True
+        if (
+            "oversold_reversal_after_ql_buy" in reason_names
+            and self.context.entry_reason_code == "entry_oversold_reversal_after_ql"
+        ):
+            return True
+        return False
+
+    def _last_sell_reason_is(self, reason_name: str) -> bool:
+        if self.context.last_real_sell_reason == reason_name:
+            return True
+        code_map = {
+            "crash_protection_exit": "exit_crash_protection",
+            "predictive_b1_break_exit": "exit_predictive_b1_break",
+        }
+        target_code = code_map.get(reason_name, "")
+        return bool(target_code and self.context.last_real_sell_reason_code == target_code)
 
     def _clear_pending_deep_oversold(self) -> None:
         self.context.pending_deep_oversold_subtype = ""
@@ -114,6 +151,7 @@ class DragonRuleEngine:
         self.context.pending_deep_oversold_b1 = None
         self.context.pending_deep_oversold_c1 = None
         self.context.pending_deep_oversold_bars_waited = 0
+        self.context.bridge_pending_deep_oversold_active = False
 
     def _queue_pending_deep_oversold(self, row: pd.Series, subtype: str) -> None:
         self.context.pending_deep_oversold_subtype = subtype
@@ -123,6 +161,7 @@ class DragonRuleEngine:
         self.context.pending_deep_oversold_b1 = float(row["b1"])
         self.context.pending_deep_oversold_c1 = float(row["c1"])
         self.context.pending_deep_oversold_bars_waited = 0
+        self.context.bridge_pending_deep_oversold_active = True
 
     @staticmethod
     def _is_glued(a1: float) -> bool:
@@ -180,7 +219,11 @@ class DragonRuleEngine:
             self.context.last_ql_sell_date = row.name.date()
 
     def _update_pending_states(self, row: pd.Series) -> None:
-        if not self.context.pending_deep_oversold_subtype or self.context.pending_deep_oversold_origin_date is 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
+        ):
             return
         if row.name.date() != self.context.pending_deep_oversold_origin_date:
             self.context.pending_deep_oversold_bars_waited += 1
@@ -460,7 +503,7 @@ class DragonRuleEngine:
         days_from_last_sell = self._days_from_last_real_sell(row.name.date())
         if days_from_last_sell is None:
             return False
-        if self.context.last_real_sell_reason != "crash_protection_exit":
+        if not self._last_sell_reason_is("crash_protection_exit"):
             return False
         if not (0 < days_from_last_sell <= self.config.state_crash_followthrough_window_days):
             return False
@@ -538,7 +581,10 @@ class DragonRuleEngine:
         return True
 
     def _pending_deep_oversold_decision(self, row: pd.Series) -> tuple[str, str]:
-        if not self.context.pending_deep_oversold_subtype:
+        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()
@@ -675,7 +721,7 @@ class DragonRuleEngine:
             return False
         if self.context.prev_real_sell_c1 >= 15:
             return False
-        if not self.context.last_real_sell_reason.startswith("negative_a1_no_b1_recovery"):
+        if not self.context.bridge_last_exit_negative_a1_no_b1_recovery:
             return False
         if bool(row["kdj_buy"]) or bool(row["ql_buy"]) or bool(row["ql_sell"]) or not bool(row["kdj_sell"]):
             return False
@@ -714,7 +760,7 @@ class DragonRuleEngine:
             if (
                 self._rule_enabled("predictive_error_reentry_buy")
                 and
-                self.context.last_real_sell_reason == "predictive_b1_break_exit"
+                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
@@ -758,7 +804,7 @@ class DragonRuleEngine:
             elif self._deep_oversold_selective_veto(row, subtype):
                 pass
             elif self._deep_oversold_requires_confirmation(row, subtype):
-                if not self.context.pending_deep_oversold_subtype:
+                if not self.context.bridge_pending_deep_oversold_active:
                     self._queue_pending_deep_oversold(row, subtype)
             else:
                 return "BUY", f"deep_oversold_rebound_buy:{subtype}"
@@ -828,7 +874,7 @@ class DragonRuleEngine:
 
         if (
             not has_sell_signal
-            and self.context.entry_reason == "glued_buy"
+            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
@@ -839,7 +885,7 @@ class DragonRuleEngine:
 
         if (
             not has_sell_signal
-            and self.context.entry_reason == "glued_buy"
+            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
@@ -915,7 +961,7 @@ class DragonRuleEngine:
             if (
                 self.context.last_ql_sell_date is not None
                 and 0 <= (row.name.date() - self.context.last_ql_sell_date).days <= 7
-                and self.context.entry_reason == "glued_buy"
+                and self._entry_reason_is("glued_buy")
                 and 84 < c1 < 86.5
                 and a1 < 0.028
                 and b1 < 0
@@ -944,7 +990,7 @@ class DragonRuleEngine:
         if (
             bool(row["ql_sell"])
             and not bool(row["kdj_sell"])
-            and self.context.entry_reason == "glued_buy"
+            and self._entry_reason_is("glued_buy")
             and self._holding_days(row.name.date()) <= 7
             and 60 < c1 < 72
             and abs(a1) < 0.01
@@ -981,7 +1027,7 @@ class DragonRuleEngine:
                 if (
                     bool(row["ql_sell"])
                     and not bool(row["kdj_sell"])
-                    and self.context.entry_reason == "glued_buy"
+                    and self._entry_reason_is("glued_buy")
                     and self._holding_days(row.name.date()) >= 40
                     and 70 < c1 < 75
                     and 0 < a1 < 0.02
@@ -993,7 +1039,7 @@ class DragonRuleEngine:
                 if (
                     bool(row["ql_sell"])
                     and not bool(row["kdj_sell"])
-                    and self.context.entry_reason == "glued_buy"
+                    and self._entry_reason_is("glued_buy")
                     and self._holding_days(row.name.date()) >= 15
                     and 60 < c1 < 66
                     and a1 > 0.01
@@ -1068,7 +1114,7 @@ class DragonRuleEngine:
         if -0.04 < a1 < -0.02:
             if (
                 bool(row["kdj_sell"])
-                and self.context.entry_reason == "dual_gold_resonance_buy"
+                and self._entry_reason_is("dual_gold_resonance_buy")
                 and c1 < 20
                 and b1 > 0
             ):
@@ -1116,6 +1162,7 @@ class DragonRuleEngine:
 
     def _post_real_buy(self, row: pd.Series, reason: str) -> None:
         self._clear_pending_deep_oversold()
+        entry_meta = classify_entry_reason(reason)
         self.context.in_position = True
         self.context.entry_date = row.name.date()
         self.context.entry_price = float(row["close"])
@@ -1123,6 +1170,9 @@ class DragonRuleEngine:
         self.context.entry_b1 = float(row["b1"])
         self.context.entry_c1 = float(row["c1"])
         self.context.entry_reason = reason
+        self.context.entry_reason_layer = entry_meta.layer.value
+        self.context.entry_reason_family = entry_meta.family.value
+        self.context.entry_reason_code = entry_meta.code
         self.context.first_exit_checked = False
         self.context.c1_over_80_seen = float(row["c1"]) > 80
         self.context.a1_big_pos_count = 1 if self._is_big_positive(float(row["a1"])) else 0
@@ -1143,12 +1193,18 @@ class DragonRuleEngine:
 
     def _post_real_sell(self, row: pd.Series, reason: str) -> None:
         self._clear_pending_deep_oversold()
+        sell_meta = classify_exit_reason(reason)
         if self.context.a1_big_pos_count >= 4:
             self.context.last_big_regime_exit_date = row.name.date()
             self.context.kdj_cross_count_since_big_regime_exit = 0
         self.context.prev_real_sell_c1 = float(row["c1"])
         self.context.last_real_sell_date = row.name.date()
         self.context.last_real_sell_reason = reason
+        self.context.last_real_sell_reason_layer = sell_meta.layer.value
+        self.context.last_real_sell_reason_family = sell_meta.family.value
+        self.context.last_real_sell_reason_code = sell_meta.code
+        self.context.bridge_last_exit_predictive_break = sell_meta.code == "exit_predictive_b1_break"
+        self.context.bridge_last_exit_negative_a1_no_b1_recovery = sell_meta.code == "exit_negative_a1_recovery"
         self.context.in_position = False
         self.context.entry_date = None
         self.context.entry_price = None
@@ -1156,6 +1212,9 @@ class DragonRuleEngine:
         self.context.entry_b1 = None
         self.context.entry_c1 = None
         self.context.entry_reason = ""
+        self.context.entry_reason_layer = "unknown"
+        self.context.entry_reason_family = "unknown"
+        self.context.entry_reason_code = ""
         self.context.first_exit_checked = False
         self.context.c1_over_80_seen = False
         self.context.a1_big_pos_count = 0
@@ -1340,3 +1399,10 @@ class DragonRuleEngine:
             trades_df["sell_reason_code"] = [meta.code for meta in sell_meta]
 
         return events_df, trades_df
+
+
+def run_with_layered_engine(df: pd.DataFrame, config: Optional[StrategyConfig] = None) -> tuple[pd.DataFrame, pd.DataFrame]:
+    from dragon_rule_engine_v2 import LayeredDragonRuleEngine
+
+    engine = LayeredDragonRuleEngine(config=config)
+    return engine.run(df)

+ 18 - 0
research/dragon/v2/dragon_weak_family_experiment.md

@@ -0,0 +1,18 @@
+# Dragon Weak Family Experiment Review
+
+- indicator source: `dragon_indicator_snapshot_full.csv`
+- window: `2016-01-01 -> 2025-12-31`
+
+## Summary
+- deep_oversold_confirmation_v2: trades `90`, win_rate `52.22%`, avg_return `3.42%`, compounded `1376.41%`
+- predictive_bridge_only: trades `90`, win_rate `52.22%`, avg_return `3.51%`, compounded `1465.93%`
+- rc1_baseline: trades `91`, win_rate `52.75%`, avg_return `3.42%`, compounded `1424.12%`
+
+## Added/Removed Trades vs RC1
+- deep_oversold_confirmation_v2: added `1` (avg `-3.18%`), removed `2` (avg `-0.03%`)
+- predictive_bridge_only: added `1` (avg `43.39%`), removed `2` (avg `19.42%`)
+
+## Execution Stress (next_open + 20 bps/side)
+- deep_oversold_confirmation_v2: avg_return `2.09%`, PF `2.33`, max_dd `-25.34%`
+- predictive_bridge_only: avg_return `2.08%`, PF `2.29`, max_dd `-25.34%`
+- rc1_baseline: avg_return `2.03%`, PF `2.28`, max_dd `-25.34%`

+ 324 - 0
research/dragon/v2/dragon_weak_family_experiment_config_snapshot.json

@@ -0,0 +1,324 @@
+{
+  "indicator_source": "dragon_indicator_snapshot_full.csv",
+  "evaluation_window": {
+    "start": "2016-01-01",
+    "end": "2025-12-31"
+  },
+  "variants": {
+    "rc1_baseline": {
+      "disabled_rules": [],
+      "post_exit_confirmation_window_days": 10,
+      "aux_sell_same_side_once_per_cycle": true,
+      "aux_sell_duplicate_cooldown_days": 5,
+      "aux_sell_high_zone_kdj_only_block_c1": 85.0,
+      "aux_sell_high_zone_kdj_only_block_b1": -0.02,
+      "aux_sell_high_zone_warning_c1": 80.0,
+      "aux_sell_strong_break_c1": 60.0,
+      "aux_sell_strong_break_b1": -0.05,
+      "aux_sell_stronger_c1_delta": 8.0,
+      "aux_sell_stronger_b1_delta": 0.05,
+      "aux_sell_high_zone_rearm_c1_delta": 2.0,
+      "state_crash_followthrough_window_days": 5,
+      "state_crash_followthrough_repeat_cooldown_days": 4,
+      "state_crash_followthrough_c1_max": 80.0,
+      "state_crash_followthrough_a1_max": 0.01,
+      "state_crash_followthrough_b1_max": -0.15,
+      "glued_high_weak_rebound_high_c1": 68.0,
+      "glued_high_weak_rebound_high_b1": -0.08,
+      "glued_high_weak_rebound_mid_c1": 50.0,
+      "glued_high_weak_rebound_mid_b1": -0.15,
+      "glued_high_weak_rebound_ql_c1_low": 35.0,
+      "glued_high_weak_rebound_ql_c1_high": 55.0,
+      "glued_high_weak_rebound_ql_b1": -0.06,
+      "glued_high_weak_rebound_ql_a1": -0.013,
+      "glued_selective_hot_c1_min": 40.0,
+      "glued_selective_hot_c1_max": 75.0,
+      "glued_selective_hot_b1_min": 0.1,
+      "glued_selective_low_c1_min": 23.0,
+      "glued_selective_low_c1_max": 28.0,
+      "glued_selective_low_b1_max": 0.02,
+      "deep_oversold_filter1_c1_low": 13.0,
+      "deep_oversold_filter1_c1_high": 15.0,
+      "deep_oversold_filter1_a1_min": -0.04,
+      "deep_oversold_filter1_b1_max": -0.08,
+      "deep_oversold_filter2_c1_low": 13.0,
+      "deep_oversold_filter2_c1_high": 14.5,
+      "deep_oversold_filter2_a1_min": -0.04,
+      "deep_oversold_filter2_b1_min": -0.06,
+      "deep_oversold_entry_c1_max": 16.0,
+      "deep_oversold_entry_a1_min": -0.09,
+      "deep_oversold_entry_b1_min": -0.1,
+      "deep_oversold_shallow_ql_fallback": true,
+      "deep_oversold_positive_b1_fallback_a1_min": -0.02,
+      "deep_oversold_block_positive_b1_rebound": false,
+      "deep_oversold_block_shallow_false_start_without_ql": false,
+      "deep_oversold_confirm_weak_with_ql": false,
+      "deep_oversold_confirm_window_bars": 2,
+      "deep_oversold_selective_positive_b1_c1_max": 15.3,
+      "deep_oversold_selective_shallow_c1_min": 12.0,
+      "deep_oversold_selective_shallow_b1_min": -0.025,
+      "deep_oversold_selective_mixed_c1_max": 10.2,
+      "deep_oversold_selective_mixed_require_no_ql": true,
+      "oversold_recovery_c1_low": 18.0,
+      "oversold_recovery_c1_high": 22.0,
+      "oversold_recovery_a1_min": -0.03,
+      "oversold_recovery_a1_max": 0.0,
+      "oversold_recovery_b1_min": -0.02,
+      "oversold_reversal_after_ql_block_c1_low": 23.0,
+      "oversold_reversal_after_ql_block_c1_high": 26.0,
+      "oversold_reversal_after_ql_block_b1_min": -0.12,
+      "oversold_reversal_after_ql_block_a1_min": -0.035,
+      "oversold_reversal_after_ql_entry_c1_low": 20.0,
+      "oversold_reversal_after_ql_entry_c1_high": 26.0,
+      "oversold_reversal_after_ql_entry_a1_min": -0.04,
+      "oversold_reversal_after_ql_entry_a1_max": 0.0,
+      "oversold_reversal_after_ql_entry_b1_min": -0.22,
+      "oversold_reversal_after_ql_entry_b1_max": 0.0,
+      "post_sell_rebound_block_high_c1": 22.0,
+      "post_sell_rebound_block_high_a1_min": -0.035,
+      "post_sell_rebound_block_high_b1_max": -0.07,
+      "post_sell_rebound_block_low_c1": 15.0,
+      "post_sell_rebound_block_low_a1_min": -0.04,
+      "post_sell_rebound_block_low_b1_max": -0.095,
+      "post_sell_rebound_entry1_c1_low": 18.0,
+      "post_sell_rebound_entry1_c1_high": 30.0,
+      "post_sell_rebound_entry1_a1_min": -0.045,
+      "post_sell_rebound_entry1_a1_max": 0.0,
+      "post_sell_rebound_entry1_b1_low": -0.09,
+      "post_sell_rebound_entry1_b1_high": -0.04,
+      "post_sell_rebound_entry2_c1_high": 19.0,
+      "post_sell_rebound_entry2_a1_min": -0.04,
+      "post_sell_rebound_entry2_a1_max": 0.0,
+      "post_sell_rebound_entry2_b1_low": -0.13,
+      "post_sell_rebound_entry2_b1_high": -0.09,
+      "predictive_b1_break_short_holding_days_max": 2,
+      "predictive_b1_break_short_a1_min": -0.02,
+      "predictive_b1_break_short_a1_max": 0.0,
+      "predictive_b1_break_short_b1_max": -0.13,
+      "predictive_b1_break_short_c1_low": 50.0,
+      "predictive_b1_break_short_c1_high": 70.0,
+      "predictive_b1_break_long_holding_days_min": 40,
+      "predictive_b1_break_long_max_c1": 80.0,
+      "predictive_b1_break_long_max_a1": 0.15,
+      "predictive_b1_break_long_max_b1": 0.3,
+      "predictive_b1_break_long_ql_days_max": 7,
+      "predictive_b1_break_long_a1_min": -0.02,
+      "predictive_b1_break_long_a1_max": 0.0,
+      "predictive_b1_break_long_b1_max": -0.12,
+      "predictive_b1_break_long_c1_low": 60.0,
+      "predictive_b1_break_long_c1_high": 65.0,
+      "enable_knife_take_profit_2_wait_ql": true
+    },
+    "deep_oversold_confirmation_v2": {
+      "disabled_rules": [],
+      "post_exit_confirmation_window_days": 10,
+      "aux_sell_same_side_once_per_cycle": true,
+      "aux_sell_duplicate_cooldown_days": 5,
+      "aux_sell_high_zone_kdj_only_block_c1": 85.0,
+      "aux_sell_high_zone_kdj_only_block_b1": -0.02,
+      "aux_sell_high_zone_warning_c1": 80.0,
+      "aux_sell_strong_break_c1": 60.0,
+      "aux_sell_strong_break_b1": -0.05,
+      "aux_sell_stronger_c1_delta": 8.0,
+      "aux_sell_stronger_b1_delta": 0.05,
+      "aux_sell_high_zone_rearm_c1_delta": 2.0,
+      "state_crash_followthrough_window_days": 5,
+      "state_crash_followthrough_repeat_cooldown_days": 4,
+      "state_crash_followthrough_c1_max": 80.0,
+      "state_crash_followthrough_a1_max": 0.01,
+      "state_crash_followthrough_b1_max": -0.15,
+      "glued_high_weak_rebound_high_c1": 68.0,
+      "glued_high_weak_rebound_high_b1": -0.08,
+      "glued_high_weak_rebound_mid_c1": 50.0,
+      "glued_high_weak_rebound_mid_b1": -0.15,
+      "glued_high_weak_rebound_ql_c1_low": 35.0,
+      "glued_high_weak_rebound_ql_c1_high": 55.0,
+      "glued_high_weak_rebound_ql_b1": -0.06,
+      "glued_high_weak_rebound_ql_a1": -0.013,
+      "glued_selective_hot_c1_min": 40.0,
+      "glued_selective_hot_c1_max": 75.0,
+      "glued_selective_hot_b1_min": 0.1,
+      "glued_selective_low_c1_min": 23.0,
+      "glued_selective_low_c1_max": 28.0,
+      "glued_selective_low_b1_max": 0.02,
+      "deep_oversold_filter1_c1_low": 13.0,
+      "deep_oversold_filter1_c1_high": 15.0,
+      "deep_oversold_filter1_a1_min": -0.04,
+      "deep_oversold_filter1_b1_max": -0.08,
+      "deep_oversold_filter2_c1_low": 13.0,
+      "deep_oversold_filter2_c1_high": 14.5,
+      "deep_oversold_filter2_a1_min": -0.04,
+      "deep_oversold_filter2_b1_min": -0.06,
+      "deep_oversold_entry_c1_max": 16.0,
+      "deep_oversold_entry_a1_min": -0.09,
+      "deep_oversold_entry_b1_min": -0.1,
+      "deep_oversold_shallow_ql_fallback": true,
+      "deep_oversold_positive_b1_fallback_a1_min": -0.02,
+      "deep_oversold_block_positive_b1_rebound": false,
+      "deep_oversold_block_shallow_false_start_without_ql": true,
+      "deep_oversold_confirm_weak_with_ql": true,
+      "deep_oversold_confirm_window_bars": 2,
+      "deep_oversold_selective_positive_b1_c1_max": 15.3,
+      "deep_oversold_selective_shallow_c1_min": 12.0,
+      "deep_oversold_selective_shallow_b1_min": -0.025,
+      "deep_oversold_selective_mixed_c1_max": 10.2,
+      "deep_oversold_selective_mixed_require_no_ql": true,
+      "oversold_recovery_c1_low": 18.0,
+      "oversold_recovery_c1_high": 22.0,
+      "oversold_recovery_a1_min": -0.03,
+      "oversold_recovery_a1_max": 0.0,
+      "oversold_recovery_b1_min": -0.02,
+      "oversold_reversal_after_ql_block_c1_low": 23.0,
+      "oversold_reversal_after_ql_block_c1_high": 26.0,
+      "oversold_reversal_after_ql_block_b1_min": -0.12,
+      "oversold_reversal_after_ql_block_a1_min": -0.035,
+      "oversold_reversal_after_ql_entry_c1_low": 20.0,
+      "oversold_reversal_after_ql_entry_c1_high": 26.0,
+      "oversold_reversal_after_ql_entry_a1_min": -0.04,
+      "oversold_reversal_after_ql_entry_a1_max": 0.0,
+      "oversold_reversal_after_ql_entry_b1_min": -0.22,
+      "oversold_reversal_after_ql_entry_b1_max": 0.0,
+      "post_sell_rebound_block_high_c1": 22.0,
+      "post_sell_rebound_block_high_a1_min": -0.035,
+      "post_sell_rebound_block_high_b1_max": -0.07,
+      "post_sell_rebound_block_low_c1": 15.0,
+      "post_sell_rebound_block_low_a1_min": -0.04,
+      "post_sell_rebound_block_low_b1_max": -0.095,
+      "post_sell_rebound_entry1_c1_low": 18.0,
+      "post_sell_rebound_entry1_c1_high": 30.0,
+      "post_sell_rebound_entry1_a1_min": -0.045,
+      "post_sell_rebound_entry1_a1_max": 0.0,
+      "post_sell_rebound_entry1_b1_low": -0.09,
+      "post_sell_rebound_entry1_b1_high": -0.04,
+      "post_sell_rebound_entry2_c1_high": 19.0,
+      "post_sell_rebound_entry2_a1_min": -0.04,
+      "post_sell_rebound_entry2_a1_max": 0.0,
+      "post_sell_rebound_entry2_b1_low": -0.13,
+      "post_sell_rebound_entry2_b1_high": -0.09,
+      "predictive_b1_break_short_holding_days_max": 2,
+      "predictive_b1_break_short_a1_min": -0.02,
+      "predictive_b1_break_short_a1_max": 0.0,
+      "predictive_b1_break_short_b1_max": -0.13,
+      "predictive_b1_break_short_c1_low": 50.0,
+      "predictive_b1_break_short_c1_high": 70.0,
+      "predictive_b1_break_long_holding_days_min": 40,
+      "predictive_b1_break_long_max_c1": 80.0,
+      "predictive_b1_break_long_max_a1": 0.15,
+      "predictive_b1_break_long_max_b1": 0.3,
+      "predictive_b1_break_long_ql_days_max": 7,
+      "predictive_b1_break_long_a1_min": -0.02,
+      "predictive_b1_break_long_a1_max": 0.0,
+      "predictive_b1_break_long_b1_max": -0.12,
+      "predictive_b1_break_long_c1_low": 60.0,
+      "predictive_b1_break_long_c1_high": 65.0,
+      "enable_knife_take_profit_2_wait_ql": true
+    },
+    "predictive_bridge_only": {
+      "disabled_rules": [
+        "predictive_b1_break_exit",
+        "predictive_error_reentry_buy"
+      ],
+      "post_exit_confirmation_window_days": 10,
+      "aux_sell_same_side_once_per_cycle": true,
+      "aux_sell_duplicate_cooldown_days": 5,
+      "aux_sell_high_zone_kdj_only_block_c1": 85.0,
+      "aux_sell_high_zone_kdj_only_block_b1": -0.02,
+      "aux_sell_high_zone_warning_c1": 80.0,
+      "aux_sell_strong_break_c1": 60.0,
+      "aux_sell_strong_break_b1": -0.05,
+      "aux_sell_stronger_c1_delta": 8.0,
+      "aux_sell_stronger_b1_delta": 0.05,
+      "aux_sell_high_zone_rearm_c1_delta": 2.0,
+      "state_crash_followthrough_window_days": 5,
+      "state_crash_followthrough_repeat_cooldown_days": 4,
+      "state_crash_followthrough_c1_max": 80.0,
+      "state_crash_followthrough_a1_max": 0.01,
+      "state_crash_followthrough_b1_max": -0.15,
+      "glued_high_weak_rebound_high_c1": 68.0,
+      "glued_high_weak_rebound_high_b1": -0.08,
+      "glued_high_weak_rebound_mid_c1": 50.0,
+      "glued_high_weak_rebound_mid_b1": -0.15,
+      "glued_high_weak_rebound_ql_c1_low": 35.0,
+      "glued_high_weak_rebound_ql_c1_high": 55.0,
+      "glued_high_weak_rebound_ql_b1": -0.06,
+      "glued_high_weak_rebound_ql_a1": -0.013,
+      "glued_selective_hot_c1_min": 40.0,
+      "glued_selective_hot_c1_max": 75.0,
+      "glued_selective_hot_b1_min": 0.1,
+      "glued_selective_low_c1_min": 23.0,
+      "glued_selective_low_c1_max": 28.0,
+      "glued_selective_low_b1_max": 0.02,
+      "deep_oversold_filter1_c1_low": 13.0,
+      "deep_oversold_filter1_c1_high": 15.0,
+      "deep_oversold_filter1_a1_min": -0.04,
+      "deep_oversold_filter1_b1_max": -0.08,
+      "deep_oversold_filter2_c1_low": 13.0,
+      "deep_oversold_filter2_c1_high": 14.5,
+      "deep_oversold_filter2_a1_min": -0.04,
+      "deep_oversold_filter2_b1_min": -0.06,
+      "deep_oversold_entry_c1_max": 16.0,
+      "deep_oversold_entry_a1_min": -0.09,
+      "deep_oversold_entry_b1_min": -0.1,
+      "deep_oversold_shallow_ql_fallback": true,
+      "deep_oversold_positive_b1_fallback_a1_min": -0.02,
+      "deep_oversold_block_positive_b1_rebound": false,
+      "deep_oversold_block_shallow_false_start_without_ql": false,
+      "deep_oversold_confirm_weak_with_ql": false,
+      "deep_oversold_confirm_window_bars": 2,
+      "deep_oversold_selective_positive_b1_c1_max": 15.3,
+      "deep_oversold_selective_shallow_c1_min": 12.0,
+      "deep_oversold_selective_shallow_b1_min": -0.025,
+      "deep_oversold_selective_mixed_c1_max": 10.2,
+      "deep_oversold_selective_mixed_require_no_ql": true,
+      "oversold_recovery_c1_low": 18.0,
+      "oversold_recovery_c1_high": 22.0,
+      "oversold_recovery_a1_min": -0.03,
+      "oversold_recovery_a1_max": 0.0,
+      "oversold_recovery_b1_min": -0.02,
+      "oversold_reversal_after_ql_block_c1_low": 23.0,
+      "oversold_reversal_after_ql_block_c1_high": 26.0,
+      "oversold_reversal_after_ql_block_b1_min": -0.12,
+      "oversold_reversal_after_ql_block_a1_min": -0.035,
+      "oversold_reversal_after_ql_entry_c1_low": 20.0,
+      "oversold_reversal_after_ql_entry_c1_high": 26.0,
+      "oversold_reversal_after_ql_entry_a1_min": -0.04,
+      "oversold_reversal_after_ql_entry_a1_max": 0.0,
+      "oversold_reversal_after_ql_entry_b1_min": -0.22,
+      "oversold_reversal_after_ql_entry_b1_max": 0.0,
+      "post_sell_rebound_block_high_c1": 22.0,
+      "post_sell_rebound_block_high_a1_min": -0.035,
+      "post_sell_rebound_block_high_b1_max": -0.07,
+      "post_sell_rebound_block_low_c1": 15.0,
+      "post_sell_rebound_block_low_a1_min": -0.04,
+      "post_sell_rebound_block_low_b1_max": -0.095,
+      "post_sell_rebound_entry1_c1_low": 18.0,
+      "post_sell_rebound_entry1_c1_high": 30.0,
+      "post_sell_rebound_entry1_a1_min": -0.045,
+      "post_sell_rebound_entry1_a1_max": 0.0,
+      "post_sell_rebound_entry1_b1_low": -0.09,
+      "post_sell_rebound_entry1_b1_high": -0.04,
+      "post_sell_rebound_entry2_c1_high": 19.0,
+      "post_sell_rebound_entry2_a1_min": -0.04,
+      "post_sell_rebound_entry2_a1_max": 0.0,
+      "post_sell_rebound_entry2_b1_low": -0.13,
+      "post_sell_rebound_entry2_b1_high": -0.09,
+      "predictive_b1_break_short_holding_days_max": 2,
+      "predictive_b1_break_short_a1_min": -0.02,
+      "predictive_b1_break_short_a1_max": 0.0,
+      "predictive_b1_break_short_b1_max": -0.13,
+      "predictive_b1_break_short_c1_low": 50.0,
+      "predictive_b1_break_short_c1_high": 70.0,
+      "predictive_b1_break_long_holding_days_min": 40,
+      "predictive_b1_break_long_max_c1": 80.0,
+      "predictive_b1_break_long_max_a1": 0.15,
+      "predictive_b1_break_long_max_b1": 0.3,
+      "predictive_b1_break_long_ql_days_max": 7,
+      "predictive_b1_break_long_a1_min": -0.02,
+      "predictive_b1_break_long_a1_max": 0.0,
+      "predictive_b1_break_long_b1_max": -0.12,
+      "predictive_b1_break_long_c1_low": 60.0,
+      "predictive_b1_break_long_c1_high": 65.0,
+      "enable_knife_take_profit_2_wait_ql": true
+    }
+  }
+}

+ 221 - 0
research/dragon/v2/dragon_weak_family_experiments.py

@@ -0,0 +1,221 @@
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pandas as pd
+
+from dragon_branch_configs import alpha_first_glued_refined_hot_cap_config
+from dragon_execution_common import apply_execution_model, summary
+from dragon_rc1_golden_baseline import _load_indicator_snapshot
+from dragon_shared import END_DATE, START_DATE, format_num, format_pct
+from dragon_strategy import DragonRuleEngine
+from dragon_strategy_config import StrategyConfig
+
+
+def _bounded_trades(trades: pd.DataFrame) -> pd.DataFrame:
+    return trades[
+        (trades["buy_date"] >= START_DATE)
+        & (trades["buy_date"] <= END_DATE)
+        & (trades["sell_date"] >= START_DATE)
+        & (trades["sell_date"] <= END_DATE)
+    ].copy()
+
+
+def _trade_key(frame: pd.DataFrame) -> pd.Series:
+    return (
+        frame["buy_date"].astype(str)
+        + "|"
+        + frame["sell_date"].astype(str)
+        + "|"
+        + frame["buy_reason"].astype(str)
+        + "|"
+        + frame["sell_reason"].astype(str)
+    )
+
+
+def _trade_diff(baseline: pd.DataFrame, candidate: pd.DataFrame, label: str) -> pd.DataFrame:
+    base = baseline.copy()
+    cand = candidate.copy()
+    base["trade_key"] = _trade_key(base)
+    cand["trade_key"] = _trade_key(cand)
+
+    removed = base.loc[~base["trade_key"].isin(cand["trade_key"])].copy()
+    removed["diff_type"] = "REMOVED"
+    added = cand.loc[~cand["trade_key"].isin(base["trade_key"])].copy()
+    added["diff_type"] = "ADDED"
+
+    diff = pd.concat([removed, added], ignore_index=True)
+    diff.insert(0, "experiment", label)
+    return diff
+
+
+def _add_execution_prices(trades: pd.DataFrame, indicators: pd.DataFrame) -> pd.DataFrame:
+    result = trades.copy()
+    ind = indicators.copy().sort_values("date").reset_index(drop=True)
+    ind["date"] = pd.to_datetime(ind["date"])
+    open_col = "open" if "open" in ind.columns else "close"
+    close_col = "close"
+    next_by_date: dict[pd.Timestamp, pd.Series] = {}
+    for idx in range(len(ind) - 1):
+        next_by_date[pd.Timestamp(ind.iloc[idx]["date"])] = ind.iloc[idx + 1]
+
+    next_open_entry: list[float] = []
+    next_open_exit: list[float] = []
+    next_close_entry: list[float] = []
+    next_close_exit: list[float] = []
+    same_close_entry: list[float] = []
+    same_close_exit: list[float] = []
+
+    for _, row in result.iterrows():
+        buy_date = pd.Timestamp(row["buy_date"])
+        sell_date = pd.Timestamp(row["sell_date"])
+        buy_next = next_by_date.get(buy_date)
+        sell_next = next_by_date.get(sell_date)
+        next_open_entry.append(float("nan") if buy_next is None else float(buy_next[open_col]))
+        next_open_exit.append(float("nan") if sell_next is None else float(sell_next[open_col]))
+        next_close_entry.append(float("nan") if buy_next is None else float(buy_next[close_col]))
+        next_close_exit.append(float("nan") if sell_next is None else float(sell_next[close_col]))
+        same_close_entry.append(float(row["buy_price"]))
+        same_close_exit.append(float(row["sell_price"]))
+
+    result["exec_same_close_entry"] = same_close_entry
+    result["exec_same_close_exit"] = same_close_exit
+    result["exec_next_open_entry"] = next_open_entry
+    result["exec_next_open_exit"] = next_open_exit
+    result["exec_next_close_entry"] = next_close_entry
+    result["exec_next_close_exit"] = next_close_exit
+    result["entry_family"] = result["buy_reason_family"].astype(str)
+    return result
+
+
+def _run_variant(indexed: pd.DataFrame, config: StrategyConfig) -> pd.DataFrame:
+    _, trades = DragonRuleEngine(config=config).run(indexed)
+    return _bounded_trades(trades)
+
+
+def main() -> None:
+    base_dir = Path(__file__).resolve().parent
+    indexed, source = _load_indicator_snapshot(base_dir)
+    indicators = indexed.reset_index(drop=False).rename(columns={"index": "date"})
+
+    baseline_cfg = alpha_first_glued_refined_hot_cap_config()
+    deep_oversold_cfg = baseline_cfg.with_updates(
+        deep_oversold_confirm_weak_with_ql=True,
+        deep_oversold_block_shallow_false_start_without_ql=True,
+        deep_oversold_selective_positive_b1_c1_max=15.3,
+    )
+    predictive_bridge_only_cfg = baseline_cfg.with_updates(
+        disabled_rules=frozenset(set(baseline_cfg.disabled_rules) | {"predictive_b1_break_exit", "predictive_error_reentry_buy"}),
+    )
+
+    variants: dict[str, StrategyConfig] = {
+        "rc1_baseline": baseline_cfg,
+        "deep_oversold_confirmation_v2": deep_oversold_cfg,
+        "predictive_bridge_only": predictive_bridge_only_cfg,
+    }
+
+    trades_by_variant = {name: _run_variant(indexed, cfg) for name, cfg in variants.items()}
+
+    summary_rows: list[dict[str, object]] = []
+    for name, trades in trades_by_variant.items():
+        returns = trades["return_pct"].astype(float)
+        summary_rows.append(
+            {
+                "experiment": name,
+                "trades": int(len(trades)),
+                "win_rate": float((returns > 0).mean()) if not trades.empty else float("nan"),
+                "avg_return": float(returns.mean()) if not trades.empty else float("nan"),
+                "median_return": float(returns.median()) if not trades.empty else float("nan"),
+                "compounded_return": float((1.0 + returns).prod() - 1.0) if not trades.empty else float("nan"),
+            }
+        )
+
+    summary_df = pd.DataFrame(summary_rows).sort_values("experiment").reset_index(drop=True)
+    summary_df.to_csv(base_dir / "dragon_weak_family_experiment_summary.csv", index=False, encoding="utf-8-sig")
+
+    baseline_trades = trades_by_variant["rc1_baseline"]
+    diff_rows = []
+    for name, trades in trades_by_variant.items():
+        if name == "rc1_baseline":
+            continue
+        diff_rows.append(_trade_diff(baseline_trades, trades, label=name))
+    diff_df = pd.concat(diff_rows, ignore_index=True) if diff_rows else pd.DataFrame()
+    if not diff_df.empty:
+        diff_df.to_csv(base_dir / "dragon_weak_family_trade_diff.csv", index=False, encoding="utf-8-sig")
+
+    stress_rows: list[dict[str, object]] = []
+    for name, trades in trades_by_variant.items():
+        enriched = _add_execution_prices(trades, indicators)
+        for model in ["same_close", "next_open", "next_close"]:
+            model_trades = apply_execution_model(enriched, model, 0.0)
+            stress_rows.append(summary(name, model_trades))
+            if model == "next_open":
+                stressed = apply_execution_model(enriched, model, 20.0)
+                stress_rows.append(summary(name, stressed))
+
+    stress_df = pd.DataFrame(stress_rows).rename(columns={"branch": "experiment"})
+    stress_df = stress_df.sort_values(["experiment", "execution_model", "cost_bps_side"]).reset_index(drop=True)
+    stress_df.to_csv(base_dir / "dragon_weak_family_execution_stress.csv", index=False, encoding="utf-8-sig")
+
+    (base_dir / "dragon_weak_family_experiment_config_snapshot.json").write_text(
+        json.dumps(
+            {
+                "indicator_source": source,
+                "evaluation_window": {"start": START_DATE, "end": END_DATE},
+                "variants": {name: cfg.__dict__ for name, cfg in variants.items()},
+            },
+            ensure_ascii=False,
+            indent=2,
+            default=list,
+        ),
+        encoding="utf-8",
+    )
+
+    lines = [
+        "# Dragon Weak Family Experiment Review",
+        "",
+        f"- indicator source: `{source}`",
+        f"- window: `{START_DATE} -> {END_DATE}`",
+        "",
+        "## Summary",
+    ]
+    for _, row in summary_df.iterrows():
+        lines.append(
+            "- "
+            f"{row['experiment']}: "
+            f"trades `{int(row['trades'])}`, "
+            f"win_rate `{format_pct(float(row['win_rate']))}`, "
+            f"avg_return `{format_pct(float(row['avg_return']))}`, "
+            f"compounded `{format_pct(float(row['compounded_return']))}`"
+        )
+
+    if not diff_df.empty:
+        lines.extend(["", "## Added/Removed Trades vs RC1"])
+        for experiment, group in diff_df.groupby("experiment", dropna=False):
+            added = int((group["diff_type"] == "ADDED").sum())
+            removed = int((group["diff_type"] == "REMOVED").sum())
+            avg_added = float(group.loc[group["diff_type"] == "ADDED", "return_pct"].mean()) if added > 0 else float("nan")
+            avg_removed = float(group.loc[group["diff_type"] == "REMOVED", "return_pct"].mean()) if removed > 0 else float("nan")
+            lines.append(
+                "- "
+                f"{experiment}: added `{added}` (avg `{format_pct(avg_added)}`), "
+                f"removed `{removed}` (avg `{format_pct(avg_removed)}`)"
+            )
+
+    lines.extend(["", "## Execution Stress (next_open + 20 bps/side)"])
+    stress_20 = stress_df[(stress_df["execution_model"] == "next_open") & (stress_df["cost_bps_side"] == 20.0)]
+    for _, row in stress_20.iterrows():
+        lines.append(
+            "- "
+            f"{row['experiment']}: "
+            f"avg_return `{format_pct(float(row['avg_return']))}`, "
+            f"PF `{format_num(float(row['profit_factor']))}`, "
+            f"max_dd `{format_pct(float(row['max_drawdown']))}`"
+        )
+
+    (base_dir / "dragon_weak_family_experiment.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
+
+
+if __name__ == "__main__":
+    main()

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

@@ -42,3 +42,36 @@
 - `specs/weak-family-research-governance/spec.md`
 - `specs/golden-regression-governance/spec.md`
 - Updated `USER.md` to persist preference: future optimization/governance推进 should use OpenSpec workflow.
+- User requested full uninterrupted execution ("全部执行完,再找我,中途不停"), and this round followed that mode.
+- Completed OpenSpec task execution for `rc1-layered-governed-optimization`:
+- added layered modules `dragon_rule_engine_v2.py`, `dragon_rules_core.py`, `dragon_rules_secondary.py`, `dragon_rules_bridge.py`.
+- `dragon_strategy.py` kept as compatibility facade and migrated high-risk control paths to structured fields:
+- `entry_reason_layer/family/code`, `last_real_sell_reason_layer/family/code`
+- bridge flags for predictive and pending deep-oversold chains.
+- Added compatibility and path-regression tests:
+- `tests/test_no_silent_path_change.py`
+- `tests/test_bridge_chain_regression.py`
+- `tests/test_deep_oversold_pending_paths.py`
+- Full test suite now: `8` tests passed.
+- Added layered attribution/report artifacts:
+- `dragon_layered_pnl_attribution.py`
+- `dragon_layered_pnl_attribution.csv`
+- `dragon_layered_family_pnl_attribution.csv`
+- `dragon_layered_entry_pnl_attribution.csv`
+- `dragon_layered_exit_pnl_attribution.csv`
+- `dragon_layered_pnl_attribution.md`
+- Added migration audit note:
+- `dragon_migration_audit.md`
+- Executed weak-family experiment pack:
+- created branch `alpha_next_weak_family_cleanup`
+- added `dragon_weak_family_experiments.py`
+- generated:
+- `dragon_weak_family_experiment_summary.csv`
+- `dragon_weak_family_trade_diff.csv`
+- `dragon_weak_family_execution_stress.csv`
+- `dragon_weak_family_experiment_config_snapshot.json`
+- `dragon_weak_family_experiment.md`
+- Added acceptance report:
+- `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`

+ 64 - 0
research/dragon/v2/openspec/changes/rc1-layered-governed-optimization/acceptance-summary.md

@@ -0,0 +1,64 @@
+# RC1 Layered Governed Optimization - Acceptance Summary
+
+Date: 2026-04-09
+
+## 1) Guardrail Results
+
+- RC1 golden manifest regenerated successfully.
+- `trade_count = 91`, `event_count = 272`, `win_rate = 52.75%`, `avg_return = 3.42%`.
+- core hash guardrails unchanged:
+  - events: `8965d1b539a998d7d0aff04432aa2a47cf30ee40df013b9d8b7eb66a3d50a331`
+  - trades: `1298be56b0898266b0b854d62a979c00c20b01629393c82bb8c804faf852cb97`
+- layer attribution mapping audit remains clean: `no unknown reason mapping`.
+
+## 2) Refactor Verification
+
+- Layered orchestration modules added:
+  - `dragon_rule_engine_v2.py`
+  - `dragon_rules_core.py`
+  - `dragon_rules_secondary.py`
+  - `dragon_rules_bridge.py`
+- `dragon_strategy.py` remains compatibility facade.
+- Structured control-state fields added and wired:
+  - `entry_reason_layer/family/code`
+  - `last_real_sell_reason_layer/family/code`
+  - bridge flags for predictive and pending deep-oversold chains.
+- High-risk string-coupled checks migrated to structured fields in predictive/pending paths.
+
+## 3) Weak-Family Experiment Pack
+
+- Dedicated branch created: `alpha_next_weak_family_cleanup`.
+- Experiment outputs generated:
+  - `dragon_weak_family_experiment_summary.csv`
+  - `dragon_weak_family_trade_diff.csv`
+  - `dragon_weak_family_execution_stress.csv`
+  - `dragon_weak_family_experiment.md`
+  - `dragon_weak_family_experiment_config_snapshot.json`
+- Added/removed attribution produced for each experiment vs RC1 baseline.
+- Execution stress replay (`same_close`, `next_open`, `next_close`, plus `next_open + 20 bps/side`) completed.
+
+## 4) Test And Pipeline Gates
+
+Executed and passed:
+
+- `py -3 -m unittest discover -s tests -v`
+  - total: `8` tests, all `OK`.
+- `py -3 dragon_daily_signal_pipeline.py --as-of 2026-04-08`
+  - completed successfully (smoke gate pass).
+
+New test coverage:
+
+- `tests/test_no_silent_path_change.py`
+- `tests/test_bridge_chain_regression.py`
+- `tests/test_deep_oversold_pending_paths.py`
+
+## 5) Reporting Additions
+
+- Layered attribution outputs generated:
+  - `dragon_layered_pnl_attribution.csv`
+  - `dragon_layered_family_pnl_attribution.csv`
+  - `dragon_layered_entry_pnl_attribution.csv`
+  - `dragon_layered_exit_pnl_attribution.csv`
+  - `dragon_layered_pnl_attribution.md`
+- Migration audit note added: `dragon_migration_audit.md`.
+

+ 19 - 19
research/dragon/v2/openspec/changes/rc1-layered-governed-optimization/tasks.md

@@ -8,31 +8,31 @@
 
 ## 2. Layered Rule-Engine Refactor
 
-- [ ] 2.1 Add `dragon_rule_engine_v2.py` orchestrator with explicit `core -> secondary -> bridge` evaluation order.
-- [ ] 2.2 Extract core rules into `dragon_rules_core.py` without behavior drift.
-- [ ] 2.3 Extract secondary rules into `dragon_rules_secondary.py` without behavior drift.
-- [ ] 2.4 Extract bridge/compatibility rules into `dragon_rules_bridge.py` without behavior drift.
-- [ ] 2.5 Keep `dragon_strategy.py` as compatibility facade and rerun golden regression.
+- [x] 2.1 Add `dragon_rule_engine_v2.py` orchestrator with explicit `core -> secondary -> bridge` evaluation order.
+- [x] 2.2 Extract core rules into `dragon_rules_core.py` without behavior drift.
+- [x] 2.3 Extract secondary rules into `dragon_rules_secondary.py` without behavior drift.
+- [x] 2.4 Extract bridge/compatibility rules into `dragon_rules_bridge.py` without behavior drift.
+- [x] 2.5 Keep `dragon_strategy.py` as compatibility facade and rerun golden regression.
 
 ## 3. Structured State Migration
 
-- [ ] 3.1 Add structured context fields (`entry_*`, `last_real_sell_*`, bridge flags).
-- [ ] 3.2 Migrate high-risk string-coupled control paths (`predictive_*`, pending chains) to structured fields.
-- [ ] 3.3 Block new direct prefix-based control checks in refactored modules.
-- [ ] 3.4 Verify no-silent-path-change for compatibility mode.
+- [x] 3.1 Add structured context fields (`entry_*`, `last_real_sell_*`, bridge flags).
+- [x] 3.2 Migrate high-risk string-coupled control paths (`predictive_*`, pending chains) to structured fields.
+- [x] 3.3 Block new direct prefix-based control checks in refactored modules.
+- [x] 3.4 Verify no-silent-path-change for compatibility mode.
 
 ## 4. Weak-Family Branch Experiments
 
-- [ ] 4.1 Create dedicated research branch for weak-family optimization (do not modify RC1 directly).
-- [ ] 4.2 Implement subtype/confirmation redesign for `deep_oversold_*` in experiment branch.
-- [ ] 4.3 Isolate `predictive_*` as bridge-only family and evaluate structure-preservation cost.
-- [ ] 4.4 Run added/removed trade attribution pack versus RC1 baseline.
-- [ ] 4.5 Run execution-stress replay before any promotion recommendation.
+- [x] 4.1 Create dedicated research branch for weak-family optimization (do not modify RC1 directly).
+- [x] 4.2 Implement subtype/confirmation redesign for `deep_oversold_*` in experiment branch.
+- [x] 4.3 Isolate `predictive_*` as bridge-only family and evaluate structure-preservation cost.
+- [x] 4.4 Run added/removed trade attribution pack versus RC1 baseline.
+- [x] 4.5 Run execution-stress replay before any promotion recommendation.
 
 ## 5. Test And Reporting Expansion
 
-- [ ] 5.1 Add `test_no_silent_path_change.py` for compatibility-preserving refactors.
-- [ ] 5.2 Add bridge chain regression tests for split-chain behavior.
-- [ ] 5.3 Add weak-family path tests for subtype/pending transitions.
-- [ ] 5.4 Add layered PnL attribution output (`core/secondary/bridge`) and migration audit note.
-- [ ] 5.5 Re-run full suite and publish acceptance summary.
+- [x] 5.1 Add `test_no_silent_path_change.py` for compatibility-preserving refactors.
+- [x] 5.2 Add bridge chain regression tests for split-chain behavior.
+- [x] 5.3 Add weak-family path tests for subtype/pending transitions.
+- [x] 5.4 Add layered PnL attribution output (`core/secondary/bridge`) and migration audit note.
+- [x] 5.5 Re-run full suite and publish acceptance summary.

+ 50 - 0
research/dragon/v2/tests/test_bridge_chain_regression.py

@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+import unittest
+from pathlib import Path
+
+import pandas as pd
+
+from dragon_branch_configs import alpha_first_glued_refined_hot_cap_config
+from dragon_rc1_golden_baseline import _load_indicator_snapshot
+from dragon_shared import END_DATE, START_DATE
+from dragon_strategy import DragonRuleEngine
+
+
+class TestBridgeChainRegression(unittest.TestCase):
+    def setUp(self) -> None:
+        self.base_dir = Path(__file__).resolve().parents[1]
+        indexed, _ = _load_indicator_snapshot(self.base_dir)
+        events, _ = DragonRuleEngine(config=alpha_first_glued_refined_hot_cap_config()).run(indexed)
+        self.events = events[(events["date"] >= START_DATE) & (events["date"] <= END_DATE)].copy()
+        self.events["date"] = pd.to_datetime(self.events["date"])
+
+    def test_predictive_reentry_requires_recent_predictive_exit(self) -> None:
+        reentries = self.events[
+            (self.events["side"] == "BUY")
+            & (self.events["layer"] == "real_trade")
+            & (self.events["reason"] == "predictive_error_reentry_buy")
+        ].copy()
+        self.assertGreaterEqual(len(reentries), 1)
+
+        sells = self.events[
+            (self.events["side"] == "SELL")
+            & (self.events["layer"] == "real_trade")
+        ][["date", "reason"]].copy()
+
+        for _, buy_row in reentries.iterrows():
+            prior = sells[sells["date"] < buy_row["date"]].sort_values("date")
+            self.assertFalse(prior.empty)
+            last_sell = prior.iloc[-1]
+            self.assertEqual(str(last_sell["reason"]), "predictive_b1_break_exit")
+            self.assertLessEqual((buy_row["date"] - last_sell["date"]).days, 3)
+
+    def test_predictive_events_stay_in_bridge_family(self) -> None:
+        predictive_events = self.events[self.events["reason"].isin(["predictive_b1_break_exit", "predictive_error_reentry_buy"])].copy()
+        self.assertGreaterEqual(len(predictive_events), 2)
+        self.assertTrue((predictive_events["reason_layer"] == "bridge").all())
+        self.assertTrue((predictive_events["reason_family"] == "predictive_break").all())
+
+
+if __name__ == "__main__":
+    unittest.main()

+ 55 - 0
research/dragon/v2/tests/test_deep_oversold_pending_paths.py

@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+import unittest
+from datetime import datetime
+
+import pandas as pd
+
+from dragon_strategy import DragonRuleEngine
+
+
+class TestDeepOversoldPendingPaths(unittest.TestCase):
+    def _row(self, **kwargs) -> pd.Series:
+        base = {
+            "close": 100.0,
+            "a1": -0.03,
+            "b1": -0.05,
+            "c1": 14.0,
+            "kdj_buy": False,
+            "kdj_sell": False,
+            "ql_buy": False,
+            "ql_sell": False,
+        }
+        base.update(kwargs)
+        row = pd.Series(base)
+        row.name = pd.Timestamp(datetime(2026, 1, 2))
+        return row
+
+    def test_pending_confirmation_emits_confirmed_buy(self) -> None:
+        engine = DragonRuleEngine()
+        engine.context.pending_deep_oversold_subtype = "shallow_false_start"
+        engine.context.pending_deep_oversold_origin_date = datetime(2026, 1, 1).date()
+        engine.context.pending_deep_oversold_bars_waited = 1
+        engine.context.bridge_pending_deep_oversold_active = True
+        row = self._row(ql_buy=True)
+
+        action, reason = engine._pending_deep_oversold_decision(row)
+        self.assertEqual(action, "BUY")
+        self.assertEqual(reason, "deep_oversold_rebound_buy:confirmed_shallow_false_start")
+        self.assertFalse(engine.context.bridge_pending_deep_oversold_active)
+
+    def test_pending_is_cleared_on_sell_signal(self) -> None:
+        engine = DragonRuleEngine()
+        engine.context.pending_deep_oversold_subtype = "mixed_oversold"
+        engine.context.pending_deep_oversold_origin_date = datetime(2026, 1, 1).date()
+        engine.context.pending_deep_oversold_bars_waited = 1
+        engine.context.bridge_pending_deep_oversold_active = True
+        row = self._row(kdj_sell=True)
+
+        action, reason = engine._pending_deep_oversold_decision(row)
+        self.assertEqual((action, reason), ("NONE", ""))
+        self.assertFalse(engine.context.bridge_pending_deep_oversold_active)
+
+
+if __name__ == "__main__":
+    unittest.main()

+ 55 - 0
research/dragon/v2/tests/test_no_silent_path_change.py

@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+import unittest
+from pathlib import Path
+
+from dragon_branch_configs import alpha_first_glued_refined_hot_cap_config
+from dragon_rc1_golden_baseline import EVENTS_CORE_COLUMNS, TRADES_CORE_COLUMNS, _df_sha256, _load_indicator_snapshot
+from dragon_shared import END_DATE, START_DATE
+from dragon_rule_engine_v2 import LayeredDragonRuleEngine
+from dragon_strategy import DragonRuleEngine
+
+
+class TestNoSilentPathChange(unittest.TestCase):
+    def setUp(self) -> None:
+        self.base_dir = Path(__file__).resolve().parents[1]
+        self.config = alpha_first_glued_refined_hot_cap_config()
+
+    def test_layered_orchestrator_matches_legacy_core_paths(self) -> None:
+        indexed, _ = _load_indicator_snapshot(self.base_dir)
+
+        legacy_events, legacy_trades = DragonRuleEngine(config=self.config).run(indexed)
+        layered_events, layered_trades = LayeredDragonRuleEngine(config=self.config).run(indexed)
+
+        legacy_events = legacy_events[(legacy_events["date"] >= START_DATE) & (legacy_events["date"] <= END_DATE)].copy()
+        layered_events = layered_events[(layered_events["date"] >= START_DATE) & (layered_events["date"] <= END_DATE)].copy()
+        legacy_trades = legacy_trades[
+            (legacy_trades["buy_date"] >= START_DATE)
+            & (legacy_trades["buy_date"] <= END_DATE)
+            & (legacy_trades["sell_date"] >= START_DATE)
+            & (legacy_trades["sell_date"] <= END_DATE)
+        ].copy()
+        layered_trades = layered_trades[
+            (layered_trades["buy_date"] >= START_DATE)
+            & (layered_trades["buy_date"] <= END_DATE)
+            & (layered_trades["sell_date"] >= START_DATE)
+            & (layered_trades["sell_date"] <= END_DATE)
+        ].copy()
+
+        legacy_events.sort_values(["date", "side", "layer", "reason"], inplace=True)
+        layered_events.sort_values(["date", "side", "layer", "reason"], inplace=True)
+        legacy_trades.sort_values(["buy_date", "sell_date", "buy_reason", "sell_reason"], inplace=True)
+        layered_trades.sort_values(["buy_date", "sell_date", "buy_reason", "sell_reason"], inplace=True)
+
+        self.assertEqual(
+            _df_sha256(legacy_events[EVENTS_CORE_COLUMNS]),
+            _df_sha256(layered_events[EVENTS_CORE_COLUMNS]),
+        )
+        self.assertEqual(
+            _df_sha256(legacy_trades[TRADES_CORE_COLUMNS]),
+            _df_sha256(layered_trades[TRADES_CORE_COLUMNS]),
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()