瀏覽代碼

research: add eth execution readiness reports

lxy 1 月之前
父節點
當前提交
a6d13059c2

+ 14 - 0
reports/eth-exploration/README.md

@@ -171,3 +171,17 @@ Reports:
 - `reports/eth-exploration/eth-focused-portfolio-signal-intent.json`
 
 Core conclusion: prioritize the ETH-focused conservative portfolio for quasi-live/read-only intent tracking. Standalone ETH TWAP is not approved for live trading under conservative maker-fill assumptions. The current portfolio signal intent has no active signal and produced no order intent.
+
+## Freqtrade comparison and live readiness
+
+Scripts:
+- `scripts/compare_eth_focused_portfolio_freqtrade.py`
+- `scripts/check_eth_focused_portfolio_live_readiness.py`
+
+Reports:
+- `reports/eth-exploration/eth-focused-portfolio-freqtrade-20260429T182815Z.md`
+- `reports/eth-exploration/eth-focused-portfolio-freqtrade-20260429T182815Z.json`
+- `reports/eth-exploration/eth-focused-portfolio-live-readiness.md`
+- `reports/eth-exploration/eth-focused-portfolio-live-readiness.json`
+
+Core conclusion: freqtrade is usable as a no-maker-dependent comparison path for the ETH/BTC RSI filter and BTC lead-lag legs, but it does not save time for TWAP execution because maker-fill lifecycle fidelity still needs custom order/fill tracking. Live readiness is not satisfied yet: post_only entry payloads, batch order intent, cancel intent/client support, dedicated state, position protection, scheduler, and append-only logging are still missing.

+ 339 - 0
reports/eth-exploration/eth-focused-portfolio-freqtrade-20260429T182815Z.json

@@ -0,0 +1,339 @@
+{
+  "decision": {
+    "freqtrade_as_execution_migration": "no_for_primary_P1_if_eth_robust_twap_remains_required",
+    "freqtrade_as_next_step": "yes_for_signal_and_accounting_comparison_of_non_maker_legs",
+    "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."
+  },
+  "existing_freqtrade_surface": {
+    "config_pair_whitelist": [
+      "BTC/USDT:USDT"
+    ],
+    "dataformat_ohlcv": "json",
+    "export_supports_eth": true,
+    "margin_mode": "isolated",
+    "readme_marks_comparison_not_replacement": true,
+    "strategy_class_present": true,
+    "strategy_uses_custom_exit": true,
+    "strategy_uses_leverage_callback": true,
+    "timeframe": "15m",
+    "trading_mode": "futures"
+  },
+  "generated_at": "2026-04-29T18:28:15Z",
+  "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"
+      ],
+      "migration_risk": "low_for_signal_comparison",
+      "requires": [
+        "ETH/USDT:USDT base pair",
+        "BTC/USDT:USDT informative pair",
+        "15m candles",
+        "stake sizing from portfolio weight if used as a portfolio leg"
+      ]
+    },
+    {
+      "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"
+      ],
+      "migration_risk": "low_for_single_leg_backtest_medium_for_multi_leg_portfolio_accounting",
+      "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"
+      ]
+    },
+    {
+      "family": "eth_robust_twap",
+      "freqtrade_fit": "partial_only",
+      "maps_to": [
+        "base RSI2 guarded long trigger",
+        "fixed leverage",
+        "max hold and stop exit"
+      ],
+      "migration_risk": "high_for_execution_fidelity",
+      "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"
+      ]
+    }
+  ],
+  "minimum_strategy_skeleton": {
+    "can_generate": true,
+    "not_generated_in_this_task": true,
+    "reason": "The request asks for a read-only comparison/report and no main strategy submission.",
+    "recommended_first_target": "no_maker_dependent ETH/BTC RSI filter + BTC lead ETH lag, because it avoids TWAP maker-fill lifecycle assumptions."
+  },
+  "mode": "readonly_freqtrade_comparison",
+  "okx_futures_data_import": {
+    "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."
+    ],
+    "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"
+    ]
+  },
+  "portfolio_candidates": [
+    {
+      "decision": "Primary next item to watch in paper/demo. It is the cleanest qualified portfolio by conservative sort, but it contains a maker-dependent TWAP leg, so real funds should wait for live fill evidence.",
+      "legs": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt480-bm240-br0.0",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb8-br0.018-gap0.006-mh8-sl0.006-tp0.018",
+        "eth_robust_twap:15m:rsi2-long-guarded-price-twap-o0.0030-0.0060-0.0090-v4-t60-l3.0-x50.0-sl0.012-mh48-fb0.0005-ps0.0000-mm25"
+      ],
+      "metrics": {
+        "cost_model": "maker_taker",
+        "max_horizon_drawdown": 0.06650656143862783,
+        "min_horizon_total_return": 0.014041380756578459,
+        "net_annualized_return": 0.07431265319093083,
+        "net_calmar": 1.0203012338692994,
+        "net_max_drawdown": 0.07283403246423034,
+        "net_total_return": 0.5746697040909923,
+        "scope": "all_legs",
+        "worst_month_return": -0.04435071071689678
+      },
+      "minimum_next_step": "Run quasi-live read-only/order-intent tracking for all legs and record per-leg signal, fill/miss, slippage, and portfolio equity for at least the next signal cycle set.",
+      "name": "all_legs-risk-3-c0124-eth_btc_rsi_filter+btc_lead_eth_lag_15m+eth_robust_twap",
+      "needs_forward_or_demo_live": true,
+      "priority": 1,
+      "real_live_now": false,
+      "status": "candidate",
+      "title": "Lowest-drawdown ETH-focused conservative portfolio",
+      "weights": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt480-bm240-br0.0=0.48537471",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb8-br0.018-gap0.006-mh8-sl0.006-tp0.018=0.06171009",
+        "eth_robust_twap:15m:rsi2-long-guarded-price-twap-o0.0030-0.0060-0.0090-v4-t60-l3.0-x50.0-sl0.012-mh48-fb0.0005-ps0.0000-mm25=0.45291520"
+      ]
+    },
+    {
+      "decision": "Best fallback if maker-fill uncertainty is treated as disqualifying. It keeps qualified portfolio behavior without the robust TWAP maker-dependent leg.",
+      "legs": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt120-bm240-br0.0",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb8-br0.018-gap0.006-mh8-sl0.006-tp0.018"
+      ],
+      "metrics": {
+        "cost_model": "maker_taker",
+        "max_horizon_drawdown": 0.07437399760660594,
+        "min_horizon_total_return": 0.01722844178015248,
+        "net_annualized_return": 0.0900769354075468,
+        "net_calmar": 0.8611995911104812,
+        "net_max_drawdown": 0.10459472616724809,
+        "net_total_return": 0.7268826579747691,
+        "scope": "no_maker_dependent",
+        "worst_month_return": -0.04507919732709942
+      },
+      "minimum_next_step": "Shadow the two legs side by side with the primary portfolio and compare realized signal overlap and drawdown path.",
+      "name": "no_maker_dependent-risk-2-c0250-eth_btc_rsi_filter+btc_lead_eth_lag_15m",
+      "needs_forward_or_demo_live": true,
+      "priority": 2,
+      "real_live_now": false,
+      "status": "candidate",
+      "title": "Simpler no-maker-dependent conservative portfolio",
+      "weights": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt120-bm240-br0.0=0.89432334",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb8-br0.018-gap0.006-mh8-sl0.006-tp0.018=0.10567666"
+      ]
+    },
+    {
+      "decision": "Return-oriented qualified variant. It is less conservative than priority 1 because drawdown is materially higher.",
+      "legs": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt480-bm240-br0.0",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb16-br0.024-gap0.006-mh32-sl0.006-tp0.018"
+      ],
+      "metrics": {
+        "cost_model": "maker_taker",
+        "max_horizon_drawdown": 0.08384178749425959,
+        "min_horizon_total_return": 0.021522808059551757,
+        "net_annualized_return": 0.13247985480488844,
+        "net_calmar": 0.8214839555775263,
+        "net_max_drawdown": 0.1612689498138176,
+        "net_total_return": 1.1990870259436797,
+        "scope": "all_legs",
+        "worst_month_return": -0.056624480428019486
+      },
+      "minimum_next_step": "Keep as a benchmark portfolio in the same quasi-live tracker; do not allocate before it beats priority 1 on realized drawdown-adjusted behavior.",
+      "name": "all_legs-risk-2-c0020-eth_btc_rsi_filter+btc_lead_eth_lag_15m",
+      "needs_forward_or_demo_live": true,
+      "priority": 3,
+      "real_live_now": false,
+      "status": "candidate",
+      "title": "Highest-return qualified conservative portfolio",
+      "weights": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt480-bm240-br0.0=0.89038020",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb16-br0.024-gap0.006-mh32-sl0.006-tp0.018=0.10961980"
+      ]
+    },
+    {
+      "decision": "Keep as secondary watchlist, not primary allocation.",
+      "metrics": {
+        "net_annualized_return": 0.3305936472652691,
+        "net_calmar": 1.4902378159621057,
+        "net_max_drawdown": 0.22183952368155146,
+        "net_total_return": 5.132970549912916,
+        "trades": 88.0,
+        "worst_month": "2020-09",
+        "worst_month_return": -0.10650826274141434
+      },
+      "minimum_next_step": "Run shadow/demo signal intent logging only; require fresh forward trades before any capital allocation because the acceptable-risk versions have few trades.",
+      "name": "bb-squeeze-l96-bw960-q0.25-sl0.01-tpnone-long-btc-up-momo-vc0.006-dd0.25-cd24",
+      "needs_forward_or_demo_live": true,
+      "priority": 4,
+      "real_live_now": false,
+      "status": "watchlist",
+      "title": "BB squeeze risk candidate"
+    }
+  ],
+  "signal_intent_legs": [
+    {
+      "bar": "15m",
+      "decision_candle_time": "2026-04-29T16:30:00Z",
+      "decision_candle_ts": 1777480200000,
+      "dry_run_action": "hold",
+      "entry_rule": "btc_close > btc_sma and btc_momentum >= minimum and eth_close > eth_sma and eth_rsi2 <= threshold",
+      "exit_signal": true,
+      "family": "eth_btc_rsi_filter",
+      "indicators": {
+        "btc_close": 75856.0,
+        "btc_momentum": -0.040101233786776325,
+        "btc_sma": 77331.09999999992,
+        "eth_close": 2266.75,
+        "eth_rsi2": 10.743438715792621,
+        "eth_sma": 2315.6154000000006
+      },
+      "latest_local_candle_time": "2026-04-29T16:45:00Z",
+      "latest_local_candle_ts": 1777481100000,
+      "leg_id": "eth_btc_rsi_filter_15m",
+      "needs_cancel": false,
+      "needs_order": false,
+      "params": {
+        "btc_min_momentum": 0.0,
+        "btc_momentum_lookback": 240,
+        "btc_trend_sma": 480,
+        "eth_exit_rsi": 55.0,
+        "eth_rsi_threshold": 3.0,
+        "eth_trend_sma": 50
+      },
+      "risk_limits": {
+        "leverage": 3,
+        "max_hold_bars": null,
+        "max_weight": 0.80314757,
+        "stop_loss_pct": null,
+        "take_profit_pct": null
+      },
+      "signal": false,
+      "suggested_weight": 0.80314757,
+      "symbol": "ETH-USDT-SWAP"
+    },
+    {
+      "bar": "15m",
+      "decision_candle_time": "2026-04-29T16:30:00Z",
+      "decision_candle_ts": 1777480200000,
+      "dry_run_action": "hold",
+      "entry_rule": "btc_return >= threshold and btc_return - eth_return >= lag_gap",
+      "exit_signal": false,
+      "family": "btc_lead_eth_lag",
+      "indicators": {
+        "btc_close": 75856.0,
+        "btc_return": -0.009247165769813437,
+        "eth_close": 2266.75,
+        "eth_return": -0.014409384796664093,
+        "return_gap": 0.005162219026850656
+      },
+      "latest_local_candle_time": "2026-04-29T16:45:00Z",
+      "latest_local_candle_ts": 1777481100000,
+      "leg_id": "btc_lead_eth_lag_15m",
+      "needs_cancel": false,
+      "needs_order": false,
+      "params": {
+        "btc_return_threshold": 0.018,
+        "lag_gap": 0.006,
+        "lead_lookback": 8,
+        "max_hold_bars": 8,
+        "stop_loss_pct": 0.006,
+        "take_profit_pct": 0.018
+      },
+      "risk_limits": {
+        "leverage": 3,
+        "max_hold_bars": 8,
+        "max_weight": 0.09459139,
+        "stop_loss_pct": 0.006,
+        "take_profit_pct": 0.018
+      },
+      "signal": false,
+      "suggested_weight": 0.09459139,
+      "symbol": "ETH-USDT-SWAP"
+    },
+    {
+      "bar": "5m",
+      "decision_candle_time": "2026-04-29T01:55:00Z",
+      "decision_candle_ts": 1777427700000,
+      "dry_run_action": "hold",
+      "entry_rule": "btc_return >= threshold and btc_return - eth_return >= lag_gap",
+      "exit_signal": false,
+      "family": "btc_lead_eth_lag",
+      "indicators": {
+        "btc_close": 76534.3,
+        "btc_return": 0.0028355077904398396,
+        "eth_close": 2291.56,
+        "eth_return": 0.0025067480958775867,
+        "return_gap": 0.00032875969456225285
+      },
+      "latest_local_candle_time": "2026-04-29T02:00:00Z",
+      "latest_local_candle_ts": 1777428000000,
+      "leg_id": "btc_lead_eth_lag_5m",
+      "needs_cancel": false,
+      "needs_order": false,
+      "params": {
+        "btc_return_threshold": 0.012,
+        "lag_gap": 0.006,
+        "lead_lookback": 16,
+        "max_hold_bars": 8,
+        "stop_loss_pct": 0.006,
+        "take_profit_pct": 0.018
+      },
+      "risk_limits": {
+        "leverage": 3,
+        "max_hold_bars": 8,
+        "max_weight": 0.10226104,
+        "stop_loss_pct": 0.006,
+        "take_profit_pct": 0.018
+      },
+      "signal": false,
+      "suggested_weight": 0.10226104,
+      "symbol": "ETH-USDT-SWAP"
+    }
+  ],
+  "sources": {
+    "btc_rsi2_strategy": "freqtrade/user_data/strategies/BtcRsi2Guarded.py",
+    "export_script": "scripts/export_freqtrade_data.py",
+    "final_report": "reports/eth-exploration/eth-conservative-portfolio-final.json",
+    "freqtrade_config": "freqtrade/config-okx-futures.json",
+    "freqtrade_readme": "freqtrade/README.md",
+    "signal_intent": "reports/eth-exploration/eth-focused-portfolio-signal-intent.json"
+  }
+}

+ 420 - 0
reports/eth-exploration/eth-focused-portfolio-freqtrade-20260429T182815Z.md

@@ -0,0 +1,420 @@
+# ETH-focused portfolio freqtrade comparison
+
+Dry-run research only. No strategy file was created and no order path was called.
+
+## Existing freqtrade surface
+
+- Config: `freqtrade/config-okx-futures.json` uses `futures` / `isolated` with `json` OHLCV.
+- Current whitelist: `['BTC/USDT:USDT']`.
+- Existing strategy: `freqtrade/user_data/strategies/BtcRsi2Guarded.py` has `populate_indicators`, entry/exit signals, `custom_exit`, and fixed leverage callback.
+- Export path: `scripts/export_freqtrade_data.py` already supports `ETH-USDT-SWAP` and `BTC-USDT-SWAP`.
+
+## Leg mapping
+
+| Leg family | Freqtrade fit | Needs | Migration risk |
+| --- | --- | --- | --- |
+| `eth_btc_rsi_filter` | `direct_strategy_logic_with_btc_informative_pair` | ETH/USDT:USDT base pair; BTC/USDT:USDT informative pair; 15m candles; stake sizing from portfolio weight if used as a portfolio leg | `low_for_signal_comparison` |
+| `btc_lead_eth_lag` | `direct_strategy_logic_with_btc_informative_pair` | 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 | `low_for_single_leg_backtest_medium_for_multi_leg_portfolio_accounting` |
+| `eth_robust_twap` | `partial_only` | 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 | `high_for_execution_fidelity` |
+
+## Data import
+
+Existing cached OKX candles can be exported into Freqtrade JSON futures format:
+
+- `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`
+
+Expected 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`
+
+## 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
+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)
+```
+
+## Decision
+
+- Freqtrade next step: `yes_for_signal_and_accounting_comparison_of_non_maker_legs`.
+- Freqtrade 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.
+
+## JSON
+
+```json
+{
+  "decision": {
+    "freqtrade_as_execution_migration": "no_for_primary_P1_if_eth_robust_twap_remains_required",
+    "freqtrade_as_next_step": "yes_for_signal_and_accounting_comparison_of_non_maker_legs",
+    "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."
+  },
+  "existing_freqtrade_surface": {
+    "config_pair_whitelist": [
+      "BTC/USDT:USDT"
+    ],
+    "dataformat_ohlcv": "json",
+    "export_supports_eth": true,
+    "margin_mode": "isolated",
+    "readme_marks_comparison_not_replacement": true,
+    "strategy_class_present": true,
+    "strategy_uses_custom_exit": true,
+    "strategy_uses_leverage_callback": true,
+    "timeframe": "15m",
+    "trading_mode": "futures"
+  },
+  "generated_at": "2026-04-29T18:28:15Z",
+  "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"
+      ],
+      "migration_risk": "low_for_signal_comparison",
+      "requires": [
+        "ETH/USDT:USDT base pair",
+        "BTC/USDT:USDT informative pair",
+        "15m candles",
+        "stake sizing from portfolio weight if used as a portfolio leg"
+      ]
+    },
+    {
+      "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"
+      ],
+      "migration_risk": "low_for_single_leg_backtest_medium_for_multi_leg_portfolio_accounting",
+      "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"
+      ]
+    },
+    {
+      "family": "eth_robust_twap",
+      "freqtrade_fit": "partial_only",
+      "maps_to": [
+        "base RSI2 guarded long trigger",
+        "fixed leverage",
+        "max hold and stop exit"
+      ],
+      "migration_risk": "high_for_execution_fidelity",
+      "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"
+      ]
+    }
+  ],
+  "minimum_strategy_skeleton": {
+    "can_generate": true,
+    "not_generated_in_this_task": true,
+    "reason": "The request asks for a read-only comparison/report and no main strategy submission.",
+    "recommended_first_target": "no_maker_dependent ETH/BTC RSI filter + BTC lead ETH lag, because it avoids TWAP maker-fill lifecycle assumptions."
+  },
+  "mode": "readonly_freqtrade_comparison",
+  "okx_futures_data_import": {
+    "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."
+    ],
+    "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"
+    ]
+  },
+  "portfolio_candidates": [
+    {
+      "decision": "Primary next item to watch in paper/demo. It is the cleanest qualified portfolio by conservative sort, but it contains a maker-dependent TWAP leg, so real funds should wait for live fill evidence.",
+      "legs": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt480-bm240-br0.0",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb8-br0.018-gap0.006-mh8-sl0.006-tp0.018",
+        "eth_robust_twap:15m:rsi2-long-guarded-price-twap-o0.0030-0.0060-0.0090-v4-t60-l3.0-x50.0-sl0.012-mh48-fb0.0005-ps0.0000-mm25"
+      ],
+      "metrics": {
+        "cost_model": "maker_taker",
+        "max_horizon_drawdown": 0.06650656143862783,
+        "min_horizon_total_return": 0.014041380756578459,
+        "net_annualized_return": 0.07431265319093083,
+        "net_calmar": 1.0203012338692994,
+        "net_max_drawdown": 0.07283403246423034,
+        "net_total_return": 0.5746697040909923,
+        "scope": "all_legs",
+        "worst_month_return": -0.04435071071689678
+      },
+      "minimum_next_step": "Run quasi-live read-only/order-intent tracking for all legs and record per-leg signal, fill/miss, slippage, and portfolio equity for at least the next signal cycle set.",
+      "name": "all_legs-risk-3-c0124-eth_btc_rsi_filter+btc_lead_eth_lag_15m+eth_robust_twap",
+      "needs_forward_or_demo_live": true,
+      "priority": 1,
+      "real_live_now": false,
+      "status": "candidate",
+      "title": "Lowest-drawdown ETH-focused conservative portfolio",
+      "weights": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt480-bm240-br0.0=0.48537471",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb8-br0.018-gap0.006-mh8-sl0.006-tp0.018=0.06171009",
+        "eth_robust_twap:15m:rsi2-long-guarded-price-twap-o0.0030-0.0060-0.0090-v4-t60-l3.0-x50.0-sl0.012-mh48-fb0.0005-ps0.0000-mm25=0.45291520"
+      ]
+    },
+    {
+      "decision": "Best fallback if maker-fill uncertainty is treated as disqualifying. It keeps qualified portfolio behavior without the robust TWAP maker-dependent leg.",
+      "legs": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt120-bm240-br0.0",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb8-br0.018-gap0.006-mh8-sl0.006-tp0.018"
+      ],
+      "metrics": {
+        "cost_model": "maker_taker",
+        "max_horizon_drawdown": 0.07437399760660594,
+        "min_horizon_total_return": 0.01722844178015248,
+        "net_annualized_return": 0.0900769354075468,
+        "net_calmar": 0.8611995911104812,
+        "net_max_drawdown": 0.10459472616724809,
+        "net_total_return": 0.7268826579747691,
+        "scope": "no_maker_dependent",
+        "worst_month_return": -0.04507919732709942
+      },
+      "minimum_next_step": "Shadow the two legs side by side with the primary portfolio and compare realized signal overlap and drawdown path.",
+      "name": "no_maker_dependent-risk-2-c0250-eth_btc_rsi_filter+btc_lead_eth_lag_15m",
+      "needs_forward_or_demo_live": true,
+      "priority": 2,
+      "real_live_now": false,
+      "status": "candidate",
+      "title": "Simpler no-maker-dependent conservative portfolio",
+      "weights": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt120-bm240-br0.0=0.89432334",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb8-br0.018-gap0.006-mh8-sl0.006-tp0.018=0.10567666"
+      ]
+    },
+    {
+      "decision": "Return-oriented qualified variant. It is less conservative than priority 1 because drawdown is materially higher.",
+      "legs": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt480-bm240-br0.0",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb16-br0.024-gap0.006-mh32-sl0.006-tp0.018"
+      ],
+      "metrics": {
+        "cost_model": "maker_taker",
+        "max_horizon_drawdown": 0.08384178749425959,
+        "min_horizon_total_return": 0.021522808059551757,
+        "net_annualized_return": 0.13247985480488844,
+        "net_calmar": 0.8214839555775263,
+        "net_max_drawdown": 0.1612689498138176,
+        "net_total_return": 1.1990870259436797,
+        "scope": "all_legs",
+        "worst_month_return": -0.056624480428019486
+      },
+      "minimum_next_step": "Keep as a benchmark portfolio in the same quasi-live tracker; do not allocate before it beats priority 1 on realized drawdown-adjusted behavior.",
+      "name": "all_legs-risk-2-c0020-eth_btc_rsi_filter+btc_lead_eth_lag_15m",
+      "needs_forward_or_demo_live": true,
+      "priority": 3,
+      "real_live_now": false,
+      "status": "candidate",
+      "title": "Highest-return qualified conservative portfolio",
+      "weights": [
+        "eth_btc_rsi_filter:15m:eth-btc-rsi-filter-et50-l3.0-x55.0-bt480-bm240-br0.0=0.89038020",
+        "btc_lead_eth_lag_15m:15m:btc-lead-eth-lag-lb16-br0.024-gap0.006-mh32-sl0.006-tp0.018=0.10961980"
+      ]
+    },
+    {
+      "decision": "Keep as secondary watchlist, not primary allocation.",
+      "metrics": {
+        "net_annualized_return": 0.3305936472652691,
+        "net_calmar": 1.4902378159621057,
+        "net_max_drawdown": 0.22183952368155146,
+        "net_total_return": 5.132970549912916,
+        "trades": 88.0,
+        "worst_month": "2020-09",
+        "worst_month_return": -0.10650826274141434
+      },
+      "minimum_next_step": "Run shadow/demo signal intent logging only; require fresh forward trades before any capital allocation because the acceptable-risk versions have few trades.",
+      "name": "bb-squeeze-l96-bw960-q0.25-sl0.01-tpnone-long-btc-up-momo-vc0.006-dd0.25-cd24",
+      "needs_forward_or_demo_live": true,
+      "priority": 4,
+      "real_live_now": false,
+      "status": "watchlist",
+      "title": "BB squeeze risk candidate"
+    }
+  ],
+  "signal_intent_legs": [
+    {
+      "bar": "15m",
+      "decision_candle_time": "2026-04-29T16:30:00Z",
+      "decision_candle_ts": 1777480200000,
+      "dry_run_action": "hold",
+      "entry_rule": "btc_close > btc_sma and btc_momentum >= minimum and eth_close > eth_sma and eth_rsi2 <= threshold",
+      "exit_signal": true,
+      "family": "eth_btc_rsi_filter",
+      "indicators": {
+        "btc_close": 75856.0,
+        "btc_momentum": -0.040101233786776325,
+        "btc_sma": 77331.09999999992,
+        "eth_close": 2266.75,
+        "eth_rsi2": 10.743438715792621,
+        "eth_sma": 2315.6154000000006
+      },
+      "latest_local_candle_time": "2026-04-29T16:45:00Z",
+      "latest_local_candle_ts": 1777481100000,
+      "leg_id": "eth_btc_rsi_filter_15m",
+      "needs_cancel": false,
+      "needs_order": false,
+      "params": {
+        "btc_min_momentum": 0.0,
+        "btc_momentum_lookback": 240,
+        "btc_trend_sma": 480,
+        "eth_exit_rsi": 55.0,
+        "eth_rsi_threshold": 3.0,
+        "eth_trend_sma": 50
+      },
+      "risk_limits": {
+        "leverage": 3,
+        "max_hold_bars": null,
+        "max_weight": 0.80314757,
+        "stop_loss_pct": null,
+        "take_profit_pct": null
+      },
+      "signal": false,
+      "suggested_weight": 0.80314757,
+      "symbol": "ETH-USDT-SWAP"
+    },
+    {
+      "bar": "15m",
+      "decision_candle_time": "2026-04-29T16:30:00Z",
+      "decision_candle_ts": 1777480200000,
+      "dry_run_action": "hold",
+      "entry_rule": "btc_return >= threshold and btc_return - eth_return >= lag_gap",
+      "exit_signal": false,
+      "family": "btc_lead_eth_lag",
+      "indicators": {
+        "btc_close": 75856.0,
+        "btc_return": -0.009247165769813437,
+        "eth_close": 2266.75,
+        "eth_return": -0.014409384796664093,
+        "return_gap": 0.005162219026850656
+      },
+      "latest_local_candle_time": "2026-04-29T16:45:00Z",
+      "latest_local_candle_ts": 1777481100000,
+      "leg_id": "btc_lead_eth_lag_15m",
+      "needs_cancel": false,
+      "needs_order": false,
+      "params": {
+        "btc_return_threshold": 0.018,
+        "lag_gap": 0.006,
+        "lead_lookback": 8,
+        "max_hold_bars": 8,
+        "stop_loss_pct": 0.006,
+        "take_profit_pct": 0.018
+      },
+      "risk_limits": {
+        "leverage": 3,
+        "max_hold_bars": 8,
+        "max_weight": 0.09459139,
+        "stop_loss_pct": 0.006,
+        "take_profit_pct": 0.018
+      },
+      "signal": false,
+      "suggested_weight": 0.09459139,
+      "symbol": "ETH-USDT-SWAP"
+    },
+    {
+      "bar": "5m",
+      "decision_candle_time": "2026-04-29T01:55:00Z",
+      "decision_candle_ts": 1777427700000,
+      "dry_run_action": "hold",
+      "entry_rule": "btc_return >= threshold and btc_return - eth_return >= lag_gap",
+      "exit_signal": false,
+      "family": "btc_lead_eth_lag",
+      "indicators": {
+        "btc_close": 76534.3,
+        "btc_return": 0.0028355077904398396,
+        "eth_close": 2291.56,
+        "eth_return": 0.0025067480958775867,
+        "return_gap": 0.00032875969456225285
+      },
+      "latest_local_candle_time": "2026-04-29T02:00:00Z",
+      "latest_local_candle_ts": 1777428000000,
+      "leg_id": "btc_lead_eth_lag_5m",
+      "needs_cancel": false,
+      "needs_order": false,
+      "params": {
+        "btc_return_threshold": 0.012,
+        "lag_gap": 0.006,
+        "lead_lookback": 16,
+        "max_hold_bars": 8,
+        "stop_loss_pct": 0.006,
+        "take_profit_pct": 0.018
+      },
+      "risk_limits": {
+        "leverage": 3,
+        "max_hold_bars": 8,
+        "max_weight": 0.10226104,
+        "stop_loss_pct": 0.006,
+        "take_profit_pct": 0.018
+      },
+      "signal": false,
+      "suggested_weight": 0.10226104,
+      "symbol": "ETH-USDT-SWAP"
+    }
+  ],
+  "sources": {
+    "btc_rsi2_strategy": "freqtrade/user_data/strategies/BtcRsi2Guarded.py",
+    "export_script": "scripts/export_freqtrade_data.py",
+    "final_report": "reports/eth-exploration/eth-conservative-portfolio-final.json",
+    "freqtrade_config": "freqtrade/config-okx-futures.json",
+    "freqtrade_readme": "freqtrade/README.md",
+    "signal_intent": "reports/eth-exploration/eth-focused-portfolio-signal-intent.json"
+  }
+}
+```

+ 144 - 0
reports/eth-exploration/eth-focused-portfolio-live-readiness.json

@@ -0,0 +1,144 @@
+{
+  "checklist": [
+    {
+      "category": "must",
+      "evidence": [
+        "Current place_order chooses ordType market when entry_price is absent and limit when entry_price is present.",
+        "The only trade submit endpoint in code is /api/v5/trade/order."
+      ],
+      "gap": "No code path emits ordType=post_only or deterministic client order ids for ETH portfolio entries.",
+      "key": "post_only",
+      "minimum_task": "Add a read-only order-intent builder that renders the exact post_only order payloads for each required entry level without submitting them.",
+      "status": "missing",
+      "title": "OKX post-only entry support"
+    },
+    {
+      "category": "must",
+      "evidence": [
+        "Current OKX client has one single-order place_order method.",
+        "No /api/v5/trade/batch-orders endpoint or multi-order method is present."
+      ],
+      "gap": "The conservative portfolio can require multiple leg/order intents; the client cannot express an atomic or coordinated batch.",
+      "key": "batch_orders",
+      "minimum_task": "Define the portfolio order-intent shape for all active legs and add a non-submitting batch payload renderer.",
+      "status": "missing",
+      "title": "Batch entry order support"
+    },
+    {
+      "category": "must",
+      "evidence": [
+        "No cancel_order, cancel_batch_orders, or orders-pending method exists in okx_client.py.",
+        "The signal-intent report always sets needs_cancel to False and no_cancel_submission to True."
+      ],
+      "gap": "A quasi-live loop cannot expire stale maker orders or clear outstanding leg orders.",
+      "key": "cancel_open_orders",
+      "minimum_task": "Add read-only cancel-intent generation from tracked open order ids, then add client cancel/list methods before any live runner is enabled.",
+      "status": "missing",
+      "title": "Cancel open orders"
+    },
+    {
+      "category": "must",
+      "evidence": [
+        "paper_engine.py persists paper_state.json.",
+        "Existing paper state assumes immediate local fills and has no exchange order lifecycle."
+      ],
+      "gap": "There is no dedicated ETH portfolio state containing signal clock, order ids, fills, exposure, and audit events.",
+      "key": "state_persistence",
+      "minimum_task": "Add a dedicated ETH portfolio state schema and read/write command for quasi-live intent tracking.",
+      "status": "partial",
+      "title": "State persistence"
+    },
+    {
+      "category": "must",
+      "evidence": [
+        "place_order calls ensure_hedge_mode before submitting.",
+        "set_leverage sends mgnMode=isolated and place_order sends tdMode=isolated with posSide."
+      ],
+      "gap": "Isolation exists only inside the single-order submitter; portfolio readiness still needs the same fields in generated leg/order intents and close intents.",
+      "key": "position_isolation",
+      "minimum_task": "Require tdMode=isolated, posSide=long, and bounded leverage in every generated ETH portfolio intent.",
+      "status": "partial",
+      "title": "Position isolation"
+    },
+    {
+      "category": "must",
+      "evidence": [
+        "okx-account reads positions.",
+        "okx-order does not check existing ETH exposure before calling place_order."
+      ],
+      "gap": "A future runner could merge with or alter pre-existing ETH-USDT-SWAP exposure in the same account.",
+      "key": "existing_position_protection",
+      "minimum_task": "Before any submit-capable command, require zero conflicting ETH-USDT-SWAP exposure or an explicitly dedicated state-owned position id.",
+      "status": "missing",
+      "title": "Existing position protection"
+    },
+    {
+      "category": "must",
+      "evidence": [
+        "build_eth_focused_portfolio_signal_intent.py evaluates cached candles once and writes dry-run output.",
+        "No scheduler, last-confirmed-candle state, or one-cycle-per-candle guard exists in CLI code."
+      ],
+      "gap": "The repo cannot run a quasi-live candle-bound signal loop for the ETH-focused portfolio.",
+      "key": "signal_scheduling",
+      "minimum_task": "Add a read-only quasi-live runner that records last confirmed candle per leg and emits intent only when a leg clock advances.",
+      "status": "missing",
+      "title": "Signal scheduling"
+    },
+    {
+      "category": "must",
+      "evidence": [
+        "No logging module usage is present in okx_codex_trader.",
+        "Existing reports are generated snapshots, not append-only runtime logs."
+      ],
+      "gap": "There is no durable audit trail for signal decisions, payloads, cancel intents, fills, or state transitions.",
+      "key": "logs",
+      "minimum_task": "Add append-only JSONL event logging for read-only signal/order/cancel intent cycles.",
+      "status": "missing",
+      "title": "Operational logs"
+    }
+  ],
+  "created_at": "2026-04-29T18:28:25Z",
+  "current_code_facts": {
+    "batch_endpoint_in_client_code": false,
+    "cancel_endpoint_in_client_code": false,
+    "cli_has_okx_order_command": true,
+    "live_plan_mentions_required_lifecycle": true,
+    "paper_state_present": true,
+    "portfolio_intent_is_readonly": true,
+    "post_only_in_client_code": false,
+    "single_order_submitter_present": true
+  },
+  "minimum_implementation_tasks": {
+    "must": [
+      "Add a read-only order-intent builder that renders the exact post_only order payloads for each required entry level without submitting them.",
+      "Define the portfolio order-intent shape for all active legs and add a non-submitting batch payload renderer.",
+      "Add read-only cancel-intent generation from tracked open order ids, then add client cancel/list methods before any live runner is enabled.",
+      "Add a dedicated ETH portfolio state schema and read/write command for quasi-live intent tracking.",
+      "Require tdMode=isolated, posSide=long, and bounded leverage in every generated ETH portfolio intent.",
+      "Before any submit-capable command, require zero conflicting ETH-USDT-SWAP exposure or an explicitly dedicated state-owned position id.",
+      "Add a read-only quasi-live runner that records last confirmed candle per leg and emits intent only when a leg clock advances.",
+      "Add append-only JSONL event logging for read-only signal/order/cancel intent cycles."
+    ],
+    "optional": [
+      "Add a demo-only execution adapter after read-only intent/state/logging proves one full signal cycle.",
+      "Add reduce-only close-intent rendering and tests before adding any close submit path.",
+      "Add portfolio-level exposure reports comparing intended weight, tracked exchange exposure, and cash limits."
+    ]
+  },
+  "readiness": {
+    "can_submit_orders_now": true,
+    "ready_for_quasi_live": false,
+    "reason": "Required order lifecycle, state ownership, scheduling, and audit pieces are missing or only partial.",
+    "should_submit_orders_now": false
+  },
+  "report": "eth-focused-portfolio-live-readiness",
+  "scope": "read-only static repository inspection; no OKX request, no order, no cancel",
+  "source_files": {
+    "cli": "okx_codex_trader/cli.py",
+    "live_plan": "reports/eth-exploration/eth-robust-twap-live-plan.md",
+    "okx_client": "okx_codex_trader/okx_client.py",
+    "paper_engine": "okx_codex_trader/paper_engine.py",
+    "portfolio_report": "reports/eth-exploration/eth-focused-portfolio-conservative-report.md",
+    "signal_intent": "scripts/build_eth_focused_portfolio_signal_intent.py"
+  }
+}

+ 78 - 0
reports/eth-exploration/eth-focused-portfolio-live-readiness.md

@@ -0,0 +1,78 @@
+# ETH-focused portfolio live readiness
+
+Read-only static inspection. No OKX request, order, or cancel was made.
+
+## Topline
+
+- Ready for quasi-live: `False`
+- Submit-capable code exists now: `True`
+- Should submit now: `False`
+- Reason: Required order lifecycle, state ownership, scheduling, and audit pieces are missing or only partial.
+
+## Checklist
+
+| Check | Status | Category | Gap | Minimum task |
+| --- | --- | --- | --- | --- |
+| OKX post-only entry support | missing | must | No code path emits ordType=post_only or deterministic client order ids for ETH portfolio entries. | Add a read-only order-intent builder that renders the exact post_only order payloads for each required entry level without submitting them. |
+| Batch entry order support | missing | must | The conservative portfolio can require multiple leg/order intents; the client cannot express an atomic or coordinated batch. | Define the portfolio order-intent shape for all active legs and add a non-submitting batch payload renderer. |
+| Cancel open orders | missing | must | A quasi-live loop cannot expire stale maker orders or clear outstanding leg orders. | Add read-only cancel-intent generation from tracked open order ids, then add client cancel/list methods before any live runner is enabled. |
+| State persistence | partial | must | There is no dedicated ETH portfolio state containing signal clock, order ids, fills, exposure, and audit events. | Add a dedicated ETH portfolio state schema and read/write command for quasi-live intent tracking. |
+| Position isolation | partial | must | Isolation exists only inside the single-order submitter; portfolio readiness still needs the same fields in generated leg/order intents and close intents. | Require tdMode=isolated, posSide=long, and bounded leverage in every generated ETH portfolio intent. |
+| Existing position protection | missing | must | A future runner could merge with or alter pre-existing ETH-USDT-SWAP exposure in the same account. | Before any submit-capable command, require zero conflicting ETH-USDT-SWAP exposure or an explicitly dedicated state-owned position id. |
+| Signal scheduling | missing | must | The repo cannot run a quasi-live candle-bound signal loop for the ETH-focused portfolio. | Add a read-only quasi-live runner that records last confirmed candle per leg and emits intent only when a leg clock advances. |
+| Operational logs | missing | must | There is no durable audit trail for signal decisions, payloads, cancel intents, fills, or state transitions. | Add append-only JSONL event logging for read-only signal/order/cancel intent cycles. |
+
+## Must implement
+
+| # | Task |
+| --- | --- |
+| 1 | Add a read-only order-intent builder that renders the exact post_only order payloads for each required entry level without submitting them. |
+| 2 | Define the portfolio order-intent shape for all active legs and add a non-submitting batch payload renderer. |
+| 3 | Add read-only cancel-intent generation from tracked open order ids, then add client cancel/list methods before any live runner is enabled. |
+| 4 | Add a dedicated ETH portfolio state schema and read/write command for quasi-live intent tracking. |
+| 5 | Require tdMode=isolated, posSide=long, and bounded leverage in every generated ETH portfolio intent. |
+| 6 | Before any submit-capable command, require zero conflicting ETH-USDT-SWAP exposure or an explicitly dedicated state-owned position id. |
+| 7 | Add a read-only quasi-live runner that records last confirmed candle per leg and emits intent only when a leg clock advances. |
+| 8 | Add append-only JSONL event logging for read-only signal/order/cancel intent cycles. |
+
+## Optional
+
+| # | Task |
+| --- | --- |
+| 1 | Add a demo-only execution adapter after read-only intent/state/logging proves one full signal cycle. |
+| 2 | Add reduce-only close-intent rendering and tests before adding any close submit path. |
+| 3 | Add portfolio-level exposure reports comparing intended weight, tracked exchange exposure, and cash limits. |
+
+## Evidence
+
+### OKX post-only entry support
+- Current place_order chooses ordType market when entry_price is absent and limit when entry_price is present.
+- The only trade submit endpoint in code is /api/v5/trade/order.
+
+### Batch entry order support
+- Current OKX client has one single-order place_order method.
+- No /api/v5/trade/batch-orders endpoint or multi-order method is present.
+
+### Cancel open orders
+- No cancel_order, cancel_batch_orders, or orders-pending method exists in okx_client.py.
+- The signal-intent report always sets needs_cancel to False and no_cancel_submission to True.
+
+### State persistence
+- paper_engine.py persists paper_state.json.
+- Existing paper state assumes immediate local fills and has no exchange order lifecycle.
+
+### Position isolation
+- place_order calls ensure_hedge_mode before submitting.
+- set_leverage sends mgnMode=isolated and place_order sends tdMode=isolated with posSide.
+
+### Existing position protection
+- okx-account reads positions.
+- okx-order does not check existing ETH exposure before calling place_order.
+
+### Signal scheduling
+- build_eth_focused_portfolio_signal_intent.py evaluates cached candles once and writes dry-run output.
+- No scheduler, last-confirmed-candle state, or one-cycle-per-candle guard exists in CLI code.
+
+### Operational logs
+- No logging module usage is present in okx_codex_trader.
+- Existing reports are generated snapshots, not append-only runtime logs.

+ 278 - 0
scripts/check_eth_focused_portfolio_live_readiness.py

@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import json
+from datetime import UTC, datetime
+from pathlib import Path
+from typing import Any
+
+
+ROOT = Path(__file__).resolve().parents[1]
+REPORT_DIR = ROOT / "reports" / "eth-exploration"
+MD_OUT = REPORT_DIR / "eth-focused-portfolio-live-readiness.md"
+JSON_OUT = REPORT_DIR / "eth-focused-portfolio-live-readiness.json"
+
+SOURCE_FILES = {
+    "okx_client": ROOT / "okx_codex_trader" / "okx_client.py",
+    "cli": ROOT / "okx_codex_trader" / "cli.py",
+    "paper_engine": ROOT / "okx_codex_trader" / "paper_engine.py",
+    "signal_intent": ROOT / "scripts" / "build_eth_focused_portfolio_signal_intent.py",
+    "live_plan": REPORT_DIR / "eth-robust-twap-live-plan.md",
+    "portfolio_report": REPORT_DIR / "eth-focused-portfolio-conservative-report.md",
+}
+
+
+def read_sources() -> dict[str, str]:
+    return {name: path.read_text(encoding="utf-8") for name, path in SOURCE_FILES.items() if path.exists()}
+
+
+def has_all(text: str, needles: tuple[str, ...]) -> bool:
+    return all(needle in text for needle in needles)
+
+
+def item(
+    key: str,
+    title: str,
+    status: str,
+    evidence: list[str],
+    gap: str,
+    minimum_task: str,
+    category: str,
+) -> dict[str, Any]:
+    return {
+        "key": key,
+        "title": title,
+        "status": status,
+        "evidence": evidence,
+        "gap": gap,
+        "minimum_task": minimum_task,
+        "category": category,
+    }
+
+
+def build_payload() -> dict[str, Any]:
+    sources = read_sources()
+    okx = sources.get("okx_client", "")
+    cli = sources.get("cli", "")
+    paper = sources.get("paper_engine", "")
+    intent = sources.get("signal_intent", "")
+    plan = sources.get("live_plan", "")
+
+    checks = [
+        item(
+            "post_only",
+            "OKX post-only entry support",
+            "missing",
+            [
+                "Current place_order chooses ordType market when entry_price is absent and limit when entry_price is present.",
+                "The only trade submit endpoint in code is /api/v5/trade/order.",
+            ],
+            "No code path emits ordType=post_only or deterministic client order ids for ETH portfolio entries.",
+            "Add a read-only order-intent builder that renders the exact post_only order payloads for each required entry level without submitting them.",
+            "must",
+        ),
+        item(
+            "batch_orders",
+            "Batch entry order support",
+            "missing",
+            [
+                "Current OKX client has one single-order place_order method.",
+                "No /api/v5/trade/batch-orders endpoint or multi-order method is present.",
+            ],
+            "The conservative portfolio can require multiple leg/order intents; the client cannot express an atomic or coordinated batch.",
+            "Define the portfolio order-intent shape for all active legs and add a non-submitting batch payload renderer.",
+            "must",
+        ),
+        item(
+            "cancel_open_orders",
+            "Cancel open orders",
+            "missing",
+            [
+                "No cancel_order, cancel_batch_orders, or orders-pending method exists in okx_client.py.",
+                "The signal-intent report always sets needs_cancel to False and no_cancel_submission to True.",
+            ],
+            "A quasi-live loop cannot expire stale maker orders or clear outstanding leg orders.",
+            "Add read-only cancel-intent generation from tracked open order ids, then add client cancel/list methods before any live runner is enabled.",
+            "must",
+        ),
+        item(
+            "state_persistence",
+            "State persistence",
+            "partial",
+            [
+                "paper_engine.py persists paper_state.json.",
+                "Existing paper state assumes immediate local fills and has no exchange order lifecycle.",
+            ],
+            "There is no dedicated ETH portfolio state containing signal clock, order ids, fills, exposure, and audit events.",
+            "Add a dedicated ETH portfolio state schema and read/write command for quasi-live intent tracking.",
+            "must",
+        ),
+        item(
+            "position_isolation",
+            "Position isolation",
+            "partial",
+            [
+                "place_order calls ensure_hedge_mode before submitting.",
+                "set_leverage sends mgnMode=isolated and place_order sends tdMode=isolated with posSide.",
+            ],
+            "Isolation exists only inside the single-order submitter; portfolio readiness still needs the same fields in generated leg/order intents and close intents.",
+            "Require tdMode=isolated, posSide=long, and bounded leverage in every generated ETH portfolio intent.",
+            "must",
+        ),
+        item(
+            "existing_position_protection",
+            "Existing position protection",
+            "missing",
+            [
+                "okx-account reads positions.",
+                "okx-order does not check existing ETH exposure before calling place_order.",
+            ],
+            "A future runner could merge with or alter pre-existing ETH-USDT-SWAP exposure in the same account.",
+            "Before any submit-capable command, require zero conflicting ETH-USDT-SWAP exposure or an explicitly dedicated state-owned position id.",
+            "must",
+        ),
+        item(
+            "signal_scheduling",
+            "Signal scheduling",
+            "missing",
+            [
+                "build_eth_focused_portfolio_signal_intent.py evaluates cached candles once and writes dry-run output.",
+                "No scheduler, last-confirmed-candle state, or one-cycle-per-candle guard exists in CLI code.",
+            ],
+            "The repo cannot run a quasi-live candle-bound signal loop for the ETH-focused portfolio.",
+            "Add a read-only quasi-live runner that records last confirmed candle per leg and emits intent only when a leg clock advances.",
+            "must",
+        ),
+        item(
+            "logs",
+            "Operational logs",
+            "missing",
+            [
+                "No logging module usage is present in okx_codex_trader.",
+                "Existing reports are generated snapshots, not append-only runtime logs.",
+            ],
+            "There is no durable audit trail for signal decisions, payloads, cancel intents, fills, or state transitions.",
+            "Add append-only JSONL event logging for read-only signal/order/cancel intent cycles.",
+            "must",
+        ),
+    ]
+
+    optional_tasks = [
+        "Add a demo-only execution adapter after read-only intent/state/logging proves one full signal cycle.",
+        "Add reduce-only close-intent rendering and tests before adding any close submit path.",
+        "Add portfolio-level exposure reports comparing intended weight, tracked exchange exposure, and cash limits.",
+    ]
+
+    readiness = {
+        "ready_for_quasi_live": False,
+        "can_submit_orders_now": has_all(okx, ('"/api/v5/trade/order"', "def place_order")),
+        "should_submit_orders_now": False,
+        "reason": "Required order lifecycle, state ownership, scheduling, and audit pieces are missing or only partial.",
+    }
+
+    return {
+        "report": "eth-focused-portfolio-live-readiness",
+        "created_at": datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z"),
+        "scope": "read-only static repository inspection; no OKX request, no order, no cancel",
+        "source_files": {name: str(path.relative_to(ROOT)) for name, path in SOURCE_FILES.items()},
+        "readiness": readiness,
+        "checklist": checks,
+        "minimum_implementation_tasks": {
+            "must": [check["minimum_task"] for check in checks if check["category"] == "must"],
+            "optional": optional_tasks,
+        },
+        "current_code_facts": {
+            "single_order_submitter_present": has_all(okx, ('"/api/v5/trade/order"', "def place_order")),
+            "post_only_in_client_code": '"post_only"' in okx or "'post_only'" in okx,
+            "batch_endpoint_in_client_code": "batch-orders" in okx,
+            "cancel_endpoint_in_client_code": "cancel-order" in okx or "cancel-batch-orders" in okx,
+            "paper_state_present": "def load_state" in paper and "def save_state" in paper,
+            "portfolio_intent_is_readonly": "submitted_orders" in intent and "no_order_submission" in intent,
+            "live_plan_mentions_required_lifecycle": "post_only" in plan and "state file" in plan.lower(),
+            "cli_has_okx_order_command": 'subparsers.add_parser("okx-order")' in cli,
+        },
+    }
+
+
+def md_table(rows: list[list[str]]) -> str:
+    lines = [
+        "| " + " | ".join(rows[0]) + " |",
+        "| " + " | ".join(["---"] * len(rows[0])) + " |",
+    ]
+    lines.extend("| " + " | ".join(row) + " |" for row in rows[1:])
+    return "\n".join(lines)
+
+
+def render_markdown(payload: dict[str, Any]) -> str:
+    rows = [["Check", "Status", "Category", "Gap", "Minimum task"]]
+    for check in payload["checklist"]:
+        rows.append(
+            [
+                check["title"],
+                check["status"],
+                check["category"],
+                check["gap"],
+                check["minimum_task"],
+            ]
+        )
+
+    must_rows = [["#", "Task"]]
+    for index, task in enumerate(payload["minimum_implementation_tasks"]["must"], start=1):
+        must_rows.append([str(index), task])
+
+    optional_rows = [["#", "Task"]]
+    for index, task in enumerate(payload["minimum_implementation_tasks"]["optional"], start=1):
+        optional_rows.append([str(index), task])
+
+    lines = [
+        "# ETH-focused portfolio live readiness",
+        "",
+        "Read-only static inspection. No OKX request, order, or cancel was made.",
+        "",
+        "## Topline",
+        "",
+        f"- Ready for quasi-live: `{payload['readiness']['ready_for_quasi_live']}`",
+        f"- Submit-capable code exists now: `{payload['readiness']['can_submit_orders_now']}`",
+        f"- Should submit now: `{payload['readiness']['should_submit_orders_now']}`",
+        f"- Reason: {payload['readiness']['reason']}",
+        "",
+        "## Checklist",
+        "",
+        md_table(rows),
+        "",
+        "## Must implement",
+        "",
+        md_table(must_rows),
+        "",
+        "## Optional",
+        "",
+        md_table(optional_rows),
+        "",
+        "## Evidence",
+        "",
+    ]
+    for check in payload["checklist"]:
+        lines.append(f"### {check['title']}")
+        for evidence in check["evidence"]:
+            lines.append(f"- {evidence}")
+        lines.append("")
+    return "\n".join(lines).rstrip() + "\n"
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description="Check ETH-focused portfolio quasi-live readiness without trading.")
+    parser.add_argument("--no-write", action="store_true")
+    args = parser.parse_args()
+
+    payload = build_payload()
+    if not args.no_write:
+        REPORT_DIR.mkdir(parents=True, exist_ok=True)
+        JSON_OUT.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
+        MD_OUT.write_text(render_markdown(payload), encoding="utf-8")
+    print(json.dumps(payload, indent=2, sort_keys=True))
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 259 - 0
scripts/compare_eth_focused_portfolio_freqtrade.py

@@ -0,0 +1,259 @@
+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())