下架合约网格策略


创建日期: 2026-04-07 13:36:52 最后修改: 2026-05-18 10:48:25
复制: 0 点击次数: 3
avatar of ianzeng123 ianzeng123
2
关注
460
关注者

⚠️ 重要提醒

在阅读和使用本策略前,请务必注意以下三点:

1. 策略需要耐心等待开仓机会

币安下架合约属于低频事件,并非每天都会发生。策略启动后可能需要等待数天甚至更长时间才会触发首次开仓,运行期间程序大部分时间处于”待机监控”状态,请做好长时间等待的心理准备,不要因为短期内没有交易而误以为策略失效。

2. 开仓检测时机仍有优化空间

本文采用的是每 15 秒轮询一次 fapi/v1/exchangeInfo 接口、通过 deliveryDate 字段变化来识别下架信号的方案。该方法实盘测试存在一定延迟,并非最快路径。读者可以根据自身需求进一步优化检测方式,例如:缩短轮询间隔、并行监控币安公告 API、订阅 WebSocket 推送、结合多源信号交叉验证等,以争取更早的入场时机。越早入场,越能吃到第一波急跌的最大跌幅。

3. 务必及时关停策略,防止利润回撤

实盘中观察到,并非所有下架币种都会一路阴跌到下架时刻。部分品种在公告后急跌一波,随后会在数小时到一天内逐步反弹,价格甚至可能恢复至公告前的水平。如果不及时关停,前期累积的浮盈会在反弹中被大幅吞噬,甚至出现由盈转亏的情况。

建议设置以下任一条件作为主动退出信号: – 达到预设的盈利目标后立即清仓退出; – 价格反弹超过公告后最低点的某一比例(如 20%~30%)时强制平仓; – 持仓回撤达到峰值利润的一定比例(如 30%~50%)时触发止盈保护。

切勿被动等到下架前 60 分钟才平仓——那是兜底机制,不是最优退出时机。

策略源码
import json
from datetime import datetime, timezone

# ═══════════════════════════════════════════
#  用户参数
# ═══════════════════════════════════════════
LEVERAGE          = 10      # 杠杆倍数
GRID_WIDTH_PCT    = 0.10    # 网格区间宽度(当前价的10%)
SHIFT_STEP_PCT    = 0.05    # 每次移动幅度(当前价的5%)
GRID_COUNT        = 10      # 每个合约的网格格数
BASE_SHORT_RATIO  = 0.5     # 底仓占分配资金比例
FORCE_CLOSE_MINS  = 60      # 下架前多少分钟强制平仓
POLL_INTERVAL     = 1000    # 网格轮询间隔 ms
MONITOR_INTERVAL  = 15000   # 监控轮询间隔 ms
FEE_RATE          = 0.0003  # 单边手续费率
PERPETUAL_END     = 4133404800000  # 永续合约默认deliveryDate

# ═══════════════════════════════════════════
#  全局状态
# ═══════════════════════════════════════════
tasks            = {}
known_delist_set = set()   # 当前批次已知下架合约
Funding          = 0.0
INIT_FUNDING     = 0.0

assets_global = {"USDT": {"total_balance": 0, "equity": 0, "margin_used": 0}}


# ═══════════════════════════════════════════
#  时间工具
# ═══════════════════════════════════════════

def get_now_ms():
    dt = datetime.strptime(_D(), "%Y-%m-%d %H:%M:%S")
    return int(dt.replace(tzinfo=timezone.utc).timestamp() * 1000)


# ═══════════════════════════════════════════
#  监控:获取即将下架的 USDT 永续合约
# ═══════════════════════════════════════════
import urllib.request

def fetch_delist_symbols():
    try:

        data = json.loads(urllib.request.urlopen("https://fapi.binance.com/fapi/v1/exchangeInfo").read().decode('utf-8'))
        Log(data)

        now_ms = get_now_ms()
        result = {}
        for s in data.get("symbols", []):
            if not s["symbol"].endswith("USDT"):
                continue
            if s.get("contractType") != "PERPETUAL":
                continue
            dd = s.get("deliveryDate", PERPETUAL_END)
            if dd < PERPETUAL_END and dd > now_ms:
                result[s["symbol"]] = dd
        return result
    except Exception as e:
        Log(f"[监控] 获取下架合约失败: {e}")
        return {}


# ═══════════════════════════════════════════
#  全局账户更新
# ═══════════════════════════════════════════

def update_global_account():
    global Funding
    try:
        acc = exchange.GetAccount()
        Sleep(300)
        if not acc:
            return False
        assets_global["USDT"]["equity"]        = acc.Equity
        assets_global["USDT"]["total_balance"] = acc.Balance + acc.FrozenBalance
        assets_global["USDT"]["margin_used"]   = acc.FrozenBalance
        Funding = acc.Equity
        return True
    except Exception as e:
        Log(f"更新全局账户异常: {e}")
        return False


# ═══════════════════════════════════════════
#  精度 / 换算(按 task)
# ═══════════════════════════════════════════

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


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


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


def get_min_qty(task):
    return task["assets"].get("MinQty", 0.001)


def get_task_price(task):
    return task["assets"].get("price", 0)


# ═══════════════════════════════════════════
#  行情 / 持仓更新(按 task)
# ═══════════════════════════════════════════

def update_task_price(task):
    try:
        ticker = exchange.GetTicker(task["symbol"] + ".swap")
        if not ticker:
            return False
        task["assets"]["price"] = ticker.Last
        return True
    except Exception as e:
        Log(f"[{task['symbol']}] 更新价格异常: {e}")
        return False


def update_task_position(task):
    try:
        positions = exchange.GetPosition(task["symbol"] + ".swap")
        Sleep(200)
        if not positions:
            task["assets"]["amount"]            = 0
            task["assets"]["hold_price"]        = 0
            task["assets"]["unrealised_profit"] = 0
            return
        for pos in positions:
            if pos["ContractType"] == "swap" and pos["Amount"] > 0 and pos["Type"] == 1:
                task["assets"]["amount"]            = pos["Amount"]
                task["assets"]["hold_price"]        = pos["Price"]
                task["assets"]["unrealised_profit"] = pos["Profit"]
                return
        task["assets"]["amount"]            = 0
        task["assets"]["hold_price"]        = 0
        task["assets"]["unrealised_profit"] = 0
    except Exception as e:
        Log(f"[{task['symbol']}] 更新持仓异常: {e}")


def get_short_position(task):
    try:
        positions = exchange.GetPosition(task["symbol"] + ".swap")
        Sleep(200)
        if not positions:
            return 0, 0, 0
        for pos in positions:
            if pos["ContractType"] == "swap" and pos["Amount"] > 0 and pos["Type"] == 1:
                return pos["Amount"], pos["Price"], pos["Profit"]
        return 0, 0, 0
    except Exception as e:
        Log(f"[{task['symbol']}] 获取持仓异常: {e}")
        return 0, 0, 0


# ═══════════════════════════════════════════
#  订单操作(按 task)
# ═══════════════════════════════════════════

def cancel_all_orders(task):
    try:
        orders = exchange.GetOrders(task["symbol"] + ".swap")
        if orders:
            for o in orders:
                exchange.CancelOrder(o["Id"])
                Sleep(100)
    except Exception as e:
        Log(f"[{task['symbol']}] 撤单异常: {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_market_short(task, usdt_amount, label="底仓开空"):
    price     = get_task_price(task)
    raw       = usdt_to_contracts(task, usdt_amount, price)
    contracts = fa(task, raw)
    if contracts <= 0:
        Log(f"[{task['symbol']}][{label}] 合约数量不足,跳过")
        return None
    Log(f"[{task['symbol']}][{label}] 市价开空 {contracts}张  ≈{usdt_amount:.2f}U")
    oid = exchange.CreateOrder(task["symbol"] + ".swap", "sell", -1, contracts)
    Log(f"  → {'成功 ID=' + str(oid) if oid else '失败'}")
    return oid


def place_limit_short(task, sell_price, usdt_amount, label=""):
    raw       = usdt_to_contracts(task, usdt_amount, sell_price)
    contracts = fa(task, raw)
    if contracts <= 0:
        return None
    if contracts < get_min_qty(task):
        return "skip_min"
    price_f = fp(task, sell_price)
    Log(f"[{task['symbol']}][{label}] 限价开空 @{price_f}  {contracts}张  ≈{usdt_amount:.2f}U")
    oid = exchange.CreateOrder(task["symbol"] + ".swap", "sell", price_f, contracts)
    Log(f"  → {'成功 ID=' + str(oid) if oid else '失败'}")
    return oid


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


def close_all_short_market(task):
    for attempt in range(10):
        remaining, _, _ = get_short_position(task)
        if remaining <= 0:
            Log(f"[{task['symbol']}][平空] 持仓已清空(第{attempt + 1}次确认)")
            return
        price = get_task_price(task)
        buy_p = fp(task, price * 1.005)
        fc    = fa(task, remaining)
        Log(f"[{task['symbol']}][平空] 第{attempt + 1}次: {fc}张 @{buy_p}")
        exchange.CreateOrder(task["symbol"] + ".swap", "closesell", buy_p, fc)
        Sleep(1500)
    remaining_final, _, _ = get_short_position(task)
    if remaining_final > 0:
        Log(f"[{task['symbol']}] 警告: 平空10次后仍有{remaining_final}张,请手动处理!")


# ═══════════════════════════════════════════
#  平掉所有 task(发现新合约时调用)
# ═══════════════════════════════════════════

def close_all_tasks():
    Log("[重置] 发现新下架合约,平掉所有现有持仓...")
    for sym, task in list(tasks.items()):
        if task.get("force_closed"):
            continue
        try:
            cancel_all_orders(task)
            Sleep(300)
            update_task_price(task)
            close_all_short_market(task)
            task["force_closed"] = True
            Log(f"[重置] {sym} 已平仓")
        except Exception as e:
            Log(f"[重置] {sym} 平仓异常: {e}")
    tasks.clear()
    Log("[重置] 所有持仓已清空,释放资金")


# ═══════════════════════════════════════════
#  底仓
# ═══════════════════════════════════════════

def open_base_short(task):
    """开底仓,失败返回 False"""
    usdt_amount = task["fund_per_task"] * BASE_SHORT_RATIO * LEVERAGE
    oid         = place_market_short(task, usdt_amount, label="底仓开空")
    Sleep(1000)
    amt, _, _   = get_short_position(task)
    task["base_short_qty"] = amt
    if amt <= 0:
        Log(f"[{task['symbol']}] 底仓开空失败!跳过该合约")
        return False
    Log(f"[{task['symbol']}] 底仓开空完成,持仓={amt}张")
    return True


# ═══════════════════════════════════════════
#  网格计算(按 task)
# ═══════════════════════════════════════════

def calc_grid_prices(task, r_high, r_low):
    step   = (r_high - r_low) / GRID_COUNT
    prices = [fp(task, r_low + i * step) for i in range(GRID_COUNT + 1)]
    prices[-1] = fp(task, r_high)
    return prices


def calc_grid_usdt(task):
    available = task["fund_per_task"] * (1 - BASE_SHORT_RATIO) * 0.8
    total     = available * LEVERAGE
    per       = total / GRID_COUNT
    Log(f"[{task['symbol']}] 网格资金: 可用={available:.2f}U  总仓位={total:.2f}U  每格={per:.2f}U")
    return [round(per, 4)] * GRID_COUNT


# ═══════════════════════════════════════════
#  格子操作(按 task)
# ═══════════════════════════════════════════

def _place_grid_sell(task, i):
    g      = task["grids"][i]
    result = place_limit_short(
        task, task["grid_prices"][i + 1], task["grid_usdt"][i],
        label=f"格{i} 开空@{task['grid_prices'][i + 1]}"
    )
    if result == "skip_min":
        g["status"] = "skip_min"
    elif result:
        g["sell_oid"] = result
        g["status"]   = "pending_sell"
    else:
        g["status"]   = "empty"


def _place_grid_cover(task, i, contracts):
    g   = task["grids"][i]
    oid = place_limit_cover(
        task, task["grid_prices"][i], contracts,
        label=f"格{i} 平空@{task['grid_prices'][i]}"
    )
    if oid:
        g["cover_oid"]      = oid
        g["sell_contracts"] = contracts
        g["status"]         = "pending_cover"
    else:
        g["status"]         = "holding_no_cover"
        g["sell_contracts"] = contracts


# ═══════════════════════════════════════════
#  激活网格(按 task)
# ═══════════════════════════════════════════

def activate_grids(task, r_high, r_low):
    task["range_high"]  = r_high
    task["range_low"]   = r_low
    task["grid_width"]  = round(r_high - r_low, 8)
    task["grid_prices"] = calc_grid_prices(task, r_high, r_low)
    task["grid_usdt"]   = calc_grid_usdt(task)

    Log(f"[{task['symbol']}] 网格激活: 上沿={r_high}  下沿={r_low}  格数={GRID_COUNT}")
    for i in range(GRID_COUNT):
        Log(f"  格{i}: 开空@{task['grid_prices'][i+1]} → 平空@{task['grid_prices'][i]}  {task['grid_usdt'][i]:.2f}U")

    task["grids"] = [
        {"sell_oid": None, "cover_oid": None, "status": "empty",
         "sell_contracts": 0, "filled_price": 0}
        for _ in range(GRID_COUNT)
    ]

    price = get_task_price(task)
    for i in range(GRID_COUNT):
        # 修复3:所有开空价 >= 当前价的格子全部挂单
        # 下跌行情中价格可能反弹超过上沿,需要提前挂好
        if task["grid_prices"][i + 1] >= price:
            _place_grid_sell(task, i)
        else:
            task["grids"][i]["status"] = "skip_below"


# ═══════════════════════════════════════════
#  区间动态移动(按 task)
# ═══════════════════════════════════════════

def check_and_shift(task):
    price      = get_task_price(task)
    grid_width = task.get("grid_width", round(task["range_high"] - task["range_low"], 8))

    # 防止小价格币种步长为0
    shift_step = price * SHIFT_STEP_PCT
    min_step   = 10 ** (-task["assets"]["PricePrecision"])
    shift_step = max(shift_step, min_step)

    shifted   = False
    direction = ""

    if price < task["range_low"]:
        steps = 0
        while price < task["range_low"]:
            task["range_high"] = fp(task, task["range_high"] - shift_step)
            task["range_low"]  = fp(task, task["range_high"] - grid_width)
            steps += 1
            if steps > 200:
                break
        direction = f"下移{steps}步"
        shifted   = True

    elif price > task["range_high"]:
        steps = 0
        while price > task["range_high"]:
            task["range_high"] = fp(task, task["range_high"] + shift_step)
            task["range_low"]  = fp(task, task["range_high"] - grid_width)
            steps += 1
            if steps > 200:
                break
        direction = f"上移{steps}步"
        shifted   = True

    if not shifted:
        return False

    task["shift_count"] += 1
    task["shift_history"].append({
        "n":         task["shift_count"],
        "direction": direction,
        "price":     round(price, 8),
        "new_high":  task["range_high"],
        "new_low":   task["range_low"],
    })

    Log(f"[{task['symbol']}] ★ 区间{direction}  新区间: {task['range_low']} ~ {task['range_high']}  触发价={price}")

    # 修复5:移动前记录所有持仓中的合约数
    holding_contracts = sum(
        g.get("sell_contracts", 0)
        for g in task["grids"]
        if g["status"] in ("pending_cover", "holding_no_cover")
    )

    cancel_all_orders(task)
    Sleep(500)
    activate_grids(task, task["range_high"], task["range_low"])

    # 修复5:移动后把原有网格持仓在新区间最低格重新挂平空保护
    if holding_contracts > 0:
        fc = fa(task, holding_contracts)
        Log(f"[{task['symbol']}] 区间移动后原持仓{fc}张重新挂平空保护")
        if fc > 0:
            _place_grid_cover(task, 0, fc)

    return True


# ═══════════════════════════════════════════
#  网格状态机(按 task)
# ═══════════════════════════════════════════

def sync_grid_orders(task):
    price  = get_task_price(task)
    grids  = task["grids"]
    ct_val = task["assets"]["ctVal"]

    for i in range(GRID_COUNT):
        g = grids[i]

        if g["status"] == "pending_sell":
            st, deal, avg_px = check_order_status(g["sell_oid"])
            if st == "filled":
                Log(f"[{task['symbol']}] 格{i} 成交确认 DealAmount={deal}  AvgPrice={avg_px}")
                contracts = fa(task, deal) if deal > 0 else fa(
                    task, usdt_to_contracts(task, task["grid_usdt"][i], task["grid_prices"][i + 1])
                )
                filled_px = avg_px if avg_px > 0 else task["grid_prices"][i + 1]
                Log(f"[{task['symbol']}] 格{i} 开空成交 均价={filled_px}  {contracts}张 → 挂平空")
                g["sell_oid"]       = None
                g["sell_contracts"] = contracts
                g["filled_price"]   = filled_px
                _place_grid_cover(task, i, contracts)
            elif st == "cancelled":
                g["sell_oid"] = None
                g["status"]   = "empty"

        elif g["status"] == "pending_cover":
            st, deal, avg_px = check_order_status(g["cover_oid"])
            if st == "filled":
                filled_px = avg_px if avg_px > 0 else task["grid_prices"][i]
                profit    = g["sell_contracts"] * ct_val * (g["filled_price"] - filled_px)
                Log(f"[{task['symbol']}] 格{i} 平空成交 均价={filled_px}  盈利≈{profit:.4f}U → 重挂开空")
                g["cover_oid"]      = None
                g["sell_contracts"] = 0
                g["filled_price"]   = 0
                g["status"]         = "empty"
                _place_grid_sell(task, i)
            elif st == "cancelled":
                g["cover_oid"] = None
                contracts      = g.get("sell_contracts", 0)
                if contracts > 0:
                    _place_grid_cover(task, i, contracts)
                else:
                    g["status"] = "empty"

        elif g["status"] in ("empty", "skip_below", "skip_min"):
            # 修复3:所有开空价 >= 当前价的格子都可以挂单
            if task["grid_prices"][i + 1] >= price:
                _place_grid_sell(task, i)

        elif g["status"] == "holding_no_cover":
            contracts = g.get("sell_contracts", 0)
            if contracts > 0:
                _place_grid_cover(task, i, contracts)
            else:
                g["status"] = "empty"


# ═══════════════════════════════════════════
#  强制平仓(按 task)
# ═══════════════════════════════════════════

def check_force_close(task):
    if task.get("force_closed"):
        return True
    now_ms    = get_now_ms()
    remaining = (task["delist_time_ms"] - now_ms) / 1000 / 60
    if remaining <= FORCE_CLOSE_MINS:
        Log(f"[{task['symbol']}] ★ 距下架仅剩 {remaining:.1f} 分钟,触发强制平仓!")
        cancel_all_orders(task)
        Sleep(500)
        update_task_price(task)
        close_all_short_market(task)
        task["force_closed"] = True
        return True
    return False


# ═══════════════════════════════════════════
#  初始化单个 task
# ═══════════════════════════════════════════

def init_task(binance_symbol, delist_time_ms, fund_per_task):
    base       = binance_symbol.replace("USDT", "")
    fmz_symbol = f"{base}_USDT"
    swapcode   = fmz_symbol + ".swap"

    Log(f"[初始化] {fmz_symbol}  下架时间={delist_time_ms}  分配资金={fund_per_task:.2f}U")

    data = exchange.GetMarkets().get(swapcode)
    if not data:
        Log(f"[初始化] 无法获取市场信息: {swapcode},跳过")
        return None

    task = {
        "symbol":         fmz_symbol,
        "base_currency":  base,
        "delist_time_ms": delist_time_ms,
        "fund_per_task":  fund_per_task,
        "base_price":     0.0,
        "base_short_qty": 0.0,
        "range_high":     0.0,
        "range_low":      0.0,
        "grid_width":     0.0,
        "grids":          [],
        "grid_prices":    [],
        "grid_usdt":      [],
        "shift_count":    0,
        "shift_history":  [],
        "force_closed":   False,
        "assets": {
            "price":             0,
            "amount":            0,
            "hold_price":        0,
            "unrealised_profit": 0,
            "AmountPrecision":   data["AmountPrecision"],
            "PricePrecision":    data["PricePrecision"],
            "MinQty":            data["MinQty"],
            "ctVal":             data["CtVal"],
        }
    }

    update_task_price(task)
    task["base_price"] = get_task_price(task)

    # 修复4:底仓失败则跳过该合约
    if not open_base_short(task):
        return None
    Sleep(500)

    price  = get_task_price(task)
    r_high = fp(task, price)
    r_low  = fp(task, price * (1 - GRID_WIDTH_PCT))
    activate_grids(task, r_high, r_low)

    Log(f"[{fmz_symbol}] 初始化完成  底仓={task['base_short_qty']}张  网格: {r_low}~{r_high}")
    return task


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

def refresh_tables():
    try:
        now_ms = get_now_ms()
        equity = assets_global["USDT"].get("equity", 0)
        profit = round(equity - INIT_FUNDING, 4)
        ppct   = round(profit / INIT_FUNDING * 100, 2) if INIT_FUNDING else 0

        task_rows = []
        for sym, task in tasks.items():
            price     = get_task_price(task)
            amt       = task["assets"].get("amount", 0)
            unreal    = task["assets"].get("unrealised_profit", 0)
            remaining = max(0, (task["delist_time_ms"] - now_ms) / 1000 / 60)
            status    = "已平仓" if task.get("force_closed") else f"运行中 ({remaining:.0f}分)"
            task_rows.append([
                sym,
                f"{price}",
                f"{task['base_price']}",
                f"{amt}张",
                f"{unreal:.4f}U",
                f"{task['range_low']} ~ {task['range_high']}",
                f"{task['shift_count']}次",
                status,
            ])

        if not task_rows:
            task_rows = [["等待下架合约...", "-", "-", "-", "-", "-", "-", "-"]]

        overview = {
            "type":  "table",
            "title": f"下架合约空头网格 | 权益={equity:.2f}U | 总盈亏={profit:+.4f}U ({ppct:+.2f}%)",
            "cols":  ["合约", "当前价", "基准价", "持仓", "未实现盈亏", "网格区间", "移动次数", "状态"],
            "rows":  task_rows
        }

        detail_parts = ["`" + json.dumps(overview, ensure_ascii=False) + "`"]

        for sym, task in tasks.items():
            if task.get("force_closed"):
                continue
            price     = get_task_price(task)
            ct_val    = task["assets"].get("ctVal", 1)
            remaining = max(0, (task["delist_time_ms"] - now_ms) / 1000 / 60)
            grid_rows = []

            for i, g in enumerate(task["grids"]):
                sell_p    = task["grid_prices"][i + 1] if i + 1 < len(task["grid_prices"]) else 0
                cover_p   = task["grid_prices"][i]     if i     < len(task["grid_prices"]) else 0
                contracts = g.get("sell_contracts", 0)
                filled_px = g.get("filled_price", 0)
                status_cn = {
                    "empty":            "⬜ 等待",
                    "pending_sell":     "🔴 挂单开空",
                    "pending_cover":    "🟢 持仓平空中",
                    "skip_below":       "⬇️ 价格已过",
                    "skip_min":         "⛔ 不足最小量",
                    "holding_no_cover": "⚠️ 待挂平空单",
                }.get(g["status"], g["status"])
                pnl_str = f"{contracts * ct_val * (filled_px - price):+.4f}U" \
                    if contracts > 0 and filled_px > 0 else "-"
                grid_rows.append([
                    f"格{i}", f"{sell_p}", f"{cover_p}",
                    f"{contracts}张" if contracts > 0 else "-",
                    f"{filled_px}"   if filled_px > 0  else "-",
                    pnl_str, status_cn,
                ])

            grid_tbl = {
                "type":  "table",
                "title": f"{sym} | 倒计时{remaining:.0f}分 | 上沿={task['range_high']} 下沿={task['range_low']}",
                "cols":  ["格子", "开空价", "平空价", "持仓量", "开仓价", "浮动盈亏", "状态"],
                "rows":  grid_rows
            }
            detail_parts.append("`" + json.dumps(grid_tbl, ensure_ascii=False) + "`")

        LogProfit(profit, "&")
        LogStatus("\n".join(detail_parts))

    except Exception as e:
        Log(f"状态面板异常: {e}")


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

def main():
    global INIT_FUNDING, Funding, tasks, known_delist_set

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

    Log("═" * 60)
    Log("下架合约空头网格策略启动")
    Log("═" * 60)

    # SetMarginLevel 只设置一次
    exchange.SetMarginLevel(LEVERAGE)

    while not update_global_account():
        Log("等待账户数据...")
        Sleep(3000)
    INIT_FUNDING = assets_global["USDT"]["equity"]
    Funding      = INIT_FUNDING
    Log(f"初始资金={Funding:.2f}U")

    last_monitor_ms = 0

    while True:
        now_ms = get_now_ms()

        # ── 监控:每15秒检查一次 ──────────────────────────
        if now_ms - last_monitor_ms >= MONITOR_INTERVAL:
            last_monitor_ms = now_ms
            delist_map      = fetch_delist_symbols()
            current_set     = set(delist_map.keys())

            # 发现新合约(当前批次未知的)
            new_symbols = current_set - known_delist_set

            if new_symbols:
                Log(f"[监控] 发现新下架合约: {new_symbols}")

                # 1. 平掉所有现有持仓,释放资金
                if tasks:
                    close_all_tasks()
                    Sleep(2000)

                # 2. 刷新账户,对当前所有下架合约重新建网格
                total_new = len(current_set)
                Log(f"[监控] 共{total_new}个下架合约,开始初始化...")

                for idx, (binance_sym, delist_ms) in enumerate(delist_map.items()):
                    # 每次初始化前重新查可用余额,按剩余数量动态分配
                    update_global_account()
                    remaining_count = total_new - idx
                    available_now   = (assets_global["USDT"]["total_balance"]
                                       - assets_global["USDT"]["margin_used"])
                    fund_per_task   = available_now * 0.8 / remaining_count
                    Log(f"[监控] {binance_sym} 剩余{remaining_count}个待初始化  "
                        f"可用={available_now:.2f}U  分配={fund_per_task:.2f}U")

                    task = init_task(binance_sym, delist_ms, fund_per_task)
                    if task:
                        tasks[binance_sym] = task

                # 3. 更新已知集合为当前完整集合
                known_delist_set = current_set

        # ── 网格运行:遍历所有 task ──────────────────────
        update_global_account()

        for sym, task in list(tasks.items()):
            if check_force_close(task):
                continue
            update_task_price(task)
            update_task_position(task)
            try:
                check_and_shift(task)
            except Exception as e:
                Log(f"[{sym}] 区间移动异常: {e}")
            try:
                sync_grid_orders(task)
            except Exception as e:
                Log(f"[{sym}] 网格同步异常: {e}")

        refresh_tables()

        # 所有合约强制平仓后清空,等待下一批新合约
        if tasks and all(t.get("force_closed") for t in tasks.values()):
            Log("所有下架合约已完成平仓,等待下一批...")
            tasks.clear()
            known_delist_set = set()

        Sleep(POLL_INTERVAL)