from __future__ import annotations import json from datetime import UTC, datetime from pathlib import Path ROOT = Path(__file__).resolve().parents[1] REPORT_DIR = ROOT / "reports" / "eth-exploration" FINAL_JSON = REPORT_DIR / "eth-conservative-portfolio-final.json" SIGNAL_INTENT_JSON = REPORT_DIR / "eth-focused-portfolio-signal-intent.json" FREQTRADE_README = ROOT / "freqtrade" / "README.md" FREQTRADE_CONFIG = ROOT / "freqtrade" / "config-okx-futures.json" BTC_RSI2_STRATEGY = ROOT / "freqtrade" / "user_data" / "strategies" / "BtcRsi2Guarded.py" EXPORT_SCRIPT = ROOT / "scripts" / "export_freqtrade_data.py" def read_json(path: Path) -> dict[str, object]: return json.loads(path.read_text(encoding="utf-8")) def read_text(path: Path) -> str: return path.read_text(encoding="utf-8") def build_payload() -> dict[str, object]: final_report = read_json(FINAL_JSON) signal_intent = read_json(SIGNAL_INTENT_JSON) freqtrade_config = read_json(FREQTRADE_CONFIG) btc_strategy = read_text(BTC_RSI2_STRATEGY) export_script = read_text(EXPORT_SCRIPT) freqtrade_readme = read_text(FREQTRADE_README) return { "generated_at": datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z"), "mode": "readonly_freqtrade_comparison", "sources": { "freqtrade_readme": str(FREQTRADE_README.relative_to(ROOT)), "freqtrade_config": str(FREQTRADE_CONFIG.relative_to(ROOT)), "btc_rsi2_strategy": str(BTC_RSI2_STRATEGY.relative_to(ROOT)), "export_script": str(EXPORT_SCRIPT.relative_to(ROOT)), "final_report": str(FINAL_JSON.relative_to(ROOT)), "signal_intent": str(SIGNAL_INTENT_JSON.relative_to(ROOT)), }, "existing_freqtrade_surface": { "config_pair_whitelist": freqtrade_config["exchange"]["pair_whitelist"], "trading_mode": freqtrade_config["trading_mode"], "margin_mode": freqtrade_config["margin_mode"], "timeframe": freqtrade_config["timeframe"], "dataformat_ohlcv": freqtrade_config["dataformat_ohlcv"], "strategy_class_present": "class BtcRsi2Guarded" in btc_strategy, "strategy_uses_leverage_callback": "def leverage(" in btc_strategy, "strategy_uses_custom_exit": "def custom_exit(" in btc_strategy, "export_supports_eth": "\"ETH-USDT-SWAP\"" in export_script, "readme_marks_comparison_not_replacement": "execution-framework comparison" in freqtrade_readme, }, "portfolio_candidates": final_report["candidates"], "signal_intent_legs": signal_intent["legs"], "leg_mapping": [ { "family": "eth_btc_rsi_filter", "freqtrade_fit": "direct_strategy_logic_with_btc_informative_pair", "maps_to": [ "populate_indicators: ETH SMA and RSI2 on base ETH dataframe", "informative BTC pair: BTC SMA and BTC momentum on the same timeframe", "populate_entry_trend: ETH trend + ETH RSI2 pullback + BTC risk-on", "populate_exit_trend/custom_exit: ETH RSI exit or BTC trend-off", "leverage callback: fixed 3x capped by exchange max", ], "requires": [ "ETH/USDT:USDT base pair", "BTC/USDT:USDT informative pair", "15m candles", "stake sizing from portfolio weight if used as a portfolio leg", ], "migration_risk": "low_for_signal_comparison", }, { "family": "btc_lead_eth_lag", "freqtrade_fit": "direct_strategy_logic_with_btc_informative_pair", "maps_to": [ "populate_indicators: ETH return over lead lookback", "informative BTC pair: BTC return over lead lookback", "populate_entry_trend: BTC return threshold and BTC-ETH return gap", "custom_exit: max_hold_bars", "stoploss/custom_exit: stop_loss_pct and take_profit_pct", ], "requires": [ "BTC informative pair on the leg timeframe", "5m and/or 15m timeframes for the signal-intent portfolio", "separate strategy classes or one strategy with informative timeframe columns", ], "migration_risk": "low_for_single_leg_backtest_medium_for_multi_leg_portfolio_accounting", }, { "family": "eth_robust_twap", "freqtrade_fit": "partial_only", "maps_to": [ "base RSI2 guarded long trigger", "fixed leverage", "max hold and stop exit", ], "requires": [ "custom_entry_price or limit order configuration for price offsets", "position adjustment if multiple TWAP levels are modeled inside one trade", "custom order/fill tracking to compare maker fill, miss, and slippage", "portfolio-level reconciliation if combined with other ETH legs", ], "migration_risk": "high_for_execution_fidelity", }, ], "okx_futures_data_import": { "existing_export_command_examples": [ "rtk uv run python scripts/export_freqtrade_data.py --symbol ETH-USDT-SWAP --bar 15m", "rtk uv run python scripts/export_freqtrade_data.py --symbol BTC-USDT-SWAP --bar 15m", "rtk uv run python scripts/export_freqtrade_data.py --symbol ETH-USDT-SWAP --bar 5m", "rtk uv run python scripts/export_freqtrade_data.py --symbol BTC-USDT-SWAP --bar 5m", ], "output_files": [ "freqtrade/user_data/data/okx/futures/ETH_USDT_USDT-15m-futures.json", "freqtrade/user_data/data/okx/futures/BTC_USDT_USDT-15m-futures.json", "freqtrade/user_data/data/okx/futures/ETH_USDT_USDT-5m-futures.json", "freqtrade/user_data/data/okx/futures/BTC_USDT_USDT-5m-futures.json", ], "config_changes_needed_for_eth_run": [ "Use pair_whitelist ETH/USDT:USDT for an ETH base strategy.", "Keep exchange.name okx, trading_mode futures, margin_mode isolated, dataformat_ohlcv json.", "Keep BTC data available as informative data, not as a tradable leg unless testing BTC directly.", ], }, "minimum_strategy_skeleton": { "can_generate": True, "recommended_first_target": "no_maker_dependent ETH/BTC RSI filter + BTC lead ETH lag, because it avoids TWAP maker-fill lifecycle assumptions.", "not_generated_in_this_task": True, "reason": "The request asks for a read-only comparison/report and no main strategy submission.", }, "decision": { "freqtrade_as_next_step": "yes_for_signal_and_accounting_comparison_of_non_maker_legs", "freqtrade_as_execution_migration": "no_for_primary_P1_if_eth_robust_twap_remains_required", "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.", }, } def skeleton_snippet() -> str: return '''class EthFocusedPortfolioFreqtrade(IStrategy): timeframe = "15m" can_short = False startup_candle_count = 480 process_only_new_candles = True minimal_roi = {"0": 100.0} stoploss = -0.006 use_exit_signal = True def informative_pairs(self): return [("BTC/USDT:USDT", "15m"), ("BTC/USDT:USDT", "5m")] def populate_indicators(self, dataframe, metadata): # ETH SMA/RSI2 on base pair; merge BTC informative SMA/momentum/returns. return dataframe def populate_entry_trend(self, dataframe, metadata): # enter_long when one selected leg is active; enter_tag records leg id. return dataframe def custom_stake_amount(self, pair, current_time, current_rate, proposed_stake, **kwargs): # map active leg tag to portfolio weight. return proposed_stake def custom_exit(self, pair, trade, current_time, current_rate, current_profit, **kwargs): # map leg exit: RSI/BTC trend-off or max_hold/take_profit. return None def leverage(self, pair, current_time, current_rate, proposed_leverage, max_leverage, entry_tag, side, **kwargs): return min(3.0, max_leverage) ''' def markdown_report(payload: dict[str, object]) -> str: lines = [ "# ETH-focused portfolio freqtrade comparison", "", "Dry-run research only. No strategy file was created and no order path was called.", "", "## Existing freqtrade surface", "", 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.", f"- Current whitelist: `{payload['existing_freqtrade_surface']['config_pair_whitelist']}`.", f"- Existing strategy: `{payload['sources']['btc_rsi2_strategy']}` has `populate_indicators`, entry/exit signals, `custom_exit`, and fixed leverage callback.", f"- Export path: `{payload['sources']['export_script']}` already supports `ETH-USDT-SWAP` and `BTC-USDT-SWAP`.", "", "## Leg mapping", "", "| Leg family | Freqtrade fit | Needs | Migration risk |", "| --- | --- | --- | --- |", ] for mapping in payload["leg_mapping"]: lines.append( f"| `{mapping['family']}` | `{mapping['freqtrade_fit']}` | {'; '.join(mapping['requires'])} | `{mapping['migration_risk']}` |" ) lines.extend( [ "", "## Data import", "", "Existing cached OKX candles can be exported into Freqtrade JSON futures format:", "", ] ) for command in payload["okx_futures_data_import"]["existing_export_command_examples"]: lines.append(f"- `{command}`") lines.extend(["", "Expected output files:"]) for path in payload["okx_futures_data_import"]["output_files"]: lines.append(f"- `{path}`") lines.extend( [ "", "## Minimum skeleton", "", "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.", "", "```python", skeleton_snippet().rstrip(), "```", "", "## Decision", "", f"- Freqtrade next step: `{payload['decision']['freqtrade_as_next_step']}`.", f"- Freqtrade execution migration: `{payload['decision']['freqtrade_as_execution_migration']}`.", f"- Shortest path: {payload['decision']['shortest_path']}", "", "## JSON", "", "```json", json.dumps(payload, indent=2, sort_keys=True), "```", "", ] ) return "\n".join(lines) def main() -> int: payload = build_payload() stamp = payload["generated_at"].replace(":", "").replace("-", "") REPORT_DIR.mkdir(parents=True, exist_ok=True) json_path = REPORT_DIR / f"eth-focused-portfolio-freqtrade-{stamp}.json" md_path = REPORT_DIR / f"eth-focused-portfolio-freqtrade-{stamp}.md" json_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") md_path.write_text(markdown_report(payload), encoding="utf-8") print(md_path.relative_to(ROOT)) print(json_path.relative_to(ROOT)) return 0 if __name__ == "__main__": raise SystemExit(main())