run_freqtrade_eth_skeleton_backtest.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. from __future__ import annotations
  2. import argparse
  3. import json
  4. import os
  5. import shlex
  6. import shutil
  7. import subprocess
  8. from datetime import UTC, datetime
  9. from pathlib import Path
  10. from typing import Any
  11. ROOT = Path(__file__).resolve().parents[1]
  12. REPORT_DIR = ROOT / "reports" / "eth-exploration"
  13. BASE_CONFIG = ROOT / "freqtrade" / "config-okx-futures.json"
  14. STRATEGY = ROOT / "freqtrade" / "user_data" / "strategies" / "EthFocusedInformativeDry.py"
  15. TMP_ROOT = Path("/tmp/okx-codex-trader-freqtrade-eth-skeleton")
  16. TMP_USERDIR = TMP_ROOT / "user_data"
  17. TMP_DATA_DIR = TMP_USERDIR / "data" / "okx" / "futures"
  18. TMP_STRATEGY_DIR = TMP_USERDIR / "strategies"
  19. TMP_CONFIG = TMP_ROOT / "config-eth-skeleton-okx-futures.json"
  20. PAIR = "ETH/USDT:USDT"
  21. EXPORTS = (
  22. ("ETH-USDT-SWAP", "5m"),
  23. ("BTC-USDT-SWAP", "5m"),
  24. ("ETH-USDT-SWAP", "15m"),
  25. ("BTC-USDT-SWAP", "15m"),
  26. )
  27. FREQTRADE_COMMAND_ENV = "FREQTRADE_COMMAND"
  28. def run_command(command: list[str]) -> dict[str, Any]:
  29. started_at = datetime.now(UTC)
  30. completed = subprocess.run(
  31. command,
  32. cwd=ROOT,
  33. text=True,
  34. stdout=subprocess.PIPE,
  35. stderr=subprocess.PIPE,
  36. check=False,
  37. )
  38. finished_at = datetime.now(UTC)
  39. return {
  40. "command": command,
  41. "returncode": completed.returncode,
  42. "started_at": started_at.isoformat(),
  43. "finished_at": finished_at.isoformat(),
  44. "stdout": completed.stdout,
  45. "stderr": completed.stderr,
  46. }
  47. def proxy_url() -> str | None:
  48. for name in ("HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy"):
  49. value = os.environ.get(name)
  50. if value:
  51. return value
  52. return None
  53. def write_tmp_config() -> dict[str, Any]:
  54. config = json.loads(BASE_CONFIG.read_text(encoding="utf-8"))
  55. config["timeframe"] = "5m"
  56. config["bot_name"] = "okx-codex-eth-skeleton-backtest"
  57. config["dry_run"] = True
  58. config["exchange"]["pair_whitelist"] = [PAIR]
  59. proxy = proxy_url()
  60. proxy_config_key = None
  61. if proxy:
  62. for key in ("ccxt_config", "ccxt_async_config"):
  63. config["exchange"].setdefault(key, {})
  64. config["exchange"][key]["httpsProxy"] = proxy
  65. proxy_config_key = "httpsProxy"
  66. TMP_ROOT.mkdir(parents=True, exist_ok=True)
  67. TMP_CONFIG.write_text(json.dumps(config, indent=2, sort_keys=True) + "\n", encoding="utf-8")
  68. return {
  69. "path": str(TMP_CONFIG),
  70. "pair_whitelist": config["exchange"]["pair_whitelist"],
  71. "timeframe": config["timeframe"],
  72. "dry_run": config["dry_run"],
  73. "proxy_injected": proxy is not None,
  74. "proxy_source": "environment" if proxy else None,
  75. "proxy_config_key": proxy_config_key,
  76. }
  77. def prepare_tmp_userdir() -> dict[str, str]:
  78. TMP_DATA_DIR.mkdir(parents=True, exist_ok=True)
  79. TMP_STRATEGY_DIR.mkdir(parents=True, exist_ok=True)
  80. target_strategy = TMP_STRATEGY_DIR / STRATEGY.name
  81. shutil.copy2(STRATEGY, target_strategy)
  82. return {
  83. "userdir": str(TMP_USERDIR),
  84. "data_dir": str(TMP_DATA_DIR),
  85. "strategy": str(target_strategy),
  86. }
  87. def export_data() -> list[dict[str, Any]]:
  88. results = []
  89. for symbol, bar in EXPORTS:
  90. results.append(
  91. run_command(
  92. [
  93. "rtk",
  94. "uv",
  95. "run",
  96. "python",
  97. "scripts/export_freqtrade_data.py",
  98. "--symbol",
  99. symbol,
  100. "--bar",
  101. bar,
  102. "--output-dir",
  103. str(TMP_DATA_DIR),
  104. ]
  105. )
  106. )
  107. return results
  108. def run_backtest() -> dict[str, Any]:
  109. freqtrade_command = shlex.split(
  110. os.environ.get(FREQTRADE_COMMAND_ENV, "uvx --from freqtrade freqtrade")
  111. )
  112. return run_command(
  113. [
  114. "rtk",
  115. *freqtrade_command,
  116. "backtesting",
  117. "--config",
  118. str(TMP_CONFIG),
  119. "--userdir",
  120. str(TMP_USERDIR),
  121. "--strategy",
  122. "EthFocusedInformativeDry",
  123. "--timeframe",
  124. "5m",
  125. "--pairs",
  126. PAIR,
  127. "--timerange",
  128. "20230101-",
  129. ]
  130. )
  131. def next_steps(payload: dict[str, Any]) -> list[str]:
  132. backtest = payload["backtest"]
  133. if backtest["returncode"] == 0:
  134. return ["Review the Freqtrade result table and compare trade count, drawdown, and total profit with the research backtest."]
  135. reason = f"{backtest['stderr']}\n{backtest['stdout']}"
  136. if "Failed to spawn" in reason or "No such file or directory" in reason:
  137. return [f"Set {FREQTRADE_COMMAND_ENV} to a runnable Freqtrade command, then rerun this script."]
  138. if "No data found" in reason or "No history data" in reason:
  139. return ["Verify the four exported futures JSON files under the temporary userdir data directory and rerun backtesting."]
  140. if "OperationalException" in reason or "ImportError" in reason:
  141. return ["Fix the reported Freqtrade strategy/config error directly, then rerun the same script."]
  142. return ["Use the full stderr/stdout captured in the JSON report to identify the first Freqtrade failure and rerun after that specific issue is fixed."]
  143. def render_markdown(payload: dict[str, Any]) -> str:
  144. export_rows = "\n".join(
  145. f"- `{ ' '.join(item['command']) }`: exit `{item['returncode']}`"
  146. for item in payload["exports"]
  147. )
  148. backtest = payload["backtest"]
  149. status = "succeeded" if backtest["returncode"] == 0 else "failed"
  150. return "\n".join(
  151. [
  152. "# Freqtrade ETH skeleton backtest attempt",
  153. "",
  154. f"- Generated at: `{payload['generated_at']}`",
  155. "- Scope: backtest only; no live or dry-run trading process was started.",
  156. "- Repo config changed: `false`; temporary config was written under `/tmp`.",
  157. f"- Temporary userdir: `{payload['tmp_userdir']['userdir']}`",
  158. f"- Temporary config: `{payload['tmp_config']['path']}`",
  159. f"- Proxy injected: `{payload['tmp_config']['proxy_injected']}`",
  160. f"- Proxy config key: `{payload['tmp_config']['proxy_config_key']}`",
  161. "",
  162. "## Data export",
  163. "",
  164. export_rows,
  165. "",
  166. "## Backtest",
  167. "",
  168. f"- Command: `{ ' '.join(backtest['command']) }`",
  169. f"- Result: `{status}` with exit `{backtest['returncode']}`",
  170. "",
  171. "## Full command output",
  172. "",
  173. "### stderr",
  174. "",
  175. "```text",
  176. backtest["stderr"].rstrip(),
  177. "```",
  178. "",
  179. "### stdout",
  180. "",
  181. "```text",
  182. backtest["stdout"].rstrip(),
  183. "```",
  184. "",
  185. "## Next step",
  186. "",
  187. *[f"- {step}" for step in payload["next_steps"]],
  188. "",
  189. ]
  190. )
  191. def main() -> int:
  192. parser = argparse.ArgumentParser()
  193. parser.add_argument("--stamp", default=datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ"))
  194. args = parser.parse_args()
  195. tmp_userdir = prepare_tmp_userdir()
  196. tmp_config = write_tmp_config()
  197. exports = export_data()
  198. backtest = run_backtest()
  199. payload: dict[str, Any] = {
  200. "generated_at": datetime.now(UTC).isoformat(),
  201. "mode": "freqtrade_eth_skeleton_backtest_attempt",
  202. "real_trading": False,
  203. "repo_config_changed": False,
  204. "strategy": str(STRATEGY.relative_to(ROOT)),
  205. "tmp_userdir": tmp_userdir,
  206. "tmp_config": tmp_config,
  207. "exports": exports,
  208. "backtest": backtest,
  209. }
  210. payload["next_steps"] = next_steps(payload)
  211. REPORT_DIR.mkdir(parents=True, exist_ok=True)
  212. json_path = REPORT_DIR / f"freqtrade-eth-skeleton-backtest-fix-{args.stamp}.json"
  213. md_path = REPORT_DIR / f"freqtrade-eth-skeleton-backtest-fix-{args.stamp}.md"
  214. payload["json_report"] = str(json_path.relative_to(ROOT))
  215. payload["markdown_report"] = str(md_path.relative_to(ROOT))
  216. json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
  217. md_path.write_text(render_markdown(payload), encoding="utf-8")
  218. print(md_path)
  219. return 0 if backtest["returncode"] == 0 else 1
  220. if __name__ == "__main__":
  221. raise SystemExit(main())