Polymarket BTC 15分钟 两腿对冲套利机器人 (双向对冲版)


创建日期: 2026-02-25 16:57:12 最后修改: 2026-02-25 17:42:59
复制: 4 点击次数: 41
avatar of ianzeng123 ianzeng123
2
关注
399
关注者
策略源码
// ============================================================
// Polymarket BTC 15分钟 两腿对冲套利机器人 v5
// 策略灵感:@the_smart_ape
// 平台:FMZ(发明者量化)托管者版本 >= 3.8.8
// ============================================================

var STATE = {
    WATCHING:      "WATCHING",
    BOTH_PENDING:  "BOTH_PENDING",   // 两腿挂单中
    LEG1_ONLY:     "LEG1_ONLY",      // 仅第一腿成交,第二腿撤销后风控
    LEG2_ONLY:     "LEG2_ONLY",      // 仅第二腿成交(异常),第一腿撤销后风控
    BOTH_DONE:     "BOTH_DONE",      // 两腿全部成交,套利锁定
    CLOSING:       "CLOSING",        // 平仓中
    DONE:          "DONE"
}

var state         = STATE.WATCHING
var currentPeriod = 0
var leg1Side      = null      // 暴跌方向,第一腿方向
var leg1OrderId   = null      // 第一腿挂单 orderId
var leg2OrderId   = null      // 第二腿挂单 orderId
var leg1EntryAsk  = 0         // 第一腿成交均价
var leg2EntryAsk  = 0         // 第二腿成交均价
var leg1Price     = 0         // 第一腿挂单价格(含滑价)
var leg2Price     = 0         // 第二腿挂单价格
var priceHistory  = []
var initialEquity = 0
var redeemDone    = false

// 平仓上下文:记录需要平的仓位方向
var closingSide   = null

// ===================== Symbol 工具 =====================

function getCurrentPeriod() {
    var now = Math.floor(Date.now() / 1000)
    return Math.floor(now / INTERVAL) * INTERVAL
}

function getSymbols(period) {
    var slug = "btc-updown-15m-" + period
    return {
        up:   slug + "_USDC.Up",
        down: slug + "_USDC.Down"
    }
}

function getElapsed(period) {
    return Math.floor(Date.now() / 1000) - period
}

// ===================== 行情工具 =====================

function getTicker(symbol) {
    try {
        var depth = exchange.GetDepth(symbol)
        if (!depth || !depth.Asks || !depth.Bids ||
            depth.Asks.length === 0 || depth.Bids.length === 0) {
            return null
        }
        return {
            Sell: depth.Asks[0].Price,
            Buy:  depth.Bids[0].Price
        }
    } catch(e) {
        return null
    }
}

function getPositionAmount(symbol) {
    try {
        var positions = exchange.GetPositions()
        for (var i = 0; i < positions.length; i++) {
            if (positions[i].Symbol === symbol) {
                return positions[i].Amount
            }
        }
    } catch(e) {
        Log("GetPositions 异常:", e)
    }
    return 0
}

// ===================== 订单查询工具 =====================

// 查询单个订单状态,返回 order 对象或 null
function queryOrder(orderId) {
    try {
        return exchange.GetOrder(orderId)
    } catch(e) {
        Log("GetOrder 异常 orderId:", orderId, e)
        return null
    }
}

// 判断订单是否已完全成交(Status === 1)
function isOrderFilled(orderId) {
    var o = queryOrder(orderId)
    return o && o.Status === 1
}

// 判断订单是否已取消(Status === 2 或 4)
function isOrderCancelled(orderId) {
    var o = queryOrder(orderId)
    return o && (o.Status === 2 || o.Status === 4)
}

// 获取订单成交均价,未成交返回 0
function getOrderAvgPrice(orderId) {
    var o = queryOrder(orderId)
    if (o && o.Status === 1) return o.AvgPrice
    return 0
}

// ===================== 撤单并确认 =====================
// 发出撤单指令,然后轮询直到状态明确
// 返回值:
//   "cancelled"  → 成功撤销
//   "filled"     → 撤单时已成交(视为成交处理)
//   "unknown"    → 超时仍不明确(极少发生)

function cancelAndConfirm(orderId) {
    Log("🗑 撤单 | orderId:", orderId)
    try {
        exchange.CancelOrder(orderId)
    } catch(e) {
        Log("   ⚠️ 撤单指令异常(忽略,继续查询):", e)
    }

    var deadline = Date.now() + ORDER_TIMEOUT_S * 1000
    while (Date.now() < deadline) {
        var o = queryOrder(orderId)
        if (o) {
            if (o.Status === 1) {
                Log("   ⚠️ 撤单时订单已成交 | 均价:", o.AvgPrice)
                return "filled"
            }
            if (o.Status === 2 || o.Status === 4) {
                Log("   ✅ 撤单确认成功")
                return "cancelled"
            }
            Log("   轮询中... Status:", o.Status, "| DealAmount:", o.DealAmount)
        } else {
            Log("   GetOrder 返回空,继续等待...")
        }
        Sleep(1000)
    }
    Log("   ⚠️ 撤单确认超时,状态未知")
    return "unknown"
}


// ===================== 撤单并确认(阻塞重试版)=====================

function cancelAndConfirmUntilClear(orderId) {
    while (true) {
        var result = cancelAndConfirm(orderId)
        if (result === "cancelled" || result === "filled") {
            return result
        }
        // unknown:继续重试撤单
        Log("   ⚠️ 撤单状态未知,重新发送撤单指令并继续等待...")
        Sleep(2000)
    }
}

// ===================== 下单并等待确认 =====================

function placeOrderAndConfirm(symbol, side, price, amount) {
    var orderPrice = price
    if (side === "buy" && price > 0) {
        orderPrice = _N(price + SLIPPAGE, 4)
        if (orderPrice > 1) orderPrice = 1
    }

    Log("📋 下单 | " + side + " " + symbol,
        "| 原价:", price, "| 实际价:", orderPrice,
        "| 份额:", amount)

    var orderId = exchange.CreateOrder(symbol, side, orderPrice, amount)
    if (!orderId) {
        Log("❌ 下单失败,未获取到 orderId")
        return { orderId: null, avgPrice: null }
    }
    Log("   orderId:", orderId, "| 开始轮询确认(超时:", ORDER_TIMEOUT_S + "s)")

    var deadline = Date.now() + ORDER_TIMEOUT_S * 1000
    while (Date.now() < deadline) {
        try {
            var order = exchange.GetOrder(orderId)
            if (!order) {
                Log("   ⚠️ GetOrder 返回空,继续等待...")
            } else {
                Log("   状态:", order.Status,
                    "| 已成交:", order.DealAmount,
                    "| 总量:", order.Amount,
                    "| 均价:", order.AvgPrice)
                if (order.Status === 1) {
                    Log("   ✅ 订单完全成交 | 均价:", order.AvgPrice)
                    return { orderId: orderId, avgPrice: order.AvgPrice }
                }
                if (order.Status === 2 || order.Status === 4) {
                    Log("   ❌ 订单已撤销或失败,Status:", order.Status)
                    return { orderId: orderId, avgPrice: null }
                }
            }
        } catch(e) {
            Log("   ⚠️ GetOrder 异常:", e)
        }
        Sleep(1000)
    }

    Log("   ⏰ 超时未成交,执行撤单")
    try { exchange.CancelOrder(orderId) } catch(e) { Log("   ⚠️ 撤单异常:", e) }

    while (true) {
        try {
            var finalOrder = exchange.GetOrder(orderId)
            if (finalOrder) {
                if (finalOrder.Status === 1) {
                    Log("   ⚠️ 撤单失败但订单已成交 | 均价:", finalOrder.AvgPrice)
                    return { orderId: orderId, avgPrice: finalOrder.AvgPrice }
                }
                if (finalOrder.Status === 2 || finalOrder.Status === 4) {
                    Log("   ✅ 订单已取消")
                    return { orderId: orderId, avgPrice: null }
                }
            }
        } catch(e) {
            Log("   ⚠️ 撤单后查询异常:", e)
        }
        Sleep(1000)
    }
}

// ===================== Redeem 处理 =====================

function doRedeem() {
    try {
        var positions = exchange.GetPositions()
        var count = 0
        for (var i = 0; i < positions.length; i++) {
            var pos = positions[i]
            if (pos.Info && pos.Info.redeemable) {
                Log("💰 Redeem | Symbol:", pos.Symbol,
                    "| 数量:", pos.Amount,
                    "| eventSlug:", pos.Info.eventSlug)
                var result = exchange.IO("redeem", pos.Symbol, true)
                Log("Redeem 结果:", result)
                count++
                Sleep(300)
            }
        }
        if (count === 0) {
            Log("📭 无可 redeem 持仓")
        } else {
            Log("✅ 共 redeem", count, "个持仓")
        }
    } catch(e) {
        Log("Redeem 异常:", e)
    }
}

// ===================== 暴跌检测 =====================

function detectDump(upAsk, downAsk) {
    var nowTs  = Date.now()
    var cutoff = nowTs - DUMP_WINDOW_S * 1000
    priceHistory = priceHistory.filter(function(h) { return h.ts >= cutoff })

    if (priceHistory.length === 0) return null

    var oldest   = priceHistory[0]
    var upDrop   = (oldest.upAsk   - upAsk)   / oldest.upAsk
    var downDrop = (oldest.downAsk - downAsk) / oldest.downAsk

    if (upDrop >= MOVE_PCT) {
        Log("🔻 Up 暴跌 " + (upDrop * 100).toFixed(2) + "%",
            oldest.upAsk.toFixed(4), "→", upAsk.toFixed(4))
        return "Up"
    }
    if (downDrop >= MOVE_PCT) {
        Log("🔻 Down 暴跌 " + (downDrop * 100).toFixed(2) + "%",
            oldest.downAsk.toFixed(4), "→", downAsk.toFixed(4))
        return "Down"
    }
    return null
}

// ===================== 核心:并发下两腿 =====================
//
// 第一腿:买暴跌方向,价格 = dumpAsk + SLIPPAGE
// 第二腿:买对立方向,限价 = SUM_TARGET - leg1Price
//         含义:两腿合计 <= SUM_TARGET 即锁定利润
//
// 使用 exchange.Go 并发提交,再分别确认结果

function executeBothLegs(symbols, dumpSide, dumpAsk) {
    if (SHARES < 5) {
        Log("❌ SHARES 不能低于5份,当前:", SHARES)
        return false
    }

    var leg1Symbol = (dumpSide === "Up") ? symbols.up   : symbols.down
    var leg2Symbol = (dumpSide === "Up") ? symbols.down : symbols.up

    leg1Price = _N(dumpAsk + SLIPPAGE, 4)
    if (leg1Price > 1) leg1Price = 1

    // 第二腿限价:SUM_TARGET - leg1Price,确保两腿合计 <= SUM_TARGET 即有利润
    leg2Price = _N(SUM_TARGET - leg1Price, 4)
    if (leg2Price <= 0 || leg2Price > 1) {
        Log("❌ 第二腿限价无效:", leg2Price, "| leg1Price:", leg1Price, "| SUM_TARGET:", SUM_TARGET, "| 放弃")
        return false
    }

    Log("🚀 并发下两腿 | 方向:", dumpSide,
        "| 份额:", SHARES,
        "| 第一腿(", leg1Symbol, ")限价:", leg1Price,
        "| 第二腿(", leg2Symbol, ")限价:", leg2Price,
        "| 两腿合计:", _N(leg1Price + leg2Price, 4), "/", SUM_TARGET,
        "| 锁定利润下限:", _N((1 - leg1Price - leg2Price) * SHARES, 4), "USDC")

    // 并发提交两个限价单
    var goLeg1 = exchange.Go("CreateOrder", leg1Symbol, "buy", leg1Price, SHARES)
    var goLeg2 = exchange.Go("CreateOrder", leg2Symbol, "buy", leg2Price, SHARES)

    // 等待两个 Go 任务返回 orderId
    var id1 = goLeg1.wait()
    var id2 = goLeg2.wait()

    Log("   第一腿 orderId:", id1, "| 第二腿 orderId:", id2)

    leg1Side     = dumpSide
    leg1OrderId  = id1
    leg2OrderId  = id2
    state        = STATE.BOTH_PENDING
    priceHistory = []

    Log("✅ 两腿已提交 | 进入 BOTH_PENDING,等待 Status 确认")
    Log("   风控参考 → 保底价:", FLOOR_PRICE,
        "| 前期止盈:", (EARLY_TAKE_PROFIT * 100) + "%",
        "| 末段止损:", (LAST_MIN_STOP_LOSS * 100) + "%")
    return true
}

// ===================== BOTH_PENDING 轮询 =====================
// 每个 tick 检查两个挂单的成交状态,更新 state

function pollBothPending(symbols) {
    var o1 = leg1OrderId ? queryOrder(leg1OrderId) : null
    var o2 = leg2OrderId ? queryOrder(leg2OrderId) : null

    var filled1  = o1 && o1.Status === 1
    var filled2  = o2 && o2.Status === 1
    var cancel1  = o1 && (o1.Status === 2 || o1.Status === 4)
    var cancel2  = o2 && (o2.Status === 2 || o2.Status === 4)
    var pending1 = o1 && (o1.Status === 0 || o1.Status === 3)
    var pending2 = o2 && (o2.Status === 0 || o2.Status === 3)

    Log("📡 BOTH_PENDING 轮询",
        "| 腿1 Status:", o1 ? o1.Status : "null",
        "| 腿2 Status:", o2 ? o2.Status : "null")

    if (filled1 && filled2) {
        leg1EntryAsk = o1.AvgPrice
        leg2EntryAsk = o2.AvgPrice
        state = STATE.BOTH_DONE
        Log("🎉 两腿全部成交!",
            "| 第一腿均价:", leg1EntryAsk.toFixed(4),
            "| 第二腿均价:", leg2EntryAsk.toFixed(4),
            "| 真实合计:", _N(leg1EntryAsk + leg2EntryAsk, 4),
            "| 锁定利润:", _N((1 - leg1EntryAsk - leg2EntryAsk) * SHARES, 4), "USDC")
        return
    }

    if (filled1 && !filled2) {
        // 第一腿成交,第二腿未成交(pending 或 cancel 或 null)
        leg1EntryAsk = o1.AvgPrice
        Log("🟡 第一腿已成交 | 均价:", leg1EntryAsk.toFixed(4),
            "| 第二腿 Status:", o2 ? o2.Status : "null", "→ 进入 LEG1_ONLY 风控")
        state = STATE.LEG1_ONLY
        return
    }

    if (filled2 && !filled1) {
        // 第二腿成交,第一腿未成交
        leg2EntryAsk = o2.AvgPrice
        Log("🟠 第二腿已成交 | 均价:", leg2EntryAsk.toFixed(4),
            "| 第一腿 Status:", o1 ? o1.Status : "null", "→ 进入 LEG2_ONLY 风控")
        state = STATE.LEG2_ONLY
        return
    }

    if (cancel1 && cancel2) {
        Log("📭 两腿均已取消,本轮结束")
        state = STATE.DONE
        return
    }

    if (cancel1 && !filled2) {
        Log("⚠️ 第一腿已取消,撤销第二腿")
        if (leg2OrderId) {
            var r2 = cancelAndConfirmUntilClear(leg2OrderId)
            if (r2 === "filled") {
                // 撤单瞬间第二腿成交,记录持仓,进入单腿风控
                var o2f = queryOrder(leg2OrderId)
                if (o2f) leg2EntryAsk = o2f.AvgPrice
                Log("⚠️ 撤单时第二腿已成交 | 均价:", leg2EntryAsk.toFixed(4), "→ LEG2_ONLY")
                state = STATE.LEG2_ONLY
                return
            }
        }
        state = STATE.DONE
        return
    }

    if (cancel2 && !filled1) {
        Log("⚠️ 第二腿已取消,撤销第一腿")
        if (leg1OrderId) {
            var r1 = cancelAndConfirmUntilClear(leg1OrderId)
            if (r1 === "filled") {
                // 撤单瞬间第一腿成交,记录持仓,进入单腿风控
                var o1f = queryOrder(leg1OrderId)
                if (o1f) leg1EntryAsk = o1f.AvgPrice
                Log("⚠️ 撤单时第一腿已成交 | 均价:", leg1EntryAsk.toFixed(4), "→ LEG1_ONLY")
                state = STATE.LEG1_ONLY
                return
            }
        }
        state = STATE.DONE
        return
    }

    // 两腿均还在挂单中(pending),继续等待
}

// ===================== 单腿风控 =====================
// 先撤对侧挂单并确认,再决定是否平仓

function handleLeg1OnlyRisk(symbols, upBid, downBid, isLastMin) {
    if (leg2OrderId) {
        var o2check = queryOrder(leg2OrderId)
        if (o2check && o2check.Status === 1) {
            leg2EntryAsk = o2check.AvgPrice
            state = STATE.BOTH_DONE
            Log("🎉 LEG1_ONLY 发现第二腿已成交 | 均价:", leg2EntryAsk.toFixed(4), "→ BOTH_DONE")
            return
        }
    }

    var holdSymbol = (leg1Side === "Up") ? symbols.up   : symbols.down
    var holdBid    = (leg1Side === "Up") ? upBid : downBid
    var profitLine = leg1EntryAsk * (1 + EARLY_TAKE_PROFIT)
    var stopLine   = leg1EntryAsk * (1 - LAST_MIN_STOP_LOSS)

    var needClose = false
    var reason    = ""

    if (holdBid <= FLOOR_PRICE) {
        needClose = true
        reason    = "止损(保底)"
    } else if (!isLastMin && holdBid >= profitLine) {
        needClose = true
        reason    = "前期止盈"
    } else if (isLastMin && holdBid <= stopLine) {
        needClose = true
        reason    = "末段止损"
    }

    if (!needClose) return

    Log("⚡ 触发风控(LEG1_ONLY) | 原因:", reason,
        "| Bid:", holdBid.toFixed(4),
        "| 先撤第二腿 orderId:", leg2OrderId)

    if (leg2OrderId) {
        var o2pre = queryOrder(leg2OrderId)
        var o2status = o2pre ? o2pre.Status : -1

        if (o2status === 1) {
            // 已成交 → 两腿全成交,不平仓
            leg2EntryAsk = o2pre.AvgPrice
            state = STATE.BOTH_DONE
            Log("🎉 风控前查到第二腿已成交,视为两腿全成交 | 均价:", leg2EntryAsk.toFixed(4))
            return
        } else if (o2status === 2 || o2status === 4) {
            // 已取消 → 跳过撤单,直接平仓
            Log("   ℹ️ 第二腿已是取消态,跳过撤单")
        } else {
            // 挂单中 → 正常撤单
            var cancelResult = cancelAndConfirmUntilClear(leg2OrderId)
            if (cancelResult === "filled") {
                var o2 = queryOrder(leg2OrderId)
                leg2EntryAsk = o2 ? o2.AvgPrice : leg2Price
                state = STATE.BOTH_DONE
                Log("🎉 撤单时第二腿已成交,视为两腿全成交 | 第二腿均价:", leg2EntryAsk.toFixed(4))
                return
            }
        }
    } else {
        Log("   ℹ️ 第二腿 orderId 为 null,跳过撤单")
    }

    // Step2:撤单确认后,平第一腿
    Log("   ✅ 第二腿处理完成,开始平第一腿 | 方向:", reason)
    closingSide = leg1Side
    state = STATE.CLOSING
    closePosition(holdSymbol, holdBid, reason)
}

function handleLeg2OnlyRisk(symbols, upBid, downBid, isLastMin) {
    if (leg1OrderId) {
        var o1check = queryOrder(leg1OrderId)
        if (o1check && o1check.Status === 1) {
            leg1EntryAsk = o1check.AvgPrice
            state = STATE.BOTH_DONE
            Log("🎉 LEG2_ONLY 发现第一腿已成交 | 均价:", leg1EntryAsk.toFixed(4), "→ BOTH_DONE")
            return
        }
    }

    // 第二腿方向 = 对立方向
    var oppSide    = (leg1Side === "Up") ? "Down" : "Up"
    var holdSymbol = (leg1Side === "Up") ? symbols.down : symbols.up
    var holdBid    = (leg1Side === "Up") ? downBid : upBid

    var profitLine = leg2EntryAsk * (1 + EARLY_TAKE_PROFIT)
    var stopLine   = leg2EntryAsk * (1 - LAST_MIN_STOP_LOSS)

    var needClose = false
    var reason    = ""

    if (holdBid <= FLOOR_PRICE) {
        needClose = true
        reason    = "止损(保底)"
    } else if (!isLastMin && holdBid >= profitLine) {
        needClose = true
        reason    = "前期止盈"
    } else if (isLastMin && holdBid <= stopLine) {
        needClose = true
        reason    = "末段止损"
    }

    if (!needClose) return

    Log("⚡ 触发风控(LEG2_ONLY) | 原因:", reason,
        "| Bid:", holdBid.toFixed(4),
        "| 先撤第一腿 orderId:", leg1OrderId)


    if (leg1OrderId) {
        var o1pre = queryOrder(leg1OrderId)
        var o1status = o1pre ? o1pre.Status : -1

        if (o1status === 1) {
            // 已成交 → 两腿全成交,不平仓
            leg1EntryAsk = o1pre.AvgPrice
            state = STATE.BOTH_DONE
            Log("🎉 风控前查到第一腿已成交,视为两腿全成交 | 均价:", leg1EntryAsk.toFixed(4))
            return
        } else if (o1status === 2 || o1status === 4) {
            // 已取消 → 跳过撤单,直接平仓
            Log("   ℹ️ 第一腿已是取消态,跳过撤单")
        } else {
            // 挂单中 → 正常撤单
            var cancelResult = cancelAndConfirmUntilClear(leg1OrderId)
            if (cancelResult === "filled") {
                var o1 = queryOrder(leg1OrderId)
                leg1EntryAsk = o1 ? o1.AvgPrice : leg1Price
                state = STATE.BOTH_DONE
                Log("🎉 撤单时第一腿已成交,视为两腿全成交 | 第一腿均价:", leg1EntryAsk.toFixed(4))
                return
            }
        }
    } else {
        Log("   ℹ️ 第一腿 orderId 为 null,跳过撤单")
    }

    // Step2:平第二腿
    Log("   ✅ 第一腿处理完成,开始平第二腿 | 方向:", reason)
    closingSide = oppSide
    state = STATE.CLOSING
    closePosition(holdSymbol, holdBid, reason)
}

// ===================== 平仓 =====================

function closePosition(symbol, bid, reason) {
    var amount = getPositionAmount(symbol)
    if (amount <= 0) {
        Log("⚠️ 无持仓可平:", symbol)
        state = STATE.DONE
        return
    }
    Log((reason.indexOf("止盈") >= 0 ? "💹" : "🛑"), reason,
        "| Bid:", bid.toFixed(4), "| 份额:", amount)

    var result = placeOrderAndConfirm(symbol, "sell", -1, amount)
    if (result.avgPrice === null) {
        Log("⚠️", reason, "未成交,保持 CLOSING 状态,下次重试")
        return
    }
    Log("   平仓均价(AvgPrice):", result.avgPrice.toFixed(4))
    state = STATE.DONE
}

// ===================== 超时挂单清理 =====================
// 接近轮次结束时,如果两腿仍有未成交挂单,全部撤销

function cancelAllPending(label) {
    Log("⚠️", label, "| 撤销所有未成交挂单")
    if (leg1OrderId) {
        var r1 = cancelAndConfirmUntilClear(leg1OrderId)
        if (r1 === "filled" && leg1EntryAsk === 0) {
            var o1 = queryOrder(leg1OrderId)
            if (o1) leg1EntryAsk = o1.AvgPrice
        }
    }
    if (leg2OrderId) {
        var r2 = cancelAndConfirmUntilClear(leg2OrderId)
        if (r2 === "filled" && leg2EntryAsk === 0) {
            var o2 = queryOrder(leg2OrderId)
            if (o2) leg2EntryAsk = o2.AvgPrice
        }
    }
    // 根据实际成交情况决定最终 state
    if (leg1EntryAsk > 0 && leg2EntryAsk > 0) {
        state = STATE.BOTH_DONE
        Log("✅ 撤单后两腿均成交,套利锁定")
    } else if (leg1EntryAsk > 0) {
        Log("🟡 撤单后仅第一腿成交,进入单腿 CLOSING")
        state = STATE.LEG1_ONLY
    } else if (leg2EntryAsk > 0) {
        Log("🟠 撤单后仅第二腿成交,进入单腿 CLOSING")
        state = STATE.LEG2_ONLY
    } else {
        state = STATE.DONE
        Log("📭 撤单后无持仓,本轮结束")
    }
}

// ===================== 状态重置 =====================

function resetState() {
    state        = STATE.WATCHING
    leg1Side     = null
    leg1OrderId  = null
    leg2OrderId  = null
    leg1EntryAsk = 0
    leg2EntryAsk = 0
    leg1Price    = 0
    leg2Price    = 0
    priceHistory = []
    redeemDone   = false
    closingSide  = null
    Log("🔄 状态重置,监控新一轮")
}

function handleStalePosition() {
    Log("⚠️ 上一轮存在未处理持仓,等待 redeem 兑付")
}


function cancelPendingOnRoundSwitch() {
    Log("🔄 轮次切换,撤销上一轮未成交挂单")
    if (leg1OrderId) {
        Log("   撤销第一腿 orderId:", leg1OrderId)
        cancelAndConfirmUntilClear(leg1OrderId)
    }
    if (leg2OrderId) {
        Log("   撤销第二腿 orderId:", leg2OrderId)
        cancelAndConfirmUntilClear(leg2OrderId)
    }
}

// ===================== 主函数 =====================

function main() {
    LogProfitReset(0)
    LogReset(0)
    Log("🤖 Polymarket BTC 15m 对冲套利机器人 v5 启动")
    Log("参数 → shares:", SHARES,
        "| sum:", SUM_TARGET,
        "| move:", (MOVE_PCT * 100) + "%",
        "| window:", WINDOW_MIN + "min")
    Log("风控 → 前期止盈:", (EARLY_TAKE_PROFIT * 100) + "%",
        "| 保底价:", FLOOR_PRICE,
        "| 最后1分钟止损:", (LAST_MIN_STOP_LOSS * 100) + "%")

    currentPeriod = getCurrentPeriod()
    Log("当前轮次:", currentPeriod,
        "|", new Date(currentPeriod * 1000).toISOString())

    var _initAcc = exchange.GetAccount()
    initialEquity = _initAcc ? _N(_initAcc.Equity, 2) : 0
    Log("初始权益(Equity):", initialEquity, "USDC")

    Log("=== 启动 Redeem 检查 ===")
    doRedeem()

    while (true) {
        var period    = getCurrentPeriod()
        var elapsed   = getElapsed(period)
        var remaining = INTERVAL - elapsed

        // ---- 新一轮切换 ----
        if (period !== currentPeriod) {
            Log("🕐 新一轮开始:", period)

            if (state === STATE.BOTH_PENDING) {
                cancelPendingOnRoundSwitch()
            } else if (state === STATE.LEG1_ONLY  ||
                       state === STATE.LEG2_ONLY  ||
                       state === STATE.CLOSING) {
                handleStalePosition()
            }
            currentPeriod = period
            resetState()
        }

        var symbols    = getSymbols(currentPeriod)
        var upTicker   = getTicker(symbols.up)
        var downTicker = getTicker(symbols.down)

        if (!upTicker || !downTicker) {
            Sleep(SLEEP_MS)
            continue
        }

        var upAsk   = upTicker.Sell
        var downAsk = downTicker.Sell
        var upBid   = upTicker.Buy
        var downBid = downTicker.Buy

        var isLastMin = (remaining <= LAST_MIN_S)

        // ===================== LogStatus 面板 =====================
        var acc        = exchange.GetAccount()
        var curEquity  = acc ? _N(acc.Equity,  2) : 0
        var curBalance = acc ? _N(acc.Balance, 2) : 0
        var pnl        = _N(curEquity - initialEquity, 2)
        var pnlPct     = initialEquity > 0 ? _N((pnl / initialEquity) * 100, 2) : 0
        var pnlDisplay = (pnl >= 0 ? "+" : "") + pnl + " (" + (pnlPct >= 0 ? "+" : "") + pnlPct + "%)"

        var phase = isLastMin                      ? "LAST-1MIN" :
                    elapsed <= WINDOW_MIN * 60     ? "WATCHING"  : "WAITING"

        // ---- 表格1:账户资金 ----
        var accountTable = {
            type: "table",
            title: "💰 账户资金 | Round: " + new Date(currentPeriod * 1000).toISOString().slice(0,19) + " UTC",
            cols: ["初始权益 (USDC)", "当前权益 (USDC)", "可用余额 (USDC)", "总盈亏 (USDC)"],
            rows: [[initialEquity.toString(), curEquity.toString(), curBalance.toString(), pnlDisplay]]
        }

        // ---- 表格2:策略状态 ----
        var stateLabel
        switch(state) {
            case STATE.WATCHING:
                stateLabel = elapsed > WINDOW_MIN * 60 ? "⏳ 等待下一轮 (窗口已关闭)" : "👁 监控中 (WATCHING)"
                break
            case STATE.BOTH_PENDING:
                stateLabel = "⏳ 两腿挂单中 (BOTH_PENDING)"
                break
            case STATE.LEG1_ONLY:
                stateLabel = "🟡 仅第一腿成交 (LEG1_ONLY)"
                break
            case STATE.LEG2_ONLY:
                stateLabel = "🟠 仅第二腿成交 (LEG2_ONLY)"
                break
            case STATE.BOTH_DONE:
                stateLabel = "✅ 两腿套利锁定 (BOTH_DONE)"
                break
            case STATE.CLOSING:
                stateLabel = "🔴 平仓中 (CLOSING)"
                break
            default:
                stateLabel = "✅ 本轮完成 (DONE)"
        }

        var tpLine = leg1EntryAsk > 0 ? _N(leg1EntryAsk * (1 + EARLY_TAKE_PROFIT), 4).toString() : "-"
        var slLine = leg1EntryAsk > 0 ? _N(leg1EntryAsk * (1 - LAST_MIN_STOP_LOSS), 4).toString() : "-"

        var profitLine = ""
        if (state === STATE.BOTH_DONE && leg1EntryAsk > 0 && leg2EntryAsk > 0) {
            var realSum    = _N(leg1EntryAsk + leg2EntryAsk, 4)
            var realProfit = _N((1 - realSum) * SHARES, 4)
            profitLine = "✅ 锁定 | 合计:" + realSum + " | 利润:" + (realProfit >= 0 ? "+" : "") + realProfit + " USDC"
        } else if (leg1Price > 0 && leg2Price > 0) {
            profitLine = "挂单价合计:" + _N(leg1Price + leg2Price, 4) + " | 最大利润空间:" + _N((1 - leg1Price - leg2Price) * SHARES, 4) + " USDC"
        } else {
            profitLine = "-"
        }

        var stateTable = {
            type: "table",
            title: "📊 策略状态 | 阶段: " + phase + " | 已过: " + elapsed + "s / " + INTERVAL + "s (剩余 " + remaining + "s)",
            cols: ["字段", "数值"],
            rows: [
                ["当前状态",           stateLabel],
                ["第一腿方向",         leg1Side ? (leg1Side === "Up" ? "📈 Up" : "📉 Down") : "-"],
                ["第一腿挂单价/成交价", leg1Price > 0 ? (leg1Price.toFixed(4) + (leg1EntryAsk > 0 ? " / " + leg1EntryAsk.toFixed(4) : " / 待成交")) : "-"],
                ["第二腿挂单价/成交价", leg2Price > 0 ? (leg2Price.toFixed(4) + (leg2EntryAsk > 0 ? " / " + leg2EntryAsk.toFixed(4) : " / 待成交")) : "-"],
                ["前期止盈线 >",       tpLine + (tpLine !== "-" ? " (入场价×" + (1 + EARLY_TAKE_PROFIT) + ")" : "")],
                ["末段止损线 <",       slLine + (slLine !== "-" ? " (入场价×" + (1 - LAST_MIN_STOP_LOSS) + ")" : "")],
                ["绝对保底价",         FLOOR_PRICE.toString()],
                ["套利利润",           profitLine]
            ]
        }

        // ---- 表格3:价格监控 ----
        var upDropPct   = "-", downDropPct = "-"
        var upMeet      = "-", downMeet    = "-"
        var refUpAsk    = "-", refDownAsk  = "-"
        var windowClosed = (state === STATE.WATCHING && elapsed > WINDOW_MIN * 60)

        if (!windowClosed && priceHistory.length > 0) {
            var oldest   = priceHistory[0]
            var upDrop   = (oldest.upAsk   - upAsk)   / oldest.upAsk
            var downDrop = (oldest.downAsk - downAsk) / oldest.downAsk
            refUpAsk    = oldest.upAsk.toFixed(4)
            refDownAsk  = oldest.downAsk.toFixed(4)
            upDropPct   = (upDrop   * 100).toFixed(2) + "%"
            downDropPct = (downDrop * 100).toFixed(2) + "%"
            upMeet      = upDrop   >= MOVE_PCT ? "✅ 满足" : "❌ 未满足"
            downMeet    = downDrop >= MOVE_PCT ? "✅ 满足" : "❌ 未满足"
        } else if (windowClosed) {
            refUpAsk = refDownAsk = "-- (窗口已关闭)"
            upMeet = downMeet = "⏳ 不检测"
        }

        var priceTable = {
            type: "table",
            title: windowClosed
                ? "📉 价格监控 | ⏳ 监控窗口已关闭"
                : "📉 价格监控 | 检测窗口: " + DUMP_WINDOW_S + "s | 阈值: " + (MOVE_PCT * 100) + "% | 样本: " + priceHistory.length,
            cols: ["方向", "参考价", "当前Ask", "当前Bid", "暴跌幅度", "是否触发"],
            rows: [
                ["📈 Up",   refUpAsk,   upAsk.toFixed(4),   upBid.toFixed(4),   upDropPct,   upMeet],
                ["📉 Down", refDownAsk, downAsk.toFixed(4), downBid.toFixed(4), downDropPct, downMeet]
            ]
        }

        // ---- 表格4:持仓明细 ----
        var positions = []
        try { positions = exchange.GetPositions() || [] } catch(e) {}
        var posRows = []
        for (var _pi = 0; _pi < positions.length; _pi++) {
            var _p = positions[_pi]
            var curBid = "-", posUnrealizedPnl = "-"
            if (_p.Symbol === symbols.up && upTicker) {
                curBid = upBid.toFixed(4)
                if (_p.Price > 0) {
                    var rawPnl = _N((upBid - _p.Price) * _p.Amount, 4)
                    posUnrealizedPnl = (rawPnl >= 0 ? "+" : "") + rawPnl + " USDC"
                }
            } else if (_p.Symbol === symbols.down && downTicker) {
                curBid = downBid.toFixed(4)
                if (_p.Price > 0) {
                    var rawPnl2 = _N((downBid - _p.Price) * _p.Amount, 4)
                    posUnrealizedPnl = (rawPnl2 >= 0 ? "+" : "") + rawPnl2 + " USDC"
                }
            }
            var posSideTag = _p.Symbol === symbols.up ? " [📈 Up]" : (_p.Symbol === symbols.down ? " [📉 Down]" : "")
            posRows.push([_p.Symbol + posSideTag, _p.Amount.toString(), _N(_p.Price, 4).toString(), curBid, posUnrealizedPnl])
        }
        if (posRows.length === 0) posRows.push(["(无持仓)", "-", "-", "-", "-"])

        var posTable = {
            type: "table",
            title: "📂 持仓明细 (" + positions.length + " 个)",
            cols: ["合约", "份额", "建仓均价", "当前Bid", "浮动盈亏"],
            rows: posRows
        }

        LogProfit(pnl, "&")
        LogStatus(
            "`" + JSON.stringify(accountTable) + "`\n" +
            "`" + JSON.stringify(stateTable)   + "`\n" +
            "`" + JSON.stringify(priceTable)   + "`\n" +
            "`" + JSON.stringify(posTable)     + "`"
        )

        // ===================== 状态机 =====================

        // ==== WATCHING ====
        if (state === STATE.WATCHING) {
            if (elapsed <= WINDOW_MIN * 60) {
                priceHistory.push({ ts: Date.now(), upAsk: upAsk, downAsk: downAsk })
                var dumpSide = detectDump(upAsk, downAsk)
                if (dumpSide) {
                    var dumpAsk = (dumpSide === "Up") ? upAsk : downAsk
                    executeBothLegs(symbols, dumpSide, dumpAsk)
                }
            }
        }

        // ==== BOTH_PENDING:轮询两腿成交状态 ====
        else if (state === STATE.BOTH_PENDING) {
            // 临近结束时强制撤销未成交挂单
            if (isLastMin) {
                Log("⏰ 最后1分钟,撤销未成交挂单")
                cancelAllPending("最后1分钟")
                // cancelAllPending 会更新 state,下一 tick 进入对应分支
            } else {
                pollBothPending(symbols)
            }
        }

        // ==== LEG1_ONLY:仅第一腿成交,风控 ====
        else if (state === STATE.LEG1_ONLY) {
            handleLeg1OnlyRisk(symbols, upBid, downBid, isLastMin)
        }

        // ==== LEG2_ONLY:仅第二腿成交,风控 ====
        else if (state === STATE.LEG2_ONLY) {
            handleLeg2OnlyRisk(symbols, upBid, downBid, isLastMin)
        }

        // ==== BOTH_DONE:套利锁定,等待 redeem ====
        else if (state === STATE.BOTH_DONE) {
            // 无需操作,等待 redeem
        }

        // ==== CLOSING:平仓重试 ====
        else if (state === STATE.CLOSING) {
            var retrySymbol = (closingSide === "Up") ? symbols.up : symbols.down
            var retryBid    = (closingSide === "Up") ? upBid : downBid
            Log("🔄 CLOSING 重试平仓 | Symbol:", retrySymbol, "| Bid:", retryBid.toFixed(4))
            closePosition(retrySymbol, retryBid, "重试止损")
        }

        // ==== 统一 Redeem 判断(840s 后执行) ====
        if (!redeemDone && elapsed >= 840) {
            doRedeem()
            redeemDone = true
        }

        Sleep(SLEEP_MS)
    }
}