compare_eth_focused_portfolio_freqtrade.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. from __future__ import annotations
  2. import json
  3. from datetime import UTC, datetime
  4. from pathlib import Path
  5. ROOT = Path(__file__).resolve().parents[1]
  6. REPORT_DIR = ROOT / "reports" / "eth-exploration"
  7. FINAL_JSON = REPORT_DIR / "eth-conservative-portfolio-final.json"
  8. SIGNAL_INTENT_JSON = REPORT_DIR / "eth-focused-portfolio-signal-intent.json"
  9. FREQTRADE_README = ROOT / "freqtrade" / "README.md"
  10. FREQTRADE_CONFIG = ROOT / "freqtrade" / "config-okx-futures.json"
  11. BTC_RSI2_STRATEGY = ROOT / "freqtrade" / "user_data" / "strategies" / "BtcRsi2Guarded.py"
  12. EXPORT_SCRIPT = ROOT / "scripts" / "export_freqtrade_data.py"
  13. def read_json(path: Path) -> dict[str, object]:
  14. return json.loads(path.read_text(encoding="utf-8"))
  15. def read_text(path: Path) -> str:
  16. return path.read_text(encoding="utf-8")
  17. def build_payload() -> dict[str, object]:
  18. final_report = read_json(FINAL_JSON)
  19. signal_intent = read_json(SIGNAL_INTENT_JSON)
  20. freqtrade_config = read_json(FREQTRADE_CONFIG)
  21. btc_strategy = read_text(BTC_RSI2_STRATEGY)
  22. export_script = read_text(EXPORT_SCRIPT)
  23. freqtrade_readme = read_text(FREQTRADE_README)
  24. return {
  25. "generated_at": datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z"),
  26. "mode": "readonly_freqtrade_comparison",
  27. "sources": {
  28. "freqtrade_readme": str(FREQTRADE_README.relative_to(ROOT)),
  29. "freqtrade_config": str(FREQTRADE_CONFIG.relative_to(ROOT)),
  30. "btc_rsi2_strategy": str(BTC_RSI2_STRATEGY.relative_to(ROOT)),
  31. "export_script": str(EXPORT_SCRIPT.relative_to(ROOT)),
  32. "final_report": str(FINAL_JSON.relative_to(ROOT)),
  33. "signal_intent": str(SIGNAL_INTENT_JSON.relative_to(ROOT)),
  34. },
  35. "existing_freqtrade_surface": {
  36. "config_pair_whitelist": freqtrade_config["exchange"]["pair_whitelist"],
  37. "trading_mode": freqtrade_config["trading_mode"],
  38. "margin_mode": freqtrade_config["margin_mode"],
  39. "timeframe": freqtrade_config["timeframe"],
  40. "dataformat_ohlcv": freqtrade_config["dataformat_ohlcv"],
  41. "strategy_class_present": "class BtcRsi2Guarded" in btc_strategy,
  42. "strategy_uses_leverage_callback": "def leverage(" in btc_strategy,
  43. "strategy_uses_custom_exit": "def custom_exit(" in btc_strategy,
  44. "export_supports_eth": "\"ETH-USDT-SWAP\"" in export_script,
  45. "readme_marks_comparison_not_replacement": "execution-framework comparison" in freqtrade_readme,
  46. },
  47. "portfolio_candidates": final_report["candidates"],
  48. "signal_intent_legs": signal_intent["legs"],
  49. "leg_mapping": [
  50. {
  51. "family": "eth_btc_rsi_filter",
  52. "freqtrade_fit": "direct_strategy_logic_with_btc_informative_pair",
  53. "maps_to": [
  54. "populate_indicators: ETH SMA and RSI2 on base ETH dataframe",
  55. "informative BTC pair: BTC SMA and BTC momentum on the same timeframe",
  56. "populate_entry_trend: ETH trend + ETH RSI2 pullback + BTC risk-on",
  57. "populate_exit_trend/custom_exit: ETH RSI exit or BTC trend-off",
  58. "leverage callback: fixed 3x capped by exchange max",
  59. ],
  60. "requires": [
  61. "ETH/USDT:USDT base pair",
  62. "BTC/USDT:USDT informative pair",
  63. "15m candles",
  64. "stake sizing from portfolio weight if used as a portfolio leg",
  65. ],
  66. "migration_risk": "low_for_signal_comparison",
  67. },
  68. {
  69. "family": "btc_lead_eth_lag",
  70. "freqtrade_fit": "direct_strategy_logic_with_btc_informative_pair",
  71. "maps_to": [
  72. "populate_indicators: ETH return over lead lookback",
  73. "informative BTC pair: BTC return over lead lookback",
  74. "populate_entry_trend: BTC return threshold and BTC-ETH return gap",
  75. "custom_exit: max_hold_bars",
  76. "stoploss/custom_exit: stop_loss_pct and take_profit_pct",
  77. ],
  78. "requires": [
  79. "BTC informative pair on the leg timeframe",
  80. "5m and/or 15m timeframes for the signal-intent portfolio",
  81. "separate strategy classes or one strategy with informative timeframe columns",
  82. ],
  83. "migration_risk": "low_for_single_leg_backtest_medium_for_multi_leg_portfolio_accounting",
  84. },
  85. {
  86. "family": "eth_robust_twap",
  87. "freqtrade_fit": "partial_only",
  88. "maps_to": [
  89. "base RSI2 guarded long trigger",
  90. "fixed leverage",
  91. "max hold and stop exit",
  92. ],
  93. "requires": [
  94. "custom_entry_price or limit order configuration for price offsets",
  95. "position adjustment if multiple TWAP levels are modeled inside one trade",
  96. "custom order/fill tracking to compare maker fill, miss, and slippage",
  97. "portfolio-level reconciliation if combined with other ETH legs",
  98. ],
  99. "migration_risk": "high_for_execution_fidelity",
  100. },
  101. ],
  102. "okx_futures_data_import": {
  103. "existing_export_command_examples": [
  104. "rtk uv run python scripts/export_freqtrade_data.py --symbol ETH-USDT-SWAP --bar 15m",
  105. "rtk uv run python scripts/export_freqtrade_data.py --symbol BTC-USDT-SWAP --bar 15m",
  106. "rtk uv run python scripts/export_freqtrade_data.py --symbol ETH-USDT-SWAP --bar 5m",
  107. "rtk uv run python scripts/export_freqtrade_data.py --symbol BTC-USDT-SWAP --bar 5m",
  108. ],
  109. "output_files": [
  110. "freqtrade/user_data/data/okx/futures/ETH_USDT_USDT-15m-futures.json",
  111. "freqtrade/user_data/data/okx/futures/BTC_USDT_USDT-15m-futures.json",
  112. "freqtrade/user_data/data/okx/futures/ETH_USDT_USDT-5m-futures.json",
  113. "freqtrade/user_data/data/okx/futures/BTC_USDT_USDT-5m-futures.json",
  114. ],
  115. "config_changes_needed_for_eth_run": [
  116. "Use pair_whitelist ETH/USDT:USDT for an ETH base strategy.",
  117. "Keep exchange.name okx, trading_mode futures, margin_mode isolated, dataformat_ohlcv json.",
  118. "Keep BTC data available as informative data, not as a tradable leg unless testing BTC directly.",
  119. ],
  120. },
  121. "minimum_strategy_skeleton": {
  122. "can_generate": True,
  123. "recommended_first_target": "no_maker_dependent ETH/BTC RSI filter + BTC lead ETH lag, because it avoids TWAP maker-fill lifecycle assumptions.",
  124. "not_generated_in_this_task": True,
  125. "reason": "The request asks for a read-only comparison/report and no main strategy submission.",
  126. },
  127. "decision": {
  128. "freqtrade_as_next_step": "yes_for_signal_and_accounting_comparison_of_non_maker_legs",
  129. "freqtrade_as_execution_migration": "no_for_primary_P1_if_eth_robust_twap_remains_required",
  130. "shortest_path": "Use freqtrade to cross-check ETH/BTC RSI filter and BTC lead-lag legs; keep maker-dependent TWAP execution in the existing self-built OKX path until real fill behavior is measured.",
  131. },
  132. }
  133. def skeleton_snippet() -> str:
  134. return '''class EthFocusedPortfolioFreqtrade(IStrategy):
  135. timeframe = "15m"
  136. can_short = False
  137. startup_candle_count = 480
  138. process_only_new_candles = True
  139. minimal_roi = {"0": 100.0}
  140. stoploss = -0.006
  141. use_exit_signal = True
  142. def informative_pairs(self):
  143. return [("BTC/USDT:USDT", "15m"), ("BTC/USDT:USDT", "5m")]
  144. def populate_indicators(self, dataframe, metadata):
  145. # ETH SMA/RSI2 on base pair; merge BTC informative SMA/momentum/returns.
  146. return dataframe
  147. def populate_entry_trend(self, dataframe, metadata):
  148. # enter_long when one selected leg is active; enter_tag records leg id.
  149. return dataframe
  150. def custom_stake_amount(self, pair, current_time, current_rate, proposed_stake, **kwargs):
  151. # map active leg tag to portfolio weight.
  152. return proposed_stake
  153. def custom_exit(self, pair, trade, current_time, current_rate, current_profit, **kwargs):
  154. # map leg exit: RSI/BTC trend-off or max_hold/take_profit.
  155. return None
  156. def leverage(self, pair, current_time, current_rate, proposed_leverage, max_leverage, entry_tag, side, **kwargs):
  157. return min(3.0, max_leverage)
  158. '''
  159. def markdown_report(payload: dict[str, object]) -> str:
  160. lines = [
  161. "# ETH-focused portfolio freqtrade comparison",
  162. "",
  163. "Dry-run research only. No strategy file was created and no order path was called.",
  164. "",
  165. "## Existing freqtrade surface",
  166. "",
  167. f"- Config: `{payload['sources']['freqtrade_config']}` uses `{payload['existing_freqtrade_surface']['trading_mode']}` / `{payload['existing_freqtrade_surface']['margin_mode']}` with `{payload['existing_freqtrade_surface']['dataformat_ohlcv']}` OHLCV.",
  168. f"- Current whitelist: `{payload['existing_freqtrade_surface']['config_pair_whitelist']}`.",
  169. f"- Existing strategy: `{payload['sources']['btc_rsi2_strategy']}` has `populate_indicators`, entry/exit signals, `custom_exit`, and fixed leverage callback.",
  170. f"- Export path: `{payload['sources']['export_script']}` already supports `ETH-USDT-SWAP` and `BTC-USDT-SWAP`.",
  171. "",
  172. "## Leg mapping",
  173. "",
  174. "| Leg family | Freqtrade fit | Needs | Migration risk |",
  175. "| --- | --- | --- | --- |",
  176. ]
  177. for mapping in payload["leg_mapping"]:
  178. lines.append(
  179. f"| `{mapping['family']}` | `{mapping['freqtrade_fit']}` | {'; '.join(mapping['requires'])} | `{mapping['migration_risk']}` |"
  180. )
  181. lines.extend(
  182. [
  183. "",
  184. "## Data import",
  185. "",
  186. "Existing cached OKX candles can be exported into Freqtrade JSON futures format:",
  187. "",
  188. ]
  189. )
  190. for command in payload["okx_futures_data_import"]["existing_export_command_examples"]:
  191. lines.append(f"- `{command}`")
  192. lines.extend(["", "Expected output files:"])
  193. for path in payload["okx_futures_data_import"]["output_files"]:
  194. lines.append(f"- `{path}`")
  195. lines.extend(
  196. [
  197. "",
  198. "## Minimum skeleton",
  199. "",
  200. "A minimum skeleton can be generated, but the first target should be the no-maker-dependent ETH/BTC RSI + BTC lead-lag comparison. The maker-dependent TWAP leg can only be approximated in a normal Freqtrade backtest unless custom order/fill tracking is added.",
  201. "",
  202. "```python",
  203. skeleton_snippet().rstrip(),
  204. "```",
  205. "",
  206. "## Decision",
  207. "",
  208. f"- Freqtrade next step: `{payload['decision']['freqtrade_as_next_step']}`.",
  209. f"- Freqtrade execution migration: `{payload['decision']['freqtrade_as_execution_migration']}`.",
  210. f"- Shortest path: {payload['decision']['shortest_path']}",
  211. "",
  212. "## JSON",
  213. "",
  214. "```json",
  215. json.dumps(payload, indent=2, sort_keys=True),
  216. "```",
  217. "",
  218. ]
  219. )
  220. return "\n".join(lines)
  221. def main() -> int:
  222. payload = build_payload()
  223. stamp = payload["generated_at"].replace(":", "").replace("-", "")
  224. REPORT_DIR.mkdir(parents=True, exist_ok=True)
  225. json_path = REPORT_DIR / f"eth-focused-portfolio-freqtrade-{stamp}.json"
  226. md_path = REPORT_DIR / f"eth-focused-portfolio-freqtrade-{stamp}.md"
  227. json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
  228. md_path.write_text(markdown_report(payload), encoding="utf-8")
  229. print(md_path.relative_to(ROOT))
  230. print(json_path.relative_to(ROOT))
  231. return 0
  232. if __name__ == "__main__":
  233. raise SystemExit(main())