Ver Fonte

feat: add guarded OKX authenticated trading commands

lxy há 1 mês atrás
pai
commit
ac4f78a677

+ 4 - 0
README.md

@@ -16,6 +16,8 @@ python -m okx_codex_trader.cli backtest-rsi2-report --symbol BTC-USDT-SWAP --bar
 python -m okx_codex_trader.cli backtest-ema-pullback-report --symbol BTC-USDT-SWAP --bar 3m --history-limit 5000 --leverage 2 --segments 8 --window-size 300 --fast-ema 20 --slow-ema 50 --stop-buffer-pct 0.005 --output-file ema-pullback-sampled-report.html
 python -m okx_codex_trader.cli paper-order --symbol BTC-USDT-SWAP --signal-file signal.json --margin-usdt 100
 python -m okx_codex_trader.cli positions --symbol BTC-USDT-SWAP
+OKX_TRADING_ENV=live python -m okx_codex_trader.cli okx-account --symbol BTC-USDT-SWAP --currency USDT
+OKX_TRADING_ENV=live python -m okx_codex_trader.cli okx-order --symbol BTC-USDT-SWAP --signal-file signal.json --margin-usdt 5 --max-margin-usdt 5 --confirm-live
 ```
 
 Supported symbols are `BTC-USDT-SWAP` and `ETH-USDT-SWAP`. Backtest leverage is restricted to `1`, `2`, or `3`.
@@ -24,6 +26,8 @@ Sampled reports generate one self-contained HTML file with switchable sampled wi
 
 `fetch-history`, `backtest`, and `paper-order` use public OKX market data only. `paper-order` and `positions` persist local simulated state in `paper_state.json`.
 
+Authenticated OKX commands require `OKX_API_KEY`, `OKX_API_SECRET`, `OKX_API_PASSPHRASE`, and `OKX_TRADING_ENV=demo|live`. Live orders require `--confirm-live`; every OKX order command also requires `--max-margin-usdt`, and rejects larger `--margin-usdt`.
+
 ## Verification notes
 
 - Public OKX market-data calls were exercised in automated tests through the client contract and CLI flow.

+ 39 - 0
okx_codex_trader/cli.py

@@ -128,6 +128,17 @@ def build_parser() -> argparse.ArgumentParser:
     positions = subparsers.add_parser("positions")
     positions.add_argument("--symbol", choices=SUPPORTED_SYMBOLS, required=True)
 
+    okx_account = subparsers.add_parser("okx-account")
+    okx_account.add_argument("--symbol", choices=SUPPORTED_SYMBOLS, required=True)
+    okx_account.add_argument("--currency", default="USDT")
+
+    okx_order = subparsers.add_parser("okx-order")
+    okx_order.add_argument("--symbol", choices=SUPPORTED_SYMBOLS, required=True)
+    okx_order.add_argument("--signal-file", required=True)
+    okx_order.add_argument("--margin-usdt", type=float, required=True)
+    okx_order.add_argument("--max-margin-usdt", type=float, required=True)
+    okx_order.add_argument("--confirm-live", action="store_true")
+
     return parser
 
 
@@ -147,6 +158,7 @@ def main_factory(
     *,
     load_config: Callable[[], Config] = load_config,
     client_factory: Callable[[], OkxClient] = OkxClient,
+    authenticated_client_factory: Callable[[Config], OkxClient] = OkxClient,
     analyze_fn: Callable = analyze_with_codex,
     report_fn: Callable = generate_backtest_report,
     bbmr_report_fn: Callable = generate_bbmr_sampled_report,
@@ -232,6 +244,33 @@ def main_factory(
             print(_dump_json(asdict(order)))
             return 0
 
+        if args.command == "okx-account":
+            auth_client = authenticated_client_factory(load_config())
+            print(
+                _dump_json(
+                    {
+                        "balance": auth_client.get_account_balance(args.currency),
+                        "positions": [asdict(position) for position in auth_client.get_positions(args.symbol)],
+                    }
+                )
+            )
+            return 0
+
+        if args.command == "okx-order":
+            config = load_config()
+            if config.trading_env == "live" and not args.confirm_live:
+                raise ValueError("live order requires --confirm-live")
+            if args.margin_usdt > args.max_margin_usdt:
+                raise ValueError("margin_usdt exceeds max_margin_usdt")
+            signal = validate_signal(json.loads(Path(args.signal_file).read_text()))
+            order = authenticated_client_factory(config).place_order(
+                symbol=args.symbol,
+                signal=signal,
+                margin_usdt=args.margin_usdt,
+            )
+            print(_dump_json(asdict(order)))
+            return 0
+
         state = load_state(state_path)
         positions = [asdict(position) for position in state.positions if position.symbol == args.symbol]
         if not state_path.exists():

+ 5 - 0
okx_codex_trader/config.py

@@ -8,6 +8,7 @@ class Config:
     api_key: str
     api_secret: str
     api_passphrase: str
+    trading_env: str = "demo"
 
 
 def load_config(env: Mapping[str, str] | None = None) -> Config:
@@ -17,8 +18,12 @@ def load_config(env: Mapping[str, str] | None = None) -> Config:
     api_passphrase = source.get("OKX_API_PASSPHRASE")
     if not api_key or not api_secret or not api_passphrase:
         raise ValueError("OKX credentials are required")
+    trading_env = source.get("OKX_TRADING_ENV", "demo")
+    if trading_env not in {"demo", "live"}:
+        raise ValueError("OKX_TRADING_ENV must be demo or live")
     return Config(
         api_key=api_key,
         api_secret=api_secret,
         api_passphrase=api_passphrase,
+        trading_env=trading_env,
     )

+ 26 - 2
okx_codex_trader/okx_client.py

@@ -118,7 +118,7 @@ class OkxClient:
                 "OK-ACCESS-SIGN": signature,
                 "OK-ACCESS-TIMESTAMP": timestamp,
                 "OK-ACCESS-PASSPHRASE": self.config.api_passphrase,
-                "x-simulated-trading": "1",
+                "x-simulated-trading": "1" if self.config.trading_env == "demo" else "0",
             }
         if json_body is not None:
             headers["Content-Type"] = "application/json"
@@ -243,7 +243,31 @@ class OkxClient:
             },
         )
 
-    def place_demo_order(self, symbol: str, signal: TradeSignal, margin_usdt: float) -> OrderResult:
+    def get_account_balance(self, currency: str = "USDT") -> dict[str, float]:
+        data = self._request("GET", "/api/v5/account/balance", params={"ccy": currency})
+        account = self._first_item(data)
+        details = account.get("details")
+        if not isinstance(details, list):
+            raise self._invalid_payload()
+        for detail in details:
+            if not isinstance(detail, dict):
+                raise self._invalid_payload()
+            if detail.get("ccy") != currency:
+                continue
+            return {
+                "total_equity_usd": _parse_finite_float(account["totalEq"]),
+                "equity": _parse_finite_float(detail["eq"]),
+                "available_equity": _parse_finite_float(detail["availEq"]),
+                "cash_balance": _parse_finite_float(detail["cashBal"]),
+            }
+        return {
+            "total_equity_usd": _parse_finite_float(account["totalEq"]),
+            "equity": 0.0,
+            "available_equity": 0.0,
+            "cash_balance": 0.0,
+        }
+
+    def place_order(self, symbol: str, signal: TradeSignal, margin_usdt: float) -> OrderResult:
         if signal.action == "flat":
             return OrderResult(
                 status="noop",

+ 113 - 1
tests/test_cli.py

@@ -7,7 +7,7 @@ import pytest
 from okx_codex_trader.backtest import run_backtest
 from okx_codex_trader.cli import main_factory
 from okx_codex_trader.config import Config
-from okx_codex_trader.models import Candle, TradeSignal
+from okx_codex_trader.models import Candle, OrderResult, Position, TradeSignal
 
 
 def sample_config() -> Config:
@@ -59,6 +59,9 @@ class FakeClient:
     def __init__(self):
         self.get_candles_called_with: tuple[str, str, int] | None = None
         self.get_last_price_called_with: str | None = None
+        self.get_account_balance_called_with: str | None = None
+        self.get_positions_called_with: str | None = None
+        self.place_order_called_with: dict[str, object] | None = None
 
     def get_candles(self, symbol: str, bar: str, limit: int) -> list[Candle]:
         self.get_candles_called_with = (symbol, bar, limit)
@@ -68,6 +71,26 @@ class FakeClient:
         self.get_last_price_called_with = symbol
         return 250.0
 
+    def get_account_balance(self, currency: str) -> dict[str, float]:
+        self.get_account_balance_called_with = currency
+        return {"total_equity_usd": 101.0, "equity": 100.0, "available_equity": 99.0, "cash_balance": 100.0}
+
+    def get_positions(self, symbol: str) -> list[Position]:
+        self.get_positions_called_with = symbol
+        return [Position(symbol=symbol, pos_side="long", size=1.0, avg_price=100.0)]
+
+    def place_order(self, *, symbol: str, signal: TradeSignal, margin_usdt: float) -> OrderResult:
+        self.place_order_called_with = {"symbol": symbol, "signal": signal, "margin_usdt": margin_usdt}
+        return OrderResult(
+            status="placed",
+            order_id="123",
+            symbol=symbol,
+            side="buy",
+            pos_side="long",
+            order_type="market",
+            size=1.0,
+        )
+
 
 def fake_client() -> FakeClient:
     return FakeClient()
@@ -175,6 +198,7 @@ def build_main_with_stubs(*, state_path: Path | None = None):
     main = main_factory(
         load_config=lambda: sample_config(),
         client_factory=lambda: client,
+        authenticated_client_factory=lambda config: client,
         analyze_fn=fake_analyze_with_codex,
         write_text=real_write_text,
         state_path=Path("paper_state.json") if state_path is None else state_path,
@@ -381,6 +405,94 @@ def test_positions_prints_local_state_positions(tmp_path, capsys):
     assert json.loads(capsys.readouterr().out) == expected
 
 
+def test_okx_account_prints_authenticated_balance_and_positions(capsys):
+    main, client, _, _, _, _ = build_main_with_stubs()
+
+    exit_code = main(["okx-account", "--symbol", "BTC-USDT-SWAP", "--currency", "USDT"])
+
+    assert exit_code == 0
+    assert client.get_account_balance_called_with == "USDT"
+    assert client.get_positions_called_with == "BTC-USDT-SWAP"
+    assert json.loads(capsys.readouterr().out) == {
+        "balance": {"total_equity_usd": 101.0, "equity": 100.0, "available_equity": 99.0, "cash_balance": 100.0},
+        "positions": [{"symbol": "BTC-USDT-SWAP", "pos_side": "long", "size": 1.0, "avg_price": 100.0}],
+    }
+
+
+def test_okx_order_places_demo_order_from_signal_file(tmp_path, capsys):
+    main, client, _, _, _, _ = build_main_with_stubs()
+    signal_file = tmp_path / "signal.json"
+    signal_file.write_text(json.dumps(valid_signal()))
+
+    exit_code = main(
+        [
+            "okx-order",
+            "--symbol",
+            "BTC-USDT-SWAP",
+            "--signal-file",
+            str(signal_file),
+            "--margin-usdt",
+            "5",
+            "--max-margin-usdt",
+            "10",
+        ]
+    )
+
+    assert exit_code == 0
+    assert client.place_order_called_with is not None
+    assert client.place_order_called_with["symbol"] == "BTC-USDT-SWAP"
+    assert client.place_order_called_with["margin_usdt"] == 5.0
+    assert json.loads(capsys.readouterr().out)["order_id"] == "123"
+
+
+def test_okx_order_rejects_live_order_without_confirmation(tmp_path):
+    client = fake_client()
+    main = main_factory(
+        load_config=lambda: Config(api_key="key", api_secret="secret", api_passphrase="passphrase", trading_env="live"),
+        client_factory=lambda: client,
+        authenticated_client_factory=lambda config: client,
+        analyze_fn=fake_analyze_with_codex,
+    )
+    signal_file = tmp_path / "signal.json"
+    signal_file.write_text(json.dumps(valid_signal()))
+
+    with pytest.raises(ValueError, match="live order requires --confirm-live"):
+        main(
+            [
+                "okx-order",
+                "--symbol",
+                "BTC-USDT-SWAP",
+                "--signal-file",
+                str(signal_file),
+                "--margin-usdt",
+                "5",
+                "--max-margin-usdt",
+                "10",
+            ]
+        )
+
+
+def test_okx_order_rejects_margin_above_cap(tmp_path):
+    main, _, _, _, _, _ = build_main_with_stubs()
+    signal_file = tmp_path / "signal.json"
+    signal_file.write_text(json.dumps(valid_signal()))
+
+    with pytest.raises(ValueError, match="margin_usdt exceeds max_margin_usdt"):
+        main(
+            [
+                "okx-order",
+                "--symbol",
+                "BTC-USDT-SWAP",
+                "--signal-file",
+                str(signal_file),
+                "--margin-usdt",
+                "11",
+                "--max-margin-usdt",
+                "10",
+            ]
+        )
+
+
 def test_fetch_history_does_not_require_credentials(capsys):
     client = fake_client()
     main = main_factory(

+ 26 - 0
tests/test_config.py

@@ -29,3 +29,29 @@ def test_load_config_uses_explicit_env_mapping():
     assert config.api_key == "key"
     assert config.api_secret == "secret"
     assert config.api_passphrase == "passphrase"
+    assert config.trading_env == "demo"
+
+
+def test_load_config_uses_live_trading_env():
+    config = load_config(
+        {
+            "OKX_API_KEY": "key",
+            "OKX_API_SECRET": "secret",
+            "OKX_API_PASSPHRASE": "passphrase",
+            "OKX_TRADING_ENV": "live",
+        }
+    )
+
+    assert config.trading_env == "live"
+
+
+def test_load_config_rejects_unknown_trading_env():
+    with pytest.raises(ValueError, match="OKX_TRADING_ENV must be demo or live"):
+        load_config(
+            {
+                "OKX_API_KEY": "key",
+                "OKX_API_SECRET": "secret",
+                "OKX_API_PASSPHRASE": "passphrase",
+                "OKX_TRADING_ENV": "paper",
+            }
+        )

+ 101 - 25
tests/test_okx_client.py

@@ -87,6 +87,10 @@ def sample_config() -> Config:
     return Config(api_key="key", api_secret="secret", api_passphrase="passphrase")
 
 
+def live_config() -> Config:
+    return Config(api_key="key", api_secret="secret", api_passphrase="passphrase", trading_env="live")
+
+
 def candles_response() -> DummyResponse:
     return DummyResponse(
         {
@@ -174,6 +178,28 @@ def ticker_response(last: str) -> DummyResponse:
     return DummyResponse({"code": "0", "msg": "", "data": [{"instId": "BTC-USDT-SWAP", "last": last}]})
 
 
+def balance_response() -> DummyResponse:
+    return DummyResponse(
+        {
+            "code": "0",
+            "msg": "",
+            "data": [
+                {
+                    "totalEq": "101.5",
+                    "details": [
+                        {
+                            "ccy": "USDT",
+                            "eq": "100.25",
+                            "availEq": "98.75",
+                            "cashBal": "100.0",
+                        }
+                    ],
+                }
+            ],
+        }
+    )
+
+
 def account_config_response(pos_mode: str) -> DummyResponse:
     return DummyResponse({"code": "0", "msg": "", "data": [{"posMode": pos_mode}]})
 
@@ -462,6 +488,17 @@ def test_signed_demo_request_attaches_headers():
     assert request.timeout > 0
 
 
+def test_signed_live_request_attaches_live_header():
+    session = DummySession()
+    client = OkxClient(config=live_config(), session=session)
+
+    client.get_candles(symbol="BTC-USDT-SWAP", bar="1H", limit=20)
+
+    request = session.last_request
+    assert request is not None
+    assert request.headers["x-simulated-trading"] == "0"
+
+
 def test_signed_post_request_uses_actual_serialized_body_bytes():
     session = DummySession(
         [
@@ -473,7 +510,7 @@ def test_signed_post_request_uses_actual_serialized_body_bytes():
     )
     client = OkxClient(config=sample_config(), session=session)
 
-    client.place_demo_order(symbol="ETH-USDT-SWAP", signal=limit_short_signal(), margin_usdt=100)
+    client.place_order(symbol="ETH-USDT-SWAP", signal=limit_short_signal(), margin_usdt=100)
 
     request = session.last_request
     assert request is not None
@@ -534,6 +571,45 @@ def test_get_candles_paginates_when_limit_exceeds_single_page():
     assert len(session.request_paths) == 2
 
 
+def test_get_account_balance_returns_usdt_equity_fields():
+    session = DummySession([balance_response()])
+    client = OkxClient(config=sample_config(), session=session)
+
+    balance = client.get_account_balance("USDT")
+
+    assert session.request_paths == ["/api/v5/account/balance"]
+    assert balance == {
+        "total_equity_usd": 101.5,
+        "equity": 100.25,
+        "available_equity": 98.75,
+        "cash_balance": 100.0,
+    }
+
+
+def test_get_account_balance_returns_zero_when_currency_detail_is_absent():
+    session = DummySession(
+        [
+            DummyResponse(
+                {
+                    "code": "0",
+                    "msg": "",
+                    "data": [{"totalEq": "0", "details": []}],
+                }
+            )
+        ]
+    )
+    client = OkxClient(config=sample_config(), session=session)
+
+    balance = client.get_account_balance("USDT")
+
+    assert balance == {
+        "total_equity_usd": 0.0,
+        "equity": 0.0,
+        "available_equity": 0.0,
+        "cash_balance": 0.0,
+    }
+
+
 def test_build_contract_size_rounds_down_to_lot_size():
     metadata = InstrumentMeta(ct_val=0.01, lot_sz=0.1, min_sz=0.1)
     assert build_contract_size(notional=251, price=25_000, metadata=metadata) == 1.0
@@ -609,7 +685,7 @@ def test_market_order_fetches_latest_price_before_sizing():
     )
     client = OkxClient(config=sample_config(), session=session)
 
-    client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
+    client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
 
     assert session.request_paths == [
         "/api/v5/account/config",
@@ -641,13 +717,13 @@ def test_fractional_margin_sizing_keeps_decimal_precision():
     )
     client = OkxClient(config=sample_config(), session=session)
 
-    client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=0.009)
+    client.place_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=0.009)
 
     assert session.last_json_body is not None
     assert session.last_json_body["sz"] == "27"
 
 
-def test_place_demo_order_fails_when_not_hedge_mode():
+def test_place_order_fails_when_not_hedge_mode():
     session = DummySession(
         [
             account_config_response(pos_mode="net_mode"),
@@ -656,7 +732,7 @@ def test_place_demo_order_fails_when_not_hedge_mode():
     client = OkxClient(config=sample_config(), session=session)
 
     with pytest.raises(ValueError):
-        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
+        client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
     assert session.request_paths == ["/api/v5/account/config"]
 
 
@@ -668,7 +744,7 @@ def test_ensure_hedge_mode_rejects_malformed_config_payload():
         client.ensure_hedge_mode()
 
 
-def test_place_demo_order_validates_size_before_setting_leverage():
+def test_place_order_validates_size_before_setting_leverage():
     session = DummySession(
         [
             account_config_response(pos_mode="long_short_mode"),
@@ -679,7 +755,7 @@ def test_place_demo_order_validates_size_before_setting_leverage():
     client = OkxClient(config=sample_config(), session=session)
 
     with pytest.raises(ValueError, match="contract size below minimum"):
-        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
+        client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
 
     assert session.request_paths == [
         "/api/v5/account/config",
@@ -699,7 +775,7 @@ def test_limit_short_order_uses_sell_and_short_pos_side():
     )
     client = OkxClient(config=sample_config(), session=session)
 
-    client.place_demo_order(symbol="ETH-USDT-SWAP", signal=limit_short_signal(), margin_usdt=100)
+    client.place_order(symbol="ETH-USDT-SWAP", signal=limit_short_signal(), margin_usdt=100)
 
     order_request = session.last_json_body
     assert order_request is not None
@@ -715,13 +791,13 @@ def test_flat_signal_returns_noop_without_order_submission():
     session = DummySession([])
     client = OkxClient(config=sample_config(), session=session)
 
-    result = client.place_demo_order(symbol="BTC-USDT-SWAP", signal=flat_signal(), margin_usdt=100)
+    result = client.place_order(symbol="BTC-USDT-SWAP", signal=flat_signal(), margin_usdt=100)
 
     assert result.status == "noop"
     assert session.request_paths == []
 
 
-def test_place_demo_order_sends_computed_sz_and_ignores_tp_sl_fields():
+def test_place_order_sends_computed_sz_and_ignores_tp_sl_fields():
     session = DummySession(
         [
             account_config_response(pos_mode="long_short_mode"),
@@ -733,7 +809,7 @@ def test_place_demo_order_sends_computed_sz_and_ignores_tp_sl_fields():
     )
     client = OkxClient(config=sample_config(), session=session)
 
-    client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
+    client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
 
     order_request = session.last_json_body
     assert order_request is not None
@@ -854,7 +930,7 @@ def test_get_instrument_meta_rejects_non_swap_type():
         client.get_instrument_meta(symbol="BTC-USDT-SWAP")
 
 
-def test_place_demo_order_raises_when_order_id_is_missing():
+def test_place_order_raises_when_order_id_is_missing():
     session = DummySession(
         [
             account_config_response(pos_mode="long_short_mode"),
@@ -867,11 +943,11 @@ def test_place_demo_order_raises_when_order_id_is_missing():
     client = OkxClient(config=sample_config(), session=session)
 
     with pytest.raises(ValueError, match="okx response payload is invalid"):
-        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
+        client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=100)
     assert session.request_paths[-1] == "/api/v5/trade/order"
 
 
-def test_place_demo_order_rejects_invalid_leverage_before_okx():
+def test_place_order_rejects_invalid_leverage_before_okx():
     session = DummySession([])
     signal = TradeSignal(
         action="long",
@@ -885,11 +961,11 @@ def test_place_demo_order_rejects_invalid_leverage_before_okx():
     client = OkxClient(config=sample_config(), session=session)
 
     with pytest.raises(ValueError, match="leverage is invalid"):
-        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
+        client.place_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
     assert session.request_paths == []
 
 
-def test_place_demo_order_rejects_fractional_leverage_before_okx():
+def test_place_order_rejects_fractional_leverage_before_okx():
     session = DummySession([])
     signal = TradeSignal(
         action="long",
@@ -903,11 +979,11 @@ def test_place_demo_order_rejects_fractional_leverage_before_okx():
     client = OkxClient(config=sample_config(), session=session)
 
     with pytest.raises(ValueError, match="leverage is invalid"):
-        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
+        client.place_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
     assert session.request_paths == []
 
 
-def test_place_demo_order_rejects_boolean_leverage_before_okx():
+def test_place_order_rejects_boolean_leverage_before_okx():
     session = DummySession([])
     signal = TradeSignal(
         action="long",
@@ -921,26 +997,26 @@ def test_place_demo_order_rejects_boolean_leverage_before_okx():
     client = OkxClient(config=sample_config(), session=session)
 
     with pytest.raises(ValueError, match="leverage is invalid"):
-        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
+        client.place_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
     assert session.request_paths == []
 
 
 @pytest.mark.parametrize("margin_usdt", [0, -1, float("nan"), float("inf")])
-def test_place_demo_order_rejects_invalid_margin_before_okx(margin_usdt):
+def test_place_order_rejects_invalid_margin_before_okx(margin_usdt):
     session = DummySession([])
     client = OkxClient(config=sample_config(), session=session)
 
     with pytest.raises(ValueError, match="margin_usdt is invalid"):
-        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=margin_usdt)
+        client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=margin_usdt)
     assert session.request_paths == []
 
 
-def test_place_demo_order_rejects_boolean_margin_before_okx():
+def test_place_order_rejects_boolean_margin_before_okx():
     session = DummySession([])
     client = OkxClient(config=sample_config(), session=session)
 
     with pytest.raises(ValueError, match="margin_usdt is invalid"):
-        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=True)
+        client.place_order(symbol="BTC-USDT-SWAP", signal=market_long_signal(), margin_usdt=True)
     assert session.request_paths == []
 
 
@@ -962,7 +1038,7 @@ def test_set_leverage_validates_public_boundary_inputs(symbol, leverage, pos_sid
     assert session.request_paths == []
 
 
-def test_place_demo_order_rejects_unknown_action_before_okx():
+def test_place_order_rejects_unknown_action_before_okx():
     session = DummySession([])
     signal = TradeSignal(
         action="hold",
@@ -976,7 +1052,7 @@ def test_place_demo_order_rejects_unknown_action_before_okx():
     client = OkxClient(config=sample_config(), session=session)
 
     with pytest.raises(ValueError, match="action is invalid"):
-        client.place_demo_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
+        client.place_order(symbol="BTC-USDT-SWAP", signal=signal, margin_usdt=100)
     assert session.request_paths == []