因子验证工作流


创建日期: 2025-10-29 16:39:09 最后修改: 2025-10-31 17:00:33
复制: 0 点击次数: 204
avatar of ianzeng123 ianzeng123
2
关注
329
关注者
策略源码
{"type":"n8n","content":"{\"workflowData\":{\"nodes\":[{\"parameters\":{\"model\":{\"__rl\":true,\"value\":\"anthropic/claude-sonnet-4.5\",\"mode\":\"list\",\"cachedResultName\":\"anthropic/claude-sonnet-4.5\"}},\"type\":\"n8n-nodes-base.lmOpenAi\",\"typeVersion\":1,\"position\":[-1440,-224],\"id\":\"66d5bf2d-40f0-48fd-aa04-ac89ee498c8a\",\"name\":\"OpenAI 模型\",\"credentials\":{\"openAiApi\":{\"id\":\"54d0b567-b3fc-4c6a-b6be-546e0b9cd83f\",\"name\":\"openrouter\"}}},{\"parameters\":{\"model\":{\"__rl\":true,\"value\":\"anthropic/claude-sonnet-4.5\",\"mode\":\"list\",\"cachedResultName\":\"anthropic/claude-sonnet-4.5\"}},\"type\":\"n8n-nodes-base.lmOpenAi\",\"typeVersion\":1,\"position\":[-592,-80],\"id\":\"6b86f0d6-a43b-496e-8c60-1b2ed319bbd0\",\"name\":\"OpenAI 模型1\",\"credentials\":{\"openAiApi\":{\"id\":\"54d0b567-b3fc-4c6a-b6be-546e0b9cd83f\",\"name\":\"openrouter\"}}},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"var coinList = $vars.coinList \\n\\n// 处理不同格式的输入\\nvar SYMBOLS = coinList\\nif (typeof coinList === 'string') {\\n    // 如果是字符串,先去除多余的引号和转义符\\n    coinList = coinList.replace(/\\\\\\\\\\\"/g, '\\\"').replace(/^\\\"|\\\"$/g, '')\\n    SYMBOLS = coinList.split(',').map(function(s) {\\n        return s.trim().replace(/^\\\"|\\\"$/g, '')\\n    })\\n}\\n\\nvar symbolObjects = SYMBOLS.map(function(symbol) {\\n    return {\\n        symbol: symbol\\n    }\\n})\\n\\nreturn symbolObjects\\n\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[-1664,-48],\"id\":\"8be2574a-8e80-4957-b93d-8dd991b48020\",\"name\":\"币种筛选\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"const inputData = $input.all();\\nlet factorData = [];\\n\\n// 遍历每个交易对\\nfor (const item of inputData) {\\n  const data = item.json;\\n  \\n  if (!data.success || !data.result) {\\n    continue;\\n  }\\n  \\n  const symbol = data.symbol;\\n  const exchange = data.exchangeName;\\n  \\n  // 遍历每条K线记录\\n  for (const record of data.result) {\\n    factorData.push({\\n      date: record.Time,\\n      symbol: symbol,\\n      exchange: exchange,\\n      open: record.Open,\\n      high: record.High,\\n      low: record.Low,\\n      close: record.Close,\\n      volume: record.Volume,\\n      openInterest: record.OpenInterest\\n    });\\n  }\\n}\\n\\n// 返回整理后的数据\\nreturn {data : factorData};\\n\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[-1248,-64],\"id\":\"024ece4f-9d05-40e4-9946-7cd4671e4fef\",\"name\":\"K线整理\"},{\"parameters\":{\"text\":\"={{JSON.stringify($json.messageText)}}\",\"options\":{\"systemMessage\":\"=你是一个专业的量化因子工程师,负责根据用户的因子描述生成可执行的JavaScript因子计算代码。\\n\\n## ⚠️ 核心原则:因子函数必须返回数值\\n**关键要求**:因子函数应该为每个有效的加密货币返回一个数值,而不是null。只有在数据严重不足的情况下才返回null。\\n\\n### 为什么不能返回null?\\n1. **排序需要**:因子排序需要为每个币种分配一个数值\\n2. **组合构建**:返回null会导致币种被排除,无法构建足够的投资组合\\n3. **统计分析**:null值会影响IC计算和因子有效性验证\\n\\n### 正确的处理方式\\n- ✅ **数据不足时**:返回null(如数组长度不够)\\n- ✅ **条件不满足时**:返回较小的数值、0、或反向数值\\n- ✅ **边界情况**:返回中性值(如0)\\n- ❌ **错误做法**:因为不满足特定条件就返回null\\n\\n## ⚠️ 核心新增:因子方向性识别(重要)\\n\\n### 关键问题:识别预期收益方向\\n用户描述中包含以下关键词时,需要特别注意因子方向:\\n\\n**正向预期(做多信号)**:\\n- \\\"上涨\\\"、\\\"涨幅大\\\"、\\\"反弹\\\"、\\\"突破\\\"\\n- \\\"收益\\\"、\\\"盈利\\\"、\\\"获利\\\"\\n- \\\"强势\\\"、\\\"牛市\\\"、\\\"利好\\\"\\n- \\\"买入\\\"、\\\"看多\\\"、\\\"积极\\\"\\n\\n**负向预期(做空信号)**:\\n- \\\"下跌\\\"、\\\"大跌\\\"、\\\"暴跌\\\"、\\\"回调\\\"\\n- \\\"亏损\\\"、\\\"损失\\\"、\\\"风险\\\"\\n- \\\"弱势\\\"、\\\"熊市\\\"、\\\"利空\\\"\\n- \\\"卖出\\\"、\\\"看空\\\"、\\\"避险\\\"\\n\\n### 因子方向设计原则\\n\\n**正向因子**(预期上涨):\\n```javascript\\n// 因子值越高 → 预期收益越高 → 适合做多\\nreturn factorValue; // 正值\\n```\\n\\n**负向因子**(预期下跌):\\n```javascript\\n// 方法1:返回负值(推荐)\\nreturn -factorValue; // 负值,因子值越高(绝对值),预期下跌越多\\n\\n// 方法2:取倒数\\nreturn 1 / Math.max(factorValue, 0.001); // 信号越强,因子值越小\\n\\n// 方法3:反向构造\\nreturn maxValue - factorValue; // 构造反向关系\\n```\\n\\n### 典型负向因子识别示例\\n\\n| 用户描述 | 预期方向 | 因子设计要点 |\\n|---------|---------|-------------|\\n| \\\"连续小跌且量缩,预测大跌\\\" | 负向 | 返回 -bearishSignal |\\n| \\\"RSI过热,预期回调\\\" | 负向 | 返回 -(RSI-50) 或 (100-RSI) |\\n| \\\"高位放量,警惕下跌\\\" | 负向 | 返回 -volumeRatio |\\n| \\\"波动率过低,风险积聚\\\" | 负向 | 返回 -volatility |\\n| \\\"连涨多日,获利了结压力\\\" | 负向 | 返回 -consecutiveGains |\\n\\n## 因子描述解析规则\\n\\n用户输入通常有三种类型,需要正确识别和处理:\\n\\n### 类型1:标准因子名称(直接计算)\\n用户直接说出因子名称,按标准定义计算即可:\\n- \\\"三日动量\\\" → 计算3日价格动量\\n- \\\"RSI\\\" → 计算相对强弱指数\\n- \\\"MACD\\\" → 计算移动平均收敛发散\\n- \\\"布林带位置\\\" → 计算价格在布林带中的位置\\n- \\\"成交量比率\\\" → 计算成交量相对均值的比率\\n\\n### 类型2:描述性因子(需要解析)\\n用户描述市场现象,包含两部分:\\n1. **因子定义**:用什么数据作为因子值(这是要计算的内容)\\n2. **预期效果**:期望这个因子能预测什么(这是验证目标,不是计算内容)\\n\\n### 类型3:复合条件因子\\n用户给出多个条件组合,需要综合考虑。\\n\\n### 解析示例对照表\\n| 用户描述 | 因子类型 | 因子定义 | 预期效果 | 因子计算方式 |\\n|---------|---------|---------|---------|-------------|\\n| \\\"三日动量\\\" | 标准因子 | 3日价格动量 | 趋势延续 | (当前价格-3日前价格)/3日前价格 |\\n| \\\"RSI\\\" | 标准因子 | 相对强弱指数 | 超买超卖信号 | 标准RSI计算公式 |\\n| \\\"连续小跌+量缩,预测大跌\\\" | 负向描述因子 | 小跌+量缩强度 | 预测下跌 | -bearishSignal |\\n| \\\"昨天振幅小,今天涨幅大\\\" | 描述性因子 | 昨日振幅 | 预测今日上涨 | -昨日振幅 |\\n| \\\"成交量大\\\" | 描述性因子 | 成交量 | 一般正向预测 | 当前成交量 |\\n| \\\"RSI过热,预期回调\\\" | 负向描述因子 | RSI过热程度 | 预测回调 | -(RSI-80) 或 (100-RSI) |\\n| \\\"高位震荡,注意风险\\\" | 负向描述因子 | 高位震荡强度 | 预测下跌风险 | -oscillationAtHigh |\\n\\n### ⚠️ 重要:方向识别流程\\n\\n```javascript\\n// 1. 首先识别用户描述的预期方向\\nconst isNegativeExpectation = /下跌|大跌|暴跌|回调|亏损|风险|看空|卖出|利空|熊市/.test(userDescription);\\n\\n// 2. 计算基础因子值\\nlet baseFactorValue = calculateBaseFactor();\\n\\n// 3. 根据预期方向调整返回值\\nif (isNegativeExpectation) {\\n    return -Math.abs(baseFactorValue); // 确保返回负值或反向值\\n} else {\\n    return Math.abs(baseFactorValue);  // 确保返回正值\\n}\\n```\\n\\n## 输出要求\\n必须严格按照以下格式输出,不得有任何偏差:\\n```javascript\\nFactorCalculator.customFactor = function(closes, volumes, highs, lows, opens, lookback) {\\n    // 基本数据检查 - 只在数据严重不足时返回null\\n    if (closes.length < minRequiredLength) return null;\\n    \\n    // 因子计算逻辑 - 必须返回数值\\n    let factorValue = 0; // 初始化为数值\\n    \\n    // 计算逻辑...\\n    \\n    // ⚠️ 关键:根据预期方向返回正确的符号\\n    // 如果预期负面结果,返回负值或反向值\\n    return factorValue; // 必须是数值,不能是null\\n};\\n```\\n\\n## 硬性格式要求\\n1. 方法名必须是: `FactorCalculator.customFactor`\\n2. 参数顺序必须是: `(closes, volumes, highs, lows, opens, lookback)`\\n3. **只在数据严重不足时返回null**,其他情况必须返回数值\\n4. **根据预期方向正确设置因子符号**\\n5. 最后必须: `return factorValue;`\\n6. 不要添加多余的注释解释,保持代码简洁\\n7. 不要使用markdown代码块标记(```javascript等)\\n\\n## 条件处理策略\\n\\n### 策略1:权重调节法(推荐用于负向因子)\\n```javascript\\nlet bearishSignal = calculateBearishConditions();\\nif (strongBearishCondition) {\\n    return -bearishSignal * 2.0;  // 强烈看空信号\\n} else if (weakBearishCondition) {\\n    return -bearishSignal * 0.5;  // 温和看空信号\\n} else {\\n    return -bearishSignal * 0.1;  // 微弱信号但不返回null\\n}\\n```\\n\\n### 策略2:累积评分法\\n```javascript\\nlet bearishScore = 0;\\nbearishScore += condition1 ? 1.0 : 0.1;  // 主要看空条件\\nbearishScore += condition2 ? 0.5 : 0.05; // 次要看空条件\\nbearishScore += baseMetric * 0.3;        // 基础指标\\nreturn -bearishScore; // 负向因子返回负值\\n```\\n\\n### 策略3:分层处理\\n```javascript\\nif (metric > highThreshold) {\\n    return -metric * 2;      // 强烈看空\\n} else if (metric > lowThreshold) {\\n    return -metric;          // 中等看空\\n} else {\\n    return -metric * 0.1;    // 弱看空(不返回null)\\n}\\n```\\n\\n## 周期参数处理规则\\n**关键**:如果用户明确指定了周期数字,必须在代码中硬编码该数字,不要使用lookback参数。\\n\\n### 周期单位说明\\n- 数组中的每个元素代表一个K线周期(可能是分钟、小时、日等)\\n- **周期数字与时间单位无关**,只代表K线根数\\n- 用户说\\\"3根K线\\\"、\\\"3分钟\\\"、\\\"3小时\\\"、\\\"3日\\\" → 都是 `const period = 3;`\\n- 用户说\\\"20根K线\\\"、\\\"20分钟\\\"、\\\"20小时\\\"、\\\"20日\\\" → 都是 `const period = 20;`\\n\\n## 重要:不要使用MathUtils\\n请直接实现数学计算,不要使用MathUtils函数:\\n- 平均值:`arr.reduce((a, b) => a + b, 0) / arr.length`\\n- 标准差:`Math.sqrt(arr.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / arr.length)`\\n- 相关系数:请手动实现皮尔逊相关系数公式\\n- 协方差:请手动实现协方差公式\\n\\n禁止使用任何MathUtils.*函数,所有计算都要直接编写。\\n\\n## 数组说明\\n- `closes[closes.length-1]`: 最新收盘价(当前K线)\\n- `closes[closes.length-2]`: 上一根K线收盘价\\n- `closes[closes.length-1-n]`: n根K线之前的收盘价\\n- 其他数组(volumes, highs, lows, opens)同理\\n- 使用 `closes.slice(-n)` 可以获取最近n根K线数据\\n- **K线周期由外部系统决定**(可能是1分钟、5分钟、1小时、1日等)\\n\\n## 负向因子代码示例\\n\\n### 示例1:连续小跌+量缩 → 预测大跌\\n```javascript\\nFactorCalculator.customFactor = function(closes, volumes, highs, lows, opens, lookback) {\\n    const period = 3;\\n    if (closes.length < period + 1 || volumes.length < period + 1) return null;\\n    \\n    let declineScore = 0;\\n    let volumeDecreaseScore = 0;\\n    \\n    // 检查连续3天小跌情况\\n    for (let i = 1; i <= period; i++) {\\n        const idx = closes.length - i;\\n        const dailyReturn = (closes[idx] - closes[idx - 1]) / closes[idx - 1];\\n        \\n        // 小跌定义:-2%到0之间\\n        if (dailyReturn < 0 && dailyReturn > -0.02) {\\n            declineScore += Math.abs(dailyReturn) * 100;\\n        }\\n    }\\n    \\n    // 检查成交量递减情况\\n    let volumeDecreasing = true;\\n    for (let i = 1; i < period; i++) {\\n        const idx = volumes.length - i;\\n        if (volumes[idx] >= volumes[idx - 1]) {\\n            volumeDecreasing = false;\\n            break;\\n        }\\n    }\\n    \\n    if (volumeDecreasing) {\\n        const vol1 = volumes[volumes.length - 1];\\n        const vol3 = volumes[volumes.length - 3];\\n        volumeDecreaseScore = (vol3 - vol1) / Math.max(vol3, 0.0001);\\n    }\\n    \\n    // 组合因子值\\n    let factorValue = 0;\\n    \\n    if (declineScore > 0) {\\n        factorValue += declineScore * 2.0;\\n        if (volumeDecreasing) {\\n            factorValue += volumeDecreaseScore * 3.0;\\n        } else {\\n            factorValue += volumeDecreaseScore * 0.3;\\n        }\\n    } else {\\n        factorValue = declineScore * 0.1 + volumeDecreaseScore * 0.1;\\n    }\\n    \\n    // 关键:返回负值,因为预测的是\\\"大跌\\\"\\n    return -Math.max(factorValue, 0);\\n};\\n```\\n\\n### 示例2:RSI过热 → 预期回调\\n```javascript\\nFactorCalculator.customFactor = function(closes, volumes, highs, lows, opens, lookback) {\\n    const period = 14;\\n    if (closes.length < period + 1) return null;\\n    \\n    let gains = 0, losses = 0;\\n    for (let i = closes.length - period; i < closes.length; i++) {\\n        const change = closes[i] - closes[i - 1];\\n        if (change > 0) gains += change;\\n        else losses += Math.abs(change);\\n    }\\n    \\n    const avgGain = gains / period;\\n    const avgLoss = losses / period;\\n    const rs = avgGain / (avgLoss || 0.0001);\\n    const rsi = 100 - (100 / (1 + rs));\\n    \\n    // 过热程度:RSI越高越过热,预期回调越强\\n    const overheatedDegree = Math.max(rsi - 70, 0);\\n    \\n    // 返回负值:过热程度越高,预期收益越低\\n    return -overheatedDegree;\\n};\\n```\\n\\n### 示例3:高位放量 → 警惕下跌\\n```javascript\\nFactorCalculator.customFactor = function(closes, volumes, highs, lows, opens, lookback) {\\n    const period = 20;\\n    if (closes.length < period + 1 || volumes.length < period + 1) return null;\\n    \\n    const currentPrice = closes[closes.length - 1];\\n    const recentPrices = closes.slice(-period);\\n    const highPrice = Math.max(...recentPrices);\\n    const pricePosition = currentPrice / highPrice;\\n    \\n    const currentVolume = volumes[volumes.length - 1];\\n    const avgVolume = volumes.slice(-period, -1).reduce((a, b) => a + b, 0) / (period - 1);\\n    const volumeRatio = currentVolume / Math.max(avgVolume, 1);\\n    \\n    // 高位(>0.9)且放量(>1.5倍)的危险信号\\n    let dangerSignal = 0;\\n    if (pricePosition > 0.9) {\\n        dangerSignal = pricePosition * Math.max(volumeRatio - 1, 0);\\n    }\\n    \\n    // 返回负值:危险信号越强,预期收益越低\\n    return -dangerSignal;\\n};\\n```\\n\\n## 因子设计核心原则\\n1. **因子值方向**:明确预期方向,正向预期返回正值,负向预期返回负值或反向值\\n2. **数据时点**:明确使用当前、昨日还是历史数据\\n3. **单一职责**:一个因子专注一个逻辑,避免复合计算\\n4. **稳健性**:添加必要的零值和边界检查\\n5. **数值返回**:除数据不足外,必须返回数值而非null\\n6. **方向一致性**:确保因子逻辑与预期收益方向一致\\n\\n## 特殊情况处理\\n- **分母为零**:加小数值如0.0001避免除零,或返回0\\n- **数据不足**:只在数据真正不足时返回null\\n- **条件不满足**:返回较小权重的数值,不返回null\\n- **异常值**:必要时进行截尾处理\\n- **单位统一**:确保不同币种间可比较\\n- **负向因子**:确保返回值符号正确反映预期方向\\n\\n现在根据因子描述生成代码:\"}},\"type\":\"@n8n/n8n-nodes-langchain.agent\",\"typeVersion\":1,\"position\":[-1440,-384],\"id\":\"f6e91e03-30ff-4231-afc3-163fc3f232a9\",\"name\":\"AI实现\"},{\"parameters\":{\"mode\":\"append\",\"numberInputs\":2},\"type\":\"n8n-nodes-base.merge\",\"typeVersion\":3.2,\"position\":[-1024,-240],\"id\":\"0a8c699b-d15e-4d8c-b8b5-e9ffd178928b\",\"name\":\"数据合并\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"const klineData = $node[\\\"K线整理\\\"].json.data;\\n\\n// ========== 稳健的代码提取方法 ==========\\nfunction extractFactorCodeRobust(aiResult) {\\n    let factorCodeText = '';\\n    \\n    try {\\n        // 1. 处理不同的输入格式\\n        if (Array.isArray(aiResult) && aiResult.length > 0) {\\n            // 数组格式 [{\\\"output\\\": \\\"...\\\"}]\\n            factorCodeText = aiResult[0].output || aiResult[0].text || aiResult[0].json || aiResult[0];\\n        } else if (typeof aiResult === 'object' && aiResult !== null) {\\n            // 对象格式 {\\\"text\\\": \\\"...\\\", \\\"output\\\": \\\"...\\\", \\\"json\\\": \\\"...\\\"}\\n            factorCodeText = aiResult.output || aiResult.text || aiResult.json || JSON.stringify(aiResult);\\n        } else {\\n            // 直接字符串格式\\n            factorCodeText = String(aiResult);\\n        }\\n        \\n        // 2. 清理代码文本\\n        let cleanCode = factorCodeText\\n            .replace(/```javascript/g, '')\\n            .replace(/```/g, '')\\n            .trim();\\n        \\n        // 3. 多种方式提取函数体\\n        let extractedCode = '';\\n        \\n        // 方法1: 提取完整的 customFactor 函数定义\\n        const fullFunctionPattern = /FactorCalculator\\\\.customFactor\\\\s*=\\\\s*function\\\\s*\\\\([^)]*\\\\)\\\\s*\\\\{([\\\\s\\\\S]*)\\\\};?\\\\s*$/;\\n        const fullMatch = cleanCode.match(fullFunctionPattern);\\n        \\n        if (fullMatch && fullMatch[1]) {\\n            extractedCode = fullMatch[1].trim();\\n        } else {\\n            // 方法2: 查找函数体内容(去掉外层函数声明)\\n            const functionBodyPattern = /function\\\\s*\\\\([^)]*\\\\)\\\\s*\\\\{([\\\\s\\\\S]*)\\\\}/;\\n            const bodyMatch = cleanCode.match(functionBodyPattern);\\n            \\n            if (bodyMatch && bodyMatch[1]) {\\n                extractedCode = bodyMatch[1].trim();\\n            } else {\\n                // 方法3: 查找大括号内的所有内容\\n                const bracePattern = /\\\\{([\\\\s\\\\S]*)\\\\}/;\\n                const braceMatch = cleanCode.match(bracePattern);\\n                \\n                if (braceMatch && braceMatch[1]) {\\n                    extractedCode = braceMatch[1].trim();\\n                } else {\\n                    // 方法4: 逐行解析\\n                    const lines = cleanCode.split('\\\\n');\\n                    let inFunction = false;\\n                    let braceCount = 0;\\n                    let functionBody = [];\\n                    \\n                    for (const line of lines) {\\n                        const trimmedLine = line.trim();\\n                        \\n                        // 检测函数开始\\n                        if (trimmedLine.includes('customFactor') && trimmedLine.includes('function')) {\\n                            inFunction = true;\\n                            const braceIndex = line.indexOf('{');\\n                            if (braceIndex !== -1) {\\n                                braceCount++;\\n                                const afterBrace = line.substring(braceIndex + 1).trim();\\n                                if (afterBrace) {\\n                                    functionBody.push(afterBrace);\\n                                }\\n                            }\\n                            continue;\\n                        }\\n                        \\n                        if (inFunction) {\\n                            // 计算大括号\\n                            for (const char of line) {\\n                                if (char === '{') braceCount++;\\n                                if (char === '}') braceCount--;\\n                            }\\n                            \\n                            if (braceCount > 0) {\\n                                functionBody.push(line);\\n                            } else {\\n                                // 函数结束,取最后一行的内容(去掉结束大括号)\\n                                const lastBraceIndex = line.lastIndexOf('}');\\n                                if (lastBraceIndex !== -1) {\\n                                    const beforeBrace = line.substring(0, lastBraceIndex).trim();\\n                                    if (beforeBrace) {\\n                                        functionBody.push(beforeBrace);\\n                                    }\\n                                }\\n                                break;\\n                            }\\n                        }\\n                    }\\n                    \\n                    extractedCode = functionBody.join('\\\\n').trim();\\n                }\\n            }\\n        }\\n        \\n        // 4. 验证提取的代码\\n        if (!extractedCode || extractedCode.length < 10) {\\n            throw new Error('提取的代码太短或为空');\\n        }\\n        \\n        // 5. 确保代码有基本的安全检查\\n        if (!extractedCode.includes('if') && !extractedCode.includes('return')) {\\n            // 如果没有基本的条件判断和返回语句,添加安全包装\\n            extractedCode = `\\n                if (closes.length < lookback + 1) return null;\\n                try {\\n                    ${extractedCode}\\n                } catch (error) {\\n                    console.error('因子计算错误:', error);\\n                    return null;\\n                }\\n            `;\\n        }\\n        \\n        return {\\n            success: true,\\n            originalCode: factorCodeText,\\n            extractedCode: extractedCode,\\n            method: fullMatch ? 'fullFunction' : bodyMatch ? 'functionBody' : braceMatch ? 'braceContent' : 'lineByLine'\\n        };\\n        \\n    } catch (error) {\\n        // 返回默认的动量因子作为fallback\\n        return {\\n            success: false,\\n            error: error.message,\\n            originalCode: factorCodeText,\\n            extractedCode: `\\n                if (closes.length < lookback + 1) return null;\\n                const currentPrice = closes[closes.length - 1];\\n                const pastPrice = closes[closes.length - 1 - lookback];\\n                return (currentPrice - pastPrice) / pastPrice;\\n            `,\\n            method: 'fallback'\\n        };\\n    }\\n}\\n\\n// 获取AI结果并提取代码\\nconst aiResult = $node[\\\"AI实现\\\"].json;\\nconst codeExtraction = extractFactorCodeRobust(aiResult);\\n\\n// ========== 工具类 ==========\\nclass MathUtils {\\n    static mean(arr) {\\n        if (arr.length === 0) return 0;\\n        return arr.reduce((a, b) => a + b, 0) / arr.length;\\n    }\\n\\n    static std(arr) {\\n        if (arr.length === 0) return 0;\\n        const avg = this.mean(arr);\\n        const squareDiffs = arr.map(value => Math.pow(value - avg, 2));\\n        return Math.sqrt(this.mean(squareDiffs));\\n    }\\n\\n    static correlation(arr1, arr2) {\\n        if (arr1.length !== arr2.length || arr1.length === 0) return 0;\\n        \\n        const mean1 = this.mean(arr1);\\n        const mean2 = this.mean(arr2);\\n        const std1 = this.std(arr1);\\n        const std2 = this.std(arr2);\\n        \\n        if (std1 === 0 || std2 === 0) return 0;\\n        \\n        let sum = 0;\\n        for (let i = 0; i < arr1.length; i++) {\\n            sum += (arr1[i] - mean1) * (arr2[i] - mean2);\\n        }\\n        \\n        return sum / (arr1.length * std1 * std2);\\n    }\\n\\n    static autocorrelation(arr, lag) {\\n        if (arr.length < lag + 1) return 0;\\n        const arr1 = arr.slice(0, -lag);\\n        const arr2 = arr.slice(lag);\\n        return this.correlation(arr1, arr2);\\n    }\\n\\n    static zScore(arr) {\\n        const avg = this.mean(arr);\\n        const stdDev = this.std(arr);\\n        if (stdDev === 0) return arr.map(() => 0);\\n        return arr.map(x => (x - avg) / stdDev);\\n    }\\n\\n    static rankArray(arr) {\\n        const sorted = arr.map((val, idx) => ({ val, idx }))\\n            .sort((a, b) => a.val - b.val);\\n        \\n        const ranks = new Array(arr.length);\\n        for (let i = 0; i < sorted.length; i++) {\\n            ranks[sorted[i].idx] = (i + 1) / arr.length;\\n        }\\n        return ranks;\\n    }\\n\\n    static skewness(arr) {\\n        if (arr.length < 3) return 0;\\n        const avg = this.mean(arr);\\n        const stdDev = this.std(arr);\\n        if (stdDev === 0) return 0;\\n        \\n        const n = arr.length;\\n        const sumCubed = arr.reduce((sum, x) => \\n            sum + Math.pow((x - avg) / stdDev, 3), 0);\\n        \\n        return (n / ((n - 1) * (n - 2))) * sumCubed;\\n    }\\n\\n    static percentile(arr, p) {\\n        if (arr.length === 0) return 0;\\n        const sorted = [...arr].sort((a, b) => a - b);\\n        const index = (p / 100) * (sorted.length - 1);\\n        const lower = Math.floor(index);\\n        const upper = Math.ceil(index);\\n        const weight = index - lower;\\n        return sorted[lower] * (1 - weight) + sorted[upper] * weight;\\n    }\\n\\n    static maxDrawdown(returns) {\\n        if (returns.length === 0) return 0;\\n        let cumulative = 1;\\n        let peak = 1;\\n        let maxDD = 0;\\n        \\n        for (const ret of returns) {\\n            cumulative *= (1 + ret);\\n            if (cumulative > peak) peak = cumulative;\\n            const drawdown = (peak - cumulative) / peak;\\n            if (drawdown > maxDD) maxDD = drawdown;\\n        }\\n        \\n        return maxDD;\\n    }\\n}\\n\\n// ========== 因子计算器(动态代码卡槽) ==========\\nclass FactorCalculator {\\n    // 动态因子计算函数 - 使用AI生成的代码\\n    static customFactor(closes, volumes, highs, lows, opens, lookback) {\\n        // ========== 动态因子计算代码卡槽 START ==========\\n        // 这里会被动态替换\\n        if (closes.length < lookback + 1) return null;\\n        const currentPrice = closes[closes.length - 1];\\n        const pastPrice = closes[closes.length - 1 - lookback];\\n        return (currentPrice - pastPrice) / pastPrice;\\n        // ========== 动态因子计算代码卡槽 END ==========\\n    }\\n\\n    static momentum(prices, lookback) {\\n        if (prices.length < lookback + 1) return null;\\n        const current = prices[prices.length - 1];\\n        const past = prices[prices.length - 1 - lookback];\\n        return (current - past) / past;\\n    }\\n\\n    static volatility(prices, lookback) {\\n        if (prices.length < lookback + 1) return null;\\n        const returns = [];\\n        for (let i = prices.length - lookback; i < prices.length; i++) {\\n            returns.push((prices[i] - prices[i - 1]) / prices[i - 1]);\\n        }\\n        return MathUtils.std(returns);\\n    }\\n\\n    static volumeRatio(volumes, lookback) {\\n        if (volumes.length < lookback * 2) return null;\\n        const recentVol = MathUtils.mean(volumes.slice(-lookback));\\n        const pastVol = MathUtils.mean(volumes.slice(-lookback * 2, -lookback));\\n        if (pastVol === 0) return null;\\n        return recentVol / pastVol - 1;\\n    }\\n\\n    static calculate(type, closes, volumes, highs, lows, opens, lookback) {\\n        switch (type) {\\n            case 'custom':\\n                return this.customFactor(closes, volumes, highs, lows, opens, lookback);\\n            case 'momentum':\\n                return this.momentum(closes, lookback);\\n            case 'volatility':\\n                return this.volatility(closes, lookback);\\n            case 'volume_ratio':\\n                return this.volumeRatio(volumes, lookback);\\n            default:\\n                return this.customFactor(closes, volumes, highs, lows, opens, lookback);\\n        }\\n    }\\n}\\n\\n// 动态替换customFactor函数的实现\\ntry {\\n    const dynamicFactorFunction = new Function(\\n        'closes', 'volumes', 'highs', 'lows', 'opens', 'lookback',\\n        codeExtraction.extractedCode\\n    );\\n    \\n    // 替换原有的customFactor方法\\n    FactorCalculator.customFactor = dynamicFactorFunction;\\n    \\n} catch (error) {\\n    console.error('动态函数创建失败,使用默认实现:', error);\\n    codeExtraction.functionError = error.message;\\n}\\n\\n// ========== 完整因子验证器 ==========\\nclass ComprehensiveFactorValidator {\\n    constructor(config = {}) {\\n        this.config = {\\n            factorType: config.factorType || 'custom',\\n            lookbackPeriod: config.lookbackPeriod || 20,\\n            topN: config.topN || 5,\\n            groupCount: config.groupCount || 5,\\n            enableShort: config.enableShort !== false,\\n            tradingFee: config.tradingFee || 0.0004,\\n            slippage: config.slippage || 0.0005,\\n            neutralizationMethod: config.neutralizationMethod || 'zscore',\\n            minSymbols: config.minSymbols || 5,\\n            minDays: config.minDays || 60\\n        };\\n    }\\n\\n    cleanData(klines) {\\n        return klines.filter(k => {\\n            if (!k.close || !k.open || !k.high || !k.low || !k.volume) return false;\\n            const dailyReturn = Math.abs((k.close - k.open) / k.open);\\n            if (dailyReturn > 0.5) return false;\\n            if (k.high < k.low || k.high < Math.max(k.open, k.close) || \\n                k.low > Math.min(k.open, k.close)) return false;\\n            return true;\\n        });\\n    }\\n\\n    groupBySymbol(data) {\\n        const grouped = {};\\n        data.forEach(item => {\\n            if (!grouped[item.symbol]) grouped[item.symbol] = [];\\n            grouped[item.symbol].push(item);\\n        });\\n        \\n        Object.keys(grouped).forEach(symbol => {\\n            grouped[symbol] = this.cleanData(grouped[symbol]);\\n            grouped[symbol].sort((a, b) => a.date - b.date);\\n            if (grouped[symbol].length < this.config.minDays) {\\n                delete grouped[symbol];\\n            }\\n        });\\n        \\n        return grouped;\\n    }\\n\\n    getAllTradingDates(groupedData) {\\n        const allDates = new Set();\\n        Object.values(groupedData).forEach(klines => {\\n            klines.forEach(k => allDates.add(k.date));\\n        });\\n        return Array.from(allDates).sort((a, b) => a - b);\\n    }\\n\\n    getPriceAtDate(klines, date) {\\n        const item = klines.find(k => k.date === date);\\n        return item ? item.close : null;\\n    }\\n\\n    getVolumeAtDate(klines, date) {\\n        const item = klines.find(k => k.date === date);\\n        return item ? item.volume : null;\\n    }\\n\\n    calculateFactorsForDate(groupedData, date) {\\n        const factors = [];\\n        \\n        Object.keys(groupedData).forEach(symbol => {\\n            const klines = groupedData[symbol];\\n            const dateIndex = klines.findIndex(k => k.date === date);\\n            \\n            if (dateIndex < this.config.lookbackPeriod) return;\\n            \\n            const historicalKlines = klines.slice(0, dateIndex + 1);\\n            const closes = historicalKlines.map(k => k.close);\\n            const volumes = historicalKlines.map(k => k.volume);\\n            const highs = historicalKlines.map(k => k.high);\\n            const lows = historicalKlines.map(k => k.low);\\n            const opens = historicalKlines.map(k => k.open);\\n            \\n            const factor = FactorCalculator.calculate(\\n                this.config.factorType, closes, volumes, highs, lows, opens, this.config.lookbackPeriod\\n            );\\n            \\n            if (factor !== null && !isNaN(factor) && isFinite(factor)) {\\n                const marketCap = closes[closes.length - 1] * volumes[volumes.length - 1];\\n                \\n                factors.push({ \\n                    symbol, \\n                    factor,\\n                    marketCap,\\n                    price: closes[closes.length - 1],\\n                    volume: volumes[volumes.length - 1]\\n                });\\n            }\\n        });\\n        \\n        return factors;\\n    }\\n\\n    neutralizeFactors(factors) {\\n        const factorValues = factors.map(f => f.factor);\\n        const normalized = this.config.neutralizationMethod === 'rank' ? \\n            MathUtils.rankArray(factorValues) : MathUtils.zScore(factorValues);\\n        \\n        return factors.map((f, i) => ({ ...f, normalizedFactor: normalized[i] }));\\n    }\\n\\n    calculateIC(factors, groupedData, currentDate, nextDate) {\\n        const samples = [];\\n        \\n        factors.forEach(f => {\\n            const klines = groupedData[f.symbol];\\n            if (!klines) return;\\n            \\n            const currentPrice = this.getPriceAtDate(klines, currentDate);\\n            const nextPrice = this.getPriceAtDate(klines, nextDate);\\n            \\n            if (currentPrice && nextPrice && currentPrice > 0) {\\n                samples.push({\\n                    factor: f.normalizedFactor || f.factor,\\n                    return: (nextPrice - currentPrice) / currentPrice,\\n                    marketCap: f.marketCap\\n                });\\n            }\\n        });\\n        \\n        if (samples.length < 3) return null;\\n        \\n        const ic = MathUtils.correlation(\\n            samples.map(s => s.factor),\\n            samples.map(s => s.return)\\n        );\\n        \\n        const rankIC = MathUtils.correlation(\\n            MathUtils.rankArray(samples.map(s => s.factor)),\\n            MathUtils.rankArray(samples.map(s => s.return))\\n        );\\n        \\n        return { ic, rankIC, samples };\\n    }\\n\\n    analyzeMonotonicity(factors, groupedData, currentDate, nextDate) {\\n        const samples = [];\\n        \\n        factors.forEach(f => {\\n            const klines = groupedData[f.symbol];\\n            if (!klines) return;\\n            \\n            const currentPrice = this.getPriceAtDate(klines, currentDate);\\n            const nextPrice = this.getPriceAtDate(klines, nextDate);\\n            \\n            if (currentPrice && nextPrice && currentPrice > 0) {\\n                samples.push({\\n                    symbol: f.symbol,\\n                    factor: f.normalizedFactor || f.factor,\\n                    return: (nextPrice - currentPrice) / currentPrice\\n                });\\n            }\\n        });\\n        \\n        if (samples.length < this.config.groupCount * 2) return null;\\n        \\n        samples.sort((a, b) => a.factor - b.factor);\\n        const groupSize = Math.floor(samples.length / this.config.groupCount);\\n        const groups = [];\\n        \\n        for (let i = 0; i < this.config.groupCount; i++) {\\n            const start = i * groupSize;\\n            const end = i === this.config.groupCount - 1 ? samples.length : (i + 1) * groupSize;\\n            const groupSamples = samples.slice(start, end);\\n            \\n            const returns = groupSamples.map(s => s.return);\\n            const avgReturn = MathUtils.mean(returns);\\n            const stdReturn = MathUtils.std(returns);\\n            \\n            groups.push({\\n                group: i + 1,\\n                avgFactor: MathUtils.mean(groupSamples.map(s => s.factor)),\\n                avgReturn: avgReturn,\\n                stdReturn: stdReturn,\\n                sharpe: stdReturn === 0 ? 0 : avgReturn / stdReturn,\\n                count: groupSamples.length,\\n                symbols: groupSamples.map(s => s.symbol)\\n            });\\n        }\\n        \\n        const returns = groups.map(g => g.avgReturn);\\n        let isMonotonic = true;\\n        for (let i = 1; i < returns.length; i++) {\\n            if (returns[i] < returns[i - 1]) {\\n                isMonotonic = false;\\n                break;\\n            }\\n        }\\n        \\n        const groupIndices = groups.map((g, i) => i + 1);\\n        const monotonicityScore = MathUtils.correlation(groupIndices, returns);\\n        \\n        return {\\n            groups,\\n            isMonotonic,\\n            monotonicityScore,\\n            longShortReturn: groups[groups.length - 1].avgReturn - groups[0].avgReturn\\n        };\\n    }\\n\\n    analyzeLongShortSymmetry(monotonicityResult) {\\n        if (!monotonicityResult) return null;\\n        \\n        const groups = monotonicityResult.groups;\\n        const topGroup = groups[groups.length - 1];\\n        const bottomGroup = groups[0];\\n        \\n        const longReturn = topGroup.avgReturn;\\n        const shortReturn = -bottomGroup.avgReturn;\\n        \\n        const asymmetry = Math.abs(longReturn - shortReturn) / (Math.abs(longReturn) + Math.abs(shortReturn));\\n        const skew = (longReturn + shortReturn) / (Math.abs(longReturn) + Math.abs(shortReturn));\\n        \\n        return {\\n            longReturn,\\n            shortReturn,\\n            longShortReturn: longReturn + shortReturn,\\n            asymmetry,\\n            skew,\\n            interpretation: asymmetry < 0.2 ? 'symmetric' : asymmetry < 0.5 ? 'moderate' : 'asymmetric'\\n        };\\n    }\\n\\n    analyzeDomain(factors, groupedData, currentDate, nextDate) {\\n        const samples = [];\\n        \\n        factors.forEach(f => {\\n            const klines = groupedData[f.symbol];\\n            if (!klines) return;\\n            \\n            const currentPrice = this.getPriceAtDate(klines, currentDate);\\n            const nextPrice = this.getPriceAtDate(klines, nextDate);\\n            \\n            if (currentPrice && nextPrice && currentPrice > 0) {\\n                samples.push({\\n                    factor: f.normalizedFactor || f.factor,\\n                    return: (nextPrice - currentPrice) / currentPrice,\\n                    marketCap: f.marketCap\\n                });\\n            }\\n        });\\n        \\n        if (samples.length < 9) return null;\\n        \\n        samples.sort((a, b) => a.marketCap - b.marketCap);\\n        const tercileSize = Math.floor(samples.length / 3);\\n        \\n        const domains = {\\n            small: samples.slice(0, tercileSize),\\n            medium: samples.slice(tercileSize, tercileSize * 2),\\n            large: samples.slice(tercileSize * 2)\\n        };\\n        \\n        const domainAnalysis = {};\\n        \\n        for (const [name, domainSamples] of Object.entries(domains)) {\\n            const ic = MathUtils.correlation(\\n                domainSamples.map(s => s.factor),\\n                domainSamples.map(s => s.return)\\n            );\\n            \\n            domainAnalysis[name] = {\\n                ic,\\n                count: domainSamples.length,\\n                avgReturn: MathUtils.mean(domainSamples.map(s => s.return))\\n            };\\n        }\\n        \\n        return domainAnalysis;\\n    }\\n\\n    analyzeDecay(dailyICs) {\\n        if (dailyICs.length < 10) return null;\\n        \\n        const autocorrs = [];\\n        for (let lag = 1; lag <= Math.min(10, Math.floor(dailyICs.length / 3)); lag++) {\\n            autocorrs.push({\\n                lag,\\n                autocorr: MathUtils.autocorrelation(dailyICs, lag)\\n            });\\n        }\\n        \\n        let halfLife = null;\\n        for (let i = 0; i < autocorrs.length; i++) {\\n            if (autocorrs[i].autocorr < 0.5) {\\n                halfLife = autocorrs[i].lag;\\n                break;\\n            }\\n        }\\n        \\n        if (halfLife === null) {\\n            halfLife = autocorrs.length;\\n        }\\n        \\n        let persistence = 'low';\\n        if (halfLife > 7) persistence = 'high';\\n        else if (halfLife > 3) persistence = 'medium';\\n        \\n        return {\\n            halfLife,\\n            persistence,\\n            autocorrelations: autocorrs,\\n            interpretation: halfLife > 5 ? \\n                '因子持续性强,适合低频调仓' : \\n                '因子持续性弱,需高频调仓'\\n        };\\n    }\\n\\n    validate(klineData) {\\n        try {\\n            const groupedData = this.groupBySymbol(klineData);\\n            const symbols = Object.keys(groupedData);\\n            \\n            if (symbols.length < this.config.minSymbols) {\\n                throw new Error(`币种数不足: ${symbols.length} < ${this.config.minSymbols}`);\\n            }\\n            \\n            const allDates = this.getAllTradingDates(groupedData);\\n            const startIndex = this.config.lookbackPeriod;\\n            \\n            if (allDates.length - startIndex < this.config.minDays) {\\n                throw new Error(`交易日不足`);\\n            }\\n            \\n            const dailyICs = [];\\n            const dailyRankICs = [];\\n            const dailyMonotonicities = [];\\n            const dailySymmetries = [];\\n            const dailyDomains = [];\\n            const netReturns = [];\\n            const grossReturns = [];\\n            const turnovers = [];\\n            let prevLong = [];\\n            let prevShort = [];\\n            \\n            for (let i = startIndex; i < allDates.length - 1; i++) {\\n                const currentDate = allDates[i];\\n                const nextDate = allDates[i + 1];\\n                \\n                let factors = this.calculateFactorsForDate(groupedData, currentDate);\\n                if (factors.length < this.config.minSymbols) continue;\\n                \\n                factors = this.neutralizeFactors(factors);\\n                \\n                const icResult = this.calculateIC(factors, groupedData, currentDate, nextDate);\\n                if (icResult) {\\n                    dailyICs.push(icResult.ic);\\n                    dailyRankICs.push(icResult.rankIC);\\n                }\\n                \\n                const monotonicityResult = this.analyzeMonotonicity(\\n                    factors, groupedData, currentDate, nextDate\\n                );\\n                if (monotonicityResult) {\\n                    dailyMonotonicities.push(monotonicityResult);\\n                    \\n                    const symmetry = this.analyzeLongShortSymmetry(monotonicityResult);\\n                    if (symmetry) dailySymmetries.push(symmetry);\\n                }\\n                \\n                const domainResult = this.analyzeDomain(\\n                    factors, groupedData, currentDate, nextDate\\n                );\\n                if (domainResult) dailyDomains.push(domainResult);\\n                \\n                const sorted = [...factors].sort((a, b) => \\n                    (b.normalizedFactor || b.factor) - (a.normalizedFactor || a.factor)\\n                );\\n                \\n                const long = sorted.slice(0, this.config.topN).map(f => f.symbol);\\n                const short = this.config.enableShort ? \\n                    sorted.slice(-this.config.topN).map(f => f.symbol) : [];\\n                \\n                const longTurnover = this.calculateTurnover(prevLong, long);\\n                const shortTurnover = this.config.enableShort ? \\n                    this.calculateTurnover(prevShort, short) : 0;\\n                \\n                turnovers.push((longTurnover + shortTurnover) / 2);\\n                \\n                const longReturn = this.calculateDailyReturn(long, groupedData, currentDate, nextDate);\\n                const shortReturn = this.config.enableShort ? \\n                    this.calculateDailyReturn(short, groupedData, currentDate, nextDate) : 0;\\n                \\n                const grossReturn = this.config.enableShort ? \\n                    (longReturn - shortReturn) / 2 : longReturn;\\n                const cost = (longTurnover + shortTurnover) * \\n                    (this.config.tradingFee + this.config.slippage);\\n                const netReturn = grossReturn - cost;\\n                \\n                grossReturns.push(grossReturn);\\n                netReturns.push(netReturn);\\n                \\n                prevLong = long;\\n                prevShort = short;\\n            }\\n            \\n            if (netReturns.length === 0) {\\n                throw new Error('没有有效的回测日期');\\n            }\\n            \\n            return this.generateReport({\\n                dailyICs,\\n                dailyRankICs,\\n                dailyMonotonicities,\\n                dailySymmetries,\\n                dailyDomains,\\n                netReturns,\\n                grossReturns,\\n                turnovers,\\n                symbolCount: symbols.length\\n            });\\n            \\n        } catch (error) {\\n            return {\\n                success: false,\\n                error: error.message\\n            };\\n        }\\n    }\\n\\n    calculateTurnover(oldPositions, newPositions) {\\n        if (oldPositions.length === 0) return 1;\\n        \\n        const allSymbols = new Set([...oldPositions, ...newPositions]);\\n        let changes = 0;\\n        \\n        allSymbols.forEach(symbol => {\\n            const oldWeight = oldPositions.includes(symbol) ? 1 : 0;\\n            const newWeight = newPositions.includes(symbol) ? 1 : 0;\\n            changes += Math.abs(newWeight - oldWeight);\\n        });\\n        \\n        return changes / (2 * oldPositions.length);\\n    }\\n\\n    calculateDailyReturn(positions, groupedData, currentDate, nextDate) {\\n        let totalReturn = 0;\\n        let validCount = 0;\\n        \\n        positions.forEach(symbol => {\\n            const klines = groupedData[symbol];\\n            if (!klines) return;\\n            \\n            const currentPrice = this.getPriceAtDate(klines, currentDate);\\n            const nextPrice = this.getPriceAtDate(klines, nextDate);\\n            \\n            if (currentPrice && nextPrice && currentPrice > 0) {\\n                totalReturn += (nextPrice - currentPrice) / currentPrice;\\n                validCount++;\\n            }\\n        });\\n        \\n        return validCount > 0 ? totalReturn / validCount : 0;\\n    }\\n\\n    generateReport(data) {\\n        const {\\n            dailyICs,\\n            dailyRankICs,\\n            dailyMonotonicities,\\n            dailySymmetries,\\n            dailyDomains,\\n            netReturns,\\n            grossReturns,\\n            turnovers,\\n            symbolCount\\n        } = data;\\n        \\n        const neutralization = {\\n            method: this.config.neutralizationMethod,\\n            applied: true\\n        };\\n        \\n        const cumNetReturn = netReturns.reduce((cum, r) => cum * (1 + r), 1) - 1;\\n        const cumGrossReturn = grossReturns.reduce((cum, r) => cum * (1 + r), 1) - 1;\\n        const tradingDays = netReturns.length;\\n        const annualNetReturn = Math.pow(1 + cumNetReturn, 252 / tradingDays) - 1;\\n        const annualVol = MathUtils.std(netReturns) * Math.sqrt(252);\\n        \\n        const factorReturn = {\\n            cumulative: cumNetReturn,\\n            annualized: annualNetReturn,\\n            volatility: annualVol,\\n            sharpe: annualVol === 0 ? 0 : annualNetReturn / annualVol,\\n            maxDrawdown: MathUtils.maxDrawdown(netReturns)\\n        };\\n        \\n        const avgIC = MathUtils.mean(dailyICs);\\n        const avgRankIC = MathUtils.mean(dailyRankICs);\\n        const icWinRate = dailyICs.filter(ic => ic > 0).length / dailyICs.length;\\n        \\n        const ic = {\\n            mean: avgIC,\\n            rankIC: avgRankIC,\\n            std: MathUtils.std(dailyICs),\\n            winRate: icWinRate,\\n            tStat: Math.abs(avgIC) * Math.sqrt(dailyICs.length) / MathUtils.std(dailyICs)\\n        };\\n        \\n        const icStd = MathUtils.std(dailyICs);\\n        const ir = {\\n            value: icStd === 0 ? 0 : avgIC / icStd,\\n            rankIR: MathUtils.std(dailyRankICs) === 0 ? 0 : \\n                avgRankIC / MathUtils.std(dailyRankICs),\\n            interpretation: ''\\n        };\\n        \\n        if (Math.abs(ir.value) > 2) ir.interpretation = 'excellent';\\n        else if (Math.abs(ir.value) > 1) ir.interpretation = 'good';\\n        else if (Math.abs(ir.value) > 0.5) ir.interpretation = 'acceptable';\\n        else ir.interpretation = 'poor';\\n        \\n        const avgMonotonicity = MathUtils.mean(\\n            dailyMonotonicities.map(m => m.monotonicityScore)\\n        );\\n        const monotonicDays = dailyMonotonicities.filter(m => m.isMonotonic).length;\\n        \\n        const lastMonotonicity = dailyMonotonicities[dailyMonotonicities.length - 1];\\n        \\n        const monotonicity = {\\n            score: avgMonotonicity,\\n            monotonicRate: monotonicDays / dailyMonotonicities.length,\\n            groups: lastMonotonicity ? lastMonotonicity.groups : [],\\n            longShortReturn: MathUtils.mean(\\n                dailyMonotonicities.map(m => m.longShortReturn)\\n            ),\\n            interpretation: avgMonotonicity > 0.8 ? 'strong' : \\n                avgMonotonicity > 0.5 ? 'moderate' : 'weak'\\n        };\\n        \\n        const decay = this.analyzeDecay(dailyICs);\\n        \\n        const avgSymmetry = dailySymmetries.length > 0 ? {\\n            longReturn: MathUtils.mean(dailySymmetries.map(s => s.longReturn)),\\n            shortReturn: MathUtils.mean(dailySymmetries.map(s => s.shortReturn)),\\n            asymmetry: MathUtils.mean(dailySymmetries.map(s => s.asymmetry)),\\n            skew: MathUtils.mean(dailySymmetries.map(s => s.skew)),\\n            interpretation: MathUtils.mean(dailySymmetries.map(s => s.asymmetry)) < 0.2 ? \\n                'symmetric' : 'asymmetric'\\n        } : null;\\n        \\n        const avgDomain = dailyDomains.length > 0 ? {\\n            small: {\\n                ic: MathUtils.mean(dailyDomains.map(d => d.small.ic)),\\n                count: dailyDomains[0].small.count\\n            },\\n            medium: {\\n                ic: MathUtils.mean(dailyDomains.map(d => d.medium.ic)),\\n                count: dailyDomains[0].medium.count\\n            },\\n            large: {\\n                ic: MathUtils.mean(dailyDomains.map(d => d.large.ic)),\\n                count: dailyDomains[0].large.count\\n            },\\n            consistency: null\\n        } : null;\\n        \\n        if (avgDomain) {\\n            const ics = [avgDomain.small.ic, avgDomain.medium.ic, avgDomain.large.ic];\\n            const allSameSign = ics.every(ic => ic * avgIC > 0);\\n            const icStd = MathUtils.std(ics);\\n            \\n            avgDomain.consistency = allSameSign && icStd < 0.05 ? 'high' : \\n                allSameSign ? 'medium' : 'low';\\n        }\\n        \\n        const avgTurnover = MathUtils.mean(turnovers);\\n        const totalCost = cumGrossReturn - cumNetReturn;\\n        const costImpact = Math.abs(cumGrossReturn) === 0 ? 0 : \\n            Math.abs(totalCost) / Math.abs(cumGrossReturn);\\n        \\n        const cost = {\\n            avgDailyTurnover: avgTurnover,\\n            costImpact: costImpact,\\n            totalCostDrag: totalCost\\n        };\\n        \\n        return {\\n            idea: $node[\\\"代码\\\"].json.messageText,\\n            success: true,\\n            neutralization,\\n            factorReturn,\\n            ic,\\n            ir,\\n            monotonicity,\\n            decay,\\n            symmetry: avgSymmetry,\\n            domain: avgDomain,\\n            cost,\\n            meta: {\\n                tradingDays,\\n                symbols: symbolCount,\\n                factorType: this.config.factorType,\\n                lookback: this.config.lookbackPeriod\\n            },\\n            codeExtraction: codeExtraction\\n        };\\n    }\\n}\\n\\n// ========== n8n入口代码 ==========\\nconst validatorConfig = {\\n    factorType: 'custom',\\n    lookbackPeriod: 20,\\n    topN: 5,\\n    groupCount: 5,\\n    enableShort: true,\\n    tradingFee: 0.0004,\\n    slippage: 0.0005,\\n    neutralizationMethod: 'zscore',\\n    minSymbols: 5,\\n    minDays: 60\\n};\\n\\nconst validator = new ComprehensiveFactorValidator(validatorConfig);\\nconst result = validator.validate(klineData);\\n\\nreturn result;\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[-816,-240],\"id\":\"fb8cdf84-e0fc-49f0-b132-b143ea55e91a\",\"name\":\"因子验证\"},{\"parameters\":{\"text\":\"=你是一位专业的加密货币量化分析师,擅长解读因子验证报告。请根据输入的因子验证结果,提供**简洁清晰**的分析报告。需要高度简洁!!!\\n\\n## 输入格式\\n你将收到一个包含以下字段的加密货币因子验证结果JSON:\\n- idea: 原始因子想法描述\\n- factorReturn: 因子收益表现\\n- ic: 信息系数(IC)分析  \\n- ir: 信息比率(IR)分析\\n- monotonicity: 单调性分析\\n- decay: 因子持续性分析\\n- symmetry: 多空对称性分析\\n- domain: 分域分析(按市值大小分组)\\n- cost: 交易成本分析\\n- codeExtraction: 生成的因子计算代码\\n- meta: 验证元数据(交易日数、标的数量等)\\n\\n## 输出要求\\n请严格按照以下JSON格式返回分析结果,**每个字段保持简洁,避免冗长描述**:\\n```json\\n{\\n  \\\"idea_evaluation\\\": {\\n    \\\"original_hypothesis\\\": \\\"一句话总结原始想法\\\",\\n    \\\"hypothesis_validity\\\": \\\"1-2句评价理论合理性\\\",\\n    \\\"market_logic_assessment\\\": \\\"1-2句评估市场逻辑\\\",\\n    \\\"implementation_accuracy\\\": \\\"1句评价代码实现准确性\\\"\\n  },\\n  \\\"overall_assessment\\\": {\\n    \\\"score\\\": \\\"数值(0-100)\\\",\\n    \\\"grade\\\": \\\"等级(A+/A/B+/B/C+/C/D)\\\",\\n    \\\"recommendation\\\": \\\"建议(推荐使用/谨慎使用/不建议使用)\\\"\\n  },\\n  \\\"factor_description\\\": {\\n    \\\"factor_logic\\\": \\\"1-2句描述计算逻辑\\\",\\n    \\\"factor_type\\\": \\\"类型(技术/基本面/情绪/量价复合等)\\\",\\n    \\\"expected_behavior\\\": \\\"1句说明预期行为\\\",\\n    \\\"generated_code\\\": \\\"返回生成的完整代码\\\"\\n  },\\n  \\\"idea_vs_reality\\\": {\\n    \\\"expectation_match\\\": \\\"1句说明是否符合预期\\\",\\n    \\\"prediction_accuracy\\\": \\\"1句评估预测准确性\\\",\\n    \\\"signal_strength\\\": \\\"1句评价信号强度\\\",\\n    \\\"market_behavior_alignment\\\": \\\"1句说明行为一致性\\\"\\n  },\\n  \\\"performance_analysis\\\": {\\n    \\\"return_assessment\\\": \\\"关键指标+简短评价(年化收益、胜率等)\\\",\\n    \\\"risk_assessment\\\": \\\"关键指标+简短评价(最大回撤、波动率等)\\\", \\n    \\\"sharpe_interpretation\\\": \\\"夏普比率值+一句解读\\\"\\n  },\\n  \\\"effectiveness_analysis\\\": {\\n    \\\"ic_interpretation\\\": \\\"IC均值+t统计量+一句评价\\\",\\n    \\\"predictive_power\\\": \\\"1句总结预测能力\\\",\\n    \\\"statistical_significance\\\": \\\"显著性结论(显著/不显著)\\\"\\n  },\\n  \\\"stability_analysis\\\": {\\n    \\\"monotonicity_assessment\\\": \\\"单调性结论(好/一般/差)+关键数据\\\",\\n    \\\"persistence_evaluation\\\": \\\"衰减周期+建议调仓频率\\\",\\n    \\\"market_cap_consistency\\\": \\\"1句评价不同市值表现\\\",\\n    \\\"robustness_score\\\": \\\"稳健性评分(0-100)\\\"\\n  },\\n  \\\"practical_considerations\\\": {\\n    \\\"trading_feasibility\\\": \\\"可行性结论+关键约束\\\",\\n    \\\"cost_impact\\\": \\\"换手率+成本侵蚀幅度\\\",\\n    \\\"implementation_difficulty\\\": \\\"难度等级(低/中/高)+主要挑战\\\",\\n    \\\"market_regime_sensitivity\\\": \\\"1句说明市场敏感性\\\"\\n  },\\n  \\\"crypto_specific_insights\\\": {\\n    \\\"volatility_adaptation\\\": \\\"1句评价波动适应性\\\",\\n    \\\"liquidity_considerations\\\": \\\"流动性要求描述\\\",\\n    \\\"market_microstructure\\\": \\\"1句关键微观结构影响\\\"\\n  },\\n  \\\"improvement_suggestions\\\": [\\n    \\\"具体建议1(一句话)\\\",\\n    \\\"具体建议2(一句话)\\\", \\n    \\\"具体建议3(一句话)\\\"\\n  ],\\n  \\\"risk_warnings\\\": [\\n    \\\"风险点1(一句话)\\\",\\n    \\\"风险点2(一句话)\\\",\\n    \\\"风险点3(一句话)\\\"\\n  ],\\n  \\\"conclusion\\\": \\\"2-3句话总结:想法有效性+验证结果+最终建议,不超过30字\\\"\\n}\\n```\\n\\n## 分析要点\\n1. **想法验证优先**:首先评估原始想法的合理性\\n2. **数据说话**:用关键指标支撑结论,少用形容词\\n3. **简洁表达**:每个评价控制在1-2句话\\n4. **突出重点**:聚焦最关键的发现和问题\\n5. **可操作性**:建议具体明确,可直接执行\\n\\n## 加密货币市场评分标准(快速参考)\\n- **IC**: >0.08优秀 | 0.04-0.08良好 | 0.02-0.04一般 | <0.02较差\\n- **夏普**: >1.5优秀 | 0.8-1.5良好 | 0.3-0.8一般 | <0.3较差\\n- **回撤**: <5%优秀 | 5-15%良好 | 15-25%一般 | >25%警惕\\n- **换手**: <20%低频 | 20-50%中频 | >50%高频\\n\\n请根据输入的加密货币因子验证结果,**用最简洁的语言**提供专业分析报告。\\n\\n{{ JSON.stringify($json)}}\",\"options\":{}},\"type\":\"@n8n/n8n-nodes-langchain.agent\",\"typeVersion\":1,\"position\":[-592,-240],\"id\":\"7c948bb7-2161-4bed-9702-ce5ac5ff5474\",\"name\":\"结果解释\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"// 获取输入文本,如果不存在则使用空字符串\\nconst inputText = $input.first().json.output || \\\"\\\";\\n\\n// 验证输入类型\\nif (typeof inputText !== \\\"string\\\") {\\n  throw new Error(\\\"Input must be a string\\\");\\n}\\n\\n// 先清理markdown代码块标记\\nlet cleanedJson = inputText\\n  .replace(/```json\\\\s*/g, \\\"\\\")\\n  .replace(/\\\\s*```\\\\s*$/g, \\\"\\\")\\n  .trim();\\n\\n// 解析JSON内容\\nlet parsedData;\\ntry {\\n  parsedData = JSON.parse(cleanedJson);\\n} catch (error) {\\n  // 如果第一次解析失败,尝试找到实际的JSON对象\\n  const jsonMatch = cleanedJson.match(/\\\\{[\\\\s\\\\S]*\\\\}/);\\n  if (jsonMatch) {\\n    try {\\n      parsedData = JSON.parse(jsonMatch[0]);\\n    } catch (e) {\\n      throw new Error(\\\"Invalid JSON format: \\\" + e.message);\\n    }\\n  } else {\\n    throw new Error(\\\"No valid JSON found: \\\" + error.message);\\n  }\\n}\\n\\n// 安全获取嵌套属性的辅助函数\\nconst safeGet = (obj, path, defaultValue = \\\"\\\") => {\\n  try {\\n    return path.split('.').reduce((current, prop) => current?.[prop], obj) || defaultValue;\\n  } catch {\\n    return defaultValue;\\n  }\\n};\\n\\n// Telegram MarkdownV2 转义函数\\nconst escapeMarkdownV2 = (text) => {\\n  if (typeof text !== 'string') {\\n    text = String(text);\\n  }\\n  \\n  // 需要转义的字符:_ * [ ] ( ) ~ ` > # + - = | { } . !\\n  return text.replace(/([_*\\\\[\\\\]()~`>#+=|{}.!-])/g, '\\\\\\\\$1');\\n};\\n\\n// 格式化列表项的函数\\nconst formatList = (items, isNumbered = false) => {\\n  if (!Array.isArray(items)) return '';\\n  \\n  return items.map((item, index) => {\\n    const prefix = isNumbered ? `${index + 1}\\\\\\\\. ` : '• ';\\n    return `${prefix}${escapeMarkdownV2(item)}`;\\n  }).join('\\\\n');\\n};\\n\\n// 提取因子评价信息(第一部分)\\nconst evaluationContent = `\\n*因子评价报告*\\n\\n*原始假设:*${escapeMarkdownV2(safeGet(parsedData, 'idea_evaluation.original_hypothesis'))}\\n\\n*假设有效性:*${escapeMarkdownV2(safeGet(parsedData, 'idea_evaluation.hypothesis_validity'))}\\n\\n*市场逻辑评估:*${escapeMarkdownV2(safeGet(parsedData, 'idea_evaluation.market_logic_assessment'))}\\n\\n*实现准确性:*${escapeMarkdownV2(safeGet(parsedData, 'idea_evaluation.implementation_accuracy'))}\\n\\n*综合评估:*\\n\\\\\\\\- 评分:${escapeMarkdownV2(safeGet(parsedData, 'overall_assessment.score'))}/100\\n\\\\\\\\- 等级:${escapeMarkdownV2(safeGet(parsedData, 'overall_assessment.grade'))}\\n\\\\\\\\- 建议:${escapeMarkdownV2(safeGet(parsedData, 'overall_assessment.recommendation'))}\\n\\n*因子描述:*\\n\\\\\\\\- 因子逻辑:${escapeMarkdownV2(safeGet(parsedData, 'factor_description.factor_logic'))}\\n\\\\\\\\- 因子类型:${escapeMarkdownV2(safeGet(parsedData, 'factor_description.factor_type'))}\\n\\\\\\\\- 预期行为:${escapeMarkdownV2(safeGet(parsedData, 'factor_description.expected_behavior'))}\\n\\n*预期与现实对比:*\\n\\\\\\\\- 预期匹配度:${escapeMarkdownV2(safeGet(parsedData, 'idea_vs_reality.expectation_match'))}\\n\\\\\\\\- 预测准确率:${escapeMarkdownV2(safeGet(parsedData, 'idea_vs_reality.prediction_accuracy'))}\\n\\\\\\\\- 信号强度:${escapeMarkdownV2(safeGet(parsedData, 'idea_vs_reality.signal_strength'))}\\n\\\\\\\\- 市场行为对齐:${escapeMarkdownV2(safeGet(parsedData, 'idea_vs_reality.market_behavior_alignment'))}\\n\\n*性能分析:*\\n\\\\\\\\- 收益评估:${escapeMarkdownV2(safeGet(parsedData, 'performance_analysis.return_assessment'))}\\n\\\\\\\\- 风险评估:${escapeMarkdownV2(safeGet(parsedData, 'performance_analysis.risk_assessment'))}\\n\\\\\\\\- 夏普比率解读:${escapeMarkdownV2(safeGet(parsedData, 'performance_analysis.sharpe_interpretation'))}\\n\\n*有效性分析:*\\n\\\\\\\\- IC解读:${escapeMarkdownV2(safeGet(parsedData, 'effectiveness_analysis.ic_interpretation'))}\\n\\\\\\\\- 预测能力:${escapeMarkdownV2(safeGet(parsedData, 'effectiveness_analysis.predictive_power'))}\\n\\\\\\\\- 统计显著性:${escapeMarkdownV2(safeGet(parsedData, 'effectiveness_analysis.statistical_significance'))}\\n\\n*稳定性分析:*\\n\\\\\\\\- 单调性评估:${escapeMarkdownV2(safeGet(parsedData, 'stability_analysis.monotonicity_assessment'))}\\n\\\\\\\\- 持续性评价:${escapeMarkdownV2(safeGet(parsedData, 'stability_analysis.persistence_evaluation'))}\\n\\\\\\\\- 市值一致性:${escapeMarkdownV2(safeGet(parsedData, 'stability_analysis.market_cap_consistency'))}\\n\\\\\\\\- 稳健性得分:${escapeMarkdownV2(safeGet(parsedData, 'stability_analysis.robustness_score'))}\\n\\n*实用性考虑:*\\n\\\\\\\\- 交易可行性:${escapeMarkdownV2(safeGet(parsedData, 'practical_considerations.trading_feasibility'))}\\n\\\\\\\\- 成本影响:${escapeMarkdownV2(safeGet(parsedData, 'practical_considerations.cost_impact'))}\\n\\\\\\\\- 实施难度:${escapeMarkdownV2(safeGet(parsedData, 'practical_considerations.implementation_difficulty'))}\\n\\\\\\\\- 市场制度敏感性:${escapeMarkdownV2(safeGet(parsedData, 'practical_considerations.market_regime_sensitivity'))}\\n\\n*加密货币特定见解:*\\n\\\\\\\\- 波动性适应:${escapeMarkdownV2(safeGet(parsedData, 'crypto_specific_insights.volatility_adaptation'))}\\n\\\\\\\\- 流动性考虑:${escapeMarkdownV2(safeGet(parsedData, 'crypto_specific_insights.liquidity_considerations'))}\\n\\\\\\\\- 市场微观结构:${escapeMarkdownV2(safeGet(parsedData, 'crypto_specific_insights.market_microstructure'))}\\n\\n*改进建议:*\\n${formatList(parsedData.improvement_suggestions || [], true)}\\n\\n*风险警告:*\\n${formatList(parsedData.risk_warnings || [], true)}\\n\\n*结论:*\\n${escapeMarkdownV2(safeGet(parsedData, 'conclusion'))}\\n`.trim();\\n\\n// 提取代码内容(第二部分)\\nconst codeContent = `\\n*生成的因子代码:*\\n\\n\\\\`\\\\`\\\\`javascript\\n${safeGet(parsedData, 'factor_description.generated_code')}\\n\\\\`\\\\`\\\\`\\n`.trim();\\n\\n// 返回分割后的内容\\nreturn [\\n  { json: { blockNumber: 1, content: evaluationContent } },\\n  { json: { blockNumber: 2, content: codeContent } }\\n];\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[-240,-240],\"id\":\"8ce7981f-71d6-4a20-adfa-b6c582c1813e\",\"name\":\"整理文本\"},{\"parameters\":{\"operation\":\"sendMessage\",\"chatId\":{\"__rl\":true,\"value\":\"=8357264271\",\"mode\":\"id\"},\"text\":\"={{ $json.content }}\",\"parseMode\":\"MarkdownV2\"},\"type\":\"n8n-nodes-base.telegram\",\"typeVersion\":1.2,\"position\":[48,-240],\"id\":\"4f558d01-9d4a-4657-8e1f-ebe694e11619\",\"name\":\"输出结果\",\"credentials\":{\"telegramApi\":{\"id\":\"64b40791-c845-4775-9d67-0efc4122d162\",\"name\":\"Telegram account\"}}},{\"parameters\":{\"notice\":\"\",\"rule\":{\"interval\":[{\"field\":\"seconds\",\"secondsInterval\":3}]},\"updates\":[\"*\"],\"additionalFields\":{}},\"type\":\"n8n-nodes-base.telegramTrigger\",\"typeVersion\":1.2,\"position\":[-1920,-256],\"id\":\"88f492bc-57a4-4e22-8116-3da558266af4\",\"name\":\"Telegram 触发器\",\"credentials\":{\"telegramApi\":{\"id\":\"64b40791-c845-4775-9d67-0efc4122d162\",\"name\":\"Telegram account\"}}},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"const messageText = $input.first().json.message.text;\\n\\n// json 必须是对象,用 value 或其他键名包装\\nreturn [\\n  {\\n    json: {messageText}\\n  }\\n];\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[-1648,-384],\"id\":\"6bbaba0c-fca1-4033-8f82-beb0b3075eb2\",\"name\":\"代码\"},{\"parameters\":{\"operation\":\"getRecords\",\"exchange\":0,\"symbol\":{\"__rl\":true,\"value\":\"={{ $json.symbol }}\",\"mode\":\"id\"},\"period\":86400,\"limit\":365},\"type\":\"n8n-nodes-base.marketInfo\",\"typeVersion\":1,\"position\":[-1232,128],\"id\":\"c5d4cc5c-9da0-4755-891f-ae04a3c8c00a\",\"name\":\"获取K线\"},{\"parameters\":{\"splitInBatchesNotice\":\"\",\"batchSize\":\"={{ $input.all().length }}\",\"options\":{}},\"type\":\"n8n-nodes-base.splitInBatches\",\"typeVersion\":3,\"position\":[-1456,-48],\"id\":\"9f416050-7ca1-40dd-bb65-d917200c7c85\",\"name\":\"循环处理项目\"}],\"pinData\":{\"因子验证\":[{\"json\":{\"idea\":\"连续3天小跌且成交量递减,预测后面会有大跌\",\"success\":true,\"neutralization\":{\"method\":\"zscore\",\"applied\":true},\"factorReturn\":{\"cumulative\":-0.28697045825193923,\"annualized\":-0.26405447496254086,\"volatility\":0.12961360027584387,\"sharpe\":-2.0372435793819452,\"maxDrawdown\":0.3129284999630468},\"ic\":{\"mean\":-0.031204223512864036,\"rankIC\":-0.048513716499328,\"std\":0.27537089687950156,\"winRate\":0.38848920863309355,\"tStat\":1.8893731485229854},\"ir\":{\"value\":-0.11331707114466263,\"rankIR\":-0.14934923506047526,\"interpretation\":\"poor\"},\"monotonicity\":{\"score\":-0.05343559901574033,\"monotonicRate\":0.0035971223021582736,\"groups\":[{\"group\":1,\"avgFactor\":-2.0783640409693707,\"avgReturn\":0.025489717786368028,\"stdReturn\":0.007962630469605707,\"sharpe\":3.2011679913648217,\"count\":2,\"symbols\":[\"LINK_USDT.swap\",\"DOT_USDT.swap\"]},{\"group\":2,\"avgFactor\":-0.5208494693541336,\"avgReturn\":0.011021432872410081,\"stdReturn\":0.0024811542533914113,\"sharpe\":4.442058714142917,\"count\":2,\"symbols\":[\"ETH_USDT.swap\",\"ETC_USDT.swap\"]},{\"group\":3,\"avgFactor\":-0.20289119997613034,\"avgReturn\":0.05175004841992727,\"stdReturn\":0.02516528430935896,\"sharpe\":2.0564062691984546,\"count\":2,\"symbols\":[\"SOL_USDT.swap\",\"LTC_USDT.swap\"]},{\"group\":4,\"avgFactor\":0.16883887151623284,\"avgReturn\":0.019276254116116734,\"stdReturn\":0.0016528300940098929,\"sharpe\":11.662574505375238,\"count\":2,\"symbols\":[\"XRP_USDT.swap\",\"BTC_USDT.swap\"]},{\"group\":5,\"avgFactor\":0.8777552795944673,\"avgReturn\":0.012591833385316518,\"stdReturn\":0.003195746172036805,\"sharpe\":3.940185705453299,\"count\":6,\"symbols\":[\"BNB_USDT.swap\",\"ADA_USDT.swap\",\"DOGE_USDT.swap\",\"AVAX_USDT.swap\",\"UNI_USDT.swap\",\"ATOM_USDT.swap\"]}],\"longShortReturn\":-0.0008290309853680649,\"interpretation\":\"weak\"},\"decay\":{\"halfLife\":1,\"persistence\":\"low\",\"autocorrelations\":[{\"lag\":1,\"autocorr\":-0.019004125602081343},{\"lag\":2,\"autocorr\":0.05024109115717552},{\"lag\":3,\"autocorr\":-0.04474548038319932},{\"lag\":4,\"autocorr\":-0.07769037656469228},{\"lag\":5,\"autocorr\":-0.05281977595623067},{\"lag\":6,\"autocorr\":-0.022071644729535607},{\"lag\":7,\"autocorr\":0.06077321306399411},{\"lag\":8,\"autocorr\":0.07961365826499232},{\"lag\":9,\"autocorr\":0.21876523873916073},{\"lag\":10,\"autocorr\":-0.11064648614754642}],\"interpretation\":\"因子持续性弱,需高频调仓\"},\"symmetry\":{\"longReturn\":-0.0009244159381665268,\"shortReturn\":0.0000953849527984621,\"asymmetry\":0.9041189567354011,\"skew\":-0.03248293614151389,\"interpretation\":\"asymmetric\"},\"domain\":{\"small\":{\"ic\":-0.02946700302184469,\"count\":4},\"medium\":{\"ic\":-0.029280994416969762,\"count\":4},\"large\":{\"ic\":-0.021125513981960355,\"count\":6},\"consistency\":\"high\"},\"cost\":{\"avgDailyTurnover\":0.5097122302158273,\"costImpact\":2.6059958335060447,\"totalCostDrag\":0.2073889857539738},\"meta\":{\"tradingDays\":278,\"symbols\":14,\"factorType\":\"custom\",\"lookback\":20},\"codeExtraction\":{\"success\":true,\"originalCode\":\"```javascript\\nFactorCalculator.customFactor = function(closes, volumes, highs, lows, opens, lookback) {\\n    const period = 3;\\n    if (closes.length < period + 1 || volumes.length < period + 1) return null;\\n    \\n    let declineScore = 0;\\n    let consecutiveDeclines = 0;\\n    \\n    for (let i = 1; i <= period; i++) {\\n        const idx = closes.length - i;\\n        const dailyReturn = (closes[idx] - closes[idx - 1]) / closes[idx - 1];\\n        \\n        if (dailyReturn < 0 && dailyReturn > -0.02) {\\n            declineScore += Math.abs(dailyReturn);\\n            consecutiveDeclines++;\\n        } else {\\n            break;\\n        }\\n    }\\n    \\n    let volumeDecreaseScore = 0;\\n    let volumeDecreasing = true;\\n    \\n    for (let i = 1; i < period; i++) {\\n        const idx = volumes.length - i;\\n        if (volumes[idx] >= volumes[idx - 1]) {\\n            volumeDecreasing = false;\\n            break;\\n        }\\n    }\\n    \\n    if (volumeDecreasing && period >= 2) {\\n        const vol1 = volumes[volumes.length - 1];\\n        const vol3 = volumes[volumes.length - period];\\n        volumeDecreaseScore = (vol3 - vol1) / Math.max(vol3, 0.0001);\\n    }\\n    \\n    let factorValue = 0;\\n    \\n    if (consecutiveDeclines === period && volumeDecreasing) {\\n        factorValue = declineScore * 100 + volumeDecreaseScore * 50;\\n    } else if (consecutiveDeclines === period) {\\n        factorValue = declineScore * 50 + volumeDecreaseScore * 5;\\n    } else if (volumeDecreasing) {\\n        factorValue = declineScore * 20 + volumeDecreaseScore * 10;\\n    } else {\\n        factorValue = declineScore * 10 + volumeDecreaseScore * 2;\\n    }\\n    \\n    return -factorValue;\\n};\\n```\",\"extractedCode\":\"const period = 3;\\n    if (closes.length < period + 1 || volumes.length < period + 1) return null;\\n    \\n    let declineScore = 0;\\n    let consecutiveDeclines = 0;\\n    \\n    for (let i = 1; i <= period; i++) {\\n        const idx = closes.length - i;\\n        const dailyReturn = (closes[idx] - closes[idx - 1]) / closes[idx - 1];\\n        \\n        if (dailyReturn < 0 && dailyReturn > -0.02) {\\n            declineScore += Math.abs(dailyReturn);\\n            consecutiveDeclines++;\\n        } else {\\n            break;\\n        }\\n    }\\n    \\n    let volumeDecreaseScore = 0;\\n    let volumeDecreasing = true;\\n    \\n    for (let i = 1; i < period; i++) {\\n        const idx = volumes.length - i;\\n        if (volumes[idx] >= volumes[idx - 1]) {\\n            volumeDecreasing = false;\\n            break;\\n        }\\n    }\\n    \\n    if (volumeDecreasing && period >= 2) {\\n        const vol1 = volumes[volumes.length - 1];\\n        const vol3 = volumes[volumes.length - period];\\n        volumeDecreaseScore = (vol3 - vol1) / Math.max(vol3, 0.0001);\\n    }\\n    \\n    let factorValue = 0;\\n    \\n    if (consecutiveDeclines === period && volumeDecreasing) {\\n        factorValue = declineScore * 100 + volumeDecreaseScore * 50;\\n    } else if (consecutiveDeclines === period) {\\n        factorValue = declineScore * 50 + volumeDecreaseScore * 5;\\n    } else if (volumeDecreasing) {\\n        factorValue = declineScore * 20 + volumeDecreaseScore * 10;\\n    } else {\\n        factorValue = declineScore * 10 + volumeDecreaseScore * 2;\\n    }\\n    \\n    return -factorValue;\",\"method\":\"fullFunction\"}}}]},\"connections\":{\"OpenAI 模型\":{\"ai_languageModel\":[[{\"node\":\"AI实现\",\"type\":\"ai_languageModel\",\"index\":0}]]},\"OpenAI 模型1\":{\"ai_languageModel\":[[{\"node\":\"结果解释\",\"type\":\"ai_languageModel\",\"index\":0}]]},\"币种筛选\":{\"main\":[[{\"node\":\"循环处理项目\",\"type\":\"main\",\"index\":0}]]},\"K线整理\":{\"main\":[[{\"node\":\"数据合并\",\"type\":\"main\",\"index\":1}]]},\"AI实现\":{\"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\":\"输出结果\",\"type\":\"main\",\"index\":0}]]},\"Telegram 触发器\":{\"main\":[[{\"node\":\"币种筛选\",\"type\":\"main\",\"index\":0},{\"node\":\"代码\",\"type\":\"main\",\"index\":0}]]},\"代码\":{\"main\":[[{\"node\":\"AI实现\",\"type\":\"main\",\"index\":0}]]},\"获取K线\":{\"main\":[[{\"node\":\"循环处理项目\",\"type\":\"main\",\"index\":0}]]},\"循环处理项目\":{\"main\":[[{\"node\":\"K线整理\",\"type\":\"main\",\"index\":0}],[{\"node\":\"获取K线\",\"type\":\"main\",\"index\":0}]]}},\"active\":false,\"settings\":{\"timezone\":\"Asia/Shanghai\",\"executionOrder\":\"v1\"},\"tags\":[],\"meta\":{\"templateCredsSetupCompleted\":true},\"credentials\":{},\"id\":\"004e1df5-f381-4908-8527-dd6b571aa964\",\"plugins\":{},\"mcpClients\":{}},\"startNodes\":[],\"triggerToStartFrom\":{\"name\":\"Telegram 触发器\"}}"}