自适应因子挖掘量化策略测试版


创建日期: 2026-03-18 13:04:48 最后修改: 2026-03-23 17:49:27
复制: 10 点击次数: 104
avatar of ianzeng123 ianzeng123
2
关注
431
关注者

策略简介

本策略是一套运行在币安永续合约上的多因子多空对冲量化系统。核心思路是利用 AI 大语言模型(LLM)自动挖掘、验证和迭代量价因子,通过 Rank IC(秩信息系数)严格筛选具有真实预测力的因子,加权合成后输出多空信号并自动执行调仓,全程无需人工干预因子设计。

核心优势在于:因子池不是写死的,而是由 AI 根据市场状态持续探索新维度、淘汰失效因子、自适应进化,真正实现”因子自己会拟合”的量化策略。


核心模块

① 标的池筛选 + 市场状态检测(小时级别)

自动拉取全市场 USDT 永续合约行情,按成交额排序筛选前 N 个主流币种构建标的池。同时分析 BTC 近期波动率分位数,将市场划分为四种状态:正常(normal)、高波动(high_vol)、低波动(low_vol)、突变(volatile),供后续因子生成和信号计算参考。

② AI 因子挖掘 + 自适应迭代(核心)

使用大语言模型作为因子研究员,每轮迭代完成以下闭环:

  • 检查现有因子池状态,识别近期 IC 衰减的因子
  • 构建结构化 Prompt,注入已覆盖维度、衰减因子、市场状态,引导 AI 探索全新方向
  • AI 生成候选因子(单行 JS 表达式),覆盖动量、反转、波动率、成交量、量价背离、均线排列等多维度
  • 对所有因子(新旧)进行完整 Rank IC 回测验证,采用 t-1 期因子值预测 t 期收益的正确时序
  • 相关性去重(高相关因子仅保留 IC 最优者)+ 末位淘汰,维持因子池在目标数量

③ 多因子信号合成 + 调仓执行

对标的池中所有币种计算各因子的截面 Z-score,以近期 IC 为权重加权合成综合评分。评分超过做多阈值的前 N 个币种做多,低于做空阈值的前 N 个做空,自动执行开平仓。不在目标持仓中的旧仓位自动平掉。

④ 实时持仓监控 + 风控(秒级别)

快速循环监控所有持仓的浮盈浮亏,内置三重退出机制:

  • 固定止损:亏损达到设定比例自动平仓
  • 固定止盈:浮盈达到设定比例自动平仓
  • 动态移动止盈:浮盈超过触发阈值后启动,根据最高浮盈自适应调整回撤容忍度(浮盈越高,允许回撤越大),从最高点回撤超限即平仓锁利

⑤ 可视化仪表板

策略运行后实时展示三张可交互表格:

  • 账户概览:初始资金、当前权益、总收益率、市场状态、因子数量、迭代次数,支持一键重置和清空因子池
  • 因子池状态:每个因子的历史 IC、近期 IC、IC 趋势、相关因子、方向、逻辑说明,衰减因子自动标记预警
  • 持仓监控:币种方向、入场价、现价、浮盈、最高浮盈、移动止盈状态,支持手动平仓

可配置参数

参数 说明 默认值
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

策略适用场景

  • 希望系统性做多空对冲、不依赖单一方向判断的量化用户
  • 对传统固定因子策略容易失效感到困扰,希望因子池能自动进化的用户
  • 具备一定合约交易基础,理解因子、IC、多空对冲等概念的进阶用户

风险提示

  • 加密货币永续合约波动极大,多空对冲可降低但无法消除系统性风险
  • AI 生成的因子基于历史统计规律,过去有效不代表未来持续有效
  • 因子 IC 验证基于小时级 K 线回测,极端行情下可能失效
  • 杠杆会成倍放大损失,建议初期使用低杠杆并控制总仓位比例
  • 强烈建议先在模拟盘运行多个迭代周期,观察因子池进化和信号质量后再切换实盘
策略源码
{"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\":\"慢触发器\"}}"}
全部留言
avatar of ianzeng123
ianzeng123
经过一段时间实盘测试,效果较为不稳定,收益起伏较大,模型引导语和个别参数需根据实盘运行结果进行合理调试。
2026-03-23 16:54:35