/** * Conformal Prediction with Guaranteed Intervals * * INTERMEDIATE: Uncertainty quantification for trading * * Uses @neural-trader/predictor for: * - Distribution-free prediction intervals * - Coverage guarantees (e.g., 95% of true values fall within interval) * - Adaptive conformal inference for time series * - Non-parametric uncertainty estimation * * Unlike traditional ML, conformal prediction provides VALID intervals * with finite-sample guarantees, regardless of the underlying distribution. */ // Conformal prediction configuration const conformalConfig = { // Confidence level (1 - α) alpha: 0.05, // 95% coverage // Calibration set size calibrationSize: 500, // Prediction method method: 'ACI', // ACI (Adaptive Conformal Inference) or ICP (Inductive CP) // Adaptive parameters adaptive: { gamma: 0.005, // Learning rate for adaptivity targetCoverage: 0.95, // Target empirical coverage windowSize: 100 // Rolling window for coverage estimation } }; // Conformity score functions const ConformityScores = { // Absolute residual (symmetric) absolute: (pred, actual) => Math.abs(pred - actual), // Signed residual (asymmetric) signed: (pred, actual) => actual - pred, // Quantile-based (for asymmetric intervals) quantile: (pred, actual, q = 0.5) => { const residual = actual - pred; return residual >= 0 ? q * residual : (1 - q) * Math.abs(residual); }, // Normalized (for heteroscedastic data) normalized: (pred, actual, sigma) => Math.abs(pred - actual) / sigma }; // Conformal Predictor base class class ConformalPredictor { constructor(config) { this.config = config; this.calibrationScores = []; this.predictionHistory = []; this.coverageHistory = []; this.adaptiveAlpha = config.alpha; } // Calibrate using historical residuals calibrate(predictions, actuals) { if (predictions.length !== actuals.length) { throw new Error('Predictions and actuals must have same length'); } this.calibrationScores = []; for (let i = 0; i < predictions.length; i++) { const score = ConformityScores.absolute(predictions[i], actuals[i]); this.calibrationScores.push(score); } // Sort for quantile computation this.calibrationScores.sort((a, b) => a - b); console.log(`Calibrated with ${this.calibrationScores.length} samples`); console.log(`Score range: [${this.calibrationScores[0].toFixed(4)}, ${this.calibrationScores[this.calibrationScores.length - 1].toFixed(4)}]`); } // Get prediction interval predict(pointPrediction) { const alpha = this.adaptiveAlpha; const n = this.calibrationScores.length; // Compute quantile for (1 - alpha) coverage // Use (1 - alpha)(1 + 1/n) quantile for finite-sample validity const quantileIndex = Math.ceil((1 - alpha) * (n + 1)) - 1; const conformalQuantile = this.calibrationScores[Math.min(quantileIndex, n - 1)]; const interval = { prediction: pointPrediction, lower: pointPrediction - conformalQuantile, upper: pointPrediction + conformalQuantile, width: conformalQuantile * 2, alpha: alpha, coverage: 1 - alpha }; return interval; } // Update for adaptive conformal inference updateAdaptive(actual, interval) { // Check if actual was covered const covered = actual >= interval.lower && actual <= interval.upper; this.coverageHistory.push(covered ? 1 : 0); // Update empirical coverage (rolling window) const windowSize = this.config.adaptive.windowSize; const recentCoverage = this.coverageHistory.slice(-windowSize); const empiricalCoverage = recentCoverage.reduce((a, b) => a + b, 0) / recentCoverage.length; // Adapt alpha based on coverage error const targetCoverage = this.config.adaptive.targetCoverage; const gamma = this.config.adaptive.gamma; // If empirical coverage < target, decrease alpha (widen intervals) // If empirical coverage > target, increase alpha (tighten intervals) this.adaptiveAlpha = Math.max(0.001, Math.min(0.2, this.adaptiveAlpha + gamma * (empiricalCoverage - targetCoverage) )); // Add new conformity score to calibration set const newScore = ConformityScores.absolute(interval.prediction, actual); this.calibrationScores.push(newScore); this.calibrationScores.sort((a, b) => a - b); // Keep calibration set bounded if (this.calibrationScores.length > 2000) { this.calibrationScores.shift(); } return { covered, empiricalCoverage, adaptiveAlpha: this.adaptiveAlpha }; } } // Asymmetric Conformal Predictor (for trading where downside ≠ upside) class AsymmetricConformalPredictor extends ConformalPredictor { constructor(config) { super(config); this.lowerScores = []; this.upperScores = []; this.lowerAlpha = config.alpha / 2; this.upperAlpha = config.alpha / 2; } calibrate(predictions, actuals) { this.lowerScores = []; this.upperScores = []; for (let i = 0; i < predictions.length; i++) { const residual = actuals[i] - predictions[i]; if (residual < 0) { this.lowerScores.push(Math.abs(residual)); } else { this.upperScores.push(residual); } } this.lowerScores.sort((a, b) => a - b); this.upperScores.sort((a, b) => a - b); console.log(`Asymmetric calibration:`); console.log(` Lower: ${this.lowerScores.length} samples`); console.log(` Upper: ${this.upperScores.length} samples`); } predict(pointPrediction) { const nLower = this.lowerScores.length; const nUpper = this.upperScores.length; // Separate quantiles for lower and upper const lowerIdx = Math.ceil((1 - this.lowerAlpha * 2) * (nLower + 1)) - 1; const upperIdx = Math.ceil((1 - this.upperAlpha * 2) * (nUpper + 1)) - 1; const lowerQuantile = this.lowerScores[Math.min(lowerIdx, nLower - 1)] || 0; const upperQuantile = this.upperScores[Math.min(upperIdx, nUpper - 1)] || 0; return { prediction: pointPrediction, lower: pointPrediction - lowerQuantile, upper: pointPrediction + upperQuantile, lowerWidth: lowerQuantile, upperWidth: upperQuantile, asymmetryRatio: upperQuantile / (lowerQuantile || 1), alpha: this.config.alpha }; } } // Generate synthetic trading data with underlying model function generateTradingData(n, seed = 42) { const data = []; let price = 100; // Simple random seed let rng = seed; const random = () => { rng = (rng * 9301 + 49297) % 233280; return rng / 233280; }; for (let i = 0; i < n; i++) { // True return with regime switching and heteroscedasticity const regime = Math.sin(i / 50) > 0 ? 1 : 0.5; const volatility = 0.02 * regime; const drift = 0.0001; const trueReturn = drift + volatility * (random() + random() - 1); price = price * (1 + trueReturn); // Features for prediction const features = { momentum: i > 10 ? (price / data[i - 10]?.price - 1) || 0 : 0, volatility: volatility, regime }; // Model prediction (with some error) const predictedReturn = drift + 0.5 * features.momentum + (random() - 0.5) * 0.005; data.push({ index: i, price, trueReturn, predictedReturn, features }); } return data; } async function main() { console.log('═'.repeat(70)); console.log('CONFORMAL PREDICTION - Guaranteed Uncertainty Intervals'); console.log('═'.repeat(70)); console.log(); // 1. Generate data console.log('1. Generating Trading Data:'); console.log('─'.repeat(70)); const data = generateTradingData(1000); const calibrationData = data.slice(0, conformalConfig.calibrationSize); const testData = data.slice(conformalConfig.calibrationSize); console.log(` Total samples: ${data.length}`); console.log(` Calibration: ${calibrationData.length}`); console.log(` Test: ${testData.length}`); console.log(` Target coverage: ${(1 - conformalConfig.alpha) * 100}%`); console.log(); // 2. Standard Conformal Predictor console.log('2. Standard (Symmetric) Conformal Prediction:'); console.log('─'.repeat(70)); const standardCP = new ConformalPredictor(conformalConfig); // Calibrate const calPredictions = calibrationData.map(d => d.predictedReturn); const calActuals = calibrationData.map(d => d.trueReturn); standardCP.calibrate(calPredictions, calActuals); // Test let standardCovered = 0; let standardWidths = []; for (const sample of testData) { const interval = standardCP.predict(sample.predictedReturn); const covered = sample.trueReturn >= interval.lower && sample.trueReturn <= interval.upper; if (covered) standardCovered++; standardWidths.push(interval.width); } const standardCoverage = standardCovered / testData.length; const avgWidth = standardWidths.reduce((a, b) => a + b, 0) / standardWidths.length; console.log(` Empirical Coverage: ${(standardCoverage * 100).toFixed(2)}%`); console.log(` Target Coverage: ${(1 - conformalConfig.alpha) * 100}%`); console.log(` Average Width: ${(avgWidth * 10000).toFixed(2)} bps`); console.log(` Coverage Valid: ${standardCoverage >= (1 - conformalConfig.alpha) - 0.02 ? '✓ YES' : '✗ NO'}`); console.log(); // 3. Adaptive Conformal Inference console.log('3. Adaptive Conformal Inference (ACI):'); console.log('─'.repeat(70)); const adaptiveCP = new ConformalPredictor({ ...conformalConfig, method: 'ACI' }); adaptiveCP.calibrate(calPredictions, calActuals); let adaptiveCovered = 0; let adaptiveWidths = []; let alphaHistory = []; for (const sample of testData) { const interval = adaptiveCP.predict(sample.predictedReturn); const update = adaptiveCP.updateAdaptive(sample.trueReturn, interval); if (update.covered) adaptiveCovered++; adaptiveWidths.push(interval.width); alphaHistory.push(adaptiveCP.adaptiveAlpha); } const adaptiveCoverage = adaptiveCovered / testData.length; const adaptiveAvgWidth = adaptiveWidths.reduce((a, b) => a + b, 0) / adaptiveWidths.length; const finalAlpha = alphaHistory[alphaHistory.length - 1]; console.log(` Empirical Coverage: ${(adaptiveCoverage * 100).toFixed(2)}%`); console.log(` Average Width: ${(adaptiveAvgWidth * 10000).toFixed(2)} bps`); console.log(` Initial Alpha: ${(conformalConfig.alpha * 100).toFixed(2)}%`); console.log(` Final Alpha: ${(finalAlpha * 100).toFixed(2)}%`); console.log(` Width vs Standard: ${((adaptiveAvgWidth / avgWidth - 1) * 100).toFixed(1)}%`); console.log(); // 4. Asymmetric Conformal Prediction console.log('4. Asymmetric Conformal Prediction:'); console.log('─'.repeat(70)); const asymmetricCP = new AsymmetricConformalPredictor(conformalConfig); asymmetricCP.calibrate(calPredictions, calActuals); let asymmetricCovered = 0; let lowerWidths = []; let upperWidths = []; for (const sample of testData) { const interval = asymmetricCP.predict(sample.predictedReturn); const covered = sample.trueReturn >= interval.lower && sample.trueReturn <= interval.upper; if (covered) asymmetricCovered++; lowerWidths.push(interval.lowerWidth); upperWidths.push(interval.upperWidth); } const asymmetricCoverage = asymmetricCovered / testData.length; const avgLower = lowerWidths.reduce((a, b) => a + b, 0) / lowerWidths.length; const avgUpper = upperWidths.reduce((a, b) => a + b, 0) / upperWidths.length; console.log(` Empirical Coverage: ${(asymmetricCoverage * 100).toFixed(2)}%`); console.log(` Avg Lower Width: ${(avgLower * 10000).toFixed(2)} bps`); console.log(` Avg Upper Width: ${(avgUpper * 10000).toFixed(2)} bps`); console.log(` Asymmetry Ratio: ${(avgUpper / avgLower).toFixed(2)}x`); console.log(); // 5. Example predictions console.log('5. Example Predictions (Last 5 samples):'); console.log('─'.repeat(70)); console.log(' Predicted │ Lower │ Upper │ Actual │ Covered │ Width'); console.log('─'.repeat(70)); const lastSamples = testData.slice(-5); for (const sample of lastSamples) { const interval = standardCP.predict(sample.predictedReturn); const covered = sample.trueReturn >= interval.lower && sample.trueReturn <= interval.upper; const predBps = (sample.predictedReturn * 10000).toFixed(2); const lowerBps = (interval.lower * 10000).toFixed(2); const upperBps = (interval.upper * 10000).toFixed(2); const actualBps = (sample.trueReturn * 10000).toFixed(2); const widthBps = (interval.width * 10000).toFixed(2); console.log(` ${predBps.padStart(9)} │ ${lowerBps.padStart(8)} │ ${upperBps.padStart(8)} │ ${actualBps.padStart(8)} │ ${covered ? ' ✓ ' : ' ✗ '} │ ${widthBps.padStart(6)}`); } console.log(); // 6. Trading application console.log('6. Trading Application - Risk Management:'); console.log('─'.repeat(70)); // Use conformal intervals for position sizing const samplePrediction = testData[testData.length - 1].predictedReturn; const conformalInterval = standardCP.predict(samplePrediction); const expectedReturn = samplePrediction; const worstCase = conformalInterval.lower; const bestCase = conformalInterval.upper; const uncertainty = conformalInterval.width; console.log(` Point Prediction: ${(expectedReturn * 10000).toFixed(2)} bps`); console.log(` 95% Worst Case: ${(worstCase * 10000).toFixed(2)} bps`); console.log(` 95% Best Case: ${(bestCase * 10000).toFixed(2)} bps`); console.log(` Uncertainty: ${(uncertainty * 10000).toFixed(2)} bps`); console.log(); // Position sizing based on uncertainty const riskBudget = 0.02; // 2% daily risk budget const maxLoss = Math.abs(worstCase); const suggestedPosition = riskBudget / maxLoss; console.log(` Risk Budget: ${(riskBudget * 100).toFixed(1)}%`); console.log(` Max Position: ${(suggestedPosition * 100).toFixed(1)}% of portfolio`); console.log(` Rationale: Position sized so 95% worst case = ${(riskBudget * 100).toFixed(1)}% loss`); console.log(); // 7. Coverage guarantee visualization console.log('7. Finite-Sample Coverage Guarantee:'); console.log('─'.repeat(70)); console.log(' Conformal prediction provides VALID coverage guarantees:'); console.log(); console.log(` P(Y ∈ Ĉ(X)) ≥ 1 - α = ${((1 - conformalConfig.alpha) * 100).toFixed(0)}%`); console.log(); console.log(' This holds for ANY data distribution, without assumptions!'); console.log(' (Unlike Gaussian intervals which require normality)'); console.log(); console.log('═'.repeat(70)); console.log('Conformal prediction analysis completed'); console.log('═'.repeat(70)); } main().catch(console.error);