dragon_rule_engine_v2.py 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. from __future__ import annotations
  2. from dataclasses import dataclass
  3. from typing import Optional
  4. import pandas as pd
  5. from dragon_decision_types import StrategyDecision
  6. from dragon_reason_types import RuleLayer
  7. from dragon_rules_bridge import BridgeRuleLayer
  8. from dragon_rules_core import CoreRuleLayer
  9. from dragon_rules_secondary import SecondaryRuleLayer
  10. from dragon_strategy import DragonRuleEngine
  11. from dragon_strategy_config import StrategyConfig
  12. @dataclass(frozen=True)
  13. class LayerRoutingResult:
  14. decision: StrategyDecision
  15. routed_layer: str
  16. class LayeredDragonRuleEngine:
  17. """
  18. Compatibility-first layered orchestrator.
  19. The current behavior path is still produced by the legacy DragonRuleEngine.
  20. This orchestrator adds explicit core -> secondary -> bridge routing semantics
  21. on top of legacy decisions without changing the trading path.
  22. """
  23. def __init__(self, config: Optional[StrategyConfig] = None):
  24. self.config = config or StrategyConfig()
  25. self.legacy = DragonRuleEngine(config=self.config)
  26. self.layers = (
  27. CoreRuleLayer(),
  28. SecondaryRuleLayer(),
  29. BridgeRuleLayer(),
  30. )
  31. def _route_decision(self, side: str, event_layer: str, reason: str) -> LayerRoutingResult:
  32. for layer in self.layers:
  33. routed = layer.route(side=side, event_layer=event_layer, reason=reason)
  34. if routed is not None:
  35. return LayerRoutingResult(decision=routed, routed_layer=layer.layer.value)
  36. fallback = self.legacy._build_decision(side=side, layer=event_layer, reason=reason)
  37. return LayerRoutingResult(decision=fallback, routed_layer=RuleLayer.UNKNOWN.value)
  38. def _enrich_events_with_layer_routing(self, events: pd.DataFrame) -> pd.DataFrame:
  39. if events.empty:
  40. return events
  41. rows = [
  42. self._route_decision(
  43. side=str(row["side"]),
  44. event_layer=str(row["layer"]),
  45. reason=str(row["reason"]),
  46. )
  47. for _, row in events.iterrows()
  48. ]
  49. enriched = events.copy()
  50. enriched["orchestrated_layer"] = [r.routed_layer for r in rows]
  51. if "reason_layer" not in enriched.columns:
  52. enriched["reason_layer"] = [r.decision.reason.layer.value if r.decision.reason else "unknown" for r in rows]
  53. enriched["reason_family"] = [r.decision.reason.family.value if r.decision.reason else "unknown" for r in rows]
  54. enriched["reason_code"] = [r.decision.reason.code if r.decision.reason else "" for r in rows]
  55. return enriched
  56. def run(self, df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
  57. events, trades = self.legacy.run(df)
  58. events = self._enrich_events_with_layer_routing(events)
  59. if not trades.empty and "orchestrated_entry_layer" not in trades.columns:
  60. trades = trades.copy()
  61. trades["orchestrated_entry_layer"] = trades.get("buy_reason_layer", pd.Series(["unknown"] * len(trades))).astype(str)
  62. trades["orchestrated_exit_layer"] = trades.get("sell_reason_layer", pd.Series(["unknown"] * len(trades))).astype(str)
  63. return events, trades