Procházet zdrojové kódy

Fix OKX live order execution

lxy před 3 týdny
rodič
revize
e38e81aabc

+ 22 - 0
deploy/bb-squeeze-executor.service

@@ -0,0 +1,22 @@
+[Unit]
+Description=OKX BB squeeze live executor
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+WorkingDirectory=/opt/okx-codex-trader
+EnvironmentFile=/etc/okx-codex-trader/okx.env
+Environment=PYTHONUNBUFFERED=1
+ExecStart=/opt/okx-codex-trader/.venv/bin/python scripts/run_bb_squeeze_executor.py --submit-live --confirm-live
+Restart=always
+RestartSec=300
+User=okxbot
+Group=okxbot
+NoNewPrivileges=true
+PrivateTmp=true
+ProtectSystem=full
+ReadWritePaths=/opt/okx-codex-trader/var
+
+[Install]
+WantedBy=multi-user.target

+ 12 - 1
deploy/install_eth_nextgen_micro_observer.sh

@@ -3,8 +3,10 @@ set -euo pipefail
 
 APP_DIR=/opt/okx-codex-trader
 OBSERVER_SERVICE=eth-nextgen-micro-observer.service
-EXECUTOR_SERVICE=eth-nextgen-micro-executor.service
+EXECUTOR_SERVICE=bb-squeeze-executor.service
 SHORT_BIAS_SERVICE=short-bias-readonly-observer.service
+CALENDAR_SERVICE=calendar-fusion-observer.service
+FOCUSED_SERVICE=eth-focused-portfolio-observer.service
 
 sudo useradd --system --create-home --shell /usr/sbin/nologin okxbot 2>/dev/null || true
 sudo mkdir -p "$APP_DIR" /etc/okx-codex-trader
@@ -36,13 +38,22 @@ fi
 sudo install -m 0644 "$APP_DIR/deploy/$OBSERVER_SERVICE" "/etc/systemd/system/$OBSERVER_SERVICE"
 sudo install -m 0644 "$APP_DIR/deploy/$EXECUTOR_SERVICE" "/etc/systemd/system/$EXECUTOR_SERVICE"
 sudo install -m 0644 "$APP_DIR/deploy/$SHORT_BIAS_SERVICE" "/etc/systemd/system/$SHORT_BIAS_SERVICE"
+sudo install -m 0644 "$APP_DIR/deploy/$CALENDAR_SERVICE" "/etc/systemd/system/$CALENDAR_SERVICE"
+sudo install -m 0644 "$APP_DIR/deploy/$FOCUSED_SERVICE" "/etc/systemd/system/$FOCUSED_SERVICE"
 sudo systemctl daemon-reload
+sudo systemctl disable --now eth-nextgen-micro-executor.service 2>/dev/null || true
 sudo systemctl enable "$OBSERVER_SERVICE"
 sudo systemctl enable "$EXECUTOR_SERVICE"
 sudo systemctl enable "$SHORT_BIAS_SERVICE"
+sudo systemctl enable "$CALENDAR_SERVICE"
+sudo systemctl enable "$FOCUSED_SERVICE"
 sudo systemctl restart "$OBSERVER_SERVICE"
 sudo systemctl restart "$EXECUTOR_SERVICE"
 sudo systemctl restart "$SHORT_BIAS_SERVICE"
+sudo systemctl restart "$CALENDAR_SERVICE"
+sudo systemctl restart "$FOCUSED_SERVICE"
 sudo systemctl status "$OBSERVER_SERVICE" --no-pager
 sudo systemctl status "$EXECUTOR_SERVICE" --no-pager
 sudo systemctl status "$SHORT_BIAS_SERVICE" --no-pager
+sudo systemctl status "$CALENDAR_SERVICE" --no-pager
+sudo systemctl status "$FOCUSED_SERVICE" --no-pager

+ 11 - 0
docs/live-eth-nextgen-micro-deployment.md

@@ -22,6 +22,14 @@ Environment file: `/etc/okx-codex-trader/okx.env`
 
 Service: `eth-nextgen-micro-observer.service`
 
+Live executor: `bb-squeeze-executor.service`
+
+Additional read-only observers:
+
+- `short-bias-readonly-observer.service`
+- `calendar-fusion-observer.service`
+- `eth-focused-portfolio-observer.service`
+
 ## Deploy
 
 ```bash
@@ -42,6 +50,9 @@ sudo systemctl restart eth-nextgen-micro-observer.service
 systemctl status eth-nextgen-micro-observer.service --no-pager
 journalctl -u eth-nextgen-micro-observer.service -f
 cat /opt/okx-codex-trader/var/eth-nextgen-micro/heartbeat.json
+tail -f /opt/okx-codex-trader/var/bb-squeeze-executor/events.jsonl
+cat /opt/okx-codex-trader/var/calendar-fusion/heartbeat.json
+cat /opt/okx-codex-trader/var/eth-focused-portfolio/heartbeat.json
 tail -f /opt/okx-codex-trader/var/eth-nextgen-micro/observer-events.jsonl
 ```
 

+ 10 - 1
okx_codex_trader/live_execution.py

@@ -3,6 +3,7 @@ from __future__ import annotations
 import json
 from dataclasses import asdict, dataclass
 from pathlib import Path
+import re
 from typing import Literal
 
 from okx_codex_trader.models import InstrumentMeta, Position
@@ -50,6 +51,7 @@ class RenderedOrder:
 
 
 EMPTY_STATE = RuntimeState(last_candle_ts=None, nextgen_active_legs=(), micro_side=None)
+CLIENT_ORDER_ID_MAX_LENGTH = 32
 
 
 def load_runtime_state(path: Path) -> RuntimeState:
@@ -216,7 +218,7 @@ def render_market_order_bodies(
                     side=side,
                     pos_side=action.side,
                     size=size,
-                    client_order_id=f"{client_order_id_prefix}-{index}-{action.action}",
+                    client_order_id=market_client_order_id(client_order_id_prefix, index, action.action),
                     reduce_only=action.reduce_only,
                 ),
             )
@@ -225,6 +227,13 @@ def render_market_order_bodies(
     return tuple(rendered)
 
 
+def market_client_order_id(prefix: str, index: int, action: str) -> str:
+    compact = re.sub(r"[^A-Za-z0-9]", "", f"{prefix}{index}{action}")
+    if not compact:
+        raise ValueError("client order id prefix is invalid")
+    return compact[:CLIENT_ORDER_ID_MAX_LENGTH]
+
+
 def _okx_side(action: PlannedAction) -> str:
     if action.reduce_only:
         return "sell" if action.side == "long" else "buy"

+ 17 - 3
okx_codex_trader/okx_client.py

@@ -66,6 +66,20 @@ def _format_number(value: float) -> str:
     return format(Decimal(str(value)).normalize(), "f")
 
 
+def _okx_error_message(payload: dict[str, object]) -> str:
+    parts = [str(payload.get("msg") or payload.get("code") or "okx api error")]
+    data = payload.get("data")
+    if isinstance(data, list):
+        for item in data:
+            if not isinstance(item, dict):
+                continue
+            code = item.get("sCode")
+            msg = item.get("sMsg")
+            if code or msg:
+                parts.append(f"{code}: {msg}")
+    return "; ".join(parts)
+
+
 class OkxClient:
     base_url = "https://www.okx.com"
     request_timeout = 10.0
@@ -244,7 +258,7 @@ class OkxClient:
         if getattr(response, "status_code", 200) >= 400:
             raise ValueError(str(payload.get("msg") or "okx http error"))
         if payload.get("code") != "0":
-            raise ValueError(str(payload.get("msg") or payload.get("code") or "okx api error"))
+            raise ValueError(_okx_error_message(payload))
         data = payload.get("data")
         if not isinstance(data, list):
             raise self._invalid_payload()
@@ -256,7 +270,7 @@ class OkxClient:
         candles_by_ts: dict[int, Candle] = {}
 
         while remaining > 0:
-            page_limit = min(remaining, 300)
+            page_limit = min(remaining, 100)
             params: dict[str, object] = {"instId": symbol, "bar": bar, "limit": page_limit}
             if after is not None:
                 params["after"] = after
@@ -286,7 +300,7 @@ class OkxClient:
             remaining = limit - len(candles_by_ts)
             oldest_ts = min(candle.ts for candle in page)
             after = oldest_ts - 1
-            if len(page) < page_limit:
+            if len(data) < page_limit:
                 break
 
         return sorted(candles_by_ts.values(), key=lambda candle: candle.ts)[:limit]

+ 60 - 29
reports/eth-exploration/eth-focused-portfolio-signal-intent.json

@@ -1,24 +1,54 @@
 {
-  "created_at": "2026-04-29T18:25:25Z",
+  "candles": {
+    "BTC-USDT-SWAP": {
+      "15m": {
+        "first_ts": 1576476000000,
+        "last_time": "2026-05-10T14:45:00Z",
+        "last_ts": 1778424300000,
+        "rows": 224388
+      },
+      "5m": {
+        "first_ts": 1576476300000,
+        "last_time": "2026-05-10T15:00:00Z",
+        "last_ts": 1778425200000,
+        "rows": 673164
+      }
+    },
+    "ETH-USDT-SWAP": {
+      "15m": {
+        "first_ts": 1577232000000,
+        "last_time": "2026-05-10T14:45:00Z",
+        "last_ts": 1778424300000,
+        "rows": 223548
+      },
+      "5m": {
+        "first_ts": 1577232000000,
+        "last_time": "2026-05-10T15:00:00Z",
+        "last_ts": 1778425200000,
+        "rows": 670645
+      }
+    }
+  },
+  "created_at": "2026-05-10T15:05:29Z",
   "legs": [
     {
       "bar": "15m",
-      "decision_candle_time": "2026-04-29T16:30:00Z",
-      "decision_candle_ts": 1777480200000,
+      "decision_candle_time": "2026-05-10T14:30:00Z",
+      "decision_candle_ts": 1778423400000,
       "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
+        "btc_close": 80858.9,
+        "btc_momentum": 0.017205678112797385,
+        "btc_sma": 80691.39562499993,
+        "eth_close": 2329.56,
+        "eth_rsi2": 77.91013708283481,
+        "eth_sma": 2326.5468
       },
-      "latest_local_candle_time": "2026-04-29T16:45:00Z",
-      "latest_local_candle_ts": 1777481100000,
+      "latest_local_candle_time": "2026-05-10T14:45:00Z",
+      "latest_local_candle_ts": 1778424300000,
       "leg_id": "eth_btc_rsi_filter_15m",
       "needs_cancel": false,
       "needs_order": false,
@@ -43,21 +73,21 @@
     },
     {
       "bar": "15m",
-      "decision_candle_time": "2026-04-29T16:30:00Z",
-      "decision_candle_ts": 1777480200000,
+      "decision_candle_time": "2026-05-10T14:30:00Z",
+      "decision_candle_ts": 1778423400000,
       "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
+        "btc_close": 80858.9,
+        "btc_return": -0.000322680348643245,
+        "eth_close": 2329.56,
+        "eth_return": 0.002759184727632702,
+        "return_gap": -0.003081865076275947
       },
-      "latest_local_candle_time": "2026-04-29T16:45:00Z",
-      "latest_local_candle_ts": 1777481100000,
+      "latest_local_candle_time": "2026-05-10T14:45:00Z",
+      "latest_local_candle_ts": 1778424300000,
       "leg_id": "btc_lead_eth_lag_15m",
       "needs_cancel": false,
       "needs_order": false,
@@ -82,21 +112,21 @@
     },
     {
       "bar": "5m",
-      "decision_candle_time": "2026-04-29T01:55:00Z",
-      "decision_candle_ts": 1777427700000,
+      "decision_candle_time": "2026-05-10T14:55:00Z",
+      "decision_candle_ts": 1778424900000,
       "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
+        "btc_close": 80938.8,
+        "btc_return": 0.00027682892466796005,
+        "eth_close": 2329.99,
+        "eth_return": 0.000854810996563371,
+        "return_gap": -0.000577982071895411
       },
-      "latest_local_candle_time": "2026-04-29T02:00:00Z",
-      "latest_local_candle_ts": 1777428000000,
+      "latest_local_candle_time": "2026-05-10T15:00:00Z",
+      "latest_local_candle_ts": 1778425200000,
       "leg_id": "btc_lead_eth_lag_5m",
       "needs_cancel": false,
       "needs_order": false,
@@ -122,6 +152,7 @@
   ],
   "mode": "dry_run_readonly_portfolio_signal_intent",
   "order_client": null,
+  "orders_submitted": 0,
   "portfolio": {
     "active_signal_count": 0,
     "active_suggested_weight": 0,

+ 64 - 33
reports/eth-exploration/eth-focused-portfolio-signal-intent.md

@@ -4,7 +4,7 @@ Dry-run only. No order or cancel request was submitted.
 
 ## Portfolio
 
-- Created at: `2026-04-29T18:25:25Z`
+- Created at: `2026-05-10T15:05:29Z`
 - Direction: `long_only`
 - Active signal count: `0`
 - Active suggested weight: `0.00000000`
@@ -15,34 +15,64 @@ Dry-run only. No order or cancel request was submitted.
 
 | Leg | Bar | Signal | Weight | Action | Decision candle |
 | --- | --- | --- | --- | --- | --- |
-| `eth_btc_rsi_filter_15m` | `15m` | `False` | `0.80314757` | `hold` | `2026-04-29T16:30:00Z` |
-| `btc_lead_eth_lag_15m` | `15m` | `False` | `0.09459139` | `hold` | `2026-04-29T16:30:00Z` |
-| `btc_lead_eth_lag_5m` | `5m` | `False` | `0.10226104` | `hold` | `2026-04-29T01:55:00Z` |
+| `eth_btc_rsi_filter_15m` | `15m` | `False` | `0.80314757` | `hold` | `2026-05-10T14:30:00Z` |
+| `btc_lead_eth_lag_15m` | `15m` | `False` | `0.09459139` | `hold` | `2026-05-10T14:30:00Z` |
+| `btc_lead_eth_lag_5m` | `5m` | `False` | `0.10226104` | `hold` | `2026-05-10T14:55:00Z` |
 
 ## Intent JSON
 
 ```json
 {
-  "created_at": "2026-04-29T18:25:25Z",
+  "candles": {
+    "BTC-USDT-SWAP": {
+      "15m": {
+        "first_ts": 1576476000000,
+        "last_time": "2026-05-10T14:45:00Z",
+        "last_ts": 1778424300000,
+        "rows": 224388
+      },
+      "5m": {
+        "first_ts": 1576476300000,
+        "last_time": "2026-05-10T15:00:00Z",
+        "last_ts": 1778425200000,
+        "rows": 673164
+      }
+    },
+    "ETH-USDT-SWAP": {
+      "15m": {
+        "first_ts": 1577232000000,
+        "last_time": "2026-05-10T14:45:00Z",
+        "last_ts": 1778424300000,
+        "rows": 223548
+      },
+      "5m": {
+        "first_ts": 1577232000000,
+        "last_time": "2026-05-10T15:00:00Z",
+        "last_ts": 1778425200000,
+        "rows": 670645
+      }
+    }
+  },
+  "created_at": "2026-05-10T15:05:29Z",
   "legs": [
     {
       "bar": "15m",
-      "decision_candle_time": "2026-04-29T16:30:00Z",
-      "decision_candle_ts": 1777480200000,
+      "decision_candle_time": "2026-05-10T14:30:00Z",
+      "decision_candle_ts": 1778423400000,
       "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
+        "btc_close": 80858.9,
+        "btc_momentum": 0.017205678112797385,
+        "btc_sma": 80691.39562499993,
+        "eth_close": 2329.56,
+        "eth_rsi2": 77.91013708283481,
+        "eth_sma": 2326.5468
       },
-      "latest_local_candle_time": "2026-04-29T16:45:00Z",
-      "latest_local_candle_ts": 1777481100000,
+      "latest_local_candle_time": "2026-05-10T14:45:00Z",
+      "latest_local_candle_ts": 1778424300000,
       "leg_id": "eth_btc_rsi_filter_15m",
       "needs_cancel": false,
       "needs_order": false,
@@ -67,21 +97,21 @@ Dry-run only. No order or cancel request was submitted.
     },
     {
       "bar": "15m",
-      "decision_candle_time": "2026-04-29T16:30:00Z",
-      "decision_candle_ts": 1777480200000,
+      "decision_candle_time": "2026-05-10T14:30:00Z",
+      "decision_candle_ts": 1778423400000,
       "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
+        "btc_close": 80858.9,
+        "btc_return": -0.000322680348643245,
+        "eth_close": 2329.56,
+        "eth_return": 0.002759184727632702,
+        "return_gap": -0.003081865076275947
       },
-      "latest_local_candle_time": "2026-04-29T16:45:00Z",
-      "latest_local_candle_ts": 1777481100000,
+      "latest_local_candle_time": "2026-05-10T14:45:00Z",
+      "latest_local_candle_ts": 1778424300000,
       "leg_id": "btc_lead_eth_lag_15m",
       "needs_cancel": false,
       "needs_order": false,
@@ -106,21 +136,21 @@ Dry-run only. No order or cancel request was submitted.
     },
     {
       "bar": "5m",
-      "decision_candle_time": "2026-04-29T01:55:00Z",
-      "decision_candle_ts": 1777427700000,
+      "decision_candle_time": "2026-05-10T14:55:00Z",
+      "decision_candle_ts": 1778424900000,
       "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
+        "btc_close": 80938.8,
+        "btc_return": 0.00027682892466796005,
+        "eth_close": 2329.99,
+        "eth_return": 0.000854810996563371,
+        "return_gap": -0.000577982071895411
       },
-      "latest_local_candle_time": "2026-04-29T02:00:00Z",
-      "latest_local_candle_ts": 1777428000000,
+      "latest_local_candle_time": "2026-05-10T15:00:00Z",
+      "latest_local_candle_ts": 1778425200000,
       "leg_id": "btc_lead_eth_lag_5m",
       "needs_cancel": false,
       "needs_order": false,
@@ -146,6 +176,7 @@ Dry-run only. No order or cancel request was submitted.
   ],
   "mode": "dry_run_readonly_portfolio_signal_intent",
   "order_client": null,
+  "orders_submitted": 0,
   "portfolio": {
     "active_signal_count": 0,
     "active_suggested_weight": 0,

+ 68 - 50
reports/eth-exploration/eth-nextgen-micro-signal-intent.json

@@ -1,5 +1,5 @@
 {
-  "created_at": "2026-04-30T19:30:17Z",
+  "created_at": "2026-05-08T16:12:57Z",
   "decision": {
     "active_engine": "nextgen",
     "intent": "observe_nextgen_no_signal",
@@ -7,21 +7,28 @@
     "needs_order": false,
     "selected_signal": "no_signal"
   },
+  "execution_intent": {
+    "blocker": "persistent strategy position state is required before entry signals can be reconciled to target position",
+    "entry_signal": "no_signal",
+    "entry_unit": 0.0,
+    "target_position": null,
+    "target_position_known": false
+  },
   "micro": {
     "bar": "15m",
     "candidate": "atr-compress-expand-r96-q0.15-sl0.008-tp0.016-mf0.25-us",
-    "decision_candle_time": "2026-04-30T18:45:00Z",
-    "decision_candle_ts": 1777574700000,
+    "decision_candle_time": "2026-05-08T15:30:00Z",
+    "decision_candle_ts": 1778254200000,
     "engine": "micro",
     "indicators": {
-      "atr_limit_previous": 0.0017170477351923388,
-      "atr_previous": 0.003304835488372205,
-      "eth_close": 2258.51,
-      "range_high": 2278.51,
-      "range_low": 2221.42
+      "atr_limit_previous": 0.0028624155322640385,
+      "atr_previous": 0.002906729910550877,
+      "eth_close": 2284.19,
+      "range_high": 2304.08,
+      "range_low": 2262.45
     },
-    "latest_local_candle_time": "2026-04-30T19:00:00Z",
-    "latest_local_candle_ts": 1777575600000,
+    "latest_local_candle_time": "2026-05-08T15:45:00Z",
+    "latest_local_candle_ts": 1778255100000,
     "params": {
       "atr_quantile": 0.15,
       "atr_quantile_window": 480,
@@ -41,6 +48,17 @@
   },
   "mode": "readonly_signal_intent",
   "nextgen": {
+    "data": {
+      "aligned_candles": 223360,
+      "btc_candles": "data/okx-candles/BTC-USDT-SWAP/15m.csv",
+      "decision_candle_time": "2026-05-08T15:30:00Z",
+      "decision_candle_ts": 1778254200000,
+      "decision_rule": "use the aligned candle immediately before the latest aligned local candle",
+      "eth_candles": "data/okx-candles/ETH-USDT-SWAP/15m.csv",
+      "latest_aligned_candle_time": "2026-05-08T15:45:00Z",
+      "latest_aligned_candle_ts": 1778255100000,
+      "source": "local_csv"
+    },
     "decision": {
       "active_signal_count": 0,
       "active_suggested_weight": 0,
@@ -54,28 +72,28 @@
         "bar": "15m",
         "conditions": {
           "btc_close_above_sma480": {
-            "distance_to_pass": 687.4174999999523,
+            "distance_to_pass": 518.1931249999761,
             "passes": false,
-            "threshold": 76987.41749999995,
-            "value": 76300.0
+            "threshold": 80448.09312499997,
+            "value": 79929.9
           },
           "btc_momentum_at_or_above_min": {
-            "distance_to_pass": 0.0046804802579742955,
+            "distance_to_pass": 0.019325194773326837,
             "passes": false,
             "threshold": 0.0,
-            "value": -0.0046804802579742955
+            "value": -0.019325194773326837
           },
           "eth_close_above_sma50": {
-            "distance_to_pass": 0.6641999999997097,
-            "passes": false,
-            "threshold": 2259.1742,
-            "value": 2258.51
+            "distance_to_pass": 0.0,
+            "passes": true,
+            "threshold": 2281.480599999999,
+            "value": 2284.19
           },
           "eth_rsi2_at_or_below_3": {
-            "distance_to_pass": 58.36192729056775,
+            "distance_to_pass": 68.44285863784671,
             "passes": false,
             "threshold": 3.0,
-            "value": 61.36192729056775
+            "value": 71.44285863784671
           }
         },
         "direction": "long",
@@ -85,12 +103,12 @@
         "exit_signal": true,
         "family": "btc_trend_eth_rsi",
         "indicators": {
-          "btc_close": 76300.0,
-          "btc_momentum_240": -0.0046804802579742955,
-          "btc_sma480": 76987.41749999995,
-          "eth_close": 2258.51,
-          "eth_rsi2": 61.36192729056775,
-          "eth_sma50": 2259.1742
+          "btc_close": 79929.9,
+          "btc_momentum_240": -0.019325194773326837,
+          "btc_sma480": 80448.09312499997,
+          "eth_close": 2284.19,
+          "eth_rsi2": 71.44285863784671,
+          "eth_sma50": 2281.480599999999
         },
         "intent": "no_signal",
         "leg_id": "btc_trend_eth_rsi",
@@ -110,40 +128,40 @@
         "bar": "15m",
         "conditions": {
           "btc_close_above_sma480": {
-            "distance_to_pass": 687.4174999999523,
+            "distance_to_pass": 518.1931249999761,
             "passes": false,
-            "threshold": 76987.41749999995,
-            "value": 76300.0
+            "threshold": 80448.09312499997,
+            "value": 79929.9
           },
           "btc_drawdown_at_or_above_floor": {
             "distance_to_pass": 0.0,
             "passes": true,
             "threshold": -0.05,
-            "value": -0.00369143422731244
+            "value": -0.004351064783904235
           },
           "btc_momentum_at_or_above_min": {
-            "distance_to_pass": 0.014680480257974296,
+            "distance_to_pass": 0.02932519477332684,
             "passes": false,
             "threshold": 0.01,
-            "value": -0.0046804802579742955
+            "value": -0.019325194773326837
           },
           "btc_realized_vol_at_or_below_max": {
             "distance_to_pass": 0.0,
             "passes": true,
             "threshold": 0.01,
-            "value": 0.0016330193566096354
+            "value": 0.0014718646466960197
           },
           "eth_close_above_sma50": {
-            "distance_to_pass": 0.6641999999997097,
-            "passes": false,
-            "threshold": 2259.1742,
-            "value": 2258.51
+            "distance_to_pass": 0.0,
+            "passes": true,
+            "threshold": 2281.480599999999,
+            "value": 2284.19
           },
           "eth_rsi2_at_or_below_3": {
-            "distance_to_pass": 58.36192729056775,
+            "distance_to_pass": 68.44285863784671,
             "passes": false,
             "threshold": 3.0,
-            "value": 61.36192729056775
+            "value": 71.44285863784671
           }
         },
         "direction": "long",
@@ -153,15 +171,15 @@
         "exit_signal": true,
         "family": "btc_shock_guard_eth_rsi",
         "indicators": {
-          "btc_close": 76300.0,
-          "btc_drawdown_96": -0.00369143422731244,
-          "btc_momentum_240": -0.0046804802579742955,
-          "btc_realized_vol_96": 0.0016330193566096354,
-          "btc_recent_high_96": 76582.7,
-          "btc_sma480": 76987.41749999995,
-          "eth_close": 2258.51,
-          "eth_rsi2": 61.36192729056775,
-          "eth_sma50": 2259.1742
+          "btc_close": 79929.9,
+          "btc_drawdown_96": -0.004351064783904235,
+          "btc_momentum_240": -0.019325194773326837,
+          "btc_realized_vol_96": 0.0014718646466960197,
+          "btc_recent_high_96": 80279.2,
+          "btc_sma480": 80448.09312499997,
+          "eth_close": 2284.19,
+          "eth_rsi2": 71.44285863784671,
+          "eth_sma50": 2281.480599999999
         },
         "intent": "no_signal",
         "leg_id": "btc_shock_guard_eth_rsi",
@@ -186,7 +204,7 @@
   "private_key_required": false,
   "risk_limits": {
     "blocked_for_live_trading": true,
-    "blocker": "persistent virtual position state is not maintained by this read-only script",
+    "blocker": "persistent virtual position state is not maintained by this read-only signal builder",
     "execution": "intent_only",
     "no_cancel_submission": true,
     "no_order_submission": true,

+ 72 - 52
reports/eth-exploration/eth-nextgen-micro-signal-intent.md

@@ -4,19 +4,21 @@ Read-only signal intent. No order or cancel request was submitted.
 
 ## Decision
 
-- Created at: `2026-04-30T19:30:17Z`
+- Created at: `2026-05-08T16:12:57Z`
 - Strategy: `switch-l30-r96_q0.15_mf0.25_us`
 - Active engine: `nextgen`
 - Selected signal: `no_signal`
+- Entry unit: `0.0`
+- Target position known: `False`
 - Needs order: `False`
 - Blocked for live trading: `True`
-- Blocker: `persistent virtual position state is not maintained by this read-only script`
+- Blocker: `persistent virtual position state is not maintained by this read-only signal builder`
 
 ## Intent JSON
 
 ```json
 {
-  "created_at": "2026-04-30T19:30:17Z",
+  "created_at": "2026-05-08T16:12:57Z",
   "decision": {
     "active_engine": "nextgen",
     "intent": "observe_nextgen_no_signal",
@@ -24,21 +26,28 @@ Read-only signal intent. No order or cancel request was submitted.
     "needs_order": false,
     "selected_signal": "no_signal"
   },
+  "execution_intent": {
+    "blocker": "persistent strategy position state is required before entry signals can be reconciled to target position",
+    "entry_signal": "no_signal",
+    "entry_unit": 0.0,
+    "target_position": null,
+    "target_position_known": false
+  },
   "micro": {
     "bar": "15m",
     "candidate": "atr-compress-expand-r96-q0.15-sl0.008-tp0.016-mf0.25-us",
-    "decision_candle_time": "2026-04-30T18:45:00Z",
-    "decision_candle_ts": 1777574700000,
+    "decision_candle_time": "2026-05-08T15:30:00Z",
+    "decision_candle_ts": 1778254200000,
     "engine": "micro",
     "indicators": {
-      "atr_limit_previous": 0.0017170477351923388,
-      "atr_previous": 0.003304835488372205,
-      "eth_close": 2258.51,
-      "range_high": 2278.51,
-      "range_low": 2221.42
+      "atr_limit_previous": 0.0028624155322640385,
+      "atr_previous": 0.002906729910550877,
+      "eth_close": 2284.19,
+      "range_high": 2304.08,
+      "range_low": 2262.45
     },
-    "latest_local_candle_time": "2026-04-30T19:00:00Z",
-    "latest_local_candle_ts": 1777575600000,
+    "latest_local_candle_time": "2026-05-08T15:45:00Z",
+    "latest_local_candle_ts": 1778255100000,
     "params": {
       "atr_quantile": 0.15,
       "atr_quantile_window": 480,
@@ -58,6 +67,17 @@ Read-only signal intent. No order or cancel request was submitted.
   },
   "mode": "readonly_signal_intent",
   "nextgen": {
+    "data": {
+      "aligned_candles": 223360,
+      "btc_candles": "data/okx-candles/BTC-USDT-SWAP/15m.csv",
+      "decision_candle_time": "2026-05-08T15:30:00Z",
+      "decision_candle_ts": 1778254200000,
+      "decision_rule": "use the aligned candle immediately before the latest aligned local candle",
+      "eth_candles": "data/okx-candles/ETH-USDT-SWAP/15m.csv",
+      "latest_aligned_candle_time": "2026-05-08T15:45:00Z",
+      "latest_aligned_candle_ts": 1778255100000,
+      "source": "local_csv"
+    },
     "decision": {
       "active_signal_count": 0,
       "active_suggested_weight": 0,
@@ -71,28 +91,28 @@ Read-only signal intent. No order or cancel request was submitted.
         "bar": "15m",
         "conditions": {
           "btc_close_above_sma480": {
-            "distance_to_pass": 687.4174999999523,
+            "distance_to_pass": 518.1931249999761,
             "passes": false,
-            "threshold": 76987.41749999995,
-            "value": 76300.0
+            "threshold": 80448.09312499997,
+            "value": 79929.9
           },
           "btc_momentum_at_or_above_min": {
-            "distance_to_pass": 0.0046804802579742955,
+            "distance_to_pass": 0.019325194773326837,
             "passes": false,
             "threshold": 0.0,
-            "value": -0.0046804802579742955
+            "value": -0.019325194773326837
           },
           "eth_close_above_sma50": {
-            "distance_to_pass": 0.6641999999997097,
-            "passes": false,
-            "threshold": 2259.1742,
-            "value": 2258.51
+            "distance_to_pass": 0.0,
+            "passes": true,
+            "threshold": 2281.480599999999,
+            "value": 2284.19
           },
           "eth_rsi2_at_or_below_3": {
-            "distance_to_pass": 58.36192729056775,
+            "distance_to_pass": 68.44285863784671,
             "passes": false,
             "threshold": 3.0,
-            "value": 61.36192729056775
+            "value": 71.44285863784671
           }
         },
         "direction": "long",
@@ -102,12 +122,12 @@ Read-only signal intent. No order or cancel request was submitted.
         "exit_signal": true,
         "family": "btc_trend_eth_rsi",
         "indicators": {
-          "btc_close": 76300.0,
-          "btc_momentum_240": -0.0046804802579742955,
-          "btc_sma480": 76987.41749999995,
-          "eth_close": 2258.51,
-          "eth_rsi2": 61.36192729056775,
-          "eth_sma50": 2259.1742
+          "btc_close": 79929.9,
+          "btc_momentum_240": -0.019325194773326837,
+          "btc_sma480": 80448.09312499997,
+          "eth_close": 2284.19,
+          "eth_rsi2": 71.44285863784671,
+          "eth_sma50": 2281.480599999999
         },
         "intent": "no_signal",
         "leg_id": "btc_trend_eth_rsi",
@@ -127,40 +147,40 @@ Read-only signal intent. No order or cancel request was submitted.
         "bar": "15m",
         "conditions": {
           "btc_close_above_sma480": {
-            "distance_to_pass": 687.4174999999523,
+            "distance_to_pass": 518.1931249999761,
             "passes": false,
-            "threshold": 76987.41749999995,
-            "value": 76300.0
+            "threshold": 80448.09312499997,
+            "value": 79929.9
           },
           "btc_drawdown_at_or_above_floor": {
             "distance_to_pass": 0.0,
             "passes": true,
             "threshold": -0.05,
-            "value": -0.00369143422731244
+            "value": -0.004351064783904235
           },
           "btc_momentum_at_or_above_min": {
-            "distance_to_pass": 0.014680480257974296,
+            "distance_to_pass": 0.02932519477332684,
             "passes": false,
             "threshold": 0.01,
-            "value": -0.0046804802579742955
+            "value": -0.019325194773326837
           },
           "btc_realized_vol_at_or_below_max": {
             "distance_to_pass": 0.0,
             "passes": true,
             "threshold": 0.01,
-            "value": 0.0016330193566096354
+            "value": 0.0014718646466960197
           },
           "eth_close_above_sma50": {
-            "distance_to_pass": 0.6641999999997097,
-            "passes": false,
-            "threshold": 2259.1742,
-            "value": 2258.51
+            "distance_to_pass": 0.0,
+            "passes": true,
+            "threshold": 2281.480599999999,
+            "value": 2284.19
           },
           "eth_rsi2_at_or_below_3": {
-            "distance_to_pass": 58.36192729056775,
+            "distance_to_pass": 68.44285863784671,
             "passes": false,
             "threshold": 3.0,
-            "value": 61.36192729056775
+            "value": 71.44285863784671
           }
         },
         "direction": "long",
@@ -170,15 +190,15 @@ Read-only signal intent. No order or cancel request was submitted.
         "exit_signal": true,
         "family": "btc_shock_guard_eth_rsi",
         "indicators": {
-          "btc_close": 76300.0,
-          "btc_drawdown_96": -0.00369143422731244,
-          "btc_momentum_240": -0.0046804802579742955,
-          "btc_realized_vol_96": 0.0016330193566096354,
-          "btc_recent_high_96": 76582.7,
-          "btc_sma480": 76987.41749999995,
-          "eth_close": 2258.51,
-          "eth_rsi2": 61.36192729056775,
-          "eth_sma50": 2259.1742
+          "btc_close": 79929.9,
+          "btc_drawdown_96": -0.004351064783904235,
+          "btc_momentum_240": -0.019325194773326837,
+          "btc_realized_vol_96": 0.0014718646466960197,
+          "btc_recent_high_96": 80279.2,
+          "btc_sma480": 80448.09312499997,
+          "eth_close": 2284.19,
+          "eth_rsi2": 71.44285863784671,
+          "eth_sma50": 2281.480599999999
         },
         "intent": "no_signal",
         "leg_id": "btc_shock_guard_eth_rsi",
@@ -203,7 +223,7 @@ Read-only signal intent. No order or cancel request was submitted.
   "private_key_required": false,
   "risk_limits": {
     "blocked_for_live_trading": true,
-    "blocker": "persistent virtual position state is not maintained by this read-only script",
+    "blocker": "persistent virtual position state is not maintained by this read-only signal builder",
     "execution": "intent_only",
     "no_cancel_submission": true,
     "no_order_submission": true,

+ 311 - 0
scripts/run_bb_squeeze_executor.py

@@ -0,0 +1,311 @@
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import sys
+from dataclasses import asdict, dataclass
+from datetime import UTC, datetime
+from pathlib import Path
+
+import pandas as pd
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+from okx_codex_trader.config import load_config
+from okx_codex_trader.live_execution import (
+    TargetPosition,
+    current_position_from_okx,
+    plan_position_delta,
+    render_market_order_bodies,
+)
+from okx_codex_trader.okx_client import OkxClient
+
+
+ROOT = Path(__file__).resolve().parents[1]
+STATE_DIR = ROOT / "var" / "bb-squeeze-executor"
+STATE_FILE = "runtime-state.json"
+EVENTS_FILE = "events.jsonl"
+SYMBOL = "ETH-USDT-SWAP"
+BAR = "15m"
+LEVERAGE = 3
+CANDLES_PATH = ROOT / "data" / "okx-candles" / SYMBOL / f"{BAR}.csv"
+
+BAND_LENGTH = 48
+BANDWIDTH_LOOKBACK = 960
+BANDWIDTH_QUANTILE = 0.25
+STOP_LOSS_PCT = 0.01
+ETH_VOL_CAP = 0.006
+COOLDOWN_BARS = 24
+
+
+@dataclass(frozen=True)
+class StrategyState:
+    last_candle_ts: int | None
+    active_side: str | None
+    entry_price: float | None
+    entry_candle_ts: int | None
+    cooldown_until_ts: int | None
+
+
+EMPTY_STATE = StrategyState(None, None, None, None, None)
+
+
+def now_iso() -> str:
+    return datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z")
+
+
+def load_state(path: Path) -> StrategyState:
+    if not path.exists():
+        return EMPTY_STATE
+    payload = json.loads(path.read_text(encoding="utf-8"))
+    return StrategyState(
+        last_candle_ts=payload["last_candle_ts"],
+        active_side=payload["active_side"],
+        entry_price=payload["entry_price"],
+        entry_candle_ts=payload["entry_candle_ts"],
+        cooldown_until_ts=payload["cooldown_until_ts"],
+    )
+
+
+def save_state(path: Path, state: StrategyState) -> None:
+    path.parent.mkdir(parents=True, exist_ok=True)
+    path.write_text(json.dumps(asdict(state), indent=2, sort_keys=True) + "\n", encoding="utf-8")
+
+
+def append_event(state_dir: Path, payload: dict[str, object]) -> None:
+    state_dir.mkdir(parents=True, exist_ok=True)
+    with (state_dir / EVENTS_FILE).open("a", encoding="utf-8") as handle:
+        handle.write(json.dumps(payload, sort_keys=True) + "\n")
+
+
+def load_frame() -> pd.DataFrame:
+    frame = pd.read_csv(CANDLES_PATH)
+    frame["time"] = pd.to_datetime(frame["ts"], unit="ms", utc=True)
+    return frame.sort_values("ts").drop_duplicates("ts", keep="last").reset_index(drop=True)
+
+
+def signal_from_frame(frame: pd.DataFrame, state: StrategyState) -> tuple[StrategyState, dict[str, object]]:
+    if len(frame) < BANDWIDTH_LOOKBACK + 3:
+        raise ValueError("not enough candles")
+    close = frame["close"].astype(float)
+    middle = close.rolling(BAND_LENGTH).mean()
+    stdev = close.rolling(BAND_LENGTH).std(ddof=0)
+    upper = middle + (2.0 * stdev)
+    lower = middle - (2.0 * stdev)
+    bandwidth = (upper - lower) / middle
+    threshold = bandwidth.rolling(BANDWIDTH_LOOKBACK).quantile(BANDWIDTH_QUANTILE)
+    eth_vol = close.pct_change().rolling(96).std(ddof=0)
+    decision_index = len(frame) - 2
+    row = frame.iloc[decision_index]
+    candle_ts = int(row["ts"])
+    candle_time = pd.Timestamp(row["time"]).isoformat().replace("+00:00", "Z")
+
+    indicators = {
+        "eth_close": float(row["close"]),
+        "middle": float(middle.iloc[decision_index]),
+        "upper": float(upper.iloc[decision_index]),
+        "lower": float(lower.iloc[decision_index]),
+        "bandwidth": float(bandwidth.iloc[decision_index]),
+        "bandwidth_threshold": float(threshold.iloc[decision_index]),
+        "eth_vol_96": float(eth_vol.iloc[decision_index]),
+    }
+
+    if state.last_candle_ts is not None and candle_ts <= state.last_candle_ts:
+        return state, {
+            "decision_candle_ts": candle_ts,
+            "decision_candle_time": candle_time,
+            "signal": "state_replay",
+            "target_side": state.active_side or "flat",
+            "indicators": indicators,
+        }
+
+    next_state = StrategyState(candle_ts, state.active_side, state.entry_price, state.entry_candle_ts, state.cooldown_until_ts)
+    signal = "hold"
+    target_side = state.active_side or "flat"
+
+    if state.active_side is not None:
+        entry_price = float(state.entry_price)
+        stop = entry_price * (1.0 - STOP_LOSS_PCT if state.active_side == "long" else 1.0 + STOP_LOSS_PCT)
+        stop_hit = (state.active_side == "long" and float(row["low"]) <= stop) or (state.active_side == "short" and float(row["high"]) >= stop)
+        middle_exit = (state.active_side == "long" and float(row["close"]) < indicators["middle"]) or (
+            state.active_side == "short" and float(row["close"]) > indicators["middle"]
+        )
+        if stop_hit or middle_exit:
+            signal = "exit_stop" if stop_hit else "exit_middle"
+            target_side = "flat"
+            next_state = StrategyState(
+                candle_ts,
+                None,
+                None,
+                None,
+                candle_ts + (COOLDOWN_BARS * 900_000),
+            )
+    else:
+        cooldown_ok = state.cooldown_until_ts is None or candle_ts >= state.cooldown_until_ts
+        compressed = indicators["bandwidth"] <= indicators["bandwidth_threshold"]
+        vol_ok = indicators["eth_vol_96"] <= ETH_VOL_CAP
+        if cooldown_ok and compressed and vol_ok and float(row["close"]) > indicators["upper"]:
+            signal = "entry_long"
+            target_side = "long"
+            next_state = StrategyState(candle_ts, "long", float(row["close"]), candle_ts, state.cooldown_until_ts)
+        elif cooldown_ok and compressed and vol_ok and float(row["close"]) < indicators["lower"]:
+            signal = "entry_short"
+            target_side = "short"
+            next_state = StrategyState(candle_ts, "short", float(row["close"]), candle_ts, state.cooldown_until_ts)
+
+    return next_state, {
+        "decision_candle_ts": candle_ts,
+        "decision_candle_time": candle_time,
+        "signal": signal,
+        "target_side": target_side,
+        "indicators": indicators,
+        "params": {
+            "band_length": BAND_LENGTH,
+            "bandwidth_lookback": BANDWIDTH_LOOKBACK,
+            "bandwidth_quantile": BANDWIDTH_QUANTILE,
+            "stop_loss_pct": STOP_LOSS_PCT,
+            "eth_vol_cap": ETH_VOL_CAP,
+            "cooldown_bars": COOLDOWN_BARS,
+            "side_mode": "both",
+        },
+    }
+
+
+def account_current_position(client: OkxClient, margin_per_unit_usdt: float) -> tuple[TargetPosition, dict[str, object]]:
+    positions = client.get_positions(SYMBOL)
+    metadata = client.get_instrument_meta(SYMBOL)
+    mark_price = client.get_last_price(SYMBOL)
+    current = current_position_from_okx(
+        positions=positions,
+        mark_price=mark_price,
+        metadata=metadata,
+        leverage=LEVERAGE,
+        margin_per_unit_usdt=margin_per_unit_usdt,
+    )
+    return current, {
+        "positions": [asdict(position) for position in positions],
+        "instrument_meta": asdict(metadata),
+        "mark_price": mark_price,
+    }
+
+
+def target_position(signal: dict[str, object], current: TargetPosition) -> TargetPosition:
+    side = str(signal["target_side"])
+    if side == "flat":
+        return TargetPosition(side="flat", unit=0.0, known=True, reason=str(signal["signal"]))
+    if current.known and current.side == side and current.unit > 0.0:
+        return TargetPosition(side=side, unit=current.unit, known=True, reason="keep existing same-side position")
+    return TargetPosition(side=side, unit=1.0, known=True, reason=str(signal["signal"]))
+
+
+def risk_arg(value: float | None, env_name: str) -> float:
+    if value is not None:
+        return value
+    raw = os.environ.get(env_name)
+    if raw is None or raw == "":
+        raise ValueError(f"{env_name} is required")
+    return float(raw)
+
+
+def run_once(
+    *,
+    state_dir: Path,
+    margin_per_unit_usdt: float,
+    max_new_margin_usdt: float,
+    max_total_margin_usdt: float,
+    submit_live: bool,
+) -> dict[str, object]:
+    state_dir.mkdir(parents=True, exist_ok=True)
+    state_path = state_dir / STATE_FILE
+    previous_state = load_state(state_path)
+    next_state, signal = signal_from_frame(load_frame(), previous_state)
+    client = OkxClient(load_config())
+    current, account = account_current_position(client, margin_per_unit_usdt)
+    target = target_position(signal, current)
+    plan = plan_position_delta(current, target)
+    orders = ()
+    if current.known and target.known:
+        from okx_codex_trader.models import InstrumentMeta
+
+        meta = account["instrument_meta"]
+        orders = render_market_order_bodies(
+            plan=plan,
+            symbol=SYMBOL,
+            mark_price=float(account["mark_price"]),
+            metadata=InstrumentMeta(ct_val=float(meta["ct_val"]), lot_sz=float(meta["lot_sz"]), min_sz=float(meta["min_sz"])),
+            leverage=LEVERAGE,
+            margin_per_unit_usdt=margin_per_unit_usdt,
+            max_new_margin_usdt=max_new_margin_usdt,
+            max_total_margin_usdt=max_total_margin_usdt,
+            client_order_id_prefix=f"bbsq-{signal['decision_candle_ts']}",
+        )
+    snapshot = {
+        "created_at": now_iso(),
+        "mode": "bb_squeeze_live_executor" if submit_live else "bb_squeeze_dry_run_executor",
+        "orders_submitted": 0,
+        "strategy": "bb-squeeze-l48-bw960-q0.25-sl0.01-tpnone-both-none-vc0.006-ddnone-cd24",
+        "previous_state": asdict(previous_state),
+        "next_state": asdict(next_state),
+        "signal": signal,
+        "current_position": asdict(current),
+        "target_position": asdict(target),
+        "execution_plan": {
+            "current": asdict(plan.current),
+            "target": asdict(plan.target),
+            "actions": [asdict(action) for action in plan.actions],
+        },
+        "rendered_orders": [asdict(order) for order in orders],
+        "account": account,
+        "risk_limits": {
+            "submit_enabled": submit_live,
+            "margin_per_unit_usdt": margin_per_unit_usdt,
+            "max_new_margin_usdt": max_new_margin_usdt,
+            "max_total_margin_usdt": max_total_margin_usdt,
+        },
+    }
+    if submit_live:
+        client.ensure_hedge_mode()
+        submitted = []
+        try:
+            for rendered in orders:
+                body = rendered.body
+                client.set_leverage(symbol=SYMBOL, leverage=LEVERAGE, pos_side=body["posSide"])
+                submitted.append(asdict(client.submit_market_order_body(body)))
+        except ValueError as exc:
+            snapshot["orders_submitted"] = len(submitted)
+            snapshot["submitted_orders"] = submitted
+            snapshot["execution_error"] = str(exc)
+            append_event(state_dir, snapshot)
+            raise
+        snapshot["orders_submitted"] = len(submitted)
+        snapshot["submitted_orders"] = submitted
+    save_state(state_path, next_state)
+    append_event(state_dir, snapshot)
+    return snapshot
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description="Run BB squeeze live executor.")
+    parser.add_argument("--state-dir", type=Path, default=STATE_DIR)
+    parser.add_argument("--margin-per-unit-usdt", type=float)
+    parser.add_argument("--max-new-margin-usdt", type=float)
+    parser.add_argument("--max-total-margin-usdt", type=float)
+    parser.add_argument("--submit-live", action="store_true")
+    parser.add_argument("--confirm-live", action="store_true")
+    args = parser.parse_args()
+    if args.submit_live != args.confirm_live:
+        raise ValueError("--submit-live and --confirm-live must be used together")
+    snapshot = run_once(
+        state_dir=args.state_dir,
+        margin_per_unit_usdt=risk_arg(args.margin_per_unit_usdt, "ETH_NEXTGEN_MARGIN_PER_UNIT_USDT"),
+        max_new_margin_usdt=risk_arg(args.max_new_margin_usdt, "ETH_NEXTGEN_MAX_NEW_MARGIN_USDT"),
+        max_total_margin_usdt=risk_arg(args.max_total_margin_usdt, "ETH_NEXTGEN_MAX_TOTAL_MARGIN_USDT"),
+        submit_live=args.submit_live,
+    )
+    print(json.dumps(snapshot, indent=2, sort_keys=True))
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 2 - 2
tests/test_eth_nextgen_micro_executor.py

@@ -78,7 +78,7 @@ def test_executor_snapshot_renders_order_body_when_positions_are_known(monkeypat
                 "posSide": "long",
                 "ordType": "market",
                 "sz": "5",
-                "clOrdId": "ethnm-1000-1-open",
+                "clOrdId": "ethnm10001open",
             },
         }
     ]
@@ -204,7 +204,7 @@ def test_live_submit_calls_okx_and_writes_state_after_success(monkeypatch, tmp_p
     )
 
     assert snapshot["orders_submitted"] == 1
-    assert client.submitted[0]["clOrdId"] == "ethnm-1000-1-open"
+    assert client.submitted[0]["clOrdId"] == "ethnm10001open"
     assert client.leverage == [("ETH-USDT-SWAP", 3, "long")]
     assert (tmp_path / executor.EXECUTOR_STATE_FILENAME).exists()
     assert (tmp_path / executor.EXECUTOR_EVENTS_FILENAME).exists()

+ 9 - 3
tests/test_live_execution.py

@@ -6,6 +6,7 @@ from okx_codex_trader.live_execution import (
     current_position_from_okx,
     plan_position_delta,
     render_market_order_bodies,
+    market_client_order_id,
     target_from_signal,
 )
 from okx_codex_trader.models import InstrumentMeta, Position
@@ -143,7 +144,7 @@ def test_render_market_order_bodies_builds_open_order_body():
         "posSide": "long",
         "ordType": "market",
         "sz": "5",
-        "clOrdId": "eth-1000-1-open",
+        "clOrdId": "eth10001open",
     }
 
 
@@ -173,7 +174,7 @@ def test_render_market_order_bodies_builds_reduce_only_close_before_reverse():
             "posSide": "long",
             "ordType": "market",
             "sz": "10",
-            "clOrdId": "eth-2000-1-close",
+            "clOrdId": "eth20001close",
             "reduceOnly": "true",
         },
         {
@@ -183,11 +184,16 @@ def test_render_market_order_bodies_builds_reduce_only_close_before_reverse():
             "posSide": "short",
             "ordType": "market",
             "sz": "5",
-            "clOrdId": "eth-2000-2-reverse",
+            "clOrdId": "eth20002reverse",
         },
     ]
 
 
+def test_market_client_order_id_removes_unsupported_characters_and_caps_length():
+    assert market_client_order_id("bbsq-1778508900000", 1, "open") == "bbsq17785089000001open"
+    assert len(market_client_order_id("x" * 40, 1, "open")) == 32
+
+
 def test_render_market_order_bodies_enforces_new_margin_cap():
     plan = plan_position_delta(
         TargetPosition(side="flat", unit=0.0, known=True, reason="current"),

+ 32 - 4
tests/test_okx_client.py

@@ -131,7 +131,7 @@ def older_candles_response() -> DummyResponse:
 
 def full_page_candles_response() -> DummyResponse:
     data = []
-    for offset in range(300):
+    for offset in range(100):
         ts = 1710000001000 - (offset * 1000)
         close = 25050 - offset
         data.append([str(ts), str(close - 50), str(close + 50), str(close - 100), str(close), "100", "1000", "1000", "1"])
@@ -563,9 +563,9 @@ def test_get_candles_paginates_when_limit_exceeds_single_page():
     session = DummySession([full_page_candles_response(), older_candles_response()])
     client = OkxClient(config=sample_config(), session=session)
 
-    candles = client.get_candles(symbol="BTC-USDT-SWAP", bar="1H", limit=302)
+    candles = client.get_candles(symbol="BTC-USDT-SWAP", bar="1H", limit=102)
 
-    assert len(candles) == 302
+    assert len(candles) == 102
     assert candles[0].ts == 1709999700000
     assert candles[1].ts == 1709999701000
     assert len(session.request_paths) == 2
@@ -984,10 +984,38 @@ def test_okx_error_payload_raises_value_error():
     session = DummySession([error_response(code="51000", msg="parameter error")])
     client = OkxClient(config=sample_config(), session=session)
 
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError, match="parameter error"):
         client.get_candles(symbol="BTC-USDT-SWAP", bar="1H", limit=20)
 
 
+def test_okx_error_payload_includes_operation_details():
+    session = DummySession(
+        [
+            DummyResponse(
+                {
+                    "code": "1",
+                    "msg": "All operations failed",
+                    "data": [{"sCode": "51000", "sMsg": "Parameter posSide error"}],
+                }
+            )
+        ]
+    )
+    client = OkxClient(config=sample_config(), session=session)
+
+    with pytest.raises(ValueError, match="All operations failed; 51000: Parameter posSide error"):
+        client.submit_market_order_body(
+            {
+                "instId": "ETH-USDT-SWAP",
+                "tdMode": "isolated",
+                "side": "sell",
+                "posSide": "short",
+                "ordType": "market",
+                "sz": "1.29",
+                "clOrdId": "test",
+            }
+        )
+
+
 def test_get_candles_rejects_non_finite_numeric_fields():
     session = DummySession([candles_with_non_finite_numeric_response()])
     client = OkxClient(config=sample_config(), session=session)