"""验证 OKX 趋势回调止损挂单:须为 stopLossPrice 条件单,不得为立即市价平仓。""" from __future__ import annotations import sys from pathlib import Path from unittest.mock import MagicMock, patch ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "crypto_monitor_okx")) def main() -> int: captured: list[dict] = [] def fake_create_order(symbol, order_type, side, amount, price, params): captured.append( { "symbol": symbol, "type": order_type, "side": side, "amount": amount, "params": dict(params or {}), } ) return {"id": "test-order", "average": 1.358} mock_exchange = MagicMock() mock_exchange.create_order = fake_create_order mock_exchange.amount_to_precision = lambda sym, amt: amt mock_exchange.market = lambda sym: {"contractSize": 1, "limits": {"amount": {"min": 0.01}}} mock_exchange.load_markets = MagicMock() mock_exchange.price_to_precision = lambda sym, px: str(px) with patch.dict( "os.environ", {"LIVE_TRADING_ENABLED": "true", "OKX_API_KEY": "k", "OKX_API_SECRET": "s", "OKX_API_PASSPHRASE": "p"}, clear=False, ): import app as okx_app okx_app.exchange = mock_exchange okx_app.MARKETS_LOADED = True with patch.object(okx_app, "ensure_okx_live_ready", return_value=(True, "")), patch.object( okx_app, "get_live_position_contracts", return_value=12.0 ), patch.object(okx_app, "cancel_okx_swap_open_orders"): okx_app._okx_place_stop_loss_only("XRP/USDT:USDT", "long", 1.1) assert len(captured) == 1, f"expected 1 create_order call, got {len(captured)}" call = captured[0] params = call["params"] assert call["side"] == "sell", call assert params.get("reduceOnly") is True, params assert "stopLossPrice" in params, f"missing stopLossPrice: {params}" assert params["stopLossPrice"] == 1.1, params assert "stopLoss" not in params, f"nested stopLoss causes immediate close: {params}" print("OK: _okx_place_stop_loss_only uses stopLossPrice conditional attach, not immediate close") return 0 if __name__ == "__main__": raise SystemExit(main())