策略源码
{"type":"n8n","content":"{\"workflowData\":{\"nodes\":[{\"parameters\":{\"splitInBatchesNotice\":\"\",\"batchSize\":5,\"options\":{}},\"id\":\"8feb76a6-5842-4f9f-8592-1a157d52bae7\",\"name\":\"分批送入AI\",\"type\":\"n8n-nodes-base.splitInBatches\",\"typeVersion\":3,\"position\":[112,480]},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"// ============================================================\\n// 交易执行:持仓判断 + 平旧开新 + 开仓 + 止损检查 + 可视化\\n// ============================================================\\n\\n// ==================== 从vars读取配置 ====================\\nvar MIN_CONF_MAP = { 0: '高', 1: '中', 2: '低' }\\nvar CONF_RANK = { '高': 2, '中': 1, '低': 0 }\\nvar MIN_CONF_VAL = parseInt($vars.MIN_CONF !== undefined ? $vars.MIN_CONF : 0)\\nif (isNaN(MIN_CONF_VAL) || MIN_CONF_MAP[MIN_CONF_VAL] === undefined) MIN_CONF_VAL = 0\\n\\nvar CONFIG = {\\n LEVERAGE: parseFloat($vars.LEVERAGE || 10),\\n MAX_POS: parseInt($vars.MAX_POS || 3),\\n MIN_CONF: MIN_CONF_MAP[MIN_CONF_VAL] || '高',\\n ORDER_TIMEOUT: 30000\\n}\\nvar OPEN_MONEY = parseFloat($vars.OPEN_MONEY || 100)\\nvar TRAILING_PCT = parseFloat($vars.TRAILING_PCT || 0.03)\\nvar FALLBACK_PCT = parseFloat($vars.FALLBACK_PCT || 0.05)\\n\\nLog('⚙️ 交易配置 | 开仓金额:$' + OPEN_MONEY\\n + ' | 杠杆:' + CONFIG.LEVERAGE\\n + ' | 最低置信:' + CONFIG.MIN_CONF\\n + ' | 最大持仓:' + CONFIG.MAX_POS\\n + ' | 移动止损:' + (TRAILING_PCT * 100) + '%'\\n + ' | 兜底止损:' + (FALLBACK_PCT * 100) + '%')\\n\\n// ==================== 工具函数 ====================\\nfunction toSwapSymbol(symbol) {\\n var base = symbol.replace(/USDT$/i, '')\\n return base + '_USDT.swap'\\n}\\n\\nfunction floorToStep(qty, step, precision) {\\n var factor = Math.pow(10, precision)\\n var result = Math.floor(qty / step * factor) / factor * step\\n return Number(result.toFixed(precision))\\n}\\n\\nfunction parseAI(raw) {\\n try {\\n var s = typeof raw === 'string' ? raw.replace(/```[a-z]*\\\\n?/gi, '').trim() : JSON.stringify(raw)\\n return JSON.parse(s)\\n } catch(e) { return null }\\n}\\n\\n// ==================== 市场信息 ====================\\nvar markets = {}\\ntry {\\n markets = exchange.GetMarkets() || {}\\n} catch(e) {\\n Log('⚠️ GetMarkets失败: ' + e.message)\\n}\\n\\nfunction getMarketInfo(symbol) {\\n var info = markets[toSwapSymbol(symbol)]\\n if (!info) return null\\n return {\\n ctVal: info.CtVal || 1,\\n amtSize: info.AmountSize || 1,\\n amtPrec: info.AmountPrecision !== undefined ? info.AmountPrecision : 3,\\n minQty: info.MinQty || 0,\\n maxQty: info.MaxQty || Infinity\\n }\\n}\\n\\nfunction getDepth(symbol) {\\n try {\\n var d = exchange.GetDepth(toSwapSymbol(symbol))\\n if (!d) return null\\n return {\\n bid: d.Bids && d.Bids[0] ? d.Bids[0].Price : 0,\\n ask: d.Asks && d.Asks[0] ? d.Asks[0].Price : 0\\n }\\n } catch(e) { return null }\\n}\\n\\nfunction getRefPrice(symbol, direction) {\\n var depth = getDepth(symbol)\\n if (!depth) return null\\n return direction === 'LONG' ? depth.ask : depth.bid\\n}\\n\\n// ==================== 下单函数 ====================\\nfunction placeMarket(symbol, direction, qty) {\\n try {\\n var swapSym = toSwapSymbol(symbol)\\n var qtyFloat = Number(qty)\\n if (isNaN(qtyFloat) || qtyFloat <= 0) {\\n Log('❌ qty异常: ' + qty)\\n return null\\n }\\n try { exchange.SetMarginLevel(swapSym, CONFIG.LEVERAGE) } catch(e) {\\n Log('⚠️ 设置杠杆失败 ' + swapSym + ': ' + e.message)\\n }\\n var side = direction === 'LONG' ? 'buy' : 'sell'\\n var oid = exchange.CreateOrder(swapSym, side, -1, qtyFloat)\\n if (!oid) return null\\n var deadline = Date.now() + CONFIG.ORDER_TIMEOUT\\n while (Date.now() < deadline) {\\n var o = exchange.GetOrder(oid)\\n if (o && o.Status === 1) return o\\n if (o && (o.Status === 2 || o.Status === 4)) return null\\n Sleep(500)\\n }\\n return null\\n } catch(e) { Log('❌ 下单异常: ' + e.message); return null }\\n}\\n\\nfunction closePosition(pos, reason) {\\n try {\\n var swapSym = toSwapSymbol(pos.symbol)\\n var side = pos.direction === 'LONG' ? 'closebuy' : 'closesell'\\n var oid = exchange.CreateOrder(swapSym, side, -1, pos.qty)\\n if (oid) { Log('✅ 平仓: ' + pos.symbol + ' | ' + reason); return true }\\n } catch(e) { Log('❌ 平仓失败: ' + e.message) }\\n return false\\n}\\n\\nfunction calcQty(symbol, direction, mkt) {\\n var refPrice = getRefPrice(symbol, direction)\\n if (!refPrice || refPrice <= 0) return { qty: 0, refPrice: 0 }\\n var rawQty = OPEN_MONEY * CONFIG.LEVERAGE / refPrice / mkt.ctVal\\n var qty = floorToStep(rawQty, mkt.amtSize, mkt.amtPrec)\\n if (qty < mkt.minQty) {\\n Log('⚠️ ' + symbol + ' 张数(' + qty + ') < MinQty,强制MinQty')\\n qty = Number(mkt.minQty.toFixed(mkt.amtPrec))\\n }\\n if (qty > mkt.maxQty) {\\n Log('⚠️ ' + symbol + ' 张数(' + qty + ') > MaxQty,截断MaxQty')\\n qty = Number(mkt.maxQty.toFixed(mkt.amtPrec))\\n }\\n return { qty: qty, refPrice: refPrice }\\n}\\n\\n// ==================== 读取持仓 ====================\\nvar positions = _G('liqPositions') || {}\\nvar openCount = 0\\nfor (var k in positions) openCount++\\n\\n// ==================== 解析AI输出 ====================\\nvar allItems = $input.all()\\nvar signals = []\\nvar results = {}\\n\\nfor (var i = 0; i < allItems.length; i++) {\\n var p = parseAI(allItems[i].json.output)\\n if (!p || !p.symbol) continue\\n if (!p.confidence || CONF_RANK[p.confidence] === undefined) p.confidence = '低'\\n if (!p.action) p.action = '不入场'\\n results[p.symbol] = {\\n action: p.action,\\n direction: p.direction || '-',\\n confidence: p.confidence,\\n reason: p.action_reason || '',\\n liqNote: p.liq_note || '',\\n trendNote: p.trend_note || '',\\n newsNote: p.news_note || '',\\n executed: false,\\n skipReason: ''\\n }\\n if (p.action === '入场') signals.push(p)\\n}\\n\\nsignals.sort(function(a, b) {\\n var rankA = CONF_RANK[a.confidence] !== undefined ? CONF_RANK[a.confidence] : 0\\n var rankB = CONF_RANK[b.confidence] !== undefined ? CONF_RANK[b.confidence] : 0\\n return rankB - rankA\\n})\\n\\n// ==================== AI判断汇总日志 ====================\\nLog('═══════════════════════════════════')\\nLog('🤖 AI判断汇总 | 共' + Object.keys(results).length + '个币种 | 入场信号:' + signals.length + '个')\\nfor (var sym in results) {\\n var r = results[sym]\\n var icon = r.action === '入场' ? '🟢' : r.action === '观望' ? '🟡' : '⏭️'\\n Log(icon + ' ' + sym + ' | ' + r.direction + ' | ' + r.action\\n + ' | 置信:' + r.confidence + ' | ' + r.reason)\\n}\\nLog('═══════════════════════════════════')\\n\\n// ==================== 逐个处理信号 ====================\\nfor (var s = 0; s < signals.length; s++) {\\n var sig = signals[s]\\n var sym = sig.symbol\\n\\n // 置信度过滤\\n if ((CONF_RANK[sig.confidence] || 0) < (CONF_RANK[CONFIG.MIN_CONF] || 0)) {\\n results[sym].skipReason = '置信度不足(' + sig.confidence + ' < ' + CONFIG.MIN_CONF + ')'\\n Log('⏭️ 跳过 | ' + sym + ' | 原因: 置信度不足(' + sig.confidence + ' < ' + CONFIG.MIN_CONF + ')')\\n continue\\n }\\n\\n // 已有该币种持仓\\n if (positions[sym]) {\\n var existing = positions[sym]\\n var newRank = CONF_RANK[sig.confidence] || 0\\n var oldRank = CONF_RANK[existing.confidence] || 0\\n if (newRank > oldRank) {\\n // 新信号置信度更高 → 平旧开新\\n Log('🔄 平旧开新 | ' + sym\\n + ' | 旧置信:' + existing.confidence\\n + ' → 新置信:' + sig.confidence)\\n var closed = closePosition(existing, '平旧开新')\\n if (closed) {\\n delete positions[sym]\\n openCount--\\n } else {\\n results[sym].skipReason = '平旧仓失败,跳过开新仓'\\n Log('❌ 平旧仓失败,跳过: ' + sym)\\n continue\\n }\\n } else {\\n results[sym].skipReason = '已有持仓且新信号置信度未超过旧仓(' + existing.confidence + ')'\\n Log('⏭️ 跳过 | ' + sym + ' | 原因: 已有持仓且置信度未超过(' + existing.confidence + ')')\\n continue\\n }\\n }\\n\\n // 持仓上限\\n if (openCount >= CONFIG.MAX_POS) {\\n results[sym].skipReason = '持仓已满(' + openCount + '/' + CONFIG.MAX_POS + ')'\\n Log('⏭️ 跳过 | ' + sym + ' | 原因: 持仓已满(' + openCount + '/' + CONFIG.MAX_POS + ')')\\n continue\\n }\\n\\n // 获取合约信息\\n var mkt = getMarketInfo(sym)\\n if (!mkt) {\\n results[sym].skipReason = '找不到合约信息(' + toSwapSymbol(sym) + ')'\\n Log('⏭️ 跳过 | ' + sym + ' | 原因: 找不到合约信息')\\n continue\\n }\\n\\n // 计算张数\\n var calc = calcQty(sym, sig.direction, mkt)\\n if (!calc.qty || calc.qty <= 0) {\\n results[sym].skipReason = '获取价格或计算张数失败'\\n Log('⏭️ 跳过 | ' + sym + ' | 原因: 获取价格或计算张数失败')\\n continue\\n }\\n\\n Log('🎯 尝试开仓 | ' + sym\\n + ' | 方向:' + sig.direction\\n + ' | swap:' + toSwapSymbol(sym)\\n + ' | 参考价:' + calc.refPrice\\n + ' | 面值:' + mkt.ctVal\\n + ' | 步长:' + mkt.amtSize\\n + ' | 张数:' + calc.qty\\n + ' | 名义:$' + (calc.qty * mkt.ctVal * calc.refPrice).toFixed(2)\\n + ' | 置信:' + sig.confidence)\\n Log(' 📊 AI理由: ' + results[sym].reason)\\n Log(' 💥 爆仓: ' + results[sym].liqNote)\\n Log(' 📈 K线: ' + results[sym].trendNote)\\n Log(' 📰 新闻: ' + results[sym].newsNote)\\n\\n var order = placeMarket(sym, sig.direction, calc.qty)\\n if (order) {\\n var ap = order.AvgPrice || calc.refPrice\\n positions[sym] = {\\n symbol: sym,\\n direction: sig.direction,\\n entryPrice: ap,\\n qty: calc.qty,\\n peak: ap,\\n maxProfit: 0,\\n ctVal: mkt.ctVal,\\n openTime: Date.now(),\\n confidence: sig.confidence\\n }\\n openCount++\\n results[sym].executed = true\\n results[sym].avgPrice = ap\\n Log('✅ 开仓成功 | ' + sym\\n + ' | 方向:' + sig.direction\\n + ' | 均价:' + ap\\n + ' | 张数:' + calc.qty\\n + ' | 名义:$' + (calc.qty * mkt.ctVal * ap).toFixed(2))\\n } else {\\n results[sym].skipReason = '下单失败(市价单未成交)'\\n Log('❌ 开仓失败 | ' + sym)\\n }\\n Sleep(500)\\n}\\n\\n// ==================== 执行结果汇总日志 ====================\\nLog('═══════════════════════════════════')\\nvar exeCount = 0\\nvar skipCount = 0\\nfor (var sym in results) {\\n var r = results[sym]\\n if (r.executed) {\\n exeCount++\\n Log('✅ 已开仓 | ' + sym + ' | ' + r.direction + ' | 均价:' + r.avgPrice + ' | 置信:' + r.confidence)\\n } else if (r.action === '入场' && r.skipReason) {\\n skipCount++\\n Log('⏭️ 未开仓 | ' + sym + ' | AI建议入场但跳过 | 原因:' + r.skipReason)\\n } else if (r.action !== '入场') {\\n Log('💤 未入场 | ' + sym + ' | AI决策:' + r.action + ' | ' + r.reason)\\n }\\n}\\nLog('📊 本轮结果: 信号' + signals.length + '个 | 开仓' + exeCount + '个 | 跳过' + skipCount + '个')\\nLog('═══════════════════════════════════')\\n\\n_G('liqLastSignals', results)\\n\\n// ==================== 止损检查 ====================\\nLog('🔍 开始止损检查 | 当前持仓数:' + openCount)\\nvar monitorResults = []\\nvar closedList = []\\n\\nfor (var sym in positions) {\\n var pos = positions[sym]\\n var depth = getDepth(sym)\\n if (!depth) {\\n Log('⚠️ 获取深度失败: ' + sym)\\n monitorResults.push({ symbol: sym, status: 'no_price', direction: pos.direction })\\n continue\\n }\\n\\n var cur = pos.direction === 'LONG' ? depth.bid : depth.ask\\n if (!cur || cur <= 0) {\\n Log('⚠️ 价格异常: ' + sym + ' cur=' + cur)\\n monitorResults.push({ symbol: sym, status: 'no_price', direction: pos.direction })\\n continue\\n }\\n\\n // 当前盈利%\\n var pnlPct = pos.direction === 'LONG'\\n ? (cur - pos.entryPrice) / pos.entryPrice * 100\\n : (pos.entryPrice - cur) / pos.entryPrice * 100\\n\\n // 更新最高盈利点\\n if (pos.maxProfit === undefined || pos.maxProfit === null) pos.maxProfit = 0\\n if (pnlPct > pos.maxProfit) {\\n pos.maxProfit = parseFloat(pnlPct.toFixed(4))\\n positions[sym].maxProfit = pos.maxProfit\\n }\\n\\n // 更新peak价格\\n if (pos.direction === 'LONG') {\\n if (!pos.peak || cur > pos.peak) { positions[sym].peak = cur; pos.peak = cur }\\n } else {\\n if (!pos.peak || cur < pos.peak) { positions[sym].peak = cur; pos.peak = cur }\\n }\\n\\n // 从最高盈利点回撤%\\n var drawdownFromPeak = parseFloat((pos.maxProfit - pnlPct).toFixed(4))\\n\\n // 止损价计算\\n var effectiveStop, triggered = false, reason = ''\\n if (pos.direction === 'LONG') {\\n var trailStop = pos.peak * (1 - TRAILING_PCT)\\n var fallStop = pos.entryPrice * (1 - FALLBACK_PCT)\\n effectiveStop = Math.min(trailStop, fallStop)\\n if (cur <= effectiveStop) {\\n triggered = true\\n reason = cur <= fallStop\\n ? '🔐 兜底止损'\\n : '🛡️ 移动止损(回撤' + drawdownFromPeak.toFixed(2) + '%)'\\n }\\n } else {\\n var trailStop = pos.peak * (1 + TRAILING_PCT)\\n var fallStop = pos.entryPrice * (1 + FALLBACK_PCT)\\n effectiveStop = Math.max(trailStop, fallStop)\\n if (cur >= effectiveStop) {\\n triggered = true\\n reason = cur >= fallStop\\n ? '🔐 兜底止损'\\n : '🛡️ 移动止损(回撤' + drawdownFromPeak.toFixed(2) + '%)'\\n }\\n }\\n\\n Log('📌 ' + sym\\n + ' | ' + pos.direction\\n + ' | 入场:' + pos.entryPrice\\n + ' | 当前:' + cur\\n + ' | 盈亏:' + (pnlPct >= 0 ? '+' : '') + pnlPct.toFixed(2) + '%'\\n + ' | 最大盈利:' + pos.maxProfit.toFixed(2) + '%'\\n + ' | 回撤:' + drawdownFromPeak.toFixed(2) + '%'\\n + ' | 止损价:' + effectiveStop.toFixed(6)\\n + (triggered ? ' | ⚠️ 触发止损!' : ''))\\n\\n if (triggered) {\\n var ok = closePosition(pos, reason)\\n if (ok) { closedList.push(sym) }\\n monitorResults.push({\\n symbol: sym,\\n status: ok ? 'closed' : 'close_failed',\\n reason: reason,\\n direction: pos.direction,\\n entryPrice: pos.entryPrice,\\n closePrice: cur,\\n pnlPct: parseFloat(pnlPct.toFixed(2)),\\n maxProfit: parseFloat(pos.maxProfit.toFixed(2)),\\n drawdownFromPeak: parseFloat(drawdownFromPeak.toFixed(2))\\n })\\n } else {\\n monitorResults.push({\\n symbol: sym,\\n status: 'monitoring',\\n direction: pos.direction,\\n entryPrice: pos.entryPrice,\\n cur: cur,\\n peak: pos.peak,\\n pnlPct: parseFloat(pnlPct.toFixed(2)),\\n maxProfit: parseFloat(pos.maxProfit.toFixed(2)),\\n drawdownFromPeak: parseFloat(drawdownFromPeak.toFixed(2)),\\n stopPrice: parseFloat(effectiveStop.toFixed(6))\\n })\\n }\\n Sleep(200)\\n}\\n\\nfor (var c = 0; c < closedList.length; c++) { delete positions[closedList[c]] }\\n_G('liqPositions', positions)\\n\\n// ==================== 可视化 ====================\\nvar accTable = {\\n type: 'table', title: '💰 账户概览',\\n cols: ['余额(USDT)', '持仓数', '累计盈亏', '移动止损', '兜底止损'],\\n rows: []\\n}\\ntry {\\n var acc = exchange.GetAccount()\\n var initBal = _G('liqInitBal')\\n if (!initBal) { _G('liqInitBal', acc.Equity); initBal = acc.Equity }\\n var profit = acc.Equity - initBal\\n accTable.rows.push([\\n '$' + acc.Equity.toFixed(2),\\n Object.keys(positions).length + ' / ' + CONFIG.MAX_POS,\\n (profit >= 0 ? '🟢 +' : '🔴 ') + '$' + Math.abs(profit).toFixed(2),\\n '回撤 ' + (TRAILING_PCT * 100) + '%',\\n '亏损 ' + (FALLBACK_PCT * 100) + '%'\\n ])\\n LogProfit(profit, '&')\\n} catch(e) {\\n accTable.rows.push(['获取失败', '-', '-', '-', '-'])\\n}\\n\\nvar posTable = {\\n type: 'table', title: '📊 持仓监控',\\n cols: ['币种', '方向', '入场价', '当前价', '盈亏%', '最大盈利', '当前回撤', '止损价', '状态'],\\n rows: []\\n}\\nfor (var r = 0; r < monitorResults.length; r++) {\\n var mr = monitorResults[r]\\n var short = (mr.symbol || '').replace('USDT', '')\\n var dir = mr.direction === 'LONG' ? '🟢 多' : '🔴 空'\\n if (mr.status === 'monitoring') {\\n posTable.rows.push([\\n short, dir,\\n mr.entryPrice, mr.cur,\\n (mr.pnlPct >= 0 ? '🟢 +' : '🔴 ') + mr.pnlPct + '%',\\n '🏆 ' + mr.maxProfit + '%',\\n '📉 ' + mr.drawdownFromPeak + '%',\\n mr.stopPrice, '✅ 持仓中'\\n ])\\n } else if (mr.status === 'closed') {\\n posTable.rows.push([\\n short, dir,\\n mr.entryPrice, mr.closePrice,\\n (mr.pnlPct >= 0 ? '🟢 +' : '🔴 ') + mr.pnlPct + '%',\\n '🏆 ' + mr.maxProfit + '%',\\n '📉 ' + mr.drawdownFromPeak + '%',\\n '-', mr.reason\\n ])\\n } else {\\n posTable.rows.push([short, dir, '-', '-', '-', '-', '-', '-', '⚠️ ' + mr.status])\\n }\\n}\\nif (posTable.rows.length === 0) posTable.rows.push(['暂无持仓', '-', '-', '-', '-', '-', '-', '-', '等待信号'])\\n\\nvar sigTable = {\\n type: 'table', title: '🤖 最新AI信号',\\n cols: ['币种', '方向', '决策', '置信度', '爆仓信号', 'K线', '新闻', '综合理由'],\\n rows: []\\n}\\nvar lastSig = _G('liqLastSignals') || {}\\nfor (var sym in lastSig) {\\n var sig = lastSig[sym]\\n var icon = sig.action === '入场' ? '🟢' : sig.action === '观望' ? '🟡' : '⏭️'\\n sigTable.rows.push([\\n sym.replace('USDT', ''),\\n sig.direction || '-',\\n icon + ' ' + sig.action,\\n sig.confidence || '-',\\n sig.liqNote || '-',\\n sig.trendNote || '-',\\n sig.newsNote || '-',\\n sig.reason || sig.skipReason || '-'\\n ])\\n}\\nif (sigTable.rows.length === 0) sigTable.rows.push(['暂无', '-', '-', '-', '-', '-', '-', '等待下次扫描'])\\n\\nLogStatus(\\n '`' + JSON.stringify(accTable) + '`\\\\n\\\\n' +\\n '`' + JSON.stringify(posTable) + '`\\\\n\\\\n' +\\n '`' + JSON.stringify(sigTable) + '`'\\n)\\n\\nreturn [{ json: { done: true, executed: exeCount, results: results } }]\",\"notice\":\"\"},\"id\":\"c0b01692-e1c0-46d9-affd-997ed8ae1793\",\"name\":\"交易执行\",\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[304,320]},{\"parameters\":{\"notice\":\"\",\"rule\":{\"interval\":[{\"field\":\"minutes\",\"minutesInterval\":1}]}},\"id\":\"c4961aa0-4ef7-46a3-904d-6acddf96d6b9\",\"name\":\"触发器\",\"type\":\"n8n-nodes-base.scheduleTrigger\",\"typeVersion\":1.2,\"position\":[-944,480]},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"// ============================================================\\n// 节点一:初始化收集节点\\n// ============================================================\\n\\nvar alreadyDone = _G('liqInitialized')\\nif (alreadyDone) {\\n Log('⏭️ 已初始化,跳过本节点')\\n return [{ json: { status: 'skipped' } }]\\n}\\n\\nvar WINDOW_MINUTES = 240\\nvar MIN_VALUE = 100\\nvar RUN_MINUTES = 59\\nvar SAVE_INTERVAL = 5 * 60 * 1000\\nvar EXCLUDE = { 'BTCUSDT': 1, 'ETHUSDT': 1 }\\n\\nvar liquidationData = _G('liquidationData') || []\\n\\nvar now = new Date()\\nvar startTs = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), 0, 0, 0).getTime()\\nvar endTs = startTs + RUN_MINUTES * 60 * 1000\\nLog('⏱ 初始化窗口: ' + new Date(startTs).toLocaleTimeString() + ' ~ ' + new Date(endTs).toLocaleTimeString())\\n\\nvar ws = Dial('wss://fstream.binance.com/ws/!forceOrder@arr')\\nif (!ws) {\\n Log('❌ WebSocket 连接失败')\\n return [{ json: { status: 'ws_failed' } }]\\n}\\nLog('✅ WebSocket 连接成功,开始采集...')\\n\\nvar lastSaveTs = Date.now()\\nvar totalNew = 0\\n\\nwhile (Date.now() < endTs) {\\n var msg = ws.read(1000)\\n\\n if (msg === '') {\\n Log('🔴 断线,重连...')\\n ws.close()\\n ws = Dial('wss://fstream.binance.com/ws/!forceOrder@arr')\\n if (!ws) { Sleep(5000); continue }\\n Log('✅ 重连成功')\\n continue\\n }\\n if (!msg) continue\\n if (msg === 'ping') { ws.write('pong'); continue }\\n if (msg === 'pong') continue\\n\\n try {\\n var obj = JSON.parse(msg)\\n var orders = Array.isArray(obj) ? obj : [obj]\\n for (var i = 0; i < orders.length; i++) {\\n var item = orders[i]\\n if (!item || !item.o) continue\\n var o = item.o\\n if (o.X !== 'FILLED') continue\\n if (EXCLUDE[o.s]) continue\\n if (!/USDT$/i.test(o.s)) continue // ✅ 只处理USDT结尾\\n\\n var price = parseFloat(o.ap || o.p)\\n var qty = parseFloat(o.z)\\n var value = price * qty\\n if (value < MIN_VALUE) continue\\n\\n liquidationData.push({\\n t: item.E || Date.now(),\\n s: o.s,\\n d: o.S,\\n v: value\\n })\\n totalNew++\\n }\\n } catch(e) {}\\n\\n var nowTs = Date.now()\\n if (nowTs - lastSaveTs >= SAVE_INTERVAL) {\\n var cutoff = nowTs - WINDOW_MINUTES * 60 * 1000\\n liquidationData = liquidationData.filter(function(d) { return d.t > cutoff })\\n _G('liquidationData', liquidationData)\\n lastSaveTs = nowTs\\n Log('💾 写回 | 总条数: ' + liquidationData.length + ' | 本轮新增: ' + totalNew)\\n }\\n}\\n\\nws.close()\\nvar cutoff = Date.now() - WINDOW_MINUTES * 60 * 1000\\nliquidationData = liquidationData.filter(function(d) { return d.t > cutoff })\\n_G('liquidationData', liquidationData)\\n_G('liqInitialized', true)\\n\\nLog('🎉 初始化完成 | 总新增: ' + totalNew + ' | 总存储: ' + liquidationData.length)\\nreturn [{ json: { status: 'initialized', total: liquidationData.length } }]\",\"notice\":\"\"},\"id\":\"1ffed204-283c-4e54-a523-f3fd67f56c3d\",\"name\":\"初始化收集\",\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[-784,480]},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"// ============================================================\\n// 节点二:数据采集 + Z-Score异常扫描\\n// ============================================================\\n\\n// ==================== 从vars读取配置 ====================\\nvar WINDOW_MINUTES = parseFloat($vars.WINDOW_MINUTES || 120)\\nvar MIN_VALUE = parseFloat($vars.MIN_VALUE || 100)\\nvar TOP_N = parseInt($vars.TOP_N || 5)\\nvar ZSCORE_THRESH = parseFloat($vars.ZSCORE_THRESH || 2.5)\\nvar DIR_THRESH = parseFloat($vars.DIR_THRESH || 0.75)\\nvar COLLECT_MS = 270 * 1000\\nvar EXCLUDE = {}\\n\\nLog('⚙️ 策略配置 | 保留:' + WINDOW_MINUTES + 'min'\\n + ' | 最小爆仓:$' + MIN_VALUE\\n + ' | Z阈值:' + ZSCORE_THRESH\\n + ' | 方向纯度:' + DIR_THRESH\\n + ' | TOP_N:' + TOP_N)\\n\\n// ==================== PART 1: 采集新数据 ====================\\nvar liquidationData = _G('liquidationData') || []\\n\\nvar ws = Dial('wss://fstream.binance.com/ws/!forceOrder@arr')\\nif (!ws) {\\n Log('❌ WebSocket 连接失败,跳过本次采集')\\n} else {\\n var collectEnd = Date.now() + COLLECT_MS\\n var newCount = 0\\n\\n while (Date.now() < collectEnd) {\\n var msg = ws.read(1000)\\n if (msg === '') {\\n ws.close()\\n ws = Dial('wss://fstream.binance.com/ws/!forceOrder@arr')\\n if (!ws) { Sleep(3000); continue }\\n continue\\n }\\n if (!msg) continue\\n if (msg === 'ping') { ws.write('pong'); continue }\\n if (msg === 'pong') continue\\n\\n try {\\n var obj = JSON.parse(msg)\\n var orders = Array.isArray(obj) ? obj : [obj]\\n for (var i = 0; i < orders.length; i++) {\\n var item = orders[i]\\n if (!item || !item.o) continue\\n var o = item.o\\n if (o.X !== 'FILLED') continue\\n if (EXCLUDE[o.s]) continue\\n if (!/USDT$/i.test(o.s)) continue\\n\\n var price = parseFloat(o.ap || o.p)\\n var qty = parseFloat(o.z)\\n var value = price * qty\\n if (value < MIN_VALUE) continue\\n liquidationData.push({ t: item.E || Date.now(), s: o.s, d: o.S, v: value })\\n newCount++\\n }\\n } catch(e) {}\\n }\\n ws.close()\\n\\n var cutoff = Date.now() - WINDOW_MINUTES * 60 * 1000\\n liquidationData = liquidationData.filter(function(d) { return d.t > cutoff })\\n _G('liquidationData', liquidationData)\\n Log('📦 新增: ' + newCount + ' | 总存储: ' + liquidationData.length)\\n}\\n\\n// ==================== PART 2: Z-Score 异常扫描 ====================\\nvar now60 = Date.now() - 60 * 60 * 1000\\nvar stats = {}\\n\\nfor (var j = 0; j < liquidationData.length; j++) {\\n var d = liquidationData[j]\\n if (d.t < now60) continue\\n if (!stats[d.s]) stats[d.s] = { buckets: new Array(12).fill(0), longV: 0, shortV: 0 }\\n var st = stats[d.s]\\n var bucket = Math.min(11, Math.floor((Date.now() - d.t) / (5 * 60 * 1000)))\\n st.buckets[bucket] += d.v\\n if (d.d === 'SELL') st.longV += d.v\\n else st.shortV += d.v\\n}\\n\\nvar anomalies = []\\nfor (var sym in stats) {\\n if (!/USDT$/i.test(sym)) continue\\n\\n var st = stats[sym]\\n var rec = st.buckets[0]\\n var hist = st.buckets.slice(1)\\n\\n var mean = 0\\n for (var k = 0; k < hist.length; k++) mean += hist[k]\\n mean = mean / hist.length\\n\\n var vari = 0\\n for (var k = 0; k < hist.length; k++) vari += Math.pow(hist[k] - mean, 2)\\n var std = Math.sqrt(vari / hist.length)\\n\\n var z = std > 0 ? (rec - mean) / std : 0\\n if (z < ZSCORE_THRESH || rec <= 0) continue\\n\\n var total = st.longV + st.shortV\\n var longRatio = total > 0 ? st.longV / total : 0.5\\n\\n var direction = null\\n if (longRatio > DIR_THRESH) direction = 'SHORT'\\n else if (longRatio < 1 - DIR_THRESH) direction = 'LONG'\\n if (!direction) continue\\n\\n anomalies.push({\\n symbol: sym,\\n direction: direction,\\n zScore: parseFloat(z.toFixed(2)),\\n longRatio: parseFloat(longRatio.toFixed(2)),\\n recentK: parseFloat((rec / 1000).toFixed(1)),\\n meanK: parseFloat((mean / 1000).toFixed(1))\\n })\\n}\\n\\nanomalies.sort(function(a, b) { return b.zScore - a.zScore })\\nanomalies = anomalies.slice(0, TOP_N)\\nLog('🚨 异常币种: ' + anomalies.length + '个')\\n\\nif (anomalies.length === 0) {\\n return [{ json: { status: 'no_anomaly' } }]\\n}\\nreturn [{ json: { status: 'anomaly_found', anomalies: anomalies } }]\",\"notice\":\"\"},\"id\":\"5aa2aa6f-6c74-4d25-a34a-6f87caa32b12\",\"name\":\"策略执行\",\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[-560,480]},{\"parameters\":{\"conditions\":{\"options\":{\"caseSensitive\":true,\"leftValue\":\"\",\"typeValidation\":\"strict\",\"version\":1},\"conditions\":[{\"id\":\"c1\",\"leftValue\":\"={{ $json.status }}\",\"rightValue\":\"anomaly_found\",\"operator\":{\"type\":\"string\",\"operation\":\"equals\"}}],\"combinator\":\"and\"},\"options\":{}},\"id\":\"bf7c7a64-6d76-4db0-b656-94c267e24b5e\",\"name\":\"异常判断\",\"type\":\"n8n-nodes-base.if\",\"typeVersion\":2,\"position\":[-336,480]},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"// ============================================================\\n// 节点三:补充数据\\n// ============================================================\\n\\nvar BRAVE_KEY = $vars.BraveApiKey || ''\\nvar NEWS_DELAY = 1200\\n\\n// ✅ symbol转换函数\\nfunction toSwapSymbol(symbol) {\\n var base = symbol.replace(/USDT$/i, '')\\n return base + '_USDT.swap'\\n}\\n\\nvar anomalies = ($input.first().json.anomalies || [])\\nif (anomalies.length === 0) return [{ json: { status: 'empty' } }]\\n\\nvar enriched = []\\n\\nfor (var i = 0; i < anomalies.length; i++) {\\n var a = anomalies[i]\\n var symbol = a.symbol\\n var swapSym = toSwapSymbol(symbol) // ✅ 转换格式\\n\\n // ---------- K线 ----------\\n var kline = null\\n try {\\n var recs = exchange.GetRecords(swapSym, PERIOD_M1) // ✅ 直接传swap格式\\n if (recs && recs.length > 30) recs = recs.slice(recs.length - 30)\\n if (recs && recs.length >= 2) {\\n var closes = recs.map(function(r) { return r.Close })\\n var first = closes[0]\\n var last = closes[closes.length - 1]\\n var change = parseFloat(((last - first) / first * 100).toFixed(2))\\n\\n var rets = []\\n for (var j = 1; j < closes.length; j++) rets.push((closes[j] - closes[j-1]) / closes[j-1])\\n var mean = rets.reduce(function(s,v){return s+v}, 0) / rets.length\\n var vari = rets.reduce(function(s,v){return s+Math.pow(v-mean,2)}, 0) / rets.length\\n var vol = parseFloat((Math.sqrt(vari) * 100).toFixed(4))\\n\\n var trend = change > 0 ? 'UP' : 'DOWN'\\n var aligned = (a.direction === 'SHORT' && trend === 'DOWN') ||\\n (a.direction === 'LONG' && trend === 'UP')\\n\\n kline = { change: change, volatility: vol, trend: trend, aligned: aligned, lastPrice: last }\\n }\\n } catch(e) { Log('⚠️ K线失败 ' + symbol + '(' + swapSym + '): ' + e.message) }\\n\\n // ---------- 新闻 ----------\\n var news = []\\n if (BRAVE_KEY) {\\n try {\\n var coin = symbol.replace('USDT', '')\\n var q = encodeURIComponent(coin + ' crypto news')\\n var raw = HttpQuery(\\n 'https://api.search.brave.com/res/v1/news/search?q=' + q + '&count=5&freshness=pd',\\n { method: 'GET', headers: { 'X-Subscription-Token': BRAVE_KEY, 'Accept': 'application/json' } }\\n )\\n var data = JSON.parse(raw)\\n if (data.results) {\\n for (var n = 0; n < data.results.length; n++) {\\n var nr = data.results[n]\\n news.push({ title: nr.title || '', desc: nr.description || '', age: nr.age || '' })\\n }\\n }\\n } catch(e) { Log('⚠️ 新闻失败 ' + symbol + ': ' + e.message) }\\n Sleep(NEWS_DELAY)\\n }\\n\\n enriched.push({\\n symbol: symbol, // ✅ 对外仍用原始格式 BEATUSDT\\n direction: a.direction,\\n zScore: a.zScore,\\n longRatio: a.longRatio,\\n recentK: a.recentK,\\n meanK: a.meanK,\\n kline: kline,\\n news: news\\n })\\n Sleep(200)\\n}\\n\\nLog('✅ 补充完成: ' + enriched.length + '个')\\nreturn enriched.map(function(item) { return { json: item } })\",\"notice\":\"\"},\"id\":\"b8841619-9289-4912-b4ec-c7cbf5cf4bb6\",\"name\":\"数据补充\",\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[-112,480]},{\"parameters\":{\"text\":\"=你是一个加密货币期货交易分析师,专注清算驱动的顺势交易策略。\\n\\n## 核心逻辑\\n连续单边爆仓说明该方向弱势仓位正被清出,清算后趋势往往延续。判断这次爆仓是否值得顺势跟进。\\n\\n## 输入数据\\n币种:{{$json.symbol}}\\n建议方向:{{$json.direction}}(SHORT=顺势做空 | LONG=顺势做多)\\nZ-Score:{{$json.zScore}}(>2.5为异常,越高越显著)\\n方向纯度(多头爆仓占比):{{$json.longRatio}}(>0.75=多头主爆→做空 | <0.25=空头主爆→做多)\\n近5分钟爆仓量:${{$json.recentK}}K USDT\\n历史5分钟均值:${{$json.meanK}}K USDT\\nK线(30根1分钟):{{$json.kline ? ('涨跌幅:' + $json.kline.change + '% | 波动率:' + $json.kline.volatility + '% | K线趋势:' + $json.kline.trend + ' | 与爆仓方向一致:' + $json.kline.aligned) : '无数据'}}\\n最新新闻:\\n{{$json.news && $json.news.length > 0 ? $json.news.map((n,i)=>`${i+1}.[${n.age}] ${n.title} — ${n.desc}`).join('\\\\n') : '无相关新闻'}}\\n\\n## 判断步骤\\n\\n**Step 1:爆仓强度**\\n- Z-Score>3 且 longRatio>0.80 或 <0.20 → 强信号\\n- Z-Score 2.5~3 且 longRatio>0.75 或 <0.25 → 中等信号\\n- 否则不入场\\n\\n**Step 2:K线趋势**\\n- aligned=true → 趋势延续概率高,加分\\n- aligned=false → 可能短暂清算而非趋势,减分\\n\\n**Step 3:新闻验证**\\n- 有实质利空/利多且与方向一致 → 加分(有基本面支撑)\\n- 无新闻 → 纯技术清算,降低置信度\\n- 新闻与方向相反 → 不入场\\n\\n## 决策矩阵\\n| 爆仓强度 | K线一致 | 新闻 | 决策 |\\n|---------|---------|------|------|\\n| 强 | 是 | 有 | 入场,置信度=高 |\\n| 强 | 是 | 无 | 入场,置信度=中 |\\n| 强 | 否 | 有 | 观望 |\\n| 中 | 是 | 有 | 入场,置信度=中 |\\n| 中 | 否或无 | - | 不入场 |\\n\\n## 输出格式\\n严格返回JSON,无任何markdown包裹:\\n{\\n \\\"symbol\\\": \\\"币种\\\",\\n \\\"direction\\\": \\\"LONG或SHORT\\\",\\n \\\"action\\\": \\\"入场/观望/不入场\\\",\\n \\\"confidence\\\": \\\"高/中/低\\\",\\n \\\"liq_note\\\": \\\"爆仓信号一句话评估\\\",\\n \\\"trend_note\\\": \\\"K线趋势一句话评估\\\",\\n \\\"news_note\\\": \\\"新闻一句话评估(无新闻写'无新闻,纯技术信号')\\\",\\n \\\"action_reason\\\": \\\"综合决策理由一句话\\\"\\n}\",\"options\":{}},\"id\":\"f167b982-048f-487f-b3d0-6f44b03ff3b1\",\"name\":\"AI判断\",\"type\":\"@n8n/n8n-nodes-langchain.agent\",\"typeVersion\":1,\"position\":[240,576],\"retryOnFail\":true},{\"parameters\":{\"model\":{\"__rl\":true,\"value\":\"deepseek/deepseek-v3.2\",\"mode\":\"list\",\"cachedResultName\":\"deepseek/deepseek-v3.2\"}},\"id\":\"bef7ecbb-a735-4877-901d-e8dc61e9d6dc\",\"name\":\"LLM模型\",\"type\":\"n8n-nodes-base.lmOpenAi\",\"typeVersion\":1,\"position\":[240,800],\"credentials\":{\"openAiApi\":{\"id\":\"54d0b567-b3fc-4c6a-b6be-546e0b9cd83f\",\"name\":\"openrouter\"}}}],\"pinData\":{},\"connections\":{\"分批送入AI\":{\"main\":[[{\"node\":\"交易执行\",\"type\":\"main\",\"index\":0}],[{\"node\":\"AI判断\",\"type\":\"main\",\"index\":0}]]},\"触发器\":{\"main\":[[{\"node\":\"初始化收集\",\"type\":\"main\",\"index\":0}]]},\"初始化收集\":{\"main\":[[{\"node\":\"策略执行\",\"type\":\"main\",\"index\":0}]]},\"策略执行\":{\"main\":[[{\"node\":\"异常判断\",\"type\":\"main\",\"index\":0}]]},\"异常判断\":{\"main\":[[{\"node\":\"数据补充\",\"type\":\"main\",\"index\":0}]]},\"数据补充\":{\"main\":[[{\"node\":\"分批送入AI\",\"type\":\"main\",\"index\":0}]]},\"AI判断\":{\"main\":[[{\"node\":\"分批送入AI\",\"type\":\"main\",\"index\":0}]]},\"LLM模型\":{\"ai_languageModel\":[[{\"node\":\"AI判断\",\"type\":\"ai_languageModel\",\"index\":0}]]}},\"active\":false,\"settings\":{\"timezone\":\"Asia/Shanghai\",\"executionOrder\":\"v1\"},\"tags\":[],\"credentials\":{},\"id\":\"d4796d16-37cf-43b1-98c4-f82bea665d20\",\"plugins\":{},\"mcpClients\":{}},\"startNodes\":[],\"triggerToStartFrom\":{\"name\":\"触发器\"}}"}