dragon_execution_runtime.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. from __future__ import annotations
  2. from typing import Any, Optional
  3. import pandas as pd
  4. from dragon_rule_catalog import classify_entry_reason, classify_exit_reason
  5. def _event_payload(row: pd.Series, side: str, layer: str, reason: str) -> dict[str, object]:
  6. return {
  7. "date": row.name.date().isoformat(),
  8. "side": side,
  9. "layer": layer,
  10. "reason": reason,
  11. "close": float(row["close"]),
  12. "a1": float(row["a1"]),
  13. "b1": float(row["b1"]),
  14. "c1": float(row["c1"]),
  15. "kdj_buy": bool(row["kdj_buy"]),
  16. "kdj_sell": bool(row["kdj_sell"]),
  17. "ql_buy": bool(row["ql_buy"]),
  18. "ql_sell": bool(row["ql_sell"]),
  19. }
  20. def _trade_payload(engine: Any, row: pd.Series, reason: str) -> dict[str, object]:
  21. return {
  22. "buy_date": engine.context.entry_date.isoformat() if engine.context.entry_date else "",
  23. "buy_price": engine.context.entry_price,
  24. "buy_reason": engine.context.entry_reason,
  25. "sell_date": row.name.date().isoformat(),
  26. "sell_price": float(row["close"]),
  27. "sell_reason": reason,
  28. "holding_days": engine._holding_days(row.name.date()),
  29. "return_pct": (
  30. float(row["close"]) / engine.context.entry_price - 1
  31. if engine.context.entry_price
  32. else None
  33. ),
  34. }
  35. def _enrich_reason_metadata(engine: Any, events_df: pd.DataFrame, trades_df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
  36. if not events_df.empty:
  37. event_decisions = [
  38. engine._build_decision(
  39. side=str(row["side"]),
  40. layer=str(row["layer"]),
  41. reason=str(row["reason"]),
  42. )
  43. for _, row in events_df.iterrows()
  44. ]
  45. events_df["reason_layer"] = [d.reason.layer.value if d.reason is not None else "unknown" for d in event_decisions]
  46. events_df["reason_family"] = [d.reason.family.value if d.reason is not None else "unknown" for d in event_decisions]
  47. events_df["reason_code"] = [d.reason.code if d.reason is not None else "" for d in event_decisions]
  48. if not trades_df.empty:
  49. buy_meta = trades_df["buy_reason"].map(classify_entry_reason)
  50. sell_meta = trades_df["sell_reason"].map(classify_exit_reason)
  51. trades_df["buy_reason_layer"] = [meta.layer.value for meta in buy_meta]
  52. trades_df["buy_reason_family"] = [meta.family.value for meta in buy_meta]
  53. trades_df["buy_reason_code"] = [meta.code for meta in buy_meta]
  54. trades_df["sell_reason_layer"] = [meta.layer.value for meta in sell_meta]
  55. trades_df["sell_reason_family"] = [meta.family.value for meta in sell_meta]
  56. trades_df["sell_reason_code"] = [meta.code for meta in sell_meta]
  57. return events_df, trades_df
  58. def run_compat_execution(engine: Any, df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
  59. """
  60. Execute the legacy-compatible strategy loop.
  61. The engine object is expected to provide the same internal hooks as
  62. DragonRuleEngine. This keeps the behavior path unchanged while physically
  63. decoupling execution runtime from strategy rule declarations.
  64. """
  65. engine.context = engine.context.__class__()
  66. events: list[dict[str, object]] = []
  67. trades: list[dict[str, object]] = []
  68. prev_row: Optional[pd.Series] = None
  69. for _, row in df.iterrows():
  70. engine._record_cross_counters(row)
  71. engine._update_position_counters(row)
  72. engine._update_pending_states(row)
  73. just_bought = False
  74. if (not engine.context.in_position) or bool(row["kdj_buy"] or row["ql_buy"]):
  75. action, reason = engine._buy_decision(row, prev_row)
  76. if action == "BUY":
  77. engine._post_real_buy(row, reason)
  78. just_bought = True
  79. events.append(_event_payload(row, side="BUY", layer="real_trade", reason=reason))
  80. elif action == "AUX_BUY":
  81. engine._post_aux_buy(row)
  82. events.append(_event_payload(row, side="BUY", layer="aux_signal", reason=reason))
  83. state_aux_sell_candidate = (not engine.context.in_position) and engine._should_emit_state_aux_sell(row)
  84. if not just_bought and (engine.context.in_position or bool(row["kdj_sell"] or row["ql_sell"]) or state_aux_sell_candidate):
  85. if engine.context.in_position and bool(row["kdj_sell"] or row["ql_sell"]):
  86. engine.context.sell_signal_count += 1
  87. if bool(row["kdj_sell"]):
  88. engine.context.kdj_sell_signal_count += 1
  89. if bool(row["ql_sell"]):
  90. engine.context.ql_sell_signal_count += 1
  91. if float(row["b1"]) < 0:
  92. engine.context.b1_negative_sell_count += 1
  93. action, reason = engine._sell_decision(row, prev_row)
  94. if action == "SELL":
  95. trades.append(_trade_payload(engine=engine, row=row, reason=reason))
  96. engine.context.first_exit_checked = True
  97. events.append(_event_payload(row, side="SELL", layer="real_trade", reason=reason))
  98. engine._post_real_sell(row, reason)
  99. elif action == "AUX_SELL":
  100. if engine.context.in_position:
  101. engine.context.first_exit_checked = True
  102. engine._post_aux_sell(row, reason)
  103. events.append(_event_payload(row, side="SELL", layer="aux_signal", reason=reason))
  104. prev_row = row
  105. events_df = pd.DataFrame(events)
  106. trades_df = pd.DataFrame(trades)
  107. return _enrich_reason_metadata(engine=engine, events_df=events_df, trades_df=trades_df)