From 8501f7fe0ed7d4c0f498e4abce08ec272207ebed Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 30 May 2026 10:06:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dokx=20=E8=B6=8B=E5=8A=BF?= =?UTF-8?q?=E5=9B=9E=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto_monitor_okx/app.py | 63 +++++++++++++++++++++++++++++--------- strategy_trend_register.py | 62 ++++++++++++++++++++++++++++++++----- 2 files changed, 103 insertions(+), 22 deletions(-) diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 51cc6e9..ac446a5 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -2675,12 +2675,46 @@ def _position_matches_wanted_contract(exchange_symbol, position): if sym == exchange_symbol: return True try: - return normalize_okx_symbol(sym or "") == normalize_okx_symbol(exchange_symbol or "") + if normalize_okx_symbol(sym or "") == normalize_okx_symbol(exchange_symbol or ""): + return True except Exception: + pass + info = position.get("info") or {} + inst = (info.get("instId") or "").strip().upper() + if not inst: return False + try: + ensure_markets_loaded() + want = exchange.market(exchange_symbol) + mid = (want.get("id") or "").strip().upper() + if mid and inst == mid: + return True + base = (want.get("base") or "").strip().upper() + quote = (want.get("quote") or "").strip().upper() + if base and quote and inst == f"{base}-{quote}-SWAP": + return True + except Exception: + pass + return False -def _fetch_okx_swap_position_rows(exchange_symbol=None): +def _okx_position_direction(position): + info = position.get("info") or {} + side = (position.get("side") or info.get("posSide") or "").strip().lower() + if side in ("long", "short"): + return side + try: + raw = float(info.get("pos") or position.get("contracts") or 0) + except (TypeError, ValueError): + raw = 0.0 + if raw > 0: + return "long" + if raw < 0: + return "short" + return "" + + +def _fetch_okx_swap_position_rows(): """OKX 单合约 fetch_positions([sym]) 常返回空;与 /api/prices 一致拉全量 SWAP 再本地匹配。""" ensure_markets_loaded() rows = None @@ -2695,16 +2729,11 @@ def _fetch_okx_swap_position_rows(exchange_symbol=None): continue if rows is None: return None - if not exchange_symbol: - return rows - out = [] - for p in rows: - if _position_matches_wanted_contract(exchange_symbol, p): - out.append(p) - return out + return rows def _select_live_position_row(rows, exchange_symbol, direction, relax_hedge=False): + exchange_symbol = normalize_okx_symbol(exchange_symbol or "") if not rows: return None candidates = [] @@ -2716,8 +2745,13 @@ def _select_live_position_row(rows, exchange_symbol, direction, relax_hedge=Fals contracts = _position_row_effective_contracts(p) if contracts <= 0: continue - if (not relax_hedge) and OKX_POS_MODE == "hedge": - if side and side != (direction or "").lower(): + want_dir = (direction or "").lower() + if OKX_POS_MODE == "net" or side == "net": + pos_dir = _okx_position_direction(p) + if pos_dir and pos_dir != want_dir: + continue + elif (not relax_hedge) and OKX_POS_MODE == "hedge": + if side and side != want_dir: continue candidates.append((contracts, p)) if not candidates and (not relax_hedge) and OKX_POS_MODE == "hedge": @@ -3004,12 +3038,11 @@ def is_no_position_error(err_msg): def get_live_position_contracts(exchange_symbol, direction): - rows = _fetch_okx_swap_position_rows(exchange_symbol) + ex_sym = normalize_okx_symbol(exchange_symbol or "") + rows = _fetch_okx_swap_position_rows() if rows is None: return None - if not rows: - return 0.0 - prow = _select_live_position_row(rows, exchange_symbol, direction) + prow = _select_live_position_row(rows, ex_sym, direction) if not prow: return 0.0 return _position_row_effective_contracts(prow) diff --git a/strategy_trend_register.py b/strategy_trend_register.py index f7e5ca3..e7752bc 100644 --- a/strategy_trend_register.py +++ b/strategy_trend_register.py @@ -34,8 +34,8 @@ MONITOR_TYPE_TREND = MONITOR_TYPE_TREND_PULLBACK # 趋势回调:交易所报空仓需连续 N 次轮询确认,避免 OKX 等 API 瞬时误判立即结束计划 _TREND_FLAT_STREAK: dict[int, int] = {} -TREND_FLAT_CONFIRM_POLLS = max(1, int(os.getenv("TREND_FLAT_CONFIRM_POLLS", "3"))) -TREND_OPEN_GRACE_SEC = max(0, int(os.getenv("TREND_OPEN_GRACE_SEC", "90"))) +TREND_FLAT_CONFIRM_POLLS = max(1, int(os.getenv("TREND_FLAT_CONFIRM_POLLS", "5"))) +TREND_OPEN_GRACE_SEC = max(0, int(os.getenv("TREND_OPEN_GRACE_SEC", "180"))) def trend_add_zone_label(direction: str) -> str: @@ -417,24 +417,58 @@ def _trend_plan_open_age_sec(row, m) -> float: to_ms = getattr(m, "_to_ms_with_fallback", None) if callable(to_ms): opened_ms = to_ms(opened_ms, row["opened_at"] if "opened_at" in row.keys() else None) + if opened_ms is None and "opened_at" in row.keys(): + opened_ms = to_ms(None, row["opened_at"]) if not opened_ms: - return 999999.0 + return 0.0 return max(0.0, (time.time() * 1000 - opened_ms) / 1000.0) +def _trend_hit_take_profit(direction: str, mark_price: float, take_profit: float, avg_entry: float) -> bool: + try: + pf = float(mark_price) + tp = float(take_profit) + entry = float(avg_entry) + except (TypeError, ValueError): + return False + if entry <= 0 or tp <= 0: + return False + direction = (direction or "long").lower() + if direction == "long": + return tp > entry and pf >= tp + return tp < entry and pf <= tp + + def _should_finalize_trend_flat(row, pos, plan_id: int, m) -> bool: """首仓后交易所报无仓:需过开仓宽限期 + 连续空仓轮询,避免误判止损。""" - if pos is None or float(pos) > 0: + if pos is None: + return False + if float(pos) > 0: _TREND_FLAT_STREAK.pop(plan_id, None) return False if not int(row["first_order_done"] or 0): return False - if _trend_plan_open_age_sec(row, m) < TREND_OPEN_GRACE_SEC: + age = _trend_plan_open_age_sec(row, m) + if age < TREND_OPEN_GRACE_SEC: _TREND_FLAT_STREAK.pop(plan_id, None) return False + try: + local_open = float(row["order_amount_open"] or 0) + except (TypeError, ValueError): + local_open = 0.0 + required = TREND_FLAT_CONFIRM_POLLS + if local_open > 0 and age < TREND_OPEN_GRACE_SEC * 2: + required = max(required, TREND_FLAT_CONFIRM_POLLS * 2) streak = int(_TREND_FLAT_STREAK.get(plan_id, 0)) + 1 _TREND_FLAT_STREAK[plan_id] = streak - return streak >= TREND_FLAT_CONFIRM_POLLS + if streak >= required: + print( + f"[trend_pullback] flat finalize plan={plan_id} sym={row['symbol']} " + f"age={age:.0f}s streak={streak} local_open={local_open}", + flush=True, + ) + return True + return False def check_trend_pullback_plans(cfg: dict) -> None: @@ -464,6 +498,19 @@ def check_trend_pullback_plans(cfg: dict) -> None: pos = m.get_live_position_contracts(ex_sym, direction) if pos is None: continue + try: + local_open = float(row["order_amount_open"] or 0) + except (TypeError, ValueError): + local_open = 0.0 + if float(pos) <= 0 and local_open > 0: + age = _trend_plan_open_age_sec(row, m) + if age < TREND_OPEN_GRACE_SEC * 2: + print( + f"[trend_pullback] pos fallback plan={plan_id} sym={sym} " + f"ex_pos=0 local_open={local_open} age={age:.0f}s", + flush=True, + ) + pos = local_open legs_done = int(row["legs_done"] or 0) try: leg_amounts = [float(x) for x in json.loads(row["leg_amounts_json"] or "[]")] @@ -473,7 +520,8 @@ def check_trend_pullback_plans(cfg: dict) -> None: grid = json.loads(row["grid_prices_json"] or "[]") except Exception: grid = [] - hit_tp = (direction == "long" and pf >= tp) or (direction == "short" and pf <= tp) + avg_e = float(row["avg_entry_price"] or pf or 0) + hit_tp = _trend_hit_take_profit(direction, pf, tp, avg_e) if hit_tp and pos > 0: try: close_resp = trend_market_close(cfg, ex_sym, direction, float(pos), lev)