策略源码
// ============================================================
// 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)
}
}