dragon_validate.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. from __future__ import annotations
  2. from pathlib import Path
  3. import pandas as pd
  4. from dragon_indicators import DragonIndicatorConfig, DragonIndicatorEngine
  5. from dragon_workbook import DragonWorkbook
  6. KDJ_BUY_MARKER = chr(0x91D1)
  7. KDJ_SELL_MARKER = chr(0x6B7B)
  8. QL_BUY_MARKER = "B"
  9. QL_SELL_MARKER = "S"
  10. def _find_workbook(base_dir: Path) -> Path:
  11. matches = sorted(base_dir.glob("*.xlsx"))
  12. if not matches:
  13. raise FileNotFoundError(f"No workbook found in {base_dir}")
  14. return matches[0]
  15. def _marker_match(workbook_value: object, computed_value: bool, positive: str) -> bool:
  16. if workbook_value is None:
  17. return not computed_value
  18. if not isinstance(workbook_value, str):
  19. return not computed_value
  20. return (positive in workbook_value) == bool(computed_value)
  21. def _explicit_marker(value: object, allowed: set[str]) -> str | None:
  22. if not isinstance(value, str):
  23. return None
  24. stripped = value.strip()
  25. return stripped if stripped in allowed else None
  26. def main() -> None:
  27. base_dir = Path(__file__).resolve().parent
  28. workbook_path = _find_workbook(base_dir)
  29. workbook = DragonWorkbook(workbook_path)
  30. split_events = workbook.split_layers()
  31. annual_summary = workbook.load_annual_summary()
  32. config = DragonIndicatorConfig(start_date="2015-01-01", end_date="2026-01-31")
  33. engine = DragonIndicatorEngine(config)
  34. raw_df = engine.fetch_daily_data()
  35. indicators = engine.compute(raw_df)
  36. indicators_out = indicators.reset_index().rename(columns={"index": "date"})
  37. indicators_out["date"] = pd.to_datetime(indicators_out["date"]).dt.date.astype(str)
  38. indicators_out.to_csv(base_dir / "dragon_indicator_snapshot.csv", index=False, encoding="utf-8-sig")
  39. benchmark_df = pd.DataFrame(
  40. [
  41. {
  42. "date": event.date.isoformat(),
  43. "side": event.side,
  44. "layer": event.layer,
  45. "signal_reason": event.signal_reason,
  46. "index_close": event.index_close,
  47. "prev_index": event.prev_index,
  48. "capital": event.capital,
  49. "pnl": event.pnl,
  50. "kdj": event.kdj,
  51. "ql": event.ql,
  52. "note": event.note,
  53. }
  54. for event in split_events
  55. ]
  56. )
  57. benchmark_df.to_csv(base_dir / "dragon_workbook_layers.csv", index=False, encoding="utf-8-sig")
  58. workbook_markers = benchmark_df[["date", "kdj", "ql"]].drop_duplicates(subset=["date"]).copy()
  59. workbook_markers["kdj_marker"] = workbook_markers["kdj"].apply(
  60. lambda value: _explicit_marker(value, {KDJ_BUY_MARKER, KDJ_SELL_MARKER})
  61. )
  62. workbook_markers["ql_marker"] = workbook_markers["ql"].apply(
  63. lambda value: _explicit_marker(value, {QL_BUY_MARKER, QL_SELL_MARKER})
  64. )
  65. compare = workbook_markers.merge(
  66. indicators_out[
  67. ["date", "a1", "b1", "c1", "kdj_buy", "kdj_sell", "ql_buy", "ql_sell", "close"]
  68. ],
  69. on="date",
  70. how="left",
  71. )
  72. compare["kdj_buy_match"] = compare.apply(
  73. lambda row: _marker_match(row["kdj_marker"], bool(row["kdj_buy"]), KDJ_BUY_MARKER),
  74. axis=1,
  75. )
  76. compare["kdj_sell_match"] = compare.apply(
  77. lambda row: _marker_match(row["kdj_marker"], bool(row["kdj_sell"]), KDJ_SELL_MARKER),
  78. axis=1,
  79. )
  80. compare["ql_buy_match"] = compare.apply(
  81. lambda row: _marker_match(row["ql_marker"], bool(row["ql_buy"]), QL_BUY_MARKER),
  82. axis=1,
  83. )
  84. compare["ql_sell_match"] = compare.apply(
  85. lambda row: _marker_match(row["ql_marker"], bool(row["ql_sell"]), QL_SELL_MARKER),
  86. axis=1,
  87. )
  88. compare.to_csv(base_dir / "dragon_signal_alignment.csv", index=False, encoding="utf-8-sig")
  89. real_events = benchmark_df[benchmark_df["layer"] == "real_trade"]
  90. aux_events = benchmark_df[benchmark_df["layer"] == "aux_signal"]
  91. kdj_rows = compare[compare["kdj_marker"].notna()]
  92. ql_rows = compare[compare["ql_marker"].notna()]
  93. kdj_sell_mismatch_dates = compare.loc[
  94. compare["kdj_marker"].eq(KDJ_SELL_MARKER) & ~compare["kdj_sell_match"], "date"
  95. ].tolist()
  96. ql_buy_mismatch_dates = compare.loc[
  97. compare["ql_marker"].eq(QL_BUY_MARKER) & ~compare["ql_buy_match"], "date"
  98. ].tolist()
  99. lines = [
  100. "# Dragon V2 Validation",
  101. "",
  102. f"- Source workbook: `{workbook_path.name}`",
  103. f"- Indicator snapshot rows: `{len(indicators_out)}`",
  104. f"- Workbook layered events: `{len(benchmark_df)}`",
  105. f"- Real trade events: `{len(real_events)}`",
  106. f"- Auxiliary events: `{len(aux_events)}`",
  107. f"- Annual summary rows: `{len(annual_summary)}`",
  108. "",
  109. "## Formula Scope",
  110. "- `A1`: EMA(8) vs EMA(EMA(8),20) normalized spread",
  111. "- `B1`: ((Y2 - Y3) / 100), where Y2/Y3 come from 38-day RSV smoothed by SMA(5,1) and SMA(10,1)",
  112. "- `C1`: (Y2 + Y3) / 2",
  113. "- `QL phoenix line`: approximated from the existing Dragon code as `B = CROSS(close, upper_line)` and `S = CROSS(lower_line, close)`",
  114. "",
  115. "## Alignment",
  116. f"- KDJ rows in workbook: `{len(kdj_rows)}`",
  117. f"- QL rows in workbook: `{len(ql_rows)}`",
  118. f"- KDJ buy marker matches: `{int(kdj_rows['kdj_buy_match'].sum())}/{len(kdj_rows)}`",
  119. f"- KDJ sell marker matches: `{int(kdj_rows['kdj_sell_match'].sum())}/{len(kdj_rows)}`",
  120. f"- QL buy marker matches: `{int(ql_rows['ql_buy_match'].sum())}/{len(ql_rows)}`",
  121. f"- QL sell marker matches: `{int(ql_rows['ql_sell_match'].sum())}/{len(ql_rows)}`",
  122. f"- KDJ sell mismatch dates: `{kdj_sell_mismatch_dates}`",
  123. f"- QL buy mismatch dates: `{ql_buy_mismatch_dates}`",
  124. "",
  125. "## Output Files",
  126. "- `dragon_indicator_snapshot.csv`",
  127. "- `dragon_workbook_layers.csv`",
  128. "- `dragon_signal_alignment.csv`",
  129. "",
  130. "## Notes",
  131. "- This validation stage checks indicator reconstruction and marker alignment only.",
  132. "- It does not yet implement the full Dragon entry/exit rule tree from the workbook narrative.",
  133. "- Repeated SELL rows after exit and repeated BUY rows during holding remain auxiliary signals by confirmed user semantics.",
  134. ]
  135. (base_dir / "dragon_validation.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
  136. if __name__ == "__main__":
  137. main()