策略源码
{"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 触发器\"}}"}