From 6184e7a11fb35b6d378e0b1e70d0fded9350a248 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 28 May 2026 12:11:02 +0800 Subject: [PATCH] okx --- crypto_monitor_okx/.env.example | 2 ++ crypto_monitor_okx/app.py | 36 ++++++++++--------- okx_orders_lib.py | 63 +++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 16 deletions(-) diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index 9228f16..3e1aa8d 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -85,6 +85,8 @@ KEY_ALERT_INTERVAL_MINUTES=5 BALANCE_REFRESH_SECONDS=60 # 后台监控轮询周期(秒) MONITOR_POLL_SECONDS=3 +# 移动保本同步交易所止盈止损的最小间隔(秒),避免频繁撤挂叠单 +BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC=60 # 使用可用资金时的缓冲比例(如0.98代表用98%) FULL_MARGIN_BUFFER_RATIO=0.98 diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index ec21929..6807b43 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -47,7 +47,7 @@ from fib_key_monitor_lib import ( key_signal_type_for_trade_record, stored_key_signal_type, ) -from okx_orders_lib import fetch_okx_all_open_orders +from okx_orders_lib import cancel_okx_all_open_orders, fetch_okx_all_open_orders from journal_chart_lib import ( JOURNAL_CHART_DEFAULT_LIMIT, JOURNAL_CHART_DEFAULT_TF1, @@ -202,6 +202,10 @@ AUTO_TRANSFER_BJ_HOUR = int(os.getenv("AUTO_TRANSFER_BJ_HOUR", "8")) WECHAT_TIMEOUT_SECONDS = int(os.getenv("WECHAT_TIMEOUT_SECONDS", "10")) AI_TIMEOUT_SECONDS = int(os.getenv("AI_TIMEOUT_SECONDS", "120")) MONITOR_POLL_SECONDS = int(os.getenv("MONITOR_POLL_SECONDS", "3")) +BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC = max( + 15, int(os.getenv("BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC", "60")) +) +_BREAKEVEN_LAST_EX_SYNC: dict[int, float] = {} KLINE_TIMEFRAME = os.getenv("KLINE_TIMEFRAME", "5m") FULL_MARGIN_BUFFER_RATIO = float(os.getenv("FULL_MARGIN_BUFFER_RATIO", "0.98")) TRANSFER_CCY = os.getenv("TRANSFER_CCY", "USDT") @@ -2579,18 +2583,7 @@ def cancel_okx_swap_open_orders(exchange_symbol): return ensure_markets_loaded() try: - exchange.cancel_all_orders(exchange_symbol) - except Exception: - pass - try: - for o in fetch_okx_all_open_orders(exchange, exchange_symbol): - oid = o.get("id") - if oid is None: - continue - try: - exchange.cancel_order(str(oid), exchange_symbol) - except Exception: - pass + cancel_okx_all_open_orders(exchange, exchange_symbol) except Exception: pass @@ -2867,7 +2860,11 @@ def cancel_okx_tpsl_slot(exchange_symbol, slot): if not oid: return ensure_markets_loaded() - exchange.cancel_order(str(oid), exchange_symbol) + cancel_id = str(oid).split(":", 1)[0] + try: + exchange.cancel_order(cancel_id, exchange_symbol, {"stop": True}) + except Exception: + exchange.cancel_order(str(oid), exchange_symbol, {"stop": True}) def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit): @@ -2877,7 +2874,9 @@ def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit): raise RuntimeError(reason or "实盘未就绪") ex_sym = resolve_monitor_exchange_symbol(order_row) direction = order_row["direction"] - cancel_okx_swap_open_orders(ex_sym) + cancelled = cancel_okx_all_open_orders(exchange, ex_sym) + if cancelled > 0: + time.sleep(0.12) pos_amt = get_live_position_contracts(ex_sym, direction) if pos_amt is None or float(pos_amt) <= 0: try: @@ -4687,10 +4686,15 @@ def check_order_monitors(): tp_ex = float(take_profit or 0) ok_live, _live_reason = ensure_okx_live_ready() synced_ex = not ok_live - if ok_live and tp_ex > 0: + last_ex_sync = float(_BREAKEVEN_LAST_EX_SYNC.get(pid, 0)) + interval_ok = ( + time.time() - last_ex_sync + ) >= BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC + if ok_live and tp_ex > 0 and interval_ok: try: replace_active_monitor_tpsl_on_exchange(r, new_sl, tp_ex) synced_ex = True + _BREAKEVEN_LAST_EX_SYNC[pid] = time.time() _clear_breakeven_exchange_warn(pid) except Exception as e: print( diff --git a/okx_orders_lib.py b/okx_orders_lib.py index ffe205b..fa22c9b 100644 --- a/okx_orders_lib.py +++ b/okx_orders_lib.py @@ -14,6 +14,31 @@ def _order_dedupe_key(order: dict) -> str: return str(order.get("id") or info.get("algoId") or info.get("ordId") or "") +def _okx_algo_cancel_id(order_id: str) -> str: + oid = str(order_id or "") + if ":" in oid: + return oid.split(":", 1)[0] + return oid + + +def _okx_order_needs_stop_cancel_param(order: dict) -> bool: + """OKX 条件/算法单撤单须 params.stop=True,否则 cancel_order 走普通单接口会静默失败。""" + if not isinstance(order, dict): + return False + info = order.get("info") or {} + if not isinstance(info, dict): + info = {} + if order.get("stopLossPrice") is not None or order.get("takeProfitPrice") is not None: + return True + if info.get("algoId") or info.get("slTriggerPx") or info.get("tpTriggerPx"): + return True + typ = str(order.get("type") or info.get("ordType") or "").lower() + for token in ("conditional", "oco", "trigger", "move_order_stop", "iceberg"): + if token in typ: + return True + return False + + def fetch_okx_all_open_orders(ex, exchange_symbol: str) -> list[dict]: """合并 OKX 普通挂单与算法挂单(去重)。""" if not exchange_symbol: @@ -51,3 +76,41 @@ def fetch_okx_all_open_orders(ex, exchange_symbol: str) -> list[dict]: except Exception: pass return out + + +def cancel_okx_all_open_orders(ex, exchange_symbol: str) -> int: + """ + 撤销某合约全部挂单(普通 + 条件/算法)。 + OKX 止盈止损在 orders-algo-pending,必须用 stop=True 才能撤掉。 + """ + if not exchange_symbol: + return 0 + ex.load_markets() + sym = exchange_symbol + try: + sym = ex.market(exchange_symbol)["symbol"] + except Exception: + pass + n = 0 + for o in fetch_okx_all_open_orders(ex, sym): + oid = _order_dedupe_key(o) + if not oid: + continue + cancel_id = _okx_algo_cancel_id(oid) + params = {"stop": True} if _okx_order_needs_stop_cancel_param(o) else None + try: + ex.cancel_order(cancel_id, sym, params) + n += 1 + continue + except Exception: + pass + try: + ex.cancel_order(oid, sym, params) + n += 1 + except Exception: + pass + try: + ex.cancel_all_orders(sym) + except Exception: + pass + return n