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