清算地图顺势策略


创建日期: 2026-03-10 17:20:25 最后修改: 2026-03-12 11:32:35
复制: 0 点击次数: 0
avatar of ianzeng123 ianzeng123
2
关注
413
关注者
策略源码
{"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\":\"触发器\"}}"}