eth-robust-twap-live-plan.md 11 KB

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:

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:

{
  "instId": "ETH-USDT-SWAP",
  "tdMode": "isolated",
  "side": "buy",
  "posSide": "long",
  "ordType": "post_only",
  "px": "<level_price>",
  "sz": "<level_contract_size>",
  "clOrdId": "ethrtwap15m-<signal_ts>-<level>"
}

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:

fill_base_qty = fill_contracts * ct_val
fill_notional_usdt = fill_base_qty * fill_price

The tracked average entry is:

avg_entry_price = sum(fill_notional_usdt) / sum(fill_base_qty)

The tracked margin is:

margin_used = sum(fill_notional_usdt) / leverage

After every new fill, recompute:

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:

{
  "instId": "ETH-USDT-SWAP",
  "tdMode": "isolated",
  "side": "sell",
  "posSide": "long",
  "ordType": "market",
  "sz": "<tracked_filled_contracts>",
  "reduceOnly": "true",
  "clOrdId": "ethrtwap15m-exit-<position_id>"
}

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:

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:

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.