在阅读和使用本策略前,请务必注意以下三点:
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)