通用网格策略(动态移动版)


创建日期: 2026-05-15 16:03:35 最后修改: 2026-06-02 14:12:30
复制: 3 点击次数: 61
avatar of ianzeng123 ianzeng123
2
关注
469
关注者
策略源码
'''backtest
start: 2026-05-07 17:00:00
end: 2026-05-19 14:53:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_OKX","currency":"SPACEX_USDT","balance":300}]
args: [["SYMBOL","SPACEX_USDT.swap"],["INIT_PRICE",3000],["GRID_WIDTH_PCT",30]]
'''

"""
通用网格策略(动态移动版)
========================================================

【方向模式说明】
  - 仅做多(long):价格低于格子价挂买多,涨到上一格止盈
  - 仅做空(short):价格高于格子价挂卖空,跌到下一格止盈
  - 多空都做(both):以区间中线为界,下半做多、上半做空

【百分比参数说明】
  - GRID_WIDTH_PCT: 区间宽度占价格的百分比,如 5 表示区间宽 = 价格 × 5%
  - SHIFT_STEP_PCT: 每次移动占价格的百分比,如 0.5 表示每步 = 价格 × 0.5%
  - BREAKOUT_TRIGGER_PCT: 触发移动的超出百分比,0 = 恰好碰到即触发
"""

import json
import math

# ═══════════════════════════════════════════
#  状态常量
# ═══════════════════════════════════════════
STATE_ACTIVE    = "ACTIVE"
STATE_SHIFTING  = "SHIFTING"
STATE_WAITING   = "WAITING"   # 新增:价格超出区间,等待回归

# ═══════════════════════════════════════════
#  全局状态
# ═══════════════════════════════════════════
state             = STATE_ACTIVE

range_low         = 0.0
range_high        = 0.0
grid_prices       = []
grid_usdt         = []

grids             = []

shift_count       = 0
shift_down_count  = 0
shift_history     = []

empty_bars_count  = 0

Funding           = 0.0
last_closed_ktime = 0

positions_cache   = []
assets = {"USDT": {"total_balance": 0, "margin_balance": 0}}
symbol        = ""
base_currency = ""
swap_code     = ""
direction     = "long"  # runtime direction


# ═══════════════════════════════════════════
#  百分比转绝对值辅助
# ═══════════════════════════════════════════

def pct_to_abs(pct, ref_price):
    """将百分比参数转换为绝对值"""
    return ref_price * pct / 100.0


# ═══════════════════════════════════════════
#  精度 / 换算
# ═══════════════════════════════════════════

def fp(price):
    try:
        return round(price, assets[base_currency]["PricePrecision"])
    except:
        return price


def fa(amount):
    try:
        if amount <= 0:
            return 0
        r = round(amount, assets[base_currency]["AmountPrecision"])
        return r if r > 0 else 0
    except:
        return 0


def usdt_to_contracts(usdt, price):
    try:
        return usdt / (assets[base_currency]["ctVal"] * price)
    except:
        return 0


def contracts_to_usdt(contracts, price):
    try:
        return contracts * assets[base_currency]["ctVal"] * price
    except:
        return 0


def get_min_qty():
    try:
        return assets[base_currency]["MinQty"]
    except:
        return 0.001


def get_price():
    return assets[base_currency]["price"]


# ═══════════════════════════════════════════
#  账户 / 行情
# ═══════════════════════════════════════════

def update_price():
    ticker = exchange.GetTicker(swap_code)
    if not ticker:
        return False
    assets[base_currency]["price"] = ticker.Last
    return True


def update_account():
    try:
        acc = exchange.GetAccount()
        Sleep(500)
        if not acc:
            return False
        assets["USDT"]["equity"]         = acc.Equity
        assets["USDT"]["total_balance"]  = acc.Balance + acc.FrozenBalance
        assets["USDT"]["margin_balance"] = acc.Balance
        global Funding
        Funding = acc.Equity
        long_amt, long_hp, long_pft = _get_position(PD_LONG)
        short_amt, short_hp, short_pft = _get_position(PD_SHORT)
        assets[base_currency]["long_amount"]   = long_amt
        assets[base_currency]["long_price"]    = long_hp
        assets[base_currency]["long_profit"]   = long_pft
        assets[base_currency]["short_amount"]  = short_amt
        assets[base_currency]["short_price"]   = short_hp
        assets[base_currency]["short_profit"]  = short_pft
        assets[base_currency]["amount"]        = long_amt + short_amt
        assets[base_currency]["unrealised_profit"] = long_pft + short_pft
        global positions_cache
        try:
            positions_cache = exchange.GetPositions(swap_code) or []
            Sleep(500)
        except Exception:
            positions_cache = []
        return True
    except Exception as e:
        Log(f"更新账户异常: {e}")
        return False


def _get_position(pos_type=PD_LONG):
    """获取指定方向的持仓"""
    try:
        positions = exchange.GetPositions(swap_code)
        Sleep(500)
        if not positions:
            return 0, 0, 0
        for pos in positions:
            if pos["Amount"] > 0 and pos["Type"] == pos_type:
                return pos["Amount"], pos["Price"], pos["Profit"]
        return 0, 0, 0
    except Exception as e:
        Log(f"获取持仓异常: {e}")
        return 0, 0, 0


def get_total_position():
    long_amt, _, _ = _get_position(PD_LONG)
    short_amt, _, _ = _get_position(PD_SHORT)
    return long_amt + short_amt


# ═══════════════════════════════════════════
#  订单操作
# ═══════════════════════════════════════════

def cancel_all_orders(sc):
    try:
        orders = exchange.GetOrders(sc)
        if orders:
            for o in orders:
                exchange.CancelOrder(o["Id"])
                Sleep(100)
    except Exception as e:
        Log(f"撤单异常: {e}")


def cancel_open_orders_only():
    """
    【V4.1 新增】只撤销开仓挂单(pending_buy 状态的格子),
    保留止盈挂单(pending_close),持仓继续持有。
    """
    cancelled = 0
    for i, g in enumerate(grids):
        if g["status"] == "pending_buy" and g.get("buy_oid"):
            cancel_order_safe(g["buy_oid"])
            g["buy_oid"] = None
            g["status"]  = "empty"   # 回归区间后 _try_reopen 会自动重挂
            cancelled += 1
    if cancelled > 0:
        Log(f"[撤开仓单] 共撤销 {cancelled} 个开仓挂单,止盈单和持仓保持不动")


def cancel_order_safe(oid):
    if not oid:
        return
    try:
        exchange.CancelOrder(oid)
        Sleep(100)
    except Exception as e:
        Log(f"撤单{oid}异常: {e}")


def check_order_status(oid):
    if oid is None:
        return "unknown", 0, 0
    try:
        order = exchange.GetOrder(oid)
        if not order:
            return "unknown", 0, 0
        status = order.get("Status", -1)
        deal   = order.get("DealAmount", 0) or 0
        avg_px = order.get("AvgPrice", 0) or order.get("Price", 0) or 0
        if status == 1:
            return "filled", deal, avg_px
        elif status == 2:
            return "cancelled", deal, avg_px
        elif status in (0, 3):
            return "active", deal, avg_px
        else:
            return "unknown", deal, avg_px
    except Exception as e:
        Log(f"查询订单{oid}异常: {e}")
        return "unknown", 0, 0


def place_limit_buy(buy_price, usdt_amount, label=""):
    """开多单(限价买入开仓)"""
    raw       = usdt_to_contracts(usdt_amount, buy_price)
    contracts = fa(raw)
    if contracts <= 0:
        return None

    min_qty      = assets[base_currency]["MinQty"]
    ct_val       = assets[base_currency]["ctVal"]
    min_notional = assets[base_currency].get("MinNotional", 5)
    amt_prec     = assets[base_currency]["AmountPrecision"]

    if contracts < min_qty:
        return "skip_min_detail", raw, min_qty

    notional = contracts * ct_val * buy_price
    if notional < min_notional:
        step = 10 ** (-amt_prec)
        contracts_needed = math.ceil(min_notional / (ct_val * buy_price) / step) * step
        contracts_needed = round(contracts_needed, amt_prec)
        if contracts_needed < min_qty:
            contracts_needed = min_qty
        if contracts_needed * ct_val * buy_price < min_notional:
            contracts_needed = round(contracts_needed + step, amt_prec)
        Log(f"[{label}] 名义价值{notional:.2f}U < {min_notional}U,"
            f"ceil补足: {contracts}张 → {contracts_needed}张 "
            f"({contracts_needed * ct_val * buy_price:.2f}U)")
        contracts = contracts_needed

    price_f = fp(buy_price)
    order_u = contracts * ct_val * buy_price
    Log(f"[{label}] 限价买开多 @{price_f}  {contracts}张  ≈{order_u:.2f}U")
    oid = exchange.CreateOrder(swap_code, "buy", price_f, contracts)
    Log(f"  → {'成功 ID=' + str(oid) if oid else '失败'}")
    return oid


def place_limit_sell_open(sell_price, usdt_amount, label=""):
    """开空单(限价卖出开仓)"""
    raw       = usdt_to_contracts(usdt_amount, sell_price)
    contracts = fa(raw)
    if contracts <= 0:
        return None

    min_qty      = assets[base_currency]["MinQty"]
    ct_val       = assets[base_currency]["ctVal"]
    min_notional = assets[base_currency].get("MinNotional", 5)
    amt_prec     = assets[base_currency]["AmountPrecision"]

    if contracts < min_qty:
        return "skip_min_detail", raw, min_qty

    notional = contracts * ct_val * sell_price
    if notional < min_notional:
        step = 10 ** (-amt_prec)
        contracts_needed = math.ceil(min_notional / (ct_val * sell_price) / step) * step
        contracts_needed = round(contracts_needed, amt_prec)
        if contracts_needed < min_qty:
            contracts_needed = min_qty
        if contracts_needed * ct_val * sell_price < min_notional:
            contracts_needed = round(contracts_needed + step, amt_prec)
        Log(f"[{label}] 名义价值{notional:.2f}U < {min_notional}U,"
            f"ceil补足: {contracts}张 → {contracts_needed}张 "
            f"({contracts_needed * ct_val * sell_price:.2f}U)")
        contracts = contracts_needed

    price_f = fp(sell_price)
    order_u = contracts * ct_val * sell_price
    Log(f"[{label}] 限价卖开空 @{price_f}  {contracts}张  ≈{order_u:.2f}U")
    oid = exchange.CreateOrder(swap_code, "sell", price_f, contracts)
    Log(f"  → {'成功 ID=' + str(oid) if oid else '失败'}")
    return oid


def place_limit_close_long(sell_price, contracts, label=""):
    """平多单(限价卖出平仓)"""
    fc = fa(contracts)
    if fc <= 0:
        return None
    price_f = fp(sell_price)
    Log(f"[{label}] 限价平多 @{price_f}  {fc}张")
    oid = exchange.CreateOrder(swap_code, "closebuy", price_f, fc)
    Log(f"  → {'成功 ID=' + str(oid) if oid else '失败'}")
    return oid


def place_limit_close_short(buy_price, contracts, label=""):
    """平空单(限价买入平仓)"""
    fc = fa(contracts)
    if fc <= 0:
        return None
    price_f = fp(buy_price)
    Log(f"[{label}] 限价平空 @{price_f}  {fc}张")
    oid = exchange.CreateOrder(swap_code, "closesell", price_f, fc)
    Log(f"  → {'成功 ID=' + str(oid) if oid else '失败'}")
    return oid


def close_all_positions_market():
    """平掉所有持仓(多头+空头)—— 仅在手动触发时使用"""
    for attempt in range(10):
        long_amt, _, _ = _get_position(PD_LONG)
        short_amt, _, _ = _get_position(PD_SHORT)
        if long_amt <= 0 and short_amt <= 0:
            Log(f"[平仓] 持仓已清空(第{attempt + 1}次确认)")
            return
        price = get_price()
        if long_amt > 0:
            sell_p = fp(price * (1 - 0.001))
            fc = fa(long_amt)
            Log(f"[平仓] 第{attempt + 1}次平多: {fc}张 @{sell_p}")
            exchange.CreateOrder(swap_code, "closebuy", sell_p, fc)
            Sleep(1000)
        if short_amt > 0:
            buy_p = fp(price * (1 + 0.001))
            fc = fa(short_amt)
            Log(f"[平仓] 第{attempt + 1}次平空: {fc}张 @{buy_p}")
            exchange.CreateOrder(swap_code, "closesell", buy_p, fc)
            Sleep(1000)
        Sleep(1500)

    long_final, _, _ = _get_position(PD_LONG)
    short_final, _, _ = _get_position(PD_SHORT)
    if long_final > 0 or short_final > 0:
        Log(f"警告: 平仓重试10次后仍有持仓(多{long_final}/空{short_final}),请手动检查!")


# ═══════════════════════════════════════════
#  动态格数计算
# ═══════════════════════════════════════════

def calc_grids(r_low, r_high):
    price       = get_price()
    range_width = r_high - r_low

    total_position_usdt = Funding * LEVERAGE / 2

    min_step_pct = FEE_RATE * 2 * FEE_PROFIT_MULTI
    min_step_abs = price * min_step_pct
    min_step_abs = max(min_step_abs, 10 ** (-assets[base_currency]["PricePrecision"]))
    max_by_range = int(range_width / min_step_abs)

    min_qty         = get_min_qty()
    ct_val          = assets[base_currency]["ctVal"]
    total_contracts = total_position_usdt / (ct_val * price)
    max_by_capital  = int(total_contracts / min_qty) if min_qty > 0 else max_by_range

    n = min(max_by_range, max_by_capital)
    if n < 1:
        Log(f"警告: 区间太窄或资金不足,无法建立有效格子 "
            f"(by_range={max_by_range}, by_capital={max_by_capital})")
        return [], []

    step   = range_width / n
    prices = [fp(r_low + i * step) for i in range(n + 1)]
    prices[-1] = fp(r_high)

    dists      = [r_high - prices[i] for i in range(n)]
    total_dist = sum(dists)
    usdt_list  = []
    for i in range(n):
        u = total_position_usdt * dists[i] / total_dist if total_dist > 0 else total_position_usdt / n
        usdt_list.append(round(u, 4))

    Log(f"动态格数计算: 区间宽度={range_width:.4f}  最小格距={min_step_abs:.4f}")
    Log(f"  理论格数={max_by_range}  资金格数={max_by_capital}  实际格数={n}")
    Log(f"  总仓位={total_position_usdt:.2f}U  总张数≈{total_contracts:.4f}  最小单张={min_qty}  格距={step:.4f}")
    Log(f"  最小格金额={min(usdt_list):.2f}U  最大格金额={max(usdt_list):.2f}U  合计={sum(usdt_list):.2f}U")
    return prices, usdt_list


# ═══════════════════════════════════════════
#  激活格子
# ═══════════════════════════════════════════

def activate_grids(r_low, r_high):
    global state, grids, grid_prices, grid_usdt, range_low, range_high

    range_low  = r_low
    range_high = r_high

    prices, usdt_list = calc_grids(r_low, r_high)
    if not prices:
        raise Exception("格子计算失败,请检查区间设置和资金")

    grid_prices = prices
    grid_usdt   = usdt_list
    n           = len(usdt_list)

    Log(f"区间激活: 下沿={r_low} 上沿={r_high} 格数={n} 方向={direction}")
    for i in range(n):
        Log(f"  格{i}: 下@{prices[i]} ↔ 上@{prices[i + 1]}  {usdt_list[i]:.2f}U")

    grids = [{"buy_oid": None, "sell_oid": None, "status": "empty",
              "buy_contracts": 0, "filled_price": 0, "grid_dir": direction} for _ in range(n)]

    price = get_price()
    mid_price = (r_low + r_high) / 2.0

    for i in range(n):
        grid_low  = prices[i]
        grid_high = prices[i + 1]

        if direction == "long":
            if grid_low < price:
                grids[i]["grid_dir"] = "long"
                _place_grid_open(i, "long")
            else:
                grids[i]["grid_dir"] = "long"
                grids[i]["status"] = "skip_above"

        elif direction == "short":
            if grid_high > price:
                grids[i]["grid_dir"] = "short"
                _place_grid_open(i, "short")
            else:
                grids[i]["grid_dir"] = "short"
                grids[i]["status"] = "skip_below"

        else:  # both
            if grid_high <= mid_price:
                grids[i]["grid_dir"] = "long"
                if grid_low < price:
                    _place_grid_open(i, "long")
                else:
                    grids[i]["status"] = "skip_above"
            else:
                grids[i]["grid_dir"] = "short"
                if grid_high > price:
                    _place_grid_open(i, "short")
                else:
                    grids[i]["status"] = "skip_below"

    state = STATE_ACTIVE


def activate_grids_keep_positions(r_low, r_high):
    """
    【V4.1 新增】移动区间时保留现有持仓的版本。
    - 对已持仓格子(pending_close / holding_no_close):保留止盈单,不动
    - 对空格子:按新区间重新挂开仓单
    """
    global state, grids, grid_prices, grid_usdt, range_low, range_high

    # 先记录当前所有持仓格子的信息,用于后续匹配
    old_holding = []
    for g in grids:
        if g["status"] in ("pending_close", "holding_no_close") and g.get("buy_contracts", 0) > 0:
            old_holding.append({
                "buy_contracts": g["buy_contracts"],
                "filled_price":  g["filled_price"],
                "sell_oid":      g.get("sell_oid"),
                "grid_dir":      g.get("grid_dir", direction),
                "status":        g["status"],
            })

    range_low  = r_low
    range_high = r_high

    prices, usdt_list = calc_grids(r_low, r_high)
    if not prices:
        raise Exception("格子计算失败,请检查区间设置和资金")

    grid_prices = prices
    grid_usdt   = usdt_list
    n           = len(usdt_list)

    Log(f"区间激活(保留持仓): 下沿={r_low} 上沿={r_high} 格数={n}  现有持仓={len(old_holding)}格继续持有")

    # 重建 grids,先全部标空
    grids = [{"buy_oid": None, "sell_oid": None, "status": "empty",
              "buy_contracts": 0, "filled_price": 0, "grid_dir": direction} for _ in range(n)]

    # 把旧持仓格子填回(按顺序匹配前几格)
    for idx, hold in enumerate(old_holding):
        if idx >= n:
            Log(f"警告: 旧持仓格{idx}超出新格数{n},该持仓止盈单将被忽略(持仓仍在,需手动处理)")
            break
        grids[idx]["buy_contracts"] = hold["buy_contracts"]
        grids[idx]["filled_price"]  = hold["filled_price"]
        grids[idx]["sell_oid"]      = hold["sell_oid"]
        grids[idx]["grid_dir"]      = hold["grid_dir"]
        grids[idx]["status"]        = hold["status"]
        Log(f"  恢复持仓 → 格{idx}: {hold['buy_contracts']}张 @{hold['filled_price']}  状态={hold['status']}")

    # 对空格子挂新开仓单
    price     = get_price()
    mid_price = (r_low + r_high) / 2.0

    for i in range(n):
        g = grids[i]
        if g["status"] in ("pending_close", "holding_no_close"):
            continue  # 跳过持仓格,不重新挂开仓单

        grid_low  = prices[i]
        grid_high = prices[i + 1]

        if direction == "long":
            g["grid_dir"] = "long"
            if grid_low < price:
                _place_grid_open(i, "long")
            else:
                g["status"] = "skip_above"

        elif direction == "short":
            g["grid_dir"] = "short"
            if grid_high > price:
                _place_grid_open(i, "short")
            else:
                g["status"] = "skip_below"

        else:  # both
            if grid_high <= mid_price:
                g["grid_dir"] = "long"
                if grid_low < price:
                    _place_grid_open(i, "long")
                else:
                    g["status"] = "skip_above"
            else:
                g["grid_dir"] = "short"
                if grid_high > price:
                    _place_grid_open(i, "short")
                else:
                    g["status"] = "skip_below"

    state = STATE_ACTIVE


def _place_grid_open(i, grid_dir):
    """挂开仓单(做多=买开,做空=卖开)"""
    n = len(grids)
    if i >= n:
        return
    g = grids[i]
    if g["status"] in ("holding", "pending_sell", "pending_buy", "pending_close"):
        return

    if grid_dir == "long":
        result = place_limit_buy(grid_prices[i], grid_usdt[i],
                                 label=f"格{i} 多@{grid_prices[i]}")
    else:
        result = place_limit_sell_open(grid_prices[i + 1], grid_usdt[i],
                                       label=f"格{i} 空@{grid_prices[i+1]}")

    if isinstance(result, tuple) and result[0] == "skip_min_detail":
        _, raw_contracts, min_qty = result
        g["status"]   = "skip_min"
        g["skip_raw"] = round(raw_contracts, 6)
        g["skip_min"] = min_qty
    elif result:
        g["buy_oid"] = result
        g["status"]  = "pending_buy"
    else:
        g["status"]  = "skip_min"


def _place_grid_close(i, contracts, grid_dir):
    """挂止盈平仓单(做多=卖平,做空=买平)"""
    n = len(grids)
    if i >= n:
        return
    g = grids[i]

    if grid_dir == "long":
        sell_price = grid_prices[i + 1]
        oid = place_limit_close_long(sell_price, contracts,
                                     label=f"格{i} 平多@{sell_price}")
    else:
        buy_price = grid_prices[i]
        oid = place_limit_close_short(buy_price, contracts,
                                      label=f"格{i} 平空@{buy_price}")

    if oid:
        g["sell_oid"]      = oid
        g["buy_contracts"] = contracts
        g["status"]        = "pending_close"
    else:
        g["status"]        = "holding_no_close"
        g["buy_contracts"] = contracts
        g["sell_oid"]      = None
        Log(f"格{i} 止盈挂单失败,标记 holding_no_close,下轮重试")


# ═══════════════════════════════════════════
#  区间突破检测 & 移动
# ═══════════════════════════════════════════

def check_breakout_and_shift():
    """
    【V4.2 改动】
    只在"有利方向"触发区间移动,不利方向完全不动、不撤单:
      - long : 涨破上沿 -> 上移(有利);跌破下沿 -> 忽略(不利,等待回归)
      - short: 跌破下沿 -> 下移(有利);涨破上沿 -> 忽略(不利,等待回归)
      - both : 双向均触发移动
    移动时不强制平仓,只撤开仓挂单,持仓继续持有。
    """
    global range_low, range_high, shift_count, state

    price = get_price()

    ref_price      = (range_low + range_high) / 2.0
    trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref_price)

    price_above = price >= range_high + trigger_offset
    price_below = price <= range_low  - trigger_offset

    if direction == "long":
        # 做多:只有涨破上沿才上移(有利);跌破下沿不动
        if price_above:
            return _do_shift_up(price)
        if price_below:
            Log(f"[做多] 价格{price}跌破下沿{range_low}(不利方向),不移动不撤单,等待回归")
            return False

    elif direction == "short":
        # 做空:只有跌破下沿才下移(有利);涨破上沿不动
        if price_below:
            return _do_shift_down_auto(price)
        if price_above:
            Log(f"[做空] 价格{price}涨破上沿{range_high}(不利方向),不移动不撤单,等待回归")
            return False

    else:  # both:双向均触发
        if price_above:
            return _do_shift_up(price)
        if price_below:
            return _do_shift_down_auto(price)

    return False


def _do_shift_up(price):
    """价格突破上沿 → 区间上移(不平仓)"""
    global range_low, range_high, shift_count, state

    old_low  = range_low
    old_high = range_high
    new_high = range_high
    new_low  = range_low
    steps    = 0

    while True:
        ref = (new_low + new_high) / 2.0
        trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
        if price < new_high + trigger_offset:
            break
        shift_abs = pct_to_abs(SHIFT_STEP_PCT, ref)
        if shift_abs <= 0:
            break
        new_high  = round(new_high + shift_abs, 8)
        width_abs = pct_to_abs(GRID_WIDTH_PCT, new_high)
        new_low   = round(new_high - width_abs, 8)
        steps    += 1
        if steps > 500:
            Log("警告: 上移计算超过500步")
            break

    Log("=" * 60)
    Log(f"★ 区间上移触发!当前价={price}  旧上沿={old_high}")
    Log(f"  旧区间: {old_low} ~ {old_high}")
    Log(f"  新区间: {fp(new_low)} ~ {fp(new_high)}  (共上移{steps}步)")
    Log(f"  ⚠️  持仓不平仓,继续持有等待止盈!")
    Log("=" * 60)

    state = STATE_SHIFTING
    _execute_shift_keep_positions(old_low, old_high, fp(new_low), fp(new_high), steps, "↑上移", price)
    return True


def _do_shift_down_auto(price):
    """价格突破下沿 → 区间下移(不平仓)"""
    global range_low, range_high, shift_count, state

    old_low  = range_low
    old_high = range_high
    new_low  = range_low
    new_high = range_high
    steps    = 0

    while True:
        ref = (new_low + new_high) / 2.0
        trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
        if price > new_low - trigger_offset:
            break
        shift_abs = pct_to_abs(SHIFT_STEP_PCT, ref)
        if shift_abs <= 0:
            break
        new_low   = round(new_low - shift_abs, 8)
        width_abs = pct_to_abs(GRID_WIDTH_PCT, new_low)
        new_high  = round(new_low + width_abs, 8)
        steps    += 1
        if steps > 500:
            Log("警告: 下移计算超过500步")
            break

    Log("=" * 60)
    Log(f"★ 区间下移触发!当前价={price}  旧下沿={old_low}")
    Log(f"  旧区间: {old_low} ~ {old_high}")
    Log(f"  新区间: {fp(new_low)} ~ {fp(new_high)}  (共下移{steps}步)")
    Log(f"  ⚠️  持仓不平仓,继续持有等待止盈!")
    Log("=" * 60)

    state = STATE_SHIFTING
    _execute_shift_keep_positions(old_low, old_high, fp(new_low), fp(new_high), steps, "↓下移(自动)", price)
    return True


def _execute_shift_keep_positions(old_low, old_high, new_low, new_high, steps, dir_label, price):
    """
    【V4.1】执行区间移动:只撤开仓单,保留持仓,用新区间重建格子。
    """
    global shift_count, state

    # 1. 只撤开仓挂单,止盈单保留
    Log("步骤1: 撤销开仓挂单(止盈单和持仓保留)...")
    cancel_open_orders_only()
    Sleep(500)

    # 2. 记录历史
    shift_count += 1
    shift_history.append({
        "n":        shift_count,
        "dir":      dir_label,
        "price":    round(price, 8),
        "old_low":  old_low,
        "old_high": old_high,
        "new_low":  new_low,
        "new_high": new_high,
    })

    # 3. 刷新账户
    Log("步骤2: 刷新账户数据...")
    for _ in range(5):
        if update_account():
            break
        Sleep(2000)

    # 4. 用保留持仓的方式激活新区间
    Log("步骤3: 激活新区间格子(保留持仓)...")
    activate_grids_keep_positions(new_low, new_high)
    Log(f"★ 区间移动完成!累计移动 {shift_count} 次,持仓继续持有中")


# ═══════════════════════════════════════════
#  ★ 区间手动下移(按钮)—— 同样不平仓
# ═══════════════════════════════════════════

def shift_range_down():
    global range_low, range_high, shift_down_count, state

    price    = get_price()
    old_low  = range_low
    old_high = range_high

    ref = (range_low + range_high) / 2.0
    shift_abs = pct_to_abs(SHIFT_STEP_PCT, ref)

    new_high = fp(round(range_high - shift_abs, 8))
    width_abs = pct_to_abs(GRID_WIDTH_PCT, new_high)
    new_low  = fp(round(new_high - width_abs, 8))

    Log("=" * 60)
    Log(f"★ 手动降低下沿触发!当前价={price}")
    Log(f"  旧区间: {old_low} ~ {old_high}")
    Log(f"  新区间: {new_low} ~ {new_high}  (下移≈{shift_abs:.4f})")
    Log(f"  ⚠️  持仓不平仓,继续持有等待止盈!")
    Log("=" * 60)

    state = STATE_SHIFTING

    cancel_open_orders_only()
    Sleep(500)

    shift_down_count += 1
    shift_history.append({
        "n":        f"↓{shift_down_count}",
        "dir":      "↓手动下移",
        "price":    round(price, 8),
        "old_low":  old_low,
        "old_high": old_high,
        "new_low":  new_low,
        "new_high": new_high,
    })

    for _ in range(5):
        if update_account():
            break
        Sleep(2000)

    activate_grids_keep_positions(new_low, new_high)
    Log(f"★ 手动区间下移完成!累计手动下移 {shift_down_count} 次,持仓继续持有中")
    return True


def shift_range_up():
    """手动升高上沿(不平仓)"""
    global range_low, range_high, shift_count, state

    price    = get_price()
    old_low  = range_low
    old_high = range_high

    ref = (range_low + range_high) / 2.0
    shift_abs = pct_to_abs(SHIFT_STEP_PCT, ref)

    new_high = fp(round(range_high + shift_abs, 8))
    width_abs = pct_to_abs(GRID_WIDTH_PCT, new_high)
    new_low  = fp(round(new_high - width_abs, 8))

    Log("=" * 60)
    Log(f"★ 手动升高上沿触发!当前价={price}")
    Log(f"  旧区间: {old_low} ~ {old_high}")
    Log(f"  新区间: {new_low} ~ {new_high}  (上移≈{shift_abs:.4f})")
    Log(f"  ⚠️  持仓不平仓,继续持有等待止盈!")
    Log("=" * 60)

    state = STATE_SHIFTING

    cancel_open_orders_only()
    Sleep(500)

    shift_count += 1
    shift_history.append({
        "n":        shift_count,
        "dir":      "↑手动上移",
        "price":    round(price, 8),
        "old_low":  old_low,
        "old_high": old_high,
        "new_low":  new_low,
        "new_high": new_high,
    })

    for _ in range(5):
        if update_account():
            break
        Sleep(2000)

    activate_grids_keep_positions(new_low, new_high)
    Log(f"★ 手动区间上移完成!累计上移 {shift_count} 次,持仓继续持有中")
    return True


# ═══════════════════════════════════════════
#  ★ 交互命令处理
# ═══════════════════════════════════════════

def handle_command():
    try:
        cmd = GetCommand()
        if not cmd:
            return False

        Log(f"收到交互命令: {cmd}")

        if cmd == "shift_down":
            Log("用户点击【降低下沿】按钮")
            shift_range_down()
            refresh_tables()
            return True
        elif cmd == "shift_up":
            Log("用户点击【升高上沿】按钮")
            shift_range_up()
            refresh_tables()
            return True

        Log(f"未知命令: {cmd}")
        return False
    except Exception as e:
        Log(f"处理命令异常: {e}")
        return False


# ═══════════════════════════════════════════
#  格子状态机
# ═══════════════════════════════════════════

def sync_grid_orders():
    global empty_bars_count
    price = get_price()
    n     = len(grids)
    if n == 0:
        return

    for i in range(n):
        g       = grids[i]
        g_dir   = g.get("grid_dir", "") or direction

        if g["status"] == "pending_buy":
            st, deal, avg_px = check_order_status(g["buy_oid"])
            if st == "filled":
                contracts = fa(deal) if deal > 0 else fa(usdt_to_contracts(grid_usdt[i], grid_prices[i]))
                if g_dir == "long":
                    filled_px = avg_px if avg_px > 0 else grid_prices[i]
                else:
                    filled_px = avg_px if avg_px > 0 else grid_prices[i + 1]
                Log(f"格{i}({g_dir}) 开仓成交 均价={filled_px}  实际{contracts}张")
                g["buy_oid"]       = None
                g["buy_contracts"] = contracts
                g["filled_price"]  = filled_px
                _place_grid_close(i, contracts, g_dir)
            elif st == "cancelled":
                Log(f"格{i} 开仓单已撤销,重置为 empty")
                g["buy_oid"] = None
                g["status"]  = "empty"

        elif g["status"] == "pending_close":
            st, deal, avg_px = check_order_status(g["sell_oid"])
            if st == "filled":
                if g_dir == "long":
                    filled_px = avg_px if avg_px > 0 else grid_prices[i + 1]
                else:
                    filled_px = avg_px if avg_px > 0 else grid_prices[i]
                Log(f"格{i}({g_dir}) 止盈成交 均价={filled_px} → 重置")
                g["sell_oid"]      = None
                g["buy_contracts"] = 0
                g["filled_price"]  = 0
                g["status"]        = "empty"
                _try_reopen(i, g_dir, price)
            elif st == "cancelled":
                Log(f"格{i} 止盈单已撤销,重新挂止盈单")
                g["sell_oid"] = None
                contracts = g.get("buy_contracts", 0)
                if contracts > 0:
                    _place_grid_close(i, contracts, g_dir)
                else:
                    g["status"] = "empty"

        elif g["status"] in ("empty", "skip_min", "skip_above", "skip_below"):
            _try_reopen(i, g_dir, price)

        elif g["status"] == "holding_no_close":
            contracts = g.get("buy_contracts", 0)
            if contracts > 0:
                Log(f"格{i} holding_no_close 重试挂止盈单")
                _place_grid_close(i, contracts, g_dir)
            else:
                g["status"] = "empty"

    # 空仓计数
    if EMPTY_TIMEOUT_BARS > 0:
        total_amt   = get_total_position()
        has_holding = any(g["status"] == "pending_close" for g in grids)
        if total_amt <= 0 and not has_holding:
            empty_bars_count += 1
        else:
            empty_bars_count = 0


def _try_reopen(i, g_dir, price):
    """尝试重新挂开仓单"""
    if g_dir == "long":
        if grid_prices[i] < price:
            _place_grid_open(i, "long")
    else:
        if grid_prices[i + 1] > price:
            _place_grid_open(i, "short")


# ═══════════════════════════════════════════
#  初始化
# ═══════════════════════════════════════════

def init():
    global symbol, base_currency, Funding, INIT_FUNDING, swap_code, direction

    direction = DIRECTION.strip().lower() if DIRECTION else "long"
    if direction not in ("long", "short", "both"):
        direction = "long"

    Log(f"通用网格策略(V4.1-不强制平仓版)启动  方向模式={direction}")
    exchange.SetMarginLevel(SYMBOL, LEVERAGE)

    if SYMBOL and SYMBOL.strip():
        symbol = SYMBOL.strip()
        if symbol.endswith(".swap"):
            symbol = symbol[:-5]
        exchange.SetCurrency(symbol)
    else:
        symbol = exchange.GetCurrency()

    base_currency = symbol.split("_")[0]
    swap_code     = symbol + ".swap"

    Log(f"交易对: {symbol}  合约: {swap_code}")

    ticker = exchange.GetTicker(swap_code)
    if not ticker:
        raise Exception(f"无法获取行情: {swap_code}")

    data = exchange.GetMarkets().get(swap_code)
    if not data:
        raise Exception(f"无法获取市场信息: {swap_code}")

    assets[base_currency] = {
        "amount": 0, "long_amount": 0, "long_price": 0, "long_profit": 0,
        "short_amount": 0, "short_price": 0, "short_profit": 0,
        "hold_price": 0, "price": ticker.Last, "unrealised_profit": 0,
        "AmountPrecision": data["AmountPrecision"],
        "PricePrecision":  data["PricePrecision"],
        "MinQty":          data["MinQty"],
        "ctVal":           data["CtVal"],
        "MinNotional":     data.get("MinNotional", 5),
    }

    cancel_all_orders(swap_code)
    acc = exchange.GetAccount()
    if acc:
        INIT_FUNDING = acc.Equity
        Funding      = INIT_FUNDING

    Log(f"账户资金={Funding:.2f}U  杠杆={LEVERAGE}x")
    Log(f"手续费=万{FEE_RATE * 10000:.0f}  格距覆盖费{FEE_PROFIT_MULTI}倍")
    Log(f"最小下单张数={get_min_qty()}  合约面值={assets[base_currency]['ctVal']}")
    Log(f"区间宽度={GRID_WIDTH_PCT}%  移动幅度={SHIFT_STEP_PCT}%  触发偏移={BREAKOUT_TRIGGER_PCT}%")


# ═══════════════════════════════════════════
#  状态面板
# ═══════════════════════════════════════════

def refresh_tables():
    try:
        price  = get_price()
        bal    = assets["USDT"]["total_balance"]
        equity = assets["USDT"].get("equity", bal)
        unreal = assets[base_currency]["unrealised_profit"]
        profit  = round(equity - INIT_FUNDING, 4)
        ppct    = round(profit / INIT_FUNDING * 100, 2) if INIT_FUNDING else 0
        n       = len(grids)
        pending = sum(1 for g in grids if g["status"] == "pending_buy")
        holding = sum(1 for g in grids if g["status"] in ("pending_close", "holding_no_close"))

        long_amt  = assets[base_currency].get("long_amount", 0)
        short_amt = assets[base_currency].get("short_amount", 0)

        dist_to_upper = round(range_high - price, 4)
        dist_to_lower = round(price - range_low, 4)

        dir_label_map = {"long": "🟢仅做多", "short": "🔴仅做空", "both": "🔵多空都做"}
        dir_label = dir_label_map.get(direction, direction)

        # 判断是否超出区间(区分有利/不利方向)
        ref_price      = (range_low + range_high) / 2.0
        trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref_price)
        above_range    = price >= range_high + trigger_offset
        below_range    = price <= range_low  - trigger_offset

        # 是否处于"不利方向超出"(等待回归,不做任何操作)
        unfavorable_wait = (
            (direction == "long"  and below_range) or
            (direction == "short" and above_range)
        )

        if state == STATE_SHIFTING:
            state_label = "🔄 区间移动中..."
        elif unfavorable_wait:
            state_label = (f"⏳ 不利方向超出区间,挂单保留等待回归 ({n}格 持仓{holding}) {dir_label}")
        elif above_range or below_range:
            state_label = (f"🔄 有利方向突破,区间即将移动 ({n}格 持仓{holding}) {dir_label}")
        else:
            state_label = f"✅ 网格运行 ({n}格 开仓挂单{pending} 持仓{holding}) {dir_label}"

        rng_w   = round(range_high - range_low, 4)
        rng_pct = round(rng_w / price * 100, 3) if price > 0 else 0

        ref = (range_low + range_high) / 2.0
        shift_abs = pct_to_abs(SHIFT_STEP_PCT, ref)

        main = {
            "type": "table",
            "title": f"通用网格(V4.1不强制平仓) | {symbol} | {state_label}",
            "cols": ["项目", "数值", "项目", "数值", "项目", "数值"],
            "rows": [
                ["状态", state_label, "账户权益", f"{equity:.4f}U",
                 "总盈亏", f"{profit:+.4f}U ({ppct:+.2f}%)"],
                ["当前价格", f"{price}", "多头持仓", f"{long_amt}张",
                 "空头持仓", f"{short_amt}张"],
                ["区间下沿", f"{range_low}", "区间上沿", f"{range_high}",
                 "区间宽度", f"{rng_w} ({rng_pct}%)"],
                ["距上沿", f"{dist_to_upper}", "距下沿", f"{dist_to_lower}",
                 "未实现盈亏", f"{unreal:.4f}U"],
                ["方向模式", dir_label,
                 "上移次数", f"{shift_count}次",
                 "手动下移", f"{shift_down_count}次"],
                ["宽度%", f"{GRID_WIDTH_PCT}%",
                 "移动%", f"{SHIFT_STEP_PCT}% (≈{shift_abs:.4f})",
                 "触发%", f"{BREAKOUT_TRIGGER_PCT}%"],
            ]
        }

        ct_val = assets[base_currency].get("ctVal", 1)

        total_hold_contracts = 0
        total_hold_cost      = 0.0
        grid_positions       = []

        for i, g in enumerate(grids):
            contracts = g.get("buy_contracts", 0)
            filled_px = g.get("filled_price", 0)
            g_dir     = g.get("grid_dir", "long")
            if contracts > 0 and filled_px > 0:
                total_hold_contracts += contracts
                total_hold_cost      += contracts * filled_px
                if g_dir == "long":
                    pnl     = contracts * ct_val * (price - filled_px)
                    pnl_pct = (price - filled_px) / filled_px * 100
                    tp_p    = grid_prices[i + 1] if i + 1 < len(grid_prices) else "-"
                else:
                    pnl     = contracts * ct_val * (filled_px - price)
                    pnl_pct = (filled_px - price) / filled_px * 100
                    tp_p    = grid_prices[i] if i < len(grid_prices) else "-"
                grid_positions.append([
                    f"格{i}({g_dir[0].upper()})",
                    f"{contracts}张",
                    f"{filled_px}",
                    f"{price}",
                    f"{pnl:+.4f}U",
                    f"{pnl_pct:+.2f}%",
                    f"{tp_p}",
                ])

        if total_hold_contracts > 0 and total_hold_cost > 0:
            avg_cost = total_hold_cost / total_hold_contracts
            grid_positions.append([
                "═ 合计 ═",
                f"{total_hold_contracts}张",
                f"{avg_cost:.4f}",
                f"{price}",
                f"{unreal:+.4f}U",
                "-",
                "-",
            ])
        else:
            grid_positions = [["无持仓", "-", "-", f"{price}", "-", "-", "-"]]

        pos_tbl = {
            "type": "table",
            "title": f"持仓汇总 | 实时价={price}",
            "cols": ["来源", "持仓数量", "持仓均价", "实时价格", "浮动盈亏", "盈亏比例", "止盈价"],
            "rows": grid_positions
        }

        def short_id(oid):
            return str(oid)[-8:] if oid else "-"

        grid_rows = []
        for i, g in enumerate(grids):
            buy_p     = grid_prices[i]     if i < len(grid_prices)     else 0
            sell_p    = grid_prices[i + 1] if i + 1 < len(grid_prices) else 0
            usdt      = grid_usdt[i]       if i < len(grid_usdt)       else 0
            contracts = g.get("buy_contracts", 0)
            filled_px = g.get("filled_price", 0)
            g_dir     = g.get("grid_dir", "long")

            status_cn = {
                "empty":            "⬜ 等待",
                "pending_buy":      "🟡 挂单开仓",
                "pending_close":    "🔵 持仓止盈中",
                "skip_above":       "⬆️ 等待(价格太低)",
                "skip_below":       "⬇️ 等待(价格太高)",
                "holding_no_close": "⚠️ 持仓待挂止盈",
            }.get(g["status"], g["status"])
            if g["status"] == "skip_min":
                skip_raw = g.get("skip_raw", 0)
                skip_min = g.get("skip_min", get_min_qty())
                status_cn = f"⛔ 不足最小({skip_raw}张 < {skip_min}张)"

            dir_icon = "🟢多" if g_dir == "long" else "🔴空"

            hold_p_str = f"{filled_px}" if (contracts > 0 and filled_px > 0) else "-"

            if contracts > 0 and filled_px > 0:
                if g_dir == "long":
                    pnl = contracts * ct_val * (price - filled_px)
                else:
                    pnl = contracts * ct_val * (filled_px - price)
                pnl_pct = pnl / (contracts * ct_val * filled_px) * 100 if filled_px > 0 else 0
                pnl_str = f"{pnl:+.2f}U ({pnl_pct:+.2f}%)"
            else:
                pnl_str = "-"

            buy_id_str  = f"开:{short_id(g.get('buy_oid'))}"  if g.get("buy_oid")  else "-"
            sell_id_str = f"平:{short_id(g.get('sell_oid'))}" if g.get("sell_oid") else "-"
            order_str   = " / ".join(x for x in [buy_id_str, sell_id_str] if x != "-") or "-"

            grid_rows.append([
                f"格{i}",
                dir_icon,
                f"{buy_p}",
                f"{sell_p}",
                f"{contracts}张" if contracts > 0 else "-",
                hold_p_str,
                pnl_str,
                order_str,
                status_cn,
            ])

        if not grid_rows:
            grid_rows = [["无格子", "-", "-", "-", "-", "-", "-", "-", "无"]]

        grid_tbl = {
            "type": "table",
            "title": f"格子明细 | 下沿={range_low}  上沿={range_high}",
            "cols": ["格子", "方向", "下价", "上价", "持仓量", "开仓价", "浮动盈亏", "挂单ID", "状态"],
            "rows": grid_rows
        }

        history_rows = []
        for h in shift_history[-8:][::-1]:
            history_rows.append([
                f"{h.get('dir', '↑上移')} 第{h['n']}次",
                f"{h['price']}",
                f"{h['old_low']} ~ {h['old_high']}",
                f"{h['new_low']} ~ {h['new_high']}",
            ])
        if not history_rows:
            history_rows = [["暂无", "-", "-", "-"]]

        history_tbl = {
            "type": "table",
            "title": f"区间移动记录(自动移动{shift_count}次 / 手动下移{shift_down_count}次)",
            "cols": ["操作", "触发价格", "旧区间", "新区间"],
            "rows": history_rows
        }

        LogProfit(profit, "&")

        btn_shift_down = {
            "type": "button",
            "cmd":  "shift_down",
            "name": f"降低下沿 (-{SHIFT_STEP_PCT}%)",
            "description": f"区间下移{SHIFT_STEP_PCT}%: 当前{range_low}~{range_high}"
        }
        btn_shift_up = {
            "type": "button",
            "cmd":  "shift_up",
            "name": f"升高上沿 (+{SHIFT_STEP_PCT}%)",
            "description": f"区间上移{SHIFT_STEP_PCT}%: 当前{range_low}~{range_high}"
        }

        LogStatus(
            "`" + json.dumps(main,           ensure_ascii=False) + "`\n"
            "`" + json.dumps(pos_tbl,        ensure_ascii=False) + "`\n"
            "`" + json.dumps(grid_tbl,       ensure_ascii=False) + "`\n"
            "`" + json.dumps(history_tbl,    ensure_ascii=False) + "`\n"
            "`" + json.dumps(btn_shift_down, ensure_ascii=False) + "`\n"
            "`" + json.dumps(btn_shift_up,   ensure_ascii=False) + "`"
        )
    except Exception as e:
        Log(f"状态面板异常: {e}")


# ═══════════════════════════════════════════
#  主入口
# ═══════════════════════════════════════════

def main():

    SetErrorFilter(
        "502:|503:|tcp|character|unexpected|network|timeout|"
        "WSARecv|Connect|GetAddr|no such|reset|http|received|EOF|reused|Unknown"
    )

    init()

    while not update_account():
        Log("等待账户数据...")
        Sleep(3000)
    while not update_price():
        Log("等待行情数据...")
        Sleep(3000)

    price_now = get_price()

    if direction in ("long", "both"):
        if INIT_PRICE > 0:
            initial_high = INIT_PRICE
            Log(f"[做多/双向] 使用用户设定的初始上沿: {initial_high}")
        else:
            initial_high = price_now
            Log(f"[做多/双向] 初始上沿 = 当前价格: {initial_high}")
        width_abs    = pct_to_abs(GRID_WIDTH_PCT, initial_high)
        initial_low  = fp(initial_high - width_abs)
        initial_high = fp(initial_high)

    else:
        if INIT_PRICE > 0:
            initial_low = INIT_PRICE
            Log(f"[做空] 使用用户设定的初始下沿: {initial_low}")
        else:
            initial_low = price_now
            Log(f"[做空] 初始下沿 = 当前价格: {initial_low}")
        width_abs    = pct_to_abs(GRID_WIDTH_PCT, initial_low)
        initial_high = fp(initial_low + width_abs)
        initial_low  = fp(initial_low)

    Log("=" * 60)
    Log(f"策略: 通用网格(V4.1-不强制平仓版)  交易对={symbol}  方向={direction}  杠杆={LEVERAGE}x")
    Log(f"初始区间: {initial_low} ~ {initial_high}  宽度={round(initial_high - initial_low, 8)} ({GRID_WIDTH_PCT}%)")
    Log(f"移动幅度: {SHIFT_STEP_PCT}%/次  触发偏移: {BREAKOUT_TRIGGER_PCT}%")
    Log("=" * 60)

    # 启动时对齐
    align_steps = 0
    if direction in ("long", "both"):
        ref = (initial_low + initial_high) / 2.0
        trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
        while price_now >= initial_high + trigger_offset:
            shift_abs    = pct_to_abs(SHIFT_STEP_PCT, ref)
            initial_high = round(initial_high + shift_abs, 8)
            width_abs    = pct_to_abs(GRID_WIDTH_PCT, initial_high)
            initial_low  = round(initial_high - width_abs, 8)
            ref          = (initial_low + initial_high) / 2.0
            trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
            align_steps += 1
            if align_steps > 500:
                break

    if direction in ("short", "both"):
        ref = (initial_low + initial_high) / 2.0
        trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
        while price_now <= initial_low - trigger_offset:
            shift_abs    = pct_to_abs(SHIFT_STEP_PCT, ref)
            initial_low  = round(initial_low - shift_abs, 8)
            width_abs    = pct_to_abs(GRID_WIDTH_PCT, initial_low)
            initial_high = round(initial_low + width_abs, 8)
            ref          = (initial_low + initial_high) / 2.0
            trigger_offset = pct_to_abs(BREAKOUT_TRIGGER_PCT, ref)
            align_steps += 1
            if align_steps > 500:
                break

    if align_steps > 0:
        initial_low  = fp(initial_low)
        initial_high = fp(initial_high)
        Log(f"启动价格({price_now})超出初始区间,自动调整{align_steps}步 → 区间 {initial_low}~{initial_high}")

    if direction in ("long", "both") and price_now >= initial_high:
        initial_high = price_now
        width_abs    = pct_to_abs(GRID_WIDTH_PCT, initial_high)
        initial_low  = fp(initial_high - width_abs)
        initial_high = fp(initial_high)
        Log(f"启动价格({price_now})超出上沿,直接重算区间 → {initial_low}~{initial_high}")

    if direction in ("short", "both") and price_now <= initial_low:
        initial_low  = price_now
        width_abs    = pct_to_abs(GRID_WIDTH_PCT, initial_low)
        initial_high = fp(initial_low + width_abs)
        initial_low  = fp(initial_low)
        Log(f"启动价格({price_now})低于下沿,直接重算区间 → {initial_low}~{initial_high}")

    activate_grids(initial_low, initial_high)

    while True:
        if not update_account():
            Sleep(5000)
            continue
        if not update_price():
            Sleep(5000)
            continue

        try:
            if handle_command():
                Sleep(POLL_INTERVAL)
                continue
        except Exception as e:
            Log(f"处理命令异常: {e}")

        try:
            if check_breakout_and_shift():
                refresh_tables()
                Sleep(POLL_INTERVAL)
                continue
        except Exception as e:
            Log(f"区间移动异常: {e},5秒后重试")
            Sleep(5000)
            continue

        sync_grid_orders()
        refresh_tables()
        Sleep(POLL_INTERVAL)