| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- 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())
|