本策略是一套运行在币安永续合约上的多因子多空对冲量化系统。核心思路是利用 AI 大语言模型(LLM)自动挖掘、验证和迭代量价因子,通过 Rank IC(秩信息系数)严格筛选具有真实预测力的因子,加权合成后输出多空信号并自动执行调仓,全程无需人工干预因子设计。
核心优势在于:因子池不是写死的,而是由 AI 根据市场状态持续探索新维度、淘汰失效因子、自适应进化,真正实现”因子自己会拟合”的量化策略。
① 标的池筛选 + 市场状态检测(小时级别)
自动拉取全市场 USDT 永续合约行情,按成交额排序筛选前 N 个主流币种构建标的池。同时分析 BTC 近期波动率分位数,将市场划分为四种状态:正常(normal)、高波动(high_vol)、低波动(low_vol)、突变(volatile),供后续因子生成和信号计算参考。
② AI 因子挖掘 + 自适应迭代(核心)
使用大语言模型作为因子研究员,每轮迭代完成以下闭环:
③ 多因子信号合成 + 调仓执行
对标的池中所有币种计算各因子的截面 Z-score,以近期 IC 为权重加权合成综合评分。评分超过做多阈值的前 N 个币种做多,低于做空阈值的前 N 个做空,自动执行开平仓。不在目标持仓中的旧仓位自动平掉。
④ 实时持仓监控 + 风控(秒级别)
快速循环监控所有持仓的浮盈浮亏,内置三重退出机制:
⑤ 可视化仪表板
策略运行后实时展示三张可交互表格:
| 参数 | 说明 | 默认值 |
|---|---|---|
| topN | 成交额过滤币种数量 | 40 |
| klineLen | 历史K线长度(用于IC回测) | 500 |
| btcChangePct | BTC突变阈值(%) | 5 |
| icThreshold | IC有效性阈值(低于此值淘汰) | 0.02 |
| initFactorCount | 初始因子数量 | 10 |
| icWindowLen | 滚动IC窗口期数 | 48 |
| icDropThreshold | IC下滑警戒值 | 0.01 |
| icDecayWindow | 近期IC评估窗口期数 | 48 |
| minFactorCount | 最少有效因子数 | 3 |
| icDecayThreshold | IC衰减警戒阈值 | -0.01 |
| corrThreshold | 因子相关性过滤阈值 | 0.7 |
| longShortN | 多空各选币种数 | 5 |
| positionRatio | 总仓位比例 | 0.5 |
| stopLossPct | 单仓止损比例(%) | 8 |
| maxLeverage | 最大杠杆倍数 | 3 |
| takeProfitPct | 单仓止盈比例(%) | 20 |
| shortThreshold | 做空评分阈值 | -0.3 |
| longThreshold | 做多评分阈值 | 0.3 |
{"type":"n8n","content":"{\"workflowData\":{\"nodes\":[{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"const STOP_LOSS_PCT = $vars.stopLossPct || 5;\\nconst TAKE_PROFIT_PCT = $vars.takeProfitPct || 10;\\nconst TRAIL_TRIGGER = 3;\\n\\nconst nowMin = new Date().getMinutes();\\nif (nowMin >= 59) {\\n Log('⏸️ 第' + nowMin + '分钟,跳过本轮(等待慢触发器执行)');\\n return [{ json: { success: true, skipped: true, ts: new Date().toISOString() } }];\\n}\\n\\nfunction fmtIC(val) {\\n if (val === 'N/A' || val === undefined) return 'N/A';\\n const v = parseFloat(val);\\n if (v >= 0.05) return '🟢 ' + val;\\n if (v >= 0.02) return '🟡 ' + val;\\n if (v >= 0) return '⚪ ' + val;\\n return '🔴 ' + val;\\n}\\nfunction fmtTrend(rec, avg) {\\n if (rec === 'N/A' || avg === 'N/A') return '➖';\\n const diff = parseFloat(rec) - parseFloat(avg);\\n if (diff > 0.005) return '📈 ↑';\\n if (diff < -0.005) return '📉 ↓';\\n return '➡️ →';\\n}\\nfunction fmtMarket(state) {\\n const map = { normal:'🌤️ normal', high_vol:'⛈️ high_vol', low_vol:'😴 low_vol', volatile:'🌪️ volatile', unknown:'❓ unknown' };\\n return map[state] || state;\\n}\\nfunction fmtPnl(pct) {\\n const v = parseFloat(pct);\\n if (v > 5) return '🚀 +' + v.toFixed(2) + '%';\\n if (v > 0) return '✅ +' + v.toFixed(2) + '%';\\n if (v > -3) return '🟡 ' + v.toFixed(2) + '%';\\n return '🔴 ' + v.toFixed(2) + '%';\\n}\\nfunction fmtProfit(p) {\\n return (p >= 0 ? '💰 +$' : '💸 -$') + Math.abs(p).toFixed(2);\\n}\\n\\nfunction getDynamicTrailDrawdown(maxPnl) {\\n if (maxPnl >= 7) return 3;\\n if (maxPnl >= 4) return 2;\\n return 1.5;\\n}\\n\\nfunction handleCommand() {\\n const cmd = GetCommand(); if (!cmd) return;\\n const parts = cmd.split(':');\\n if (parts[0]==='重置') { LogReset(); _G('afi_initmoney',null); Log('🔄 已重置账户基准'); }\\n else if (parts[0]==='清空因子池') { _G('afi_factorPool',JSON.stringify([])); _G('afi_icHistory',JSON.stringify({})); Log('🗑️ 因子池已清空'); }\\n else if (parts[0]==='手动平仓') {\\n const coin=parts[1]; if(!coin) return;\\n try {\\n const poss=exchange.GetPositions()||[];\\n for (const pos of poss) {\\n if (pos.Symbol.indexOf(coin)!==-1 && Math.abs(pos.Amount)>0) {\\n const isLong=pos.Type===PD_LONG||pos.Type===0;\\n exchange.CreateOrder(pos.Symbol, isLong ? 'closebuy' : 'closesell', -1, Math.abs(pos.Amount));\\n _G(coin+'_maxpnl',null); _G(coin+'_trail',null);\\n Log('✅ ' + coin + ' 手动平仓成功');\\n }\\n }\\n } catch(e){ Log('❌ ' + coin + ' 手动平仓失败:', e.message); }\\n }\\n}\\n\\nfunction monitorTPSL(positions, tickers) {\\n for (const pos of (positions||[])) {\\n if (Math.abs(pos.Amount)===0) continue;\\n const cm=pos.Symbol.match(/^(.+)_USDT/); if(!cm) continue;\\n const coin=cm[1]; const ticker=tickers[coin+'_USDT.swap']; if(!ticker) continue;\\n const isLong=pos.Type===PD_LONG||pos.Type===0;\\n const cur=ticker.Last; const ent=pos.Price; const amt=Math.abs(pos.Amount);\\n const pnlPct=(cur-ent)*(isLong?1:-1)/ent*100;\\n const maxKey=coin+'_maxpnl'; const trailKey=coin+'_trail';\\n let maxPnl=_G(maxKey); if(maxPnl===null){maxPnl=pnlPct;_G(maxKey,maxPnl);}\\n else if(pnlPct>maxPnl){maxPnl=pnlPct;_G(maxKey,maxPnl);}\\n if(!_G(trailKey)&&maxPnl>=TRAIL_TRIGGER){\\n _G(trailKey,true);\\n Log('🎯 ' + coin + ' 启动移动止盈,浮盈: +' + pnlPct.toFixed(2) + '%');\\n }\\n const trailDrawdown = getDynamicTrailDrawdown(maxPnl);\\n let reason=null;\\n if(_G(trailKey)&&(maxPnl-pnlPct)>=trailDrawdown) reason='📉 移动止盈(回撤'+(maxPnl-pnlPct).toFixed(2)+'%, 阈值'+trailDrawdown+'%)';\\n if(!reason&&pnlPct<=-STOP_LOSS_PCT) reason='🛑 止损('+pnlPct.toFixed(2)+'%)';\\n if(!reason&&pnlPct>=TAKE_PROFIT_PCT) reason='🎉 止盈('+pnlPct.toFixed(2)+'%)';\\n if(reason){\\n try{\\n exchange.CreateOrder(pos.Symbol, isLong ? 'closebuy' : 'closesell', -1, amt);\\n Log(coin, '触发', reason); _G(maxKey,null); _G(trailKey,null);\\n }catch(e){ Log('❌ ' + coin + ' 自动平仓失败:', e.message); }\\n }\\n }\\n}\\n\\nhandleCommand();\\nconst account=exchange.GetAccount();\\nconst positions=exchange.GetPositions()||[];\\nconst tickers={};\\ntry{const ts=exchange.GetTickers()||[];for(const t of ts)tickers[t.Symbol]=t;}catch(e){}\\nmonitorTPSL(positions,tickers);\\nconst initMoney=_G('afi_initmoney')||account.Balance;\\nconst equity=account.Equity||account.Balance;\\nconst profit=equity-initMoney;\\nconst pct=(profit/initMoney*100).toFixed(2);\\nconst factorPool=JSON.parse(_G('afi_factorPool')||'[]');\\nconst icHistory=JSON.parse(_G('afi_icHistory')||'{}');\\nLogProfit(profit,'&');\\n\\nconst marketState = _G('afi_marketState')||'unknown';\\nconst t1={\\n type:'table', title:'📊 账户概览',\\n cols:['💵 初始资金','💰 当前权益','📈 总收益','📊 收益率','🌤️ 市场状态','🧬 因子数','🔄 迭代次数','⚙️ 操作'],\\n rows:[[\\n '$'+initMoney.toFixed(2),\\n '$'+equity.toFixed(2),\\n fmtProfit(profit),\\n (parseFloat(pct)>=0?'🟢 +':'🔴 ')+pct+'%',\\n fmtMarket(marketState),\\n '🧬 '+factorPool.length+'个',\\n '🔄 '+(_G('afi_iterateCount')||0)+'次',\\n [{type:'button',cmd:'重置',name:'🔄 重置'},{type:'button',cmd:'清空因子池',name:'🗑️ 清空因子池'}]\\n ]]\\n};\\n\\nconst t2={\\n type:'table', title:'🧬 因子池状态',\\n cols:['🏷️ 因子名','📊 历史IC','📉 近期IC(20)','📈 IC趋势','🔗 相关因子','🧭 方向','💡 逻辑说明'],\\n rows: factorPool.length===0\\n ? [['⏳ 暂无因子','-','-','-','-','-','等待LLM生成']]\\n : factorPool.map(f=>{\\n const arr=icHistory[f.name]||[];\\n const avgVal=arr.length>0?(arr.reduce((a,b)=>a+b,0)/arr.length):null;\\n const recVal=arr.length>=20?(arr.slice(-20).reduce((a,b)=>a+b,0)/20):null;\\n const avg=avgVal!==null?avgVal.toFixed(4):'N/A';\\n const rec=recVal!==null?recVal.toFixed(4):'N/A';\\n const trend=fmtTrend(rec,avg);\\n const nameDisplay=(f.isDecaying?'⚠️ ':'')+f.name;\\n const corrDisplay=f.corrGroup&&f.corrGroup!==f.name?('🔗 '+f.corrGroup):'-';\\n const dirDisplay=f.direction===1?'🟢 正向':'🔴 反向';\\n return[nameDisplay, fmtIC(avg), fmtIC(rec), trend, corrDisplay, dirDisplay, f.rationale||'-'];\\n })\\n};\\n\\nconst t3={\\n type:'table', title:'📋 持仓监控',\\n cols:['🪙 币种','📍 方向','🏷️ 入场价','💲 现价','📊 浮盈%','🏆 最高浮盈%','🎯 移动止盈','⚙️ 操作'],\\n rows:[]\\n};\\nlet hasPos=false;\\nfor(const pos of positions){\\n if(Math.abs(pos.Amount)===0) continue;\\n const cm=pos.Symbol.match(/^(.+)_USDT/); if(!cm) continue;\\n const coin=cm[1]; const tk=tickers[coin+'_USDT.swap']; if(!tk) continue;\\n hasPos=true;\\n const isLong=pos.Type===PD_LONG||pos.Type===0;\\n const cur=tk.Last; const ent=pos.Price;\\n const pnlPct=(cur-ent)*(isLong?1:-1)/ent*100;\\n const maxPnl=_G(coin+'_maxpnl')||pnlPct;\\n const trailDrawdown=getDynamicTrailDrawdown(maxPnl);\\n t3.rows.push([\\n '🪙 '+coin,\\n isLong?'🟢 多':'🔴 空',\\n ent.toFixed(6),\\n cur.toFixed(6),\\n fmtPnl(pnlPct),\\n fmtPnl(maxPnl),\\n _G(coin+'_trail')?('🎯 回撤阈值'+trailDrawdown+'%'):'⏸️ 未启用',\\n [{type:'button',cmd:'手动平仓:'+coin,name:'平仓'}]\\n ]);\\n}\\nif(!hasPos) t3.rows=[['😴 当前无持仓','-','-','-','-','-','等待信号','-']];\\n\\nLogStatus('`'+JSON.stringify(t1)+'`\\\\n\\\\n`'+JSON.stringify(t2)+'`\\\\n\\\\n`'+JSON.stringify(t3)+'`');\\nreturn [{ json: { success: true, ts: new Date().toISOString() } }];\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[576,112],\"id\":\"460fd9a2-80f9-4b8b-88c6-48558f9e90c2\",\"name\":\"持仓监控+仪表板\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"const input = $input.first().json;\\nconst longList = input.longList || [];\\nconst shortList = input.shortList || [];\\nif (longList.length === 0 && shortList.length === 0) { Log('无信号,跳过调仓'); return [{ json: { ...input, rebalanced: false } }]; }\\nconst positionRatio = $vars.positionRatio || 0.8;\\nconst maxLeverage = $vars.maxLeverage || 3;\\nconst account = exchange.GetAccount();\\nconst equity = account.Equity || account.Balance;\\nconst balance = account.Balance;\\nconst perAmt = (equity * positionRatio) / (longList.length + shortList.length);\\nLog('开始调仓 | 权益:', equity.toFixed(2), '| 可用余额:', balance.toFixed(2), '| 单仓:', perAmt.toFixed(2), '| 多:', longList.length, '空:', shortList.length);\\nlet positions = [];\\ntry { positions = exchange.GetPositions() || []; } catch(e) {}\\nconst currentHoldings = {};\\nfor (const pos of positions) { if (Math.abs(pos.Amount) > 0) currentHoldings[pos.Symbol] = pos; }\\nconst targetSet = new Set([...longList, ...shortList]);\\nfor (const sym of Object.keys(currentHoldings)) {\\n if (!targetSet.has(sym)) {\\n try {\\n const pos = currentHoldings[sym];\\n const isLong = pos.Type === PD_LONG || pos.Type === 0;\\n const side = isLong ? 'closebuy' : 'closesell';\\n const id = exchange.CreateOrder(sym, side, -1, Math.abs(pos.Amount));\\n Log('平仓:', sym, isLong ? '多' : '空', '订单ID:', id);\\n\\n // 添加这两行\\n const cm = sym.match(/^(.+)_USDT/);\\n if (cm) { _G(cm[1] + '_maxpnl', null); _G(cm[1] + '_trail', null); }\\n Sleep(200);\\n } catch(e) { Log('平仓失败:', sym, e.message); }\\n }\\n}\\nSleep(500);\\nlet allMarkets = {};\\ntry { allMarkets = exchange.GetMarkets() || {}; } catch(e) {}\\nfunction openPos(sym, isLong) {\\n if (currentHoldings[sym]) return;\\n try {\\n const market = allMarkets[sym];\\n if (!market) { Log('跳过:', sym, '无法获取市场信息'); return; }\\n exchange.SetMarginLevel(sym, maxLeverage);\\n const ticker = exchange.GetTicker(sym); if (!ticker) return;\\n const price = ticker.Last;\\n const ctVal = (market.CtVal && market.CtVal > 0) ? market.CtVal : 1;\\n const amtPrec = (market.AmountPrecision !== undefined) ? market.AmountPrecision : 0;\\n const minQty = (market.MinQty && market.MinQty > 0) ? market.MinQty : 1;\\n const maxQty = (market.MaxQty && market.MaxQty > 0) ? market.MaxQty : 999999;\\n let qty = _N(perAmt / price / ctVal, amtPrec);\\n qty = Math.min(qty, maxQty);\\n if (qty < minQty) { Log('数量不足最小下单量,跳过:', sym, qty, '<', minQty); return; }\\n const side = isLong ? 'buy' : 'sell';\\n const id = exchange.CreateOrder(sym, side, -1, qty);\\n Log(isLong ? '开多' : '开空', sym, '价格:', price, 'CtVal:', ctVal, '数量:', qty, '订单ID:', id);\\n // ✅ 在这里也要加清除\\n const cm = sym.match(/^(.+)_USDT/);\\n if (cm) { _G(cm[1] + '_maxpnl', null); _G(cm[1] + '_trail', null); }\\n Sleep(200);\\n } catch(e) { Log(isLong ? '开多' : '开空', '失败:', sym, e.message); }\\n}\\nfor (const sym of longList) openPos(sym, true);\\nfor (const sym of shortList) openPos(sym, false);\\n_G('afi_lastRebalance', Date.now());\\nLog('调仓完成');\\nreturn [{ json: { ...input, rebalanced: true } }];\\n\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[1808,-224],\"id\":\"e995a600-e5ef-45d7-adec-b7045235c2a3\",\"name\":\"执行调仓\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"// ============================================================\\n// 修复1: 线上IC追踪时序对齐\\n// 用 t-1 期因子值预测 t 期收益\\n// 即用 records[0..n-2] 算因子,预测 records[n-1] vs records[n-2] 的涨跌\\n// 与 calcRankICFull 验证逻辑保持一致\\n// ============================================================\\nconst input = $input.first().json;\\nconst symbols = JSON.parse(_G('afi_symbolPool') || '[]');\\nconst factorPool = JSON.parse(_G('afi_factorPool') || '[]');\\nif (factorPool.length === 0) { Log('无有效因子,跳过'); return []; }\\nif (symbols.length === 0) { Log('标的池为空'); return []; }\\nLog('计算信号 | 因子数:', factorPool.length, '| 标的:', symbols.length);\\nconst allRecords = {};\\nfor (let i = 0; i < symbols.length; i++) {\\n try {\\n const records = exchange.GetRecords(symbols[i], PERIOD_H1);\\n if (records && records.length >= 30) allRecords[symbols[i]] = records.slice(-80);\\n } catch(e) { Log('K线获取失败:', symbols[i], e.message); }\\n if (i % 20 === 0) Sleep(300);\\n}\\nconst validSyms = Object.keys(allRecords);\\n\\n// 修复1: 用 records[0..n-2] 计算因子值(去掉最后一根K线)\\n// 这样因子值是基于 t-1 时刻的信息,预测 t 时刻的涨跌\\nconst rawMatrix = {};\\nfor (const sym of validSyms) {\\n rawMatrix[sym] = {};\\n const fullRecords = allRecords[sym];\\n // 去掉最后一根,模拟 t-1 时刻的信息集\\n const records = fullRecords.slice(0, fullRecords.length - 1);\\n const n = records.length;\\n for (const f of factorPool) {\\n try {\\n const v = (function(){ return eval(f.code); })();\\n rawMatrix[sym][f.name] = isNaN(v)||!isFinite(v)?null:v;\\n } catch(e) { rawMatrix[sym][f.name] = null; }\\n }\\n}\\n\\nfunction zscore(fname) {\\n const vals = validSyms.map(s=>({sym:s,val:rawMatrix[s][fname]})).filter(x=>x.val!==null);\\n if (vals.length < 5) return {};\\n const mean = vals.reduce((a,b)=>a+b.val,0)/vals.length;\\n const std = Math.sqrt(vals.reduce((a,b)=>a+(b.val-mean)**2,0)/vals.length);\\n const r = {}; vals.forEach(x=>r[x.sym]=std>0?(x.val-mean)/std:0);\\n return r;\\n}\\n\\n// 修复1: IC追踪也用时序正确的方式\\n// t-1期因子值 vs t期实际收益(最后一根K线 vs 倒数第二根)\\nconst icHistory = JSON.parse(_G('afi_icHistory') || '{}');\\nfor (const f of factorPool) {\\n const fVals = []; const nRets = [];\\n for (const sym of validSyms) {\\n const v = rawMatrix[sym][f.name]; // t-1期因子值\\n const fullRecords = allRecords[sym];\\n const nn = fullRecords.length;\\n if (v === null || nn < 2) continue;\\n fVals.push({ sym, val: v });\\n // t期实际收益:最后一根 vs 倒数第二根\\n nRets.push({ sym, ret: (fullRecords[nn-1].Close - fullRecords[nn-2].Close) / fullRecords[nn-2].Close });\\n }\\n if (fVals.length < 8) continue;\\n const fRank = {}; [...fVals].sort((a,b)=>a.val-b.val).forEach((x,i)=>fRank[x.sym]=i);\\n const rRank = {}; [...nRets].sort((a,b)=>a.ret-b.ret).forEach((x,i)=>rRank[x.sym]=i);\\n const ss = fVals.map(x=>x.sym);\\n const fr = ss.map(s=>fRank[s]); const rr = ss.map(s=>rRank[s]);\\n const n2 = ss.length;\\n const fm = fr.reduce((a,b)=>a+b,0)/n2; const rm = rr.reduce((a,b)=>a+b,0)/n2;\\n const num = fr.map((f2,i)=>(f2-fm)*(rr[i]-rm)).reduce((a,b)=>a+b,0);\\n const den = Math.sqrt(fr.map(f2=>(f2-fm)**2).reduce((a,b)=>a+b,0)*rr.map(r=>(r-rm)**2).reduce((a,b)=>a+b,0));\\n if (den > 0) {\\n const ic = num / den;\\n if (!icHistory[f.name]) icHistory[f.name] = [];\\n icHistory[f.name].push(ic);\\n if (icHistory[f.name].length > 500) icHistory[f.name] = icHistory[f.name].slice(-500);\\n }\\n}\\n_G('afi_icHistory', JSON.stringify(icHistory));\\n\\n// 因子权重:用近期IC加权(近期权重更高,衰减因子用ICIR)\\nconst weights = {}; let totalW = 0;\\nfor (const f of factorPool) {\\n const arr = icHistory[f.name]||[];\\n if (arr.length === 0) { weights[f.name] = 0.01; totalW += 0.01; continue; }\\n // 近48期IC均值(衰减监控用)\\n const recentArr = arr.slice(-48);\\n const recentIC = recentArr.reduce((a,b)=>a+b,0) / recentArr.length;\\n // 只用近期正IC贡献权重,负IC因子权重置0\\n const w = Math.max(0, recentIC);\\n weights[f.name] = w;\\n totalW += w;\\n}\\nif (totalW > 0) Object.keys(weights).forEach(k=>weights[k]/=totalW);\\nelse factorPool.forEach(f=>weights[f.name]=1/factorPool.length);\\n\\nconst scores = {};\\nfor (const sym of validSyms) {\\n let score=0; let covered=0;\\n for (const f of factorPool) {\\n const z = zscore(f.name)[sym];\\n if (z!==undefined) { score+=weights[f.name]*f.direction*z; covered++; }\\n }\\n if (covered>0) scores[sym]=score;\\n}\\nconst sorted = Object.keys(scores).sort((a,b)=>scores[b]-scores[a]);\\nconst longShortN = $vars.longShortN || 5;\\nconst longThreshold = $vars.longThreshold || 0.3;\\nconst shortThreshold = $vars.shortThreshold || -0.3;\\nconst longCandidates = sorted.filter(s => scores[s] >= longThreshold);\\nconst shortCandidates = sorted.slice().reverse().filter(s => scores[s] <= shortThreshold);\\nconst longList = longCandidates.slice(0, longShortN);\\nconst shortList = shortCandidates.slice(0, longShortN);\\nconst longSet = new Set(longList);\\nconst shortListFinal = shortList.filter(s => !longSet.has(s));\\nconst allScores = sorted.map(s => scores[s]);\\nconst avgScore = allScores.reduce((a,b)=>a+b,0) / allScores.length;\\nLog('市场平均评分:', avgScore.toFixed(3), '| 多头候选:', longCandidates.length, '空头候选:', shortCandidates.length);\\n_G('afi_lastScores', JSON.stringify(scores));\\n_G('afi_lastSignalTime', Date.now());\\nLog('做多:', longList.join(',') || '无', '| 做空:', shortListFinal.join(',') || '无');\\nLog('因子权重:', Object.keys(weights).map(k=>k+':'+weights[k].toFixed(3)).join(' | '));\\nreturn [{ json: { ...input, longList, shortList: shortListFinal, scores, weights, avgScore } }];\\n\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[1664,-224],\"id\":\"498e7e09-a19c-45e3-9a18-fc3e7ae2f146\",\"name\":\"计算信号\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"const input = $node['构建Prompt'].json;\\nif (input.skipLLM) {\\n return [{ json: { ...input, validatedCount: 0 } }];\\n}\\nconst aiOutput = $input.first().json.output || $input.first().json.text || '';\\nLog('🤖 AI输出前200字:', aiOutput.substring(0, 200));\\nfunction parseAI(output) {\\n try {\\n const cleaned = output.replace(/```[a-z]*\\\\n?/gi, '').trim();\\n const match = cleaned.match(/\\\\{[\\\\s\\\\S]*\\\\}/);\\n if (!match) throw new Error('未找到JSON');\\n return JSON.parse(match[0]);\\n } catch(e) { Log('❌ AI解析失败:', e.message); return null; }\\n}\\nconst result = parseAI(aiOutput);\\nconst newFactors = (result && result.factors) ? result.factors : [];\\nLog('🤖 AI生成新因子', newFactors.length, '个:', newFactors.map(f=>f.name).join(', '));\\n\\nconst icThreshold = $vars.icThreshold || 0.02;\\nconst klineLen = $vars.klineLen || 500;\\nconst targetFactorCount = $vars.targetFactorCount || 10;\\nconst icDecayWindow = $vars.icDecayWindow || 48;\\nconst icDecayThreshold = $vars.icDecayThreshold || -0.01;\\nconst corrThreshold = $vars.corrThreshold || 0.7;\\n\\nconst symbols = JSON.parse(_G('afi_symbolPool') || '[]').slice(0, 40);\\nLog('📥 拉取K线,使用', symbols.length, '个币种...');\\nconst allRecords = {};\\nfor (let i = 0; i < symbols.length; i++) {\\n try {\\n const records = exchange.GetRecords(symbols[i], PERIOD_H1);\\n if (records && records.length >= 60) allRecords[symbols[i]] = records.slice(-Math.min(klineLen, records.length));\\n } catch(e) { Log('❌ K线获取失败:', symbols[i], e.message); }\\n if (i % 10 === 0) Sleep(300);\\n}\\nconst symCount = Object.keys(allRecords).length;\\nLog('📊 K线获取完成:', symCount, '个币种');\\nif (symCount > 0) {\\n const lengths = Object.values(allRecords).map(r => r.length);\\n const minL = Math.min(...lengths), maxL = Math.max(...lengths);\\n const avgL = Math.round(lengths.reduce((a,b)=>a+b,0)/lengths.length);\\n Log('📏 K线长度 | 最短:', minL, '| 最长:', maxL, '| 平均:', avgL);\\n}\\n\\nfunction calcRankICFull(code, symRecords, factorName) {\\n const syms = Object.keys(symRecords);\\n const icList = [];\\n const minLen = 30;\\n const allLengths = syms.map(s => symRecords[s].length);\\n const minSymLen = Math.min(...allLengths);\\n const testLen = Math.min(500, minSymLen - 1);\\n Log(' 📐 [' + factorName + '] testLen:', testLen, '| 可迭代次数:', Math.max(0, testLen - minLen));\\n if (testLen <= minLen) {\\n Log(' ❌ [' + factorName + '] K线不足,无法计算IC');\\n return { avgIC: 0, icList: [] };\\n }\\n let evalErrorCount = 0, evalErrorSample = '', nanCount = 0, thinSliceCount = 0, distinctCheck = false;\\n for (let t = minLen; t < testLen; t++) {\\n const fVals = []; const nRets = [];\\n for (const sym of syms) {\\n const fullRecords = symRecords[sym];\\n const records = fullRecords.slice(0, t);\\n const n = records.length;\\n if (n < minLen) continue;\\n try {\\n const v = (function() {\\n try { return eval(code); }\\n catch(e) { if(evalErrorCount===0) evalErrorSample=e.message; evalErrorCount++; return NaN; }\\n })();\\n if (isNaN(v) || !isFinite(v)) { nanCount++; continue; }\\n fVals.push({ sym, val: v });\\n nRets.push({ sym, ret: (fullRecords[t].Close - fullRecords[t-1].Close) / fullRecords[t-1].Close });\\n } catch(e) { evalErrorCount++; }\\n }\\n if (fVals.length < 8) { thinSliceCount++; continue; }\\n if (!distinctCheck) {\\n distinctCheck = true;\\n const uniqueVals = new Set(fVals.map(x=>x.val));\\n if (uniqueVals.size < 3) Log(' ⚠️ [' + factorName + '] 因子值无区分度,唯一值:', uniqueVals.size);\\n }\\n const fRank = {}; [...fVals].sort((a,b)=>a.val-b.val).forEach((x,i)=>fRank[x.sym]=i);\\n const rRank = {}; [...nRets].sort((a,b)=>a.ret-b.ret).forEach((x,i)=>rRank[x.sym]=i);\\n const ss = fVals.map(x=>x.sym);\\n const fr = ss.map(s=>fRank[s]); const rr = ss.map(s=>rRank[s]);\\n const n2 = ss.length;\\n const fm = fr.reduce((a,b)=>a+b,0)/n2; const rm = rr.reduce((a,b)=>a+b,0)/n2;\\n const num = fr.map((f,i)=>(f-fm)*(rr[i]-rm)).reduce((a,b)=>a+b,0);\\n const den = Math.sqrt(fr.map(f=>(f-fm)**2).reduce((a,b)=>a+b,0)*rr.map(r=>(r-rm)**2).reduce((a,b)=>a+b,0));\\n if (den > 0) icList.push(num/den);\\n }\\n const avgIC = icList.length > 0 ? icList.reduce((a,b)=>a+b,0)/icList.length : 0;\\n const icEmoji = avgIC >= 0.05 ? '🟢' : avgIC >= 0.02 ? '🟡' : avgIC >= 0 ? '⚪' : '🔴';\\n Log(' ' + icEmoji + ' [' + factorName + '] IC样本数:', icList.length, '| avgIC:', avgIC.toFixed(4),\\n '| eval错误:', evalErrorCount, '| NaN:', nanCount, '| 样本不足:', thinSliceCount);\\n if (evalErrorCount > 0) Log(' ❌ [' + factorName + '] eval错误:', evalErrorSample, '| code:', code.substring(0,120));\\n if (nanCount > 0 && evalErrorCount === 0) Log(' ⚠️ [' + factorName + '] 含NaN,可能除零 | code:', code.substring(0,120));\\n return { avgIC, icList };\\n}\\n\\nfunction calcFactorScores(code, symRecords) {\\n const syms = Object.keys(symRecords);\\n const scores = {};\\n for (const sym of syms) {\\n const fullRecords = symRecords[sym];\\n const records = fullRecords.slice(0, fullRecords.length - 1);\\n const n = records.length;\\n if (n < 30) continue;\\n try {\\n const v = (function(){ try { return eval(code); } catch(e) { return NaN; } })();\\n if (!isNaN(v) && isFinite(v)) scores[sym] = v;\\n } catch(e) {}\\n }\\n return scores;\\n}\\n\\nfunction calcCorrelation(scoresA, scoresB) {\\n const syms = Object.keys(scoresA).filter(s => scoresB[s] !== undefined);\\n if (syms.length < 10) return 0;\\n const a = syms.map(s => scoresA[s]);\\n const b = syms.map(s => scoresB[s]);\\n const n = syms.length;\\n const ma = a.reduce((x,y)=>x+y,0)/n;\\n const mb = b.reduce((x,y)=>x+y,0)/n;\\n const num = a.map((v,i)=>(v-ma)*(b[i]-mb)).reduce((x,y)=>x+y,0);\\n const den = Math.sqrt(a.map(v=>(v-ma)**2).reduce((x,y)=>x+y,0) * b.map(v=>(v-mb)**2).reduce((x,y)=>x+y,0));\\n return den > 0 ? num/den : 0;\\n}\\n\\nlet factorPool = JSON.parse(_G('afi_factorPool') || '[]');\\nconst icHistory = JSON.parse(_G('afi_icHistory') || '{}');\\n\\n// ── 验证现有因子 ──────────────────────────────────────────────\\nLog('🔬 ═══ 验证现有因子: ' + factorPool.length + ' 个 ═══');\\nconst survivedFactors = [];\\nfor (const factor of factorPool) {\\n try {\\n const { avgIC, icList } = calcRankICFull(factor.code, allRecords, factor.name);\\n if (avgIC > icThreshold) {\\n icHistory[factor.name] = icList.slice(-500);\\n const recentArr = icList.slice(-icDecayWindow);\\n const recentIC = recentArr.length > 0 ? recentArr.reduce((a,b)=>a+b,0)/recentArr.length : avgIC;\\n const isDecaying = recentIC < icDecayThreshold;\\n if (isDecaying) {\\n Log(' ⚠️ 衰减预警:', factor.name, '历史IC=' + avgIC.toFixed(4), '近期IC=' + recentIC.toFixed(4), '→ 末位竞争');\\n }\\n survivedFactors.push({ ...factor, icAvg: avgIC, recentIC, isDecaying });\\n Log(' ✅ 保留:', factor.name, 'IC=' + avgIC.toFixed(4), '近期IC=' + recentIC.toFixed(4), isDecaying ? '⚠️ [衰减]' : '');\\n } else {\\n icHistory[factor.name] = icList.slice(-500);\\n Log(' ❌ 删除:', factor.name, 'IC=' + avgIC.toFixed(4), '< 阈值', icThreshold);\\n }\\n } catch(e) { Log(' 💥 验证异常:', factor.name, e.message); }\\n Sleep(100);\\n}\\n\\n// ── 验证新因子 ────────────────────────────────────────────────\\nconst maxNew = input.explorationCount || 3;\\nlet newValidCount = 0;\\nif (newFactors.length > 0) {\\n Log('🧪 ═══ 验证新因子: ' + newFactors.length + ' 个(上限接受 ' + maxNew + ' 个)═══');\\n const newCandidates = [];\\n for (const factor of newFactors) {\\n if (survivedFactors.some(f => f.name === factor.name)) {\\n Log(' ⏭️ 跳过重复因子:', factor.name);\\n continue;\\n }\\n try {\\n const { avgIC, icList } = calcRankICFull(factor.code, allRecords, factor.name);\\n if (avgIC > icThreshold) {\\n const recentArr = icList.slice(-icDecayWindow);\\n const recentIC = recentArr.length > 0 ? recentArr.reduce((a,b)=>a+b,0)/recentArr.length : avgIC;\\n newCandidates.push({\\n name: factor.name, rationale: factor.rationale,\\n code: factor.code, direction: factor.direction || 1,\\n icAvg: avgIC, recentIC, icList, isDecaying: false,\\n addedAt: new Date().toISOString(),\\n source: input.action || 'unknown',\\n type: factor.type || 'exploration'\\n });\\n Log(' 🌟 候选新因子:', factor.name, 'IC=' + avgIC.toFixed(4));\\n } else {\\n Log(' ❌ 新因子淘汰:', factor.name, 'IC=' + avgIC.toFixed(4), '< 阈值', icThreshold);\\n }\\n } catch(e) { Log(' 💥 验证异常:', factor.name, e.message); }\\n Sleep(100);\\n }\\n newCandidates.sort((a, b) => b.icAvg - a.icAvg);\\n for (const candidate of newCandidates.slice(0, maxNew)) {\\n icHistory[candidate.name] = candidate.icList.slice(-500);\\n survivedFactors.push(candidate);\\n newValidCount++;\\n Log(' ✅ 新因子入池:', candidate.name, 'IC=' + candidate.icAvg.toFixed(4));\\n }\\n if (newCandidates.length > maxNew) {\\n Log(' 🗑️ 超出上限,丢弃较弱因子:', newCandidates.slice(maxNew).map(f=>f.name).join(', '));\\n }\\n}\\n\\n// ── 相关性过滤 ────────────────────────────────────────────────\\nLog('🔗 ═══ 相关性过滤,阈值: ' + corrThreshold + ' ═══');\\nsurvivedFactors.sort((a, b) => b.icAvg - a.icAvg);\\nconst factorScoresMap = {};\\nfor (const f of survivedFactors) {\\n factorScoresMap[f.name] = calcFactorScores(f.code, allRecords);\\n}\\nconst decorrelatedFactors = [];\\nfor (const factor of survivedFactors) {\\n let isRedundant = false;\\n for (const selected of decorrelatedFactors) {\\n const corr = Math.abs(calcCorrelation(factorScoresMap[factor.name], factorScoresMap[selected.name]));\\n if (corr > corrThreshold) {\\n Log(' 🔗 过滤:', factor.name, '与', selected.name, '相关系数=' + corr.toFixed(3), '> 阈值,丢弃');\\n isRedundant = true;\\n // 修复: corrGroup只记录被吸收的相关因子名,不包含自身\\n selected.corrGroup = (selected.corrGroup ? selected.corrGroup + ',' : '') + factor.name;\\n break;\\n }\\n }\\n if (!isRedundant) {\\n // 修复: corrGroup初始化为空字符串,不是自身名字\\n decorrelatedFactors.push({ ...factor, corrGroup: '' });\\n }\\n}\\nLog('🔗 相关性过滤后剩余:', decorrelatedFactors.length, '个因子');\\n\\n// ── 末位淘汰 ──────────────────────────────────────────────────\\nLog('🏆 ═══ 末位淘汰,目标保留: ' + targetFactorCount + ' 个 ═══');\\ndecorrelatedFactors.sort((a, b) => {\\n const scoreA = a.isDecaying ? a.recentIC : a.icAvg;\\n const scoreB = b.isDecaying ? b.recentIC : b.icAvg;\\n return scoreB - scoreA;\\n});\\nconst finalPool = decorrelatedFactors.slice(0, targetFactorCount);\\nif (decorrelatedFactors.length > targetFactorCount) {\\n Log(' 🗑️ 末位淘汰,移除:', decorrelatedFactors.slice(targetFactorCount).map(f=>f.name).join(', '));\\n}\\n\\n_G('afi_factorPool', JSON.stringify(finalPool));\\n_G('afi_icHistory', JSON.stringify(icHistory));\\nconst iterCount = (_G('afi_iterateCount') || 0) + 1;\\n_G('afi_iterateCount', iterCount);\\n\\nLog('');\\nLog('📊 ═══════════════════════════════════════');\\nLog('✅ 验证完成 | 因子池:', finalPool.length, '个 | 本轮新增:', newValidCount, '| 第', iterCount, '次迭代');\\nLog('🧬 当前因子池(按IC排序):');\\nfinalPool.forEach((f, i) => {\\n const decay = f.isDecaying ? ' ⚠️' : '';\\n const icEmoji = f.icAvg >= 0.05 ? '🟢' : f.icAvg >= 0.02 ? '🟡' : '⚪';\\n Log(' ' + (i+1) + '. ' + icEmoji + ' ' + f.name + ' | IC=' + f.icAvg.toFixed(4) + ' | 近期=' + (f.recentIC||f.icAvg).toFixed(4) + decay);\\n});\\nLog('═══════════════════════════════════════');\\nreturn [{ json: { ...input, factorPool: finalPool, validFactors: finalPool, newValidCount } }];\\n\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[1520,-224],\"id\":\"ae188711-8143-4dba-b86c-39e236f0ef0a\",\"name\":\"解析AI结果+验证IC\"},{\"parameters\":{\"text\":\"={{ $json.prompt }}\",\"options\":{\"systemMessage\":\"你是加密货币量价因子研究员,专注挖掘具有真实预测力的量价信号。\\n\\n【可用算子-发明者平台TA库】\\nrecords: K线数组(.Open .High .Low .Close .Volume), n: records当前长度\\n\\n【TA函数返回格式-必须严格遵守】\\nTA.RSI(records,14) → 直接取末位: TA.RSI(records,14)[n-1]\\nTA.MA(records,p) → 直接取末位: TA.MA(records,p)[n-1]\\nTA.EMA(records,p) → 直接取末位: TA.EMA(records,p)[n-1]\\nTA.ATR(records,p) → 直接取末位: TA.ATR(records,p)[n-1]\\nTA.OBV(records) → 直接取末位: TA.OBV(records)[n-1]\\nTA.CMF(records,p) → 直接取末位: TA.CMF(records,p)[n-1]\\n\\nTA.BOLL(records,p) → 返回[upper数组, mid数组, lower数组]\\n 上轨: TA.BOLL(records,20)[0][n-1]\\n 中轨: TA.BOLL(records,20)[1][n-1]\\n 下轨: TA.BOLL(records,20)[2][n-1]\\n\\nTA.MACD(records,f,s,sig) → 返回[dif数组, dea数组, hist数组]\\n DIF: TA.MACD(records,12,26,9)[0][n-1]\\n DEA: TA.MACD(records,12,26,9)[1][n-1]\\n HIST: TA.MACD(records,12,26,9)[2][n-1]\\n\\nTA.KDJ(records,n,k,d) → 返回[K数组, D数组, J数组]\\n K线: TA.KDJ(records,9,3,3)[0][n-1]\\n D线: TA.KDJ(records,9,3,3)[1][n-1]\\n J线: TA.KDJ(records,9,3,3)[2][n-1]\\n\\nTA.Highest(records,n,'Close') → 返回单个数值,直接使用\\nTA.Lowest(records,n,'Close') → 返回单个数值,直接使用\\n\\n【单行复杂因子写法范例】\\n// 多均线发散(正值=多头排列,负值=空头排列)\\n(TA.MA(records,5)[n-1] - TA.MA(records,20)[n-1]) / TA.MA(records,20)[n-1]\\n\\n// 多均线排列得分(-2到+2)\\n(TA.MA(records,5)[n-1]>TA.MA(records,10)[n-1]?1:-1) + (TA.MA(records,10)[n-1]>TA.MA(records,20)[n-1]?1:-1)\\n\\n// 均线收敛度(偏离均值,正=发散看多)\\nTA.MA(records,5)[n-1] - (TA.MA(records,5)[n-1]+TA.MA(records,10)[n-1]+TA.MA(records,20)[n-1])/3\\n\\n// 量价非对称(防除零)\\n(records[n-1].Close - records[n-2].Close) / (records[n-1].Volume + records[n-2].Volume + 1)\\n\\n// K线实体强度(收盘相对振幅位置,-1到1)\\n(records[n-1].Close - (records[n-1].High+records[n-1].Low)/2) / (records[n-1].High - records[n-1].Low + 0.0001)\\n\\n// 连续上涨根数(用数组reduce替代循环)\\n[1,2,3,4,5].reduce((acc,i) => acc + (records[n-i].Close > records[n-i-1].Close ? 1 : 0), 0)\\n\\n// 成交量Z-score(防除零)\\n(records[n-1].Volume - TA.MA(records,20)[n-1]) / (TA.ATR(records,20)[n-1] + 0.0001)\\n\\n// 价格历史分位数(防除零)\\n(records[n-1].Close - TA.Lowest(records,20,'Close')) / (TA.Highest(records,20,'Close') - TA.Lowest(records,20,'Close') + 0.0001)\\n\\n// 多周期RSI加权合成\\nTA.RSI(records,6)[n-1]*0.5 + TA.RSI(records,14)[n-1]*0.3 + TA.RSI(records,24)[n-1]*0.2\\n\\n// OBV加速度(二阶差分)\\nTA.OBV(records)[n-1] - 2*TA.OBV(records)[n-2] + TA.OBV(records)[n-3]\\n\\n// 亚盘偏移(1H K线,过去8根近似亚盘段收益)\\n(records[n-1].Close - records[n-9].Close) / (records[n-9].Close + 0.0001)\\n\\n// 布林带位置百分比(防除零)\\n(records[n-1].Close - TA.BOLL(records,20)[2][n-1]) / (TA.BOLL(records,20)[0][n-1] - TA.BOLL(records,20)[2][n-1] + 0.0001)\\n\\n// MACD柱状图加速度\\nTA.MACD(records,12,26,9)[2][n-1] - TA.MACD(records,12,26,9)[2][n-2]\\n\\n// 上下影线比(正值=下影线长=看多支撑)\\n((Math.min(records[n-1].Open,records[n-1].Close) - records[n-1].Low) - (records[n-1].High - Math.max(records[n-1].Open,records[n-1].Close))) / (records[n-1].High - records[n-1].Low + 0.0001)\\n\\n// KDJ超买超卖合成\\n(100 - TA.KDJ(records,9,3,3)[0][n-1]) * 0.5 + (100 - TA.KDJ(records,9,3,3)[1][n-1]) * 0.5\\n\\n// 短期动量加速度\\n(records[n-1].Close - records[n-6].Close)/records[n-6].Close - (records[n-2].Close - records[n-7].Close)/(records[n-7].Close + 0.0001)\\n\\n// 价格与OBV背离度(防除零)\\n(records[n-1].Close - records[n-6].Close)/(records[n-6].Close + 0.0001) - (TA.OBV(records)[n-1] - TA.OBV(records)[n-6])/(Math.abs(TA.OBV(records)[n-6]) + 1)\\n\\n【因子搜索空间-优先从以下维度创新】\\n价格行为: 动量加速度、价格历史分位数、K线实体比例、上下影线强度、高低点序列、跳空缺口\\n成交量行为: 量价非对称性、成交量历史Z-score、大K线后量能衰减、OBV加速度、量价背离\\n波动率结构: ATR历史百分位、波动率均值回归、振幅收缩持续时间、布林带宽度变化\\n时间结构: 亚盘收益偏移、近N根收益偏度、价格运动自相关性\\n多周期合成: 均线排列得分、多周期RSI加权、短期动量与中期趋势背离\\n\\n【因子设计原则】\\n1. 每个因子返回单个数值,越大越看多越小越看空\\n2. 必须有明确的加密市场微观行为逻辑,说明为什么有预测力\\n3. 优先设计非线性组合因子,单指标直接取值预测力有限\\n4. 所有除法必须防除零,分母加 +0.0001 或 +1\\n5. window期数建议5-60之间,避免过拟合\\n6. 因子之间必须覆盖不同维度,禁止同质化变体\\n\\n【代码规范-严格执行】\\n- code必须是单行JS表达式,禁止换行、分号、let/const/var声明\\n- 禁止用 [n-1].K / [n-1].hist / [n-1].mid 等对象属性语法\\n- 禁止用 .dif .dea .upper .lower .K .D .J 等任何属性名\\n- 只能用二维数组索引 [维度][n-1]\\n- 用三元运算符替代if语句: (条件 ? 值1 : 值2)\\n- 用数组reduce替代循环: [1,2,3].reduce((acc,i) => acc + ..., 0)\\n- 历史K线直接用 records[n-1-k].Close,k从0开始\\n\\n【加密市场先验知识】\\n- 散户追涨:短期动量显著有效,但持续性弱,1-3根K线后反转\\n- 亚盘偏移:UTC 0-8点趋势常在欧美盘发生均值回归\\n- 量价背离:价格新高但OBV不创新高,顶部概率大\\n- 波动率聚集:低波动率之后往往跟随大波动,方向不确定\\n- 流动性冲击:成交量突增3倍后次根K线有反转倾向\\n- 实体强度:大实体K线(实体>振幅70%)后趋势延续概率高\\n- 均线粘合:多均线收敛后往往跟随方向性突破\\n\\n【严格禁止】\\n- 输出JSON以外的任何文字\\n- 使用```json```等代码块包裹\\n- 生成与已有因子同质化的变体\\n- 分母不加保护的除法\\n\\n【只输出纯JSON】\\n{\\\"factors\\\":[{\\\"name\\\":\\\"英文因子名\\\",\\\"rationale\\\":\\\"加密市场微观逻辑,说明为何有预测力,30字内\\\",\\\"code\\\":\\\"单行JS表达式\\\",\\\"direction\\\":1,\\\"type\\\":\\\"exploitation或exploration\\\"}]}\"}},\"type\":\"@n8n/n8n-nodes-langchain.agent\",\"typeVersion\":1,\"position\":[1200,-224],\"id\":\"dc598a45-08bd-437c-9e52-22fb932926a7\",\"name\":\"AI因子生成智能体\",\"retryOnFail\":true},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"const input = $input.first().json;\\nconst isInitial = (input.action === 'generate_initial');\\nconst targetFactorCount = input.targetFactorCount || $vars.targetFactorCount || 10;\\nconst explorationCount = input.explorationCount || 3;\\nconst marketState = input.marketState || 'normal';\\nconst btcVolPct = _G('afi_btcVolPct') || '0.5';\\nconst factorPool = input.factorPool || [];\\nconst validFactors = input.validFactors || [];\\nconst degradedFactors = input.degradedFactors || [];\\nconst icHistory = JSON.parse(_G('afi_icHistory') || '{}');\\nconst usedDimensions = factorPool.map(f => f.name + '(' + (f.rationale || '') + ')').join(', ') || '暂无';\\nconst allDimensions = [\\n '价格动量(N日涨跌幅)', '短期反转(RSI超买超卖)', '波动率(ATR/布林带宽)',\\n '成交量异常(量比/放量)', '量价背离(OBV与价格背离)', '资金流向(CMF/MFI)',\\n '布林带位置(价格在通道的百分位)', '跨期价格结构(近远期价差)', 'MACD动量(柱状图斜率)',\\n 'KDJ超买超卖', '高低价通道突破(唐奇安)', '均线多空排列(短中长期MA)',\\n '价格加速度(动量的变化率)', '振幅收缩后突破(波动率低位后扩张)',\\n '成交量加权价格偏离(VWAP偏差)', '开盘缺口(跳空后回补概率)',\\n '高频换手(短期成交量/流通量)', '价格分形结构(高低点序列)',\\n 'K线实体强度(实体占振幅比)', '上下影线不对称(多空压力对比)',\\n '量能衰减(大K线后成交量萎缩)', '亚盘欧盘收益偏移', '多周期RSI共振'\\n];\\nconst unusedDimensions = allDimensions.filter(d => {\\n return !factorPool.some(f => d.includes(f.name) || (f.rationale && d.includes(f.rationale.slice(0, 4))));\\n});\\nconst unusedSample = unusedDimensions.slice(0, 8).join('、') || '请自由发挥';\\nlet prompt = '';\\nif (isInitial) {\\n prompt = '当前加密货币市场状态: ' + marketState + ' | BTC波动率分位: ' + btcVolPct + '\\\\n\\\\n';\\n prompt += '生成' + targetFactorCount + '个初始量价因子,必须覆盖以下不同维度,每个维度最多1个:\\\\n';\\n prompt += '动量、反转、波动率、成交量、量价关系、资金流向、布林带、均线排列、MACD、KDJ\\\\n\\\\n';\\n prompt += '优先生成非线性组合因子,单指标直接取值预测力有限。\\\\n\\\\ntype全部填exploration';\\n} else {\\n const validSummary = validFactors.map(f => {\\n const arr = icHistory[f.name] || [];\\n const avg = arr.length > 0 ? (arr.reduce((a,b)=>a+b,0)/arr.length).toFixed(4) : 'N/A';\\n const recent = arr.length >= 20 ? (arr.slice(-20).reduce((a,b)=>a+b,0)/20).toFixed(4) : 'N/A';\\n return f.name + ': 历史IC=' + avg + ' 近期IC=' + recent + ' | 逻辑: ' + f.rationale;\\n }).join('\\\\n') || '暂无';\\n const degradedSummary = degradedFactors.length > 0\\n ? degradedFactors.map(f => f.name + ': 近期IC=' + f.recentIC + ' | 原逻辑: ' + f.rationale).join('\\\\n')\\n : '本轮无衰减因子';\\n prompt = '【市场状态】' + marketState + ' | BTC波动率分位: ' + btcVolPct + '\\\\n\\\\n';\\n prompt += '【当前有效因子(不需要生成变体)】\\\\n' + validSummary + '\\\\n\\\\n';\\n prompt += '【近期衰减因子(这些维度已失效,禁止在这些维度上微调)】\\\\n' + degradedSummary + '\\\\n\\\\n';\\n prompt += '【已覆盖维度(禁止重复)】\\\\n' + usedDimensions + '\\\\n\\\\n';\\n prompt += '【尚未探索的维度(优先从这里选)】\\\\n' + unusedSample + '\\\\n\\\\n';\\n prompt += '【任务】\\\\n';\\n prompt += '生成' + explorationCount + '个全新方向因子,要求:\\\\n';\\n prompt += '1. 必须与【已覆盖维度】完全不同,禁止微调失效因子\\\\n';\\n prompt += '2. 优先从【尚未探索的维度】中选取\\\\n';\\n prompt += '3. 优先设计非线性组合因子\\\\n';\\n prompt += '4. 针对当前' + marketState + '市场状态设计\\\\n';\\n prompt += '5. type全部填exploration\\\\n\\\\n';\\n prompt += '这' + explorationCount + '个新因子将与现有' + validFactors.length + '个因子竞争,按IC排序并经相关性过滤后取最优' + ($vars.targetFactorCount||10) + '个入池。';\\n}\\nreturn [{ json: { ...input, prompt, skipLLM: false } }];\\n\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[1056,-224],\"id\":\"b4606aaa-79de-42f4-91fb-c04b16a1d8e5\",\"name\":\"构建Prompt\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"const input = $input.first().json;\\nconst factorPool = JSON.parse(_G('afi_factorPool') || '[]');\\nconst icHistory = JSON.parse(_G('afi_icHistory') || '{}');\\nconst icDecayWindow = $vars.icDecayWindow || 48;\\nconst icDecayThreshold = $vars.icDecayThreshold || -0.01;\\nconst targetFactorCount = $vars.targetFactorCount || 10;\\nconst marketState = input.marketState || 'normal';\\nconst degradedFactors = [];\\nfor (const factor of factorPool) {\\n const icArr = icHistory[factor.name] || [];\\n if (icArr.length >= 20) {\\n const window = Math.min(icArr.length, icDecayWindow);\\n const recentAvg = icArr.slice(-window).reduce((a, b) => a + b, 0) / window;\\n if (recentAvg < icDecayThreshold) {\\n degradedFactors.push({ name: factor.name, recentIC: recentAvg.toFixed(4), rationale: factor.rationale });\\n }\\n }\\n}\\nconst validCount = factorPool.length;\\nconst explorationBuffer = $vars.explorationBuffer || 3;\\nconst explorationCount = Math.max(\\n explorationBuffer,\\n targetFactorCount - validCount + explorationBuffer\\n);\\nconst action = factorPool.length === 0 ? 'generate_initial' : 'iterate_factors';\\nLog('因子检查 | 当前池:', validCount, '| 近期衰减:', degradedFactors.length,\\n '| 目标:', targetFactorCount, '| 本轮新增:', explorationCount, '| 市场:', marketState);\\nreturn [{ json: {\\n ...input, action, factorPool,\\n validFactors: factorPool,\\n degradedFactors, validCount,\\n explorationCount, targetFactorCount\\n} }];\\n\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[896,-224],\"id\":\"df73b2c8-3a43-4177-8a6f-16581883daaa\",\"name\":\"检查因子池状态\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"const topN = $vars.topN || 150;\\nconst tickers = exchange.GetTickers();\\nif (!tickers || tickers.length === 0) { Log('GetTickers失败'); return []; }\\nconst filtered = tickers\\n .filter(t => t.Symbol.endsWith('USDT.swap'))\\n .map(t => ({ symbol: t.Symbol, quoteVolume: t.Last * t.Volume }))\\n .sort((a, b) => b.quoteVolume - a.quoteVolume)\\n .slice(0, topN)\\n .map(t => t.symbol);\\nLog('标的池更新:', filtered.length, '个币种');\\n_G('afi_symbolPool', JSON.stringify(filtered));\\nconst btcChangePct = $vars.btcChangePct || 5;\\nlet marketState = 'normal';\\nlet btcVolPercentile = 0.5;\\ntry {\\n const btcR = exchange.GetRecords('BTC_USDT.swap', PERIOD_H4);\\n if (btcR && btcR.length >= 30) {\\n const n = btcR.length;\\n const latestChange = Math.abs((btcR[n-1].Close - btcR[n-2].Close) / btcR[n-2].Close * 100);\\n if (latestChange >= btcChangePct) { marketState = 'volatile'; Log('BTC突变:', latestChange.toFixed(2) + '%'); }\\n const returns20 = [];\\n for (let i = n - 20; i < n; i++) returns20.push(Math.abs((btcR[i].Close - btcR[i-1].Close) / btcR[i-1].Close));\\n const avgVol = returns20.reduce((a, b) => a + b, 0) / returns20.length;\\n const allVols = [];\\n for (let i = 1; i < n; i++) allVols.push(Math.abs((btcR[i].Close - btcR[i-1].Close) / btcR[i-1].Close));\\n allVols.sort((a, b) => a - b);\\n btcVolPercentile = allVols.findIndex(v => v >= avgVol) / allVols.length;\\n if (btcVolPercentile > 0.8) marketState = 'high_vol';\\n else if (btcVolPercentile < 0.3) marketState = 'low_vol';\\n }\\n} catch(e) { Log('BTC检测失败:', e.message); }\\n_G('afi_marketState', marketState);\\n_G('afi_btcVolPct', btcVolPercentile.toFixed(2));\\nLog('市场状态:', marketState, '| 波动率分位:', (btcVolPercentile * 100).toFixed(1) + '%');\\nreturn [{ json: { ...$input.first().json, symbols: filtered, symbolCount: filtered.length, marketState } }];\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[736,-224],\"id\":\"f46dbe55-be11-45c9-8154-3a62e0bcb7e5\",\"name\":\"获取标的池\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"if (_G('afi_initialized') === null) {\\n _G('afi_initialized', true);\\n _G('afi_factorPool', JSON.stringify([]));\\n _G('afi_icHistory', JSON.stringify({}));\\n _G('afi_iterateCount', 0);\\n _G('afi_runCount', 0);\\n _G('afi_marketState', 'unknown');\\n _G('afi_btcVolPct', '0.5');\\n _G('afi_symbolPool', JSON.stringify([]));\\n const account = exchange.GetAccount();\\n _G('afi_initmoney', account.Balance);\\n Log('=== 自适应因子挖掘策略初始化完成 ===');\\n Log('初始资金:', account.Balance, 'USDT');\\n}\\nconst runCount = (_G('afi_runCount') || 0) + 1;\\n_G('afi_runCount', runCount);\\nconst account = exchange.GetAccount();\\nconst initMoney = _G('afi_initmoney');\\nconst equity = account.Equity || account.Balance;\\nconst totalReturn = ((equity - initMoney) / initMoney * 100).toFixed(2);\\nconst factorPool = JSON.parse(_G('afi_factorPool') || '[]');\\nLogProfit(equity - initMoney, '&');\\nLog('【状态】运行次数:', runCount, '| 权益:', equity.toFixed(2), '| 收益率:', totalReturn + '%', '| 因子池:', factorPool.length, '个');\\nreturn [{\\n json: {\\n runCount,\\n equity: equity.toFixed(2),\\n availableCash: account.Balance.toFixed(2),\\n initMoney,\\n totalReturn: totalReturn + '%',\\n factorCount: factorPool.length\\n }\\n}];\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[576,-224],\"id\":\"b38e1f12-5e17-4a87-ac53-2212149e6e8d\",\"name\":\"初始化状态\"},{\"parameters\":{\"notice\":\"\",\"rule\":{\"interval\":[{\"field\":\"hours\",\"hoursInterval\":1,\"triggerAtMinute\":0}]}},\"type\":\"n8n-nodes-base.scheduleTrigger\",\"typeVersion\":1.2,\"position\":[416,-224],\"id\":\"d77377f4-e32d-40ef-8e7c-2364f78010ab\",\"name\":\"慢触发器\"},{\"parameters\":{\"notice\":\"\",\"rule\":{\"interval\":[{\"field\":\"seconds\",\"secondsInterval\":3}]}},\"type\":\"n8n-nodes-base.scheduleTrigger\",\"typeVersion\":1.2,\"position\":[416,112],\"id\":\"f706a775-6bc2-4ecb-a174-8b87631647b7\",\"name\":\"快触发器\",\"logLevel\":0},{\"parameters\":{\"model\":{\"__rl\":true,\"value\":\"anthropic/claude-sonnet-4.6\",\"mode\":\"list\",\"cachedResultName\":\"anthropic/claude-sonnet-4.6\"}},\"type\":\"n8n-nodes-base.lmOpenAi\",\"typeVersion\":1,\"position\":[1200,-48],\"id\":\"400accfe-eb19-4966-9696-ac9346a7151b\",\"name\":\"大模型\",\"credentials\":{\"openAiApi\":{\"id\":\"54d0b567-b3fc-4c6a-b6be-546e0b9cd83f\",\"name\":\"openrouter\"}}}],\"pinData\":{},\"connections\":{\"计算信号\":{\"main\":[[{\"node\":\"执行调仓\",\"type\":\"main\",\"index\":0}]]},\"解析AI结果+验证IC\":{\"main\":[[{\"node\":\"计算信号\",\"type\":\"main\",\"index\":0}]]},\"AI因子生成智能体\":{\"main\":[[{\"node\":\"解析AI结果+验证IC\",\"type\":\"main\",\"index\":0}]]},\"构建Prompt\":{\"main\":[[{\"node\":\"AI因子生成智能体\",\"type\":\"main\",\"index\":0}]]},\"检查因子池状态\":{\"main\":[[{\"node\":\"构建Prompt\",\"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}]]},\"大模型\":{\"ai_languageModel\":[[{\"node\":\"AI因子生成智能体\",\"type\":\"ai_languageModel\",\"index\":0}]]}},\"active\":false,\"settings\":{\"timezone\":\"Asia/Shanghai\",\"executionOrder\":\"v1\"},\"tags\":[],\"credentials\":{},\"id\":\"adaptive-factor-mining-v2\",\"plugins\":{},\"mcpClients\":{}},\"startNodes\":[],\"triggerToStartFrom\":{\"name\":\"慢触发器\"}}"}