| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- from __future__ import annotations
- from typing import Any, Optional
- import pandas as pd
- from dragon_rule_catalog import classify_entry_reason, classify_exit_reason
- def _event_payload(row: pd.Series, side: str, layer: str, reason: str) -> dict[str, object]:
- return {
- "date": row.name.date().isoformat(),
- "side": side,
- "layer": layer,
- "reason": reason,
- "close": float(row["close"]),
- "a1": float(row["a1"]),
- "b1": float(row["b1"]),
- "c1": float(row["c1"]),
- "kdj_buy": bool(row["kdj_buy"]),
- "kdj_sell": bool(row["kdj_sell"]),
- "ql_buy": bool(row["ql_buy"]),
- "ql_sell": bool(row["ql_sell"]),
- }
- def _trade_payload(engine: Any, row: pd.Series, reason: str) -> dict[str, object]:
- return {
- "buy_date": engine.context.entry_date.isoformat() if engine.context.entry_date else "",
- "buy_price": engine.context.entry_price,
- "buy_reason": engine.context.entry_reason,
- "sell_date": row.name.date().isoformat(),
- "sell_price": float(row["close"]),
- "sell_reason": reason,
- "holding_days": engine._holding_days(row.name.date()),
- "return_pct": (
- float(row["close"]) / engine.context.entry_price - 1
- if engine.context.entry_price
- else None
- ),
- }
- def _enrich_reason_metadata(engine: Any, events_df: pd.DataFrame, trades_df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
- if not events_df.empty:
- event_decisions = [
- engine._build_decision(
- side=str(row["side"]),
- layer=str(row["layer"]),
- reason=str(row["reason"]),
- )
- for _, row in events_df.iterrows()
- ]
- events_df["reason_layer"] = [d.reason.layer.value if d.reason is not None else "unknown" for d in event_decisions]
- events_df["reason_family"] = [d.reason.family.value if d.reason is not None else "unknown" for d in event_decisions]
- events_df["reason_code"] = [d.reason.code if d.reason is not None else "" for d in event_decisions]
- if not trades_df.empty:
- buy_meta = trades_df["buy_reason"].map(classify_entry_reason)
- sell_meta = trades_df["sell_reason"].map(classify_exit_reason)
- trades_df["buy_reason_layer"] = [meta.layer.value for meta in buy_meta]
- trades_df["buy_reason_family"] = [meta.family.value for meta in buy_meta]
- trades_df["buy_reason_code"] = [meta.code for meta in buy_meta]
- trades_df["sell_reason_layer"] = [meta.layer.value for meta in sell_meta]
- trades_df["sell_reason_family"] = [meta.family.value for meta in sell_meta]
- trades_df["sell_reason_code"] = [meta.code for meta in sell_meta]
- return events_df, trades_df
- def run_compat_execution(engine: Any, df: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
- """
- Execute the legacy-compatible strategy loop.
- The engine object is expected to provide the same internal hooks as
- DragonRuleEngine. This keeps the behavior path unchanged while physically
- decoupling execution runtime from strategy rule declarations.
- """
- engine.context = engine.context.__class__()
- events: list[dict[str, object]] = []
- trades: list[dict[str, object]] = []
- prev_row: Optional[pd.Series] = None
- for _, row in df.iterrows():
- engine._record_cross_counters(row)
- engine._update_position_counters(row)
- engine._update_pending_states(row)
- just_bought = False
- if (not engine.context.in_position) or bool(row["kdj_buy"] or row["ql_buy"]):
- action, reason = engine._buy_decision(row, prev_row)
- if action == "BUY":
- engine._post_real_buy(row, reason)
- just_bought = True
- events.append(_event_payload(row, side="BUY", layer="real_trade", reason=reason))
- elif action == "AUX_BUY":
- engine._post_aux_buy(row)
- events.append(_event_payload(row, side="BUY", layer="aux_signal", reason=reason))
- state_aux_sell_candidate = (not engine.context.in_position) and engine._should_emit_state_aux_sell(row)
- if not just_bought and (engine.context.in_position or bool(row["kdj_sell"] or row["ql_sell"]) or state_aux_sell_candidate):
- if engine.context.in_position and bool(row["kdj_sell"] or row["ql_sell"]):
- engine.context.sell_signal_count += 1
- if bool(row["kdj_sell"]):
- engine.context.kdj_sell_signal_count += 1
- if bool(row["ql_sell"]):
- engine.context.ql_sell_signal_count += 1
- if float(row["b1"]) < 0:
- engine.context.b1_negative_sell_count += 1
- action, reason = engine._sell_decision(row, prev_row)
- if action == "SELL":
- trades.append(_trade_payload(engine=engine, row=row, reason=reason))
- engine.context.first_exit_checked = True
- events.append(_event_payload(row, side="SELL", layer="real_trade", reason=reason))
- engine._post_real_sell(row, reason)
- elif action == "AUX_SELL":
- if engine.context.in_position:
- engine.context.first_exit_checked = True
- engine._post_aux_sell(row, reason)
- events.append(_event_payload(row, side="SELL", layer="aux_signal", reason=reason))
- prev_row = row
- events_df = pd.DataFrame(events)
- trades_df = pd.DataFrame(trades)
- return _enrich_reason_metadata(engine=engine, events_df=events_df, trades_df=trades_df)
|