|
| 1 | +#!/usr/bin/env node |
| 2 | +/** |
| 3 | + * Analysis Reporter Module for PineOptimizer |
| 4 | + * |
| 5 | + * Analysis and reporting methods: analyzeParameterSensitivity, generateOptimizationReport, |
| 6 | + * generateConsoleOptimizationReport, generateHTMLOptimizationReport |
| 7 | + */ |
| 8 | + |
| 9 | +class AnalysisReporter { |
| 10 | + constructor() { |
| 11 | + // No initialization needed |
| 12 | + } |
| 13 | + |
| 14 | + /** |
| 15 | + * Analyze parameter sensitivity |
| 16 | + */ |
| 17 | + analyzeParameterSensitivity(allResults) { |
| 18 | + if (allResults.length < 2) return {}; |
| 19 | + |
| 20 | + const paramNames = Object.keys(allResults[0].params); |
| 21 | + const sensitivities = {}; |
| 22 | + |
| 23 | + for (const param of paramNames) { |
| 24 | + // Calculate correlation between parameter values and scores |
| 25 | + const scores = allResults.map((r) => r.score); |
| 26 | + |
| 27 | + // Simple sensitivity: standard deviation of scores for this parameter |
| 28 | + const meanScore = scores.reduce((a, b) => a + b, 0) / scores.length; |
| 29 | + const scoreVariance = |
| 30 | + scores.reduce((sum, score) => sum + Math.pow(score - meanScore, 2), 0) / scores.length; |
| 31 | + |
| 32 | + sensitivities[param] = Math.sqrt(scoreVariance) / meanScore; |
| 33 | + } |
| 34 | + |
| 35 | + return sensitivities; |
| 36 | + } |
| 37 | + |
| 38 | + /** |
| 39 | + * Generate optimization report |
| 40 | + */ |
| 41 | + generateOptimizationReport(results, options) { |
| 42 | + const { outputFormat = 'console' } = options; |
| 43 | + |
| 44 | + switch (outputFormat) { |
| 45 | + case 'json': |
| 46 | + return JSON.stringify(results, null, 2); |
| 47 | + case 'html': |
| 48 | + return this.generateHTMLOptimizationReport(results, options); |
| 49 | + case 'console': |
| 50 | + default: |
| 51 | + return this.generateConsoleOptimizationReport(results, options); |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + /** |
| 56 | + * Generate console optimization report |
| 57 | + */ |
| 58 | + generateConsoleOptimizationReport(results, options) { |
| 59 | + const { |
| 60 | + method, |
| 61 | + bestParams, |
| 62 | + bestScore, |
| 63 | + bestResult, |
| 64 | + allResults, |
| 65 | + totalCombinations, |
| 66 | + testedCombinations, |
| 67 | + } = results; |
| 68 | + const { metric } = options; |
| 69 | + |
| 70 | + console.log(`\n${'='.repeat(60)}`); |
| 71 | + console.log('🏆 OPTIMIZATION RESULTS'); |
| 72 | + console.log('='.repeat(60)); |
| 73 | + |
| 74 | + console.log(`\nOptimization Method: ${method.toUpperCase()}`); |
| 75 | + console.log(`Optimization Metric: ${metric.toUpperCase()}`); |
| 76 | + console.log(`Parameter Combinations: ${testedCombinations}/${totalCombinations || 'N/A'}`); |
| 77 | + |
| 78 | + console.log('\n🎯 BEST PARAMETERS:'); |
| 79 | + console.log('─'.repeat(40)); |
| 80 | + Object.entries(bestParams).forEach(([param, value]) => { |
| 81 | + console.log(` ${param}: ${value}`); |
| 82 | + }); |
| 83 | + console.log(`\n Best ${metric} score: ${bestScore.toFixed(4)}`); |
| 84 | + |
| 85 | + if (bestResult) { |
| 86 | + const perf = bestResult.performance; |
| 87 | + console.log('\n📊 PERFORMANCE WITH BEST PARAMETERS:'); |
| 88 | + console.log('─'.repeat(40)); |
| 89 | + console.log( |
| 90 | + ` Net Profit: $${perf.netProfit.toFixed(2)} (${((perf.netProfit / bestResult.parameters.initialCapital) * 100).toFixed(2)}%)` |
| 91 | + ); |
| 92 | + console.log(` Total Trades: ${perf.totalTrades}`); |
| 93 | + console.log(` Win Rate: ${(perf.winRate * 100).toFixed(2)}%`); |
| 94 | + console.log(` Profit Factor: ${perf.profitFactor.toFixed(2)}`); |
| 95 | + console.log(` Sharpe Ratio: ${perf.sharpeRatio.toFixed(2)}`); |
| 96 | + console.log(` Max Drawdown: ${perf.maxDrawdownPct.toFixed(2)}%`); |
| 97 | + console.log(` Recovery Factor: ${perf.recoveryFactor.toFixed(2)}`); |
| 98 | + } |
| 99 | + |
| 100 | + // Show top 5 results |
| 101 | + if (allResults.length > 1) { |
| 102 | + console.log('\n🏅 TOP 5 PARAMETER COMBINATIONS:'); |
| 103 | + console.log('─'.repeat(40)); |
| 104 | + const topResults = allResults.slice(0, 5); |
| 105 | + topResults.forEach((result, i) => { |
| 106 | + console.log(`\n${i + 1}. Score: ${result.score.toFixed(4)}`); |
| 107 | + Object.entries(result.params).forEach(([param, value]) => { |
| 108 | + console.log(` ${param}: ${value}`); |
| 109 | + }); |
| 110 | + }); |
| 111 | + } |
| 112 | + |
| 113 | + // Parameter sensitivity analysis |
| 114 | + if (allResults.length >= 5) { |
| 115 | + const sensitivities = this.analyzeParameterSensitivity(allResults); |
| 116 | + console.log('\n📈 PARAMETER SENSITIVITY ANALYSIS:'); |
| 117 | + console.log('─'.repeat(40)); |
| 118 | + Object.entries(sensitivities) |
| 119 | + .sort(([, a], [, b]) => b - a) |
| 120 | + .forEach(([param, sensitivity]) => { |
| 121 | + const sensitivityLevel = |
| 122 | + sensitivity > 0.3 ? 'High' : sensitivity > 0.1 ? 'Medium' : 'Low'; |
| 123 | + console.log(` ${param}: ${sensitivity.toFixed(4)} (${sensitivityLevel})`); |
| 124 | + }); |
| 125 | + } |
| 126 | + |
| 127 | + console.log(`\n${'='.repeat(60)}`); |
| 128 | + |
| 129 | + return results; |
| 130 | + } |
| 131 | + |
| 132 | + /** |
| 133 | + * Generate HTML optimization report |
| 134 | + */ |
| 135 | + generateHTMLOptimizationReport(results, options) { |
| 136 | + const { method, bestParams, bestScore, allResults } = results; |
| 137 | + const { metric } = options; |
| 138 | + |
| 139 | + // Get top 10 results for the table |
| 140 | + const topResults = allResults.slice(0, 10); |
| 141 | + |
| 142 | + return ` |
| 143 | +<!DOCTYPE html> |
| 144 | +<html> |
| 145 | +<head> |
| 146 | + <title>Optimization Report - ${method.toUpperCase()}</title> |
| 147 | + <style> |
| 148 | + body { font-family: Arial, sans-serif; margin: 40px; } |
| 149 | + .header { background: #2c3e50; color: white; padding: 20px; border-radius: 5px; } |
| 150 | + .best-params { background: #27ae60; color: white; padding: 20px; border-radius: 5px; margin: 20px 0; } |
| 151 | + .param { display: inline-block; background: #3498db; color: white; padding: 8px 15px; margin: 5px; border-radius: 3px; } |
| 152 | + table { width: 100%; border-collapse: collapse; margin: 20px 0; } |
| 153 | + th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; } |
| 154 | + th { background: #f2f2f2; } |
| 155 | + .score-good { color: #27ae60; font-weight: bold; } |
| 156 | + .score-avg { color: #f39c12; } |
| 157 | + .score-poor { color: #e74c3c; } |
| 158 | + .sensitivity-high { color: #e74c3c; font-weight: bold; } |
| 159 | + .sensitivity-medium { color: #f39c12; } |
| 160 | + .sensitivity-low { color: #27ae60; } |
| 161 | + </style> |
| 162 | +</head> |
| 163 | +<body> |
| 164 | + <div class="header"> |
| 165 | + <h1>🏆 Optimization Report</h1> |
| 166 | + <h2>Method: ${method.toUpperCase()} | Metric: ${metric.toUpperCase()}</h2> |
| 167 | + <p>Generated: ${new Date().toLocaleString()}</p> |
| 168 | + </div> |
| 169 | + |
| 170 | + <div class="best-params"> |
| 171 | + <h2>Best Parameters</h2> |
| 172 | + <p>Best ${metric} score: <strong>${bestScore.toFixed(4)}</strong></p> |
| 173 | + <div> |
| 174 | + ${Object.entries(bestParams) |
| 175 | + .map(([param, value]) => `<span class="param">${param}: ${value}</span>`) |
| 176 | + .join('')} |
| 177 | + </div> |
| 178 | + </div> |
| 179 | +
|
| 180 | + <h2>Top Parameter Combinations</h2> |
| 181 | + <table> |
| 182 | + <thead> |
| 183 | + <tr> |
| 184 | + <th>Rank</th> |
| 185 | + <th>Score</th> |
| 186 | + ${Object.keys(bestParams) |
| 187 | + .map((param) => `<th>${param}</th>`) |
| 188 | + .join('')} |
| 189 | + </tr> |
| 190 | + </thead> |
| 191 | + <tbody> |
| 192 | + ${topResults |
| 193 | + .map( |
| 194 | + (result, i) => ` |
| 195 | + <tr> |
| 196 | + <td>${i + 1}</td> |
| 197 | + <td class="score-${result.score > bestScore * 0.9 ? 'good' : result.score > bestScore * 0.7 ? 'avg' : 'poor'}"> |
| 198 | + ${result.score.toFixed(4)} |
| 199 | + </td> |
| 200 | + ${Object.values(result.params) |
| 201 | + .map((value) => `<td>${value}</td>`) |
| 202 | + .join('')} |
| 203 | + </tr> |
| 204 | + ` |
| 205 | + ) |
| 206 | + .join('')} |
| 207 | + </tbody> |
| 208 | + </table> |
| 209 | +
|
| 210 | + ${ |
| 211 | + allResults.length >= 5 |
| 212 | + ? ` |
| 213 | + <h2>Parameter Sensitivity Analysis</h2> |
| 214 | + <table> |
| 215 | + <thead> |
| 216 | + <tr> |
| 217 | + <th>Parameter</th> |
| 218 | + <th>Sensitivity</th> |
| 219 | + <th>Impact</th> |
| 220 | + </tr> |
| 221 | + </thead> |
| 222 | + <tbody> |
| 223 | + ${(() => { |
| 224 | + const sensitivities = this.analyzeParameterSensitivity(allResults); |
| 225 | + return Object.entries(sensitivities) |
| 226 | + .sort(([, a], [, b]) => b - a) |
| 227 | + .map( |
| 228 | + ([param, sensitivity]) => ` |
| 229 | + <tr> |
| 230 | + <td>${param}</td> |
| 231 | + <td>${sensitivity.toFixed(4)}</td> |
| 232 | + <td class="sensitivity-${sensitivity > 0.3 ? 'high' : sensitivity > 0.1 ? 'medium' : 'low'}"> |
| 233 | + ${sensitivity > 0.3 ? 'High' : sensitivity > 0.1 ? 'Medium' : 'Low'} |
| 234 | + </td> |
| 235 | + </tr> |
| 236 | + ` |
| 237 | + ) |
| 238 | + .join(''); |
| 239 | + })()} |
| 240 | + </tbody> |
| 241 | + </table> |
| 242 | + ` |
| 243 | + : '' |
| 244 | + } |
| 245 | +
|
| 246 | + <h2>Optimization Details</h2> |
| 247 | + <ul> |
| 248 | + <li>Method: ${method.toUpperCase()}</li> |
| 249 | + <li>Metric: ${metric.toUpperCase()}</li> |
| 250 | + <li>Total combinations tested: ${allResults.length}</li> |
| 251 | + <li>Best score: ${bestScore.toFixed(4)}</li> |
| 252 | + <li>Generated: ${new Date().toLocaleString()}</li> |
| 253 | + </ul> |
| 254 | +
|
| 255 | + <footer style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; color: #7f8c8d;"> |
| 256 | + <p>Generated by PineScript Optimizer</p> |
| 257 | + </footer> |
| 258 | +</body> |
| 259 | +</html>`; |
| 260 | + } |
| 261 | + |
| 262 | + /** |
| 263 | + * Generate JSON report |
| 264 | + */ |
| 265 | + generateJSONReport(results, options) { |
| 266 | + const report = { |
| 267 | + metadata: { |
| 268 | + generated: new Date().toISOString(), |
| 269 | + method: results.method, |
| 270 | + metric: options.metric, |
| 271 | + totalCombinations: results.totalCombinations, |
| 272 | + testedCombinations: results.testedCombinations, |
| 273 | + }, |
| 274 | + bestParameters: results.bestParams, |
| 275 | + bestScore: results.bestScore, |
| 276 | + topResults: results.allResults.slice(0, 10), |
| 277 | + parameterSensitivity: this.analyzeParameterSensitivity(results.allResults), |
| 278 | + }; |
| 279 | + |
| 280 | + return JSON.stringify(report, null, 2); |
| 281 | + } |
| 282 | + |
| 283 | + /** |
| 284 | + * Generate summary statistics |
| 285 | + */ |
| 286 | + generateSummaryStatistics(results) { |
| 287 | + const scores = results.allResults.map((r) => r.score); |
| 288 | + |
| 289 | + if (scores.length === 0) { |
| 290 | + return { |
| 291 | + count: 0, |
| 292 | + mean: 0, |
| 293 | + median: 0, |
| 294 | + stdDev: 0, |
| 295 | + min: 0, |
| 296 | + max: 0, |
| 297 | + }; |
| 298 | + } |
| 299 | + |
| 300 | + // Calculate statistics |
| 301 | + const mean = scores.reduce((a, b) => a + b, 0) / scores.length; |
| 302 | + const sorted = [...scores].sort((a, b) => a - b); |
| 303 | + const median = sorted[Math.floor(sorted.length / 2)]; |
| 304 | + const variance = |
| 305 | + scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / scores.length; |
| 306 | + const stdDev = Math.sqrt(variance); |
| 307 | + |
| 308 | + return { |
| 309 | + count: scores.length, |
| 310 | + mean: Number(mean.toFixed(4)), |
| 311 | + median: Number(median.toFixed(4)), |
| 312 | + stdDev: Number(stdDev.toFixed(4)), |
| 313 | + min: Number(Math.min(...scores).toFixed(4)), |
| 314 | + max: Number(Math.max(...scores).toFixed(4)), |
| 315 | + range: Number((Math.max(...scores) - Math.min(...scores)).toFixed(4)), |
| 316 | + }; |
| 317 | + } |
| 318 | + |
| 319 | + /** |
| 320 | + * Export results to CSV |
| 321 | + */ |
| 322 | + exportResultsToCSV(results, filename) { |
| 323 | + const { allResults } = results; |
| 324 | + |
| 325 | + if (allResults.length === 0) { |
| 326 | + return ''; |
| 327 | + } |
| 328 | + |
| 329 | + const paramNames = Object.keys(allResults[0].params); |
| 330 | + const headers = ['score', ...paramNames].join(','); |
| 331 | + |
| 332 | + const rows = allResults.map((result) => { |
| 333 | + const score = result.score.toFixed(6); |
| 334 | + const paramValues = paramNames.map((param) => result.params[param]); |
| 335 | + return [score, ...paramValues].join(','); |
| 336 | + }); |
| 337 | + |
| 338 | + return `${headers}\n${rows.join('\n')}`; |
| 339 | + } |
| 340 | + |
| 341 | + /** |
| 342 | + * Export results to JSON |
| 343 | + */ |
| 344 | + exportResultsToJSON(results, filename) { |
| 345 | + return JSON.stringify(results, null, 2); |
| 346 | + } |
| 347 | +} |
| 348 | + |
| 349 | +module.exports = AnalysisReporter; |
0 commit comments