From 0760873d9dd512dea2bc16acf9851e7951c7d042 Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 7 Jun 2026 17:38:26 +0800 Subject: [PATCH] fix(trend): use mark price for DCA trigger on gate_bot Poll trend plans with mark price (same as UI) instead of ticker last, add get_symbol_mark_price to gate_bot, tolerate position API blips, and log DCA order failures. Co-authored-by: Cursor --- crypto_monitor_gate_bot/app.py | 18 +++++++++ strategy_trend_register.py | 69 ++++++++++++++++++++++++++++------ tests/test_trend_dca_pnl.py | 6 +++ 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 294a38f..b5a713c 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -4370,6 +4370,24 @@ def get_price(symbol): except: return None + +def get_symbol_mark_price(symbol): + """趋势补仓/标记价展示:优先 ticker.mark,与页面浮盈亏口径一致。""" + ex_sym = normalize_exchange_symbol(symbol) + try: + ensure_markets_loaded() + ticker = exchange.fetch_ticker(ex_sym) + m = _coerce_float(ticker.get("mark"), ticker.get("last")) + if m is None: + info = ticker.get("info") or {} + m = _coerce_float(info.get("mark_price"), info.get("last")) + if m is not None and m > 0: + return float(m) + except Exception: + pass + p = get_price(symbol) + return float(p) if p is not None else None + # 获取5分钟K线收盘价 def get_5m_close(symbol): try: diff --git a/strategy_trend_register.py b/strategy_trend_register.py index f2f70c5..cedfd38 100644 --- a/strategy_trend_register.py +++ b/strategy_trend_register.py @@ -686,6 +686,33 @@ def _trend_hit_take_profit(direction: str, mark_price: float, take_profit: float return tp < entry and pf <= tp +def _trend_poll_price(m, sym: str, ex_sym: str, direction: str) -> Optional[float]: + """补仓/止盈判定用标记价(与页面「标记价」一致),无标记价时回退 last。""" + fn = getattr(m, "get_symbol_mark_price", None) + if callable(fn): + try: + px = fn(sym) + if px is not None and float(px) > 0: + return float(px) + except Exception: + pass + metrics_fn = getattr(m, "get_live_position_exchange_metrics", None) + if callable(metrics_fn): + try: + met = metrics_fn(ex_sym, direction) + if met and met.get("mark_price") is not None: + px = float(met["mark_price"]) + if px > 0: + return px + except Exception: + pass + px = m.get_price(sym) + try: + return float(px) if px is not None else None + except (TypeError, ValueError): + return None + + def _should_finalize_trend_flat(row, pos, plan_id: int, m) -> bool: """首仓后交易所报无仓:需过开仓宽限期 + 连续空仓轮询,避免误判止损。""" if pos is None: @@ -736,20 +763,22 @@ def check_trend_pullback_plans(cfg: dict) -> None: sl = float(row["stop_loss"]) tp = float(row["take_profit"]) lev = int(row["leverage"] or 1) - p = m.get_price(sym) - if not p: - continue - pf = float(p) - last_p = row["last_mark_price"] - last_pf = float(last_p) if last_p is not None else pf - 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: + pf = _trend_poll_price(m, sym, ex_sym, direction) + if pf is None: + continue + last_p = row["last_mark_price"] + last_pf = float(last_p) if last_p is not None else pf + pos = m.get_live_position_contracts(ex_sym, direction) + if pos is None: + if local_open > 0 and int(row["first_order_done"] or 0): + pos = local_open + else: + continue + elif float(pos) <= 0 and local_open > 0: age = _trend_plan_open_age_sec(row, m) if age < TREND_OPEN_GRACE_SEC * 2: print( @@ -791,8 +820,21 @@ def check_trend_pullback_plans(cfg: dict) -> None: break amt = float(m.exchange.amount_to_precision(ex_sym, leg_amounts[legs_done])) if amt <= 0: + print( + f"[trend_pullback] dca skip plan={plan_id} leg={legs_done + 1} " + f"amt_precision=0 raw={leg_amounts[legs_done]}", + flush=True, + ) + break + try: + add_resp = trend_market_add(cfg, ex_sym, direction, amt, lev) + except Exception as e: + print( + f"[trend_pullback] dca order failed plan={plan_id} sym={sym} " + f"leg={legs_done + 1} level={level} mark={pf} err={e}", + flush=True, + ) break - add_resp = trend_market_add(cfg, ex_sym, direction, amt, lev) fill_px = m.extract_trade_price_from_order(add_resp) or pf old_avg = float(row["avg_entry_price"] or fill_px) old_open = float(row["order_amount_open"] or 0) @@ -812,6 +854,11 @@ def check_trend_pullback_plans(cfg: dict) -> None: row = conn.execute( "SELECT * FROM trend_pullback_plans WHERE id=?", (row["id"],) ).fetchone() + print( + f"[trend_pullback] dca filled plan={plan_id} leg={legs_done} " + f"fill={fill_px} avg={new_avg} open={old_open + amt}", + flush=True, + ) try: trend_refresh_stop_only(cfg, ex_sym, direction, sl) except Exception: diff --git a/tests/test_trend_dca_pnl.py b/tests/test_trend_dca_pnl.py index 81ee689..54ee8b9 100644 --- a/tests/test_trend_dca_pnl.py +++ b/tests/test_trend_dca_pnl.py @@ -17,6 +17,12 @@ def test_trend_dca_short_not_before_first_level(): assert trend_dca_level_reached(direction, 0.3413, 0.3413) +def test_trend_dca_long_mark_below_trigger(): + direction = "long" + assert trend_dca_level_reached(direction, 0.344, 0.3465) + assert not trend_dca_level_reached(direction, 0.347, 0.3465) + + def test_trend_effective_margin_first_leg_only(): plan = { "plan_margin_capital": 12.11,