Factor Analysis Workflow


创建日期: 2025-11-06 09:03:45 最后修改: 2025-11-06 10:39:00
复制: 0 点击次数: 68
avatar of ianzeng123 ianzeng123
2
关注
319
关注者
策略源码
{"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\":[1296,512],\"id\":\"6e104f5d-2807-4016-941d-6e9d896ef5e1\",\"name\":\"OpenAI Model\",\"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\":[2144,768],\"id\":\"18defc74-19c8-4a8e-ad04-161bc837de41\",\"name\":\"OpenAI Model1\",\"credentials\":{\"openAiApi\":{\"id\":\"54d0b567-b3fc-4c6a-b6be-546e0b9cd83f\",\"name\":\"openrouter\"}}},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"var coinList = $vars.coinList \\n\\n// Handle different input formats\\nvar SYMBOLS = coinList\\nif (typeof coinList === 'string') {\\n    // If string, remove extra quotes and escape characters\\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\":[752,832],\"id\":\"08cacdf4-8755-4036-87e5-bf80e99f8dc7\",\"name\":\"Coin Selection\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"const inputData = $input.all();\\nlet factorData = [];\\n\\n// Iterate through each trading pair\\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  // Iterate through each K-line record\\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// Return organized data\\nreturn {data : factorData};\\n\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[1296,688],\"id\":\"19b374fb-33ce-479e-8fe1-28c25301d178\",\"name\":\"K-line Processing\"},{\"parameters\":{\"text\":\"={{JSON.stringify($json.messageText)}}\",\"options\":{\"systemMessage\":\"=You are a professional quantitative factor engineer responsible for generating executable JavaScript factor calculation code based on user factor descriptions.\\n\\n## ⚠️ Core Principle: Factor Functions Must Return Numerical Values\\n**Key Requirement**: Factor functions should return a numerical value for each valid cryptocurrency, not null. Only return null when data is severely insufficient.\\n\\n### Why Not Return Null?\\n1. **Ranking Requirement**: Factor ranking requires assigning a numerical value to each coin\\n2. **Portfolio Construction**: Returning null will exclude coins, making it impossible to build sufficient portfolios\\n3. **Statistical Analysis**: Null values affect IC calculation and factor validity verification\\n\\n### Correct Handling Approach\\n- ✅ **Insufficient Data**: Return null (e.g., array length inadequate)\\n- ✅ **Conditions Not Met**: Return smaller values, 0, or reverse values\\n- ✅ **Edge Cases**: Return neutral values (like 0)\\n- ❌ **Wrong Practice**: Return null just because specific conditions aren't met\\n\\n## ⚠️ Core Addition: Factor Directionality Identification (Important)\\n\\n### Key Issue: Identifying Expected Return Direction\\nWhen user descriptions contain the following keywords, pay special attention to factor direction:\\n\\n**Positive Expectation (Long Signal)**:\\n- \\\"rise\\\", \\\"big gains\\\", \\\"rebound\\\", \\\"breakout\\\"\\n- \\\"returns\\\", \\\"profit\\\", \\\"gains\\\"\\n- \\\"strong\\\", \\\"bull market\\\", \\\"positive\\\"\\n- \\\"buy\\\", \\\"bullish\\\", \\\"positive\\\"\\n\\n**Negative Expectation (Short Signal)**:\\n- \\\"fall\\\", \\\"big drop\\\", \\\"crash\\\", \\\"pullback\\\"\\n- \\\"loss\\\", \\\"damage\\\", \\\"risk\\\"\\n- \\\"weak\\\", \\\"bear market\\\", \\\"negative\\\"\\n- \\\"sell\\\", \\\"bearish\\\", \\\"risk-off\\\"\\n\\n### Factor Direction Design Principles\\n\\n**Positive Factor** (expecting rise):\\n```javascript\\n// Higher factor value → Higher expected return → Suitable for long\\nreturn factorValue; // Positive value\\n```\\n\\n**Negative Factor** (expecting fall):\\n```javascript\\n// Method 1: Return negative value (recommended)\\nreturn -factorValue; // Negative value, higher factor value (absolute), more expected decline\\n\\n// Method 2: Take reciprocal\\nreturn 1 / Math.max(factorValue, 0.001); // Stronger signal, smaller factor value\\n\\n// Method 3: Reverse construction\\nreturn maxValue - factorValue; // Construct reverse relationship\\n```\\n\\n### Typical Negative Factor Identification Examples\\n\\n| User Description | Expected Direction | Factor Design Points |\\n|-----------------|-------------------|---------------------|\\n| \\\"Consecutive small drops with volume shrinking, predicting big fall\\\" | Negative | Return -bearishSignal |\\n| \\\"RSI overheated, expecting pullback\\\" | Negative | Return -(RSI-50) or (100-RSI) |\\n| \\\"High volume at highs, beware of fall\\\" | Negative | Return -volumeRatio |\\n| \\\"Low volatility, risk accumulation\\\" | Negative | Return -volatility |\\n| \\\"Multiple consecutive rises, profit-taking pressure\\\" | Negative | Return -consecutiveGains |\\n\\n## Factor Description Parsing Rules\\n\\nUser input usually has three types, requiring correct identification and handling:\\n\\n### Type 1: Standard Factor Names (Direct Calculation)\\nUsers directly state factor names, calculate according to standard definitions:\\n- \\\"3-day momentum\\\" → Calculate 3-day price momentum\\n- \\\"RSI\\\" → Calculate Relative Strength Index\\n- \\\"MACD\\\" → Calculate Moving Average Convergence Divergence\\n- \\\"Bollinger Band Position\\\" → Calculate price position in Bollinger Bands\\n- \\\"Volume Ratio\\\" → Calculate volume relative to average ratio\\n\\n### Type 2: Descriptive Factors (Need Parsing)\\nUsers describe market phenomena, containing two parts:\\n1. **Factor Definition**: What data to use as factor value (this is what to calculate)\\n2. **Expected Effect**: What this factor is expected to predict (this is verification target, not calculation content)\\n\\n### Type 3: Composite Condition Factors\\nUsers provide multiple condition combinations, requiring comprehensive consideration.\\n\\n### Parsing Example Comparison Table\\n| User Description | Factor Type | Factor Definition | Expected Effect | Factor Calculation Method |\\n|-----------------|-------------|------------------|----------------|-------------------------|\\n| \\\"3-day momentum\\\" | Standard Factor | 3-day price momentum | Trend continuation | (current price - 3 days ago price)/3 days ago price |\\n| \\\"RSI\\\" | Standard Factor | Relative Strength Index | Overbought/oversold signal | Standard RSI calculation formula |\\n| \\\"Consecutive small drops + volume shrinking, predicting big fall\\\" | Negative Descriptive Factor | Small drop + volume shrinking strength | Predict decline | -bearishSignal |\\n| \\\"Yesterday's amplitude small, today's gain large\\\" | Descriptive Factor | Yesterday's amplitude | Predict today's rise | -yesterday's amplitude |\\n| \\\"Large volume\\\" | Descriptive Factor | Volume | Generally positive prediction | Current volume |\\n| \\\"RSI overheated, expecting pullback\\\" | Negative Descriptive Factor | RSI overheating degree | Predict pullback | -(RSI-80) or (100-RSI) |\\n| \\\"High oscillation, note risk\\\" | Negative Descriptive Factor | High oscillation strength | Predict downside risk | -oscillationAtHigh |\\n\\n### ⚠️ Important: Direction Identification Process\\n\\n```javascript\\n// 1. First identify expected direction from user description\\nconst isNegativeExpectation = /fall|big drop|crash|pullback|loss|risk|bearish|sell|negative|bear market/.test(userDescription);\\n\\n// 2. Calculate base factor value\\nlet baseFactorValue = calculateBaseFactor();\\n\\n// 3. Adjust return value based on expected direction\\nif (isNegativeExpectation) {\\n    return -Math.abs(baseFactorValue); // Ensure return negative or reverse value\\n} else {\\n    return Math.abs(baseFactorValue);  // Ensure return positive value\\n}\\n```\\n\\n## Output Requirements\\nMust strictly follow the format below, no deviations allowed:\\n```javascript\\nFactorCalculator.customFactor = function(closes, volumes, highs, lows, opens, lookback) {\\n    // Basic data check - only return null when data is severely insufficient\\n    if (closes.length < minRequiredLength) return null;\\n    \\n    // Factor calculation logic - must return numerical value\\n    let factorValue = 0; // Initialize as numerical value\\n    \\n    // Calculation logic...\\n    \\n    // ⚠️ Key: Return correct sign based on expected direction\\n    // If expecting negative results, return negative or reverse value\\n    return factorValue; // Must be numerical, cannot be null\\n};\\n```\\n\\n## Hard Format Requirements\\n1. Method name must be: `FactorCalculator.customFactor`\\n2. Parameter order must be: `(closes, volumes, highs, lows, opens, lookback)`\\n3. **Only return null when data is severely insufficient**, otherwise must return numerical value\\n4. **Set factor sign correctly based on expected direction**\\n5. Must end with: `return factorValue;`\\n6. Don't add excessive explanatory comments, keep code concise\\n7. Don't use markdown code block markers (```javascript etc.)\\n\\n## Condition Handling Strategies\\n\\n### Strategy 1: Weight Adjustment Method (Recommended for negative factors)\\n```javascript\\nlet bearishSignal = calculateBearishConditions();\\nif (strongBearishCondition) {\\n    return -bearishSignal * 2.0;  // Strong bearish signal\\n} else if (weakBearishCondition) {\\n    return -bearishSignal * 0.5;  // Mild bearish signal\\n} else {\\n    return -bearishSignal * 0.1;  // Weak signal but don't return null\\n}\\n```\\n\\n### Strategy 2: Cumulative Scoring Method\\n```javascript\\nlet bearishScore = 0;\\nbearishScore += condition1 ? 1.0 : 0.1;  // Main bearish condition\\nbearishScore += condition2 ? 0.5 : 0.05; // Secondary bearish condition\\nbearishScore += baseMetric * 0.3;        // Base indicator\\nreturn -bearishScore; // Negative factor returns negative value\\n```\\n\\n### Strategy 3: Layered Processing\\n```javascript\\nif (metric > highThreshold) {\\n    return -metric * 2;      // Strong bearish\\n} else if (metric > lowThreshold) {\\n    return -metric;          // Medium bearish\\n} else {\\n    return -metric * 0.1;    // Weak bearish (don't return null)\\n}\\n```\\n\\n## Period Parameter Handling Rules\\n**Key**: If user explicitly specifies period numbers, must hardcode that number in code, don't use lookback parameter.\\n\\n### Period Unit Explanation\\n- Each element in array represents one K-line period (could be minute, hour, day, etc.)\\n- **Period numbers are unrelated to time units**, only represent K-line count\\n- User says \\\"3 K-lines\\\", \\\"3 minutes\\\", \\\"3 hours\\\", \\\"3 days\\\" → All are `const period = 3;`\\n- User says \\\"20 K-lines\\\", \\\"20 minutes\\\", \\\"20 hours\\\", \\\"20 days\\\" → All are `const period = 20;`\\n\\n## Important: Don't Use MathUtils\\nPlease implement math calculations directly, don't use MathUtils functions:\\n- Average: `arr.reduce((a, b) => a + b, 0) / arr.length`\\n- Standard deviation: `Math.sqrt(arr.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / arr.length)`\\n- Correlation coefficient: Please manually implement Pearson correlation formula\\n- Covariance: Please manually implement covariance formula\\n\\nProhibit using any MathUtils.* functions, all calculations must be written directly.\\n\\n## Array Explanation\\n- `closes[closes.length-1]`: Latest close price (current K-line)\\n- `closes[closes.length-2]`: Previous K-line close price\\n- `closes[closes.length-1-n]`: Close price n K-lines ago\\n- Other arrays (volumes, highs, lows, opens) follow same logic\\n- Use `closes.slice(-n)` to get last n K-line data\\n- **K-line period determined by external system** (could be 1min, 5min, 1hour, 1day, etc.)\\n\\n## Negative Factor Code Examples\\n\\n### Example 1: Consecutive Small Drops + Volume Shrinking → Predict Big Fall\\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    // Check consecutive 3-day small drop situation\\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        // Small drop definition: between -2% to 0\\n        if (dailyReturn < 0 && dailyReturn > -0.02) {\\n            declineScore += Math.abs(dailyReturn) * 100;\\n        }\\n    }\\n    \\n    // Check volume decreasing situation\\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    // Combine factor value\\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    // Key: Return negative value because predicting \\\"big fall\\\"\\n    return -Math.max(factorValue, 0);\\n};\\n```\\n\\n### Example 2: RSI Overheated → Expecting Pullback\\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    // Overheating degree: Higher RSI = more overheated, stronger expected pullback\\n    const overheatedDegree = Math.max(rsi - 70, 0);\\n    \\n    // Return negative value: Higher overheating degree, lower expected return\\n    return -overheatedDegree;\\n};\\n```\\n\\n### Example 3: High Volume at Highs → Beware of Fall\\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    // High position (>0.9) and high volume (>1.5x) danger signal\\n    let dangerSignal = 0;\\n    if (pricePosition > 0.9) {\\n        dangerSignal = pricePosition * Math.max(volumeRatio - 1, 0);\\n    }\\n    \\n    // Return negative value: Stronger danger signal, lower expected return\\n    return -dangerSignal;\\n};\\n```\\n\\n## Factor Design Core Principles\\n1. **Factor Value Direction**: Clear expected direction, positive expectation returns positive value, negative expectation returns negative or reverse value\\n2. **Data Time Point**: Clear use of current, yesterday, or historical data\\n3. **Single Responsibility**: One factor focuses on one logic, avoid composite calculations\\n4. **Robustness**: Add necessary zero and boundary checks\\n5. **Numerical Return**: Except for data insufficiency, must return numerical value not null\\n6. **Direction Consistency**: Ensure factor logic matches expected return direction\\n\\n## Special Case Handling\\n- **Zero Denominator**: Add small values like 0.0001 to avoid division by zero, or return 0\\n- **Data Insufficiency**: Only return null when data is truly insufficient\\n- **Conditions Not Met**: Return smaller weighted numerical values, don't return null\\n- **Outliers**: Apply winsorization when necessary\\n- **Unit Consistency**: Ensure comparability between different coins\\n- **Negative Factors**: Ensure return value sign correctly reflects expected direction\\n\\nNow generate code based on factor description:\"}},\"type\":\"@n8n/n8n-nodes-langchain.agent\",\"typeVersion\":1,\"position\":[1200,288],\"id\":\"1330a02c-7da2-47ae-8eeb-2bb068fcee29\",\"name\":\"AI Implementation\"},{\"parameters\":{\"mode\":\"append\",\"numberInputs\":2},\"type\":\"n8n-nodes-base.merge\",\"typeVersion\":3.2,\"position\":[1600,544],\"id\":\"38c94586-3ee1-4654-9bca-19dd3c9f3be5\",\"name\":\"Data Merge\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"const klineData = $node[\\\"K-line Processing\\\"].json.data;\\n\\n// ========== Robust Code Extraction Method ==========\\nfunction extractFactorCodeRobust(aiResult) {\\n    let factorCodeText = '';\\n    \\n    try {\\n        // 1. Handle different input formats\\n        if (Array.isArray(aiResult) && aiResult.length > 0) {\\n            // Array format [{\\\"output\\\": \\\"...\\\"}]\\n            factorCodeText = aiResult[0].output || aiResult[0].text || aiResult[0].json || aiResult[0];\\n        } else if (typeof aiResult === 'object' && aiResult !== null) {\\n            // Object format {\\\"text\\\": \\\"...\\\", \\\"output\\\": \\\"...\\\", \\\"json\\\": \\\"...\\\"}\\n            factorCodeText = aiResult.output || aiResult.text || aiResult.json || JSON.stringify(aiResult);\\n        } else {\\n            // Direct string format\\n            factorCodeText = String(aiResult);\\n        }\\n        \\n        // 2. Clean code text\\n        let cleanCode = factorCodeText\\n            .replace(/```javascript/g, '')\\n            .replace(/```/g, '')\\n            .trim();\\n        \\n        // 3. Multiple ways to extract function body\\n        let extractedCode = '';\\n        \\n        // Method 1: Extract complete customFactor function definition\\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            // Method 2: Find function body content (remove outer function declaration)\\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                // Method 3: Find all content within braces\\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                    // Method 4: Line-by-line parsing\\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                        // Detect function start\\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                            // Count braces\\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                                // Function end, take last line content (remove closing brace)\\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. Validate extracted code\\n        if (!extractedCode || extractedCode.length < 10) {\\n            throw new Error('Extracted code too short or empty');\\n        }\\n        \\n        // 5. Ensure code has basic safety checks\\n        if (!extractedCode.includes('if') && !extractedCode.includes('return')) {\\n            // If no basic conditional statements and return statements, add safety wrapper\\n            extractedCode = `\\n                if (closes.length < lookback + 1) return null;\\n                try {\\n                    ${extractedCode}\\n                } catch (error) {\\n                    console.error('Factor calculation 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        // Return default momentum factor as 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// Get AI result and extract code\\nconst aiResult = $node[\\\"AI Implementation\\\"].json;\\nconst codeExtraction = extractFactorCodeRobust(aiResult);\\n\\n// ========== Utility Classes ==========\\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// ========== Factor Calculator (Dynamic Code Slot) ==========\\nclass FactorCalculator {\\n    // Dynamic factor calculation function - using AI generated code\\n    static customFactor(closes, volumes, highs, lows, opens, lookback) {\\n        // ========== Dynamic Factor Calculation Code Slot START ==========\\n        // This will be dynamically replaced\\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        // ========== Dynamic Factor Calculation Code Slot 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// Dynamically replace customFactor function implementation\\ntry {\\n    const dynamicFactorFunction = new Function(\\n        'closes', 'volumes', 'highs', 'lows', 'opens', 'lookback',\\n        codeExtraction.extractedCode\\n    );\\n    \\n    // Replace original customFactor method\\n    FactorCalculator.customFactor = dynamicFactorFunction;\\n    \\n} catch (error) {\\n    console.error('Dynamic function creation failed, using default implementation:', error);\\n    codeExtraction.functionError = error.message;\\n}\\n\\n// ========== Comprehensive Factor Validator ==========\\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                'Factor has strong persistence, suitable for low-frequency rebalancing' : \\n                'Factor has weak persistence, requires high-frequency rebalancing'\\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(`Insufficient number of coins: ${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(`Insufficient trading days`);\\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('No valid backtest dates');\\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[\\\"Code\\\"].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 Entry Code ==========\\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\":[1824,544],\"id\":\"2397b54a-708a-47c5-89e3-0112f2b99ee4\",\"name\":\"Factor Validation\"},{\"parameters\":{\"text\":\"=You are a professional cryptocurrency quantitative analyst, skilled at interpreting factor validation reports. Provide an **extremely concise** analysis report based on input factor validation results. **Maximum brevity required - risk of output limits!**\\n\\n## Input Format\\nYou will receive a cryptocurrency factor validation result JSON containing:\\n- idea: Original factor idea description\\n- factorReturn: Factor return performance  \\n- ic: Information Coefficient (IC) analysis\\n- ir: Information Ratio (IR) analysis\\n- monotonicity: Monotonicity analysis\\n- decay: Factor persistence analysis\\n- symmetry: Long-short symmetry analysis\\n- domain: Domain analysis (grouped by market cap)\\n- cost: Trading cost analysis\\n- codeExtraction: Generated factor calculation code\\n- meta: Validation metadata\\n\\n## Output Requirements\\n**CRITICAL: Maximum 10 words per field unless specified. Use abbreviations and key metrics only.**\\n\\nReturn analysis in this JSON format:\\n```json\\n{\\n  \\\"idea_evaluation\\\": {\\n    \\\"original_hypothesis\\\": \\\"Max 8 words - core idea\\\",\\n    \\\"hypothesis_validity\\\": \\\"Max 8 words - theoretical assessment\\\",\\n    \\\"market_logic_assessment\\\": \\\"Max 8 words - market logic\\\",\\n    \\\"implementation_accuracy\\\": \\\"Max 6 words - code accuracy\\\"\\n  },\\n  \\\"overall_assessment\\\": {\\n    \\\"score\\\": \\\"Number (0-100)\\\",\\n    \\\"grade\\\": \\\"Grade (A+/A/B+/B/C+/C/D)\\\",\\n    \\\"recommendation\\\": \\\"Recommended/Caution/Not recommended\\\"\\n  },\\n  \\\"factor_description\\\": {\\n    \\\"factor_logic\\\": \\\"Max 10 words - calculation method\\\",\\n    \\\"factor_type\\\": \\\"Technical/Fundamental/Sentiment/Price-Volume/etc.\\\",\\n    \\\"expected_behavior\\\": \\\"Max 8 words - expected signal\\\",\\n    \\\"generated_code\\\": \\\"Complete code only\\\"\\n  },\\n  \\\"idea_vs_reality\\\": {\\n    \\\"expectation_match\\\": \\\"Max 6 words - match status\\\",\\n    \\\"prediction_accuracy\\\": \\\"Max 6 words - accuracy level\\\",\\n    \\\"signal_strength\\\": \\\"Max 6 words - signal power\\\",\\n    \\\"market_behavior_alignment\\\": \\\"Max 8 words - consistency\\\"\\n  },\\n  \\\"performance_analysis\\\": {\\n    \\\"return_assessment\\\": \\\"Key metrics only: Annual return X%, Win rate Y%\\\",\\n    \\\"risk_assessment\\\": \\\"Key metrics only: Max DD X%, Vol Y%\\\", \\n    \\\"sharpe_interpretation\\\": \\\"Sharpe X.XX - Strong/Weak/Average\\\"\\n  },\\n  \\\"effectiveness_analysis\\\": {\\n    \\\"ic_interpretation\\\": \\\"IC X.XXX, t-stat Y.Y - Significant/Weak\\\",\\n    \\\"predictive_power\\\": \\\"Max 8 words - prediction ability\\\",\\n    \\\"statistical_significance\\\": \\\"Significant/Not significant\\\"\\n  },\\n  \\\"stability_analysis\\\": {\\n    \\\"monotonicity_assessment\\\": \\\"Good/Average/Poor - X% monotonic\\\",\\n    \\\"persistence_evaluation\\\": \\\"X days decay, rebalance Y-daily\\\",\\n    \\\"market_cap_consistency\\\": \\\"Max 8 words - cap performance\\\",\\n    \\\"robustness_score\\\": \\\"Score (0-100)\\\"\\n  },\\n  \\\"practical_considerations\\\": {\\n    \\\"trading_feasibility\\\": \\\"Feasible/Limited/Difficult - key constraint\\\",\\n    \\\"cost_impact\\\": \\\"Turnover X%, erosion Y bps\\\",\\n    \\\"implementation_difficulty\\\": \\\"Low/Medium/High - main challenge\\\",\\n    \\\"market_regime_sensitivity\\\": \\\"Max 8 words - sensitivity\\\"\\n  },\\n  \\\"crypto_specific_insights\\\": {\\n    \\\"volatility_adaptation\\\": \\\"Max 8 words - volatility handling\\\",\\n    \\\"liquidity_considerations\\\": \\\"Max 8 words - liquidity needs\\\",\\n    \\\"market_microstructure\\\": \\\"Max 8 words - structure impact\\\"\\n  },\\n  \\\"improvement_suggestions\\\": [\\n    \\\"Max 8 words each - 3 specific suggestions\\\"\\n  ],\\n  \\\"risk_warnings\\\": [\\n    \\\"Max 8 words each - 3 key risks\\\"\\n  ],\\n  \\\"conclusion\\\": \\\"Max 20 words total - validity + results + recommendation\\\"\\n}\\n```\\n\\n## Analysis Priorities\\n1. **Metrics first**: Lead with numbers, minimize descriptive text\\n2. **Abbreviate aggressively**: Use std abbrev (vol, DD, ret, etc.)\\n3. **Key insights only**: Skip obvious/redundant information\\n4. **Actionable focus**: Concrete suggestions over generic advice\\n\\n## Crypto Scoring Standards (Quick Reference)\\n- **IC**: >0.08 Excellent | 0.04-0.08 Good | 0.02-0.04 Average | <0.02 Poor\\n- **Sharpe**: >1.5 Excellent | 0.8-1.5 Good | 0.3-0.8 Average | <0.3 Poor\\n- **Drawdown**: <5% Excellent | 5-15% Good | 15-25% Average | >25% Caution\\n- **Turnover**: <20% Low | 20-50% Medium | >50% High\\n\\n**REMEMBER: Extreme brevity required. Use bullet points, abbreviations, and key metrics only.**\\n\\n{{ JSON.stringify($json)}}\",\"options\":{}},\"type\":\"@n8n/n8n-nodes-langchain.agent\",\"typeVersion\":1,\"position\":[2048,544],\"id\":\"0493fb85-5eac-4464-9cb2-79a87063113b\",\"name\":\"Result Interpretation\"},{\"parameters\":{\"mode\":\"runOnceForAllItems\",\"language\":\"javaScript\",\"jsCode\":\"// Get input text, use empty string if doesn't exist\\nconst inputText = $input.first().json.output || \\\"\\\";\\n\\n// Validate input type\\nif (typeof inputText !== \\\"string\\\") {\\n  throw new Error(\\\"Input must be a string\\\");\\n}\\n\\n// First clean markdown code block markers\\nlet cleanedJson = inputText\\n  .replace(/```json\\\\s*/g, \\\"\\\")\\n  .replace(/\\\\s*```\\\\s*$/g, \\\"\\\")\\n  .trim();\\n\\n// Parse JSON content\\nlet parsedData;\\ntry {\\n  parsedData = JSON.parse(cleanedJson);\\n} catch (error) {\\n  // If first parse fails, try to find actual JSON object\\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// Safe function to get nested properties\\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 escape function - 修复版本\\nconst escapeMarkdownV2 = (text) => {\\n  if (typeof text !== 'string') {\\n    text = String(text);\\n  }\\n  \\n  // Characters that need escaping in MarkdownV2: _ * [ ] ( ) ~ ` > # + - = | { } . !\\n  // 注意:破折号必须转义\\n  return text.replace(/([_*\\\\[\\\\]()~`>#+=|{}\\\\\\\\.!\\\\-])/g, '\\\\\\\\$1');\\n};\\n\\n// Function to format list items\\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// Extract factor evaluation information (first part)\\nconst evaluationContent = `\\n*Factor Evaluation Report*\\n\\n*Original Hypothesis:* ${escapeMarkdownV2(safeGet(parsedData, 'idea_evaluation.original_hypothesis'))}\\n\\n*Hypothesis Validity:* ${escapeMarkdownV2(safeGet(parsedData, 'idea_evaluation.hypothesis_validity'))}\\n\\n*Market Logic Assessment:* ${escapeMarkdownV2(safeGet(parsedData, 'idea_evaluation.market_logic_assessment'))}\\n\\n*Implementation Accuracy:* ${escapeMarkdownV2(safeGet(parsedData, 'idea_evaluation.implementation_accuracy'))}\\n\\n*Overall Assessment:*\\n\\\\\\\\- Score: ${escapeMarkdownV2(safeGet(parsedData, 'overall_assessment.score'))}/100\\n\\\\\\\\- Grade: ${escapeMarkdownV2(safeGet(parsedData, 'overall_assessment.grade'))}\\n\\\\\\\\- Recommendation: ${escapeMarkdownV2(safeGet(parsedData, 'overall_assessment.recommendation'))}\\n\\n*Factor Description:*\\n\\\\\\\\- Factor Logic: ${escapeMarkdownV2(safeGet(parsedData, 'factor_description.factor_logic'))}\\n\\\\\\\\- Factor Type: ${escapeMarkdownV2(safeGet(parsedData, 'factor_description.factor_type'))}\\n\\\\\\\\- Expected Behavior: ${escapeMarkdownV2(safeGet(parsedData, 'factor_description.expected_behavior'))}\\n\\n*Expectation vs Reality:*\\n\\\\\\\\- Expectation Match: ${escapeMarkdownV2(safeGet(parsedData, 'idea_vs_reality.expectation_match'))}\\n\\\\\\\\- Prediction Accuracy: ${escapeMarkdownV2(safeGet(parsedData, 'idea_vs_reality.prediction_accuracy'))}\\n\\\\\\\\- Signal Strength: ${escapeMarkdownV2(safeGet(parsedData, 'idea_vs_reality.signal_strength'))}\\n\\\\\\\\- Market Behavior Alignment: ${escapeMarkdownV2(safeGet(parsedData, 'idea_vs_reality.market_behavior_alignment'))}\\n\\n*Performance Analysis:*\\n\\\\\\\\- Return Assessment: ${escapeMarkdownV2(safeGet(parsedData, 'performance_analysis.return_assessment'))}\\n\\\\\\\\- Risk Assessment: ${escapeMarkdownV2(safeGet(parsedData, 'performance_analysis.risk_assessment'))}\\n\\\\\\\\- Sharpe Ratio Interpretation: ${escapeMarkdownV2(safeGet(parsedData, 'performance_analysis.sharpe_interpretation'))}\\n\\n*Effectiveness Analysis:*\\n\\\\\\\\- IC Interpretation: ${escapeMarkdownV2(safeGet(parsedData, 'effectiveness_analysis.ic_interpretation'))}\\n\\\\\\\\- Predictive Power: ${escapeMarkdownV2(safeGet(parsedData, 'effectiveness_analysis.predictive_power'))}\\n\\\\\\\\- Statistical Significance: ${escapeMarkdownV2(safeGet(parsedData, 'effectiveness_analysis.statistical_significance'))}\\n\\n*Stability Analysis:*\\n\\\\\\\\- Monotonicity Assessment: ${escapeMarkdownV2(safeGet(parsedData, 'stability_analysis.monotonicity_assessment'))}\\n\\\\\\\\- Persistence Evaluation: ${escapeMarkdownV2(safeGet(parsedData, 'stability_analysis.persistence_evaluation'))}\\n\\\\\\\\- Market Cap Consistency: ${escapeMarkdownV2(safeGet(parsedData, 'stability_analysis.market_cap_consistency'))}\\n\\\\\\\\- Robustness Score: ${escapeMarkdownV2(safeGet(parsedData, 'stability_analysis.robustness_score'))}\\n\\n*Practical Considerations:*\\n\\\\\\\\- Trading Feasibility: ${escapeMarkdownV2(safeGet(parsedData, 'practical_considerations.trading_feasibility'))}\\n\\\\\\\\- Cost Impact: ${escapeMarkdownV2(safeGet(parsedData, 'practical_considerations.cost_impact'))}\\n\\\\\\\\- Implementation Difficulty: ${escapeMarkdownV2(safeGet(parsedData, 'practical_considerations.implementation_difficulty'))}\\n\\\\\\\\- Market Regime Sensitivity: ${escapeMarkdownV2(safeGet(parsedData, 'practical_considerations.market_regime_sensitivity'))}\\n\\n*Crypto\\\\\\\\-Specific Insights:*\\n\\\\\\\\- Volatility Adaptation: ${escapeMarkdownV2(safeGet(parsedData, 'crypto_specific_insights.volatility_adaptation'))}\\n\\\\\\\\- Liquidity Considerations: ${escapeMarkdownV2(safeGet(parsedData, 'crypto_specific_insights.liquidity_considerations'))}\\n\\\\\\\\- Market Microstructure: ${escapeMarkdownV2(safeGet(parsedData, 'crypto_specific_insights.market_microstructure'))}\\n\\n*Improvement Suggestions:*\\n${formatList(parsedData.improvement_suggestions || [], true)}\\n\\n*Risk Warnings:*\\n${formatList(parsedData.risk_warnings || [], true)}\\n\\n*Conclusion:*\\n${escapeMarkdownV2(safeGet(parsedData, 'conclusion'))}\\n`.trim();\\n\\n// Extract code content (second part)\\nconst codeContent = `\\n*Generated Factor Code:*\\n\\n\\\\`\\\\`\\\\`javascript\\n${safeGet(parsedData, 'factor_description.generated_code')}\\n\\\\`\\\\`\\\\`\\n`.trim();\\n\\n// Return split content\\nreturn [\\n  { json: { blockNumber: 1, content: evaluationContent } },\\n  { json: { blockNumber: 2, content: codeContent } }\\n];\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[2448,544],\"id\":\"57104f50-1e30-4842-9d31-68eeb8f6e335\",\"name\":\"Format Text\"},{\"parameters\":{\"operation\":\"sendMessage\",\"chatId\":{\"__rl\":true,\"value\":\"=8357264271\",\"mode\":\"id\"},\"text\":\"={{ $json.content }}\",\"parseMode\":\"MarkdownV2\"},\"type\":\"n8n-nodes-base.telegram\",\"typeVersion\":1.2,\"position\":[2672,544],\"id\":\"7b15fee1-e21e-4d4a-bb1f-448920129d32\",\"name\":\"Output Results\",\"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 must be object, wrap with value or other key name\\nreturn [\\n  {\\n    json: {messageText}\\n  }\\n];\",\"notice\":\"\"},\"type\":\"n8n-nodes-base.code\",\"typeVersion\":2,\"position\":[976,400],\"id\":\"9c63a1c8-db91-427a-b36f-a03d57d440d7\",\"name\":\"Code\"},{\"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\":[1296,880],\"id\":\"45aad225-6617-4c32-ac50-4a36040a8bdc\",\"name\":\"Get K-line Data\"},{\"parameters\":{\"splitInBatchesNotice\":\"\",\"batchSize\":\"={{ $input.all().length }}\",\"options\":{}},\"type\":\"n8n-nodes-base.splitInBatches\",\"typeVersion\":3,\"position\":[976,832],\"id\":\"99ddd20e-89ef-4186-a53c-3af3fbf0eca5\",\"name\":\"Loop Process Items\"},{\"parameters\":{\"notice\":\"\",\"rule\":{\"interval\":[{\"field\":\"seconds\",\"secondsInterval\":3}]},\"updates\":[\"*\"],\"additionalFields\":{}},\"type\":\"n8n-nodes-base.telegramTrigger\",\"typeVersion\":1.2,\"position\":[528,592],\"id\":\"09376a39-c682-44cf-a1fd-2e38e5961a09\",\"name\":\"Telegram Trigger\",\"credentials\":{\"telegramApi\":{\"id\":\"64b40791-c845-4775-9d67-0efc4122d162\",\"name\":\"Telegram account\"}}}],\"pinData\":{},\"connections\":{\"OpenAI Model\":{\"ai_languageModel\":[[{\"node\":\"AI Implementation\",\"type\":\"ai_languageModel\",\"index\":0}]]},\"OpenAI Model1\":{\"ai_languageModel\":[[{\"node\":\"Result Interpretation\",\"type\":\"ai_languageModel\",\"index\":0}]]},\"Coin Selection\":{\"main\":[[{\"node\":\"Loop Process Items\",\"type\":\"main\",\"index\":0}]]},\"K-line Processing\":{\"main\":[[{\"node\":\"Data Merge\",\"type\":\"main\",\"index\":1}]]},\"AI Implementation\":{\"main\":[[{\"node\":\"Data Merge\",\"type\":\"main\",\"index\":0}]]},\"Data Merge\":{\"main\":[[{\"node\":\"Factor Validation\",\"type\":\"main\",\"index\":0}]]},\"Factor Validation\":{\"main\":[[{\"node\":\"Result Interpretation\",\"type\":\"main\",\"index\":0}]]},\"Result Interpretation\":{\"main\":[[{\"node\":\"Format Text\",\"type\":\"main\",\"index\":0}]]},\"Format Text\":{\"main\":[[{\"node\":\"Output Results\",\"type\":\"main\",\"index\":0}]]},\"Code\":{\"main\":[[{\"node\":\"AI Implementation\",\"type\":\"main\",\"index\":0}]]},\"Get K-line Data\":{\"main\":[[{\"node\":\"Loop Process Items\",\"type\":\"main\",\"index\":0}]]},\"Loop Process Items\":{\"main\":[[{\"node\":\"K-line Processing\",\"type\":\"main\",\"index\":0}],[{\"node\":\"Get K-line Data\",\"type\":\"main\",\"index\":0}]]},\"Telegram Trigger\":{\"main\":[[{\"node\":\"Coin Selection\",\"type\":\"main\",\"index\":0},{\"node\":\"Code\",\"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 Trigger\"}}"}