修复okx 趋势回调
This commit is contained in:
@@ -2971,7 +2971,11 @@ def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):
|
|||||||
|
|
||||||
|
|
||||||
def _okx_place_stop_loss_only(exchange_symbol, direction, stop_loss):
|
def _okx_place_stop_loss_only(exchange_symbol, direction, stop_loss):
|
||||||
"""OKX 永续:仅挂止损(趋势回调),止盈由程序监控。"""
|
"""OKX 永续:仅挂止损(趋势回调),止盈由程序监控。
|
||||||
|
|
||||||
|
须用 stopLossPrice 挂条件单;勿用 reduce-only 市价单 + params['stopLoss'],
|
||||||
|
后者会当成立即市价平仓(开仓后约 1 秒内全平)。
|
||||||
|
"""
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
pos_amt = get_live_position_contracts(exchange_symbol, direction)
|
pos_amt = get_live_position_contracts(exchange_symbol, direction)
|
||||||
if pos_amt is None or float(pos_amt) <= 0:
|
if pos_amt is None or float(pos_amt) <= 0:
|
||||||
@@ -2979,12 +2983,27 @@ def _okx_place_stop_loss_only(exchange_symbol, direction, stop_loss):
|
|||||||
cancel_okx_swap_open_orders(exchange_symbol)
|
cancel_okx_swap_open_orders(exchange_symbol)
|
||||||
close_side = "sell" if direction == "long" else "buy"
|
close_side = "sell" if direction == "long" else "buy"
|
||||||
amt = float(exchange.amount_to_precision(exchange_symbol, float(pos_amt)))
|
amt = float(exchange.amount_to_precision(exchange_symbol, float(pos_amt)))
|
||||||
params = build_okx_order_params(direction, reduce_only=True)
|
if amt <= 0:
|
||||||
params["stopLoss"] = {
|
raise RuntimeError("止损:可平数量经精度舍入后为 0")
|
||||||
"triggerPrice": _okx_algo_trigger_price_str(exchange_symbol, stop_loss),
|
base = build_okx_order_params(direction, reduce_only=True)
|
||||||
"type": "market",
|
sl_px = float(stop_loss)
|
||||||
}
|
last_err = None
|
||||||
exchange.create_order(exchange_symbol, "market", close_side, amt, None, params)
|
for attempt in range(6):
|
||||||
|
try:
|
||||||
|
exchange.create_order(
|
||||||
|
exchange_symbol,
|
||||||
|
"market",
|
||||||
|
close_side,
|
||||||
|
amt,
|
||||||
|
None,
|
||||||
|
{**base, "stopLossPrice": sl_px},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
last_err = e
|
||||||
|
cancel_okx_swap_open_orders(exchange_symbol)
|
||||||
|
time.sleep(0.2 * (attempt + 1))
|
||||||
|
raise RuntimeError(f"OKX 未接受止损条件单:{last_err}")
|
||||||
|
|
||||||
|
|
||||||
def calc_trend_manual_breakeven_stop(direction, entry_price, offset_pct=None):
|
def calc_trend_manual_breakeven_stop(direction, entry_price, offset_pct=None):
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""验证 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())
|
||||||
Reference in New Issue
Block a user