# ETH Robust Price TWAP 15m live execution plan This is an execution design only. It does not implement trading and does not submit orders. ## Source basis Read scope: - `okx_codex_trader/okx_client.py`: current OKX client signs requests, fetches confirmed history candles, reads balances and positions, and has only a single-order `place_order` path. - `okx_codex_trader/cli.py`: current trading commands are `paper-order`, `positions`, `okx-account`, and `okx-order`; the existing live submitter is intentionally not used by this plan. - `okx_codex_trader/paper_engine.py`: current paper state assumes immediate local fills and is not an exchange-order lifecycle model. - `freqtrade/README.md` and `freqtrade/user_data/strategies/BtcRsi2Guarded.py`: freqtrade is isolated as a comparison framework; it uses `process_only_new_candles = True` but currently contains only BTC RSI2 guarded logic, not ETH price TWAP execution. - `scripts/search_eth_twap_robustness_10y.py`, `scripts/search_eth_price_twap_variants.py`, and `scripts/explore_ultrashort.py`: these define the ETH price-TWAP mechanics used for this design. - `reports/eth-exploration/eth-twap-robustness-10y-summary.md`: selected candidate is `rsi2-long-guarded-price-twap-o0.0030-0.0060-0.0090-v4-t60-l3.0-x50.0-sl0.012-mh48`. Selected live design parameters: | Field | Value | | --- | --- | | Symbol | `ETH-USDT-SWAP` | | Timeframe | `15m` | | Direction | long only | | Leverage | `3` | | Trend | SMA(`close`, `60`) | | RSI | RSI(`close`, `2`) | | Entry signal | `close > trend` and `rsi2 <= 3.0` | | Entry levels | `0.003`, `0.006`, `0.009` below decision close | | Entry validity | `4` full 15m bars after decision candle | | Stop | `1.2%` below weighted average entry | | Exit signal | `rsi2 >= 50.0` or held bars `>= 48` | | Cost assumption | maker entry, taker exit | ## Signal clock The strategy computes only once per 15m candle close. The decision input is the latest OKX `history-candles` row for `ETH-USDT-SWAP`, `bar=15m`, whose confirm flag is `1`. The next cycle is skipped until that confirmed candle timestamp advances beyond `state.signal.last_confirmed_candle_ts`. The decision candle is the latest confirmed 15m candle. Indicator inputs are all confirmed candles up to and including that decision candle. The open 15m candle, ticker price, and any candle row without confirm flag `1` are not indicator inputs. Execution starts in the next 15m candle. If candle `T` closes and produces an entry signal, the three entry orders are created after `T` is confirmed and are eligible during candles `T+1`, `T+2`, `T+3`, and `T+4`. At the first scheduler tick after `T+4` closes, any unfilled entry orders from that signal are canceled. The minimum live runner cadence is one poll per minute. Signal calculation remains candle-bound; the extra polling only reconciles order state, partial fills, stop reach, and expiry. ## Entry orders Entry is exactly three isolated long limit orders with `post_only` order type: | Level | Limit price | | --- | --- | | L1 | `decision_close * (1 - 0.003)` | | L2 | `decision_close * (1 - 0.006)` | | L3 | `decision_close * (1 - 0.009)` | Each level receives one third of `planned_margin_usdt`. Contract size is computed per level with the existing sizing rule shape: ```text slice_margin = planned_margin_usdt / 3 notional = slice_margin * 3 raw_contracts = notional / (limit_price * ct_val) size = floor_to_lot_size(raw_contracts, lotSz) ``` If any slice is below `minSz`, the test margin is too small for this design and no live cycle should be started. The order-intent command should surface that as a hard configuration error before any submission command exists. Order payload shape needed in the future implementation: ```json { "instId": "ETH-USDT-SWAP", "tdMode": "isolated", "side": "buy", "posSide": "long", "ordType": "post_only", "px": "", "sz": "", "clOrdId": "ethrtwap15m--" } ``` Cancellation rules are direct: - Cancel all unfilled entry orders when the fourth valid bar has closed. - Cancel all unfilled entry orders immediately when the tracked long position is closed. - Cancel all unfilled entry orders immediately when a confirmed exit signal appears. - Cancel all unfilled entry orders immediately when the stop condition is reached. - Do not replace rejected `post_only` orders with taker orders. ## Partial fills The live position is built from exchange fills, not from submitted order sizes. For each fill: ```text fill_base_qty = fill_contracts * ct_val fill_notional_usdt = fill_base_qty * fill_price ``` The tracked average entry is: ```text avg_entry_price = sum(fill_notional_usdt) / sum(fill_base_qty) ``` The tracked margin is: ```text margin_used = sum(fill_notional_usdt) / leverage ``` After every new fill, recompute: ```text stop_price = avg_entry_price * (1 - 0.012) ``` `first_fill_ts` and `entry_candle_ts` are set by the first fill only. Held bars are counted from `entry_candle_ts` to the latest confirmed candle. If only one or two levels fill, the strategy runs with that partial position and cancels the remaining unfilled levels at expiry or exit. ## Exit and stop There are two exits. Signal exit uses confirmed candles only. If the latest confirmed candle has `rsi2 >= 50.0`, or if held bars from `entry_candle_ts` reach `48`, the runner cancels unfilled entries and closes the tracked long position. Stop exit is price-driven. Once a position exists, the runner watches the live mark or last price against `stop_price`. If price reaches or crosses `stop_price`, it cancels unfilled entries and closes the tracked long position immediately. Execution assumption for the first live design: | Action | Order assumption | Reason | | --- | --- | --- | | Entry | maker | `post_only` limit orders only | | Signal exit | taker | exposure must be closed once the confirmed exit condition exists | | Stop exit | taker | stop is a risk boundary, not a maker objective | The robustness report's `maker_taker` accounting is therefore the right planning cost model for this first live design. Maker exit can be designed later as a separate strategy; it is not part of this minimal path. Future close payload shape: ```json { "instId": "ETH-USDT-SWAP", "tdMode": "isolated", "side": "sell", "posSide": "long", "ordType": "market", "sz": "", "reduceOnly": "true", "clOrdId": "ethrtwap15m-exit-" } ``` ## State file Use a dedicated state file, for example `state/eth_robust_twap_15m_live.json`. It must be separate from `paper_state.json` because paper state has no exchange order lifecycle. Required fields: | Area | Fields | | --- | --- | | Identity | `strategy_id`, `symbol`, `bar`, `td_mode`, `pos_side`, `leverage` | | Signal | `last_confirmed_candle_ts`, `last_signal_candle_ts`, `trend_sma`, `rsi2`, `decision` | | Entry plan | `decision_close`, `entry_offsets`, `entry_valid_bars`, `expires_after_candle_ts` | | Orders | `client_order_id`, `okx_order_id`, `level`, `price`, `requested_size`, `filled_size`, `state`, `created_at`, `expires_at` | | Position | `filled_contracts`, `filled_base_qty`, `filled_notional_usdt`, `avg_entry_price`, `margin_used`, `stop_price`, `first_fill_ts`, `entry_candle_ts` | | Risk | `planned_margin_usdt`, `max_margin_usdt`, `account_isolation` | | Audit | append-only `events`, `updated_at` | State transitions are single-owner: ```text idle -> entry_orders_open -> partial_or_full_position -> closing -> idle ``` The runner should not start a new signal while state is `entry_orders_open`, `partial_or_full_position`, or `closing`. ## Minimum real-funds test Use OKX demo first. For real funds, use a dedicated OKX subaccount. That is the clean way to avoid touching an existing ETH position. If a subaccount is unavailable, the test account must have no existing `ETH-USDT-SWAP` long position before starting. The runner must query `okx-account --symbol ETH-USDT-SWAP` and require zero long size before any future live submit command is used. All test orders use deterministic client order ids with prefix `ethrtwap15m-`, so manual inspection can distinguish them from other account activity. Minimum test sequence: 1. Run the static plan check: `uv run python scripts/design_eth_robust_twap_live_plan.py`. 2. Build the future read-only signal command and run it through at least one full confirmed signal cycle without order submission. 3. Build the future order-intent command and verify three `post_only` payloads, rounded sizes, expiry timestamps, and stop price. 4. In demo, submit one complete three-level cycle with the smallest `planned_margin_usdt` that passes `minSz` for all three levels. 5. Wait until either fills occur or four valid bars close; verify unfilled orders are canceled. 6. If any position remains after the test, close it manually or with the future reduce-only close command, then verify `okx-account` reports no tracked long exposure. No current command should be used for this test as-is. Existing `okx-order` is single-order and can submit market or plain limit orders, but it does not express the required three-level `post_only` lifecycle. ## Commands and files Current commands that are useful as inputs: | Command | Use | | --- | --- | | `fetch-history` | confirmed candle source shape | | `okx-account` | account balance and existing position inspection | | `positions` | local paper positions only | | `paper-order` | not suitable for this plan because it assumes immediate fills | | `okx-order` | not suitable for this plan because it is a single-order submitter | Minimum implementation path: 1. Add a read-only signal command, for example `eth-robust-twap-signal`, that fetches confirmed 15m candles and emits the next-cycle entry plan. 2. Add a read-only order-intent command, for example `eth-robust-twap-order-intent`, that converts a signal into three `post_only` payloads and a state transition preview. 3. Add OKX client methods needed for lifecycle execution: submit post-only limit, query order, cancel order, list fills, and reduce-only close. 4. Add a state-backed quasi-live runner command only after the read-only commands are tested. 5. Add tests with fake OKX responses for confirmed-candle selection, indicator timing, level prices, lot rounding, partial fill average price, stop recomputation, expiry cancellation, and state transitions. Freqtrade should remain a comparison harness for this repo. This execution design should live in the existing OKX client and CLI path because it requires exchange order ids, client order ids, fill reconciliation, and explicit state transitions. ## Static check The added static check is: ```bash uv run python scripts/design_eth_robust_twap_live_plan.py ``` It validates that this report and the structured JSON plan exist, include the required sections, and do not describe direct use of the current executable order call.